diff --git a/changes/n05-salary-calc/.spec-superflow.yaml b/changes/n05-salary-calc/.spec-superflow.yaml index ab04fe3..9ee1d63 100644 --- a/changes/n05-salary-calc/.spec-superflow.yaml +++ b/changes/n05-salary-calc/.spec-superflow.yaml @@ -2,28 +2,28 @@ # Derived data. Always rebuildable from artifacts. Lost/corrupt → fall back to content-level detection. # === Core state === -state: approved-for-build +state: closing workflow: full revision: 1 # === Hashes (fast staleness detection) === -artifacts_hash: sha256:c7e046b12a81b409ff08716e34062a9a4312a0af4cc20bdbd0442a7bea9e2b1e +artifacts_hash: sha256:5e15964e215b2645ea2679d15626cf2cb04fba15b8285049fc4ba8104d7ad330 contract_hash: sha256:c85abc5d5ddd79b03da8ff42816436f4389bb99a5739d4fcb2a960c3c81867b1 # === Execution progress === execution_mode: sdd -execution_plan_hash: sha256:77341d6e000ef1624d59d66b8b361c24e7ef8f1b5d065dd4d648a2a7537e72d5 +execution_plan_hash: sha256:b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c execution_plan_revision: 1 batches_completed: 4 -test_result: null -spec_merged: false -spec_publication_receipt: null +test_result: pass +spec_merged: true +spec_publication_receipt: eyJ2ZXJzaW9uIjoxLCJzb3VyY2VfaGFzaCI6InNoYTI1NjozOTkxMDk2Zjk4ZTU1MGJhNTZhMmQ4YTY1YWZjZjFmNzUxNTc4OGNlZTM3YTYwMmVkYmZmOGNiNTcxZmYwNjM0IiwiYmFzZWxpbmVfYmVmb3JlX2hhc2giOiJzaGEyNTY6ZDM4YjQyMzcwODdhYTRkNTJkNWMyNTBkY2M4MmYwN2FjNDAyOGYzMWJmMDhiNDljMjczNDJhYTJkMGY2MjRhNiIsImJhc2VsaW5lX2FmdGVyX2hhc2giOiJzaGEyNTY6NzM4ZWNiYjFkYTRjNTM4MDIzMTFjZjM0ZWU1NzBlYjQ3NDU5OWNmMTRhZGEwZWRmMzY0MDE2NmU3M2IyNjFlZiIsImNhcGFiaWxpdGllcyI6WyJzYWxhcnktY2FsY3VsYXRpb24iLCJzYWxhcnktaGlzdG9yeSJdfQ # === Metadata === change_name: n05-salary-calc -last_transition: 2026-09-20T08:15:42.567Z -last_transition_from: bridging -last_transition_to: approved-for-build +last_transition: 2026-09-21T05:09:14.898Z +last_transition_from: executing +last_transition_to: closing # === Decision points === dp_0_decisions: 变更名称:n05-salary-calc;一句话意图:为已登录 STUDENT 提供按地级行政区配置的城市默认参数、用户覆盖、到手工资估算和历史快照能力。workflow_path=full;选择原因:新增模块、V40/V41 迁移、六个 API、权限、幂等与跨模块注销清理。范围:仅后端;不支持匿名、ADMIN、前端、个人数据导出、薪资参考或 Offer 关联。计算:税前月薪、个人五险一金简化模型、累计预扣;全国目标城市全量配置;历史物理删除,账号注销时清理。约束:城市级不拆区县;用户覆盖优先;政策版本校验;仅 STUDENT;日志不记录金额。优化:保持聚焦,不纳入前端、企业成本、年终奖、年度汇算、管理员维护和薪资大数据。沟通方式:四类规划工件一次生成,完成验证后统一进入 DP-2 审查。artifact_language=zh-CN @@ -36,13 +36,13 @@ dp_2_result: approved: layered CITY/PROVINCE_FALLBACK data strategy, updated spe dp_2_timestamp: 2026-09-20T08:15:41Z dp_3_result: approved: updated execution contract with layered city policy data and dataLevel propagation dp_3_timestamp: 2026-09-20T08:15:41Z -dp_4_result: sdd: plan revision 1; user-confirmed; 分层城市数据方案已批准;重新建立当前 W1-W4 执行计划 -dp_4_timestamp: 2026-09-20T08:16:35.054Z +dp_4_result: sdd: plan revision 1; user-confirmed; 任务勾选已完成,重建当前工件哈希并保留同一 W1-W4 执行范围 +dp_4_timestamp: 2026-09-20T09:13:10.032Z dp_5_result: null dp_5_timestamp: null dp_5_decisions: null dp_5_confirmed: null -dp_6_result: null -dp_6_timestamp: null -dp_7_result: null -dp_7_timestamp: null +dp_6_result: conditional: N05 salary calculator verified; pre-existing 2 failures/9 errors outside N05 conditionally accepted +dp_6_timestamp: 2026-09-21T05:09:12Z +dp_7_result: confirmed: N05 salary calculator archived; branch feat/n05-salary-calc ready for merge +dp_7_timestamp: 2026-09-21T05:09:12Z diff --git a/changes/n05-salary-calc/.superpowers/sdd/.stale-plans/execution-plan-rev1-before-task-checkboxes.json b/changes/n05-salary-calc/.superpowers/sdd/.stale-plans/execution-plan-rev1-before-task-checkboxes.json new file mode 100644 index 0000000..abf452b --- /dev/null +++ b/changes/n05-salary-calc/.superpowers/sdd/.stale-plans/execution-plan-rev1-before-task-checkboxes.json @@ -0,0 +1,155 @@ +{ + "mode": "sdd", + "source": "user-confirmed", + "rationale": "分层城市数据方案已批准;重新建立当前 W1-W4 执行计划", + "waves": [ + { + "id": "W1", + "strategy": "serial", + "tasks": [ + "1", + "2" + ], + "depends_on": [] + }, + { + "id": "W2", + "strategy": "serial", + "tasks": [ + "3", + "4" + ], + "depends_on": [ + "W1" + ] + }, + { + "id": "W3", + "strategy": "serial", + "tasks": [ + "5", + "6", + "7" + ], + "depends_on": [ + "W2" + ] + }, + { + "id": "W4", + "strategy": "serial", + "tasks": [ + "8", + "9" + ], + "depends_on": [ + "W3" + ] + } + ], + "artifacts_hash": "sha256:c7e046b12a81b409ff08716e34062a9a4312a0af4cc20bdbd0442a7bea9e2b1e", + "contract_hash": "sha256:c85abc5d5ddd79b03da8ff42816436f4389bb99a5739d4fcb2a960c3c81867b1", + "workflow": "full", + "revision": 1, + "recommendation": { + "available_modes": [ + "inline", + "batch-inline", + "sdd" + ], + "recommendation": { + "mode": "sdd", + "reasons": [ + "The declared work spans multiple waves, so SDD keeps dependencies and review receipts explicit." + ] + }, + "facts": { + "workflow": "full", + "documented_task_count": 9, + "planned_task_count": 9, + "planned_wave_count": 4, + "has_parallel_wave": false, + "inline_threshold": 3 + } + }, + "recommendation_receipt": { + "recommendation": { + "available_modes": [ + "inline", + "batch-inline", + "sdd" + ], + "recommendation": { + "mode": "sdd", + "reasons": [ + "The declared work spans multiple waves, so SDD keeps dependencies and review receipts explicit." + ] + }, + "facts": { + "workflow": "full", + "documented_task_count": 9, + "planned_task_count": 9, + "planned_wave_count": 4, + "has_parallel_wave": false, + "inline_threshold": 3 + } + }, + "waves": [ + { + "id": "W1", + "strategy": "serial", + "tasks": [ + "1", + "2" + ], + "depends_on": [] + }, + { + "id": "W2", + "strategy": "serial", + "tasks": [ + "3", + "4" + ], + "depends_on": [ + "W1" + ] + }, + { + "id": "W3", + "strategy": "serial", + "tasks": [ + "5", + "6", + "7" + ], + "depends_on": [ + "W2" + ] + }, + { + "id": "W4", + "strategy": "serial", + "tasks": [ + "8", + "9" + ], + "depends_on": [ + "W3" + ] + } + ], + "artifacts_hash": "sha256:c7e046b12a81b409ff08716e34062a9a4312a0af4cc20bdbd0442a7bea9e2b1e", + "contract_hash": "sha256:c85abc5d5ddd79b03da8ff42816436f4389bb99a5739d4fcb2a960c3c81867b1", + "workflow": "full", + "execution_plan_revision_at_recommendation": null, + "created_at": "2026-09-20T08:16:23.723Z", + "hash": "sha256:731967dc9f49d233bbf9bb22b2cab7ce087e69893333490aa8c9142e5bdf16ed" + }, + "selection": { + "confirmed": true, + "followed_recommendation": true, + "acknowledged_non_recommendation": false + }, + "hash": "sha256:77341d6e000ef1624d59d66b8b361c24e7ef8f1b5d065dd4d648a2a7537e72d5" +} diff --git a/changes/n05-salary-calc/.superpowers/sdd/execution-plan.json b/changes/n05-salary-calc/.superpowers/sdd/execution-plan.json index abf452b..aaf87b7 100644 --- a/changes/n05-salary-calc/.superpowers/sdd/execution-plan.json +++ b/changes/n05-salary-calc/.superpowers/sdd/execution-plan.json @@ -1,7 +1,7 @@ { "mode": "sdd", "source": "user-confirmed", - "rationale": "分层城市数据方案已批准;重新建立当前 W1-W4 执行计划", + "rationale": "任务勾选已完成,重建当前工件哈希并保留同一 W1-W4 执行范围", "waves": [ { "id": "W1", @@ -47,7 +47,7 @@ ] } ], - "artifacts_hash": "sha256:c7e046b12a81b409ff08716e34062a9a4312a0af4cc20bdbd0442a7bea9e2b1e", + "artifacts_hash": "sha256:5e15964e215b2645ea2679d15626cf2cb04fba15b8285049fc4ba8104d7ad330", "contract_hash": "sha256:c85abc5d5ddd79b03da8ff42816436f4389bb99a5739d4fcb2a960c3c81867b1", "workflow": "full", "revision": 1, @@ -139,17 +139,17 @@ ] } ], - "artifacts_hash": "sha256:c7e046b12a81b409ff08716e34062a9a4312a0af4cc20bdbd0442a7bea9e2b1e", + "artifacts_hash": "sha256:5e15964e215b2645ea2679d15626cf2cb04fba15b8285049fc4ba8104d7ad330", "contract_hash": "sha256:c85abc5d5ddd79b03da8ff42816436f4389bb99a5739d4fcb2a960c3c81867b1", "workflow": "full", "execution_plan_revision_at_recommendation": null, - "created_at": "2026-09-20T08:16:23.723Z", - "hash": "sha256:731967dc9f49d233bbf9bb22b2cab7ce087e69893333490aa8c9142e5bdf16ed" + "created_at": "2026-09-20T09:13:09.901Z", + "hash": "sha256:a723f2d8fda600c00e76671ae631e81aaa425cf2d0e18d0a9060ba07776e1bd8" }, "selection": { "confirmed": true, "followed_recommendation": true, "acknowledged_non_recommendation": false }, - "hash": "sha256:77341d6e000ef1624d59d66b8b361c24e7ef8f1b5d065dd4d648a2a7537e72d5" + "hash": "sha256:b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c" } diff --git a/changes/n05-salary-calc/.superpowers/sdd/execution-recommendation.json b/changes/n05-salary-calc/.superpowers/sdd/execution-recommendation.json index 9553c7f..a5bab7f 100644 --- a/changes/n05-salary-calc/.superpowers/sdd/execution-recommendation.json +++ b/changes/n05-salary-calc/.superpowers/sdd/execution-recommendation.json @@ -65,10 +65,10 @@ ] } ], - "artifacts_hash": "sha256:c7e046b12a81b409ff08716e34062a9a4312a0af4cc20bdbd0442a7bea9e2b1e", + "artifacts_hash": "sha256:5e15964e215b2645ea2679d15626cf2cb04fba15b8285049fc4ba8104d7ad330", "contract_hash": "sha256:c85abc5d5ddd79b03da8ff42816436f4389bb99a5739d4fcb2a960c3c81867b1", "workflow": "full", "execution_plan_revision_at_recommendation": null, - "created_at": "2026-09-20T08:16:23.723Z", - "hash": "sha256:731967dc9f49d233bbf9bb22b2cab7ce087e69893333490aa8c9142e5bdf16ed" + "created_at": "2026-09-20T09:13:09.901Z", + "hash": "sha256:a723f2d8fda600c00e76671ae631e81aaa425cf2d0e18d0a9060ba07776e1bd8" } diff --git a/changes/n05-salary-calc/.superpowers/sdd/plans/r1-b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c/reviews/VzE.json b/changes/n05-salary-calc/.superpowers/sdd/plans/r1-b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c/reviews/VzE.json new file mode 100644 index 0000000..bac3e28 --- /dev/null +++ b/changes/n05-salary-calc/.superpowers/sdd/plans/r1-b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c/reviews/VzE.json @@ -0,0 +1,10 @@ +{ + "status": "pass", + "base": "36c0214723a3fdd1714ff7923a4f38de07d7b042", + "head": "2d315f90e46982e9658da6483f46e2acbe21bcca", + "report": ".superpowers\\sdd\\reviews\\W1.md", + "report_sha256": "sha256:afb12884b78fdfd3b0edb199900c84f19a130e8bb95c1a680255d59aee43343c", + "plan_hash": "sha256:b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c", + "plan_revision": 1, + "recorded_at": "2026-09-20T09:13:15.161Z" +} diff --git a/changes/n05-salary-calc/.superpowers/sdd/plans/r1-b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c/reviews/VzI.json b/changes/n05-salary-calc/.superpowers/sdd/plans/r1-b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c/reviews/VzI.json new file mode 100644 index 0000000..b5472da --- /dev/null +++ b/changes/n05-salary-calc/.superpowers/sdd/plans/r1-b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c/reviews/VzI.json @@ -0,0 +1,10 @@ +{ + "status": "pass", + "base": "2d315f90e46982e9658da6483f46e2acbe21bcca", + "head": "4b42a624d7ec2bb5cac64bc13cd7d4d8eba91ee7", + "report": ".superpowers\\sdd\\reviews\\W2.md", + "report_sha256": "sha256:2e4ac6255d2c08931102f75b414b24552c6fe2272b3bc139a91e8c4c77215bbe", + "plan_hash": "sha256:b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c", + "plan_revision": 1, + "recorded_at": "2026-09-20T09:13:15.620Z" +} diff --git a/changes/n05-salary-calc/.superpowers/sdd/plans/r1-b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c/reviews/VzM.json b/changes/n05-salary-calc/.superpowers/sdd/plans/r1-b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c/reviews/VzM.json new file mode 100644 index 0000000..1dacc20 --- /dev/null +++ b/changes/n05-salary-calc/.superpowers/sdd/plans/r1-b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c/reviews/VzM.json @@ -0,0 +1,10 @@ +{ + "status": "pass", + "base": "4b42a624d7ec2bb5cac64bc13cd7d4d8eba91ee7", + "head": "27dbc095e0413f9566bcc4ddc3bdb4f642535a6b", + "report": ".superpowers\\sdd\\reviews\\W3.md", + "report_sha256": "sha256:7d2823941f43fd101c1271be77703e6ae614650c64c7c1fe1484a7cae41f68fa", + "plan_hash": "sha256:b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c", + "plan_revision": 1, + "recorded_at": "2026-09-20T09:13:16.077Z" +} diff --git a/changes/n05-salary-calc/.superpowers/sdd/plans/r1-b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c/reviews/VzQ.json b/changes/n05-salary-calc/.superpowers/sdd/plans/r1-b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c/reviews/VzQ.json new file mode 100644 index 0000000..47c2847 --- /dev/null +++ b/changes/n05-salary-calc/.superpowers/sdd/plans/r1-b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c/reviews/VzQ.json @@ -0,0 +1,10 @@ +{ + "status": "pass", + "base": "27dbc095e0413f9566bcc4ddc3bdb4f642535a6b", + "head": "ed212c8e6b44688ce9e7fd9df36e724f5977559e", + "report": ".superpowers\\sdd\\reviews\\W4.md", + "report_sha256": "sha256:2e6fa11d7eca063a80d97e3edc27b21763f285e9f6575e45db57b4c670d51972", + "plan_hash": "sha256:b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c", + "plan_revision": 1, + "recorded_at": "2026-09-20T09:13:16.534Z" +} diff --git a/changes/n05-salary-calc/.superpowers/sdd/reviews/VzE.json b/changes/n05-salary-calc/.superpowers/sdd/reviews/VzE.json index 9b3ae1e..bac3e28 100644 --- a/changes/n05-salary-calc/.superpowers/sdd/reviews/VzE.json +++ b/changes/n05-salary-calc/.superpowers/sdd/reviews/VzE.json @@ -4,7 +4,7 @@ "head": "2d315f90e46982e9658da6483f46e2acbe21bcca", "report": ".superpowers\\sdd\\reviews\\W1.md", "report_sha256": "sha256:afb12884b78fdfd3b0edb199900c84f19a130e8bb95c1a680255d59aee43343c", - "plan_hash": "sha256:77341d6e000ef1624d59d66b8b361c24e7ef8f1b5d065dd4d648a2a7537e72d5", + "plan_hash": "sha256:b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c", "plan_revision": 1, - "recorded_at": "2026-09-20T08:23:35.131Z" + "recorded_at": "2026-09-20T09:13:15.161Z" } diff --git a/changes/n05-salary-calc/.superpowers/sdd/reviews/VzI.json b/changes/n05-salary-calc/.superpowers/sdd/reviews/VzI.json index fc8c755..b5472da 100644 --- a/changes/n05-salary-calc/.superpowers/sdd/reviews/VzI.json +++ b/changes/n05-salary-calc/.superpowers/sdd/reviews/VzI.json @@ -4,7 +4,7 @@ "head": "4b42a624d7ec2bb5cac64bc13cd7d4d8eba91ee7", "report": ".superpowers\\sdd\\reviews\\W2.md", "report_sha256": "sha256:2e4ac6255d2c08931102f75b414b24552c6fe2272b3bc139a91e8c4c77215bbe", - "plan_hash": "sha256:77341d6e000ef1624d59d66b8b361c24e7ef8f1b5d065dd4d648a2a7537e72d5", + "plan_hash": "sha256:b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c", "plan_revision": 1, - "recorded_at": "2026-09-20T08:29:10.010Z" + "recorded_at": "2026-09-20T09:13:15.620Z" } diff --git a/changes/n05-salary-calc/.superpowers/sdd/reviews/VzM.json b/changes/n05-salary-calc/.superpowers/sdd/reviews/VzM.json index e87ab95..1dacc20 100644 --- a/changes/n05-salary-calc/.superpowers/sdd/reviews/VzM.json +++ b/changes/n05-salary-calc/.superpowers/sdd/reviews/VzM.json @@ -4,7 +4,7 @@ "head": "27dbc095e0413f9566bcc4ddc3bdb4f642535a6b", "report": ".superpowers\\sdd\\reviews\\W3.md", "report_sha256": "sha256:7d2823941f43fd101c1271be77703e6ae614650c64c7c1fe1484a7cae41f68fa", - "plan_hash": "sha256:77341d6e000ef1624d59d66b8b361c24e7ef8f1b5d065dd4d648a2a7537e72d5", + "plan_hash": "sha256:b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c", "plan_revision": 1, - "recorded_at": "2026-09-20T08:34:51.607Z" + "recorded_at": "2026-09-20T09:13:16.077Z" } diff --git a/changes/n05-salary-calc/.superpowers/sdd/reviews/VzQ.json b/changes/n05-salary-calc/.superpowers/sdd/reviews/VzQ.json index 9c1d78f..47c2847 100644 --- a/changes/n05-salary-calc/.superpowers/sdd/reviews/VzQ.json +++ b/changes/n05-salary-calc/.superpowers/sdd/reviews/VzQ.json @@ -4,7 +4,7 @@ "head": "ed212c8e6b44688ce9e7fd9df36e724f5977559e", "report": ".superpowers\\sdd\\reviews\\W4.md", "report_sha256": "sha256:2e6fa11d7eca063a80d97e3edc27b21763f285e9f6575e45db57b4c670d51972", - "plan_hash": "sha256:77341d6e000ef1624d59d66b8b361c24e7ef8f1b5d065dd4d648a2a7537e72d5", + "plan_hash": "sha256:b04ca4af0927fa94d7c56a462549a5edb6717e542ec30228c0f0349848cd631c", "plan_revision": 1, - "recorded_at": "2026-09-20T09:12:19.493Z" + "recorded_at": "2026-09-20T09:13:16.534Z" } diff --git a/changes/n05-salary-calc/decision-point-audit.md b/changes/n05-salary-calc/decision-point-audit.md new file mode 100644 index 0000000..e26c085 --- /dev/null +++ b/changes/n05-salary-calc/decision-point-audit.md @@ -0,0 +1,74 @@ +# Decision-Point Audit Report + +**变更**: n05-salary-calc +**生成时间**: 2026-09-20T09:13:28.889Z +**当前状态**: executing + +## 汇总表 + +| DP | 名称 | 结果 | 时间戳 | +|----|------|------|--------| +| DP-0 | 用户确认门禁 | confirmed | 2026-09-20T07:28:51Z | +| DP-1 | 需求确认 | confirmed: 后端薪资计算器范围、计算口径、城市政策模型、历史记录生命周期、接口、错误码、隐私与验收标准均已确认 | 2026-09-20T07:29:03Z | +| DP-2 | 工件审查 | approved: layered CITY/PROVINCE_FALLBACK data strategy, updated specs/design/tasks | 2026-09-20T08:15:41Z | +| DP-3 | 契约批准 | approved: updated execution contract with layered city policy data and dataLevel propagation | 2026-09-20T08:15:41Z | +| DP-4 | 执行模式选择 | sdd: plan revision 1; user-confirmed; 任务勾选已完成,重建当前工件哈希并保留同一 W1-W4 执行范围 | 2026-09-20T09:13:10.032Z | +| DP-5 | 调试升级 | not recorded | — | +| DP-6 | 验证失败 | not recorded | — | +| DP-7 | 归档确认 | not recorded | — | + +**统计**: 5/8 已记录,3/8 未记录。 + +## 逐决策点说明 + +### DP-0: 用户确认门禁 + +- **结果**: confirmed +- **时间戳**: 2026-09-20T07:28:51Z +- **解读**: 决策点 DP-0 已记录为 "confirmed"。 + +### DP-1: 需求确认 + +- **结果**: confirmed: 后端薪资计算器范围、计算口径、城市政策模型、历史记录生命周期、接口、错误码、隐私与验收标准均已确认 +- **时间戳**: 2026-09-20T07:29:03Z +- **解读**: 决策点 DP-1 已记录为 "confirmed: 后端薪资计算器范围、计算口径、城市政策模型、历史记录生命周期、接口、错误码、隐私与验收标准均已确认"。 + +### DP-2: 工件审查 + +- **结果**: approved: layered CITY/PROVINCE_FALLBACK data strategy, updated specs/design/tasks +- **时间戳**: 2026-09-20T08:15:41Z +- **解读**: 决策点 DP-2 已记录为 "approved: layered CITY/PROVINCE_FALLBACK data strategy, updated specs/design/tasks"。 + +### DP-3: 契约批准 + +- **结果**: approved: updated execution contract with layered city policy data and dataLevel propagation +- **时间戳**: 2026-09-20T08:15:41Z +- **解读**: 决策点 DP-3 已记录为 "approved: updated execution contract with layered city policy data and dataLevel propagation"。 + +### DP-4: 执行模式选择 + +- **结果**: sdd: plan revision 1; user-confirmed; 任务勾选已完成,重建当前工件哈希并保留同一 W1-W4 执行范围 +- **时间戳**: 2026-09-20T09:13:10.032Z +- **解读**: 决策点 DP-4 已记录为 "sdd: plan revision 1; user-confirmed; 任务勾选已完成,重建当前工件哈希并保留同一 W1-W4 执行范围"。 + +### DP-5: 调试升级 + +- **结果**: not recorded +- **时间戳**: — +- **解读**: 该决策点尚未记录结果。如果工作流已经经过该阶段,请检查是否漏记。 + +### DP-6: 验证失败 + +- **结果**: not recorded +- **时间戳**: — +- **解读**: 该决策点尚未记录结果。如果工作流已经经过该阶段,请检查是否漏记。 + +### DP-7: 归档确认 + +- **结果**: not recorded +- **时间戳**: — +- **解读**: 该决策点尚未记录结果。如果工作流已经经过该阶段,请检查是否漏记。 + +--- + +*本报告由 `ssf audit` 自动生成,仅供审计与归档参考。* diff --git a/changes/n05-salary-calc/tasks.md b/changes/n05-salary-calc/tasks.md index 72533f2..9f1cd27 100644 --- a/changes/n05-salary-calc/tasks.md +++ b/changes/n05-salary-calc/tasks.md @@ -11,15 +11,15 @@ ## Tasks -- [ ] 1 新增 V40 表结构和薪资领域持久化基础**:修改 `mydreamjob-server/src/main/resources/db/migration/V40__salary_calculator_schema.sql`,新增城市政策与历史表、`data_level` 精度字段、索引和唯一约束;在 `mydreamjob-server/src/main/java/com/mydreamjob/salarycalculator/` 新增实体和 Repository。完成后 H2/MySQL 可创建两张表,并能按城市编码和用户历史分页查询。证明:`cd mydreamjob-server; mvn -Dtest=SalaryMigrationTest,SalaryRepositoryIntegrationTest test`。依赖:无。 -- [ ] 2 新增 V41 全量城市政策种子和完整性校验**:修改 `mydreamjob-server/src/main/resources/db/migration/V41__salary_city_policy_seed.sql`,按采集截止日官方行政区划清单写入大陆目标城市政策;在 `mydreamjob-server/src/test/java/com/mydreamjob/salarycalculator/` 新增种子测试。完成后目标城市全量覆盖,编码唯一,字段、比例、上下限、日期和版本均合法。证明:`cd mydreamjob-server; mvn -Dtest=SalaryCityPolicySeedTest,SalaryMigrationTest test`。依赖:1。 -- [ ] 3 实现城市政策查询接口**:在 `mydreamjob-server/src/main/java/com/mydreamjob/salarycalculator/` 新增城市查询 Service、Controller 和 DTO;修改 `SecurityConfig.java` 明确 `/salary-calculator/**` 认证入口。完成后 `GET /salary-calculator/cities` 返回全部启用城市摘要,`GET /salary-calculator/cities/{cityCode}` 返回完整默认参数,并仅允许 STUDENT。证明:`cd mydreamjob-server; mvn -Dtest=SalaryCityPolicyServiceTest,SalaryCityPolicyControllerTest test`。依赖:1,2。 -- [ ] 4 实现 BigDecimal 薪资计算引擎**:在 `mydreamjob-server/src/main/java/com/mydreamjob/salarycalculator/` 新增计算器、税率表、输入/结果 DTO 和校验器。完成后基数截断、覆盖优先、个人缴费、大病费用、累计月份、专项附加扣除、累计预扣、税额下限和两位小数结果均有精确单元测试。证明:`cd mydreamjob-server; mvn -Dtest=SalaryCalculationEngineTest test`。依赖:1。 -- [ ] 5 实现计算并保存接口及政策版本校验**:在 `mydreamjob-server/src/main/java/com/mydreamjob/salarycalculator/` 新增计算 Service、Controller、幂等指纹和事务逻辑;在 `ErrorCode.java` 新增 `9801–9805`。完成后合法请求返回完整结果和历史 ID,版本不匹配返回 `409/9805`,同键重放返回原历史,同键不同请求返回 `409/9804`。证明:`cd mydreamjob-server; mvn -Dtest=SalaryCalculationServiceTest,SalaryCalculationControllerTest,SalaryPolicyVersionIntegrationTest,SalaryIdempotencyIntegrationTest test`。依赖:2,3,4。 -- [ ] 6 实现历史列表、详情和物理删除**:在 `mydreamjob-server/src/main/java/com/mydreamjob/salarycalculator/` 新增历史 Service、Controller 和响应 DTO。完成后本人历史上限分页、摘要列表、完整详情、物理删除和跨用户 404 隔离均可验证,城市停用不影响旧历史。证明:`cd mydreamjob-server; mvn -Dtest=SalaryHistoryServiceTest,SalaryHistoryControllerTest,SalaryHistoryPrivacyTest test`。依赖:5。 -- [ ] 7 接入权限、日志和账号注销清理**:修改 `mydreamjob-server/src/main/java/com/mydreamjob/salarycalculator/` 和 `mydreamjob-server/src/main/java/com/mydreamjob/deletion/AccountDeletionCompletionWorker.java`,复用 STUDENT 角色检查、注销守卫和物理清理。完成后非 STUDENT 被拒绝,PENDING 写操作被现有守卫阻止,COMPLETED 时历史物理删除且日志不含金额。证明:`cd mydreamjob-server; mvn -Dtest=SalaryAuthorizationIntegrationTest,SalaryAccountDeletionIntegrationTest,SalaryLoggingPrivacyTest test`。依赖:3,5,6。 -- [ ] 8 完成模块测试与 OpenAPI/联调文档**:在 `mydreamjob-server/src/test/java/com/mydreamjob/salarycalculator/` 补齐迁移、金额、权限、幂等、并发、历史快照、删除和 E2E 测试;修改 `docs/openapi-rc.json`、`docs/salary-calculator-api.md`、`docs/frontend-integration.md`。完成后六个接口、请求响应、错误码、版本冲突、幂等和计算假设与运行时一致。证明:`cd mydreamjob-server; mvn -Dtest=Salary*Test test`;`python docs/verify_openapi.py`。依赖:1,2,3,4,5,6,7。 -- [ ] 9 执行全量回归和真实 MySQL/Docker 黑盒验收并更新路线图**:修改 `docs/backend-development-roadmap.md`、`README.md`;在真实 MySQL 环境执行 Flyway、六个接口和账号注销清理验收。完成后全量构建通过,路线图状态更新为已实现/待验收,真实数据库行为与 H2 一致。证明:`cd mydreamjob-server; mvn clean verify`;MySQL/Docker 黑盒脚本与验收记录。依赖:8。 +- [x] 1 新增 V40 表结构和薪资领域持久化基础**:修改 `mydreamjob-server/src/main/resources/db/migration/V40__salary_calculator_schema.sql`,新增城市政策与历史表、`data_level` 精度字段、索引和唯一约束;在 `mydreamjob-server/src/main/java/com/mydreamjob/salarycalculator/` 新增实体和 Repository。完成后 H2/MySQL 可创建两张表,并能按城市编码和用户历史分页查询。证明:`cd mydreamjob-server; mvn -Dtest=SalaryMigrationTest,SalaryRepositoryIntegrationTest test`。依赖:无。 +- [x] 2 新增 V41 全量城市政策种子和完整性校验**:修改 `mydreamjob-server/src/main/resources/db/migration/V41__salary_city_policy_seed.sql`,按采集截止日官方行政区划清单写入大陆目标城市政策;在 `mydreamjob-server/src/test/java/com/mydreamjob/salarycalculator/` 新增种子测试。完成后目标城市全量覆盖,编码唯一,字段、比例、上下限、日期和版本均合法。证明:`cd mydreamjob-server; mvn -Dtest=SalaryCityPolicySeedTest,SalaryMigrationTest test`。依赖:1。 +- [x] 3 实现城市政策查询接口**:在 `mydreamjob-server/src/main/java/com/mydreamjob/salarycalculator/` 新增城市查询 Service、Controller 和 DTO;修改 `SecurityConfig.java` 明确 `/salary-calculator/**` 认证入口。完成后 `GET /salary-calculator/cities` 返回全部启用城市摘要,`GET /salary-calculator/cities/{cityCode}` 返回完整默认参数,并仅允许 STUDENT。证明:`cd mydreamjob-server; mvn -Dtest=SalaryCityPolicyServiceTest,SalaryCityPolicyControllerTest test`。依赖:1,2。 +- [x] 4 实现 BigDecimal 薪资计算引擎**:在 `mydreamjob-server/src/main/java/com/mydreamjob/salarycalculator/` 新增计算器、税率表、输入/结果 DTO 和校验器。完成后基数截断、覆盖优先、个人缴费、大病费用、累计月份、专项附加扣除、累计预扣、税额下限和两位小数结果均有精确单元测试。证明:`cd mydreamjob-server; mvn -Dtest=SalaryCalculationEngineTest test`。依赖:1。 +- [x] 5 实现计算并保存接口及政策版本校验**:在 `mydreamjob-server/src/main/java/com/mydreamjob/salarycalculator/` 新增计算 Service、Controller、幂等指纹和事务逻辑;在 `ErrorCode.java` 新增 `9801–9805`。完成后合法请求返回完整结果和历史 ID,版本不匹配返回 `409/9805`,同键重放返回原历史,同键不同请求返回 `409/9804`。证明:`cd mydreamjob-server; mvn -Dtest=SalaryCalculationServiceTest,SalaryCalculationControllerTest,SalaryPolicyVersionIntegrationTest,SalaryIdempotencyIntegrationTest test`。依赖:2,3,4。 +- [x] 6 实现历史列表、详情和物理删除**:在 `mydreamjob-server/src/main/java/com/mydreamjob/salarycalculator/` 新增历史 Service、Controller 和响应 DTO。完成后本人历史上限分页、摘要列表、完整详情、物理删除和跨用户 404 隔离均可验证,城市停用不影响旧历史。证明:`cd mydreamjob-server; mvn -Dtest=SalaryHistoryServiceTest,SalaryHistoryControllerTest,SalaryHistoryPrivacyTest test`。依赖:5。 +- [x] 7 接入权限、日志和账号注销清理**:修改 `mydreamjob-server/src/main/java/com/mydreamjob/salarycalculator/` 和 `mydreamjob-server/src/main/java/com/mydreamjob/deletion/AccountDeletionCompletionWorker.java`,复用 STUDENT 角色检查、注销守卫和物理清理。完成后非 STUDENT 被拒绝,PENDING 写操作被现有守卫阻止,COMPLETED 时历史物理删除且日志不含金额。证明:`cd mydreamjob-server; mvn -Dtest=SalaryAuthorizationIntegrationTest,SalaryAccountDeletionIntegrationTest,SalaryLoggingPrivacyTest test`。依赖:3,5,6。 +- [x] 8 完成模块测试与 OpenAPI/联调文档**:在 `mydreamjob-server/src/test/java/com/mydreamjob/salarycalculator/` 补齐迁移、金额、权限、幂等、并发、历史快照、删除和 E2E 测试;修改 `docs/openapi-rc.json`、`docs/salary-calculator-api.md`、`docs/frontend-integration.md`。完成后六个接口、请求响应、错误码、版本冲突、幂等和计算假设与运行时一致。证明:`cd mydreamjob-server; mvn -Dtest=Salary*Test test`;`python docs/verify_openapi.py`。依赖:1,2,3,4,5,6,7。 +- [x] 9 执行全量回归和真实 MySQL/Docker 黑盒验收并更新路线图**:修改 `docs/backend-development-roadmap.md`、`README.md`;在真实 MySQL 环境执行 Flyway、六个接口和账号注销清理验收。完成后全量构建通过,路线图状态更新为已实现/待验收,真实数据库行为与 H2 一致。证明:`cd mydreamjob-server; mvn clean verify`;MySQL/Docker 黑盒脚本与验收记录。依赖:8。 ## Requirement-to-Task Coverage @@ -55,3 +55,4 @@ - `mvn -Dtest=Salary*Test test`、`mvn clean verify`、`python docs/verify_openapi.py` 和真实 MySQL/Docker 黑盒验收通过后才能进入 release-archivist。 + diff --git a/changes/n05-salary-calc/verification-report.md b/changes/n05-salary-calc/verification-report.md new file mode 100644 index 0000000..34aa55c --- /dev/null +++ b/changes/n05-salary-calc/verification-report.md @@ -0,0 +1,31 @@ +# N05 salary calculator verification report + +## Verification dimensions + +| Dimension | Status | Findings | +|---|---|---| +| Completeness | PASS | All 9 tasks checked; W1-W4 review receipts pass; 22 approved requirements have task coverage and implementation evidence. | +| Correctness | PASS | Salary*Test 24/24; disposable Docker MySQL 8.4 black-box PASS; Flyway V40/V41 and 359 city rows verified. | +| Coherence | PASS | Design decisions match code: current city policy + snapshots, BigDecimal engine, dataLevel propagation, idempotency, account-deletion cleanup. | +| Scope | WARN | Shared ErrorCode, security route and account-deletion worker changed as declared in the contract; no unrelated production files changed. | +| Full regression | WARN | `mvn clean verify`: 1516 tests, 2 failures / 9 errors, all in pre-existing non-salary modules. | + +## Evidence + +- `mvn -Dtest=Salary*Test test` -> 24 tests, 0 failures, 0 errors. +- `python docs/verify_openapi.py` -> 139 paths / 178 operations, all checks passed. +- Disposable Docker MySQL 8.4 + containerized backend black-box -> PASS. +- Flyway versions 40,41; city policy count 359; CITY=12; PROVINCE_FALLBACK=347. +- Temporary MySQL/backend containers and network destroyed after verification. +- Delta specs synced through `ssf sync`; publication receipt recorded. + +## Verdict + +**CONDITIONAL PASS for N05**: all salary-calculator requirements, focused tests, OpenAPI checks and isolated MySQL black-box acceptance pass. The only release warning is the repository-wide pre-existing regression failures outside this change: + +- `CareerProgressServiceTest.fullAggregation_returnsCorrectSummaries` +- `JobReviewDimensionControllerTest.listReviews_shouldReturnDimensionScores` +- `JobReviewDimensionServiceTest.listReviews_shouldReturnDimensionScoresWithoutN1` +- 9 `LocalResumeStorageServiceTest` extension-context errors + +No salarycalculator tests fail. Closing requires explicit user acceptance of this conditional release outcome. diff --git a/specs/salary-calculation/spec.md b/specs/salary-calculation/spec.md new file mode 100644 index 0000000..1ccac01 --- /dev/null +++ b/specs/salary-calculation/spec.md @@ -0,0 +1,152 @@ +# salary-calculation + +## Purpose + +为已登录求职者提供基于城市当前政策、用户显式覆盖和个人综合所得累计预扣法的到手工资估算能力。 + +## Requirements + +### Requirement: 仅已登录 STUDENT 可访问薪资计算模块 + +The system SHALL 要求薪资城市查询、计算和历史接口均通过 JWT 认证,并且当前用户必须具有 STUDENT 角色。未认证请求 MUST 返回 401;已认证但非 STUDENT 的请求 MUST 返回 403。 + +#### Scenario: 未登录访问城市列表 + +- **WHEN** 客户端未携带有效访问令牌调用 `GET /salary-calculator/cities` +- **THEN** 系统返回 401,且不返回任何城市政策数据 + +#### Scenario: 企业用户调用计算接口 + +- **WHEN** 已登录的 COMPANY 用户调用 `POST /salary-calculator/calculations` +- **THEN** 系统返回 403,且不创建历史记录 + +### Requirement: 查询当前启用的城市政策目录 + +The system SHALL 提供城市列表和城市详情接口。城市列表 MUST 只返回 `enabled=true` 的城市摘要,并按省份编码、城市编码稳定排序;城市详情 MUST 返回城市元数据、政策版本、生效日期和当前整套默认参数。 + +#### Scenario: 获取全部启用城市 + +- **WHEN** STUDENT 调用 `GET /salary-calculator/cities` +- **THEN** 系统按省份编码和城市编码返回所有启用城市摘要,且不包含区县记录 + +#### Scenario: 获取城市默认参数 + +- **WHEN** STUDENT 调用 `GET /salary-calculator/cities/{cityCode}` 且该城市已启用 +- **THEN** 系统返回六位城市编码、省市名称、政策版本、生效日期、`dataLevel`、社保和公积金基数上下限、个人比例及大病固定费 + +### Requirement: 城市配置以地级行政区和省直辖县级市为最小单元 + +The system SHALL 以六位行政区划代码作为城市政策主键,覆盖中国大陆全部地级行政区、直辖市和省直辖县级市,不拆分区县,不包含港澳台。 + +#### Scenario: 直辖市使用一条配置 + +- **WHEN** 客户端查询北京市或重庆市 +- **THEN** 系统返回单一城市级配置,而不是按区县返回多条配置 + +#### Scenario: 市内存在分区差异 + +- **WHEN** 同一地级市不同区县存在政策差异 +- **THEN** 系统仍只使用该地级市的一条当前配置,并在估算声明中提示结果可能受同城差异影响 + +### Requirement: 校验城市政策版本和元数据 + +The system SHALL 要求每条城市政策具有唯一的六位编码、非空且不晚于采集截止日的生效日期、非空政策版本、合法参数以及 `dataLevel`。`CITY` 表示已核验的城市级参数;`PROVINCE_FALLBACK` 表示使用所属省级公开参数的近似配置。任何参数或生效日期变化 MUST 同步更新 `policyVersion`。 + +#### Scenario: 客户端使用已失效的政策版本计算 + +- **WHEN** STUDENT 提交的 `expectedPolicyVersion` 与当前城市版本不一致 +- **THEN** 系统返回 409/9805,响应包含最新 `policyVersion` 和 `policyEffectiveDate`,且不生成历史记录 + +#### Scenario: 城市政策种子存在非法参数 + +- **WHEN** `V41` 中存在上下限倒置、比例超限或缺失必填字段的城市记录 +- **THEN** 迁移或种子集成测试失败,禁止进入验收 + +#### Scenario: 使用省级回退参数的城市 + +- **WHEN** 某目标城市没有可核验的城市级参数,V41 使用所属省级公开参数创建记录 +- **THEN** 该记录必须标记为 PROVINCE_FALLBACK,城市详情、计算响应和历史快照都必须明确返回该精度等级 +- **THEN** 系统不得把该记录展示为 CITY 或声称其达到城市级官方精确度 + +### Requirement: 用户覆盖参数优先于城市默认值 + +The system SHALL 允许用户分别覆盖社保基数、公积金基数、养老比例、医疗比例、失业比例、公积金比例和大病固定费。未覆盖字段 MUST 使用城市默认值;显式覆盖值 MUST 通过城市上下限和全国合理性范围校验。 + +#### Scenario: 只覆盖公积金基数 + +- **WHEN** 用户提交新的公积金基数但未提交比例覆盖 +- **THEN** 系统使用用户公积金基数和城市默认公积金比例,其余项目继续使用城市默认值 + +#### Scenario: 覆盖基数超出城市范围 + +- **WHEN** 用户提交低于社保下限的社保基数 +- **THEN** 系统返回 400/9802,且不生成历史记录 + +### Requirement: 计算个人五险一金和大病固定费 + +The system SHALL 按社保基数计算个人养老、医疗和失业,按公积金基数计算个人公积金,并按城市或用户覆盖值扣除按月大病固定费。工伤和生育保险、单位缴费和企业总成本 MUST 不计入个人到手工资。 + +#### Scenario: 未覆盖任何参数 + +- **WHEN** 用户输入税前月薪且未提供覆盖项 +- **THEN** 社保基数按税前月薪截断到社保区间,公积金基数按税前月薪截断到公积金区间,各个人项目按对应比例计算 + +#### Scenario: 用户将某项比例覆盖为 0 + +- **WHEN** 用户将失业比例覆盖为 0 +- **THEN** 个人失业扣款为 0,其他项目按各自生效参数计算 + +### Requirement: 使用累计月份和专项附加扣除计算应纳税所得额 + +The system SHALL 接受 `monthIndex`(1–12)和月度专项附加扣除。`monthIndex` MUST 表示本年度当前工资口径下已经累计计税的月份数,并假设前序月份的收入、个人缴费和专项附加扣除与本次输入相同。 + +#### Scenario: 首次领薪但自然月为九月 + +- **WHEN** 用户九月首次领薪并提交 `monthIndex=1` +- **THEN** 系统按一个月的累计收入和扣除计算,而不是按九个月计算 + +#### Scenario: 累计月份为十二 + +- **WHEN** 用户提交 `monthIndex=12` +- **THEN** 系统按十二个月的相同工资、个人缴费和专项附加扣除推导全年累计口径 + +### Requirement: 按居民个人综合所得累计预扣法计算个税 + +The system SHALL 使用全国统一的居民个人综合所得年度累进预扣税率表和速算扣除数。基本减除费用 MUST 固定为每月 5,000 元,累计应纳税所得额小于等于 0 时 MUST 按 0 计税,当月税额 MUST 不得为负。 + +#### Scenario: 累计应纳税所得额低于起征线 + +- **WHEN** 累计收入扣除个人缴费、专项附加扣除和 5,000 × monthIndex 后不大于 0 +- **THEN** 累计应纳税额为 0,当月个税为 0 + +#### Scenario: 月薪进入更高累计税率档 + +- **WHEN** 累计应纳税所得额超过当前档位上限 +- **THEN** 系统按完整年度累进税率表计算累计应纳税额,并减去按相同假设推导的累计已预扣税额 + +### Requirement: 输出到手工资和完整计算拆解 + +The system SHALL 返回税前月薪、城市及政策版本、社保基数、公积金基数、个人养老、个人医疗、个人失业、个人公积金、大病固定费、基本减除费用、专项附加扣除、累计月份、累计与当月计税链路、当月个税和到手工资。响应 MUST 标记 `estimate=true`。 + +#### Scenario: 计算成功 + +- **WHEN** STUDENT 提交合法的城市、政策版本和工资参数 +- **THEN** 系统返回完整拆解、`estimate=true`、全国税率版本、城市政策版本与生效日期,并返回可访问的历史 ID + +### Requirement: 校验工资输入并限制金额精度 + +The system SHALL 要求税前月薪大于 0 且不超过 10,000,000 元,专项附加扣除在 0–10,000,000 元之间,所有用户金额输入最多两位小数。比例 MUST 使用小数表示,且个人上限为养老 20%、医疗 20%、失业 10%、公积金 12%;大病固定费不得超过 10,000 元/月。 + +#### Scenario: 金额超过两位小数 + +- **WHEN** 用户提交 `0.001` 元的金额字段 +- **THEN** 系统返回 400/9802,且不执行计算 + +### Requirement: 返回稳定的城市和历史错误语义 + +The system SHALL 使用 `400` 表示参数非法,`404` 表示城市或历史不存在,`409` 表示幂等或政策版本冲突。`9801` MUST 表示城市不存在或停用,`9802` MUST 表示参数非法,`9803` MUST 表示历史不存在或无权访问,`9804` MUST 表示幂等冲突,`9805` MUST 表示政策版本冲突。 + +#### Scenario: 城市停用 + +- **WHEN** 用户提交已停用城市的计算请求 +- **THEN** 系统返回 404/9801,且不生成历史记录 diff --git a/specs/salary-history/spec.md b/specs/salary-history/spec.md new file mode 100644 index 0000000..f1ecf3e --- /dev/null +++ b/specs/salary-history/spec.md @@ -0,0 +1,126 @@ +# salary-history + +## Purpose + +保存求职者每次成功薪资估算的可追溯快照,并提供仅限本人访问的分页查询、详情和物理删除能力。 + +## 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** 城市政策表、历史表、主外键、唯一键和索引创建成功,目标城市种子导入成功