feat: 汇总大表四页(城市汇总/城市分市州/中口径分析/道路运输周转量)随报表期数据驱动刷新至 1-8月

问题:导出 2026-08 时,这四张跨页合成页仍是上一报表期(1-7月/7月)的静态值,标题与数值都不随报表期变化。
方案 A:沿用货运排名页做法——同比基数取母版台账页自带的 2025 年同期列缓存值(库中暂无 2025 城市客运/网约车数据)。

- 新增 refreshSummaryCumulativeSheets(wb, year, month),插在 refreshSummaryRankSheets 之后、applySummaryReferenceLayout 之前。
- 台账取数按行标签识别(不写死行号):公交/出租/网约车(客运量、周转量、其中城市内客运量、其中城市内周转量)、
轨道轮渡、班线包车、货运(规上+规下货物周转量);19 维指标位;2026 逐月列 C..Q,2025 同期列 V..AJ。
- 口径:城市客运量/周转量 = 公交+出租+网约车(城市内) + 轨道 + 轮渡;中口径 = 班线包车 + (总量 − 城市内)×(公交/出租/网约车);
道路货运周转量 = 规上 + 规下;同比 = 当期 ÷ 2025 同期 − 1。
- 写入规则:目标格原为公式则跳过(保留母版公式,打开重算);0 且原为空格不新增;标题先 setBlank 再写(母版标题是 inlineStr)。
- 实测:2026-08 导出四页随期刷新 1056 格,日志「缺数提示=[]」;2026-07 导出精确回退为母版 1-7 月权威值
(城市汇总 B3=237412.33、中口径 B3=27524.4906、周转量 G4=15222118.44638293、城市分市州 B6=135474.77989442425);
由生成件自身台账页独立重算复核 586 格,差异 0 格。
- 文档:docs/功能测试报告/功能测试报告_汇总大表四页数据驱动_2026-09-13.md、docs/工作日结/工作日结_2026-09-13.md。
1个文件已修改
2个文件已添加
691 ■■■■■ 已修改文件
docs/功能测试报告/功能测试报告_汇总大表四页数据驱动_2026-09-13.md 135 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/工作日结/工作日结_2026-09-13.md 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java 496 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_»ã×Ü´ó±íËÄÒ³Êý¾ÝÇý¶¯_2026-09-13.md
New file
@@ -0,0 +1,135 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_汇总大表四页数据驱动_2026-09-13
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
### 1. èƒŒæ™¯
用户反馈:导出 2026 å¹´ 8 æœˆã€Šé“路运输量汇总表》后,以下 4 å¼ **跨页合成页**仍是上一报表期的静态值(标题写「2026å¹´1-7月 / 2026å¹´7月」,数值也是 7 æœˆçš„,不随报表期变化):
| é¡µå | æ¯ç‰ˆåŽŸçŠ¶æ€ |
| --- | --- |
| `城市汇总` | æ•´é¡µé™æ€ï¼ˆ1-7 æœˆç´¯è®¡ + 7 æœˆå½“月) |
| `城市分市州` | 4 å—整页静态 |
| `中口径分析` | 2 å—整页静态 |
| `道路运输周转量` | 2 å—整页静态 |
用户选定**方案 A**:沿用货运排名页的做法——用母版台账页自带的 2025 å¹´åŒæœŸåˆ—缓存值当同比基数,把「当年累计值」刷成 1-8 æœˆï¼ˆåŒæ¯”列沿用母版口径),4 é¡µä¸€æ¬¡æ€§åšå‡ºæ¥ã€‚
### 2. å£å¾„(先验证、后编码)
编码前先用母版台账页逐格聚合反推四页口径,与四页成品逐格比对 **0 å·®å¼‚**后确认:
| æŒ‡æ ‡ | å£å¾„ |
| --- | --- |
| åŸŽå¸‚客运量 | å…¬äº¤åŸŽå¸‚内客运量 + å·¡æ¸¸å‡ºç§ŸåŸŽå¸‚内客运量 + ç½‘约车城市内客运量 + è½¨é“客运量 + è½®æ¸¡å®¢è¿é‡ |
| åŸŽå¸‚客运周转量 | åŒä¸Šï¼Œæ¢æˆå„自旅客周转量 |
| ä¸­å£å¾„客运量/周转量 | å…¬è·¯ç­çº¿åŒ…车 + åŸŽé™…城乡公交 + åŸŽé™…城乡巡游出租 + åŸŽé™…城乡网约车,其中「城际城乡 X = X æ€»é‡ âˆ’ X åŸŽå¸‚内」(仅公交/出租/网约车;班线包车无城市内概念,直接用总量) |
| é“路货运周转量 | è´§è¿é¡µå¸‚州「规上货物周转量 + è§„下货物周转量」(**不用**母版「规上+规下」公式行) |
| é“路运输周转量页「道路客运周转量」 | ç­çº¿åŒ…车周转量 + åŸŽé™…城乡公交/出租/网约车周转量 |
| åŒæ¯” | å½“期 Ã· æ¯ç‰ˆå°è´¦é¡µ 2025 å¹´åŒæœŸåˆ— âˆ’ 1 |
**关于 2025 åŒæœŸåŸºæ•°**:库中只有 2026 å¹´åŸŽå¸‚客运/网约车数据,没有 2025 å¹´æ•°æ®ï¼Œæ•… 2025 åŒæœŸä¸€å¾‹å–**母版台账页自带的 2025 å¹´é€æœˆåˆ—缓存值**(V=1月、X=2月 â€¦ AJ=8月)。这也正是方案 A çš„取巧点:母版台账页本身带全 2025 é€æœˆåˆ—,无需等 2025 æ•°æ®å…¥åº“。
### 3. å®žçް
改动文件:`traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java`(+494 âˆ’2)。
- æ–°å¢žå…¥å£ `refreshSummaryCumulativeSheets(XSSFWorkbook wb, int year, int month)`;接在导出链路的 `refreshSummaryRankSheets(...)` ä¹‹åŽã€`applySummaryReferenceLayout(wb)` ä¹‹å‰è°ƒç”¨ã€‚
- å°è´¦å–æ•° `smReadLedgerMetrics`:**按行标签识别、不写死行号**
  - `公交`/`出租`/`网约车`:每市州「客运量 / æ—…客周转量 / å…¶ä¸­åŸŽå¸‚内客运量 / å…¶ä¸­åŸŽå¸‚内旅客周转量」4 è¡Œï¼›
  - `轨道、轮渡`:轨道(全省/武汉/黄石)与轮渡(全省/武汉)分块;
  - `班线包车`:全省与 17 å¸‚州客运量/周转量(「其中个体」行不重复计入);
  - ` è´§è¿`(页名带前导空格):市州「规上货物周转量 + è§„下货物周转量」。
