Procházet zdrojové kódy

!491 合并flowable工作流功能
* update 优化 表字段映射于数据库保持一致
* remove 删除无用代码
* remove 删除无用代码
* fix 修复 实体类未实现序列化接口问题
* update 优化 表字段映射于数据库保持一致
* update 优化 统一sql名称
* fix 修复 接口名称编写错误
* merge dev
* update 调整sql 添加抄送查询
* update 调整菜单
* update 调整sql脚本
* update 调整任务查询 添加抄送
* add 抄送任务
* remove 删除错误代码
* remove 删除无用代码
* update 调整作废,撤销等校验
* fix 修复 流程作废异常问题
* update 优化 flowable 配置到主yml文件
* update 调整 数据排序规则
* fix 修复 数据库无法自动执行建表sql问题
* update 优化 工作流id生成器保持全局统一
* add 添加附件任务查询
* add 添加审批附件上传
* update 调整bpmn文件修复驳回失败问题
* update 调整会签类型转换异常
* add 添加获取运行中流程信息,流程扩展信息,补充注解,删除无用代码
* update 调整流程转换,流程启动,上传新bpmn文件
* update 调整方法
* update 调整模型修改
* fix 修复 user与dept xml 编写错误
* remove 移除原生ui接口,增加新ui接口
* update 优化 下拉选接口数据权限
* update 优化 删除观测用日志记录
* reset 还原修复命名
* update 修复命名
* add 新增 用户、部门、角色、岗位 下拉选接口与代码实现优化
* update 调整任务办理异步时流程状态错误问题
* add 工作流用户查询
* remove 删除无用注释 添加非空校验
* update 优化获取审批记录
* update 调整事件办理
* update 调整工作流选人接口
* Merge branch 'dev' into future/flowable
* update 办理调整执行顺序
* update 调整流程办理优化撤销,驳回,草稿等动作
* fix 修复子流程中设置发起人变量错误问题
* Merge branch 'dev' into future/flowable
* update 调整流程执行非空校验,调整任务节点执行
* update 调整注释
* add 添加自定义任务监听策略
* !469 update-完善对模型key校验逻辑
* update-修改常量命名
* update-完善对模型key校验逻辑
* add 添加sql脚本
* Merge branch 'dev' into future/flowable
* Merge branch 'dev' into future/flowable
* Merge branch 'future/flowable' of https://gitee.com/dromara/RuoYi-Vue-…
* add 添加SQLserve脚本
* add 添加流程监听示例
* update 调整获取审批记录
* Merge branch 'future/flowable' of https://gitee.com/dromara/RuoYi-Vue-…
* update 调整请假查询 修改流程定义查看xml
* update 调整流程实例删除
* update 调整sql
* add 添加sql脚本
* update 调整sql
* update 调整请假申请,调整菜单sql
* update 调整设计器保存发起人变量,修改菜单sql
* update 依赖调整
* update 调整flw依赖
* update 升级7.0后移除画图mule类型
* update 调整flw依赖
* update 移动模型设计器翻译方法
* update 调整flw依赖
* fix 修复 误删依赖
* Merge remote-tracking branch 'origin/dev' into future/flowable
* remove 移除动态表单
* Merge remote-tracking branch 'origin/dev' into future/flowable
* update 优化代码结构
* update 调整请假申请包结构
* Merge branch 'dev' into future/flowable
* add 添加文件,调整分类查询
* Merge branch 'dev' into future/flowable
* Merge branch 'future/flowable' of https://gitee.com/dromara/RuoYi-Vue-…
* add bpmn文件 调整流程办理
* Merge branch 'dev' into future/flowable
* Merge branch 'future/flowable' of https://gitee.com/dromara/RuoYi-Vue-…
* Merge branch '5.X' into future/flowable
* update 调整消息发送
* update 调整名称
* update 调整流程实例查询
* add 添加任务催办,任务改派
* fix 修复 用户注册接口校验用户名不区分租户问题
* update 还原待办任务,添加待办消息发送
* update 优化任务待办,排除非待办任务
* Merge branch '5.X' into future/flowable
* update 修改流程启动后重新覆盖流程变量,删除并行流程驳回,撤销后,垃圾数据
* update 升级flowable7.0,添加业务单据删除流程信息
* Merge branch '5.X' into future/flowable
* add 添加动态表单提交流程
* Merge branch 'future/flowable' of https://gitee.com/dromara/RuoYi-Vue-…
* add 添加动态表单单据
* update 升级flowable到7.0.0.M2,调整工作流提交校验,调整工作流工具类
* add 新增流程定义与表单关联
* update 调整修改流程分类后更新流程分类编码
* update 调整流程定义图片预览
* update 调整人员查询
* update 优化作废,撤销等备注
* Merge branch '5.X' into future/flowable
* Merge branch 'future/flowable' of https://gitee.com/dromara/RuoYi-Vue-…
* fix 解决设计器选择设置流程发起人设置变量有问题
* add 添加引擎调度监听
* merge 合并5.x分支代码
* remove 移除flow-ui
* update 调整日志打印
* add 添加按照业务id删除流程记录
* add 添加请假申请示例,添加流程定义文件部署,添加sql菜单
* update 移除流程表单 formConfig 属性,表单配置信息都放一起便于使用。
* update 调整菜单
* add 添加mysql工作流菜单
* update 调整获取加签人,审判记录
* update 调整流程作废
* add 添加任务完成状态
* add 添加加签,减签人员接口
* update 调整任务驳回后设置审批人
* add 添加驳回申请人
* add 添加查询当前租户所有待办,已办任务
* add 添加会签任务加签减签,添加任务作废理由
* update 调整流程实例,流程定义检索
* update 调整撤销流程申请,当前登录人单据
* add 添加办理人名称翻译
* add 添加流程流程实例,流程定义分类查询
* add 添加模型分类查询
* add 添加流程分类
* add 添加流程表单操作相关接口
* fix 修复修改流程历史流程实例错误问题
* update 调整已办任务排序,添加注释
* update 调整用户,用户组查询
* add 添加获取当前任务参与者,优化任务待办,已办
* add 添加当前登录人单据列表,添加单据状态
* update 补充任务撤销事务
* add 添加撤销流程申请
* update 优化流程实例删除
* fix 修复流程实例查询挂起状态错误
* update 优化流程办理 流程挂起抛出异常
* add 添加业务状态枚举。添加流程启动,审批,终止等状态
* update 优化流程启动
* add 添加流程实例作废,运行中流程实例删除,已完成流程实例删除
* add 添加节点信息
* 调整流程预览
* add 添加审批记录
* 还原代码
* fix 修复模型导出错误
* add 增加委托办理,调整流程启动
* add 添加转办任务
* add 添加任务拾取,任务归还,任务终止,任务委托
* fix 修复任务,流程实例分页模糊查询失效
* add 添加流程实例运行中,已结束分页查询
* add 添加通过流程实例id获取历史流程图,添加flowable配置,调整流程办理
* add 添加流程办理,流程待办,已办分页查询
* 删除无用导入
* 调整流程查询租户id
* add 添加流程启动
* 添加模型人员用户,组查询
* add 添加模型部署模型校验
* 修改模型部署导出校验
* fix 修复模型画图保存时key不回显问题
* add 添加流程定义转换为模型
* 优化模型编辑校验,流程定义删除,流程定义激活挂起等
* add 添加流程定义删除,流程定义挂起激活,流程定义版本迁移
* 调整ObjectNode.put警告
* 删除无用依赖,优化模型修改,导出,部署非空校验
* 删除无用导入
* 添加流程定义分页,查看图片,查看xml
* 添加模型部署,导出模型
* 修改画图账户登录信息
* 添加模型编辑key重复校验,添加租户查询,删除忽略token注解
* 添加模型新增校验
* 添加工作流模型新增,修改,查询,删除
* 【ADD】集成原生Flowable-ui
* 添加workflow模块,添加flowable依赖,yml配置信息

疯狂的狮子Li před 2 roky
rodič
revize
d54772815b
100 změnil soubory, kde provedl 9320 přidání a 4 odebrání
  1. 18 0
      pom.xml
  2. 6 0
      ruoyi-admin/pom.xml
  3. 2 2
      ruoyi-admin/src/main/resources/application-dev.yml
  4. 2 2
      ruoyi-admin/src/main/resources/application-prod.yml
  5. 18 0
      ruoyi-admin/src/main/resources/application.yml
  6. 1 0
      ruoyi-modules/pom.xml
  7. 93 0
      ruoyi-modules/ruoyi-workflow/pom.xml
  8. 27 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/annotation/FlowListenerAnnotation.java
  9. 28 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/PageEntity.java
  10. 92 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/constant/FlowConstant.java
  11. 93 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/enums/BusinessStatusEnum.java
  12. 31 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/enums/MessageTypeEnum.java
  13. 90 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/enums/TaskStatusEnum.java
  14. 135 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/ActModelController.java
  15. 146 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/ActProcessDefinitionController.java
  16. 157 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/ActProcessInstanceController.java
  17. 246 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/ActTaskController.java
  18. 107 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/TestLeaveController.java
  19. 106 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/WfCategoryController.java
  20. 72 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/WorkflowUserController.java
  21. 152 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/ActHiProcinst.java
  22. 193 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/ActHiTaskinst.java
  23. 58 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/TestLeave.java
  24. 52 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/WfCategory.java
  25. 40 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/AddMultiBo.java
  26. 43 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/BackProcessBo.java
  27. 65 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/CompleteTaskBo.java
  28. 38 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/DelegateBo.java
  29. 52 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/DeleteMultiBo.java
  30. 69 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/ModelBo.java
  31. 37 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/ProcessDefinitionBo.java
  32. 46 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/ProcessInstanceBo.java
  33. 31 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/ProcessInvalidBo.java
  34. 45 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/StartProcessBo.java
  35. 34 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/SysUserMultiBo.java
  36. 36 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/TaskBo.java
  37. 34 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/TaskUrgingBo.java
  38. 37 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/TerminationBo.java
  39. 75 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/TestLeaveBo.java
  40. 37 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/TransmitBo.java
  41. 54 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/WfCategoryBo.java
  42. 89 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/ActHistoryInfoVo.java
  43. 47 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/GraphicInfoVo.java
  44. 27 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/GroupRepresentation.java
  45. 48 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/ModelVo.java
  46. 33 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/MultiInstanceVo.java
  47. 43 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/ParticipantVo.java
  48. 65 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/ProcessDefinitionVo.java
  49. 95 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/ProcessInstanceVo.java
  50. 143 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/TaskVo.java
  51. 70 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/TestLeaveVo.java
  52. 58 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/WfCategoryVo.java
  53. 23 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/WfCopy.java
  54. 108 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/CustomDefaultProcessDiagramCanvas.java
  55. 1120 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/CustomDefaultProcessDiagramGenerator.java
  56. 61 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/cmd/AddSequenceMultiInstanceCmd.java
  57. 68 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/cmd/AttachmentCmd.java
  58. 36 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/cmd/DeleteExecutionCmd.java
  59. 82 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/cmd/DeleteSequenceMultiInstanceCmd.java
  60. 39 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/cmd/ExecutionChildByExecutionIdCmd.java
  61. 37 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/cmd/UpdateBusinessStatusCmd.java
  62. 51 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/cmd/UpdateHiTaskInstCmd.java
  63. 32 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/config/FlowableConfig.java
  64. 91 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/config/GlobalFlowableListener.java
  65. 73 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/strategy/FlowEventStrategy.java
  66. 20 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/strategy/FlowProcessEventHandler.java
  67. 20 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/strategy/FlowTaskEventHandler.java
  68. 24 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/listener/TestCustomProcessHandler.java
  69. 24 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/listener/TestCustomTaskHandler.java
  70. 29 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/listener/TestLeaveExecutionListener.java
  71. 21 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/listener/TestLeaveTaskListener.java
  72. 16 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/ActHiProcinstMapper.java
  73. 16 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/ActHiTaskinstMapper.java
  74. 38 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/ActTaskMapper.java
  75. 15 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/TestLeaveMapper.java
  76. 15 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/WfCategoryMapper.java
  77. 31 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IActHiProcinstService.java
  78. 11 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IActHiTaskinstService.java
  79. 71 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IActModelService.java
  80. 90 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IActProcessDefinitionService.java
  81. 113 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IActProcessInstanceService.java
  82. 129 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IActTaskService.java
  83. 49 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/ITestLeaveService.java
  84. 51 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IWfCategoryService.java
  85. 60 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IWorkflowUserService.java
  86. 51 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/ActHiProcinstServiceImpl.java
  87. 18 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/ActHiTaskinstServiceImpl.java
  88. 334 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/ActModelServiceImpl.java
  89. 328 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/ActProcessDefinitionServiceImpl.java
  90. 695 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/ActProcessInstanceServiceImpl.java
  91. 708 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/ActTaskServiceImpl.java
  92. 124 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/TestLeaveServiceImpl.java
  93. 128 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/WfCategoryServiceImpl.java
  94. 216 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/WorkflowUserServiceImpl.java
  95. 220 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/utils/ModelUtils.java
  96. 341 0
      ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/utils/WorkflowUtils.java
  97. 3 0
      ruoyi-modules/ruoyi-workflow/src/main/resources/mapper/package-info.md
  98. 7 0
      ruoyi-modules/ruoyi-workflow/src/main/resources/mapper/workflow/ActHiProcinstMapper.xml
  99. 7 0
      ruoyi-modules/ruoyi-workflow/src/main/resources/mapper/workflow/ActHiTaskinstMapper.xml
  100. 60 0
      ruoyi-modules/ruoyi-workflow/src/main/resources/mapper/workflow/ActTaskMapper.xml

+ 18 - 0
pom.xml

@@ -57,6 +57,9 @@
         <maven-compiler-plugin.verison>3.11.0</maven-compiler-plugin.verison>
         <maven-surefire-plugin.version>3.1.2</maven-surefire-plugin.version>
         <flatten-maven-plugin.version>1.3.0</flatten-maven-plugin.version>
+
+        <!--工作流配置-->
+        <flowable.version>7.0.0</flowable.version>
     </properties>
 
     <profiles>
@@ -111,6 +114,14 @@
                 <scope>import</scope>
             </dependency>
 
+            <dependency>
+                <groupId>org.flowable</groupId>
+                <artifactId>flowable-bom</artifactId>
+                <version>${flowable.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+
             <!-- JustAuth 的依赖配置-->
             <dependency>
                 <groupId>me.zhyd.oauth</groupId>
@@ -353,6 +364,13 @@
                 <version>${revision}</version>
             </dependency>
 
+            <!--  工作流模块  -->
+            <dependency>
+                <groupId>org.dromara</groupId>
+                <artifactId>ruoyi-workflow</artifactId>
+                <version>${revision}</version>
+            </dependency>
+
         </dependencies>
     </dependencyManagement>
 

+ 6 - 0
ruoyi-admin/pom.xml

@@ -75,6 +75,12 @@
             <artifactId>ruoyi-demo</artifactId>
         </dependency>
 
+        <!--  工作流模块  -->
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-workflow</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>de.codecentric</groupId>
             <artifactId>spring-boot-admin-starter-client</artifactId>

+ 2 - 2
ruoyi-admin/src/main/resources/application-dev.yml

@@ -43,7 +43,7 @@ spring:
           driverClassName: com.mysql.cj.jdbc.Driver
           # jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
           # rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
-          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
+          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
           username: root
           password: root
         # 从库数据源
@@ -51,7 +51,7 @@ spring:
           lazy: true
           type: ${spring.datasource.type}
           driverClassName: com.mysql.cj.jdbc.Driver
-          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
+          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
           username:
           password:
 #        oracle:

+ 2 - 2
ruoyi-admin/src/main/resources/application-prod.yml

@@ -46,7 +46,7 @@ spring:
           driverClassName: com.mysql.cj.jdbc.Driver
           # jdbc 所有参数配置参考 https://lionli.blog.csdn.net/article/details/122018562
           # rewriteBatchedStatements=true 批处理优化 大幅提升批量插入更新删除性能(对数据库有性能损耗 使用批量操作应考虑性能问题)
-          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
+          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
           username: root
           password: root
         # 从库数据源
@@ -54,7 +54,7 @@ spring:
           lazy: true
           type: ${spring.datasource.type}
           driverClassName: com.mysql.cj.jdbc.Driver
-          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
+          url: jdbc:mysql://localhost:3306/ry-vue?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8&autoReconnect=true&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true&nullCatalogMeansCurrent=true
           username:
           password:
 #        oracle:

+ 18 - 0
ruoyi-admin/src/main/resources/application.yml

@@ -266,3 +266,21 @@ websocket:
   path: /resource/websocket
   # 设置访问源地址
   allowedOrigins: '*'
+
+--- #flowable配置
+flowable:
+  async-executor-activate: false #关闭定时任务JOB
+  #  将databaseSchemaUpdate设置为true。当Flowable发现库与数据库表结构不一致时,会自动将数据库表结构升级至新版本。
+  database-schema-update: true
+  activity-font-name: 宋体
+  label-font-name: 宋体
+  annotation-font-name: 宋体
+  # 关闭各个模块生成表,目前只使用工作流基础表
+  idm:
+    enabled: false
+  cmmn:
+    enabled: false
+  dmn:
+    enabled: false
+  app:
+    enabled: false

+ 1 - 0
ruoyi-modules/pom.xml

@@ -14,6 +14,7 @@
         <module>ruoyi-generator</module>
         <module>ruoyi-job</module>
         <module>ruoyi-system</module>
+        <module>ruoyi-workflow</module>
     </modules>
 
     <artifactId>ruoyi-modules</artifactId>

+ 93 - 0
ruoyi-modules/ruoyi-workflow/pom.xml

@@ -0,0 +1,93 @@
+<?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>
+        <groupId>org.dromara</groupId>
+        <artifactId>ruoyi-modules</artifactId>
+        <version>${revision}</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <packaging>jar</packaging>
+    <artifactId>ruoyi-workflow</artifactId>
+
+    <description>
+        工作流模块
+    </description>
+
+    <dependencies>
+
+        <!--引入flowable依赖-->
+        <dependency>
+            <groupId>org.flowable</groupId>
+            <artifactId>flowable-spring-boot-autoconfigure</artifactId>
+            <exclusions>
+                <exclusion>
+                    <groupId>org.flowable</groupId>
+                    <artifactId>flowable-spring-security</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <dependency>
+            <groupId>org.flowable</groupId>
+            <artifactId>flowable-spring-configurator</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.flowable</groupId>
+            <artifactId>flowable-spring-boot-starter-actuator</artifactId>
+        </dependency>
+
+        <!-- 绘制flowable流程图 -->
+        <dependency>
+            <groupId>org.flowable</groupId>
+            <artifactId>flowable-image-generator</artifactId>
+        </dependency>
+
+        <!-- flowable json 转换 -->
+        <dependency>
+            <groupId>org.flowable</groupId>
+            <artifactId>flowable-json-converter</artifactId>
+            <version>6.8.0</version>
+        </dependency>
+
+        <!-- svg转png图片工具-->
+        <dependency>
+            <groupId>org.apache.xmlgraphics</groupId>
+            <artifactId>batik-all</artifactId>
+            <version>1.10</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>xalan</groupId>
+                    <artifactId>xalan</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+
+        <!--系统模块-->
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-system</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-websocket</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-mail</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.dromara</groupId>
+            <artifactId>ruoyi-common-sms</artifactId>
+        </dependency>
+
+    </dependencies>
+
+</project>
+

+ 27 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/annotation/FlowListenerAnnotation.java

@@ -0,0 +1,27 @@
+package org.dromara.workflow.annotation;
+
+
+import java.lang.annotation.*;
+
+/**
+ * 流程任务监听注解
+ *
+ * @author may
+ * @date 2023-12-27
+ */
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+@Inherited
+public @interface FlowListenerAnnotation {
+
+    /**
+     * 流程定义key
+     */
+    String processDefinitionKey();
+
+    /**
+     * 节点id
+     */
+    String taskDefId() default "";
+}

+ 28 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/PageEntity.java

@@ -0,0 +1,28 @@
+package org.dromara.workflow.common;
+
+
+import lombok.Data;
+
+/**
+ * 分页参数
+ *
+ * @author may
+ */
+@Data
+public class PageEntity {
+
+    /**
+     * 当前页码
+     */
+    private Integer pageNum = 0;
+
+    /**
+     * 页容量
+     */
+    private Integer pageSize = 10;
+
+    public Integer getPageNum() {
+        return (pageNum - 1) * pageSize;
+    }
+
+}

+ 92 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/constant/FlowConstant.java

@@ -0,0 +1,92 @@
+package org.dromara.workflow.common.constant;
+
+
+/**
+ * 工作流常量
+ *
+ * @author may
+ */
+public interface FlowConstant {
+
+    String MESSAGE_CURRENT_TASK_IS_NULL = "当前任务不存在或你不是任务办理人!";
+
+    String MESSAGE_SUSPENDED = "当前任务已挂起不可审批!";
+
+    /**
+     * 连线
+     */
+    String SEQUENCE_FLOW = "sequenceFlow";
+
+    /**
+     * 并行网关
+     */
+    String PARALLEL_GATEWAY = "parallelGateway";
+
+    /**
+     * 排它网关
+     */
+    String EXCLUSIVE_GATEWAY = "exclusiveGateway";
+
+    /**
+     * 包含网关
+     */
+    String INCLUSIVE_GATEWAY = "inclusiveGateway";
+
+    /**
+     * 结束节点
+     */
+    String END_EVENT = "endEvent";
+
+
+    /**
+     * 流程委派标识
+     */
+    String PENDING = "PENDING";
+
+    /**
+     * 候选人标识
+     */
+    String CANDIDATE = "candidate";
+
+    /**
+     * 会签任务总数
+     */
+    String NUMBER_OF_INSTANCES = "nrOfInstances";
+
+    /**
+     * 正在执行的会签总数
+     */
+    String NUMBER_OF_ACTIVE_INSTANCES = "nrOfActiveInstances";
+
+    /**
+     * 已完成的会签任务总数
+     */
+    String NUMBER_OF_COMPLETED_INSTANCES = "nrOfCompletedInstances";
+
+    /**
+     * 循环的索引值,可以使用elementIndexVariable属性修改loopCounter的变量名
+     */
+    String LOOP_COUNTER = "loopCounter";
+
+    String ZIP = "ZIP";
+
+    /**
+     * 流程实例对象
+     */
+    String PROCESS_INSTANCE_VO = "processInstanceVo";
+
+    /**
+     * 流程发起人
+     */
+    String INITIATOR = "initiator";
+
+    /**
+     * 开启跳过表达式变量
+     */
+    String FLOWABLE_SKIP_EXPRESSION_ENABLED = "_FLOWABLE_SKIP_EXPRESSION_ENABLED";
+
+    /**
+     * 模型标识key命名规范正则表达式
+     */
+    String MODEL_KEY_PATTERN = "^[a-zA-Z][a-zA-Z0-9_]{0,254}$";
+}

+ 93 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/enums/BusinessStatusEnum.java

@@ -0,0 +1,93 @@
+package org.dromara.workflow.common.enums;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.StringUtils;
+
+import java.util.Arrays;
+
+/**
+ * 业务状态枚举
+ *
+ * @author may
+ */
+@Getter
+@AllArgsConstructor
+public enum BusinessStatusEnum {
+    /**
+     * 已撤销
+     */
+    CANCEL("cancel", "已撤销"),
+    /**
+     * 草稿
+     */
+    DRAFT("draft", "草稿"),
+    /**
+     * 待审核
+     */
+    WAITING("waiting", "待审核"),
+    /**
+     * 已完成
+     */
+    FINISH("finish", "已完成"),
+    /**
+     * 已作废
+     */
+    INVALID("invalid", "已作废"),
+    /**
+     * 已退回
+     */
+    BACK("back", "已退回"),
+    /**
+     * 已终止
+     */
+    TERMINATION("termination", "已终止");
+
+    /**
+     * 状态
+     */
+    private final String status;
+
+    /**
+     * 描述
+     */
+    private final String desc;
+
+    /**
+     * 获取业务状态
+     *
+     * @param status 状态
+     */
+    public static String findByStatus(String status) {
+        if (StringUtils.isBlank(status)) {
+            return StrUtil.EMPTY;
+        }
+        return Arrays.stream(BusinessStatusEnum.values())
+            .filter(statusEnum -> statusEnum.getStatus().equals(status))
+            .findFirst()
+            .map(BusinessStatusEnum::getDesc)
+            .orElse(StrUtil.EMPTY);
+    }
+
+    /**
+     * 启动流程校验
+     *
+     * @param status 状态
+     */
+    public static void checkStartStatus(String status) {
+        if (WAITING.getStatus().equals(status)) {
+            throw new ServiceException("该单据已提交过申请,正在审批中!");
+        } else if (FINISH.getStatus().equals(status)) {
+            throw new ServiceException("该单据已完成申请!");
+        } else if (INVALID.getStatus().equals(status)) {
+            throw new ServiceException("该单据已作废!");
+        } else if (TERMINATION.getStatus().equals(status)) {
+            throw new ServiceException("该单据已终止!");
+        } else if (StringUtils.isBlank(status)) {
+            throw new ServiceException("流程状态为空!");
+        }
+    }
+}
+

+ 31 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/enums/MessageTypeEnum.java

@@ -0,0 +1,31 @@
+package org.dromara.workflow.common.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+/**
+ * 消息类型枚举
+ *
+ * @author may
+ */
+@Getter
+@AllArgsConstructor
+public enum MessageTypeEnum {
+    /**
+     * 站内信
+     */
+    SYSTEM_MESSAGE("1", "站内信"),
+    /**
+     * 邮箱
+     */
+    EMAIL_MESSAGE("2", "邮箱"),
+    /**
+     * 短信
+     */
+    SMS_MESSAGE("3", "短信");
+
+    private final String code;
+
+    private final String desc;
+}
+

+ 90 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/common/enums/TaskStatusEnum.java

@@ -0,0 +1,90 @@
+package org.dromara.workflow.common.enums;
+
+import cn.hutool.core.util.StrUtil;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import org.apache.commons.lang3.StringUtils;
+
+import java.util.Arrays;
+
+/**
+ * 任务状态枚举
+ *
+ * @author may
+ */
+@Getter
+@AllArgsConstructor
+public enum TaskStatusEnum {
+    /**
+     * 撤销
+     */
+    CANCEL("cancel", "撤销"),
+    /**
+     * 通过
+     */
+    PASS("pass", "通过"),
+    /**
+     * 待审核
+     */
+    WAITING("waiting", "待审核"),
+    /**
+     * 作废
+     */
+    INVALID("invalid", "作废"),
+    /**
+     * 退回
+     */
+    BACK("back", "退回"),
+    /**
+     * 终止
+     */
+    TERMINATION("termination", "终止"),
+    /**
+     * 转办
+     */
+    TRANSFER("transfer", "转办"),
+    /**
+     * 委托
+     */
+    PENDING("pending", "委托"),
+    /**
+     * 抄送
+     */
+    COPY("copy", "抄送"),
+    /**
+     * 加签
+     */
+    SIGN("sign", "加签"),
+    /**
+     * 减签
+     */
+    SIGN_OFF("sign_off", "减签");
+
+    /**
+     * 状态
+     */
+    private final String status;
+
+    /**
+     * 描述
+     */
+    private final String desc;
+
+    /**
+     * 任务业务状态
+     *
+     * @param status 状态
+     */
+    public static String findByStatus(String status) {
+        if (StringUtils.isBlank(status)) {
+            return StrUtil.EMPTY;
+        }
+
+        return Arrays.stream(TaskStatusEnum.values())
+            .filter(statusEnum -> statusEnum.getStatus().equals(status))
+            .findFirst()
+            .map(TaskStatusEnum::getDesc)
+            .orElse(StrUtil.EMPTY);
+    }
+}
+

+ 135 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/ActModelController.java

@@ -0,0 +1,135 @@
+package org.dromara.workflow.controller;
+
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.workflow.domain.bo.ModelBo;
+import org.dromara.workflow.domain.vo.ModelVo;
+import org.dromara.workflow.service.IActModelService;
+import org.flowable.engine.RepositoryService;
+import org.flowable.engine.repository.Model;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Arrays;
+
+/**
+ * 模型管理 控制层
+ *
+ * @author may
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/workflow/model")
+public class ActModelController extends BaseController {
+
+    private final RepositoryService repositoryService;
+
+    private final IActModelService actModelService;
+
+
+    /**
+     * 分页查询模型
+     *
+     * @param modelBo 模型参数
+     */
+    @GetMapping("/list")
+    public TableDataInfo<Model> page(ModelBo modelBo) {
+        return actModelService.page(modelBo);
+    }
+
+    /**
+     * 新增模型
+     *
+     * @param modelBo 模型请求对象
+     */
+    @Log(title = "模型管理", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping("/save")
+    public R<Void> saveNewModel(@Validated(AddGroup.class) @RequestBody ModelBo modelBo) {
+        return toAjax(actModelService.saveNewModel(modelBo));
+    }
+
+    /**
+     * 查询模型
+     *
+     * @param id 模型id
+     */
+    @GetMapping("/getInfo/{id}")
+    public R<ModelVo> getInfo(@NotBlank(message = "模型id不能为空") @PathVariable String id) {
+        return R.ok(actModelService.getInfo(id));
+    }
+
+    /**
+     * 修改模型信息
+     *
+     * @param modelBo 模型数据
+     */
+    @Log(title = "模型管理", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping(value = "/update")
+    public R<Void> update(@RequestBody ModelBo modelBo) {
+        return toAjax(actModelService.update(modelBo));
+    }
+
+    /**
+     * 编辑XMl模型
+     *
+     * @param modelBo 模型数据
+     */
+    @Log(title = "模型管理", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping(value = "/editModelXml")
+    public R<Void> editModel(@Validated(EditGroup.class) @RequestBody ModelBo modelBo) {
+        return toAjax(actModelService.editModelXml(modelBo));
+    }
+
+    /**
+     * 删除流程模型
+     *
+     * @param ids 模型id
+     */
+    @Log(title = "模型管理", businessType = BusinessType.DELETE)
+    @RepeatSubmit()
+    @DeleteMapping("/{ids}")
+    @Transactional(rollbackFor = Exception.class)
+    public R<Void> delete(@NotEmpty(message = "主键不能为空") @PathVariable String[] ids) {
+        Arrays.stream(ids).parallel().forEachOrdered(repositoryService::deleteModel);
+        return R.ok();
+    }
+
+    /**
+     * 模型部署
+     *
+     * @param id 模型id
+     */
+    @Log(title = "模型管理", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping("/modelDeploy/{id}")
+    public R<Void> deploy(@NotBlank(message = "模型id不能为空") @PathVariable("id") String id) {
+        return toAjax(actModelService.modelDeploy(id));
+    }
+
+    /**
+     * 导出模型zip压缩包
+     *
+     * @param modelId  模型id
+     * @param response 相应
+     */
+    @GetMapping("/export/zip/{modelId}")
+    public void exportZip(@NotEmpty(message = "模型id不能为空") @PathVariable String modelId,
+                          HttpServletResponse response) {
+        actModelService.exportZip(modelId, response);
+    }
+}

+ 146 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/ActProcessDefinitionController.java

@@ -0,0 +1,146 @@
+package org.dromara.workflow.controller;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.workflow.domain.bo.ProcessDefinitionBo;
+import org.dromara.workflow.domain.vo.ProcessDefinitionVo;
+import org.dromara.workflow.service.IActProcessDefinitionService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+
+/**
+ * 流程定义管理 控制层
+ *
+ * @author may
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/workflow/processDefinition")
+public class ActProcessDefinitionController extends BaseController {
+
+    private final IActProcessDefinitionService actProcessDefinitionService;
+
+    /**
+     * 分页查询
+     *
+     * @param processDefinitionBo 参数
+     */
+    @GetMapping("/list")
+    public TableDataInfo<ProcessDefinitionVo> page(ProcessDefinitionBo processDefinitionBo) {
+        return actProcessDefinitionService.page(processDefinitionBo);
+    }
+
+    /**
+     * 查询历史流程定义列表
+     *
+     * @param key 流程定义key
+     */
+    @GetMapping("/getProcessDefinitionListByKey/{key}")
+    public R<List<ProcessDefinitionVo>> getProcessDefinitionListByKey(@NotEmpty(message = "流程定义key不能为空") @PathVariable String key) {
+        return R.ok("操作成功", actProcessDefinitionService.getProcessDefinitionListByKey(key));
+    }
+
+    /**
+     * 查看流程定义图片
+     *
+     * @param processDefinitionId 流程定义id
+     */
+    @GetMapping("/processDefinitionImage/{processDefinitionId}")
+    public R<String> processDefinitionImage(@PathVariable String processDefinitionId) {
+        return R.ok("操作成功", actProcessDefinitionService.processDefinitionImage(processDefinitionId));
+    }
+
+    /**
+     * 查看流程定义xml文件
+     *
+     * @param processDefinitionId 流程定义id
+     */
+    @GetMapping("/processDefinitionXml/{processDefinitionId}")
+    public R<Map<String, Object>> getXml(@NotBlank(message = "流程定义id不能为空") @PathVariable String processDefinitionId) {
+        Map<String, Object> map = new HashMap<>();
+        String xmlStr = actProcessDefinitionService.processDefinitionXml(processDefinitionId);
+        map.put("xml", Arrays.asList(xmlStr.split("\n")));
+        map.put("xmlStr", xmlStr);
+        return R.ok(map);
+    }
+
+    /**
+     * 删除流程定义
+     *
+     * @param deploymentId        部署id
+     * @param processDefinitionId 流程定义id
+     */
+    @Log(title = "流程定义管理", businessType = BusinessType.DELETE)
+    @RepeatSubmit()
+    @DeleteMapping("/{deploymentId}/{processDefinitionId}")
+    public R<Void> deleteDeployment(@NotBlank(message = "流程部署id不能为空") @PathVariable String deploymentId,
+                                    @NotBlank(message = "流程定义id不能为空") @PathVariable String processDefinitionId) {
+        return toAjax(actProcessDefinitionService.deleteDeployment(deploymentId, processDefinitionId));
+    }
+
+    /**
+     * 激活或者挂起流程定义
+     *
+     * @param processDefinitionId 流程定义id
+     */
+    @Log(title = "流程定义管理", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping("/updateProcessDefState/{processDefinitionId}")
+    public R<Void> updateProcDefState(@NotBlank(message = "流程定义id不能为空") @PathVariable String processDefinitionId) {
+        return toAjax(actProcessDefinitionService.updateProcessDefState(processDefinitionId));
+    }
+
+    /**
+     * 迁移流程定义
+     *
+     * @param currentProcessDefinitionId 当前流程定义id
+     * @param fromProcessDefinitionId    需要迁移到的流程定义id
+     */
+    @Log(title = "流程定义管理", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping("/migrationProcessDefinition/{currentProcessDefinitionId}/{fromProcessDefinitionId}")
+    public R<Void> migrationProcessDefinition(@NotBlank(message = "当前流程定义id") @PathVariable String currentProcessDefinitionId,
+                                              @NotBlank(message = "需要迁移到的流程定义id") @PathVariable String fromProcessDefinitionId) {
+        return toAjax(actProcessDefinitionService.migrationProcessDefinition(currentProcessDefinitionId, fromProcessDefinitionId));
+    }
+
+    /**
+     * 流程定义转换为模型
+     *
+     * @param processDefinitionId 流程定义id
+     */
+    @Log(title = "流程定义管理", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping("/convertToModel/{processDefinitionId}")
+    public R<Void> convertToModel(@NotEmpty(message = "流程定义id不能为空") @PathVariable String processDefinitionId) {
+        return toAjax(actProcessDefinitionService.convertToModel(processDefinitionId));
+    }
+
+    /**
+     * 通过zip或xml部署流程定义
+     *
+     * @param file         文件
+     * @param categoryCode 分类
+     */
+    @Log(title = "流程定义管理", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping("/deployByFile")
+    public R<Void> deployByFile(@RequestParam("file") MultipartFile file, @RequestParam("categoryCode") String categoryCode) {
+        return toAjax(actProcessDefinitionService.deployByFile(file, categoryCode));
+    }
+}

+ 157 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/ActProcessInstanceController.java

@@ -0,0 +1,157 @@
+package org.dromara.workflow.controller;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.workflow.domain.bo.ProcessInstanceBo;
+import org.dromara.workflow.domain.bo.ProcessInvalidBo;
+import org.dromara.workflow.domain.bo.TaskUrgingBo;
+import org.dromara.workflow.domain.vo.ProcessInstanceVo;
+import org.dromara.workflow.service.IActProcessInstanceService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Arrays;
+import java.util.Map;
+
+/**
+ * 流程实例管理 控制层
+ *
+ * @author may
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/workflow/processInstance")
+public class ActProcessInstanceController extends BaseController {
+
+    private final IActProcessInstanceService actProcessInstanceService;
+
+    /**
+     * 分页查询正在运行的流程实例
+     *
+     * @param processInstanceBo 参数
+     */
+    @GetMapping("/getProcessInstanceRunningByPage")
+    public TableDataInfo<ProcessInstanceVo> getProcessInstanceRunningByPage(ProcessInstanceBo processInstanceBo) {
+        return actProcessInstanceService.getProcessInstanceRunningByPage(processInstanceBo);
+    }
+
+    /**
+     * 分页查询已结束的流程实例
+     *
+     * @param processInstanceBo 参数
+     */
+    @GetMapping("/getProcessInstanceFinishByPage")
+    public TableDataInfo<ProcessInstanceVo> getProcessInstanceFinishByPage(ProcessInstanceBo processInstanceBo) {
+        return actProcessInstanceService.getProcessInstanceFinishByPage(processInstanceBo);
+    }
+
+    /**
+     * 通过流程实例id获取历史流程图
+     *
+     * @param processInstanceId 流程实例id
+     */
+    @GetMapping("/getHistoryProcessImage/{processInstanceId}")
+    public R<String> getHistoryProcessImage(@NotBlank(message = "流程实例id不能为空") @PathVariable String processInstanceId) {
+        return R.ok("操作成功", actProcessInstanceService.getHistoryProcessImage(processInstanceId));
+    }
+
+    /**
+     * 通过流程实例id获取历史流程图运行中,历史等节点
+     *
+     * @param processInstanceId 流程实例id
+     */
+    @GetMapping("/getHistoryProcessList/{processInstanceId}")
+    public R<Map<String, Object>> getHistoryProcessList(@NotBlank(message = "流程实例id不能为空") @PathVariable String processInstanceId) {
+        return R.ok("操作成功", actProcessInstanceService.getHistoryProcessList(processInstanceId));
+    }
+
+    /**
+     * 获取审批记录
+     *
+     * @param processInstanceId 流程实例id
+     */
+    @GetMapping("/getHistoryRecord/{processInstanceId}")
+    public R<Map<String, Object>> getHistoryRecord(@NotBlank(message = "流程实例id不能为空") @PathVariable String processInstanceId) {
+        return R.ok(actProcessInstanceService.getHistoryRecord(processInstanceId));
+    }
+
+    /**
+     * 作废流程实例,不会删除历史记录(删除运行中的实例)
+     *
+     * @param processInvalidBo 参数
+     */
+    @Log(title = "流程实例管理", businessType = BusinessType.DELETE)
+    @RepeatSubmit()
+    @PostMapping("/deleteRuntimeProcessInst")
+    public R<Void> deleteRuntimeProcessInst(@Validated(AddGroup.class) @RequestBody ProcessInvalidBo processInvalidBo) {
+        return toAjax(actProcessInstanceService.deleteRuntimeProcessInst(processInvalidBo));
+    }
+
+    /**
+     * 运行中的实例 删除程实例,删除历史记录,删除业务与流程关联信息
+     *
+     * @param processInstanceIds 流程实例id
+     */
+    @Log(title = "流程实例管理", businessType = BusinessType.DELETE)
+    @RepeatSubmit()
+    @DeleteMapping("/deleteRuntimeProcessAndHisInst/{processInstanceIds}")
+    public R<Void> deleteRuntimeProcessAndHisInst(@NotNull(message = "流程实例id不能为空") @PathVariable String[] processInstanceIds) {
+        return toAjax(actProcessInstanceService.deleteRuntimeProcessAndHisInst(Arrays.asList(processInstanceIds)));
+    }
+
+    /**
+     * 已完成的实例 删除程实例,删除历史记录,删除业务与流程关联信息
+     *
+     * @param processInstanceIds 流程实例id
+     */
+    @Log(title = "流程实例管理", businessType = BusinessType.DELETE)
+    @RepeatSubmit()
+    @DeleteMapping("/deleteFinishProcessAndHisInst/{processInstanceIds}")
+    public R<Void> deleteFinishProcessAndHisInst(@NotNull(message = "流程实例id不能为空") @PathVariable String[] processInstanceIds) {
+        return toAjax(actProcessInstanceService.deleteFinishProcessAndHisInst(Arrays.asList(processInstanceIds)));
+    }
+
+    /**
+     * 撤销流程申请
+     *
+     * @param processInstanceId 流程实例id
+     */
+    @Log(title = "流程实例管理", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping("/cancelProcessApply/{processInstanceId}")
+    public R<Void> cancelProcessApply(@NotBlank(message = "流程实例id不能为空") @PathVariable String processInstanceId) {
+        return toAjax(actProcessInstanceService.cancelProcessApply(processInstanceId));
+    }
+
+    /**
+     * 分页查询当前登录人单据
+     *
+     * @param processInstanceBo 参数
+     */
+    @GetMapping("/getCurrentSubmitByPage")
+    public TableDataInfo<ProcessInstanceVo> getCurrentSubmitByPage(ProcessInstanceBo processInstanceBo) {
+        return actProcessInstanceService.getCurrentSubmitByPage(processInstanceBo);
+    }
+
+    /**
+     * 任务催办(给当前任务办理人发送站内信,邮件,短信等)
+     *
+     * @param taskUrgingBo 任务催办
+     */
+    @Log(title = "流程实例管理", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping("/taskUrging")
+    public R<Void> taskUrging(@RequestBody TaskUrgingBo taskUrgingBo) {
+        return toAjax(actProcessInstanceService.taskUrging(taskUrgingBo));
+    }
+
+}

+ 246 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/ActTaskController.java

@@ -0,0 +1,246 @@
+package org.dromara.workflow.controller;
+
+import cn.hutool.core.convert.Convert;
+import jakarta.validation.constraints.NotBlank;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.workflow.domain.bo.*;
+import org.dromara.workflow.domain.vo.TaskVo;
+import org.dromara.workflow.service.IActTaskService;
+import org.dromara.workflow.utils.WorkflowUtils;
+import org.flowable.engine.TaskService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
+ * 任务管理 控制层
+ *
+ * @author may
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/workflow/task")
+public class ActTaskController extends BaseController {
+
+    private final IActTaskService actTaskService;
+
+    private final TaskService taskService;
+
+
+    /**
+     * 启动任务
+     *
+     * @param startProcessBo 启动流程参数
+     */
+    @Log(title = "任务管理", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping("/startWorkFlow")
+    public R<Map<String, Object>> startWorkFlow(@RequestBody StartProcessBo startProcessBo) {
+        Map<String, Object> map = actTaskService.startWorkFlow(startProcessBo);
+        return R.ok("提交成功", map);
+    }
+
+    /**
+     * 办理任务
+     *
+     * @param completeTaskBo 办理任务参数
+     */
+    @Log(title = "任务管理", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping("/completeTask")
+    public R<Void> completeTask(@Validated(AddGroup.class) @RequestBody CompleteTaskBo completeTaskBo) {
+        return toAjax(actTaskService.completeTask(completeTaskBo));
+    }
+
+    /**
+     * 查询当前用户的待办任务
+     *
+     * @param taskBo 参数
+     */
+    @GetMapping("/getTaskWaitByPage")
+    public TableDataInfo<TaskVo> getTaskWaitByPage(TaskBo taskBo) {
+        return actTaskService.getTaskWaitByPage(taskBo);
+    }
+
+    /**
+     * 查询当前租户所有待办任务
+     *
+     * @param taskBo 参数
+     */
+    @GetMapping("/getAllTaskWaitByPage")
+    public TableDataInfo<TaskVo> getAllTaskWaitByPage(TaskBo taskBo) {
+        return actTaskService.getAllTaskWaitByPage(taskBo);
+    }
+
+    /**
+     * 查询当前用户的已办任务
+     *
+     * @param taskBo 参数
+     */
+    @GetMapping("/getTaskFinishByPage")
+    public TableDataInfo<TaskVo> getTaskFinishByPage(TaskBo taskBo) {
+        return actTaskService.getTaskFinishByPage(taskBo);
+    }
+
+    /**
+     * 查询当前用户的抄送
+     *
+     * @param taskBo 参数
+     */
+    @GetMapping("/getTaskCopyByPage")
+    public TableDataInfo<TaskVo> getTaskCopyByPage(TaskBo taskBo) {
+        return actTaskService.getTaskCopyByPage(taskBo);
+    }
+
+    /**
+     * 查询当前租户所有已办任务
+     *
+     * @param taskBo 参数
+     */
+    @GetMapping("/getAllTaskFinishByPage")
+    public TableDataInfo<TaskVo> getAllTaskFinishByPage(TaskBo taskBo) {
+        return actTaskService.getAllTaskFinishByPage(taskBo);
+    }
+
+    /**
+     * 签收(拾取)任务
+     *
+     * @param taskId 任务id
+     */
+    @Log(title = "任务管理", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping("/claim/{taskId}")
+    public R<Void> claimTask(@NotBlank(message = "任务id不能为空") @PathVariable String taskId) {
+        try {
+            taskService.claim(taskId, Convert.toStr(LoginHelper.getUserId()));
+            return R.ok();
+        } catch (Exception e) {
+            e.printStackTrace();
+            return R.fail("签收任务失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 归还(拾取的)任务
+     *
+     * @param taskId 任务id
+     */
+    @Log(title = "任务管理", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping("/returnTask/{taskId}")
+    public R<Void> returnTask(@NotBlank(message = "任务id不能为空") @PathVariable String taskId) {
+        try {
+            taskService.setAssignee(taskId, null);
+            return R.ok();
+        } catch (Exception e) {
+            e.printStackTrace();
+            return R.fail("归还任务失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 委派任务
+     *
+     * @param delegateBo 参数
+     */
+    @Log(title = "任务管理", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping("/delegateTask")
+    public R<Void> delegateTask(@Validated({AddGroup.class}) @RequestBody DelegateBo delegateBo) {
+        return toAjax(actTaskService.delegateTask(delegateBo));
+    }
+
+    /**
+     * 终止任务
+     *
+     * @param terminationBo 参数
+     */
+    @Log(title = "任务管理", businessType = BusinessType.DELETE)
+    @RepeatSubmit()
+    @PostMapping("/terminationTask")
+    public R<Void> terminationTask(@RequestBody TerminationBo terminationBo) {
+        return toAjax(actTaskService.terminationTask(terminationBo));
+    }
+
+    /**
+     * 转办任务
+     *
+     * @param transmitBo 参数
+     */
+    @Log(title = "任务管理", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping("/transferTask")
+    public R<Void> transferTask(@Validated({AddGroup.class}) @RequestBody TransmitBo transmitBo) {
+        return toAjax(actTaskService.transferTask(transmitBo));
+    }
+
+    /**
+     * 会签任务加签
+     *
+     * @param addMultiBo 参数
+     */
+    @Log(title = "任务管理", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping("/addMultiInstanceExecution")
+    public R<Void> addMultiInstanceExecution(@Validated({AddGroup.class}) @RequestBody AddMultiBo addMultiBo) {
+        return toAjax(actTaskService.addMultiInstanceExecution(addMultiBo));
+    }
+
+    /**
+     * 会签任务减签
+     *
+     * @param deleteMultiBo 参数
+     */
+    @Log(title = "任务管理", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping("/deleteMultiInstanceExecution")
+    public R<Void> deleteMultiInstanceExecution(@Validated({AddGroup.class}) @RequestBody DeleteMultiBo deleteMultiBo) {
+        return toAjax(actTaskService.deleteMultiInstanceExecution(deleteMultiBo));
+    }
+
+    /**
+     * 驳回审批
+     *
+     * @param backProcessBo 参数
+     */
+    @Log(title = "任务管理", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping("/backProcess")
+    public R<String> backProcess(@RequestBody BackProcessBo backProcessBo) {
+        return R.ok(actTaskService.backProcess(backProcessBo));
+    }
+
+    /**
+     * 获取流程状态
+     *
+     * @param taskId 任务id
+     */
+    @GetMapping("/getBusinessStatus/{taskId}")
+    public R<String> getBusinessStatus(@PathVariable String taskId) {
+        return R.ok("操作成功", WorkflowUtils.getBusinessStatusByTaskId(taskId));
+    }
+
+
+    /**
+     * 修改任务办理人
+     *
+     * @param taskIds 任务id
+     * @param userId  办理人id
+     */
+    @Log(title = "任务管理", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping("/updateAssignee/{taskIds}/{userId}")
+    public R<Void> updateAssignee(@PathVariable String[] taskIds, @PathVariable String userId) {
+        return toAjax(actTaskService.updateAssignee(taskIds, userId));
+    }
+}

+ 107 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/TestLeaveController.java

@@ -0,0 +1,107 @@
+package org.dromara.workflow.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.workflow.domain.TestLeave;
+import org.dromara.workflow.domain.bo.TestLeaveBo;
+import org.dromara.workflow.domain.vo.TestLeaveVo;
+import org.dromara.workflow.service.ITestLeaveService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 请假
+ *
+ * @author may
+ * @date 2023-07-21
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/demo/leave")
+public class TestLeaveController extends BaseController {
+
+    private final ITestLeaveService testLeaveService;
+
+    /**
+     * 查询请假列表
+     */
+    @SaCheckPermission("demo:leave:list")
+    @GetMapping("/list")
+    public TableDataInfo<TestLeaveVo> list(TestLeaveBo bo, PageQuery pageQuery) {
+        return testLeaveService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 导出请假列表
+     */
+    @SaCheckPermission("demo:leave:export")
+    @Log(title = "请假", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(TestLeaveBo bo, HttpServletResponse response) {
+        List<TestLeaveVo> list = testLeaveService.queryList(bo);
+        ExcelUtil.exportExcel(list, "请假", TestLeaveVo.class, response);
+    }
+
+    /**
+     * 获取请假详细信息
+     *
+     * @param id 主键
+     */
+    @SaCheckPermission("demo:leave:query")
+    @GetMapping("/{id}")
+    public R<TestLeaveVo> getInfo(@NotNull(message = "主键不能为空")
+                                  @PathVariable Long id) {
+        return R.ok(testLeaveService.queryById(id));
+    }
+
+    /**
+     * 新增请假
+     */
+    @SaCheckPermission("demo:leave:add")
+    @Log(title = "请假", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping()
+    public R<TestLeave> add(@Validated(AddGroup.class) @RequestBody TestLeaveBo bo) {
+        return R.ok(testLeaveService.insertByBo(bo));
+    }
+
+    /**
+     * 修改请假
+     */
+    @SaCheckPermission("demo:leave:edit")
+    @Log(title = "请假", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping()
+    public R<TestLeave> edit(@Validated(EditGroup.class) @RequestBody TestLeaveBo bo) {
+        return R.ok(testLeaveService.updateByBo(bo));
+    }
+
+    /**
+     * 删除请假
+     *
+     * @param ids 主键串
+     */
+    @SaCheckPermission("demo:leave:remove")
+    @Log(title = "请假", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public R<Void> remove(@NotEmpty(message = "主键不能为空")
+                          @PathVariable Long[] ids) {
+        return toAjax(testLeaveService.deleteWithValidByIds(List.of(ids)));
+    }
+}

+ 106 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/WfCategoryController.java

@@ -0,0 +1,106 @@
+package org.dromara.workflow.controller;
+
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.excel.utils.ExcelUtil;
+import org.dromara.common.idempotent.annotation.RepeatSubmit;
+import org.dromara.common.log.annotation.Log;
+import org.dromara.common.log.enums.BusinessType;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.workflow.domain.bo.WfCategoryBo;
+import org.dromara.workflow.domain.vo.WfCategoryVo;
+import org.dromara.workflow.service.IWfCategoryService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 流程分类
+ *
+ * @author may
+ * @date 2023-06-28
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/workflow/category")
+public class WfCategoryController extends BaseController {
+
+    private final IWfCategoryService wfCategoryService;
+
+    /**
+     * 查询流程分类列表
+     */
+    @SaCheckPermission("workflow:category:list")
+    @GetMapping("/list")
+    public R<List<WfCategoryVo>> list(WfCategoryBo bo) {
+        List<WfCategoryVo> list = wfCategoryService.queryList(bo);
+        return R.ok(list);
+
+    }
+
+    /**
+     * 导出流程分类列表
+     */
+    @SaCheckPermission("workflow:category:export")
+    @Log(title = "流程分类", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(WfCategoryBo bo, HttpServletResponse response) {
+        List<WfCategoryVo> list = wfCategoryService.queryList(bo);
+        ExcelUtil.exportExcel(list, "流程分类", WfCategoryVo.class, response);
+    }
+
+    /**
+     * 获取流程分类详细信息
+     *
+     * @param id 主键
+     */
+    @SaCheckPermission("workflow:category:query")
+    @GetMapping("/{id}")
+    public R<WfCategoryVo> getInfo(@NotNull(message = "主键不能为空")
+                                   @PathVariable Long id) {
+        return R.ok(wfCategoryService.queryById(id));
+    }
+
+    /**
+     * 新增流程分类
+     */
+    @SaCheckPermission("workflow:category:add")
+    @Log(title = "流程分类", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping()
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody WfCategoryBo bo) {
+        return toAjax(wfCategoryService.insertByBo(bo));
+    }
+
+    /**
+     * 修改流程分类
+     */
+    @SaCheckPermission("workflow:category:edit")
+    @Log(title = "流程分类", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping()
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody WfCategoryBo bo) {
+        return toAjax(wfCategoryService.updateByBo(bo));
+    }
+
+    /**
+     * 删除流程分类
+     *
+     * @param ids 主键串
+     */
+    @SaCheckPermission("workflow:category:remove")
+    @Log(title = "流程分类", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public R<Void> remove(@NotEmpty(message = "主键不能为空")
+                          @PathVariable Long[] ids) {
+        return toAjax(wfCategoryService.deleteWithValidByIds(List.of(ids), true));
+    }
+}

+ 72 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/controller/WorkflowUserController.java

@@ -0,0 +1,72 @@
+package org.dromara.workflow.controller;
+
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.R;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.web.core.BaseController;
+import org.dromara.system.domain.bo.SysUserBo;
+import org.dromara.system.domain.vo.SysUserVo;
+import org.dromara.workflow.domain.bo.SysUserMultiBo;
+import org.dromara.workflow.domain.vo.TaskVo;
+import org.dromara.workflow.service.IWorkflowUserService;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+
+/**
+ * 工作流用户选人管理 控制层
+ *
+ * @author may
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/workflow/user")
+public class WorkflowUserController extends BaseController {
+
+    private final IWorkflowUserService workflowUserService;
+
+    /**
+     * 分页查询工作流选择加签人员
+     *
+     * @param sysUserMultiBo 参数
+     */
+    @GetMapping("/getWorkflowAddMultiListByPage")
+    public TableDataInfo<SysUserVo> getWorkflowAddMultiInstanceByPage(SysUserMultiBo sysUserMultiBo) {
+        return workflowUserService.getWorkflowAddMultiInstanceByPage(sysUserMultiBo);
+    }
+
+    /**
+     * 查询工作流选择减签人员
+     *
+     * @param taskId 任务id
+     */
+    @GetMapping("/getWorkflowDeleteMultiInstanceList/{taskId}")
+    public R<List<TaskVo>> getWorkflowDeleteMultiInstanceList(@PathVariable String taskId) {
+        return R.ok(workflowUserService.getWorkflowDeleteMultiInstanceList(taskId));
+    }
+
+    /**
+     * 按照用户id查询用户
+     *
+     * @param userIds 用户id
+     */
+    @GetMapping("/getUserListByIds/{userIds}")
+    public R<List<SysUserVo>> getUserListByIds(@PathVariable List<Long> userIds) {
+        return R.ok(workflowUserService.getUserListByIds(userIds));
+    }
+
+    /**
+     * 分页查询用户
+     *
+     * @param sysUserBo 参数
+     * @param pageQuery 分页
+     */
+    @GetMapping("/getUserListByPage")
+    public TableDataInfo<SysUserVo> getUserListByPage(SysUserBo sysUserBo, PageQuery pageQuery) {
+        return workflowUserService.getUserListByPage(sysUserBo, pageQuery);
+    }
+}

+ 152 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/ActHiProcinst.java

@@ -0,0 +1,152 @@
+package org.dromara.workflow.domain;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 流程实例对象 act_hi_procinst
+ *
+ * @author may
+ * @date 2023-07-22
+ */
+@Data
+@TableName("act_hi_procinst")
+public class ActHiProcinst implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     *
+     */
+    @TableId(value = "ID_")
+    private String id;
+
+    /**
+     *
+     */
+    @TableField(value = "REV_")
+    private Long rev;
+
+    /**
+     *
+     */
+    @TableField(value = "PROC_INST_ID_")
+    private String procInstId;
+
+    /**
+     *
+     */
+    @TableField(value = "BUSINESS_KEY_")
+    private String businessKey;
+
+    /**
+     *
+     */
+    @TableField(value = "PROC_DEF_ID_")
+    private String procDefId;
+
+    /**
+     *
+     */
+    @TableField(value = "START_TIME_")
+    private Date startTime;
+
+    /**
+     *
+     */
+    @TableField(value = "END_TIME_")
+    private Date endTime;
+
+    /**
+     *
+     */
+    @TableField(value = "DURATION_")
+    private Long duration;
+
+    /**
+     *
+     */
+    @TableField(value = "START_USER_ID_")
+    private String startUserId;
+
+    /**
+     *
+     */
+    @TableField(value = "START_ACT_ID_")
+    private String startActId;
+
+    /**
+     *
+     */
+    @TableField(value = "END_ACT_ID_")
+    private String endActId;
+
+    /**
+     *
+     */
+    @TableField(value = "SUPER_PROCESS_INSTANCE_ID_")
+    private String superProcessInstanceId;
+
+    /**
+     *
+     */
+    @TableField(value = "DELETE_REASON_")
+    private String deleteReason;
+
+    /**
+     *
+     */
+    @TableField(value = "TENANT_ID_")
+    private String tenantId;
+
+    /**
+     *
+     */
+    @TableField(value = "NAME_")
+    private String name;
+
+    /**
+     *
+     */
+    @TableField(value = "CALLBACK_ID_")
+    private String callbackId;
+
+    /**
+     *
+     */
+    @TableField(value = "CALLBACK_TYPE_")
+    private String callbackType;
+
+    /**
+     *
+     */
+    @TableField(value = "REFERENCE_ID_")
+    private String referenceId;
+
+    /**
+     *
+     */
+    @TableField(value = "REFERENCE_TYPE_")
+    private String referenceType;
+
+    /**
+     *
+     */
+    @TableField(value = "PROPAGATED_STAGE_INST_ID_")
+    private String propagatedStageInstId;
+
+    /**
+     *
+     */
+    @TableField(value = "BUSINESS_STATUS_")
+    private String businessStatus;
+
+
+}

+ 193 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/ActHiTaskinst.java

@@ -0,0 +1,193 @@
+package org.dromara.workflow.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+import java.io.Serial;
+
+/**
+ * 流程历史任务对象 act_hi_taskinst
+ *
+ * @author gssong
+ * @date 2024-03-02
+ */
+@Data
+@TableName("act_hi_taskinst")
+public class ActHiTaskinst implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     *
+     */
+    @TableId(value = "ID_")
+    private String id;
+
+    /**
+     * 版本
+     */
+    @TableField(value = "REV_")
+    private Long rev;
+
+    /**
+     * 流程定义id
+     */
+    @TableField(value = "PROC_DEF_ID_")
+    private String procDefId;
+
+    /**
+     *
+     */
+    @TableField(value = "TASK_DEF_ID_")
+    private String taskDefId;
+
+    /**
+     * 任务节点id
+     */
+    @TableField(value = "TASK_DEF_KEY_")
+    private String taskDefKey;
+
+    /**
+     * 流程实例id
+     */
+    @TableField(value = "PROC_INST_ID_")
+    private String procInstId;
+
+    /**
+     * 流程执行id
+     */
+    @TableField(value = "EXECUTION_ID")
+    private String executionId;
+
+    /**
+     *
+     */
+    @TableField(value = "SCOPE_ID_")
+    private String scopeId;
+
+    /**
+     *
+     */
+    @TableField(value = "SUB_SCOPE_ID_")
+    private String subScopeId;
+
+    /**
+     * 先用当前字段标识抄送类型
+     */
+    @TableField(value = "SCOPE_TYPE_")
+    private String scopeType;
+
+    /**
+     *
+     */
+    @TableField(value = "SCOPE_DEFINITION_ID_")
+    private String scopeDefinitionId;
+
+    /**
+     *
+     */
+    @TableField(value = "PROPAGATED_STAGE_INST_ID_")
+    private String propagatedStageInstId;
+
+    /**
+     * 任务名称
+     */
+    @TableField(value = "NAME_")
+    private String name;
+
+    /**
+     * 父级id
+     */
+    @TableField(value = "PARENT_TASK_ID_")
+    private String parentTaskId;
+
+    /**
+     * 描述
+     */
+    @TableField(value = "DESCRIPTION_")
+    private String description;
+
+    /**
+     * 办理人
+     */
+    @TableField(value = "OWNER_")
+    private String owner;
+
+    /**
+     * 办理人
+     */
+    @TableField(value = "ASSIGNEE_")
+    private String assignee;
+
+    /**
+     * 开始事件
+     */
+    @TableField(value = "START_TIME_")
+    private Date startTime;
+
+    /**
+     * 认领时间
+     */
+    @TableField(value = "CLAIM_TIME_")
+    private Date claimTime;
+
+    /**
+     * 结束时间
+     */
+    @TableField(value = "END_TIME_")
+    private Date endTime;
+
+    /**
+     * 持续时间
+     */
+    @TableField(value = "DURATION_")
+    private Long duration;
+
+    /**
+     * 删除原因
+     */
+    @TableField(value = "DELETE_REASON_")
+    private String deleteReason;
+
+    /**
+     * 优先级
+     */
+    @TableField(value = "PRIORITY_")
+    private Long priority;
+
+    /**
+     * 到期时间
+     */
+    @TableField(value = "DUE_DATE_")
+    private Date dueDate;
+
+    /**
+     *
+     */
+    @TableField(value = "FORM_KEY_")
+    private String formKey;
+
+    /**
+     * 分类
+     */
+    @TableField(value = "CATEGORY_")
+    private String category;
+
+    /**
+     * 最后修改时间
+     */
+    @TableField(value = "LAST_UPDATED_TIME_")
+    private Date lastUpdatedTime;
+
+    /**
+     * 租户id
+     */
+    @TableField(value = "TENANT_ID_")
+    private String tenantId;
+
+
+}

+ 58 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/TestLeave.java

@@ -0,0 +1,58 @@
+package org.dromara.workflow.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+
+import java.io.Serial;
+import java.util.Date;
+
+/**
+ * 请假对象 test_leave
+ *
+ * @author may
+ * @date 2023-07-21
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("test_leave")
+public class TestLeave extends BaseEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 请假类型
+     */
+    private String leaveType;
+
+    /**
+     * 开始时间
+     */
+    private Date startDate;
+
+    /**
+     * 结束时间
+     */
+    private Date endDate;
+
+    /**
+     * 请假天数
+     */
+    private Integer leaveDays;
+
+    /**
+     * 请假原因
+     */
+    private String remark;
+
+
+}

+ 52 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/WfCategory.java

@@ -0,0 +1,52 @@
+package org.dromara.workflow.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.tenant.core.TenantEntity;
+
+import java.io.Serial;
+
+/**
+ * 流程分类对象 wf_category
+ *
+ * @author may
+ * @date 2023-06-27
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("wf_category")
+public class WfCategory extends TenantEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 分类名称
+     */
+    private String categoryName;
+
+    /**
+     * 分类编码
+     */
+    private String categoryCode;
+
+    /**
+     * 父级id
+     */
+    private Long parentId;
+
+    /**
+     * 排序
+     */
+    private Long sortNum;
+
+
+}

+ 40 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/AddMultiBo.java

@@ -0,0 +1,40 @@
+package org.dromara.workflow.domain.bo;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+import org.dromara.common.core.validate.AddGroup;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 加签参数请求
+ *
+ * @author may
+ */
+@Data
+public class AddMultiBo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 任务ID
+     */
+    @NotBlank(message = "任务ID不能为空", groups = AddGroup.class)
+    private String taskId;
+
+    /**
+     * 加签人员id
+     */
+    @NotEmpty(message = "加签人员不能为空", groups = AddGroup.class)
+    private List<Long> assignees;
+
+    /**
+     * 加签人员名称
+     */
+    @NotEmpty(message = "加签人员不能为空", groups = AddGroup.class)
+    private List<String> assigneeNames;
+}

+ 43 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/BackProcessBo.java

@@ -0,0 +1,43 @@
+package org.dromara.workflow.domain.bo;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import org.dromara.common.core.validate.AddGroup;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+
+/**
+ * 驳回参数请求
+ *
+ * @author may
+ */
+@Data
+public class BackProcessBo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 任务ID
+     */
+    @NotBlank(message = "任务ID不能为空", groups = AddGroup.class)
+    private String taskId;
+
+    /**
+     * 消息类型
+     */
+    private List<String> messageType;
+
+    /**
+     * 驳回的节点id(目前未使用,直接驳回到申请人)
+     */
+    private String targetActivityId;
+
+    /**
+     * 办理意见
+     */
+    private String message;
+}

+ 65 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/CompleteTaskBo.java

@@ -0,0 +1,65 @@
+package org.dromara.workflow.domain.bo;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.workflow.domain.vo.WfCopy;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 办理任务请求对象
+ *
+ * @author may
+ */
+@Data
+public class CompleteTaskBo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 任务id
+     */
+    @NotBlank(message = "任务id不能为空", groups = {AddGroup.class})
+    private String taskId;
+
+    /**
+     * 附件id
+     */
+    private String fileId;
+
+    /**
+     * 抄送人员
+     */
+    private List<WfCopy> wfCopyList;
+
+    /**
+     * 消息类型
+     */
+    private List<String> messageType;
+
+    /**
+     * 办理意见
+     */
+    private String message;
+
+    /**
+     * 流程变量
+     */
+    private Map<String, Object> variables;
+
+    public Map<String, Object> getVariables() {
+        if (variables == null) {
+            return new HashMap<>(16);
+        }
+        variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
+        return variables;
+    }
+
+}

+ 38 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/DelegateBo.java

@@ -0,0 +1,38 @@
+package org.dromara.workflow.domain.bo;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import org.dromara.common.core.validate.AddGroup;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 委派任务请求对象
+ *
+ * @author may
+ */
+@Data
+public class DelegateBo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 委派人id
+     */
+    @NotBlank(message = "委派人id不能为空", groups = {AddGroup.class})
+    private String userId;
+
+    /**
+     * 委派人名称
+     */
+    @NotBlank(message = "委派人名称不能为空", groups = {AddGroup.class})
+    private String nickName;
+
+    /**
+     * 任务id
+     */
+    @NotBlank(message = "任务id不能为空", groups = {AddGroup.class})
+    private String taskId;
+}

+ 52 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/DeleteMultiBo.java

@@ -0,0 +1,52 @@
+package org.dromara.workflow.domain.bo;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import lombok.Data;
+import org.dromara.common.core.validate.AddGroup;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 减签参数请求
+ *
+ * @author may
+ */
+@Data
+public class DeleteMultiBo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 任务ID
+     */
+    @NotBlank(message = "任务ID不能为空", groups = AddGroup.class)
+    private String taskId;
+
+    /**
+     * 减签人员
+     */
+    @NotEmpty(message = "减签人员不能为空", groups = AddGroup.class)
+    private List<String> taskIds;
+
+    /**
+     * 执行id
+     */
+    @NotEmpty(message = "执行id不能为空", groups = AddGroup.class)
+    private List<String> executionIds;
+
+    /**
+     * 人员id
+     */
+    @NotEmpty(message = "减签人员id不能为空", groups = AddGroup.class)
+    private List<Long> assigneeIds;
+
+    /**
+     * 人员名称
+     */
+    @NotEmpty(message = "减签人员不能为空", groups = AddGroup.class)
+    private List<String> assigneeNames;
+}

+ 69 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/ModelBo.java

@@ -0,0 +1,69 @@
+package org.dromara.workflow.domain.bo;
+
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.Pattern;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.workflow.common.PageEntity;
+import org.dromara.workflow.common.constant.FlowConstant;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 模型请求对象
+ *
+ * @author may
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ModelBo extends PageEntity implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 模型id
+     */
+    @NotBlank(message = "模型ID不能为空", groups = {EditGroup.class})
+    private String id;
+
+    /**
+     * 模型名称
+     */
+    @NotBlank(message = "模型名称不能为空", groups = {AddGroup.class})
+    private String name;
+
+    /**
+     * 模型标识key
+     */
+    @NotBlank(message = "模型标识key不能为空", groups = {AddGroup.class})
+    @Pattern(regexp = FlowConstant.MODEL_KEY_PATTERN, message = "模型标识key只能字符或者下划线开头", groups = {AddGroup.class})
+    private String key;
+
+    /**
+     * 模型分类
+     */
+    @NotBlank(message = "模型分类不能为空", groups = {AddGroup.class})
+    private String categoryCode;
+
+    /**
+     * 模型XML
+     */
+    @NotBlank(message = "模型XML不能为空", groups = {AddGroup.class})
+    private String xml;
+
+    /**
+     * 模型SVG图片
+     */
+    @NotBlank(message = "模型SVG不能为空", groups = {EditGroup.class})
+    private String svg;
+
+    /**
+     * 备注
+     */
+    private String description;
+
+}

+ 37 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/ProcessDefinitionBo.java

@@ -0,0 +1,37 @@
+package org.dromara.workflow.domain.bo;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.workflow.common.PageEntity;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 流程定义请求对象
+ *
+ * @author may
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ProcessDefinitionBo extends PageEntity implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 流程定义名称key
+     */
+    private String key;
+
+    /**
+     * 流程定义名称
+     */
+    private String name;
+
+    /**
+     * 模型分类
+     */
+    private String categoryCode;
+
+}

+ 46 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/ProcessInstanceBo.java

@@ -0,0 +1,46 @@
+package org.dromara.workflow.domain.bo;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.workflow.common.PageEntity;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 流程实例请求对象
+ *
+ * @author may
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class ProcessInstanceBo extends PageEntity implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 流程名称
+     */
+    private String name;
+
+    /**
+     * 流程key
+     */
+    private String key;
+
+    /**
+     * 任务发起人
+     */
+    private String startUserId;
+
+    /**
+     * 业务id
+     */
+    private String businessKey;
+
+    /**
+     * 模型分类
+     */
+    private String categoryCode;
+}

+ 31 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/ProcessInvalidBo.java

@@ -0,0 +1,31 @@
+package org.dromara.workflow.domain.bo;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import org.dromara.common.core.validate.AddGroup;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 流程实例作废请求对象
+ *
+ * @author may
+ */
+@Data
+public class ProcessInvalidBo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 流程实例id
+     */
+    @NotBlank(message = "流程实例id不能为空", groups = {AddGroup.class})
+    private String processInstanceId;
+
+    /**
+     * 作废原因
+     */
+    private String deleteReason;
+}

+ 45 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/StartProcessBo.java

@@ -0,0 +1,45 @@
+package org.dromara.workflow.domain.bo;
+
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 启动流程对象
+ *
+ * @author may
+ */
+@Data
+public class StartProcessBo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 业务唯一值id
+     */
+    private String businessKey;
+
+    /**
+     * 流程执行key
+     */
+    private String processKey;
+
+    /**
+     * 流程变量,前端会提交一个元素{'entity': {业务详情数据对象}}
+     */
+    private Map<String, Object> variables;
+
+    public Map<String, Object> getVariables() {
+        if (variables == null) {
+            return new HashMap<>(16);
+        }
+        variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
+        return variables;
+    }
+}

+ 34 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/SysUserMultiBo.java

@@ -0,0 +1,34 @@
+package org.dromara.workflow.domain.bo;
+
+import lombok.Data;
+import org.dromara.workflow.common.PageEntity;
+
+
+/**
+ * 用户加签查询
+ *
+ * @author may
+ */
+@Data
+
+public class SysUserMultiBo extends PageEntity {
+    /**
+     * 人员名称
+     */
+    private String userName;
+
+    /**
+     * 人员名称
+     */
+    private String nickName;
+
+    /**
+     * 部门id
+     */
+    private String deptId;
+
+    /**
+     * 任务id
+     */
+    private String taskId;
+}

+ 36 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/TaskBo.java

@@ -0,0 +1,36 @@
+package org.dromara.workflow.domain.bo;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.workflow.common.PageEntity;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 任务请求对象
+ *
+ * @author may
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class TaskBo extends PageEntity implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 任务名称
+     */
+    private String name;
+
+    /**
+     * 流程定义名称
+     */
+    private String processDefinitionName;
+
+    /**
+     * 流程定义key
+     */
+    private String processDefinitionKey;
+}

+ 34 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/TaskUrgingBo.java

@@ -0,0 +1,34 @@
+package org.dromara.workflow.domain.bo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 任务催办
+ *
+ * @author may
+ */
+@Data
+public class TaskUrgingBo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 流程实例id
+     */
+    private String processInstanceId;
+
+    /**
+     * 消息类型
+     */
+    private List<String> messageType;
+
+    /**
+     * 催办内容(为空默认系统内置信息)
+     */
+    private String message;
+}

+ 37 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/TerminationBo.java

@@ -0,0 +1,37 @@
+package org.dromara.workflow.domain.bo;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import org.dromara.common.core.validate.AddGroup;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 终止任务请求对象
+ *
+ * @author may
+ */
+@Data
+public class TerminationBo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 任务id
+     */
+    @NotBlank(message = "任务id为空", groups = AddGroup.class)
+    private String taskId;
+
+    /**
+     * 转办人id
+     */
+    @NotBlank(message = "转办人不能为空", groups = AddGroup.class)
+    private String userId;
+
+    /**
+     * 审批意见
+     */
+    private String comment;
+}

+ 75 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/TestLeaveBo.java

@@ -0,0 +1,75 @@
+package org.dromara.workflow.domain.bo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.github.linpeilie.annotations.AutoMapper;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import org.dromara.workflow.domain.TestLeave;
+
+import java.util.Date;
+
+/**
+ * 请假业务对象 test_leave
+ *
+ * @author may
+ * @date 2023-07-21
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = TestLeave.class, reverseConvertGenerate = false)
+public class TestLeaveBo extends BaseEntity {
+
+    /**
+     * 主键
+     */
+    @NotNull(message = "主键不能为空", groups = {EditGroup.class})
+    private Long id;
+
+    /**
+     * 请假类型
+     */
+    @NotBlank(message = "请假类型不能为空", groups = {AddGroup.class, EditGroup.class})
+    private String leaveType;
+
+    /**
+     * 开始时间
+     */
+    @NotNull(message = "开始时间不能为空", groups = {AddGroup.class, EditGroup.class})
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date startDate;
+
+    /**
+     * 结束时间
+     */
+    @NotNull(message = "结束时间不能为空", groups = {AddGroup.class, EditGroup.class})
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date endDate;
+
+    /**
+     * 请假天数
+     */
+    @NotNull(message = "请假天数不能为空", groups = {AddGroup.class, EditGroup.class})
+    private Integer leaveDays;
+
+    /**
+     * 开始时间
+     */
+    private Integer startLeaveDays;
+
+    /**
+     * 结束时间
+     */
+    private Integer endLeaveDays;
+
+    /**
+     * 请假原因
+     */
+    private String remark;
+
+
+}

+ 37 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/TransmitBo.java

@@ -0,0 +1,37 @@
+package org.dromara.workflow.domain.bo;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+import org.dromara.common.core.validate.AddGroup;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 终转办务请求对象
+ *
+ * @author may
+ */
+@Data
+public class TransmitBo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 任务id
+     */
+    @NotBlank(message = "任务id为空", groups = AddGroup.class)
+    private String taskId;
+
+    /**
+     * 转办人id
+     */
+    @NotBlank(message = "转办人不能为空", groups = AddGroup.class)
+    private String userId;
+
+    /**
+     * 审批意见
+     */
+    private String comment;
+}

+ 54 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/bo/WfCategoryBo.java

@@ -0,0 +1,54 @@
+package org.dromara.workflow.domain.bo;
+
+import io.github.linpeilie.annotations.AutoMapper;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.dromara.common.core.validate.AddGroup;
+import org.dromara.common.core.validate.EditGroup;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import org.dromara.workflow.domain.WfCategory;
+
+/**
+ * 流程分类业务对象 wf_category
+ *
+ * @author may
+ * @date 2023-06-27
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = WfCategory.class, reverseConvertGenerate = false)
+public class WfCategoryBo extends BaseEntity {
+
+    /**
+     * 主键
+     */
+    @NotNull(message = "主键不能为空", groups = {EditGroup.class})
+    private Long id;
+
+    /**
+     * 分类名称
+     */
+    @NotBlank(message = "分类名称不能为空", groups = {AddGroup.class, EditGroup.class})
+    private String categoryName;
+
+    /**
+     * 分类编码
+     */
+    @NotBlank(message = "分类编码不能为空", groups = {AddGroup.class, EditGroup.class})
+    private String categoryCode;
+
+    /**
+     * 父级id
+     */
+    @NotNull(message = "父级id不能为空", groups = {AddGroup.class, EditGroup.class})
+    private Long parentId;
+
+    /**
+     * 排序
+     */
+    private Long sortNum;
+
+
+}

+ 89 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/ActHistoryInfoVo.java

@@ -0,0 +1,89 @@
+package org.dromara.workflow.domain.vo;
+
+import lombok.Data;
+import org.dromara.common.translation.annotation.Translation;
+import org.dromara.common.translation.constant.TransConstant;
+import org.flowable.engine.task.Attachment;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 流程审批记录视图
+ *
+ * @author may
+ */
+@Data
+public class ActHistoryInfoVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+    /**
+     * 任务id
+     */
+    private String id;
+    /**
+     * 节点id
+     */
+    private String taskDefinitionKey;
+    /**
+     * 任务名称
+     */
+    private String name;
+    /**
+     * 流程实例id
+     */
+    private String processInstanceId;
+    /**
+     * 开始时间
+     */
+    private Date startTime;
+    /**
+     * 结束时间
+     */
+    private Date endTime;
+    /**
+     * 运行时长
+     */
+    private String runDuration;
+    /**
+     * 状态
+     */
+    private String status;
+    /**
+     * 状态
+     */
+    private String statusName;
+    /**
+     * 办理人id
+     */
+    private Long assignee;
+
+    /**
+     * 办理人名称
+     */
+    @Translation(type = TransConstant.USER_ID_TO_NICKNAME, mapper = "assignee")
+    private String nickName;
+
+    /**
+     * 办理人id
+     */
+    private String owner;
+
+    /**
+     * 审批信息id
+     */
+    private String commentId;
+
+    /**
+     * 审批信息
+     */
+    private String comment;
+
+    /**
+     * 审批附件
+     */
+    private List<Attachment> attachmentList;
+}

+ 47 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/GraphicInfoVo.java

@@ -0,0 +1,47 @@
+package org.dromara.workflow.domain.vo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 节点图形信息
+ *
+ * @author may
+ */
+@Data
+public class GraphicInfoVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+    /**
+     * x坐标
+     */
+    private double x;
+
+    /**
+     * y坐标
+     */
+    private double y;
+
+    /**
+     * 节点高度
+     */
+    private double height;
+
+    /**
+     * 节点宽度
+     */
+    private double width;
+
+    /**
+     * 节点id
+     */
+    private String nodeId;
+
+    /**
+     * 节点名称
+     */
+    private String nodeName;
+}

+ 27 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/GroupRepresentation.java

@@ -0,0 +1,27 @@
+/* Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.dromara.workflow.domain.vo;
+
+import lombok.Data;
+
+/**
+ * @author Joram Barrez
+ */
+@Data
+public class GroupRepresentation {
+
+    protected String id;
+    protected String name;
+    protected String type;
+
+}

+ 48 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/ModelVo.java

@@ -0,0 +1,48 @@
+package org.dromara.workflow.domain.vo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 模型视图对象
+ *
+ * @author may
+ */
+@Data
+public class ModelVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 模型id
+     */
+    private String id;
+
+    /**
+     * 模型名称
+     */
+    private String name;
+
+    /**
+     * 模型标识key
+     */
+    private String key;
+
+    /**
+     * 模型分类
+     */
+    private String categoryCode;
+
+    /**
+     * 模型XML
+     */
+    private String xml;
+
+    /**
+     * 备注
+     */
+    private String description;
+}

+ 33 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/MultiInstanceVo.java

@@ -0,0 +1,33 @@
+package org.dromara.workflow.domain.vo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 多实例信息
+ *
+ * @author may
+ */
+@Data
+public class MultiInstanceVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 会签类型(串行,并行)
+     */
+    private Object type;
+
+    /**
+     * 会签人员KEY
+     */
+    private String assignee;
+
+    /**
+     * 会签人员集合KEY
+     */
+    private String assigneeList;
+}

+ 43 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/ParticipantVo.java

@@ -0,0 +1,43 @@
+package org.dromara.workflow.domain.vo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 参与者
+ *
+ * @author may
+ */
+@Data
+public class ParticipantVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 组id(角色id)
+     */
+    private List<Long> groupIds;
+
+    /**
+     * 候选人id(用户id) 当组id不为空时,将组内人员查出放入candidate
+     */
+    private List<Long> candidate;
+
+    /**
+     * 候选人名称(用户名称) 当组id不为空时,将组内人员查出放入candidateName
+     */
+    private List<String> candidateName;
+
+    /**
+     * 是否认领标识
+     * 当为空时默认当前任务不需要认领
+     * 当为true时当前任务说明为候选模式并且有人已经认领了任务可以归还,
+     * 当为false时当前任务说明为候选模式该任务未认领,
+     */
+    private Boolean claim;
+
+}

+ 65 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/ProcessDefinitionVo.java

@@ -0,0 +1,65 @@
+package org.dromara.workflow.domain.vo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 流程定义视图
+ *
+ * @author may
+ */
+@Data
+public class ProcessDefinitionVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 流程定义id
+     */
+    private String id;
+
+    /**
+     * 流程定义名称
+     */
+    private String name;
+
+    /**
+     * 流程定义标识key
+     */
+    private String key;
+
+    /**
+     * 流程定义版本
+     */
+    private int version;
+
+    /**
+     * 流程定义挂起或激活 1激活 2挂起
+     */
+    private int suspensionState;
+
+    /**
+     * 流程xml名称
+     */
+    private String resourceName;
+
+    /**
+     * 流程图片名称
+     */
+    private String diagramResourceName;
+
+    /**
+     * 流程部署id
+     */
+    private String deploymentId;
+
+    /**
+     * 流程部署时间
+     */
+    private Date deploymentTime;
+
+}

+ 95 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/ProcessInstanceVo.java

@@ -0,0 +1,95 @@
+package org.dromara.workflow.domain.vo;
+
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 流程实例视图
+ *
+ * @author may
+ */
+@Data
+public class ProcessInstanceVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 流程实例id
+     */
+    private String id;
+
+    /**
+     * 流程定义id
+     */
+    private String processDefinitionId;
+
+    /**
+     * 流程定义名称
+     */
+    private String processDefinitionName;
+
+    /**
+     * 流程定义key
+     */
+    private String processDefinitionKey;
+
+    /**
+     * 流程定义版本
+     */
+    private String processDefinitionVersion;
+
+    /**
+     * 部署id
+     */
+    private String deploymentId;
+
+    /**
+     * 业务id
+     */
+    private String businessKey;
+
+    /**
+     * 是否挂起
+     */
+    private Boolean isSuspended;
+
+    /**
+     * 租户id
+     */
+    private String tenantId;
+
+    /**
+     * 启动时间
+     */
+    private Date startTime;
+
+    /**
+     * 结束时间
+     */
+    private Date endTime;
+
+    /**
+     * 启动人id
+     */
+    private String startUserId;
+
+    /**
+     * 流程状态
+     */
+    private String businessStatus;
+
+    /**
+     * 流程状态
+     */
+    private String businessStatusName;
+
+    /**
+     * 待办任务集合
+     */
+    private List<TaskVo> taskVoList;
+}

+ 143 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/TaskVo.java

@@ -0,0 +1,143 @@
+package org.dromara.workflow.domain.vo;
+
+import lombok.Data;
+import org.dromara.common.translation.annotation.Translation;
+import org.dromara.common.translation.constant.TransConstant;
+
+import java.util.Date;
+
+/**
+ * 任务视图
+ *
+ * @author may
+ */
+@Data
+public class TaskVo {
+
+    /**
+     * 任务id
+     */
+    private String id;
+
+    /**
+     * 任务名称
+     */
+    private String name;
+
+    /**
+     * 描述
+     */
+    private String description;
+
+    /**
+     * 优先级
+     */
+    private Integer priority;
+
+    /**
+     * 负责此任务的人员的用户id
+     */
+    private String owner;
+
+    /**
+     * 办理人id
+     */
+    private Long assignee;
+
+    /**
+     * 办理人
+     */
+    @Translation(type = TransConstant.USER_ID_TO_NICKNAME, mapper = "assignee")
+    private String assigneeName;
+
+
+    /**
+     * 流程实例id
+     */
+    private String processInstanceId;
+
+    /**
+     * 执行id
+     */
+    private String executionId;
+
+    /**
+     * 无用
+     */
+    private String taskDefinitionId;
+
+    /**
+     * 流程定义id
+     */
+    private String processDefinitionId;
+
+    /**
+     * 创建时间
+     */
+    private Date createTime;
+
+    /**
+     * 结束时间
+     */
+    private Date endTime;
+
+    /**
+     * 节点id
+     */
+    private String taskDefinitionKey;
+
+    /**
+     * 任务截止日期
+     */
+    private Date dueDate;
+
+    /**
+     * 流程类别
+     */
+    private String category;
+
+    /**
+     * 父级任务id
+     */
+    private String parentTaskId;
+
+    /**
+     * 租户id
+     */
+    private String tenantId;
+
+    /**
+     * 认领时间
+     */
+    private Date claimTime;
+
+    /**
+     * 流程状态
+     */
+    private String businessStatus;
+
+    /**
+     * 流程状态
+     */
+    private String businessStatusName;
+
+    /**
+     * 流程定义名称
+     */
+    private String processDefinitionName;
+
+    /**
+     * 流程定义key
+     */
+    private String processDefinitionKey;
+
+    /**
+     * 参与者
+     */
+    private ParticipantVo participantVo;
+
+    /**
+     * 是否会签
+     */
+    private Boolean multiInstance;
+}

+ 70 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/TestLeaveVo.java

@@ -0,0 +1,70 @@
+package org.dromara.workflow.domain.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.workflow.domain.TestLeave;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+
+/**
+ * 请假视图对象 test_leave
+ *
+ * @author may
+ * @date 2023-07-21
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = TestLeave.class)
+public class TestLeaveVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @ExcelProperty(value = "主键")
+    private Long id;
+
+    /**
+     * 请假类型
+     */
+    @ExcelProperty(value = "请假类型")
+    private String leaveType;
+
+    /**
+     * 开始时间
+     */
+    @ExcelProperty(value = "开始时间")
+    private Date startDate;
+
+    /**
+     * 结束时间
+     */
+    @ExcelProperty(value = "结束时间")
+    private Date endDate;
+
+    /**
+     * 请假天数
+     */
+    @ExcelProperty(value = "请假天数")
+    private Integer leaveDays;
+
+    /**
+     * 备注
+     */
+    @ExcelProperty(value = "请假原因")
+    private String remark;
+
+    /**
+     * 流程实例对象
+     */
+    private ProcessInstanceVo processInstanceVo;
+
+
+}

+ 58 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/WfCategoryVo.java

@@ -0,0 +1,58 @@
+package org.dromara.workflow.domain.vo;
+
+import com.alibaba.excel.annotation.ExcelIgnoreUnannotated;
+import com.alibaba.excel.annotation.ExcelProperty;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import org.dromara.workflow.domain.WfCategory;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+
+/**
+ * 流程分类视图对象 wf_category
+ *
+ * @author may
+ * @date 2023-06-27
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = WfCategory.class)
+public class WfCategoryVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @ExcelProperty(value = "主键")
+    private Long id;
+
+    /**
+     * 分类名称
+     */
+    @ExcelProperty(value = "分类名称")
+    private String categoryName;
+
+    /**
+     * 分类编码
+     */
+    @ExcelProperty(value = "分类编码")
+    private String categoryCode;
+
+    /**
+     * 父级id
+     */
+    @ExcelProperty(value = "父级id")
+    private Long parentId;
+
+    /**
+     * 排序
+     */
+    @ExcelProperty(value = "排序")
+    private Long sortNum;
+
+
+}

+ 23 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/domain/vo/WfCopy.java

@@ -0,0 +1,23 @@
+package org.dromara.workflow.domain.vo;
+
+import lombok.Data;
+
+/**
+ * 抄送
+ *
+ * @author may
+ */
+@Data
+public class WfCopy {
+
+    /**
+     * 用户id
+     */
+    private Long userId;
+
+    /**
+     * 用户名称
+     */
+    private String userName;
+
+}

+ 108 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/CustomDefaultProcessDiagramCanvas.java

@@ -0,0 +1,108 @@
+package org.dromara.workflow.flowable;
+
+import org.flowable.bpmn.model.AssociationDirection;
+import org.flowable.image.impl.DefaultProcessDiagramCanvas;
+
+import java.awt.*;
+import java.awt.geom.Line2D;
+import java.awt.geom.RoundRectangle2D;
+
+public class CustomDefaultProcessDiagramCanvas extends DefaultProcessDiagramCanvas {
+    //设置高亮线的颜色  这里我设置成绿色
+    protected static Color HIGHLIGHT_SEQUENCEFLOW_COLOR = Color.GREEN;
+
+    public CustomDefaultProcessDiagramCanvas(int width, int height, int minX, int minY, String imageType, String activityFontName, String labelFontName, String annotationFontName, ClassLoader customClassLoader) {
+        super(width, height, minX, minY, imageType, activityFontName, labelFontName, annotationFontName, customClassLoader);
+    }
+
+    /**
+     * 画线颜色设置
+     */
+    public void drawConnection(int[] xPoints, int[] yPoints, boolean conditional, boolean isDefault, String connectionType,
+                               AssociationDirection associationDirection, boolean highLighted, double scaleFactor) {
+
+        Paint originalPaint = g.getPaint();
+        Stroke originalStroke = g.getStroke();
+
+        g.setPaint(CONNECTION_COLOR);
+        if (connectionType.equals("association")) {
+            g.setStroke(ASSOCIATION_STROKE);
+        } else if (highLighted) {
+            //设置线的颜色
+            g.setPaint(HIGHLIGHT_SEQUENCEFLOW_COLOR);
+            g.setStroke(HIGHLIGHT_FLOW_STROKE);
+        }
+
+        for (int i = 1; i < xPoints.length; i++) {
+            Integer sourceX = xPoints[i - 1];
+            Integer sourceY = yPoints[i - 1];
+            Integer targetX = xPoints[i];
+            Integer targetY = yPoints[i];
+            Line2D.Double line = new Line2D.Double(sourceX, sourceY, targetX, targetY);
+            g.draw(line);
+        }
+
+        if (isDefault) {
+            Line2D.Double line = new Line2D.Double(xPoints[0], yPoints[0], xPoints[1], yPoints[1]);
+            drawDefaultSequenceFlowIndicator(line, scaleFactor);
+        }
+
+        if (conditional) {
+            Line2D.Double line = new Line2D.Double(xPoints[0], yPoints[0], xPoints[1], yPoints[1]);
+            drawConditionalSequenceFlowIndicator(line, scaleFactor);
+        }
+
+        if (associationDirection == AssociationDirection.ONE || associationDirection == AssociationDirection.BOTH) {
+            Line2D.Double line = new Line2D.Double(xPoints[xPoints.length - 2], yPoints[xPoints.length - 2], xPoints[xPoints.length - 1], yPoints[xPoints.length - 1]);
+            drawArrowHead(line, scaleFactor);
+        }
+        if (associationDirection == AssociationDirection.BOTH) {
+            Line2D.Double line = new Line2D.Double(xPoints[1], yPoints[1], xPoints[0], yPoints[0]);
+            drawArrowHead(line, scaleFactor);
+        }
+        g.setPaint(originalPaint);
+        g.setStroke(originalStroke);
+    }
+
+    /**
+     * 高亮节点设置
+     */
+    public void drawHighLight(int x, int y, int width, int height) {
+        Paint originalPaint = g.getPaint();
+        Stroke originalStroke = g.getStroke();
+        //设置高亮节点的颜色
+        g.setPaint(HIGHLIGHT_COLOR);
+        g.setStroke(THICK_TASK_BORDER_STROKE);
+
+        RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, width, height, 20, 20);
+        g.draw(rect);
+
+        g.setPaint(originalPaint);
+        g.setStroke(originalStroke);
+    }
+
+    /**
+     * @description: 高亮节点红色
+     * @param: x
+     * @param: y
+     * @param: width
+     * @param: height
+     * @return: void
+     * @author: gssong
+     * @date: 2022/4/12
+     */
+    public void drawHighLightRed(int x, int y, int width, int height) {
+        Paint originalPaint = g.getPaint();
+        Stroke originalStroke = g.getStroke();
+        //设置高亮节点的颜色
+        g.setPaint(Color.green);
+        g.setStroke(THICK_TASK_BORDER_STROKE);
+
+        RoundRectangle2D rect = new RoundRectangle2D.Double(x, y, width, height, 20, 20);
+        g.draw(rect);
+
+        g.setPaint(originalPaint);
+        g.setStroke(originalStroke);
+    }
+
+}

+ 1120 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/CustomDefaultProcessDiagramGenerator.java

@@ -0,0 +1,1120 @@
+/* Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.dromara.workflow.flowable;
+
+import org.flowable.bpmn.model.Event;
+import org.flowable.bpmn.model.Process;
+import org.flowable.bpmn.model.*;
+import org.flowable.image.ProcessDiagramGenerator;
+
+import java.awt.*;
+import java.awt.image.BufferedImage;
+import java.io.InputStream;
+import java.util.List;
+import java.util.*;
+
+/**
+ * Class to generate an image based the diagram interchange information in a BPMN 2.0 process.
+ *
+ * @author Joram Barrez
+ * @author Tijs Rademakers
+ * @author Zheng Ji
+ */
+public class CustomDefaultProcessDiagramGenerator implements ProcessDiagramGenerator {
+
+    protected Map<Class<? extends BaseElement>, ActivityDrawInstruction> activityDrawInstructions = new HashMap<>();
+    protected Map<Class<? extends BaseElement>, ArtifactDrawInstruction> artifactDrawInstructions = new HashMap<>();
+
+    public CustomDefaultProcessDiagramGenerator() {
+        this(1.0);
+    }
+
+    // The instructions on how to draw a certain construct is
+    // created statically and stored in a map for performance.
+    public CustomDefaultProcessDiagramGenerator(final double scaleFactor) {
+        // start event
+        activityDrawInstructions.put(StartEvent.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                StartEvent startEvent = (StartEvent) flowNode;
+                if (startEvent.getEventDefinitions() != null && !startEvent.getEventDefinitions().isEmpty()) {
+                    EventDefinition eventDefinition = startEvent.getEventDefinitions().get(0);
+                    if (eventDefinition instanceof TimerEventDefinition) {
+                        processDiagramCanvas.drawTimerStartEvent(graphicInfo, scaleFactor);
+                    } else if (eventDefinition instanceof ErrorEventDefinition) {
+                        processDiagramCanvas.drawErrorStartEvent(graphicInfo, scaleFactor);
+                    } else if (eventDefinition instanceof EscalationEventDefinition) {
+                        processDiagramCanvas.drawEscalationStartEvent(graphicInfo, scaleFactor);
+                    } else if (eventDefinition instanceof ConditionalEventDefinition) {
+                        processDiagramCanvas.drawConditionalStartEvent(graphicInfo, scaleFactor);
+                    } else if (eventDefinition instanceof SignalEventDefinition) {
+                        processDiagramCanvas.drawSignalStartEvent(graphicInfo, scaleFactor);
+                    } else if (eventDefinition instanceof MessageEventDefinition) {
+                        processDiagramCanvas.drawMessageStartEvent(graphicInfo, scaleFactor);
+                    } else {
+                        processDiagramCanvas.drawNoneStartEvent(graphicInfo);
+                    }
+                } else {
+                    List<ExtensionElement> eventTypeElements = startEvent.getExtensionElements().get("eventType");
+                    if (eventTypeElements != null && eventTypeElements.size() > 0) {
+                        processDiagramCanvas.drawEventRegistryStartEvent(graphicInfo, scaleFactor);
+
+                    } else {
+                        processDiagramCanvas.drawNoneStartEvent(graphicInfo);
+                    }
+                }
+            }
+        });
+
+        // signal catch
+        activityDrawInstructions.put(IntermediateCatchEvent.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                IntermediateCatchEvent intermediateCatchEvent = (IntermediateCatchEvent) flowNode;
+                if (intermediateCatchEvent.getEventDefinitions() != null && !intermediateCatchEvent.getEventDefinitions().isEmpty()) {
+
+                    if (intermediateCatchEvent.getEventDefinitions().get(0) instanceof SignalEventDefinition) {
+                        processDiagramCanvas.drawCatchingSignalEvent(flowNode.getName(), graphicInfo, true, scaleFactor);
+                    } else if (intermediateCatchEvent.getEventDefinitions().get(0) instanceof TimerEventDefinition) {
+                        processDiagramCanvas.drawCatchingTimerEvent(flowNode.getName(), graphicInfo, true, scaleFactor);
+                    } else if (intermediateCatchEvent.getEventDefinitions().get(0) instanceof MessageEventDefinition) {
+                        processDiagramCanvas.drawCatchingMessageEvent(flowNode.getName(), graphicInfo, true, scaleFactor);
+                    } else if (intermediateCatchEvent.getEventDefinitions().get(0) instanceof ConditionalEventDefinition) {
+                        processDiagramCanvas.drawCatchingConditionalEvent(flowNode.getName(), graphicInfo, true, scaleFactor);
+                    }
+                }
+            }
+        });
+
+        // signal throw
+        activityDrawInstructions.put(ThrowEvent.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                ThrowEvent throwEvent = (ThrowEvent) flowNode;
+                if (throwEvent.getEventDefinitions() != null && !throwEvent.getEventDefinitions().isEmpty()) {
+                    if (throwEvent.getEventDefinitions().get(0) instanceof SignalEventDefinition) {
+                        processDiagramCanvas.drawThrowingSignalEvent(graphicInfo, scaleFactor);
+                    } else if (throwEvent.getEventDefinitions().get(0) instanceof EscalationEventDefinition) {
+                        processDiagramCanvas.drawThrowingEscalationEvent(graphicInfo, scaleFactor);
+                    } else if (throwEvent.getEventDefinitions().get(0) instanceof CompensateEventDefinition) {
+                        processDiagramCanvas.drawThrowingCompensateEvent(graphicInfo, scaleFactor);
+                    } else {
+                        processDiagramCanvas.drawThrowingNoneEvent(graphicInfo, scaleFactor);
+                    }
+                } else {
+                    processDiagramCanvas.drawThrowingNoneEvent(graphicInfo, scaleFactor);
+                }
+            }
+        });
+
+        // end event
+        activityDrawInstructions.put(EndEvent.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                EndEvent endEvent = (EndEvent) flowNode;
+                if (endEvent.getEventDefinitions() != null && !endEvent.getEventDefinitions().isEmpty()) {
+                    if (endEvent.getEventDefinitions().get(0) instanceof ErrorEventDefinition) {
+                        processDiagramCanvas.drawErrorEndEvent(flowNode.getName(), graphicInfo, scaleFactor);
+                    } else if (endEvent.getEventDefinitions().get(0) instanceof EscalationEventDefinition) {
+                        processDiagramCanvas.drawEscalationEndEvent(flowNode.getName(), graphicInfo, scaleFactor);
+                    } else {
+                        processDiagramCanvas.drawNoneEndEvent(graphicInfo, scaleFactor);
+                    }
+                } else {
+                    processDiagramCanvas.drawNoneEndEvent(graphicInfo, scaleFactor);
+                }
+            }
+        });
+
+        // task
+        activityDrawInstructions.put(Task.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                processDiagramCanvas.drawTask(flowNode.getName(), graphicInfo, scaleFactor);
+            }
+        });
+
+        // user task
+        activityDrawInstructions.put(UserTask.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                processDiagramCanvas.drawUserTask(flowNode.getName(), graphicInfo, scaleFactor);
+            }
+        });
+
+        // script task
+        activityDrawInstructions.put(ScriptTask.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                processDiagramCanvas.drawScriptTask(flowNode.getName(), graphicInfo, scaleFactor);
+            }
+        });
+
+        // service task
+        activityDrawInstructions.put(ServiceTask.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                ServiceTask serviceTask = (ServiceTask) flowNode;
+                if ("camel".equalsIgnoreCase(serviceTask.getType())) {
+                    processDiagramCanvas.drawCamelTask(serviceTask.getName(), graphicInfo, scaleFactor);
+                }else if (ServiceTask.HTTP_TASK.equalsIgnoreCase(serviceTask.getType())) {
+                    processDiagramCanvas.drawHttpTask(serviceTask.getName(), graphicInfo, scaleFactor);
+                } else if (ServiceTask.DMN_TASK.equalsIgnoreCase(serviceTask.getType())) {
+                    processDiagramCanvas.drawDMNTask(serviceTask.getName(), graphicInfo, scaleFactor);
+                } else if (ServiceTask.SHELL_TASK.equalsIgnoreCase(serviceTask.getType())) {
+                    processDiagramCanvas.drawShellTask(serviceTask.getName(), graphicInfo, scaleFactor);
+                } else {
+                    processDiagramCanvas.drawServiceTask(serviceTask.getName(), graphicInfo, scaleFactor);
+                }
+            }
+        });
+
+        // http service task
+        activityDrawInstructions.put(HttpServiceTask.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                processDiagramCanvas.drawHttpTask(flowNode.getName(), graphicInfo, scaleFactor);
+            }
+        });
+
+        // receive task
+        activityDrawInstructions.put(ReceiveTask.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                processDiagramCanvas.drawReceiveTask(flowNode.getName(), graphicInfo, scaleFactor);
+            }
+        });
+
+        // send task
+        activityDrawInstructions.put(SendTask.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                processDiagramCanvas.drawSendTask(flowNode.getName(), graphicInfo, scaleFactor);
+            }
+        });
+
+        // manual task
+        activityDrawInstructions.put(ManualTask.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                processDiagramCanvas.drawManualTask(flowNode.getName(), graphicInfo, scaleFactor);
+            }
+        });
+
+        // send event service task
+        activityDrawInstructions.put(SendEventServiceTask.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                processDiagramCanvas.drawSendEventServiceTask(flowNode.getName(), graphicInfo, scaleFactor);
+            }
+        });
+
+        // external worker service task
+        activityDrawInstructions.put(ExternalWorkerServiceTask.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                ServiceTask serviceTask = (ServiceTask) flowNode;
+                processDiagramCanvas.drawServiceTask(serviceTask.getName(), graphicInfo, scaleFactor);
+            }
+        });
+
+        // case service task
+        activityDrawInstructions.put(CaseServiceTask.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                processDiagramCanvas.drawCaseServiceTask(flowNode.getName(), graphicInfo, scaleFactor);
+            }
+        });
+
+        // businessRuleTask task
+        activityDrawInstructions.put(BusinessRuleTask.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                processDiagramCanvas.drawBusinessRuleTask(flowNode.getName(), graphicInfo, scaleFactor);
+            }
+        });
+
+        // exclusive gateway
+        activityDrawInstructions.put(ExclusiveGateway.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                processDiagramCanvas.drawExclusiveGateway(graphicInfo, scaleFactor);
+            }
+        });
+
+        // inclusive gateway
+        activityDrawInstructions.put(InclusiveGateway.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                processDiagramCanvas.drawInclusiveGateway(graphicInfo, scaleFactor);
+            }
+        });
+
+        // parallel gateway
+        activityDrawInstructions.put(ParallelGateway.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                processDiagramCanvas.drawParallelGateway(graphicInfo, scaleFactor);
+            }
+        });
+
+        // event based gateway
+        activityDrawInstructions.put(EventGateway.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                processDiagramCanvas.drawEventBasedGateway(graphicInfo, scaleFactor);
+            }
+        });
+
+        // Boundary timer
+        activityDrawInstructions.put(BoundaryEvent.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                BoundaryEvent boundaryEvent = (BoundaryEvent) flowNode;
+                if (boundaryEvent.getEventDefinitions() != null && !boundaryEvent.getEventDefinitions().isEmpty()) {
+                    EventDefinition eventDefinition = boundaryEvent.getEventDefinitions().get(0);
+                    if (eventDefinition instanceof TimerEventDefinition) {
+                        processDiagramCanvas.drawCatchingTimerEvent(flowNode.getName(), graphicInfo, boundaryEvent.isCancelActivity(), scaleFactor);
+
+                    } else if (eventDefinition instanceof ConditionalEventDefinition) {
+                        processDiagramCanvas.drawCatchingConditionalEvent(graphicInfo, boundaryEvent.isCancelActivity(), scaleFactor);
+
+                    } else if (eventDefinition instanceof ErrorEventDefinition) {
+                        processDiagramCanvas.drawCatchingErrorEvent(graphicInfo, boundaryEvent.isCancelActivity(), scaleFactor);
+
+                    } else if (eventDefinition instanceof EscalationEventDefinition) {
+                        processDiagramCanvas.drawCatchingEscalationEvent(graphicInfo, boundaryEvent.isCancelActivity(), scaleFactor);
+
+                    } else if (eventDefinition instanceof SignalEventDefinition) {
+                        processDiagramCanvas.drawCatchingSignalEvent(flowNode.getName(), graphicInfo, boundaryEvent.isCancelActivity(), scaleFactor);
+
+                    } else if (eventDefinition instanceof MessageEventDefinition) {
+                        processDiagramCanvas.drawCatchingMessageEvent(flowNode.getName(), graphicInfo, boundaryEvent.isCancelActivity(), scaleFactor);
+
+                    } else if (eventDefinition instanceof CompensateEventDefinition) {
+                        processDiagramCanvas.drawCatchingCompensateEvent(graphicInfo, boundaryEvent.isCancelActivity(), scaleFactor);
+                    }
+
+                } else {
+                    List<ExtensionElement> eventTypeElements = boundaryEvent.getExtensionElements().get("eventType");
+                    if (eventTypeElements != null && eventTypeElements.size() > 0) {
+                        processDiagramCanvas.drawCatchingEventRegistryEvent(flowNode.getName(), graphicInfo, boundaryEvent.isCancelActivity(), scaleFactor);
+                    }
+                }
+            }
+        });
+
+        // subprocess
+        activityDrawInstructions.put(SubProcess.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                if (graphicInfo.getExpanded() != null && !graphicInfo.getExpanded()) {
+                    processDiagramCanvas.drawCollapsedSubProcess(flowNode.getName(), graphicInfo, false, scaleFactor);
+                } else {
+                    processDiagramCanvas.drawExpandedSubProcess(flowNode.getName(), graphicInfo, false, scaleFactor);
+                }
+            }
+        });
+
+        // transaction
+        activityDrawInstructions.put(Transaction.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                if (graphicInfo.getExpanded() != null && !graphicInfo.getExpanded()) {
+                    processDiagramCanvas.drawCollapsedSubProcess(flowNode.getName(), graphicInfo, false, scaleFactor);
+                } else {
+                    processDiagramCanvas.drawExpandedTransaction(flowNode.getName(), graphicInfo, scaleFactor);
+                }
+            }
+        });
+
+        // Event subprocess
+        activityDrawInstructions.put(EventSubProcess.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                if (graphicInfo.getExpanded() != null && !graphicInfo.getExpanded()) {
+                    processDiagramCanvas.drawCollapsedSubProcess(flowNode.getName(), graphicInfo, true, scaleFactor);
+                } else {
+                    processDiagramCanvas.drawExpandedSubProcess(flowNode.getName(), graphicInfo, true, scaleFactor);
+                }
+            }
+        });
+
+        // Adhoc subprocess
+        activityDrawInstructions.put(AdhocSubProcess.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                if (graphicInfo.getExpanded() != null && !graphicInfo.getExpanded()) {
+                    processDiagramCanvas.drawCollapsedSubProcess(flowNode.getName(), graphicInfo, false, scaleFactor);
+                } else {
+                    processDiagramCanvas.drawExpandedSubProcess(flowNode.getName(), graphicInfo, false, scaleFactor);
+                }
+            }
+        });
+
+        // call activity
+        activityDrawInstructions.put(CallActivity.class, new ActivityDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+                processDiagramCanvas.drawCollapsedCallActivity(flowNode.getName(), graphicInfo, scaleFactor);
+            }
+        });
+
+        // text annotation
+        artifactDrawInstructions.put(TextAnnotation.class, new ArtifactDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, Artifact artifact) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(artifact.getId());
+                TextAnnotation textAnnotation = (TextAnnotation) artifact;
+                processDiagramCanvas.drawTextAnnotation(textAnnotation.getText(), graphicInfo, scaleFactor);
+            }
+        });
+
+        // association
+        artifactDrawInstructions.put(Association.class, new ArtifactDrawInstruction() {
+
+            @Override
+            public void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, Artifact artifact) {
+                Association association = (Association) artifact;
+                String sourceRef = association.getSourceRef();
+                String targetRef = association.getTargetRef();
+
+                // source and target can be instance of FlowElement or Artifact
+                BaseElement sourceElement = bpmnModel.getFlowElement(sourceRef);
+                BaseElement targetElement = bpmnModel.getFlowElement(targetRef);
+                if (sourceElement == null) {
+                    sourceElement = bpmnModel.getArtifact(sourceRef);
+                }
+                if (targetElement == null) {
+                    targetElement = bpmnModel.getArtifact(targetRef);
+                }
+                List<GraphicInfo> graphicInfoList = bpmnModel.getFlowLocationGraphicInfo(artifact.getId());
+                graphicInfoList = connectionPerfectionizer(processDiagramCanvas, bpmnModel, sourceElement, targetElement, graphicInfoList);
+                int[] xPoints = new int[graphicInfoList.size()];
+                int[] yPoints = new int[graphicInfoList.size()];
+                for (int i = 1; i < graphicInfoList.size(); i++) {
+                    GraphicInfo graphicInfo = graphicInfoList.get(i);
+                    GraphicInfo previousGraphicInfo = graphicInfoList.get(i - 1);
+
+                    if (i == 1) {
+                        xPoints[0] = (int) previousGraphicInfo.getX();
+                        yPoints[0] = (int) previousGraphicInfo.getY();
+                    }
+                    xPoints[i] = (int) graphicInfo.getX();
+                    yPoints[i] = (int) graphicInfo.getY();
+                }
+
+                AssociationDirection associationDirection = association.getAssociationDirection();
+                processDiagramCanvas.drawAssociation(xPoints, yPoints, associationDirection, false, scaleFactor);
+            }
+        });
+    }
+
+    @Override
+    public InputStream generateDiagram(BpmnModel bpmnModel, String imageType, List<String> highLightedActivities, List<String> highLightedFlows,
+                                       String activityFontName, String labelFontName, String annotationFontName, ClassLoader customClassLoader, double scaleFactor, boolean drawSequenceFlowNameWithNoLabelDI) {
+
+        return generateProcessDiagram(bpmnModel, imageType, highLightedActivities, highLightedFlows,
+            activityFontName, labelFontName, annotationFontName, customClassLoader, scaleFactor, drawSequenceFlowNameWithNoLabelDI).generateImage(imageType);
+    }
+
+    @Override
+    public InputStream generateDiagram(BpmnModel bpmnModel, String imageType, List<String> highLightedActivities, List<String> highLightedFlows, boolean drawSequenceFlowNameWithNoLabelDI) {
+        return generateDiagram(bpmnModel, imageType, highLightedActivities, highLightedFlows, null, null, null, null, 1.0, drawSequenceFlowNameWithNoLabelDI);
+    }
+
+    @Override
+    public InputStream generateDiagram(BpmnModel bpmnModel, String imageType,
+                                       List<String> highLightedActivities, List<String> highLightedFlows, double scaleFactor, boolean drawSequenceFlowNameWithNoLabelDI) {
+        return generateDiagram(bpmnModel, imageType, highLightedActivities, highLightedFlows, null, null, null, null, scaleFactor, drawSequenceFlowNameWithNoLabelDI);
+    }
+
+    @Override
+    public InputStream generateDiagram(BpmnModel bpmnModel, String imageType, List<String> highLightedActivities, boolean drawSequenceFlowNameWithNoLabelDI) {
+        return generateDiagram(bpmnModel, imageType, highLightedActivities, Collections.emptyList(), drawSequenceFlowNameWithNoLabelDI);
+    }
+
+    @Override
+    public InputStream generateDiagram(BpmnModel bpmnModel, String imageType, List<String> highLightedActivities, double scaleFactor, boolean drawSequenceFlowNameWithNoLabelDI) {
+        return generateDiagram(bpmnModel, imageType, highLightedActivities, Collections.emptyList(), scaleFactor, drawSequenceFlowNameWithNoLabelDI);
+    }
+
+    @Override
+    public InputStream generateDiagram(BpmnModel bpmnModel, String imageType, String activityFontName,
+                                       String labelFontName, String annotationFontName, ClassLoader customClassLoader, boolean drawSequenceFlowNameWithNoLabelDI) {
+
+        return generateDiagram(bpmnModel, imageType, Collections.emptyList(), Collections.emptyList(),
+            activityFontName, labelFontName, annotationFontName, customClassLoader, 1.0, drawSequenceFlowNameWithNoLabelDI);
+    }
+
+    @Override
+    public InputStream generateDiagram(BpmnModel bpmnModel, String imageType, String activityFontName,
+                                       String labelFontName, String annotationFontName, ClassLoader customClassLoader, double scaleFactor, boolean drawSequenceFlowNameWithNoLabelDI) {
+
+        return generateDiagram(bpmnModel, imageType, Collections.emptyList(), Collections.emptyList(),
+            activityFontName, labelFontName, annotationFontName, customClassLoader, scaleFactor, drawSequenceFlowNameWithNoLabelDI);
+    }
+
+    @Override
+    public InputStream generatePngDiagram(BpmnModel bpmnModel, boolean drawSequenceFlowNameWithNoLabelDI) {
+        return generatePngDiagram(bpmnModel, 1.0, drawSequenceFlowNameWithNoLabelDI);
+    }
+
+    @Override
+    public InputStream generatePngDiagram(BpmnModel bpmnModel, double scaleFactor, boolean drawSequenceFlowNameWithNoLabelDI) {
+        return generateDiagram(bpmnModel, "png", Collections.emptyList(), Collections.emptyList(), scaleFactor, drawSequenceFlowNameWithNoLabelDI);
+    }
+
+    @Override
+    public InputStream generateJpgDiagram(BpmnModel bpmnModel) {
+        return generateJpgDiagram(bpmnModel, 1.0, false);
+    }
+
+    @Override
+    public InputStream generateJpgDiagram(BpmnModel bpmnModel, double scaleFactor, boolean drawSequenceFlowNameWithNoLabelDI) {
+        return generateDiagram(bpmnModel, "jpg", Collections.emptyList(), Collections.emptyList(), drawSequenceFlowNameWithNoLabelDI);
+    }
+
+    public BufferedImage generateImage(BpmnModel bpmnModel, String imageType, List<String> highLightedActivities, List<String> highLightedFlows,
+                                       String activityFontName, String labelFontName, String annotationFontName, ClassLoader customClassLoader, double scaleFactor, boolean drawSequenceFlowNameWithNoLabelDI) {
+
+        return generateProcessDiagram(bpmnModel, imageType, highLightedActivities, highLightedFlows,
+            activityFontName, labelFontName, annotationFontName, customClassLoader, scaleFactor, drawSequenceFlowNameWithNoLabelDI).generateBufferedImage(imageType);
+    }
+
+    public BufferedImage generateImage(BpmnModel bpmnModel, String imageType,
+                                       List<String> highLightedActivities, List<String> highLightedFlows, double scaleFactor, boolean drawSequenceFlowNameWithNoLabelDI) {
+
+        return generateImage(bpmnModel, imageType, highLightedActivities, highLightedFlows, null, null, null, null, scaleFactor, drawSequenceFlowNameWithNoLabelDI);
+    }
+
+    @Override
+    public BufferedImage generatePngImage(BpmnModel bpmnModel, double scaleFactor) {
+        return generateImage(bpmnModel, "png", Collections.emptyList(), Collections.emptyList(), scaleFactor, false);
+    }
+
+    protected CustomDefaultProcessDiagramCanvas generateProcessDiagram(BpmnModel bpmnModel, String imageType,
+                                                                       List<String> highLightedActivities, List<String> highLightedFlows,
+                                                                       String activityFontName, String labelFontName, String annotationFontName, ClassLoader customClassLoader, double scaleFactor, boolean drawSequenceFlowNameWithNoLabelDI) {
+
+        prepareBpmnModel(bpmnModel);
+
+        CustomDefaultProcessDiagramCanvas processDiagramCanvas = initProcessDiagramCanvas(bpmnModel, imageType, activityFontName, labelFontName, annotationFontName, customClassLoader);
+
+        // Draw pool shape, if process is participant in collaboration
+        for (Pool pool : bpmnModel.getPools()) {
+            GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(pool.getId());
+            processDiagramCanvas.drawPoolOrLane(pool.getName(), graphicInfo, scaleFactor);
+        }
+
+        // Draw lanes
+        for (Process process : bpmnModel.getProcesses()) {
+            for (Lane lane : process.getLanes()) {
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(lane.getId());
+                processDiagramCanvas.drawPoolOrLane(lane.getName(), graphicInfo, scaleFactor);
+            }
+        }
+
+        // Draw activities and their sequence-flows
+        for (Process process : bpmnModel.getProcesses()) {
+            for (FlowNode flowNode : process.findFlowElementsOfType(FlowNode.class)) {
+                if (!isPartOfCollapsedSubProcess(flowNode, bpmnModel)) {
+                    drawActivity(processDiagramCanvas, bpmnModel, flowNode, highLightedActivities, highLightedFlows, scaleFactor, drawSequenceFlowNameWithNoLabelDI);
+                }
+            }
+        }
+
+        // Draw artifacts
+        for (Process process : bpmnModel.getProcesses()) {
+
+            for (Artifact artifact : process.getArtifacts()) {
+                drawArtifact(processDiagramCanvas, bpmnModel, artifact);
+            }
+
+            List<SubProcess> subProcesses = process.findFlowElementsOfType(SubProcess.class, true);
+            if (subProcesses != null) {
+                for (SubProcess subProcess : subProcesses) {
+
+                    GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(subProcess.getId());
+                    if (graphicInfo != null && graphicInfo.getExpanded() != null && !graphicInfo.getExpanded()) {
+                        continue;
+                    }
+
+                    if (!isPartOfCollapsedSubProcess(subProcess, bpmnModel)) {
+                        for (Artifact subProcessArtifact : subProcess.getArtifacts()) {
+                            drawArtifact(processDiagramCanvas, bpmnModel, subProcessArtifact);
+                        }
+                    }
+                }
+            }
+        }
+
+        return processDiagramCanvas;
+    }
+
+    protected void prepareBpmnModel(BpmnModel bpmnModel) {
+
+        // Need to make sure all elements have positive x and y.
+        // Check all graphicInfo and update the elements accordingly
+
+        List<GraphicInfo> allGraphicInfos = new ArrayList<>();
+        if (bpmnModel.getLocationMap() != null) {
+            allGraphicInfos.addAll(bpmnModel.getLocationMap().values());
+        }
+        if (bpmnModel.getLabelLocationMap() != null) {
+            allGraphicInfos.addAll(bpmnModel.getLabelLocationMap().values());
+        }
+        if (bpmnModel.getFlowLocationMap() != null) {
+            for (List<GraphicInfo> flowGraphicInfos : bpmnModel.getFlowLocationMap().values()) {
+                allGraphicInfos.addAll(flowGraphicInfos);
+            }
+        }
+
+        if (allGraphicInfos.size() > 0) {
+
+            boolean needsTranslationX = false;
+            boolean needsTranslationY = false;
+
+            double lowestX = 0.0;
+            double lowestY = 0.0;
+
+            // Collect lowest x and y
+            for (GraphicInfo graphicInfo : allGraphicInfos) {
+
+                double x = graphicInfo.getX();
+                double y = graphicInfo.getY();
+
+                if (x < lowestX) {
+                    needsTranslationX = true;
+                    lowestX = x;
+                }
+                if (y < lowestY) {
+                    needsTranslationY = true;
+                    lowestY = y;
+                }
+
+            }
+
+            // Update all graphicInfo objects
+            if (needsTranslationX || needsTranslationY) {
+
+                double translationX = Math.abs(lowestX);
+                double translationY = Math.abs(lowestY);
+
+                for (GraphicInfo graphicInfo : allGraphicInfos) {
+                    if (needsTranslationX) {
+                        graphicInfo.setX(graphicInfo.getX() + translationX);
+                    }
+                    if (needsTranslationY) {
+                        graphicInfo.setY(graphicInfo.getY() + translationY);
+                    }
+                }
+            }
+
+        }
+
+    }
+
+    protected void drawActivity(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel,
+                                FlowNode flowNode, List<String> highLightedActivities, List<String> highLightedFlows, double scaleFactor, Boolean drawSequenceFlowNameWithNoLabelDI) {
+
+        ActivityDrawInstruction drawInstruction = activityDrawInstructions.get(flowNode.getClass());
+        if (drawInstruction != null) {
+
+            drawInstruction.draw(processDiagramCanvas, bpmnModel, flowNode);
+
+            // Gather info on the multi instance marker
+            boolean multiInstanceSequential = false;
+            boolean multiInstanceParallel = false;
+            boolean collapsed = false;
+            if (flowNode instanceof Activity) {
+                Activity activity = (Activity) flowNode;
+                MultiInstanceLoopCharacteristics multiInstanceLoopCharacteristics = activity.getLoopCharacteristics();
+                if (multiInstanceLoopCharacteristics != null) {
+                    multiInstanceSequential = multiInstanceLoopCharacteristics.isSequential();
+                    multiInstanceParallel = !multiInstanceSequential;
+                }
+            }
+
+            // Gather info on the collapsed marker
+            GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+            if (flowNode instanceof SubProcess) {
+                collapsed = graphicInfo.getExpanded() != null && !graphicInfo.getExpanded();
+            } else if (flowNode instanceof CallActivity) {
+                collapsed = true;
+            }
+
+            if (scaleFactor == 1.0) {
+                // Actually draw the markers
+                processDiagramCanvas.drawActivityMarkers((int) graphicInfo.getX(), (int) graphicInfo.getY(), (int) graphicInfo.getWidth(), (int) graphicInfo.getHeight(),
+                    multiInstanceSequential, multiInstanceParallel, collapsed);
+            }
+
+            // Draw highlighted activities
+            if (highLightedActivities.contains(flowNode.getId())) {
+                drawHighLightRed(processDiagramCanvas, bpmnModel.getGraphicInfo(flowNode.getId()));
+            } else if (highLightedActivities.contains(Color.RED.toString() + flowNode.getId())) {
+                drawHighLight(processDiagramCanvas, bpmnModel.getGraphicInfo(flowNode.getId()));
+            }
+
+        } else if (flowNode instanceof Task) {
+            activityDrawInstructions.get(Task.class).draw(processDiagramCanvas, bpmnModel, flowNode);
+
+            if (highLightedActivities.contains(flowNode.getId())) {
+                drawHighLightRed(processDiagramCanvas, bpmnModel.getGraphicInfo(flowNode.getId()));
+            } else if (highLightedActivities.contains(Color.RED.toString() + flowNode.getId())) {
+                drawHighLight(processDiagramCanvas, bpmnModel.getGraphicInfo(flowNode.getId()));
+            }
+        }
+
+        // Outgoing transitions of activity
+        for (SequenceFlow sequenceFlow : flowNode.getOutgoingFlows()) {
+            boolean highLighted = (highLightedFlows.contains(sequenceFlow.getId()));
+            String defaultFlow = null;
+            if (flowNode instanceof Activity) {
+                defaultFlow = ((Activity) flowNode).getDefaultFlow();
+            } else if (flowNode instanceof Gateway) {
+                defaultFlow = ((Gateway) flowNode).getDefaultFlow();
+            }
+
+            boolean isDefault = false;
+            if (defaultFlow != null && defaultFlow.equalsIgnoreCase(sequenceFlow.getId())) {
+                isDefault = true;
+            }
+            boolean drawConditionalIndicator = sequenceFlow.getConditionExpression() != null && sequenceFlow.getConditionExpression().trim().length() > 0 && !(flowNode instanceof Gateway);
+
+            String sourceRef = sequenceFlow.getSourceRef();
+            String targetRef = sequenceFlow.getTargetRef();
+            FlowElement sourceElement = bpmnModel.getFlowElement(sourceRef);
+            FlowElement targetElement = bpmnModel.getFlowElement(targetRef);
+            List<GraphicInfo> graphicInfoList = bpmnModel.getFlowLocationGraphicInfo(sequenceFlow.getId());
+            if (graphicInfoList != null && graphicInfoList.size() > 0) {
+                graphicInfoList = connectionPerfectionizer(processDiagramCanvas, bpmnModel, sourceElement, targetElement, graphicInfoList);
+                int[] xPoints = new int[graphicInfoList.size()];
+                int[] yPoints = new int[graphicInfoList.size()];
+
+                for (int i = 1; i < graphicInfoList.size(); i++) {
+                    GraphicInfo graphicInfo = graphicInfoList.get(i);
+                    GraphicInfo previousGraphicInfo = graphicInfoList.get(i - 1);
+
+                    if (i == 1) {
+                        xPoints[0] = (int) previousGraphicInfo.getX();
+                        yPoints[0] = (int) previousGraphicInfo.getY();
+                    }
+                    xPoints[i] = (int) graphicInfo.getX();
+                    yPoints[i] = (int) graphicInfo.getY();
+
+                }
+
+                processDiagramCanvas.drawSequenceflow(xPoints, yPoints, drawConditionalIndicator, isDefault, highLighted, scaleFactor);
+
+                // Draw sequenceflow label
+                GraphicInfo labelGraphicInfo = bpmnModel.getLabelGraphicInfo(sequenceFlow.getId());
+                if (labelGraphicInfo != null) {
+                    processDiagramCanvas.drawLabel(sequenceFlow.getName(), labelGraphicInfo, false);
+                } else {
+                    if (drawSequenceFlowNameWithNoLabelDI) {
+                        GraphicInfo lineCenter = getLineCenter(graphicInfoList);
+                        processDiagramCanvas.drawLabel(sequenceFlow.getName(), lineCenter, false);
+                    }
+
+                }
+            }
+        }
+
+        // Nested elements
+        if (flowNode instanceof FlowElementsContainer) {
+            for (FlowElement nestedFlowElement : ((FlowElementsContainer) flowNode).getFlowElements()) {
+                if (nestedFlowElement instanceof FlowNode && !isPartOfCollapsedSubProcess(nestedFlowElement, bpmnModel)) {
+                    drawActivity(processDiagramCanvas, bpmnModel, (FlowNode) nestedFlowElement,
+                        highLightedActivities, highLightedFlows, scaleFactor, drawSequenceFlowNameWithNoLabelDI);
+                }
+            }
+        }
+    }
+
+    /**
+     * This method makes coordinates of connection flow better.
+     *
+     * @param processDiagramCanvas
+     * @param bpmnModel
+     * @param sourceElement
+     * @param targetElement
+     * @param graphicInfoList
+     * @return
+     */
+    protected static List<GraphicInfo> connectionPerfectionizer(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, BaseElement sourceElement, BaseElement targetElement, List<GraphicInfo> graphicInfoList) {
+        GraphicInfo sourceGraphicInfo = bpmnModel.getGraphicInfo(sourceElement.getId());
+        GraphicInfo targetGraphicInfo = bpmnModel.getGraphicInfo(targetElement.getId());
+
+        CustomDefaultProcessDiagramCanvas.SHAPE_TYPE sourceShapeType = getShapeType(sourceElement);
+        CustomDefaultProcessDiagramCanvas.SHAPE_TYPE targetShapeType = getShapeType(targetElement);
+
+        return processDiagramCanvas.connectionPerfectionizer(sourceShapeType, targetShapeType, sourceGraphicInfo, targetGraphicInfo, graphicInfoList);
+    }
+
+    /**
+     * This method returns shape type of base element.<br>
+     * Each element can be presented as rectangle, rhombus, or ellipse.
+     *
+     * @param baseElement
+     * @return CustomDefaultProcessDiagramCanvas.SHAPE_TYPE
+     */
+    protected static CustomDefaultProcessDiagramCanvas.SHAPE_TYPE getShapeType(BaseElement baseElement) {
+        if (baseElement instanceof Task || baseElement instanceof Activity || baseElement instanceof TextAnnotation) {
+            return CustomDefaultProcessDiagramCanvas.SHAPE_TYPE.Rectangle;
+        } else if (baseElement instanceof Gateway) {
+            return CustomDefaultProcessDiagramCanvas.SHAPE_TYPE.Rhombus;
+        } else if (baseElement instanceof Event) {
+            return CustomDefaultProcessDiagramCanvas.SHAPE_TYPE.Ellipse;
+        } else {
+            // unknown source element, just do not correct coordinates
+        }
+        return null;
+    }
+
+    protected static GraphicInfo getLineCenter(List<GraphicInfo> graphicInfoList) {
+        GraphicInfo gi = new GraphicInfo();
+
+        int[] xPoints = new int[graphicInfoList.size()];
+        int[] yPoints = new int[graphicInfoList.size()];
+
+        double length = 0;
+        double[] lengths = new double[graphicInfoList.size()];
+        lengths[0] = 0;
+        double m;
+        for (int i = 1; i < graphicInfoList.size(); i++) {
+            GraphicInfo graphicInfo = graphicInfoList.get(i);
+            GraphicInfo previousGraphicInfo = graphicInfoList.get(i - 1);
+
+            if (i == 1) {
+                xPoints[0] = (int) previousGraphicInfo.getX();
+                yPoints[0] = (int) previousGraphicInfo.getY();
+            }
+            xPoints[i] = (int) graphicInfo.getX();
+            yPoints[i] = (int) graphicInfo.getY();
+
+            length += Math.sqrt(
+                Math.pow((int) graphicInfo.getX() - (int) previousGraphicInfo.getX(), 2) +
+                    Math.pow((int) graphicInfo.getY() - (int) previousGraphicInfo.getY(), 2));
+            lengths[i] = length;
+        }
+        m = length / 2;
+        int p1 = 0;
+        int p2 = 1;
+        for (int i = 1; i < lengths.length; i++) {
+            double len = lengths[i];
+            p1 = i - 1;
+            p2 = i;
+            if (len > m) {
+                break;
+            }
+        }
+
+        GraphicInfo graphicInfo1 = graphicInfoList.get(p1);
+        GraphicInfo graphicInfo2 = graphicInfoList.get(p2);
+
+        double AB = (int) graphicInfo2.getX() - (int) graphicInfo1.getX();
+        double OA = (int) graphicInfo2.getY() - (int) graphicInfo1.getY();
+        double OB = lengths[p2] - lengths[p1];
+        double ob = m - lengths[p1];
+        double ab = AB * ob / OB;
+        double oa = OA * ob / OB;
+
+        double mx = graphicInfo1.getX() + ab;
+        double my = graphicInfo1.getY() + oa;
+
+        gi.setX(mx);
+        gi.setY(my);
+        return gi;
+    }
+
+    protected void drawArtifact(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, Artifact artifact) {
+
+        ArtifactDrawInstruction drawInstruction = artifactDrawInstructions.get(artifact.getClass());
+        if (drawInstruction != null) {
+            drawInstruction.draw(processDiagramCanvas, bpmnModel, artifact);
+        }
+    }
+
+    private static void drawHighLight(CustomDefaultProcessDiagramCanvas processDiagramCanvas, GraphicInfo graphicInfo) {
+        processDiagramCanvas.drawHighLight((int) graphicInfo.getX(), (int) graphicInfo.getY(), (int) graphicInfo.getWidth(), (int) graphicInfo.getHeight());
+
+    }
+
+    private static void drawHighLightRed(CustomDefaultProcessDiagramCanvas processDiagramCanvas, GraphicInfo graphicInfo) {
+        processDiagramCanvas.drawHighLightRed((int) graphicInfo.getX(), (int) graphicInfo.getY(), (int) graphicInfo.getWidth(), (int) graphicInfo.getHeight());
+
+    }
+
+    protected static CustomDefaultProcessDiagramCanvas initProcessDiagramCanvas(BpmnModel bpmnModel, String imageType,
+                                                                                String activityFontName, String labelFontName, String annotationFontName, ClassLoader customClassLoader) {
+
+        // We need to calculate maximum values to know how big the image will be in its entirety
+        double minX = Double.MAX_VALUE;
+        double maxX = 0;
+        double minY = Double.MAX_VALUE;
+        double maxY = 0;
+
+        for (Pool pool : bpmnModel.getPools()) {
+            GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(pool.getId());
+            minX = graphicInfo.getX();
+            maxX = graphicInfo.getX() + graphicInfo.getWidth();
+            minY = graphicInfo.getY();
+            maxY = graphicInfo.getY() + graphicInfo.getHeight();
+        }
+
+        List<FlowNode> flowNodes = gatherAllFlowNodes(bpmnModel);
+        for (FlowNode flowNode : flowNodes) {
+
+            GraphicInfo flowNodeGraphicInfo = bpmnModel.getGraphicInfo(flowNode.getId());
+
+            // width
+            if (flowNodeGraphicInfo.getX() + flowNodeGraphicInfo.getWidth() > maxX) {
+                maxX = flowNodeGraphicInfo.getX() + flowNodeGraphicInfo.getWidth();
+            }
+            if (flowNodeGraphicInfo.getX() < minX) {
+                minX = flowNodeGraphicInfo.getX();
+            }
+            // height
+            if (flowNodeGraphicInfo.getY() + flowNodeGraphicInfo.getHeight() > maxY) {
+                maxY = flowNodeGraphicInfo.getY() + flowNodeGraphicInfo.getHeight();
+            }
+            if (flowNodeGraphicInfo.getY() < minY) {
+                minY = flowNodeGraphicInfo.getY();
+            }
+
+            for (SequenceFlow sequenceFlow : flowNode.getOutgoingFlows()) {
+                List<GraphicInfo> graphicInfoList = bpmnModel.getFlowLocationGraphicInfo(sequenceFlow.getId());
+                if (graphicInfoList != null) {
+                    for (GraphicInfo graphicInfo : graphicInfoList) {
+                        // width
+                        if (graphicInfo.getX() > maxX) {
+                            maxX = graphicInfo.getX();
+                        }
+                        if (graphicInfo.getX() < minX) {
+                            minX = graphicInfo.getX();
+                        }
+                        // height
+                        if (graphicInfo.getY() > maxY) {
+                            maxY = graphicInfo.getY();
+                        }
+                        if (graphicInfo.getY() < minY) {
+                            minY = graphicInfo.getY();
+                        }
+                    }
+                }
+            }
+        }
+
+        List<Artifact> artifacts = gatherAllArtifacts(bpmnModel);
+        for (Artifact artifact : artifacts) {
+
+            GraphicInfo artifactGraphicInfo = bpmnModel.getGraphicInfo(artifact.getId());
+
+            if (artifactGraphicInfo != null) {
+                // width
+                if (artifactGraphicInfo.getX() + artifactGraphicInfo.getWidth() > maxX) {
+                    maxX = artifactGraphicInfo.getX() + artifactGraphicInfo.getWidth();
+                }
+                if (artifactGraphicInfo.getX() < minX) {
+                    minX = artifactGraphicInfo.getX();
+                }
+                // height
+                if (artifactGraphicInfo.getY() + artifactGraphicInfo.getHeight() > maxY) {
+                    maxY = artifactGraphicInfo.getY() + artifactGraphicInfo.getHeight();
+                }
+                if (artifactGraphicInfo.getY() < minY) {
+                    minY = artifactGraphicInfo.getY();
+                }
+            }
+
+            List<GraphicInfo> graphicInfoList = bpmnModel.getFlowLocationGraphicInfo(artifact.getId());
+            if (graphicInfoList != null) {
+                for (GraphicInfo graphicInfo : graphicInfoList) {
+                    // width
+                    if (graphicInfo.getX() > maxX) {
+                        maxX = graphicInfo.getX();
+                    }
+                    if (graphicInfo.getX() < minX) {
+                        minX = graphicInfo.getX();
+                    }
+                    // height
+                    if (graphicInfo.getY() > maxY) {
+                        maxY = graphicInfo.getY();
+                    }
+                    if (graphicInfo.getY() < minY) {
+                        minY = graphicInfo.getY();
+                    }
+                }
+            }
+        }
+
+        int nrOfLanes = 0;
+        for (Process process : bpmnModel.getProcesses()) {
+            for (Lane l : process.getLanes()) {
+
+                nrOfLanes++;
+
+                GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(l.getId());
+                // // width
+                if (graphicInfo.getX() + graphicInfo.getWidth() > maxX) {
+                    maxX = graphicInfo.getX() + graphicInfo.getWidth();
+                }
+                if (graphicInfo.getX() < minX) {
+                    minX = graphicInfo.getX();
+                }
+                // height
+                if (graphicInfo.getY() + graphicInfo.getHeight() > maxY) {
+                    maxY = graphicInfo.getY() + graphicInfo.getHeight();
+                }
+                if (graphicInfo.getY() < minY) {
+                    minY = graphicInfo.getY();
+                }
+            }
+        }
+
+        // Special case, see https://activiti.atlassian.net/browse/ACT-1431
+        if (flowNodes.isEmpty() && bpmnModel.getPools().isEmpty() && nrOfLanes == 0) {
+            // Nothing to show
+            minX = 0;
+            minY = 0;
+        }
+
+        return new CustomDefaultProcessDiagramCanvas((int) maxX + 10, (int) maxY + 10, (int) minX, (int) minY,
+            imageType, activityFontName, labelFontName, annotationFontName, customClassLoader);
+    }
+
+    protected static List<Artifact> gatherAllArtifacts(BpmnModel bpmnModel) {
+        List<Artifact> artifacts = new ArrayList<>();
+        for (Process process : bpmnModel.getProcesses()) {
+            artifacts.addAll(process.getArtifacts());
+        }
+        return artifacts;
+    }
+
+    protected static List<FlowNode> gatherAllFlowNodes(BpmnModel bpmnModel) {
+        List<FlowNode> flowNodes = new ArrayList<>();
+        for (Process process : bpmnModel.getProcesses()) {
+            flowNodes.addAll(gatherAllFlowNodes(process));
+        }
+        return flowNodes;
+    }
+
+    protected static List<FlowNode> gatherAllFlowNodes(FlowElementsContainer flowElementsContainer) {
+        List<FlowNode> flowNodes = new ArrayList<>();
+        for (FlowElement flowElement : flowElementsContainer.getFlowElements()) {
+            if (flowElement instanceof FlowNode) {
+                flowNodes.add((FlowNode) flowElement);
+            }
+            if (flowElement instanceof FlowElementsContainer) {
+                flowNodes.addAll(gatherAllFlowNodes((FlowElementsContainer) flowElement));
+            }
+        }
+        return flowNodes;
+    }
+
+    protected boolean isPartOfCollapsedSubProcess(FlowElement flowElement, BpmnModel model) {
+        SubProcess subProcess = flowElement.getSubProcess();
+        if (subProcess != null) {
+            GraphicInfo graphicInfo = model.getGraphicInfo(subProcess.getId());
+            if (graphicInfo != null && graphicInfo.getExpanded() != null && !graphicInfo.getExpanded()) {
+                return true;
+            }
+
+            return isPartOfCollapsedSubProcess(subProcess, model);
+        }
+
+        return false;
+    }
+
+    public Map<Class<? extends BaseElement>, ActivityDrawInstruction> getActivityDrawInstructions() {
+        return activityDrawInstructions;
+    }
+
+    public void setActivityDrawInstructions(
+        Map<Class<? extends BaseElement>, ActivityDrawInstruction> activityDrawInstructions) {
+        this.activityDrawInstructions = activityDrawInstructions;
+    }
+
+    public Map<Class<? extends BaseElement>, ArtifactDrawInstruction> getArtifactDrawInstructions() {
+        return artifactDrawInstructions;
+    }
+
+    public void setArtifactDrawInstructions(
+        Map<Class<? extends BaseElement>, ArtifactDrawInstruction> artifactDrawInstructions) {
+        this.artifactDrawInstructions = artifactDrawInstructions;
+    }
+
+    protected interface ActivityDrawInstruction {
+        void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, FlowNode flowNode);
+    }
+
+    protected interface ArtifactDrawInstruction {
+        void draw(CustomDefaultProcessDiagramCanvas processDiagramCanvas, BpmnModel bpmnModel, Artifact artifact);
+    }
+}

+ 61 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/cmd/AddSequenceMultiInstanceCmd.java

@@ -0,0 +1,61 @@
+package org.dromara.workflow.flowable.cmd;
+
+import cn.hutool.core.collection.CollUtil;
+import org.flowable.common.engine.impl.interceptor.Command;
+import org.flowable.common.engine.impl.interceptor.CommandContext;
+import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
+import org.flowable.engine.impl.persistence.entity.ExecutionEntityManager;
+import org.flowable.engine.impl.util.CommandContextUtil;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.dromara.workflow.common.constant.FlowConstant.NUMBER_OF_INSTANCES;
+
+/**
+ * 串行加签
+ *
+ * @author may
+ */
+public class AddSequenceMultiInstanceCmd implements Command<Void> {
+
+    /**
+     * 执行id
+     */
+    private final String executionId;
+
+    /**
+     * 会签人员集合KEY
+     */
+    private final String assigneeList;
+
+    /**
+     * 加签人员
+     */
+    private final List<Long> assignees;
+
+    public AddSequenceMultiInstanceCmd(String executionId, String assigneeList, List<Long> assignees) {
+        this.executionId = executionId;
+        this.assigneeList = assigneeList;
+        this.assignees = assignees;
+    }
+
+    @Override
+    public Void execute(CommandContext commandContext) {
+        ExecutionEntityManager executionEntityManager = CommandContextUtil.getExecutionEntityManager();
+        ExecutionEntity entity = executionEntityManager.findById(executionId);
+        // 多实例任务总数加 assignees.size()
+        if (entity.getVariable(NUMBER_OF_INSTANCES) instanceof Integer nrOfInstances) {
+            entity.setVariable(NUMBER_OF_INSTANCES, nrOfInstances + assignees.size());
+        }
+        // 设置流程变量
+        if (entity.getVariable(assigneeList) instanceof List<?> userIds) {
+            CollUtil.addAll(userIds, assignees);
+            Map<String, Object> variables = new HashMap<>(16);
+            variables.put(assigneeList, userIds);
+            entity.setVariables(variables);
+        }
+        return null;
+    }
+}

+ 68 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/cmd/AttachmentCmd.java

@@ -0,0 +1,68 @@
+package org.dromara.workflow.flowable.cmd;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.system.domain.vo.SysOssVo;
+import org.dromara.system.service.ISysOssService;
+import org.flowable.common.engine.impl.interceptor.Command;
+import org.flowable.common.engine.impl.interceptor.CommandContext;
+import org.flowable.engine.impl.persistence.entity.AttachmentEntity;
+import org.flowable.engine.impl.persistence.entity.AttachmentEntityManager;
+import org.flowable.engine.impl.util.CommandContextUtil;
+
+import java.util.Arrays;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * 串行加签
+ *
+ * @author 附件上传
+ */
+public class AttachmentCmd implements Command<Boolean> {
+
+    private final String fileId;
+
+    private final String taskId;
+
+    private final String processInstanceId;
+
+    public AttachmentCmd(String fileId, String taskId, String processInstanceId) {
+        this.fileId = fileId;
+        this.taskId = taskId;
+        this.processInstanceId = processInstanceId;
+    }
+
+    @Override
+    public Boolean execute(CommandContext commandContext) {
+        try {
+            if (StringUtils.isNotBlank(fileId)) {
+                List<Long> fileIds = StreamUtils.toList(Arrays.asList(fileId.split(StrUtil.COMMA)), Long::valueOf);
+                List<SysOssVo> sysOssVos = SpringUtils.getBean(ISysOssService.class).listByIds(fileIds);
+                if (CollUtil.isNotEmpty(sysOssVos)) {
+                    for (SysOssVo sysOssVo : sysOssVos) {
+                        AttachmentEntityManager attachmentEntityManager = CommandContextUtil.getAttachmentEntityManager();
+                        AttachmentEntity attachmentEntity = attachmentEntityManager.create();
+                        attachmentEntity.setRevision(1);
+                        attachmentEntity.setUserId(LoginHelper.getUserId().toString());
+                        attachmentEntity.setName(sysOssVo.getOriginalName());
+                        attachmentEntity.setDescription(sysOssVo.getOriginalName());
+                        attachmentEntity.setType(sysOssVo.getFileSuffix());
+                        attachmentEntity.setTaskId(taskId);
+                        attachmentEntity.setProcessInstanceId(processInstanceId);
+                        attachmentEntity.setContentId(sysOssVo.getOssId().toString());
+                        attachmentEntity.setTime(new Date());
+                        attachmentEntityManager.insert(attachmentEntity);
+                    }
+                }
+            }
+        } catch (Exception e) {
+            throw new RuntimeException(e);
+        }
+        return true;
+    }
+}

+ 36 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/cmd/DeleteExecutionCmd.java

@@ -0,0 +1,36 @@
+package org.dromara.workflow.flowable.cmd;
+
+import org.flowable.common.engine.impl.interceptor.Command;
+import org.flowable.common.engine.impl.interceptor.CommandContext;
+import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
+import org.flowable.engine.impl.persistence.entity.ExecutionEntityManager;
+import org.flowable.engine.impl.util.CommandContextUtil;
+
+import java.io.Serializable;
+
+/**
+ * 删除执行数据
+ *
+ * @author may
+ */
+public class DeleteExecutionCmd implements Command<Void>, Serializable {
+
+    /**
+     * 执行id
+     */
+    private final String executionId;
+
+    public DeleteExecutionCmd(String executionId) {
+        this.executionId = executionId;
+    }
+
+    @Override
+    public Void execute(CommandContext commandContext) {
+        ExecutionEntityManager executionEntityManager = CommandContextUtil.getExecutionEntityManager();
+        ExecutionEntity entity = executionEntityManager.findById(executionId);
+        if (entity != null) {
+            executionEntityManager.deleteExecutionAndRelatedData(entity, "", false, false);
+        }
+        return null;
+    }
+}

+ 82 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/cmd/DeleteSequenceMultiInstanceCmd.java

@@ -0,0 +1,82 @@
+package org.dromara.workflow.flowable.cmd;
+
+import cn.hutool.core.util.ObjectUtil;
+import lombok.AllArgsConstructor;
+import org.flowable.common.engine.impl.interceptor.Command;
+import org.flowable.common.engine.impl.interceptor.CommandContext;
+import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
+import org.flowable.engine.impl.persistence.entity.ExecutionEntityManager;
+import org.flowable.engine.impl.util.CommandContextUtil;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.dromara.workflow.common.constant.FlowConstant.LOOP_COUNTER;
+import static org.dromara.workflow.common.constant.FlowConstant.NUMBER_OF_INSTANCES;
+
+
+/**
+ * 串行减签
+ *
+ * @author may
+ */
+@AllArgsConstructor
+public class DeleteSequenceMultiInstanceCmd implements Command<Void> {
+
+    /**
+     * 当前节点审批人员id
+     */
+    private final String currentUserId;
+
+    /**
+     * 执行id
+     */
+    private final String executionId;
+
+    /**
+     * 会签人员集合KEY
+     */
+    private final String assigneeList;
+
+    /**
+     * 减签人员
+     */
+    private final List<Long> assignees;
+
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public Void execute(CommandContext commandContext) {
+        ExecutionEntityManager executionEntityManager = CommandContextUtil.getExecutionEntityManager();
+        ExecutionEntity entity = executionEntityManager.findById(executionId);
+        // 设置流程变量
+        List<Long> userIds = new ArrayList<>();
+        List<Object> variable = (List<Object>) entity.getVariable(assigneeList);
+        for (Object o : variable) {
+            userIds.add(Long.valueOf(o.toString()));
+        }
+        List<Long> userIdList = new ArrayList<>();
+        userIds.forEach(e -> {
+            Long userId = assignees.stream().filter(id -> ObjectUtil.equals(id, e)).findFirst().orElse(null);
+            if (userId == null) {
+                userIdList.add(e);
+            }
+        });
+        // 当前任务执行位置
+        int loopCounterIndex = -1;
+        for (int i = 0; i < userIdList.size(); i++) {
+            Long userId = userIdList.get(i);
+            if (currentUserId.equals(userId.toString())) {
+                loopCounterIndex = i;
+            }
+        }
+        Map<String, Object> variables = new HashMap<>(16);
+        variables.put(NUMBER_OF_INSTANCES, userIdList.size());
+        variables.put(assigneeList, userIdList);
+        variables.put(LOOP_COUNTER, loopCounterIndex);
+        entity.setVariables(variables);
+        return null;
+    }
+}

+ 39 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/cmd/ExecutionChildByExecutionIdCmd.java

@@ -0,0 +1,39 @@
+package org.dromara.workflow.flowable.cmd;
+
+import org.dromara.common.core.utils.StreamUtils;
+import org.flowable.common.engine.impl.interceptor.Command;
+import org.flowable.common.engine.impl.interceptor.CommandContext;
+import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
+import org.flowable.engine.impl.persistence.entity.ExecutionEntityManager;
+import org.flowable.engine.impl.util.CommandContextUtil;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 获取并行网关执行后保留的执行实例数据
+ *
+ * @author may
+ */
+public class ExecutionChildByExecutionIdCmd implements Command<List<ExecutionEntity>>, Serializable {
+
+    /**
+     * 当前任务执行实例id
+     */
+    private final String executionId;
+
+    public ExecutionChildByExecutionIdCmd(String executionId) {
+        this.executionId = executionId;
+    }
+
+    @Override
+    public List<ExecutionEntity> execute(CommandContext commandContext) {
+        ExecutionEntityManager executionEntityManager = CommandContextUtil.getExecutionEntityManager();
+        // 获取当前执行数据
+        ExecutionEntity executionEntity = executionEntityManager.findById(executionId);
+        // 通过当前执行数据的父执行,查询所有子执行数据
+        List<ExecutionEntity> allChildrenExecution =
+            executionEntityManager.collectChildren(executionEntity.getParent());
+        return StreamUtils.filter(allChildrenExecution, e -> !e.isActive());
+    }
+}

+ 37 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/cmd/UpdateBusinessStatusCmd.java

@@ -0,0 +1,37 @@
+package org.dromara.workflow.flowable.cmd;
+
+import org.dromara.common.core.exception.ServiceException;
+import org.flowable.common.engine.impl.interceptor.Command;
+import org.flowable.common.engine.impl.interceptor.CommandContext;
+import org.flowable.engine.impl.persistence.entity.HistoricProcessInstanceEntity;
+import org.flowable.engine.impl.persistence.entity.HistoricProcessInstanceEntityManager;
+import org.flowable.engine.impl.util.CommandContextUtil;
+
+/**
+ * 修改流程状态
+ *
+ * @author may
+ */
+public class UpdateBusinessStatusCmd implements Command<Boolean> {
+
+    private final String processInstanceId;
+    private final String status;
+
+    public UpdateBusinessStatusCmd(String processInstanceId, String status) {
+        this.processInstanceId = processInstanceId;
+        this.status = status;
+    }
+
+    @Override
+    public Boolean execute(CommandContext commandContext) {
+        try {
+            HistoricProcessInstanceEntityManager manager = CommandContextUtil.getHistoricProcessInstanceEntityManager();
+            HistoricProcessInstanceEntity processInstance = manager.findById(processInstanceId);
+            processInstance.setBusinessStatus(status);
+            manager.update(processInstance);
+            return true;
+        } catch (Exception e) {
+            throw new ServiceException(e.getMessage());
+        }
+    }
+}

+ 51 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/cmd/UpdateHiTaskInstCmd.java

@@ -0,0 +1,51 @@
+package org.dromara.workflow.flowable.cmd;
+
+import org.dromara.common.core.exception.ServiceException;
+import org.flowable.common.engine.impl.interceptor.Command;
+import org.flowable.common.engine.impl.interceptor.CommandContext;
+import org.flowable.engine.impl.util.CommandContextUtil;
+import org.flowable.task.service.HistoricTaskService;
+import org.flowable.task.service.impl.persistence.entity.HistoricTaskInstanceEntity;
+
+import java.util.Date;
+import java.util.List;
+
+
+/**
+ * 修改流程历史
+ *
+ * @author may
+ */
+public class UpdateHiTaskInstCmd implements Command<Boolean> {
+
+    private final List<String> taskIds;
+
+    private final String processDefinitionId;
+
+    private final String processInstanceId;
+
+    public UpdateHiTaskInstCmd(List<String> taskIds, String processDefinitionId, String processInstanceId) {
+        this.taskIds = taskIds;
+        this.processDefinitionId = processDefinitionId;
+        this.processInstanceId = processInstanceId;
+    }
+
+    @Override
+    public Boolean execute(CommandContext commandContext) {
+        try {
+            HistoricTaskService historicTaskService = CommandContextUtil.getHistoricTaskService();
+            for (String taskId : taskIds) {
+                HistoricTaskInstanceEntity historicTask = historicTaskService.getHistoricTask(taskId);
+                if (historicTask != null) {
+                    historicTask.setProcessDefinitionId(processDefinitionId);
+                    historicTask.setProcessInstanceId(processInstanceId);
+                    historicTask.setCreateTime(new Date());
+                    CommandContextUtil.getHistoricTaskService().updateHistoricTask(historicTask, true);
+                }
+            }
+            return true;
+        } catch (Exception e) {
+            throw new ServiceException(e.getMessage());
+        }
+    }
+}

+ 32 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/config/FlowableConfig.java

@@ -0,0 +1,32 @@
+package org.dromara.workflow.flowable.config;
+
+import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
+import com.baomidou.mybatisplus.core.toolkit.IdWorker;
+import org.flowable.common.engine.impl.cfg.IdGenerator;
+import org.flowable.spring.SpringProcessEngineConfiguration;
+import org.flowable.spring.boot.EngineConfigurationConfigurer;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+
+import java.util.Collections;
+
+
+/**
+ * flowable配置
+ *
+ * @author may
+ */
+@Configuration
+public class FlowableConfig implements EngineConfigurationConfigurer<SpringProcessEngineConfiguration> {
+
+    @Autowired
+    private GlobalFlowableListener globalFlowableListener;
+    @Autowired
+    private IdentifierGenerator identifierGenerator;
+
+    @Override
+    public void configure(SpringProcessEngineConfiguration processEngineConfiguration) {
+        processEngineConfiguration.setIdGenerator(() -> identifierGenerator.nextId(null).toString());
+        processEngineConfiguration.setEventListeners(Collections.singletonList(globalFlowableListener));
+    }
+}

+ 91 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/config/GlobalFlowableListener.java

@@ -0,0 +1,91 @@
+package org.dromara.workflow.flowable.config;
+
+import cn.hutool.core.collection.CollUtil;
+import org.dromara.workflow.common.enums.TaskStatusEnum;
+import org.flowable.bpmn.model.BoundaryEvent;
+import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.bpmn.model.FlowElement;
+import org.flowable.common.engine.api.delegate.event.*;
+import org.flowable.common.engine.impl.cfg.TransactionState;
+import org.flowable.engine.RepositoryService;
+import org.flowable.engine.RuntimeService;
+import org.flowable.engine.TaskService;
+import org.flowable.engine.runtime.Execution;
+import org.flowable.engine.task.Comment;
+import org.flowable.task.api.Task;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+
+/**
+ * 引擎调度监听
+ *
+ * @author may
+ */
+@Component
+public class GlobalFlowableListener implements FlowableEventListener {
+
+    @Autowired
+    @Lazy
+    private TaskService taskService;
+
+    @Autowired
+    @Lazy
+    private RuntimeService runtimeService;
+
+    @Autowired
+    @Lazy
+    private RepositoryService repositoryService;
+
+    @Override
+    public void onEvent(FlowableEvent flowableEvent) {
+        if (flowableEvent instanceof FlowableEngineEvent flowableEngineEvent) {
+            FlowableEngineEventType engineEventType = (FlowableEngineEventType) flowableEvent.getType();
+            switch (engineEventType) {
+                case JOB_EXECUTION_SUCCESS -> jobExecutionSuccess((FlowableEngineEntityEvent) flowableEngineEvent);
+            }
+        }
+    }
+
+    @Override
+    public boolean isFailOnException() {
+        return true;
+    }
+
+    @Override
+    public boolean isFireOnTransactionLifecycleEvent() {
+        return false;
+    }
+
+    @Override
+    public String getOnTransaction() {
+        return TransactionState.COMMITTED.name();
+    }
+
+    /**
+     * 处理边界定时事件自动审批记录
+     *
+     * @param event 事件
+     */
+    protected void jobExecutionSuccess(FlowableEngineEntityEvent event) {
+        Execution execution = runtimeService.createExecutionQuery().executionId(event.getExecutionId()).singleResult();
+        BpmnModel bpmnModel = repositoryService.getBpmnModel(event.getProcessDefinitionId());
+        FlowElement flowElement = bpmnModel.getFlowElement(execution.getActivityId());
+        if (flowElement instanceof BoundaryEvent) {
+            String attachedToRefId = ((BoundaryEvent) flowElement).getAttachedToRefId();
+            List<Execution> list = runtimeService.createExecutionQuery().activityId(attachedToRefId).list();
+            for (Execution ex : list) {
+                Task task = taskService.createTaskQuery().executionId(ex.getId()).singleResult();
+                if (task != null) {
+                    List<Comment> taskComments = taskService.getTaskComments(task.getId());
+                    if (CollUtil.isEmpty(taskComments)) {
+                        taskService.addComment(task.getId(), task.getProcessInstanceId(), TaskStatusEnum.PASS.getStatus(), "超时自动审批!");
+                    }
+                }
+            }
+        }
+    }
+}

+ 73 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/strategy/FlowEventStrategy.java

@@ -0,0 +1,73 @@
+package org.dromara.workflow.flowable.strategy;
+
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.workflow.annotation.FlowListenerAnnotation;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 流程任务监听策略
+ *
+ * @author may
+ * @date 2023-12-27
+ */
+@Component
+public class FlowEventStrategy implements BeanPostProcessor {
+
+    private final Map<String, FlowTaskEventHandler> flowTaskEventHandlers = new HashMap<>();
+    private final Map<String, FlowProcessEventHandler> flowProcessEventHandlers = new HashMap<>();
+
+    @Override
+    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
+        if (bean instanceof FlowTaskEventHandler) {
+            FlowListenerAnnotation annotation = bean.getClass().getAnnotation(FlowListenerAnnotation.class);
+            if (null != annotation) {
+                if (StringUtils.isNotBlank(annotation.processDefinitionKey()) && StringUtils.isNotBlank(annotation.taskDefId())) {
+                    String id = annotation.processDefinitionKey() + "_" + annotation.taskDefId();
+                    if (!flowTaskEventHandlers.containsKey(id)) {
+                        flowTaskEventHandlers.put(id, (FlowTaskEventHandler) bean);
+                    }
+                }
+            }
+        }
+        if (bean instanceof FlowProcessEventHandler) {
+            FlowListenerAnnotation annotation = bean.getClass().getAnnotation(FlowListenerAnnotation.class);
+            if (null != annotation) {
+                if (StringUtils.isNotBlank(annotation.processDefinitionKey()) && StringUtils.isBlank(annotation.taskDefId())) {
+                    if (!flowProcessEventHandlers.containsKey(annotation.processDefinitionKey())) {
+                        flowProcessEventHandlers.put(annotation.processDefinitionKey(), (FlowProcessEventHandler) bean);
+                    }
+                }
+            }
+        }
+        return BeanPostProcessor.super.postProcessBeforeInitialization(bean, beanName);
+    }
+
+    /**
+     * 获取可执行bean
+     *
+     * @param key key
+     */
+    public FlowTaskEventHandler getTaskHandler(String key) {
+        if (!flowTaskEventHandlers.containsKey(key)) {
+            return null;
+        }
+        return flowTaskEventHandlers.get(key);
+    }
+
+    /**
+     * 获取可执行bean
+     *
+     * @param key key
+     */
+    public FlowProcessEventHandler getProcessHandler(String key) {
+        if (!flowProcessEventHandlers.containsKey(key)) {
+            return null;
+        }
+        return flowProcessEventHandlers.get(key);
+    }
+}

+ 20 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/strategy/FlowProcessEventHandler.java

@@ -0,0 +1,20 @@
+package org.dromara.workflow.flowable.strategy;
+
+
+/**
+ * 流程监听
+ *
+ * @author may
+ * @date 2023-12-27
+ */
+public interface FlowProcessEventHandler {
+
+    /**
+     * 执行办理任务监听
+     *
+     * @param businessKey 业务id
+     * @param status      状态
+     * @param submit      当为true时为申请人节点办理
+     */
+    void handleProcess(String businessKey, String status, boolean submit);
+}

+ 20 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/flowable/strategy/FlowTaskEventHandler.java

@@ -0,0 +1,20 @@
+package org.dromara.workflow.flowable.strategy;
+
+import org.flowable.task.api.Task;
+
+/**
+ * 流程任务监听
+ *
+ * @author may
+ * @date 2023-12-27
+ */
+public interface FlowTaskEventHandler {
+
+    /**
+     * 执行办理任务监听
+     *
+     * @param task        任务
+     * @param businessKey 业务id
+     */
+    void handleTask(Task task, String businessKey);
+}

+ 24 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/listener/TestCustomProcessHandler.java

@@ -0,0 +1,24 @@
+package org.dromara.workflow.listener;
+
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.workflow.annotation.FlowListenerAnnotation;
+import org.dromara.workflow.flowable.strategy.FlowProcessEventHandler;
+import org.springframework.stereotype.Component;
+
+/**
+ * 自定义监听测试
+ *
+ * @author may
+ * @date 2023-12-27
+ */
+@Slf4j
+@Component
+@FlowListenerAnnotation(processDefinitionKey = "leave1")
+public class TestCustomProcessHandler implements FlowProcessEventHandler {
+
+
+    @Override
+    public void handleProcess(String businessKey, String status, boolean submit) {
+        log.info("业务ID:" + businessKey + ",状态:" + status);
+    }
+}

+ 24 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/listener/TestCustomTaskHandler.java

@@ -0,0 +1,24 @@
+package org.dromara.workflow.listener;
+
+import lombok.extern.slf4j.Slf4j;
+import org.dromara.workflow.annotation.FlowListenerAnnotation;
+import org.dromara.workflow.flowable.strategy.FlowTaskEventHandler;
+import org.flowable.task.api.Task;
+import org.springframework.stereotype.Component;
+
+/**
+ * 自定义监听测试
+ *
+ * @author may
+ * @date 2023-12-27
+ */
+@Slf4j
+@Component
+@FlowListenerAnnotation(processDefinitionKey = "leave1", taskDefId = "Activity_14633hx")
+public class TestCustomTaskHandler implements FlowTaskEventHandler {
+
+    @Override
+    public void handleTask(Task task, String businessKey) {
+        log.info("任务名称:" + task.getName() + ",业务ID:" + businessKey);
+    }
+}

+ 29 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/listener/TestLeaveExecutionListener.java

@@ -0,0 +1,29 @@
+package org.dromara.workflow.listener;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.flowable.engine.TaskService;
+import org.flowable.engine.delegate.DelegateExecution;
+import org.flowable.engine.delegate.ExecutionListener;
+import org.flowable.task.api.Task;
+import org.springframework.stereotype.Component;
+
+/**
+ * 流程实例监听测试
+ *
+ * @author may
+ * @date 2023-12-12
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Component("testLeaveExecutionListener")
+public class TestLeaveExecutionListener implements ExecutionListener {
+
+    private final TaskService taskService;
+
+    @Override
+    public void notify(DelegateExecution execution) {
+        Task task = taskService.createTaskQuery().executionId(execution.getId()).singleResult();
+        log.info("执行监听【" + task.getName() + "】");
+    }
+}

+ 21 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/listener/TestLeaveTaskListener.java

@@ -0,0 +1,21 @@
+package org.dromara.workflow.listener;
+
+import lombok.extern.slf4j.Slf4j;
+import org.flowable.task.service.delegate.DelegateTask;
+import org.flowable.task.service.delegate.TaskListener;
+import org.springframework.stereotype.Component;
+
+/**
+ * 流程任务监听测试
+ *
+ * @author may
+ * @date 2023-12-12
+ */
+@Slf4j
+@Component("testLeaveTaskListener")
+public class TestLeaveTaskListener implements TaskListener {
+    @Override
+    public void notify(DelegateTask delegateTask) {
+        log.info("执行监听【" + delegateTask.getName() + "】");
+    }
+}

+ 16 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/ActHiProcinstMapper.java

@@ -0,0 +1,16 @@
+package org.dromara.workflow.mapper;
+
+import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.workflow.domain.ActHiProcinst;
+
+/**
+ * 流程实例Mapper接口
+ *
+ * @author may
+ * @date 2023-07-22
+ */
+@InterceptorIgnore(tenantLine = "true")
+public interface ActHiProcinstMapper extends BaseMapperPlus<ActHiProcinst, ActHiProcinst> {
+
+}

+ 16 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/ActHiTaskinstMapper.java

@@ -0,0 +1,16 @@
+package org.dromara.workflow.mapper;
+
+import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
+import org.dromara.workflow.domain.ActHiTaskinst;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+
+/**
+ * 流程历史任务Mapper接口
+ *
+ * @author gssong
+ * @date 2024-03-02
+ */
+@InterceptorIgnore(tenantLine = "true")
+public interface ActHiTaskinstMapper extends BaseMapperPlus<ActHiTaskinst, ActHiTaskinst> {
+
+}

+ 38 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/ActTaskMapper.java

@@ -0,0 +1,38 @@
+package org.dromara.workflow.mapper;
+
+import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Constants;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import org.apache.ibatis.annotations.Param;
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.workflow.domain.vo.TaskVo;
+
+
+/**
+ * 任务信息Mapper接口
+ *
+ * @author gssong
+ * @date 2024-03-02
+ */
+@InterceptorIgnore(tenantLine = "true")
+public interface ActTaskMapper extends BaseMapperPlus<TaskVo, TaskVo> {
+    /**
+     * 获取待办信息
+     *
+     * @param page         分页
+     * @param queryWrapper 条件
+     * @return 结果
+     */
+    Page<TaskVo> getTaskWaitByPage(@Param("page") Page<TaskVo> page, @Param(Constants.WRAPPER) Wrapper<TaskVo> queryWrapper);
+
+    /**
+     * 查询当前用户的抄送
+     *
+     * @param page         分页
+     * @param queryWrapper 条件
+     * @return 结果
+     */
+    Page<TaskVo> getTaskCopyByPage(@Param("page") Page<TaskVo> page, @Param(Constants.WRAPPER) QueryWrapper<TaskVo> queryWrapper);
+}

+ 15 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/TestLeaveMapper.java

@@ -0,0 +1,15 @@
+package org.dromara.workflow.mapper;
+
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.workflow.domain.TestLeave;
+import org.dromara.workflow.domain.vo.TestLeaveVo;
+
+/**
+ * 请假Mapper接口
+ *
+ * @author may
+ * @date 2023-07-21
+ */
+public interface TestLeaveMapper extends BaseMapperPlus<TestLeave, TestLeaveVo> {
+
+}

+ 15 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/mapper/WfCategoryMapper.java

@@ -0,0 +1,15 @@
+package org.dromara.workflow.mapper;
+
+import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
+import org.dromara.workflow.domain.WfCategory;
+import org.dromara.workflow.domain.vo.WfCategoryVo;
+
+/**
+ * 流程分类Mapper接口
+ *
+ * @author may
+ * @date 2023-06-27
+ */
+public interface WfCategoryMapper extends BaseMapperPlus<WfCategory, WfCategoryVo> {
+
+}

+ 31 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IActHiProcinstService.java

@@ -0,0 +1,31 @@
+package org.dromara.workflow.service;
+
+
+import org.dromara.workflow.domain.ActHiProcinst;
+
+import java.util.List;
+
+/**
+ * 流程实例Service接口
+ *
+ * @author may
+ * @date 2023-07-22
+ */
+public interface IActHiProcinstService {
+
+    /**
+     * 按照业务id查询
+     *
+     * @param businessKeys 业务id
+     * @return 结果
+     */
+    List<ActHiProcinst> selectByBusinessKeyIn(List<String> businessKeys);
+
+    /**
+     * 按照业务id查询
+     *
+     * @param businessKey 业务id
+     * @return 结果
+     */
+    ActHiProcinst selectByBusinessKey(String businessKey);
+}

+ 11 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IActHiTaskinstService.java

@@ -0,0 +1,11 @@
+package org.dromara.workflow.service;
+
+
+/**
+ * 流程历史任务Service接口
+ *
+ * @author gssong
+ * @date 2024-03-02
+ */
+public interface IActHiTaskinstService {
+}

+ 71 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IActModelService.java

@@ -0,0 +1,71 @@
+package org.dromara.workflow.service;
+
+import jakarta.servlet.http.HttpServletResponse;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.workflow.domain.bo.ModelBo;
+import org.dromara.workflow.domain.vo.ModelVo;
+import org.flowable.engine.repository.Model;
+
+
+/**
+ * 模型管理 服务层
+ *
+ * @author may
+ */
+public interface IActModelService {
+    /**
+     * 分页查询模型
+     *
+     * @param modelBo 模型参数
+     * @return 返回分页列表
+     */
+    TableDataInfo<Model> page(ModelBo modelBo);
+
+    /**
+     * 新增模型
+     *
+     * @param modelBo 模型请求对象
+     * @return 结果
+     */
+    boolean saveNewModel(ModelBo modelBo);
+
+    /**
+     * 查询模型
+     *
+     * @param modelId 模型id
+     * @return 模型数据
+     */
+    ModelVo getInfo(String modelId);
+
+    /**
+     * 修改模型信息
+     *
+     * @param modelBo 模型数据
+     * @return 结果
+     */
+    boolean update(ModelBo modelBo);
+
+    /**
+     * 编辑模型XML
+     *
+     * @param modelBo 模型数据
+     * @return 结果
+     */
+    boolean editModelXml(ModelBo modelBo);
+
+    /**
+     * 模型部署
+     *
+     * @param id 模型id
+     * @return 结果
+     */
+    boolean modelDeploy(String id);
+
+    /**
+     * 导出模型zip压缩包
+     *
+     * @param modelId  模型id
+     * @param response 相应
+     */
+    void exportZip(String modelId, HttpServletResponse response);
+}

+ 90 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IActProcessDefinitionService.java

@@ -0,0 +1,90 @@
+package org.dromara.workflow.service;
+
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.workflow.domain.bo.ProcessDefinitionBo;
+import org.dromara.workflow.domain.vo.ProcessDefinitionVo;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.List;
+
+/**
+ * 流程定义 服务层
+ *
+ * @author may
+ */
+public interface IActProcessDefinitionService {
+    /**
+     * 分页查询
+     *
+     * @param processDefinitionBo 参数
+     * @return 返回分页列表
+     */
+    TableDataInfo<ProcessDefinitionVo> page(ProcessDefinitionBo processDefinitionBo);
+
+    /**
+     * 查询历史流程定义列表
+     *
+     * @param key 流程定义key
+     * @return 结果
+     */
+    List<ProcessDefinitionVo> getProcessDefinitionListByKey(String key);
+
+    /**
+     * 查看流程定义图片
+     *
+     * @param processDefinitionId 流程定义id
+     * @return 结果
+     */
+    String processDefinitionImage(String processDefinitionId);
+
+    /**
+     * 查看流程定义xml文件
+     *
+     * @param processDefinitionId 流程定义id
+     * @return 结果
+     */
+    String processDefinitionXml(String processDefinitionId);
+
+    /**
+     * 删除流程定义
+     *
+     * @param deploymentId        部署id
+     * @param processDefinitionId 流程定义id
+     * @return 结果
+     */
+    boolean deleteDeployment(String deploymentId, String processDefinitionId);
+
+    /**
+     * 激活或者挂起流程定义
+     *
+     * @param processDefinitionId 流程定义id
+     * @return 结果
+     */
+    boolean updateProcessDefState(String processDefinitionId);
+
+    /**
+     * 迁移流程定义
+     *
+     * @param currentProcessDefinitionId 当前流程定义id
+     * @param fromProcessDefinitionId    需要迁移到的流程定义id
+     * @return 结果
+     */
+    boolean migrationProcessDefinition(String currentProcessDefinitionId, String fromProcessDefinitionId);
+
+    /**
+     * 流程定义转换为模型
+     *
+     * @param processDefinitionId 流程定义id
+     * @return 结果
+     */
+    boolean convertToModel(String processDefinitionId);
+
+    /**
+     * 通过zip或xml部署流程定义
+     *
+     * @param file         文件
+     * @param categoryCode 分类
+     * @return 结果
+     */
+    boolean deployByFile(MultipartFile file, String categoryCode);
+}

+ 113 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IActProcessInstanceService.java

@@ -0,0 +1,113 @@
+package org.dromara.workflow.service;
+
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.workflow.domain.bo.ProcessInstanceBo;
+import org.dromara.workflow.domain.bo.ProcessInvalidBo;
+import org.dromara.workflow.domain.bo.TaskUrgingBo;
+import org.dromara.workflow.domain.vo.ProcessInstanceVo;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 流程实例 服务层
+ *
+ * @author may
+ */
+public interface IActProcessInstanceService {
+    /**
+     * 通过流程实例id获取历史流程图
+     *
+     * @param processInstanceId 流程实例id
+     * @return 结果
+     */
+    String getHistoryProcessImage(String processInstanceId);
+
+    /**
+     * 通过流程实例id获取历史流程图运行中,历史等节点
+     *
+     * @param processInstanceId 流程实例id
+     * @return 结果
+     */
+    Map<String, Object> getHistoryProcessList(String processInstanceId);
+
+    /**
+     * 分页查询正在运行的流程实例
+     *
+     * @param processInstanceBo 参数
+     * @return 结果
+     */
+    TableDataInfo<ProcessInstanceVo> getProcessInstanceRunningByPage(ProcessInstanceBo processInstanceBo);
+
+    /**
+     * 分页查询已结束的流程实例
+     *
+     * @param processInstanceBo 参数
+     * @return 结果
+     */
+    TableDataInfo<ProcessInstanceVo> getProcessInstanceFinishByPage(ProcessInstanceBo processInstanceBo);
+
+    /**
+     * 获取审批记录
+     *
+     * @param processInstanceId 流程实例id
+     * @return 结果
+     */
+    Map<String, Object> getHistoryRecord(String processInstanceId);
+
+    /**
+     * 作废流程实例,不会删除历史记录(删除运行中的实例)
+     *
+     * @param processInvalidBo 参数
+     * @return 结果
+     */
+    boolean deleteRuntimeProcessInst(ProcessInvalidBo processInvalidBo);
+
+    /**
+     * 运行中的实例 删除程实例,删除历史记录,删除业务与流程关联信息
+     *
+     * @param processInstanceIds 流程实例id
+     * @return 结果
+     */
+    boolean deleteRuntimeProcessAndHisInst(List<String> processInstanceIds);
+
+    /**
+     * 按照业务id删除 运行中的实例 删除程实例,删除历史记录,删除业务与流程关联信息
+     *
+     * @param businessKeys 业务id
+     * @return 结果
+     */
+    boolean deleteRuntimeProcessAndHisInstByBusinessKeys(List<String> businessKeys);
+
+    /**
+     * 已完成的实例 删除程实例,删除历史记录,删除业务与流程关联信息
+     *
+     * @param processInstanceIds 流程实例id
+     * @return 结果
+     */
+    boolean deleteFinishProcessAndHisInst(List<String> processInstanceIds);
+
+    /**
+     * 撤销流程申请
+     *
+     * @param processInstanceId 流程实例id
+     * @return 结果
+     */
+    boolean cancelProcessApply(String processInstanceId);
+
+    /**
+     * 分页查询当前登录人单据
+     *
+     * @param processInstanceBo 参数
+     * @return 结果
+     */
+    TableDataInfo<ProcessInstanceVo> getCurrentSubmitByPage(ProcessInstanceBo processInstanceBo);
+
+    /**
+     * 任务催办(给当前任务办理人发送站内信,邮件,短信等)
+     *
+     * @param taskUrgingBo 任务催办
+     * @return 结果
+     */
+    boolean taskUrging(TaskUrgingBo taskUrgingBo);
+}

+ 129 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IActTaskService.java

@@ -0,0 +1,129 @@
+package org.dromara.workflow.service;
+
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.workflow.domain.bo.*;
+import org.dromara.workflow.domain.vo.TaskVo;
+
+import java.util.Map;
+
+/**
+ * 任务 服务层
+ *
+ * @author may
+ */
+public interface IActTaskService {
+    /**
+     * 启动任务
+     *
+     * @param startProcessBo 启动流程参数
+     * @return 结果
+     */
+    Map<String, Object> startWorkFlow(StartProcessBo startProcessBo);
+
+
+    /**
+     * 办理任务
+     *
+     * @param completeTaskBo 办理任务参数
+     * @return 结果
+     */
+    boolean completeTask(CompleteTaskBo completeTaskBo);
+
+    /**
+     * 查询当前用户的待办任务
+     *
+     * @param taskBo 参数
+     * @return 结果
+     */
+    TableDataInfo<TaskVo> getTaskWaitByPage(TaskBo taskBo);
+
+    /**
+     * 查询当前租户所有待办任务
+     *
+     * @param taskBo 参数
+     * @return 结果
+     */
+    TableDataInfo<TaskVo> getAllTaskWaitByPage(TaskBo taskBo);
+
+
+    /**
+     * 查询当前用户的已办任务
+     *
+     * @param taskBo 参数
+     * @return 结果
+     */
+    TableDataInfo<TaskVo> getTaskFinishByPage(TaskBo taskBo);
+
+    /**
+     * 查询当前用户的抄送
+     *
+     * @param taskBo 参数
+     * @return 结果
+     */
+    TableDataInfo<TaskVo> getTaskCopyByPage(TaskBo taskBo);
+
+    /**
+     * 查询当前租户所有已办任务
+     *
+     * @param taskBo 参数
+     * @return 结果
+     */
+    TableDataInfo<TaskVo> getAllTaskFinishByPage(TaskBo taskBo);
+
+    /**
+     * 委派任务
+     *
+     * @param delegateBo 参数
+     * @return 结果
+     */
+    boolean delegateTask(DelegateBo delegateBo);
+
+    /**
+     * 终止任务
+     *
+     * @param terminationBo 参数
+     * @return 结果
+     */
+    boolean terminationTask(TerminationBo terminationBo);
+
+    /**
+     * 转办任务
+     *
+     * @param transmitBo 参数
+     * @return 结果
+     */
+    boolean transferTask(TransmitBo transmitBo);
+
+    /**
+     * 会签任务加签
+     *
+     * @param addMultiBo 参数
+     * @return 结果
+     */
+    boolean addMultiInstanceExecution(AddMultiBo addMultiBo);
+
+    /**
+     * 会签任务减签
+     *
+     * @param deleteMultiBo 参数
+     * @return 结果
+     */
+    boolean deleteMultiInstanceExecution(DeleteMultiBo deleteMultiBo);
+
+    /**
+     * 驳回审批
+     *
+     * @param backProcessBo 参数
+     * @return 流程实例id
+     */
+    String backProcess(BackProcessBo backProcessBo);
+
+    /**
+     * 修改任务办理人
+     *
+     * @param taskIds 任务id
+     * @param userId  办理人id
+     * @return 结果
+     */
+    boolean updateAssignee(String[] taskIds, String userId);
+}

+ 49 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/ITestLeaveService.java

@@ -0,0 +1,49 @@
+package org.dromara.workflow.service;
+
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.workflow.domain.TestLeave;
+import org.dromara.workflow.domain.bo.TestLeaveBo;
+import org.dromara.workflow.domain.vo.TestLeaveVo;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 请假Service接口
+ *
+ * @author may
+ * @date 2023-07-21
+ */
+public interface ITestLeaveService {
+
+    /**
+     * 查询请假
+     */
+    TestLeaveVo queryById(Long id);
+
+    /**
+     * 查询请假列表
+     */
+    TableDataInfo<TestLeaveVo> queryPageList(TestLeaveBo bo, PageQuery pageQuery);
+
+    /**
+     * 查询请假列表
+     */
+    List<TestLeaveVo> queryList(TestLeaveBo bo);
+
+    /**
+     * 新增请假
+     */
+    TestLeave insertByBo(TestLeaveBo bo);
+
+    /**
+     * 修改请假
+     */
+    TestLeave updateByBo(TestLeaveBo bo);
+
+    /**
+     * 校验并批量删除请假信息
+     */
+    Boolean deleteWithValidByIds(Collection<Long> ids);
+}

+ 51 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IWfCategoryService.java

@@ -0,0 +1,51 @@
+package org.dromara.workflow.service;
+
+import org.dromara.workflow.domain.WfCategory;
+import org.dromara.workflow.domain.bo.WfCategoryBo;
+import org.dromara.workflow.domain.vo.WfCategoryVo;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 流程分类Service接口
+ *
+ * @author may
+ * @date 2023-06-28
+ */
+public interface IWfCategoryService {
+
+    /**
+     * 查询流程分类
+     */
+    WfCategoryVo queryById(Long id);
+
+
+    /**
+     * 查询流程分类列表
+     */
+    List<WfCategoryVo> queryList(WfCategoryBo bo);
+
+    /**
+     * 新增流程分类
+     */
+    Boolean insertByBo(WfCategoryBo bo);
+
+    /**
+     * 修改流程分类
+     */
+    Boolean updateByBo(WfCategoryBo bo);
+
+    /**
+     * 校验并批量删除流程分类信息
+     */
+    Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
+
+    /**
+     * 按照类别编码查询
+     *
+     * @param categoryCode 分类比吗
+     * @return 结果
+     */
+    WfCategory queryByCategoryCode(String categoryCode);
+}

+ 60 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/IWorkflowUserService.java

@@ -0,0 +1,60 @@
+package org.dromara.workflow.service;
+
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.system.domain.SysUserRole;
+import org.dromara.system.domain.bo.SysUserBo;
+import org.dromara.system.domain.vo.SysUserVo;
+import org.dromara.workflow.domain.bo.SysUserMultiBo;
+import org.dromara.workflow.domain.vo.TaskVo;
+
+import java.util.List;
+
+/**
+ * 工作流用户选人管理 服务层
+ *
+ * @author may
+ */
+public interface IWorkflowUserService {
+
+    /**
+     * 分页查询工作流选择加签人员
+     *
+     * @param sysUserMultiBo 参数
+     * @return 结果
+     */
+    TableDataInfo<SysUserVo> getWorkflowAddMultiInstanceByPage(SysUserMultiBo sysUserMultiBo);
+
+    /**
+     * 查询工作流选择减签人员
+     *
+     * @param taskId 任务id
+     * @return 结果
+     */
+    List<TaskVo> getWorkflowDeleteMultiInstanceList(String taskId);
+
+    /**
+     * 按照用户id查询用户
+     *
+     * @param userIds 用户id
+     * @return 结果
+     */
+    List<SysUserVo> getUserListByIds(List<Long> userIds);
+
+    /**
+     * 按照角色id查询关联用户id
+     *
+     * @param roleIds 角色id
+     * @return 结果
+     */
+    List<SysUserRole> getUserRoleListByRoleIds(List<Long> roleIds);
+
+    /**
+     * 分页查询用户
+     *
+     * @param sysUserBo 参数
+     * @param pageQuery 分页
+     * @return 结果
+     */
+    TableDataInfo<SysUserVo> getUserListByPage(SysUserBo sysUserBo, PageQuery pageQuery);
+}

+ 51 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/ActHiProcinstServiceImpl.java

@@ -0,0 +1,51 @@
+package org.dromara.workflow.service.impl;
+
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.workflow.domain.ActHiProcinst;
+import org.dromara.workflow.mapper.ActHiProcinstMapper;
+import org.dromara.workflow.service.IActHiProcinstService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+
+/**
+ * 流程实例Service业务层处理
+ *
+ * @author may
+ * @date 2023-07-22
+ */
+@RequiredArgsConstructor
+@Service
+public class ActHiProcinstServiceImpl implements IActHiProcinstService {
+
+    private final ActHiProcinstMapper baseMapper;
+
+    /**
+     * 按照业务id查询
+     *
+     * @param businessKeys 业务id
+     */
+    @Override
+    public List<ActHiProcinst> selectByBusinessKeyIn(List<String> businessKeys) {
+        return baseMapper.selectList(new LambdaQueryWrapper<ActHiProcinst>()
+            .in(ActHiProcinst::getBusinessKey, businessKeys)
+            .eq(TenantHelper.isEnable(), ActHiProcinst::getTenantId, TenantHelper.getTenantId()));
+    }
+
+    /**
+     * 按照业务id查询
+     *
+     * @param businessKey 业务id
+     */
+    @Override
+    public ActHiProcinst selectByBusinessKey(String businessKey) {
+        return baseMapper.selectOne(new LambdaQueryWrapper<ActHiProcinst>()
+            .eq(ActHiProcinst::getBusinessKey, businessKey)
+            .eq(TenantHelper.isEnable(), ActHiProcinst::getTenantId, TenantHelper.getTenantId()));
+
+    }
+}

+ 18 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/ActHiTaskinstServiceImpl.java

@@ -0,0 +1,18 @@
+package org.dromara.workflow.service.impl;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.dromara.workflow.service.IActHiTaskinstService;
+
+
+/**
+ * 流程历史任务Service业务层处理
+ *
+ * @author gssong
+ * @date 2024-03-02
+ */
+@RequiredArgsConstructor
+@Service
+public class ActHiTaskinstServiceImpl implements IActHiTaskinstService {
+
+}

+ 334 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/ActModelServiceImpl.java

@@ -0,0 +1,334 @@
+package org.dromara.workflow.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.lang.Validator;
+import cn.hutool.core.util.ArrayUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.core.util.ZipUtil;
+import cn.hutool.json.JSONUtil;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.apache.batik.transcoder.TranscoderInput;
+import org.apache.batik.transcoder.TranscoderOutput;
+import org.apache.batik.transcoder.image.PNGTranscoder;
+import org.apache.commons.io.IOUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.workflow.common.constant.FlowConstant;
+import org.dromara.workflow.domain.bo.ModelBo;
+import org.dromara.workflow.domain.vo.ModelVo;
+import org.dromara.workflow.service.IActModelService;
+import org.dromara.workflow.utils.ModelUtils;
+import org.flowable.bpmn.model.BpmnModel;
+import org.flowable.engine.RepositoryService;
+import org.flowable.engine.repository.Deployment;
+import org.flowable.engine.repository.Model;
+import org.flowable.engine.repository.ModelQuery;
+import org.flowable.engine.repository.ProcessDefinition;
+import org.flowable.validation.ValidationError;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.*;
+import java.util.stream.Collectors;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+/**
+ * 模型管理 服务层实现
+ *
+ * @author may
+ */
+@RequiredArgsConstructor
+@Service
+public class ActModelServiceImpl implements IActModelService {
+
+    private final RepositoryService repositoryService;
+
+    /**
+     * 分页查询模型
+     *
+     * @param modelBo 模型参数
+     * @return 返回分页列表
+     */
+    @Override
+    public TableDataInfo<Model> page(ModelBo modelBo) {
+        ModelQuery query = repositoryService.createModelQuery();
+        query.modelTenantId(TenantHelper.getTenantId());
+        if (StringUtils.isNotEmpty(modelBo.getName())) {
+            query.modelNameLike("%" + modelBo.getName() + "%");
+        }
+        if (StringUtils.isNotEmpty(modelBo.getKey())) {
+            query.modelKey(modelBo.getKey());
+        }
+        if (StringUtils.isNotEmpty(modelBo.getCategoryCode())) {
+            query.modelCategory(modelBo.getCategoryCode());
+        }
+        query.orderByLastUpdateTime().desc();
+        // 创建时间降序排列
+        query.orderByCreateTime().desc();
+        // 分页查询
+        List<Model> modelList = query.listPage(modelBo.getPageNum(), modelBo.getPageSize());
+        // 总记录数
+        long total = query.count();
+        return new TableDataInfo<>(modelList, total);
+    }
+
+    /**
+     * 新增模型
+     *
+     * @param modelBo 模型请求对象
+     * @return 结果
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean saveNewModel(ModelBo modelBo) {
+        try {
+            int version = 0;
+            String key = modelBo.getKey();
+            String name = modelBo.getName();
+            String description = modelBo.getDescription();
+            String categoryCode = modelBo.getCategoryCode();
+            String xml = modelBo.getXml();
+            Model checkModel = repositoryService.createModelQuery().modelKey(key).modelTenantId(TenantHelper.getTenantId()).singleResult();
+            if (ObjectUtil.isNotNull(checkModel)) {
+                throw new ServiceException("模型key已存在!");
+            }
+            //初始空的模型
+            Model model = repositoryService.newModel();
+            model.setKey(key);
+            model.setName(name);
+            model.setVersion(version);
+            model.setCategory(categoryCode);
+            model.setMetaInfo(description);
+            model.setTenantId(TenantHelper.getTenantId());
+            //保存初始化的模型基本信息数据
+            repositoryService.saveModel(model);
+            repositoryService.addModelEditorSource(model.getId(), StrUtil.utf8Bytes(xml));
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new ServiceException(e.getMessage());
+        }
+    }
+
+    /**
+     * 查询模型
+     *
+     * @param id 模型id
+     * @return 模型数据
+     */
+    @Override
+    public ModelVo getInfo(String id) {
+        ModelVo modelVo = new ModelVo();
+        Model model = repositoryService.getModel(id);
+        if (model != null) {
+            try {
+                byte[] modelEditorSource = repositoryService.getModelEditorSource(model.getId());
+                modelVo.setXml(StrUtil.utf8Str(modelEditorSource));
+                modelVo.setId(model.getId());
+                modelVo.setKey(model.getKey());
+                modelVo.setName(model.getName());
+                modelVo.setCategoryCode(model.getCategory());
+                modelVo.setDescription(model.getMetaInfo());
+                return modelVo;
+            } catch (Exception e) {
+                throw new ServiceException(e.getMessage());
+            }
+        }
+        return modelVo;
+    }
+
+    /**
+     * 修改模型信息
+     *
+     * @param modelBo 模型数据
+     * @return 结果
+     */
+    @Override
+    public boolean update(ModelBo modelBo) {
+        try {
+            Model model = repositoryService.getModel(modelBo.getId());
+            List<Model> list = repositoryService.createModelQuery().modelTenantId(TenantHelper.getTenantId()).modelKey(modelBo.getKey()).list();
+            list.stream().filter(e -> !e.getId().equals(model.getId())).findFirst().ifPresent(e -> {
+                throw new ServiceException("模型KEY已存在!");
+            });
+            model.setCategory(modelBo.getCategoryCode());
+            model.setMetaInfo(modelBo.getDescription());
+            repositoryService.saveModel(model);
+        } catch (Exception e) {
+            throw new ServiceException(e.getMessage());
+        }
+        return true;
+    }
+
+    /**
+     * 编辑模型XML
+     *
+     * @param modelBo 模型数据
+     * @return 结果
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean editModelXml(ModelBo modelBo) {
+        try {
+            String xml = modelBo.getXml();
+            String svg = modelBo.getSvg();
+            String modelId = modelBo.getId();
+            String key = modelBo.getKey();
+            String name = modelBo.getName();
+            BpmnModel bpmnModel = ModelUtils.xmlToBpmnModel(xml);
+            ModelUtils.checkBpmnModel(bpmnModel);
+            Model model = repositoryService.getModel(modelId);
+            List<Model> list = repositoryService.createModelQuery().modelTenantId(TenantHelper.getTenantId()).modelKey(key).list();
+            list.stream().filter(e -> !e.getId().equals(model.getId())).findFirst().ifPresent(e -> {
+                throw new ServiceException("模型KEY已存在!");
+            });
+            // 校验key命名规范
+            if (!Validator.isMatchRegex(FlowConstant.MODEL_KEY_PATTERN, key)) {
+                throw new ServiceException("模型标识KEY只能字符或者下划线开头!");
+            }
+            model.setKey(key);
+            model.setName(name);
+            model.setVersion(model.getVersion() + 1);
+            repositoryService.saveModel(model);
+            repositoryService.addModelEditorSource(model.getId(), StrUtil.utf8Bytes(xml));
+            // 转换图片
+            InputStream svgStream = new ByteArrayInputStream(StrUtil.utf8Bytes(svg));
+            TranscoderInput input = new TranscoderInput(svgStream);
+
+            PNGTranscoder transcoder = new PNGTranscoder();
+            ByteArrayOutputStream outStream = new ByteArrayOutputStream();
+            TranscoderOutput output = new TranscoderOutput(outStream);
+
+            transcoder.transcode(input, output);
+            final byte[] result = outStream.toByteArray();
+            repositoryService.addModelEditorSourceExtra(model.getId(), result);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new ServiceException(e.getMessage());
+        }
+    }
+
+    /**
+     * 模型部署
+     *
+     * @param id 模型id
+     * @return 结果
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean modelDeploy(String id) {
+        try {
+            // 查询流程定义模型xml
+            byte[] xmlBytes = repositoryService.getModelEditorSource(id);
+            if (ArrayUtil.isEmpty(xmlBytes)) {
+                throw new ServiceException("模型数据为空,请先设计流程定义模型,再进行部署!");
+            }
+            if (JSONUtil.isTypeJSON(IOUtils.toString(xmlBytes, StandardCharsets.UTF_8.toString()))) {
+                byte[] bytes = ModelUtils.bpmnJsonToXmlBytes(xmlBytes);
+                if (ArrayUtil.isEmpty(bytes)) {
+                    throw new ServiceException("模型不能为空,请至少设计一条主线流程!");
+                }
+            }
+            BpmnModel bpmnModel = ModelUtils.xmlToBpmnModel(xmlBytes);
+            // 校验模型
+            ModelUtils.checkBpmnModel(bpmnModel);
+            List<ValidationError> validationErrors = repositoryService.validateProcess(bpmnModel);
+            if (CollUtil.isNotEmpty(validationErrors)) {
+                String errorMsg = validationErrors.stream().map(ValidationError::getProblem).distinct().collect(Collectors.joining(","));
+                throw new ServiceException(errorMsg);
+            }
+            // 查询模型的基本信息
+            Model model = repositoryService.getModel(id);
+            // xml资源的名称 ,对应act_ge_bytearray表中的name_字段
+            String processName = model.getName() + ".bpmn20.xml";
+            // 调用部署相关的api方法进行部署流程定义
+            Deployment deployment = repositoryService.createDeployment()
+                // 部署名称
+                .name(model.getName())
+                // 部署标识key
+                .key(model.getKey())
+                // 部署流程分类
+                .category(model.getCategory())
+                // bpmn20.xml资源
+                .addBytes(processName, xmlBytes)
+                // 租户id
+                .tenantId(TenantHelper.getTenantId())
+                .deploy();
+
+            // 更新 部署id 到流程定义模型数据表中
+            model.setDeploymentId(deployment.getId());
+            repositoryService.saveModel(model);
+            // 更新分类
+            ProcessDefinition definition = repositoryService.createProcessDefinitionQuery().deploymentId(deployment.getId()).singleResult();
+            repositoryService.setProcessDefinitionCategory(definition.getId(), model.getCategory());
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new ServiceException(e.getMessage());
+        }
+    }
+
+    /**
+     * 导出模型zip压缩包
+     *
+     * @param modelId  模型id
+     * @param response 相应
+     */
+    @Override
+    public void exportZip(String modelId, HttpServletResponse response) {
+        ZipOutputStream zos = null;
+        try {
+            zos = ZipUtil.getZipOutputStream(response.getOutputStream(), StandardCharsets.UTF_8);
+            // 压缩包文件名
+            String zipName = "模型不存在";
+            // 查询模型基本信息
+            Model model = repositoryService.getModel(modelId);
+            byte[] xmlBytes = repositoryService.getModelEditorSource(modelId);
+            if (ObjectUtil.isNotNull(model)) {
+                if (JSONUtil.isTypeJSON(IOUtils.toString(xmlBytes, StandardCharsets.UTF_8.toString())) && ArrayUtil.isEmpty(ModelUtils.bpmnJsonToXmlBytes(xmlBytes))) {
+                    zipName = "模型不能为空,请至少设计一条主线流程!";
+                    zos.putNextEntry(new ZipEntry(zipName + ".txt"));
+                    zos.write(zipName.getBytes(StandardCharsets.UTF_8));
+                } else if (ArrayUtil.isEmpty(xmlBytes)) {
+                    zipName = "模型数据为空,请先设计流程定义模型,再进行部署!";
+                    zos.putNextEntry(new ZipEntry(zipName + ".txt"));
+                    zos.write(zipName.getBytes(StandardCharsets.UTF_8));
+                } else {
+                    String fileName = model.getName() + "-" + model.getKey();
+                    // 压缩包文件名
+                    zipName = fileName + ".zip";
+                    // 将xml添加到压缩包中(指定xml文件名:请假流程.bpmn20.xml
+                    zos.putNextEntry(new ZipEntry(fileName + ".bpmn20.xml"));
+                    zos.write(xmlBytes);
+                }
+            }
+            response.setHeader("Content-Disposition",
+                "attachment; filename=" + URLEncoder.encode(zipName, StandardCharsets.UTF_8) + ".zip");
+            // 刷出响应流
+            response.flushBuffer();
+        } catch (IOException e) {
+            e.printStackTrace();
+        } finally {
+            if (zos != null) {
+                try {
+                    zos.closeEntry();
+                    zos.close();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
+    }
+}

+ 328 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/ActProcessDefinitionServiceImpl.java

@@ -0,0 +1,328 @@
+package org.dromara.workflow.service.impl;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.ObjectUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import org.apache.commons.io.IOUtils;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.workflow.common.constant.FlowConstant;
+import org.dromara.workflow.domain.WfCategory;
+import org.dromara.workflow.domain.bo.ProcessDefinitionBo;
+import org.dromara.workflow.domain.vo.ProcessDefinitionVo;
+import org.dromara.workflow.service.IActProcessDefinitionService;
+import org.dromara.workflow.service.IWfCategoryService;
+import org.flowable.engine.HistoryService;
+import org.flowable.engine.ProcessMigrationService;
+import org.flowable.engine.RepositoryService;
+import org.flowable.engine.impl.bpmn.deployer.ResourceNameUtil;
+import org.flowable.engine.repository.Deployment;
+import org.flowable.engine.repository.Model;
+import org.flowable.engine.repository.ProcessDefinition;
+import org.flowable.engine.repository.ProcessDefinitionQuery;
+import org.flowable.task.api.history.HistoricTaskInstance;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+import java.util.zip.ZipInputStream;
+
+/**
+ * 流程定义 服务层实现
+ *
+ * @author may
+ */
+@RequiredArgsConstructor
+@Service
+public class ActProcessDefinitionServiceImpl implements IActProcessDefinitionService {
+
+    private final RepositoryService repositoryService;
+
+    private final HistoryService historyService;
+
+    private final ProcessMigrationService processMigrationService;
+
+    private final IWfCategoryService wfCategoryService;
+
+    /**
+     * 分页查询
+     *
+     * @param processDefinitionBo 参数
+     * @return 返回分页列表
+     */
+    @Override
+    public TableDataInfo<ProcessDefinitionVo> page(ProcessDefinitionBo processDefinitionBo) {
+        ProcessDefinitionQuery query = repositoryService.createProcessDefinitionQuery();
+        query.processDefinitionTenantId(TenantHelper.getTenantId());
+        if (StringUtils.isNotEmpty(processDefinitionBo.getKey())) {
+            query.processDefinitionKey(processDefinitionBo.getKey());
+        }
+        if (StringUtils.isNotEmpty(processDefinitionBo.getCategoryCode())) {
+            query.processDefinitionCategory(processDefinitionBo.getCategoryCode());
+        }
+        if (StringUtils.isNotEmpty(processDefinitionBo.getName())) {
+            query.processDefinitionNameLike("%" + processDefinitionBo.getName() + "%");
+        }
+        query.orderByDeploymentId().desc();
+        // 分页查询
+        List<ProcessDefinitionVo> processDefinitionVoList = new ArrayList<>();
+        List<ProcessDefinition> definitionList = query.latestVersion().listPage(processDefinitionBo.getPageNum(), processDefinitionBo.getPageSize());
+        List<Deployment> deploymentList = null;
+        if (CollUtil.isNotEmpty(definitionList)) {
+            List<String> deploymentIds = StreamUtils.toList(definitionList, ProcessDefinition::getDeploymentId);
+            deploymentList = repositoryService.createDeploymentQuery().deploymentIds(deploymentIds).list();
+        }
+        for (ProcessDefinition processDefinition : definitionList) {
+            ProcessDefinitionVo processDefinitionVo = BeanUtil.toBean(processDefinition, ProcessDefinitionVo.class);
+            if (CollUtil.isNotEmpty(deploymentList)) {
+                // 部署时间
+                deploymentList.stream().filter(e -> e.getId().equals(processDefinition.getDeploymentId())).findFirst().ifPresent(e -> {
+                    processDefinitionVo.setDeploymentTime(e.getDeploymentTime());
+                });
+            }
+            processDefinitionVoList.add(processDefinitionVo);
+        }
+        // 总记录数
+        long total = query.count();
+
+        return new TableDataInfo<>(processDefinitionVoList, total);
+    }
+
+    /**
+     * 查询历史流程定义列表
+     *
+     * @param key 流程定义key
+     */
+    @Override
+    public List<ProcessDefinitionVo> getProcessDefinitionListByKey(String key) {
+        List<ProcessDefinitionVo> processDefinitionVoList = new ArrayList<>();
+        ProcessDefinitionQuery query = repositoryService.createProcessDefinitionQuery();
+        List<ProcessDefinition> definitionList = query.processDefinitionTenantId(TenantHelper.getTenantId()).processDefinitionKey(key).list();
+        List<Deployment> deploymentList = null;
+        if (CollUtil.isNotEmpty(definitionList)) {
+            List<String> deploymentIds = definitionList.stream().map(ProcessDefinition::getDeploymentId).collect(Collectors.toList());
+            deploymentList = repositoryService.createDeploymentQuery()
+                .deploymentIds(deploymentIds).list();
+        }
+        for (ProcessDefinition processDefinition : definitionList) {
+            ProcessDefinitionVo processDefinitionVo = BeanUtil.toBean(processDefinition, ProcessDefinitionVo.class);
+            if (CollUtil.isNotEmpty(deploymentList)) {
+                // 部署时间
+                deploymentList.stream().filter(e -> e.getId().equals(processDefinition.getDeploymentId())).findFirst().ifPresent(e -> {
+                    processDefinitionVo.setDeploymentTime(e.getDeploymentTime());
+                });
+            }
+            processDefinitionVoList.add(processDefinitionVo);
+        }
+        return CollectionUtil.reverse(processDefinitionVoList);
+    }
+
+    /**
+     * 查看流程定义图片
+     *
+     * @param processDefinitionId 流程定义id
+     */
+    @SneakyThrows
+    @Override
+    public String processDefinitionImage(String processDefinitionId) {
+        InputStream inputStream = repositoryService.getProcessDiagram(processDefinitionId);
+        return Base64.encode(IOUtils.toByteArray(inputStream));
+    }
+
+    /**
+     * 查看流程定义xml文件
+     *
+     * @param processDefinitionId 流程定义id
+     */
+    @Override
+    public String processDefinitionXml(String processDefinitionId) {
+        StringBuilder xml = new StringBuilder();
+        ProcessDefinition processDefinition = repositoryService.getProcessDefinition(processDefinitionId);
+        InputStream inputStream;
+        try {
+            inputStream = repositoryService.getResourceAsStream(processDefinition.getDeploymentId(), processDefinition.getResourceName());
+            xml.append(IOUtils.toString(inputStream, StandardCharsets.UTF_8));
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return xml.toString();
+    }
+
+    /**
+     * 删除流程定义
+     *
+     * @param deploymentId        部署id
+     * @param processDefinitionId 流程定义id
+     */
+    @Override
+    public boolean deleteDeployment(String deploymentId, String processDefinitionId) {
+        try {
+            List<HistoricTaskInstance> taskInstanceList = historyService.createHistoricTaskInstanceQuery()
+                .processDefinitionId(processDefinitionId).list();
+            if (CollectionUtil.isNotEmpty(taskInstanceList)) {
+                throw new ServiceException("当前流程定义已被使用不可删除!");
+            }
+            //删除流程定义
+            repositoryService.deleteDeployment(deploymentId);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new ServiceException(e.getMessage());
+        }
+    }
+
+    /**
+     * 激活或者挂起流程定义
+     *
+     * @param processDefinitionId 流程定义id
+     */
+    @Override
+    public boolean updateProcessDefState(String processDefinitionId) {
+        try {
+            ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
+                .processDefinitionId(processDefinitionId).processDefinitionTenantId(TenantHelper.getTenantId()).singleResult();
+            //将当前为挂起状态更新为激活状态
+            //参数说明:参数1:流程定义id,参数2:是否激活(true是否级联对应流程实例,激活了则对应流程实例都可以审批),
+            //参数3:什么时候激活,如果为null则立即激活,如果为具体时间则到达此时间后激活
+            if (processDefinition.isSuspended()) {
+                repositoryService.activateProcessDefinitionById(processDefinitionId, true, null);
+            } else {
+                repositoryService.suspendProcessDefinitionById(processDefinitionId, true, null);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new ServiceException("操作失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 迁移流程定义
+     *
+     * @param currentProcessDefinitionId 当前流程定义id
+     * @param fromProcessDefinitionId    需要迁移到的流程定义id
+     */
+
+    @Override
+    public boolean migrationProcessDefinition(String currentProcessDefinitionId, String fromProcessDefinitionId) {
+        try {
+            // 迁移验证
+            boolean migrationValid = processMigrationService.createProcessInstanceMigrationBuilder()
+                .migrateToProcessDefinition(currentProcessDefinitionId)
+                .validateMigrationOfProcessInstances(fromProcessDefinitionId)
+                .isMigrationValid();
+            if (!migrationValid) {
+                throw new ServiceException("流程定义差异过大无法迁移,请修改流程图");
+            }
+            // 已结束的流程实例不会迁移
+            processMigrationService.createProcessInstanceMigrationBuilder()
+                .migrateToProcessDefinition(currentProcessDefinitionId)
+                .migrateProcessInstances(fromProcessDefinitionId);
+            return true;
+        } catch (Exception e) {
+            throw new ServiceException(e.getMessage());
+        }
+    }
+
+    /**
+     * 流程定义转换为模型
+     *
+     * @param processDefinitionId 流程定义id
+     */
+    @Override
+    public boolean convertToModel(String processDefinitionId) {
+        ProcessDefinition pd = repositoryService.createProcessDefinitionQuery()
+            .processDefinitionId(processDefinitionId).singleResult();
+        InputStream inputStream = repositoryService.getResourceAsStream(pd.getDeploymentId(), pd.getResourceName());
+        Model model = repositoryService.createModelQuery().modelKey(pd.getKey()).modelTenantId(TenantHelper.getTenantId()).singleResult();
+        try {
+            if (ObjectUtil.isNotNull(model)) {
+                repositoryService.addModelEditorSource(model.getId(), IoUtil.readBytes(inputStream));
+            } else {
+                Model modelData = repositoryService.newModel();
+                modelData.setKey(pd.getKey());
+                modelData.setName(pd.getName());
+                modelData.setTenantId(pd.getTenantId());
+                repositoryService.saveModel(modelData);
+                repositoryService.addModelEditorSource(modelData.getId(), IoUtil.readBytes(inputStream));
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new ServiceException(e.getMessage());
+        }
+    }
+
+    /**
+     * 通过zip或xml部署流程定义
+     *
+     * @param file         文件
+     * @param categoryCode 分类
+     */
+    @Override
+    public boolean deployByFile(MultipartFile file, String categoryCode) {
+        try {
+            WfCategory wfCategory = wfCategoryService.queryByCategoryCode(categoryCode);
+            if (wfCategory == null) {
+                throw new ServiceException("流程分类不存在");
+            }
+            // 文件名 = 流程名称-流程key
+            String filename = file.getOriginalFilename();
+            assert filename != null;
+            String[] splitFilename = filename.substring(0, filename.lastIndexOf(".")).split("-");
+            if (splitFilename.length < 2) {
+                throw new ServiceException("流程分类不能为空(文件名 = 流程名称-流程key)");
+            }
+            //流程名称
+            String processName = splitFilename[0];
+            //流程key
+            String processKey = splitFilename[1];
+            // 文件后缀名
+            String suffix = filename.substring(filename.lastIndexOf(".") + 1).toUpperCase();
+            InputStream inputStream = file.getInputStream();
+            Deployment deployment;
+            if (FlowConstant.ZIP.equals(suffix)) {
+                deployment = repositoryService.createDeployment()
+                    .tenantId(TenantHelper.getTenantId())
+                    .addZipInputStream(new ZipInputStream(inputStream)).name(processName).key(processKey).category(categoryCode).deploy();
+            } else {
+                String[] list = ResourceNameUtil.BPMN_RESOURCE_SUFFIXES;
+                boolean flag = false;
+                for (String str : list) {
+                    if (filename.contains(str)) {
+                        flag = true;
+                        break;
+                    }
+                }
+                if (flag) {
+                    deployment = repositoryService.createDeployment()
+                        .tenantId(TenantHelper.getTenantId())
+                        .addInputStream(filename, inputStream).name(processName).key(processKey).category(categoryCode).deploy();
+                } else {
+                    throw new ServiceException("文件类型上传错误!");
+                }
+            }
+            // 更新分类
+            ProcessDefinition definition = repositoryService.createProcessDefinitionQuery().deploymentId(deployment.getId()).singleResult();
+            repositoryService.setProcessDefinitionCategory(definition.getId(), categoryCode);
+
+            return true;
+        } catch (IOException e) {
+            e.printStackTrace();
+            throw new ServiceException("部署失败" + e.getMessage());
+        }
+    }
+}

+ 695 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/ActProcessInstanceServiceImpl.java

@@ -0,0 +1,695 @@
+package org.dromara.workflow.service.impl;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.codec.Base64;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import lombok.RequiredArgsConstructor;
+import lombok.SneakyThrows;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.io.IOUtils;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.workflow.common.constant.FlowConstant;
+import org.dromara.workflow.common.enums.BusinessStatusEnum;
+import org.dromara.workflow.common.enums.TaskStatusEnum;
+import org.dromara.workflow.domain.ActHiProcinst;
+import org.dromara.workflow.domain.bo.ProcessInstanceBo;
+import org.dromara.workflow.domain.bo.ProcessInvalidBo;
+import org.dromara.workflow.domain.bo.TaskUrgingBo;
+import org.dromara.workflow.domain.vo.ActHistoryInfoVo;
+import org.dromara.workflow.domain.vo.GraphicInfoVo;
+import org.dromara.workflow.domain.vo.ProcessInstanceVo;
+import org.dromara.workflow.domain.vo.TaskVo;
+import org.dromara.workflow.flowable.CustomDefaultProcessDiagramGenerator;
+import org.dromara.workflow.flowable.strategy.FlowEventStrategy;
+import org.dromara.workflow.flowable.cmd.DeleteExecutionCmd;
+import org.dromara.workflow.flowable.cmd.ExecutionChildByExecutionIdCmd;
+import org.dromara.workflow.flowable.strategy.FlowProcessEventHandler;
+import org.dromara.workflow.service.IActHiProcinstService;
+import org.dromara.workflow.service.IActProcessInstanceService;
+import org.dromara.workflow.utils.WorkflowUtils;
+import org.flowable.bpmn.model.*;
+import org.flowable.engine.*;
+import org.flowable.engine.history.HistoricActivityInstance;
+import org.flowable.engine.history.HistoricProcessInstance;
+import org.flowable.engine.history.HistoricProcessInstanceQuery;
+import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
+import org.flowable.engine.repository.ProcessDefinition;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.flowable.engine.runtime.ProcessInstanceQuery;
+import org.flowable.engine.task.Attachment;
+import org.flowable.engine.task.Comment;
+import org.flowable.task.api.Task;
+import org.flowable.task.api.history.HistoricTaskInstance;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.awt.*;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.*;
+import java.util.stream.Collectors;
+
+
+/**
+ * 流程实例 服务层实现
+ *
+ * @author may
+ */
+@Slf4j
+@RequiredArgsConstructor
+@Service
+public class ActProcessInstanceServiceImpl implements IActProcessInstanceService {
+
+    private final RepositoryService repositoryService;
+    private final RuntimeService runtimeService;
+    private final HistoryService historyService;
+    private final TaskService taskService;
+    private final IActHiProcinstService actHiProcinstService;
+    private final ManagementService managementService;
+    private final FlowEventStrategy flowEventStrategy;
+
+    @Value("${flowable.activity-font-name}")
+    private String activityFontName;
+
+    @Value("${flowable.label-font-name}")
+    private String labelFontName;
+
+    @Value("${flowable.annotation-font-name}")
+    private String annotationFontName;
+
+    /**
+     * 分页查询正在运行的流程实例
+     *
+     * @param processInstanceBo 参数
+     */
+    @Override
+    public TableDataInfo<ProcessInstanceVo> getProcessInstanceRunningByPage(ProcessInstanceBo processInstanceBo) {
+        List<ProcessInstanceVo> list = new ArrayList<>();
+        ProcessInstanceQuery query = runtimeService.createProcessInstanceQuery();
+        query.processInstanceTenantId(TenantHelper.getTenantId());
+        if (StringUtils.isNotBlank(processInstanceBo.getName())) {
+            query.processInstanceNameLikeIgnoreCase("%" + processInstanceBo.getName() + "%");
+        }
+        if (StringUtils.isNotBlank(processInstanceBo.getKey())) {
+            query.processDefinitionKey(processInstanceBo.getKey());
+        }
+        if (StringUtils.isNotBlank(processInstanceBo.getStartUserId())) {
+            query.startedBy(processInstanceBo.getStartUserId());
+        }
+        if (StringUtils.isNotBlank(processInstanceBo.getBusinessKey())) {
+            query.processInstanceBusinessKey(processInstanceBo.getBusinessKey());
+        }
+        if (StringUtils.isNotBlank(processInstanceBo.getCategoryCode())) {
+            query.processDefinitionCategory(processInstanceBo.getCategoryCode());
+        }
+        query.orderByStartTime().desc();
+        List<ProcessInstance> processInstances = query.listPage(processInstanceBo.getPageNum(), processInstanceBo.getPageSize());
+        for (ProcessInstance processInstance : processInstances) {
+            ProcessInstanceVo processInstanceVo = BeanUtil.toBean(processInstance, ProcessInstanceVo.class);
+            processInstanceVo.setIsSuspended(processInstance.isSuspended());
+            processInstanceVo.setBusinessStatusName(BusinessStatusEnum.findByStatus(processInstance.getBusinessStatus()));
+            list.add(processInstanceVo);
+        }
+        long count = query.count();
+        return new TableDataInfo<>(list, count);
+    }
+
+    /**
+     * 分页查询已结束的流程实例
+     *
+     * @param processInstanceBo 参数
+     */
+    @Override
+    public TableDataInfo<ProcessInstanceVo> getProcessInstanceFinishByPage(ProcessInstanceBo processInstanceBo) {
+        List<ProcessInstanceVo> list = new ArrayList<>();
+        HistoricProcessInstanceQuery query = historyService.createHistoricProcessInstanceQuery().finished()
+            .orderByProcessInstanceEndTime().desc();
+        query.processInstanceTenantId(TenantHelper.getTenantId());
+        if (StringUtils.isNotEmpty(processInstanceBo.getName())) {
+            query.processInstanceNameLikeIgnoreCase("%" + processInstanceBo.getName() + "%");
+        }
+        if (StringUtils.isNotBlank(processInstanceBo.getKey())) {
+            query.processDefinitionKey(processInstanceBo.getKey());
+        }
+        if (StringUtils.isNotEmpty(processInstanceBo.getStartUserId())) {
+            query.startedBy(processInstanceBo.getStartUserId());
+        }
+        if (StringUtils.isNotBlank(processInstanceBo.getBusinessKey())) {
+            query.processInstanceBusinessKey(processInstanceBo.getBusinessKey());
+        }
+        if (StringUtils.isNotBlank(processInstanceBo.getCategoryCode())) {
+            query.processDefinitionCategory(processInstanceBo.getCategoryCode());
+        }
+        List<HistoricProcessInstance> historicProcessInstances = query.listPage(processInstanceBo.getPageNum(), processInstanceBo.getPageSize());
+        for (HistoricProcessInstance historicProcessInstance : historicProcessInstances) {
+            ProcessInstanceVo processInstanceVo = BeanUtil.toBean(historicProcessInstance, ProcessInstanceVo.class);
+            processInstanceVo.setBusinessStatusName(BusinessStatusEnum.findByStatus(historicProcessInstance.getBusinessStatus()));
+            list.add(processInstanceVo);
+        }
+        long count = query.count();
+        return new TableDataInfo<>(list, count);
+    }
+
+    /**
+     * 通过流程实例id获取历史流程图
+     *
+     * @param processInstanceId 流程实例id
+     */
+    @SneakyThrows
+    @Override
+    public String getHistoryProcessImage(String processInstanceId) {
+        String processDefinitionId;
+        // 获取当前的流程实例
+        ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
+        // 如果流程已经结束,则得到结束节点
+        if (Objects.isNull(processInstance)) {
+            HistoricProcessInstance pi = historyService.createHistoricProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
+            processDefinitionId = pi.getProcessDefinitionId();
+        } else {
+            // 根据流程实例ID获得当前处于活动状态的ActivityId合集
+            ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
+            processDefinitionId = pi.getProcessDefinitionId();
+        }
+
+        // 获得活动的节点
+        List<HistoricActivityInstance> highLightedFlowList = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).orderByHistoricActivityInstanceStartTime().asc().list();
+
+        List<String> highLightedFlows = new ArrayList<>();
+        List<String> highLightedNodes = new ArrayList<>();
+        //高亮
+        for (HistoricActivityInstance tempActivity : highLightedFlowList) {
+            if (FlowConstant.SEQUENCE_FLOW.equals(tempActivity.getActivityType())) {
+                //高亮线
+                highLightedFlows.add(tempActivity.getActivityId());
+            } else {
+                //高亮节点
+                if (tempActivity.getEndTime() == null) {
+                    highLightedNodes.add(Color.RED.toString() + tempActivity.getActivityId());
+                } else {
+                    highLightedNodes.add(tempActivity.getActivityId());
+                }
+            }
+        }
+        List<String> highLightedNodeList = new ArrayList<>();
+        //运行中的节点
+        List<String> redNodeCollect = StreamUtils.filter(highLightedNodes, e -> e.contains(Color.RED.toString()));
+        //排除与运行中相同的节点
+        for (String nodeId : highLightedNodes) {
+            if (!nodeId.contains(Color.RED.toString()) && !redNodeCollect.contains(Color.RED + nodeId)) {
+                highLightedNodeList.add(nodeId);
+            }
+        }
+        highLightedNodeList.addAll(redNodeCollect);
+        BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefinitionId);
+        CustomDefaultProcessDiagramGenerator diagramGenerator = new CustomDefaultProcessDiagramGenerator();
+        InputStream inputStream = diagramGenerator.generateDiagram(bpmnModel, "png", highLightedNodeList, highLightedFlows, activityFontName, labelFontName, annotationFontName, null, 1.0, true);
+        return Base64.encode(IOUtils.toByteArray(inputStream));
+    }
+
+    /**
+     * 通过流程实例id获取历史流程图运行中,历史等节点
+     *
+     * @param processInstanceId 流程实例id
+     */
+    @Override
+    public Map<String, Object> getHistoryProcessList(String processInstanceId) {
+        Map<String, Object> map = new HashMap<>();
+        List<Map<String, Object>> taskList = new ArrayList<>();
+        HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
+        StringBuilder xml = new StringBuilder();
+        ProcessDefinition processDefinition = repositoryService.getProcessDefinition(historicProcessInstance.getProcessDefinitionId());
+        // 获取节点
+        List<HistoricActivityInstance> highLightedFlowList = historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId).orderByHistoricActivityInstanceStartTime().asc().list();
+        for (HistoricActivityInstance tempActivity : highLightedFlowList) {
+            Map<String, Object> task = new HashMap<>();
+            if (!FlowConstant.SEQUENCE_FLOW.equals(tempActivity.getActivityType()) &&
+                !FlowConstant.PARALLEL_GATEWAY.equals(tempActivity.getActivityType()) &&
+                !FlowConstant.EXCLUSIVE_GATEWAY.equals(tempActivity.getActivityType()) &&
+                !FlowConstant.INCLUSIVE_GATEWAY.equals(tempActivity.getActivityType())
+            ) {
+                task.put("key", tempActivity.getActivityId());
+                task.put("completed", tempActivity.getEndTime() != null);
+                task.put("activityType", tempActivity.getActivityType());
+                taskList.add(task);
+            }
+        }
+        ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(processInstanceId).singleResult();
+        if (processInstance != null) {
+            taskList = taskList.stream().filter(e -> !e.get("activityType").equals(FlowConstant.END_EVENT)).collect(Collectors.toList());
+        }
+        //查询出运行中节点
+        List<Map<String, Object>> runtimeNodeList = taskList.stream().filter(e -> !(Boolean) e.get("completed")).collect(Collectors.toList());
+        if (CollUtil.isNotEmpty(runtimeNodeList)) {
+            Iterator<Map<String, Object>> iterator = taskList.iterator();
+            while (iterator.hasNext()) {
+                Map<String, Object> next = iterator.next();
+                runtimeNodeList.stream().filter(t -> t.get("key").equals(next.get("key")) && (Boolean) next.get("completed")).findFirst().ifPresent(t -> iterator.remove());
+            }
+        }
+        map.put("taskList", taskList);
+        List<ActHistoryInfoVo> historyTaskList = getHistoryTaskList(processInstanceId);
+        map.put("historyList", historyTaskList);
+        InputStream inputStream;
+        try {
+            inputStream = repositoryService.getResourceAsStream(processDefinition.getDeploymentId(), processDefinition.getResourceName());
+            xml.append(IOUtils.toString(inputStream, String.valueOf(StandardCharsets.UTF_8)));
+            map.put("xml", xml.toString());
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return map;
+    }
+
+    /**
+     * 获取历史任务节点信息
+     *
+     * @param processInstanceId 流程实例id
+     */
+    private List<ActHistoryInfoVo> getHistoryTaskList(String processInstanceId) {
+        //查询任务办理记录
+        List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery().processInstanceId(processInstanceId).orderByHistoricTaskInstanceEndTime().desc().list();
+        list = StreamUtils.sorted(list, Comparator.comparing(HistoricTaskInstance::getEndTime, Comparator.nullsFirst(Date::compareTo)).reversed());
+        List<ActHistoryInfoVo> actHistoryInfoVoList = new ArrayList<>();
+        for (HistoricTaskInstance historicTaskInstance : list) {
+            ActHistoryInfoVo actHistoryInfoVo = new ActHistoryInfoVo();
+            BeanUtils.copyProperties(historicTaskInstance, actHistoryInfoVo);
+            actHistoryInfoVo.setStatus(actHistoryInfoVo.getEndTime() == null ? "待处理" : "已处理");
+            if (ObjectUtil.isNotEmpty(historicTaskInstance.getDurationInMillis())) {
+                actHistoryInfoVo.setRunDuration(getDuration(historicTaskInstance.getDurationInMillis()));
+            }
+            actHistoryInfoVoList.add(actHistoryInfoVo);
+        }
+        List<ActHistoryInfoVo> historyInfoVoList = new ArrayList<>();
+        Map<String, List<ActHistoryInfoVo>> groupByKey = StreamUtils.groupByKey(actHistoryInfoVoList, ActHistoryInfoVo::getTaskDefinitionKey);
+        for (Map.Entry<String, List<ActHistoryInfoVo>> entry : groupByKey.entrySet()) {
+            ActHistoryInfoVo historyInfoVo = new ActHistoryInfoVo();
+            BeanUtils.copyProperties(entry.getValue().get(0), historyInfoVo);
+            actHistoryInfoVoList.stream().filter(e -> e.getTaskDefinitionKey().equals(entry.getKey()) && e.getEndTime() == null).findFirst()
+                .ifPresent(e -> {
+                    historyInfoVo.setStatus("待处理");
+                    historyInfoVo.setStartTime(e.getStartTime());
+                    historyInfoVo.setEndTime(null);
+                    historyInfoVo.setRunDuration(null);
+                });
+            historyInfoVoList.add(historyInfoVo);
+        }
+        return historyInfoVoList;
+    }
+
+    /**
+     * 获取审批记录
+     *
+     * @param processInstanceId 流程实例id
+     */
+    @Override
+    public Map<String, Object> getHistoryRecord(String processInstanceId) {
+        Map<String, Object> map = new HashMap<>();
+        // 查询任务办理记录
+        List<HistoricTaskInstance> list = historyService.createHistoricTaskInstanceQuery()
+            .processInstanceId(processInstanceId).taskTenantId(TenantHelper.getTenantId()).orderByHistoricTaskInstanceEndTime().desc().list();
+        list = StreamUtils.sorted(list, Comparator.comparing(HistoricTaskInstance::getEndTime, Comparator.nullsFirst(Date::compareTo)).reversed());
+        List<ActHistoryInfoVo> actHistoryInfoVoList = new ArrayList<>();
+        List<Comment> processInstanceComments = taskService.getProcessInstanceComments(processInstanceId);
+        //附件
+        List<Attachment> attachmentList = taskService.getProcessInstanceAttachments(processInstanceId);
+        for (HistoricTaskInstance historicTaskInstance : list) {
+            ActHistoryInfoVo actHistoryInfoVo = new ActHistoryInfoVo();
+            BeanUtils.copyProperties(historicTaskInstance, actHistoryInfoVo);
+            if (actHistoryInfoVo.getEndTime() == null) {
+                actHistoryInfoVo.setStatus(TaskStatusEnum.WAITING.getStatus());
+                actHistoryInfoVo.setStatusName(TaskStatusEnum.WAITING.getDesc());
+            }
+            if (CollUtil.isNotEmpty(processInstanceComments)) {
+                processInstanceComments.stream().filter(e -> e.getTaskId().equals(historicTaskInstance.getId())).findFirst().ifPresent(e -> {
+                    actHistoryInfoVo.setComment(e.getFullMessage());
+                    actHistoryInfoVo.setStatus(e.getType());
+                    actHistoryInfoVo.setStatusName(TaskStatusEnum.findByStatus(e.getType()));
+                });
+            }
+            if (ObjectUtil.isNotEmpty(historicTaskInstance.getDurationInMillis())) {
+                actHistoryInfoVo.setRunDuration(getDuration(historicTaskInstance.getDurationInMillis()));
+            }
+            try {
+                actHistoryInfoVo.setAssignee(StringUtils.isNotBlank(historicTaskInstance.getAssignee()) ? Long.valueOf(historicTaskInstance.getAssignee()) : null);
+            } catch (NumberFormatException ignored) {
+                log.warn("当前任务【{}】,办理人转换人员ID【{}】异常!", historicTaskInstance.getName(), historicTaskInstance.getAssignee());
+            }
+            //附件
+            if (CollUtil.isNotEmpty(attachmentList)) {
+                List<Attachment> attachments = attachmentList.stream().filter(e -> e.getTaskId().equals(historicTaskInstance.getId())).collect(Collectors.toList());
+                if (CollUtil.isNotEmpty(attachments)) {
+                    actHistoryInfoVo.setAttachmentList(attachments);
+                }
+            }
+            actHistoryInfoVoList.add(actHistoryInfoVo);
+        }
+        List<ActHistoryInfoVo> collect = new ArrayList<>();
+        // 待办理
+        List<ActHistoryInfoVo> waitingTask = StreamUtils.filter(actHistoryInfoVoList, e -> e.getEndTime() == null);
+        // 已办理
+        List<ActHistoryInfoVo> finishTask = StreamUtils.filter(actHistoryInfoVoList, e -> e.getEndTime() != null);
+        collect.addAll(waitingTask);
+        collect.addAll(finishTask);
+        // 审批记录
+        map.put("historyRecordList", collect);
+        List<ActHistoryInfoVo> nodeInfoList = new ArrayList<>();
+        Map<String, List<ActHistoryInfoVo>> groupByKey = StreamUtils.groupByKey(actHistoryInfoVoList, ActHistoryInfoVo::getTaskDefinitionKey);
+        for (Map.Entry<String, List<ActHistoryInfoVo>> entry : groupByKey.entrySet()) {
+            ActHistoryInfoVo actHistoryInfoVo = BeanUtil.toBean(entry.getValue().get(0), ActHistoryInfoVo.class);
+            String nickName = entry.getValue().stream().filter(e -> StringUtils.isNotBlank(e.getNickName()) && e.getEndTime() == null).map(ActHistoryInfoVo::getNickName).toList().stream().distinct().collect(Collectors.joining(StringUtils.SEPARATOR));
+            if (StringUtils.isNotBlank(nickName)) {
+                actHistoryInfoVo.setNickName(nickName);
+            }
+            actHistoryInfoVoList.stream().filter(e -> e.getTaskDefinitionKey().equals(entry.getKey()) && e.getEndTime() != null).findFirst()
+                .ifPresent(e -> {
+                    actHistoryInfoVo.setStatus("已处理");
+                    actHistoryInfoVo.setStartTime(e.getStartTime());
+                });
+            actHistoryInfoVoList.stream().filter(e -> e.getTaskDefinitionKey().equals(entry.getKey()) && e.getEndTime() == null).findFirst()
+                .ifPresent(e -> {
+                    actHistoryInfoVo.setStatus("待处理");
+                    actHistoryInfoVo.setStartTime(e.getStartTime());
+                    actHistoryInfoVo.setEndTime(null);
+                    actHistoryInfoVo.setRunDuration(null);
+                });
+            nodeInfoList.add(actHistoryInfoVo);
+        }
+        // 节点信息
+        map.put("nodeListInfo", nodeInfoList);
+        BpmnModel bpmnModel = repositoryService.getBpmnModel(list.get(0).getProcessDefinitionId());
+        List<GraphicInfoVo> graphicInfoVos = new ArrayList<>();
+        Collection<FlowElement> flowElements = bpmnModel.getMainProcess().getFlowElements();
+        //节点图形信息
+        buildGraphicInfo(flowElements, graphicInfoVos, bpmnModel);
+        map.put("graphicInfoVos", graphicInfoVos);
+        return map;
+    }
+
+    /**
+     * 构建节点图形信息
+     *
+     * @param flowElements 节点
+     */
+    private static void buildGraphicInfo(Collection<FlowElement> flowElements, List<GraphicInfoVo> graphicInfoVos, BpmnModel bpmnModel) {
+        for (FlowElement flowElement : flowElements) {
+            if (flowElement instanceof SubProcess) {
+                Collection<FlowElement> subFlowElements = ((SubProcess) flowElement).getFlowElements();
+                buildGraphicInfo(subFlowElements, graphicInfoVos, bpmnModel);
+            } else {
+                if (flowElement instanceof UserTask) {
+                    GraphicInfo graphicInfo = bpmnModel.getGraphicInfo(flowElement.getId());
+                    GraphicInfoVo graphicInfoVo = BeanUtil.toBean(graphicInfo, GraphicInfoVo.class);
+                    graphicInfoVo.setNodeId(flowElement.getId());
+                    graphicInfoVo.setNodeName(flowElement.getName());
+                    graphicInfoVos.add(graphicInfoVo);
+                }
+            }
+        }
+    }
+
+    /**
+     * 任务完成时间处理
+     *
+     * @param time 时间
+     */
+    private String getDuration(long time) {
+
+        long day = time / (24 * 60 * 60 * 1000);
+        long hour = (time / (60 * 60 * 1000) - day * 24);
+        long minute = ((time / (60 * 1000)) - day * 24 * 60 - hour * 60);
+        long second = (time / 1000 - day * 24 * 60 * 60 - hour * 60 * 60 - minute * 60);
+
+        if (day > 0) {
+            return day + "天" + hour + "小时" + minute + "分钟";
+        }
+        if (hour > 0) {
+            return hour + "小时" + minute + "分钟";
+        }
+        if (minute > 0) {
+            return minute + "分钟";
+        }
+        if (second > 0) {
+            return second + "秒";
+        } else {
+            return 0 + "秒";
+        }
+    }
+
+    /**
+     * 作废流程实例,不会删除历史记录(删除运行中的实例)
+     *
+     * @param processInvalidBo 参数
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean deleteRuntimeProcessInst(ProcessInvalidBo processInvalidBo) {
+        try {
+            List<Task> list = taskService.createTaskQuery().processInstanceId(processInvalidBo.getProcessInstanceId())
+                .taskTenantId(TenantHelper.getTenantId()).list();
+            List<Task> subTasks = StreamUtils.filter(list, e -> StringUtils.isNotBlank(e.getParentTaskId()));
+            if (CollUtil.isNotEmpty(subTasks)) {
+                subTasks.forEach(e -> taskService.deleteTask(e.getId()));
+            }
+            String deleteReason = LoginHelper.getLoginUser().getNickname() + "作废了当前申请!";
+            if (StringUtils.isNotBlank(processInvalidBo.getDeleteReason())) {
+                deleteReason = LoginHelper.getLoginUser().getNickname() + "作废理由:" + processInvalidBo.getDeleteReason();
+            }
+            for (Task task : StreamUtils.filter(list, e -> StringUtils.isBlank(e.getParentTaskId()))) {
+                taskService.addComment(task.getId(), task.getProcessInstanceId(), TaskStatusEnum.INVALID.getStatus(), deleteReason);
+            }
+            HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery()
+                .processInstanceId(processInvalidBo.getProcessInstanceId()).processInstanceTenantId(TenantHelper.getTenantId()).singleResult();
+            if (ObjectUtil.isNotEmpty(historicProcessInstance) && BusinessStatusEnum.FINISH.getStatus().equals(historicProcessInstance.getBusinessStatus())) {
+                throw new ServiceException("该单据已完成申请!");
+            }
+            runtimeService.updateBusinessStatus(processInvalidBo.getProcessInstanceId(), BusinessStatusEnum.INVALID.getStatus());
+            runtimeService.deleteProcessInstance(processInvalidBo.getProcessInstanceId(), deleteReason);
+            FlowProcessEventHandler processHandler = flowEventStrategy.getProcessHandler(historicProcessInstance.getProcessDefinitionKey());
+            if (processHandler != null) {
+                processHandler.handleProcess(historicProcessInstance.getBusinessKey(), BusinessStatusEnum.INVALID.getStatus(), false);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new ServiceException(e.getMessage());
+        }
+    }
+
+    /**
+     * 运行中的实例 删除程实例,删除历史记录,删除业务与流程关联信息
+     *
+     * @param processInstanceIds 流程实例id
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean deleteRuntimeProcessAndHisInst(List<String> processInstanceIds) {
+        try {
+            // 1.删除运行中流程实例
+            List<Task> list = taskService.createTaskQuery().processInstanceIdIn(processInstanceIds)
+                .taskTenantId(TenantHelper.getTenantId()).list();
+            List<Task> subTasks = StreamUtils.filter(list, e -> StringUtils.isNotBlank(e.getParentTaskId()));
+            if (CollUtil.isNotEmpty(subTasks)) {
+                subTasks.forEach(e -> taskService.deleteTask(e.getId()));
+            }
+            runtimeService.bulkDeleteProcessInstances(processInstanceIds, LoginHelper.getUserId() + "删除了当前流程申请");
+            // 2.删除历史记录
+            List<HistoricProcessInstance> historicProcessInstanceList = historyService.createHistoricProcessInstanceQuery()
+                .processInstanceTenantId(TenantHelper.getTenantId()).processInstanceIds(new HashSet<>(processInstanceIds)).list();
+            if (ObjectUtil.isNotEmpty(historicProcessInstanceList)) {
+                historyService.bulkDeleteHistoricProcessInstances(processInstanceIds);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new ServiceException(e.getMessage());
+        }
+    }
+
+    /**
+     * 按照业务id删除 运行中的实例 删除程实例,删除历史记录,删除业务与流程关联信息
+     *
+     * @param businessKeys 业务id
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean deleteRuntimeProcessAndHisInstByBusinessKeys(List<String> businessKeys) {
+        try {
+            // 1.删除运行中流程实例
+            List<ActHiProcinst> actHiProcinsts = actHiProcinstService.selectByBusinessKeyIn(businessKeys);
+            if (CollUtil.isEmpty(actHiProcinsts)) {
+                log.warn("当前业务ID:{}查询到流程实例为空!", businessKeys);
+                return false;
+            }
+            List<String> processInstanceIds = StreamUtils.toList(actHiProcinsts, ActHiProcinst::getId);
+            List<Task> list = taskService.createTaskQuery().processInstanceIdIn(processInstanceIds)
+                .taskTenantId(TenantHelper.getTenantId()).list();
+            List<Task> subTasks = StreamUtils.filter(list, e -> StringUtils.isNotBlank(e.getParentTaskId()));
+            if (CollUtil.isNotEmpty(subTasks)) {
+                subTasks.forEach(e -> taskService.deleteTask(e.getId()));
+            }
+            runtimeService.bulkDeleteProcessInstances(processInstanceIds, LoginHelper.getUserId() + "删除了当前流程申请");
+            // 2.删除历史记录
+            List<HistoricProcessInstance> historicProcessInstanceList = historyService.createHistoricProcessInstanceQuery()
+                .processInstanceTenantId(TenantHelper.getTenantId()).processInstanceIds(new HashSet<>(processInstanceIds)).list();
+            if (ObjectUtil.isNotEmpty(historicProcessInstanceList)) {
+                historyService.bulkDeleteHistoricProcessInstances(processInstanceIds);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new ServiceException(e.getMessage());
+        }
+    }
+
+    /**
+     * 已完成的实例 删除程实例,删除历史记录,删除业务与流程关联信息
+     *
+     * @param processInstanceIds 流程实例id
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean deleteFinishProcessAndHisInst(List<String> processInstanceIds) {
+        try {
+            historyService.bulkDeleteHistoricProcessInstances(processInstanceIds);
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new ServiceException(e.getMessage());
+        }
+    }
+
+    /**
+     * 撤销流程申请
+     *
+     * @param processInstanceId 流程实例id
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean cancelProcessApply(String processInstanceId) {
+        try {
+            ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
+                .processInstanceId(processInstanceId).processInstanceTenantId(TenantHelper.getTenantId()).startedBy(String.valueOf(LoginHelper.getUserId())).singleResult();
+            if (ObjectUtil.isNull(processInstance)) {
+                throw new ServiceException("您不是流程发起人,撤销失败!");
+            }
+            if (processInstance.isSuspended()) {
+                throw new ServiceException(FlowConstant.MESSAGE_SUSPENDED);
+            }
+            if (BusinessStatusEnum.CANCEL.getStatus().equals(processInstance.getBusinessStatus())) {
+                throw new ServiceException("该单据已撤销!");
+            }
+            List<Task> taskList = taskService.createTaskQuery().taskTenantId(TenantHelper.getTenantId()).processInstanceId(processInstanceId).list();
+            for (Task task : taskList) {
+                taskService.setAssignee(task.getId(), String.valueOf(LoginHelper.getUserId()));
+                taskService.addComment(task.getId(), processInstanceId, TaskStatusEnum.CANCEL.getStatus(), LoginHelper.getLoginUser().getNickname() + ":撤销申请");
+            }
+            HistoricTaskInstance historicTaskInstance = historyService.createHistoricTaskInstanceQuery().finished().orderByHistoricTaskInstanceEndTime().asc().list().get(0);
+            List<String> nodeIds = StreamUtils.toList(taskList, Task::getTaskDefinitionKey);
+            runtimeService.createChangeActivityStateBuilder()
+                .processInstanceId(processInstanceId)
+                .moveActivityIdsToSingleActivityId(nodeIds, historicTaskInstance.getTaskDefinitionKey()).changeState();
+            Task task = taskService.createTaskQuery().taskTenantId(TenantHelper.getTenantId()).processInstanceId(processInstanceId).list().get(0);
+            taskService.setAssignee(task.getId(), historicTaskInstance.getAssignee());
+            //获取并行网关执行后保留的执行实例数据
+            ExecutionChildByExecutionIdCmd childByExecutionIdCmd = new ExecutionChildByExecutionIdCmd(task.getExecutionId());
+            List<ExecutionEntity> executionEntities = managementService.executeCommand(childByExecutionIdCmd);
+            //删除流程实例垃圾数据
+            for (ExecutionEntity executionEntity : executionEntities) {
+                DeleteExecutionCmd deleteExecutionCmd = new DeleteExecutionCmd(executionEntity.getId());
+                managementService.executeCommand(deleteExecutionCmd);
+            }
+            runtimeService.updateBusinessStatus(processInstanceId, BusinessStatusEnum.CANCEL.getStatus());
+            FlowProcessEventHandler processHandler = flowEventStrategy.getProcessHandler(processInstance.getProcessDefinitionKey());
+            if (processHandler != null) {
+                processHandler.handleProcess(processInstance.getBusinessKey(), BusinessStatusEnum.CANCEL.getStatus(), false);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new ServiceException("撤销失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 分页查询当前登录人单据
+     *
+     * @param processInstanceBo 参数
+     */
+    @Override
+    public TableDataInfo<ProcessInstanceVo> getCurrentSubmitByPage(ProcessInstanceBo processInstanceBo) {
+        List<ProcessInstanceVo> list = new ArrayList<>();
+        HistoricProcessInstanceQuery query = historyService.createHistoricProcessInstanceQuery();
+        query.processInstanceTenantId(TenantHelper.getTenantId());
+        query.startedBy(processInstanceBo.getStartUserId());
+        if (StringUtils.isNotBlank(processInstanceBo.getName())) {
+            query.processInstanceNameLikeIgnoreCase("%" + processInstanceBo.getName() + "%");
+        }
+        if (StringUtils.isNotBlank(processInstanceBo.getKey())) {
+            query.processDefinitionKey(processInstanceBo.getKey());
+        }
+        if (StringUtils.isNotBlank(processInstanceBo.getBusinessKey())) {
+            query.processInstanceBusinessKey(processInstanceBo.getBusinessKey());
+        }
+        if (StringUtils.isNotBlank(processInstanceBo.getCategoryCode())) {
+            query.processDefinitionCategory(processInstanceBo.getCategoryCode());
+        }
+        query.orderByProcessInstanceStartTime().desc();
+        List<HistoricProcessInstance> historicProcessInstanceList = query.listPage(processInstanceBo.getPageNum(), processInstanceBo.getPageSize());
+        List<TaskVo> taskVoList = new ArrayList<>();
+        if (CollUtil.isNotEmpty(historicProcessInstanceList)) {
+            List<String> processInstanceIds = StreamUtils.toList(historicProcessInstanceList, HistoricProcessInstance::getId);
+            List<Task> taskList = taskService.createTaskQuery().processInstanceIdIn(processInstanceIds).taskTenantId(TenantHelper.getTenantId()).list();
+            for (Task task : taskList) {
+                taskVoList.add(BeanUtil.toBean(task, TaskVo.class));
+            }
+        }
+        for (HistoricProcessInstance processInstance : historicProcessInstanceList) {
+            ProcessInstanceVo processInstanceVo = BeanUtil.toBean(processInstance, ProcessInstanceVo.class);
+            processInstanceVo.setBusinessStatusName(BusinessStatusEnum.findByStatus(processInstance.getBusinessStatus()));
+            if (CollUtil.isNotEmpty(taskVoList)) {
+                List<TaskVo> collect = StreamUtils.filter(taskVoList, e -> e.getProcessInstanceId().equals(processInstance.getId()));
+                processInstanceVo.setTaskVoList(CollUtil.isNotEmpty(collect) ? collect : Collections.emptyList());
+            }
+            list.add(processInstanceVo);
+        }
+        long count = query.count();
+        return new TableDataInfo<>(list, count);
+    }
+
+    /**
+     * 任务催办(给当前任务办理人发送站内信,邮件,短信等)
+     *
+     * @param taskUrgingBo 任务催办
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean taskUrging(TaskUrgingBo taskUrgingBo) {
+        try {
+            ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
+                .processInstanceId(taskUrgingBo.getProcessInstanceId())
+                .processInstanceTenantId(TenantHelper.getTenantId()).singleResult();
+            if (processInstance == null) {
+                throw new ServiceException("任务已结束!");
+            }
+            String message = taskUrgingBo.getMessage();
+            if (StringUtils.isBlank(message)) {
+                message = "您的【" + processInstance.getName() + "】单据还未审批,请您及时处理。";
+            }
+            List<Task> list = taskService.createTaskQuery().processInstanceId(taskUrgingBo.getProcessInstanceId()).list();
+            WorkflowUtils.sendMessage(list, processInstance.getName(), taskUrgingBo.getMessageType(), message);
+        } catch (ServiceException e) {
+            throw new ServiceException(e.getMessage());
+        }
+        return true;
+    }
+}

+ 708 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/ActTaskServiceImpl.java

@@ -0,0 +1,708 @@
+package org.dromara.workflow.service.impl;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.convert.Convert;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.domain.dto.RoleDTO;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.satoken.utils.LoginHelper;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.workflow.common.constant.FlowConstant;
+import org.dromara.workflow.common.enums.BusinessStatusEnum;
+import org.dromara.workflow.common.enums.TaskStatusEnum;
+import org.dromara.workflow.domain.bo.*;
+import org.dromara.workflow.domain.vo.MultiInstanceVo;
+import org.dromara.workflow.domain.vo.TaskVo;
+import org.dromara.workflow.domain.vo.WfCopy;
+import org.dromara.workflow.flowable.strategy.FlowEventStrategy;
+import org.dromara.workflow.flowable.cmd.*;
+import org.dromara.workflow.flowable.strategy.FlowProcessEventHandler;
+import org.dromara.workflow.flowable.strategy.FlowTaskEventHandler;
+import org.dromara.workflow.mapper.ActTaskMapper;
+import org.dromara.workflow.service.IActTaskService;
+import org.dromara.workflow.utils.WorkflowUtils;
+import org.flowable.common.engine.api.FlowableObjectNotFoundException;
+import org.flowable.common.engine.impl.identity.Authentication;
+import org.flowable.engine.*;
+import org.flowable.engine.history.HistoricProcessInstance;
+import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior;
+import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior;
+import org.flowable.engine.impl.persistence.entity.ExecutionEntity;
+import org.flowable.engine.runtime.ProcessInstance;
+import org.flowable.task.api.Task;
+import org.flowable.task.api.TaskQuery;
+import org.flowable.task.api.history.HistoricTaskInstance;
+import org.flowable.task.api.history.HistoricTaskInstanceQuery;
+import org.flowable.task.service.impl.persistence.entity.TaskEntity;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static org.dromara.workflow.common.constant.FlowConstant.FLOWABLE_SKIP_EXPRESSION_ENABLED;
+import static org.dromara.workflow.common.constant.FlowConstant.INITIATOR;
+
+/**
+ * 任务 服务层实现
+ *
+ * @author may
+ */
+@RequiredArgsConstructor
+@Service
+public class ActTaskServiceImpl implements IActTaskService {
+
+    private final RuntimeService runtimeService;
+    private final TaskService taskService;
+    private final HistoryService historyService;
+    private final IdentityService identityService;
+    private final ManagementService managementService;
+    private final FlowEventStrategy flowEventStrategy;
+    private final ActTaskMapper actTaskMapper;
+
+    /**
+     * 启动任务
+     *
+     * @param startProcessBo 启动流程参数
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Map<String, Object> startWorkFlow(StartProcessBo startProcessBo) {
+        Map<String, Object> map = new HashMap<>();
+        if (StringUtils.isBlank(startProcessBo.getBusinessKey())) {
+            throw new ServiceException("启动工作流时必须包含业务ID");
+        }
+        // 判断当前业务是否启动过流程
+        HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery().processInstanceBusinessKey(startProcessBo.getBusinessKey()).processInstanceTenantId(TenantHelper.getTenantId()).singleResult();
+        if (ObjectUtil.isNotEmpty(historicProcessInstance)) {
+            BusinessStatusEnum.checkStartStatus(historicProcessInstance.getBusinessStatus());
+        }
+        TaskQuery taskQuery = taskService.createTaskQuery();
+        List<Task> taskResult = taskQuery.processInstanceBusinessKey(startProcessBo.getBusinessKey()).taskTenantId(TenantHelper.getTenantId()).list();
+        if (CollUtil.isNotEmpty(taskResult)) {
+            if (CollUtil.isNotEmpty(startProcessBo.getVariables())) {
+                taskService.setVariables(taskResult.get(0).getId(), startProcessBo.getVariables());
+            }
+            map.put("processInstanceId", taskResult.get(0).getProcessInstanceId());
+            map.put("taskId", taskResult.get(0).getId());
+            return map;
+        }
+        // 设置启动人
+        identityService.setAuthenticatedUserId(String.valueOf(LoginHelper.getUserId()));
+        Authentication.setAuthenticatedUserId(String.valueOf(LoginHelper.getUserId()));
+        // 启动流程实例(提交申请)
+        Map<String, Object> variables = startProcessBo.getVariables();
+        // 启动跳过表达式
+        variables.put(FLOWABLE_SKIP_EXPRESSION_ENABLED, true);
+        // 流程发起人
+        variables.put(INITIATOR, (String.valueOf(LoginHelper.getUserId())));
+        ProcessInstance pi;
+        try {
+            pi = runtimeService.startProcessInstanceByKeyAndTenantId(startProcessBo.getProcessKey(), startProcessBo.getBusinessKey(), variables, TenantHelper.getTenantId());
+        } catch (FlowableObjectNotFoundException e) {
+            throw new ServiceException("找不到当前【" + startProcessBo.getProcessKey() + "】流程定义!");
+        }
+        // 将流程定义名称 作为 流程实例名称
+        runtimeService.setProcessInstanceName(pi.getProcessInstanceId(), pi.getProcessDefinitionName());
+        // 申请人执行流程
+        List<Task> taskList = taskService.createTaskQuery().processInstanceId(pi.getId()).taskTenantId(TenantHelper.getTenantId()).list();
+        if (taskList.size() > 1) {
+            throw new ServiceException("请检查流程第一个环节是否为申请人!");
+        }
+
+        runtimeService.updateBusinessStatus(pi.getProcessInstanceId(), BusinessStatusEnum.DRAFT.getStatus());
+        taskService.setAssignee(taskList.get(0).getId(), LoginHelper.getUserId().toString());
+        taskService.setVariable(taskList.get(0).getId(), "processInstanceId", pi.getProcessInstanceId());
+        map.put("processInstanceId", pi.getProcessInstanceId());
+        map.put("taskId", taskList.get(0).getId());
+        return map;
+    }
+
+    /**
+     * 办理任务
+     *
+     * @param completeTaskBo 办理任务参数
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean completeTask(CompleteTaskBo completeTaskBo) {
+        try {
+            List<RoleDTO> roles = LoginHelper.getLoginUser().getRoles();
+            String userId = String.valueOf(LoginHelper.getUserId());
+            TaskQuery taskQuery = taskService.createTaskQuery();
+            taskQuery.taskId(completeTaskBo.getTaskId()).taskTenantId(TenantHelper.getTenantId()).taskCandidateOrAssigned(userId);
+            if (CollUtil.isNotEmpty(roles)) {
+                List<String> groupIds = StreamUtils.toList(roles, e -> String.valueOf(e.getRoleId()));
+                taskQuery.taskCandidateGroupIn(groupIds);
+            }
+            Task task = taskQuery.singleResult();
+            if (task == null) {
+                throw new ServiceException(FlowConstant.MESSAGE_CURRENT_TASK_IS_NULL);
+            }
+            if (task.isSuspended()) {
+                throw new ServiceException(FlowConstant.MESSAGE_SUSPENDED);
+            }
+            ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(task.getProcessInstanceId()).singleResult();
+            //办理委托任务
+            if (ObjectUtil.isNotEmpty(task.getDelegationState()) && FlowConstant.PENDING.equals(task.getDelegationState().name())) {
+                taskService.resolveTask(completeTaskBo.getTaskId());
+                TaskEntity newTask = WorkflowUtils.createNewTask(task);
+                taskService.addComment(newTask.getId(), task.getProcessInstanceId(), completeTaskBo.getMessage());
+                taskService.complete(newTask.getId());
+                return true;
+            }
+            //附件上传
+            AttachmentCmd attachmentCmd = new AttachmentCmd(completeTaskBo.getFileId(), task.getId(), task.getProcessInstanceId());
+            managementService.executeCommand(attachmentCmd);
+            FlowProcessEventHandler processHandler = flowEventStrategy.getProcessHandler(processInstance.getProcessDefinitionKey());
+            String businessStatus = WorkflowUtils.getBusinessStatus(task.getProcessInstanceId());
+            if (BusinessStatusEnum.DRAFT.getStatus().equals(businessStatus) || BusinessStatusEnum.BACK.getStatus().equals(businessStatus) || BusinessStatusEnum.CANCEL.getStatus().equals(businessStatus)) {
+                if (processHandler != null) {
+                    processHandler.handleProcess(processInstance.getBusinessKey(), businessStatus, true);
+                }
+            }
+            runtimeService.updateBusinessStatus(task.getProcessInstanceId(), BusinessStatusEnum.WAITING.getStatus());
+            String key = processInstance.getProcessDefinitionKey() + "_" + task.getTaskDefinitionKey();
+            FlowTaskEventHandler taskHandler = flowEventStrategy.getTaskHandler(key);
+            if (taskHandler != null) {
+                taskHandler.handleTask(task, processInstance.getBusinessKey());
+            }
+            //办理意见
+            taskService.addComment(completeTaskBo.getTaskId(), task.getProcessInstanceId(), TaskStatusEnum.PASS.getStatus(), StringUtils.isBlank(completeTaskBo.getMessage()) ? "同意" : completeTaskBo.getMessage());
+            //办理任务
+            if (CollUtil.isNotEmpty(completeTaskBo.getVariables())) {
+                taskService.complete(completeTaskBo.getTaskId(), completeTaskBo.getVariables());
+            } else {
+                taskService.complete(completeTaskBo.getTaskId());
+            }
+            ProcessInstance pi = runtimeService.createProcessInstanceQuery().processInstanceId(task.getProcessInstanceId())
+                .processInstanceTenantId(TenantHelper.getTenantId()).singleResult();
+            if (pi == null) {
+                UpdateBusinessStatusCmd updateBusinessStatusCmd = new UpdateBusinessStatusCmd(task.getProcessInstanceId(), BusinessStatusEnum.FINISH.getStatus());
+                managementService.executeCommand(updateBusinessStatusCmd);
+                if (processHandler != null) {
+                    processHandler.handleProcess(processInstance.getBusinessKey(), BusinessStatusEnum.FINISH.getStatus(), false);
+                }
+            } else {
+                List<Task> list = taskService.createTaskQuery().taskTenantId(TenantHelper.getTenantId()).processInstanceId(task.getProcessInstanceId()).list();
+                if (CollUtil.isNotEmpty(list) && CollUtil.isNotEmpty(completeTaskBo.getWfCopyList())) {
+                    TaskEntity newTask = WorkflowUtils.createNewTask(task);
+                    taskService.addComment(newTask.getId(), task.getProcessInstanceId(), TaskStatusEnum.COPY.getStatus(),
+                        LoginHelper.getLoginUser().getNickname() + "【抄送】给" + String.join(",", StreamUtils.toList(completeTaskBo.getWfCopyList(), WfCopy::getUserName)));
+                    taskService.complete(newTask.getId());
+                    List<Task> taskList = taskService.createTaskQuery().processInstanceId(task.getProcessInstanceId()).list();
+                    WorkflowUtils.createCopyTask(taskList, StreamUtils.toList(completeTaskBo.getWfCopyList(), WfCopy::getUserId));
+                }
+                sendMessage(list, processInstance.getName(), completeTaskBo.getMessageType(), null);
+            }
+            return true;
+        } catch (Exception e) {
+            throw new ServiceException(e.getMessage());
+        }
+    }
+
+    /**
+     * 发送消息
+     *
+     * @param list        任务
+     * @param name        流程名称
+     * @param messageType 消息类型
+     * @param message     消息内容,为空则发送默认配置的消息内容
+     */
+    @Async
+    public void sendMessage(List<Task> list, String name, List<String> messageType, String message) {
+        WorkflowUtils.sendMessage(list, name, messageType, message);
+    }
+
+    /**
+     * 查询当前用户的待办任务
+     *
+     * @param taskBo 参数
+     */
+    @Override
+    public TableDataInfo<TaskVo> getTaskWaitByPage(TaskBo taskBo) {
+        PageQuery pageQuery = new PageQuery();
+        pageQuery.setPageNum(taskBo.getPageNum());
+        pageQuery.setPageSize(taskBo.getPageSize());
+        QueryWrapper<TaskVo> queryWrapper = new QueryWrapper<>();
+        List<RoleDTO> roles = LoginHelper.getLoginUser().getRoles();
+        String userId = String.valueOf(LoginHelper.getUserId());
+        queryWrapper.eq("t.business_status_", BusinessStatusEnum.WAITING.getStatus());
+        queryWrapper.eq("t.tenant_id_", TenantHelper.getTenantId());
+        queryWrapper.and(w1 ->
+            w1.eq("t.assignee_", userId)
+                .or(w2 -> w2.isNull("t.assignee_")
+                    .and(w3 -> w3.eq("t.user_id_", userId).or().in("t.group_id_", StreamUtils.toList(roles, RoleDTO::getRoleId))))
+        );
+        if (StringUtils.isNotBlank(taskBo.getName())) {
+            queryWrapper.like("t.name_", taskBo.getName());
+        }
+        if (StringUtils.isNotBlank(taskBo.getProcessDefinitionName())) {
+            queryWrapper.like("t.processDefinitionName", taskBo.getProcessDefinitionName());
+        }
+        if (StringUtils.isNotBlank(taskBo.getProcessDefinitionKey())) {
+            queryWrapper.eq("t.processDefinitionKey", taskBo.getProcessDefinitionKey());
+        }
+        Page<TaskVo> page = actTaskMapper.getTaskWaitByPage(pageQuery.build(), queryWrapper);
+
+        List<TaskVo> taskList = page.getRecords();
+        for (TaskVo task : taskList) {
+            task.setBusinessStatusName(BusinessStatusEnum.findByStatus(task.getBusinessStatus()));
+            task.setParticipantVo(WorkflowUtils.getCurrentTaskParticipant(task.getId()));
+            task.setMultiInstance(WorkflowUtils.isMultiInstance(task.getProcessDefinitionId(), task.getTaskDefinitionKey()) != null);
+        }
+        return new TableDataInfo<>(taskList, page.getTotal());
+    }
+
+    /**
+     * 查询当前租户所有待办任务
+     *
+     * @param taskBo 参数
+     */
+    @Override
+    public TableDataInfo<TaskVo> getAllTaskWaitByPage(TaskBo taskBo) {
+        TaskQuery query = taskService.createTaskQuery().taskTenantId(TenantHelper.getTenantId());
+        if (StringUtils.isNotBlank(taskBo.getName())) {
+            query.taskNameLike("%" + taskBo.getName() + "%");
+        }
+        if (StringUtils.isNotBlank(taskBo.getProcessDefinitionName())) {
+            query.processDefinitionNameLike("%" + taskBo.getProcessDefinitionName() + "%");
+        }
+        if (StringUtils.isNotBlank(taskBo.getProcessDefinitionKey())) {
+            query.processDefinitionKey(taskBo.getProcessDefinitionKey());
+        }
+        query.orderByTaskCreateTime().desc();
+        List<Task> taskList = query.listPage(taskBo.getPageNum(), taskBo.getPageSize());
+        List<ProcessInstance> processInstanceList = null;
+        if (CollUtil.isNotEmpty(taskList)) {
+            Set<String> processInstanceIds = StreamUtils.toSet(taskList, Task::getProcessInstanceId);
+            processInstanceList = runtimeService.createProcessInstanceQuery().processInstanceIds(processInstanceIds).list();
+        }
+        List<TaskVo> list = new ArrayList<>();
+        for (Task task : taskList) {
+            TaskVo taskVo = BeanUtil.toBean(task, TaskVo.class);
+            if (CollUtil.isNotEmpty(processInstanceList)) {
+                processInstanceList.stream().filter(e -> e.getId().equals(task.getProcessInstanceId())).findFirst().ifPresent(e -> {
+                    taskVo.setBusinessStatus(e.getBusinessStatus());
+                    taskVo.setBusinessStatusName(BusinessStatusEnum.findByStatus(taskVo.getBusinessStatus()));
+                    taskVo.setProcessDefinitionKey(e.getProcessDefinitionKey());
+                    taskVo.setProcessDefinitionName(e.getProcessDefinitionName());
+                });
+            }
+            taskVo.setAssignee(StringUtils.isNotBlank(task.getAssignee()) ? Long.valueOf(task.getAssignee()) : null);
+            taskVo.setParticipantVo(WorkflowUtils.getCurrentTaskParticipant(task.getId()));
+            taskVo.setMultiInstance(WorkflowUtils.isMultiInstance(task.getProcessDefinitionId(), task.getTaskDefinitionKey()) != null);
+            list.add(taskVo);
+        }
+        long count = query.count();
+        return new TableDataInfo<>(list, count);
+    }
+
+    /**
+     * 查询当前用户的已办任务
+     *
+     * @param taskBo 参数
+     */
+    @Override
+    public TableDataInfo<TaskVo> getTaskFinishByPage(TaskBo taskBo) {
+        String userId = String.valueOf(LoginHelper.getUserId());
+        HistoricTaskInstanceQuery query = historyService.createHistoricTaskInstanceQuery().taskAssignee(userId).taskTenantId(TenantHelper.getTenantId()).finished().orderByHistoricTaskInstanceStartTime().desc();
+        if (StringUtils.isNotBlank(taskBo.getName())) {
+            query.taskNameLike("%" + taskBo.getName() + "%");
+        }
+        if (StringUtils.isNotBlank(taskBo.getProcessDefinitionName())) {
+            query.processDefinitionNameLike("%" + taskBo.getProcessDefinitionName() + "%");
+        }
+        if (StringUtils.isNotBlank(taskBo.getProcessDefinitionKey())) {
+            query.processDefinitionKey(taskBo.getProcessDefinitionKey());
+        }
+        List<HistoricTaskInstance> taskInstanceList = query.listPage(taskBo.getPageNum(), taskBo.getPageSize());
+        List<HistoricProcessInstance> historicProcessInstanceList = null;
+        if (CollUtil.isNotEmpty(taskInstanceList)) {
+            Set<String> processInstanceIds = StreamUtils.toSet(taskInstanceList, HistoricTaskInstance::getProcessInstanceId);
+            historicProcessInstanceList = historyService.createHistoricProcessInstanceQuery().processInstanceIds(processInstanceIds).list();
+        }
+        List<TaskVo> list = new ArrayList<>();
+        for (HistoricTaskInstance task : taskInstanceList) {
+            TaskVo taskVo = BeanUtil.toBean(task, TaskVo.class);
+            if (CollUtil.isNotEmpty(historicProcessInstanceList)) {
+                historicProcessInstanceList.stream().filter(e -> e.getId().equals(task.getProcessInstanceId())).findFirst().ifPresent(e -> {
+                    taskVo.setBusinessStatus(e.getBusinessStatus());
+                    taskVo.setBusinessStatusName(BusinessStatusEnum.findByStatus(taskVo.getBusinessStatus()));
+                    taskVo.setProcessDefinitionKey(e.getProcessDefinitionKey());
+                    taskVo.setProcessDefinitionName(e.getProcessDefinitionName());
+                });
+            }
+            taskVo.setAssignee(StringUtils.isNotBlank(task.getAssignee()) ? Long.valueOf(task.getAssignee()) : null);
+            list.add(taskVo);
+        }
+        long count = query.count();
+        return new TableDataInfo<>(list, count);
+    }
+
+    /**
+     * 查询当前用户的抄送
+     *
+     * @param taskBo 参数
+     */
+    @Override
+    public TableDataInfo<TaskVo> getTaskCopyByPage(TaskBo taskBo) {
+        PageQuery pageQuery = new PageQuery();
+        pageQuery.setPageNum(taskBo.getPageNum());
+        pageQuery.setPageSize(taskBo.getPageSize());
+        QueryWrapper<TaskVo> queryWrapper = new QueryWrapper<>();
+        String userId = String.valueOf(LoginHelper.getUserId());
+        if (StringUtils.isNotBlank(taskBo.getName())) {
+            queryWrapper.like("t.name_", taskBo.getName());
+        }
+        if (StringUtils.isNotBlank(taskBo.getProcessDefinitionName())) {
+            queryWrapper.like("t.processDefinitionName", taskBo.getProcessDefinitionName());
+        }
+        if (StringUtils.isNotBlank(taskBo.getProcessDefinitionKey())) {
+            queryWrapper.eq("t.processDefinitionKey", taskBo.getProcessDefinitionKey());
+        }
+        queryWrapper.eq("t.assignee_", userId);
+        Page<TaskVo> page = actTaskMapper.getTaskCopyByPage(pageQuery.build(), queryWrapper);
+
+        List<TaskVo> taskList = page.getRecords();
+        for (TaskVo task : taskList) {
+            task.setBusinessStatusName(BusinessStatusEnum.findByStatus(task.getBusinessStatus()));
+            task.setMultiInstance(WorkflowUtils.isMultiInstance(task.getProcessDefinitionId(), task.getTaskDefinitionKey()) != null);
+        }
+        return new TableDataInfo<>(taskList, page.getTotal());
+    }
+
+    /**
+     * 查询当前租户所有已办任务
+     *
+     * @param taskBo 参数
+     */
+    @Override
+    public TableDataInfo<TaskVo> getAllTaskFinishByPage(TaskBo taskBo) {
+        HistoricTaskInstanceQuery query = historyService.createHistoricTaskInstanceQuery().taskTenantId(TenantHelper.getTenantId()).finished().orderByHistoricTaskInstanceStartTime().desc();
+        if (StringUtils.isNotBlank(taskBo.getName())) {
+            query.taskNameLike("%" + taskBo.getName() + "%");
+        }
+        if (StringUtils.isNotBlank(taskBo.getProcessDefinitionName())) {
+            query.processDefinitionNameLike("%" + taskBo.getProcessDefinitionName() + "%");
+        }
+        if (StringUtils.isNotBlank(taskBo.getProcessDefinitionKey())) {
+            query.processDefinitionKey(taskBo.getProcessDefinitionKey());
+        }
+        List<HistoricTaskInstance> taskInstanceList = query.listPage(taskBo.getPageNum(), taskBo.getPageSize());
+        List<HistoricProcessInstance> historicProcessInstanceList = null;
+        if (CollUtil.isNotEmpty(taskInstanceList)) {
+            Set<String> processInstanceIds = StreamUtils.toSet(taskInstanceList, HistoricTaskInstance::getProcessInstanceId);
+            historicProcessInstanceList = historyService.createHistoricProcessInstanceQuery().processInstanceIds(processInstanceIds).list();
+        }
+        List<TaskVo> list = new ArrayList<>();
+        for (HistoricTaskInstance task : taskInstanceList) {
+            TaskVo taskVo = BeanUtil.toBean(task, TaskVo.class);
+            if (CollUtil.isNotEmpty(historicProcessInstanceList)) {
+                historicProcessInstanceList.stream().filter(e -> e.getId().equals(task.getProcessInstanceId())).findFirst().ifPresent(e -> {
+                    taskVo.setBusinessStatus(e.getBusinessStatus());
+                    taskVo.setBusinessStatusName(BusinessStatusEnum.findByStatus(taskVo.getBusinessStatus()));
+                    taskVo.setProcessDefinitionKey(e.getProcessDefinitionKey());
+                    taskVo.setProcessDefinitionName(e.getProcessDefinitionName());
+                });
+            }
+            taskVo.setAssignee(Convert.toLong(task.getAssignee()));
+            list.add(taskVo);
+        }
+        long count = query.count();
+        return new TableDataInfo<>(list, count);
+    }
+
+    /**
+     * 委派任务
+     *
+     * @param delegateBo 参数
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean delegateTask(DelegateBo delegateBo) {
+        TaskEntity task = (TaskEntity) taskService.createTaskQuery().taskTenantId(TenantHelper.getTenantId()).taskId(delegateBo.getTaskId()).taskCandidateOrAssigned(String.valueOf(LoginHelper.getUserId())).singleResult();
+        if (ObjectUtil.isEmpty(task)) {
+            throw new ServiceException(FlowConstant.MESSAGE_CURRENT_TASK_IS_NULL);
+        }
+        if (task.isSuspended()) {
+            throw new ServiceException(FlowConstant.MESSAGE_SUSPENDED);
+        }
+        try {
+            TaskEntity newTask = WorkflowUtils.createNewTask(task);
+            taskService.addComment(newTask.getId(), task.getProcessInstanceId(), TaskStatusEnum.PENDING.getStatus(), "【" + LoginHelper.getLoginUser().getNickname() + "】委派给【" + delegateBo.getNickName() + "】");
+            //委托任务
+            taskService.delegateTask(delegateBo.getTaskId(), delegateBo.getUserId());
+            //办理生成的任务记录
+            taskService.complete(newTask.getId());
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new ServiceException(e.getMessage());
+        }
+    }
+
+    /**
+     * 终止任务
+     *
+     * @param terminationBo 参数
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean terminationTask(TerminationBo terminationBo) {
+        Task task = taskService.createTaskQuery().taskTenantId(TenantHelper.getTenantId()).taskId(terminationBo.getTaskId()).singleResult();
+
+        if (ObjectUtil.isEmpty(task)) {
+            throw new ServiceException(FlowConstant.MESSAGE_CURRENT_TASK_IS_NULL);
+        }
+        if (task.isSuspended()) {
+            throw new ServiceException(FlowConstant.MESSAGE_SUSPENDED);
+        }
+        HistoricProcessInstance historicProcessInstance = historyService.createHistoricProcessInstanceQuery().processInstanceBusinessKey(task.getProcessInstanceId()).processInstanceTenantId(TenantHelper.getTenantId()).singleResult();
+        if (ObjectUtil.isNotEmpty(historicProcessInstance) && BusinessStatusEnum.TERMINATION.getStatus().equals(historicProcessInstance.getBusinessStatus())) {
+            throw new ServiceException("该单据已终止!");
+        }
+        try {
+            if (StringUtils.isBlank(terminationBo.getComment())) {
+                terminationBo.setComment(LoginHelper.getLoginUser().getNickname() + "终止了申请");
+            } else {
+                terminationBo.setComment(LoginHelper.getLoginUser().getNickname() + "终止了申请:" + terminationBo.getComment());
+            }
+            taskService.addComment(task.getId(), task.getProcessInstanceId(), TaskStatusEnum.TERMINATION.getStatus(), terminationBo.getComment());
+            List<Task> list = taskService.createTaskQuery().taskTenantId(TenantHelper.getTenantId()).processInstanceId(task.getProcessInstanceId()).list();
+            if (CollectionUtil.isNotEmpty(list)) {
+                List<Task> subTasks = StreamUtils.filter(list, e -> StringUtils.isNotBlank(e.getParentTaskId()));
+                if (CollectionUtil.isNotEmpty(subTasks)) {
+                    subTasks.forEach(e -> taskService.deleteTask(e.getId()));
+                }
+                runtimeService.deleteProcessInstance(task.getProcessInstanceId(), StrUtil.EMPTY);
+            }
+            runtimeService.updateBusinessStatus(task.getProcessInstanceId(), BusinessStatusEnum.TERMINATION.getStatus());
+            FlowProcessEventHandler processHandler = flowEventStrategy.getProcessHandler(historicProcessInstance.getProcessDefinitionKey());
+            if (processHandler != null) {
+                processHandler.handleProcess(historicProcessInstance.getBusinessKey(), BusinessStatusEnum.TERMINATION.getStatus(), false);
+            }
+            return true;
+        } catch (Exception e) {
+            throw new ServiceException(e.getMessage());
+        }
+    }
+
+    /**
+     * 转办任务
+     *
+     * @param transmitBo 参数
+     */
+    @Override
+    public boolean transferTask(TransmitBo transmitBo) {
+        Task task = taskService.createTaskQuery().taskId(transmitBo.getTaskId()).taskTenantId(TenantHelper.getTenantId()).taskCandidateOrAssigned(String.valueOf(LoginHelper.getUserId())).singleResult();
+        if (ObjectUtil.isEmpty(task)) {
+            throw new ServiceException(FlowConstant.MESSAGE_CURRENT_TASK_IS_NULL);
+        }
+        if (task.isSuspended()) {
+            throw new ServiceException(FlowConstant.MESSAGE_SUSPENDED);
+        }
+        try {
+            TaskEntity newTask = WorkflowUtils.createNewTask(task);
+            taskService.addComment(newTask.getId(), task.getProcessInstanceId(), TaskStatusEnum.TRANSFER.getStatus(), StringUtils.isNotBlank(transmitBo.getComment()) ? transmitBo.getComment() : LoginHelper.getUsername() + "转办了任务");
+            taskService.complete(newTask.getId());
+            taskService.setAssignee(task.getId(), transmitBo.getUserId());
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new ServiceException(e.getMessage());
+        }
+    }
+
+    /**
+     * 会签任务加签
+     *
+     * @param addMultiBo 参数
+     */
+    @Override
+    public boolean addMultiInstanceExecution(AddMultiBo addMultiBo) {
+        TaskQuery taskQuery = taskService.createTaskQuery();
+        taskQuery.taskId(addMultiBo.getTaskId());
+        taskQuery.taskTenantId(TenantHelper.getTenantId());
+        if (!LoginHelper.isSuperAdmin() && !LoginHelper.isTenantAdmin()) {
+            taskQuery.taskCandidateOrAssigned(String.valueOf(LoginHelper.getUserId()));
+        }
+        Task task = taskQuery.singleResult();
+        if (ObjectUtil.isEmpty(task)) {
+            throw new ServiceException(FlowConstant.MESSAGE_CURRENT_TASK_IS_NULL);
+        }
+        if (task.isSuspended()) {
+            throw new ServiceException(FlowConstant.MESSAGE_SUSPENDED);
+        }
+        String taskDefinitionKey = task.getTaskDefinitionKey();
+        String processInstanceId = task.getProcessInstanceId();
+        String processDefinitionId = task.getProcessDefinitionId();
+
+        try {
+            MultiInstanceVo multiInstanceVo = WorkflowUtils.isMultiInstance(processDefinitionId, taskDefinitionKey);
+            if (multiInstanceVo == null) {
+                throw new ServiceException("当前环节不是会签节点");
+            }
+            if (multiInstanceVo.getType() instanceof ParallelMultiInstanceBehavior) {
+                for (Long assignee : addMultiBo.getAssignees()) {
+                    runtimeService.addMultiInstanceExecution(taskDefinitionKey, processInstanceId, Collections.singletonMap(multiInstanceVo.getAssignee(), assignee));
+                }
+            } else if (multiInstanceVo.getType() instanceof SequentialMultiInstanceBehavior) {
+                AddSequenceMultiInstanceCmd addSequenceMultiInstanceCmd = new AddSequenceMultiInstanceCmd(task.getExecutionId(), multiInstanceVo.getAssigneeList(), addMultiBo.getAssignees());
+                managementService.executeCommand(addSequenceMultiInstanceCmd);
+            }
+            List<String> assigneeNames = addMultiBo.getAssigneeNames();
+            String username = LoginHelper.getUsername();
+            TaskEntity newTask = WorkflowUtils.createNewTask(task);
+            taskService.addComment(newTask.getId(), processInstanceId, TaskStatusEnum.SIGN.getStatus(), username + "加签【" + String.join(StringUtils.SEPARATOR, assigneeNames) + "】");
+            taskService.complete(newTask.getId());
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new ServiceException(e.getMessage());
+        }
+    }
+
+    /**
+     * 会签任务减签
+     *
+     * @param deleteMultiBo 参数
+     */
+    @Override
+    public boolean deleteMultiInstanceExecution(DeleteMultiBo deleteMultiBo) {
+        TaskQuery taskQuery = taskService.createTaskQuery();
+        taskQuery.taskId(deleteMultiBo.getTaskId());
+        taskQuery.taskTenantId(TenantHelper.getTenantId());
+        if (!LoginHelper.isSuperAdmin() && !LoginHelper.isTenantAdmin()) {
+            taskQuery.taskCandidateOrAssigned(String.valueOf(LoginHelper.getUserId()));
+        }
+        Task task = taskQuery.singleResult();
+        if (ObjectUtil.isEmpty(task)) {
+            throw new ServiceException(FlowConstant.MESSAGE_CURRENT_TASK_IS_NULL);
+        }
+        if (task.isSuspended()) {
+            throw new ServiceException(FlowConstant.MESSAGE_SUSPENDED);
+        }
+        String taskDefinitionKey = task.getTaskDefinitionKey();
+        String processInstanceId = task.getProcessInstanceId();
+        String processDefinitionId = task.getProcessDefinitionId();
+        try {
+            MultiInstanceVo multiInstanceVo = WorkflowUtils.isMultiInstance(processDefinitionId, taskDefinitionKey);
+            if (multiInstanceVo == null) {
+                throw new ServiceException("当前环节不是会签节点");
+            }
+            if (multiInstanceVo.getType() instanceof ParallelMultiInstanceBehavior) {
+                for (String executionId : deleteMultiBo.getExecutionIds()) {
+                    runtimeService.deleteMultiInstanceExecution(executionId, false);
+                }
+                for (String taskId : deleteMultiBo.getTaskIds()) {
+                    historyService.deleteHistoricTaskInstance(taskId);
+                }
+            } else if (multiInstanceVo.getType() instanceof SequentialMultiInstanceBehavior) {
+                DeleteSequenceMultiInstanceCmd deleteSequenceMultiInstanceCmd = new DeleteSequenceMultiInstanceCmd(task.getAssignee(), task.getExecutionId(), multiInstanceVo.getAssigneeList(), deleteMultiBo.getAssigneeIds());
+                managementService.executeCommand(deleteSequenceMultiInstanceCmd);
+            }
+            List<String> assigneeNames = deleteMultiBo.getAssigneeNames();
+            String username = LoginHelper.getUsername();
+            TaskEntity newTask = WorkflowUtils.createNewTask(task);
+            taskService.addComment(newTask.getId(), processInstanceId, TaskStatusEnum.SIGN_OFF.getStatus(), username + "减签【" + String.join(StringUtils.SEPARATOR, assigneeNames) + "】");
+            taskService.complete(newTask.getId());
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            throw new ServiceException(e.getMessage());
+        }
+    }
+
+    /**
+     * 驳回审批
+     *
+     * @param backProcessBo 参数
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public String backProcess(BackProcessBo backProcessBo) {
+        Task task = taskService.createTaskQuery().taskId(backProcessBo.getTaskId()).taskTenantId(TenantHelper.getTenantId()).taskAssignee(String.valueOf(LoginHelper.getUserId())).singleResult();
+        if (ObjectUtil.isEmpty(task)) {
+            throw new ServiceException(FlowConstant.MESSAGE_CURRENT_TASK_IS_NULL);
+        }
+        if (task.isSuspended()) {
+            throw new ServiceException(FlowConstant.MESSAGE_SUSPENDED);
+        }
+        try {
+            String processInstanceId = task.getProcessInstanceId();
+            ProcessInstance processInstance = runtimeService.createProcessInstanceQuery().processInstanceId(task.getProcessInstanceId()).singleResult();
+            //获取并行网关执行后保留的执行实例数据
+            ExecutionChildByExecutionIdCmd childByExecutionIdCmd = new ExecutionChildByExecutionIdCmd(task.getExecutionId());
+            List<ExecutionEntity> executionEntities = managementService.executeCommand(childByExecutionIdCmd);
+            //校验单据
+            if (BusinessStatusEnum.BACK.getStatus().equals(processInstance.getBusinessStatus())) {
+                throw new ServiceException("该单据已退回!");
+            }
+            //判断是否有多个任务
+            List<Task> taskList = taskService.createTaskQuery().processInstanceId(processInstanceId).taskTenantId(TenantHelper.getTenantId()).list();
+            //申请人节点
+            HistoricTaskInstance historicTaskInstance = historyService.createHistoricTaskInstanceQuery().processInstanceId(processInstanceId).finished().orderByHistoricTaskInstanceEndTime().asc().list().get(0);
+            String backTaskDefinitionKey = historicTaskInstance.getTaskDefinitionKey();
+            taskService.addComment(task.getId(), processInstanceId, TaskStatusEnum.BACK.getStatus(), StringUtils.isNotBlank(backProcessBo.getMessage()) ? backProcessBo.getMessage() : "退回");
+            if (taskList.size() > 1) {
+                //当前多个任务驳回到单个节点
+                runtimeService.createChangeActivityStateBuilder().processInstanceId(processInstanceId).moveActivityIdsToSingleActivityId(taskList.stream().map(Task::getTaskDefinitionKey).distinct().collect(Collectors.toList()), backTaskDefinitionKey).changeState();
+            } else {
+                //当前单个节点驳回单个节点
+                runtimeService.createChangeActivityStateBuilder().processInstanceId(processInstanceId).moveActivityIdTo(task.getTaskDefinitionKey(), backTaskDefinitionKey).changeState();
+            }
+            List<Task> list = taskService.createTaskQuery().processInstanceId(processInstanceId).taskTenantId(TenantHelper.getTenantId()).list();
+            for (Task t : list) {
+                taskService.setAssignee(t.getId(), historicTaskInstance.getAssignee());
+            }
+            //发送消息
+            String message = "您的【" + processInstance.getName() + "】单据已经被驳回,请您注意查收。";
+            sendMessage(list, processInstance.getName(), backProcessBo.getMessageType(), message);
+            //删除流程实例垃圾数据
+            for (ExecutionEntity executionEntity : executionEntities) {
+                DeleteExecutionCmd deleteExecutionCmd = new DeleteExecutionCmd(executionEntity.getId());
+                managementService.executeCommand(deleteExecutionCmd);
+            }
+            runtimeService.updateBusinessStatus(processInstanceId, BusinessStatusEnum.BACK.getStatus());
+            FlowProcessEventHandler processHandler = flowEventStrategy.getProcessHandler(processInstance.getProcessDefinitionKey());
+            if (processHandler != null) {
+                processHandler.handleProcess(processInstance.getBusinessKey(), BusinessStatusEnum.BACK.getStatus(), false);
+            }
+        } catch (Exception e) {
+            throw new ServiceException(e.getMessage());
+        }
+        return task.getProcessInstanceId();
+    }
+
+    /**
+     * 修改任务办理人
+     *
+     * @param taskIds 任务id
+     * @param userId  办理人id
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean updateAssignee(String[] taskIds, String userId) {
+        try {
+            List<Task> list = taskService.createTaskQuery().taskIds(Arrays.asList(taskIds)).taskTenantId(TenantHelper.getTenantId()).list();
+            for (Task task : list) {
+                taskService.setAssignee(task.getId(), userId);
+            }
+        } catch (Exception e) {
+            throw new ServiceException("修改失败:" + e.getMessage());
+        }
+        return true;
+    }
+}

+ 124 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/TestLeaveServiceImpl.java

@@ -0,0 +1,124 @@
+package org.dromara.workflow.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.core.domain.BaseEntity;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.workflow.domain.TestLeave;
+import org.dromara.workflow.domain.bo.TestLeaveBo;
+import org.dromara.workflow.domain.vo.TestLeaveVo;
+import org.dromara.workflow.mapper.TestLeaveMapper;
+import org.dromara.workflow.service.IActProcessInstanceService;
+import org.dromara.workflow.service.ITestLeaveService;
+import org.dromara.workflow.utils.WorkflowUtils;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 请假Service业务层处理
+ *
+ * @author may
+ * @date 2023-07-21
+ */
+@RequiredArgsConstructor
+@Service
+public class TestLeaveServiceImpl implements ITestLeaveService {
+
+    private final TestLeaveMapper baseMapper;
+    private final IActProcessInstanceService iActProcessInstanceService;
+
+    /**
+     * 查询请假
+     */
+    @Override
+    public TestLeaveVo queryById(Long id) {
+        return baseMapper.selectVoById(id);
+    }
+
+    /**
+     * 查询请假列表
+     */
+    @Override
+    public TableDataInfo<TestLeaveVo> queryPageList(TestLeaveBo bo, PageQuery pageQuery) {
+        LambdaQueryWrapper<TestLeave> lqw = buildQueryWrapper(bo);
+        Page<TestLeaveVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        TableDataInfo<TestLeaveVo> build = TableDataInfo.build(result);
+        List<TestLeaveVo> rows = build.getRows();
+        if (CollUtil.isNotEmpty(rows)) {
+            List<String> ids = StreamUtils.toList(rows, e -> String.valueOf(e.getId()));
+            WorkflowUtils.setProcessInstanceListVo(rows, ids, "id");
+        }
+        return build;
+    }
+
+    /**
+     * 查询请假列表
+     */
+    @Override
+    public List<TestLeaveVo> queryList(TestLeaveBo bo) {
+        LambdaQueryWrapper<TestLeave> lqw = buildQueryWrapper(bo);
+        return baseMapper.selectVoList(lqw);
+    }
+
+    private LambdaQueryWrapper<TestLeave> buildQueryWrapper(TestLeaveBo bo) {
+        LambdaQueryWrapper<TestLeave> lqw = Wrappers.lambdaQuery();
+        lqw.eq(StringUtils.isNotBlank(bo.getLeaveType()), TestLeave::getLeaveType, bo.getLeaveType());
+        lqw.ge(bo.getStartLeaveDays() != null,TestLeave::getLeaveDays, bo.getStartLeaveDays());
+        lqw.le(bo.getEndLeaveDays() != null,TestLeave::getLeaveDays, bo.getEndLeaveDays());
+        lqw.orderByDesc(BaseEntity::getCreateTime);
+        return lqw;
+    }
+
+    /**
+     * 新增请假
+     */
+    @Override
+    public TestLeave insertByBo(TestLeaveBo bo) {
+        TestLeave add = MapstructUtils.convert(bo, TestLeave.class);
+        validEntityBeforeSave(add);
+        boolean flag = baseMapper.insert(add) > 0;
+        if (flag) {
+            bo.setId(add.getId());
+        }
+        return add;
+    }
+
+    /**
+     * 修改请假
+     */
+    @Override
+    public TestLeave updateByBo(TestLeaveBo bo) {
+        TestLeave update = MapstructUtils.convert(bo, TestLeave.class);
+        validEntityBeforeSave(update);
+        return baseMapper.updateById(update) > 0 ? update : null;
+    }
+
+    /**
+     * 保存前的数据校验
+     */
+    private void validEntityBeforeSave(TestLeave entity) {
+        //TODO 做一些数据校验,如唯一约束
+    }
+
+    /**
+     * 批量删除请假
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean deleteWithValidByIds(Collection<Long> ids) {
+        List<String> idList = StreamUtils.toList(ids, String::valueOf);
+        iActProcessInstanceService.deleteRuntimeProcessAndHisInstByBusinessKeys(idList);
+        return baseMapper.deleteBatchIds(ids) > 0;
+    }
+}

+ 128 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/WfCategoryServiceImpl.java

@@ -0,0 +1,128 @@
+package org.dromara.workflow.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.utils.MapstructUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.workflow.domain.WfCategory;
+import org.dromara.workflow.domain.bo.WfCategoryBo;
+import org.dromara.workflow.domain.vo.WfCategoryVo;
+import org.dromara.workflow.mapper.WfCategoryMapper;
+import org.dromara.workflow.service.IWfCategoryService;
+import org.flowable.engine.RepositoryService;
+import org.flowable.engine.repository.Deployment;
+import org.flowable.engine.repository.Model;
+import org.flowable.engine.repository.ProcessDefinition;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 流程分类Service业务层处理
+ *
+ * @author may
+ * @date 2023-06-28
+ */
+@RequiredArgsConstructor
+@Service
+public class WfCategoryServiceImpl implements IWfCategoryService {
+
+    private final WfCategoryMapper baseMapper;
+
+    private final RepositoryService repositoryService;
+
+    /**
+     * 查询流程分类
+     */
+    @Override
+    public WfCategoryVo queryById(Long id) {
+        return baseMapper.selectVoById(id);
+    }
+
+
+    /**
+     * 查询流程分类列表
+     */
+    @Override
+    public List<WfCategoryVo> queryList(WfCategoryBo bo) {
+        LambdaQueryWrapper<WfCategory> lqw = buildQueryWrapper(bo);
+        return baseMapper.selectVoList(lqw);
+    }
+
+    private LambdaQueryWrapper<WfCategory> buildQueryWrapper(WfCategoryBo bo) {
+        LambdaQueryWrapper<WfCategory> lqw = Wrappers.lambdaQuery();
+        lqw.like(StringUtils.isNotBlank(bo.getCategoryName()), WfCategory::getCategoryName, bo.getCategoryName());
+        lqw.eq(StringUtils.isNotBlank(bo.getCategoryCode()), WfCategory::getCategoryCode, bo.getCategoryCode());
+        return lqw;
+    }
+
+    /**
+     * 新增流程分类
+     */
+    @Override
+    public Boolean insertByBo(WfCategoryBo bo) {
+        WfCategory add = MapstructUtils.convert(bo, WfCategory.class);
+        validEntityBeforeSave(add);
+        boolean flag = baseMapper.insert(add) > 0;
+        if (flag) {
+            bo.setId(add.getId());
+        }
+        return flag;
+    }
+
+    /**
+     * 修改流程分类
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public Boolean updateByBo(WfCategoryBo bo) {
+        WfCategory update = MapstructUtils.convert(bo, WfCategory.class);
+        validEntityBeforeSave(update);
+        WfCategoryVo wfCategoryVo = baseMapper.selectVoById(bo.getId());
+        List<ProcessDefinition> processDefinitionList = repositoryService.createProcessDefinitionQuery().processDefinitionCategory(wfCategoryVo.getCategoryCode()).list();
+        for (ProcessDefinition processDefinition : processDefinitionList) {
+            repositoryService.setProcessDefinitionCategory(processDefinition.getId(), bo.getCategoryCode());
+        }
+        List<Deployment> deploymentList = repositoryService.createDeploymentQuery().deploymentCategory(wfCategoryVo.getCategoryCode()).list();
+        for (Deployment deployment : deploymentList) {
+            repositoryService.setDeploymentCategory(deployment.getId(), bo.getCategoryCode());
+        }
+        List<Model> modelList = repositoryService.createModelQuery().modelCategory(wfCategoryVo.getCategoryCode()).list();
+        for (Model model : modelList) {
+            model.setCategory(bo.getCategoryCode());
+            repositoryService.saveModel(model);
+        }
+        return baseMapper.updateById(update) > 0;
+    }
+
+    /**
+     * 保存前的数据校验
+     */
+    private void validEntityBeforeSave(WfCategory entity) {
+        //TODO 做一些数据校验,如唯一约束
+    }
+
+    /**
+     * 批量删除流程分类
+     */
+    @Override
+    public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
+        if (isValid) {
+            //TODO 做一些业务上的校验,判断是否需要校验
+        }
+        return baseMapper.deleteBatchIds(ids) > 0;
+    }
+
+    /**
+     * 按照类别编码查询
+     *
+     * @param categoryCode 分类比吗
+     */
+    @Override
+    public WfCategory queryByCategoryCode(String categoryCode) {
+        return baseMapper.selectOne(new LambdaQueryWrapper<WfCategory>().eq(WfCategory::getCategoryCode, categoryCode));
+    }
+}

+ 216 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/service/impl/WorkflowUserServiceImpl.java

@@ -0,0 +1,216 @@
+package org.dromara.workflow.service.impl;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.CollectionUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import lombok.RequiredArgsConstructor;
+import org.dromara.common.core.enums.UserStatus;
+import org.dromara.common.core.exception.ServiceException;
+import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.mybatis.core.page.PageQuery;
+import org.dromara.common.mybatis.core.page.TableDataInfo;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.system.domain.SysUser;
+import org.dromara.system.domain.SysUserRole;
+import org.dromara.system.domain.bo.SysUserBo;
+import org.dromara.system.domain.vo.SysUserVo;
+import org.dromara.system.mapper.SysUserMapper;
+import org.dromara.system.mapper.SysUserRoleMapper;
+import org.dromara.workflow.domain.bo.SysUserMultiBo;
+import org.dromara.workflow.domain.vo.MultiInstanceVo;
+import org.dromara.workflow.domain.vo.TaskVo;
+import org.dromara.workflow.service.IWorkflowUserService;
+import org.dromara.workflow.utils.WorkflowUtils;
+import org.flowable.engine.RuntimeService;
+import org.flowable.engine.TaskService;
+import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior;
+import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior;
+import org.flowable.task.api.Task;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 工作流用户选人管理 业务处理层
+ *
+ * @author may
+ */
+@RequiredArgsConstructor
+@Service
+public class WorkflowUserServiceImpl implements IWorkflowUserService {
+
+    private final SysUserMapper sysUserMapper;
+    private final SysUserRoleMapper sysUserRoleMapper;
+    private final TaskService taskService;
+    private final RuntimeService runtimeService;
+
+    /**
+     * 分页查询工作流选择加签人员
+     *
+     * @param sysUserMultiBo 参数
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    public TableDataInfo<SysUserVo> getWorkflowAddMultiInstanceByPage(SysUserMultiBo sysUserMultiBo) {
+        Task task = taskService.createTaskQuery().taskId(sysUserMultiBo.getTaskId()).singleResult();
+        if (task == null) {
+            throw new ServiceException("任务不存在");
+        }
+        MultiInstanceVo multiInstance = WorkflowUtils.isMultiInstance(task.getProcessDefinitionId(), task.getTaskDefinitionKey());
+        if (multiInstance == null) {
+            return TableDataInfo.build();
+        }
+        LambdaQueryWrapper<SysUser> queryWrapper = Wrappers.lambdaQuery();
+        //检索条件
+        queryWrapper.eq(StringUtils.isNotEmpty(sysUserMultiBo.getDeptId()), SysUser::getDeptId, sysUserMultiBo.getDeptId());
+        queryWrapper.eq(SysUser::getStatus, UserStatus.OK.getCode());
+        if (multiInstance.getType() instanceof SequentialMultiInstanceBehavior) {
+            List<Long> assigneeList = (List<Long>) runtimeService.getVariable(task.getExecutionId(), multiInstance.getAssigneeList());
+            queryWrapper.notIn(CollectionUtil.isNotEmpty(assigneeList), SysUser::getUserId, assigneeList);
+        } else {
+            List<Task> list = taskService.createTaskQuery().processInstanceId(task.getProcessInstanceId()).list();
+            List<Long> userIds = StreamUtils.toList(list, e -> Long.valueOf(e.getAssignee()));
+            queryWrapper.notIn(CollectionUtil.isNotEmpty(userIds), SysUser::getUserId, userIds);
+        }
+        queryWrapper.like(StringUtils.isNotEmpty(sysUserMultiBo.getUserName()), SysUser::getUserName, sysUserMultiBo.getUserName());
+        queryWrapper.like(StringUtils.isNotEmpty(sysUserMultiBo.getNickName()), SysUser::getNickName, sysUserMultiBo.getNickName());
+        Page<SysUser> page = new Page<>(sysUserMultiBo.getPageNum(), sysUserMultiBo.getPageSize());
+        Page<SysUserVo> userPage = sysUserMapper.selectVoPage(page, queryWrapper);
+        return TableDataInfo.build(recordPage(userPage));
+    }
+
+    /**
+     * 查询工作流选择减签人员
+     *
+     * @param taskId 任务id 任务id
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    public List<TaskVo> getWorkflowDeleteMultiInstanceList(String taskId) {
+        Task task = taskService.createTaskQuery().taskTenantId(TenantHelper.getTenantId()).taskId(taskId).singleResult();
+        List<Task> taskList = taskService.createTaskQuery().taskTenantId(TenantHelper.getTenantId()).processInstanceId(task.getProcessInstanceId()).list();
+        MultiInstanceVo multiInstance = WorkflowUtils.isMultiInstance(task.getProcessDefinitionId(), task.getTaskDefinitionKey());
+        List<TaskVo> taskListVo = new ArrayList<>();
+        if (multiInstance == null) {
+            return Collections.emptyList();
+        }
+        List<Long> assigneeList = new ArrayList<>();
+        if (multiInstance.getType() instanceof SequentialMultiInstanceBehavior) {
+            List<Object> variable = (List<Object>) runtimeService.getVariable(task.getExecutionId(), multiInstance.getAssigneeList());
+            for (Object o : variable) {
+                assigneeList.add(Long.valueOf(o.toString()));
+            }
+        }
+
+        if (multiInstance.getType() instanceof SequentialMultiInstanceBehavior) {
+            List<Long> userIds = StreamUtils.filter(assigneeList, e -> !String.valueOf(e).equals(task.getAssignee()));
+            List<SysUserVo> sysUsers = null;
+            if (CollectionUtil.isNotEmpty(userIds)) {
+                sysUsers = sysUserMapper.selectVoBatchIds(userIds);
+            }
+            for (Long userId : userIds) {
+                TaskVo taskVo = new TaskVo();
+                taskVo.setId("串行会签");
+                taskVo.setExecutionId("串行会签");
+                taskVo.setProcessInstanceId(task.getProcessInstanceId());
+                taskVo.setName(task.getName());
+                taskVo.setAssignee(userId);
+                if (CollectionUtil.isNotEmpty(sysUsers)) {
+                    sysUsers.stream().filter(u -> u.getUserId().toString().equals(userId.toString())).findFirst().ifPresent(u -> taskVo.setAssigneeName(u.getNickName()));
+                }
+                taskListVo.add(taskVo);
+            }
+            return taskListVo;
+        } else if (multiInstance.getType() instanceof ParallelMultiInstanceBehavior) {
+            List<Task> tasks = StreamUtils.filter(taskList, e -> StringUtils.isBlank(e.getParentTaskId()) && !e.getExecutionId().equals(task.getExecutionId()) && e.getTaskDefinitionKey().equals(task.getTaskDefinitionKey()));
+            if (CollectionUtil.isNotEmpty(tasks)) {
+                List<Long> userIds = StreamUtils.toList(tasks, e -> Long.valueOf(e.getAssignee()));
+                List<SysUserVo> sysUsers = null;
+                if (CollectionUtil.isNotEmpty(userIds)) {
+                    sysUsers = sysUserMapper.selectVoBatchIds(userIds);
+                }
+                for (Task t : tasks) {
+                    TaskVo taskVo = new TaskVo();
+                    taskVo.setId(t.getId());
+                    taskVo.setExecutionId(t.getExecutionId());
+                    taskVo.setProcessInstanceId(t.getProcessInstanceId());
+                    taskVo.setName(t.getName());
+                    taskVo.setAssignee(Long.valueOf(t.getAssignee()));
+                    if (CollectionUtil.isNotEmpty(sysUsers)) {
+                        sysUsers.stream().filter(u -> u.getUserId().toString().equals(t.getAssignee())).findFirst().ifPresent(e -> taskVo.setAssigneeName(e.getNickName()));
+                    }
+                    taskListVo.add(taskVo);
+                }
+                return taskListVo;
+            }
+        }
+        return Collections.emptyList();
+    }
+
+    /**
+     * 翻译部门
+     *
+     * @param page 用户分页数据
+     */
+    private Page<SysUserVo> recordPage(Page<SysUserVo> page) {
+        List<SysUserVo> records = page.getRecords();
+        if (CollUtil.isEmpty(records)) {
+            return page;
+        }
+        List<Long> collectDeptId = StreamUtils.toList(records, SysUserVo::getDeptId);
+        if (CollUtil.isEmpty(collectDeptId)) {
+            return page;
+        }
+        page.setRecords(records);
+        return page;
+    }
+
+    /**
+     * 按照用户id查询用户
+     *
+     * @param userIds 用户id
+     */
+    @Override
+    public List<SysUserVo> getUserListByIds(List<Long> userIds) {
+        if (CollUtil.isEmpty(userIds)) {
+            return Collections.emptyList();
+        }
+        LambdaQueryWrapper<SysUser> queryWrapper = Wrappers.lambdaQuery();
+        // 检索条件
+        queryWrapper.eq(SysUser::getStatus, UserStatus.OK.getCode());
+        queryWrapper.in(SysUser::getUserId, userIds);
+        return sysUserMapper.selectVoList(queryWrapper);
+    }
+
+    /**
+     * 按照角色id查询关联用户id
+     *
+     * @param roleIds 角色id
+     */
+    @Override
+    public List<SysUserRole> getUserRoleListByRoleIds(List<Long> roleIds) {
+        return sysUserRoleMapper.selectList(new LambdaQueryWrapper<SysUserRole>().in(SysUserRole::getRoleId, roleIds));
+    }
+
+    /**
+     * 分页查询用户
+     *
+     * @param sysUserBo 参数
+     * @param pageQuery 分页
+     */
+    @Override
+    public TableDataInfo<SysUserVo> getUserListByPage(SysUserBo sysUserBo, PageQuery pageQuery) {
+        LambdaQueryWrapper<SysUser> queryWrapper = Wrappers.lambdaQuery();
+        queryWrapper.eq(sysUserBo.getDeptId() != null, SysUser::getDeptId, sysUserBo.getDeptId());
+        queryWrapper.eq(SysUser::getStatus, UserStatus.OK.getCode());
+        queryWrapper.like(StringUtils.isNotEmpty(sysUserBo.getUserName()), SysUser::getUserName, sysUserBo.getUserName());
+        queryWrapper.like(StringUtils.isNotEmpty(sysUserBo.getNickName()), SysUser::getNickName, sysUserBo.getNickName());
+        Page<SysUserVo> userPage = sysUserMapper.selectVoPage(pageQuery.build(), queryWrapper);
+        return TableDataInfo.build(recordPage(userPage));
+    }
+}

+ 220 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/utils/ModelUtils.java

@@ -0,0 +1,220 @@
+package org.dromara.workflow.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.collection.CollectionUtil;
+import cn.hutool.core.io.IoUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.json.utils.JsonUtils;
+import org.dromara.workflow.domain.vo.MultiInstanceVo;
+import org.flowable.bpmn.converter.BpmnXMLConverter;
+import org.flowable.bpmn.model.*;
+import org.flowable.bpmn.model.Process;
+import org.flowable.editor.language.json.converter.BpmnJsonConverter;
+import org.flowable.engine.impl.util.ProcessDefinitionUtil;
+
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.stream.XMLStreamReader;
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.rmi.ServerException;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 模型工具
+ *
+ * @author may
+ */
+public class ModelUtils {
+    public ModelUtils() {
+    }
+
+    public static BpmnModel xmlToBpmnModel(String xml) throws IOException {
+        if (xml == null) {
+            throw new ServerException("xml不能为空");
+        }
+        try {
+            InputStream inputStream = new ByteArrayInputStream(StrUtil.utf8Bytes(xml));
+            XMLInputFactory factory = XMLInputFactory.newInstance();
+            XMLStreamReader reader = factory.createXMLStreamReader(inputStream);
+            return new BpmnXMLConverter().convertToBpmnModel(reader);
+        } catch (XMLStreamException e) {
+            throw new ServerException(e.getMessage());
+        }
+    }
+
+    /**
+     * bpmnModel转为xml
+     *
+     * @param jsonBytes json
+     */
+    public static byte[] bpmnJsonToXmlBytes(byte[] jsonBytes) throws IOException {
+        if (jsonBytes == null) {
+            return new byte[0];
+        }
+        // 1. json字节码转成 BpmnModel 对象
+        ObjectMapper objectMapper = JsonUtils.getObjectMapper();
+        JsonNode jsonNode = objectMapper.readTree(jsonBytes);
+        BpmnModel bpmnModel = new BpmnJsonConverter().convertToBpmnModel(jsonNode);
+
+        if (bpmnModel.getProcesses().isEmpty()) {
+            return new byte[0];
+        }
+        // 2.将bpmnModel转为xml
+        return new BpmnXMLConverter().convertToXML(bpmnModel);
+    }
+
+    /**
+     * xml转为bpmnModel
+     *
+     * @param xmlBytes xml
+     */
+    public static BpmnModel xmlToBpmnModel(byte[] xmlBytes) throws XMLStreamException {
+        ByteArrayInputStream byteArrayInputStream = IoUtil.toStream(xmlBytes);
+        XMLInputFactory xif = XMLInputFactory.newInstance();
+        XMLStreamReader xtr = xif.createXMLStreamReader(byteArrayInputStream);
+        return new BpmnXMLConverter().convertToBpmnModel(xtr);
+    }
+
+    /**
+     * 校验模型
+     *
+     * @param bpmnModel bpmn模型
+     */
+    public static void checkBpmnModel(BpmnModel bpmnModel) throws ServerException {
+        Collection<FlowElement> flowElements = bpmnModel.getMainProcess().getFlowElements();
+
+        checkBpmnNode(flowElements, false);
+
+        List<SubProcess> subProcessList = flowElements.stream().filter(SubProcess.class::isInstance).map(SubProcess.class::cast).collect(Collectors.toList());
+        if (!CollUtil.isEmpty(subProcessList)) {
+            for (SubProcess subProcess : subProcessList) {
+                Collection<FlowElement> subProcessFlowElements = subProcess.getFlowElements();
+                checkBpmnNode(subProcessFlowElements, true);
+            }
+        }
+        List<MultiInstanceVo> multiInstanceVoList = new ArrayList<>();
+        for (FlowElement flowElement : flowElements) {
+            if (flowElement instanceof UserTask && ObjectUtil.isNotEmpty(((UserTask) flowElement).getLoopCharacteristics()) && StringUtils.isNotBlank(((UserTask) flowElement).getLoopCharacteristics().getInputDataItem())) {
+                MultiInstanceVo multiInstanceVo = new MultiInstanceVo();
+                multiInstanceVo.setAssigneeList(((UserTask) flowElement).getLoopCharacteristics().getInputDataItem());
+                multiInstanceVoList.add(multiInstanceVo);
+            }
+        }
+
+        if (CollectionUtil.isNotEmpty(multiInstanceVoList) && multiInstanceVoList.size() > 1) {
+            Map<String, List<MultiInstanceVo>> assigneeListGroup = StreamUtils.groupByKey(multiInstanceVoList, MultiInstanceVo::getAssigneeList);
+            for (Map.Entry<String, List<MultiInstanceVo>> entry : assigneeListGroup.entrySet()) {
+                List<MultiInstanceVo> value = entry.getValue();
+                if (CollectionUtil.isNotEmpty(value) && value.size() > 1) {
+                    String key = entry.getKey();
+                    throw new ServerException("会签人员集合【" + key + "】重复,请重新设置集合KEY");
+                }
+            }
+        }
+    }
+
+    /**
+     * 校验bpmn节点是否合法
+     *
+     * @param flowElements 节点集合
+     * @param subtask      是否子流程
+     */
+    private static void checkBpmnNode(Collection<FlowElement> flowElements, boolean subtask) throws ServerException {
+
+        if (CollUtil.isEmpty(flowElements)) {
+            throw new ServerException(subtask ? "子流程必须存在节点" : "必须存在节点!");
+        }
+
+        List<StartEvent> startEventList = flowElements.stream().filter(StartEvent.class::isInstance).map(StartEvent.class::cast).collect(Collectors.toList());
+        if (CollUtil.isEmpty(startEventList)) {
+            throw new ServerException(subtask ? "子流程必须存在开始节点" : "必须存在开始节点!");
+        }
+
+        if (startEventList.size() > 1) {
+            throw new ServerException(subtask ? "子流程只能存在一个开始节点" : "只能存在一个开始节点!");
+        }
+
+        StartEvent startEvent = startEventList.get(0);
+        List<SequenceFlow> outgoingFlows = startEvent.getOutgoingFlows();
+        if (CollUtil.isEmpty(outgoingFlows)) {
+            throw new ServerException(subtask ? "子流程流程节点为空,请至少设计一条主线流程!" : "流程节点为空,请至少设计一条主线流程!");
+        }
+
+        FlowElement targetFlowElement = outgoingFlows.get(0).getTargetFlowElement();
+        if (!(targetFlowElement instanceof UserTask) && !subtask) {
+            throw new ServerException("开始节点后第一个节点必须是用户任务!");
+        }
+
+        List<EndEvent> endEventList = flowElements.stream().filter(EndEvent.class::isInstance).map(EndEvent.class::cast).collect(Collectors.toList());
+        if (CollUtil.isEmpty(endEventList)) {
+            throw new ServerException(subtask ? "子流程必须存在结束节点!" : "必须存在结束节点!");
+        }
+    }
+
+    /**
+     * 获取流程全部节点
+     *
+     * @param processDefinitionId 流程定义id
+     */
+    public static List<FlowElement> getFlowElements(String processDefinitionId) {
+        BpmnModel bpmnModel = ProcessDefinitionUtil.getBpmnModel(processDefinitionId);
+        List<FlowElement> list = new ArrayList<>();
+        List<Process> processes = bpmnModel.getProcesses();
+        Collection<FlowElement> flowElements = processes.get(0).getFlowElements();
+        buildFlowElements(flowElements, list);
+        return list;
+    }
+
+    /**
+     * 递归获取所有节点
+     *
+     * @param flowElements 节点信息
+     * @param list         集合
+     */
+    private static void buildFlowElements(Collection<FlowElement> flowElements, List<FlowElement> list) {
+        for (FlowElement flowElement : flowElements) {
+            list.add(flowElement);
+            if (flowElement instanceof SubProcess) {
+                Collection<FlowElement> subFlowElements = ((SubProcess) flowElement).getFlowElements();
+                buildFlowElements(subFlowElements, list);
+            }
+        }
+    }
+
+    /**
+     * 获取全部扩展信息
+     *
+     * @param processDefinitionId 流程定义id
+     */
+    public Map<String, List<ExtensionElement>> getExtensionElements(String processDefinitionId) {
+        Map<String, List<ExtensionElement>> map = new HashMap<>();
+        List<FlowElement> flowElements = getFlowElements(processDefinitionId);
+        for (FlowElement flowElement : flowElements) {
+            if (flowElement instanceof UserTask && CollUtil.isNotEmpty(flowElement.getExtensionElements())) {
+                map.putAll(flowElement.getExtensionElements());
+            }
+        }
+        return map;
+    }
+
+    /**
+     * 获取某个节点的扩展信息
+     *
+     * @param processDefinitionId 流程定义id
+     * @param flowElementId       节点id
+     */
+    public Map<String, List<ExtensionElement>> getExtensionElement(String processDefinitionId, String flowElementId) {
+        BpmnModel bpmnModel = ProcessDefinitionUtil.getBpmnModel(processDefinitionId);
+        Process process = bpmnModel.getMainProcess();
+        FlowElement flowElement = process.getFlowElement(flowElementId);
+        return flowElement.getExtensionElements();
+    }
+}

+ 341 - 0
ruoyi-modules/ruoyi-workflow/src/main/java/org/dromara/workflow/utils/WorkflowUtils.java

@@ -0,0 +1,341 @@
+package org.dromara.workflow.utils;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import org.dromara.common.core.utils.SpringUtils;
+import org.dromara.common.core.utils.StreamUtils;
+import org.dromara.common.core.utils.StringUtils;
+import org.dromara.common.core.utils.reflect.ReflectUtils;
+import org.dromara.common.mail.utils.MailUtils;
+import org.dromara.common.tenant.helper.TenantHelper;
+import org.dromara.common.websocket.dto.WebSocketMessageDto;
+import org.dromara.common.websocket.utils.WebSocketUtils;
+import org.dromara.system.domain.SysUserRole;
+import org.dromara.system.domain.vo.SysUserVo;
+import org.dromara.workflow.common.constant.FlowConstant;
+import org.dromara.workflow.common.enums.BusinessStatusEnum;
+import org.dromara.workflow.common.enums.MessageTypeEnum;
+import org.dromara.workflow.common.enums.TaskStatusEnum;
+import org.dromara.workflow.domain.ActHiProcinst;
+import org.dromara.workflow.domain.ActHiTaskinst;
+import org.dromara.workflow.domain.vo.MultiInstanceVo;
+import org.dromara.workflow.domain.vo.ParticipantVo;
+import org.dromara.workflow.domain.vo.ProcessInstanceVo;
+import org.dromara.workflow.flowable.cmd.UpdateHiTaskInstCmd;
+import org.dromara.workflow.mapper.ActHiTaskinstMapper;
+import org.dromara.workflow.service.IActHiProcinstService;
+import org.dromara.workflow.service.IWorkflowUserService;
+import org.dromara.workflow.service.impl.WorkflowUserServiceImpl;
+import org.flowable.bpmn.model.*;
+import org.flowable.common.engine.api.delegate.Expression;
+import org.flowable.engine.ProcessEngine;
+import org.flowable.engine.history.HistoricProcessInstance;
+import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior;
+import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior;
+import org.flowable.identitylink.api.history.HistoricIdentityLink;
+import org.flowable.task.api.Task;
+import org.flowable.task.api.history.HistoricTaskInstance;
+import org.flowable.task.service.impl.persistence.entity.TaskEntity;
+
+import java.util.*;
+
+import static org.dromara.workflow.common.constant.FlowConstant.*;
+
+/**
+ * 工作流工具
+ *
+ * @author may
+ */
+public class WorkflowUtils {
+
+    public WorkflowUtils() {
+    }
+
+    private static final ProcessEngine PROCESS_ENGINE = SpringUtils.getBean(ProcessEngine.class);
+    private static final IWorkflowUserService I_WORKFLOW_USER_SERVICE = SpringUtils.getBean(WorkflowUserServiceImpl.class);
+    private static final IActHiProcinstService I_ACT_HI_PROCINST_SERVICE = SpringUtils.getBean(IActHiProcinstService.class);
+    private static final ActHiTaskinstMapper ACT_HI_TASKINST_MAPPER = SpringUtils.getBean(ActHiTaskinstMapper.class);
+
+    /**
+     * 创建一个新任务
+     *
+     * @param currentTask 参数
+     */
+    public static TaskEntity createNewTask(Task currentTask) {
+        TaskEntity task = null;
+        if (ObjectUtil.isNotEmpty(currentTask)) {
+            task = (TaskEntity) PROCESS_ENGINE.getTaskService().newTask();
+            task.setCategory(currentTask.getCategory());
+            task.setDescription(currentTask.getDescription());
+            task.setTenantId(currentTask.getTenantId());
+            task.setAssignee(currentTask.getAssignee());
+            task.setName(currentTask.getName());
+            task.setProcessDefinitionId(currentTask.getProcessDefinitionId());
+            task.setProcessInstanceId(currentTask.getProcessInstanceId());
+            task.setTaskDefinitionKey(currentTask.getTaskDefinitionKey());
+            task.setPriority(currentTask.getPriority());
+            task.setCreateTime(new Date());
+            task.setTenantId(TenantHelper.getTenantId());
+            PROCESS_ENGINE.getTaskService().saveTask(task);
+        }
+        if (ObjectUtil.isNotNull(task)) {
+            UpdateHiTaskInstCmd updateHiTaskInstCmd = new UpdateHiTaskInstCmd(Collections.singletonList(task.getId()), task.getProcessDefinitionId(), task.getProcessInstanceId());
+            PROCESS_ENGINE.getManagementService().executeCommand(updateHiTaskInstCmd);
+        }
+        return task;
+    }
+
+    /**
+     * 抄送任务
+     *
+     * @param parentTaskList 父级任务
+     * @param userIds        人员id
+     */
+    public static void createCopyTask(List<Task> parentTaskList, List<Long> userIds) {
+        List<Task> list = new ArrayList<>();
+        for (Task parentTask : parentTaskList) {
+            for (Long userId : userIds) {
+                TaskEntity newTask = (TaskEntity) PROCESS_ENGINE.getTaskService().newTask();
+                newTask.setParentTaskId(parentTask.getId());
+                newTask.setAssignee(userId.toString());
+                newTask.setName("【抄送】-" + parentTask.getName());
+                newTask.setProcessDefinitionId(parentTask.getProcessDefinitionId());
+                newTask.setProcessInstanceId(parentTask.getProcessInstanceId());
+                newTask.setTaskDefinitionKey(parentTask.getTaskDefinitionKey());
+                newTask.setTenantId(TenantHelper.getTenantId());
+                list.add(newTask);
+            }
+        }
+        PROCESS_ENGINE.getTaskService().bulkSaveTasks(list);
+        if (CollUtil.isNotEmpty(list) && CollUtil.isNotEmpty(parentTaskList)) {
+            String processInstanceId = parentTaskList.get(0).getProcessInstanceId();
+            String processDefinitionId = parentTaskList.get(0).getProcessDefinitionId();
+            List<String> taskIds = StreamUtils.toList(list, Task::getId);
+            ActHiTaskinst actHiTaskinst = new ActHiTaskinst();
+            actHiTaskinst.setProcDefId(processDefinitionId);
+            actHiTaskinst.setProcInstId(processInstanceId);
+            actHiTaskinst.setScopeType(TaskStatusEnum.COPY.getStatus());
+            actHiTaskinst.setTenantId(TenantHelper.getTenantId());
+            LambdaUpdateWrapper<ActHiTaskinst> updateWrapper = new LambdaUpdateWrapper<>();
+            updateWrapper.in(ActHiTaskinst::getId, taskIds);
+            ACT_HI_TASKINST_MAPPER.update(actHiTaskinst, updateWrapper);
+            for (Task task : list) {
+                PROCESS_ENGINE.getTaskService().addComment(task.getId(), task.getProcessInstanceId(), TaskStatusEnum.COPY.getStatus(), StrUtil.EMPTY);
+            }
+        }
+    }
+
+    /**
+     * 获取当前任务参与者
+     *
+     * @param taskId 任务id
+     */
+    public static ParticipantVo getCurrentTaskParticipant(String taskId) {
+        ParticipantVo participantVo = new ParticipantVo();
+        List<HistoricIdentityLink> linksForTask = PROCESS_ENGINE.getHistoryService().getHistoricIdentityLinksForTask(taskId);
+        Task task = PROCESS_ENGINE.getTaskService().createTaskQuery().taskTenantId(TenantHelper.getTenantId()).taskId(taskId).singleResult();
+        if (task != null && CollUtil.isNotEmpty(linksForTask)) {
+            List<HistoricIdentityLink> groupList = StreamUtils.filter(linksForTask, e -> StringUtils.isNotBlank(e.getGroupId()));
+            if (CollUtil.isNotEmpty(groupList)) {
+                List<Long> groupIds = StreamUtils.toList(groupList, e -> Long.valueOf(e.getGroupId()));
+                List<SysUserRole> sysUserRoles = I_WORKFLOW_USER_SERVICE.getUserRoleListByRoleIds(groupIds);
+                if (CollUtil.isNotEmpty(sysUserRoles)) {
+                    participantVo.setGroupIds(groupIds);
+                    List<Long> userIdList = StreamUtils.toList(sysUserRoles, SysUserRole::getUserId);
+                    List<SysUserVo> sysUsers = I_WORKFLOW_USER_SERVICE.getUserListByIds(userIdList);
+                    if (CollUtil.isNotEmpty(sysUsers)) {
+                        List<Long> userIds = StreamUtils.toList(sysUsers, SysUserVo::getUserId);
+                        List<String> nickNames = StreamUtils.toList(sysUsers, SysUserVo::getNickName);
+                        participantVo.setCandidate(userIds);
+                        participantVo.setCandidateName(nickNames);
+                        participantVo.setClaim(!StringUtils.isBlank(task.getAssignee()));
+                    }
+                }
+            } else {
+                List<HistoricIdentityLink> candidateList = StreamUtils.filter(linksForTask, e -> FlowConstant.CANDIDATE.equals(e.getType()));
+                List<Long> userIdList = new ArrayList<>();
+                for (HistoricIdentityLink historicIdentityLink : linksForTask) {
+                    try {
+                        userIdList.add(Long.valueOf(historicIdentityLink.getUserId()));
+                    } catch (NumberFormatException ignored) {
+
+                    }
+                }
+                List<SysUserVo> sysUsers = I_WORKFLOW_USER_SERVICE.getUserListByIds(userIdList);
+                if (CollUtil.isNotEmpty(sysUsers)) {
+                    List<Long> userIds = StreamUtils.toList(sysUsers, SysUserVo::getUserId);
+                    List<String> nickNames = StreamUtils.toList(sysUsers, SysUserVo::getNickName);
+                    participantVo.setCandidate(userIds);
+                    participantVo.setCandidateName(nickNames);
+                    if (StringUtils.isBlank(task.getAssignee()) && CollUtil.isNotEmpty(candidateList)) {
+                        participantVo.setClaim(false);
+                    }
+                    if (!StringUtils.isBlank(task.getAssignee()) && CollUtil.isNotEmpty(candidateList)) {
+                        participantVo.setClaim(true);
+                    }
+                }
+            }
+        }
+        return participantVo;
+    }
+
+    /**
+     * 判断当前节点是否为会签节点
+     *
+     * @param processDefinitionId 流程定义id
+     * @param taskDefinitionKey   流程定义id
+     */
+    public static MultiInstanceVo isMultiInstance(String processDefinitionId, String taskDefinitionKey) {
+        BpmnModel bpmnModel = PROCESS_ENGINE.getRepositoryService().getBpmnModel(processDefinitionId);
+        FlowNode flowNode = (FlowNode) bpmnModel.getFlowElement(taskDefinitionKey);
+        MultiInstanceVo multiInstanceVo = new MultiInstanceVo();
+        //判断是否为并行会签节点
+        if (flowNode.getBehavior() instanceof ParallelMultiInstanceBehavior behavior && behavior.getCollectionExpression() != null) {
+            Expression collectionExpression = behavior.getCollectionExpression();
+            String assigneeList = collectionExpression.getExpressionText();
+            String assignee = behavior.getCollectionElementVariable();
+            multiInstanceVo.setType(behavior);
+            multiInstanceVo.setAssignee(assignee);
+            multiInstanceVo.setAssigneeList(assigneeList);
+            return multiInstanceVo;
+            //判断是否为串行会签节点
+        } else if (flowNode.getBehavior() instanceof SequentialMultiInstanceBehavior behavior && behavior.getCollectionExpression() != null) {
+            Expression collectionExpression = behavior.getCollectionExpression();
+            String assigneeList = collectionExpression.getExpressionText();
+            String assignee = behavior.getCollectionElementVariable();
+            multiInstanceVo.setType(behavior);
+            multiInstanceVo.setAssignee(assignee);
+            multiInstanceVo.setAssigneeList(assigneeList);
+            return multiInstanceVo;
+        }
+        return null;
+    }
+
+    /**
+     * 获取当前流程状态
+     *
+     * @param taskId 任务id
+     */
+    public static String getBusinessStatusByTaskId(String taskId) {
+        HistoricTaskInstance historicTaskInstance = PROCESS_ENGINE.getHistoryService().createHistoricTaskInstanceQuery().taskId(taskId).taskTenantId(TenantHelper.getTenantId()).singleResult();
+        HistoricProcessInstance historicProcessInstance = PROCESS_ENGINE.getHistoryService().createHistoricProcessInstanceQuery().processInstanceId(historicTaskInstance.getProcessInstanceId()).processInstanceTenantId(TenantHelper.getTenantId()).singleResult();
+        return historicProcessInstance.getBusinessStatus();
+    }
+
+    /**
+     * 获取当前流程状态
+     *
+     * @param processInstanceId 流程实例id
+     */
+    public static String getBusinessStatus(String processInstanceId) {
+        HistoricProcessInstance historicProcessInstance = PROCESS_ENGINE.getHistoryService().createHistoricProcessInstanceQuery().processInstanceId(processInstanceId).processInstanceTenantId(TenantHelper.getTenantId()).singleResult();
+        return historicProcessInstance.getBusinessStatus();
+    }
+
+    /**
+     * 设置流程实例对象
+     *
+     * @param obj         业务对象
+     * @param businessKey 业务id
+     */
+    public static void setProcessInstanceVo(Object obj, String businessKey) {
+        if (StringUtils.isBlank(businessKey)) {
+            return;
+        }
+        ActHiProcinst actHiProcinst = I_ACT_HI_PROCINST_SERVICE.selectByBusinessKey(businessKey);
+        if (actHiProcinst == null) {
+            ProcessInstanceVo processInstanceVo = new ProcessInstanceVo();
+            processInstanceVo.setBusinessStatus(BusinessStatusEnum.DRAFT.getStatus());
+            ReflectUtils.invokeSetter(obj, PROCESS_INSTANCE_VO, processInstanceVo);
+            return;
+        }
+        ProcessInstanceVo processInstanceVo = BeanUtil.toBean(actHiProcinst, ProcessInstanceVo.class);
+        processInstanceVo.setBusinessStatusName(BusinessStatusEnum.findByStatus(processInstanceVo.getBusinessStatus()));
+        ReflectUtils.invokeSetter(obj, PROCESS_INSTANCE_VO, processInstanceVo);
+    }
+
+    /**
+     * 设置流程实例对象
+     *
+     * @param obj       业务对象
+     * @param idList    业务id
+     * @param fieldName 主键属性名称
+     */
+    public static void setProcessInstanceListVo(Object obj, List<String> idList, String fieldName) {
+        if (CollUtil.isEmpty(idList)) {
+            return;
+        }
+        List<ActHiProcinst> actHiProcinstList = I_ACT_HI_PROCINST_SERVICE.selectByBusinessKeyIn(idList);
+        if (obj instanceof Collection<?> collection) {
+            for (Object o : collection) {
+                String fieldValue = ReflectUtils.invokeGetter(o, fieldName).toString();
+                if (CollUtil.isEmpty(actHiProcinstList)) {
+                    ProcessInstanceVo processInstanceVo = new ProcessInstanceVo();
+                    processInstanceVo.setBusinessStatus(BusinessStatusEnum.DRAFT.getStatus());
+                    processInstanceVo.setBusinessStatusName(BusinessStatusEnum.findByStatus(processInstanceVo.getBusinessStatus()));
+                    ReflectUtils.invokeSetter(o, PROCESS_INSTANCE_VO, processInstanceVo);
+                } else {
+                    ActHiProcinst actHiProcinst = actHiProcinstList.stream().filter(e -> e.getBusinessKey().equals(fieldValue)).findFirst().orElse(null);
+                    if (ObjectUtil.isNotEmpty(actHiProcinst)) {
+                        ProcessInstanceVo processInstanceVo = BeanUtil.toBean(actHiProcinst, ProcessInstanceVo.class);
+                        processInstanceVo.setBusinessStatusName(BusinessStatusEnum.findByStatus(processInstanceVo.getBusinessStatus()));
+                        ReflectUtils.invokeSetter(o, PROCESS_INSTANCE_VO, processInstanceVo);
+                    } else {
+                        ProcessInstanceVo processInstanceVo = new ProcessInstanceVo();
+                        processInstanceVo.setBusinessStatus(BusinessStatusEnum.DRAFT.getStatus());
+                        processInstanceVo.setBusinessStatusName(BusinessStatusEnum.findByStatus(processInstanceVo.getBusinessStatus()));
+                        ReflectUtils.invokeSetter(o, PROCESS_INSTANCE_VO, processInstanceVo);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * 发送消息
+     *
+     * @param list        任务
+     * @param name        流程名称
+     * @param messageType 消息类型
+     * @param message     消息内容,为空则发送默认配置的消息内容
+     */
+    public static void sendMessage(List<Task> list, String name, List<String> messageType, String message) {
+        Set<Long> userIds = new HashSet<>();
+        if (StringUtils.isBlank(message)) {
+            message = "有新的【" + name + "】单据已经提交至您的待办,请您及时处理。";
+        }
+        for (Task t : list) {
+            ParticipantVo taskParticipant = WorkflowUtils.getCurrentTaskParticipant(t.getId());
+            if (CollUtil.isNotEmpty(taskParticipant.getGroupIds())) {
+                List<SysUserRole> sysUserRoles = I_WORKFLOW_USER_SERVICE.getUserRoleListByRoleIds(taskParticipant.getGroupIds());
+                if (CollUtil.isNotEmpty(sysUserRoles)) {
+                    userIds.addAll(StreamUtils.toList(sysUserRoles, SysUserRole::getUserId));
+                }
+            }
+            List<Long> candidate = taskParticipant.getCandidate();
+            if (CollUtil.isNotEmpty(candidate)) {
+                userIds.addAll(candidate);
+            }
+        }
+        if (CollUtil.isNotEmpty(userIds)) {
+            List<SysUserVo> sysUserVoList = I_WORKFLOW_USER_SERVICE.getUserListByIds(new ArrayList<>(userIds));
+            for (String code : messageType) {
+                if (code.equals(MessageTypeEnum.SYSTEM_MESSAGE.getCode())) {
+                    WebSocketMessageDto dto = new WebSocketMessageDto();
+                    dto.setSessionKeys(new ArrayList<>(userIds));
+                    dto.setMessage(message);
+                    WebSocketUtils.publishMessage(dto);
+                }
+                if (code.equals(MessageTypeEnum.EMAIL_MESSAGE.getCode())) {
+                    MailUtils.sendText(StreamUtils.join(sysUserVoList, SysUserVo::getEmail), "单据审批提醒", message);
+                }
+                if (code.equals(MessageTypeEnum.SMS_MESSAGE.getCode())) {
+                    //todo 短信发送
+                }
+            }
+        }
+    }
+}

+ 3 - 0
ruoyi-modules/ruoyi-workflow/src/main/resources/mapper/package-info.md

@@ -0,0 +1,3 @@
+java包使用 `.` 分割 resource 目录使用 `/` 分割
+<br>
+此文件目的 防止文件夹粘连找不到 `xml` 文件

+ 7 - 0
ruoyi-modules/ruoyi-workflow/src/main/resources/mapper/workflow/ActHiProcinstMapper.xml

@@ -0,0 +1,7 @@
+<?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="org.dromara.workflow.mapper.ActHiProcinstMapper">
+
+</mapper>

+ 7 - 0
ruoyi-modules/ruoyi-workflow/src/main/resources/mapper/workflow/ActHiTaskinstMapper.xml

@@ -0,0 +1,7 @@
+<?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="org.dromara.workflow.mapper.ActHiTaskinstMapper">
+
+</mapper>

+ 60 - 0
ruoyi-modules/ruoyi-workflow/src/main/resources/mapper/workflow/ActTaskMapper.xml

@@ -0,0 +1,60 @@
+<?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="org.dromara.workflow.mapper.ActTaskMapper">
+    <resultMap type="org.dromara.workflow.domain.vo.TaskVo" id="TaskWaitingVoResult">
+        <result property="id" column="ID_"/>
+        <result property="name" column="NAME_"/>
+        <result property="description" column="DESCRIPTION_"/>
+        <result property="priority" column="PRIORITY_"/>
+        <result property="owner" column="OWNER_"/>
+        <result property="assignee" column="ASSIGNEE_"/>
+        <result property="processInstanceId" column="PROC_INST_ID_"/>
+        <result property="executionId" column="EXECUTION_ID_"/>
+        <result property="taskDefinitionId" column="TASK_DEF_ID_"/>
+        <result property="processDefinitionId" column="PROC_DEF_ID_"/>
+        <result property="createTime" column="CREATE_TIME_"/>
+        <result property="endTime" column="END_TIME_"/>
+        <result property="taskDefinitionKey" column="TASK_DEF_KEY_"/>
+        <result property="dueDate" column="DUE_DATE_"/>
+        <result property="processDefinitionKey" column="key_"/>
+        <result property="category" column="CATEGORY_"/>
+        <result property="parentTaskId" column="PARENT_TASK_ID_"/>
+        <result property="tenantId" column="TENANT_ID_"/>
+        <result property="claimTime" column="CLAIM_TIME"/>
+        <result property="businessStatus" column="BUSINESS_STATUS_"/>
+        <result property="processDefinitionName" column="processDefinitionName"/>
+        <result property="processDefinitionKey" column="processDefinitionName"/>
+
+    </resultMap>
+    <select id="getTaskWaitByPage" resultMap="TaskWaitingVoResult">
+        select *
+        from (SELECT RES.*,
+                     AHP.BUSINESS_STATUS_,
+                     ARP.NAME_ as processDefinitionName,
+                     ARP.KEY_  as processDefinitionKey,
+                     LINK.USER_ID_,
+                     LINK.GROUP_ID_
+              FROM ACT_RU_TASK RES
+                       INNER JOIN ACT_HI_PROCINST AHP ON RES.PROC_INST_ID_ = AHP.PROC_INST_ID_
+                       INNER JOIN ACT_RE_PROCDEF ARP ON ARP.ID_ = RES.PROC_DEF_ID_
+                       LEFT JOIN ACT_RU_IDENTITYLINK LINK ON LINK.TASK_ID_ = RES.ID_ AND LINK.TYPE_ = 'candidate'
+              WHERE RES.PARENT_TASK_ID_ is null
+              ORDER BY RES.CREATE_TIME_ DESC) t ${ew.getCustomSqlSegment}
+    </select>
+
+    <select id="getTaskCopyByPage" resultMap="TaskWaitingVoResult">
+        select *
+        from (SELECT AHT.*,
+                     AHP.BUSINESS_STATUS_,
+                     ARP.NAME_ as processDefinitionName,
+                     ARP.KEY_  as processDefinitionKey
+              FROM ACT_HI_TASKINST AHT
+                       INNER JOIN ACT_HI_PROCINST AHP ON AHT.PROC_INST_ID_ = AHP.PROC_INST_ID_
+                       INNER JOIN ACT_RE_PROCDEF ARP ON ARP.ID_ = AHT.PROC_DEF_ID_
+              WHERE AHT.PARENT_TASK_ID_ is not null
+                and AHT.scope_type_ = 'copy'
+              ORDER BY AHT.START_TIME_ DESC) t ${ew.getCustomSqlSegment}
+    </select>
+</mapper>

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů