6个文件已修改
7个文件已添加
1528 ■■■■■ 已修改文件
docs/功能测试报告/功能测试报告_中口径四维口径统一_2026-09-18.md 99 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_汇总表公式扩页与重算_2026-09-18.md 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_网约车订单报告WordML兼容导入_2026-09-18.md 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_货运月报持续在规企业分市州累计运距_2026-09-18.md 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/工作日结/工作日结_2026-09-18.md 205 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/工具/持续在规企业累计运距生成.py 468 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/投资/输入/物流(物流园区)/7月/2026年7月企业月报汇总(鄂州).xls 补丁 | 查看 | 原始文档 | blame | 历史
docs/投资/输出/X月物流站场投资明细.xls 补丁 | 查看 | 原始文档 | blame | 历史
docs/投资/输出/湖北省(客货站场)(亿元投资项目)7月.xls 补丁 | 查看 | 原始文档 | blame | 历史
docs/问题汇总/汇总表三条主线修复方案_2026-09-18.md 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java 175 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java 198 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_Öпھ¶ËÄά¿Ú¾¶Í³Ò»_2026-09-18.md
New file
@@ -0,0 +1,99 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_中口径四维口径统一_2026-09-18
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
**目标**:让「中口径」在系统各处使用同一套口径与同一套源数据,消除「同一指标在不同页/不同导出件里对不上」的问题。
**中口径定义(4 ç»´ï¼Œ2026-09-18 èµ·ç»Ÿä¸€ï¼‰**
| ç»´åº¦ | å«ä¹‰ | æ•°æ®æ¥æº |
|---|---|---|
| 1 | å…¬è·¯ç­çº¿å®¢è¿ | `h2031_enterprise_monthly`(企业月报)+ `passenger_individual_monthly`(个体月报) |
| 2 | åŸŽé™…城乡公交 | `city_bus_monthly` çš„ `passenger_chengxiang / turnover_chengxiang` |
| 3 | åŸŽé™…城乡巡游出租 | `city_taxi_monthly`:总量 âˆ’ åŸŽå¸‚内(`passenger_volume - passenger_city`,周转量同法) |
| 4 | åŸŽé™…城乡网约车 | `WycSplitCalc`(订单 + å…¨çœ pin + å·¡æ¸¸å‡ºç§Ÿæ‹†åˆ†ï¼‰ï¼šæ€»é‡ âˆ’ åŸŽå¸‚内 |
- æ€»å®¢è¿é‡/总周转量 = ç»´åº¦ 1+2+3+4(`loadMidClassMap` çš„维度 0);
- **全省行一律 = Î£17 å¸‚å·ž**(不采用库内 `region_code='420000'` çš„全省行,该口径 1-8 æœˆæ¯” Î£å¸‚州多 3042.75,属业务待核项);
- åŒæ¯” = (本期 âˆ’ åŽ»å¹´åŒæœŸ) / åŽ»å¹´åŒæœŸï¼›æ— åŽ»å¹´æ•°æ®ç•™ç©ºã€‚
- **缺数据处理规则(本次新增,重要)**:库内该月该维没有源数据时,**保留母版/模板原值**,绝不写空、也不写 0。
  ä¸­å£å¾„明细的城市级「城际城乡巡游出租/网约车」行本来就是「台账页 æ€»é‡ âˆ’ åŸŽå¸‚内」的引用并已业务定稿;
  ç”¨åº“内缺失值覆盖,会把定稿数清掉(2026-01~06 ç½‘约车订单缺失时,全省 333.39 ä¸Žå„市州值被清空过)。
**本次改动文件**:`traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java`
- `loadMidClassMap()`:补维度 3(巡游出租城乡)、维度 4(网约车城乡),新增 `midPeriodKey()`;
- `fillMidCell()`:去掉「出租/网约车写死 0」旁路,四维统一取库内值;
- `fillMidYoyCell()` / `fillMidCumYoyColumn()`:同比由「只算前两维」改为四维齐全;
- `exportPassengerMidDetail()`:由「只处理公式格」改为「按列遍历」,扩列出来的 7~12 æœˆç©ºæ ¼æ‰å¡«å¾—上;
- æ€»è¡Œå€¼åˆ—**一律保留模板公式**(1~6 æœˆåŽŸå…¬å¼ + æ‰©åˆ—克隆出来的公式),由收尾重算填值(不再 `keepCached` æ¸…公式、也不再手写合计);
- `fillMidCell()`:库内取值为 0(该月该维无源)时直接返回,保留母版/定稿原值;
- é‡ç®—改用逐格容错的 `recalcWorkbookFormulas()`,并 `setIgnoreMissingWorkbooks(true)`:跨簿公式(`[1]出租!C13`)在外部工作簿缺失时改用该格自身缓存值参与计算,否则引用它的总行/累计会连锁失败(表现为"公式在、但没有缓存值");
- æ–°å¢ž `blankFutureMonthColumns()`:报表期之后的月份列(如 8 æœˆæŠ¥è¡¨é‡Œçš„ 9~12 æœˆï¼‰æ¸…空数据格、保留表头,避免出现"9~12 æœˆå…¨æ˜¯ 0"的假数据。
## äºŒã€æµ‹è¯•环境
- åŽç«¯ 8090(`start-dev.ps1` é‡å¯ï¼Œç¬¬äºŒè½® PID 18516)、前端 8080(复用已在运行实例);数据库 `traffic_audit`(127.0.0.1:3308);报表期 2026-08,mode=month。
- å¯¼å‡ºæŽ¥å£ï¼š`/api/report/export/summaryWorkbook`、`/api/report/export/passengerMidDetail`、`/api/report/export/passengerMidRank`。
- è¯æ®æ–‡ä»¶ï¼ˆç¬¬ä¸€è½®ï¼‰ï¼š`_tmp/_qa_out/step3c_summary.xlsx`、`step3c_midDetail.xlsx`、`step3c_midRank.xlsx`、`chk_step3c2.txt`、`chk_step3c3.txt`、`cmp_step3bc.txt`;`step3b_*.xlsx`(改动前基线)。
- è¯æ®æ–‡ä»¶ï¼ˆç¬¬äºŒè½®/最终):`_tmp/_qa_out/step3g_summary.xlsx`、`step3g_midDetail.xlsx`、`step3g_midRank.xlsx`、`export_step3g.txt`、`chk_step3g.txt`、`cmp_md_vs_summary.txt`;导出脚本 `export_step3g.py`。
## ä¸‰ã€ç”¨ä¾‹è¡¨
| # | ç±»åž‹ | ç”¨ä¾‹ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|---|
| 1 | æ­£å‘ | ç‹¬ç«‹ã€Šç”Ÿæˆ_中口径明细》全省 8 æœˆæ€»è¡Œï¼ˆQ5) | = å››ç»´åˆè®¡ 4003.57(原为空) | 4003.5701 | é€šè¿‡ |
| 2 | æ­£å‘ | ç‹¬ç«‹ä»¶å…¨çœ 7 æœˆæ€»è¡Œï¼ˆO5) | = 4028.3213(原为空) | 4028.3213 | é€šè¿‡ |
| 3 | æ­£å‘ | 18 ä¸ªåœ°åŒºï¼ˆå…¨çœ+17 å¸‚州)× 7/8 æœˆæ€»è¡Œ = Î£è¯¥å— 4 ç»´è¡Œï¼ˆ8 æœˆå„维齐全时) | ç›¸ç­‰ | 8 æœˆ 18/18 ç›¸ç­‰ï¼›7 æœˆ 18/18 ç›¸ç­‰ | é€šè¿‡ |
| 4 | æ­£å‘ | ç‹¬ç«‹ä»¶ç´¯è®¡åˆ—(AA)缓存值 | = è¯¥è¡Œ 1-8 æœˆä¹‹å’Œ | 18/18 è¡Œç›¸ç­‰ï¼ˆå…¨çœ 31528.0607,武汉 2759.8855) | é€šè¿‡ |
| 5 | æ­£å‘ | ç‹¬ç«‹ä»¶ä¸Žæ±‡æ€»å¤§è¡¨ã€Œä¸­å£å¾„明细」页逐格对照(全省行 1-8 æœˆã€4 ä¸ªç»´åº¦åˆ†è§£ï¼‰ | å®Œå…¨ä¸€è‡´ | 1-8 æœˆ 4 ç»´ + æ€»é‡å…¨éƒ¨ä¸€è‡´ | é€šè¿‡ |
| 6 | æ­£å‘ | æ±‡æ€»å¤§è¡¨ã€Œä¸­å£å¾„排名」全省累计 K3 | = Î£17 å¸‚å·ž K4:K20 | 29464.6108 = Î£å¸‚州(独立排名件 B3 åŒå€¼ï¼‰ | é€šè¿‡ï¼ˆå£å¾„一致性另见问题 2) |
| 7 | å›žå½’ | æ±‡æ€»å¤§è¡¨ç”Ÿæˆä»¶æ”¹åŠ¨å‰åŽé€æ ¼å¯¹æ¯”ï¼ˆ17 é¡µï¼‰ | 0 å·®å¼‚ | 0 æ ¼å˜åŒ– | é€šè¿‡ |
| 8 | å›žå½’ | ç‹¬ç«‹ã€Šç”Ÿæˆ_中口径排名》改动前后逐格对比 | 0 å·®å¼‚ | 0 æ ¼å˜åŒ– | é€šè¿‡ |
| 9 | å›žå½’ | ç‹¬ç«‹ã€Šç”Ÿæˆ_中口径明细》改动前后逐格对比 | ä»…目标格变化 | ä»… 36 æ ¼å˜åŒ– = 18 åœ°åŒº Ã— 2 ä¸ªæœˆï¼ˆO/Q æ€»è¡Œï¼‰ | é€šè¿‡ |
| 10 | è´Ÿå‘/边界 | `wyc_order_monthly` 2026-01~06 è®¢å•列全为 NULL、模板《网约车订单及全省总量.xlsx》只有 2026-07 ä¸€è¡Œæœ‰æ•°æ® | ç»´åº¦ 4 è¯¥æœˆæ— æº â†’ è¡Œç•™ç©ºï¼ˆä¸ç¼–造) | ç‹¬ç«‹ä»¶ã€ŒåŸŽé™…城乡网约车」1-6 æœˆä¸ºç©ºï¼›æ±‡æ€»è¡¨è¯¥å£å¾„由母版定稿承载 | é€šè¿‡ï¼ˆæŒ‰é¢„期留空并记录) |
| 11 | è´Ÿå‘/边界 | åŽ»å¹´åŒæœŸæ— æ•°æ®ï¼ˆå¦‚ 2025-01 ç½‘约车订单缺失) | åŒæ¯”留空 | ç•™ç©ºï¼Œæœªå†™ 0 / #DIV/0! | é€šè¿‡ |
| 12 | è´Ÿå‘/边界 | ç¼ºæ¨¡æ¿æ–‡ä»¶ | æŠ›ã€Œæœªæ‰¾åˆ°æ¨¡æ¿æ–‡ä»¶ã€å¹¶å†™å…¥åŽç«¯æ—¥å¿— | æœªè§¦å‘(模板齐全) | ä¸é€‚用 |
| 13 | æ­£å‘ | ç‹¬ç«‹ä»¶åŸŽå¸‚级「城际城乡网约车」(R14 æ­¦æ±‰ / R19 é»„石 â€¦ï¼‰1~6 æœˆ | æœ‰å€¼ï¼ˆæ²¿ç”¨å®šç¨¿ï¼š0 / 13.5998 / 10.668 / â€¦ï¼‰ï¼Œä¸è¢«æ¸…空 | 0 / 13.599765541547697 / 10.668042925780014 â€¦ | é€šè¿‡ |
| 14 | æ­£å‘ | ç‹¬ç«‹ä»¶ 18 åœ°åŒº Ã— 12 ä¸ªæœˆï¼šæ€»è¡Œ = Î£ è¯¥å— 4 ç»´è¡Œã€ç´¯è®¡ = Î£1-8 æœˆ | å…¨éƒ¨ç›¸ç­‰ | ä¸ä¸€è‡´æ•° 0 | é€šè¿‡ |
| 15 | æ­£å‘ | ç‹¬ç«‹ä»¶ä¸Žæ±‡æ€»å¤§è¡¨ã€Œä¸­å£å¾„明细」页 1~8 æœˆé€æ ¼å¯¹ç…§ï¼ˆ18 åœ°åŒº Ã— 5 è¡Œ Ã— 8 æœˆï¼‰ | å®Œå…¨ä¸€è‡´ | 704 å¯¹æ ¼ 0 å·®å¼‚ | é€šè¿‡ |
| 16 | è´Ÿå‘/边界 | æŠ¥è¡¨æœŸä¹‹åŽçš„æœˆä»½åˆ—(9~12 æœˆï¼‰ | æ•°æ®æ ¼ä¸ºç©ºã€è¡¨å¤´ä¿ç•™ | æ•°æ®æ ¼å…¨ç©ºã€è¡¨å¤´ `2026å¹´9月…12月` ä¿ç•™ | é€šè¿‡ |
| 17 | å›žå½’ | æœ¬è½®æ”¹åŠ¨å‰åŽï¼šæ±‡æ€»å¤§è¡¨ 17 é¡µã€ç‹¬ç«‹ã€Šç”Ÿæˆ_中口径排名》 | 0 å·®å¼‚ | å‡ 0 å·®å¼‚;独立《生成_中口径明细》差异仅落在 æ€»è¡Œ/出租城乡/网约车城乡 ä¸‰ç±»è¡Œçš„æœˆä»½åˆ—与累计列 | é€šè¿‡ |
**入库抽查**(报表期 2026-08,与源表逐项对齐)
| æŠ½æŸ¥é¡¹ | åº“内值 | ç”Ÿæˆä»¶å€¼ | ç»“论 |
|---|---|---|---|
| `city_bus_monthly` 8 æœˆ 17 å¸‚州合计 | 12315.37 | ä¸­å£å¾„维度 2 = 1436.63(城际城乡口径,非合计口径) | å£å¾„不同,非同一指标 |
| `city_taxi_monthly` 8 æœˆåŸŽé™…城乡差(荆州 100.94 / å­æ„Ÿ 73.90 / æ©æ–½ 53.54 / è¥„阳 49.84 / å®œæ˜Œ 46.81 / æ­¦æ±‰ 0) | Î£ = 493.90 | ç»´åº¦ 3 å…¨çœ = 493.90 | é€šè¿‡ |
| `wyc_order_monthly` 2026-08(`order_sum` 30508652、`order_wuhan` 11550551、`pin_k` 4997.74) | â€” | ç»´åº¦ 4 å…¨çœ = 426.56 | é€šè¿‡ |
## å››ã€å‘现的问题与处置
1. **(本次已修)独立《生成_中口径明细》扩列后的 7~12 æœˆæ€»è¡Œä¸ºç©ºã€ç´¯è®¡åœåœ¨ 6 æœˆ**
   - æ ¹å› ï¼šæ€»è¡Œå€¼åˆ—在扩列出来的月份里既不是公式也没有缓存值,旧分支 `keepCached` ç­‰äºŽæŠŠæ ¼å­æ¸…空;`evaluateAll()` åˆåœ¨è·¨ç°¿å…¬å¼å¤„整本中断,累计列得不到新值。
   - å¤„置:扩列月份总行改由库内四维合计直接写入;重算改逐格容错。验证见用例 1-4、9。
2. **(未修,需口径决策)《生成_中口径排名》的全省累计与同比与《生成_中口径明细》不自洽**
   - çŽ°è±¡ï¼šæŽ’åä»¶å…¨çœç´¯è®¡ = 29464.6108、累计同比 = 0.638;中口径明细全省累计 = 31528.0607、累计同比 = 0.0581,差值 2063.45 = 2026 å¹´ 1-6 æœˆç»´åº¦ 4(333.39+294.49+347.23+335.39+369.27+383.68)。
   - æ ¹å› ï¼šæŽ’名件由 Java ç”¨ `loadMidClassMap()` çŽ°ç®—ï¼ˆ1-6 æœˆç½‘约车无源 â†’ å°‘一块,且 2025 å¹´åŸºçº¿ä¹Ÿåªæœ‰åº“内口径);中口径明细页走的是母版公式链 + å®šç¨¿ç¼“存值。
   - å»ºè®®ï¼šè§ã€Šæ±‡æ€»è¡¨ä¸‰æ¡ä¸»çº¿ä¿®å¤æ–¹æ¡ˆ_2026-09-18》第 3 æ­¥â€”—统一由「母版公式链 + å°è´¦é¡µã€å‡ºæ•°ï¼ŒJava ä¸å†å¦ç®—一套(详见本轮答复)。
3. **(已纠正的误判)"母版公式引错页"不成立**
   - ä¸Šä¸€è½®ç»“论误判的经过:控制台里中文乱码(`����!`),把 `=出租!C13-出租!C15` å½“成 `=公交!C13-公交!C15`。
   - å¤æ ¸ç»“论(写入文件后按列读取,避开乱码):母版《中口径明细》与独立模板都是**正确的公式链**——
     åŸŽå¸‚级城乡行 = `=[1]出租!C13-[1]出租!C15` / `=[1]网约车!C13-[1]网约车!C15`(台账页 æ€»é‡ âˆ’ åŸŽå¸‚内),
     å…¨çœè¡Œ = `=C13+C18+…`(Σ17 å¸‚州),总行 = `=C6+C7+C8+C9`。
   - åå‘验证:台账页「总量 âˆ’ åŸŽå¸‚内」按 17 å¸‚州求和 = **535.83 / 333.39**,与母版全省行定稿值完全一致;
     æ­¦æ±‰ å‡ºç§Ÿ/网约车城乡 = 0(该市全部为城市内),黄石 = 26.01 / 13.5998,与母版逐项一致。
   - çœŸæ­£çš„æ ¹å› åœ¨æˆ‘们自己的代码:库内无源时把模板公式/缓存值清空(见问题 1 ä¸Ž"缺数据处理规则")。
4. **(数据缺口)2026 å¹´ 1-6 æœˆç½‘约车订单未入库**
   - `wyc_order_monthly` 2026-01~06 ä»…有 `pin_*`,`order_*` å…¨ NULL;模板文件也只有 2026-07 ä¸€è¡Œæœ‰æ•°æ® â†’ ç»´åº¦ 4 çš„ 1-6 æœˆæ— æºã€‚
## äº”、结论
本次改动把「中口径」从 2 ç»´ï¼ˆç­çº¿+公交)统一为 4 ç»´ï¼Œå¹¶ä¿®æŽ‰äº†ç‹¬ç«‹ã€Šç”Ÿæˆ_中口径明细》扩列月份总行为空、累计停在上一期的问题;已验证 12 æ¡ç”¨ä¾‹ï¼ˆ6 æ­£å‘ / 3 å›žå½’ / 3 è´Ÿå‘边界)全部通过,汇总大表 17 é¡µä¸Žã€Šç”Ÿæˆ_中口径排名》零回归。
仍待处理:排名件与明细件的口径归一(问题 2,需业务/方案确认)、网约车 2026-01~06 ä¸Ž 2025-01/09/10 è®¢å•数据缺口(问题 4)。
**补记(同日第二轮)**:本轮把"缺数据保留定稿值 + æ€»è¡Œä¿ç•™å…¬å¼ + è·¨ç°¿å®¹é”™é‡ç®— + æŠ¥è¡¨æœŸä¹‹åŽæœˆä»½æ¸…空"四条并进,
独立《生成_中口径明细》现在**整表自洽**(18 åœ°åŒº Ã— 12 ä¸ªæœˆï¼šæ€»è¡Œ = Î£4 ç»´ã€ç´¯è®¡ = Î£1-8 æœˆï¼Œä¸ä¸€è‡´æ•° 0),
且与汇总大表「中口径明细」页 1~8 æœˆé€æ ¼ä¸€è‡´ï¼ˆ704 æ ¼ 0 å·®å¼‚);汇总大表 17 é¡µä¸ŽæŽ’名件零回归。
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_»ã×Ü±í¹«Ê½À©Ò³ÓëÖØËã_2026-09-18.md
New file
@@ -0,0 +1,85 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_汇总表「公式扩页 + ç”Ÿæˆä»¶é‡ç®—」_2026-09-18
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
修复《道路运输量汇总表》生成件的三条主线问题(用户 2026-09-18 æå‡ºï¼‰ï¼š
| ä¸»çº¿ | çŽ°è±¡ | æ ¹å›  | æœ¬æ¬¡å¤„ç½® |
|---|---|---|---|
| 1 | åŸŽå¸‚客运 / ä¸­å£å¾„明细 8 æœˆæ•´åˆ—为空、累计=1~7 æœˆ | `SummaryWorkbookFiller.ensureMonthFormulas()` çš„**页范围**只含 6 ä¸ªå°è´¦é¡µï¼Œæ¼äº†è¿™ä¸¤é¡µï¼ˆåŒæ¯”公式用的页范围却是含的,所以出现"同比列有公式、当月值列空白") | é¡µèŒƒå›´æ”¹ç”¨ `isMonthLedgerSheet`(含 åŸŽå¸‚客运 / ä¸­å£å¾„明细 / å…¬è·¯æ€»å®¢è¿ / ä¸­å£å¾„客运量) |
| 2 | å…¬äº¤/出租/网约车/城市分市州的**全省行**停在上一期 | å…¨çœè¡Œæ˜¯ `=C9+C13+…`(Σ17市州)公式,但生成件**不带缓存值**,预览/二次处理只读到母版上一期的旧值 | ç”Ÿæˆæ”¶å°¾æ–°å¢ž `recalcWorkbookFormulas()`,逐格 `evaluateFormulaCell` æŠŠç»“果写回缓存值;失败兜底 `fullCalcOnLoad=true` |
| 3(部分) | ä¸­å£å¾„排名数据不对 | `K3/O3` æ˜¯ `SUM(K4:K20)` å…¬å¼ï¼ˆåŒæ ·æ— ç¼“存值);`M3/Q3` åŒæ¯”是写死的库内全省口径 | æœ¬æ¬¡åªè§£å†³"SUM æ— ç¼“存值";写死同比口径 + `loadMidClassMap` ç¼ºä¸¤ç»´**留待下一步**(改动会动口径,需先确认) |
改动文件:
- `traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java`
  - `ensureMonthFormulas()` é¡µèŒƒå›´ä¿®æ­£ï¼›æ–°å¢žæ´¾ç”Ÿé¡µç»“构性常量行按月沿用;新增 `isDerivedFormulaSheet()` / `structuralConstant()`;移除已无用的 `isFilledLedgerSheet()`;加 logger。
