@@ -0,0 +1,168 @@
|
||||
package smsser
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"91porn-server/app/appg"
|
||||
"91porn-server/common"
|
||||
"91porn-server/common/constant"
|
||||
"91porn-server/common/constant/redisconst"
|
||||
"91porn-server/common/log"
|
||||
"91porn-server/common/maths"
|
||||
"91porn-server/common/redis"
|
||||
"91porn-server/common/sms"
|
||||
"91porn-server/common/stderr"
|
||||
v10 "91porn-server/common/v10"
|
||||
"91porn-server/common/ysphone"
|
||||
"91porn-server/models/v/usermod"
|
||||
)
|
||||
|
||||
type smsCaptchaLimitRule struct {
|
||||
window time.Duration
|
||||
maxCount int64
|
||||
}
|
||||
|
||||
var smsCaptchaLimitRules = []smsCaptchaLimitRule{
|
||||
{window: time.Minute, maxCount: redisconst.MobileCaptchaMaxCountInMinute},
|
||||
{window: time.Hour, maxCount: redisconst.MobileCaptchaMaxCountInHour},
|
||||
{window: 24 * time.Hour, maxCount: redisconst.MobileCaptchaMaxCountInDay},
|
||||
}
|
||||
|
||||
func SendSmsCode(ctx context.Context, mobile string, typ int) stderr.Code {
|
||||
mobile = strings.TrimSpace(mobile)
|
||||
mobile = ysphone.FormatPhoneNumber(mobile)
|
||||
if !v10.IsGlobalizationPhoneNumber(mobile) {
|
||||
return stderr.ErrMobileInvalid
|
||||
}
|
||||
//如果发送的验证码 用户手机绑定 则先校验 当前手机号是否被绑定
|
||||
if typ == 1 {
|
||||
//手机号是否绑定
|
||||
if user, _ := usermod.FindUserByMobile(mobile); user != nil {
|
||||
return stderr.ErrMobileHasBindByOther
|
||||
}
|
||||
}
|
||||
|
||||
// 限制同一手机号在 1 分钟/1 小时/24 小时内的发送次数,使用滑动窗口统计。
|
||||
rateCode := checkAndRecordSMSCaptchaRate(ctx, mobile)
|
||||
if rateCode != stderr.Success {
|
||||
return rateCode
|
||||
}
|
||||
|
||||
redisKey := redisconst.MobileCaptchaKey(mobile)
|
||||
redisCode, err := appg.Redis.Get(redisKey)
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, "SendCaptcha redis get err", log.Any("mobile", mobile), log.E(err))
|
||||
return stderr.ErrNetWorkBusy
|
||||
}
|
||||
var code string
|
||||
if redisCode != nil {
|
||||
code = *redisCode
|
||||
} else {
|
||||
code = maths.RandDigits(constant.CaptchaLen)
|
||||
}
|
||||
if err = appg.Redis.Set(redisKey, code, redisconst.MobileCaptchaExpire); err != nil {
|
||||
log.ErrorX(ctx, "SendCaptcha Save Redis error", log.Any("mobile", mobile), log.E(err))
|
||||
return stderr.ErrNetWorkBusy
|
||||
}
|
||||
common.Go(func() {
|
||||
err = sms.Send(sms.FengNiao, mobile, code)
|
||||
if err != nil {
|
||||
log.ErrorX(ctx, "SendCaptcha send err", log.Any("mobile", mobile), log.Any("Platform", sms.FengNiao),
|
||||
log.E(err))
|
||||
yunErr := sms.Send(sms.YunPian, mobile, code)
|
||||
if yunErr != nil {
|
||||
log.ErrorX(ctx, "SendCaptcha send err", log.Any("mobile", mobile), log.Any("Platform", sms.YunPian),
|
||||
log.E(err))
|
||||
}
|
||||
}
|
||||
})
|
||||
return stderr.Success
|
||||
}
|
||||
|
||||
func checkAndRecordSMSCaptchaRate(ctx context.Context, mobile string) stderr.Code {
|
||||
rateKey := redisconst.SMSCaptchaPhoneNumberKey(mobile)
|
||||
for retry := 0; retry < 2; retry++ {
|
||||
ret, err := checkAndRecordSMSCaptchaRateOnce(rateKey)
|
||||
if err == nil {
|
||||
return ret
|
||||
}
|
||||
if retry == 0 && isRedisWrongTypeError(err) {
|
||||
_, _ = appg.Redis.Del(rateKey)
|
||||
continue
|
||||
}
|
||||
log.ErrorX(ctx, "checkAndRecordSMSCaptchaRate failed", log.Any("mobile", mobile), log.E(err))
|
||||
return stderr.ErrNetWorkBusy
|
||||
}
|
||||
return stderr.ErrNetWorkBusy
|
||||
}
|
||||
|
||||
func checkAndRecordSMSCaptchaRateOnce(rateKey string) (stderr.Code, error) {
|
||||
now := time.Now().Unix()
|
||||
dayWindowSec := int64((24 * time.Hour) / time.Second)
|
||||
|
||||
// 先清理 24 小时窗口外的数据,避免集合无界增长。
|
||||
_, err := appg.Redis.ZRemRangeByScore(rateKey, "-inf", strconv.FormatInt(now-dayWindowSec, 10))
|
||||
if err != nil {
|
||||
return stderr.ErrNetWorkBusy, err
|
||||
}
|
||||
|
||||
for _, rule := range smsCaptchaLimitRules {
|
||||
windowSec := int64(rule.window / time.Second)
|
||||
start := strconv.FormatInt(now-windowSec+1, 10)
|
||||
end := strconv.FormatInt(now, 10)
|
||||
cnt, countErr := appg.Redis.ZCount(rateKey, start, end)
|
||||
if countErr != nil {
|
||||
return stderr.ErrNetWorkBusy, countErr
|
||||
}
|
||||
if cnt >= rule.maxCount {
|
||||
return stderr.VisitLimit, nil
|
||||
}
|
||||
}
|
||||
|
||||
member := fmt.Sprintf("%d:%d", now, time.Now().UnixNano())
|
||||
_, err = appg.Redis.ZAdd(rateKey, redis.Member{Score: float64(now), Member: member})
|
||||
if err != nil {
|
||||
return stderr.ErrNetWorkBusy, err
|
||||
}
|
||||
_, err = appg.Redis.ExpireKey(rateKey, 25*time.Hour)
|
||||
if err != nil {
|
||||
return stderr.ErrNetWorkBusy, err
|
||||
}
|
||||
return stderr.Success, nil
|
||||
}
|
||||
|
||||
func isRedisWrongTypeError(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), "WRONGTYPE")
|
||||
}
|
||||
|
||||
func VerifySmsCode(mobile string, code string) stderr.Code {
|
||||
mobile = strings.TrimSpace(mobile)
|
||||
if strings.HasPrefix(mobile, "+86122") { // pass the test mobile
|
||||
log.Info("VerifySmsCode dismiss test mobile", log.Any("mobile", mobile))
|
||||
return stderr.Success
|
||||
}
|
||||
if !v10.IsGlobalizationPhoneNumber(mobile) {
|
||||
return stderr.ErrMobileInvalid
|
||||
}
|
||||
mobile = ysphone.FormatPhoneNumber(mobile)
|
||||
redisKey := redisconst.MobileCaptchaKey(mobile)
|
||||
redisCode, err := appg.Redis.Get(redisKey)
|
||||
if err != nil {
|
||||
log.Error("VerifySmsCode redis get err", log.Any("mobile", mobile), log.E(err))
|
||||
return stderr.ErrNetWorkBusy
|
||||
}
|
||||
if redisCode != nil && *redisCode == code {
|
||||
log.Info("VerifySmsCode check correct", log.Any("mobile", mobile), log.Any("code", code), log.Any("redisCode", redisCode))
|
||||
return stderr.Success
|
||||
}
|
||||
if redisCode == nil {
|
||||
log.Info("VerifySmsCode miss", log.Any("mobile", mobile), log.Any("code", code), log.Any("redisCode", redisCode))
|
||||
return stderr.ErrCaptcha
|
||||
}
|
||||
log.Info("VerifySmsCode invalid", log.Any("mobile", mobile), log.Any("code", code), log.Any("redisCode", redisCode))
|
||||
return stderr.ErrCaptchaInvalid
|
||||
}
|
||||
Reference in New Issue
Block a user