This commit is contained in:
2026-05-13 14:43:52 +08:00
4 changed files with 100 additions and 36 deletions

View File

@@ -10,6 +10,7 @@ import (
"qnc-server/app/main/model" "qnc-server/app/main/model"
"qnc-server/common/ctxdata" "qnc-server/common/ctxdata"
"qnc-server/common/globalkey" "qnc-server/common/globalkey"
"qnc-server/common/reviewphone"
"qnc-server/common/xerr" "qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto" "qnc-server/pkg/lzkit/crypto"
@@ -53,8 +54,12 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type
if req.Referrer == "" { if req.Referrer == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("请填写邀请信息"), "") return nil, errors.Wrapf(xerr.NewErrMsg("请填写邀请信息"), "")
} }
// 2. 校验验证码开发环境跳过验证码校验) // 2. 校验验证码开发环境跳过;审核预留号段 + 固定码;其余 Redis
if os.Getenv("ENV") != "development" { if os.Getenv("ENV") == "development" {
// skip
} else if reviewphone.IsAppReviewDemoMobile(req.Mobile) && req.Code == reviewphone.DemoVerifyCode {
l.Infof("[ApplyForAgent] 审核体验号段固定验证码通过, mobile: %s", req.Mobile)
} else {
redisKey := fmt.Sprintf("%s:%s", "agentApply", encryptedMobile) redisKey := fmt.Sprintf("%s:%s", "agentApply", encryptedMobile)
cacheCode, err := l.svcCtx.Redis.Get(redisKey) cacheCode, err := l.svcCtx.Redis.Get(redisKey)
if err != nil { if err != nil {

View File

@@ -8,11 +8,13 @@ import (
"qnc-server/app/main/model" "qnc-server/app/main/model"
"qnc-server/common/ctxdata" "qnc-server/common/ctxdata"
"qnc-server/common/globalkey" "qnc-server/common/globalkey"
"qnc-server/common/reviewphone"
"qnc-server/common/xerr" "qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto" "qnc-server/pkg/lzkit/crypto"
"strconv" "strconv"
"time" "time"
"github.com/go-sql-driver/mysql"
"github.com/google/uuid" "github.com/google/uuid"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/redis" "github.com/zeromicro/go-zero/core/stores/redis"
@@ -24,6 +26,36 @@ import (
"github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/logx"
) )
func isMySQLDuplicateEntry(err error) bool {
var me *mysql.MySQLError
return errors.As(err, &me) && me.Number == 1062
}
// insertMobileUserAuthOrSkip 写入 mobile 认证;若并发重复插入触发唯一键冲突,且该手机号已绑定同一用户则视为成功(幂等)。
func (l *RegisterByInviteCodeLogic) insertMobileUserAuthOrSkip(ctx context.Context, session sqlx.Session, userID string, encryptedMobile string) error {
_, err := l.svcCtx.UserAuthModel.Insert(ctx, session, &model.UserAuth{
Id: uuid.NewString(),
UserId: userID,
AuthType: model.UserAuthTypeMobile,
AuthKey: encryptedMobile,
})
if err == nil {
return nil
}
if !isMySQLDuplicateEntry(err) {
return err
}
exist, findErr := l.svcCtx.UserAuthModel.FindOneByAuthTypeAuthKey(ctx, model.UserAuthTypeMobile, encryptedMobile)
if findErr != nil {
return findErr
}
if exist != nil && exist.UserId == userID {
l.Infof("[insertMobileUserAuthOrSkip] mobile 认证已由并发请求创建,跳过: userId=%s", userID)
return nil
}
return errors.Wrapf(xerr.NewErrMsg("该手机号已被占用"), "")
}
type RegisterByInviteCodeLogic struct { type RegisterByInviteCodeLogic struct {
logx.Logger logx.Logger
ctx context.Context ctx context.Context
@@ -48,8 +80,12 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
} }
l.Infof("[RegisterByInviteCode] 手机号加密完成, encryptedMobile: %s", encryptedMobile) l.Infof("[RegisterByInviteCode] 手机号加密完成, encryptedMobile: %s", encryptedMobile)
// 校验验证码开发环境跳过验证码校验) // 校验验证码开发环境跳过;审核预留号段 + 固定码走专用通道;其余走 Redis
if os.Getenv("ENV") != "development" && req.Code != "143838" { if os.Getenv("ENV") == "development" {
l.Infof("[RegisterByInviteCode] 开发环境跳过验证码校验")
} else if reviewphone.IsAppReviewDemoMobile(req.Mobile) && req.Code == reviewphone.DemoVerifyCode {
l.Infof("[RegisterByInviteCode] 审核体验号段固定验证码通过, mobile: %s", req.Mobile)
} else {
redisKey := fmt.Sprintf("%s:%s", "agentApply", encryptedMobile) redisKey := fmt.Sprintf("%s:%s", "agentApply", encryptedMobile)
cacheCode, err := l.svcCtx.Redis.Get(redisKey) cacheCode, err := l.svcCtx.Redis.Get(redisKey)
if err != nil { if err != nil {
@@ -64,8 +100,6 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "") return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "")
} }
l.Infof("[RegisterByInviteCode] 验证码校验通过, mobile: %s", req.Mobile) l.Infof("[RegisterByInviteCode] 验证码校验通过, mobile: %s", req.Mobile)
} else {
l.Infof("[RegisterByInviteCode] 开发环境跳过验证码校验")
} }
// 获取当前登录态(可能为空) // 获取当前登录态(可能为空)
@@ -471,14 +505,8 @@ func (l *RegisterByInviteCodeLogic) handleMobileNotExists(ctx context.Context, s
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户失败: %v", err) return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户失败: %v", err)
} }
l.Infof("[handleMobileNotExists] 用户创建成功, userId: %s", newUser.Id) l.Infof("[handleMobileNotExists] 用户创建成功, userId: %s", newUser.Id)
// 创建 mobile 认证 if err := l.insertMobileUserAuthOrSkip(ctx, session, newUser.Id, encryptedMobile); err != nil {
if _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, &model.UserAuth{ return "", errors.Wrap(err, "创建手机号认证失败")
Id: uuid.NewString(),
UserId: newUser.Id,
AuthType: model.UserAuthTypeMobile,
AuthKey: encryptedMobile,
}); err != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err)
} }
l.Infof("[handleMobileNotExists] 手机号认证创建成功, userId: %s", newUser.Id) l.Infof("[handleMobileNotExists] 手机号认证创建成功, userId: %s", newUser.Id)
return newUser.Id, nil return newUser.Id, nil
@@ -496,14 +524,8 @@ func (l *RegisterByInviteCodeLogic) handleMobileNotExists(ctx context.Context, s
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新手机号失败: %v", err) return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新手机号失败: %v", err)
} }
l.Infof("[handleMobileNotExists] 用户升级为正式用户成功, userId: %s", currentUserID) l.Infof("[handleMobileNotExists] 用户升级为正式用户成功, userId: %s", currentUserID)
// 创建 mobile 认证 if err := l.insertMobileUserAuthOrSkip(ctx, session, currentUserID, encryptedMobile); err != nil {
if _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, &model.UserAuth{ return "", errors.Wrap(err, "创建手机号认证失败")
Id: uuid.NewString(),
UserId: currentUserID,
AuthType: model.UserAuthTypeMobile,
AuthKey: encryptedMobile,
}); err != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err)
} }
l.Infof("[handleMobileNotExists] 手机号认证创建成功, userId: %s", currentUserID) l.Infof("[handleMobileNotExists] 手机号认证创建成功, userId: %s", currentUserID)
return currentUserID, nil return currentUserID, nil

