package agent import ( "bdqr-server/app/main/model" "bdqr-server/common/ctxdata" "bdqr-server/common/globalkey" "bdqr-server/common/xerr" "bdqr-server/pkg/lzkit/crypto" "context" "database/sql" "fmt" "os" "strconv" "time" "github.com/google/uuid" "github.com/pkg/errors" "github.com/zeromicro/go-zero/core/stores/redis" "github.com/zeromicro/go-zero/core/stores/sqlx" "bdqr-server/app/main/api/internal/svc" "bdqr-server/app/main/api/internal/types" "github.com/zeromicro/go-zero/core/logx" ) type RegisterByInviteCodeLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewRegisterByInviteCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterByInviteCodeLogic { return &RegisterByInviteCodeLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } // truncateAuthKey 截断 authKey 用于日志,避免敏感信息过长 func truncateAuthKey(s string) string { if len(s) <= 12 { return s } return s[:6] + "..." + s[len(s)-4:] } func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByInviteCodeReq) (resp *types.RegisterByInviteCodeResp, err error) { l.Infof("[RegisterByInviteCode] 开始处理代理注册请求 | mobile: %s, referrer: %s, code: %s", req.Mobile, req.Referrer, req.Code) secretKey := l.svcCtx.Config.Encrypt.SecretKey encryptedMobile, err := crypto.EncryptMobile(req.Mobile, secretKey) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "加密手机号失败: %v", err) } l.Infof("[RegisterByInviteCode] 手机号加密完成 | mobile: %s, encryptedMobile: %s", req.Mobile, encryptedMobile) // 校验验证码(开发环境下跳过验证码校验) if os.Getenv("ENV") != "development" && req.Code != "143838" { redisKey := fmt.Sprintf("%s:%s", "agentApply", encryptedMobile) cacheCode, err := l.svcCtx.Redis.Get(redisKey) if err != nil { if errors.Is(err, redis.Nil) { l.Infof("[RegisterByInviteCode] 验证码已过期, mobile: %s", req.Mobile) return nil, errors.Wrapf(xerr.NewErrMsg("验证码已过期"), "") } return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "读取验证码失败, %v", err) } if cacheCode != req.Code { l.Infof("[RegisterByInviteCode] 验证码不正确, mobile: %s, expected: %s, got: %s", req.Mobile, cacheCode, req.Code) return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "") } l.Infof("[RegisterByInviteCode] 验证码校验通过, mobile: %s", req.Mobile) } else { l.Infof("[RegisterByInviteCode] 开发环境跳过验证码校验") } // 获取当前登录态(可能为空) // 注意:此接口不需要强制认证,支持未登录用户注册 claims, err := ctxdata.GetClaimsFromCtx(l.ctx) if err != nil && !errors.Is(err, ctxdata.ErrNoInCtx) { l.Errorf("[RegisterByInviteCode] 获取用户信息失败: %v", err) return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败: %v", err) } if claims != nil { l.Infof("[RegisterByInviteCode] 当前登录态 | userId: %s, authType: %s, userType: %d, authKeyPrefix: %s", claims.UserId, claims.AuthType, claims.UserType, truncateAuthKey(claims.AuthKey)) } else { l.Infof("[RegisterByInviteCode] 未登录状态(claims 为 nil,将按未登录流程处理)") } // 前置检查:如果当前用户是正式用户(有手机号),进行拦截检查 if claims != nil { currentUser, err := l.svcCtx.UserModel.FindOne(l.ctx, claims.UserId) if err != nil && !errors.Is(err, model.ErrNotFound) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户失败: %v", err) } if currentUser != nil && currentUser.Mobile.Valid && currentUser.Mobile.String != "" { l.Infof("[RegisterByInviteCode] 前置检查-正式用户 | userId: %s, user.mobile: %s, 请求加密手机号: %s, 匹配: %v", claims.UserId, currentUser.Mobile.String, encryptedMobile, currentUser.Mobile.String == encryptedMobile) // 当前用户是正式用户,检查是否已是代理 agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, claims.UserId) if err != nil && !errors.Is(err, model.ErrNotFound) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理失败: %v", err) } if agent != nil { l.Infof("[RegisterByInviteCode] 前置检查-已是代理拒绝 | userId: %s, agentId: %s", claims.UserId, agent.Id) return nil, errors.Wrapf(xerr.NewErrMsg("您已经是代理,不能重复注册"), "") } // 正式用户手机号必须匹配 if currentUser.Mobile.String != encryptedMobile { l.Infof("[RegisterByInviteCode] 前置检查-手机号不匹配拒绝 | userId: %s, currentMobile: %s, requestEncrypted: %s", claims.UserId, currentUser.Mobile.String, encryptedMobile) return nil, errors.Wrapf(xerr.NewErrMsg("请输入当前账号的手机号码"), "") } l.Infof("[RegisterByInviteCode] 前置检查通过 | 正式用户且手机号匹配,非代理") } else { hasMobile := currentUser != nil && currentUser.Mobile.Valid && currentUser.Mobile.String != "" l.Infof("[RegisterByInviteCode] 前置检查-临时用户或无手机号 | userId: %s, userHasMobile: %v", claims.UserId, hasMobile) } } // 验证邀请码是否有效 var inviteCodeModel *model.AgentInviteCode inviteCodeModel, err = l.svcCtx.AgentInviteCodeModel.FindOneByCode(l.ctx, req.Referrer) if err != nil && !errors.Is(err, model.ErrNotFound) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询邀请码失败, %v", err) } if inviteCodeModel != nil { l.Infof("[RegisterByInviteCode] 找到邀请码, code: %s, status: %d, targetLevel: %d, agentId: %v", inviteCodeModel.Code, inviteCodeModel.Status, inviteCodeModel.TargetLevel, inviteCodeModel.AgentId) if inviteCodeModel.Status != 0 { if inviteCodeModel.Status == 1 { l.Infof("[RegisterByInviteCode] 邀请码已使用, code: %s", inviteCodeModel.Code) return nil, errors.Wrapf(xerr.NewErrMsg("邀请码已使用"), "") } l.Infof("[RegisterByInviteCode] 邀请码已失效, code: %s, status: %d", inviteCodeModel.Code, inviteCodeModel.Status) return nil, errors.Wrapf(xerr.NewErrMsg("邀请码已失效"), "") } if inviteCodeModel.ExpireTime.Valid && inviteCodeModel.ExpireTime.Time.Before(time.Now()) { l.Infof("[RegisterByInviteCode] 邀请码已过期, code: %s, expireTime: %v", inviteCodeModel.Code, inviteCodeModel.ExpireTime.Time) return nil, errors.Wrapf(xerr.NewErrMsg("邀请码已过期"), "") } l.Infof("[RegisterByInviteCode] 邀请码验证通过, code: %s", inviteCodeModel.Code) } else { l.Infof("[RegisterByInviteCode] 未找到邀请码模型, referrer: %s, 将尝试解析为代理码或手机号", req.Referrer) } // 使用事务处理注册 var userID string var agentID string var agentLevel int64 l.Infof("[RegisterByInviteCode] 开始事务处理") err = l.svcCtx.AgentInviteCodeModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error { // 1. 查找目标用户(通过手机号,可能命中缓存) targetUser, findUserErr := l.svcCtx.UserModel.FindOneByMobile(transCtx, sql.NullString{String: encryptedMobile, Valid: true}) if findUserErr != nil && !errors.Is(findUserErr, model.ErrNotFound) { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户失败, %v", findUserErr) } if targetUser != nil { l.Infof("[RegisterByInviteCode] FindOneByMobile 命中 | targetUserId: %s, encryptedMobile: %s | 走场景2(手机号已存在)", targetUser.Id, encryptedMobile) } else { l.Infof("[RegisterByInviteCode] FindOneByMobile 未命中 | targetUser=nil, encryptedMobile: %s | 走场景1(手机号不存在)。若用户刚执行过 bindMobile 仍为 nil,疑为 FindOneByMobile 缓存未失效", encryptedMobile) } // 2. 获取当前登录态信息 var currentUserID string var currentAuthType string var currentAuthKey string if claims != nil { currentUserID = claims.UserId currentAuthType = claims.AuthType currentAuthKey = claims.AuthKey l.Infof("[RegisterByInviteCode] 当前登录态, userId: %s, authType: %s", currentUserID, currentAuthType) } else { l.Infof("[RegisterByInviteCode] 未登录状态") } // 3. 根据目标用户是否存在,处理用户和认证 if targetUser == nil { l.Infof("[RegisterByInviteCode] 分支: 场景1-手机号不存在 | currentUserID: %s, encryptedMobile: %s", currentUserID, encryptedMobile) userID, err = l.handleMobileNotExists(transCtx, session, encryptedMobile, currentUserID) if err != nil { l.Errorf("[RegisterByInviteCode] 场景1失败 | currentUserID: %s, encryptedMobile: %s, err: %v", currentUserID, encryptedMobile, err) return err } l.Infof("[RegisterByInviteCode] 场景1完成 | userID: %s", userID) } else { l.Infof("[RegisterByInviteCode] 分支: 场景2-手机号已存在 | targetUserId: %s, currentUserID: %s, sameUser: %v", targetUser.Id, currentUserID, targetUser.Id == currentUserID) userID, err = l.handleMobileExists(transCtx, session, targetUser, currentUserID, currentAuthType, currentAuthKey) if err != nil { l.Errorf("[RegisterByInviteCode] 场景2失败 | targetUserId: %s, currentUserID: %s, err: %v", targetUser.Id, currentUserID, err) return err } l.Infof("[RegisterByInviteCode] 场景2完成 | userID: %s", userID) } // 4. 处理邀请码和上级关系 var targetLevel int64 var parentAgentId string if inviteCodeModel != nil { targetLevel = inviteCodeModel.TargetLevel if inviteCodeModel.AgentId.Valid { parentAgentId = inviteCodeModel.AgentId.String } l.Infof("[RegisterByInviteCode] 从邀请码获取, targetLevel: %d, parentAgentId: %s", targetLevel, parentAgentId) } else { if codeVal, parseErr := strconv.ParseInt(req.Referrer, 10, 64); parseErr == nil && codeVal > 0 { l.Infof("[RegisterByInviteCode] 解析为代理码, code: %d", codeVal) parentAgent, err := l.findAgentByCode(transCtx, codeVal) if err != nil { return errors.Wrapf(err, "") } parentAgentId = parentAgent.Id targetLevel = 1 l.Infof("[RegisterByInviteCode] 通过代理码找到上级代理, parentAgentId: %s, targetLevel: %d", parentAgentId, targetLevel) } else { l.Infof("[RegisterByInviteCode] 解析为手机号, referrer: %s", req.Referrer) encRefMobile, _ := crypto.EncryptMobile(req.Referrer, l.svcCtx.Config.Encrypt.SecretKey) agents, findErr := l.svcCtx.AgentModel.FindAll(transCtx, l.svcCtx.AgentModel.SelectBuilder().Where("mobile = ? AND del_state = ?", encRefMobile, globalkey.DelStateNo).Limit(1), "") if findErr != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询上级代理失败, %v", findErr) } if len(agents) == 0 { l.Infof("[RegisterByInviteCode] 邀请信息无效, referrer: %s", req.Referrer) return errors.Wrapf(xerr.NewErrMsg("邀请信息无效"), "") } parentAgentId = agents[0].Id targetLevel = 1 l.Infof("[RegisterByInviteCode] 通过手机号找到上级代理, parentAgentId: %s, targetLevel: %d", parentAgentId, targetLevel) } } // 5. 创建代理记录 l.Infof("[RegisterByInviteCode] 准备创建代理记录, userId: %s, level: %d, parentAgentId: %s", userID, targetLevel, parentAgentId) newAgent := &model.Agent{Id: uuid.NewString(), UserId: userID, Level: targetLevel, Mobile: encryptedMobile} if req.Region != "" { newAgent.Region = sql.NullString{String: req.Region, Valid: true} } if req.WechatId != "" { newAgent.WechatId = sql.NullString{String: req.WechatId, Valid: true} } // 6. 处理上级关系 if parentAgentId != "" { l.Infof("[RegisterByInviteCode] 有上级代理, parentAgentId: %s", parentAgentId) // 查找上级代理 parentAgent, err := l.svcCtx.AgentModel.FindOne(transCtx, parentAgentId) if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询上级代理失败, %v", err) } l.Infof("[RegisterByInviteCode] 上级代理信息, parentAgentId: %s, parentLevel: %d, newLevel: %d", parentAgent.Id, parentAgent.Level, newAgent.Level) // 验证关系是否允许(下级不能比上级等级高) if newAgent.Level > parentAgent.Level { l.Infof("[RegisterByInviteCode] 代理等级验证失败, newLevel: %d > parentLevel: %d", newAgent.Level, parentAgent.Level) return errors.Wrapf(xerr.NewErrMsg("代理等级不能高于上级代理"), "") } // 查找团队首领(钻石代理) teamLeaderId, err := l.findTeamLeader(transCtx, parentAgent.Id) if err != nil { return errors.Wrapf(err, "查找团队首领失败") } if teamLeaderId != "" { newAgent.TeamLeaderId = sql.NullString{String: teamLeaderId, Valid: true} l.Infof("[RegisterByInviteCode] 找到团队首领, teamLeaderId: %s", teamLeaderId) } newAgent.AgentCode = 0 _, err = l.svcCtx.AgentModel.Insert(transCtx, session, newAgent) if err != nil { return errors.Wrapf(err, "创建代理记录失败") } agentID = newAgent.Id l.Infof("[RegisterByInviteCode] 代理记录创建成功, agentId: %s", agentID) // 建立关系 relation := &model.AgentRelation{Id: uuid.NewString(), ParentId: parentAgent.Id, ChildId: agentID, RelationType: 1} if _, err := l.svcCtx.AgentRelationModel.Insert(transCtx, session, relation); err != nil { return errors.Wrapf(err, "建立代理关系失败") } l.Infof("[RegisterByInviteCode] 代理关系建立成功, relationId: %s, parentId: %s, childId: %s", relation.Id, relation.ParentId, relation.ChildId) } else { // 平台发放的钻石邀请码,独立成团队 if targetLevel == 3 { l.Infof("[RegisterByInviteCode] 钻石代理,独立成团队") newAgent.AgentCode = 0 _, err = l.svcCtx.AgentModel.Insert(transCtx, session, newAgent) if err != nil { return errors.Wrapf(err, "创建代理记录失败") } agentID = newAgent.Id // 设置自己为团队首领 newAgent.TeamLeaderId = sql.NullString{String: agentID, Valid: true} if err := l.svcCtx.AgentModel.UpdateInTransaction(transCtx, session, newAgent); err != nil { return errors.Wrapf(err, "更新团队首领失败") } l.Infof("[RegisterByInviteCode] 钻石代理创建成功, agentId: %s, 设置为团队首领", agentID) } else { // 白银/黄金代理,但没有上级(异常情况) l.Infof("[RegisterByInviteCode] 白银/黄金代理,无上级(异常情况), level: %d", targetLevel) newAgent.AgentCode = 0 _, err = l.svcCtx.AgentModel.Insert(transCtx, session, newAgent) if err != nil { return errors.Wrapf(err, "创建代理记录失败") } agentID = newAgent.Id l.Infof("[RegisterByInviteCode] 代理记录创建成功, agentId: %s", agentID) } } // 7. 初始化钱包 l.Infof("[RegisterByInviteCode] 初始化钱包, agentId: %s", agentID) wallet := &model.AgentWallet{Id: uuid.NewString(), AgentId: agentID} if _, err := l.svcCtx.AgentWalletModel.Insert(transCtx, session, wallet); err != nil { return errors.Wrapf(err, "初始化钱包失败") } l.Infof("[RegisterByInviteCode] 钱包初始化成功, walletId: %s", wallet.Id) // 8. 更新邀请码状态 // 钻石级别的邀请码只能使用一次,使用后立即失效 // 白银级别的邀请码可以无限使用,不更新状态 if inviteCodeModel != nil { if targetLevel == 3 { // 钻石邀请码:使用后失效 inviteCodeModel.Status = 1 // 已使用(使用后立即失效) l.Infof("[RegisterByInviteCode] 钻石邀请码,标记为已使用, code: %s", inviteCodeModel.Code) } inviteCodeModel.UsedUserId = sql.NullString{String: userID, Valid: true} inviteCodeModel.UsedAgentId = sql.NullString{String: agentID, Valid: true} inviteCodeModel.UsedTime = sql.NullTime{Time: time.Now(), Valid: true} if err := l.svcCtx.AgentInviteCodeModel.UpdateWithVersion(transCtx, session, inviteCodeModel); err != nil { return errors.Wrapf(err, "更新邀请码状态失败") } l.Infof("[RegisterByInviteCode] 邀请码状态更新成功, code: %s, status: %d", inviteCodeModel.Code, inviteCodeModel.Status) } // 9. 记录邀请码使用历史(用于统计和查询) if inviteCodeModel != nil { usage := &model.AgentInviteCodeUsage{Id: uuid.NewString(), InviteCodeId: inviteCodeModel.Id, Code: inviteCodeModel.Code, UserId: userID, AgentId: agentID, AgentLevel: targetLevel, UsedTime: time.Now()} if _, err := l.svcCtx.AgentInviteCodeUsageModel.Insert(transCtx, session, usage); err != nil { return errors.Wrapf(err, "记录邀请码使用历史失败") } l.Infof("[RegisterByInviteCode] 邀请码使用历史记录成功, usageId: %s", usage.Id) } agentLevel = targetLevel l.Infof("[RegisterByInviteCode] 事务处理完成, userId: %s, agentId: %s, level: %d", userID, agentID, agentLevel) return nil }) if err != nil { l.Errorf("[RegisterByInviteCode] 事务失败 | mobile: %s, referrer: %s, err: %v | 排查可看上文 FindOneByMobile 是否命中、场景1/2 及 handleMobileNotExists/Exists 日志", req.Mobile, req.Referrer, err) return nil, err } l.Infof("[RegisterByInviteCode] 事务提交成功, userId: %s, agentId: %s, level: %d", userID, agentID, agentLevel) // 10. 生成并返回token l.Infof("[RegisterByInviteCode] 开始生成token, userId: %s", userID) token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, userID) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成token失败: %v", err) } l.Infof("[RegisterByInviteCode] Token生成成功") now := time.Now().Unix() // 获取等级名称 levelName := "" switch agentLevel { case 1: levelName = "白银" case 2: levelName = "黄金" case 3: levelName = "钻石" } agent, _ := l.svcCtx.AgentModel.FindOne(l.ctx, agentID) agentCode := int64(0) if agent != nil { agentCode = agent.AgentCode } l.Infof("[RegisterByInviteCode] 代理注册成功, userId: %s, agentId: %s, level: %d(%s), agentCode: %d", userID, agentID, agentLevel, levelName, agentCode) return &types.RegisterByInviteCodeResp{ AccessToken: token, AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter, AgentId: agentID, Level: agentLevel, LevelName: levelName, AgentCode: agentCode, }, nil } // findTeamLeader 查找团队首领(钻石代理) func (l *RegisterByInviteCodeLogic) findTeamLeader(ctx context.Context, agentId string) (string, error) { l.Infof("[findTeamLeader] 开始查找团队首领, agentId: %s", agentId) currentId := agentId maxDepth := 100 depth := 0 for depth < maxDepth { builder := l.svcCtx.AgentRelationModel.SelectBuilder(). Where("child_id = ? AND relation_type = ? AND del_state = ?", currentId, 1, 0) relations, err := l.svcCtx.AgentRelationModel.FindAll(ctx, builder, "") if err != nil { return "", err } if len(relations) == 0 { agent, err := l.svcCtx.AgentModel.FindOne(ctx, currentId) if err != nil { return "", err } if agent.Level == 3 { l.Infof("[findTeamLeader] 找到团队首领, agentId: %s, level: %d", agent.Id, agent.Level) return agent.Id, nil } l.Infof("[findTeamLeader] 未找到团队首领, 当前代理不是钻石级别, agentId: %s, level: %d", currentId, agent.Level) return "", nil } parentAgent, err := l.svcCtx.AgentModel.FindOne(ctx, relations[0].ParentId) if err != nil { return "", err } l.Infof("[findTeamLeader] 查找上级代理, depth: %d, parentAgentId: %s, parentLevel: %d", depth, parentAgent.Id, parentAgent.Level) if parentAgent.Level == 3 { l.Infof("[findTeamLeader] 找到团队首领, agentId: %s, level: %d", parentAgent.Id, parentAgent.Level) return parentAgent.Id, nil } currentId = parentAgent.Id depth++ } l.Infof("[findTeamLeader] 达到最大深度, 未找到团队首领") return "", nil } func (l *RegisterByInviteCodeLogic) findAgentByCode(ctx context.Context, code int64) (*model.Agent, error) { l.Infof("[findAgentByCode] 通过代理码查找代理, code: %d", code) builder := l.svcCtx.AgentModel.SelectBuilder().Where("agent_code = ? AND del_state = ?", code, globalkey.DelStateNo).Limit(1) agents, err := l.svcCtx.AgentModel.FindAll(ctx, builder, "") if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询上级代理失败, %v", err) } if len(agents) == 0 { l.Infof("[findAgentByCode] 代理码不存在, code: %d", code) return nil, errors.Wrapf(xerr.NewErrMsg("上级邀请码不存在"), "") } l.Infof("[findAgentByCode] 找到代理, agentId: %s, code: %d", agents[0].Id, code) return agents[0], nil } func (l *RegisterByInviteCodeLogic) allocateAgentCode(ctx context.Context, session sqlx.Session) (int64, error) { builder := l.svcCtx.AgentModel.SelectBuilder().OrderBy("agent_code DESC").Limit(1) rows, err := l.svcCtx.AgentModel.FindAll(ctx, builder, "") if err != nil { return 0, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理编码失败, %v", err) } var next int64 = 16800 if len(rows) > 0 && rows[0].AgentCode > 0 { next = rows[0].AgentCode + 1 } return next, nil } // handleMobileNotExists 处理手机号不存在的情况 func (l *RegisterByInviteCodeLogic) handleMobileNotExists(ctx context.Context, session sqlx.Session, encryptedMobile string, currentUserID string) (string, error) { if currentUserID == "" { l.Infof("[handleMobileNotExists] 子分支 1.1: 未登录+手机号不存在 | 创建新用户, encryptedMobile: %s", encryptedMobile) newUser := &model.User{Id: uuid.NewString(), Mobile: sql.NullString{String: encryptedMobile, Valid: true}} if _, err := l.svcCtx.UserModel.Insert(ctx, session, newUser); err != nil { return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建用户失败: %v", err) } l.Infof("[handleMobileNotExists] 1.1 用户已创建 | userId: %s", newUser.Id) l.Infof("[handleMobileNotExists] 1.1 即将插入 user_auth(mobile) | userId: %s, authKey: %s", newUser.Id, encryptedMobile) if _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, &model.UserAuth{ Id: uuid.NewString(), UserId: newUser.Id, AuthType: model.UserAuthTypeMobile, AuthKey: encryptedMobile, }); err != nil { l.Errorf("[handleMobileNotExists] 1.1 插入 user_auth 失败(可能 Duplicate) | userId: %s, err: %v", newUser.Id, err) return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err) } l.Infof("[handleMobileNotExists] 1.1 完成 | userId: %s", newUser.Id) return newUser.Id, nil } // 场景1.2: 已登录临时用户 + 手机号不存在 -> 升级为正式用户 l.Infof("[handleMobileNotExists] 子分支 1.2: 已登录+手机号不存在 | currentUserID: %s, encryptedMobile: %s, 将更新 user.mobile 并插入 user_auth", currentUserID, encryptedMobile) currentUser, err := l.svcCtx.UserModel.FindOne(ctx, currentUserID) if err != nil { return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询当前用户失败: %v", err) } currentUser.Mobile = sql.NullString{String: encryptedMobile, Valid: true} if _, err := l.svcCtx.UserModel.Update(ctx, session, currentUser); err != nil { return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新手机号失败: %v", err) } l.Infof("[handleMobileNotExists] 1.2 user.mobile 已更新 | userId: %s", currentUserID) l.Infof("[handleMobileNotExists] 1.2 即将插入 user_auth(mobile) | userId: %s, authKey: %s | 若报 Duplicate 说明该手机号已绑定,疑缓存导致误走场景1", currentUserID, encryptedMobile) if _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, &model.UserAuth{ Id: uuid.NewString(), UserId: currentUserID, AuthType: model.UserAuthTypeMobile, AuthKey: encryptedMobile, }); err != nil { l.Errorf("[handleMobileNotExists] 1.2 插入 user_auth 失败 | userId: %s, err: %v | 常见为 Duplicate entry 'mobile-xxx',多为 FindOneByMobile 缓存未失效", currentUserID, err) return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err) } l.Infof("[handleMobileNotExists] 1.2 完成 | userId: %s", currentUserID) return currentUserID, nil } // handleMobileExists 处理手机号已存在的情况 func (l *RegisterByInviteCodeLogic) handleMobileExists(ctx context.Context, session sqlx.Session, targetUser *model.User, currentUserID string, currentAuthType string, currentAuthKey string) (string, error) { userID := targetUser.Id l.Infof("[handleMobileExists] 入口 | targetUserId: %s, currentUserID: %s, authType: %s", userID, currentUserID, currentAuthType) existingAgent, err := l.svcCtx.AgentModel.FindOneByUserId(ctx, userID) if err != nil && !errors.Is(err, model.ErrNotFound) { return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败: %v", err) } if existingAgent != nil { l.Infof("[handleMobileExists] 目标已是代理拒绝 | userId: %s, agentId: %s", userID, existingAgent.Id) return "", errors.Wrapf(xerr.NewErrMsg("该手机号已经是代理,不能重复注册"), "") } if currentUserID == "" { l.Infof("[handleMobileExists] 子分支 2.1: 未登录+手机号存在 | 直接使用 targetUserId: %s", userID) return userID, nil } if currentUserID == userID { l.Infof("[handleMobileExists] 子分支 2.2: 已登录且与目标同一用户 | userId: %s", userID) return userID, nil } l.Infof("[handleMobileExists] 子分支 2.3: 已登录临时用户+手机号属他人 | 合并 source: %s -> target: %s, authType: %s", currentUserID, userID, currentAuthType) return l.mergeTempUserToTarget(ctx, session, currentUserID, userID, currentAuthType, currentAuthKey) } // mergeTempUserToTarget 合并临时用户到目标用户 func (l *RegisterByInviteCodeLogic) mergeTempUserToTarget(ctx context.Context, session sqlx.Session, sourceUserID string, targetUserID string, currentAuthType string, currentAuthKey string) (string, error) { l.Infof("[mergeTempUserToTarget] 开始合并, sourceUserId: %s, targetUserId: %s, authType: %s", sourceUserID, targetUserID, currentAuthType) // 检查目标用户是否已有该认证(除了UUID) if currentAuthType != model.UserAuthTypeUUID { targetAuth, err := l.svcCtx.UserAuthModel.FindOneByUserIdAuthType(ctx, targetUserID, currentAuthType) if err != nil && !errors.Is(err, model.ErrNotFound) { return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找认证信息失败: %v", err) } if targetAuth != nil && targetAuth.AuthKey != currentAuthKey { // 目标用户已有该类型的其他认证,证明手机号绑定过其他微信等 l.Infof("[mergeTempUserToTarget] 目标用户已有其他认证, targetUserId: %s, authType: %s, targetAuthKey: %s, currentAuthKey: %s", targetUserID, currentAuthType, targetAuth.AuthKey, currentAuthKey) if currentAuthType == model.UserAuthTypeWxh5OpenID { return "", errors.Wrapf(xerr.NewErrMsg("该手机号已绑定其他微信号"), "") } if currentAuthType == model.UserAuthTypeWxMiniOpenID { return "", errors.Wrapf(xerr.NewErrMsg("该手机号已绑定其他微信号"), "") } return "", errors.Wrapf(xerr.NewErrMsg("该手机号已绑定其他终端"), "") } l.Infof("[mergeTempUserToTarget] 微信唯一性检查通过, targetUserId: %s, authType: %s", targetUserID, currentAuthType) } // 查找当前认证 existingAuth, err := l.svcCtx.UserAuthModel.FindOneByAuthTypeAuthKey(ctx, currentAuthType, currentAuthKey) if err != nil && !errors.Is(err, model.ErrNotFound) { return "", errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找认证信息失败: %v", err) } if existingAuth != nil { l.Infof("[mergeTempUserToTarget] 找到现有认证, authId: %s, userId: %s", existingAuth.Id, existingAuth.UserId) } else { l.Infof("[mergeTempUserToTarget] 未找到现有认证, 将创建新认证") } // 执行账号合并 if err := l.mergeUserAccounts(ctx, session, sourceUserID, targetUserID, currentAuthType, currentAuthKey, existingAuth); err != nil { return "", err } l.Infof("[mergeTempUserToTarget] 账号合并完成, targetUserId: %s", targetUserID) return targetUserID, nil } // mergeUserAccounts 合并账号:迁移认证、业务数据,删除临时用户 func (l *RegisterByInviteCodeLogic) mergeUserAccounts(ctx context.Context, session sqlx.Session, sourceUserID string, targetUserID string, currentAuthType string, currentAuthKey string, existingAuth *model.UserAuth) error { l.Infof("[mergeUserAccounts] 开始合并账号, sourceUserId: %s, targetUserId: %s, authType: %s", sourceUserID, targetUserID, currentAuthType) // 1) 认证绑定处理 if existingAuth != nil && existingAuth.UserId != targetUserID { l.Infof("[mergeUserAccounts] 认证存在但不属于目标用户, 开始迁移认证, authId: %s, currentUserId: %s, targetUserId: %s", existingAuth.Id, existingAuth.UserId, targetUserID) // 认证存在但不属于目标用户,迁移到目标用户 if currentAuthType == model.UserAuthTypeUUID { // UUID替换策略:如果目标用户已有UUID认证,替换UUID;否则迁移认证 l.Infof("[mergeUserAccounts] UUID认证类型, 执行替换策略") targetUUIDAuth, _ := l.svcCtx.UserAuthModel.FindOneByUserIdAuthType(ctx, targetUserID, model.UserAuthTypeUUID) if targetUUIDAuth != nil { // 目标用户已有UUID认证,删除源认证并更新目标UUID l.Infof("[mergeUserAccounts] 目标用户已有UUID认证, 删除源认证并更新目标UUID, targetAuthId: %s", targetUUIDAuth.Id) if err := l.svcCtx.UserAuthModel.Delete(ctx, session, existingAuth.Id); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "删除旧UUID认证失败: %v", err) } if targetUUIDAuth.AuthKey != currentAuthKey { targetUUIDAuth.AuthKey = currentAuthKey if _, err := l.svcCtx.UserAuthModel.Update(ctx, session, targetUUIDAuth); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新目标UUID认证失败: %v", err) } l.Infof("[mergeUserAccounts] UUID认证更新成功, authId: %s", targetUUIDAuth.Id) } } else { // 目标用户没有UUID认证,迁移源认证 l.Infof("[mergeUserAccounts] 目标用户没有UUID认证, 迁移源认证, authId: %s", existingAuth.Id) existingAuth.UserId = targetUserID if _, err := l.svcCtx.UserAuthModel.Update(ctx, session, existingAuth); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "迁移UUID认证失败: %v", err) } l.Infof("[mergeUserAccounts] UUID认证迁移成功") } } else { // 其他认证类型,直接迁移 l.Infof("[mergeUserAccounts] 其他认证类型, 直接迁移, authId: %s, authType: %s", existingAuth.Id, currentAuthType) existingAuth.UserId = targetUserID if _, err := l.svcCtx.UserAuthModel.Update(ctx, session, existingAuth); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "迁移认证失败: %v", err) } l.Infof("[mergeUserAccounts] 认证迁移成功, authId: %s", existingAuth.Id) } } else if existingAuth == nil { // 认证不存在,创建新认证 l.Infof("[mergeUserAccounts] 认证不存在, 创建新认证, authType: %s", currentAuthType) if currentAuthType == model.UserAuthTypeUUID { // UUID特殊处理:如果目标用户已有UUID认证,更新UUID;否则创建新认证 targetUUIDAuth, _ := l.svcCtx.UserAuthModel.FindOneByUserIdAuthType(ctx, targetUserID, model.UserAuthTypeUUID) if targetUUIDAuth != nil { l.Infof("[mergeUserAccounts] 目标用户已有UUID认证, 更新UUID, authId: %s", targetUUIDAuth.Id) if targetUUIDAuth.AuthKey != currentAuthKey { targetUUIDAuth.AuthKey = currentAuthKey if _, err := l.svcCtx.UserAuthModel.Update(ctx, session, targetUUIDAuth); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新目标UUID认证失败: %v", err) } l.Infof("[mergeUserAccounts] UUID认证更新成功") } } else { newAuth := &model.UserAuth{ Id: uuid.NewString(), UserId: targetUserID, AuthType: currentAuthType, AuthKey: currentAuthKey, } if _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, newAuth); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建UUID认证失败: %v", err) } l.Infof("[mergeUserAccounts] UUID认证创建成功, authId: %s", newAuth.Id) } } else { // 其他认证类型,创建新认证 newAuth := &model.UserAuth{ Id: uuid.NewString(), UserId: targetUserID, AuthType: currentAuthType, AuthKey: currentAuthKey, } if _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, newAuth); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建认证失败: %v", err) } l.Infof("[mergeUserAccounts] 认证创建成功, authId: %s, authType: %s", newAuth.Id, currentAuthType) } } // 2) 业务数据迁移:迁移订单和报告到目标用户 l.Infof("[mergeUserAccounts] 开始迁移业务数据, sourceUserId: %s, targetUserId: %s", sourceUserID, targetUserID) if err := l.svcCtx.OrderModel.UpdateUserIDWithSession(ctx, session, sourceUserID, targetUserID); err != nil { return errors.Wrapf(err, "迁移订单失败") } l.Infof("[mergeUserAccounts] 订单迁移完成") if err := l.svcCtx.QueryModel.UpdateUserIDWithSession(ctx, session, sourceUserID, targetUserID); err != nil { return errors.Wrapf(err, "迁移报告失败") } l.Infof("[mergeUserAccounts] 报告迁移完成") // 3) 删除临时用户 l.Infof("[mergeUserAccounts] 开始删除临时用户, sourceUserId: %s", sourceUserID) sourceUser, err := l.svcCtx.UserModel.FindOne(ctx, sourceUserID) if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找源用户失败: %v", err) } if err := l.svcCtx.UserModel.Delete(ctx, session, sourceUser.Id); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "删除源用户失败: %v", err) } l.Infof("[mergeUserAccounts] 临时用户删除成功, sourceUserId: %s", sourceUserID) l.Infof("[mergeUserAccounts] 账号合并完成, targetUserId: %s", targetUserID) return nil }