Pārlūkot izejas kodu

调整工作流分配规则配置

zjc 1 gadu atpakaļ
vecāks
revīzija
af9fcdaf47
59 mainītis faili ar 2138 papildinājumiem un 120 dzēšanām
  1. 2 0
      yudao-framework/pom.xml
  2. 46 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringAopUtils.java
  3. 58 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerDecorator.java
  4. 42 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/TenantRedisMessageInterceptor.java
  5. 47 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java
  6. 27 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java
  7. 37 0
      yudao-framework/yudao-spring-boot-starter-flowable/pom.xml
  8. 43 0
      yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/config/YudaoFlowableConfiguration.java
  9. 1 0
      yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/package-info.java
  10. 82 0
      yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/FlowableUtils.java
  11. 35 0
      yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/web/FlowableWebFilter.java
  12. 1 0
      yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/package-info.java
  13. 1 0
      yudao-framework/yudao-spring-boot-starter-flowable/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  14. 162 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/config/YudaoMQAutoConfiguration.java
  15. 87 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/RedisMQTemplate.java
  16. 26 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/interceptor/RedisMessageInterceptor.java
  17. 29 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/message/AbstractRedisMessage.java
  18. 21 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/pubsub/AbstractChannelMessage.java
  19. 103 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/pubsub/AbstractChannelMessageListener.java
  20. 21 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/stream/AbstractStreamMessage.java
  21. 113 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/stream/AbstractStreamMessageListener.java
  22. 80 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/job/RedisPendingMessageResendJob.java
  23. 62 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/org/springframework/data/redis/stream/DefaultStreamMessageListenerContainerX.java
  24. 24 0
      yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/expression/AndExpressionX.java
  25. 24 0
      yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/expression/OrExpressionX.java
  26. 1 0
      yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/package-info.java
  27. 9 0
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/resilience4j/package-info.java
  28. 1 0
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md
  29. 113 0
      yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/RedisKeyDefine.java
  30. 28 0
      yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/RedisKeyRegistry.java
  31. 85 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLog.java
  32. 107 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLog.java
  33. 14 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketHandlerConfig.java
  34. 24 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/UserHandshakeInterceptor.java
  35. 9 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketKeyDefine.java
  36. 24 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketMessageDO.java
  37. 36 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketSessionHandler.java
  38. 31 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketUtils.java
  39. 49 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/YudaoWebSocketHandlerDecorator.java
  40. 33 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTaskAssignRuleTypeEnum.java
  41. 30 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTaskRuleScriptEnum.java
  42. 8 7
      yudao-module-bpm/yudao-module-bpm-biz/pom.xml
  43. 23 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleBaseVO.java
  44. 24 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleCreateReqVO.java
  45. 28 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleRespVO.java
  46. 20 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleUpdateReqVO.java
  47. 12 10
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskAssignRuleController.java
  48. 0 44
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/BpmTaskAssignRuleRespVO.java
  49. 40 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmTaskAssignRuleConvert.java
  50. 83 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmTaskAssignRuleDO.java
  51. 0 51
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmTaskAssignRuleDO.java
  52. 24 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/task/BpmTaskAssignRuleMapper.java
  53. 1 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/web/config/BpmWebConfiguration.java
  54. 8 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java
  55. 13 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
  56. 8 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionService.java
  57. 5 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java
  58. 24 2
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskAssignRuleService.java
  59. 49 4
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskAssignRuleServiceImpl.java

+ 2 - 0
yudao-framework/pom.xml

@@ -28,6 +28,8 @@
         <module>yudao-spring-boot-starter-biz-tenant</module>
         <module>yudao-spring-boot-starter-biz-data-permission</module>
         <module>yudao-spring-boot-starter-biz-ip</module>
+
+        <module>yudao-spring-boot-starter-flowable</module>
     </modules>
 
     <artifactId>yudao-framework</artifactId>

+ 46 - 0
yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringAopUtils.java

@@ -0,0 +1,46 @@
+package cn.iocoder.yudao.framework.common.util.spring;
+
+import cn.hutool.core.bean.BeanUtil;
+import org.springframework.aop.framework.AdvisedSupport;
+import org.springframework.aop.framework.AopProxy;
+import org.springframework.aop.support.AopUtils;
+
+/**
+ * Spring AOP 工具类
+ *
+ * 参考波克尔 http://www.bubuko.com/infodetail-3471885.html 实现
+ */
+public class SpringAopUtils {
+
+    /**
+     * 获取代理的目标对象
+     *
+     * @param proxy 代理对象
+     * @return 目标对象
+     */
+    public static Object getTarget(Object proxy) throws Exception {
+        // 不是代理对象
+        if (!AopUtils.isAopProxy(proxy)) {
+            return proxy;
+        }
+        // Jdk 代理
+        if (AopUtils.isJdkDynamicProxy(proxy)) {
+            return getJdkDynamicProxyTargetObject(proxy);
+        }
+        // Cglib 代理
+        return getCglibProxyTargetObject(proxy);
+    }
+
+    private static Object getCglibProxyTargetObject(Object proxy) throws Exception {
+        Object dynamicAdvisedInterceptor = BeanUtil.getFieldValue(proxy, "CGLIB$CALLBACK_0");
+        AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(dynamicAdvisedInterceptor, "advised");
+        return advisedSupport.getTargetSource().getTarget();
+    }
+
+    private static Object getJdkDynamicProxyTargetObject(Object proxy) throws Exception {
+        AopProxy aopProxy = (AopProxy) BeanUtil.getFieldValue(proxy, "h");
+        AdvisedSupport advisedSupport = (AdvisedSupport) BeanUtil.getFieldValue(aopProxy, "advised");
+        return advisedSupport.getTargetSource().getTarget();
+    }
+
+}

+ 58 - 0
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerDecorator.java

@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.framework.tenant.core.job;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.quartz.core.handler.JobHandler;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
+import cn.iocoder.yudao.framework.tenant.core.service.TenantFrameworkService;
+import lombok.AllArgsConstructor;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 多租户 JobHandler 装饰器
+ * 任务执行时,会按照租户逐个执行 Job 的逻辑
+ *
+ * 注意,需要保证 JobHandler 的幂等性。因为 Job 因为某个租户执行失败重试时,之前执行成功的租户也会再次执行。
+ *
+ * @author 芋道源码
+ */
+@AllArgsConstructor
+public class TenantJobHandlerDecorator implements JobHandler {
+
+    private final TenantFrameworkService tenantFrameworkService;
+    /**
+     * 被装饰的 Job
+     */
+    private final JobHandler jobHandler;
+
+    @Override
+    public final String execute(String param) throws Exception {
+        // 获得租户列表
+        List<Long> tenantIds = tenantFrameworkService.getTenantIds();
+        if (CollUtil.isEmpty(tenantIds)) {
+            return null;
+        }
+
+        // 逐个租户,执行 Job
+        Map<Long, String> results = new ConcurrentHashMap<>();
+        tenantIds.parallelStream().forEach(tenantId -> { // TODO 芋艿:先通过 parallel 实现并行;1)多个租户,是一条执行日志;2)异常的情况
+            try {
+                // 设置租户
+                TenantContextHolder.setTenantId(tenantId);
+                // 执行 Job
+                String result = jobHandler.execute(param);
+                // 添加结果
+                results.put(tenantId, result);
+            } catch (Exception e) {
+                throw new RuntimeException(e);
+            } finally {
+                TenantContextHolder.clear();
+            }
+        });
+        return JsonUtils.toJsonString(results);
+    }
+
+}

+ 42 - 0
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/TenantRedisMessageInterceptor.java

@@ -0,0 +1,42 @@
+package cn.iocoder.yudao.framework.tenant.core.mq;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
+import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
+
+import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
+
+/**
+ * 多租户 {@link AbstractRedisMessage} 拦截器
+ *
+ * 1. Producer 发送消息时,将 {@link TenantContextHolder} 租户编号,添加到消息的 Header 中
+ * 2. Consumer 消费消息时,将消息的 Header 的租户编号,添加到 {@link TenantContextHolder} 中
+ *
+ * @author 芋道源码
+ */
+public class TenantRedisMessageInterceptor implements RedisMessageInterceptor {
+
+    @Override
+    public void sendMessageBefore(AbstractRedisMessage message) {
+        Long tenantId = TenantContextHolder.getTenantId();
+        if (tenantId != null) {
+            message.addHeader(HEADER_TENANT_ID, tenantId.toString());
+        }
+    }
+
+    @Override
+    public void consumeMessageBefore(AbstractRedisMessage message) {
+        String tenantIdStr = message.getHeader(HEADER_TENANT_ID);
+        if (StrUtil.isNotEmpty(tenantIdStr)) {
+            TenantContextHolder.setTenantId(Long.valueOf(tenantIdStr));
+        }
+    }
+
+    @Override
+    public void consumeMessageAfter(AbstractRedisMessage message) {
+        // 注意,Consumer 是一个逻辑的入口,所以不考虑原本上下文就存在租户编号的情况
+        TenantContextHolder.clear();
+    }
+
+}

+ 47 - 0
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java

@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.framework.tenant.core.redis;
+
+import cn.hutool.core.util.ArrayUtil;
+import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
+
+import java.time.Duration;
+
+/**
+ * 多租户拓展的 RedisKeyDefine 实现类
+ *
+ * 由于 Redis 不同于 MySQL 有 column 字段,无法通过类似 WHERE tenant_id = ? 的方式过滤
+ * 所以需要通过在 Redis Key 上增加后缀的方式,进行租户之间的隔离。具体的步骤是:
+ * 1. 假设 Redis Key 是 user:%d,示例是 user:1;对应到多租户的 Redis Key 是 user:%d:%d,
+ * 2. 在 Redis DAO 中,需要使用 {@link #formatKey(Object...)} 方法,进行 Redis Key 的格式化
+ *
+ * 注意,大多数情况下,并不用使用 TenantRedisKeyDefine 实现。主要的使用场景,还是 Redis Key 可能存在冲突的情况。
+ * 例如说,租户 1 和 2 都有一个手机号作为 Key,则他们会存在冲突的问题
+ *
+ * @author 芋道源码
+ */
+public class TenantRedisKeyDefine extends RedisKeyDefine {
+
+    /**
+     * 多租户的 KEY 模板
+     */
+    private static final String KEY_TEMPLATE_SUFFIX = ":%d";
+
+    public TenantRedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, Duration timeout) {
+        super(memo, buildKeyTemplate(keyTemplate), keyType, valueType, timeout);
+    }
+
+    public TenantRedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, TimeoutTypeEnum timeoutType) {
+        super(memo, buildKeyTemplate(keyTemplate), keyType, valueType, timeoutType);
+    }
+
+    private static String buildKeyTemplate(String keyTemplate) {
+        return keyTemplate + KEY_TEMPLATE_SUFFIX;
+    }
+
+    @Override
+    public String formatKey(Object... args) {
+        args = ArrayUtil.append(args, TenantContextHolder.getRequiredTenantId());
+        return super.formatKey(args);
+    }
+
+}

