Browse Source

fix(api): 投递时间线字段映射 + 接入投递匹配状态

- 后端 timeline 节点字段是 toStatus/reason/createdAt,前端 ApplicationTimelineEvent
  用 status/note/time,此前直接透传导致企业端投递详情时间线渲染空白
- fetchApplicationTimeline 增加映射:toStatus→status(经 token 转换)、reason→note、createdAt→time
- 新增 fetchApplicationMatch 接入 GET /applications/{id}/match,含 ApplicationMatch 类型
- 补 2 条回归测试(timeline 映射 + match 透传)
master
doublex001 4 weeks ago
parent
commit
b22ed57151
  1. 40
      src/__tests__/enterprise-applications.test.ts
  2. 32
      src/api/api.ts
  3. 1
      src/api/index.ts
  4. 31
      src/types/application.ts
  5. 2
      src/types/index.ts

40
src/__tests__/enterprise-applications.test.ts

@ -42,6 +42,8 @@ import {
createCandidateFeedback,
deleteCandidateFeedback,
fetchCandidateFeedback,
fetchApplicationMatch,
fetchApplicationTimeline,
fetchCompanyApplicationDetail,
fetchCompanyApplications,
toBackendApplicationStatus,
@ -227,4 +229,42 @@ describe('企业端投递 / 反馈(真实模式)', () => {
await updateCandidateFeedback('app-1001', { rating: 4, content: '改', tags: [] });
expect(seenMethod).toBe('PUT');
});
it('时间线把后端 toStatus/reason/createdAt 映射成 status/note/time', async () => {
setMockMode(false);
responder = (url) => {
expect(url).toBe(`${API_BASE_URL}/applications/app-1001/timeline`);
return okData([
{ id: 1, operatorUserId: 4, toStatus: 'SUBMITTED', reason: 'Student applied', createdAt: '2026-09-30T01:57:26' },
]);
};
const timeline = await fetchApplicationTimeline('app-1001');
expect(timeline).toEqual([
{ status: 'submitted', note: 'Student applied', time: '2026-09-30T01:57:26' },
]);
});
it('匹配状态打 /applications/{id}/match 并透传结构', async () => {
setMockMode(false);
responder = (url) => {
expect(url).toBe(`${API_BASE_URL}/applications/1/match`);
return okData({
status: 'PENDING_BOTH',
matchVersion: 'RULE_THRESHOLD_V1',
candidateScoreSource: 'MANUAL_INTEREST',
hrScoreSource: 'HR_MANUAL',
companyRequiredCandidateScore: 60,
studentRequiredHrScore: 60,
applicationId: 1,
jobId: 166,
companyId: 48,
candidateId: 4,
});
};
const match = await fetchApplicationMatch('1');
expect(match.status).toBe('PENDING_BOTH');
expect(match.companyRequiredCandidateScore).toBe(60);
expect(match.studentRequiredHrScore).toBe(60);
expect(match.jobId).toBe(166);
});
});

32
src/api/api.ts

@ -191,8 +191,10 @@ import type {
CarrierPhoneResult,
ApplicationDetail,
ApplicationItem,
ApplicationMatch,
ApplicationStatus,
ApplicationTimelineEvent,
BackendApplicationTimelineEvent,
CandidateFeedback,
CandidateFeedbackInput,
CompanyApplicationItem,
@ -2188,11 +2190,31 @@ export async function fetchApplicationTimeline(id: string): Promise<ApplicationT
if (isMockMode()) {
return [];
}
const data = await request<{ timeline: ApplicationTimelineEvent[] } | ApplicationTimelineEvent[]>(
`/applications/${id}/timeline`,
{ method: 'GET', auth: true },
);
return Array.isArray(data) ? data : data.timeline ?? [];
// 后端 timeline 节点字段是 toStatus / reason / createdAt,与前端 ApplicationTimelineEvent
// 的 status / note / time 不同构,必须在此映射,否则时间线渲染成一片空白。
const raw = await request<BackendApplicationTimelineEvent[]>(`/applications/${id}/timeline`, {
method: 'GET',
auth: true,
});
return (raw ?? []).map((e) => ({
status: toApplicationStatus(e.toStatus),
note: e.reason,
time: e.createdAt,
}));
}
/**
* 获取投递匹配状态(学生本人或岗位所属企业可见)
* @method GET
* @url /applications/{id}/match
* @param id 投递记录 ID
* @returns Promise<ApplicationMatch> 匹配状态
*/
export async function fetchApplicationMatch(id: string): Promise<ApplicationMatch> {
if (isMockMode()) {
throw new Error('mock 模式下无匹配数据');
}
return request<ApplicationMatch>(`/applications/${id}/match`, { method: 'GET', auth: true });
}
/**

1
src/api/index.ts

@ -210,6 +210,7 @@ export {
fetchMyApplications,
fetchApplicationDetail,
fetchApplicationTimeline,
fetchApplicationMatch,
withdrawApplication,
toBackendApplicationStatus,
fetchCompanyApplications,

31
src/types/application.ts

@ -64,6 +64,37 @@ export interface ApplicationTimelineEvent {
note?: string;
}
/** 后端投递时间线节点(GET /applications/{id}/timeline 实测结构) */
export interface BackendApplicationTimelineEvent {
id: number;
operatorUserId: number;
/** 变更后状态(大写 token,如 SUBMITTED) */
toStatus: string;
/** 变更原因,如 "Student applied" */
reason?: string;
createdAt: string;
}
/** 投递匹配状态(GET /applications/{id}/match,学生/企业双方可见) */
export interface ApplicationMatch {
/** 匹配状态,如 PENDING_BOTH */
status: string;
/** 匹配规则版本,如 RULE_THRESHOLD_V1 */
matchVersion: string;
/** 候选人意向分来源,如 MANUAL_INTEREST */
candidateScoreSource: string;
/** HR 适合度分来源,如 HR_MANUAL */
hrScoreSource: string;
/** 企业要求的最低候选人意向分(0-100) */
companyRequiredCandidateScore: number;
/** 学生要求的最低 HR 适配分(0-100) */
studentRequiredHrScore: number;
applicationId: number;
jobId: number;
companyId: number;
candidateId: number;
}
/** 分页结果(后端统一分页结构) */
export interface PageResult<T> {
items: T[];

2
src/types/index.ts

@ -321,8 +321,10 @@ export type { QualificationInfo, QualificationLicense, QualificationLicenseType
export type {
ApplicationDetail,
ApplicationItem,
ApplicationMatch,
ApplicationStatus,
ApplicationTimelineEvent,
BackendApplicationTimelineEvent,
CandidateFeedback,
CandidateFeedbackInput,
CompanyApplicationItem,

Loading…
Cancel
Save