# 设备管理系统 Bug 分析 - 最终确认版 ## 场景还原 ### 用户操作流程 1. **设备A** 最初通过设备登录(DeviceLogin),系统自动创建用户1 + 设备A记录 2. **设备B** 最初也通过设备登录,系统自动创建用户2 + 设备B记录 3. **设备A** 绑定邮箱 xxx@example.com,用户1变为"邮箱+设备"用户 4. **设备B** 绑定**同一个邮箱** xxx@example.com - 系统发现邮箱已存在,执行设备转移 - 设备B 从用户2迁移到用户1 - 用户2被删除 - 现在用户1拥有:设备A + 设备B + 邮箱认证 5. **在设备A上操作**,从设备列表移除设备B 6. **问题**:设备B没有被踢下线,仍然能使用 --- ## 数据流分析 ### 绑定邮箱后的状态(第4步后) ``` User 表: ┌─────┬───────────────┐ │ Id │ 用户1 │ └─────┴───────────────┘ user_device 表: ┌─────────────┬───────────┐ │ Identifier │ UserId │ ├─────────────┼───────────┤ │ device-a │ 用户1 │ │ device-b │ 用户1 │ <- 设备B迁移到用户1 └─────────────┴───────────┘ user_auth_methods 表: ┌────────────┬────────────────┬───────────┐ │ AuthType │ AuthIdentifier │ UserId │ ├────────────┼────────────────┼───────────┤ │ device │ device-a │ 用户1 │ │ device │ device-b │ 用户1 │ │ email │ xxx@email.com │ 用户1 │ └────────────┴────────────────┴───────────┘ DeviceManager (内存 WebSocket 连接): ┌───────────────────────────────────────────────────┐ │ userDevices sync.Map │ ├───────────────────────────────────────────────────┤ │ 用户1 -> [Device{DeviceID="device-a", ...}] │ │ 用户2 -> [Device{DeviceID="device-b", ...}] ❌ │ <- 问题!设备B的连接仍在用户2名下 └───────────────────────────────────────────────────┘ ``` ### 问题根源 **设备B绑定邮箱时**(`bindEmailWithVerificationLogic.go`): - ✅ 数据库:设备B的 `UserId` 被更新为用户1 - ❌ 内存:`DeviceManager` 中设备B的 WebSocket 连接仍然在**用户2**名下 - ❌ 缓存:`device:device-b` -> 旧的 sessionId(可能关联用户2) **解绑设备B时**(`unbindDeviceLogic.go`): ```go // 第 48 行:验证设备属于当前用户 if device.UserId != u.Id { // device.UserId=用户1, u.Id=用户1, 验证通过 return errors.Wrapf(...) } // 第 123 行:踢出设备 l.svcCtx.DeviceManager.KickDevice(u.Id, identifier) // KickDevice(用户1, "device-b") ``` **KickDevice 执行时**: ```go func (dm *DeviceManager) KickDevice(userID int64, deviceID string) { val, ok := dm.userDevices.Load(userID) // 查找用户1的设备列表 // 用户1的设备列表只有 device-a // 找不到 device-b!因为 device-b 的连接还在用户2名下 } ``` --- ## 根本原因总结 | 操作 | 数据库 | DeviceManager 内存 | Redis 缓存 | |------|--------|-------------------|------------| | 设备B绑定邮箱 | ✅ 更新 UserId | ❌ 未更新 | ❌ 未清理 | | 解绑设备B | ✅ 创建新用户 | ❌ 找不到设备 | ✅ 尝试清理 | **核心问题**:设备绑定邮箱(转移用户)时,没有更新 `DeviceManager` 中的连接归属。 --- ## 修复方案 ### 方案1:在绑定邮箱时踢出旧连接(推荐) 在 `bindEmailWithVerificationLogic.go` 迁移设备后,踢出设备的旧连接: ```go // 迁移设备到邮箱用户后 for _, device := range devices { // 更新设备归属 device.UserId = emailUserId err = l.svcCtx.UserModel.UpdateDevice(l.ctx, device) // 新增:踢出旧连接(使用原用户ID) l.svcCtx.DeviceManager.KickDevice(u.Id, device.Identifier) } ``` ### 方案2:在解绑时遍历所有用户查找设备 修改 `KickDevice` 或 `unbindDeviceLogic` 逻辑,不依赖用户ID查找设备。 ### 方案3:清理 Redis 缓存使旧 Token 失效 确保设备转移后,旧的 session 和 device 缓存被清理: ```go deviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, device.Identifier) if sessionId, _ := l.svcCtx.Redis.Get(ctx, deviceCacheKey).Result(); sessionId != "" { sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) l.svcCtx.Redis.Del(ctx, deviceCacheKey, sessionIdCacheKey) } ``` --- ## 推荐修复策略 **双管齐下**: 1. **修复 `bindEmailWithVerificationLogic.go`**: - 设备转移后立即踢出旧连接 - 清理旧用户的缓存 2. **修复 `unbindDeviceLogic.go`**(防御性编程): - 补充 `user_sessions` 清理逻辑(参考 `deleteUserDeviceLogic.go`) --- ## 涉及文件 | 文件 | 修改内容 | |------|----------| | `internal/logic/public/user/bindEmailWithVerificationLogic.go` | 设备转移后踢出旧连接 | | `internal/logic/public/user/unbindDeviceLogic.go` | 补充 user_sessions 清理 | --- ## 验收标准 1. 设备B绑定邮箱后,设备B的旧连接被踢出 2. 从设备A解绑设备B后,设备B立即被踢下线 3. 设备B的 Token 失效,无法继续调用 API