package user import ( "context" "crypto/rand" "encoding/hex" "fmt" "github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/constant" "github.com/perfect-panel/server/pkg/logger" "github.com/perfect-panel/server/pkg/uuidx" "github.com/perfect-panel/server/pkg/xerr" "github.com/pkg/errors" "gorm.io/gorm" ) type DeleteAccountLogic struct { logger.Logger ctx context.Context svcCtx *svc.ServiceContext } // NewDeleteAccountLogic 创建注销账号逻辑实例 func NewDeleteAccountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteAccountLogic { return &DeleteAccountLogic{ Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } // DeleteAccount 注销当前设备账号逻辑 (改为精准解绑) func (l *DeleteAccountLogic) DeleteAccount() (resp *types.DeleteAccountResponse, err error) { // 获取当前用户 currentUser, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) if !ok { return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } // 获取当前调用设备 ID currentDeviceId, _ := l.ctx.Value(constant.CtxKeyDeviceID).(int64) resp = &types.DeleteAccountResponse{} var newUserId int64 // 如果没有识别到设备 ID (可能是旧版 Token),则执行安全注销:仅清除 Session if currentDeviceId == 0 { l.Infow("未识别到设备 ID,仅清理当前会话", logger.Field("user_id", currentUser.Id)) l.clearCurrentSession(currentUser.Id) resp.Success = true resp.Message = "会话已清除" return resp, nil } // 开始数据库事务 err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error { // 1. 查找当前设备 var currentDevice user.Device if err := tx.Where("id = ? AND user_id = ?", currentDeviceId, currentUser.Id).First(¤tDevice).Error; err != nil { l.Infow("当前请求设备记录不存在或归属不匹配", logger.Field("device_id", currentDeviceId), logger.Field("error", err.Error())) return nil // 不抛错,直接走清理 Session 流程 } // 2. 检查用户是否有其他认证方式 (如邮箱) 或 其他设备 var authMethodsCount int64 tx.Model(&user.AuthMethods{}).Where("user_id = ?", currentUser.Id).Count(&authMethodsCount) var devicesCount int64 tx.Model(&user.Device{}).Where("user_id = ?", currentUser.Id).Count(&devicesCount) // 判定是否是主账号解绑:如果除了当前设备外,还有邮箱或其他设备,则只解绑当前设备 isMainAccount := authMethodsCount > 1 || devicesCount > 1 if isMainAccount { l.Infow("主账号解绑,仅迁移当前设备", logger.Field("user_id", currentUser.Id), logger.Field("device_id", currentDeviceId)) // 为当前设备创建新用户并迁移 newUser, err := l.registerUserAndDevice(tx, currentDevice.Identifier, currentDevice.Ip, currentDevice.UserAgent) if err != nil { return err } newUserId = newUser.Id // 从原用户删除当前设备的认证方式 (按 Identifier 准确删除) if err := tx.Where("user_id = ? AND auth_type = ? AND auth_identifier = ?", currentUser.Id, "device", currentDevice.Identifier).Delete(&user.AuthMethods{}).Error; err != nil { return errors.Wrap(err, "删除原设备认证失败") } // 从原用户删除当前设备记录 if err := tx.Where("id = ?", currentDeviceId).Delete(&user.Device{}).Error; err != nil { return errors.Wrap(err, "删除原设备记录失败") } } else { l.Infow("纯设备账号注销,执行物理删除并重置", logger.Field("user_id", currentUser.Id), logger.Field("device_id", currentDeviceId)) // 完全删除原用户相关资产 tx.Where("user_id = ?", currentUser.Id).Delete(&user.AuthMethods{}) tx.Where("user_id = ?", currentUser.Id).Delete(&user.Device{}) tx.Where("user_id = ?", currentUser.Id).Delete(&user.Subscribe{}) tx.Delete(&user.User{}, currentUser.Id) // 重新注册一个新用户 newUser, err := l.registerUserAndDevice(tx, currentDevice.Identifier, currentDevice.Ip, currentDevice.UserAgent) if err != nil { return err } newUserId = newUser.Id } return nil }) if err != nil { return nil, err } // 最终清理当前 Session l.clearCurrentSession(currentUser.Id) resp.Success = true resp.Message = "注销成功" resp.UserId = newUserId resp.Code = 200 return resp, nil } // clearCurrentSession 清理当前请求的会话 func (l *DeleteAccountLogic) clearCurrentSession(userId int64) { if sessionId, ok := l.ctx.Value(constant.CtxKeySessionID).(string); ok && sessionId != "" { sessionKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) _ = l.svcCtx.Redis.Del(l.ctx, sessionKey).Err() // 从用户会话集合中移除当前session sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userId) _ = l.svcCtx.Redis.ZRem(l.ctx, sessionsKey, sessionId).Err() } } // generateReferCode 生成推荐码 func generateReferCode() string { bytes := make([]byte, 4) rand.Read(bytes) return hex.EncodeToString(bytes) } func (l *DeleteAccountLogic) registerUserAndDevice(tx *gorm.DB, identifier, ip, userAgent string) (*user.User, error) { // 1. 创建新用户 userInfo := &user.User{ Salt: "default", OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase, } if err := tx.Create(userInfo).Error; err != nil { l.Errorw("failed to create user", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create user failed: %v", err) } // 2. 更新推荐码 userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) if err := tx.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil { l.Errorw("failed to update refer code", logger.Field("user_id", userInfo.Id), logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update refer code failed: %v", err) } // 3. 创建设备认证方式 authMethod := &user.AuthMethods{ UserId: userInfo.Id, AuthType: "device", AuthIdentifier: identifier, Verified: true, } if err := tx.Create(authMethod).Error; err != nil { l.Errorw("failed to create device auth method", logger.Field("user_id", userInfo.Id), logger.Field("identifier", identifier), logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device auth method failed: %v", err) } // 4. 插入设备记录 deviceInfo := &user.Device{ Ip: ip, UserId: userInfo.Id, UserAgent: userAgent, Identifier: identifier, Enabled: true, Online: false, } if err := tx.Create(deviceInfo).Error; err != nil { l.Errorw("failed to insert device", logger.Field("user_id", userInfo.Id), logger.Field("identifier", identifier), logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert device failed: %v", err) } return userInfo, nil }