Files
kra-oa/internal/integration/sms/sms.go
T
2026-09-29 18:15:35 +08:00

173 lines
6.1 KiB
Go

// Package sms implements cloud SMS delivery using saved integration settings.
package sms
import (
"context"
"encoding/json"
"errors"
"regexp"
"strings"
"time"
openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client"
aliyun "github.com/alibabacloud-go/dysmsapi-20170525/v4/client"
util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea"
kratoserrors "github.com/go-kratos/kratos/v3/errors"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
tcerrors "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
tencent "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111"
biz "kra/internal/biz/integration"
"kra/internal/config"
"kra/internal/integration/runtimeconfig"
)
type aliyunClient interface {
SendSmsWithOptions(*aliyun.SendSmsRequest, *util.RuntimeOptions) (*aliyun.SendSmsResponse, error)
}
type tencentClient interface {
SendSmsWithContext(context.Context, *tencent.SendSmsRequest) (*tencent.SendSmsResponse, error)
}
type Sender struct {
store *runtimeconfig.Store
newAliyun func(*openapi.Config) (aliyunClient, error)
newTencent func(string, string, string) (tencentClient, error)
}
func NewSender(store *runtimeconfig.Store) biz.SMSSender {
return &Sender{store: store,
newAliyun: func(c *openapi.Config) (aliyunClient, error) { return aliyun.NewClient(c) },
newTencent: func(id, secret, region string) (tencentClient, error) {
p := profile.NewClientProfile()
p.HttpProfile.Endpoint = "sms.tencentcloudapi.com"
p.HttpProfile.ReqTimeout = 10
p.DisableRegionBreaker = true
p.NetworkFailureMaxRetries = 0
p.RateLimitExceededMaxRetries = 0
p.UnsafeRetryOnConnectionFailure = false
return tencent.NewClient(common.NewCredential(id, secret), region, p)
},
}
}
func (s *Sender) Send(ctx context.Context, m *biz.SMSMessage) (*biz.SMSResult, error) {
if err := biz.ValidateSMSMessage(m); err != nil {
return nil, err
}
if err := ctx.Err(); err != nil {
return nil, err
}
c, ok := s.store.Get(biz.IntegrationKindSMS, m.Provider)
if !ok || !c.Enabled {
return nil, biz.ErrSMSDisabled
}
values := map[string]any{}
if json.Unmarshal(c.Values, &values) != nil {
return nil, biz.ErrSMSInvalid
}
if err := biz.ValidateIntegrationConfig(biz.IntegrationKindSMS, m.Provider, values); err != nil {
return nil, err
}
for key, value := range values {
if text, ok := value.(string); ok && biz.IsIntegrationSecretKey(key) && config.IsMaskedSecret(text) {
return nil, biz.ErrSMSDisabled
}
}
get := func(key string) string { v, _ := values[key].(string); return v }
if m.Provider == "aliyun" {
return s.sendAliyun(ctx, m, get)
}
return s.sendTencent(ctx, m, get)
}
func (s *Sender) sendAliyun(ctx context.Context, m *biz.SMSMessage, get func(string) string) (*biz.SMSResult, error) {
// This SDK has no context-aware SendSms; bound both connection and read time.
timeout := 10000
if deadline, ok := ctx.Deadline(); ok {
remaining := int(time.Until(deadline).Milliseconds())
if remaining <= 0 {
return nil, context.DeadlineExceeded
}
if remaining < timeout {
timeout = remaining
}
}
client, err := s.newAliyun(&openapi.Config{
AccessKeyId: tea.String(get("access_key_id")), AccessKeySecret: tea.String(get("access_key_secret")),
Endpoint: tea.String("dysmsapi.aliyuncs.com"), Protocol: tea.String("https"),
ConnectTimeout: tea.Int(timeout), ReadTimeout: tea.Int(timeout),
})
if err != nil {
return nil, sdkError(err)
}
params := m.NamedParams
if params == nil {
params = map[string]string{}
}
raw, _ := json.Marshal(params)
response, err := client.SendSmsWithOptions(&aliyun.SendSmsRequest{
PhoneNumbers: tea.String(strings.TrimPrefix(m.Phone, "+86")), SignName: tea.String(get("sign_name")),
TemplateCode: tea.String(get("template_id")), TemplateParam: tea.String(string(raw)),
}, &util.RuntimeOptions{Autoretry: tea.Bool(false), MaxAttempts: tea.Int(1), ConnectTimeout: tea.Int(timeout), ReadTimeout: tea.Int(timeout)})
if err != nil {
return nil, sdkError(err)
}
if response == nil || response.Body == nil {
return nil, biz.ErrSMSFailed
}
if tea.StringValue(response.Body.Code) != "OK" {
return nil, providerError(tea.StringValue(response.Body.Code))
}
return &biz.SMSResult{RequestID: tea.StringValue(response.Body.RequestId), MessageID: tea.StringValue(response.Body.BizId)}, nil
}
func (s *Sender) sendTencent(ctx context.Context, m *biz.SMSMessage, get func(string) string) (*biz.SMSResult, error) {
client, err := s.newTencent(get("secret_id"), get("secret_key"), get("region"))
if err != nil {
return nil, sdkError(err)
}
request := tencent.NewSendSmsRequest()
request.PhoneNumberSet = common.StringPtrs([]string{m.Phone})
request.SmsSdkAppId = common.StringPtr(get("sdk_app_id"))
request.SignName = common.StringPtr(get("sign_name"))
request.TemplateId = common.StringPtr(get("template_id"))
request.TemplateParamSet = common.StringPtrs(m.Params)
response, err := client.SendSmsWithContext(ctx, request)
if err != nil {
return nil, sdkError(err)
}
if response == nil || response.Response == nil || len(response.Response.SendStatusSet) != 1 || response.Response.SendStatusSet[0] == nil {
return nil, biz.ErrSMSFailed
}
status := response.Response.SendStatusSet[0]
if tea.StringValue(status.Code) != "Ok" {
return nil, providerError(tea.StringValue(status.Code))
}
return &biz.SMSResult{RequestID: tea.StringValue(response.Response.RequestId), MessageID: tea.StringValue(status.SerialNo)}, nil
}
var providerCodePattern = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9._-]{0,127}$`)
func providerError(code string) error {
if !providerCodePattern.MatchString(code) {
return biz.ErrSMSFailed
}
return kratoserrors.BadRequest("SMS_SEND_FAILED", "短信提交失败,服务商错误码:"+code)
}
// Return only provider codes: raw SDK errors can contain signed requests and PII.
func sdkError(err error) error {
var tc *tcerrors.TencentCloudSDKError
if errors.As(err, &tc) {
return providerError(tc.Code)
}
var ali *tea.SDKError
if errors.As(err, &ali) {
return providerError(tea.StringValue(ali.Code))
}
return biz.ErrSMSFailed
}