Files
bdqr-server/app/main/api/internal/logic/user/bindmobilelogic.go
2026-02-28 19:29:08 +08:00

250 lines
12 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package user
import (
"context"
"database/sql"
"fmt"
"os"
"time"
"bdqr-server/app/main/api/internal/svc"
"bdqr-server/app/main/api/internal/types"
"bdqr-server/app/main/model"
"bdqr-server/common/ctxdata"
"bdqr-server/common/xerr"
"bdqr-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) {
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)
}
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)
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) {
l.Errorf("[BindMobile] FindOneByMobile 查询失败: %v", err)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DB_ERROR), "查找用户失败: %v", err)
}
if targetUser == nil {
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)
}
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)
}
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)
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
}
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("账号数据异常:源用户存在代理记录,请联系技术支持"), "")
}
}
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)
}
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)
}
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互不影响
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("该手机号已绑定其他小程序微信号"), "")
}
}
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 {
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 {
l.Errorf("[BindMobile] 事务失败 | sourceUserId: %s, targetUserId: %s, err: %v", currentUserID, finalUserID, err)
return nil, err
}
l.Infof("[BindMobile] 事务完成-合并成功 | 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)
}
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
}