package user import ( "context" "database/sql" "fmt" "os" "time" "ycc-server/app/main/api/internal/svc" "ycc-server/app/main/api/internal/types" "ycc-server/app/main/model" "ycc-server/common/ctxdata" "ycc-server/common/xerr" "ycc-server/pkg/lzkit/crypto" "github.com/google/uuid" "github.com/pkg/errors" "github.com/zeromicro/go-zero/core/logx" "github.com/zeromicro/go-zero/core/stores/redis" "github.com/zeromicro/go-zero/core/stores/sqlx" ) type BindMobileLogic struct { logx.Logger ctx context.Context svcCtx *svc.ServiceContext } func NewBindMobileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindMobileLogic { return &BindMobileLogic{ Logger: logx.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *BindMobileLogic) BindMobile(req *types.BindMobileReq) (resp *types.BindMobileResp, err error) { // 从上下文中获取当前登录态的用户声明(可能是临时用户或正式用户),包含UserId/AuthType/AuthKey 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) } // 当前登录用户信息(用于后续合并/绑定) currentUserID := claims.UserId currentAuthType := claims.AuthType currentAuthKey := claims.AuthKey // 加密手机号(所有手机号以密文存储) 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读取并比对) if os.Getenv("ENV") != "development" { redisKey := fmt.Sprintf("%s:%s", "bindMobile", encryptedMobile) cacheCode, err := l.svcCtx.Redis.Get(redisKey) if err != nil { if errors.Is(err, redis.Nil) { return nil, errors.Wrapf(xerr.NewErrMsg("验证码已过期"), "") } return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "读取验证码失败: %v", err) } if cacheCode != req.Code { return nil, errors.Wrapf(xerr.NewErrMsg("验证码不正确"), "") } } // 通过加密后的手机号查找目标用户(手机号用户视为正式用户) targetUser, err := l.svcCtx.UserModel.FindOneByMobile(l.ctx, sql.NullString{String: encryptedMobile, Valid: true}) if err != nil && !errors.Is(err, model.ErrNotFound) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找用户失败: %v", err) } var finalUserID string if targetUser == nil { // 手机号不存在:直接将当前用户升级为正式用户(写入mobile与mobile认证) 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 { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "更新手机号失败: %v", err) } // 记录mobile认证(确保后续可通过手机号登录) if _, err := l.svcCtx.UserAuthModel.Insert(l.ctx, nil, &model.UserAuth{Id: uuid.NewString(), UserId: finalUserID, AuthType: model.UserAuthTypeMobile, AuthKey: encryptedMobile}); err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建手机号认证失败: %v", err) } // 发放token(userType会根据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() return &types.BindMobileResp{AccessToken: token, AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter}, nil } // 手机号已存在:进入账号合并或快捷登录流程 finalUserID = targetUser.Id // 保护校验:若将不同用户进行合并,确保源用户不存在代理记录(临时用户不应为代理) if 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 { 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 { 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() return &types.BindMobileResp{AccessToken: token, AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter}, nil } // 微信唯一性约束(按类型): // - H5 与 小程序各自只能绑定一个 openid(互不影响) if currentAuthType == model.UserAuthTypeWxh5OpenID { wxh5Auth, err := l.svcCtx.UserAuthModel.FindOneByUserIdAuthType(l.ctx, finalUserID, model.UserAuthTypeWxh5OpenID) if err != nil && !errors.Is(err, model.ErrNotFound) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找认证信息失败: %v", err) } if wxh5Auth != nil && wxh5Auth.AuthKey != currentAuthKey { return nil, errors.Wrapf(xerr.NewErrMsg("该手机号已绑定其他H5微信号"), "") } } if currentAuthType == model.UserAuthTypeWxMiniOpenID { wxminiAuth, err := l.svcCtx.UserAuthModel.FindOneByUserIdAuthType(l.ctx, finalUserID, model.UserAuthTypeWxMiniOpenID) if err != nil && !errors.Is(err, model.ErrNotFound) { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找认证信息失败: %v", err) } if wxminiAuth != nil && wxminiAuth.AuthKey != currentAuthKey { return nil, errors.Wrapf(xerr.NewErrMsg("该手机号已绑定其他小程序微信号"), "") } } // 事务处理: // - 将当前登录态的认证(uuid / 微信openid 等)绑定到目标手机号用户(finalUserID) // - 将源用户(currentUserID)的业务数据(订单、报告)迁移到目标用户,避免数据分裂 // - 对源用户执行软删除,清理无主临时账号,保持数据一致性 // 注意:所有步骤必须在同一个事务中执行,任何一步失败均会回滚,确保原子性 err = l.svcCtx.UserModel.Trans(l.ctx, func(ctx context.Context, session sqlx.Session) error { // 1) 认证绑定处理(UUID替换策略) if currentAuthType == model.UserAuthTypeUUID { targetUUIDAuth, _ := l.svcCtx.UserAuthModel.FindOneByUserIdAuthType(ctx, finalUserID, model.UserAuthTypeUUID) if existingAuth != nil && existingAuth.UserId != finalUserID { if targetUUIDAuth != nil { if targetUUIDAuth.AuthKey != currentAuthKey { if err := l.svcCtx.UserAuthModel.Delete(ctx, session, existingAuth.Id); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "删除旧UUID认证失败: %v", err) } 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.Delete(ctx, session, existingAuth.Id); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "删除重复UUID认证失败: %v", err) } } } else { existingAuth.UserId = finalUserID if _, err := l.svcCtx.UserAuthModel.Update(ctx, session, existingAuth); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "迁移UUID认证失败: %v", err) } } } else if existingAuth == nil { 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 { _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, &model.UserAuth{Id: uuid.NewString(), UserId: finalUserID, AuthType: currentAuthType, AuthKey: currentAuthKey}) if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建UUID认证失败: %v", err) } } } } else { if existingAuth != nil && existingAuth.UserId != finalUserID { existingAuth.UserId = finalUserID 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 { _, err := l.svcCtx.UserAuthModel.Insert(ctx, session, &model.UserAuth{Id: uuid.NewString(), UserId: finalUserID, AuthType: currentAuthType, AuthKey: currentAuthKey}) if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "创建认证绑定失败: %v", err) } } } // 2) 业务数据迁移 // 当源用户与目标用户不同时,迁移源用户的订单与报告归属到finalUserID,避免合并后数据仍挂在旧用户 if currentUserID != finalUserID { if err := l.svcCtx.OrderModel.UpdateUserIDWithSession(ctx, session, currentUserID, finalUserID); err != nil { return err } if err := l.svcCtx.QueryModel.UpdateUserIDWithSession(ctx, session, currentUserID, finalUserID); err != nil { return err } // 3) 源用户软删除 // 软删源用户(通常为临时用户),防止遗留无效账号;软删可保留历史痕迹,满足审计需求 currentUser, err := l.svcCtx.UserModel.FindOne(ctx, currentUserID) if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找当前用户失败: %v", err) } if err := l.svcCtx.UserModel.Delete(ctx, session, currentUser.Id); err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "删除当前用户失败: %v", err) } } return nil }) if err != nil { return nil, err } // 合并完成后生成token(userType会根据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() return &types.BindMobileResp{AccessToken: token, AccessExpire: now + l.svcCtx.Config.JwtAuth.AccessExpire, RefreshAfter: now + l.svcCtx.Config.JwtAuth.RefreshAfter}, nil }