refactor(api): 关系/匹配域采用 2026-09-14 canonical 路径,消除三代并存
## 问题:同一能力存在三代路径,前端指向的是已废止的一代
按 mtime 排查后发现"最新"并非上一轮以为的 /relations/*,而是 2026-09-14 的 match 模块:
| 代际 | 位置 | 日期 | 路径 |
|---|---|---|---|
| canonical | platform.match: Like/Block/MatchController | 09-14 12:13~12:14 | /likes /blocks /matches |
| 二代 | platform.relationship: Relationship(Status)Controller | 09-10 10:59 | /relations/* /relationship/* |
| 一代(规范) | REST API 接口详细设计规范.md | 08-28 | /relationships/*(多一个 hips) |
| 前端 | miniapp/services/matchService.js | 08-29 | 混用二代 + 一代 |
后果:缘分三榜、黑名单页、成功牵手流程全部 404;另有两份重复 success 实现 + 一份桩实现。
## 裁决:采用 2026-09-14 最新路径,前端与二代让位(用户指令)
### 补齐 canonical 模块缺口(此前只有 javadoc 声明或服务方法,无控制器)
- RelationService.unlike() + LikeController `DELETE /likes/{targetUserId}`
- 新增 PassController `POST /passes`(RelationService.pass 在 09-14 模块内却无控制器,补齐与 /likes、/blocks 对称)
- BlockController `GET /blocks`(javadoc 早声明"我的 Block 列表"但未实现,mapper findBlockedByUser 已就绪)
- MatchController `POST /matches/{id}/success/request|confirm|decline`(javadoc 标"Phase 12 预留"),
委派真实实现 RelationshipStatusService;authorizedMatch 对"不存在"与"无权"统一回 404,避免探测他人 Match
- LikeResponse 对齐前端 LikeResult:{ relationStatus, mutualMatch, matchedConversationId, matchId }
Mutual 时调 ChatService.getOrCreateConversation 取会话 id → "双向喜欢→跳聊天"链路真正打通
- LikeController/PassController/BlockController 补 targetUserId 空值校验
### 移除被取代的路由与桩实现
- RelationshipController 移除 like/cancelLike/pass/block/unblock/matches/unmatch/success,仅保留 /fate/*
- RelationshipStatusController 移除 /success/*(已由 /matches/{id}/success/* 取代),保留关系状态生命周期
- RelationshipService 删除 requestSuccess/confirmSuccess 桩 ——
前者自述 "TODO: 查询 relationship_success_requests 表" 却返回硬编码 status="pending",
等于绕过 AGENTS 硬规则 6「成功牵手必须双确认」
- 注意:RelationshipService.like/block/pass 被 P0FixVerificationTest /
RelationshipBlockPriorityPhase08Test 直接测试,方法体保留
### 列表信封统一
- CursorPage 从 chat.dto 提升到 shared.dto(消除 match 模块反向依赖 chat DTO)
- /relations/fate/mutual|liked|likers 与 GET /matches 改返回 { items, nextCursor }
### 前端对齐
- matchService.js 全部路径改到 canonical;补 declineSuccess
- matchStore 补 _decorateBlockedList,修复"模板渲染 blockedAtText 但全仓从未计算"的缺陷
(与上一轮 lastMessageAtText 同源,同类字段一律在 store 装饰一次)
### 文档
- docs/api/API_INVENTORY.md 关系/缘分/匹配三节重写为 canonical 路径;
顶部加 /me/* 路径族显式警告(小程序按规范写 /me/*,后端实为 /profile/*、/photos、/relationship/*,
约 15 个端点会 404,收敛方向未裁决,防止后续照抄踩坑)
- docs/STATUS.md:Backend 回归债务由「待清(277 tests 4F/8E)」更新为「已清偿(297 tests 全绿)」
## 验证
- 新增 CanonicalRouteContractTest(3 例):用 RequestMappingHandlerMapping 断言
canonical 路径已注册、被取代的旧路径不再注册、关系状态生命周期保留 —— 路由再漂移即红
- 后端 297 tests, 0 failures, 0 errors, 3 skipped, BUILD SUCCESS
- 小程序 tsc --noEmit 通过
This commit is contained in:
+115
@@ -0,0 +1,115 @@
|
||||
package com.danyang.match.platform.contract;
|
||||
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* 路由契约测试:锁定「互动/匹配/成功牵手」域的 canonical 路径。
|
||||
*
|
||||
* 背景(2026-09-15):
|
||||
* 同一能力曾同时存在两套路径 —— 2026-09-10 的 relationship 模块
|
||||
* (/relations/{id}/like|pass|block、/relations/mutual|liked|likers、/relations/success/*)
|
||||
* 与 2026-09-14 的 match 模块(/likes、/blocks、/matches)。
|
||||
* 前端 matchService(08-29)走的是前者,且 /me/blacklist、/matches/{id}/success/* 后端完全无实现,
|
||||
* 导致缘分三榜、黑名单页、成功牵手流程全部 404。
|
||||
*
|
||||
* 裁决:采用 2026-09-14 的路径(最新),前端向其对齐,被取代的旧路径移除。
|
||||
* 本测试把这套裁决固化为断言,避免后续再次出现"两套路径并存/前端指向已删路径"。
|
||||
*/
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("test")
|
||||
class CanonicalRouteContractTest {
|
||||
|
||||
/** 按名字取 MVC 的 HandlerMapping —— Actuator 也注册了一个同类型 bean,直接按类型注入会歧义 */
|
||||
@Autowired
|
||||
@Qualifier("requestMappingHandlerMapping")
|
||||
private RequestMappingHandlerMapping handlerMapping;
|
||||
|
||||
/** 当前上下文已注册的全部 URL 模式 */
|
||||
private Set<String> patterns() {
|
||||
return handlerMapping.getHandlerMethods().keySet().stream()
|
||||
.flatMap(info -> info.getPatternValues().stream())
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("互动与匹配的 canonical 路径必须已注册")
|
||||
void canonicalRoutesRegistered() {
|
||||
Set<String> p = patterns();
|
||||
|
||||
// Like / Unlike
|
||||
assertTrue(p.contains("/likes"), "缺少 POST /likes");
|
||||
assertTrue(p.contains("/likes/{targetUserId}"), "缺少 DELETE /likes/{targetUserId}");
|
||||
|
||||
// Pass
|
||||
assertTrue(p.contains("/passes"), "缺少 POST /passes");
|
||||
|
||||
// Block / Unblock / 黑名单列表
|
||||
assertTrue(p.contains("/blocks"), "缺少 POST /blocks 与 GET /blocks");
|
||||
assertTrue(p.contains("/blocks/{targetUserId}"), "缺少 DELETE /blocks/{targetUserId}");
|
||||
|
||||
// Match
|
||||
assertTrue(p.contains("/matches"), "缺少 GET /matches");
|
||||
assertTrue(p.contains("/matches/{id}"), "缺少 GET /matches/{id}");
|
||||
assertTrue(p.contains("/matches/{id}/unmatch"), "缺少 POST /matches/{id}/unmatch");
|
||||
|
||||
// 成功牵手双确认(AGENTS 硬规则 6)
|
||||
assertTrue(p.contains("/matches/{id}/success/request"), "缺少 POST /matches/{id}/success/request");
|
||||
assertTrue(p.contains("/matches/{id}/success/confirm"), "缺少 POST /matches/{id}/success/confirm");
|
||||
assertTrue(p.contains("/matches/{id}/success/decline"), "缺少 POST /matches/{id}/success/decline");
|
||||
|
||||
// 缘分三榜(无更新归属,保留在此前缀下)
|
||||
assertTrue(p.contains("/relations/fate/mutual"), "缺少 GET /relations/fate/mutual");
|
||||
assertTrue(p.contains("/relations/fate/liked"), "缺少 GET /relations/fate/liked");
|
||||
assertTrue(p.contains("/relations/fate/likers"), "缺少 GET /relations/fate/likers");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("被 2026-09-14 路径取代的旧路由必须不再注册")
|
||||
void supersededRoutesRemoved() {
|
||||
Set<String> p = patterns();
|
||||
|
||||
assertFalse(p.contains("/relations/{targetUserId}/like"),
|
||||
"旧路径 POST/DELETE /relations/{targetUserId}/like 应已由 /likes 取代");
|
||||
assertFalse(p.contains("/relations/{targetUserId}/pass"),
|
||||
"旧路径 POST /relations/{targetUserId}/pass 应已由 /passes 取代");
|
||||
assertFalse(p.contains("/relations/{targetUserId}/block"),
|
||||
"旧路径 POST/DELETE /relations/{targetUserId}/block 应已由 /blocks 取代");
|
||||
assertFalse(p.contains("/relations/matches"),
|
||||
"旧路径 GET /relations/matches 应已由 /matches 取代");
|
||||
assertFalse(p.contains("/relations/matches/{targetUserId}/unmatch"),
|
||||
"旧路径应已由 /matches/{id}/unmatch 取代");
|
||||
assertFalse(p.contains("/relations/success/request"),
|
||||
"桩实现 /relations/success/request 应已由 /matches/{id}/success/request 取代");
|
||||
assertFalse(p.contains("/relations/success/confirm"),
|
||||
"桩实现 /relations/success/confirm 应已由 /matches/{id}/success/confirm 取代");
|
||||
assertFalse(p.contains("/relationship/success/request"),
|
||||
"重复路由 /relationship/success/request 应已由 /matches/{id}/success/request 取代");
|
||||
assertFalse(p.contains("/relationship/success/confirm"),
|
||||
"重复路由 /relationship/success/confirm 应已由 /matches/{id}/success/confirm 取代");
|
||||
assertFalse(p.contains("/relationship/success/decline"),
|
||||
"重复路由 /relationship/success/decline 应已由 /matches/{id}/success/decline 取代");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("关系状态生命周期端点保留在 /relationship 下")
|
||||
void relationshipStatusLifecycleKept() {
|
||||
Set<String> p = patterns();
|
||||
assertTrue(p.contains("/relationship/status"), "缺少 GET /relationship/status");
|
||||
assertTrue(p.contains("/relationship/pause"), "缺少 PUT /relationship/pause");
|
||||
assertTrue(p.contains("/relationship/resume"), "缺少 PUT /relationship/resume");
|
||||
assertTrue(p.contains("/relationship/getting-to-know"), "缺少 PUT /relationship/getting-to-know");
|
||||
assertTrue(p.contains("/relationship/exit"), "缺少 PUT /relationship/exit");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user