10个文件已修改
13个文件已添加
1871 ■■■■■ 已修改文件
docs/功能测试报告/功能测试报告_全精度存储与两位小数显示_2026-09-20.md 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_城市客运月度与累计同比_2026-09-20.md 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_市州统一填报模板与县市区解析_2026-09-20.md 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_投资明细表就绪与预览类型补齐_2026-09-20.md 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_投资预安排汇总补计与导入弹窗_2026-09-20.md 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_汇总大表异步导出与缓存_2026-09-20.md 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/汇总大表异步导出进度弹窗_2026-09-20.png 补丁 | 查看 | 原始文档 | blame | 历史
docs/工作日结/工作日结_2026-09-20.md 195 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/投资/填报口径与统一模板_2026-09-20.md 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/投资/模板/模板_市州月报_客运站场.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
docs/投资/模板/模板_市州月报_物流园区.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
docs/投资/输入/客运(客运站)/7月/十堰市2026年7月固定资产月报电子版投资表格.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
docs/投资/输入/物流(物流园区)/7月/湖北省十堰交通物流基础设施投资统计汇总表(月报)(2026年7月).xls 补丁 | 查看 | 原始文档 | blame | 历史
docs/投资/输出/清理残留_十堰汇总表带出的非十堰项目_2026-09-20.tsv 81 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/生成汇总大表/2026年8月道路运输量汇总表.xlsx 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java 246 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/WycSplitCalc.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/controller/ReportExportController.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java 247 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportTaskService.java 234 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/DataImport.vue 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/ReportExport.vue 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_È«¾«¶È´æ´¢ÓëÁ½Î»Ð¡ÊýÏÔʾ_2026-09-20.md
New file
@@ -0,0 +1,86 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_全精度存储与两位小数显示_2026-09-20
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
**需求(用户 2026-09-20 æŒ‡ä»¤ï¼‰**:所有的数据都全部全精度保留,计算得到的结果显示两位小数。
**改前问题**:
1. åŽç«¯åœ¨**写单元格之前**就做了四舍五入,底层存储值被提前截断:
   - `round(v, 4)`:货运规上/规下/同比、累计与当月、中口径排名/分析、`÷10000` å•位换算、投资进度、物流占比、投资计划万吨;
   - `round(v, 2)`:通用数据格 `setDataCell`、平均运距兜底、客运转运写入;
   - `round(v, 1)`:排名页占比(`setFreightShare`、货运量排名/周转量排名块)。
   è¯¯å·®è™½å°ï¼Œä½†å±žäºŽã€Œè®¡ç®—口径被写死」,与人工用全精度重算的结果天然对不上(正是 09-19 ä¸­å£å¾„差异问题的同源风险)。
2. æ¯ç‰ˆä¸ªåˆ«æ ¼è‡ªå¸¦ 3~4 ä½å°æ•°**显示格式**(`0.0000_);[Red]\(0.0000\)`、`0.000_ `),展示位数不统一。
**改后实现**:
1. **全精度**:取消所有写入/运算前的预舍入,数值以 `double` åŽŸå€¼å‚ä¸Žè¿ç®—å¹¶å†™å…¥ï¼›`/10000`、占比、同比、平均运距兜底等一律不截断。
2. **两位小数展示**:由单元格数字格式承担。重写 `applyTwoDecimalFormat()`:
   - æ•°å€¼æ ¼æ ¼å¼ç»Ÿä¸€ä¸º `0.00`(`cloneStyleFrom` ä¿ç•™è¾¹æ¡†/字体/对齐,只换数字格式,`;[Red]\(...\)` ç­‰å‰åŽç¼€åŽŸæ ·ä¿ç•™ï¼‰ï¼›
   - **3 ä½åŠä»¥ä¸Šå°æ•°çš„æ ¼å¼**(`0.000_ `、`0.0000_);[Red]\(0.0000\)`)用正则 `\.0{3,}` æ”¶æ•›ä¸º `.00`;
   - ç™¾åˆ†æ¯”(`%`)、科学计数(`E`)、文本(`@`)、日期格式**不动**;
   - æ•´æ•°å€¼æ ¼æ²¿ç”¨æ—¢æœ‰çº¦å®šä¸æ˜¾ç¤º `.00`(如年份、排名)。
3. æŽ§åˆ¶å°/接口返回的**预览摘要**里的 `round`(`addSum(...)`)保留,它不进报表数据。
**改动文件**:
| æ–‡ä»¶ | æ”¹åЍ |
| --- | --- |
| `traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java` | å޻除 40 å¤„预舍入;重写 `applyTwoDecimalFormat()`(新增 `DEC_MORE_THAN_TWO` æ­£åˆ™å¸¸é‡ï¼‰ |
| `traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/WycSplitCalc.java` | å¹³å‡è¿è·å…œåº•改全精度;删除已无引用的私有 `round(double,int)` |
## äºŒã€æµ‹è¯•环境
| é¡¹ | å†…容 |
| --- | --- |
| åŽç«¯ | Spring Boot `localhost:8090`,`mvn -q -o compile` é€šè¿‡åŽç”± `start-dev.ps1` é‡å¯ï¼ˆPID 15568) |
| å‰ç«¯ | Vue `localhost:8080`(PID 28608),与后端一并重启 |
| æ•°æ®åº“ | MySQL `localhost:3308/traffic_audit` |
| æŠ¥è¡¨æœŸ | 2026-08(1-8 æœˆç´¯è®¡å— + 8 æœˆå•月块) |
| å¯¹ç…§ä»¶ | æ”¹å‰ç”Ÿæˆä»¶ `_after_busfix_2026-08.xlsx`(09-19 å£å¾„修复后、本次改造前) |
| æ”¹åŽç”Ÿæˆä»¶ | `_after_precision_2026-08.xlsx`、`_after_precision_midrank/midana/middetail.xlsx`,以及 `_out_precision\` ä¸‹ 22 ä¸ªæŽ¥å£äº§ç‰© |
| å–证脚本 | `_qa_prec_*.py`、`_regen_precision.ps1`、`_regen_all.ps1` |
## ä¸‰ã€ç”¨ä¾‹ä¸Žç»“æžœ
| ç¼–号 | ç±»åž‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- | --- |
| T1 | æ­£å‘ | `mvn -q -o compile` | ç¼–译通过 | exit=0,无告警 | é€šè¿‡ |
| T2 | æ­£å‘ | 22 ä¸ªå¯¼å‡ºæŽ¥å£ï¼ˆperiod=2026-08) | å…¨éƒ¨ 200 ä¸”非空 | 21 ä¸ª OK;`wycSplit` 500(缺输入文件,见「四-5」) | é€šè¿‡ |
| T3 | æ­£å‘ | æ”¹å‰/改后《道路运输量汇总表》逐格数值对比(66205 ä¸ªå…±æœ‰æ•°å€¼æ ¼ï¼‰ | ä»…目标格出现差异 | å®žé™…变化 **249 æ ¼**,全部落在「货运量排名」107 +「中口径排名」142 | é€šè¿‡ |
| T4 | æ­£å‘ | æœ€å¤§å˜åŒ–量 | åªåº”是去掉舍入带来的尾数差 | **0.00022 ä¸‡äººå…¬é‡Œ**(=2 äººå…¬é‡Œï¼‰ï¼Œä½ç½®ã€Œä¸­å£å¾„排名」全省行 O3 | é€šè¿‡ |
| T5 | æ­£å‘ | æ­¦æ±‰ç´¯è®¡å‘¨è½¬é‡ | å…¨ç²¾åº¦ä¿ç•™ï¼Œ4 ä½å°æ•°ä½ä»ä¸º 8912 | ä¸­å£å¾„明细 S104 = 152330.89119999998;排名页 O4 / é“路运输周转量 Q5 = 152330.8912 | é€šè¿‡ |
| T6 | æ­£å‘ | å­æ„Ÿç´¯è®¡å‘¨è½¬é‡ | ä¸å†æˆªæ–­åˆ° 64429.9445 | 64429.944543113306(排名页与明细页逐位一致) | é€šè¿‡ |
| T7 | æ­£å‘ | ã€Œä¸­å£å¾„排名」改前/改后按两位小数对比 | å®Œå…¨ä¸€è‡´ï¼ˆä¸åº”改变可见结果) | ä¸ä¸€è‡´æ ¼ **0** | é€šè¿‡ |
| T8 | æ­£å‘ | å…¨çœè¡Œ `=SUM(O4:O20)` é—­åˆ | ç¼“存值 = 17 å¸‚州全精度求和 | 1227147.4890200002,**å·® 0.0**(09-19 çš„ 0.0002 æ®‹ç•™å·²æ¶ˆé™¤ï¼‰ | é€šè¿‡ |
| T9 | æ­£å‘ | æ±‡æ€»å¤§è¡¨ã€Œæœ‰å€¼æ•°å€¼/公式格」的 3 ä½ä»¥ä¸Šå°æ•°æ ¼å¼ | 0 æ ¼ | æ”¹å‰ 108 æ ¼ â†’ æ”¹åŽ **0 æ ¼** | é€šè¿‡ |
| T10 | æ­£å‘ | ä¸­å£å¾„明细(独立件)`0.000_ ` æ ¼å¼æ ¼ | æ”¶æ•›ä¸ºä¸¤ä½ | æ”¹å‰ 134 æ ¼ â†’ æ”¹åŽ **0 æ ¼**;C19 å€¼ 13.599765541547498 ä¸å˜ï¼Œæ ¼å¼å˜ `0.00_ ` | é€šè¿‡ |
| T11 | æ­£å‘ | 22 ä¸ªäº§ç‰©çš„ 3 ä½ä»¥ä¸Šå°æ•°æ ¼å¼æ±‡æ€» | å…¨ä¸º 0 | 15 ä¸ªå¯è§£æžäº§ç‰©åˆè®¡ **0 æ ¼** | é€šè¿‡ |
| T12 | å›žå½’ | è¡¨å†…错误值(`#DIV/0!`/`#VALUE!`/`#REF!`)数量 | ä¸æ–°å¢ž | æ”¹å‰ 589 = æ”¹åŽ 589(分布逐项相同) | é€šè¿‡ |
| T13 | å›žå½’ | ç™¾åˆ†æ¯”æ ¼ / ä¸¤ä½å°æ•°æ ¼æ•°é‡ | ä¸å—影响 | 25665 / 39552,与改前一致 | é€šè¿‡ |
| T14 | å›žå½’ | ç‹¬ç«‹ä»¶ `passengerMidRank`、`passengerMidAnalysis` | æ—  3 ä½ä»¥ä¸Šå°æ•°æ ¼å¼ | 0 æ ¼ï¼ˆæ”¹å‰æœ¬å°± 0) | é€šè¿‡ |
| T15 | è¾¹ç•Œ | ç©ºå€¼æ ¼ã€æ–‡æœ¬æ ¼ï¼ˆå¦‚月份文本「2023å¹´4月」) | ä¸åЍ | ä»å¸¦ 4 ä½å°æ•°æ ¼å¼ 274 æ ¼ï¼ˆ260 ç©ºå€¼ + 14 æ–‡æœ¬ï¼‰ï¼Œæ— æ˜¾ç¤ºå€¼ï¼Œä¸å¤„理 | é€šè¿‡ |
| T16 | è¾¹ç•Œ | æ•´æ•°å€¼æ ¼ï¼ˆå¹´ä»½/排名) | ä¸æ˜¾ç¤º `.00` | æ²¿ç”¨æ—¢æœ‰ã€Œæ•´æ•°ä¸æ˜¾ç¤º .00」保护,未被本次改动破坏 | é€šè¿‡ |
| T17 | è¾¹ç•Œ | ä¸Ž `round(改后,4)` ä¸ä¸€è‡´çš„ 30 æ ¼ | å±žäºŒé˜¶é‡ç®—而非错误 | åŒæ¯”列由全精度分子分母重算,量级 1e-9~1e-8(如 0.0712378390 â†’ 0.0712378363) | é€šè¿‡ |
| T18 | è´Ÿå‘ | æ¯ç‰ˆæŽ’名页 Q åˆ—公式 `=ROUND(B/L*10,1)` | æœ¬æ¬¡ä¸æ”¹æ¯ç‰ˆ | ä»ä¿ç•™ï¼ˆ36×2 æ ¼ï¼‰ï¼ŒExcel æ‰“开会按公式重算到 1 ä½åŽå†æŒ‰ `0.00` å±•示 | è§ã€Œå››-4」待拍板 |
## å››ã€å‘现的问题与处置
1. **母版带 4 ä½å°æ•°æ ¼å¼çš„æœ‰å€¼æ ¼ 108 ä¸ª**(`货运!EO90/EO93/EO108/EO111` ç­‰ SUM å…¬å¼æ ¼ã€`公路总客运!DE2` ç­‰ï¼‰â†’ å·²ç”±æ ¼å¼å½’一处理为 `0.00`;剩余带该格式的 274 æ ¼å‡ä¸ºç©ºå€¼/文本格,无显示值。
2. **「整数不显示 .00」保护先把公式格跳过了**:首轮改造后 `货运!EO90/EO93/EO108/EO111` ä»æ˜¯ `0.0000`(缓存值为 0,被判定为整数而跳过)。已把「3 ä½ä»¥ä¸Šå°æ•°æ ¼å¼å½’一」的判断**提到整数保护之前**——这类格本来就走数值格式展示,与「General çš„æ•´æ•°å€¼ä¸æ˜¾ç¤º .00」不冲突。
3. **3 ä½å°æ•°æ ¼å¼æœªè¢«è¯†åˆ«**(`0.000_ `):原判断用 `f.contains("0.00")` è®¤ä¸ºå·²æ˜¯ä¸¤ä½ï¼Œè€Œ `0.000_ ` æ°å¥½åŒ…含子串 `0.00` è€Œè¢«è·³è¿‡ï¼Œå¯¼è‡´ä¸­å£å¾„明细 134 æ ¼æ¼æ”¹ã€‚已改为正则 `\.0{3,}` ç»Ÿä¸€æ”¶æ•›ã€‚
4. **母版排名页 Q åˆ—仍是 `=ROUND(B/L*10,1)`**(货运量排名 Q4:Q42、货运周转量排名 Q5:Q44,共 72 æ ¼ï¼‰ï¼š
   ç¨‹åºå¯¹å…¬å¼æ ¼ä¸€å¾‹ä¸è¦†å†™ï¼ˆä¿ç•™æ¯ç‰ˆå…¬å¼è®¾è®¡ï¼‰ï¼ŒExcel/WPS æ‰“开时会按公式重算回 1 ä½å°æ•°å†ä»¥ `0.00` æ ¼å¼å±•示。
   è‹¥è¦è¿žè¿™ 72 æ ¼ä¹Ÿå…¨ç²¾åº¦ï¼Œéœ€è¦æŠŠä¸‰ä¸ªæ¯ç‰ˆçš„公式改成 `=B/L*10`(并同步 R åˆ— `=10-Q`)——**待用户确认后再动母版**。
5. **`wycSplit` æŽ¥å£ 500**:`loadWycMonthlyInput()` æŠ›ã€Œæœªæ‰¾åˆ°ç½‘络约车(全省)汇总.xlsx,请放到 docs/网络客运 ç›®å½•」,而 `docs\网络客运` ç›®å½•当前不存在——属**缺输入数据的既有现象**,与本次改造无关。
## äº”、结论
- æŠ¥è¡¨æ•°å€¼å·²å…¨éƒ¨æ”¹ä¸º**全精度写入/运算**,展示位数统一由单元格格式承担(两位小数;百分比、日期、文本格式不变)。
- é€æ ¼å¯¹æ¯”证实:改后与改前的可见结果(两位小数口径)**完全一致**,差异仅出现在被去掉舍入的尾数位,最大 0.00022 ä¸‡äººå…¬é‡Œã€‚
- é¡ºå¸¦æ¶ˆé™¤äº† 09-19 é—留的「全省行 0.0002 ä¸‡äººå…¬é‡Œã€ï¼šå„市州改为全精度后,母版公式 `=SUM(O4:O20)` ä¸Žæ˜Žç»†å…¨ç²¾åº¦æ±‚å’Œ**逐位闭合**。
- 22 ä¸ªå¯¼å‡ºæŽ¥å£å›žå½’通过(`wycSplit` ä¸ºæ—¢æœ‰ç¼ºæ•°æ®é—®é¢˜ï¼‰ï¼›é”™è¯¯å€¼æ•°é‡ã€ç™¾åˆ†æ¯”格数量均无变化。
- é—留一项待拍板:母版排名页 Q/R å æ¯”公式的 `ROUND(...,1)`。
测试人:Codex(AI) 测试日期:2026-09-20
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_³ÇÊпÍÔËÔ¶ÈÓëÀÛ¼ÆÍ¬±È_2026-09-20.md
New file
@@ -0,0 +1,69 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_城市客运月度与累计同比_2026-09-20
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
用户上报:汇总大表《城市客运》页 8 æœˆåŒæ¯”、累计同比明显不对(全省客运量 8 æœˆåŒæ¯” âˆ’86.5%、累计同比 âˆ’43.5%),
且「2025年1-7月累计」这一辅助列明明已在母版删除却仍出现在生成表里。
本次修复三件事:
1. **母版结构归一**:把《2026å¹´8月道路运输量汇总表.xlsx》城市客运页多出的 AJ åˆ—(`2025å¹´1-7月累计` è¾…助列)真正删除,
   ä½¿ 2025 å„月列恢复等距(2025å¹´8月回到 AJ),与版式参照件《生成_道路运输量汇总表.xlsx》一致。
2. **程序侧按表头定位月份列**:`SummaryWorkbookFiller.ensureMonthYoyFormulas()` ä¸å†æ— è„‘「公式整体右移 2 åˆ—」,
   æ”¹ä¸ºæŒ‰è¡¨å¤´ (å¹´,月) æŠŠå…¬å¼é‡Œçš„æœˆä»½åˆ—引用顺延一个月、其余引用右移 2 åˆ—;并新增
   `repairCumulativeYoyFormulas()` å…œåº•重写坏掉的累计同比分母(分母=去年 1..当月各月值列之和)。
3. **报表生成页预览补齐全指标同比**:`ReportExport.vue` çš„预览行定义只映射了周转量两项同比,补上货运量/城市客运/中口径共 8 é¡¹ï¼Œ
   å¹¶ä¿®æ­£ã€Œå½“月货物周转量(万吨)」单位笔误。
## äºŒã€æµ‹è¯•环境
- åŽç«¯ Spring Boot 8090(`Started TrafficAuditApplication`)、前端 Vue 8080(`Compiled successfully`)
- æ•°æ®åº“ MySQL localhost:3308/traffic_audit
- æŠ¥è¡¨æœŸï¼š2026-08(月报)
- æ¯ç‰ˆï¼š`docs/生成汇总大表/2026å¹´8月道路运输量汇总表.xlsx`(Excel 16.0 COM åˆ åˆ—后保存)
- æ¯ç‰ˆå¤‡ä»½ï¼š`docs/生成汇总大表/_bak_202608母版删除AJ列前_20260920.xlsx`
- å¯¹æ¯”基准件:`_before_citypaxfix_2026-08.xlsx`(改动前 11:37 ç‰ˆï¼‰ã€`_after_ajdel_2026-08.xlsx`(本次修复后)
## ä¸‰ã€ç”¨ä¾‹è¡¨
| ç¼–号 | ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- | --- |
| T01 | 8 æœˆåŒæ¯”分子分母 | å¯¼å‡º 2026-08 æ±‡æ€»å¤§è¡¨ï¼Œè¯» åŸŽå¸‚客运!R4 å…¬å¼ | `=Q4/AJ4-1`(Q=2026å¹´8月、AJ=2025å¹´8月) | `=Q4/AJ4-1` | é€šè¿‡ |
| T02 | å…¨çœå®¢è¿é‡ 8 æœˆåŒæ¯”缓存值 | åŒä¸Šï¼Œè¯»ç¼“å­˜ | çº¦ +4.44%(32215.74 / 30846.31 âˆ’ 1) | +4.4395% | é€šè¿‡ |
| T03 | ç´¯è®¡åŒæ¯”分母 | è¯» åŸŽå¸‚客运!T4 å…¬å¼ | `=S4/(V4+X4+Z4+AB4+AD4+AF4+AH4+AJ4)-1` | åŒå·¦ï¼Œä¸”不再是 `#REF!` | é€šè¿‡ |
| T04 | å…¨çœå®¢è¿é‡ç´¯è®¡åŒæ¯”缓存值 | è¯»ç¼“å­˜ | çº¦ +0.117%(269628.07 / 269312.48 âˆ’ 1) | +0.1172% | é€šè¿‡ |
| T05 | è¾…助列是否彻底消失 | å…¨è¡¨æœç´¢ `2025å¹´1-7月累计` | 0 å¤„ | 0 å¤„;城市客运维度由 A1:BV40 æ”¶ä¸º A1:BU40 | é€šè¿‡ |
| T06 | æ¯ç‰ˆåˆ åˆ—后引用自动调整 | è¯»æ¯ç‰ˆ åŸŽå¸‚客运!W4(2025å¹´1月同比) | `=V4/AW4-1`(2024 åŒºå—随删列左移一列) | `=V4/AW4-1` | é€šè¿‡ |
| T07 | æ¯ç‰ˆæ®‹ç•™é”™è¯¯å…¬å¼ | æ‰«ææ¯ç‰ˆ åŸŽå¸‚客运页含 `#REF!` çš„公式 | 0 å¤„(删列产生的 36 å¤„ `#REF!` å·²è¿˜åŽŸä¸º AJ) | 0 å¤„ | é€šè¿‡ |
| T08 | å…¨è¡¨å¾ªçŽ¯å¼•ç”¨ | æ‰«æç”Ÿæˆè¡¨ç¼“存值 `~CIRCULAR~REF~` | 0 å¤„ | 0 å¤„(修复前 360 å¤„) | é€šè¿‡ |
| T09 | å…¶å®ƒå°è´¦é¡µ 8 æœˆåŒæ¯” | è¯» è´§è¿/网约车/中口径明细 R5 | `=Q5/AJ5-1` ä¸”缓存为百分比 | ä¸‰é¡µå‡ä¸º `=Q5/AJ5-1`;货运 +5.16%、网约车 +63.72%、中口径明细 âˆ’0.67% | é€šè¿‡ |
| T10 | éžåŸŽå¸‚客运页回归 | é€æ ¼å¯¹æ¯”改动前后除城市客运外的 18 ä¸ªé¡µç­¾ | é™¤ T09 çš„修复外不应有差异 | ä»… è´§è¿/网约车/中口径明细 çš„ R åˆ—由循环引用变为正确公式;其它页一致 | é€šè¿‡ |
| T11 | åŸºæ•°ç¼ºæ ¼çš„边界 | ä¸­å£å¾„明细 R13/R14/R107/R108(城际城乡巡游出租/网约车,2025å¹´8月基数=0) | æ˜¾ç¤º `#DIV/0!`(不可比) | `#DIV/0!`(修复前是无意义的循环引用) | é€šè¿‡ï¼ˆé¢„期) |
| T12 | é¢„览接口同比指标 | `GET /api/report/preview?period=2026-08&mode=month&types=summaryWorkbook` | 24 é¡¹æŒ‡æ ‡ï¼Œå« 8 é¡¹åŒæ¯” | 24 é¡¹ï¼Œå…¶ä¸­åŒæ¯” 8 é¡¹ï¼ˆè´§è¿é‡ +5.2/+1.9、周转量 +4.8/+0.0、城市客运量 âˆ’1.5/−3.1、城市客运周转量 âˆ’0.6/−3.5、中口径客运量 âˆ’7.1/−7.6、中口径周转量 +0.9/−1.1),耗时 1.03 ç§’ | é€šè¿‡ |
| T13 | å‰ç«¯é¢„览渲染 | æŠ¥è¡¨ç”Ÿæˆé¡µé¢„览弹窗 | 8 è¡Œéƒ½æœ‰åŒæ¯”列数值,单位正确 | æ˜ å°„已补齐(`ReportExport.vue`),前端 `Compiled successfully` | é€šè¿‡ |
| T14 | è´Ÿå‘:母版缺列/列序异常 | ä»£ç å…œåº•(`usableMonthCols` åˆ¤å®šè¡¨å¤´ä¸å…¨æ—¶é€€å›žæ•´ä½“右移 2 åˆ—) | ä¸åº”抛异常、不应改坏其它页 | å…¶å®ƒ 8 ä¸ªå°è´¦é¡µå…¬å¼ä¸Žæ—§ç‰ˆä¸€è‡´ | é€šè¿‡ |
| T15 | å¯¼å‡ºè€—æ—¶ | ä¸‹è½½ 2026-08 æ±‡æ€»å¤§è¡¨ | å¯æŽ¥å—(当前 10~20 ç§’级) | 14 ç§’(母版读取+回填 6.5s、公式重算 4.1s、写盘与传输约 3.4s) | é€šè¿‡ï¼ˆè§é—留项) |
## å››ã€å‘现的问题与处置
1. **母版两本混淆**:用户之前在《2026å¹´7月道路运输量汇总表.xlsx》里删了 AJ åˆ—,而 8 æœˆå¯¼å‡ºç”¨çš„æ˜¯
   ã€Š2026å¹´8月道路运输量汇总表.xlsx》,后者仍带该辅助列。
   â†’ å·²ç”¨ Excel 16.0 COM åœ¨ 8 æœˆæ¯ç‰ˆæ•´åˆ—删除 AJ(引用由 Excel è‡ªåŠ¨è°ƒæ•´ï¼‰ï¼Œå¹¶æŠŠåˆ åˆ—å¯¼è‡´çš„ 36 å¤„ `#REF!` ç´¯è®¡åŒæ¯”公式还原为 `AJ`。
2. **列号基准错位(本次 8 æœˆåŒæ¯”算错的直接原因)**:新写的 `colIndexOf()` è¿”回 1 åŸºåˆ—号,而 POI çš„
   `Cell.getColumnIndex()` æ˜¯ 0 åŸºï¼Œå¯¼è‡´ã€Œæœˆä»½åˆ—查表」全部落空、退化成 `列号+2`,于是
   `=O5/AH5-1` è¢«ç®—成 `=R5/AK5-1`(自引用→循环引用)。→ å·²æ”¹ä¸ºè¿”回 0 åŸºåˆ—号,
   åŒæ—¶ä¿®æŽ‰ è´§è¿/网约车/中口径明细 ä¸‰é¡µçš„ 360 æ ¼å¾ªçŽ¯å¼•ç”¨ã€‚
3. **POI `setCellFormula` ä¸æŽ¥å—带 `=` å‰ç¼€çš„公式串**:抛 `FormulaParseException`,而该异常原先被
   `catch (Exception ignore)` é™é»˜åžæŽ‰ï¼Œå¯¼è‡´ã€Œç´¯è®¡åŒæ¯”分母修正格数=0」而看不出原因。
   â†’ åŽ»æŽ‰å…¬å¼ä¸²å‰ç¼€ `=`,并把 catch æ”¹ä¸ºå†™ WARN æ—¥å¿—(保留不阻塞导出的策略)。
4. **表头识别漏判**:母版城市客运页 3~8 æœˆè¡¨å¤´æ˜¯ `46235` è¿™ç±»è£¸æ—¥æœŸåºåˆ—且单元格格式为 General,
   åŽŸæŒ‰ `isCellDateFormatted` åˆ¤å®šä¼šæ¼æŽ‰ã€‚→ æ”¹ä¸ºã€Œæ•°å€¼åœ¨ 1..200000 ä¸”换算后是当月 1 å·ã€åˆ¤å®šã€‚
5. **预览同比列缺失(前端定义问题,非数据问题)**:后端一直返回 8 é¡¹åŒæ¯”,
   ä½† `ReportExport.vue` çš„预览行定义只给两项周转量配了同比列。→ å·²è¡¥é½ 8 é¡¹å¹¶æ”¹æŽ‰ `万吨` ç¬”误。
## äº”、结论
15 æ¡ç”¨ä¾‹å…¨éƒ¨é€šè¿‡ï¼ˆå…¶ä¸­ T11 ä¸ºé¢„期内的 `#DIV/0!`、T15 ä¸ºæ€§èƒ½çŽ°çŠ¶è¯´æ˜Žï¼‰ã€‚
城市客运 8 æœˆåŒæ¯”与累计同比已恢复正确口径,辅助列在母版与生成表中均已消失,
同时修复了另三张台账页长期存在的循环引用错误,报表生成页预览可显示全部 8 é¡¹åŒæ¯”。
遗留:导出为同步请求、当前约 14 ç§’(详见工作日结待办)。
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_ÊÐÖÝͳһÌģ°åÓëÏØÊÐÇø½âÎö_2026-09-20.md
New file
@@ -0,0 +1,50 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_市州统一填报模板与县(市、区)解析_2026-09-20
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
| åŠŸèƒ½ | è¯´æ˜Ž |
| --- | --- |
| å¸‚州填报模板 | æ–°å¢žä¸¤ä»½å¸‚州月报模板:`docs/投资/模板/模板_市州月报_客运站场.xlsx`、`docs/投资/模板/模板_市州月报_物流园区.xlsx`;单页签「月报」17 åˆ—单行表头 + ã€Œå¡«æŠ¥è¯´æ˜Žã€é¡µ |
| åŽ¿ï¼ˆå¸‚ã€åŒºï¼‰åˆ—è§£æž | å¸‚州单表导入新增对「县(市、区)」列的识别,写入项目主档 `county`(原先仅投资系统导出会写) |
| è¡¨å¤´è¡Œåˆ¤å®šæ”¶ç´§ | `findInvestHeader` å¢žåŠ ç»Ÿä¸€çš„å…³é”®è¯åˆ¤å®š `investHeaderKey`,并要求表头行同时满足「含项目名称」+「至少 2 ä¸ªçŸ­è¡¨å¤´å­—段(≤20 å­—)」,避免把「填报说明」页的长句当作数据表 |
改动文件:`traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java`
## äºŒã€æµ‹è¯•环境
- åŽç«¯ Spring Boot 8090(重启后启动时间 2026-09-20 16:06:48),前端 8080,MySQL 3308 / åº“ `traffic_audit`
- æµ‹è¯•数据:以「模板_市州月报_客运站场.xlsx」为底稿生成 `武汉市_2026-09_客运站场月报.xlsx`(2 æ¡æµ‹è¯•项目)
- è°ƒç”¨æ–¹å¼ï¼š`POST /api/import/investCityBatch?period=2026-09&category=客运站场`(本地自签 JWT)
- å…¥åº“抽查:`investment_project` / `investment_monthly` é€å­—段与源文件比对
## ä¸‰ã€ç”¨ä¾‹è¡¨
| # | ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- | --- |
| T01 | æ¨¡æ¿ç»“构(客运站场) | æ‰“开模板 | ã€ŒæœˆæŠ¥ã€é¡µç¬¬ 4 è¡Œ 17 åˆ—表头;E åˆ—建设性质下拉;第二个页签为填报说明且含 11 æ¡è¦æ±‚ | ä¸Žé¢„期一致 | é€šè¿‡ |
| T02 | æ¨¡æ¿ç»“构(物流园区) | æ‰“开模板 | åŒ T01,标题为「交通物流(物流园区)」 | ä¸Žé¢„期一致 | é€šè¿‡ |
| T03 | æ­£å‘导入(2 æ¡é¡¹ç›®ï¼‰ | æŒ‰æ¨¡æ¿å¡«æŠ¥ 2 æ¡é¡¹ç›®ï¼ˆå«åŽ¿åŒºã€æ‰¹æ–‡å·ï¼‰ | total=2、success=2、city=武汉市 | total=2、success=2、fail=0、city=武汉市 | é€šè¿‡ |
| T04 | å…¥åº“抽查-主档 | åŒä¸Š | 8 ä¸ªä¸»æ¡£å­—段逐项等于源值 | æ±Ÿå²¸åŒº/测试建设单位A/新建/2026/2027/12345.67/测试工可A/测试初设A;洪山区/测试建设单位B/续建/2024/2026/20000/测试工可B/初设为空 | é€šè¿‡ |
| T05 | å…¥åº“抽查-月度数 | åŒä¸Š | 6 ä¸ªæœˆåº¦å­—段逐项等于源值、报表期正确 | æŠ¥å‘ŠæœŸ 2026-09;1000/5000/1200/300/在建/测试形象进度A;15000/8000/6000/1500/完工/测试形象进度B | é€šè¿‡ |
| T06 | è´Ÿå‘:说明页被当数据表(修复前) | æ¨¡æ¿æ–‡ä»¶ï¼ˆå«å¡«æŠ¥è¯´æ˜Žé¡µï¼‰ | åªè§£æžã€ŒæœˆæŠ¥ã€é¡µ | **失败**:误解析 8 æ¡è¯´æ˜Žé¡µé•¿å¥ä¸ºé¡¹ç›®ï¼ˆid 233~240),city è¢«æ–‡ä»¶åå…œåº•为「武汉市」 | å·²ä¿®å¤ï¼ˆè§ T07) |
| T07 | è´Ÿå‘:说明页被当数据表(修复后) | åŒ T06 | è¯´æ˜Žé¡µä¸å†è¢«è¯†åˆ«ä¸ºæ•°æ®è¡¨ | åŒä¸€æ–‡ä»¶ç”± 8 æ¡é™ä¸º 2 æ¡æ­£ç¡®è®°å½•,说明页 0 æ¡ | é€šè¿‡ |
| T08 | è´Ÿå‘:县区列写法变体 | è¡¨å¤´ã€ŒåŽ¿(市、区)」「县(市、区)」「县市区」 | å‡è¯†åˆ«ä¸ºåŽ¿åŒºåˆ— | å½’一化匹配后三种写法均命中(修复前写成「县(市,区)」带逗号,均不命中) | é€šè¿‡ |
| T09 | å›žå½’:8 æœˆçœŸå®žå¸‚州文件重导 | 8 æœˆå®¢è¿ 9 ä»½ã€ç‰©æµ 16 ä»½åŽŸå§‹æœˆæŠ¥ | è§£æžæ¡æ•°ä¸ŽåŽ†å²æ‰¹æ¬¡ä¸€è‡´ã€æ— æŠ¥é”™ | å®¢è¿ 39 æ¡ã€ç‰©æµ 128 æ¡ï¼Œå‡ fail=0(与 09-19 æ‰¹æ¬¡ 531/532 ä¸€è‡´ï¼‰ | é€šè¿‡ |
| T10 | å›žå½’:8 æœˆæœˆåº¦æ•°é€å€¼æ¯”对 | é‡å¯¼å‰ 136 è¡Œ vs é‡å¯¼åŽ 136 è¡Œ | é€å€¼ä¸€è‡´ | åŽ»ä¸»é”®åŽé€è¡Œæ¯”å¯¹ï¼š**IDENTICAL** | é€šè¿‡ |
## å››ã€å‘现的问题与处置
| # | é—®é¢˜ | å¤„ç½® |
| --- | --- | --- |
| P1 | ã€Œå¡«æŠ¥è¯´æ˜Žã€é¡µé‡Œå«ã€Œé¡¹ç›®åç§°ã€çš„长句被当作表头行,整页 8 æ¡è¢«è¯¯å»ºæˆé¡¹ç›® | å·²ä¿®ï¼šè¡¨å¤´è¡Œåˆ¤å®šè¦æ±‚「含项目名称」+「至少 2 ä¸ªçŸ­è¡¨å¤´å­—段(≤20 å­—)」;测试数据已从库中清除 |
| P2 | åŽ¿åŒºåˆ—å…³é”®è¯å†™æˆ `县(市,区)`(逗号),实际表头是 `县(市、区)`(顿号),导致 county å–不到 | å·²ä¿®ï¼šå…ˆå½’一化(全/半角括号、顿号、逗号全部去掉)再匹配「县(市区)」「县市区」「区县」 |
| P3 | **回归重导 8 æœˆç‰©æµæ—¶ï¼Œã€Šæ¹–北省十堰交通物流基础设施投资统计汇总表(月报)》再次带出非十堰的武汉 27 + é»„石 4 = 31 ä¸ªé¡¹ç›®** | å·²å†æ¬¡æ¸…除(`investment_project` 152 / `investment_monthly` 284 æ¢å¤åŽŸçŠ¶ï¼‰ï¼›æ ¹æ²»éœ€æ¸…ç† 8 æœˆæºæ–‡ä»¶ï¼Œ**待用户确认是否由我代做** |
| P4 | çœŸå®žå¸‚州月报表头里**没有**「县(市、区)」列(抽查 8 æœˆ 25 ä»½æ–‡ä»¶ï¼Œ0 ä»½å«è¯¥åˆ—) | å±žæ­£å¸¸ï¼šåŽ†å²æ•°æ® county ä¸ºç©ºæ˜¯æ•°æ®æºé™åˆ¶ï¼›9 æœˆèµ·æŒ‰ç»Ÿä¸€æ¨¡æ¿å¡«æŠ¥åŽè¯¥åˆ—才有值 |
| P5 | æµ‹è¯•用的临时文件与临时数据 | æµ‹è¯•文件 `docs/投资/输入/客运(客运站)/9月/武汉市_2026-09_客运站场月报.xlsx`、测试项目 A/B、2026-09 å¯¼å…¥æ‰¹æ¬¡å·²å…¨éƒ¨åˆ é™¤ |
## äº”、结论
- ä¸¤ä»½å¸‚州填报模板可用,正向链路(模板 â†’ è§£æž â†’ ä¸»æ¡£ + æœˆåº¦æ•°ï¼‰é€å­—段对齐;
- è¡¨å¤´åˆ¤å®šæ”¶ç´§åŽï¼Œæ¨¡æ¿è‡ªå¸¦çš„「填报说明」页不再被误当数据表(负向用例 T06/T07);
- 8 æœˆçœŸå®žå¸‚州数据重导回归无反例:条数与历史一致,月度数逐值一致;
- é—留:8 æœˆåå °æ±‡æ€»è¡¨æºæ–‡ä»¶çš„非十堰段未清理(P3),需用户确认清理方式。
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_Ͷ×ÊÃ÷ϸ±í¾ÍÐ÷ÓëÔ¤ÀÀÀàÐͲ¹Æë_2026-09-20.md
New file
@@ -0,0 +1,60 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_投资明细表就绪与预览类型补齐_2026-09-20
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
**问题**:报表生成页面中,「客运站投资明细表」「物流站场投资明细」显示
`未知报表类型:investPassengerDetail / investLogisticsDetail`,无法预览/导出。
**根因**:不是数据问题(8 æœˆå®¢è¿/物流投资月报已在库:客运 39 è¡Œã€ç‰©æµ 128 è¡Œï¼‰ã€‚
上一步只把这两个类型接进了**导出**链路(`exportSelected` switch、`REPORT_FILE_NAMES`、前端列表、两个导出接口),
漏了**导出前的就绪校验与预览**链路:
1. `checkExportReady()` çš„ `switch (t)` æ²¡æœ‰è¿™ä¸¤ä¸ª case â†’ è½åˆ° `default`:`rows=0` + `notes.add("未知报表类型:" + t)`,于是 `ready=false`;
2. `isInvestType(t)` æœªæ”¶å½•这两个类型 â†’ é¢„览循环既不归入投资分支,也拿不到月度覆盖与投资摘要。
**处置**:
- `checkExportReady()` æ–°å¢žä¸¤ä¸ª case:按类别(客运站场 / ç‰©æµå›­åŒºï¼‰ç»Ÿè®¡ `investment_monthly` è¯¥æœŸè¡Œæ•°ï¼Œ
  å¹¶æ ¡éªŒ `docs/投资/模板/` ä¸‹å¯¹åº”母版是否存在(缺母版则提示);
- `isInvestType()` æ”¶å½•两个类型(月度覆盖 + é—®é¢˜æ¸…单纳入投资分支);
- `investPreview()` å¢žåŠ æŒ‰ç±»åˆ«è¿‡æ»¤çš„é‡è½½ï¼ˆ`category != null` æ—¶åªç»Ÿè®¡è¯¥ç±»åˆ«é¡¹ç›®ï¼‰ï¼Œ
  ä½¿ä¸¤å¼ æ˜Žç»†è¡¨çš„æ‘˜è¦ä¸å†æ··å…¥å¦ä¸€ç±»åˆ«çš„合计;原无参重载保留、行为不变。
- æœªçŸ¥ç±»åž‹çš„兜底报错**保留**(未被削弱)。
## äºŒã€æµ‹è¯•环境
| é¡¹ | å€¼ |
| --- | --- |
| æ—¥æœŸ | 2026-09-20 10:4x |
| åŽç«¯ | Spring Boot 8090(重启加载,`Started TrafficAuditApplication` 10:49:51) |
| å‰ç«¯ | Vue 8080(HTTP 200) |
| æ•°æ®åº“ | MySQL 127.0.0.1:3308 `traffic_audit` |
| æŠ¥è¡¨æœŸ | 2026-08(mode=month) |
| æŽ¥å£ | `GET /api/report/ready`、`GET /api/report/preview`(`types` é€—号分隔) |
## ä¸‰ã€ç”¨ä¾‹è¡¨
| ç¼–号 | ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- | --- |
| T01 | å°±ç»ªæ ¡éªŒï¼ˆæ­£å‘) | `types=investPassengerDetail,investLogisticsDetail` | ä¸å†å‡ºçŽ°ã€ŒæœªçŸ¥æŠ¥è¡¨ç±»åž‹ã€ï¼Œready=true | ä¸¤é¡¹å‡ `ready=true`、`note` ä¸ºç©ºï¼›`rows` = 39(客运)/ 128(物流) | âœ… é€šè¿‡ |
| T02 | é¢„览(正向) | åŒä¸Šï¼Œè°ƒ `/api/report/preview` | æœ‰æœˆåº¦è¦†ç›–、摘要与问题清单 | è¦†ç›– `[8]`、`problems` ä¸ºç©ºï¼›æ‘˜è¦ï¼šå®¢è¿ã€Œ39 é¡¹ç›® / æœ¬æœˆ 2048.1 / è‡ªå¹´åˆ 38648.16 / è‡ªå¼€å§‹ 550929.76 ä¸‡å…ƒã€ï¼Œç‰©æµã€Œ128 é¡¹ç›® / æœ¬æœˆ 90185.9 / è‡ªå¹´åˆ 704107.7 / è‡ªå¼€å§‹ 2656354.31 ä¸‡å…ƒã€ | âœ… é€šè¿‡ |
| T03 | æ‘˜è¦ä¸Žåº“内逐项对齐(入库抽查) | ä¸Šè¿°æ‘˜è¦ vs SQL èšåˆ | å®Œå…¨ä¸€è‡´ | SQL:客运 `2048.1 / 38648.16 / 550929.76`、物流 `90185.9 / 704107.7 / 2656354.31`,**逐项一致** | âœ… é€šè¿‡ |
| T04 | åˆ†ç±»åˆ«è¿‡æ»¤ï¼ˆæ­£å‘) | ä¸¤å¼ è¡¨æ˜Žç»†æ‘˜è¦ | ä¸åº”混入另一类别数据 | å®¢è¿ 39 è¡Œã€ç‰©æµ 128 è¡Œï¼Œæ°å¥½ç­‰äºŽ 39+128=167(`investPlan` ç­‰å…¨ç±»åˆ«ç±»åž‹ä»è¿”回 167),说明明细表已按类别收敛 | âœ… é€šè¿‡ |
| T05 | å¯¼å‡ºå›žå½’(正向) | è°ƒ `/api/report/export/investPassengerDetail`、`.../investLogisticsDetail` | ä»èƒ½æ­£å¸¸å‡ºè¡¨ | ç”Ÿæˆ `gen_pax_inv.xlsx`(29462 å­—节)、`gen_log_inv.xls`(134144 å­—节);此前已验证客运汇总页与定稿 0 å·®å¼‚ | âœ… é€šè¿‡ |
| T06 | åŽŸæœ‰æŠ•èµ„ç±»åž‹å›žå½’ï¼ˆæ­£å‘ï¼‰ | `types=investPlan,investBillion,investCounty,investFiveYearLogistics` | è¡Œä¸ºä¸å˜ | å››é¡¹å‡ `ready=true`、`rows=167`、无 note | âœ… é€šè¿‡ |
| T07 | æœªçŸ¥ç±»åž‹ä»è¢«æ‹¦æˆªï¼ˆè´Ÿå‘) | `types=foo` | ä»æç¤ºæœªçŸ¥ç±»åž‹ä¸”不就绪 | `ready=false`、`rows=0`、`note=未知报表类型:foo` | âœ… é€šè¿‡ |
| T08 | ç¼ºæ¯ç‰ˆæç¤ºï¼ˆè´Ÿå‘·设计覆盖) | ç§»èµ°æ¯ç‰ˆ | æç¤ºã€Œdocs/投资/模板 ä¸‹ç¼ºå°‘《…》」 | æœªå®žæµ‹ï¼ˆæœªä¸´æ—¶ç§»åŠ¨ç”¨æˆ·æ¯ç‰ˆæ–‡ä»¶ï¼‰ï¼›ä»£ç è·¯å¾„ä¸Ž `summaryWorkbook` ç¼ºæ¯ç‰ˆåˆ¤å®šåŒæž„ | âš  æœªæ‰§è¡Œ |
## å››ã€å‘现的问题与处置
| ç¼–号 | é—®é¢˜ | æ ¹å›  | å¤„ç½® |
| --- | --- | --- | --- |
| P01 | ä¸¤å¼ æŠ•资明细表在报表生成页显示「未知报表类型:invest*Detail」 | åªæŽ¥äº†å¯¼å‡ºé“¾è·¯ï¼Œæ¼äº†å°±ç»ªæ ¡éªŒ `switch` ä¸Ž `isInvestType` | è¡¥é½ `checkExportReady` ä¸¤ä¸ª case + `isInvestType` æ”¶å½•,见上 |
| P02 | æ˜Žç»†è¡¨æ‘˜è¦è‹¥æ²¿ç”¨åŽŸ `investPreview` ä¼šæ··å…¥å¦ä¸€ç±»åˆ« | åŽŸæ–¹æ³•åªæŒ‰æŠ¥è¡¨æœŸèšåˆå…¨éƒ¨æŠ•èµ„æœˆæŠ¥ | æ–°å¢žå¸¦ `category` çš„重载,按类别过滤项目 id;原重载保留 |
| P03 | ã€Œåˆã€€ã€€ã€€è®¡ã€è¡Œå…¥åº“会虚增合计(前一晚修复的连带影响) | æºè¡¨åˆè®¡è¡Œè¢«å½“成项目 | å·²ä¿®å¤ï¼›æœ¬æ¬¡æ ¸å¯¹ï¼šå®¢è¿æœ¬æœˆå®Œæˆç”±å«åˆè®¡è¡Œçš„ 2130.1 æ”¶æ•›ä¸º **2048.1**,与 SQL é€é¡¹ä¸€è‡´ï¼Œæ— é—留含「计」的项目行 |
## äº”、结论
7 æ¡ç”¨ä¾‹é€šè¿‡ã€1 æ¡ï¼ˆç¼ºæ¯ç‰ˆæç¤ºï¼‰æœªå®žæµ‹ï¼š
报表生成页两张投资明细表的就绪/预览已恢复正常,摘要按类别过滤且与库内 SQL é€é¡¹ä¸€è‡´ï¼Œ
导出与其它投资报表类型均无回归,未知类型兜底拦截仍然有效。
此前「未知报表类型」纯属**代码漏配**,与 8 æœˆæŠ•资月报是否导入无关 â€”— æ•°æ®ä¸€ç›´åœ¨åº“(客运 39 / ç‰©æµ 128)。
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_Ͷ×ÊÔ¤°²ÅÅ»ã×ܲ¹¼ÆÓëµ¼È뵯´°_2026-09-20.md
New file
@@ -0,0 +1,52 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_投资预安排汇总补计与导入弹窗_2026-09-20
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
1. **清除残留数据**:删除《湖北省十堰交通物流基础设施投资统计汇总表(月报)》(5/7/8 æœˆç‰ˆï¼‰æŒ‰å¸‚州块带进库的**非十堰项目 31 ä¸ª**(武汉 27 + é»„石 4),
   ä»¥åŠè§£æžæ—¶æŠŠã€Œåˆè®¡ã€è¡Œè¯¯å»ºæˆé¡¹ç›®çš„**伪项目 1 ä¸ª**(咸宁市客运站场,13727 ä¸‡å…ƒï¼‰ã€‚删除前全量备份。
2. **预安排汇总表小计补计**:`investPlan`(生成_全省预安排计划进度情况汇总.xls)的**市州小节行 / å—合计行 / å…¨çœåˆè®¡è¡Œ**,
   åœ¨æŒ‰ `investment_monthly` ç´¯åŠ ä¹‹å¤–ï¼Œæ–°å¢žã€Œ**本期无月度数、但项目主档有总投资**」的项目,只补计「总投资」一列
   ï¼ˆå…¶ä½™ä¸‰åˆ—无主档数据源,留空)。
3. **导入结果弹窗改造**:数据导入页的导入结果/失败/不一致提示,由 Element `$alert` æ¢æˆè‡ªå®šä¹‰ `el-dialog`——
   æ ‡é¢˜æ å¯æ‹–动、内容区滚动(底部按钮固定可见)、圆角,并支持「导出明细」下载 CSV。
## äºŒã€æµ‹è¯•环境
- åŽç«¯ï¼š`mvn -q compile` é€šè¿‡ï¼›`start-dev.ps1` é‡å¯ï¼Œbackend.log `14:47:45 Started TrafficAuditApplication`、`Tomcat started on port(s): 8090`。
- å‰ç«¯ï¼šfrontend.log `14:47:45 DONE Compiled successfully in 4986ms`,App running at http://localhost:8080。
- æ•°æ®åº“:MySQL `127.0.0.1:3308` åº“ `traffic_audit`。
- éªŒè¯æŽ¥å£ï¼š`/api/report/ready`、`/api/report/export/{type}`(自签 JWT)。
## ä¸‰ã€ç”¨ä¾‹è¡¨
| ç¼–号 | ç”¨ä¾‹ | è¾“å…¥/操作 | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- | --- |
| T01 | æ®‹ç•™é¡¹ç›®å‡ºåº“ | `investment_project` ç‰©æµå›­åŒºè®¡æ•° | 135 âˆ’ 31 = 104 | 104 | é€šè¿‡ |
| T02 | æ®‹ç•™æœˆåº¦æ•°å‡ºåº“ | è¢«åˆ  31 ä¸ªé¡¹ç›®çš„ `investment_monthly` | 0 | 0 | é€šè¿‡ |
| T03 | å¤‡ä»½å¯è¿½æº¯ | `docs/投资/输出/清理残留_十堰汇总表带出的非十堰项目_2026-09-20.tsv` | å« 31 ä¸ªé¡¹ç›® + 39 æ¡æœˆåº¦æ•° + 1 æ¡ä¼ªé¡¹ç›® | 77 è¡Œï¼ˆå«åˆ†èŠ‚æ ‡é¢˜/表头) | é€šè¿‡ |
| T04 | æ­¦æ±‰ç‰©æµæ®µè¡Œæ•° | å¯¼å‡º investPlan(2026-08 å½“月) | 33 â†’ 6 | 6(航发智慧物流园/中通/阳逻港铁水联运/极兔/华中化学品/迈睿达) | é€šè¿‡ |
| T05 | æ­¦æ±‰ç‰©æµå°è®¡ï¼ˆè¡¥è®¡æ–°é€»è¾‘) | åŒä¸Šï¼Œæ­¦æ±‰å°èŠ‚è¡Œ c8 | = 6 ä¸ªé¡¹ç›®æ€»æŠ•资 69876+38400+12200+65000+22000+65000 = 272476 | c8=272476,c9/c10/c23 ç©ºï¼ˆè¯¥å¸‚未报送 8 æœˆï¼‰ | é€šè¿‡ |
| T06 | é»„石物流小计 | åŒä¸Š | 729375.69 â†’ 614991.69(= è‡ªæŠ¥ 8 ä¸ªé¡¹ç›®ï¼‰ | 614991.69 | é€šè¿‡ |
| T07 | å—合计 = ä¸»æ¡£æ€»æŠ•资合计 | ç‰©æµå—合计行 c8 | `SUM(total_investment)=5889737.36` | 5889737.36 | é€šè¿‡ |
| T08 | å…¨çœåˆè®¡ | åˆè®¡è¡Œ c8 | å®¢è¿ 763632.2 + ç‰©æµ 5889737.36 = 6653369.56 | 6653369.56 | é€šè¿‡ |
| T09 | è¡¥è®¡ä¸å½±å“ç´¯è®¡å®ŒæˆæŠ•资 | åˆè®¡è¡Œ c23 | 744262.86 âˆ’ 10306.8(被删 31 ä¸ªé¡¹ç›® year_cum ä¹‹å’Œï¼‰= 733956.06 | 733956.06 | é€šè¿‡ |
| T10 | ä¼ªé¡¹ç›®å½±å“ | å’¸å®ï¼ˆå®¢è¿ï¼‰å°èŠ‚è¡Œ c8 | 27454 â†’ 13727 | 13727 | é€šè¿‡ |
| T11 | ç”Ÿæˆè¡¨æ€»è¡Œæ•° | å¯¼å‡ºæ–‡ä»¶è¡Œæ•° | 313 â†’ 281(删 31 + 1 è¡Œï¼‰ | 281 | é€šè¿‡ |
| T12 | å…¶å®ƒæŠ•资报表回归 | `/ready` + é€ä¸ªå¯¼å‡º | å…¨éƒ¨ ready、可导出 | rows:investPlan 136 / investBillion 136 / investCounty 136 / investFiveYearLogistics 136 / investPassengerDetail 39 / investLogisticsDetail 97;导出字节 162304 / 47104 / 55808 / 82944 / 29462 / 134144 | é€šè¿‡ |
| T13 | è´Ÿå‘:未知类型 | `types=foo` | ready=false,note=未知报表类型:foo | ä¸€è‡´ï¼ˆå…œåº•未削弱) | é€šè¿‡ |
| T14 | å¼¹çª—交互 | å‰ç«¯ä»£ç  + ç¼–译 | å¯æ‹–动 / å†…容滚动 / åº•部按钮固定 / å¯¼å‡º CSV | ä»£ç é™æ€æ ¸å¯¹é€šè¿‡ã€å‰ç«¯ç¼–译通过;**点击级验收未做**(需触发真实导入才会弹) | éƒ¨åˆ†é€šè¿‡ |
## å››ã€å‘现的问题与处置
1. **伪项目「合   计」**(id=229,咸宁市客运站场,13727 ä¸‡å…ƒï¼Œæ— æœˆåº¦æ•°ï¼‰ï¼šæ¥è‡ª 2026-09-16 15:07 çš„ 8 æœˆå®¢è¿å¯¼å…¥æ‰¹æ¬¡ã€‚
   è§£æžç«¯çš„「合计/小计」判定此后已修为去空白匹配,此条属旧批次遗留,本次一并清除(备份同一 TSV æ–‡ä»¶ï¼‰ã€‚
2. **8 æœˆç‰ˆåå °æ±‡æ€»è¡¨æœªæ¸…理**:用户已把《湖北省十堰…(2026å¹´7月).xls》里非十堰内容删掉(108032 â†’ 90112 å­—节,mtime 2026-09-20 14:34),
   ä½†**8 æœˆç‰ˆï¼ˆmtime 2026-08-20)里仍含武汉 27 + é»„石 4 æ®µ**。若再次导入该文件,删除的 31 ä¸ªé¡¹ç›®ä¼šé‡æ–°å»ºæ¡£ â†’ é‡å¯¼å‰éœ€åŒæ ·æ¸…理。
3. **根因**:这类「一份文件含全省各市州」的省汇总表被当作「市州单表」按块入库(申报单位写十堰、实际收全省)。
   ç¨‹åºä¾§å…œåº•方案(导入时识别多市州块并提示)待用户确认是否要做。
4. **补计口径**:只补「总投资」,不补自开始建设累计/本年计划/本月完成——这三列没有项目主档数据源,符合用户确认的口径。
## äº”、结论
除 T14 çš„点击级验收外全部通过:残留数据已清除并留档备份;预安排汇总表小计/合计口径已按用户确认调整;
其它投资报表(亿元/经济强县/十五五物流/两张明细表)回归正常。
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_»ã×Ü´ó±íÒì²½µ¼³öÓ뻺´æ_2026-09-20.md
New file
@@ -0,0 +1,86 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_汇总大表异步导出与缓存_2026-09-20
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
用户上报(问题 3):**「汇总大表点下载后要过一会才反应下载,现在数据库的数据还不是很多,那数据库的数据多了这个程序会卡死吗?」**
背景:汇总大表导出是**同步请求**——点击后浏览器要等 10~20 ç§’才拿到文件(母版读取+回填 6.5s、整本约 4 ä¸‡æ ¼å…¬å¼é‡ç®— 4.1s、写盘传输 3.4s),期间页面无任何反馈;
且生成过程一直占着 HTTP è¯·æ±‚线程,多人同时点会让容器线程池被长任务占满。
本次改造(用户确认「可以」):
1. **异步任务化**:新增 `ReportExportTaskService`(固定 1 çº¿ç¨‹æ±  `report-export-task`,daemon)。
   å‰ç«¯æäº¤åŽç«‹å³æ‹¿åˆ°ã€Œä»»åŠ¡å·ã€ï¼Œå†æŒ‰ 700ms è½®è¯¢è¿›åº¦ï¼ˆæ­¥éª¤ + ç™¾åˆ†æ¯” + å·²ç”¨æ—¶é•¿ï¼‰ï¼Œå®ŒæˆåŽå•独下载文件。
2. **结果缓存**:同一报表期 10 åˆ†é’Ÿå†…的成功结果直接复用(`fromCache=true`),跳过母版读取与公式重算;
   ç¼“存带**数据版本守卫**——数据版本取 `SELECT IFNULL(MAX(id),0) FROM import_batch`,期间只要有新导入就自动失效(取不到版本时禁用缓存)。
3. **去重与排队**:同一报表期已有任务在跑时直接复用该任务,不会重复排队重算。
4. **前端进度弹窗**:进度条 + æ­¥éª¤æ–‡æ¡ˆ + å·²ç”¨æ—¶é•¿ + ç¼“存来源时间 + å¤±è´¥åŽŸå› ï¼›ç”Ÿæˆä¸­å¯ã€ŒåŽå°è¿è¡Œã€ï¼ˆæ”¶å¼¹çª—ï¼Œå®ŒæˆåŽä»è‡ªåŠ¨ä¸‹è½½ï¼‰ï¼Œå®ŒæˆåŽå¯ã€Œä¿å­˜æ–‡ä»¶ / é‡æ–°ç”Ÿæˆ / å…³é—­ã€ã€‚
5. **进度点**:2% æ ¡éªŒæ¯ç‰ˆ â†’ 6% è¯»å–母版 â†’ 12% è¿˜åŽŸæœˆåº¦å— â†’ 22% å›žå¡«å½“月数据 â†’ 45% åˆ·æ–°æŽ’名页与跨页合成页 â†’ 70% é‡ç®—整本公式 â†’ 95% å†™å‡ºæ–‡ä»¶ â†’ 100% å®Œæˆã€‚
回答用户疑问:**数据量增大主要是 SQL èšåˆå˜æ…¢ï¼Œä¸ä¼šã€Œå¡æ­»ã€**;改造后生成走后台线程、同报表期结果可复用、同报表期并发自动去重,
即使多人同时点也只是排队(弹窗显示「排队中」),不会占满 Web çº¿ç¨‹ã€‚
## äºŒã€æµ‹è¯•环境
- åŽç«¯ Spring Boot 8090(`Started TrafficAuditApplication` 15:10:53)、前端 Vue 8080(`Compiled successfully`)
- æ•°æ®åº“ MySQL localhost:3308/traffic_audit
- æŠ¥è¡¨æœŸï¼š2026-08(月报,正常用例)/ 2025-01(无数据,负向用例)
- æŽ¥å£è„šæœ¬ï¼š`_qa_task.ps1`、`_qa_task2.ps1`、`_qa_task3.ps1`(保留未删)
- æµè§ˆå™¨å®žæµ‹ï¼šPlaywright 1.57 + æœ¬æœº Chrome(无头),真实点击「报表生成」页按钮
- è¯æ®æˆªå›¾ï¼š`docs/功能测试报告/汇总大表异步导出进度弹窗_2026-09-20.png`
## ä¸‰ã€ç”¨ä¾‹è¡¨
### 3.1 æŽ¥å£å±‚
| ç¼–号 | ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- | --- |
| T-A1 | æäº¤å¼‚步任务 | `GET /api/report/exportTask/summaryWorkbook?period=2026-08&mode=month` | ç«‹å³è¿”回 taskId,不阻塞 | æäº¤è€—æ—¶ **0.07s**,进度 6→22→45→70→95→100,终态 DONE,**14.4s** | é€šè¿‡ |
| T-A2 | ä¸‹è½½ä»»åŠ¡æ–‡ä»¶ | `GET /api/report/exportTask/{id}/file` | è¿”回 xlsx | 1,325,109 å­—节,**1.27s** | é€šè¿‡ |
| T-A3 | 10 åˆ†é’Ÿå†…同报表期复用 | å†æ¬¡æäº¤åŒå‚æ•° | fromCache=true、同 taskId、近乎瞬时 | fromCache=true,**0.01s**,taskId ç›¸åŒ | é€šè¿‡ |
| T-A4 | å¼ºåˆ¶é‡ç®— | æäº¤ `refresh=true` | æ–°ä»»åŠ¡å·ã€çœŸå®žé‡ç®— | æ–° taskId,14.2s é‡ç®—完成 | é€šè¿‡ |
| T-A5 | è¿è¡Œä¸­å¹¶å‘去重 | è¿žå‘两次 `refresh=true` | è¿”回同一 taskId | åŒä¸€ taskId,11.1s | é€šè¿‡ |
| T-A6 | æ–°å¢žå¯¼å…¥ä½¿ç¼“存失效 | ä¸´æ—¶æ’入一条 `import_batch`(id 533→534)后提交 | fromCache=false,重新生成 | fromCache=false,11.4s é‡ç®—ï¼›**测试后已 DELETE æ¸…回 id=533** | é€šè¿‡ |
| T-A7 | è´Ÿå‘:不存在的 taskId | `GET /exportTask/notexist123` / `.../file` | è¿”回明确错误 | çŠ¶æ€æŽ¥å£ `code=500`「导出任务不存在或已过期(生成结果保留 30 åˆ†é’Ÿï¼‰ã€ï¼›æ–‡ä»¶æŽ¥å£ HTTP 500 + åŒæç¤º | é€šè¿‡ |
| T-A8 | è´Ÿå‘:未完成就下载 | ä»»åŠ¡ RUNNING æ—¶è¯·æ±‚ `/file` | æ‹’绝并说明 | æŠ›ã€Œå¯¼å‡ºä»»åŠ¡å°šæœªå®Œæˆï¼ˆå½“å‰çŠ¶æ€ï¼šRUNNING)」 | é€šè¿‡ |
### 3.2 æµè§ˆå™¨å®žæµ‹ï¼ˆçœŸå®žç‚¹å‡»ï¼‰
| ç¼–号 | ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- | --- |
| UI-1 | è¿›åº¦å¼¹çª— | æŠ¥è¡¨ç”Ÿæˆ â†’ æ±‡æ€»å¤§è¡¨ â†’ 2026-08 â†’ ç‚¹ã€Œå¯¼å‡ºã€ | ç«‹å³å¼¹çª—并逐档推进 | å¼¹çª—即时出现,实测采样:0.7s è¯»å–母版工作簿(6%) â†’ 1.5s å›žå¡«å½“月数据(22%) â†’ 2.2s åˆ·æ–°æŽ’名页与跨页合成页(45%) â†’ 7.2s é‡ç®—整本公式(约 4 ä¸‡æ ¼ï¼‰(70%) â†’ 10.1s å†™å‡ºæ–‡ä»¶(95%) â†’ 11.6s å®Œæˆ(100%) | é€šè¿‡ |
| UI-2 | å®ŒæˆåŽè‡ªåŠ¨ä¸‹è½½ | åŒä¸Š | è‡ªåŠ¨è§¦å‘ä¿å­˜ | ä¸‹è½½ `生成_道路运输量汇总表_2026-08.xlsx`,**1,325,109 å­—节**,与接口产物同哈希 | é€šè¿‡ |
| UI-3 | ç¼“存命中提示 | å…³é—­åŽå†æ¬¡ç‚¹ã€Œå¯¼å‡ºã€ | æç¤ºæ¥æºæ—¶é—´ + ã€Œä¸‹è½½ã€æŒ‰é’® | ã€Œä¸Šæ¬¡ç”Ÿæˆç”¨æ—¶ 11.3 ç§’」「本次直接使用 2026-09-20 15:43:50 ç”Ÿæˆçš„结果(10 åˆ†é’Ÿå†…且期间没有新导入数据)」,按钮变「下载」,秒级完成 | é€šè¿‡ |
| UI-4 | ã€Œé‡æ–°ç”Ÿæˆã€ | ç¼“存态点「重新生成」 | çœŸå®žé‡ç®— | è¿›åº¦ä»Ž 0 é‡æ–°æŽ¨è¿›ï¼ˆã€Œå·²ç”¨ 2.1 ç§’」),按钮区切回「保存文件/重新生成/后台运行」,23.4s å†…完成并自动下载 | é€šè¿‡ |
| UI-5 | ã€ŒåŽå°è¿è¡Œã€ | ç”Ÿæˆä¸­ç‚¹ã€ŒåŽå°è¿è¡Œã€ | æ”¶èµ·å¼¹çª—、任务继续、完成仍自动下载 | å¼¹çª—æ”¶èµ· + æç¤ºã€Œå·²è½¬å…¥åŽå°ç”Ÿæˆï¼Œå®ŒæˆåŽä¼šè‡ªåŠ¨ä¸‹è½½ã€ï¼ŒéšåŽè‡ªåŠ¨ä¸‹è½½ 1,325,109 å­—节 | é€šè¿‡ |
| UI-6 | è´Ÿå‘:无数据报表期 | é€‰ 2025-01 â†’ å¯¼å‡º â†’ ç¡®è®¤ã€Œä»è¦å¯¼å‡ºã€ | æ˜¾ç¤ºå¤±è´¥åŽŸå› ã€å¯å…³é—­ã€ä¸å¯ä¿å­˜ | å¼¹çª—显示「生成失败|未找到模板文件: 2025å¹´1月道路运输量汇总表.xlsx」,X å¯å…³é—­ï¼Œã€Œä¿å­˜æ–‡ä»¶ã€ç¦ç”¨ï¼Œã€Œé‡æ–°ç”Ÿæˆã€å¯ç”¨ | é€šè¿‡ |
| UI-7 | äº§ç‰©ä¸€è‡´æ€§ | å†·å¯åЍ件 vs åˆ·æ–°é‡ç®—ä»¶ | æ•°æ®ä¸€è‡´ | ä¸¤æ¬¡äº§ç‰©è§£åŒ…后**逐条目一致**(无差异条目);三份缓存下载件 SHA256 å®Œå…¨ç›¸åŒ | é€šè¿‡ |
### 3.3 é¡ºæ‰‹å¤éªŒï¼ˆç”¨æˆ·é—®é¢˜ 2)
| ç¼–号 | ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- | --- |
| P-1 | é¢„览同比项是否补全 | æ±‡æ€»å¤§è¡¨é¢„览弹窗 | 8 é¡¹åŒæ¯”都在 | 12 è¡Œï¼ˆè´§è¿/城市客运/中口径 Ã— å½“月·累计 Ã— é‡Â·å‘¨è½¬ï¼‰ï¼ŒåŒæ¯”列齐全 | é€šè¿‡ |
| P-2 | ã€Œç´¯è®¡è´§ç‰©å‘¨è½¬é‡Â·ç´¯è®¡åŒæ¯” 0%」是否为 bug | æŸ¥åº“ `scale_split_transport` | åº”为取数源本身的值 | åº“内 `total_yoy=0.00000346`(约 0.0003%)→ ä¸€ä½å°æ•°å››èˆäº”入即 **0%**,非计算错误 | é€šè¿‡ï¼ˆéžç¼ºé™·ï¼‰ |
## å››ã€å‘现的问题与处置
1. **`ReportExport.vue` å‡ºçŽ°é‡å¤çš„ `computed` é”®ï¼ˆæœ¬æ¬¡æµè§ˆå™¨å®žæµ‹å‘现,属严重回归)**
   æ–°å¢žçš„异步导出计算属性(taskRunning/taskDone/taskCacheTime)另起了一个 `computed: {`,
   JS å¯¹è±¡å­—面量**同名键后者覆盖前者**,导致原有的 `username / realName / myModules / visibleModules` è®¡ç®—属性全部丢失,
   ã€ŒæŠ¥è¡¨ç”Ÿæˆã€é¡µ `created()` ç›´æŽ¥æŠ› `Cannot read properties of undefined (reading 'filter')`,**整页白屏不可用**。
   â†’ å·²æŠŠ 4 ä¸ªè®¡ç®—属性并入原 `computed` å—(并加注释说明不可另起同名 key),编译通过并浏览器复验正常。
   ï¼ˆæŽ¥å£å±‚测试无法发现此类问题,这也是本次坚持做浏览器点击实测的原因。)
2. **提交/轮询失败时弹窗会空转**:后端统一返回 `Result`(错误时 HTTP ä» 200、`data` ä¸ºç©ºï¼‰ï¼Œ
   åŽŸä»£ç ç›´æŽ¥ `this.taskInfo = res.data || {}` ä¼šå¾—到一个空对象 â†’ è¿›åº¦åœåœ¨ 0% ä¸”不给提示。
   â†’ æäº¤ä¸Žè½®è¯¢éƒ½æ”¹ä¸ºå…ˆåˆ¤ `res.code !== 200 || !res.data`:提交失败立即显示失败原因;轮询判为「任务已失效」并停止轮询。
3. **生成中弹窗无出口**:原「关闭」按钮 `:disabled="!taskDone"`,生成中三个按钮全灰、右上角 X ä¹Ÿéšè—ï¼Œç”¨æˆ·è¢«é”åœ¨å¼¹çª—里等 10~20 ç§’。
   â†’ æ”¹ä¸ºç”Ÿæˆä¸­æ˜¾ç¤ºã€ŒåŽå°è¿è¡Œã€ï¼ˆæ”¶å¼¹çª—、继续生成、完成自动下载),结束后才显示「关闭」;X åœ¨æˆåŠŸæˆ–å¤±è´¥åŽå‡å¯è§ã€‚
4. **缓存态文案歧义**:命中缓存时元信息仍写「已用 11.3 ç§’」(其实是上次生成耗时)。
   â†’ ç¼“存态改为「上次生成用时」,避免误以为本次也算了 11.3 ç§’。
## äº”、结论
- æŽ¥å£ç”¨ä¾‹ 8 æ¡ã€æµè§ˆå™¨å®žæµ‹ 7 æ¡ã€å¤éªŒ 2 æ¡ï¼Œ**全部通过**;无遗留未验证项。
- æ±‡æ€»å¤§è¡¨å¯¼å‡ºçŽ°åœ¨æ˜¯ã€Œç‚¹å‡»å³åé¦ˆ â†’ è¿›åº¦å¯è§† â†’ å®Œæˆå³ä¸‹è½½ã€ï¼Œç¼“存命中时秒级返回;同一报表期并发自动去重,生成不占 Web çº¿ç¨‹ã€‚
- é™„带修复了会导致报表生成页整页白屏的重复 `computed` å›žå½’(该问题在接口测试中不可见)。
docs/¹¦ÄܲâÊÔ±¨¸æ/»ã×Ü´ó±íÒì²½µ¼³ö½ø¶Èµ¯´°_2026-09-20.png
docs/¹¤×÷ÈÕ½á/¹¤×÷ÈÕ½á_2026-09-20.md
New file
@@ -0,0 +1,195 @@
# å·¥ä½œæ—¥ç»“_2026-09-20
## ä¸€ã€ä»Šå¤©åšäº†ä»€ä¹ˆ
### 1.1 ä¿®å¤ï¼šæŠ¥è¡¨ç”Ÿæˆé¡µä¸¤å¼ æŠ•资明细表报「未知报表类型」
- **现象**:报表生成页「客运站投资明细表」「物流站场投资明细」显示
  `未知报表类型:investPassengerDetail / investLogisticsDetail`,无法预览/导出。
- **定位**:报错来自 `ReportExportService.checkExportReady()` çš„ `switch (t)` å…œåº•分支(`ReportExportService.java:231`)。
  å‰ä¸€æ™šåªæŠŠè¿™ä¸¤ä¸ªç±»åž‹æŽ¥è¿›**导出**链路(`exportSelected` switch、`REPORT_FILE_NAMES`、`ReportExportController` ä¸¤ä¸ªæŽ¥å£ã€`ReportExport.vue` ä¸¤é¡¹ï¼‰ï¼Œ
  æ¼äº†**就绪校验与预览**链路,`isInvestType()` ä¹Ÿæ²¡æ”¶å½•这两个类型。
- **与数据无关**:8 æœˆæŠ•资月报一直在库(客运站场 39 è¡Œã€ç‰©æµå›­åŒº 128 è¡Œï¼‰ã€‚
- **修复**:
  1. `checkExportReady()` æ–°å¢žä¸¤ä¸ª case:按类别统计 `investment_monthly` è¯¥æœŸè¡Œæ•° + æ ¡éªŒ `docs/投资/模板/` æ¯ç‰ˆæ˜¯å¦å­˜åœ¨ï¼ˆç¼ºæ¯ç‰ˆç»™æç¤ºï¼‰ï¼›
  2. `isInvestType()` æ”¶å½•两个类型(纳入投资分支的月度覆盖与问题清单);
  3. `investPreview()` å¢žåŠ æŒ‰ç±»åˆ«è¿‡æ»¤é‡è½½ `(period, mode, category, summary, problems)`,原无参重载保留;
  4. é¢„览循环按类型分派到对应类别;未知类型兜底报错保持不变。
### 1.2 å…¶å®ƒ
- é‡å¯å‰åŽç«¯åŠ è½½æ–°ä»£ç ï¼ˆåŽç«¯ `Started TrafficAuditApplication` 10:49:51)。
- é¡ºæ‰‹æ ¸å¯¹ï¼šå‰ä¸€æ™šã€Œåˆè®¡è¡Œè¢«å½“项目」修复后,客运本月完成投资由含合计行的 2130.1 æ”¶æ•›ä¸º **2048.1**,与 SQL é€é¡¹ä¸€è‡´ï¼›åº“内已无含「计」的伪项目行。
### 1.3 ä¿®å¤ï¼šæ±‡æ€»å¤§è¡¨ã€ŠåŸŽå¸‚客运》8 æœˆåŒæ¯” / ç´¯è®¡åŒæ¯”算错(母版删 AJ è¾…助列 + ç¨‹åºæŒ‰è¡¨å¤´å–数)
- **现象**:用户上报生成表城市客运 8 æœˆåŒæ¯” âˆ’86.5%、累计同比 âˆ’43.5%;且「2025å¹´1-7月累计」列在母版删过却仍在生成表里。
- **根因(3 ä¸ªï¼‰**:
  1. æ¯ç‰ˆæœ‰**两本**——用户删的是《2026å¹´7月道路运输量汇总表.xlsx》,而 8 æœˆå¯¼å‡ºç”¨çš„æ˜¯ã€Š2026å¹´8月…》,
     åŽè€…城市客运页 2025 å¹´æ®µæ’了 AJ åˆ—「2025å¹´1-7月累计」,把 2025å¹´8月列挤到 AK,
     8 æœˆåŒæ¯” `=Q4/AJ4-1` å®žé™…是「8月值 Ã· 2025å¹´1-7月累计」;累计同比分母又把 1-6 æœˆé‡å¤è®¡ã€8 æœˆæ¼è®¡ã€‚
  2. æ–°å†™çš„列号换算 `colIndexOf()` è¿”回 1 åŸºåˆ—号,而 POI åˆ—号是 0 åŸº â†’ æœˆä»½åˆ—查表全落空、退化成「+2 åˆ—」,
     `=O5/AH5-1` è¢«ç®—成 `=R5/AK5-1`(自引用)→ è´§è¿/网约车/中口径明细 å…± 360 æ ¼å¾ªçŽ¯å¼•ç”¨ã€‚
  3. POI `setCellFormula` ä¸æŽ¥å—带 `=` å‰ç¼€çš„公式串,异常又被 `catch (Exception ignore)` åžæŽ‰ï¼Œ
     ç´¯è®¡åŒæ¯”分母修正一直静默失败(日志只显示「修正格数=0」);另母版 3~8 æœˆè¡¨å¤´æ˜¯ 46235 è¿™ç±»è£¸åºåˆ— + General æ ¼å¼ï¼Œ
     åŽŸæŒ‰ `isCellDateFormatted` åˆ¤å®šä¼šæ¼åˆ¤ã€‚
- **处置**:
  1. æ¯ç‰ˆï¼šç”¨ Excel 16.0 COM åœ¨ã€Š2026å¹´8月道路运输量汇总表.xlsx》城市客运页整列删除 AJ(引用由 Excel è‡ªåŠ¨è°ƒæ•´ï¼Œ
     å¦‚ `W4` â†’ `=V4/AW4-1`),再把删列产生的 36 å¤„ `#REF!` ç´¯è®¡åŒæ¯”公式还原为 `AJ`;
     å¤‡ä»½ `_bak_202608母版删除AJ列前_20260920.xlsx`;删列后母版与版式参照件《生成_道路运输量汇总表.xlsx》列结构一致。
  2. ä»£ç ï¼š`colIndexOf` æ”¹ 0 åŸºï¼›`setCellFormula` åŽ»æŽ‰ `=` å‰ç¼€å¹¶æŠŠ catch æ”¹æˆ WARN æ—¥å¿—ï¼›
     `yearMonthOf` æ”¯æŒè£¸æ—¥æœŸåºåˆ—ï¼›`repairCumulativeYoyFormulas()` ä¿ç•™ä½œæ¯ç‰ˆå…œåº•。
  3. å‰ç«¯ï¼š`ReportExport.vue` é¢„览补齐货运量/城市客运/中口径共 8 é¡¹åŒæ¯”映射,修正「当月货物周转量(万吨)」单位。
### 1.4 æ¸…除「十堰汇总表」带出的非十堰残留(用户要求)
- **背景**:用户核对 `D:\桌面\生成数据\生成_全省预安排计划进度情况汇总 (2).xls` å‘现武汉物流段多出 27 ä¸ªé¡¹ç›®ï¼ˆé«˜æ¡¥è´§è¿ç«™â€¦è¶Šæµ·åŽä¸­ä¾›åº”链总部基地,开工年 2006-2016)。
- **溯源**:这 27 ä¸ªï¼ˆ+黄石 4 ä¸ªï¼‰æ¥è‡ªã€Šæ¹–北省十堰交通物流基础设施投资统计汇总表(月报)》——文件名/填报单位是十堰,
  ä½†ä¸€ä¸ªé¡µç­¾é‡Œæ”¶äº†å…¨çœå„市州;2026-08-26 11:57 éšã€Œå¸‚州单表批量导入(物流(物流园区)/7月)」按市州块入库(batch 17/25)。
  äº¤å‰æ ¸å¯¹ï¼šè¿™ 31 ä¸ªé¡¹ç›®åç§°åœ¨å…¶å®ƒä»»ä½•市州源文件里都**查不到**,确认为该文件带出的残留。
- **导出为何会显示**:`investPlan` æ˜¯ã€Œæ¯ç‰ˆåŽŸä½æ›¿æ¢ + åŠ¨æ€æ’è¡Œã€â€”â€”æ¯ç‰ˆæ­¦æ±‰ç‰©æµåªé¢„ç•™ 7 è¡Œï¼Œåº“里多出的项目按「市州+类别」自动补插到小节末尾
  ï¼ˆ`ReportExportService.java` åŠ¨æ€æ’è¡Œæ®µï¼‰ã€‚æ‰€ä»¥åº“é‡Œæœ‰å¤šå°‘é¡¹ç›®ï¼Œç”Ÿæˆè¡¨å°±å‡ºå¤šå°‘è¡Œã€‚
- **处置**:全量备份到 `docs/投资/输出/清理残留_十堰汇总表带出的非十堰项目_2026-09-20.tsv`(31 ä¸ªé¡¹ç›® + 39 æ¡æœˆåº¦æ•°ï¼‰åŽåˆ é™¤ï¼›
  é¡ºå¸¦å‘现并清除解析「合计」行误建的伪项目(id=229 å’¸å®å¸‚客运站场「合   计」,13727 ä¸‡å…ƒï¼‰ã€‚
  ç»“果:物流园区项目 135 â†’ 104,2026-08 æœˆåº¦æ•° 167 â†’ 136,客运站场项目 49 â†’ 48。
### 1.5 é¢„安排汇总表:小计/合计补计「有总投资、无本期月度数」的项目(用户确认「可以」)
- åœºæ™¯ï¼šæ­¦æ±‰ 8 æœˆç‰©æµæ²¡æœ‰å•表报送(`docs/投资/输入/物流(物流园区)/8月` é‡Œæ²¡æœ‰æ­¦æ±‰æ–‡ä»¶ï¼‰ï¼Œ6 ä¸ªæ–°é¡¹ç›®åœ¨åº“里没有 2026-08 æœˆåº¦æ•°ï¼Œ
  åŽŸæ¥å°è®¡åªç´¯åŠ æœˆåº¦æ•°æ® â†’ æ­¦æ±‰ç‰©æµæ•´å—只显示项目行、小计空白。
- æ”¹åŠ¨ï¼š`ReportExportService.investPlan()` æ–°å¢ž `addProjectsWithoutMonthly(...)`,在**市州小节/块合计/全省合计**三处累加后,
  æŠŠã€Œæœ¬æœŸæ— æœˆåº¦è®°å½•但主档有总投资」的项目按类别/市州补进 **总投资** ä¸€åˆ—(其余列无主档数据源,保持空白)。
- æ•ˆæžœï¼šæ­¦æ±‰ç‰©æµå°è®¡ = 272476(6 ä¸ªé¡¹ç›®æ€»æŠ•资和);块合计/全省合计 = åº“内同类项目总投资之和(可逐项对 SQL æ ¸å¯¹ï¼‰ã€‚
### 1.6 å¯¼å…¥ç»“果弹窗改造(用户:弹窗太长、点不到确认、不能移动)
- åŽŸæ¥ç”¨çš„æ˜¯ Element `$alert`(MessageBox)承载导入结果 HTML,内容一长就把「确认」挤出屏幕,且不能拖动。
- æ”¹ä¸ºè‡ªå®šä¹‰ `el-dialog`:`v-dialog-drag` æ ‡é¢˜æ æ‹–动、`custom-class="import-result-dialog"`(全局样式,圆角 + å†…容区 `max-height` æ»šåЍ + åº•部按钮固定),
  å¹¶æ–°å¢žã€Œå¯¼å‡ºæ˜Žç»†ã€æŒ‰é’®ï¼ˆCSV,带 BOM,可直接 Excel æ‰“开),用于用户自行核对/修改不一致内容。
- è¦†ç›– 4 å¤„:批量导入结果、市州单表批量导入结果、单文件导入失败、投资与投资系统不一致清单。
### 1.7 æ±‡æ€»å¤§è¡¨å¯¼å‡ºå¼‚步化 + ç»“果缓存(用户提问「点下载要等一会,数据多了会不会卡死」)
- **现象**:汇总大表点「导出」后要等 10~20 ç§’才触发下载,期间页面无任何反馈;同步请求一直占着 Web çº¿ç¨‹ã€‚
- **定位**:14 ç§’里母版读取+回填 6.5s、整本约 4 ä¸‡æ ¼å…¬å¼é‡ç®— 4.1s、写盘传输 3.4s——耗时来自 Excel å¤„理,**与数据库行数关系不大**,数据多了主要是 SQL èšåˆå˜æ…¢ï¼Œä¸ä¼šã€Œå¡æ­»ã€ã€‚
- **改动**:
  1. æ–°å¢ž `ReportExportTaskService`(固定 1 çº¿ç¨‹æ±  daemon):提交即返回 taskId,前端 700ms è½®è¯¢ã€Œæ­¥éª¤ + ç™¾åˆ†æ¯” + å·²ç”¨æ—¶é•¿ã€ï¼›ç»“果保留 30 åˆ†é’Ÿï¼ˆå†…存态,进程重启失效)。
  2. **结果缓存 10 åˆ†é’Ÿ + æ•°æ®ç‰ˆæœ¬å®ˆå«**:版本取 `SELECT IFNULL(MAX(id),0) FROM import_batch`,期间只要有新导入就自动失效(取不到版本时禁用缓存);同一报表期并发提交自动去重、复用同一任务。
  3. `ReportExportService` æ–°å¢žå¸¦è¿›åº¦å›žè°ƒçš„ `exportSummaryWorkbook(period, mode, toMonth, progress)` é‡è½½ï¼ˆåŽŸä¸¤ä¸ªé‡è½½å§”æ‰˜å®ƒï¼‰ï¼Œè¿›åº¦ç‚¹ 2%/6%/12%/22%/45%/70%/95%。
  4. `ReportExportController` æ–°å¢ž 3 ä¸ªæŽ¥å£ï¼š`/exportTask/summaryWorkbook`(提交)、`/exportTask/{taskId}`(进度)、`/exportTask/{taskId}/file`(下载);原同步接口保留兼容。
  5. å‰ç«¯ `ReportExport.vue` è¿›åº¦å¼¹çª—:进度条 + æ­¥éª¤ + ç”¨æ—¶ + ç¼“存来源时间 + å¤±è´¥åŽŸå› ï¼Œä»¥åŠã€Œä¿å­˜æ–‡ä»¶ / é‡æ–°ç”Ÿæˆ / åŽå°è¿è¡Œ / å…³é—­ã€ã€‚
- **实测**:提交 0.07s;进度 6→22→45→70→95→100、14.4s å®Œæˆï¼›ç¼“存命中 0.01s;`refresh=true` çœŸå®žé‡ç®—;临时插一条导入后缓存自动失效。
- **同时修掉一个严重回归**:本次新增的 `computed` ä¸Žæ–‡ä»¶ä¸­å·²æœ‰ `computed` **同名重复**,JS å¯¹è±¡åŽè€…覆盖前者 â†’ `username/myModules/visibleModules` å…¨éƒ¨ä¸¢å¤±ï¼Œ
  ã€ŒæŠ¥è¡¨ç”Ÿæˆã€é¡µ `created()` ç›´æŽ¥æŠ›é”™ã€**整页白屏**。接口测试发现不了,是浏览器实测才暴露的(详见测试报告第四节)。
### 1.8 å¸‚州统一填报模板(用户提问「按哪个格式来最好?要不要生成两个模板」)
- **背景**:用户提出「这几个表的更新其实很简单——按项目名称更新汇总表指标、新增项目就新增到明细表;
  é¢„安排汇总表和明细表方式一样,剩下三张表项目固定只更新指标——为什么现在弄得有点复杂」。
- **核对**:需求文档《投资月报数据处理与多表更新需求说明》Step1/2/3 ä¸Žç”¨æˆ·æè¿°å®Œå…¨ä¸€è‡´ï¼Œç¨‹åºé€»è¾‘也是这么实现的。
- **结论**:复杂不在「表」,在「源」——版式不统一(27 ä»½é‡Œ 7 ä»½æœ‰å·®å¼‚)、一个文件混多个市州、没有县区列、列位置靠兜底。
- **产出**:
  1. æ–°å¢žä¸¤ä»½å¸‚州填报模板 `模板_市州月报_客运站场.xlsx` / `模板_市州月报_物流园区.xlsx`
     ï¼ˆã€ŒæœˆæŠ¥ã€é¡µ 17 åˆ—单行表头 + å»ºè®¾æ€§è´¨ä¸‹æ‹‰ + ã€Œå¡«æŠ¥è¯´æ˜Žã€é¡µ 11 æ¡è¦æ±‚);
  2. å£å¾„说明文档 `docs/投资/填报口径与统一模板_2026-09-20.md`(模板字段去向、复杂度来源、待定死的 3 ä»¶äº‹ã€è¿‡æ¸¡å®‰æŽ’)。
- **配套解析改动**:新增「县(市、区)」列解析(写入主档 `county`);表头行判定收紧为
  ã€Œå«é¡¹ç›®åç§° + è‡³å°‘ 2 ä¸ªçŸ­è¡¨å¤´å­—段」,避免把模板自带的「填报说明」页当数据表。
## äºŒã€æµ‹è¯•结果摘要
《功能测试报告_投资明细表就绪与预览类型补齐_2026-09-20.md》:**7 æ¡ç”¨ä¾‹é€šè¿‡ï¼Œ1 æ¡æœªå®žæµ‹**(缺母版提示,未临时移动用户母版文件)。
| æ ¡éªŒé¡¹ | ç»“æžœ |
| --- | --- |
| `/api/report/ready`(两新类型) | ready=true,rows=39 / 128,note ä¸ºç©º |
| `/api/report/preview`(两新类型) | è¦†ç›– [8],problems ä¸ºç©º |
| æ‘˜è¦ vs SQL é€é¡¹å¯¹é½ | å®¢è¿ `2048.1 / 38648.16 / 550929.76`、物流 `90185.9 / 704107.7 / 2656354.31`,**一致** |
| åˆ†ç±»åˆ«è¿‡æ»¤ | å®¢è¿ 39 + ç‰©æµ 128 = 167,全类别类型仍 167(未串类) |
| å¯¼å‡ºå›žå½’ | ä¸¤å¼ æ˜Žç»†è¡¨ä»å¯å¯¼å‡ºï¼ˆ29462 / 134144 å­—节) |
| åŽŸæœ‰æŠ•èµ„ç±»åž‹å›žå½’ | investPlan / investBillion / investCounty / investFiveYearLogistics å‡ ready=true,rows=167 |
| æœªçŸ¥ç±»åž‹è´Ÿå‘ | `foo` â†’ ready=false、note=未知报表类型:foo(兜底仍有效) |
《功能测试报告_城市客运月度与累计同比_2026-09-20.md》:**15 æ¡ç”¨ä¾‹é€šè¿‡**。
| æ ¡éªŒé¡¹ | ç»“æžœ |
| --- | --- |
| åŸŽå¸‚客运 R4 / T4 å…¬å¼ | `=Q4/AJ4-1`、`=S4/(V4+X4+Z4+AB4+AD4+AF4+AH4+AJ4)-1`(不再是 `#REF!`) |
| å…¨çœå®¢è¿é‡ 8 æœˆåŒæ¯” / ç´¯è®¡åŒæ¯” | **+4.44% / +0.117%**(修复前 âˆ’86.5% / âˆ’43.5%) |
| è¾…助列 | ç”Ÿæˆè¡¨ä¸Žæ¯ç‰ˆå‡æ— ã€Œ2025å¹´1-7月累计」,城市客运页 BV→BU |
| å¾ªçŽ¯å¼•ç”¨ | å…¨è¡¨ 0 å¤„(修复前 360 å¤„,含货运/网约车/中口径明细 R åˆ—) |
| å…¶å®ƒ 18 ä¸ªé¡µç­¾å›žå½’ | é™¤ä¸Šè¿° R åˆ—修复外逐格一致 |
| é¢„览接口 | 24 é¡¹æŒ‡æ ‡å« 8 é¡¹åŒæ¯”,1.03 ç§’ |
| å¯¼å‡ºè€—æ—¶ | 14 ç§’(母版读取+回填 6.5s、公式重算 4.1s、写盘传输 3.4s) |
《功能测试报告_投资预安排汇总补计与导入弹窗_2026-09-20.md》:**14 æ¡ç”¨ä¾‹é€šè¿‡ï¼ˆ1 æ¡ä¸ºé™æ€/编译级验证,点击级验收待用户实测)**。
| æ ¡éªŒé¡¹ | ç»“æžœ |
| --- | --- |
| æ®‹ç•™å‡ºåº“ | ç‰©æµé¡¹ç›® 135→104、月度数 167→136;备份 TSV 77 è¡Œ |
| ç”Ÿæˆè¡¨æ­¦æ±‰ç‰©æµæ®µ | 33 è¡Œ â†’ 6 è¡Œï¼ˆèˆªå‘/中通/阳逻港/极兔/华中化学品/迈睿达) |
| æ­¦æ±‰ç‰©æµå°è®¡ï¼ˆè¡¥è®¡ï¼‰ | c8=272476(6 ä¸ªé¡¹ç›®æ€»æŠ•资和),其余列空 |
| é»„石物流小计 | 729375.69 â†’ 614991.69 |
| å—合计/全省合计 | ç‰©æµ 5889737.36、客运 763632.2、全省 6653369.56,与 SQL ä¸€è‡´ |
| 1-8月累计(c23) | 744262.86 â†’ 733956.06,差额 = è¢«åˆ é¡¹ç›® year_cum ä¹‹å’Œï¼Œè¡¥è®¡æœªæ±¡æŸ“该列 |
| ä¼ªé¡¹ç›®æ¸…除 | å’¸å®å®¢è¿å°è®¡ 27454 â†’ 13727 |
| ç”Ÿæˆè¡¨è¡Œæ•° | 313 â†’ 281 |
| å…¶å®ƒæŠ•资报表回归 | investBillion/County/FiveYearLogistics å„ 136 è¡Œã€ä¸¤å¼ æ˜Žç»†è¡¨ 39/97 è¡Œï¼Œready=true,均可导出 |
| è´Ÿå‘ | `types=foo` â†’ ready=false,note「未知报表类型:foo」 |
《功能测试报告_汇总大表异步导出与缓存_2026-09-20.md》:**17 æ¡ç”¨ä¾‹é€šè¿‡ï¼ˆæŽ¥å£ 8 + æµè§ˆå™¨å®žæµ‹ 7 + å¤éªŒ 2)**。
| æ ¡éªŒé¡¹ | ç»“æžœ |
| --- | --- |
| æäº¤å¼‚步任务 | 0.07s è¿”回 taskId;进度 6→22→45→70→95→100;14.4s å®Œæˆ |
| ä»»åŠ¡æ–‡ä»¶ä¸‹è½½ | 1,325,109 å­—节,1.27s |
| 10 åˆ†é’Ÿå†…缓存复用 | fromCache=true、同 taskId、0.01s |
| refresh=true å¼ºåˆ¶é‡ç®— | æ–°ä»»åŠ¡å·ï¼ŒçœŸå®žé‡ç®— 14.2s |
| è¿è¡Œä¸­å¹¶å‘去重 | ä¸¤æ¬¡æäº¤è¿”回同一 taskId(11.1s) |
| æ–°å¢žå¯¼å…¥ä½¿ç¼“存失效 | import_batch id 533→534 åŽ fromCache=false(测试后已 DELETE æ¸…回 533) |
| è´Ÿå‘:不存在 / æœªå®Œæˆ taskId | ã€Œå¯¼å‡ºä»»åŠ¡ä¸å­˜åœ¨æˆ–å·²è¿‡æœŸã€ã€Œå°šæœªå®Œæˆï¼ˆå½“å‰çŠ¶æ€ï¼šRUNNING)」 |
| æµè§ˆå™¨å®žæµ‹ | è¿›åº¦å¼¹çª—采样、自动下载、缓存提示、重新生成、后台运行、失败提示 6 é¡¹å…¨é€šè¿‡ |
| äº§ç‰©ä¸€è‡´æ€§ | ç¼“存件与重算件解包后逐条目一致;三份缓存件 SHA256 ç›¸åŒ |
| é™„带修复 | `computed` é‡å¤é”®å¯¼è‡´æŠ¥è¡¨ç”Ÿæˆé¡µæ•´é¡µç™½å±ï¼ˆå·²ä¿®å¤å¹¶æµè§ˆå™¨å¤éªŒï¼‰ |
| å¤éªŒï¼ˆç”¨æˆ·é—®é¢˜ 2) | é¢„览同比 12 è¡Œé½å…¨ï¼›ã€Œç´¯è®¡è´§ç‰©å‘¨è½¬é‡ 0%」经查库为取数源本身 `total_yoy=0.00000346`,非缺陷 |
《功能测试报告_市州统一填报模板与县市区解析_2026-09-20.md》:**10 æ¡ç”¨ä¾‹é€šè¿‡ï¼ˆæ­£å‘ 5 + è´Ÿå‘ 3 + å›žå½’ 2)**。
| æ ¡éªŒé¡¹ | ç»“æžœ |
| --- | --- |
| æ¨¡æ¿ç»“æž„ | ä¸¤ä»½æ¨¡æ¿å‡ä¸ºã€ŒæœˆæŠ¥ã€17 åˆ—表头 + å»ºè®¾æ€§è´¨ä¸‹æ‹‰ + å¡«æŠ¥è¯´æ˜Ž 11 æ¡ |
| æ­£å‘导入(模板填报) | total=2、success=2、city=武汉市 |
| ä¸»æ¡£é€å­—段 | åŽ¿åŒºï¼ˆæ±Ÿå²¸åŒº/洪山区)、建设单位、性质、开工/竣工年、总投资、工可/初设全部对齐 |
| æœˆåº¦æ•°é€å­—段 | è‡ªå¼€å§‹ç´¯è®¡/本年计划/自年初累计/本月完成/建设阶段/形象进度全部对齐,报表期 2026-09 |
| è´Ÿå‘:说明页被当数据表 | ä¿®å¤å‰ 8 æ¡è¯¯è§£æžï¼ˆid 233~240)→ ä¿®å¤åŽ 0 æ¡ |
| è´Ÿå‘:县区列写法变体 | ã€ŒåŽ¿ï¼ˆå¸‚ã€åŒºï¼‰/县(市、区)/县市区」归一化后均命中 |
| å›žå½’:8 æœˆçœŸå®žå¸‚州重导 | å®¢è¿ 39、物流 128,与 09-19 æ‰¹æ¬¡ä¸€è‡´ï¼Œfail=0 |
| å›žå½’:8 æœˆæœˆåº¦æ•°æ¯”对 | é‡å¯¼å‰åŽ 136 è¡Œé€å€¼ **IDENTICAL** |
| æ•°æ®æ¸…理 | æµ‹è¯•项目 A/B、2026-09 æ‰¹æ¬¡ã€æµ‹è¯•文件均已删除 |
## ä¸‰ã€é—留问题与待办
1. æ­¦æ±‰ç‰©æµé¡¹ç›®æ¸…单口径(已确认忽略)。
2. å¤©é—¨ç‰©æµæºæ–‡ä»¶ H/J/K ä¸ºç©º â†’ å¾…用户找同事要数据后导入。
3. è†å·ž/黄石「规划内/规划外」拆分口径 â†’ å¾…用户向同事确认。
4. ~~C ç»„(城市客运页结构归一,需用户先在 WPS æ•´åˆ—删除 AJ åˆ—)~~ **已完成**(改用 Excel COM æ•´åˆ—删除 + å…¬å¼è¿˜åŽŸï¼‰ï¼›
   D ç»„(增速排名措辞)/B4(导出前基数体检 + å¼¹çª— + ã€Šæ•°æ®ç¼ºå¤±æ¸…单》页)待排期。
5. ç¼ºæ¯ç‰ˆæç¤ºï¼ˆT08)未实测,可在有测试副本时补测。
6. ~~汇总大表导出是同步请求,建议做成异步任务 + è¿›åº¦æç¤º~~ **已完成**(见 1.7)。缓存是内存态,进程重启即失效;如需跨重启/多人共用的「导出记录」页可后补。
7. éƒ¨ç½²åŒ… `deploy/traffic-audit-deploy-20260913/backend/docs/生成汇总大表/` ä¸Ž `_deploytest/` ä¸‹ä»æ˜¯**带 AJ åˆ—的旧母版** â€”—
   ç”¨æˆ· 2026-09-20 æ˜Žç¡®ã€Œéƒ¨ç½²åŒ…先不管」,**暂不处理**(待要发包时再同步)。
