All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m41s
修改绑定邮箱接口返回登录凭证,优化用户数据迁移流程 添加用户缓存清理逻辑,确保设备绑定后数据一致性 完善邮箱验证和绑定逻辑的注释和错误处理
500 lines
19 KiB
Go
500 lines
19 KiB
Go
package auth
|
||
|
||
import (
|
||
"context"
|
||
|
||
"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"
|
||
)
|
||
|
||
// BindDeviceLogic 设备绑定逻辑处理器
|
||
// 负责处理设备与用户的绑定关系,包括新设备创建、设备重新绑定、数据迁移等核心功能
|
||
// 主要解决设备用户与邮箱用户之间的数据合并问题
|
||
type BindDeviceLogic struct {
|
||
logger.Logger // 日志记录器,用于记录操作过程和错误信息
|
||
ctx context.Context // 上下文,用于传递请求信息和控制超时
|
||
svcCtx *svc.ServiceContext // 服务上下文,包含数据库连接、配置等依赖
|
||
}
|
||
|
||
// NewBindDeviceLogic 创建设备绑定逻辑处理器实例
|
||
// 参数:
|
||
// - ctx: 请求上下文,用于传递请求信息和控制超时
|
||
// - svcCtx: 服务上下文,包含数据库连接、配置等依赖
|
||
// 返回:
|
||
// - *BindDeviceLogic: 设备绑定逻辑处理器实例
|
||
func NewBindDeviceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindDeviceLogic {
|
||
return &BindDeviceLogic{
|
||
Logger: logger.WithContext(ctx),
|
||
ctx: ctx,
|
||
svcCtx: svcCtx,
|
||
}
|
||
}
|
||
|
||
// BindDeviceToUser 将设备绑定到用户
|
||
// 这是设备绑定的核心入口函数,处理三种主要场景:
|
||
// 1. 新设备绑定:设备不存在时,创建新的设备记录并绑定到当前用户
|
||
// 2. 设备已绑定当前用户:更新设备的IP和UserAgent信息
|
||
// 3. 设备已绑定其他用户:执行设备重新绑定,可能涉及数据迁移
|
||
//
|
||
// 参数:
|
||
// - identifier: 设备唯一标识符(如设备ID、MAC地址等)
|
||
// - ip: 设备当前IP地址
|
||
// - userAgent: 设备的User-Agent信息
|
||
// - currentUserId: 当前要绑定的用户ID
|
||
//
|
||
// 返回:
|
||
// - error: 绑定过程中的错误,nil表示成功
|
||
//
|
||
// 注意:如果设备已绑定其他用户且该用户为"纯设备用户"(无其他认证方式),
|
||
// 将触发完整的数据迁移并禁用原用户
|
||
func (l *BindDeviceLogic) BindDeviceToUser(identifier, ip, userAgent string, currentUserId int64) error {
|
||
// 检查设备标识符是否为空
|
||
if identifier == "" {
|
||
// 没有提供设备标识符,跳过绑定过程
|
||
return nil
|
||
}
|
||
|
||
// 记录设备绑定开始的日志
|
||
l.Infow("binding device to user",
|
||
logger.Field("identifier", identifier),
|
||
logger.Field("user_id", currentUserId),
|
||
logger.Field("ip", ip),
|
||
)
|
||
|
||
// 第一步:查询设备是否已存在
|
||
// 通过设备标识符查找现有的设备记录
|
||
deviceInfo, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, identifier)
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
// 设备不存在,创建新设备记录并绑定到当前用户
|
||
// 这是场景1:新设备绑定
|
||
return l.createDeviceForUser(identifier, ip, userAgent, currentUserId)
|
||
}
|
||
// 数据库查询出错,记录错误并返回
|
||
l.Errorw("failed to query device",
|
||
logger.Field("identifier", identifier),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query device failed: %v", err.Error())
|
||
}
|
||
|
||
// 第二步:检查设备是否已绑定到当前用户
|
||
if deviceInfo.UserId == currentUserId {
|
||
// 设备已绑定到当前用户,只需更新IP和UserAgent
|
||
// 这是场景2:设备已绑定当前用户
|
||
l.Infow("device already bound to current user, updating info",
|
||
logger.Field("identifier", identifier),
|
||
logger.Field("user_id", currentUserId),
|
||
)
|
||
deviceInfo.Ip = ip
|
||
deviceInfo.UserAgent = userAgent
|
||
if err := l.svcCtx.UserModel.UpdateDevice(l.ctx, deviceInfo); err != nil {
|
||
l.Errorw("failed to update device",
|
||
logger.Field("identifier", identifier),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err.Error())
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// 第三步:设备绑定到其他用户,需要重新绑定
|
||
// 这是场景3:设备已绑定其他用户,需要执行重新绑定逻辑
|
||
l.Infow("device bound to another user, rebinding",
|
||
logger.Field("identifier", identifier),
|
||
logger.Field("old_user_id", deviceInfo.UserId),
|
||
logger.Field("new_user_id", currentUserId),
|
||
)
|
||
|
||
// 调用重新绑定函数,可能涉及数据迁移
|
||
return l.rebindDeviceToNewUser(deviceInfo, ip, userAgent, currentUserId)
|
||
}
|
||
|
||
// createDeviceForUser 为用户创建新的设备记录
|
||
// 当设备标识符在系统中不存在时调用此函数,执行完整的设备创建流程
|
||
// 包括创建设备认证方法记录和设备信息记录,确保数据一致性
|
||
//
|
||
// 参数:
|
||
// - identifier: 设备唯一标识符
|
||
// - ip: 设备IP地址
|
||
// - userAgent: 设备User-Agent信息
|
||
// - userId: 要绑定的用户ID
|
||
//
|
||
// 返回:
|
||
// - error: 创建过程中的错误,nil表示成功
|
||
//
|
||
// 注意:此函数使用数据库事务确保认证方法和设备记录的原子性创建
|
||
func (l *BindDeviceLogic) createDeviceForUser(identifier, ip, userAgent string, userId int64) error {
|
||
// 记录开始创建设备的日志
|
||
l.Infow("creating new device for user",
|
||
logger.Field("identifier", identifier),
|
||
logger.Field("user_id", userId),
|
||
)
|
||
|
||
// 使用数据库事务确保数据一致性
|
||
// 如果任何一步失败,整个操作都会回滚
|
||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||
// 第一步:创建设备认证方法记录
|
||
// 在auth_methods表中记录该设备的认证信息
|
||
authMethod := &user.AuthMethods{
|
||
UserId: userId, // 关联的用户ID
|
||
AuthType: "device", // 认证类型为设备认证
|
||
AuthIdentifier: identifier, // 设备标识符
|
||
Verified: true, // 设备认证默认为已验证状态
|
||
}
|
||
if err := db.Create(authMethod).Error; err != nil {
|
||
l.Errorw("failed to create device auth method",
|
||
logger.Field("user_id", userId),
|
||
logger.Field("identifier", identifier),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device auth method failed: %v", err)
|
||
}
|
||
|
||
// 第二步:创建设备信息记录
|
||
// 在device表中记录设备的详细信息
|
||
deviceInfo := &user.Device{
|
||
Ip: ip, // 设备IP地址
|
||
UserId: userId, // 关联的用户ID
|
||
UserAgent: userAgent, // 设备User-Agent信息
|
||
Identifier: identifier, // 设备唯一标识符
|
||
Enabled: true, // 设备默认启用状态
|
||
Online: false, // 设备默认离线状态
|
||
}
|
||
if err := db.Create(deviceInfo).Error; err != nil {
|
||
l.Errorw("failed to create device",
|
||
logger.Field("user_id", userId),
|
||
logger.Field("identifier", identifier),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device failed: %v", err)
|
||
}
|
||
|
||
return nil
|
||
})
|
||
|
||
// 检查事务执行结果
|
||
if err != nil {
|
||
l.Errorw("device creation failed",
|
||
logger.Field("identifier", identifier),
|
||
logger.Field("user_id", userId),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
return err
|
||
}
|
||
|
||
// 清理用户缓存,确保新设备能正确显示在用户的设备列表中
|
||
userInfo, err := l.svcCtx.UserModel.FindOne(l.ctx, userId)
|
||
if err != nil {
|
||
l.Errorw("failed to find user for cache clearing",
|
||
logger.Field("user_id", userId),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
// 不因为缓存清理失败而返回错误,因为设备创建已经成功
|
||
} else {
|
||
if err := l.svcCtx.UserModel.ClearUserCache(l.ctx, userInfo); err != nil {
|
||
l.Errorw("failed to clear user cache",
|
||
logger.Field("user_id", userId),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
// 不因为缓存清理失败而返回错误,因为设备创建已经成功
|
||
} else {
|
||
l.Infow("cleared user cache after device creation",
|
||
logger.Field("user_id", userId),
|
||
)
|
||
}
|
||
}
|
||
|
||
// 记录设备创建成功的日志
|
||
l.Infow("device created successfully",
|
||
logger.Field("identifier", identifier),
|
||
logger.Field("user_id", userId),
|
||
)
|
||
|
||
return nil
|
||
}
|
||
|
||
// rebindDeviceToNewUser 将设备重新绑定到新用户
|
||
// 这是设备绑定合并逻辑的核心函数,处理设备从一个用户转移到另一个用户的复杂场景
|
||
// 主要解决"设备用户"与"邮箱用户"之间的数据合并问题
|
||
//
|
||
// 核心判断逻辑:
|
||
// 1. 如果原用户是"纯设备用户"(只有设备认证,无邮箱等其他认证方式):
|
||
// - 执行完整数据迁移(订单、订阅、余额、赠送金额)
|
||
// - 禁用原用户账户
|
||
// 2. 如果原用户有其他认证方式(如邮箱、手机等):
|
||
// - 只转移设备绑定关系
|
||
// - 保留原用户账户和数据
|
||
//
|
||
// 参数:
|
||
// - deviceInfo: 现有的设备信息记录
|
||
// - ip: 设备新的IP地址
|
||
// - userAgent: 设备新的User-Agent信息
|
||
// - newUserId: 要绑定到的新用户ID
|
||
//
|
||
// 返回:
|
||
// - error: 重新绑定过程中的错误,nil表示成功
|
||
//
|
||
// 注意:整个过程在数据库事务中执行,确保数据一致性
|
||
func (l *BindDeviceLogic) rebindDeviceToNewUser(deviceInfo *user.Device, ip, userAgent string, newUserId int64) error {
|
||
oldUserId := deviceInfo.UserId
|
||
|
||
// 使用数据库事务确保所有操作的原子性
|
||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||
// 第一步:查询原用户的所有认证方式
|
||
// 用于判断原用户是否为"纯设备用户"
|
||
var authMethods []user.AuthMethods
|
||
if err := db.Where("user_id = ?", oldUserId).Find(&authMethods).Error; err != nil {
|
||
l.Errorw("failed to query auth methods for old user",
|
||
logger.Field("old_user_id", oldUserId),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query auth methods failed: %v", err)
|
||
}
|
||
|
||
// 第二步:统计非设备认证方式的数量
|
||
// 如果只有设备认证,说明是"纯设备用户"
|
||
nonDeviceAuthCount := 0
|
||
for _, auth := range authMethods {
|
||
if auth.AuthType != "device" {
|
||
nonDeviceAuthCount++
|
||
}
|
||
}
|
||
|
||
// 第三步:根据用户类型执行不同的处理逻辑
|
||
if nonDeviceAuthCount == 0 {
|
||
// 原用户是"纯设备用户",执行完整的数据迁移和用户禁用
|
||
|
||
// 3.1 先执行数据迁移(订单、订阅、余额等)
|
||
if err := l.migrateUserData(db, oldUserId, newUserId); err != nil {
|
||
l.Errorw("failed to migrate user data",
|
||
logger.Field("old_user_id", oldUserId),
|
||
logger.Field("new_user_id", newUserId),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "migrate user data failed: %v", err)
|
||
}
|
||
|
||
// 3.2 禁用原用户账户
|
||
// 使用指针确保布尔值正确传递给GORM
|
||
falseVal := false
|
||
if err := db.Model(&user.User{}).Where("id = ?", oldUserId).Update("enable", &falseVal).Error; err != nil {
|
||
l.Errorw("failed to disable old user",
|
||
logger.Field("old_user_id", oldUserId),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "disable old user failed: %v", err)
|
||
}
|
||
|
||
l.Infow("disabled old user after data migration (no other auth methods)",
|
||
logger.Field("old_user_id", oldUserId),
|
||
logger.Field("new_user_id", newUserId),
|
||
)
|
||
} else {
|
||
// 原用户有其他认证方式,只转移设备绑定,保留原用户
|
||
l.Infow("old user has other auth methods, not disabling",
|
||
logger.Field("old_user_id", oldUserId),
|
||
logger.Field("non_device_auth_count", nonDeviceAuthCount),
|
||
)
|
||
}
|
||
|
||
// 第四步:更新设备认证方法的用户归属
|
||
// 将auth_methods表中的设备认证记录转移到新用户
|
||
if err := db.Model(&user.AuthMethods{}).
|
||
Where("auth_type = ? AND auth_identifier = ?", "device", deviceInfo.Identifier).
|
||
Update("user_id", newUserId).Error; err != nil {
|
||
l.Errorw("failed to update device auth method",
|
||
logger.Field("identifier", deviceInfo.Identifier),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device auth method failed: %v", err)
|
||
}
|
||
|
||
// 第五步:更新设备记录信息
|
||
// 更新device表中的设备信息,包括用户ID、IP、UserAgent等
|
||
deviceInfo.UserId = newUserId // 更新设备归属用户
|
||
deviceInfo.Ip = ip // 更新设备IP
|
||
deviceInfo.UserAgent = userAgent // 更新设备UserAgent
|
||
deviceInfo.Enabled = true // 确保设备处于启用状态
|
||
|
||
if err := db.Save(deviceInfo).Error; err != nil {
|
||
l.Errorw("failed to update device",
|
||
logger.Field("identifier", deviceInfo.Identifier),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update device failed: %v", err)
|
||
}
|
||
|
||
return nil
|
||
})
|
||
|
||
// 检查事务执行结果
|
||
if err != nil {
|
||
l.Errorw("device rebinding failed",
|
||
logger.Field("identifier", deviceInfo.Identifier),
|
||
logger.Field("old_user_id", oldUserId),
|
||
logger.Field("new_user_id", newUserId),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
return err
|
||
}
|
||
|
||
// 清理新用户的缓存,确保用户信息能正确更新
|
||
// 这是关键步骤:设备迁移后必须清理缓存,否则用户看到的还是旧的设备列表
|
||
newUser, err := l.svcCtx.UserModel.FindOne(l.ctx, newUserId)
|
||
if err != nil {
|
||
l.Errorw("failed to find new user for cache clearing",
|
||
logger.Field("new_user_id", newUserId),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
// 不因为缓存清理失败而返回错误,因为数据迁移已经成功
|
||
} else {
|
||
if err := l.svcCtx.UserModel.ClearUserCache(l.ctx, newUser); err != nil {
|
||
l.Errorw("failed to clear new user cache",
|
||
logger.Field("new_user_id", newUserId),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
// 不因为缓存清理失败而返回错误,因为数据迁移已经成功
|
||
} else {
|
||
l.Infow("cleared new user cache after device rebinding",
|
||
logger.Field("new_user_id", newUserId),
|
||
)
|
||
}
|
||
}
|
||
|
||
// 清理原用户的缓存(如果原用户没有被禁用的话)
|
||
oldUser, err := l.svcCtx.UserModel.FindOne(l.ctx, oldUserId)
|
||
if err != nil {
|
||
l.Errorw("failed to find old user for cache clearing",
|
||
logger.Field("old_user_id", oldUserId),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
// 不因为缓存清理失败而返回错误
|
||
} else {
|
||
if err := l.svcCtx.UserModel.ClearUserCache(l.ctx, oldUser); err != nil {
|
||
l.Errorw("failed to clear old user cache",
|
||
logger.Field("old_user_id", oldUserId),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
// 不因为缓存清理失败而返回错误
|
||
} else {
|
||
l.Infow("cleared old user cache after device rebinding",
|
||
logger.Field("old_user_id", oldUserId),
|
||
)
|
||
}
|
||
}
|
||
|
||
// 记录设备重新绑定成功的日志
|
||
l.Infow("device rebound successfully",
|
||
logger.Field("identifier", deviceInfo.Identifier),
|
||
logger.Field("old_user_id", oldUserId),
|
||
logger.Field("new_user_id", newUserId),
|
||
)
|
||
|
||
return nil
|
||
}
|
||
|
||
// migrateUserData 执行用户数据迁移
|
||
// 当"纯设备用户"需要合并到"邮箱用户"时,此函数负责迁移所有相关的用户数据
|
||
// 确保用户的历史数据(订单、订阅、余额等)不会丢失
|
||
//
|
||
// 迁移的数据类型包括:
|
||
// 1. 订单数据:将所有历史订单从原用户转移到新用户
|
||
// 2. 订阅数据:将所有订阅记录从原用户转移到新用户(注意:存在重复套餐问题)
|
||
// 3. 用户余额:将原用户的账户余额累加到新用户
|
||
// 4. 赠送金额:将原用户的赠送金额累加到新用户
|
||
//
|
||
// 参数:
|
||
// - db: 数据库事务对象,确保所有操作在同一事务中执行
|
||
// - oldUserId: 原用户ID(数据来源)
|
||
// - newUserId: 新用户ID(数据目标)
|
||
//
|
||
// 返回:
|
||
// - error: 迁移过程中的错误,nil表示成功
|
||
//
|
||
// 已知问题:
|
||
// - 订阅迁移采用简单的user_id更新,可能导致重复套餐问题
|
||
// - 缺少智能合并策略来处理相同套餐的订阅记录
|
||
// - 流量使用统计在订阅级别跟踪,不需要单独迁移
|
||
func (l *BindDeviceLogic) migrateUserData(db *gorm.DB, oldUserId, newUserId int64) error {
|
||
// 记录数据迁移开始的日志
|
||
l.Infow("starting user data migration",
|
||
logger.Field("old_user_id", oldUserId),
|
||
logger.Field("new_user_id", newUserId),
|
||
)
|
||
|
||
// 第一步:迁移订单数据
|
||
// 将order表中所有属于原用户的订单转移到新用户
|
||
// 使用表名直接操作以提高性能
|
||
if err := db.Table("order").Where("user_id = ?", oldUserId).Update("user_id", newUserId).Error; err != nil {
|
||
l.Errorw("failed to migrate orders",
|
||
logger.Field("old_user_id", oldUserId),
|
||
logger.Field("new_user_id", newUserId),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
return err
|
||
}
|
||
|
||
// 第二步:迁移用户订阅数据
|
||
// 将subscribe表中所有属于原用户的订阅转移到新用户
|
||
// 注意:这里存在重复套餐问题,简单的user_id更新可能导致用户拥有多个相同的套餐订阅
|
||
// TODO: 实现智能合并策略,合并相同套餐的订阅记录
|
||
if err := db.Model(&user.Subscribe{}).Where("user_id = ?", oldUserId).Update("user_id", newUserId).Error; err != nil {
|
||
l.Errorw("failed to migrate user subscriptions",
|
||
logger.Field("old_user_id", oldUserId),
|
||
logger.Field("new_user_id", newUserId),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
return err
|
||
}
|
||
|
||
// 第三步:获取原用户的余额和赠送金额信息
|
||
// 需要先查询原用户的财务数据,然后累加到新用户
|
||
var oldUser user.User
|
||
if err := db.Where("id = ?", oldUserId).First(&oldUser).Error; err != nil {
|
||
l.Errorw("failed to get old user data",
|
||
logger.Field("old_user_id", oldUserId),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
return err
|
||
}
|
||
|
||
// 第四步:迁移用户余额和赠送金额
|
||
// 将原用户的余额和赠送金额累加到新用户账户
|
||
// 只有当原用户有余额或赠送金额时才执行更新操作
|
||
if oldUser.Balance > 0 || oldUser.GiftAmount > 0 {
|
||
if err := db.Model(&user.User{}).Where("id = ?", newUserId).Updates(map[string]interface{}{
|
||
"balance": gorm.Expr("balance + ?", oldUser.Balance), // 累加余额
|
||
"gift_amount": gorm.Expr("gift_amount + ?", oldUser.GiftAmount), // 累加赠送金额
|
||
}).Error; err != nil {
|
||
l.Errorw("failed to migrate user balance and gift",
|
||
logger.Field("old_user_id", oldUserId),
|
||
logger.Field("new_user_id", newUserId),
|
||
logger.Field("old_balance", oldUser.Balance),
|
||
logger.Field("old_gift", oldUser.GiftAmount),
|
||
logger.Field("error", err.Error()),
|
||
)
|
||
return err
|
||
}
|
||
}
|
||
|
||
// 注意事项说明:
|
||
// 1. 认证方法(auth_methods)不在此处迁移,由调用方单独处理
|
||
// 2. 流量使用统计(Upload/Download)在订阅级别跟踪,随订阅一起迁移
|
||
// 3. 其他用户相关表如需迁移,可在此处添加
|
||
|
||
// 记录数据迁移完成的日志
|
||
l.Infow("user data migration completed",
|
||
logger.Field("old_user_id", oldUserId),
|
||
logger.Field("new_user_id", newUserId),
|
||
logger.Field("migrated_balance", oldUser.Balance),
|
||
logger.Field("migrated_gift", oldUser.GiftAmount),
|
||
)
|
||
|
||
return nil
|
||
}
|