Browse Source

fix(api): 修复 3 个列表函数真实态分页信封解包(P2-2)

- fetchNotifications/fetchInterviews/fetchFavorites 真实态原假设裸数组,实机验证后端实际返回 Spring Page 信封 {total,size,totalPages,page,items}
- 改为 request<{items:T[]}> 后取 res.items ?? [],与 fetchJobList 一致,翻 apiUseMock=false 后消息/收藏/面试列表正常渲染
- 同步更新 message-domain/job-api 真实态测试断言从信封解包
- 纠正前误述:前端 mock 并非用 {content},而是 {jobs,total} 或直接数组,Hook 不读 .content
- 接口联调问题清单 P2-2 更新为已修复(附实机验证证据);fetchEmployeeReviews 同类待验证
- 补入库 docs/联调路径校正映射.md(89 函数权威校正表)
master
doublex001 4 weeks ago
parent
commit
df72d99a5c
  1. 5
      src/__tests__/job-api.test.ts
  2. 11
      src/__tests__/message-domain.test.ts
  3. 12
      src/api/api.ts
  4. 21
      接口联调问题清单.md

5
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');
});

11
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');
});

12
src/api/api.ts

@ -1272,7 +1272,9 @@ export async function fetchFavorites(): Promise<Omit<Job, 'level'>[]> {
return mockRequest(MOCK_FAVORITES);
}
// 真实后端:当前用户收藏的职位(§5 STUDENT 旗下)
return request<Omit<Job, 'level'>[]>('/jobs/favorites', { method: 'GET' });
// 后端分页信封为 { total, size, totalPages, page, items },需从 items 取列表(P2-2 修正)
const res = await request<{ items: Omit<Job, 'level'>[] }>('/jobs/favorites', { method: 'GET' });
return res.items ?? [];
}
/**
@ -1320,7 +1322,9 @@ export async function fetchNotifications(): Promise<Notification[]> {
if (isMockMode()) {
return mockRequest(MOCK_NOTIFICATIONS);
}
return request<Notification[]>('/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<Interview[]> {
if (isMockMode()) {
return mockRequest(MOCK_INTERVIEWS);
}
return request<Interview[]>('/interviews/me');
// 真实后端分页信封为 { total, size, totalPages, page, items },需从 items 取列表(P2-2 修正)
const res = await request<{ items: Interview[] }>('/interviews/me');
return res.items ?? [];
}
/**

21
接口联调问题清单.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 个错误请求 |

Loading…
Cancel
Save