Browse Source

Merge branch 'master' of http://git.dgtis.com/15896567520/oneportal_saas into saas_240708

zhaopeiqing 8 months ago
parent
commit
f0306d3dfc
79 changed files with 3261 additions and 15 deletions
  1. 1 1
      pom.xml
  2. 2 0
      yudao-framework/pom.xml
  3. 46 0
      yudao-framework/yudao-common/src/main/java/cn/iocoder/yudao/framework/common/util/spring/SpringAopUtils.java
  4. 58 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/job/TenantJobHandlerDecorator.java
  5. 42 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/mq/TenantRedisMessageInterceptor.java
  6. 47 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/main/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefine.java
  7. 27 0
      yudao-framework/yudao-spring-boot-starter-biz-tenant/src/test/java/cn/iocoder/yudao/framework/tenant/core/redis/TenantRedisKeyDefineTest.java
  8. 37 0
      yudao-framework/yudao-spring-boot-starter-flowable/pom.xml
  9. 43 0
      yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/config/YudaoFlowableConfiguration.java
  10. 1 0
      yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/package-info.java
  11. 82 0
      yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/util/FlowableUtils.java
  12. 35 0
      yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/core/web/FlowableWebFilter.java
  13. 1 0
      yudao-framework/yudao-spring-boot-starter-flowable/src/main/java/cn/iocoder/yudao/framework/flowable/package-info.java
  14. 1 0
      yudao-framework/yudao-spring-boot-starter-flowable/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
  15. 162 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/config/YudaoMQAutoConfiguration.java
  16. 87 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/RedisMQTemplate.java
  17. 26 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/interceptor/RedisMessageInterceptor.java
  18. 29 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/message/AbstractRedisMessage.java
  19. 21 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/pubsub/AbstractChannelMessage.java
  20. 103 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/pubsub/AbstractChannelMessageListener.java
  21. 21 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/stream/AbstractStreamMessage.java
  22. 113 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/core/stream/AbstractStreamMessageListener.java
  23. 80 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/cn/iocoder/yudao/framework/mq/job/RedisPendingMessageResendJob.java
  24. 62 0
      yudao-framework/yudao-spring-boot-starter-mq/src/main/java/org/springframework/data/redis/stream/DefaultStreamMessageListenerContainerX.java
  25. 24 0
      yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/expression/AndExpressionX.java
  26. 24 0
      yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/expression/OrExpressionX.java
  27. 1 0
      yudao-framework/yudao-spring-boot-starter-mybatis/src/main/java/cn/iocoder/yudao/framework/package-info.java
  28. 9 0
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/resilience4j/package-info.java
  29. 1 0
      yudao-framework/yudao-spring-boot-starter-protection/src/main/java/cn/iocoder/yudao/framework/resilience4j/《芋道 Spring Boot 服务容错 Resilience4j 入门》.md
  30. 113 0
      yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/RedisKeyDefine.java
  31. 28 0
      yudao-framework/yudao-spring-boot-starter-redis/src/main/java/cn/iocoder/yudao/framework/redis/core/RedisKeyRegistry.java
  32. 85 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiAccessLog.java
  33. 107 0
      yudao-framework/yudao-spring-boot-starter-web/src/main/java/cn/iocoder/yudao/framework/apilog/core/service/ApiErrorLog.java
  34. 14 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/config/WebSocketHandlerConfig.java
  35. 24 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/UserHandshakeInterceptor.java
  36. 9 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketKeyDefine.java
  37. 24 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketMessageDO.java
  38. 36 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketSessionHandler.java
  39. 31 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/WebSocketUtils.java
  40. 49 0
      yudao-framework/yudao-spring-boot-starter-websocket/src/main/java/cn/iocoder/yudao/framework/websocket/core/YudaoWebSocketHandlerDecorator.java
  41. 7 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java
  42. 33 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTaskAssignRuleTypeEnum.java
  43. 30 0
      yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTaskRuleScriptEnum.java
  44. 8 7
      yudao-module-bpm/yudao-module-bpm-biz/pom.xml
  45. 9 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmUserGroupController.java
  46. 1 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/group/BpmUserGroupRespVO.java
  47. 1 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/group/BpmUserGroupSaveReqVO.java
  48. 23 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleBaseVO.java
  49. 24 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleCreateReqVO.java
  50. 28 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleRespVO.java
  51. 20 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/rule/BpmTaskAssignRuleUpdateReqVO.java
  52. 58 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmTaskAssignRuleController.java
  53. 37 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/BpmTaskAssignRulePageReqVO.java
  54. 35 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/BpmTaskAssignRuleSaveReqVO.java
  55. 40 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/definition/BpmTaskAssignRuleConvert.java
  56. 83 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmTaskAssignRuleDO.java
  57. 1 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmUserGroupDO.java
  58. 54 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/mysql/task/BpmTaskAssignRuleMapper.java
  59. 34 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/script/BpmTaskAssignScript.java
  60. 70 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignLeaderAbstractScript.java
  61. 27 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignLeaderX1Script.java
  62. 27 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignLeaderX2Script.java
  63. 40 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignStartUserScript.java
  64. 1 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateGroupStrategy.java
  65. 1 1
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/web/config/BpmWebConfiguration.java
  66. 8 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelService.java
  67. 13 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java
  68. 8 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionService.java
  69. 5 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java
  70. 98 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskAssignRuleService.java
  71. 352 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskAssignRuleServiceImpl.java
  72. 12 0
      yudao-module-bpm/yudao-module-bpm-biz/src/main/resources/mapper/task/BpmTaskAssignRuleMapper.xml
  73. 9 0
      yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApi.java
  74. 7 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApiImpl.java
  75. 3 0
      yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/user/UserConvert.java
  76. 2 2
      yudao-server/pom.xml
  77. 53 0
      yudao-ui/yudao-ui-admin-vue2/src/api/bpm/task/index.js
  78. 122 0
      yudao-ui/yudao-ui-admin-vue2/src/views/bpm/task/TaskAssignRuleForm.vue
  79. 171 0
      yudao-ui/yudao-ui-admin-vue2/src/views/bpm/task/index.vue

+ 1 - 1
pom.xml

@@ -18,7 +18,7 @@
         <module>yudao-module-member</module>
         <module>yudao-module-bpm</module>
         <module>yudao-module-report</module>
-        <module>yudao-module-mp</module>
+        <!--<module>yudao-module-mp</module>-->
         <module>yudao-module-pay</module>
         <module>yudao-module-mall</module>
         <module>yudao-module-crm</module>

+ 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;
+    }
+}

+ 7 - 0
yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java

