规则表达式的临时方案¶
场景介绍¶
由于公司的客户对于市面上买卖丧葬用品的价格不透明,AB价格交易的情况有杜绝倾向,所以和公司商讨一套系统,用于管理这些用品及其交易情况, 有需要对订单进行管理,同时订单涉及到订单的物品,订单物品的类型等,用户在下单的时候,需要对下单的物品进行提醒, 告知物品的价格过高,明显不合理。那么就有一个面板需要对物品的类型进行管控,这一类的物品价格必须满足后台设置的给定值, 否则就给予预警。
那么后台对于预警规则的设置就需要一套规则,正常的思路就是集成规则引擎的框架,用于管理各类规则,但是项目给的时间少,要求尽快开发出来, 对于开发人员而言,还需要去调研规则引擎的选类,学习框架的使用,时间上面不允许,而且集成框架可能会对原有的老系统的资源, 有一定的影响。我们研发只能是自己规定设计一套勉强能用的方案。
前后端约定¶
首先需要和前后端约定,对于这个临时的规则的传参需要怎么设计:
- 首先前端需要按照要求传字符串,要求如代码注释
- 后端根据算法解析规则,判断价格是否超出预警阈值
- 数据库使用一个字段存储规则,使用
JSON
类型
算法设计¶
我们后端就需要对前端传递的数据,一个List<RuleScript>
进行解析、使用。
1️⃣对数据分组计算¶
- 首先遍历所有规则,计算物品价格是否满足当前的预警值(
true
或false
),并将它存储在临时变量中(无需从前端传递) - 将连续的
AND
条件视为一组,并通过OR
条件分隔这些组。然后,对每一组执行逻辑AND
操作以确定该组的最终布尔值。
我们先操作第一条和第二条
private List<List<RuleScript>> groupScripts(List<RuleScript> ruleScripts) {
List<List<RuleScript>> groups = new ArrayList<>();
int i = 0;
int size = ruleScripts.size();
while (i < size) {
if (ruleScripts.get(i).getCondition() == RuleConditionEnum.AND) {
List<RuleScript> group = new ArrayList<>();
// 把所有连续的AND加入组
while (i < size && ruleScripts.get(i).getCondition() == RuleConditionEnum.AND) {
group.add(ruleScripts.get(i));
i++;
}
// 如果还有元素,把下一个元素(可能是OR)加入组
if (i < size) {
group.add(ruleScripts.get(i));
i++;
}
groups.add(group);
} else if (Objects.isNull(ruleScripts.get(i).getCondition())
|| ruleScripts.get(i).getCondition() == RuleConditionEnum.OR) {
List<RuleScript> group = new ArrayList<>();
group.add(ruleScripts.get(i));
groups.add(group);
// 如果有下一个元素,下一个元素单独成组
if (i < size - 1) {
List<RuleScript> nextGroup = new ArrayList<>();
nextGroup.add(ruleScripts.get(i + 1));
groups.add(nextGroup);
i += 2;
} else {
i++;
}
} else {
i++;
}
}
return groups;
}
2️⃣计算最终结果¶
- 最后,对所有组应用逻辑
OR
操作,以决定整个条件列表的最终布尔结果,从而判断是否需要触发预警。
private boolean calculateFinalResult(List<List<RuleScript>> groups) {
// 对每个组进行逻辑与操作,并收集结果
List<Boolean> bools = groups.stream()
.map(group -> group.stream()
.allMatch(script -> script.getTemp() != null && script.getTemp()))
.collect(Collectors.toList());
// 对所有组的结果进行逻辑或操作
return bools.stream().anyMatch(result -> result);
}
3️⃣每一条规则的计算和对前端规则的校验¶
- 那么每一条规则的计算如何避免复杂性,可以使用
枚举
的方式 - 为了保证前端传递参数的准确性,我们还需要对规则进行校验
@Getter
@AllArgsConstructor
public enum JudgeEnum {
ge(">=", "大于等于") {
@Override
public boolean compare(BigDecimal price, BigDecimal value) {
return price.compareTo(value) >= 0;
}
},
gt(">", "大于") {
@Override
public boolean compare(BigDecimal price, BigDecimal value) {
return price.compareTo(value) > 0;
}
},
eq("==", "等于") {
@Override
public boolean compare(BigDecimal price, BigDecimal value) {
return price.compareTo(value) == 0;
}
},
le("<=", "小于等于") {
@Override
public boolean compare(BigDecimal price, BigDecimal value) {
return price.compareTo(value) <= 0;
}
},
lt("<", "小于") {
@Override
public boolean compare(BigDecimal price, BigDecimal value) {
return price.compareTo(value) < 0;
}
};
private final String value;
private final String label;
/**
* 根据 value 获取对应的枚举值
*/
public static JudgeEnum getByValue(String value) {
for (JudgeEnum judgeEnum : JudgeEnum.values()) {
if (judgeEnum.getValue().equals(value)) {
return judgeEnum;
}
}
return null;
}
public abstract boolean compare(BigDecimal price, BigDecimal value);
}
private boolean takeRulesEffect(List<RuleScript> ruleScripts, BigDecimal price) {
for (RuleScript ruleScript : ruleScripts) {
if (StrUtil.isNotBlank(ruleScript.getSymbol())) {
// 通过枚举值获取对应的比较逻辑
JudgeEnum judgeEnum = JudgeEnum.getByValue(ruleScript.getSymbol());
if (Objects.isNull(judgeEnum)) {
throw new RuntimeException("给予的规则不匹配,请重新设置");
}
// 使用枚举类中的比较逻辑
boolean hasWarn = judgeEnum.compare(price, ruleScript.getValue());
ruleScript.setTemp(hasWarn);
} else {
ruleScript.setTemp(false);
}
}
return calculateFinalResult(groupScripts(ruleScripts));
}
4️⃣验算¶
写一个测试类,可以看到结果符合第四条和第五条的规则,返回true
,可以多试几次,自己口算试试看是否是对的。
public static void main(String[] args) {
BigDecimal price = new BigDecimal("2000.00");
List<RuleScript> ruleScripts = Lists.newArrayList();
ruleScripts.add(new RuleScript(">", new BigDecimal("100.00"), RuleConditionEnum.AND, null));
ruleScripts.add(new RuleScript("<", new BigDecimal("200.00"), RuleConditionEnum.OR, null));
ruleScripts.add(new RuleScript(">=", new BigDecimal("2000.00"), RuleConditionEnum.AND, null));
ruleScripts.add(new RuleScript("<", new BigDecimal("3000.00"), RuleConditionEnum.OR, null));
ruleScripts.add(new RuleScript(">=", new BigDecimal("5000.00"), RuleConditionEnum.AND, null));
ruleScripts.add(new RuleScript("<=", new BigDecimal("7000.00"), RuleConditionEnum.AND, null));
ruleScripts.add(new RuleScript(">", new BigDecimal("8000.00"), RuleConditionEnum.AND, null));
ruleScripts.add(new RuleScript("<", new BigDecimal("9000.00"), RuleConditionEnum.OR, null));
ruleScripts.add(new RuleScript(">", new BigDecimal("9000.00"), RuleConditionEnum.AND, null));
ruleScripts.add(new RuleScript("<=", new BigDecimal("10000.00"), RuleConditionEnum.AND, null));
System.out.println(takeRulesEffect(ruleScripts, price));
}
总结¶
没有过多的文字介绍,代码本身就是一种语言,它自己会说话传递信息,我就不多嘴了。这样的一种东西只能是临时使用, 它的优点就是轻,快,但是和规则引擎相比,业务代码要和计算逻辑耦合,无法使业务逻辑与这代码分离,也不便于维护和更新, 对于扩展和更复杂的逻辑处理,更是不可能,注定只是一种临时方案应付一下,但是应付的能力还是要有,故记录一下。