6个文件已修改
6个文件已添加
1328 ■■■■■ 已修改文件
docs/功能测试报告/功能测试报告_数据导入页空白修复_2026-09-11.md 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_汇总大表8月数据回填与版式对齐_2026-09-11.md 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_汇总表排名页刷新与同比百分数_2026-09-11.md 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_网约车订单报告导入_2026-09-11.md 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/城市客运/模板_网约车订单.docx 补丁 | 查看 | 原始文档 | blame | 历史
docs/工作日结/工作日结_2026-09-11.md 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/controller/DataImportController.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java 212 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java 379 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/WycSplitCalc.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java 316 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/DataImport.vue 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_Êý¾Ýµ¼ÈëÒ³¿Õ°×ÐÞ¸´_2026-09-11.md
New file
@@ -0,0 +1,86 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_数据导入页空白修复_2026-09-11
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
**用户反馈**:登录后点「数据导入」,界面变成一片空白。
**排查与根因**:用无头 Chrome(CDP è¿œç¨‹è°ƒè¯•)复现后,控制台报:
```
[Vue warn]: Failed to mount component: template or render function not defined.
found in
---> <DataImport> at src/views/DataImport.vue
```
`traffic-audit-web/src/views/DataImport.vue` **缺少最外层的 `<template>` å¼€æ ‡ç­¾**,文件直接从 `<el-container>` å¼€å§‹ã€‚SFC æ²¡æœ‰ template å— â†’ vue-loader ç¼–不出 render å‡½æ•° â†’ ç»„件挂载失败 â†’ æ•´é¡µç©ºç™½ï¼ˆ`#app` åªå‰© 78 å­—符)。
历史沿革(`git log -S "SKIP/CHECK"`):
| æäº¤ | æ–‡ä»¶é¦–行 | çŠ¶æ€ |
| --- | --- | --- |
| `0f7b7d1^` | `<template>` | æ­£å¸¸ |
| `0f7b7d1` | `SKIP/CHECK (0): TYPE_LABELS upper <template>` | é¦–行被标记文字覆盖 â†’ å·²æŸå |
| `da614c0` | `  <el-container direction="vertical">` | æ ‡è®°è¡Œè¢«åˆ æŽ‰ï¼Œ`<template>` ä»æœªè¡¥å›ž â†’ ä»æŸå |
| æœ¬æ¬¡ä¿®å¤ | `<template>`(已补回) | æ­£å¸¸ |
**修复**:在文件首行补回 `<template>`(一行改动,未动其余内容)。
## äºŒã€æµ‹è¯•环境
- å‰ç«¯ `http://localhost:8080`(vue-cli-service å¼€å‘服务器,改后 17:44:48 é‡æ–°ç¼–译成功)。
- å¤çް/验证方式:无头 Chrome `--remote-debugging-port=9222` + CDP(`Page.navigate` / `Runtime.evaluate` / æŽ§åˆ¶å°äº‹ä»¶æ•获),先在 `sessionStorage` å†™å…¥ç™»å½• token å†è®¿é—®å—保护路由。
- æµ‹è¯•时间:2026-09-11。
## ä¸‰ã€ç”¨ä¾‹è¡¨
| ç¼–号 | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-1 | ä¿®å¤å‰è®¿é—® `#/import`(登录态) | å¤çŽ°ç”¨æˆ·é—®é¢˜ | æŽ§åˆ¶å° `Failed to mount component â€¦ <DataImport>`;`#app` innerHTML é•¿åº¦ 78,页面无文字 | å¤çŽ°æˆåŠŸ |
| TC-2 | ä¿®å¤åŽè®¿é—® `#/import` | é¡µé¢æ­£å¸¸æ¸²æŸ“ | `#app` innerHTML é•¿åº¦ **20,821**;可见「导入业务数据 / æŠ¥è¡¨æœŸ / é€‰æ‹©æ•°æ®ç±»åž‹ï¼ˆé“路货运 å…¬è·¯æ—…客 åŸŽå¸‚客运 ç«™åœºæŠ•资 è´§è¿èƒ½è€—季报)/ æ¨¡æ¿ï¼šæ¨¡æ¿_交企统H203-2表… / å¯¼å…¥ç»“果(2026-09)/ å¯¼å…¥è¿›åº¦ã€ | é€šè¿‡ |
| TC-3 | ä¿®å¤åŽæŽ§åˆ¶å° | æ—  Vue/JS é”™è¯¯ | é”™è¯¯äº‹ä»¶æ•° **0**(含 `Runtime.exceptionThrown`、`console.error`) | é€šè¿‡ |
| TC-4 | å…¨é‡è·¯ç”±æ‰«æ `#/dashboard`、`#/import`、`#/data`、`#/explain`、`#/audit`、`#/report`、`#/rules`、`#/users` | 8 ä¸ªé¡µé¢éƒ½èƒ½æ¸²æŸ“、无报错 | 8/8 æ¸²æŸ“成功(innerHTML é•¿åº¦ 4,409~293,029),每个页面错误数均为 0 | é€šè¿‡ |
| TC-5 | é™æ€æ£€æŸ¥æ‰€æœ‰ `.vue` æ˜¯å¦ä»¥ `<template>` å¼€å¤´ | æ— åŒç±»é—®é¢˜ | 13 ä¸ª `.vue` æ–‡ä»¶ä¸­ä»… `DataImport.vue` å¼‚常,修复后全部 OK | é€šè¿‡ |
| TC-N1 | è´Ÿå‘:用带 hash æŸ¥è¯¢ä¸²çš„地址访问 `#/import?t=…` | è¯´æ˜Žè·¯ç”±ä¸åŒ¹é…æ—¶çš„表现(辅助定位) | è·¯ç”±æ— åŒ¹é… â†’ ç©ºç™½ä¸”无报错(与本次 bug çŽ°è±¡ä¸åŒï¼Œæ®æ­¤æŽ’é™¤äº†ç¼“å­˜å‡è±¡ï¼‰ | é€šè¿‡ï¼ˆå®šä½ç”¨ï¼‰ |
## å››ã€å‘现的问题与处置
1. **首行被工具写入的标记文字覆盖**:`0f7b7d1` é‚£æ¬¡çš„补丁把 `<template>` æ¢æˆäº† `SKIP/CHECK (0): TYPE_LABELS upper <template>` è¿™æ ·çš„说明文字。提示:以后改 SFC é¦–行要复核首行是否仍是 `<template>`。
2. **同一文件近期被改名/改动多次**:本次仅补首行,未做其它改动,避免引入新风险。
3. å¤çŽ°è¿‡ç¨‹ä¸­å‘çŽ°æ— å¤´æµè§ˆå™¨ä¼šç¼“å­˜æ—§ bundle,需要 `Page.reload({ignoreCache:true})` æ‰èƒ½æ‹¿åˆ°æ–°ç¼–译结果(测试方法记录,非产品问题)。
## äº”、结论
- ã€Œæ•°æ®å¯¼å…¥ã€ç™½å±å·²ä¿®å¤ï¼Œé¡µé¢ä¸Žå…¶ä½™ 7 ä¸ªè·¯ç”±å‡æ­£å¸¸æ¸²æŸ“、控制台 0 æŠ¥é”™ã€‚
- å‰ç«¯å¼€å‘服务器已热编译生效,用户刷新页面(`Ctrl+F5`)即可恢复使用。
## å…­ã€å‘布与二次独立验证(2026-09-11 18:0x)
**代码提交**:`e8ce82e` fix: ä¿®å¤ã€Œæ•°æ®å¯¼å…¥ã€æ•´é¡µç©ºç™½â€”—DataImport.vue è¡¥å›žç¼ºå¤±çš„ `<template>` å¼€æ ‡ç­¾
| åŠ¨ä½œ | å‘½ä»¤ | ç»“æžœ |
| --- | --- | --- |
| æ‹‰å–公司仓库 | `git fetch gitea` | è¿œç«¯ `main` æ— æ–°æäº¤ï¼ˆleft/right = 1/0),无需合并 |
| æŽ¨é€å…¬å¸ä»“库 | `git push gitea main` | `a76a8cc..e8ce82e  main -> main` |
| æ‹‰å–个人仓库 | `git fetch origin` | è¿œç«¯ `master` æ— æ–°æäº¤ï¼ˆ1/0),无需合并 |
| æŽ¨é€ä¸ªäººä»“库 | `git push origin main:master` | `a76a8cc..e8ce82e  main -> master` |
| åŒæ­¥æœ¬åœ°åˆ†æ”¯æŒ‡é’ˆ | `git branch -f master main` | `master` = `e8ce82e`,`git status` æ˜¾ç¤º `main...gitea/main` å·²åŒæ­¥ |
**产物级验证(不依赖浏览器,直接校验开发服务器下发的编译结果)**
| æ£€æŸ¥é¡¹ | æ–¹æ³• | ç»“æžœ |
| --- | --- | --- |
| å‰ç«¯æ˜¯å¦å·²çƒ­ç¼–译 | è¯»å– `traffic-audit-web/frontend.log` | æœ€åŽä¸€æ¬¡ `DONE  Compiled successfully in 1251ms`,时间 17:44:48,`App running at: - Local: http://localhost:8080/` |
| ç«¯å£å­˜æ´» | `Test-NetConnection 127.0.0.1 -Port 8080/8090` | 8080=True、8090=True |
| ä¿®å¤æ˜¯å¦çœŸçš„进了下发文件 | ä»Ž `http://localhost:8080/js/6.js`(含 `TYPE_LABELS` çš„ DataImport åˆ†å—)抓取并检索 | å‘½ä¸­ `el-container`;上下文为 `var render = function render() { ... _c("el-container", { attrs: { direction: "vertical" } }) ... _c("app-header", { attrs: { title: "交通统计报表审核系统 - æ•°æ®å¯¼å…¥" } })`,即模板已正常编译为 render å‡½æ•° |
**端到端复验(无头 Chrome + CDP,独立重跑)**
| ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-6 | å†™å…¥ `sessionStorage.token` åŽè®¿é—® `http://localhost:8080/#/import`,并 `Page.reload({ignoreCache:true})` | é¡µé¢å®Œæ•´æ¸²æŸ“ | `location.href` = `http://localhost:8080/#/import`;`#app` innerHTML = **20,821** å­—符;顶栏标题「交通统计报表审核系统 - æ•°æ®å¯¼å…¥ã€ï¼›8 ä¸ªå·¦ä¾§èœå•、11 ä¸ªæŒ‰é’®ã€1 ä¸ªä¸Šä¼ æŽ§ä»¶ã€5 ä¸ªä¸‹æ‹‰æ¡†ã€3 ä¸ªå¡ç‰‡ | é€šè¿‡ |
| TC-7 | åŒä¸Šï¼Œé‡‡é›†æŽ§åˆ¶å° | 0 é”™è¯¯ | `Runtime.consoleAPICalled(error)` = `[]`;`Runtime.exceptionThrown` = `[]` | é€šè¿‡ |
| TC-8 | è®¿é—® `http://localhost:8080/import`(无 `#/`) | è¯´æ˜Žè·¯ç”±å½¢æ€ï¼ŒæŽ’除误判 | è·¯ç”±ä¸º **hash æ¨¡å¼**,该地址落到 `#/dashboard`(工作台),`#app` = 4409 å­—符、0 é”™è¯¯ | é€šè¿‡ï¼ˆæ¾„清项) |
**截图留证**:`_shots/数据导入页_修复后_2026-09-11.png`(1600x1000,整页捕获,可见「导入业务数据 / æŠ¥è¡¨æœŸ / é€‰æ‹©æ•°æ®ç±»åž‹ï¼šé“路货运 å…¬è·¯æ—…客 åŸŽå¸‚客运 ç«™åœºæŠ•资 è´§è¿èƒ½è€—季报 / æ¨¡æ¿ä¸‹è½½ / é€‰æ‹©æ–‡ä»¶ / ç«‹å³å¯¼å…¥ / å¯¼å…¥ç»“果(2026-09)/ å¯¼å…¥è¿›åº¦ã€ï¼‰ã€‚
**用户侧结论**:前端开发服务器已加载修复后的代码,用户浏览器若仍白屏,是本地缓存了旧 bundle,`Ctrl+F5` å¼ºåˆ·å³å¯æ¢å¤ã€‚
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_»ã×Ü´ó±í8ÔÂÊý¾Ý»ØÌîÓë°æÊ½¶ÔÆë_2026-09-11.md
New file
@@ -0,0 +1,70 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_汇总大表8月数据回填与版式对齐_2026-09-11
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
针对用户反馈的 4 ä¸ªé—®é¢˜ï¼Œå¯¹ã€Šé“路运输量汇总表》导出链路做数据驱动回填与版式对齐:
1. **8 æœˆæ•°æ®å·²å…¥åº“但汇总表没有 8 æœˆæ•°æ®** â†’ æ±‡æ€»å·¥ä½œç°¿å¡«å……器识别「当月值列」(8 æœˆä¸º Q åˆ—)并回填;
2. **同比一律以汇总表 2025 å¹´ 8 æœˆæ•°æ®ä¸ºå‡†** â†’ å½“月同比列(R åˆ—)公式统一为 `=Q../AJ..-1`,AJ åˆ—即 2025 å¹´ 8 æœˆï¼›
3. **公交、出租数据是日期形式** â†’ å›žå¡«æ•°å€¼æ—¶æŒ‰åŒè¡Œ 1 æœˆåˆ—样式套用,避免数值被套上日期格式(显示成 1902/1910 å¹´ï¼‰ï¼›
4. **班线包车、轨道轮渡只有分市州没有全省数据** â†’ ä¸ºå…¨çœè¡Œè¡¥é½åˆè®¡å…¬å¼ï¼ˆæ¯ç‰ˆåŽŸæœ¬åªå¯¹ 1 æœˆå†™æ­»å…¬å¼ï¼‰ï¼›
5. **版式完全按《生成_道路运输量汇总表.xlsx》** â†’ é€åˆ—套用参照件列宽/隐藏标记,**后续扩列(8 æœˆ Q/R åˆ—)也按参照件宽度**。
改动范围:
- `SummaryWorkbookFiller`:`fillFreightSheet` / `fillWycSheet` é‡å†™ï¼ˆåªå›žå¡«ç›®æ ‡æœŸå½“月,派生行保持公式);新增 `ensureMonthFormulas`、`ensureMonthYoyFormulas`、`monthColumnCapacity`;
- `ReportExportService`:导出时调用 `ensureMonthFormulas` + æ–°å¢ž `applySummaryReferenceLayout`(按参照件对齐列宽);
- `WycSplitCalc`:汇总表网约车取数优先读库。
关键口径:当月值列(0 åŸºï¼‰= `2 + 2*(m-1)`,同比列 = å€¼åˆ— + 1;同比分母固定为 AJ åˆ—(2025 å¹´ 8 æœˆï¼‰ã€‚
## äºŒã€æµ‹è¯•环境
- åŽç«¯ `http://localhost:8090`,导出接口 `GET /api/report/export/summaryWorkbook?period=YYYY-MM`。
- æ¯ç‰ˆï¼š`docs/生成汇总大表/2026å¹´8月道路运输量汇总表.xlsx`;版式参照:`docs/生成汇总大表/生成_道路运输量汇总表.xlsx`。
- æµ‹è¯•时间:2026-09-11。
## ä¸‰ã€ç”¨ä¾‹è¡¨
### 3.1 æ­£å‘:2026-08 ç”Ÿæˆä¸Žå½“月取值
| ç¼–号 | æ£€æŸ¥é¡¹ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-1 | å¯¼å‡º 2026-08 | HTTP 200,正常产出文件 | 4.6 ç§’返回,948,735 å­—节 | é€šè¿‡ |
| TC-2 | åŽç«¯å›žå¡«æ—¥å¿— | æœ‰å†™å…¥æ ¼æ•°ç»Ÿè®¡ã€æ— ç¼ºæ•°æç¤º | å†™å…¥å½“月值格数=1659,补写当月同比公式格数=600,补写当月合计公式格数=43,对齐列宽列数=422,缺数提示=[] | é€šè¿‡ |
| TC-3 | è´§è¿é¡µå…¨çœå—(行5-10)Q åˆ— | è§„上/规下货运量与周转量有值,合计行为公式 | è§„上货运量 3666.1401、规下 13222.6214、规上周转 570004.4355、规下周转 1700006.166;Q5=`=Q7+Q9`、Q6=`=Q8+Q10` | é€šè¿‡ |
| TC-4 | è´§è¿é¡µ Î£17市州 = å…¨çœ | ç›¸ç­‰ | Î£17市州货运量 = 16888.7615;全省 Q5 = 3666.1401 + 13222.6214 = 16888.7615 | é€šè¿‡ |
| TC-5 | è´§è¿é¡µå¸‚州派生行 | ä¸å†™æ­»å€¼ï¼Œä¿æŒå…¬å¼ | æ­¦æ±‰ Q14(规上+规下周转量)=`=Q15+Q16`;规上/规下分项为数值 | é€šè¿‡ |
| TC-6 | ç½‘约车页 | 17 å¸‚州当月有值,全省行为公式 | æ­¦æ±‰ Q9=1879.9049…、Q10=13407.6982…;全省 Q5=`=Q9+Q13+…+Q73`;Σ17市州客运量=4997.74、周转量=29166.7 | é€šè¿‡ |
| TC-7 | å…¬äº¤é¡µ | å¸‚州当月有值且**不是日期显示** | Q9=3856.83(武汉客运量),单元格格式 `0.00`;全省 Q5 ä¸ºæ±‚和公式 | é€šè¿‡ |
| TC-8 | å‡ºç§Ÿé¡µ | åŒä¸Š | Q9=1058.95(武汉客运量),格式 `0.00`;全省 Q5 ä¸ºæ±‚和公式 | é€šè¿‡ |
| TC-9 | ç­çº¿åŒ…车页全省行 | æœ‰å…¨çœåˆè®¡ï¼ˆåŽŸç¼ºå¤±ï¼‰ | Q4=`=Q8+Q10+Q14+…+Q56`、Q5=`=Q9+Q11+…+Q57`、Q6/Q7 ä¸ªä½“合计亦为公式 | é€šè¿‡ |
| TC-10 | è½¨é“、轮渡页全省行 | æœ‰å…¨çœåˆè®¡ï¼ˆåŽŸç¼ºå¤±ï¼‰ | è½¨é“ å…¨çœ Q4=`=SUM(Q6,Q8)`、Q5=`=Q7+Q9`;轮渡 å…¨çœ Q13=`=Q15`、Q14=`=Q16` | é€šè¿‡ |
| TC-11 | åŒæ¯”公式指向 2025 å¹´ 8 æœˆ | R åˆ—分母为 AJ åˆ— | å„页表头 AJ åˆ— = `2025å¹´8月`;R åˆ—公式形如 `=Q9/AJ9-1`;AJ åˆ—确有 2025 å¹´ 8 æœˆæ¯ç‰ˆæ•°å€¼ï¼ˆå¦‚货运 AJ7=3716.1158) | é€šè¿‡ |
| TC-12 | æ‰“开即重算 | æ–‡ä»¶å¸¦å…¨é‡é‡ç®—标记 | `xl/workbook.xml` å†… `<calcPr calcId="191029" fullCalcOnLoad="true"/>`,Excel/WPS æ‰“开即算出全省合计 | é€šè¿‡ |
### 3.2 ç‰ˆå¼å¯¹é½ï¼ˆç¬¬ 5 é¡¹ï¼‰
| ç¼–号 | æ£€æŸ¥é¡¹ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-13 | å…¨è¡¨åˆ—宽与《生成_道路运输量汇总表》比对(展开 `<col min..max>` èŒƒå›´é€åˆ—比对) | å®Œå…¨ä¸€è‡´ | 19 ä¸ªé¡µç­¾å…¨éƒ¨æ¯”对,**真实差异 0 åˆ—** | é€šè¿‡ |
| TC-14 | æ‰©åˆ—(8 æœˆ Q/R)列宽 | ä¸Žå‚ç…§ä»¶ 8 æœˆä¸€è‡´ | è´§è¿ P/Q/R = 12.67/10.55/11.67(参照 12.67/10.56/11.67,仅 0.01 æµ®ç‚¹èˆå…¥ï¼‰ï¼›ç½‘约车/轨道轮渡 Q/R=13.0/13.0;公交 Q/R=11.89/13.0;出租 Q/R=11.89/13.0;班线包车 Q/R=10.22/9.78 â€”— å‡ä¸Žå‚照件相同 | é€šè¿‡ |
### 3.3 è´Ÿå‘与回归
| ç¼–号 | æ£€æŸ¥é¡¹ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-15 | åŽ†å²æœˆï¼ˆ2026-07)不得被当月回填污染:与 `docs/生成汇总大表/_备份_2026å¹´7月道路运输量汇总表_清理前_20260904.xlsx` é€æ ¼æ¯”对 | é™¤æ—¢å®šæ”¹é€ å¤–无差异 | å·®å¼‚共三类:① 426 å¤„浮点末位差(最大相对差 7.26e-15,属 Excel 15 ä½æœ‰æ•ˆæ•°å­—截断,无实质差异);② 57 å¤„"文本 vs ç­‰å€¼æ•°å­—";③ 148 å¤„全部集中在「中口径排名」页 â€”— è¯¥é¡µå·¦ä¾§é™ˆæ—§å—已于 2026-09-08 æŒ‰éœ€æ±‚整块删除,属**预期变化**。**无回归** | é€šè¿‡ |
| TC-16 | ç¼ºæœˆæç¤º | ç¼ºæ•°æ®çš„æœˆä»½æœ‰æç¤º | 2026-08 å¯¼å‡º `缺数提示=[]` | é€šè¿‡ |
## å››ã€å‘现的问题与处置
1. **问题(已修复)**:`monthColumnCapacity` åŽŸå®žçŽ°åªè®¤"含 2026+累计"的表头,而货运页表头是"累计周转量",导致其恒返回默认列数、8 æœˆåˆ—永不回填。**处置**:改为「累计列」与「最后一个月份/日期格式月列」取较大值。
2. **问题(已修复)**:`shiftFormulaColumns` è¿žç»­ç»„引用被拼成 `Q5AJ5O5`。**处置**:`sb.append(quoteReplacement(...))` â†’ `m.appendReplacement(sb, quoteReplacement(...))`。
3. **问题(已修复)**:`ensureMonthYoyFormulas` ä¸­ `setCellFormula` å¤±è´¥è¢«é™é»˜åžæŽ‰ã€‚**处置**:改为抛出含页签/单元格/原式/目标式的 `IllegalStateException`,便于定位。
4. **问题(已修复)**:回填目标格若被套上日期格式,数值会显示成 1902/1910 å¹´ï¼ˆå³ç”¨æˆ·åé¦ˆçš„"日期形式")。**处置**:`setNumeric` å¯¹ç©º/日期格式格套用同行 1 æœˆåˆ—样式;对已有公式格直接跳过不覆盖。
5. **已知无害告警**:2026-08 å¯¼å‡ºæ—¶æ—¥å¿—出现 `汇总工作簿未找到同期备份母版(_备份_2026å¹´8月…),输出将保持母版现状`。原因是 8 æœˆæ²¡æœ‰"清理前备份母版",而 8 æœˆæ¯ç‰ˆæœ¬èº«å·²æ˜¯å®Œæ•´æ¯ç‰ˆï¼Œæ— éœ€ä»Žå¤‡ä»½è¿˜åŽŸï¼Œæ•…ä¸å½±å“è¾“å‡ºï¼ˆå·²é€é¡¹éªŒè¯å–å€¼æ­£ç¡®ï¼‰ã€‚7 æœˆå› å­˜åœ¨ `_备份_2026å¹´7月…` æ— æ­¤å‘Šè­¦ã€‚
6. **性能注意**:`applySummaryReferenceLayout` çš„列数上限保留 `min(参照件已用列数, ç›®æ ‡ä»¶å·²ç”¨åˆ—æ•° + 4)` â€”— åŽ»æŽ‰è¯¥ä¸Šé™ä¼šè®© POI åœ¨å¤§åˆ—号(货运页 147 åˆ—)上出现指数级卡顿。
## äº”、结论
8 æœˆæ±‡æ€»å¤§è¡¨å·²èƒ½ç”±åº“中数据自动生成:货运规上/规下、网约车、公交、出租、班线包车、轨道轮渡的当月列均已回填,全省合计与分市州合计一致;同比统一取 2025 å¹´ 8 æœˆï¼›å…¬äº¤/出租不再显示为日期;班线包车与轨道轮渡的全省数据已补齐(公式,打开即算);全表列宽与用户指定的《生成_道路运输量汇总表》逐列一致(含 8 æœˆæ‰©åˆ—宽度)。7 æœˆåŽ†å²æœˆå›žå½’æ— å®žè´¨å·®å¼‚ã€‚
遗留:请将真实的《网约车行业2026å¹´8月运行监测情况分析报告》重传一次,覆盖当前用 7 æœˆæŠ¥å‘Šä¸´æ—¶å‰¯æœ¬å¯¼å…¥çš„ 8 æœˆå¸‚州订单数。
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_»ã×ܱíÅÅÃûҳˢÐÂÓëͬ±È°Ù·ÖÊý_2026-09-11.md
New file
@@ -0,0 +1,80 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_汇总表排名页刷新与同比百分数_2026-09-11
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
用户反馈《道路运输量汇总表》两个问题:**1. æ‰€æœ‰æŽ’名表里面的数据没有更新;2. åŒæ¯”应该是百分数而不是小数**。本轮定位并修复:
### 1. æŽ’名页数据刷新(问题 1)
- çŽ°è±¡ï¼šæ±‡æ€»å¤§è¡¨æ¯ç‰ˆ `2026å¹´8月道路运输量汇总表.xlsx` é‡Œçš„ `货运量排名` / `货运周转量排名` / `中口径排名` ä¸‰é¡µæ˜¯**上一报表期的静态值**(标题写「2026å¹´1-7月 / 2026å¹´7月」,数值也是 7 æœˆï¼‰ï¼Œå¯¼å‡º 8 æœˆæ—¶æ•´é¡µä¸éšæ•°æ®å˜åŒ–。
- å¤„置:导出链路新增 `refreshSummaryRankSheets`,用程序自己的独立排名表(`生成_货运量排名.xlsx` / `生成_周转量排名.xlsx` / `生成_中口径排名.xlsx`,与用户日常核对口径一致)刷新这三页:
  - æ•°å€¼æ ¼æ¬æ•°å€¼ï¼›æºä¾§æ˜¯å…¬å¼çš„(RANK/占比等)按目标块偏移整体平移后写成公式(打开时按汇总表自身数据重算);目标格本身已是公式的(母版 SUM/RANK/占比)保留不动;
  - å—标题随报表期同步(`2026å¹´1-7月…` â†’ `2026å¹´1-8月…`、`2026å¹´7月…` â†’ `2026å¹´8月…`)。
### 2. åŒæ¯”列显示成小数(问题 2)
- çŽ°è±¡ï¼šæ¯ç‰ˆæŠŠæ–°æœˆä»½ï¼ˆ8 æœˆï¼‰çš„同比列做成了「普通数值 `0.00`」格式,而 7 æœˆåŠä»¥å‰çš„同比列是 `0.00%`;回填公式后 8 æœˆåŒæ¯”显示成 `-0.0294` è¿™æ ·çš„小数。
- å¤„置:`SummaryWorkbookFiller.ensureMonthYoyFormulas` åŽŸæ¥åªåœ¨ç›®æ ‡æ ¼ä¸ºç©ºæ—¶å¥—ç”¨ä¸ŠæœˆåŒæ¯”åˆ—æ ·å¼ï¼Œæ”¹ä¸º**无条件沿用上月同比列样式**(`0.00%`)。
### æ”¹åŠ¨æ–‡ä»¶
- `traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java`:新增 `refreshSummaryRankSheets` / `copyRankValues` / `rankCell` / `shiftFormula`,在 `ensureMonthFormulas` ä¹‹åŽã€`applySummaryReferenceLayout` ä¹‹å‰è°ƒç”¨ã€‚
- `traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java`:`ensureMonthYoyFormulas` æ ·å¼æ²¿ç”¨ã€‚
## äºŒã€æµ‹è¯•环境
- åŽç«¯ `http://localhost:8090`(`start-dev.ps1` å¯åŠ¨ï¼‰ï¼Œå¯¼å‡ºæŽ¥å£ `GET /api/report/export/summaryWorkbook?period=YYYY-MM`。
- æ¯ç‰ˆï¼š`docs/生成汇总大表/2026å¹´8月道路运输量汇总表.xlsx`;版式参照:`docs/生成汇总大表/生成_道路运输量汇总表.xlsx`。
- ç‹¬ç«‹æŽ’名表底稿:`docs/货运/生成_货运量排名.xlsx`、`docs/货运/生成_周转量排名.xlsx`、`docs/公路旅客/生成_中口径排名.xlsx`。
- æµ‹è¯•时间:2026-09-11;测试产物:`_gen2_2026-08.xlsx`、`_gen2_2026-07.xlsx`、`_v2_freight_rank.xlsx`、`_v2_turnover_rank.xlsx`、`_v2_mid_rank.xlsx`。
## ä¸‰ã€ç”¨ä¾‹è¡¨
### 3.1 æ­£å‘用例
| ç¼–号 | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-1 | å¯¼å‡º `period=2026-08` | HTTP 200,正常产出文件 | 949,956 å­—节,23.8 ç§’返回 | é€šè¿‡ |
| TC-2 | åŽç«¯å›žå¡«æ—¥å¿— | æœ‰æŽ’名页刷新统计、无缺数提示 | å†™å…¥å½“月值格数=1659,补写当月同比公式格数=600,补写当月合计公式格数=43,**刷新排名页数值格数=711**,对齐列宽列数=422,缺数提示=[] | é€šè¿‡ |
| TC-3 | è´§è¿é‡æŽ’名块标题 | éšæŠ¥è¡¨æœŸæ”¹ä¸º 1-8 æœˆ/8 æœˆ | `A1=2026å¹´1-8月全省分市州累计完成公路货运量情况`;`A22=2026å¹´8月全省分市州完成公路货运量情况` | é€šè¿‡ |
| TC-4 | è´§è¿å‘¨è½¬é‡æŽ’名块标题 | åŒä¸Š | `A2=…2026å¹´1-8月…`;`A24=…2026å¹´8月…` | é€šè¿‡ |
| TC-5 | ä¸­å£å¾„排名块标题 | åŒä¸Š | `J1=2026å¹´1-8月全省分市州累计完成道路客运生产情况`;`J22=2026å¹´8月…` | é€šè¿‡ |
| TC-6 | æ ‡é¢˜è½ç›˜æ–¹å¼ï¼ˆåŽŸå§‹ XML æ ¡éªŒï¼‰ | `<is><t>` ä¸Žæ˜¾ç¤ºä¸€è‡´ï¼ˆä¸èƒ½å†å‡ºçް `<v>` æ–°å€¼ + `<is>` æ—§å€¼ï¼‰ | `xl/worksheets/sheet2.xml`:`<c r="A1" s="776" t="s"><v>0</v></c>`,共享串 0 = `2026å¹´1-8月…`(旧文件是 inlineStr,`<v>`=8月、`<is><t>`=7月) | é€šè¿‡ |
| TC-7 | è´§è¿é‡æŽ’名逐格入库抽查(与独立排名表比对) | å…¨å—一致 | ç´¯è®¡å— + å½“月块共 **454 æ ¼é€ä¸ªæ¯”对,0 å¤„不一致** | é€šè¿‡ |
| TC-8 | è´§è¿å‘¨è½¬é‡æŽ’名当月块抽查 | ä¸Žç‹¬ç«‹è¡¨ä¸€è‡´ | æ­¦æ±‰ B28=192320.5226(源 B27)、G28=504369.61288609065(源 G27)、E28=0.3334642531154999(源 E27)、J28=-0.021161120884320206(源 J27) | é€šè¿‡ |
| TC-9 | è´§è¿å‘¨è½¬é‡æŽ’名母版公式保留 | ä¸å†™æ­»å€¼ï¼Œä¿ç•™ SUM/RANK/占比 | å…¨çœ B27=`=SUM(B28:B44)`;武汉 C28=`=RANK(B28,B$28:B$44)`、D28=`=B28/B$27` | é€šè¿‡ |
| TC-10 | ä¸­å£å¾„排名数值抽查 | ä¸Žç‹¬ç«‹è¡¨ä¸€è‡´ | K4=2759.8855(源 B4)、O4=152330.9312(源 F4)、K25=349.536(源 L4)、O25=18917.1637(源 P4) | é€šè¿‡ |
| TC-11 | ä¸­å£å¾„排名「增速排名」列 | ä¸å†æ˜¯é™æ€æ€ªå€¼ï¼Œä¸”与页内同比自洽 | ç”±é™æ€å€¼ï¼ˆå¤šè¡ŒåŒä¸º 1.0)改为 `=RANK(M4,M$4:M$20)`、`=RANK(M25,M$25:M$41)` ç­‰è‡ªæ´½å…¬å¼ï¼›ç¼“存值与母版原值一致(L25=5、N25=10、P25=2、R25=5) | é€šè¿‡ |
| TC-12 | 8 æœˆåŒæ¯”列格式(问题 2) | ç™¾åˆ†æ•° `0.00%` | è´§è¿ R5=`=Q5/AJ5-1` / `0.00%`;班线包车、城市客运、公交、出租、网约车、轨道轮渡、中口径明细 8 æœˆåŒæ¯”列(R åˆ—)**全部 `0.00%`** | é€šè¿‡ |
| TC-13 | æŽ’名页同比列格式 | ç™¾åˆ†æ•° `0.00%` | è´§è¿é‡æŽ’名 E4=-0.0763 / `0.00%`、D5=0.3207… / `0.00%`;货运周转量排名、中口径排名同比列同为 `0.00%` | é€šè¿‡ |
| TC-14 | æ‰“开即重算 | ä»å¸¦å…¨é‡é‡ç®—标记 | `<calcPr â€¦ fullCalcOnLoad="true"/>` ä¿ç•™ | é€šè¿‡ |
### 3.2 è´Ÿå‘用例
| ç¼–号 | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-N1 | å…¨è¡¨æ‰«æã€Œè¡¨å¤´å«åŒæ¯” + æ•°å€¼/公式 + æ ¼å¼éžç™¾åˆ†æ•°ã€çš„æ ¼ | ä¸åº”有新引入的小数同比 | 8 æœˆä»¶å‘½ä¸­ 3 æ ¼ï¼Œå…¨éƒ¨åœ¨éšè—é¡µï¼ˆ`2025年城市客运量!E13`、`中口径计算过程!G85/G86`),母版同位置同样如此 | é€šè¿‡ï¼ˆéžæœ¬è½®å¼•入) |
| TC-N2 | ç‹¬ç«‹æŽ’名表源侧同比为空时(库中无 2025 å¹´ä¸­å£å¾„数据) | ä¸åº”把 POI å¯¹ç©ºå€¼çš„æ±‚值结果(全为 1)搬进汇总表 | æ”¹ä¸ºå†™å…¬å¼åŽï¼Œè¯¥åˆ—为 `=RANK(…)` å…¬å¼ï¼ŒæŒ‰é¡µå†…同比取值,不再是 17 è¡Œå…¨ 1 | é€šè¿‡ |
| TC-N3 | è°ƒè¯•日志清理 | å¯¼å‡ºæ—¥å¿—不应残留 `RANKDBG` | `backend.log` ä¸­ `RANKDBG` è®¡æ•° = 0 | é€šè¿‡ |
### 3.3 åŽ†å²æœˆå›žå½’ï¼ˆ2026-07)
| ç¼–号 | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- |
| TC-R1 | å¯¼å‡º `period=2026-07` | æ­£å¸¸äº§å‡ºã€å½“月同比仍指向 2025 å¹´ 7 æœˆ | 941,903 å­—节;货运 P5=`=O5/AF5-1` / `0.00%` | é€šè¿‡ |
| TC-R2 | ä¸Žæ”¹åŠ¨å‰ 7 æœˆäº§ç‰©ï¼ˆ`_gen_2026-07.xlsx`)逐格 diff | å·®å¼‚仅限被刷新的排名页 | ä»… `货运量排名` 51 æ ¼ã€`中口径排名` 133 æ ¼æœ‰å·®å¼‚(排名页由静态值改为按数据刷新,属预期);其余 16 é¡µ 0 å·®å¼‚ | é€šè¿‡ |
| TC-R3 | 7 æœˆæŽ’名页标题 | ä»ä¸º 1-7 æœˆ/7 æœˆï¼ˆæŒ‰æŠ¥è¡¨æœŸï¼‰ | `A1=2026å¹´1-7月…`、`A22=2026å¹´7月…`;中口径 `J22=2026å¹´7月…` | é€šè¿‡ |
| TC-R4 | 7 æœˆåŒæ¯”格式 | æ— æ–°å¢žå°æ•°åŒæ¯” | å…¨è¡¨å‘½ä¸­ 3 æ ¼ï¼Œå‡ä¸ºéšè—é¡µæ¯ç‰ˆåŽŸæœ‰æ ¼ | é€šè¿‡ |
## å››ã€å‘现的问题与处置
1. **排名页标题改了但 Excel ä»æ˜¾ç¤ºæ—§æœˆä»½**:母版标题单元格是 `inlineStr`,POI åœ¨ `t="inlineStr"` å•元格上 `setCellValue(String)` åªå†™ `<v>`、不动 `<is><t>`,而 Excel/WPS ä¼˜å…ˆè¯» `<is><t>`。处置:写值前先 `setBlank()` æ¸…掉原单元格再写(落盘为共享串,`<is>` ä¸å†æ®‹ç•™æ—§æ–‡æ¡ˆï¼‰ã€‚
2. **POI æ±‚值口径偏差**:源独立排名表里手工留空的同比会让 `RANK` å‡½æ•°æ±‚值一律返回 1,直接搬值会把 17 è¡Œã€Œå¢žé€ŸæŽ’名」写成全 1。处置:源侧公式不再搬求值结果,而是按目标块偏移平移成公式写入,再用汇总表自身数据求值缓存。
3. **中口径排名同比列仍为母版旧值**:库中只有 2026-01~08 çš„客运数据(`h2031_enterprise_monthly` / `city_bus_monthly`),没有 2025 å®šç¨¿æ•°æ®ï¼Œç‹¬ç«‹æŽ’名表算不出中口径同比 â†’ è¯¥åˆ—按设计保留母版值;等 2025 æ•°æ®å…¥åº“后随导出自动刷新。
4. **静态页待办**:`城市汇总`、`城市分市州`、`中口径分析`、`道路运输周转量` å››é¡µåœ¨æ¯ç‰ˆé‡Œæ˜¯æ•´é¡µé™æ€ï¼ˆæœ¬è½®å¯¼å‡ºä¸Žæ¯ç‰ˆé€æ ¼ 0 å·®å¼‚),页标题仍写「1-7 æœˆ / 7 æœˆã€ã€‚因整页数据未数据驱动,本轮**未**只改标题(避免标题 8 æœˆã€æ•°æ® 7 æœˆï¼‰ï¼Œç•™å¾…后续做成数据驱动时一并处理。
## äº”、结论
- ç”¨æˆ·åé¦ˆçš„两个问题均已修复并验证:三张排名页的标题与数值随报表期刷新(货运量排名 454 æ ¼é€æ ¼æ¯”对 0 å·®å¼‚),8 æœˆåŒæ¯”列统一为百分数显示。
- 2026-08 / 2026-07 åŒæœŸå¯¼å‡ºé€šè¿‡ï¼Œ7 æœˆåŽ†å²æœˆå›žå½’å·®å¼‚ä»…é™è¢«åˆ·æ–°çš„æŽ’åé¡µï¼Œæ— å‰¯ä½œç”¨ã€‚
- é—留:中口径同比待 2025 å¹´æ•°æ®å…¥åº“;四张静态成品页待数据驱动改造。
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_ÍøÔ¼³µ¶©µ¥±¨¸æµ¼Èë_2026-09-11.md
New file
@@ -0,0 +1,41 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_网约车订单报告导入_2026-09-11
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
支持直接上传部级《网约车行业YYYYå¹´M月运行监测情况分析报告》(.docx),系统自动解析报告正文附件表中的 **17 å¸‚州订单数**并写入数据库 `wyc_order_monthly`(F åˆ—订单合计 + G-W 17 å¸‚州订单列);
汇总大表「网约车」页取数改为**优先读库**,实现"上传报告 â†’ æ±‡æ€»è¡¨å½“月网约车数据自动出现"。
改动范围:
- åŽç«¯ `DataImportService.importWycOrderReport()` / `parseWycOrderReportTables()` / `periodOfDocx()`:解析报告、定位报表期、落库 `wyc_order_monthly`,并在缺失时从 `wyc_total_monthly` è¡¥é½å…¨çœ pin(B-E)。
- åŽç«¯ `DataImportController`:新增 `POST /api/import/wycOrderReport`(参数 file + period)。
- åŽç«¯ `WycSplitCalc.loadMonthlyInputFromDb()`:汇总表取数优先读 `wyc_order_monthly`(库里该期订单全空时回退读 `docs/城市客运/模板_网约车订单及全省总量.xlsx`)。
- å‰ç«¯ `DataImport.vue`:导入类型新增「网约车订单报告」,模板提示为 `城市客运/模板_网约车订单.docx`,接口 `/import/wycOrderReport`,文件类型限 `.docx/.doc`。
- æ¸…空映射:`wycOrderReport` å…³è”清空 `WYC_ORDER` æ‰¹æ¬¡ï¼›`wycOrder` ä¸Ž `wycOrderReport` äº’为关联清除。
报表期取值规则:优先取报告标题中的「网约车行业YYYY年M月」,取不到时用页面所选报表期;两者都取不到则报错。
## äºŒã€æµ‹è¯•环境
- åŽç«¯ï¼šSpring Boot,`http://localhost:8090`(8090 å·²ç›‘听);前端:`http://localhost:8080`。
- æ•°æ®åº“:MySQL 5.7 `127.0.0.1:3308` / `traffic_audit`。
- ç™»å½•:admin;测试时间:2026-09-11。
## ä¸‰ã€ç”¨ä¾‹è¡¨
| ç¼–号 | ç±»åž‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
| --- | --- | --- | --- | --- | --- |
| TC-1 | æ­£å‘ | ä¸Šä¼  `docs/城市客运/模板_网约车订单.docx`(《网约车行业2026å¹´7月运行监测情况分析报告》),period=2026-07 | è§£æžæˆåŠŸï¼ŒæŠ¥è¡¨æœŸå–æ ‡é¢˜çš„ 2026-07,17 å¸‚州全部命中 | è¿”回"报表期 2026-07,订单合计 29,730,791 å•,已写入 17 ä¸ªå¸‚å·ž" | é€šè¿‡ |
| TC-2 | æ­£å‘ | åŒä¸Šï¼Œæ£€æŸ¥åŽç«¯æ—¥å¿— | è®°å½• period/sum/cities/missing | `WycOrderReport imported: period=2026-07 sum=2.9730791E7 cities=17 missing=[]` | é€šè¿‡ |
| TC-3 | å…¥åº“抽查 | docx é™„件表「地市名称 / è®¢å•数总数」17 è¡Œï¼Œé€é¡¹ä¸Ž `wyc_order_monthly` æ¯”对 | 17 å¸‚州逐一相等,合计相等 | æ­¦æ±‰ 11,183,267;黄石 983,296;十堰 2,155,144;宜昌 3,024,336;襄阳 2,390,572;鄂州 321,348;荆门 942,124;孝感 1,220,812;荆州 1,573,514;黄冈 1,125,829;咸宁 928,460;随州 528,210;恩施 2,166,015;仙桃 528,557;潜江 267,879;天门 387,894;林区 3,534;合计 29,730,791 â€”— **与 DB å®Œå…¨ä¸€è‡´** | é€šè¿‡ |
| TC-4 | è´Ÿå‘(文件类型错) | æŠŠ `模板_网约车订单及全省总量.xlsx` ä¼ ç»™æŠ¥å‘Šå¯¼å…¥æŽ¥å£ | ç»™å‡ºå¯è¯»æç¤ºï¼Œä¸è½åº“ | è¿”回"导入失败: æ— æ³•读取 Word æ–‡æ¡£ï¼Œè¯·ä¸Šä¼  .docx æ ¼å¼çš„《网约车行业YYYYå¹´M月运行监测情况分析报告》;若上传的其实是 Excel/PDF,请改用对应模板导入。(原始错误:…)" | é€šè¿‡ï¼ˆå«ä¿®å¤ï¼Œè§ç¬¬å››èŠ‚ï¼‰ |
| TC-5 | è´Ÿå‘(内容不符) | ä¸Šä¼  `城市客运数据优化.docx`(非网约车监测报告) | æ˜Žç¡®æç¤ºæœªè§£æžåˆ°å¸‚州订单数 | è¿”回"导入失败: æœªè§£æžåˆ°ä»»ä½•市州订单数,请确认上传的是《网约车行业YYYYå¹´M月运行监测情况分析报告》" | é€šè¿‡ |
| TC-6 | æ­£å‘(重复导入幂等) | åŒä¸€ä»½ 7 æœˆæŠ¥å‘Šè¿žç»­å¯¼å…¥ä¸¤æ¬¡ | ç¬¬äºŒæ¬¡è¦†ç›–同报表期行,不产生重复行 | ä»è¿”回 17 å¸‚å·žï¼›`wyc_order_monthly` ä¸­ `2026-07` ä»… 1 è¡Œ | é€šè¿‡ |
| TC-7 | è”è°ƒ | æŠ¥å‘Šå¯¼å…¥åŽé‡æ–°å¯¼å‡º `period=2026-08` æ±‡æ€»å¤§è¡¨ | ç½‘约车页当月列(Q åˆ—)出现数值,全省行为求和公式 | Q åˆ— 17 å¸‚州全部有值,Q5 èµ· 4 ä¸ªå…¨çœè¡Œä¸º `=Q9+Q13+…` æ±‚和公式,R åˆ—同比公式 `=Q../AJ..-1`(AJ=2025å¹´8月) | é€šè¿‡ï¼ˆè¯¦è§ã€ŠåŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_汇总大表8月数据回填与版式对齐_2026-09-11.md》) |
## å››ã€å‘现的问题与处置
1. **问题(已修复)**:把 Excel/PDF è¯¯ä¼ åˆ°ã€Œç½‘约车订单报告」类型时,接口返回的是 xmlbeans çš„英文技术异常(`org.apache.xmlbeans.XmlException: ... is not a valid document`),页面用户无法理解。
   **处置**:`DataImportService.importWycOrderReport()` çš„解析段统一捕获异常,改写为用户可读提示,并把原始错误以"(原始错误:…)"附在末尾便于排查。修复后 TC-4 å¤æµ‹é€šè¿‡ã€‚
