You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

12 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_at
  • UNIQUE(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 / FAILED
  • output_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 → 通过 TransactionSynchronizationManager afterCommit 触发 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)。

  1. 登录学生账号(Mock 验证码:日志不再输出,本地按 docs/frontend-integration.md §3 的受控方式取得;生产禁止查表)。
  2. POST /profile/me/resume-versions(multipart:file + name=秋招版)→ 200,versionNo=1。
  3. 再次上传 → versionNo=2;设置 name=社招版。
  4. GET /profile/me/resume-versions → 列表含两个版本。
  5. POST /profile/me/resume-versions/{id}/default → default=true;再次设置另一个 → 默认切换。
  6. DELETE 默认版本 → 409;先设其他默认后删除 → 200。
  7. POST /profile/me/resume-versions/{id}/export → 200 PENDING;GET /resume-export-tasks/{taskId} 轮询至 SUCCESS;GET /resume-export-tasks/{taskId}/download 下载导出文件。
  8. 越权:另一学生 token 访问详情/导出任务/下载 → 403;无 token → 401;未完成任务下载 → 409。

9. 已知限制

  1. 导出产物为原文件副本:不解析/渲染 PDF/DOC 内容(无渲染依赖),outputName 为原文件名。
  2. 版本号冲突重试有上限:极端并发(3 次重试仍冲突)返回 400,客户端可重试;DB 唯一约束保证不会产生重复版本号。
  3. version_no 不回收:删除版本不回收编号(不影响功能)。
  4. 默认简历仅作用于版本体系:不联动旧 student_profiles.resume_url(按确认保持独立)。
  5. 被导出任务引用的旧文件可能延迟清理:替换版本文件时,若旧文件正被 PENDING/RUNNING 导出任务引用则保留(保导出一致性);任务结束后这些文件不会自动回收,需后续清理策略。
  6. OpenAPI 快照:docs/openapi-rc.json 由 H2 集成测试上下文导出(49 路径,server URL 为测试默认值),接入现网后可用 GET /api/v1/v3/api-docs 重新导出。
  7. 不包含恶意软件扫描:当前只执行文件格式、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 导出任务未完成,不可下载