diff --git a/tc_sms/README.md b/tc_sms/README.md new file mode 100644 index 0000000..4502777 --- /dev/null +++ b/tc_sms/README.md @@ -0,0 +1,104 @@ +# tc_sms + +> 简单易用的 腾讯云SMS短信发送 SDK + +### 基础示例 + +```ini +# .env +TENCENTCLOUD_SECRET_ID=AKIDxxxxxx +TENCENTCLOUD_SECRET_KEY=APxxxxxx +TC_SDK_APP_ID=1xxxxxx +``` + +```go +// sms_job.go +import ( + // 自动加载.env环境变量 + _ "github.com/joho/godotenv/autoload" + // 引入tc_sms + "gitee.com/we-mid/go/tc_sms" +) + +var ( + // 腾讯云-短信-国内短信-正文模板管理 + // https://console.cloud.tencent.com/smsv2/csms-template + templateId = "2197493" // 运营数据通知 + template = `{1} 近期运营数据: +*{2} PV:[{3}] UV:[{4}] 留存:[{5}] +*{6} PV:[{7}] UV:[{8}] 留存:[{9}] +*{10} PV:[{11}] UV:[{12}] 留存:[{13}] +*{14} PV:[{15}] UV:[{16}] 留存:[{17}]` + feeLimit = 3 // 单条消息发送fee上限设定 + sigName = "XX应用" // 短信签名 + phones = []string{"+8613xxxx", "+8618xxxx", "+8613xxxx"} // 接收手机号码 +) + +func smsJob() { + // 组装参数 + var params []string + // ... + // 发送前预览完整文本,自动计算fee + result, n, fee := tc_sms.SmsPreview(template, params) + log.Println("smsJob preview:", result) + log.Printf("smsJob preview: n=%d, fee=%d\n", n, fee) + // 针对fee设置风控门槛 + if fee > feeLimit { + log.Printf("smsJob fee exceeded! n=%d, fee=%d\n", n, fee) + return + } + // 发送短信 + res, err := tc_sms.SmsSend(signName, templateId, params, phones) + if err != nil { + log.Println("smsJob error:", err) + return + } + // 检查返回结果是否全部发送成功 + str := res.ToJsonString() + if strings.Count(str, "send success") < len(phones) { + log.Println("smsJob some failed", str) + return + } + log.Println("smsJob all success:", str) +} +``` + +### 错误码 InvalidParameterValue.TemplateParameterLengthLimit 及文本截断 + +```go +// 注意:错误码 InvalidParameterValue.TemplateParameterLengthLimit +// 非验证码短信:每个变量取值最多支持6个字符。 +// https://cloud.tencent.cn/document/product/382/52075 +const paramLimit = 6 + +func smsJob() { + // ... + for i, v := range params { + // ❌ 注意:错误的截断方式 + // if len(v) > paramLimit { + // params[i] = v[:paramLimit] + // } + // ✅ 注意:正确处理中文字符长度 + if len([]rune(v)) > paramLimit { + params[i] = truncateString(v, paramLimit) // 直接截断 + // ...或进行其他自定义的智能截断处理 + } + } + // ... +} + +func truncateString(s string, length int) string { + runes := []rune(s) + if len(runes) <= length { + return s + } + return string(runes[:length]) +} +``` + +### 参考链接 + +- 腾讯云-短信-国内短信-正文模板管理:https://console.cloud.tencent.com/smsv2/csms-template +- 关于单条短信计费fee:https://console.cloud.tencent.com/smsv2/csms-template/create +- 非验证码短信:每个变量取值最多支持6个字符:https://cloud.tencent.cn/document/product/382/52075 +- 参考了GitHub上的代码:https://github.com/ixre/go2o/blob/2c7f7c875501432b008b84636ab41cdac5527bd1/core/sp/tencent/tecent_sms.go diff --git a/tc_sms/go.mod b/tc_sms/go.mod new file mode 100644 index 0000000..1a04522 --- /dev/null +++ b/tc_sms/go.mod @@ -0,0 +1,8 @@ +module gitee.com/we-mid/go/tc_sms + +go 1.21.1 + +require ( + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.975 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.975 +) diff --git a/tc_sms/go.sum b/tc_sms/go.sum new file mode 100644 index 0000000..8f46eb0 --- /dev/null +++ b/tc_sms/go.sum @@ -0,0 +1,4 @@ +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.975 h1:MvrNR+UtZhinneKgDngVlsm2kzuqOQ25rfwhaxedptE= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.975/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.975 h1:nFBhBNjeQO7TgMdMgYg1d7JhOamooper7WJ7OQYjHKI= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms v1.0.975/go.mod h1:XXAOh54ULn6wv0KPpFX9DI26lBujGeldB/sQgMMHn98= diff --git a/tc_sms/preview.go b/tc_sms/preview.go new file mode 100644 index 0000000..656c639 --- /dev/null +++ b/tc_sms/preview.go @@ -0,0 +1,39 @@ +package tc_sms + +import ( + "math" + "regexp" + "strconv" + "strings" +) + +// 注意:关于单条短信计费fee -- 1. 汉字、字母、数字、标点符号(不区分全角/半角)以及空格等,都按1个字计算 +// 2. 国内短信短信长度(签名+正文)不超过70字时,按照1条短信计费;超过70字即为长短信时,按67字/条分隔成多条计费,但会在1条短信内呈现。例如,短信长度为150字,则按照67字/67字/16字分隔成3条计费 +// https://console.cloud.tencent.com/smsv2/csms-template/create +const nPerFee = 67 + +// 发送前预览完整文本,自动计算fee +func SmsPreview(template string, params []string) (string, int, int) { + out := replacePlaceholders(template, params) + n := len([]rune(out)) // 注意是 字符数 + return out, n, int(math.Ceil(float64(n) / float64(nPerFee))) +} + +func replacePlaceholders(s string, params []string) string { + // 正则表达式匹配形如 {数字} 的模式 + reTmpl := regexp.MustCompile(`\{(\d+)\}`) + return reTmpl.ReplaceAllStringFunc(s, func(match string) string { + // 提取数字部分并转换为整型 + indexStr := strings.Trim(match, "{}") + index, err := strconv.Atoi(indexStr) + index -= 1 + if err != nil { + return match // 如果转换失败,返回原样 + } + // 确保索引在切片范围内 + if index < 0 || index >= len(params) { + return match // 如果索引越界,返回原样 + } + return params[index] + }) +} diff --git a/tc_sms/send.go b/tc_sms/send.go new file mode 100644 index 0000000..8a603b7 --- /dev/null +++ b/tc_sms/send.go @@ -0,0 +1,35 @@ +package tc_sms + +import ( + "os" + + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/regions" + sms "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111" +) + +// 参考了GitHub上的代码 +// https://github.com/ixre/go2o/blob/2c7f7c875501432b008b84636ab41cdac5527bd1/core/sp/tencent/tecent_sms.go +func SmsSend(signName, templateId string, params, phones []string) (*sms.SendSmsResponse, error) { + // 硬编码密钥到代码中有可能随代码泄露而暴露,有安全隐患,并不推荐。 + // 为了保护密钥安全,建议将密钥设置在环境变量中或者配置文件中,请参考本文凭证管理章节。 + credential := common.NewCredential( + os.Getenv("TENCENTCLOUD_SECRET_ID"), + os.Getenv("TENCENTCLOUD_SECRET_KEY"), + ) + cpf := profile.NewClientProfile() + cpf.HttpProfile.ReqMethod = "POST" + cpf.HttpProfile.Endpoint = "sms.tencentcloudapi.com" + cpf.SignMethod = "HmacSHA1" + client, _ := sms.NewClient(credential, regions.Guangzhou, cpf) + + req := sms.NewSendSmsRequest() + // 配置签名和应用Id + req.SmsSdkAppId = common.StringPtr(os.Getenv("TC_SDK_APP_ID")) + req.SignName = common.StringPtr(signName) + req.TemplateId = common.StringPtr(templateId) + req.TemplateParamSet = common.StringPtrs(params) + req.PhoneNumberSet = common.StringPtrs(phones) + return client.SendSms(req) +}