15 KiB
简历版本管理与导出模块接口说明
职来 (mydreamjob) · 简历版本管理与导出基础模块 基础路径:
/api/v1· 除明确标注外全部接口需Authorization: Bearer <accessToken>
1. 概述
学生在既有单简历(/profile/me/resume,上传即替换)之外,新增多版本简历管理:
创建/列表/详情/修改/删除/设默认,以及异步导出任务(复制该版本文件到导出目录并查询状态)。
本轮不做 AI 简历生成/优化、简历查重、词云、匹配算法、SHAP、Offer 新功能。
与旧单简历 student_profiles.resume_url 完全独立,不修改其原有语义。
2. 数据模型(Flyway V14 / V15)
resume_versions(简历版本)
id、user_id(FK→users)、name(版本名,可空)、version_no(每用户递增:max+1)file_storage_path/file_name/content_type/file_size(复用ResumeStorageService存储)default_key:可空判别列,默认时'DEFAULT'否则NULL;UNIQUE(user_id, default_key)保证每用户最多一个默认(多个 NULL 不冲突)status(ACTIVE/DELETED)、created_at/updated_atUNIQUE(user_id, version_no):并发创建时版本号唯一(DB 硬约束,Service 层独立事务重试)
resume_export_tasks(导出任务)
id、user_id(FK→users)、resume_version_id(FK→resume_versions ON DELETE SET NULL)resume_version_name(版本名快照,删除版本后任务仍可读)- 源文件快照(V15 新增,创建任务时写入,worker 据此读取而非查当前版本):
source_storage_path、source_file_name、source_content_type、source_file_size status:PENDING → RUNNING → SUCCESS / FAILEDoutput_path/output_name(成功时,output_path仅内部使用不对外返回)、error_message(失败必须记录原因)created_at/started_at/finished_at
2.1 文件一致性保证
- 修改版本文件:先校验并保存新文件 → 更新数据库 → 事务提交后再删除旧文件;数据库保存失败时清理刚保存的新文件,旧文件保持可用;若旧文件正被进行中的导出任务引用(快照),则不删除以保导出一致性。
- 删除版本:先完成数据库删除,事务提交后再删除存储文件;数据库失败时不得删除原文件。
- 创建版本:版本号并发重试最终失败或数据库写入失败时,清理已保存但未被引用的新文件,不留下孤儿文件。
3. 接口清单
3.1 创建简历版本
POST /profile/me/resume-versions (multipart/form-data)
权限:学生本人。
表单字段:
| 字段 | 必填 | 约束 |
|---|---|---|
file |
是 | PDF/DOC/DOCX,≤10MB;扩展名、Content-Type 与实际文件结构必须一致 |
name |
否 | ≤120 字符,可空 |
默认状态 ACTIVE、非默认;版本号自动分配(max+1)。并发创建由 DB 唯一约束兜底,冲突自动重试(独立事务)。
3.2 简历版本列表
GET /profile/me/resume-versions
权限:学生本人。返回本人 ACTIVE 版本,version_no 倒序。
响应不包含 userId、filePath 等内部字段:
{ "code": 200, "data": [ { "id": 1, "name": "秋招版", "versionNo": 2,
"fileName": "resume.pdf", "contentType": "application/pdf", "fileSize": 1048576,
"default": false, "status": "ACTIVE",
"createdAt": "...", "updatedAt": "..." } ] }
3.3 简历版本详情
GET /profile/me/resume-versions/{id}
权限:学生本人。他人 403(FORBIDDEN),不存在/已删除 404(RESUME_VERSION_NOT_FOUND)。
3.4 修改简历版本
PUT /profile/me/resume-versions/{id} (multipart/form-data)
权限:学生本人。可选字段(均不传则不更新):
file:替换文件(校验同上,替换时删除旧文件);name:重命名。
3.5 删除简历版本
DELETE /profile/me/resume-versions/{id}
权限:学生本人。
- 默认版本不可删除:必须先设置其他默认(409
DEFAULT_RESUME_DELETE_NOT_ALLOWED); - 有 PENDING/RUNNING 导出任务不可删除(409
RESUME_EXPORT_IN_PROGRESS); - 其余:删除存储文件并移除记录(硬删除)。
3.6 设为默认简历
POST /profile/me/resume-versions/{id}/default
权限:学生本人。同事务内清除旧默认并设置新默认;已默认幂等;并发冲突 409 DUPLICATE_DEFAULT_RESUME。
同一用户最多一个默认(DB 唯一约束 uk_resume_versions_default)。
3.7 创建导出任务
POST /profile/me/resume-versions/{id}/export
权限:学生本人。创建 PENDING 任务,并将当前版本的源文件快照(source_storage_path/file_name/content_type/file_size)写入任务;事务提交后触发 @Async worker:
读取任务快照中的源文件 → 复制到独立导出目录 ./resume-exports/{userId}/{taskId}{ext} → SUCCESS;
源文件缺失/IO 异常 → FAILED + error_message。返回任务视图。
由于 worker 从任务快照读取源文件,任务创建后即使立刻修改/替换简历版本,导出结果仍为创建任务时的原文件。
3.8 导出任务状态
GET /resume-export-tasks/{taskId}
权限:学生本人(task.userId = 当前用户,否则 403;不存在 404)。
响应不包含 userId、outputPath、source_* 等内部字段:
{ "code": 200, "data": { "id": 10, "resumeVersionId": 1, "resumeVersionName": "秋招版",
"status": "SUCCESS", "outputName": "resume.pdf", "downloadAvailable": true,
"errorMessage": null, "createdAt": "...", "startedAt": "...", "finishedAt": "..." } }
3.9 下载导出产物
GET /resume-export-tasks/{taskId}/download
权限:学生本人。
- 仅任务创建者本人且任务状态为
SUCCESS可下载; - 其他用户 403(
FORBIDDEN);任务不存在 404(RESUME_EXPORT_TASK_NOT_FOUND);未完成/失败 409(RESUME_EXPORT_NOT_READY); - 使用
Content-Disposition: attachment,文件名取任务保存的outputName,Content-Type 取source_content_type; - 不返回任何 Windows/Linux 绝对路径、
file_storage_path、output_path。
4. 权限矩阵
| 操作 | 本人(学生) | 其他学生 | 企业 | 未登录 |
|---|---|---|---|---|
| 版本 增删改查 / 设默认 / 创建导出 | ✅ | 403 | 403 | 401 |
| 导出任务查询 / 下载 | ✅(task.userId=本人) | 403 | 403 | 401 |
安全说明:
- 身份一律来自 JWT(
auth.getName()= userId),不信任请求体/路径中的 userId; - 不能读取其他用户的简历(owner 校验);
- 不返回内部堆栈;错误响应不泄露数据库结构。
- 上传文件名拒绝路径分隔符、控制字符和响应头换行;下载历史文件时再次净化文件名。
- PDF 校验文件头和结尾标记;DOC 校验 OLE/Word 文档结构标记;DOCX 校验 OOXML 必需条目,并限制 ZIP 条目数、解压总量和压缩比,拒绝路径穿越及压缩炸弹。
- 文件替换采用“新文件落盘 → 数据库更新 → 事务提交后删除旧文件”;回滚时清理新文件。底层本地存储先写
.uploading临时文件,再原子移动到最终随机存储名,失败不暴露半文件。 - 上述措施属于格式与资源安全校验,不等同于病毒查杀或沙箱检测;生产部署仍应在存储接入层配置恶意文件扫描。
5. 异步导出设计
- 新增
AsyncConfig:@EnableAsync+ 专用线程池resumeExportExecutor(core 2 / max 4 / queue 100,可配app.async.resume-export.*)。 ResumeExportService.createExportTask:校验本人+版本 → 保存PENDING→ 通过TransactionSynchronizationManagerafterCommit 触发ResumeExportWorker.execute(taskId)(保证 worker 在任务行提交后才读得到)。ResumeExportWorker.execute:@Async + @Transactional。RUNNING→ 从任务快照读取源文件 → 复制到导出目录 →SUCCESS;任何异常 →FAILED+ 原因;finally 置finished_at。所有异常内部捕获,不产生未处理异步异常。不按 resumeVersionId 查当前版本文件。- 导出目录:
app.resume.export-dir(默认./resume-exports),与简历原始存储./resumes分离。
6. 并发控制
- 每用户最多一个默认:
UNIQUE(user_id, default_key)DB 硬约束 + Service 事务内"清旧设新"。 - 版本号不重复:
UNIQUE(user_id, version_no)DB 硬约束;ResumeVersionWriter用REQUIRES_NEW独立事务插入(唯一键冲突会标记事务 rollback-only,独立事务保证单次冲突只回滚自身,外层短退避后重试)。 - 并发测试:两版本并发设默认 → 最终恰一个默认;并发创建 → 版本号不重复。
7. 数据库迁移说明(V14)
文件:src/main/resources/db/migration/V14__resume_versions.sql(兼容 MySQL 8 / H2 MODE=MySQL)。
resume_versions:如上,含 FK(user)、uk_resume_versions_user_no、uk_resume_versions_default、索引(user_id, created_at)、(status)。resume_export_tasks:FK(user)、FK(version) ON DELETE SET NULL(删除版本不破坏任务历史)、索引(user_id, created_at)、(status)、(resume_version_id)。
8. Apifox 测试步骤
前置:启动基础设施 → 启动应用 → 导入 docs/openapi-rc.json(或 GET /api/v1/v3/api-docs)。
- 登录学生账号(Mock 验证码:日志不再输出,本地按
docs/frontend-integration.md§3 的受控方式取得;生产禁止查表)。 POST /profile/me/resume-versions(multipart:file+name=秋招版)→ 200,versionNo=1。- 再次上传 →
versionNo=2;设置name=社招版。 GET /profile/me/resume-versions→ 列表含两个版本。POST /profile/me/resume-versions/{id}/default→default=true;再次设置另一个 → 默认切换。DELETE默认版本 → 409;先设其他默认后删除 → 200。POST /profile/me/resume-versions/{id}/export→ 200PENDING;GET /resume-export-tasks/{taskId}轮询至SUCCESS;GET /resume-export-tasks/{taskId}/download下载导出文件。- 越权:另一学生 token 访问详情/导出任务/下载 → 403;无 token → 401;未完成任务下载 → 409。
9. 已知限制
- 导出产物为原文件副本:不解析/渲染 PDF/DOC 内容(无渲染依赖),
outputName为原文件名。 - 版本号冲突重试有上限:极端并发(3 次重试仍冲突)返回 400,客户端可重试;DB 唯一约束保证不会产生重复版本号。
version_no不回收:删除版本不回收编号(不影响功能)。- 默认简历仅作用于版本体系:不联动旧
student_profiles.resume_url(按确认保持独立)。 - 被导出任务引用的旧文件可能延迟清理:替换版本文件时,若旧文件正被 PENDING/RUNNING 导出任务引用则保留(保导出一致性);任务结束后这些文件不会自动回收,需后续清理策略。
- OpenAPI 快照:
docs/openapi-rc.json由 H2 集成测试上下文导出(49 路径,server URL 为测试默认值),接入现网后可用GET /api/v1/v3/api-docs重新导出。 - 不包含恶意软件扫描:当前只执行文件格式、OOXML 结构、路径和资源消耗防护;不对宏、嵌入对象或恶意载荷作杀毒结论。
10. 错误码(新增 82xx)
| 错误码 | 枚举 | HTTP | 含义 |
|---|---|---|---|
| 8201 | RESUME_VERSION_NOT_FOUND |
404 | 简历版本不存在/已删除 |
| 8202 | DEFAULT_RESUME_DELETE_NOT_ALLOWED |
409 | 默认简历删除前需先设其他默认 |
| 8203 | RESUME_EXPORT_TASK_NOT_FOUND |
404 | 导出任务不存在 |
| 8204 | RESUME_EXPORT_IN_PROGRESS |
409 | 导出任务进行中,不可删除版本 |
| 8205 | DUPLICATE_DEFAULT_RESUME |
409 | 并发设置默认冲突 |
| 8206 | RESUME_EXPORT_NOT_READY |
409 | 导出任务未完成,不可下载 |
S03-B:企业下载候选人简历
GET /company/candidates/{applicationId}/resume
新增的唯一企业侧简历读取入口。返回文件流,不返回 resumeUrl、存储相对/绝对路径、
学生用户 ID、手机号或邮箱。
- 认证:
Authorization: Bearer <COMPANY 用户 Access Token> - 路径参数:
applicationId(本企业岗位下的投递) - 响应:二进制文件流;
Content-Type只取自上传时已校验的白名单 (application/pdf、application/msword、DOCX),否则回退application/octet-stream - 响应头:
Content-Disposition: attachment; filename*=UTF-8''<已净化文件名>, 以及Cache-Control: no-store(私有简历不进任何中间缓存) - 文件名继续经过
ResumeFilenameSanitizer,拒绝路径分隔符、控制字符与响应头换行
错误语义
| 场景 | 结果 |
|---|---|
| 未登录 / 无有效会话 | 401 |
| 非 COMPANY 角色(STUDENT / ADMIN) | 403 |
| 当前用户没有企业资料 | 403 |
| 投递不存在 | 404 APPLICATION_NOT_FOUND |
| 跨企业投递 | 403 |
| 隐私设置不允许(含 PRIVATE / 无设置) | 403 FORBIDDEN |
| 授权通过但学生没有简历 | 404 PROFILE_NOT_FOUND |
授权与副作用
- 授权顺序:当前 COMPANY 用户 → 当前 Company → Application+Job →
Job.companyId→ Student → privacy setting → profile/file。归属信息全部由服务端推导, 不接受客户端传入的companyId/studentUserId。 - 拒绝路径不会调用存储读取(测试以计数包装器验证为零次)。
- 该接口只读:不修改
student_profiles、resume_versions、投递、评分、排名、推荐、邀约、 消息、通知或风险表;不发送 RabbitMQ、短信或邮件。 - 学生本人的上传、下载、版本与导出接口语义完全不变。
- 企业候选人列表/详情 DTO 仍不含
resumeUrl。 - 不新增按
studentUserId或candidateKey下载简历的入口;人才发现本轮不得直接下载简历。
工作经历优化模拟接口(AI-001,2026-10-22)
新增 POST /api/v1/profile/me/work-experience-optimization,仅学生本人登录可调用,默认关闭。设置 ai.work-experience.stub-enabled=true 后用于合成数据联调,返回 simulated=true;不调用外部 AI、不保存工作经历。真实 AI 优化仍未实现。详见工作区根目录 docs/AI-001-工作经历AI优化-前端对接方案.md。
AI-001 v0.6开发真实模式
mode配置off/stub/deepseek,默认off,兼容旧stub-enabled。deepseek仅dev且Key完整时启动,返回simulated=false、promptVersion=work-experience-deepseek-v1;stub仍true。新增模型安全错误400/502/503/504,不保存内容;仅隔离开发合成数据联调,未生产验收。