@@ -74,5 +74,12 @@ public interface ErrorCodeConstants {
 
     // ========== BPM 流程表达式 1-009-014-000 ==========
     ErrorCode PROCESS_EXPRESSION_NOT_EXISTS = new ErrorCode(1_009_014_000, "流程表达式不存在");
+    // ========== BPM 流程规则 1_009_015_000 ==========
+    ErrorCode TASK_ASSIGN_RULE_NOT_EXISTS = new ErrorCode(1_009_015_000, "Bpm 任务规则不存在");
+    ErrorCode TASK_ASSIGN_RULE_EXISTS = new ErrorCode(1_009_006_000, "流程({}) 的任务({}) 已经存在分配规则");
+    ErrorCode TASK_UPDATE_FAIL_NOT_MODEL = new ErrorCode(1_009_006_002, "只有流程模型的任务分配规则,才允许被修改");
+    ErrorCode TASK_ASSIGN_SCRIPT_NOT_EXISTS = new ErrorCode(1_009_006_004, "操作失败,原因:任务分配脚本({}) 不存在");
+
+
 
 }

+ 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>

+ 9 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmUserGroupController.java

@@ -4,9 +4,11 @@ import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
 import cn.iocoder.yudao.framework.common.pojo.CommonResult;
 import cn.iocoder.yudao.framework.common.pojo.PageResult;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormRespVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group.BpmUserGroupPageReqVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group.BpmUserGroupRespVO;
 import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.group.BpmUserGroupSaveReqVO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
 import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO;
 import cn.iocoder.yudao.module.bpm.service.definition.BpmUserGroupService;
 import io.swagger.v3.oas.annotations.Operation;
@@ -80,4 +82,11 @@ public class BpmUserGroupController {
         return success(convertList(list, group -> new BpmUserGroupRespVO().setId(group.getId()).setName(group.getName())));
     }
 
+    @GetMapping("/list-all-simple")
+    @Operation(summary = "获取用户组精简信息列表", description = "只包含被开启的用户组")
+    public CommonResult<List<BpmUserGroupRespVO>> getUserGroupSimpleListAll() {
+        List<BpmUserGroupDO> list = userGroupService.getUserGroupListByStatus(CommonStatusEnum.ENABLE.getStatus());
+        return success(convertList(list, group -> new BpmUserGroupRespVO().setId(group.getId()).setName(group.getName())));
+    }
+
 }

+ 1 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/group/BpmUserGroupRespVO.java

