Hello World

吞风吻雨葬落日 欺山赶海踏雪径

0%

规则引擎

基于 QLExpress 规则引擎

这里不讨论 QLExpress的使用,只基于 QLExpress 构建自己的规则描述方案。

基础实现

规则描述模型 Rule -> Group -> Condition

Rule

  • String andor
    • and
    • or
    • not
  • List groups

Group

  • String andor
  • List conditions

Condition

  • dataCode 数据标识
  • valueType 数据类型
    • normal 固定值
    • data_code 指标值
  • compare 对比逻辑
    • greater (>)
    • less (<)
    • equal (==)
    • not_equal (!=)
    • greater_equal (>=)
    • less_equal (<=)
    • not_null (!=null)
    • is_null (==null)
    • like ( like )
  • thresholdValueType 阈值类型
    • normal 固定值
    • data_code 指标值
  • thresholdCode 阈值标识
  • thresholdArithmetic 阈值运算符
  • thresholdValue 阈值
  • name
  • actionDesc
  • enablePeriod [] 生效小时实现一天内部分规则定时执行

比如描述

1
A类违规天数90天内 == false and (虚假交易扣分 < 48 or 假冒扣分 <= 12) and 待整改卖家 < 总数*0.8  and (宝贝相符DSR > 4.6 or 职位名称 like %开发%)

描述逻辑

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
private Rule getRule(){

// A类违规天数90天内
Rule.RuleCondition condition1= new Rule.RuleCondition();
condition1.setName("A类违规天数");
condition1.setCode("A类违规天数90天内");
condition1.setCompare(OperatorEnum.EQUAL.getSymbol());
condition1.setThresholdValueType(RuleValueTypeEnum.normal.name());
condition1.setThresholdValue("false");

Rule.RuleGroup group1 = new Rule.RuleGroup();
group1.setConditions(Arrays.asList(condition1));

// (虚假交易扣分<48 or 假冒扣分<=12)
Rule.RuleCondition condition2= new Rule.RuleCondition();
condition2.setName("虚假交易扣分");
condition2.setCode("虚假交易扣分");
condition2.setCompare(OperatorEnum.LESS.getSymbol());
condition2.setThresholdValueType(RuleValueTypeEnum.normal.name());
condition2.setThresholdValue("48");

Rule.RuleCondition condition3= new Rule.RuleCondition();
condition3.setName("假冒扣分");
condition3.setCode("假冒扣分");
condition3.setCompare(OperatorEnum.LESS_EQUAL.getSymbol());
condition3.setThresholdValueType(RuleValueTypeEnum.normal.name());
condition3.setThresholdValue("12");

Rule.RuleGroup group2 = new Rule.RuleGroup();
group2.setAndor(LogicalRelationEnum.or.name());
group2.setConditions(Arrays.asList(condition2,condition3));

// 待整改卖家 == false
Rule.RuleCondition condition4= new Rule.RuleCondition();
condition4.setName("待整改卖家");
condition4.setCode("待整改卖家");
condition4.setCompare(OperatorEnum.LESS.getSymbol());
condition4.setThresholdValueType(RuleValueTypeEnum.data_code.name());
condition4.setThresholdCode("总数");
condition4.setThresholdArithmetic("*");
condition4.setThresholdValue("0.8");

Rule.RuleGroup group3 = new Rule.RuleGroup();
group3.setConditions(Arrays.asList(condition4));

// (宝贝相符DSR>4.6 or 职位名称 like ("%菜菜%"))
Rule.RuleCondition condition5= new Rule.RuleCondition();
condition5.setName("宝贝相符DSR");
condition5.setCode("宝贝相符DSR");
condition5.setCompare(OperatorEnum.GREATER.getSymbol());
condition5.setThresholdValueType(RuleValueTypeEnum.normal.name());
condition5.setThresholdValue("4.6");

Rule.RuleCondition condition6= new Rule.RuleCondition();
condition6.setName("职位名称");
condition6.setCode("职位名称");
condition6.setCompare(OperatorEnum.LIKE.getSymbol());
condition6.setThresholdValueType(RuleValueTypeEnum.normal.name());
condition6.setThresholdValue("'%开发%'");

Rule.RuleGroup group4 = new Rule.RuleGroup();
group4.setAndor(LogicalRelationEnum.or.name());
group4.setConditions(Arrays.asList(condition5,condition6));


Rule rule = new Rule();
rule.setAndor(LogicalRelationEnum.and.name());
rule.setGroups(Arrays.asList(group1,group2,group3,group4));

return rule;
}