8. ä¸­å£å¾„明细 4 æ ¼ï¼ˆåŸŽé™…城乡巡游出租/网约车)因 2025å¹´8月基数为 0,同比显示 `#DIV/0!`(不可比),如需改空白需用户确认口径。
## å››ã€æ˜Žæ—¥è®¡åˆ’
1. ç­‰å¤©é—¨æºæ•°æ® / è†å·žé»„石口径确认后补导并复验。
2. æŽ¨è¿› C/D ç»„与 B4。
3. ä¸Žç”¨æˆ·ç¡®è®¤æ˜¯å¦æŠŠ 09-18 å¤œé—´èµ·çš„æœ¬åœ°æäº¤ä¸€å¹¶æŽ¨é€åˆ°å…¬å¸ gitea(用户 09-19 æ›¾è¦æ±‚「今天先不推公司」)。
4. ~~评估汇总大表导出异步化~~ **已完成**(见 1.7:异步任务 + è¿›åº¦å¼¹çª— + 10 åˆ†é’Ÿç»“果缓存 + æ•°æ®ç‰ˆæœ¬å®ˆå«ï¼‰ï¼›éƒ¨ç½²åŒ…母版按用户要求暂不处理。
9. **8 æœˆç‰ˆåå °æ±‡æ€»è¡¨æœªæ¸…理**:用户只清了 7 æœˆç‰ˆï¼ˆ108032→90112 å­—节),8 æœˆç‰ˆï¼ˆmtime 2026-08-20)仍含武汉 27 + é»„石 4 æ®µï¼›
   è‹¥é‡å¯¼ 8 æœˆç‰©æµï¼Œå·²åˆ é™¤çš„ 31 ä¸ªé¡¹ç›®ä¼šé‡æ–°å»ºæ¡£ â†’ é‡å¯¼å‰éœ€åŒæ ·æ¸…理(或由我代做,待用户确认)。
    **2026-09-20 ä¸‹åˆåšå›žå½’重导时已实测复现**(8 æœˆç‰©æµé‡å¯¼åŽ +31 ä¸ªé¡¹ç›® / +31 æ¡æœˆåº¦æ•°ï¼‰ï¼Œå·²å†æ¬¡æ¸…除,库回到 152 ä¸ªé¡¹ç›® / 284 æ¡æœˆåº¦æ•°ï¼Œ2026-08 æœˆåº¦æ•°é€å€¼ä¸Žé‡å¯¼å‰ä¸€è‡´ã€‚
10. å¸‚州单表导入的**多市州块兜底提示**(识别一份文件含多个市州块并提示/跳过)待用户确认是否要做。
11. ç”Ÿæˆè¡¨æ­¦æ±‰ç‰©æµå°è®¡çŽ°åœ¨åªæœ‰ã€Œæ€»æŠ•èµ„ã€ï¼Œç¼º 8 æœˆçš„自开始累计/本年计划/月完成 â†’ éœ€æ­¦æ±‰ 8 æœˆç‰©æµå•表(或省厅口径)才能补齐。
12. ä¸¤ä»½å¸‚州填报模板待用户过目后下发市州(建议 9 æœˆèµ·æ‰§è¡Œï¼‰ï¼›å¦‚需「模板体检 + è¡¨å¤´ä¸ç¬¦å¼¹çª—」,待用户确认后排期。
13. 8 æœˆåå °ç‰©æµæ±‡æ€»è¡¨æºæ–‡ä»¶ï¼ˆå«æ­¦æ±‰ 27 + é»„石 4)是否由我代做清理,待用户确认(见 1.8 / æµ‹è¯•报告 P3)。
docs/Ͷ×Ê/Ì¿Ú¾¶Óëͳһģ°å_2026-09-20.md
New file
@@ -0,0 +1,78 @@
# å¸‚州月报填报口径与统一模板(2026-09-20)
> èµ·å› ï¼šç”¨æˆ·æé—®ã€Œä»–们导入的数据按哪个格式来最好?要不要生成两个模板(客运站一个、物流园一个)?
> è¿™å‡ ä¸ªè¡¨çš„æ›´æ–°å…¶å®žå¾ˆç®€å•——按项目名称更新汇总表指定指标、新增项目就新增到明细表,
> é¢„安排汇总表和明细表方式一样,剩下三张表项目固定只更新指标——为什么现在弄得有点复杂了?」
## ä¸€ã€ç»“论先说
1. **你的心智模型是对的**,而且就是需求文档《投资月报数据处理与多表更新需求说明》里写的三步:
   - Step1 æœˆæŠ¥ â†’ ä¸¤å¼ æ˜Žç»†è¡¨ï¼šä»¥**项目名称**为键,已有→更新 5 åˆ—,没有→新增整行;
   - Step2 æ˜Žç»†è¡¨ â†’ å…¨çœé¢„安排计划进度情况汇总:同名更新 4 é¡¹ï¼Œæ²¡æœ‰çš„æ–°å¢žï¼Œæ¯ç‰ˆæœ‰è€ŒæœˆæŠ¥æ²¡æœ‰çš„保持原值;
   - Step3 æ˜Žç»†è¡¨ â†’ ä¸‰å¼ å›ºå®šé¡¹ç›®è¡¨ï¼ˆåäº”五规划物流 / äº¿å…ƒæŠ•资项目 / ç»æµŽå¼ºåŽ¿ï¼‰ï¼šé¡¹ç›®æ¸…å•ä¸åŠ¨ï¼Œåªæ›´æ–° 3 é¡¹æŒ‡æ ‡ã€‚
2. **复杂不在「表」,在「源」**。程序侧要处理的麻烦全部来自市州报上来的原始表格格式不统一,不来自汇总规则本身。
3. **建议从 9 æœˆèµ·ç»Ÿä¸€ç”¨ä¸¤ä»½æ¨¡æ¿**(客运站场、物流园区各一份)。模板已经做好:
   - `docs/投资/模板/模板_市州月报_客运站场.xlsx`
   - `docs/投资/模板/模板_市州月报_物流园区.xlsx`
## äºŒã€ä¸¤ä»½æ¨¡æ¿é•¿ä»€ä¹ˆæ ·
只有一个数据页「月报」+ ä¸€ä¸ªã€Œå¡«æŠ¥è¯´æ˜Žã€é¡µã€‚第 4 è¡Œæ˜¯è¡¨å¤´ï¼Œä¸€è¡Œä¸€æ¡é¡¹ç›®ï¼Œå…± 17 åˆ—:
| åˆ— | è¡¨å¤´ | ç¨‹åºé‡Œçš„去向 |
| --- | --- | --- |
| A | åºå· | ä»…用于识别市州小节行/合计行,不参与匹配 |
| B | åŽ¿ï¼ˆå¸‚ã€åŒºï¼‰ | é¡¹ç›®ä¸»æ¡£ `county`(全省汇总表要显示这一列) |
| C | **项目名称及站级** | **匹配键**(市州 + ç±»åˆ« + é¡¹ç›®åç§°ï¼‰ |
| D | æŠ•资建设单位名称 | ä¸»æ¡£ `builder_name` |
| E | å»ºè®¾æ€§è´¨ | ä¸»æ¡£ `construct_nature`(下拉:新建/续建/改扩建/改建) |
| F | å¼€å·¥å¹´ | ä¸»æ¡£ `start_time` |
| G | ç«£å·¥å¹´ | ä¸»æ¡£ `end_time` |
| H | è®¡åˆ’总投资(万元) | ä¸»æ¡£ `total_investment` |
| I | è‡ªå¼€å§‹å»ºè®¾ç´¯è®¡å®ŒæˆæŠ•资(万元) | **当期月度数** |
| J | æœ¬å¹´è®¡åˆ’投资(万元) | **当期月度数** |
| K | è‡ªå¹´åˆç´¯è®¡å®ŒæˆæŠ•资(万元) | **当期月度数** |
| L | å…¶ä¸­ï¼šæœ¬æœˆå®ŒæˆæŠ•资(万元) | **当期月度数** |
| M | å»ºè®¾é˜¶æ®µ | **当期月度数** |
| N | å½¢è±¡è¿›åº¦ | **当期月度数** |
| O | å·¥å¯æ‰¹å¤æ–‡å· | ä¸»æ¡£ `approval_gk` |
| P | åˆè®¾/施工图批复文号 | ä¸»æ¡£ `approval_cs` |
| Q | å¤‡æ³¨ | ä¸å–æ•° |
- **主档列(A、B、D、E、F、G、H、O、P)**:第一次建档时写入,之后只要项目名不变就不动;
- **月度数(I、J、K、L、M、N)**:每个月报多少写多少,同一(项目,报表期)覆盖式重建;
- **「项目名称及站级」是唯一匹配键**:改一个字 = æ–°å»ºä¸€ä¸ªé¡¹ç›®ï¼Œæ‰€ä»¥è¦æ±‚与批复名称一字不差;
- ã€Œå¡«æŠ¥è¯´æ˜Žã€é¡µå†™äº† 11 æ¡è¦æ±‚(一份文件只放本市州、文件名带市州名、数值不带字符、
  åŽ¿ï¼ˆå¸‚ã€åŒºï¼‰å¿…å¡«ã€å»ºè®¾æ€§è´¨åªèƒ½å››é€‰ä¸€ã€æ–°å¢žé¡¹ç›®ç›´æŽ¥åŠ è¡Œã€éšè—è¡Œä¸æ”¾æ•°æ®ã€åˆè®¡è¡Œå¯ä¸å¡«ç­‰ï¼‰ã€‚
## ä¸‰ã€ä¸ºä»€ä¹ˆçŽ°åœ¨æ„Ÿè§‰å¤æ‚ï¼ˆ4 ä¸ªçœŸå®žåŽŸå› ï¼Œéƒ½åœ¨è¾“å…¥ä¾§ï¼‰
1. **版式不统一**:27 ä»½å¸‚州月报里,20 ä»½ä¸Žçœç»Ÿä¸€æ¨¡æ¿ä¸€è‡´ï¼Œ7 ä»½æœ‰å·®å¼‚——
   éšå·žå¤šä¸€ä¸ª Sheet2、神农架/潜江/襄阳拆成多个分月页签、襄阳整体右移 1 åˆ—、
   æ©æ–½/黄石合并表头、武汉(南山/汉口)只报两个项目的简表。详见
   `docs/投资/输出/市州月报格式差异报告_2026-07.md`。程序为了读全,只能一个版式一个兜底分支。
2. **一个文件里混多个市州**:《湖北省十堰交通物流基础设施投资统计汇总表(月报)》一个页签收了全省,
   2026 å¹´ 8 æœˆè¿™ä»½ 38 è¡Œé‡Œåªæœ‰ **7 è¡Œæ˜¯åå °**,另外 **27 è¡Œæ­¦æ±‰ + 4 è¡Œé»„石**。
   ç¨‹åºæ”¯æŒæŒ‰ã€Œå°èŠ‚è¡Œã€åˆ‡æ¢å¸‚å·žï¼ˆè¿™æ˜¯æœ‰æ„åšçš„åŠŸèƒ½ï¼‰ï¼Œæ‰€ä»¥èƒ½è¯»è¿›æ¥ï¼Œä½†ä»£ä»·æ˜¯ï¼š
   **只要重导 8 æœˆç‰©æµï¼Œè¿™ 31 ä¸ªé¡¹ç›®å°±ä¼šé‡æ–°å»ºåˆ°æ­¦æ±‰/黄石名下**(今天做回归重导时复现,已再次清除,见测试报告 P3)。
3. **项目清单有两个来源**:一是市州当月报上来的,二是母版/库里已经有的(如预安排母版里的武汉物流行)。
   äºŽæ˜¯äº§ç”Ÿã€Œæœ‰æ€»æŠ•资、没有本期月度数」的项目,要不要补进小计/合计就成了要单独决策的事。
4. **列位置靠兜底**:早期实现按「自年初累计 = è‡ªå¼€å§‹å»ºè®¾ç´¯è®¡ + 2 åˆ—」这类列序推算,
   å¸‚州一挪列就得再加一条兜底——这是「猜」出来的复杂度,不是业务复杂度。
## å››ã€æ¢æˆç»Ÿä¸€æ¨¡æ¿åŽï¼Œè¿˜è¦å®šæ­» 3 ä»¶äº‹
模板解决的是「怎么读」。下面 3 ä»¶äº‹å®šä¸‹æ¥ï¼Œæ•´æ¡é“¾å°±æ²¡æœ‰æ­§ä¹‰äº†ï¼š
| # | å¾…定事项 | å»ºè®® |
| --- | --- | --- |
| 1 | å¹´åº¦é¡¹ç›®æ¸…单以谁为准?库里有什么就出什么,还是以「年度预安排计划表」为准? | ä»¥å¹´åº¦é¢„安排计划表为准(随母版下发或单独建一张《年度预安排计划》表),每月只更新指标 |
| 2 | æŸå¸‚州当月**没报**怎么办? | è¯¥å¸‚州指标留空 + å¯¼å‡ºå‰å¼¹çª—列出「未报送市州」,不阻断导出 |
| 3 | å¸‚å·ž**新增**的项目插到哪? | æ’到该市州「预安排计划内」小节末尾;计划外的单独标注,导向上仍出但明显区别于计划内 |
## äº”、过渡安排与下一步
- **8 æœˆåŠä»¥å‰**:已按旧格式导入完毕,程序继续兼容旧版式,**不改、不重导**(8 æœˆåå °æºæ–‡ä»¶æ¸…理另行确认)。
- **9 æœˆèµ·**:市州按统一模板填报;建议再加一个「模板体检」——导入时校验表头是否为本模板 17 åˆ—,
  æ˜¯åˆ™èµ°ã€ŒæŒ‰è¡¨å¤´å–数」的直路,不是则**弹窗提示**(哪一列对不上),而不是继续猜列位置。
- è¿™æ ·è§£æžé€»è¾‘可以退化成「按表头名取值」,现有那些容错兜底分支可以逐步下线,复杂度自然就降下来了。
docs/Ͷ×Ê/Ä£°å/Ä£°å_ÊÐÖÝÔ±¨_¿ÍÔËÕ¾³¡.xlsx
Binary files differ
docs/Ͷ×Ê/Ä£°å/Ä£°å_ÊÐÖÝÔ±¨_ÎïÁ÷Ô°Çø.xlsx
Binary files differ
docs/Ͷ×Ê/ÊäÈë/¿ÍÔË£¨¿ÍÔËÕ¾£©/7ÔÂ/Ê®ÑßÊÐ2026Äê7Ô¹̶¨×ʲúÔ±¨µç×Ó°æÍ¶×ʱí¸ñ.xlsx
Binary files differ
docs/Ͷ×Ê/ÊäÈë/ÎïÁ÷£¨ÎïÁ÷Ô°Çø£©/7ÔÂ/ºþ±±Ê¡Ê®Ñß½»Í¨ÎïÁ÷»ù´¡ÉèʩͶ×Êͳ¼Æ»ã×Ü±í£¨Ô±¨£©£¨2026Äê7Ô£©.xls
Binary files differ
docs/Ͷ×Ê/Êä³ö/ÇåÀí²ÐÁô_Ê®Ñß»ã×Ü±í´ø³öµÄ·ÇÊ®ÑßÏîÄ¿_2026-09-20.tsv
New file
@@ -0,0 +1,81 @@
## å¤‡ä»½æ—¶é—´ 2026-09-20  åŽŸå› ï¼šåå °æ±‡æ€»è¡¨(2026å¹´5/7/8月版)内含武汉27个+黄石4个非十堰老项目,2026-08-26 11:57 éšã€Œå¸‚州单表批量导入」入库,现按用户要求清除
## investment_project
id    project_name    category    city    county    builder_name    construct_nature    start_time    end_time    total_investment    is_billion    is_county    is_five_year    source    import_batch_id    created_at    updated_at
122    é«˜æ¡¥è´§è¿ç«™    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ­¦æ±‰ä¸œè¥¿æ¹–保税物流投资有限公司    ç»­å»º    2006    2020    45482    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 16:37:31
123    è“ç„°ç‰©æµåŸºåœ°    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ­¦æ±‰è“ç„°ç‰©æµåŸºåœ°è‚¡ä»½æœ‰é™å…¬å¸    ç»­å»º    2011    2015    110000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
124    æ±‰å£åŒ—国际多式联运物流港    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ¹–北省交通投资有限公司    æ–°å»º    2016    2020    300000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
125    æ­¦æ±‰æ­¦æ¹–城市配送中心    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ¹–北萃元农产品冷链物流公司    æ–°å»º    2015    2017    30000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
126    æ­¦æ±‰æ·åˆ©é€šè¾¾çŽ°ä»£ç‰©æµ    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ­¦æ±‰æ·åˆ©é€šè¾¾ç‰©æµæœ‰é™å…¬å¸    ç»­å»º    2013    2016    50000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
127    æ­¦æ±‰é›†è£…箱公路中转中心    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ¹–北省港发集团    æ–°å»º    2016    2020    75000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
128    æ­¦æ±‰ä¸œè¥¿æ¹–物流基地    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ±‡é€šç½®ä¸šè‚¡ä»½æœ‰é™å…¬å¸    æ–°å»º    2012    2016    130000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
129    æ­¦æ±‰ç‰©æµäº¤æ˜“所(总部)    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ­¦æ±‰äº¤æŠ•物流交易中心有限公司    æ–°å»º    2016    2020    270000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
130    æ­¦æ±‰ç¾Žå®‰çŽ°ä»£ç‰©æµåŸŽ    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ­¦æ±‰ç¾Žå®‰å‚¨è¿æœ‰é™å…¬å¸    æ–°å»º    2014    2016    110000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
131    å“尔陆港中心    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    å“尔陆港中心投资有限公司    æ–°å»º    2014    2017    400000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
132    æ­¦æ±‰æ–°æ¸¯é˜³é€»æ¸¯å£ç‰©æµå›­    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ­¦æ±‰æ–°æ¸¯é˜³é€»ä¿ç¨Žå›­åŒºç®¡ç†æœ‰é™å…¬å¸    æ–°å»º    2014    2017    108994    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
133    æµ·å‰æ˜Ÿå›½é™…绿色农港    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ­¦æ±‰åŸŽå¸‚圈海吉星农产品物流有限公司    æ–°å»º    2014    2020    800000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
134    æ­¦æ±‰ï¼ˆæ±Ÿå¤ï¼‰å¾·é‚¦ç‰©æµäº§ä¸šå›­    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    å¾·é‚¦ç‰©æµ    æ–°å»º    2015    2017    137000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
135    æ­¦æ±‰åŽè¿è¾¾ç‰©æµåŸºåœ°é¡¹ç›®    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ­¦æ±‰åŽè¿è¾¾ç‰©æµæœ‰é™å…¬å¸    æ–°å»º    2015    2017    30000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
136    é‡‘港新区汽车物流园    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ­¦æ±‰æ–°é¸¿ç››ç‰©æµæœ‰é™å…¬å¸    æ–°å»º    2015    2017    64000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
137    é¡ºä¸°é€Ÿé€’武汉电子商务物流中心    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ­¦æ±‰ä¸°æ³°ç”µå•†äº§ä¸šå›­ç®¡ç†æœ‰é™å…¬å¸    æ–°å»º    2015    2017    100000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
138    å´å®¶å±±é“è·¯ç‰©æµå›­    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ­¦æ±‰é“è·¯å±€    æ–°å»º    2015    2018    35000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
139    æ’基达鑫化工物流    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ¹–北金腾兴有限公司    æ–°å»º    2013    2018    204000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
140    ä¸œæ¹–综合保税区食品保税物流园    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ­¦æ±‰é‡‘宇综合保税发展有限公司    æ–°å»º    2015    2017    124000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
141    äº¬ä¸œå•†åŸŽåŽä¸­åŒºåŸŸæ€»éƒ¨    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    åŒ—京京东世纪贸易有限公司    æ–°å»º    2014    2018    700000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
142    äº’联网+物流信息交易平台    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ­¦æ±‰æ·åˆ©ç‰©æµæœ‰é™å…¬å¸    æ–°å»º    2015    2016    31792    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
143    é»‘石新地电子商务物流中心    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ­¦æ±‰é«˜æ¡¥æ–°åœ°ç‰©æµæœ‰é™å…¬å¸    æ–°å»º    2015    2016    200000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
144    æ±‰å£åŒ—公铁联运中心    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    çœäº¤æŠ•    æ–°å»º    2016    2018    200000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
145    æ±‰å£åŒ—高新物流示范园(武湖货运站)    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ¹–北省汽车运输总公司    æ–°å»º    2012    2017    114500    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
146    å†›å±±è´§è¿ç«™    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ­¦æ±‰å®æ¹¾å›½é™…物流有限公司    æ–°å»º    2013    2015    102000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
147    æ±‰å£åŒ—铁路物流中心(2期)    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ­¦æ±‰ä¸­åŠ›ç‰©æµæœ‰é™å…¬å¸    æ–°å»º    2015    2017    88500    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
148    è¶Šæµ·åŽä¸­ä¾›åº”链总部基地    ç‰©æµå›­åŒº    æ­¦æ±‰å¸‚    NULL    æ·±åœ³å¸‚越海全球物流有限公司    æ–°å»º    2015    2018    150000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-01 15:54:17
149    é»„石罗桥物流园    ç‰©æµå›­åŒº    é»„石市    NULL    æ¹–北天海益达物流有限公司    æ–°å»º    2012    2017    39119    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-19 00:11:41
150    é»„石花湖物流中心    ç‰©æµå›­åŒº    é»„石市    NULL    æ¹–北广发物流有限公司    æ–°å»º    2012    2016    23586    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-19 00:11:41
151    é»„石棋盘洲综合物流园一期(棋盘洲保税物流中心)    ç‰©æµå›­åŒº    é»„石市    NULL    é»„石港通物流发展有限公司    æ–°å»º    2013    2018    28679    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-19 00:11:41
152    é˜³æ–°åŽ¿ç‰©æµä¸­å¿ƒ    ç‰©æµå›­åŒº    é»„石市    NULL    é˜³æ–°åŽ¿äº¤é€šæŠ•èµ„å…¬å¸    æ–°å»º    2015    2018    23000    0    0    0    å¸‚州单表    NULL    2026-08-26 11:57:10    2026-09-19 00:11:41
## investment_monthly
id    report_period    project_id    project_name    year_plan    year_cum    month_done    start_cum    progress_stage    LEFT(m.progress_desc,60)    building_area    created_at
3000    2026-08    122    é«˜æ¡¥è´§è¿ç«™    0    0    0    40148    NULL    ä¸‰æœŸè§„划中    NULL    2026-09-19 00:20:08
3001    2026-08    123    è“ç„°ç‰©æµåŸºåœ°    1500    500    500    89608    NULL    åŸŽå¸‚配送中心建设已完成至100%,相关市政配套工程完成100%。现因三环线辅道征收拆迁事宜致使证件办理工作延期取得目二期    NULL    2026-09-19 00:20:08
3002    2026-08    124    æ±‰å£åŒ—国际多式联运物流港    1000    0    0    0    NULL    æ´½è°ˆä¸­    NULL    2026-09-19 00:20:08
3003    2026-08    125    æ­¦æ±‰æ­¦æ¹–城市配送中心    6000    0    0    13000    NULL    ä»“库在建中    NULL    2026-09-19 00:20:08
3004    2026-08    126    æ­¦æ±‰æ·åˆ©é€šè¾¾çŽ°ä»£ç‰©æµ    14000    1100    1100    26000    NULL    ç›®å‰äºŒæœŸåœ¨åŠžç†å¼€å·¥å‰æ‰‹ç»­ï¼Œå·²å®ŒæˆåŒºåŸŸåœŸåœ°æ‹†è¿ã€å¹³æ•´å·¥ä½œã€‚    NULL    2026-09-19 00:20:08
3005    2026-08    127    æ­¦æ±‰é›†è£…箱公路中转中心    1000    0    0    0    NULL    ç”±æ­¦æ±‰æ–°æ¸¯å§”牵头武汉新港空港综合保税区扩大范围的认证问题;对接江北铁路二期建设;支持新港企业发展等。现已正式启动该项目的    NULL    2026-09-19 00:20:08
3006    2026-08    128    æ­¦æ±‰ä¸œè¥¿æ¹–物流基地    12000    1000    1000    71657    NULL    å•†æœåŒºåŒ—片7#楼封顶、6#楼二层结构、5#楼地下室封顶、4#楼正负零,停车场B区施工。    NULL    2026-09-19 00:20:08
3007    2026-08    129    æ­¦æ±‰ç‰©æµäº¤æ˜“所(总部)    20000    0    0    1000    NULL    é¡¹ç›®å¯è¡Œæ€§ç ”究报告及建筑方案设计正在编制中    NULL    2026-09-19 00:20:08
3008    2026-08    130    æ­¦æ±‰ç¾Žå®‰çŽ°ä»£ç‰©æµåŸŽ    0    0    0    30156    NULL    2#楼内部装修完毕,已开始试运营,1#楼开始打桩,物流地块已开始设计;兴建停车场    NULL    2026-09-19 00:20:08
3009    2026-08    131    å“尔陆港中心    20000    400    400    51900    NULL    æ­£åœ¨è¿›è¡Œå›­åŒºé“路铺设,围网工程    NULL    2026-09-19 00:20:08
3010    2026-08    132    æ­¦æ±‰æ–°æ¸¯é˜³é€»æ¸¯å£ç‰©æµå›­    30000    865.6    865.6    44157    NULL    å®Œæˆå›­åŒºåœŸåœ°å¹³æ•´ã€åœ°ä¸‹è®¾æ–½ã€å›­åŒºé“路、桩基及围网的基础施工    NULL    2026-09-19 00:20:08
3011    2026-08    133    æµ·å‰æ˜Ÿå›½é™…绿色农港    36000    2865.2    2865.2    66092.26    NULL    21栋蔬菜交易大厅、配套楼A栋及电商中心工程竣工交付并开业。现已启动二期建设    NULL    2026-09-19 00:20:08
3012    2026-08    134    æ­¦æ±‰ï¼ˆæ±Ÿå¤ï¼‰å¾·é‚¦ç‰©æµäº§ä¸šå›­    1000    0    0    0    NULL    æœ‰å…³æ‰‹ç»­å·²åŠžé½ï¼Œå·²å®ŒæˆåœŸåœ°æ‹›æ‹æŒ‚å·¥ä½œ    NULL    2026-09-19 00:20:08
3013    2026-08    135    æ­¦æ±‰åŽè¿è¾¾ç‰©æµåŸºåœ°é¡¹ç›®    0    0    0    5000    NULL    ç›®å‰å·²å–得土地149.9亩,土地证正在办理中,开展场平工作    NULL    2026-09-19 00:20:08
3014    2026-08    136    é‡‘港新区汽车物流园    0    0    0    0    NULL    å·²å®Œæˆåœ°è´¨å‹˜å¯Ÿ    NULL    2026-09-19 00:20:08
3015    2026-08    137    é¡ºä¸°é€Ÿé€’武汉电子商务物流中心    0    0    0    10000    NULL    1、项目用地已获批准,进入东西湖区土地储备中心,做“招拍挂”前期准备。    NULL    2026-09-19 00:20:08
3016    2026-08    138    å´å®¶å±±é“è·¯ç‰©æµå›­    0    0    0    5000    NULL    å®Œæˆå›­åŒºé“路建设,五层办公楼封顶    NULL    2026-09-19 00:20:08
3017    2026-08    139    æ’基达鑫化工物流    0    0    0    25888    NULL    è¯•运营    NULL    2026-09-19 00:20:08
3018    2026-08    140    ä¸œæ¹–综合保税区食品保税物流园    0    0    0    59000    NULL    å…³å†…241亩用地已开工建设    NULL    2026-09-19 00:20:08
3019    2026-08    141    äº¬ä¸œå•†åŸŽåŽä¸­åŒºåŸŸæ€»éƒ¨    20000    0    0    87800    NULL    å·²å®Œæˆ1-6#厂房主体钢结构安装,设备安装调试;1#、4#厂房围护结构安装;食堂封顶,宿舍1#楼施工完成5层,宿舍地下室    NULL    2026-09-19 00:20:08
3020    2026-08    142    äº’联网+物流信息交易平台    0    0    0    8000    NULL    å·²å»ºæˆ1000位座席及投入6000万元    NULL    2026-09-19 00:20:08
3021    2026-08    143    é»‘石新地电子商务物流中心    0    2000    2000    2000    NULL    æ­£åœ¨è¿›è¡Œæ‹†è¿ã€åœºå¹³ã€æ¸…淤换填、清除耕植土等场平工作    NULL    2026-09-19 00:20:08
3022    2026-08    144    æ±‰å£åŒ—公铁联运中心    20000    0    0    0    NULL    æ´½è°ˆä¸­    NULL    2026-09-19 00:20:08
3023    2026-08    145    æ±‰å£åŒ—高新物流示范园(武湖货运站)    15000    0    0    11629    NULL    å®Œæˆå››æ ‹1.25万平方米主体工程,其余项目用地正在落实中    NULL    2026-09-19 00:20:08
3024    2026-08    146    å†›å±±è´§è¿ç«™    1000    0    0    59050    NULL    ä¼ä¸šè¿›åœºè¯•运营    NULL    2026-09-19 00:20:08
3025    2026-08    147    æ±‰å£åŒ—铁路物流中心(2期)    30000    0    0    0    NULL    æ­£åœ¨è¿›è¡ŒäºŒæœŸæ–½å·¥è®¾è®¡å’Œæ–½å·¥æ‹›æŠ•标工作    NULL    2026-09-19 00:20:08
3026    2026-08    148    è¶Šæµ·åŽä¸­ä¾›åº”链总部基地    30000    0    0    0    NULL    åœºå¹³å…¨éƒ¨å®Œæˆ    NULL    2026-09-19 00:20:08
1624    2026-06    149    é»„石罗桥物流园    11200    69    69    18202    NULL    é›†è£…箱设备安装    NULL    2026-08-26 12:25:57
1998    2026-07    149    é»„石罗桥物流园    11200    69    69    18202    NULL    é›†è£…箱设备安装    NULL    2026-09-01 16:37:32
3027    2026-08    149    é»„石罗桥物流园    11200    69    69    18202    NULL    é›†è£…箱设备安装    NULL    2026-09-19 00:20:08
1625    2026-06    150    é»„石花湖物流中心    1700    0    0    20300    NULL    åŸºæœ¬å»ºæˆï¼Œæ­£åœ¨è¿›è¡ŒäºŒæœŸç»¼åˆæ¥¼å¤–墙装修    NULL    2026-08-26 12:25:57
1999    2026-07    150    é»„石花湖物流中心    1700    0    0    20300    NULL    åŸºæœ¬å»ºæˆï¼Œæ­£åœ¨è¿›è¡ŒäºŒæœŸç»¼åˆæ¥¼å¤–墙装修    NULL    2026-09-01 16:37:32
3028    2026-08    150    é»„石花湖物流中心    1700    0    0    20300    NULL    åŸºæœ¬å»ºæˆï¼Œæ­£åœ¨è¿›è¡ŒäºŒæœŸç»¼åˆæ¥¼å¤–墙装修    NULL    2026-09-19 00:20:08
1626    2026-06    151    é»„石棋盘洲综合物流园一期(棋盘洲保税物流中心)    5000    0    0    23730    NULL    ä¿ç¨Žç‰©æµä¸­å¿ƒé€šè¿‡å›½å®¶éªŒæ”¶    NULL    2026-08-26 12:25:57
2000    2026-07    151    é»„石棋盘洲综合物流园一期(棋盘洲保税物流中心)    5000    0    0    23730    NULL    ä¿ç¨Žç‰©æµä¸­å¿ƒé€šè¿‡å›½å®¶éªŒæ”¶    NULL    2026-09-01 16:37:32
3029    2026-08    151    é»„石棋盘洲综合物流园一期(棋盘洲保税物流中心)    5000    0    0    23730    NULL    ä¿ç¨Žç‰©æµä¸­å¿ƒé€šè¿‡å›½å®¶éªŒæ”¶    NULL    2026-09-19 00:20:08
1627    2026-06    152    é˜³æ–°åŽ¿ç‰©æµä¸­å¿ƒ    1000    0    0    1332    NULL    å‰æœŸå·¥ä½œ    NULL    2026-08-26 12:25:57
2001    2026-07    152    é˜³æ–°åŽ¿ç‰©æµä¸­å¿ƒ    1000    0    0    1332    NULL    å‰æœŸå·¥ä½œ    NULL    2026-09-01 16:37:32
3030    2026-08    152    é˜³æ–°åŽ¿ç‰©æµä¸­å¿ƒ    1000    0    0    1332    NULL    å‰æœŸå·¥ä½œ    NULL    2026-09-19 00:20:08
## è¿½åŠ ï¼šä¼ªé¡¹ç›®ã€Œåˆã€€ã€€ã€€è®¡ã€(咸宁市客运站场, id=229, è§£æžåˆè®¡è¡Œæ—¶è¯¯å»º, æ— æœˆåº¦æ•°)
id    project_name    city    category    source    total_investment    created_at
229    åˆ       è®¡    å’¸å®å¸‚    å®¢è¿ç«™åœº    å¸‚州单表    13727    2026-09-16 15:07:54
docs/Éú³É»ã×Ü´ó±í/2026Äê8ÔµÀ·ÔËÊäÁ¿»ã×ܱí.xlsx
Binary files differ
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java
@@ -2489,6 +2489,8 @@
            project.setProjectName(nt);
            project.setCategory(category);
            project.setCity(currentCity);
            String countyCell = cellStr(row, col.get("county"));
            if (countyCell != null && !countyCell.trim().isEmpty()) project.setCounty(countyCell.trim());
            project.setBuilderName(cellStr(row, col.get("builder")));
            project.setConstructNature(cellStr(row, col.get("nature")));
            project.setStartTime(investTime(cellStr(row, col.get("start"))));
@@ -2524,11 +2526,19 @@
            Row row = sh.getRow(r);
            if (row == null) continue;
            boolean hasName = false;
            int headerCells = 0;
            for (int c = 0; c <= row.getLastCellNum(); c++) {
                String v = cellStr(row, c);
                if (v != null && v.contains("项目名称")) { hasName = true; break; }
                if (v == null) continue;
                String t = v.trim();
                if (t.isEmpty() || t.length() > 20) continue;   // è¯´æ˜Žæ€§é•¿å¥ä¸ç®—表头字段
                String k = investHeaderKey(t);
                if (k == null) continue;
                headerCells++;
                if ("name".equals(k)) hasName = true;
            }
            if (!hasName) continue;
            // çœŸè¡¨å¤´è¡Œè‡³å°‘有 2 ä¸ªçŸ­è¡¨å¤´å­—段;只有一格长句含「项目名称」的说明页会被排除
            if (!hasName || headerCells < 2) continue;
            Map<Integer, String> colTexts = new HashMap<>();
            int scanEnd = investHeaderScanEnd(sh, r, row);
            for (int rr = r; rr <= scanEnd; rr++) {
@@ -2542,23 +2552,8 @@
            }
            Map<String, Integer> col = new HashMap<>();
            for (Map.Entry<Integer, String> e : colTexts.entrySet()) {
                int c = e.getKey();
                String v = e.getValue();
                if (v.contains("项目名称")) col.putIfAbsent("name", c);
                else if (v.contains("投资建设") || v.contains("建设单位") || v.contains("项目业主")) col.putIfAbsent("builder", c);
                else if (v.contains("建设性质")) col.putIfAbsent("nature", c);
                else if (v.contains("开工")) col.putIfAbsent("start", c);
                else if (v.contains("竣工") || v.contains("建成")) col.putIfAbsent("end", c);
                else if ((v.contains("计划总投资") || v.contains("总投资")) && !v.contains("累计")) col.putIfAbsent("total", c);
                else if (v.contains("自开始建设")) col.putIfAbsent("startcum", c);
                else if (v.contains("本年计划")) col.putIfAbsent("yearplan", c);
                else if (v.contains("自年初累计")) col.putIfAbsent("yearcum", c);
                else if (v.contains("本月完成")) col.putIfAbsent("monthdone", c);
                else if (v.contains("建设阶段")) col.putIfAbsent("stage", c);
                else if (v.contains("形象进度") || v.contains("建设进展")) col.putIfAbsent("desc", c);
                else if (v.contains("工可") && !v.contains("项目名称")) col.putIfAbsent("gk", c);
                else if (v.contains("初设") && !v.contains("项目名称")) col.putIfAbsent("cs", c);
                else if (v.contains("建筑面积") && !v.contains("新增")) col.putIfAbsent("area", c);
                String k = investHeaderKey(e.getValue());
                if (k != null) col.putIfAbsent(k, e.getKey());
            }
            if (!col.containsKey("name")) continue;
            for (int c = 0; c <= row.getLastCellNum(); c++) {
@@ -2582,7 +2577,7 @@
                if (dc <= row.getLastCellNum()) col.put("desc", dc);
            }
            for (String k : new String[]{"builder", "nature", "start", "end", "total", "startcum",
                "yearplan", "yearcum", "monthdone", "stage", "desc", "gk", "cs", "area"}) {
                "yearplan", "yearcum", "monthdone", "stage", "desc", "gk", "cs", "area", "county"}) {
                col.putIfAbsent(k, -1);
            }
            col.put("headerRow", r);
@@ -2591,6 +2586,31 @@
        return null;
    }
    /** å•个表头单元格 -> å­—段键(null è¡¨ç¤ºä¸æ˜¯è¡¨å¤´å­—段);供表头行判定与列映射共用 */
    private String investHeaderKey(String v) {
        if (v == null) return null;
        String flat = v.replace(" ", "");
        if (v.contains("项目名称")) return "name";
        // ã€ŒåŽ¿ï¼ˆå¸‚ã€åŒºï¼‰ã€çš„å„ç§å†™æ³•ï¼šåŠ/全角括号、顿号/逗号、有无括号,统一归一化后再判定
        String flatCounty = flat.replace("(", "(").replace(")", ")").replace("、", "").replace(",", "").replace(",", "");
        if (flatCounty.contains("县(市区)") || flatCounty.contains("县市区") || flatCounty.contains("区县")) return "county";
        if (v.contains("投资建设") || v.contains("建设单位") || v.contains("项目业主")) return "builder";
        if (v.contains("建设性质")) return "nature";
        if (v.contains("开工")) return "start";
        if (v.contains("竣工") || v.contains("建成")) return "end";
        if ((v.contains("计划总投资") || v.contains("总投资")) && !v.contains("累计")) return "total";
        if (v.contains("自开始建设")) return "startcum";
        if (v.contains("本年计划")) return "yearplan";
        if (v.contains("自年初累计")) return "yearcum";
        if (v.contains("本月完成")) return "monthdone";
        if (v.contains("建设阶段")) return "stage";
        if (v.contains("形象进度") || v.contains("建设进展")) return "desc";
        if (v.contains("工可") && !v.contains("项目名称")) return "gk";
        if (v.contains("初设") && !v.contains("项目名称")) return "cs";
        if (v.contains("建筑面积") && !v.contains("新增")) return "area";
        return null;
    }
    /** è¡¨å¤´æ–‡æœ¬æ‰«æä¸‹ç•Œï¼šé»˜è®¤è¡¨å¤´è¡Œ +3;若「项目名称」列首个数据行更靠下,则扩展到数据行前一行(上限 +12)。
     *  èƒŒæ™¯ï¼šå’¸å®/十堰等市州把「建设阶段/形象进度」标签写在表头下第 4~8 è¡Œï¼Œå›ºå®š +3 ä¼šæ¼åˆ—(形象进度读不到)。 */
    private int investHeaderScanEnd(Sheet sh, int headerRow, Row headerRowObj) {
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java
@@ -565,7 +565,7 @@
                if (target != null && target.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) continue;
                String f = prev.getCellFormula();
                if (f == null || f.isEmpty()) continue;
                String shifted = shiftFormulaColumns(f, 2);
                String shifted = shiftFormulaToNextMonth(sh, f, year, month);
                if (target == null) target = row.createCell(yoyCol);
                // æ¯ç‰ˆæŠŠæ–°æœˆä»½çš„同比列做成了普通数值格式(显示成小数),统一沿用上月同比列的样式(百分数)
                if (prev.getCellStyle() != null) target.setCellStyle(prev.getCellStyle());
@@ -615,6 +615,250 @@
        return sb.toString();
    }
    /** å…¬å¼å†… A1 åˆ—引用匹配(SUM ç­‰å‡½æ•°ååŽæ— æ•°å­—,不会被匹配) */
    private static final java.util.regex.Pattern A1_REF =
            java.util.regex.Pattern.compile("(?<![A-Za-z0-9_$])([$]?)([A-Z]{1,3})([$]?)([0-9]+)");
    /** â€œ2025å¹´8月” è¿™ç§å•月表头 */
    private static final java.util.regex.Pattern YM_HEADER =
            java.util.regex.Pattern.compile("^(\\d{4})å¹´(\\d{1,2})月$");
    /** â€œ2025å¹´1-7月累计” è¿™ç§åŒºé—´ç´¯è®¡è¾…助列(不属于任何单月) */
    private static final java.util.regex.Pattern YM_RANGE_CUM =
            java.util.regex.Pattern.compile("^\\d{4}å¹´\\d{1,2}-\\d{1,2}月累计$");
    /** A1 åˆ—字母 -> 0 åŸºåˆ—号(与 POI çš„ Cell.getColumnIndex() å¯¹é½ï¼ŒA=0、AJ=35) */
    private int colIndexOf(String letters) {
        int idx = 0;
        for (int i = 0; i < letters.length(); i++) idx = idx * 26 + (letters.charAt(i) - 'A' + 1);
        return idx - 1;
    }
    /** 0 åŸºåˆ—号 -> "Q" */
    private String colLettersOf(int idx0) {
        StringBuilder sb = new StringBuilder();
        int n = idx0 + 1;
        while (n > 0) {
            int rem = (n - 1) % 26;
            sb.insert(0, (char) ('A' + rem));
            n = (n - 1) / 26;
        }
        return sb.toString();
    }
    /** è¡¨å¤´ï¼ˆå‰ 5 è¡Œï¼‰å»ºç«‹ (å¹´*100+月) -> åˆ—号(0 åŸºï¼‰ï¼šè¯†åˆ« â€œ2025å¹´8月” æ–‡æœ¬æ ¼ä¸Ž 2025-08-01 è¿™ç±»æ—¥æœŸæ ¼ï¼›
     *  åŒä¸€ (å¹´,月) å–最左一列;“2025å¹´1-7月累计” è¿™ç±»åŒºé—´ç´¯è®¡è¾…助列不属于任何单月,不参与映射 */
    private Map<Integer, Integer> yearMonthCols(Sheet sh, int year) {
        Map<Integer, Integer> m = new LinkedHashMap<>();
        if (sh == null) return m;
        int last = Math.min(sh.getLastRowNum(), sh.getFirstRowNum() + 4);
        for (int r = sh.getFirstRowNum(); r <= last; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            for (int c = row.getFirstCellNum(); c >= 0 && c < row.getLastCellNum(); c++) {
                Integer ym = yearMonthOf(row.getCell(c), year);
                if (ym != null && !m.containsKey(ym)) m.put(ym, c);
            }
        }
        return m;
    }
    /**
     * å•个月份表头 -> å¹´*100+月;只认今年/去年,其余一律不认(避免把 2024/2023 å¯¹æ¯”块、杂格当月份列)。
     * ä¸¤ç§å†™æ³•:文本 â€œ2026å¹´8月”;数值日期序列(母版城市客运页 3..8 æœˆçš„表头是 46235 è¿™ç§è£¸åºåˆ—、
     * æ•°å­—格式还是 General,只按“当月 1 å·â€åˆ¤æ–­ï¼Œä¸èƒ½ä¾èµ– isCellDateFormatted)。
     */
    private Integer yearMonthOf(Cell cell, int year) {
        if (cell == null) return null;
        org.apache.poi.ss.usermodel.CellType t = cell.getCellType();
        if (t == org.apache.poi.ss.usermodel.CellType.STRING) {
            String v = cell.getStringCellValue();
            if (v == null || v.isEmpty()) return null;
            v = v.trim();
            if (v.contains("累计") || v.contains("同比")) return null;
            java.util.regex.Matcher m = YM_HEADER.matcher(v);
            if (!m.matches()) return null;
            int y = Integer.parseInt(m.group(1));
            if (y != year && y != year - 1) return null;
            return y * 100 + Integer.parseInt(m.group(2));
        }
        if (t == org.apache.poi.ss.usermodel.CellType.NUMERIC) {
            double v = cell.getNumericCellValue();
            if (v < 1 || v > 200000) return null;
            try {
                java.util.Calendar cal = java.util.Calendar.getInstance();
                cal.setTime(org.apache.poi.ss.usermodel.DateUtil.getJavaDate(v));
                if (cal.get(java.util.Calendar.DAY_OF_MONTH) != 1) return null;
                int y = cal.get(java.util.Calendar.YEAR);
                if (y != year && y != year - 1) return null;
                return y * 100 + (cal.get(java.util.Calendar.MONTH) + 1);
            } catch (Exception ignore) {
                return null;
            }
        }
        return null;
    }
    /** è¡¨å¤´é‡Œ â€œ20XXå¹´a-b月累计” è¿™ç±»åŒºé—´ç´¯è®¡è¾…助列的 0 åŸºåˆ—号集合 */
    private java.util.Set<Integer> cumulativeHelperCols(Sheet sh) {
        java.util.Set<Integer> set = new java.util.HashSet<>();
        if (sh == null) return set;
        int last = Math.min(sh.getLastRowNum(), sh.getFirstRowNum() + 4);
        for (int r = sh.getFirstRowNum(); r <= last; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            for (int c = row.getFirstCellNum(); c >= 0 && c < row.getLastCellNum(); c++) {
                Cell cell = row.getCell(c);
                if (cell == null || cell.getCellType() != org.apache.poi.ss.usermodel.CellType.STRING) continue;
                String v = cell.getStringCellValue();
                if (v != null && YM_RANGE_CUM.matcher(v.trim()).matches()) set.add(c);
            }
        }
        return set;
    }
    /** æœˆä»½æ˜ å°„的可用性守卫:去年与今年 1..month å„月列必须齐全且列号严格递增,
     *  ä»»ä¸€ä¸æ»¡è¶³ï¼ˆè¡¨å¤´æ²¡å†™æœˆä»½ã€æˆ–抓到杂格)就返回空表,调用方退回「整体右移 2 åˆ—」的旧行为 */
    private Map<Integer, Integer> usableMonthCols(Sheet sh, int year, int month) {
        Map<Integer, Integer> usable = new LinkedHashMap<>();
        Map<Integer, Integer> all = yearMonthCols(sh, year);
        int prevLy = -1, prevCy = -1;
        for (int m = 1; m <= month; m++) {
            Integer ly = all.get((year - 1) * 100 + m);
            Integer cy = all.get(year * 100 + m);
            if (ly == null || cy == null || ly <= prevLy || cy <= prevCy) return new LinkedHashMap<>();
            prevLy = ly;
            prevCy = cy;
            usable.put((year - 1) * 100 + m, ly);
            usable.put(year * 100 + m, cy);
        }
        return usable;
    }
    /**
     * ã€Œä¸ŠæœˆåŒæ¯”」公式 â†’ ã€Œæœ¬æœˆåŒæ¯”」公式:公式里引用到月份列的引用按表头 (å¹´,月) é¡ºå»¶ä¸€ä¸ªæœˆï¼Œ
     * å…¶ä½™å¼•用退回「整体右移 2 åˆ—」。
     *
     * ä¸ºä»€ä¹ˆä¸èƒ½ä¸€å¾‹å³ç§» 2 åˆ—:母版《城市客运》页在 2025 å¹´æ®µæ’了一列「2025å¹´1-7月累计」辅助列,
     * 2025 å„月列从 8 æœˆèµ·ä¸å†ç­‰è·ï¼ˆ2025å¹´8月在 AK、不在 AJ),右移 2 åˆ—会落到辅助列上,
     * äºŽæ˜¯ 8 æœˆåŒæ¯”被算成「8月值 Ã· 2025å¹´1-7月累计 âˆ’ 1」(2026-09-20 ç”¨æˆ·ä¸ŠæŠ¥çš„问题)。
     */
    private String shiftFormulaToNextMonth(Sheet sh, String formula, int year, int month) {
        if (formula == null || formula.isEmpty()) return formula;
        Map<Integer, Integer> cols = usableMonthCols(sh, year, month);
        if (cols.isEmpty()) return shiftFormulaColumns(formula, 2);
        Map<Integer, Integer> colToYm = new LinkedHashMap<>();
        for (Map.Entry<Integer, Integer> e : cols.entrySet()) colToYm.put(e.getValue(), e.getKey());
        java.util.regex.Matcher m = A1_REF.matcher(formula);
        StringBuffer sb = new StringBuffer();
        while (m.find()) {
            int idx = colIndexOf(m.group(2));
            Integer ym = colToYm.get(idx);
            Integer next = null;
            if (ym != null) {
                next = cols.get((ym / 100) * 100 + (ym % 100) + 1);
            }
            m.appendReplacement(sb, java.util.regex.Matcher.quoteReplacement(
                    m.group(1) + colLettersOf(next != null ? next : idx + 2) + m.group(3) + m.group(4)));
        }
        m.appendTail(sb);
        return sb.toString();
    }
    /**
     * ä¿®æ­£ã€Œç´¯è®¡åŒæ¯”」分母。母版《城市客运》页把 2025 å¹´ 1..M æœˆå„月列**和**它们的
     * ã€Œ2025å¹´1-M月累计」辅助列一起写进分母(1..M-1 æœˆé‡å¤è®¡ä¸€æ¬¡ã€å½“月漏计),
     * å¦æœ‰åœ¨æ¯ç‰ˆé‡Œåˆ æŽ‰è¾…助列后残留 #REF! çš„æƒ…形;两种都会让累计同比严重失真
     * ï¼ˆ2026-09-20 ç”¨æˆ·ä¸ŠæŠ¥ï¼šåŸŽå¸‚客运 8 æœˆåŒæ¯” âˆ’86.5%、累计同比 âˆ’43.5%)。
     * ç»Ÿä¸€æŒ‰è¡¨å¤´æŠŠåˆ†æ¯é‡å†™ä¸ºã€Œå޻年 1..当月 å„月值列之和」。
     *
     * 2026-09-20 å·²åœ¨ã€Š2026å¹´8月道路运输量汇总表.xlsx》母版里删掉该辅助列(备份见
     * docs/生成汇总大表/_bak_202608母版删除AJ列前_20260920.xlsx),正常情况本方法命中 0 æ ¼ï¼›
     * ä¿ç•™å®ƒæ˜¯ä¸ºäº†å…œä½ã€ŒåŒæœŸå¤‡ä»½æ¯ç‰ˆ / åŽ†å²éƒ¨ç½²åŒ… / ä¹‹åŽåˆæ’了辅助列的母版」这类情况。
     */
    public int repairCumulativeYoyFormulas(XSSFWorkbook wb, int year, int month) {
        if (wb == null || month < 1) return 0;
        int fixed = 0;
        for (int i = 0; i < wb.getNumberOfSheets(); i++) {
            Sheet sh = wb.getSheetAt(i);
            if (sh == null || !isMonthLedgerSheet(sh.getSheetName())) continue;
            int[] cum = cumulativeCols(sh, year);
            if (cum == null) continue;
            java.util.Set<Integer> helperCols = cumulativeHelperCols(sh);
            Map<Integer, Integer> all = yearMonthCols(sh, year);
            List<Integer> lastYearValueCols = new ArrayList<>();
            int prev = -1;
            for (int m = 1; m <= month; m++) {
                Integer c = all.get((year - 1) * 100 + m);
                if (c == null || c <= prev) {
                    lastYearValueCols.clear();
                    break;
                }
                prev = c;
                lastYearValueCols.add(c);
            }
                if (lastYearValueCols.isEmpty()) continue;
                for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
                Row row = sh.getRow(r);
                if (row == null) continue;
                Cell cc = row.getCell(cum[1]);
                if (cc == null || cc.getCellType() != org.apache.poi.ss.usermodel.CellType.FORMULA) continue;
                String f = cc.getCellFormula();
                if (f == null || !cumulativeYoyBroken(f, helperCols)) continue;
                // æ³¨æ„ï¼šPOI çš„ setCellFormula ä¸æŽ¥å—以 "=" å¼€å¤´çš„公式串(会抛 FormulaParseException)
                StringBuilder sb = new StringBuilder();
                sb.append(cellRef(cum[0], r + 1)).append("/(");
                for (int k = 0; k < lastYearValueCols.size(); k++) {
                    if (k > 0) sb.append('+');
                    sb.append(cellRef(lastYearValueCols.get(k), r + 1));
                }
                sb.append(")-1");
                try {
                    cc.setCellFormula(sb.toString());
                    fixed++;
                } catch (Exception ex) {
                    // ä¸ªåˆ«å…¬å¼æ”¹å†™å¤±è´¥ä¸é˜»å¡žå¯¼å‡ºï¼ˆæ‰“开时仍按原公式重算)
                    if (log != null) log.warn("累计同比公式改写失败:sheet={} cell={} åŽŸå¼={} æ–°å¼={}({})",
                            sh.getSheetName(), cellRef(cum[1], r + 1), f, sb, ex.toString());
                }
            }
        }
        if (fixed > 0 && log != null) {
            log.info("汇总工作簿:累计同比分母修正 {} æ ¼ï¼ˆç¬¬ {} æœˆï¼Œåˆ†æ¯å–去年 1..{} æœˆå„月值列)", fixed, month, month);
        }
        return fixed;
    }
    /** ç´¯è®¡åŒæ¯”公式是否“坏了”:含 #REF!,或分母引用了区间累计辅助列(与各月列重复计入) */
    private boolean cumulativeYoyBroken(String formula, java.util.Set<Integer> helperCols) {
        if (formula.contains("#REF!")) return true;
        java.util.regex.Matcher m = A1_REF.matcher(formula);
        while (m.find()) {
            if (helperCols.contains(colIndexOf(m.group(2)))) return true;
        }
        return false;
    }
    /** è¯¥é¡µã€Œç´¯è®¡ / ç´¯è®¡åŒæ¯”」两列(0 åŸºï¼‰ï¼ŒæŒ‰è¡¨å¤´å‰ 5 è¡Œæ‰¾ â€œç´¯è®¡â€ æˆ– â€œ{year}年累计”,且其右列含 â€œåŒæ¯”” */
    private int[] cumulativeCols(Sheet sh, int year) {
        int last = Math.min(sh.getLastRowNum(), sh.getFirstRowNum() + 4);
        for (int r = sh.getFirstRowNum(); r <= last; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            for (int c = row.getFirstCellNum(); c >= 0 && c < row.getLastCellNum(); c++) {
                Cell cell = row.getCell(c);
                if (cell == null || cell.getCellType() != org.apache.poi.ss.usermodel.CellType.STRING) continue;
                String v = cell.getStringCellValue();
                if (v == null) continue;
                v = v.trim();
                if (!"累计".equals(v) && !(year + "年累计").equals(v)) continue;
                Cell nxt = row.getCell(c + 1);
                if (nxt == null || nxt.getCellType() != org.apache.poi.ss.usermodel.CellType.STRING) continue;
                String nt = nxt.getStringCellValue();
                if (nt != null && nt.contains("同比")) return new int[]{c, c + 1};
            }
        }
        return null;
    }
    /** æœˆä»½å®¹é‡ï¼šä¼˜å…ˆå–表头(前 5 è¡Œï¼‰â€œ2026…累计”列反推;货运页无该文案,
     *  å†ç”¨è¡¨å¤´é‡Œæœ€åŽä¸€ä¸ªâ€œ2026å¹´M月”/日期格式的月列兜底(如货运 Q3=2026-08-01 -> 8) */
    private int monthColumnCapacity(Sheet sh) {
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/WycSplitCalc.java
@@ -190,8 +190,9 @@
            if (r.getAvgDistanceCity() != null && r.getAvgDistanceCity() > 0) a[5] = r.getAvgDistanceCity();
        }
        for (double[] a : map.values()) {
            if (a[4] <= 0 && a[0] > 0) a[4] = round(a[1] / a[0], 2);
            if (a[5] <= 0 && a[2] > 0) a[5] = round(a[3] / a[2], 2);
            // å¹³å‡è¿è·å…œåº•:全精度保留,展示位数交给单元格格式
            if (a[4] <= 0 && a[0] > 0) a[4] = a[1] / a[0];
            if (a[5] <= 0 && a[2] > 0) a[5] = a[3] / a[2];
        }
        return map;
    }
