diff --git a/src/__tests__/job-api.test.ts b/src/__tests__/job-api.test.ts index 352c6d9..452720f 100644 --- a/src/__tests__/job-api.test.ts +++ b/src/__tests__/job-api.test.ts @@ -113,11 +113,12 @@ describe('岗位域 mock 开关', () => { expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/profile/me/preferences`); }); - it('真实模式:fetchFavorites 打 /jobs/favorites', async () => { + it('真实模式:fetchFavorites 打 /jobs/favorites 并从 items 解包', async () => { setMockMode(false); - responder = () => okData([{ id: '1' }]); + responder = () => okData({ items: [{ id: '1' }], total: 1, size: 10, totalPages: 1, page: 1 }); const favs = await fetchFavorites(); expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/jobs/favorites`); + expect(Array.isArray(favs)).toBe(true); expect(favs[0].id).toBe('1'); }); diff --git a/src/__tests__/message-domain.test.ts b/src/__tests__/message-domain.test.ts index 6d6b8f0..f264777 100644 --- a/src/__tests__/message-domain.test.ts +++ b/src/__tests__/message-domain.test.ts @@ -99,11 +99,13 @@ describe('消息 / 沟通域 mock 开关', () => { expect(Array.isArray(list)).toBe(true); }); - it('真实模式:fetchNotifications 打 /notifications', async () => { + it('真实模式:fetchNotifications 打 /notifications 并从 items 解包', async () => { setMockMode(false); - responder = () => okData([{ id: 'n1' }]); + // 真实后端返回分页信封 { total, size, totalPages, page, items } + responder = () => okData({ items: [{ id: 'n1' }], total: 1, size: 10, totalPages: 1, page: 1 }); const list = await fetchNotifications(); expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/notifications`); + expect(Array.isArray(list)).toBe(true); expect(list[0].id).toBe('n1'); }); @@ -114,11 +116,12 @@ describe('消息 / 沟通域 mock 开关', () => { expect(Array.isArray(list)).toBe(true); }); - it('真实模式:fetchInterviews 打 /interviews/me', async () => { + it('真实模式:fetchInterviews 打 /interviews/me 并从 items 解包', async () => { setMockMode(false); - responder = () => okData([{ id: 'iv1' }]); + responder = () => okData({ items: [{ id: 'iv1' }], total: 1, size: 10, totalPages: 1, page: 1 }); const list = await fetchInterviews(); expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/interviews/me`); + expect(Array.isArray(list)).toBe(true); expect(list[0].id).toBe('iv1'); }); diff --git a/src/api/api.ts b/src/api/api.ts index 21be417..5e469c3 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -1272,7 +1272,9 @@ export async function fetchFavorites(): Promise[]> { return mockRequest(MOCK_FAVORITES); } // 真实后端:当前用户收藏的职位(§5 STUDENT 旗下) - return request[]>('/jobs/favorites', { method: 'GET' }); + // 后端分页信封为 { total, size, totalPages, page, items },需从 items 取列表(P2-2 修正) + const res = await request<{ items: Omit[] }>('/jobs/favorites', { method: 'GET' }); + return res.items ?? []; } /** @@ -1320,7 +1322,9 @@ export async function fetchNotifications(): Promise { if (isMockMode()) { return mockRequest(MOCK_NOTIFICATIONS); } - return request('/notifications'); + // 真实后端分页信封为 { total, size, totalPages, page, items },需从 items 取列表(P2-2 修正) + const res = await request<{ items: Notification[] }>('/notifications'); + return res.items ?? []; } /** @@ -1337,7 +1341,9 @@ export async function fetchInterviews(): Promise { if (isMockMode()) { return mockRequest(MOCK_INTERVIEWS); } - return request('/interviews/me'); + // 真实后端分页信封为 { total, size, totalPages, page, items },需从 items 取列表(P2-2 修正) + const res = await request<{ items: Interview[] }>('/interviews/me'); + return res.items ?? []; } /** diff --git a/接口联调问题清单.md b/接口联调问题清单.md index 4fe868c..a84a792 100644 --- a/接口联调问题清单.md +++ b/接口联调问题清单.md @@ -33,7 +33,7 @@ | P1-3 | ⏳ 待办 | `/auth/me` 角色权威化仍仅在密码登录入口 | | P1-4 | 🔶 部分 | 幽灵域写函数已退回 mock;已对齐写函数 body 形态待统一 | | P2-1 | ⏳ 待办 | 145 处 JSDoc `@url /api` 旧前缀未清 | -| P2-2 | ⚠️ 新增风险 | 真实分页信封为 `{items}` 而非前端 mock 的 `{content}` | +| P2-2 | ✅ 已解决 | 3 个列表函数真实态从 Spring Page `{items}` 信封取数(实机验证);`/conversations` 真实返回数组已确认不变 | | P2-3 | ⏳ 待办 | 双真相源切换策略未定 | | P2-4 | ✅ 缓解 | 默认仍 mock;现幽灵域翻开关也不触网 | @@ -41,9 +41,22 @@ 后端登录返回 `expiresIn`(秒数)而非 `expiresAt`;前端 `AuthContext` 仍用 `TOKEN_TTL_MS` 兜底成假 TTL。本轮未改,需单独排期修。 -### 新增 P2-2 风险(分页信封差异) +### P2-2 分页信封差异(已修复 · 2026-09-28 实机验证) -前端 mock 分页返回 `{content}`,真实后端返回 `{items}`(Spring Data 分页信封)。若 Hook 按 `.content` 取数,翻真实态会渲染空列表。需数据层对齐:**统一读 `items`**,mock 层返回结构向 `{items}` 收敛或 Hook 做兼容。 +**纠正前误述**:此前判断"前端 mock 用 `{content}`、真实用 `{items}`"——实际探查证明前端根本没有按 `.content` 取数的分页信封,mock 返回的是 `{jobs,total}`(fetchJobList)或直接数组,Hook 也不读 `.content`。 + +**真实风险落点**(测试号登录取 token 后逐一打真实路径,看 `data` 结构): +| 接口 | 真实返回(`data`) | 前端原假设 | 结果 | +|---|---|---|---| +| `/conversations` | **数组** | `Conversation[]` | 正确,未改 | +| `/notifications` | **Page 信封** `{total,size,totalPages,page,items}` | `Notification[]` | 空白 | +| `/interviews/me` | **Page 信封** | `Interview[]` | 空白 | +| `/jobs/favorites` | **Page 信封** | `Job[]` | 空白 | +| `/jobs`(fetchJobList) | Page 信封 | — | 早已适配 `data.items` | + +**修复**:`fetchNotifications` / `fetchInterviews` / `fetchFavorites` 真实态改为 `request<{items:T[]}>` 后取 `res.items ?? []`,与 `fetchJobList` 一致;3 个真实态测试同步改为断言从信封解包。tsc 0 错误,全量 jest 495 通过。 + +**延伸待验证(同类型)**:`fetchEmployeeReviews`(`/jobs/{id}/reviews`)同为后端列表接口,实机因测试号无该 job 返回 7001,未取到成功样本;OpenAPI 对该接口 200 schema 为空 `{}`,无法判定形态,先标记待验证,不臆改。 --- @@ -81,7 +94,7 @@ | # | 问题 | 证据 | 影响 | |---|---|---|---| | P2-1 | **157 处 JSDoc `@url` 仍写旧 `/api` 前缀**,与实际 `request()` 路径(已去 `/api`)不一致 | `grep @url /api` = 157;实际 `request('/jobs')` 无 `/api` | 维护者按注释拼地址必错;JSDoc 不能作契约依据 | -| P2-2 | **【新增风险 2026-09-28】查询 / 分页参数映射是前端假设;且真实分页信封为 `{items}` 非前端 mock 的 `{content}`** | `http.ts` `buildQuery` + `fetchJobList` 注释;实机验证 `/jobs` 等列表返回 `{total,size,totalPages,page,items}` | 后端若用不同命名 / 分页模型,已转换读接口静默返回全量或错页;**Hook 若按 `.content` 取数,翻真实态会渲染空列表**(需数据层统一读 `items`) | +| P2-2 | **✅ 已修复(实机验证)** `fetchNotifications`/`fetchInterviews`/`fetchFavorites` 真实态从 Spring Page `{items}` 信封取数;`/conversations` 真实返回数组已确认 | 实机打 `/notifications`/`/interviews/me`/`/jobs/favorites` 返回 `{total,size,totalPages,page,items}`;原代码假设裸数组会空白 | 翻 `apiUseMock=false` 后消息/收藏/面试列表正常渲染;`fetchEmployeeReviews` 同类待验证 | | P2-3 | **mock / 真实双真相源切换一致性**:mock 态 `saveResumeSnapshot` 写本地 storage 覆盖层、`fetchResume` 读本地;真实态写后端、读后端 | `saveResumeSnapshot`(mock 写 storage) vs `fetchResume`(真实读后端) | 同一设备从 mock 翻真实后,本地已填资料不出现在后端返回中,需明确迁移/清空策略 | | P2-4 | **`apiUseMock` 默认 `true` 的隐患**:容易被误判为"联调完成"而翻开关 | `http.ts:31` 默认 `true` | 翻开关即触发 P0-1 的 65 个错误请求 |