feat: 网约车订单运行监测报告(docx)导入 + 汇总大表8月数据驱动回填与版式对齐

【用户第1项:网约车订单报告导入】
- 新增 POST /api/import/wycOrderReport(DataImportService.importWycOrderReport)
解析《网约车行业YYYY年M月运行监测情况分析报告》附件表「地市名称/订单数总数」,
取17市州订单数写入 wyc_order_monthly(F订单合计 + G-W市州列);报表期优先取报告标题年月
- 全省 pin(B-E) 该期缺失时从 wyc_total_monthly 同报表期补齐
- 误传 Excel/PDF 时原先抛 xmlbeans 英文异常,改为用户可读提示(原始错误附后)
- 前端导入界面新增「网约车订单报告」类型(模板_网约车订单.docx,限 .docx/.doc),接入按类型清空
- WycSplitCalc 网约车取数改为优先读库,库里无数据回退读 xlsx 母版

【用户第2/3/4/5项:汇总大表8月回填与版式】
- 8月当月值列(Q列)自动回填:写入当月值格数=1659,历史月不受影响
- 同比统一以汇总表2025年8月(AJ列)为分母,公式 =Q../AJ..-1(补写600格)
- 公交/出租数值不再被日期格式吞掉(按同行1月列样式套用,保留0.00)
- 班线包车、轨道轮渡「全省」行补齐合计公式(补写43格),打开即算(fullCalcOnLoad)
- 版式按用户参照件《生成_道路运输量汇总表》逐列对齐列宽/隐藏,真实差异0列,含8月扩列Q/R宽度

【修复】
- monthColumnCapacity 对货运页表头(累计周转量)恒返回默认列数导致8月列永不回填
- shiftFormulaColumns 连续组引用被拼接错(Q5AJ5O5)
- ensureMonthYoyFormulas 公式写入失败被静默吞掉,改为抛出定位异常
- setNumeric 对公式格不再覆盖,对空/日期格式格套用1月列样式