+ 27 - 0
yudao-framework/yudao-spring-boot-starter-biz-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.framework.tenant.core.redis;
+
+import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
+import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+class TenantRedisKeyDefineTest {
+
+    @Test
+    public void testFormatKey() {
+        Long tenantId = 30L;
+        TenantContextHolder.setTenantId(tenantId);
+        // 准备参数
+        TenantRedisKeyDefine define = new TenantRedisKeyDefine("", "user:%d:%d", RedisKeyDefine.KeyTypeEnum.HASH,
+                Object.class, RedisKeyDefine.TimeoutTypeEnum.FIXED);
+        Long userId = 10L;
+        Integer userType = 1;
+
+        // 调用
+        String key = define.formatKey(userId, userType);
+        // 断言
+        assertEquals("user:10:1:30", key);
+    }
+
+}

+ 37 - 0
yudao-framework/yudao-spring-boot-starter-flowable/pom.xml

@@ -0,0 +1,37 @@
+<?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>yudao-framework</artifactId>
+        <groupId>cn.iocoder.boot</groupId>
+        <version>${revision}</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+
+    <artifactId>yudao-spring-boot-starter-flowable</artifactId>
+
+    <dependencies>
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-common</artifactId>
+        </dependency>
+
+        <!-- Web 相关 -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-security</artifactId>
+        </dependency>
+
+        <!-- flowable 工作流相关 -->
+        <dependency>
+            <groupId>org.flowable</groupId>
+            <artifactId>flowable-spring-boot-starter-process</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.flowable</groupId>
+            <artifactId>flowable-spring-boot-starter-actuator</artifactId>
+        </dependency>
+    </dependencies>
+
+</project>

+ 43 - 0
yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/config/YudaoFlowableConfiguration.java

@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.framework.flowable.config;
+
+import cn.iocoder.yudao.framework.common.enums.WebFilterOrderEnum;
+import cn.iocoder.yudao.framework.flowable.core.web.FlowableWebFilter;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.core.task.AsyncListenableTaskExecutor;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+@AutoConfiguration
+public class YudaoFlowableConfiguration {
+
+    /**
+     * 参考 {@link org.flowable.spring.boot.FlowableJobConfiguration} 类,创建对应的 AsyncListenableTaskExecutor Bean
+     *
+     * 如果不创建,会导致项目启动时,Flowable 报错的问题
+     */
+    @Bean
+    public AsyncListenableTaskExecutor taskExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(8);
+        executor.setMaxPoolSize(8);
+        executor.setQueueCapacity(100);
+        executor.setThreadNamePrefix("flowable-task-Executor-");
+        executor.setAwaitTerminationSeconds(30);
+        executor.setWaitForTasksToCompleteOnShutdown(true);
+        executor.setAllowCoreThreadTimeOut(true);
+        executor.initialize();
+        return executor;
+    }
+
+    /**
+     * 配置 flowable Web 过滤器
+     */
+    @Bean
+    public FilterRegistrationBean<FlowableWebFilter> flowableWebFilter() {
+        FilterRegistrationBean<FlowableWebFilter> registrationBean = new FilterRegistrationBean<>();
+        registrationBean.setFilter(new FlowableWebFilter());
+        registrationBean.setOrder(WebFilterOrderEnum.FLOWABLE_FILTER);
+        return registrationBean;
+    }
+}

+ 1 - 0
yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/package-info.java

@@ -0,0 +1 @@
+package cn.iocoder.yudao.framework.flowable.core;

+ 82 - 0
yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/FlowableUtils.java

@@ -0,0 +1,82 @@
+package cn.iocoder.yudao.framework.flowable.core.util;
+
+import org.flowable.bpmn.converter.BpmnXMLConverter;
+import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.bpmn.model.FlowElement;
+import org.flowable.common.engine.impl.identity.Authentication;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Flowable 相关的工具方法
+ *
+ * @author 芋道源码
+ */
+public class FlowableUtils {
+
+    // ========== User 相关的工具方法 ==========
+
+    public static void setAuthenticatedUserId(Long userId) {
+        Authentication.setAuthenticatedUserId(String.valueOf(userId));
+    }
+
+    public static void clearAuthenticatedUserId() {
+        Authentication.setAuthenticatedUserId(null);
+    }
+
+    // ========== BPMN 相关的工具方法 ==========
+
+    /**
+     * 获得 BPMN 流程中,指定的元素们
+     *
+     * @param model
+     * @param clazz 指定元素。例如说,{@link org.flowable.bpmn.model.UserTask}、{@link org.flowable.bpmn.model.Gateway} 等等
+     * @return 元素们
+     */
+    public static <T extends FlowElement> List<T> getBpmnModelElements(BpmnModel model, Class<T> clazz) {
+        List<T> result = new ArrayList<>();
+        model.getProcesses().forEach(process -> {
+            process.getFlowElements().forEach(flowElement -> {
+                if (flowElement.getClass().isAssignableFrom(clazz)) {
+                    result.add((T) flowElement);
+                }
+            });
+        });
+        return result;
+    }
+
+    /**
+     * 比较 两个bpmnModel 是否相同
+     * @param oldModel  老的bpmn model
+     * @param newModel 新的bpmn model
+     */
+    public static boolean equals(BpmnModel oldModel, BpmnModel newModel) {
+        // 由于 BpmnModel 未提供 equals 方法,所以只能转成字节数组,进行比较
+        return Arrays.equals(getBpmnBytes(oldModel), getBpmnBytes(newModel));
+    }
+
+    /**
+     * 把 bpmnModel 转换成 byte[]
+     * @param model  bpmnModel
+     */
+    public  static byte[] getBpmnBytes(BpmnModel model) {
+        if (model == null) {
+            return new byte[0];
+        }
+        BpmnXMLConverter converter = new BpmnXMLConverter();
+        return converter.convertToXML(model);
+    }
+
+    // ========== Execution 相关的工具方法 ==========
+
+    public static String formatCollectionVariable(String activityId) {
+        return activityId + "_assignees";
+    }
+
+    public static String formatCollectionElementVariable(String activityId) {
+        return activityId + "_assignee";
+    }
+
+}

+ 35 - 0
yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/web/FlowableWebFilter.java

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.framework.flowable.core.web;
+
+import cn.iocoder.yudao.framework.flowable.core.util.FlowableUtils;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import org.springframework.web.filter.OncePerRequestFilter;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+/**
+ * flowable Web 过滤器,将 userId 设置到 {@link org.flowable.common.engine.impl.identity.Authentication} 中
+ *
+ * @author jason
+ */
+public class FlowableWebFilter extends OncePerRequestFilter {
+
+    @Override
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException {
+        try {
+            // 设置工作流的用户
+            Long userId = SecurityFrameworkUtils.getLoginUserId();
+            if (userId != null) {
+                FlowableUtils.setAuthenticatedUserId(userId);
+            }
+            // 过滤
+            chain.doFilter(request, response);
+        } finally {
+            // 清理
+            FlowableUtils.clearAuthenticatedUserId();
+        }
+    }
+}

+ 1 - 0
yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/package-info.java

@@ -0,0 +1 @@
+package cn.iocoder.yudao.framework.flowable;

+ 1 - 0
yudao-framework/yudao-spring-boot-starter-flowable/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@@ -0,0 +1 @@
+cn.iocoder.yudao.framework.flowable.config.YudaoFlowableConfiguration

+ 162 - 0
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/config/YudaoMQAutoConfiguration.java