- æœˆä»½åˆ—:2026 å¹´é€æœˆåˆ— C=1月 â€¦ Q=8月(`SM_CUR_COL0=2`),2025 å¹´åŒæœŸåˆ— V=1月 â€¦ AJ=8月(`SM_LAST_COL0=21`),每月间隔 2 åˆ—(值列 + åŒæ¯”列)。
- 19 ç»´æŒ‡æ ‡ä½ï¼ˆ`SM_DIM=19`):公交/出租/网约车 å„ 4 ç»´ï¼ˆæ€»é‡ã€å‘¨è½¬é‡ã€åŸŽå¸‚内总量、城市内周转量)+ è½¨é“ 2 ç»´ + è½®æ¸¡ 2 ç»´ + ç­çº¿åŒ…车 2 ç»´ + è´§è¿å‘¨è½¬é‡ 1 ç»´ã€‚
- å†™å€¼è§„则 `smSetVal`:**目标格原本是公式 â†’ ä¿ç•™æ¯ç‰ˆå…¬å¼ä¸è¦†ç›–**(打开即重算);目标格为空且新值为 0 â†’ ä¸æ–°å¢žï¼›å…¶ä½™å…ˆ `setBlank()` å†å†™ï¼Œé¿å… inlineStr æ®‹ç•™ã€‚
- å››é¡µå®žé™…写入 **1056 æ ¼**。
## äºŒã€æµ‹è¯•环境
- åŽç«¯ `http://localhost:8090`(`start-dev.ps1` å¯åŠ¨ï¼ŒPID 27840),前端 `http://localhost:8080`(PID 23416)均就绪。
- æ¯ç‰ˆï¼š`docs/生成汇总大表/2026å¹´8月道路运输量汇总表.xlsx`;版式参照:`docs/生成汇总大表/生成_道路运输量汇总表.xlsx`。
- å¯¼å‡ºæŽ¥å£ï¼š`GET /api/report/export/summaryWorkbook?period=YYYY-MM`(Bearer token)。
- æµ‹è¯•时间:2026-09-13。
- æµ‹è¯•产物:`_t3_2026-08.xlsx`(950,783 å­—节 / HTTP 200 / 36.8 ç§’)、`_t3_2026-07.xlsx`(942,691 å­—节 / HTTP 200 / 25.7 ç§’);归档件 `docs/生成汇总大表/输出/生成_道路运输量汇总表_2026-08_四页数据驱动_2026-09-13.xlsx`(950,783 å­—节)。
- ç‹¬ç«‹å¤æ ¸è„šæœ¬ï¼š`_v5_four.py`(输出 `_v5_four.txt`)、`_verify_four.py`(输出 `_verify_four.txt`)。
## ä¸‰ã€ç”¨ä¾‹è¡¨
### 3.1 æ­£å‘用例
| ç¼–号 | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-1 | å¯¼å‡º `period=2026-08` | HTTP 200,正常产出 | 950,783 å­—节,36.8 ç§’ | é€šè¿‡ |
| TC-2 | ä¸Ž 09-13 13:06 å½’档件比对 | åŒå‚数导出结果确定 | ä¸¤æ¬¡äº§ç‰©åŒä¸º 950,783 å­—节 | é€šè¿‡ |
| TC-3 | åŽç«¯å›žå¡«æ—¥å¿— | æœ‰å››é¡µåˆ·æ–°ç»Ÿè®¡ã€æ— ç¼ºæ•°æç¤ºã€æ— å¼‚常 | ã€Œåˆ·æ–°å››é¡µï¼ˆåŸŽå¸‚汇总/城市分市州/中口径分析/道路运输周转量)数值格数=1056」「刷新排名页数值格数=711」「缺数提示=[]」;最近 4MB æ—¥å¿—æ—  `ERROR`/`Exception`/`WARN` | é€šè¿‡ |
| TC-4 | `城市汇总` æ ‡é¢˜ | éšæŠ¥è¡¨æœŸæ”¹ä¸º 1-8 æœˆ / 8 æœˆ | `A1=2026å¹´1-8月全省累计完成城市客运量情况`、`A12=2026å¹´8月全省完成城市客运量情况` | é€šè¿‡ |
| TC-5 | `城市汇总` ç´¯è®¡å—(B3:E9) | å®¢è¿é‡/同比/周转量/同比 å„ 7 è¡Œ | 28 æ ¼å…¨éƒ¨ä¸€è‡´ï¼ˆå¦‚ B3=269628.07、C3=0.00117、D3=1681467.65、E3=0.00835) | é€šè¿‡ |
| TC-6 | `城市汇总` å½“月块(B14:E20) | åŒä¸Šï¼Œå½“月口径 | 28 æ ¼å…¨éƒ¨ä¸€è‡´ï¼ˆå¦‚ B14=32215.74、C14=0.0444) | é€šè¿‡ |
| TC-7 | `城市分市州` æ ‡é¢˜ Ã—4 | 4 å—标题随报表期 | `A1=…1-8月累计情况`、`A23=…1-8月累计情况`、`A46=…8月情况`、`A68=…8月情况` | é€šè¿‡ |
| TC-8 | `城市分市州` 4 å— Ã— 17 å¸‚å·ž Ã— 6 å€¼åˆ— | 6 ç»„值 = æ€»é‡/公交/出租/网约车/轨道/轮渡 | 408 æ ¼å…¨éƒ¨ä¸€è‡´ | é€šè¿‡ |
| TC-9 | `中口径分析` æ ‡é¢˜ + æ•°å€¼ | 2 å—标题 + æ¯å— æ€»/班线/城乡公交/城乡出租/城乡网约车 | `A1=1-8月中口径客运量及周转量`、`A10=8月中口径客运量及周转量`;B3=31528.0607、F3=1227147.4890 ç­‰ 10 æ ¼å…¨éƒ¨ä¸€è‡´ | é€šè¿‡ |
| TC-10 | `道路运输周转量` æ ‡é¢˜ + æ•°å€¼ | 2 å—标题 + 17 å¸‚å·ž G/L/Q/W/X/Y | `A1=2026å¹´1-8月全省累计完成交通周转量情况`、`A23=2026å¹´8月全省完成交通周转量情况`;102 æ ¼å…¨éƒ¨ä¸€è‡´ï¼ˆå¦‚ G4=17492129.04788293、L4=269628.07、Q4=1227147.48902;W/X/Y åŒæœŸåˆ—取 2025 å¹´åŒæœŸç´¯è®¡ï¼Œå¦‚ W4=17492068.4575、X4=269312.48、Y4=1256684.2157) | é€šè¿‡ |
### 3.2 è´Ÿå‘用例
| ç¼–号 | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-N1 | ç›®æ ‡æ ¼æ˜¯æ¯ç‰ˆå…¬å¼ï¼ˆ`城市分市州` å…¨çœè¡Œ `B5=G5+L5+Q5+V5+X5`、`道路运输周转量` `J4=G4/W4-1`) | ä¸è¢«å†™å€¼è¦†ç›–,保留母版公式 | ä¸¤è€…仍为公式;四页写入格数 1056 < å››é¡µç†è®ºæ ¼æ•°ï¼Œè¯´æ˜Žå…¬å¼æ ¼è¢«è·³è¿‡ | é€šè¿‡ |
| TC-N2 | ç›®æ ‡æ ¼ä¸ºç©ºä¸”新值为 0 | ä¸æ–°å¢ž 0,避免把成品页写成满页 0 | `SM_货运` æ— æ•°æ®çš„市州轨道/轮渡值未产生新增 0 æ ¼ | é€šè¿‡ |
| TC-N3 | 2025 åŒæœŸæ— åº“数据 | å–母版台账页 2025 åˆ—缓存,不出现 NaN/Inf | åŒæ¯”全部为有限数(如 `城市汇总 C7=0.4870`、`C14=0.0444`) | é€šè¿‡ |
| TC-N4 | å°è´¦é¡µè¯»ä¸åˆ°å¸‚州数据(极端情况) | è®° `WARN` å¹¶**保持母版原值**、不写坏页 | ä»£ç è·¯å¾„ `curCum.isEmpty()` â†’ `log.warn("…台账页未读到市州数据,跳过(保持母版原值)")`;本轮台账页数据齐全,未实际触发(**代码审查用例**) | é€šè¿‡ï¼ˆå®¡æŸ¥ï¼‰ |
| TC-N5 | æ¯ç‰ˆæ ‡é¢˜ä¸º inlineStr | æ ‡é¢˜å¿…须真正落盘更新,不能只改 `<v>` | `smSetTitle` å…ˆ `setBlank()` å†å†™ï¼Œæ ‡é¢˜æ ¡éªŒå…¨éƒ¨è¯»å–到新文案 | é€šè¿‡ |
### 3.3 å›žå½’用例
| ç¼–号 | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-R1 | å¯¼å‡º `period=2026-07` | ä¸å´©ï¼Œä¸”四页应回退成「1-7 æœˆã€å£å¾„ | HTTP 200 / 942,691 å­—节 / 25.7 ç§’;标题 `城市汇总 A1=2026å¹´1-7月…`、`城市客运客运量分市州1-7月累计情况`、`1-7月中口径客运量及周转量`、`2026å¹´1-7月全省累计完成交通周转量情况` | é€šè¿‡ |
| TC-R2 | 2026-07 å¯¼å‡ºå€¼ vs æ¯ç‰ˆ 1-7 æœˆé™æ€æƒå¨å€¼ | **应完全一致**(证明口径与母版一致,只是随月份延长) | åŸŽå¸‚汇总 B3=237412.33000000002(母版 237412.33)、中口径 B3=27524.4906、道路运输周转量 G4=15222118.44638293、城市分市州 B6=135474.77989442425 â€”— ä¸Žæ¯ç‰ˆé€é¡¹ä¸€è‡´ | é€šè¿‡ |
| TC-R3 | åŽç«¯æœåŠ¡å¯ç”¨æ€§ | å‰ç«¯é¦–页可访问 | `http://localhost:8080/` HTTP 200,标题「交通统计报表审核系统」 | é€šè¿‡ |
### 3.4 é€æ ¼ç‹¬ç«‹å¤æ ¸ï¼ˆå…³é”®è¯æ®ï¼‰
**方法**:从生成件**自身的台账页**重新聚合(不读库、不参考任何其它文件),再与四页逐格比对。
| å¤æ ¸èŒƒå›´ | æ ¼æ•° |
| --- | --- |
| æ ‡é¢˜ï¼ˆåŸŽå¸‚汇总 2 + åŸŽå¸‚分市州 4 + ä¸­å£å¾„ 2 + é“路运输周转量 2) | 10 |
| åŸŽå¸‚汇总 ç´¯è®¡å—(7 è¡Œ Ã— å®¢è¿é‡/同比/周转量/同比) | 28 |
| åŸŽå¸‚汇总 å½“月块(同上) | 28 |
| åŸŽå¸‚分市州 4 å— Ã— 17 å¸‚å·ž Ã— 6 å€¼åˆ— | 408 |
| ä¸­å£å¾„分析 2 å— Ã— 5 è¡Œ Ã— 2 åˆ— | 10 |
| é“路运输周转量 2 å— Ã— 17 å¸‚å·ž Ã— 6 åˆ—(G/L/Q + J/O/T + W/X/Y) | 102 |
| **合计** | **586** |
结果:**比对 586 æ ¼ï¼Œå·®å¼‚ 0 æ ¼**(`_v5_four.txt` æœ«å°¾ã€Œæ±‡æ€»ï¼šæ¯”对格数 = 586,差异格数 = 0」)。
补充自洽校验(全省行 = Î£17 å¸‚州):道路运输周转量 G4=17492129.04788293 = Î£(G5:G21)、L4=269628.07 = Î£(L5:L21)、Q4=1227147.48902 = Î£(Q5:Q21)、W4/X4/Y4 åŒç†ï¼ˆå·® â‰¤ 6e-11,浮点误差级)。
另:13:06 é‚£ä¸€ç‰ˆå½’档件用 `_verify_four.py` å¤æ ¸ï¼ˆå«ã€Œä¸Žæ¯ç‰ˆ 1-7 æœˆé™æ€å€¼å¯¹ç…§ã€å°èŠ‚ï¼‰ï¼ŒåŒæ ·**差异 0 æ ¼**。
与母版静态值的对照(证明「确实从静态变动态」):
| å•元格 | æ¯ç‰ˆï¼ˆ1-7 æœˆé™æ€ï¼‰ | ç”Ÿæˆä»¶ï¼ˆ1-8 æœˆï¼‰ |
| --- | --- | --- |
| åŸŽå¸‚汇总 B3 | 237412.33 | 269628.07 |
| åŸŽå¸‚汇总 A12 | 2026å¹´7月全省完成城市客运量情况 | 2026å¹´8月全省完成城市客运量情况 |
| åŸŽå¸‚分市州 B6 | 135474.779894424 | 153695.70483259356 |
| ä¸­å£å¾„分析 B3 | 27524.4906 | 31528.060700000005 |
| é“路运输周转量 G4 | 15222118.4463982 | 17492129.04788293 |
## å››ã€å‘现的问题与处置
1. **母版标题是 inlineStr**:POI ç›´æŽ¥ `setCellValue` åªå†™ `<v>`、不改 `<is>`,Excel ä»æ˜¾ç¤ºæ—§æ–‡æ¡ˆï¼ˆ7 æœˆï¼‰ã€‚→ `smSetTitle` æ”¹ä¸ºå…ˆ `setBlank()` å†å†™ã€‚
2. **成品页里混有母版公式**(城市分市州全省行、道路运输周转量 J åˆ—同比等):若一律写值会破坏母版自洽公式。→ `smSetVal` é‡ `FORMULA` ç›´æŽ¥è·³è¿‡ï¼Œå†™å…¥æ ¼æ•° 1056 å°äºŽå››é¡µç†è®ºæ ¼æ•°å³ä¸ºæ­¤ã€‚
3. **城市汇总第 5 è¡Œæ˜¯ã€Œå‡ºç§Ÿ+网约车」合计**,不是单出租。→ ä»£ç æŒ‰ã€Œå‡ºç§Ÿ + ç½‘约车」写入(母版第 6 è¡Œæ‰æ˜¯å•出租),与母版口径一致。
4. **中口径的「城际城乡」不能直接取台账总量**。→ æŒ‰ã€Œæ€»é‡ âˆ’ åŸŽå¸‚内」计算(仅公交/出租/网约车)。
5. **库中无 2025 å¹´åŸŽå¸‚客运/网约车数据**。→ åŒæ¯”基数取母版台账页 2025 å¹´é€æœˆåˆ—缓存值(方案 A çš„æ ¸å¿ƒï¼‰ï¼ŒåŽç»­ 2025 æ•°æ®å…¥åº“后可再切换为读库。
## äº”、结论
- æ­£å‘ 10 ä¾‹ã€è´Ÿå‘ 5 ä¾‹ã€å›žå½’ 3 ä¾‹ã€é€æ ¼ç‹¬ç«‹å¤æ ¸ 586 æ ¼ï¼Œå…¨éƒ¨é€šè¿‡ã€‚
- å››å¼ è·¨é¡µåˆæˆé¡µå·²ä»Ž**静态**变为**随报表期数据驱动**:导出 2026-08 æ—¶æ ‡é¢˜ä¸Žæ•°å€¼å‡ä¸º 1-8 æœˆ/8 æœˆå£å¾„;导出 2026-07 æ—¶åˆèƒ½ç²¾ç¡®å›žé€€æˆä¸Žæ¯ç‰ˆä¸€è‡´çš„ 1-7 æœˆæƒå¨å€¼ã€‚
- æ¯ç‰ˆå…¬å¼ä¿ç•™ã€åŒæ¯”基数沿用母版 2025 ç¼“存,报表打开后仍走母版自带的重算链路(`fullCalcOnLoad`)。
- é—留:中口径排名页的「同比增速」列仍是母版旧值(库中无 2025 å¹´ä¸­å£å¾„定稿数据,算不出同比),待 2025 æ•°æ®å…¥åº“后刷新。
docs/¹¤×÷ÈÕ½á/¹¤×÷ÈÕ½á_2026-09-13.md
New file
@@ -0,0 +1,60 @@
# å·¥ä½œæ—¥ç»“_2026-09-13
> è¡¥è®°ï¼š2026-09-12 æ— ä»£ç æ”¹åŠ¨ã€æ— æäº¤ï¼ˆ`git log --since=2026-09-12` ä¸ºç©ºï¼‰ï¼Œæ•…未单独留档。
## ä¸€ã€ä»Šå¤©åšäº†ä»€ä¹ˆ
### 1. æ±‡æ€»å¤§è¡¨ã€Œå››é¡µé™æ€ã€é—®é¢˜å®šä½ä¸Žæ–¹æ¡ˆç¡®è®¤
- ç”¨æˆ·åé¦ˆï¼šå¯¼å‡º 2026-08 æ±‡æ€»å¤§è¡¨åŽï¼Œ`城市汇总`、`城市分市州`、`中口径分析`、`道路运输周转量` å››é¡µæ ‡é¢˜ä»å†™ã€Œ1-7月/7月」、数值也是 7 æœˆé™æ€å€¼ã€‚
- ç»™å‡ºä¸‰å¥—方案(A æ²¿ç”¨æ¯ç‰ˆ 2025 ç¼“存做同比基数的快路子 / B ç­‰ 2025 æ•°æ®å…¥åº“ / C åªåšå½“月块),用户选定 **方案 A**。
### 2. å£å¾„反推(先证后写)
- æŠŠæ¯ç‰ˆå°è´¦é¡µï¼ˆ`公交`/`出租`/`网约车`/`轨道、轮渡`/`班线包车`/` è´§è¿`)逐格聚合,反推四页口径,与四页成品**逐格 0 å·®å¼‚**后才动代码。
- ç¡®è®¤å£å¾„:城市客运量 = å…¬äº¤åŸŽå¸‚内 + å‡ºç§ŸåŸŽå¸‚内 + ç½‘约车城市内 + è½¨é“ + è½®æ¸¡ï¼›ä¸­å£å¾„ = ç­çº¿åŒ…车 + (总量 âˆ’ åŸŽå¸‚内)×(公交/出租/网约车);道路货运周转量 = è§„上 + è§„下;同比 = å½“期 Ã· æ¯ç‰ˆå°è´¦é¡µ 2025 åŒæœŸåˆ— âˆ’ 1(库中无 2025 åŸŽå¸‚客运数据,故取母版缓存)。
### 3. ä»£ç å®žçŽ°ï¼ˆå”¯ä¸€æ”¹åŠ¨æ–‡ä»¶ï¼‰
- `traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java`(+494 âˆ’2):
  - æ–°å¢ž `refreshSummaryCumulativeSheets(wb, year, month)`,插在 `refreshSummaryRankSheets` ä¹‹åŽã€`applySummaryReferenceLayout` ä¹‹å‰ï¼›
  - å°è´¦å–æ•° `smReadLedgerMetrics` å…¨éƒ¨**按行标签识别**(不写死行号),19 ç»´æŒ‡æ ‡ä½ï¼›2026 é€æœˆåˆ— C…Q、2025 åŒæœŸåˆ— V…AJ;
  - å†™å€¼ `smSetVal` é‡ç›®æ ‡æ ¼æ˜¯å…¬å¼**直接跳过**(保留母版公式,打开重算)、0 ä¸”原为空格不新增;`smSetTitle` å…ˆ `setBlank()` å†å†™ï¼ˆæ¯ç‰ˆæ ‡é¢˜æ˜¯ inlineStr,POI åªå†™ `<v>` ä¸æ›´æ–° `<is>` çš„坑)。
