# REC-规则引擎组件
**Repository Path**: loopstack/rec
## Basic Information
- **Project Name**: REC-规则引擎组件
- **Description**: REC(Rule Engine Component)规则引擎组件:提供统一的规则处理方式和策略。支持SDK接入;只需要进行页面配置即可完成规则的处理,支持简单模式和复杂模式进行编辑。前后端分离项目,可独立部署,也可以前后端打包部署。体验地址:https://rec.loopstack.icanci.cn/
此项目提供最基础的规则引擎实现,可以对其进行二次迭代增强开发。
- **Primary Language**: Java
- **License**: Apache-2.0
- **Default Branch**: master
- **Homepage**: https://rec.loopstack.icanci.cn/
- **GVP Project**: No
## Statistics
- **Stars**: 65
- **Forks**: 27
- **Created**: 2022-07-06
- **Last Updated**: 2025-06-09
## Categories & Tags
**Categories**: Uncategorized
**Tags**: 规则引擎, VueBoot, Netty
## README
# REC-规则引擎组件
## 介绍
- REC(Rule Engine Component)规则引擎组件:提供统一的规则处理方式和策略。支持SDK接入、HTTP接入;只需要进行页面配置即可完成规则的处理,支持简单模式和复杂模式进行编辑。前后端分离项目,可独立部署,也可以前后端打包部署。
## 项目模块说明
- rec-admin:后台管理模块
- rec-admin-biz:业务模块
- rec-admin-dal:数据库管理模块
- rec-admin-views:视图模块
- rec-admin-web:对前端接口模块
- rec-common:基础模型、枚举模块、DTO模型、规则聚合等
- rec-engine:规则引擎处理器
- rec-engine-script:规则引擎脚本处理器
- rec-engine-sdk:规则引擎SDK、SpringBootStarter模块(此处可能不太优雅)
- rec-engine-sdk-http:规则引擎SDK的SPI实现,加载数据,HTTP实现
- rec-spi:内部事件机制
## 什么是规则引擎
- 当我们在对复杂的业务进行开发的时候,程序本身逻辑代码和业务代码`相互嵌套、耦合`,与此同时,`维护成本比较高,可扩展性也比较差`,我们现在想要举一个营销活动的例子,假如双十一活动,针对某些会员(比如白银会员、黄金会员、白金会员)
,每个会员级别可以领取的抵扣券是不一样的,同时每个会员的满减也是不一样的,也就是我们的运营人员在进行活动配置的时候,需要根据不同的条件组合完成响应的逻辑。这些配置是高度灵活、修改是经常发生的,并且每个时间的活动规则都可能不一样,而在活动进行的过程中,不可能随时修改代码,去处理这种动态的数据变更,所以就需要使用规则引擎。
- 总结下来就是:规则引擎是可降低复杂业务逻辑组件复杂性、降低应用程序的维护和可扩展性成本的组件。
- 下面用一张图来表示`**输入输出**`关系

