# 岗位与企业展示字段 · 前端交接说明 > 批次:job-company-display-fields > 后端基线:`1cbadc3e18a786ef642b0d329adde6b173c7b2ac`(分支 `fix/mysql57-module-migrations-20261009`) > 数据库迁移:`V57__job_company_display_fields.sql` > 本文档面向前端工程师,描述本批岗位展示字段的接入参考,不改变既有字段语义。 > 更新:2026-10-19。本地后端验收已通过,但尚未提交、推送或发布;目标服务器是否具备这些字段必须按实际部署版本确认。本文不是前端已经完成接入的证明。 --- ## 1. 新字段在哪些端点返回 | 端点 | 新增顶层字段 | `company` 摘要 | |---|---|---| | `GET /jobs`(公开列表) | ✅ 5 个 | ✅ 新增 | | `GET /jobs/search`(公开搜索) | ✅ 5 个 | ✅ 新增(兼容扩展) | | `GET /jobs/public/{jobId}`(公开详情) | ✅ 5 个 | ✅ 新增 | | `GET /companies/{companyId}/jobs` | ✅ 5 个 | ✅ 新增 | | `GET /jobs/{jobId}`(企业端自己看) | ✅ 5 个 | ❌ 不返回 | | `POST /jobs`、`PUT /jobs/{jobId}`、`PATCH /jobs/{jobId}/status` | ✅ 5 个(响应同企业端视图) | ❌ 不返回 | | `GET /companies/me/jobs`(企业端岗位管理) | ✅ 5 个 | ❌ 不返回 | | `GET /companies/{companyId}`(公开企业详情) | `companySize` | — | | `GET/POST/PUT /companies/me`(企业端自己看) | `companySize` | — | | `GET /jobs/favorites`(我的收藏) | ✅ 5 个 | ❌ 不返回 | 「5 个」= `experience`、`education`、`benefits`、`salaryMonths`、`workLocation`。 > 说明:收藏列表复用公开视图组装,因此自动带上 5 个新增顶层字段;本批未为收藏域增加 `company` 嵌套。若前端需要企业名,请先用岗位的 `companyId` 调 `GET /companies/{companyId}`。 --- ## 2. 字段类型、单位、缺省值与更新语义 ### 2.1 岗位新增字段 | 字段 | 类型 | 缺省值 | 写入语义 | |---|---|---|---| | `experience` | string \| null | `null` | `null`/缺省 = 保持原值;**空白串 = 清空为 null**;非空 = 去两端空白后保存;最长 40 | | `education` | string \| null | `null` | 同 `experience`,最长 40 | | `benefits` | string[] | `[]`(**永不返回 null**) | `null`/缺省 = 保持原值;**`[]` = 清空**;非空 = 逐项 trim → 拒绝空白项 → 单项 ≤40 → **去重且保留首次出现顺序** → 最多 20 项;违反任一条返回 400 | | `salaryMonths` | int \| null | `null` | `null`/缺省 = 保持原值(**本批不提供显式清空**);非 null 必须 **1—24**,**`0` 视为非法**返回 400;**只接受 JSON 整数——小数(`1.9`/`1.0`)、数字字符串(`"12"`/`""`)、布尔、数组、对象、超出 int 范围一律 400**,绝不截断或四舍五入 | | `workLocation` | string \| null | `null` | 同 `experience`,最长 255 | ### 2.2 企业新增字段 | 字段 | 类型 | 缺省值 | 写入语义 | |---|---|---|---| | `companySize` | string \| null | `null` | `null`/缺省 = 保持原值;**空白串 = 清空为 null**;非空 = trim 后保存;**超过 40 字符返回 400,不截断** | 上述「缺省/null 保持原值」针对更新;创建时没有旧值,对应可选字段存 NULL。响应中的福利始终为数组,请求中的 `benefits` 可以为 null。 ### 2.3 福利归一化与 OpenAPI 3.1 - 去重后的数量上限为 20,不是原始请求数组上限:21 个相同标签会成为 1 项并可接受,21 个不同标签返回 400。 - 单项长度在 trim 后判断;空白项拒绝,不是自动删去。 - 更新 `benefits: null` 保留原值,`benefits: []` 清空。不要把这两种请求合并。 - 静态及运行时契约用 `x-normalization` 描述 `trimItems`、`deduplicate`、`rejectBlankItems`、`maxItems=20`、`itemMaxLength=40`,不对原始请求误加 `maxItems`/元素 `maxLength`。 - 这是项目的业务扩展,通用 OpenAPI 客户端不会自动执行归一化。若前端加校验,应按相同顺序处理;后端仍是最终校验方。 - 运行时为 OpenAPI 3.1,可空字段使用包含 null 的联合类型,不以缺少 OpenAPI 3.0 的 `nullable` 判为不可空。 ### 2.4 单位与语义(**保持不变**) - `salaryMin` / `salaryMax`:**单位仍是「元/月」**,本批未改成 K。前端展示 K 时自行 `/1000`(既有约定)。 - 行业仍来自 `companies.industry`,**不在岗位表重复存储**。 ### 2.5 只读约定 - `benefits` 读取时:库中为 `NULL` → 返回 `[]`(不是 `null`)。前端可安全地对它做 `.length` / `.map`,无需判空。 - `company`:企业不存在时返回 `null`,**不会编造数据、也不会 500**。前端需容忍 `company === null`。 --- ## 3. `company` 嵌套结构 → 前端类型映射 后端 `company` 对象(白名单,**仅这 7 个字段**): ```ts interface BackendJobCompany { id: number; name: string; logoUrl: string | null; description: string | null; industry: string | null; verified: boolean; companySize: string | null; } ``` 建议按此前前端类型映射到 `src/types/job.ts`;本轮未修改或重新验收前端,具体类型请以接手工程师当前代码为准: | 后端字段 | 前端字段 | 备注 | |---|---|---| | `company.id` | —(可用于跳企业页) | 前端 `Job` 无对应字段 | | `company.name` | `Job.companyName` | 已有映射 `company.name → companyName` | | `company.logoUrl` | — | 前端暂无对应字段 | | `company.description` | — | 前端暂无对应字段 | | **`company.industry`** | **`Job.industry`** | 本批接通,取代前端写死的空值 | | `company.verified` | — | 前端暂无对应字段 | | **`company.companySize`** | **`JobDetail.companySize`** | 本批接通 | | `job.workLocation` | (见 §4) | | | `job.city` | `Job.city` | 既有 | | `job.title` | `Job.positionName` | 既有 | | `job.salaryMin/salaryMax` | `Job.salaryMin/salaryMax` | **单位元→K 需 /1000** | | `job.experience` | `Job.experience` | 本批接通 | | `job.education` | `Job.education` | 本批接通 | | `job.benefits` | `Job.benefits` | 本批接通 | | `job.salaryMonths` | `JobDetail.salaryMonths` | 本批接通(展示为「N 薪」) | | `job.description` / `requirements` | `JobDetail.description` / `requirements` | 既有,原样保留换行、不截断 | **逐条要点**: 1. `company.industry` → 前端 `industry`:直接取 `company.industry`,缺失时为 `null`。 2. `company.companySize` → 前端 `companySize`:直接取 `company.companySize`,缺失时为 `null`。 3. 顶层 `experience`、`education`、`benefits`、`salaryMonths` 直接映射到同名前端字段。 --- ## 4. `workLocation` 的处理 - `workLocation` 是**岗位明确录入的工作地点**,独立于 `city`。 - 若 `workLocation` 缺失(`null`),**前端可退化为展示 `city`**。 - **但不得据此回写**:不要因为展示用了 `city`,就把 `city` 当作详细办公地址保存或提交回后端。后端的「企业登记地址」(`companies.address`)也**不会被**当作公开招聘地址下发,前端同样不要拿它当工作地点展示。 --- ## 5. 评分与评价:继续调用既有真实接口 本批**没有**新增/修改任何评分、评价、排名相关字段或端点。前端请继续使用既有接口: - 岗位个性化评分:`GET /jobs/{jobId}/personalized-review-score` - 岗位评价列表 / 汇总:`GET /jobs/{jobId}/reviews`、`GET /jobs/{jobId}/reviews/summary` 本批也**不补造**综合评分、浏览量、排名或员工评价——这些字段若前端仍写死,属前端侧待接,不是后端缺字段。 --- ## 6. 前端工程师需核对的字段映射 以下字段在本批后端验收制品中可返回。此前交接发现部分前端映射为空;接手工程师需核对当前实现,不能由本文件推断前端仍未接入或服务器已发布: | 前端字段 | 后端来源 | 备注 | |---|---|---| | `Job.experience` | `job.experience` | 本批新增下发 | | `Job.education` | `job.education` | 本批新增下发 | | `Job.benefits` | `job.benefits` | 本批新增下发,恒为数组 | | `Job.industry` | `company.industry` | 本批新增下发 | | `JobDetail.companySize` | `company.companySize` | 本批新增下发 | | `JobDetail.salaryMonths` | `job.salaryMonths` | 本批新增下发 | 需接入的展示字段应按上述来源映射;`JobDetail.views`(浏览数)本批**未新增提供**,不要把本地模拟值当服务器真实数据。 --- ## 7. 投递 / 沟通按钮 投递、沟通、邀请面试等按钮属于**前端入口**。本批只是补齐后端岗位与企业资料字段,**不会自动生成任何按钮**,也不会新增/修改投递与沟通的接口契约。 --- ## 8. 响应示例 ### 8.1 新增字段完整(`GET /jobs/public/{jobId}`) ```json { "code": 200, "message": "ok", "data": { "id": 101, "companyId": 5, "title": "前端开发工程师", "description": "负责招聘平台 App 的页面开发。", "requirements": "1. 熟悉 React Native\n2. 熟悉 TypeScript", "city": "北京", "employmentType": "FULL_TIME", "salaryMin": 18000, "salaryMax": 26000, "experience": "3-5年", "education": "本科", "benefits": ["五险一金", "年终奖", "弹性工作"], "salaryMonths": 14, "workLocation": "北京市海淀区中关村大街 1 号 A 座 12 层", "status": "PUBLISHED", "publishedAt": "2026-10-01T10:00:00", "createdAt": "2026-09-30T18:20:00", "company": { "id": 5, "name": "演示示例科技有限公司", "logoUrl": "https://example.invalid/logo.png", "description": "非真实招聘,仅用于本地演示。", "industry": "互联网/IT", "verified": true, "companySize": "100-499人" } }, "requestId": "app-xxxxxx", "timestamp": "2026-10-01T10:00:01Z" } ``` ### 8.2 历史数据字段缺失(老岗位,未补录) ```json { "code": 200, "message": "ok", "data": { "id": 7, "companyId": 2, "title": "后端开发实习生", "description": "参与接口开发。", "requirements": null, "city": "武汉", "employmentType": "INTERN", "salaryMin": 3000, "salaryMax": 5000, "experience": null, "education": null, "benefits": [], "salaryMonths": null, "workLocation": null, "status": "PUBLISHED", "publishedAt": "2026-09-01T09:00:00", "createdAt": "2026-08-31T20:00:00", "company": { "id": 2, "name": "演示联合信息科技有限公司", "logoUrl": null, "description": null, "industry": null, "verified": false, "companySize": null } }, "requestId": "app-yyyyyy", "timestamp": "2026-10-01T10:00:01Z" } ``` > 缺失即为 `null` / `[]`,**后端不会填充「本科」「3—5年」「13薪」等默认值**。前端请按缺失处理(隐藏区块或显示「未填写」)。 --- ## 9. 兼容性提示 1. **旧请求不受影响**:不传新字段的创建/修改请求行为完全不变。 2. **旧响应字段不变**:`companyId` 等既有字段保留,未删除、未改名、未改类型;`GET /jobs/search` 原 `company` 结构为**兼容扩展**(只在其中新增字段)。 3. **草稿与已关闭岗位的可见性规则不变**:DRAFT 岗位公开详情仍不可见(404)。 4. `error_code` 约定不变:本批所有新增字段的校验失败统一返回既有参数错误响应(`code=1001`)。 ## 10. 发布后联调检查(尚未执行) 以下为待办,不是已完成的验收结果。仅在对应后端版本发布后,使用获得授权的测试账号执行。 1. 确认响应来自目标服务而非本地示例;企业名、规模、行业和岗位五字段按实际值显示,缺失时隐藏或提示未填写。 2. 月薪元到 K 的转换只做一次;`salaryMonths: null` 不默认为 13 或 0,不向更新接口提交 0。 3. 检查字段缺省/null/空白/空数组语义,福利重复和 trim 边界与后端一致。 4. 验证公开摘要仅七字段、`company: null` 可正常渲染;不要用企业登记地址代替工作地点。 5. 评分和员工评价继续使用真实接口,不能用演示值补齐;投递和沟通入口按已有业务权限处理。 6. 对参数错误展示可理解提示,失败后重新读取确认没有部分更新;不伪造成功。 ## 11. 验收与相关说明 - 最新本地全量 `clean verify`:3464 项通过,失败/错误/跳过均为 0。真实 HTTP 黑盒合计 106 项,其中岗位企业展示 48 项;重启合计 6 项。 - 静态/运行时契约对照 129 项通过,契约反向测试 13 项通过。不是前端端到端验收结果。 - 交付报告:`docs/batch-job-company-display-fields-report.md`;简历权限与文件名规则:`docs/resume-ownership-hardening-20261016.md`。 - 最新证据:`D:/Acceptance/qsp-final-20261019/evidence/acceptance-summary.json`。未发布、未连接生产库、未向线上导入演示企业岗位。