@@ -0,0 +1,162 @@
+package cn.iocoder.yudao.framework.mq.config;
+
+import cn.hutool.core.map.MapUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.system.SystemUtil;
+import cn.iocoder.yudao.framework.common.enums.DocumentEnum;
+import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
+import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
+import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessageListener;
+import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessageListener;
+import cn.iocoder.yudao.framework.mq.job.RedisPendingMessageResendJob;
+import cn.iocoder.yudao.framework.redis.config.YudaoRedisAutoConfiguration;
+import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RedissonClient;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.AutoConfiguration;
+import org.springframework.context.annotation.Bean;
+import org.springframework.data.redis.connection.RedisServerCommands;
+import org.springframework.data.redis.connection.stream.Consumer;
+import org.springframework.data.redis.connection.stream.ObjectRecord;
+import org.springframework.data.redis.connection.stream.ReadOffset;
+import org.springframework.data.redis.connection.stream.StreamOffset;
+import org.springframework.data.redis.core.RedisCallback;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.data.redis.listener.ChannelTopic;
+import org.springframework.data.redis.listener.RedisMessageListenerContainer;
+import org.springframework.data.redis.stream.DefaultStreamMessageListenerContainerX;
+import org.springframework.data.redis.stream.StreamMessageListenerContainer;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+import java.util.List;
+import java.util.Properties;
+
+/**
+ * 消息队列配置类
+ *
+ * @author 芋道源码
+ */
+@Slf4j
+@EnableScheduling // 启用定时任务,用于 RedisPendingMessageResendJob 重发消息
+@AutoConfiguration(after = YudaoRedisAutoConfiguration.class)
+public class YudaoMQAutoConfiguration {
+
+    @Bean
+    public RedisMQTemplate redisMQTemplate(StringRedisTemplate redisTemplate,
+                                           List<RedisMessageInterceptor> interceptors) {
+        RedisMQTemplate redisMQTemplate = new RedisMQTemplate(redisTemplate);
+        // 添加拦截器
+        interceptors.forEach(redisMQTemplate::addInterceptor);
+        return redisMQTemplate;
+    }
+
+    // ========== 消费者相关 ==========
+
+    /**
+     * 创建 Redis Pub/Sub 广播消费的容器
+     */
+    @Bean(initMethod = "start", destroyMethod = "stop")
+    public RedisMessageListenerContainer redisMessageListenerContainer(
+            RedisMQTemplate redisMQTemplate, List<AbstractChannelMessageListener<?>> listeners) {
+        // 创建 RedisMessageListenerContainer 对象
+        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
+        // 设置 RedisConnection 工厂。
+        container.setConnectionFactory(redisMQTemplate.getRedisTemplate().getRequiredConnectionFactory());
+        // 添加监听器
+        listeners.forEach(listener -> {
+            listener.setRedisMQTemplate(redisMQTemplate);
+            container.addMessageListener(listener, new ChannelTopic(listener.getChannel()));
+            log.info("[redisMessageListenerContainer][注册 Channel({}) 对应的监听器({})]",
+                    listener.getChannel(), listener.getClass().getName());
+        });
+        return container;
+    }
+
+    /**
+     * 创建 Redis Stream 重新消费的任务
+     */
+    @Bean
+    public RedisPendingMessageResendJob redisPendingMessageResendJob(List<AbstractStreamMessageListener<?>> listeners,
+                                                                     RedisMQTemplate redisTemplate,
+                                                                     @Value("${spring.application.name}") String groupName,
+                                                                     RedissonClient redissonClient) {
+        return new RedisPendingMessageResendJob(listeners, redisTemplate, groupName, redissonClient);
+    }
+
+    /**
+     * 创建 Redis Stream 集群消费的容器
+     * <p>
+     * Redis Stream 的 xreadgroup 命令:https://www.geek-book.com/src/docs/redis/redis/redis.io/commands/xreadgroup.html
+     */
+    @Bean(initMethod = "start", destroyMethod = "stop")
+    public StreamMessageListenerContainer<String, ObjectRecord<String, String>> redisStreamMessageListenerContainer(
+            RedisMQTemplate redisMQTemplate, List<AbstractStreamMessageListener<?>> listeners) {
+        RedisTemplate<String, ?> redisTemplate = redisMQTemplate.getRedisTemplate();
+        checkRedisVersion(redisTemplate);
+        // 第一步,创建 StreamMessageListenerContainer 容器
+        // 创建 options 配置
+        StreamMessageListenerContainer.StreamMessageListenerContainerOptions<String, ObjectRecord<String, String>> containerOptions =
+                StreamMessageListenerContainer.StreamMessageListenerContainerOptions.builder()
+                        .batchSize(10) // 一次性最多拉取多少条消息
+                        .targetType(String.class) // 目标类型。统一使用 String,通过自己封装的 AbstractStreamMessageListener 去反序列化
+                        .build();
+        // 创建 container 对象
+        StreamMessageListenerContainer<String, ObjectRecord<String, String>> container =
+//                StreamMessageListenerContainer.create(redisTemplate.getRequiredConnectionFactory(), containerOptions);
+                DefaultStreamMessageListenerContainerX.create(redisMQTemplate.getRedisTemplate().getRequiredConnectionFactory(), containerOptions);
+
+        // 第二步,注册监听器,消费对应的 Stream 主题
+        String consumerName = buildConsumerName();
+        listeners.parallelStream().forEach(listener -> {
+            log.info("[redisStreamMessageListenerContainer][开始注册 StreamKey({}) 对应的监听器({})]",
+                    listener.getStreamKey(), listener.getClass().getName());
+            // 创建 listener 对应的消费者分组
+            try {
+                redisTemplate.opsForStream().createGroup(listener.getStreamKey(), listener.getGroup());
+            } catch (Exception ignore) {
+            }
+            // 设置 listener 对应的 redisTemplate
+            listener.setRedisMQTemplate(redisMQTemplate);
+            // 创建 Consumer 对象
+            Consumer consumer = Consumer.from(listener.getGroup(), consumerName);
+            // 设置 Consumer 消费进度,以最小消费进度为准
+            StreamOffset<String> streamOffset = StreamOffset.create(listener.getStreamKey(), ReadOffset.lastConsumed());
+            // 设置 Consumer 监听
+            StreamMessageListenerContainer.StreamReadRequestBuilder<String> builder = StreamMessageListenerContainer.StreamReadRequest
+                    .builder(streamOffset).consumer(consumer)
+                    .autoAcknowledge(false) // 不自动 ack
+                    .cancelOnError(throwable -> false); // 默认配置,发生异常就取消消费,显然不符合预期;因此,我们设置为 false
+            container.register(builder.build(), listener);
+            log.info("[redisStreamMessageListenerContainer][完成注册 StreamKey({}) 对应的监听器({})]",
+                    listener.getStreamKey(), listener.getClass().getName());
+        });
+        return container;
+    }
+
+    /**
+     * 构建消费者名字,使用本地 IP + 进程编号的方式。
+     * 参考自 RocketMQ clientId 的实现
+     *
+     * @return 消费者名字
+     */
+    private static String buildConsumerName() {
+        return String.format("%s@%d", SystemUtil.getHostInfo().getAddress(), SystemUtil.getCurrentPID());
+    }
+
+    /**
+     * 校验 Redis 版本号,是否满足最低的版本号要求!
+     */
+    private static void checkRedisVersion(RedisTemplate<String, ?> redisTemplate) {
+        // 获得 Redis 版本
+        Properties info = redisTemplate.execute((RedisCallback<Properties>) RedisServerCommands::info);
+        String version = MapUtil.getStr(info, "redis_version");
+        // 校验最低版本必须大于等于 5.0.0
+        int majorVersion = Integer.parseInt(StrUtil.subBefore(version, '.', false));
+        if (majorVersion < 5) {
+            throw new IllegalStateException(StrUtil.format("您当前的 Redis 版本为 {},小于最低要求的 5.0.0 版本!" +
+                    "请参考 {} 文档进行安装。", version, DocumentEnum.REDIS_INSTALL.getUrl()));
+        }
+    }
+
+}

+ 87 - 0
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/RedisMQTemplate.java

@@ -0,0 +1,87 @@
+package cn.iocoder.yudao.framework.mq.core;
+
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
+import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
+import cn.iocoder.yudao.framework.mq.core.pubsub.AbstractChannelMessage;
+import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessage;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.springframework.data.redis.connection.stream.RecordId;
+import org.springframework.data.redis.connection.stream.StreamRecords;
+import org.springframework.data.redis.core.RedisTemplate;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Redis MQ 操作模板类
+ *
+ * @author 芋道源码
+ */
+@AllArgsConstructor
+public class RedisMQTemplate {
+
+    @Getter
+    private final RedisTemplate<String, ?> redisTemplate;
+    /**
+     * 拦截器数组
+     */
+    @Getter
+    private final List<RedisMessageInterceptor> interceptors = new ArrayList<>();
+
+    /**
+     * 发送 Redis 消息,基于 Redis pub/sub 实现
+     *
+     * @param message 消息
+     */
+    public <T extends AbstractChannelMessage> void send(T message) {
+        try {
+            sendMessageBefore(message);
+            // 发送消息
+            redisTemplate.convertAndSend(message.getChannel(), JsonUtils.toJsonString(message));
+        } finally {
+            sendMessageAfter(message);
+        }
+    }
+
+    /**
+     * 发送 Redis 消息,基于 Redis Stream 实现
+     *
+     * @param message 消息
+     * @return 消息记录的编号对象
+     */
+    public <T extends AbstractStreamMessage> RecordId send(T message) {
+        try {
+            sendMessageBefore(message);
+            // 发送消息
+            return redisTemplate.opsForStream().add(StreamRecords.newRecord()
+                    .ofObject(JsonUtils.toJsonString(message)) // 设置内容
+                    .withStreamKey(message.getStreamKey())); // 设置 stream key
+        } finally {
+            sendMessageAfter(message);
+        }
+    }
+
+    /**
+     * 添加拦截器
+     *
+     * @param interceptor 拦截器
+     */
+    public void addInterceptor(RedisMessageInterceptor interceptor) {
+        interceptors.add(interceptor);
+    }
+
+    private void sendMessageBefore(AbstractRedisMessage message) {
+        // 正序
+        interceptors.forEach(interceptor -> interceptor.sendMessageBefore(message));
+    }
+
+    private void sendMessageAfter(AbstractRedisMessage message) {
+        // 倒序
+        for (int i = interceptors.size() - 1; i >= 0; i--) {
+            interceptors.get(i).sendMessageAfter(message);
+        }
+    }
+
+}

+ 26 - 0
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/interceptor/RedisMessageInterceptor.java

@@ -0,0 +1,26 @@
+package cn.iocoder.yudao.framework.mq.core.interceptor;
+
+import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
+
+/**
+ * {@link AbstractRedisMessage} 消息拦截器
+ * 通过拦截器,作为插件机制,实现拓展。
+ * 例如说,多租户场景下的 MQ 消息处理
+ *
+ * @author 芋道源码
+ */
+public interface RedisMessageInterceptor {
+
+    default void sendMessageBefore(AbstractRedisMessage message) {
+    }
+
+    default void sendMessageAfter(AbstractRedisMessage message) {
+    }
+
+    default void consumeMessageBefore(AbstractRedisMessage message) {
+    }
+
+    default void consumeMessageAfter(AbstractRedisMessage message) {
+    }
+
+}

+ 29 - 0
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/message/AbstractRedisMessage.java

@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.framework.mq.core.message;
+
+import lombok.Data;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Redis 消息抽象基类
+ *
+ * @author 芋道源码
+ */
+@Data
+public abstract class AbstractRedisMessage {
+
+    /**
+     * 头
+     */
+    private Map<String, String> headers = new HashMap<>();
+
+    public String getHeader(String key) {
+        return headers.get(key);
+    }
+
+    public void addHeader(String key, String value) {
+        headers.put(key, value);
+    }
+
+}

+ 21 - 0
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/pubsub/AbstractChannelMessage.java

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.framework.mq.core.pubsub;
+
+import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+/**
+ * Redis Channel Message 抽象类
+ *
+ * @author 芋道源码
+ */
+public abstract class AbstractChannelMessage extends AbstractRedisMessage {
+
+    /**
+     * 获得 Redis Channel
+     *
+     * @return Channel
+     */
+    @JsonIgnore // 避免序列化。原因是,Redis 发布 Channel 消息的时候,已经会指定。
+    public abstract String getChannel();
+
+}

+ 103 - 0
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/pubsub/AbstractChannelMessageListener.java

