重构项目的实现,优化使用方法与使用逻辑

This commit is contained in:
2026-06-25 00:03:59 +08:00
parent a6e8101e09
commit 6072ec57e8
49 changed files with 1663 additions and 12534 deletions

View File

@@ -1,6 +1,7 @@
package sms
import (
"context"
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
@@ -11,29 +12,40 @@ import (
"net/url"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
"git.toowon.com/jimmy/go-common/config"
"git.toowon.com/jimmy/go-common/logger"
)
// SendResponse 发送短信响应
type SendResponse struct {
// RequestID 请求ID
RequestID string `json:"RequestId"`
// Code 响应码
Code string `json:"Code"`
// Message 响应消息
Message string `json:"Message"`
// BizID 业务ID
BizID string `json:"BizId"`
Code string `json:"Code"`
Message string `json:"Message"`
BizID string `json:"BizId"`
}
// SMS 短信发送器
type SMS struct {
config *config.SMSConfig
async bool
queue chan smsTask
workers int
wg sync.WaitGroup
closed bool
mu sync.Mutex
dropped atomic.Uint64
}
type smsTask struct {
phones []string
templateParam interface{}
templateCode string
requestID string
}
// NewSMS 创建短信发送器
@@ -41,81 +53,87 @@ func NewSMS(cfg *config.Config) *SMS {
if cfg == nil || cfg.SMS == nil {
return &SMS{config: nil}
}
return &SMS{config: cfg.SMS}
s := &SMS{
config: cfg.SMS,
async: cfg.SMS.IsAsync(),
workers: cfg.SMS.Workers,
}
if s.workers <= 0 {
s.workers = 2
}
queueSize := cfg.SMS.QueueSize
if queueSize <= 0 {
queueSize = 1000
}
if s.async {
s.queue = make(chan smsTask, queueSize)
for i := 0; i < s.workers; i++ {
s.wg.Add(1)
go s.worker()
}
}
return s
}
func (s *SMS) worker() {
defer s.wg.Done()
for task := range s.queue {
if _, err := s.SendSMS(task.phones, task.templateParam, task.templateCode); err != nil {
logger.FromContext(context.Background()).Error("async sms send failed", map[string]any{
"error": err.Error(),
"request_id": task.requestID,
"phones": task.phones,
})
}
}
}
// getSMSConfig 获取短信配置(内部方法)
func (s *SMS) getSMSConfig() (*config.SMSConfig, error) {
if s.config == nil {
return nil, fmt.Errorf("SMS config is nil")
}
if s.config.AccessKeyID == "" {
return nil, fmt.Errorf("AccessKeyID is required")
}
if s.config.AccessKeySecret == "" {
return nil, fmt.Errorf("AccessKeySecret is required")
}
if s.config.SignName == "" {
return nil, fmt.Errorf("SignName is required")
}
// 设置默认值
if s.config.Region == "" {
s.config.Region = "cn-hangzhou"
}
if s.config.Timeout == 0 {
s.config.Timeout = 10
s.config.Timeout = 5
}
return s.config, nil
}
// SendSMS 发送短信
// phoneNumbers: 手机号列表
// templateParam: 模板参数map或JSON字符串
// templateCode: 模板代码(可选,如果为空使用配置中的模板代码)
// SendSMS 同步发送短信
func (s *SMS) SendSMS(phoneNumbers []string, templateParam interface{}, templateCode ...string) (*SendResponse, error) {
cfg, err := s.getSMSConfig()
if err != nil {
return nil, err
}
if len(phoneNumbers) == 0 {
return nil, fmt.Errorf("phone numbers are required")
}
// 使用配置中的模板代码(如果请求中未指定)
templateCodeValue := ""
templateCodeValue := cfg.TemplateCode
if len(templateCode) > 0 && templateCode[0] != "" {
templateCodeValue = templateCode[0]
} else {
templateCodeValue = cfg.TemplateCode
}
if templateCodeValue == "" {
return nil, fmt.Errorf("template code is required")
}
signName := cfg.SignName
// 处理模板参数
var templateParamJSON string
if templateParam != nil {
switch v := templateParam.(type) {
case string:
// 直接使用字符串必须是有效的JSON
templateParamJSON = v
case map[string]string:
// 转换为JSON字符串
paramBytes, err := json.Marshal(v)
if err != nil {
return nil, fmt.Errorf("failed to marshal template param: %w", err)
}
templateParamJSON = string(paramBytes)
default:
// 尝试JSON序列化
paramBytes, err := json.Marshal(v)
if err != nil {
return nil, fmt.Errorf("failed to marshal template param: %w", err)
@@ -126,104 +144,120 @@ func (s *SMS) SendSMS(phoneNumbers []string, templateParam interface{}, template
templateParamJSON = "{}"
}
// 构建请求参数
params := make(map[string]string)
params["Action"] = "SendSms"
params["Version"] = "2017-05-25"
params["RegionId"] = cfg.Region
params["AccessKeyId"] = cfg.AccessKeyID
params["Format"] = "JSON"
params["SignatureMethod"] = "HMAC-SHA1"
params["SignatureVersion"] = "1.0"
params["SignatureNonce"] = fmt.Sprint(time.Now().UnixNano())
params["Timestamp"] = time.Now().UTC().Format("2006-01-02T15:04:05Z")
params["PhoneNumbers"] = strings.Join(phoneNumbers, ",")
params["SignName"] = signName
params["TemplateCode"] = templateCodeValue
params["TemplateParam"] = templateParamJSON
params := map[string]string{
"Action": "SendSms",
"Version": "2017-05-25",
"RegionId": cfg.Region,
"AccessKeyId": cfg.AccessKeyID,
"Format": "JSON",
"SignatureMethod": "HMAC-SHA1",
"SignatureVersion": "1.0",
"SignatureNonce": fmt.Sprint(time.Now().UnixNano()),
"Timestamp": time.Now().UTC().Format("2006-01-02T15:04:05Z"),
"PhoneNumbers": strings.Join(phoneNumbers, ","),
"SignName": cfg.SignName,
"TemplateCode": templateCodeValue,
"TemplateParam": templateParamJSON,
}
params["Signature"] = s.calculateSignature(params, "POST", cfg.AccessKeySecret)
// 计算签名
signature := s.calculateSignature(params, "POST", cfg.AccessKeySecret)
params["Signature"] = signature
// 构建请求URL
endpoint := cfg.Endpoint
if endpoint == "" {
endpoint = "https://dysmsapi.aliyuncs.com"
}
// 发送HTTP请求
formData := url.Values{}
for k, v := range params {
formData.Set(k, v)
}
httpReq, err := http.NewRequest("POST", endpoint, strings.NewReader(formData.Encode()))
httpReq, err := http.NewRequest(http.MethodPost, endpoint, strings.NewReader(formData.Encode()))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
httpReq.Header.Set("Accept", "application/json")
client := &http.Client{
Timeout: time.Duration(cfg.Timeout) * time.Second,
}
client := &http.Client{Timeout: time.Duration(cfg.Timeout) * time.Second}
resp, err := client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("failed to send request: %w", err)
}
defer resp.Body.Close()
// 读取响应
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
// 解析响应
var sendResp SendResponse
if err := json.Unmarshal(body, &sendResp); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
// 检查响应码
if sendResp.Code != "OK" {
return &sendResp, fmt.Errorf("SMS send failed: Code=%s, Message=%s", sendResp.Code, sendResp.Message)
}
return &sendResp, nil
}
// calculateSignature 计算签名
// SendSMSAsync 异步发送短信
func (s *SMS) SendSMSAsync(ctx context.Context, phoneNumbers []string, templateParam interface{}, templateCode ...string) {
code := ""
if len(templateCode) > 0 {
code = templateCode[0]
}
task := smsTask{
phones: append([]string(nil), phoneNumbers...),
templateParam: templateParam,
templateCode: code,
requestID: logger.RequestIDFromContext(ctx),
}
if !s.async {
_, _ = s.SendSMS(task.phones, task.templateParam, task.templateCode)
return
}
select {
case s.queue <- task:
default:
s.dropped.Add(1)
logger.FromContext(ctx).Error("sms queue full, task dropped", map[string]any{
"phones": phoneNumbers,
})
}
}
// Close 关闭异步 worker
func (s *SMS) Close() error {
if !s.async {
return nil
}
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return nil
}
s.closed = true
s.mu.Unlock()
close(s.queue)
s.wg.Wait()
return nil
}
func (s *SMS) calculateSignature(params map[string]string, method, accessKeySecret string) string {
// 对参数进行排序
keys := make([]string, 0, len(params))
for k := range params {
keys = append(keys, k)
}
sort.Strings(keys)
// 构建查询字符串
var queryParts []string
for _, k := range keys {
v := params[k]
// URL编码
encodedKey := url.QueryEscape(k)
encodedValue := url.QueryEscape(v)
queryParts = append(queryParts, encodedKey+"="+encodedValue)
queryParts = append(queryParts, url.QueryEscape(k)+"="+url.QueryEscape(params[k]))
}
queryString := strings.Join(queryParts, "&")
// 构建待签名字符串
stringToSign := method + "&" + url.QueryEscape("/") + "&" + url.QueryEscape(queryString)
// 计算HMAC-SHA1签名
mac := hmac.New(sha1.New, []byte(accessKeySecret+"&"))
mac.Write([]byte(stringToSign))
signature := base64.StdEncoding.EncodeToString(mac.Sum(nil))
return signature
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
}