Files
2024-11-15 19:01:25 +08:00

382 lines
14 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 tencent
import (
"context"
"encoding/json"
"errors"
"fmt"
"code.yun.ink/pkg/smsx/entity"
"code.yun.ink/pkg/smsx/interfaces"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
errorsTen "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors"
"github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
sms "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sms/v20210111" // 引入sms
"github.com/yuninks/loggerx"
)
type Tencent struct {
interfaces.DefaultSmsx
client *sms.Client
params consts.SmsConfigData
logger loggerx.LoggerInterface
}
func (l *Tencent) InitSmsx(ctx context.Context, params consts.SmsConfigData, logger loggerx.LoggerInterface) (interfaces.Smsx, error) {
l.logger = logger
if params.SysType != consts.Platform3rdTypeTencent || params.Tencent == nil {
return nil, errors.New("not tencent")
}
if params.Tencent.Endpoint == "" {
params.Tencent.Endpoint = "sms.tencentcloudapi.com"
}
if params.Tencent.Region == "" {
params.Tencent.Region = "ap-guangzhou"
}
credential := common.NewCredential(
params.Tencent.SecretId,
params.Tencent.SecretKey,
)
/* 非必要步骤:
* 实例化一个客户端配置对象,可以指定超时时间等配置 */
cpf := profile.NewClientProfile()
/* SDK默认使用POST方法。
* 如果您一定要使用GET方法,可以在这里设置。GET方法无法处理一些较大的请求 */
cpf.HttpProfile.ReqMethod = "POST"
cpf.HttpProfile.ReqTimeout = 10 // 请求超时时间,单位为秒(默认60秒)
/* 指定接入地域域名,默认就近地域接入域名为 sms.tencentcloudapi.com ,也支持指定地域域名访问,例如广州地域的域名为 sms.ap-guangzhou.tencentcloudapi.com */
cpf.HttpProfile.Endpoint = params.Tencent.Endpoint
/* SDK默认用TC3-HMAC-SHA256进行签名,非必要请不要修改这个字段 */
cpf.SignMethod = "HmacSHA1"
/* 实例化要请求产品(以sms为例)的client对象
* 第二个参数是地域信息,可以直接填写字符串ap-guangzhou,支持的地域列表参考 https://cloud.tencent.com/document/api/382/52071#.E5.9C.B0.E5.9F.9F.E5.88.97.E8.A1.A8 */
client, err := sms.NewClient(credential, params.Tencent.Region, cpf)
if err != nil {
return nil, err
}
return &Tencent{
client: client,
params: params,
logger: logger,
}, nil
}
// Send 发送短信
func (l *Tencent) Send(ctx context.Context, templateId string, phone string, params []interfaces.SmsSendParam) error {
if l.client == nil {
return errors.New("not init")
}
l.logger.Infof(ctx, "tencent send templateId:%+v phone:%+v params:%+v", templateId, phone, params)
/* 实例化一个请求对象,根据调用的接口和实际情况,可以进一步设置请求参数
* 您可以直接查询SDK源码确定接口有哪些属性可以设置
* 属性可能是基本类型,也可能引用了另一个数据结构
* 推荐使用IDE进行开发,可以方便的跳转查阅各个接口和数据结构的文档说明 */
request := sms.NewSendSmsRequest()
/* 基本类型的设置:
* SDK采用的是指针风格指定参数,即使对于基本类型您也需要用指针来对参数赋值。
* SDK提供对基本类型的指针引用封装函数
* 帮助链接:
* 短信控制台: https://console.cloud.tencent.com/smsv2
* 腾讯云短信小助手: https://cloud.tencent.com/document/product/382/3773#.E6.8A.80.E6.9C.AF.E4.BA.A4.E6.B5.81 */
/* 短信应用ID: 短信SdkAppId在 [短信控制台] 添加应用后生成的实际SdkAppId,示例如1400006666 */
// 应用 ID 可前往 [短信控制台](https://console.cloud.tencent.com/smsv2/app-manage) 查看
request.SmsSdkAppId = common.StringPtr(l.params.Tencent.SmsSdkAppId)
/* 短信签名内容: 使用 UTF-8 编码,必须填写已审核通过的签名 */
// 签名信息可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-sign) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-sign) 的签名管理查看
request.SignName = common.StringPtr(l.params.Tencent.SignName)
/* 模板 ID: 必须填写已审核通过的模板 ID */
// 模板 ID 可前往 [国内短信](https://console.cloud.tencent.com/smsv2/csms-template) 或 [国际/港澳台短信](https://console.cloud.tencent.com/smsv2/isms-template) 的正文模板管理查看
request.TemplateId = common.StringPtr(templateId)
tempParams := []string{}
for _, val := range params {
tempParams = append(tempParams, val.Value)
}
/* 模板参数: 模板参数的个数需要与 TemplateId 对应模板的变量个数保持一致,若无模板参数,则设置为空*/
request.TemplateParamSet = common.StringPtrs(tempParams)
/* 下发手机号码,采用 E.164 标准,+[国家或地区码][手机号]
* 示例如:+8613711112222, 其中前面有一个+号 86为国家码,13711112222为手机号,最多不要超过200个手机号*/
request.PhoneNumberSet = common.StringPtrs([]string{phone})
/* 用户的 session 内容(无需要可忽略): 可以携带用户侧 ID 等上下文信息,server 会原样返回 */
request.SessionContext = common.StringPtr("")
/* 短信码号扩展号(无需要可忽略): 默认未开通,如需开通请联系 [腾讯云短信小助手] */
request.ExtendCode = common.StringPtr("")
/* 国内短信无需填写该项;国际/港澳台短信已申请独立 SenderId 需要填写该字段,默认使用公共 SenderId,无需填写该字段。注:月度使用量达到指定量级可申请独立 SenderId 使用,详情请联系 [腾讯云短信小助手](https://cloud.tencent.com/document/product/382/3773#.E6.8A.80.E6.9C.AF.E4.BA.A4.E6.B5.81)。 */
request.SenderId = common.StringPtr("")
// 通过client对象调用想要访问的接口,需要传入请求对象
response, err := l.client.SendSms(request)
l.logger.Infof(ctx, "tencent send response:%+v err:%+v", response.ToJsonString(), err)
// 处理异常
if _, ok := err.(*errorsTen.TencentCloudSDKError); ok {
l.logger.Errorf(ctx, "An API error has returned: %s", err)
// return
return err
}
// 非SDK异常,直接失败。实际代码中可以加入其他的处理。
if err != nil {
// panic(err)
l.logger.Errorf(ctx, "An API error has returned: %s", err)
return err
}
if response == nil || response.Response == nil {
l.logger.Errorf(ctx, "response is nil")
return errors.New("response is nil")
}
if response.Response.SendStatusSet == nil || len(response.Response.SendStatusSet) == 0 {
l.logger.Errorf(ctx, "response.Response.SendStatusSet is nil")
return errors.New("response is nil")
}
if *response.Response.SendStatusSet[0].Code != "Ok" {
b, _ := json.Marshal(response.Response)
l.logger.Errorf(ctx, "val:%+v", string(b))
return errors.New(*response.Response.SendStatusSet[0].Message)
}
// {
// "SendStatusSet": [
// {
// "SerialNo": "",
// "PhoneNumber": "18819446148",
// "Fee": 0,
// "SessionContext": "",
// "Code": "FailedOperation.SignatureIncorrectOrUnapproved",
// "Message": "signature format is incorrect or signature is not approved",
// "IsoCode": ""
// }
// ],
// "RequestId": "33550d74-3654-4317-aabb-0fc50c2f4ab3"
// }
// {
// "SendStatusSet": [
// {
// "SerialNo": "4413:108110024517247563857264614",
// "PhoneNumber": "+8618819446148",
// "Fee": 1,
// "SessionContext": "",
// "Code": "Ok",
// "Message": "send success",
// "IsoCode": "CN"
// }
// ],
// "RequestId": "fd30a822-8664-4b85-b9b0-c8c1fa6293f9"
// }
/* 当出现以下错误码时,快速解决方案参考
* [FailedOperation.SignatureIncorrectOrUnapproved](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Afailedoperation.signatureincorrectorunapproved-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F)
* [FailedOperation.TemplateIncorrectOrUnapproved](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Afailedoperation.templateincorrectorunapproved-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F)
* [UnauthorizedOperation.SmsSdkAppIdVerifyFail](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Aunauthorizedoperation.smssdkappidverifyfail-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F)
* [UnsupportedOperation.ContainDomesticAndInternationalPhoneNumber](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Aunsupportedoperation.containdomesticandinternationalphonenumber-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F)
* 更多错误,可咨询[腾讯云助手](https://tccc.qcloud.com/web/im/index.html#/chat?webAppId=8fa15978f85cb41f7e2ea36920cb3ae1&title=Sms)
*/
return nil
}
// GetTemp 获取模板
func (l *Tencent) GetTemp(ctx context.Context) ([]interfaces.TemplateInfo, error) {
if l.client == nil {
return nil, errors.New("not init")
}
// International 是 Integer 是否国际/港澳台短信:
// 0:表示国内短信。
// 1:表示国际/港澳台短信。
// 示例值:0
// 国内
ins, err := l.getTempPages(ctx, 0)
l.logger.Infof(ctx, "ins:%+v", ins)
if err != nil {
return nil, err
}
// 国外
outs, err := l.getTempPages(ctx, 1)
l.logger.Infof(ctx, "outs:%+v", outs)
if err != nil {
return nil, err
}
return append(ins, outs...), nil
}
func (l *Tencent) getTempPages(ctx context.Context, internal uint64) ([]interfaces.TemplateInfo, error) {
page := 0
list := []interfaces.TemplateInfo{}
for {
page++
r, err := l.getTemp(ctx, uint64(page), internal)
if err != nil {
return nil, err
}
// break
if len(r) == 0 {
break
}
list = append(list, r...)
}
return list, nil
}
// 文档
// https://cloud.tencent.com/document/api/382/52067
func (l *Tencent) getTemp(ctx context.Context, page uint64, internal uint64) ([]interfaces.TemplateInfo, error) {
// 实例化一个请求对象,每个接口都会对应一个request对象
request := sms.NewDescribeSmsTemplateListRequest()
request.Limit = common.Uint64Ptr(10)
request.Offset = common.Uint64Ptr((page - 1) * 10)
request.International = common.Uint64Ptr(internal)
// 返回的resp是一个DescribeSmsTemplateListResponse的实例,与请求对象对应
response, err := l.client.DescribeSmsTemplateList(request)
if _, ok := err.(*errorsTen.TencentCloudSDKError); ok {
l.logger.Errorf(ctx, "An API error has returned: %s", err)
return nil, err
}
if err != nil {
// panic(err)
l.logger.Errorf(ctx, "An API error has returned: %s", err)
return nil, err
}
// 输出json格式的字符串回包
// fmt.Printf("%s", response.ToJsonString())
l.logger.Infof(ctx, "response:%+v", response.ToJsonString())
if response == nil || response.Response == nil {
l.logger.Errorf(ctx, "response is nil")
return nil, errors.New("response is nil")
}
resps := []interfaces.TemplateInfo{}
for _, val := range response.Response.DescribeTemplateStatusSet {
// fmt.Printf("pppppp:%+v\n", val)
info := interfaces.TemplateInfo{}
if val.TemplateId != nil {
info.TempId = fmt.Sprintf("%d", *val.TemplateId)
}
if val.TemplateName != nil {
info.TempName = *val.TemplateName
}
if val.TemplateContent != nil {
info.Content = *val.TemplateContent
p := []consts.SmsTemplateParam{}
s := l.extractParams(*val.TemplateContent)
for _, v := range s {
p = append(p, consts.SmsTemplateParam{
FieldName: v,
})
}
info.Params = p
}
if val.International != nil {
info.TempType = l.tempTypePlatToLocal(*val.International)
info.TempRange = l.tempRangePlatToLocal(*val.International)
}
if val.StatusCode != nil {
info.Status = l.auditStatusPlatToLocal(*val.StatusCode)
}
resps = append(resps, info)
}
// fmt.Printf("resps:%+v\n", resps)
return resps, nil
}
// TempDel 删除模板
func (l *Tencent) TempDel(ctx context.Context, templateId string) error {
if l.client == nil {
return errors.New("not init")
}
return nil
}
// 判断作用范围
func (l *Tencent) tempRangePlatToLocal(plat uint64) consts.SmsTemplateRange {
// 是否国际/港澳台短信,其中0表示国内短信,1表示国际/港澳台短信,3表示该模板既支持国内短信也支持国际/港澳台短信。
switch plat {
case 0:
return consts.SmsTemplateRangeChina
case 1:
return consts.SmsTemplateRangeInternational
case 3:
return consts.SmsTemplateRangeGlobal
default:
return consts.SmsTemplateRangeChina // 若未找到匹配项,默认为国内短信
}
}
// 审核状态转换
func (l *Tencent) auditStatusPlatToLocal(plat int64) consts.SmsTemplateStatus {
// 申请模板状态,其中0表示审核通过且已生效,1表示审核中,2表示审核通过待生效,-1表示审核未通过或审核失败。注:只有状态值为0时该模板才能使用。
switch plat {
case 0:
return consts.SmsTemplateStatusPass
case 1:
return consts.SmsTemplateStatusAudit
case 2:
return consts.SmsTemplateStatusAudit
case -1:
return consts.SmsTemplateStatusUnPass
default:
return consts.SmsTemplateStatusUnPass // 若未找到匹配项,默认为审核未通过或审核失败
}
}
// 模板类型转换(查询接口没返回)
func (l *Tencent) tempTypePlatToLocal(plat uint64) consts.SmsTemplateType {
// 是否国际/港澳台短信,其中0表示国内短信,1表示国际/港澳台短信,3表示该模板既支持国内短信也支持国际/港澳台短信。
return consts.SmsTemplateTypeUnknown
}
// 提取参数
func (l *Tencent) extractParams(str string) []string {
params := []string{}
for i := 0; i < len(str); i++ {
if str[i] == '{' {
start := i
for j := i + 1; j < len(str); j++ {
if str[j] == '}' {
params = append(params, str[start+1:j])
i = j
break
}
}
}
}
return params
}