118 lines
3.7 KiB
Markdown
118 lines
3.7 KiB
Markdown
# 设备移出和邀请码优化 - 共识文档(更新版)
|
||
|
||
## 需求概述
|
||
|
||
修复两个 Bug:
|
||
1. **Bug 1**:设备B绑定邮箱后被从设备A移除,设备B没有被踢下线
|
||
2. **Bug 2**:输入不存在的邀请码时,提示信息不友好
|
||
|
||
---
|
||
|
||
## Bug 1:设备移出后未自动退出
|
||
|
||
### 根本原因
|
||
|
||
设备B绑定邮箱(迁移到邮箱用户)时:
|
||
- ✅ 数据库更新了设备的 `UserId`
|
||
- ❌ `DeviceManager` 内存中设备B的 WebSocket 连接仍在**原用户**名下
|
||
- ❌ Redis 缓存中设备B的 session 未被清理
|
||
|
||
解绑设备B时,`KickDevice(用户1, "device-b")` 在用户1的设备列表中找不到 device-b(因为连接还在原用户名下)。
|
||
|
||
### 修复方案
|
||
|
||
**文件1:`bindEmailWithVerificationLogic.go`**
|
||
|
||
在设备迁移后,踢出旧连接并清理缓存:
|
||
|
||
```go
|
||
// 第 139-158 行之后添加
|
||
for _, device := range devices {
|
||
device.UserId = emailUserId
|
||
err = l.svcCtx.UserModel.UpdateDevice(l.ctx, device)
|
||
// ...existing code...
|
||
|
||
// 新增:踢出旧连接并清理缓存
|
||
l.svcCtx.DeviceManager.KickDevice(u.Id, device.Identifier)
|
||
|
||
deviceCacheKey := fmt.Sprintf("%v:%v", config.DeviceCacheKeyKey, device.Identifier)
|
||
if sessionId, _ := l.svcCtx.Redis.Get(l.ctx, deviceCacheKey).Result(); sessionId != "" {
|
||
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
|
||
_ = l.svcCtx.Redis.Del(l.ctx, deviceCacheKey).Err()
|
||
_ = l.svcCtx.Redis.Del(l.ctx, sessionIdCacheKey).Err()
|
||
}
|
||
}
|
||
```
|
||
|
||
**文件2:`unbindDeviceLogic.go`**(防御性修复)
|
||
|
||
补充 `user_sessions` 清理逻辑,与 `deleteUserDeviceLogic.go` 保持一致:
|
||
|
||
```go
|
||
// 第 118-122 行,补充 sessionsKey 清理
|
||
if sessionId, rerr := l.svcCtx.Redis.Get(ctx, deviceCacheKey).Result(); rerr == nil && sessionId != "" {
|
||
_ = l.svcCtx.Redis.Del(ctx, deviceCacheKey).Err()
|
||
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
|
||
_ = l.svcCtx.Redis.Del(ctx, sessionIdCacheKey).Err()
|
||
// 新增:清理 user_sessions
|
||
sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, device.UserId)
|
||
_ = l.svcCtx.Redis.ZRem(ctx, sessionsKey, sessionId).Err()
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## Bug 2:邀请码错误提示不友好
|
||
|
||
### 根本原因
|
||
|
||
`bindInviteCodeLogic.go` 中未区分"邀请码不存在"和"数据库错误"。
|
||
|
||
### 修复方案
|
||
|
||
```go
|
||
// 第 44-47 行修改为
|
||
referrer, err := l.svcCtx.UserModel.FindOneByReferCode(l.ctx, req.InviteCode)
|
||
if err != nil {
|
||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||
return errors.Wrapf(xerr.NewErrCodeMsg(xerr.InviteCodeError, "无邀请码"), "invite code not found")
|
||
}
|
||
logger.WithContext(l.ctx).Error(err)
|
||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query referrer failed: %v", err.Error())
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 涉及文件汇总
|
||
|
||
| 文件 | 修改类型 | 优先级 |
|
||
|------|----------|--------|
|
||
| `internal/logic/public/user/bindEmailWithVerificationLogic.go` | 核心修复 | 高 |
|
||
| `internal/logic/public/user/unbindDeviceLogic.go` | 防御性修复 | 中 |
|
||
| `internal/logic/public/user/bindInviteCodeLogic.go` | Bug 修复 | 中 |
|
||
|
||
---
|
||
|
||
## 验收标准
|
||
|
||
### Bug 1 验收
|
||
- [ ] 设备B绑定邮箱后,设备B的旧 Token 失效
|
||
- [ ] 设备B绑定邮箱后,设备B的 WebSocket 连接被断开
|
||
- [ ] 在设备A上移除设备B后,设备B立即被踢下线
|
||
- [ ] 设备B无法继续使用旧 Token 调用 API
|
||
|
||
### Bug 2 验收
|
||
- [ ] 输入不存在的邀请码时,返回错误码 20009
|
||
- [ ] 错误消息显示"无邀请码"
|
||
|
||
---
|
||
|
||
## 验证计划
|
||
|
||
1. **编译验证**:`go build ./...`
|
||
2. **手动测试**:
|
||
- 设备B绑定邮箱 → 检查是否被踢下线
|
||
- 设备A移除设备B → 检查设备B是否被踢下线
|
||
- 输入无效邀请码 → 检查错误提示
|