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 }