2. **需要用户操作的事项**:当前库中 `2026-08` çš„**市州订单数**是用 7 æœˆæŠ¥å‘Šæ”¹æ ‡é¢˜çš„临时副本(`_t_wyc_aug.docx`)导入的,其市州值与 7 æœˆç›¸åŒï¼›`pin`(B-E,即客运量/周转量口径)取自 `wyc_total_monthly` çš„ 2026-08 è¡Œï¼Œæ˜¯æ­£ç¡®çš„。**请把真实的《网约车行业2026å¹´8月运行监测情况分析报告》重新上传一次**(类型选「网约车订单报告」,报表期选 2026-08),即可覆盖为真实 8 æœˆå¸‚州订单数。
## äº”、结论
网约车订单运行监测报告(docx)导入功能可用:正向解析 17 å¸‚州订单数准确入库并与源文件逐项一致;文件类型错误、内容不符两类负向场景均能给出明确提示;重复导入幂等;导入后汇总大表可自动取到当月数据。建议将真实 8 æœˆæŠ¥å‘Šé‡ä¼ åŽå†æ¬¡å¯¼å‡ºæ ¸å¯¹ã€‚
docs/³ÇÊпÍÔË/Ä£°å_ÍøÔ¼³µ¶©µ¥.docx
Binary files differ
docs/¹¤×÷ÈÕ½á/¹¤×÷ÈÕ½á_2026-09-11.md
New file
@@ -0,0 +1,66 @@
# å·¥ä½œæ—¥ç»“_2026-09-11
## ä¸€ã€ä»Šå¤©åšäº†ä»€ä¹ˆ
### 1. åŸŽå¸‚客运:网约车订单运行监测报告(docx)导入(用户第 1 é¡¹ï¼‰
- åŽç«¯æ–°å¢ž `POST /api/import/wycOrderReport` ä¸Ž `DataImportService.importWycOrderReport()`:解析《网约车行业YYYYå¹´M月运行监测情况分析报告》附件表「地市名称 / è®¢å•数总数」,取 17 å¸‚州订单数写入 `wyc_order_monthly`(F è®¢å•合计 + G-W å¸‚州列),报表期优先取报告标题年月。
- å…¨çœ pin(B-E)在该期行缺失时,从 `wyc_total_monthly` åŒæŠ¥è¡¨æœŸè¡¥é½ã€‚
- å‰ç«¯å¯¼å…¥ç•Œé¢æ–°å¢žç±»åž‹ã€Œç½‘约车订单报告」,绑定 `城市客运/模板_网约车订单.docx`(限 .docx/.doc),并接入按类型清空。
- æ±‡æ€»å¤§è¡¨ç½‘约车取数改为**优先读库**(`WycSplitCalc.loadMonthlyInputFromDb`),库里无数据自动回退读 xlsx æ¯ç‰ˆã€‚
- ä¿®å¤ï¼šè¯¯ä¼  Excel æ—¶æŠ›å‡º xmlbeans è‹±æ–‡å¼‚常 â†’ æ”¹ä¸ºç”¨æˆ·å¯è¯»æç¤ºã€‚
### 2. æ±‡æ€»å¤§è¡¨ 8 æœˆæ•°æ®å›žå¡«ä¸Žç‰ˆå¼å¯¹é½ï¼ˆç”¨æˆ·ç¬¬ 2/3/4/5 é¡¹ï¼‰
- 8 æœˆæºæ•°æ®å·²åœ¨åº“ â†’ å¯¼å‡ºæ—¶è‡ªåŠ¨å›žå¡«å½“æœˆå€¼åˆ—ï¼ˆQ åˆ—),历史月不动。
- åŒæ¯”统一以汇总表 **2025 å¹´ 8 æœˆï¼ˆAJ åˆ—)** ä¸ºåˆ†æ¯ï¼Œå…¬å¼ `=Q../AJ..-1`。
- å…¬äº¤/出租数值不再被日期格式吞掉(按同行 1 æœˆåˆ—样式套用,`0.00`)。
- ç­çº¿åŒ…车、轨道轮渡「全省」行补齐合计公式(母版原只对 1 æœˆå†™æ­»å…¬å¼ï¼‰ã€‚
- ç‰ˆå¼æŒ‰ç”¨æˆ·æŒ‡å®šå‚ç…§ä»¶ `docs/生成汇总大表/生成_道路运输量汇总表.xlsx` é€åˆ—对齐列宽/隐藏;**8 æœˆæ‰©åˆ— Q/R çš„宽度也取自参照件**。
### 3. æ±‡æ€»è¡¨æŽ’名页刷新 + åŒæ¯”百分数(用户新反馈 2 é¡¹ï¼‰
- ç”¨æˆ·åé¦ˆã€Œè´§è¿çš„æ±‡æ€»è¡¨é‡Œ 1.所有排名表数据没有更新;2.同比应该是百分数而不是小数」。
- æŽ’名页:母版里 `货运量排名`/`货运周转量排名`/`中口径排名` ä¸‰é¡µæ˜¯ä¸Šä¸€æœŸçš„静态值。导出时改用程序自己的独立排名表(生成_货运量排名/周转量排名/中口径排名)刷新这三页:数值搬值、源侧公式按偏移平移后写公式(打开重算)、母版自带公式保留、块标题随报表期同步(1-7月→1-8月)。
- åŒæ¯”格式:`ensureMonthYoyFormulas` æ”¹ä¸ºæ— æ¡ä»¶æ²¿ç”¨ä¸ŠæœˆåŒæ¯”列样式(母版把新月份同比列做成普通数值 `0.00`,显示成小数)。
- ä¿®æŽ‰ä¸¤ä¸ªå‘:① æ¯ç‰ˆæ ‡é¢˜æ˜¯ inlineStr,POI `setCellValue` åªå†™ `<v>` ä¸æ›´æ–° `<is>`,Excel ç»§ç»­æ˜¾ç¤º 7 æœˆ â†’ å…ˆ `setBlank()` å†å†™ï¼›â‘¡ ç›´æŽ¥æ¬ POI å¯¹æºè¡¨ RANK çš„æ±‚值结果会把「增速排名」写成 17 è¡Œå…¨ 1 â†’ æ”¹ä¸ºå¹³ç§»å†™å…¬å¼ã€‚
### 3. ä¿®ã€Œæ•°æ®å¯¼å…¥ã€ç™½å±ï¼ˆç”¨æˆ·ç´§æ€¥åé¦ˆï¼‰
- çŽ°è±¡ï¼šç™»å½•åŽç‚¹ã€Œæ•°æ®å¯¼å…¥ã€æ•´é¡µç©ºç™½ã€‚
- æ ¹å› ï¼š`DataImport.vue` é¦–行的 `<template>` è¢«æ—©å‰æŸæ¬¡è¡¥ä¸è¦†ç›–成说明文字 `SKIP/CHECK (0): â€¦`,本次推送的 `da614c0` åˆæŠŠè¿™è¡Œåˆ æŽ‰ï¼Œå¯¼è‡´ SFC æ²¡æœ‰ template å— â†’ `Failed to mount component: template or render function not defined` â†’ æ•´é¡µç©ºç™½ï¼ˆ`#app` ä»… 78 å­—符)。
- ä¿®å¤ï¼šé¦–行补回 `<template>`(单行改动);无头 Chrome + CDP å¤çŽ°å¹¶éªŒè¯ï¼Œ8 ä¸ªè·¯ç”±å…¨éƒ¨æ­£å¸¸ã€æŽ§åˆ¶å° 0 æŠ¥é”™ã€‚
### 3. æ–‡æ¡£å½’æ¡£
- æ˜¨å¤©å·²å®Œæˆã€Šæ¹–北省道路货物运输量分市州统计方法(2023)编制说明》学习笔记归档(`docs/货运/学习笔记_分市州道路货物运输量统计方法_2026-09-10.md`),今天未再改动。
## äºŒã€æµ‹è¯•结果摘要(详见《功能测试报告》)
- `docs/功能测试报告/功能测试报告_网约车订单报告导入_2026-09-11.md`:TC-1~TC-7 å…¨éƒ¨é€šè¿‡ï¼›7 æœˆæŠ¥å‘Šè§£æž 17 å¸‚州、合计 29,730,791,与库中逐项一致;两类负向用例提示明确。
- `docs/功能测试报告/功能测试报告_汇总大表8月数据回填与版式对齐_2026-09-11.md`:TC-1~TC-16 å…¨éƒ¨é€šè¿‡ã€‚
- `docs/功能测试报告/功能测试报告_汇总表排名页刷新与同比百分数_2026-09-11.md`:正向 14 ä¾‹ / è´Ÿå‘ 3 ä¾‹ / å›žå½’ 4 ä¾‹å…¨éƒ¨é€šè¿‡ã€‚
- `docs/功能测试报告/功能测试报告_数据导入页空白修复_2026-09-11.md`:5 ä¾‹é€šè¿‡ï¼ˆä¿®å¤å‰å¤çŽ°ç™½å± â†’ ä¿®å¤åŽ `#app` 20,821 å­—符、8 ä¸ªè·¯ç”±å…¨é€šè¿‡ã€0 æŽ§åˆ¶å°æŠ¥é”™ï¼‰ã€‚
  - 2026-08 å¯¼å‡ºæˆåŠŸï¼ˆ23.8s / 949,956 å­—节);日志「刷新排名页数值格数=711,缺数提示=[]」。
  - ä¸‰å¼ æŽ’名页标题已到 1-8 æœˆ/8 æœˆï¼›è´§è¿é‡æŽ’名与独立排名表逐格比对 454 æ ¼ 0 å·®å¼‚ï¼›8 æœˆåŒæ¯”列全部 `0.00%`。
  - 7 æœˆåŽ†å²æœˆå›žå½’ï¼šä»…è¢«åˆ·æ–°çš„æŽ’åé¡µæœ‰é¢„æœŸå·®å¼‚ï¼ˆè´§è¿é‡æŽ’å 51 æ ¼ã€ä¸­å£å¾„排名 133 æ ¼ï¼‰ï¼Œå…¶ä½™ 16 é¡µ 0 å·®å¼‚。
  - 2026-08 å¯¼å‡ºæˆåŠŸï¼ˆ4.6s / 948,735 å­—节);写入当月值 1659 æ ¼ã€å½“月同比公式 600 æ ¼ã€å½“月合计公式 43 æ ¼ã€ç¼ºæ•°æç¤ºä¸ºç©ºã€‚
  - è´§è¿ Î£17 å¸‚å·ž = å…¨çœï¼ˆ16888.7615);网约车/公交/出租/班线包车/轨道轮渡全省行与市州行均正确。
  - åˆ—宽与参照件**真实差异 0 åˆ—**。
  - 7 æœˆåŽ†å²æœˆå›žå½’ï¼šæ— å®žè´¨å·®å¼‚ï¼ˆ148 å¤„差异全部是「中口径排名」页 09-08 å·²æŒ‰éœ€æ±‚删除的旧左块)。