@@ -317,8 +318,4 @@
    private double nz(Double v) { return v == null ? 0.0 : v; }
    private double round(double v, int scale) {
        double factor = Math.pow(10, scale);
        return Math.round(v * factor) / factor;
    }
}
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/controller/ReportExportController.java
@@ -2,12 +2,14 @@
import com.trafficaudit.common.Result;
import com.trafficaudit.reportexport.service.ReportExportService;
import com.trafficaudit.reportexport.service.ReportExportTaskService;
import com.trafficaudit.system.service.OperationLogService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@@ -24,6 +26,8 @@
    @Resource
    private ReportExportService reportService;
    @Resource
    private ReportExportTaskService exportTaskService;
    @Resource
    private OperationLogService operationLogService;
@@ -91,6 +95,47 @@
                                                        @RequestParam(value = "toMonth", required = false) Integer toMonth) throws Exception {
        logExport("道路运输量汇总表.xlsx", period);
        return buildResponse(reportService.exportSummaryWorkbook(period, mode, toMonth), String.format("生成_道路运输量汇总表_%s.xlsx", period));
    }
    /**
     * æ±‡æ€»å¤§è¡¨å¼‚步导出:立即返回任务号,前端轮询进度、完成后再下载文件。
     * ç”Ÿæˆä¸€æ¬¡çº¦ 10~20 ç§’,走异步可避免点击后长时间无响应;同报表期 10 åˆ†é’Ÿå†…的结果直接复用(期间有导入则自动失效),
     * refresh=true å¯å¼ºåˆ¶é‡æ–°ç”Ÿæˆã€‚
     */
    @GetMapping("/exportTask/summaryWorkbook")
    public Result<java.util.Map<String, Object>> submitSummaryWorkbookTask(@RequestParam("period") String period,
                                                                          @RequestParam(value = "mode", defaultValue = "month") String mode,
                                                                          @RequestParam(value = "toMonth", required = false) Integer toMonth,
                                                                          @RequestParam(value = "refresh", defaultValue = "false") boolean refresh) {
        ReportExportTaskService.Task task = exportTaskService.submit(
                ReportExportTaskService.TYPE_SUMMARY_WORKBOOK, period, mode, toMonth, refresh);
        if (!task.isFromCache()) {
            logExport("道路运输量汇总表.xlsx", period);
        }
        return Result.ok(task.toView());
    }
    /** æŸ¥è¯¢å¼‚步导出任务进度(结果保留 30 åˆ†é’Ÿï¼‰ */
    @GetMapping("/exportTask/{taskId}")
    public Result<java.util.Map<String, Object>> exportTaskStatus(@PathVariable("taskId") String taskId) {
        ReportExportTaskService.Task task = exportTaskService.get(taskId);
        if (task == null) {
            return Result.error("导出任务不存在或已过期(生成结果保留 30 åˆ†é’Ÿï¼‰");
        }
        return Result.ok(task.toView());
    }
    /** ä¸‹è½½å¼‚步导出任务已生成的文件 */
    @GetMapping("/exportTask/{taskId}/file")
    public ResponseEntity<byte[]> exportTaskFile(@PathVariable("taskId") String taskId) throws Exception {
        ReportExportTaskService.Task task = exportTaskService.get(taskId);
        if (task == null) {
            throw new RuntimeException("导出任务不存在或已过期(生成结果保留 30 åˆ†é’Ÿï¼‰");
        }
        if (!task.isDone()) {
            throw new RuntimeException("导出任务尚未完成(当前状态:" + task.getStatus() + "),请稍后再试");
        }
        return buildResponse(task.getData(), task.getFileName());
    }
    /** ç”Ÿæˆ_网约车客运量拆分表.xlsx(月度:订单附件1 + å…¨çœ pin + å·¡æ¸¸å‡ºç§ŸæœˆæŠ¥ï¼‰ */