- `traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java`
  - æ–°å¢ž `recalcWorkbookFormulas(XSSFWorkbook)`,并在 `exportSummaryWorkbook()` å†™æ–‡ä»¶å‰è°ƒç”¨ã€‚
## äºŒã€æµ‹è¯•环境
- åŽç«¯ Spring Boot(8090)、前端 Vue(8080),`start-dev.ps1` å¯åŠ¨ï¼›MySQL 127.0.0.1:3308 / traffic_audit。
- æ¯ç‰ˆï¼š`docs/生成汇总大表/2026å¹´8月道路运输量汇总表.xlsx`(同期备份母版 `_备份_2026å¹´8月…` **不存在**,日志 WARN,属已知背景)。
- æŠ¥è¡¨æœŸï¼š`2026-08`;入口:`GET /api/report/export/summaryWorkbook?period=2026-08&mode=month`。
- äº§ç‰©ï¼š`_tmp/_qa_out/gen_2026-08_after.xlsx`(1,329,471 B,生成耗时 12.6 s)。
- è¯æ®ï¼š`_tmp/_qa_out/{verify2.txt, verify3.txt, verify5.txt, diff_old_new.txt, after_cols.txt, xml_check.txt, v_cmp.txt}`。
- åŽç«¯æ—¥å¿—:`汇总工作簿公式重算完成:成功 40713 æ ¼ï¼Œå¤±è´¥ 0 æ ¼`、`派生页结构性常量行按月沿用 4 æ ¼ï¼ˆç¬¬ 8 æœˆï¼‰`。
## ä¸‰ã€ç”¨ä¾‹è¡¨
### æ­£å‘用例
| # | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|
| TC1 | ç”Ÿæˆ 2026-08 æ±‡æ€»è¡¨ï¼Œæ£€æŸ¥ã€ŒåŸŽå¸‚客运」Q åˆ—(8 æœˆå½“月值) | æ¯è¡Œåº”有当月值公式(跨表引用右移) | Q4~Q39 å…± 36 æ ¼å…¨éƒ¨æœ‰å…¬å¼ï¼Œå¦‚ `Q9 = =公交!Q16+出租!Q16+网约车!Q16+'轨道、轮渡'!Q9` | é€šè¿‡ |
| TC2 | åŒä¸Šï¼Œæ£€æŸ¥ã€Œä¸­å£å¾„明细」Q åˆ— | 180 è¡Œæœ‰å½“月值(公式或结构性常量) | 180 æ ¼æœ‰å€¼ï¼ˆ176 å…¬å¼ + 4 å¸¸é‡ï¼‰ | é€šè¿‡ |
| TC3 | æŠ½æŸ¥ã€Œä¸­å£å¾„明细」结构性常量行(武汉 åŸŽé™…城乡巡游出租/网约车 = 0) | æŒ‰æœˆæ²¿ç”¨ 0,不留空 | Q13=0.0、Q14=0.0、Q107=0.0、Q108=0.0,日志记「沿用 4 æ ¼ã€ | é€šè¿‡ |
| TC4 | æ ¸å¯¹å…¨çœè¡Œ = Î£17 å¸‚州(独立求和 vs ç¼“存值) | ä¸¤è€…相等 | å…¬äº¤ 12315.37 = 12315.37;出租 5786.81 = 5786.81;网约车 4997.74 = 4997.74 | é€šè¿‡ |
| TC5 | æ£€æŸ¥ 8 æœˆç´¯è®¡åˆ—(S) | åº”含 8 æœˆï¼ˆ=C+E+…+Q) | å…¬äº¤ S5=110776.34、出租 S5=48069.02、网约车 S5=33864.94、中口径明细 S5=31528.06(改动前分别停在 1~7 æœˆï¼‰ | é€šè¿‡ |
| TC6 | æ£€æŸ¥ã€Œä¸­å£å¾„排名」全省行 | `=SUM(K4:K20)` åº”有缓存值 = Î£17市州 | K3 ç¼“å­˜ 24481.7407(与手工 Î£17 å¸‚州一致)、O3 ç¼“å­˜ 1131273.449 | é€šè¿‡ |
| TC7 | æ£€æŸ¥å…¬å¼æ˜¯å¦è¢«ç ´åï¼ˆé‡ç®—是否误用 evaluateInCell) | å…¬å¼ä¸Žç¼“存值并存(`<f>` + `<v>`) | XML å…¨éƒ¨ä¸º `<c ...><f>…</f><v>…</v></c>`,公式保留 | é€šè¿‡ |
| TC8 | æ£€æŸ¥ã€ŒåŸŽå¸‚汇总」「中口径分析」等跨页页 | éšæŠ¥è¡¨æœŸåˆ·æ–°ï¼Œä¸å—本次改动影响 | åŸŽå¸‚汇总 æ€»å®¢è¿é‡ 269628.07(1-8月),母版为 237412.33(1-7月) | é€šè¿‡ |
### è´Ÿå‘ / è¾¹ç•Œç”¨ä¾‹
| # | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|
| TC9 | æ¯ç‰ˆåŒæœŸå¤‡ä»½ï¼ˆ`_备份_2026å¹´8月…`)缺失 | ä¸ä¸­æ–­ç”Ÿæˆï¼Œè®° WARN | æ—¥å¿— `汇总工作簿未找到同期备份母版…`,正常出件 | é€šè¿‡ï¼ˆå·²çŸ¥èƒŒæ™¯ï¼‰ |
| TC10 | å…¬å¼é‡ç®—遇到不支持函数/外部引用 | è·³è¿‡è¯¥æ ¼å¹¶è®¡æ•°ï¼Œä¸ä¸­æ–­å¯¼å‡º | æœ¬æ¬¡å¤±è´¥ 0 æ ¼ï¼›ä»£ç ä¿ç•™é€æ ¼ try-catch + å¤±è´¥æ ·ä¾‹æ—¥å¿— | é€šè¿‡ï¼ˆæœªè§¦å‘) |
| TC11 | æ´¾ç”Ÿé¡µã€Œå…¨æœˆåŒå¸¸é‡ã€åˆ¤å®šè¢«è¯¯ç”¨äºŽå°è´¦é¡µ | å°è´¦é¡µä¸å¾—沿用上月数值(防"结转") | `structuralConstant()` ä»…在 `isDerivedFormulaSheet()` ä¸ºçœŸæ—¶ç”Ÿæ•ˆï¼ˆ6 ä¸ªå°è´¦é¡µä¸é€‚用) | é€šè¿‡ï¼ˆé€»è¾‘隔离) |
| TC12 | é‡ç®—误伤数值格(非公式格) | åªå¤„理 `CellType.FORMULA` çš„æ ¼ | éåŽ†æ—¶è·³è¿‡æ‰€æœ‰éžå…¬å¼æ ¼ | é€šè¿‡ |
### å…¥åº“抽查(库 â†’ æ±‡æ€»è¡¨é€é¡¹å¯¹é½ï¼‰
| å¯¹è±¡ | åº“(`city_bus_monthly` 2026-08) | ç”Ÿæˆä»¶ | ç»“论 |
|---|---|---|---|
| æ­¦æ±‰å¸‚ å®¢è¿é‡ | 3856.83 | å…¬äº¤é¡µ Q9 = 3856.83 | ä¸€è‡´ |
| é»„石市 å®¢è¿é‡ | 879.12 | å…¬äº¤é¡µ Q13 = 879.12 | ä¸€è‡´ |
| 17 å¸‚州合计 | 12315.37 | å…¬äº¤é¡µ Q5(全省行缓存)= 12315.37 | ä¸€è‡´ |
| æ­¦æ±‰å¸‚ åŸŽå¸‚内客运量 | 3837.20 | å…¬äº¤é¡µ Q11 = 3837.20(城市客运页 Q6 å¼•用它) | ä¸€è‡´ |
## å››ã€å›žå½’比对(改动前 vs æ”¹åŠ¨åŽï¼‰
逐格对比改动前后两次生成件(`diff_old_new.txt`):
| é¡µ | å·®å¼‚格数 |
|---|---|
| åŸŽå¸‚客运 | 36 |
| ä¸­å£å¾„明细 | 180 |
| å…¶ä½™ 17 é¡µï¼ˆå«è´§è¿ã€å…¬äº¤ã€å‡ºç§Ÿã€ç½‘约车、班线包车、排名页、跨页合成页) | 0 |
| **合计** | **216** |
差异全部落在本次目标页,且都是「空 â†’ å…¬å¼ã€ï¼›å…¶å®ƒé¡µé€æ ¼ä¸€è‡´ï¼Œæ— å›žå½’。
## äº”、发现的问题与处置
1. **`FormulaEvaluator.evaluate(Cell)` æ— å‰¯ä½œç”¨**(POI 4.1.0):首次实现用了 `evaluate()`,日志显示"成功 40713 æ ¼"但文件里没有缓存值(`<f>` æ—  `<v>`)。改用 `evaluateFormulaCell(Cell)` åŽå†™å…¥æ­£å¸¸ã€‚已把结论写进代码注释。
2. **中口径排名同比仍为写死值**:SUMMARY é‡Œ `M3=0.7424 / Q3=0.2317` ä»æ˜¯æ¯ç‰ˆ/库内全省口径,与 Î£17 å¸‚州(24481.74)口径不一致 â€”— éœ€æŒ‰ç¬¬ 3 æ­¥ç»Ÿä¸€ï¼ˆæ¶‰åŠå£å¾„判定,未在本次改动)。
3. **独立《生成_公路旅客中口径明细》口径缺口**:`loadMidClassMap()` åªè£…"公路班线 + åŸŽé™…城乡公交"两维,缺巡游出租、网约车 â€”— æœªæ”¹ï¼Œç•™å¾…第 3 æ­¥ã€‚
4. **库内 h2031「全省」行 â‰  Î£17 å¸‚å·ž**(1-8 æœˆ 27524.49 vs 24481.74,差 3042.75):属业务口径问题,建议业务侧核对,不阻塞。
## å…­ã€ç»“论
- ç”¨æˆ·æŠ¥çš„"8 æœˆæ²¡æ•°æ® / å…¨çœè¡Œåœåœ¨ä¸Šä¸€æœŸ"两条主线**已修复并经回归验证**:城市客运、中口径明细 8 æœˆåˆ—补齐;全省行、累计列、排名 SUM å‡å¸¦æ­£ç¡®ç¼“存值。
- æ”¹åŠ¨é¢å¯æŽ§ï¼ˆ216 æ ¼ï¼Œå…¨éƒ¨å‘½ä¸­ç›®æ ‡é¡µï¼‰ï¼Œæ— å›žå½’。
- ç¬¬ 3 æ­¥ï¼ˆä¸­å£å¾„排名同比口径统一 + `loadMidClassMap` è¡¥ä¸¤ç»´ï¼‰å¾…用户确认后实施。
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_ÍøÔ¼³µ¶©µ¥±¨¸æWordML¼æÈݵ¼Èë_2026-09-18.md
New file
@@ -0,0 +1,72 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_网约车订单报告 WordML å…¼å®¹å¯¼å…¥_2026-09-18
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
**背景(用户报障)**:把《湖北网络预约出租汽车2025年09月分析报告.docx》放到桌面后用「网约车订单报告」导入,一直失败。
**定位结论**:该文件**不是真正的 docx**。它是 **Word 2003 XML(WordML å¹³é“º XML,单文件)**被改名成了 `.docx`:
- æ–‡ä»¶å¤´æ˜¯ `<?xml version="1.0" ...?> <?mso-application progid="Word.Document"?> <w:wordDocument xmlns:w="http://schemas.microsoft.com/office/word/2003/wordml" ...>`
- 3.1MB çº¯ XML,**不是 zip åŒ…**;POI çš„ `XWPFDocument` åªèƒ½è¯» OPC(zip) åŒ…,于是抛 `File is not a zip file`。
- è¿™ç±»æ–‡ä»¶æ˜¯ WPS/Word「Word 2003 XML æ–‡æ¡£ã€æ ¼å¼ï¼Œç½‘约车监管平台/同事之间流转很常见,会反复出现。
**改动**:`traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java`
1. `importWycOrderReport()` å…¥å£å…ˆæŒ‰æ–‡ä»¶å¤´å—…探格式,再分派:
   - `PK` å¼€å¤´ â†’ docx(zip):原 `XWPFDocument` è·¯å¾„ï¼›
   - `<w:wordDocument` + `wordml` â†’ **WordML å¹³é“º XML:新增解析路径**;
   - `D0CF11E0` â†’ OLE2 è€ `.doc`:给出「请另存为 docx」的明确提示;
   - å…¶å®ƒ â†’ ã€Œæ— æ³•识别的文件格式」。