## ä¸‰ã€é—留问题与待办
1. **需用户操作**:库中 `2026-08` çš„市州订单数目前是用 7 æœˆæŠ¥å‘Šæ”¹æ ‡é¢˜çš„临时副本(`_t_wyc_aug.docx`)导入的,市州值与 7 æœˆç›¸åŒï¼›è¯·æŠŠçœŸå®žçš„ 8 æœˆè¿è¡Œç›‘测报告重传一次(类型「网约车订单报告」,报表期 2026-08)覆盖。
2. 2026-08 å¯¼å‡ºä¼šæ‰“印「未找到同期备份母版(_备份_2026å¹´8月…)」告警,属无害提示(8 æœˆæ¯ç‰ˆæœ¬èº«å®Œæ•´ï¼‰ï¼ŒåŽç»­å¯è€ƒè™‘按需求决定是否消除。
3. æ ¹ç›®å½•堆积较多历史临时文件(`_t_*.py`、`_*.sql`、`_gen_*.xlsx`、`_bak_*.java`、`_token.txt` ç­‰ï¼‰ï¼Œå¾…用户同意后集中清理。
4. ç‰ˆå¼å‚ç…§ä»¶ `docs/生成汇总大表/生成_道路运输量汇总表.xlsx` ç›®å‰ä¸ºæœªå…¥åº“文件,是否纳入版本管理待确认。
5. ä¸­å£å¾„排名的「同比增速」列仍是母版旧值:库中只有 2026 å¹´æ•°æ®ï¼Œæ²¡æœ‰ 2025 å¹´ä¸­å£å¾„定稿数据,算不出 8 æœˆåŒæ¯”;等 2025 æ•°æ®å…¥åº“后自动刷新。
6. `城市汇总`/`城市分市州`/`中口径分析`/`道路运输周转量` å››é¡µåœ¨æ¯ç‰ˆé‡Œæ˜¯æ•´é¡µé™æ€ï¼ˆæœ¬è½®å¯¼å‡ºä¸Žæ¯ç‰ˆ 0 å·®å¼‚),标题仍写 7 æœˆï¼Œéœ€åŽç»­åšæˆæ•°æ®é©±åŠ¨åŽä¸€å¹¶æ›´æ–°ã€‚
### 4. ã€Œæ•°æ®å¯¼å…¥ã€ç™½å±ä¿®å¤çš„发布(2026-09-11 å‚æ™šï¼‰
- æäº¤ `e8ce82e`(`DataImport.vue` è¡¥å›žç¼ºå¤±çš„ `<template>` å¼€æ ‡ç­¾ï¼‰å·²æŽ¨é€åŒä»“库:`git push gitea main`(`a76a8cc..e8ce82e`)、`git push origin main:master`(`a76a8cc..e8ce82e`),并 `git branch -f master main` åŒæ­¥æœ¬åœ° `master` æŒ‡é’ˆã€‚推送前 `git fetch gitea` / `git fetch origin` å‡ç¡®è®¤è¿œç«¯æ— æ–°æäº¤ï¼Œæœªå‘生覆盖。
- å‘布后做了两层独立复验:① äº§ç‰©å±‚——从开发服务器抓 `http://localhost:8080/js/6.js`,确认 DataImport åˆ†å—已含编译后的 `render` å‡½æ•°ï¼ˆ`_c("el-container", ...)`、标题「交通统计报表审核系统 - æ•°æ®å¯¼å…¥ã€ï¼‰ï¼›â‘¡ ç«¯åˆ°ç«¯â€”—无头 Chrome + CDP è®¿é—® `#/import`,`#app` 20,821 å­—符、8 èœå•/11 æŒ‰é’®/1 ä¸Šä¼ æŽ§ä»¶é½å…¨ã€æŽ§åˆ¶å° 0 é”™è¯¯ï¼Œæˆªå›¾å­˜ `_shots/数据导入页_修复后_2026-09-11.png`。
- å¤ç›˜æ¾„清:本项目前端路由是 hash æ¨¡å¼ï¼Œå¤éªŒåœ°å€å¿…须是 `http://localhost:8080/#/import`;直接访问 `/import` ä¼šé‡å®šå‘到 `#/dashboard`,容易被误判成「白屏没修好」。
- æŠ¥å‘Šï¼š`docs/功能测试报告/功能测试报告_数据导入页空白修复_2026-09-11.md`(已补「六、发布与二次独立验证」)。
## å››ã€æ˜Žå¤©è®¡åˆ’
1. ç”¨æˆ·é‡ä¼ çœŸå®ž 8 æœˆç½‘约车报告后,重新导出 2026-08 å¹¶é€é¡¹å¤æ ¸ç½‘约车页。
2. ç»§ç»­æŒ‰ç”¨æˆ·åé¦ˆæ ¸å¯¹ 8 æœˆæ±‡æ€»å¤§è¡¨æˆå“ï¼ˆé‡ç‚¹ï¼šä¸­å£å¾„各页、货运排名页)。
3. æ¸…理临时文件(需用户确认)。
4. è§†ç”¨æˆ·åé¦ˆå†³å®šæ˜¯å¦æŠŠå››å¼ é™æ€æˆå“é¡µï¼ˆåŸŽå¸‚汇总/城市分市州/中口径分析/道路运输周转量)做成数据驱动。
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/controller/DataImportController.java
@@ -269,6 +269,24 @@
        }
    }
    /** ç½‘约车订单运行监测分析报告(docx:《网约车行业YYYYå¹´M月运行监测情况分析报告》) */
    @PostMapping("/wycOrderReport")
    public Result<String> importWycOrderReport(@RequestParam("file") MultipartFile file,
                                               @RequestParam("period") String period) {
        try {
            DataImportService.WycOrderReportResult r = importService.importWycOrderReport(file, period);
            StringBuilder msg = new StringBuilder("网约车订单报告导入成功:报表期 " + r.getPeriod()
                    + ",订单合计 " + String.format("%,.0f", r.getOrderSum()) + " å•,已写入 " + r.getCityCount() + " ä¸ªå¸‚å·ž");
            if (!r.getMissingCities().isEmpty()) {
                msg.append(";以下市州未在报告中解析到,未写入:").append(String.join("、", r.getMissingCities()));
            }
            return Result.ok(msg.toString());
        } catch (Exception e) {
            log.error("wycOrderReport import error", e);
            return Result.error("导入失败: " + e.getMessage());
        }
    }
    /** æŠ•资月报(全省汇总大表):category=客运站场/物流园区 */
    @PostMapping("/investment")
    public Result<String> importInvestment(@RequestParam("file") MultipartFile file,
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java
@@ -56,6 +56,10 @@
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFTable;
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
import cn.hutool.core.io.IoUtil;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -1345,6 +1349,7 @@
        CLEAR_TYPE_TO_OP.put("cityTaxiAuth", "CITY_TAXI_AUTH");
        CLEAR_TYPE_TO_OP.put("wycTotal", "WYC_TOTAL");
        CLEAR_TYPE_TO_OP.put("wycOrder", "WYC_ORDER");
        CLEAR_TYPE_TO_OP.put("wycOrderReport", "WYC_ORDER");
        CLEAR_TYPE_TO_AUDIT.put("h2032", new String[]{"H2032"});
        CLEAR_TYPE_TO_AUDIT.put("transportAuth", new String[]{"H2032"});
@@ -1386,7 +1391,8 @@
        CLEAR_TYPE_TO_BATCH.put("cityTaxi", new String[]{"CITY_TAXI"});
        CLEAR_TYPE_TO_BATCH.put("cityTaxiAuth", new String[]{"CITY_TAXI_AUTH"});
        CLEAR_TYPE_TO_BATCH.put("wycTotal", new String[]{"WYC_TOTAL"});
        CLEAR_TYPE_TO_BATCH.put("wycOrder", new String[]{"WYC_ORDER"});
        CLEAR_TYPE_TO_BATCH.put("wycOrder", new String[]{"WYC_ORDER", "WYC_ORDER_REPORT"});
        CLEAR_TYPE_TO_BATCH.put("wycOrderReport", new String[]{"WYC_ORDER_REPORT", "WYC_ORDER"});
    }
    /**
@@ -3144,6 +3150,209 @@
        return ok;
    }
    // ========== ç½‘约车订单运行监测分析报告(docx)导入 ==========
    /** ç½‘约车订单报告导入结果(报表期/订单合计/覆盖市州数/缺失市州) */
    public static class WycOrderReportResult {
        private final String period;
        private final double orderSum;
        private final int cityCount;
        private final List<String> missingCities;
        public WycOrderReportResult(String period, double orderSum, int cityCount, List<String> missingCities) {
            this.period = period;
            this.orderSum = orderSum;
            this.cityCount = cityCount;
            this.missingCities = missingCities;
        }
        public String getPeriod() { return period; }
        public double getOrderSum() { return orderSum; }
        public int getCityCount() { return cityCount; }
        public List<String> getMissingCities() { return missingCities; }
    }
    /**
     * ã€Šç½‘约车行业YYYYå¹´M月运行监测情况分析报告》(docx)导入:
     * è§£æžæŠ¥å‘Šé™„件中「地市名称 | â€¦ | è®¢å•数总数」表的 17 å¸‚州订单数,写入 wyc_order_monthly çš„
     * F è®¢å•合计与 G-W 17 å¸‚州订单列;全省 pin(B-E)沿用该期已有行,若无该期行则从
     * wyc_total_monthly åŒæŠ¥è¡¨æœŸå¤åˆ¶ã€‚报表期优先取报告标题年月,取不到时用页面所选报表期。
     */
    public synchronized WycOrderReportResult importWycOrderReport(MultipartFile file, String period) throws Exception {
        Map<String, Double> orders = new LinkedHashMap<>();
        double[] sumHolder = new double[]{0.0};
        String docPeriod;
        try (java.io.InputStream in = file.getInputStream();
             XWPFDocument doc = new XWPFDocument(in)) {
            docPeriod = periodOfDocx(doc);
            parseWycOrderReportTables(doc, orders, sumHolder);
        } catch (Exception e) {
            throw new RuntimeException("无法读取 Word æ–‡æ¡£ï¼Œè¯·ä¸Šä¼  .docx æ ¼å¼çš„《网约车行业YYYYå¹´M月运行监测情况分析报告》;"
                    + "若上传的其实是 Excel/PDF,请改用对应模板导入。(原始错误:" + e.getMessage() + ")");
        }
        if (orders.isEmpty()) {
            throw new RuntimeException("未解析到任何市州订单数,请确认上传的是《网约车行业YYYYå¹´M月运行监测情况分析报告》");
        }
        String rp = null;
        if (docPeriod != null && docPeriod.matches("\\d{4}-\\d{2}")) rp = docPeriod;
        else if (period != null && period.matches("\\d{4}-\\d{2}")) rp = period;
        if (rp == null) {
            throw new RuntimeException("无法确定报表期:报告标题未含「YYYYå¹´M月」,且页面未选择报表期");
        }
        List<String> missing = new ArrayList<>();
        double citySum = 0.0;
        for (String city : RegionUtil.cityList()) {
            Double v = orders.get(city);
            if (v == null) missing.add(city);
            else citySum += v;
        }
        double sum = sumHolder[0] > 0 ? sumHolder[0] : citySum;
        WycOrderMonthly it = wycOrderMapper.selectOne(new LambdaQueryWrapper<WycOrderMonthly>()
                .eq(WycOrderMonthly::getReportPeriod, rp).last("LIMIT 1"));
        if (it == null) it = new WycOrderMonthly();
        it.setReportPeriod(rp);
        it.setOrderSum(sum > 0 ? sum : null);
        it.setOrderWuhan(orderOf(orders, "武汉市"));
        it.setOrderHuangshi(orderOf(orders, "黄石市"));
        it.setOrderShiyan(orderOf(orders, "十堰市"));
        it.setOrderYichang(orderOf(orders, "宜昌市"));
        it.setOrderXiangyang(orderOf(orders, "襄阳市"));
        it.setOrderEzhou(orderOf(orders, "鄂州市"));
        it.setOrderJingmen(orderOf(orders, "荆门市"));
        it.setOrderXiaogan(orderOf(orders, "孝感市"));
        it.setOrderJingzhou(orderOf(orders, "荆州市"));
        it.setOrderHuanggang(orderOf(orders, "黄冈市"));
        it.setOrderXianning(orderOf(orders, "咸宁市"));
        it.setOrderSuizhou(orderOf(orders, "随州市"));
        it.setOrderEnshi(orderOf(orders, "恩施州"));
        it.setOrderXiantao(orderOf(orders, "仙桃市"));
        it.setOrderQianjiang(orderOf(orders, "潜江市"));
        it.setOrderTianmen(orderOf(orders, "天门市"));
        it.setOrderShennong(orderOf(orders, "神农架林区"));
        if (it.getPinK() == null || it.getPinCK() == null || it.getPinZ() == null || it.getPinCZ() == null) {
            WycTotalMonthly tot = wycTotalMapper.selectOne(new LambdaQueryWrapper<WycTotalMonthly>()
                    .eq(WycTotalMonthly::getReportPeriod, rp).last("LIMIT 1"));
            if (tot != null) {
                if (it.getPinK() == null) it.setPinK(tot.getPinK());
                if (it.getPinCK() == null) it.setPinCK(tot.getPinCK());
                if (it.getPinZ() == null) it.setPinZ(tot.getPinZ());
                if (it.getPinCZ() == null) it.setPinCZ(tot.getPinCZ());
            }
        }
        wycOrderMapper.delete(new LambdaQueryWrapper<WycOrderMonthly>().eq(WycOrderMonthly::getReportPeriod, rp));
        wycOrderMapper.insert(it);
        recordBatch(file.getOriginalFilename(), "WYC_ORDER_REPORT", rp, 1, 1, 0, null);
        log.info("WycOrderReport imported: period={} sum={} cities={} missing={}", rp, sum, orders.size(), missing);
        return new WycOrderReportResult(rp, sum, orders.size(), missing);
    }
    /** æŠ¥å‘Šæ ‡é¢˜ä¸­çš„æŠ¥è¡¨æœŸï¼šç½‘约车行业2026å¹´7月 -> 2026-07;取不到返回 null */
    private String periodOfDocx(XWPFDocument doc) {
        java.util.regex.Pattern titlePat = java.util.regex.Pattern.compile("网约车行业\\s*(\\d{4})\\s*å¹´\\s*(\\d{1,2})\\s*月");
        java.util.regex.Pattern anyPat = java.util.regex.Pattern.compile("(\\d{4})\\s*å¹´\\s*(\\d{1,2})\\s*月");
        String fallback = null;
        int scanned = 0;
        for (XWPFParagraph para : doc.getParagraphs()) {
            String text = para.getText();
            if (text == null) continue;
            String flat = text.replaceAll("\\s+", "");
            if (flat.isEmpty()) continue;
            java.util.regex.Matcher m = titlePat.matcher(flat);
            if (m.find()) return ymd(m.group(1), m.group(2));
            if (fallback == null && scanned < 10) {
                java.util.regex.Matcher m2 = anyPat.matcher(flat);
                if (m2.find()) fallback = ymd(m2.group(1), m2.group(2));
            }
            scanned++;
        }
        return fallback;
    }
    private String ymd(String y, String mo) {
        try {
            int month = Integer.parseInt(mo);
            if (month < 1 || month > 12) return null;
            return String.format("%04d-%02d", Integer.parseInt(y), month);
        } catch (NumberFormatException e) {
            return null;
        }
    }
    /** è§£æžæŠ¥å‘Šä¸­çš„「地市名称 | â€¦ | è®¢å•数总数」表,orders=市州订单,sumHolder[0]=合计行订单数 */
    private void parseWycOrderReportTables(XWPFDocument doc, Map<String, Double> orders, double[] sumHolder) {
        for (XWPFTable table : doc.getTables()) {
            List<XWPFTableRow> rows = table.getRows();
            if (rows.size() < 3) continue;
            int headerRow = -1;
            int orderCol = -1;
            for (int r = 0; r < Math.min(rows.size(), 3); r++) {
                List<org.apache.poi.xwpf.usermodel.XWPFTableCell> cells = rows.get(r).getTableCells();
                for (int c = 0; c < cells.size(); c++) {
                    String norm = flat(cells.get(c).getText());
                    if (norm.contains("订单数总数") || "订单数".equals(norm)) {
                        headerRow = r;
                        orderCol = c;
                        break;
                    }
                }
                if (headerRow >= 0) break;
            }
            if (headerRow < 0 || orderCol < 0) continue;
            Map<String, Double> found = new LinkedHashMap<>();
            double citySum = 0.0;
            boolean hasTotal = false;
            for (int r = headerRow + 1; r < rows.size(); r++) {
                List<org.apache.poi.xwpf.usermodel.XWPFTableCell> cells = rows.get(r).getTableCells();
                if (orderCol >= cells.size()) continue;
                String name = flat(cells.get(0).getText());
                if (name.isEmpty()) continue;
                Double val = parsePlainNumber(cells.get(orderCol).getText());
                if (val == null) continue;
                String city = RegionUtil.normalizeCityName(name);
                if (city != null && RegionUtil.cityList().contains(city)) {
                    found.put(city, val);
                    citySum += val;
                } else if (name.contains("合计") || name.contains("总计") || name.contains("全省") || name.contains("湖北")) {
                    hasTotal = true;
                    sumHolder[0] = val;
                }
            }
            if (!found.isEmpty()) {
                orders.clear();
                orders.putAll(found);
                if (!hasTotal) sumHolder[0] = citySum;
                return;
            }
        }
    }
    private String flat(String s) {
        return s == null ? "" : s.replaceAll("\\s+", "").trim();
    }
    /** ã€Œ11,183,267 å•」这类文本取数值 */
    private Double parsePlainNumber(String s) {
        if (s == null) return null;
        String t = s.replaceAll("[,\\s]", "").trim();
        if (t.isEmpty()) return null;
        java.util.regex.Matcher m = java.util.regex.Pattern.compile("-?\\d+(?:\\.\\d+)?").matcher(t);
        if (!m.find()) return null;
        try {
            return Double.parseDouble(m.group());
        } catch (NumberFormatException e) {
            return null;
        }
    }
    private Double orderOf(Map<String, Double> orders, String city) {
        Double v = orders.get(city);
        return (v == null || v <= 0) ? null : v;
    }
    private String periodOfCell(Cell c) {
        if (c == null) return null;
        if (c.getCellType() == CellType.NUMERIC) {
@@ -3513,6 +3722,7 @@
            case "scaleSplit": return new ImportResult(importScaleSplit(file, period), 0, new ArrayList<>());
            case "wycTotal": return new ImportResult(importWycTotal(file, period), 0, new ArrayList<>());
            case "wycOrder": return new ImportResult(importWycOrder(file, period), 0, new ArrayList<>());
            case "wycOrderReport": return new ImportResult(importWycOrderReport(file, period).getCityCount(), 0, new ArrayList<>());
            default: throw new RuntimeException("不支持的导入类型:" + type);
        }
    }
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java
@@ -1,6 +1,8 @@
package com.trafficaudit.reportexport.calc;
import com.trafficaudit.common.util.RegionUtil;
import com.trafficaudit.reportexport.calc.FreightCalc.FreightMatrix;
import com.trafficaudit.reportexport.calc.FreightCalc.FreightMetrics;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
@@ -36,12 +38,16 @@
    private MidCalc midCalc;
    @Resource
    private PaxCalc paxCalc;
    @Resource
    private FreightCalc freightCalc;
    @Resource
    private WycSplitCalc wycSplitCalc;
    /** 2026 å¹´ 1 æœˆå½“月值列(0 åŸºï¼Œå³ C åˆ—) */
    private static final int FIRST_MONTH_COL = 2;
    /** å›žå¡« 1..keepMonths æœˆå°è´¦é¡µï¼Œè¿”回写入格数;缺数提示写入 problems */
    public int fillMonthlyLedger(XSSFWorkbook wb, int year, int keepMonths, List<String> problems) {
    public int fillMonthlyLedger(XSSFWorkbook wb, String period, int year, int keepMonths, List<String> problems) {
        if (wb == null || keepMonths <= 0) return 0;
        if (problems == null) problems = new ArrayList<>();
        String yearPrefix = year + "-";
@@ -50,6 +56,8 @@
        filled += fillCityRows(wb, "公交", paxCalc.busMonthly(yearPrefix, keepMonths), year, keepMonths, problems, false);
        filled += fillCityRows(wb, "出租", paxCalc.taxiMonthly(yearPrefix, keepMonths), year, keepMonths, problems, false);
        filled += fillTrackFerry(wb, paxCalc.railFerryMonthly(yearPrefix, keepMonths), year, keepMonths, problems);
        filled += fillFreightSheet(wb, period, keepMonths, problems);
        filled += fillWycSheet(wb, period, keepMonths, problems);
        return filled;
    }
@@ -117,6 +125,177 @@
            }
        }
        return filled;
    }
    /** è´§è¿é¡µå›žå¡«ï¼šå…¨çœå— + 17 å¸‚州块的 è§„上/规下/合计 Ã— è´§è¿é‡ã€å‘¨è½¬é‡ï¼ˆé€æœˆçŽ°ç®—ï¼›å½“æœˆå€¼åˆ—=2+2*(m-1)) */
    private int fillFreightSheet(XSSFWorkbook wb, String period, int keepMonths, List<String> problems) {
        Sheet sh = wb.getSheet(" è´§è¿");
        if (sh == null) sh = wb.getSheet("货运");
        if (sh == null) {
            problems.add("货运页不存在(母版缺失该页),跳过自动回填");
            return 0;
        }
        int cap = monthColumnCapacity(sh);
        int mMax = Math.min(keepMonths, cap);
        if (mMax <= 0) {
            problems.add("货运页未识别到 2026 å¹´ç´¯è®¡åˆ—,跳过自动回填");
            return 0;
        }
        int year = periodYear(period);
        // å—(全省/市州):指标键 -> è¡Œå·
        Map<String, Map<String, Integer>> blockRows = new LinkedHashMap<>();
        String cur = null;
        for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String a = text(row.getCell(0));
            if (a != null && !a.trim().isEmpty()) {
                String norm = RegionUtil.normalizeCityName(a.trim());
                if (FreightCalc.PROVINCE.equals(norm) || RegionUtil.cityList().contains(norm)) {
                    cur = norm;
                    blockRows.computeIfAbsent(cur, k -> new LinkedHashMap<>());
                } else {
                    cur = null; // æ ‡é¢˜è¡Œ / éžå…¨çœéžå¸‚å·ž -> è„±ç¦»å—
                }
            }
            if (cur == null) continue;
            String key = freightMetricKey(text(row.getCell(1)));
            if (key == null) continue;
            blockRows.get(cur).putIfAbsent(key, r);
        }
        if (blockRows.isEmpty()) {
            problems.add("货运页未识别到全省/市州行,跳过自动回填");
            return 0;
        }
        int filled = 0;
        // åªå›žå¡«ç›®æ ‡æœŸå½“月:1..目标月-1 çš„值以母版(含同期备份母版还原)为准,避免改写已确认口径
        int targetM = Math.min(periodMonth(period), mMax);
        for (int m = targetM; m <= targetM; m++) {
            String mp = String.format("%04d-%02d", year, m);
            FreightMatrix mx;
            try {
                mx = freightCalc.calc(mp);
            } catch (Exception e) {
                problems.add("货运页 " + mp + " å–数失败:" + e.getMessage());
                continue;
            }
            int col = FIRST_MONTH_COL + 2 * (m - 1);
            for (Map.Entry<String, Map<String, Integer>> e : blockRows.entrySet()) {
                FreightMetrics fm = mx.getMonth().get(e.getKey());
                if (fm == null) continue;
                Map<String, Integer> rows = e.getValue();
                // 1 æœˆåˆ—是公式的“派生行”(全省合计 è´§è¿é‡/周转量、市州 è§„上+规下周转量)不写值,
                // ç”± ensureMonthFormulas æŠŠå…¬å¼æŒ‰æœˆå³ç§»ï¼Œä¿æŒä¸Žæ¯ç‰ˆä¸€è‡´
                filled += setFreightCell(sh, skipIfDerived(sh, rows.get("totalFreight")), col, fm.getTotalFreight());
                filled += setFreightCell(sh, skipIfDerived(sh, rows.get("aboveFreight")), col, fm.getAboveFreight());
                filled += setFreightCell(sh, skipIfDerived(sh, rows.get("belowFreight")), col, fm.getBelowFreight());
                filled += setFreightCell(sh, skipIfDerived(sh, rows.get("totalTurnover")), col, fm.getTotalTurnover());
                filled += setFreightCell(sh, skipIfDerived(sh, rows.get("aboveTurnover")), col, fm.getAboveTurnover());
                filled += setFreightCell(sh, skipIfDerived(sh, rows.get("belowTurnover")), col, fm.getBelowTurnover());
            }
        }
        return filled;
    }
    /** è¯¥è¡Œ 1 æœˆåˆ—(C)是公式 -> æ´¾ç”Ÿè¡Œï¼Œè¿”回 null è¡¨ç¤ºä¸å›žå¡«æ•°å€¼ */
    private Integer skipIfDerived(Sheet sh, Integer r) {
        if (r == null) return null;
        Row row = sh.getRow(r);
        if (row == null) return r;
        Cell c = row.getCell(FIRST_MONTH_COL);
        if (c != null && c.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) return null;
        return r;
    }
    private int setFreightCell(Sheet sh, Integer r, int col, Double v) {
        if (r == null || v == null) return 0;
        return setNumeric(sh, r, col, v);
    }
    /** è´§è¿é¡µ B åˆ—指标文案 -> æŒ‡æ ‡é”®ï¼ˆè§„上+规下 / è§„上 / è§„下 Ã— è´§è¿é‡ / å‘¨è½¬é‡ï¼‰ */
    private String freightMetricKey(String b) {
        if (b == null) return null;
        String s = b.replaceAll("\\s+", "");
        if (s.isEmpty()) return null;
        boolean turnover = s.contains("周转量");
        if (s.contains("规上+规下")) return turnover ? "totalTurnover" : "totalFreight";
        if (s.contains("规上")) return turnover ? "aboveTurnover" : "aboveFreight";
        if (s.contains("规下")) return turnover ? "belowTurnover" : "belowFreight";
        return turnover ? "totalTurnover" : "totalFreight"; // å…¨çœå—的“货运量/货物周转量”= åˆè®¡
    }
    /** ç½‘约车页回填:17 å¸‚å·ž çš„ å®¢è¿é‡/周转量/其中城市内客运量/其中城市内周转量(逐月现算)
     *  å…¨çœè¡Œï¼ˆ=17 å¸‚州求和公式)由 ensureMonthFormulas æŒ‰æœˆæ‰©åˆ—,不在此处写值。 */
    private int fillWycSheet(XSSFWorkbook wb, String period, int keepMonths, List<String> problems) {
        Sheet sh = wb.getSheet("网约车");
        if (sh == null) {
            problems.add("网约车页不存在(母版缺失该页),跳过自动回填");
            return 0;
        }
        int cap = monthColumnCapacity(sh);
        int mMax = Math.min(keepMonths, cap);
        if (mMax <= 0) {
            problems.add("网约车页未识别到 2026 å¹´ç´¯è®¡åˆ—,跳过自动回填");
            return 0;
        }
        int year = periodYear(period);
        Map<String, Map<String, Integer>> cityMetricRows = new LinkedHashMap<>();
        String cur = null;
        for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String a = text(row.getCell(0));
            if (a != null && !a.trim().isEmpty()) {
                String norm = RegionUtil.normalizeCityName(a.trim());
                cur = RegionUtil.cityList().contains(norm) ? norm : null;
                if (cur != null) cityMetricRows.computeIfAbsent(cur, k -> new LinkedHashMap<>());
            }
            if (cur == null) continue;
            String key = wycMetricKey(text(row.getCell(1)));
            if (key == null) continue;
            cityMetricRows.get(cur).putIfAbsent(key, r);
        }
        if (cityMetricRows.isEmpty()) {
            problems.add("网约车页未识别到市州行,跳过自动回填");
            return 0;
        }
        int filled = 0;
        // åªå›žå¡«ç›®æ ‡æœŸå½“月(历史月以母版/历史生成件为准)
        int targetM = Math.min(periodMonth(period), mMax);
        for (int m = targetM; m <= targetM; m++) {
            String mp = String.format("%04d-%02d", year, m);
            WycSplitCalc.WycResult wr;
            try {
                wr = wycSplitCalc.calc(mp);
            } catch (Exception e) {
                if (m == mMax) {
                    problems.add("网约车页 " + mp + " æºæ•°æ®ç¼ºå¤±ï¼Œå½“月未回填:" + e.getMessage());
                }
                continue;
            }
            int col = FIRST_MONTH_COL + 2 * (m - 1);
            for (Map.Entry<String, Map<String, Integer>> e : cityMetricRows.entrySet()) {
                WycSplitCalc.WycMetrics wm = wr.getByCity().get(e.getKey());
                if (wm == null) continue;
                Map<String, Integer> rows = e.getValue();
                filled += setFreightCell(sh, rows.get("pax"), col, wm.getTotalPax());
                filled += setFreightCell(sh, rows.get("turnover"), col, wm.getTotalTurnover());
                filled += setFreightCell(sh, rows.get("cityPax"), col, wm.getCityPax());
                filled += setFreightCell(sh, rows.get("cityTurnover"), col, wm.getCityTurnover());
            }
        }
        return filled;
    }
    /** ç½‘约车页 B åˆ—指标文案 -> æŒ‡æ ‡é”® */
    private String wycMetricKey(String b) {
        if (b == null) return null;
        String s = b.replaceAll("\\s+", "");
        if (s.isEmpty()) return null;
        boolean city = s.contains("城市内");
        boolean turnover = s.contains("周转量");
        if (city) return turnover ? "cityTurnover" : "cityPax";
        return turnover ? "turnover" : "pax";
    }
    /** è½¨é“轮渡页(轨道=武汉/黄石;轮渡仅武汉)专用回填 */
