Files
shz-admin/docs/pc-jobseeker-api-audit.md
lapuda b4b17f2ada
Some checks failed
Node CI / build (14.x, macOS-latest) (push) Has been cancelled
Node CI / build (14.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (14.x, windows-latest) (push) Has been cancelled
Node CI / build (16.x, macOS-latest) (push) Has been cancelled
Node CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node CI / build (16.x, windows-latest) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
coverage CI / build (push) Has been cancelled
Node pnpm CI / build (16.x, macOS-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, ubuntu-latest) (push) Has been cancelled
Node pnpm CI / build (16.x, windows-latest) (push) Has been cancelled
feat: 添加小程序二维码标题和体验版提示信息
2026-07-23 17:23:13 +08:00

289 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# PC 求职者端接口调用深度审查
> 审查日期2026-07-23
> 审查对象:`shz-admin` 中由 PC 求职者路由、其共享头部组件及求职者专用弹窗实际引用的接口调用。
> 本文是**前端静态调用清单**,并用测试环境匿名首页的实际网络记录交叉验证;不代表后端已对每个接口的权限、字段和错误码逐一验收。
## 1. 结论摘要
| 项目 | 结果 |
| --- | --- |
| 求职者路由数 | 21 条(首页、职位、招聘会、简历、个人中心、政策、直播等) |
| 已识别的不同“方法 + 路径”调用 | **60 条** |
| `/cms/` 类调用 | **33 条** |
| 非 `/cms/` 的 App / SSO / 会话调用 | **27 条** |
| 直接 `fetch` / `axios` 调用 | 未发现;页面统一通过 Umi `request` 发起 |
| 风险结论 | 求职者端大量直接调用 `/cms/` 路径其中包含个人资料、收藏、投递、投诉、消息、屏蔽企业、字典、行业及政策导出。路径命名与“CMS 是后台接口”的边界不一致,需要后端按求职者身份做强制鉴权和资源归属校验。 |
**最重要的审查发现:**`/cms/` 不能仅凭前端路径前缀被视为“后台专用且不会被求职者调用”。本项目的 PC 求职者代码已实际引用 33 个此类端点。若后端仅凭 URI 前缀而不是用户角色、数据归属和操作权限做控制,会形成越权风险。
## 2. 审查范围与方法
### 覆盖范围
1. 路由定义:[config/routes.ts](../config/routes.ts#L40) 的 `/job-portal/**` 分支。
2. 页面代码:`src/pages/JobPortal/**`
3. 求职者页面必经的共享组件:
- [JobPortalHeader](../src/components/JobPortalHeader/index.tsx)
- [JobComplaintModal](../src/components/JobComplaintModal/index.tsx)
- [JobTitleSelector](../src/components/JobTitleSelector.tsx)
4. 由上述页面/组件导入的 service`services/jobportal``services/common/jobTitle``services/cms/policyInfo``services/cms/jobComplaint``services/classify/industry``services/system/dict``services/Management/list``services/session` 与 SSO 辅助模块。
### 识别规则
-`request(...)`、下载请求和页面内直接 `request(...)` 为接口调用源。
- 仅列出从求职者路由实际可达的调用;同一 URI 的不同 HTTP 方法视为不同接口。
- **CMS 类接口**:请求字符串中包含 `/cms/`。本分类基于调用路径,不推断后端最终的角色授权。
- 纯管理端 service 中未被求职者页面导入的方法不纳入清单。
### 生产路径换算
[src/app.tsx](../src/app.tsx#L269) 为生产请求配置了当前域名下的 `/api/shihezi/` 基址,并会将源码请求参数开头的 `/api` 去掉。因此表中的“源码 request 路径”会在测试环境实际成为:
```text
源码:/api/cms/job/recommend
实际https://test.xjshzly.longbiosphere.com/api/shihezi/cms/job/recommend
源码:/app/job/123
实际https://test.xjshzly.longbiosphere.com/api/shihezi/app/job/123
```
文中使用源码路径,方便与前端代码逐字对照;部署时请统一在此前面补上 `/api/shihezi`
## 3. 测试环境实测证据
### 入口
- 用户提供的入口:`https://test.xjshzly.longbiosphere.com/shihezi/`
- 恢复后最终路由:`/shihezi/job-portal`
- 身份:未登录的 PC 求职者;没有读取浏览器 Cookie、Storage 或 Token也没有执行写操作。
### 首屏真实请求
| 源码请求路径 | 实际状态 | 关联代码 | 说明 |
| --- | --: | --- | --- |
| `GET /api/app/common/jobTitle/treeselect` | 502 | 首页、职位列表、简历、职位选择组件 | 职位树加载失败。 |
| `GET /api/cms/job/recommend?order=2` | 502 | 首页、全局头部、职位列表 | 热门职位加载失败。 |
页面在请求失败时仍呈现示例职位卡片,属于前端兜底展示,不能视为接口成功返回真实业务数据。该 502 是审查时刻的测试环境可用性快照,不影响下文静态调用覆盖结论。
## 4. 求职者路由与功能边界
| 路由 | 页面 | 核心功能 | 调用类别 |
| --- | --- | --- | --- |
| `/job-portal` | 首页 | 职位分类、热门推荐 | App + CMS |
| `/job-portal/list` | 职位列表 | 搜索、职位详情预览、收藏/投递/投诉/屏蔽 | App + CMS |
| `/job-portal/detail` | 职位详情 | 详情、竞争力、收藏/投递/投诉/屏蔽 | App + CMS |
| `/job-portal/job-fair` | 招聘会 | 线上与线下招聘会列表、日历与摘要 | App |
| `/job-portal/job-fair/detail` | 招聘会详情 | 招聘会详情、线上关联岗位 | App |
| `/job-portal/resume` | 简历 | 基础资料、工作经历、技能维护 | App + CMS |
| `/job-portal/personal-center` | 个人中心 | 个人统计与资料展示 | CMS |
| `/job-portal/personal-center/applications` | 已投递 | 已投岗位 | CMS |
| `/job-portal/personal-center/favorites` | 收藏 | 收藏列表、取消收藏 | CMS |
| `/job-portal/personal-center/footprints` | 足迹 | 浏览足迹 | CMS |
| `/job-portal/personal-center/showcase` | 我的风采 | 文件上传、查询、删除 | App |
| `/job-portal/personal-center/interviews` | 面试邀约 | 查询、接受/拒绝 | App |
| `/job-portal/personal-center/complaints` | 我的投诉 | 查询、撤销投诉 | CMS |
| `/job-portal/personal-center/blocked-companies` | 屏蔽企业 | 查询、解除屏蔽 | CMS |
| `/job-portal/message` | 消息 | 查询、已读、删除、全部已读 | CMS |
| `/job-portal/career-recommendation` | 职业推荐 | 读取当前求职者资料 | CMS |
| `/job-portal/policy` | 政策列表 | 门户政策、按用户类型筛选、下载 | App + CMS |
| `/job-portal/policy/detail` | 政策详情 | 门户政策详情 | App + CMS |
| `/job-portal/live-recruitment` | 直播带岗 | 直播列表 | App |
| `/job-portal/profile` | 我的 | 读取客户端缓存展示,未发现新增远程请求 | 无新增 |
`JobPortalHeader` 会被绝大多数页面复用,因而其中的职位联想、热门职位和登录后消息未读数请求是**跨路由调用**。
## 5. CMS 类接口清单33 条)
> 这是本次审查的重点。所有条目均由求职者端页面或其共用组件导入并调用;“需登录”表示前端存在登录/用户 ID 前置校验或该功能显然依赖个人数据,最终权限仍必须由后端校验。
| ID | 方法与源码 request 路径 | 用途与主要参数/请求体 | 调用页面/组件 | 登录/触发条件 |
| --- | --- | --- | --- | --- |
| C01 | `GET /api/cms/job/recommend` | 职位推荐;`order=2` 为热门,`order=0,isPublish=1,jobCategory` 为分类列表;可带 `jobTitle` | 首页、职位列表、`JobPortalHeader` | 公共浏览 |
| C02 | `POST /api/cms/userworkexperiences` | 新增工作经历:`companyName,position,startDate,endDate,description` | 简历 | 需登录 |
| C03 | `GET /api/cms/appUser/getUserInfo` | 当前求职者资料 | 职业推荐、简历、登录身份预取 | 需登录 |
| C04 | `GET /api/cms/appUser/getMyTj` | 我的投递/收藏等统计 | 个人中心 | 需登录 |
| C05 | `GET /api/cms/job/getAppUserYhsq` | 已投递岗位;`userId` | 已投递 | 需登录 |
| C06 | `GET /api/cms/job/getAppUserYhsc` | 收藏岗位;`userId` | 收藏 | 需登录 |
| C07 | `GET /api/cms/job/getAppUserYhfwzj` | 浏览足迹;`userId` | 足迹 | 需登录 |
| C08 | `POST /api/cms/job/collection` | 收藏岗位:`jobId,userId` | 职位列表、职位详情 | 需登录 |
| C09 | `POST /api/cms/job/collectionCancel` | 取消收藏:`jobId,userId` | 职位列表、职位详情、收藏 | 需登录 |
| C10 | `PUT /api/cms/jobApply` | 投递岗位:`jobId,userId` | 职位列表、职位详情 | 需登录 |
| C11 | `POST /api/cms/job/browse` | 记录职位浏览:`jobId` | 职位列表、职位详情 | 需登录后触发 |
| C12 | `GET /api/cms/notice/appNoticList` | 消息列表;`pageNum,pageSize,noticeType,isRead` | 消息 | 需登录 |
| C13 | `GET /api/cms/notice/appNoticReadList` | 未读消息数量/列表 | 消息 | 需登录 |
| C14 | `GET /api/cms/notice/noticTotal` | 消息总数与未读数 | 消息、全局头部 | 需登录 |
| C15 | `GET /api/cms/notice/appNoticYdList` | 已读消息列表;`pageNum,pageSize` | 消息 | 需登录 |
| C16 | `POST /api/cms/notice/read/sysNotice?id={id}` | 标记单条消息已读 | 消息 | 需登录 |
| C17 | `POST /api/cms/appUser/markAllMessagesRead` | 标记全部消息已读 | 消息 | 需登录 |
| C18 | `DELETE /api/cms/notice/deleteNotice/{id}` | 删除消息 | 消息 | 需登录 |
| C19 | `POST /api/cms/jobComplaint` | 提交岗位投诉:`userId,jobId,complaintType,complaintContent,contactPhone` | 职位投诉弹窗 | 需登录 |
| C20 | `POST /api/cms/blockCompany` | 屏蔽企业:`userId,companyId` | 职位列表、职位详情 | 需登录 |
| C21 | `GET /api/cms/blockCompany/list` | 屏蔽企业列表;`pageNum,pageSize` | 屏蔽企业 | 需登录 |
| C22 | `DELETE /api/cms/blockCompany/{id}` | 解除指定屏蔽记录 | 屏蔽企业 | 需登录 |
| C23 | `GET /api/cms/dict/data/type/{dictType}` | CMS 字典值;详见下一节的字典类型 | 首页、职位、简历、个人中心、投诉、政策 | 公共展示/依页面而定 |
| C24 | `GET /api/cms/dict/jobCategory` | 简历技能分类联想;`name` | 简历 | 需登录 |
| C25 | `GET /api/cms/industry/treeselect` | 行业树 | 职位列表、职位详情 | 公共浏览 |
| C26 | `PUT /api/cms/appUser` | 更新简历基础信息/求职意向 | 简历 | 需登录 |
| C27 | `DELETE /api/cms/userworkexperiences/{id}` | 删除工作经历 | 简历 | 需登录 |
| C28 | `PUT /api/cms/userworkexperiences` | 编辑工作经历;含 `id` 与经历字段 | 简历 | 需登录 |
| C29 | `PUT /api/cms/appskill/edit` | 修改技能:`id,userId,name,levels` | 简历 | 需登录 |
| C30 | `POST /api/cms/appskill/add` | 新增技能:`name,levels` | 简历 | 需登录 |
| C31 | `GET /api/cms/jobComplaint/listSelf` | 当前用户投诉列表;`currentPage,pageSize,complaintType,complaintStatus` | 我的投诉 | 需登录 |
| C32 | `DELETE /api/cms/jobComplaint/{id}` | 撤销投诉 | 我的投诉 | 需登录 |
| C33 | `GET /api/cms/policyInfo/exportUtil?id={id}` | 下载政策文件,响应为 Blob | 政策列表 | 公共入口可见,后端应决定下载权限 |
### C23求职者端实际请求的 CMS 字典类型
`getDictValueEnum(type, isDigital, true)` 会固定落到 C23。已从调用点枚举到
| 字典类型 | 使用位置 |
| ----------------- | ------------------------ |
| `company_nature` | 首页、职位列表、职位详情 |
| `education` | 职位列表、简历、个人中心 |
| `scale` | 职位列表、职位详情 |
| `sex` | 简历、个人中心 |
| `area` | 简历、个人中心 |
| `age` | 简历、个人中心 |
| `complaint_type` | 投诉弹窗、我的投诉 |
| `user_type` | 政策列表、政策详情 |
| `policy_category` | 政策列表 |
实现依据:[getDictValueEnum](../src/services/system/dict.ts#L34)、[行业树 service](../src/services/classify/industry.ts#L17)、[求职者 user service](../src/services/jobportal/user.ts#L3)。
## 6. 非 CMS 接口清单27 条)
| ID | 方法与源码 request 路径 | 用途与主要参数/请求体 | 调用页面/组件 | 登录/触发条件 |
| --- | --- | --- | --- | --- |
| P01 | `GET /api/app/common/jobTitle/treeselect` | 职位/职位分类树 | 首页、职位列表、简历、职位选择组件 | 公共浏览 |
| P02 | `GET /api/app/job/suggest` | 搜索联想:`keyword,limit` | `JobPortalHeader` | 输入关键词时 |
| P03 | `GET /app/job/{jobId}` | 职位详情,注释说明含 `isApply,isCollection` | 职位列表、职位详情 | 公共详情;状态字段依登录态 |
| P04 | `GET /app/job/competitiveness/{jobId}` | 职位竞争力、匹配分、排行、雷达数据 | 职位列表、职位详情 | 需登录,前端未登录时不请求 |
| P05 | `POST /app/file/upload?bussinessid={userId}` | 上传“我的风采”文件,`multipart/form-data` | 我的风采 | 需登录、用户选择文件 |
| P06 | `GET /app/file/list` | 查询“我的风采”文件;`bussinessid` | 我的风采 | 需登录 |
| P07 | `DELETE /app/file/{id}` | 删除“我的风采”文件 | 我的风采 | 需登录 |
| P08 | `DELETE /api/app/appskill/{id}` | 删除技能 | 简历 | 需登录 |
| P09 | `GET /api/app/interview/list` | 我的面试邀约;`userId` | 面试邀约 | 需登录 |
| P10 | `PUT /api/app/interview/status/{id}` | 接受/拒绝邀约;请求体 `{status}` | 面试邀约 | 需登录 |
| P11 | `GET /api/app/policyInfo/portalList` | 门户政策列表 | 政策列表 | 公共浏览 |
| P12 | `GET /api/app/policyInfo/detail/{id}` | 门户政策详情 | 政策详情 | 公共浏览 |
| P13 | `GET /api/app/live/list` | 直播带岗列表;可带 `title,companyName` | 直播带岗 | 公共浏览 |
| P14 | `GET /app/jobfair/public/jobfair/page` | 线上招聘会分页;`pageNum,pageSize,jobFairTitle,zphjbsj` | 招聘会 | 公共浏览 |
| P15 | `GET /app/outdoor-fair/page` | 线下招聘会分页;参数同 P14 | 招聘会 | 公共浏览 |
| P16 | `GET /app/jobfair/public/jobfair/dates` | 线上招聘会日期集合 | 招聘会 | 公共浏览 |
| P17 | `GET /app/outdoor-fair/dates` | 线下招聘会日期集合 | 招聘会 | 公共浏览 |
| P18 | `GET /app/jobfair/public/jobfair/currentMonth` | 当月线上招聘会摘要 | 招聘会 | 公共浏览 |
| P19 | `GET /app/outdoor-fair/currentMonth` | 当月线下招聘会摘要 | 招聘会 | 公共浏览 |
| P20 | `GET /app/jobfair/public/jobfair/currentQuarter` | 本季度线上招聘会摘要 | 招聘会 | 公共浏览 |
| P21 | `GET /app/outdoor-fair/currentQuarter` | 本季度线下招聘会摘要 | 招聘会 | 公共浏览 |
| P22 | `GET /app/jobfair/public/jobfair/detail` | 线上招聘会详情;`jobFairId` | 招聘会详情 | 公共浏览 |
| P23 | `GET /app/outdoor-fair/{jobFairId}` | 线下招聘会详情 | 招聘会详情 | 公共浏览 |
| P24 | `GET /app/jobfair/public/jobfair/enterprises-with-jobs-by-job-fair-id` | 线上招聘会企业及关联岗位;`jobFairId` | 招聘会详情 | 仅线上招聘会 |
| P25 | `POST /api/sso/pcms/code/login` | 第三方 `code` 换取会话;请求体 `{code}`,不附现有 Token | 根路径/`login-tow` 的 SSO 过渡 | URL 含 `code` |
| P26 | `POST /api/sso/pc/code/login` | 第三方 `token` 换取会话;请求体 `{code: token}`,不附现有 Token | 根路径/`login-tow` 的 SSO 过渡 | URL 含 `token` |
| P27 | `GET /api/getInfo` | 当前会话用户资料;首页在“有 Token 但缓存未就绪”时补拉 | 首页、身份预取 | 有有效会话 |
实现依据:[职位通用 service](../src/services/common/jobTitle.ts#L3)、[招聘会 service](../src/services/jobportal/jobFair.ts#L139)、[竞争力 service](../src/services/jobportal/competitiveness.ts#L30)、[政策门户 service](../src/services/cms/policyInfo.ts#L12)、[SSO service](../src/services/jobportal/auth.ts#L10)。
## 7. 调用链和登录边界
```text
浏览器 /shihezi/
└─ 前端路由 /job-portal
├─ 全局头部P02、C01登录后还会调用 C14
├─ 首页P01、C01、C23有会话且缓存缺失时 P27
├─ 职位列表/详情P01、P03、P04、C01、C08~C11、C19、C20、C23、C25
├─ 简历P01、P08、C02、C03、C23、C24、C26~C30
├─ 个人中心C03~C07、C21~C23、C31~C32、P05~P07、P09~P10
├─ 消息C12~C18
├─ 政策P11~P12、C23、C33
├─ 招聘会P14~P24
├─ 直播带岗P13
└─ 第三方进入P25 / P26
```
前端会在收藏、投递、投诉、屏蔽、简历和个人中心入口执行登录/用户 ID 前置校验;实现见 [jobPortalAuth.ts](../src/utils/jobPortalAuth.ts#L111)。这是用户体验保护,**不是**后端授权边界。特别是请求体中显式携带 `userId` 的 C05~C10、C19、C20 以及 P05/P06/P09后端必须从认证主体推导用户 ID或至少校验参数与认证主体一致。
## 8. 代码审查发现与建议
### 高优先级CMS 路径在求职者端广泛使用
- **证据**C01~C33 全部存在于求职者可达调用链,其中写操作至少包括收藏、取消收藏、投递、浏览记录、消息已读/删除、投诉、屏蔽、简历编辑、工作经历、技能维护及政策下载。
- **风险**:若网关/后端将 `/cms/**` 视为后台路由,可能导致求职端功能被错误拒绝;反过来,若为兼容而放宽 `/cms/**`,则容易放大管理后台能力暴露面。
- **建议**
1. 优先为求职者能力建立 `/app/**``/portal/**` 的专用 DTO、Controller 与权限策略;不要继续把“后台资源的 URI”作为前端契约。
2. 迁移前,后端应按“求职者角色 + 资源归属”对全部 C05~C10、C19~C22、C26~C32 做强制校验。
3. 迁移时保留短期兼容层,记录 `/cms/**` 的求职者访问量,确认无调用后再下线旧路由。
### 高优先级:`userId` 由客户端传入的水平越权面
涉及 C05~C10、C19、C20、P05、P06、P09。前端虽然通常从当前用户缓存读取 ID但浏览器请求可被篡改。
- 后端不应信任请求中的 `userId` 来决定数据所有者。
- 对“查看自己的投递/收藏/足迹/面试/文件”“增删自己的简历、投诉、屏蔽”等操作,应以认证 Token 的 subject 为准。
- 若保留 `userId` 兼容字段,至少断言它等于当前登录求职者;不匹配时返回 403 并记录审计日志。
### 中优先级:方法与注释/命名不一致,容易误审权限
源码中存在多处注释与真实 HTTP 合约不一致:
- `favoriteJob` 注释写为 `/api/job-portal/list`,真实为 `POST /api/cms/job/collection`
- `unfavoriteJob` 注释写为 `DELETE /app/company/card/collection`,真实为 `POST /api/cms/job/collectionCancel`
- `applyJob` 注释写 `POST /api/cms/jobApply/apply`,真实为 `PUT /api/cms/jobApply`
- 多个消息函数的注释写 `appUser/*`,真实路径为 `notice/*`
建议以 OpenAPI/接口类型为单一事实来源,自动生成 service至少在服务函数名、JSDoc、HTTP 方法与实际 path 四者之间增加契约测试。
### 中优先级:匿名首页存在接口 502且 UI 回退会掩盖问题
审查时 P01 与 C01 均返回 502但首页仍展示静态示例岗位。建议
- 生产环境不要把演示招聘数据作为接口失败的静默回退,改为明确的“加载失败/重试”状态。
- 为 P01 和 C01 增加测试环境健康检查、网关 upstream 监控与告警。
- UI 事件中记录去敏后的请求失败码和功能上下文,便于区分“暂无数据”与“网关故障”。
### 中优先级:政策下载混入 CMS 资源边界
政策列表/详情使用 P11/P12 门户接口,但下载使用 C33 的 `/cms/policyInfo/exportUtil`。建议提供对应的门户下载端点,并明确是否允许匿名下载、是否需要鉴权和审计。
### 低优先级:响应类型与权限语义需收敛
- `getJobDetail`、文件接口等返回类型过于宽泛(如 `API.Result` 或未显式类型),会削弱调用端对字段与错误码的约束。
- 建议将列表、详情、写操作、下载和分页响应定义为独立类型;对 401、403、404、409 和 5xx 在 UI 层做一致处理。
## 9. 后端复核清单
在后端/网关侧逐项核查以下内容,才能把本前端审查升级为完整安全验收:
1. 对 C01~C33 所有实际路由,导出 Controller 映射、允许角色、认证注解和数据范围规则。
2. 对所有含 `userId` 的 C05~C10、C19、C20、P05、P06、P09验证越权请求会得到 403而不会读取或写入他人数据。
3. 对 C02、C26~C32 的写操作,确认审计字段(创建人、更新人、时间、操作来源)由服务端写入,不接受客户端伪造。
4. 对 C33、P05~P07 的文件接口验证文件归属、下载授权、MIME/大小限制、路径穿越防护与病毒扫描策略。
5. 对 P25/P26 的 SSO 换票,确认 `code`/`token` 一次性、时效、重放防护、来源校验和 Token 不出现在日志中。
6. 修复/定位测试环境 P01、C01 的 502 后,再分别用匿名求职者和测试登录求职者做只读接口冒烟测试。
## 10. 可复现的审查命令
以下命令只读取代码,可用于后续补充或复核:
```bash
# 列出求职者路由
rg -n "job-portal" config/routes.ts
# 找出求职者页面直接发起的 request
rg -n "request\\(" src/pages/JobPortal src/components/JobPortalHeader \
src/components/JobComplaintModal src/components/JobTitleSelector.tsx
# 找出涉及 CMS 的 service 调用
rg -n "['\\\"]/api/cms/" src/pages/JobPortal src/services/jobportal \
src/services/common src/services/cms src/services/classify src/services/system src/services/Management
```
## 11. 未纳入范围
- 纯管理端、企业端、系统工具和小程序的接口。
- 后端 Controller、数据库、网关权限的逐条实现验证。
- 已被 service 定义但没有被求职者可达页面调用的管理端接口。
- 本次没有使用测试账号执行任何写接口;表中所有写操作均来自静态调用链审查。