hi-server/internal/logic/auth/bindDeviceLogic.go
shanshanzhong fde3210a88
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m41s
feat(用户): 实现邮箱绑定功能并返回登录凭证
修改绑定邮箱接口返回登录凭证,优化用户数据迁移流程
添加用户缓存清理逻辑,确保设备绑定后数据一致性
完善邮箱验证和绑定逻辑的注释和错误处理
2025-10-23 10:07:59 -07:00

500 lines
19 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 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
}