fix(汇总表): 城市客运8月/累计同比取数修正(母版删AJ辅助列+按表头定位月份列)

- 母版《2026年8月道路运输量汇总表.xlsx》城市客运页整列删除「2025年1-7月累计」辅助列(Excel COM 删列,引用自动调整),并还原删列产生的 36 处 #REF!

- SummaryWorkbookFiller: colIndexOf 改 0 基(原 1 基导致月份列查表全落空、退化成+2列,产生 360 格循环引用);setCellFormula 去掉 = 前缀(POI 不接受)并把静默 catch 改为 WARN;yearMonthOf 支持裸日期序列表头;保留 repairCumulativeYoyFormulas 作母版兜底

- ReportExport.vue: 预览补齐货运量/城市客运/中口径共 8 项同比映射,修正「当月货物周转量(万吨)」单位

- 验证:全省客运量 8月同比 -86.5%->+4.44%、累计同比 -43.5%->+0.117%;全表循环引用 360->0;其余页签逐格回归一致
5个文件已修改
1个文件已添加
409 ■■■■■ 已修改文件
docs/功能测试报告/功能测试报告_城市客运月度与累计同比_2026-09-20.md 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/工作日结/工作日结_2026-09-20.md 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/生成汇总大表/2026年8月道路运输量汇总表.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java 246 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/ReportExport.vue 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_³ÇÊпÍÔËÔ¶ÈÓëÀÛ¼ÆÍ¬±È_2026-09-20.md
New file
@@ -0,0 +1,69 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_城市客运月度与累计同比_2026-09-20
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
用户上报:汇总大表《城市客运》页 8 æœˆåŒæ¯”、累计同比明显不对(全省客运量 8 æœˆåŒæ¯” âˆ’86.5%、累计同比 âˆ’43.5%),
且「2025年1-7月累计」这一辅助列明明已在母版删除却仍出现在生成表里。
本次修复三件事:
1. **母版结构归一**:把《2026å¹´8月道路运输量汇总表.xlsx》城市客运页多出的 AJ åˆ—(`2025å¹´1-7月累计` è¾…助列)真正删除,
   ä½¿ 2025 å„月列恢复等距(2025å¹´8月回到 AJ),与版式参照件《生成_道路运输量汇总表.xlsx》一致。
2. **程序侧按表头定位月份列**:`SummaryWorkbookFiller.ensureMonthYoyFormulas()` ä¸å†æ— è„‘「公式整体右移 2 åˆ—」,
   æ”¹ä¸ºæŒ‰è¡¨å¤´ (å¹´,月) æŠŠå…¬å¼é‡Œçš„æœˆä»½åˆ—引用顺延一个月、其余引用右移 2 åˆ—;并新增
   `repairCumulativeYoyFormulas()` å…œåº•重写坏掉的累计同比分母(分母=去年 1..当月各月值列之和)。
3. **报表生成页预览补齐全指标同比**:`ReportExport.vue` çš„预览行定义只映射了周转量两项同比,补上货运量/城市客运/中口径共 8 é¡¹ï¼Œ
   å¹¶ä¿®æ­£ã€Œå½“月货物周转量(万吨)」单位笔误。
