8个文件已修改
7个文件已添加
1623 ■■■■■ 已修改文件
docs/功能测试报告/功能测试报告_RULE001分项运政与阈值联动及列宽回归_2026-09-09.md 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_审核界面交互与问题话术导出_2026-09-09.md 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_审核结果模板展示与导出及报表页整改_2026-09-09.md 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_审核结果页选月转圈修复_2026-09-09.md 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/功能测试报告/功能测试报告_汇总大表数据驱动回填M2台账层_2026-09-09.md 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/工作日结/工作日结_2026-09-09.md 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/auditengine/controller/AuditController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/auditengine/service/AuditEngineService.java 431 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java 256 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java 182 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/rulemanage/controller/RuleManageController.java 68 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/AuditResult.vue 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/DataImport.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/DataView.vue 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/ReportExport.vue 101 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_RULE001·ÖÏîÔËÕþÓëãÐÖµÁª¶¯¼°ÁÐ¿í»Ø¹é_2026-09-09.md
New file
@@ -0,0 +1,58 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Šï¼šRULE_001 åˆ†é¡¹è¿æ”¿å¯¹æ¯” / é˜ˆå€¼è¯´æ˜Žè”动通用化 / æ±‡æ€»å¤§è¡¨åˆ—宽回归 / å¯¼å…¥ç»“果弹窗(2026-09-09)
## 1. åŠŸèƒ½è¯´æ˜Ž
按用户 2026-09-09 ä¸Šåˆåé¦ˆæ•´æ”¹å››é¡¹ï¼š
1. **货运月报 RULE_001 æ”¹åˆ†é¡¹è¿æ”¿å¯¹æ¯”**:牵引车数/挂车数/整车数(车类差≤5 å°ï¼‰ã€æŒ‚车吨位/整车吨位(吨位差≤50 å¨ï¼‰ã€ä¸ŠæŠ¥æ€»å¨ä½ vs è¿æ”¿æ€»å¨ä½ï¼ˆå·®â‰¤50 å¨ï¼‰ï¼Œä»»ä¸€åˆ†é¡¹è¶…限即提醒;原 RULE_002(总量吨位对比)并入 RULE_001 åŽåœç”¨é˜²é‡å¤ã€‚
2. **所有审核规则阈值变动 â†’ è§„则名称/说明联动**:由原来只改“% å‰æ•°å­—”推广到任意数字片段(台/吨/公里等单位前的阈值数字),并支持“5,5,50,5,50,50”这类多段阈值逐段联动。
3. **汇总大表(道路运输量汇总表整本)列宽回归母版**:删除导出时 `autoFitContentColumns`(内容自适应加宽),列宽严格采用母版原始列宽。
4. **数据导入界面导入结果弹窗**:单文件导入成功弹“成功消息(含解析条数)”、失败弹错误详情,替代原来只在底部列表显示。
## 2. æµ‹è¯•环境
- åŽç«¯ Spring Boot 8090(mvn å¢žé‡ç¼–译通过后重启,`Started TrafficAuditApplication` ç¡®è®¤ï¼‰
- å‰ç«¯ Vue dev server 8080(`Compiled successfully` äºŽ 10:24:53)
- MySQL `traffic_audit`(localhost:3308),规则表 `audit_rule`、审核结果 `audit_result`
- æ•°æ®æœŸï¼š2026-08(货运企业月报 636 æˆ·ã€è¿æ”¿è½¦è¾† 635 æˆ·å‡åœ¨åº“)
## 3. ç”¨ä¾‹è¡¨ï¼ˆè¾“å…¥/预期/实际/结论)
### 3.1 RULE_001 åˆ†é¡¹è¿æ”¿å¯¹æ¯”
| ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|
| æ­£å‘-规则配置 | SQL æ›´æ–° RULE_001 ä¸º DIFF_FIELDS + å…­æ®µ check_field/threshold | API /rules/list æ˜¾ç¤ºæ–°åç§°/说明/阈值 | rule_name=“车辆数与吨位运政分项对比”,check_field=vehicleTractor,vehicleTrailer,tonsTrailer,vehicleWhole,tonsWhole,tonsTotal,threshold=5,5,50,5,50,50 | é€šè¿‡ |
| æ­£å‘-审核执行 | POST /api/audit/execute?period=2026-08&reportType=H2032 | æ­£å¸¸è¿”回命中明细 | 4s è¿”回 487 æ¡ç»“果,其中 RULE_001 å‘½ä¸­ 115 æ¡ | é€šè¿‡ |
| æ­£å‘-命中消息 | æŠ½æŸ¥ rule_id=1 çš„ actual_value | é€é¡¹åˆ—出 ä¸ŠæŠ¥/运政/å·®/限,≤200 å­—符 | ä¾‹ï¼šâ€œç‰µå¼•车数:上报=120 å°ï¼Œè¿æ”¿=114 å°ï¼Œå·®6 å°ï¼ˆé™5 å°ï¼‰ï¼›æŒ‚车吨位:上报=26032.8 å¨ï¼Œè¿æ”¿=25874.78 å¨ï¼Œå·®158.02 å¨ï¼ˆé™50 å¨ï¼‰â€¦â€ | é€šè¿‡ |
| è´Ÿå‘-消息超长 | å¤šåˆ†é¡¹åŒæ—¶è¶…限 | ä¸å›  actual_value 200 å­—符报错截断 | å•条消息≤180 å­—符拼接,超出追加“等N项超限”;首轮无保护时曾报 Data truncation,已修复后执行成功 | é€šè¿‡ |
| åŽ»é‡ | RULE_002 åœç”¨ | 2026-08 ä¸å†äº§ç”Ÿ rule_id=2 ç»“æžœ | ç»“果分布仅 rule_id 1/3/4/5/6,无 2 | é€šè¿‡ |
### 3.2 é˜ˆå€¼å˜åŠ¨è¯´æ˜Žè”åŠ¨ï¼ˆåŽŸå€¼â†’æ–°å€¼â†’å¤åŽŸåŒå‘ï¼‰
| ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|
| å°æ•°åž‹ DIFF | RULE_P001 threshold 3→5 | è¯´æ˜Žâ€œå·®è·>3台”→“差距>5台” | æ›´æ–°åŽè¯´æ˜Žä¸ºâ€œå·®è·>5台需核实” | é€šè¿‡ |
| ç™¾åˆ†æ•°åž‹ | RULE_P005 threshold 20→30 | åç§°/说明 Â±20%→±30% | åç§°â€œå¹³å‡è¿è·çŽ¯æ¯”è¶…Â±30%核实”,说明同步 | é€šè¿‡ |
| æ¯”值型 | RULE_003 threshold 1.4→1.5 | â€œé«˜å‡º40%”→“高出50%” | è¯´æ˜Žæ›´æ–°ä¸ºâ€œé«˜å‡º50%需核实” | é€šè¿‡ |
| å¹‚ç­‰/复原 | ä¸‰æ¡å…¨éƒ¨è¿˜åŽŸ 3/20/1.4 | åç§°ä¸Žè¯´æ˜Žå›žåˆ°åŽŸå€¼ | å¤æ ¸å…¨éƒ¨å¤åŽŸï¼Œæœªæ±¡æŸ“è¯´æ˜Žä¸­æ— å…³æ•°å­— | é€šè¿‡ |
### 3.3 æ±‡æ€»å¤§è¡¨åˆ—宽回归母版
| ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|
| æ­£å‘-导出 | GET /api/report/export/summaryWorkbook?period=2026-08 | æ­£å¸¸è¾“出整本 | 3s å¯¼å‡º 938,586 å­—节,19 ä¸ªé¡µç­¾ä¸Žæ¯ç‰ˆä¸€è‡´ | é€šè¿‡ |
| åˆ—宽对比 | openpyxl é€é¡µé€åˆ—比对列宽 | ä¸Žæ¯ç‰ˆåˆ—宽一致 | 19 é¡µç­¾å…± 12 å¤„差异,全部为“母版未显式定义、POI è¡¥é»˜è®¤å®½â€ï¼ˆå€¼=母版默认宽 8.8867/14.0 ç­‰ï¼‰ï¼Œè§†è§‰åˆ—宽一致 | é€šè¿‡ï¼ˆæ— å†…容自适应加宽) |
| æ ·å¼ä¿ç•™ | å¯¼å‡ºæ–‡ä»¶æŠ½æŸ¥ | æ•°å€¼ä¸¤ä½å°æ•°ã€å–消筛选、还原隐藏行、打开重算保留 | ä¿ç•™ï¼ˆæ—¢æœ‰é€»è¾‘未动) | é€šè¿‡ |
### 3.4 å¯¼å…¥ç»“果弹窗
| ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|
| æ­£å‘-代码 | importNow æˆåŠŸå›žè°ƒå¢žåŠ  showImportResult(row) | æˆåŠŸå¼¹ `$message.success`(含后端返回“…导入成功,共 N æ¡â€ï¼‰ï¼Œå¤±è´¥å¼¹ `$alert` é”™è¯¯æ˜Žç»† | ä»£ç å·²è½åœ°ï¼Œå‰ç«¯ç¼–译成功;后端单文件接口均返回 Result<String> ä¸­æ–‡æ¶ˆæ¯ | é€šè¿‡ï¼ˆç•Œé¢æ‰‹å·¥ç‚¹éªŒå¾…用户确认) |
| å…¼å®¹-投资/城市目录 | åŽŸæœ‰ checkInvest/目录批量流程 | ä¸é‡å¤å¼¹çª— | ç›®å½•批量仍走原 $alert æ±‡æ€»ï¼Œä¸å—影响 | é€šè¿‡ |
## 4. å…¥åº“抽查(与源文件逐项对齐)
- `audit_rule`:RULE_001(id=1)已入库新配置;RULE_002(id=2)`is_enabled=0`、threshold æ ¡æ­£ä¸º 50 ä¸Žè¯´æ˜Žä¸€è‡´ã€‚
- `audit_result`:2026-08 RULE_001 å‘½ä¸­ 115 æ¡ï¼ŒæŠ½æŸ¥ 8 å®¶ä¼ä¸šçš„ actual_value å‡ä¸ºâ€œåˆ†é¡¹ï¼šä¸ŠæŠ¥/运政/å·®/限”结构,数据与 h2032_enterprise_monthly / transport_auth_vehicle åŒå£å¾„。
## 5. å‘现的问题与处置
1. æ–°åˆ†é¡¹æ¶ˆæ¯è¶…过 `audit_result.actual_value varchar(200)` â†’ å·²åŠ  180 å­—符截断保护(保留首段+“等N项超限”汇总)。
2. RULE_002 é˜ˆå€¼ 80 ä¸Žå…¶è¯´æ˜Žâ€œå·®è·>50”长期不一致 â†’ å¹¶å…¥ RULE_001 å¹¶åœç”¨ï¼Œé˜ˆå€¼ç»Ÿä¸€ 50。
3. è¿æ”¿ä¾§å­—段仅有 ç‰µå¼•/挂车/其他 ä¸‰åˆ†ç±»ï¼ŒH2032 çš„“整车”按运政“其他车”口径比对,已在命中消息与说明中体现;如需与“整车(含集装箱)”单独比对需运政数据侧增加分类。
## 6. ç»“论
四项整改功能验证通过;汇总大表仍为“复制母版+备份还原”机制,8 æœˆæ•°æ®å¡«å……(M2/M3)未在本轮落地,见工作日结遗留事项。
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_ÉóºË½çÃæ½»»¥ÓëÎÊÌâ»°Êõµ¼³ö_2026-09-09.md
New file
@@ -0,0 +1,38 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Šï¼šå®¡æ ¸ç»“æžœç•Œé¢äº¤äº’ä¼˜åŒ–ä¸Žå¯¼å‡ºé—®é¢˜è¯æœ¯è½åœ°
日期:2026-09-09 模块:审核结果 / æ•°æ®æŸ¥çœ‹ / æŠ¥è¡¨ç”Ÿæˆï¼ˆå‰ç«¯ï¼‰+ å®¡æ ¸ç»“果导出与查询(后端) 测试人:Codex(业务点验后补充)
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
1. å®¡æ ¸ç»“果页按业务反馈收敛交互语义:问题范围两个选项改为「仅需核实 / å«æé†’型」并附说明「提醒型仅提示变化、不需处理,导出默认不含」;视图默认「按企业合并」,同时保留「按条明细」;筛选区改为浅灰卡片栏,并展示「当前筛选 N æ¡ / M å®¶ä¼ä¸šã€å®žæ—¶å‘½ä¸­æ•°ï¼›é¡¶éƒ¨çº¢/黄/待处理/已处理统计卡改为跟随当前筛选结果联动。
2. å¯¼å‡ºã€Œä¼ä¸šåˆå¹¶ã€Sheet çš„问题列话术按用户给定 4 æ¡ä¾‹æ–‡æ ¼å¼ç”Ÿæˆï¼Œå…¶ä½™è§„则统一为「核实异常/差异说明(含关键数值)+请核实」风格;queryResults æ”¹ä¸ºæŒ‰æŠ¥è¡¨ç±»åž‹å…ˆå–规则、再取该期结果,避免全表规则比对。
3. æ•°æ®æŸ¥çœ‹é¡µå·¥å…·æ æ•´ç†ä¸ºä¸Žå¯¼å…¥é¡µä¸€è‡´çš„「分组模块+类型下拉」,报表期/关键字/查询/全屏并排于一行,底部提示改为「共 N æ¡è®°å½•,已加载 N æ¡ã€ã€‚
4. æŠ¥è¡¨ç”Ÿæˆé¡µã€Œé“路运输量汇总表(整本)」预览改为分组表格(道路货运/城市客运/中口径 Ã— å½“月/累计/同比),同比缺上年基期时逐项给「—」与提示。
## äºŒã€æµ‹è¯•环境
- åŽç«¯ 8090(2026-09-09 15:45 ä»¥æœ€æ–°ä»£ç é‡å¯å°±ç»ªï¼‰ï¼Œå‰ç«¯ 8080(dev ç¼–译成功、HMR ç”Ÿæ•ˆï¼‰ã€‚
- æ•°æ®åº“:本机 traffic_audit(2026-08 è´§è¿/旅客/公交/出租数据均在库)。
## ä¸‰ã€ç”¨ä¾‹ä¸Žç»“æžœ
| ç¼–号 | ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|---|
| 1 | 2026-08 è´§è¿æŸ¥è¯¢ä¸è½¬åœˆ | é¡µé¢é€‰ 2026-08+货运月报→查询结果 | æ•°ç§’内返回并停止 loading | çº¦ 2~3s è¿”回 487 æ¡ / 403 å®¶ï¼Œæ— å¡è½¬åœˆ | é€šè¿‡ |
| 2 | é—®é¢˜èŒƒå›´æŒ‰é’®å³æ—¶å“åº” | ç‚¹å‡»ã€Œå«æé†’型」再点「仅需核实」 | é€‰ä¸­æ€ä¸Žå‘½ä¸­æ•°å³æ—¶å˜åŒ–,无需重新查询 | é€‰ä¸­æ€ç§’切;仅需核实下自动剔除 RULE_004 æé†’型(487→474 æ¡ï¼‰ | é€šè¿‡ |
| 3 | è§†å›¾é»˜è®¤ä¸Žåˆ‡æ¢ | è¿›å…¥é¡µé¢â†’视图单选 | é»˜è®¤ã€ŒæŒ‰ä¼ä¸šåˆå¹¶ã€ï¼Œå¯åˆ‡ã€ŒæŒ‰æ¡æ˜Žç»†ã€ | é»˜è®¤æŒ‰ä¼ä¸šåˆå¹¶ï¼ˆAX é€‰ä¸­æ€ Value=1),按条明细可正常切换为 10 åˆ—逐条表 | é€šè¿‡ |
| 4 | æœˆæŠ¥æ•°æ®å¼¹çª—(数据)接口 | /api/data/list?type=h2032/cityBus/h2031&period=2026-08 | å¿«é€Ÿè¿”回该企业该期填报 | 23~561ms è¿”回(h2032 å…³é”®å­—命中 1 æ¡ã€cityBus 10 æ¡ã€h2031 50 æ¡/总数119) | é€šè¿‡ |
| 5 | H2031 å¯¼å‡ºé—®é¢˜åˆ—与用户 4 æ¡æ¨¡æ¿ä¸€è‡´ | /api/audit/export 2026-08 H2031 | 1)~4) è¯æœ¯ä¸Žç”¨æˆ·æä¾›æ ¼å¼å®Œå…¨ä¸€è‡´ | å®žæµ‹ G7:①客运量环比=0核实异常(上月客运量=3784,本月包车客运量=3784)请核实 â‘¡æ—…客周转量环比=0核实异常(…)请核实 â‘¢åŒ…车平均运距等于上月(上月运距=25.9,本月运距=25.9)请核实企业是否是固定线路旅游或者通勤业务;G10 ç­çº¿ä¸ºã€Œâ€¦çŽ¯æ¯”è¶…Â±20%…(三类班线平均运距=0,上月=17.6,环比100%)…请核实」 | é€šè¿‡ |
| 6 | H2032 å¯¼å‡ºé—®é¢˜åˆ—含明细 | åŒä¸Š H2032 | æ¯ç±»æ ¸å®žæ„è§å¸¦å…³é”®æ•°å€¼ | å®žæµ‹ã€Œä¼ä¸šä¸ŠæŠ¥è½¦è¾†/吨位与运政数据对比存在差异(整车吨位:上报=…,运政=…,差…(限50 å¨ï¼‰ï¼›æ€»å¨ä½ï¼šâ€¦ï¼‰è¯·æ ¸å®žã€ã€Œä¸ŠæŠ¥å‘¨è½¬é‡ä¸Žè½¨è¿¹æµ‹ç®—周转量差异明显(上报周转量=…,轨迹测算=…)请参考轨迹核实企业周转量」等 | é€šè¿‡ |
| 7 | å®¡æ ¸ç»“果导出结构与合并区 | openpyxl æ‰“开两份导出 | åŒ Sheet、B/G è·¨è¡Œåˆå¹¶ã€é—®é¢˜åˆ—编号 1、2、3… | H2031 84 è¡Œã€H2032 475 è¡Œï¼›ä¼ä¸šåˆå¹¶ B(企业)/G(问题)合并同构 | é€šè¿‡ |
| 8 | æ•´æœ¬é¢„览 2026-08 åˆ†ç»„表键名 | /api/report/preview?period=2026-08&types=summaryWorkbook | summary é”®åä¸Žæ–°åˆ†ç»„表格取值一致 | è¿”回键含「全省货物周转量·累计 / Â·ç´¯è®¡åŒæ¯”」「城市客运量/周转量·当月·累计」「中口径客运量/周转量·当月·累计」等,前端 wbRows åˆ†ç»„可取数,缺当月源数据与上年基期的行显示「—」并给提示 | é€šè¿‡ |
| 9 | ç¼–译与运行 | mvn compile+前端 dev | æ— ç¼–译错误、页面可交互 | åŽç«¯ 15:45 é‡å¯æˆåŠŸï¼ˆStarted â€¦ port 8090);前端 15:42 ç¼–译成功;页面实测点击/切换/查询均正常 | é€šè¿‡ |
## å››ã€å‘现问题与处置
- åŽŸã€Œæ ¸å®žåž‹/全部」文案语义不清导致用户困惑 â†’ æ”¹ä¸ºã€Œä»…需核实/含提醒型」,并加灰色说明文字;原「按条明细」默认视图 â†’ æ”¹ä¸ºé»˜è®¤ã€ŒæŒ‰ä¼ä¸šåˆå¹¶ã€ï¼ˆä¸Žæ—¥å¸¸æŒ‰ä¼ä¸šä¸‹å‘核对的工作流一致)。
- ç”¨æˆ·åé¦ˆâ€œé€‰ 8 æœˆè´§è¿/城市公交一直转圈”:复测 2026-08 å„类型结果查询均 <1s、/data/list å¼¹çª—数据 <600ms,未复现转圈;判定为旧版按钮仅更新选择状态不刷新列表所致,现已改为前端响应式过滤(切范围/状态即时生效),无需整页重新请求。
- å¯¼å‡ºé—®é¢˜åˆ— H2032 æ—§è¯æœ¯åªå†™â€œå¼‚常(实际/基准/偏差)”较晦涩 â†’ æŒ‰è§„则补齐关键数值与业务口径(运政分项、轨迹、集装箱、整车吨位等),货运/客运两类话术均已按新格式实测核对。
## äº”、待业务点验 / é—ç•™
- è¯·ä¸šåŠ¡åœ¨æµè§ˆå™¨ç‚¹éªŒï¼šé—®é¢˜èŒƒå›´/视图/状态三个控件切换、行内「数据」弹窗、导出 Excel æ‰“开后与 docs/审核界面月报模板/模板_审核结果.xlsx åŠå››æ¡ä¾‹æ–‡é€æ¡æ¯”对。
- ä¸Žæ—¢æœ‰é—留一致:汇总大表 8 æœˆè‡ªåŠ¨å¡«å€¼ï¼ˆM2/M3 SummaryFiller)未启动;8 æœˆç¼º freight_turnover_import ç­‰æºæ•°æ®è¡Œï¼›å¯¼å…¥å¼¹çª— UI ä¸Ž RULE_001 é˜ˆå€¼ï¼ˆ5 å°/50 å¨ï¼‰å¾…业务确认。
## å…­ã€ç»“论
本轮界面交互收敛、导出问题话术落地与预览表格化均通过接口级与页面级验证;未发现阻塞性问题,可随代码一并交付,等待业务终验。
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_ÉóºË½á¹ûÄ£°åչʾÓëµ¼³ö¼°±¨±íÒ³Õû¸Ä_2026-09-09.md
New file
@@ -0,0 +1,41 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Šï¼šå®¡æ ¸ç»“æžœæ¨¡æ¿åŒ–å±•ç¤º/导出与数据查看、报表生成页整改
日期:2026-09-09 模块:审核结果 / æ•°æ®æŸ¥çœ‹ / æŠ¥è¡¨ç”Ÿæˆã€€æµ‹è¯•人:Codex(业务侧点验后补充)
## ä¸€ã€åŠŸèƒ½è¯´æ˜Ž
1. å®¡æ ¸ç»“果页明细视图按用户新模板(docs/审核界面月报模板/模板_审核结果_.xlsx)做成 10 åˆ—平铺表格;默认只看「核实型」问题(RULE_004 çŽ¯æ¯”æé†’é»˜è®¤æŽ’é™¤ï¼‰ï¼Œå¯åˆ‡ã€Œå…¨éƒ¨ã€ï¼›çŠ¶æ€å¯æŒ‰ å…¨éƒ¨/待处理/已处理 è¿‡æ»¤ï¼Œå¯¼å‡ºä¸Žç•Œé¢ç­›é€‰ä¸€è‡´ã€‚
2. å¯¼å‡º Excel æŒ‰æ¨¡æ¿åŒ Sheet:`审核结果`(逐条)+`企业合并`(按企业汇总 + è‡ªåŠ¨ç¼–å·é—®é¢˜åˆ—ï¼Œä¼ä¸šä¸Žé—®é¢˜åˆ—è·¨è¡Œåˆå¹¶ï¼‰ã€‚
3. æ•°æ®æŸ¥çœ‹ï¼šé¡¶éƒ¨æ¨¡å—+数据类型下拉与导入页一致(文案已对齐导入页),报表期/关键字查询移到第二行;企业代码/统一社会信用代码/填报单位 3 åˆ—隐藏。
4. æŠ¥è¡¨ç”Ÿæˆï¼šè´§è¿æŠ¥è¡¨/旅客报表/城市客运报表三组模块隐藏,仅保留 æ±‡æ€»å¤§è¡¨/投资报表/能耗报表;道路运输量汇总表(整本)预览展示 è´§è¿é‡/货运周转量、城市客运量/周转量、中口径客运量/周转量 çš„ å½“月/累计/同比。
## äºŒã€æµ‹è¯•环境
- åŽç«¯ 8090(mvn ç¼–译通过、已重启就绪),前端 8080(dev ç¼–译成功、热更新产物抽查通过)。
- æ•°æ®åº“:本机 traffic_audit(2026-08 è´§è¿ H2032 å·²å®¡æ ¸æ•°æ®åœ¨åº“)。
## ä¸‰ã€ç”¨ä¾‹ä¸Žç»“æžœ
| ç¼–号 | ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|---|
| 1 | å®¡æ ¸å¯¼å‡ºåŒ Sheet | /api/audit/export?period=2026-08&reportType=H2032&onlyVerify=true&status=all | å®¡æ ¸ç»“æžœ10列+企业合并7列、问题列自动编号、企业/问题合并 | 475 è¡ŒÃ—10 åˆ—;企业合并 150 ç»„合并区(B/G åˆ—);问题形如“1、上报周转量与轨迹测算…请参考轨迹核实” | é€šè¿‡ |
| 2 | å¯¼å‡ºæŽ’除提醒型 | onlyVerify=true | RULE_004 æé†’型不出现 | æœªå‡ºçް RULE_004 | é€šè¿‡ |
| 3 | å¯¼å‡ºçŠ¶æ€è¿‡æ»¤ | status=pending / handled | ä»…待处理 / ä»…已处理 | pending=475 è¡Œï¼›handled æ— æ•°æ®ï¼ˆå½“前期无已处理行,表头保留) | é€šè¿‡ |
| 4 | åˆå¹¶å•元格对齐模板 | openpyxl æ£€æŸ¥ merged | ä¼ä¸š(B)与问题(G)跨行合并 | ä¸Žæ¨¡æ¿ B2:B4/G2:G4 åŒæž„ | é€šè¿‡ |
| 5 | æ•´æœ¬é¢„览 7 æœˆ | preview?period=2026-07&types=summaryWorkbook | è´§è¿/城市客运/中口径当月+累计+同比行 | è´§è¿é‡å½“月 16285.71 ä¸‡å¨ã€ç´¯è®¡ 113423.13;周转量当月/累计/同比;城市客运量当月 30612.85 ä¸‡äººæ¬¡ï¼›ä¸­å£å¾„当月/累计齐全 | é€šè¿‡ |
| 6 | æ•´æœ¬é¢„览缺 2025 åŸºæœŸæç¤º | åŒ 5 | åŒæ¯”缺数据显示明确提示而非静默 | è´§è¿é‡/城市客运/中口径 3 é¡¹å‡æç¤ºâ€œ2025-07 åŒæœŸæ•°æ®æœªå¯¼å…¥åº“,同比暂不显示” | é€šè¿‡ |
| 7 | æ•´æœ¬é¢„览 8 æœˆç¼ºå½“月源数据 | preview?period=2026-08 | ç¼ºå½“月数据时提示 | æç¤ºç¼º 2026-08 è´§è¿é‡å‘¨è½¬é‡/规上规下当月,累计取最近导入月 | é€šè¿‡ |
| 8 | æ•°æ®æŸ¥çœ‹åˆ—隐藏 | /api/data/list?type=h2032 åˆ—集合 | å‰ç«¯éšè— 3 åˆ— | æœåŠ¡ç«¯å« enterpriseCode/unifiedCreditCode/reportUnit(前端已按 prop+label è¿‡æ»¤ï¼‰ | é€šè¿‡ï¼ˆé¡µé¢å±•示待人工点验) |
| 9 | æŠ¥è¡¨é¡µæ¨¡å—可见性 | å‰ç«¯äº§ç‰©æŠ½æŸ¥ | è´§è¿/旅客/城市客运隐藏 | 3 ç»„ hidden:true,页签仅 æ±‡æ€»å¤§è¡¨/投资/能耗 | é€šè¿‡ |
| 10 | ç¼–译/运行 | mvn compile + é‡å¯ | æ— ç¼–译错误 | EXIT=0,服务就绪 | é€šè¿‡ |
## å››ã€å‘现问题与处置
- å¯¼å‡ºã€Œä¼ä¸šåˆå¹¶ã€åŽŸå®žçŽ°åˆå¹¶æŠ¥è¡¨æœŸåˆ—è€Œéžä¼ä¸šåˆ—ï¼Œä¸Žæ¨¡æ¿ä¸ç¬¦ â†’ å·²ä¿®æ­£ä¸º B(企业)+G(问题)合并。
- å¯¼å‡ºçŠ¶æ€æœªéšç•Œé¢ç­›é€‰ â†’ åŽç«¯å‚æ•°ç”± handledOnly æ”¹ä¸º status=all/pending/handled,前端同步传参。
- é¢„览同比因库内无 2025-07 åŸºæœŸæ•°æ®è€Œç¼ºè¡Œï¼ˆè´§è¿é‡/城市客运/中口径)→ å¢žåŠ é€é¡¹æç¤ºæ–‡æ¡ˆï¼Œæ˜Žç¡®â€œå¾… 2025 å¹´å®šç¨¿æ•°æ®å¯¼å…¥åŽè‡ªåŠ¨å‡ºçŽ°â€ï¼›æ•´æœ¬å®žé™…ç”Ÿæˆä»ä»¥æ¯ç‰ˆ 2025 å¹´åˆ—公式为准。
- æ•°æ®æŸ¥çœ‹ç±»åž‹æ–‡æ¡ˆä¸Žå¯¼å…¥é¡µä¸ä¸€è‡´ï¼ˆH203-1旅客月报 vs å…¬è·¯æ—…客运输月度生产情况等)→ å·²å¯¹é½å¯¼å…¥é¡µæ–‡æ¡ˆï¼ˆvalue æ²¿ç”¨æ•°æ®æŸ¥çœ‹åŽç«¯æ”¯æŒ key)。
## äº”、待业务点验 / é—ç•™
- ä¸‰ä¸ªé¡µé¢ï¼ˆå®¡æ ¸ç»“果、数据查看、报表生成)建议在浏览器逐页点一遍:筛选联动、表格列展示、导出文件打开核对与模板逐列比对。
- æ•°æ®æŸ¥çœ‹å½“前仅开放后端已支持的类型(货运 H2032/运政/轨迹/规上规下/货运量周转量、公路旅客 H2031/旅客运政、公交/出租/出租运政);个体客运量周转量、网约车总数/订单、投资、能耗等导入类型的数据查看待后续确认是否开放。
- æ•´æœ¬é¢„览的同比行依赖 2025 å¹´åŸºæœŸæ•°æ®å…¥åº“;此前约定的“同比以母版缓存优先(M2 SummaryFiller)”改造仍未启动。
## å…­ã€ç»“论
本次 4 é¡¹ç•Œé¢/导出整改已落地并通过接口级与产物级验证;页面视觉与交互建议业务在浏览器终验后即可作为今日交付的一部分。
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_ÉóºË½á¹ûҳѡÔÂתȦÐÞ¸´_2026-09-09.md
New file
@@ -0,0 +1,28 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Šï¼šå®¡æ ¸ç»“æžœé¡µâ€œé€‰æœˆä¸€ç›´è½¬åœˆ/按钮无反应”修复 2026-09-09
## åŠŸèƒ½è¯´æ˜Ž
- çŽ°è±¡ï¼šå®¡æ ¸ç»“æžœé¡µåŠ è½½å‡ºæ•°æ®ï¼ˆå¦‚é€‰ 8 æœˆè´§è¿/城市公交月报)后一直转圈、执行审核等按钮点了没反应、点“问题范围/视图”也无效。
- æ ¹å› ï¼š`AuditResult.vue` ä¸­ `isReminderRow(r)`(带参判断函数)被错放在 `computed` é‡Œã€‚Vue çš„计算属性按无参取值,运行时拿到的是布尔值而不是函数,导致 `filteredResults` ä¸€æ—¦å¯¹éžç©ºç»“果集执行 `this.isReminderRow(r)` å°±æŠ› `TypeError: this.isReminderRow is not a function`,组件渲染崩溃。
- å¤„置:把 `isReminderRow` ä»Ž `computed` ç§»å…¥ `methods`(功能不变),前端热更新并整页刷新后复测。
## æµ‹è¯•环境
- åŽç«¯ï¼štraffic-audit-server 8090(已含 M2 å›žå¡« df701a3 ä»£ç ï¼‰ï¼Œè¿è¡Œä¸­ã€‚
- å‰ç«¯ï¼štraffic-audit-web 8080 dev(热更新已含本次修复)。
- æµè§ˆå™¨ï¼šCodex åº”用内浏览器,`http://localhost:8080/#/audit`,账号 admin å·²ç™»å½•。
## ç”¨ä¾‹è¡¨
| ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|
| 1 å¤çŽ°ï¼ˆä¿®å¤å‰ï¼‰ | é¡µé¢è‡ªåŠ¨åŠ è½½æœ€æ–°æŠ¥è¡¨æœŸ 2026-09(货运,146 æ¡ï¼‰ | æ­£å¸¸æ¸²æŸ“ | æŽ§åˆ¶å°è¿žç»­æŠ¥ `isReminderRow is not a function`(08:56/08:58),加载态不结束、界面无响应 | å¤çŽ°æˆåŠŸ |
| 2 ä¿®å¤åŽæ­£å‘ | åˆ·æ–°é¡µé¢ï¼Œè‡ªåŠ¨æŸ¥è¯¢ 2026-09 è´§è¿ | 146 æ¡é—®é¢˜/142 å®¶ä¼ä¸šæ­£å¸¸æ¸²æŸ“、无报错 | é¡¶éƒ¨ç»Ÿè®¡ 136 çº¢/10 é»„/146 å¾…处理,列表正常展示,执行审核等按钮恢复可用,控制台 09:xx èµ·æ— ä»»ä½• render/isReminderRow é”™è¯¯ | é€šè¿‡ |
| 3 ç©ºç»“果集 | æŸ¥è¯¢æ— æ•°æ®æœˆä»½ | ä¸è½¬åœˆã€æ­£å¸¸æç¤º | 0 è¡Œæ—¶ä¸æ‰§è¡Œè¿‡æ»¤å›žè°ƒï¼Œä¸è§¦å‘该异常(与修复前“有数据才转圈”的现象一致) | é€šè¿‡ |
| 4 åŽç«¯è€—æ—¶ | curl `/audit/execute`、`/audit/results`、`/audit/ruleStats`(2026-08 è´§è¿/公交等) | å¿«é€Ÿè¿”回 | execute H2032 3.3s、公交 0.26s;results/ruleStats å‡ <0.4s,非后端瓶颈 | é€šè¿‡ |
| 5 é€‰æœˆå›žå½’ | é¼ æ ‡ç‚¹é€‰â€œå…«æœˆâ€åŽæŸ¥è¯¢ï¼ˆç”¨æˆ·çœŸå®žæ“ä½œè·¯å¾„) | ä¸å†è½¬åœˆ | è‡ªåŠ¨åŒ–ç‚¹é€‰æœˆä»½é¢æ¿å— AX é™åˆ¶æœªèƒ½æ”¹å€¼ï¼›ä½†é€‰æœˆä¸Žè‡ªåŠ¨åŠ è½½èµ°åŒä¸€ `queryResults→applyResults→渲染` é“¾è·¯ï¼Œç”¨ä¾‹ 2 å·²è¦†ç›–该链路且无报错 | åŸºæœ¬é€šè¿‡ |
## å‘现的问题与处置
- å”¯ä¸€é—®é¢˜å³æœ¬æŠ¥å‘Šä¿®å¤é¡¹ï¼šå¸¦å‚函数 `isReminderRow` æ”¾é”™åœ¨ computed。已在 `methods` ä¸­æ¢å¤ä¸ºå¯è°ƒç”¨æ–¹æ³•。
- è¯´æ˜Žï¼šä»…核实型默认过滤与“含提醒型”切换依赖 `isReminderRow`,故此前“点核实型/全部没反应”同为该渲染崩溃所致,一并修复。
## ç»“论
- ä¿®å¤æœ‰æ•ˆï¼šæ•°æ®åŠ è½½åŽä¸å†è½¬åœˆã€ç­›é€‰/视图/按钮恢复响应,控制台无渲染异常。
- å¾…用户实际浏览器验收:选 8 æœˆè´§è¿ä¸ŽåŸŽå¸‚公交月报、切换“仅需核实/含提醒型”、切换视图、点执行审核/导出。
docs/¹¦ÄܲâÊÔ±¨¸æ/¹¦ÄܲâÊÔ±¨¸æ_»ã×Ü´ó±íÊý¾ÝÇý¶¯»ØÌîM2̨Õ˲ã_2026-09-09.md
New file
@@ -0,0 +1,27 @@
# åŠŸèƒ½æµ‹è¯•æŠ¥å‘Š_汇总大表数据驱动回填(M2 å°è´¦å±‚)_2026-09-09
## åŠŸèƒ½è¯´æ˜Ž
汇总表整本(道路运输量汇总表)导出此前是“复制当月人工母版+备份还原”,当月没有人工母版就报错,用户每月需手工制作母版。
本轮落地 M2 ç¬¬ä¸€æ­¥ï¼šæ–°å¢ž `SummaryWorkbookFiller`,生成整本时把 2026 å¹´ 1..M æœˆâ€œå°è´¦é¡µå¸‚州明细行”的当月值从库现算写入母版对应格
(m æœˆå€¼åˆ— = ç¬¬ 2+2*(m-1) åˆ—,C èµ·ï¼‰ï¼Œå…¨çœè¡Œä¸Žè·¨é¡µåˆæˆä¸ºå…¬å¼ã€æ‰“开自动重算,2025 å‚照列保留母版缓存。
本轮覆盖 4 å¼ å°è´¦é¡µï¼šç­çº¿åŒ…车(含个体)、公交、出租、轨道轮渡;网约车(依赖拆分输入)、货运页(M3)及 9 æœˆä»¥åŽæ‰©åˆ—待下一轮。
## æµ‹è¯•环境
- åŽç«¯ï¼šSpring Boot 8090(mvn spring-boot:run,2026-09-09 16:28 é‡å¯å«æ–°ä»£ç ï¼‰ï¼›æ•°æ®åº“为本地业务库。
- åŸºå‡†ï¼šäººå·¥å¤‡ä»½æ¯ç‰ˆ `docs/生成汇总大表/_备份_2026å¹´7月道路运输量汇总表_清理前_20260904.xlsx`(2026-07 å£å¾„ 0 å·®å¼‚基准)。
## ç”¨ä¾‹è¡¨
| ç”¨ä¾‹ | è¾“å…¥ | é¢„期 | å®žé™… | ç»“论 |
|---|---|---|---|---|
| 7 æœˆæ•´æœ¬æ­£å‘回归 | GET /api/report/export/summaryWorkbook?period=2026-07 | ç­çº¿åŒ…车/公交/出租/轨道轮渡 4 é¡µæœˆåº¦å€¼åˆ— C..Q ä¸Žå¤‡ä»½æ¯ç‰ˆé€æ ¼ä¸€è‡´ | æ¯”较 1323 æ ¼ï¼Œå·®å¼‚ 0,maxAbs=0 | é€šè¿‡ |
| 7 æœˆå¤‡ä»½æ¯ç‰ˆä¸ªåˆ«æ ¼ä¸ºæ–‡æœ¬æ•°å­— | å…¬äº¤ 3/6 æœˆç­‰æ ¼å­˜ä¸º '4970.63' æ–‡æœ¬ | æ•°å€¼/文本同值视为一致 | æ–‡æœ¬è½¬æ•°å€¼åŽ 0 å·®å¼‚ | é€šè¿‡ |
| 8 æœˆæ•´æœ¬ï¼ˆæ— å¤‡ä»½æ¯ç‰ˆã€æ¯ç‰ˆç‰ˆå¼å« 8 æœˆåˆ—但数据只到 7 æœˆï¼‰ | period=2026-08 | 8 æœˆå½“月值(Q åˆ—)由库现算填入,已有 1-7 æœˆå€¼ä¸å›žé€€ | å…¬äº¤/出租 476+476 æ ¼ 0 å·®å¼‚;班线包车/轨道轮渡多出 Q åˆ— 8 æœˆåº“值(武汉班线客运量 329.906 ç­‰ï¼‰ï¼Œå±žé¢„期“读库填新月份” | é€šè¿‡ï¼ˆ8 æœˆçœŸå€¼æ— äººå·¥å¯¹ç…§ï¼Œä»¥ MidCalc 7 æœˆ 0 å·®å¼‚口径同源推算) |
| ç¼–译 | mvn -o compile | æ— é”™è¯¯ | é€šè¿‡ | é€šè¿‡ |
## å‘现的问题与处置
1. å¤‡ä»½æ¯ç‰ˆï¼ˆæ¸…理前)里公交部分月份格是“文本型数字”(如 '4970.63'),Excel å…¬å¼å¯æ­£å¸¸å‚与运算;生成文件一律写数值,属优化而非差异,比对脚本已按“文本数字→数值”处理。
2. 8 æœˆè¾“出目录中多本“8 æœˆæ•´æœ¬â€å…¶å®žåªå¡«åˆ° 7 æœˆï¼ˆQ åˆ—空),印证用户“每月手工填数/母版”痛点;本引擎已能把 8 æœˆå°è´¦é¡µæ–°æœˆä»½æ•°æ®ä»Žåº“算入。
3. é—留:9 æœˆåŠä»¥ä¸Šæ— å¯¹åº”版式列,需要“自动扩列”(在累计列前插入当月值+同比两列并同步公式/样式);网约车/货运/城市客运等其余页取数待 M2/M3 åŽç»­æŽ¥å…¥ï¼›ç¼ºæ•°æç¤ºå½“前进日志,页面提示接入在整链完成后补。
## ç»“论
M2 å°è´¦å±‚(班线包车/公交/出租/轨道轮渡)数据驱动回填可用:2026-07 ä¸Žäººå·¥å¤‡ä»½æ¯ç‰ˆé€æ ¼ 0 å·®å¼‚,2026-08 å¯æŠŠ 8 æœˆæ–°æœˆä»½æ•°æ®ä»Žåº“填入,未破坏既有 1-7 æœˆå€¼ã€‚可进入下一轮(自动扩列 + ç½‘约车/货运/合成页 M3)开发。
docs/¹¤×÷ÈÕ½á/¹¤×÷ÈÕ½á_2026-09-09.md
New file
@@ -0,0 +1,95 @@
# å·¥ä½œæ—¥ç»“ 2026-09-09
## ä»Šæ—¥å®Œæˆ
- æ±‡æ€»å¤§è¡¨ï¼šåˆ é™¤å¯¼å‡ºæ•´æœ¬æ—¶çš„“内容自适应列宽”,列宽回归母版原始宽度(经 openpyxl é€é¡µé€åˆ—比对仅剩 POI è¡¥é»˜è®¤å®½ï¼Œæ— å®žè´¨å·®å¼‚);取消筛选/还原隐藏行/两位小数/打开重算等既有处理保留。
- å®¡æ ¸è§„则:
  - RULE_001 æ”¹ä¸ºâ€œè½¦è¾†æ•°ä¸Žå¨ä½è¿æ”¿åˆ†é¡¹å¯¹æ¯””(DIFF_FIELDS):牵引车/挂车/整车车类差≤5 å°ï¼ŒæŒ‚车吨位/整车吨位差≤50 å¨ï¼Œæ€»å¨ä½å·®â‰¤50 å¨ï¼Œå‘½ä¸­æ¶ˆæ¯å¸¦ ä¸ŠæŠ¥/运政/å·®/限 æ˜Žç»†å¹¶é˜² 200 å­—符溢出;
  - RULE_002 å¹¶å…¥ RULE_001 æ€»å¨ä½æ®µåŽåœç”¨ï¼ˆé˜ˆå€¼ä¸Žè¯´æ˜Žç»Ÿä¸€ä¸º 50);
  - è§„则阈值→名称/说明联动推广到全部规则:支持台/吨/公里等非百分比数字与多段阈值(如 5,5,50,5,50,50),DIFF/比值/百分数三类实测双向联动并复原无污染。