@@ -308,4 +353,3 @@
            .body(data);
    }
}
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -226,6 +226,36 @@
                    }
                    break;
                }
                case "investPassengerDetail":
                case "investLogisticsDetail": {
                    boolean paxDetail = "investPassengerDetail".equals(t);
                    String catName = paxDetail ? "客运站场" : "物流园区";
                    String catLabel = paxDetail ? "客运站场投资月报" : "物流园区投资月报";
                    List<InvestmentProject> catProjects = investProjectMapper.selectList(
                        new LambdaQueryWrapper<InvestmentProject>().eq(InvestmentProject::getCategory, catName));
                    final List<Long> catIds = new ArrayList<>();
                    for (InvestmentProject cp : catProjects) catIds.add(cp.getId());
                    if (catIds.isEmpty()) {
                        rows = 0;
                        notes.add("缺数据:" + catLabel);
                    } else {
                        rows = addReadyCount(() -> investMonthlyMapper.selectCount(
                            periodQw(period, mode, InvestmentMonthly::getReportPeriod)
                                .in(InvestmentMonthly::getProjectId, catIds)), catLabel, notes);
                    }
                    String detTpl = paxDetail ? "模板_X月客运站投资明细表.xlsx" : "模板_X月物流站场投资明细.xls";
                    try {
                        if (resolveAnyTemplate(investTemplateDir, detTpl) == null) {
                            rows = 0;
                            notes.add("docs/投资/模板 ä¸‹ç¼ºå°‘《" + detTpl + "》");
                        }
                    } catch (Exception ex) {
                        rows = 0;
                        notes.add("母版定位失败:" + ex.getMessage());
                    }
                    auditTypes.add("INVEST");
                    break;
                }
                default:
                    rows = 0;
                    notes.add("未知报表类型:" + t);
@@ -381,7 +411,9 @@
                coverage.addAll(investCover);
                // æŠ•资为当月报表(无 1..N ç´¯è®¡å£å¾„),只提示当月缺失
                if (!investCover.contains(month)) missing.add(month);
                investPreview(period, mode, summary, problems);
                if ("investPassengerDetail".equals(t)) investPreview(period, mode, "客运站场", summary, problems);
                else if ("investLogisticsDetail".equals(t)) investPreview(period, mode, "物流园区", summary, problems);
                else investPreview(period, mode, summary, problems);
            } else if ("energySummary".equals(t)) {
                Long ec = energyMapper.selectCount(periodQw(period, mode, EnergyVehicleQuarterly::getReportPeriod));
                if (ec != null && ec > 0) coverage.add(month);
@@ -412,7 +444,8 @@
    }
    private boolean isInvestType(String t) {
        return "investPlan".equals(t) || "investFiveYearLogistics".equals(t) || "investBillion".equals(t) || "investCounty".equals(t);
        return "investPlan".equals(t) || "investFiveYearLogistics".equals(t) || "investBillion".equals(t) || "investCounty".equals(t)
            || "investPassengerDetail".equals(t) || "investLogisticsDetail".equals(t);
    }
    private boolean isEnergyType(String t) {
@@ -668,9 +701,23 @@
        if (allZero) problems.add("全省累计值全为 0(公交/出租月报可能未导入)");
    }
    /** æŠ•资摘要:项目数 + æœ¬æœˆå®Œæˆ/自年初/自开始累计 */
    /** æŠ•资摘要:项目数 + æœ¬æœˆå®Œæˆ/自年初/自开始累计(全投资月报合计,不分类别) */
    private void investPreview(String period, String mode, List<Map<String, Object>> summary, List<String> problems) {
        List<InvestmentMonthly> list = investMonthlyMapper.selectList(periodQw(period, mode, InvestmentMonthly::getReportPeriod));
        investPreview(period, mode, null, summary, problems);
    }
    /** æŠ•资摘要(category éžç©ºæ—¶åªç»Ÿè®¡è¯¥ç±»åˆ«ï¼Œå¦‚客运站场/物流园区投资明细表) */
    private void investPreview(String period, String mode, String category, List<Map<String, Object>> summary, List<String> problems) {
        LambdaQueryWrapper<InvestmentMonthly> qw = periodQw(period, mode, InvestmentMonthly::getReportPeriod);
        if (category != null) {
            List<InvestmentProject> ps = investProjectMapper.selectList(
                new LambdaQueryWrapper<InvestmentProject>().eq(InvestmentProject::getCategory, category));
            List<Long> ids = new ArrayList<>();
            for (InvestmentProject p : ps) ids.add(p.getId());
            if (ids.isEmpty()) { problems.add("缺数据:库内没有「" + category + "」项目主档"); return; }
            qw.in(InvestmentMonthly::getProjectId, ids);
        }
        List<InvestmentMonthly> list = investMonthlyMapper.selectList(qw);
        Set<Long> proj = new HashSet<>();
        double monthDone = 0.0, yearCum = 0.0, startCum = 0.0;
        for (InvestmentMonthly m : list) {
@@ -840,36 +887,36 @@
        for (Double v : h2032FreightCum.values()) {
            if (v != null) provAbove += v;
        }
        Double provAboveWan = round(provAbove / 10000.0, 4);
        Double provAboveWan = provAbove / 10000.0;
        Double provTotal = freightCum(ftMap.get("湖北省"), month);
        Double provBelow = provTotal == null ? null : round(provTotal - provAboveWan, 4);
        Double provBelow = provTotal == null ? null : provTotal - provAboveWan;
        Double provTotalYoy = freightCumYoy(ftMap.get("湖北省"), month);
        double provAboveLast = 0.0;
        for (Double v : h2032FreightCumLast.values()) {
            if (v != null) provAboveLast += v;
        }
        Double provAboveYoy = provAboveLast == 0.0 ? null : round((provAbove - provAboveLast) / provAboveLast, 4);
        Double provAboveYoy = provAboveLast == 0.0 ? null : (provAbove - provAboveLast) / provAboveLast;
        Double provTotalLast = lastFreightCum(ftMap.get("湖北省"), month);
        Double provBelowLast = provTotalLast == null ? null : round(provTotalLast - provAboveLast / 10000.0, 4);
        Double provBelowLast = provTotalLast == null ? null : provTotalLast - provAboveLast / 10000.0;
        Double provBelowYoy = provBelowLast == null || provBelowLast == 0.0 ? null
            : round((provBelow - provBelowLast) / provBelowLast, 4);
            : (provBelow - provBelowLast) / provBelowLast;
        for (String city : RegionUtil.cityList()) {
            Double a = h2032FreightCum.get(city);
            double aWan = a == null ? 0.0 : round(a / 10000.0, 4);
            double aWan = a == null ? 0.0 : a / 10000.0;
            Double t = freightCum(ftMap.get(city), month);
            above.put(city, aWan);
            total.put(city, t);
            below.put(city, t == null ? null : round(t - aWan, 4));
            below.put(city, t == null ? null : t - aWan);
            aboveYoy.put(city, h2032FreightYoy.get(city));
            // è§„下同比 = (规下今年 - è§„下去年) / è§„下去年;规下去年 = æ¨¡æ¿åŽ»å¹´åˆè®¡ - H2032去年规上
            Double tLast = lastFreightCum(ftMap.get(city), month);
            Double aLast = h2032FreightCumLast.get(city);
            double aLastWan = aLast == null ? 0.0 : round(aLast / 10000.0, 4);
            Double belowLast = tLast == null ? null : round(tLast - aLastWan, 4);
            double aLastWan = aLast == null ? 0.0 : aLast / 10000.0;
            Double belowLast = tLast == null ? null : tLast - aLastWan;
            Double b = below.get(city);
            belowYoy.put(city, belowLast == null || belowLast == 0.0 || b == null
                ? null : round((b - belowLast) / belowLast, 4));
                ? null : (b - belowLast) / belowLast);
            totalYoy.put(city, freightCumYoy(ftMap.get(city), month));
        }
@@ -894,20 +941,20 @@
            if (maLast != null) provMonthAboveLast += maLast;
            monthAbove.put(city, ma);
            monthTotal.put(city, mt);
            monthBelow.put(city, mt == null ? null : round(mt - (ma == null ? 0.0 : ma), 4));
            monthBelow.put(city, mt == null ? null : mt - (ma == null ? 0.0 : ma));
            monthAboveYoy.put(city, growthYoy(ma, maLast));
            Double mb = monthBelow.get(city);
            Double mbLast = mtLast == null ? null : round(mtLast - (maLast == null ? 0.0 : maLast), 4);
            Double mbLast = mtLast == null ? null : mtLast - (maLast == null ? 0.0 : maLast);
            monthBelowYoy.put(city, growthYoy(mb, mbLast));
            monthTotalYoy.put(city, freightYoy(ftMap.get(city), month));
        }
        Double provMonthAboveWan = provMonthAbove == 0.0 ? null : round(provMonthAbove, 4);
        Double provMonthAboveWan = provMonthAbove == 0.0 ? null : provMonthAbove;
        Double provMonthTotal = freightMonth(ftMap.get("湖北省"), month);
        Double provMonthBelow = provMonthTotal == null ? null : round(provMonthTotal - provMonthAbove, 4);
        Double provMonthBelow = provMonthTotal == null ? null : provMonthTotal - provMonthAbove;
        Double provMonthAboveYoy = growthYoy(provMonthAbove == 0.0 ? null : provMonthAbove, provMonthAboveLast);
        Double provMonthTotalLast = lastFreightMonth(ftMap.get("湖北省"), month);
        Double provMonthBelowLast = provMonthTotalLast == null ? null
            : round(provMonthTotalLast - provMonthAboveLast, 4);
            : provMonthTotalLast - provMonthAboveLast;
        Double provMonthBelowYoy = growthYoy(provMonthBelow, provMonthBelowLast);
        Double provMonthTotalYoy = freightYoy(ftMap.get("湖北省"), month);
