Files
go-common/sms/sms.go

230 lines
5.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}