增加短信渠道

This commit is contained in:
Yvan
2026-09-29 18:15:35 +08:00
parent a16b8f8e89
commit 89cc7761b5
38 changed files with 1469 additions and 39 deletions
@@ -14,6 +14,7 @@ import (
)
const (
IntegrationKindSMS = "sms"
IntegrationKindPayment = "payment"
IntegrationKindMQ = "mq"
IntegrationKindWebSocket = "websocket"
@@ -255,6 +256,8 @@ func ValidateIntegrationConfig(kind, provider string, values map[string]any) err
kind = normalizeIntegrationPart(kind)
provider = normalizeIntegrationPart(provider)
switch kind {
case IntegrationKindSMS:
return validateSMSConfig(provider, values)
case IntegrationKindPayment:
return validatePaymentIntegrationConfig(provider, values)
case IntegrationKindMQ, IntegrationKindWebSocket:
@@ -120,6 +120,22 @@ func genericPaymentDefinition(provider, name, description string) IntegrationCon
}
var integrationDefinitions = map[string][]IntegrationConfigDefinition{
IntegrationKindSMS: {
{Kind: IntegrationKindSMS, Provider: "aliyun", Name: "阿里云短信", Description: "中国大陆短信,使用已审核的签名和模板。", Defaults: map[string]any{"access_key_id": "", "access_key_secret": "", "sign_name": "", "template_id": ""}, Fields: []IntegrationConfigField{
integrationField("access_key_id", "AccessKey ID", true, false, "text"),
integrationField("access_key_secret", "AccessKey Secret", true, true, "password"),
integrationField("sign_name", "短信签名", true, false, "text"),
integrationField("template_id", "模板 CODE", true, false, "text"),
}},
{Kind: IntegrationKindSMS, Provider: "tencent", Name: "腾讯云短信", Description: "使用短信应用、已审核的签名和模板,模板参数按顺序填写。", Defaults: map[string]any{"secret_id": "", "secret_key": "", "sdk_app_id": "", "region": "ap-guangzhou", "sign_name": "", "template_id": ""}, Fields: []IntegrationConfigField{
integrationField("secret_id", "Secret ID", true, true, "password"),
integrationField("secret_key", "Secret Key", true, true, "password"),
integrationField("sdk_app_id", "短信应用 SDK App ID", true, false, "text"),
integrationField("region", "地域", true, false, "text"),
integrationField("sign_name", "短信签名", true, false, "text"),
integrationField("template_id", "模板 ID", true, false, "text"),
}},
},
IntegrationKindMQ: {
{
Kind: IntegrationKindMQ, Provider: "emqx", Name: "EMQX", Description: "EMQX MQTT 消息服务",
+1 -1
View File
@@ -3,4 +3,4 @@ package integration
import "github.com/google/wire"
// ProviderSet wires communication and integration-configuration usecases.
var ProviderSet = wire.NewSet(NewIntegrationConfigUsecase)
var ProviderSet = wire.NewSet(NewIntegrationConfigUsecase, NewSMSUsecase)
+86
View File
@@ -0,0 +1,86 @@
package integration
import (
"context"
"regexp"
"strings"
"github.com/go-kratos/kratos/v3/errors"
)
var (
ErrSMSInvalid = errors.BadRequest("SMS_INVALID", "短信参数无效")
ErrSMSDisabled = errors.BadRequest("SMS_DISABLED", "短信渠道未配置或未启用")
ErrSMSFailed = errors.BadRequest("SMS_SEND_FAILED", "短信提交失败,请检查服务商配置和账户状态")
)
// SMSMessage carries one recipient and provider-specific template parameters.
type SMSMessage struct {
Provider string
Phone string
NamedParams map[string]string
Params []string
}
type SMSResult struct {
RequestID string
MessageID string
}
type SMSSender interface {
Send(context.Context, *SMSMessage) (*SMSResult, error)
}
type SMSUsecase struct{ sender SMSSender }
func NewSMSUsecase(sender SMSSender) *SMSUsecase { return &SMSUsecase{sender: sender} }
var smsPhonePattern = regexp.MustCompile(`^\+[1-9][0-9]{6,14}$`)
func ValidateSMSMessage(m *SMSMessage) error {
if m == nil || (m.Provider != "aliyun" && m.Provider != "tencent") || !smsPhonePattern.MatchString(m.Phone) {
return ErrSMSInvalid
}
// The Aliyun adapter uses the mainland China SendSms API.
if m.Provider == "aliyun" && (!strings.HasPrefix(m.Phone, "+861") || len(m.Phone) != 14 || len(m.Params) != 0) {
return errors.BadRequest("SMS_INVALID", "阿里云短信请使用 +86 开头的中国大陆手机号及对象格式模板参数")
}
if m.Provider == "tencent" && len(m.NamedParams) != 0 {
return errors.BadRequest("SMS_INVALID", "腾讯云短信模板参数必须是按模板顺序排列的字符串数组")
}
if len(m.NamedParams) > 20 || len(m.Params) > 20 {
return ErrSMSInvalid
}
for key, value := range m.NamedParams {
if strings.TrimSpace(key) == "" || len(key) > 64 || len(value) > 500 {
return ErrSMSInvalid
}
}
for _, value := range m.Params {
if len(value) > 500 {
return ErrSMSInvalid
}
}
return nil
}
func (uc *SMSUsecase) Send(ctx context.Context, m *SMSMessage) (*SMSResult, error) {
if err := ValidateSMSMessage(m); err != nil {
return nil, err
}
return uc.sender.Send(ctx, m)
}
func validateSMSConfig(provider string, values map[string]any) error {
definition, ok := IntegrationDefinition(IntegrationKindSMS, provider)
if !ok {
return errors.BadRequest("SMS_PROVIDER_INVALID", "不支持的短信服务商")
}
for _, field := range definition.Fields {
value, ok := values[field.Key].(string)
if !ok || len(value) > 512 || (field.Required && strings.TrimSpace(value) == "") {
return errors.BadRequest("SMS_CONFIG_INVALID", "请填写有效的"+field.Label)
}
}
return nil
}