@@ -0,0 +1,103 @@
+package cn.iocoder.yudao.framework.mq.core.pubsub;
+
+import cn.hutool.core.util.TypeUtil;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
+import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
+import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
+import lombok.Setter;
+import lombok.SneakyThrows;
+import org.springframework.data.redis.connection.Message;
+import org.springframework.data.redis.connection.MessageListener;
+
+import java.lang.reflect.Type;
+import java.util.List;
+
+/**
+ * Redis Pub/Sub 监听器抽象类,用于实现广播消费
+ *
+ * @param <T> 消息类型。一定要填写噢,不然会报错
+ *
+ * @author 芋道源码
+ */
+public abstract class AbstractChannelMessageListener<T extends AbstractChannelMessage> implements MessageListener {
+
+    /**
+     * 消息类型
+     */
+    private final Class<T> messageType;
+    /**
+     * Redis Channel
+     */
+    private final String channel;
+    /**
+     * RedisMQTemplate
+     */
+    @Setter
+    private RedisMQTemplate redisMQTemplate;
+
+    @SneakyThrows
+    protected AbstractChannelMessageListener() {
+        this.messageType = getMessageClass();
+        this.channel = messageType.getDeclaredConstructor().newInstance().getChannel();
+    }
+
+    /**
+     * 获得 Sub 订阅的 Redis Channel 通道
+     *
+     * @return channel
+     */
+    public final String getChannel() {
+        return channel;
+    }
+
+    @Override
+    public final void onMessage(Message message, byte[] bytes) {
+        T messageObj = JsonUtils.parseObject(message.getBody(), messageType);
+        try {
+            consumeMessageBefore(messageObj);
+            // 消费消息
+            this.onMessage(messageObj);
+        } finally {
+            consumeMessageAfter(messageObj);
+        }
+    }
+
+    /**
+     * 处理消息
+     *
+     * @param message 消息
+     */
+    public abstract void onMessage(T message);
+
+    /**
+     * 通过解析类上的泛型,获得消息类型
+     *
+     * @return 消息类型
+     */
+    @SuppressWarnings("unchecked")
+    private Class<T> getMessageClass() {
+        Type type = TypeUtil.getTypeArgument(getClass(), 0);
+        if (type == null) {
+            throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
+        }
+        return (Class<T>) type;
+    }
+
+    private void consumeMessageBefore(AbstractRedisMessage message) {
+        assert redisMQTemplate != null;
+        List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors();
+        // 正序
+        interceptors.forEach(interceptor -> interceptor.consumeMessageBefore(message));
+    }
+
+    private void consumeMessageAfter(AbstractRedisMessage message) {
+        assert redisMQTemplate != null;
+        List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors();
+        // 倒序
+        for (int i = interceptors.size() - 1; i >= 0; i--) {
+            interceptors.get(i).consumeMessageAfter(message);
+        }
+    }
+
+}

+ 21 - 0
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/stream/AbstractStreamMessage.java

@@ -0,0 +1,21 @@
+package cn.iocoder.yudao.framework.mq.core.stream;
+
+import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
+import com.fasterxml.jackson.annotation.JsonIgnore;
+
+/**
+ * Redis Stream Message 抽象类
+ *
+ * @author 芋道源码
+ */
+public abstract class AbstractStreamMessage extends AbstractRedisMessage {
+
+    /**
+     * 获得 Redis Stream Key
+     *
+     * @return Channel
+     */
+    @JsonIgnore // 避免序列化
+    public abstract String getStreamKey();
+
+}

+ 113 - 0
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/stream/AbstractStreamMessageListener.java

@@ -0,0 +1,113 @@
+package cn.iocoder.yudao.framework.mq.core.stream;
+
+import cn.hutool.core.util.TypeUtil;
+import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
+import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
+import cn.iocoder.yudao.framework.mq.core.interceptor.RedisMessageInterceptor;
+import cn.iocoder.yudao.framework.mq.core.message.AbstractRedisMessage;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.SneakyThrows;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.connection.stream.ObjectRecord;
+import org.springframework.data.redis.stream.StreamListener;
+
+import java.lang.reflect.Type;
+import java.util.List;
+
+/**
+ * Redis Stream 监听器抽象类,用于实现集群消费
+ *
+ * @param <T> 消息类型。一定要填写噢,不然会报错
+ *
+ * @author 芋道源码
+ */
+public abstract class AbstractStreamMessageListener<T extends AbstractStreamMessage>
+        implements StreamListener<String, ObjectRecord<String, String>> {
+
+    /**
+     * 消息类型
+     */
+    private final Class<T> messageType;
+    /**
+     * Redis Channel
+     */
+    @Getter
+    private final String streamKey;
+
+    /**
+     * Redis 消费者分组,默认使用 spring.application.name 名字
+     */
+    @Value("${spring.application.name}")
+    @Getter
+    private String group;
+    /**
+     * RedisMQTemplate
+     */
+    @Setter
+    private RedisMQTemplate redisMQTemplate;
+
+    @SneakyThrows
+    protected AbstractStreamMessageListener() {
+        this.messageType = getMessageClass();
+        this.streamKey = messageType.getDeclaredConstructor().newInstance().getStreamKey();
+    }
+
+    @Override
+    public void onMessage(ObjectRecord<String, String> message) {
+        // 消费消息
+        T messageObj = JsonUtils.parseObject(message.getValue(), messageType);
+        try {
+            consumeMessageBefore(messageObj);
+            // 消费消息
+            this.onMessage(messageObj);
+            // ack 消息消费完成
+            redisMQTemplate.getRedisTemplate().opsForStream().acknowledge(group, message);
+            // TODO 芋艿:需要额外考虑以下几个点:
+            // 1. 处理异常的情况
+            // 2. 发送日志;以及事务的结合
+            // 3. 消费日志;以及通用的幂等性
+            // 4. 消费失败的重试,https://zhuanlan.zhihu.com/p/60501638
+        } finally {
+            consumeMessageAfter(messageObj);
+        }
+    }
+
+    /**
+     * 处理消息
+     *
+     * @param message 消息
+     */
+    public abstract void onMessage(T message);
+
+    /**
+     * 通过解析类上的泛型,获得消息类型
+     *
+     * @return 消息类型
+     */
+    @SuppressWarnings("unchecked")
+    private Class<T> getMessageClass() {
+        Type type = TypeUtil.getTypeArgument(getClass(), 0);
+        if (type == null) {
+            throw new IllegalStateException(String.format("类型(%s) 需要设置消息类型", getClass().getName()));
+        }
+        return (Class<T>) type;
+    }
+
+    private void consumeMessageBefore(AbstractRedisMessage message) {
+        assert redisMQTemplate != null;
+        List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors();
+        // 正序
+        interceptors.forEach(interceptor -> interceptor.consumeMessageBefore(message));
+    }
+
+    private void consumeMessageAfter(AbstractRedisMessage message) {
+        assert redisMQTemplate != null;
+        List<RedisMessageInterceptor> interceptors = redisMQTemplate.getInterceptors();
+        // 倒序
+        for (int i = interceptors.size() - 1; i >= 0; i--) {
+            interceptors.get(i).consumeMessageAfter(message);
+        }
+    }
+
+}

+ 80 - 0
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/job/RedisPendingMessageResendJob.java

@@ -0,0 +1,80 @@
+package cn.iocoder.yudao.framework.mq.job;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.mq.core.RedisMQTemplate;
+import cn.iocoder.yudao.framework.mq.core.stream.AbstractStreamMessageListener;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.springframework.data.redis.connection.stream.Consumer;
+import org.springframework.data.redis.connection.stream.MapRecord;
+import org.springframework.data.redis.connection.stream.PendingMessagesSummary;
+import org.springframework.data.redis.connection.stream.ReadOffset;
+import org.springframework.data.redis.connection.stream.StreamOffset;
+import org.springframework.data.redis.connection.stream.StreamRecords;
+import org.springframework.data.redis.core.StreamOperations;
+import org.springframework.scheduling.annotation.Scheduled;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 这个任务用于处理,crash 之后的消费者未消费完的消息
+ */
+@Slf4j
+@AllArgsConstructor
+public class RedisPendingMessageResendJob {
+
+    private static final String LOCK_KEY = "redis:pending:msg:lock";
+
+    private final List<AbstractStreamMessageListener<?>> listeners;
+    private final RedisMQTemplate redisTemplate;
+    private final String groupName;
+    private final RedissonClient redissonClient;
+
+    /**
+     * 一分钟执行一次,这里选择每分钟的35秒执行,是为了避免整点任务过多的问题
+     */
+    @Scheduled(cron = "35 * * * * ?")
+    public void messageResend() {
+        RLock lock = redissonClient.getLock(LOCK_KEY);
+        // 尝试加锁
+        if (lock.tryLock()) {
+            try {
+                execute();
+            } catch (Exception ex) {
+                log.error("[messageResend][执行异常]", ex);
+            } finally {
+                lock.unlock();
+            }
+        }
+    }
+
+    private void execute() {
+        StreamOperations<String, Object, Object> ops = redisTemplate.getRedisTemplate().opsForStream();
+        listeners.forEach(listener -> {
+            PendingMessagesSummary pendingMessagesSummary = ops.pending(listener.getStreamKey(), groupName);
+            // 每个消费者的 pending 队列消息数量
+            Map<String, Long> pendingMessagesPerConsumer = pendingMessagesSummary.getPendingMessagesPerConsumer();
+            pendingMessagesPerConsumer.forEach((consumerName, pendingMessageCount) -> {
+                log.info("[processPendingMessage][消费者({}) 消息数量({})]", consumerName, pendingMessageCount);
+
+                // 从消费者的 pending 队列中读取消息
+                List<MapRecord<String, Object, Object>> records = ops.read(Consumer.from(groupName, consumerName), StreamOffset.create(listener.getStreamKey(), ReadOffset.from("0")));
+                if (CollUtil.isEmpty(records)) {
+                    return;
+                }
+                for (MapRecord<String, Object, Object> record : records) {
+                    // 重新投递消息
+                    redisTemplate.getRedisTemplate().opsForStream().add(StreamRecords.newRecord()
+                            .ofObject(record.getValue()) // 设置内容
+                            .withStreamKey(listener.getStreamKey()));
+
+                    // ack 消息消费完成
+                    redisTemplate.getRedisTemplate().opsForStream().acknowledge(groupName, record);
+                }
+            });
+        });
+    }
+}

+ 62 - 0
yudao-framework/yudao-spring-boot-starter-mq/src/main/java/org/springframework/data/redis/stream/DefaultStreamMessageListenerContainerX.java

