traffic-audit-server/src/main/java/com/trafficaudit/rulemanage/controller/RuleManageController.java
@@ -55,29 +55,108 @@
        if (body.getThreshold() != null) rule.setThreshold(body.getThreshold());
        if (body.getAlertLevel() != null) rule.setAlertLevel(body.getAlertLevel());
        if (body.getDescription() != null) rule.setDescription(body.getDescription());
        // 阈值变化时同步规则名称与说明(名称/说明里的 ±N% 按新阈值改写),并随保存持久化
        // 阈值变化时同步规则名称与说明(名称/说明里与旧阈值对应的 N% 片段按新阈值改写),并随保存持久化
        if (body.getThreshold() != null && !body.getThreshold().equals(oldThreshold)) {
            syncThresholdText(rule);
            syncThresholdText(rule, oldThreshold);
        }
        rule.setUpdatedAt(LocalDateTime.now());
        ruleMapper.updateById(rule);
        return Result.ok("已保存");
    }
    /** ±N% 字样匹配(名称/说明中随阈值动态变化的片段) */
    private static final Pattern PCT_TOKEN = Pattern.compile("±\\s*\\d+(?:\\.\\d+)?\\s*%");
    /** 名称/说明中随阈值动态变化的数字片段:匹配任意数字(% 前后、台/吨/公里等单位前均可) */
    private static final Pattern NUM_TOKEN = Pattern.compile("\\d+(?:\\.\\d+)?");
    /** 把文本中的 ±N% 替换为 ±threshold%(含已带 ± 的前缀,避免双 ±) */
    private String decorateThresholdText(String text, String threshold) {
        if (text == null || text.trim().isEmpty() || threshold == null || threshold.trim().isEmpty()) return text;
        String token = "±" + threshold.trim() + "%";
        return PCT_TOKEN.matcher(text).replaceAll(Matcher.quoteReplacement(token));
    /** 解析阈值字符串中的所有数值(支持“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;
    }
    /** 名称/说明随阈值同步(读列表时也动态校正,DB 中旧文本不阻塞显示) */
    private void syncThresholdText(AuditRule rule) {
    /** 数值展示:整数去掉小数位,其余保留原精度 */
    private String fmtNum(double v) {
        long l = Math.round(v);
        if (Math.abs(v - l) < 1e-9) return Long.toString(l);
        return new java.math.BigDecimal(String.valueOf(v)).stripTrailingZeros().toPlainString();
    }
    /** 由阈值数值推导“名称/说明里写的是百分之几”的候选口径:
     *  ① 阈值本身就是百分数(如 20 → ±20%);
     *  ② 阈值是小数比例(如 0.02 → 2%、0.5 → 50%);此时 <1 才启用;
     *  ③ 阈值是比值(如 1.3 → 高出30%);此时 >1 才启用;
     *  其余按原数匹配(台/吨/公里等阈值原样出现在文字中)。 */
    private double[] thresholdPercentCandidates(double v) {
        double[] cands;
        if (v > 0 && v < 1) {
            cands = new double[]{v, v * 100};
        } else if (v > 1) {
            cands = new double[]{v, (v - 1) * 100};
        } else {
            cands = new double[]{v};
        }
        return cands;
    }
    /** 按“旧阈值→新阈值”逐段改写名称/说明中的数字:
     *  只替换与旧阈值某一段(或其百分比口径)对得上的数字;多段阈值(逗号分隔)逐段对应;
     *  与阈值无关的数字(日期、与规则无关的固定数值等)保持不动。 */
    private String decorateThresholdText(String text, String oldThreshold, String newThreshold) {
        if (text == null || text.trim().isEmpty()) return 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;
            try {
                token = Double.parseDouble(m.group());
            } catch (Exception e) {
                continue;
            }
            String repl = null;
            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;
            m.appendReplacement(sb, Matcher.quoteReplacement(repl));
        }
        m.appendTail(sb);
        return sb.toString();
    }
    /** 名称/说明随阈值同步(update 时按 旧→新 改写并持久化;多段阈值同样逐段联动) */
    private void syncThresholdText(AuditRule rule, String oldThreshold) {
        if (rule == null || rule.getThreshold() == null) return;
        rule.setRuleName(decorateThresholdText(rule.getRuleName(), rule.getThreshold()));
        rule.setDescription(decorateThresholdText(rule.getDescription(), rule.getThreshold()));
        rule.setRuleName(decorateThresholdText(rule.getRuleName(), oldThreshold, rule.getThreshold()));
        rule.setDescription(decorateThresholdText(rule.getDescription(), oldThreshold, rule.getThreshold()));
    }
}
    /** 读列表时自愈:仅当文本片段与当前阈值对应时才改写(幂等,不误伤与阈值无关的数字) */
    private void syncThresholdText(AuditRule rule) {
        syncThresholdText(rule, rule == null ? null : rule.getThreshold());
    }
}