基于Java实现的简易规则引擎(日常开发难点记录)

深入理解业务管理系统中的规则引擎实现

一、背景介绍

这段时间工作上,需要开发一个功能,可以动态选择表和字段形成一条条规则,然后规则又可以进行不通的组合,比如三条中至少满足一条并且满足另外一条这种刁钻的规则条件,具体功能描述可以看图。当时还真是为了实现这个功能考虑到头大,他难点在于既要实现规则的判断,又要满足下面条件的二次判断,好在也在同事的帮助下想出了思路,这里和大家分享下,后续类似地方都可以参考这个思路。

二、核心思路分析

1. 问题分析

在系统中,我们需要实现以下功能:

  • 支持灵活配置各种判定规则
  • 支持规则之间的逻辑组合(AND/OR)
  • 支持规则组的概念(设置最小 / 最大满足数量)
  • 支持可视化配置规则树

2. 设计思路

为了解决这些问题,我们采用了以下设计思路:

(1)数据模型设计

将规则系统分为两个核心部分:

  • 规则定义:存储具体的规则内容(如血压 > 140mmHg)
  • 规则配置:存储规则之间的逻辑关系(如规则 A AND 规则 B)
(2)树形结构设计

使用树形结构来表示规则之间的逻辑关系,支持三种节点类型:

  • RULE:具体的规则节点
  • OPERATOR:逻辑运算符节点(AND/OR)
  • GROUP:规则组节点
(3)递归处理机制

使用递归算法来处理树形结构,实现规则树的转换和解析。

三、核心代码讲解

1. 数据模型定义

(1)规则定义表(MonRuleDefinitions)
less 复制代码
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("MON_RULE_DEFINITIONS")
@ApiModel(value = "MonRuleDefinitions对象", description = "判定规则定义表")
public class MonRuleDefinitions extends Model<MonRuleDefinitions> {

    @ApiModelProperty(value = "主键")
    @TableId(value = "ID", type = IdType.ASSIGN_ID)
    private String id;

    @ApiModelProperty(value = "规则组id")
    @TableField("RULE_GROUP_ID")
    private String ruleGroupId;

    @ApiModelProperty(value = "来源字典编码【字典】;table_name")
    @TableField("FROM_DICT_CODE")
    private String fromDictCode;

    @ApiModelProperty(value = "具体字段编码")
    @TableField("SPECIFIC_FIELD_CODE")
    private String specificFieldCode;

    @ApiModelProperty(value = "匹配值代码【字典】;more_range")
    @TableField("MATCH_TYPE_CODE")
    private String matchTypeCode;

    @ApiModelProperty(value = "指标值")
    @TableField("ITEM_VALUE")
    private String itemValue;

    // ... 其他字段
}

代码分析

  • 使用 MyBatis-Plus 的注解来映射数据库表
  • 每个规则定义包含来源、字段、匹配类型、指标值等信息
  • 支持多种匹配类型(如大于、小于、等于)
(2)规则配置表(MonBaseRuleConfig)
less 复制代码
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("MON_RULE_CONFIG")
@ApiModel(value = "MonBaseRuleConfig对象", description = "判定规则配置表")
public class MonBaseRuleConfig extends Model<MonBaseRuleConfig> {

    @ApiModelProperty(value = "主键")
    @TableId(value = "ID", type = IdType.ASSIGN_ID)
    private String id;

    @ApiModelProperty(value = "规则类型 1-疾病分级 2-患者管理 3-质控")
    @TableField("RULE_TYPE")
    private String ruleType;

    @ApiModelProperty(value = "规则组id")
    @TableField("RULE_GROUP_ID")
    private String ruleGroupId;

    @ApiModelProperty(value = "父节点ID,用于构建树结构")
    @TableField("PARENT_NODE_ID")
    private String parentNodeId;

    @ApiModelProperty(value = "节点类型(RULE、OPERATOR、GROUP)")
    @TableField("NODE_TYPE")
    private String nodeType;

    @ApiModelProperty(value = "逻辑运算符(AND、OR);more_range")
    @TableField("OPERATOR")
    private String operator;

    @ApiModelProperty(value = "如果节点是规则,则关联规则ID")
    @TableField("RULE_ID")
    private String ruleId;