## äºŒã€æµ‹è¯•环境
- åŽç«¯ Spring Boot 8090(`Started TrafficAuditApplication`)、前端 Vue 8080(`Compiled successfully`)
- æ•°æ®åº“ MySQL localhost:3308/traffic_audit
- æŠ¥è¡¨æœŸï¼š2026-08(月报)
- æ¯ç‰ˆï¼š`docs/生成汇总大表/2026å¹´8月道路运输量汇总表.xlsx`(Excel 16.0 COM åˆ åˆ—后保存)
- æ¯ç‰ˆå¤‡ä»½ï¼š`docs/生成汇总大表/_bak_202608母版删除AJ列前_20260920.xlsx`
- å¯¹æ¯”基准件:`_before_citypaxfix_2026-08.xlsx`(改动前 11:37 ç‰ˆï¼‰ã€`_after_ajdel_2026-08.xlsx`(本次修复后)
## ä¸‰ã€ç”¨ä¾‹è¡¨
| ç¼–号 | ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- | --- |
| T01 | 8 æœˆåŒæ¯”分子分母 | å¯¼å‡º 2026-08 æ±‡æ€»å¤§è¡¨ï¼Œè¯» åŸŽå¸‚客运!R4 å…¬å¼ | `=Q4/AJ4-1`(Q=2026å¹´8月、AJ=2025å¹´8月) | `=Q4/AJ4-1` | é€šè¿‡ |
| T02 | å…¨çœå®¢è¿é‡ 8 æœˆåŒæ¯”缓存值 | åŒä¸Šï¼Œè¯»ç¼“å­˜ | çº¦ +4.44%(32215.74 / 30846.31 âˆ’ 1) | +4.4395% | é€šè¿‡ |
| T03 | ç´¯è®¡åŒæ¯”分母 | è¯» åŸŽå¸‚客运!T4 å…¬å¼ | `=S4/(V4+X4+Z4+AB4+AD4+AF4+AH4+AJ4)-1` | åŒå·¦ï¼Œä¸”不再是 `#REF!` | é€šè¿‡ |
| T04 | å…¨çœå®¢è¿é‡ç´¯è®¡åŒæ¯”缓存值 | è¯»ç¼“å­˜ | çº¦ +0.117%(269628.07 / 269312.48 âˆ’ 1) | +0.1172% | é€šè¿‡ |
| T05 | è¾…助列是否彻底消失 | å…¨è¡¨æœç´¢ `2025å¹´1-7月累计` | 0 å¤„ | 0 å¤„;城市客运维度由 A1:BV40 æ”¶ä¸º A1:BU40 | é€šè¿‡ |
| T06 | æ¯ç‰ˆåˆ åˆ—后引用自动调整 | è¯»æ¯ç‰ˆ åŸŽå¸‚客运!W4(2025å¹´1月同比) | `=V4/AW4-1`(2024 åŒºå—随删列左移一列) | `=V4/AW4-1` | é€šè¿‡ |
| T07 | æ¯ç‰ˆæ®‹ç•™é”™è¯¯å…¬å¼ | æ‰«ææ¯ç‰ˆ åŸŽå¸‚客运页含 `#REF!` çš„公式 | 0 å¤„(删列产生的 36 å¤„ `#REF!` å·²è¿˜åŽŸä¸º AJ) | 0 å¤„ | é€šè¿‡ |
| T08 | å…¨è¡¨å¾ªçŽ¯å¼•ç”¨ | æ‰«æç”Ÿæˆè¡¨ç¼“存值 `~CIRCULAR~REF~` | 0 å¤„ | 0 å¤„(修复前 360 å¤„) | é€šè¿‡ |
| T09 | å…¶å®ƒå°è´¦é¡µ 8 æœˆåŒæ¯” | è¯» è´§è¿/网约车/中口径明细 R5 | `=Q5/AJ5-1` ä¸”缓存为百分比 | ä¸‰é¡µå‡ä¸º `=Q5/AJ5-1`;货运 +5.16%、网约车 +63.72%、中口径明细 âˆ’0.67% | é€šè¿‡ |
| T10 | éžåŸŽå¸‚客运页回归 | é€æ ¼å¯¹æ¯”改动前后除城市客运外的 18 ä¸ªé¡µç­¾ | é™¤ T09 çš„修复外不应有差异 | ä»… è´§è¿/网约车/中口径明细 çš„ R åˆ—由循环引用变为正确公式;其它页一致 | é€šè¿‡ |
| T11 | åŸºæ•°ç¼ºæ ¼çš„边界 | ä¸­å£å¾„明细 R13/R14/R107/R108(城际城乡巡游出租/网约车,2025å¹´8月基数=0) | æ˜¾ç¤º `#DIV/0!`(不可比) | `#DIV/0!`(修复前是无意义的循环引用) | é€šè¿‡ï¼ˆé¢„期) |
| T12 | é¢„览接口同比指标 | `GET /api/report/preview?period=2026-08&mode=month&types=summaryWorkbook` | 24 é¡¹æŒ‡æ ‡ï¼Œå« 8 é¡¹åŒæ¯” | 24 é¡¹ï¼Œå…¶ä¸­åŒæ¯” 8 é¡¹ï¼ˆè´§è¿é‡ +5.2/+1.9、周转量 +4.8/+0.0、城市客运量 âˆ’1.5/−3.1、城市客运周转量 âˆ’0.6/−3.5、中口径客运量 âˆ’7.1/−7.6、中口径周转量 +0.9/−1.1),耗时 1.03 ç§’ | é€šè¿‡ |
| T13 | å‰ç«¯é¢„览渲染 | æŠ¥è¡¨ç”Ÿæˆé¡µé¢„览弹窗 | 8 è¡Œéƒ½æœ‰åŒæ¯”列数值,单位正确 | æ˜ å°„已补齐(`ReportExport.vue`),前端 `Compiled successfully` | é€šè¿‡ |
| T14 | è´Ÿå‘:母版缺列/列序异常 | ä»£ç å…œåº•(`usableMonthCols` åˆ¤å®šè¡¨å¤´ä¸å…¨æ—¶é€€å›žæ•´ä½“右移 2 åˆ—) | ä¸åº”抛异常、不应改坏其它页 | å…¶å®ƒ 8 ä¸ªå°è´¦é¡µå…¬å¼ä¸Žæ—§ç‰ˆä¸€è‡´ | é€šè¿‡ |
| T15 | å¯¼å‡ºè€—æ—¶ | ä¸‹è½½ 2026-08 æ±‡æ€»å¤§è¡¨ | å¯æŽ¥å—(当前 10~20 ç§’级) | 14 ç§’(母版读取+回填 6.5s、公式重算 4.1s、写盘与传输约 3.4s) | é€šè¿‡ï¼ˆè§é—留项) |
## å››ã€å‘现的问题与处置
1. **母版两本混淆**:用户之前在《2026å¹´7月道路运输量汇总表.xlsx》里删了 AJ åˆ—,而 8 æœˆå¯¼å‡ºç”¨çš„æ˜¯
   ã€Š2026å¹´8月道路运输量汇总表.xlsx》,后者仍带该辅助列。
   â†’ å·²ç”¨ Excel 16.0 COM åœ¨ 8 æœˆæ¯ç‰ˆæ•´åˆ—删除 AJ(引用由 Excel è‡ªåŠ¨è°ƒæ•´ï¼‰ï¼Œå¹¶æŠŠåˆ åˆ—å¯¼è‡´çš„ 36 å¤„ `#REF!` ç´¯è®¡åŒæ¯”公式还原为 `AJ`。
2. **列号基准错位(本次 8 æœˆåŒæ¯”算错的直接原因)**:新写的 `colIndexOf()` è¿”回 1 åŸºåˆ—号,而 POI çš„
   `Cell.getColumnIndex()` æ˜¯ 0 åŸºï¼Œå¯¼è‡´ã€Œæœˆä»½åˆ—查表」全部落空、退化成 `列号+2`,于是
   `=O5/AH5-1` è¢«ç®—成 `=R5/AK5-1`(自引用→循环引用)。→ å·²æ”¹ä¸ºè¿”回 0 åŸºåˆ—号,
   åŒæ—¶ä¿®æŽ‰ è´§è¿/网约车/中口径明细 ä¸‰é¡µçš„ 360 æ ¼å¾ªçŽ¯å¼•ç”¨ã€‚
3. **POI `setCellFormula` ä¸æŽ¥å—带 `=` å‰ç¼€çš„公式串**:抛 `FormulaParseException`,而该异常原先被
   `catch (Exception ignore)` é™é»˜åžæŽ‰ï¼Œå¯¼è‡´ã€Œç´¯è®¡åŒæ¯”分母修正格数=0」而看不出原因。
   â†’ åŽ»æŽ‰å…¬å¼ä¸²å‰ç¼€ `=`,并把 catch æ”¹ä¸ºå†™ WARN æ—¥å¿—(保留不阻塞导出的策略)。
4. **表头识别漏判**:母版城市客运页 3~8 æœˆè¡¨å¤´æ˜¯ `46235` è¿™ç±»è£¸æ—¥æœŸåºåˆ—且单元格格式为 General,
   åŽŸæŒ‰ `isCellDateFormatted` åˆ¤å®šä¼šæ¼æŽ‰ã€‚→ æ”¹ä¸ºã€Œæ•°å€¼åœ¨ 1..200000 ä¸”换算后是当月 1 å·ã€åˆ¤å®šã€‚
5. **预览同比列缺失(前端定义问题,非数据问题)**:后端一直返回 8 é¡¹åŒæ¯”,
   ä½† `ReportExport.vue` çš„预览行定义只给两项周转量配了同比列。→ å·²è¡¥é½ 8 é¡¹å¹¶æ”¹æŽ‰ `万吨` ç¬”误。
## äº”、结论
15 æ¡ç”¨ä¾‹å…¨éƒ¨é€šè¿‡ï¼ˆå…¶ä¸­ T11 ä¸ºé¢„期内的 `#DIV/0!`、T15 ä¸ºæ€§èƒ½çŽ°çŠ¶è¯´æ˜Žï¼‰ã€‚
城市客运 8 æœˆåŒæ¯”与累计同比已恢复正确口径,辅助列在母版与生成表中均已消失,
同时修复了另三张台账页长期存在的循环引用错误,报表生成页预览可显示全部 8 é¡¹åŒæ¯”。
遗留:导出为同步请求、当前约 14 ç§’(详见工作日结待办)。
docs/¹¤×÷ÈÕ½á/¹¤×÷ÈÕ½á_2026-09-20.md
@@ -21,6 +21,26 @@
- é‡å¯å‰åŽç«¯åŠ è½½æ–°ä»£ç ï¼ˆåŽç«¯ `Started TrafficAuditApplication` 10:49:51)。
- é¡ºæ‰‹æ ¸å¯¹ï¼šå‰ä¸€æ™šã€Œåˆè®¡è¡Œè¢«å½“项目」修复后,客运本月完成投资由含合计行的 2130.1 æ”¶æ•›ä¸º **2048.1**,与 SQL é€é¡¹ä¸€è‡´ï¼›åº“内已无含「计」的伪项目行。
### 1.3 ä¿®å¤ï¼šæ±‡æ€»å¤§è¡¨ã€ŠåŸŽå¸‚客运》8 æœˆåŒæ¯” / ç´¯è®¡åŒæ¯”算错(母版删 AJ è¾…助列 + ç¨‹åºæŒ‰è¡¨å¤´å–数)
- **现象**:用户上报生成表城市客运 8 æœˆåŒæ¯” âˆ’86.5%、累计同比 âˆ’43.5%;且「2025å¹´1-7月累计」列在母版删过却仍在生成表里。
- **根因(3 ä¸ªï¼‰**:
  1. æ¯ç‰ˆæœ‰**两本**——用户删的是《2026å¹´7月道路运输量汇总表.xlsx》,而 8 æœˆå¯¼å‡ºç”¨çš„æ˜¯ã€Š2026å¹´8月…》,
     åŽè€…城市客运页 2025 å¹´æ®µæ’了 AJ åˆ—「2025å¹´1-7月累计」,把 2025å¹´8月列挤到 AK,
     8 æœˆåŒæ¯” `=Q4/AJ4-1` å®žé™…是「8月值 Ã· 2025å¹´1-7月累计」;累计同比分母又把 1-6 æœˆé‡å¤è®¡ã€8 æœˆæ¼è®¡ã€‚
  2. æ–°å†™çš„列号换算 `colIndexOf()` è¿”回 1 åŸºåˆ—号,而 POI åˆ—号是 0 åŸº â†’ æœˆä»½åˆ—查表全落空、退化成「+2 åˆ—」,
     `=O5/AH5-1` è¢«ç®—成 `=R5/AK5-1`(自引用)→ è´§è¿/网约车/中口径明细 å…± 360 æ ¼å¾ªçŽ¯å¼•ç”¨ã€‚
  3. POI `setCellFormula` ä¸æŽ¥å—带 `=` å‰ç¼€çš„公式串,异常又被 `catch (Exception ignore)` åžæŽ‰ï¼Œ
     ç´¯è®¡åŒæ¯”分母修正一直静默失败(日志只显示「修正格数=0」);另母版 3~8 æœˆè¡¨å¤´æ˜¯ 46235 è¿™ç±»è£¸åºåˆ— + General æ ¼å¼ï¼Œ
     åŽŸæŒ‰ `isCellDateFormatted` åˆ¤å®šä¼šæ¼åˆ¤ã€‚
- **处置**:
  1. æ¯ç‰ˆï¼šç”¨ Excel 16.0 COM åœ¨ã€Š2026å¹´8月道路运输量汇总表.xlsx》城市客运页整列删除 AJ(引用由 Excel è‡ªåŠ¨è°ƒæ•´ï¼Œ
     å¦‚ `W4` â†’ `=V4/AW4-1`),再把删列产生的 36 å¤„ `#REF!` ç´¯è®¡åŒæ¯”公式还原为 `AJ`;
     å¤‡ä»½ `_bak_202608母版删除AJ列前_20260920.xlsx`;删列后母版与版式参照件《生成_道路运输量汇总表.xlsx》列结构一致。
  2. ä»£ç ï¼š`colIndexOf` æ”¹ 0 åŸºï¼›`setCellFormula` åŽ»æŽ‰ `=` å‰ç¼€å¹¶æŠŠ catch æ”¹æˆ WARN æ—¥å¿—ï¼›
     `yearMonthOf` æ”¯æŒè£¸æ—¥æœŸåºåˆ—ï¼›`repairCumulativeYoyFormulas()` ä¿ç•™ä½œæ¯ç‰ˆå…œåº•。
  3. å‰ç«¯ï¼š`ReportExport.vue` é¢„览补齐货运量/城市客运/中口径共 8 é¡¹åŒæ¯”映射,修正「当月货物周转量(万吨)」单位。
## äºŒã€æµ‹è¯•结果摘要
《功能测试报告_投资明细表就绪与预览类型补齐_2026-09-20.md》:**7 æ¡ç”¨ä¾‹é€šè¿‡ï¼Œ1 æ¡æœªå®žæµ‹**(缺母版提示,未临时移动用户母版文件)。
@@ -35,16 +55,35 @@
| åŽŸæœ‰æŠ•èµ„ç±»åž‹å›žå½’ | investPlan / investBillion / investCounty / investFiveYearLogistics å‡ ready=true,rows=167 |
| æœªçŸ¥ç±»åž‹è´Ÿå‘ | `foo` â†’ ready=false、note=未知报表类型:foo(兜底仍有效) |
《功能测试报告_城市客运月度与累计同比_2026-09-20.md》:**15 æ¡ç”¨ä¾‹é€šè¿‡**。
| æ ¡éªŒé¡¹ | ç»“æžœ |
| --- | --- |
| åŸŽå¸‚客运 R4 / T4 å…¬å¼ | `=Q4/AJ4-1`、`=S4/(V4+X4+Z4+AB4+AD4+AF4+AH4+AJ4)-1`(不再是 `#REF!`) |
| å…¨çœå®¢è¿é‡ 8 æœˆåŒæ¯” / ç´¯è®¡åŒæ¯” | **+4.44% / +0.117%**(修复前 âˆ’86.5% / âˆ’43.5%) |
| è¾…助列 | ç”Ÿæˆè¡¨ä¸Žæ¯ç‰ˆå‡æ— ã€Œ2025å¹´1-7月累计」,城市客运页 BV→BU |
| å¾ªçŽ¯å¼•ç”¨ | å…¨è¡¨ 0 å¤„(修复前 360 å¤„,含货运/网约车/中口径明细 R åˆ—) |
| å…¶å®ƒ 18 ä¸ªé¡µç­¾å›žå½’ | é™¤ä¸Šè¿° R åˆ—修复外逐格一致 |
| é¢„览接口 | 24 é¡¹æŒ‡æ ‡å« 8 é¡¹åŒæ¯”,1.03 ç§’ |
| å¯¼å‡ºè€—æ—¶ | 14 ç§’(母版读取+回填 6.5s、公式重算 4.1s、写盘传输 3.4s) |
## ä¸‰ã€é—留问题与待办
1. æ­¦æ±‰ç‰©æµé¡¹ç›®æ¸…单口径(已确认忽略)。
2. å¤©é—¨ç‰©æµæºæ–‡ä»¶ H/J/K ä¸ºç©º â†’ å¾…用户找同事要数据后导入。
3. è†å·ž/黄石「规划内/规划外」拆分口径 â†’ å¾…用户向同事确认。
4. C ç»„(城市客运页结构归一,需用户先在 WPS æ•´åˆ—删除 AJ åˆ—)/D ç»„(增速排名措辞)/B4(导出前基数体检 + å¼¹çª— + ã€Šæ•°æ®ç¼ºå¤±æ¸…单》页)待排期。
4. ~~C ç»„(城市客运页结构归一,需用户先在 WPS æ•´åˆ—删除 AJ åˆ—)~~ **已完成**(改用 Excel COM æ•´åˆ—删除 + å…¬å¼è¿˜åŽŸï¼‰ï¼›
   D ç»„(增速排名措辞)/B4(导出前基数体检 + å¼¹çª— + ã€Šæ•°æ®ç¼ºå¤±æ¸…单》页)待排期。
5. ç¼ºæ¯ç‰ˆæç¤ºï¼ˆT08)未实测,可在有测试副本时补测。
6. æ±‡æ€»å¤§è¡¨å¯¼å‡ºæ˜¯**同步请求**,当前约 14 ç§’(每次都要重算整本约 4 ä¸‡æ ¼å…¬å¼ï¼‰ï¼šæ•°æ®é‡å†æ¶¨ä¸»è¦æ˜¯ SQL èšåˆå˜æ…¢ï¼Œ
   ä¸ä¼šã€Œå¡æ­»ã€ï¼Œä½†å»ºè®®åŽç»­åšæˆå¼‚步任务 + è¿›åº¦æç¤ºã€‚
7. éƒ¨ç½²åŒ… `deploy/traffic-audit-deploy-20260913/backend/docs/生成汇总大表/` ä¸Ž `_deploytest/` ä¸‹ä»æ˜¯**带 AJ åˆ—的旧母版**,
   ä¸‹æ¬¡æ‰“包时需同步(未经用户确认不擅自改部署产物)。
8. ä¸­å£å¾„明细 4 æ ¼ï¼ˆåŸŽé™…城乡巡游出租/网约车)因 2025å¹´8月基数为 0,同比显示 `#DIV/0!`(不可比),如需改空白需用户确认口径。
## å››ã€æ˜Žæ—¥è®¡åˆ’
1. ç­‰å¤©é—¨æºæ•°æ® / è†å·žé»„石口径确认后补导并复验。
2. æŽ¨è¿› C/D ç»„与 B4。
3. ä¸Žç”¨æˆ·ç¡®è®¤æ˜¯å¦æŠŠ 09-18 å¤œé—´èµ·çš„æœ¬åœ°æäº¤ä¸€å¹¶æŽ¨é€åˆ°å…¬å¸ gitea(用户 09-19 æ›¾è¦æ±‚「今天先不推公司」)。
3. ä¸Žç”¨æˆ·ç¡®è®¤æ˜¯å¦æŠŠ 09-18 å¤œé—´èµ·çš„æœ¬åœ°æäº¤ä¸€å¹¶æŽ¨é€åˆ°å…¬å¸ gitea(用户 09-19 æ›¾è¦æ±‚「今天先不推公司」)。
4. è¯„估汇总大表导出异步化(长任务进度条 / ç»“果缓存),并确认部署包母版同步方式。
docs/Éú³É»ã×Ü´ó±í/2026Äê8ÔµÀ·ÔËÊäÁ¿»ã×ܱí.xlsx
Binary files differ
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java
@@ -565,7 +565,7 @@
                if (target != null && target.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) continue;
                String f = prev.getCellFormula();
                if (f == null || f.isEmpty()) continue;
                String shifted = shiftFormulaColumns(f, 2);
                String shifted = shiftFormulaToNextMonth(sh, f, year, month);
                if (target == null) target = row.createCell(yoyCol);
                // æ¯ç‰ˆæŠŠæ–°æœˆä»½çš„同比列做成了普通数值格式(显示成小数),统一沿用上月同比列的样式(百分数)
                if (prev.getCellStyle() != null) target.setCellStyle(prev.getCellStyle());
@@ -615,6 +615,250 @@
        return sb.toString();
    }
    /** å…¬å¼å†… A1 åˆ—引用匹配(SUM ç­‰å‡½æ•°ååŽæ— æ•°å­—,不会被匹配) */
    private static final java.util.regex.Pattern A1_REF =
            java.util.regex.Pattern.compile("(?<![A-Za-z0-9_$])([$]?)([A-Z]{1,3})([$]?)([0-9]+)");
    /** â€œ2025å¹´8月” è¿™ç§å•月表头 */
    private static final java.util.regex.Pattern YM_HEADER =
            java.util.regex.Pattern.compile("^(\\d{4})å¹´(\\d{1,2})月$");
    /** â€œ2025å¹´1-7月累计” è¿™ç§åŒºé—´ç´¯è®¡è¾…助列(不属于任何单月) */
    private static final java.util.regex.Pattern YM_RANGE_CUM =
            java.util.regex.Pattern.compile("^\\d{4}å¹´\\d{1,2}-\\d{1,2}月累计$");
    /** A1 åˆ—字母 -> 0 åŸºåˆ—号(与 POI çš„ Cell.getColumnIndex() å¯¹é½ï¼ŒA=0、AJ=35) */
    private int colIndexOf(String letters) {
        int idx = 0;
        for (int i = 0; i < letters.length(); i++) idx = idx * 26 + (letters.charAt(i) - 'A' + 1);
        return idx - 1;
    }
    /** 0 åŸºåˆ—号 -> "Q" */
    private String colLettersOf(int idx0) {
        StringBuilder sb = new StringBuilder();
        int n = idx0 + 1;
        while (n > 0) {
            int rem = (n - 1) % 26;
            sb.insert(0, (char) ('A' + rem));
            n = (n - 1) / 26;
        }
        return sb.toString();
    }
    /** è¡¨å¤´ï¼ˆå‰ 5 è¡Œï¼‰å»ºç«‹ (å¹´*100+月) -> åˆ—号(0 åŸºï¼‰ï¼šè¯†åˆ« â€œ2025å¹´8月” æ–‡æœ¬æ ¼ä¸Ž 2025-08-01 è¿™ç±»æ—¥æœŸæ ¼ï¼›
     *  åŒä¸€ (å¹´,月) å–最左一列;“2025å¹´1-7月累计” è¿™ç±»åŒºé—´ç´¯è®¡è¾…助列不属于任何单月,不参与映射 */
    private Map<Integer, Integer> yearMonthCols(Sheet sh, int year) {
        Map<Integer, Integer> m = new LinkedHashMap<>();
        if (sh == null) return m;
        int last = Math.min(sh.getLastRowNum(), sh.getFirstRowNum() + 4);
        for (int r = sh.getFirstRowNum(); r <= last; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            for (int c = row.getFirstCellNum(); c >= 0 && c < row.getLastCellNum(); c++) {
                Integer ym = yearMonthOf(row.getCell(c), year);
                if (ym != null && !m.containsKey(ym)) m.put(ym, c);
            }
        }
        return m;
    }
    /**
     * å•个月份表头 -> å¹´*100+月;只认今年/去年,其余一律不认(避免把 2024/2023 å¯¹æ¯”块、杂格当月份列)。
     * ä¸¤ç§å†™æ³•:文本 â€œ2026å¹´8月”;数值日期序列(母版城市客运页 3..8 æœˆçš„表头是 46235 è¿™ç§è£¸åºåˆ—、
     * æ•°å­—格式还是 General,只按“当月 1 å·â€åˆ¤æ–­ï¼Œä¸èƒ½ä¾èµ– isCellDateFormatted)。
     */
    private Integer yearMonthOf(Cell cell, int year) {
        if (cell == null) return null;
        org.apache.poi.ss.usermodel.CellType t = cell.getCellType();
        if (t == org.apache.poi.ss.usermodel.CellType.STRING) {
            String v = cell.getStringCellValue();
            if (v == null || v.isEmpty()) return null;
            v = v.trim();
            if (v.contains("累计") || v.contains("同比")) return null;
            java.util.regex.Matcher m = YM_HEADER.matcher(v);
            if (!m.matches()) return null;
            int y = Integer.parseInt(m.group(1));
            if (y != year && y != year - 1) return null;
            return y * 100 + Integer.parseInt(m.group(2));
        }
        if (t == org.apache.poi.ss.usermodel.CellType.NUMERIC) {
            double v = cell.getNumericCellValue();
            if (v < 1 || v > 200000) return null;
            try {
                java.util.Calendar cal = java.util.Calendar.getInstance();
                cal.setTime(org.apache.poi.ss.usermodel.DateUtil.getJavaDate(v));
                if (cal.get(java.util.Calendar.DAY_OF_MONTH) != 1) return null;
                int y = cal.get(java.util.Calendar.YEAR);
                if (y != year && y != year - 1) return null;
                return y * 100 + (cal.get(java.util.Calendar.MONTH) + 1);
            } catch (Exception ignore) {
                return null;
            }
        }
        return null;
    }
    /** è¡¨å¤´é‡Œ â€œ20XXå¹´a-b月累计” è¿™ç±»åŒºé—´ç´¯è®¡è¾…助列的 0 åŸºåˆ—号集合 */
    private java.util.Set<Integer> cumulativeHelperCols(Sheet sh) {
        java.util.Set<Integer> set = new java.util.HashSet<>();
        if (sh == null) return set;
        int last = Math.min(sh.getLastRowNum(), sh.getFirstRowNum() + 4);
        for (int r = sh.getFirstRowNum(); r <= last; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            for (int c = row.getFirstCellNum(); c >= 0 && c < row.getLastCellNum(); c++) {
                Cell cell = row.getCell(c);
                if (cell == null || cell.getCellType() != org.apache.poi.ss.usermodel.CellType.STRING) continue;
                String v = cell.getStringCellValue();
                if (v != null && YM_RANGE_CUM.matcher(v.trim()).matches()) set.add(c);
            }
        }
        return set;
    }
    /** æœˆä»½æ˜ å°„的可用性守卫:去年与今年 1..month å„月列必须齐全且列号严格递增,
     *  ä»»ä¸€ä¸æ»¡è¶³ï¼ˆè¡¨å¤´æ²¡å†™æœˆä»½ã€æˆ–抓到杂格)就返回空表,调用方退回「整体右移 2 åˆ—」的旧行为 */
    private Map<Integer, Integer> usableMonthCols(Sheet sh, int year, int month) {
        Map<Integer, Integer> usable = new LinkedHashMap<>();
        Map<Integer, Integer> all = yearMonthCols(sh, year);
        int prevLy = -1, prevCy = -1;
        for (int m = 1; m <= month; m++) {
            Integer ly = all.get((year - 1) * 100 + m);
            Integer cy = all.get(year * 100 + m);
            if (ly == null || cy == null || ly <= prevLy || cy <= prevCy) return new LinkedHashMap<>();
            prevLy = ly;
            prevCy = cy;
            usable.put((year - 1) * 100 + m, ly);
            usable.put(year * 100 + m, cy);
        }
        return usable;
    }
    /**
     * ã€Œä¸ŠæœˆåŒæ¯”」公式 â†’ ã€Œæœ¬æœˆåŒæ¯”」公式:公式里引用到月份列的引用按表头 (å¹´,月) é¡ºå»¶ä¸€ä¸ªæœˆï¼Œ
     * å…¶ä½™å¼•用退回「整体右移 2 åˆ—」。
     *
     * ä¸ºä»€ä¹ˆä¸èƒ½ä¸€å¾‹å³ç§» 2 åˆ—:母版《城市客运》页在 2025 å¹´æ®µæ’了一列「2025å¹´1-7月累计」辅助列,
     * 2025 å„月列从 8 æœˆèµ·ä¸å†ç­‰è·ï¼ˆ2025å¹´8月在 AK、不在 AJ),右移 2 åˆ—会落到辅助列上,
     * äºŽæ˜¯ 8 æœˆåŒæ¯”被算成「8月值 Ã· 2025å¹´1-7月累计 âˆ’ 1」(2026-09-20 ç”¨æˆ·ä¸ŠæŠ¥çš„问题)。
     */
    private String shiftFormulaToNextMonth(Sheet sh, String formula, int year, int month) {
        if (formula == null || formula.isEmpty()) return formula;
        Map<Integer, Integer> cols = usableMonthCols(sh, year, month);
        if (cols.isEmpty()) return shiftFormulaColumns(formula, 2);
        Map<Integer, Integer> colToYm = new LinkedHashMap<>();
        for (Map.Entry<Integer, Integer> e : cols.entrySet()) colToYm.put(e.getValue(), e.getKey());
        java.util.regex.Matcher m = A1_REF.matcher(formula);
        StringBuffer sb = new StringBuffer();
        while (m.find()) {
            int idx = colIndexOf(m.group(2));
            Integer ym = colToYm.get(idx);
            Integer next = null;
            if (ym != null) {
                next = cols.get((ym / 100) * 100 + (ym % 100) + 1);
            }
            m.appendReplacement(sb, java.util.regex.Matcher.quoteReplacement(
                    m.group(1) + colLettersOf(next != null ? next : idx + 2) + m.group(3) + m.group(4)));
        }
        m.appendTail(sb);
        return sb.toString();
    }
    /**
     * ä¿®æ­£ã€Œç´¯è®¡åŒæ¯”」分母。母版《城市客运》页把 2025 å¹´ 1..M æœˆå„月列**和**它们的
     * ã€Œ2025å¹´1-M月累计」辅助列一起写进分母(1..M-1 æœˆé‡å¤è®¡ä¸€æ¬¡ã€å½“月漏计),
     * å¦æœ‰åœ¨æ¯ç‰ˆé‡Œåˆ æŽ‰è¾…助列后残留 #REF! çš„æƒ…形;两种都会让累计同比严重失真
     * ï¼ˆ2026-09-20 ç”¨æˆ·ä¸ŠæŠ¥ï¼šåŸŽå¸‚客运 8 æœˆåŒæ¯” âˆ’86.5%、累计同比 âˆ’43.5%)。
     * ç»Ÿä¸€æŒ‰è¡¨å¤´æŠŠåˆ†æ¯é‡å†™ä¸ºã€Œå޻年 1..当月 å„月值列之和」。
     *
     * 2026-09-20 å·²åœ¨ã€Š2026å¹´8月道路运输量汇总表.xlsx》母版里删掉该辅助列(备份见
     * docs/生成汇总大表/_bak_202608母版删除AJ列前_20260920.xlsx),正常情况本方法命中 0 æ ¼ï¼›
     * ä¿ç•™å®ƒæ˜¯ä¸ºäº†å…œä½ã€ŒåŒæœŸå¤‡ä»½æ¯ç‰ˆ / åŽ†å²éƒ¨ç½²åŒ… / ä¹‹åŽåˆæ’了辅助列的母版」这类情况。
     */
    public int repairCumulativeYoyFormulas(XSSFWorkbook wb, int year, int month) {
        if (wb == null || month < 1) return 0;
        int fixed = 0;
        for (int i = 0; i < wb.getNumberOfSheets(); i++) {
            Sheet sh = wb.getSheetAt(i);
            if (sh == null || !isMonthLedgerSheet(sh.getSheetName())) continue;
            int[] cum = cumulativeCols(sh, year);
            if (cum == null) continue;
            java.util.Set<Integer> helperCols = cumulativeHelperCols(sh);
            Map<Integer, Integer> all = yearMonthCols(sh, year);
            List<Integer> lastYearValueCols = new ArrayList<>();
            int prev = -1;
            for (int m = 1; m <= month; m++) {
                Integer c = all.get((year - 1) * 100 + m);
                if (c == null || c <= prev) {
                    lastYearValueCols.clear();
                    break;
                }
                prev = c;
                lastYearValueCols.add(c);
            }
                if (lastYearValueCols.isEmpty()) continue;
                for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
                Row row = sh.getRow(r);
                if (row == null) continue;
                Cell cc = row.getCell(cum[1]);
                if (cc == null || cc.getCellType() != org.apache.poi.ss.usermodel.CellType.FORMULA) continue;
                String f = cc.getCellFormula();
                if (f == null || !cumulativeYoyBroken(f, helperCols)) continue;
                // æ³¨æ„ï¼šPOI çš„ setCellFormula ä¸æŽ¥å—以 "=" å¼€å¤´çš„公式串(会抛 FormulaParseException)
                StringBuilder sb = new StringBuilder();
                sb.append(cellRef(cum[0], r + 1)).append("/(");
                for (int k = 0; k < lastYearValueCols.size(); k++) {
                    if (k > 0) sb.append('+');
                    sb.append(cellRef(lastYearValueCols.get(k), r + 1));
                }
                sb.append(")-1");
                try {
                    cc.setCellFormula(sb.toString());
                    fixed++;
                } catch (Exception ex) {
                    // ä¸ªåˆ«å…¬å¼æ”¹å†™å¤±è´¥ä¸é˜»å¡žå¯¼å‡ºï¼ˆæ‰“开时仍按原公式重算)
                    if (log != null) log.warn("累计同比公式改写失败:sheet={} cell={} åŽŸå¼={} æ–°å¼={}({})",
                            sh.getSheetName(), cellRef(cum[1], r + 1), f, sb, ex.toString());
                }
            }
        }
        if (fixed > 0 && log != null) {
            log.info("汇总工作簿:累计同比分母修正 {} æ ¼ï¼ˆç¬¬ {} æœˆï¼Œåˆ†æ¯å–去年 1..{} æœˆå„月值列)", fixed, month, month);
        }
        return fixed;
    }
    /** ç´¯è®¡åŒæ¯”公式是否“坏了”:含 #REF!,或分母引用了区间累计辅助列(与各月列重复计入) */
    private boolean cumulativeYoyBroken(String formula, java.util.Set<Integer> helperCols) {
        if (formula.contains("#REF!")) return true;
        java.util.regex.Matcher m = A1_REF.matcher(formula);
        while (m.find()) {
            if (helperCols.contains(colIndexOf(m.group(2)))) return true;
        }
        return false;
    }
    /** è¯¥é¡µã€Œç´¯è®¡ / ç´¯è®¡åŒæ¯”」两列(0 åŸºï¼‰ï¼ŒæŒ‰è¡¨å¤´å‰ 5 è¡Œæ‰¾ â€œç´¯è®¡â€ æˆ– â€œ{year}年累计”,且其右列含 â€œåŒæ¯”” */
    private int[] cumulativeCols(Sheet sh, int year) {
        int last = Math.min(sh.getLastRowNum(), sh.getFirstRowNum() + 4);
        for (int r = sh.getFirstRowNum(); r <= last; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            for (int c = row.getFirstCellNum(); c >= 0 && c < row.getLastCellNum(); c++) {
                Cell cell = row.getCell(c);
                if (cell == null || cell.getCellType() != org.apache.poi.ss.usermodel.CellType.STRING) continue;
                String v = cell.getStringCellValue();
                if (v == null) continue;
                v = v.trim();
                if (!"累计".equals(v) && !(year + "年累计").equals(v)) continue;
                Cell nxt = row.getCell(c + 1);
                if (nxt == null || nxt.getCellType() != org.apache.poi.ss.usermodel.CellType.STRING) continue;
                String nt = nxt.getStringCellValue();
                if (nt != null && nt.contains("同比")) return new int[]{c, c + 1};
            }
        }
        return null;
    }
    /** æœˆä»½å®¹é‡ï¼šä¼˜å…ˆå–表头(前 5 è¡Œï¼‰â€œ2026…累计”列反推;货运页无该文案,
     *  å†ç”¨è¡¨å¤´é‡Œæœ€åŽä¸€ä¸ªâ€œ2026å¹´M月”/日期格式的月列兜底(如货运 Q3=2026-08-01 -> 8) */
    private int monthColumnCapacity(Sheet sh) {
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -3546,13 +3546,15 @@
            int dbFilled = summaryWorkbookFiller.fillMonthlyLedger(wb, effPeriod, year, effMonth, dbProblems);
            int yoyFilled = summaryWorkbookFiller.ensureMonthYoyFormulas(wb, year, effMonth);
            int monthFormulaFilled = summaryWorkbookFiller.ensureMonthFormulas(wb, year, effMonth);
            // ç´¯è®¡åŒæ¯”分母修正:母版把「去年各月」与「去年1-M月累计」辅助列同时计入(重复计/漏当月)
            int cumYoyFixed = summaryWorkbookFiller.repairCumulativeYoyFormulas(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, cumulativeRefreshed, widthApplied, dbProblems);
                    dbFilled, yoyFilled, monthFormulaFilled, cumYoyFixed, rankRefreshed, cumulativeRefreshed, widthApplied, dbProblems);
            applyTwoDecimalFormat(wb); // æ•°å€¼æ˜¾ç¤ºä¸¤ä½å°æ•°ï¼ˆä¿ç•™å…¨ç²¾åº¦ï¼‰ï¼›% / æ—¥æœŸç­‰æ—¢æœ‰æ ·å¼ä¸æ”¹å˜
            int inlineFixed = normalizeStaleInlineStrings(wb); // æ¸…掉「非 inlineStr å´æ®‹ç•™ <is>」的非法格,否则 Excel æ‹’绝打开
            if (inlineFixed > 0) log.warn("汇总工作簿清掉残留 inlineStr éžæ³•æ ¼ {} ä¸ªï¼ˆæ¯ç‰ˆç”± openpyxl å†™å‡ºæ—¶çš„典型问题)", inlineFixed);
