1. 统一包名修改

This commit is contained in:
chenbowen
2025-09-22 11:55:27 +08:00
parent a001fc8f16
commit 0d46897482
2739 changed files with 512 additions and 512 deletions

24
zt-module-rule/pom.xml Normal file
View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>zt</artifactId>
<groupId>com.zt.plat</groupId>
<version>${revision}</version>
</parent>
<modules>
<module>zt-module-rule-api</module>
<module>zt-module-rule-server</module>
</modules>
<modelVersion>4.0.0</modelVersion>
<artifactId>zt-module-rule</artifactId>
<packaging>pom</packaging>
<name>${project.artifactId}</name>
<description>
Rule 模块。
</description>
</project>

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>zt-module-rule</artifactId>
<groupId>com.zt.plat</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>zt-module-rule-api</artifactId>
<packaging>jar</packaging>
<name>${project.artifactId}</name>
<description>
暴露给其它模块调用
</description>
<dependencies>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-common</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>org.springdoc</groupId> <!-- 接口文档:使用最新版本的 Swagger 模型 -->
<artifactId>springdoc-openapi-starter-webmvc-api</artifactId>
<scope>provided</scope>
</dependency>
<!-- 参数校验 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<optional>true</optional>
</dependency>
<!-- RPC 远程调用相关 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,17 @@
package com.zt.plat.module.rule.enums;
import com.zt.plat.framework.common.exception.ErrorCode;
/**
* rule 错误码枚举类
*
* rule 系统,使用 1-xxx-xxx-xxx 段
*
* @author ZT
*/
public interface ErrorCodeConstants {
// ========== 示例模块 1-001-000-000 ==========
ErrorCode EXAMPLE_NOT_EXISTS = new ErrorCode(1_001_000_001, "示例不存在");
}

View File

@@ -0,0 +1,158 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>zt-module-rule</artifactId>
<groupId>com.zt.plat</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>zt-module-rule-server</artifactId>
<name>${project.artifactId}</name>
<description>
Rule 模块。
</description>
<dependencies>
<!-- Spring Cloud 基础 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-env</artifactId>
</dependency>
<!-- 依赖服务 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-module-system-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-module-infra-api</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-module-rule-api</artifactId>
<version>${revision}</version>
</dependency>
<!-- 业务组件 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-biz-data-permission</artifactId>
</dependency>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-biz-tenant</artifactId>
</dependency>
<!-- Web 相关 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-security</artifactId>
</dependency>
<!-- DB 相关 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-mybatis</artifactId>
</dependency>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-redis</artifactId>
</dependency>
<!-- RPC 远程调用相关 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-rpc</artifactId>
</dependency>
<!-- Registry 注册中心相关 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Config 配置中心相关 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Job 定时任务相关 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-job</artifactId>
</dependency>
<!-- 消息队列相关 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-mq</artifactId>
</dependency>
<!-- Test 测试相关 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-test</artifactId>
</dependency>
<!-- 工具类相关 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-excel</artifactId>
</dependency>
<!-- 监控相关 -->
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-monitor</artifactId>
</dependency>
<dependency>
<groupId>com.zt.plat</groupId>
<artifactId>zt-spring-boot-starter-biz-business</artifactId>
<version>${revision}</version>
</dependency>
<!-- LiteFlow 规则引擎相关 -->
<dependency>
<groupId>com.yomahub</groupId>
<artifactId>liteflow-spring-boot-starter</artifactId>
<version>2.15.0</version>
</dependency>
</dependencies>
<build>
<!-- 设置构建的 jar 包名 -->
<finalName>${project.artifactId}</finalName>
<plugins>
<!-- 打包 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal> <!-- 将引入的 jar 打入其中 -->
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,18 @@
package com.zt.plat.module.rule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Rule 模块的启动类
*
* @author ZT
*/
@SpringBootApplication
public class RuleServerApplication {
public static void main(String[] args) {
SpringApplication.run(RuleServerApplication.class, args);
}
}

View File

@@ -0,0 +1,136 @@
package com.zt.plat.module.rule.controller.admin.rule;
import com.zt.plat.framework.common.pojo.CommonResult;
import com.zt.plat.framework.common.pojo.PageParam;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.common.util.object.BeanUtils;
import com.zt.plat.framework.excel.core.util.ExcelUtils;
import com.zt.plat.framework.operatelog.core.annotations.OperateLog;
import com.zt.plat.module.rule.controller.admin.rule.vo.*;
import com.zt.plat.module.rule.convert.rule.RuleConvert;
import com.zt.plat.module.rule.dal.dataobject.rule.RuleDO;
import com.zt.plat.module.rule.service.rule.RuleService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
import java.io.IOException;
import java.util.List;
import static com.zt.plat.framework.common.pojo.CommonResult.success;
import static com.zt.plat.framework.operatelog.core.enums.OperateTypeEnum.*;
@Tag(name = "管理后台 - 规则引擎")
@RestController
@RequestMapping("/admin/rule/rule")
@Validated
public class RuleController {
@Resource
private RuleService ruleService;
@PostMapping("/create")
@Operation(summary = "创建规则")
@PreAuthorize("@ss.hasPermission('rule:rule:create')")
@OperateLog(type = CREATE)
public CommonResult<Long> createRule(@Valid @RequestBody RuleCreateReqVO createReqVO) {
return success(ruleService.createRule(createReqVO));
}
@PutMapping("/update")
@Operation(summary = "更新规则")
@PreAuthorize("@ss.hasPermission('rule:rule:update')")
@OperateLog(type = UPDATE)
public CommonResult<Boolean> updateRule(@Valid @RequestBody RuleUpdateReqVO updateReqVO) {
ruleService.updateRule(updateReqVO);
return success(true);
}
@DeleteMapping("/delete")
@Operation(summary = "删除规则")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('rule:rule:delete')")
@OperateLog(type = DELETE)
public CommonResult<Boolean> deleteRule(@RequestParam("id") Long id) {
ruleService.deleteRule(id);
return success(true);
}
@GetMapping("/get")
@Operation(summary = "获得规则")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('rule:rule:query')")
public CommonResult<RuleRespVO> getRule(@RequestParam("id") Long id) {
RuleDO rule = ruleService.getRule(id);
return success(RuleConvert.INSTANCE.convert(rule));
}
@GetMapping("/page")
@Operation(summary = "获得规则分页")
@PreAuthorize("@ss.hasPermission('rule:rule:query')")
public CommonResult<PageResult<RuleRespVO>> getRulePage(@Valid RulePageReqVO pageReqVO) {
PageResult<RuleDO> pageResult = ruleService.getRulePage(pageReqVO);
return success(RuleConvert.INSTANCE.convertPage(pageResult));
}
@GetMapping("/export-excel")
@Operation(summary = "导出规则 Excel")
@PreAuthorize("@ss.hasPermission('rule:rule:export')")
@OperateLog(type = EXPORT)
public void exportRuleExcel(@Valid RulePageReqVO pageReqVO,
HttpServletResponse response) throws IOException {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<RuleDO> list = ruleService.getRulePage(pageReqVO).getList();
// 导出 Excel
ExcelUtils.write(response, "规则.xls", "数据", RuleRespVO.class,
BeanUtils.toBean(list, RuleRespVO.class));
}
@PostMapping("/execute")
@Operation(summary = "执行规则")
@PreAuthorize("@ss.hasPermission('rule:rule:execute')")
@OperateLog(type = OTHER)
public CommonResult<RuleExecuteRespVO> executeRule(@Valid @RequestBody RuleExecuteReqVO executeReqVO) {
return success(ruleService.executeRule(executeReqVO));
}
@PutMapping("/enable")
@Operation(summary = "启用规则")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('rule:rule:update')")
@OperateLog(type = UPDATE)
public CommonResult<Boolean> enableRule(@RequestParam("id") Long id) {
ruleService.enableRule(id);
return success(true);
}
@PutMapping("/disable")
@Operation(summary = "禁用规则")
@Parameter(name = "id", description = "编号", required = true)
@PreAuthorize("@ss.hasPermission('rule:rule:update')")
@OperateLog(type = UPDATE)
public CommonResult<Boolean> disableRule(@RequestParam("id") Long id) {
ruleService.disableRule(id);
return success(true);
}
@PostMapping("/validate")
@Operation(summary = "验证规则配置")
@PreAuthorize("@ss.hasPermission('rule:rule:validate')")
public CommonResult<Boolean> validateRuleConfig(@RequestBody String config) {
return success(ruleService.validateRuleConfig(config));
}
@GetMapping("/hello")
@Operation(summary = "Hello Rule")
public CommonResult<String> hello() {
return success("Hello, Rule!");
}
}

View File