    @ApiModelProperty(value = "对于GROUP类型,最少满足的规则数量;item_result_unit_code")
    @TableField("MIN_COUNT")
    private String minCount;

    @ApiModelProperty(value = "对于GROUP类型,最多满足的规则数量")
    @TableField("MAX_COUNT")
    private String maxCount;

    // ... 其他字段
}

代码分析

  • 支持三种节点类型:RULE、OPERATOR、GROUP
  • 使用 parentNodeId 来构建树形结构
  • GROUP 类型支持设置最小 / 最大满足数量

2. 规则节点工具类(RuleNodeUtil)

(1)递归转换规则节点
scss 复制代码
public static void convertAndCollectNodes(RuleNodeDTO currentNode, String ruleGroupId,
                                          String parentNodeId, List<MonBaseRuleConfig> allNodes) {

    if ("RULE".equals(currentNode.getNodeType())&¤tNode.getRuleId() == null) {
        throw new BaseException(998,"规则节点缺少规则ID");
    }

    // 转换当前DTO为数据库实体
    MonBaseRuleConfig dbNode = new MonBaseRuleConfig();
    BeanUtil.copyProperties(currentNode, dbNode);
    dbNode.setRuleGroupId(ruleGroupId);
    
    // 特殊处理GROUP类型
    if ("GROUP".equals(currentNode.getNodeType())) {
        dbNode.setRuleId(String.join(",",(List<String>) currentNode.getRuleId()));
        if ("min".equals(currentNode.getCountType())){
            dbNode.setMinCount(currentNode.getCount());
            dbNode.setMaxCount("");
        }else {
            dbNode.setMaxCount(currentNode.getCount());
            dbNode.setMinCount("");
        }
    }
    
    allNodes.add(dbNode);

    // 递归处理子节点
    if (!CollectionUtils.isEmpty(currentNode.getChildren())) {
        for (RuleNodeDTO child : currentNode.getChildren()) {
            convertAndCollectNodes(child, ruleGroupId, currentNode.getId(), allNodes);
        }
    }
}

代码分析

  • 递归处理规则树,将前端传递的 DTO 转换为数据库实体
  • 特殊处理 GROUP 类型,支持设置最小 / 最大满足数量
  • 验证规则节点是否缺少规则 ID
(2)构建规则树
scss 复制代码
public static RuleNodeDTO buildNode(MonBaseRuleConfig currentNode, Map<String, List<MonBaseRuleConfig>> parentMap) {
    // 转换为DTO
    RuleNodeDTO nodeDTO = BeanUtil.copyProperties(currentNode,RuleNodeDTO.class);
    
    // 获取当前节点的子节点列表
    List<MonBaseRuleConfig> children = parentMap.getOrDefault(currentNode.getId(), new ArrayList<>());

    // 递归构建子节点
    List<RuleNodeDTO> childDTOs = children.stream()
            .map(child -> buildNode(child, parentMap))
            .collect(Collectors.toList());

    // GROUP节点特殊处理
    if ("GROUP".equals(currentNode.getNodeType())) {
        if (!childDTOs.isEmpty()) {
            List<Object> ruleIds = childDTOs.stream().map(RuleNodeDTO::getRuleId).collect(Collectors.toList());
            RuleNodeDTO ruleNodeDTO = childDTOs.get(0);
            ruleNodeDTO.setRuleId(ruleIds);
            nodeDTO.setChildren(Collections.singletonList(ruleNodeDTO));
        }else {
            nodeDTO.setChildren(childDTOs);
        }
    } else {
        // 非GROUP节点保留完整嵌套结构
        nodeDTO.setChildren(childDTOs);
    }

    return nodeDTO;
}

代码分析

  • 递归构建规则树,将数据库实体转换为前端需要的 DTO
  • 特殊处理 GROUP 类型,合并子节点的规则 ID
  • 保留完整的嵌套结构,方便前端展示

3. 规则配置服务实现(MonBaseRuleConfigServiceImpl)

