个人博客:无奈何杨(wnhyang)
个人语雀:wnhyang
共享语雀:在线知识共享
Github:wnhyang - Overview
参考
https://juejin.cn/post/7005869798483558431
🍉组件参数 | LiteFlow
🍍组件标签 | LiteFlow
https://gitee.com/freshday/radar/wikis/home
条件配置
在需要灵活配置规则的业务系统中,如上图这样的条件配置非常常见。
风控系统之普通规则条件,使用LiteFlow实现
风控系统之通用规则条件设计,算术单元/逻辑单元/函数式接口
不管是在第一个参考链接(https://juejin.cn/post/7005869798483558431)里或是从自己的分析中都可以知道条件配置就是对同一逻辑单元的与或非编排。对于LiteFlow
相同组件的编排有组件标签和组件参数两种解决方法。
组件标签实现
通过组件标签实现,就是在编排LiteFlow
时加上组件tag
。
那么就将条件存储在数据库中,在组件运行时通过tag
再到数据库中去取。
id | type | value | logic_type | expect_type | expect_value |
---|---|---|---|---|---|
1 | normal | N_S_appName | eq | input | phone |
2 | normal | N_S_payerAccount | eq | input | 123456 |
3 | normal | N_F_transAmount | gt | input | 15 |
4 | normal | N_S_appName | eq | input | phone |
5 | normal | N_F_transAmount | lt | input | 100 |
6 | normal | N_F_transAmount | gte | input | 20.0 |
7 | normal | N_S_appName | eq | context | N_S_ipCity |
8 | zb | 1 | gt | input | 20 |
如:IF(AND(c_cn.tag("4"),c_cn.tag("5")),r_tcn.tag("2"),r_fcn);
就是对条件组件c_cn.tag("id")
进行与或非编排,就能几乎适用于所有条件。
create table de_condition
(
id bigint auto_increment comment '主键' primary key,
type varchar(20) default 'normal' not null comment '类型',
value varchar(32) default '' not null comment '操作对象',
logic_type varchar(32) default 'null' not null comment '逻辑类型',
expect_type varchar(32) default 'input' not null comment '期望值类型',
expect_value varchar(32) default '' not null comment '期望值',
description varchar(64) charset utf8mb4 default '' null comment '描述',
creator varchar(64) charset utf8mb4 default '' null comment '创建者',
create_time datetime default CURRENT_TIMESTAMP not null comment '创建时间',
updater varchar(64) charset utf8mb4 default '' null comment '更新者',
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP comment '更新时间',
deleted bit default b'0' not null comment '是否删除'
)comment '规则条件表';
忽略公共字段,实际上有用的只有type
、value
、logicType
、expect_type
、expect_value
。
/**
* @author wnhyang
* @date 2024/6/18
**/
public interface ConditionType {
/**
* 普通条件
*/
String NORMAL = "normal";
/**
* 指标条件
*/
String ZB = "zb";
/**
* 正则条件
*/
String REGULAR = "regular";
/**
* 名单条件
*/
String LIST = "list";
/**
* 脚本条件
*/
String SCRIPT = "script";
}
与type
相对,value
代表的意义不同,type
为normal
时,value
是系统字段名;为zb
时,value
是指标id
,等等,可参考以下组件示例代码。
/**
* @author wnhyang
* @date 2024/4/3
**/
@AllArgsConstructor
@Getter
public enum LogicType {
NULL("null"),
NOT_NULL("not_null"),
EQ("eq"),
NOT_EQ("not_eq"),
GT("gt"),
GTE("gte"),
LT("lt"),
LTE("lte"),
CONTAINS("contains"),
NOT_CONTAINS("not_contains"),
PREFIX("prefix"),
NOT_PREFIX("not_prefix"),
SUFFIX("suffix"),
NOT_SUFFIX("not_suffix"),
/**
* 适用正则、名单条件
*/
MATCH("match"),
/**
* 适用正则、名单条件
*/
NOT_MATCH("not_match"),
/**
* 适用正则条件
*/
MATCH_IGNORE_CASE("match_ignore_case"),
/**
* 适用正则条件
*/
NOT_MATCH_IGNORE_CASE("not_match_ignore_case");
private final String type;
public static LogicType getByType(String type) {
for (LogicType logicType : LogicType.values()) {
if (logicType.getType().equals(type)) {
return logicType;
}
}
return null;
}
}
logicType
取值如上,同样,根据type
和value
的不同,逻辑适用的也不同,具体不同参考以下普通字段类型和逻辑函数式接口的实现。
/**
* @author wnhyang
* @date 2024/5/8
**/
public interface ExpectType {
/**
* 常量
*/
String INPUT = "input";
/**
* 上下文
*/
String CONTEXT = "context";
/**
* 名单集
*/
String LIST_SET = "listSet";
}
expectType
有上,同样适用于不同条件。
字段
字段命名规则为[N/D]_[S/N/F/D/E/B]_{name}
。
N
普通字段,D
动态字段;S/N/F/D/E/B
字段类型;name
字段名。
如:N_S_appName
表示普通字符字段appName
;N_D_transTime
表示普通日期字段transTime
。
/**
* @author wnhyang
* @date 2024/3/13
**/
@AllArgsConstructor
@Getter
public enum FieldType {
/**
* 字符型,支持【等于、不等于、包含、不包含、前缀、非前缀、后缀、非后缀、为空、不为空、存在于、不存在于】
*/
STRING("S"),
/**
* 整数型,支持【等于、不等于、大于、小于、大于等于、小于等于、为空、不为空、存在于,不存在于】
*/
NUMBER("N"),
/**
* 小数型,支持【等于、不等于、大于、小于、大于等于、小于等于、为空、不为空】
*/
FLOAT("F"),
/**
* 日期型,支持【等于、不等于、大于、小于、大于等于、小于等于、为空、不为空】
*/
DATE("D"),
/**
* 枚举型,支持【等于、不等于、为空、不为空】
*/
ENUM("E"),
/**
* 布尔型,支持【等于、不等于、为空、不为空】
*/
BOOLEAN("B");
private final String type;
public static FieldType getByType(String type) {
for (FieldType value : values()) {
if (value.getType().equals(type)) {
return value;
}
}
return null;
}
public static FieldType getByFieldName(String fieldName) {
if (fieldName.length() >= 3) {
String sub = StrUtil.sub(fieldName, 2, 3);
return getByType(sub);
}
return null;
}
}
逻辑运算
/**
* @author wnhyang
* @date 2024/6/14
**/
@FunctionalInterface
public interface LogicOp<T> {
boolean apply(T a, LogicType logicType, T b);
}
逻辑运算实现有下。
/**
* @author wnhyang
* @date 2024/5/16
**/
@Slf4j
public class FunUtil {
private FunUtil() {
}
public static final FunUtil INSTANCE = new FunUtil();
public LogicOp<String> stringLogicOp = (a, logicType, b) -> switch (Objects.requireNonNull(logicType)) {
case NULL -> StrUtil.isBlank(a);
case NOT_NULL -> !StrUtil.isBlank(a);
case EQ -> a.equals(b);
case NOT_EQ -> !a.equals(b);
case CONTAINS -> a.contains(b);
case NOT_CONTAINS -> !a.contains(b);
case PREFIX -> a.startsWith(b);
case NOT_PREFIX -> !a.startsWith(b);
case SUFFIX -> a.endsWith(b);
case NOT_SUFFIX -> !a.endsWith(b);
default -> false;
};
public LogicOp<Integer> integerLogicOp = (a, logicType, b) -> switch (Objects.requireNonNull(logicType)) {
case NULL -> a == null;
case NOT_NULL -> a != null;
case EQ -> a.equals(b);
case NOT_EQ -> !a.equals(b);
case LT -> a < b;
case LTE -> a <= b;
case GT -> a > b;
case GTE -> a >= b;
default -> false;
};
public LogicOp<Double> doubleLogicOp = (a, logicType, b) -> switch (Objects.requireNonNull(logicType)) {
case NULL -> a == null;
case NOT_NULL -> a != null;
case EQ -> a.equals(b);
case NOT_EQ -> !a.equals(b);
case LT -> a < b;
case LTE -> a <= b;
case GT -> a > b;
case GTE -> a >= b;
default -> false;
};
public LogicOp<LocalDateTime> dateLogicOp = (a, logicType, b) -> switch (Objects.requireNonNull(logicType)) {
case NULL -> a == null;
case NOT_NULL -> a != null;
case EQ -> a.equals(b);
case NOT_EQ -> !a.equals(b);
case LT -> a.isBefore(b);
case LTE -> a.isBefore(b) || a.equals(b);
case GT -> a.isAfter(b);
case GTE -> a.isAfter(b) || a.equals(b);
default -> false;
};
public LogicOp<String> enumLogicOp = (a, logicType, b) -> switch (Objects.requireNonNull(logicType)) {
case NULL -> a == null;
case NOT_NULL -> a != null;
case EQ -> a.equals(b);
case NOT_EQ -> !a.equals(b);
default -> false;
};
public LogicOp<Boolean> booleanLogicOp = (a, logicType, b) -> switch (Objects.requireNonNull(logicType)) {
case NULL -> a == null;
case NOT_NULL -> a != null;
case EQ -> a.equals(b);
case NOT_EQ -> !a.equals(b);
default -> false;
};
public LogicOp<String> regularLogicOp = (a, logicType, b) -> switch (Objects.requireNonNull(logicType)) {
case MATCH -> ReUtil.isMatch(b, a);
case MATCH_IGNORE_CASE -> ReUtil.isMatch("(?i)" + b, a);
case NOT_MATCH -> !ReUtil.isMatch(b, a);
case NOT_MATCH_IGNORE_CASE -> !ReUtil.isMatch("(?i)" + b, a);
default -> false;
};
}
这样做是为了简化条件组件代码,抽象一些实现。
条件组件|组件标签
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS_BOOLEAN, nodeId = LFUtil.CONDITION_COMMON_NODE, nodeType = NodeTypeEnum.BOOLEAN)
public boolean cond(NodeComponent bindCmp) {
// 获取当前tag
String tag = bindCmp.getTag();
// 获取当前tag对应的条件
Condition condition = conditionMapper.selectById(tag);
// 获取上下文
AccessRequest accessRequest = bindCmp.getContextBean(AccessRequest.class);
String type = condition.getType();
boolean cond = false;
LogicType byType = LogicType.getByType(condition.getLogicType());
try {
// 普通条件,适用指标、规则
if (ConditionType.NORMAL.equals(type)) {
// 获取条件字段
String fieldName = condition.getValue();
FieldType fieldType = FieldType.getByFieldName(fieldName);
String expectValue = condition.getExpectValue();
if (ExpectType.CONTEXT.equals(condition.getExpectType())) {
expectValue = accessRequest.getStringData(expectValue);
}
if (fieldType == null || byType == null) {
return false;
}
switch (fieldType) {
case STRING:
String stringData = accessRequest.getStringData(fieldName);
log.debug("字段值:{}, 操作:{}, 期望值:{}", stringData, byType, expectValue);
cond = FunUtil.INSTANCE.stringLogicOp.apply(stringData, byType, expectValue);
break;
case NUMBER:
Integer numberData = accessRequest.getNumberData(fieldName);
Integer expectInteger = Integer.parseInt(expectValue);
log.debug("字段值:{}, 操作:{}, 期望值:{}", numberData, byType, expectInteger);
cond = FunUtil.INSTANCE.integerLogicOp.apply(numberData, byType, expectInteger);
break;
case FLOAT:
Double floatData = accessRequest.getFloatData(fieldName);
Double expectDouble = Double.parseDouble(expectValue);
log.debug("字段值:{}, 操作:{}, 期望值:{}", floatData, byType, expectDouble);
cond = FunUtil.INSTANCE.doubleLogicOp.apply(floatData, byType, expectDouble);
break;
case DATE:
LocalDateTime dateData = accessRequest.getDateData(fieldName);
LocalDateTime expectDateTime = LocalDateTimeUtil.parse(expectValue, DatePattern.NORM_DATETIME_FORMATTER);
log.debug("字段值:{}, 操作:{}, 期望值:{}", dateData, byType, expectDateTime);
cond = FunUtil.INSTANCE.dateLogicOp.apply(dateData, byType, expectDateTime);
break;
case ENUM:
String enumData = accessRequest.getEnumData(fieldName);
log.debug("字段值:{}, 操作:{}, 期望值:{}", enumData, byType, expectValue);
cond = FunUtil.INSTANCE.enumLogicOp.apply(enumData, byType, expectValue);
break;
case BOOLEAN:
Boolean booleanData = accessRequest.getBooleanData(fieldName);
log.debug("字段值:{}", booleanData);
cond = FunUtil.INSTANCE.booleanLogicOp.apply(booleanData, byType, Boolean.parseBoolean(expectValue));
break;
}
} else if (ConditionType.ZB.equals(type)) {
log.info("指标条件");
String indicatorId = condition.getValue();
IndicatorContext indicatorContext = bindCmp.getContextBean(IndicatorContext.class);
String indicatorValue = indicatorContext.getIndicatorValue(Long.valueOf(indicatorId));
String expectValue = condition.getExpectValue();
if (ExpectType.CONTEXT.equals(condition.getExpectType())) {
expectValue = accessRequest.getStringData(expectValue);
}
cond = FunUtil.INSTANCE.doubleLogicOp.apply(Double.parseDouble(indicatorValue), byType, Double.valueOf(expectValue));
} else if (ConditionType.REGULAR.equals(type)) {
log.info("正则条件");
String fieldName = condition.getValue();
String stringData = accessRequest.getStringData(fieldName);
cond = FunUtil.INSTANCE.regularLogicOp.apply(stringData, byType, condition.getExpectValue());
} else if (ConditionType.LIST.equals(type)) {
log.info("名单条件");
String fieldName = condition.getValue();
String stringData = accessRequest.getStringData(fieldName);
// 查名单集做匹配
cond = listDataService.hasListData(Long.valueOf(condition.getExpectValue()), stringData);
} else if (ConditionType.SCRIPT.equals(type)) {
// TODO 脚本条件
log.info("脚本条件");
} else {
log.error("未知条件类型:{}", type);
}
} catch (Exception e) {
log.error("条件:{}, 运行异常:{}", condition, e.getMessage());
}
return cond;
}
组件参数实现
组件参数与组件标签不同,数据就存储在EL
表达式中。
与上面组件标签表达式IF(AND(c_cn.tag("4"),c_cn.tag("5")),r_tcn.tag("2"),r_fcn);
对应的组件参数表达式是:
IF(
AND(
c_cn.tag('{"type":"normal","value":"N_S_appName","logic_type":"eq","expect_type":"input","expect_value":"Phone"}'),
c_cn.tag('{"type":"normal","value":"N_F_transAmount","logic_type":"lt","expect_type":"input","expect_value":"100"}')
),
r_tcn.tag("2"),
r_fcn
);
这样在条件组件中就不用再通过tag再次查询数据库来获取数据了。
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS_BOOLEAN, nodeId = LFUtil.CONDITION_COMMON_NODE, nodeType = NodeTypeEnum.BOOLEAN)
public boolean cond(NodeComponent bindCmp) {
// 获取当前tag
String tag = bindCmp.getTag();
// 获取当前tag对应的条件
Condition condition = conditionMapper.selectById(tag);
如上的组件标签组件实现替换为以下方法就好。
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS_BOOLEAN, nodeId = LFUtil.CONDITION_COMMON_NODE, nodeType = NodeTypeEnum.BOOLEAN)
public boolean cond(NodeComponent bindCmp) {
Condition cmpData = bindCmp.getCmpData(Condition.class);
而且对于这样的条件配置几乎不会复用,都是配置即存在删除即无,省去了删除条件配置同时要在数据库中删除多条条件数据的繁琐步骤。对于其缺点,可能就是在需要反向查找时,有点麻烦。因为绝大多数时候都是使用条件配置确认最终的条件,但是如果反向要查找某字段、指标、名单集被引用的地方,就比较麻烦了,这就需要从Chain
解析到所有条件,然后再判断,不如数据库中直接能找到引用的。
EL与或非表达式与条件json的正反向解析
上面都是关于后端条件数据存储的一点想法,但其最终到前端都是要成为统一的结构化数据的。
配置的规则条件总要反显到前端吧,前端条件json
总要送给后端用于组成EL
表达式吧。
如果有这样的条件类,其中logicOp
取值有AND|OR|NOT
,其与children
共生共存,与其他条件字段互斥,即logicOp
和children
要么都有值要么都为空,当其有值时其他条件字段为空,当其为空时,其他一定有值,表示具体的逻辑条件。
还有一点要注意AND
和OR
是接受复合子条件的,而NOT
仅支持单个子条件。
@Data
public class Cond implements Serializable {
@Serial
private static final long serialVersionUID = -1831587613757992692L;
/**
* AND|OR|NOT
*/
private String logicOp;
private List<Cond> children;
/**
* 条件类型
*/
private String type;
/**
* 操作对象
*/
private String value;
/**
* 操作类型
*/
private String logicType;
/**
* 期望类型
*/
private String expectType;
/**
* 期望值
*/
private String expectValue;
/**
* 描述
*/
private String description;
}
在以下示例中并不完善,使用仅是字符串的处理,大家可以尝试一下ElBus
的构建,而且没有正确性校验,仅供参考。
其提供了条件类到EL
表达式的构建和EL
表达式反向解析到类的方法。
/**
* @author wnhyang
* @date 2024/7/18
**/
public class LFUtil {
private static final ObjectMapper objectMapper = new ObjectMapper();
static {
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.registerModules(buildJavaTimeModule());
}
private static String[] splitExpressions(String expression) {
List<String> parts = new ArrayList<>();
int level = 0;
int startIndex = 0;
for (int i = 0; i < expression.length(); i++) {
char c = expression.charAt(i);
if (c == '(') {
level++;
} else if (c == ')') {
level--;
} else if (c == ',' && level == 0) {
parts.add(expression.substring(startIndex, i));
startIndex = i + 1;
}
}
parts.add(expression.substring(startIndex));
return parts.toArray(new String[0]);
}
@SneakyThrows
private static String buildCondEl(Cond cond) {
if (cond != null) {
if (cond.getLogicOp() != null && cond.getChildren() != null && !cond.getChildren().isEmpty()) {
List<String> expressions = cond.getChildren().stream()
.map(LFUtil::buildCondEl)
.collect(Collectors.toList());
return cond.getLogicOp() + "(" + String.join(", ", expressions) + ")";
} else {
return "c_cn.data('" + objectMapper.writeValueAsString(cond) + "')";
}
}
return "";
}
public static Cond parseToCond(String expression) throws Exception {
expression = expression.replaceAll("\s+", "");
return parseExpressionToCond(expression);
}
private static Cond parseExpressionToCond(String expression) throws Exception {
if (expression.startsWith("AND(")) {
return parseLogicExpression("AND", expression.substring(4, expression.length() - 1));
} else if (expression.startsWith("OR(")) {
return parseLogicExpression("OR", expression.substring(3, expression.length() - 1));
} else if (expression.startsWith("NOT(")) {
return parseLogicExpression("NOT", expression.substring(4, expression.length() - 1));
} else {
return parseVariable(expression);
}
}
private static Cond parseVariable(String variableExpression) throws Exception {
if (variableExpression.contains(".data('")) {
int dataIndex = variableExpression.indexOf(".data('");
String jsonData = variableExpression.substring(dataIndex + 7, variableExpression.length() - 2).trim();
return objectMapper.readValue(jsonData, Cond.class);
}
return new Cond();
}
private static Cond parseLogicExpression(String operator, String subExpression) throws Exception {
Cond cond = new Cond();
cond.setLogicOp(operator);
cond.setChildren(new ArrayList<>());
String[] subExpressions = splitExpressions(subExpression);
for (String subExp : subExpressions) {
cond.getChildren().add(parseExpressionToCond(subExp.trim()));
}
return cond;
}
public static void main(String[] args) throws Exception {
Cond cond = new Cond();
cond.setLogicOp("AND");
List<Cond> children = new ArrayList<>();
children.add(new Cond().setType("normal").setValue("N_S_appName").setLogicType("eq").setExpectType("input").setExpectValue("Phone"));
children.add(new Cond().setType("normal").setValue("N_F_transAmount").setLogicType("lt").setExpectType("input").setExpectValue("100"));
cond.setChildren(children);
String condEl = buildCondEl(cond);
System.out.println(condEl);
System.out.println(parseToCond(condEl));
}
}
测试结果
AND(c_cn.data('{"type":"normal","value":"N_S_appName","logicType":"eq","expectType":"input","expectValue":"Phone"}'), c_cn.data('{"type":"normal","value":"N_F_transAmount","logicType":"lt","expectType":"input","expectValue":"100"}'))
Cond(logicOp=AND, children=[Cond(logicOp=null, children=null, type=normal, value=N_S_appName, logicType=eq, expectType=input, expectValue=Phone, description=null), Cond(logicOp=null, children=null, type=normal, value=N_F_transAmount, logicType=lt, expectType=input, expectValue=100, description=null)], type=null, value=null, logicType=null, expectType=null, expectValue=null, description=null)
元数据管理
🫔查看指定规则下的所有组件 | LiteFlow
在v2.12.0
中加入的元数据管理,可以通过FlowBus
获取Chain
和Node
的信息。
如下面这样的chain
,可以通过FlowBus.getChain
和FlowBus.getNodesByChainId
分别获取Chain
和Chain
对应的组件Node
。
<chain name="R_C#1">
THEN(IF(
AND(c_cn.data('{"id":"1","name":"wnhyang","phoneNumber":"123456789"}'),c_cn.data('{"id":"1","name":"wnhyang","phoneNumber":"123456789"}'),NOT(c_cn.data('{"id":"1","name":"wnhyang","phoneNumber":"123456789"}'))),a,b),IF(
AND(c_cn.data('{"id":"1","name":"wnhyang","phoneNumber":"123456789"}'),c_cn.data('{"id":"1","name":"wnhyang","phoneNumber":"123456789"}')),a,b));
</chain>
测试Debug
一下。
@Test
void test3() {
Chain chain = FlowBus.getChain("R_C#1");
log.info("chain: {}", chain);
List<Node> nodesByChainId = FlowBus.getNodesByChainId("R_C#1");
log.info("nodesByChainId: {}", nodesByChainId);
}
Chain
的所有信息都是有的,不过这个就要求对于LiteFlow
的Chain
和Node
设计理解比较深了。
相比于直接获取Chain
,获取Node
看起来就简单多了,尤其对于我们在文档中常见的组件标签和组件参数,一览无遗。
总结
最后总结一下组件标签和组件参数在以上应用中所带来的差别,简称方式1和2了,下面👇
其实其带来的差别只有方式1
多了一张条件表,所以差别就在这里,废话🤪
对于创建EL
:方式1
需要多创建一条数据,方式2
就避免这样的麻烦,直接放在EL
就好了。
对于更新EL
:如果当作全新的条件,方式1
要先删除原有条件,再插入新的条件;如果要对比新老后再修改,那方式1
就更麻烦了。而对于方式2
,不管是怎样,直接覆盖就好了。
对于删除EL
:方式1
需要删除所有的条件数据,方式2
就不会有这些烦恼。
对于运行组件:方式1
需要多查一步数据库,方式2
更简单了使用获取组件方法就好,而且支持直接转类型,很方便。
从上面对比来看方式2
爆杀啊!但还没对比完呢!
当需要反向查找时,方式2
就有劣势了。反向查找指的是需要查询上面设计条件里的字段、指标、名单集的引用时,方式1
可以方便的直接到数据库层查找引用的EL
,而方式2
就漫无目的了,不经其他设计的话,只能全部EL
遍历了。
写在最后
拙作艰辛,字句心血,望诸君垂青,多予支持,不胜感激。
个人博客:无奈何杨(wnhyang)
个人语雀:wnhyang
共享语雀:在线知识共享
Github:wnhyang - Overview