@@ -20,7 +20,7 @@ public class BpmUserGroupRespVO {
     private String description;
 
     @Schema(description = "成员编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
-    private Set<Long> userIds;
+    private Set<Long> memberUserIds;
 
     @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     private Integer status;

+ 1 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/group/BpmUserGroupSaveReqVO.java

@@ -22,7 +22,7 @@ public class BpmUserGroupSaveReqVO {
 
     @Schema(description = "成员编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "1,2,3")
     @NotNull(message = "成员编号数组不能为空")
-    private Set<Long> userIds;
+    private Set<Long> memberUserIds;
 
     @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
     @NotNull(message = "状态不能为空")

+ 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;
+
+}

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

@@ -0,0 +1,58 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.task;
+
+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 io.swagger.v3.oas.annotations.Parameters;
+import org.springframework.web.bind.annotation.*;
+import javax.annotation.Resource;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.security.access.prepost.PreAuthorize;
+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.*;
+import java.util.*;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import cn.iocoder.yudao.module.bpm.service.task.BpmTaskAssignRuleService;
+
+@Tag(name = "管理后台 - Bpm 任务规则")
+@RestController
+@RequestMapping("/bpm/task-assign-rule")
+@Validated
+public class BpmTaskAssignRuleController {
+
+    @Resource
+    private BpmTaskAssignRuleService taskAssignRuleService;
+
+    @GetMapping("/list")
+    @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<BpmTaskAssignRuleRespVO>> getTaskAssignRuleList(
+            @RequestParam(value = "modelId", required = false) String modelId,
+            @RequestParam(value = "processDefinitionId", required = false) String processDefinitionId) {
+        return success(taskAssignRuleService.getTaskAssignRuleList(modelId, processDefinitionId));
+    }
+
+    @PostMapping("/create")
+    @Operation(summary = "创建任务分配规则")
+    @PreAuthorize("@ss.hasPermission('bpm:task-assign-rule:create')")
+    public CommonResult<Long> createTaskAssignRule(@Valid @RequestBody BpmTaskAssignRuleCreateReqVO reqVO) {
+        return success(taskAssignRuleService.createTaskAssignRule(reqVO));
+    }
+
+    @PutMapping("/update")
+    @Operation(summary = "更新任务分配规则")
+    @PreAuthorize("@ss.hasPermission('bpm:task-assign-rule:update')")
+    public CommonResult<Boolean> updateTaskAssignRule(@Valid @RequestBody BpmTaskAssignRuleUpdateReqVO reqVO) {
+        taskAssignRuleService.updateTaskAssignRule(reqVO);
+        return success(true);
+    }
+
+}

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

@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.task.vo;
+
+import lombok.*;
+import java.util.*;
+import io.swagger.v3.oas.annotations.media.Schema;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import org.springframework.format.annotation.DateTimeFormat;
+import java.time.LocalDateTime;
+
+import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
+
+@Schema(description = "管理后台 - Bpm 任务规则分页 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class BpmTaskAssignRulePageReqVO extends PageParam {
+
+    @Schema(description = "流程模型的编号", example = "1174")
+    private String modelId;
+
+    @Schema(description = "流程定义的编号", example = "23738")
+    private String processDefinitionId;
+
+    @Schema(description = "流程任务定义的key")
+    private String taskDefinitionKey;
+
+    @Schema(description = "规则类型", example = "1")
+    private Integer type;
+
+    @Schema(description = "规则值,JSON 数组")
+    private String options;
+
+    @Schema(description = "创建时间")
+    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
+    private LocalDateTime[] createTime;
+
+}

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

@@ -0,0 +1,35 @@
+package cn.iocoder.yudao.module.bpm.controller.admin.task.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.*;
+import java.util.*;
+import javax.validation.constraints.*;
+
+@Schema(description = "管理后台 - Bpm 任务规则新增/修改 Request VO")
+@Data
+public class BpmTaskAssignRuleSaveReqVO {
+
+    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "17826")
+    private Long id;
+
+    @Schema(description = "流程模型的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1174")
+    @NotEmpty(message = "流程模型的编号不能为空")
+    private String modelId;
+
+    @Schema(description = "流程定义的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "23738")
+    @NotEmpty(message = "流程定义的编号不能为空")
+    private String processDefinitionId;
+
+    @Schema(description = "流程任务定义的key", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "流程任务定义的key不能为空")
+    private String taskDefinitionKey;
+
+    @Schema(description = "规则类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "规则类型不能为空")
+    private Integer type;
+
+    @Schema(description = "规则值,JSON 数组", requiredMode = Schema.RequiredMode.REQUIRED)
+    @NotEmpty(message = "规则值,JSON 数组不能为空")
+    private String options;
+
+}

+ 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;
+
+}

+ 1 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmUserGroupDO.java

@@ -45,6 +45,6 @@ public class BpmUserGroupDO extends BaseDO {
      * 成员用户编号数组
      */
     @TableField(typeHandler = JsonLongSetTypeHandler.class)
-    private Set<Long> userIds;
+    private Set<Long> memberUserIds;
 
 }

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

@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.bpm.dal.mysql.task;
+
+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.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
+ *
+ * @author 芋道源码
+ */
+@Mapper
+public interface BpmTaskAssignRuleMapper extends BaseMapperX<BpmTaskAssignRuleDO> {
+
+    default PageResult<BpmTaskAssignRuleDO> selectPage(BpmTaskAssignRulePageReqVO reqVO) {
+        return selectPage(reqVO, new LambdaQueryWrapperX<BpmTaskAssignRuleDO>()
+                .eqIfPresent(BpmTaskAssignRuleDO::getModelId, reqVO.getModelId())
+                .eqIfPresent(BpmTaskAssignRuleDO::getProcessDefinitionId, reqVO.getProcessDefinitionId())
+                .eqIfPresent(BpmTaskAssignRuleDO::getTaskDefinitionKey, reqVO.getTaskDefinitionKey())
+                .eqIfPresent(BpmTaskAssignRuleDO::getType, reqVO.getType())
+                .eqIfPresent(BpmTaskAssignRuleDO::getOptions, reqVO.getOptions())
+                .betweenIfPresent(BpmTaskAssignRuleDO::getCreateTime, reqVO.getCreateTime())
+                .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));
+    }
+
+}

+ 34 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/script/BpmTaskAssignScript.java

@@ -0,0 +1,34 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.script;
+
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskRuleScriptEnum;
+import org.flowable.engine.delegate.DelegateExecution;
+
+import java.util.Set;
+
+/**
+ * Bpm 任务分配的自定义 Script 脚本
+ * 使用场景:
+ * 1. 设置审批人为发起人
+ * 2. 设置审批人为发起人的 Leader
+ * 3. 甚至审批人为发起人的 Leader 的 Leader
+ *
+ * @author 芋道源码
+ */
+public interface BpmTaskAssignScript {
+
+    /**
+     * 基于执行任务,获得任务的候选用户们
+     *
+     * @param execution 执行任务
+     * @return 候选人用户的编号数组
+     */
+    Set<Long> calculateTaskCandidateUsers(DelegateExecution execution);
+
+    /**
+     * 获得枚举值
+     *
+     * @return 枚举值
+     */
+    BpmTaskRuleScriptEnum getEnum();
+}
+

+ 70 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignLeaderAbstractScript.java

@@ -0,0 +1,70 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.script.impl;
+
+import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.script.BpmTaskAssignScript;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.util.Assert;
+
+import javax.annotation.Resource;
+import java.util.Set;
+
+import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
+import static java.util.Collections.emptySet;
+
+/**
+ * 分配给发起人的 Leader 审批的 Script 实现类
+ * 目前 Leader 的定义是,
+ *
+ * @author 芋道源码
+ */
+public abstract class BpmTaskAssignLeaderAbstractScript implements BpmTaskAssignScript {
+
+    @Resource
+    private AdminUserApi adminUserApi;
+    @Resource
+    private DeptApi deptApi;
+    @Resource
+    @Lazy // 解决循环依赖
+    private BpmProcessInstanceService bpmProcessInstanceService;
+
+    protected Set<Long> calculateTaskCandidateUsers(DelegateExecution execution, int level) {
+        Assert.isTrue(level > 0, "level 必须大于 0");
+        // 获得发起人
+        ProcessInstance processInstance = bpmProcessInstanceService.getProcessInstance(execution.getProcessInstanceId());
+        Long startUserId = NumberUtils.parseLong(processInstance.getStartUserId());
+        // 获得对应 leve 的部门
+        DeptRespDTO dept = null;
+        for (int i = 0; i < level; i++) {
+            // 获得 level 对应的部门
+            if (dept == null) {
+                dept = getStartUserDept(startUserId);
+                if (dept == null) { // 找不到发起人的部门,所以无法使用该规则
+                    return emptySet();
+                }
+            } else {
+                DeptRespDTO parentDept = deptApi.getDept(dept.getParentId());
+                if (parentDept == null) { // 找不到父级部门,所以只好结束寻找。原因是:例如说,级别比较高的人,所在部门层级比较少
+                    break;
+                }
+                dept = parentDept;
+            }
+        }
+        return dept.getLeaderUserId() != null ? asSet(dept.getLeaderUserId()) : emptySet();
+    }
+
+    private DeptRespDTO getStartUserDept(Long startUserId) {
+        AdminUserRespDTO startUser = adminUserApi.getUser(startUserId);
+        if (startUser.getDeptId() == null) { // 找不到部门,所以无法使用该规则
+            return null;
+        }
+        return deptApi.getDept(startUser.getDeptId());
+    }
+
+}

+ 27 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignLeaderX1Script.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.script.impl;
+
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskRuleScriptEnum;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.springframework.stereotype.Component;
+
+import java.util.Set;
+
+/**
+ * 分配给发起人的一级 Leader 审批的 Script 实现类
+ *
+ * @author 芋道源码
+ */
+@Component
+public class BpmTaskAssignLeaderX1Script extends BpmTaskAssignLeaderAbstractScript {
+
+    @Override
+    public Set<Long> calculateTaskCandidateUsers(DelegateExecution execution) {
+        return calculateTaskCandidateUsers(execution, 1);
+    }
+
+    @Override
+    public BpmTaskRuleScriptEnum getEnum() {
+        return BpmTaskRuleScriptEnum.LEADER_X1;
+    }
+
+}

+ 27 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignLeaderX2Script.java

@@ -0,0 +1,27 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.script.impl;
+
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskRuleScriptEnum;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.springframework.stereotype.Component;
+
+import java.util.Set;
+
+/**
+ * 分配给发起人的二级 Leader 审批的 Script 实现类
+ *
+ * @author 芋道源码
+ */
+@Component
+public class BpmTaskAssignLeaderX2Script extends BpmTaskAssignLeaderAbstractScript {
+
+    @Override
+    public Set<Long> calculateTaskCandidateUsers(DelegateExecution execution) {
+        return calculateTaskCandidateUsers(execution, 2);
+    }
+
+    @Override
+    public BpmTaskRuleScriptEnum getEnum() {
+        return BpmTaskRuleScriptEnum.LEADER_X2;
+    }
+
+}

+ 40 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/script/impl/BpmTaskAssignStartUserScript.java

@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.script.impl;
+
+import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
+import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskRuleScriptEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.script.BpmTaskAssignScript;
+import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+import java.util.Set;
+
+/**
+ * 分配给发起人审批的 Script 实现类
+ *
+ * @author 芋道源码
+ */
+@Component
+public class BpmTaskAssignStartUserScript implements BpmTaskAssignScript {
+
+    @Resource
+    @Lazy // 解决循环依赖
+    private BpmProcessInstanceService bpmProcessInstanceService;
+
+    @Override
+    public Set<Long> calculateTaskCandidateUsers(DelegateExecution execution) {
+        ProcessInstance processInstance = bpmProcessInstanceService.getProcessInstance(execution.getProcessInstanceId());
+        Long startUserId = NumberUtils.parseLong(processInstance.getStartUserId());
+        return SetUtils.asSet(startUserId);
+    }
+
+    @Override
+    public BpmTaskRuleScriptEnum getEnum() {
+        return BpmTaskRuleScriptEnum.START_USER;
+    }
+
+}

+ 1 - 1
yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/BpmTaskCandidateGroupStrategy.java

@@ -41,7 +41,7 @@ public class BpmTaskCandidateGroupStrategy implements BpmTaskCandidateStrategy {
     public Set<Long> calculateUsers(DelegateExecution execution, String param) {
         Set<Long> groupIds = StrUtils.splitToLongSet(param);
         List<BpmUserGroupDO> groups = userGroupService.getUserGroupList(groupIds);
-        return convertSetByFlatMap(groups, BpmUserGroupDO::getUserIds, Collection::stream);
+        return convertSetByFlatMap(groups, BpmUserGroupDO::getMemberUserIds, Collection::stream);
     }
 
 }

+ 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);
+    }
+
 }

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

