From 516ca4aefdde3b869358f2491bf18a0ae0fadbbc Mon Sep 17 00:00:00 2001 From: chenbowen Date: Sun, 21 Sep 2025 21:48:54 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E4=BC=98=E5=8C=96=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E5=85=A8=E5=B1=80=E5=85=AC=E5=8F=B8=E4=B8=8E=E9=83=A8=E9=97=A8?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E7=9A=84=E4=BD=93=E9=AA=8C=202.=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E8=A7=84=E5=88=99=E5=BC=95=E6=93=8E=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 2 +- .../yudao-module-rule-server/pom.xml | 7 + .../controller/admin/rule/RuleController.java | 131 ++++++++- .../controller/admin/rule/vo/RuleBaseVO.java | 44 +++ .../admin/rule/vo/RuleCreateReqVO.java | 14 + .../admin/rule/vo/RuleExecuteReqVO.java | 116 ++++++++ .../admin/rule/vo/RuleExecuteRespVO.java | 59 ++++ .../admin/rule/vo/RulePageReqVO.java | 32 +++ .../controller/admin/rule/vo/RuleRespVO.java | 25 ++ .../admin/rule/vo/RuleUpdateReqVO.java | 20 ++ .../module/rule/convert/rule/RuleConvert.java | 31 +++ .../rule/dal/dataobject/rule/RuleDO.java | 70 +++++ .../rule/dal/mysql/rule/RuleMapper.java | 28 ++ .../module/rule/enums/ErrorCodeConstants.java | 21 ++ .../component/action/DataSetComponent.java | 75 ++++++ .../action/MathCalculateComponent.java | 96 +++++++ .../component/base/BaseRuleComponent.java | 149 +++++++++++ .../common/NumberCompareComponent.java | 80 ++++++ .../common/StringConditionComponent.java | 98 +++++++ .../config/LiteFlowConfiguration.java | 24 ++ .../liteflow/service/LiteFlowService.java | 212 +++++++++++++++ .../module/rule/service/rule/RuleService.java | 84 ++++++ .../rule/service/rule/RuleServiceImpl.java | 252 ++++++++++++++++++ .../src/main/resources/application.yml | 26 ++ .../resources/liteflow/default-rules.json | 36 +++ .../src/main/resources/sql/rule_rule.sql | 31 +++ .../rule/example/RuleEngineExample.java | 222 +++++++++++++++ .../service/rule/RuleServiceImplTest.java | 193 ++++++++++++++ .../controller/admin/dept/DeptController.java | 8 + .../system/service/dept/DeptService.java | 9 + .../system/service/dept/DeptServiceImpl.java | 59 ++++ 31 files changed, 2241 insertions(+), 13 deletions(-) create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleBaseVO.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleCreateReqVO.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleExecuteReqVO.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleExecuteRespVO.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RulePageReqVO.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleRespVO.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleUpdateReqVO.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/convert/rule/RuleConvert.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/dal/dataobject/rule/RuleDO.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/dal/mysql/rule/RuleMapper.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/enums/ErrorCodeConstants.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/component/action/DataSetComponent.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/component/action/MathCalculateComponent.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/component/base/BaseRuleComponent.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/component/common/NumberCompareComponent.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/component/common/StringConditionComponent.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/config/LiteFlowConfiguration.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/service/LiteFlowService.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/service/rule/RuleService.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/service/rule/RuleServiceImpl.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/resources/liteflow/default-rules.json create mode 100644 yudao-module-rule/yudao-module-rule-server/src/main/resources/sql/rule_rule.sql create mode 100644 yudao-module-rule/yudao-module-rule-server/src/test/java/cn/iocoder/yudao/module/rule/example/RuleEngineExample.java create mode 100644 yudao-module-rule/yudao-module-rule-server/src/test/java/cn/iocoder/yudao/module/rule/service/rule/RuleServiceImplTest.java diff --git a/pom.xml b/pom.xml index a0233c13..3761eddd 100644 --- a/pom.xml +++ b/pom.xml @@ -23,7 +23,7 @@ yudao-module-template - + yudao-module-rule diff --git a/yudao-module-rule/yudao-module-rule-server/pom.xml b/yudao-module-rule/yudao-module-rule-server/pom.xml index 2b95508f..99005d03 100644 --- a/yudao-module-rule/yudao-module-rule-server/pom.xml +++ b/yudao-module-rule/yudao-module-rule-server/pom.xml @@ -126,6 +126,13 @@ yudao-spring-boot-starter-biz-business ${revision} + + + + com.yomahub + liteflow-spring-boot-starter + 2.15.0 + diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/RuleController.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/RuleController.java index ae37549b..6909cca6 100644 --- a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/RuleController.java +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/RuleController.java @@ -1,25 +1,132 @@ package cn.iocoder.yudao.module.rule.controller.admin.rule; -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.tags.Tag; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RestController; - import cn.iocoder.yudao.framework.common.pojo.CommonResult; +import cn.iocoder.yudao.framework.common.pojo.PageParam; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.common.util.object.BeanUtils; +import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils; +import cn.iocoder.yudao.framework.operatelog.core.annotations.OperateLog; +import cn.iocoder.yudao.module.rule.controller.admin.rule.vo.*; +import cn.iocoder.yudao.module.rule.convert.rule.RuleConvert; +import cn.iocoder.yudao.module.rule.dal.dataobject.rule.RuleDO; +import cn.iocoder.yudao.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 cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.operatelog.core.enums.OperateTypeEnum.*; -/** - * Rule 控制器 - * - * @author ZT - */ -@Tag(name = "管理后台 - Rule") +@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 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 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 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 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> getRulePage(@Valid RulePageReqVO pageReqVO) { + PageResult 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 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 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 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 disableRule(@RequestParam("id") Long id) { + ruleService.disableRule(id); + return success(true); + } + + @PostMapping("/validate") + @Operation(summary = "验证规则配置") + @PreAuthorize("@ss.hasPermission('rule:rule:validate')") + public CommonResult validateRuleConfig(@RequestBody String config) { + return success(ruleService.validateRuleConfig(config)); + } + @GetMapping("/hello") @Operation(summary = "Hello Rule") public CommonResult hello() { diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleBaseVO.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleBaseVO.java new file mode 100644 index 00000000..f911fcd1 --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleBaseVO.java @@ -0,0 +1,44 @@ +package cn.iocoder.yudao.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; + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleCreateReqVO.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleCreateReqVO.java new file mode 100644 index 00000000..85272fbf --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleCreateReqVO.java @@ -0,0 +1,14 @@ +package cn.iocoder.yudao.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 { + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleExecuteReqVO.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleExecuteReqVO.java new file mode 100644 index 00000000..fbad6fc3 --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleExecuteReqVO.java @@ -0,0 +1,116 @@ +package cn.iocoder.yudao.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 contextData; + + /** + * 扩展参数 + */ + private Map extParams; + + /** + * 规则执行配置(JSON格式的规则定义) + */ + @Data + public static class RuleConfig { + + /** + * 规则链配置 + */ + private ChainConfig chain; + + /** + * 节点配置列表 + */ + private List 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 properties; + + /** + * 是否启用 + */ + private Boolean enable; + } + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleExecuteRespVO.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleExecuteRespVO.java new file mode 100644 index 00000000..7b81489a --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleExecuteRespVO.java @@ -0,0 +1,59 @@ +package cn.iocoder.yudao.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 resultData; + + /** + * 执行耗时(毫秒) + */ + private Long executionTime; + + /** + * 执行开始时间 + */ + private LocalDateTime startTime; + + /** + * 执行结束时间 + */ + private LocalDateTime endTime; + + /** + * 执行的规则链ID + */ + private String chainId; + + /** + * 执行的节点列表 + */ + private String executedNodes; + + /** + * 执行上下文快照 + */ + private Map contextSnapshot; + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RulePageReqVO.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RulePageReqVO.java new file mode 100644 index 00000000..572d5881 --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RulePageReqVO.java @@ -0,0 +1,32 @@ +package cn.iocoder.yudao.module.rule.controller.admin.rule.vo; + +import cn.iocoder.yudao.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; + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleRespVO.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleRespVO.java new file mode 100644 index 00000000..77901151 --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleRespVO.java @@ -0,0 +1,25 @@ +package cn.iocoder.yudao.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; + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleUpdateReqVO.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleUpdateReqVO.java new file mode 100644 index 00000000..08ffb833 --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/controller/admin/rule/vo/RuleUpdateReqVO.java @@ -0,0 +1,20 @@ +package cn.iocoder.yudao.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; + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/convert/rule/RuleConvert.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/convert/rule/RuleConvert.java new file mode 100644 index 00000000..da713d7b --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/convert/rule/RuleConvert.java @@ -0,0 +1,31 @@ +package cn.iocoder.yudao.module.rule.convert.rule; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.rule.controller.admin.rule.vo.*; +import cn.iocoder.yudao.module.rule.dal.dataobject.rule.RuleDO; +import org.mapstruct.Mapper; +import org.mapstruct.factory.Mappers; + +import java.util.List; + +/** + * 规则 Convert + * + * @author 芋道源码 + */ +@Mapper +public interface RuleConvert { + + RuleConvert INSTANCE = Mappers.getMapper(RuleConvert.class); + + RuleDO convert(RuleCreateReqVO bean); + + RuleDO convert(RuleUpdateReqVO bean); + + RuleRespVO convert(RuleDO bean); + + List convertList(List list); + + PageResult convertPage(PageResult page); + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/dal/dataobject/rule/RuleDO.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/dal/dataobject/rule/RuleDO.java new file mode 100644 index 00000000..46eb6d73 --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/dal/dataobject/rule/RuleDO.java @@ -0,0 +1,70 @@ +package cn.iocoder.yudao.module.rule.dal.dataobject.rule; + +import cn.iocoder.yudao.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 芋道源码 + */ +@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; + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/dal/mysql/rule/RuleMapper.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/dal/mysql/rule/RuleMapper.java new file mode 100644 index 00000000..5e30bbba --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/dal/mysql/rule/RuleMapper.java @@ -0,0 +1,28 @@ +package cn.iocoder.yudao.module.rule.dal.mysql.rule; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; +import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; +import cn.iocoder.yudao.module.rule.controller.admin.rule.vo.RulePageReqVO; +import cn.iocoder.yudao.module.rule.dal.dataobject.rule.RuleDO; +import org.apache.ibatis.annotations.Mapper; + +/** + * 规则 Mapper + * + * @author 芋道源码 + */ +@Mapper +public interface RuleMapper extends BaseMapperX { + + default PageResult selectPage(RulePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .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)); + } + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/enums/ErrorCodeConstants.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/enums/ErrorCodeConstants.java new file mode 100644 index 00000000..2b67e6b6 --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/enums/ErrorCodeConstants.java @@ -0,0 +1,21 @@ +package cn.iocoder.yudao.module.rule.enums; + +import cn.iocoder.yudao.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, "规则验证失败"); + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/component/action/DataSetComponent.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/component/action/DataSetComponent.java new file mode 100644 index 00000000..e1952b27 --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/component/action/DataSetComponent.java @@ -0,0 +1,75 @@ +package cn.iocoder.yudao.module.rule.framework.liteflow.component.action; + +import cn.iocoder.yudao.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 芋道源码 + */ +@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 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); + } + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/component/action/MathCalculateComponent.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/component/action/MathCalculateComponent.java new file mode 100644 index 00000000..ae55422c --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/component/action/MathCalculateComponent.java @@ -0,0 +1,96 @@ +package cn.iocoder.yudao.module.rule.framework.liteflow.component.action; + +import cn.iocoder.yudao.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 芋道源码 + */ +@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); + } + } + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/component/base/BaseRuleComponent.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/component/base/BaseRuleComponent.java new file mode 100644 index 00000000..6aa6ad7f --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/component/base/BaseRuleComponent.java @@ -0,0 +1,149 @@ +package cn.iocoder.yudao.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 芋道源码 + */ +public abstract class BaseRuleComponent extends NodeComponent { + + /** + * 获取节点配置属性 + * + * @param key 属性键 + * @param clazz 属性类型 + * @return 属性值 + */ + @SuppressWarnings("unchecked") + protected T getNodeProperty(String key, Class 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 configMap = (Map) 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 getContextData(String key, Class 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 convertValue(Object value, Class 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()); + } + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/component/common/NumberCompareComponent.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/component/common/NumberCompareComponent.java new file mode 100644 index 00000000..c2c3b46f --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/component/common/NumberCompareComponent.java @@ -0,0 +1,80 @@ +package cn.iocoder.yudao.module.rule.framework.liteflow.component.common; + +import cn.iocoder.yudao.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 芋道源码 + */ +@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); + } + } + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/component/common/StringConditionComponent.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/component/common/StringConditionComponent.java new file mode 100644 index 00000000..0b411c0b --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/component/common/StringConditionComponent.java @@ -0,0 +1,98 @@ +package cn.iocoder.yudao.module.rule.framework.liteflow.component.common; + +import cn.hutool.core.util.StrUtil; +import cn.iocoder.yudao.module.rule.framework.liteflow.component.base.BaseRuleComponent; +import com.yomahub.liteflow.annotation.LiteflowComponent; +import lombok.extern.slf4j.Slf4j; + +/** + * 字符串判断组件 + * 支持字符串相等、包含、长度等判断 + * + * @author 芋道源码 + */ +@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); + } + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/config/LiteFlowConfiguration.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/config/LiteFlowConfiguration.java new file mode 100644 index 00000000..c5c700a5 --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/config/LiteFlowConfiguration.java @@ -0,0 +1,24 @@ +package cn.iocoder.yudao.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 芋道源码 + */ +@Configuration +public class LiteFlowConfiguration { + + /** + * 组件扫描器 + * 自动扫描LiteFlow组件 + */ + @Bean + public ComponentScanner componentScanner() { + return new ComponentScanner("cn.iocoder.yudao.module.rule.framework.liteflow.component"); + } + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/service/LiteFlowService.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/service/LiteFlowService.java new file mode 100644 index 00000000..2c08ba14 --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/framework/liteflow/service/LiteFlowService.java @@ -0,0 +1,212 @@ +package cn.iocoder.yudao.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 cn.iocoder.yudao.module.rule.controller.admin.rule.vo.RuleExecuteReqVO; +import cn.iocoder.yudao.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 芋道源码 + */ +@Service +@Slf4j +public class LiteFlowService { + + @Resource + private FlowExecutor flowExecutor; + + /** + * 执行规则 + * + * @param chainId 规则链ID + * @param contextData 上下文数据 + * @param extParams 扩展参数 + * @return 执行结果 + */ + public Map executeRule(String chainId, Map contextData, Map 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 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 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; + } + } + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/service/rule/RuleService.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/service/rule/RuleService.java new file mode 100644 index 00000000..75bf8046 --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/service/rule/RuleService.java @@ -0,0 +1,84 @@ +package cn.iocoder.yudao.module.rule.service.rule; + +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.rule.controller.admin.rule.vo.*; +import cn.iocoder.yudao.module.rule.dal.dataobject.rule.RuleDO; + +import javax.validation.Valid; + +/** + * 规则 Service 接口 + * + * @author 芋道源码 + */ +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 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); + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/service/rule/RuleServiceImpl.java b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/service/rule/RuleServiceImpl.java new file mode 100644 index 00000000..0647e3b0 --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/java/cn/iocoder/yudao/module/rule/service/rule/RuleServiceImpl.java @@ -0,0 +1,252 @@ +package cn.iocoder.yudao.module.rule.service.rule; + +import cn.hutool.core.util.StrUtil; +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil; +import cn.iocoder.yudao.framework.common.pojo.PageResult; +import cn.iocoder.yudao.module.rule.controller.admin.rule.vo.*; +import cn.iocoder.yudao.module.rule.convert.rule.RuleConvert; +import cn.iocoder.yudao.module.rule.dal.dataobject.rule.RuleDO; +import cn.iocoder.yudao.module.rule.dal.mysql.rule.RuleMapper; +import cn.iocoder.yudao.module.rule.enums.ErrorCodeConstants; +import cn.iocoder.yudao.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 cn.iocoder.yudao.module.rule.enums.ErrorCodeConstants.*; + +/** + * 规则 Service 实现类 + * + * @author 芋道源码 + */ +@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 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 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; + } + } + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/resources/application.yml b/yudao-module-rule/yudao-module-rule-server/src/main/resources/application.yml index 91295496..6bcd4fb2 100644 --- a/yudao-module-rule/yudao-module-rule-server/src/main/resources/application.yml +++ b/yudao-module-rule/yudao-module-rule-server/src/main/resources/application.yml @@ -120,4 +120,30 @@ yudao: 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 diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/resources/liteflow/default-rules.json b/yudao-module-rule/yudao-module-rule-server/src/main/resources/liteflow/default-rules.json new file mode 100644 index 00000000..ac035446 --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/resources/liteflow/default-rules.json @@ -0,0 +1,36 @@ +{ + "flow": { + "nodes": [ + { + "id": "numberCompareNode", + "name": "数值比较节点", + "type": "common", + "clazz": "cn.iocoder.yudao.module.rule.framework.liteflow.component.common.NumberCompareComponent" + }, + { + "id": "stringConditionNode", + "name": "字符串条件节点", + "type": "common", + "clazz": "cn.iocoder.yudao.module.rule.framework.liteflow.component.common.StringConditionComponent" + }, + { + "id": "mathCalculateNode", + "name": "数学计算节点", + "type": "common", + "clazz": "cn.iocoder.yudao.module.rule.framework.liteflow.component.action.MathCalculateComponent" + }, + { + "id": "dataSetNode", + "name": "数据设置节点", + "type": "common", + "clazz": "cn.iocoder.yudao.module.rule.framework.liteflow.component.action.DataSetComponent" + } + ], + "chains": [ + { + "name": "示例规则链", + "condition": "THEN(numberCompareNode, mathCalculateNode, dataSetNode)" + } + ] + } +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/main/resources/sql/rule_rule.sql b/yudao-module-rule/yudao-module-rule-server/src/main/resources/sql/rule_rule.sql new file mode 100644 index 00000000..838441fa --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/main/resources/sql/rule_rule.sql @@ -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":"cn.iocoder.yudao.module.rule.framework.liteflow.component.common.NumberCompareComponent","properties":{"leftValue":"100","operator":">","rightValue":"50"},"enable":true},{"nodeId":"mathCalculateNode","nodeName":"积分计算","nodeType":"common","clazz":"cn.iocoder.yudao.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":"cn.iocoder.yudao.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); \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/test/java/cn/iocoder/yudao/module/rule/example/RuleEngineExample.java b/yudao-module-rule/yudao-module-rule-server/src/test/java/cn/iocoder/yudao/module/rule/example/RuleEngineExample.java new file mode 100644 index 00000000..1e0a423c --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/test/java/cn/iocoder/yudao/module/rule/example/RuleEngineExample.java @@ -0,0 +1,222 @@ +package cn.iocoder.yudao.module.rule.example; + +import cn.hutool.json.JSONUtil; +import cn.iocoder.yudao.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 芋道源码 + */ +@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 nodes = new ArrayList<>(); + + // 节点1:金额检查 + RuleExecuteReqVO.NodeConfig amountCheckNode = new RuleExecuteReqVO.NodeConfig(); + amountCheckNode.setNodeId("amountCheck"); + amountCheckNode.setNodeName("金额检查"); + amountCheckNode.setNodeType("condition"); + amountCheckNode.setClazz("cn.iocoder.yudao.module.rule.framework.liteflow.component.common.NumberCompareComponent"); + Map 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("cn.iocoder.yudao.module.rule.framework.liteflow.component.action.MathCalculateComponent"); + Map 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("cn.iocoder.yudao.module.rule.framework.liteflow.component.action.MathCalculateComponent"); + Map 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("cn.iocoder.yudao.module.rule.framework.liteflow.component.action.DataSetComponent"); + Map 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("cn.iocoder.yudao.module.rule.framework.liteflow.component.action.DataSetComponent"); + Map 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 nodes = new ArrayList<>(); + + // 节点1:VIP检查 + RuleExecuteReqVO.NodeConfig vipCheckNode = new RuleExecuteReqVO.NodeConfig(); + vipCheckNode.setNodeId("vipCheck"); + vipCheckNode.setNodeName("VIP用户检查"); + vipCheckNode.setNodeType("condition"); + vipCheckNode.setClazz("cn.iocoder.yudao.module.rule.framework.liteflow.component.common.StringConditionComponent"); + Map 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("cn.iocoder.yudao.module.rule.framework.liteflow.component.common.NumberCompareComponent"); + Map 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("cn.iocoder.yudao.module.rule.framework.liteflow.component.action.DataSetComponent"); + Map 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("cn.iocoder.yudao.module.rule.framework.liteflow.component.action.DataSetComponent"); + Map 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("cn.iocoder.yudao.module.rule.framework.liteflow.component.action.DataSetComponent"); + Map 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()); + } + +} \ No newline at end of file diff --git a/yudao-module-rule/yudao-module-rule-server/src/test/java/cn/iocoder/yudao/module/rule/service/rule/RuleServiceImplTest.java b/yudao-module-rule/yudao-module-rule-server/src/test/java/cn/iocoder/yudao/module/rule/service/rule/RuleServiceImplTest.java new file mode 100644 index 00000000..e70a0c01 --- /dev/null +++ b/yudao-module-rule/yudao-module-rule-server/src/test/java/cn/iocoder/yudao/module/rule/service/rule/RuleServiceImplTest.java @@ -0,0 +1,193 @@ +package cn.iocoder.yudao.module.rule.service.rule; + +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; +import cn.iocoder.yudao.module.rule.controller.admin.rule.vo.*; +import cn.iocoder.yudao.module.rule.dal.dataobject.rule.RuleDO; +import cn.iocoder.yudao.module.rule.dal.mysql.rule.RuleMapper; +import cn.iocoder.yudao.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 cn.iocoder.yudao.framework.test.core.util.AssertUtils.*; +import static cn.iocoder.yudao.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 芋道源码 + */ +@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 contextData = new HashMap<>(); + contextData.put("amount", 100); + executeReqVO.setContextData(contextData); + + // mock 方法 + Map 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\": \"cn.iocoder.yudao.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\": \"cn.iocoder.yudao.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" + + "}"; + } + +} \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/DeptController.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/DeptController.java index c5e1b031..424a98b6 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/DeptController.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/controller/admin/dept/DeptController.java @@ -80,6 +80,14 @@ public class DeptController { return success(BeanUtils.toBean(list, DeptSimpleRespVO.class)); } + @GetMapping(value = {"/simple-user-dept-list"}) + @Operation(summary = "获取当前用户归属部门精简信息列表", description = "只包含当前用户归属的启用部门,主要用于前端的下拉选项") + @Parameter(name = "companyId", description = "公司ID,可选参数,用于过滤指定公司下的部门", required = false, example = "1") + public CommonResult> getCurrentUserDeptList(@RequestParam(value = "companyId", required = false) Long companyId) { + List list = deptService.getCurrentUserDeptList(companyId); + return success(BeanUtils.toBean(list, DeptSimpleRespVO.class)); + } + @GetMapping(value = {"/list-company-simple", "/simple-company-list"}) @Operation(summary = "获取用户所属公司精简信息列表", description = "只包含被开启的部门,主要用于前端的下拉选项") public CommonResult> getSimpleCompanyList() { diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java index 883f9815..82c2a8ed 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptService.java @@ -117,6 +117,15 @@ public interface DeptService { List getUserCompanyList(); + /** + * 获取当前用户归属的部门列表 + * 只返回当前登录用户归属的启用部门 + * + * @param companyId 可选的公司ID,用于过滤指定公司下的部门 + * @return 当前用户归属的部门列表 + */ + List getCurrentUserDeptList(Long companyId); + Set getCompanyDeptInfoListByUserId(Long userId); /** diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java index e084d2a6..f479f413 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/service/dept/DeptServiceImpl.java @@ -292,6 +292,65 @@ public class DeptServiceImpl implements DeptService { return getDeptList(companyIds); } + /** + * 获取当前用户归属的部门列表 + * 只返回当前登录用户归属的启用部门 + * + * @return 当前用户归属的部门列表 + */ + @Override + public List getCurrentUserDeptList(Long companyId) { + Set deptIds = userDeptMapper.selectValidListByUserIds(singleton(getLoginUserId())) + .stream() + .map(UserDeptDO::getDeptId) + .collect(Collectors.toSet()); + if (CollUtil.isEmpty(deptIds)) { + return Collections.emptyList(); + } + // 获取部门信息并过滤启用的部门 + List deptList = getDeptList(deptIds).stream() + .filter(dept -> CommonStatusEnum.ENABLE.getStatus().equals(dept.getStatus())) + .collect(Collectors.toList()); + + // 如果指定了公司ID,则进一步过滤属于该公司的部门 + if (companyId != null) { + return deptList.stream() + .filter(dept -> isUnderCompany(dept, companyId)) + .collect(Collectors.toList()); + } + + return deptList; + } + + /** + * 判断部门是否属于指定公司 + * + * @param dept 部门 + * @param companyId 公司ID + * @return 是否属于指定公司 + */ + private boolean isUnderCompany(DeptDO dept, Long companyId) { + if (dept == null || companyId == null) { + return false; + } + + // 如果部门本身就是指定的公司 + if (dept.getId().equals(companyId) && Boolean.TRUE.equals(dept.getIsCompany())) { + return true; + } + + // 向上递归查找,看是否有祖先部门是指定的公司 + DeptDO current = dept; + while (current != null && current.getParentId() != null && !DeptDO.PARENT_ID_ROOT.equals(current.getParentId())) { + current = getDept(current.getParentId()); + if (current != null && current.getId().equals(companyId) && Boolean.TRUE.equals(current.getIsCompany())) { + return true; + } + } + + return false; + } + /** * 根据用户ID查询其归属公司及直属部门关系列表(不递归下级公司) */