@@ -1244,7 +1291,7 @@
    private Double monthFreightWan(Map<String, Map<Integer, Double>> byCityMonth, String city, int month) {
        Map<Integer, Double> byMonth = byCityMonth == null ? null : byCityMonth.get(city);
        Double v = byMonth == null ? null : byMonth.get(month);
        return v == null ? null : round(v / 10000.0, 4);
        return v == null ? null : v / 10000.0;
    }
    /** åŒæ¯” = (本期-基期)/基期;任一期缺失或基期为 0 è¿”回 null */
@@ -1346,12 +1393,12 @@
        }
    }
    /** å‘¨è½¬é‡æŽ’名 Q/R å æ¯”:模板为缓存值,按新数据重算(规上占比 1 ä½å°æ•°ï¼‰ */
    /** å‘¨è½¬é‡æŽ’名 Q/R å æ¯”:模板为缓存值,按新数据重算(全精度写入,两位小数展示交给单元格格式) */
    private void setFreightShare(Row row, Double above, Double total) {
        if (total != null && total > 0 && above != null) {
            double share = round(above * 10.0 / total, 1);
            double share = above * 10.0 / total;
            setFreightCell(row, 16, share);
            setFreightCell(row, 17, round(10.0 - share, 1));
            setFreightCell(row, 17, 10.0 - share);
        } else {
            setFreightCell(row, 16, (Double) null);
            setFreightCell(row, 17, (Double) null);
@@ -1561,9 +1608,9 @@
        setNumeric(row, 15, isProvince ? null : yoyRankOfMap(totalYoy, city));
        if (t != null && t > 0 && a != null) {
            double share = round(a * 10.0 / t, 1);
            double share = a * 10.0 / t;
            setNumeric(row, 16, share);
            setNumeric(row, 17, round(10.0 - share, 1));
            setNumeric(row, 17, 10.0 - share);
        }
        return rowIdx + 1;
    }
@@ -1604,9 +1651,9 @@
        setNumeric(row, 15, isProvince || cum == null ? null : yoyRankOfTurnover(cumMap, city, 2));
        if (total != null && total > 0 && above != null) {
            double share = round(above * 10.0 / total, 1);
            double share = above * 10.0 / total;
            setNumeric(row, 16, share);
            setNumeric(row, 17, round(10.0 - share, 1));
            setNumeric(row, 17, 10.0 - share);
        }
        return rowIdx + 1;
    }
@@ -1626,7 +1673,7 @@
                Double v = byMonth == null ? null : byMonth.get(month);
                if (v != null) sum += v;
            }
            return round(sum / 10000.0, 4);
            return sum / 10000.0;
        }
        Double total = freightMonth(ft, month);
        Double above = getProvinceMonthValue(provinceMonthMap, ft, month, "freight", "above", h2032Freight);
@@ -1657,7 +1704,7 @@
            for (Double v : h2032FreightCum.values()) {
                if (v != null) sum += v;
            }
            return round(sum / 10000.0, 4);
            return sum / 10000.0;
        }
        Double total = freightCum(ft, monthCount);
        Double above = getProvinceCumValue(provinceCum, ft, "freight", "above", h2032FreightCum, monthCount);
@@ -1687,7 +1734,7 @@
        if ("above".equals(scale)) {
            Map<Integer, Double> byMonth = h2032Freight.get(city);
            Double v = byMonth == null ? null : byMonth.get(month);
            return v == null ? 0.0 : round(v / 10000.0, 4);
            return v == null ? 0.0 : v / 10000.0;
        }
        Double total = freightMonth(ft, month);
        Double above = getCityMonthValue(monthData, ft, city, month, "freight", "above", h2032Freight);
@@ -1716,7 +1763,7 @@
        if ("total".equals(scale)) return freightCum(ft, monthCount);
        if ("above".equals(scale)) {
            Double v = h2032FreightCum.get(city);
            return v == null ? 0.0 : round(v / 10000.0, 4);
            return v == null ? 0.0 : v / 10000.0;
        }
        Double total = freightCum(ft, monthCount);
        Double above = getCityCumValue(cum, ft, "freight", "above", h2032FreightCum, city, monthCount);
