跳转至

规则表达式的临时方案

场景介绍

由于公司的客户对于市面上买卖丧葬用品的价格不透明,AB价格交易的情况有杜绝倾向,所以和公司商讨一套系统,用于管理这些用品及其交易情况, 有需要对订单进行管理,同时订单涉及到订单的物品,订单物品的类型等,用户在下单的时候,需要对下单的物品进行提醒, 告知物品的价格过高,明显不合理。那么就有一个面板需要对物品的类型进行管控,这一类的物品价格必须满足后台设置的给定值, 否则就给予预警。

那么后台对于预警规则的设置就需要一套规则,正常的思路就是集成规则引擎的框架,用于管理各类规则,但是项目给的时间少,要求尽快开发出来, 对于开发人员而言,还需要去调研规则引擎的选类,学习框架的使用,时间上面不允许,而且集成框架可能会对原有的老系统的资源, 有一定的影响。我们研发只能是自己规定设计一套勉强能用的方案。

前后端约定

首先需要和前后端约定,对于这个临时的规则的传参需要怎么设计:

  1. 首先前端需要按照要求传字符串,要求如代码注释
  2. 后端根据算法解析规则,判断价格是否超出预警阈值
  3. 数据库使用一个字段存储规则,使用JSON类型
@Data
@AllArgsConstructor
public class RuleScript {
    /**
    * 运算的类型 < <= > >= ==  如<=
    */
    private String symbol;

    /**
    * 具体的数值 如20000.00
    */
    private BigDecimal value;

    /**
    * 与后一个条件的关联关系 如或
    */
    private RuleConditionEnum condition;

    /**这个值前端不给值,后端零时使用*/
    private Boolean temp;
}
@Getter
@AllArgsConstructor
public enum RuleConditionEnum {
    OR(1, "或"),
    AND(2, "且");
    private final Integer value;
    private final String label;
}

算法设计

我们后端就需要对前端传递的数据,一个List<RuleScript>进行解析、使用。

1️⃣对数据分组计算

  • 首先遍历所有规则,计算物品价格是否满足当前的预警值(truefalse),并将它存储在临时变量中(无需从前端传递)
  • 将连续的 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));
}

总结

没有过多的文字介绍,代码本身就是一种语言,它自己会说话传递信息,我就不多嘴了。这样的一种东西只能是临时使用, 它的优点就是轻,快,但是和规则引擎相比,业务代码要和计算逻辑耦合,无法使业务逻辑与这代码分离,也不便于维护和更新, 对于扩展和更复杂的逻辑处理,更是不可能,注定只是一种临时方案应付一下,但是应付的能力还是要有,故记录一下。