- 我们在`**业务节点**`触发规则引擎的判断,然后对`**规则引擎**`输入`**规则集合簇**`和`**输入参数**`,经过`**规则引擎计算**`,输入`**执行结果**`,然后根据执行结果进行相应的`业务逻辑处理`,这样的话,每个业务节点就可定制业务规则,交给规则引擎去执行判断,然后拿到结果之后判断是否需要执行。
- 那么通过上述的介绍,相信你就可以了解规则引擎的作用了,那么在实际的业务开发中,我们经常会遇到这种场景,就需要规则引擎进行规则定制执行,来满足业务的诉求。
## 设计理念
- 方便运营进行操作,只需页面配置,然后发布,即可动态变更数据。
- 而对于页面的操作,将具备完善的文档,方便快速接入,并且理解配置,使得配置不具备复杂性。
- 可以通过SDK的方式进行接入,支持服务端、客户端通信进行处理。
## 规则引擎要素
### 域
- **域**作为一个业务单元的概念,尤其是在分布式系统中,域可作为原子系统的边界,也可以理解为每个单独的应用,是一个`**业务域**`
- 每个域的数据相互隔离,在进行数据发送的时候,按照域进行加载和发送,之后的所有配置都依托域来进行配置
### 场景
- 域下业务节点,比如淘宝买东西,业务节点有:下单成功、物流通知、收货成功、评价等等,这些都是域中,也就是应用中的业务节点,那么这些业务节点可以称之为**场景**,那么`**域加上场景**`,就确定了一个唯一的业务节点。既然可以确定唯一的业务节点,就可以进行接下来的配置
### 元数据
- 元数据是一些基本的数据,比如常用的,不会经常变更的,其类似于Java中的枚举,是一组数据的集合,用来归类一些通用的配置项
### 操作符
- 操作符即比较符,是一些数学上的概念,比如大于、小于、等于、不等于、包含、不包含,通过操作符,判断左右值,即可判断出当前执行的是否成功,从而判断是否执行下面的节点
### 基础数据
- 基础数据是取值上的概念,通过多种脚本执行引擎,可以通过编写脚本来进行取值,来和目标值进行比较运算
### 数据源
- 数据源,在执行规则判断的时候,可能存在一些计算得到的数据源,并不是仅仅传参,这样就可以动态的组织起始数据
### 策略组
- 策略组是多组策略的集合
### 决策树
- 对策略组进行编排,构建决策树,因为策略组具备多个可能的执行流程分支,执行分支可能是且、或、存在等的关系,通过决策树执行最后的结果,可定义简单返回类型,然后后续业务逻辑会根据返回类型进行决断
## 规则引擎操作页面
- 操作页面按照规则引擎要素进行划分,分为域配置、场景配置、元数据配置、基础数据配置、策略组配置,具体的页面UI等开发完毕之后进行贴图即可
### 域配置
- 域的作用主要是用来划分业务系统和原子系统,本质上是个聚合的分布式系统上下文的概念。在域中,可以配置场景,并且发布等,发布是通知客户端进行更新最新的数据


### 场景配置
- 场景依托于域存在,场景指的是这个业务系统的某些需要动态配置的节点,比如物理系统中的消息通知等等,可在场景配置中配置

### 元数据配置
- 元数据是一类Key-Value的集合,对应映射到Java语言中,应是枚举,这些是一些不会经常变更、比较稳定的数据


### 基础数据配置
- 基础数据是根据指定的入参或者无参进行取值,取值的方式通过脚本取值,这样就能满足动态的取值逻辑,但是也会有接入复杂度,可能脚本会写的比较复杂,并且有进程风险,因为脚本完全是动态的



### 数据源配置
- 数据源指的是规则执行的前置数据,当然了,客户端也可以传入数据,系统会自动对数据进行聚合,优先以传入的数据为准,进行合并数据,然后将合并的数据交给规则引擎处理
- 获取值的方式支持HTTP、Groovy、Mvel2.0等



### 策略组
- 策略组就是一些配置项目,真正进行规则判断的地方,现在只支持简单的配置类型,但是支持复杂规则模式,简单规则模式只会返回布尔值,复杂规则模式支持执行中断,并返回目标值



## 规则引擎单节点执行逻辑
- 如下图所示
- 业务节点触发规则执行,执行的时候先判断一些数据的验证逻辑,通过之后才能将数据推到`Rule Engine Calculation`进行执行,在此过程中判断当前业务节点的决策树是否构建完毕,如果构建完毕,则从本地缓存中取出数据,否则重新构建决策树,然后存储到本地缓存执行,最终返回执行结果

## 技术与架构选型
- 存储技术选型:MongoDB。因为是一些配置的数据,有一些的数据可能经常的发生改动,使用MySQL不方便经常的变更数据结构,因此使用文档数据库MongoDB
- 开发框架:SpringBoot 2.2.2.RELEASE、Vue2.0、Netty4.x、Vue-Template-Admin
- 数据请求:Hutool Http
- 脚本执行引擎:Groovy、JavaScript、Mvel2.0等
- 分布式锁实现:基于MongoDB实现
## 架构设计与落地
- 架构主要分为两个方面,一方面是页面的规则配置,另外一方面是仓储(本地缓存,JVM进程级别的缓存)的设计
- 页面进行操作,然后可选择域进行发布,发布之后就会更新对应域的仓储数据
- 接入方可以选择使用SDK方式的接入,但是这种接入方式限制了开发语言为Java;也可以使用HTTP接口的方式提供服务(单独开启服务加载所有域的数据);
- 但是使用HTTP接口的方式,需要注意序列化和反序列化问题,因为在JVM进程中,SDK接入的方式,进行脚本执行的时候,是使用的指定对象,但是经过序列化和反序列化之后,对象可能发生了变化,导致执行的结果和预期不一致
- 实际上,在页面上进行测试的时候,使用的是String类型的JSON数据,本质上也是序列化执行的,如果能够保证最终执行数据一致,那么则不会有问题
- 通过SDK的方式需要在当前JVM进程加载当前域所在的配置数据;而通过HTTP接口的方式,则会在HTTP服务端加载所有仓储的数据。当然在更新的时候也是按需更新
- 当配置触发发布的时候,会将数据推送到触发线程队列,然后由线程池消费队列,触发客户端的消息更新