@@ -0,0 +1,98 @@
+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.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.controller.admin.task.vo.*;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmTaskAssignRuleDO;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.springframework.lang.Nullable;
+
+/**
+ * Bpm 任务规则 Service 接口
+ *
+ * @author 芋道源码
+ */
+public interface BpmTaskAssignRuleService {
+
+    /**
+     * 获得流程定义的任务分配规则数组
+     *
+     * @param processDefinitionId 流程定义的编号
+     * @param taskDefinitionKey 流程任务定义的 Key。允许空
+     * @return 任务规则数组
+     */
+    List<BpmTaskAssignRuleDO> getTaskAssignRuleListByProcessDefinitionId(String processDefinitionId,
+                                                                         @Nullable String taskDefinitionKey);
+
+    /**
+     * 获得流程模型的任务规则数组
+     *
+     * @param modelId 流程模型的编号
+     * @return 任务规则数组
+     */
+    List<BpmTaskAssignRuleDO> getTaskAssignRuleListByModelId(String modelId);
+
+    /**
+     * 获得流程定义的任务分配规则数组
+     *
+     * @param modelId 流程模型的编号
+     * @param processDefinitionId 流程定义的编号
+     * @return 任务规则数组
+     */
+    List<BpmTaskAssignRuleRespVO> getTaskAssignRuleList(String modelId, String processDefinitionId);
+
+    /**
+     * 创建任务分配规则
+     *
+     * @param reqVO 创建信息
+     * @return 规则编号
+     */
+    Long createTaskAssignRule(@Valid BpmTaskAssignRuleCreateReqVO reqVO);
+
+    /**
+     * 更新任务分配规则
+     *
+     * @param reqVO 创建信息
+     */
+    void updateTaskAssignRule(@Valid BpmTaskAssignRuleUpdateReqVO reqVO);
+
+    /**
+     * 判断指定流程模型和流程定义的分配规则是否相等
+     *
+     * @param modelId 流程模型编号
+     * @param processDefinitionId 流程定义编号
+     * @return 是否相等
+     */
+    boolean isTaskAssignRulesEquals(String modelId, String processDefinitionId);
+
+    /**
+     * 将流程流程模型的任务分配规则,复制一份给流程定义
+     * 目的:每次流程模型部署时,都会生成一个新的流程定义,此时考虑到每次部署的流程不可变性,所以需要复制一份给该流程定义
+     *
+     * @param fromModelId 流程模型编号
+     * @param toProcessDefinitionId 流程定义编号
+     */
+    void copyTaskAssignRules(String fromModelId, String toProcessDefinitionId);
+
+    /**
+     * 校验流程模型的任务分配规则全部都配置了
+     * 目的:如果有规则未配置,会导致流程任务找不到负责人,进而流程无法进行下去!
+     *
+     * @param id 流程模型编号
+     */
+    void checkTaskAssignRuleAllConfig(String id);
+
+    /**
+     * 计算当前执行任务的处理人
+     *
+     * @param execution 执行任务
+     * @return 处理人的编号数组
+     */
+    Set<Long> calculateTaskCandidateUsers(DelegateExecution execution);
+
+}

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

