This commit is contained in:
2026-02-28 19:29:08 +08:00
parent a88154e8e2
commit 7e2dda986f
2 changed files with 91 additions and 86 deletions

View File

@@ -36,24 +36,26 @@ func NewBindMobileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindMo
}
func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.BindMobileResp, err error) {
// 从上下文中获取当前登录态的用户声明可能是临时用户或正式用户包含UserId/AuthType/AuthKey
l.Infof("[BindMobile] 开始 | mobile: %s", req.Mobile)
claims, err := ctxdata.GetClaimsFromCtx(l.ctx)
if err != nil && !errors.Is(err, ctxdata.ErrNoInCtx) {
l.Errorf("[BindMobile] 获取登录态失败: %v", err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "获取用户信息失败: %v", err)
}
// 当前登录用户信息(用于后续合并/绑定)
currentUserID := claims.UserId
currentAuthType := claims.AuthType
currentAuthKey := claims.AuthKey
l.Infof("[BindMobile] 当前登录态 | userId: %s, authType: %s", currentUserID, currentAuthType)
// 加密手机号(所有手机号以密文存储)
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)
}
// 非开发环境下校验短信验证码从Redis读取并比对
l.Infof("[BindMobile] 手机号加密完成 | mobile: %s, encryptedMobile: %s", req.Mobile, encryptedMobile)
if os.Getenv("ENV") != "development" {
redisKey := fmt.Sprintf("%s:%s", "bindMobile", encryptedMobile)
cacheCode, err := l.svcCtx.Redis.Get(redisKey)
@@ -68,59 +70,62 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind
}
}
// 通过加密后的手机号查找目标用户(手机号用户视为正式用户
// 通过加密后的手机号查找目标用户(可能命中缓存
targetUser, err := l.svcCtx.UserModel.FindOneByMobile(l.ctx, sql.NullString{String: encryptedMobile, Valid: true})
if err != nil && !errors.Is(err, model.ErrNotFound) {
l.Errorf("[BindMobile] FindOneByMobile 查询失败: %v", err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找用户失败: %v", err)
}
var finalUserID string
if targetUser == nil {
// 手机号不存在:直接将当前用户升级为正式用户写入mobile与mobile认证)
finalUserID = currentUserID
l.Infof("[BindMobile] FindOneByMobile 未命中 | targetUser=nil, encryptedMobile: %s | 分支: 手机号不存在当前用户升级为正式用户", encryptedMobile)
finalUserID := currentUserID
currentUser, err := l.svcCtx.UserModel.FindOne(l.ctx, currentUserID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找当前用户失败: %v", err)
}
currentUser.Mobile = sql.NullString{String: encryptedMobile, Valid: true}
if _, err := l.svcCtx.UserModel.Update(l.ctx, nil, currentUser); err != nil {
l.Errorf("[BindMobile] 更新 user.mobile 失败 | userId: %s, err: %v", currentUserID, err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新手机号失败: %v", err)
}
// 记录mobile认证确保后续可通过手机号登录
l.Infof("[BindMobile] user.mobile 已更新 | userId: %s", currentUserID)
if _, err := l.svcCtx.UserAuthModel.Insert(l.ctx, nil, &model.UserAuth{Id: uuid.NewString(), UserId: finalUserID, AuthType: model.UserAuthTypeMobile, AuthKey: encryptedMobile}); err != nil {
l.Errorf("[BindMobile] 插入 user_auth(mobile) 失败 | userId: %s, err: %v", finalUserID, err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err)
}
// 使 UserModel.FindOneByMobile 的缓存失效,避免后续流程(如代理注册)仍命中“手机号不存在”的旧缓存
l.Infof("[BindMobile] user_auth(mobile) 已插入 | userId: %s", finalUserID)
userMobileCacheKey := fmt.Sprintf("cache:bdqr:user:mobile:%v", sql.NullString{String: encryptedMobile, Valid: true})
_, _ = l.svcCtx.Redis.DelCtx(l.ctx, userMobileCacheKey)
// 发放tokenuserType会根据mobile字段动态计算
l.Infof("[BindMobile] 已失效 FindOneByMobile 缓存 | key: %s | 后续代理注册将能正确按手机号查到本用户", userMobileCacheKey)
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成Token失败: %v", err)
}
now := time.Now().Unix()
l.Infof("[BindMobile] 完成-升级正式用户 | userId: %s, mobile: %s", finalUserID, req.Mobile)
return &types.BindMobileResp{AccessToken: token, AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter}, nil
}
// 手机号已存在进入账号合并或快捷登录流程
finalUserID = targetUser.Id
// 保护校验:若将不同用户进行合并,确保源用户不存在代理记录(临时用户不应为代理)
l.Infof("[BindMobile] FindOneByMobile 命中 | targetUserId: %s, encryptedMobile: %s | 分支: 手机号已存在进入合并或快捷登录", targetUser.Id, encryptedMobile)
finalUserID := targetUser.Id
if currentUserID != finalUserID {
l.Infof("[BindMobile] 当前用户与手机号用户不同 | currentUserID: %s, finalUserID: %s, 需合并或绑定", currentUserID, finalUserID)
agent, err := l.svcCtx.AgentModel.FindOneByUserId(l.ctx, currentUserID)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查询代理信息失败: %v", err)
}
if agent != nil {
l.Infof("[BindMobile] 拒绝: 源用户已是代理 | currentUserID: %s, agentId: %s", currentUserID, agent.Id)
return nil, errors.Wrapf(xerr.NewErrMsg("账号数据异常:源用户存在代理记录,请联系技术支持"), "")
}
}
// 查找当前登录态使用的认证例如uuid或微信openid是否已存在
existingAuth, err := l.svcCtx.UserAuthModel.FindOneByAuthTypeAuthKey(l.ctx, currentAuthType, currentAuthKey)
if err != nil && !errors.Is(err, model.ErrNotFound) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找认证信息失败: %v", err)
}
// 如果当前认证已属于目标手机号用户直接发放token无需合并
if existingAuth != nil && existingAuth.UserId == finalUserID {
l.Infof("[BindMobile] 当前认证已属于目标用户,直接发 token | finalUserID: %s", finalUserID)
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成Token失败: %v", err)
@@ -128,6 +133,7 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind
now := time.Now().Unix()
return &types.BindMobileResp{AccessToken: token, AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter}, nil
}
l.Infof("[BindMobile] 需合并/绑定认证 | existingAuth: %v, finalUserID: %s, authType: %s", existingAuth != nil, finalUserID, currentAuthType)
// 微信唯一性约束(按类型):
// - H5 与 小程序各自只能绑定一个 openid互不影响
@@ -150,11 +156,7 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind
}
}
// 事务处理:
// - 将当前登录态的认证uuid / 微信openid 等绑定到目标手机号用户finalUserID
// - 将源用户currentUserID的业务数据订单、报告迁移到目标用户避免数据分裂
// - 对源用户执行软删除,清理无主临时账号,保持数据一致性
// 注意:所有步骤必须在同一个事务中执行,任何一步失败均会回滚,确保原子性
l.Infof("[BindMobile] 开始事务: 合并认证与数据 | sourceUserId: %s, targetUserId: %s", currentUserID, finalUserID)
err = l.svcCtx.UserModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error {
// 1) 认证绑定处理UUID替换策略
if currentAuthType == model.UserAuthTypeUUID {
@@ -232,14 +234,16 @@ func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.Bind
return nil
})
if err != nil {
l.Errorf("[BindMobile] 事务失败 | sourceUserId: %s, targetUserId: %s, err: %v", currentUserID, finalUserID, err)
return nil, err
}
l.Infof("[BindMobile] 事务完成-合并成功 | finalUserID: %s", finalUserID)
// 合并完成后生成tokenuserType会根据mobile字段动态计算
token, err := l.svcCtx.UserService.GeneralUserToken(l.ctx, finalUserID)
if err != nil {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.SERVER_COMMON_ERROR), "生成Token失败: %v", err)
}
now := time.Now().Unix()
l.Infof("[BindMobile] 完成-合并后发 token | finalUserID: %s, mobile: %s", finalUserID, req.Mobile)
return &types.BindMobileResp{AccessToken: token, AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter}, nil
}