hi-server/internal/logic/public/user/deleteAccountLogic.go
shanshanzhong a98fcbfe73
Some checks failed
Build docker and publish / build (20.15.1) (push) Has been cancelled
下载
2026-01-23 03:48:30 -08:00

198 lines
7.1 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
}
// 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
}