Browse Source

feat(api): 加 mock 开关并转岗位域候选读取接口(业务端点连接口)

承接后端联调地基,开始接入真实业务端点:

- 请求层新增 mock 开关:http.ts 的 isMockMode()/setMockMode() 读 app.config.js 的
  extra.apiUseMock(默认 true)。业务函数 if (isMockMode()) 返回本地数据不触网,
  联调期置 false 直连后端,避免"全转真实破坏开发期 UI"。RequestOptions 加 query
  支持(数组逗号拼接、undefined 跳过)。
- 岗位域 6 个候选侧读取接口改造成统一形态(保留签名与返回类型 T,仅换函数体):
  fetchJobList→GET /jobs(pageSize→size 映射)、fetchJobDetail→/jobs/{id}、
  fetchJobOverallScore→/jobs/{id}/score、fetchJobPreference→/profile/me/job-preference、
  fetchFavorites→/profile/me/favorites、fetchJobProgress→/tools/job-progress。
  旧 @url 多带 /api 前缀、未带 /v1,已按规范 §7 重映射。
- auth.ts 6 个鉴权函数同样挂 isMockMode() 分支(mock 返回本地双 Token/成功),
  保开发期 parity——否则 dev 登录会直打真后端必崩。
- 新增 job-api.test.ts(8 用例:开关两面 + query 拼装);password.test.ts 鉴权断言改在
  mock 关闭态校验真实契约。
- 文档 后端联调前端实现.md 补 §三 业务端点转换进度与路径重映射表。

OpenAPI 主机在本环境被网络封禁、无法取真实字段,故 T 沿用现有项目类型(AGENTS.md
"保留签名只换函数体"),真实结构待真机联调按 OpenAPI 校正。
tsc 0 错误;eslint 0 错误;jest 406 passed / 29 suites。
master
doublex001 1 month ago
parent
commit
075f7c1f41
  1. 2
      app.config.js
  2. 155
      src/__tests__/job-api.test.ts
  3. 4
      src/__tests__/password.test.ts
  4. 55
      src/api/api.ts
  5. 25
      src/api/auth.ts
  6. 37
      src/api/http.ts
  7. 2
      src/api/index.ts
  8. 33
      后端联调前端实现.md

2
app.config.js