【测试】
- 2026-08 导出 4.6s/948,735字节,缺数提示为空;货运Σ17市州=全省(16888.7615)
- 网约车/公交/出租/班线包车/轨道轮渡当月与全省行全部正确
- 7月历史月与备份母版逐格比对无实质差异(差异仅「中口径排名」页09-08已按需求删除的旧左块)
- 详见 docs/功能测试报告/功能测试报告_网约车订单报告导入_2026-09-11.md、
docs/功能测试报告/功能测试报告_汇总大表8月数据回填与版式对齐_2026-09-11.md
6个文件已修改
4个文件已添加
924 ■■■■■ 已修改文件
docs/功能测试报告/功能测试报告_汇总大表8月数据回填与版式对齐_2026-09-11.md 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_网约车订单报告导入_2026-09-11.md 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/城市客运/模板_网约车订单.docx 补丁 | 查看 | 原始文档 | blame | 历史
docs/工作日结/工作日结_2026-09-11.md 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/controller/DataImportController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java 212 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java 380 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/WycSplitCalc.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/DataImport.vue 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_»ã×Ü´ó±í8ÔÂÊý¾Ý»ØÌîÓë°æÊ½¶ÔÆë_2026-09-11.md
New file
@@ -0,0 +1,70 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_汇总大表8月数据回填与版式对齐_2026-09-11
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
针对用户反馈的 4 ä¸ªé—®é¢˜ï¼Œå¯¹ã€Šé“路运输量汇总表》导出链路做数据驱动回填与版式对齐:
1. **8 æœˆæ•°æ®å·²å…¥åº“但汇总表没有 8 æœˆæ•°æ®** â†’ æ±‡æ€»å·¥ä½œç°¿å¡«å……器识别「当月值列」(8 æœˆä¸º Q åˆ—)并回填;
2. **同比一律以汇总表 2025 å¹´ 8 æœˆæ•°æ®ä¸ºå‡†** â†’ å½“月同比列(R åˆ—)公式统一为 `=Q../AJ..-1`,AJ åˆ—即 2025 å¹´ 8 æœˆï¼›
3. **公交、出租数据是日期形式** â†’ å›žå¡«æ•°å€¼æ—¶æŒ‰åŒè¡Œ 1 æœˆåˆ—样式套用,避免数值被套上日期格式(显示成 1902/1910 å¹´ï¼‰ï¼›
4. **班线包车、轨道轮渡只有分市州没有全省数据** â†’ ä¸ºå…¨çœè¡Œè¡¥é½åˆè®¡å…¬å¼ï¼ˆæ¯ç‰ˆåŽŸæœ¬åªå¯¹ 1 æœˆå†™æ­»å…¬å¼ï¼‰ï¼›
5. **版式完全按《生成_道路运输量汇总表.xlsx》** â†’ é€åˆ—套用参照件列宽/隐藏标记,**后续扩列(8 æœˆ Q/R åˆ—)也按参照件宽度**。
改动范围:
- `SummaryWorkbookFiller`:`fillFreightSheet` / `fillWycSheet` é‡å†™ï¼ˆåªå›žå¡«ç›®æ ‡æœŸå½“月,派生行保持公式);新增 `ensureMonthFormulas`、`ensureMonthYoyFormulas`、`monthColumnCapacity`;
- `ReportExportService`:导出时调用 `ensureMonthFormulas` + æ–°å¢ž `applySummaryReferenceLayout`(按参照件对齐列宽);
- `WycSplitCalc`:汇总表网约车取数优先读库。
关键口径:当月值列(0 åŸºï¼‰= `2 + 2*(m-1)`,同比列 = å€¼åˆ— + 1;同比分母固定为 AJ åˆ—(2025 å¹´ 8 æœˆï¼‰ã€‚
## äºŒã€æµ‹è¯•环境
- åŽç«¯ `http://localhost:8090`,导出接口 `GET /api/report/export/summaryWorkbook?period=YYYY-MM`。
- æ¯ç‰ˆï¼š`docs/生成汇总大表/2026å¹´8月道路运输量汇总表.xlsx`;版式参照:`docs/生成汇总大表/生成_道路运输量汇总表.xlsx`。
- æµ‹è¯•时间:2026-09-11。
## ä¸‰ã€ç”¨ä¾‹è¡¨
### 3.1 æ­£å‘:2026-08 ç”Ÿæˆä¸Žå½“月取值
| ç¼–号 | æ£€æŸ¥é¡¹ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-1 | å¯¼å‡º 2026-08 | HTTP 200,正常产出文件 | 4.6 ç§’返回,948,735 å­—节 | é€šè¿‡ |
| TC-2 | åŽç«¯å›žå¡«æ—¥å¿— | æœ‰å†™å…¥æ ¼æ•°ç»Ÿè®¡ã€æ— ç¼ºæ•°æç¤º | å†™å…¥å½“月值格数=1659,补写当月同比公式格数=600,补写当月合计公式格数=43,对齐列宽列数=422,缺数提示=[] | é€šè¿‡ |
| TC-3 | è´§è¿é¡µå…¨çœå—(行5-10)Q åˆ— | è§„上/规下货运量与周转量有值,合计行为公式 | è§„上货运量 3666.1401、规下 13222.6214、规上周转 570004.4355、规下周转 1700006.166;Q5=`=Q7+Q9`、Q6=`=Q8+Q10` | é€šè¿‡ |
| TC-4 | è´§è¿é¡µ Î£17市州 = å…¨çœ | ç›¸ç­‰ | Î£17市州货运量 = 16888.7615;全省 Q5 = 3666.1401 + 13222.6214 = 16888.7615 | é€šè¿‡ |
| TC-5 | è´§è¿é¡µå¸‚州派生行 | ä¸å†™æ­»å€¼ï¼Œä¿æŒå…¬å¼ | æ­¦æ±‰ Q14(规上+规下周转量)=`=Q15+Q16`;规上/规下分项为数值 | é€šè¿‡ |
| TC-6 | ç½‘约车页 | 17 å¸‚州当月有值,全省行为公式 | æ­¦æ±‰ Q9=1879.9049…、Q10=13407.6982…;全省 Q5=`=Q9+Q13+…+Q73`;Σ17市州客运量=4997.74、周转量=29166.7 | é€šè¿‡ |
| TC-7 | å…¬äº¤é¡µ | å¸‚州当月有值且**不是日期显示** | Q9=3856.83(武汉客运量),单元格格式 `0.00`;全省 Q5 ä¸ºæ±‚和公式 | é€šè¿‡ |
| TC-8 | å‡ºç§Ÿé¡µ | åŒä¸Š | Q9=1058.95(武汉客运量),格式 `0.00`;全省 Q5 ä¸ºæ±‚和公式 | é€šè¿‡ |
| TC-9 | ç­çº¿åŒ…车页全省行 | æœ‰å…¨çœåˆè®¡ï¼ˆåŽŸç¼ºå¤±ï¼‰ | Q4=`=Q8+Q10+Q14+…+Q56`、Q5=`=Q9+Q11+…+Q57`、Q6/Q7 ä¸ªä½“合计亦为公式 | é€šè¿‡ |
| TC-10 | è½¨é“、轮渡页全省行 | æœ‰å…¨çœåˆè®¡ï¼ˆåŽŸç¼ºå¤±ï¼‰ | è½¨é“ å…¨çœ Q4=`=SUM(Q6,Q8)`、Q5=`=Q7+Q9`;轮渡 å…¨çœ Q13=`=Q15`、Q14=`=Q16` | é€šè¿‡ |
| TC-11 | åŒæ¯”公式指向 2025 å¹´ 8 æœˆ | R åˆ—分母为 AJ åˆ— | å„页表头 AJ åˆ— = `2025å¹´8月`;R åˆ—公式形如 `=Q9/AJ9-1`;AJ åˆ—确有 2025 å¹´ 8 æœˆæ¯ç‰ˆæ•°å€¼ï¼ˆå¦‚货运 AJ7=3716.1158) | é€šè¿‡ |
| TC-12 | æ‰“开即重算 | æ–‡ä»¶å¸¦å…¨é‡é‡ç®—标记 | `xl/workbook.xml` å†… `<calcPr calcId="191029" fullCalcOnLoad="true"/>`,Excel/WPS æ‰“开即算出全省合计 | é€šè¿‡ |
### 3.2 ç‰ˆå¼å¯¹é½ï¼ˆç¬¬ 5 é¡¹ï¼‰
| ç¼–号 | æ£€æŸ¥é¡¹ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-13 | å…¨è¡¨åˆ—宽与《生成_道路运输量汇总表》比对(展开 `<col min..max>` èŒƒå›´é€åˆ—比对) | å®Œå…¨ä¸€è‡´ | 19 ä¸ªé¡µç­¾å…¨éƒ¨æ¯”对,**真实差异 0 åˆ—** | é€šè¿‡ |
| TC-14 | æ‰©åˆ—(8 æœˆ Q/R)列宽 | ä¸Žå‚ç…§ä»¶ 8 æœˆä¸€è‡´ | è´§è¿ P/Q/R = 12.67/10.55/11.67(参照 12.67/10.56/11.67,仅 0.01 æµ®ç‚¹èˆå…¥ï¼‰ï¼›ç½‘约车/轨道轮渡 Q/R=13.0/13.0;公交 Q/R=11.89/13.0;出租 Q/R=11.89/13.0;班线包车 Q/R=10.22/9.78 â€”— å‡ä¸Žå‚照件相同 | é€šè¿‡ |
### 3.3 è´Ÿå‘与回归
| ç¼–号 | æ£€æŸ¥é¡¹ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-15 | åŽ†å²æœˆï¼ˆ2026-07)不得被当月回填污染:与 `docs/生成汇总大表/_备份_2026å¹´7月道路运输量汇总表_清理前_20260904.xlsx` é€æ ¼æ¯”对 | é™¤æ—¢å®šæ”¹é€ å¤–无差异 | å·®å¼‚共三类:① 426 å¤„浮点末位差(最大相对差 7.26e-15,属 Excel 15 ä½æœ‰æ•ˆæ•°å­—截断,无实质差异);② 57 å¤„"文本 vs ç­‰å€¼æ•°å­—";③ 148 å¤„全部集中在「中口径排名」页 â€”— è¯¥é¡µå·¦ä¾§é™ˆæ—§å—已于 2026-09-08 æŒ‰éœ€æ±‚整块删除,属**预期变化**。**无回归** | é€šè¿‡ |
| TC-16 | ç¼ºæœˆæç¤º | ç¼ºæ•°æ®çš„æœˆä»½æœ‰æç¤º | 2026-08 å¯¼å‡º `缺数提示=[]` | é€šè¿‡ |
## å››ã€å‘现的问题与处置
1. **问题(已修复)**:`monthColumnCapacity` åŽŸå®žçŽ°åªè®¤"含 2026+累计"的表头,而货运页表头是"累计周转量",导致其恒返回默认列数、8 æœˆåˆ—永不回填。**处置**:改为「累计列」与「最后一个月份/日期格式月列」取较大值。
2. **问题(已修复)**:`shiftFormulaColumns` è¿žç»­ç»„引用被拼成 `Q5AJ5O5`。**处置**:`sb.append(quoteReplacement(...))` â†’ `m.appendReplacement(sb, quoteReplacement(...))`。
3. **问题(已修复)**:`ensureMonthYoyFormulas` ä¸­ `setCellFormula` å¤±è´¥è¢«é™é»˜åžæŽ‰ã€‚**处置**:改为抛出含页签/单元格/原式/目标式的 `IllegalStateException`,便于定位。
4. **问题(已修复)**:回填目标格若被套上日期格式,数值会显示成 1902/1910 å¹´ï¼ˆå³ç”¨æˆ·åé¦ˆçš„"日期形式")。**处置**:`setNumeric` å¯¹ç©º/日期格式格套用同行 1 æœˆåˆ—样式;对已有公式格直接跳过不覆盖。
5. **已知无害告警**:2026-08 å¯¼å‡ºæ—¶æ—¥å¿—出现 `汇总工作簿未找到同期备份母版(_备份_2026å¹´8月…),输出将保持母版现状`。原因是 8 æœˆæ²¡æœ‰"清理前备份母版",而 8 æœˆæ¯ç‰ˆæœ¬èº«å·²æ˜¯å®Œæ•´æ¯ç‰ˆï¼Œæ— éœ€ä»Žå¤‡ä»½è¿˜åŽŸï¼Œæ•…ä¸å½±å“è¾“å‡ºï¼ˆå·²é€é¡¹éªŒè¯å–å€¼æ­£ç¡®ï¼‰ã€‚7 æœˆå› å­˜åœ¨ `_备份_2026å¹´7月…` æ— æ­¤å‘Šè­¦ã€‚
6. **性能注意**:`applySummaryReferenceLayout` çš„列数上限保留 `min(参照件已用列数, ç›®æ ‡ä»¶å·²ç”¨åˆ—æ•° + 4)` â€”— åŽ»æŽ‰è¯¥ä¸Šé™ä¼šè®© POI åœ¨å¤§åˆ—号(货运页 147 åˆ—)上出现指数级卡顿。
## äº”、结论
8 æœˆæ±‡æ€»å¤§è¡¨å·²èƒ½ç”±åº“中数据自动生成:货运规上/规下、网约车、公交、出租、班线包车、轨道轮渡的当月列均已回填,全省合计与分市州合计一致;同比统一取 2025 å¹´ 8 æœˆï¼›å…¬äº¤/出租不再显示为日期;班线包车与轨道轮渡的全省数据已补齐(公式,打开即算);全表列宽与用户指定的《生成_道路运输量汇总表》逐列一致(含 8 æœˆæ‰©åˆ—宽度)。7 æœˆåŽ†å²æœˆå›žå½’æ— å®žè´¨å·®å¼‚ã€‚
遗留:请将真实的《网约车行业2026å¹´8月运行监测情况分析报告》重传一次,覆盖当前用 7 æœˆæŠ¥å‘Šä¸´æ—¶å‰¯æœ¬å¯¼å…¥çš„ 8 æœˆå¸‚州订单数。
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_ÍøÔ¼³µ¶©µ¥±¨¸æµ¼Èë_2026-09-11.md
New file
@@ -0,0 +1,41 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_网约车订单报告导入_2026-09-11
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
支持直接上传部级《网约车行业YYYYå¹´M月运行监测情况分析报告》(.docx),系统自动解析报告正文附件表中的 **17 å¸‚州订单数**并写入数据库 `wyc_order_monthly`(F åˆ—订单合计 + G-W 17 å¸‚州订单列);
汇总大表「网约车」页取数改为**优先读库**,实现"上传报告 â†’ æ±‡æ€»è¡¨å½“月网约车数据自动出现"。
改动范围:
- åŽç«¯ `DataImportService.importWycOrderReport()` / `parseWycOrderReportTables()` / `periodOfDocx()`:解析报告、定位报表期、落库 `wyc_order_monthly`,并在缺失时从 `wyc_total_monthly` è¡¥é½å…¨çœ pin(B-E)。
- åŽç«¯ `DataImportController`:新增 `POST /api/import/wycOrderReport`(参数 file + period)。
- åŽç«¯ `WycSplitCalc.loadMonthlyInputFromDb()`:汇总表取数优先读 `wyc_order_monthly`(库里该期订单全空时回退读 `docs/城市客运/模板_网约车订单及全省总量.xlsx`)。
- å‰ç«¯ `DataImport.vue`:导入类型新增「网约车订单报告」,模板提示为 `城市客运/模板_网约车订单.docx`,接口 `/import/wycOrderReport`,文件类型限 `.docx/.doc`。
- æ¸…空映射:`wycOrderReport` å…³è”清空 `WYC_ORDER` æ‰¹æ¬¡ï¼›`wycOrder` ä¸Ž `wycOrderReport` äº’为关联清除。
报表期取值规则:优先取报告标题中的「网约车行业YYYY年M月」,取不到时用页面所选报表期;两者都取不到则报错。
## äºŒã€æµ‹è¯•环境
- åŽç«¯ï¼šSpring Boot,`http://localhost:8090`(8090 å·²ç›‘听);前端:`http://localhost:8080`。
- æ•°æ®åº“:MySQL 5.7 `127.0.0.1:3308` / `traffic_audit`。
- ç™»å½•:admin;测试时间:2026-09-11。
## ä¸‰ã€ç”¨ä¾‹è¡¨
| ç¼–号 | ç±»åž‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- | --- |
| TC-1 | æ­£å‘ | ä¸Šä¼  `docs/城市客运/模板_网约车订单.docx`(《网约车行业2026å¹´7月运行监测情况分析报告》),period=2026-07 | è§£æžæˆåŠŸï¼ŒæŠ¥è¡¨æœŸå–æ ‡é¢˜çš„ 2026-07,17 å¸‚州全部命中 | è¿”回"报表期 2026-07,订单合计 29,730,791 å•,已写入 17 ä¸ªå¸‚å·ž" | é€šè¿‡ |
| TC-2 | æ­£å‘ | åŒä¸Šï¼Œæ£€æŸ¥åŽç«¯æ—¥å¿— | è®°å½• period/sum/cities/missing | `WycOrderReport imported: period=2026-07 sum=2.9730791E7 cities=17 missing=[]` | é€šè¿‡ |
| TC-3 | å…¥åº“抽查 | docx é™„件表「地市名称 / è®¢å•数总数」17 è¡Œï¼Œé€é¡¹ä¸Ž `wyc_order_monthly` æ¯”对 | 17 å¸‚州逐一相等,合计相等 | æ­¦æ±‰ 11,183,267;黄石 983,296;十堰 2,155,144;宜昌 3,024,336;襄阳 2,390,572;鄂州 321,348;荆门 942,124;孝感 1,220,812;荆州 1,573,514;黄冈 1,125,829;咸宁 928,460;随州 528,210;恩施 2,166,015;仙桃 528,557;潜江 267,879;天门 387,894;林区 3,534;合计 29,730,791 â€”— **与 DB å®Œå…¨ä¸€è‡´** | é€šè¿‡ |
| TC-4 | è´Ÿå‘(文件类型错) | æŠŠ `模板_网约车订单及全省总量.xlsx` ä¼ ç»™æŠ¥å‘Šå¯¼å…¥æŽ¥å£ | ç»™å‡ºå¯è¯»æç¤ºï¼Œä¸è½åº“ | è¿”回"导入失败: æ— æ³•读取 Word æ–‡æ¡£ï¼Œè¯·ä¸Šä¼  .docx æ ¼å¼çš„《网约车行业YYYYå¹´M月运行监测情况分析报告》;若上传的其实是 Excel/PDF,请改用对应模板导入。(原始错误:…)" | é€šè¿‡ï¼ˆå«ä¿®å¤ï¼Œè§ç¬¬å››èŠ‚ï¼‰ |
| TC-5 | è´Ÿå‘(内容不符) | ä¸Šä¼  `城市客运数据优化.docx`(非网约车监测报告) | æ˜Žç¡®æç¤ºæœªè§£æžåˆ°å¸‚州订单数 | è¿”回"导入失败: æœªè§£æžåˆ°ä»»ä½•市州订单数,请确认上传的是《网约车行业YYYYå¹´M月运行监测情况分析报告》" | é€šè¿‡ |
| TC-6 | æ­£å‘(重复导入幂等) | åŒä¸€ä»½ 7 æœˆæŠ¥å‘Šè¿žç»­å¯¼å…¥ä¸¤æ¬¡ | ç¬¬äºŒæ¬¡è¦†ç›–同报表期行,不产生重复行 | ä»è¿”回 17 å¸‚å·žï¼›`wyc_order_monthly` ä¸­ `2026-07` ä»… 1 è¡Œ | é€šè¿‡ |
| TC-7 | è”è°ƒ | æŠ¥å‘Šå¯¼å…¥åŽé‡æ–°å¯¼å‡º `period=2026-08` æ±‡æ€»å¤§è¡¨ | ç½‘约车页当月列(Q åˆ—)出现数值,全省行为求和公式 | Q åˆ— 17 å¸‚州全部有值,Q5 èµ· 4 ä¸ªå…¨çœè¡Œä¸º `=Q9+Q13+…` æ±‚和公式,R åˆ—同比公式 `=Q../AJ..-1`(AJ=2025å¹´8月) | é€šè¿‡ï¼ˆè¯¦è§ã€ŠåŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_汇总大表8月数据回填与版式对齐_2026-09-11.md》) |
## å››ã€å‘现的问题与处置
1. **问题(已修复)**:把 Excel/PDF è¯¯ä¼ åˆ°ã€Œç½‘约车订单报告」类型时,接口返回的是 xmlbeans çš„英文技术异常(`org.apache.xmlbeans.XmlException: ... is not a valid document`),页面用户无法理解。
   **处置**:`DataImportService.importWycOrderReport()` çš„解析段统一捕获异常,改写为用户可读提示,并把原始错误以"(原始错误:…)"附在末尾便于排查。修复后 TC-4 å¤æµ‹é€šè¿‡ã€‚
