Initial commit

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 13:57:10 +08:00
co-authored by Claude Opus 5
commit 8679200f41
1897 changed files with 257900 additions and 0 deletions
+168
View File
@@ -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
}