# 简历版本管理与导出模块接口说明 > 职来 (mydreamjob) · 简历版本管理与导出基础模块 > 基础路径:`/api/v1` · 除明确标注外全部接口需 `Authorization: Bearer ` ## 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` 等内部字段: ```json { "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_*` 等内部字段: ```json { "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 校验); - 不返回内部堆栈;错误响应不泄露数据库结构。 ## 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 验证码,见日志)。 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` 重新导出。 ## 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 | 导出任务未完成,不可下载 |