fix(chat): Phase 09 复验 —— 修复 WebSocket 下行死链路 / Unmatch 后可发消息 / Block 未落库 / 聊天 IDOR
对 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 未启动,按计划每阶段出报告后停止等待确认。
This commit is contained in:
@@ -5,9 +5,13 @@
|
||||
|
||||
> **2026-09-16 状态校正:** Phase 07 UI 已按 2026-09-13 规格重新实现,**首批 11 页于 2026-09-16 通过用户验收(结论:基本达标)**,本文件此前"视觉否决/REOPENED"口径随之关闭。
|
||||
>
|
||||
> **2026-09-16 Phase 09 复验关闭:** EPIC-P09-V11 已完成。复验中发现并修复 4 处真实缺陷(WebSocket 下行整条链路为死代码、Unmatch 后仍可发消息、Block 未落库 Match、聊天四接口 IDOR 越权),
|
||||
> 新增 `ChatPhase09IntegrationTest`(13 用例)与 `ChatWebSocketHandlerTest`(7 用例),并把生产消息唯一索引下沉到测试库。后端全量 **328 tests / 0 failures / 0 errors**。
|
||||
> 证据:`docs/Phase-09-V11-Recovery-Completion-Report-2026-09-16.md`。**Phase 10 需用户确认后启动。**
|
||||
>
|
||||
> **2026-09-16 Phase 08 复验关闭:** EPIC-P08-V11 已完成。复验中发现并修复 3 处真实缺陷(搜索绕过 Block / 详情无 Block 检查 / `matches.source` 取值越界),
|
||||
> 新增 `DiscoverBlockInvisibilityIntegrationTest`(9 用例)并把生产 `chk_match_source` 约束下沉到测试库。后端全量 **308 tests / 0 failures / 0 errors**。
|
||||
> 证据:`docs/Phase-08-V11-Recovery-Completion-Report-2026-09-16.md`。**Phase 09 需用户确认后启动。**
|
||||
> 新增 `DiscoverBlockInvisibilityIntegrationTest`(9 用例)并把生产 `chk_match_source` 约束下沉到测试库。
|
||||
> 证据:`docs/Phase-08-V11-Recovery-Completion-Report-2026-09-16.md`。
|
||||
>
|
||||
> **2026-09-13 状态校正(历史):** 本文件中的既有 `✅` 是历史开发声明,不再单独作为验收证据。Phase 01~06 已由恢复版 Completion Report 重新举证;Phase 07 仅保留业务测试证据,管理端与小程序视觉已被用户否决并重新打开。Phase 08 暂停,当前先执行 UI 清理与参考图重设计。
|
||||
|
||||
@@ -20,7 +24,8 @@
|
||||
| Phase 05 统一审核扩展 | 已完成 | 见 `Phase-05-V11-Recovery-Completion-Report-2026-09-12.md`;Phase 06 尚未开始 |
|
||||
| Phase 06 Match Eligibility 增量 | 已完成 | 见 `Phase-06-V11-Recovery-Completion-Report-2026-09-12.md`;Phase 07 尚未开始 |
|
||||
| Phase 07 V1.1 首页唯一实现 + 找对象 | 已完成:视觉已通过用户验收(2026-09-16) | 保留首页聚合、Capability、8 入口和推荐规则证据;UI 已按 2026-09-13 新规格重新实现,11 页经用户验收通过 |
|
||||
| Phase 08 Like / Match / Block 复验 | 已完成(2026-09-16) | 见 `Phase-08-V11-Recovery-Completion-Report-2026-09-16.md`;修复 3 处真实缺陷(搜索绕过 Block、详情无 Block 检查、`matches.source` 取值越界),后端 308 tests 全绿;Phase 09 尚未获准 |
|
||||
| Phase 08 Like / Match / Block 复验 | 已完成(2026-09-16) | 见 `Phase-08-V11-Recovery-Completion-Report-2026-09-16.md`;修复 3 处真实缺陷(搜索绕过 Block、详情无 Block 检查、`matches.source` 取值越界),后端 308 tests 全绿 |
|
||||
| Phase 09 Text Chat 复验 | 已完成(2026-09-16) | 见 `Phase-09-V11-Recovery-Completion-Report-2026-09-16.md`;修复 4 处真实缺陷(WebSocket 下行死代码、Unmatch 后仍可发消息、Block 未落库 Match、聊天接口 IDOR),后端 328 tests 全绿;Phase 10 尚未获准 |
|
||||
| UI-CLEANUP-20260913 | 已完成 | 废弃 UI 代码、冲突规范、旧计划和无效验收证据已清理;`ui-document-baseline-contracts` 锁定唯一视觉入口 |
|
||||
| UI-ADMIN-20260913 | 已完成(待桌面端目视复验) | 管理端全局框架与工作台已按规格交付;18 modules + 22 routes,vue-tsc 与 vite build 全绿 |
|
||||
| UI-MINIAPP-20260913 | 已完成 | 小程序 11 个核心页面已重构;11/11 页 TS-only,2026-09-16 通过用户验收 |
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
# 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 ✅
|
||||
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
# Phase 09 V1.1 Recovery Completion Report
|
||||
|
||||
> 阶段:Text Chat — KEEP-VERIFY/EXTEND
|
||||
> 分支:`main`
|
||||
> 日期:2026-09-16
|
||||
> 结论:**Phase 09 复验通过并关闭**;期间发现并修复 4 处真实缺陷(2 处 Critical、1 处 Critical 安全缺陷、1 处规范不符),并补齐 WebSocket 端到端链路
|
||||
> 未进入 Phase 10 —— 按计划每阶段输出报告后停止,等待用户确认
|
||||
|
||||
---
|
||||
|
||||
## 1. 阶段目标(来自客户需求基线增量开发与修复计划)
|
||||
|
||||
- [x] 验证 REST send + WebSocket receive、消息幂等、重连和未读计数。
|
||||
- [x] 验证非 active Match 禁聊,Unmatch/Block 后即时禁聊。
|
||||
- [x] 让活动双确认产生的 Conversation 复用同一消息体系。
|
||||
|
||||
**Gate:** 未 Mutual 或活动未双确认时不能发消息;正常会话不重复投递。
|
||||
|
||||
---
|
||||
|
||||
## 2. 复验方式
|
||||
|
||||
同样不采信历史 ✅,而是把计划的三条各自拆成**可执行的代码路径**逐条核对,并对每条路径追问"这段代码真的会被调用吗"。
|
||||
本轮大量缺陷属于**死代码 / 缺接线**类:组件存在、逻辑看着对,但没有任何调用点。
|
||||
|
||||
### 2.1 复验结论矩阵
|
||||
|
||||
| 计划条目 | 承载代码 | 复验前 | 处置 |
|
||||
|---|---|---|---|
|
||||
| REST send | `MessageController.send` → `ChatService.sendText` | ✅ 可用 | 增强(加 Match 活性校验) |
|
||||
| **WebSocket receive** | `ChatWebSocketHandler` + `WebSocketConfig` | ❌ **整条链路是死代码** | **已修复(缺陷 C1)** |
|
||||
| 消息幂等 | `sendText` 按 `clientMessageId` 重放返回原消息 | ✅ 逻辑正确 | 补测试锁定"不重复落库 + 不重复投递" |
|
||||
| 重连 | WS handler 会话登记/替换 | ❌ 无鉴权、无 userId → 登记表恒空 | 随 C1 一并修复 + ping/pong 保活 |
|
||||
| 未读计数 | `incrementUnread` / `resetUnread` / `sumUnreadByUser` | ✅ 正确 | 补测试 |
|
||||
| **非 active Match 禁聊** | `sendText` 的会话状态校验 | ❌ **只校验会话,不校验 Match** | **已修复(缺陷 C2)** |
|
||||
| **Unmatch 后即时禁聊** | `MatchController.unmatch` | ❌ **完全不联动会话** | **已修复(缺陷 C2)** |
|
||||
| Block 后即时禁聊 | `sendText` 的拉黑关系双向校验 | ✅ 发送被拒 | ✅;但 Block 的 Match 落库有缺陷(C3) |
|
||||
| 活动双确认复用同一消息体系 | `ConversationController.fromMatch` → `getOrCreateConversation` | ✅ 只绑 `match_id`,与 source 无关 | 补端到端测试 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 本轮发现并修复的缺陷
|
||||
|
||||
### 缺陷 C1(Critical)— WebSocket 下行链路整体是死代码
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 位置 | `chat/websocket/ChatWebSocketHandler`、`chat/config/WebSocketConfig` |
|
||||
| 症状 1 | **全局没有任何 `HandshakeInterceptor`**,也没有任何代码往 `session.getAttributes()` 写 `userId`,而 `extractUserId()` 只从 attributes 读 → **恒为 null** → `sessions` 映射永远为空。 |
|
||||
| 症状 2 | `sendToUser` / `sendToConversation` **在生产代码里零调用点**:`ChatService.sendText` 落库后从不推送。 |
|
||||
| 合并后果 | "REST send + WebSocket receive" **不存在**:消息发出去后对方只能靠重进会话才能看到,实时接收完全缺失。同时未认证连接可随意建立(违反规范第 126 章)。 |
|
||||
| 修复 1 | 新增 `ChatHandshakeInterceptor`:从 `?token=`(或 `Authorization: Bearer`)解析 JWT,校验通过后把 `userId` 写入 session attributes;校验失败**拒绝握手**。仅接受 `type=access` 的**用户** token,`operator_access` 一律拒绝(AGENTS 第 7 条)。 |
|
||||
| 修复 2 | `ChatWebSocketHandler` 重写:按规范第 125 章输出 `{event, data}` 信封,提供 `sendEvent(userId, event, data)`;无 userId 的连接直接 `close(NOT_ACCEPTABLE)`;同账号重连时旧连接被替换并关闭(避免重复投递);新增 ping/pong 保活。 |
|
||||
| 修复 3 | `ChatService.sendText` 落库并递增未读后,向对方推送 `message.new`。**幂等重放分支在上方 `return`,走不到推送**,因此正常会话天然不会重复投递。 |
|
||||
|
||||
关于 SockJS:`WebSocketConfig` 原本调用 `.withSockJS()`,会把端点改写为 SockJS 协议族,原生 WebSocket 客户端(小程序 `wx.connectSocket`、规范第 124 章的 `/wss/chat`)**无法直连**,与本类注释自述的 `/ws/chat?token=` 自相矛盾。两端都是原生客户端,故移除 SockJS。
|
||||
|
||||
### 缺陷 C2(Critical)— Unmatch 后仍可继续发消息
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 位置 | `match/controller/MatchController#unmatch`、`chat/service/ChatService#sendText` |
|
||||
| 症状 | `unmatch` 只把 `matches.status` 改成 `unmatched` 就结束,**从不联动会话**;而 `sendText` 只校验 `conversation.status == active`,会话仍是 `active` → **解除匹配后双方仍能正常发消息**,直接违反 Gate「未 Mutual 不能发消息」。 |
|
||||
| 另一层 | `ChatService.syncFromMatch` —— 本该承担这个联动 —— **零调用点**。 |
|
||||
| 修复 1 | `sendText` 增加 `assertMatchAlive(conv)`:按 `matchId` 回查 Match,状态不在 `{active, getting_to_know}` 即拒绝。**校验不依赖会话状态是否漂移**,属独立防线。 |
|
||||
| 修复 2 | `MatchController.unmatch` 落库后调用 `chatService.syncFromMatch(...)`,向双方推送 `conversation.closed`,客户端可立即把会话置灰。 |
|
||||
|
||||
### 缺陷 C3(Critical)— Block 的"自动 Unmatch"实际是空操作
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 位置 | `match/service/RelationService#block` |
|
||||
| 症状 | 调用 `MatchService.unmatch(m, userId)` —— 但 `MatchService` 是 `private` 构造的**静态工具类**,`unmatch` 只改传入对象的内存态、**不落库**,且此处没有后续 `updateById`。于是 **Block 之后 Match 在库里仍是 `active`**,"自动 Unmatch"完全没发生;用户侧仍会看到双方互相匹配。 |
|
||||
| 附带 | Block 也从不通知会话,对方界面不会即时停用。 |
|
||||
| 修复 | 补 `matchMapper.updateById(m)` 持久化;并调用 `chatService.closeConversationForMatch(matchId, "blocked")` 通知双方。 |
|
||||
|
||||
> 对比:legacy 的 `RelationshipService.block` 反而是**正确**的(它内联改字段并 `updateById` 落库),canonical 路径却漏了 —— 典型"两套实现、新的那套漏了一步"。
|
||||
|
||||
### 缺陷 C4(Critical,安全)— 聊天接口越权(IDOR)
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 位置 | `MessageController.load` / `markRead`、`ConversationController.detail` / `unread` |
|
||||
| 症状 | 四个接口**完全没有会话成员校验**。任意已登录用户只要拿到(或枚举)`conversationId`,就能读取**他人完整聊天记录**、看到对方昵称与最后一条消息、甚至改写他人已读状态。`SecurityConfig` 只有 `/**` → `authenticated()`,没有对象级授权。 |
|
||||
| 影响 | 直接违反 AGENTS 第 7 条(普通工作人员不能查看私人聊天)与隐私要求。 |
|
||||
| 修复 | `ChatService.isMember(convId, userId)` + 四个接口前置校验;非成员按既有约定返回 `success=false / code=NOT_FOUND`(与 `MatchController.authorizedMatch` 同一模式,不区分"不存在"与"无权",避免被用来探测会话是否存在)。 |
|
||||
|
||||
### 缺陷 C5(Medium)— 上行订阅与事件信封不符规范
|
||||
|
||||
| 项 | 内容 |
|
||||
|---|---|
|
||||
| 规范第 126 章 | "**不允许客户端自行订阅任意 conversationId**"、事件名清单 `message.new / message.read / conversation.closed / match.created / notification.new / account.status_changed`(第 124 章)、信封 `{event, data}`(第 125 章)。 |
|
||||
| 修复前 | 上行帧只被 `log.debug` 记一行,订阅语义未表态;事件信封为自定义 `type + payload`。 |
|
||||
| 修复 | 上行只接受 `ping`(回 `pong`),其余忽略并有注释说明规范依据;下行统一 `{event, data}`,事件名用规范清单。 |
|
||||
|
||||
---
|
||||
|
||||
## 4. 刻意**不**改的一处(+ 登记为待决项)
|
||||
|
||||
**会话 `status` 是否应在 Unmatch/Block 后置为非 active —— 保持现状。**
|
||||
|
||||
`ConversationMapper.findActiveByUser` 过滤 `status = 'active'`。一旦把 status 改成 `unmatched`/`blocked`,会话会**从"我的消息"列表消失**。
|
||||
但 `conversation-item` 组件恰恰要在列表里渲染"会话已停用 / 会话已停用(已拉黑)"的灰显态,AGENTS 也要求 Block 后保留历史。
|
||||
|
||||
因此本轮把"关闭语义"交给**事件 + 发送时校验**承载,而不是 status:
|
||||
|
||||
- 发送权限 → `assertMatchAlive` + 拉黑关系双向校验(**与 status 无关,无法绕过**);
|
||||
- 实时表现 → 推送 `conversation.closed`,客户端立即灰显;
|
||||
- 读接口 → `toConversationDTO` 已用 `matchAlive && !blocked` 算出 `canSendMessage=false` 与 `relationStatus`。
|
||||
|
||||
**列表可见性是否应在关闭后收回,是未被本阶段授权的产品决策**,故保持现状并在本节显式登记,请用户裁决。
|
||||
|
||||
---
|
||||
|
||||
## 5. 验证证据
|
||||
|
||||
| 项目 | 结果 |
|
||||
|---|---|
|
||||
| Backend 全量回归(离线容器,`mvn -o test`) | **328 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS**(基线 308,+20) |
|
||||
| 新增测试 | `ChatPhase09IntegrationTest`(13 用例,真实 H2 + 真实 SQL + 真实 HTTP 层)、`ChatWebSocketHandlerTest`(7 用例,纯单元) |
|
||||
| **负向验证** | 同时还原 `assertMatchAlive` 与消息读接口的成员校验后,**恰好 3 个用例变红**(`outsiderCannotReadMessages`、`sendRejectedWhenMatchNotAlive`、`unmatchEndpointImmediatelyDisablesChat`)✔ 守卫有效 |
|
||||
| 定向复跑 | `ChatPhase09IntegrationTest` + `P0FixVerificationTest`:21 tests / 0 failures / 0 errors |
|
||||
|
||||
新增/增强测试覆盖:
|
||||
|
||||
| 用例 | 断言要点 |
|
||||
|---|---|
|
||||
| 幂等重试不重复投递 | 同 `clientMessageId` 返回同一条;库内只 1 行;未读只 +1;**只推一次** |
|
||||
| 推送信封 | `sendEvent(peer, "message.new", ...)`,`data.conversationId` + `data.message.{messageId,content,senderId,sentAt}` |
|
||||
| 未读随发送递增、标记已读清零 | 发送方不计未读 |
|
||||
| Match 已 unmatched 时拒发 | **刻意只改 Match、不改会话**,证明校验不依赖会话状态 |
|
||||
| Unmatch 端点即时禁聊 | 调 `MatchController.unmatch` 后双方均拒发 + 收到 `conversation.closed` |
|
||||
| Block 即时禁聊 + **Match 落库为 unmatched** | 直接锁死 C3 回归 |
|
||||
| 活动双确认复用同一消息体系 | 确认 → `from-match` 建会话 → 同一套发送/推送/未读 |
|
||||
| 未形成可聊 Match 无法建会话 | 不存在 / 已失效的 Match 均拒绝(Gate 第二分句) |
|
||||
| 非成员读消息/详情/未读被拒 | 越权响应**不泄露聊天正文**;成员侧不受影响 |
|
||||
| WS handler(7 例) | userId 路由、`{event,data}` 信封、未认证连接被关、断开后不推、重连替换旧连接、上行只支持 ping |
|
||||
| 幂等由 DB 唯一索引兜底(2 例) | 绕过 service 幂等查询直接插重复 `clientMessageId` → 被唯一索引拒绝;无幂等键的系统消息仍可多条共存 |
|
||||
|
||||
### 5.1 测试保真度补强(延续 Phase 08 的方法论)
|
||||
|
||||
"正常会话不重复投递"在**并发**下的真正保障不是 service 里的 `SELECT-then-INSERT`(两者之间存在竞态窗口),
|
||||
而是生产库的唯一索引:
|
||||
|
||||
```sql
|
||||
-- V004__add_messaging_and_recommendation_tables.sql:71
|
||||
CREATE UNIQUE INDEX uk_message_client_id ON messages(sender_id, client_message_id)
|
||||
WHERE client_message_id IS NOT NULL;
|
||||
```
|
||||
|
||||
**而测试库 `schema-h2.sql` 里没有这条索引** —— 与 Phase 08 的 `chk_match_source` 属同一类保真度缺口:
|
||||
约束只存在于生产库,测试环境无从验证。本轮已补齐:
|
||||
|
||||
- H2 不支持过滤索引,故省略 `WHERE`;但 H2 的唯一索引遵循 SQL 标准(多个 NULL 互不冲突),
|
||||
语义与生产的部分索引等价 —— 未带幂等键的系统消息(`RiskNotice`)仍可多条存在,已由测试守住。
|
||||
- 新增两条测试:**绕过 service 层直接落库**的重复 `clientMessageId` 必须被索引拒绝;
|
||||
无幂等键的系统消息不得被误伤。
|
||||
|
||||
---
|
||||
|
||||
## 6. 已知限制(本轮登记,未处理)
|
||||
|
||||
1. **业务拒绝仍映射为 HTTP 500。** `sendText` 用 `IllegalStateException` 表达"未 Mutual / 已拉黑 / 非成员",而 `GlobalExceptionHandler` 的兜底把它映射成 **500 内部错误**,而非 4xx。前端目前靠 `canSendMessage=false` 规避,不依赖该状态码,故本轮未动(改动涉及全平台错误码契约,超出本阶段授权)。
|
||||
2. **`Conversation.status` 的关闭语义未定** —— 见第 4 节。
|
||||
3. **WebSocket 为单实例内存实现**:多实例部署需 Redis Pub/Sub(原注释已标注属于 Phase 22+)。当前不构成缺陷。
|
||||
4. **`MatchService.unmatch` 未持久化**这一"静态工具方法只改内存态"的隐患仍存在于方法签名本身(调用方必须自己 `updateById`)。本轮只在 canonical 调用点补上,未改设计。
|
||||
|
||||
---
|
||||
|
||||
## 7. Gate 判定
|
||||
|
||||
| Gate 条款 | 判定 | 依据 |
|
||||
|---|---|---|
|
||||
| 未 Mutual 时不能发消息 | ✅ | 无 Match → 无会话;且 `assertMatchAlive` 独立拒发 |
|
||||
| 活动未双确认时不能发消息 | ✅ | 无 Match → 无会话 → 无成员 → 拒发;`getOrCreateConversation` 对不存在/失效 Match 直接拒建 |
|
||||
| 正常会话不重复投递 | ✅ | 相同 `clientMessageId` 重放返回原消息且**不推送**;测试断言只推 1 次 |
|
||||
| REST send + WebSocket receive | ✅ | 握手鉴权 + 落库后推送 `message.new`,端到端链路打通 |
|
||||
|
||||
**Phase 09:通过并关闭。**
|
||||
|
||||
---
|
||||
|
||||
## 8. 变更清单
|
||||
|
||||
### 主代码
|
||||
- `chat/websocket/ChatHandshakeInterceptor.java` — **新增**:握手 Token 鉴权 + userId 绑定(fail-closed)
|
||||
- `chat/websocket/ChatWebSocketHandler.java` — 重写:`{event,data}` 信封、`sendEvent` 下行 API、未认证连接关闭、重连替换、ping/pong
|
||||
- `chat/config/WebSocketConfig.java` — 注册拦截器;移除 SockJS
|
||||
- `chat/service/ChatService.java` — 注入 WS handler;`assertMatchAlive`;落库后推 `message.new`;`syncFromMatch` 推 `conversation.closed`;新增 `closeConversationForMatch`、`isMember`
|
||||
- `chat/controller/MessageController.java` — `load`/`markRead` 补成员校验
|
||||
- `chat/controller/ConversationController.java` — `detail`/`unread` 补成员校验
|
||||
- `match/controller/MatchController.java` — `unmatch` 联动会话关闭
|
||||
- `match/service/RelationService.java` — Block 补 Match 落库 + 会话关闭通知
|
||||
|
||||
### 测试
|
||||
- `chat/ChatPhase09IntegrationTest.java` — **新增 13 用例**
|
||||
- `chat/websocket/ChatWebSocketHandlerTest.java` — **新增 7 用例**
|
||||
- `backend/src/test/resources/schema-h2.sql` — 补生产消息唯一索引 `uk_message_client_id`
|
||||
|
||||
---
|
||||
|
||||
## 9. 下一阶段
|
||||
|
||||
**不自动进入 Phase 10。** 按计划要求,每阶段输出报告后停止,等待用户确认。
|
||||
|
||||
Phase 10(Image & Chat Safety — KEEP-VERIFY)待办预告:
|
||||
- 验证图片私有访问、类型/大小校验、审核和水印
|
||||
- 验证风险识别、举报入口和违规内容处理
|
||||
- 检查日志不记录聊天正文、身份证、手机号或签名 URL
|
||||
|
||||
> 附:`ChatSafetyService` 的频控为 Redis **fail-open**(Redis 不可用时放行),Phase 10 应一并复核该降级策略是否符合风控要求。
|
||||
+5
-4
@@ -1,8 +1,8 @@
|
||||
# 项目状态跟踪 — STATUS.md
|
||||
|
||||
> **2026-09-16 当前有效结论:`V1.1 INCREMENTAL RECOVERY PHASE 4 COMPLETE / UI REDESIGN ACCEPTED BY USER / PHASE 08 RECOVERY CLOSED / GIT CLEAN`。**
|
||||
> **2026-09-16 当前有效结论:`V1.1 INCREMENTAL RECOVERY PHASE 4 COMPLETE / UI REDESIGN ACCEPTED BY USER / PHASE 08-09 RECOVERY CLOSED / GIT CLEAN`。**
|
||||
>
|
||||
> 阶段 0(在途收口)、阶段 1(小程序 C 组 + discover 筛选重设计)、阶段 2(管理端 Vue 新框架适配)、阶段 3(CapabilityService 独立化)、阶段 4(双端总验收)全部完成。三端构建通过、**13/13 契约全绿**、核心 11 页面**全部 TS-only(11/11)**。后端全量回归已清偿(308 tests / 0 failures / 0 errors)。**UI 重设计首批 11 页已于 2026-09-16 通过用户验收(结论:基本达标)**;`/me/*` 前后端契约同轮收敛,17 个 404 调用已恢复。**Phase 08(Like / Match / Block)复验关闭**,期间修复 3 处真实缺陷并补齐测试库的生产约束保真度。
|
||||
> 阶段 0(在途收口)、阶段 1(小程序 C 组 + discover 筛选重设计)、阶段 2(管理端 Vue 新框架适配)、阶段 3(CapabilityService 独立化)、阶段 4(双端总验收)全部完成。三端构建通过、**13/13 契约全绿**、核心 11 页面**全部 TS-only(11/11)**。后端全量回归已清偿(328 tests / 0 failures / 0 errors)。**UI 重设计首批 11 页已于 2026-09-16 通过用户验收(结论:基本达标)**;`/me/*` 前后端契约同轮收敛,17 个 404 调用已恢复。**Phase 08(Like / Match / Block)与 Phase 09(Text Chat)复验已关闭**,两轮共修复 7 处真实缺陷,并各自补齐测试保真度与守卫。**Phase 10 需用户确认后启动。**
|
||||
|
||||
最后更新: 2026-09-16 — 分支 `main`(`feat-ui-v11` 保留为冗余锚点)
|
||||
|
||||
@@ -13,13 +13,14 @@
|
||||
| Phase 01~06 Recovery | 已完成 | 各 Phase Completion Report;38+9+3+4+21 后端专项通过 |
|
||||
| Phase 07 V1.1 首页业务与找对象 | **业务+UI 已通过用户验收**(2026-09-16) | HomeApplicationService 真实聚合;CapabilityService 独立化;discover 筛选完整重设计(5 chip + 4 section + 底部抽屉 + 滚动定位高亮);UI 按 2026-09-13 规格重新实现,11 页经用户验收通过 |
|
||||
| **Phase 08 V1.1 Like / Match / Block** | **已完成**(2026-09-16) | KEEP-VERIFY 复验。修复 3 处真实缺陷:① `DiscoverService.searchUsers` 无查看者身份 → INV-004「搜索不可见」整体失效;② 详情路径无 Block 判断;③ `RelationshipService.like()` 把 `"liked"` 当 match source 写库 → 违反 `V003 chk_match_source`(H2 测试库无该约束故长期静默)。新增 `DiscoverBlockInvisibilityIntegrationTest`(9 用例,含反向拉黑);`schema-h2.sql` 补齐生产约束与 `user_profiles`;负向验证确认守卫会红。报告:`docs/Phase-08-V11-Recovery-Completion-Report-2026-09-16.md` |
|
||||
| **Phase 09 V1.1 Text Chat** | **已完成**(2026-09-16) | KEEP-VERIFY 复验。修复 4 处真实缺陷:① WebSocket 下行整条链路是死代码(无握手鉴权/无 userId 写入/`sendToUser` 零调用点)→ 新增 `ChatHandshakeInterceptor` + 落库后推 `message.new`;② Unmatch 不联动会话 + 发消息只校验会话状态 → 解除后仍可发消息 → 新增 `assertMatchAlive` + `MatchController.unmatch` 联动;③ `RelationService.block` 调静态 `MatchService.unmatch`(只改内存态)→ Block 后 Match 未落库;④ 聊天四个接口无成员校验 → IDOR。新增 `ChatPhase09IntegrationTest`(11 用例)+ `ChatWebSocketHandlerTest`(7 用例);负向验证 3 处守卫精准变红。报告:`docs/Phase-09-V11-Recovery-Completion-Report-2026-09-16.md` |
|
||||
| 阶段 0 在途收口 | 已完成 | 56 个改动分类提交为 7 个 commit;Git 清零;Flyway 实际 V035 已登记 |
|
||||
| 阶段 1 小程序 C 组 + discover | 已完成 | C 组 content/anti-fraud/service-station 迁 TS 唯一源;discover 筛选从参数到 UI 完整重写 |
|
||||
| 阶段 2 管理端 Vue 适配 | 已完成 | 18 modules + 22 routes + vue-tsc 全绿 + vite build 11.68s |
|
||||
| 阶段 3 CapabilityService 独立化 | 已完成 | New `capability` 模块:CapabilityService(getCapabilities + 公共判断方法)+ CapabilityController(GET /api/v1/capabilities);HomeApplicationService 完全委托 |
|
||||
| 阶段 4 双端总验收 | **已完成**(2026-09-14) | 三端构建全绿 + 13/13 契约全绿 + 源码健康检查通过;验收报告见 docs/superpowers/plans/2026-09-14-phase4-acceptance-report.md |
|
||||
| Flyway 迁移 | V001~V035 | V028~V035 已纳入 STATUS |
|
||||
| Backend 全量回归 | **已清偿**(2026-09-16) | 离线容器(`danyang-match-backend-test`)运行:**308 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS**;含 `CanonicalRouteContractTest`、`ChatContractJsonTest`、`MeRouteContractTest` 与新增 `DiscoverBlockInvisibilityIntegrationTest` |
|
||||
| Backend 全量回归 | **已清偿**(2026-09-16) | 离线容器(`danyang-match-backend-test`)运行:**328 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS**;含 `CanonicalRouteContractTest`、`ChatContractJsonTest`、`MeRouteContractTest`、`DiscoverBlockInvisibilityIntegrationTest`、`ChatPhase09IntegrationTest`、`ChatWebSocketHandlerTest` |
|
||||
| 路由/字段契约收敛 | **已完成**(2026-09-15) | 字段:会话 `target*` + `sentAt` + `relationStatus`(commit `687a156`);路径:采用 2026-09-14 canonical(`/likes` `/passes` `/blocks` `/matches`,缘分保留 `/relations/fate/*`),旧路径与 `success` 桩实现已移除;裁决由 `CanonicalRouteContractTest` 锁定 |
|
||||
| `/me/*` 路径族 | **已完成**(2026-09-16) | 小程序按 REST 规范(§16 / §22-§40 / §80 / §83-§84)调用 `/me/*`,后端此前**无 `/me` 基路径**致 17 个调用 404。新增 `MeController` 委托既有 service 暴露规范路径,旧路径全部保留,小程序三条历史写法(`/me/privacy-settings`、`/me/photos/{id}/main`、`/me/appeals`)一并兼容;由 `MeRouteContractTest` 锁定 |
|
||||
| Admin Web | **验证通过** | 2026-09-14 阶段 4:vue-tsc ✅、vite build ✅ 11.68s;18 modules 齐全;壳层+工作台通过参考图 QA |
|
||||
@@ -27,7 +28,7 @@
|
||||
| UI 契约测试 | **13/13 通过** | 2026-09-16 运行:全部退出码 0;含 capability 独立化适配与文档基线口径 |
|
||||
| Git 工作区 | **已清零** | 阶段 0~4 共 24+ commit 可追溯;`git status --porcelain` 0 行 |
|
||||
| 核心待清债务 | 0 项 | 已全部清偿:3 页 JS→TS 迁移、backend 全量回归(299 tests)、DevTools 真机截图留档(2026-09-14)、`/me/*` 路径族收敛(2026-09-16) |
|
||||
| UAT / RC / Production | **UI 验收通过,前后端契约已收敛,Phase 08 已关闭** | 双端构建/契约/源码全部通过;UI 首批 11 页 2026-09-16 通过用户验收;`/me/*` 契约同轮收敛;Phase 08 复验通过并关闭,可推进 Phase 09(**需用户确认后启动**) |
|
||||
| UAT / RC / Production | **UI 验收通过,前后端契约已收敛,Phase 08-09 已关闭** | 双端构建/契约/源码全部通过;UI 首批 11 页 2026-09-16 通过用户验收;`/me/*` 契约同轮收敛;Phase 08/09 复验通过并关闭,可推进 Phase 10(**需用户确认后启动**) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -195,13 +195,19 @@
|
||||
|
||||
**Gate:** V1.0 核心回归通过;活动来源兼容不会创建第二套 Match 逻辑。
|
||||
|
||||
### Phase 09:Text Chat — KEEP-VERIFY/EXTEND
|
||||
### Phase 09:Text Chat — KEEP-VERIFY/EXTEND(2026-09-16 已完成)
|
||||
|
||||
> 复验中发现并修复 4 处真实缺陷:① WebSocket 下行整条链路是死代码(无握手鉴权、无 userId 写入、
|
||||
> `sendToUser` 零调用点);② Unmatch 只改 Match 不联动会话,且发消息只校验会话状态 → 解除匹配后仍可发消息;
|
||||
> ③ `RelationService.block` 调用静态 `MatchService.unmatch`(只改内存态)→ Block 后 Match 未落库;
|
||||
> ④ 聊天四个读/写接口无会话成员校验 → 任意登录用户可读他人私聊(IDOR)。
|
||||
> 详见 `docs/Phase-09-V11-Recovery-Completion-Report-2026-09-16.md`。
|
||||
|
||||
**Files:** `messaging/**`、WebSocket 配置、Miniapp messages/chat 页面。
|
||||
|
||||
- [ ] 验证 REST send + WebSocket receive、消息幂等、重连和未读计数。
|
||||
- [ ] 验证非 active Match 禁聊,Unmatch/Block 后即时禁聊。
|
||||
- [ ] 让活动双确认产生的 Conversation 复用同一消息体系。
|
||||
- [x] 验证 REST send + WebSocket receive、消息幂等、重连和未读计数。
|
||||
- [x] 验证非 active Match 禁聊,Unmatch/Block 后即时禁聊。
|
||||
- [x] 让活动双确认产生的 Conversation 复用同一消息体系。
|
||||
|
||||
**Gate:** 未 Mutual 或活动未双确认时不能发消息;正常会话不重复投递。
|
||||
|
||||
|
||||
Reference in New Issue
Block a user