@@ -0,0 +1,62 @@
+package org.springframework.data.redis.stream;
+
+import cn.hutool.core.util.ReflectUtil;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.stream.ByteRecord;
+import org.springframework.data.redis.connection.stream.ReadOffset;
+import org.springframework.data.redis.connection.stream.Record;
+import org.springframework.util.Assert;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * 拓展 DefaultStreamMessageListenerContainer 实现,解决 Spring Data Redis + Redisson 结合使用时,Redisson 在 Stream 获得不到数据时,返回 null 而不是空 List,导致 NPE 异常。
+ * 对应 issue:https://github.com/spring-projects/spring-data-redis/issues/2147 和 https://github.com/redisson/redisson/issues/4006
+ * 目前看下来 Spring Data Redis 不肯加 null 判断,Redisson 暂时也没改返回 null 到空 List 的打算,所以暂时只能自己改,哽咽!
+ *
+ * @author 芋道源码
+ */
+public class DefaultStreamMessageListenerContainerX<K, V extends Record<K, ?>> extends DefaultStreamMessageListenerContainer<K, V> {
+
+    /**
+     * 参考 {@link StreamMessageListenerContainer#create(RedisConnectionFactory, StreamMessageListenerContainerOptions)} 的实现
+     */
+    public static <K, V extends Record<K, ?>> StreamMessageListenerContainer<K, V> create(RedisConnectionFactory connectionFactory, StreamMessageListenerContainer.StreamMessageListenerContainerOptions<K, V> options) {
+        Assert.notNull(connectionFactory, "RedisConnectionFactory must not be null!");
+        Assert.notNull(options, "StreamMessageListenerContainerOptions must not be null!");
+        return new DefaultStreamMessageListenerContainerX<>(connectionFactory, options);
+    }
+
+    public DefaultStreamMessageListenerContainerX(RedisConnectionFactory connectionFactory, StreamMessageListenerContainerOptions<K, V> containerOptions) {
+        super(connectionFactory, containerOptions);
+    }
+
+    /**
+     * 参考 {@link DefaultStreamMessageListenerContainer#register(StreamReadRequest, StreamListener)} 的实现
+     */
+    @Override
+    public Subscription register(StreamReadRequest<K> streamRequest, StreamListener<K, V> listener) {
+        return this.doRegisterX(getReadTaskX(streamRequest, listener));
+    }
+
+    @SuppressWarnings("unchecked")
+    private StreamPollTask<K, V> getReadTaskX(StreamReadRequest<K> streamRequest, StreamListener<K, V> listener) {
+        StreamPollTask<K, V> task = ReflectUtil.invoke(this, "getReadTask", streamRequest, listener);
+        // 修改 readFunction 方法
+        Function<ReadOffset, List<ByteRecord>> readFunction = (Function<ReadOffset, List<ByteRecord>>) ReflectUtil.getFieldValue(task, "readFunction");
+        ReflectUtil.setFieldValue(task, "readFunction", (Function<ReadOffset, List<ByteRecord>>) readOffset -> {
+            List<ByteRecord> records = readFunction.apply(readOffset);
+            //【重点】保证 records 不是空,避免 NPE 的问题!!!
+            return records != null ? records : Collections.emptyList();
+        });
+        return task;
+    }
+
+    private Subscription doRegisterX(Task task) {
+        return ReflectUtil.invoke(this, "doRegister", task);
+    }
+
+}
+

+ 24 - 0
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/expression/AndExpressionX.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.framework.expression;
+
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.operators.conditional.AndExpression;
+
+/**
+ * AndExpression 的扩展类(会在原有表达式两端加上括号)
+ */
+public class AndExpressionX extends AndExpression {
+
+    public AndExpressionX() {
+    }
+
+    public AndExpressionX(Expression leftExpression, Expression rightExpression) {
+        this.setLeftExpression(leftExpression);
+        this.setRightExpression(rightExpression);
+    }
+
+    @Override
+    public String toString() {
+        return "(" + super.toString() + ")";
+    }
+
+}

+ 24 - 0
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/expression/OrExpressionX.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.framework.expression;
+
+import net.sf.jsqlparser.expression.Expression;
+import net.sf.jsqlparser.expression.operators.conditional.OrExpression;
+
+/**
+ * OrExpression 的扩展类(会在原有表达式两端加上括号)
+ */
+public class OrExpressionX extends OrExpression {
+
+    public OrExpressionX() {
+    }
+
+    public OrExpressionX(Expression leftExpression, Expression rightExpression) {
+        this.setLeftExpression(leftExpression);
+        this.setRightExpression(rightExpression);
+    }
+
+    @Override
+    public String toString() {
+        return "(" + super.toString() + ")";
+    }
+
+}

+ 1 - 0
yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/package-info.java

@@ -0,0 +1 @@
+package cn.iocoder.yudao.framework;

+ 9 - 0
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/resilience4j/package-info.java

@@ -0,0 +1,9 @@
+/**
+ * 使用 Resilience4j 组件,实现服务保障,包括:
+ * 1. 熔断器
+ * 2. 限流器
+ * 3. 舱壁隔离
+ * 4. 重试
+ * 5. 限时器
+ */
+package cn.iocoder.yudao.framework.resilience4j;

+ 1 - 0
yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md

@@ -0,0 +1 @@
+<https://www.iocoder.cn/Spring-Boot/Resilience4j/?yudao>

+ 113 - 0
yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/RedisKeyDefine.java

@@ -0,0 +1,113 @@
+package cn.iocoder.yudao.framework.redis.core;
+
+import com.fasterxml.jackson.annotation.JsonValue;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.Getter;
+
+import java.time.Duration;
+
+/**
+ * Redis Key 定义类
+ *
+ * @author 芋道源码
+ */
+@Data
+public class RedisKeyDefine {
+
+    @Getter
+    @AllArgsConstructor
+    public enum KeyTypeEnum {
+
+        STRING("String"),
+        LIST("List"),
+        HASH("Hash"),
+        SET("Set"),
+        ZSET("Sorted Set"),
+        STREAM("Stream"),
+        PUBSUB("Pub/Sub");
+
+        /**
+         * 类型
+         */
+        @JsonValue
+        private final String type;
+
+    }
+
+    @Getter
+    @AllArgsConstructor
+    public enum TimeoutTypeEnum {
+
+        FOREVER(1), // 永不超时
+        DYNAMIC(2), // 动态超时
+        FIXED(3); // 固定超时
+
+        /**
+         * 类型
+         */
+        @JsonValue
+        private final Integer type;
+
+    }
+
+    /**
+     * Key 模板
+     */
+    private final String keyTemplate;
+    /**
+     * Key 类型的枚举
+     */
+    private final KeyTypeEnum keyType;
+    /**
+     * Value 类型
+     *
+     * 如果是使用分布式锁,设置为 {@link java.util.concurrent.locks.Lock} 类型
+     */
+    private final Class<?> valueType;
+    /**
+     * 超时类型
+     */
+    private final TimeoutTypeEnum timeoutType;
+    /**
+     * 过期时间
+     */
+    private final Duration timeout;
+    /**
+     * 备注
+     */
+    private final String memo;
+
+    private RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType,
+                           TimeoutTypeEnum timeoutType, Duration timeout) {
+        this.memo = memo;
+        this.keyTemplate = keyTemplate;
+        this.keyType = keyType;
+        this.valueType = valueType;
+        this.timeout = timeout;
+        this.timeoutType = timeoutType;
+        // 添加注册表
+        RedisKeyRegistry.add(this);
+    }
+
+    public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, Duration timeout) {
+        this(memo, keyTemplate, keyType, valueType, TimeoutTypeEnum.FIXED, timeout);
+    }
+
+    public RedisKeyDefine(String memo, String keyTemplate, KeyTypeEnum keyType, Class<?> valueType, TimeoutTypeEnum timeoutType) {
+        this(memo, keyTemplate, keyType, valueType, timeoutType, Duration.ZERO);
+    }
+
+    /**
+     * 格式化 Key
+     *
+     * 注意,内部采用 {@link String#format(String, Object...)} 实现
+     *
+     * @param args 格式化的参数
+     * @return Key
+     */
+    public String formatKey(Object... args) {
+        return String.format(keyTemplate, args);
+    }
+
+}

+ 28 - 0
yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/RedisKeyRegistry.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.framework.redis.core;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * {@link RedisKeyDefine} 注册表
+ */
+public class RedisKeyRegistry {
+
+    /**
+     * Redis RedisKeyDefine 数组
+     */
+    private static final List<RedisKeyDefine> DEFINES = new ArrayList<>();
+
+    public static void add(RedisKeyDefine define) {
+        DEFINES.add(define);
+    }
+
+    public static List<RedisKeyDefine> list() {
+        return DEFINES;
+    }
+
+    public static int size() {
+        return DEFINES.size();
+    }
+
+}

+ 85 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLog.java

@@ -0,0 +1,85 @@
+package cn.iocoder.yudao.framework.apilog.core.service;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+/**
+ * API 访问日志
+ *
+ * @author 芋道源码
+ */
+@Data
+public class ApiAccessLog {
+
+    /**
+     * 链路追踪编号
+     */
+    private String traceId;
+    /**
+     * 用户编号
+     */
+    private Long userId;
+    /**
+     * 用户类型
+     */
+    private Integer userType;
+    /**
+     * 应用名
+     */
+    @NotNull(message = "应用名不能为空")
+    private String applicationName;
+
+    /**
+     * 请求方法名
+     */
+    @NotNull(message = "http 请求方法不能为空")
+    private String requestMethod;
+    /**
+     * 访问地址
+     */
+    @NotNull(message = "访问地址不能为空")
+    private String requestUrl;
+    /**
+     * 请求参数
+     */
+    @NotNull(message = "请求参数不能为空")
+    private String requestParams;
+    /**
+     * 用户 IP
+     */
+    @NotNull(message = "ip 不能为空")
+    private String userIp;
+    /**
+     * 浏览器 UA
+     */
+    @NotNull(message = "User-Agent 不能为空")
+    private String userAgent;
+
+    /**
+     * 开始请求时间
+     */
+    @NotNull(message = "开始请求时间不能为空")
+    private LocalDateTime beginTime;
+    /**
+     * 结束请求时间
+     */
+    @NotNull(message = "结束请求时间不能为空")
+    private LocalDateTime endTime;
+    /**
+     * 执行时长,单位:毫秒
+     */
+    @NotNull(message = "执行时长不能为空")
+    private Integer duration;
+    /**
+     * 结果码
+     */
+    @NotNull(message = "错误码不能为空")
+    private Integer resultCode;
+    /**
+     * 结果提示
+     */
+    private String resultMsg;
+
+}

+ 107 - 0
yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLog.java

@@ -0,0 +1,107 @@
+package cn.iocoder.yudao.framework.apilog.core.service;
+
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.time.LocalDateTime;
+
+/**
+ * API 错误日志
+ *
+ * @author 芋道源码
+ */
+@Data
+public class ApiErrorLog {
+
+    /**
+     * 链路编号
+     */
+    private String traceId;
+    /**
+     * 账号编号
+     */
+    private Long userId;
+    /**
+     * 用户类型
+     */
+    private Integer userType;
+    /**
+     * 应用名
+     */
+    @NotNull(message = "应用名不能为空")
+    private String applicationName;
+
+    /**
+     * 请求方法名
+     */
+    @NotNull(message = "http 请求方法不能为空")
+    private String requestMethod;
+    /**
+     * 访问地址
+     */
+    @NotNull(message = "访问地址不能为空")
+    private String requestUrl;
+    /**
+     * 请求参数
+     */
+    @NotNull(message = "请求参数不能为空")
+    private String requestParams;
+    /**
+     * 用户 IP
+     */
+    @NotNull(message = "ip 不能为空")
+    private String userIp;
+    /**
+     * 浏览器 UA
+     */
+    @NotNull(message = "User-Agent 不能为空")
+    private String userAgent;
+
+    /**
+     * 异常时间
+     */
+    @NotNull(message = "异常时间不能为空")
+    private LocalDateTime exceptionTime;
+    /**
+     * 异常名
+     */
+    @NotNull(message = "异常名不能为空")
+    private String exceptionName;
+    /**
+     * 异常发生的类全名
+     */
+    @NotNull(message = "异常发生的类全名不能为空")
+    private String exceptionClassName;
+    /**
+     * 异常发生的类文件
+     */
+    @NotNull(message = "异常发生的类文件不能为空")
+    private String exceptionFileName;
+    /**
+     * 异常发生的方法名
+     */
+    @NotNull(message = "异常发生的方法名不能为空")
+    private String exceptionMethodName;
+    /**
+     * 异常发生的方法所在行
+     */
+    @NotNull(message = "异常发生的方法所在行不能为空")
+    private Integer exceptionLineNumber;
+    /**
+     * 异常的栈轨迹异常的栈轨迹
+     */
+    @NotNull(message = "异常的栈轨迹不能为空")
+    private String exceptionStackTrace;
+    /**
+     * 异常导致的根消息
+     */
+    @NotNull(message = "异常导致的根消息不能为空")
+    private String exceptionRootCauseMessage;
+    /**
+     * 异常导致的消息
+     */
+    @NotNull(message = "异常导致的消息不能为空")
+    private String exceptionMessage;
+
+
+}