@@ -192,15 +371,27 @@
        return -1;
    }
    /** å†™æ•°å€¼ï¼šæ ¼ä¸å­˜åœ¨åˆ™å»ºæ ¼å¹¶å¤åˆ¶åŒè¡Œ 1 æœˆåˆ—样式;返回 1 */
    /** å†™æ•°å€¼ï¼šæ ¼ä¸å­˜åœ¨åˆ™å»ºæ ¼ï¼›æ ¼å¸¦æ—¥æœŸæ ·å¼ï¼ˆå¦‚公交/出租 8 æœˆåˆ—继承了表头“yyyyå¹´m月”)时改回同行 1 æœˆåˆ—的数值样式,避免数值显示成日期 */
    private int setNumeric(Sheet sh, int r, int col, double val) {
        Row row = sh.getRow(r);
        if (row == null) row = sh.createRow(r);
        Cell c = row.getCell(col);
        if (c != null && c.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) {
            return 0; // æ¯ç‰ˆæœ¬æ¥å°±é å…¬å¼ç®—的行(合计/同页求和)保持公式,交 Excel é‡ç®—
        }
        Cell ref = row.getCell(FIRST_MONTH_COL);
        boolean needStyle = false;
        if (c == null) {
            c = row.createCell(col);
            Cell ref = row.getCell(FIRST_MONTH_COL);
            if (ref != null && ref.getCellStyle() != null) c.setCellStyle(ref.getCellStyle());
            needStyle = true;
        } else if (c.getCellType() == org.apache.poi.ss.usermodel.CellType.BLANK
                || (c.getCellType() == org.apache.poi.ss.usermodel.CellType.NUMERIC
                    && org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(c))) {
            // æ¯ç‰ˆæŠŠ 8 æœˆåˆ—的表头日期格式带到了数据格(或有格式无值),写值前改回同行 1 æœˆåˆ—的数字格式
            needStyle = true;
        }
        if (needStyle && ref != null && ref.getCellStyle() != null) {
            c.setCellStyle(ref.getCellStyle());
        }
        c.setCellValue(val);
        return 1;
@@ -229,20 +420,192 @@
        return row == null ? null : cityOf(row.getCell(0));
    }
    /** æœˆä»½å®¹é‡ï¼šåœ¨è¡¨å¤´ï¼ˆå‰ 5 è¡Œï¼‰æ‰¾â€œ2026…累计”列,容量=(累计列-1 æœˆåˆ—)/2 */
    /**
     * ä¸ºç¬¬ month æœˆè¡¥â€œå½“月值”列公式:取同一行最近一个月仍为公式的值列,整列右移过来
     * ï¼ˆå¦‚公交 7 æœˆåˆ— O çš„ =O9+O13+... -> 8 æœˆåˆ— Q çš„ =Q9+Q13+...)。
     * åªè¡¥ç©ºå•元格;已有数值(货运全省块/市州块由数据回填)或已有公式的格不动。
     */
    public int ensureMonthFormulas(XSSFWorkbook wb, int year, int month) {
        if (wb == null || month <= 1) return 0;
        int targetCol = FIRST_MONTH_COL + 2 * (month - 1);
        int written = 0;
        for (int i = 0; i < wb.getNumberOfSheets(); i++) {
            Sheet sh = wb.getSheetAt(i);
            if (sh == null || !isFilledLedgerSheet(sh.getSheetName())) continue;
            for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
                Row row = sh.getRow(r);
                if (row == null) continue;
                if (hasContent(row.getCell(targetCol))) continue;
                Cell srcCell = null;
                int srcMonth = -1;
                for (int s = month - 1; s >= 1; s--) {
                    Cell c = row.getCell(FIRST_MONTH_COL + 2 * (s - 1));
                    if (c != null && c.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) {
                        srcCell = c;
                        srcMonth = s;
                        break;
                    }
                }
                if (srcCell == null) continue;
                String f = srcCell.getCellFormula();
                if (f == null || f.isEmpty()) continue;
                Cell target = row.getCell(targetCol);
                if (target == null) target = row.createCell(targetCol);
                if (srcCell.getCellStyle() != null) target.setCellStyle(srcCell.getCellStyle());
                target.setCellFormula(shiftFormulaColumns(f, 2 * (month - srcMonth)));
                written++;
            }
        }
        return written;
    }
    /** è¯¥æ ¼æ˜¯å¦å·²æœ‰å†…容(数值/文本/公式);仅带样式的空壳(如母版里继承表头日期格式的格子)视为空 */
    private boolean hasContent(Cell c) {
        if (c == null) return false;
        switch (c.getCellType()) {
            case BLANK: return false;
            case STRING: return c.getStringCellValue() != null && !c.getStringCellValue().trim().isEmpty();
            default: return true;
        }
    }
    /** å‚与逐月数据回填的台账页(货运/班线包车/公交/出租/网约车/轨道轮渡) */
    private boolean isFilledLedgerSheet(String name) {
        if (name == null) return false;
        String n = name.trim();
        return "货运".equals(n) || "班线包车".equals(n) || "公交".equals(n)
                || "出租".equals(n) || "网约车".equals(n) || "轨道、轮渡".equals(n);
    }
    /** 0 åŸºåˆ—号 + 1 åŸºè¡Œå· -> "Q5" */
    private String cellRef(int col0, int row1) {
        StringBuilder sb = new StringBuilder();
        int n = col0 + 1;
        while (n > 0) {
            int rem = (n - 1) % 26;
            sb.insert(0, (char) ('A' + rem));
            n = (n - 1) / 26;
        }
        return sb.toString() + row1;
    }
    /** ä»Ž "2026-08" å–月份(解析失败返回 1) */
    private int periodMonth(String period) {
        if (period != null && period.length() >= 7) {
            try { return Integer.parseInt(period.substring(5, 7)); } catch (Exception ignore) { }
        }
        return 1;
    }
    /** ä»Ž "2026-08" å–年份 */
    private int periodYear(String period) {
        if (period != null && period.length() >= 4) {
            try { return Integer.parseInt(period.substring(0, 4)); } catch (Exception ignore) { }
        }
        return 0;
    }
    /**
     * ä¸ºç¬¬ m æœˆè¡¥â€œä¸ŽåŽ»å¹´åŒæ¯”â€å…¬å¼ï¼šæŠŠç¬¬ m-1 æœˆåŒæ¯”列的公式整体右移 2 åˆ—
     * ï¼ˆå¦‚公交 7 æœˆ P5 å…¬å¼ =O5/AH5-1 -> 8 æœˆ R5 =Q5/AJ5-1),
     * å› æ­¤åŒæ¯”分母取的就是汇总表 2025 å¹´åŒæœˆåˆ—(用户口径)。
     */
    public int ensureMonthYoyFormulas(XSSFWorkbook wb, int year, int month) {
        if (wb == null || month <= 1) return 0;
        int valueCol = FIRST_MONTH_COL + 2 * (month - 1);
        int yoyCol = valueCol + 1;
        int prevYoyCol = yoyCol - 2;
        int written = 0;
        for (int i = 0; i < wb.getNumberOfSheets(); i++) {
            Sheet sh = wb.getSheetAt(i);
            if (sh == null) continue;
            if (!isMonthLedgerSheet(sh.getSheetName())) continue;
            for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
                Row row = sh.getRow(r);
                if (row == null) continue;
                Cell prev = row.getCell(prevYoyCol);
                if (prev == null || prev.getCellType() != org.apache.poi.ss.usermodel.CellType.FORMULA) continue;
                Cell target = row.getCell(yoyCol);
                if (target != null && target.getCellType() == org.apache.poi.ss.usermodel.CellType.FORMULA) continue;
                String f = prev.getCellFormula();
                if (f == null || f.isEmpty()) continue;
                String shifted = shiftFormulaColumns(f, 2);
                if (target == null) target = row.createCell(yoyCol);
                // æ¯ç‰ˆæŠŠæ–°æœˆä»½çš„同比列做成了普通数值格式(显示成小数),统一沿用上月同比列的样式(百分数)
                if (prev.getCellStyle() != null) target.setCellStyle(prev.getCellStyle());
                try {
                    target.setCellFormula(shifted);
                    written++;
                } catch (Exception e) {
                    throw new IllegalStateException(String.format(
                            "同比公式补写失败:sheet=%s cell=%s åŽŸå¼=%s ç›®æ ‡å¼=%s",
                            sh.getSheetName(), cellRef(yoyCol, r + 1), f, shifted), e);
                }
            }
        }
        return written;
    }
    /** éšæœˆæ‰©åˆ—的长表页(表头在 1..3 è¡Œã€æœˆä»½ç»„为“当月值+同比”交替) */
    private boolean isMonthLedgerSheet(String name) {
        if (name == null) return false;
        String n = name.trim();
        return "货运".equals(n) || "公路总客运".equals(n) || "班线包车".equals(n) || "城市客运".equals(n)
                || "公交".equals(n) || "出租".equals(n) || "网约车".equals(n)
                || "轨道、轮渡".equals(n) || "轨道轮渡".equals(n)
                || "中口径明细".equals(n) || "中口径客运量".equals(n);
    }
    /** å…¬å¼å†…所有 A1 å½¢å¼çš„列引用右移 delta åˆ—(SUM ç­‰å‡½æ•°ååŽæ— æ•°å­—,不会被匹配) */
    private String shiftFormulaColumns(String formula, int delta) {
        java.util.regex.Matcher m = java.util.regex.Pattern
                .compile("(?<![A-Za-z0-9_$])([$]?)([A-Z]{1,3})([$]?)([0-9]+)")
                .matcher(formula);
        StringBuffer sb = new StringBuffer();
        while (m.find()) {
            String col = m.group(2);
            int idx = 0;
            for (int i = 0; i < col.length(); i++) idx = idx * 26 + (col.charAt(i) - 'A' + 1);
            int nidx = idx + delta;
            StringBuilder nc = new StringBuilder();
            while (nidx > 0) {
                int rem = (nidx - 1) % 26;
                nc.insert(0, (char) ('A' + rem));
                nidx = (nidx - 1) / 26;
            }
            m.appendReplacement(sb, java.util.regex.Matcher.quoteReplacement(m.group(1) + nc + m.group(3) + m.group(4)));
        }
        m.appendTail(sb);
        return sb.toString();
    }
    /** æœˆä»½å®¹é‡ï¼šä¼˜å…ˆå–表头(前 5 è¡Œï¼‰â€œ2026…累计”列反推;货运页无该文案,
     *  å†ç”¨è¡¨å¤´é‡Œæœ€åŽä¸€ä¸ªâ€œ2026å¹´M月”/日期格式的月列兜底(如货运 Q3=2026-08-01 -> 8) */
    private int monthColumnCapacity(Sheet sh) {
        int byCumulative = 0;
        int byMonthHeader = 0;
        for (int r = 0; r <= 4 && r <= sh.getLastRowNum(); r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            for (Cell c : row) {
                if (c == null) continue;
                int idx = c.getColumnIndex();
                if (idx <= FIRST_MONTH_COL || (idx - FIRST_MONTH_COL) % 2 != 0) continue; // åªçœ‹â€œå½“月值”列
                String t = text(c);
                if (t != null && t.contains("2026") && t.contains("累计") && c.getColumnIndex() > FIRST_MONTH_COL) {
                    return (c.getColumnIndex() - FIRST_MONTH_COL) / 2;
                if (byCumulative == 0 && t != null && t.contains("2026") && t.contains("累计")) {
                    byCumulative = (idx - FIRST_MONTH_COL) / 2;
                }
                if (t != null && t.contains("2026") && t.contains("月") && !t.contains("累计")
                        && !t.contains("同比")) {
                    byMonthHeader = Math.max(byMonthHeader, (idx - FIRST_MONTH_COL) / 2 + 1);
                } else if (c.getCellType() == org.apache.poi.ss.usermodel.CellType.NUMERIC
                        && org.apache.poi.ss.usermodel.DateUtil.isCellDateFormatted(c)) {
                    byMonthHeader = Math.max(byMonthHeader, (idx - FIRST_MONTH_COL) / 2 + 1);
                }
            }
        }
        return 7;
        int cap = Math.max(byCumulative, byMonthHeader);
        return cap > 0 ? cap : 7;
    }
    private String text(Cell c) {
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/WycSplitCalc.java
@@ -3,7 +3,9 @@
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.trafficaudit.common.util.RegionUtil;
import com.trafficaudit.dataimport.entity.CityTaxiMonthly;
import com.trafficaudit.dataimport.entity.WycOrderMonthly;
import com.trafficaudit.dataimport.mapper.CityTaxiMonthlyMapper;
import com.trafficaudit.dataimport.mapper.WycOrderMonthlyMapper;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellType;
import org.apache.poi.ss.usermodel.DataFormatter;
@@ -76,6 +78,8 @@
    @Resource
    private CityTaxiMonthlyMapper cityTaxiMapper;
    @Resource
    private WycOrderMonthlyMapper wycOrderMapper;
    @Value("${city-passenger.template-dir:docs/城市客运}")
    private String templateDir;
@@ -190,6 +194,8 @@
    /** è¯»å– docs/城市客运/《网约车订单及全省总量.xlsx》:按 period(yyyy-MM) è¡Œå– pin(B-E) ä¸Ž 17 å¸‚州订单(G-W) */
    private Map<String, Object> loadMonthlyInput(String period) throws Exception {
        Map<String, Object> db = loadMonthlyInputFromDb(period);
        if (db != null) return db;
        File f = findInput();
        try (InputStream in = new FileInputStream(f);
             Workbook wb = WorkbookFactory.create(in)) {
@@ -226,6 +232,38 @@
        }
    }
    /** ä¼˜å…ˆä»Žåº“取数(wyc_order_monthly);库里无该期或订单全空时返回 null,回退读母版文件 */
    private Map<String, Object> loadMonthlyInputFromDb(String period) {
        WycOrderMonthly row = wycOrderMapper.selectOne(new LambdaQueryWrapper<WycOrderMonthly>()
                .eq(WycOrderMonthly::getReportPeriod, period).last("LIMIT 1"));
        if (row == null) return null;
        List<String> cities = RegionUtil.cityList();
        Double[] vals = {
                row.getOrderWuhan(), row.getOrderHuangshi(), row.getOrderShiyan(), row.getOrderYichang(),
                row.getOrderXiangyang(), row.getOrderEzhou(), row.getOrderJingmen(), row.getOrderXiaogan(),
                row.getOrderJingzhou(), row.getOrderHuanggang(), row.getOrderXianning(), row.getOrderSuizhou(),
                row.getOrderEnshi(), row.getOrderXiantao(), row.getOrderQianjiang(), row.getOrderTianmen(),
                row.getOrderShennong()
        };
        double[] orders = new double[cities.size()];
        double sum = 0.0;
        for (int i = 0; i < orders.length && i < vals.length; i++) {
            orders[i] = vals[i] == null ? 0.0 : vals[i];
            sum += orders[i];
        }
        if (sum <= 0) return null;
        double orderSum = sum;
        if (row.getOrderSum() != null && row.getOrderSum() > 0) orderSum = row.getOrderSum();
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("pinK", row.getPinK() == null ? 0.0 : row.getPinK());
        map.put("pinCK", row.getPinCK() == null ? 0.0 : row.getPinCK());
        map.put("pinZ", row.getPinZ() == null ? 0.0 : row.getPinZ());
        map.put("pinCZ", row.getPinCZ() == null ? 0.0 : row.getPinCZ());
        map.put("orders", orders);
        map.put("orderSum", orderSum);
        map.put("source", "db");
        return map;
    }
    private File findInput() throws Exception {
        String rel = templateDir;
        while (rel.startsWith("./")) rel = rel.substring(2);
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -53,6 +53,7 @@
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
@@ -3347,8 +3348,15 @@
            cleanSummarySheetPresentation(wb); // å–消各页筛选、还原隐藏行(否则“班线包车”等页看起来像缺数据)
            int effMonth = (toMonth != null && toMonth > 0 && toMonth < month) ? toMonth : month;
            List<String> dbProblems = new ArrayList<>();
            int dbFilled = summaryWorkbookFiller.fillMonthlyLedger(wb, year, effMonth, dbProblems);
            log.info("汇总工作簿数据驱动回填完成:写入台账页当月值格数={},缺数提示={}", dbFilled, dbProblems);
            String effPeriod = String.format("%04d-%02d", year, effMonth);
            int dbFilled = summaryWorkbookFiller.fillMonthlyLedger(wb, effPeriod, year, effMonth, dbProblems);
            int yoyFilled = summaryWorkbookFiller.ensureMonthYoyFormulas(wb, year, effMonth);
            int monthFormulaFilled = summaryWorkbookFiller.ensureMonthFormulas(wb, year, effMonth);
            int rankRefreshed = refreshSummaryRankSheets(wb, effPeriod, "month");
            int widthApplied = applySummaryReferenceLayout(wb);
            log.info("汇总工作簿数据驱动回填完成:写入当月值格数={},补写当月同比公式格数={},补写当月合计公式格数={},"
                    + "刷新排名页数值格数={},按《生成_道路运输量汇总表》对齐列宽列数={},缺数提示={}",
                    dbFilled, yoyFilled, monthFormulaFilled, rankRefreshed, widthApplied, dbProblems);
            applyTwoDecimalFormat(wb); // æ•°å€¼æ˜¾ç¤ºä¸¤ä½å°æ•°ï¼ˆä¿ç•™å…¨ç²¾åº¦ï¼‰ï¼›% / æ—¥æœŸç­‰æ—¢æœ‰æ ·å¼ä¸æ”¹å˜
            if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr();
            wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // è¿˜åŽŸ/改写公式后打开即重算
@@ -3358,6 +3366,310 @@
        }
    }
    /**
     * ç‰ˆå¼å¯¹é½ï¼šæŒ‰ docs/生成汇总大表/《生成_道路运输量汇总表.xlsx》(用户提供的版式参照)逐列套用列宽与隐藏标记,
     * ä½¿ç”Ÿæˆä»¶ä¸Žå‚照件“格式完全一致”,并让母版没定义宽度的“后续扩列”(如 8 æœˆåˆ—)也有与 8 æœˆä¸€è‡´çš„列宽。
     * å‚照件缺失时静默跳过(返回 0),不影响原有导出。
     */
    private int applySummaryReferenceLayout(XSSFWorkbook wb) {
        File ref;
        try {
            ref = resolveSummaryLayoutRef();
        } catch (Exception e) {
            return 0;
        }
        if (ref == null) return 0;
        double savedZipRatio = ZipSecureFile.getMinInflateRatio();
        ZipSecureFile.setMinInflateRatio(0.0001);
        int applied = 0;
        try (InputStream in = new FileInputStream(ref); XSSFWorkbook rwb = new XSSFWorkbook(in)) {
            for (int i = 0; i < rwb.getNumberOfSheets(); i++) {
                org.apache.poi.xssf.usermodel.XSSFSheet rs = rwb.getSheetAt(i);
                if (rs == null) continue;
                org.apache.poi.xssf.usermodel.XSSFSheet ts = wb.getSheet(rs.getSheetName());
                if (ts == null) continue;
                int maxCol = Math.min(usedColumnCount(rs), usedColumnCount(ts) + 4);
                if (maxCol <= 0) continue;
                double[] width = new double[maxCol];
                boolean[] hasWidth = new boolean[maxCol];
                boolean[] hidden = new boolean[maxCol];
                for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCols cols : rs.getCTWorksheet().getColsArray()) {
                    for (org.openxmlformats.schemas.spreadsheetml.x2006.main.CTCol col : cols.getColArray()) {
                        long min = col.getMin();
                        long max = col.getMax();
                        for (long c = min; c <= max; c++) {
                            int idx = (int) (c - 1);
                            if (idx < 0 || idx >= maxCol) {
                                if (idx >= maxCol) break;
                                continue;
                            }
                            if (col.isSetWidth()) {
                                width[idx] = col.getWidth();
                                hasWidth[idx] = true;
                            }
                            hidden[idx] = col.isSetHidden() && col.getHidden();
                        }
                    }
                }
                for (int c = 0; c < maxCol; c++) {
                    if (hasWidth[c]) {
                        int w = (int) Math.round(width[c] * 256d);
                        if (ts.getColumnWidth(c) != w) {
                            ts.setColumnWidth(c, w);
                            applied++;
                        }
                    }
                    if (hidden[c] != ts.isColumnHidden(c)) ts.setColumnHidden(c, hidden[c]);
                }
            }
            log.info("汇总工作簿版式对齐《生成_道路运输量汇总表》完成:套用列宽 {} åˆ—(参照件:{})", applied, ref.getName());
        } catch (Exception e) {
            log.warn("汇总工作簿版式对齐失败(忽略,继续按母版版式输出):{}", e.getMessage());
            return 0;
        } finally {
            ZipSecureFile.setMinInflateRatio(savedZipRatio);
        }
        return applied;
    }
    /** ä¸€é¡µå·²ç”¨åˆ°çš„æœ€å¤§åˆ—数(0 åŸºè®¡æ•°ï¼‰ */
    private int usedColumnCount(org.apache.poi.ss.usermodel.Sheet sh) {
        int max = 0;
        for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
            org.apache.poi.ss.usermodel.Row row = sh.getRow(r);
            if (row == null) continue;
            if (row.getLastCellNum() > max) max = row.getLastCellNum();
        }
        return max;
    }
    // ==================== æ±‡æ€»å·¥ä½œç°¿å†…的排名页刷新 ====================
    /**
     * æ±‡æ€»å·¥ä½œç°¿é‡Œçš„「货运量排名 / è´§è¿å‘¨è½¬é‡æŽ’名 / ä¸­å£å¾„排名」在母版里是上一报表期的静态值,
     * å¯¼å‡ºæ—¶ç”¨ç‹¬ç«‹æŽ’名表(《生成_货运量排名》《生成_周转量排名》《生成_中口径排名》,与用户日常核对的口径一致)
     * çš„结果刷新数值,避免汇总表里的排名页停留在旧月份。
     * æ•°å€¼æ ¼æ¬å€¼ã€å…¬å¼æ ¼æŒ‰åç§»å¹³ç§»å…¬å¼åŽå†™å…¥ï¼›ç›®æ ‡æ ¼æœ¬èº«å·²æ˜¯å…¬å¼çš„(SUM/RANK/占比/补数)保留不动,打开时按新数据重算。
     */
    private int refreshSummaryRankSheets(XSSFWorkbook wb, String period, String mode) throws Exception {
        int n = 0;
        n += copyRankValues(wb, "货运量排名", exportFreightRank(period, mode), new int[][]{{0, 0}, {22, 0}});
        n += copyRankValues(wb, "货运周转量排名", exportTurnoverRank(period, mode), new int[][]{{0, 0}, {22, 0}});
        n += copyRankValues(wb, "中口径排名", exportPassengerMidRank(period, mode), new int[][]{{0, 0}, {0, 10}});
        return n;
    }
    /**
     * æŠŠç‹¬ç«‹æŽ’名表的各个块按标题对齐搬进汇总大表对应页。
     * srcBlocks[i] = æºå— i çš„èµ·å§‹ {行, åˆ—}(0 åŸºï¼‰ï¼›ç›®æ ‡å—按标题顺序(累计块在前、当月块在后)对应。
     * æ•°å€¼æ ¼ç›´æŽ¥æ¬å€¼ï¼›æºä¾§æ˜¯å…¬å¼çš„(排名/占比等)按目标块偏移整体平移后写入公式,打开时按汇总表自身数据重算。
     */
    private int copyRankValues(XSSFWorkbook dstWb, String dstSheetName, byte[] srcBytes, int[][] srcBlocks) throws Exception {
        XSSFSheet dst = dstWb.getSheet(dstSheetName);
        if (dst == null) return 0;
        List<XSSFCell> dstTitles = rankTitleCells(dst);
        if (dstTitles.isEmpty()) return 0;
        int copied = 0;
        FormulaEvaluator dstEv = dstWb.getCreationHelper().createFormulaEvaluator();
        try (InputStream sin = new ByteArrayInputStream(srcBytes);
             XSSFWorkbook srcWb = new XSSFWorkbook(sin)) {
            XSSFSheet src = srcWb.getSheetAt(0);
            FormulaEvaluator ev = srcWb.getCreationHelper().createFormulaEvaluator();
            int blocks = Math.min(srcBlocks.length, dstTitles.size());
            for (int b = 0; b < blocks; b++) {
                int sr0 = srcBlocks[b][0];
                int sc0 = srcBlocks[b][1];
                XSSFCell dstTitle = dstTitles.get(b);
                int rowOff = dstTitle.getRowIndex() - sr0;
                int colOff = dstTitle.getColumnIndex() - sc0;
                int h = rankBlockHeight(src, sr0, sc0);
                int w = rankBlockWidth(src, sr0, sc0);
                for (int r = 0; r < h; r++) {
                    Row srcRow = src.getRow(sr0 + r);
                    if (srcRow == null) continue;
                    Row dstRow = dst.getRow(sr0 + r + rowOff);
                    if (dstRow == null) dstRow = dst.createRow(sr0 + r + rowOff);
                    for (int c = 0; c < w; c++) {
                        Cell sc = srcRow.getCell(sc0 + c);
                        if (sc == null) continue;
                        int dc = sc0 + c + colOff;
                        if (r == 0) {
                            // æ ‡é¢˜è¡Œï¼šåŒæ­¥å—标题文案。母版标题是 inlineStr,POI çš„ setCellValue åªå†™ <v>、
                            // ä¸æ›´æ–° <is>,Excel ä¼šç»§ç»­æ˜¾ç¤ºæ—§æœˆä»½ï¼Œå¿…须先 setBlank æ¸…掉再写。
                            Cell dt = dstRow.getCell(dc);
                            if (dt == null) dt = rankCell(dstRow, dc);
                            if (sc.getCellType() == CellType.STRING && dt.getCellType() != CellType.FORMULA) {
                                String text = sc.getStringCellValue();
                                String old = dt.getCellType() == CellType.STRING ? dt.getStringCellValue() : null;
                                if (text != null && !text.equals(old)) {
                                    dt.setBlank();
                                    dt.setCellValue(text);
                                }
                            }
                            continue;
                        }
                        Cell dcCell = dstRow.getCell(dc);
                        if (dcCell != null && dcCell.getCellType() == CellType.FORMULA) continue; // ä¿ç•™æ¯ç‰ˆè‡ªå·±çš„公式
                        if (sc.getCellType() == CellType.FORMULA) {
                            // æºä¾§å…¬å¼ï¼ˆæŽ’名/占比):按目标块偏移平移后原样写入,再按汇总表自身数据求值缓存。
                            // ä¸ç›´æŽ¥æ¬ POI å¯¹æºè¡¨çš„æ±‚值结果(如 RANK å¯¹æ‰‹å·¥ç•™ç©ºçš„同比一律返回 1)。
                            String f = sc.getCellFormula();
                            if (f == null || f.isEmpty()) continue;
                            String shifted = shiftFormula(f, colOff, rowOff);
                            if (dcCell == null) dcCell = rankCell(dstRow, dc);
                            dcCell.setCellFormula(shifted);
                            try {
                                dstEv.evaluateFormulaCell(dcCell);
                            } catch (Exception ignore) {
                                // æ±‚值失败不阻塞导出:打开时由 Excel/WPS é‡ç®—
                            }
                            copied++;
                            continue;
                        }
                        Double v = rankNumeric(sc, ev);
                        if (v == null) continue;
                        if (dcCell == null) dcCell = rankCell(dstRow, dc);
                        dcCell.setCellValue(v);
                        copied++;
                    }
                }
            }
        }
        return copied;
    }
    /** ç›®æ ‡å—缺格时新建,并沿用同行左邻格样式(防新格丢边框/百分数格式) */
    private Cell rankCell(Row row, int col) {
        Cell c = row.createCell(col);
        Cell left = col > 0 ? row.getCell(col - 1) : null;
        if (left != null) c.setCellStyle(left.getCellStyle());
        return c;
    }
    /** å…¬å¼å†…所有 A1 å¼•用整体平移:列 +dCol、行 +dRow(SUM/RANK ç­‰å‡½æ•°ååŽæŽ¥æ‹¬å·ä¸ä¼šè¢«åŒ¹é…ï¼‰ */
    private String shiftFormula(String formula, int dCol, int dRow) {
        if (formula == null || (dCol == 0 && dRow == 0)) return formula;
        java.util.regex.Matcher m = java.util.regex.Pattern
                .compile("(?<![A-Za-z0-9_$])([$]?)([A-Z]{1,3})([$]?)([0-9]+)")
                .matcher(formula);
        StringBuffer sb = new StringBuffer();
        while (m.find()) {
            String col = m.group(2);
            int idx = 0;
            for (int i = 0; i < col.length(); i++) idx = idx * 26 + (col.charAt(i) - 'A' + 1);
            int nidx = Math.max(1, idx + dCol);
            StringBuilder nc = new StringBuilder();
            while (nidx > 0) {
                int rem = (nidx - 1) % 26;
                nc.insert(0, (char) ('A' + rem));
                nidx = (nidx - 1) / 26;
            }
            int nrow = Math.max(1, Integer.parseInt(m.group(4)) + dRow);
            m.appendReplacement(sb, java.util.regex.Matcher.quoteReplacement(m.group(1) + nc + m.group(3) + nrow));
        }
        m.appendTail(sb);
        return sb.toString();
    }
    /** æŽ’名页里的“块标题”单元格(含“全省分市州”的说明文字),按行、列顺序返回 */
    private List<XSSFCell> rankTitleCells(Sheet sh) {
        List<XSSFCell> out = new ArrayList<>();
        for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            for (int c = row.getFirstCellNum(); c < row.getLastCellNum(); c++) {
                Cell cell = row.getCell(c);
                if (cell == null || cell.getCellType() != CellType.STRING) continue;
                String t = cell.getStringCellValue();
                if (t != null && t.contains("全省分市州")) out.add((XSSFCell) cell);
            }
        }
        return out;
    }
    /** å—高度:从起始行向下直到整行为空(在块列范围内) */
    private int rankBlockHeight(Sheet sh, int r0, int c0) {
        int h = 0;
        for (int r = r0; r <= sh.getLastRowNum(); r++) {
            Row row = sh.getRow(r);
            boolean any = false;
            if (row != null) {
                int last = row.getLastCellNum();
                for (int c = c0; c < last; c++) {
                    if (rankHasContent(row.getCell(c))) { any = true; break; }
                }
            }
            if (!any) break;
            h++;
        }
        return h;
    }
    /** å—宽度:从起始列向右直到整列为空(在块行范围内,扫描上限 40 åˆ—) */
    private int rankBlockWidth(Sheet sh, int r0, int c0) {
        int w = 0;
        for (int c = c0; c < c0 + 40; c++) {
            boolean any = false;
            for (int r = r0; r < r0 + 30; r++) {
                Row row = sh.getRow(r);
                if (row != null && rankHasContent(row.getCell(c))) { any = true; break; }
            }
            if (!any) break;
            w++;
        }
        return w;
    }
    private boolean rankHasContent(Cell c) {
        if (c == null) return false;
        CellType t = c.getCellType();
        if (t == CellType.BLANK) return false;
        if (t == CellType.STRING) {
            String v = c.getStringCellValue();
            return v != null && !v.trim().isEmpty();
        }
        return true;
    }
    /** å–单元格数值(公式取求值结果,纯数字文本按数值解析),取不到返回 null */
    private Double rankNumeric(Cell c, FormulaEvaluator ev) {
        try {
            CellType t = c.getCellType();
            if (t == CellType.NUMERIC) return c.getNumericCellValue();
            if (t == CellType.FORMULA) {
                org.apache.poi.ss.usermodel.CellValue cv = ev.evaluate(c);
                return cv != null && cv.getCellType() == CellType.NUMERIC ? cv.getNumberValue() : null;
            }
            if (t == CellType.STRING) {
                String v = c.getStringCellValue();
                if (v == null) return null;
                String x = v.replace(",", "").trim();
                if (x.isEmpty()) return null;
                try { return Double.parseDouble(x); } catch (NumberFormatException ignore) { return null; }
            }
        } catch (Exception ignore) {
            return null;
        }
        return null;
    }
    /** å®šä½ç‰ˆå¼å‚ç…§ä»¶ docs/生成汇总大表/生成_道路运输量汇总表.xlsx(优先级同母版:配置目录 â†’ user.dir ç›¸å¯¹ â†’ ä¸Šçº§ï¼‰ */
    private File resolveSummaryLayoutRef() {
        String rel = summaryTemplateDir;
        while (rel != null && rel.startsWith("./")) rel = rel.substring(2);
        String[] roots = {
            summaryTemplateDir,
            System.getProperty("user.dir") + "/" + rel,
            System.getProperty("user.dir") + "/../" + rel
        };
        for (String root : roots) {
            if (root == null || root.trim().isEmpty()) continue;
            File f = new File(root, "生成_道路运输量汇总表.xlsx");
            if (f.isFile()) return f;
        }
        return null;
    }
    /** æ±‡æ€»è¡¨æ¯ç‰ˆå®šä½ï¼šä»…精确匹配 {year}å¹´{month}月…(P0-P1 éª¨æž¶æœŸä¸å…è®¸è·¨æœˆ/跨年静默回退,
     *  ç›®æ ‡æœŸæ— å¯¹åº”母版时明确报错,月份扩展规则待 P0 æ¯ç‰ˆå®šç¨¿åŽå†æ”¾å¼€ï¼‰ */
    private File resolveSummaryMother(String period, String mode) throws Exception {
traffic-audit-web/src/views/DataImport.vue
@@ -1,4 +1,4 @@
SKIP/CHECK (0): TYPE_LABELS upper <template>
<template>
  <el-container direction="vertical">
    <app-header title="交通统计报表审核系统 - æ•°æ®å¯¼å…¥"></app-header>
    <el-container>
@@ -54,7 +54,7 @@
                <div v-else-if="moduleSel[g.key] && !isDirOnlyType(moduleSel[g.key])" class="module-file">
                  <div class="file-name" :title="fileNamesText(moduleSel[g.key])">{{ fileNamesText(moduleSel[g.key]) || '未选择文件' }}</div>
                  <div class="file-btns">
                    <el-upload action="#" :auto-upload="false" :show-file-list="false" multiple accept=".xlsx,.xls,.et"
                    <el-upload action="#" :auto-upload="false" :show-file-list="false" multiple :accept="acceptOf(moduleSel[g.key])"
                               :ref="'up' + g.key" :on-change="f => onFilePicked(g.key, f)" style="display:inline-block">
                      <el-button size="mini" type="primary" plain icon="el-icon-folder-opened" :disabled="importing">选择文件</el-button>
                    </el-upload>
@@ -165,11 +165,11 @@
  investCityPassenger: '市州单表(客运站场)', investCityLogistics: '市州单表(物流园区)',
  'INVEST_客运站场_CITY': '市州单表(客运站场)', 'INVEST_物流园区_CITY': '市州单表(物流园区)',
  cityBus: '城市公交月报', cityTaxi: '巡游出租月报', cityTaxiAuth: '出租车运政车辆',
  wycTotal: '网约车总数', wycOrder: '网约车订单及全省总量',
  wycTotal: '网约车总数', wycOrder: '网约车订单及全省总量', wycOrderReport: '网约车订单报告',
  H2032: 'H203-2月报', H2031: 'H203-1旅客月报', H204: 'H204能耗季报', PASSENGER_AUTH: '旅客运政车辆数',
  PASSENGER_INDIVIDUAL: '个体客运量/周转量', ENERGY_AUTH: '能耗车辆运政信息', TRANSPORT_AUTH: '运政车辆',
  TRACK_MILEAGE: '轨迹里程', SCALE_SPLIT: '规上规下拆分', FREIGHT_TURNOVER: '货运量周转量',
  INVEST_SYSTEM: '投资系统导出', CITY_BUS: '城市公交月报', CITY_TAXI: '巡游出租月报', CITY_TAXI_AUTH: '出租车运政车辆', WYC_TOTAL: '网约车总数', WYC_ORDER: '网约车订单及全省总量'
  INVEST_SYSTEM: '投资系统导出', CITY_BUS: '城市公交月报', CITY_TAXI: '巡游出租月报', CITY_TAXI_AUTH: '出租车运政车辆', WYC_TOTAL: '网约车总数', WYC_ORDER: '网约车订单及全省总量', WYC_ORDER_REPORT: '网约车订单报告'
}
const BATCH_TYPE_MAP = {
  H2032: 'h2032', TRANSPORT_AUTH: 'transportAuth', TRACK_MILEAGE: 'trackMileage',
@@ -177,7 +177,7 @@
  H2031: 'passengerMonthly', PASSENGER_AUTH: 'passengerAuth', PASSENGER_INDIVIDUAL: 'passengerIndividual',
  H204: 'energyMonthly', ENERGY_AUTH: 'energyAuth',
  CITY_BUS: 'cityBus', CITY_TAXI: 'cityTaxi', CITY_TAXI_AUTH: 'cityTaxiAuth',
  WYC_TOTAL: 'wycTotal', WYC_ORDER: 'wycOrder',
  WYC_TOTAL: 'wycTotal', WYC_ORDER: 'wycOrder', WYC_ORDER_REPORT: 'wycOrderReport',
  INVEST_SYSTEM: 'investmentSystem'
}
export default {
@@ -226,7 +226,8 @@
            { value: 'cityTaxi', label: '巡游出租汽车运营服务情况月报' },
            { value: 'cityTaxiAuth', label: '出租车运政车辆' },
            { value: 'wycTotal', label: '网约车总数' },
            { value: 'wycOrder', label: '网约车订单及全省总量' }
            { value: 'wycOrder', label: '网约车订单及全省总量' },
            { value: 'wycOrderReport', label: '网约车订单报告' }
          ]
        },
        {
@@ -299,7 +300,8 @@
        cityTaxi: '导入_巡游出租汽车运营服务情况月报(YYYYå¹´M月).xls(市州级17行)',
        cityTaxiAuth: '导入模板_出租车信息(运政车辆信息).xlsx(车辆级明细,用于运营车数对比)',
        wycTotal: '模板_网约车总数.xlsx(A列=月份,B-E=全省客运量/城市内/周转量/城市内周转量)',
        wycOrder: '模板_网约车订单及全省总量.xlsx(A列=月份,B-E=全省pin,F=订单合计,G-W=17市州订单)'
        wycOrder: '模板_网约车订单及全省总量.xlsx(A列=月份,B-E=全省pin,F=订单合计,G-W=17市州订单)',
        wycOrderReport: '模板_网约车订单.docx(《网约车行业YYYYå¹´M月运行监测情况分析报告》,自动解析附件表「地市名称…订单数总数」的 17 å¸‚州订单)'
      }
      return map[this.importType] || ''
    },
