package sms import ( "crypto/hmac" "crypto/sha1" "encoding/base64" "encoding/json" "fmt" "io" "net/http" "net/url" "sort" "strings" "time" "git.toowon.com/jimmy/go-common/config" ) // 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"` } // SMS 短信发送器 type SMS struct { config *config.SMSConfig } // NewSMS 创建短信发送器 func NewSMS(cfg *config.Config) *SMS { if cfg == nil || cfg.SMS == nil { return &SMS{config: nil} } return &SMS{config: cfg.SMS} } // 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 } return s.config, nil } // SendSMS 发送短信 // phoneNumbers: 手机号列表 // templateParam: 模板参数(map或JSON字符串) // templateCode: 模板代码(可选,如果为空使用配置中的模板代码) 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 := "" 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) } templateParamJSON = string(paramBytes) } } else { 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 // 计算签名 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())) 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, } 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 计算签名 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) } 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 }