+ 14 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketHandlerConfig.java

@@ -0,0 +1,14 @@
+package cn.iocoder.yudao.framework.websocket.config;
+
+import cn.iocoder.yudao.framework.websocket.core.UserHandshakeInterceptor;
+import org.springframework.boot.context.properties.EnableConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+@EnableConfigurationProperties(WebSocketProperties.class)
+public class WebSocketHandlerConfig {
+    @Bean
+    public HandshakeInterceptor handshakeInterceptor() {
+        return new UserHandshakeInterceptor();
+    }
+}

+ 24 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/UserHandshakeInterceptor.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.framework.websocket.core;
+
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
+import org.springframework.http.server.ServerHttpRequest;
+import org.springframework.http.server.ServerHttpResponse;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.server.HandshakeInterceptor;
+
+import java.util.Map;
+
+public class UserHandshakeInterceptor implements HandshakeInterceptor {
+    @Override
+    public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
+        LoginUser loginUser = SecurityFrameworkUtils.getLoginUser();
+        attributes.put(WebSocketKeyDefine.LOGIN_USER, loginUser);
+        return true;
+    }
+
+    @Override
+    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {
+
+    }
+}

+ 9 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketKeyDefine.java

@@ -0,0 +1,9 @@
+package cn.iocoder.yudao.framework.websocket.core;
+
+
+import lombok.Data;
+
+@Data
+public class WebSocketKeyDefine {
+    public static final String LOGIN_USER ="LOGIN_USER";
+}

+ 24 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketMessageDO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.framework.websocket.core;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.List;
+
+@Data
+@Accessors(chain = true)
+public class WebSocketMessageDO {
+    /**
+     * 接收消息的seesion
+     */
+    private List<Object> seesionKeyList;
+    /**
+     * 发送消息
+     */
+    private String msgText;
+
+    public static WebSocketMessageDO build(List<Object> seesionKeyList, String msgText) {
+        return new WebSocketMessageDO().setMsgText(msgText).setSeesionKeyList(seesionKeyList);
+    }
+
+}

+ 36 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketSessionHandler.java

@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.framework.websocket.core;
+
+import org.springframework.web.socket.WebSocketSession;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+
+public final class WebSocketSessionHandler {
+    private WebSocketSessionHandler() {
+    }
+
+    private static final Map<String, WebSocketSession> SESSION_MAP = new ConcurrentHashMap<>();
+
+    public static void addSession(Object sessionKey, WebSocketSession session) {
+        SESSION_MAP.put(sessionKey.toString(), session);
+    }
+
+    public static void removeSession(Object sessionKey) {
+        SESSION_MAP.remove(sessionKey.toString());
+    }
+
+    public static WebSocketSession getSession(Object sessionKey) {
+        return SESSION_MAP.get(sessionKey.toString());
+    }
+
+    public static Collection<WebSocketSession> getSessions() {
+        return SESSION_MAP.values();
+    }
+
+    public static Set<String> getSessionKeys() {
+        return SESSION_MAP.keySet();
+    }
+
+}

+ 31 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketUtils.java

