zhizhijie
2026-09-20 a08c70db67e49bf6cd125df04ebaf68c26f0d8dd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
package com.trafficaudit.rulemanage.controller;
 
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.trafficaudit.common.Result;
import com.trafficaudit.rulemanage.entity.AuditRule;
import com.trafficaudit.rulemanage.mapper.AuditRuleMapper;
import org.springframework.web.bind.annotation.*;
 
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.List;
 
/** 规则管理:按报表类型查看/启停/调整阈值 */
@RestController
@RequestMapping("/api/rules")
public class RuleManageController {
 
    @Resource
    private AuditRuleMapper ruleMapper;
 
    /** 规则列表,可按报表类型过滤 */
    @GetMapping("/list")
    public Result<List<AuditRule>> list(@RequestParam(value = "reportType", required = false) String reportType) {
        LambdaQueryWrapper<AuditRule> qw = new LambdaQueryWrapper<>();
        if (reportType != null && !reportType.trim().isEmpty()) {
            qw.eq(AuditRule::getReportType, reportType.trim());
        }
        qw.orderByAsc(AuditRule::getSortOrder);
        List<AuditRule> list = ruleMapper.selectList(qw);
        for (AuditRule r : list) syncThresholdText(r); // 名称/说明随阈值动态展示(如 ±10% → ±20%)
        return Result.ok(list);
    }
 
    /** 启停规则 */
    @PostMapping("/toggle")
    public Result<String> toggle(@RequestParam("id") Long id,
                                 @RequestParam("enabled") Integer enabled) {
        AuditRule rule = ruleMapper.selectById(id);
        if (rule == null) return Result.error("规则不存在");
        rule.setIsEnabled(enabled == null || enabled == 0 ? 0 : 1);
        rule.setUpdatedAt(LocalDateTime.now());
        ruleMapper.updateById(rule);
        return Result.ok("已更新");
    }
 
    /** 更新阈值/等级/描述 */
    @PostMapping("/update")
    public Result<String> update(@RequestBody AuditRule body) {
        if (body.getId() == null) return Result.error("缺少规则ID");
        AuditRule rule = ruleMapper.selectById(body.getId());
        if (rule == null) return Result.error("规则不存在");
        String oldThreshold = rule.getThreshold();
        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% 片段按新阈值改写),并随保存持久化
        if (body.getThreshold() != null && !body.getThreshold().equals(oldThreshold)) {
            syncThresholdText(rule, oldThreshold);
        }
        rule.setUpdatedAt(LocalDateTime.now());
        ruleMapper.updateById(rule);
        return Result.ok("已保存");
    }
 
    /** 名称/说明中随阈值动态变化的数字片段:匹配任意数字(% 前后、台/吨/公里等单位前均可) */
    private static final Pattern NUM_TOKEN = Pattern.compile("\\d+(?:\\.\\d+)?");
 
    /** 解析阈值字符串中的所有数值(支持“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;
    }
 
    /** 数值展示:整数去掉小数位,其余保留原精度 */
    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(), oldThreshold, rule.getThreshold()));
        rule.setDescription(decorateThresholdText(rule.getDescription(), oldThreshold, rule.getThreshold()));
    }
 
    /** 读列表时自愈:仅当文本片段与当前阈值对应时才改写(幂等,不误伤与阈值无关的数字) */
    private void syncThresholdText(AuditRule rule) {
        syncThresholdText(rule, rule == null ? null : rule.getThreshold());
    }
}