fix: 09-09下午第三轮——审核结果默认按企业合并与问题范围语义收敛/导出企业合并问题列按用户4条例文话术、数据查看工具栏整理、报表整本预览分组表格化 + 测试报告与工作日结
5个文件已修改
1个文件已添加
507 ■■■■■ 已修改文件
docs/功能测试报告/功能测试报告_审核界面交互与问题话术导出_2026-09-09.md 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/工作日结/工作日结_2026-09-09.md 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/auditengine/service/AuditEngineService.java 247 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/AuditResult.vue 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/DataView.vue 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/ReportExport.vue 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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
@@ -38,3 +38,24 @@
- /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 å¨ï¼‰æ˜¯å¦ç¬¦åˆä¸šåŠ¡é¢„æœŸã€‚
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) {
@@ -1512,25 +1500,84 @@
    /** ä¼ä¸šåˆå¹¶â€œé—®é¢˜â€åˆ—话术:按规则类型生成可读的核实意见 */
    private String issueSentence(AuditResult res) {
        String code = res.getRuleCode();
        String dev = valueOf(res.getDeviation());
        String name = valueOf(res.getRuleName());
        String act = valueOf(res.getActualValue());
        String thr = valueOf(res.getThresholdValue());
        if ("RULE_001".equals(code) || "RULE_002".equals(code)) {
            return "企业上报车辆/吨位与运政数据对比存在差异(" + dev + "),请核实";
            return "企业上报车辆/吨位与运政数据对比存在差异(" + act + "),请核实";
        }
        if ("RULE_003".equals(code)) {
            return "上报周转量与轨迹测算周转量差异明显(" + dev + "),请参考轨迹核实企业周转量";
            return "上报周转量与轨迹测算周转量差异明显(" + act + "),请参考轨迹核实企业周转量";
        }
        if ("RULE_004".equals(code)) {
            return "货运类型环比发生变化,请核实";
            return "货运类型环比发生变化" + (act.isEmpty() ? "" : "(" + act + ")") + ",请关注是否填报正常";
        }
        if ("RULE_005".equals(code)) {
            return "无集装箱车但存在集装箱货运量,请核实车辆类型与货运量填报";
            return "无集装箱车但存在集装箱货运量(" + act + "),请核实车辆类型与货运量填报";
        }
        if ("RULE_006".equals(code)) {
            return "整车吨位超出 4~40 å¨åˆç†èŒƒå›´ï¼Œè¯·æ ¸å®žå¹³å‡å¨ä½å¡«æŠ¥";
            return "整车平均吨位超出合理范围(" + act + "),请核实平均吨位填报";
        }
        return valueOf(res.getRuleName()) + "异常(实际:" + act + ";基准:" + thr + ";" + dev + "),请核实";
        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) {
traffic-audit-web/src/views/AuditResult.vue
@@ -50,7 +50,9 @@
          <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>
@@ -59,11 +61,12 @@
              <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-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-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">
@@ -87,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"
@@ -354,7 +359,7 @@
  data() {
    return {
      period: '', reportType: 'H2032', loading: false, marking: false, results: [], ruleStats: [], statsCollapsed: false,
      enterpriseFilter: '', ruleFilter: '', viewMode: 'detail', issueScope: 'verify', statusScope: 'all', 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: []
    }
@@ -367,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++
@@ -769,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/DataView.vue
@@ -16,32 +16,23 @@
      </el-aside>
      <el-main>
        <h3>导入数据查看</h3>
        <div class="type-row">
          <span class="type-label">数据类型</span>
        <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:300px" placeholder="选择数据类型" @change="onTypeSelect">
          <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>
        <el-form :inline="true" class="filter-row">
          <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>
        <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%">
@@ -52,12 +43,8 @@
              </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>
@@ -99,7 +86,7 @@
      period: '',
      keyword: '',
      page: 1,
      size: 20,
      size: 50,
      total: 0,
      records: [],
      columns: [],
@@ -126,9 +113,14 @@
      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()
    },
    onTypeSelect() { this.loadData() },
    loadData() {
      this.loading = true
      this.loadingMore = false
@@ -262,43 +254,14 @@
.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 .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;
}
#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
@@ -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>
@@ -380,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 '无数据'
@@ -494,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>