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
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 PCT_NUM = 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;
        }
    }
 
    /** 数值展示:整数去掉小数位,其余保留原精度 */
    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;
        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);
        StringBuffer sb = new StringBuffer();
        while (m.find()) {
            double token;
            try {
                token = Double.parseDouble(m.group());
            } catch (Exception e) {
                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;
                }
            }
            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());
    }
}