230 lines
5.6 KiB
Go
230 lines
5.6 KiB
Go
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
|
||
}
|
||
|