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.
10 KiB
10 KiB
n05-salary-calc 技术设计
Facts and Constraints
- 当前项目为 Spring Boot 3.5.16、Java 17、Spring Data JPA、Flyway、MySQL 8.4,测试使用 H2
MODE=MySQL。 - 现有 Flyway 迁移最高为
V39,本次只能新增V40、V41,不得修改已发布迁移。 - JWT 认证过滤器把用户 ID 设置为 Authentication principal;现有业务模块通过
UserRepository查询用户并检查User.getRoles()是否包含STUDENT。 - 现有统一响应为
ApiResponse,分页响应为PageResponse,分页默认值为page=0、size=20。 - 现有账号注销守卫在
PENDING时阻止 POST/PUT/PATCH/DELETE,在COMPLETED时阻止全部业务请求;注销完成事务会脱敏用户 PII 并保留业务数据。 - 路线图当前将薪资计算器列为“未开始”,仓库中没有现成城市政策数据表或薪资模块代码。
- 城市政策必须按中国大陆地级行政区、直辖市和省直辖县级市配置,不拆分到区县;目标城市全量覆盖是发布门槛。
- 用户确认不提供 ADMIN 城市政策维护接口,城市政策通过迁移发布。
- 薪资历史不进入个人数据导出,不写业务审计日志,账号注销时物理删除。
Goals and Non-Goals
Goals
- 提供可验证的六个
/salary-calculator/**接口,并只允许当前具有STUDENT角色的登录用户使用。 - 通过
cityCode加载当前城市默认参数,执行可覆盖、可追溯的到手工资估算。 - 保存每次成功计算的可独立读取的历史快照,支持分页查询、详情和物理删除。
- 通过政策版本和幂等键分别解决“计算时政策已变化”和“请求重复提交”问题。
- 让城市政策、计算结果、权限和历史生命周期在测试中具有明确、可复现的边界。
Non-Goals
- 不设计前端页面、交互状态或客户端缓存策略。
- 不计算企业成本、年度汇算、奖金、股权、多收入来源或非居民税制。
- 不提供城市政策管理后台、自动抓取或第三方实时政策 API。
- 不把薪资历史纳入个人数据导出,也不保留用于业务审计的历史副本。
- 不支持港澳台地区或不适用内地职工社保、公积金政策的地区。
Decisions
Decision: 城市政策采用“当前有效行 + 历史快照”,不建立政策版本链
- Choice:
salary_city_policy每个六位city_code保留一条当前记录,保存policy_version、policy_effective_date、参数和enabled;历史计算把实际使用的参数复制到自身 JSON 快照。 - Rationale:每城市只有一套当前默认参数,约数百条记录足以维护;历史结果不应因为后续政策更新或停用而改变。
- Alternatives:建立
city_policy_versions表并按生效期查询;前端携带完整默认参数。- 版本链会引入查询时间、发布调度和历史外键复杂度;前端传参加重客户端信任边界。
- Consequences:城市政策变更必须同步提升
policy_version;历史表不需要关联具体版本行,但必须保存完整政策快照。
Decision: 使用两张核心表,历史核心字段列式存储、快照 JSON 化
- Choice:新增
salary_city_policy和salary_calculation_history。历史表保存用户 ID、城市编码/名称、税前月薪、到手工资、月份、幂等键、请求指纹、创建时间等核心列,以及输入、政策、结果三组 JSON 快照。 - Rationale:核心列用于权限隔离、分页、排序和摘要展示;JSON 快照避免为每个未来字段新增大量列,同时保留历史可重建性。
- Alternatives:把所有历史和计算结果规范化为多张明细表;只存一个最终结果 JSON。
- 多表模型增加查询和迁移成本;只存结果 JSON 无法稳定做列表排序、权限过滤或金额摘要。
- Consequences:需要定义稳定的 JSON 结构和最大长度;H2/MySQL 集成测试必须验证 JSON 字段读写和唯一约束。
Decision: 计算引擎在 Java 中使用 BigDecimal,全国税制与地方政策分层
- Choice:城市表只存地方社保、公积金和大病参数;全国统一个税税率表、速算扣除数、5,000 元基本减除费用和税率版本作为 Java 常量。计算公式按以下顺序执行:
- 根据城市政策截断或采用用户覆盖基数。
- 计算个人养老、医疗、失业、公积金和大病固定费。
- 按
monthIndex推导累计收入、累计个人缴费、累计专项附加扣除和累计基本减除费用。 - 计算累计应纳税所得额、按全国税率表计算累计应纳税额和当月税额。
- 计算到手工资并按两位小数输出。
- Rationale:全国税制不应复制到每个城市;高精度十进制才能避免金额和累计税款的舍入漂移。
- Alternatives:由数据库 SQL 计算;用
double计算。- SQL 会混合迁移和算法版本;
double不适用于金额和累计税额。
- SQL 会混合迁移和算法版本;
- Consequences:算法和税率版本进入独立单元测试;历史记录保存累计税费和
taxRulesVersion,以便变更税率时有审计轨迹。
Decision: 计算请求使用 expectedPolicyVersion 做乐观版本校验
- Choice:计算请求必须携带前端已加载的
expectedPolicyVersion。后端读取当前城市配置后先比较版本;不一致时返回409/9805,响应只附最新policyVersion和policyEffectiveDate,不生成历史。 - Rationale:城市详情和计算请求之间可能发生政策更新;静默使用新参数会让前端展示、用户覆盖和历史解释互相矛盾。
- Alternatives:后端始终使用最新配置;客户端把完整默认参数回传。
- 静默使用最新版本会产生不可解释的历史;回传完整参数会扩大请求和信任边界。
- Consequences:政策维护流程必须保证任何参数变更都提升版本号;前端需要在版本冲突时重新加载城市详情并重新提交。
Decision: 幂等键和规范化请求指纹在同一事务中处理
- Choice:
idempotencyKey可选、最长 64 字符、空白视为未提供。请求先归一化可选字段、默认值和覆盖项,再计算业务字段指纹;同一用户同键且指纹相同返回原历史,指纹不同返回409/9804。 - Rationale:原始 JSON 的字段顺序、空白和显式 null 不能代表业务请求是否相同;事务和数据库唯一约束必须作为并发最终防线。
- Alternatives:直接比较原始请求体;不提供幂等控制。
- 原始请求比较误判同义请求;不提供幂等会在网络重试或双击时生成重复历史。
- Consequences:历史表需要
(student_user_id, idempotency_key)唯一约束和请求指纹列;删除历史同时释放对应键。
Decision: 历史接口只暴露摘要和本人快照,删除采用物理删除
- Choice:列表返回摘要,详情返回完整快照;不提供更新接口。删除本人历史时物理删除记录和幂等键;账号注销完成事务调用薪资历史清理服务。访问他人历史和不存在历史统一返回
404/9803。 - Rationale:历史是用户个人试算数据,不应允许覆盖或通过 ID 枚举发现;物理删除符合用户主动清理和账号注销预期。
- Alternatives:软删除、保留历史用于审计、提供备注和编辑。
- 软删除仍保留可关联个人数据;编辑或备注会破坏不可变快照语义;审计保留没有明确业务需求。
- Consequences:需要在注销完成事务中增加清理调用和对应集成测试;历史记录不可作为审计或财务凭证。
Decision: 城市政策通过 V41 种子交付,并采用城市级与省级回退的分层精度
- Choice:
V40建表,V41为全部目标城市写入可计算记录,并增加data_level字段:CITY:已核验到城市级公开参数的记录,使用城市自己的上下限和比例。PROVINCE_FALLBACK:无法核验城市级参数的记录,复制所属省级公开参数,并在城市列表、城市详情、计算响应和历史快照中明确标记。
- Rationale:公开资料没有覆盖全部地级行政区的统一城市级结构化数据集;分层精度可以在保持全国目标城市都可计算的同时,避免把省级近似值伪装成城市级官方值。
- Alternatives:等待人工逐城市补齐 300+ 条政策;用省级默认值但不加标识;只发布已核验城市。
- 逐城市核验会无限期阻塞首期;无标识回退会误导用户;只发布部分城市不满足全国可计算目标。
- Consequences:所有消费方必须展示
dataLevel;PROVINCE_FALLBACK结果的解释必须明确说明精度较低。后续核验到城市级参数时,通过新迁移更新该城市记录、提升policyVersion并更新dataLevel。
Risks and Verification
- 城市政策数据错误或缺失 → 采用采集截止日基线、目标行政区清单校验、字段范围测试和代表性城市人工抽查 →
SalaryCityPolicySeedTest、迁移集成测试和 MySQL/Docker 黑盒验收。 - 累计税额或 rounding 与工资单不一致 → 使用高精度十进制、固定税率版本和明确假设 → 覆盖税率档边界、基数上下限、零缴费、
monthIndex=1/12的精确金额测试。 - 政策更新与计算竞态 → 强制
expectedPolicyVersion校验,冲突不生成历史 →SalaryPolicyVersionIntegrationTest。 - 重复提交产生重复历史 → 规范化指纹、事务和用户+幂等键唯一约束 →
SalaryIdempotencyIntegrationTest。 - 跨用户读取或删除历史 → 查询和删除同时限定当前用户,统一隐藏资源存在性 →
SalaryHistoryPrivacyTest。 - 账号注销后仍保留薪资历史 → 在注销完成事务中执行物理清理 →
SalaryAccountDeletionIntegrationTest。 - 历史 JSON 在 MySQL 与 H2 行为不一致 → 两套集成测试和真实 MySQL 黑盒验收 →
mvn -Dtest=Salary*Test test、mvn clean verify、python docs/verify_openapi.py。