feat: merge user
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user