diff --git a/services/user/internal/logic/authHelper.go b/services/user/internal/logic/authHelper.go index 95ba07d..832da92 100644 --- a/services/user/internal/logic/authHelper.go +++ b/services/user/internal/logic/authHelper.go @@ -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 diff --git a/services/user/internal/logic/bindMobileLogic.go b/services/user/internal/logic/bindMobileLogic.go index 38502f2..688ecb8 100644 --- a/services/user/internal/logic/bindMobileLogic.go +++ b/services/user/internal/logic/bindMobileLogic.go @@ -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 { diff --git a/services/user/internal/logic/mergeUser.go b/services/user/internal/logic/mergeUser.go new file mode 100644 index 0000000..38a72b9 --- /dev/null +++ b/services/user/internal/logic/mergeUser.go @@ -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 +} diff --git a/services/user/internal/logic/response.go b/services/user/internal/logic/response.go index 33733f7..95bdc17 100644 --- a/services/user/internal/logic/response.go +++ b/services/user/internal/logic/response.go @@ -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 +}