hi-server/internal/logic/public/user/deleteAccountLogic.go
2026-01-30 22:15:17 -08:00

365 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(&currentDevice).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))
// 【重要】先删除旧的认证记录,再创建新用户,避免唯一键冲突
// 从原用户删除当前设备的认证方式 (按 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, "删除原设备记录失败")
}
// 为当前设备创建新用户并迁移
newUser, err := l.registerUserAndDevice(tx, currentDevice.Identifier, currentDevice.Ip, currentDevice.UserAgent)
if err != nil {
return err
}
newUserId = newUser.Id
} 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
}
// DeleteAccountAll 注销账号逻辑 (全部解绑默认创建账号)
func (l *DeleteAccountLogic) DeleteAccountAll() (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
// 开始数据库事务
err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
// 1. 预先查找该用户下的所有设备记录 (因为稍后要迁移)
var userDevices []user.Device
if err := tx.Where("user_id = ?", currentUser.Id).Find(&userDevices).Error; err != nil {
l.Errorw("查询用户设备列表失败", logger.Field("user_id", currentUser.Id), logger.Field("error", err.Error()))
return err
}
// 如果没有识别到调用设备 ID记录日志但继续执行 (全量注销不应受限)
if currentDeviceId == 0 {
l.Infow("未识别到当前设备 ID将执行全量注销并尝试迁移所有已知设备", logger.Field("user_id", currentUser.Id), logger.Field("found_devices", len(userDevices)))
}
l.Infow("执行账号全量注销-迁移设备并删除旧数据", logger.Field("user_id", currentUser.Id), logger.Field("device_count", len(userDevices)))
// 2. 循环为每个设备创建新用户并迁移记录 (保留设备ID)
for _, dev := range userDevices {
// A. 创建新匿名用户
newUser, err := l.createAnonymousUser(tx)
if err != nil {
l.Errorw("为设备分配新用户主体失败", logger.Field("device_id", dev.Id), logger.Field("error", err.Error()))
return err
}
// B. 迁移设备记录 (Update user_id)
if err := tx.Model(&user.Device{}).Where("id = ?", dev.Id).Update("user_id", newUser.Id).Error; err != nil {
l.Errorw("迁移设备记录失败", logger.Field("device_id", dev.Id), logger.Field("error", err.Error()))
return errors.Wrap(err, "迁移设备记录失败")
}
// C. 迁移设备认证方式 (Update user_id)
if err := tx.Model(&user.AuthMethods{}).
Where("user_id = ? AND auth_type = ? AND auth_identifier = ?", currentUser.Id, "device", dev.Identifier).
Update("user_id", newUser.Id).Error; err != nil {
l.Errorw("迁移设备认证失败", logger.Field("device_id", dev.Id), logger.Field("error", err.Error()))
return errors.Wrap(err, "迁移设备认证失败")
}
// 如果是当前请求的设备,记录其新 UserID 返回给前端
if dev.Id == currentDeviceId || dev.Identifier == l.getIdentifierByDeviceID(userDevices, currentDeviceId) {
newUserId = newUser.Id
}
l.Infow("旧设备已迁移至新匿名账号",
logger.Field("old_user_id", currentUser.Id),
logger.Field("new_user_id", newUser.Id),
logger.Field("device_id", dev.Id),
logger.Field("identifier", dev.Identifier))
}
// 3. 删除旧账号的剩余数据
// 删除剩余的认证方式 (排除已迁移的device类型剩下的如email/mobile等)
// 注意刚才已经把由currentUser拥有的device类型auth都迁移走了所以这里直接删剩下的即可
if err := tx.Where("user_id = ?", currentUser.Id).Delete(&user.AuthMethods{}).Error; err != nil {
return errors.Wrap(err, "删除剩余认证方式失败")
}
// 设备记录已经全部迁移,理论上 user_id = currentUser.Id 的 device 应该没了,但为了保险可以删一下(或者是0)
if err := tx.Where("user_id = ?", currentUser.Id).Delete(&user.Device{}).Error; err != nil {
return errors.Wrap(err, "删除残留设备记录失败")
}
// 删除所有订阅
if err := tx.Where("user_id = ?", currentUser.Id).Delete(&user.Subscribe{}).Error; err != nil {
return errors.Wrap(err, "删除订阅失败")
}
// 删除用户主体
if err := tx.Delete(&user.User{}, currentUser.Id).Error; err != nil {
return errors.Wrap(err, "删除用户失败")
}
return nil
})
if err != nil {
return nil, err
}
// 最终清理所有 Session (踢掉所有设备)
l.clearAllSessions(currentUser.Id)
resp.Success = true
resp.Message = "注销成功"
resp.UserId = newUserId
resp.Code = 200
return resp, nil
}
// 辅助方法:通过 ID 查找 Identifier (以防 currentDeviceId 只是 ID)
func (l *DeleteAccountLogic) getIdentifierByDeviceID(devices []user.Device, id int64) string {
for _, d := range devices {
if d.Id == id {
return d.Identifier
}
}
return ""
}
// 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()
l.Infow("[SessionMonitor] 注销账号清除 Session",
logger.Field("user_id", userId),
logger.Field("session_id", sessionId))
}
}
// clearAllSessions 清理指定用户的所有会话
func (l *DeleteAccountLogic) clearAllSessions(userId int64) {
sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userId)
// 获取所有 session id
sessions, err := l.svcCtx.Redis.ZRange(l.ctx, sessionsKey, 0, -1).Result()
if err != nil {
l.Errorw("获取用户会话列表失败", logger.Field("user_id", userId), logger.Field("error", err.Error()))
return
}
// 删除每个 session 的详情 key
for _, sid := range sessions {
sessionKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sid)
_ = l.svcCtx.Redis.Del(l.ctx, sessionKey).Err()
// 同时尝试删除 detail key (如果存在)
detailKey := fmt.Sprintf("%s:detail:%s", config.SessionIdKey, sid)
_ = l.svcCtx.Redis.Del(l.ctx, detailKey).Err()
}
// 删除用户的 session 集合 key
_ = l.svcCtx.Redis.Del(l.ctx, sessionsKey).Err()
l.Infow("[SessionMonitor] 注销账号-清除所有Session", logger.Field("user_id", userId), logger.Field("count", len(sessions)))
}
// 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
}
// createAnonymousUser 创建一个新的匿名用户主体 (仅User表)
func (l *DeleteAccountLogic) createAnonymousUser(tx *gorm.DB) (*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)
}
return userInfo, nil
}