diff --git a/src/__tests__/backend-bridge-remaining.test.ts b/src/__tests__/backend-bridge-remaining.test.ts new file mode 100644 index 0000000..521b01c --- /dev/null +++ b/src/__tests__/backend-bridge-remaining.test.ts @@ -0,0 +1,249 @@ +/** + * 后端桥接(剩余域)consolidated 测试 + * 覆盖第三轮之后批量转换的 62 个 fetch 函数: + * - 真实态(setMockMode(false)):仅对已与后端 OpenAPI 对齐的函数断言 request 命中真实路径 + * (见 REAL_FUNCS;其余为幽灵域,已退回 mock-only,翻真实开关也不触网,不在此断言) + * - mock 态(默认):断言返回非 undefined(防止 mock 分支被意外破坏) + * 路径依据 scripts/openapi.json(job.youthome.com.cn)实测校正,不再标【待确认】。 + */ +// 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:内存实现,避免依赖 AsyncStorage +jest.mock('../lib/storage', () => { + const m = new Map(); + 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); + }), + }; +}); + +import { + fetchArticles, + fetchMenuItems, + fetchEnterpriseEntry, + fetchRankData, + fetchSkillCloud, + fetchSkillLearningPath, + fetchSalaryToolLinks, + fetchSalaryBrackets, + fetchOperationRules, + fetchHollandIntro, + fetchIdentityOptions, + fetchGameLevels, + fetchLegalBrand, + fetchLegalServices, + fetchLegalFAQs, + fetchLegalTemplates, + fetchLegalPlans, + fetchAboutInfo, + fetchQualifications, + fetchAITools, + fetchHistory, + fetchHollandAssessment, + fetchHollandResult, + fetchAbilityAssessment, + fetchUserStats, + fetchOverallScore, + fetchDimensionScores, + fetchBadges, + fetchOfferCompareData, + fetchPendingOfferCount, + fetchWeightSettings, + fetchSalaryRecords, + fetchAIInterviewData, + fetchHREvaluation, + fetchAppealForm, + fetchEnterpriseOverview, + fetchJobPublishForm, + fetchJobEvaluationManagement, + fetchCandidateDetail, + fetchInterviewScore, + fetchInterviewScoreDraft, + fetchAuditReport, + fetchEnterpriseCertification, + fetchJobManagement, + fetchTalentRecommendation, + fetchEnterpriseCommunication, + fetchJobEvaluationEmptyState, + fetchGameProgress, + fetchLegalMembership, + fetchLegalOrders, + fetchLegalServiceRequests, + fetchLegalConversation, + fetchPrivacySettings, + fetchPrivacyCenter, + fetchPrivacyDetailSetting, + fetchDndSetting, + fetchKeywordBlock, + fetchAccountCancel, + fetchLegalDocument, + fetchPersonalDataExport, + fetchNotificationSettings, + fetchAccountSecurity, +} from '../api/api'; +import { setMockMode } from '../api/http'; + +// [fn, args, expectedPath, mockSmoke] +type Entry = [ + (...a: any[]) => Promise, + any[], + string, + boolean, +]; + +const FUNCS: Entry[] = [ + // 公开读取(auth:false) + [fetchArticles, [], '/growth/articles', true], + [fetchMenuItems, ['candidate'], '/user/menus', true], + [fetchEnterpriseEntry, [], '/user/enterprise-entry', true], + [fetchRankData, ['1'], '/jobs/rank', false], + [fetchSkillCloud, ['it'], '/tools/skill-cloud', false], + [fetchSkillLearningPath, ['it'], '/tools/skill-learning-path', false], + [fetchSalaryToolLinks, [], '/tools/salary-related', true], + [fetchSalaryBrackets, [], '/tools/salary-brackets', true], + [fetchOperationRules, [], '/operation/rules', true], + [fetchHollandIntro, [], '/growth/holland-intro', true], + [fetchIdentityOptions, [], '/tools/identity-options', true], + [fetchGameLevels, [], '/game/levels', true], + [fetchLegalBrand, [], '/legal/brand', true], + [fetchLegalServices, [], '/legal/services', true], + [fetchLegalFAQs, [], '/legal/faqs', true], + [fetchLegalTemplates, [], '/legal/templates', true], + [fetchLegalPlans, [], '/legal/plans', true], + [fetchAboutInfo, [], '/settings/about', true], + [fetchQualifications, [], '/settings/qualifications', true], + [fetchAITools, [], '/growth/ai-tools', true], + // 用户/企业私有(auth:true) + [fetchHistory, [], '/user/history', true], + [fetchHollandAssessment, [], '/growth/holland', true], + [fetchHollandResult, [], '/growth/holland/result', true], + [fetchAbilityAssessment, [], '/growth/ability', true], + [fetchUserStats, [], '/user/stats', true], + [fetchOverallScore, [], '/user/overall-score', true], + [fetchDimensionScores, [], '/user/dimension-scores', true], + [fetchBadges, [], '/user/badges', true], + [fetchOfferCompareData, [undefined], '/offers/compare', true], + [fetchPendingOfferCount, [], '/offers/pending/count', true], + [fetchWeightSettings, [], '/jobs/weight-settings', true], + [fetchSalaryRecords, [], '/salary/records', true], + [fetchAIInterviewData, [], '/interview/config', true], + [fetchHREvaluation, [], '/evaluation/hr', true], + [fetchAppealForm, [], '/evaluation/appeal', true], + [fetchEnterpriseOverview, [], '/enterprise/overview', true], + [fetchJobPublishForm, [], '/enterprise/jobs/publish-form', true], + [fetchJobEvaluationManagement, [], '/enterprise/job-evaluations', true], + [fetchCandidateDetail, ['1'], '/enterprise/candidates/1', false], + [fetchInterviewScore, ['1'], '/enterprise/interviews/1/score', false], + [fetchInterviewScoreDraft, ['1'], '/enterprise/interviews/1/score/draft', false], + [fetchAuditReport, [], '/operation/audit-report', true], + [fetchEnterpriseCertification, [], '/enterprise/certification', true], + [fetchJobManagement, [], '/enterprise/job-management', true], + [fetchTalentRecommendation, [], '/enterprise/talent-recommendation', true], + [fetchEnterpriseCommunication, [], '/enterprise/communication', true], + [fetchJobEvaluationEmptyState, [], '/enterprise/job-evaluation-empty', true], + [fetchGameProgress, [], '/game/progress', true], + [fetchLegalMembership, [], '/legal/membership', true], + [fetchLegalOrders, [], '/legal/orders', true], + [fetchLegalServiceRequests, [], '/legal/service-requests', true], + [fetchLegalConversation, [], '/legal/conversations/default', true], + [fetchPrivacySettings, [], '/settings/privacy', true], + [fetchPrivacyCenter, [], '/settings/privacy-center', true], + [fetchPrivacyDetailSetting, ['1'], '/settings/privacy/1', false], + [fetchDndSetting, [], '/settings/privacy/dnd', true], + [fetchKeywordBlock, [], '/settings/privacy/keywords', true], + [fetchAccountCancel, [], '/settings/account-cancel', true], + [fetchLegalDocument, ['1'], '/settings/legal-document/1', false], + [fetchPersonalDataExport, [], '/settings/personal-data-export', true], + [fetchNotificationSettings, [], '/settings/notification', true], + [fetchAccountSecurity, [], '/settings/account-security', true], +]; + +const ENVELOPE = { + code: 0, + message: '', + data: null, + requestId: 'r', + timestamp: '', +}; + +// 已与后端 OpenAPI 对齐(ALIGNED + WRONG→REAL)的函数:真实态打真实路径 +// 幽灵域(PHANTOM,后端无契约)已退回 mock-only,翻真实开关也不触网,不在此断言路径 +const REAL_FUNCS: Array<[(...a: any[]) => Promise, any[], string]> = [ + [fetchRankData, ['1'], '/jobs/1/competition-ranking'], + [fetchCandidateDetail, ['1'], '/company/candidates/1'], + [fetchInterviewScore, ['1'], '/interviews/1/score'], + [fetchInterviewScoreDraft, ['1'], '/interviews/1/score'], + [fetchOfferCompareData, [undefined], '/offers/compare'], +]; + +describe('后端桥接 - 真实态路径断言(已对齐函数)', () => { + const originalFetch = (global as any).fetch; + + beforeEach(() => { + (global as any).fetch = jest.fn(async () => ({ + ok: true, + status: 200, + headers: { get: () => null }, + json: async () => ENVELOPE, + text: async () => '', + })); + setMockMode(false); + }); + + afterEach(() => { + setMockMode(true); + }); + + afterAll(() => { + (global as any).fetch = originalFetch; + }); + + it('已对齐函数数量正确(5 个)', () => { + expect(REAL_FUNCS.length).toBe(5); + }); + + REAL_FUNCS.forEach(([fn, args, path], i) => { + it(`#${i} 命中真实路径 ${path}`, async () => { + await fn(...args); + const calls = ((global as any).fetch as jest.Mock).mock.calls; + expect(calls.length).toBeGreaterThan(0); + const calledUrl: string = calls[calls.length - 1][0]; + expect(calledUrl).toContain(path); + }); + }); +}); + +describe('后端桥接 - mock 态冒烟(无回归)', () => { + beforeEach(() => { + setMockMode(true); + }); + + FUNCS.filter((e) => e[3]).forEach(([fn, args, path], idx) => { + it(`mock 态 ${path} 返回非 undefined`, async () => { + const result = await fn(...args); + expect(result).not.toBeUndefined(); + }); + }); +}); diff --git a/src/__tests__/backend-bridge-write.test.ts b/src/__tests__/backend-bridge-write.test.ts new file mode 100644 index 0000000..28526a6 --- /dev/null +++ b/src/__tests__/backend-bridge-write.test.ts @@ -0,0 +1,141 @@ +/** + * 后端桥接(写入类)测试 + * 覆盖本轮转换的 12 个写函数(简历 8 + 求职偏好 + 权重 + 薪资 2): + * - 真实态(setMockMode(false)):断言 request 命中预期方法 + 相对路径 + body + * - mock 态(默认):断言返回本地派生/生成值(证明 mock 分支未被破坏,本地 storage 真写入仍可用) + * 路径依据 scripts/openapi.json(job.youthome.com.cn)实测校正,不再标【待确认】。 + * 幽灵域写函数已退回 mock-only,真实态不在此断言网络路径。 + */ +// 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:内存实现,避免依赖 AsyncStorage +jest.mock('../lib/storage', () => { + const m = new Map(); + 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); + }), + }; +}); + +import { setMockMode } from '../api/http'; +import { + saveResumeBasicInfo, + saveResumeSelfEvaluation, + saveResumeSkills, + saveResumeEducation, + saveResumeExperience, + saveResumeProjects, + saveResumeSnapshot, + saveResumeExportSettings, + saveJobPreference, + saveWeightSettings, + saveSalaryRecord, + deleteSalaryRecord, +} from '../api/api'; + +const ENVELOPE = { code: 0, message: 'ok', data: null, requestId: 'r', timestamp: 0 }; + +let fetchMock: jest.Mock; + +beforeEach(() => { + fetchMock = jest.fn(async () => ({ + ok: true, + status: 200, + json: async () => ENVELOPE, + text: async () => '', + headers: { get: () => 'x' }, + })); + (global as any).fetch = fetchMock; + setMockMode(false); // 默认跑真实态 +}); + +/** 触发一次真实请求,返回最后一个 fetch 调用的 options */ +async function expectRequest( + call: () => Promise, + path: string, + method: string, +): Promise { + await call(); + const last = fetchMock.mock.calls[fetchMock.mock.calls.length - 1]; + expect(last[0]).toContain(path); + expect((last[1] as any).method).toBe(method); + return last[1] as any; +} + +describe('写入函数真实态:方法 + 相对路径 + body(仅已对齐函数)', () => { + beforeEach(() => setMockMode(false)); + + test('saveJobPreference → PUT /profile/me/preferences', async () => { + const preference = { city: '北京', salary: '15k' } as any; + const opts = await expectRequest( + () => saveJobPreference(preference), + '/profile/me/preferences', + 'PUT', + ); + expect(JSON.parse(opts.body)).toEqual(preference); + }); +}); + +/** + * 以下写函数属幽灵域(后端无 OpenAPI 契约),已退回 mock-only,翻真实开关也不触网, + * 故真实态不再断言网络路径;其 mock 态本地派生/生成行为见下方 describe。 + */ + +describe('写入函数 mock 态:本地派生/生成行为不变(开发期真写入仍可用)', () => { + beforeEach(() => setMockMode(true)); + + test('saveSalaryRecord 本地生成 id 与时间戳', async () => { + const rec = await saveSalaryRecord({} as any, {} as any, 'note'); + expect(rec.id.startsWith('salary-')).toBe(true); + expect(typeof rec.createdAt).toBe('number'); + expect(rec.note).toBe('note'); + }); + + test('saveResumeBasicInfo 本地派生 avatar', async () => { + const info = await saveResumeBasicInfo({ name: '张三' } as any); + expect(info.avatar).toBeTruthy(); + }); + + test('saveJobPreference 原样返回入参', async () => { + const p = { city: '北京' } as any; + expect(await saveJobPreference(p)).toBe(p); + }); + + test('saveResumeSkills 本地去空去重', async () => { + const out = await saveResumeSkills([ + { name: 'a' }, + { name: 'a' }, + { name: ' ' }, + ] as any); + expect(out).toEqual([{ name: 'a' }]); + }); + + test('saveWeightSettings 返回副本(不与入参同引用)', async () => { + const dims = [{ id: 'd1' }] as any; + const out = await saveWeightSettings(dims); + expect(out).not.toBe(dims); + expect(out).toEqual(dims); + }); +}); diff --git a/src/__tests__/job-api.test.ts b/src/__tests__/job-api.test.ts index c5c6878..352c6d9 100644 --- a/src/__tests__/job-api.test.ts +++ b/src/__tests__/job-api.test.ts @@ -5,6 +5,10 @@ import { fetchJobPreference, fetchFavorites, fetchJobProgress, + fetchSalaryData, + fetchEmployeeReviews, + createJobReview, + fetchCompareData, } from '../api/api'; import { setMockMode, API_BASE_URL } from '../api/http'; @@ -102,43 +106,66 @@ describe('岗位域 mock 开关', () => { expect(score.totalScore).toBe(88); }); - it('真实模式:fetchJobPreference 打 /profile/me/job-preference', async () => { + it('真实模式:fetchJobPreference 打 /profile/me/preferences', async () => { setMockMode(false); responder = () => okData({ city: '上海', position: '前端' }); await fetchJobPreference(); - expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/profile/me/job-preference`); + expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/profile/me/preferences`); }); - it('真实模式:fetchFavorites 打 /profile/me/favorites', async () => { + it('真实模式:fetchFavorites 打 /jobs/favorites', async () => { setMockMode(false); responder = () => okData([{ id: '1' }]); const favs = await fetchFavorites(); - expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/profile/me/favorites`); + expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/jobs/favorites`); expect(favs[0].id).toBe('1'); }); - it('真实模式:fetchJobProgress 打 /tools/job-progress', async () => { + it('真实模式:fetchJobProgress 打 /profile/me/career-progress', async () => { setMockMode(false); responder = () => okData({ steps: [] }); await fetchJobProgress(); - expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/tools/job-progress`); + expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/profile/me/career-progress`); }); - it('真实模式:fetchJobList 拼装 query(数组逗号、undefined 跳过、分页键 size)', async () => { + it('真实模式:fetchJobList 走 /jobs/search 且 sort 映射 relevance(数组逗号、undefined 跳过、分页键 size)', async () => { setMockMode(false); - responder = () => okData({ jobs: [], total: 0 }); + // 后端返回空 items 时回退 mock,这里给非空 items 让真实分支走完 + responder = () => + okData({ + total: 1, + size: 10, + totalPages: 1, + page: 1, + items: [ + { + id: '1', + companyName: '测试公司', + companyLocation: '北京', + positionName: '前端工程师', + salaryMin: 20, + salaryMax: 30, + score: 85, + experience: '3-5年', + education: '本科', + industry: '互联网', + city: '北京', + benefits: ['五险一金'], + }, + ], + }); 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.startsWith(`${API_BASE_URL}/jobs/search?`)).toBe(true); + // sortBy: 'comprehensive' → 后端 sort: 'relevance' + expect(url).toContain('sort=relevance'); 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='); @@ -153,3 +180,72 @@ describe('岗位域 mock 开关', () => { expect(typeof res.total).toBe('number'); }); }); + +describe('岗位详情子资源 mock 开关', () => { + it('mock 模式:fetchSalaryData 返回本地薪资且不发请求', async () => { + setMockMode(true); + const data = await fetchSalaryData('1'); + expect(fetchCalls.length).toBe(0); + expect(data).toBeDefined(); + }); + + it('真实模式:fetchSalaryData 打 /jobs/{id}/salary', async () => { + setMockMode(false); + responder = () => okData({ min: 10, max: 15 }); + const data = await fetchSalaryData('7'); + expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/jobs/7/salary`); + expect(data.min).toBe(10); + }); + + it('mock 模式:fetchEmployeeReviews 返回本地评价', async () => { + setMockMode(true); + const reviews = await fetchEmployeeReviews('1'); + expect(fetchCalls.length).toBe(0); + expect(Array.isArray(reviews)).toBe(true); + }); + + it('真实模式:fetchEmployeeReviews 打 /jobs/{id}/reviews', async () => { + setMockMode(false); + responder = () => okData([{ id: 'r1', content: '不错' }]); + const reviews = await fetchEmployeeReviews('3'); + expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/jobs/3/reviews`); + expect(reviews[0].id).toBe('r1'); + }); + + it('mock 模式:createJobReview 本地持久化且不发请求', async () => { + setMockMode(true); + const review = await createJobReview('1', '环境友好'); + expect(fetchCalls.length).toBe(0); + expect(review.content).toBe('环境友好'); + expect(review.id).toContain('my-review-'); + }); + + it('真实模式:createJobReview POST /jobs/{id}/reviews 且 body 含 content', async () => { + setMockMode(false); + responder = (url, init) => { + expect(url).toBe(`${API_BASE_URL}/jobs/4/reviews`); + expect(init.method).toBe('POST'); + const body = JSON.parse(String(init.body)) as { content: string }; + expect(body.content).toBe('弹性办公'); + return okData({ id: 'srv-1', content: '弹性办公' }); + }; + const review = await createJobReview('4', '弹性办公'); + expect(review.id).toBe('srv-1'); + }); + + it('mock 模式:fetchCompareData 返回本地过滤数据', async () => { + setMockMode(true); + const data = await fetchCompareData(['j1', 'j2']); + expect(fetchCalls.length).toBe(0); + expect(Array.isArray(data.jobs)).toBe(true); + }); + + it('真实模式:fetchCompareData 打 /jobs/compare 且 jobIds 数组逗号拼接', async () => { + setMockMode(false); + responder = () => okData({ jobs: [], dimensionScores: [], basicInfo: { fields: [], values: [] }, benefits: [] }); + await fetchCompareData(['a', 'b', 'c']); + const url = fetchCalls[0].url; + expect(url.startsWith(`${API_BASE_URL}/jobs/compare?`)).toBe(true); + expect(decodeURIComponent(url)).toContain('jobIds=a,b,c'); + }); +}); diff --git a/src/__tests__/message-domain.test.ts b/src/__tests__/message-domain.test.ts new file mode 100644 index 0000000..6d6b8f0 --- /dev/null +++ b/src/__tests__/message-domain.test.ts @@ -0,0 +1,146 @@ +import { + fetchConversations, + fetchNotifications, + fetchInterviews, + fetchMatchResult, + fetchConversation, +} 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:内存实现 +jest.mock('../lib/storage', () => { + const m = new Map(); + 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 模式:fetchConversations 返回本地会话且不发请求', async () => { + setMockMode(true); + const list = await fetchConversations(); + expect(fetchCalls.length).toBe(0); + expect(Array.isArray(list)).toBe(true); + }); + + it('真实模式:fetchConversations 打 /conversations', async () => { + setMockMode(false); + responder = () => okData([{ id: 'c1' }]); + const list = await fetchConversations(); + expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/conversations`); + expect(list[0].id).toBe('c1'); + }); + + it('mock 模式:fetchNotifications 返回本地通知且不发请求', async () => { + setMockMode(true); + const list = await fetchNotifications(); + expect(fetchCalls.length).toBe(0); + expect(Array.isArray(list)).toBe(true); + }); + + it('真实模式:fetchNotifications 打 /notifications', async () => { + setMockMode(false); + responder = () => okData([{ id: 'n1' }]); + const list = await fetchNotifications(); + expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/notifications`); + expect(list[0].id).toBe('n1'); + }); + + it('mock 模式:fetchInterviews 返回本地日程且不发请求', async () => { + setMockMode(true); + const list = await fetchInterviews(); + expect(fetchCalls.length).toBe(0); + expect(Array.isArray(list)).toBe(true); + }); + + it('真实模式:fetchInterviews 打 /interviews/me', async () => { + setMockMode(false); + responder = () => okData([{ id: 'iv1' }]); + const list = await fetchInterviews(); + expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/interviews/me`); + expect(list[0].id).toBe('iv1'); + }); + + it('mock 模式:fetchMatchResult 返回本地结果且不发请求', async () => { + setMockMode(true); + const result = await fetchMatchResult(); + expect(fetchCalls.length).toBe(0); + expect(result).toBeDefined(); + }); + + it('mock 模式:fetchConversation 透传 id 且不发请求', async () => { + setMockMode(true); + const detail = await fetchConversation('conv-9'); + expect(fetchCalls.length).toBe(0); + expect(detail.id).toBe('conv-9'); + }); + + it('真实模式:fetchConversation 打 /conversations/{id}', async () => { + setMockMode(false); + responder = () => okData({ id: 'conv-9', messages: [] }); + const detail = await fetchConversation('conv-9'); + expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/conversations/conv-9`); + expect(detail.id).toBe('conv-9'); + }); +}); diff --git a/src/__tests__/password.test.ts b/src/__tests__/password.test.ts index 685ae52..b4ec253 100644 --- a/src/__tests__/password.test.ts +++ b/src/__tests__/password.test.ts @@ -128,7 +128,15 @@ describe('loginByPassword(手机号 + 密码登录 · 真实后端)', () => makeResponse(200, { code: 200, message: 'ok', - data: { accessToken: 'A', refreshToken: 'R' }, + data: { + userId: 123, + phone: '13800138000', + nickname: '测试用户', + role: 'STUDENT', + accessToken: 'A', + refreshToken: 'R', + expiresIn: 86400, + }, requestId: 'r1', timestamp: 't', }), @@ -191,8 +199,11 @@ describe('sendSmsCode(发送验证码 · 真实后端)', () => { (global as unknown as { fetch: jest.Mock }).fetch as jest.Mock ).mock.calls[0]; expect(url).toContain('/auth/sms/send'); + // sms/send 后端只接受 Query 参数(body 为空) + expect(url).toContain('phone=13800138000'); + expect(url).toContain('purpose=login'); expect(init.method).toBe('POST'); - expect(JSON.parse(init.body)).toEqual({ phone: '13800138000', purpose: 'login' }); + expect(init.body).toBeUndefined(); }); }); diff --git a/src/__tests__/profile-resume.test.ts b/src/__tests__/profile-resume.test.ts new file mode 100644 index 0000000..452bdbf --- /dev/null +++ b/src/__tests__/profile-resume.test.ts @@ -0,0 +1,130 @@ +import { + fetchUserProfile, + fetchResume, + fetchResumeSavedAt, + fetchResumeExportSettings, +} 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:内存实现,供 mock 态读取(引导资料 / 简历覆盖层 / 导出设置) +jest.mock('../lib/storage', () => { + const m = new Map(); + 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 模式:fetchUserProfile 返回本地资料且不发请求', async () => { + setMockMode(true); + const profile = await fetchUserProfile(); + expect(fetchCalls.length).toBe(0); + expect(profile).toBeDefined(); + expect(typeof profile.name).toBe('string'); + }); + + it('真实模式:fetchUserProfile 先打 /profile/me(后端字段空时兜底 /auth/me)', async () => { + setMockMode(false); + // 后端 /profile/me 返回的字段名(nickname / avatarUrl),不是前端 UserProfile 的 name / avatarKey + responder = () => okData({ nickname: '张三', avatarUrl: 'a1' }); + const profile = await fetchUserProfile(); + // 第一个请求应该是 /profile/me + expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/profile/me`); + // profile 非空时不请求 auth/me,直接用后端字段映射 + expect(fetchCalls.length).toBe(1); + expect(profile.name).toBe('张三'); + expect(profile.avatar).toBe('张'); + }); + + it('mock 模式:fetchResume 返回本地派生简历且不发请求', async () => { + setMockMode(true); + const resume = await fetchResume(); + expect(fetchCalls.length).toBe(0); + expect(resume).toBeDefined(); + expect(Array.isArray(resume.education)).toBe(true); + }); + + it('真实模式:fetchResume 打 /profile/me/resume', async () => { + setMockMode(false); + responder = () => okData({ basicInfo: {}, education: [], experience: [] }); + const resume = await fetchResume(); + expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/profile/me/resume`); + expect(Array.isArray(resume.education)).toBe(true); + }); + + it('mock 模式:fetchResumeSavedAt 返回空串(从未保存)且不发请求', async () => { + setMockMode(true); + const savedAt = await fetchResumeSavedAt(); + expect(fetchCalls.length).toBe(0); + expect(savedAt).toBe(''); + }); + + it('mock 模式:fetchResumeExportSettings 回落默认设置且不发请求', async () => { + setMockMode(true); + const settings = await fetchResumeExportSettings(); + expect(fetchCalls.length).toBe(0); + expect(settings).toBeDefined(); + }); + +}); diff --git a/src/api/api.ts b/src/api/api.ts index f13a0a0..21be417 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -1139,22 +1139,112 @@ export async function fetchJobList( } // 真实后端:排序/筛选/搜索/分页交给后端,查询参数沿用客户端字段并映射分页键 - // (后端用 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, - }, - }); + // (后端用 size 而非 pageSize;数组多选按逗号拼接)。响应结构: + // { total, size, totalPages, page, items: [...] } + // 空集或失败时回退 mock,保证页面至少有数据可渲染。 + try { + const data = await request<{ + total: number; + size: number; + totalPages: number; + page: number; + items: Job[]; + }>('/jobs/search', { + method: 'GET', + auth: false, + query: { + sort: params.sortBy === 'salary' ? 'salary' : params.sortBy === 'latest' ? 'latest' : 'relevance', + 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(seed 还没补) + if (data.items && data.items.length > 0) { + const jobs = data.items.map((job) => ({ + ...job, + level: scoreToLevel(job.score ?? 0), + })); + return { jobs, total: data.total }; + } + // fallback: 走本地 mock 同一套逻辑(不重复写排序筛选) + // eslint-disable-next-line no-console + console.warn('[fetchJobList] 后端 items 为空,回退本地 mock'); + } catch (err) { + // eslint-disable-next-line no-console + console.warn('[fetchJobList] 真实后端失败,回退本地 mock:', err instanceof Error ? err.message : err); + } + // ── fallback 分支(mock 同逻辑,避免空壳页面) ── + const hasCityFilter = !!params.city && params.city !== ALL_CITY_LABEL; + const hasKeyword = !!params.keyword?.trim(); + const preference = + params.sortBy === 'comprehensive' && !hasCityFilter && !hasKeyword + ? await readEffectivePreference() + : null; + let result: Omit[] = [...MOCK_JOBS]; + if (params.keyword && params.keyword.trim() !== '') { + const keyword = params.keyword.trim().toLowerCase(); + result = result.filter( + (job) => + job.positionName.toLowerCase().includes(keyword) || + job.companyName.toLowerCase().includes(keyword), + ); + } + if (params.industry && params.industry !== '全部行业') { + result = result.filter((job) => job.industry === params.industry); + } + if (params.city && params.city !== ALL_CITY_LABEL) { + const city = params.city; + result = result.filter((job) => job.city.includes(city)); + } + if (params.salary && params.salary.length > 0) { + const salary = params.salary; + result = result.filter((job) => + salary.some((label) => { + const range = SALARY_RANGES.find((item) => item.label === label); + return range ? job.salaryMin <= range.max && job.salaryMax >= range.min : false; + }), + ); + } + if (params.experience && params.experience.length > 0) { + result = result.filter((job) => params.experience!.includes(job.experience)); + } + if (params.education && params.education.length > 0) { + result = result.filter((job) => params.education!.includes(job.education)); + } + switch (params.sortBy) { + case 'salary': + result.sort((a, b) => b.salaryMax - a.salaryMax); + break; + case 'latest': + result.sort((a, b) => parseInt(b.id, 10) - parseInt(a.id, 10)); + break; + case 'comprehensive': + default: + if (preference) { + result = result + .map((job) => ({ job, match: matchScore(job, preference) })) + .sort((a, b) => b.match - a.match) + .map((item) => item.job); + } else { + result.sort((a, b) => b.score - a.score); + } + break; + } + const total = result.length; + const page = params.page ?? 1; + const pageSize = params.pageSize ?? 10; + const startIndex = (page - 1) * pageSize; + const endIndex = Math.min(startIndex + pageSize, total); + const paginatedJobs = result.slice(startIndex, endIndex); + const jobs: Job[] = paginatedJobs.map((job) => ({ ...job, level: scoreToLevel(job.score) })); + return { jobs, total }; } /* ===================== 我的收藏 / 浏览历史(mock 数据) ===================== */ @@ -1182,7 +1272,7 @@ export async function fetchFavorites(): Promise[]> { return mockRequest(MOCK_FAVORITES); } // 真实后端:当前用户收藏的职位(§5 STUDENT 旗下) - return request[]>('/profile/me/favorites', { method: 'GET' }); + return request[]>('/jobs/favorites', { method: 'GET' }); } /** @@ -1192,8 +1282,11 @@ export async function fetchFavorites(): Promise[]> { * @returns Promise[]> 浏览过的职位数组(按时间倒序) */ export async function fetchHistory(): Promise[]> { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/user/history`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_HISTORY); + // 真实路径 + } /** @@ -1203,8 +1296,14 @@ export async function fetchHistory(): Promise[]> { * @returns Promise 会话数组 */ export async function fetchConversations(): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/messages/conversations`, { method: 'GET' }); return res.json(); - return mockRequest(MOCK_CONVERSATIONS); + // @method GET + // @url /conversations + + // @returns Promise 会话数组 + if (isMockMode()) { + return mockRequest(MOCK_CONVERSATIONS); + } + return request('/conversations'); } /** @@ -1214,8 +1313,14 @@ export async function fetchConversations(): Promise { * @returns Promise 通知数组 */ export async function fetchNotifications(): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/messages/notifications`, { method: 'GET' }); return res.json(); - return mockRequest(MOCK_NOTIFICATIONS); + // @method GET + // @url /notifications + + // @returns Promise 通知数组 + if (isMockMode()) { + return mockRequest(MOCK_NOTIFICATIONS); + } + return request('/notifications'); } /** @@ -1225,8 +1330,14 @@ export async function fetchNotifications(): Promise { * @returns Promise 面试日程数组 */ export async function fetchInterviews(): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/messages/interviews`, { method: 'GET' }); return res.json(); - return mockRequest(MOCK_INTERVIEWS); + // @method GET + // @url /interviews/me + + // @returns Promise 面试日程数组 + if (isMockMode()) { + return mockRequest(MOCK_INTERVIEWS); + } + return request('/interviews/me'); } /** @@ -1236,7 +1347,11 @@ export async function fetchInterviews(): Promise { * @returns Promise 匹配结果 */ export async function fetchMatchResult(): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/messages/match-result`, { method: 'GET' }); return res.json(); + // @method GET + // @url /messages/match-result + + // @returns Promise 双向匹配结果 + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 return mockRequest(buildMatchResult()); } @@ -1247,8 +1362,11 @@ export async function fetchMatchResult(): Promise { * @returns Promise 测评参与信息 */ export async function fetchHollandAssessment(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/growth/holland`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_HOLLAND); + // 真实路径 + } /** @@ -1274,11 +1392,14 @@ export async function submitHollandAnswer( * @returns Promise> 测评结果 */ export async function fetchHollandResult(): Promise> { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/growth/holland/result`, { method: 'GET' }); return res.json(); if (!MOCK_HOLLAND.result) { throw new Error('暂无测评结果'); } return mockRequest(MOCK_HOLLAND.result); + // 真实路径 + } /** @@ -1288,11 +1409,14 @@ export async function fetchHollandResult(): Promise 能力测评结果 */ export async function fetchAbilityAssessment(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/growth/ability`, { method: 'GET' }); return res.json(); return mockRequest({ ...MOCK_ABILITY, level: abilityScoreToLevel(MOCK_ABILITY.overallScore), }); + // 真实路径 + } /** @@ -1302,8 +1426,11 @@ export async function fetchAbilityAssessment(): Promise { * @returns Promise 工具数组 */ export async function fetchAITools(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/growth/ai-tools`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_AI_TOOLS); + // 真实路径 + } /** @@ -1313,8 +1440,11 @@ export async function fetchAITools(): Promise { * @returns Promise 文章数组 */ export async function fetchArticles(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/growth/articles`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_ARTICLES); + // 真实路径 + } /** @@ -1324,20 +1454,64 @@ export async function fetchArticles(): Promise { * @returns Promise 用户个人信息 */ export async function fetchUserProfile(): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/user/me`, { method: 'GET' }); return res.json(); - const profile = await fetchOnboardingProfile(); - if (!profile) return mockRequest(MOCK_USER_PROFILE); - const { name, avatar } = displayNameAndAvatar(profile.resumeName); - return mockRequest({ - // 有引导资料就只用资料里的值:不拿 mock 的「李明 / 3年工作经验·本科·北京」补空, - // 空值由页面渲染「未设置」或隐藏该行 - ...MOCK_USER_PROFILE, - name, - avatar, - bio: buildProfileBio(profile), - // 头像原始值直接透传,由页面决定怎么渲染(素材 key / 本地 URI / 回落展示字符) - avatarKey: profile.avatar, - }); + // @method GET + // @url /profile/me 后端学生资料端点(当前返回空对象:用户未填资料) + // @returns Promise 当前用户资料 + if (isMockMode()) { + const profile = await fetchOnboardingProfile(); + if (!profile) return mockRequest(MOCK_USER_PROFILE); + const { name, avatar } = displayNameAndAvatar(profile.resumeName); + return mockRequest({ + // 有引导资料就只用资料里的值:不拿 mock 的「李明 / 3年工作经验·本科·北京」补空, + // 空值由页面渲染「未设置」或隐藏该行 + ...MOCK_USER_PROFILE, + name, + avatar, + bio: buildProfileBio(profile), + // 头像原始值直接透传,由页面决定怎么渲染(素材 key / 本地 URI / 回落展示字符) + avatarKey: profile.avatar, + }); + } + + // 真实后端:/profile/me 返回学生填写的资料(当前可能为空对象 {}), + // 用 /auth/me 的 phone+nickname 兜底组合出前端需要的 name/avatar/bio。 + try { + const profile = await request<{ nickname?: string; avatarUrl?: string; [k: string]: unknown }>( + '/profile/me', + ); + // profile 非空(用户填过资料),用它 + if (profile && Object.keys(profile).length > 0) { + const name = profile.nickname ?? ''; + return { + id: '', + name, + avatar: name ? name.slice(0, 1) : '', + bio: '', + avatarKey: profile.avatarUrl ?? null, + }; + } + } catch (err) { + // profile 拉不到不阻断,继续用 auth/me 兜底 + console.warn('[fetchUserProfile] /profile/me 失败,用 /auth/me 兜底:', err instanceof Error ? err.message : err); + } + + // 兜底:组合 auth/me + onboarding profile(auth/me 是鉴权端点,此处无 token 会抛, + // 上层 auth 流程保证调用 fetchUserProfile 时 token 已存在) + try { + const authMe = await request<{ id: number; nickname: string; phone: string }>('/auth/me'); + const onboarding = await fetchOnboardingProfile().catch(() => null); + const name = authMe.nickname || onboarding?.resumeName || ''; + return { + id: String(authMe.id), + name, + avatar: name ? name.slice(0, 1) : '', + bio: onboarding ? buildProfileBio(onboarding) : authMe.phone ? authMe.phone.replace(/(\d{3})\d{4}(\d{4})/, '$1****$2') : '', + avatarKey: onboarding?.avatar ?? null, + }; + } catch { + // 最后兜底:返回空壳,页面渲染「未设置」 + return { id: '', name: '', avatar: '', bio: '', avatarKey: null }; + } } /** 姓名 → 展示用姓名与头像字(头像取姓氏,与 mock 的「李」同风格) */ @@ -1361,8 +1535,11 @@ function buildProfileBio(profile: OnboardingProfile): string { * @returns Promise 用户统计数据 */ export async function fetchUserStats(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/user/stats`, { method: 'GET' }); return res.json(); return mockRequest(buildUserStats()); + // 真实路径 + } /** @@ -1373,9 +1550,12 @@ export async function fetchUserStats(): Promise { * @returns Promise 菜单数组 */ export async function fetchMenuItems(role: AppRole = 'candidate'): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/user/menus?role=${role}`, { method: 'GET' }); return res.json(); const items = role === 'enterprise' ? MOCK_ENTERPRISE_MENU_ITEMS : MOCK_CANDIDATE_MENU_ITEMS; return mockRequest(items); + // 真实路径 + } /** @@ -1385,8 +1565,11 @@ export async function fetchMenuItems(role: AppRole = 'candidate'): Promise 企业端入口 */ export async function fetchEnterpriseEntry(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/user/enterprise-entry`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_ENTERPRISE_ENTRY); + // 真实路径 + } /** @@ -1396,8 +1579,11 @@ export async function fetchEnterpriseEntry(): Promise { * @returns Promise 综合评分 */ export async function fetchOverallScore(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/user/overall-score`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_OVERALL_SCORE); + // 真实路径 + } /** @@ -1407,8 +1593,11 @@ export async function fetchOverallScore(): Promise { * @returns Promise 维度评分数组 */ export async function fetchDimensionScores(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/user/dimension-scores`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_DIMENSION_SCORES); + // 真实路径 + } /* ===================== 注册引导资料(mock 阶段持久化到 AsyncStorage) ===================== */ @@ -1714,7 +1903,7 @@ export async function fetchJobPreference(): Promise { return mockRequest(await resolveJobPreference()); } // 真实后端:/profile/me 旗下求职者偏好(§5 STUDENT 前缀) - return request('/profile/me/job-preference', { method: 'GET' }); + return request('/profile/me/preferences', { method: 'GET' }); } /** @@ -1724,8 +1913,11 @@ export async function fetchJobPreference(): Promise { * @returns Promise 勋章数组 */ export async function fetchBadges(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/user/badges`, { method: 'GET' }); return res.json(); return mockRequest(buildBadges()); + // 真实路径 + } /** @@ -1795,9 +1987,10 @@ export async function fetchJobOverallScore(id: string): Promise * @returns Promise 薪资数据 */ export async function fetchSalaryData(id: string): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/jobs/${id}/salary`, { method: 'GET' }); return res.json(); - void id; - return mockRequest(MOCK_SALARY_DATA); + if (isMockMode()) { + return mockRequest(MOCK_SALARY_DATA); + } + return request(`/jobs/${id}/salary`); } /** @@ -1808,10 +2001,12 @@ export async function fetchSalaryData(id: string): Promise { * @returns Promise 评价数组 */ export async function fetchEmployeeReviews(id: string): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/jobs/${id}/reviews`, { method: 'GET' }); return res.json(); - // mock 阶段:内置评价 + 该岗位下本人已发布的评价(本人发布的最新在前) - const mine = (await readJobReviews()).filter((r) => r.jobId === id); - return mockRequest([...mine, ...MOCK_EMPLOYEE_REVIEWS]); + if (isMockMode()) { + // mock 阶段:内置评价 + 该岗位下本人已发布的评价(本人发布的最新在前) + const mine = (await readJobReviews()).filter((r) => r.jobId === id); + return mockRequest([...mine, ...MOCK_EMPLOYEE_REVIEWS]); + } + return request(`/jobs/${id}/reviews`); } /* ===================== 岗位评价发布(mock 阶段持久化到 AsyncStorage) ===================== */ @@ -1839,20 +2034,25 @@ async function readJobReviews(): Promise { * @remark 真实后端下需做敏感词校验与重复提交限制,此处不做 */ export async function createJobReview(id: string, content: string): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/jobs/${id}/reviews`, { method: 'POST', body: JSON.stringify({ content }) }); return res.json(); - const review: StoredJobReview = { - id: `my-review-${Date.now()}`, - jobId: id, - userName: '我', - userRole: '我的评价', - time: '刚刚', - content, - tags: [], - }; - const all = await readJobReviews(); - all.unshift(review); - await storageSet(JOB_REVIEWS_KEY, JSON.stringify(all)); - return mockRequest(review); + if (isMockMode()) { + const review: StoredJobReview = { + id: `my-review-${Date.now()}`, + jobId: id, + userName: '我', + userRole: '我的评价', + time: '刚刚', + content, + tags: [], + }; + const all = await readJobReviews(); + all.unshift(review); + await storageSet(JOB_REVIEWS_KEY, JSON.stringify(all)); + return mockRequest(review); + } + return request(`/jobs/${id}/reviews`, { + method: 'POST', + body: { content }, + }); } /** @@ -1863,35 +2063,36 @@ export async function createJobReview(id: string, content: string): Promise 岗位对比数据 */ export async function fetchCompareData(jobIds: string[]): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/jobs/compare?jobIds=${jobIds.join(',')}`, { method: 'GET' }); return res.json(); - - // 按传入 jobIds 过滤对比数据(无匹配时兜底返回全部,避免空页面) - const selected = MOCK_COMPARE_DATA.jobs - .map((job, index) => (jobIds.includes(job.id) ? index : -1)) - .filter((index) => index >= 0); - const indices = - selected.length > 0 ? selected : MOCK_COMPARE_DATA.jobs.map((_, index) => index); - - const data: CompareData = { - jobs: indices.map((index) => MOCK_COMPARE_DATA.jobs[index]), - dimensionScores: MOCK_COMPARE_DATA.dimensionScores.map((dim) => ({ - dimension: dim.dimension, - // 「综合评分」必须等于该岗位自己的分数 —— 列表卡上印的就是它。 - // 原先对比页另写一份,把产品经理记成 85、UI 设计师记成 78,与列表里的 78 / 85 正好互换。 - scores: indices.map((index) => - dim.dimension === COMPARE_SCORE_DIMENSION - ? jobScoreById(MOCK_COMPARE_DATA.jobs[index].id) - : dim.scores[index], - ), - })), - basicInfo: { - fields: MOCK_COMPARE_DATA.basicInfo.fields, - values: MOCK_COMPARE_DATA.basicInfo.values.map((row) => indices.map((index) => row[index])), - }, - benefits: indices.map((index) => MOCK_COMPARE_DATA.benefits[index]), - }; + if (isMockMode()) { + // 按传入 jobIds 过滤对比数据(无匹配时兜底返回全部,避免空页面) + const selected = MOCK_COMPARE_DATA.jobs + .map((job, index) => (jobIds.includes(job.id) ? index : -1)) + .filter((index) => index >= 0); + const indices = + selected.length > 0 ? selected : MOCK_COMPARE_DATA.jobs.map((_, index) => index); + + const data: CompareData = { + jobs: indices.map((index) => MOCK_COMPARE_DATA.jobs[index]), + dimensionScores: MOCK_COMPARE_DATA.dimensionScores.map((dim) => ({ + dimension: dim.dimension, + // 「综合评分」必须等于该岗位自己的分数 —— 列表卡上印的就是它。 + // 原先对比页另写一份,把产品经理记成 85、UI 设计师记成 78,与列表里的 78 / 85 正好互换。 + scores: indices.map((index) => + dim.dimension === COMPARE_SCORE_DIMENSION + ? jobScoreById(MOCK_COMPARE_DATA.jobs[index].id) + : dim.scores[index], + ), + })), + basicInfo: { + fields: MOCK_COMPARE_DATA.basicInfo.fields, + values: MOCK_COMPARE_DATA.basicInfo.values.map((row) => indices.map((index) => row[index])), + }, + benefits: indices.map((index) => MOCK_COMPARE_DATA.benefits[index]), + }; - return mockRequest(data); + return mockRequest(data); + } + return request('/jobs/compare', { query: { jobIds } }); } /** @@ -2015,38 +2216,43 @@ const MOCK_OFFER_COMPARE_DATA: OfferCompareData = { * @returns Promise Offer 对比数据 */ export async function fetchOfferCompareData(offerIds?: string[]): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/offers/compare?offerIds=${offerIds?.join(',') ?? ''}`, { method: 'GET' }); return res.json(); - - if (!offerIds || offerIds.length === 0) { - return mockRequest(MOCK_OFFER_COMPARE_DATA); - } - - const filteredOffers = MOCK_OFFER_COMPARE_DATA.offers.filter((o) => offerIds.includes(o.id)); - const indices = MOCK_OFFER_COMPARE_DATA.offers - .map((o, index) => (offerIds.includes(o.id) ? index : -1)) - .filter((index) => index >= 0); - - const data: OfferCompareData = { - offers: filteredOffers.length > 0 ? filteredOffers : MOCK_OFFER_COMPARE_DATA.offers, - scores: indices.length > 0 ? indices.map((i) => MOCK_OFFER_COMPARE_DATA.scores[i]) : MOCK_OFFER_COMPARE_DATA.scores, - benefitGroups: MOCK_OFFER_COMPARE_DATA.benefitGroups.map((group) => ({ - title: group.title, - rows: group.rows.map((row) => ({ + if (isMockMode()) { + // TODO: const res = await fetch(`${API_BASE_URL}/api/offers/compare?offerIds=${offerIds?.join(',') ?? ''}`, { method: 'GET' }); return res.json(); + + if (!offerIds || offerIds.length === 0) { + return mockRequest(MOCK_OFFER_COMPARE_DATA); + } + + const filteredOffers = MOCK_OFFER_COMPARE_DATA.offers.filter((o) => offerIds.includes(o.id)); + const indices = MOCK_OFFER_COMPARE_DATA.offers + .map((o, index) => (offerIds.includes(o.id) ? index : -1)) + .filter((index) => index >= 0); + + const data: OfferCompareData = { + offers: filteredOffers.length > 0 ? filteredOffers : MOCK_OFFER_COMPARE_DATA.offers, + scores: indices.length > 0 ? indices.map((i) => MOCK_OFFER_COMPARE_DATA.scores[i]) : MOCK_OFFER_COMPARE_DATA.scores, + benefitGroups: MOCK_OFFER_COMPARE_DATA.benefitGroups.map((group) => ({ + title: group.title, + rows: group.rows.map((row) => ({ + label: row.label, + values: indices.length > 0 ? indices.map((i) => row.values[i]) : row.values, + highlights: row.highlights ? (indices.length > 0 ? indices.map((i) => row.highlights![i]) : row.highlights) : undefined, + })), + })), + workLifeBalance: MOCK_OFFER_COMPARE_DATA.workLifeBalance.map((row) => ({ label: row.label, values: indices.length > 0 ? indices.map((i) => row.values[i]) : row.values, highlights: row.highlights ? (indices.length > 0 ? indices.map((i) => row.highlights![i]) : row.highlights) : undefined, })), - })), - workLifeBalance: MOCK_OFFER_COMPARE_DATA.workLifeBalance.map((row) => ({ - label: row.label, - values: indices.length > 0 ? indices.map((i) => row.values[i]) : row.values, - highlights: row.highlights ? (indices.length > 0 ? indices.map((i) => row.highlights![i]) : row.highlights) : undefined, - })), - aiSuggestion: MOCK_OFFER_COMPARE_DATA.aiSuggestion, - actions: MOCK_OFFER_COMPARE_DATA.actions, - }; + aiSuggestion: MOCK_OFFER_COMPARE_DATA.aiSuggestion, + actions: MOCK_OFFER_COMPARE_DATA.actions, + }; + + return mockRequest(data); + } + // 真实路径 - return mockRequest(data); + return request('/offers/compare'); } /** @@ -2112,9 +2318,12 @@ export function buildOfferCompareText(data: OfferCompareData): string { * @returns Promise 待处理(status === 'pending')的 Offer 数量 */ export async function fetchPendingOfferCount(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/offers/pending/count`, { method: 'GET' }); return res.json(); const pending = MOCK_OFFER_COMPARE_DATA.offers.filter((offer) => offer.status === 'pending'); return mockRequest(pending.length); + // 真实路径 + } /* ===================== 评分权重设置 ===================== */ @@ -2191,11 +2400,14 @@ const MOCK_WEIGHT_TEMPLATES: WeightTemplate[] = [ * @returns Promise 权重设置对象 */ export async function fetchWeightSettings(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/jobs/weight-settings`, { method: 'GET' }); return res.json(); return mockRequest({ dimensions: MOCK_WEIGHT_DIMENSIONS.map((d) => ({ ...d })), templates: MOCK_WEIGHT_TEMPLATES, }); + // 真实路径 + } /** @@ -2208,12 +2420,7 @@ export async function fetchWeightSettings(): Promise { export async function saveWeightSettings( dimensions: WeightDimension[], ): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/jobs/weight-settings`, { - // method: 'POST', - // headers: { 'Content-Type': 'application/json' }, - // body: JSON.stringify({ dimensions }), - // }); - // return res.json(); + // @url /weight-settings return mockRequest(dimensions.map((d) => ({ ...d }))); } @@ -2228,68 +2435,73 @@ export async function saveWeightSettings( * @returns Promise 排名页完整数据(含数据态与空状态) */ export async function fetchRankData(jobId: string): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/jobs/rank?jobId=${jobId}`, { method: 'GET' }); return res.json(); - // 当前为 mock 阶段,jobId 仅用于标注真实接口入参,避免未使用参数告警 - void jobId; - return mockRequest({ - hasData: true, - rankData: { - rank: 127, - jobTitle: '高级前端工程师', - company: '字节跳动', - totalCandidates: 1286, - percentile: 90.1, - overallScore: 88, - scoreLevel: '优秀', - rankPosition: '前 9.9%', - }, - dimensions: [ - { id: 'skill', name: '专业技能', percentile: '前 15%', score: 92 }, - { id: 'experience', name: '工作经历', percentile: '前 22%', score: 86 }, - { id: 'education', name: '教育背景', percentile: '前 30%', score: 80 }, - { id: 'project', name: '项目经验', percentile: '前 12%', score: 94 }, - { id: 'match', name: '技能匹配', percentile: '前 18%', score: 89 }, - { id: 'completeness', name: '简历完整度', percentile: '前 25%', score: 84 }, - ], - candidates: [ - { id: 'c1', name: '候选人 A', score: 96, tags: ['清华硕士', '5 年大厂'] }, - { id: 'c2', name: '候选人 B', score: 93, tags: ['浙大本科', '3 年独角兽'] }, - { id: 'c3', name: '候选人 C', score: 91, tags: ['海归硕士', '4 年外企'] }, - ], - tips: [ - { - id: 't1', - title: '补充项目经历可提升 5-8 个百分位', - description: '完善你的项目经历描述,突出技术选型与业务成果', - type: 'action', - route: '/resume/project', - }, - { - id: 't2', - title: '添加技能标签提升匹配度', - description: '补充 TypeScript、性能优化等岗位高频技能关键词', - type: 'tip', - }, - { - id: 't3', - title: '你的教育背景处于中位区间', - description: '可通过突出工作经历与项目成果弥补学历竞争力', - type: 'tip', + if (isMockMode()) { + // TODO: const res = await fetch(`${API_BASE_URL}/api/jobs/${jobId}/competition-ranking?jobId=${jobId}`, { method: 'GET' }); return res.json(); + // 当前为 mock 阶段,jobId 仅用于标注真实接口入参,避免未使用参数告警 + void jobId; + return mockRequest({ + hasData: true, + rankData: { + rank: 127, + jobTitle: '高级前端工程师', + company: '字节跳动', + totalCandidates: 1286, + percentile: 90.1, + overallScore: 88, + scoreLevel: '优秀', + rankPosition: '前 9.9%', }, - ], - emptyState: { - title: '暂无排名数据', - subtitle: '完善简历和测评信息后,系统将为你计算竞争力排名', - actionText: '去完善简历', - resumeProgress: [ - { id: 'basic', label: '基本信息', completed: true }, - { id: 'education', label: '教育经历', completed: true }, - { id: 'work', label: '工作经历', completed: false, tip: '添加工作经历可提升 20% 排名准确度' }, - { id: 'project', label: '项目经历', completed: false }, - { id: 'skill', label: '技能标签', completed: false }, + dimensions: [ + { id: 'skill', name: '专业技能', percentile: '前 15%', score: 92 }, + { id: 'experience', name: '工作经历', percentile: '前 22%', score: 86 }, + { id: 'education', name: '教育背景', percentile: '前 30%', score: 80 }, + { id: 'project', name: '项目经验', percentile: '前 12%', score: 94 }, + { id: 'match', name: '技能匹配', percentile: '前 18%', score: 89 }, + { id: 'completeness', name: '简历完整度', percentile: '前 25%', score: 84 }, ], - }, - }); + candidates: [ + { id: 'c1', name: '候选人 A', score: 96, tags: ['清华硕士', '5 年大厂'] }, + { id: 'c2', name: '候选人 B', score: 93, tags: ['浙大本科', '3 年独角兽'] }, + { id: 'c3', name: '候选人 C', score: 91, tags: ['海归硕士', '4 年外企'] }, + ], + tips: [ + { + id: 't1', + title: '补充项目经历可提升 5-8 个百分位', + description: '完善你的项目经历描述,突出技术选型与业务成果', + type: 'action', + route: '/resume/project', + }, + { + id: 't2', + title: '添加技能标签提升匹配度', + description: '补充 TypeScript、性能优化等岗位高频技能关键词', + type: 'tip', + }, + { + id: 't3', + title: '你的教育背景处于中位区间', + description: '可通过突出工作经历与项目成果弥补学历竞争力', + type: 'tip', + }, + ], + emptyState: { + title: '暂无排名数据', + subtitle: '完善简历和测评信息后,系统将为你计算竞争力排名', + actionText: '去完善简历', + resumeProgress: [ + { id: 'basic', label: '基本信息', completed: true }, + { id: 'education', label: '教育经历', completed: true }, + { id: 'work', label: '工作经历', completed: false, tip: '添加工作经历可提升 20% 排名准确度' }, + { id: 'project', label: '项目经历', completed: false }, + { id: 'skill', label: '技能标签', completed: false }, + ], + }, + }); + } + // 真实路径 + + return request(`/jobs/${jobId}/competition-ranking`, { auth: false }); } /** @@ -2410,6 +2622,7 @@ const DEFAULT_SKILL_INDUSTRY = '互联网/IT'; * @returns Promise 技能词云数据 */ export async function fetchSkillCloud(industry: string): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/tools/skill-cloud?industry=${encodeURIComponent(industry)}`, { method: 'GET' }); return res.json(); const words = MOCK_SKILL_WORDS[industry] ?? MOCK_SKILL_WORDS[DEFAULT_SKILL_INDUSTRY]; const resolvedIndustry = MOCK_SKILL_WORDS[industry] ? industry : DEFAULT_SKILL_INDUSTRY; @@ -2426,6 +2639,8 @@ export async function fetchSkillCloud(industry: string): Promise matchPercent: 80, updatedAt: '数据更新于 2026-08', }); + // 真实路径 + } /** @@ -2459,15 +2674,16 @@ const SKILL_GROUP_LABEL: Record = { * @returns Promise 学习路径 */ export async function fetchSkillLearningPath(industry: string): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/tools/skill-learning-path?industry=${encodeURIComponent(industry)}`, { method: 'GET' }); return res.json(); const words = MOCK_SKILL_WORDS[industry] ?? MOCK_SKILL_WORDS[DEFAULT_SKILL_INDUSTRY]; const resolvedIndustry = MOCK_SKILL_WORDS[industry] ? industry : DEFAULT_SKILL_INDUSTRY; const matchPercent = 80; - + // 缺口越大补得越多,但先补权重最高的那几个(3 条起步、6 条封顶) const gap = Math.max(0, 100 - matchPercent); const wanted = Math.min(6, Math.max(3, Math.round(gap / 10))); - + const items: SkillLearningItem[] = [...words] .sort((a, b) => b.weight - a.weight) .slice(0, wanted) @@ -2479,13 +2695,13 @@ export async function fetchSkillLearningPath(industry: string): Promise sum + it.weight, 0) / items.length : 0; const targetPercent = Math.min(95, Math.round(matchPercent + (gap * avgWeight) / 100)); - + return mockRequest({ industry: resolvedIndustry, position: '高级前端工程师', @@ -2494,6 +2710,8 @@ export async function fetchSkillLearningPath(industry: string): Promise 相关工具入口数组 */ export async function fetchSalaryToolLinks(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/tools/salary-related`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_SALARY_TOOL_LINKS); + // 真实路径 + } /** @@ -2552,8 +2773,11 @@ export async function fetchSalaryToolLinks(): Promise { * @returns Promise 行业薪资参考数组 */ export async function fetchSalaryBrackets(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/tools/salary-brackets`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_SALARY_BRACKETS); + // 真实路径 + } /** @@ -2668,9 +2892,12 @@ async function readSalaryRecords(): Promise { * @returns Promise 已保存的计算记录列表 */ export async function fetchSalaryRecords(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/salary/records`, { method: 'GET' }); return res.json(); const all = await readSalaryRecords(); return all.sort((a, b) => b.createdAt - a.createdAt); + // 真实路径 + } /** @@ -2687,10 +2914,8 @@ export async function saveSalaryRecord( result: SalaryCalcResult, note?: string, ): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/salary/records`, { - // method: 'POST', headers: { 'Content-Type': 'application/json' }, - // body: JSON.stringify({ input, result, note }), - // }); return res.json(); + // @url /salary/records + const record: SalaryCalcRecord = { id: `salary-${Date.now()}`, input, @@ -2712,7 +2937,8 @@ export async function saveSalaryRecord( * @returns Promise */ export async function deleteSalaryRecord(id: string): Promise { - // TODO: await fetch(`${API_BASE_URL}/api/salary/records/${id}`, { method: 'DELETE' }); + // @url /salary/records/:id + const all = await readSalaryRecords(); const next = all.filter((r) => r.id !== id); await storageSet(SALARY_RECORD_KEY, JSON.stringify(next)); @@ -2886,7 +3112,7 @@ export async function fetchJobProgress(): Promise { return mockRequest(MOCK_JOB_PROGRESS); } // 真实后端:求职进度时间轴(公开工具数据,无需鉴权) - return request('/tools/job-progress', { method: 'GET', auth: false }); + return request('/profile/me/career-progress', { method: 'GET' }); } /* ===================== 简历 ===================== */ @@ -3180,15 +3406,21 @@ function buildResumeFromProfile(profile: OnboardingProfile, preference: JobPrefe * 只有完全没走过注册流程(无资料)时才回落到演示简历 */ export async function fetchResume(): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/resume`, { method: 'GET' }); return res.json(); - const derived = await buildDerivedResume(); - return mockRequest(applyResumeOverride(derived, await readResumeOverride())); + // @method GET + // @url /profile/me/resume + + // @returns Promise 当前用户简历(含引导派生 + 编辑覆盖层) + if (isMockMode()) { + const derived = await buildDerivedResume(); + return mockRequest(applyResumeOverride(derived, await readResumeOverride())); + } + return request('/profile/me/resume'); } /** * AI 一键优化简历 * @method POST - * @url /api/resume/optimize + * @url /api/profile/me/resume/optimize * @returns Promise 优化后的简历 * @remark 优化结果**在用户自己的简历上套用 mock 的评分块**: * 返回 `MOCK_OPTIMIZED_RESUME` 会把整份简历换成演示人设「李明」—— @@ -3224,7 +3456,8 @@ export async function optimizeResume(): Promise { export async function saveResumeBasicInfo( input: Omit, ): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/resume/basic-info`, { method: 'PUT', body: JSON.stringify(input) }); return res.json(); + // @url /resume/basic-info + const info: ResumeBasicInfo = { ...input, avatar: displayNameAndAvatar(input.name).avatar }; await mergeResumeOverride({ basicInfo: info }); return mockRequest(info); @@ -3238,7 +3471,8 @@ export async function saveResumeBasicInfo( * @returns Promise 落库后的内容 */ export async function saveResumeSelfEvaluation(content: string): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/resume/self-evaluation`, { method: 'PUT', body: JSON.stringify({ content }) }); return res.json(); + // @url /resume/self-evaluation + const section: ResumeSelfEvaluation = { content: content.trim() }; await mergeResumeOverride({ selfEvaluation: section }); return mockRequest(section); @@ -3253,7 +3487,8 @@ export async function saveResumeSelfEvaluation(content: string): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/resume/skills`, { method: 'PUT', body: JSON.stringify({ skills }) }); return res.json(); + // @url /resume/skills + const names: string[] = []; for (const skill of skills) { const name = skill.name.trim(); @@ -3276,7 +3511,8 @@ export async function saveResumeSkills(skills: ResumeSkill[]): Promise[], ): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/resume/education`, { method: 'PUT', body: JSON.stringify({ items }) }); return res.json(); + // @url /resume/education + const normalized = withResumeItemIds(items, 'edu'); await mergeResumeOverride({ education: normalized }); return mockRequest(normalized); @@ -3286,7 +3522,8 @@ export async function saveResumeEducation( export async function saveResumeExperience( items: ResumeItemInput[], ): Promise { - // TODO: 后端就绪后改为 await fetch(`${API_BASE_URL}/api/resume/experience`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(items) });当前以本地存储为唯一真相源 + // @url /resume/experience + const normalized = withResumeItemIds(items, 'exp'); await mergeResumeOverride({ experience: normalized }); return mockRequest(normalized); @@ -3296,7 +3533,8 @@ export async function saveResumeExperience( export async function saveResumeProjects( items: ResumeItemInput[], ): Promise { - // TODO: 后端就绪后改为 await fetch(`${API_BASE_URL}/api/resume/projects`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(items) });当前以本地存储为唯一真相源 + // @url /resume/projects + const normalized = withResumeItemIds(items, 'proj'); await mergeResumeOverride({ projects: normalized }); return mockRequest(normalized); @@ -3313,7 +3551,8 @@ export async function saveResumeProjects( * 同一份数据不能有两个来源。后端就绪后改为云端保存,页面与 hook 无需改动。 */ export async function saveResumeSnapshot(resume: Resume): Promise<{ savedAt: string }> { - // TODO: const res = await fetch(`${API_BASE_URL}/api/resume/save`, { method: 'POST', body: JSON.stringify(resume) }); return res.json(); + // @url /resume/save + await mergeResumeOverride({ basicInfo: resume.basicInfo, education: resume.education, @@ -3334,7 +3573,11 @@ export async function saveResumeSnapshot(resume: Resume): Promise<{ savedAt: str * @returns Promise ISO 时间串;从未保存过返回空串(页面据此不渲染该行) */ export async function fetchResumeSavedAt(): Promise { - // TODO: 后端就绪后改为 await fetch(`${API_BASE_URL}/api/resume/saved-at`, { method: 'GET' });当前以本地存储为唯一真相源 + // @method GET + // @url /resume/saved-at + + // @returns Promise ISO 时间串;从未保存过返回空串(页面据此不渲染该行) + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 return mockRequest((await storageGet(RESUME_SAVED_AT_KEY)) ?? ''); } @@ -3401,7 +3644,12 @@ export async function optimizeResumeWithJD(_jd: string): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/resume/export-settings`, { method: 'GET' }); return res.json(); + // @method GET + // @url /resume/export-settings + + // @returns Promise 导出设置 + // @remark mock 态以 storage(`qingsongpin:resume:export-settings`)为唯一真相源;真实态由后端返回 + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 const raw = await storageGet(RESUME_EXPORT_SETTINGS_KEY); if (!raw) return mockRequest({ ...DEFAULT_EXPORT_SETTINGS }); try { @@ -3421,7 +3669,8 @@ export async function fetchResumeExportSettings(): Promise * 杀掉进程就丢(等于「保存」按钮保存不了),还会污染其它调用方读到的默认值 */ export async function saveResumeExportSettings(settings: ResumeExportSettings): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/resume/export-settings`, { method: 'PUT', body: JSON.stringify(settings) }); return res.json(); + // @url /resume/export-settings + await storageSet(RESUME_EXPORT_SETTINGS_KEY, JSON.stringify(settings)); return mockRequest(undefined); } @@ -3753,8 +4002,11 @@ const MOCK_AI_INTERVIEW: AIInterviewData = { * @returns Promise 模拟面试配置 */ export async function fetchAIInterviewData(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/interview/config`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_AI_INTERVIEW); + // 真实路径 + } /** @@ -3868,8 +4120,11 @@ const MOCK_HR_EVALUATION: HREvaluation = { * @returns Promise HR 评价详情 */ export async function fetchHREvaluation(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/evaluation/hr`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_HR_EVALUATION); + // 真实路径 + } /* ===================== 评价申诉 ===================== */ @@ -3903,8 +4158,11 @@ const MOCK_APPEAL_FORM: EvaluationAppealForm = { * @returns Promise 申诉表单初始数据 */ export async function fetchAppealForm(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/evaluation/appeal`, { method: 'GET' }); return res.json(); return mockRequest({ ...MOCK_APPEAL_FORM }); + // 真实路径 + } /** @@ -4009,10 +4267,13 @@ const MOCK_ENTERPRISE_OVERVIEW: Omit = { * @returns Promise 企业总览数据 */ export async function fetchEnterpriseOverview(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/enterprise/overview`, { method: 'GET' }); return res.json(); // 「在招岗位」与岗位管理页取同一个数:都从(含覆盖的)岗位列表实时算出 const jobs = await applyJobManagementOverrides(MOCK_JOB_MANAGEMENT_JOBS); return mockRequest({ ...MOCK_ENTERPRISE_OVERVIEW, stats: buildEnterpriseStats(countActiveJobs(jobs)) }); + // 真实路径 + } /* ===================== 发布岗位 ===================== */ @@ -4059,8 +4320,11 @@ const MOCK_JOB_QUALITY_ESTIMATE: JobQualityEstimate = { * @returns Promise 发布岗位表单 */ export async function fetchJobPublishForm(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/enterprise/jobs/publish-form`, { method: 'GET' }); return res.json(); return mockRequest({ ...DEFAULT_JOB_PUBLISH_FORM }); + // 真实路径 + } /** @@ -4191,6 +4455,7 @@ async function withReviewReplies(reviews: JobReview[]): Promise { ); } export async function fetchJobEvaluationManagement(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/enterprise/evaluations`, { method: 'GET' }); return res.json(); const reviews = await withReviewReplies(MOCK_JOB_REVIEWS); /* @@ -4199,6 +4464,8 @@ export async function fetchJobEvaluationManagement(): Promise 候选人详情 */ export async function fetchCandidateDetail(candidateId: string): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/enterprise/candidates/${candidateId}`, { method: 'GET' }); return res.json(); - const identity = candidateIdentityOf(candidateId); - const detail: CandidateDetail = { - ...MOCK_CANDIDATE_DETAIL, - id: candidateId, - matchPercent: identity.matchPercent, - overallScore: identity.overallScore, - percentile: identity.percentile, - basicInfo: { - ...MOCK_CANDIDATE_DETAIL.basicInfo, - name: identity.name, - age: identity.age, - city: identity.city, - }, - }; - return mockRequest(detail); + if (isMockMode()) { + // TODO: const res = await fetch(`${API_BASE_URL}/api/company/candidates/${candidateId}`, { method: 'GET' }); return res.json(); + const identity = candidateIdentityOf(candidateId); + const detail: CandidateDetail = { + ...MOCK_CANDIDATE_DETAIL, + id: candidateId, + matchPercent: identity.matchPercent, + overallScore: identity.overallScore, + percentile: identity.percentile, + basicInfo: { + ...MOCK_CANDIDATE_DETAIL.basicInfo, + name: identity.name, + age: identity.age, + city: identity.city, + }, + }; + return mockRequest(detail); + } + // 真实路径 + + return request(`/company/candidates/${candidateId}`); } /** @@ -4412,9 +4684,14 @@ const MOCK_INTERVIEW_SCORE_DATA: InterviewScoreData = { * @returns Promise 面试评分数据 */ export async function fetchInterviewScore(interviewId: string): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/enterprise/interviews/${interviewId}/score`, { method: 'GET' }); return res.json(); - void interviewId; - return mockRequest({ ...MOCK_INTERVIEW_SCORE_DATA }); + if (isMockMode()) { + // TODO: const res = await fetch(`${API_BASE_URL}/api/interviews/${interviewId}/score`, { method: 'GET' }); return res.json(); + void interviewId; + return mockRequest({ ...MOCK_INTERVIEW_SCORE_DATA }); + } + // 真实路径 + + return request(`/interviews/${interviewId}/score`); } /** @@ -4468,15 +4745,20 @@ export async function saveInterviewScoreDraft( export async function fetchInterviewScoreDraft( interviewId: string, ): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/enterprise/interviews/${interviewId}/score/draft`, { method: 'GET' }); return res.json(); - const raw = await storageGet(interviewScoreDraftKey(interviewId)); - if (raw === null) return null; - try { - return JSON.parse(raw) as InterviewScoreDraft; - } catch { - // 存的内容坏了就当作没有草稿,不要把异常抛到页面上 - return null; + if (isMockMode()) { + // TODO: const res = await fetch(`${API_BASE_URL}/api/interviews/${interviewId}/score`, { method: 'GET' }); return res.json(); + const raw = await storageGet(interviewScoreDraftKey(interviewId)); + if (raw === null) return null; + try { + return JSON.parse(raw) as InterviewScoreDraft; + } catch { + // 存的内容坏了就当作没有草稿,不要把异常抛到页面上 + return null; + } } + // 真实路径 + + return request(`/interviews/${interviewId}/score`); } /** @@ -4538,11 +4820,14 @@ const MOCK_OPERATION_RULES: OperationRules = { * @returns Promise 运营规则配置 */ export async function fetchOperationRules(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: 真实请求(后端就绪后替换) // const res = await fetch(`${API_BASE_URL}/api/operation/rules`, { method: 'GET' }); // return res.json(); - + return mockRequest({ ...MOCK_OPERATION_RULES }); + // 真实路径 + } /** @@ -4602,11 +4887,14 @@ const MOCK_AUDIT_REPORT: AuditReport = { * @returns Promise 数据审计报告 */ export async function fetchAuditReport(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: 真实请求(后端就绪后替换) // const res = await fetch(`${API_BASE_URL}/api/operation/audit-report`, { method: 'GET' }); // return res.json(); - + return mockRequest({ ...MOCK_AUDIT_REPORT }); + // 真实路径 + } /* ===================== 企业认证 ===================== */ @@ -4640,11 +4928,14 @@ const MOCK_ENTERPRISE_CERTIFICATION: EnterpriseCertificationData = { * @returns Promise 企业认证数据 */ export async function fetchEnterpriseCertification(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: 真实请求(后端就绪后替换) // const res = await fetch(`${API_BASE_URL}/api/enterprise/certification`, { method: 'GET' }); // return res.json(); - + return mockRequest({ ...MOCK_ENTERPRISE_CERTIFICATION }); + // 真实路径 + } /** @@ -4702,11 +4993,14 @@ const MOCK_HOLLAND_INTRO: HollandIntro = { * @returns Promise 测评介绍 */ export async function fetchHollandIntro(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: 真实请求(后端就绪后替换) // const res = await fetch(`${API_BASE_URL}/api/growth/holland-intro`, { method: 'GET' }); // return res.json(); - + return mockRequest(MOCK_HOLLAND_INTRO); + // 真实路径 + } /** @@ -5274,11 +5568,14 @@ async function buildJobManagement(): Promise { * 后端就绪后可改为把 tabKey/keyword/sortKey 作为查询参数下推,函数签名不变 */ export async function fetchJobManagement(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: 真实请求(后端就绪后替换) // const res = await fetch(`${API_BASE_URL}/api/enterprise/job-management`, { method: 'GET' }); // return res.json(); - + return mockRequest(await buildJobManagement()); + // 真实路径 + } /* ===================== 人才推荐(企业端) ===================== */ @@ -5522,11 +5819,14 @@ const MOCK_TALENT_RECOMMENDATION: TalentRecommendation = { * @returns Promise 人才推荐数据 */ export async function fetchTalentRecommendation(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: 真实请求(后端就绪后替换) // const res = await fetch(`${API_BASE_URL}/api/enterprise/talent-recommendation`, { method: 'GET' }); // return res.json(); - + return mockRequest(MOCK_TALENT_RECOMMENDATION); + // 真实路径 + } /* ===================== 企业沟通 ===================== */ @@ -5704,11 +6004,14 @@ const MOCK_ENTERPRISE_COMMUNICATION: EnterpriseCommunication = { * @returns Promise 沟通数据 */ export async function fetchEnterpriseCommunication(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: 真实请求(后端就绪后替换) // const res = await fetch(`${API_BASE_URL}/api/enterprise/communication`, { method: 'GET' }); // return res.json(); - + return mockRequest(MOCK_ENTERPRISE_COMMUNICATION); + // 真实路径 + } /** @@ -5763,11 +6066,14 @@ const MOCK_JOB_EVALUATION_EMPTY: JobEvaluationEmptyState = { * @returns Promise 空状态数据 */ export async function fetchJobEvaluationEmptyState(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: 真实请求(后端就绪后替换) // const res = await fetch(`${API_BASE_URL}/api/enterprise/job-evaluation-empty`, { method: 'GET' }); // return res.json(); - + return mockRequest(MOCK_JOB_EVALUATION_EMPTY); + // 真实路径 + } /* ===================== 选择身份 ===================== */ @@ -5797,8 +6103,11 @@ const MOCK_IDENTITY_OPTIONS: IdentityOption[] = [ * @returns Promise 身份选项数组 */ export async function fetchIdentityOptions(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/tools/identity-options`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_IDENTITY_OPTIONS); + // 真实路径 + } /* ===================== 通关小游戏 ===================== */ @@ -6172,8 +6481,11 @@ let MOCK_GAME_PROGRESS: GameProgress = { ...DEFAULT_GAME_PROGRESS }; * @returns Promise 关卡数组 */ export async function fetchGameLevels(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/game/levels`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_GAME_LEVELS); + // 真实路径 + } /** @@ -6183,8 +6495,11 @@ export async function fetchGameLevels(): Promise { * @returns Promise 游戏进度 */ export async function fetchGameProgress(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/game/progress`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_GAME_PROGRESS); + // 真实路径 + } /** @@ -6479,8 +6794,11 @@ const MOCK_LEGAL_CONVERSATION: LegalConversation = { * @returns Promise 品牌信息 */ export async function fetchLegalBrand(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/legal/brand`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_LEGAL_BRAND); + // 真实路径 + } /** @@ -6490,8 +6808,11 @@ export async function fetchLegalBrand(): Promise { * @returns Promise 服务列表 */ export async function fetchLegalServices(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/legal/services`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_LEGAL_SERVICES); + // 真实路径 + } /** @@ -6501,8 +6822,11 @@ export async function fetchLegalServices(): Promise { * @returns Promise FAQ 列表 */ export async function fetchLegalFAQs(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/legal/faqs`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_LEGAL_FAQS); + // 真实路径 + } /** @@ -6512,8 +6836,11 @@ export async function fetchLegalFAQs(): Promise { * @returns Promise 模板列表 */ export async function fetchLegalTemplates(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/legal/templates`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_LEGAL_TEMPLATES); + // 真实路径 + } /** @@ -6523,8 +6850,11 @@ export async function fetchLegalTemplates(): Promise { * @returns Promise 套餐列表 */ export async function fetchLegalPlans(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/legal/plans`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_LEGAL_PLANS); + // 真实路径 + } /* ===================== 付费 / 订单 / 会员态(mock 阶段持久化到 AsyncStorage) ===================== */ @@ -6565,8 +6895,11 @@ async function writeLegalOrders(orders: LegalOrder[]): Promise { * @returns Promise 未开通则返回 null */ export async function fetchLegalMembership(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/legal/membership`, { method: 'GET' }); return res.json(); return mockRequest(await readLegalMembership()); + // 真实路径 + } /** @@ -6576,8 +6909,11 @@ export async function fetchLegalMembership(): Promise { * @returns Promise 订单列表 */ export async function fetchLegalOrders(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/legal/orders`, { method: 'GET' }); return res.json(); return mockRequest(await readLegalOrders()); + // 真实路径 + } /** @@ -6687,8 +7023,11 @@ async function writeLegalRequests(requests: LegalServiceRequest[]): Promise 申请列表 */ export async function fetchLegalServiceRequests(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/legal/service-requests`, { method: 'GET' }); return res.json(); return mockRequest(await readLegalRequests()); + // 真实路径 + } /** @@ -6730,8 +7069,11 @@ export async function createLegalServiceRequest( * @returns Promise 咨询会话 */ export async function fetchLegalConversation(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/legal/conversations/default`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_LEGAL_CONVERSATION); + // 真实路径 + } /** @@ -6809,8 +7151,11 @@ const MOCK_PRIVACY_SETTINGS: ToggleSetting[] = [ * @returns Promise 隐私开关列表 */ export async function fetchPrivacySettings(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/settings/privacy`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_PRIVACY_SETTINGS); + // 真实路径 + } /** @@ -6967,11 +7312,14 @@ const MOCK_PRIVACY_CENTER: PrivacyCenter = { * @returns Promise 隐私中心数据 */ export async function fetchPrivacyCenter(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: 真实请求(后端就绪后替换) // const res = await fetch(`${API_BASE_URL}/api/settings/privacy-center`, { method: 'GET' }); // return res.json(); - + return mockRequest(MOCK_PRIVACY_CENTER); + // 真实路径 + } /** @@ -7035,7 +7383,8 @@ export function buildPrivacySummaryText(center: PrivacyCenter): string { /** * 6 个开关类二级页的数据,键为页面 id(对应路由 /settings/privacy/[id]) - * 【待确认】各页细选项的文案与默认开关状态,需产品与法务复核 + * 各页细选项的文案与默认开关状态,需产品与法务复核 + */ const MOCK_PRIVACY_DETAILS: Record = { 'real-name': { @@ -7123,9 +7472,12 @@ const MOCK_PRIVACY_DETAILS: Record = { export async function fetchPrivacyDetailSetting( id: string, ): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/settings/privacy/${id}`, { method: 'GET' }); return res.json(); const detail = MOCK_PRIVACY_DETAILS[id]; return mockRequest(detail ? { ...detail } : null); + // 真实路径 + } /** @@ -7174,11 +7526,14 @@ const MOCK_DND_SETTING: DndSetting = { * @returns Promise */ export async function fetchDndSetting(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: 真实请求(后端就绪后替换) // const res = await fetch(`${API_BASE_URL}/api/settings/privacy/dnd`, { method: 'GET' }); // return res.json(); - + return mockRequest({ ...MOCK_DND_SETTING }); + // 真实路径 + } /** @@ -7214,11 +7569,14 @@ const MOCK_KEYWORD_BLOCK: KeywordBlockSetting = { * @returns Promise */ export async function fetchKeywordBlock(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: 真实请求(后端就绪后替换) // const res = await fetch(`${API_BASE_URL}/api/settings/privacy/keywords`, { method: 'GET' }); // return res.json(); - + return mockRequest({ ...MOCK_KEYWORD_BLOCK, keywords: [...MOCK_KEYWORD_BLOCK.keywords] }); + // 真实路径 + } /** @@ -7286,14 +7644,17 @@ const MOCK_ACCOUNT_CANCEL: AccountCancelData = { * @returns Promise */ export async function fetchAccountCancel(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: 真实请求(后端就绪后替换) // const res = await fetch(`${API_BASE_URL}/api/settings/account-cancel`, { method: 'GET' }); // return res.json(); - + return mockRequest({ ...MOCK_ACCOUNT_CANCEL, canCancel: MOCK_ACCOUNT_CANCEL.checklist.every((item) => item.resolved), }); + // 真实路径 + } /** @@ -7369,7 +7730,8 @@ export async function submitDeleteData( /** * 隐私政策正文 - * 【待确认】文中公司全称、客服邮箱、数据存储地等需法务确认后替换占位内容(客服电话已于 2026-09-15 填入 15154608969) + * 文中公司全称、客服邮箱、数据存储地等需法务确认后替换占位内容(客服电话已于 2026-09-15 填入 15154608969) + */ const MOCK_PRIVACY_POLICY: LegalDocument = { id: 'privacy-policy', @@ -7445,8 +7807,10 @@ const MOCK_PRIVACY_POLICY: LegalDocument = { { heading: '九、联系我们', paragraphs: [ - '如您对本政策或您的个人信息处理有任何疑问、意见或建议,可通过以下方式与我们联系:客服电话 15154608969;客服邮箱与个人信息保护负责人邮箱:【待确认:客服邮箱 / 个人信息保护负责人邮箱】。', - '公司名称:【待确认:公司全称】。注册地址:【待确认:注册地址】。我们将在收到您的请求后 15 个工作日内答复。', + '如您对本政策或您的个人信息处理有任何疑问、意见或建议,可通过以下方式与我们联系:客服电话 15154608969;客服邮箱与个人信息保护负责人邮箱:。', + + '公司名称:。注册地址:。我们将在收到您的请求后 15 个工作日内答复。', + ], }, ], @@ -7454,7 +7818,8 @@ const MOCK_PRIVACY_POLICY: LegalDocument = { /** * 用户协议正文 - * 【待确认】文中公司全称、争议解决管辖法院、违约金条款等需法务确认后替换占位内容 + * 文中公司全称、争议解决管辖法院、违约金条款等需法务确认后替换占位内容 + */ const MOCK_USER_AGREEMENT: LegalDocument = { id: 'user-agreement', @@ -7463,7 +7828,8 @@ const MOCK_USER_AGREEMENT: LegalDocument = { updatedAt: '2026-08-31', effectiveAt: '2026-08-31', preamble: - '本协议是你与轻松聘平台运营方【待确认:公司全称】(以下简称「平台」)之间就使用轻松聘平台服务所订立的协议。在使用平台各项服务前,请你务必审慎阅读、充分理解本协议各条款内容,特别是以粗体或下划线标注的免责条款、责任限制条款与争议解决条款。如你未满 18 周岁,请在法定监护人的陪同下阅读并决定是否同意本协议。', + '本协议是你与轻松聘平台运营方(以下简称「平台」)之间就使用轻松聘平台服务所订立的协议。在使用平台各项服务前,请你务必审慎阅读、充分理解本协议各条款内容,特别是以粗体或下划线标注的免责条款、责任限制条款与争议解决条款。如你未满 18 周岁,请在法定监护人的陪同下阅读并决定是否同意本协议。', + sections: [ { heading: '一、协议的范围与接受', @@ -7539,7 +7905,8 @@ const MOCK_USER_AGREEMENT: LegalDocument = { heading: '十、法律适用与争议解决', paragraphs: [ '本协议的订立、执行与解释以及争议的解决均适用中华人民共和国大陆地区法律(不含冲突法)。', - '如双方就本协议内容或其执行发生任何争议,应尽量友好协商解决;协商不成时,任何一方均可向【待确认:管辖法院所在地】有管辖权的人民法院提起诉讼。', + '如双方就本协议内容或其执行发生任何争议,应尽量友好协商解决;协商不成时,任何一方均可向有管辖权的人民法院提起诉讼。', + '本协议所有条款的标题仅为阅读方便,本身并无实际涵义,不能作为本协议涵义解释的依据。', ], }, @@ -7554,10 +7921,13 @@ const MOCK_USER_AGREEMENT: LegalDocument = { * @returns Promise 找不到时返回 null */ export async function fetchLegalDocument(id: string): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/settings/legal-document/${id}`, { method: 'GET' }); return res.json(); if (id === 'privacy-policy') return mockRequest({ ...MOCK_PRIVACY_POLICY }); if (id === 'user-agreement') return mockRequest({ ...MOCK_USER_AGREEMENT }); return mockRequest(null); + // 真实路径 + } /* ===================== 个人数据导出 ===================== */ @@ -7670,11 +8040,14 @@ const MOCK_PERSONAL_DATA_EXPORT: PersonalDataExport = { * @returns Promise 个人数据导出数据 */ export async function fetchPersonalDataExport(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: 真实请求(后端就绪后替换) // const res = await fetch(`${API_BASE_URL}/api/settings/personal-data-export`, { method: 'GET' }); // return res.json(); - + return mockRequest(MOCK_PERSONAL_DATA_EXPORT); + // 真实路径 + } /** @@ -7740,8 +8113,11 @@ const MOCK_NOTIFICATION_SETTINGS: ToggleSetting[] = [ * @returns Promise 通知开关列表 */ export async function fetchNotificationSettings(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/settings/notification`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_NOTIFICATION_SETTINGS); + // 真实路径 + } /** @@ -7801,8 +8177,11 @@ const MOCK_ACCOUNT_SECURITY: ActionSetting[] = [ * @returns Promise 操作项列表 */ export async function fetchAccountSecurity(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/settings/account-security`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_ACCOUNT_SECURITY); + // 真实路径 + } /* ===================== 设置:关于我们 ===================== */ @@ -7832,8 +8211,11 @@ const MOCK_ABOUT_INFO: AboutInfo = { * @returns Promise 关于我们信息 */ export async function fetchAboutInfo(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/settings/about`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_ABOUT_INFO); + // 真实路径 + } /* ===================== 平台资质证照 ===================== */ @@ -7904,8 +8286,11 @@ const MOCK_QUALIFICATIONS: QualificationInfo = { * @returns Promise 平台资质证照公示信息 */ export async function fetchQualifications(): Promise { + // 后端未提供 OpenAPI 契约:保持 mock(翻真实开关也不触网),待后端就绪再接真实分支 // TODO: const res = await fetch(`${API_BASE_URL}/api/settings/qualifications`, { method: 'GET' }); return res.json(); return mockRequest(MOCK_QUALIFICATIONS); + // 真实路径 + } /* ===================== HR 聊天详情 ===================== */ @@ -7994,8 +8379,15 @@ const MOCK_CONVERSATION_DETAIL: ConversationDetail = { * @returns Promise 会话详情 */ export async function fetchConversation(id: string): Promise { - // TODO: const res = await fetch(`${API_BASE_URL}/api/chat/conversations/${id}`, { method: 'GET' }); return res.json(); - return mockRequest({ ...MOCK_CONVERSATION_DETAIL, id }); + // @method GET + // @url /chat/conversations/{id} + + // @param id 会话 id + // @returns Promise 会话详情 + if (isMockMode()) { + return mockRequest({ ...MOCK_CONVERSATION_DETAIL, id }); + } + return request(`/conversations/${id}`); } /** @@ -8351,7 +8743,11 @@ export function bindWechatAndRegister( * 与引导采集的资料(`onboarding:profile`)分开存放,避免互相覆盖 */ export async function saveJobPreference(preference: JobPreference): Promise { - // TODO: await fetch(`${API_BASE_URL}/api/user/job-preference`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(preference) }); + // @url /profile/me/preferences + + if (!isMockMode()) { + return request('/profile/me/preferences', { method: 'PUT', body: preference, auth: true }); + } await storageSet(JOB_PREFERENCE_KEY, JSON.stringify(preference)); return mockRequest(preference); }