## 数据表设计
- 数据表设计,根据规则引擎要素可以发现聚合了6张表,实际上域和场景应该是绑定到一起的,可以合并,也可以考虑分开,此处选择分开存储
- 在数据设计的过程中,有些数据是固化的,每6张表数据都有的,如下表,因此这些字段不再单独列出,具体的可在存储的数据结构中看到
| **字段名称** | **类型** | **备注** |
| --- | --- | --- |
| id | object(String) | mongodb 自带id |
| uuid | String | 雪花算法随机UUID |
| desc | String | 功能描述 |
| createTime | Date | 创建时间 |
| updateTime | Date | 更新时间 |
| isDelete | int | 状态 1无效,0有效 |
| env | String | 环境 |
- 域数据存储设计(文档名称:rec-domain)
| **字段名称** | **类型** | **备注** |
| --- | --- | --- |
| domainName | String | 域名称 |
| domainCode | String | 域Code |
- 域场景(文档名称:rec-scene)
| **字段名称** | **类型** | **备注** |
| --- | --- | --- |
| domainCode | String | 域Code |
| scenePairs | List | 场景对 |
- 元数据(文档名称:rec-metadata)
| **字段名称** | **类型** | **备注** |
| --- | --- | --- |
| domainCode | String | 域Code |
| metadataName | String | 元数据名称 |
| metadataPairs | List | 元数据枚举值 |
- 基础数据(文档名称:rec-basedata)
| **字段名称** | **类型** | **备注** |
| --- | --- | --- |
| domainCode | String | 域Code |
| fieldName | String | 基础数据名称 |
| dataType | String | 数据类型(布尔、字符串、数值、日期、元数据等) |
| metadataUuid | String | 关联的元数据uuid |
| scriptType | String | 脚本执行类型 |
| scriptContent | String | 脚本内容 |
| resultType | String | 脚本执行返回类型(只能是基本数据类型) |
- 数据源(文档名称:rec-dataSource)
| **字段名称** | **类型** | **备注** |
| --- | --- | --- |
| domainCode | String | 域Code |
| dataSourceName | String | 数据源名称 |
| dataSourceType | String | 数据源类型(脚本、HTTP、SQL) |
| scriptInfo | Object | 数据源为脚本的执行数据集 |
| httpInfo | Object | 数据源为HTTP的执行数据集 |
| sqlInfo | Object | 数据源为SQL的执行数据集 |
- 策略组(文档名称:rec-strategy)
| **字段名称** | **类型** | **备注** |
| --- | --- | --- |
| domainCode | String | 域Code |
| sceneCode | String | 场景Code |
| strategyName | String | 策略组名称 |
| dataSourceUuid | String | 数据源关联uuid |
| ruleType | String | 规则配置类型(默认为List) |
| ruleMode | String | 规则模式 默认为simple |
| ruleListInfo | Object | 规则配置类型为List时候的规则数据 |
| ruleTreeInfo | Object | 规则配置类型为Tree时候的规则数据 |
## 注册中心设计
- 为什么需要注册中心,因为SDK是在客户端使用,如果需要动态变更数据,那么就需要能够通知到相应的SDK进行数据变更,在规则引擎中,没有使用第三方的注册中心实现,所有就有自己进行编写注册中心
- 总体逻辑比较简单,在SDK所在的客户端启动的时候,会主动将自身的服务ip、服务端口、应用信息等发送给Admin(注册中心),然后注册中心会对这些数据进行注册,写到注册表中进行物理维护,并且定时触发心跳,判断SDK客户端是否还在线,并且维护状态
- 而当需要数据更新的时候,则需要触发消息通知,就会对注册中心的注册信息进行轮询,然后进行服务通知,相关的数据交互,参见架构设计与落地部分
- 在向Admin注册的时候,会将所有可用的域Code发送,假设当前机器需要加载100个域,那么注册中心将会有100条记录,区别就是domain不一致,这样做是为了方便消息通知
- **注册表**设计如下
| **字段名称** | **类型** | **备注** |
| --- | --- | --- |
| id | object(String) | mongodb 自带id |
| uuid | String | 雪花算法随机UUID |
| desc | String | 功能描述 |
| createTime | Date | 创建时间 |
| updateTime | Date | 更新时间 |
| isDelete | int | 状态 1有效,0无效 |
| env | String | 环境 |
| clientAddress | String | SDK 服务ip地址 |
| clientPort | int | SDK 服务端口 |
| appName | String | SDK 服务服务名字 |
| registerTime | Date | 服务注册时间 |
| lastUpdateTime | Date | 上次注册更新时间 |
| domain | String | 注册绑定的domain |

