2026-08-09 10:43:45 +08:00
|
|
|
|
package sms
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"math/rand"
|
|
|
|
|
|
"regexp"
|
|
|
|
|
|
"strings"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
2026-08-09 12:41:08 +08:00
|
|
|
|
"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"
|
2026-08-09 10:43:45 +08:00
|
|
|
|
"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
|
|
|
|
|
|
|
2026-09-22 21:15:34 +08:00
|
|
|
|
// 验证码配置缺失时返回明确错误,避免运行期空指针 panic
|
|
|
|
|
|
if config.Spec.Code == nil {
|
|
|
|
|
|
return nil, excode.ErrCodeConfig
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-09 10:43:45 +08:00
|
|
|
|
if in.GetPhone() == "" || !VerifyPhone(in.GetPhone()) {
|
|
|
|
|
|
return nil, excode.ErrPhone
|
|
|
|
|
|
}
|
|
|
|
|
|
if in.GetTemplateCode() == "" {
|
|
|
|
|
|
return nil, excode.ErrTemplate
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 21:15:34 +08:00
|
|
|
|
// 是否验证黑名单:Redis 黑名单集合与配置黑名单任一命中即拦截
|
|
|
|
|
|
if impl.RedisService.Client.SIsMember(impl.RedisService.Ctx, BlackListCacheKey, in.GetPhone()).Val() ||
|
|
|
|
|
|
inBlackListConf(in.GetPhone()) {
|
2026-08-09 10:43:45 +08:00
|
|
|
|
return nil, excode.ErrInBlackList
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 每天限制
|
|
|
|
|
|
limitKey := LimitCacheKey + time.Now().Format(FormatDay) + in.GetPhone()
|
|
|
|
|
|
|
2026-09-22 21:15:34 +08:00
|
|
|
|
// check limit:发送前校验,达到上限即拒绝
|
2026-08-09 10:43:45 +08:00
|
|
|
|
twice, err := impl.RedisService.Client.Get(impl.RedisService.Ctx, limitKey).Int()
|
|
|
|
|
|
if err != nil && !errors.Is(err, redis.Nil) {
|
|
|
|
|
|
return nil, errcode.ErrRedis
|
|
|
|
|
|
}
|
2026-09-22 21:15:34 +08:00
|
|
|
|
if config.Spec.Code.MaxSentLimit > 0 && twice >= config.Spec.Code.MaxSentLimit {
|
2026-08-09 10:43:45 +08:00
|
|
|
|
return nil, excode.ErrSentLimit
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 从redis获取验证码,如果没有重新生成
|
|
|
|
|
|
key := KeyPrefix + in.GetPhone()
|
2026-09-22 21:15:34 +08:00
|
|
|
|
expire := time.Second * time.Duration(config.Spec.Code.Expire)
|
2026-08-09 10:43:45 +08:00
|
|
|
|
if in.GetIsGenCode() {
|
|
|
|
|
|
//验证码最少4位,最大10位。
|
|
|
|
|
|
if config.Spec.Code.Length < 4 || config.Spec.Code.Length > 10 {
|
|
|
|
|
|
config.Spec.Code.Length = 6
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 新生成验证码
|
2026-09-22 21:15:34 +08:00
|
|
|
|
newCode := GenValidateCode(config.Spec.Code.Length)
|
2026-08-09 10:43:45 +08:00
|
|
|
|
//sms code write to redis
|
2026-09-22 21:15:34 +08:00
|
|
|
|
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())
|
2026-08-09 10:43:45 +08:00
|
|
|
|
} else {
|
2026-09-22 21:15:34 +08:00
|
|
|
|
// 键已存在:不覆盖Redis中的旧码,必须发送实际保存的验证码,否则用户收码后校验不过
|
|
|
|
|
|
exist, err := impl.RedisService.Client.Get(impl.RedisService.Ctx, key).Result()
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, errcode.ErrRedis
|
|
|
|
|
|
}
|
|
|
|
|
|
smsCode = exist
|
2026-08-09 10:43:45 +08:00
|
|
|
|
}
|
2026-09-22 21:15:34 +08:00
|
|
|
|
} 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
|
2026-08-09 10:43:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var result map[string]interface{}
|
2026-09-22 21:15:34 +08:00
|
|
|
|
if impl.Provider == nil {
|
|
|
|
|
|
return nil, excode.ErrProviderIsNil
|
|
|
|
|
|
}
|
2026-08-09 10:43:45 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 21:15:34 +08:00
|
|
|
|
// 未返回发送结果的实现不能视为发送成功
|
|
|
|
|
|
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()))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-09 10:43:45 +08:00
|
|
|
|
jsonBytes, _ := json.Marshal(result)
|
|
|
|
|
|
fmt.Println("短信发送结果:", string(jsonBytes))
|
|
|
|
|
|
return &pb.SmsReply{
|
|
|
|
|
|
Reply: string(jsonBytes),
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 21:15:34 +08:00
|
|
|
|
// 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())
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-09 10:43:45 +08:00
|
|
|
|
func AliyunSender(args *pb.SmsSendRequest, code string) (map[string]any, error) {
|
|
|
|
|
|
// 构建发送参数
|
|
|
|
|
|
var templateParam = map[string]any{
|
|
|
|
|
|
"code": code,
|
|
|
|
|
|
}
|
|
|
|
|
|
for key, val := range args.Paramters {
|
2026-09-22 21:15:34 +08:00
|
|
|
|
// 验证码字段由服务端生成或从Redis读取,禁止调用方覆盖
|
|
|
|
|
|
if key == "code" {
|
|
|
|
|
|
continue
|
|
|
|
|
|
}
|
2026-08-09 10:43:45 +08:00
|
|
|
|
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) {
|
2026-09-22 21:15:34 +08:00
|
|
|
|
// 腾讯云短信需要账号级 SmsSdkAppId 与有序的模板参数,现有 proto/config 均无法提供,
|
|
|
|
|
|
// 因此显式返回未实现错误,绝不能返回 nil,nil 让调用方误判为发送成功。
|
|
|
|
|
|
return nil, excode.ErrNotImplement
|
2026-08-09 10:43:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
}
|