feat: aliyun sms

This commit is contained in:
zzw
2026-09-03 17:11:34 +08:00
parent 493416551b
commit 73a5fa41ff
17 changed files with 488 additions and 50 deletions
+19
View File
@@ -0,0 +1,19 @@
package config
import (
"lone-services/pkg/sms"
"lone-services/pkg/utils"
)
func NewSMS() (sms.SMS, error) {
return sms.New(sms.Config{
AccessKeyId: utils.GetConfigString("sms.accessKeyId"),
AccessKeySecret: utils.GetConfigString("sms.accessKeySecret"),
SignName: utils.GetConfigString("sms.signName"),
Endpoint: utils.GetConfigString("sms.endpoint"),
TemplateCode: utils.GetConfigString("sms.template.code"),
TemplatePwd: utils.GetConfigString("sms.template.password"),
Timeout: utils.GetConfigInt("sms.timeout"),
})
}
+9 -5
View File
@@ -13,15 +13,19 @@ const (
GrantTypePassword = "password"
GrantTypeSms = "sms"
// Type* 登录身份(写入 session.type / X-User-Type),与订单侧约定一致
TypeSale uint8 = 1 // 销售
TypeStore uint8 = 2 // 门店
TypeUser uint8 = 3 // 普通用户
SmsSceneLogin int32 = 1
SmsSceneResetPassword int32 = 2
SmsRateLimitSeconds = 60
SmsCodeLength = 6
TypeSale uint8 = 1
TypeStore uint8 = 2
TypeUser uint8 = 3
SubjectTypeSale = "sale"
SubjectTypeStore = "store"
SubjectTypeUser = "user"
// ExtraJsonEmpty MySQL JSON 列不能写空串,无扩展字段时用 {}
ExtraJsonEmpty = "{}"
)
@@ -0,0 +1,120 @@
package logic
import (
"context"
"crypto/rand"
"fmt"
"math/big"
"strconv"
"time"
"lone-services/pkg/redis"
"lone-services/pkg/sms"
"lone-services/pkg/utils"
"lone-services/pkg/validate"
user "lone-services/rpc/user/pb"
"lone-services/services/user/internal/dao"
"lone-services/services/user/internal/svc"
"lone-services/services/user/validator"
"github.com/zeromicro/go-zero/core/logx"
)
type SendSmsLogic struct {
ctx context.Context
svcCtx *svc.ServiceContext
logx.Logger
}
func NewSendSmsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendSmsLogic {
return &SendSmsLogic{
ctx: ctx,
svcCtx: svcCtx,
Logger: logx.WithContext(ctx),
}
}
func (l *SendSmsLogic) SendSms(in *user.SendSmsReq) (*user.Response, error) {
var req validator.SendSmsValidator
if msg := validate.ValidateFromProto(in, &req); msg != utils.StringEmpty {
return outResponse(utils.ErrorParams, msg), nil
}
if l.svcCtx.SMS == nil {
return outResponse(utils.Fail, "短信服务未配置"), nil
}
kind, ok := smsSceneKind(req.Scene)
if !ok {
return outResponse(utils.ErrorParams, "短信场景不正确"), nil
}
freqKey := smsFreqRedisKey(req.Scene, req.Mobile)
okSet, err := redis.Client.SetNX(l.ctx, freqKey, "1", time.Duration(dao.SmsRateLimitSeconds)*time.Second).Result()
if err != nil {
l.Errorf("sms freq setnx: %v", err)
return failResponse(utils.Fail), nil
}
if !okSet {
return outResponse(utils.ErrorParams, "发送过于频繁,请稍后再试"), nil
}
code, err := genDigitCode(dao.SmsCodeLength)
if err != nil {
_ = redis.Client.Del(l.ctx, freqKey).Err()
l.Errorf("sms gen code: %v", err)
return failResponse(utils.Fail), nil
}
if err := l.svcCtx.SMS.SendByKind(req.Mobile, kind, map[string]string{"code": code}); err != nil {
_ = redis.Client.Del(l.ctx, freqKey).Err()
l.Errorf("sms send: %v", err)
return outResponse(utils.Fail, "短信发送失败"), nil
}
ttl := l.svcCtx.SMS.Timeout()
if ttl <= utils.NumberZero {
ttl = 600
}
codeKey := smsCodeRedisKey(req.Scene, req.Mobile)
if err := redis.Client.Set(l.ctx, codeKey, code, time.Duration(ttl)*time.Second).Err(); err != nil {
l.Errorf("sms cache code: %v", err)
return failResponse(utils.Fail), nil
}
return okResponse(nil), nil
}
func smsSceneKind(scene int32) (sms.TemplateKind, bool) {
switch scene {
case dao.SmsSceneLogin:
return sms.TemplateCode, true
case dao.SmsSceneResetPassword:
return sms.TemplatePassword, true
default:
return "", false
}
}
func smsCodeRedisKey(scene int32, mobile string) string {
return utils.CodeKey + strconv.Itoa(int(scene)) + ":" + mobile
}
func smsFreqRedisKey(scene int32, mobile string) string {
return "sms:freq:" + strconv.Itoa(int(scene)) + ":" + mobile
}
func genDigitCode(length int) (string, error) {
if length <= utils.NumberZero {
return utils.StringEmpty, fmt.Errorf("invalid code length")
}
out := make([]byte, length)
for i := range out {
n, err := rand.Int(rand.Reader, big.NewInt(10))
if err != nil {
return utils.StringEmpty, err
}
out[i] = byte('0' + n.Int64())
}
return string(out), nil
}
@@ -23,6 +23,11 @@ func NewUserServer(svcCtx *svc.ServiceContext) *UserServer {
}
}
func (s *UserServer) SendSms(ctx context.Context, in *user.SendSmsReq) (*user.Response, error) {
l := logic.NewSendSmsLogic(ctx, s.svcCtx)
return l.SendSms(in)
}
func (s *UserServer) Login(ctx context.Context, in *user.LoginReq) (*user.Response, error) {
l := logic.NewLoginLogic(ctx, s.svcCtx)
return l.Login(in)
@@ -1,6 +1,7 @@
package svc
import (
"lone-services/pkg/sms"
"lone-services/pkg/utils"
"lone-services/services/user/internal/config"
@@ -14,6 +15,7 @@ type ServiceContext struct {
Prefix string
JWT *config.JWTAuth
SaleSvcName string
SMS sms.SMS
}
func NewServiceContext(c config.Config, db *gorm.DB, jwtAuth *config.JWTAuth) *ServiceContext {
@@ -21,11 +23,18 @@ func NewServiceContext(c config.Config, db *gorm.DB, jwtAuth *config.JWTAuth) *S
if saleSvc == utils.StringEmpty {
logx.Error("config services.sale empty")
}
smsClient, err := config.NewSMS()
if err != nil {
logx.Errorf("sms init: %v", err)
}
return &ServiceContext{
Config: c,
DB: db,
Prefix: utils.GetConfigString("mysql.prefix"),
JWT: jwtAuth,
SaleSvcName: saleSvc,
SMS: smsClient,
}
}