执行规则逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void testRunRule() throws Exception {
String expression = getRule().getExpression();

System.out.println(expression);

DefaultContext<String, Object> context = new DefaultContext<String, Object>();
context.put("A类违规天数90天内", false);
context.put("虚假交易扣分", 49);
context.put("假冒扣分", 11);
context.put("待整改卖家", 70);
context.put("总数", 100);
context.put("宝贝相符DSR", 4.0);
context.put("职位名称", "开发小组");

List<String> errorList = new ArrayList<>();
boolean result = (Boolean)getRunner().execute(expression, context, errorList, true, false);

System.out.println("result :" + result);
System.out.println("errorList :" + JSON.toJSONString(errorList));
}

输出

1
2
3
4
A类违规天数90天内 == false and (虚假交易扣分 < 48 or 假冒扣分 <= 12) and 待整改卖家 < (总数*0.8) and (宝贝相符DSR > 4.6 or 职位名称 like '%开发%')
result :true
errorList :[" 虚假交易扣分:49 < 48 "," 宝贝相符DSR:4.0 > 4.6 "]

这里加上Runner的初始化

1
2
3
4
5
6
7
8
9
10
11
12
private ExpressRunner getRunner(){
ExpressRunner expressRunner = new ExpressRunner(true, false);
expressRunner.getOperatorFactory().getOperator("<").setErrorInfo("$1<$2");
expressRunner.getOperatorFactory().getOperator(">").setErrorInfo("$1>$2");
expressRunner.getOperatorFactory().getOperator("<=").setErrorInfo("$1<=$2");
expressRunner.getOperatorFactory().getOperator(">=").setErrorInfo("$1>=$2");
expressRunner.getOperatorFactory().getOperator("!=").setErrorInfo("$1!=$2");
expressRunner.getOperatorFactory().getOperator("<>").setErrorInfo("$1<>$2");
expressRunner.getOperatorFactory().getOperator("==").setErrorInfo("$1==$2");
expressRunner.getOperatorFactory().getOperator("like").setErrorInfo("$1like$2");
return expressRunner;
}

20220812更新

原版本不支持字符串的比较, 比如

1
2
3
4
5
6
Rule.RuleCondition condition3= new Rule.RuleCondition();
condition3.setName("职位名称");
condition3.setCode("职位名称");
condition3.setCompare(OperatorEnum.EQUAL.getSymbol());
condition3.setThresholdValueType(RuleValueTypeEnum.normal.name());
condition3.setThresholdValue("开发小组");

这种情况下, 拼接的表达式逻辑是 职位名称 == 开发小组 导致 QlExpress不能正确解析两边的内容(应该是把 开发小组 没有当成值)。
所以这里需要明确指定 开发小组 是变量,增加双引号。

RuleCondition类中增加对Value值的处理逻辑,目前没有好的方案,只是判断值是否是数值,不是数值的话都转成字符串(注意这种情况其实也会影响true/false的判断逻辑,所以true/false默认还是设置成String类型比较)。

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
@Getter
@Setter
public static class RuleCondition{

private String code;

/**
* {@link RuleValueTypeEnum}
*/
private String valueType;

/**
* {@link OperatorEnum}
*/
private String compare;

private String thresholdValueType;

private String thresholdCode;

private String thresholdArithmetic;

private String thresholdValue;

private String name ;

private List<String> enablePeriod;

public String getExpression(){

RuleValueTypeEnum typeEnum = RuleValueTypeEnum.of(thresholdValueType);
if(typeEnum == null){
throw new RuntimeException("invalid thresholdValueType");
}

StringBuilder express = new StringBuilder();
express.append(code).append(" ").append(compare).append(" ");

if(RuleValueTypeEnum.normal == typeEnum){
express.append(generateNormalValue(thresholdValue));
}else if(RuleValueTypeEnum.data_code == typeEnum){

if(thresholdArithmetic != null && thresholdValue != null ){
express.append("(").append(thresholdCode)
.append(thresholdArithmetic)
.append(thresholdValue)
.append(")");
}else{
express.append(thresholdCode);
}
}
return express.toString();
}

private String generateNormalValue(String thresholdValue){

boolean isNumber = NumberUtils.isCreatable(thresholdValue);

if(isNumber){
return thresholdValue;
}

return "\"" + thresholdValue + "\"";
}
}