365 lines
14 KiB
Go
365 lines
14 KiB
Go
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))
|
||
|
||
// 【重要】先删除旧的认证记录,再创建新用户,避免唯一键冲突
|
||
// 从原用户删除当前设备的认证方式 (按 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
|
||
}
|