sync: 同步短信服务 v1.0.4 并脱敏

This commit is contained in:
MiFaZhan
2026-10-29 14:17:14 +08:00
parent 6c26f7a0e1
commit e6997f51ed
22 changed files with 450 additions and 168 deletions
+69 -4
View File
@@ -2,6 +2,7 @@ package sms
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
@@ -14,13 +15,18 @@ const (
)
var (
ErrProviderNotFound = errors.New("未选择有效的短信发送供应商")
ErrInvalidRequest = errors.New("短信请求参数不正确")
ErrProviderNotFound = errors.New("未选择有效的短信发送供应商")
ErrInvalidRequest = errors.New("短信请求参数不正确")
ErrSceneNotConfigured = errors.New("短信场景未配置或已停用")
)
// Config contains the provider credentials and template configuration.
// serviceType: 1=Aliyun, 2=Tencent Cloud.
//
// Scene 为可选业务场景码(如 bind_phone)。当 Scene 非空时,短信服务会按该场景从数据库
// 配置表读取模板ID/签名等配置并回填到本次请求,调用方无需关心具体模板。
type Config struct {
Scene string `json:"scene" yaml:"scene"`
ServiceType string `json:"serviceType" yaml:"serviceType"`
RegionID string `json:"regionId" yaml:"regionId"`
AccessKeyID string `json:"accessKeyId" yaml:"accessKeyId"`
@@ -49,6 +55,7 @@ type Registry struct {
mu sync.RWMutex
providers map[string]Provider
defaultConfig Config
scenes SceneProvider
}
// NewRegistry 创建一个无默认配置的注册表。
@@ -58,8 +65,9 @@ func NewRegistry() *Registry {
// NewRegistryWithDefault 创建一个带默认配置的注册表。调用方请求中未显式提供的字段
// 将回退到 defaultConfig(例如供应商凭证与签名由短信服务自身环境变量注入)。
func NewRegistryWithDefault(dc Config) *Registry {
return &Registry{providers: make(map[string]Provider), defaultConfig: dc}
// scenes 为可选的场景配置提供方(按 scene 从数据库读取模板配置),可为 nil。
func NewRegistryWithDefault(dc Config, scenes SceneProvider) *Registry {
return &Registry{providers: make(map[string]Provider), defaultConfig: dc, scenes: scenes}
}
func (r *Registry) Register(serviceType string, provider Provider) {
@@ -69,6 +77,19 @@ func (r *Registry) Register(serviceType string, provider Provider) {
}
func (r *Registry) Send(ctx context.Context, config Config, message Message) (Result, error) {
// 按业务场景加载模板配置(来自数据库),实现“改配置不改代码、不重启服务”。
if scene := strings.TrimSpace(config.Scene); scene != "" {
sceneCfg, err := r.loadSceneConfig(ctx, scene)
if err != nil {
if errors.Is(err, ErrSceneNotConfigured) {
return Result{Code: 400, Message: err.Error()}, err
}
return Result{Code: 500, Message: "查询短信场景配置失败: " + err.Error()}, err
}
config = mergeSceneConfig(sceneCfg, config)
message.MsgContent = renderSceneMsgContent(sceneCfg.MsgContentTemplate, message.MsgContent)
}
// 回退默认值:请求中未提供的字段使用短信服务自身的默认配置。
config = mergeConfig(r.defaultConfig, config)
if err := Validate(config, message); err != nil {
@@ -84,6 +105,50 @@ func (r *Registry) Send(ctx context.Context, config Config, message Message) (Re
return provider.Send(ctx, config, message)
}
// loadSceneConfig 按场景码加载模板配置,并校验场景已启用。
func (r *Registry) loadSceneConfig(ctx context.Context, scene string) (SceneConfig, error) {
if r.scenes == nil {
return SceneConfig{}, fmt.Errorf("%w: %s", ErrSceneNotConfigured, scene)
}
cfg, err := r.scenes.Lookup(ctx, scene)
if err != nil {
return SceneConfig{}, err
}
if cfg == nil || cfg.Status != sceneStatusEnabled {
return SceneConfig{}, fmt.Errorf("%w: %s", ErrSceneNotConfigured, scene)
}
return *cfg, nil
}
// mergeSceneConfig 用场景配置填充请求中未显式提供的字段,调用方显式值优先。
func mergeSceneConfig(scene SceneConfig, config Config) Config {
if strings.TrimSpace(config.ServiceType) == "" {
config.ServiceType = scene.ServiceType
}
if strings.TrimSpace(config.SignName) == "" {
config.SignName = scene.SignName
}
if strings.TrimSpace(config.TemplateID) == "" {
config.TemplateID = scene.TemplateID
}
return config
}
// renderSceneMsgContent 按场景内容模板渲染短信参数;模板为空时直接使用调用方内容。
// 支持 {content}(原样)与 {contentJson}(JSON 转义)两种占位符。
func renderSceneMsgContent(template string, content string) string {
template = strings.TrimSpace(template)
if template == "" {
return content
}
contentJSON, _ := json.Marshal(content)
replacer := strings.NewReplacer(
"{content}", content,
"{contentJson}", string(contentJSON),
)
return replacer.Replace(template)
}
func Validate(config Config, message Message) error {
missing := make([]string, 0, 6)
if strings.TrimSpace(config.ServiceType) == "" {