View File

@@ -4,6 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"math/rand" "math/rand"
"qnc-server/common/reviewphone"
"qnc-server/common/xerr" "qnc-server/common/xerr"
"qnc-server/pkg/captcha" "qnc-server/pkg/captcha"
"qnc-server/pkg/lzkit/crypto" "qnc-server/pkg/lzkit/crypto"
@@ -38,22 +39,36 @@ func NewSendSmsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendSmsLo
func (l *SendSmsLogic) SendSms(req *types.SendSmsReq, clientIP string, userAgent string) error { func (l *SendSmsLogic) SendSms(req *types.SendSmsReq, clientIP string, userAgent string) error {
secretKey := l.svcCtx.Config.Encrypt.SecretKey secretKey := l.svcCtx.Config.Encrypt.SecretKey
encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey) encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey)
if err != nil { if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 加密手机号失败: %v", err) return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 加密手机号失败: %v", err)
} }
// 1. 滑块验证码校验(可选,支持微信环境跳过验证 // 审核体验:预留号段申请代理时不发短信、不校验滑块,仅写入与注册校验一致的固定验证
cfg := l.svcCtx.Config.Captcha if req.ActionType == "agentApply" && reviewphone.IsAppReviewDemoMobile(req.Mobile) {
captchaResult := captcha.VerifyOptionalWithUserAgent(captcha.Config{ codeKey := fmt.Sprintf("%s:%s", req.ActionType, encryptedMobile)
AccessKeyID: cfg.AccessKeyID, limitCodeKey := fmt.Sprintf("limit:%s:%s", req.ActionType, encryptedMobile)
AccessKeySecret: cfg.AccessKeySecret, if err := l.svcCtx.Redis.Setex(codeKey, reviewphone.DemoVerifyCode, l.svcCtx.Config.VerifyCode.ValidTime); err != nil {
EndpointURL: cfg.EndpointURL, return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 审核号写入验证码失败: %v", err)
SceneID: cfg.SceneID, }
}, req.CaptchaVerifyParam, userAgent) if err := l.svcCtx.Redis.Setex(limitCodeKey, "1", 60); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 审核号限流标记失败: %v", err)
}
l.Infof("短信发送, 审核体验号段跳过真实短信, mobile=%s", req.Mobile)
return nil
}
if captchaResult.VerifyErr != nil { // 1. 滑块验证码校验(可选,支持微信环境跳过验证)
return captchaResult.VerifyErr cfg := l.svcCtx.Config.Captcha
} captchaResult := captcha.VerifyOptionalWithUserAgent(captcha.Config{
AccessKeyID: cfg.AccessKeyID,
AccessKeySecret: cfg.AccessKeySecret,
EndpointURL: cfg.EndpointURL,
SceneID: cfg.SceneID,
}, req.CaptchaVerifyParam, userAgent)
if captchaResult.VerifyErr != nil {
return captchaResult.VerifyErr
}
// 2. 防刷策略 // 2. 防刷策略
if captchaResult.Skipped { if captchaResult.Skipped {

View File

@@ -0,0 +1,22 @@
// Package reviewphone 定义微信小程序等平台审核时使用的固定体验手机号段与验证码,
// 仅用于审核人员完整体验代理注册流程,勿用于开放环境绕过真实校验。
package reviewphone
import "strconv"
const (
// DemoMobileMin / DemoMobileMax 为 inclusive 区间,共 101 个号码。
DemoMobileMin int64 = 13100009999
DemoMobileMax int64 = 13100010099
// DemoVerifyCode 与 DemoMobile 区间配合使用,通过服务端校验。
DemoVerifyCode = "143838"
)
// IsAppReviewDemoMobile 判断是否为审核预留手机号1310000999913100010099
func IsAppReviewDemoMobile(mobile string) bool {
n, err := strconv.ParseInt(mobile, 10, 64)
if err != nil {
return false
}
return n >= DemoMobileMin && n <= DemoMobileMax
}