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.
 
 
 

9.7 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 常量。计算公式按以下顺序执行:
    1. 根据城市政策截断或采用用户覆盖基数。
    2. 计算个人养老、医疗、失业、公积金和大病固定费。
    3. 按 monthIndex 推导累计收入、累计个人缴费、累计专项附加扣除和累计基本减除费用。
    4. 计算累计应纳税所得额、按全国税率表计算累计应纳税额和当月税额。
    5. 计算到手工资并按两位小数输出。
  • Rationale:全国税制不应复制到每个城市;高精度十进制才能避免金额和累计税款的舍入漂移。
  • Alternatives:由数据库 SQL 计算;用 double 计算。
    • SQL 会混合迁移和算法版本;double 不适用于金额和累计税额。
  • 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 写入按官方公开资料整理的目标城市政策;采集截止日为实际开始整理数据的日期。种子发布前执行目标行政区全覆盖、编码唯一、字段非空、上下限正确、比例合法、生效日期不晚于截止日、版本号非空测试。
  • Rationale:数据量和维护频率较小,迁移是可审查、可版本化的交付单元;全量覆盖是产品可使用的必要条件。
  • Alternatives:运行时调用第三方接口;只在测试环境维护示例城市。
    • 第三方接口存在授权、稳定性和网络依赖;示例数据无法满足全国估算目标。
  • Consequences:政策更新必须新增迁移,不能静默覆盖历史种子;来源字段可选,但数据整理仍需以官方公开资料为依据。

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。