169 lines
5.3 KiB
Go
169 lines
5.3 KiB
Go
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
|
|
}
|