2. **需要用户操作的事项**:当前库中 `2026-08` çš„**市州订单数**是用 7 æœˆæŠ¥å‘Šæ”¹æ ‡é¢˜çš„临时副本(`_t_wyc_aug.docx`)导入的,其市州值与 7 æœˆç›¸åŒï¼›`pin`(B-E,即客运量/周转量口径)取自 `wyc_total_monthly` çš„ 2026-08 è¡Œï¼Œæ˜¯æ­£ç¡®çš„。**请把真实的《网约车行业2026å¹´8月运行监测情况分析报告》重新上传一次**(类型选「网约车订单报告」,报表期选 2026-08),即可覆盖为真实 8 æœˆå¸‚州订单数。
## äº”、结论
网约车订单运行监测报告(docx)导入功能可用:正向解析 17 å¸‚州订单数准确入库并与源文件逐项一致;文件类型错误、内容不符两类负向场景均能给出明确提示;重复导入幂等;导入后汇总大表可自动取到当月数据。建议将真实 8 æœˆæŠ¥å‘Šé‡ä¼ åŽå†æ¬¡å¯¼å‡ºæ ¸å¯¹ã€‚
docs/³ÇÊпÍÔË/Ä£°å_ÍøÔ¼³µ¶©µ¥.docx
Binary files differ
docs/¹¤×÷ÈÕ½á/¹¤×÷ÈÕ½á_2026-09-11.md
New file
@@ -0,0 +1,40 @@
# å·¥ä½œæ—¥ç»“_2026-09-11
## ä¸€ã€ä»Šå¤©åšäº†ä»€ä¹ˆ
### 1. åŸŽå¸‚客运:网约车订单运行监测报告(docx)导入(用户第 1 é¡¹ï¼‰
- åŽç«¯æ–°å¢ž `POST /api/import/wycOrderReport` ä¸Ž `DataImportService.importWycOrderReport()`:解析《网约车行业YYYYå¹´M月运行监测情况分析报告》附件表「地市名称 / è®¢å•数总数」,取 17 å¸‚州订单数写入 `wyc_order_monthly`(F è®¢å•合计 + G-W å¸‚州列),报表期优先取报告标题年月。
- å…¨çœ pin(B-E)在该期行缺失时,从 `wyc_total_monthly` åŒæŠ¥è¡¨æœŸè¡¥é½ã€‚
- å‰ç«¯å¯¼å…¥ç•Œé¢æ–°å¢žç±»åž‹ã€Œç½‘约车订单报告」,绑定 `城市客运/模板_网约车订单.docx`(限 .docx/.doc),并接入按类型清空。
- æ±‡æ€»å¤§è¡¨ç½‘约车取数改为**优先读库**(`WycSplitCalc.loadMonthlyInputFromDb`),库里无数据自动回退读 xlsx æ¯ç‰ˆã€‚
- ä¿®å¤ï¼šè¯¯ä¼  Excel æ—¶æŠ›å‡º xmlbeans è‹±æ–‡å¼‚常 â†’ æ”¹ä¸ºç”¨æˆ·å¯è¯»æç¤ºã€‚
### 2. æ±‡æ€»å¤§è¡¨ 8 æœˆæ•°æ®å›žå¡«ä¸Žç‰ˆå¼å¯¹é½ï¼ˆç”¨æˆ·ç¬¬ 2/3/4/5 é¡¹ï¼‰
- 8 æœˆæºæ•°æ®å·²åœ¨åº“ â†’ å¯¼å‡ºæ—¶è‡ªåŠ¨å›žå¡«å½“æœˆå€¼åˆ—ï¼ˆQ åˆ—),历史月不动。
- åŒæ¯”统一以汇总表 **2025 å¹´ 8 æœˆï¼ˆAJ åˆ—)** ä¸ºåˆ†æ¯ï¼Œå…¬å¼ `=Q../AJ..-1`。
- å…¬äº¤/出租数值不再被日期格式吞掉(按同行 1 æœˆåˆ—样式套用,`0.00`)。
- ç­çº¿åŒ…车、轨道轮渡「全省」行补齐合计公式(母版原只对 1 æœˆå†™æ­»å…¬å¼ï¼‰ã€‚
- ç‰ˆå¼æŒ‰ç”¨æˆ·æŒ‡å®šå‚ç…§ä»¶ `docs/生成汇总大表/生成_道路运输量汇总表.xlsx` é€åˆ—对齐列宽/隐藏;**8 æœˆæ‰©åˆ— Q/R çš„宽度也取自参照件**。
### 3. æ–‡æ¡£å½’æ¡£
- æ˜¨å¤©å·²å®Œæˆã€Šæ¹–北省道路货物运输量分市州统计方法(2023)编制说明》学习笔记归档(`docs/货运/学习笔记_分市州道路货物运输量统计方法_2026-09-10.md`),今天未再改动。
## äºŒã€æµ‹è¯•结果摘要(详见《功能测试报告》)
- `docs/功能测试报告/功能测试报告_网约车订单报告导入_2026-09-11.md`:TC-1~TC-7 å…¨éƒ¨é€šè¿‡ï¼›7 æœˆæŠ¥å‘Šè§£æž 17 å¸‚州、合计 29,730,791,与库中逐项一致;两类负向用例提示明确。
- `docs/功能测试报告/功能测试报告_汇总大表8月数据回填与版式对齐_2026-09-11.md`:TC-1~TC-16 å…¨éƒ¨é€šè¿‡ã€‚
  - 2026-08 å¯¼å‡ºæˆåŠŸï¼ˆ4.6s / 948,735 å­—节);写入当月值 1659 æ ¼ã€å½“月同比公式 600 æ ¼ã€å½“月合计公式 43 æ ¼ã€ç¼ºæ•°æç¤ºä¸ºç©ºã€‚
  - è´§è¿ Î£17 å¸‚å·ž = å…¨çœï¼ˆ16888.7615);网约车/公交/出租/班线包车/轨道轮渡全省行与市州行均正确。
  - åˆ—宽与参照件**真实差异 0 åˆ—**。
  - 7 æœˆåŽ†å²æœˆå›žå½’ï¼šæ— å®žè´¨å·®å¼‚ï¼ˆ148 å¤„差异全部是「中口径排名」页 09-08 å·²æŒ‰éœ€æ±‚删除的旧左块)。