2. æ–°å¢ž `isWordPackage()`:zip ä½†åŒ…内没有 `word/document.xml`(如 xlsx æ”¹åï¼‰æ—¶ï¼Œç›´æŽ¥æç¤ºã€Œè¿™ä¸æ˜¯ Word æ–‡æ¡£â€¦â€¦å¤šåŠæ˜¯ Excel æˆ–其它格式」,不再抛出 XMLBeans çš„英文堆栈。
3. æ–°å¢ž `parseXmlSafely()`:DOM è§£æžå¹¶**关闭外部实体/DTD**(报告来自外部平台,防 XXE)。
4. è¡¨æ ¼åŒ¹é…é€»è¾‘抽成与格式无关的 `matchWycOrderTable(tables, orders, sumHolder)`:
   - docx è·¯å¾„把 `XWPFTable` æ–‡æœ¬åŒ–后调用;
   - WordML è·¯å¾„把 `<w:tbl>/<w:tr>/<w:tc>` æ–‡æœ¬åŒ–后调用;
   - è¡¨å¤´è¯†åˆ«ã€17 å¸‚州取值、合计行识别**完全复用同一套逻辑**,两条路径不会出现口径分叉。
5. æŠ¥è¡¨æœŸè¯†åˆ«æŠ½æˆ `periodOfParagraphs(List<String>)`,两种格式共用(标题 `网约车行业YYYYå¹´M月` ä¼˜å…ˆï¼Œå–不到回退「前 10 æ®µä»»æ„ YYYYå¹´M月」)。
## äºŒã€æµ‹è¯•环境
- åŽç«¯ 8090(`mvn -o compile` åŽé‡å¯ï¼ŒåŠ è½½æ–°ç±»ï¼›å¯åŠ¨æ—¥å¿— `Started TrafficAuditApplication`);数据库 `traffic_audit`(127.0.0.1:3308)。
- æŽ¥å£ï¼š`POST /api/import/wycOrderReport`(`file` + `period`,`period` å¯ä¸ºç©º â†’ ç”±æŠ¥å‘Šæ ‡é¢˜æŽ¨æ–­ï¼‰ã€‚
- æµ‹è¯•文件:`D:\桌面\湖北网络预约出租汽车2025å¹´09月分析报告.docx`(WordML,3,145,560 å­—节);`D:\05_数据分类\城市客运\8月\湖北网络预约出租汽车2026å¹´08月分析报告0910最终版.docx`(真 docx)。
- è¯æ®æ–‡ä»¶ï¼š`_tmp/_qa_out/`(命令输出见本报告表格;数据核对见 `_wyc_2025_09_row.txt`)。
## ä¸‰ã€ç”¨ä¾‹è¡¨
| ç¼–号 | ç±»åž‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|---|
| 1 | æ­£å‘ | WordML「2025å¹´09月」报告,`period=2025-09` | è§£æžå‡º 17 å¸‚州订单,写入 2025-09 | `导入成功:报表期 2025-09,订单合计 17,652,159 å•,已写入 17 ä¸ªå¸‚å·ž` | é€šè¿‡ |
| 2 | æ­£å‘ | åŒä¸Šï¼Œ`period=` ç•™ç©º | ç”±æ ‡é¢˜ `网约车行业2025å¹´9月` æŽ¨æ–­å‡º 2025-09 | æŠ¥è¡¨æœŸ 2025-09,结果与用例 1 ä¸€è‡´ | é€šè¿‡ |
| 3 | æ­£å‘ | çœŸ docx「2026å¹´08月」报告重导 | ä¸Žåº“内既有值一致(幂等,不改变结果) | è®¢å•合计 30,508,652、武汉 11,550,551,与导入前完全相同 | é€šè¿‡ |
| 4 | æ­£å‘ | 2026å¹´06月报告 | è®¢å•合计 = æŠ¥å‘Šæ­£æ–‡ã€Œå…¨çœè®¢å•总数 2684.30 ä¸‡å•」 | åº“内 2026-06 = 26,843,037 | é€šè¿‡ |
| 5 | è´Ÿå‘ | `.doc`(OLE2 å¤åˆæ–‡æ¡£ï¼ŒWord 97-2003) | æ˜Žç¡®æç¤ºæ”¹ç”¨ docx | `这是 Word 97-2003 è€æ ¼å¼ï¼ˆ.doc),请用 WPS/Word æ‰“开后「另存为」docx æ ¼å¼å†å¯¼å…¥` | é€šè¿‡ |
| 6 | è´Ÿå‘ | xlsx æ”¹åæˆ `.docx` ä¸Šä¼  | æ˜Žç¡®æç¤ºä¸æ˜¯ Word | `这不是 Word æ–‡æ¡£ï¼ˆåŽ‹ç¼©åŒ…é‡Œæ²¡æœ‰ word/document.xml),多半是 Excel æˆ–其它格式……` | é€šè¿‡ |
| 7 | è´Ÿå‘ | éžç½‘约车报告的 docx(《投资表之间的关系.docx》) | æç¤ºæœªè§£æžåˆ°è®¢å•æ•° | `未解析到任何市州订单数,请确认上传的是《网约车行业YYYYå¹´M月运行监测情况分析报告》` | é€šè¿‡ |
| 8 | è¾¹ç•Œ | WordML ä¸­è¡¨æ ¼å« 16 åˆ—、表头在第 1 è¡Œã€å¸‚州名用简称(`武汉`/`林区`) | ç®€ç§°èƒ½æ˜ å°„到规范名(武汉市/神农架林区) | 17 å¸‚州全部识别,无缺失 | é€šè¿‡ |
**入库抽查**(报表期 2025-09,与报告附件1逐项对齐)
| é¡¹ | æŠ¥å‘Šé™„ä»¶1 | åº“内 `wyc_order_monthly` | ç»“论 |
|---|---|---|---|
| è®¢å•合计 | 17,652,159 | 17,652,159 | ä¸€è‡´ |
| æ­¦æ±‰ | 8,076,610 | 8,076,610 | ä¸€è‡´ |
| é»„石 | 676,644 | 676,644 | ä¸€è‡´ |
| åå ° | 1,048,593 | 1,048,593 | ä¸€è‡´ |
| æ©æ–½ | 804,736 | 804,736 | ä¸€è‡´ |
| ç¥žå†œæž¶æž—区 | 1,424 | 1,424 | ä¸€è‡´ |
| å…¨çœ pin(B~E) | æŠ¥å‘Šæ—  | 2794.31 / 2522.54 / 16394.73 / 12998.74(从 `wyc_total_monthly` åŒæœŸå¤åˆ¶ï¼‰ | ç¬¦åˆè®¾è®¡ |
## å››ã€å‘现的问题与处置
1. **格式兼容缺口(本次修复)**:WordML æŠ¥å‘Šæ— æ³•导入。已加解析路径;两种格式共用同一套表格匹配逻辑,避免口径分叉。
2. **错误信息不友好(本次修复)**:xlsx æ”¹ååŽæŠ¥ `org.apache.xmlbeans.XmlException: Element workbook@... is not a valid document@...`。已改为中文业务提示。
3. **20XX å¹´å‰åŽæŠ¥å‘Šæ ‡é¢˜æ ¼å¼ä¸åŒ**:2025 å¹´æŠ¥å‘Šæ ‡é¢˜æ˜¯ã€Œæ¹–北**网约车行业 2025å¹´9月** è¿è¡Œæƒ…况分析报告」,2026 å¹´æ˜¯ã€Œç½‘约车行业2026å¹´6月运行监测情况分析报告」。同一套正则均可命中,无需再改。
4. **数据缺口已由用户补齐**:2026-01~06(14:21 å¯¼å…¥ 6 ä»½ï¼‰ã€2025-01(14:47)、2025-10~12(14:49)均已入库;2025-09 ç”±æœ¬æ¬¡ä¿®å¤åŽè¡¥å…¥ï¼ˆ14:59 é¦–次、15:04 å¤æ ¸ï¼‰ã€‚
   - å…¨éƒ¨ 2025-01~12、2026-01~08 çš„订单与 pin çŽ°å·²é½å…¨ã€‚
   - å½±å“ï¼šä¸­å£å¾„维度 4(城际城乡网约车)1~6 æœˆç”±ã€Œæ— æºã€å˜ä¸ºã€Œå¯ç®—」,**中口径排名件与中口径明细件累计口径自动归一**(此前差值 2063.45 = 1~6 æœˆç½‘约车城乡)。
## äº”、结论
- WordML(Word 2003 XML æ”¹å .docx)报告现已可直接导入,17 å¸‚å·ž + åˆè®¡è§£æžæ­£ç¡®ã€å…¥åº“逐项对齐;真 docx è·¯å¾„无回归。
- 3 æ¡è´Ÿå‘用例均给出中文可操作提示(.doc / éž Word åŒ… / éžæœ¬æŠ¥å‘Šï¼‰ã€‚
- 8 ä¾‹ç”¨ä¾‹å…¨éƒ¨é€šè¿‡ï¼›`docs/功能测试报告/` ä¸‹å·²å½’档本报告。
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_»õÔËÔ±¨³ÖÐøÔÚ¹æÆóÒµ·ÖÊÐÖÝÀÛ¼ÆÔ˾à_2026-09-18.md
New file
@@ -0,0 +1,104 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_货运月报持续在规企业分市州累计运距_2026-09-18
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
- **需求**:将货运月报「持续在规企业」分市州 1-8 æœˆç´¯è®¡è¿è·åŠåŒæ¯”情况整理成 Excel è¡¨æ ¼ï¼Œç”Ÿæˆåˆ°æ¡Œé¢ã€‚
- **口径**:
  - æŒç»­åœ¨è§„企业 = 2026 å¹´ 1-8 æœˆè¿žç»­ 8 ä¸ªæœˆå‡ä¸ŠæŠ¥ã€Šé“路货物运输月度生产情况》(H203-2)月报的企业,共 **636 å®¶**;
  - ç´¯è®¡å¹³å‡è¿è·ï¼ˆå…¬é‡Œï¼‰= 1-8 æœˆç´¯è®¡è´§ç‰©å‘¨è½¬é‡ Ã· 1-8 æœˆç´¯è®¡è´§è¿é‡ï¼ˆåŠ æƒå¹³å‡ï¼‰ï¼›
  - åŒæ¯” = 2026 å¹´ 1-8 æœˆç´¯è®¡å€¼ Ã· 2025 å¹´ 1-8 æœˆç´¯è®¡å€¼ âˆ’ 1,**基期为 2025 å¹´ 1-8 æœˆå…¨éƒ¨è§„上企业上报数**(全省 664 å®¶ï¼‰ï¼Œä¸Žæ±‡æ€»è¡¨ã€Œè´§è¿ã€é¡µ T åˆ—公式 `=S/SUM(V7,X7,…,AJ7)-1` åŒä¸€å£å¾„(v1 æ›¾è¯¯ç”¨ã€Œä¸¤å¹´å‡æŒç»­åœ¨è§„çš„ 545 å®¶ã€ä½œåŸºæœŸï¼Œå·²è®¢æ­£ï¼Œè§ç¬¬å…­èŠ‚ï¼‰ã€‚
- **数据来源**:`traffic_audit.h2032_enterprise_monthly`(2025-01 ~ 2026-08 ä¼ä¸šçº§æœˆæŠ¥ï¼‰ã€‚
- **产出**:`C:\Users\郑玉洁\Desktop\货运月报_持续在规企业_分市州1-8月累计运距及同比_2026-09-18.xlsx`,含 5 ä¸ªé¡µç­¾ï¼š
  `说明与口径`、`分市州汇总`、`分市州分月`、`企业明细`(636 è¡Œï¼‰ã€`口径对比`、`口径校验`。
- **生成脚本**:`docs\工具\持续在规企业累计运距生成.py`(复跑需先生成两份 TSV å¯¼å‡ºä»¶ï¼ŒSQL è§é™„录)。
## äºŒã€æµ‹è¯•环境
| é¡¹ | å†…容 |
| --- | --- |
| æ•°æ®åº“ | MySQL 127.0.0.1:3308 / traffic_audit,表 `h2032_enterprise_monthly` |
| è¿è¡ŒçŽ¯å¢ƒ | Python 3.14.2 + openpyxl 3.1.5 |
| å¯¹ç…§ç‰© 1 | ã€Š2026å¹´8月规上货物周转量与平均运距分析报告(草稿)》全省当月/累计口径 |
| å¯¹ç…§ç‰© 2 | `docs\生成汇总大表\2026å¹´8月道路运输量汇总表.xlsx`「 è´§è¿ã€é¡µã€Œå…¶ä¸­è§„上货运量/规上货物周转量」 |
| è¾“出目录 | å·¥ä½œåŒº `outputs\`(生成)+ æ¡Œé¢ï¼ˆäº¤ä»˜ï¼‰ï¼Œä¸¤ä»½ SHA256 ä¸€è‡´ |
## ä¸‰ã€ç”¨ä¾‹è¡¨
| ç¼–号 | ç±»åž‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- | --- |
| TC01 | æ­£å‘ | 2026-08 å…¨çœè§„上货运量 | 3665.36 ä¸‡å¨ | 3665.36 ä¸‡å¨ | é€šè¿‡ |
| TC02 | æ­£å‘ | 2026-08 å…¨çœè§„上货物周转量 | 570004.44 ä¸‡å¨å…¬é‡Œ | 570004.44 ä¸‡å¨å…¬é‡Œ | é€šè¿‡ |
| TC03 | æ­£å‘ | 2026 å¹´ 1-8 æœˆç´¯è®¡è´§è¿é‡/周转量 | 31399.82 / 4927161.77 | 31399.82 / 4927161.77 | é€šè¿‡ |
| TC04 | æ­£å‘ | 2026-01~07 é€æœˆè§„上货运量、周转量与汇总表「 è´§è¿ã€é¡µæ¯”对(14 é¡¹ï¼‰ | å…¨éƒ¨ä¸€è‡´ | 14/14 ä¸€è‡´ï¼ˆ4 ä½å°æ•°ï¼‰ | é€šè¿‡ |
| TC05 | æ­£å‘ | 17 å¸‚州覆盖面 | å…¨è¦†ç›–、无未识别地区码 | 17/17,未识别 0 æ¡ | é€šè¿‡ |
| TC06 | æ­£å‘ | ä¼ä¸šæ˜Žç»† 636 è¡Œæ±‚å’Œ | = åˆ†å¸‚州汇总合计 | 31399.8248 / 4927161.7743,差异 0 | é€šè¿‡ |
| TC07 | æ­£å‘ | åˆ†å¸‚州分月页 | 8 æœˆå…¨çœ = å‘布口径 155.51 å…¬é‡Œï¼›å„市州累计列 = æ±‡æ€»é¡µç´¯è®¡åˆ— | 155.51;17 ä¸ªå¸‚州逐项 0 å·®å¼‚ | é€šè¿‡ |
| TC08 | æ­£å‘ | å…¨çœ 1-8 æœˆç´¯è®¡å¹³å‡è¿è· | = ç´¯è®¡å‘¨è½¬é‡ Ã· ç´¯è®¡è´§è¿é‡ | 156.92 = 4927161.77 Ã· 31399.82 | é€šè¿‡ |
| TC09 | è¾¹ç•Œ | 2026 å„月上报企业数 | å„月一致,支撑「持续在规」判定 | å„月均为 636 å®¶ï¼Œå…¨æœŸä¸€è‡´ | é€šè¿‡ |
| TC10 | è¾¹ç•Œ | 5 å®¶ 1-8 æœˆè´§è¿é‡ã€å‘¨è½¬é‡å…¨ä¸º 0 çš„企业 | è®¡å…¥ä¼ä¸šæ•°ï¼Œä¸å½±å“å¸‚州运距 | è¿è·æ±‡æ€»æœªå—影响 | é€šè¿‡ |
| TC11 | è¾¹ç•Œ | æ­¦æ±‰çŸ³åŒ–交通运输有限公司、湖北捷龙交通运业有限公司 1 æœˆåœ°åŒºç  420100、其后 420107/420103 | åŒå±žæ­¦æ±‰å¸‚,归集不变 | å‡å½’入武汉市 | é€šè¿‡ |
| TC12 | è´Ÿå‘ | ä»™æ¡ƒå¸‚「两年均持续在规」企业数 = 0 | åŒæ¯”留空 + æ˜Žç¡®å¤‡æ³¨ï¼Œä¸é™¤é›¶æŠ¥é”™ | åŒæ¯”列空,备注「同比不适用:2025 å¹´ 1-8 æœˆæ— è¿žç»­ä¸ŠæŠ¥è®°å½•」 | é€šè¿‡ |
| TC13 | è´Ÿå‘ | ç´¯è®¡è´§è¿é‡ä¸º 0 çš„企业 | ç´¯è®¡è¿è·ç•™ç©ºï¼Œä¸é™¤é›¶æŠ¥é”™ | é€»è¾‘已防除零(本表 636 å®¶æ— æ­¤ç±»ä¼ä¸šï¼‰ | é€šè¿‡ |
| TC14 | è´Ÿå‘ | åœ°åŒºç ä¸åœ¨ 17 å¸‚州映射内 | å½’入「未识别」并在校验页暴露 | å®žé™… 0 æ¡ | é€šè¿‡ |
## å››ã€å‘现的问题与处置
1. **生成脚本行号错位(已修)**:首次生成时汇总页表头被数据覆盖、末行「神农架林区」被合计行覆盖,根因是 `rr` è‡ªå¢žæ¬¡æ•°ä¸Žå†™è¡Œä½ç½®ä¸åŒ¹é…ã€‚修正后重跑,`分市州汇总` ä¸ºè¡¨å¤´ + 17 å¸‚å·ž + åˆè®¡å…± 18 è¡Œï¼Œé€è¡Œæ ¸å¯¹æ— è¯¯ã€‚
2. **桌面副本一度为修正前版本(已修)**:重新同步并校验工作区与桌面文件 SHA256 ä¸€è‡´ï¼ˆ94869 å­—节)。
3. **口径待业务确认(2 é¡¹ï¼‰**:
   - ã€ŒæŒç»­åœ¨è§„企业」按「2026 å¹´ 1-8 æœˆè¿žç»­ 8 ä¸ªæœˆä¸ŠæŠ¥ã€å®šä¹‰ä¸º 636 å®¶ï¼Œ**用户 2026-09-18 å·²ç¡®è®¤æ­¤å£å¾„**(原列出「连续两年在规」「连续 12 ä¸ªæœˆåœ¨è§„」两种备选,均不采用)。
   - åŒæ¯”采用 545 å®¶å¯æ¯”口径(剔除 91 å®¶ 2025 å¹´æ— åŒæœŸè¿žç»­ä¸ŠæŠ¥çš„企业),仙桃市因无可比企业同比留空。
## äº”、结论
14 æ¡ç”¨ä¾‹ï¼ˆ8 æ­£å‘ / 3 è¾¹ç•Œ / 3 è´Ÿå‘)全部通过;订正后另补 TC15–TC18 å…± 4 æ¡ï¼Œäº¦å…¨éƒ¨é€šè¿‡ï¼›å…¨çœå½“月、累计、分市州三个层级与已发布口径**零差异**,明细与汇总逐项对账一致,交付件已生成到桌面。口径已于 2026-09-18 ç”±ç”¨æˆ·ç¡®è®¤ï¼ˆ636 å®¶ï¼‰ï¼ŒåŒæ¯”基期口径已按汇总表公式订正为全口径。
## é™„录:复跑用 SQL
```sql
-- ä¼ä¸šçº§ç´¯è®¡ï¼ˆå¯¼å‡ºä¸º _tmp_persist_scope.tsv)
SELECT MAX(region_code) rc, enterprise_name en, IFNULL(MAX(enterprise_code),'') ec,
       IFNULL(MAX(unified_credit_code),'') ucc,
       SUM(CASE WHEN report_period BETWEEN '2026-01' AND '2026-08' THEN IFNULL(freight_total,0) ELSE 0 END) f26,
       SUM(CASE WHEN report_period BETWEEN '2026-01' AND '2026-08' THEN IFNULL(turnover_total,0) ELSE 0 END) t26,
       COUNT(DISTINCT CASE WHEN report_period BETWEEN '2026-01' AND '2026-08' THEN report_period END) m26,
       SUM(CASE WHEN report_period BETWEEN '2025-01' AND '2025-08' THEN IFNULL(freight_total,0) ELSE 0 END) f25,
       SUM(CASE WHEN report_period BETWEEN '2025-01' AND '2025-08' THEN IFNULL(turnover_total,0) ELSE 0 END) t25,
       COUNT(DISTINCT CASE WHEN report_period BETWEEN '2025-01' AND '2025-08' THEN report_period END) m25
FROM h2032_enterprise_monthly
WHERE report_period BETWEEN '2025-01' AND '2026-08'
GROUP BY enterprise_name HAVING m26 = 8;
-- ä¼ä¸šçº§åˆ†æœˆï¼ˆå¯¼å‡ºä¸º _tmp_persist_scope_monthly.tsv)
SELECT report_period, region_code, enterprise_name, IFNULL(freight_total,0), IFNULL(turnover_total,0)
FROM h2032_enterprise_monthly WHERE report_period BETWEEN '2025-01' AND '2026-08';
```
## å…­ã€è®¢æ­£è®°å½•(2026-09-18,v2)
**问题**:用户反馈 1-8 æœˆç´¯è®¡è¿è·åŒæ¯”不对。核查发现 v1 çš„同比基期取「两年均持续在规的 545 å®¶ä¼ä¸šã€ï¼Œ
而汇总表「货运」页 T åˆ—(累计同比)的公式是 `=S7/SUM(V7,X7,Z7,AB7,AD7,AF7,AH7,AJ7)-1`,
基期为 **2025 å¹´ 1-8 æœˆå…¨éƒ¨è§„上企业**(全省 664 å®¶ï¼‰ã€‚两套基期导致同比系统性偏小。
**证据**:
| é¡¹ç›® | v1(545 å®¶å¯æ¯”口径) | v2(官方全口径,修正后) | å·²å‘布报告 |
| --- | --- | --- | --- |
| 2025 å¹´ 1-8 æœˆè´§è¿é‡åŸºæœŸ(万吨) | â€” | 33994.4483 | æ±‡æ€»è¡¨ V7..AJ7 ç¼“存值合计一致 |
| 2025 å¹´ 1-8 æœˆå‘¨è½¬é‡åŸºæœŸ(万吨公里) | â€” | 5102518.9463 | æ±‡æ€»è¡¨ V8..AJ8 ç¼“存值合计一致 |
| ç´¯è®¡è´§è¿é‡åŒæ¯” | âˆ’7.96% | **−7.63%** | âˆ’7.63% |
| ç´¯è®¡å‘¨è½¬é‡åŒæ¯” | âˆ’5.30% | **−3.44%** | âˆ’3.44% |
| å…¨çœ 1-8 æœˆç´¯è®¡å¹³å‡è¿è·åŒæ¯” | +2.90% | **+4.54%** | â€”(= 156.92 Ã· 150.10 âˆ’ 1) |
**处置**:重算全表,`分市州汇总` æ”¹ä¸ºã€Œ2026 å…¨å£å¾„ / 2025 å…¨å£å¾„ / åŒæ¯”」三段列,新增 `口径对比` é¡µå¹¶åˆ—官方口径与同企业口径,
供业务判断市州落差来源;所有校验项 0 å·®å¼‚。
| ç¼–号 | ç±»åž‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- | --- |
| TC15 | æ­£å‘ | 2025 å¹´ 1-8 æœˆåŸºæœŸè´§è¿é‡ã€å‘¨è½¬é‡ | = æ±‡æ€»è¡¨ã€Œè´§è¿ã€é¡µ V7..AJ7 / V8..AJ8 | 33994.45 / 5102518.95,0 å·®å¼‚ | é€šè¿‡ |
| TC16 | æ­£å‘ | ç´¯è®¡åŒæ¯”(货运量、周转量) | = å‘布报告 âˆ’7.63% / âˆ’3.44% | å®Œå…¨ä¸€è‡´ | é€šè¿‡ |
| TC17 | æ­£å‘ | å…¨çœç´¯è®¡å¹³å‡è¿è·åŒæ¯” | = 156.92 Ã· 150.10 âˆ’ 1 = +4.54% | +4.54% | é€šè¿‡ |
| TC18 | è´Ÿå‘ | v1 çš„ 545 å®¶å£å¾„ | å·®å¼‚可量化、不残留于交付件 | å·®å¼‚ +1.64 ä¸ªç™¾åˆ†ç‚¹ï¼Œå·²æ”¹ä¸ºå¹¶åˆ—展示并标注为参考口径 | é€šè¿‡ |
**遗留提示**:仙桃市官方口径同比 âˆ’89.1%(2025 å¹´ 2 å®¶ä¸ºè¶…长途小运量企业、2026 å¹´æ¢ä¸ºçŸ­é€”大运量企业)、
黄石 +35.3%、襄阳 +21.0% ç­‰ï¼Œå¤šä¸ºä¸¤å¹´è§„上企业名单变动所致,建议业务侧按 `口径对比` é¡µæ ¸å¯¹ã€‚
docs/¹¤×÷ÈÕ½á/¹¤×÷ÈÕ½á_2026-09-18.md
New file
@@ -0,0 +1,205 @@
# å·¥ä½œæ—¥ç»“_2026-09-18
## ä¸€ã€ä»Šå¤©åšäº†ä»€ä¹ˆ
### 1. æ±‡æ€»è¡¨ä¸‰æ¡ä¸»çº¿ï¼ˆç”¨æˆ· 09-18 ä¸Šåˆæå‡ºï¼Œå·²ä¿®å‰ä¸¤æ¡ï¼‰
**先定位,再动手**:复核 8 æœˆç”Ÿæˆä»¶ï¼Œç¡®è®¤ä¸‰æ¡ä¸»çº¿æ˜¯ã€Œä¸€ä¸ªç»Ÿä¸€æ ¹å›  + ä¸€å¤„页范围漏项 + ä¸€å¤„口径缺口」:
- æ¯ç‰ˆæœ¬èº«æ˜¯å…¬å¼é©±åŠ¨çš„ï¼šå…¨çœè¡Œ = Î£17 å¸‚州(`公交!C5 = =C9+C13+…`)、城市客运/中口径明细市州行 = è·¨è¡¨å¼•用(`城市客运!C9 = =公交!C16+出租!C16+网约车!C16+'轨道、轮渡'!C9`)→ è¿™ä¸¤é¡µæ ¹æœ¬ä¸ç”¨å•独写数据。
- ä¸»çº¿ 1 æ ¹å› ï¼š`SummaryWorkbookFiller.ensureMonthFormulas()` é¡µèŒƒå›´åªå« 6 ä¸ªå°è´¦é¡µï¼Œæ¼ åŸŽå¸‚客运/中口径明细;而同比公式用的页范围是含的 â†’ å‡ºçް"同比列有公式、当月值列空白"。
- ä¸»çº¿ 2/3 æ ¹å› ï¼šç”Ÿæˆä»¶**不带公式缓存值**,预览/二次处理读到的是母版上一期的旧值。
**改动**:
- `SummaryWorkbookFiller.ensureMonthFormulas()`:页范围改用 `isMonthLedgerSheet`;派生页新增"结构性常量行按月沿用"(中口径明细 æ­¦æ±‰ åŸŽé™…城乡出租/网约车 æ’为 0 çš„ 4 æ ¼ï¼‰ï¼›åŠ  logger;删掉已无用的 `isFilledLedgerSheet()`。
- `ReportExportService.recalcWorkbookFormulas()`:新增,生成收尾逐格 `evaluateFormulaCell` å†™å›žç¼“存值(失败兜底 `fullCalcOnLoad=true`)。
**踩坑记录**:POI 4.1 çš„ `FormulaEvaluator.evaluate(Cell)` **无副作用**(只算不写),第一版白跑(日志"成功 40713 æ ¼"但文件无 `<v>`),必须用 `evaluateFormulaCell(Cell)`。
### 2. å…¶ä»–
- å†™äº†ã€Šæ±‡æ€»è¡¨ä¸‰æ¡ä¸»çº¿ä¿®å¤æ–¹æ¡ˆ_2026-09-18》(`docs/问题汇总/`),把根因、证据、4 æ­¥ä¿®æ³•、投资"按项目名称更新/新增插行"现状一并记录。
- æŠ•资侧核实:导入确实按「项目名称 + ç±»åˆ«ã€upsert(库内 å®¢è¿ç«™åœº 49/49、物流园区 135/135 æ— é‡å¤è¡Œï¼‰ï¼›ç¼ºå£æ˜¯"模板里没有的新项目不会插行"、"资金渠道分组行导入时被跳过"、"同名不同项目不弹窗"。
### 3. ç¬¬ 3 æ­¥ï¼šä¸­å£å¾„四维口径统一(下午)
**口径定死**(写进《功能测试报告_中口径四维口径统一_2026-09-18》):1 å…¬è·¯ç­çº¿ï¼ˆh2031+个体)= 2 åŸŽé™…城乡公交(city_bus)= 3 åŸŽé™…城乡巡游出租(city_taxi:总量−城市内)= 4 åŸŽé™…城乡网约车(WycSplitCalc æ‹†åˆ†ï¼‰ï¼›æ€»é‡ = å››ç»´ä¹‹å’Œï¼›**全省行一律 Î£17 å¸‚å·ž**。
**改动**(`ReportExportService.java`):
- `loadMidClassMap()` è¡¥ç»´åº¦ 3、维度 4(新增 `midPeriodKey()`);
- `fillMidCell()` åŽ»æŽ‰ã€Œå‡ºç§Ÿ/网约车写死 0」旁路;`fillMidYoyCell()`/`fillMidCumYoyColumn()` åŒæ¯”改四维齐全;
- `exportPassengerMidDetail()` ç”±ã€ŒåªéåŽ†å…¬å¼æ ¼ã€æ”¹ä¸ºã€ŒæŒ‰åˆ—éåŽ†ã€ï¼ˆæ‰©åˆ—å‡ºçš„ 7~12 æœˆç©ºæ ¼æ‰å¡«å¾—上);
- æ‰©åˆ—月份的总行值列改由库内四维合计直接写入(模板本无此列,克隆后既无公式也无缓存值);
- è¯¥å‡½æ•°é‡ç®—改用逐格容错的 `recalcWorkbookFormulas()`——`evaluateAll()` ä¼šåœ¨è·¨ç°¿å…¬å¼ï¼ˆSheet2 çš„ `[1]xx!A1`)处整本中断,累计列就永远拿不到新缓存值。
**验证**:独立《生成_中口径明细》8 æœˆå…¨çœæ€»è¡Œ 4003.5701、7 æœˆ 4028.3213(原为空),18 ä¸ªåœ°åŒºæ€»è¡Œ = Î£4 ç»´ã€ç´¯è®¡åˆ— = Î£1-8 æœˆï¼›ä¸Žæ±‡æ€»å¤§è¡¨ã€Œä¸­å£å¾„明细」页逐格一致;回归 = æ±‡æ€»è¡¨ 17 é¡µ 0 å·®å¼‚、排名件 0 å·®å¼‚、明细件仅 36 æ ¼ï¼ˆ18 åœ°åŒº Ã— 2 ä¸ªæœˆæ€»è¡Œï¼‰å˜åŒ–。
**顺带查清两个老问题**:
- ã€Šç”Ÿæˆ_中口径排名》全省累计 29464.6108 / åŒæ¯” 0.638,比中口径明细少 2063.45,差值正好是 2026 å¹´ 1-6 æœˆç»´åº¦ 4(网约车)→ æŽ’名件是 Java çŽ°ç®—ã€æ˜Žç»†é¡µèµ°æ¯ç‰ˆå…¬å¼é“¾ï¼Œä¸¤å¥—å£å¾„æ‰“æž¶ï¼ˆç¬¬ 3 æ­¥çš„真正收口点)。
- æ¯ç‰ˆã€Œä¸­å£å¾„明细」城市级「城际城乡巡游出租/网约车」行公式引错页(`=公交!C13-公交!C15`,应指向 `出租!`/`网约车!`),城市级恒 0、全省行靠缓存值 â†’ éœ€ä¿®æ¯ç‰ˆå…¬å¼ä¸€æ¬¡ã€‚
### 4. ç¬¬ 3 æ­¥æ”¶å°¾ï¼šç¼ºæ•°æ®ä¸å†è¦†ç›–定稿值(13:30)
用户反馈「独立《生成_中口径明细》8 æœˆæ²¡é—®é¢˜äº†ï¼Œä½† 1~6 æœˆåè€Œè¢«æ¸…空」后,追出**真正根因**:
- `exportPassengerMidDetail()` å¯¹æ‰©åˆ—月份(>6 æœˆï¼‰çš„æ€»è¡Œå€¼åˆ—是「直接写库内四维合计、写 0 å°± setBlank」;
  è€Œ 1~6 æœˆèµ° `keepCached()` æŠŠå…¬å¼æ¸…成空格 â†’ è¡¨é¢ä¸Š 7/8 æœˆä¿®å¥½äº†ï¼Œå®žé™…上把母版的定稿链路掐断了。
- `fillMidCell()` åŒæ ·ã€Œåº“内取值为 0 â†’ setBlank」,于是 2026-01~06 ç½‘约车城乡(库内该 6 ä¸ªæœˆæ— è®¢å•、维度 4 æ— æºï¼‰è¢«æ•´ç‰‡æ¸…空,
  è¿žå¸¦æ€»è¡Œã€ç´¯è®¡ã€åŒæ¯”全错。
**改动(`ReportExportService.java`,4 å¤„)**:
1. `exportPassengerMidDetail()` æ€»è¡Œå€¼åˆ—:删掉「扩列月份手写库内合计」与 `keepCached` æ¸…公式两条旁路,**一律保留模板公式**,由收尾重算写回缓存值。
2. `fillMidCell()`:库内取值为 0(该月该维无源)时**直接 return,保留母版/定稿原值**——绝不写空、也绝不写 0。
3. `recalcWorkbookFormulas()`:新增 `ev.setIgnoreMissingWorkbooks(true)`。跨簿公式(`[1]出租!C13`)在外部工作簿缺失时默认抛异常,
   ä¼šè®©å¼•用它的总行/累计连锁失败(表现为「公式在、没有缓存值」),置 true åŽæ”¹ç”¨è¯¥æ ¼è‡ªèº«ç¼“存值参与计算。
4. æ–°å¢ž `blankFutureMonthColumns()`:报表期之后的月份列(8 æœˆæŠ¥è¡¨é‡Œçš„ 9~12 æœˆï¼‰**清数据格、留表头**,避免克隆公式算出「9~12 æœˆå…¨æ˜¯ 0」的假数据。
**重要纠正(写进测试报告,也在此留痕)**:早前判断「母版中口径明细城市级公式引错页(`=公交!C13-公交!C15`)」是**误判**——
那是控制台中文乱码造成的误读;母版/模板真实公式是正确的 `=[1]出租!C13-[1]出租!C15` / `=[1]网约车!C13-[1]网约车!C15`,
台账页「总量−城市内」Σ17 å¸‚å·ž = 535.83 / 333.39,与母版定稿全省行完全一致。**母版公式不需要修,之前"待修母版"的结论作废。**
### 5. ç½‘约车订单报告:Word 2003 XML(WordML)兼容导入(15:00)
**用户报障**:《湖北网络预约出租汽车2025年09月分析报告.docx》放在桌面导不进去。
**定位**:该文件根本不是 docx â€”— æ˜¯ **Word 2003 XML(WordML å¹³é“º XML)** è¢«æ”¹åæˆ `.docx`(文件头 `<?mso-application progid="Word.Document"?><w:wordDocument xmlns:w=".../word/2003/wordml">`,3.1MB çº¯ XML、不是 zip åŒ…),POI çš„ `XWPFDocument` åªèƒ½è¯» OPC(zip) åŒ… â†’ æŠ› `File is not a zip file`。监管平台/同事间流转的报告常是这种格式,会反复出现。
**改动(`DataImportService.java`)**:
- `importWycOrderReport()` å…¥å£æŒ‰æ–‡ä»¶å¤´å—…探格式分派:zip→docx;`<w:wordDocument`+`wordml`→**新增 WordML DOM è§£æžè·¯å¾„**;`D0CF11E0`→明确提示「.doc è¯·å¦å­˜ä¸º docx」;其它→「无法识别」;
- æ–°å¢ž `isWordPackage()`:zip ä½†æ—  `word/document.xml`(xlsx æ”¹åï¼‰â†’ ä¸­æ–‡æç¤ºï¼Œä¸å†æŠ› XMLBeans è‹±æ–‡å †æ ˆï¼›
- æ–°å¢ž `parseXmlSafely()`:DOM è§£æžå…³é—­å¤–部实体/DTD(防 XXE);
- è¡¨æ ¼åŒ¹é…æŠ½æˆä¸Žæ ¼å¼æ— å…³çš„ `matchWycOrderTable()`,docx ä¸Ž WordML ä¸¤æ¡è·¯å¾„**共用同一套表头识别/17 å¸‚州取值/合计行逻辑**,不会口径分叉;
- æŠ¥è¡¨æœŸè¯†åˆ«æŠ½æˆ `periodOfParagraphs()`,两种格式共用。
**验证**:8 æ¡ç”¨ä¾‹å…¨éƒ¨é€šè¿‡ï¼ˆè§ã€ŠåŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_网约车订单报告WordML兼容导入_2026-09-18》)。2025-09 å…¥åº“:订单合计 17,652,159;武汉 8,076,610 / é»„石 676,644 / åå ° 1,048,593 / æ©æ–½ 804,736 / æž—区 1,424,与报告附件1 é€é¡¹ä¸€è‡´ï¼›pin ä»Ž `wyc_total_monthly` åŒæœŸå¤åˆ¶ã€‚真 docx(2026-08)重导幂等无变化;`.doc`、xlsx æ”¹åã€éžæœ¬æŠ¥å‘Š docx ä¸‰ç§è´Ÿå‘均给出中文可操作提示。
**数据缺口已补齐(重要)**:用户在 14:21~14:49 ä¹‹é—´è‡ªè¡Œå¯¼å…¥ 2026-01~06(6 ä»½ï¼‰ã€2025-01、2025-10~12;2025-09 ç”±æœ¬æ¬¡ä¿®å¤åŽè¡¥å…¥ã€‚现在 **2025-01~12 + 2026-01~08 çš„订单与 pin å…¨éƒ¨é½å…¨**。直接效果:中口径维度 4 çš„ 1~6 æœˆä»Žã€Œæ— æºã€å˜ä¸ºã€Œå¯ç®—」,**中口径排名件与中口径明细件的累计口径自动归一**(两者 1-8 æœˆç´¯è®¡å‡ä¸º 31528.0606,此前差 2063.45)。
## äºŒã€æµ‹è¯•结果摘要
见 `docs/功能测试报告/功能测试报告_汇总表公式扩页与重算_2026-09-18.md`:8 æ¡æ­£å‘ + 4 æ¡è´Ÿå‘/边界 + 4 é¡¹å…¥åº“抽查,全部通过。
- åŸŽå¸‚客运 36 æ ¼ã€ä¸­å£å¾„明细 180 æ ¼ 8 æœˆåˆ—补齐(公式 + ç¼“存值)。
- å…¨çœè¡Œ = Î£17 å¸‚州三方一致:公交 12315.37、出租 5786.81、网约车 4997.74(与库内 `city_bus_monthly` 2026-08 å„市州求和逐项对齐;武汉 3856.83 / é»„石 879.12 æŠ½æŸ¥ä¸€è‡´ï¼‰ã€‚
- ä¸­å£å¾„排名 K3 ç¼“å­˜ 24481.7407 = æ‰‹å·¥ Î£17 å¸‚州。
- å›žå½’:改动前后逐格对比,仅 216 æ ¼å˜åŒ–,全部落在目标两页,其余 17 é¡µ 0 å·®å¼‚。
### ç¬¬ 3 æ­¥ï¼ˆä¸‹åˆï¼‰æµ‹è¯•结果
见 `docs/功能测试报告/功能测试报告_中口径四维口径统一_2026-09-18.md`:6 æ¡æ­£å‘ + 3 æ¡å›žå½’ + 3 æ¡è´Ÿå‘边界,全部通过。
- ç‹¬ç«‹ã€Šç”Ÿæˆ_中口径明细》全省 8 æœˆæ€»å®¢è¿é‡ 4003.5701 = ç­çº¿ 1646.4801 + å…¬äº¤ 1436.63 + å‡ºç§ŸåŸŽä¹¡ 493.90 + ç½‘约车城乡 426.56;
- 18 ä¸ªåœ°åŒº Ã—(7、8 æœˆï¼‰æ€»è¡Œ = Î£ è¯¥å— 4 ç»´è¡Œï¼›ç´¯è®¡åˆ—缓存值 = Î£1-8 æœˆï¼ˆå…¨çœ 31528.0607);
- å›žå½’:汇总大表 17 é¡µ 0 æ ¼å˜åŒ–、《生成_中口径排名》0 æ ¼å˜åŒ–、《生成_中口径明细》仅 36 æ ¼å˜åŒ–(全部是目标格)。
### ç¬¬ 3 æ­¥æ”¶å°¾ï¼ˆ13:30)验证
- ç‹¬ç«‹ã€Šç”Ÿæˆ_中口径明细》:18 åœ°åŒº Ã— 12 ä¸ªæœˆï¼Œ**总行 = Î£4 ç»´ã€ç´¯è®¡ = Î£1-8 æœˆï¼Œä¸ä¸€è‡´æ•° 0**;9~12 æœˆæ•°æ®æ ¼å…¨ç©ºã€è¡¨å¤´ä¿ç•™ã€‚
- ä¸Žæ±‡æ€»å¤§è¡¨ã€Œä¸­å£å¾„明细」页 1~8 æœˆ **704 å¯¹æ ¼ 0 å·®å¼‚**。
- å…¨çœ 1-8 æœˆæ€»å®¢è¿é‡ï¼š3733.79 / 3777.83 / 3874.44 / 3959.25 / 4115.87 / 4034.99 / 4028.32 / 4003.57;
  ç½‘约车城乡:333.39 / 294.49 / 347.23 / 335.39 / 369.27 / 383.68 / 421.46 / 426.56;累计 AA5 = 31528.0607、AA9 = 2911.47。
- åŸŽå¸‚级网约车城乡:武汉 0、黄石 13.599765541547697(沿用定稿,未被清空)。
- å›žå½’:汇总大表 17 é¡µ **0 æ ¼å·®å¼‚**、《生成_中口径排名》**0 æ ¼å·®å¼‚**;
  æ˜Žç»†ä»¶å·®å¼‚仅落在 æ€»è¡Œ / å‡ºç§ŸåŸŽä¹¡ / ç½‘约车城乡 è¡Œçš„æœˆä»½åˆ—与 AA ç´¯è®¡åˆ—(属预期)。
### ç½‘约车订单报告 WordML å¯¼å…¥ï¼ˆ15:00)验证
- 8 æ¡ç”¨ä¾‹ï¼ˆ4 æ­£å‘ / 3 è´Ÿå‘ / 1 è¾¹ç•Œï¼‰å…¨éƒ¨é€šè¿‡ï¼šWordML 2025-09 å¯¼å…¥æˆåŠŸã€æŠ¥è¡¨æœŸç•™ç©ºæ—¶é æ ‡é¢˜è‡ªåŠ¨è¯†åˆ«ã€çœŸ docx å¹‚等无变化、`.doc`/xlsx æ”¹å/非本报告 docx å‡ç»™å‡ºä¸­æ–‡æç¤ºã€‚
- æ•°æ®è¡¥é½åŽé‡è·‘ 8 æœˆå¯¼å‡ºå›žå½’:独立《生成_中口径明细》报表期内不一致 0、报表期后仍有值 0;与汇总大表「中口径明细」页 **704 å¯¹æ ¼ 0 å·®å¼‚**;全省 1-8 æœˆæ€»å®¢è¿é‡ 3733.79…4003.57,网约车城乡 333.39…426.56,累计 AA5=31528.0607、AA9=2911.47。
- æŽ’名件与明细件累计口径一致(均 31528.0606),差值 2063.45 æ¶ˆå¤±ã€‚
## ä¸‰ã€é—留问题与待办
1. ~~中口径排名同比仍是写死值~~ â†’ **已按新口径取值**(M3=0.638 / Q3=0.2247),但仍与中口径明细的 0.0581 ä¸è‡ªæ´½ï¼Œå·®å€¼ 2063.45 = ç½‘约车 1-6 æœˆæ— æºï¼›**收口办法见下方"第 3 æ­¥æ”¶å£å»ºè®®"**。
2. ~~`loadMidClassMap()` ç¼ºå·¡æ¸¸å‡ºç§Ÿ + ç½‘约车两维~~ â†’ **已补**(维度 3、4),独立中口径明细四维齐全。
3. ~~**母版缺陷待修**:中口径明细城市级公式引错页~~ â†’ **误判,已作废**:真实公式是正确的跨簿引用,母版无需修改;当时的"引错页"是控制台中文乱码误读。
4. ~~**数据缺口**:2026-01~06 ç½‘约车订单未入库~~ â†’ **已补齐**(用户 14:21~14:49 å¯¼å…¥ 2026-01~06 + 2025-01 + 2025-10~12;2025-09 ç» WordML ä¿®å¤åŽè¡¥å…¥ï¼‰ã€‚现在 2025-01~12 + 2026-01~08 è®¢å•齐全,维度 4 å…¨æœŸå¯ç®—,排名件/明细件口径归一。仍建议补一条「导入时订单列全空」的显式提示(《模板_网约车订单及全省总量.xlsx》那类文件现在会静默成功)。
5. åº“内 h2031「全省」行 â‰  Î£17 å¸‚州(1-8 æœˆ 27524.49 vs 24481.74,差 3042.75):业务口径问题,建议业务侧核对。
6. æŠ•资:模板里没有的新项目不会自动插行;明细表资金渠道分组行未入库(需加 `fund_channel`);同名不同项目未弹窗。
7. æŠ•资两张明细表(`X月客运站投资明细表` / `X月物流站场投资明细`)生成器未做。
### ç¬¬ 3 æ­¥æ”¶å£å»ºè®®ï¼ˆå¾…用户拍板)
不再让 Java å„算一套,改为**一条链路出数**:母版公式链(全省行 = Î£17 å¸‚州、累计 = Î£å„月、派生页跨页引用)+ å°è´¦é¡µå†™æ•° + ç”Ÿæˆæ”¶å°¾é‡ç®—。
1. å°è´¦é¡µï¼ˆè´§è¿ / ç­çº¿åŒ…车 / å…¬äº¤ / å‡ºç§Ÿ / ç½‘约车 / è½¨é“轮渡)只写当月值;
2. æ´¾ç”Ÿé¡µï¼ˆåŸŽå¸‚客运 / ä¸­å£å¾„明细 / å…¬è·¯æ€»å®¢è¿ / ä¸­å£å¾„客运量 / æŽ’名)**只补公式 + é‡ç®—,不单独写数**;
3. åªæœ‰æ¯ç‰ˆé‡Œæœ¬å°±æ˜¯ç¡¬ç¼–码数值的行(中口径明细的城市级"城际城乡出租/网约车"、全省行)按四维口径回填;
4. ç‹¬ç«‹å¯¼å‡ºä»¶ï¼ˆç”Ÿæˆ_中口径明细 / æŽ’名 / å…¬è·¯æ—…客分市州)改为直接取汇总表对应页的值,或共用同一口径函数;
5. ~~母版里引错页的城市级公式修一次~~ â†’ **无需**(误判,已作废);
6. ç”ŸæˆåŽè·‘"生成件 vs æ¯ç‰ˆé‡ç®—"逐格回归比对,差异清单落日志,作为验收基线;
7. åº“内缺失(该月该维无源)时**一律保留母版定稿值、不覆盖**——已在第 4 æ¡æ”¹åŠ¨ä¸­è½åœ°ã€‚
## å››ã€æ˜Žå¤©è®¡åˆ’
1. ç¬¬ 3 æ­¥æ”¶å£ï¼šæŽ’名件与明细件口径归一(差值 2063.45 = 2026 å¹´ 1-6 æœˆç½‘约车城乡)→ æŽ’名件改取汇总表同源值 â†’ å›ºåŒ–"生成件 vs æ¯ç‰ˆé‡ç®—"逐格回归脚本。
2. ç½‘约车订单导入空列提示(页面明确提示"该期订单列为空,未写入订单数")——数据虽已补齐,但静默成功仍是隐患。
3. æŠ•资"新增项目插行 + åˆ†ç»„行入库(`fund_channel`)"方案落地(等用户对"模块"口径的最后确认)。
4. æŠ•资两张明细表生成器(`X月客运站投资明细表` / `X月物流站场投资明细`)。
---
## äº”、傍晚追加:货运月报「持续在规企业」分市州 1-8 æœˆç´¯è®¡è¿è·åŠåŒæ¯”(桌面交付)
### 5.1 éœ€æ±‚与口径
用户要求:把货运月报「持续在规企业」分市州 1-8 æœˆç´¯è®¡è¿è·åŠåŒæ¯”情况整理成 Excel,生成到桌面。
- **持续在规企业** = 2026 å¹´ 1-8 æœˆè¿žç»­ 8 ä¸ªæœˆå‡ä¸ŠæŠ¥ H203-2 æœˆæŠ¥çš„企业,**636 å®¶**(2026 å¹´å„月上报企业数均为 636,全期一致)。
- **累计平均运距** = 1-8 æœˆç´¯è®¡è´§ç‰©å‘¨è½¬é‡ Ã· ç´¯è®¡è´§è¿é‡ï¼ˆåŠ æƒå¹³å‡ï¼‰ã€‚
- **同比** = 2026 å¹´ 1-8 æœˆç´¯è®¡è¿è· Ã· 2025 å¹´ 1-8 æœˆç´¯è®¡è¿è· âˆ’ 1,只在**两年均持续在规的 545 å®¶**上计算(可比口径)。
### 5.2 æ•°æ®ä¸Žäº§å‡º
数据源:`traffic_audit.h2032_enterprise_monthly`(2025-01 ~ 2026-08,13,056 æ¡ä¼ä¸šçº§è®°å½•)。
产出:`C:\Users\郑玉洁\Desktop\货运月报_持续在规企业_分市州1-8月累计运距及同比_2026-09-18.xlsx`(5 é¡µç­¾ï¼šè¯´æ˜Žä¸Žå£å¾„ / åˆ†å¸‚州汇总 / åˆ†å¸‚州分月 / ä¼ä¸šæ˜Žç»† 636 è¡Œ / å£å¾„校验),生成脚本 `docs\工具\持续在规企业累计运距生成.py`。
### 5.3 å…³é”®ç»“æžœ
| å¸‚å·ž | æŒç»­åœ¨è§„(å®¶) | 1-8月累计平均运距(公里) | åŒæ¯” | å¸‚å·ž | æŒç»­åœ¨è§„(å®¶) | 1-8月累计平均运距(公里) | åŒæ¯” |
| --- | --- | --- | --- | --- | --- | --- | --- |
| æ­¦æ±‰å¸‚ | 199 | 162.11 | +13.5% | é»„冈市 | 71 | 135.14 | +1.1% |
| é»„石市 | 20 | 117.63 | +20.2% | å’¸å®å¸‚ | 17 | 113.33 | âˆ’24.1% |
| åå °å¸‚ | 13 | 321.13 | âˆ’2.9% | éšå·žå¸‚ | 26 | 187.28 | âˆ’17.1% |
| å®œæ˜Œå¸‚ | 65 | 88.55 | +2.6% | æ©æ–½å·ž | 4 | 130.11 | +10.3% |
| è¥„阳市 | 81 | 274.50 | +8.4% | ä»™æ¡ƒå¸‚ | 2 | 65.61 | ä¸é€‚用 |
| é„‚州市 | 6 | 38.15 | âˆ’59.0% | æ½œæ±Ÿå¸‚ | 13 | 394.20 | +10.9% |
| è†é—¨å¸‚ | 40 | 281.41 | âˆ’4.7% | å¤©é—¨å¸‚ | 4 | 61.02 | âˆ’54.6% |
| å­æ„Ÿå¸‚ | 21 | 164.00 | âˆ’7.4% | ç¥žå†œæž¶æž—区 | 2 | 57.68 | âˆ’49.1% |
| è†å·žå¸‚ | 52 | 324.47 | âˆ’4.7% | **全省** | **636** | **156.92** | **+2.9%** |
- å…¨çœç´¯è®¡è´§è¿é‡ 31399.82 ä¸‡å¨ã€ç´¯è®¡å‘¨è½¬é‡ 4927161.77 ä¸‡å¨å…¬é‡Œï¼Œä¸Žã€Š2026å¹´8月规上货物周转量与平均运距分析报告》**完全一致**;8 æœˆå½“月全省平均运距 155.51 å…¬é‡Œäº¦ä¸Žå‘布口径一致。
- ç´¯è®¡è¿è·æ˜Žæ˜¾æ‹‰é•¿çš„只有武汉(+13.5%)、黄石(+20.2%);鄂州(−59.0%)、天门(−54.6%)、林区(−49.1%)、咸宁(−24.1%)、随州(−17.1%)为大幅缩短,建议业务侧关注。
### 5.4 æµ‹è¯•与问题
《功能测试报告_货运月报持续在规企业分市州累计运距_2026-09-18.md》:14 æ¡ç”¨ä¾‹ï¼ˆ8 æ­£å‘ / 3 è¾¹ç•Œ / 3 è´Ÿå‘)全部通过;2026-01~07 é€æœˆè§„上货运量、周转量与汇总表「 è´§è¿ã€é¡µ 14 é¡¹æ¯”对 0 å·®å¼‚。
- ç”Ÿæˆè„šæœ¬è¡Œå·è‡ªå¢žé”™ä½å¯¼è‡´è¡¨å¤´/末行被覆盖 â†’ å·²ä¿®æ­£å¹¶é€è¡Œå¤æ ¸ï¼ˆè¡¨å¤´ + 17 å¸‚å·ž + åˆè®¡å…± 18 è¡Œï¼‰ã€‚
- æ¡Œé¢å‰¯æœ¬ç¬¬ä¸€ç‰ˆä¸ºä¿®æ­£å‰æ–‡ä»¶ â†’ å·²é‡æ–°åŒæ­¥ï¼ŒSHA256 ä¸€è‡´ã€‚
### 5.5 å¾…办
1. ~~请业务确认「持续在规企业」定义~~ â†’ **用户 2026-09-18 å·²ç¡®è®¤**:按「2026 å¹´ 1-8 æœˆè¿žç»­ 8 ä¸ªæœˆä¸ŠæŠ¥ã€= 636 å®¶ï¼›äº¤ä»˜ä»¶æ— éœ€è°ƒæ•´ã€‚
2. ä»™æ¡ƒå¸‚ 2026 å¹´ 2 å®¶æŒç»­åœ¨è§„企业 2025 å¹´ 1-8 æœˆæ— è¿žç»­ä¸ŠæŠ¥è®°å½•,同比留空,建议核对该市州历史上报。
### 5.6 è®¢æ­£ï¼šåŒæ¯”基期口径(v2,17:0x)
用户反馈「1-8 æœˆç´¯è®¡è¿è·åŒæ¯”不对」。核查后确认:**v1 çš„同比基期取错了**。
- æ±‡æ€»è¡¨ã€Œè´§è¿ã€é¡µ T åˆ—(累计同比)公式 = `=S7/SUM(V7,X7,Z7,AB7,AD7,AF7,AH7,AJ7)-1`,基期是 **2025 å¹´ 1-8 æœˆå…¨éƒ¨è§„上企业**(全省 664 å®¶ï¼‰ï¼›v1 ç”¨çš„æ˜¯ã€Œä¸¤å¹´å‡æŒç»­åœ¨è§„çš„ 545 å®¶ã€ï¼Œå¯¼è‡´åŒæ¯”系统性偏小。
- è¯æ®ï¼šDB 2025 å¹´ 1-8 æœˆ = 33994.4483 ä¸‡å¨ / 5102518.9463 ä¸‡å¨å…¬é‡Œï¼Œä¸Žæ±‡æ€»è¡¨ V7..AJ7、V8..AJ8 ç¼“存值**逐项一致**。
| æŒ‡æ ‡ | v1(545 å®¶ï¼‰ | v2(官方全口径) | å‘布报告 |
| --- | --- | --- | --- |
| ç´¯è®¡è´§è¿é‡åŒæ¯” | âˆ’7.96% | **−7.63%** | âˆ’7.63% |
| ç´¯è®¡å‘¨è½¬é‡åŒæ¯” | âˆ’5.30% | **−3.44%** | âˆ’3.44% |
| å…¨çœ 1-8 æœˆç´¯è®¡å¹³å‡è¿è· | 158.18 / 153.73 = +2.90% | **156.92 / 150.10 = +4.54%** | â€” |
- é‡ç®—后 `分市州汇总` æ”¹ä¸ºã€Œ2026 å…¨å£å¾„ / 2025 å…¨å£å¾„ / åŒæ¯”」三段列;新增 `口径对比` é¡µå¹¶åˆ—两种口径,标出差异超 10 ä¸ªç™¾åˆ†ç‚¹çš„市州。
- ä¿®æ­£åŽå¸‚州同比:武汉 +17.0%、黄石 +35.3%、襄阳 +21.0%、宜昌 +7.4%、潜江 +5.6%、随州 +4.9%、恩施 +1.2%、荆州 âˆ’0.2%、黄冈 âˆ’0.3%、十堰 âˆ’1.9%、荆门 âˆ’6.6%、孝感 âˆ’13.3%、咸宁 âˆ’25.4%、林区 âˆ’49.1%、天门 âˆ’54.6%、鄂州 âˆ’63.1%、仙桃 âˆ’89.1%。
- å·²åŒæ­¥æ¡Œé¢äº¤ä»˜ä»¶ï¼ˆSHA256 ä¸€è‡´ï¼‰ã€‚
docs/¹¤¾ß/³ÖÐøÔÚ¹æÆóÒµÀÛ¼ÆÔ˾àÉú³É.py
New file
@@ -0,0 +1,468 @@
# -*- coding: utf-8 -*-
"""货运月报「持续在规企业」分市州 1-8 æœˆç´¯è®¡è¿è·åŠåŒæ¯” å°è´¦ç”Ÿæˆï¼ˆv2:同比基期改为官方全口径)
数据源:traffic_audit.h2032_enterprise_monthly(《道路货物运输月度生产情况》H203-2 ä¼ä¸šæœˆæŠ¥ï¼‰
        å¯¼å‡ºä»¶ _tmp_persist_scope.tsv(企业级累计)、_tmp_persist_scope_monthly.tsv(企业级分月)
口径(v2 ä¿®è®¢ï¼‰ï¼š
  Â· æŒç»­åœ¨è§„企业 = 2026 å¹´ 1-8 æœˆè¿žç»­ 8 ä¸ªæœˆå‡ä¸ŠæŠ¥æœˆæŠ¥çš„企业(636 å®¶ï¼Œç”¨æˆ· 2026-09-18 ç¡®è®¤ï¼‰ï¼›
  Â· ç´¯è®¡å¹³å‡è¿è·ï¼ˆå…¬é‡Œï¼‰= ç´¯è®¡è´§ç‰©å‘¨è½¬é‡ Ã· ç´¯è®¡è´§è¿é‡ï¼ˆåŠ æƒå¹³å‡ï¼‰ï¼›
  Â· åŒæ¯” = 2026 å¹´ 1-8 æœˆç´¯è®¡å€¼ Ã· 2025 å¹´ 1-8 æœˆç´¯è®¡å€¼ âˆ’ 1,**基期为 2025 å¹´ 1-8 æœˆå…¨éƒ¨è§„上企业上报数**
    ï¼ˆå…¨çœ 664 å®¶ï¼‰ï¼Œä¸Žæ±‡æ€»è¡¨ã€Œè´§è¿ã€é¡µ T åˆ— `=S/SUM(V7,X7,…,AJ7)-1` åŒä¸€å£å¾„ï¼›
    v1 è¯¯ç”¨ã€Œä¸¤å¹´å‡æŒç»­åœ¨è§„çš„ 545 å®¶ã€ä½œåŸºæœŸï¼Œå¯¼è‡´åŒæ¯”偏小,已修正。
"""
import io, os
from openpyxl import Workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
SRC = r'D:\文档\ChatGPT\traffic-audit\_tmp_persist_scope.tsv'
MONTHLY_SRC = r'D:\文档\ChatGPT\traffic-audit\_tmp_persist_scope_monthly.tsv'
OUT_DIR = r'D:\文档\ChatGPT\traffic-audit\outputs'
OUT = os.path.join(OUT_DIR, '货运月报_持续在规企业_分市州1-8月累计运距及同比_2026-09-18.xlsx')
PROV = {
    'vol26': 31399.82, 'turn26': 4927161.77,       # å·²å‘布:2026 å¹´ 1-8 æœˆç´¯è®¡
    'vol25': 33994.45, 'turn25': 5102518.95,       # æ±‡æ€»è¡¨ã€Œè´§è¿ã€é¡µ 2025 å¹´ 1-8 æœˆï¼ˆåŒæ¯”基期)
    'vol26_08': 3665.36, 'turn26_08': 570004.44,   # å·²å‘布:2026 å¹´ 8 æœˆå½“月
    'yoy_vol': -0.0763, 'yoy_turn': -0.0344,       # å·²å‘布:1-8 æœˆç´¯è®¡åŒæ¯”
}
CITY_ORDER = ['武汉市', '黄石市', '十堰市', '宜昌市', '襄阳市', '鄂州市', '荆门市', '孝感市', '荆州市',
              '黄冈市', '咸宁市', '随州市', '恩施州', '仙桃市', '潜江市', '天门市', '神农架林区']
CITY = {'4201': '武汉市', '4202': '黄石市', '4203': '十堰市', '4205': '宜昌市', '4206': '襄阳市',
        '4207': '鄂州市', '4208': '荆门市', '4209': '孝感市', '4210': '荆州市', '4211': '黄冈市',
        '4212': '咸宁市', '4213': '随州市', '4228': '恩施州',
        '429004': '仙桃市', '429005': '潜江市', '429006': '天门市', '429021': '神农架林区'}
MONTHS26 = ['2026-%02d' % m for m in range(1, 9)]
def city_of(rc):
    rc = (rc or '').strip()
    return CITY[rc] if rc in CITY else CITY.get(rc[:4], '未识别(%s)' % rc)
def num(x):
    try:
        return float(x)
    except Exception:
        return 0.0
def dist(f, t):
    return (t / f) if f else None
def growth(now, base):
    return (now / base - 1) if (now is not None and base) else None
# ---------- è¯»ä¼ä¸šçº§ç´¯è®¡æ•°æ® ----------
rows = []
with io.open(SRC, 'r', encoding='utf-8') as f:
    hdr = f.readline().rstrip('\r\n').split('\t')
    for line in f:
        p = line.rstrip('\r\n').split('\t')
        if len(p) < len(hdr):
            continue
        d = dict(zip(hdr, p))
        if int(num(d['m26'])) != 8:
            continue
        rows.append(dict(city=city_of(d['rc']), rc=d['rc'], en=d['en'], ec=d['ec'], ucc=d['ucc'],
                         f26=num(d['f26']), t26=num(d['t26']),
                         f25=num(d['f25']), t25=num(d['t25']), cmp=(int(num(d['m25'])) == 8)))
by_city = {}
for r in rows:
    c = by_city.setdefault(r['city'], dict(n=0, f26=0.0, t26=0.0))
    c['n'] += 1
    c['f26'] += r['f26']; c['t26'] += r['t26']
tot = dict(n=0, f26=0.0, t26=0.0)
for c in by_city.values():
    for k in tot:
        tot[k] += c[k]
by_city_cmp = {}
for r in rows:
    if not r['cmp']:
        continue
    c = by_city_cmp.setdefault(r['city'], dict(n=0, f26c=0.0, t26c=0.0, f25c=0.0, t25c=0.0))
    c['n'] += 1
    c['f26c'] += r['f26']; c['t26c'] += r['t26']
    c['f25c'] += r['f25']; c['t25c'] += r['t25']
tot_cmp = dict(n=0, f26c=0.0, t26c=0.0, f25c=0.0, t25c=0.0)
for c in by_city_cmp.values():
    for k in tot_cmp:
        tot_cmp[k] += c[k]
unknown = sorted(k for k in by_city if k.startswith('未识别'))
persist_names = set(r['en'] for r in rows)
# ---------- è¯»ä¼ä¸šçº§åˆ†æœˆæ•°æ® ----------
cust = {}
with io.open(MONTHLY_SRC, 'r', encoding='utf-8') as f:
    for line in f:
        p = line.rstrip('\r\n').split('\t')
        if len(p) < 5 or p[0] == 'report_period':
            continue
        per, rc, en, fr, tu = p[0], p[1], p[2], p[3], p[4]
        cust.setdefault((per, city_of(rc)), {})[en] = (num(fr), num(tu))
def city_month(period, city, names=None):
    d = cust.get((period, city))
    if not d:
        return None, 0.0, 0.0
    fr = tu = 0.0
    for en, v in d.items():
        if names is None or en in names:
            fr += v[0]; tu += v[1]
    return dist(fr, tu), fr, tu
# ---------- 2025 å¹´ 1-8 æœˆå…¨å£å¾„基期 ----------
B25 = {}
for (per, city), d in cust.items():
    if not ('2025-01' <= per <= '2025-08'):
        continue
    c = B25.setdefault(city, dict(ents=set(), f=0.0, t=0.0))
    for en, v in d.items():
        c['ents'].add(en)
        c['f'] += v[0]; c['t'] += v[1]
tot25 = dict(ents=set(), f=0.0, t=0.0)
for c in B25.values():
    tot25['ents'] |= c['ents']
    tot25['f'] += c['f']; tot25['t'] += c['t']
# ---------- æ ·å¼ ----------
THIN = Side(style='thin', color='BFBFBF')
BORDER = Border(left=THIN, right=THIN, top=THIN, bottom=THIN)
HDR_FILL = PatternFill('solid', fgColor='DDEBF7')
TOT_FILL = PatternFill('solid', fgColor='FFF2CC')
TITLE_FILL = PatternFill('solid', fgColor='1F4E79')
CT = Alignment(horizontal='center', vertical='center', wrap_text=True)
LT = Alignment(horizontal='left', vertical='center', wrap_text=True)
RT = Alignment(horizontal='right', vertical='center')
NF2, NF1, NFP = '#,##0.00', '#,##0.0', '0.00%'
wb = Workbook()
# ---------------- Sheet1 è¯´æ˜Žä¸Žå£å¾„ ----------------
ws = wb.active
ws.title = '说明与口径'
ws.sheet_view.showGridLines = False
ws.column_dimensions['A'].width = 16
ws.column_dimensions['B'].width = 106
t = ws.cell(1, 1, '货运月报「持续在规企业」分市州 1-8 æœˆç´¯è®¡è¿è·åŠåŒæ¯”情况表')
t.font = Font(bold=True, size=15, color='FFFFFF')
t.fill = TITLE_FILL
t.alignment = Alignment(horizontal='center', vertical='center')
ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=2)
ws.row_dimensions[1].height = 30
info = [
    ('数据来源', '系统数据库 traffic_audit.h2032_enterprise_monthly《道路货物运输月度生产情况》(H203-2)企业月报,'
                 '取 2025 å¹´ 1 æœˆâ€”2026 å¹´ 8 æœˆå…¨éƒ¨ä¼ä¸šçº§ä¸ŠæŠ¥è®°å½•。'),
    ('持续在规企业', '2026 å¹´ 1—8 æœˆè¿žç»­ 8 ä¸ªæœˆå‡ä¸ŠæŠ¥æœˆæŠ¥çš„企业,共 %d å®¶ï¼ˆ2026 å¹´å„月上报企业数均为 636 å®¶ï¼Œå…¨æœŸä¸€è‡´ï¼›'
                 '口径经用户 2026-09-18 ç¡®è®¤ï¼‰ã€‚' % tot['n']),
    ('累计运距口径', '累计平均运距(公里)= 1—8 æœˆç´¯è®¡è´§ç‰©å‘¨è½¬é‡ Ã· 1—8 æœˆç´¯è®¡è´§è¿é‡ï¼ˆåŠ æƒå¹³å‡ï¼‰ã€‚æºè¡¨å•ä½ä¸ºå¨ã€å¨å…¬é‡Œï¼Œ'
                     '本表折算为万吨、万吨公里,比值不变。'),
    ('同比口径', '同比 = 2026 å¹´ 1—8 æœˆç´¯è®¡å€¼ Ã· 2025 å¹´ 1—8 æœˆç´¯è®¡å€¼ âˆ’ 1。**基期为 2025 å¹´ 1—8 æœˆå…¨éƒ¨è§„上企业上报数**'
                 '(全省 %d å®¶ï¼‰ï¼Œä¸Žæ±‡æ€»è¡¨ã€Œè´§è¿ã€é¡µ T åˆ—公式 =S/SUM(V7,X7,…,AJ7)-1 åŒä¸€å£å¾„,可与发布报告直接对齐:'
                 '全省累计货运量同比 âˆ’7.63%%、周转量同比 âˆ’3.44%%、平均运距同比 +4.54%%。' % len(tot25['ents'])),
    ('全省合计', '一律为 17 ä¸ªå¸‚州之和(不含跨市州重叠口径)。'),
    ('报表期', '本期累计 2026-01-01—2026-08-31;同比基期 2025-01-01—2025-08-31。'),
    ('页签说明', '「分市州汇总」按市州汇总 1-8 æœˆç´¯è®¡è¿è·ä¸ŽåŒæ¯”;「分市州分月」为各市州 2026 å¹´é€æœˆå¹³å‡è¿è·ï¼›'
                 '「企业明细」为 636 å®¶æŒç»­åœ¨è§„企业逐户数据(企业级同比仅对两年均连续上报的 545 å®¶ç»™å‡ºï¼‰ï¼›'
                 '「口径对比」并列官方口径与同企业口径的同比;「口径校验」为明细与汇总对账,以及与汇总表、已发布报告的比对。'),
    ('注意事项', '① æœ‰ 5 å®¶ä¼ä¸š 1—8 æœˆæœˆæŠ¥è´§è¿é‡ã€å‘¨è½¬é‡å‡ä¸º 0,计入企业数但不影响运距;'
                 '② æ­¦æ±‰çŸ³åŒ–交通运输有限公司、湖北捷龙交通运业有限公司 2026 å¹´ 1 æœˆçš„地区码与其后月份不同(420100 â†’ 420107 / 420103),'
                 '但同属武汉市,不影响市州归集;③ ç´¯è®¡è´§è¿é‡ä¸º 0 çš„企业累计运距留空。'),
    ('版本', 'v2(2026-09-18):修正同比基期口径——v1 è¯¯ç”¨ã€Œä¸¤å¹´å‡æŒç»­åœ¨è§„çš„ 545 å®¶ã€ä½œåŸºæœŸï¼Œå…¨çœç´¯è®¡è¿è·åŒæ¯”ç®—å¾— +2.90%%,'
             '官方口径应为 +4.54%%。'),
    ('生成时间', '2026-09-18'),
]
r = 3
for k, v in info:
    a = ws.cell(r, 1, k); a.font = Font(bold=True); a.alignment = LT; a.border = BORDER
    b = ws.cell(r, 2, v); b.alignment = LT; b.border = BORDER
    r += 1
# ---------------- Sheet2 åˆ†å¸‚州汇总 ----------------
ws2 = wb.create_sheet('分市州汇总')
heads = ['序号', '市州', '2026å¹´1-8月持续在规企业数(å®¶)', '2025å¹´1-8月规上企业数(å®¶)',
         '2026å¹´1-8月累计货运量(万吨)', '2026å¹´1-8月累计货物周转量(万吨公里)', '2026å¹´1-8月累计平均运距(公里)',
         '2025å¹´1-8月累计货运量(万吨)', '2025å¹´1-8月累计货物周转量(万吨公里)', '2025å¹´1-8月累计平均运距(公里)',
         '累计平均运距同比(%)', '累计货运量同比(%)', '累计货物周转量同比(%)']
widths = [6, 14, 20, 18, 20, 24, 20, 20, 24, 20, 15, 15, 17]
ws2.cell(1, 1, '货运月报持续在规企业分市州 1-8 æœˆç´¯è®¡å¹³å‡è¿è·åŠåŒæ¯”情况(同比基期:2025 å¹´ 1-8 æœˆå…¨éƒ¨è§„上企业)').font = Font(bold=True, size=13, color='FFFFFF')
ws2.cell(1, 1).fill = TITLE_FILL
ws2.cell(1, 1).alignment = Alignment(horizontal='center', vertical='center')
ws2.merge_cells(start_row=1, start_column=1, end_row=1, end_column=len(heads))
ws2.row_dimensions[1].height = 26
ws2.cell(2, 1, '注:第 5—7 åˆ—为 2026 å¹´æŒç»­åœ¨è§„企业(636 å®¶ï¼‰ç´¯è®¡å€¼ï¼›ç¬¬ 8—10 åˆ—为 2025 å¹´ 1-8 æœˆå…¨éƒ¨è§„上企业(全省 664 å®¶ï¼‰ç´¯è®¡å€¼ï¼Œ'
               '即汇总表「货运」页 T åˆ—同比公式的基期;第 11—13 åˆ—同比口径与发布报告一致。')
ws2.cell(2, 1).font = Font(size=9, italic=True, color='808080')
ws2.merge_cells(start_row=2, start_column=1, end_row=2, end_column=len(heads))
for j, h in enumerate(heads, 1):
    c = ws2.cell(3, j, h)
    c.font = Font(bold=True); c.fill = HDR_FILL; c.alignment = CT; c.border = BORDER
    ws2.column_dimensions[get_column_letter(j)].width = widths[j - 1]
ws2.row_dimensions[3].height = 46
rr = 3
idx = 0
for city in CITY_ORDER:
    c = by_city.get(city)
    if not c or c['n'] == 0:
        continue
    idx += 1
    rr += 1
    b = B25.get(city, dict(ents=set(), f=0.0, t=0.0))
    d26, d25 = dist(c['f26'], c['t26']), dist(b['f'], b['t'])
    vals = [idx, city, c['n'], len(b['ents']),
            c['f26'] / 10000.0, c['t26'] / 10000.0, d26,
            b['f'] / 10000.0, b['t'] / 10000.0, d25,
            growth(d26, d25), growth(c['f26'], b['f']), growth(c['t26'], b['t'])]
    for j, v in enumerate(vals, 1):
        cell = ws2.cell(rr, j, v)
        cell.border = BORDER
        cell.alignment = CT if j <= 4 else RT
        if j in (5, 6, 7, 8, 9, 10):
            cell.number_format = NF2
        elif j in (11, 12, 13):
            cell.number_format = NFP
rr += 1
d26p, d25p = dist(tot['f26'], tot['t26']), dist(tot25['f'], tot25['t'])
vals = ['', '全省合计', tot['n'], len(tot25['ents']),
        tot['f26'] / 10000.0, tot['t26'] / 10000.0, d26p,
        tot25['f'] / 10000.0, tot25['t'] / 10000.0, d25p,
        growth(d26p, d25p), growth(tot['f26'], tot25['f']), growth(tot['t26'], tot25['t'])]
for j, v in enumerate(vals, 1):
    cell = ws2.cell(rr, j, v)
    cell.border = BORDER
    cell.fill = TOT_FILL
    cell.font = Font(bold=True)
    cell.alignment = CT if j <= 4 else RT
    if j in (5, 6, 7, 8, 9, 10):
        cell.number_format = NF2
    elif j in (11, 12, 13):
        cell.number_format = NFP
ws2.freeze_panes = 'C4'
# ---------------- Sheet3 åˆ†å¸‚州分月 ----------------
ws3 = wb.create_sheet('分市州分月')
h3 = ['序号', '市州', '持续在规企业数(å®¶)'] + ['%s月平均运距(公里)' % m for m in range(1, 9)] + \
     ['2026å¹´1-8月累计平均运距(公里)', '2025å¹´1-8月累计平均运距(公里)', '累计运距同比(%)']
w3 = [6, 14, 18] + [15] * 8 + [24, 24, 15]
ws3.cell(1, 1, '分市州 2026 å¹´é€æœˆå¹³å‡è¿è·ï¼ˆæŒç»­åœ¨è§„企业口径;同比基期:2025 å¹´ 1-8 æœˆå…¨éƒ¨è§„上企业)').font = Font(bold=True, size=13, color='FFFFFF')
ws3.cell(1, 1).fill = TITLE_FILL
ws3.cell(1, 1).alignment = Alignment(horizontal='center', vertical='center')
ws3.merge_cells(start_row=1, start_column=1, end_row=1, end_column=len(h3))
ws3.row_dimensions[1].height = 26
for j, h in enumerate(h3, 1):
    c = ws3.cell(2, j, h)
    c.font = Font(bold=True); c.fill = HDR_FILL; c.alignment = CT; c.border = BORDER
    ws3.column_dimensions[get_column_letter(j)].width = w3[j - 1]
ws3.row_dimensions[2].height = 40
rr = 2
idx = 0
for city in CITY_ORDER:
    c = by_city.get(city)
    if not c or c['n'] == 0:
        continue
    idx += 1
    rr += 1
    b = B25.get(city, dict(ents=set(), f=0.0, t=0.0))
    d26, d25 = dist(c['f26'], c['t26']), dist(b['f'], b['t'])
    vals = [idx, city, c['n']] + [city_month(p, city, persist_names)[0] for p in MONTHS26] + \
           [d26, d25, growth(d26, d25)]
    for j, v in enumerate(vals, 1):
        cell = ws3.cell(rr, j, v)
        cell.border = BORDER
        cell.alignment = CT if j <= 3 else RT
        if j >= 4:
            cell.number_format = NFP if j == 14 else NF2
rr += 1
vals = ['', '全省合计', tot['n']] + [city_month(p, None)[0] for p in MONTHS26] + \
       [d26p, d25p, growth(d26p, d25p)]
for j, v in enumerate(vals, 1):
    cell = ws3.cell(rr, j, v)
    cell.border = BORDER
    cell.fill = TOT_FILL
    cell.font = Font(bold=True)
    cell.alignment = CT if j <= 3 else RT
    if j >= 4:
        cell.number_format = NFP if j == 14 else NF2
ws3.freeze_panes = 'D3'
# ---------------- Sheet4 ä¼ä¸šæ˜Žç»† ----------------
ws4 = wb.create_sheet('企业明细')
h4 = ['序号', '市州', '企业名称', '企业代码', '统一社会信用代码',
      '2026å¹´1-8月货运量(万吨)', '2026å¹´1-8月周转量(万吨公里)', '2026å¹´1-8月累计平均运距(公里)',
      '2025å¹´1-8月货运量(万吨)', '2025å¹´1-8月周转量(万吨公里)', '2025å¹´1-8月累计平均运距(公里)',
      '平均运距同比(%)', '是否两年均持续在规']
w4 = [6, 12, 42, 22, 24, 20, 24, 22, 20, 24, 22, 14, 18]
ws4.cell(1, 1, '持续在规企业逐户累计平均运距(2026 å¹´ 1-8 æœˆï¼›åŒæœŸå€¼ä¸ŽåŒæ¯”仅对两年均持续在规的 545 å®¶ç»™å‡ºï¼‰').font = Font(bold=True, size=13, color='FFFFFF')
ws4.cell(1, 1).fill = TITLE_FILL
ws4.cell(1, 1).alignment = Alignment(horizontal='center', vertical='center')
ws4.merge_cells(start_row=1, start_column=1, end_row=1, end_column=len(h4))
ws4.row_dimensions[1].height = 26
for j, h in enumerate(h4, 1):
    c = ws4.cell(2, j, h)
    c.font = Font(bold=True); c.fill = HDR_FILL; c.alignment = CT; c.border = BORDER
    ws4.column_dimensions[get_column_letter(j)].width = w4[j - 1]
ws4.row_dimensions[2].height = 40
order = {c: i for i, c in enumerate(CITY_ORDER)}
rows_sorted = sorted(rows, key=lambda x: (order.get(x['city'], 99), -(x['t26'])))
rr = 2
for i, x in enumerate(rows_sorted, 1):
    rr += 1
    d26, d25 = dist(x['f26'], x['t26']), dist(x['f25'], x['t25'])
    vals = [i, x['city'], x['en'], x['ec'], x['ucc'],
            x['f26'] / 10000.0, x['t26'] / 10000.0, d26,
            (x['f25'] / 10000.0) if x['cmp'] else None,
            (x['t25'] / 10000.0) if x['cmp'] else None,
            d25 if x['cmp'] else None,
            growth(d26, d25) if x['cmp'] else None,
            '是' if x['cmp'] else '否']
    for j, v in enumerate(vals, 1):
        cell = ws4.cell(rr, j, v)
        cell.border = BORDER
        cell.alignment = LT if j in (3, 4, 5) else (CT if j in (1, 2, 13) else RT)
        if j in (6, 7, 8, 9, 10, 11):
            cell.number_format = NF2
        elif j == 12:
            cell.number_format = NFP
        if j == 12 and isinstance(v, float) and v > 0.5:
            cell.font = Font(color='C00000', bold=True)
ws4.freeze_panes = 'D3'
ws4.auto_filter.ref = 'A2:M%d' % rr
# ---------------- Sheet5 å£å¾„对比 ----------------
ws6 = wb.create_sheet('口径对比')
h6 = ['序号', '市州', '2026å¹´1-8月累计平均运距(公里)\n[持续在规企业636å®¶]',
      '2025å¹´1-8月累计平均运距(公里)\n[全部规上企业]', '官方口径同比(%)',
      '2025å¹´1-8月累计平均运距(公里)\n[同企业545å®¶]', '同企业口径同比(%)', '两者差异(百分点)', '说明']
w6 = [6, 14, 26, 24, 15, 24, 15, 16, 46]
ws6.cell(1, 1, '两种同比口径对比(官方口径 = ä¸Žæ±‡æ€»è¡¨ã€Œè´§è¿ã€é¡µ T åˆ—一致;同企业口径 = ä»…两年均持续在规企业)').font = Font(bold=True, size=13, color='FFFFFF')
ws6.cell(1, 1).fill = TITLE_FILL
ws6.cell(1, 1).alignment = Alignment(horizontal='center', vertical='center')
ws6.merge_cells(start_row=1, start_column=1, end_row=1, end_column=len(h6))
ws6.row_dimensions[1].height = 26
ws6.cell(2, 1, '注:交付主表(分市州汇总)采用官方口径;本页两个口径并列,差异大的市州多为两年间规上企业名单变动(停产、新增、拆分)所致,供业务核对参考。')
ws6.cell(2, 1).font = Font(size=9, italic=True, color='808080')
ws6.merge_cells(start_row=2, start_column=1, end_row=2, end_column=len(h6))
for j, h in enumerate(h6, 1):
    c = ws6.cell(3, j, h)
    c.font = Font(bold=True); c.fill = HDR_FILL; c.alignment = CT; c.border = BORDER
    ws6.column_dimensions[get_column_letter(j)].width = w6[j - 1]
ws6.row_dimensions[3].height = 48
rr = 3
idx = 0
for city in CITY_ORDER:
    c = by_city.get(city)
    if not c or c['n'] == 0:
        continue
    idx += 1
    rr += 1
    b = B25.get(city, dict(ents=set(), f=0.0, t=0.0))
    g = by_city_cmp.get(city)
    d26, d25 = dist(c['f26'], c['t26']), dist(b['f'], b['t'])
    g_off = growth(d26, d25)
    if g:
        dc26, dc25 = dist(g['f26c'], g['t26c']), dist(g['f25c'], g['t25c'])
        g_same = growth(dc26, dc25)
    else:
        dc25, g_same = None, None
    diff = (g_off - g_same) if (g_off is not None and g_same is not None) else None
    note = ''
    if diff is not None and abs(diff) > 0.10:
        note = '两口径差异超 10 ä¸ªç™¾åˆ†ç‚¹ï¼Œä¸»è¦å› ä¸¤å¹´è§„上企业名单变动,建议核对该市州企业变动情况'
    vals = [idx, city, d26, d25, g_off, dc25, g_same, (diff * 100 if diff is not None else None), note]
    for j, v in enumerate(vals, 1):
        cell = ws6.cell(rr, j, v)
        cell.border = BORDER
        cell.alignment = LT if j in (1, 9) else (CT if j == 2 else RT)
        if j in (3, 4, 6):
            cell.number_format = NF2
        elif j in (5, 7):
            cell.number_format = NFP
        elif j == 8:
            cell.number_format = NF1
rr += 1
g_cmp = growth(dist(tot_cmp['f26c'], tot_cmp['t26c']), dist(tot_cmp['f25c'], tot_cmp['t25c']))
vals = ['', '全省合计', d26p, d25p, growth(d26p, d25p), dist(tot_cmp['f25c'], tot_cmp['t25c']), g_cmp,
        (growth(d26p, d25p) - g_cmp) * 100 if g_cmp is not None else None, '']
for j, v in enumerate(vals, 1):
    cell = ws6.cell(rr, j, v)
    cell.border = BORDER
    cell.fill = TOT_FILL
    cell.font = Font(bold=True)
    cell.alignment = CT if j == 2 else RT
    if j in (3, 4, 6):
        cell.number_format = NF2
    elif j in (5, 7):
        cell.number_format = NFP
    elif j == 8:
        cell.number_format = NF1
ws6.freeze_panes = 'C4'
# ---------------- Sheet5 å£å¾„校验 ----------------
ws5 = wb.create_sheet('口径校验')
ws5.sheet_view.showGridLines = False
ws5.column_dimensions['A'].width = 46
for col, w in (('B', 20), ('C', 20), ('D', 14), ('E', 46)):
    ws5.column_dimensions[col].width = w
ws5.cell(1, 1, '口径校验与对账').font = Font(bold=True, size=13, color='FFFFFF')
ws5.cell(1, 1).fill = TITLE_FILL
ws5.merge_cells(start_row=1, start_column=1, end_row=1, end_column=5)
sum_f = sum(r['f26'] for r in rows) / 10000.0
sum_t = sum(r['t26'] for r in rows) / 10000.0
aug_f = sum(v[0] for c in CITY_ORDER for en, v in cust.get(('2026-08', c), {}).items() if en in persist_names) / 10000.0
aug_t = sum(v[1] for c in CITY_ORDER for en, v in cust.get(('2026-08', c), {}).items() if en in persist_names) / 10000.0
chk = [
    ('校验项', '本表数值', '对照数值', '差异', '说明'),
    ('明细表企业数 = æ±‡æ€»è¡¨ä¼ä¸šæ•°', len(rows), tot['n'], len(rows) - tot['n'], '636 å®¶æŒç»­åœ¨è§„企业逐户一行'),
    ('明细表货运量合计(万吨)', round(sum_f, 2), round(tot['f26'] / 10000.0, 2), round(sum_f - tot['f26'] / 10000.0, 2), '明细求和 = 17 å¸‚州汇总合计'),
    ('明细表周转量合计(万吨公里)', round(sum_t, 2), round(tot['t26'] / 10000.0, 2), round(sum_t - tot['t26'] / 10000.0, 2), '明细求和 = 17 å¸‚州汇总合计'),
    ('2026å¹´1-8月累计货运量(万吨)', round(tot['f26'] / 10000.0, 2), PROV['vol26'], round(tot['f26'] / 10000.0 - PROV['vol26'], 2), '对照已发布报告 1-8 æœˆç´¯è®¡'),
    ('2026å¹´1-8月累计周转量(万吨公里)', round(tot['t26'] / 10000.0, 2), PROV['turn26'], round(tot['t26'] / 10000.0 - PROV['turn26'], 2), '同上'),
    ('2026å¹´8月全省货运量(万吨)', round(aug_f, 2), PROV['vol26_08'], round(aug_f - PROV['vol26_08'], 2), '验证市州归集无误'),
    ('2026å¹´8月全省周转量(万吨公里)', round(aug_t, 2), PROV['turn26_08'], round(aug_t - PROV['turn26_08'], 2), '同上'),
    ('2025å¹´1-8月累计货运量(万吨)【同比基期】', round(tot25['f'] / 10000.0, 2), PROV['vol25'], round(tot25['f'] / 10000.0 - PROV['vol25'], 2), '对照汇总表「货运」页 V7..AJ7(缓存值)'),
    ('2025å¹´1-8月累计周转量(万吨公里)【同比基期】', round(tot25['t'] / 10000.0, 2), PROV['turn25'], round(tot25['t'] / 10000.0 - PROV['turn25'], 2), '对照汇总表「货运」页 V8..AJ8(缓存值)'),
    ('2025å¹´1-8月全省规上企业数(å®¶)', len(tot25['ents']), 664, len(tot25['ents']) - 664, '同比基期企业范围'),
    ('累计货运量同比', growth(tot['f26'], tot25['f']), PROV['yoy_vol'], round(growth(tot['f26'], tot25['f']) - PROV['yoy_vol'], 4), '对照已发布报告 1-8 æœˆç´¯è®¡åŒæ¯”'),
    ('累计周转量同比', growth(tot['t26'], tot25['t']), PROV['yoy_turn'], round(growth(tot['t26'], tot25['t']) - PROV['yoy_turn'], 4), '同上'),
    ('2025å¹´1-8月累计平均运距(公里)', round(d25p, 2), None, None, '= 2025 ç´¯è®¡å‘¨è½¬é‡ Ã· ç´¯è®¡è´§è¿é‡'),
    ('2026å¹´1-8月累计平均运距(公里)', round(d26p, 2), None, None, '= 2026 ç´¯è®¡å‘¨è½¬é‡ Ã· ç´¯è®¡è´§è¿é‡'),
    ('全省累计平均运距同比', growth(d26p, d25p), None, None, '= 156.92 Ã· 150.10 âˆ’ 1'),
    ('覆盖市州数', len([c for c in CITY_ORDER if by_city.get(c, {}).get('n')]), 17, None, '17 ä¸ªå¸‚州全覆盖'),
    ('未识别地区码', len(unknown), 0, None, ('、'.join(unknown) if unknown else '无')),
]
for i, row in enumerate(chk, 1):
    for j, v in enumerate(row, 1):
        cell = ws5.cell(i + 2, j, v)
        cell.border = BORDER
        cell.alignment = CT if i == 1 else (LT if j in (1, 5) else RT)
        if i == 1:
            cell.font = Font(bold=True); cell.fill = HDR_FILL
        elif j in (2, 3, 4):
            cell.number_format = NFP if row[0].endswith('同比') else NF2
os.makedirs(OUT_DIR, exist_ok=True)
wb.save(OUT)
print('OK ->', OUT)
print('2026 1-8月: %.4f ä¸‡å¨ / %.4f ä¸‡å¨å…¬é‡Œ / %.4f å…¬é‡Œ' % (tot['f26'] / 10000.0, tot['t26'] / 10000.0, d26p))
print('2025 1-8月: %.4f ä¸‡å¨ / %.4f ä¸‡å¨å…¬é‡Œ / %.4f å…¬é‡Œ (%d å®¶)' % (tot25['f'] / 10000.0, tot25['t'] / 10000.0, d25p, len(tot25['ents'])))
print('全省同比: è´§è¿é‡ %.2f%%  å‘¨è½¬é‡ %.2f%%  å¹³å‡è¿è· %.2f%%' %
      (growth(tot['f26'], tot25['f']) * 100, growth(tot['t26'], tot25['t']) * 100, growth(d26p, d25p) * 100))
for city in CITY_ORDER:
    c = by_city.get(city)
    if c and c['n']:
        b = B25.get(city, dict(ents=set(), f=0.0, t=0.0))
        d1, d0 = dist(c['f26'], c['t26']), dist(b['f'], b['t'])
        print('%-8s n=%-4d 2026=%8.2f 2025=%8s åŒæ¯”=%s' % (
            city, c['n'], d1, ('%.2f' % d0) if d0 else '-', ('%.1f%%' % (growth(d1, d0) * 100)) if d0 else '-'))
docs/Ͷ×Ê/ÊäÈë/ÎïÁ÷£¨ÎïÁ÷Ô°Çø£©/7ÔÂ/2026Äê7ÔÂÆóÒµÔ±¨»ã×Ü£¨¶õÖÝ£©.xls
Binary files differ
docs/Ͷ×Ê/Êä³ö/XÔÂÎïÁ÷Õ¾³¡Í¶×ÊÃ÷ϸ.xls
Binary files differ
docs/Ͷ×Ê/Êä³ö/ºþ±±Ê¡£¨¿Í»õÕ¾³¡£©£¨ÒÚԪͶ×ÊÏîÄ¿£©7ÔÂ.xls
Binary files differ
docs/ÎÊÌâ»ã×Ü/»ã×ܱíÈýÌõÖ÷ÏßÐÞ¸´·½°¸_2026-09-18.md
New file
@@ -0,0 +1,66 @@
# æ±‡æ€»è¡¨ä¸‰æ¡ä¸»çº¿ä¿®å¤æ–¹æ¡ˆ + æŠ•资「按项目名称更新」现状(2026-09-18)
> ç»“论先行:三条主线不是三个独立 bug,而是 **一个统一根因(公式不重算)+ ä¸€å¤„页范围漏项 + ä¸€å¤„口径缺口**。
> è¯æ®æ–‡ä»¶ï¼š`_tmp\_qa_out\生成_道路运输量汇总表_2026-08.xlsx`、`_tmp\_qa_out\gencols.txt`、`_tmp\_qa_out\genrows.txt`。
## ä¸€ã€ä¸‰æ¡ä¸»çº¿çš„æ ¹å› 
### è¯æ®ï¼ˆåˆšå¤æ ¸ï¼‰
| é¡µ | 8月当月值列(Q) | 8月同比列(R) | è¯´æ˜Ž |
|---|---|---|---|
| å…¬äº¤ / å‡ºç§Ÿ / ç½‘约车 / ç­çº¿åŒ…车 / è½¨é“、轮渡 | **有公式**(`=Q9+Q13+…`),**无缓存值**(cached=None) | æœ‰å…¬å¼ | å…¬å¼å†™äº†ä½†æ²¡ç®—,读值工具只看到空/旧值 |
| åŸŽå¸‚客运 / ä¸­å£å¾„明细 | **连公式都没有**(Q5=None、Q9=None) | **有公式**(`=Q5/AJ5-1`) | åŒæ¯”写在、当月值没写 â†’ å…¸åž‹"页范围不一致" |
| ä¸­å£å¾„排名 | `K3='=SUM(K4:K20)'`、`O3='=SUM(O4:O20)'`(无缓存值) | `M3=0.7424`、`Q3=0.2317` **是写死的数** | SUM æ²¡é‡ç®— + å…¨çœåŒæ¯”写死成库内全省口径 |
### å…³é”®äº‹å®ž
1. **母版本身就是公式驱动的**(不需要人工再做模板):
   - å…¨çœè¡Œ = Î£17 å¸‚州:`公交!C5 = =C9+C13+…+C73`、`中口径明细!C5 = =C6+C7+C8+C9`;
   - åŸŽå¸‚客运 / ä¸­å£å¾„明细的市州行 = è·¨è¡¨å¼•用:`城市客运!C9 = =公交!C16+出租!C16+网约车!C16+'轨道、轮渡'!C9`、`中口径明细!C11 = =班线包车!C8`。
   - æ‰€ä»¥è¿™ä¸¤é¡µ**根本不需要单独写数据**,只要"按月右移公式"补齐 + é‡ç®—,整页自动就对。
2. **两个"补公式"函数的页范围不一致**(`SummaryWorkbookFiller.java`):
   - `ensureMonthYoyFormulas()` ç”¨ `isMonthLedgerSheet()` â†’ å« åŸŽå¸‚客运 / ä¸­å£å¾„明细 / å…¬è·¯æ€»å®¢è¿ï¼›
   - `ensureMonthFormulas()` ç”¨ `isFilledLedgerSheet()` â†’ åªæœ‰ 6 ä¸ªå°è´¦é¡µï¼ˆè´§è¿/班线包车/公交/出租/网约车/轨道轮渡)← **漏项就在这里**。
3. **生成件里的公式没有缓存值**:POI å†™æ–‡ä»¶æ—¶ä¸é‡ç®—,母版里 7 æœˆçš„缓存值被原样留下,所以"全省行/累计/排名停在上一期"。Excel æ‰“开靠 `fullCalcOnLoad` ä¼šç®—,但**系统预览、二次处理、部分 WPS éƒ½çœ‹ä¸åˆ°æ–°å€¼**。
### å½’纳
| ä¸»çº¿ | çŽ°è±¡ | æ ¹å›  |
|---|---|---|
| 1 | åŸŽå¸‚客运 / ä¸­å£å¾„明细 8月整列空白、累计=1~7月 | `ensureMonthFormulas()` é¡µèŒƒå›´æ¼äº†è¿™ä¸¤é¡µ |
| 2 | å…¬äº¤/出租/网约车/城市分市州的**全省行**停在 7 æœˆ | å…¬å¼æ— ç¼“存值(未重算) |
| 3 | ä¸­å£å¾„排名数据不对 | åŒä¸»çº¿ 2;另加"全省同比"写死成库内全省口径(1-8月 27524.49)与 Î£17市州(24481.74)打架 |
## äºŒã€ä¿®å¤æ–¹æ¡ˆï¼ˆ4 æ­¥ï¼‰
1. **补公式页范围**:`SummaryWorkbookFiller.ensureMonthFormulas()` çš„页判断改用 `isMonthLedgerSheet`(含 åŸŽå¸‚客运 / ä¸­å£å¾„明细 / å…¬è·¯æ€»å®¢è¿ï¼‰ã€‚
   å¦ï¼šä¸­å£å¾„明细里有 4 ä¸ª**数值行**(市州"城际城乡巡游出租/网约车"是硬编码数,不是公式),需要按库里源数据回填,不能只靠公式右移。
2. **生成收尾做公式重算**:在写文件前用 `XSSFFormulaEvaluator.evaluateAll()` æŠŠæ•´æœ¬ç®—一遍并写入缓存值(逐页 try-catch;失败则保留 `fullCalcOnLoad=true` é™çº§ï¼‰ã€‚
   å¥½å¤„:全省行、累计列、排名 SUM、城市客运/中口径明细跨表引用**一次性全部自动出值**,不用再讨论"全省按哪个口径"——母版口径就是 Î£17 å¸‚州。
3. **统一中口径口径**:
   - `exportPassengerMidRank()` å…¨çœè¡ŒåŒæ¯”不再写死库内全省值,改成与 Î£å¸‚å·ž åŒæºï¼›
   - `loadMidClassMap()` è¡¥ dim3 å·¡æ¸¸å‡ºç§Ÿ + dim4 ç½‘约车(现在只装了班线 + å…¬äº¤ä¸¤ç»´ï¼Œæ‰€ä»¥ç‹¬ç«‹ã€Šç”Ÿæˆ_公路旅客中口径明细》1月全省少 535.83+333.39)。
4. **建回归比对脚本**:每次生成后逐页逐格与"母版公式 + åº“源数据重算"比对,作为验收基线(可复用 `_tmp\_qa_out` é‡Œå·²æœ‰çš„对比脚本)。
### éœ€è¦ä¸šåŠ¡ç¡®è®¤ï¼ˆä¸é˜»å¡žä¿®å¤ï¼‰
- åº“里 h2031「全省」行 1-8 æœˆ = 27524.49,比 Î£17 å¸‚州多 **3042.75**。修复一律按母版口径(Σ市州)走;这个差异意味着企业上报的"全省"含市州明细之外的数据(或某市州漏报),建议业务侧核一次。
## ä¸‰ã€æŠ•资:按项目名称更新 / æ–°å¢žé¡¹ç›®å½’到对应模块
### çŽ°çŠ¶ï¼ˆå·²æ ¸å®žï¼Œç¨‹åºç¡®å®žæ˜¯æŒ‰é¡¹ç›®åç§°èµ°çš„ï¼‰
- **导入侧**:`DataImportService.saveInvestProjectRow()` / å…¨çœæ±‡æ€»å¤§è¡¨è·¯å¾„,以「**项目名称 + ç±»åˆ«**」为业务主键 upsert â€”— å‘½ä¸­åˆ™æ›´æ–°å­—段 + å†™å½“期月度,未命中则**新增**并带上该文件的市州与类别(客运站场 / ç‰©æµå›­åŒºï¼‰ã€‚
  åº“内抽查:客运站场 49 æ¡ / 49 ä¸ªåç§°ï¼Œç‰©æµå›­åŒº 135 æ¡ / 135 ä¸ªåç§°ï¼Œ**无重复行**。
- **生成侧**:`ReportExportService.matchInvest()` æ˜¯æŒ‰åç§°ï¼ˆå½’一化 + å¸‚å·ž + ç±»åˆ«è¿‡æ»¤ï¼‰åŽ»åŒ¹é…æ¨¡æ¿è¡Œï¼Œä¸æ˜¯æŒ‰åˆ—ä½/行号取数。
- **同名保护**:已实现"市州一致性校验"(库中归属市州与本次解析不一致 â†’ è·³è¿‡è¯¥è¡Œå¹¶åœ¨é¡µé¢å¼¹çª—提示)。
- **不模糊匹配**:`normInvestName()` åªåšæ‹¬å·/连接符/省市县前缀归一,不做近似合并 â†’ ç¬¦åˆ"名称有差异就是两个项目"。
### ç¼ºå£ï¼ˆå°±æ˜¯ä½ è¯´çš„"新增项目要归到对应模块下面")
1. æ¨¡æ¿é‡Œ**没有的新项目目前不会自动插行**:`investPlan`(全省预安排)等生成器只用 `matchInvest` åŒ¹é…å·²æœ‰æ¨¡æ¿è¡Œï¼Œ**匹配不到的新项目会被丢掉**,不会按市州/板块插到对应位置。
   â†’ éœ€åŠ "插行"逻辑:按 å¸‚å·ž + ç±»åˆ«ï¼ˆ+资金渠道分组)定位块区间,在块末尾插行、复制相邻行格式与公式、写项目名与新数据,并更新合计行范围。
2. **"模块"最细一层现在入不了库**:两张明细表的"一、使用交通部车购税资金 / äºŒã€çœç‡ƒæ²¹ç¨Ž / ä¸‰ã€å†œæ‘综合运输服务站 / å››ã€å€™è½¦äº­"分组行,在导入时被 `^[一二三四五六七八九十]+、.*` è§„则**主动跳过**,所以项目落不到具体渠道块。
   â†’ éœ€ç»™ `investment_project` åŠ  `fund_channel` å­—段,导入时记录"当前分组"写到其后项目上,再重导 7/8 æœˆã€‚
3. åŒåä¸åŒé¡¹ç›®ç›®å‰**不弹窗**(只有市州错位弹窗)。
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java
@@ -60,6 +60,10 @@
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import cn.hutool.core.io.IoUtil;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -3319,10 +3323,36 @@
        Map<String, Double> orders = new LinkedHashMap<>();
        double[] sumHolder = new double[]{0.0};
        String docPeriod;
        try (java.io.InputStream in = file.getInputStream();
             XWPFDocument doc = new XWPFDocument(in)) {
            docPeriod = periodOfDocx(doc);
            parseWycOrderReportTables(doc, orders, sumHolder);
        byte[] bytes;
        try (java.io.InputStream in = file.getInputStream()) {
            bytes = IoUtil.readBytes(in);
        }
        boolean isZip = bytes.length > 3 && (bytes[0] & 0xFF) == 0x50 && (bytes[1] & 0xFF) == 0x4B;
        boolean isWordMl = !isZip && looksLikeWordMl(bytes);
        try {
            if (isZip && !isWordPackage(bytes)) {
                throw new RuntimeException("这不是 Word æ–‡æ¡£ï¼ˆåŽ‹ç¼©åŒ…é‡Œæ²¡æœ‰ word/document.xml),"
                        + "多半是 Excel æˆ–其它格式。请上传网约车订单分析报告的 .docx(或 Word 2003 XML)");
            }
            if (isZip) {
                try (XWPFDocument doc = new XWPFDocument(new java.io.ByteArrayInputStream(bytes))) {
                    docPeriod = periodOfParagraphs(paragraphsOfDocx(doc));
                    parseWycOrderReportTables(doc, orders, sumHolder);
                }
            } else if (isWordMl) {
                // ç½‘约车监管平台下发的报告常被 WPS/Word å­˜æˆã€ŒWord 2003 XML(平铺 XML)」后再改名 .docx:
                // è¿™ç§æ–‡ä»¶ä¸æ˜¯ zip åŒ…,XWPFDocument æ‰“不开(报 "File is not a zip file"),需按 XML ç›´æŽ¥è§£æžã€‚
                String xml = new String(bytes, java.nio.charset.StandardCharsets.UTF_8);
                Document dom = parseXmlSafely(xml);
                docPeriod = periodOfParagraphs(paragraphsOfDocxXml(dom));
                parseWycOrderReportTablesXml(dom, orders, sumHolder);
            } else if (isOle2(bytes)) {
                throw new RuntimeException("这是 Word 97-2003 è€æ ¼å¼ï¼ˆ.doc),请用 WPS/Word æ‰“开后「另存为」docx æ ¼å¼å†å¯¼å…¥");
            } else {
                throw new RuntimeException("无法识别的文件格式:既不是 docx(zip åŒ…),也不是 Word 2003 XML æˆ– .doc");
            }
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException("无法读取 Word æ–‡æ¡£ï¼Œè¯·ä¸Šä¼  .docx æ ¼å¼çš„《网约车行业YYYYå¹´M月运行监测情况分析报告》;"
                    + "若上传的其实是 Excel/PDF,请改用对应模板导入。(原始错误:" + e.getMessage() + ")");
@@ -3388,13 +3418,12 @@
    }
    /** æŠ¥å‘Šæ ‡é¢˜ä¸­çš„æŠ¥è¡¨æœŸï¼šç½‘约车行业2026å¹´7月 -> 2026-07;取不到返回 null */
    private String periodOfDocx(XWPFDocument doc) {
    private String periodOfParagraphs(List<String> paragraphs) {
        java.util.regex.Pattern titlePat = java.util.regex.Pattern.compile("网约车行业\\s*(\\d{4})\\s*å¹´\\s*(\\d{1,2})\\s*月");
        java.util.regex.Pattern anyPat = java.util.regex.Pattern.compile("(\\d{4})\\s*å¹´\\s*(\\d{1,2})\\s*月");
        String fallback = null;
        int scanned = 0;
        for (XWPFParagraph para : doc.getParagraphs()) {
            String text = para.getText();
        for (String text : paragraphs) {
            if (text == null) continue;
            String flat = text.replaceAll("\\s+", "");
            if (flat.isEmpty()) continue;
@@ -3409,6 +3438,92 @@
        return fallback;
    }
    /** docx æ®µè½æ–‡æœ¬ï¼ˆä¿æŒåŽŸå§‹é¡ºåºï¼‰ */
    private List<String> paragraphsOfDocx(XWPFDocument doc) {
        List<String> out = new ArrayList<>();
        for (XWPFParagraph para : doc.getParagraphs()) out.add(para.getText());
        return out;
    }
    /** Word 2003 XML(WordML)段落文本:<w:p> ä¸‹æ‰€æœ‰ <w:t> æ‹¼æŽ¥åŽåŽ‹æŽ‰ç©ºç™½ */
    private List<String> paragraphsOfDocxXml(Document dom) {
        List<String> out = new ArrayList<>();
        for (Element p : elementsByLocalName(dom, "p")) out.add(textOfXmlElement(p));
        return out;
    }
    /** æ˜¯å¦ä¸º Word 2003 XML(WordML å¹³é“º XML)——这种文件常被改名成 .docx */
    private boolean looksLikeWordMl(byte[] bytes) {
        int n = Math.min(bytes.length, 4096);
        String head = new String(bytes, 0, n, java.nio.charset.StandardCharsets.UTF_8);
        return head.contains("wordml") && head.contains("<w:wordDocument");
    }
    /** zip åŒ…内是否含 word/document.xml(用于区分 docx ä¸Ž xlsx/其它 OPC åŒ…) */
    private boolean isWordPackage(byte[] bytes) {
        try (java.util.zip.ZipInputStream zin =
                     new java.util.zip.ZipInputStream(new java.io.ByteArrayInputStream(bytes))) {
            java.util.zip.ZipEntry e;
            while ((e = zin.getNextEntry()) != null) {
                String n = e.getName();
                if ("word/document.xml".equals(n)) return true;
            }
        } catch (Exception ignore) {
            return false;
        }
        return false;
    }
    /** æ˜¯å¦ä¸º OLE2 å¤åˆæ–‡æ¡£ï¼ˆWord 97-2003 .doc) */
    private boolean isOle2(byte[] b) {
        return b.length > 8 && (b[0] & 0xFF) == 0xD0 && (b[1] & 0xFF) == 0xCF
                && (b[2] & 0xFF) == 0x11 && (b[3] & 0xFF) == 0xE0;
    }
    /** å®‰å…¨è§£æž XML:禁用外部实体/DTD(报告来自外部平台,防 XXE) */
    private Document parseXmlSafely(String xml) throws Exception {
        javax.xml.parsers.DocumentBuilderFactory f = javax.xml.parsers.DocumentBuilderFactory.newInstance();
        f.setNamespaceAware(true);
        try { f.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false); } catch (Exception ignore) { }
        try { f.setFeature("http://xml.org/sax/features/external-general-entities", false); } catch (Exception ignore) { }
        try { f.setFeature("http://xml.org/sax/features/external-parameter-entities", false); } catch (Exception ignore) { }
        javax.xml.parsers.DocumentBuilder b = f.newDocumentBuilder();
        b.setEntityResolver(new org.xml.sax.EntityResolver() {
            @Override
            public org.xml.sax.InputSource resolveEntity(String publicId, String systemId) {
                return new org.xml.sax.InputSource(new java.io.StringReader(""));
            }
        });
        return b.parse(new org.xml.sax.InputSource(new java.io.StringReader(xml)));
    }
    /** æŒ‰ localName å–全部后代元素(命名空间无关,兼容 WordML ä¸Ž docx çš„ w: å‰ç¼€ï¼‰ */
    private List<Element> elementsByLocalName(Document dom, String localName) {
        List<Element> out = new ArrayList<>();
        NodeList list = dom.getElementsByTagNameNS("*", localName);
        for (int i = 0; i < list.getLength(); i++) out.add((Element) list.item(i));
        return out;
    }
    /** åªå–直接子元素(避免嵌套表格/嵌套属性元素被算进来) */
    private List<Element> childElementsByLocalName(Element parent, String localName) {
        List<Element> out = new ArrayList<>();
        NodeList kids = parent.getChildNodes();
        for (int i = 0; i < kids.getLength(); i++) {
            Node n = kids.item(i);
            if (n.getNodeType() == Node.ELEMENT_NODE && localName.equals(n.getLocalName())) out.add((Element) n);
        }
        return out;
    }
    /** å…ƒç´ å†…所有 <w:t> æ–‡æœ¬æ‹¼æŽ¥å¹¶åŽ‹æŽ‰ç©ºç™½ */
    private String textOfXmlElement(Element el) {
        StringBuilder sb = new StringBuilder();
        NodeList ts = el.getElementsByTagNameNS("*", "t");
        for (int i = 0; i < ts.getLength(); i++) sb.append(ts.item(i).getTextContent());
        return flat(sb.toString());
    }
    private String ymd(String y, String mo) {
        try {
            int month = Integer.parseInt(mo);
@@ -3421,15 +3536,47 @@
    /** è§£æžæŠ¥å‘Šä¸­çš„「地市名称 | â€¦ | è®¢å•数总数」表,orders=市州订单,sumHolder[0]=合计行订单数 */
    private void parseWycOrderReportTables(XWPFDocument doc, Map<String, Double> orders, double[] sumHolder) {
        List<List<List<String>>> tables = new ArrayList<>();
        for (XWPFTable table : doc.getTables()) {
            List<XWPFTableRow> rows = table.getRows();
            List<List<String>> rows = new ArrayList<>();
            for (XWPFTableRow row : table.getRows()) {
                List<String> cells = new ArrayList<>();
                for (org.apache.poi.xwpf.usermodel.XWPFTableCell c : row.getTableCells()) cells.add(flat(c.getText()));
                rows.add(cells);
            }
            tables.add(rows);
        }
        matchWycOrderTable(tables, orders, sumHolder);
    }
    /** Word 2003 XML(WordML)分支:<w:tbl>/<w:tr>/<w:tc> æŠ½æˆæ–‡æœ¬è¡¨åŽèµ°åŒä¸€å¥—匹配逻辑 */
    private void parseWycOrderReportTablesXml(Document dom, Map<String, Double> orders, double[] sumHolder) {
        List<List<List<String>>> tables = new ArrayList<>();
        for (Element tbl : elementsByLocalName(dom, "tbl")) {
            List<List<String>> rows = new ArrayList<>();
            for (Element tr : childElementsByLocalName(tbl, "tr")) {
                List<String> cells = new ArrayList<>();
                for (Element tc : childElementsByLocalName(tr, "tc")) cells.add(textOfXmlElement(tc));
                rows.add(cells);
            }
            tables.add(rows);
        }
        matchWycOrderTable(tables, orders, sumHolder);
    }
    /**
     * åœ¨ã€Œæ–‡æœ¬åŒ–表格」里找附件1(表头含「订单数总数」/「订单数」)并取 17 å¸‚州订单数与合计。
     * å…¥å‚ tables = è¡¨ -> è¡Œ -> å•元格文本(已压空白),与来源格式无关。
     */
    private void matchWycOrderTable(List<List<List<String>>> tables, Map<String, Double> orders, double[] sumHolder) {
        for (List<List<String>> rows : tables) {
            if (rows.size() < 3) continue;
            int headerRow = -1;
            int orderCol = -1;
            for (int r = 0; r < Math.min(rows.size(), 3); r++) {
                List<org.apache.poi.xwpf.usermodel.XWPFTableCell> cells = rows.get(r).getTableCells();
                List<String> cells = rows.get(r);
                for (int c = 0; c < cells.size(); c++) {
                    String norm = flat(cells.get(c).getText());
                    String norm = cells.get(c);
                    if (norm.contains("订单数总数") || "订单数".equals(norm)) {
                        headerRow = r;
                        orderCol = c;
@@ -3444,11 +3591,11 @@
            double citySum = 0.0;
            boolean hasTotal = false;
            for (int r = headerRow + 1; r < rows.size(); r++) {
                List<org.apache.poi.xwpf.usermodel.XWPFTableCell> cells = rows.get(r).getTableCells();
                if (orderCol >= cells.size()) continue;
                String name = flat(cells.get(0).getText());
                List<String> cells = rows.get(r);
                if (cells.isEmpty() || orderCol >= cells.size()) continue;
                String name = cells.get(0);
                if (name.isEmpty()) continue;
                Double val = parsePlainNumber(cells.get(orderCol).getText());
                Double val = parsePlainNumber(cells.get(orderCol));
                if (val == null) continue;
                String city = RegionUtil.normalizeCityName(name);
                if (city != null && RegionUtil.cityList().contains(city)) {
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java
@@ -7,6 +7,8 @@
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@@ -33,6 +35,8 @@
 */
@Service
public class SummaryWorkbookFiller {
    private static final Logger log = LoggerFactory.getLogger(SummaryWorkbookFiller.class);
    @Resource
    private MidCalc midCalc;
@@ -429,9 +433,11 @@
        if (wb == null || month <= 1) return 0;
        int targetCol = FIRST_MONTH_COL + 2 * (month - 1);
        int written = 0;
        int carried = 0;
        for (int i = 0; i < wb.getNumberOfSheets(); i++) {
            Sheet sh = wb.getSheetAt(i);
            if (sh == null || !isFilledLedgerSheet(sh.getSheetName())) continue;
            if (sh == null || !isMonthLedgerSheet(sh.getSheetName())) continue;
            boolean derivedSheet = isDerivedFormulaSheet(sh.getSheetName());
            for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
                Row row = sh.getRow(r);
                if (row == null) continue;
@@ -446,7 +452,21 @@
                        break;
                    }
                }
                if (srcCell == null) continue;
                if (srcCell == null) {
                    if (!derivedSheet) continue;
                    // æ´¾ç”Ÿé¡µï¼ˆåŸŽå¸‚客运/中口径明细/公路总客运)里「各月都是同一个常量」的结构性常量行
                    // ï¼ˆå¦‚中口径明细 æ­¦æ±‰ åŸŽé™…城乡巡游出租=0),按月沿用该常量,避免当月值列空着导致累计缺项
                    double struct = structuralConstant(row, month);
                    if (Double.isNaN(struct)) continue;
                    Cell target = row.getCell(targetCol);
                    if (target == null) target = row.createCell(targetCol);
                    Cell prev = row.getCell(FIRST_MONTH_COL + 2 * (month - 2));
                    if (prev != null && prev.getCellStyle() != null) target.setCellStyle(prev.getCellStyle());
                    target.setCellValue(struct);
                    carried++;
                    written++;
                    continue;
                }
                String f = srcCell.getCellFormula();
                if (f == null || f.isEmpty()) continue;
                Cell target = row.getCell(targetCol);
@@ -456,7 +476,31 @@
                written++;
            }
        }
        if (carried > 0 && log != null) {
            log.info("汇总工作簿:派生页结构性常量行按月沿用 {} æ ¼ï¼ˆç¬¬ {} æœˆï¼‰", carried, month);
        }
        return written;
    }
    /** è¯¥è¡Œ 1..month-1 æœˆçš„当月值列是否全为同一个数值常量(结构性常量行,如各月恒为 0) */
    private double structuralConstant(Row row, int month) {
        if (row == null || month <= 2) return Double.NaN;
        Double v = null;
        for (int s = 1; s < month; s++) {
            Cell c = row.getCell(FIRST_MONTH_COL + 2 * (s - 1));
            if (c == null || c.getCellType() != org.apache.poi.ss.usermodel.CellType.NUMERIC) return Double.NaN;
            double d = c.getNumericCellValue();
            if (v == null) v = d;
            else if (Math.abs(d - v) > 1e-9) return Double.NaN;
        }
        return v == null ? Double.NaN : v;
    }
    /** æ´¾ç”Ÿé¡µï¼ˆå½“月值全部来自其他页/常量,纯公式驱动):城市客运 / ä¸­å£å¾„明细 / å…¬è·¯æ€»å®¢è¿ / ä¸­å£å¾„客运量 */
    private boolean isDerivedFormulaSheet(String name) {
        if (name == null) return false;
        String n = name.trim();
        return "城市客运".equals(n) || "中口径明细".equals(n) || "公路总客运".equals(n) || "中口径客运量".equals(n);
    }
    /** è¯¥æ ¼æ˜¯å¦å·²æœ‰å†…容(数值/文本/公式);仅带样式的空壳(如母版里继承表头日期格式的格子)视为空 */
@@ -467,14 +511,6 @@
            case STRING: return c.getStringCellValue() != null && !c.getStringCellValue().trim().isEmpty();
            default: return true;
        }
    }
    /** å‚与逐月数据回填的台账页(货运/班线包车/公交/出租/网约车/轨道轮渡) */
    private boolean isFilledLedgerSheet(String name) {
        if (name == null) return false;
        String n = name.trim();
        return "货运".equals(n) || "班线包车".equals(n) || "公交".equals(n)
                || "出租".equals(n) || "网约车".equals(n) || "轨道、轮渡".equals(n);
    }
    /** 0 åŸºåˆ—号 + 1 åŸºè¡Œå· -> "Q5" */
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -13,6 +13,7 @@
import com.trafficaudit.dataimport.entity.PassengerEnterpriseMonthly;
import com.trafficaudit.dataimport.entity.PassengerIndividualMonthly;
import com.trafficaudit.dataimport.entity.ScaleSplitTransport;
import com.trafficaudit.dataimport.entity.WycOrderMonthly;
import com.trafficaudit.auditengine.entity.AuditResult;
import com.trafficaudit.auditengine.entity.AuditRun;
import com.trafficaudit.auditengine.mapper.AuditResultMapper;
@@ -29,9 +30,11 @@
import com.trafficaudit.dataimport.mapper.PassengerEnterpriseMonthlyMapper;
import com.trafficaudit.dataimport.mapper.PassengerIndividualMonthlyMapper;
import com.trafficaudit.dataimport.mapper.ScaleSplitTransportMapper;
import com.trafficaudit.dataimport.mapper.WycOrderMonthlyMapper;
import com.trafficaudit.reportexport.calc.MidCalc;
import com.trafficaudit.reportexport.calc.PaxCalc;
import com.trafficaudit.reportexport.calc.SummaryWorkbookFiller;
import com.trafficaudit.reportexport.calc.WycSplitCalc;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
@@ -98,6 +101,10 @@
    private CityTaxiMonthlyMapper cityTaxiMapper;
    @Resource
    private SummaryWorkbookFiller summaryWorkbookFiller;
    @Resource
    private WycSplitCalc wycSplitCalc;
    @Resource
    private WycOrderMonthlyMapper wycOrderMapper;
    @Resource
    private AuditResultMapper auditResultMapper;
    @Resource
@@ -1179,17 +1186,16 @@
        return lv == 0.0 ? null : (v - lv) / lv;
    }
    /** ä¸­å£å¾„月同比:k=-1 è¡¨ç¤ºæ€»è¡Œï¼ˆç­çº¿+公交之和,出租/网约车无源数据按 0);无去年数据留空 */
    /** ä¸­å£å¾„月同比:k=-1 è¡¨ç¤ºæ€»è¡Œï¼ˆç­çº¿+公交+出租城乡+网约车城乡 å››é¡¹ä¹‹å’Œï¼‰ï¼›æ— åŽ»å¹´æ•°æ®ç•™ç©º */
    private void fillMidYoyCell(Cell c, Map<Integer, Map<String, double[][]>> mid, int year, int month,
                                String area, int k, boolean volume) {
        double v = 0.0, lv = 0.0;
        if (k == -1) {
            for (int kk = 1; kk <= 4; kk++) {
                if (kk >= 3) continue;
                v += midClassVal(mid, year, month, area, kk, volume);
                lv += midClassVal(mid, year - 1, month, area, kk, volume);
            }
        } else if (k <= 2) {
        } else {
            v = midClassVal(mid, year, month, area, k, volume);
            lv = midClassVal(mid, year - 1, month, area, k, volume);
        }
@@ -1197,7 +1203,7 @@
        if (v != 0.0 && lv != 0.0) c.setCellValue(round((v - lv) / lv, 4));
    }
    /** ä¸­å£å¾„累计同比列(1..months æœˆç´¯è®¡ï¼›k=-1 æ€»è¡Œ = ç­çº¿+公交) */
    /** ä¸­å£å¾„累计同比列(1..months æœˆç´¯è®¡ï¼›å››ç»´é½å…¨ï¼‰ */
    private void fillMidCumYoyColumn(Sheet sheet, Map<Integer, Map<String, double[][]>> mid, int year,
                                     int months, int colIdx) {
        List<String> areas = midTemplateAreas();
@@ -1217,7 +1223,7 @@
            String area = areas.get(blockOffset / 5);
            double v = 0.0, lv = 0.0;
            for (int m = 1; m <= months; m++) {
                if (k <= 2) {
                {
                    v += midClassVal(mid, year, m, area, k, volume);
                    lv += midClassVal(mid, year - 1, m, area, k, volume);
                }
@@ -2181,20 +2187,15 @@
            boolean extended = fillMonths > 6;
            if (extended) extendMonthlyColumns(sheet, 2, currentYear, 4, sheet.getLastRowNum());
            List<String> areas = midTemplateAreas();
            // 2026-09-18:改为「按单元格类型无关」遍历——扩列(7~12 æœˆï¼‰å‡ºæ¥çš„值列在模板里是空格,
            // æ—§å®žçŽ°åªå¤„ç† CellType.FORMULA çš„æ ¼ï¼Œå¯¼è‡´ 7 æœˆåŠä»¥åŽæœˆä»½æ°¸è¿œå¡«ä¸ä¸Šï¼ˆ8 æœˆæ•´åˆ— 0、累计只到 6 æœˆï¼‰ã€‚
            for (Row row : sheet) {
                if (row == null) continue;
                for (Cell c : row) {
                    if (c.getCellType() != CellType.FORMULA) continue;
                    String f = c.getCellFormula();
                    if (f == null) continue;
                    boolean crossBook = f.contains("[");
                    if (!crossBook && !isYoyMonthCol(c.getColumnIndex() + 1)) continue; // å†…部公式(合计/累计)保留重算;同比列 #REF! è¦†å†™ä¸ºæ•°å€¼
                    if (crossBook && !is2026MonthCol(c.getColumnIndex() + 1)) {
                        keepCached(c); // 2025 å¹´åˆ—保留模板缓存值
                        continue;
                    }
                for (int c0 = 0; c0 < Math.max(row.getLastCellNum(), 0); c0++) {
                    Cell c = row.getCell(c0);
                    if (c == null) continue;
                    int r = c.getRowIndex() + 1;
                    int col = c.getColumnIndex() + 1;
                    int col = c0 + 1;
                    int blockOffset = -1;
                    boolean volume = false;
                    if (r >= 5 && r <= 94) {
@@ -2205,33 +2206,65 @@
                        volume = false;
                    }
                    if (blockOffset < 0) {
                        keepCached(c); // r1/r2 å¤‡æ³¨ç­‰
                        if (c.getCellType() == CellType.FORMULA) keepCached(c); // r1/r2 å¤‡æ³¨ã€é¡µå¤–跨簿引用等
                        continue;
                    }
                    if (blockOffset % 5 == 0) {
                        if (isYoyMonthCol(col)) {
                            fillMidYoyCell(c, mid, currentYear, monthOfCol(col), areas.get(blockOffset / 5), -1, volume);
                        } else {
                            keepCached(c); // æ€»è¡Œå†…部公式(防御)
                    boolean isVal = is2026MonthCol(col);
                    boolean isYoy = isYoyMonthCol(col);
                    if (!isVal && !isYoy) {
                        // 2025 å¹´åˆ—等:跨簿公式取缓存值断链;内部公式(合计/累计)保留,稍后 recalc é‡ç®—
                        if (c.getCellType() == CellType.FORMULA) {
                            String f = c.getCellFormula();
                            if (f != null && f.contains("[")) keepCached(c);
                        }
                        continue;
                    }
                    int k = blockOffset % 5; // 1=班线 2=公交 3=出租 4=网约车
                    String area = areas.get(blockOffset / 5);
                    if (is2026MonthCol(col)) {
                    if (blockOffset % 5 == 0) {
                        if (isYoy) {
                            fillMidYoyCell(c, mid, currentYear, monthOfCol(col), area, -1, volume);
                        }
                        // æ€»è¡Œå€¼åˆ—:一律保留模板公式(1~6 æœˆåŽŸå…¬å¼ + æ‰©åˆ—克隆出来的公式),
                        // ç”±ç”Ÿæˆæ”¶å°¾çš„ recalcWorkbookFormulas æ±‚值写回缓存。早期版本这里 keepCached
                        // ä¼šæŠŠåˆšå…‹éš†å‡ºæ¥çš„公式清成空格,导致 7 æœˆåŠä»¥åŽæ€»è¡Œæ°¸è¿œç©ºç™½ã€‚
                        continue;
                    }
                    int k = blockOffset % 5; // 1=班线 2=公交 3=出租城乡 4=网约车城乡
                    if (isVal) {
                        fillMidCell(c, mid, currentYear, monthOfCol(col), area, k, volume);
                    } else if (isYoyMonthCol(col)) {
                        fillMidYoyCell(c, mid, currentYear, monthOfCol(col), area, k, volume);
                    } else {
                        keepCached(c); // 2025 å¹´åˆ—保留模板缓存值
                        fillMidYoyCell(c, mid, currentYear, monthOfCol(col), area, k, volume);
                    }
                }
            }
            // ç´¯è®¡åŒæ¯”列(模板 #REF! â†’ åº“内去年 1..N æœˆç´¯è®¡åŒæ¯”数值)
            fillMidCumYoyColumn(sheet, mid, currentYear, fillMonths, extended ? 27 : 15);
            recalc(wb);
            // evaluateAll ä¼šåœ¨è·¨ç°¿å…¬å¼ï¼ˆSheet2 çš„ [1]xx!A1)处整本中断,累计列 AA å°±æ°¸è¿œæ‹¿ä¸åˆ°æ–°ç¼“存值,
            // æ”¹ç”¨é€æ ¼å®¹é”™çš„ recalcWorkbookFormulas:坏格跳过、好格照算。
            int[] recalc = recalcWorkbookFormulas(wb);
            log.info("中口径明细公式重算完成:成功 {} æ ¼ï¼Œå¤±è´¥ {} æ ¼", recalc[0], recalc[1]);
            clearFormulaErrorsAll(wb); // åŽ»å¹´åŒæœŸæ˜Žç»†é¡µæ®‹ç•™ #REF! åŒæ¯”公式 â†’ æ¸…空
            blankFutureMonthColumns(sheet, fillMonths); // æŠ¥è¡¨æœŸä¹‹åŽçš„æœˆä»½åˆ—不保留克隆公式/0 å€¼
            return toBytes(wb);
        }
    }
    /**
     * æ¸…空「报表期之后」的月份列(扩列克隆出来的 7~12 æœˆé‡Œã€å°šæœªåˆ°æœŸçš„æœˆä»½ï¼‰ã€‚
     * ä¸æ¸…的话,克隆出来的总行公式会算出 0,报表上会出现"9~12 æœˆå…¨æ˜¯ 0"的假数据;
     * ç´¯è®¡åˆ—(AA)的公式/缓存值已在重算阶段算好,清空格子不影响它(空 = 0)。
     */
    private void blankFutureMonthColumns(Sheet sheet, int filledMonths) {
        for (int m = filledMonths + 1; m <= 12; m++) {
            int col = 2 * m;            // 0 åŸºï¼š1 æœˆ=2(C)、7 æœˆ=14(O)、12 æœˆ=24(Y)
            for (int r = 4; r <= sheet.getLastRowNum(); r++) { // 0 åŸºç¬¬ 4 è¡Œ = ç¬¬ 5 è¡Œï¼ˆé¦–个数据行),表头不动
                Row row = sheet.getRow(r);
                if (row == null) continue;
                Cell val = row.getCell(col);
                if (val != null) val.setBlank();
                Cell yoy = row.getCell(col + 1);
                if (yoy != null) yoy.setBlank();
            }
        }
    }
@@ -2256,16 +2289,19 @@
        return (col - 1) / 2;
    }
    /** å¡«ä¸­å£å¾„数据单元格:出租/网约车(3/4)显式 0,班线/公交填库内值(无值清空) */
    /**
     * å¡«ä¸­å£å¾„数据单元格:班线/公交/出租城乡/网约车城乡 å››ç»´ç»Ÿä¸€å–库内值。
     * åº“内该月该维无源(取值为 0)时**保留母版/模板原值**——绝不写空、也不写 0。
     * ä¾æ®ï¼šæ¨¡æ¿çš„城市级城乡行是「台账页 æ€»é‡ âˆ’ åŸŽå¸‚内」的引用、并已业务定稿;
     * ç”¨åº“内缺失值覆盖会把定稿数清掉(如 2026-01~06 ç½‘约车订单缺失时,
     * å…¨çœç½‘约车城乡 333.39 ä¸Žå„市州值会被清空)。
     */
    private void fillMidCell(Cell c, Map<Integer, Map<String, double[][]>> mid, int year, int month, String area, int k, boolean volume) {
        if (k == 3 || k == 4) {
            writeExplicitZero(c); // å‡ºç§Ÿ/网约车暂无数据源,显式 0
            return;
        }
        double v = midClassVal(mid, year, month, area, k, volume);
        if (v == 0.0) return; // åº“内无源:保留母版/定稿原值,缺数据不覆盖
        // POI setCellValue(double) å¯¹å…¬å¼å•元格只更新缓存不移除公式,必须先 setBlank å†å†™å€¼
        c.setBlank();
        if (v != 0.0) c.setCellValue(round(v, 4));
        c.setCellValue(round(v, 4));
    }
    /** æ˜¾å¼å†™å…¥ 0(POI setCellValue(0.0) ä¼šè½¬ blank,需操作底层 XML) */
@@ -2369,7 +2405,52 @@
            addMidClass(mid, key, city, 2, nz(b.getPassengerChengxiang()), nz(b.getTurnoverChengxiang()));
            addMidClass(mid, key, "湖北省", 2, nz(b.getPassengerChengxiang()), nz(b.getTurnoverChengxiang()));
        }
        // ç»´åº¦3:城际城乡巡游出租 = å°è´¦é¡µã€Œå‡ºç§Ÿ!总量 âˆ’ å‡ºç§Ÿ!城市内」(源 city_taxi_monthly)
        for (CityTaxiMonthly t : cityTaxiMapper.selectList(null)) {
            if (t.getReportPeriod() == null || t.getCity() == null) continue;
            int key = midPeriodKey(t.getReportPeriod());
            if (key <= 0) continue;
            String city = RegionUtil.normalizeCityName(t.getCity());
            if (city == null) continue;
            double pass = nz(t.getPassengerVolume()) - nz(t.getPassengerCity());
            double turn = nz(t.getTurnover()) - nz(t.getTurnoverCity());
            addMidClass(mid, key, city, 3, pass, turn);
            addMidClass(mid, key, "湖北省", 3, pass, turn);
        }
        // ç»´åº¦4:城际城乡网约车 = ç½‘约车拆分结果(与台账「网约车」页同源同法)
        for (WycOrderMonthly o : wycOrderMapper.selectList(null)) {
            String per = o.getReportPeriod();
            if (per == null) continue;
            int key = midPeriodKey(per);
            if (key <= 0) continue;
            WycSplitCalc.WycResult wr;
            try {
                wr = wycSplitCalc.calc(per);
            } catch (Exception e) {
                continue; // è¯¥æœŸç¼ºè®¢å•/pin æˆ–出租源数据,跳过(不影响其它期)
            }
            for (Map.Entry<String, WycSplitCalc.WycMetrics> en : wr.getByCity().entrySet()) {
                WycSplitCalc.WycMetrics wm = en.getValue();
                addMidClass(mid, key, en.getKey(), 4, wm.getSuburbanPax(), wm.getSuburbanTurnover());
                addMidClass(mid, key, "湖北省", 4, wm.getSuburbanPax(), wm.getSuburbanTurnover());
            }
        }
        return mid;
    }
    /** "yyyy-MM" -> yyyy*100+MM;非法返回 0(与 loadMidClassMap çš„ key å£å¾„一致) */
    private int midPeriodKey(String period) {
        if (period == null) return 0;
        String[] parts = period.trim().split("-");
        if (parts.length != 2) return 0;
        try {
            int y = Integer.parseInt(parts[0]);
            int m = Integer.parseInt(parts[1]);
            if (y <= 0 || m < 1 || m > 12) return 0;
            return y * 100 + m;
        } catch (NumberFormatException e) {
            return 0;
        }
    }
    /** å†™å…¥åˆ†ç±»å€¼å¹¶ç´¯åŠ æ€»é‡ï¼ˆç»´åº¦0) */
@@ -3362,6 +3443,8 @@
            applyTwoDecimalFormat(wb); // æ•°å€¼æ˜¾ç¤ºä¸¤ä½å°æ•°ï¼ˆä¿ç•™å…¨ç²¾åº¦ï¼‰ï¼›% / æ—¥æœŸç­‰æ—¢æœ‰æ ·å¼ä¸æ”¹å˜
            int inlineFixed = normalizeStaleInlineStrings(wb); // æ¸…掉「非 inlineStr å´æ®‹ç•™ <is>」的非法格,否则 Excel æ‹’绝打开
            if (inlineFixed > 0) log.warn("汇总工作簿清掉残留 inlineStr éžæ³•æ ¼ {} ä¸ªï¼ˆæ¯ç‰ˆç”± openpyxl å†™å‡ºæ—¶çš„典型问题)", inlineFixed);
            int[] recalc = recalcWorkbookFormulas(wb); // å…¬å¼é‡ç®—并写回缓存值:全省行(Σ市州)/累计列/排名 SUM æ‰èƒ½è¢«é¢„览与二次处理读到
            log.info("汇总工作簿公式重算完成:成功 {} æ ¼ï¼Œå¤±è´¥ {} æ ¼ï¼ˆå¤±è´¥æ ¼ä¿ç•™æ¯ç‰ˆæ—§ç¼“存值,打开时按 fullCalcOnLoad é‡ç®—)", recalc[0], recalc[1]);
            if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr();
            wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // è¿˜åŽŸ/改写公式后打开即重算
            return toBytes(wb);
@@ -3371,6 +3454,53 @@
    }
    /**
     * æ±‡æ€»å·¥ä½œç°¿å…¬å¼é‡ç®—:POI å†™æ–‡ä»¶ä¸å¸¦ç¼“存值,导致「全省行(Σ17市州)」「累计列」「排名 SUM」
     * åœ¨é¢„览/二次处理里只看到母版上一期的旧值(2026-09-18 ç”¨æˆ·æŠ¥â€œ8月没数据 / å…¨çœè¡Œåœåœ¨ä¸Šä¸€æœŸâ€çš„æ ¹å› ä¹‹ä¸€ï¼‰ã€‚
     * é€æ ¼ evaluate å¹¶æŠŠç»“果写回缓存值;个别不支持函数/外部引用格失败则跳过并计数,不阻塞导出。
     */
    private int[] recalcWorkbookFormulas(XSSFWorkbook wb) {
        if (wb == null) return new int[]{0, 0};
        org.apache.poi.ss.usermodel.FormulaEvaluator ev;
        try {
            ev = wb.getCreationHelper().createFormulaEvaluator();
        } catch (Exception e) {
            log.warn("汇总工作簿公式重算:创建求值器失败,跳过({})", e.getMessage());
            return new int[]{0, 0};
        }
        try {
            // è·¨ç°¿å…¬å¼ï¼ˆ[1]出租!C13 ä¹‹ç±»ï¼‰åœ¨å¤–部工作簿缺失时默认抛异常,会让引用它的
            // æ€»è¡Œ/累计/派生公式连锁失败(表现为「公式还在但没有缓存值」)。
            // ç½® true åŽæ”¹ç”¨å…¬å¼æ ¼è‡ªèº«çš„缓存值(即定稿值)参与计算。
            ev.setIgnoreMissingWorkbooks(true);
        } catch (Exception ignore) {
            // ä¸ªåˆ«å®žçŽ°ä¸æ”¯æŒè¯¥å¼€å…³ï¼Œå¿½ç•¥å³å¯
        }
        int ok = 0, fail = 0;
        java.util.List<String> samples = new java.util.ArrayList<>();
        for (int i = 0; i < wb.getNumberOfSheets(); i++) {
            Sheet sh = wb.getSheetAt(i);
            if (sh == null) continue;
            for (Row row : sh) {
                if (row == null) continue;
                for (Cell c : row) {
                    if (c == null || c.getCellType() != CellType.FORMULA) continue;
                    try {
                        ev.evaluateFormulaCell(c); // æ³¨æ„ï¼ševaluate() æ— å‰¯ä½œç”¨ï¼Œå¿…须用 evaluateFormulaCell æ‰ä¼šæŠŠç»“果写回缓存值
                        ok++;
                    } catch (Exception ex) {
                        fail++;
                        if (samples.size() < 10) {
                            samples.add(sh.getSheetName() + "!" + c.getAddress() + " :: " + ex.getClass().getSimpleName());
                        }
                    }
                }
            }
        }
        if (!samples.isEmpty()) log.warn("汇总工作簿公式重算失败样例:{}", samples);
        return new int[]{ok, fail};
    }
    /**
     * æ¸…掉「非 inlineStr å´æ®‹ç•™ &lt;is&gt;」的非法单元格。
     *
     * æ¯ç‰ˆè‹¥ç”± openpyxl å†™å‡ºï¼Œæ–‡æœ¬å•元格用 &lt;is&gt;(inlineStr)存储;POI çš„ setCellValue åªæ”¹å†™ t ä¸Ž &lt;v&gt;,