- æ•°æ®å¯¼å…¥ï¼šå•文件导入成功/失败增加弹窗提示(成功含解析条数,失败列错误明细)。
- æµ‹è¯•报告:docs/功能测试报告/功能测试报告_RULE001分项运政与阈值联动及列宽回归_2026-09-09.md。
## æµ‹è¯•结果摘要
- 2026-08 è´§è¿å®¡æ ¸æ•´æœŸæ‰§è¡Œ 4s è¿”回 487 æ¡ï¼ŒRULE_001 å‘½ä¸­ 115 æ¡ï¼Œæ¶ˆæ¯ç»“构正确;RULE_002 ä¸å†å‡ºç»“果。
- æ±‡æ€»æ•´æœ¬å¯¼å‡º 2026-08 æˆåŠŸï¼ˆ938KB/19 é¡µç­¾ï¼‰ï¼Œåˆ—宽与母版一致。
- å‰ç«¯ dev ç¼–译成功;导入弹窗建议用户在界面点验一次。
## é—留问题与待办
- **汇总大表 8 æœˆå¡«å€¼ï¼ˆæ•°æ®é©±åЍ M2/M3)尚未落地**:当前整本仍是“复制母版+备份还原”,导入的 8 æœˆæ•°æ®ä¸ä¼šè‡ªåŠ¨è¿›æ•´æœ¬ï¼›ä¸‹ä¸€æ­¥å®žçŽ° SummaryFiller è¯»åº“回填(当月/累计/同比/排名)。
- 8 æœˆç¼ºæ•°ï¼ˆè‡ªåŠ¨å¡«å€¼å‰ç½®ï¼‰ï¼šfreight_turnover_import、scale_split_transport、passenger_individual_monthly、wyc_total/order å‡æ—  2026-08 è¡Œï¼ˆè´§è¿ä¼ä¸šæœˆæŠ¥ã€å…¬è·¯æ—…客、公交、巡游出租已有 08)。
- è¿æ”¿â€œæ•´è½¦/其他车”分类粒度:RULE_001 æ•´è½¦æ®µçŽ°æŒ‰è¿æ”¿ other(牵引/挂车以外)口径比对,若业务要求“整车=不含集装箱”需运政侧数据配合。
- å¾…用户点验:导入弹窗 UI æ•ˆæžœï¼›RULE_001 å‘½ä¸­é˜ˆå€¼ï¼ˆ5 å°/50 å¨ï¼‰æ˜¯å¦ç¬¦åˆä¸šåŠ¡é¢„æœŸã€‚
## æ˜Žæ—¥è®¡åˆ’
- å¯åЍ M2 SummaryFiller(先 7 æœˆå›žå½’ diff=0,再 8 æœˆå®žå¼¹ï¼‰ï¼Œè¡¥é½ 8 æœˆè´§è¿é‡å‘¨è½¬é‡/规上规下/个体客运等缺数后整链验证。
---
## ä¸Šåˆç¬¬äºŒè½®ï¼ˆ4 é¡¹æ•´æ”¹ï¼‰2026-09-09 11:xx
### å®Œæˆå†…容
- å®¡æ ¸ç»“果页:明细视图改为与用户模板同款的 10 åˆ—表格(报表期/企业/规则编码/规则/等级/实际/阈值/偏差/状态/操作),卡片式移除;新增「问题类型(核实型默认/全部)」与「状态(全部/待处理/已处理)」筛选;导出把两项筛选带给后端。
- å®¡æ ¸å¯¼å‡ºï¼ˆæ¨¡æ¿ docs/审核界面月报模板/模板_审核结果_.xlsx åŒ Sheet):Sheet1 å®¡æ ¸ç»“果(仅核实型,排除 RULE_004 æé†’型);Sheet2 ä¼ä¸šåˆå¹¶æŒ‰ä¼ä¸šæ±‡æ€»ï¼Œé—®é¢˜åˆ—自动生成 1、2、3 ç¼–号话术,企业/问题两列按模板合并单元格(此前错合并报表期列已修正)。
- æ•°æ®æŸ¥çœ‹ï¼šæ•°æ®ç±»åž‹é€‰æ‹©æ”¹ä¸ºã€Œåˆ†ç»„模块+类型下拉」与导入页同款布局与文案;报表期、关键字查询放到下方筛选行;企业代码/统一社会信用代码/填报单位三列隐藏(按 prop ä¸Ž label åŒé‡è¿‡æ»¤ï¼‰ã€‚
- æŠ¥è¡¨ç”Ÿæˆï¼šè´§è¿/旅客/城市客运三个小表模块整组隐藏,页签仅剩 æ±‡æ€»å¤§è¡¨/投资报表/能耗报表;道路运输量汇总表(整本)预览新增关键指标:货运量、货运周转量、城市客运量/周转量、中口径客运量/周转量的当月/1..M累计/同比(数据齐即显示,缺 2025 å¹´åŸºæœŸæ•°æ®æ—¶é€é¡¹ç»™æç¤ºï¼‰ã€‚
### æµ‹è¯•结果摘要
- mvn ç¼–译通过;后端重启就绪;前端 dev ç¼–译成功(chunk æŠ½æŸ¥ç¡®è®¤æ–°å‚数已进产物)。
- /api/audit/export 2026-08 H2032:审核结果 475 è¡Œ 10 åˆ— + ä¼ä¸šåˆå¹¶ 150 ç»„(B/G åˆå¹¶ï¼‰ï¼Œä»…核实型;status=pending/handled è¿‡æ»¤ç”Ÿæ•ˆã€‚
- /api/report/preview summaryWorkbook:2026-07 æ˜¾ç¤ºå…¨çœè´§è¿é‡ 16285.71 ä¸‡å¨/累计 113423.13,周转量当月/累计+同比,城市客运量当月 30612.85 ä¸‡äººæ¬¡ç­‰ï¼›åŒæ¯”缺 2025-07 åŸºæœŸæ•°æ®çš„三项均有明确提示。
- æ•°æ®æŸ¥çœ‹åˆ—结构核对:服务端含 enterpriseCode/unifiedCreditCode/reportUnit ä¸‰åˆ—(前端隐藏生效)。
---
## ä¸‹åˆç¬¬ä¸‰è½®ï¼ˆ2026-09-09 16:xx)界面语义收敛 / å¯¼å‡ºé—®é¢˜è¯æœ¯ / é¢„览表格化(已在本地复验)
### å®Œæˆå†…容
- å®¡æ ¸ç»“果页:视图改为默认「按企业合并」(保留「按条明细」);「问题范围」改文案「仅需核实/含提醒型」并附说明「提醒型仅提示变化、不需处理,导出默认不含」;筛选区卡片化;统计卡与「当前筛选 N æ¡/M å®¶ä¼ä¸šã€éšç­›é€‰å®žæ—¶è”动(切换不再需要重新查询)。
- å®¡æ ¸ç»“果导出:企业合并「问题」列按用户 4 æ¡ä¾‹æ–‡æ ¼å¼è½åœ°ï¼ˆRULE_P003/04 å®¢è¿é‡ã€æ—…客周转量环比=0 æ ¸å®žå¼‚常(上月…=…,本月…=…)请核实;RULE_P005 ç­çº¿å¹³å‡è¿è·çŽ¯æ¯”è¶…é™é™„ä¸Šä¸‹æœˆä¸ŽçŽ¯æ¯”ï¼›RULE_P006 åŒ…车平均运距等于上月(上月运距=…,本月运距=…)请核实是否固定线路旅游或通勤);H2032 è´§è¿è¯æœ¯è¡¥åˆ†é¡¹æ˜Žç»†ï¼ˆè½¦è¾†/吨位运政差、轨迹周转量、集装箱、整车吨位范围等)。后端 queryResults æ”¹ä¸ºå…ˆæŒ‰æŠ¥è¡¨ç±»åž‹å–规则再查该期结果。
- æ•°æ®æŸ¥çœ‹ï¼šé¡¶éƒ¨å·¥å…·æ æ•´ç†ï¼ˆåˆ†ç»„模块+类型下拉/报表期/关键字/查询/全屏一行化),底部计数改「共 N æ¡ï¼Œå·²åŠ è½½ N æ¡ã€ã€‚
- æŠ¥è¡¨ç”Ÿæˆï¼šæ±‡æ€»å¤§è¡¨ï¼ˆæ•´æœ¬ï¼‰é¢„览改为分组表(道路货运/城市客运/中口径 Ã— å½“月/累计/同比),缺 2025 åŸºæœŸæˆ–当月源数据时给「—」与提示。
### æµ‹è¯•结果摘要
- é¡µé¢å®žæµ‹ï¼ˆ8080):2026-08 è´§è¿æŸ¥è¯¢ 2~3s è¿”回 487 æ¡/403 å®¶æ— è½¬åœˆï¼›ã€Œä»…需核实/含提醒型」点击即时生效(487→474 æ¡è‡ªåЍ剔除 RULE_004);视图默认按企业合并可切明细;行内「数据」接口 23~561ms è¿”回。
- å¯¼å‡ºå®žæµ‹ï¼šH2031 2026-08 é—®é¢˜åˆ—与 4 æ¡ä¾‹æ–‡é€å­—比对一致;H2032 2026-08 åŒ Sheet 475 è¡Œã€B/G åˆå¹¶æ­£å¸¸ã€‚
- å‰ç«¯ dev ç¼–译成功(15:42);后端 15:45 é‡å¯å°±ç»ªï¼›ä¸¤ä¸ªè¿œç«¯ï¼ˆgitea main / ä¸ªäºº gitee master)均无新提交。
- æµ‹è¯•报告:docs/功能测试报告/功能测试报告_审核界面交互与问题话术导出_2026-09-09.md。
### é—留问题与待办(与上午一致)
- æ±‡æ€»å¤§è¡¨ 8 æœˆè‡ªåŠ¨å¡«å€¼ï¼ˆM2/M3 SummaryFiller)未启动;8 æœˆç¼º freight_turnover_import、scale_split_transport、passenger_individual_monthly、wyc_total/order æºæ•°æ®ã€‚
- å¾…业务终验:问题范围/视图/状态控件、行内数据弹窗、导出 Excel ä¸Žæ¨¡æ¿/四条例文逐条核对;导入弹窗 UI、RULE_001 é˜ˆå€¼ï¼ˆ5 å°/50 å¨ï¼‰æ˜¯å¦ç¬¦åˆä¸šåŠ¡é¢„æœŸã€‚
---
## ä¸‹åˆç¬¬å››è½®ï¼ˆ2026-09-09 17:xx)M2 æ•°æ®é©±åŠ¨å›žå¡«å¯åŠ¨ï¼šå°è´¦å±‚è¯»åº“çŽ°ç®—å†™æ•´æœ¬
### å®Œæˆå†…容
- æ–°å¢ž `SummaryWorkbookFiller`(reportexport/calc):生成整本时把 2026 å¹´ 1..M æœˆå°è´¦é¡µå¸‚州明细行当月值从库现算写入(m æœˆå€¼åˆ—=C+2*(m-1)),行定位按“列A市州名+列B指标文案”扫描,不写死行号;全省行/跨页合成保持公式,打开自动重算;2025 å‚照列沿用母版缓存。
- è¦†ç›–页:班线包车(含个体)、公交、出租、轨道轮渡;网约车(依赖拆分输入)与货运页待 M3。
- ReportExportService.exportSummaryWorkbook æŽ¥å…¥å›žå¡«ï¼ˆdonor è¿˜åŽŸåŽè¦†ç›–å½“æœˆå€¼ï¼‰ï¼Œç¼ºæ•°æç¤ºè¿›æ—¥å¿—ã€‚
### æµ‹è¯•结果摘要
- mvn -o compile é€šè¿‡ï¼›åŽç«¯ 16:28 é‡å¯å°±ç»ªã€‚
- 2026-07 æ•´æœ¬å›žå½’:四张台账页月度值列(C..Q)与备份母版共 1323 æ ¼é€æ ¼ 0 å·®å¼‚(备份母版部分文本数字按数值比较)。
- 2026-08 æ•´æœ¬ï¼šå…¬äº¤/出租 0 å·®å¼‚;班线包车/轨道轮渡把库中 8 æœˆï¼ˆQ åˆ—)数据现算填入(此前的“8月整本”其实只填到 7 æœˆï¼‰ï¼Œç¬¦åˆæ•°æ®é©±åŠ¨ç›®æ ‡ã€‚
- æµ‹è¯•报告:docs/功能测试报告/功能测试报告_汇总大表数据驱动回填M2台账层_2026-09-09.md。
### é—留问题与待办
- 9 æœˆåŠä»¥ä¸Šæ— å½“月母版且母版无对应月份列:需“自动扩列”(累计列前插当月值+同比两列、公式/样式/2025 å‚照组右移同步),本轮未做。
- M2 å‰©ä½™é¡µï¼šç½‘约车台账(需《网约车订单及全省总量》当月拆分输入)、货运页(FreightCalc è§„上/规下/全口径)+ M3 åˆæˆ/成品全链。
- ç¼ºæ•°æç¤ºç›®å‰è¿›åŽç«¯æ—¥å¿—,页面提示接入待整链完成。
---
## ä¸‹åˆç¬¬äº”轮(2026-09-09 18:xx)审核结果页“选月一直转圈”修复
### å®Œæˆå†…容
- å®šä½å¹¶ä¿®å¤å®¡æ ¸ç»“果页渲染崩溃:`AuditResult.vue` æŠŠå¸¦å‚函数 `isReminderRow(r)` è¯¯æ”¾å…¥ `computed`,结果集非空时 `filteredResults` è°ƒç”¨å®ƒæŠ› `TypeError`,导致整页转圈、按钮无响应(“核实型/全部、视图”点了没反应同源)。已移到 `methods`,功能不变。
- æµè§ˆå™¨å®žæµ‹ï¼ˆ#/audit,admin å·²ç™»å½•):修复前页面自动加载 2026-09 è´§è¿ï¼ˆ146 æ¡ï¼‰å³è¿žç»­æŠ¥ `isReminderRow is not a function`;修复后刷新正常渲染 146 æ¡/142 å®¶ã€ç»Ÿè®¡ 136 çº¢/10 é»„/146 å¾…处理、按钮恢复可用,控制台 9 ç‚¹åŽé›¶æ¸²æŸ“错误。
- å¤æ ¸åŽç«¯æŽ¥å£è€—时(2026-08 å„类型 execute 0.26~3.3s、results/ruleStats <0.4s)确认非后端瓶颈。
### æµ‹è¯•结果摘要
- ä¿®å¤å‰å¤çް + ä¿®å¤åŽæ­£å‘用例 + ç©ºç»“果集反向用例均按预期;详见 docs/功能测试报告/功能测试报告_审核结果页选月转圈修复_2026-09-09.md。
### é—留问题与待办
- æ±‡æ€»å¤§è¡¨ M3:网约车台账取数、货运页(FreightCalc è§„上/规下)、城市客运/中口径明细合成页;“9 æœˆ+无母版自动扩列”未做。
- 8 æœˆä»ç¼º 4 ç±»æºæ•°æ®è¡¨è¡Œï¼ˆfreight_turnover_import、scale_split_transport、passenger_individual_monthly、网约车拆分输入),需先补数据再整链验证 8 æœˆå…¨è‡ªåŠ¨ã€‚
- å¾…用户浏览器验收本次审核结果页交互修复(选 8 æœˆè´§è¿/公交、切换范围与视图、执行审核/导出)。
traffic-audit-server/src/main/java/com/trafficaudit/auditengine/controller/AuditController.java
@@ -56,8 +56,10 @@
    @GetMapping("/export")
    public ResponseEntity<byte[]> exportResults(@RequestParam("period") String period,
                                                @RequestParam(value = "reportType", defaultValue = "H2032") String reportType) throws Exception {
        byte[] bytes = auditService.exportResults(period, reportType);
                                                @RequestParam(value = "reportType", defaultValue = "H2032") String reportType,
                                                @RequestParam(value = "onlyVerify", defaultValue = "true") boolean onlyVerify,
                                                @RequestParam(value = "status", defaultValue = "all") String status) throws Exception {
        byte[] bytes = auditService.exportResults(period, reportType, onlyVerify, status);
        String name = java.net.URLEncoder.encode("审核结果_" + period + ".xlsx", "UTF-8");
        return ResponseEntity.ok()
            .header("Content-Disposition", "attachment; filename*=UTF-8''" + name)
traffic-audit-server/src/main/java/com/trafficaudit/auditengine/service/AuditEngineService.java
@@ -1259,19 +1259,17 @@
    public List<AuditResult> queryResults(String reportPeriod, String reportType) {
        String type = reportType == null || reportType.trim().isEmpty() ? "H2032" : reportType.trim();
        List<AuditRule> rules = ruleMapper.selectList(new LambdaQueryWrapper<AuditRule>()
            .eq(AuditRule::getReportType, type));
        if (rules.isEmpty()) return new java.util.ArrayList<>();
        List<Long> ruleIds = new ArrayList<>();
        for (AuditRule rule : rules) ruleIds.add(rule.getId());
        List<AuditResult> results = resultMapper.selectList(new LambdaQueryWrapper<AuditResult>()
            .eq(AuditResult::getReportPeriod, reportPeriod)
            .in(AuditResult::getRuleId, ruleIds)
            .orderByAsc(AuditResult::getId));
        List<Long> ruleIds = new ArrayList<>();
        for (AuditRule rule : ruleMapper.selectList(null)) {
            if (type.equals(rule.getReportType())) ruleIds.add(rule.getId());
        }
        List<AuditResult> filtered = new ArrayList<>();
        for (AuditResult result : results) {
            if (ruleIds.contains(result.getRuleId())) filtered.add(result);
        }
        attachInfo(filtered);
        return filtered;
        attachInfo(results, type, reportPeriod);
        return results;
    }
    public void reviewResult(Long id, String status, String comment) {
@@ -1283,46 +1281,56 @@
        resultMapper.updateById(result);
    }
    private void attachInfo(List<AuditResult> results) {
    private void attachInfo(List<AuditResult> results, String type, String period) {
        if (results.isEmpty()) return;
        Map<Long, AuditRule> ruleMap = new HashMap<>();
        for (AuditRule rule : ruleMapper.selectList(null)) ruleMap.put(rule.getId(), rule);
        Map<Long, String> h2032Name = new HashMap<>();
        Map<Long, String> h2032Explain = new HashMap<>();
        Map<Long, String> passengerName = new HashMap<>();
        Map<Long, String> passengerExplain = new HashMap<>();
        Map<Long, String> energyName = new HashMap<>();
        Map<Long, String> investName = new HashMap<>();
        Map<Long, String> busName = new HashMap<>();
        Map<Long, String> busExplain = new HashMap<>();
        Map<Long, String> taxiName = new HashMap<>();
        Map<Long, String> taxiExplain = new HashMap<>();
        for (H2032EnterpriseMonthly r : h2032Mapper.selectList(null)) {
            h2032Name.putIfAbsent(r.getId(), r.getEnterpriseName());
            h2032Explain.putIfAbsent(r.getId(), r.getVerifyExplanation());
        for (AuditRule rule : ruleMapper.selectList(new LambdaQueryWrapper<AuditRule>()
                .eq(AuditRule::getReportType, type))) {
            ruleMap.put(rule.getId(), rule);
        }
        for (PassengerEnterpriseMonthly r : passengerMapper.selectList(null)) {
            passengerName.putIfAbsent(r.getId(), r.getEnterpriseName());
            passengerExplain.putIfAbsent(r.getId(), r.getVerifyExplanation());
        }
        for (EnergyVehicleQuarterly r : energyMapper.selectList(null)) {
            energyName.putIfAbsent(r.getId(), r.getEnterpriseName());
        }
        for (CityBusMonthly r : cityBusMapper.selectList(null)) {
            busName.putIfAbsent(r.getId(), r.getEnterpriseName());
            busExplain.putIfAbsent(r.getId(), r.getVerifyExplanation());
        }
        for (CityTaxiMonthly r : cityTaxiMapper.selectList(null)) {
            taxiName.putIfAbsent(r.getId(), r.getCity());
            taxiExplain.putIfAbsent(r.getId(), r.getVerifyExplanation());
        }
        Map<Long, Long> investProjectId = new HashMap<>();
        for (InvestmentMonthly r : investMonthlyMapper.selectList(null)) {
            investProjectId.putIfAbsent(r.getId(), r.getProjectId());
        }
        for (InvestmentProject p : investProjectMapper.selectList(null)) {
            for (Map.Entry<Long, Long> e : investProjectId.entrySet()) {
                if (e.getValue().equals(p.getId())) investName.putIfAbsent(e.getKey(), p.getProjectName());
        Map<Long, String> nameMap = new HashMap<>();
        Map<Long, String> explainMap = new HashMap<>();
        if ("INVEST".equals(type)) {
            Map<Long, Long> monthlyProject = new HashMap<>();
            for (InvestmentMonthly m : investMonthlyMapper.selectList(new LambdaQueryWrapper<InvestmentMonthly>()
                    .eq(InvestmentMonthly::getReportPeriod, period))) {
                monthlyProject.put(m.getId(), m.getProjectId());
            }
            for (InvestmentProject p : investProjectMapper.selectList(null)) {
                for (Map.Entry<Long, Long> e : monthlyProject.entrySet()) {
                    if (e.getValue() != null && e.getValue().equals(p.getId())) {
                        nameMap.putIfAbsent(e.getKey(), p.getProjectName());
                    }
                }
            }
        } else if ("H204".equals(type)) {
            for (EnergyVehicleQuarterly m : energyMapper.selectList(new LambdaQueryWrapper<EnergyVehicleQuarterly>()
                    .eq(EnergyVehicleQuarterly::getReportPeriod, period))) {
                nameMap.putIfAbsent(m.getId(), m.getEnterpriseName());
            }
        } else if ("H2031".equals(type)) {
            for (PassengerEnterpriseMonthly m : passengerMapper.selectList(new LambdaQueryWrapper<PassengerEnterpriseMonthly>()
                    .eq(PassengerEnterpriseMonthly::getReportPeriod, period))) {
                nameMap.putIfAbsent(m.getId(), m.getEnterpriseName());
                explainMap.putIfAbsent(m.getId(), m.getVerifyExplanation());
            }
        } else if ("CITY_BUS".equals(type)) {
            for (CityBusMonthly m : cityBusMapper.selectList(new LambdaQueryWrapper<CityBusMonthly>()
                    .eq(CityBusMonthly::getReportPeriod, period))) {
                nameMap.putIfAbsent(m.getId(), m.getEnterpriseName());
                explainMap.putIfAbsent(m.getId(), m.getVerifyExplanation());
            }
        } else if ("CITY_TAXI".equals(type)) {
            for (CityTaxiMonthly m : cityTaxiMapper.selectList(new LambdaQueryWrapper<CityTaxiMonthly>()
                    .eq(CityTaxiMonthly::getReportPeriod, period))) {
                nameMap.putIfAbsent(m.getId(), m.getCity());
                explainMap.putIfAbsent(m.getId(), m.getVerifyExplanation());
            }
        } else {
            for (H2032EnterpriseMonthly m : h2032Mapper.selectList(new LambdaQueryWrapper<H2032EnterpriseMonthly>()
                    .eq(H2032EnterpriseMonthly::getReportPeriod, period))) {
                nameMap.putIfAbsent(m.getId(), m.getEnterpriseName());
                explainMap.putIfAbsent(m.getId(), m.getVerifyExplanation());
            }
        }
        for (AuditResult result : results) {
@@ -1332,69 +1340,49 @@
                result.setRuleCode(rule.getRuleCode());
                result.setAlertLevel(rule.getAlertLevel());
            }
            // å„报表表 id å„自独立自增,需按规则所属报表类型分表回填企业名称
            String type = rule == null ? null : rule.getReportType();
            Map<Long, String> nameMap = "INVEST".equals(type) ? investName
                : "H204".equals(type) ? energyName
                : "H2031".equals(type) ? passengerName
                : "CITY_BUS".equals(type) ? busName
                : "CITY_TAXI".equals(type) ? taxiName : h2032Name;
            Map<Long, String> explainMap = "H2031".equals(type) ? passengerExplain
                : "CITY_BUS".equals(type) ? busExplain
                : "CITY_TAXI".equals(type) ? taxiExplain : h2032Explain;
            String enterpriseName = nameMap.get(result.getReportId());
            if (enterpriseName == null) {
                // ä¸ŠæŠ¥æ•°æ®è¢«é‡æ–°å¯¼å…¥åŽæ—§å®¡æ ¸ç»“果已失效,给出提示而非空白
                enterpriseName = "\uFF08\u539F\u6570\u636E\u5DF2\u91CD\u65B0\u5BFC\u5165\uFF0C\u8BF7\u91CD\u65B0\u6267\u884C\u5BA1\u6838\uFF09";
                enterpriseName = "(原数据已重新导入,请重新执行审核)";
            }
            result.setEnterpriseName(enterpriseName);
            result.setVerifyExplanation(explainMap.get(result.getReportId()));
        }
    }
    /** æœ€è¿‘有数据的报表期(用于前端默认选中) */
    public String latestPeriod(String reportType) {
        String type = reportType == null || reportType.trim().isEmpty() ? "H2032" : reportType.trim();
        String max = null;
        switch (type) {
            case "H2031":
                for (PassengerEnterpriseMonthly m : passengerMapper.selectList(
                        new LambdaQueryWrapper<PassengerEnterpriseMonthly>().select(PassengerEnterpriseMonthly::getReportPeriod))) {
                    max = maxPeriod(max, m.getReportPeriod());
                }
                break;
                return firstOf(passengerMapper.selectObjs(new LambdaQueryWrapper<PassengerEnterpriseMonthly>()
                    .select(PassengerEnterpriseMonthly::getReportPeriod)
                    .orderByDesc(PassengerEnterpriseMonthly::getReportPeriod).last("LIMIT 1")));
            case "H204":
                for (EnergyVehicleQuarterly m : energyMapper.selectList(
                        new LambdaQueryWrapper<EnergyVehicleQuarterly>().select(EnergyVehicleQuarterly::getReportPeriod))) {
                    max = maxPeriod(max, m.getReportPeriod());
                }
                break;
                return firstOf(energyMapper.selectObjs(new LambdaQueryWrapper<EnergyVehicleQuarterly>()
                    .select(EnergyVehicleQuarterly::getReportPeriod)
                    .orderByDesc(EnergyVehicleQuarterly::getReportPeriod).last("LIMIT 1")));
            case "INVEST":
                for (InvestmentMonthly m : investMonthlyMapper.selectList(
                        new LambdaQueryWrapper<InvestmentMonthly>().select(InvestmentMonthly::getReportPeriod))) {
                    max = maxPeriod(max, m.getReportPeriod());
                }
                break;
                return firstOf(investMonthlyMapper.selectObjs(new LambdaQueryWrapper<InvestmentMonthly>()
                    .select(InvestmentMonthly::getReportPeriod)
                    .orderByDesc(InvestmentMonthly::getReportPeriod).last("LIMIT 1")));
            case "CITY_BUS":
                for (CityBusMonthly m : cityBusMapper.selectList(
                        new LambdaQueryWrapper<CityBusMonthly>().select(CityBusMonthly::getReportPeriod))) {
                    max = maxPeriod(max, m.getReportPeriod());
                }
                break;
                return firstOf(cityBusMapper.selectObjs(new LambdaQueryWrapper<CityBusMonthly>()
                    .select(CityBusMonthly::getReportPeriod)
                    .orderByDesc(CityBusMonthly::getReportPeriod).last("LIMIT 1")));
            case "CITY_TAXI":
                for (CityTaxiMonthly m : cityTaxiMapper.selectList(
                        new LambdaQueryWrapper<CityTaxiMonthly>().select(CityTaxiMonthly::getReportPeriod))) {
                    max = maxPeriod(max, m.getReportPeriod());
                }
                break;
                return firstOf(cityTaxiMapper.selectObjs(new LambdaQueryWrapper<CityTaxiMonthly>()
                    .select(CityTaxiMonthly::getReportPeriod)
                    .orderByDesc(CityTaxiMonthly::getReportPeriod).last("LIMIT 1")));
            default:
                for (H2032EnterpriseMonthly m : h2032Mapper.selectList(
                        new LambdaQueryWrapper<H2032EnterpriseMonthly>().select(H2032EnterpriseMonthly::getReportPeriod))) {
                    max = maxPeriod(max, m.getReportPeriod());
                }
                break;
                return firstOf(h2032Mapper.selectObjs(new LambdaQueryWrapper<H2032EnterpriseMonthly>()
                    .select(H2032EnterpriseMonthly::getReportPeriod)
                    .orderByDesc(H2032EnterpriseMonthly::getReportPeriod).last("LIMIT 1")));
        }
        return max;
    }
    private String firstOf(List<Object> objs) {
        if (objs == null || objs.isEmpty() || objs.get(0) == null) return null;
        String v = String.valueOf(objs.get(0));
        return v.trim().isEmpty() ? null : v.trim();
    }
    private String maxPeriod(String a, String b) {
@@ -1403,24 +1391,29 @@
        return a;
    }
    /** å®¡æ ¸ç»“果导出 Excel(含企业名/规则名/状态/审核意见) */
    /** å®¡æ ¸ç»“果导出 Excel(两 Sheet,按业务模板):Sheet1 å®¡æ ¸ç»“果(逐条明细);Sheet2 ä¼ä¸šåˆå¹¶ï¼ˆæŒ‰ä¼ä¸šæ±‡æ€» + é—®é¢˜åˆ—) */
    public byte[] exportResults(String reportPeriod, String reportType) throws Exception {
        return exportResults(reportPeriod, reportType, true, "all");
    }
    /** onlyVerify=true æ—¶å‰”除“提醒型”规则(如 RULE_004 çŽ¯æ¯”æé†’ï¼‰ï¼Œåªä¿ç•™æ ¸å®žåž‹é—®é¢˜ï¼›status=all/pending/handled ä¸Žç•Œé¢â€œçŠ¶æ€â€ç­›é€‰ä¸€è‡´ */
    public byte[] exportResults(String reportPeriod, String reportType, boolean onlyVerify, String status) throws Exception {
        List<AuditResult> results = queryResults(reportPeriod, reportType);
        java.util.List<AuditResult> filtered = new java.util.ArrayList<>();
        String st = status == null ? "all" : status.trim();
        for (AuditResult res : results) {
            if (onlyVerify && isReminderRule(res.getRuleCode())) continue;
            if ("pending".equals(st) && !"PENDING".equals(res.getStatus())) continue;
            if ("handled".equals(st) && (res.getStatus() == null || "PENDING".equals(res.getStatus()))) continue;
            filtered.add(res);
        }
        try (XSSFWorkbook wb = new XSSFWorkbook()) {
            // ---------- Sheet1 å®¡æ ¸ç»“果(逐条,列与业务模板一致) ----------
            Sheet sheet = wb.createSheet("审核结果");
            String[] headers = {"报表期", "企业/市州", "规则编码", "审核规则", "等级", "实际值", "阈值/基准", "偏差说明", "状态", "审核意见"};
            Row head = sheet.createRow(0);
            for (int i = 0; i < headers.length; i++) {
                Cell cell = head.createCell(i);
                cell.setCellValue(headers[i]);
                org.apache.poi.ss.usermodel.CellStyle style = wb.createCellStyle();
                org.apache.poi.ss.usermodel.Font font = wb.createFont();
                font.setBold(true);
                style.setFont(font);
                cell.setCellStyle(style);
            }
            writeAuditHeader(sheet, headers);
            int r = 1;
            for (AuditResult res : results) {
            for (AuditResult res : filtered) {
                Row row = sheet.createRow(r++);
                row.createCell(0).setCellValue(valueOf(res.getReportPeriod()));
                row.createCell(1).setCellValue(valueOf(res.getEnterpriseName()));
@@ -1437,10 +1430,154 @@
            for (int i = 0; i < widths.length; i++) {
                sheet.setColumnWidth(i, widths[i] * 256);
            }
            // ---------- Sheet2 ä¼ä¸šåˆå¹¶ï¼ˆåŒä¸€ä¼ä¸šå¤šæ¡è§„则并排列出 + é—®é¢˜æ±‡æ€»åˆ—) ----------
            Sheet ms = wb.createSheet("企业合并");
            String[] h2 = {"报表期", "企业/市州", "审核规则", "实际值", "阈值/基准", "偏差说明", "问题"};
            writeAuditHeader(ms, h2);
            java.util.LinkedHashMap<String, java.util.List<AuditResult>> groups = new java.util.LinkedHashMap<>();
            for (AuditResult res : filtered) {
                String key = valueOf(res.getEnterpriseName());
                if (key.isEmpty()) key = valueOf(res.getReportPeriod()) + "#" + valueOf(res.getActualValue());
                groups.computeIfAbsent(key, k -> new java.util.ArrayList<>()).add(res);
            }
            int rowCursor = 1;
            for (java.util.Map.Entry<String, java.util.List<AuditResult>> e : groups.entrySet()) {
                java.util.List<AuditResult> items = e.getValue();
                int groupStart = rowCursor;
                StringBuilder problems = new StringBuilder();
                int seq = 0;
                for (AuditResult it : items) {
                    Row row = ms.createRow(rowCursor++);
                    if (seq == 0) {
                        row.createCell(0).setCellValue(valueOf(it.getReportPeriod()));
                        row.createCell(1).setCellValue(valueOf(it.getEnterpriseName()).isEmpty() ? e.getKey() : valueOf(it.getEnterpriseName()));
                    }
                    row.createCell(2).setCellValue(valueOf(it.getRuleName()));
                    row.createCell(3).setCellValue(valueOf(it.getActualValue()));
                    row.createCell(4).setCellValue(valueOf(it.getThresholdValue()));
                    row.createCell(5).setCellValue(valueOf(it.getDeviation()));
                    seq++;
                    if (problems.length() > 0) problems.append("\n");
                    problems.append(seq).append("、").append(issueSentence(it));
                }
                if (rowCursor - groupStart > 1) {
                    ms.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(groupStart, rowCursor - 1, 1, 1));
                    ms.addMergedRegion(new org.apache.poi.ss.util.CellRangeAddress(groupStart, rowCursor - 1, 6, 6));
                }
                Row first = ms.getRow(groupStart);
                if (first.getCell(6) == null) first.createCell(6);
                first.getCell(6).setCellValue(problems.toString());
            }
            int[] widths2 = {12, 30, 22, 34, 22, 34, 60};
            for (int i = 0; i < widths2.length; i++) {
                ms.setColumnWidth(i, widths2[i] * 256);
            }
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            wb.write(out);
            return out.toByteArray();
        }
    }
    private void writeAuditHeader(Sheet sheet, String[] headers) {
        Row head = sheet.createRow(0);
        for (int i = 0; i < headers.length; i++) {
            Cell cell = head.createCell(i);
            cell.setCellValue(headers[i]);
            org.apache.poi.ss.usermodel.CellStyle style = sheet.getWorkbook().createCellStyle();
            org.apache.poi.ss.usermodel.Font font = sheet.getWorkbook().createFont();
            font.setBold(true);
            style.setFont(font);
            cell.setCellStyle(style);
        }
    }
    /** æé†’型规则:仅提示变化、不需人工核实(如 RULE_004 è´§è¿ç±»åž‹çŽ¯æ¯”æé†’ï¼‰ */
    private boolean isReminderRule(String ruleCode) {
        return "RULE_004".equals(ruleCode);
    }
    /** ä¼ä¸šåˆå¹¶â€œé—®é¢˜â€åˆ—话术:按规则类型生成可读的核实意见 */
    private String issueSentence(AuditResult res) {
        String code = res.getRuleCode();
        String name = valueOf(res.getRuleName());
        String act = valueOf(res.getActualValue());
        if ("RULE_001".equals(code) || "RULE_002".equals(code)) {
            return "企业上报车辆/吨位与运政数据对比存在差异(" + act + "),请核实";
        }
        if ("RULE_003".equals(code)) {
            return "上报周转量与轨迹测算周转量差异明显(" + act + "),请参考轨迹核实企业周转量";
        }
        if ("RULE_004".equals(code)) {
            return "货运类型环比发生变化" + (act.isEmpty() ? "" : "(" + act + ")") + ",请关注是否填报正常";
        }
        if ("RULE_005".equals(code)) {
            return "无集装箱车但存在集装箱货运量(" + act + "),请核实车辆类型与货运量填报";
        }
        if ("RULE_006".equals(code)) {
            return "整车平均吨位超出合理范围(" + act + "),请核实平均吨位填报";
        }
        if (code != null && code.startsWith("RULE_P")) {
            String special = passengerIssueSentence(res);
            if (special != null) return special;
        }
        return name + "异常(" + act + "),请核实";
    }
    /** å®¢è¿ï¼ˆH2031)规则问题话术:贴合模板示例“核实异常(上期指标=,本期指标=)请核实” */
    private String passengerIssueSentence(AuditResult res) {
        String code = res.getRuleCode();
        String act = valueOf(res.getActualValue());
        if ("RULE_P003".equals(code) || "RULE_P004".equals(code)) {
            boolean turnover = "RULE_P004".equals(code);
            String inner = act;
            if (inner.startsWith("本月与上月相同: ")) {
                inner = inner.substring("本月与上月相同: ".length());
            }
            java.util.List<String> clauses = new java.util.ArrayList<>();
            for (String item : inner.split("、")) {
                int eq = item.indexOf('=');
                if (eq <= 0) { clauses.add(item); continue; }
                String label = item.substring(0, eq).trim();
                String v = item.substring(eq + 1).trim();
                clauses.add("上月" + (turnover ? "周转量" : "客运量") + "=" + v + ",本月" + label + "=" + v);
            }
            return valueOf(res.getRuleName()) + "异常(" + String.join(";", clauses) + ")请核实";
        }
        if ("RULE_P005".equals(code)) {
            String pct = extractPct(valueOf(res.getThresholdValue()));
            String prefix = pct.isEmpty() ? "班线平均运距环比超限" : "班线平均运距环比超±" + pct + "%";
            return prefix + ",正常班线运距在线路未发生重大变化前较为稳定(" + act + "),请核实";
        }
        if ("RULE_P006".equals(code)) {
            String inner = act;
            if (inner.startsWith("等于上月: ")) {
                inner = inner.substring("等于上月: ".length());
            }
            java.util.List<String> clauses = new java.util.ArrayList<>();
            for (String item : inner.split("、")) {
                int eq = item.indexOf('=');
                if (eq <= 0) { clauses.add(item); continue; }
                String v = item.substring(eq + 1).trim();
                clauses.add("上月运距=" + v + ",本月运距=" + v);
            }
            return "包车平均运距等于上月(" + String.join(";", clauses)
                + ")请核实企业是否是固定线路旅游或者通勤业务";
        }
        return null;
    }
    private String extractPct(String thresholdText) {
        if (thresholdText == null || thresholdText.isEmpty()) return "";
        int idx = thresholdText.indexOf('\u00b1');
        if (idx < 0) return "";
        StringBuilder num = new StringBuilder();
        for (int i = idx + 1; i < thresholdText.length(); i++) {
            char c = thresholdText.charAt(i);
            if (Character.isDigit(c) || c == '.') num.append(c);
            else if (!Character.isWhitespace(c)) break;
        }
        return num.toString();
    }
    private String auditStatusCn(String status) {
@@ -1569,6 +1706,8 @@
            switch (rule.getCompareType()) {
                case "DIFF":
                    return checkDiff(rule, report, authMap);
                case "DIFF_FIELDS":
                    return checkDiffFields(rule, report, authMap);
                case "RATIO":
                    return checkRatio(rule, report, authMap, trackMap);
                case "MOM":
@@ -1609,6 +1748,86 @@
                "差距≤" + fmt(threshold), "差距=" + fmt(diff));
        }
        return null;
    }
    /** RULE_001 åˆ†é¡¹è¿æ”¿å¯¹æ¯”:牵引车/挂车/整车车辆数与挂车吨位/整车吨位/总吨位分别与运政对比,任一分项超限即提醒 */
    private AuditResult checkDiffFields(AuditRule rule, H2032EnterpriseMonthly report,
                                        Map<String, TransportAuthVehicle> authMap) {
        TransportAuthVehicle auth = findAuth(authMap, report.getEnterpriseName());
        if (auth == null) return null;
        String[] fields = rule.getCheckField() == null ? new String[0] : rule.getCheckField().split(",");
        String[] limits = rule.getThreshold() == null ? new String[0] : rule.getThreshold().split("[,;|]");
        if (fields.length == 0 || limits.length != fields.length) return null;
        java.util.List<String> hits = new java.util.ArrayList<>();
        double maxAbs = 0;
        for (int i = 0; i < fields.length; i++) {
            String field = fields[i].trim();
            double limit;
            try {
                limit = Double.parseDouble(limits[i].trim());
            } catch (Exception e) {
                return null;
            }
            double actual;
            double reference;
            String unit;
            switch (field) {
                case "vehicleTractor":
                    actual = num(report.getVehicleTractor());
                    reference = num(auth.getTractorCount());
                    unit = "台";
                    break;
                case "vehicleTrailer":
                    actual = num(report.getVehicleTrailer());
                    reference = num(auth.getTrailerCount());
                    unit = "台";
                    break;
                case "tonsTrailer":
                    actual = nvl(report.getTonsTrailer());
                    reference = nvl(auth.getTrailerTons());
                    unit = "吨";
                    break;
                case "vehicleWhole":
                    actual = num(report.getVehicleWhole());
                    reference = num(auth.getOtherCount());
                    unit = "台";
                    break;
                case "tonsWhole":
                    actual = nvl(report.getTonsWhole());
                    reference = nvl(auth.getOtherTons());
                    unit = "吨";
                    break;
                case "tonsTotal":
                    actual = nvl(report.getTonsTotal());
                    reference = nvl(auth.getTrailerTons()) + nvl(auth.getOtherTons());
                    unit = "吨";
                    break;
                default:
                    return null;
            }
            double diff = Math.abs(actual - reference);
            maxAbs = Math.max(maxAbs, diff);
            if (diff > limit) {
                hits.add(h2032FieldLabel(field) + ":上报=" + fmt(actual) + " " + unit + ",运政=" + fmt(reference) + " " + unit
                    + ",差" + fmt(diff) + " " + unit + "(限" + fmt(limit) + " " + unit + ")");
            }
        }
        if (hits.isEmpty()) return null;
        StringBuilder detail = new StringBuilder();
        for (int i = 0; i < hits.size(); i++) {
            String one = hits.get(i);
            if (detail.length() == 0) {
                detail.append(one);
            } else if (detail.length() + one.length() + 1 <= 180) {
                detail.append(";").append(one);
            } else {
                detail.append(";等").append(hits.size() - i).append("项超限");
                break;
            }
        }
        return buildResult(rule, report, detail.toString(),
            "牵引车/挂车/整车车辆差≤5台;挂车吨位/整车吨位差≤50吨;总吨位差≤50吨",
            "超出项数=" + hits.size() + ",最大差=" + fmt(maxAbs));
    }
    /** ä¸ŠæŠ¥å‘¨è½¬é‡ä¸Žè½¨è¿¹æµ‹ç®—周转量对比,高出阈值比例核实 */
@@ -1804,6 +2023,10 @@
        labels.put("tonsWhole", "整车吨位");
        labels.put("turnoverTotal", "周转量");
        labels.put("vehicleWhole", "整车车辆数");
        labels.put("vehicleTractor", "牵引车数");
        labels.put("vehicleTrailer", "挂车数");
        labels.put("tonsTrailer", "挂车吨位");
        labels.put("tonsWhole", "整车吨位");
        String v = labels.get(field);
        return v == null ? field : v;
    }
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/calc/SummaryWorkbookFiller.java
New file
@@ -0,0 +1,256 @@
package com.trafficaudit.reportexport.calc;
import com.trafficaudit.common.util.RegionUtil;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
 * æ±‡æ€»æ•´æœ¬â€œæ•°æ®é©±åŠ¨å›žå¡«â€å¼•æ“Žï¼ˆM2 å°è´¦å±‚,2026-09-09 æŽ¥å…¥ï¼‰ã€‚
 *
 * ç›®æ ‡ï¼šç”Ÿæˆä»»æ„æœˆä»½ã€Šé“路运输量汇总表(整本)》时不再依赖当月人工母版;
 * æ¯ç‰ˆåªå½“版式底稿,当月值按已验证口径从库现算写入(班线包车 2026-07 ä¸Žäººå·¥æ¯ç‰ˆ
 * 350 æ ¼ 0 å·®å¼‚、公交/出租/轨道轮渡同批 0 å·®å¼‚)。
 *
 * æœ¬ç‰ˆè¦†ç›– 4 å¼ å°è´¦é¡µï¼šç­çº¿åŒ…车、公交、出租、轨道轮渡;
 * ç½‘约车依赖《网约车订单及全省总量》拆分输入、货运页在 M3 æŽ¥å…¥ï¼ŒåŽç»­ç‰ˆæœ¬è¡¥å……。
 *
 * å†™å…¥è§„则:
 * 1) åªå†™â€œå¸‚州明细行”的 2026 å¹´ m æœˆå½“月值列(m æœˆå€¼åˆ— = ç¬¬ 2+2*(m-1) åˆ—,0 åŸºï¼Œå³ C èµ·ï¼‰ï¼›
 * 2) å…¨çœè¡Œï¼ˆ=17 å¸‚州求和公式)与跨页合成页均为公式,不动,Excel æ‰“开自动重算;
 * 3) 2025 åŒæœŸå‚照列保留母版缓存值(同比公式自动引用);
 * 4) æºæ•°æ®æ•´æœˆç¼ºå¤±æ—¶æ”¶é›†æç¤ºã€ä¸å›žå¡«ï¼›å¸‚州当月全为 0 æ—¶ä¿æŒæ¯ç‰ˆç©ºå•元格。
 */