## ä¸‰ã€é—留问题与待办
1. **需用户操作**:库中 `2026-08` çš„市州订单数目前是用 7 æœˆæŠ¥å‘Šæ”¹æ ‡é¢˜çš„临时副本(`_t_wyc_aug.docx`)导入的,市州值与 7 æœˆç›¸åŒï¼›è¯·æŠŠçœŸå®žçš„ 8 æœˆè¿è¡Œç›‘测报告重传一次(类型「网约车订单报告」,报表期 2026-08)覆盖。
2. 2026-08 å¯¼å‡ºä¼šæ‰“印「未找到同期备份母版(_备份_2026å¹´8月…)」告警,属无害提示(8 æœˆæ¯ç‰ˆæœ¬èº«å®Œæ•´ï¼‰ï¼ŒåŽç»­å¯è€ƒè™‘按需求决定是否消除。
3. æ ¹ç›®å½•堆积较多历史临时文件(`_t_*.py`、`_*.sql`、`_gen_*.xlsx`、`_bak_*.java`、`_token.txt` ç­‰ï¼‰ï¼Œå¾…用户同意后集中清理。
4. ç‰ˆå¼å‚ç…§ä»¶ `docs/生成汇总大表/生成_道路运输量汇总表.xlsx` ç›®å‰ä¸ºæœªå…¥åº“文件,是否纳入版本管理待确认。
## å››ã€æ˜Žå¤©è®¡åˆ’
1. ç”¨æˆ·é‡ä¼ çœŸå®ž 8 æœˆç½‘约车报告后,重新导出 2026-08 å¹¶é€é¡¹å¤æ ¸ç½‘约车页。
2. ç»§ç»­æŒ‰ç”¨æˆ·åé¦ˆæ ¸å¯¹ 8 æœˆæ±‡æ€»å¤§è¡¨æˆå“ï¼ˆé‡ç‚¹ï¼šä¸­å£å¾„各页、货运排名页)。
3. æ¸…理临时文件(需用户确认)。
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/controller/DataImportController.java
@@ -269,6 +269,24 @@
        }
    }
    /** ç½‘约车订单运行监测分析报告(docx:《网约车行业YYYYå¹´M月运行监测情况分析报告》) */
    @PostMapping("/wycOrderReport")
    public Result<String> importWycOrderReport(@RequestParam("file") MultipartFile file,
                                               @RequestParam("period") String period) {
        try {
            DataImportService.WycOrderReportResult r = importService.importWycOrderReport(file, period);
            StringBuilder msg = new StringBuilder("网约车订单报告导入成功:报表期 " + r.getPeriod()
                    + ",订单合计 " + String.format("%,.0f", r.getOrderSum()) + " å•,已写入 " + r.getCityCount() + " ä¸ªå¸‚å·ž");
            if (!r.getMissingCities().isEmpty()) {
                msg.append(";以下市州未在报告中解析到,未写入:").append(String.join("、", r.getMissingCities()));
            }
            return Result.ok(msg.toString());
        } catch (Exception e) {
            log.error("wycOrderReport import error", e);
            return Result.error("导入失败: " + e.getMessage());
        }
    }
    /** æŠ•资月报(全省汇总大表):category=客运站场/物流园区 */
    @PostMapping("/investment")
    public Result<String> importInvestment(@RequestParam("file") MultipartFile file,
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java
@@ -56,6 +56,10 @@
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import cn.hutool.core.io.IoUtil;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -1345,6 +1349,7 @@
        CLEAR_TYPE_TO_OP.put("cityTaxiAuth", "CITY_TAXI_AUTH");
        CLEAR_TYPE_TO_OP.put("wycTotal", "WYC_TOTAL");
        CLEAR_TYPE_TO_OP.put("wycOrder", "WYC_ORDER");
        CLEAR_TYPE_TO_OP.put("wycOrderReport", "WYC_ORDER");
        CLEAR_TYPE_TO_AUDIT.put("h2032", new String[]{"H2032"});
        CLEAR_TYPE_TO_AUDIT.put("transportAuth", new String[]{"H2032"});
@@ -1386,7 +1391,8 @@
        CLEAR_TYPE_TO_BATCH.put("cityTaxi", new String[]{"CITY_TAXI"});
        CLEAR_TYPE_TO_BATCH.put("cityTaxiAuth", new String[]{"CITY_TAXI_AUTH"});
        CLEAR_TYPE_TO_BATCH.put("wycTotal", new String[]{"WYC_TOTAL"});
        CLEAR_TYPE_TO_BATCH.put("wycOrder", new String[]{"WYC_ORDER"});
        CLEAR_TYPE_TO_BATCH.put("wycOrder", new String[]{"WYC_ORDER", "WYC_ORDER_REPORT"});
        CLEAR_TYPE_TO_BATCH.put("wycOrderReport", new String[]{"WYC_ORDER_REPORT", "WYC_ORDER"});
    }
    /**
@@ -3144,6 +3150,209 @@
        return ok;
    }
    // ========== ç½‘约车订单运行监测分析报告(docx)导入 ==========
    /** ç½‘约车订单报告导入结果(报表期/订单合计/覆盖市州数/缺失市州) */
    public static class WycOrderReportResult {
        private final String period;
        private final double orderSum;
        private final int cityCount;
        private final List<String> missingCities;
        public WycOrderReportResult(String period, double orderSum, int cityCount, List<String> missingCities) {
            this.period = period;
            this.orderSum = orderSum;
            this.cityCount = cityCount;
            this.missingCities = missingCities;
        }
        public String getPeriod() { return period; }
        public double getOrderSum() { return orderSum; }
        public int getCityCount() { return cityCount; }
        public List<String> getMissingCities() { return missingCities; }
    }
    /**
     * ã€Šç½‘约车行业YYYYå¹´M月运行监测情况分析报告》(docx)导入:
     * è§£æžæŠ¥å‘Šé™„件中「地市名称 | â€¦ | è®¢å•数总数」表的 17 å¸‚州订单数,写入 wyc_order_monthly çš„
     * F è®¢å•合计与 G-W 17 å¸‚州订单列;全省 pin(B-E)沿用该期已有行,若无该期行则从
     * wyc_total_monthly åŒæŠ¥è¡¨æœŸå¤åˆ¶ã€‚报表期优先取报告标题年月,取不到时用页面所选报表期。
     */
    public synchronized WycOrderReportResult importWycOrderReport(MultipartFile file, String period) throws Exception {
        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);
        } catch (Exception e) {
            throw new RuntimeException("无法读取 Word æ–‡æ¡£ï¼Œè¯·ä¸Šä¼  .docx æ ¼å¼çš„《网约车行业YYYYå¹´M月运行监测情况分析报告》;"
                    + "若上传的其实是 Excel/PDF,请改用对应模板导入。(原始错误:" + e.getMessage() + ")");
        }
        if (orders.isEmpty()) {
            throw new RuntimeException("未解析到任何市州订单数,请确认上传的是《网约车行业YYYYå¹´M月运行监测情况分析报告》");
        }
        String rp = null;
        if (docPeriod != null && docPeriod.matches("\\d{4}-\\d{2}")) rp = docPeriod;
        else if (period != null && period.matches("\\d{4}-\\d{2}")) rp = period;
        if (rp == null) {
            throw new RuntimeException("无法确定报表期:报告标题未含「YYYYå¹´M月」,且页面未选择报表期");
        }
        List<String> missing = new ArrayList<>();
        double citySum = 0.0;
        for (String city : RegionUtil.cityList()) {
            Double v = orders.get(city);
            if (v == null) missing.add(city);
            else citySum += v;
        }
        double sum = sumHolder[0] > 0 ? sumHolder[0] : citySum;
        WycOrderMonthly it = wycOrderMapper.selectOne(new LambdaQueryWrapper<WycOrderMonthly>()
                .eq(WycOrderMonthly::getReportPeriod, rp).last("LIMIT 1"));
        if (it == null) it = new WycOrderMonthly();
        it.setReportPeriod(rp);
        it.setOrderSum(sum > 0 ? sum : null);
        it.setOrderWuhan(orderOf(orders, "武汉市"));
        it.setOrderHuangshi(orderOf(orders, "黄石市"));
        it.setOrderShiyan(orderOf(orders, "十堰市"));
        it.setOrderYichang(orderOf(orders, "宜昌市"));
        it.setOrderXiangyang(orderOf(orders, "襄阳市"));
        it.setOrderEzhou(orderOf(orders, "鄂州市"));
        it.setOrderJingmen(orderOf(orders, "荆门市"));
        it.setOrderXiaogan(orderOf(orders, "孝感市"));
        it.setOrderJingzhou(orderOf(orders, "荆州市"));
        it.setOrderHuanggang(orderOf(orders, "黄冈市"));
        it.setOrderXianning(orderOf(orders, "咸宁市"));
        it.setOrderSuizhou(orderOf(orders, "随州市"));
        it.setOrderEnshi(orderOf(orders, "恩施州"));
        it.setOrderXiantao(orderOf(orders, "仙桃市"));
        it.setOrderQianjiang(orderOf(orders, "潜江市"));
        it.setOrderTianmen(orderOf(orders, "天门市"));
        it.setOrderShennong(orderOf(orders, "神农架林区"));
        if (it.getPinK() == null || it.getPinCK() == null || it.getPinZ() == null || it.getPinCZ() == null) {
            WycTotalMonthly tot = wycTotalMapper.selectOne(new LambdaQueryWrapper<WycTotalMonthly>()
                    .eq(WycTotalMonthly::getReportPeriod, rp).last("LIMIT 1"));
            if (tot != null) {
                if (it.getPinK() == null) it.setPinK(tot.getPinK());
                if (it.getPinCK() == null) it.setPinCK(tot.getPinCK());
                if (it.getPinZ() == null) it.setPinZ(tot.getPinZ());
                if (it.getPinCZ() == null) it.setPinCZ(tot.getPinCZ());
            }
        }
        wycOrderMapper.delete(new LambdaQueryWrapper<WycOrderMonthly>().eq(WycOrderMonthly::getReportPeriod, rp));
        wycOrderMapper.insert(it);
        recordBatch(file.getOriginalFilename(), "WYC_ORDER_REPORT", rp, 1, 1, 0, null);
        log.info("WycOrderReport imported: period={} sum={} cities={} missing={}", rp, sum, orders.size(), missing);
        return new WycOrderReportResult(rp, sum, orders.size(), missing);
    }
    /** æŠ¥å‘Šæ ‡é¢˜ä¸­çš„æŠ¥è¡¨æœŸï¼šç½‘约车行业2026å¹´7月 -> 2026-07;取不到返回 null */
    private String periodOfDocx(XWPFDocument doc) {
        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();
            if (text == null) continue;
            String flat = text.replaceAll("\\s+", "");
            if (flat.isEmpty()) continue;
            java.util.regex.Matcher m = titlePat.matcher(flat);
            if (m.find()) return ymd(m.group(1), m.group(2));
            if (fallback == null && scanned < 10) {
                java.util.regex.Matcher m2 = anyPat.matcher(flat);
                if (m2.find()) fallback = ymd(m2.group(1), m2.group(2));
            }
            scanned++;
        }
        return fallback;
    }
    private String ymd(String y, String mo) {
        try {
            int month = Integer.parseInt(mo);
            if (month < 1 || month > 12) return null;
            return String.format("%04d-%02d", Integer.parseInt(y), month);
        } catch (NumberFormatException e) {
            return null;
        }
    }
    /** è§£æžæŠ¥å‘Šä¸­çš„「地市名称 | â€¦ | è®¢å•数总数」表,orders=市州订单,sumHolder[0]=合计行订单数 */
    private void parseWycOrderReportTables(XWPFDocument doc, Map<String, Double> orders, double[] sumHolder) {
        for (XWPFTable table : doc.getTables()) {
            List<XWPFTableRow> rows = table.getRows();
            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();
                for (int c = 0; c < cells.size(); c++) {
                    String norm = flat(cells.get(c).getText());
                    if (norm.contains("订单数总数") || "订单数".equals(norm)) {
                        headerRow = r;
                        orderCol = c;
                        break;
                    }
                }
                if (headerRow >= 0) break;
            }
            if (headerRow < 0 || orderCol < 0) continue;
            Map<String, Double> found = new LinkedHashMap<>();
            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());
                if (name.isEmpty()) continue;
                Double val = parsePlainNumber(cells.get(orderCol).getText());
                if (val == null) continue;
                String city = RegionUtil.normalizeCityName(name);
                if (city != null && RegionUtil.cityList().contains(city)) {
                    found.put(city, val);
                    citySum += val;
                } else if (name.contains("合计") || name.contains("总计") || name.contains("全省") || name.contains("湖北")) {
                    hasTotal = true;
                    sumHolder[0] = val;
                }
            }
            if (!found.isEmpty()) {
                orders.clear();
                orders.putAll(found);
                if (!hasTotal) sumHolder[0] = citySum;
                return;
            }
        }
    }
    private String flat(String s) {
        return s == null ? "" : s.replaceAll("\\s+", "").trim();
    }
    /** ã€Œ11,183,267 å•」这类文本取数值 */
    private Double parsePlainNumber(String s) {
        if (s == null) return null;
        String t = s.replaceAll("[,\\s]", "").trim();
        if (t.isEmpty()) return null;
        java.util.regex.Matcher m = java.util.regex.Pattern.compile("-?\\d+(?:\\.\\d+)?").matcher(t);
        if (!m.find()) return null;
        try {
            return Double.parseDouble(m.group());
        } catch (NumberFormatException e) {
            return null;
        }
    }
    private Double orderOf(Map<String, Double> orders, String city) {
        Double v = orders.get(city);
        return (v == null || v <= 0) ? null : v;
    }
    private String periodOfCell(Cell c) {
        if (c == null) return null;
        if (c.getCellType() == CellType.NUMERIC) {
@@ -3513,6 +3722,7 @@
            case "scaleSplit": return new ImportResult(importScaleSplit(file, period), 0, new ArrayList<>());
            case "wycTotal": return new ImportResult(importWycTotal(file, period), 0, new ArrayList<>());
            case "wycOrder": return new ImportResult(importWycOrder(file, period), 0, new ArrayList<>());
            case "wycOrderReport": return new ImportResult(importWycOrderReport(file, period).getCityCount(), 0, new ArrayList<>());
            default: throw new RuntimeException("不支持的导入类型:" + type);
        }
    }
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java
@@ -1,6 +1,8 @@
package com.trafficaudit.reportexport.calc;
import com.trafficaudit.common.util.RegionUtil;
import com.trafficaudit.reportexport.calc.FreightCalc.FreightMatrix;
import com.trafficaudit.reportexport.calc.FreightCalc.FreightMetrics;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
@@ -36,12 +38,16 @@
    private MidCalc midCalc;
    @Resource
    private PaxCalc paxCalc;
    @Resource
    private FreightCalc freightCalc;
    @Resource
    private WycSplitCalc wycSplitCalc;
    /** 2026 å¹´ 1 æœˆå½“月值列(0 åŸºï¼Œå³ C åˆ—) */
    private static final int FIRST_MONTH_COL = 2;
    /** å›žå¡« 1..keepMonths æœˆå°è´¦é¡µï¼Œè¿”回写入格数;缺数提示写入 problems */
    public int fillMonthlyLedger(XSSFWorkbook wb, int year, int keepMonths, List<String> problems) {
    public int fillMonthlyLedger(XSSFWorkbook wb, String period, int year, int keepMonths, List<String> problems) {
        if (wb == null || keepMonths <= 0) return 0;
        if (problems == null) problems = new ArrayList<>();
        String yearPrefix = year + "-";
@@ -50,6 +56,8 @@
        filled += fillCityRows(wb, "公交", paxCalc.busMonthly(yearPrefix, keepMonths), year, keepMonths, problems, false);
        filled += fillCityRows(wb, "出租", paxCalc.taxiMonthly(yearPrefix, keepMonths), year, keepMonths, problems, false);
        filled += fillTrackFerry(wb, paxCalc.railFerryMonthly(yearPrefix, keepMonths), year, keepMonths, problems);
        filled += fillFreightSheet(wb, period, keepMonths, problems);
        filled += fillWycSheet(wb, period, keepMonths, problems);
        return filled;
    }
@@ -117,6 +125,177 @@
            }
        }
        return filled;
    }
    /** è´§è¿é¡µå›žå¡«ï¼šå…¨çœå— + 17 å¸‚州块的 è§„上/规下/合计 Ã— è´§è¿é‡ã€å‘¨è½¬é‡ï¼ˆé€æœˆçŽ°ç®—ï¼›å½“æœˆå€¼åˆ—=2+2*(m-1)) */
    private int fillFreightSheet(XSSFWorkbook wb, String period, int keepMonths, List<String> problems) {
        Sheet sh = wb.getSheet(" è´§è¿");
        if (sh == null) sh = wb.getSheet("货运");
        if (sh == null) {
            problems.add("货运页不存在(母版缺失该页),跳过自动回填");
            return 0;
        }
        int cap = monthColumnCapacity(sh);
        int mMax = Math.min(keepMonths, cap);
        if (mMax <= 0) {
            problems.add("货运页未识别到 2026 å¹´ç´¯è®¡åˆ—,跳过自动回填");
            return 0;
        }
        int year = periodYear(period);
        // å—(全省/市州):指标键 -> è¡Œå·
        Map<String, Map<String, Integer>> blockRows = new LinkedHashMap<>();
        String cur = null;
        for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String a = text(row.getCell(0));
            if (a != null && !a.trim().isEmpty()) {
                String norm = RegionUtil.normalizeCityName(a.trim());
                if (FreightCalc.PROVINCE.equals(norm) || RegionUtil.cityList().contains(norm)) {
                    cur = norm;
                    blockRows.computeIfAbsent(cur, k -> new LinkedHashMap<>());
                } else {
                    cur = null; // æ ‡é¢˜è¡Œ / éžå…¨çœéžå¸‚å·ž -> è„±ç¦»å—
                }
            }
            if (cur == null) continue;
            String key = freightMetricKey(text(row.getCell(1)));
            if (key == null) continue;
            blockRows.get(cur).putIfAbsent(key, r);
        }
        if (blockRows.isEmpty()) {
            problems.add("货运页未识别到全省/市州行,跳过自动回填");
            return 0;
        }
        int filled = 0;
        // åªå›žå¡«ç›®æ ‡æœŸå½“月:1..目标月-1 çš„值以母版(含同期备份母版还原)为准,避免改写已确认口径
        int targetM = Math.min(periodMonth(period), mMax);
        for (int m = targetM; m <= targetM; m++) {
            String mp = String.format("%04d-%02d", year, m);
            FreightMatrix mx;
            try {
                mx = freightCalc.calc(mp);
            } catch (Exception e) {
                problems.add("货运页 " + mp + " å–数失败:" + e.getMessage());
                continue;
            }
            int col = FIRST_MONTH_COL + 2 * (m - 1);
            for (Map.Entry<String, Map<String, Integer>> e : blockRows.entrySet()) {
                FreightMetrics fm = mx.getMonth().get(e.getKey());
                if (fm == null) continue;
                Map<String, Integer> rows = e.getValue();
                // 1 æœˆåˆ—是公式的“派生行”(全省合计 è´§è¿é‡/周转量、市州 è§„上+规下周转量)不写值,
                // ç”± ensureMonthFormulas æŠŠå…¬å¼æŒ‰æœˆå³ç§»ï¼Œä¿æŒä¸Žæ¯ç‰ˆä¸€è‡´
                filled += setFreightCell(sh, skipIfDerived(sh, rows.get("totalFreight")), col, fm.getTotalFreight());
                filled += setFreightCell(sh, skipIfDerived(sh, rows.get("aboveFreight")), col, fm.getAboveFreight());
                filled += setFreightCell(sh, skipIfDerived(sh, rows.get("belowFreight")), col, fm.getBelowFreight());
                filled += setFreightCell(sh, skipIfDerived(sh, rows.get("totalTurnover")), col, fm.getTotalTurnover());
                filled += setFreightCell(sh, skipIfDerived(sh, rows.get("aboveTurnover")), col, fm.getAboveTurnover());
                filled += setFreightCell(sh, skipIfDerived(sh, rows.get("belowTurnover")), col, fm.getBelowTurnover());
            }
        }
        return filled;
    }
    /** è¯¥è¡Œ 1 æœˆåˆ—(C)是公式 -> æ´¾ç”Ÿè¡Œï¼Œè¿”回 null è¡¨ç¤ºä¸å›žå¡«æ•°å€¼ */
    private Integer skipIfDerived(Sheet sh, Integer r) {
        if (r == null) return null;
        Row row = sh.getRow(r);
        if (row == null) return r;
        Cell c = row.getCell(FIRST_MONTH_COL);
        if (c != null && c.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) return null;
        return r;
    }
    private int setFreightCell(Sheet sh, Integer r, int col, Double v) {
        if (r == null || v == null) return 0;
        return setNumeric(sh, r, col, v);
    }
    /** è´§è¿é¡µ B åˆ—指标文案 -> æŒ‡æ ‡é”®ï¼ˆè§„上+规下 / è§„上 / è§„下 Ã— è´§è¿é‡ / å‘¨è½¬é‡ï¼‰ */
    private String freightMetricKey(String b) {
        if (b == null) return null;
        String s = b.replaceAll("\\s+", "");
        if (s.isEmpty()) return null;
        boolean turnover = s.contains("周转量");
        if (s.contains("规上+规下")) return turnover ? "totalTurnover" : "totalFreight";
        if (s.contains("规上")) return turnover ? "aboveTurnover" : "aboveFreight";
        if (s.contains("规下")) return turnover ? "belowTurnover" : "belowFreight";
        return turnover ? "totalTurnover" : "totalFreight"; // å…¨çœå—的“货运量/货物周转量”= åˆè®¡
    }
    /** ç½‘约车页回填:17 å¸‚å·ž çš„ å®¢è¿é‡/周转量/其中城市内客运量/其中城市内周转量(逐月现算)
     *  å…¨çœè¡Œï¼ˆ=17 å¸‚州求和公式)由 ensureMonthFormulas æŒ‰æœˆæ‰©åˆ—,不在此处写值。 */
    private int fillWycSheet(XSSFWorkbook wb, String period, int keepMonths, List<String> problems) {
        Sheet sh = wb.getSheet("网约车");
        if (sh == null) {
            problems.add("网约车页不存在(母版缺失该页),跳过自动回填");
            return 0;
        }
        int cap = monthColumnCapacity(sh);
        int mMax = Math.min(keepMonths, cap);
        if (mMax <= 0) {
            problems.add("网约车页未识别到 2026 å¹´ç´¯è®¡åˆ—,跳过自动回填");
            return 0;
        }
        int year = periodYear(period);
        Map<String, Map<String, Integer>> cityMetricRows = new LinkedHashMap<>();
        String cur = null;
        for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String a = text(row.getCell(0));
            if (a != null && !a.trim().isEmpty()) {
                String norm = RegionUtil.normalizeCityName(a.trim());
                cur = RegionUtil.cityList().contains(norm) ? norm : null;
                if (cur != null) cityMetricRows.computeIfAbsent(cur, k -> new LinkedHashMap<>());
            }
            if (cur == null) continue;
            String key = wycMetricKey(text(row.getCell(1)));
            if (key == null) continue;
            cityMetricRows.get(cur).putIfAbsent(key, r);
        }
        if (cityMetricRows.isEmpty()) {
            problems.add("网约车页未识别到市州行,跳过自动回填");
            return 0;
        }
        int filled = 0;
        // åªå›žå¡«ç›®æ ‡æœŸå½“月(历史月以母版/历史生成件为准)
        int targetM = Math.min(periodMonth(period), mMax);
        for (int m = targetM; m <= targetM; m++) {
            String mp = String.format("%04d-%02d", year, m);
            WycSplitCalc.WycResult wr;
            try {
                wr = wycSplitCalc.calc(mp);
            } catch (Exception e) {
                if (m == mMax) {
                    problems.add("网约车页 " + mp + " æºæ•°æ®ç¼ºå¤±ï¼Œå½“月未回填:" + e.getMessage());
                }
                continue;
            }
            int col = FIRST_MONTH_COL + 2 * (m - 1);
            for (Map.Entry<String, Map<String, Integer>> e : cityMetricRows.entrySet()) {
                WycSplitCalc.WycMetrics wm = wr.getByCity().get(e.getKey());
                if (wm == null) continue;
                Map<String, Integer> rows = e.getValue();
                filled += setFreightCell(sh, rows.get("pax"), col, wm.getTotalPax());
                filled += setFreightCell(sh, rows.get("turnover"), col, wm.getTotalTurnover());
                filled += setFreightCell(sh, rows.get("cityPax"), col, wm.getCityPax());
                filled += setFreightCell(sh, rows.get("cityTurnover"), col, wm.getCityTurnover());
            }
        }
        return filled;
    }
    /** ç½‘约车页 B åˆ—指标文案 -> æŒ‡æ ‡é”® */
    private String wycMetricKey(String b) {
        if (b == null) return null;
        String s = b.replaceAll("\\s+", "");
        if (s.isEmpty()) return null;
        boolean city = s.contains("城市内");
        boolean turnover = s.contains("周转量");
        if (city) return turnover ? "cityTurnover" : "cityPax";
        return turnover ? "turnover" : "pax";
    }
    /** è½¨é“轮渡页(轨道=武汉/黄石;轮渡仅武汉)专用回填 */
