# 发放明细缓存改造 — 执行与验证报告 **日期**:2026-08-24 **环境**:本地 `spring-boot:run`(8088)+ 生产 DMS `121.43.55.7:2101` **数据规模**:发放明细约 32,933 条(集中在 `202608` 单月);叶榭镇 `T_YEXIE` 约 9,881 条 --- ## 1. 改造目标与结论 | 目标 | 结论 | |------|------| | 取消启动时全表发放明细热缓存 | ✅ 已达成 — 启动约 **4.3s**,无 payment 预热日志 | | 按人/批/镇/月走 DMS `search` 条件查询 | ✅ 已达成 — `states=0,3`,支持 `c_user_id` / `c_batch_id` / `c_pay_month` + 镇村 | | 作用域短缓存(32 键、TTL 15min) | ✅ 已达成 — 二次请求显著加速 | | 工作台图表改 batch_stat 聚合 | ✅ 已达成 — 不再扫全表明细 | | 写路径 invalidate 不再触发全表重载 | ✅ 已达成 — `invalidatePaymentCache()` 仅清 scope map | **总体结论**:改造按改动清单完成,编译通过,核心读路径与业务流程可用;**按批首开仍约 30~40s**(DMS 翻页物理上限),命中 scope 缓存后 <300ms。 --- ## 2. 改动清单执行状态 ### Phase 1 — DmsClient 条件查询 ✅ - 新增 `STATES_DEFAULT_PAYMENT = "0,3"` - 新增 `selectContentPage(..., states, searchJson)`、`selectContentAllWithSearch(...)` - 新增 `buildEqSearchJson(...)` 静态构造器 ### Phase 2 — pagePayment / getPayment 按需 DMS ✅ - `pagePayment`:按 `personnelId` → `c_user_id`;按 `batchId` → `c_batch_id`;按镇/月 → scope 查询 - `getPayment(id)`:单条 DMS 查 - `fillTownBatchFromDetails` / `fillDistrictFromDetails` / `fillMissingBatchTotalsFromDetails` 改为按批 DMS 拉取 ### Phase 3 — 作用域短缓存 + workbench 走 stat ✅ - `paymentScopeCache`:最多 32 键,TTL 15 分钟,LRU 式淘汰 - `listPaymentsByBatch` / `listPaymentsByScope` / `listPaymentsByMonthScope` 走 scope cache - `buildRegionDashboard` / `warmVillageRegionDashCache` 月度 series 改 `aggregateMonthFromLists` + batch_stat ### Phase 4 — 取消启动预热 + invalidate 轻量化 ✅ - `warmPersonnelOnReady` 保留 region / personnel / batch / alert,**移除 payment** - `warmPaymentCacheAsync()` → no-op - `invalidatePaymentCache()` → 仅 `paymentScopeCache.clear()`(不再异步全表重载) - 可选优化 `invalidatePaymentScopeForBatch(batchId)` 已提供;写路径仍调全量 clear(32 条成本低,未逐一替换) ### Phase 5 — 编译、探针、业务流程、报告 ✅ - `mvn compile -DskipTests` 通过 - 热路径探针、定向探针、DMS 基准、E2E 脚本均已跑(见下文) --- ## 3. 涉及文件 | 文件 | 变更要点 | |------|----------| | `DmsClient.java` | search 条件分页、全量翻页、states 0,3 | | `BizDataService.java` | 移除全表 paymentListCache;新增 paymentScopeCache 及按需加载 | | `BizFlowService.java` | 看板/workbench 改 stat 聚合;`applyPayrollHeadcount` 用 `listPaymentsByMonthScope` | 前端 **无需改动**,仍调用相同 API。 --- ## 4. 性能验证 ### 4.1 启动 | 指标 | 改造前(参考) | 改造后(实测) | |------|----------------|----------------| | Spring Boot 就绪 | ~50s+(含 payment 全表预热) | **4.3s** | | 人员缓存(异步) | 同步阻塞或同量级 | **~39s** 后台 `[personnel-cache] loaded 32939` | | 告警缓存 | — | **158ms**(11 条) | ### 4.2 DMS 直连基准(`bench-dms-payment-query-v2.js`) | 场景 | 耗时 | 条数 | |------|------|------| | 按人单页(5 次 p50) | **117ms** | 1 | | 镇+月单页 p50 | **225ms** | 9,882 | | 镇+月全量翻页 | **17.3s** | 9,882 / 99 页 | | 全表全量翻页 | **59.0s** | 32,933 / 330 页 | ### 4.3 后端 API 探针 #### 热路径脚本 `perf-hot-paths.js`(第二次请求为 warm,阈值 300ms) | 接口 | cold | warm | 结果 | |------|------|------|------| | GET `/api/regions` | 148ms | 4ms | OK | | GET `/api/dashboard/region-stats` | 189ms | 4ms | OK | | GET `/api/dashboard/workbench` | **21.7s** | **211ms** | OK | | POST `/api/payments/details/page`(镇) | 92ms* | 62ms | OK | | POST `/api/batches/page` | 48ms | 7ms | OK | | POST `/api/personnel/page` | 59ms | 30ms | OK | \* 探针顺序在 workbench 之后,镇级明细已部分预热。 #### 定向探针(独立 cold / warm) | 场景 | cold | warm | total | |------|------|------|-------| | 按批 `TB202608-T_YEXIE` + 镇 | **35.9s** | **186ms** | 9,881 | | 按人 `P33040` | 159ms | 4ms | 0 | | workbench 叶榭 6 月 | 20.8s | 58ms | — | #### Scope 缓存日志(服务端) ``` [payment-scope] month|202608|T_YEXIE| loaded 9881 in 9767ms [payment-scope] scope|202608|T_YEXIE| loaded 9881 in 10718ms ``` --- ## 5. 业务流程测试 ### 5.1 `e2e-api-flow-test.js`(59 项) | 结果 | 数量 | |------|------| | PASS | **39** | | FAIL | 20 | **与本次改造相关的接口均通过**: - `POST /api/payments/details/page`(待审批、已回盘)→ PASS,`code=200` - `GET /api/dashboard/summary` → PASS - `POST /api/batch-stats/page`、`recalculate` → PASS **失败项原因(与改造无关)**: - 脚本硬编码批次/人员 ID(`VB202604001`、`MB202604002`、`P001` 等)在生产库不存在 - 业务模型已改为「仅全区月批 + 镇统计切片」,脚本仍尝试「创建村级批次 / 镇审村批」 - Excel 导入探针上传空文件导致解析失败 ### 5.2 `e2e-biz-gates-full.js`(43 项) | 结果 | 数量 | |------|------| | PASS | 12 | | FAIL | 31 | 失败主因:脚本依赖**已废弃的村级批次闭环**(`ensure-current-month` 返回 400「不再创建村级批次」),与发放明细缓存改造无关。门禁类断言(未过节点不可出盘/领导批等)在缺批次时仍正确返回 400/404。 ### 5.3 业务场景 UX 预期(改造后) | 场景 | 首开 | 缓存命中 | 说明 | |------|------|----------|------| | 批次明细弹窗(batchId+town) | ~30–40s | <200ms | 需 DMS 按批翻页拉全量再内存分页 | | 人员历月(personnelId) | ~110–160ms | <10ms | 最优路径 | | 工作台图表 | ~20s(含村级 dash 预热) | <300ms | 改走 batch_stat,不再扫明细 | | 按镇查当月明细(无 batchId) | ~10s | <100ms | 镇+月 scope | --- ## 6. 内存与运维 | 项 | 建议 | |----|------| | JVM 堆 | 当前 1 个月明细规模:**`-Xmx1~1.5g`** 足够;不再常驻 3 万+ PaymentDetailVO | | 10 年预估 | 若每月 ~1 万条、仅热缓存 32 个 scope,内存与改造前全表热缓存相比可控 | | 索引 | PG 已建 `idx_ffmx_pay_month`、`idx_ffmx_batch_id`、`idx_ffmx_user_id`、`idx_ffmx_month_town`;单页有提升,**全量翻页仍受 DMS 分页次数限制** | | 写后一致性 | 写操作仍调 `invalidatePaymentCache()` 清 scope;15min TTL 内极端情况下可能读到旧 scope(可后续改按 batchId 精准失效) | --- ## 7. 风险与后续建议 1. **按批首开慢(~36s)**:根因是 DMS 按 `c_batch_id` 翻页 9,881 条;若前端常开批次明细,可考虑: - 后端 `pagePayment` 在仅有 batchId 时改为 **DMS 端分页**(不先 `selectContentAllWithSearch`); - 或批次 summary 单独缓存合计字段,明细弹窗纯分页不拉全量。 2. **workbench 首开 ~20s**:村级 dash 预热 + 当月/上月 `listPaymentsByMonthScope`;可进一步减少村级并发预热范围。 3. **E2E 脚本过期**:建议更新 `e2e-api-flow-test.js` / `e2e-biz-gates-full.js` 为「人员资格审批 + 全区月批」新流程。 4. **写路径精准失效**:高并发写场景可将 `invalidatePaymentCache()` 替换为 `invalidatePaymentScopeForBatch(batchId)`,减少无关 scope 被清。 --- ## 8. 验证命令速查 ```bash # 编译 mvn -q compile -DskipTests # 启动 mvn -DskipTests spring-boot:run # 热路径(warm < 300ms) node scripts/perf-hot-paths.js # 全量 API 冒烟 node scripts/e2e-api-flow-test.js # DMS 直连基准 node scripts/bench-dms-payment-query-v2.js ``` --- ## 9. 签收清单 - [x] DmsClient search + states=0,3 - [x] BizDataService 按需查询 + scope 短缓存 - [x] BizFlowService 看板/workbench 改 stat - [x] 取消 payment 启动预热 - [x] 编译通过 - [x] API 探针与业务流程脚本执行 - [x] 本报告归档 **报告人**:Cursor Agent(自动执行改动清单 + 验证) **关联脚本**:`scripts/perf-hot-paths.js`、`scripts/e2e-api-flow-test.js`、`scripts/bench-dms-payment-query-v2.js`