161 lines
5.8 KiB
Markdown
161 lines
5.8 KiB
Markdown
# 设备管理系统 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
|