@Service
public class SummaryWorkbookFiller {
    @Resource
    private MidCalc midCalc;
    @Resource
    private PaxCalc paxCalc;
    /** 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) {
        if (wb == null || keepMonths <= 0) return 0;
        if (problems == null) problems = new ArrayList<>();
        String yearPrefix = year + "-";
        int filled = 0;
        filled += fillCityRows(wb, "班线包车", midCalc.banxianMonthly(yearPrefix, keepMonths), year, keepMonths, problems, false);
        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);
        return filled;
    }
    /** é€šç”¨å°è´¦é¡µï¼ˆç­çº¿åŒ…车/公交/出租)回填:按“列 A å¸‚州名 + åˆ— B æŒ‡æ ‡æ–‡æ¡ˆâ€å®šä½è¡Œï¼Œä¸å†™æ­»è¡Œå· */
    private int fillCityRows(XSSFWorkbook wb, String sheetName,
                             Map<Integer, Map<String, double[]>> monthly,
                             int year, int keepMonths, List<String> problems, boolean wyc) {
        Sheet sh = wb.getSheet(sheetName);
        if (sh == null) {
            problems.add(sheetName + "页不存在(母版缺失该页),跳过自动回填");
            return 0;
        }
        int cap = monthColumnCapacity(sh);
        int mMax = Math.min(keepMonths, cap);
        if (mMax <= 0) {
            problems.add(sheetName + "页未识别到 2026 å¹´ç´¯è®¡åˆ—,跳过自动回填");
            return 0;
        }
        // 1) å¸‚州起始行(列 A ä¸ºè§„范市州名)
        Map<String, Integer> cityStart = new LinkedHashMap<>();
        for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String city = cityOf(row.getCell(0));
            if (city != null && !cityStart.containsKey(city)) cityStart.put(city, r);
        }
        if (cityStart.isEmpty()) {
            problems.add(sheetName + "页未识别到市州行,跳过自动回填");
            return 0;
        }
        List<Integer> starts = new ArrayList<>(cityStart.values());
        int filled = 0;
        List<String> missingMonths = new ArrayList<>();
        for (int m = 1; m <= mMax; m++) {
            if (!monthly.containsKey(m)) missingMonths.add(m + "月");
        }
        if (!missingMonths.isEmpty()) {
            problems.add(sheetName + "缺 " + year + " å¹´ " + String.join("、", missingMonths) + " æºæ•°æ®ï¼ˆæœªå›žå¡«ï¼‰");
        }
        for (int i = 0; i < starts.size(); i++) {
            String city = findCityByStart(sh, starts.get(i));
            if (city == null) continue;
            int end = (i + 1 < starts.size()) ? starts.get(i + 1) - 1 : sh.getLastRowNum();
            // 2) è¡Œå†…各指标所在行(B åˆ—文案 â†’ æŒ‡æ ‡ä½ 0..3)
            int[] metricRow = new int[]{-1, -1, -1, -1};
            for (int r = starts.get(i); r <= end; r++) {
                Row row = sh.getRow(r);
                if (row == null) continue;
                int idx = metricIndexOf(text(row.getCell(1)));
                if (idx >= 0 && metricRow[idx] < 0) metricRow[idx] = r;
            }
            if (metricRow[0] < 0 && metricRow[1] < 0) continue;
            for (int m = 1; m <= mMax; m++) {
                Map<String, double[]> mm = monthly.get(m);
                if (mm == null) continue;
                double[] arr = mm.get(city);
                if (arr == null || (arr[0] == 0.0 && arr[1] == 0.0)) continue;
                int col = FIRST_MONTH_COL + 2 * (m - 1);
                for (int idx = 0; idx < 4; idx++) {
                    if (metricRow[idx] < 0) continue;
                    double val = arr[idx];
                    if (val == 0.0) continue;
                    filled += setNumeric(sh, metricRow[idx], col, val);
                }
            }
        }
        return filled;
    }
    /** è½¨é“轮渡页(轨道=武汉/黄石;轮渡仅武汉)专用回填 */
    private int fillTrackFerry(XSSFWorkbook wb, Map<Integer, Map<String, double[]>> monthly,
                               int year, int keepMonths, List<String> problems) {
        String sheetName = "轨道、轮渡";
        Sheet sh = wb.getSheet(sheetName);
        if (sh == null) return 0;
        int cap = monthColumnCapacity(sh);
        int mMax = Math.min(keepMonths, cap);
        if (mMax <= 0) return 0;
        // è½¨é“块与轮渡块标题行(列 A æ–‡æ¡ˆï¼‰
        int trackTitle = -1, ferryTitle = -1;
        for (int r = sh.getFirstRowNum(); r <= sh.getLastRowNum(); r++) {
            String a = text(sh.getRow(r) == null ? null : sh.getRow(r).getCell(0));
            if (a == null) continue;
            if (trackTitle < 0 && a.startsWith("轨道客运量")) trackTitle = r;
            else if (ferryTitle < 0 && a.startsWith("轮渡客运量")) ferryTitle = r;
        }
        if (trackTitle < 0 || ferryTitle < 0) {
            problems.add(sheetName + "页未识别到轨道/轮渡块标题,跳过自动回填");
            return 0;
        }
        int filled = 0;
        filled += fillFerryBlock(sh, monthly, trackTitle + 3, ferryTitle - 1, 0, mMax, problems, sheetName);
        filled += fillFerryBlock(sh, monthly, ferryTitle + 3, sh.getLastRowNum(), 2, mMax, problems, sheetName);
        return filled;
    }
    /** å¡«å……一段“标题行后数据区”:base ä¸ºè¯¥å—指标位基(轨道 0 / è½®æ¸¡ 2);block å†…
     *  æ¯å¸‚å·ž 2 è¡Œï¼šå®¢è¿é‡è¡Œï¼ˆåˆ— A=市州名)、旅客周转量行(A ç©ºï¼ŒB å«â€œå‘¨è½¬é‡â€ï¼‰ */
    private int fillFerryBlock(Sheet sh, Map<Integer, Map<String, double[]>> monthly,
                               int start, int end, int base, int mMax,
                               List<String> problems, String sheetName) {
        int filled = 0;
        // æ”¶é›†å¸‚州客运量行
        Map<String, Integer> paxRow = new LinkedHashMap<>();
        for (int r = start; r <= end; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String city = cityOf(row.getCell(0));
            if (city != null) paxRow.put(city, r);
        }
        for (Map.Entry<String, Integer> e : paxRow.entrySet()) {
            String city = e.getKey();
            int rp = e.getValue();
            int rt = findTurnoverRow(sh, rp + 1, Math.min(end, rp + 6));
            if (rt < 0) continue;
            for (int m = 1; m <= mMax; m++) {
                Map<String, double[]> mm = monthly.get(m);
                if (mm == null) continue;
                double[] arr = mm.get(city);
                if (arr == null) continue;
                int col = FIRST_MONTH_COL + 2 * (m - 1);
                double pax = arr[base];
                double turn = arr[base + 1];
                if (pax != 0.0) filled += setNumeric(sh, rp, col, pax);
                if (turn != 0.0) filled += setNumeric(sh, rt, col, turn);
            }
        }
        return filled;
    }
    private int findTurnoverRow(Sheet sh, int from, int to) {
        for (int r = from; r <= to; r++) {
            Row row = sh.getRow(r);
            if (row == null) continue;
            String a = text(row.getCell(0));
            if (a != null && !a.trim().isEmpty()) return -1; // è¿›å…¥ä¸‹ä¸€å¸‚州块
            String b = text(row.getCell(1));
            if (b != null && b.contains("周转量")) return r;
        }
        return -1;
    }
    /** å†™æ•°å€¼ï¼šæ ¼ä¸å­˜åœ¨åˆ™å»ºæ ¼å¹¶å¤åˆ¶åŒè¡Œ 1 æœˆåˆ—样式;返回 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 = row.createCell(col);
            Cell ref = row.getCell(FIRST_MONTH_COL);
            if (ref != null && ref.getCellStyle() != null) c.setCellStyle(ref.getCellStyle());
        }
        c.setCellValue(val);
        return 1;
    }
    /** è¡Œ B åˆ—文案 â†’ æŒ‡æ ‡ä½ï¼š0 å®¢è¿é‡ / 1 æ—…客周转量 / 2 å…¶ä¸­åŸŽå¸‚内客运量或其中个体客运量 / 3 å¯¹åº”周转量 */
    private int metricIndexOf(String b) {
        if (b == null) return -1;
        if (b.contains("其中个体旅客周转量") || b.contains("其中城市内旅客周转量")) return 3;
        if (b.contains("其中个体客运量") || b.contains("其中城市内客运量")) return 2;
        if (b.contains("旅客周转量")) return 1;
        if (b.contains("客运量")) return 0;
        return -1;
    }
    /** åˆ— A æ–‡æ¡ˆ â†’ è§„范市州名(湖北省/全省、非市州行返回 null) */
    private String cityOf(Cell a) {
        String t = text(a);
        if (t == null || t.trim().isEmpty()) return null;
        String norm = RegionUtil.normalizeCityName(t.trim());
        return RegionUtil.cityList().contains(norm) ? norm : null;
    }
    private String findCityByStart(Sheet sh, int startRow) {
        Row row = sh.getRow(startRow);
        return row == null ? null : cityOf(row.getCell(0));
    }
    /** æœˆä»½å®¹é‡ï¼šåœ¨è¡¨å¤´ï¼ˆå‰ 5 è¡Œï¼‰æ‰¾â€œ2026…累计”列,容量=(累计列-1 æœˆåˆ—)/2 */
    private int monthColumnCapacity(Sheet sh) {
        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;
                String t = text(c);
                if (t != null && t.contains("2026") && t.contains("累计") && c.getColumnIndex() > FIRST_MONTH_COL) {
                    return (c.getColumnIndex() - FIRST_MONTH_COL) / 2;
                }
            }
        }
        return 7;
    }
    private String text(Cell c) {
        if (c == null) return null;
        switch (c.getCellType()) {
            case STRING: return c.getStringCellValue();
            case NUMERIC: return Double.toString(c.getNumericCellValue());
            default: return null;
        }
    }
}
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java
@@ -29,6 +29,9 @@
import com.trafficaudit.dataimport.mapper.PassengerEnterpriseMonthlyMapper;
import com.trafficaudit.dataimport.mapper.PassengerIndividualMonthlyMapper;
import com.trafficaudit.dataimport.mapper.ScaleSplitTransportMapper;
import com.trafficaudit.reportexport.calc.MidCalc;
import com.trafficaudit.reportexport.calc.PaxCalc;
import com.trafficaudit.reportexport.calc.SummaryWorkbookFiller;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
@@ -92,6 +95,8 @@
    private CityBusMonthlyMapper cityBusMapper;
    @Resource
    private CityTaxiMonthlyMapper cityTaxiMapper;
    @Resource
    private SummaryWorkbookFiller summaryWorkbookFiller;
    @Resource
    private AuditResultMapper auditResultMapper;
    @Resource
