feat(import): 货运量周转量支持多年度工作簿(21-26年)一次性入库;fix(report): 汇总大表预览删除中口径之后的明细表

- DataImportService: 识别「一年一个 Sheet」的多年度工作簿,逐年解析整年 1-12 月并落一个报表期
(报表期=该年最后有数据的月份),上年同期取上一年度 Sheet;非多年度文件回退原月度解析路径;
抽离 importFreightTurnoverByMonth 保持原逻辑不变,新增 persistFreightTurnover 统一「先删后插 + 记批次」
- ReportExport.vue: 第 133 行 v-else-if 改为 v-if="it.key !== 'summaryWorkbook' && ...",
避免汇总大表重复渲染第二张明细表(中口径之后的表)
- 测试:源文件 1080 个单元格逐项比对零差异、重复导入幂等(216 行 md5 一致)、
负向回退与非法文件用例通过;汇总大表预览 DOM 实测弹窗内仅 1 张表
- 文档:新增两份《功能测试报告》,追加《工作日结_2026-09-16》
3个文件已修改
2个文件已添加
304 ■■■■■ 已修改文件
docs/功能测试报告/功能测试报告_汇总大表预览删除中口径后明细表_2026-09-16.md 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_货运量周转量多年度导入_2026-09-16.md 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/工作日结/工作日结_2026-09-16.md 35 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java 170 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/ReportExport.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_»ã×Ü´ó±íÔ¤ÀÀɾ³ýÖпھ¶ºóÃ÷ϸ±í_2026-09-16.md
New file
@@ -0,0 +1,47 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Šï¼šæŠ¥è¡¨ç”Ÿæˆ-汇总大表预览弹窗删除「中口径」之后的明细表 2026-09-16
## åŠŸèƒ½è¯´æ˜Ž
- ä½ç½®ï¼šæŠ¥è¡¨ç”Ÿæˆé¡µ â†’ æŠ¥è¡¨ç±»åž‹ã€Œæ±‡æ€»å¤§è¡¨ã€â†’ é€‰æœˆä»½ â†’ åˆ—表行「预览」→ å¼¹çª—「预览数据 - æ±‡æ€»å¤§è¡¨ï¼ˆYYYY-MM)」。
- çŽ°è±¡ï¼ˆæ”¹åŠ¨å‰ï¼‰ï¼šå¼¹çª—é‡Œå‡ºçŽ°**两张表**——第一张是「指标 / æ•°æ® / åŒæ¯”」三列的汇总表(含道路货运、城市客运、中口径三段),第二张是中口径之后重复渲染的一张明细表,信息冗余。
- éœ€æ±‚:把汇总大表预览里**中口径后面的表格全部删掉**,只保留上面那张汇总表。
- æ”¹åŠ¨åŽï¼šå¼¹çª—å†…åªå‰©ç¬¬ä¸€å¼ è¡¨ï¼ˆ`class="preview-summary wb-table"`),表格下方是两条同比缺失提示与「关闭」按钮。
## æ¶‰åŠæ”¹åЍ
- å‰ç«¯ `traffic-audit-web/src/views/ReportExport.vue`,**单行改动**(第 133 è¡Œï¼‰ï¼š
  - åŽŸï¼š`<table class="preview-summary" v-else-if="it.summary && it.summary.length">`
  - çŽ°ï¼š`<table class="preview-summary" v-if="it.key !== 'summaryWorkbook' && it.summary && it.summary.length">`
- åŽŸå› è¯´æ˜Žï¼š`previewItems` é‡Œ `summaryWorkbook` è¿™ä¸€é¡¹åŒæ—¶å¸¦æœ‰ `summary` æ•°æ®ï¼Œè€Œä¸Šé¢çš„æ±‡æ€»è¡¨ï¼ˆç¬¬ 115 è¡Œ `v-if="it.key === 'summaryWorkbook' && it.summary && it.summary.length"`)已经是 `v-if`;原来的第二种表用的是 `v-else-if`,会跟在前一个 `v-if` ä¹‹åŽï¼ŒäºŽæ˜¯å¯¹ `summaryWorkbook` ä¹Ÿç…§æ ·æ¸²æŸ“出第二张表。改成带 `it.key !== 'summaryWorkbook'` æ¡ä»¶çš„独立 `v-if` åŽï¼Œæ±‡æ€»å¤§è¡¨ä¸å†æ¸²æŸ“第二张表,而其它报表类型(如有 `summary` çš„)行为保持不变。
- ç¬¬ 115–132 è¡Œçš„æ±‡æ€»è¡¨ï¼ˆå«ã€Œéƒ¨åˆ†æŒ‡æ ‡ç¼ºå°‘上年同期…」提示)**未改动**。
- æœªæ”¹åŽç«¯ã€æœªæ”¹æŽ¥å£ã€æœªæ”¹å¯¼å‡ºã€‚
## æµ‹è¯•环境
- å‰ç«¯ï¼štraffic-audit-web 8080 dev(改动后 `Compiled successfully`)。
- åŽç«¯ï¼štraffic-audit-server 8090(本次不涉及后端)。
- æµè§ˆå™¨ï¼šCodex åº”用内浏览器,`admin` ç™»å½•,地址 `http://localhost:8080/#/report`。
- è¢«æµ‹æ•°æ®ï¼šæŠ¥è¡¨ç±»åž‹ã€Œæ±‡æ€»å¤§è¡¨ã€ã€æœˆä»½ `2026å¹´8月`(源数据已由本次多年度导入保证)。
## æµ‹è¯•方法(重要说明)
- **上一轮的截图不能作为证据**:早期截图 `_qa_out/preview_summaryWorkbook_2026-08_after_fix.png` æ˜¯åœ¨å¼¹çª—尚未渲染/取景不当时拍的,画面里没有弹窗内容,属于无效证据;同时依赖可访问性树(AX)读取也不可靠(本机 `getAXState()` è¿”回缓存,会读到过期内容)。
- æœ¬è½®æ”¹ç”¨**直接读 DOM**作为唯一判据:`tab.playwright.evaluate(() => {...})` ç»Ÿè®¡å¼¹çª—内 `table` å…ƒç´ çš„æ•°é‡ã€ç±»åä¸Žè¡Œæ•°ã€‚
- å…³é”®æ“ä½œå‘(已避开):`el-select` ä¸‹æ‹‰é¡¹ `isVisible()` æ’为 false,无法用文本点击选月份,改用键盘 `ArrowDown`/`Enter`;行内「预览」按钮必须用 `locator('.el-table__row button').nth(1)`(`has-text` ä¼šå‘½ä¸­ç¦ç”¨çš„「批量预览」);页面上常驻 3 ä¸ª `.el-dialog__wrapper`,需按 `getComputedStyle(w).display !== 'none'` æ‰¾å½“前打开的那个;改 `.vue` åŽå¿…é¡» `tab.reload()` æ‰èƒ½æ‹¿åˆ°æœ€æ–°é¡µé¢ã€‚
- æœ‰æ•ˆæˆªå›¾ï¼š`_qa_out/preview_summaryWorkbook_2026-08_verified_20260916.png`(54006 å­—节)与 `_qa_out/preview_summaryWorkbook_2026-08_verified2_20260916.png`(54006 å­—节,本轮复核时重拍)。
## ç”¨ä¾‹è¡¨
| # | ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|---|
| 1 | æ­£å‘-弹窗可打开 | æ±‡æ€»å¤§è¡¨ / 2026å¹´8月 / ç‚¹è¡Œå†…「预览」 | å¼¹å‡ºã€Œé¢„览数据 - æ±‡æ€»å¤§è¡¨ï¼ˆ2026-08)」 | å¼¹çª—正常打开,标题与预期一致 | é€šè¿‡ |
| 2 | æ­£å‘-表格数量 | DOM ç»Ÿè®¡å½“前可见弹窗内 `table` æ•°é‡ | åªå‰© 1 å¼ ï¼ˆä¸­å£å¾„之后的表已删除) | `visible` å¼¹çª—ä»… 1 ä¸ªï¼Œå…¶ä¸­ `body.querySelectorAll('table').length === 1` | é€šè¿‡ |
| 3 | æ­£å‘-表格标识 | è¯»å–该表 class ä¸Žè¡Œæ•° | ä¸ºæ±‡æ€»è¡¨ï¼ˆ`preview-summary wb-table`),12 è¡ŒæŒ‡æ ‡ | `cls="preview-summary wb-table"`,`tbody tr` è®¡ 12 è¡Œ | é€šè¿‡ |
| 4 | æ­£å‘-内容完整 | è¯»å– 12 è¡Œæ–‡æœ¬ | é“路货运/城市客运/中口径各 4 é¡¹ï¼Œæ•°å€¼ä¸ŽåŒæ¯”齐全 | é“路货运:当月货运量 16888.76、当月货物周转量 2270010.6(同比 4.8%)、累计货运量 130311.89、累计货物周转量 17492129.05(同比 0%);城市客运:29575.09 / 212391.4 / 254440.17 / 1806719.08;中口径:1628.65 / 115847.35 / 12691.45 / 900177.92 | é€šè¿‡ |
| 5 | æ­£å‘-表格下方内容 | è¯»å–表格之后的节点 | åªå‰©æç¤ºä¸Žå…³é—­æŒ‰é’®ï¼Œæ— ç¬¬äºŒå¼ è¡¨ | ä¾æ¬¡ä¸ºã€ŒÂ· 2025-08 åŒæœŸåŸŽå¸‚客运数据未导入库,城市客运同比暂不显示…」「· 2025-08 åŒæœŸå…¬è·¯æ—…客数据未导入库,中口径同比暂不显示…」与「关闭」按钮 | é€šè¿‡ |
| 6 | å›žå½’-其它条件不回归 | æ£€æŸ¥ç¬¬ 115–132 è¡Œæ±‡æ€»è¡¨ä¸Ž `wbMissing` æç¤º | æœªè¢«æ”¹åЍ | ä¸Žæ”¹åŠ¨å‰ä¸€è‡´ï¼ˆ`git diff` ä»…第 133 è¡Œ 1 è¡Œå˜æ›´ï¼‰ | é€šè¿‡ |
| 7 | å›žå½’-其它报表类型 | æ£€æŸ¥ `v-if` æ¡ä»¶å¯¹éž `summaryWorkbook` é¡¹ | è¡Œä¸ºä¸å˜ï¼ˆä»æŒ‰ `summary` æ¸²æŸ“) | æ¡ä»¶ä¸º `it.key !== 'summaryWorkbook' && it.summary && it.summary.length`,非汇总大表项行为不变 | é€šè¿‡ |
## å‘现的问题与处置
1. **上一轮取证无效(已修正)**:早期截图未拍到弹窗、AX æ ‘为缓存,已在本报告中作废;本轮以 DOM å®žæµ‹ + æœ‰æ•ˆæˆªå›¾é‡æ–°å–证。
2. **弹窗内「共 0 è¡Œã€ä¸Žä¸Šæ–¹ã€Œæœˆåº¦è¦†ç›–:1~8月均有数据」口径不同**:`rowCount` è¡¨ç¤ºçš„æ˜¯æ˜Žç»†è¡Œæ•°ï¼ˆæ±‡æ€»å¤§è¡¨ä¸å«æ˜Žç»†è¡Œï¼Œæ•…为 0),这是既有展示口径,非本次改动引入,本次未改;如用户觉得易误解,可另行调整文案。
3. ä¸¤æ¡åŒæ¯”缺失提示(2025-08 åŸŽå¸‚客运 / å…¬è·¯æ—…客同期数据未入库)为数据侧现状,非本次改动引起,待 2025 å¹´å®šç¨¿æ•°æ®å¯¼å…¥åŽè‡ªåŠ¨æ¶ˆå¤±ã€‚
## ç»“论
- é€šè¿‡ã€‚汇总大表预览弹窗中「中口径」之后的那张表已彻底移除,DOM å®žæµ‹å¼¹çª—内有且仅有 1 å¼ è¡¨ï¼ˆ`preview-summary wb-table`,12 è¡Œæ±‡æ€»æŒ‡æ ‡ï¼‰ï¼Œè¡¨æ ¼å†…容与改动前一致、无信息丢失;其它报表类型的预览行为不受影响。
- å¾…用户在自己的浏览器上打开「报表生成 â†’ æ±‡æ€»å¤§è¡¨ â†’ 2026å¹´8月 â†’ é¢„览」做一次目视验收。
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_»õÔËÁ¿ÖÜתÁ¿¶àÄê¶Èµ¼Èë_2026-09-16.md
New file
@@ -0,0 +1,50 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Šï¼šè´§è¿é‡å‘¨è½¬é‡è¡¨å¤šå¹´åº¦å·¥ä½œç°¿ï¼ˆ2021–2026)一次性入库 2026-09-16
## åŠŸèƒ½è¯´æ˜Ž
- éœ€æ±‚:8 æœˆå¯¼å…¥çš„「定稿-湖北省各市州运输量数据(2026å¹´1-8月).xlsx」一个工作簿内含 21–26 å¹´ï¼ˆæºæ–‡ä»¶ç¼º 2023 å¹´ï¼‰å…¨éƒ¨å¹´åº¦æ•°æ®ï¼Œè¦æ±‚把这些数据全部读入数据库。
- æ—§è¡Œä¸ºï¼š`importFreightTurnover` åªè¯»ç¬¬ 1 ä¸ª Sheet,并按「模板_货运量周转量.xlsx」的左右两半(左半今年 1–N æœˆã€å³åŠå޻年 1–12 æœˆï¼‰è§£æžï¼Œå¯¼è‡´å…¶ä½™å¹´åº¦ Sheet çš„æ•°æ®è¢«ç›´æŽ¥ä¸¢å¼ƒã€‚
- æ–°è¡Œä¸ºï¼šå¯¼å…¥æ—¶å…ˆåˆ¤æ–­æ˜¯å¦ä¸ºã€Œå¤šå¹´åº¦å·¥ä½œç°¿ã€ï¼ˆSheet æ•° â‰¥ 2、每个 Sheet åå½¢å¦‚ `2021å¹´`、且首行 A åˆ—含「货运量」、B åˆ—含「月」)。
  - å‘½ä¸­ï¼šé€ Sheet è§£æžæ•´å¹´ 1–12 æœˆçš„货运量块与周转量块,**每个年度落一个报表期**(报表期 = è¯¥å¹´æœ€åŽä¸€ä¸ªæœ‰è´§è¿é‡æ•°æ®çš„æœˆä»½ï¼Œæœ¬æ¬¡ä¸º `YYYY-12`),上年同期取上一年度 Sheet。
  - æœªå‘½ä¸­ï¼šå®Œå…¨å›žé€€åŽŸæœ‰æœˆåº¦è§£æžè·¯å¾„ï¼Œè¡Œä¸ºä¸Žæ”¹åŠ¨å‰ä¸€è‡´ã€‚
