This commit is contained in:
2025-12-11 19:01:28 +08:00
parent cff3a32da4
commit a1859fdeb9
4 changed files with 300 additions and 84 deletions

4
.gitignore vendored
View File

@@ -21,8 +21,8 @@ data/*
# 打包出来的可执行文件
/app/api
/app/main/api/main
/app/main/api/debug
/app/main/api/test
/app/main/api/_debug
/app/main/api/_test
# 文档目录
documents/*

View File

@@ -41,7 +41,7 @@ Wxpay:
MchPublicKeyPath: "etc/merchant/pub_key.pem"
MchPlatformRAS: "5630D013C88EA348BF66E642B6C39AA0180D4B15"
NotifyUrl: "https://www.onecha.cn/api/v1/pay/wechat/callback"
RefundNotifyUrl: "https://www.onecha.cn/api/v1/wechat/refund_callback"
RefundNotifyUrl: "https://www.onecha.cn/api/v1/pay/wechat/refund_callback"
Applepay:
ProductionVerifyURL: "https://api.storekit.itunes.apple.com/inApps/v1/transactions/receipt"
SandboxVerifyURL: "https://api.storekit-sandbox.itunes.apple.com/inApps/v1/transactions/receipt"

View File

@@ -60,6 +60,35 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
}
}
// 获取当前登录态(可能为空)
claims, err := ctxdata.GetClaimsFromCtx(l.ctx)
if err != nil && !errors.Is(err, ctxdata.ErrNoInCtx) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败: %v", err)
}
// 前置检查:如果当前用户是正式用户(有手机号),进行拦截检查
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 != "" {
// 当前用户是正式用户,检查是否已是代理
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 {
return nil, errors.Wrapf(xerr.NewErrMsg("您已经是代理,不能重复注册"), "")
}
// 正式用户手机号必须匹配
if currentUser.Mobile.String != encryptedMobile {
return nil, errors.Wrapf(xerr.NewErrMsg("请输入当前账号的手机号码"), "")
}
}
}
// 验证邀请码是否有效
var inviteCodeModel *model.AgentInviteCode
inviteCodeModel, err = l.svcCtx.AgentInviteCodeModel.FindOneByCode(l.ctx, req.Referrer)
if err != nil && !errors.Is(err, model.ErrNotFound) {
@@ -77,56 +106,43 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
}
}
// 4. 使用事务处理注册
// 使用事务处理注册
var userID string
var agentID string
var agentLevel int64
err = l.svcCtx.AgentInviteCodeModel.Trans(l.ctx, func(transCtx context.Context, session sqlx.Session) error {
// 4.1 检查用户是否已存在
user, findUserErr := l.svcCtx.UserModel.FindOneByMobile(l.ctx, sql.NullString{String: encryptedMobile, Valid: true})
// 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 user == nil {
// 用户不存在,注册新用户
userID, err = l.svcCtx.UserService.RegisterUser(l.ctx, encryptedMobile)
if err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "注册用户失败: %v", err)
}
} else {
// 用户已存在,检查是否已是代理
existingAgent, err := l.svcCtx.AgentModel.FindOneByUserId(transCtx, user.Id)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败, %v", err)
}
if existingAgent != nil {
return errors.Wrapf(xerr.NewErrMsg("您已经是代理"), "")
}
// 检查用户是否有mobile绑定没有mobile则不能成为代理
// 如果是临时用户(微信环境下),需要先绑定手机号
claims, err := ctxdata.GetClaimsFromCtx(l.ctx)
if err == nil && claims != nil {
// 获取用户的mobile信息
if !user.Mobile.Valid || user.Mobile.String == "" {
// 临时用户无mobile不能直接成为代理需要先绑定mobile
return errors.Wrapf(xerr.NewErrMsg("请先绑定手机号后再申请成为代理"), "")
}
// 检查是否已绑定手机号认证(用于确保后续可通过手机号登录)
userAuth, err := l.svcCtx.UserAuthModel.FindOneByUserIdAuthType(l.ctx, user.Id, claims.AuthType)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询用户认证失败, %v", err)
}
if userAuth != nil && userAuth.AuthKey != claims.AuthKey {
return errors.Wrapf(xerr.NewErrMsg("该手机号已绑定其他微信号"), "")
}
}
userID = user.Id
// 2. 获取当前登录态信息
var currentUserID string
var currentAuthType string
var currentAuthKey string
if claims != nil {
currentUserID = claims.UserId
currentAuthType = claims.AuthType
currentAuthKey = claims.AuthKey
}
// 3. 根据目标用户是否存在,处理用户和认证
if targetUser == nil {
// 场景1: 手机号不存在
userID, err = l.handleMobileNotExists(transCtx, session, encryptedMobile, currentUserID)
if err != nil {
return err
}
} else {
// 场景2: 手机号已存在
userID, err = l.handleMobileExists(transCtx, session, targetUser, currentUserID, currentAuthType, currentAuthKey)
if err != nil {
return err
}
}
// 4. 处理邀请码和上级关系
var targetLevel int64
var parentAgentId string
if inviteCodeModel != nil {
@@ -156,7 +172,8 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
}
}
// 4.3 创建代理记录
// 5. 创建代理记录
newAgent := &model.Agent{Id: uuid.NewString(), UserId: userID, Level: targetLevel, Mobile: encryptedMobile}
if req.Region != "" {
newAgent.Region = sql.NullString{String: req.Region, Valid: true}
@@ -165,7 +182,7 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
newAgent.WechatId = sql.NullString{String: req.WechatId, Valid: true}
}
// 4.4 处理上级关系
// 6. 处理上级关系
if parentAgentId != "" {
// 查找上级代理
parentAgent, err := l.svcCtx.AgentModel.FindOne(transCtx, parentAgentId)
@@ -225,13 +242,13 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
}
}
// 4.5 初始化钱包
// 7. 初始化钱包
wallet := &model.AgentWallet{Id: uuid.NewString(), AgentId: agentID}
if _, err := l.svcCtx.AgentWalletModel.Insert(transCtx, session, wallet); err != nil {
return errors.Wrapf(err, "初始化钱包失败")
}
// 4.6 更新邀请码状态
// 8. 更新邀请码状态
// 钻石级别的邀请码只能使用一次,使用后立即失效
// 普通级别的邀请码可以无限使用,不更新状态
if targetLevel == 3 {
@@ -247,7 +264,7 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
}
}
// 4.7 记录邀请码使用历史(用于统计和查询)
// 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 {
@@ -263,7 +280,7 @@ func (l *RegisterByInviteCodeLogic) RegisterByInviteCode(req *types.RegisterByIn
return nil, err
}
// 5. 生成并返回token
// 10. 生成并返回token
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)
@@ -361,3 +378,195 @@ func (l *RegisterByInviteCodeLogic) allocateAgentCode(ctx context.Context, sessi
}
return next, nil
}
// handleMobileNotExists 处理手机号不存在的情况
func (l *RegisterByInviteCodeLogic) handleMobileNotExists(ctx context.Context, session sqlx.Session, encryptedMobile string, currentUserID string) (string, error) {
if currentUserID == "" {
// 场景1.1: 未登录 + 手机号不存在 -> 创建新用户
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)
}
// 创建 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)
}
return newUser.Id, nil
} else {
// 场景1.2: 已登录临时用户 + 手机号不存在 -> 升级为正式用户
// 前置检查已保证不是正式用户,所以这里一定是临时用户
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)
}
// 创建 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)
}
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
// 检查目标用户是否已是代理
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 {
return "", errors.Wrapf(xerr.NewErrMsg("该手机号已经是代理,不能重复注册"), "")
}
if currentUserID == "" {
// 场景2.1: 未登录 + 手机号存在 -> 直接使用目标用户(验证码已确认身份)
return userID, nil
} else if currentUserID == userID {
// 场景2.2: 已登录正式用户 + 手机号匹配 -> 直接使用
// 前置检查已保证手机号匹配且不是代理
return userID, nil
} else {
// 场景2.3: 已登录临时用户 + 手机号存在 -> 需要合并账号
// 前置检查已保证是临时用户(不是正式用户)
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) {
// 检查目标用户是否已有该认证除了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 {
// 目标用户已有该类型的其他认证,证明手机号绑定过其他微信等
if currentAuthType == model.UserAuthTypeWxh5OpenID {
return "", errors.Wrapf(xerr.NewErrMsg("该手机号已绑定其他微信号"), "")
}
if currentAuthType == model.UserAuthTypeWxMiniOpenID {
return "", errors.Wrapf(xerr.NewErrMsg("该手机号已绑定其他微信号"), "")
}
return "", errors.Wrapf(xerr.NewErrMsg("该手机号已绑定其他终端"), "")
}
}
// 查找当前认证
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 err := l.mergeUserAccounts(ctx, session, sourceUserID, targetUserID, currentAuthType, currentAuthKey, existingAuth); err != nil {
return "", err
}
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 {
// 1) 认证绑定处理
if existingAuth != nil && existingAuth.UserId != targetUserID {
// 认证存在但不属于目标用户,迁移到目标用户
if currentAuthType == model.UserAuthTypeUUID {
// UUID替换策略如果目标用户已有UUID认证替换UUID否则迁移认证
targetUUIDAuth, _ := l.svcCtx.UserAuthModel.FindOneByUserIdAuthType(ctx, targetUserID, model.UserAuthTypeUUID)
if targetUUIDAuth != nil {
// 目标用户已有UUID认证删除源认证并更新目标UUID
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)
}
}
} else {
// 目标用户没有UUID认证迁移源认证
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)
}
}
} else {
// 其他认证类型,直接迁移
existingAuth.UserId = targetUserID
if _, err := l.svcCtx.UserAuthModel.Update(ctx, session, existingAuth); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "迁移认证失败: %v", err)
}
}
} else if existingAuth == nil {
// 认证不存在,创建新认证
if currentAuthType == model.UserAuthTypeUUID {
// UUID特殊处理如果目标用户已有UUID认证更新UUID否则创建新认证
targetUUIDAuth, _ := l.svcCtx.UserAuthModel.FindOneByUserIdAuthType(ctx, targetUserID, model.UserAuthTypeUUID)
if targetUUIDAuth != nil {
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)
}
}
} else {
if _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, &model.UserAuth{
Id: uuid.NewString(),
UserId: targetUserID,
AuthType: currentAuthType,
AuthKey: currentAuthKey,
}); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建UUID认证失败: %v", err)
}
}
} else {
// 其他认证类型,创建新认证
if _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, &model.UserAuth{
Id: uuid.NewString(),
UserId: targetUserID,
AuthType: currentAuthType,
AuthKey: currentAuthKey,
}); err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建认证失败: %v", err)
}
}
}
// 2) 业务数据迁移:迁移订单和报告到目标用户
if err := l.svcCtx.OrderModel.UpdateUserIDWithSession(ctx, session, sourceUserID, targetUserID); err != nil {
return errors.Wrapf(err, "迁移订单失败")
}
if err := l.svcCtx.QueryModel.UpdateUserIDWithSession(ctx, session, sourceUserID, targetUserID); err != nil {
return errors.Wrapf(err, "迁移报告失败")
}
// 3) 删除临时用户
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)
}
return nil
}

View File

@@ -142,12 +142,10 @@ func (s *UserService) getAuthTypeByPlatform(platform string) string {
}
// RegisterUser 注册用户返回用户ID
// 传入手机号自动注册如果ctx存在临时用户则临时用户转为正式用户
// 只负责创建新用户(手机号不存在时),不处理合并逻辑
// 如果有临时用户claims会将临时用户的认证绑定到新用户
func (s *UserService) RegisterUser(ctx context.Context, mobile string) (string, error) {
claims, err := ctxdata.GetClaimsFromCtx(ctx)
if err != nil && !errors.Is(err, ctxdata.ErrNoInCtx) {
return "", err
}
// 检查手机号是否已存在
user, err := s.userModel.FindOneByMobile(ctx, sql.NullString{String: mobile, Valid: true})
if err != nil && !errors.Is(err, model.ErrNotFound) {
return "", err
@@ -155,51 +153,60 @@ func (s *UserService) RegisterUser(ctx context.Context, mobile string) (string,
if user != nil {
return "", errors.New("用户已注册")
}
// 普通注册
if claims == nil {
var userId string
err = s.userModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error {
user := &model.User{Id: uuid.NewString(), Mobile: sql.NullString{String: mobile, Valid: true}}
if _, userInsertErr := s.userModel.Insert(ctx, session, user); userInsertErr != nil {
return userInsertErr
}
userId = user.Id
_, userAuthInsertErr := s.userAuthModel.Insert(ctx, session, &model.UserAuth{Id: uuid.NewString(), UserId: userId, AuthType: model.UserAuthTypeMobile, AuthKey: mobile})
if userAuthInsertErr != nil {
return userAuthInsertErr
}
return nil
})
if err != nil {
return "", err
}
return userId, nil
}
// 双重判断是否已经注册根据mobile判断而不是userType
currentUser, err := s.userModel.FindOne(ctx, claims.UserId)
if err != nil && !errors.Is(err, model.ErrNotFound) {
// 获取当前登录态(可能为空
claims, err := ctxdata.GetClaimsFromCtx(ctx)
if err != nil && !errors.Is(err, ctxdata.ErrNoInCtx) {
return "", err
}
if currentUser != nil && currentUser.Mobile.Valid && currentUser.Mobile.String != "" {
return "", errors.New("用户已注册")
}
var userId string
// 临时用户绑定mobile转正式注册
err = s.userModel.Trans(ctx, func(ctx context.Context, session sqlx.Session) error {
// 创建新用户
user := &model.User{Id: uuid.NewString(), Mobile: sql.NullString{String: mobile, Valid: true}}
if _, userInsertErr := s.userModel.Insert(ctx, session, user); userInsertErr != nil {
return userInsertErr
}
userId = user.Id
_, userAuthInsertErr := s.userAuthModel.Insert(ctx, session, &model.UserAuth{Id: uuid.NewString(), UserId: userId, AuthType: model.UserAuthTypeMobile, AuthKey: mobile})
// 创建 mobile 认证
_, userAuthInsertErr := s.userAuthModel.Insert(ctx, session, &model.UserAuth{
Id: uuid.NewString(),
UserId: userId,
AuthType: model.UserAuthTypeMobile,
AuthKey: mobile,
})
if userAuthInsertErr != nil {
return userAuthInsertErr
}
tempUserBindErr := s.TempUserBindUser(ctx, session, userId)
if tempUserBindErr != nil {
return tempUserBindErr
// 如果有临时用户,将临时用户的认证绑定到新用户
if claims != nil {
// 检查临时用户是否已有该认证类型
existingAuth, err := s.userAuthModel.FindOneByAuthTypeAuthKey(ctx, claims.AuthType, claims.AuthKey)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return err
}
// 如果认证不存在,创建新的认证绑定
if existingAuth == nil {
_, err = s.userAuthModel.Insert(ctx, session, &model.UserAuth{
Id: uuid.NewString(),
UserId: userId,
AuthType: claims.AuthType,
AuthKey: claims.AuthKey,
})
if err != nil {
return err
}
} else if existingAuth.UserId != userId {
// 如果认证已存在但属于其他用户,迁移到新用户
existingAuth.UserId = userId
if _, err := s.userAuthModel.Update(ctx, session, existingAuth); err != nil {
return err
}
}
}
return nil
})
if err != nil {