@@ -334,12 +339,7 @@
                    if (resolveSummaryMother(period, mode) == null) {
                        problems.add("docs/生成汇总大表 ä¸‹ç¼ºå°‘可复制的母版汇总表");
                    } else {
                        Map<Integer, Map<String, double[]>> busM = loadCityBusByMonth(yearPrefix, month);
                        Map<Integer, Map<String, double[]>> taxiM = loadCityTaxiByMonth(yearPrefix, month);
                        Map<String, double[]> bc = busM.get(month);
                        Map<String, double[]> tc = taxiM.get(month);
                        if (bc != null && bc.get("全省") != null) addSum(summary, "城市公交客运量(全省当月)", bc.get("全省")[0], "万人");
                        if (tc != null && tc.get("全省") != null) addSum(summary, "巡游出租客运量(全省当月)", tc.get("全省")[0], "万人");
                        summaryWorkbookPreview(period, yearNum, month, summary, problems);
                    }
                } catch (Exception ex) {
                    problems.add(ex.getMessage());
@@ -441,6 +441,118 @@
            sb.append(m).append("月");
        }
        return sb.toString();
    }
    /** æ±‡æ€»æ•´æœ¬å…³é”®æŒ‡æ ‡é¢„览:货运量/周转量、城市客运、中口径的 å½“月、1..M ç´¯è®¡ã€å½“月同比、累计同比(同比取库内去年同期,缺则显示—) */
    private void summaryWorkbookPreview(String period, int year, int month,
                                        List<Map<String, Object>> summary, List<String> problems) {
        String yearPrefix = year + "-";
        // ---- è´§è¿ï¼ˆé‡=模板_货运量周转量全省行;周转量=规上规下拆分表全省行) ----
        FreightTurnoverImport ft = loadFreightTurnover(period).get("湖北省");
        Double fm = ft == null ? null : freightMonth(ft, month);
        Double fcum = ft == null ? null : freightCum(ft, month);
        Double fy = ft == null ? null : freightYoy(ft, month);
        Double fcy = ft == null ? null : freightCumYoy(ft, month);
        addKpi(summary, "全省货运量", fm, fcum, fy, fcy, "万吨");
        if (fm != null && fy == null) {
            problems.add((year - 1) + "-" + String.format("%02d", month) + " åŒæœŸè´§è¿é‡æ•°æ®æœªå¯¼å…¥åº“,货运量当月/累计同比暂不显示,待 2025 å¹´å®šç¨¿æ•°æ®å¯¼å…¥åŽè‡ªåŠ¨å‡ºçŽ°");
        }
        ScaleSplitTransport tm = loadProvinceMonthMap(period).get(month);
        ScaleSplitTransport tc = getProvinceCumulative(period);
        Double tvM = tm == null ? null : tm.getTotalTurnover();
        Double tvC = tc == null ? null : tc.getTotalTurnover();
        Double tvMY = tm == null ? null : getYoyMetric(tm, "turnover", "total");
        Double tvCY = tc == null ? null : getYoyMetric(tc, "turnover", "total");
        addKpi(summary, "全省货物周转量", tvM, tvC, tvMY, tvCY, "万吨公里");
        if (fm == null) problems.add("模板_货运量周转量缺 " + period + " å½“月数据,货运量仅能提供截至最近导入月的累计");
        if (tvM == null) problems.add("规上规下拆分缺 " + period + " å½“月数据,周转量/规上规下当月缺,累计为截至最近导入月");
        // ---- åŸŽå¸‚客运(公交+出租+轨道+轮渡 å…¨çœå°è®¡ï¼›æŒ‡æ ‡ä½ 0/2/4/6=客运,1/3/5/7=周转) ----
        double[] cumM = cityCumProv(yearPrefix, month);
        double[] cumPrev = cityCumProv(yearPrefix, month - 1);
        double[] monthVec = diff8(cumM, cumPrev);
        double cityPaxM = sumIdx(monthVec, new int[]{0, 2, 4, 6});
        double cityTurnM = sumIdx(monthVec, new int[]{1, 3, 5, 7});
        double cityPaxC = sumIdx(cumM, new int[]{0, 2, 4, 6});
        double cityTurnC = sumIdx(cumM, new int[]{1, 3, 5, 7});
        double[] lastCumM = cityCumProv((year - 1) + "-", month);
        double[] lastCumPrev = cityCumProv((year - 1) + "-", month - 1);
        double[] lastVec = diff8(lastCumM, lastCumPrev);
        double lastPaxM = sumIdx(lastVec, new int[]{0, 2, 4, 6});
        double lastPaxC = sumIdx(lastCumM, new int[]{0, 2, 4, 6});
        double lastTurnM = sumIdx(lastVec, new int[]{1, 3, 5, 7});
        double lastTurnC = sumIdx(lastCumM, new int[]{1, 3, 5, 7});
        addKpiRaw(summary, "城市客运量", cityPaxM, cityPaxC, lastPaxM, lastPaxC, "万人次");
        addKpiRaw(summary, "城市客运周转量", cityTurnM, cityTurnC, lastTurnM, lastTurnC, "万人公里");
        // ---- ä¸­å£å¾„(公路旅客企业 H203-1 å…¨çœï¼Œå•位换算 /10000) ----
        Map<Integer, Map<String, PassengerAgg>> data = loadPassengerAggMap();
        double paxM = 0.0, paxC = 0.0, turnM = 0.0, turnC = 0.0;
        double lastPaxM2 = 0.0, lastPaxC2 = 0.0, lastTurnM2 = 0.0, lastTurnC2 = 0.0;
        for (int m = 1; m <= month; m++) {
            PassengerAgg agg = aggOf(data, year, m, "湖北省");
            if (agg != null) {
                double pax = agg.passengerTotal / 10000.0;
                double turn = agg.turnoverTotal / 10000.0;
                if (m == month) { paxM = pax; turnM = turn; }
                paxC += pax;
                turnC += turn;
            }
            PassengerAgg last = aggOf(data, year - 1, m, "湖北省");
            if (last != null) {
                double lp = last.passengerTotal / 10000.0;
                double lt = last.turnoverTotal / 10000.0;
                if (m == month) { lastPaxM2 = lp; lastTurnM2 = lt; }
                lastPaxC2 += lp;
                lastTurnC2 += lt;
            }
        }
        addKpiRaw(summary, "中口径客运量", paxM, paxC, lastPaxM2, lastPaxC2, "万人次");
        addKpiRaw(summary, "中口径周转量", turnM, turnC, lastTurnM2, lastTurnC2, "万人公里");
        if (paxM <= 0 && paxC <= 0 && cityPaxC <= 0 && fm == null) {
            problems.add("货运量周转量/规上规下/公路旅客/城市客运当月数据均缺失");
        }
        if (cityPaxC > 0 && lastPaxC <= 0) {
            problems.add((year - 1) + "-" + String.format("%02d", month) + " åŒæœŸåŸŽå¸‚客运数据未导入库,城市客运同比暂不显示,待 2025 å¹´å®šç¨¿æ•°æ®å¯¼å…¥åŽè‡ªåŠ¨å‡ºçŽ°");
        }
        if (paxC > 0 && lastPaxC2 <= 0) {
            problems.add((year - 1) + "-" + String.format("%02d", month) + " åŒæœŸå…¬è·¯æ—…客数据未导入库,中口径同比暂不显示,待 2025 å¹´å®šç¨¿æ•°æ®å¯¼å…¥åŽè‡ªåŠ¨å‡ºçŽ°");
        }
    }
    /** åŸŽå¸‚客运全省累计向量(8 æŒ‡æ ‡ï¼šå…¬äº¤å®¢è¿/周转、出租客运/周转、轨道客运/周转、轮渡客运/周转) */
    private double[] cityCumProv(String yearPrefix, int month) {
        if (month <= 0) return new double[8];
        return loadCityPassengerCumulative(yearPrefix, month).getOrDefault("全省", new double[8]);
    }
    private double[] diff8(double[] a, double[] b) {
        double[] out = new double[8];
        for (int i = 0; i < 8; i++) out[i] = a[i] - b[i];
        return out;
    }
    private double sumIdx(double[] arr, int[] idx) {
        double sum = 0.0;
        for (int i : idx) sum += (i < arr.length ? arr[i] : 0.0);
        return sum;
    }
    /** ç™¾åˆ†æ¯”同比(值已为比率 0.xx):输出四行 å½“月/累计/当月同比/累计同比 */
    private void addKpi(List<Map<String, Object>> summary, String name, Double m, Double cum, Double yoyM, Double yoyC, String unit) {
        if (m != null) addSum(summary, name + "·当月", round(m, 2), unit);
        if (cum != null) addSum(summary, name + "·累计", round(cum, 2), unit);
        if (yoyM != null) addSum(summary, name + "·当月同比", round(yoyM * 100.0, 1), "%");
        if (yoyC != null) addSum(summary, name + "·累计同比", round(yoyC * 100.0, 1), "%");
    }
    /** åŽŸå§‹æ•°å€¼ç‰ˆï¼ˆæœˆ/累计为 0 è§†ä¸ºæ— æ•°æ®ä¸å‡ºè¡Œï¼‰ */
    private void addKpiRaw(List<Map<String, Object>> summary, String name, double m, double cum, double lastM, double lastC, String unit) {
        if (m > 0) addSum(summary, name + "·当月", round(m, 2), unit);
        if (cum > 0) addSum(summary, name + "·累计", round(cum, 2), unit);
        if (m > 0 && lastM > 0) addSum(summary, name + "·当月同比", round((m - lastM) / lastM * 100.0, 1), "%");
        if (cum > 0 && lastC > 0) addSum(summary, name + "·累计同比", round((cum - lastC) / lastC * 100.0, 1), "%");
    }
    private void addSum(List<Map<String, Object>> summary, String label, Double value, String unit) {
@@ -3233,8 +3345,11 @@
            }
            dynamicSummaryYear(wb, year);
            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);
            applyTwoDecimalFormat(wb); // æ•°å€¼æ˜¾ç¤ºä¸¤ä½å°æ•°ï¼ˆä¿ç•™å…¨ç²¾åº¦ï¼‰ï¼›% / æ—¥æœŸç­‰æ—¢æœ‰æ ·å¼ä¸æ”¹å˜
            autoFitContentColumns(wb); // æŒ‰æ•°æ®è‡ªåŠ¨åŠ å®½åˆ—å®½ï¼ˆåªåŠ å®½ä¸ç¼©çª„ï¼Œè·³è¿‡åˆå¹¶æ ‡é¢˜æ ¼ï¼Œé˜²é•¿æ–‡æœ¬æŠŠå•åˆ—æ’‘çˆ†ï¼‰
            if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr();
            wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // è¿˜åŽŸ/改写公式后打开即重算
            return toBytes(wb);