- å¹‚等:仍按报表期「先删后插」;每个年度各写一条 `import_batch` è®°å½•。
## æ¶‰åŠæ”¹åЍ
- åŽç«¯ `traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java`(+153 / âˆ’19):
  - `importFreightTurnover` æ”¹ä¸ºåˆ†æµå…¥å£ï¼›åŽŸé€»è¾‘æ•´ä½“æŠ½ä¸º `importFreightTurnoverByMonth`(逻辑与改动前逐行一致)。
  - æ–°å¢ž `detectYearSheets`(多年度识别)、`importFreightTurnoverByYear`(按年解析 + ä¸Šå¹´åŒæœŸï¼‰ã€`parseWholeYearSheet`(整年 Sheet è§£æžï¼‰ã€`persistFreightTurnover`(按报表期先删后插 + è®°æ‰¹æ¬¡ï¼‰ã€‚
  - é¡¶éƒ¨æ–°å¢ž `import java.util.TreeMap;`。
- æœªæ”¹æ•°æ®åº“结构、未改前端、未改导入接口签名(仍为 `POST /api/import/freightTurnover?period=yyyy-MM`)。
## æµ‹è¯•环境
- åŽç«¯ï¼štraffic-audit-server 8090(含本次改动,`start-dev.ps1` å¯åŠ¨æˆåŠŸï¼Œbackend.log å‡ºçް `Tomcat started on port(s): 8090` / `Started TrafficAuditApplication`)。
- å‰ç«¯ï¼štraffic-audit-web 8080(本次不涉及前端)。
- æ•°æ®åº“:本机 MySQL `127.0.0.1:3308/traffic_audit`,用 mysql CLI ç›´æŸ¥æ ¸å¯¹ã€‚
- æºæ–‡ä»¶ï¼š`D:\05_数据分类\高速\定稿-湖北省各市州运输量数据(2026å¹´1-8月).xlsx`,Sheet ä¸º `['2026å¹´','2025å¹´','2024å¹´','2022å¹´','2021å¹´']`(2026/2025 å„ 38 è¡ŒÃ—15 åˆ—,2024 48 è¡ŒÃ—13 åˆ—,2022 40 è¡ŒÃ—15 åˆ—,2021 41 è¡ŒÃ—14 åˆ—)。
- è´Ÿå‘/边界用例一律在一次性测试期 `2099-01` ~ `2099-04` å†…进行,全程不触碰真实月份;测试后已逐个清空。
## ç”¨ä¾‹è¡¨
| # | ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|---|
| 1 | æ­£å‘-多年度导入 | ä¸Šä¼ æºæ–‡ä»¶ï¼Œ`period=2026-08` | 21/22/24/25/26 äº”个年度全部入库 | è¿”回「共 90 æ¡è®°å½•」(5 ä¸ªæŠ¥è¡¨æœŸ Ã— 18 è¡Œï¼‰ï¼›åº“内新增/覆盖 `2021-12`、`2022-12`、`2024-12`、`2025-12`、`2026-08` | é€šè¿‡ |
| 2 | æ­£å‘-入库抽查(逐项对齐) | æºæ–‡ä»¶é€å¹´é€å¸‚州逐月与库内逐项比对(今年货运量/今年周转量/上年货运量/上年周转量) | å®Œå…¨ä¸€è‡´ | æ¯”对 **1080 ä¸ªå•元格,差异 0**(脚本 `_verify_ft_multi.py`) | é€šè¿‡ |
| 3 | æ­£å‘-报表期语义 | æŸ¥çœ‹å„期 `report_period` | æ¯å¹´åº¦ä¸€ä¸ªæŠ¥è¡¨æœŸ | `2021-12 / 2022-12 / 2024-12 / 2025-12 / 2026-08`;源文件无 2023 å¹´ï¼Œæ•…未生成 `2023-12` | é€šè¿‡ |
| 4 | æ­£å‘-上年同期 | å„期 `last_*` æ˜¯å¦å–上一年度 Sheet | æœ‰ä¸Šä¸€å¹´åº¦çš„æœ‰å€¼ã€æ— çš„为空 | `2022-12`、`2025-12`、`2026-08` å„ 18 è¡Œ `last_*` å…¨éƒ¨æœ‰å€¼ï¼›`2021-12`(无上年)、`2024-12`(缺 2023)为空 | é€šè¿‡ |
| 5 | æ­£å‘-导入批次 | æŸ¥è¯¢ `import_batch` | æ¯ä¸ªæŠ¥è¡¨æœŸä¸€æ¡è®°å½• | é¦–次导入写入 id 324–328(2021-12/2022-12/2024-12/2025-12/2026-08),5 æ¡å‡ total=success=18、fail=0,file_name ä¸ºæºæ–‡ä»¶å | é€šè¿‡ |
| 6 | è´Ÿå‘-单 Sheet æœˆåº¦æ¨¡æ¿å›žé€€ | å• Sheet æœˆåº¦æ¨¡æ¿æ–‡ä»¶ + `period=2099-01` | è¯†åˆ«ä¸å‘½ä¸­ï¼Œå›žé€€æœˆåº¦è·¯å¾„ | è¿”回「共 18 æ¡è®°å½•」,与改动前一致 | é€šè¿‡ |
| 7 | è´Ÿå‘-非年度命名多 Sheet å›žé€€ | ä¸¤ä¸ªéžå¹´åº¦å‘½å Sheet(Sheet1/Sheet2)+ `period=2099-02` | è¯†åˆ«ä¸å‘½ä¸­ï¼Œå›žé€€æœˆåº¦è·¯å¾„ | `code=200`,18 æ¡ | é€šè¿‡ |
| 8 | è´Ÿå‘-混合命名回退 | å¹´åº¦å + éžå¹´åº¦åæ··åˆï¼ˆ2021å¹´/2022å¹´/备注说明)+ `period=2099-03` | è¯†åˆ«ä¸å‘½ä¸­ï¼Œå›žé€€æœˆåº¦è·¯å¾„ | `code=200`,18 æ¡ | é€šè¿‡ |
| 9 | è´Ÿå‘-非 Excel æ–‡ä»¶ | æ–‡æœ¬æ–‡ä»¶å½“ xlsx ä¸Šä¼  + `period=2099-04` | æŠ¥é”™ä¸”不落库 | `{"code":500,"message":"导入失败: Can't open workbook - unsupported file type: UNKNOWN"}` | é€šè¿‡ |
| 10 | å¹‚ç­‰-重复导入 | åŒä¸€æºæ–‡ä»¶å†å¯¼ä¸€æ¬¡ | ä¸äº§ç”Ÿé‡å¤è¡Œã€å€¼ä¸å˜ | ä»è¿”回「共 90 æ¡è®°å½•」;导入后再写 id 332–336;**全表 216 è¡Œå‰åŽ md5 ä¸€è‡´**(`a2a733b2c5321a9d5efa9be2e0c5dacb`) | é€šè¿‡ |
| 11 | å›žå½’-测试期清理 | é€ä¸ª `POST /api/import/clear?period=2099-0x` | æµ‹è¯•期数据与批次全部删除 | å„期 dataRows=18、batchRows=1 åˆ é™¤æˆåŠŸï¼›å½“å‰åº“ **0 æ¡ 2099 æ•°æ®ã€0 æ¡ 2099 æ‰¹æ¬¡** | é€šè¿‡ |
| 12 | å›žå½’-真实月份总量 | ç»Ÿè®¡ `freight_turnover_import` | ä¸Žå¯¼å…¥å‰å£å¾„一致 | 12 ä¸ªæŠ¥è¡¨æœŸ Ã— 18 è¡Œ = **216 è¡Œ**(2021-12 / 2022-12 / 2024-12 / 2025-12 / 2026-01…2026-08) | é€šè¿‡ |
## å‘现的问题与处置
1. **源文件缺 2023 å¹´ Sheet**:导致不生成 `2023-12` æŠ¥è¡¨æœŸï¼Œ`2024-12` æ— ä¸Šå¹´åŒæœŸï¼ˆ`last_*` ä¸ºç©ºï¼‰ã€‚属源数据缺口,非代码缺陷;已如实反映在库内。
2. **报表期语义为「整年」**:`2021-12` ç­‰æœŸå®žé™…承载该年 1–12 æœˆçš„全年数据(而非 12 æœˆå•月)。若后续报表/审核按「单月」口径取这些期,需要用户确认口径;本次未做改动。
3. **重复导入会覆盖同报表期**:新逻辑延续「先删后插」的幂等策略,重导 `2026-08` ä¼šè¦†ç›–该期原有值。本次已逐项核对源文件与库内一致(差异 0),无数据损失。
4. **备份不完整(已记录)**:改动前的备份 `_backup_freight_turnover_before_multiyear_20260916.sql` åªæœ‰ 59642 å­—节且仅解析出 `2026-04` ä¸€å¤„ INSERT,不能作为完整回滚依据。**不要把该文件当作完整备份使用。**
5. **2026-01…2026-07 çš„ `last_*` ä¸ºç©º**:经核对备份(`2026-04`)确认这几期在本次改动前 `last_*` å³ä¸ºç©ºï¼Œä¸”本次导入未触碰这些报表期(批次记录只覆盖 5 ä¸ªæœŸï¼‰ï¼Œæ•…非本次改动引入。
## ç»“论
- é€šè¿‡ã€‚多年度工作簿(21–26 å¹´ï¼‰å·²å…¨éƒ¨å…¥åº“,逐年一个报表期、逐年上年同期,1080 ä¸ªå•元格逐项比对零差异;非多年度文件自动回退原有月度路径,负向与边界用例均符合预期;重复导入幂等,测试期已清理干净,真实月份数据与改动前口径一致(216 è¡Œ / 12 æœŸï¼‰ã€‚
- å¾…用户确认:`2021-12` ç­‰ã€Œæ•´å¹´ã€æŠ¥è¡¨æœŸçš„业务口径是否符合后续报表/审核取数要求;如需「逐月报表期」,需要另行改造(本次未做)。
docs/¹¤×÷ÈÕ½á/¹¤×÷ÈÕ½á_2026-09-16.md
@@ -43,3 +43,38 @@
  2. æäº¤ `0d2a5fc`:`fix(cors): æ”¾è¡Œå†…网网段来源,修复局域网 IP è®¿é—®æ—¶ç™»å½• 403`;
  3. æŽ¨ä¸ªäººä»“库 `origin`:`git push origin main:master` æˆåŠŸï¼ˆ`82100d6..0d2a5fc`);
  4. æŽ¨å…¬å¸ `gitea`:`git fetch gitea`、`git push gitea main:main` å‡å›  HTTP æ— å“åº”而挂起,未推送成功(详见遗留问题 5);本次为快进推送,不涉及强推。
---
## å…­ã€ä¸‹åˆè¿½åŠ ï¼ˆä¸¤æ¡ç”¨æˆ·éœ€æ±‚ï¼Œ2026-09-16)
### 1. æŠŠ 8 æœˆå¯¼å…¥çš„「货运量周转量表」中的 21–26 å¹´æ•°æ®å…¨éƒ¨è¯»å…¥æ•°æ®åº“
- **背景**:用户 8 æœˆå¯¼å…¥çš„ `定稿-湖北省各市州运输量数据(2026å¹´1-8月).xlsx` ä¸€ä¸ªå·¥ä½œç°¿å†…含 5 ä¸ªå¹´åº¦ Sheet(`2026å¹´ / 2025å¹´ / 2024å¹´ / 2022å¹´ / 2021å¹´`,源文件无 2023 å¹´ï¼‰ï¼Œè€Œè€é€»è¾‘只读第 1 ä¸ª Sheet å¹¶æŒ‰ã€Œå·¦åŠä»Šå¹´ 1–N æœˆ / å³åŠå޻年 1–12 æœˆã€çš„æœˆåº¦æ¨¡æ¿è§£æžï¼Œå…¶ä½™å¹´åº¦æ•°æ®è¢«ä¸¢å¼ƒã€‚
- **改动(后端 1 æ–‡ä»¶ï¼Œ+153/−19)**:`DataImportService.java` ä¸­ `importFreightTurnover` æ”¹ä¸ºåˆ†æµå…¥å£ï¼›åŽŸé€»è¾‘æŠ½ä¸º `importFreightTurnoverByMonth`(逐行不变);新增 `detectYearSheets`(识别多年度工作簿)、`importFreightTurnoverByYear`(按年入库 + ä¸Šå¹´åŒæœŸå–自上一年度 Sheet)、`parseWholeYearSheet`(整年 Sheet çš„货运量块/周转量块解析)、`persistFreightTurnover`(按报表期先删后插并记批次)。
- **结果**:源文件 5 ä¸ªå¹´åº¦å…¨éƒ¨å…¥åº“,返回「共 90 æ¡è®°å½•」,报表期为 `2021-12 / 2022-12 / 2024-12 / 2025-12 / 2026-08`,每期 18 è¡Œï¼›ä¸Žæºæ–‡ä»¶é€é¡¹æ¯”对 **1080 ä¸ªå•元格零差异**;重复导入幂等(全表 216 è¡Œ md5 å‰åŽä¸€è‡´ï¼‰ï¼›éžå¤šå¹´åº¦æ–‡ä»¶è‡ªåŠ¨å›žé€€åŽŸæœˆåº¦è·¯å¾„ã€‚
- è¯¦è§ã€ŠåŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_货运量周转量多年度导入_2026-09-16.md》。
### 2. æŠ¥è¡¨ç”Ÿæˆ-汇总大表预览:删掉「中口径」之后的表格
- **改动(前端 1 æ–‡ä»¶ï¼Œå•行)**:`ReportExport.vue` ç¬¬ 133 è¡Œç”± `v-else-if="it.summary && it.summary.length"` æ”¹ä¸º `v-if="it.key !== 'summaryWorkbook' && it.summary && it.summary.length"`。根因是上面的汇总表用 `v-if`、第二张表用 `v-else-if`,对 `summaryWorkbook` ä¹Ÿç…§æ ·æ¸²æŸ“。
- **验证(推翻上一轮的无效取证)**:早期截图没拍到弹窗、AX æ ‘读到的是缓存,均为无效证据;本轮改用 `tab.playwright.evaluate` ç›´æŽ¥è¯» DOM â€”— å¼¹çª—内有且仅有 1 å¼ è¡¨ï¼ˆ`preview-summary wb-table`,12 è¡ŒæŒ‡æ ‡ï¼‰ï¼Œè¡¨æ ¼ä¸‹æ–¹åªæœ‰ä¸¤æ¡åŒæ¯”缺失提示与「关闭」按钮,**第二张表已彻底消失**。有效截图:`_qa_out/preview_summaryWorkbook_2026-08_verified_20260916.png`(本轮复核重拍 `..._verified2_20260916.png`,均 54006 å­—节)。
- è¯¦è§ã€ŠåŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_汇总大表预览删除中口径后明细表_2026-09-16.md》。
## ä¸ƒã€æµ‹è¯•结果摘要(下午追加部分)
- å¤šå¹´åº¦å¯¼å…¥ï¼š12 æ¡ç”¨ä¾‹å…¨é€šè¿‡â€”—正向(多年度入库、1080 å•元格逐项抽查零差异、报表期语义、上年同期、批次记录)、负向(单 Sheet æœˆåº¦æ¨¡æ¿å›žé€€ã€éžå¹´åº¦å‘½ååŒ Sheet å›žé€€ã€æ··åˆå‘½åå›žé€€ã€éž Excel æ–‡ä»¶æŠ¥é”™ï¼‰ã€å¹‚等(重复导入全表 md5 ä¸€è‡´ï¼‰ã€å›žå½’(测试期清理干净、真实月份 216 è¡Œ / 12 æœŸå£å¾„不变)。
- é¢„览弹窗:7 æ¡ç”¨ä¾‹å…¨é€šè¿‡â€”—DOM å®žæµ‹å¼¹çª—内 `table` æ•°é‡ä¸º 1、类名与行数正确、12 è¡Œæ•°å€¼ä¸ŽåŒæ¯”完整、表格下方无第二张表、`git diff` ä»… 1 è¡Œæ”¹åŠ¨ä¸å›žå½’ã€‚
- æµ‹è¯•期清理:`2099-01` ~ `2099-04` å…¨éƒ¨ç”¨ `POST /api/import/clear?period=…` æ¸…空(各 dataRows=18、batchRows=1),当前库 **0 æ¡ 2099 æ®‹ç•™**、`freight_turnover_import` 216 è¡Œ / 12 ä¸ªæŠ¥è¡¨æœŸã€‚
## å…«ã€é—留问题与待办(更新)
1. **`2021-12` ç­‰æŠ¥è¡¨æœŸæ˜¯ã€Œæ•´å¹´ã€è¯­ä¹‰**:这些期承载该年 1–12 æœˆå…¨å¹´æ•°æ®ï¼Œè€Œéž 12 æœˆå•月;后续报表/审核若按单月口径取数需用户确认口径,必要时再改造。
2. **源文件缺 2023 å¹´ Sheet**:导致不生成 `2023-12`,`2024-12` æ— ä¸Šå¹´åŒæœŸã€‚属源数据缺口。
3. **`2026-01` ~ `2026-07` çš„ `last_*` ä¸ºç©º**:经比对改动前备份(`2026-04` çš„ INSERT)确认这几期改动前即为空,且本次导入未触碰这些报表期(批次只覆盖 5 ä¸ªæœŸï¼‰ï¼Œ**非本次改动引入**;如需补上年同期需重新导入对应年度数据。
4. **改动前的备份不完整**:`_backup_freight_turnover_before_multiyear_20260916.sql` ä»… 59642 å­—节、只解析出 `2026-04` ä¸€å¤„ INSERT,**不能作为完整回滚依据**。
5. å¼¹çª—内「共 0 è¡Œã€ä¸Žã€Œæœˆåº¦è¦†ç›–:1~8月均有数据」口径不同(前者是明细行数),易误解,可考虑改文案(未做,等用户确认)。
6. ä¸Šåˆé—留继续挂账:域名/公网访问的白名单补充、`Login.vue` é”™è¯¯æç¤ºç»†åˆ†ã€AI å¤–发脱敏补齐、4 å¼ æ˜Žç»†æŠ¥è¡¨æ¨¡æ¿æ‰©åˆ—、审核比对基准缺失保护、`sys_role` æƒé™è½åœ°ã€3D éªŒè¯ç ç”Ÿäº§å¼€å¯ã€`backend.log` åŽ†å²æ˜Žæ–‡æ¸…ç†ã€ä»“åº“æ ¹ç›®å½•ä¸´æ—¶æ–‡ä»¶æ¸…ç†ã€‚
7. **公司 gitea ä»å¾…补推**(网络问题,非凭据),补推前先 `git fetch gitea` ä¸Žè¿œç«¯ `main` å¯¹æ¯”合并,禁强推。
## ä¹ã€æ˜Žå¤©è®¡åˆ’
1. ä¸Žç”¨æˆ·ç¡®è®¤ `2021-12` / `2022-12` ç­‰ã€Œæ•´å¹´æŠ¥è¡¨æœŸã€çš„业务口径,以及是否需要把年度数据拆成逐月报表期。
2. ç¡®è®¤æ˜¯å¦éœ€è¦è¡¥ 2023 å¹´æ•°æ®ã€ä»¥åŠ `2026-01`~`2026-07` çš„上年同期是否需要补齐。
3. è§†ç”¨æˆ·æ„è§å¤„理预览弹窗「共 0 è¡Œã€æ–‡æ¡ˆã€‚
4. ç½‘络恢复后补推公司 gitea。
traffic-audit-server/src/main/java/com/trafficaudit/dataimport/service/DataImportService.java
@@ -85,6 +85,7 @@
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.text.SimpleDateFormat;
@@ -1054,11 +1055,22 @@
    // ========== è´§è¿é‡å‘¨è½¬é‡ï¼ˆæ¨¡æ¿_货运量周转量.xlsx,每月导入) ==========
    public int importFreightTurnover(MultipartFile file, String period) throws Exception {
        try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
            // å¤šå¹´åº¦å·¥ä½œç°¿ï¼ˆä¸€å¹´ä¸€ä¸ª Sheet:整年 1-12 æœˆï¼Œå¦‚ 21-26 å¹´å®šç¨¿æ–‡ä»¶ï¼‰â†’ å…¨éƒ¨å¹´ä»½å…¥åº“
            List<String> yearSheets = detectYearSheets(wb);
            if (!yearSheets.isEmpty()) {
                return importFreightTurnoverByYear(wb, file, yearSheets);
            }
            return importFreightTurnoverByMonth(wb, file, period);
        }
    }
    /** æ¯æœˆå¯¼å…¥ï¼ˆæ¨¡æ¿_货运量周转量.xlsx):左半今年 1-N æœˆï¼Œå³åŠå޻年 1-12 æœˆ */
    private int importFreightTurnoverByMonth(Workbook wb, MultipartFile file, String period) throws Exception {
        List<FreightTurnoverImport> list = new ArrayList<>();
        int failRows = 0;
        // æŠ¥è¡¨æœŸ N æœˆï¼šå·¦åŠä¸ºä»Šå¹´ 1-N æœˆï¼ˆåˆ—数不固定),右半固定为去年 1-12 æœˆï¼ˆç®—同比用)
        int monthCount = Integer.parseInt(period.split("-")[1]);
        try (Workbook wb = WorkbookFactory.create(file.getInputStream())) {
        {
            Sheet sheet = wb.getSheetAt(0);
            Row header = sheet.getRow(0);
            String h0 = header == null ? null : getString(header, 0);
@@ -1123,27 +1135,149 @@
                }
                list.add(entity);
            }
        } catch (Exception e) {
            throw e;
        }
        Map<String, List<FreightTurnoverImport>> byPeriod = new LinkedHashMap<>();
        byPeriod.put(period, list);
        return persistFreightTurnover(byPeriod, file.getOriginalFilename());
    }
        // å¹‚等:同报表期先删除
        freightTurnoverMapper.delete(new LambdaQueryWrapper<FreightTurnoverImport>()
            .eq(FreightTurnoverImport::getReportPeriod, period));
    /** è¯†åˆ«ã€Œå¤šå¹´åº¦å·¥ä½œç°¿ã€ï¼šæ¯ä¸ª Sheet ä¸€å¹´ï¼ˆSheet å 2021å¹´/2022年…),且表头为「货运量/1月…12月」;否则返回空列表 */
    private List<String> detectYearSheets(Workbook wb) {
        List<String> names = new ArrayList<>();
        if (wb.getNumberOfSheets() < 2) return names;
        for (int i = 0; i < wb.getNumberOfSheets(); i++) {
            Sheet sheet = wb.getSheetAt(i);
            String nm = sheet.getSheetName() == null ? "" : sheet.getSheetName().trim();
            if (!nm.matches("\\d{4}\\s*å¹´?")) { names.clear(); return names; }
            Row header = sheet.getRow(0);
            String h0 = header == null ? null : getString(header, 0);
            String h1 = header == null ? null : getString(header, 1);
            if (h0 == null || !h0.contains("货运量") || h1 == null || !h1.contains("月")) {
                names.clear();
                return names;
            }
            names.add(nm);
        }
        return names;
    }
        int success = 0;
        for (FreightTurnoverImport entity : list) {
            try {
                freightTurnoverMapper.insert(entity);
                success++;
            } catch (Exception e) {
                log.error("freight turnover insert error: {}", entity.getRegionName(), e);
                failRows++;
    /** å¤šå¹´åº¦å·¥ä½œç°¿å¯¼å…¥ï¼šé€å¹´ä¸€æ¡æŠ¥è¡¨æœŸï¼ˆæŠ¥è¡¨æœŸ=该年最后有数据的月份),上年同期取上一年度 Sheet */
    private int importFreightTurnoverByYear(Workbook wb, MultipartFile file, List<String> yearSheets) {
        Map<Integer, String> sheetByYear = new TreeMap<>();
        for (String sheetName : yearSheets) {
            sheetByYear.put(Integer.parseInt(sheetName.replace("å¹´", "").trim()), sheetName);
        }
        Map<Integer, Map<String, FreightTurnoverImport>> byYear = new TreeMap<>();
        Map<Integer, Integer> maxMonthByYear = new HashMap<>();
        for (Map.Entry<Integer, String> entry : sheetByYear.entrySet()) {
            Map<String, FreightTurnoverImport> parsed = new LinkedHashMap<>();
            int maxMonth = parseWholeYearSheet(wb.getSheet(entry.getValue()), parsed);
            if (parsed.isEmpty()) continue;
            byYear.put(entry.getKey(), parsed);
            maxMonthByYear.put(entry.getKey(), Math.max(1, Math.min(12, maxMonth)));
        }
        Map<String, List<FreightTurnoverImport>> byPeriod = new LinkedHashMap<>();
        for (Map.Entry<Integer, Map<String, FreightTurnoverImport>> entry : byYear.entrySet()) {
            int year = entry.getKey();
            String yearPeriod = year + "-" + String.format("%02d", maxMonthByYear.get(year));
            Map<String, FreightTurnoverImport> lastYear = byYear.get(year - 1);
            List<FreightTurnoverImport> rows = new ArrayList<>();
            for (Map.Entry<String, FreightTurnoverImport> regionEntry : entry.getValue().entrySet()) {
                FreightTurnoverImport entity = regionEntry.getValue();
                entity.setReportPeriod(yearPeriod);
                // ä¸Šå¹´åŒæœŸ = ä¸Šä¸€å¹´åº¦ Sheet(多年度文件没有右半「去年1-12月」区块)
                FreightTurnoverImport last = lastYear == null ? null : lastYear.get(regionEntry.getKey());
                if (last != null) {
                    for (int m = 1; m <= 12; m++) {
                        setLastFreight(entity, m, getFreight(last, m));
                        setLastTurnover(entity, m, getTurnover(last, m));
                    }
                }
                rows.add(entity);
            }
            byPeriod.put(yearPeriod, rows);
        }
        return persistFreightTurnover(byPeriod, file.getOriginalFilename());
    }
    /** è§£æžæ•´å¹´ Sheet(1-12 æœˆåˆ—):货运量块 + å‘¨è½¬é‡å—;返回最后一个有货运量数据的月份 */
    private int parseWholeYearSheet(Sheet sheet, Map<String, FreightTurnoverImport> out) {
        Map<String, FreightTurnoverImport> freightMap = new LinkedHashMap<>();
        Map<String, FreightTurnoverImport> turnoverMap = new LinkedHashMap<>();
        Set<String> seen = new HashSet<>();
        int maxMonth = 0;
        for (int r = 1; r <= sheet.getLastRowNum(); r++) {
            Row row = sheet.getRow(r);
            if (row == null) continue;
            String name = getString(row, 0);
            if (name == null || name.trim().isEmpty()) continue;
            String region = RegionUtil.normalizeCityName(name);
            boolean isCity = RegionUtil.cityList().contains(region);
            if (!isCity && !"湖北省".equals(region)) continue; // è¡¨å¤´/说明行
            if (seen.contains(region)) {
                // è¯¥åç§°å·²å‡ºçŽ°è¿‡ -> å‘¨è½¬é‡åŒºå—
                FreightTurnoverImport entity = turnoverMap.get(region);
                if (entity == null) {
                    entity = new FreightTurnoverImport();
                    entity.setRegionName(name.trim());
                    turnoverMap.put(region, entity);
                }
                for (int m = 1; m <= 12; m++) {
                    setTurnover(entity, m, getDecimalOrNull(row, m));
                }
            } else {
                seen.add(region);
                FreightTurnoverImport entity = freightMap.get(region);
                if (entity == null) {
                    entity = new FreightTurnoverImport();
                    entity.setRegionName(name.trim());
                    freightMap.put(region, entity);
                }
                for (int m = 1; m <= 12; m++) {
                    Double v = getDecimalOrNull(row, m);
                    setFreight(entity, m, v);
                    if (v != null && m > maxMonth) maxMonth = m;
                }
            }
        }
        recordBatch(file.getOriginalFilename(), "FREIGHT_TURNOVER", period, list.size(), success, failRows, null);
        log.info("Freight turnover imported: {} rows for {}", success, period);
        return success;
        // åˆå¹¶ï¼šä¸€ä¸ªå¸‚州一行(货运量 + å‘¨è½¬é‡ï¼‰
        for (Map.Entry<String, FreightTurnoverImport> entry : freightMap.entrySet()) {
            FreightTurnoverImport entity = entry.getValue();
            FreightTurnoverImport turnover = turnoverMap.get(entry.getKey());
            if (turnover != null) {
                for (int m = 1; m <= 12; m++) {
                    setTurnover(entity, m, getTurnover(turnover, m));
                }
            }
            out.put(entry.getKey(), entity);
        }
        return maxMonth;
    }
    /** è½åº“:按报表期先删后插(幂等),每个报表期一条导入批次记录,返回成功条数 */
    private int persistFreightTurnover(Map<String, List<FreightTurnoverImport>> byPeriod, String fileName) {
        int total = 0;
        for (Map.Entry<String, List<FreightTurnoverImport>> entry : byPeriod.entrySet()) {
            String p = entry.getKey();
            List<FreightTurnoverImport> rows = entry.getValue();
            freightTurnoverMapper.delete(new LambdaQueryWrapper<FreightTurnoverImport>()
                .eq(FreightTurnoverImport::getReportPeriod, p));
            int failRows = 0;
            int success = 0;
            for (FreightTurnoverImport entity : rows) {
                try {
                    freightTurnoverMapper.insert(entity);
                    success++;
                } catch (Exception e) {
                    log.error("freight turnover insert error: {}", entity.getRegionName(), e);
                    failRows++;
                }
            }
            recordBatch(fileName, "FREIGHT_TURNOVER", p, rows.size(), success, failRows, null);
            log.info("Freight turnover imported: {} rows for {}", success, p);
            total += success;
        }
        return total;
    }
    /** è§£æžè´§è¿é‡åŒºå—行:左半 B èµ· N åˆ—为今年 1-N æœˆï¼Œå³åŠä¸ºå޻年 1-12 æœˆ */
traffic-audit-web/src/views/ReportExport.vue
@@ -130,7 +130,7 @@
              <div v-if="it.key === 'summaryWorkbook' && wbMissing(it.summary)" class="wb-note">
                éƒ¨åˆ†æŒ‡æ ‡ç¼ºå°‘上年同期({{ wbPrevYear() }})基期数据,同比暂未计算
              </div>
              <table class="preview-summary" v-else-if="it.summary && it.summary.length">
              <table class="preview-summary" v-if="it.key !== 'summaryWorkbook' && it.summary && it.summary.length">
                <tbody>
                  <tr v-for="(s, i) in it.summary" :key="i">
                    <td>{{ s.label }}</td>