Files
full/module/base/sender/internal/logic/sms/send.go
2026-09-22 21:15:34 +08:00

236 lines
6.8 KiB
Go
Raw 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 (
"context"
"encoding/json"
"errors"
"fmt"
"math/rand"
"regexp"
"strings"
"time"
"bsm/full/module/base/sender/internal/config"
"bsm/full/module/base/sender/internal/excode"
"bsm/full/module/base/sender/internal/impl"
pb "bsm/full/module/base/sender/pb"
"git.apinb.com/bsm-sdk/core/cache/redis"
"git.apinb.com/bsm-sdk/core/errcode"
AliYunClient "github.com/alibabacloud-go/darabonba-openapi/v2/client"
AliYunUtil "github.com/alibabacloud-go/darabonba-openapi/v2/utils"
"github.com/alibabacloud-go/tea/dara"
"github.com/alibabacloud-go/tea/tea"
"github.com/spf13/cast"
)
func Send(ctx context.Context, in *pb.SmsSendRequest) (reply *pb.SmsReply, err error) {
var smsCode string
// 验证码配置缺失时返回明确错误,避免运行期空指针 panic
if config.Spec.Code == nil {
return nil, excode.ErrCodeConfig
}
if in.GetPhone() == "" || !VerifyPhone(in.GetPhone()) {
return nil, excode.ErrPhone
}
if in.GetTemplateCode() == "" {
return nil, excode.ErrTemplate
}
// 是否验证黑名单Redis 黑名单集合与配置黑名单任一命中即拦截
if impl.RedisService.Client.SIsMember(impl.RedisService.Ctx, BlackListCacheKey, in.GetPhone()).Val() ||
inBlackListConf(in.GetPhone()) {
return nil, excode.ErrInBlackList
}
// 每天限制
limitKey := LimitCacheKey + time.Now().Format(FormatDay) + in.GetPhone()
// check limit发送前校验达到上限即拒绝
twice, err := impl.RedisService.Client.Get(impl.RedisService.Ctx, limitKey).Int()
if err != nil && !errors.Is(err, redis.Nil) {
return nil, errcode.ErrRedis
}
if config.Spec.Code.MaxSentLimit > 0 && twice >= config.Spec.Code.MaxSentLimit {
return nil, excode.ErrSentLimit
}
// 从redis获取验证码如果没有重新生成
key := KeyPrefix + in.GetPhone()
expire := time.Second * time.Duration(config.Spec.Code.Expire)
if in.GetIsGenCode() {
//验证码最少4位最大10位。
if config.Spec.Code.Length < 4 || config.Spec.Code.Length > 10 {
config.Spec.Code.Length = 6
}
// 新生成验证码
newCode := GenValidateCode(config.Spec.Code.Length)
//sms code write to redis
ok, err := impl.RedisService.Client.SetNX(impl.RedisService.Ctx, key, newCode, expire).Result()
if err != nil {
return nil, errcode.ErrRedis
}
if ok {
smsCode = newCode
// 新码写入成功,重置该号码的校验失败计数
impl.RedisService.Client.Del(impl.RedisService.Ctx, VerifyFailCacheKey+in.GetPhone())
} else {
// 键已存在不覆盖Redis中的旧码必须发送实际保存的验证码否则用户收码后校验不过
exist, err := impl.RedisService.Client.Get(impl.RedisService.Ctx, key).Result()
if err != nil {
return nil, errcode.ErrRedis
}
smsCode = exist
}
} else {
// 不生成新码时仅允许复用Redis中已有的验证码禁止调用方自行指定验证码内容
exist, err := impl.RedisService.Client.Get(impl.RedisService.Ctx, key).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return nil, excode.ErrExpired
}
return nil, errcode.ErrRedis
}
smsCode = exist
}
var result map[string]interface{}
if impl.Provider == nil {
return nil, excode.ErrProviderIsNil
}
switch strings.ToLower(in.GetProvider()) {
case "aliyun":
if impl.Provider.Aliyun == nil {
return nil, excode.ErrProviderIsNil
}
result, err = AliyunSender(in, smsCode)
case "tencent":
if impl.Provider.Tencent == nil {
return nil, excode.ErrProviderIsNil
}
result, err = TencentSender(in)
default:
return nil, excode.ErrNotProvider
}
if err != nil {
return nil, err
}
// 未返回发送结果的实现不能视为发送成功
if result == nil {
return nil, excode.ErrNotImplement
}
// 发送成功后累计当日发送量,并在首次计数时把过期时间设到当天结束
count := impl.RedisService.Client.Incr(impl.RedisService.Ctx, limitKey).Val()
if count == 1 {
impl.RedisService.Client.ExpireAt(impl.RedisService.Ctx, limitKey, endOfDay(time.Now()))
}
jsonBytes, _ := json.Marshal(result)
fmt.Println("短信发送结果:", string(jsonBytes))
return &pb.SmsReply{
Reply: string(jsonBytes),
}, nil
}
// inBlackListConf 判断号码是否命中配置中的短信黑名单
func inBlackListConf(phone string) bool {
for _, v := range config.Spec.Code.BlackListFilter {
if v == phone {
return true
}
}
return false
}
// endOfDay 返回当天23:59:59用于设置日发送量计数键的过期时间
func endOfDay(t time.Time) time.Time {
return time.Date(t.Year(), t.Month(), t.Day(), 23, 59, 59, 0, t.Location())
}
func AliyunSender(args *pb.SmsSendRequest, code string) (map[string]any, error) {
// 构建发送参数
var templateParam = map[string]any{
"code": code,
}
for key, val := range args.Paramters {
// 验证码字段由服务端生成或从Redis读取禁止调用方覆盖
if key == "code" {
continue
}
templateParam[key] = val
}
jsonBytes, _ := json.Marshal(templateParam)
params := map[string]any{
// 必填,接收短信的手机号码
"PhoneNumbers": tea.String(cast.ToString(args.Phone)),
// 必填,短信签名名称
"SignName": tea.String(cast.ToString(args.SignName)),
// 必填短信模板ID
"TemplateCode": tea.String(cast.ToString(args.TemplateCode)),
// 可选,模板参数
"TemplateParam": string(jsonBytes),
}
runtime := &dara.RuntimeOptions{}
request := &AliYunClient.OpenApiRequest{
Query: AliYunUtil.Query(params),
}
clientParams := &AliYunClient.Params{
// 接口名称
Action: tea.String("SendSms"),
// 接口版本
Version: tea.String("2017-05-25"),
// 接口协议
Protocol: tea.String("HTTPS"),
// 接口 HTTP 方法
Method: tea.String("POST"),
AuthType: tea.String("AK"),
Style: tea.String("RPC"),
// 接口 PATH
Pathname: tea.String("/"),
// 接口请求体内容格式
ReqBodyType: tea.String("json"),
// 接口响应体内容格式
BodyType: tea.String("json"),
}
fmt.Println("请求参数params为", params)
fmt.Println("请求参数clientParams为", clientParams)
return impl.Provider.Aliyun.CallApi(clientParams, request, runtime)
}
func TencentSender(args *pb.SmsSendRequest) (map[string]any, error) {
// 腾讯云短信需要账号级 SmsSdkAppId 与有序的模板参数,现有 proto/config 均无法提供,
// 因此显式返回未实现错误,绝不能返回 nil,nil 让调用方误判为发送成功。
return nil, excode.ErrNotImplement
}
func VerifyPhone(phone string) bool {
result, _ := regexp.MatchString(`^(1[3|4|5|6|7|8|9][0-9]\d{4,8})$`, phone)
return result
}
// GenValidateCode .
func GenValidateCode(width int64) string {
if width == 0 {
width = 4
}
l := 10
numeric := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
rand.Seed(time.Now().UnixNano())
var sb strings.Builder
for i := int64(0); i < width; i++ {
fmt.Fprintf(&sb, "%d", numeric[rand.Intn(l)])
}
return sb.String()
}