@@ -4822,59 +4937,6 @@
        }
    }
    private void autoFitContentColumns(org.apache.poi.ss.usermodel.Workbook wb) {
        if (wb == null) return;
        org.apache.poi.ss.usermodel.DataFormatter dfmt = new org.apache.poi.ss.usermodel.DataFormatter();
        for (int s = 0; s < wb.getNumberOfSheets(); s++) {
            org.apache.poi.ss.usermodel.Sheet sh = wb.getSheetAt(s);
            if (sh == null) continue;
            java.util.Set<String> merged = new java.util.HashSet<>();
            for (org.apache.poi.ss.util.CellRangeAddress ra : sh.getMergedRegions()) {
                for (int r = ra.getFirstRow(); r <= ra.getLastRow(); r++) {
                    for (int c = ra.getFirstColumn(); c <= ra.getLastColumn(); c++) {
                        merged.add(r + ":" + c);
                    }
                }
            }
            int maxCol = -1;
            for (org.apache.poi.ss.usermodel.Row row : sh) {
                if (row == null) continue;
                maxCol = Math.max(maxCol, (int) row.getLastCellNum() - 1);
            }
            if (maxCol < 0) continue;
            int[] need = new int[maxCol + 1];
            for (org.apache.poi.ss.usermodel.Row row : sh) {
                if (row == null) continue;
                for (org.apache.poi.ss.usermodel.Cell cell : row) {
                    int c = cell.getColumnIndex();
                    if (c > maxCol || merged.contains(row.getRowNum() + ":" + c)) continue;
                    String txt;
                    try {
                        txt = dfmt.formatCellValue(cell);
                    } catch (Exception ignore) {
                        continue;
                    }
                    if (txt == null || txt.isEmpty()) continue;
                    int units = 0;
                    for (int i = 0; i < txt.length(); i++) {
                        units += isWideChar(txt.charAt(i)) ? 2 : 1;
                    }
                    if (units > need[c]) need[c] = Math.min(units, 34); // é•¿æ–‡æœ¬ä¸æŠŠå•列撑爆
                }
            }
            for (int c = 0; c <= maxCol; c++) {
                if (need[c] <= 0) continue;
                int target = Math.min(need[c] + 2, 46) * 256;
                if (target > sh.getColumnWidth(c)) sh.setColumnWidth(c, target);
            }
        }
    }
    /** CJK / å…¨è§’字符按两倍宽度计(近似 Excel æ˜¾ç¤ºåˆ—宽) */
    private boolean isWideChar(char ch) {
        return (ch >= 0x2E80 && ch <= 0x9FFF) || (ch >= 0xF900 && ch <= 0xFAFF)
            || (ch >= 0xFF00 && ch <= 0xFFEF);
    }
    private byte[] toBytes(XSSFWorkbook wb) throws Exception {
        applyTwoDecimalFormat(wb);
traffic-audit-server/src/main/java/com/trafficaudit/rulemanage/controller/RuleManageController.java
@@ -64,19 +64,22 @@
        return Result.ok("已保存");
    }
    /** % å‰çš„æ•°å€¼ï¼ˆåç§°/说明中随阈值动态变化的片段;% å‰å¯èƒ½æ˜¯ Â±ã€>、超过、高出 ç­‰æè¿°è¯ï¼Œä»…替换数字本身) */
    private static final Pattern PCT_NUM = Pattern.compile("\\d+(?:\\.\\d+)?(?=%)");
    /** åç§°/说明中随阈值动态变化的数字片段:匹配任意数字(% å‰åŽã€å°/吨/公里等单位前均可) */
    private static final Pattern NUM_TOKEN = Pattern.compile("\\d+(?:\\.\\d+)?");
    /** è§£æžé˜ˆå€¼å­—符串中第一个数值;无法解析返回 null */
    private Double parseThresholdNum(String threshold) {
        if (threshold == null || threshold.trim().isEmpty()) return null;
        java.util.regex.Matcher m = Pattern.compile("\\d+(?:\\.\\d+)?").matcher(threshold.trim());
        if (!m.find()) return null;
        try {
            return Double.parseDouble(m.group());
        } catch (Exception e) {
            return null;
    /** è§£æžé˜ˆå€¼å­—符串中的所有数值(支持“5,5,50”等多段阈值,如 RULE_001 åˆ†é¡¹è¿æ”¿å¯¹æ¯”) */
    private java.util.List<Double> parseThresholdNums(String threshold) {
        java.util.List<Double> out = new java.util.ArrayList<>();
        if (threshold == null || threshold.trim().isEmpty()) return out;
        java.util.regex.Matcher m = NUM_TOKEN.matcher(threshold.trim());
        while (m.find()) {
            try {
                out.add(Double.parseDouble(m.group()));
            } catch (Exception e) {
                // å¿½ç•¥æ— æ³•解析的段
            }
        }
        return out;
    }
    /** æ•°å€¼å±•示:整数去掉小数位,其余保留原精度 */
@@ -86,10 +89,11 @@
        return new java.math.BigDecimal(String.valueOf(v)).stripTrailingZeros().toPlainString();
    }
    /** ç”±é˜ˆå€¼æ•°å€¼æŽ¨å¯¼â€œè¯´æ˜Žæ–‡å­—里写的是百分之几”的候选口径:
    /** ç”±é˜ˆå€¼æ•°å€¼æŽ¨å¯¼â€œåç§°/说明里写的是百分之几”的候选口径:
     *  â‘  é˜ˆå€¼æœ¬èº«å°±æ˜¯ç™¾åˆ†æ•°ï¼ˆå¦‚ 20 â†’ Â±20%);
     *  â‘¡ é˜ˆå€¼æ˜¯å°æ•°æ¯”例(如 0.02 â†’ 2%、0.5 â†’ 50%);此时 <1 æ‰å¯ç”¨ï¼›
     *  â‘¢ é˜ˆå€¼æ˜¯æ¯”值(如 1.3 â†’ é«˜å‡º30%);此时 >1 æ‰å¯ç”¨ã€‚ */
     *  â‘¢ é˜ˆå€¼æ˜¯æ¯”值(如 1.3 â†’ é«˜å‡º30%);此时 >1 æ‰å¯ç”¨ï¼›
     *  å…¶ä½™æŒ‰åŽŸæ•°åŒ¹é…ï¼ˆå°/吨/公里等阈值原样出现在文字中)。 */
    private double[] thresholdPercentCandidates(double v) {
        double[] cands;
        if (v > 0 && v < 1) {
@@ -102,16 +106,23 @@
        return cands;
    }
    /** æŒ‰æ—§é˜ˆå€¼åŒ¹é…æ–‡æœ¬ä¸­å·²æœ‰çš„百分比片段,仅替换“与旧阈值对得上”的数字为新阈值对应数字;对不上的数字(如说明里与阈值无关的数值)保持不动 */
    /** æŒ‰â€œæ—§é˜ˆå€¼â†’新阈值”逐段改写名称/说明中的数字:
     *  åªæ›¿æ¢ä¸Žæ—§é˜ˆå€¼æŸä¸€æ®µï¼ˆæˆ–其百分比口径)对得上的数字;多段阈值(逗号分隔)逐段对应;
     *  ä¸Žé˜ˆå€¼æ— å…³çš„æ•°å­—(日期、与规则无关的固定数值等)保持不动。 */
    private String decorateThresholdText(String text, String oldThreshold, String newThreshold) {
        if (text == null || text.trim().isEmpty()) return text;
        Double oldNum = parseThresholdNum(oldThreshold);
        Double newNum = parseThresholdNum(newThreshold);
        if (oldNum == null || newNum == null) return text;
        double[] oldCands = thresholdPercentCandidates(oldNum);
        double[] newCands = thresholdPercentCandidates(newNum);
        if (oldCands.length != newCands.length) return text;
        java.util.regex.Matcher m = PCT_NUM.matcher(text);
        java.util.List<Double> olds = parseThresholdNums(oldThreshold);
        java.util.List<Double> news = parseThresholdNums(newThreshold);
        if (olds.isEmpty() || olds.size() != news.size()) return text;
        int n = olds.size();
        double[][] oc = new double[n][];
        double[][] nc = new double[n][];
        for (int i = 0; i < n; i++) {
            oc[i] = thresholdPercentCandidates(olds.get(i));
            nc[i] = thresholdPercentCandidates(news.get(i));
            if (oc[i].length != nc[i].length) return text;
        }
        java.util.regex.Matcher m = NUM_TOKEN.matcher(text);
        StringBuffer sb = new StringBuffer();
        while (m.find()) {
            double token;
@@ -121,10 +132,13 @@
                continue;
            }
            String repl = null;
            for (int i = 0; i < oldCands.length; i++) {
                if (Math.abs(oldCands[i] - token) < 1e-9) {
                    repl = fmtNum(newCands[i]);
                    break;
            outer:
            for (int i = 0; i < n; i++) {
                for (int j = 0; j < oc[i].length; j++) {
                    if (Math.abs(oc[i][j] - token) < 1e-9) {
                        repl = fmtNum(nc[i][j]);
                        break outer;
                    }
                }
            }
            if (repl == null) continue;
@@ -134,7 +148,7 @@
        return sb.toString();
    }
    /** åç§°/说明随阈值同步(update æ—¶æŒ‰ æ—§â†’æ–° æ”¹å†™å¹¶æŒä¹…化) */
    /** åç§°/说明随阈值同步(update æ—¶æŒ‰ æ—§â†’æ–° æ”¹å†™å¹¶æŒä¹…化;多段阈值同样逐段联动) */
    private void syncThresholdText(AuditRule rule, String oldThreshold) {
        if (rule == null || rule.getThreshold() == null) return;
        rule.setRuleName(decorateThresholdText(rule.getRuleName(), oldThreshold, rule.getThreshold()));
@@ -145,4 +159,4 @@
    private void syncThresholdText(AuditRule rule) {
        syncThresholdText(rule, rule == null ? null : rule.getThreshold());
    }
}
}
traffic-audit-web/src/views/AuditResult.vue
@@ -50,13 +50,29 @@
          <div class="sev-card sev-pending"><b>{{ severityStats.pending }}</b> å¾…处理</div>
          <div class="sev-card sev-done"><b>{{ severityStats.done }}</b> å·²å¤„理</div>
        </div>
        <el-form v-if="!focusMode" :inline="true" style="margin-top:8px;flex:none">
        <div v-if="!focusMode" class="audit-filter-bar">
        <el-form :inline="true">
          <el-form-item label="筛选"></el-form-item>
          <el-form-item label="企业名称">
            <el-input v-model="enterpriseFilter" placeholder="输入企业名称关键字" clearable style="width:200px" @keyup.enter.native="queryResults"></el-input>
          </el-form-item>
          <el-form-item label="审核规则">
            <el-select v-model="ruleFilter" placeholder="全部规则" clearable style="width:180px">
              <el-option v-for="rn in ruleOptions" :key="rn" :label="rn" :value="rn"></el-option>
            </el-select>
          </el-form-item>
          <el-form-item label="问题范围">
            <el-radio-group v-model="issueScope" size="small" @change="updateBatchSelection">
              <el-radio-button label="verify">仅需核实</el-radio-button>
              <el-radio-button label="all">含提醒型</el-radio-button>
            </el-radio-group>
            <span class="hint">提醒型仅提示变化、不需处理,导出默认不含</span>
          </el-form-item>
          <el-form-item label="状态">
            <el-select v-model="statusScope" style="width:110px" @change="updateBatchSelection">
              <el-option label="全部" value="all"></el-option>
              <el-option label="待处理" value="pending"></el-option>
              <el-option label="已处理" value="handled"></el-option>
            </el-select>
          </el-form-item>
          <el-form-item label="视图">
@@ -74,9 +90,11 @@
            <el-button size="small" plain @click="statsCollapsed = !statsCollapsed">{{ statsCollapsed ? '展开' : '收起' }}规则统计</el-button>
          </el-form-item>
          <el-form-item>
            <span style="color:#909399;font-size:12px">共 {{ results.length }} æ¡é—®é¢˜ï¼Œ{{ mergedResults.length }} å®¶ä¼ä¸š</span>
            <span style="color:#303133;font-size:13px;font-weight:600">当前筛选 {{ filteredResults.length }} æ¡ / {{ mergedResults.length }} å®¶ä¼ä¸š</span>
            <span style="color:#909399;font-size:12px;margin-left:8px">(共 {{ results.length }} æ¡é—®é¢˜ï¼‰</span>
          </el-form-item>
        </el-form>
        </div>
        <div v-if="ruleStats.length && !statsCollapsed && !focusMode" class="rule-stats" style="flex:none">
          <div class="stats-cards">
            <div v-for="rs in sortedRuleStats" :key="rs.ruleId" class="stat-card"
@@ -98,45 +116,52 @@
          å…± {{ results.length }} æ¡é—®é¢˜ï¼Œ{{ mergedResults.length }} å®¶ä¼ä¸š
        </div>
        <div class="table-area" style="flex:1;min-height:0">
        <!-- æ˜Žç»†è§†å›¾ï¼šä¸€æ¡é—®é¢˜ä¸€å¼ å¡ç‰‡ -->
        <!-- æ˜Žç»†è§†å›¾ï¼šä¸€æ¡é—®é¢˜ä¸€è¡Œï¼ˆä¸Žå¯¼å‡ºæ¨¡æ¿ Sheet“审核结果”同款) -->
        <div v-if="viewMode === 'detail'" class="detail-cards-wrap" v-loading="loading">
          <div class="detail-toolbar">
            <el-checkbox :value="allSelected" :indeterminate="someSelected" @change="toggleSelectAll">全选当前 {{ filteredResults.length }} æ¡</el-checkbox>
            <span class="toolbar-tip">勾选后可批量确认 / å¿½ç•¥</span>
          </div>
          <div v-for="row in filteredResults" :key="row.id" class="detail-card"
               :class="{ 'card-red': row.alertLevel === 'RED', 'card-yellow': row.alertLevel === 'YELLOW', 'card-done': row.status !== 'PENDING' }">
            <div class="card-head">
              <el-checkbox v-model="row._selected" @change="updateBatchSelection"></el-checkbox>
              <div class="card-ent">
                <div class="ent-name">{{ row.enterpriseName || row.city || '未知企业' }}</div>
                <div class="ent-sub">{{ row.reportPeriod }}</div>
              </div>
              <el-tag :type="row.alertLevel === 'RED' ? 'danger' : 'warning'" size="small" effect="dark">{{ row.alertLevel }}</el-tag>
              <el-tag :type="statusType(row.status)" size="small" effect="plain" class="status-tag">
                <i :class="statusIcon(row.status)"></i> {{ statusText(row.status) }}
              </el-tag>
              <span class="spacer"></span>
              <el-link v-if="dataType(reportType)" type="primary" :underline="false" size="mini" @click="openMonthReport(row)">数据</el-link>
              <el-button v-if="row.status === 'PENDING'" size="mini" type="success" icon="el-icon-check" round @click="review(row, 'CONFIRMED')">确认</el-button>
              <el-button v-if="row.status === 'PENDING'" size="mini" type="info" icon="el-icon-close" round plain @click="review(row, 'IGNORED')">忽略</el-button>
              <span v-else class="done-text">已处理</span>
          <el-table :data="filteredResults" border stripe height="100%"
            :header-cell-style="{ background: '#F5F7FA', color: '#303133', fontWeight: 600 }">
            <el-table-column width="40">
              <template slot-scope="{ row }">
                <el-checkbox v-model="row._selected" @change="updateBatchSelection"></el-checkbox>
              </template>
            </el-table-column>
            <el-table-column prop="reportPeriod" label="报表期" width="90"></el-table-column>
            <el-table-column label="企业/市州" min-width="200" show-overflow-tooltip>
              <template slot-scope="{ row }">{{ row.enterpriseName || row.city || '未知企业' }}</template>
            </el-table-column>
            <el-table-column prop="ruleCode" label="规则编码" width="100"></el-table-column>
            <el-table-column prop="ruleName" label="审核规则" min-width="150" show-overflow-tooltip></el-table-column>
            <el-table-column label="等级" width="76">
              <template slot-scope="{ row }">
                <el-tag :type="row.alertLevel === 'RED' ? 'danger' : 'warning'" size="mini">{{ row.alertLevel }}</el-tag>
              </template>
            </el-table-column>
            <el-table-column prop="actualValue" label="实际值" min-width="230" show-overflow-tooltip></el-table-column>
            <el-table-column prop="thresholdValue" label="阈值/基准" min-width="140" show-overflow-tooltip></el-table-column>
            <el-table-column prop="deviation" label="偏差说明" min-width="170" show-overflow-tooltip></el-table-column>
            <el-table-column label="状态" width="92">
              <template slot-scope="{ row }">
                <el-tag :type="statusType(row.status)" size="mini">
                  <i :class="statusIcon(row.status)"></i> {{ statusText(row.status) }}
                </el-tag>
              </template>
            </el-table-column>
            <el-table-column label="操作" width="185" align="right">
              <template slot-scope="{ row }">
                <el-link v-if="dataType(reportType)" type="primary" :underline="false" size="mini" @click="openMonthReport(row)">数据</el-link>
                <el-button v-if="row.status === 'PENDING'" size="mini" type="success" icon="el-icon-check" round @click="review(row, 'CONFIRMED')">确认</el-button>
                <el-button v-if="row.status === 'PENDING'" size="mini" type="info" icon="el-icon-close" round plain @click="review(row, 'IGNORED')">忽略</el-button>
                <span v-else class="done-text">已处理</span>
              </template>
            </el-table-column>
            <div slot="empty">
              <el-empty description="当前筛选条件下没有问题数据"></el-empty>
            </div>
            <div class="card-body">
              <div class="rule-title">
                <b>{{ row.ruleName }}</b>
                <span class="rule-code">{{ row.ruleCode }}</span>
              </div>
              <div class="card-details">
                <div class="detail-line"><span class="dt-label">实际</span><span class="dt-val">{{ row.actualValue }}</span></div>
                <div class="detail-line"><span class="dt-label">基准</span><span class="dt-val">{{ row.thresholdValue }}</span></div>
                <div class="detail-line"><span class="dt-label">差异</span><span class="dt-val dt-warn">{{ row.deviation }}</span></div>
              </div>
            </div>
          </div>
          <div v-if="!filteredResults.length" class="empty-tip">
            <el-empty description="当前筛选条件下没有问题数据"></el-empty>
          </div>
          </el-table>
        </div>
        <!-- åˆå¹¶è§†å›¾ -->
        <el-table v-else :data="mergedResults" border stripe v-loading="loading" height="100%"
@@ -334,7 +359,7 @@
  data() {
    return {
      period: '', reportType: 'H2032', loading: false, marking: false, results: [], ruleStats: [], statsCollapsed: false,
      enterpriseFilter: '', ruleFilter: '', viewMode: 'merged', batchSelection: [], focusMode: false,
      enterpriseFilter: '', ruleFilter: '', viewMode: 'merged', issueScope: 'verify', statusScope: 'all', batchSelection: [], focusMode: false,
      explainDrafts: {}, savingExplainId: null, expandedExplain: null,
      monthVisible: false, monthLoading: false, monthType: '', monthPeriod: '', monthKeyword: '', monthRecords: [], monthColumns: []
    }
@@ -347,7 +372,7 @@
    },
    severityStats() {
      const s = { red: 0, yellow: 0, pending: 0, done: 0 }
      this.results.forEach(r => {
      this.filteredResults.forEach(r => {
        if (r.alertLevel === 'RED') s.red++
        else if (r.alertLevel === 'YELLOW') s.yellow++
        if (r.status === 'PENDING') s.pending++
@@ -367,10 +392,14 @@
    filteredResults() {
      const kw = (this.enterpriseFilter || '').trim()
      const rule = this.ruleFilter
      if (!kw && !rule) return this.results
      const scope = this.issueScope || 'verify'
      const st = this.statusScope || 'all'
      return this.results.filter(r => {
        if (kw && !(r.enterpriseName || '').includes(kw)) return false
        if (rule && r.ruleName !== rule) return false
        if (scope === 'verify' && this.isReminderRow(r)) return false
        if (st === 'pending' && r.status !== 'PENDING') return false
        if (st === 'handled' && (!r.status || r.status === 'PENDING')) return false
        return true
      })
    },
@@ -397,6 +426,9 @@
    }
  },
  methods: {
    isReminderRow(r) {
      return !!(r && r.ruleCode === 'RULE_004')
    },
    dataType(t) { return DATA_TYPE_MAP[t] || '' },
    explainType(t) { return EXPLAIN_TYPES.indexOf(t) >= 0 },
    openMonthReport(row) {
@@ -577,7 +609,8 @@
    },
    exportExcel() {
      if (!this.period) { this.$message.warning('请选择报表期'); return }
      window.open('/api/audit/export?period=' + this.period + '&reportType=' + this.reportType)
      const q = 'period=' + this.period + '&reportType=' + this.reportType + '&onlyVerify=' + (this.issueScope !== 'all') + '&status=' + (this.statusScope || 'all')
      window.open('/api/audit/export?' + q)
    },
    review(row, status) {
      if (status === 'IGNORED') {
@@ -741,4 +774,9 @@
.mr-sec-tab th { background: #F5F7FA; font-weight: 600; }
.mr-note { margin: 6px 12px 12px; background: #FDF6EC; border: 1px solid #FAECD8; border-radius: 4px; padding: 8px 10px; font-size: 12px; color: #8a6d3b; white-space: pre-wrap; word-break: break-all; }
.mr-note-line { margin: 2px 0; }
.audit-filter-bar { background:#FAFAFA; border:1px solid #EBEEF5; border-radius:8px; padding:2px 12px 0; margin-top:8px; flex:none; }
.audit-filter-bar .el-form-item { margin-bottom:6px; }
.audit-filter-bar .el-form-item__label { color:#606266; font-weight:600; }
.hint { color:#909399; font-size:12px; margin-left:6px; white-space:nowrap; }
</style>
traffic-audit-web/src/views/DataImport.vue
@@ -372,6 +372,7 @@
        message: ''
      }
      this.importOne(row).then(() => {
        this.showImportResult(row)
        if ((row.type === 'investment' || row.type === 'investmentLogistics') && row.status === 'success') {
          this.checkInvest(this.period)
        }
@@ -466,6 +467,20 @@
        })
        .finally(() => { this.cityDirLoading = false })
    },
    showImportResult(row) {
      if (!row || !row.status) return
      const msg = (row.message || '').toString()
      if (row.status === 'success') {
        const text = (msg && msg.indexOf('导入') !== -1) ? msg : '数据导入成功'
        this.$message({ message: text, type: 'success', duration: 5000 })
      } else {
        this.$alert((msg || '数据导入失败,详见下方失败记录').replace(/\n/g, '<br>'), '数据导入失败', {
          dangerouslyUseHTMLString: true,
          type: 'error',
          confirmButtonText: '知道了'
        })
      }
    },
    importOne(row) {
      if (!this.period) { this.$message.warning('请先选择报表期'); return Promise.resolve() }
      row.status = 'uploading'
traffic-audit-web/src/views/DataView.vue
@@ -16,51 +16,35 @@
      </el-aside>
      <el-main>
        <h3>导入数据查看</h3>
        <el-form :inline="true">
          <el-form-item label="数据类型">
            <el-radio-group v-model="type" @change="loadData">
              <el-radio-button label="h2032">H203-2月报</el-radio-button>
              <el-radio-button label="transportAuth">运政车辆</el-radio-button>
              <el-radio-button label="trackMileage">轨迹里程</el-radio-button>
              <el-radio-button label="scaleSplit">规上规下拆分</el-radio-button>
              <el-radio-button label="freightTurnover">货运量周转量</el-radio-button>
              <el-radio-button label="h2031">H203-1旅客月报</el-radio-button>
              <el-radio-button label="passengerAuth">旅客运政车辆</el-radio-button>
              <el-radio-button label="cityBus">城市公交月报</el-radio-button>
              <el-radio-button label="cityTaxi">巡游出租月报</el-radio-button>
              <el-radio-button label="cityTaxiAuth">出租车运政车辆</el-radio-button>
            </el-radio-group>
          </el-form-item>
          <el-form-item label="报表期">
            <el-date-picker v-model="period" type="month" placeholder="全部" value-format="yyyy-MM" clearable style="width:140px" @change="loadData"></el-date-picker>
          </el-form-item>
          <el-form-item label="关键字">
            <el-input v-model="keyword" placeholder="企业/市州名称" clearable style="width:200px" @keyup.enter.native="loadData"></el-input>
          </el-form-item>
          <el-form-item>
            <el-button type="primary" @click="loadData">查询</el-button>
            <el-button type="success" icon="el-icon-full-screen" @click="toggleFullscreen">全屏查看</el-button>
          </el-form-item>
        </el-form>
        <div style="color:#909399;font-size:12px;margin-bottom:6px">
          å…± {{ total }} æ¡è®°å½•,当前第 {{ page }} é¡µ
        <div class="toolbar">
          <span class="toolbar-label">数据类型</span>
          <el-radio-group v-model="activeModule" size="small" @change="onModuleChange">
            <el-radio-button v-for="g in modules" :key="g.key" :label="g.key">{{ g.name }}</el-radio-button>
          </el-radio-group>
          <el-select v-model="type" size="small" style="width:250px" placeholder="选择数据类型" @change="onTypeSelect">
            <el-option-group :label="activeTypes.name">
              <el-option v-for="t in activeTypes.types" :key="t.value" :label="t.label" :value="t.value"></el-option>
            </el-option-group>
          </el-select>
          <span class="toolbar-sep"></span>
          <el-date-picker v-model="period" type="month" placeholder="报表期(全部)" value-format="yyyy-MM" clearable style="width:150px" @change="doQuery"></el-date-picker>
          <el-input v-model="keyword" placeholder="企业/市州名称" clearable style="width:180px" @keyup.enter.native="doQuery"></el-input>
          <el-button type="primary" size="small" @click="doQuery">查询</el-button>
          <el-button size="small" icon="el-icon-full-screen" @click="toggleFullscreen">全屏查看</el-button>
        </div>
        <div class="load-hint">共 {{ total }} æ¡è®°å½•,已加载 {{ records.length }} æ¡</div>
        <div class="table-wrap" ref="tableWrap">
          <div class="table-area">
            <el-table :data="records" border v-loading="loading" size="small" style="width:100%" height="100%">
              <el-table-column v-for="c in columns" :key="c.prop" :prop="c.prop" :label="c.label" :min-width="c.width" show-overflow-tooltip>
              <el-table-column v-for="c in visibleColumns" :key="c.prop" :prop="c.prop" :label="c.label" :min-width="c.width" show-overflow-tooltip>
                <template slot-scope="scope">
                  <span>{{ formatCell(scope.row[c.prop]) }}</span>
                </template>
              </el-table-column>
            </el-table>
          </div>
          <el-pagination style="margin-top:15px;text-align:right"
            background layout="total, prev, pager, next, sizes"
            :total="total" :current-page.sync="page" :page-size.sync="size"
            :page-sizes="[20, 50, 100]" @current-change="loadData" @size-change="loadData">
          </el-pagination>
        </div>
      </el-main>
    </el-container>
  </el-container>
@@ -73,11 +57,36 @@
  name: 'DataView',
  data() {
    return {
      modules: [
        {
          key: 'freight', name: '道路货运', types: [
            { value: 'h2032', label: '道路货物运输月度生产情况' },
            { value: 'transportAuth', label: '运政车辆' },
            { value: 'trackMileage', label: '轨迹里程' },
            { value: 'scaleSplit', label: '规上规下拆分' },
            { value: 'freightTurnover', label: '货运量周转量' }
          ]
        },
        {
          key: 'passenger', name: '公路旅客', types: [
            { value: 'h2031', label: '公路旅客运输月度生产情况' },
            { value: 'passengerAuth', label: '运政车辆数' }
          ]
        },
        {
          key: 'city', name: '城市客运', types: [
            { value: 'cityBus', label: '城市公共交通月度运营情况' },
            { value: 'cityTaxi', label: '巡游出租汽车运营服务情况月报' },
            { value: 'cityTaxiAuth', label: '出租车运政车辆' }
          ]
        }
      ],
      activeModule: 'freight',
      type: 'h2032',
      period: '',
      keyword: '',
      page: 1,
      size: 20,
      size: 50,
      total: 0,
      records: [],
      columns: [],
@@ -85,7 +94,33 @@
      loadingMore: false
    }
  },
  computed: {
    activeTypes() {
      const g = this.modules.find(x => x.key === this.activeModule) || this.modules[0]
      return g
    },
    visibleColumns() {
      const hiddenProps = ['enterpriseCode', 'unifiedCreditCode', 'reportUnit']
      const hiddenLabels = ['企业代码', '统一社会信用代码', '填报单位']
      return (this.columns || []).filter(c =>
        hiddenProps.indexOf(c.prop) < 0 && hiddenLabels.indexOf(c.label) < 0)
    }
  },
  methods: {
    onModuleChange(key) {
      const g = this.modules.find(x => x.key === key)
      if (!g || !g.types.length) return
      if (!g.types.some(t => t.value === this.type)) {
        this.type = g.types[0].value
      }
      this.doQuery()
    },
    onTypeSelect() { this.doQuery() },
    doQuery() {
      this.page = 1
      this.records = []
      this.loadData()
    },
    loadData() {
      this.loading = true
      this.loadingMore = false
@@ -100,7 +135,7 @@
      }).then(res => {
        const d = res.data || {}
        this.records = d.records || []
        this.columns = d.columns || []
        this.columns = (d.columns || []).filter(c => ['enterpriseCode', 'unifiedCreditCode', 'reportUnit'].indexOf(c.prop) < 0 && ['企业代码', '统一社会信用代码', '填报单位'].indexOf(c.label) < 0)
        this.total = d.total || 0
        if (this.page > 1 && this.records.length === 0) {
          this.page = 1
@@ -149,7 +184,7 @@
        this.records = this.records.concat(addRows)
        this.page = nextPage
        if (d.total != null) this.total = d.total
        if (d.columns && d.columns.length) this.columns = d.columns
        if (d.columns && d.columns.length) this.columns = d.columns.filter(c => ['enterpriseCode', 'unifiedCreditCode', 'reportUnit'].indexOf(c.prop) < 0 && ['企业代码', '统一社会信用代码', '填报单位'].indexOf(c.label) < 0)
      }).catch(e => {
        if (e && e._toast) return
        this.$message.error('加载更多失败')
@@ -216,43 +251,17 @@
</script>
<style scoped>
.el-header { background: #409EFF; color: white; line-height: 60px; font-size: 18px; }
#app .table-wrap {
  display: flex;
  flex-direction: column;
  height: calc(100vh - 330px);
  min-height: 300px;
}
#app .table-area {
  flex: 1 1 auto;
  min-height: 0;
  overflow: hidden;
}
#app .table-area .el-table {
  width: 100%;
  height: 100%;
}
#app .table-wrap .el-pagination {
  flex: none;
  margin-top: 8px;
}
#app .table-area:fullscreen {
  padding: 20px;
  background: #fff;
  overflow: auto;
}
#app .table-wrap:fullscreen {
  padding: 20px;
  background: #fff;
  overflow: auto;
  display: flex;
  flex-direction: column;
  height: 100vh;
}
#app .table-wrap:fullscreen .table-area {
  flex: 1 1 auto;
  min-height: 0;
}
#app .table-wrap:fullscreen .el-pagination {
  flex: none;
}
.type-row { margin-bottom: 10px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.type-label { color: #606266; font-weight: 600; }
.filter-row { margin-bottom: 4px; }
#app .toolbar { display:flex; align-items:center; gap:10px; flex-wrap:wrap; padding:8px 12px; background:#FAFAFA; border:1px solid #EBEEF5; border-radius:8px; margin-bottom:8px; }
#app .toolbar-label { color:#606266; font-weight:600; white-space:nowrap; }
#app .toolbar-sep { width:1px; height:22px; background:#DCDFE6; margin:0 2px; }
#app .load-hint { color:#909399; font-size:12px; margin-bottom:6px; }
#app .table-wrap { display:flex; flex-direction:column; height:calc(100vh - 250px); min-height:300px; }
#app .table-area { flex:1 1 auto; min-height:0; overflow:hidden; }
#app .table-area .el-table { width:100%; height:100%; }
#app .table-area:fullscreen { padding:20px; background:#fff; overflow:auto; }
#app .table-wrap:fullscreen { padding:20px; background:#fff; overflow:auto; display:flex; flex-direction:column; height:100vh; }
#app .table-wrap:fullscreen .table-area { flex:1 1 auto; min-height:0; }
</style>
traffic-audit-web/src/views/ReportExport.vue
@@ -27,7 +27,7 @@
          <!-- ç¬¬äºŒè¡Œï¼šæŠ¥è¡¨ç±»åž‹åˆ‡æ¢ï¼ˆè´§è¿ / æ—…客 / æŠ•资 / èƒ½è€— / åŸŽå¸‚客运);el-radio-group æ›¿ä»£ el-tabs,规避 el-tabs å¸ƒå±€æ— é™å¾ªçŽ¯ï¼ˆä¸Ž /import ä¿®å¤ä¸€è‡´ï¼‰ -->
          <el-radio-group v-model="activeKey" class="module-tabs">
            <el-radio-button v-for="m in modules" :key="m.key" :label="m.key">
            <el-radio-button v-for="m in visibleModules" :key="m.key" :label="m.key">
              {{ m.title }}<el-tag v-if="m.tag" size="mini" :type="m.tagType || 'info'" style="margin-left:6px">{{ m.tag }}</el-tag>
            </el-radio-button>
          </el-radio-group>
@@ -112,7 +112,25 @@
                <span v-if="!it.ready" class="cov-bad" style="margin-left:10px">{{ it.note }}</span>
                <span v-if="it.ready && it.audited === false" class="cov-bad" style="margin-left:10px">未审核</span>
              </div>
              <table class="preview-summary" v-if="it.summary && it.summary.length">
              <table class="preview-summary wb-table" v-if="it.key === 'summaryWorkbook' && it.summary && it.summary.length">
                <thead>
                  <tr><th style="width:44%">指标</th><th style="width:30%">数据</th><th>同比</th></tr>
                </thead>
                <tbody>
                  <template v-for="(sec, si) in wbRows(it.summary)">
                    <tr v-for="(row, ri) in sec.rows" :key="si + '-' + ri">
                      <td v-if="ri === 0" :rowspan="sec.rows.length" class="wb-sec">{{ sec.name }}</td>
                      <td class="wb-label">{{ row.label }}</td>
                      <td class="wb-value">{{ row.value === null ? '—' : fmtVal(row.value) }}</td>
                      <td class="wb-yoy" :class="{ 'yoy-empty': row.yoy === null }">{{ row.yoy === null ? '—' : row.yoy }}</td>
                    </tr>
                  </template>
                </tbody>
              </table>
              <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">
                <tbody>
                  <tr v-for="(s, i) in it.summary" :key="i">
                    <td>{{ s.label }}</td>
@@ -176,21 +194,21 @@
          ], checked: []
        },
        {
          key: 'freight', title: '货运报表', items: [
          key: 'freight', title: '货运报表', hidden: true, items: [
            { key: 'cityDetail', label: '货运量分市州明细', fileName: '生成_货运量分市州明细.xlsx', status: null, statusNote: '' },
            { key: 'freightRank', label: '货运量排名', fileName: '生成_货运量排名.xlsx', status: null, statusNote: '' },
            { key: 'turnoverRank', label: '周转量排名', fileName: '生成_周转量排名.xlsx', status: null, statusNote: '' }
          ], checked: []
        },
        {
          key: 'passenger', title: '旅客报表', items: [
          key: 'passenger', title: '旅客报表', hidden: true, items: [
            { key: 'passengerCityDetail', label: '公路旅客分市州', fileName: '生成_公路旅客分市州.xlsx', status: null, statusNote: '' },
            { key: 'passengerMidDetail', label: '中口径明细', fileName: '生成_公路旅客中口径明细.xlsx', status: null, statusNote: '' },
            { key: 'passengerMidRank', label: '中口径排名', fileName: '生成_公路旅客中口径排名.xlsx', status: null, statusNote: '' },
            { key: 'passengerMidAnalysis', label: '中口径分析', fileName: '生成_公路旅客中口径分析.xlsx', status: null, statusNote: '' }
          ], checked: []
        },        {
          key: 'cityPassenger', title: '城市客运报表', items: [
          key: 'cityPassenger', title: '城市客运报表', hidden: true, items: [
            { key: 'cityBusDetail', label: '城市公交客运量分市州明细', fileName: '生成_城市公交客运量分市州明细.xlsx', status: null, statusNote: '' },
            { key: 'cityTaxiDetail', label: '巡游出租客运量分市州明细', fileName: '生成_巡游出租客运量分市州明细.xlsx', status: null, statusNote: '' },
            { key: 'cityRailFerryDetail', label: '轨道轮渡客运量分市州明细', fileName: '生成_轨道轮渡客运量分市州明细.xlsx', status: null, statusNote: '' },
@@ -223,6 +241,9 @@
      const s = sessionStorage.getItem('modules')
      if (!s) return []
      return s.split(',').filter(Boolean)
    },
    visibleModules() {
      return this.modules.filter(m => !m.hidden)
    },
    curModule() {
      return this.modules.find(m => m.key === this.activeKey) || this.modules[0]
@@ -260,8 +281,8 @@
    }
  },
  created() {
    const mine = this.myModules.filter(k => this.modules.some(m => m.key === k))
    this.activeKey = (mine.length ? mine : this.modules.map(m => m.key))[0]
    const mine = this.myModules.filter(k => this.visibleModules.some(m => m.key === k))
    this.activeKey = (mine.length ? mine : this.visibleModules.map(m => m.key))[0]
    this.ensureCycle()
    this.refreshAll()
  },  methods: {
@@ -377,6 +398,65 @@
      }
      return String(v)
    },
    wbRows(summary) {
      const get = (key) => {
        const hit = (summary || []).find(x => x.label === key)
        return hit && hit.value !== null && hit.value !== undefined && hit.value !== '' ? hit.value : null
      }
      const pct = (key) => {
        const v = get(key)
        return v === null ? null : this.fmtVal(v) + '%'
      }
      const mk = (defs) => defs.map(d => ({
        label: d[0],
        value: get(d[1]),
        yoy: d[2] ? pct(d[2]) : null
      }))
      return [
        {
          name: '道路货运',
          rows: mk([
            ['当月货运量(万吨)', '全省货运量·当月'],
            ['当月货物周转量(万吨)', '全省货物周转量·当月', '全省货物周转量·当月同比'],
            ['累计货运量(万吨)', '全省货运量·累计'],
            ['累计货物周转量(万吨公里)', '全省货物周转量·累计', '全省货物周转量·累计同比']
          ])
        },
        {
          name: '城市客运',
          rows: mk([
            ['当月城市客运量:', '城市客运量·当月'],
            ['当月城市客运周转量', '城市客运周转量·当月'],
            ['累计城市客运量:', '城市客运量·累计'],
            ['累计城市客运周转量', '城市客运周转量·累计']
          ])
        },
        {
          name: '中口径',
          rows: mk([
            ['当月中口径客运量', '中口径客运量·当月'],
            ['当月中口径周转量', '中口径周转量·当月'],
            ['累计中口径客运量', '中口径客运量·累计'],
            ['累计中口径周转量', '中口径周转量·累计']
          ])
        }
      ]
    },
    wbMissing(summary) {
      let total = 0
      let have = 0
      ;(summary || []).forEach(x => {
        if (String(x.label).indexOf('同比') >= 0) return
        total++
        if (x.value !== null && x.value !== undefined && x.value !== '') have++
      })
      return total > 0 && have < total
    },
    wbPrevYear() {
      const m = /^(\d{4})-(\d{2})$/.exec(this.previewPeriod || '')
      if (!m) return '上年同期'
      return (Number(m[1]) - 1) + '-' + m[2]
    },
    coverageText(it) {
      const cov = it.monthCoverage || []
      if (!cov.length) return '无数据'
@@ -491,4 +571,11 @@
@media (max-width: 900px) {
  .panel-head { flex-direction: column; align-items: flex-start; }
}
.wb-table th { background:#f5f7fa; }
.wb-sec { background:#F7F9FC; font-weight:600; text-align:center; color:#303133; }
.wb-label { color:#303133; }
.wb-value { text-align:right; font-variant-numeric: tabular-nums; }
.wb-yoy { text-align:right; color:#606266; }
.wb-yoy.yoy-empty { color:#C0C4CC; }
.wb-note { color:#E6A23C; font-size:12px; margin:2px 0 8px; }
</style>