(1)保存规则节点
scss 复制代码
public Integer saveNode(List<RuleNodeDTO> judgeGroup, String ruleGroupId, List<MonRuleDefinitions> ruleDefinitions) {
    if (CollectionUtils.isEmpty(judgeGroup)) {
        return 0;
    }
    
    // 收集所有转换后的数据库实体
    List<MonBaseRuleConfig> allNodes = new ArrayList<>();
    
    // 处理顶级节点(递归处理所有子节点)
    for (RuleNodeDTO rootNode : judgeGroup) {
        RuleNodeUtil.convertAndCollectNodes(rootNode, ruleGroupId, null, allNodes);
    }
    
    // 批量插入数据库
    if (!CollectionUtils.isEmpty(allNodes)) {
        // 删除旧的规则配置
        List<MonBaseRuleConfig> list = this.lambdaQuery()
                .eq(MonBaseRuleConfig::getRuleGroupId, ruleGroupId)
                .list();
        list.removeIf(judge ->allNodes.stream()
                .map(MonBaseRuleConfig::getId).collect(Collectors.toList()).contains(judge.getId()));
        List<String> monConfigDel = list.stream().map(MonBaseRuleConfig::getId).collect(Collectors.toList());
        this.removeByIds(monConfigDel);
        
        // 保存新的规则配置
        this.saveOrUpdateBatch(allNodes);
    }
    
    return allNodes.size();
}

代码分析

  • 批量保存规则节点,支持新增和更新
  • 先删除旧的规则配置,再保存新的规则配置
  • 支持规则组的概念,方便管理多个规则

四、使用场景

1. 疾病分级判定

scss 复制代码
高血压分级 = (收缩压 > 140mmHg OR 舒张压 > 90mmHg) AND 年龄 > 65岁

2. 患者管理规则

scss 复制代码
高危患者 = (糖尿病 AND 血压 > 130/80mmHg) OR (冠心病 AND 血脂异常)

3. 医疗质量控制

scss 复制代码
质控合格 = (病历书写完整率 > 95%) AND (随访及时率 > 90%) AND (患者满意度 > 85%)

五、总结

通过以上分析,我们可以看到这套规则引擎实现方案具有以下特点:

  1. 灵活性:支持任意复杂的规则组合
  2. 可扩展性:方便添加新的规则类型和逻辑
  3. 可视化:支持前端可视化配置规则树
  4. 复用性:规则可以被多个规则组复用

在实际应用中,我们可以根据具体业务需求,灵活配置各种判定规则,提高系统的灵活性和可维护性。

六、优化建议

  1. 性能优化:递归处理在规则树层级较深时可能存在性能问题,可以考虑使用迭代方式替代递归
  2. 事务管理:在批量保存规则节点时,建议添加事务管理,确保数据一致性
  3. 缓存机制:对于频繁使用的规则配置,可以添加缓存机制,减少数据库查询
  4. 规则验证:在保存规则前添加更严格的验证逻辑,确保规则配置的合法性
typescript 复制代码
// 优化建议:添加事务管理
@Transactional(rollbackFor = Exception.class)
public Integer saveNode(List<RuleNodeDTO> judgeGroup, String ruleGroupId, List<MonRuleDefinitions> ruleDefinitions) {
    // ... 原有代码
}

通过以上优化,可以进一步提高规则引擎的性能和可靠性。

相关推荐
葫芦和十三1 天前
图解 MongoDB 21|选举与 failover:Primary 是怎么选出来的
后端·mongodb·agent
GetcharZp1 天前
26k Star 开源内网穿透神器 NetBird,一分钟实现全球设备互联!
后端
考虑考虑1 天前
Mybatis实现批量插入
java·后端·mybatis
咖啡八杯1 天前
GoF设计模式——中介者模式
java·后端·spring·设计模式
lizhongxuan1 天前
多Agent之间的区别
后端
青石路1 天前
记一次多JDK版本问题的排查,一坑套一坑,差点没爬上来
java
杨充1 天前
1.面向对象设计思想
后端
IT_陈寒1 天前
Java的Date类又坑了我一次,改用时间戳真香
前端·人工智能·后端
systemPro1 天前
2.6亿条设备数据,历史查询从超时到50ms,我做了什么
后端
要阿尔卑斯吗1 天前
提示词优化启示:为什么“按顺序输出“比“关键度评分“更有效
后端