hi-server/internal/logic/public/user/accountMergeHelper.go
shanshanzhong 4d913c1728
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m26s
修复缓存
2026-03-06 21:58:29 -08:00

183 lines
5.7 KiB
Go

package user
import (
"context"
"time"
modelUser "github.com/perfect-panel/server/internal/model/user"
"github.com/perfect-panel/server/internal/svc"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type accountMergeResult struct {
OwnerUserID int64
DeviceUserID int64
MovedDevices []modelUser.Device
RemovedSubscribes []modelUser.Subscribe
}
type accountMergeHelper struct {
ctx context.Context
svcCtx *svc.ServiceContext
}
func newAccountMergeHelper(ctx context.Context, svcCtx *svc.ServiceContext) *accountMergeHelper {
return &accountMergeHelper{
ctx: ctx,
svcCtx: svcCtx,
}
}
func (h *accountMergeHelper) mergeIntoOwner(ownerUserID, deviceUserID int64, source string) (*accountMergeResult, error) {
if ownerUserID == 0 || deviceUserID == 0 {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "merge user id is empty")
}
if ownerUserID == deviceUserID {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "cannot merge same user id")
}
result := &accountMergeResult{
OwnerUserID: ownerUserID,
DeviceUserID: deviceUserID,
}
// Capture device user's auth methods BEFORE the transaction migrates them
deviceAuthMethods, _ := h.svcCtx.UserModel.FindUserAuthMethods(h.ctx, deviceUserID)
err := h.svcCtx.DB.WithContext(h.ctx).Transaction(func(tx *gorm.DB) error {
var owner modelUser.User
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id = ?", ownerUserID).
First(&owner).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "owner user %d not found", ownerUserID)
}
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query owner user failed")
}
var device modelUser.User
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
Where("id = ?", deviceUserID).
First(&device).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "device user %d not found", deviceUserID)
}
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device user failed")
}
exitHelper := newFamilyExitHelper(h.ctx, h.svcCtx)
if err := exitHelper.removeUserFromActiveFamily(tx, deviceUserID, false); err != nil {
return err
}
removedSubscribes, err := clearMemberSubscribes(tx, deviceUserID)
if err != nil {
return err
}
result.RemovedSubscribes = removedSubscribes
var devices []modelUser.Device
if err := tx.Where("user_id = ?", deviceUserID).Find(&devices).Error; err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device list failed")
}
if len(devices) > 0 {
if err := tx.Model(&modelUser.Device{}).
Where("user_id = ?", deviceUserID).
Updates(map[string]interface{}{
"user_id": ownerUserID,
"updated_at": time.Now(),
}).Error; err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device ownership failed")
}
}
result.MovedDevices = devices
if err := tx.Model(&modelUser.AuthMethods{}).
Where("user_id = ?", deviceUserID).
Update("user_id", ownerUserID).Error; err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "migrate auth methods failed")
}
if err := tx.Model(&modelUser.User{}).
Where("id = ?", deviceUserID).
Update("enable", false).Error; err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "disable device user failed")
}
if err := tx.Where("id = ?", deviceUserID).Delete(&modelUser.User{}).Error; err != nil {
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "soft delete device user failed")
}
return nil
})
if err != nil {
return nil, err
}
if err := h.clearCaches(result, deviceAuthMethods); err != nil {
return nil, err
}
logger.WithContext(h.ctx).Infow("device account merged into owner",
logger.Field("owner_user_id", ownerUserID),
logger.Field("device_user_id", deviceUserID),
logger.Field("moved_devices", len(result.MovedDevices)),
logger.Field("removed_subscribes", len(result.RemovedSubscribes)),
logger.Field("source", source),
)
return result, nil
}
func (h *accountMergeHelper) clearCaches(result *accountMergeResult, deviceAuthMethods []*modelUser.AuthMethods) error {
if result == nil {
return nil
}
// Fetch owner user with AuthMethods for proper cache key generation
var users []*modelUser.User
if u, err := h.svcCtx.UserModel.FindOne(h.ctx, result.OwnerUserID); err == nil {
users = append(users, u)
}
// For device user, FindOne won't have AuthMethods anymore (migrated in tx),
// so we build a minimal User with the pre-captured auth methods
deviceUser := &modelUser.User{Id: result.DeviceUserID}
if len(deviceAuthMethods) > 0 {
authMethods := make([]modelUser.AuthMethods, len(deviceAuthMethods))
for i, am := range deviceAuthMethods {
authMethods[i] = *am
}
deviceUser.AuthMethods = authMethods
}
users = append(users, deviceUser)
if len(users) > 0 {
if err := h.svcCtx.UserModel.ClearUserCache(h.ctx, users...); err != nil {
return err
}
}
if len(result.MovedDevices) > 0 {
deviceModels := make([]*modelUser.Device, 0, len(result.MovedDevices))
for i := range result.MovedDevices {
device := result.MovedDevices[i]
deviceModels = append(deviceModels, &device)
}
if err := h.svcCtx.UserModel.ClearDeviceCache(h.ctx, deviceModels...); err != nil {
return err
}
}
if len(result.RemovedSubscribes) > 0 {
familyHelper := newFamilyBindingHelper(h.ctx, h.svcCtx)
if err := familyHelper.clearRemovedMemberSubscribeCache(result.RemovedSubscribes); err != nil {
return err
}
}
return nil
}