@ -77,6 +77,8 @@ export default {
extra: {
// 业务接口基地址(规范 §1)。默认生产 HTTPS 域名;联调/本地可用环境变量 API_BASE_URL 覆盖。
apiBaseUrl: process.env.API_BASE_URL || 'https://job.youthome.com.cn/api/v1',
// mock 开关(默认开):开发期不触网、不破坏 UI;联调期置 false 统一走真实后端。
apiUseMock: true,
// EAS 项目 ID:`eas init` 后会自动注入此处(与 updates.url 中的 PROJECT_ID 对应)。
eas: {
projectId: 'REPLACE_WITH_PROJECT_ID',

155
src/__tests__/job-api.test.ts

@ -0,0 +1,155 @@
import {
fetchJobList,
fetchJobDetail,
fetchJobOverallScore,
fetchJobPreference,
fetchFavorites,
fetchJobProgress,
} from '../api/api';
import { setMockMode, API_BASE_URL } from '../api/http';
// expo-constants:extra 为空 → 走默认生产域名;mock 开关由 setMockMode 控制
jest.mock('expo-constants', () => ({ expoConfig: { extra: {} } }));
// token-store:内存双 Token,避免依赖 AsyncStorage
jest.mock('../lib/token-store', () => {
let mem: { accessToken: string; refreshToken: string; expiresAt?: number } | null = null;
return {
getAccessToken: jest.fn(() => mem?.accessToken ?? null),
getTokens: jest.fn(() => mem),
setTokens: jest.fn((a: string, r: string, e?: number) => {
mem = { accessToken: a, refreshToken: r, expiresAt: e };
}),
clearTokens: jest.fn(() => {
mem = null;
}),
hydrateTokens: jest.fn(async () => undefined),
};
});
// storage:内存实现,供 resolveJobPreference 等读取引导资料
jest.mock('../lib/storage', () => {
const m = new Map<string, string>();
return {
storageGet: jest.fn(async (k: string) => m.get(k) ?? null),
storageSet: jest.fn(async (k: string, v: string) => {
m.set(k, v);
}),
storageRemove: jest.fn(async (k: string) => {
m.delete(k);
}),
};
});
interface MockResponseSpec {
status: number;
body: unknown;
}
type Responder = (url: string, init: RequestInit) => MockResponseSpec;
let responder: Responder | null = null;
let fetchCalls: Array<{ url: string; init: RequestInit }> = [];
function installFetch(): void {
(global as unknown as { fetch: jest.Mock }).fetch = jest.fn(
async (url: string, init: RequestInit) => {
fetchCalls.push({ url, init });
const spec = responder
? responder(url, init)
: { status: 200, body: { code: 200, message: 'ok', data: null, requestId: 'r', timestamp: 't' } };
return {
status: spec.status,
ok: spec.status >= 200 && spec.status < 300,
headers: { get: () => 'r' },
text: async () => JSON.stringify(spec.body),
} as unknown as Response;
},
);
}
function okData(data: unknown): MockResponseSpec {
return { status: 200, body: { code: 200, message: 'ok', data, requestId: 'r', timestamp: 't' } };
}
beforeEach(() => {
fetchCalls = [];
responder = null;
installFetch();
});
describe('岗位域 mock 开关', () => {
it('mock 模式:fetchJobDetail 返回本地详情且不发任何请求', async () => {
setMockMode(true);
const detail = await fetchJobDetail('1');
expect(fetchCalls.length).toBe(0);
expect(detail.id).toBe('1');
expect(typeof detail.level).toBe('string');
});
it('真实模式:fetchJobDetail 打 /jobs/{id} 并返回 data', async () => {
setMockMode(false);
responder = () => okData({ id: '1', positionName: '后端工程师' });
const detail = await fetchJobDetail('1');
expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/jobs/1`);
expect(detail.positionName).toBe('后端工程师');
});
it('真实模式:fetchJobOverallScore 打 /jobs/{id}/score', async () => {
setMockMode(false);
responder = () => okData({ totalScore: 88 });
const score = await fetchJobOverallScore('2');
expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/jobs/2/score`);
expect(score.totalScore).toBe(88);
});
it('真实模式:fetchJobPreference 打 /profile/me/job-preference', async () => {
setMockMode(false);
responder = () => okData({ city: '上海', position: '前端' });
await fetchJobPreference();
expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/profile/me/job-preference`);
});
it('真实模式:fetchFavorites 打 /profile/me/favorites', async () => {
setMockMode(false);
responder = () => okData([{ id: '1' }]);
const favs = await fetchFavorites();
expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/profile/me/favorites`);
expect(favs[0].id).toBe('1');
});
it('真实模式:fetchJobProgress 打 /tools/job-progress', async () => {
setMockMode(false);
responder = () => okData({ steps: [] });
await fetchJobProgress();
expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/tools/job-progress`);
});
it('真实模式:fetchJobList 拼装 query(数组逗号、undefined 跳过、分页键 size)', async () => {
setMockMode(false);
responder = () => okData({ jobs: [], total: 0 });
await fetchJobList({
sortBy: 'comprehensive',
salary: ['10-15K'],
experience: ['3-5年'],
});
const url = fetchCalls[0].url;
expect(url.startsWith(`${API_BASE_URL}/jobs?`)).toBe(true);
expect(url).toContain('sortBy=comprehensive');
expect(url).toContain('page=1');
expect(url).toContain('size=10');
expect(url).toContain('salary=10-15K');
// 中文被 URLSearchParams 编码,解码后断言
expect(decodeURIComponent(url)).toContain('experience=3-5年');
// keyword / industry / city / education 未传 → 不出现在 query 中
expect(url).not.toContain('keyword=');
expect(url).not.toContain('industry=');
});
it('mock 模式:fetchJobList 返回本地 mock 且分页字段齐全', async () => {
setMockMode(true);
const res = await fetchJobList({ sortBy: 'comprehensive' });
expect(fetchCalls.length).toBe(0);
expect(Array.isArray(res.jobs)).toBe(true);
expect(typeof res.total).toBe('number');
});
});

4
src/__tests__/password.test.ts

@ -5,7 +5,7 @@ import {
validatePassword,
validatePasswordConfirm,
} from '../api/api';
import { loginByPassword, sendSmsCode } from '../api';
import { loginByPassword, sendSmsCode, setMockMode } from '../api';
import { PASSWORD_RULE_LABEL } from '../constants';
import type { PasswordRuleError } from '../types';
import { isValidPhone, isValidSmsCode } from '../utils';
@ -111,6 +111,7 @@ describe('validators(手机号 / 验证码格式)', () => {
describe('loginByPassword(手机号 + 密码登录 · 真实后端)', () => {
beforeEach(() => {
setMockMode(false);
(global as unknown as { fetch: jest.Mock }).fetch = jest.fn();
});
@ -163,6 +164,7 @@ describe('loginByPassword(手机号 + 密码登录 · 真实后端)', () =>
describe('sendSmsCode(发送验证码 · 真实后端)', () => {
beforeEach(() => {
setMockMode(false);
(global as unknown as { fetch: jest.Mock }).fetch = jest.fn();
});

55
src/api/api.ts

@ -197,6 +197,7 @@ import { storageGet, storageSet, storageRemove } from '../lib/storage';
import { exportResumePdf } from '../lib/resumeExport';
import { clearCacheDir, readCacheBytes } from '../lib/cache';
import { getCarrierNumber, CarrierAuthError, MOCK_CARRIER_PHONE } from '../lib/carrier-sdk';
import { isMockMode, request } from './http';
/** 接口地址(通过 expo-constants 从 app.config.js 的 extra 读取) */
export const API_BASE_URL: string =
@ -1035,11 +1036,7 @@ function mockRequest<T>(data: T): Promise<T> {
export async function fetchJobList(
params: JobListParams,
): Promise<{ jobs: Job[]; total: number }> {
// TODO: 真实请求(后端就绪后替换)
// const query = new URLSearchParams({ sortBy: params.sortBy, page: String(params.page ?? 1) });
// const res = await fetch(`${API_BASE_URL}/api/jobs?${query}`, { method: 'GET' });
// return res.json();
if (isMockMode()) {
// 仅在「默认推荐视图」叠加偏好加分:用户主动选了城市或输入关键词时,
// 一律尊重用户的选择,不额外加分(也把回归面限制在默认视图内)
const hasCityFilter = !!params.city && params.city !== ALL_CITY_LABEL;
@ -1139,6 +1136,25 @@ export async function fetchJobList(
resolve({ jobs, total });
}, MOCK_DELAY);
});
}
// 真实后端:排序/筛选/搜索/分页交给后端,查询参数沿用客户端字段并映射分页键
// (后端用 size 而非 pageSize;数组多选按逗号拼接)。响应结构以 OpenAPI 为准,此处为暂定契约。
return request<{ jobs: Job[]; total: number }>('/jobs', {
method: 'GET',
auth: false,
query: {
sortBy: params.sortBy,
keyword: params.keyword,
industry: params.industry,
city: params.city,
salary: params.salary,
experience: params.experience,
education: params.education,
page: params.page ?? 1,
size: params.pageSize ?? 10,
},
});
}
/* ===================== 我的收藏 / 浏览历史(mock 数据) ===================== */
@ -1162,8 +1178,11 @@ const MOCK_HISTORY: Omit<Job, 'level'>[] = MOCK_JOBS.filter((job) =>
* @returns Promise<Omit<Job, 'level'>[]> 收藏的职位数组
*/
export async function fetchFavorites(): Promise<Omit<Job, 'level'>[]> {
// TODO: const res = await fetch(`${API_BASE_URL}/api/user/favorites`, { method: 'GET' }); return res.json();
if (isMockMode()) {
return mockRequest(MOCK_FAVORITES);
}
// 真实后端:当前用户收藏的职位(§5 STUDENT 旗下)
return request<Omit<Job, 'level'>[]>('/profile/me/favorites', { method: 'GET' });
}
/**
@ -1690,8 +1709,12 @@ async function resolveJobPreference(): Promise<JobPreference> {
* @remark 取值优先级见 `resolveJobPreference`
*/
export async function fetchJobPreference(): Promise<JobPreference> {
// TODO: const res = await fetch(`${API_BASE_URL}/api/user/job-preference`, { method: 'GET' }); return res.json();
if (isMockMode()) {
// 引导采集阶段:偏好从 OnboardingProfile 派生(见 resolveJobPreference),未采集字段留空由页面渲染「未设置」
return mockRequest(await resolveJobPreference());
}
// 真实后端:/profile/me 旗下求职者偏好(§5 STUDENT 前缀)
return request<JobPreference>('/profile/me/job-preference', { method: 'GET' });
}
/**
@ -1713,10 +1736,7 @@ export async function fetchBadges(): Promise<Badge[]> {
* @returns Promise<JobDetail> 岗位详情
*/
export async function fetchJobDetail(id: string): Promise<JobDetail> {
// TODO: 真实请求(后端就绪后替换)
// const res = await fetch(`${API_BASE_URL}/api/jobs/${id}`, { method: 'GET' });
// return res.json();
if (isMockMode()) {
const base = MOCK_JOBS.find((job) => job.id === id);
if (!base) throw new Error(`岗位不存在: ${id}`);
@ -1731,6 +1751,9 @@ export async function fetchJobDetail(id: string): Promise<JobDetail> {
...MOCK_JOB_DETAILS[id],
};
return mockRequest(detail);
}
// 真实后端:公开岗位详情(§7)
return request<JobDetail>(`/jobs/${id}`, { method: 'GET', auth: false });
}
/**
@ -1741,7 +1764,7 @@ export async function fetchJobDetail(id: string): Promise<JobDetail> {
* @returns Promise<JobOverallScore> 综合评分
*/
export async function fetchJobOverallScore(id: string): Promise<JobOverallScore> {
// TODO: const res = await fetch(`${API_BASE_URL}/api/jobs/${id}/score`, { method: 'GET' }); return res.json();
if (isMockMode()) {
// 必须按 id 取该岗位自己的分:原先 `void id;` 直接返回固定的一份(恒 92 分 · 优秀),
// 于是列表卡上「产品经理 78 分 · 良好」点进去变成「92 分 · 优秀」,9 个岗位里只有第 1 个对得上。
const score = jobScoreById(id);
@ -1759,6 +1782,9 @@ export async function fetchJobOverallScore(id: string): Promise<JobOverallScore>
value: Math.round(dim.value * factor),
})),
});
}
// 真实后端:岗位综合评分(§7 公开读取;响应结构以 OpenAPI 为准)
return request<JobOverallScore>(`/jobs/${id}/score`, { method: 'GET', auth: false });
}
/**
@ -2856,8 +2882,11 @@ const MOCK_JOB_PROGRESS: JobProgress = {
* @returns Promise<JobProgress> 求职进度数据
*/
export async function fetchJobProgress(): Promise<JobProgress> {
// TODO: const res = await fetch(`${API_BASE_URL}/api/tools/job-progress`, { method: 'GET' }); return res.json();
if (isMockMode()) {
return mockRequest(MOCK_JOB_PROGRESS);
}
// 真实后端:求职进度时间轴(公开工具数据,无需鉴权)
return request<JobProgress>('/tools/job-progress', { method: 'GET', auth: false });
}
/* ===================== 简历 ===================== */

25
src/api/auth.ts

@ -1,4 +1,4 @@
import { request } from './http';
import { request, isMockMode } from './http';
import type { AppRole } from '../types';
/* ===================== 鉴权接口(真实后端) =====================
@ -50,6 +50,7 @@ export async function sendSmsCode(payload: SmsSendPayload): Promise<void> {
if (!isValidPhone(payload.phone)) {
throw new Error('手机号格式不正确');
}
if (isMockMode()) return;
await request('/auth/sms/send', { method: 'POST', auth: false, body: payload });
}
@ -63,6 +64,9 @@ export async function loginByPassword(payload: PasswordLoginPayload): Promise<Au
if (!isValidPhone(payload.phone)) {
throw new Error('手机号格式不正确');
}
if (isMockMode()) {
return mockAuthTokens(`pwd_${payload.phone}`);
}
return request<AuthTokens>('/auth/password/login', {
method: 'POST',
auth: false,
@ -70,6 +74,15 @@ export async function loginByPassword(payload: PasswordLoginPayload): Promise<Au
});
}
/** mock 形态下返回的双 Token(开发期不触网) */
function mockAuthTokens(nonce: string): AuthTokens {
return {
accessToken: `mock_access_${nonce}`,
refreshToken: `mock_refresh_${nonce}`,
expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000,
};
}
export interface SmsLoginPayload {
phone: string;
code: string;
@ -81,6 +94,9 @@ export interface SmsLoginPayload {
* 除非产品和后端明确确认。保留实现以便联调与兜底。
*/
export async function loginBySms(payload: SmsLoginPayload): Promise<AuthTokens> {
if (isMockMode()) {
return mockAuthTokens(`sms_${payload.phone}`);
}
return request<AuthTokens>('/auth/login', { method: 'POST', auth: false, body: payload });
}
@ -94,6 +110,9 @@ export interface CurrentUser {
/** 拉取当前用户与角色(§4 步骤3):角色以后端为准并映射 */
export async function fetchCurrentUser(): Promise<CurrentUser> {
if (isMockMode()) {
return { id: 'mock-user', role: 'candidate' };
}
const data = await request<{ id: string; role: BackendRole } & Record<string, unknown>>(
'/auth/me',
{ method: 'GET' },
@ -103,6 +122,7 @@ export async function fetchCurrentUser(): Promise<CurrentUser> {
/** 退出登录(§4 步骤5):前端清会话由 AuthContext 负责,这里只通知后端 */
export async function requestLogout(): Promise<void> {
if (isMockMode()) return;
await request('/auth/logout', { method: 'POST' });
}
@ -127,6 +147,9 @@ export async function registerCompany(payload: CompanyRegisterPayload): Promise<
if (!isValidPhone(payload.phone)) {
throw new Error('手机号格式不正确');
}
if (isMockMode()) {
return mockAuthTokens(`company_${payload.phone}`);
}
return request<AuthTokens>('/auth/company/register', {
method: 'POST',
auth: false,

37
src/api/http.ts

@ -23,6 +23,24 @@ import {
export const API_BASE_URL: string =
Constants.expoConfig?.extra?.apiBaseUrl ?? 'https://job.youthome.com.cn/api/v1';
/* ===================== mock 开关 =====================
* 开发期(Expo Go / 联调前)默认开 mock:业务函数返回本地数据,不触网、不破坏 UI。
* 联调期在 app.config.js 的 extra.apiUseMock 设 false,统一翻为真实请求。
* 默认 true 是为了保持当前「app 全程 mock 可跑」的状态;翻开关即进入联调形态。
*/
let mockMode: boolean =
(Constants.expoConfig?.extra?.apiUseMock as boolean | undefined) ?? true;
/** 当前是否处于 mock 模式(业务函数据此决定是否直连后端) */
export function isMockMode(): boolean {
return mockMode;
}
/** 仅供单测切换;生产代码不要调用 */
export function setMockMode(value: boolean): void {
mockMode = value;
}
/** 后端响应外层结构(§3) */
export interface ApiEnvelope<T> {
code: number;
@ -64,6 +82,8 @@ export interface RequestOptions {
auth?: boolean;
/** 超时(毫秒),默认 10000 */
timeoutMs?: number;
/** 查询参数;值为数组时按逗号拼接(后端多选用法),undefined 跳过 */
query?: Record<string, string | number | boolean | undefined | Array<string | number>>;
}
/** 401 清会话回调:由 AuthContext 注册,刷新失败 / 无 refreshToken 时触发 */
@ -94,6 +114,21 @@ interface InternalOptions extends RequestOptions {
_isRefresh?: boolean;
}
function buildQuery(query?: RequestOptions['query']): string {
if (!query) return '';
const params = new URLSearchParams();
Object.entries(query).forEach(([key, value]) => {
if (value === undefined) return;
if (Array.isArray(value)) {
if (value.length > 0) params.set(key, value.join(','));
} else {
params.set(key, String(value));
}
});
const s = params.toString();
return s ? `?${s}` : '';
}
async function doRefresh(): Promise<boolean> {
if (refreshPromise) return refreshPromise;
refreshPromise = (async () => {
@ -128,7 +163,7 @@ async function doRefresh(): Promise<boolean> {
}
async function rawRequest<T>(path: string, opts: InternalOptions): Promise<T> {
const url = `${API_BASE_URL}${path}`;
const url = `${API_BASE_URL}${path}${buildQuery(opts.query)}`;
const requestId = genRequestId();
const headers: Record<string, string> = {
'Content-Type': 'application/json',

2
src/api/index.ts

@ -211,6 +211,8 @@ export {
request,
checkHealth,
setUnauthorizedHandler,
isMockMode,
setMockMode,
API_BASE_URL,
} from './http';
export { ApiError } from './http';

33
后端联调前端实现.md

@ -14,8 +14,9 @@
| 会话 | `src/context/AuthContext.tsx` | `setSession(access,refresh,expiresAt)`;启动时 `hydrateTokens`;注册 401 清会话回调 |
| 角色 | `src/types/user.ts` + `auth.ts` | `AppRole` 扩 `admin`;`mapBackendRole(STUDENT→candidate / COMPANY→enterprise / ADMIN→admin)` |
| 环境 | `app.config.js` | 默认 `apiBaseUrl = https://job.youthome.com.cn/api/v1`(保留 `API_BASE_URL` 环境变量覆盖) |
| mock 开关 | `app.config.js` + `http.ts` | `extra.apiUseMock`(默认 `true`);`isMockMode()` 决定业务函数走 mock 还是真实请求;联调期置 `false` |
业务端点(`jobs` / `profile` / `messages` 等 100+ mock 函数)**仍走 mock**,按 AGENTS.md 约定后续逐个替换为真实 `request()` 调用即可,页面与 hook 零改动。
业务端点按 **mock 开关** 分阶段落地:开发期(`apiUseMock=true`)返回本地数据不触网,联调期(`false`)直连后端。`jobs` / `profile` / `messages` 等 100+ mock 函数**逐个替换**为真实 `request()` 调用,页面与 hook 零改动。
## 二、规范条款逐条对照
@ -33,13 +34,37 @@
| §8 状态码语义 | `pickHttpMessage` 覆盖 400/401/403/404/409/429/500/503;网络异常降级为 `httpStatus=0` |
| §10 安全 | 不写 Token/验证码/密码到日志或前端 DOM;`X-Request-Id` 用于联调报障 |
## 三、明确未做(下一阶段)
## 三、业务端点转换进度(mock 开关 + 岗位域)
> 转换规则:保留函数签名与返回类型 `T`(即 AGENTS.md「保留签名、只换函数体」),`if (isMockMode())` 分支保留本地数据,真实分支走 `request<T>(path, { auth, query })`。路径按规范 §7 重映射(旧 `@url` 多带 `/api` 前缀、未带 `/v1`,已校正)。
### 3.1 已完成(岗位域 · 候选侧读取)
| 函数 | mock 分支 | 真实路径(§7) | 鉴权 | 说明 |
|---|---|---|---|---|
| `fetchJobList` | 本地筛选/排序/分页 | `GET /jobs` | 否 | query 映射:`pageSize→size`;数组多选逗号拼接;`keyword/industry/city/education` 等空值跳过 |
| `fetchJobDetail` | 本地详情 | `GET /jobs/{id}` | 否 | 公开岗位详情 |
| `fetchJobOverallScore` | 本地八维缩放 | `GET /jobs/{id}/score` | 否 | 综合评分;响应结构以 OpenAPI 为准 |
| `fetchJobPreference` | `resolveJobPreference()` 派生 | `GET /profile/me/job-preference` | 是 | 求职者偏好(§5 STUDENT 前缀) |
| `fetchFavorites` | 本地收藏子集 | `GET /profile/me/favorites` | 是 | 当前用户收藏 |
| `fetchJobProgress` | 本地进度 | `GET /tools/job-progress` | 否 | 求职进度工具数据 |
> 鉴权列「是」= 默认带 Bearer;「否」= `auth:false` 匿名读取(§7 公开岗位)。
### 3.2 待转(下一阶段,按业务域分批)
- **岗位详情子资源**:`fetchSalaryData` / `fetchEmployeeReviews` / `createJobReview` / `fetchCompareData`(仍处于 `/api/jobs/:id/...` 旧路径,需校正)。
- **企业端岗位**:`fetchJobManagement` / `fetchJobPublishForm` / `fetchJobEvaluationManagement` / `fetchJobEvaluationEmptyState`(§7 未列,需 OpenAPI 确认前缀)。
- **简历 / 资料**:`fetchResume` / `fetchUserProfile` / `fetchProfileXxx`(STUDENT 前缀)。
- **消息 / 沟通**:`fetchConversations` / `fetchNotifications` / `fetchMessages`(§7 未列)。
- **其余公开读取**:`fetchJobFilters` / `fetchJobSearch` 等若后端拆成独立端点,需补映射。
## 四、明确未做(下一阶段)
- **业务端点转真实**:`jobs`、`profile/me`、`companies/me/onboarding` 等仍 mock,需按 OpenAPI 逐个换 `request()`。
- **WebSocket**:`wss://job.youthome.com.cn/api/v1/ws` 未接入(无对应实时功能)。
- **企业入驻状态机**:`/companies/me/onboarding` 的 `missingFields` / `allowedActions` 驱动 UI 尚未接。
- **其余入口页角色纠正**:仅密码登录页已接 `/auth/me`;微信登录、验证码登录等入口需在登录后补齐角色权威化。
## 四、命名待确认(非阻塞)
## 五、命名待确认(非阻塞)
- 域名 `youthome.com.cn`(青年家)与 App 品牌「有职 / youzhi」不一致;Storage 前缀仍为历史遗留 `qingsongpin:`,**勿擅自改名**(会导致老用户本地态失效)。

Loading…
Cancel
Save