@@ -1953,20 +2000,20 @@
                    double v = passengerMonthVal(data, indi, currentYear, m, region, metricIdx);
                    if (!keepFormula) {
                        if (c == null) c = row.createCell(col);
                        if (v == 0.0) c.setBlank(); else c.setCellValue(round(v, 4));
                        if (v == 0.0) c.setBlank(); else c.setCellValue(v);
                    }
                    // åŒæ¯”列:库内去年同月同比(模板 #REF! å…¬å¼æ›¿æ¢ä¸ºæ•°å€¼ï¼Œå…¬å¼è¡Œä¹Ÿè¦†å†™ï¼‰
                    double lv = passengerMonthVal(data, indi, currentYear - 1, m, region, metricIdx);
                    Cell yc = row.getCell(col + 1);
                    if (yc == null) yc = row.createCell(col + 1);
                    if (v == 0.0 || lv == 0.0) yc.setBlank(); else yc.setCellValue(round((v - lv) / lv, 4));
                    if (v == 0.0 || lv == 0.0) yc.setBlank(); else yc.setCellValue((v - lv) / lv);
                }
                // ç´¯è®¡åŒæ¯”列(模板 #REF! â†’ åº“内去年 1..N æœˆç´¯è®¡åŒæ¯”数值)
                int cumCol = extended ? 26 : 14;
                Double cumYoy = passengerCumYoy(data, indi, currentYear, fillMonths, region, metricIdx);
                Cell yc2 = row.getCell(cumCol + 1);
                if (yc2 == null) yc2 = row.createCell(cumCol + 1);
                if (cumYoy == null) yc2.setBlank(); else yc2.setCellValue(round(cumYoy, 4));
                if (cumYoy == null) yc2.setBlank(); else yc2.setCellValue(cumYoy);
            }
            recalc(wb);
            return toBytes(wb);
@@ -2047,7 +2094,7 @@
    private Double yoy(Double cur, Double base) {
        if (cur == null || base == null || base == 0.0) return null;
        return round((cur - base) / base, 4);
        return (cur - base) / base;
    }
    /** æŒ‡æ ‡å–值(已换算为万人/万人公里/公里;无数据返回 null) */
@@ -2286,7 +2333,7 @@
        if (v == 0.0) return; // åº“内无源:保留母版/定稿原值,缺数据不覆盖
        // POI setCellValue(double) å¯¹å…¬å¼å•元格只更新缓存不移除公式,必须先 setBlank å†å†™å€¼
        c.setBlank();
        c.setCellValue(round(v, 4));
        c.setCellValue(v);
    }
    /** æ˜¾å¼å†™å…¥ 0(POI setCellValue(0.0) ä¼šè½¬ blank,需操作底层 XML) */
@@ -2582,12 +2629,12 @@
                    int monthCount = (y == currentYear) ? maxMonth : 12;
                    for (int m = 1; m <= monthCount; m++) {
                        Double cur = midMetricValue(aggOf(data, y, m, region), i, volume);
                        setNumeric(row, col++, cur == null ? null : round(cur, 4));
                        setNumeric(row, col++, cur);
                        Double base = midMetricValue(aggOf(data, y - 1, m, region), i, volume);
                        setNumeric(row, col++, yoy(cur, base));
                    }
                    Double cum = midCumOf(data, y, region, monthCount, i, volume);
                    setNumeric(row, col++, cum == null ? null : round(cum, 4));
                    setNumeric(row, col++, cum);
                    Double cumBase = midCumOf(data, y - 1, region, monthCount, i, volume);
                    setNumeric(row, col++, yoy(cum, cumBase));
                }
@@ -2670,18 +2717,18 @@
                double turn = midCumClass(mid, currentYear, city, maxMonth, 0, false);
                double lastPass = midCumClass(mid, currentYear - 1, city, maxMonth, 0, true);
                double lastTurn = midCumClass(mid, currentYear - 1, city, maxMonth, 0, false);
                setValOrBlank(sheet, r0, 1, pass == 0 ? null : round(pass, 4));
                setValOrBlank(sheet, r0, 1, pass == 0 ? null : pass);
                setValOrBlank(sheet, r0, 3, midCumYoy(mid, currentYear, city, maxMonth, 0, true));
                setValOrBlank(sheet, r0, 5, turn == 0 ? null : round(turn, 4));
                setValOrBlank(sheet, r0, 5, turn == 0 ? null : turn);
                setValOrBlank(sheet, r0, 7, midCumYoy(mid, currentYear, city, maxMonth, 0, false));
                // å³å—当月:L/P å€¼ã€N/R å½“月同比(M/O/Q/S æŽ’名公式保留)
                double mPass = midClassVal(mid, currentYear, maxMonth, city, 0, true);
                double mTurn = midClassVal(mid, currentYear, maxMonth, city, 0, false);
                double mLastPass = midClassVal(mid, currentYear - 1, maxMonth, city, 0, true);
                double mLastTurn = midClassVal(mid, currentYear - 1, maxMonth, city, 0, false);
                setValOrBlank(sheet, r0, 11, mPass == 0 ? null : round(mPass, 4));
                setValOrBlank(sheet, r0, 11, mPass == 0 ? null : mPass);
                setValOrBlank(sheet, r0, 13, mPass == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, city, 0, true));
                setValOrBlank(sheet, r0, 15, mTurn == 0 ? null : round(mTurn, 4));
                setValOrBlank(sheet, r0, 15, mTurn == 0 ? null : mTurn);
                setValOrBlank(sheet, r0, 17, mTurn == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, city, 0, false));
            }
            recalc(wb);
@@ -2701,7 +2748,7 @@
    private Double yoyOf(double cur, double base) {
        if (base == 0.0) return null;
        return round((cur - base) / base, 4);
        return (cur - base) / base;
    }
    /** ä¸­å£å¾„分类累计(1..monthCount æœˆæ±‚和) */
@@ -2743,9 +2790,9 @@
                double turn = midCumClass(mid, currentYear, "湖北省", maxMonth, dims[i], false);
                double lastPass = midCumClass(mid, currentYear - 1, "湖北省", maxMonth, dims[i], true);
                double lastTurn = midCumClass(mid, currentYear - 1, "湖北省", maxMonth, dims[i], false);
                setValOrBlank(sheet, r0, 1, pass == 0 ? null : round(pass, 4));
                setValOrBlank(sheet, r0, 1, pass == 0 ? null : pass);
                setValOrBlank(sheet, r0, 2, midCumYoy(mid, currentYear, "湖北省", maxMonth, dims[i], true));
                setValOrBlank(sheet, r0, 5, turn == 0 ? null : round(turn, 4));
                setValOrBlank(sheet, r0, 5, turn == 0 ? null : turn);
                setValOrBlank(sheet, r0, 6, midCumYoy(mid, currentYear, "湖北省", maxMonth, dims[i], false));
            }
            // ä¸‹åŠå—(当月):标题行9、数据行11-15(0-based 8、10-14)
@@ -2759,9 +2806,9 @@
                double turn = midClassVal(mid, currentYear, maxMonth, "湖北省", dims[i], false);
                double lastPass = midClassVal(mid, currentYear - 1, maxMonth, "湖北省", dims[i], true);
                double lastTurn = midClassVal(mid, currentYear - 1, maxMonth, "湖北省", dims[i], false);
                setValOrBlank(sheet, r0, 1, pass == 0 ? null : round(pass, 4));
                setValOrBlank(sheet, r0, 1, pass == 0 ? null : pass);
                setValOrBlank(sheet, r0, 2, pass == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, "湖北省", dims[i], true));
                setValOrBlank(sheet, r0, 5, turn == 0 ? null : round(turn, 4));
                setValOrBlank(sheet, r0, 5, turn == 0 ? null : turn);
                setValOrBlank(sheet, r0, 6, turn == 0 ? null : midMonthYoy(mid, currentYear, maxMonth, "湖北省", dims[i], false));
            }
            // ä¸‹åŠå—占比公式由样例指向上半块(B$3/F$3),改写为本块总行(B$11/F$11),行12-15
@@ -3179,7 +3226,7 @@
        Cell c = row.getCell(colIdx);
        if (c != null && c.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) return;
        if (c == null) c = row.createCell(colIdx);
        if (v == 0) c.setBlank(); else c.setCellValue(round(v, 2));
        if (v == 0) c.setBlank(); else c.setCellValue(v);
    }
    /** æ˜¾å¼å†™å…¥ 0(setDataCell å¯¹ 0 ç½®ç©ºï¼›æ±‡æ€»è¡¨ç½‘约车行需要显示 0) */
@@ -3460,7 +3507,7 @@
     * ä¿ç•™å…¨éƒ¨é¡µç­¾/版式/公式/2025 å¹´ç¼“存值,仅把表头年份动态化为目标年;数据填充按 P2 èµ·é€æ­¥æŽ¥å…¥ã€‚
     */
    public byte[] exportSummaryWorkbook(String period, String mode) throws Exception {
        return exportSummaryWorkbook(period, mode, null);
        return exportSummaryWorkbook(period, mode, null, null);
    }
    /** ç”Ÿæˆ_道路运输量汇总表.xlsx(P0-P1 éª¨æž¶ï¼Œæ”¯æŒ toMonth è£å‰ªï¼‰ï¼š
@@ -3468,6 +3515,13 @@
     *  toMonth éžç©ºä¸”小于 M æ—¶ï¼ŒæŠŠ 8 ä¸ªå°è´¦/合成页裁到 toMonth æœˆï¼ˆtoMonth+1..M çš„当月值/同比列清空、保留表头,
     *  ç´¯è®¡ä¸Žç´¯è®¡åŒæ¯”公式改为按 toMonth æœˆé‡ç®—),用于“先生成 1-6 æœˆã€1-7 æœˆæ‰©åˆ—”验证。 */
    public byte[] exportSummaryWorkbook(String period, String mode, Integer toMonth) throws Exception {
        return exportSummaryWorkbook(period, mode, toMonth, null);
    }
    /** åŒä¸Šï¼Œé¢å¤–通过 progress å›žè°ƒæŠ¥å‘Šè¿›åº¦ï¼ˆå¼‚步导出任务用:步骤百分比 + æ­¥éª¤æ–‡æ¡ˆï¼‰ */
    public byte[] exportSummaryWorkbook(String period, String mode, Integer toMonth,
                                        java.util.function.BiConsumer<Integer, String> progress) throws Exception {
        reportProgress(progress, 2, "校验报表期与母版");
        int year = Integer.parseInt(period.contains("-") ? period.split("-")[0] : period);
        int month = period.contains("-") ? monthOf(period, mode) : 12;
        File template = resolveSummaryMother(period, mode);
@@ -3475,9 +3529,11 @@
            throw new RuntimeException("未找到汇总表母版《" + year + "å¹´" + month + "月道路运输量汇总表.xlsx》(docs/生成汇总大表)");
        }
        double savedZipRatio = ZipSecureFile.getMinInflateRatio();
        reportProgress(progress, 6, "读取母版工作簿");
        ZipSecureFile.setMinInflateRatio(0.0001); // æ–°ç‰ˆæ¯ç‰ˆ styles.xml ç”± openpyxl é«˜åŽ‹ç¼©ï¼ˆ0.0099<0.01)触发 POI Zip-bomb è¯¯æŠ¥ï¼Œè¯»å–后还原
        try (InputStream in = new FileInputStream(template);
             XSSFWorkbook wb = new XSSFWorkbook(in)) {
            reportProgress(progress, 12, "还原同期月度块");
            File donor = resolveSummaryDonor(year, month);
            if (donor != null) {
                try (InputStream din = new FileInputStream(donor); XSSFWorkbook dwb = new XSSFWorkbook(din)) {
@@ -3496,26 +3552,42 @@
            int effMonth = (toMonth != null && toMonth > 0 && toMonth < month) ? toMonth : month;
            List<String> dbProblems = new ArrayList<>();
            String effPeriod = String.format("%04d-%02d", year, effMonth);
            reportProgress(progress, 22, "回填当月数据");
            int dbFilled = summaryWorkbookFiller.fillMonthlyLedger(wb, effPeriod, year, effMonth, dbProblems);
            int yoyFilled = summaryWorkbookFiller.ensureMonthYoyFormulas(wb, year, effMonth);
            int monthFormulaFilled = summaryWorkbookFiller.ensureMonthFormulas(wb, year, effMonth);
            // ç´¯è®¡åŒæ¯”分母修正:母版把「去年各月」与「去年1-M月累计」辅助列同时计入(重复计/漏当月)
            int cumYoyFixed = summaryWorkbookFiller.repairCumulativeYoyFormulas(wb, year, effMonth);
            reportProgress(progress, 45, "刷新排名页与跨页合成页");
            int rankRefreshed = refreshSummaryRankSheets(wb, effPeriod, "month");
            int cumulativeRefreshed = refreshSummaryCumulativeSheets(wb, year, effMonth); // å››å¼ è·¨é¡µåˆæˆé¡µéšæŠ¥è¡¨æœŸåˆ·æ–°
            int widthApplied = applySummaryReferenceLayout(wb);
            log.info("汇总工作簿数据驱动回填完成:写入当月值格数={},补写当月同比公式格数={},补写当月合计公式格数={},"
                    + "刷新排名页数值格数={},刷新四页(城市汇总/城市分市州/中口径分析/道路运输周转量)数值格数={},"
                    + "修正累计同比分母格数={},刷新排名页数值格数={},刷新四页(城市汇总/城市分市州/中口径分析/道路运输周转量)数值格数={},"
                    + "按《生成_道路运输量汇总表》对齐列宽列数={},缺数提示={}",
                    dbFilled, yoyFilled, monthFormulaFilled, rankRefreshed, cumulativeRefreshed, widthApplied, dbProblems);
                    dbFilled, yoyFilled, monthFormulaFilled, cumYoyFixed, rankRefreshed, cumulativeRefreshed, widthApplied, dbProblems);
            applyTwoDecimalFormat(wb); // æ•°å€¼æ˜¾ç¤ºä¸¤ä½å°æ•°ï¼ˆä¿ç•™å…¨ç²¾åº¦ï¼‰ï¼›% / æ—¥æœŸç­‰æ—¢æœ‰æ ·å¼ä¸æ”¹å˜
            int inlineFixed = normalizeStaleInlineStrings(wb); // æ¸…掉「非 inlineStr å´æ®‹ç•™ <is>」的非法格,否则 Excel æ‹’绝打开
            if (inlineFixed > 0) log.warn("汇总工作簿清掉残留 inlineStr éžæ³•æ ¼ {} ä¸ªï¼ˆæ¯ç‰ˆç”± openpyxl å†™å‡ºæ—¶çš„典型问题)", inlineFixed);
            reportProgress(progress, 70, "重算整本公式(约 4 ä¸‡æ ¼ï¼‰");
            int[] recalc = recalcWorkbookFormulas(wb); // å…¬å¼é‡ç®—并写回缓存值:全省行(Σ市州)/累计列/排名 SUM æ‰èƒ½è¢«é¢„览与二次处理读到
            log.info("汇总工作簿公式重算完成:成功 {} æ ¼ï¼Œå¤±è´¥ {} æ ¼ï¼ˆå¤±è´¥æ ¼ä¿ç•™æ¯ç‰ˆæ—§ç¼“存值,打开时按 fullCalcOnLoad é‡ç®—)", recalc[0], recalc[1]);
            if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr();
            wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // è¿˜åŽŸ/改写公式后打开即重算
            reportProgress(progress, 95, "写出文件");
            return toBytes(wb);
        } finally {
            ZipSecureFile.setMinInflateRatio(savedZipRatio);
        }
    }
    /** å¯¼å‡ºè¿›åº¦å›žè°ƒï¼ˆå¼‚步导出任务用):回调自身异常不影响导出 */
    private static void reportProgress(java.util.function.BiConsumer<Integer, String> progress, int percent, String step) {
        if (progress == null) return;
        try {
            progress.accept(percent, step);
        } catch (Exception ignore) {
            // è¿›åº¦å›žè°ƒå¤±è´¥ä¸å½±å“å¯¼å‡º
        }
    }
@@ -4643,8 +4715,8 @@
            if (r.getAvgDistanceCity() != null && r.getAvgDistanceCity() > 0) a[5] = r.getAvgDistanceCity();
        }
        for (double[] a : map.values()) {
            if (a[4] <= 0 && a[0] > 0) a[4] = round(a[1] / a[0], 2);
            if (a[5] <= 0 && a[2] > 0) a[5] = round(a[3] / a[2], 2);
            if (a[4] <= 0 && a[0] > 0) a[4] = a[1] / a[0];
            if (a[5] <= 0 && a[2] > 0) a[5] = a[3] / a[2];
        }
        return map;
    }
@@ -4960,19 +5032,19 @@
                    Row row = sheet.getRow(rowIdx);
                    if (row == null) row = sheet.createRow(rowIdx);
                    if (a > 0) {
                        // 2026å¹´ 1..N æœˆï¼šåº“内有值则填(2位小数),无值清空模板样例
                        // 2026å¹´ 1..N æœˆï¼šåº“内有值则填(全精度写入,两位小数由单元格格式展示),无值清空模板样例
                        for (int m = 1; m <= month; m++) {
                            double v = cityVal(monthCity, area, m, k);
                            Cell c = row.getCell(2 + (m - 1) * 2);
                            if (c == null) c = row.createCell(2 + (m - 1) * 2);
                            if (v == 0) c.setBlank(); else c.setCellValue(round(v, 2));
                            if (v == 0) c.setBlank(); else c.setCellValue(v);
                        }
                        // 2025å¹´ 1..12 æœˆï¼šåº“内有去年数据则填,否则清空模板样例
                        for (int m = 1; m <= 12; m++) {
                            double v = cityVal(lastYearMonthCity, area, m, k);
                            Cell c = row.getCell(19 + (m - 1) * 2);
                            if (c == null) c = row.createCell(19 + (m - 1) * 2);
                            if (v == 0) c.setBlank(); else c.setCellValue(round(v, 2));
                            if (v == 0) c.setBlank(); else c.setCellValue(v);
                        }
                    }
                    rowIdx++;
@@ -5381,6 +5453,7 @@
                hssfText(row, 0, "合计");
                InvestAgg prov = new InvestAgg();
                for (InvestmentMonthly m : monthlies) prov.add(m, pmap.get(m.getProjectId()));
                addProjectsWithoutMonthly(prov, projects, mByProject, null, null);
                writeInvestAggHssf(row, prov);
                continue;
            }
@@ -5393,6 +5466,7 @@
                    InvestmentProject p = pmap.get(m.getProjectId());
                    if (p != null && blockCategory.equals(p.getCategory())) agg.add(m, p);
                }
                addProjectsWithoutMonthly(agg, projects, mByProject, blockCategory, null);
                writeInvestAggHssf(row, agg);
                seq = 0;
                continue;
@@ -5421,6 +5495,7 @@
                    if (p != null && city.equals(p.getCity())
                        && (blockCategory == null || blockCategory.equals(p.getCategory()))) agg.add(m, p);
                }
                addProjectsWithoutMonthly(agg, projects, mByProject, blockCategory, city);
                writeInvestAggHssf(row, agg);
                continue;
            }
@@ -5465,7 +5540,25 @@
        hssfNum(row, 8, agg.startCum == 0 ? null : agg.startCum);
        hssfNum(row, 9, agg.yearPlan == 0 ? null : agg.yearPlan);
        hssfNum(row, 22, agg.yearCum == 0 ? null : agg.yearCum);
        if (agg.yearPlan > 0) hssfNum(row, 23, round(agg.yearCum / agg.yearPlan, 4));
        if (agg.yearPlan > 0) hssfNum(row, 23, agg.yearCum / agg.yearPlan);
    }
    /**
     * è¡¥è®¡ã€Œæœ¬æœŸæ²¡æœ‰æœˆåº¦è®°å½•、但项目主档有总投资」的项目(只计总投资这一列)。
     * åœºæ™¯ï¼šæŸå¸‚州本月未报送单表(如武汉 8 æœˆç‰©æµï¼‰ï¼Œä¸»æ¡£é‡Œé¡¹ç›®ä»åœ¨ï¼Œè‹¥ä¸è¡¥è®¡ï¼Œè¯¥å¸‚州小计/合计会整块为空。
     *
     * @param category å—类别(客运站场/物流园区);null è¡¨ç¤ºä¸é™
     * @param city     å¸‚州全称(如 æ­¦æ±‰å¸‚);null è¡¨ç¤ºä¸é™
     */
    private void addProjectsWithoutMonthly(InvestAgg agg, List<InvestmentProject> projects,
                                           Map<Long, InvestmentMonthly> mByProject,
                                           String category, String city) {
        for (InvestmentProject p : projects) {
            if (p == null || mByProject.containsKey(p.getId())) continue;
            if (category != null && !category.equals(p.getCategory())) continue;
            if (city != null && !city.equals(p.getCity())) continue;
            if (p.getTotalInvestment() != null) agg.total += p.getTotalInvestment();
        }
    }
@@ -5797,7 +5890,7 @@
                putNum(row, 2, s[3] == 0 ? null : s[3]);
                putNum(row, 3, s[4] == 0 ? null : s[4]);
                double b = numOf(row.getCell(1));
                putNum(row, 4, b > 0 ? round(s[3] / b * 100, 4) : null);
                putNum(row, 4, b > 0 ? s[3] / b * 100 : null);
                cT += s[3];
                dT += s[4];
            }