- å››é¡µéšæŠ¥è¡¨æœŸå†™å…¥ **1056 æ ¼**。
### 4. ç¼–译、重启与实测
- `mvn -o -q compile` é€šè¿‡ï¼›`start-dev.ps1` é‡å¯åŽç«¯ï¼ˆPID 27840)与前端(PID 23416)均就绪。
- å¯¼å‡º 2026-08:HTTP 200 / 950,783 å­—节 / 36.8 ç§’;日志「刷新四页…数值格数=1056、刷新排名页数值格数=711、缺数提示=[]」,最近 4MB æ—¥å¿—æ—  ERROR/WARN。
- å½’档件:`docs/生成汇总大表/输出/生成_道路运输量汇总表_2026-08_四页数据驱动_2026-09-13.xlsx`。
### 5. ç‹¬ç«‹å¤æ ¸ï¼ˆä»Žç”Ÿæˆä»¶è‡ªèº«å°è´¦é¡µé‡æ–°èšåˆï¼‰
- æ–°å†™ `_v5_four.py`:覆盖标题 10 æ ¼ + åŸŽå¸‚汇总累计块 28 æ ¼ + å½“月块 28 æ ¼ + åŸŽå¸‚分市州 408 æ ¼ + ä¸­å£å¾„ 10 æ ¼ + é“路运输周转量 102 æ ¼ = **586 æ ¼ï¼Œå·®å¼‚ 0 æ ¼**。
- å¦ç”¨ `_verify_four.py` å¤æ ¸ 13:06 å½’档件(含与母版 1-7 æœˆé™æ€å€¼å¯¹ç…§ï¼‰ï¼ŒåŒæ · **0 å·®å¼‚**。
- å…¨çœè¡Œè‡ªæ´½ï¼šG4/L4/Q4/W4/X4/Y4 = Î£17 å¸‚州(差 â‰¤ 6e-11)。
### 6. æ–‡æ¡£
- ã€ŠåŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_汇总大表四页数据驱动_2026-09-13.md》写入 `docs/功能测试报告/`。
- æœ¬ã€Šå·¥ä½œæ—¥ç»“》。
## äºŒã€æµ‹è¯•结果摘要(详见《功能测试报告》)
| é¡¹ | ç»“æžœ |
| --- | --- |
| å¯¼å‡º 2026-08 | HTTP 200 / 950,783 å­—节 / 36.8 ç§’;两次导出字节数一致(确定性) |
| å››é¡µæ ‡é¢˜ | åŸŽå¸‚汇总、城市分市州(4 å—)、中口径分析(2 å—)、道路运输周转量(2 å—)全部更新为 1-8月/8月 |
| é€æ ¼å¤æ ¸ | 586 æ ¼ï¼Œå·®å¼‚ 0 æ ¼ï¼ˆä»Žç”Ÿæˆä»¶è‡ªèº«å°è´¦é¡µç‹¬ç«‹é‡ç®—) |
| æœˆä»½å›žé€€ï¼ˆå›žå½’) | å¯¼å‡º 2026-07 æ—¶å››é¡µç²¾ç¡®å›žé€€æˆæ¯ç‰ˆ 1-7 æœˆæƒå¨å€¼ï¼šåŸŽå¸‚汇总 B3=237412.33、中口径 B3=27524.4906、周转量 G4=15222118.44638293、城市分市州 B6=135474.77989442425 â€”— ä¸Žæ¯ç‰ˆé€é¡¹ä¸€è‡´ |
| è´Ÿå‘ | æ¯ç‰ˆå…¬å¼æ ¼æœªè¢«è¦†ç›–(`城市分市州!B5`、`道路运输周转量!J4` ä»ä¸ºå…¬å¼ï¼‰ï¼›2025 æ— åº“数据时同比取母版缓存、不出现 NaN/Inf;空+0 ä¸æ–°å¢ž |
| å‰ç«¯ | `http://localhost:8080/` HTTP 200(首页正常) |
## ä¸‰ã€é—留问题与待办
1. **中口径排名页「同比增速」列仍是母版旧值**:库中只有 2026 å¹´æ•°æ®ã€æ²¡æœ‰ 2025 å¹´ä¸­å£å¾„定稿数据,算不出 8 æœˆåŒæ¯”;等 2025 æ•°æ®å…¥åº“后自动刷新。
2. **网约车 8 æœˆè®¢å•数疑似未更新**:库中 `wyc_order_monthly` çš„ 2026-07 ä¸Ž 2026-08 `order_sum` éƒ½æ˜¯ 29,730,791(7、8 æœˆç›¸åŒï¼‰ï¼Œè¯·ç”¨æˆ·æ ¸å¯¹æ˜¯å¦é‡ä¼ è¿‡çœŸå®ž 8 æœˆè¿è¡Œç›‘测报告。
3. **根目录临时文件**:`_*.py` / `_*.sql` / `_*.xlsx` / `_*.txt` / `_bak_*.java` ç­‰ 100+ ä¸ªæœªè·Ÿè¸ªæ–‡ä»¶ï¼ˆç”¨æˆ·æœªåŒæ„å‰ä¸æ¸…理、不提交)。
4. `docs/生成汇总大表/生成_道路运输量汇总表.xlsx`(版式参照件)与 `docs/生成汇总大表/输出/` ä¸‹çš„历史产物均为未跟踪文件,暂按惯例不纳入提交。
5. 2026-08 å¯¼å‡ºçš„「未找到同期备份母版」告警属无害提示,是否消除待用户定。
## å››ã€æ˜Žå¤©è®¡åˆ’
1. ç”¨æˆ·æ ¸å¯¹/重传真实 8 æœˆç½‘约车运行监测报告后,重导 2026-08 å¹¶å¤æ ¸ç½‘约车页。
2. æŒ‰ç”¨æˆ·åé¦ˆç»§ç»­æ ¸å¯¹å››é¡µæˆå“ï¼ˆé‡ç‚¹ï¼šåŸŽå¸‚分市州增速列、道路运输周转量同期列口径是否符合业务预期)。
3. è·Ÿè¿› 2025 å¹´åŸŽå¸‚客运/中口径数据入库,入库后把四页同比从「母版缓存」切到「读库」。
4. è§†ç”¨æˆ·æ„æ„¿æ¸…理根目录临时文件。
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -3353,10 +3353,12 @@
            int yoyFilled = summaryWorkbookFiller.ensureMonthYoyFormulas(wb, year, effMonth);
            int monthFormulaFilled = summaryWorkbookFiller.ensureMonthFormulas(wb, year, effMonth);
            int rankRefreshed = refreshSummaryRankSheets(wb, effPeriod, "month");
            int cumulativeRefreshed = refreshSummaryCumulativeSheets(wb, year, effMonth); // å››å¼ è·¨é¡µåˆæˆé¡µéšæŠ¥è¡¨æœŸåˆ·æ–°
            int widthApplied = applySummaryReferenceLayout(wb);
            log.info("汇总工作簿数据驱动回填完成:写入当月值格数={},补写当月同比公式格数={},补写当月合计公式格数={},"
                    + "刷新排名页数值格数={},按《生成_道路运输量汇总表》对齐列宽列数={},缺数提示={}",
                    dbFilled, yoyFilled, monthFormulaFilled, rankRefreshed, widthApplied, dbProblems);
                    + "刷新排名页数值格数={},刷新四页(城市汇总/城市分市州/中口径分析/道路运输周转量)数值格数={},"
                    + "按《生成_道路运输量汇总表》对齐列宽列数={},缺数提示={}",
                    dbFilled, yoyFilled, monthFormulaFilled, rankRefreshed, cumulativeRefreshed, widthApplied, dbProblems);
            applyTwoDecimalFormat(wb); // æ•°å€¼æ˜¾ç¤ºä¸¤ä½å°æ•°ï¼ˆä¿ç•™å…¨ç²¾åº¦ï¼‰ï¼›% / æ—¥æœŸç­‰æ—¢æœ‰æ ·å¼ä¸æ”¹å˜
            if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr();
            wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // è¿˜åŽŸ/改写公式后打开即重算