@@ -320,6 +322,7 @@
        cityTaxiAuth: '城市客运/导入模板_出租车信息(运政车辆信息).xlsx',
        wycTotal: '城市客运/模板_网约车总数.xlsx',
        wycOrder: '城市客运/模板_网约车订单及全省总量.xlsx',
        wycOrderReport: '城市客运/模板_网约车订单.docx',
        investment: '投资/输入/客运(客运站)/更新_7月客运站投资明细表(市州汇总大表,给领导).xlsx',
        investmentLogistics: '投资/输入/物流(物流园区)/更新_7月物流站场投资明细.xls',
        investmentSystem: '投资/模板_查询结果(投资系统-客运+物流 2026.7).xls'
@@ -359,7 +362,8 @@
        cityTaxi: '/import/cityTaxi',
        cityTaxiAuth: '/import/cityTaxiAuth',
        wycTotal: '/import/wycTotal',
        wycOrder: '/import/wycOrder'
        wycOrder: '/import/wycOrder',
        wycOrderReport: '/import/wycOrderReport'
      }
      return map[t] || ''
    },
@@ -464,6 +468,7 @@
          this.$message.error((e.response && e.response.data && e.response.data.message) || '批量导入失败')
        })
    },
    acceptOf(type) { return type === 'wycOrderReport' ? '.docx,.doc' : '.xlsx,.xls,.et' },
    isCityDirType(t) { return t === 'investCityPassenger' || t === 'investCityLogistics' },
    isDirOnlyType(t) { return t === 'investCityPassenger' || t === 'investCityLogistics' },
    importCityDir(type) {
@@ -550,6 +555,7 @@
              map[key] = { totalRows: r.totalRows, successRows: r.successRows, failRows: r.failRows, latestId: r.id }
            }
          })
          if (map.wycOrderReport && !map.wycOrder) map.wycOrder = map.wycOrderReport
          this.overview = map
        })
        .catch(e => { if (!e || !e._toast) this.$message.error('导入记录加载失败,请刷新重试') })