@@ -0,0 +1,44 @@
package com.zt.plat.module.rule.controller.admin.rule.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* 规则 Base VO提供给添加、修改、详细的子 VO 使用
* 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
*/
@Data
public class RuleBaseVO {
@Schema(description = "规则名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "用户积分计算规则")
@NotBlank(message = "规则名称不能为空")
private String name;
@Schema(description = "规则描述", example = "根据用户行为计算积分奖励")
private String description;
@Schema(description = "规则类型1-原子规则 2-链式规则", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "规则类型不能为空")
private Integer type;
@Schema(description = "规则状态0-禁用 1-启用", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "规则状态不能为空")
private Integer status;
@Schema(description = "规则配置JSON格式", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "规则配置不能为空")
private String config;
@Schema(description = "LiteFlow规则链ID", example = "userPointsChain")
private String chainId;
@Schema(description = "规则版本", example = "1.0.0")
private String version;
@Schema(description = "排序", example = "1")
private Integer sort;
}

View File

@@ -0,0 +1,14 @@
package com.zt.plat.module.rule.controller.admin.rule.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
@Schema(description = "管理后台 - 规则创建 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class RuleCreateReqVO extends RuleBaseVO {
}

View File

@@ -0,0 +1,116 @@
package com.zt.plat.module.rule.controller.admin.rule.vo;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
import java.util.Map;
/**
* 规则执行请求 VO
*/
@Data
public class RuleExecuteReqVO {
/**
* 规则ID
*/
private Long ruleId;
/**
* 规则链ID与ruleId二选一
*/
private String chainId;
/**
* 执行上下文数据
*/
private Map<String, Object> contextData;
/**
* 扩展参数
*/
private Map<String, Object> extParams;
/**
* 规则执行配置JSON格式的规则定义
*/
@Data
public static class RuleConfig {
/**
* 规则链配置
*/
private ChainConfig chain;
/**
* 节点配置列表
*/
private List<NodeConfig> nodes;
}
/**
* 规则链配置
*/
@Data
public static class ChainConfig {
/**
* 链ID
*/
private String chainId;
/**
* 链名称
*/
private String chainName;
/**
* 执行表达式THEN、WHEN、FOR等
*/
private String expression;
/**
* 是否启用
*/
private Boolean enable;
}
/**
* 节点配置
*/
@Data
public static class NodeConfig {
/**
* 节点ID
*/
private String nodeId;
/**
* 节点名称
*/
private String nodeName;
/**
* 节点类型common-普通节点condition-条件节点switch-选择节点for-循环节点
*/
private String nodeType;
/**
* 节点类全限定名
*/
private String clazz;
/**
* 节点配置参数
*/
private Map<String, Object> properties;
/**
* 是否启用
*/
private Boolean enable;
}
}

View File

@@ -0,0 +1,59 @@
package com.zt.plat.module.rule.controller.admin.rule.vo;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.Map;
/**
* 规则执行结果 VO
*/
@Data
public class RuleExecuteRespVO {
/**
* 执行是否成功
*/
private Boolean success;
/**
* 错误消息
*/
private String errorMessage;
/**
* 执行结果数据
*/
private Map<String, Object> resultData;
/**
* 执行耗时(毫秒)
*/
private Long executionTime;
/**
* 执行开始时间
*/
private LocalDateTime startTime;
/**
* 执行结束时间
*/
private LocalDateTime endTime;
/**
* 执行的规则链ID
*/
private String chainId;
/**
* 执行的节点列表
*/
private String executedNodes;
/**
* 执行上下文快照
*/
private Map<String, Object> contextSnapshot;
}

View File

@@ -0,0 +1,32 @@
package com.zt.plat.module.rule.controller.admin.rule.vo;
import com.zt.plat.framework.common.pojo.PageParam;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 规则分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class RulePageReqVO extends PageParam {
@Schema(description = "规则名称", example = "用户积分规则")
private String name;
@Schema(description = "规则类型1-原子规则 2-链式规则", example = "1")
private Integer type;
@Schema(description = "规则状态0-禁用 1-启用", example = "1")
private Integer status;
@Schema(description = "规则链ID", example = "userPointsChain")
private String chainId;
@Schema(description = "创建时间")
private LocalDateTime[] createTime;
}

View File

@@ -0,0 +1,25 @@
package com.zt.plat.module.rule.controller.admin.rule.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - 规则 Response VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class RuleRespVO extends RuleBaseVO {
@Schema(description = "规则ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private Long id;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "更新时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,20 @@
package com.zt.plat.module.rule.controller.admin.rule.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import javax.validation.constraints.NotNull;
@Schema(description = "管理后台 - 规则更新 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class RuleUpdateReqVO extends RuleBaseVO {
@Schema(description = "规则ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotNull(message = "规则ID不能为空")
private Long id;
}

View File

@@ -0,0 +1,31 @@
package com.zt.plat.module.rule.convert.rule;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.module.rule.controller.admin.rule.vo.*;
import com.zt.plat.module.rule.dal.dataobject.rule.RuleDO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* 规则 Convert
*
* @author ZT源码
*/
@Mapper
public interface RuleConvert {
RuleConvert INSTANCE = Mappers.getMapper(RuleConvert.class);
RuleDO convert(RuleCreateReqVO bean);
RuleDO convert(RuleUpdateReqVO bean);
RuleRespVO convert(RuleDO bean);
List<RuleRespVO> convertList(List<RuleDO> list);
PageResult<RuleRespVO> convertPage(PageResult<RuleDO> page);
}

View File

@@ -0,0 +1,70 @@
package com.zt.plat.module.rule.dal.dataobject.rule;
import com.zt.plat.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* 规则 DO
*
* @author ZT源码
*/
@TableName("rule_rule")
@KeySequence("rule_rule_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RuleDO extends BaseDO {
/**
* 规则ID
*/
@TableId
private Long id;
/**
* 规则名称
*/
private String name;
/**
* 规则描述
*/
private String description;
/**
* 规则类型1-原子规则 2-链式规则
*/
private Integer type;
/**
* 规则状态0-禁用 1-启用
*/
private Integer status;
/**
* 规则配置JSON格式
*/
private String config;
/**
* LiteFlow规则链ID
*/
private String chainId;
/**
* 规则版本
*/
private String version;
/**
* 排序
*/
private Integer sort;
}

View File

@@ -0,0 +1,28 @@
package com.zt.plat.module.rule.dal.mysql.rule;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.framework.mybatis.core.mapper.BaseMapperX;
import com.zt.plat.framework.mybatis.core.query.LambdaQueryWrapperX;
import com.zt.plat.module.rule.controller.admin.rule.vo.RulePageReqVO;
import com.zt.plat.module.rule.dal.dataobject.rule.RuleDO;
import org.apache.ibatis.annotations.Mapper;
/**
* 规则 Mapper
*
* @author ZT源码
*/
@Mapper
public interface RuleMapper extends BaseMapperX<RuleDO> {
default PageResult<RuleDO> selectPage(RulePageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<RuleDO>()
.likeIfPresent(RuleDO::getName, reqVO.getName())
.eqIfPresent(RuleDO::getType, reqVO.getType())
.eqIfPresent(RuleDO::getStatus, reqVO.getStatus())
.eqIfPresent(RuleDO::getChainId, reqVO.getChainId())
.betweenIfPresent(RuleDO::getCreateTime, reqVO.getCreateTime())
.orderByDesc(RuleDO::getId));
}
}

View File

@@ -0,0 +1,21 @@
package com.zt.plat.module.rule.enums;
import com.zt.plat.framework.common.exception.ErrorCode;
/**
* Rule 错误码枚举类
*
* rule 系统,使用 1-009-000-000 段
*/
public interface ErrorCodeConstants {
// ========== 规则相关错误码 1-009-001-000 ==========
ErrorCode RULE_NOT_EXISTS = new ErrorCode(1_009_001_000, "规则不存在");
ErrorCode RULE_CONFIG_INVALID = new ErrorCode(1_009_001_001, "规则配置无效");
ErrorCode RULE_LOAD_FAILED = new ErrorCode(1_009_001_002, "规则加载失败");
ErrorCode RULE_NOT_ENABLED = new ErrorCode(1_009_001_003, "规则未启用");
ErrorCode RULE_CHAIN_ID_EMPTY = new ErrorCode(1_009_001_004, "规则链ID不能为空");
ErrorCode RULE_EXECUTE_FAILED = new ErrorCode(1_009_001_005, "规则执行失败");
ErrorCode RULE_VALIDATION_FAILED = new ErrorCode(1_009_001_006, "规则验证失败");
}

View File

@@ -0,0 +1,75 @@
package com.zt.plat.module.rule.framework.liteflow.component.action;
import com.zt.plat.module.rule.framework.liteflow.component.base.BaseRuleComponent;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
/**
* 数据设置组件
* 用于设置上下文数据
*
* @author ZT源码
*/
@LiteflowComponent("dataSetNode")
@Slf4j
public class DataSetComponent extends BaseRuleComponent {
@Override
public void process() throws Exception {
// 获取参数
String dataKey = getNodeProperty("dataKey", String.class);
Object dataValue = getNodeProperty("dataValue", Object.class);
String valueType = getNodeProperty("valueType", String.class);
if (dataKey == null) {
throw new IllegalArgumentException("数据设置组件参数不完整: dataKey 不能为空");
}
Object finalValue = dataValue;
// 根据valueType进行类型转换
if (valueType != null && dataValue != null) {
String strValue = String.valueOf(dataValue);
switch (valueType.toLowerCase()) {
case "string":
finalValue = strValue;
break;
case "integer":
case "int":
finalValue = Integer.valueOf(strValue);
break;
case "long":
finalValue = Long.valueOf(strValue);
break;
case "double":
finalValue = Double.valueOf(strValue);
break;
case "boolean":
finalValue = Boolean.valueOf(strValue);
break;
case "object":
// 保持原有类型
break;
default:
log.warn("不支持的数据类型: {}, 将保持原有类型", valueType);
}
}
// 设置数据到上下文
setContextData(dataKey, finalValue);
// 同时设置到结果数据中
Map<String, Object> resultData = getContextData("resultData", Map.class);
if (resultData == null) {
resultData = new HashMap<>();
setContextData("resultData", resultData);
}
resultData.put(dataKey, finalValue);
log.info("设置数据: key={}, value={}, valueType={}", dataKey, finalValue, valueType);
}
}

View File

@@ -0,0 +1,96 @@
package com.zt.plat.module.rule.framework.liteflow.component.action;
import com.zt.plat.module.rule.framework.liteflow.component.base.BaseRuleComponent;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
* 数值计算组件
* 支持加减乘除等数学运算
*
* @author ZT源码
*/
@LiteflowComponent("mathCalculateNode")
@Slf4j
public class MathCalculateComponent extends BaseRuleComponent {
@Override
public void process() throws Exception {
// 获取参数
String leftValue = getNodeProperty("leftValue", String.class);
String rightValue = getNodeProperty("rightValue", String.class);
String operator = getNodeProperty("operator", String.class);
String resultKey = getNodeProperty("resultKey", String.class);
Integer scale = getNodeProperty("scale", Integer.class);
if (leftValue == null || rightValue == null || operator == null) {
throw new IllegalArgumentException("数值计算组件参数不完整: leftValue, rightValue, operator 都不能为空");
}
if (resultKey == null) {
resultKey = "calculateResult";
}
if (scale == null) {
scale = 2; // 默认保留2位小数
}
try {
// 转换为数值
BigDecimal left = new BigDecimal(leftValue);
BigDecimal right = new BigDecimal(rightValue);
BigDecimal result;
// 根据操作符进行计算
switch (operator) {
case "+":
case "add":
result = left.add(right);
break;
case "-":
case "subtract":
result = left.subtract(right);
break;
case "*":
case "multiply":
result = left.multiply(right);
break;
case "/":
case "divide":
if (right.compareTo(BigDecimal.ZERO) == 0) {
throw new IllegalArgumentException("除数不能为0");
}
result = left.divide(right, scale, RoundingMode.HALF_UP);
break;
case "%":
case "mod":
result = left.remainder(right);
break;
case "max":
result = left.max(right);
break;
case "min":
result = left.min(right);
break;
case "pow":
result = left.pow(right.intValue());
break;
default:
throw new IllegalArgumentException("不支持的数学操作符: " + operator);
}
// 设置计算结果
setContextData(resultKey, result);
setContextData("lastCalculateResult", result);
log.info("数值计算: {} {} {} = {}", leftValue, operator, rightValue, result);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("数值格式错误: leftValue=" + leftValue + ", rightValue=" + rightValue, e);
}
}
}

View File

@@ -0,0 +1,149 @@
package com.zt.plat.module.rule.framework.liteflow.component.base;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.yomahub.liteflow.core.NodeComponent;
import com.yomahub.liteflow.slot.DefaultContext;
import java.util.Map;
/**
* 基础规则组件
* 提供通用的属性获取和上下文操作方法
*
* @author ZT源码
*/
public abstract class BaseRuleComponent extends NodeComponent {
/**
* 获取节点配置属性
*
* @param key 属性键
* @param clazz 属性类型
* @return 属性值
*/
@SuppressWarnings("unchecked")
protected <T> T getNodeProperty(String key, Class<T> clazz) {
DefaultContext context = this.getContextBean(DefaultContext.class);
if (context == null) {
return null;
}
// 首先尝试从节点配置中获取
String nodeConfigKey = "nodeConfig_" + this.getNodeId();
Object nodeConfig = context.getData(nodeConfigKey);
if (nodeConfig instanceof Map) {
Map<String, Object> configMap = (Map<String, Object>) nodeConfig;
Object value = configMap.get(key);
if (value != null) {
return convertValue(value, clazz);
}
}
// 其次尝试从上下文数据中获取
Object value = context.getData(key);
if (value != null) {
return convertValue(value, clazz);
}
return null;
}
/**
* 设置上下文数据
*
* @param key 键
* @param value 值
*/
protected void setContextData(String key, Object value) {
DefaultContext context = this.getContextBean(DefaultContext.class);
if (context != null) {
context.setData(key, value);
}
}
/**
* 获取上下文数据
*
* @param key 键
* @param clazz 类型
* @return 值
*/
@SuppressWarnings("unchecked")
protected <T> T getContextData(String key, Class<T> clazz) {
DefaultContext context = this.getContextBean(DefaultContext.class);
if (context == null) {
return null;
}
Object value = context.getData(key);
if (value != null) {
return convertValue(value, clazz);
}
return null;
}
/**
* 类型转换
*
* @param value 原始值
* @param clazz 目标类型
* @return 转换后的值
*/
@SuppressWarnings("unchecked")
private <T> T convertValue(Object value, Class<T> clazz) {
if (value == null) {
return null;
}
if (clazz.isInstance(value)) {
return (T) value;
}
if (clazz == String.class) {
return (T) String.valueOf(value);
}
if (clazz == Integer.class || clazz == int.class) {
if (value instanceof Number) {
return (T) Integer.valueOf(((Number) value).intValue());
}
return (T) Integer.valueOf(String.valueOf(value));
}
if (clazz == Long.class || clazz == long.class) {
if (value instanceof Number) {
return (T) Long.valueOf(((Number) value).longValue());
}
return (T) Long.valueOf(String.valueOf(value));
}
if (clazz == Boolean.class || clazz == boolean.class) {
if (value instanceof Boolean) {
return (T) value;
}
return (T) Boolean.valueOf(String.valueOf(value));
}
if (clazz == Double.class || clazz == double.class) {
if (value instanceof Number) {
return (T) Double.valueOf(((Number) value).doubleValue());
}
return (T) Double.valueOf(String.valueOf(value));
}
// 尝试JSON转换
if (value instanceof String && !clazz.isPrimitive()) {
String strValue = (String) value;
if (JSONUtil.isTypeJSON(strValue)) {
return JSONUtil.toBean(strValue, clazz);
}
}
throw new IllegalArgumentException("无法将类型 " + value.getClass().getSimpleName() +
" 转换为 " + clazz.getSimpleName());
}
}

View File

@@ -0,0 +1,80 @@
package com.zt.plat.module.rule.framework.liteflow.component.common;
import com.zt.plat.module.rule.framework.liteflow.component.base.BaseRuleComponent;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
/**
* 条件判断组件 - 数值比较
* 支持大于、小于、等于、大于等于、小于等于比较
*
* @author ZT源码
*/
@LiteflowComponent("numberCompareNode")
@Slf4j
public class NumberCompareComponent extends BaseRuleComponent {
@Override
public void process() throws Exception {
// 获取参数
String leftValue = getNodeProperty("leftValue", String.class);
String operator = getNodeProperty("operator", String.class);
String rightValue = getNodeProperty("rightValue", String.class);
if (leftValue == null || operator == null || rightValue == null) {
throw new IllegalArgumentException("数值比较组件参数不完整: leftValue, operator, rightValue 都不能为空");
}
try {
// 转换为数值
BigDecimal left = new BigDecimal(leftValue);
BigDecimal right = new BigDecimal(rightValue);
boolean result = false;
// 根据操作符进行比较
switch (operator) {
case ">":
case "gt":
result = left.compareTo(right) > 0;
break;
case "<":
case "lt":
result = left.compareTo(right) < 0;
break;
case "=":
case "==":
case "eq":
result = left.compareTo(right) == 0;
break;
case ">=":
case "gte":
result = left.compareTo(right) >= 0;
break;
case "<=":
case "lte":
result = left.compareTo(right) <= 0;
break;
case "!=":
case "ne":
result = left.compareTo(right) != 0;
break;
default:
throw new IllegalArgumentException("不支持的比较操作符: " + operator);
}
// 设置比较结果
this.setIsEnd(!result);
getSlot().setData("compareResult", result);
log.info("数值比较: {} {} {} = {}", leftValue, operator, rightValue, result);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("数值格式错误: leftValue=" + leftValue + ", rightValue=" + rightValue, e);
}
}
}

View File

@@ -0,0 +1,98 @@
package com.zt.plat.module.rule.framework.liteflow.component.common;
import cn.hutool.core.util.StrUtil;
import com.zt.plat.module.rule.framework.liteflow.component.base.BaseRuleComponent;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import lombok.extern.slf4j.Slf4j;
/**
* 字符串判断组件
* 支持字符串相等、包含、长度等判断
*
* @author ZT源码
*/
@LiteflowComponent("stringConditionNode")
@Slf4j
public class StringConditionComponent extends BaseRuleComponent {
@Override
public void process() throws Exception {
// 获取参数
String sourceValue = getNodeProperty("sourceValue", String.class);
String targetValue = getNodeProperty("targetValue", String.class);
String operator = getNodeProperty("operator", String.class);
if (sourceValue == null || operator == null) {
throw new IllegalArgumentException("字符串判断组件参数不完整: sourceValue, operator 不能为空");
}
boolean result = false;
// 根据操作符进行判断
switch (operator) {
case "equals":
case "eq":
result = StrUtil.equals(sourceValue, targetValue);
break;
case "equalsIgnoreCase":
case "eqIgnoreCase":
result = StrUtil.equalsIgnoreCase(sourceValue, targetValue);
break;
case "contains":
result = StrUtil.contains(sourceValue, targetValue);
break;
case "startsWith":
result = StrUtil.startWith(sourceValue, targetValue);
break;
case "endsWith":
result = StrUtil.endWith(sourceValue, targetValue);
break;
case "isEmpty":
result = StrUtil.isEmpty(sourceValue);
break;
case "isNotEmpty":
result = StrUtil.isNotEmpty(sourceValue);
break;
case "isBlank":
result = StrUtil.isBlank(sourceValue);
break;
case "isNotBlank":
result = StrUtil.isNotBlank(sourceValue);
break;
case "lengthEquals":
Integer expectedLength = getNodeProperty("expectedLength", Integer.class);
if (expectedLength != null) {
result = sourceValue.length() == expectedLength;
}
break;
case "lengthGreaterThan":
Integer minLength = getNodeProperty("minLength", Integer.class);
if (minLength != null) {
result = sourceValue.length() > minLength;
}
break;
case "lengthLessThan":
Integer maxLength = getNodeProperty("maxLength", Integer.class);
if (maxLength != null) {
result = sourceValue.length() < maxLength;
}
break;
case "matches":
String pattern = getNodeProperty("pattern", String.class);
if (pattern != null) {
result = sourceValue.matches(pattern);
}
break;
default:
throw new IllegalArgumentException("不支持的字符串操作符: " + operator);
}
// 设置判断结果
this.setIsEnd(!result);
setContextData("stringConditionResult", result);
log.info("字符串判断: sourceValue={}, operator={}, targetValue={}, result={}",
sourceValue, operator, targetValue, result);
}
}

View File

@@ -0,0 +1,24 @@
package com.zt.plat.module.rule.framework.liteflow.config;
import com.yomahub.liteflow.spring.ComponentScanner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* LiteFlow 配置类
*
* @author ZT源码
*/
@Configuration
public class LiteFlowConfiguration {
/**
* 组件扫描器
* 自动扫描LiteFlow组件
*/
@Bean
public ComponentScanner componentScanner() {
return new ComponentScanner("com.zt.plat.module.rule.framework.liteflow.component");
}
}

View File

@@ -0,0 +1,212 @@
package com.zt.plat.module.rule.framework.liteflow.service;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.zt.plat.module.rule.controller.admin.rule.vo.RuleExecuteReqVO;
import com.zt.plat.module.rule.dal.dataobject.rule.RuleDO;
import com.yomahub.liteflow.core.FlowExecutor;
import com.yomahub.liteflow.flow.LiteflowResponse;
import com.yomahub.liteflow.flow.element.chain.LiteFlowChain;
import com.yomahub.liteflow.flow.element.chain.LiteFlowChainBuilder;
import com.yomahub.liteflow.slot.DefaultContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* LiteFlow 服务实现类
*
* @author ZT源码
*/
@Service
@Slf4j
public class LiteFlowService {
@Resource
private FlowExecutor flowExecutor;
/**
* 执行规则
*
* @param chainId 规则链ID
* @param contextData 上下文数据
* @param extParams 扩展参数
* @return 执行结果
*/
public Map<String, Object> executeRule(String chainId, Map<String, Object> contextData, Map<String, Object> extParams) {
try {
// 创建上下文
DefaultContext context = new DefaultContext();
// 设置上下文数据
if (MapUtil.isNotEmpty(contextData)) {
contextData.forEach(context::setData);
}
// 设置扩展参数
if (MapUtil.isNotEmpty(extParams)) {
extParams.forEach(context::setData);
}
// 执行规则链
LiteflowResponse response = flowExecutor.execute2Resp(chainId, null, context);
// 构建返回结果
Map<String, Object> resultData = new HashMap<>();
resultData.put("success", response.isSuccess());
resultData.put("message", response.getMessage());
resultData.put("executeStepStr", response.getExecuteStepStr());
// 获取执行结果数据
if (response.getSlot() != null && response.getSlot().getContextBean() instanceof DefaultContext) {
DefaultContext resultContext = (DefaultContext) response.getSlot().getContextBean();
if (resultContext != null) {
resultData.put("contextData", resultContext.getData());
}
}
return resultData;
} catch (Exception e) {
log.error("执行规则链失败: chainId={}", chainId, e);
throw new RuntimeException("规则执行失败: " + e.getMessage(), e);
}
}
/**
* 加载规则到LiteFlow
*
* @param rule 规则DO
*/
public void loadRule(RuleDO rule) {
try {
if (StrUtil.isBlank(rule.getConfig())) {
throw new IllegalArgumentException("规则配置不能为空");
}
// 解析JSON配置
JSONObject configJson = JSONUtil.parseObj(rule.getConfig());
RuleExecuteReqVO.RuleConfig ruleConfig = JSONUtil.toBean(configJson, RuleExecuteReqVO.RuleConfig.class);
if (ruleConfig.getChain() == null) {
throw new IllegalArgumentException("规则链配置不能为空");
}
// 构建LiteFlow规则链
String chainId = StrUtil.isNotBlank(rule.getChainId()) ? rule.getChainId() : ruleConfig.getChain().getChainId();
String expression = ruleConfig.getChain().getExpression();
if (StrUtil.isBlank(expression)) {
throw new IllegalArgumentException("规则链表达式不能为空");
}
// 创建并加载规则链
LiteFlowChain chain = LiteFlowChainBuilder.createChain()
.setChainId(chainId)
.setChainName(ruleConfig.getChain().getChainName())
.setEL(expression)
.build();
// 注册规则链
flowExecutor.getChainContainer().setChain(chainId, chain);
log.info("成功加载规则链: chainId={}, expression={}", chainId, expression);
} catch (Exception e) {
log.error("加载规则到LiteFlow失败: ruleId={}", rule.getId(), e);
throw new RuntimeException("加载规则失败: " + e.getMessage(), e);
}
}
/**
* 重新加载规则
*
* @param rule 规则DO
*/
public void reloadRule(RuleDO rule) {
// 先移除再加载
if (StrUtil.isNotBlank(rule.getChainId())) {
removeRule(rule.getChainId());
}
loadRule(rule);
}
/**
* 移除规则
*
* @param chainId 规则链ID
*/
public void removeRule(String chainId) {
try {
if (StrUtil.isNotBlank(chainId)) {
flowExecutor.getChainContainer().removeChain(chainId);
log.info("成功移除规则链: chainId={}", chainId);
}
} catch (Exception e) {
log.error("移除规则链失败: chainId={}", chainId, e);
throw new RuntimeException("移除规则失败: " + e.getMessage(), e);
}
}
/**
* 验证规则配置
*
* @param config 规则配置JSON
* @return 验证结果
*/
public Boolean validateRuleConfig(String config) {
try {
if (StrUtil.isBlank(config)) {
return false;
}
// 解析JSON
JSONObject configJson = JSONUtil.parseObj(config);
RuleExecuteReqVO.RuleConfig ruleConfig = JSONUtil.toBean(configJson, RuleExecuteReqVO.RuleConfig.class);
// 验证必要字段
if (ruleConfig.getChain() == null) {
log.warn("规则链配置为空");
return false;
}
if (StrUtil.isBlank(ruleConfig.getChain().getChainId())) {
log.warn("规则链ID为空");
return false;
}
if (StrUtil.isBlank(ruleConfig.getChain().getExpression())) {
log.warn("规则链表达式为空");
return false;
}
// 验证节点配置
List<RuleExecuteReqVO.NodeConfig> nodes = ruleConfig.getNodes();
if (nodes != null) {
for (RuleExecuteReqVO.NodeConfig node : nodes) {
if (StrUtil.isBlank(node.getNodeId())) {
log.warn("节点ID为空");
return false;
}
if (StrUtil.isBlank(node.getClazz())) {
log.warn("节点类名为空: nodeId={}", node.getNodeId());
return false;
}
}
}
return true;
} catch (Exception e) {
log.warn("验证规则配置时发生异常", e);
return false;
}
}
}

View File

@@ -0,0 +1,42 @@
package com.zt.plat.module.rule.framework.security.config;
import com.zt.plat.framework.security.config.AuthorizeRequestsCustomizer;
import com.zt.plat.module.infra.enums.ApiConstants;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AuthorizeHttpRequestsConfigurer;
/**
* Rule 模块的 Security 配置
*
* @author ZT
*/
@Configuration(proxyBeanMethods = false)
public class SecurityConfiguration {
@Bean
public AuthorizeRequestsCustomizer authorizeRequestsCustomizer() {
return new AuthorizeRequestsCustomizer() {
@Override
public void customize(AuthorizeHttpRequestsConfigurer<HttpSecurity>.AuthorizationManagerRequestMatcherRegistry registry) {
// Swagger 接口文档
registry.requestMatchers("/v3/api-docs/**").permitAll()
.requestMatchers("/webjars/**").permitAll()
.requestMatchers("/swagger-ui").permitAll()
.requestMatchers("/swagger-ui/**").permitAll();
// Druid 监控
registry.requestMatchers("/druid/**").permitAll();
// Spring Boot Actuator 的安全配置
registry.requestMatchers("/actuator").permitAll()
.requestMatchers("/actuator/**").permitAll();
// RPC 服务的安全配置
registry.requestMatchers(ApiConstants.PREFIX + "/**").permitAll();
}
};
}
}

View File

@@ -0,0 +1,84 @@
package com.zt.plat.module.rule.service.rule;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.module.rule.controller.admin.rule.vo.*;
import com.zt.plat.module.rule.dal.dataobject.rule.RuleDO;
import javax.validation.Valid;
/**
* 规则 Service 接口
*
* @author ZT源码
*/
public interface RuleService {
/**
* 创建规则
*
* @param createReqVO 创建信息
* @return 编号
*/
Long createRule(@Valid RuleCreateReqVO createReqVO);
/**
* 更新规则
*
* @param updateReqVO 更新信息
*/
void updateRule(@Valid RuleUpdateReqVO updateReqVO);
/**
* 删除规则
*
* @param id 编号
*/
void deleteRule(Long id);
/**
* 获得规则
*
* @param id 编号
* @return 规则
*/
RuleDO getRule(Long id);
/**
* 获得规则分页
*
* @param pageReqVO 分页查询
* @return 规则分页
*/
PageResult<RuleDO> getRulePage(RulePageReqVO pageReqVO);
/**
* 执行规则
*
* @param executeReqVO 执行请求
* @return 执行结果
*/
RuleExecuteRespVO executeRule(@Valid RuleExecuteReqVO executeReqVO);
/**
* 启用规则
*
* @param id 规则ID
*/
void enableRule(Long id);
/**
* 禁用规则
*
* @param id 规则ID
*/
void disableRule(Long id);
/**
* 验证规则配置
*
* @param config 规则配置JSON
* @return 验证结果
*/
Boolean validateRuleConfig(String config);
}

View File

@@ -0,0 +1,252 @@
package com.zt.plat.module.rule.service.rule;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.zt.plat.framework.common.exception.util.ServiceExceptionUtil;
import com.zt.plat.framework.common.pojo.PageResult;
import com.zt.plat.module.rule.controller.admin.rule.vo.*;
import com.zt.plat.module.rule.convert.rule.RuleConvert;
import com.zt.plat.module.rule.dal.dataobject.rule.RuleDO;
import com.zt.plat.module.rule.dal.mysql.rule.RuleMapper;
import com.zt.plat.module.rule.enums.ErrorCodeConstants;
import com.zt.plat.module.rule.framework.liteflow.service.LiteFlowService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.util.Map;
import static com.zt.plat.module.rule.enums.ErrorCodeConstants.*;
/**
* 规则 Service 实现类
*
* @author ZT源码
*/
@Service
@Validated
@Slf4j
public class RuleServiceImpl implements RuleService {
@Resource
private RuleMapper ruleMapper;
@Resource
private LiteFlowService liteFlowService;
@Override
@Transactional(rollbackFor = Exception.class)
public Long createRule(RuleCreateReqVO createReqVO) {
// 验证规则配置
if (!validateRuleConfig(createReqVO.getConfig())) {
throw ServiceExceptionUtil.exception(RULE_CONFIG_INVALID);
}
// 插入
RuleDO rule = RuleConvert.INSTANCE.convert(createReqVO);
rule.setVersion("1.0.0");
ruleMapper.insert(rule);
// 如果是启用状态加载到LiteFlow中
if (rule.getStatus() == 1) {
try {
liteFlowService.loadRule(rule);
} catch (Exception e) {
log.error("加载规则到LiteFlow失败", e);
throw ServiceExceptionUtil.exception(RULE_LOAD_FAILED);
}
}
return rule.getId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void updateRule(RuleUpdateReqVO updateReqVO) {
// 校验存在
validateRuleExists(updateReqVO.getId());
// 验证规则配置
if (!validateRuleConfig(updateReqVO.getConfig())) {
throw ServiceExceptionUtil.exception(RULE_CONFIG_INVALID);
}
// 更新
RuleDO updateObj = RuleConvert.INSTANCE.convert(updateReqVO);
ruleMapper.updateById(updateObj);
// 重新加载到LiteFlow中
RuleDO rule = ruleMapper.selectById(updateReqVO.getId());
if (rule.getStatus() == 1) {
try {
liteFlowService.reloadRule(rule);
} catch (Exception e) {
log.error("重新加载规则到LiteFlow失败", e);
throw ServiceExceptionUtil.exception(RULE_LOAD_FAILED);
}
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteRule(Long id) {
// 校验存在
RuleDO rule = validateRuleExists(id);
// 从LiteFlow中移除
try {
liteFlowService.removeRule(rule.getChainId());
} catch (Exception e) {
log.error("从LiteFlow中移除规则失败", e);
}
// 删除
ruleMapper.deleteById(id);
}
private RuleDO validateRuleExists(Long id) {
RuleDO rule = ruleMapper.selectById(id);
if (rule == null) {
throw ServiceExceptionUtil.exception(RULE_NOT_EXISTS);
}
return rule;
}
@Override
public RuleDO getRule(Long id) {
return ruleMapper.selectById(id);
}
@Override
public PageResult<RuleDO> getRulePage(RulePageReqVO pageReqVO) {
return ruleMapper.selectPage(pageReqVO);
}
@Override
public RuleExecuteRespVO executeRule(RuleExecuteReqVO executeReqVO) {
LocalDateTime startTime = LocalDateTime.now();
try {
// 获取规则配置
String chainId;
if (executeReqVO.getRuleId() != null) {
RuleDO rule = getRule(executeReqVO.getRuleId());
if (rule == null) {
throw ServiceExceptionUtil.exception(RULE_NOT_EXISTS);
}
if (rule.getStatus() != 1) {
throw ServiceExceptionUtil.exception(RULE_NOT_ENABLED);
}
chainId = rule.getChainId();
} else {
chainId = executeReqVO.getChainId();
}
if (StrUtil.isBlank(chainId)) {
throw ServiceExceptionUtil.exception(RULE_CHAIN_ID_EMPTY);
}
// 执行规则
Map<String, Object> resultData = liteFlowService.executeRule(
chainId,
executeReqVO.getContextData(),
executeReqVO.getExtParams()
);
LocalDateTime endTime = LocalDateTime.now();
// 构建响应
RuleExecuteRespVO response = new RuleExecuteRespVO();
response.setSuccess(true);
response.setResultData(resultData);
response.setStartTime(startTime);
response.setEndTime(endTime);
response.setExecutionTime(java.time.Duration.between(startTime, endTime).toMillis());
response.setChainId(chainId);
response.setContextSnapshot(executeReqVO.getContextData());
return response;
} catch (Exception e) {
log.error("规则执行失败", e);
LocalDateTime endTime = LocalDateTime.now();
RuleExecuteRespVO response = new RuleExecuteRespVO();
response.setSuccess(false);
response.setErrorMessage(e.getMessage());
response.setStartTime(startTime);
response.setEndTime(endTime);
response.setExecutionTime(java.time.Duration.between(startTime, endTime).toMillis());
response.setContextSnapshot(executeReqVO.getContextData());
return response;
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void enableRule(Long id) {
RuleDO rule = validateRuleExists(id);
// 更新状态
RuleDO updateObj = new RuleDO();
updateObj.setId(id);
updateObj.setStatus(1);
ruleMapper.updateById(updateObj);
// 加载到LiteFlow中
try {
rule.setStatus(1);
liteFlowService.loadRule(rule);
} catch (Exception e) {
log.error("启用规则时加载到LiteFlow失败", e);
throw ServiceExceptionUtil.exception(RULE_LOAD_FAILED);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void disableRule(Long id) {
RuleDO rule = validateRuleExists(id);
// 更新状态
RuleDO updateObj = new RuleDO();
updateObj.setId(id);
updateObj.setStatus(0);
ruleMapper.updateById(updateObj);
// 从LiteFlow中移除
try {
liteFlowService.removeRule(rule.getChainId());
} catch (Exception e) {
log.error("禁用规则时从LiteFlow中移除失败", e);
}
}
@Override
public Boolean validateRuleConfig(String config) {
if (StrUtil.isBlank(config)) {
return false;
}
try {
// 验证JSON格式
if (!JSONUtil.isTypeJSON(config)) {
return false;
}
// 进一步验证规则配置结构
return liteFlowService.validateRuleConfig(config);
} catch (Exception e) {
log.warn("规则配置验证失败", e);
return false;
}
}
}

View File

@@ -0,0 +1,107 @@
spring:
# 数据源配置项
autoconfigure:
exclude:
datasource:
druid: # Druid 【监控】相关的全局配置
web-stat-filter:
enabled: true
stat-view-servlet:
enabled: true
allow: # 设置白名单,不填则允许所有访问
url-pattern: /druid/*
login-username: # 控制台管理用户名和密码
login-password:
filter:
stat:
enabled: true
log-slow-sql: true # 慢 SQL 记录
slow-sql-millis: 100
merge-sql: true
wall:
config:
multi-statement-allow: true
dynamic: # 多数据源配置
druid: # Druid 【连接池】相关的全局配置
initial-size: 5 # 初始连接数
min-idle: 10 # 最小连接池数量
max-active: 20 # 最大连接池数量
max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒
time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒
min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒
max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒
validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效
test-while-idle: true
test-on-borrow: false
test-on-return: false
primary: master
datasource:
master:
url: jdbc:dm://172.16.46.247:1050?schema=RUOYI-VUE-PRO
username: SYSDBA
password: pgbsci6ddJ6Sqj@e
slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改
lazy: true # 开启懒加载,保证启动速度
url: jdbc:dm://172.16.46.247:1050?schema=RUOYI-VUE-PRO
username: SYSDBA
password: pgbsci6ddJ6Sqj@e
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
data:
redis:
host: 172.16.46.63 # 地址
port: 30379 # 端口
database: 0 # 数据库索引
# password: 123456 # 密码,建议生产环境开启
xxl:
job:
admin:
addresses: http://172.16.46.63:30082/xxl-job-admin # 调度中心部署跟地址
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
# Actuator 监控端点的配置项
management:
endpoints:
web:
base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator
exposure:
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。
# 日志文件配置
logging:
file:
name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径
justauth:
enabled: true
type:
DINGTALK: # 钉钉
client-id: dingvrnreaje3yqvzhxg
client-secret: i8E6iZyDvZj51JIb0tYsYfVQYOks9Cq1lgryEjFRqC79P3iJcrxEwT6Qk2QvLrLI
ignore-check-redirect-uri: true
WECHAT_ENTERPRISE: # 企业微信
client-id: wwd411c69a39ad2e54
client-secret: 1wTb7hYxnpT2TUbIeHGXGo7T0odav1ic10mLdyyATOw
agent-id: 1000004
ignore-check-redirect-uri: true
# noinspection SpringBootApplicationYaml
WECHAT_MINI_PROGRAM: # 微信小程序
client-id: ${dollar}{wx.miniapp.appid}
client-secret: ${dollar}{wx.miniapp.secret}
ignore-check-redirect-uri: true
ignore-check-state: true # 微信小程序,不会使用到 state所以不进行校验
WECHAT_MP: # 微信公众号
client-id: ${dollar}{wx.mp.app-id}
client-secret: ${dollar}{wx.mp.secret}
ignore-check-redirect-uri: true
cache:
type: REDIS
prefix: 'social_auth_state:' # 缓存前缀,目前只对 Redis 缓存生效,默认 JUSTAUTH::STATE::
timeout: 24h # 超时时长,目前只对 Redis 缓存生效,默认 3 分钟

View File

@@ -0,0 +1,97 @@
spring:
# 数据源配置项
autoconfigure:
# noinspection SpringBootApplicationYaml
exclude:
- com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure # 排除 Druid 的自动配置,使用 dynamic-datasource-spring-boot-starter 配置多数据源
datasource:
druid: # Druid 【监控】相关的全局配置
web-stat-filter:
enabled: true
stat-view-servlet:
enabled: true
allow: # 设置白名单,不填则允许所有访问
url-pattern: /druid/*
login-username: # 控制台管理用户名和密码
login-password:
filter:
stat:
enabled: true
log-slow-sql: true # 慢 SQL 记录
slow-sql-millis: 100
merge-sql: true
wall:
config:
multi-statement-allow: true
dynamic: # 多数据源配置
druid: # Druid 【连接池】相关的全局配置
initial-size: 1 # 初始连接数
min-idle: 1 # 最小连接池数量
max-active: 20 # 最大连接池数量
max-wait: 600000 # 配置获取连接等待超时的时间,单位:毫秒
time-between-eviction-runs-millis: 60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位:毫秒
min-evictable-idle-time-millis: 300000 # 配置一个连接在池中最小生存的时间,单位:毫秒
max-evictable-idle-time-millis: 900000 # 配置一个连接在池中最大生存的时间,单位:毫秒
validation-query: SELECT 1 FROM DUAL # 配置检测连接是否有效
test-while-idle: true
test-on-borrow: false
test-on-return: false
primary: master
datasource:
master:
url: jdbc:dm://172.16.46.247:1050?schema=RUOYI-VUE-PRO
username: SYSDBA
password: pgbsci6ddJ6Sqj@e
slave: # 模拟从库,可根据自己需要修改 # 模拟从库,可根据自己需要修改
lazy: true # 开启懒加载,保证启动速度
url: jdbc:dm://172.16.46.247:1050?schema=RUOYI-VUE-PRO
username: SYSDBA
password: pgbsci6ddJ6Sqj@e
# Redis 配置。Redisson 默认的配置足够使用,一般不需要进行调优
data:
redis:
host: 172.16.46.63 # 地址
port: 30379 # 端口
database: 0 # 数据库索引
# password: 123456 # 密码,建议生产环境开启
xxl:
job:
admin:
addresses: http://172.16.46.63:30082/xxl-job-admin # 调度中心部署跟地址
# Lock4j 配置项
lock4j:
acquire-timeout: 3000 # 获取分布式锁超时时间,默认为 3000 毫秒
expire: 30000 # 分布式锁的超时时间,默认为 30 毫秒
# Actuator 监控端点的配置项
management:
endpoints:
web:
base-path: /actuator # Actuator 提供的 API 接口的根目录。默认为 /actuator
exposure:
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。
# 日志文件配置
logging:
level:
# 配置自己写的 MyBatis Mapper 打印日志
com.zt.plat.module.rule.dal.mysql: debug
org.springframework.context.support.PostProcessorRegistrationDelegate: ERROR
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# ZT配置项设置当前项目所有自定义的配置
cloud:
env: # 多环境的配置项
tag: ${HOSTNAME}
security:
mock-enable: true
access-log: # 访问日志的配置项
enable: true

View File

@@ -0,0 +1,149 @@
spring:
application:
name: rule-server
profiles:
active: ${env.name}
#统一nacos配置使用 profile 管理
cloud:
nacos:
server-addr: ${config.server-addr} # Nacos 服务器地址
username: ${config.username} # Nacos 账号
password: ${config.password} # Nacos 密码
discovery: # 【配置中心】配置项
namespace: ${config.namespace} # 命名空间。这里使用 maven Profile 资源过滤进行动态替换
group: ${config.group} # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
metadata:
version: 1.0.0 # 服务实例的版本号,可用于灰度发布
config: # 【注册中心】配置项
namespace: ${config.namespace} # 命名空间。这里使用 maven Profile 资源过滤进行动态替换
group: ${config.group} # 使用的 Nacos 配置分组,默认为 DEFAULT_GROUP
main:
allow-circular-references: true # 允许循环依赖,因为项目是三层架构,无法避免这个情况。
allow-bean-definition-overriding: true # 允许 Bean 覆盖,例如说 Feign 等会存在重复定义的服务
config:
import:
- optional:classpath:application-${spring.profiles.active}.yaml # 加载【本地】配置
- optional:nacos:${spring.application.name}-${spring.profiles.active}.yaml # 加载【Nacos】的配置
# Servlet 配置
servlet:
# 文件上传相关配置项
multipart:
max-file-size: 16MB # 单个文件大小
max-request-size: 32MB # 设置总上传的文件大小
# Jackson 配置项
jackson:
serialization:
write-dates-as-timestamps: true # 设置 LocalDateTime 的格式,使用时间戳
write-date-timestamps-as-nanoseconds: false # 设置不使用 nanoseconds 的格式。例如说 1611460870.401,而是直接 1611460870401
write-durations-as-timestamps: true # 设置 Duration 的格式,使用时间戳
fail-on-empty-beans: false # 允许序列化无属性的 Bean
time-zone: Asia/Shanghai
# Cache 配置项
cache:
type: REDIS
redis:
time-to-live: 1h # 设置过期时间为 1 小时
server:
port: 48101
logging:
file:
name: ${user.home}/logs/${spring.application.name}.log # 日志文件名,全路径
springdoc:
api-docs:
enabled: true # 1. 是否开启 Swagger 接文档的元数据
path: /v3/api-docs
swagger-ui:
enabled: true # 2.1 是否开启 Swagger 文档的官方 UI 界面
path: /swagger-ui.html
default-flat-param-object: true # 参见 https://doc.xiaominfo.com/docs/faq/v4/knife4j-parameterobject-flat-param 文档
knife4j:
enable: true # 2.2 是否开启 Swagger 文档的 Knife4j UI 界面
setting:
language: zh_cn
# MyBatis Plus 的配置项
mybatis-plus:
configuration:
map-underscore-to-camel-case: true # 虽然默认为 true ,但是还是显示去指定下。
global-config:
db-config:
id-type: NONE # “智能”模式,基于 IdTypeEnvironmentPostProcessor + 数据源的类型,自动适配成 AUTO、INPUT 模式。
# id-type: AUTO # 自增 ID适合 MySQL 等直接自增的数据库
# id-type: INPUT # 用户输入 ID适合 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库
# id-type: ASSIGN_ID # 分配 ID默认使用雪花算法。注意Oracle、PostgreSQL、Kingbase、DB2、H2 数据库时,需要去除实体类上的 @KeySequence 注解
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
banner: false # 关闭控制台的 Banner 打印
type-aliases-package: com.zt.plat.module.*.dal.dataobject
encryptor:
password: XDV71a+xqStEA3WH # 加解密的秘钥,可使用 https://www.imaegoo.com/2020/aes-key-generator/ 网站生成
mybatis-plus-join:
banner: false # 关闭控制台的 Banner 打印
# VO 转换(数据翻译)相关
easy-trans:
is-enable-global: false # 启用全局翻译(拦截所有 SpringMVC ResponseBody 进行自动翻译 )。如果对于性能要求很高可关闭此配置,或通过 @IgnoreTrans 忽略某个接口
xxl:
job:
executor:
appname: ${spring.application.name} # 执行器 AppName
logpath: ${user.home}/logs/xxl-job/${spring.application.name} # 执行器运行日志文件存储磁盘路径
accessToken: default_token # 执行器通讯TOKEN
cloud:
info:
version: 1.0.0
base-package: com.zt.plat.module.rule
web:
admin-ui:
url: http://dashboard.cloud.iocoder.cn # Admin 管理后台 UI 的地址
xss:
enable: false
exclude-urls: # 如下两个 url仅仅是为了演示去掉配置也没关系
- ${spring.boot.admin.context-path}/** # 不处理 Spring Boot Admin 的请求
- ${management.endpoints.web.base-path}/** # 不处理 Actuator 的请求
swagger:
title: 管理后台
description: 提供管理员管理的所有功能
version: ${cloud.info.version}
tenant: # 多租户相关配置项
enable: true
# LiteFlow 配置
liteflow:
# 规则来源支持本地文件、zookeeper、nacos、etcd、sql、apollo、redis等
rule-source: classpath:liteflow/*.json
# 启用
enable: true
# 解析格式,支持 xml,json,yml
parse-type: json
# 是否开启主要过程耗时统计
main-executor-works: true
# 是否开启监控
monitor:
enable-log: true
# 是否支持多种类型的规则文件混合
support-multiple-type: true
# 全局重试次数
retry-count: 0
# 并行执行器的最大等待时间(毫秒)
when-max-wait-time: 15000
# 并行执行器的最大等待时间是否开启
when-max-wait-time-enable: true
# 异步线程的最大等待时间(毫秒)
async-max-wait-time: 60000
# 异步线程池最大线程数
thread-executor-max-pool-size: 256
debug: false

View File

@@ -0,0 +1,36 @@
{
"flow": {
"nodes": [
{
"id": "numberCompareNode",
"name": "数值比较节点",
"type": "common",
"clazz": "com.zt.plat.module.rule.framework.liteflow.component.common.NumberCompareComponent"
},
{
"id": "stringConditionNode",
"name": "字符串条件节点",
"type": "common",
"clazz": "com.zt.plat.module.rule.framework.liteflow.component.common.StringConditionComponent"
},
{
"id": "mathCalculateNode",
"name": "数学计算节点",
"type": "common",
"clazz": "com.zt.plat.module.rule.framework.liteflow.component.action.MathCalculateComponent"
},
{
"id": "dataSetNode",
"name": "数据设置节点",
"type": "common",
"clazz": "com.zt.plat.module.rule.framework.liteflow.component.action.DataSetComponent"
}
],
"chains": [
{
"name": "示例规则链",
"condition": "THEN(numberCompareNode, mathCalculateNode, dataSetNode)"
}
]
}
}

View File

@@ -0,0 +1,31 @@
-- ----------------------------
-- Table structure for rule_rule
-- ----------------------------
DROP TABLE IF EXISTS `rule_rule`;
CREATE TABLE `rule_rule` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '规则ID',
`name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '规则名称',
`description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '规则描述',
`type` tinyint NOT NULL COMMENT '规则类型1-原子规则 2-链式规则',
`status` tinyint NOT NULL DEFAULT '1' COMMENT '规则状态0-禁用 1-启用',
`config` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '规则配置JSON格式',
`chain_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'LiteFlow规则链ID',
`version` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '1.0.0' COMMENT '规则版本',
`sort` int DEFAULT '1' COMMENT '排序',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT '0' COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `uk_chain_id` (`chain_id`,`deleted`,`tenant_id`) USING BTREE COMMENT '规则链ID唯一索引',
KEY `idx_name` (`name`) USING BTREE COMMENT '规则名称索引',
KEY `idx_status` (`status`) USING BTREE COMMENT '规则状态索引',
KEY `idx_type` (`type`) USING BTREE COMMENT '规则类型索引'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='规则表';
-- ----------------------------
-- Records of rule_rule
-- ----------------------------
INSERT INTO `rule_rule` VALUES (1, '用户积分计算规则', '根据用户行为计算积分奖励', 2, 1, '{"chain":{"chainId":"userPointsChain","chainName":"用户积分计算链","expression":"THEN(numberCompareNode, mathCalculateNode, dataSetNode)","enable":true},"nodes":[{"nodeId":"numberCompareNode","nodeName":"数值比较","nodeType":"common","clazz":"com.zt.plat.module.rule.framework.liteflow.component.common.NumberCompareComponent","properties":{"leftValue":"100","operator":">","rightValue":"50"},"enable":true},{"nodeId":"mathCalculateNode","nodeName":"积分计算","nodeType":"common","clazz":"com.zt.plat.module.rule.framework.liteflow.component.action.MathCalculateComponent","properties":{"leftValue":"100","operator":"*","rightValue":"1.5","resultKey":"finalPoints","scale":"0"},"enable":true},{"nodeId":"dataSetNode","nodeName":"设置结果","nodeType":"common","clazz":"com.zt.plat.module.rule.framework.liteflow.component.action.DataSetComponent","properties":{"dataKey":"userPoints","dataValue":"150","valueType":"integer"},"enable":true}]}', 'userPointsChain', '1.0.0', 1, '', '2024-01-01 00:00:00', '', '2024-01-01 00:00:00', b'0', 1);

View File

@@ -0,0 +1,222 @@
package com.zt.plat.module.rule.example;
import cn.hutool.json.JSONUtil;
import com.zt.plat.module.rule.controller.admin.rule.vo.RuleExecuteReqVO;
import lombok.extern.slf4j.Slf4j;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 规则引擎使用示例
* 演示如何使用JSON配置来定义和组合原子规则
*
* @author ZT源码
*/
@Slf4j
public class RuleEngineExample {
/**
* 示例1用户积分计算规则
* 规则逻辑如果用户消费金额大于100元则积分=消费金额*1.5,否则积分=消费金额*1.0
*/
public static String buildUserPointsRule() {
RuleExecuteReqVO.RuleConfig ruleConfig = new RuleExecuteReqVO.RuleConfig();
// 设置规则链
RuleExecuteReqVO.ChainConfig chain = new RuleExecuteReqVO.ChainConfig();
chain.setChainId("userPointsChain");
chain.setChainName("用户积分计算链");
chain.setExpression("IF(amountCheck, THEN(highLevelCalc, setHighPoints), THEN(normalCalc, setNormalPoints))");
chain.setEnable(true);
ruleConfig.setChain(chain);
// 设置节点列表
List<RuleExecuteReqVO.NodeConfig> nodes = new ArrayList<>();
// 节点1金额检查
RuleExecuteReqVO.NodeConfig amountCheckNode = new RuleExecuteReqVO.NodeConfig();
amountCheckNode.setNodeId("amountCheck");
amountCheckNode.setNodeName("金额检查");
amountCheckNode.setNodeType("condition");
amountCheckNode.setClazz("com.zt.plat.module.rule.framework.liteflow.component.common.NumberCompareComponent");
Map<String, Object> amountCheckProps = new HashMap<>();
amountCheckProps.put("leftValue", "#{amount}"); // 从上下文获取
amountCheckProps.put("operator", ">");
amountCheckProps.put("rightValue", "100");
amountCheckNode.setProperties(amountCheckProps);
amountCheckNode.setEnable(true);
nodes.add(amountCheckNode);
// 节点2高等级计算
RuleExecuteReqVO.NodeConfig highLevelCalcNode = new RuleExecuteReqVO.NodeConfig();
highLevelCalcNode.setNodeId("highLevelCalc");
highLevelCalcNode.setNodeName("高等级积分计算");
highLevelCalcNode.setNodeType("common");
highLevelCalcNode.setClazz("com.zt.plat.module.rule.framework.liteflow.component.action.MathCalculateComponent");
Map<String, Object> highCalcProps = new HashMap<>();
highCalcProps.put("leftValue", "#{amount}");
highCalcProps.put("operator", "*");
highCalcProps.put("rightValue", "1.5");
highCalcProps.put("resultKey", "points");
highCalcProps.put("scale", "0");
highLevelCalcNode.setProperties(highCalcProps);
highLevelCalcNode.setEnable(true);
nodes.add(highLevelCalcNode);
// 节点3普通计算
RuleExecuteReqVO.NodeConfig normalCalcNode = new RuleExecuteReqVO.NodeConfig();
normalCalcNode.setNodeId("normalCalc");
normalCalcNode.setNodeName("普通积分计算");
normalCalcNode.setNodeType("common");
normalCalcNode.setClazz("com.zt.plat.module.rule.framework.liteflow.component.action.MathCalculateComponent");
Map<String, Object> normalCalcProps = new HashMap<>();
normalCalcProps.put("leftValue", "#{amount}");
normalCalcProps.put("operator", "*");
normalCalcProps.put("rightValue", "1.0");
normalCalcProps.put("resultKey", "points");
normalCalcProps.put("scale", "0");
normalCalcNode.setProperties(normalCalcProps);
normalCalcNode.setEnable(true);
nodes.add(normalCalcNode);
// 节点4设置高积分结果
RuleExecuteReqVO.NodeConfig setHighPointsNode = new RuleExecuteReqVO.NodeConfig();
setHighPointsNode.setNodeId("setHighPoints");
setHighPointsNode.setNodeName("设置高积分结果");
setHighPointsNode.setNodeType("common");
setHighPointsNode.setClazz("com.zt.plat.module.rule.framework.liteflow.component.action.DataSetComponent");
Map<String, Object> setHighProps = new HashMap<>();
setHighProps.put("dataKey", "level");
setHighProps.put("dataValue", "VIP");
setHighProps.put("valueType", "string");
setHighPointsNode.setProperties(setHighProps);
setHighPointsNode.setEnable(true);
nodes.add(setHighPointsNode);
// 节点5设置普通积分结果
RuleExecuteReqVO.NodeConfig setNormalPointsNode = new RuleExecuteReqVO.NodeConfig();
setNormalPointsNode.setNodeId("setNormalPoints");
setNormalPointsNode.setNodeName("设置普通积分结果");
setNormalPointsNode.setNodeType("common");
setNormalPointsNode.setClazz("com.zt.plat.module.rule.framework.liteflow.component.action.DataSetComponent");
Map<String, Object> setNormalProps = new HashMap<>();
setNormalProps.put("dataKey", "level");
setNormalProps.put("dataValue", "NORMAL");
setNormalProps.put("valueType", "string");
setNormalPointsNode.setProperties(setNormalProps);
setNormalPointsNode.setEnable(true);
nodes.add(setNormalPointsNode);
ruleConfig.setNodes(nodes);
return JSONUtil.toJsonPrettyStr(ruleConfig);
}
/**
* 示例2商品折扣规则
* 规则逻辑VIP用户享受8折普通用户满200元享受9折否则无折扣
*/
public static String buildDiscountRule() {
RuleExecuteReqVO.RuleConfig ruleConfig = new RuleExecuteReqVO.RuleConfig();
// 设置规则链
RuleExecuteReqVO.ChainConfig chain = new RuleExecuteReqVO.ChainConfig();
chain.setChainId("discountChain");
chain.setChainName("商品折扣计算链");
chain.setExpression("IF(vipCheck, THEN(setVipDiscount), IF(amountCheck, setNormalDiscount, setNoDiscount))");
chain.setEnable(true);
ruleConfig.setChain(chain);
// 设置节点列表
List<RuleExecuteReqVO.NodeConfig> nodes = new ArrayList<>();
// 节点1VIP检查
RuleExecuteReqVO.NodeConfig vipCheckNode = new RuleExecuteReqVO.NodeConfig();
vipCheckNode.setNodeId("vipCheck");
vipCheckNode.setNodeName("VIP用户检查");
vipCheckNode.setNodeType("condition");
vipCheckNode.setClazz("com.zt.plat.module.rule.framework.liteflow.component.common.StringConditionComponent");
Map<String, Object> vipCheckProps = new HashMap<>();
vipCheckProps.put("sourceValue", "#{userLevel}");
vipCheckProps.put("operator", "equals");
vipCheckProps.put("targetValue", "VIP");
vipCheckNode.setProperties(vipCheckProps);
vipCheckNode.setEnable(true);
nodes.add(vipCheckNode);
// 节点2金额检查
RuleExecuteReqVO.NodeConfig amountCheckNode = new RuleExecuteReqVO.NodeConfig();
amountCheckNode.setNodeId("amountCheck");
amountCheckNode.setNodeName("金额检查");
amountCheckNode.setNodeType("condition");
amountCheckNode.setClazz("com.zt.plat.module.rule.framework.liteflow.component.common.NumberCompareComponent");
Map<String, Object> amountCheckProps = new HashMap<>();
amountCheckProps.put("leftValue", "#{totalAmount}");
amountCheckProps.put("operator", ">=");
amountCheckProps.put("rightValue", "200");
amountCheckNode.setProperties(amountCheckProps);
amountCheckNode.setEnable(true);
nodes.add(amountCheckNode);
// 节点3设置VIP折扣
RuleExecuteReqVO.NodeConfig setVipDiscountNode = new RuleExecuteReqVO.NodeConfig();
setVipDiscountNode.setNodeId("setVipDiscount");
setVipDiscountNode.setNodeName("设置VIP折扣");
setVipDiscountNode.setNodeType("common");
setVipDiscountNode.setClazz("com.zt.plat.module.rule.framework.liteflow.component.action.DataSetComponent");
Map<String, Object> setVipProps = new HashMap<>();
setVipProps.put("dataKey", "discount");
setVipProps.put("dataValue", "0.8");
setVipProps.put("valueType", "double");
setVipDiscountNode.setProperties(setVipProps);
setVipDiscountNode.setEnable(true);
nodes.add(setVipDiscountNode);
// 节点4设置普通折扣
RuleExecuteReqVO.NodeConfig setNormalDiscountNode = new RuleExecuteReqVO.NodeConfig();
setNormalDiscountNode.setNodeId("setNormalDiscount");
setNormalDiscountNode.setNodeName("设置普通折扣");
setNormalDiscountNode.setNodeType("common");
setNormalDiscountNode.setClazz("com.zt.plat.module.rule.framework.liteflow.component.action.DataSetComponent");
Map<String, Object> setNormalProps = new HashMap<>();
setNormalProps.put("dataKey", "discount");
setNormalProps.put("dataValue", "0.9");
setNormalProps.put("valueType", "double");
setNormalDiscountNode.setProperties(setNormalProps);
setNormalDiscountNode.setEnable(true);
nodes.add(setNormalDiscountNode);
// 节点5设置无折扣
RuleExecuteReqVO.NodeConfig setNoDiscountNode = new RuleExecuteReqVO.NodeConfig();
setNoDiscountNode.setNodeId("setNoDiscount");
setNoDiscountNode.setNodeName("设置无折扣");
setNoDiscountNode.setNodeType("common");
setNoDiscountNode.setClazz("com.zt.plat.module.rule.framework.liteflow.component.action.DataSetComponent");
Map<String, Object> setNoProps = new HashMap<>();
setNoProps.put("dataKey", "discount");
setNoProps.put("dataValue", "1.0");
setNoProps.put("valueType", "double");
setNoDiscountNode.setProperties(setNoProps);
setNoDiscountNode.setEnable(true);
nodes.add(setNoDiscountNode);
ruleConfig.setNodes(nodes);
return JSONUtil.toJsonPrettyStr(ruleConfig);
}
/**
* 打印示例配置
*/
public static void main(String[] args) {
log.info("=== 用户积分计算规则配置 ===");
log.info(buildUserPointsRule());
log.info("\n=== 商品折扣规则配置 ===");
log.info(buildDiscountRule());
}
}

View File

@@ -0,0 +1,193 @@
package com.zt.plat.module.rule.service.rule;
import com.zt.plat.framework.test.core.ut.BaseDbUnitTest;
import com.zt.plat.module.rule.controller.admin.rule.vo.*;
import com.zt.plat.module.rule.dal.dataobject.rule.RuleDO;
import com.zt.plat.module.rule.dal.mysql.rule.RuleMapper;
import com.zt.plat.module.rule.framework.liteflow.service.LiteFlowService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.Import;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
import static com.zt.plat.framework.test.core.util.AssertUtils.*;
import static com.zt.plat.framework.test.core.util.RandomUtils.*;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;
/**
* {@link RuleServiceImpl} 的单元测试类
*
* @author ZT源码
*/
@Import(RuleServiceImpl.class)
public class RuleServiceImplTest extends BaseDbUnitTest {
@Resource
private RuleServiceImpl ruleService;
@Resource
private RuleMapper ruleMapper;
@MockBean
private LiteFlowService liteFlowService;
@Test
public void testCreateRule_success() {
// 准备参数
RuleCreateReqVO createReqVO = randomPojo(RuleCreateReqVO.class, o -> {
o.setName("测试规则");
o.setType(1);
o.setStatus(1);
o.setConfig(buildValidRuleConfig());
});
// mock 方法
when(liteFlowService.validateRuleConfig(anyString())).thenReturn(true);
// 调用
Long ruleId = ruleService.createRule(createReqVO);
// 断言
assertNotNull(ruleId);
RuleDO rule = ruleMapper.selectById(ruleId);
assertNotNull(rule);
assertEquals("测试规则", rule.getName());
assertEquals(1, rule.getType());
assertEquals(1, rule.getStatus());
assertEquals("1.0.0", rule.getVersion());
}
@Test
public void testExecuteRule_success() {
// 准备数据
RuleDO rule = randomPojo(RuleDO.class, o -> {
o.setId(1L);
o.setName("测试规则");
o.setStatus(1);
o.setChainId("testChain");
o.setConfig(buildValidRuleConfig());
});
ruleMapper.insert(rule);
// 准备参数
RuleExecuteReqVO executeReqVO = new RuleExecuteReqVO();
executeReqVO.setRuleId(1L);
Map<String, Object> contextData = new HashMap<>();
contextData.put("amount", 100);
executeReqVO.setContextData(contextData);
// mock 方法
Map<String, Object> mockResult = new HashMap<>();
mockResult.put("success", true);
mockResult.put("result", "执行成功");
when(liteFlowService.executeRule(eq("testChain"), any(), any())).thenReturn(mockResult);
// 调用
RuleExecuteRespVO result = ruleService.executeRule(executeReqVO);
// 断言
assertTrue(result.getSuccess());
assertNotNull(result.getResultData());
assertEquals("testChain", result.getChainId());
assertNotNull(result.getExecutionTime());
}
@Test
public void testValidateRuleConfig_success() {
// 准备参数
String config = buildValidRuleConfig();
// mock 方法
when(liteFlowService.validateRuleConfig(config)).thenReturn(true);
// 调用
Boolean result = ruleService.validateRuleConfig(config);
// 断言
assertTrue(result);
verify(liteFlowService).validateRuleConfig(config);
}
@Test
public void testEnableRule_success() {
// 准备数据
RuleDO rule = randomPojo(RuleDO.class, o -> {
o.setId(1L);
o.setStatus(0);
o.setChainId("testChain");
o.setConfig(buildValidRuleConfig());
});
ruleMapper.insert(rule);
// 调用
ruleService.enableRule(1L);
// 断言
RuleDO updatedRule = ruleMapper.selectById(1L);
assertEquals(1, updatedRule.getStatus());
verify(liteFlowService).loadRule(any(RuleDO.class));
}
@Test
public void testDisableRule_success() {
// 准备数据
RuleDO rule = randomPojo(RuleDO.class, o -> {
o.setId(1L);
o.setStatus(1);
o.setChainId("testChain");
});
ruleMapper.insert(rule);
// 调用
ruleService.disableRule(1L);
// 断言
RuleDO updatedRule = ruleMapper.selectById(1L);
assertEquals(0, updatedRule.getStatus());
verify(liteFlowService).removeRule("testChain");
}
private String buildValidRuleConfig() {
return "{\n" +
" \"chain\": {\n" +
" \"chainId\": \"testChain\",\n" +
" \"chainName\": \"测试规则链\",\n" +
" \"expression\": \"THEN(numberCompareNode, mathCalculateNode)\",\n" +
" \"enable\": true\n" +
" },\n" +
" \"nodes\": [\n" +
" {\n" +
" \"nodeId\": \"numberCompareNode\",\n" +
" \"nodeName\": \"数值比较\",\n" +
" \"nodeType\": \"common\",\n" +
" \"clazz\": \"com.zt.plat.module.rule.framework.liteflow.component.common.NumberCompareComponent\",\n" +
" \"properties\": {\n" +
" \"leftValue\": \"100\",\n" +
" \"operator\": \">\",\n" +
" \"rightValue\": \"50\"\n" +
" },\n" +
" \"enable\": true\n" +
" },\n" +
" {\n" +
" \"nodeId\": \"mathCalculateNode\",\n" +
" \"nodeName\": \"数学计算\",\n" +
" \"nodeType\": \"common\",\n" +
" \"clazz\": \"com.zt.plat.module.rule.framework.liteflow.component.action.MathCalculateComponent\",\n" +
" \"properties\": {\n" +
" \"leftValue\": \"100\",\n" +
" \"operator\": \"*\",\n" +
" \"rightValue\": \"1.5\",\n" +
" \"resultKey\": \"result\"\n" +
" },\n" +
" \"enable\": true\n" +
" }\n" +
" ]\n" +
"}";
}
}