@@ -3654,6 +3656,496 @@
    }
    /** å®šä½ç‰ˆå¼å‚ç…§ä»¶ docs/生成汇总大表/生成_道路运输量汇总表.xlsx(优先级同母版:配置目录 â†’ user.dir ç›¸å¯¹ â†’ ä¸Šçº§ï¼‰ */
    // ==================== æ±‡æ€»å¤§è¡¨ï¼šå››å¼ â€œè·¨é¡µåˆæˆâ€é¡µéšæŠ¥è¡¨æœŸåˆ·æ–° ====================
    // åŸŽå¸‚汇总 / åŸŽå¸‚分市州 / ä¸­å£å¾„分析 / é“路运输周转量 æ­¤å‰æ•´é¡µæ²¿ç”¨æ¯ç‰ˆé™æ€å€¼ï¼ˆæ ‡é¢˜åœåœ¨ 1-7 æœˆï¼‰ã€‚
    // æœ¬æ®µæŒ‰ 2026-09-13 å·²éªŒè¯å£å¾„(与 8 æœˆæ¯ç‰ˆé€æ ¼ 0 å·®å¼‚)从母版台账页现算:
    //   åŸŽå¸‚客运量     = åŸŽå¸‚内公交 + åŸŽå¸‚内巡游出租 + åŸŽå¸‚内网约车 + è½¨é“ + è½®æ¸¡
    //   é“路客运周转量 = å…¬è·¯ç­çº¿ + åŸŽé™…城乡公交 + åŸŽé™…城乡巡游出租 + åŸŽé™…城乡网约车
    //   é“路货运周转量 = è´§è¿é¡µ å¸‚å·ž è§„上货物周转量 + è§„下货物周转量
    // 2026 å¹´ 1..N æœˆå–台账页 2026 åˆ—(C/E/G…),去年同期取母版缓存 2025 åˆ—(V/X/Z…)——库里没有 2025 å¹´åŸŽå¸‚客运数据。
    /** åŸŽå¸‚客运口径指标位(0..17 åŸŽå¸‚客运,18 è´§è¿å‘¨è½¬é‡ï¼‰ */
    private static final int SM_BUS_PAX = 0, SM_BUS_TURN = 1;
    private static final int SM_TAXI_PAX = 2, SM_TAXI_TURN = 3;
    private static final int SM_WYC_PAX = 4, SM_WYC_TURN = 5;
    private static final int SM_RAIL_PAX = 6, SM_RAIL_TURN = 7;
    private static final int SM_FERRY_PAX = 8, SM_FERRY_TURN = 9;
    private static final int SM_BX_PAX = 10, SM_BX_TURN = 11;
    private static final int SM_BUS_CX_PAX = 12, SM_BUS_CX_TURN = 13;
    private static final int SM_TAXI_CX_PAX = 14, SM_TAXI_CX_TURN = 15;
    private static final int SM_WYC_CX_PAX = 16, SM_WYC_CX_TURN = 17;
    private static final int SM_FREIGHT_TURN = 18;
    private static final int SM_DIM = 19;
    /** 2026 å¹´ 1 æœˆå½“月值列(0 åŸºï¼ŒC åˆ—);2025 å¹´ 1 æœˆå½“月值列(0 åŸºï¼ŒV åˆ—) */
    private static final int SM_CUR_COL0 = 2;
    private static final int SM_LAST_COL0 = 21;
    /**
     * åˆ·æ–°å››å¼ è·¨é¡µåˆæˆé¡µï¼šå½“年累计(1..month æœˆï¼‰ã€å½“月(month æœˆï¼‰åŠå¯¹åº”同比;
     * 2026 ä¸Ž 2025 åŒæœŸéƒ½å–自母版台账页缓存,返回写入单元格数。
     */
    private int refreshSummaryCumulativeSheets(XSSFWorkbook wb, int year, int month) {
        if (wb == null || month <= 0) return 0;
        int[] cumMonths = new int[month];
        for (int m = 1; m <= month; m++) cumMonths[m - 1] = m;
        int[] oneMonth = new int[]{month};
        Map<String, double[]> curCum = smReadLedgerMetrics(wb, true, cumMonths);
        Map<String, double[]> lastCum = smReadLedgerMetrics(wb, false, cumMonths);
        Map<String, double[]> curMon = smReadLedgerMetrics(wb, true, oneMonth);
        Map<String, double[]> lastMon = smReadLedgerMetrics(wb, false, oneMonth);
        if (curCum.isEmpty()) {
            log.warn("汇总大表四页刷新:台账页未读到市州数据,跳过(保持母版原值)");
            return 0;
        }
        double[] pCurCum = smProvinceSum(curCum);
        double[] pLastCum = smProvinceSum(lastCum);
        double[] pCurMon = smProvinceSum(curMon);
        double[] pLastMon = smProvinceSum(lastMon);
        int n = 0;
        n += smWriteCitySummary(wb, year, month, pCurCum, pLastCum, pCurMon, pLastMon);
        n += smWriteCityByRegion(wb, month, curCum, lastCum, curMon, lastMon);
        n += smWriteMidAnalysis(wb, month, pCurCum, pLastCum, pCurMon, pLastMon);
        n += smWriteTurnoverSheets(wb, year, month, curCum, lastCum, curMon, lastMon);
        return n;
    }
    /** 17 å¸‚州合计(不含台账页“全省”公式行) */
    private double[] smProvinceSum(Map<String, double[]> m) {
        double[] p = new double[SM_DIM];
        for (String c : RegionUtil.cityList()) {
            double[] a = m.get(c);
            if (a == null) continue;
            for (int i = 0; i < SM_DIM; i++) p[i] += a[i];
        }
        return p;
    }
    /** å°è´¦é¡µç´¯è®¡å–数:市州规范名 -> 19 ç»´æŒ‡æ ‡ */
    private Map<String, double[]> smReadLedgerMetrics(XSSFWorkbook wb, boolean curYear, int[] months) {
        Map<String, double[]> out = new LinkedHashMap<>();
        smReadCityPaxPage(wb, "公交", curYear, months, out, SM_BUS_PAX, SM_BUS_TURN, SM_BUS_CX_PAX, SM_BUS_CX_TURN);
        smReadCityPaxPage(wb, "出租", curYear, months, out, SM_TAXI_PAX, SM_TAXI_TURN, SM_TAXI_CX_PAX, SM_TAXI_CX_TURN);
        smReadCityPaxPage(wb, "网约车", curYear, months, out, SM_WYC_PAX, SM_WYC_TURN, SM_WYC_CX_PAX, SM_WYC_CX_TURN);
        smReadRailFerry(wb, curYear, months, out);
        smReadBanxian(wb, curYear, months, out);
        smReadFreightTurnover(wb, curYear, months, out);
        return out;
    }
    /** å…¬äº¤/出租/网约车页:每市州 4 è¡Œï¼ˆå®¢è¿é‡/旅客周转量/其中城市内客运量/其中城市内旅客周转量) */
    private void smReadCityPaxPage(XSSFWorkbook wb, String sheetName, boolean curYear, int[] months,
                                   Map<String, double[]> out, int paxIdx, int turnIdx, int cxPaxIdx, int cxTurnIdx) {
        Sheet sh = wb.getSheet(sheetName);
        if (sh == null) return;
        int last = sh.getLastRowNum();
        for (int r = sh.getFirstRowNum(); r <= last; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String city = smCityOf(row.getCell(0));
            if (city == null) continue;
            int paxRow = -1, turnRow = -1, cxPaxRow = -1, cxTurnRow = -1;
            for (int k = r; k <= Math.min(last, r + 4); k++) {
                Row kr = sh.getRow(k);
                if (kr == null) continue;
                String b = smText(kr.getCell(1));
                if (b == null || b.contains("个体")) continue;
                boolean inner = b.contains("城市内");
                boolean turn = b.contains("周转量");
                if (inner) {
                    if (turn) { if (cxTurnRow < 0) cxTurnRow = k; }
                    else if (cxPaxRow < 0) cxPaxRow = k;
                } else {
                    if (turn) { if (turnRow < 0) turnRow = k; }
                    else if (paxRow < 0) paxRow = k;
                }
            }
            double totalPax = smSumMonths(sh, paxRow, curYear, months);
            double totalTurn = smSumMonths(sh, turnRow, curYear, months);
            double cityPax = smSumMonths(sh, cxPaxRow, curYear, months);
            double cityTurn = smSumMonths(sh, cxTurnRow, curYear, months);
            double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]);
            arr[paxIdx] += cityPax;                 // åŸŽå¸‚客运口径取“城市内”
            arr[turnIdx] += cityTurn;
            arr[cxPaxIdx] += totalPax - cityPax;    // åŸŽé™…城乡 = æ€»é‡ - åŸŽå¸‚内
            arr[cxTurnIdx] += totalTurn - cityTurn;
        }
    }
    /** è½¨é“、轮渡页:轨道块(武汉/黄石)+ è½®æ¸¡å—(武汉) */
    private void smReadRailFerry(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) {
        Sheet sh = wb.getSheet("轨道、轮渡");
        if (sh == null) return;
        int last = sh.getLastRowNum();
        int ferryTitle = -1;
        for (int r = sh.getFirstRowNum(); r <= last; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String a = smText(row.getCell(0));
            if (a != null && a.contains("轮渡客运量")) { ferryTitle = r; break; }
        }
        smReadRailFerryBlock(sh, curYear, months, out, sh.getFirstRowNum(),
                ferryTitle < 0 ? last : ferryTitle - 1, SM_RAIL_PAX);
        if (ferryTitle >= 0) smReadRailFerryBlock(sh, curYear, months, out, ferryTitle, last, SM_FERRY_PAX);
    }
    private void smReadRailFerryBlock(Sheet sh, boolean curYear, int[] months, Map<String, double[]> out,
                                      int start, int end, int baseIdx) {
        for (int r = start; r <= end; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String city = smCityOf(row.getCell(0));
            if (city == null) continue;
            int paxRow = -1, turnRow = -1;
            for (int k = r; k <= Math.min(end, r + 3); k++) {
                Row kr = sh.getRow(k);
                if (kr == null) continue;
                String b = smText(kr.getCell(1));
                if (b == null) continue;
                if (b.contains("周转量")) { if (turnRow < 0) turnRow = k; }
                else if (paxRow < 0) paxRow = k;
            }
            double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]);
            arr[baseIdx] += smSumMonths(sh, paxRow, curYear, months);
            arr[baseIdx + 1] += smSumMonths(sh, turnRow, curYear, months);
        }
    }
    /** ç­çº¿åŒ…车页:每市州 å®¢è¿é‡ / æ—…客周转量(“其中个体”行不重复计入) */
    private void smReadBanxian(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) {
        Sheet sh = wb.getSheet("班线包车");
        if (sh == null) return;
        int last = sh.getLastRowNum();
        for (int r = sh.getFirstRowNum(); r <= last; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String city = smCityOf(row.getCell(0));
            if (city == null) continue;
            int paxRow = -1, turnRow = -1;
            for (int k = r; k <= Math.min(last, r + 4); k++) {
                Row kr = sh.getRow(k);
                if (kr == null) continue;
                String b = smText(kr.getCell(1));
                if (b == null || b.contains("个体")) continue;
                if (b.contains("周转量")) { if (turnRow < 0) turnRow = k; }
                else if (b.contains("客运量") && paxRow < 0) paxRow = k;
            }
            double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]);
            arr[SM_BX_PAX] += smSumMonths(sh, paxRow, curYear, months);
            arr[SM_BX_TURN] += smSumMonths(sh, turnRow, curYear, months);
        }
    }
    /** è´§è¿é¡µï¼šæ¯å¸‚å·ž è§„上货物周转量 + è§„下货物周转量(“规上+规下”行本身是求和公式,不直接取) */
    private void smReadFreightTurnover(XSSFWorkbook wb, boolean curYear, int[] months, Map<String, double[]> out) {
        Sheet sh = wb.getSheet(" è´§è¿");
        if (sh == null) sh = wb.getSheet("货运");
        if (sh == null) return;
        int last = sh.getLastRowNum();
        for (int r = sh.getFirstRowNum(); r <= last; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String city = smCityOf(row.getCell(0));
            if (city == null) continue;
            int above = -1, below = -1;
            for (int k = r; k <= Math.min(last, r + 6); k++) {
                Row kr = sh.getRow(k);
                if (kr == null) continue;
                String b = smText(kr.getCell(1));
                if (b == null || b.contains("其中")) continue;
                if (b.contains("规上货物周转量")) { if (above < 0) above = k; }
                else if (b.contains("规下货物周转量")) { if (below < 0) below = k; }
            }
            double[] arr = out.computeIfAbsent(city, k -> new double[SM_DIM]);
            arr[SM_FREIGHT_TURN] += smSumMonths(sh, above, curYear, months) + smSumMonths(sh, below, curYear, months);
        }
    }
    private double smSumMonths(Sheet sh, int rowIdx, boolean curYear, int[] months) {
        if (sh == null || rowIdx < 0) return 0;
        Row row = sh.getRow(rowIdx);
        if (row == null) return 0;
        double s = 0;
        for (int m : months) {
            int col = (curYear ? SM_CUR_COL0 : SM_LAST_COL0) + 2 * (m - 1);
            s += smNum(row.getCell(col));
        }
        return s;
    }
    /** å°è´¦é¡µå•元格取数(数字 / æ•°å­—文本 / å…¬å¼ç¼“存) */
    private double smNum(Cell c) {
        if (c == null) return 0;
        try {
            CellType t = c.getCellType();
            if (t == CellType.NUMERIC) return c.getNumericCellValue();
            if (t == CellType.FORMULA) {
                return c.getCachedFormulaResultType() == CellType.NUMERIC ? c.getNumericCellValue() : 0;
            }
            if (t == CellType.STRING) {
                String s = c.getStringCellValue();
                if (s == null) return 0;
                s = s.replace(",", "").replace("%", "").trim();
                if (s.isEmpty()) return 0;
                return Double.parseDouble(s);
            }
        } catch (Exception ignore) {
            return 0;
        }
        return 0;
    }
    private String smText(Cell c) {
        if (c == null) return null;
        try {
            return c.getCellType() == CellType.STRING ? c.getStringCellValue() : null;
        } catch (Exception e) {
            return null;
        }
    }
    /** åˆ— A æ–‡æ¡ˆ -> è§„范市州名(“全省/湖北省”与非市州行返回 null) */
    private String smCityOf(Cell a) {
        String t = smText(a);
        if (t == null || t.trim().isEmpty()) return null;
        String norm = RegionUtil.normalizeCityName(t.trim());
        if (norm == null) return null;
        return RegionUtil.cityList().contains(norm) ? norm : null;
    }
    private double smYoy(double cur, double last) {
        return last == 0.0 ? 0.0 : (cur - last) / last;
    }
    /** å†™å€¼ï¼šå…¬å¼æ ¼ä¸åŠ¨ï¼ˆä¿ç•™æ¯ç‰ˆå…¬å¼ï¼Œæ‰“å¼€å³é‡ç®—ï¼‰ï¼›0 ä¸”原为空格则不新增 */
    private int smSetVal(Sheet sh, int r0, int c0, double v) {
        Row row = sh.getRow(r0);
        if (row == null) return 0;
        Cell c = row.getCell(c0);
        if (c != null && c.getCellType() == CellType.FORMULA) return 0;
        if (c == null) {
            if (v == 0.0) return 0;
            c = row.createCell(c0);
            Cell left = row.getCell(c0 - 1);
            if (left != null) c.setCellStyle(left.getCellStyle());
        } else if (v == 0.0 && c.getCellType() == CellType.BLANK) {
            return 0;
        }
        c.setBlank();
        c.setCellValue(v);
        return 1;
    }
    private void smSetTitle(Sheet sh, int r0, int c0, String text) {
        if (sh == null) return;
        Row row = sh.getRow(r0);
        if (row == null) return;
        Cell c = row.getCell(c0);
        if (c == null || c.getCellType() == CellType.FORMULA) return;
        c.setBlank();
        c.setCellValue(text);
    }
    /** åŸŽå¸‚汇总页:块1(1..N æœˆç´¯è®¡ï¼‰è¡Œ 3-9,块2(当月)行 14-20;列 B/C/D/E = å®¢è¿é‡/同比/周转量/同比 */
    private int smWriteCitySummary(XSSFWorkbook wb, int year, int month,
                                   double[] pCurCum, double[] pLastCum, double[] pCurMon, double[] pLastMon) {
        Sheet sh = wb.getSheet("城市汇总");
        if (sh == null) return 0;
        smSetTitle(sh, 0, 0, year + "å¹´1-" + month + "月全省累计完成城市客运量情况");
        smSetTitle(sh, 11, 0, year + "å¹´" + month + "月全省完成城市客运量情况");
        int n = 0;
        int[] rows = {2, 3, 4, 5, 6, 7, 8};
        for (int i = 0; i < rows.length; i++) {
            double[] v = smCitySummaryRow(pCurCum, pLastCum, i);
            n += smSetVal(sh, rows[i], 1, v[0]);
            n += smSetVal(sh, rows[i], 2, v[2]);
            n += smSetVal(sh, rows[i], 3, v[1]);
            n += smSetVal(sh, rows[i], 4, v[3]);
        }
        for (int i = 0; i < rows.length; i++) {
            double[] v = smCitySummaryRow(pCurMon, pLastMon, i);
            n += smSetVal(sh, rows[i] + 11, 1, v[0]);
            n += smSetVal(sh, rows[i] + 11, 2, v[2]);
            n += smSetVal(sh, rows[i] + 11, 3, v[1]);
            n += smSetVal(sh, rows[i] + 11, 4, v[3]);
        }
        return n;
    }
    /** i=0 æ€»å®¢è¿é‡ 1 å…¬äº¤ 2 å‡ºç§Ÿè½¦ 3 å·¡æ¸¸å‡ºç§Ÿ 4 ç½‘约车 5 è½¨é“ 6 è½®æ¸¡ï¼›è¿”回 [客运量, å‘¨è½¬é‡, å®¢è¿é‡åŒæ¯”, å‘¨è½¬é‡åŒæ¯”] */
    private double[] smCitySummaryRow(double[] p, double[] pl, int i) {
        double pax, turn, lPax, lTurn;
        if (i == 0) {
            pax = p[SM_BUS_PAX] + p[SM_TAXI_PAX] + p[SM_WYC_PAX] + p[SM_RAIL_PAX] + p[SM_FERRY_PAX];
            turn = p[SM_BUS_TURN] + p[SM_TAXI_TURN] + p[SM_WYC_TURN] + p[SM_RAIL_TURN] + p[SM_FERRY_TURN];
            lPax = pl[SM_BUS_PAX] + pl[SM_TAXI_PAX] + pl[SM_WYC_PAX] + pl[SM_RAIL_PAX] + pl[SM_FERRY_PAX];
            lTurn = pl[SM_BUS_TURN] + pl[SM_TAXI_TURN] + pl[SM_WYC_TURN] + pl[SM_RAIL_TURN] + pl[SM_FERRY_TURN];
        } else if (i == 1) {
            pax = p[SM_BUS_PAX]; turn = p[SM_BUS_TURN]; lPax = pl[SM_BUS_PAX]; lTurn = pl[SM_BUS_TURN];
        } else if (i == 2) {
            pax = p[SM_TAXI_PAX] + p[SM_WYC_PAX]; turn = p[SM_TAXI_TURN] + p[SM_WYC_TURN];
            lPax = pl[SM_TAXI_PAX] + pl[SM_WYC_PAX]; lTurn = pl[SM_TAXI_TURN] + pl[SM_WYC_TURN];
        } else if (i == 3) {
            pax = p[SM_TAXI_PAX]; turn = p[SM_TAXI_TURN]; lPax = pl[SM_TAXI_PAX]; lTurn = pl[SM_TAXI_TURN];
        } else if (i == 4) {
            pax = p[SM_WYC_PAX]; turn = p[SM_WYC_TURN]; lPax = pl[SM_WYC_PAX]; lTurn = pl[SM_WYC_TURN];
        } else if (i == 5) {
            pax = p[SM_RAIL_PAX]; turn = p[SM_RAIL_TURN]; lPax = pl[SM_RAIL_PAX]; lTurn = pl[SM_RAIL_TURN];
        } else {
            pax = p[SM_FERRY_PAX]; turn = p[SM_FERRY_TURN]; lPax = pl[SM_FERRY_PAX]; lTurn = pl[SM_FERRY_TURN];
        }
        return new double[]{pax, turn, smYoy(pax, lPax), smYoy(turn, lTurn)};
    }
    /** åŸŽå¸‚分市州页:4 å—(累计客运量/累计周转量/当月客运量/当月周转量),每块 å…¨çœ 1 è¡Œ + 17 å¸‚å·ž */
    private int smWriteCityByRegion(XSSFWorkbook wb, int month,
                                    Map<String, double[]> curCum, Map<String, double[]> lastCum,
                                    Map<String, double[]> curMon, Map<String, double[]> lastMon) {
        Sheet sh = wb.getSheet("城市分市州");
        if (sh == null) return 0;
        smSetTitle(sh, 0, 0, "城市客运客运量分市州1-" + month + "月累计情况");
        smSetTitle(sh, 22, 0, "城市客运客运周转量分市州1-" + month + "月累计情况");
        smSetTitle(sh, 45, 0, "城市客运客运量分市州" + month + "月情况");
        smSetTitle(sh, 67, 0, "城市客运客运周转量分市州" + month + "月情况");
        int n = 0;
        n += smWriteCityByRegionBlock(sh, 4, true, curCum, lastCum);
        n += smWriteCityByRegionBlock(sh, 26, false, curCum, lastCum);
        n += smWriteCityByRegionBlock(sh, 49, true, curMon, lastMon);
        n += smWriteCityByRegionBlock(sh, 71, false, curMon, lastMon);
        return n;
    }
    private int smWriteCityByRegionBlock(Sheet sh, int provRow, boolean pax,
                                         Map<String, double[]> cur, Map<String, double[]> last) {
        int n = 0;
        double[] provCur = smProvinceSum(cur);
        double[] provLast = smProvinceSum(last);
        List<String> cities = RegionUtil.cityList();
        for (int i = -1; i < cities.size(); i++) {
            int r = provRow + i + 1;
            double[] c = i < 0 ? provCur : cur.get(cities.get(i));
            double[] l = i < 0 ? provLast : last.get(cities.get(i));
            if (c == null) c = new double[SM_DIM];
            if (l == null) l = new double[SM_DIM];
            double[] g = smCityGroupValues(c, l, pax);
            n += smSetVal(sh, r, 1, g[0]);
            n += smSetVal(sh, r, 4, g[6]);
            n += smSetVal(sh, r, 6, g[1]);
            n += smSetVal(sh, r, 9, g[7]);
            n += smSetVal(sh, r, 11, g[2]);
            n += smSetVal(sh, r, 14, g[8]);
            n += smSetVal(sh, r, 16, g[3]);
            n += smSetVal(sh, r, 19, g[9]);
            n += smSetVal(sh, r, 21, g[4]);
            n += smSetVal(sh, r, 22, g[10]);
            n += smSetVal(sh, r, 23, g[5]);
            n += smSetVal(sh, r, 24, g[11]);
        }
        return n;
    }
    /** 6 ç»„值 + 6 ç»„增速:[总量, å…¬äº¤, å‡ºç§Ÿ, ç½‘约车, è½¨é“, è½®æ¸¡, æ€»é‡å¢žé€Ÿ, å…¬äº¤â€¦, è½®æ¸¡] */
    private double[] smCityGroupValues(double[] c, double[] l, boolean pax) {
        int busI = pax ? SM_BUS_PAX : SM_BUS_TURN;
        int taxiI = pax ? SM_TAXI_PAX : SM_TAXI_TURN;
        int wycI = pax ? SM_WYC_PAX : SM_WYC_TURN;
        int railI = pax ? SM_RAIL_PAX : SM_RAIL_TURN;
        int ferryI = pax ? SM_FERRY_PAX : SM_FERRY_TURN;
        double bus = c[busI], taxi = c[taxiI], wyc = c[wycI], rail = c[railI], ferry = c[ferryI];
        double lBus = l[busI], lTaxi = l[taxiI], lWyc = l[wycI], lRail = l[railI], lFerry = l[ferryI];
        double total = bus + taxi + wyc + rail + ferry;
        double lTotal = lBus + lTaxi + lWyc + lRail + lFerry;
        return new double[]{total, bus, taxi, wyc, rail, ferry,
                smYoy(total, lTotal), smYoy(bus, lBus), smYoy(taxi, lTaxi), smYoy(wyc, lWyc),
                smYoy(rail, lRail), smYoy(ferry, lFerry)};
    }
    /** ä¸­å£å¾„分析页:块1(累计)行 3-7,块2(当月)行 12-16;列 B/C/F/G = å®¢è¿é‡/同比/周转量/同比 */
    private int smWriteMidAnalysis(XSSFWorkbook wb, int month,
                                   double[] pCurCum, double[] pLastCum, double[] pCurMon, double[] pLastMon) {
        Sheet sh = wb.getSheet("中口径分析");
        if (sh == null) return 0;
        smSetTitle(sh, 0, 0, "1-" + month + "月中口径客运量及周转量");
        smSetTitle(sh, 9, 0, month + "月中口径客运量及周转量");
        int n = 0;
        int[] rows = {2, 3, 4, 5, 6};
        for (int i = 0; i < rows.length; i++) {
            double[] v = smMidRow(pCurCum, pLastCum, i);
            n += smSetVal(sh, rows[i], 1, v[0]);
            n += smSetVal(sh, rows[i], 2, v[2]);
            n += smSetVal(sh, rows[i], 5, v[1]);
            n += smSetVal(sh, rows[i], 6, v[3]);
        }
        for (int i = 0; i < rows.length; i++) {
            double[] v = smMidRow(pCurMon, pLastMon, i);
            n += smSetVal(sh, rows[i] + 9, 1, v[0]);
            n += smSetVal(sh, rows[i] + 9, 2, v[2]);
            n += smSetVal(sh, rows[i] + 9, 5, v[1]);
            n += smSetVal(sh, rows[i] + 9, 6, v[3]);
        }
        return n;
    }
    /** i=0 æ€» 1 å…¬è·¯ç­çº¿ 2 åŸŽé™…城乡公交 3 åŸŽé™…城乡巡游出租 4 åŸŽé™…城乡网约车;返回 [客运量, å‘¨è½¬é‡, å®¢è¿é‡åŒæ¯”, å‘¨è½¬é‡åŒæ¯”] */
    private double[] smMidRow(double[] p, double[] pl, int i) {
        int[] paxIdx = {SM_BX_PAX, SM_BX_PAX, SM_BUS_CX_PAX, SM_TAXI_CX_PAX, SM_WYC_CX_PAX};
        int[] turnIdx = {SM_BX_TURN, SM_BX_TURN, SM_BUS_CX_TURN, SM_TAXI_CX_TURN, SM_WYC_CX_TURN};
        double pax, turn, lPax, lTurn;
        if (i == 0) {
            pax = p[SM_BX_PAX] + p[SM_BUS_CX_PAX] + p[SM_TAXI_CX_PAX] + p[SM_WYC_CX_PAX];
            turn = p[SM_BX_TURN] + p[SM_BUS_CX_TURN] + p[SM_TAXI_CX_TURN] + p[SM_WYC_CX_TURN];
            lPax = pl[SM_BX_PAX] + pl[SM_BUS_CX_PAX] + pl[SM_TAXI_CX_PAX] + pl[SM_WYC_CX_PAX];
            lTurn = pl[SM_BX_TURN] + pl[SM_BUS_CX_TURN] + pl[SM_TAXI_CX_TURN] + pl[SM_WYC_CX_TURN];
        } else {
            pax = p[paxIdx[i]]; turn = p[turnIdx[i]];
            lPax = pl[paxIdx[i]]; lTurn = pl[turnIdx[i]];
        }
        return new double[]{pax, turn, smYoy(pax, lPax), smYoy(turn, lTurn)};
    }
    /** é“路运输周转量页:块1(累计)行 4-21,块2(当月)行 26-43;只写值列(B/E/V ç­‰å…¬å¼ä¿ç•™ï¼‰ */
    private int smWriteTurnoverSheets(XSSFWorkbook wb, int year, int month,
                                      Map<String, double[]> curCum, Map<String, double[]> lastCum,
                                      Map<String, double[]> curMon, Map<String, double[]> lastMon) {
        Sheet sh = wb.getSheet("道路运输周转量");
        if (sh == null) return 0;
        smSetTitle(sh, 0, 0, year + "å¹´1-" + month + "月全省累计完成交通周转量情况");
        smSetTitle(sh, 22, 0, year + "å¹´" + month + "月全省完成交通周转量情况");
        int n = 0;
        n += smWriteTurnoverBlock(sh, 3, curCum, lastCum);
        n += smWriteTurnoverBlock(sh, 25, curMon, lastMon);
        return n;
    }
    private int smWriteTurnoverBlock(Sheet sh, int provRow, Map<String, double[]> cur, Map<String, double[]> last) {
        int n = 0;
        double[] provCur = smProvinceSum(cur);
        double[] provLast = smProvinceSum(last);
        List<String> cities = RegionUtil.cityList();
        for (int i = -1; i < cities.size(); i++) {
            int r = provRow + i + 1;
            double[] c = i < 0 ? provCur : cur.get(cities.get(i));
            double[] l = i < 0 ? provLast : last.get(cities.get(i));
            if (c == null) c = new double[SM_DIM];
            if (l == null) l = new double[SM_DIM];
            double freight = c[SM_FREIGHT_TURN], lFreight = l[SM_FREIGHT_TURN];
            double pax = c[SM_BUS_PAX] + c[SM_TAXI_PAX] + c[SM_WYC_PAX] + c[SM_RAIL_PAX] + c[SM_FERRY_PAX];
            double lPax = l[SM_BUS_PAX] + l[SM_TAXI_PAX] + l[SM_WYC_PAX] + l[SM_RAIL_PAX] + l[SM_FERRY_PAX];
            double road = c[SM_BX_TURN] + c[SM_BUS_CX_TURN] + c[SM_TAXI_CX_TURN] + c[SM_WYC_CX_TURN];
            double lRoad = l[SM_BX_TURN] + l[SM_BUS_CX_TURN] + l[SM_TAXI_CX_TURN] + l[SM_WYC_CX_TURN];
            n += smSetVal(sh, r, 6, freight);
            n += smSetVal(sh, r, 9, smYoy(freight, lFreight));
            n += smSetVal(sh, r, 11, pax);
            n += smSetVal(sh, r, 14, smYoy(pax, lPax));
            n += smSetVal(sh, r, 16, road);
            n += smSetVal(sh, r, 19, smYoy(road, lRoad));
            n += smSetVal(sh, r, 22, lFreight);
            n += smSetVal(sh, r, 23, lPax);
            n += smSetVal(sh, r, 24, lRoad);
        }
        return n;
    }
    private File resolveSummaryLayoutRef() {
        String rel = summaryTemplateDir;
        while (rel != null && rel.startsWith("./")) rel = rel.substring(2);