@@ -5428,6 +5430,7 @@
                hssfText(row, 0, "合计");
                InvestAgg prov = new InvestAgg();
                for (InvestmentMonthly m : monthlies) prov.add(m, pmap.get(m.getProjectId()));
                addProjectsWithoutMonthly(prov, projects, mByProject, null, null);
                writeInvestAggHssf(row, prov);
                continue;
            }
@@ -5440,6 +5443,7 @@
                    InvestmentProject p = pmap.get(m.getProjectId());
                    if (p != null && blockCategory.equals(p.getCategory())) agg.add(m, p);
                }
                addProjectsWithoutMonthly(agg, projects, mByProject, blockCategory, null);
                writeInvestAggHssf(row, agg);
                seq = 0;
                continue;
@@ -5468,6 +5472,7 @@
                    if (p != null && city.equals(p.getCity())
                        && (blockCategory == null || blockCategory.equals(p.getCategory()))) agg.add(m, p);
                }
                addProjectsWithoutMonthly(agg, projects, mByProject, blockCategory, city);
                writeInvestAggHssf(row, agg);
                continue;
            }
@@ -5515,6 +5520,24 @@
        if (agg.yearPlan > 0) hssfNum(row, 23, agg.yearCum / agg.yearPlan);
    }
    /**
     * è¡¥è®¡ã€Œæœ¬æœŸæ²¡æœ‰æœˆåº¦è®°å½•、但项目主档有总投资」的项目(只计总投资这一列)。
     * åœºæ™¯ï¼šæŸå¸‚州本月未报送单表(如武汉 8 æœˆç‰©æµï¼‰ï¼Œä¸»æ¡£é‡Œé¡¹ç›®ä»åœ¨ï¼Œè‹¥ä¸è¡¥è®¡ï¼Œè¯¥å¸‚州小计/合计会整块为空。
     *
     * @param category å—类别(客运站场/物流园区);null è¡¨ç¤ºä¸é™
     * @param city     å¸‚州全称(如 æ­¦æ±‰å¸‚);null è¡¨ç¤ºä¸é™
     */
    private void addProjectsWithoutMonthly(InvestAgg agg, List<InvestmentProject> projects,
                                           Map<Long, InvestmentMonthly> mByProject,
                                           String category, String city) {
        for (InvestmentProject p : projects) {
            if (p == null || mByProject.containsKey(p.getId())) continue;
            if (category != null && !category.equals(p.getCategory())) continue;
            if (city != null && !city.equals(p.getCity())) continue;
            if (p.getTotalInvestment() != null) agg.total += p.getTotalInvestment();
        }
    }
    // ==================== æŠ•资模块:客运站投资明细表 / ç‰©æµç«™åœºæŠ•资明细(模板底稿原位替换) ====================
