This commit is contained in:
Mrx
2026-05-06 16:42:27 +08:00
parent 2312f54e1e
commit 7f0383b7d6
4 changed files with 100 additions and 36 deletions

View File

@@ -10,6 +10,7 @@ import (
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/reviewphone"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
@@ -53,8 +54,12 @@ func (l *ApplyForAgentLogic) ApplyForAgent(req *types.AgentApplyReq) (resp *type
if req.Referrer == "" {
return nil, errors.Wrapf(xerr.NewErrMsg("请填写邀请信息"), "")
}
// 2. 校验验证码开发环境跳过验证码校验)
if os.Getenv("ENV") != "development" {
// 2. 校验验证码开发环境跳过;审核预留号段 + 固定码;其余 Redis
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)
cacheCode, err := l.svcCtx.Redis.Get(redisKey)
if err != nil {

View File

@@ -8,11 +8,13 @@ import (
"qnc-server/app/main/model"
"qnc-server/common/ctxdata"
"qnc-server/common/globalkey"
"qnc-server/common/reviewphone"
"qnc-server/common/xerr"
"qnc-server/pkg/lzkit/crypto"
"strconv"
"time"
"github.com/go-sql-driver/mysql"
"github.com/google/uuid"
"github.com/pkg/errors"
"github.com/zeromicro/go-zero/core/stores/redis"
@@ -24,6 +26,36 @@ import (
"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 {
logx.Logger
ctx context.Context
@@ -48,8 +80,12 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
}
l.Infof("[RegisterByInviteCode] 手机号加密完成, encryptedMobile: %s", encryptedMobile)
// 校验验证码开发环境跳过验证码校验)
if os.Getenv("ENV") != "development" && req.Code != "143838" {
// 校验验证码开发环境跳过;审核预留号段 + 固定码走专用通道;其余走 Redis
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)
cacheCode, err := l.svcCtx.Redis.Get(redisKey)
if err != nil {
@@ -64,8 +100,6 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "")
}
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)
}
l.Infof("[handleMobileNotExists] 用户创建成功, userId: %s", newUser.Id)
// 创建 mobile 认证
if _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, &model.UserAuth{
Id: uuid.NewString(),
UserId: newUser.Id,
AuthType: model.UserAuthTypeMobile,
AuthKey: encryptedMobile,
}); err != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err)
if err := l.insertMobileUserAuthOrSkip(ctx, session, newUser.Id, encryptedMobile); err != nil {
return "", errors.Wrap(err, "创建手机号认证失败")
}
l.Infof("[handleMobileNotExists] 手机号认证创建成功, userId: %s", newUser.Id)
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)
}
l.Infof("[handleMobileNotExists] 用户升级为正式用户成功, userId: %s", currentUserID)
// 创建 mobile 认证
if _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, &model.UserAuth{
Id: uuid.NewString(),
UserId: currentUserID,
AuthType: model.UserAuthTypeMobile,
AuthKey: encryptedMobile,
}); err != nil {
return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err)
if err := l.insertMobileUserAuthOrSkip(ctx, session, currentUserID, encryptedMobile); err != nil {
return "", errors.Wrap(err, "创建手机号认证失败")
}
l.Infof("[handleMobileNotExists] 手机号认证创建成功, userId: %s", currentUserID)
return currentUserID, nil

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"math/rand"
"qnc-server/common/reviewphone"
"qnc-server/common/xerr"
"qnc-server/pkg/captcha"
"qnc-server/pkg/lzkit/crypto"
@@ -42,6 +43,20 @@ func (l *SendSmsLogic) SendSms(req *types.SendSmsReq, clientIP string, userAgent
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 加密手机号失败: %v", err)
}
// 审核体验:预留号段申请代理时不发短信、不校验滑块,仅写入与注册校验一致的固定验证码
if req.ActionType == "agentApply" && reviewphone.IsAppReviewDemoMobile(req.Mobile) {
codeKey := fmt.Sprintf("%s:%s", req.ActionType, encryptedMobile)
limitCodeKey := fmt.Sprintf("limit:%s:%s", req.ActionType, encryptedMobile)
if err := l.svcCtx.Redis.Setex(codeKey, reviewphone.DemoVerifyCode, l.svcCtx.Config.VerifyCode.ValidTime); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 审核号写入验证码失败: %v", err)
}
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
}
// 1. 滑块验证码校验(可选,支持微信环境跳过验证)
cfg := l.svcCtx.Config.Captcha
captchaResult := captcha.VerifyOptionalWithUserAgent(captcha.Config{

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
}