From 7f0383b7d6355b598f1598db46100b51d06cb0da Mon Sep 17 00:00:00 2001 From: Mrx <18278715334@163.com> Date: Wed, 6 May 2026 16:42:27 +0800 Subject: [PATCH] f --- .../logic/agent/applyforagentlogic.go | 9 ++- .../logic/agent/registerbyinvitecodelogic.go | 62 +++++++++++++------ .../api/internal/logic/auth/sendsmslogic.go | 43 ++++++++----- common/reviewphone/reviewphone.go | 22 +++++++ 4 files changed, 100 insertions(+), 36 deletions(-) create mode 100644 common/reviewphone/reviewphone.go diff --git a/app/main/api/internal/logic/agent/applyforagentlogic.go b/app/main/api/internal/logic/agent/applyforagentlogic.go index 697adb4..39f4c67 100644 --- a/app/main/api/internal/logic/agent/applyforagentlogic.go +++ b/app/main/api/internal/logic/agent/applyforagentlogic.go @@ -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 { diff --git a/app/main/api/internal/logic/agent/registerbyinvitecodelogic.go b/app/main/api/internal/logic/agent/registerbyinvitecodelogic.go index 976c7f4..116c814 100644 --- a/app/main/api/internal/logic/agent/registerbyinvitecodelogic.go +++ b/app/main/api/internal/logic/agent/registerbyinvitecodelogic.go @@ -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 diff --git a/app/main/api/internal/logic/auth/sendsmslogic.go b/app/main/api/internal/logic/auth/sendsmslogic.go index cc98ba0..dd0143d 100644 --- a/app/main/api/internal/logic/auth/sendsmslogic.go +++ b/app/main/api/internal/logic/auth/sendsmslogic.go @@ -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" @@ -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 { secretKey := l.svcCtx.Config.Encrypt.SecretKey encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey) - if err != nil { - return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 加密手机号失败: %v", err) - } + if err != nil { + return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "短信发送, 加密手机号失败: %v", err) + } - // 1. 滑块验证码校验(可选,支持微信环境跳过验证) - 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 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 + } - if captchaResult.VerifyErr != nil { - return captchaResult.VerifyErr - } + // 1. 滑块验证码校验(可选,支持微信环境跳过验证) + 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. 防刷策略 if captchaResult.Skipped { diff --git a/common/reviewphone/reviewphone.go b/common/reviewphone/reviewphone.go new file mode 100644 index 0000000..9867f64 --- /dev/null +++ b/common/reviewphone/reviewphone.go @@ -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 判断是否为审核预留手机号(13100009999–13100010099)。 +func IsAppReviewDemoMobile(mobile string) bool { + n, err := strconv.ParseInt(mobile, 10, 64) + if err != nil { + return false + } + return n >= DemoMobileMin && n <= DemoMobileMax +}