diff --git a/app/jobs/[id].tsx b/app/jobs/[id].tsx index 45b74cf..5a61a1c 100644 --- a/app/jobs/[id].tsx +++ b/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 (
router.back()} favorited={false} onToggleFavorite={() => {}} /> @@ -652,23 +651,9 @@ export default function JobDetailScreen() { - {/* 3. 真实薪资区间 */} - {/* 「全部」入口已摘除:下面这张卡已经把 4 档经验区间列全了,点进去没有更多内容 */} - - - {salaryData.ranges.map((range, index) => ( - - {range.experience} - - {range.min}-{range.max}K - - - ))} - {salaryData.dataSource} - + {/* 3. 真实薪资区间:已摘除 */} + {/* 后端无「按经验段拆分薪资」端点(`/jobs/{id}/salary` 不存在,salary-calculator 域实网 404), + 该表无数据来源;岗位薪资已在顶部基础信息卡按 salaryMin-salaryMax 展示,不重复占位。 */} {/* 4. 员工评价(汇总 → 列表) */} @@ -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', diff --git a/src/__tests__/job-api.test.ts b/src/__tests__/job-api.test.ts index e22eb43..a126c32 100644 --- a/src/__tests__/job-api.test.ts +++ b/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'); diff --git a/src/__tests__/share-text.test.ts b/src/__tests__/share-text.test.ts index 6c3b2ac..b6714fc 100644 --- a/src/__tests__/share-text.test.ts +++ b/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); }); }); diff --git a/src/api/api.ts b/src/api/api.ts index 0f426c1..ec1a2f1 100644 --- a/src/api/api.ts +++ b/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 { } /** - * 获取岗位综合评分(含 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 综合评分 */ @@ -3168,22 +3178,12 @@ export async function fetchJobOverallScore(id: string): Promise })), }); } - // 真实后端:岗位综合评分(§7 公开读取;响应结构以 OpenAPI 为准) - return request(`/jobs/${id}/score`, { method: 'GET', auth: false }); -} - -/** - * 获取岗位真实薪资区间 - * @method GET - * @url /api/jobs/:id/salary - * @param id { string } 岗位 id - * @returns Promise 薪资数据 - */ -export async function fetchSalaryData(id: string): Promise { - if (isMockMode()) { - return mockRequest(MOCK_SALARY_DATA); - } - return request(`/jobs/${id}/salary`); + // 真实后端:岗位个性化评分(§7 公开读取,STUDENT 可用;响应结构见 personalized-review-score) + const d = await request(`/jobs/${id}/personalized-review-score`, { + method: 'GET', + auth: false, + }); + return mapPersonalizedScore(d); } /** @@ -3432,7 +3432,6 @@ export async function fetchCompareData(jobIds: string[]): Promise { * 把岗位详情拼成可分享的纯文本(分享用,纯函数)。 * @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 { 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('、')}`); } diff --git a/src/api/index.ts b/src/api/index.ts index 59d4383..01d8534 100644 --- a/src/api/index.ts +++ b/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, diff --git a/src/hooks/useJobDetail.ts b/src/hooks/useJobDetail.ts index 726789b..5d190ef 100644 --- a/src/hooks/useJobDetail.ts +++ b/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 => { - 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, diff --git a/src/types/index.ts b/src/types/index.ts index 3d647b5..ce7c808 100644 --- a/src/types/index.ts +++ b/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 { diff --git a/src/types/job.ts b/src/types/job.ts index f340055..686c785 100644 --- a/src/types/job.ts +++ b/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; + effectiveWeights: Record; + /** 维度名 → 分值 */ + dimensionScores: Record; + coveredWeight: number; + missingDimensions: string[]; } /** 员工评价 */