# 账号注销(Account Deletion)API 联调说明 > 模块:账号注销。当前登录用户可对自己的账号发起注销申请、查询状态、在冷静期内取消注销。 > 适用于 **STUDENT / COMPANY / ADMIN** 所有登录角色;未登录一律返回 `HTTP 401`。 ## 1. 接口概览 | 方法 | 路径 | 角色 | 说明 | |------|------|------|------| | POST | `/profile/me/deletion` | 所有登录角色 | 申请注销当前账号(`NONE → PENDING`;PENDING 重复提交幂等返回 200;COMPLETED 再申请返回 409/9301) | | GET | `/profile/me/deletion` | 所有登录角色 | 查询当前用户注销状态;无记录返回默认对象 `NONE`,不写库 | | POST | `/profile/me/deletion/cancel` | 所有登录角色 | 仅在 PENDING 且未超过 `effectiveAt` 时可取消(`PENDING → NONE`) | 身份一律从 JWT / SecurityContext 获取,**不接受** `userId`、`candidateId` 等身份参数。 ## 2. 注销状态机 状态仅有三种: ```text NONE —— 正常态(未注销) PENDING —— 冷静期(可取消;业务写操作被限制) COMPLETED —— 终态(账号已脱敏,登录被阻止,不可恢复) ``` 转换规则: ```text NONE ----发起注销----> PENDING PENDING ----冷静期内取消-> NONE PENDING ----30天到期------> COMPLETED COMPLETED(终态,不可恢复) ``` ## 3. 冷静期 - 默认 **30 天**,可通过 `app.deletion.cooling-days`(环境变量 `DELETION_COOLING_DAYS`)配置。 - 发起注销时 `effectiveAt = requestedAt + 冷静期`。 - 冷静期内允许登录、允许 GET 查询,但**业务写操作被拦截**。 - 超过 `effectiveAt` 后,PENDING 会转换为 COMPLETED;此后不可取消。 ## 4. 默认 NONE 行为 `GET /profile/me/deletion` 在用户**从未发起注销**时返回默认对象,且**不会自动向数据库插入记录**: ```json { "code": 200, "message": "ok", "data": { "userId": 2, "status": "NONE", "version": 0 }, "timestamp": "2026-08-25T10:00:00Z" } ``` `requestedAt / effectiveAt / cancelledAt / completedAt` 仅在对应状态下返回(无值不输出)。 ## 5. 请求与响应示例 ### 5.1 发起注销 ```http POST /api/v1/profile/me/deletion Authorization: Bearer ``` ```json { "code": 200, "message": "ok", "data": { "userId": 2, "status": "PENDING", "requestedAt": "2026-08-25T10:00:00", "effectiveAt": "2026-09-24T10:00:00", "version": 0 }, "timestamp": "2026-08-25T10:00:00Z" } ``` PENDING 状态下重复提交:返回 200 当前状态,**不重复写入**`ACCOUNT_DELETION_INITIATED` 审计。 ### 5.2 查询状态 ```http GET /api/v1/profile/me/deletion Authorization: Bearer ``` 响应结构同默认对象;已在 PENDING 时返回上述 PENDING 结构。 ### 5.3 取消注销(冷静期内) ```http POST /api/v1/profile/me/deletion/cancel Authorization: Bearer ``` ```json { "code": 200, "message": "ok", "data": { "userId": 2, "status": "NONE", "cancelledAt": "2026-08-26T09:00:00", "version": 1 }, "timestamp": "2026-08-26T09:00:00Z" } ``` 非 PENDING(如 NONE)重复取消返回当前状态,保持幂等,不重复写审计。 ## 6. PENDING 写操作限制 当当前用户状态为 **PENDING** 时,`DeletionGuardInterceptor` 会拦截所有业务**写操作** (`POST / PUT / PATCH / DELETE`),返回: ```http HTTP/1.1 409 Conflict Content-Type: application/json { "code": 9302, "message": "account deletion pending, business write is not allowed" } ``` - `GET`、`OPTIONS` 默认放行。 - 白名单放行: - `POST /profile/me/deletion` - `GET /profile/me/deletion` - `POST /profile/me/deletion/cancel` - `OPTIONS /**` 以下写接口会被拦截(示例,不止于此):`PUT /profile/me`、`PUT /profile/me/preferences`、 `POST /applications`、`POST /offers`、`POST /conversations/messages`、`PUT /profile/me/privacy`。 未注销用户、未认证请求、GET 请求**不会被误拦截**。 ## 7. COMPLETED 脱敏规则与旧 JWT 处理 ### 7.1 触发完成 - **定时任务**:每日扫描 `PENDING 且 effective_at <= 当前时间` 的记录,转为 COMPLETED 并脱敏。 - **懒转换**:登录、已认证请求、注销查询或写操作时,若发现 PENDING 已过期,先转换为 COMPLETED 再按 COMPLETED 规则处理。 转换与脱敏**事务化、幂等、并发安全**(`@Version` 乐观锁 + `REQUIRES_NEW` 独立事务)。 ### 7.2 脱敏规则(只脱敏 `users` 表 PII,不删除任何业务数据) | 字段 | 处理 | |------|------| | `phone` | 改为 `deleted_{userId}`,保证唯一 | | `email` | 置空 | | `nickname` | 置为 `deleted` | | `avatarUrl` | 置空 | | `passwordHash` | 置空 | | `is_active` | `false` | ### 7.3 旧 JWT COMPLETED 状态属于**终态**,即使旧 JWT 尚未过期也会被拒绝: - 登录时校验到 COMPLETED → 阻止登录(`ACCOUNT_DELETED`)。 - 普通业务请求(即使带有效 JWT)→ `409 / 9303`。 - 注销接口专用于查询终态或返回正确错误: - `GET /profile/me/deletion` → 可查看 `COMPLETED`; - `POST /profile/me/deletion` → `409 / 9301`; - `POST /profile/me/deletion/cancel` → `409 / 9301`(或 9305)。 未注销用户的 JWT 登录流程不受影响。 ### 7.4 懒转换首次查询即返回 COMPLETED 懒转换(到期 PENDING → COMPLETED)在独立事务(`REQUIRES_NEW`)中完成并提交。触发懒转换后, `GET /profile/me/deletion` 会在**独立只读事务(新快照)**中重读最新记录,因此**第一次 GET 就返回 `status=COMPLETED` 与 `completedAt`**,不会返回过期的 `PENDING`。随后任何 GET 均保持一致状态, 不会出现"第一次 PENDING、第二次 COMPLETED"的不一致。 ### 7.5 原手机号禁止重新注册(墓碑) - 用户完成注销(COMPLETED)时,在将 `users.phone` 改为 `deleted_{userId}` **之前**, 使用服务端密钥(配置:`app.deletion.phone-hash-secret` / 环境变量 `DELETION_PHONE_HASH_SECRET`) 对原手机号做 **HMAC-SHA256**,仅把摘要写入 `account_deletions.deleted_phone_hash`(唯一索引)。 - **不**保存原手机号明文到注销表,**不**写入审计日志。 - 登录/注册流程对输入手机号计算同样 HMAC 并查询 `deleted_phone_hash`:命中即拒绝 —— `409 / 9303 ACCOUNT_DELETED`,**不自动创建新账号**。 - 已注销账号仍以 `deleted_` 作为 `users.phone`(原脱敏规则不变)。 - 既不允许通过原手机号恢复原账号,也不允许通过原手机号重新注册新账号。 - 数据库变更由 **V29** 迁移完成(仅新增 `deleted_phone_hash` 字段,不改动 V1-V28 及状态机)。 ## 8. 不删除业务数据 脱敏仅修改 `users` 表。以下业务数据一律**保留**: `applications`、`jobs`、`offers`、`interviews`、`messages`、`notifications`、`reviews`、 `candidate_dimension_scores`、`audit_logs` 及其他业务数据。不物理删除任何业务数据。 ## 9. 错误的角色权限矩阵 | 状态/角色 | STUDENT | COMPANY | ADMIN | 未登录 | |-----------|:-------:|:-------:|:-----:|:------:| | 申请注销(POST deletion) | 200 / 409(9301) | 200 / 409(9301) | 200 / 409(9301/9306) | 401 | | 查询状态(GET deletion) | 200 | 200 | 200 | 401 | | 取消注销(POST cancel) | 200 / 409(9305) | 200 / 409(9305) | 200 / 409(9305) | 401 | - **ADMIN**:若为系统中**最后一个有效 ADMIN**(`role=ADMIN 且 is_active=true` 计数 ≤ 1),不允许注销,返回 `409 / 9306 LAST_ADMIN_CANNOT_DELETE`;存在多个有效 ADMIN 时允许其中一个注销。 ## 10. 错误码(9301-9306) | 业务码 | HTTP | 枚举 | 说明 | |--------|------|------|------| | 9301 | 409 | `INVALID_DELETION_STATE` | 状态不允许该操作(如 COMPLETED 再申请 / COMPLETED 取消恢复) | | 9302 | 409 | `ACCOUNT_DELETION_PENDING` | 冷静期内业务写操作被拦截 | | 9303 | 409 | `ACCOUNT_DELETED` | 账号已注销(COMPLETED),业务请求被拒 | | 9304 | 409 | `ACCOUNT_DELETION_CONCURRENT_UPDATE` | 乐观锁并发冲突 | | 9305 | 409 | `ACCOUNT_DELETION_CANCEL_DEADLINE_PASSED` | 已超过冷静期,无法取消 | | 9306 | 409 | `LAST_ADMIN_CANNOT_DELETE` | 最后一个有效 ADMIN 不能注销 | ## 11. 审计日志 写入以下 action: - `ACCOUNT_DELETION_INITIATED` - `ACCOUNT_DELETION_CANCELLED` - `ACCOUNT_DELETION_COMPLETED` - `ACCOUNT_DATA_DESENSITIZED` 细节(details)仅记录:`userId`、状态变化、时间、操作类型。**禁止**写入 `phone / email / password / passwordHash / token / jwt / resumeUrl / 验证码`。 同一次完成流程不会重复写完成审计与脱敏审计。 ## 12. 并发与幂等 - `user_id` 唯一约束保证同一用户仅一条注销记录。 - POST deletion 在 PENDING 时幂等返回 200。 - cancel 在 NONE/COMPLETED 时幂等返回当前状态或明确错误。 - 使用 `@Version` 乐观锁;并发状态迁移只有一个事务成功,后到者捕获 `OptimisticLockingFailureException` 映射为 `9304`。