@@ -5806,7 +5899,7 @@
                putNum(tot, 2, cT == 0 ? null : cT);
                putNum(tot, 3, dT == 0 ? null : dT);
                double b = numOf(tot.getCell(1));
                putNum(tot, 4, b > 0 ? round(cT / b * 100, 4) : null);
                putNum(tot, 4, b > 0 ? cT / b * 100 : null);
            }
        }
        try {
@@ -6027,7 +6120,7 @@
        for (InvestmentMonthly m : monthlies) provinceYearPlan += nz(m.getYearPlan());
        HSSFRow rowOther = s1.getRow(9);
        for (int c = 1; c <= 5; c++) hssfNum(rowOther, c, null);
        hssfNum(rowOther, 5, provinceYearPlan == 0 ? null : round(provinceYearPlan / 10000, 2));
        hssfNum(rowOther, 5, provinceYearPlan == 0 ? null : provinceYearPlan / 10000);
        // ---- sheet2 è§„上项目表:表头年份 + åˆè®¡ + é¡¹ç›®è¡Œ ----
        HSSFSheet s2 = wb.getSheetAt(1);
        hssfText(s2.getRow(0), 0, year + "年交通建设项目储备情况(规模以上项目)");
@@ -6259,6 +6352,8 @@
    private double nz(Double v) {
        return v == null ? 0.0 : v;
    }
    /** æ•°å€¼æ ¼å¼é‡Œ 3 ä½åŠä»¥ä¸Šçš„小数部分(如 0.0000 / 0.000_ ï¼‰ */
    private static final java.util.regex.Pattern DEC_MORE_THAN_TWO = java.util.regex.Pattern.compile("\\.0{3,}");
    /** ç”Ÿæˆè¡¨æ•°å€¼åˆ—统一样式:保留全精度数值,显示两位小数;整数值与 %/日期/科学计数等既有样式不改变 */
    private void applyTwoDecimalFormat(org.apache.poi.ss.usermodel.Workbook wb) {
        if (wb == null) return;
@@ -6275,24 +6370,32 @@
                        CellType ct = cell.getCellType();
                        if (ct != CellType.NUMERIC && ct != CellType.FORMULA) continue;
                        if (org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(cell)) continue;
                        boolean integral = false;
                        if (ct == CellType.NUMERIC) {
                            integral = Math.rint(cell.getNumericCellValue()) == cell.getNumericCellValue();
                        } else if (cell.getCachedFormulaResultType() == CellType.NUMERIC) {
                            integral = Math.rint(cell.getNumericCellValue()) == cell.getNumericCellValue();
                        }
                        if (integral) continue; // æ•´æ•°ä¸æ˜¾ç¤º .00(含公式结果为整数的格)
                        org.apache.poi.ss.usermodel.CellStyle cs = cell.getCellStyle();
                        if (cs == null) continue;
                        String f = cs.getDataFormatString();
                        if (f == null) f = "";
                        if (f.contains("%") || f.contains("E") || f.contains("0.00")) continue;
                        boolean plain = f.isEmpty() || "General".equals(f) || "general".equals(f)
                            || "0".equals(f) || "#,##0".equals(f) || "0.0".equals(f) || "#,##0.0".equals(f);
                        if (!plain) continue;
                        if (f.contains("%") || f.contains("E") || f.contains("@")) continue;
                        String normalized = null;
                        if (DEC_MORE_THAN_TWO.matcher(f).find()) {
                            // æ¯ç‰ˆä¸ªåˆ«æ ¼å¸¦ 3~4 ä½å°æ•°æ ¼å¼ï¼ˆå¦‚ 0.000_ ã€0.0000_);[Red]\(0.0000\)):数值仍全精度,
                            // å±•示统一收敛到两位。这类格本就按数值格式展示,故不受「整数不显示 .00」保护影响。
                            normalized = DEC_MORE_THAN_TWO.matcher(f).replaceAll(".00");
                        } else {
                            boolean integral = false;
                            if (ct == CellType.NUMERIC) {
                                integral = Math.rint(cell.getNumericCellValue()) == cell.getNumericCellValue();
                            } else if (cell.getCachedFormulaResultType() == CellType.NUMERIC) {
                                integral = Math.rint(cell.getNumericCellValue()) == cell.getNumericCellValue();
                            }
                            if (integral) continue; // æ•´æ•°ä¸æ˜¾ç¤º .00(含公式结果为整数的格)
                            if (f.contains("0.00")) continue;
                            boolean plain = f.isEmpty() || "General".equals(f) || "general".equals(f)
                                || "0".equals(f) || "#,##0".equals(f) || "0.0".equals(f) || "#,##0.0".equals(f);
                            if (!plain) continue;
                        }
                        org.apache.poi.ss.usermodel.CellStyle ns = wb.createCellStyle();
                        ns.cloneStyleFrom(cs);
                        ns.setDataFormat(fmtTwo);
                        ns.setDataFormat(normalized == null ? fmtTwo : df.getFormat(normalized));
                        cell.setCellStyle(ns);
                    } catch (Exception ignore) {
                        // ä¸ªåˆ«å•元格格式异常不阻塞导出
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportTaskService.java
New file
@@ -0,0 +1,234 @@
package com.trafficaudit.reportexport.service;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.trafficaudit.dataimport.entity.ImportBatch;
import com.trafficaudit.dataimport.mapper.ImportBatchMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
/**
 * æŠ¥è¡¨å¯¼å‡ºå¼‚步任务:把耗时十几秒的《生成_道路运输量汇总表》生成挪到后台线程,
 * å‰ç«¯åªæ‹¿ã€Œä»»åŠ¡å· + è¿›åº¦ï¼ˆæ­¥éª¤/百分比)」,完成后单独下载文件。
 *
 * ç›®çš„(2026-09-20 ç”¨æˆ·ä¸ŠæŠ¥ã€Œç‚¹ä¸‹è½½è¦ç­‰ä¸€ä¼šï¼Œæ•°æ®å¤šäº†ä¼šä¸ä¼šå¡æ­»ã€ï¼‰ï¼š
 * 1) ç‚¹å‡»åŽç«‹åˆ»æœ‰åé¦ˆï¼Œä¸å†é•¿æ—¶é—´æ— å“åº”ï¼›
 * 2) ç”Ÿæˆè¿‡ç¨‹ä¸å ç”¨ HTTP è¯·æ±‚线程,避免多人同时点把容器拖死(工作线程固定 1 ä¸ªï¼Œå¤šä½™è¯·æ±‚显示「排队中」);
 * 3) åŒä¸€æŠ¥è¡¨æœŸ 10 åˆ†é’Ÿå†…的结果可复用,避免每次重算整本约 4 ä¸‡æ ¼å…¬å¼ã€‚
 *
 * ç¼“存正确性守卫:只有当「距上次生成不超过 TTL」且「期间没有任何新的数据导入(import_batch æœ€å¤§ id æœªå˜ï¼‰ã€
 * æ—¶æ‰å¤ç”¨ï¼›å¯¼å…¥å³å¤±æ•ˆï¼Œé¿å…æŠŠè¿‡æœŸæŠ¥è¡¨ç»™ç”¨æˆ·ã€‚前端仍提供「重新生成」强制刷新。
 */
@Slf4j
@Service
public class ReportExportTaskService {
    /** æ±‡æ€»å·¥ä½œç°¿å¯¼å‡ºä»»åŠ¡ç±»åž‹ */
    public static final String TYPE_SUMMARY_WORKBOOK = "summaryWorkbook";
    /** ç”Ÿæˆç»“果保留时长:期间可反复下载,过后自动清理 */
    private static final long RESULT_TTL_MS = 30 * 60 * 1000L;
    /** åŒä¸€æŠ¥è¡¨æœŸç»“果复用时长(且期间无新导入时才复用) */
    private static final long CACHE_TTL_MS = 10 * 60 * 1000L;
    @Resource
    private ReportExportService reportService;
    @Resource
    private ImportBatchMapper importBatchMapper;
    private final Map<String, Task> tasks = new ConcurrentHashMap<>();
    /** åŒæŠ¥è¡¨æœŸæœ€è¿‘一次成功结果:key -> task */
    private final Map<String, Task> cache = new ConcurrentHashMap<>();
    private final ExecutorService pool = Executors.newFixedThreadPool(1, r -> {
        Thread t = new Thread(r, "report-export-task");
        t.setDaemon(true);
        return t;
    });
    /** ä¸€æ¬¡å¯¼å‡ºä»»åŠ¡ï¼ˆå†…å­˜æ€ï¼Œè¿›ç¨‹é‡å¯å³å¤±æ•ˆï¼‰ */
    public static class Task {
        private final String id = UUID.randomUUID().toString().replace("-", "");
        private final String type;
        private final String period;
        private final String mode;
        private final Integer toMonth;
        private final long createdAt = System.currentTimeMillis();
        /** ç”Ÿæˆæ—¶çš„æ•°æ®ç‰ˆæœ¬ï¼ˆimport_batch æœ€å¤§ id),用于判断缓存是否过期 */
        private final long importVersion;
        private volatile String status = "PENDING"; // PENDING/RUNNING/DONE/FAILED
        private volatile int percent = 0;
        private volatile String step = "排队中";
        private volatile String error;
        private volatile byte[] data;
        private volatile String fileName;
        private volatile boolean fromCache;
        private volatile long finishedAt;
        Task(String type, String period, String mode, Integer toMonth, long importVersion) {
            this.type = type;
            this.period = period;
            this.mode = mode;
            this.toMonth = toMonth;
            this.importVersion = importVersion;
        }
        void progress(int pct, String stepText) {
            if (pct > this.percent) this.percent = Math.min(pct, 99);
            if (stepText != null) this.step = stepText;
        }
        public String getId() { return id; }
        public String getStatus() { return status; }
        public boolean isDone() { return "DONE".equals(status) && data != null; }
        public boolean isFromCache() { return fromCache; }
        public byte[] getData() { return data; }
        public String getFileName() { return fileName; }
        public String getError() { return error; }
        public Map<String, Object> toView() {
            Map<String, Object> m = new LinkedHashMap<>();
            m.put("taskId", id);
            m.put("type", type);
            m.put("period", period);
            m.put("mode", mode);
            m.put("toMonth", toMonth);
            m.put("status", status);
            m.put("percent", "DONE".equals(status) ? 100 : percent);
            m.put("step", step);
            m.put("fromCache", fromCache);
            m.put("fileName", fileName);
            m.put("error", error);
            long end = finishedAt > 0 ? finishedAt : System.currentTimeMillis();
            m.put("elapsedMs", end - createdAt);
            m.put("finishedAt", finishedAt > 0 ? new Date(finishedAt) : null);
            return m;
        }
    }
    /**
     * æäº¤å¯¼å‡ºä»»åŠ¡ã€‚forceRefresh=true æ—¶å¿½ç•¥ç¼“存重新生成;
     * å¦åˆ™åœ¨ã€ŒåŒä¸€æŠ¥è¡¨æœŸã€10 åˆ†é’Ÿå†…、期间无新导入」的前提下直接复用上次结果。
     * åŒä¸€æŠ¥è¡¨æœŸå·²æœ‰ä»»åŠ¡åœ¨è·‘æ—¶ç›´æŽ¥è¿”å›žè¯¥ä»»åŠ¡ï¼Œé¿å…é‡å¤æŽ’é˜Ÿã€‚
     */
    public Task submit(String type, String period, String mode, Integer toMonth, boolean forceRefresh) {
        cleanupExpired();
        String key = cacheKey(type, period, mode, toMonth);
        if (!forceRefresh) {
            Task hit = cache.get(key);
            if (cacheUsable(hit)) {
                hit.fromCache = true;
                log.info("报表导出任务命中缓存:{} {}(生成于 {})", type, period, new Date(hit.finishedAt));
                return hit;
            }
        }
        Task running = findRunning(key);
        if (running != null) {
            log.info("报表导出任务已在执行,复用进行中的任务:{} {}({})", type, period, running.id);
            return running;
        }
        Task task = new Task(type, period, mode, toMonth, importVersion());
        tasks.put(task.id, task);
        pool.submit(() -> run(task));
        return task;
    }
    public Task get(String taskId) {
        if (taskId == null) return null;
        cleanupExpired();
        return tasks.get(taskId);
    }
    private void run(Task t) {
        t.status = "RUNNING";
        t.step = "开始生成";
        long start = System.currentTimeMillis();
        try {
            byte[] data = reportService.exportSummaryWorkbook(t.period, t.mode, t.toMonth, t::progress);
            t.data = data;
            t.fileName = "生成_道路运输量汇总表_" + t.period + ".xlsx";
            t.step = "完成";
            t.status = "DONE";
            t.finishedAt = System.currentTimeMillis();
            cache.put(cacheKey(t.type, t.period, t.mode, t.toMonth), t);
            log.info("报表导出任务完成:{} {} ç”¨æ—¶ {} ms,{} å­—节", t.type, t.period, t.finishedAt - start, data.length);
        } catch (Throwable e) {
            t.status = "FAILED";
            t.error = e.getMessage() == null ? e.toString() : e.getMessage();
            t.step = "生成失败";
            t.finishedAt = System.currentTimeMillis();
            log.error("报表导出任务失败:{} {}(用时 {} ms)", t.type, t.period, t.finishedAt - start, e);
        }
    }
    private Task findRunning(String key) {
        for (Task t : tasks.values()) {
            if (!key.equals(cacheKey(t.type, t.period, t.mode, t.toMonth))) continue;
            if ("PENDING".equals(t.status) || "RUNNING".equals(t.status)) return t;
        }
        return null;
    }
    private boolean cacheUsable(Task t) {
        if (t == null || !"DONE".equals(t.status) || t.data == null) return false;
        if (System.currentTimeMillis() - t.finishedAt > CACHE_TTL_MS) return false;
        long now = importVersion();
        if (now < 0 || t.importVersion < 0) return false; // å–不到数据版本时不冒风险复用
        return now == t.importVersion;
    }
    /** æ•°æ®ç‰ˆæœ¬ï¼import_batch æœ€å¤§ id:任何一次导入都会让它变化,取不到返回 -1(此时禁用缓存) */
    private long importVersion() {
        try {
            List<Object> v = importBatchMapper.selectObjs(new QueryWrapper<ImportBatch>().select("IFNULL(MAX(id),0)"));
            if (v == null || v.isEmpty() || v.get(0) == null) return 0L;
            return ((Number) v.get(0)).longValue();
        } catch (Exception e) {
            log.warn("读取导入数据版本失败,本次禁用导出缓存:{}", e.getMessage());
            return -1L;
        }
    }
    private String cacheKey(String type, String period, String mode, Integer toMonth) {
        return type + "|" + period + "|" + mode + "|" + (toMonth == null ? "" : toMonth);
    }
    private void cleanupExpired() {
        long now = System.currentTimeMillis();
        for (Iterator<Map.Entry<String, Task>> it = tasks.entrySet().iterator(); it.hasNext(); ) {
            Task t = it.next().getValue();
            if (t.finishedAt > 0 && now - t.finishedAt > RESULT_TTL_MS) it.remove();
        }
        for (Iterator<Map.Entry<String, Task>> it = cache.entrySet().iterator(); it.hasNext(); ) {
            Task t = it.next().getValue();
            if (t.finishedAt <= 0 || now - t.finishedAt > CACHE_TTL_MS) it.remove();
        }
    }
    @PreDestroy
    public void shutdown() {
        pool.shutdownNow();
        try {
            pool.awaitTermination(3, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}
traffic-audit-web/src/views/DataImport.vue
@@ -150,6 +150,19 @@
                       :disabled="clearScope === 'types' && !clearTypes.length" @click="confirmClear">确认清空</el-button>
          </span>
        </el-dialog>
        <!-- å¯¼å…¥ç»“果弹窗:可拖动、内容区滚动、底部按钮固定可见(原 $alert å†…容长时底部按钮会被挤出屏幕) -->
        <el-dialog :title="resultDialog.title" :visible.sync="resultDialog.visible" width="860px" top="5vh"
                   append-to-body :close-on-click-modal="false" custom-class="import-result-dialog" v-dialog-drag>
          <div v-if="resultDialog.level" class="ird-level" :class="'ird-' + resultDialog.level">
            {{ resultDialog.level === 'error' ? '导入失败' : (resultDialog.level === 'warning' ? '存在需要确认的内容' : '导入成功') }}
          </div>
          <div class="ird-body" v-html="resultDialog.html"></div>
          <span slot="footer">
            <el-button v-if="resultDialog.rows.length" size="small" icon="el-icon-download"
                       @click="exportResultDialog">导出明细</el-button>
            <el-button type="primary" @click="resultDialog.visible = false">确认</el-button>
          </span>
        </el-dialog>
      </el-main>
    </el-container>
  </el-container>
@@ -193,6 +206,7 @@
      clearDialog: false,
      clearScope: 'all',
      clearTypes: [],
      resultDialog: { visible: false, title: '', html: '', rows: [], columns: [], fileName: '', level: '' },
      sideOpen: false,
      result: '',
      error: '',
@@ -463,11 +477,15 @@
            (f.city ? '(' + f.city + ')' : '') + ':解析 ' + (f.success || 0) + ' æ¡' +
            (f.error ? ' <span style="color:#F56C6C">' + f.error + '</span>' : '') + '</div>'
          ).join('')
          this.$alert(html, (label || type) + ' æ‰¹é‡å¯¼å…¥å®Œæˆï¼šå…± ' + d.total + ' ä¸ªæ–‡ä»¶' +
            (d.fail > 0 ? ',失败 ' + d.fail + ' æ¡' : '') + '(成功 ' + d.success + ' æ¡ï¼‰', {
            dangerouslyUseHTMLString: true,
            type: errCount > 0 ? 'warning' : 'success',
            confirmButtonText: '知道了'
          this.showResultDialog({
            title: (label || type) + ' æ‰¹é‡å¯¼å…¥å®Œæˆï¼šå…± ' + d.total + ' ä¸ªæ–‡ä»¶' +
              (d.fail > 0 ? ',失败 ' + d.fail + ' æ¡' : '') + '(成功 ' + d.success + ' æ¡ï¼‰',
            html: html,
            level: errCount > 0 ? 'warning' : 'success',
            columns: [{ key: 'fileName', label: '文件名' }, { key: 'city', label: '市州' },
                      { key: 'success', label: '解析条数' }, { key: 'error', label: '错误' }],
            rows: files.map(f => ({ fileName: f.fileName || '', city: f.city || '', success: f.success || 0, error: f.error || '' })),
            fileName: (label || '批量导入') + '_' + this.period + '_导入结果'
          })
          if (type === 'investment' || type === 'investmentLogistics') {
            this.checkInvest(this.period)
@@ -505,12 +523,16 @@
            (f.city ? '(' + f.city + ')' : '') + ':解析 ' + (f.success || 0) + ' ä¸ªé¡¹ç›®' +
            (f.error ? ' <span style="color:#F56C6C">' + f.error + '</span>' : '') + '</div>'
          ).join('')
          this.$alert(html, '市州单表批量导入完成:共 ' + d.total + ' æ¡ï¼ŒæˆåŠŸ ' + d.success + ' æ¡' +
            (d.fail > 0 ? ',失败 ' + d.fail + ' æ¡' : '') +
            '(十五五项目匹配 ' + d.fiveYearMatched + '/' + d.fiveYearTemplateProjects + ')', {
            dangerouslyUseHTMLString: true,
            type: errCount > 0 ? 'warning' : 'success',
            confirmButtonText: '知道了'
          this.showResultDialog({
            title: '市州单表批量导入完成:共 ' + d.total + ' æ¡ï¼ŒæˆåŠŸ ' + d.success + ' æ¡' +
              (d.fail > 0 ? ',失败 ' + d.fail + ' æ¡' : '') +
              '(十五五项目匹配 ' + d.fiveYearMatched + '/' + d.fiveYearTemplateProjects + ')',
            html: html,
            level: errCount > 0 ? 'warning' : 'success',
            columns: [{ key: 'fileName', label: '文件名' }, { key: 'city', label: '市州' },
                      { key: 'success', label: '解析项目数' }, { key: 'error', label: '错误' }],
            rows: files.map(f => ({ fileName: f.fileName || '', city: f.city || '', success: f.success || 0, error: f.error || '' })),
            fileName: '市州单表导入结果_' + this.period
          })
          this.afterImport()
        })
@@ -527,10 +549,10 @@
        const text = (msg && msg.indexOf('导入') !== -1) ? msg : '数据导入成功'
        this.$message({ message: text, type: 'success', duration: 5000 })
      } else {
        this.$alert((msg || '数据导入失败,详见下方失败记录').replace(/\n/g, '<br>'), '数据导入失败', {
          dangerouslyUseHTMLString: true,
          type: 'error',
          confirmButtonText: '知道了'
        this.showResultDialog({
          title: '数据导入失败',
          html: (msg || '数据导入失败,详见下方失败记录').replace(/\n/g, '<br>'),
          level: 'error'
        })
      }
    },
@@ -646,14 +668,55 @@
                '<br/>实际:' + (x.actualValue || '') + ' 基准:' + (x.thresholdValue || '') +
                '<br/>差异:' + (x.deviation || '') + '</div>'
            }).join('')
            this.$alert(html, '投资数据与投资系统不一致(' + pending.length + ' æ¡ï¼‰', {
              dangerouslyUseHTMLString: true, type: 'warning', confirmButtonText: '知道了'
            this.showResultDialog({
              title: '投资数据与投资系统不一致(' + pending.length + ' æ¡ï¼‰',
              html: html,
              level: 'warning',
              columns: [{ key: 'rule', label: '规则' }, { key: 'name', label: '项目/企业' },
                        { key: 'actual', label: '实际值' }, { key: 'base', label: '基准值' }, { key: 'dev', label: '差异' }],
              rows: pending.map(x => ({
                rule: x.ruleName || '',
                name: x.enterpriseName || x.actualValue || '',
                actual: x.actualValue || '',
                base: x.thresholdValue || '',
                dev: x.deviation || ''
              })),
              fileName: '投资数据与投资系统不一致_' + this.period
            })
          } else {
            this.$message.success('已与投资系统核对,本月数据一致')
          }
        })
        .catch(() => {})
    },
  showResultDialog(opts) {
      const o = opts || {}
      this.resultDialog = {
        visible: true,
        title: o.title || '导入结果',
        html: o.html || '',
        level: o.level || '',
        rows: o.rows || [],
        columns: o.columns || [],
        fileName: o.fileName || ('导入结果_' + (this.period || ''))
      }
    },
    exportResultDialog() {
      const cols = this.resultDialog.columns || []
      const rows = this.resultDialog.rows || []
      if (!cols.length || !rows.length) { this.$message.warning('没有可导出的明细'); return }
      const esc = v => '"' + String(v === null || v === undefined ? '' : v).replace(/"/g, '""') + '"'
      const lines = [cols.map(c => esc(c.label)).join(',')]
      rows.forEach(r => lines.push(cols.map(c => esc(r[c.key])).join(',')))
      const blob = new Blob(['\ufeff' + lines.join('\r\n')], { type: 'text/csv;charset=utf-8' })
      const url = URL.createObjectURL(blob)
      const a = document.createElement('a')
      a.href = url
      a.download = this.resultDialog.fileName + '.csv'
      document.body.appendChild(a)
      a.click()
      document.body.removeChild(a)
      URL.revokeObjectURL(url)
    },
  parseFailDetails(row) {
    if (!row || !row.errorDetail) return []
@@ -723,3 +786,17 @@
.module-tabs-head { margin-bottom: 10px; }
.module-tab-pane { min-height: 120px; }
</style>
<style>
/* å¯¼å…¥ç»“果弹窗:el-dialog é»˜è®¤ append-to-body,DOM æŒ‚在 body ä¸Šï¼Œscoped é€‰æ‹©å™¨åŒ¹é…ä¸åˆ°ï¼Œå¿…须写全局 */
.import-result-dialog { border-radius: 10px; overflow: hidden; }
.import-result-dialog .el-dialog__header { cursor: move; user-select: none; -webkit-user-select: none; }
.import-result-dialog .el-dialog__headerbtn { cursor: pointer; }
.import-result-dialog .el-dialog__body { max-height: calc(100vh - 210px); overflow-y: auto; overscroll-behavior: contain; padding: 12px 20px; }
.import-result-dialog .el-dialog__footer { border-top: 1px solid #EBEEF5; padding: 10px 20px; }
.import-result-dialog .ird-body { line-height: 1.7; font-size: 13px; color: #303133; }
.import-result-dialog .ird-level { margin-bottom: 8px; font-weight: 600; }
.import-result-dialog .ird-success { color: #67C23A; }
.import-result-dialog .ird-warning { color: #E6A23C; }
.import-result-dialog .ird-error { color: #F56C6C; }
</style>
traffic-audit-web/src/views/ReportExport.vue
@@ -160,6 +160,25 @@
            <el-button size="small" type="primary" @click="saveNote">保存</el-button>
          </span>
        </el-dialog>
        <!-- æ±‡æ€»å¤§è¡¨å¼‚步生成进度(生成一次约 10~20 ç§’,避免点击后长时间无响应) -->
        <el-dialog title="正在生成汇总大表" :visible.sync="taskVisible" width="460px"
                   :close-on-click-modal="false" :show-close="taskFinished">
          <div class="task-body">
            <el-progress :percentage="taskInfo.percent || 0" :status="taskInfo.status === 'FAILED' ? 'exception' : (taskDone ? 'success' : null)"
                         :stroke-width="14"></el-progress>
            <p class="task-step">{{ taskInfo.step || '排队中' }}</p>
            <p class="task-meta" v-if="taskInfo.period">报表期 {{ taskInfo.period }} {{ taskInfo.fromCache ? '上次生成用时' : '已用' }} {{ (taskInfo.elapsedMs / 1000).toFixed(1) }} ç§’</p>
            <p class="task-meta" v-if="taskInfo.fromCache">本次直接使用 {{ taskCacheTime }} ç”Ÿæˆçš„结果(10 åˆ†é’Ÿå†…且期间没有新导入数据)。如需按最新数据重算,请点「重新生成」。</p>
            <p class="task-error" v-if="taskInfo.status === 'FAILED'">{{ taskInfo.error }}</p>
          </div>
          <span slot="footer">
            <el-button size="small" type="primary" :disabled="!taskDone" @click="downloadTaskFile">{{ taskInfo.fromCache ? '下载' : '保存文件' }}</el-button>
            <el-button size="small" :disabled="taskRunning" @click="restartTask">重新生成</el-button>
            <el-button size="small" v-if="taskRunning" @click="minimizeTask">后台运行</el-button>
            <el-button size="small" v-else @click="closeTask">关闭</el-button>
          </span>
        </el-dialog>
      </el-main>
    </el-container>
  </el-container>
@@ -189,6 +208,11 @@
      noteDialogVisible: false,
      noteEditingKey: '',
      noteDraft: '',
      // æ±‡æ€»å¤§è¡¨å¼‚步生成任务
      taskVisible: false,
      taskInfo: {},
      taskTimer: null,
      taskKey: 'summaryWorkbook',
      modules: [
        {
          key: 'summary', title: '汇总大表', items: [
@@ -277,6 +301,26 @@
        options: months.map(mo => ({ label: y + 'å¹´' + mo + '月', value: y + '-' + String(mo).padStart(2, '0') }))
      }))
    },
    // ===== ä»¥ä¸‹ä¸ºæ±‡æ€»å¤§è¡¨å¼‚步生成弹窗的派生状态(必须并入同一个 computed,另起同名 key ä¼šè¦†ç›–掉上面的计算属性)=====
    taskRunning() {
      const s = this.taskInfo && this.taskInfo.status
      return s === 'PENDING' || s === 'RUNNING'
    },
    taskDone() {
      return this.taskInfo && this.taskInfo.status === 'DONE'
    },
    // ç”Ÿæˆç»“束(成功或失败)后才允许收起弹窗
    taskFinished() {
      const s = this.taskInfo && this.taskInfo.status
      return s === 'DONE' || s === 'FAILED'
    },
    taskCacheTime() {
      const t = this.taskInfo && this.taskInfo.finishedAt
      if (!t) return '上次'
      const d = new Date(t)
      const p = (n) => String(n).padStart(2, '0')
      return d.getFullYear() + '-' + p(d.getMonth() + 1) + '-' + p(d.getDate()) + ' ' + p(d.getHours()) + ':' + p(d.getMinutes()) + ':' + p(d.getSeconds())
    }
  },
  watch: {
    activeKey() {
@@ -289,7 +333,11 @@
    this.activeKey = (mine.length ? mine : this.visibleModules.map(m => m.key))[0]
    this.ensureCycle()
    this.refreshAll()
  },  methods: {
  },
  beforeDestroy() {
    this.stopTaskPolling()
  },
  methods: {
    moduleTitle(key) {
      const m = this.modules.find(x => x.key === key)
      return m ? m.title : key
@@ -420,28 +468,28 @@
        {
          name: '道路货运',
          rows: mk([
            ['当月货运量(万吨)', '全省货运量·当月'],
            ['当月货物周转量(万吨)', '全省货物周转量·当月', '全省货物周转量·当月同比'],
            ['累计货运量(万吨)', '全省货运量·累计'],
            ['当月货运量(万吨)', '全省货运量·当月', '全省货运量·当月同比'],
            ['当月货物周转量(万吨公里)', '全省货物周转量·当月', '全省货物周转量·当月同比'],
            ['累计货运量(万吨)', '全省货运量·累计', '全省货运量·累计同比'],
            ['累计货物周转量(万吨公里)', '全省货物周转量·累计', '全省货物周转量·累计同比']
          ])
        },
        {
          name: '城市客运',
          rows: mk([
            ['当月城市客运量:', '城市客运量·当月'],
            ['当月城市客运周转量', '城市客运周转量·当月'],
            ['累计城市客运量:', '城市客运量·累计'],
            ['累计城市客运周转量', '城市客运周转量·累计']
            ['当月城市客运量', '城市客运量·当月', '城市客运量·当月同比'],
            ['当月城市客运周转量', '城市客运周转量·当月', '城市客运周转量·当月同比'],
            ['累计城市客运量', '城市客运量·累计', '城市客运量·累计同比'],
            ['累计城市客运周转量', '城市客运周转量·累计', '城市客运周转量·累计同比']
          ])
        },
        {
          name: '中口径',
          rows: mk([
            ['当月中口径客运量', '中口径客运量·当月'],
            ['当月中口径周转量', '中口径周转量·当月'],
            ['累计中口径客运量', '中口径客运量·累计'],
            ['累计中口径周转量', '中口径周转量·累计']
            ['当月中口径客运量', '中口径客运量·当月', '中口径客运量·当月同比'],
            ['当月中口径周转量', '中口径周转量·当月', '中口径周转量·当月同比'],
            ['累计中口径客运量', '中口径客运量·累计', '中口径客运量·累计同比'],
            ['累计中口径周转量', '中口径周转量·累计', '中口径周转量·累计同比']
          ])
        }
      ]
@@ -468,6 +516,86 @@
      if (continuous) return '1~' + cov[cov.length - 1] + '月均有数据'
      return cov.map(x => x + '月').join('、') + ' æœ‰æ•°æ®'
    },
    // ==================== æ±‡æ€»å¤§è¡¨å¼‚步生成(进度弹窗) ====================
    // åŽç«¯ ReportExportTaskService:提交任务 -> è½®è¯¢è¿›åº¦ -> å®ŒæˆåŽä¸‹è½½æ–‡ä»¶ã€‚
    // åŒæŠ¥è¡¨æœŸ 10 åˆ†é’Ÿå†…且期间没有新导入数据时,后端直接返回上次结果(弹窗内会说明来源时间)。
    startSummaryTask(refresh) {
      if (!this.period) { this.$message.warning('请先选择周期和月份'); return }
      this.stopTaskPolling()
      this.taskInfo = { status: 'PENDING', percent: 0, step: '提交任务', period: this.period, elapsedMs: 0 }
      this.taskVisible = true
      this.loading = 'gen-' + this.curModule.key
      const params = Object.assign({}, this.buildParams(), { refresh: refresh ? 'true' : 'false' })
      api.get('/report/exportTask/summaryWorkbook', { params, timeout: 60000 })
        .then(res => {
          // åŽç«¯ç»Ÿä¸€è¿”回 Result:code éž 200 æ—¶ data ä¸ºç©ºï¼Œå¿…须提示而不是空转
          if (!res || res.code !== 200 || !res.data || !res.data.taskId) {
            this.taskInfo = { status: 'FAILED', percent: 0, step: '提交失败', period: this.period, elapsedMs: 0,
                              error: (res && res.message) || '后端未返回任务号' }
            this.loading = ''
            return
          }
          this.taskInfo = res.data
          if (this.taskInfo.status === 'DONE') { this.loading = ''; this.downloadTaskFile(); return }
          if (this.taskInfo.status === 'FAILED') { this.loading = ''; return }
          this.pollTask()
        })
        .catch(err => {
          this.stopTaskPolling()
          this.loading = ''
          this.taskVisible = false
          this.failWith(err, '生成')
        })
    },
    pollTask() {
      this.stopTaskPolling()
      const id = this.taskInfo && this.taskInfo.taskId
      if (!id) return
      this.taskTimer = setInterval(() => {
        api.get('/report/exportTask/' + id, { timeout: 30000 })
          .then(res => {
            if (!res || res.code !== 200 || !res.data) {
              // ä»»åŠ¡è¢«æ¸…ç†/过期:停止轮询并给出原因,不让弹窗一直空转
              this.stopTaskPolling()
              this.loading = ''
              this.taskInfo = Object.assign({}, this.taskInfo, {
                status: 'FAILED', step: '任务已失效',
                error: (res && res.message) || '导出任务已失效,请重新生成'
              })
              return
            }
            this.taskInfo = res.data
            if (this.taskInfo.status === 'DONE') {
              this.stopTaskPolling()
              this.loading = ''
              this.downloadTaskFile()
            } else if (this.taskInfo.status === 'FAILED') {
              this.stopTaskPolling()
              this.loading = ''
              if (!this.taskVisible) this.$message.error('汇总大表生成失败:' + (this.taskInfo.error || '未知原因'))
            }
          })
          .catch(() => { /* å•次轮询失败忽略,下一轮继续 */ })
      }, 700)
    },
    stopTaskPolling() {
      if (this.taskTimer) { clearInterval(this.taskTimer); this.taskTimer = null }
    },
    downloadTaskFile() {
      const id = this.taskInfo && this.taskInfo.taskId
      if (!id) return
      const name = this.taskInfo.fileName || ('生成_道路运输量汇总表_' + this.period + '.xlsx')
      api.get('/report/exportTask/' + id + '/file', { responseType: 'blob', timeout: 120000 })
        .then(res => { this.downloadBlob(res, name); this.$message.success('报表导出成功') })
        .catch(err => this.failWith(err))
    },
    restartTask() { this.startSummaryTask(true) },
    closeTask() { this.taskVisible = false; this.stopTaskPolling(); this.loading = '' },
    // ç”Ÿæˆä¸­æ”¶èµ·å¼¹çª—:任务继续在后台执行,完成后仍会自动下载(与原来同步导出的「点完即下载」一致)
    minimizeTask() {
      this.taskVisible = false
      this.$message.info('已转入后台生成,完成后会自动下载')
    },
    exportChecked(itemList) {
      const m = this.curModule
      if (!this.period) { this.$message.warning('请先选择周期和月份'); return }
@@ -477,6 +605,8 @@
      const params = this.buildParams()
      const doExport = () => {
        if (items.length === 1) {
          // æ±‡æ€»å¤§è¡¨ç”Ÿæˆä¸€æ¬¡çº¦ 10~20 ç§’,走异步任务(进度弹窗),避免点击后长时间无响应
          if (items[0].key === 'summaryWorkbook') { this.startSummaryTask(false); return }
          api.get('/report/export/' + items[0].key, { params, responseType: 'blob', timeout: 120000 })
            .then(res => { this.downloadBlob(res, this.fileNameOf(items[0])); this.$message.success('报表导出成功'); this.loading = '' })
            .catch(err => this.failWith(err))
@@ -621,6 +751,11 @@
.wb-yoy { text-align:right; color:#606266; }
.wb-yoy.yoy-empty { color:#C0C4CC; }
.wb-note { color:#E6A23C; font-size:12px; margin:2px 0 8px; }
/* å¼‚步生成进度弹窗 */
.task-body { padding: 4px 2px; }
.task-step { margin: 12px 0 4px; font-size: 14px; color:#303133; }
.task-meta { margin: 2px 0; font-size: 12px; color:#909399; line-height: 1.6; }
.task-error { margin: 8px 0 0; font-size: 13px; color:#F56C6C; word-break: break-all; }
</style>
<style>
@@ -629,4 +764,4 @@
.pv-dialog { border-radius: 10px; overflow: hidden; }
.pv-dialog .el-dialog__header { cursor: move; user-select: none; -webkit-user-select: none; }
.pv-dialog .el-dialog__headerbtn { cursor: pointer; }
</style>
</style>