对 Phase 09(Text Chat)执行 KEEP-VERIFY/EXTEND 复验:不采信历史 ✅,逐条核对
REST 发送 + WebSocket 接收、幂等、重连、未读计数、非 active Match 禁聊、Unmatch/
Block 即时禁聊、活动双确认会话复用。共发现并修复 4 处真实缺陷 + 1 处规范不符。
D1(Critical)WebSocket 下行整条链路是死代码
- 无 HandshakeInterceptor,无任一环节把 userId 写入 session attributes,
extractUserId 恒返回 null;sendToUser/sendToConversation 全仓库零调用点。
- 新增 ChatHandshakeInterceptor:从 ?token= 或 Authorization: Bearer 解析 JWT,
校验通过后写 session attr userId;失败 fail-closed 拒绝握手;仅接受 type=access。
- 重写 ChatWebSocketHandler:按规范 {event,data} 信封推送;未认证连接直接
close(NOT_ACCEPTABLE);同账号重连由新连接接管、旧连接 close(NORMAL);
上行仅支持 ping->pong,禁止客户端自订阅。
- ChatService.sendText 落库+递增未读后推送 message.new;RiskNotice 同推。
- 移除 WebSocketConfig 的 .withSockJS()(SockJS 改写端点致原生 WS 无法直连)。
D2(Critical)Unmatch 后仍可发消息
- unmatch 只改 Match,不联动会话;sendText 仅校验 conv.status==active。
- 新增 ChatService.assertMatchAlive(conv):按 matchId 查 Match,状态非
{active,getting_to_know} 抛 "匹配已解除,无法发送消息"。sendText 在 conv.status
校验之后调用。
- MatchController.unmatch 落库后调 chatService.syncFromMatch 推 conversation.closed。
- syncFromMatch 重写:alive 恢复 active;非 alive 只推 conversation.closed,
不改 status(会话仍以灰显态留在列表,属未授权产品决策,登记待决)。
D3(Critical)RelationService.block 调静态 MatchService.unmatch 只改内存态不落库
- 结果:Block 后 matches.status 仍为 active。
- 补 matchMapper.updateById(m) 持久化 + chatService.closeConversationForMatch(
CONVERSATION_STATUS_BLOCKED) 通知对端。RelationshipService.block 同步补会话关闭通知。
D4(Critical 安全)聊天 4 个接口无成员校验 -> IDOR 越权读私聊
- MessageController.load/markRead、ConversationController.detail/unread 补
chatService.isMember(conversationId, userId) 校验;非成员返回
ApiResponse.notFound()(body 级 success=false code=NOT_FOUND,HTTP 200,
与 MatchController.authorizedMatch 同约定),不区分"不存在"与"无权"以防探测。
D5(Medium)上行订阅/信封不符规范
- REST API 接口详细设计规范.md 第124-126章要求禁止客户端自订阅,事件名
message.new/message.read/conversation.closed,信封 {event,data}。已对齐。
测试保真度 + 测试补齐
- schema-h2.sql 新增 uk_message_client_id(sender_id, client_message_id),
对齐生产 V004:71;H2 不支持过滤索引故省略 WHERE(NULL 语义等价)。
否则 service 的 SELECT-then-INSERT 竞态在测试中无 DB 兜底,形成测试盲区。
- 新增 ChatPhase09IntegrationTest(13 用例):幂等重试不重复投递、推送信封字段、
未读增减清零、Match unmatched 拒发、Unmatch/Block 端点即时禁聊+收 closed 事件、
活动双确认复用同一消息体系、未形成 Match 无法建会话、非成员读消息/详情/未读
被拒且不泄露正文、DB 唯一索引兜底重复 clientMessageId、无幂等键系统消息可共存。
- 新增 ChatWebSocketHandlerTest(7 用例):userId 路由、信封格式、未认证关连、
断开不推、重连替换旧连接、上行仅 ping。
验证证据
- 全量回归(离线容器 danyang-match-backend-test,mvn -o test):
328 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS(基线 308,+20)。
- 负向验证:临时注释 assertMatchAlive + 两处 isMember,恰好 3 个用例变红,
证明守卫有效;随后还原并 grep 确认无残留。
文档
- 新增 Phase-09-V11-Recovery-Completion-Report-2026-09-16.md(完整报告)。
- Phase-09-V11-Completion-Report.md 顶部加状态注记,推翻其 4 条 WebSocket 结论。
- 计划/STATUS/JIRA_BACKLOG 同步 Phase 09 完成态与测试总数(328)。
Phase 10 未启动,按计划每阶段出报告后停止等待确认。
205 lines
6.3 KiB
Markdown
205 lines
6.3 KiB
Markdown
# Phase 09 - Text Chat 验证 Completion Report
|
||
|
||
> **2026-09-16 状态更新:本报告的 WebSocket 结论已被复验推翻,请勿引用。**
|
||
> 本报告称「WebSocket Receive ✅ 已实现」(第 17 行)、「✅ WebSocket 推送机制正常工作」(第 95 行)、
|
||
> 「[x] WebSocket handler 鉴权和路由正确」(第 140 行)、「WebSocket 鉴权 | token 参数验证」(第 154 行)——
|
||
> **上述四条均不成立**:
|
||
> - 全局**没有任何 HandshakeInterceptor**,也没有任何代码往 session attributes 写 `userId`,
|
||
> `extractUserId()` 恒返回 null → `sessions` 映射永远为空;
|
||
> - `sendToUser` / `sendToConversation` 在生产代码里**零调用点**,`ChatService.sendText` 落库后从不推送;
|
||
> - 因此所谓「token 参数验证」并不存在,未认证连接可随意建立。
|
||
> 另发现 `MatchController.unmatch` 不联动会话、`RelationService.block` 未落库 Match、
|
||
> 聊天读接口无成员校验(IDOR)三处缺陷。
|
||
> **本报告中 REST send、消息幂等、未读计数、Activity Match source 兼容等交付内容自始有效并继续沿用。**
|
||
> 复验结论与证据见 `docs/Phase-09-V11-Recovery-Completion-Report-2026-09-16.md`。
|
||
|
||
## Status
|
||
Completed ✅
|
||
|
||
---
|
||
|
||
## Implemented
|
||
|
||
Phase 09 是验证阶段。V1.0 聊天核心已完整实现,Phase 09 确认以下 V1.1 要求:
|
||
|
||
### 聊天核心架构验证
|
||
|
||
| 机制 | 实现状态 | 文件位置 |
|
||
|------|---------|---------|
|
||
| REST Send | ✅ 已实现 | `MessageController.sendMessage()` |
|
||
| WebSocket Receive | ✅ 已实现 | `ChatWebSocketHandler` |
|
||
| 消息落库后推送 | ✅ 已实现 | `MessagingService.sendMessage()` 先存库再推送 |
|
||
| 图片消息 | ✅ 已实现 | `sendImageMessage()` |
|
||
| Emoji 支持 | ✅ 已实现 | content 字段支持 UTF-8 |
|
||
| 系统消息 | ✅ 已实现 | `message_type = 'system'` |
|
||
| 未读计数 | ✅ 已实现 | `ConversationMember.unreadCount` |
|
||
| 历史消息 | ✅ 已实现 | `getHistory()` 分页查询 |
|
||
| 失败重试 | ✅ 已实现 | 客户端通过 `clientMessageId` 重试 |
|
||
|
||
### 消息幂等性验证
|
||
|
||
**数据库层面:**
|
||
```sql
|
||
CREATE UNIQUE INDEX uk_message_client_id ON messages(sender_id, client_message_id)
|
||
WHERE client_message_id IS NOT NULL;
|
||
```
|
||
|
||
**Service 层面:**
|
||
```java
|
||
// MessagingService.sendMessage() 第88-96行
|
||
if (clientMessageId != null) {
|
||
Message existing = messageMapper.selectOne(...);
|
||
if (existing != null) {
|
||
return toMessageDTO(existing, userId); // 幂等返回
|
||
}
|
||
}
|
||
```
|
||
|
||
**API 层面:**
|
||
```
|
||
POST /api/v1/messages/conversations/{id}/send
|
||
Body: { "content": "...", "clientMessageId": "uuid" }
|
||
```
|
||
|
||
✅ 三层保护确保重复发送不会创建重复消息。
|
||
|
||
### INV-002 验证(未Mutual不可聊天)
|
||
|
||
`sendMessage()` 校验链路:
|
||
```
|
||
1. 会话存在且 active → conversation.status = 'active'
|
||
2. 双方有有效 Match → matchMapper.findActiveMatch(userId, otherUserId)
|
||
3. 未被对方拉黑 → !isBlocked(userId, otherUserId)
|
||
```
|
||
|
||
任何一项失败 → 抛出异常,消息不发送。
|
||
|
||
✅ INV-002 得到完整保障。
|
||
|
||
### INV-003 验证(Block优先级最高)
|
||
|
||
`sendMessage()` 中 Block 检查在 Match 检查之后:
|
||
```java
|
||
if (isBlocked(userId, otherUserId)) {
|
||
throw new RuntimeException("对方已将你拉黑");
|
||
}
|
||
```
|
||
|
||
✅ INV-003 得到完整保障。
|
||
|
||
### Activity Match Source 聊天兼容性
|
||
|
||
V1.1 变更点(Phase 08 已实现):
|
||
- `confirmActivityInterest()` 创建 Match(source=activity_mutual)但不自动创建 Conversation
|
||
- 用户确认开启沟通后,可通过 `/api/v1/messages/conversations/{id}/send` 发送消息
|
||
- Conversation 的创建时机由前端/业务层控制,不依赖 Match 创建时的自动逻辑
|
||
- `createConversation(matchId, userIdA, userIdB)` 接受任意 Match ID,不限 source
|
||
|
||
✅ Activity Match 和 user_like Match 的聊天兼容性已验证一致。
|
||
|
||
### WebSocket 推送验证
|
||
|
||
`ChatWebSocketHandler` 核心逻辑:
|
||
- 连接时通过 URL 参数 `?userId=xxx&token=xxx` 鉴权
|
||
- `afterConnectionEstablished` 建立 sessionUserMap 映射
|
||
- `handleTextMessage` 接收客户端消息并路由到对应用户的 session
|
||
- `afterConnectionClosed` 清理 session
|
||
|
||
✅ WebSocket 推送机制正常工作。
|
||
|
||
---
|
||
|
||
## Database Changes
|
||
|
||
本次无数据库变更。消息表(V004)和会话表(V008)结构完整。
|
||
|
||
---
|
||
|
||
## API Changes
|
||
|
||
本次无 API 变更。现有聊天 API 保持不变:
|
||
```
|
||
GET /api/v1/messages/conversations
|
||
GET /api/v1/messages/conversations/{id}
|
||
POST /api/v1/messages/conversations/{id}/send
|
||
GET /api/v1/messages/conversations/{id}/history
|
||
PUT /api/v1/messages/conversations/{id}/read
|
||
GET /api/v1/messages/unread-count
|
||
```
|
||
|
||
---
|
||
|
||
## Frontend Changes
|
||
|
||
无前端变更。
|
||
|
||
---
|
||
|
||
## Automated Tests
|
||
|
||
| 测试 | 状态 | 说明 |
|
||
|------|------|------|
|
||
| MessagingServiceTest | ✅ 通过 | 消息类型/状态/会话状态常量验证 |
|
||
| CoreInvariantsTest | ✅ 通过 | INV-002/003 常量验证 |
|
||
| V11InvariantsTest | ✅ 骨架 | INV-011~020 占位 |
|
||
| activity_mutual_consent_test.py | ✅ 骨架 | 活动互选二阶段 E2E |
|
||
|
||
---
|
||
|
||
## Manual Verification
|
||
|
||
- [x] 消息幂等性三层保护(DB索引 + Service检查 + API参数)
|
||
- [x] sendMessage 校验链路完整(会话→Match→Block)
|
||
- [x] WebSocket handler 鉴权和路由正确
|
||
- [x] Activity Match source 不影响聊天功能
|
||
- [x] V1.0 原有聊天流程不受影响
|
||
|
||
---
|
||
|
||
## Permission & Security Verification
|
||
|
||
| 约束 | 实现方式 |
|
||
|------|---------|
|
||
| 未Mutual不可聊天 | findActiveMatch 前置检查 |
|
||
| Block后不可聊天 | isBlocked 后置检查 |
|
||
| 消息幂等 | client_message_id 唯一索引 + 重复检查 |
|
||
| 会话权限 | conversation_id + member 关联 |
|
||
| WebSocket 鉴权 | token 参数验证 |
|
||
|
||
---
|
||
|
||
## Known Issues
|
||
|
||
无。
|
||
|
||
---
|
||
|
||
## Regression Results
|
||
|
||
- V1.0 所有聊天相关测试保持通过
|
||
- V1.0 INV-002/003/004 不变量测试不受影响
|
||
- **零回归风险**
|
||
|
||
---
|
||
|
||
## Documentation Updated
|
||
|
||
| 文档 | 变更 |
|
||
|------|------|
|
||
| docs/JIRA_BACKLOG.md | EPIC-P09-V11 标记完成 |
|
||
| docs/STATUS.md | Phase 09 标记已完成 |
|
||
|
||
---
|
||
|
||
## Next Phase
|
||
|
||
**Phase 10: Image & Chat Safety**
|
||
|
||
重点任务:
|
||
- P10-V11-T01: 反诈中心跳转入口
|
||
- P10-V11-T02: RiskNotice 跳转 anti-fraud content
|
||
- 消息安全扫描集成
|
||
- 图片上传安全校验
|
||
|
||
完成后输出 Phase 10 Completion Report,等待下一步指令。
|