@@ -0,0 +1,352 @@
+package cn.iocoder.yudao.module.bpm.service.task;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
+import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
+import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
+import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
+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.convert.definition.BpmTaskAssignRuleConvert;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmTaskAssignRuleDO;
+import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO;
+import cn.iocoder.yudao.module.bpm.enums.DictTypeConstants;
+import cn.iocoder.yudao.module.bpm.enums.definition.BpmTaskAssignRuleTypeEnum;
+import cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior.script.BpmTaskAssignScript;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
+import cn.iocoder.yudao.module.bpm.service.definition.BpmUserGroupService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.PostApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.dict.DictDataApi;
+import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
+import cn.iocoder.yudao.module.system.api.permission.RoleApi;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import com.google.common.annotations.VisibleForTesting;
+import lombok.extern.slf4j.Slf4j;
+import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.bpmn.model.UserTask;
+import org.flowable.common.engine.api.FlowableException;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import javax.validation.Valid;
+
+import org.springframework.validation.annotation.Validated;
+import cn.iocoder.yudao.framework.flowable.core.util.FlowableUtils;
+
+import java.util.*;
+
+import cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmTaskAssignRuleMapper;
+
+import static cn.hutool.core.text.CharSequenceUtil.format;
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+import static cn.iocoder.yudao.framework.common.util.json.JsonUtils.toJsonString;
+import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
+
+/**
+ * Bpm 任务规则 Service 实现类
+ *
+ * @author 芋道源码
+ */
+@Service
+@Validated
+@Slf4j
+public class BpmTaskAssignRuleServiceImpl implements BpmTaskAssignRuleService {
+
+    @Resource
+    private BpmTaskAssignRuleMapper taskRuleMapper;
+    @Resource
+    @Lazy // 解决循环依赖
+    private BpmModelService modelService;
+    @Resource
+    @Lazy // 解决循环依赖
+    private BpmProcessDefinitionService processDefinitionService;
+    @Resource
+    private BpmUserGroupService userGroupService;
+    @Resource
+    private RoleApi roleApi;
+    @Resource
+    private DeptApi deptApi;
+    @Resource
+    private PostApi postApi;
+    @Resource
+    private AdminUserApi adminUserApi;
+    @Resource
+    private DictDataApi dictDataApi;
+    @Resource
+    private PermissionApi permissionApi;
+    /**
+     * 任务分配脚本
+     */
+    private Map<Long, BpmTaskAssignScript> scriptMap = Collections.emptyMap();
+
+    @Resource
+    public void setScripts(List<BpmTaskAssignScript> scripts) {
+        this.scriptMap = convertMap(scripts, script -> script.getEnum().getId());
+    }
+
+    @Override
+    public List<BpmTaskAssignRuleDO> getTaskAssignRuleListByProcessDefinitionId(String processDefinitionId,
+                                                                                String taskDefinitionKey) {
+        return taskRuleMapper.selectListByProcessDefinitionId(processDefinitionId, taskDefinitionKey);
+    }
+
+    @Override
+    public List<BpmTaskAssignRuleDO> getTaskAssignRuleListByModelId(String modelId) {
+        return taskRuleMapper.selectListByModelId(modelId);
+    }
+
+    @Override
+    public List<BpmTaskAssignRuleRespVO> getTaskAssignRuleList(String modelId, String processDefinitionId) {
+        // 获得规则
+        List<BpmTaskAssignRuleDO> rules = Collections.emptyList();
+        BpmnModel model = null;
+        if (StrUtil.isNotEmpty(modelId)) {
+            rules = getTaskAssignRuleListByModelId(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 Long createTaskAssignRule(@Valid BpmTaskAssignRuleCreateReqVO reqVO) {
+        // 校验参数
+        validTaskAssignRuleOptions(reqVO.getType(), reqVO.getOptions());
+        // 校验是否已经配置
+        BpmTaskAssignRuleDO existRule =
+                taskRuleMapper.selectListByModelIdAndTaskDefinitionKey(reqVO.getModelId(), reqVO.getTaskDefinitionKey());
+        if (existRule != null) {
+            throw exception(TASK_ASSIGN_RULE_EXISTS, reqVO.getModelId(), reqVO.getTaskDefinitionKey());
+        }
+
+        // 存储
+        BpmTaskAssignRuleDO rule = BpmTaskAssignRuleConvert.INSTANCE.convert(reqVO)
+                .setProcessDefinitionId(BpmTaskAssignRuleDO.PROCESS_DEFINITION_ID_NULL); // 只有流程模型,才允许新建
+        taskRuleMapper.insert(rule);
+        return rule.getId();
+    }
+
+    @Override
+    public void updateTaskAssignRule(@Valid BpmTaskAssignRuleUpdateReqVO reqVO) {
+        // 校验参数
+        validTaskAssignRuleOptions(reqVO.getType(), reqVO.getOptions());
+        // 校验是否存在
+        BpmTaskAssignRuleDO existRule = taskRuleMapper.selectById(reqVO.getId());
+        if (existRule == null) {
+            throw exception(TASK_ASSIGN_RULE_NOT_EXISTS);
+        }
+        // 只允许修改流程模型的规则
+        if (!Objects.equals(BpmTaskAssignRuleDO.PROCESS_DEFINITION_ID_NULL, existRule.getProcessDefinitionId())) {
+            throw exception(TASK_UPDATE_FAIL_NOT_MODEL);
+        }
+
+        // 执行更新
+        taskRuleMapper.updateById(BpmTaskAssignRuleConvert.INSTANCE.convert(reqVO));
+    }
+
+    @Override
+    public boolean isTaskAssignRulesEquals(String modelId, String processDefinitionId) {
+        // 调用 VO 接口的原因是,过滤掉流程模型不需要的规则,保持和 copyTaskAssignRules 方法的一致性
+        List<BpmTaskAssignRuleRespVO> modelRules = getTaskAssignRuleList(modelId, null);
+        List<BpmTaskAssignRuleRespVO> processInstanceRules = getTaskAssignRuleList(null, processDefinitionId);
+        if (modelRules.size() != processInstanceRules.size()) {
+            return false;
+        }
+
+        // 遍历,匹配对应的规则
+        Map<String, BpmTaskAssignRuleRespVO> processInstanceRuleMap =
+                CollectionUtils.convertMap(processInstanceRules, BpmTaskAssignRuleRespVO::getTaskDefinitionKey);
+        for (BpmTaskAssignRuleRespVO modelRule : modelRules) {
+            BpmTaskAssignRuleRespVO processInstanceRule = processInstanceRuleMap.get(modelRule.getTaskDefinitionKey());
+            if (processInstanceRule == null) {
+                return false;
+            }
+            if (!ObjectUtil.equals(modelRule.getType(), processInstanceRule.getType()) || !ObjectUtil.equal(
+                    modelRule.getOptions(), processInstanceRule.getOptions())) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public void copyTaskAssignRules(String fromModelId, String toProcessDefinitionId) {
+        List<BpmTaskAssignRuleRespVO> rules = getTaskAssignRuleList(fromModelId, null);
+        if (CollUtil.isEmpty(rules)) {
+            return;
+        }
+        // 开始复制
+        List<BpmTaskAssignRuleDO> newRules = BpmTaskAssignRuleConvert.INSTANCE.convertList2(rules);
+        newRules.forEach(rule -> rule.setProcessDefinitionId(toProcessDefinitionId).setId(null).setCreateTime(null)
+                .setUpdateTime(null));
+        taskRuleMapper.insertBatch(newRules);
+    }
+
+    @Override
+    public void checkTaskAssignRuleAllConfig(String id) {
+        // 一个用户任务都没配置,所以无需配置规则
+        List<BpmTaskAssignRuleRespVO> taskAssignRules = getTaskAssignRuleList(id, null);
+        if (CollUtil.isEmpty(taskAssignRules)) {
+            return;
+        }
+        // 校验未配置规则的任务
+        taskAssignRules.forEach(rule -> {
+            if (CollUtil.isEmpty(rule.getOptions())) {
+                throw exception(MODEL_DEPLOY_FAIL_TASK_CANDIDATE_NOT_CONFIG, rule.getTaskDefinitionName());
+            }
+        });
+    }
+
+    private void validTaskAssignRuleOptions(Integer type, Set<Long> options) {
+        if (Objects.equals(type, BpmTaskAssignRuleTypeEnum.ROLE.getType())) {
+            roleApi.validRoleList(options);
+        } else if (ObjectUtils.equalsAny(type, BpmTaskAssignRuleTypeEnum.DEPT_MEMBER.getType(),
+                BpmTaskAssignRuleTypeEnum.DEPT_LEADER.getType())) {
+            deptApi.validateDeptList(options);
+        } else if (Objects.equals(type, BpmTaskAssignRuleTypeEnum.POST.getType())) {
+            postApi.validPostList(options);
+        } else if (Objects.equals(type, BpmTaskAssignRuleTypeEnum.USER.getType())) {
+            adminUserApi.validateUserList(options);
+        } else if (Objects.equals(type, BpmTaskAssignRuleTypeEnum.USER_GROUP.getType())) {
+            userGroupService.validUserGroups(options);
+        } else if (Objects.equals(type, BpmTaskAssignRuleTypeEnum.SCRIPT.getType())) {
+            dictDataApi.validateDictDataList(DictTypeConstants.TASK_ASSIGN_SCRIPT,
+                    CollectionUtils.convertSet(options, String::valueOf));
+        } else {
+            throw new IllegalArgumentException(format("未知的规则类型({})", type));
+        }
+    }
+
+    @Override
+    @DataPermission(enable = false) // 忽略数据权限,不然分配会存在问题
+    public Set<Long> calculateTaskCandidateUsers(DelegateExecution execution) {
+        BpmTaskAssignRuleDO rule = getTaskRule(execution);
+        return calculateTaskCandidateUsers(execution, rule);
+    }
+
+    @VisibleForTesting
+    BpmTaskAssignRuleDO getTaskRule(DelegateExecution execution) {
+        List<BpmTaskAssignRuleDO> taskRules = getTaskAssignRuleListByProcessDefinitionId(
+                execution.getProcessDefinitionId(), execution.getCurrentActivityId());
+        if (CollUtil.isEmpty(taskRules)) {
+            throw new FlowableException(format("流程任务({}/{}/{}) 找不到符合的任务规则",
+                    execution.getId(), execution.getProcessDefinitionId(), execution.getCurrentActivityId()));
+        }
+        if (taskRules.size() > 1) {
+            throw new FlowableException(format("流程任务({}/{}/{}) 找到过多任务规则({})",
+                    execution.getId(), execution.getProcessDefinitionId(), execution.getCurrentActivityId()));
+        }
+        return taskRules.get(0);
+    }
+
+    @VisibleForTesting
+    Set<Long> calculateTaskCandidateUsers(DelegateExecution execution, BpmTaskAssignRuleDO rule) {
+        Set<Long> assigneeUserIds = null;
+        if (Objects.equals(BpmTaskAssignRuleTypeEnum.ROLE.getType(), rule.getType())) {
+            assigneeUserIds = calculateTaskCandidateUsersByRole(rule);
+        } else if (Objects.equals(BpmTaskAssignRuleTypeEnum.DEPT_MEMBER.getType(), rule.getType())) {
+            assigneeUserIds = calculateTaskCandidateUsersByDeptMember(rule);
+        } else if (Objects.equals(BpmTaskAssignRuleTypeEnum.DEPT_LEADER.getType(), rule.getType())) {
+            assigneeUserIds = calculateTaskCandidateUsersByDeptLeader(rule);
+        } else if (Objects.equals(BpmTaskAssignRuleTypeEnum.POST.getType(), rule.getType())) {
+            assigneeUserIds = calculateTaskCandidateUsersByPost(rule);
+        } else if (Objects.equals(BpmTaskAssignRuleTypeEnum.USER.getType(), rule.getType())) {
+            assigneeUserIds = calculateTaskCandidateUsersByUser(rule);
+        } else if (Objects.equals(BpmTaskAssignRuleTypeEnum.USER_GROUP.getType(), rule.getType())) {
+            assigneeUserIds = calculateTaskCandidateUsersByUserGroup(rule);
+        } else if (Objects.equals(BpmTaskAssignRuleTypeEnum.SCRIPT.getType(), rule.getType())) {
+            assigneeUserIds = calculateTaskCandidateUsersByScript(execution, rule);
+        }
+
+        // 移除被禁用的用户
+        removeDisableUsers(assigneeUserIds);
+        // 如果候选人为空,抛出异常
+        if (CollUtil.isEmpty(assigneeUserIds)) {
+            log.error("[calculateTaskCandidateUsers][流程任务({}/{}/{}) 任务规则({}) 找不到候选人]", execution.getId(),
+                    execution.getProcessDefinitionId(), execution.getCurrentActivityId(), toJsonString(rule));
+            throw exception(TASK_CREATE_FAIL_NO_CANDIDATE_USER);
+        }
+        return assigneeUserIds;
+    }
+
+    private Set<Long> calculateTaskCandidateUsersByRole(BpmTaskAssignRuleDO rule) {
+        return permissionApi.getUserRoleIdListByRoleIds(rule.getOptions());
+    }
+
+    private Set<Long> calculateTaskCandidateUsersByDeptMember(BpmTaskAssignRuleDO rule) {
+        List<AdminUserRespDTO> users = adminUserApi.getUserListByDeptIds(rule.getOptions());
+        return convertSet(users, AdminUserRespDTO::getId);
+    }
+
+    private Set<Long> calculateTaskCandidateUsersByDeptLeader(BpmTaskAssignRuleDO rule) {
+        List<DeptRespDTO> depts = deptApi.getDeptList(rule.getOptions());
+        return convertSet(depts, DeptRespDTO::getLeaderUserId);
+    }
+
+    private Set<Long> calculateTaskCandidateUsersByPost(BpmTaskAssignRuleDO rule) {
+        List<AdminUserRespDTO> users = adminUserApi.getUsersByPostIds(rule.getOptions());
+        return convertSet(users, AdminUserRespDTO::getId);
+    }
+
+    private Set<Long> calculateTaskCandidateUsersByUser(BpmTaskAssignRuleDO rule) {
+        return rule.getOptions();
+    }
+
+    private Set<Long> calculateTaskCandidateUsersByUserGroup(BpmTaskAssignRuleDO rule) {
+        List<BpmUserGroupDO> userGroups = userGroupService.getUserGroupList(rule.getOptions());
+        Set<Long> userIds = new HashSet<>();
+        userGroups.forEach(group -> userIds.addAll(group.getMemberUserIds()));
+        return userIds;
+    }
+
+    private Set<Long> calculateTaskCandidateUsersByScript(DelegateExecution execution, BpmTaskAssignRuleDO rule) {
+        // 获得对应的脚本
+        List<BpmTaskAssignScript> scripts = new ArrayList<>(rule.getOptions().size());
+        rule.getOptions().forEach(id -> {
+            BpmTaskAssignScript script = scriptMap.get(id);
+            if (script == null) {
+                throw exception(TASK_ASSIGN_SCRIPT_NOT_EXISTS, id);
+            }
+            scripts.add(script);
+        });
+        // 逐个计算任务
+        Set<Long> userIds = new HashSet<>();
+        scripts.forEach(script -> CollUtil.addAll(userIds, script.calculateTaskCandidateUsers(execution)));
+        return userIds;
+    }
+
+    @VisibleForTesting
+    void removeDisableUsers(Set<Long> assigneeUserIds) {
+        if (CollUtil.isEmpty(assigneeUserIds)) {
+            return;
+        }
+        Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(assigneeUserIds);
+        assigneeUserIds.removeIf(id -> {
+            AdminUserRespDTO user = userMap.get(id);
+            return user == null || !CommonStatusEnum.ENABLE.getStatus().equals(user.getStatus());
+        });
+    }
+
+
+}

+ 12 - 0
yudao-module-bpm/yudao-module-bpm-biz/src/main/resources/mapper/task/BpmTaskAssignRuleMapper.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmTaskAssignRuleMapper">
+
+    <!--
+        一般情况下,尽可能使用 Mapper 进行 CRUD 增删改查即可。
+        无法满足的场景,例如说多表关联查询,才使用 XML 编写 SQL。
+        代码生成器暂时只生成 Mapper XML 文件本身,更多推荐 MybatisX 快速开发插件来生成查询。
+        文档可见:https://www.iocoder.cn/MyBatis/x-plugins/
+     -->
+
+</mapper>

+ 9 - 0
yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApi.java

@@ -86,4 +86,13 @@ public interface AdminUserApi {
      */
     void validateUserList(Collection<Long> ids);
 
+    /**
+     * 获得指定岗位的用户数组
+     *
+     * @param postIds 岗位数组
+     * @return 用户数组
+     */
+    List<AdminUserRespDTO> getUsersByPostIds(Collection<Long> postIds);
+
+
 }

+ 7 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/api/user/AdminUserApiImpl.java

@@ -4,6 +4,7 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjUtil;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
 import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import cn.iocoder.yudao.module.system.convert.user.UserConvert;
 import cn.iocoder.yudao.module.system.dal.dataobject.dept.DeptDO;
 import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO;
 import cn.iocoder.yudao.module.system.service.dept.DeptService;
@@ -88,4 +89,10 @@ public class AdminUserApiImpl implements AdminUserApi {
         userService.validateUserList(ids);
     }
 
+    @Override
+    public List<AdminUserRespDTO> getUsersByPostIds(Collection<Long> postIds) {
+        List<AdminUserDO> users = userService.getUserListByPostIds(postIds);
+        return UserConvert.INSTANCE.convertList4(users);
+    }
+
 }

+ 3 - 0
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/user/UserConvert.java

@@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.system.convert.user;
 import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
 import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
 import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
 import cn.iocoder.yudao.module.system.controller.admin.dept.vo.dept.DeptSimpleRespVO;
 import cn.iocoder.yudao.module.system.controller.admin.dept.vo.post.PostSimpleRespVO;
 import cn.iocoder.yudao.module.system.controller.admin.permission.vo.role.RoleSimpleRespVO;
@@ -55,4 +56,6 @@ public interface UserConvert {
         return userVO;
     }
 
+    List<AdminUserRespDTO> convertList4(List<AdminUserDO> users);
+
 }

+ 2 - 2
yudao-server/pom.xml

@@ -59,11 +59,11 @@
         </dependency>
 
         <!-- 微信公众号模块。默认注释,保证编译速度 -->
-        <dependency>
+        <!--<dependency>
             <groupId>cn.iocoder.boot</groupId>
             <artifactId>yudao-module-mp-biz</artifactId>
             <version>${revision}</version>
-        </dependency>
+        </dependency>-->
 
         <!-- 商城相关模块。默认注释,保证编译速度-->
         <dependency>

+ 53 - 0
yudao-ui/yudao-ui-admin-vue2/src/api/bpm/task/index.js

@@ -0,0 +1,53 @@
+import request from '@/utils/request'
+
+// 创建Bpm 任务规则
+export function createTaskAssignRule(data) {
+  return request({
+    url: '/bpm/task-assign-rule/create',
+    method: 'post',
+    data: data
+  })
+}
+
+// 更新Bpm 任务规则
+export function updateTaskAssignRule(data) {
+  return request({
+    url: '/bpm/task-assign-rule/update',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除Bpm 任务规则
+export function deleteTaskAssignRule(id) {
+  return request({
+    url: '/bpm/task-assign-rule/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 获得Bpm 任务规则
+export function getTaskAssignRule(id) {
+  return request({
+    url: '/bpm/task-assign-rule/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 获得Bpm 任务规则分页
+export function getTaskAssignRulePage(params) {
+  return request({
+    url: '/bpm/task-assign-rule/page',
+    method: 'get',
+    params
+  })
+}
+// 导出Bpm 任务规则 Excel
+export function exportTaskAssignRuleExcel(params) {
+  return request({
+    url: '/bpm/task-assign-rule/export-excel',
+    method: 'get',
+    params,
+    responseType: 'blob'
+  })
+}

+ 122 - 0
yudao-ui/yudao-ui-admin-vue2/src/views/bpm/task/TaskAssignRuleForm.vue

@@ -0,0 +1,122 @@
+<template>
+  <div class="app-container">
+    <!-- 对话框(添加 / 修改) -->
+    <el-dialog :title="dialogTitle" :visible.sync="dialogVisible" width="45%" v-dialogDrag append-to-body>
+      <el-form ref="formRef" :model="formData" :rules="formRules" v-loading="formLoading" label-width="100px">
+                    <el-form-item label="流程模型的编号" prop="modelId">
+                      <el-input v-model="formData.modelId" placeholder="请输入流程模型的编号" />
+                    </el-form-item>
+                    <el-form-item label="流程定义的编号" prop="processDefinitionId">
+                      <el-input v-model="formData.processDefinitionId" placeholder="请输入流程定义的编号" />
+                    </el-form-item>
+                    <el-form-item label="流程任务定义的key" prop="taskDefinitionKey">
+                      <el-input v-model="formData.taskDefinitionKey" placeholder="请输入流程任务定义的key" />
+                    </el-form-item>
+                    <el-form-item label="规则类型" prop="type">
+                      <el-select v-model="formData.type" placeholder="请选择规则类型">
+                            <el-option v-for="dict in this.getDictDatas(DICT_TYPE.BPM_TASK_CANDIDATE_STRATEGY)"
+                                       :key="dict.value" :label="dict.label" :value="parseInt(dict.value)" />
+                      </el-select>
+                    </el-form-item>
+                    <el-form-item label="规则值,JSON 数组" prop="options">
+                      <el-input v-model="formData.options" placeholder="请输入规则值,JSON 数组" />
+                    </el-form-item>
+      </el-form>
+              <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm" :disabled="formLoading">确 定</el-button>
+        <el-button @click="dialogVisible = false">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+  import * as TaskAssignRuleApi from '@/api/bpm/task';
+      export default {
+    name: "TaskAssignRuleForm",
+    components: {
+                    },
+    data() {
+      return {
+        // 弹出层标题
+        dialogTitle: "",
+        // 是否显示弹出层
+        dialogVisible: false,
+        // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
+        formLoading: false,
+        // 表单参数
+        formData: {
+                            id: undefined,
+                            modelId: undefined,
+                            processDefinitionId: undefined,
+                            taskDefinitionKey: undefined,
+                            type: undefined,
+                            options: undefined,
+        },
+        // 表单校验
+        formRules: {
+                        modelId: [{ required: true, message: '流程模型的编号不能为空', trigger: 'blur' }],
+                        processDefinitionId: [{ required: true, message: '流程定义的编号不能为空', trigger: 'blur' }],
+                        taskDefinitionKey: [{ required: true, message: '流程任务定义的key不能为空', trigger: 'blur' }],
+                        type: [{ required: true, message: '规则类型不能为空', trigger: 'change' }],
+                        options: [{ required: true, message: '规则值,JSON 数组不能为空', trigger: 'blur' }],
+        },
+                        };
+    },
+    methods: {
+      /** 打开弹窗 */
+     async open(id) {
+        this.dialogVisible = true;
+        this.reset();
+        // 修改时,设置数据
+        if (id) {
+          this.formLoading = true;
+          try {
+            const res = await TaskAssignRuleApi.getTaskAssignRule(id);
+            this.formData = res.data;
+            this.title = "修改Bpm 任务规则";
+          } finally {
+            this.formLoading = false;
+          }
+        }
+        this.title = "新增Bpm 任务规则";
+              },
+      /** 提交按钮 */
+      async submitForm() {
+        // 校验主表
+        await this.$refs["formRef"].validate();
+                  this.formLoading = true;
+        try {
+          const data = this.formData;
+                  // 修改的提交
+          if (data.id) {
+            await TaskAssignRuleApi.updateTaskAssignRule(data);
+            this.$modal.msgSuccess("修改成功");
+            this.dialogVisible = false;
+            this.$emit('success');
+            return;
+          }
+          // 添加的提交
+          await TaskAssignRuleApi.createTaskAssignRule(data);
+          this.$modal.msgSuccess("新增成功");
+          this.dialogVisible = false;
+          this.$emit('success');
+        } finally {
+          this.formLoading = false;
+        }
+      },
+                      /** 表单重置 */
+      reset() {
+        this.formData = {
+                            id: undefined,
+                            modelId: undefined,
+                            processDefinitionId: undefined,
+                            taskDefinitionKey: undefined,
+                            type: undefined,
+                            options: undefined,
+        };
+        this.resetForm("formRef");
+      }
+    }
+  };
+</script>

+ 171 - 0
yudao-ui/yudao-ui-admin-vue2/src/views/bpm/task/index.vue

@@ -0,0 +1,171 @@
+<template>
+  <div class="app-container">
+    <!-- 搜索工作栏 -->
+    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="流程模型的编号" prop="modelId">
+        <el-input v-model="queryParams.modelId" placeholder="请输入流程模型的编号" clearable @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="流程定义的编号" prop="processDefinitionId">
+        <el-input v-model="queryParams.processDefinitionId" placeholder="请输入流程定义的编号" clearable @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="流程任务定义的key" prop="taskDefinitionKey">
+        <el-input v-model="queryParams.taskDefinitionKey" placeholder="请输入流程任务定义的key" clearable @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="规则类型" prop="type">
+        <el-select v-model="queryParams.type" placeholder="请选择规则类型" clearable size="small">
+          <el-option v-for="dict in this.getDictDatas(DICT_TYPE.BPM_TASK_CANDIDATE_STRATEGY)"
+                       :key="dict.value" :label="dict.label" :value="dict.value"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="规则值,JSON 数组" prop="options">
+        <el-input v-model="queryParams.options" placeholder="请输入规则值,JSON 数组" clearable @keyup.enter.native="handleQuery"/>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker v-model="queryParams.createTime" style="width: 240px" value-format="yyyy-MM-dd HH:mm:ss" type="daterange"
+                        range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" :default-time="['00:00:00', '23:59:59']" />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 操作工具栏 -->
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button type="primary" plain icon="el-icon-plus" size="mini" @click="openForm(undefined)"
+                   v-hasPermi="['bpm:task-assign-rule:create']">新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button type="warning" plain icon="el-icon-download" size="mini" @click="handleExport" :loading="exportLoading"
+                   v-hasPermi="['bpm:task-assign-rule:export']">导出</el-button>
+      </el-col>
+              <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+            <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
+            <el-table-column label="编号" align="center" prop="id" />
+      <el-table-column label="流程模型的编号" align="center" prop="modelId" />
+      <el-table-column label="流程定义的编号" align="center" prop="processDefinitionId" />
+      <el-table-column label="流程任务定义的key" align="center" prop="taskDefinitionKey" />
+      <el-table-column label="规则类型" align="center" prop="type">
+        <template v-slot="scope">
+          <dict-tag :type="DICT_TYPE.BPM_TASK_CANDIDATE_STRATEGY" :value="scope.row.type" />
+        </template>
+      </el-table-column>
+      <el-table-column label="规则值,JSON 数组" align="center" prop="options" />
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180">
+        <template v-slot="scope">
+          <span>{{ parseTime(scope.row.createTime) }}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template v-slot="scope">
+          <el-button size="mini" type="text" icon="el-icon-edit" @click="openForm(scope.row.id)"
+                     v-hasPermi="['bpm:task-assign-rule:update']">修改</el-button>
+          <el-button size="mini" type="text" icon="el-icon-delete" @click="handleDelete(scope.row)"
+                     v-hasPermi="['bpm:task-assign-rule:delete']">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <!-- 分页组件 -->
+    <pagination v-show="total > 0" :total="total" :page.sync="queryParams.pageNo" :limit.sync="queryParams.pageSize"
+                @pagination="getList"/>
+    <!-- 对话框(添加 / 修改) -->
+    <TaskAssignRuleForm ref="formRef" @success="getList" />
+    </div>
+</template>
+
+<script>
+import * as TaskAssignRuleApi from '@/api/bpm/task';
+import TaskAssignRuleForm from './TaskAssignRuleForm.vue';
+export default {
+  name: "TaskAssignRule",
+  components: {
+          TaskAssignRuleForm
+  },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 显示搜索条件
+      showSearch: true,
+              // 总条数
+        total: 0,
+      // Bpm 任务规则列表
+      list: [],
+      // 是否展开,默认全部展开
+      isExpandAll: true,
+      // 重新渲染表格状态
+      refreshTable: true,
+      // 选中行
+      currentRow: {},
+      // 查询参数
+      queryParams: {
+                    pageNo: 1,
+            pageSize: 10,
+        modelId: null,
+        processDefinitionId: null,
+        taskDefinitionKey: null,
+        type: null,
+        options: null,
+        createTime: [],
+      },
+            };
+  },
+  created() {
+    this.getList();
+  },
+  methods: {
+    /** 查询列表 */
+    async getList() {
+      try {
+      this.loading = true;
+              const res = await TaskAssignRuleApi.getTaskAssignRulePage(this.queryParams);
+        this.list = res.data.list;
+        this.total = res.data.total;
+      } finally {
+        this.loading = false;
+      }
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNo = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    /** 添加/修改操作 */
+    openForm(id) {
+      this.$refs["formRef"].open(id);
+    },
+    /** 删除按钮操作 */
+    async handleDelete(row) {
+      const id = row.id;
+      await this.$modal.confirm('是否确认删除Bpm 任务规则编号为"' + id + '"的数据项?')
+      try {
+       await TaskAssignRuleApi.deleteTaskAssignRule(id);
+       await this.getList();
+       this.$modal.msgSuccess("删除成功");
+      } catch {}
+    },
+    /** 导出按钮操作 */
+    async handleExport() {
+      await this.$modal.confirm('是否确认导出所有Bpm 任务规则数据项?');
+      try {
+        this.exportLoading = true;
+        const res = await TaskAssignRuleApi.exportTaskAssignRuleExcel(this.queryParams);
+        this.$download.excel(res, 'Bpm 任务规则.xls');
+      } catch {
+      } finally {
+        this.exportLoading = false;
+      }
+    },
+              }
+};
+</script>