Browse Source

fix(api): 修正两处幽灵接口调用(评分路径 / 移除不存在的薪资格)

- fetchJobOverallScore 改打真实端点 /jobs/{id}/personalized-review-score,
  新增 mapPersonalizedScore 把后端 dimensionScores 对象映射为雷达图维度数组
- 移除 fetchSalaryData:后端无 /jobs/{id}/salary(salary-calculator 域实网 404),
  它在 useJobDetail 的 Promise.all 中,404 会让整个岗位详情页加载失败
- 岗位详情页删除无数据来源的「真实薪资区间」表(薪资仍由 detail.salaryMin/Max 展示)
- 清理 SalaryData/SalaryRange 类型、MOCK_SALARY_DATA 及相关测试
master
doublex001 4 weeks ago
parent
commit
1e75b7d470
  1. 51
      app/jobs/[id].tsx
  2. 40
      src/__tests__/job-api.test.ts
  3. 42
      src/__tests__/share-text.test.ts
  4. 67
      src/api/api.ts
  5. 3
      src/api/index.ts
  6. 27
      src/hooks/useJobDetail.ts
  7. 3
      src/types/index.ts
  8. 33
      src/types/job.ts

51
app/jobs/[id].tsx

@ -475,7 +475,6 @@ export default function JobDetailScreen() {
const {
detail,
overallScore,
salaryData,
reviews,
reviewSummary,
loading,
@ -507,7 +506,7 @@ export default function JobDetailScreen() {
const handleShare = async () => {
if (!detail) return;
const outcome = await shareText(
buildJobShareText(detail, overallScore, salaryData, reviews.length),
buildJobShareText(detail, overallScore, reviews.length),
`${detail.positionName}|${detail.companyName}`,
);
if (!outcome.ok) Alert.alert('无法分享', outcome.message);
@ -575,7 +574,7 @@ export default function JobDetailScreen() {
}
// 加载态
if (loading || !detail || !overallScore || !salaryData) {
if (loading || !detail || !overallScore) {
return (
<SafeAreaView style={styles.safeArea} edges={['top', 'bottom']}>
<Header onBack={() => router.back()} favorited={false} onToggleFavorite={() => {}} />
@ -652,23 +651,9 @@ export default function JobDetailScreen() {
</Pressable>
</View>
{/* 3. 真实薪资区间 */}
{/* 「全部」入口已摘除:下面这张卡已经把 4 档经验区间列全了,点进去没有更多内容 */}
<SectionTitle title="真实薪资区间" />
<View style={styles.card}>
{salaryData.ranges.map((range, index) => (
<View
key={range.experience}
style={[styles.salaryRow, index === salaryData.ranges.length - 1 && styles.salaryRowLast]}
>
<Text style={styles.salaryExp}>{range.experience}</Text>
<Text style={styles.salaryRange}>
{range.min}-{range.max}K
</Text>
</View>
))}
<Text style={styles.salarySource}>{salaryData.dataSource}</Text>
</View>
{/* 3. 真实薪资区间:已摘除 */}
{/* 后端无「按经验段拆分薪资」端点(`/jobs/{id}/salary` 不存在,salary-calculator 域实网 404),
该表无数据来源;岗位薪资已在顶部基础信息卡按 salaryMin-salaryMax 展示,不重复占位。 */}
{/* 4. 员工评价(汇总 → 列表) */}
<SectionTitle title="员工评价" />
@ -958,32 +943,6 @@ const styles = StyleSheet.create({
fontWeight: '600',
color: colors.primary,
},
/* ---- 薪资区间 ---- */
salaryRow: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 12,
borderBottomWidth: 1,
borderBottomColor: colors.borderLight,
},
salaryRowLast: {
borderBottomWidth: 0,
},
salaryExp: {
fontSize: 14,
color: colors.textSecondary,
},
salaryRange: {
fontSize: 15,
fontWeight: '700',
color: colors.textPrimary,
},
salarySource: {
marginTop: 12,
fontSize: 12,
color: colors.textHint,
},
/* ---- 员工评价 ---- */
reviewHeader: {
flexDirection: 'row',

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

@ -6,7 +6,6 @@ import {
saveJobPreference,
fetchFavorites,
fetchJobProgress,
fetchSalaryData,
fetchEmployeeReviews,
createJobReview,
updateJobReview,
@ -117,12 +116,30 @@ describe('岗位域 mock 开关', () => {
expect(detail.status).toBe('招聘中');
});
it('真实模式:fetchJobOverallScore 打 /jobs/{id}/score', async () => {
it('真实模式:fetchJobOverallScore 打 /jobs/{id}/personalized-review-score 并映射维度', async () => {
setMockMode(false);
responder = () => okData({ totalScore: 88 });
responder = () =>
okData({
jobId: 2,
score: 88,
status: '超过 88% 同行业岗位',
source: 'DEFAULT',
templateCode: 'T1',
configuredWeights: {},
effectiveWeights: {},
dimensionScores: { salary: 90, culture: 80 },
coveredWeight: 1,
missingDimensions: [],
});
const score = await fetchJobOverallScore('2');
expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/jobs/2/score`);
expect(fetchCalls[0].url).toBe(`${API_BASE_URL}/jobs/2/personalized-review-score`);
expect(score.totalScore).toBe(88);
expect(score.rank).toBe('超过 88% 同行业岗位');
expect(score.dimensions).toEqual([
{ id: 'salary', name: 'salary', value: 90 },
{ id: 'culture', name: 'culture', value: 80 },
]);
expect(score.level).toBeTruthy();
});
it('真实模式:fetchJobPreference 打 /profile/me/preferences 并把多选数组取首项', async () => {
@ -236,21 +253,6 @@ describe('岗位域 mock 开关', () => {
});
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');

42
src/__tests__/share-text.test.ts

@ -40,7 +40,6 @@ import {
fetchJobDetail,
fetchJobOverallScore,
fetchPrivacyCenter,
fetchSalaryData,
} from '../api/api';
import type {
AIInterviewData,
@ -59,18 +58,17 @@ const hasLineStartingWith = (text: string, prefix: string) =>
describe('buildJobShareText(岗位详情分享)', () => {
async function fixtures() {
const [detail, overallScore, salaryData] = await Promise.all([
const [detail, overallScore] = await Promise.all([
fetchJobDetail('1'),
fetchJobOverallScore('1'),
fetchSalaryData('1'),
]);
return { detail, overallScore, salaryData };
return { detail, overallScore };
}
it('首三行是「岗位|公司」「地点·经验·学历」「薪资区间」', async () => {
const { detail, overallScore, salaryData } = await fixtures();
const { detail, overallScore } = await fixtures();
const lines = textOf(buildJobShareText(detail, overallScore, salaryData, 3));
const lines = textOf(buildJobShareText(detail, overallScore, 3));
expect(lines[0]).toBe(`${detail.positionName}|${detail.companyName}`);
expect(lines[1]).toContain(detail.companyLocation);
@ -80,58 +78,50 @@ describe('buildJobShareText(岗位详情分享)', () => {
});
it('薪资月数缺失时不输出「0薪」这种话', async () => {
const { detail, overallScore, salaryData } = await fixtures();
const { detail, overallScore } = await fixtures();
const text = buildJobShareText({ ...detail, salaryMonths: 0 }, overallScore, salaryData, 3);
const text = buildJobShareText({ ...detail, salaryMonths: 0 }, overallScore, 3);
expect(text).not.toContain('0薪');
expect(hasLineStartingWith(text, '薪资 ')).toBe(true);
});
it('没有评分数据时整行不出现(而不是输出 null 或 0 分)', async () => {
const { detail, salaryData } = await fixtures();
const { detail } = await fixtures();
const text = buildJobShareText(detail, null, salaryData, 3);
const text = buildJobShareText(detail, null, 3);
expect(text).not.toContain('岗位综合评分');
expect(text).not.toContain('null');
expect(text).not.toContain('undefined');
});
it('真实薪资样本数为 0 时不输出「真实薪资」(避免展示无依据的数据)', async () => {
const { detail, overallScore, salaryData } = await fixtures();
const text = buildJobShareText(detail, overallScore, { ...salaryData, totalCount: 0 }, 3);
expect(text).not.toContain('真实薪资');
});
it('福利为空时不留下「福利:」这个空壳', async () => {
const { detail, overallScore, salaryData } = await fixtures();
const { detail, overallScore } = await fixtures();
const text = buildJobShareText({ ...detail, benefits: [] }, overallScore, salaryData, 3);
const text = buildJobShareText({ ...detail, benefits: [] }, overallScore, 3);
expect(text).not.toContain('福利:');
});
it('评价数为 0 时说「暂无员工评价」,不说「员工评价 0 条」', async () => {
const { detail, overallScore, salaryData } = await fixtures();
const { detail, overallScore } = await fixtures();
const text = buildJobShareText(detail, overallScore, salaryData, 0);
const text = buildJobShareText(detail, overallScore, 0);
expect(text).toContain('暂无员工评价');
expect(text).not.toContain('员工评价 0 条');
});
it('评价数大于 0 时给出条数', async () => {
const { detail, overallScore, salaryData } = await fixtures();
const { detail, overallScore } = await fixtures();
expect(buildJobShareText(detail, overallScore, salaryData, 3)).toContain('员工评价 3 条');
expect(buildJobShareText(detail, overallScore, 3)).toContain('员工评价 3 条');
});
it('以签名收尾', async () => {
const { detail, overallScore, salaryData } = await fixtures();
expect(buildJobShareText(detail, overallScore, salaryData, 3)).toContain(SIGNATURE);
const { detail, overallScore } = await fixtures();
expect(buildJobShareText(detail, overallScore, 3)).toContain(SIGNATURE);
});
});

67
src/api/api.ts

@ -47,6 +47,8 @@ import type {
JobLevel,
JobListParams,
JobOverallScore,
JobDimensionScore,
BackendPersonalizedScore,
JobPreference,
MatchResult,
MenuItem,
@ -76,7 +78,6 @@ import type {
ResumeProject,
ResumeSelfEvaluation,
ResumeSkill,
SalaryData,
SalaryBracket,
SalaryCalcInput,
SalaryCalcResult,
@ -950,18 +951,6 @@ const MOCK_JOB_OVERALL_SCORE: JobOverallScore = {
],
};
/** 真实薪资区间 */
const MOCK_SALARY_DATA: SalaryData = {
ranges: [
{ experience: '1-3年', min: 15, max: 25 },
{ experience: '3-5年', min: 25, max: 40 },
{ experience: '5-10年', min: 40, max: 65 },
{ experience: '10年以上', min: 65, max: 100 },
],
dataSource: '基于2,846份真实简历数据',
totalCount: 2846,
};
/**
* 员工评价列表(公开评价,非本人发布)。
*
@ -3142,9 +3131,30 @@ export async function fetchJobDetail(id: string): Promise<JobDetail> {
}
/**
* 获取岗位综合评分(含 8 维度雷达图数据)
* 后端个性化评分响应 → 前端 JobOverallScore。
* 后端 dimensionScores 是「维度名 → 分值」的对象,统一映射成 JobDimensionScore[] 供雷达图渲染。
*/
function mapPersonalizedScore(d: BackendPersonalizedScore): JobOverallScore {
const total = typeof d.score === 'number' ? d.score : 0;
const dimensions: JobDimensionScore[] = Object.entries(d.dimensionScores ?? {}).map(
([name, value]) => ({
id: name,
name,
value: typeof value === 'number' ? value : 0,
}),
);
return {
totalScore: total,
level: JOB_LEVEL_LABEL[scoreToLevel(total)],
rank: d.status || '',
dimensions,
};
}
/**
* 获取岗位综合评分(含维度评分,用于雷达图)
* @method GET
* @url /api/jobs/:id/score
* @url /jobs/{jobId}/personalized-review-score
* @param id { string } 岗位 id
* @returns Promise<JobOverallScore> 综合评分
*/
@ -3168,22 +3178,12 @@ export async function fetchJobOverallScore(id: string): Promise<JobOverallScore>
})),
});
}
// 真实后端:岗位综合评分(§7 公开读取;响应结构以 OpenAPI 为准)
return request<JobOverallScore>(`/jobs/${id}/score`, { method: 'GET', auth: false });
}
/**
* 获取岗位真实薪资区间
* @method GET
* @url /api/jobs/:id/salary
* @param id { string } 岗位 id
* @returns Promise<SalaryData> 薪资数据
*/
export async function fetchSalaryData(id: string): Promise<SalaryData> {
if (isMockMode()) {
return mockRequest(MOCK_SALARY_DATA);
}
return request<SalaryData>(`/jobs/${id}/salary`);
// 真实后端:岗位个性化评分(§7 公开读取,STUDENT 可用;响应结构见 personalized-review-score)
const d = await request<BackendPersonalizedScore>(`/jobs/${id}/personalized-review-score`, {
method: 'GET',
auth: false,
});
return mapPersonalizedScore(d);
}
/**
@ -3432,7 +3432,6 @@ export async function fetchCompareData(jobIds: string[]): Promise<CompareData> {
* 把岗位详情拼成可分享的纯文本(分享用,纯函数)。
* @param detail 岗位详情
* @param overallScore 岗位综合评分(未取到传 null)
* @param salaryData 真实薪资数据(未取到传 null)
* @param reviewCount 员工评价条数
* @returns string 形如「岗位名|公司」+ 薪资 / 评分 / 福利 / 评价数
* @remark 本项目没有岗位的 Web 落地页,因此只拼文本、不编造分享链接。
@ -3440,7 +3439,6 @@ export async function fetchCompareData(jobIds: string[]): Promise<CompareData> {
export function buildJobShareText(
detail: JobDetail,
overallScore: JobOverallScore | null,
salaryData: SalaryData | null,
reviewCount: number,
): string {
const lines: string[] = [
@ -3452,9 +3450,6 @@ export function buildJobShareText(
if (overallScore) {
lines.push(`岗位综合评分 ${overallScore.totalScore} 分(${overallScore.level})· ${overallScore.rank}`);
}
if (salaryData && salaryData.totalCount > 0) {
lines.push(`真实薪资:${salaryData.dataSource}`);
}
if (detail.benefits.length > 0) {
lines.push(`福利:${detail.benefits.join('、')}`);
}

3
src/api/index.ts

@ -2,6 +2,8 @@
* 接口统一导出
*/
export type { LoginResult } from './api';
export type { PasswordOperationResult } from '../types';
export type { AuthLoginData } from './auth';
export type { WechatLoginResult, WechatSession, WechatBindPhonePayload } from '../types';
export type {
ChangePasswordPayload,
@ -159,7 +161,6 @@ export {
fetchResumeSavedAt,
clearResumeOverride,
fetchSalaryBrackets,
fetchSalaryData,
fetchSalaryRecords,
fetchSalaryToolLinks,
saveSalaryRecord,

27
src/hooks/useJobDetail.ts

@ -1,6 +1,11 @@
/**
* 岗位详情数据 hook:并行组合岗位详情、综合评分、薪资区间、员工评价、评价汇总五类数据,
* 岗位详情数据 hook:并行组合岗位详情、综合评分、员工评价、评价汇总四类数据,
* 统一管理 loading / error / refresh 三态,并暴露评价的增 / 改 / 删动作。
*
* 注:早期这里还会并行拉取「分经验段真实薪资」(fetchSalaryData),命中不存在的
* `/jobs/{id}/salary`(后端 salary-calculator 域尚未实现,实网 404)。因它在
* Promise.all 中,404 会让整个详情页加载失败,且详情页薪资本就取自岗位对象
* 的 salaryMin/salaryMax,故移除该请求。
*/
import { useCallback } from 'react';
import {
@ -10,7 +15,6 @@ import {
fetchJobDetail,
fetchJobOverallScore,
fetchJobReviewSummary,
fetchSalaryData,
updateJobReview,
} from '../api';
import type {
@ -20,7 +24,6 @@ import type {
JobReviewCreateInput,
JobReviewSummary,
JobReviewUpdateInput,
SalaryData,
} from '../types';
import { useAsyncData } from './useAsyncData';
@ -28,22 +31,19 @@ import { useAsyncData } from './useAsyncData';
interface JobDetailData {
detail: JobDetail;
overallScore: JobOverallScore;
salaryData: SalaryData;
reviews: EmployeeReview[];
reviewSummary: JobReviewSummary;
}
export function useJobDetail(id: string) {
const fetcher = useCallback(async (): Promise<JobDetailData> => {
const [detail, overallScore, salaryData, reviews, reviewSummary] =
await Promise.all([
fetchJobDetail(id),
fetchJobOverallScore(id),
fetchSalaryData(id),
fetchEmployeeReviews(id),
fetchJobReviewSummary(id),
]);
return { detail, overallScore, salaryData, reviews, reviewSummary };
const [detail, overallScore, reviews, reviewSummary] = await Promise.all([
fetchJobDetail(id),
fetchJobOverallScore(id),
fetchEmployeeReviews(id),
fetchJobReviewSummary(id),
]);
return { detail, overallScore, reviews, reviewSummary };
}, [id]);
const { data, loading, error, refresh } = useAsyncData(fetcher);
@ -83,7 +83,6 @@ export function useJobDetail(id: string) {
return {
detail: data?.detail ?? null,
overallScore: data?.overallScore ?? null,
salaryData: data?.salaryData ?? null,
reviews: data?.reviews ?? [],
reviewSummary: data?.reviewSummary ?? null,
loading,

3
src/types/index.ts

@ -5,6 +5,7 @@ export type {
BackendJobCompany,
BackendJobItem,
BackendJobReviewSummary,
BackendPersonalizedScore,
EmployeeReview,
Job,
JobDetail,
@ -16,8 +17,6 @@ export type {
JobReviewStatus,
JobReviewSummary,
JobReviewUpdateInput,
SalaryData,
SalaryRange,
SortType,
} from './job';
export type {

33
src/types/job.ts

@ -92,24 +92,21 @@ export interface JobOverallScore {
dimensions: JobDimensionScore[];
}
/** 薪资区间(按经验分段) */
export interface SalaryRange {
/** 经验段,如 "1-3年" */
experience: string;
/** 最低薪资(K) */
min: number;
/** 最高薪资(K) */
max: number;
}
/** 薪资数据(真实薪资区间) */
export interface SalaryData {
/** 各经验段薪资区间 */
ranges: SalaryRange[];
/** 数据来源说明,如 "基于2,846份真实简历数据" */
dataSource: string;
/** 样本总数,如 2846 */
totalCount: number;
/** 后端个性化评分响应(GET /jobs/{jobId}/personalized-review-score) */
export interface BackendPersonalizedScore {
jobId: number;
/** 综合分(0-100) */
score: number;
/** 等级/排名文案,如 "超过91%同行业岗位" */
status: string;
source: string;
templateCode: string;
configuredWeights: Record<string, number>;
effectiveWeights: Record<string, number>;
/** 维度名 → 分值 */
dimensionScores: Record<string, number>;
coveredWeight: number;
missingDimensions: string[];
}
/** 员工评价 */

Loading…
Cancel
Save