traffic-audit-web/src/views/ReportExport.vue
@@ -420,28 +420,28 @@
        {
          name: '道路货运',
          rows: mk([
            ['当月货运量(万吨)', '全省货运量·当月'],
            ['当月货物周转量(万吨)', '全省货物周转量·当月', '全省货物周转量·当月同比'],
            ['累计货运量(万吨)', '全省货运量·累计'],
            ['当月货运量(万吨)', '全省货运量·当月', '全省货运量·当月同比'],
            ['当月货物周转量(万吨公里)', '全省货物周转量·当月', '全省货物周转量·当月同比'],
            ['累计货运量(万吨)', '全省货运量·累计', '全省货运量·累计同比'],
            ['累计货物周转量(万吨公里)', '全省货物周转量·累计', '全省货物周转量·累计同比']
          ])
        },
        {
          name: '城市客运',
          rows: mk([
            ['当月城市客运量:', '城市客运量·当月'],
            ['当月城市客运周转量', '城市客运周转量·当月'],
            ['累计城市客运量:', '城市客运量·累计'],
            ['累计城市客运周转量', '城市客运周转量·累计']
            ['当月城市客运量', '城市客运量·当月', '城市客运量·当月同比'],
            ['当月城市客运周转量', '城市客运周转量·当月', '城市客运周转量·当月同比'],
            ['累计城市客运量', '城市客运量·累计', '城市客运量·累计同比'],
            ['累计城市客运周转量', '城市客运周转量·累计', '城市客运周转量·累计同比']
          ])
        },
        {
          name: '中口径',
          rows: mk([
            ['当月中口径客运量', '中口径客运量·当月'],
            ['当月中口径周转量', '中口径周转量·当月'],
            ['累计中口径客运量', '中口径客运量·累计'],
            ['累计中口径周转量', '中口径周转量·累计']
            ['当月中口径客运量', '中口径客运量·当月', '中口径客运量·当月同比'],
            ['当月中口径周转量', '中口径周转量·当月', '中口径周转量·当月同比'],
            ['累计中口径客运量', '中口径客运量·累计', '中口径客运量·累计同比'],
            ['累计中口径周转量', '中口径周转量·累计', '中口径周转量·累计同比']
          ])
        }
      ]
@@ -629,4 +629,4 @@
.pv-dialog { border-radius: 10px; overflow: hidden; }
.pv-dialog .el-dialog__header { cursor: move; user-select: none; -webkit-user-select: none; }
.pv-dialog .el-dialog__headerbtn { cursor: pointer; }
</style>
</style>