fix: 09-09上午整改——RULE_001分项运政对比(6段)+阈值说明联动通用化+汇总大表列宽回归母版+导入结果弹窗 + 测试报告与工作日结
4个文件已修改
2个文件已添加
299 ■■■■■ 已修改文件
docs/功能测试报告/功能测试报告_RULE001分项运政与阈值联动及列宽回归_2026-09-09.md 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/工作日结/工作日结_2026-09-09.md 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/auditengine/service/AuditEngineService.java 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/reportexport/service/ReportExportService.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-server/src/main/java/com/trafficaudit/rulemanage/controller/RuleManageController.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traffic-audit-web/src/views/DataImport.vue 15 ●●●●● 补丁 | 查看 | 原始文档 | 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,24 @@
# å·¥ä½œæ—¥ç»“ 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 æœˆè´§è¿é‡å‘¨è½¬é‡/规上规下/个体客运等缺数后整链验证。
traffic-audit-server/src/main/java/com/trafficaudit/auditengine/service/AuditEngineService.java
@@ -1569,6 +1569,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 +1611,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 +1886,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/service/ReportExportService.java
@@ -3234,7 +3234,6 @@
            dynamicSummaryYear(wb, year);
            cleanSummarySheetPresentation(wb); // å–消各页筛选、还原隐藏行(否则“班线包车”等页看起来像缺数据)
            applyTwoDecimalFormat(wb); // æ•°å€¼æ˜¾ç¤ºä¸¤ä½å°æ•°ï¼ˆä¿ç•™å…¨ç²¾åº¦ï¼‰ï¼›% / æ—¥æœŸç­‰æ—¢æœ‰æ ·å¼ä¸æ”¹å˜
            autoFitContentColumns(wb); // æŒ‰æ•°æ®è‡ªåŠ¨åŠ å®½åˆ—å®½ï¼ˆåªåŠ å®½ä¸ç¼©çª„ï¼Œè·³è¿‡åˆå¹¶æ ‡é¢˜æ ¼ï¼Œé˜²é•¿æ–‡æœ¬æŠŠå•åˆ—æ’‘çˆ†ï¼‰
            if (!wb.getCTWorkbook().isSetCalcPr()) wb.getCTWorkbook().addNewCalcPr();
            wb.getCTWorkbook().getCalcPr().setFullCalcOnLoad(true); // è¿˜åŽŸ/改写公式后打开即重算
            return toBytes(wb);
@@ -4822,59 +4821,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;
    /** è§£æžé˜ˆå€¼å­—符串中的所有数值(支持“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 {
            return Double.parseDouble(m.group());
                out.add(Double.parseDouble(m.group()));
        } catch (Exception e) {
            return null;
                // å¿½ç•¥æ— æ³•解析的段
        }
        }
        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()));
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'