feat: merge user

This commit is contained in:
zzw
2026-10-20 13:09:55 +08:00
parent cb5644d1f7
commit 08fd224f09
4 changed files with 356 additions and 18 deletions
@@ -247,6 +247,12 @@ func issueLoginToken(ctx context.Context, jwtAuth *config.JWTAuth, row *dao.User
if err := fillSessionBizIds(ctx, &session, row.Id, client); err != nil {
return nil, err
}
// C 端多小程序共用同一 token:已登录则续期并更新会话;门店/销售仍发新 token
if renewed, ok := tryRenewUserLogin(jwtAuth, session, client); ok {
return renewed, nil
}
ret, err := buildJwtToken(jwtAuth, session)
if err != nil {
return nil, err
+73 -18
View File
@@ -3,11 +3,14 @@ package logic
import (
"context"
"lone-services/pkg/modelbase"
"lone-services/pkg/utils"
"lone-services/pkg/validate"
analysispb "lone-services/rpc/analysis/pb"
user "lone-services/rpc/user/pb"
userconfig "lone-services/services/user/internal/config"
"lone-services/services/user/internal/dao"
"lone-services/services/user/internal/model"
"lone-services/services/user/internal/svc"
"lone-services/services/user/validator"
@@ -46,7 +49,8 @@ func (l *BindMobileLogic) BindMobile(in *user.BindMobileReq) (*user.Response, er
return outResponse(utils.ErrorParams, "端编码不能为空"), nil
}
if _, err := loadEnabledClient(loginUser.ClientCode); err != nil {
client, err := loadEnabledClient(loginUser.ClientCode)
if err != nil {
l.Errorf("bind mobile load client: %v", err)
return failResponse(utils.Fail), nil
}
@@ -76,27 +80,63 @@ func (l *BindMobileLogic) BindMobile(in *user.BindMobileReq) (*user.Response, er
return failResponse(utils.Fail), nil
}
_, updateErr := NewInternalUpdateMobileLogic(l.ctx, l.svcCtx).InternalUpdateMobile(&user.InternalUpdateMobileReq{
UserId: loginUser.ID,
Mobile: mobile,
})
if updateErr != nil {
l.Errorf("bind mobile update: %v", updateErr)
if st, ok := status.FromError(updateErr); ok {
switch st.Code() {
case codes.InvalidArgument:
return outResponse(utils.ErrorParams, st.Message()), nil
case codes.AlreadyExists:
return failResponse(utils.ErrorDataIsExist), nil
case codes.NotFound:
return failResponse(utils.ErrorNotFund), nil
}
}
encryptMobile, encErr := utils.EncryptPhone(mobile)
if encErr != nil {
l.Errorf("bind mobile encrypt: %v", encErr)
return failResponse(utils.ErrorEncryptAesError), nil
}
finalUserId := loginUser.ID
occupied, occErr := l.findUserByMobile(encryptMobile)
if occErr != nil {
l.Errorf("bind mobile find by mobile: %v", occErr)
return failResponse(utils.Fail), nil
}
if occupied != nil && occupied.Id > utils.NumberZero && occupied.Id != loginUser.ID {
if err := mergeUserInto(l.ctx, l.svcCtx, loginUser.ID, occupied.Id); err != nil {
l.Errorf("bind mobile merge: %v", err)
return failResponse(utils.Fail), nil
}
finalUserId = occupied.Id
row, userErr := loadEnabledUser(finalUserId)
if userErr != nil {
l.Errorf("bind mobile load merged user: %v", userErr)
return failResponse(utils.Fail), nil
}
session := toSession(*row, mobile, client)
if fillErr := fillSessionBizIds(l.ctx, &session, finalUserId, client); fillErr != nil {
l.Errorf("bind mobile fill session: %v", fillErr)
return failResponse(utils.Fail), nil
}
if remapErr := remaponUserLoginAfterMerge(l.svcCtx.JWT, loginUser.ID, finalUserId, session); remapErr != nil {
l.Errorf("bind mobile remapon login: %v", remapErr)
return failResponse(utils.Fail), nil
}
} else {
_, updateErr := NewInternalUpdateMobileLogic(l.ctx, l.svcCtx).InternalUpdateMobile(&user.InternalUpdateMobileReq{
UserId: loginUser.ID,
Mobile: mobile,
})
if updateErr != nil {
l.Errorf("bind mobile update: %v", updateErr)
if st, ok := status.FromError(updateErr); ok {
switch st.Code() {
case codes.InvalidArgument:
return outResponse(utils.ErrorParams, st.Message()), nil
case codes.AlreadyExists:
return failResponse(utils.ErrorDataIsExist), nil
case codes.NotFound:
return failResponse(utils.ErrorNotFund), nil
}
}
return failResponse(utils.Fail), nil
}
}
if loginUser.ClientCode == clientCodeUserSkin {
if err := l.markAnalysisRegistered(loginUser.ID, loginUser.ClientCode, loginUser.BizId); err != nil {
if err := l.markAnalysisRegistered(finalUserId, loginUser.ClientCode, utils.NumberZero); err != nil {
l.Errorf("bind mobile set registered: %v", err)
return failResponse(utils.Fail), nil
}
@@ -105,6 +145,21 @@ func (l *BindMobileLogic) BindMobile(in *user.BindMobileReq) (*user.Response, er
return okResponse(nil), nil
}
func (l *BindMobileLogic) findUserByMobile(encryptMobile string) (*dao.UserRow, error) {
var occupied dao.UserRow
userModel := model.UserModel{}.Init()
err := userModel.GetOne(modelbase.Params{
Eq: map[string]string{"mobile": encryptMobile},
}, &occupied)
if err != nil {
return nil, err
}
if occupied.Id < utils.NumberOne {
return nil, nil
}
return &occupied, nil
}
func (l *BindMobileLogic) markAnalysisRegistered(userId int64, clientCode string, bizId int64) error {
if bizId < utils.NumberOne {
if err := ensureClientBizUser(l.ctx, l.svcCtx, userId, clientCode); err != nil {
+148
View File
@@ -0,0 +1,148 @@
package logic
import (
"context"
"strconv"
"lone-services/pkg/modelbase"
"lone-services/pkg/utils"
"lone-services/services/user/internal/dao"
"lone-services/services/user/internal/model"
"lone-services/services/user/internal/svc"
"gorm.io/gorm"
)
// mergeUserInto 将 fromUserId 的凭证与端关联合并到 toUserId,并删除 fromUserId。
func mergeUserInto(ctx context.Context, svcCtx *svc.ServiceContext, fromUserId, toUserId int64) error {
if fromUserId < utils.NumberOne || toUserId < utils.NumberOne || fromUserId == toUserId {
return nil
}
return svcCtx.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := mergeCredentials(tx, fromUserId, toUserId); err != nil {
return err
}
if err := mergeUserClients(tx, fromUserId, toUserId); err != nil {
return err
}
if err := deleteUserDetail(tx, fromUserId); err != nil {
return err
}
return deleteUser(tx, fromUserId)
})
}
func mergeCredentials(tx *gorm.DB, fromUserId, toUserId int64) error {
credModel := model.UserCredentialModel{}.Init()
credModel.Base = credModel.Base.WithTX(tx)
var creds []dao.UserCredential
if err := credModel.Items(modelbase.Params{
Eq: map[string]string{"user_id": strconv.FormatInt(fromUserId, utils.NumberTen)},
}, &creds); err != nil {
return err
}
for _, cred := range creds {
var exist dao.UserCredential
if err := credModel.GetOne(modelbase.Params{
Eq: map[string]string{
"credential_type": cred.CredentialType,
"identifier": cred.Identifier,
"subject_type": cred.SubjectType,
},
}, &exist); err != nil {
return err
}
if exist.Id > utils.NumberZero {
// 目标用户(或其他用户)已有相同凭证,丢弃来源侧重复行
if _, err := credModel.Del(modelbase.Params{
Eq: map[string]string{"id": strconv.FormatInt(cred.Id, utils.NumberTen)},
}, &dao.UserCredential{}); err != nil {
return err
}
continue
}
if _, err := credModel.Edit(modelbase.Params{
Eq: map[string]string{"id": strconv.FormatInt(cred.Id, utils.NumberTen)},
}, map[string]interface{}{
"user_id": toUserId,
}); err != nil {
return err
}
}
return nil
}
func mergeUserClients(tx *gorm.DB, fromUserId, toUserId int64) error {
clientModel := model.UserClientModel{}.Init()
clientModel.Base = clientModel.Base.WithTX(tx)
var clients []dao.UserClient
if err := clientModel.Items(modelbase.Params{
Eq: map[string]string{"user_id": strconv.FormatInt(fromUserId, utils.NumberTen)},
}, &clients); err != nil {
return err
}
for _, uc := range clients {
var exist dao.UserClient
if err := clientModel.GetOne(modelbase.Params{
Eq: map[string]string{
"user_id": strconv.FormatInt(toUserId, utils.NumberTen),
"client_code": uc.ClientCode,
},
}, &exist); err != nil {
return err
}
if exist.Id > utils.NumberZero {
updates := map[string]interface{}{}
if exist.BizId < utils.NumberOne && uc.BizId > utils.NumberZero {
updates["biz_id"] = uc.BizId
}
if exist.Status != dao.StatusEnabled {
updates["status"] = dao.StatusEnabled
}
if len(updates) > utils.NumberZero {
if _, err := clientModel.Edit(modelbase.Params{
Eq: map[string]string{"id": strconv.FormatInt(exist.Id, utils.NumberTen)},
}, updates); err != nil {
return err
}
}
if _, err := clientModel.Del(modelbase.Params{
Eq: map[string]string{"id": strconv.FormatInt(uc.Id, utils.NumberTen)},
}, &dao.UserClient{}); err != nil {
return err
}
continue
}
if _, err := clientModel.Edit(modelbase.Params{
Eq: map[string]string{"id": strconv.FormatInt(uc.Id, utils.NumberTen)},
}, map[string]interface{}{
"user_id": toUserId,
}); err != nil {
return err
}
}
return nil
}
func deleteUserDetail(tx *gorm.DB, userId int64) error {
detailModel := model.UserDetailModel{}.Init()
detailModel.Base = detailModel.Base.WithTX(tx)
_, err := detailModel.Del(modelbase.Params{
Eq: map[string]string{"user_id": strconv.FormatInt(userId, utils.NumberTen)},
}, &dao.UserDetail{})
return err
}
func deleteUser(tx *gorm.DB, userId int64) error {
userModel := model.UserModel{}.Init()
userModel.Base = userModel.Base.WithTX(tx)
_, err := userModel.Del(modelbase.Params{
Eq: map[string]string{"id": strconv.FormatInt(userId, utils.NumberTen)},
}, &dao.UserRow{})
return err
}
+129
View File
@@ -141,3 +141,132 @@ func setLogin(jwtAuth *config.JWTAuth, accessToken, refreshToken string, session
}
return utils.Ok
}
// tryRenewUserLogin 仅对 C 端(subject_type=user)复用已有 token 并续期;
// 门店/销售不走此逻辑。成功时更新会话中的 client_code/biz_id 等字段。
func tryRenewUserLogin(jwtAuth *config.JWTAuth, session dao.UserLoginSession, client *dao.Client) (*dao.Token, bool) {
if jwtAuth == nil || jwtAuth.Access == nil || jwtAuth.Refresh == nil {
return nil, false
}
if loginCacheType(client) != utils.LoginTypeUser {
return nil, false
}
if redis.Client == nil || session.Id < utils.NumberOne {
return nil, false
}
ctx := context.Background()
loginType := utils.LoginTypeUser
infoKey := utils.GetLoginKeysKey(loginType, strconv.FormatInt(session.Id, utils.NumberTen))
oldAuth, err := redis.Client.Get(ctx, infoKey).Result()
if err != nil {
return nil, false
}
var loginInfo utils.LoginRedis
if err := jsoniter.Unmarshal([]byte(oldAuth), &loginInfo); err != nil {
return nil, false
}
if len(loginInfo.Token) <= utils.NumberOne || len(loginInfo.Refresh) <= utils.NumberOne {
return nil, false
}
tokenKey := utils.GetLoginKey(loginType, loginInfo.Token)
if _, err := redis.Client.Get(ctx, tokenKey).Result(); err != nil {
return nil, false
}
accessExpire := time.Duration(jwtAuth.Access.TTL) * time.Second
refreshExpire := time.Duration(jwtAuth.Refresh.TTL) * time.Second
userStr, _ := jsoniter.Marshal(session)
if err := redis.Client.Set(ctx, tokenKey, userStr, accessExpire).Err(); err != nil {
return nil, false
}
loginInfo.Info = userStr
newStr, _ := jsoniter.Marshal(loginInfo)
if err := redis.Client.Set(ctx, infoKey, newStr, refreshExpire).Err(); err != nil {
return nil, false
}
refreshKey := utils.GetLoginRefreshKey(loginType, loginInfo.Refresh)
if err := redis.Client.Set(ctx, refreshKey, strconv.FormatInt(session.Id, utils.NumberTen), refreshExpire).Err(); err != nil {
return nil, false
}
return &dao.Token{
AccessToken: loginInfo.Token,
RefreshToken: loginInfo.Refresh,
Info: session,
}, true
}
func remaponUserLoginAfterMerge(jwtAuth *config.JWTAuth, fromUserId, toUserId int64, session dao.UserLoginSession) error {
if jwtAuth == nil || jwtAuth.Access == nil || jwtAuth.Refresh == nil {
return nil
}
if redis.Client == nil || fromUserId < utils.NumberOne || toUserId < utils.NumberOne {
return nil
}
if session.Id != toUserId {
session.Id = toUserId
}
ctx := context.Background()
loginType := utils.LoginTypeUser
fromInfoKey := utils.GetLoginKeysKey(loginType, strconv.FormatInt(fromUserId, utils.NumberTen))
oldAuth, err := redis.Client.Get(ctx, fromInfoKey).Result()
if err != nil {
return nil
}
var loginInfo utils.LoginRedis
if err := jsoniter.Unmarshal([]byte(oldAuth), &loginInfo); err != nil {
return err
}
if len(loginInfo.Token) <= utils.NumberOne {
return nil
}
accessExpire := time.Duration(jwtAuth.Access.TTL) * time.Second
refreshExpire := time.Duration(jwtAuth.Refresh.TTL) * time.Second
userStr, _ := jsoniter.Marshal(session)
tokenKey := utils.GetLoginKey(loginType, loginInfo.Token)
if err := redis.Client.Set(ctx, tokenKey, userStr, accessExpire).Err(); err != nil {
return err
}
toInfoKey := utils.GetLoginKeysKey(loginType, strconv.FormatInt(toUserId, utils.NumberTen))
if oldTo, err := redis.Client.Get(ctx, toInfoKey).Result(); err == nil {
var toLogin utils.LoginRedis
if uErr := jsoniter.Unmarshal([]byte(oldTo), &toLogin); uErr == nil {
if len(toLogin.Token) > utils.NumberOne && toLogin.Token != loginInfo.Token {
_ = redis.Client.Del(ctx, utils.GetLoginKey(loginType, toLogin.Token)).Err()
}
if len(toLogin.Refresh) > utils.NumberOne && toLogin.Refresh != loginInfo.Refresh {
_ = redis.Client.Del(ctx, utils.GetLoginRefreshKey(loginType, toLogin.Refresh)).Err()
}
}
}
loginInfo.Info = userStr
newStr, _ := jsoniter.Marshal(loginInfo)
if err := redis.Client.Set(ctx, toInfoKey, newStr, refreshExpire).Err(); err != nil {
return err
}
if fromUserId != toUserId {
_ = redis.Client.Del(ctx, fromInfoKey).Err()
}
if len(loginInfo.Refresh) > utils.NumberOne {
refreshKey := utils.GetLoginRefreshKey(loginType, loginInfo.Refresh)
if err := redis.Client.Set(ctx, refreshKey, strconv.FormatInt(toUserId, utils.NumberTen), refreshExpire).Err(); err != nil {
return err
}
}
return nil
}