hi-server/docs/设备移出和邀请码优化/ALIGNMENT_设备移出和邀请码优化.md

5.8 KiB
Raw Blame History

设备管理系统 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

// 第 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 执行时

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 迁移设备后,踢出设备的旧连接:

// 迁移设备到邮箱用户后
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在解绑时遍历所有用户查找设备

修改 KickDeviceunbindDeviceLogic 逻辑不依赖用户ID查找设备。

方案3清理 Redis 缓存使旧 Token 失效

确保设备转移后,旧的 session 和 device 缓存被清理:

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