@@ -192,15 +371,27 @@
        return -1;
    }
    /** å†™æ•°å€¼ï¼šæ ¼ä¸å­˜åœ¨åˆ™å»ºæ ¼å¹¶å¤åˆ¶åŒè¡Œ 1 æœˆåˆ—样式;返回 1 */
    /** å†™æ•°å€¼ï¼šæ ¼ä¸å­˜åœ¨åˆ™å»ºæ ¼ï¼›æ ¼å¸¦æ—¥æœŸæ ·å¼ï¼ˆå¦‚公交/出租 8 æœˆåˆ—继承了表头“yyyyå¹´m月”)时改回同行 1 æœˆåˆ—的数值样式,避免数值显示成日期 */
    private int setNumeric(Sheet sh, int r, int col, double val) {
        Row row = sh.getRow(r);
        if (row == null) row = sh.createRow(r);
        Cell c = row.getCell(col);
        if (c != null && c.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) {
            return 0; // æ¯ç‰ˆæœ¬æ¥å°±é å…¬å¼ç®—的行(合计/同页求和)保持公式,交 Excel é‡ç®—
        }
        Cell ref = row.getCell(FIRST_MONTH_COL);
        boolean needStyle = false;
        if (c == null) {
            c = row.createCell(col);
            Cell ref = row.getCell(FIRST_MONTH_COL);
            if (ref != null && ref.getCellStyle() != null) c.setCellStyle(ref.getCellStyle());
            needStyle = true;
        } else if (c.getCellType() == org.apache.poi.ss.usermodel.CellType.BLANK
                || (c.getCellType() == org.apache.poi.ss.usermodel.CellType.NUMERIC
                    && org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(c))) {
            // æ¯ç‰ˆæŠŠ 8 æœˆåˆ—的表头日期格式带到了数据格(或有格式无值),写值前改回同行 1 æœˆåˆ—的数字格式
            needStyle = true;
        }
        if (needStyle && ref != null && ref.getCellStyle() != null) {
            c.setCellStyle(ref.getCellStyle());
        }
        c.setCellValue(val);
        return 1;
@@ -229,20 +420,193 @@
        return row == null ? null : cityOf(row.getCell(0));
    }
    /** æœˆä»½å®¹é‡ï¼šåœ¨è¡¨å¤´ï¼ˆå‰ 5 è¡Œï¼‰æ‰¾â€œ2026…累计”列,容量=(累计列-1 æœˆåˆ—)/2 */
    /**
     * ä¸ºç¬¬ month æœˆè¡¥â€œå½“月值”列公式:取同一行最近一个月仍为公式的值列,整列右移过来
     * ï¼ˆå¦‚公交 7 æœˆåˆ— O çš„ =O9+O13+... -> 8 æœˆåˆ— Q çš„ =Q9+Q13+...)。
     * åªè¡¥ç©ºå•元格;已有数值(货运全省块/市州块由数据回填)或已有公式的格不动。
     */
    public int ensureMonthFormulas(XSSFWorkbook wb, int year, int month) {
        if (wb == null || month <= 1) return 0;
        int targetCol = FIRST_MONTH_COL + 2 * (month - 1);
        int written = 0;
        for (int i = 0; i < wb.getNumberOfSheets(); i++) {
            Sheet sh = wb.getSheetAt(i);
            if (sh == null || !isFilledLedgerSheet(sh.getSheetName())) continue;
            for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
                Row row = sh.getRow(r);
                if (row == null) continue;
                if (hasContent(row.getCell(targetCol))) continue;
                Cell srcCell = null;
                int srcMonth = -1;
                for (int s = month - 1; s >= 1; s--) {
                    Cell c = row.getCell(FIRST_MONTH_COL + 2 * (s - 1));
                    if (c != null && c.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) {
                        srcCell = c;
                        srcMonth = s;
                        break;
                    }
                }
                if (srcCell == null) continue;
                String f = srcCell.getCellFormula();
                if (f == null || f.isEmpty()) continue;
                Cell target = row.getCell(targetCol);
                if (target == null) target = row.createCell(targetCol);
                if (srcCell.getCellStyle() != null) target.setCellStyle(srcCell.getCellStyle());
                target.setCellFormula(shiftFormulaColumns(f, 2 * (month - srcMonth)));
                written++;
            }
        }
        return written;
    }
    /** è¯¥æ ¼æ˜¯å¦å·²æœ‰å†…容(数值/文本/公式);仅带样式的空壳(如母版里继承表头日期格式的格子)视为空 */
    private boolean hasContent(Cell c) {
        if (c == null) return false;
        switch (c.getCellType()) {
            case BLANK: return false;
            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" */
    private String cellRef(int col0, int row1) {
        StringBuilder sb = new StringBuilder();
        int n = col0 + 1;
        while (n > 0) {
            int rem = (n - 1) % 26;
            sb.insert(0, (char) ('A' + rem));
            n = (n - 1) / 26;
        }
        return sb.toString() + row1;
    }
    /** ä»Ž "2026-08" å–月份(解析失败返回 1) */
    private int periodMonth(String period) {
        if (period != null && period.length() >= 7) {
            try { return Integer.parseInt(period.substring(5, 7)); } catch (Exception ignore) { }
        }
        return 1;
    }
    /** ä»Ž "2026-08" å–年份 */
    private int periodYear(String period) {
        if (period != null && period.length() >= 4) {
            try { return Integer.parseInt(period.substring(0, 4)); } catch (Exception ignore) { }
        }
        return 0;
    }
    /**
     * ä¸ºç¬¬ m æœˆè¡¥â€œä¸ŽåŽ»å¹´åŒæ¯”â€å…¬å¼ï¼šæŠŠç¬¬ m-1 æœˆåŒæ¯”列的公式整体右移 2 åˆ—
     * ï¼ˆå¦‚公交 7 æœˆ P5 å…¬å¼ =O5/AH5-1 -> 8 æœˆ R5 =Q5/AJ5-1),
     * å› æ­¤åŒæ¯”分母取的就是汇总表 2025 å¹´åŒæœˆåˆ—(用户口径)。
     */
    public int ensureMonthYoyFormulas(XSSFWorkbook wb, int year, int month) {
        if (wb == null || month <= 1) return 0;
        int valueCol = FIRST_MONTH_COL + 2 * (month - 1);
        int yoyCol = valueCol + 1;
        int prevYoyCol = yoyCol - 2;
        int written = 0;
        for (int i = 0; i < wb.getNumberOfSheets(); i++) {
            Sheet sh = wb.getSheetAt(i);
            if (sh == null) continue;
            if (!isMonthLedgerSheet(sh.getSheetName())) continue;
            for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
                Row row = sh.getRow(r);
                if (row == null) continue;
                Cell prev = row.getCell(prevYoyCol);
                if (prev == null || prev.getCellType() != org.apache.poi.ss.usermodel.CellType.FORMULA) continue;
                Cell target = row.getCell(yoyCol);
                if (target != null && target.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) continue;
                String f = prev.getCellFormula();
                if (f == null || f.isEmpty()) continue;
                String shifted = shiftFormulaColumns(f, 2);
                if (target == null) {
                    target = row.createCell(yoyCol);
                    if (prev.getCellStyle() != null) target.setCellStyle(prev.getCellStyle());
                }
                try {
                    target.setCellFormula(shifted);
                    written++;
                } catch (Exception e) {
                    throw new IllegalStateException(String.format(
                            "同比公式补写失败:sheet=%s cell=%s åŽŸå¼=%s ç›®æ ‡å¼=%s",
                            sh.getSheetName(), cellRef(yoyCol, r + 1), f, shifted), e);
                }
            }
        }
        return written;
    }
    /** éšæœˆæ‰©åˆ—的长表页(表头在 1..3 è¡Œã€æœˆä»½ç»„为“当月值+同比”交替) */
    private boolean isMonthLedgerSheet(String name) {
        if (name == null) return false;
        String n = name.trim();
        return "货运".equals(n) || "公路总客运".equals(n) || "班线包车".equals(n) || "城市客运".equals(n)
                || "公交".equals(n) || "出租".equals(n) || "网约车".equals(n)
                || "轨道、轮渡".equals(n) || "轨道轮渡".equals(n)
                || "中口径明细".equals(n) || "中口径客运量".equals(n);
    }
    /** å…¬å¼å†…所有 A1 å½¢å¼çš„列引用右移 delta åˆ—(SUM ç­‰å‡½æ•°ååŽæ— æ•°å­—,不会被匹配) */
    private String shiftFormulaColumns(String formula, int delta) {
        java.util.regex.Matcher m = java.util.regex.Pattern
                .compile("(?<![A-Za-z0-9_$])([$]?)([A-Z]{1,3})([$]?)([0-9]+)")
                .matcher(formula);
        StringBuffer sb = new StringBuffer();
        while (m.find()) {
            String col = m.group(2);
            int idx = 0;
            for (int i = 0; i < col.length(); i++) idx = idx * 26 + (col.charAt(i) - 'A' + 1);
            int nidx = idx + delta;
            StringBuilder nc = new StringBuilder();
            while (nidx > 0) {
                int rem = (nidx - 1) % 26;
                nc.insert(0, (char) ('A' + rem));
                nidx = (nidx - 1) / 26;
            }
            m.appendReplacement(sb, java.util.regex.Matcher.quoteReplacement(m.group(1) + nc + m.group(3) + m.group(4)));
        }
        m.appendTail(sb);
        return sb.toString();
    }
    /** æœˆä»½å®¹é‡ï¼šä¼˜å…ˆå–表头(前 5 è¡Œï¼‰â€œ2026…累计”列反推;货运页无该文案,
     *  å†ç”¨è¡¨å¤´é‡Œæœ€åŽä¸€ä¸ªâ€œ2026å¹´M月”/日期格式的月列兜底(如货运 Q3=2026-08-01 -> 8) */
    private int monthColumnCapacity(Sheet sh) {
        int byCumulative = 0;
        int byMonthHeader = 0;
        for (int r = 0; r <= 4 && r <= sh.getLastRowNum(); r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            for (Cell c : row) {
                if (c == null) continue;
                int idx = c.getColumnIndex();
                if (idx <= FIRST_MONTH_COL || (idx - FIRST_MONTH_COL) % 2 != 0) continue; // åªçœ‹â€œå½“月值”列
                String t = text(c);
                if (t != null && t.contains("2026") && t.contains("累计") && c.getColumnIndex() > FIRST_MONTH_COL) {
                    return (c.getColumnIndex() - FIRST_MONTH_COL) / 2;
                if (byCumulative == 0 && t != null && t.contains("2026") && t.contains("累计")) {
                    byCumulative = (idx - FIRST_MONTH_COL) / 2;
                }
                if (t != null && t.contains("2026") && t.contains("月") && !t.contains("累计")
                        && !t.contains("同比")) {
                    byMonthHeader = Math.max(byMonthHeader, (idx - FIRST_MONTH_COL) / 2 + 1);
                } else if (c.getCellType() == org.apache.poi.ss.usermodel.CellType.NUMERIC
                        && org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(c)) {
                    byMonthHeader = Math.max(byMonthHeader, (idx - FIRST_MONTH_COL) / 2 + 1);
                }
            }
        }
        return 7;
        int cap = Math.max(byCumulative, byMonthHeader);
        return cap > 0 ? cap : 7;
    }
    private String text(Cell c) {
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/WycSplitCalc.java
@@ -3,7 +3,9 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.trafficaudit.common.util.RegionUtil;
import com.trafficaudit.dataimport.entity.CityTaxiMonthly;
import com.trafficaudit.dataimport.entity.WycOrderMonthly;
import com.trafficaudit.dataimport.mapper.CityTaxiMonthlyMapper;
import com.trafficaudit.dataimport.mapper.WycOrderMonthlyMapper;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.DataFormatter;
@@ -76,6 +78,8 @@
    @Resource
    private CityTaxiMonthlyMapper cityTaxiMapper;
    @Resource
    private WycOrderMonthlyMapper wycOrderMapper;
    @Value("${city-passenger.template-dir:docs/城市客运}")
    private String templateDir;
@@ -190,6 +194,8 @@
    /** è¯»å– docs/城市客运/《网约车订单及全省总量.xlsx》:按 period(yyyy-MM) è¡Œå– pin(B-E) ä¸Ž 17 å¸‚州订单(G-W) */
    private Map<String, Object> loadMonthlyInput(String period) throws Exception {
        Map<String, Object> db = loadMonthlyInputFromDb(period);
        if (db != null) return db;
        File f = findInput();
        try (InputStream in = new FileInputStream(f);
             Workbook wb = WorkbookFactory.create(in)) {
@@ -226,6 +232,38 @@
        }
    }
    /** ä¼˜å…ˆä»Žåº“取数(wyc_order_monthly);库里无该期或订单全空时返回 null,回退读母版文件 */
    private Map<String, Object> loadMonthlyInputFromDb(String period) {
        WycOrderMonthly row = wycOrderMapper.selectOne(new LambdaQueryWrapper<WycOrderMonthly>()
                .eq(WycOrderMonthly::getReportPeriod, period).last("LIMIT 1"));
        if (row == null) return null;
        List<String> cities = RegionUtil.cityList();
        Double[] vals = {
                row.getOrderWuhan(), row.getOrderHuangshi(), row.getOrderShiyan(), row.getOrderYichang(),
                row.getOrderXiangyang(), row.getOrderEzhou(), row.getOrderJingmen(), row.getOrderXiaogan(),
                row.getOrderJingzhou(), row.getOrderHuanggang(), row.getOrderXianning(), row.getOrderSuizhou(),
                row.getOrderEnshi(), row.getOrderXiantao(), row.getOrderQianjiang(), row.getOrderTianmen(),
                row.getOrderShennong()
        };
        double[] orders = new double[cities.size()];
        double sum = 0.0;
        for (int i = 0; i < orders.length && i < vals.length; i++) {
            orders[i] = vals[i] == null ? 0.0 : vals[i];
            sum += orders[i];
        }
        if (sum <= 0) return null;
        double orderSum = sum;
        if (row.getOrderSum() != null && row.getOrderSum() > 0) orderSum = row.getOrderSum();
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("pinK", row.getPinK() == null ? 0.0 : row.getPinK());
        map.put("pinCK", row.getPinCK() == null ? 0.0 : row.getPinCK());
        map.put("pinZ", row.getPinZ() == null ? 0.0 : row.getPinZ());
        map.put("pinCZ", row.getPinCZ() == null ? 0.0 : row.getPinCZ());
        map.put("orders", orders);
        map.put("orderSum", orderSum);
        map.put("source", "db");
        return map;
    }
    private File findInput() throws Exception {
        String rel = templateDir;
        while (rel.startsWith("./")) rel = rel.substring(2);
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -3347,8 +3347,14 @@
            cleanSummarySheetPresentation(wb); // å–消各页筛选、还原隐藏行(否则“班线包车”等页看起来像缺数据)
            int effMonth = (toMonth != null && toMonth > 0 && toMonth < month) ? toMonth : month;
            List<String> dbProblems = new ArrayList<>();
            int dbFilled = summaryWorkbookFiller.fillMonthlyLedger(wb, year, effMonth, dbProblems);
            log.info("汇总工作簿数据驱动回填完成:写入台账页当月值格数={},缺数提示={}", dbFilled, dbProblems);
            String effPeriod = String.format("%04d-%02d", year, effMonth);
            int dbFilled = summaryWorkbookFiller.fillMonthlyLedger(wb, effPeriod, year, effMonth, dbProblems);
            int yoyFilled = summaryWorkbookFiller.ensureMonthYoyFormulas(wb, year, effMonth);
            int monthFormulaFilled = summaryWorkbookFiller.ensureMonthFormulas(wb, year, effMonth);
            int widthApplied = applySummaryReferenceLayout(wb);
            log.info("汇总工作簿数据驱动回填完成:写入当月值格数={},补写当月同比公式格数={},补写当月合计公式格数={},"
                    + "按《生成_道路运输量汇总表》对齐列宽列数={},缺数提示={}",
                    dbFilled, yoyFilled, monthFormulaFilled, widthApplied, dbProblems);
            applyTwoDecimalFormat(wb); // æ•°å€¼æ˜¾ç¤ºä¸¤ä½å°æ•°ï¼ˆä¿ç•™å…¨ç²¾åº¦ï¼‰ï¼›% / æ—¥æœŸç­‰æ—¢æœ‰æ ·å¼ä¸æ”¹å˜
            if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr();
            wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // è¿˜åŽŸ/改写公式后打开即重算
@@ -3358,6 +3364,100 @@
        }
    }
    /**
     * ç‰ˆå¼å¯¹é½ï¼šæŒ‰ docs/生成汇总大表/《生成_道路运输量汇总表.xlsx》(用户提供的版式参照)逐列套用列宽与隐藏标记,
     * ä½¿ç”Ÿæˆä»¶ä¸Žå‚照件“格式完全一致”,并让母版没定义宽度的“后续扩列”(如 8 æœˆåˆ—)也有与 8 æœˆä¸€è‡´çš„列宽。
     * å‚照件缺失时静默跳过(返回 0),不影响原有导出。
     */
    private int applySummaryReferenceLayout(XSSFWorkbook wb) {
        File ref;
        try {
            ref = resolveSummaryLayoutRef();
        } catch (Exception e) {
            return 0;
        }
        if (ref == null) return 0;
        double savedZipRatio = ZipSecureFile.getMinInflateRatio();
        ZipSecureFile.setMinInflateRatio(0.0001);
        int applied = 0;
        try (InputStream in = new FileInputStream(ref); XSSFWorkbook rwb = new XSSFWorkbook(in)) {
            for (int i = 0; i < rwb.getNumberOfSheets(); i++) {
                org.apache.poi.xssf.usermodel.XSSFSheet rs = rwb.getSheetAt(i);
                if (rs == null) continue;
                org.apache.poi.xssf.usermodel.XSSFSheet ts = wb.getSheet(rs.getSheetName());
                if (ts == null) continue;
                int maxCol = Math.min(usedColumnCount(rs), usedColumnCount(ts) + 4);
                if (maxCol <= 0) continue;
                double[] width = new double[maxCol];
                boolean[] hasWidth = new boolean[maxCol];
                boolean[] hidden = new boolean[maxCol];
                for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCols cols : rs.getCTWorksheet().getColsArray()) {
                    for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCol col : cols.getColArray()) {
                        long min = col.getMin();
                        long max = col.getMax();
                        for (long c = min; c <= max; c++) {
                            int idx = (int) (c - 1);
                            if (idx < 0 || idx >= maxCol) {
                                if (idx >= maxCol) break;
                                continue;
                            }
                            if (col.isSetWidth()) {
                                width[idx] = col.getWidth();
                                hasWidth[idx] = true;
                            }
                            hidden[idx] = col.isSetHidden() && col.getHidden();
                        }
                    }
                }
                for (int c = 0; c < maxCol; c++) {
                    if (hasWidth[c]) {
                        int w = (int) Math.round(width[c] * 256d);
                        if (ts.getColumnWidth(c) != w) {
                            ts.setColumnWidth(c, w);
                            applied++;
                        }
                    }
                    if (hidden[c] != ts.isColumnHidden(c)) ts.setColumnHidden(c, hidden[c]);
                }
            }
            log.info("汇总工作簿版式对齐《生成_道路运输量汇总表》完成:套用列宽 {} åˆ—(参照件:{})", applied, ref.getName());
        } catch (Exception e) {
            log.warn("汇总工作簿版式对齐失败(忽略,继续按母版版式输出):{}", e.getMessage());
            return 0;
        } finally {
            ZipSecureFile.setMinInflateRatio(savedZipRatio);
        }
        return applied;
    }
    /** ä¸€é¡µå·²ç”¨åˆ°çš„æœ€å¤§åˆ—数(0 åŸºè®¡æ•°ï¼‰ */
    private int usedColumnCount(org.apache.poi.ss.usermodel.Sheet sh) {
        int max = 0;
        for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
            org.apache.poi.ss.usermodel.Row row = sh.getRow(r);
            if (row == null) continue;
            if (row.getLastCellNum() > max) max = row.getLastCellNum();
        }
        return max;
    }
    /** å®šä½ç‰ˆå¼å‚ç…§ä»¶ docs/生成汇总大表/生成_道路运输量汇总表.xlsx(优先级同母版:配置目录 â†’ user.dir ç›¸å¯¹ â†’ ä¸Šçº§ï¼‰ */
    private File resolveSummaryLayoutRef() {
        String rel = summaryTemplateDir;
        while (rel != null && rel.startsWith("./")) rel = rel.substring(2);
        String[] roots = {
            summaryTemplateDir,
            System.getProperty("user.dir") + "/" + rel,
            System.getProperty("user.dir") + "/../" + rel
        };
        for (String root : roots) {
            if (root == null || root.trim().isEmpty()) continue;
            File f = new File(root, "生成_道路运输量汇总表.xlsx");
            if (f.isFile()) return f;
        }
        return null;
    }
    /** æ±‡æ€»è¡¨æ¯ç‰ˆå®šä½ï¼šä»…精确匹配 {year}å¹´{month}月…(P0-P1 éª¨æž¶æœŸä¸å…è®¸è·¨æœˆ/跨年静默回退,
     *  ç›®æ ‡æœŸæ— å¯¹åº”母版时明确报错,月份扩展规则待 P0 æ¯ç‰ˆå®šç¨¿åŽå†æ”¾å¼€ï¼‰ */
    private File resolveSummaryMother(String period, String mode) throws Exception {
traffic-audit-web/src/views/DataImport.vue
@@ -1,4 +1,3 @@
SKIP/CHECK (0): TYPE_LABELS upper <template>
  <el-container direction="vertical">
    <app-header title="交通统计报表审核系统 - æ•°æ®å¯¼å…¥"></app-header>
    <el-container>
@@ -54,7 +53,7 @@
                <div v-else-if="moduleSel[g.key] && !isDirOnlyType(moduleSel[g.key])" class="module-file">
                  <div class="file-name" :title="fileNamesText(moduleSel[g.key])">{{ fileNamesText(moduleSel[g.key]) || '未选择文件' }}</div>
                  <div class="file-btns">
                    <el-upload action="#" :auto-upload="false" :show-file-list="false" multiple accept=".xlsx,.xls,.et"
                    <el-upload action="#" :auto-upload="false" :show-file-list="false" multiple :accept="acceptOf(moduleSel[g.key])"
                               :ref="'up' + g.key" :on-change="f => onFilePicked(g.key, f)" style="display:inline-block">
                      <el-button size="mini" type="primary" plain icon="el-icon-folder-opened" :disabled="importing">选择文件</el-button>
                    </el-upload>
@@ -165,11 +164,11 @@
  investCityPassenger: '市州单表(客运站场)', investCityLogistics: '市州单表(物流园区)',
  'INVEST_客运站场_CITY': '市州单表(客运站场)', 'INVEST_物流园区_CITY': '市州单表(物流园区)',
  cityBus: '城市公交月报', cityTaxi: '巡游出租月报', cityTaxiAuth: '出租车运政车辆',
  wycTotal: '网约车总数', wycOrder: '网约车订单及全省总量',
  wycTotal: '网约车总数', wycOrder: '网约车订单及全省总量', wycOrderReport: '网约车订单报告',
  H2032: 'H203-2月报', H2031: 'H203-1旅客月报', H204: 'H204能耗季报', PASSENGER_AUTH: '旅客运政车辆数',
  PASSENGER_INDIVIDUAL: '个体客运量/周转量', ENERGY_AUTH: '能耗车辆运政信息', TRANSPORT_AUTH: '运政车辆',
  TRACK_MILEAGE: '轨迹里程', SCALE_SPLIT: '规上规下拆分', FREIGHT_TURNOVER: '货运量周转量',
  INVEST_SYSTEM: '投资系统导出', CITY_BUS: '城市公交月报', CITY_TAXI: '巡游出租月报', CITY_TAXI_AUTH: '出租车运政车辆', WYC_TOTAL: '网约车总数', WYC_ORDER: '网约车订单及全省总量'
  INVEST_SYSTEM: '投资系统导出', CITY_BUS: '城市公交月报', CITY_TAXI: '巡游出租月报', CITY_TAXI_AUTH: '出租车运政车辆', WYC_TOTAL: '网约车总数', WYC_ORDER: '网约车订单及全省总量', WYC_ORDER_REPORT: '网约车订单报告'
}
const BATCH_TYPE_MAP = {
  H2032: 'h2032', TRANSPORT_AUTH: 'transportAuth', TRACK_MILEAGE: 'trackMileage',
@@ -177,7 +176,7 @@
  H2031: 'passengerMonthly', PASSENGER_AUTH: 'passengerAuth', PASSENGER_INDIVIDUAL: 'passengerIndividual',
  H204: 'energyMonthly', ENERGY_AUTH: 'energyAuth',
  CITY_BUS: 'cityBus', CITY_TAXI: 'cityTaxi', CITY_TAXI_AUTH: 'cityTaxiAuth',
  WYC_TOTAL: 'wycTotal', WYC_ORDER: 'wycOrder',
  WYC_TOTAL: 'wycTotal', WYC_ORDER: 'wycOrder', WYC_ORDER_REPORT: 'wycOrderReport',
  INVEST_SYSTEM: 'investmentSystem'
}
export default {
@@ -226,7 +225,8 @@
            { value: 'cityTaxi', label: '巡游出租汽车运营服务情况月报' },
            { value: 'cityTaxiAuth', label: '出租车运政车辆' },
            { value: 'wycTotal', label: '网约车总数' },
            { value: 'wycOrder', label: '网约车订单及全省总量' }
            { value: 'wycOrder', label: '网约车订单及全省总量' },
            { value: 'wycOrderReport', label: '网约车订单报告' }
          ]
        },
        {
@@ -299,7 +299,8 @@
        cityTaxi: '导入_巡游出租汽车运营服务情况月报(YYYYå¹´M月).xls(市州级17行)',
        cityTaxiAuth: '导入模板_出租车信息(运政车辆信息).xlsx(车辆级明细,用于运营车数对比)',
        wycTotal: '模板_网约车总数.xlsx(A列=月份,B-E=全省客运量/城市内/周转量/城市内周转量)',
        wycOrder: '模板_网约车订单及全省总量.xlsx(A列=月份,B-E=全省pin,F=订单合计,G-W=17市州订单)'
        wycOrder: '模板_网约车订单及全省总量.xlsx(A列=月份,B-E=全省pin,F=订单合计,G-W=17市州订单)',
        wycOrderReport: '模板_网约车订单.docx(《网约车行业YYYYå¹´M月运行监测情况分析报告》,自动解析附件表「地市名称…订单数总数」的 17 å¸‚州订单)'
      }
      return map[this.importType] || ''
    },
