# 薪资历史能力规格 ## Purpose 保存求职者每次成功薪资估算的可追溯快照,并提供仅限本人访问的分页查询、详情和物理删除能力。 ## ADDED Requirements ### Requirement: 成功计算自动保存历史 The system SHALL 在计算并保存接口成功完成时自动创建一条历史记录,并在响应中返回历史 ID 和创建时间。系统 MUST NOT 要求调用方再调用第二个保存接口。 #### Scenario: 首次成功计算 - **WHEN** STUDENT 提交合法且政策版本一致的计算请求 - **THEN** 系统在同一事务中完成计算并保存历史,返回结果及新历史 ID ### Requirement: 历史保存完整且可重建的快照 The system SHALL 在历史记录中保存原始输入、城市编码与名称、`dataLevel`、城市默认参数、用户覆盖项、最终生效参数、完整结果、`monthIndex`、城市政策版本、政策生效日期、全国税率版本和创建时间。三类 JSON 快照 MUST 能独立重建当时结果。 #### Scenario: 城市参数后来变化 - **WHEN** 历史创建后城市默认参数或政策版本发生变化 - **THEN** 历史详情仍返回创建时的输入、政策、覆盖项和完整结果,不重新读取当前城市配置,包括当时的 `CITY` 或 `PROVINCE_FALLBACK` 精度标识 ### Requirement: 计算保存使用幂等键防重 The system SHALL 支持可选 `idempotencyKey`,最长 64 字符,空白值按未提供处理。同一用户相同幂等键且规范化业务字段相同 MUST 返回原历史;相同幂等键但规范化字段不同 MUST 返回 409/9804。 #### Scenario: 相同请求网络重试 - **WHEN** 客户端使用相同用户、相同幂等键和相同规范化业务字段重试计算 - **THEN** 系统返回同一条历史和相同计算结果,不新增数据库行 #### Scenario: 相同幂等键不同请求体 - **WHEN** 客户端复用幂等键但修改税前月薪或政策版本 - **THEN** 系统返回 409/9804,且不创建第二条历史 ### Requirement: 同一幂等键并发只能生成一条历史 The system SHALL 使用事务和数据库唯一约束处理并发。相同用户相同幂等键并发请求 MUST 最多生成一条历史;同键不同请求并发时,一方成功,另一方 MUST 返回 409/9804。 #### Scenario: 并发相同请求 - **WHEN** 两个请求同时提交相同用户、幂等键和规范化业务字段 - **THEN** 数据库中最多存在一条历史,两个成功响应指向同一历史 ID ### Requirement: 历史列表和详情仅返回本人数据 The system SHALL 提供按创建时间倒序、ID 倒序的分页列表和详情接口。列表 MUST 只返回摘要;详情 MUST 返回完整快照。查询必须同时按当前登录用户和历史 ID 过滤。 #### Scenario: 查询本人历史 - **WHEN** STUDENT 调用历史列表或详情接口 - **THEN** 系统只返回该 STUDENT 的历史,按最新创建时间排序 #### Scenario: 猜测他人历史 ID - **WHEN** STUDENT 请求不属于自己的历史 ID - **THEN** 系统返回 404/9803,不泄露该记录是否存在 ### Requirement: 历史不可编辑且可以物理删除单条 The system SHALL 不提供历史更新接口。历史创建后不可修改;用户删除本人历史时 MUST 物理删除该行及其幂等键,后续可以为该幂等键创建新历史。 #### Scenario: 删除本人历史 - **WHEN** STUDENT 删除自己的历史记录 - **THEN** 系统物理删除该记录并返回成功响应,后续详情查询返回 404/9803 #### Scenario: 重复删除历史 - **WHEN** STUDENT 再次删除已经删除的历史 - **THEN** 系统返回 404/9803,不返回成功状态 ### Requirement: 历史列表采用统一分页且不提供额外筛选 The system SHALL 使用 `page=0`、`size=20` 的分页默认值,`size` 最大为 100;首期 MUST NOT 增加城市、日期范围或薪资范围筛选。 #### Scenario: 超出分页上限 - **WHEN** 客户端请求 `size=101` - **THEN** 系统返回 400/9802,不返回历史列表 ### Requirement: 历史数据按用户长期保留 The system SHALL 允许历史记录长期保留直至用户主动删除,不设置自动过期、定时清理或业务数量配额。城市停用、政策更新或历史数量增长 MUST NOT 影响历史详情读取。 #### Scenario: 城市停用后查看旧历史 - **WHEN** 历史所属城市后来被停用 - **THEN** STUDENT 仍可查看该历史详情 ### Requirement: 历史不进入审计日志或个人数据导出 The system SHALL 不因薪资计算或单条历史删除写入业务审计日志,也不得把税前工资、基数、税额、到手工资或 JSON 快照写入应用日志。薪资历史 MUST NOT 纳入现有个人数据导出 ZIP。 #### Scenario: 计算成功后的日志检查 - **WHEN** 系统完成一次薪资计算和历史保存 - **THEN** 日志仅包含请求 ID、用户 ID、历史 ID、城市编码、状态和耗时,不包含金额或参数快照 ### Requirement: 账号注销清理薪资历史 The system SHALL 在账号注销完成事务中物理删除该用户全部薪资历史。注销处于 `PENDING` 时 MUST 禁止计算和删除写入,`COMPLETED` 时所有薪资接口 MUST 返回 409/9303。 #### Scenario: 注销完成后清理历史 - **WHEN** 用户账号注销完成 - **THEN** 数据库中不存在该用户的 `salary_calculation_history` 行 #### Scenario: 注销冷静期尝试计算 - **WHEN** 用户处于 `PENDING` 状态调用计算接口 - **THEN** 现有注销守卫返回 409/9302,且不创建历史 ### Requirement: 数据库迁移保证城市政策和历史完整性 The system SHALL 通过 `V40` 创建 `salary_city_policy` 与 `salary_calculation_history`,通过 `V41` 写入目标城市种子。城市编码 MUST 唯一,历史幂等键 MUST 按用户唯一,历史快照 MUST 保留且不依赖后续城市更新。 #### Scenario: 迁移初始化 - **WHEN** 在 MySQL 或 H2 MySQL 模式执行 `V40` 和 `V41` - **THEN** 城市政策表、历史表、主外键、唯一键和索引创建成功,目标城市种子导入成功