All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 8m5s
- bindEmailWithVerificationLogic: 新邮箱路径改为创建独立 email user + joinFamily - familyBindingHelper: clearMemberSubscribes → transferMemberSubscribesToOwner,订阅转移给 owner 而非删除 - accountMergeHelper: 同步更新调用点 Co-Authored-By: claude-flow <ruv@ruv.net>
183 lines
5.7 KiB
Go
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 := transferMemberSubscribesToOwner(tx, deviceUserID, ownerUserID)
|
|
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
|
|
}
|