@@ -320,6 +321,7 @@
        cityTaxiAuth: '城市客运/导入模板_出租车信息(运政车辆信息).xlsx',
        wycTotal: '城市客运/模板_网约车总数.xlsx',
        wycOrder: '城市客运/模板_网约车订单及全省总量.xlsx',
        wycOrderReport: '城市客运/模板_网约车订单.docx',
        investment: '投资/输入/客运(客运站)/更新_7月客运站投资明细表(市州汇总大表,给领导).xlsx',
        investmentLogistics: '投资/输入/物流(物流园区)/更新_7月物流站场投资明细.xls',
        investmentSystem: '投资/模板_查询结果(投资系统-客运+物流 2026.7).xls'
@@ -359,7 +361,8 @@
        cityTaxi: '/import/cityTaxi',
        cityTaxiAuth: '/import/cityTaxiAuth',
        wycTotal: '/import/wycTotal',
        wycOrder: '/import/wycOrder'
        wycOrder: '/import/wycOrder',
        wycOrderReport: '/import/wycOrderReport'
      }
      return map[t] || ''
    },
@@ -464,6 +467,7 @@
          this.$message.error((e.response && e.response.data && e.response.data.message) || '批量导入失败')
        })
    },
    acceptOf(type) { return type === 'wycOrderReport' ? '.docx,.doc' : '.xlsx,.xls,.et' },
    isCityDirType(t) { return t === 'investCityPassenger' || t === 'investCityLogistics' },
    isDirOnlyType(t) { return t === 'investCityPassenger' || t === 'investCityLogistics' },
    importCityDir(type) {
@@ -550,6 +554,7 @@
              map[key] = { totalRows: r.totalRows, successRows: r.successRows, failRows: r.failRows, latestId: r.id }
            }
          })
          if (map.wycOrderReport && !map.wycOrder) map.wycOrder = map.wycOrderReport
          this.overview = map
        })
        .catch(e => { if (!e || !e._toast) this.$message.error('导入记录加载失败,请刷新重试') })