## 接入方式
- 引入依赖,其中 `rec-engine-sdk-http` 是加载本地缓存的默认实现方式
```xml
8
8
2.2.2.RELEASE
0.0.0.3.RELEASE
org.springframework.boot
spring-boot-dependencies
org.springframework.boot
spring-boot-starter-logging
${spring.boot.version}
import
pom
cn.icanci.loopstack.rec
rec-engine-sdk
${rec.version}
cn.icanci.loopstack.rec
rec-engine-sdk-http
${rec.version}
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-logging
cn.icanci.loopstack.rec
rec-engine-sdk
cn.icanci.loopstack.rec
rec-engine-sdk-http
```
- 在Resource根目录下加入HTTP加载方式的配置文件 `rec-http-spi-load.properties`,配置如下
```properties
# Service address for loading data
rec-http-request-url=http://localhost:9999
```
- 并且需要将此文件打包进去
```xml
true
src/main/resources
**/*.properties
```
- 然后添加 application.yaml 配置文件
```yaml
# 接入方需要上报自己的信息给注册中心
rec:
env: test
load: true
load-all: true
client-port: 12000
server-ips: 127.0.0.1
server-port: 9999
app-name: rec-sample-sdk-test
domain: rec
```
- 示例项目:https://gitee.com/loopstack/rec-sample
## 迭代版本
- 0.0.0.3.RELEASE: 支持服务端和客户端,支持SDK、提供HTTP SDK SPI的实现
## 启动项目
### 项目依赖
- JDK8以上
- NodeJS
- Vue
- MongoDB
- Tomcat,已在SpringBoot中内含
- **Tips**
- 如果没有安装NodeJS,可选择注释掉 `启动模块 rec-admin-views` 中pom.xml的 `前后端分离注释部分`
- 更改MongoDB链接配置,并且建立MongoDB数据库:rec
- 执行打包命令:mvn clean install
- 主方法运行:AdminViewApplication
- 浏览器访问:http://localhost:9999/#/login 登录即可
- 分支已经将前端项目打包之后的文件放在了 `启动模块 rec-admin-views` 的 `/resources/static` 下,可直接运行
- 如果要自行开发,则需要安装以上需要的依赖
### 非前后端启动
- 更改MongoDB链接配置,并且建立MongoDB数据库:rec
- 执行打包命令:mvn clean install
- 主方法运行:AdminViewApplication
- 浏览器访问:http://localhost:9999/#/login 登录即可
### 前后端分离启动
- 注释掉启动模块 rec-admin-views 中pom.xml的 `前后端分离注释部分`,如果不注释,则会启动编译,打包会比较慢
- 执行打包命令:mvn clean install
- 此时如果访问后端服务端口,则进入的是编译之后的前端页面;如果访问前端端口,则进入是运行时前端页面
- 进入`启动模块 rec-admin-views` 的 `vueboot`,执行 `npm run dev`,然后浏览器会打开 http://localhost:9528/#/login
- 主方法运行:AdminViewApplication
- 页面登录,进入到配置页面
### 备注
- 前端开发如果提示有报错,请禁用:ESLint
- npm 版本过高会有打包问题
- mvn clean deploy
- mvn versions:set -DnewVersion=0.0.0.1
- mvn versions:revert
- mvn versions:commit
## ISSUE
- rec-group:组的概念SAAS
- mongodb数据库导出:mongodump -d rec -o /Users/icanci/Desktop
- 测试mongo的数据文档参见 `static/rec`
- 思考:有个很扯淡的事情,既然已经支持SDK去加载数据了,那是否还需要定义SPI,然后提供默认实现加载数据呢? 这个问题值得商榷,因为服务端向客户端通知的时候,就可以携带数据了