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(@RequestParam(value = "reportType", required = false) String reportType) { LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); if (reportType != null && !reportType.trim().isEmpty()) { qw.eq(AuditRule::getReportType, reportType.trim()); } qw.orderByAsc(AuditRule::getSortOrder); List list = ruleMapper.selectList(qw); for (AuditRule r : list) syncThresholdText(r); // 名称/说明随阈值动态展示(如 ±10% → ±20%) return Result.ok(list); } /** 启停规则 */ @PostMapping("/toggle") public Result 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 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 parseThresholdNums(String threshold) { java.util.List 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 olds = parseThresholdNums(oldThreshold); java.util.List 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()); } }