@@ -0,0 +1,31 @@
+package cn.iocoder.yudao.framework.websocket.core;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.socket.TextMessage;
+import org.springframework.web.socket.WebSocketSession;
+
+import java.io.IOException;
+
+@Slf4j
+public class WebSocketUtils {
+    public static boolean sendMessage(WebSocketSession seesion, String message) {
+        if (seesion == null) {
+            log.error("seesion 不存在");
+            return false;
+        }
+        if (seesion.isOpen()) {
+            try {
+                seesion.sendMessage(new TextMessage(message));
+            } catch (IOException e) {
+                log.error("WebSocket 消息发送异常 Session={} | msg= {} | exception={}", seesion, message, e);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    public static boolean sendMessage(Object sessionKey, String message) {
+        WebSocketSession session = WebSocketSessionHandler.getSession(sessionKey);
+        return sendMessage(session, message);
+    }
+}

+ 49 - 0
yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/YudaoWebSocketHandlerDecorator.java

@@ -0,0 +1,49 @@
+package cn.iocoder.yudao.framework.websocket.core;
+
+import cn.iocoder.yudao.framework.security.core.LoginUser;
+import org.springframework.web.socket.CloseStatus;
+import org.springframework.web.socket.WebSocketHandler;
+import org.springframework.web.socket.WebSocketSession;
+import org.springframework.web.socket.handler.WebSocketHandlerDecorator;
+
+public class YudaoWebSocketHandlerDecorator extends WebSocketHandlerDecorator {
+    public YudaoWebSocketHandlerDecorator(WebSocketHandler delegate) {
+        super(delegate);
+    }
+
+    /**
+     * websocket 连接时执行的动作
+     * @param session websocket session 对象
+     * @throws Exception 异常对象
+     */
+    @Override
+    public void afterConnectionEstablished(final WebSocketSession session) throws Exception {
+        Object sessionKey = sessionKeyGen(session);
+        WebSocketSessionHandler.addSession(sessionKey, session);
+    }
+
+    /**
+     * websocket 关闭连接时执行的动作
+     * @param session websocket session 对象
+     * @param closeStatus 关闭状态对象
+     * @throws Exception 异常对象
+     */
+    @Override
+    public void afterConnectionClosed(final WebSocketSession session, CloseStatus closeStatus) throws Exception {
+        Object sessionKey = sessionKeyGen(session);
+        WebSocketSessionHandler.removeSession(sessionKey);
+    }
+
+    public Object sessionKeyGen(WebSocketSession webSocketSession) {
+
+        Object obj = webSocketSession.getAttributes().get(WebSocketKeyDefine.LOGIN_USER);
+
+        if (obj instanceof LoginUser) {
+            LoginUser loginUser = (LoginUser) obj;
+            // userId 作为唯一区分
+            return String.valueOf(loginUser.getId());
+        }
+
+        return null;
+    }
+}

+ 33 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTaskAssignRuleTypeEnum.java

@@ -0,0 +1,33 @@
+package cn.iocoder.yudao.module.bpm.enums.definition;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * BPM 任务分配规则的类型枚举
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmTaskAssignRuleTypeEnum {
+
+    ROLE(10, "角色"),
+    DEPT_MEMBER(20, "部门的成员"), // 包括负责人
+    DEPT_LEADER(21, "部门的负责人"),
+    POST(22, "岗位"),
+    USER(30, "用户"),
+    USER_GROUP(40, "用户组"),
+    SCRIPT(50, "自定义脚本"), // 例如说,发起人所在部门的领导、发起人所在部门的领导的领导
+    ;
+
+    /**
+     * 类型
+     */
+    private final Integer type;
+    /**
+     * 描述
+     */
+    private final String desc;
+
+}

+ 30 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTaskRuleScriptEnum.java

@@ -0,0 +1,30 @@
+package cn.iocoder.yudao.module.bpm.enums.definition;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * BPM 任务规则的脚本枚举
+ * 目前暂时通过 TODO 芋艿:硬编码,未来可以考虑 Groovy 动态脚本的方式
+ *
+ * @author 芋道源码
+ */
+@Getter
+@AllArgsConstructor
+public enum BpmTaskRuleScriptEnum {
+
+    START_USER(10L, "流程发起人"),
+
+    LEADER_X1(20L, "流程发起人的一级领导"),
+    LEADER_X2(21L, "流程发起人的二级领导");
+
+    /**
+     * 脚本编号
+     */
+    private final Long id;
+    /**
+     * 脚本描述
+     */
+    private final String desc;
+
+}

+ 8 - 7
yudao-module-bpm/yudao-module-bpm-biz/pom.xml

@@ -66,14 +66,15 @@
             <artifactId>yudao-spring-boot-starter-excel</artifactId>
         </dependency>
 
-        <!-- Flowable 工作流相关 -->
-        <dependency>
-            <groupId>org.flowable</groupId>
-            <artifactId>flowable-spring-boot-starter-process</artifactId>
-        </dependency>
+
+        <!-- 工作流相关 -->
+
         <dependency>
-            <groupId>org.flowable</groupId>
-            <artifactId>flowable-spring-boot-starter-actuator</artifactId>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-spring-boot-starter-flowable</artifactId>
+            <version>2.1.0-jdk8-snapshot</version>
+            <scope>compile</scope>
         </dependency>
+
     </dependencies>
 </project>

+ 23 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleBaseVO.java

@@ -0,0 +1,23 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.rule;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import javax.validation.constraints.NotNull;
+import java.util.Set;
+
+/**
+ * 流程任务分配规则 Base VO,提供给添加、修改、详细的子 VO 使用
+ * 如果子 VO 存在差异的字段,请不要添加到这里,影响 Swagger 文档生成
+ */
+@Data
+public class BpmTaskAssignRuleBaseVO {
+
+    @Schema(description = "规则类型", required = true, example = "bpm_task_assign_rule_type")
+    @NotNull(message = "规则类型不能为空")
+    private Integer type;
+
+    @Schema(description = "规则值数组", required = true, example = "1,2,3")
+    @NotNull(message = "规则值数组不能为空")
+    private Set<Long> options;
+
+}

+ 24 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleCreateReqVO.java

@@ -0,0 +1,24 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.rule;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import javax.validation.constraints.NotEmpty;
+
+@Schema(description = "管理后台 - 流程任务分配规则的创建 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class BpmTaskAssignRuleCreateReqVO extends BpmTaskAssignRuleBaseVO {
+
+    @Schema(description = "流程模型的编号", required = true, example = "1024")
+    @NotEmpty(message = "流程模型的编号不能为空")
+    private String modelId;
+
+    @Schema(description = "流程任务定义的编号", required = true, example = "2048")
+    @NotEmpty(message = "流程任务定义的编号不能为空")
+    private String taskDefinitionKey;
+
+}

+ 28 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleRespVO.java

@@ -0,0 +1,28 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.rule;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+@Schema(description = "管理后台 - 流程任务分配规则的 Response VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class BpmTaskAssignRuleRespVO extends BpmTaskAssignRuleBaseVO {
+
+    @Schema(description = "任务分配规则的编号", required = true, example = "1024")
+    private Long id;
+
+    @Schema(description = "流程模型的编号", required = true, example = "2048")
+    private String modelId;
+
+    @Schema(description = "流程定义的编号", required = true, example = "4096")
+    private String processDefinitionId;
+
+    @Schema(description = "流程任务定义的编号", required = true, example = "2048")
+    private String taskDefinitionKey;
+    @Schema(description = "流程任务定义的名字", required = true, example = "关注芋道")
+    private String taskDefinitionName;
+
+}

+ 20 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleUpdateReqVO.java

@@ -0,0 +1,20 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.rule;
+
+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 BpmTaskAssignRuleUpdateReqVO extends BpmTaskAssignRuleBaseVO {
+
+    @Schema(description = "任务分配规则的编号", required = true, example = "1024")
+    @NotNull(message = "任务分配规则的编号不能为空")
+    private Long id;
+
+}

+ 12 - 10
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskAssignRuleController.java

@@ -1,8 +1,8 @@
 package cn.iocoder.yudao.module.bpm.controller.admin.task;
 
-import cn.iocoder.yudao.module.bpm.convert.definition.BpmProcessDefinitionConvert;
-import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
-import org.flowable.engine.repository.ProcessDefinition;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleRespVO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmTaskAssignRuleDO;
+import io.swagger.v3.oas.annotations.Parameters;
 import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import org.springframework.validation.annotation.Validated;
@@ -11,7 +11,6 @@ import io.swagger.v3.oas.annotations.tags.Tag;
 import io.swagger.v3.oas.annotations.Parameter;
 import io.swagger.v3.oas.annotations.Operation;
 
-import javax.validation.constraints.*;
 import javax.validation.*;
 import javax.servlet.http.*;
 import java.util.*;
@@ -30,7 +29,6 @@ import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.*;
 import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
 
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.*;
-import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmTaskAssignRuleDO;
 import cn.iocoder.yudao.module.bpm.service.task.BpmTaskAssignRuleService;
 
 @Tag(name = "管理后台 - Bpm 任务规则")
@@ -76,12 +74,16 @@ public class BpmTaskAssignRuleController {
     }
 
     @GetMapping("/list")
-    @Operation(summary = "获得Bpm 任务规则")
-    @Parameter(name = "modelId", description = "流程模型的编号", required = true, example = "1024")
+    @Operation(summary = "获得任务分配规则列表")
+    @Parameters({
+            @Parameter(name = "modelId", description = "模型编号", example = "1024"),
+            @Parameter(name = "processDefinitionId", description = "流程定义的编号", example = "2048")
+    })
     @PreAuthorize("@ss.hasPermission('bpm:task-assign-rule:query')")
-    public CommonResult<List<BpmTaskAssignRuleDO>> getTaskAssignRule(@RequestParam("modelId") String modelId) {
-        List<BpmTaskAssignRuleDO> taskAssignRule = taskAssignRuleService.getTaskAssignRuleModelId(modelId);
-        return success(taskAssignRule);
+    public CommonResult<List<BpmTaskAssignRuleRespVO>> getTaskAssignRuleList(
+            @RequestParam(value = "modelId", required = false) String modelId,
+            @RequestParam(value = "processDefinitionId", required = false) String processDefinitionId) {
+        return success(taskAssignRuleService.getTaskAssignRuleList(modelId, processDefinitionId));
     }
 
     @GetMapping("/page")

+ 0 - 44
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/BpmTaskAssignRuleRespVO.java

@@ -1,44 +0,0 @@
-package cn.iocoder.yudao.module.bpm.controller.admin.task.vo;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-import lombok.*;
-import java.util.*;
-import java.util.*;
-import org.springframework.format.annotation.DateTimeFormat;
-import java.time.LocalDateTime;
-import com.alibaba.excel.annotation.*;
-
-@Schema(description = "管理后台 - Bpm 任务规则 Response VO")
-@Data
-@ExcelIgnoreUnannotated
-public class BpmTaskAssignRuleRespVO {
-
-    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17826")
-    @ExcelProperty("编号")
-    private Long id;
-
-    @Schema(description = "流程模型的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1174")
-    @ExcelProperty("流程模型的编号")
-    private String modelId;
-
-    @Schema(description = "流程定义的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23738")
-    @ExcelProperty("流程定义的编号")
-    private String processDefinitionId;
-
-    @Schema(description = "流程任务定义的key", requiredMode = Schema.RequiredMode.REQUIRED)
-    @ExcelProperty("流程任务定义的key")
-    private String taskDefinitionKey;
-
-    @Schema(description = "规则类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
-    @ExcelProperty("规则类型")
-    private Integer type;
-
-    @Schema(description = "规则值,JSON 数组", requiredMode = Schema.RequiredMode.REQUIRED)
-    @ExcelProperty("规则值,JSON 数组")
-    private String options;
-
-    @Schema(description = "创建时间")
-    @ExcelProperty("创建时间")
-    private LocalDateTime createTime;
-
-}

+ 40 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmTaskAssignRuleConvert.java

@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.bpm.convert.definition;
+
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleCreateReqVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleRespVO;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleUpdateReqVO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmTaskAssignRuleDO;
+import org.flowable.bpmn.model.UserTask;
+import org.mapstruct.Mapper;
+import org.mapstruct.factory.Mappers;
+
+import java.util.List;
+import java.util.Map;
+
+@Mapper
+public interface BpmTaskAssignRuleConvert {
+    BpmTaskAssignRuleConvert INSTANCE = Mappers.getMapper(BpmTaskAssignRuleConvert.class);
+
+    default List<BpmTaskAssignRuleRespVO> convertList(List<UserTask> tasks, List<BpmTaskAssignRuleDO> rules) {
+        Map<String, BpmTaskAssignRuleDO> ruleMap = CollectionUtils.convertMap(rules, BpmTaskAssignRuleDO::getTaskDefinitionKey);
+        // 以 UserTask 为主维度,原因是:流程图编辑后,一些规则实际就没用了。
+        return CollectionUtils.convertList(tasks, task -> {
+            BpmTaskAssignRuleRespVO respVO = convert(ruleMap.get(task.getId()));
+            if (respVO == null) {
+                respVO = new BpmTaskAssignRuleRespVO();
+                respVO.setTaskDefinitionKey(task.getId());
+            }
+            respVO.setTaskDefinitionName(task.getName());
+            return respVO;
+        });
+    }
+
+    BpmTaskAssignRuleRespVO convert(BpmTaskAssignRuleDO bean);
+
+    BpmTaskAssignRuleDO convert(BpmTaskAssignRuleCreateReqVO bean);
+
+    BpmTaskAssignRuleDO convert(BpmTaskAssignRuleUpdateReqVO bean);
+
+    List<BpmTaskAssignRuleDO> convertList2(List<BpmTaskAssignRuleRespVO> list);
+}

+ 83 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmTaskAssignRuleDO.java

@@ -0,0 +1,83 @@
+package cn.iocoder.yudao.module.bpm.dal.dataobject.definition;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.framework.mybatis.core.type.JsonLongSetTypeHandler;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskAssignRuleTypeEnum;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskRuleScriptEnum;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.util.Set;
+
+/**
+ * Bpm 任务分配的规则表,用于自定义配置每个任务的负责人、候选人的分配规则。
+ * 也就是说,废弃 BPMN 原本的 UserTask 设置的 assignee、candidateUsers 等配置,而是通过使用该规则进行计算对应的负责人。
+ *
+ * 1. 默认情况下,{@link #processDefinitionId} 为 {@link #PROCESS_DEFINITION_ID_NULL} 值,表示贵改则与流程模型关联
+ * 2. 在流程模型部署后,会将他的所有规则记录,复制出一份新部署出来的流程定义,通过设置 {@link #processDefinitionId} 为新的流程定义的编号进行关联
+ *
+ * @author 芋道源码
+ */
+@TableName(value = "bpm_task_assign_rule", autoResultMap = true)
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class BpmTaskAssignRuleDO extends BaseDO {
+
+    /**
+     * {@link #processDefinitionId} 空串,用于标识属于流程模型,而不属于流程定义
+     */
+    public static final String PROCESS_DEFINITION_ID_NULL = "";
+
+    /**
+     * 编号
+     */
+    @TableId
+    private Long id;
+
+    /**
+     * 流程模型编号
+     *
+     * 关联 Model 的 id 属性
+     */
+    private String modelId;
+    /**
+     * 流程定义编号
+     *
+     * 关联 ProcessDefinition 的 id 属性
+     */
+    private String processDefinitionId;
+    /**
+     * 流程任务的定义 Key
+     *
+     * 关联 Task 的 taskDefinitionKey 属性
+     */
+    private String taskDefinitionKey;
+
+    /**
+     * 规则类型
+     *
+     * 枚举 {@link BpmTaskAssignRuleTypeEnum}
+     */
+    @TableField("`type`")
+    private Integer type;
+    /**
+     * 规则值数组,一般关联指定表的编号
+     * 根据 type 不同,对应的值是不同的:
+     *
+     * 1. {@link BpmTaskAssignRuleTypeEnum#ROLE} 时:角色编号
+     * 2. {@link BpmTaskAssignRuleTypeEnum#DEPT_MEMBER} 时:部门编号
+     * 3. {@link BpmTaskAssignRuleTypeEnum#DEPT_LEADER} 时:部门编号
+     * 4. {@link BpmTaskAssignRuleTypeEnum#USER} 时:用户编号
+     * 5. {@link BpmTaskAssignRuleTypeEnum#USER_GROUP} 时:用户组编号
+     * 6. {@link BpmTaskAssignRuleTypeEnum#SCRIPT} 时:脚本编号,目前通过 {@link BpmTaskRuleScriptEnum#getId()} 标识
+     */
+    @TableField(typeHandler = JsonLongSetTypeHandler.class)
+    private Set<Long> options;
+
+}

+ 0 - 51
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/task/BpmTaskAssignRuleDO.java

@@ -1,51 +0,0 @@
-package cn.iocoder.yudao.module.bpm.dal.dataobject.task;
-
-import lombok.*;
-import java.util.*;
-import java.time.LocalDateTime;
-import java.time.LocalDateTime;
-import com.baomidou.mybatisplus.annotation.*;
-import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
-
-/**
- * Bpm 任务规则 DO
- *
- * @author 芋道源码
- */
-@TableName("bpm_task_assign_rule")
-@KeySequence("bpm_task_assign_rule_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
-@Data
-@EqualsAndHashCode(callSuper = true)
-@ToString(callSuper = true)
-@Builder
-@NoArgsConstructor
-@AllArgsConstructor
-public class BpmTaskAssignRuleDO extends BaseDO {
-
-    /**
-     * 编号
-     */
-    @TableId
-    private Long id;
-    /**
-     * 流程模型的编号
-     */
-    private String modelId;
-    /**
-     * 流程定义的编号
-     */
-    private String processDefinitionId;
-    /**
-     * 流程任务定义的key
-     */
-    private String taskDefinitionKey;
-    /**
-     * 规则类型
-     */
-    private Integer type;
-    /**
-     * 规则值,JSON 数组
-     */
-    private String options;
-
-}

+ 24 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/task/BpmTaskAssignRuleMapper.java

@@ -5,9 +5,11 @@ import java.util.*;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
-import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmTaskAssignRuleDO;
+import cn.iocoder.yudao.framework.mybatis.core.query.QueryWrapperX;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmTaskAssignRuleDO;
 import org.apache.ibatis.annotations.Mapper;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.*;
+import org.springframework.lang.Nullable;
 
 /**
  * Bpm 任务规则 Mapper
@@ -28,4 +30,25 @@ public interface BpmTaskAssignRuleMapper extends BaseMapperX<BpmTaskAssignRuleDO
                 .orderByDesc(BpmTaskAssignRuleDO::getId));
     }
 
+    default List<BpmTaskAssignRuleDO> selectListByModelId(String modelId) {
+        return selectList(new QueryWrapperX<BpmTaskAssignRuleDO>()
+                .eq("model_id", modelId)
+                .eq("process_definition_id", BpmTaskAssignRuleDO.PROCESS_DEFINITION_ID_NULL));
+    }
+
+    default List<BpmTaskAssignRuleDO> selectListByProcessDefinitionId(String processDefinitionId,
+                                                                      @Nullable String taskDefinitionKey) {
+        return selectList(new QueryWrapperX<BpmTaskAssignRuleDO>()
+                .eq("process_definition_id", processDefinitionId)
+                .eqIfPresent("task_definition_key", taskDefinitionKey));
+    }
+
+    default BpmTaskAssignRuleDO selectListByModelIdAndTaskDefinitionKey(String modelId,
+                                                                        String taskDefinitionKey) {
+        return selectOne(new QueryWrapperX<BpmTaskAssignRuleDO>()
+                .eq("model_id", modelId)
+                .eq("process_definition_id", BpmTaskAssignRuleDO.PROCESS_DEFINITION_ID_NULL)
+                .eq("task_definition_key", taskDefinitionKey));
+    }
+
 }

+ 1 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/web/config/BpmWebConfiguration.java

@@ -13,7 +13,7 @@ import org.springframework.context.annotation.Configuration;
  *
  * @author 芋道源码
  */
-@Configuration(proxyBeanMethods = false)
+//@Configuration(proxyBeanMethods = false)
 public class BpmWebConfiguration {
 
     /**

+ 8 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java

@@ -84,4 +84,12 @@ public interface BpmModelService {
      */
     BpmnModel getBpmnModelByDefinitionId(String processDefinitionId);
 
+    /**
+     * 获得流程模型编号对应的 BPMN Model
+     *
+     * @param id 流程模型编号
+     * @return BPMN Model
+     */
+    BpmnModel getBpmnModel(String id);
+
 }

+ 13 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java

@@ -1,5 +1,6 @@
 package cn.iocoder.yudao.module.bpm.service.definition;
 
+import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.StrUtil;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
@@ -16,10 +17,12 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
 import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
 import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
 import lombok.extern.slf4j.Slf4j;
+import org.flowable.bpmn.converter.BpmnXMLConverter;
 import org.flowable.bpmn.model.BpmnModel;
 import org.flowable.bpmn.model.StartEvent;
 import org.flowable.bpmn.model.UserTask;
 import org.flowable.common.engine.impl.db.SuspensionState;
+import org.flowable.common.engine.impl.util.io.BytesStreamSource;
 import org.flowable.engine.RepositoryService;
 import org.flowable.engine.repository.Model;
 import org.flowable.engine.repository.ModelQuery;
@@ -208,6 +211,16 @@ public class BpmModelServiceImpl implements BpmModelService {
         return repositoryService.getBpmnModel(processDefinitionId);
     }
 
+    @Override
+    public BpmnModel getBpmnModel(String id) {
+        byte[] bpmnBytes = repositoryService.getModelEditorSource(id);
+        if (ArrayUtil.isEmpty(bpmnBytes)) {
+            return null;
+        }
+        BpmnXMLConverter converter = new BpmnXMLConverter();
+        return converter.convertToBpmnModel(new BytesStreamSource(bpmnBytes), true, true);
+    }
+
     /**
      * 校验流程表单已配置
      *

+ 8 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionService.java

@@ -159,4 +159,12 @@ public interface BpmProcessDefinitionService {
      */
     Deployment getDeployment(String id);
 
+    /**
+     * 获得 Bpmn 模型
+     *
+     * @param processDefinitionId 流程定义的编号
+     * @return Bpmn 模型
+     */
+    BpmnModel getBpmnModel(String processDefinitionId);
+
 }

+ 5 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java

@@ -199,4 +199,9 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
         return query.list();
     }
 
+    @Override
+    public BpmnModel getBpmnModel(String processDefinitionId) {
+        return repositoryService.getBpmnModel(processDefinitionId);
+    }
+
 }

+ 24 - 2
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskAssignRuleService.java

@@ -2,10 +2,12 @@ package cn.iocoder.yudao.module.bpm.service.task;
 
 import java.util.*;
 import javax.validation.*;
+
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleRespVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.*;
-import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmTaskAssignRuleDO;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmTaskAssignRuleDO;
+import org.springframework.lang.Nullable;
 
 /**
  * Bpm 任务规则 Service 接口
@@ -14,6 +16,17 @@ import cn.iocoder.yudao.framework.common.pojo.PageParam;
  */
 public interface BpmTaskAssignRuleService {
 
+    /**
+     * 获得流程定义的任务分配规则数组
+     *
+     * @param processDefinitionId 流程定义的编号
+     * @param taskDefinitionKey 流程任务定义的 Key。允许空
+     * @return 任务规则数组
+     */
+    List<BpmTaskAssignRuleDO> getTaskAssignRuleListByProcessDefinitionId(String processDefinitionId,
+                                                                         @Nullable String taskDefinitionKey);
+
+
     /**
      * 创建Bpm 任务规则
      *
@@ -62,4 +75,13 @@ public interface BpmTaskAssignRuleService {
      */
     PageResult<BpmTaskAssignRuleDO> getTaskAssignRulePage(BpmTaskAssignRulePageReqVO pageReqVO);
 
+    /**
+     * 获得流程定义的任务分配规则数组
+     *
+     * @param modelId             流程模型的编号
+     * @param processDefinitionId 流程定义的编号
+     * @return 任务规则数组
+     */
+    List<BpmTaskAssignRuleRespVO> getTaskAssignRuleList(String modelId, String processDefinitionId);
+
 }

+ 49 - 4
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskAssignRuleServiceImpl.java

@@ -1,15 +1,23 @@
 package cn.iocoder.yudao.module.bpm.service.task;
 
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.rule.BpmTaskAssignRuleRespVO;
+import cn.iocoder.yudao.module.bpm.convert.definition.BpmTaskAssignRuleConvert;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmTaskAssignRuleDO;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
+import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.bpmn.model.UserTask;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import javax.annotation.Resource;
 import org.springframework.validation.annotation.Validated;
-import org.springframework.transaction.annotation.Transactional;
+import cn.iocoder.yudao.framework.flowable.core.util.FlowableUtils;
 
 import java.util.*;
 import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.*;
-import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmTaskAssignRuleDO;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
-import cn.iocoder.yudao.framework.common.pojo.PageParam;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 
 import cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmTaskAssignRuleMapper;
@@ -28,6 +36,12 @@ public class BpmTaskAssignRuleServiceImpl implements BpmTaskAssignRuleService {
 
     @Resource
     private BpmTaskAssignRuleMapper taskAssignRuleMapper;
+    @Resource
+    @Lazy // 解决循环依赖
+    private BpmModelService modelService;
+    @Resource
+    @Lazy // 解决循环依赖
+    private BpmProcessDefinitionService processDefinitionService;
 
     @Override
     public Long createTaskAssignRule(BpmTaskAssignRuleSaveReqVO createReqVO) {
@@ -68,7 +82,7 @@ public class BpmTaskAssignRuleServiceImpl implements BpmTaskAssignRuleService {
 
     @Override
     public List<BpmTaskAssignRuleDO> getTaskAssignRuleModelId(String modelId) {
-        return taskAssignRuleMapper.selectList(BpmTaskAssignRuleDO::getModelId, modelId);
+        return taskAssignRuleMapper.selectListByModelId( modelId);
     }
 
     @Override
@@ -76,4 +90,35 @@ public class BpmTaskAssignRuleServiceImpl implements BpmTaskAssignRuleService {
         return taskAssignRuleMapper.selectPage(pageReqVO);
     }
 
+    @Override
+    public List<BpmTaskAssignRuleRespVO> getTaskAssignRuleList(String modelId, String processDefinitionId) {
+        // 获得规则
+        List<BpmTaskAssignRuleDO> rules = Collections.emptyList();
+        BpmnModel model = null;
+        if (StrUtil.isNotEmpty(modelId)) {
+            rules = getTaskAssignRuleModelId(modelId);
+            model = modelService.getBpmnModel(modelId);
+        } else if (StrUtil.isNotEmpty(processDefinitionId)) {
+            rules = getTaskAssignRuleListByProcessDefinitionId(processDefinitionId, null);
+            model = processDefinitionService.getBpmnModel(processDefinitionId);
+        }
+        if (model == null) {
+            return Collections.emptyList();
+        }
+        // 获得用户任务,只有用户任务才可以设置分配规则
+        List<UserTask> userTasks = FlowableUtils.getBpmnModelElements(model, UserTask.class);
+        if (CollUtil.isEmpty(userTasks)) {
+            return Collections.emptyList();
+        }
+        // 转换数据
+        return BpmTaskAssignRuleConvert.INSTANCE.convertList(userTasks, rules);
+
+    }
+
+    @Override
+    public List<BpmTaskAssignRuleDO> getTaskAssignRuleListByProcessDefinitionId(String processDefinitionId,
+                                                                                String taskDefinitionKey) {
+        return taskAssignRuleMapper.selectListByProcessDefinitionId(processDefinitionId, taskDefinitionKey);
+    }
+
 }