init: 导入RuoYi‑Vue‑Plus 6.X完整代码

This commit is contained in:
admin
2026-08-09 17:51:11 +08:00
commit 51cac459b3
914 changed files with 103833 additions and 0 deletions
+107
View File
@@ -0,0 +1,107 @@
<?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>
<artifactId>ruoyi-workflow</artifactId>
<description>
工作流模块
</description>
<dependencies>
<!-- 消息推送模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-push</artifactId>
</dependency>
<!-- 接口模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-doc</artifactId>
</dependency>
<!-- 邮件服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mail</artifactId>
</dependency>
<!-- 短信模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-sms</artifactId>
</dependency>
<!-- 数据库服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-mybatis</artifactId>
</dependency>
<!-- web服务 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-web</artifactId>
</dependency>
<!-- 日志记录 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-log</artifactId>
</dependency>
<!-- excel -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-excel</artifactId>
</dependency>
<!-- 翻译模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-translation</artifactId>
</dependency>
<!-- 安全模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-security</artifactId>
</dependency>
<!-- api模块 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-api</artifactId>
</dependency>
<!-- Warm-Flow 工作流引擎 -->
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-mybatis-plus-sb3-starter</artifactId>
</dependency>
<!-- Warm-Flow UI 插件 -->
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-plugin-ui-sb-web</artifactId>
</dependency>
<!-- LiteFlow 规则编排 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-liteflow</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,29 @@
package org.dromara.workflow.common;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义条件注解,用于基于配置启用或禁用特定功能
* <p>
* 该注解只会在配置文件中 `warm-flow.enabled=true` 时,标注了此注解的类或方法才会被 Spring 容器加载
* <p>
* 示例配置:
* <pre>
* warm-flow:
* enabled: true # 设置为 true 时,启用工作流功能
* </pre>
* <p>
* 使用此注解时,可以动态控制工作流功能是否启用,而不需要修改代码逻辑
*
* @author Lion Li
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
@ConditionalOnProperty(value = "warm-flow.enabled", havingValue = "true")
public @interface ConditionalOnEnable {
}
@@ -0,0 +1,111 @@
package org.dromara.workflow.common.constant;
/**
* 工作流常量
*
* @author may
*/
public interface FlowConstant {
/**
* 流程发起人
*/
String INITIATOR = "initiator";
/**
* 业务id
*/
String BUSINESS_ID = "businessId";
/**
* 部门id
*/
String INITIATOR_DEPT_ID = "initiatorDeptId";
/**
* 流程分类 ID 转名称的翻译标识。
*/
String CATEGORY_ID_TO_NAME = "category_id_to_name";
/**
* 流程分类名称缓存键。
*/
String FLOW_CATEGORY_NAME = "flow_category_name#30d";
/**
* 默认租户 OA 申请分类 ID。
*/
Long FLOW_CATEGORY_ID = 100L;
/**
* 申请人提交动作标识。
*/
String SUBMIT = "submit";
/**
* 抄送列表流程变量名。
*/
String FLOW_COPY_LIST = "flowCopyList";
/**
* 消息类型流程变量名。
*/
String MESSAGE_TYPE = "messageType";
/**
* 消息通知内容流程变量名。
*/
String MESSAGE_NOTICE = "messageNotice";
/**
* 我的发起页面路径。
*/
String PATH_MY_DOCUMENT = "/task/myDocument";
/**
* 我的待办页面路径。
*/
String PATH_TASK_WAITING = "/task/taskWaiting";
/**
* 我的已办页面路径。
*/
String PATH_TASK_FINISH = "/task/taskFinish";
/**
* 我的抄送页面路径。
*/
String PATH_TASK_COPY = "/task/taskCopyList";
/**
* 任务状态字典类型编码。
*/
String WF_TASK_STATUS = "wf_task_status";
/**
* 自动通过变量标识。
*/
String AUTO_PASS = "autoPass";
/**
* 业务编码流程变量名。
*/
String BUSINESS_CODE = "businessCode";
/**
* 忽略-办理权限校验(true:忽略,false:不忽略)
*/
String VAR_IGNORE = "ignore";
/**
* 忽略-委派处理(true:忽略,false:不忽略)
*/
String VAR_IGNORE_DEPUTE = "ignoreDepute";
/**
* 忽略-会签票签处理(true:忽略,false:不忽略)
*/
String VAR_IGNORE_COOPERATE = "ignoreCooperate";
}
@@ -0,0 +1,76 @@
package org.dromara.workflow.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 按钮权限枚举
*
* @author AprilWind
*/
@Getter
@AllArgsConstructor
public enum ButtonPermissionEnum implements NodeExtEnum {
/**
* 是否弹窗选人
*/
POP("是否弹窗选人", "pop", false),
/**
* 是否能委托
*/
TRUST("是否能委托", "trust", false),
/**
* 是否能转办
*/
TRANSFER("是否能转办", "transfer", false),
/**
* 是否能抄送
*/
COPY("是否能抄送", "copy", true),
/**
* 是否显示退回
*/
BACK("是否显示退回", "back", true),
/**
* 是否能加签
*/
ADD_SIGN("是否能加签", "addSign", false),
/**
* 是否能减签
*/
SUB_SIGN("是否能减签", "subSign", false),
/**
* 是否能终止
*/
TERMINATION("是否能终止", "termination", true),
/**
* 是否能上传附件
*/
FILE("是否能上传附件", "file", true);
/**
* 按钮展示名称。
*/
private final String label;
/**
* 按钮权限值。
*/
private final String value;
/**
* 是否默认开启。
*/
private final boolean selected;
}
@@ -0,0 +1,32 @@
package org.dromara.workflow.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 抄送设置枚举
*
* @author AprilWind
*/
@Getter
@AllArgsConstructor
public enum CopySettingEnum implements NodeExtEnum {
;
/**
* 展示名称。
*/
private final String label;
/**
* 配置值。
*/
private final String value;
/**
* 是否默认选中。
*/
private final boolean selected;
}
@@ -0,0 +1,61 @@
package org.dromara.workflow.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 消息类型枚举,定义流程通知支持的消息通道。
*
* @author may
*/
@Getter
@AllArgsConstructor
public enum MessageTypeEnum {
/**
* 站内信
*/
SYSTEM_MESSAGE("1", "站内信"),
/**
* 邮箱
*/
EMAIL_MESSAGE("2", "邮箱"),
/**
* 短信
*/
SMS_MESSAGE("3", "短信");
/**
* 消息类型编码。
*/
private final String code;
/**
* 消息类型描述。
*/
private final String desc;
/**
* 消息类型编码缓存。
*/
private static final Map<String, MessageTypeEnum> MESSAGE_TYPE_ENUM_MAP = Arrays.stream(values())
.collect(Collectors.toConcurrentMap(MessageTypeEnum::getCode, Function.identity()));
/**
* 根据消息类型编码获取枚举实例。
*
* @param code 消息类型code
* @return MessageTypeEnum
*/
public static MessageTypeEnum getByCode(String code) {
return MESSAGE_TYPE_ENUM_MAP.getOrDefault(code, null);
}
}
@@ -0,0 +1,32 @@
package org.dromara.workflow.common.enums;
/**
* 节点扩展属性枚举通用接口,约束扩展选项的展示名、值和默认勾选状态。
*
* @author AprilWind
*/
public interface NodeExtEnum {
/**
* 获取选项展示名称。
*
* @return 选项label
*/
String getLabel();
/**
* 获取选项值。
*
* @return 选项值
*/
String getValue();
/**
* 是否默认选中。
*
* @return 是否默认选中
*/
boolean isSelected();
}
@@ -0,0 +1,141 @@
package org.dromara.workflow.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
/**
* 任务分配人枚举
*
* @author AprilWind
*/
@Getter
@AllArgsConstructor
public enum TaskAssigneeEnum {
/**
* 用户
*/
USER("用户", ""),
/**
* 角色
*/
ROLE("角色", "role:"),
/**
* 部门
*/
DEPT("部门", "dept:"),
/**
* 岗位
*/
POST("岗位", "post:"),
/**
* SPEL表达式
*/
SPEL("SpEL表达式", "");
/**
* 类型描述。
*/
private final String desc;
/**
* 类型编码前缀。
*/
private final String code;
/**
* 根据描述获取对应的枚举类型
* <p>
* 通过传入描述,查找并返回匹配的枚举项。如果未找到匹配项,会抛出 {@link ServiceException}。
* </p>
*
* @param desc 描述,用于匹配对应的枚举项
* @return TaskAssigneeEnum 返回对应的枚举类型
* @throws ServiceException 如果未找到匹配的枚举项
*/
public static TaskAssigneeEnum fromDesc(String desc) {
for (TaskAssigneeEnum type : values()) {
if (type.getDesc().equals(desc)) {
return type;
}
}
throw new ServiceException("未知的办理人类型: " + desc);
}
/**
* 根据代码获取对应的枚举类型
* <p>
* 通过传入代码,查找并返回匹配的枚举项。如果未找到匹配项,会抛出 {@link ServiceException}。
* </p>
*
* @param code 代码,用于匹配对应的枚举项
* @return TaskAssigneeEnum 返回对应的枚举类型
* @throws IllegalArgumentException 如果未找到匹配的枚举项
*/
public static TaskAssigneeEnum fromCode(String code) {
for (TaskAssigneeEnum type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
throw new ServiceException("未知的办理人类型代码: " + code);
}
/**
* 获取所有办理人类型的描述列表,通常用于前端展示选择项。
*
* @return List<String> 返回所有办理人类型的描述列表
*/
public static List<String> getAssigneeTypeList() {
return Arrays.stream(values())
.map(TaskAssigneeEnum::getDesc)
.collect(Collectors.toList());
}
/**
* 获取所有办理人类型的代码列表,通常用于程序内部逻辑判断。
*
* @return List<String> 返回所有办理人类型的代码列表
*/
public static List<String> getAssigneeCodeList() {
return Arrays.stream(values())
.map(TaskAssigneeEnum::getCode)
.collect(Collectors.toList());
}
/**
* 判断当前办理人类型是否需要调用部门服务。
*
* @return 如果类型是 USER、DEPT 或 POST,则返回 true;否则返回 false
*/
public boolean needsDeptService() {
return this == USER || this == DEPT || this == POST;
}
/**
* 判断给定字符串是否符合 SPEL 表达式格式(以 $ 或 # 开头)
*
* @param value 待判断字符串
* @return 是否为 SPEL 表达式
*/
public static boolean isSpelExpression(String value) {
if (value == null) {
return false;
}
// $前缀表示默认办理人变量策略
// #前缀表示spel办理人变量策略
return StringUtils.startsWith(value, "$") || StringUtils.startsWith(value, "#");
}
}
@@ -0,0 +1,49 @@
package org.dromara.workflow.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 人员类型
*
* @author AprilWind
*/
@Getter
@AllArgsConstructor
public enum TaskAssigneeType {
/**
* 待办任务的审批人权限
* <p>该权限表示用户是待办任务的审批人,负责审核任务的执行情况。</p>
*/
APPROVER("1", "待办任务的审批人权限"),
/**
* 待办任务的转办人权限
* <p>该权限表示用户是待办任务的转办人,负责将任务分配给其他人员。</p>
*/
TRANSFER("2", "待办任务的转办人权限"),
/**
* 待办任务的委托人权限
* <p>该权限表示用户是待办任务的委托人,能够委托其他人代为处理任务。</p>
*/
DELEGATE("3", "待办任务的委托人权限"),
/**
* 待办任务的抄送人权限
* <p>该权限表示用户是待办任务的抄送人,仅接收任务信息的通知,不参与任务的审批或处理。</p>
*/
COPY("4", "待办任务的抄送人权限");
/**
* 类型
*/
private final String code;
/**
* 描述
*/
private final String description;
}
@@ -0,0 +1,66 @@
package org.dromara.workflow.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 任务操作类型枚举,定义流程任务支持的运行时操作。
*
* @author may
*/
@Getter
@AllArgsConstructor
public enum TaskOperationEnum {
/**
* 委派
*/
DELEGATE_TASK("delegateTask", "委派"),
/**
* 转办
*/
TRANSFER_TASK("transferTask", "转办"),
/**
* 加签
*/
ADD_SIGNATURE("addSignature", "加签"),
/**
* 减签
*/
REDUCTION_SIGNATURE("reductionSignature", "减签");
/**
* 操作编码,供接口与前端交互使用。
*/
private final String code;
/**
* 操作描述。
*/
private final String desc;
/**
* 操作编码缓存。
*/
private static final Map<String, TaskOperationEnum> CODE_MAP = Arrays.stream(values())
.collect(Collectors.toConcurrentMap(TaskOperationEnum::getCode, Function.identity()));
/**
* 根据 code 获取枚举实例。
*
* @param code 操作编码
* @return 对应的枚举实例
*/
public static TaskOperationEnum getByCode(String code) {
return CODE_MAP.get(code);
}
}
@@ -0,0 +1,115 @@
package org.dromara.workflow.common.enums;
import cn.hutool.core.util.StrUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 任务状态枚举
*
* @author may
*/
@Getter
@AllArgsConstructor
public enum TaskStatusEnum {
/**
* 撤销
*/
CANCEL("cancel", "撤销"),
/**
* 通过
*/
PASS("pass", "通过"),
/**
* 待审核
*/
WAITING("waiting", "待审核"),
/**
* 作废
*/
INVALID("invalid", "作废"),
/**
* 退回
*/
BACK("back", "退回"),
/**
* 终止
*/
TERMINATION("termination", "终止"),
/**
* 转办
*/
TRANSFER("transfer", "转办"),
/**
* 委托
*/
DEPUTE("depute", "委托"),
/**
* 抄送
*/
COPY("copy", "抄送"),
/**
* 加签
*/
SIGN("sign", "加签"),
/**
* 减签
*/
SIGN_OFF("sign_off", "减签"),
/**
* 超时
*/
TIMEOUT("timeout", "超时");
/**
* 状态编码与描述缓存。
*/
private static final Map<String, String> STATUS_DESC_MAP = Arrays.stream(values())
.collect(Collectors.toConcurrentMap(TaskStatusEnum::getStatus, TaskStatusEnum::getDesc));
/**
* 状态
*/
private final String status;
/**
* 状态描述。
*/
private final String desc;
/**
* 根据状态编码获取任务业务状态描述。
*
* @param status 状态
* @return 状态描述
*/
public static String findByStatus(String status) {
// 从缓存中直接获取描述
return STATUS_DESC_MAP.getOrDefault(status, StrUtil.EMPTY);
}
/**
* 判断状态是否为通过或退回
*
* @param status 状态值
* @return true 表示是通过或退回状态
*/
public static boolean isPassOrBack(String status) {
return PASS.getStatus().equals(status) || BACK.getStatus().equals(status);
}
}
@@ -0,0 +1,32 @@
package org.dromara.workflow.common.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 节点扩展变量枚举,占位用于后续补充可配置变量定义。
*
* @author AprilWind
*/
@Getter
@AllArgsConstructor
public enum VariablesEnum implements NodeExtEnum {
;
/**
* 展示名称。
*/
private final String label;
/**
* 变量值。
*/
private final String value;
/**
* 是否选中。
*/
private final boolean selected;
}
@@ -0,0 +1,16 @@
package org.dromara.workflow.config;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.springframework.context.annotation.Configuration;
/**
* WarmFlow 工作流配置入口,在工作流开关开启时注册相关组件。
*
* @author may
*/
@ConditionalOnEnable
@Configuration
public class WarmFlowConfig {
}
@@ -0,0 +1,149 @@
package org.dromara.workflow.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.lang.tree.Tree;
import jakarta.servlet.http.HttpServletResponse;
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.ExcelBuilder;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.redis.annotation.RepeatSubmit;
import org.dromara.common.web.core.BaseController;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.constant.FlowConstant;
import org.dromara.workflow.domain.bo.FlowCategoryBo;
import org.dromara.workflow.domain.vo.FlowCategoryVo;
import org.dromara.workflow.service.IFlwCategoryService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 流程分类
*
* @author may
*/
@ConditionalOnEnable
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/workflow/category")
public class FlwCategoryController extends BaseController {
private final IFlwCategoryService flwCategoryService;
/**
* 查询流程分类列表。
*
* @param bo 查询条件
* @return 流程分类列表
*/
@SaCheckPermission("workflow:category:list")
@GetMapping("/list")
public R<List<FlowCategoryVo>> list(FlowCategoryBo bo) {
List<FlowCategoryVo> list = flwCategoryService.queryList(bo);
return R.ok(list);
}
/**
* 导出流程分类列表。
*
* @param bo 查询条件
* @param response 响应流
*/
@SaCheckPermission("workflow:category:export")
@Log(title = "流程分类", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(FlowCategoryBo bo, HttpServletResponse response) {
List<FlowCategoryVo> list = flwCategoryService.queryList(bo);
ExcelBuilder.of(list, FlowCategoryVo.class).sheetName("流程分类").toResponse(response);
}
/**
* 获取单个流程分类详情。
*
* @param categoryId 主键
* @return 流程分类详情
*/
@SaCheckPermission("workflow:category:query")
@GetMapping("/{categoryId}")
public R<FlowCategoryVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long categoryId) {
return R.ok(flwCategoryService.queryById(categoryId));
}
/**
* 新增流程分类,并校验分类名称唯一性。
*
* @param category 分类信息
* @return 操作结果
*/
@SaCheckPermission("workflow:category:add")
@Log(title = "流程分类", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody FlowCategoryBo category) {
if (!flwCategoryService.checkCategoryNameUnique(category)) {
return R.fail("新增流程分类'" + category.getCategoryName() + "'失败,流程分类名称已存在");
}
return toAjax(flwCategoryService.insertByBo(category));
}
/**
* 修改流程分类,并校验名称唯一及父子关系合法性。
*
* @param category 分类信息
* @return 操作结果
*/
@SaCheckPermission("workflow:category:edit")
@Log(title = "流程分类", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody FlowCategoryBo category) {
Long categoryId = category.getCategoryId();
if (!flwCategoryService.checkCategoryNameUnique(category)) {
return R.fail("修改流程分类'" + category.getCategoryName() + "'失败,流程分类名称已存在");
} else if (category.getParentId().equals(categoryId)) {
return R.fail("修改流程分类'" + category.getCategoryName() + "'失败,上级流程分类不能是自己");
}
return toAjax(flwCategoryService.updateByBo(category));
}
/**
* 删除流程分类,删除前校验默认分类、子节点和绑定流程定义。
*
* @param categoryId 主键
* @return 操作结果
*/
@SaCheckPermission("workflow:category:remove")
@Log(title = "流程分类", businessType = BusinessType.DELETE)
@DeleteMapping("/{categoryId}")
public R<Void> remove(@PathVariable Long categoryId) {
if (FlowConstant.FLOW_CATEGORY_ID.equals(categoryId)) {
return R.warn("默认流程分类,不允许删除");
}
if (flwCategoryService.hasChildByCategoryId(categoryId)) {
return R.warn("存在下级流程分类,不允许删除");
}
if (flwCategoryService.checkCategoryExistDefinition(categoryId)) {
return R.warn("流程分类存在流程定义,不允许删除");
}
return toAjax(flwCategoryService.deleteWithValidById(categoryId));
}
/**
* 获取流程分类树列表,用于流程定义选择分类节点。
*
* @param categoryBo 流程分类
* @return 流程分类树
*/
@GetMapping("/categoryTree")
public R<List<Tree<String>>> categoryTree(FlowCategoryBo categoryBo) {
return R.ok(flwCategoryService.selectCategoryTreeList(categoryBo));
}
}
@@ -0,0 +1,223 @@
package org.dromara.workflow.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.PageResult;
import org.dromara.common.core.domain.R;
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.redis.annotation.RepeatSubmit;
import org.dromara.common.web.core.BaseController;
import org.dromara.warm.flow.core.entity.Definition;
import org.dromara.warm.flow.core.service.DefService;
import org.dromara.warm.flow.orm.entity.FlowDefinition;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.vo.FlowDefinitionVo;
import org.dromara.workflow.service.IFlwDefinitionService;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
/**
* 流程定义管理 控制层
*
* @author may
*/
@ConditionalOnEnable
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/workflow/definition")
public class FlwDefinitionController extends BaseController {
private final DefService defService;
private final IFlwDefinitionService flwDefinitionService;
/**
* 分页查询流程定义列表。
*
* @param flowDefinition 查询条件
* @param pageQuery 分页参数
* @return 流程定义分页数据
*/
@GetMapping("/list")
@SaCheckPermission("workflow:definition:list")
public R<PageResult<FlowDefinitionVo>> list(FlowDefinition flowDefinition, PageQuery pageQuery) {
return R.ok(flwDefinitionService.queryList(flowDefinition, pageQuery));
}
/**
* 分页查询未发布的流程定义列表。
*
* @param flowDefinition 查询条件
* @param pageQuery 分页参数
* @return 未发布流程定义分页数据
*/
@GetMapping("/unPublishList")
@SaCheckPermission("workflow:definition:list")
public R<PageResult<FlowDefinitionVo>> unPublishList(FlowDefinition flowDefinition, PageQuery pageQuery) {
return R.ok(flwDefinitionService.unPublishList(flowDefinition, pageQuery));
}
/**
* 获取流程定义详细信息。
*
* @param id 流程定义id
* @return 流程定义详情
*/
@GetMapping(value = "/{id}")
@SaCheckPermission("workflow:definition:query")
public R<Definition> getInfo(@PathVariable Long id) {
return R.ok(defService.getById(id));
}
/**
* 新增流程定义并执行格式校验。
*
* @param flowDefinition 流程定义信息
* @return 操作结果
*/
@Log(title = "流程定义", businessType = BusinessType.INSERT)
@PostMapping
@RepeatSubmit()
@Transactional(rollbackFor = Exception.class)
@SaCheckPermission("workflow:definition:add")
public R<Boolean> add(@RequestBody FlowDefinition flowDefinition) {
return R.ok(defService.checkAndSave(flowDefinition));
}
/**
* 修改流程定义。
*
* @param flowDefinition 流程定义信息
* @return 操作结果
*/
@Log(title = "流程定义", businessType = BusinessType.UPDATE)
@PutMapping
@RepeatSubmit()
@Transactional(rollbackFor = Exception.class)
@SaCheckPermission("workflow:definition:edit")
public R<Boolean> edit(@RequestBody FlowDefinition flowDefinition) {
return R.ok(defService.updateById(flowDefinition));
}
/**
* 发布流程定义,使其进入可用状态。
*
* @param id 流程定义id
* @return 发布结果
*/
@Log(title = "流程定义", businessType = BusinessType.INSERT)
@PutMapping("/publish/{id}")
@RepeatSubmit()
@SaCheckPermission("workflow:definition:publish")
public R<Boolean> publish(@PathVariable Long id) {
return R.ok(flwDefinitionService.publish(id));
}
/**
* 取消发布流程定义。
*
* @param id 流程定义id
* @return 操作结果
*/
@Log(title = "流程定义", businessType = BusinessType.INSERT)
@PutMapping("/unPublish/{id}")
@RepeatSubmit()
@Transactional(rollbackFor = Exception.class)
@SaCheckPermission("workflow:definition:publish")
public R<Boolean> unPublish(@PathVariable Long id) {
return R.ok(defService.unPublish(id));
}
/**
* 批量删除流程定义。
*
* @param ids 流程定义ID集合
* @return 操作结果
*/
@Log(title = "流程定义", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
@SaCheckPermission("workflow:definition:remove")
public R<Void> remove(@PathVariable List<Long> ids) {
return toAjax(flwDefinitionService.removeDef(ids));
}
/**
* 复制一份流程定义,便于快速创建相似流程。
*
* @param id 流程定义id
* @return 复制结果
*/
@Log(title = "流程定义", businessType = BusinessType.INSERT)
@PostMapping("/copy/{id}")
@RepeatSubmit()
@Transactional(rollbackFor = Exception.class)
@SaCheckPermission("workflow:definition:copy")
public R<Boolean> copy(@PathVariable Long id) {
return R.ok(defService.copyDef(id));
}
/**
* 通过文件导入流程定义。
*
* @param file 文件
* @param category 分类
* @return 导入结果
*/
@Log(title = "流程定义", businessType = BusinessType.IMPORT)
@PostMapping("/importDef")
@SaCheckPermission("workflow:definition:import")
public R<Boolean> importDef(MultipartFile file, String category) {
return R.ok(flwDefinitionService.importJson(file, category));
}
/**
* 导出流程定义文件。
*
* @param id 流程定义id
* @param response 响应
* @throws IOException 异常
*/
@Log(title = "流程定义", businessType = BusinessType.EXPORT)
@PostMapping("/exportDef/{id}")
@SaCheckPermission("workflow:definition:export")
public void exportDef(@PathVariable Long id, HttpServletResponse response) throws IOException {
flwDefinitionService.exportDef(id, response);
}
/**
* 获取流程定义 JSON 字符串。
*
* @param id 流程定义id
* @return 流程定义 JSON
*/
@GetMapping("/xmlString/{id}")
@SaCheckPermission("workflow:definition:query")
public R<String> xmlString(@PathVariable Long id) {
return R.data(defService.exportJson(id));
}
/**
* 激活或挂起流程定义。
*
* @param id 流程定义id
* @param active 激活/挂起
* @return 处理结果
*/
@RepeatSubmit()
@PutMapping("/active/{id}")
@Transactional(rollbackFor = Exception.class)
@Log(title = "流程定义", businessType = BusinessType.UPDATE)
@SaCheckPermission("workflow:definition:active")
public R<Boolean> active(@PathVariable Long id, @RequestParam boolean active) {
return R.ok(active ? defService.active(id) : defService.unActive(id));
}
}
@@ -0,0 +1,214 @@
package org.dromara.workflow.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.convert.Convert;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.PageResult;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.utils.StreamUtils;
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.redis.annotation.RepeatSubmit;
import org.dromara.common.web.core.BaseController;
import org.dromara.warm.flow.core.service.InsService;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.bo.FlowCancelBo;
import org.dromara.workflow.domain.bo.FlowInstanceBo;
import org.dromara.workflow.domain.bo.FlowInvalidBo;
import org.dromara.workflow.domain.bo.FlowVariableBo;
import org.dromara.workflow.domain.vo.FlowInstanceVo;
import org.dromara.workflow.service.IFlwInstanceService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
/**
* 流程实例管理 控制层
*
* @author may
*/
@ConditionalOnEnable
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/workflow/instance")
public class FlwInstanceController extends BaseController {
private final InsService insService;
private final IFlwInstanceService flwInstanceService;
/**
* 分页查询正在运行的流程实例。
*
* @param flowInstanceBo 流程实例
* @param pageQuery 分页
* @return 正在运行的流程实例分页数据
*/
@GetMapping("/pageByRunning")
@SaCheckPermission("workflow:instance:list")
public R<PageResult<FlowInstanceVo>> selectRunningInstanceList(FlowInstanceBo flowInstanceBo, PageQuery pageQuery) {
return R.ok(flwInstanceService.selectRunningInstanceList(flowInstanceBo, pageQuery));
}
/**
* 分页查询已结束的流程实例。
*
* @param flowInstanceBo 流程实例
* @param pageQuery 分页
* @return 已结束的流程实例分页数据
*/
@GetMapping("/pageByFinish")
@SaCheckPermission("workflow:instance:list")
public R<PageResult<FlowInstanceVo>> selectFinishInstanceList(FlowInstanceBo flowInstanceBo, PageQuery pageQuery) {
return R.ok(flwInstanceService.selectFinishInstanceList(flowInstanceBo, pageQuery));
}
/**
* 根据业务 id 查询流程实例详细信息。
*
* @param businessId 业务id
* @return 流程实例详情
*/
@GetMapping("/getInfo/{businessId}")
@SaCheckPermission("workflow:instance:query")
public R<FlowInstanceVo> getInfo(@PathVariable Long businessId) {
return R.ok(flwInstanceService.queryByBusinessId(businessId));
}
/**
* 按业务 id 批量删除流程实例。
*
* @param businessIds 业务id
* @return 操作结果
*/
@DeleteMapping("/deleteByBusinessIds/{businessIds}")
@Log(title = "流程实例管理", businessType = BusinessType.DELETE)
@SaCheckPermission("workflow:instance:remove")
public R<Void> deleteByBusinessIds(@PathVariable List<Long> businessIds) {
return toAjax(flwInstanceService.deleteByBusinessIds(StreamUtils.toList(businessIds, Convert::toStr)));
}
/**
* 按实例 id 批量删除流程实例。
*
* @param instanceIds 实例id
* @return 操作结果
*/
@DeleteMapping("/deleteByInstanceIds/{instanceIds}")
@Log(title = "流程实例管理", businessType = BusinessType.DELETE)
@SaCheckPermission("workflow:instance:remove")
public R<Void> deleteByInstanceIds(@PathVariable List<Long> instanceIds) {
return toAjax(flwInstanceService.deleteByInstanceIds(instanceIds));
}
/**
* 按实例 id 批量删除已完成的流程实例。
*
* @param instanceIds 实例id
* @return 操作结果
*/
@DeleteMapping("/deleteHisByInstanceIds/{instanceIds}")
@Log(title = "流程实例管理", businessType = BusinessType.DELETE)
@SaCheckPermission("workflow:instance:remove")
public R<Void> deleteHisByInstanceIds(@PathVariable List<Long> instanceIds) {
return toAjax(flwInstanceService.deleteHisByInstanceIds(instanceIds));
}
/**
* 撤销当前申请人发起的流程。
*
* @param bo 参数
* @return 操作结果
*/
@RepeatSubmit()
@PutMapping("/cancelProcessApply")
@Log(title = "流程实例管理", businessType = BusinessType.UPDATE)
@SaCheckPermission("workflow:instance:cancel")
public R<Void> cancelProcessApply(@RequestBody FlowCancelBo bo) {
return toAjax(flwInstanceService.cancelProcessApply(bo));
}
/**
* 激活或挂起流程实例。
*
* @param id 流程实例id
* @param active 激活/挂起
* @return 处理结果
*/
@RepeatSubmit()
@PutMapping("/active/{id}")
@Log(title = "流程实例管理", businessType = BusinessType.UPDATE)
@SaCheckPermission("workflow:instance:active")
public R<Boolean> active(@PathVariable Long id, @RequestParam boolean active) {
return R.ok(active ? insService.active(id) : insService.unActive(id));
}
/**
* 获取当前登录人发起的流程实例列表。
*
* @param flowInstanceBo 参数
* @param pageQuery 分页
* @return 当前用户发起的流程实例分页数据
*/
@GetMapping("/pageByCurrent")
@SaCheckPermission("workflow:instance:currentList")
public R<PageResult<FlowInstanceVo>> selectCurrentInstanceList(FlowInstanceBo flowInstanceBo, PageQuery pageQuery) {
return R.ok(flwInstanceService.selectCurrentInstanceList(flowInstanceBo, pageQuery));
}
/**
* 获取流程图和流程记录,用于展示实例流转轨迹。
*
* @param businessId 业务id
* @return 流程图与历史节点信息
*/
@GetMapping("/flowHisTaskList/{businessId}")
@SaCheckPermission("workflow:instance:query")
public R<Map<String, Object>> flowHisTaskList(@PathVariable String businessId) {
return R.ok(flwInstanceService.flowHisTaskList(businessId));
}
/**
* 获取流程变量。
*
* @param instanceId 流程实例id
* @return 流程变量
*/
@GetMapping("/instanceVariable/{instanceId}")
@SaCheckPermission("workflow:instance:variableQuery")
public R<Map<String, Object>> instanceVariable(@PathVariable Long instanceId) {
return R.ok(flwInstanceService.instanceVariable(instanceId));
}
/**
* 修改流程变量。
*
* @param bo 参数
* @return 操作结果
*/
@RepeatSubmit()
@PutMapping("/updateVariable")
@Log(title = "流程实例管理", businessType = BusinessType.UPDATE)
@SaCheckPermission("workflow:instance:variable")
public R<Void> updateVariable(@Validated @RequestBody FlowVariableBo bo) {
return toAjax(flwInstanceService.updateVariable(bo));
}
/**
* 作废流程实例。
*
* @param bo 参数
* @return 处理结果
*/
@Log(title = "流程实例管理", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping("/invalid")
@SaCheckPermission("workflow:instance:invalid")
public R<Boolean> invalid(@Validated @RequestBody FlowInvalidBo bo) {
return R.ok(flwInstanceService.processInvalid(bo));
}
}
@@ -0,0 +1,105 @@
package org.dromara.workflow.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.PageResult;
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.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.redis.annotation.RepeatSubmit;
import org.dromara.common.web.core.BaseController;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.bo.FlowSpelBo;
import org.dromara.workflow.domain.vo.FlowSpelVo;
import org.dromara.workflow.service.IFlwSpelService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 流程 SpEL 表达式定义控制器,负责表达式规则的增删改查。
*
* @author Michelle.Chung
* @date 2025-07-04
*/
@ConditionalOnEnable
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/workflow/spel")
public class FlwSpelController extends BaseController {
private final IFlwSpelService flwSpelService;
/**
* 分页查询流程 SpEL 表达式定义列表。
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 表达式分页数据
*/
@SaCheckPermission("workflow:spel:list")
@GetMapping("/list")
public R<PageResult<FlowSpelVo>> list(FlowSpelBo bo, PageQuery pageQuery) {
return R.ok(flwSpelService.queryPageList(bo, pageQuery));
}
/**
* 获取流程 SpEL 表达式定义详情。
*
* @param id 主键
* @return 表达式详情
*/
@SaCheckPermission("workflow:spel:query")
@GetMapping("/{id}")
public R<FlowSpelVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(flwSpelService.queryById(id));
}
/**
* 新增流程 SpEL 表达式定义。
*
* @param bo 表达式信息
* @return 操作结果
*/
@SaCheckPermission("workflow:spel:add")
@Log(title = "流程spel表达式定义", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<Void> add(@Validated(AddGroup.class) @RequestBody FlowSpelBo bo) {
return toAjax(flwSpelService.insertByBo(bo));
}
/**
* 修改流程 SpEL 表达式定义。
*
* @param bo 表达式信息
* @return 操作结果
*/
@SaCheckPermission("workflow:spel:edit")
@Log(title = "流程spel表达式定义", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<Void> edit(@Validated(EditGroup.class) @RequestBody FlowSpelBo bo) {
return toAjax(flwSpelService.updateByBo(bo));
}
/**
* 批量删除流程 SpEL 表达式定义。
*
* @param ids 主键串
* @return 操作结果
*/
@SaCheckPermission("workflow:spel:remove")
@Log(title = "流程spel表达式定义", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
return toAjax(flwSpelService.deleteWithValidByIds(List.of(ids), true));
}
}
@@ -0,0 +1,242 @@
package org.dromara.workflow.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.PageResult;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
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.redis.annotation.RepeatSubmit;
import org.dromara.common.web.core.BaseController;
import org.dromara.system.api.domain.UserDTO;
import org.dromara.warm.flow.core.entity.Node;
import org.dromara.warm.flow.orm.entity.FlowNode;
import org.dromara.workflow.api.domain.StartProcessReturnDTO;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.bo.*;
import org.dromara.workflow.domain.vo.FlowHisTaskVo;
import org.dromara.workflow.domain.vo.FlowTaskVo;
import org.dromara.workflow.service.IFlwTaskService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 任务管理 控制层
*
* @author may
*/
@ConditionalOnEnable
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/workflow/task")
public class FlwTaskController extends BaseController {
private final IFlwTaskService flwTaskService;
/**
* 启动流程任务。
*
* @param startProcessBo 启动流程参数
* @return 启动结果及后续流程信息
*/
@Log(title = "任务管理", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping("/startWorkFlow")
public R<StartProcessReturnDTO> startWorkFlow(@Validated(AddGroup.class) @RequestBody StartProcessBo startProcessBo) {
StartProcessReturnDTO startProcessReturn = flwTaskService.startWorkFlow(startProcessBo);
return R.ok("提交成功", startProcessReturn);
}
/**
* 办理当前任务节点。
*
* @param completeTaskBo 办理任务参数
* @return 操作结果
*/
@Log(title = "任务管理", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping("/completeTask")
public R<Void> completeTask(@Validated(AddGroup.class) @RequestBody CompleteTaskBo completeTaskBo) {
return toAjax(flwTaskService.completeTask(completeTaskBo));
}
/**
* 查询当前用户的待办任务。
*
* @param flowTaskBo 参数
* @param pageQuery 分页
* @return 待办任务分页数据
*/
@GetMapping("/pageByTaskWait")
public R<PageResult<FlowTaskVo>> pageByTaskWait(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
return R.ok(flwTaskService.pageByTaskWait(flowTaskBo, pageQuery));
}
/**
* 查询当前用户的已办任务。
*
* @param flowTaskBo 参数
* @param pageQuery 分页
* @return 已办任务分页数据
*/
@GetMapping("/pageByTaskFinish")
public R<PageResult<FlowHisTaskVo>> pageByTaskFinish(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
return R.ok(flwTaskService.pageByTaskFinish(flowTaskBo, pageQuery));
}
/**
* 查询全部待办任务。
*
* @param flowTaskBo 参数
* @param pageQuery 分页
* @return 待办任务分页数据
*/
@SaCheckPermission("workflow:task:list")
@GetMapping("/pageByAllTaskWait")
public R<PageResult<FlowTaskVo>> pageByAllTaskWait(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
return R.ok(flwTaskService.pageByAllTaskWait(flowTaskBo, pageQuery));
}
/**
* 查询全部已办任务。
*
* @param flowTaskBo 参数
* @param pageQuery 分页
* @return 已办任务分页数据
*/
@SaCheckPermission("workflow:task:list")
@GetMapping("/pageByAllTaskFinish")
public R<PageResult<FlowHisTaskVo>> pageByAllTaskFinish(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
return R.ok(flwTaskService.pageByAllTaskFinish(flowTaskBo, pageQuery));
}
/**
* 查询当前用户收到的抄送任务。
*
* @param flowTaskBo 参数
* @param pageQuery 分页
* @return 抄送任务分页数据
*/
@GetMapping("/pageByTaskCopy")
public R<PageResult<FlowTaskVo>> pageByTaskCopy(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
return R.ok(flwTaskService.pageByTaskCopy(flowTaskBo, pageQuery));
}
/**
* 根据任务 id 查询任务详情。
*
* @param taskId 任务id
* @return 任务详情
*/
@GetMapping("/getTask/{taskId}")
public R<FlowTaskVo> getTask(@PathVariable Long taskId) {
return R.ok(flwTaskService.selectById(taskId));
}
/**
* 获取流程下一节点信息。
*
* @param bo 参数
* @return 下一节点列表
*/
@PostMapping("/getNextNodeList")
public R<List<FlowNode>> getNextNodeList(@RequestBody FlowNextNodeBo bo) {
return R.ok(flwTaskService.getNextNodeList(bo));
}
/**
* 终止流程任务。
*
* @param bo 参数
* @return 处理结果
*/
@Log(title = "任务管理", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping("/terminationTask")
public R<Boolean> terminationTask(@RequestBody FlowTerminationBo bo) {
return R.ok(flwTaskService.terminationTask(bo));
}
/**
* 执行任务操作,例如委派、转办、加签或减签。
*
* @param bo 参数
* @param taskOperation 操作类型,委派 delegateTask、转办 transferTask、加签 addSignature、减签 reductionSignature
* @return 操作结果
*/
@Log(title = "任务管理", businessType = BusinessType.UPDATE)
@RepeatSubmit
@PostMapping("/taskOperation/{taskOperation}")
public R<Void> taskOperation(@Validated @RequestBody TaskOperationBo bo, @PathVariable String taskOperation) {
return toAjax(flwTaskService.taskOperation(bo, taskOperation));
}
/**
* 批量修改任务办理人。
*
* @param taskIdList 任务id
* @param userId 办理人id
* @return 操作结果
*/
@Log(title = "任务管理", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@SaCheckPermission("workflow:task:edit")
@PutMapping("/updateAssignee/{userId}")
public R<Void> updateAssignee(@RequestBody List<Long> taskIdList, @PathVariable String userId) {
return toAjax(flwTaskService.updateAssignee(taskIdList, userId));
}
/**
* 驳回审批到前置节点。
*
* @param bo 参数
* @return 操作结果
*/
@Log(title = "任务管理", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping("/backProcess")
public R<Void> backProcess(@Validated({AddGroup.class}) @RequestBody BackProcessBo bo) {
return toAjax(flwTaskService.backProcess(bo));
}
/**
* 获取当前任务可驳回的前置节点。
*
* @param taskId 任务id
* @param nowNodeCode 当前节点
* @return 可驳回节点列表
*/
@GetMapping("/getBackTaskNode/{taskId}/{nowNodeCode}")
public R<List<Node>> getBackTaskNode(@PathVariable Long taskId, @PathVariable String nowNodeCode) {
return R.ok(flwTaskService.getBackTaskNode(taskId, nowNodeCode));
}
/**
* 获取当前任务的所有办理人。
*
* @param taskId 任务id
* @return 办理人列表
*/
@GetMapping("/currentTaskAllUser/{taskId}")
public R<List<UserDTO>> currentTaskAllUser(@PathVariable Long taskId) {
return R.ok(flwTaskService.currentTaskAllUser(List.of(taskId)));
}
/**
* 催办任务。
*
* @param bo 参数
* @return 结果
*/
@PostMapping("/urgeTask")
@Log(title = "任务管理", businessType = BusinessType.INSERT)
@SaCheckPermission("workflow:task:edit")
public R<Void> urgeTask(@RequestBody FlowUrgeTaskBo bo) {
return toAjax(flwTaskService.urgeTask(bo));
}
}
@@ -0,0 +1,137 @@
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.PageResult;
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.ExcelBuilder;
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.redis.annotation.RepeatSubmit;
import org.dromara.common.web.core.BaseController;
import org.dromara.workflow.common.ConditionalOnEnable;
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
*/
@ConditionalOnEnable
@Validated
@RequiredArgsConstructor
@RestController
@RequestMapping("/workflow/leave")
public class TestLeaveController extends BaseController {
private final ITestLeaveService testLeaveService;
/**
* 分页查询请假列表。
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 请假分页数据
*/
@SaCheckPermission("workflow:leave:list")
@GetMapping("/list")
public R<PageResult<TestLeaveVo>> list(TestLeaveBo bo, PageQuery pageQuery) {
return R.ok(testLeaveService.queryPageList(bo, pageQuery));
}
/**
* 导出请假列表。
*
* @param bo 查询条件
* @param response 响应流
*/
@SaCheckPermission("workflow:leave:export")
@Log(title = "请假", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(TestLeaveBo bo, HttpServletResponse response) {
List<TestLeaveVo> list = testLeaveService.queryList(bo);
ExcelBuilder.of(list, TestLeaveVo.class).sheetName("请假").toResponse(response);
}
/**
* 获取请假单详情。
*
* @param id 主键
* @return 请假详情
*/
@SaCheckPermission("workflow:leave:query")
@GetMapping("/{id}")
public R<TestLeaveVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(testLeaveService.queryById(id));
}
/**
* 新增请假单。
*
* @param bo 请假信息
* @return 新增后的请假单
*/
@SaCheckPermission("workflow:leave:add")
@Log(title = "请假", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping()
public R<TestLeaveVo> add(@Validated(AddGroup.class) @RequestBody TestLeaveBo bo) {
return R.ok(testLeaveService.insertByBo(bo));
}
/**
* 提交请假单并同步发起流程。
*
* @param bo 请假信息
* @return 提交后的请假单
*/
@SaCheckPermission("workflow:leave:add")
@Log(title = "请假", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping("/submitAndFlowStart")
public R<TestLeaveVo> submitAndFlowStart(@Validated(AddGroup.class) @RequestBody TestLeaveBo bo) {
return R.ok(testLeaveService.submitAndFlowStart(bo));
}
/**
* 修改请假单。
*
* @param bo 请假信息
* @return 修改后的请假单
*/
@SaCheckPermission("workflow:leave:edit")
@Log(title = "请假", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping()
public R<TestLeaveVo> edit(@Validated(EditGroup.class) @RequestBody TestLeaveBo bo) {
return R.ok(testLeaveService.updateByBo(bo));
}
/**
* 批量删除请假单。
*
* @param ids 主键串
* @return 操作结果
*/
@SaCheckPermission("workflow: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)));
}
}
@@ -0,0 +1,67 @@
package org.dromara.workflow.domain;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
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.ArrayList;
import java.util.List;
/**
* 流程分类对象 wf_category
*
* @author may
* @date 2023-06-27
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("flow_category")
public class FlowCategory extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 流程分类ID
*/
@TableId(value = "category_id")
private Long categoryId;
/**
* 父流程分类id
*/
private Long parentId;
/**
* 祖级列表
*/
private String ancestors;
/**
* 流程分类名称
*/
private String categoryName;
/**
* 显示顺序
*/
private Long orderNum;
/**
* 删除标志(0代表存在 1代表删除)
*/
@TableLogic
private String delFlag;
/**
* 子菜单
*/
@TableField(exist = false)
private List<FlowCategory> children = new ArrayList<>();
}
@@ -0,0 +1,59 @@
package org.dromara.workflow.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.io.Serial;
/**
* 流程实例业务扩展对象 flow_instance_biz_ext
*
* @author may
* @date 2025-08-05
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("flow_instance_biz_ext")
public class FlowInstanceBizExt extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id")
private Long id;
/**
* 流程实例ID
*/
private Long instanceId;
/**
* 业务ID
*/
private String businessId;
/**
* 业务编码
*/
private String businessCode;
/**
* 业务标题
*/
private String businessTitle;
/**
* 删除标志(0代表存在 1代表删除)
*/
@TableLogic
private String delFlag;
}
@@ -0,0 +1,69 @@
package org.dromara.workflow.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import java.io.Serial;
/**
* 流程spel表达式定义对象 flow_spel
*
* @author Michelle.Chung
* @date 2025-07-04
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("flow_spel")
public class FlowSpel extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键id
*/
@TableId(value = "id")
private Long id;
/**
* 组件名称
*/
private String componentName;
/**
* 方法名
*/
private String methodName;
/**
* 参数
*/
private String methodParams;
/**
* 预览spel表达式
*/
private String viewSpel;
/**
* 状态(0正常 1停用)
*/
private String status;
/**
* 备注
*/
private String remark;
/**
* 删除标志
*/
@TableLogic
private String delFlag;
}
@@ -0,0 +1,68 @@
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.time.LocalDateTime;
/**
* 请假对象 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 applyCode;
/**
* 请假类型
*/
private String leaveType;
/**
* 开始时间
*/
private LocalDateTime startDate;
/**
* 结束时间
*/
private LocalDateTime endDate;
/**
* 请假天数
*/
private Integer leaveDays;
/**
* 请假原因
*/
private String remark;
/**
* 状态
*/
private String status;
}
@@ -0,0 +1,70 @@
package org.dromara.workflow.domain.bo;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.dromara.common.core.validate.AddGroup;
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 BackProcessBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 任务ID
*/
@NotNull(message = "任务ID不能为空", groups = AddGroup.class)
private Long taskId;
/**
* 附件id
*/
private String fileId;
/**
* 消息类型
*/
private List<String> messageType;
/**
* 驳回的节点id(目前未使用,直接驳回到申请人)
*/
private String nodeCode;
/**
* 办理意见
*/
private String message;
/**
* 通知
*/
private String notice;
/**
* 流程变量
*/
private Map<String, Object> variables;
public Map<String, Object> getVariables() {
if (variables == null) {
variables = new HashMap<>(16);
return variables;
}
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables;
}
}
@@ -0,0 +1,85 @@
package org.dromara.workflow.domain.bo;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.dromara.common.core.validate.AddGroup;
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
*/
@NotNull(message = "任务id不能为空", groups = {AddGroup.class})
private Long taskId;
/**
* 附件id
*/
private String fileId;
/**
* 抄送人员
*/
private List<FlowCopyBo> flowCopyList;
/**
* 消息类型
*/
private List<String> messageType;
/**
* 办理意见
*/
private String message;
/**
* 消息通知
*/
private String notice;
/**
* 办理人(可不填 用于覆盖当前节点办理人)
*/
private String handler;
/**
* 流程变量
*/
private Map<String, Object> variables;
/**
* 弹窗选择的办理人
*/
private Map<String, Object> assigneeMap;
/**
* 扩展变量(此处为逗号分隔的ossId)
*/
private String ext;
public Map<String, Object> getVariables() {
if (variables == null) {
variables = new HashMap<>(16);
return variables;
}
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables;
}
}
@@ -0,0 +1,18 @@
package org.dromara.workflow.domain.bo;
import jakarta.validation.constraints.NotBlank;
import org.dromara.common.core.validate.AddGroup;
/**
* 撤销流程请求对象。
*
* @param businessId 业务 ID
* @param message 撤销说明
* @author may
*/
public record FlowCancelBo(
@NotBlank(message = "业务ID不能为空", groups = AddGroup.class)
String businessId,
String message
) {
}
@@ -0,0 +1,50 @@
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 org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.workflow.domain.FlowCategory;
import java.io.Serial;
import java.io.Serializable;
/**
* 流程分类业务对象 wf_category
*
* @author may
* @date 2023-06-27
*/
@Data
@AutoMapper(target = FlowCategory.class, reverseConvertGenerate = false)
public class FlowCategoryBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 流程分类ID
*/
@NotNull(message = "流程分类ID不能为空", groups = {EditGroup.class})
private Long categoryId;
/**
* 父流程分类id
*/
@NotNull(message = "父流程分类id不能为空", groups = {AddGroup.class, EditGroup.class})
private Long parentId;
/**
* 流程分类名称
*/
@NotBlank(message = "流程分类名称不能为空", groups = {AddGroup.class, EditGroup.class})
private String categoryName;
/**
* 显示顺序
*/
private Long orderNum;
}
@@ -0,0 +1,30 @@
package org.dromara.workflow.domain.bo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 流程抄送请求对象。
*
* @author may
*/
@Data
public class FlowCopyBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户id
*/
private Long userId;
/**
* 用户昵称
*/
private String nickName;
}
@@ -0,0 +1,55 @@
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 FlowInstanceBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 流程定义名称
*/
private String flowName;
/**
* 流程定义编码
*/
private String flowCode;
/**
* 任务发起人
*/
private String startUserId;
/**
* 业务id
*/
private String businessId;
/**
* 流程分类id
*/
private String category;
/**
* 任务名称
*/
private String nodeName;
/**
* 申请人Ids
*/
private List<String> createByIds;
}
@@ -0,0 +1,18 @@
package org.dromara.workflow.domain.bo;
import jakarta.validation.constraints.NotNull;
import org.dromara.common.core.validate.AddGroup;
/**
* 作废流程请求对象。
*
* @param id 流程实例 ID
* @param comment 作废意见
* @author may
*/
public record FlowInvalidBo(
@NotNull(message = "流程实例id为空", groups = AddGroup.class)
Long id,
String comment
) {
}
@@ -0,0 +1,44 @@
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 FlowNextNodeBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 任务id
*/
private Long taskId;
/**
* 流程变量
*/
private Map<String, Object> variables;
/**
* 获取流程变量并自动剔除空值。
*
* @return 流程变量
*/
public Map<String, Object> getVariables() {
if (variables == null) {
variables = new HashMap<>(16);
return variables;
}
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables;
}
}
@@ -0,0 +1,70 @@
package org.dromara.workflow.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.workflow.domain.FlowSpel;
import java.io.Serial;
import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
/**
* 流程spel表达式定义业务对象 flow_spel
*
* @author Michelle.Chung
* @date 2025-07-04
*/
@Data
@AutoMapper(target = FlowSpel.class, reverseConvertGenerate = false)
public class FlowSpelBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键id
*/
private Long id;
/**
* 组件名称
*/
private String componentName;
/**
* 方法名
*/
private String methodName;
/**
* 参数
*/
private String methodParams;
/**
* 预览spel值
*/
@NotBlank(message = "预览spel值不能为空", groups = {AddGroup.class, EditGroup.class})
private String viewSpel;
/**
* 状态(0正常 1停用)
*/
@NotBlank(message = "状态(0正常 1停用)不能为空", groups = {AddGroup.class, EditGroup.class})
private String status;
/**
* 备注
*/
private String remark;
/**
* 请求参数
*/
private Map<String, Object> params = new HashMap<>();
}
@@ -0,0 +1,69 @@
package org.dromara.workflow.domain.bo;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 任务请求对象
*
* @author may
*/
@Data
public class FlowTaskBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 任务名称
*/
private String nodeName;
/**
* 流程定义名称
*/
private String flowName;
/**
* 流程定义编码
*/
private String flowCode;
/**
* 流程分类id
*/
private String category;
/**
* 流程实例id
*/
private Long instanceId;
/**
* 流程状态
*/
private String flowStatus;
/**
* 权限列表
*/
private List<String> permissionList;
/**
* 申请人Ids
*/
private List<Long> createByIds;
/**
* 请求参数
*/
@JsonInclude(JsonInclude.Include.NON_EMPTY)
private Map<String, Object> params = new HashMap<>();
}
@@ -0,0 +1,18 @@
package org.dromara.workflow.domain.bo;
import jakarta.validation.constraints.NotNull;
import org.dromara.common.core.validate.AddGroup;
/**
* 终止任务请求对象
*
* @param taskId 任务 ID
* @param comment 终止意见
* @author may
*/
public record FlowTerminationBo(
@NotNull(message = "任务id为空", groups = AddGroup.class)
Long taskId,
String comment
) {
}
@@ -0,0 +1,38 @@
package org.dromara.workflow.domain.bo;
import jakarta.validation.constraints.NotNull;
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 FlowUrgeTaskBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 任务id
*/
@NotNull(message = "任务id为空", groups = AddGroup.class)
private List<Long> taskIdList;
/**
* 消息类型
*/
private List<String> messageType;
/**
* 催办内容
*/
@NotNull(message = "催办内容为空", groups = AddGroup.class)
private String message;
}
@@ -0,0 +1,22 @@
package org.dromara.workflow.domain.bo;
import jakarta.validation.constraints.NotNull;
import org.dromara.common.core.validate.AddGroup;
/**
* 流程变量参数
*
* @param instanceId 流程实例 ID
* @param key 变量键
* @param value 变量值
* @author may
*/
public record FlowVariableBo(
@NotNull(message = "流程实例id为空", groups = AddGroup.class)
Long instanceId,
@NotNull(message = "流程变量key为空", groups = AddGroup.class)
String key,
@NotNull(message = "流程变量value为空", groups = AddGroup.class)
String value
) {
}
@@ -0,0 +1,69 @@
package org.dromara.workflow.domain.bo;
import cn.hutool.core.util.ObjectUtil;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.workflow.domain.FlowInstanceBizExt;
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
*/
@NotBlank(message = "业务ID不能为空", groups = {AddGroup.class})
private String businessId;
/**
* 流程定义编码
*/
@NotBlank(message = "流程定义编码不能为空", groups = {AddGroup.class})
private String flowCode;
/**
* 办理人(可不填 用于覆盖当前节点办理人)
*/
private String handler;
/**
* 流程变量,前端会提交一个元素{'entity': {业务详情数据对象}}
*/
private Map<String, Object> variables;
/**
* 流程业务扩展信息
*/
private FlowInstanceBizExt bizExt;
public Map<String, Object> getVariables() {
if (variables == null) {
variables = new HashMap<>(16);
return variables;
}
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables;
}
public FlowInstanceBizExt getBizExt() {
if (ObjectUtil.isNull(bizExt)) {
bizExt = new FlowInstanceBizExt();
}
return bizExt;
}
}
@@ -0,0 +1,53 @@
package org.dromara.workflow.domain.bo;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
/**
* 任务操作业务对象,用于描述任务委派、转办、加签等操作的必要参数
* 包含了用户ID、任务ID、任务相关的消息、以及加签/减签的用户ID
*
* @author AprilWind
*/
@Data
public class TaskOperationBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 委派/转办人的用户ID(必填,准对委派/转办人操作)
*/
@NotNull(message = "委派/转办人id不能为空", groups = {AddGroup.class})
private String userId;
/**
* 加签/减签人的用户ID列表(必填,针对加签/减签操作)
*/
@NotNull(message = "加签/减签id不能为空", groups = {EditGroup.class})
private List<String> userIds;
/**
* 任务ID(必填)
*/
@NotNull(message = "任务id不能为空")
private Long taskId;
/**
* 消息类型
*/
private List<String> messageType;
/**
* 意见或备注信息(可选)
*/
private String message;
}
@@ -0,0 +1,100 @@
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 org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.workflow.domain.TestLeave;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
/**
* 请假业务对象 test_leave
*
* @author may
* @date 2023-07-21
*/
@Data
@AutoMapper(target = TestLeave.class, reverseConvertGenerate = false)
public class TestLeaveBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@NotNull(message = "主键不能为空", groups = {EditGroup.class})
private Long id;
/**
* 流程code
*/
private String flowCode;
/**
* 申请编号
*/
private String applyCode;
/**
* 请假类型
*/
@NotBlank(message = "请假类型不能为空", groups = {AddGroup.class, EditGroup.class})
private String leaveType;
/**
* 开始时间
*/
@NotNull(message = "开始时间不能为空", groups = {AddGroup.class, EditGroup.class})
@DateTimeFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDateTime startDate;
/**
* 结束时间
*/
@NotNull(message = "结束时间不能为空", groups = {AddGroup.class, EditGroup.class})
@DateTimeFormat(pattern = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd")
private LocalDateTime endDate;
/**
* 请假天数
*/
private Integer leaveDays;
/**
* 开始时间
*/
private Integer startLeaveDays;
/**
* 结束时间
*/
private Integer endLeaveDays;
/**
* 请假原因
*/
private String remark;
/**
* 状态
*/
private String status;
/**
* 请求参数
*/
private Map<String, Object> params = new HashMap<>();
}
@@ -0,0 +1,44 @@
package org.dromara.workflow.domain.context;
import lombok.Data;
import org.dromara.warm.flow.core.dto.FlowParams;
import org.dromara.warm.flow.core.entity.Instance;
import org.dromara.warm.flow.orm.entity.FlowTask;
import org.dromara.workflow.domain.bo.CompleteTaskBo;
/**
* 办理任务 LiteFlow 上下文。
* <p>
* 链路内各节点通过该对象传递任务、实例、办理参数等中间结果,避免组件之间重复查询。
*
* @author may
*/
@Data
public class CompleteTaskContext {
/**
* 办理任务入参。
*/
private final CompleteTaskBo completeTaskBo;
/**
* 当前待办理的运行任务,由校验节点加载。
*/
private FlowTask flowTask;
/**
* 当前任务所属流程实例,由校验节点加载。
*/
private Instance instance;
/**
* Warm-Flow 办理参数,由参数构建节点生成并供执行、自动审批节点复用。
*/
private FlowParams flowParams;
/**
* 流程实例是否开启自动审批,由参数构建节点从实例变量中解析。
*/
private Boolean autoPass;
}
@@ -0,0 +1,59 @@
package org.dromara.workflow.domain.context;
import lombok.Data;
import org.dromara.warm.flow.orm.entity.FlowInstance;
import java.util.Collection;
import java.util.List;
/**
* 删除流程实例 LiteFlow 上下文。
*
* @author may
*/
@Data
public class InstanceDeleteContext {
/**
* 按业务 id 删除时的业务 id 集合。
*/
private final List<String> businessIds;
/**
* 按实例 id 删除时的实例 id 集合。
*/
private final Collection<Long> instanceIds;
/**
* 是否删除历史实例及其历史任务数据。
*/
private final boolean history;
/**
* 待删除流程实例。
*/
private List<FlowInstance> flowInstances;
/**
* 实际执行删除的实例 id。
*/
private List<Long> deleteInstanceIds;
/**
* 删除结果。
*/
private boolean result;
public static InstanceDeleteContext byBusinessIds(List<String> businessIds) {
return new InstanceDeleteContext(businessIds, null, false);
}
public static InstanceDeleteContext byInstanceIds(Collection<Long> instanceIds) {
return new InstanceDeleteContext(null, instanceIds, false);
}
public static InstanceDeleteContext byHistoryInstanceIds(Collection<Long> instanceIds) {
return new InstanceDeleteContext(null, instanceIds, true);
}
}
@@ -0,0 +1,70 @@
package org.dromara.workflow.domain.context;
import lombok.Data;
import org.dromara.warm.flow.core.entity.Definition;
import org.dromara.warm.flow.core.entity.Instance;
import org.dromara.warm.flow.core.entity.Task;
import org.dromara.warm.flow.orm.entity.FlowInstance;
import org.dromara.workflow.api.domain.StartProcessReturnDTO;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import org.dromara.workflow.domain.bo.StartProcessBo;
import java.util.List;
import java.util.Map;
/**
* 启动流程 LiteFlow 上下文。
* <p>
* 链路内保存启动参数、流程定义、流程实例和首个任务等中间结果。
*
* @author may
*/
@Data
public class StartProcessContext {
/**
* 启动流程入参。
*/
private final StartProcessBo startProcessBo;
/**
* 业务唯一标识。
*/
private String businessId;
/**
* 流程变量。
*/
private Map<String, Object> variables;
/**
* 流程实例业务扩展信息。
*/
private FlowInstanceBizExt bizExt;
/**
* 已存在的流程实例,非空时走续提交逻辑。
*/
private FlowInstance existingInstance;
/**
* 已发布的流程定义。
*/
private Definition definition;
/**
* 新启动的流程实例。
*/
private Instance instance;
/**
* 当前流程实例下生成的任务列表。
*/
private List<Task> taskList;
/**
* 启动流程返回结果。
*/
private StartProcessReturnDTO startProcessReturn;
}
@@ -0,0 +1,55 @@
package org.dromara.workflow.domain.context;
import lombok.Data;
import org.dromara.warm.flow.core.dto.FlowParams;
import org.dromara.warm.flow.core.entity.Task;
import org.dromara.warm.flow.orm.entity.FlowNode;
import org.dromara.workflow.common.enums.TaskOperationEnum;
import org.dromara.workflow.domain.bo.TaskOperationBo;
/**
* 任务操作 LiteFlow 上下文。
* <p>
* 用于在委派、转办、加签、减签链路中传递操作类型、任务、节点和办理参数。
*
* @author may
*/
@Data
public class TaskOperationContext {
/**
* 任务操作入参。
*/
private final TaskOperationBo taskOperationBo;
/**
* 操作类型编码。
*/
private final String taskOperation;
/**
* 解析后的操作枚举。
*/
private TaskOperationEnum operation;
/**
* Warm-Flow 操作参数。
*/
private FlowParams flowParams;
/**
* 当前任务。
*/
private Task task;
/**
* 当前任务对应的流程节点。
*/
private FlowNode flowNode;
/**
* 操作执行结果。
*/
private boolean result;
}
@@ -0,0 +1,52 @@
package org.dromara.workflow.domain.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 按钮权限视图对象。
*
* @author may
* @date 2025-02-28
*/
@Data
public class ButtonPermissionVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 唯一编码
*/
private String code;
/**
* 选项值
*/
private String value;
/**
* 是否显示
*/
private Boolean show;
/**
* 无参构造方法。
*/
public ButtonPermissionVo() {
}
/**
* 使用按钮编码和显示状态构造按钮权限对象。
*
* @param code 按钮编码
* @param show 是否显示
*/
public ButtonPermissionVo(String code, Boolean show) {
this.code = code;
this.show = show;
}
}
@@ -0,0 +1,69 @@
package org.dromara.workflow.domain.vo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import org.dromara.common.translation.annotation.Translation;
import org.dromara.workflow.common.constant.FlowConstant;
import org.dromara.workflow.domain.FlowCategory;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 流程分类视图对象 wf_category
*
* @author may
* @date 2023-06-27
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = FlowCategory.class)
public class FlowCategoryVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 流程分类ID
*/
@ExcelProperty(value = "流程分类ID")
private Long categoryId;
/**
* 父级分类id
*/
private Long parentId;
/**
* 父级分类名称
*/
@Translation(type = FlowConstant.CATEGORY_ID_TO_NAME, mapper = "parentId")
private String parentName;
/**
* 祖级列表
*/
private String ancestors;
/**
* 流程分类名称
*/
@ExcelProperty(value = "流程分类名称")
private String categoryName;
/**
* 显示顺序
*/
@ExcelProperty(value = "显示顺序")
private Long orderNum;
/**
* 创建时间
*/
@ExcelProperty(value = "创建时间")
private LocalDateTime createTime;
}
@@ -0,0 +1,41 @@
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.io.Serial;
import java.io.Serializable;
/**
* 流程抄送视图对象。
*
* @author AprilWind
*/
@Data
public class FlowCopyVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 用户id
*/
private Long userId;
/**
* 用户昵称
*/
@Translation(type = TransConstant.USER_ID_TO_NICKNAME, mapper = "userId")
private String nickName;
/**
* 使用用户 ID 构造抄送对象。
*
* @param userId 用户ID
*/
public FlowCopyVo(Long userId) {
this.userId = userId;
}
}
@@ -0,0 +1,102 @@
package org.dromara.workflow.domain.vo;
import lombok.Data;
import org.dromara.common.translation.annotation.Translation;
import org.dromara.workflow.common.constant.FlowConstant;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 流程定义视图
*
* @author may
*/
@Data
public class FlowDefinitionVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 流程定义主键
*/
private Long id;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 删除标记
*/
private String delFlag;
/**
* 流程定义编码
*/
private String flowCode;
/**
* 流程定义名称
*/
private String flowName;
/**
* 流程分类id
*/
private String category;
/**
* 流程分类名称
*/
@Translation(type = FlowConstant.CATEGORY_ID_TO_NAME, mapper = "category")
private String categoryName;
/**
* 流程版本
*/
private String version;
/**
* 是否发布(0未发布 1已发布 9失效)
*/
private Integer isPublish;
/**
* 审批表单是否自定义(Y是 N否)
*/
private String formCustom;
/**
* 审批表单路径
*/
private String formPath;
/**
* 流程激活状态(0挂起 1激活)
*/
private Integer activityStatus;
/**
* 监听器类型
*/
private String listenerType;
/**
* 监听器路径
*/
private String listenerPath;
/**
* 扩展字段,预留给业务系统使用
*/
private String ext;
}
@@ -0,0 +1,254 @@
package org.dromara.workflow.domain.vo;
import lombok.Data;
import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.translation.annotation.Translation;
import org.dromara.common.translation.constant.TransConstant;
import org.dromara.warm.flow.core.enums.CooperateType;
import org.dromara.workflow.common.constant.FlowConstant;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
/**
* 历史任务视图
*
* @author may
*/
@Data
public class FlowHisTaskVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 历史任务主键
*/
private Long id;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 删除标记
*/
private String delFlag;
/**
* 对应flow_definition表的id
*/
private Long definitionId;
/**
* 流程定义名称
*/
private String flowName;
/**
* 流程实例表id
*/
private Long instanceId;
/**
* 任务表id
*/
private Long taskId;
/**
* 协作方式(1审批 2转办 3委派 4会签 5票签 6加签 7减签)
*/
private Integer cooperateType;
/**
* 协作方式(1审批 2转办 3委派 4会签 5票签 6加签 7减签)
*/
private String cooperateTypeName;
/**
* 业务id
*/
private String businessId;
/**
* 开始节点编码
*/
private String nodeCode;
/**
* 开始节点名称
*/
private String nodeName;
/**
* 开始节点类型(0开始节点 1中间节点 2结束节点 3互斥网关 4并行网关)
*/
private Integer nodeType;
/**
* 目标节点编码
*/
private String targetNodeCode;
/**
* 结束节点名称
*/
private String targetNodeName;
/**
* 审批者
*/
private String approver;
/**
* 审批者
*/
@Translation(type = TransConstant.USER_ID_TO_NICKNAME, mapper = "approver")
private String approverName;
/**
* 协作人(只有转办、会签、票签、委派)
*/
private String collaborator;
/**
* 权限标识 permissionFlag的list形式
*/
private List<String> permissionList;
/**
* 跳转类型(PASS通过 REJECT退回 NONE无动作)
*/
private String skipType;
/**
* 流程状态
*/
private String flowStatus;
/**
* 任务状态
*/
private String flowTaskStatus;
/**
* 流程状态
*/
private String flowStatusName;
/**
* 审批意见
*/
private String message;
/**
* 业务详情 存业务类的json
*/
private String ext;
/**
* 创建者
*/
private String createBy;
/**
* 申请人
*/
@Translation(type = TransConstant.USER_ID_TO_NICKNAME, mapper = "createBy")
private String createByName;
/**
* 流程分类id
*/
private String category;
/**
* 流程分类名称
*/
@Translation(type = FlowConstant.CATEGORY_ID_TO_NAME, mapper = "category")
private String categoryName;
/**
* 审批表单是否自定义(Y是 N否)
*/
private String formCustom;
/**
* 审批表单路径
*/
private String formPath;
/**
* 流程定义编码
*/
private String flowCode;
/**
* 流程版本号
*/
private String version;
/**
* 运行时长
*/
private String runDuration;
//业务扩展信息开始
/**
* 业务编码
*/
private String businessCode;
/**
* 业务标题
*/
private String businessTitle;
//业务扩展信息结束
/**
* 设置创建时间并计算任务运行时长
*
* @param createTime 创建时间
*/
public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
updateRunDuration();
}
/**
* 设置更新时间并计算任务运行时长
*
* @param updateTime 更新时间
*/
public void setUpdateTime(LocalDateTime updateTime) {
this.updateTime = updateTime;
updateRunDuration();
}
/**
* 更新运行时长
*/
private void updateRunDuration() {
// 如果创建时间和更新时间均不为空,计算它们之间的时长
if (this.updateTime != null && this.createTime != null) {
this.runDuration = DateUtils.formatBetweenBySecond(this.updateTime, this.createTime);
}
}
/**
* 设置协作方式,并通过协作方式获取名称
*/
public void setCooperateType(Integer cooperateType) {
this.cooperateType = cooperateType;
this.cooperateTypeName = CooperateType.getValueByKey(cooperateType);
}
}
@@ -0,0 +1,147 @@
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.dromara.workflow.common.constant.FlowConstant;
import java.time.LocalDateTime;
/**
* 流程实例视图对象。
*
* @author may
*/
@Data
public class FlowInstanceVo {
/**
* 流程实例ID。
*/
private Long id;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 删除标记
*/
private String delFlag;
/**
* 对应flow_definition表的id
*/
private Long definitionId;
/**
* 流程定义名称
*/
private String flowName;
/**
* 流程定义编码
*/
private String flowCode;
/**
* 业务id
*/
private String businessId;
/**
* 节点类型(0开始节点 1中间节点 2结束节点 3互斥网关 4并行网关)
*/
private Integer nodeType;
/**
* 流程节点编码 每个流程的nodeCode是唯一的,即definitionId+nodeCode唯一,在数据库层面做了控制
*/
private String nodeCode;
/**
* 流程节点名称
*/
private String nodeName;
/**
* 流程变量
*/
private String variable;
/**
* 流程状态
*/
private String flowStatus;
/**
* 流程状态
*/
private String flowStatusName;
/**
* 流程激活状态(0挂起 1激活)
*/
private Integer activityStatus;
/**
* 审批表单是否自定义(Y是 N否)
*/
private String formCustom;
/**
* 审批表单路径
*/
private String formPath;
/**
* 扩展字段,预留给业务系统使用
*/
private String ext;
/**
* 流程定义版本
*/
private String version;
/**
* 创建者
*/
private String createBy;
/**
* 申请人
*/
@Translation(type = TransConstant.USER_ID_TO_NICKNAME, mapper = "createBy")
private String createByName;
/**
* 流程分类id
*/
private String category;
/**
* 流程分类名称
*/
@Translation(type = FlowConstant.CATEGORY_ID_TO_NAME, mapper = "category")
private String categoryName;
//业务扩展信息开始
/**
* 业务编码
*/
private String businessCode;
/**
* 业务标题
*/
private String businessTitle;
//业务扩展信息结束
}
@@ -0,0 +1,79 @@
package org.dromara.workflow.domain.vo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.convert.ExcelDictConvert;
import org.dromara.workflow.domain.FlowSpel;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 流程spel表达式定义视图对象 flow_spel
*
* @author Michelle.Chung
* @date 2025-07-04
*/
@Data
@ExcelIgnoreUnannotated
@AutoMapper(target = FlowSpel.class)
public class FlowSpelVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键id
*/
@ExcelProperty(value = "主键id")
private Long id;
/**
* 组件名称
*/
@ExcelProperty(value = "组件名称")
private String componentName;
/**
* 方法名
*/
@ExcelProperty(value = "方法名")
private String methodName;
/**
* 参数
*/
@ExcelProperty(value = "参数")
private String methodParams;
/**
* 预览spel值
*/
@ExcelProperty(value = "预览spel值")
private String viewSpel;
/**
* 状态(0正常 1停用)
*/
@ExcelProperty(value = "状态", converter = ExcelDictConvert.class)
@ExcelDictFormat(readConverterExp = "0=正常,1=停用")
private String status;
/**
* 备注
*/
@ExcelProperty(value = "备注")
private String remark;
/**
* 创建时间
*/
@ExcelProperty(value = "创建时间")
private LocalDateTime createTime;
}
@@ -0,0 +1,212 @@
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.dromara.warm.flow.core.entity.User;
import org.dromara.workflow.common.constant.FlowConstant;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* 流程任务视图对象。
*
* @author may
*/
@Data
public class FlowTaskVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 任务ID。
*/
private Long id;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 删除标记
*/
private String delFlag;
/**
* 对应flow_definition表的id
*/
private Long definitionId;
/**
* 流程实例表id
*/
private Long instanceId;
/**
* 流程定义名称
*/
private String flowName;
/**
* 业务id
*/
private String businessId;
/**
* 节点编码
*/
private String nodeCode;
/**
* 节点名称
*/
private String nodeName;
/**
* 节点类型(0开始节点 1中间节点 2结束节点 3互斥网关 4并行网关)
*/
private Integer nodeType;
/**
* 权限标识 permissionFlag的list形式
*/
private List<String> permissionList;
/**
* 流程用户列表
*/
private List<User> userList;
/**
* 审批表单是否自定义(Y是 N否)
*/
private String formCustom;
/**
* 审批表单
*/
private String formPath;
/**
* 流程定义编码
*/
private String flowCode;
/**
* 流程版本号
*/
private String version;
/**
* 流程状态
*/
private String flowStatus;
/**
* 流程分类id
*/
private String category;
/**
* 流程分类名称
*/
@Translation(type = FlowConstant.CATEGORY_ID_TO_NAME, mapper = "category")
private String categoryName;
/**
* 流程状态
*/
@Translation(type = TransConstant.DICT_TYPE_TO_LABEL, mapper = "flowStatus", other = "wf_business_status")
private String flowStatusName;
/**
* 办理人类型
*/
private String type;
/**
* 办理人ids
*/
private String assigneeIds;
/**
* 办理人名称
*/
@Translation(type = TransConstant.USER_ID_TO_NICKNAME, mapper = "assigneeIds")
private String assigneeNames;
/**
* 抄送人id
*/
private String processedBy;
/**
* 抄送人名称
*/
@Translation(type = TransConstant.USER_ID_TO_NICKNAME, mapper = "processedBy")
private String processedByName;
/**
* 流程签署比例值 大于0为票签,会签
*/
private String nodeRatio;
/**
* 申请人id
*/
private String createBy;
/**
* 申请人名称
*/
@Translation(type = TransConstant.USER_ID_TO_NICKNAME, mapper = "createBy")
private String createByName;
/**
* 是否为申请人节点
*/
private Boolean applyNode;
/**
* 按钮权限
*/
private List<ButtonPermissionVo> buttonList;
/**
* 抄送对象 ID 集合
* <p>
* 根据扩展属性中 CopySettingEnum 类型的数据生成,存储需要抄送的对象 ID
*/
private List<FlowCopyVo> copyList;
/**
* 自定义参数 Map
* <p>
* 根据扩展属性中 VariablesEnum 类型的数据生成,存储 key=value 格式的自定义参数
*/
private Map<String, String> varList;
//业务扩展信息开始
/**
* 业务编码
*/
private String businessCode;
/**
* 业务标题
*/
private String businessTitle;
//业务扩展信息结束
}
@@ -0,0 +1,45 @@
package org.dromara.workflow.domain.vo;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Node 扩展属性解析结果 VO
* <p>
* 用于封装从扩展属性 JSON 中解析出的各类信息,包括按钮权限、抄送对象和自定义参数。
*
* @author AprilWind
*/
@Data
public class NodeExtVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 按钮权限列表
* <p>
* 根据扩展属性中 ButtonPermissionEnum 类型的数据生成,每个元素表示一个按钮及其是否勾选。
*/
private List<ButtonPermissionVo> buttonPermissions;
/**
* 抄送对象 ID 集合
* <p>
* 根据扩展属性中 CopySettingEnum 类型的数据生成,存储需要抄送的对象 ID
*/
private Set<String> copySettings;
/**
* 自定义参数 Map
* <p>
* 根据扩展属性中 VariablesEnum 类型的数据生成,存储 key=value 格式的自定义参数
*/
private Map<String, String> variables;
}
@@ -0,0 +1,76 @@
package org.dromara.workflow.domain.vo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.apache.fesod.sheet.annotation.ExcelIgnoreUnannotated;
import org.apache.fesod.sheet.annotation.ExcelProperty;
import org.dromara.workflow.domain.TestLeave;
import java.io.Serial;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 请假视图对象 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 applyCode;
/**
* 请假类型
*/
@ExcelProperty(value = "请假类型")
private String leaveType;
/**
* 开始时间
*/
@ExcelProperty(value = "开始时间")
private LocalDateTime startDate;
/**
* 结束时间
*/
@ExcelProperty(value = "结束时间")
private LocalDateTime endDate;
/**
* 请假天数
*/
@ExcelProperty(value = "请假天数")
private Integer leaveDays;
/**
* 备注
*/
@ExcelProperty(value = "请假原因")
private String remark;
/**
* 状态
*/
@ExcelProperty(value = "状态")
private String status;
}
@@ -0,0 +1,23 @@
package org.dromara.workflow.event;
import cn.hutool.core.collection.CollUtil;
import org.dromara.warm.flow.core.entity.Task;
import org.dromara.workflow.domain.bo.FlowCopyBo;
import java.util.List;
/**
* 工作流抄送事件。
*
* @param task 当前任务
* @param flowCopyList 抄送人列表
*/
public record WorkflowCopyEvent(Task task, List<FlowCopyBo> flowCopyList) {
public WorkflowCopyEvent {
if (CollUtil.isNotEmpty(flowCopyList)) {
flowCopyList = List.copyOf(flowCopyList);
}
}
}
@@ -0,0 +1,23 @@
package org.dromara.workflow.event;
import cn.hutool.core.collection.CollUtil;
import java.util.List;
/**
* 工作流结果消息事件。
*
* @param flowName 流程名称
* @param status 流程状态
* @param createBy 发起人用户 ID
* @param messageType 消息类型
*/
public record WorkflowResultMessageEvent(String flowName, String status, String createBy, List<String> messageType) {
public WorkflowResultMessageEvent {
if (CollUtil.isNotEmpty(messageType)) {
messageType = List.copyOf(messageType);
}
}
}
@@ -0,0 +1,23 @@
package org.dromara.workflow.event;
import cn.hutool.core.collection.CollUtil;
import java.util.List;
/**
* 工作流待办消息事件。
*
* @param flowName 流程名称
* @param instanceId 流程实例 ID
* @param messageType 消息类型
* @param notice 通知内容
*/
public record WorkflowTaskMessageEvent(String flowName, Long instanceId, List<String> messageType, String notice) {
public WorkflowTaskMessageEvent {
if (CollUtil.isNotEmpty(messageType)) {
messageType = List.copyOf(messageType);
}
}
}
@@ -0,0 +1,33 @@
package org.dromara.workflow.handler;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.R;
import org.dromara.warm.flow.core.exception.FlowException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* 工作流失败异常
*
* @author Lion Li
*/
@Slf4j
@RestControllerAdvice
public class FlowExceptionHandler {
/**
* 工作流失败异常。
*
* @param e 异常信息
* @param request 当前请求
* @return 统一失败响应
*/
@ExceptionHandler(FlowException.class)
public R<Void> handleFlowException(FlowException e, HttpServletRequest request) {
String requestURI = request.getRequestURI();
log.error("请求地址'{}'", requestURI, e);
return R.fail(e.getMessage());
}
}
@@ -0,0 +1,89 @@
package org.dromara.workflow.handler;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.warm.flow.core.entity.Instance;
import org.dromara.warm.flow.core.entity.Task;
import org.dromara.workflow.api.event.ProcessDeleteEvent;
import org.dromara.workflow.api.event.ProcessEvent;
import org.dromara.workflow.api.event.ProcessTaskEvent;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.springframework.stereotype.Component;
import java.util.Map;
/**
* 流程监听服务
*
* @author may
* @date 2024-06-02
*/
@ConditionalOnEnable
@Slf4j
@Component
public class FlowProcessEventHandler {
/**
* 总体流程监听(例如: 草稿,撤销,退回,作废,终止,已完成等)
*
* @param flowCode 流程定义编码
* @param instance 实例数据
* @param status 流程状态
* @param params 办理参数
* @param submit 当为true时为申请人节点办理
*/
public void processHandler(String flowCode, Instance instance, String status, Map<String, Object> params, boolean submit) {
log.info("【流程事件发布】流程编码: {}, 业务ID: {}, 流程状态: {}, 节点类型: {}, 节点编码: {}, 节点名称: {}, 是否申请人节点: {}, 参数: {}",
flowCode, instance.getBusinessId(), status, instance.getNodeType(), instance.getNodeCode(), instance.getNodeName(), submit, params);
ProcessEvent processEvent = new ProcessEvent();
processEvent.setFlowCode(flowCode);
processEvent.setInstanceId(instance.getId());
processEvent.setBusinessId(instance.getBusinessId());
processEvent.setNodeType(instance.getNodeType());
processEvent.setNodeCode(instance.getNodeCode());
processEvent.setNodeName(instance.getNodeName());
processEvent.setStatus(status);
processEvent.setParams(params);
processEvent.setSubmit(submit);
SpringUtils.context().publishEvent(processEvent);
}
/**
* 执行创建任务监听
*
* @param flowCode 流程定义编码
* @param instance 实例数据
* @param nextTask 任务
* @param params 上一个任务的办理参数
*/
public void processTaskHandler(String flowCode, Instance instance, Task nextTask, Map<String, Object> params) {
log.info("【流程任务事件发布】流程编码: {}, 业务ID: {}, 节点类型: {}, 节点编码: {}, 节点名称: {}, 任务ID: {}",
flowCode, instance.getBusinessId(), nextTask.getNodeType(), nextTask.getNodeCode(), nextTask.getNodeName(), nextTask.getId());
ProcessTaskEvent processTaskEvent = new ProcessTaskEvent();
processTaskEvent.setFlowCode(flowCode);
processTaskEvent.setInstanceId(instance.getId());
processTaskEvent.setBusinessId(instance.getBusinessId());
processTaskEvent.setNodeType(nextTask.getNodeType());
processTaskEvent.setNodeCode(nextTask.getNodeCode());
processTaskEvent.setNodeName(nextTask.getNodeName());
processTaskEvent.setTaskId(nextTask.getId());
processTaskEvent.setStatus(instance.getFlowStatus());
processTaskEvent.setParams(params);
SpringUtils.context().publishEvent(processTaskEvent);
}
/**
* 删除流程监听
*
* @param flowCode 流程定义编码
* @param businessId 业务ID
*/
public void processDeleteHandler(String flowCode, String businessId) {
log.info("【流程删除事件发布】流程编码: {}, 业务ID: {}", flowCode, businessId);
ProcessDeleteEvent processDeleteEvent = new ProcessDeleteEvent();
processDeleteEvent.setFlowCode(flowCode);
processDeleteEvent.setBusinessId(businessId);
SpringUtils.context().publishEvent(processDeleteEvent);
}
}
@@ -0,0 +1,70 @@
package org.dromara.workflow.handler;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.api.domain.UserDTO;
import org.dromara.warm.flow.core.dto.FlowParams;
import org.dromara.warm.flow.core.handler.PermissionHandler;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.service.IFlwTaskAssigneeService;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
/**
* 办理人权限处理器
*
* @author AprilWind
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@Component
@Slf4j
public class WorkflowPermissionHandler implements PermissionHandler {
private final IFlwTaskAssigneeService flwTaskAssigneeService;
/**
* 办理人权限标识,比如用户,角色,部门等,用于校验是否有权限办理任务
* 后续在{@link FlowParams#getPermissionFlag} 中获取
* 返回当前用户权限集合
*
* @return 当前用户权限集合
*/
@Override
public List<String> permissions() {
return Collections.singletonList(LoginHelper.getUserIdStr());
}
/**
* 获取当前办理人
*
* @return 当前办理人
*/
@Override
public String getHandler() {
return LoginHelper.getUserIdStr();
}
/**
* 将预设办理人转换为实际用户 ID 列表。
*
* @param permissions 预设权限标识列表
* @return 用户 ID 列表
*/
@Override
public List<String> convertPermissions(List<String> permissions) {
if (CollUtil.isEmpty(permissions)) {
return permissions;
}
String storageIds = CollUtil.join(permissions, StringUtils.SEPARATOR);
List<UserDTO> users = flwTaskAssigneeService.fetchUsersByStorageIds(storageIds);
return StreamUtils.toList(users, userDTO -> Convert.toStr(userDTO.getUserId()));
}
}
@@ -0,0 +1,328 @@
package org.dromara.workflow.listener;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.system.api.UserService;
import org.dromara.warm.flow.core.FlowEngine;
import org.dromara.warm.flow.core.dto.FlowParams;
import org.dromara.warm.flow.core.entity.Definition;
import org.dromara.warm.flow.core.entity.Instance;
import org.dromara.warm.flow.core.entity.Task;
import org.dromara.warm.flow.core.listener.GlobalListener;
import org.dromara.warm.flow.core.listener.ListenerVariable;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.constant.FlowConstant;
import org.dromara.workflow.common.enums.TaskStatusEnum;
import org.dromara.workflow.domain.bo.FlowCopyBo;
import org.dromara.workflow.domain.vo.NodeExtVo;
import org.dromara.workflow.event.WorkflowCopyEvent;
import org.dromara.workflow.event.WorkflowResultMessageEvent;
import org.dromara.workflow.event.WorkflowTaskMessageEvent;
import org.dromara.workflow.handler.FlowProcessEventHandler;
import org.dromara.workflow.service.IFlwCommonService;
import org.dromara.workflow.service.IFlwInstanceService;
import org.dromara.workflow.service.IFlwNodeExtService;
import org.dromara.workflow.service.IFlwTaskService;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 工作流全局监听器,处理任务流转中的扩展变量、消息和事件发布。
*
* @author may
*/
@ConditionalOnEnable
@Component
@Slf4j
@RequiredArgsConstructor
public class WorkflowGlobalListener implements GlobalListener {
private final IFlwTaskService flwTaskService;
private final IFlwInstanceService flwInstanceService;
private final FlowProcessEventHandler flowProcessEventHandler;
private final IFlwCommonService flwCommonService;
private final IFlwNodeExtService nodeExtService;
private final UserService userService;
/**
* 任务创建回调,当前预留扩展。
*
* @param listenerVariable 监听器变量
*/
@Override
public void create(ListenerVariable listenerVariable) {
}
/**
* 任务开始办理时解析节点扩展配置。
*
* @param listenerVariable 监听器变量
*/
@Override
public void start(ListenerVariable listenerVariable) {
String ext = listenerVariable.getNode().getExt();
if (StringUtils.isNotBlank(ext)) {
Map<String, Object> variable = listenerVariable.getVariable();
if (CollUtil.isEmpty(variable)) {
variable = new HashMap<>();
}
NodeExtVo nodeExt = nodeExtService.parseNodeExt(ext, variable);
Set<String> copyList = nodeExt.getCopySettings();
if (CollUtil.isNotEmpty(copyList)) {
List<Long> userIds = StreamUtils.toList(copyList, Convert::toLong);
Map<Long, String> nickNameMap = userService.selectUserNicksByIds(userIds);
List<FlowCopyBo> list = StreamUtils.toList(copyList, x -> {
FlowCopyBo bo = new FlowCopyBo();
Long id = Convert.toLong(x);
bo.setUserId(id);
bo.setNickName(nickNameMap.getOrDefault(id, StringUtils.EMPTY));
return bo;
});
variable.put(FlowConstant.FLOW_COPY_LIST, list);
}
if (CollUtil.isNotEmpty(nodeExt.getVariables())) {
variable.putAll(nodeExt.getVariables());
}
}
}
/**
* 任务分派时动态调整办理权限。
*
* @param listenerVariable 监听器变量
*/
@Override
public void assignment(ListenerVariable listenerVariable) {
Map<String, Object> variable = listenerVariable.getVariable();
List<Task> nextTasks = listenerVariable.getNextTasks();
FlowParams flowParams = listenerVariable.getFlowParams();
Definition definition = listenerVariable.getDefinition();
Instance instance = listenerVariable.getInstance();
String applyNodeCode = flwCommonService.applyNodeCode(definition.getId());
String hisStatus = flowParams != null ? flowParams.getHisStatus() : null;
for (Task flowTask : nextTasks) {
String nodeCode = flowTask.getNodeCode();
// 处理办理或退回时指定办理人的情况
if (TaskStatusEnum.PASS.getStatus().equals(hisStatus)) {
processTaskPermission(variable, flowTask, hisStatus);
} else if (TaskStatusEnum.BACK.getStatus().equals(hisStatus)) {
processTaskPermission(variable, flowTask, hisStatus);
}
// 如果是申请节点,则把启动人添加到办理人
if (nodeCode.equals(applyNodeCode) && StringUtils.isNotBlank(instance.getCreateBy())) {
flowTask.setPermissionList(List.of(instance.getCreateBy()));
}
}
}
/**
* 处理任务权限设置
*
* @param variable 变量集合
* @param flowTask 流程任务
* @param taskStatus 任务状态
*/
private void processTaskPermission(Map<String, Object> variable, Task flowTask, String taskStatus) {
String nodeKey = taskStatus + StringUtils.COLON + flowTask.getNodeCode();
// 检查是否存在状态相关的变量
if (!variable.containsKey(nodeKey)) {
return;
}
// 获取用户ID字符串
Object userIdsObj = variable.get(nodeKey);
if (userIdsObj == null) {
return;
}
String userIds = userIdsObj.toString();
if (StringUtils.isBlank(userIds)) {
return;
}
// 分割用户ID并设置权限列表
List<String> userIdList = StringUtils.str2List(userIds, StringUtils.SEPARATOR, true, true);
if (CollUtil.isNotEmpty(userIdList)) {
flowTask.setPermissionList(userIdList);
if (TaskStatusEnum.PASS.getStatus().equals(taskStatus)) {
// 办理指定人变量只消费一次;驳回指定人变量需要保留给后续重复驳回。
variable.remove(nodeKey);
FlowEngine.insService().removeVariables(flowTask.getInstanceId(), nodeKey);
}
}
}
/**
* 任务完成后发布流程事件并处理抄送和通知。
*
* @param listenerVariable 监听器变量
*/
@Override
public void finish(ListenerVariable listenerVariable) {
Instance instance = listenerVariable.getInstance();
Definition definition = listenerVariable.getDefinition();
Task task = listenerVariable.getTask();
List<Task> nextTasks = listenerVariable.getNextTasks();
Map<String, Object> params = new HashMap<>();
FlowParams flowParams = listenerVariable.getFlowParams();
Map<String, Object> variable = new HashMap<>();
if (ObjectUtil.isNotNull(flowParams)) {
// 历史任务扩展(通常为附件)
params.put("hisTaskExt", flowParams.getHisTaskExt());
// 办理人
params.put("handler", flowParams.getHandler());
// 办理意见
params.put("message", flowParams.getMessage());
variable = flowParams.getVariable();
}
//申请人提交事件
Boolean submit = MapUtil.getBool(variable, FlowConstant.SUBMIT);
if (submit != null && submit) {
String status = determineFlowStatus(instance);
flowProcessEventHandler.processHandler(definition.getFlowCode(), instance, status, variable, true);
} else {
// 判断流程状态(发布:撤销,退回,作废,终止,已完成事件)
String status = determineFlowStatus(instance);
if (StringUtils.isNotBlank(status)) {
flowProcessEventHandler.processHandler(definition.getFlowCode(), instance, status, params, false);
notifyInitiatorIfNeeded(definition, instance, status, variable);
}
if (!BusinessStatusEnum.initialState(instance.getFlowStatus())) {
if (task != null && CollUtil.isNotEmpty(nextTasks) && nextTasks.size() == 1
&& flwCommonService.applyNodeCode(definition.getId()).equals(nextTasks.getFirst().getNodeCode())) {
// 如果为画线指定驳回 线条指定为驳回 驳回得节点为申请人节点 则修改流程状态为退回
flowProcessEventHandler.processHandler(definition.getFlowCode(), instance, BusinessStatusEnum.BACK.getStatus(), params, false);
notifyInitiatorIfNeeded(definition, instance, BusinessStatusEnum.BACK.getStatus(), variable);
// 修改流程实例状态
instance.setFlowStatus(BusinessStatusEnum.BACK.getStatus());
FlowEngine.insService().updateById(instance);
}
}
}
//发布任务事件
if (CollUtil.isNotEmpty(nextTasks)) {
for (Task nextTask : nextTasks) {
flowProcessEventHandler.processTaskHandler(definition.getFlowCode(), instance, nextTask, params);
}
}
if (ObjectUtil.isNull(flowParams)) {
return;
}
// 只有办理或者退回的时候才执行消息通知和抄送
if (!TaskStatusEnum.isPassOrBack(flowParams.getHisStatus())) {
return;
}
if (ObjectUtil.isNull(variable)) {
return;
}
if (variable.containsKey(FlowConstant.FLOW_COPY_LIST)) {
List<FlowCopyBo> flowCopyList = MapUtil.get(variable, FlowConstant.FLOW_COPY_LIST, new TypeReference<>() {
});
// 添加抄送人
SpringUtils.context().publishEvent(new WorkflowCopyEvent(task, flowCopyList));
}
if (variable.containsKey(FlowConstant.MESSAGE_TYPE)) {
List<String> messageType = MapUtil.get(variable, FlowConstant.MESSAGE_TYPE, new TypeReference<>() {
});
String notice = MapUtil.getStr(variable, FlowConstant.MESSAGE_NOTICE);
// 退回到申请人时只保留“已退回”结果消息,避免再追加一条“新的待办”形成重复提醒。
if (shouldSendTaskMessage(flowParams, definition, nextTasks)) {
SpringUtils.context().publishEvent(new WorkflowTaskMessageEvent(definition.getFlowName(), instance.getId(), messageType, notice));
}
}
FlowEngine.insService().removeVariables(instance.getId(),
FlowConstant.FLOW_COPY_LIST,
FlowConstant.MESSAGE_TYPE,
FlowConstant.MESSAGE_NOTICE,
FlowConstant.SUBMIT
);
}
/**
* 判断是否需要发送后续待办消息。
*
* @param flowParams 流程参数
* @param definition 流程定义
* @param nextTasks 后续任务列表
* @return 是否发送待办消息
*/
private boolean shouldSendTaskMessage(FlowParams flowParams, Definition definition, List<Task> nextTasks) {
if (flowParams == null || !TaskStatusEnum.BACK.getStatus().equals(flowParams.getHisStatus())) {
return true;
}
if (CollUtil.isEmpty(nextTasks) || nextTasks.size() != 1) {
return true;
}
// 只有“退回到申请人”场景需要拦截待办提醒,其余退回/流转仍然保留待办消息。
String applyNodeCode = flwCommonService.applyNodeCode(definition.getId());
return !StringUtils.equals(applyNodeCode, nextTasks.getFirst().getNodeCode());
}
/**
* 在流程完成或退回时通知发起人。
*
* @param definition 流程定义
* @param instance 流程实例
* @param status 业务状态
* @param variable 流程变量
*/
private void notifyInitiatorIfNeeded(Definition definition, Instance instance, String status, Map<String, Object> variable) {
if (!StringUtils.equalsAny(status, BusinessStatusEnum.FINISH.getStatus(), BusinessStatusEnum.BACK.getStatus())) {
return;
}
if (StringUtils.isBlank(instance.getCreateBy())) {
return;
}
// 已完成、已退回这类结果消息只发给发起人,不再混入处理人待办消息。
List<String> messageType = null;
if (MapUtil.isNotEmpty(variable) && variable.containsKey(FlowConstant.MESSAGE_TYPE)) {
messageType = MapUtil.get(variable, FlowConstant.MESSAGE_TYPE, new TypeReference<>() {
});
}
SpringUtils.context().publishEvent(new WorkflowResultMessageEvent(definition.getFlowName(), status, instance.getCreateBy(), messageType));
}
/**
* 根据流程实例确定最终状态
*
* @param instance 流程实例
* @return 流程最终状态
*/
private String determineFlowStatus(Instance instance) {
String flowStatus = instance.getFlowStatus();
if (StringUtils.isNotBlank(flowStatus) && BusinessStatusEnum.initialState(flowStatus)) {
log.info("流程实例当前状态: {}", flowStatus);
return flowStatus;
} else {
Long instanceId = instance.getId();
if (flwTaskService.isTaskEnd(instanceId)) {
String status = BusinessStatusEnum.FINISH.getStatus();
// 更新流程状态为已完成
flwInstanceService.updateStatus(instanceId, status);
log.info("流程已结束,状态更新为: {}", status);
return status;
}
return null;
}
}
}
@@ -0,0 +1,85 @@
package org.dromara.workflow.listener;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.system.api.UserService;
import org.dromara.system.api.domain.UserDTO;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.MessageTypeEnum;
import org.dromara.workflow.event.WorkflowCopyEvent;
import org.dromara.workflow.event.WorkflowResultMessageEvent;
import org.dromara.workflow.event.WorkflowTaskMessageEvent;
import org.dromara.workflow.service.IFlwCommonService;
import org.dromara.workflow.service.IFlwTaskService;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.List;
/**
* 工作流副作用事件监听器。
*
* @author may
*/
@ConditionalOnEnable
@Component
@RequiredArgsConstructor
public class WorkflowSideEffectListener {
private final IFlwTaskService flwTaskService;
private final IFlwCommonService flwCommonService;
private final UserService userService;
/**
* 保存工作流抄送记录。
*
* @param event 工作流抄送事件
*/
@EventListener
public void handleCopy(WorkflowCopyEvent event) {
flwTaskService.setCopy(event.task(), event.flowCopyList());
}
/**
* 发送工作流待办消息。
*
* @param event 工作流待办消息事件
*/
@EventListener
public void handleTaskMessage(WorkflowTaskMessageEvent event) {
flwCommonService.sendMessage(event.flowName(), event.instanceId(), event.messageType(), event.notice());
}
/**
* 发送工作流结果消息。
*
* @param event 工作流结果消息事件
*/
@EventListener
public void handleResultMessage(WorkflowResultMessageEvent event) {
if (!StringUtils.equalsAny(event.status(), BusinessStatusEnum.FINISH.getStatus(), BusinessStatusEnum.BACK.getStatus())) {
return;
}
Long createBy = Convert.toLong(event.createBy(), null);
if (createBy == null) {
return;
}
BusinessStatusEnum status = BusinessStatusEnum.getByStatus(event.status());
if (status == null) {
return;
}
UserDTO initiator = userService.selectById(createBy);
if (initiator == null || initiator.getUserId() == null) {
return;
}
List<String> messageType = CollUtil.isNotEmpty(event.messageType())
? event.messageType()
: Collections.singletonList(MessageTypeEnum.SYSTEM_MESSAGE.getCode());
flwCommonService.sendResultMessage(event.flowName(), status, messageType, Collections.singletonList(initiator));
}
}
@@ -0,0 +1,95 @@
package org.dromara.workflow.liteflow.complete;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.query.QueryBuilder;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.warm.flow.core.FlowEngine;
import org.dromara.warm.flow.core.dto.FlowParams;
import org.dromara.warm.flow.core.entity.User;
import org.dromara.warm.flow.core.service.TaskService;
import org.dromara.warm.flow.orm.entity.FlowTask;
import org.dromara.warm.flow.orm.mapper.FlowTaskMapper;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.CompleteTaskContext;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.dromara.workflow.common.constant.FlowConstant.*;
/**
* 执行当前处理人的后续自动审批。
* <p>
* 仅当流程实例开启自动审批时生效,用于连续跳过同一登录人负责的后续待办任务。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("completeAutoPass")
public class CompleteAutoPassComponent extends NodeComponent {
private final TaskService taskService;
private final FlowTaskMapper flowTaskMapper;
@Override
public void process() {
CompleteTaskContext context = getContextBean(CompleteTaskContext.class);
FlowTask flowTask = context.getFlowTask();
autoPass(flowTask.getId(), context.getFlowParams(), flowTask.getInstanceId());
}
/**
* 递归跳过当前处理人的后续任务。
*
* @param taskId 本轮已处理任务 id,用于避免重复跳过同一任务
* @param flowParams 当前办理参数,会在自动审批时覆盖为自动审批消息和空抄送变量
* @param instanceId 流程实例 id
*/
private void autoPass(Long taskId, FlowParams flowParams, Long instanceId) {
List<FlowTask> flowTaskList = selectByInstId(instanceId);
if (CollUtil.isEmpty(flowTaskList)) {
return;
}
List<User> userList = FlowEngine.userService()
.getByAssociateds(StreamUtils.toList(flowTaskList, FlowTask::getId));
if (CollUtil.isEmpty(userList)) {
return;
}
for (FlowTask task : flowTaskList) {
if (task.getId().equals(taskId)) {
continue;
}
// 自动审批只处理当前登录人仍是办理人的后续任务,避免替其他候选人或并行分支误审批。
List<User> users = StreamUtils.filter(userList,
e -> ObjectUtil.equals(task.getId(), e.getAssociated()) && ObjectUtil.equal(e.getProcessedBy(), LoginHelper.getUserIdStr()));
if (CollUtil.isEmpty(users)) {
continue;
}
// 自动审批不继承人工办理时的抄送和消息通知,避免重复发送业务消息。
flowParams
.message("流程引擎自动审批!")
.variable(Map.of(
SUBMIT, false,
FLOW_COPY_LIST, Collections.emptyList(),
MESSAGE_NOTICE, StringUtils.EMPTY));
taskService.skip(task.getId(), flowParams);
// 跳过后可能生成新的后续任务,继续扫描直到当前登录人不再有可自动办理任务。
autoPass(task.getId(), flowParams, instanceId);
}
}
private List<FlowTask> selectByInstId(Long instanceId) {
return flowTaskMapper.selectList(QueryBuilder.lambda(FlowTask.class)
.eq(FlowTask::getInstanceId, instanceId)
.build());
}
}
@@ -0,0 +1,54 @@
package org.dromara.workflow.liteflow.complete;
import cn.hutool.core.convert.Convert;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.warm.flow.core.dto.FlowParams;
import org.dromara.warm.flow.core.enums.SkipType;
import org.dromara.warm.flow.core.service.TaskService;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.TaskStatusEnum;
import org.dromara.workflow.domain.bo.CompleteTaskBo;
import org.dromara.workflow.domain.context.CompleteTaskContext;
import java.util.Map;
import static org.dromara.workflow.common.constant.FlowConstant.*;
/**
* 构建办理参数并执行当前任务跳转。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("completeExecute")
public class CompleteExecuteComponent extends NodeComponent {
private final TaskService taskService;
@Override
public void process() {
CompleteTaskContext context = getContextBean(CompleteTaskContext.class);
CompleteTaskBo completeTaskBo = context.getCompleteTaskBo();
Map<String, Object> variables = completeTaskBo.getVariables();
FlowParams flowParams = FlowParams.build()
.handler(completeTaskBo.getHandler())
.variable(variables)
.ignore(Convert.toBool(variables.getOrDefault(VAR_IGNORE, false)))
.ignoreDepute(Convert.toBool(variables.getOrDefault(VAR_IGNORE_DEPUTE, false)))
.ignoreCooperate(Convert.toBool(variables.getOrDefault(VAR_IGNORE_COOPERATE, false)))
.skipType(SkipType.PASS.getKey())
.message(completeTaskBo.getMessage())
.flowStatus(BusinessStatusEnum.WAITING.getStatus())
.hisStatus(TaskStatusEnum.PASS.getStatus())
.hisTaskExt(completeTaskBo.getFileId());
context.setFlowParams(flowParams);
context.setAutoPass(Convert.toBool(context.getInstance().getVariableMap().getOrDefault(AUTO_PASS, false)));
taskService.skip(context.getFlowTask().getId(), flowParams);
}
}
@@ -0,0 +1,23 @@
package org.dromara.workflow.liteflow.complete;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeBooleanComponent;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.CompleteTaskContext;
/**
* 判断办理任务后是否进入自动审批分支。
*
* @author may
*/
@ConditionalOnEnable
@LiteflowComponent("completeNeedAutoPass")
public class CompleteNeedAutoPassComponent extends NodeBooleanComponent {
@Override
public boolean processBoolean() {
CompleteTaskContext context = getContextBean(CompleteTaskContext.class);
return Boolean.TRUE.equals(context.getAutoPass());
}
}
@@ -0,0 +1,100 @@
package org.dromara.workflow.liteflow.complete;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.warm.flow.core.entity.Instance;
import org.dromara.warm.flow.core.service.InsService;
import org.dromara.warm.flow.orm.entity.FlowTask;
import org.dromara.warm.flow.orm.mapper.FlowTaskMapper;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.TaskStatusEnum;
import org.dromara.workflow.domain.bo.CompleteTaskBo;
import org.dromara.workflow.domain.context.CompleteTaskContext;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.dromara.workflow.common.constant.FlowConstant.*;
/**
* 准备任务办理上下文。
* <p>
* 负责加载任务实例、补齐流程变量,并解析弹窗选择的后续办理人。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("completePrepare")
public class CompletePrepareComponent extends NodeComponent {
private final FlowTaskMapper flowTaskMapper;
private final InsService insService;
@Override
public void process() {
CompleteTaskContext context = getContextBean(CompleteTaskContext.class);
CompleteTaskBo completeTaskBo = context.getCompleteTaskBo();
FlowTask flowTask = flowTaskMapper.selectById(completeTaskBo.getTaskId());
if (ObjectUtil.isNull(flowTask)) {
throw new ServiceException("流程任务不存在或任务已审批!");
}
Instance instance = insService.getById(flowTask.getInstanceId());
if (ObjectUtil.isNull(instance)) {
throw new ServiceException("流程实例不存在");
}
context.setFlowTask(flowTask);
context.setInstance(instance);
Map<String, Object> variables = completeTaskBo.getVariables();
variables.put(FLOW_COPY_LIST, completeTaskBo.getFlowCopyList());
variables.put(MESSAGE_TYPE, completeTaskBo.getMessageType());
variables.put(MESSAGE_NOTICE, completeTaskBo.getNotice());
if (BusinessStatusEnum.isDraftOrCancelOrBack(instance.getFlowStatus())) {
// 草稿、撤销、驳回状态再次办理时等同于重新提交,条件表达式通过 SUBMIT 识别提交分支。
variables.put(SUBMIT, true);
}
Map<String, Object> assigneeMap = setPopAssigneeMap(completeTaskBo.getAssigneeMap(), instance.getVariableMap());
if (CollUtil.isNotEmpty(assigneeMap)) {
variables.putAll(assigneeMap);
}
}
/**
* 将弹窗处理人映射成通过/退回状态下的办理人变量。
*/
private Map<String, Object> setPopAssigneeMap(Map<String, Object> assigneeMap, Map<String, Object> variablesMap) {
Map<String, Object> map = new HashMap<>();
if (CollUtil.isEmpty(assigneeMap)) {
return map;
}
for (Map.Entry<String, Object> entry : assigneeMap.entrySet()) {
if (variablesMap.containsKey(entry.getKey())) {
String userIds = variablesMap.get(entry.getKey()).toString();
if (StringUtils.isNotBlank(userIds)) {
Set<String> hashSet = CollUtil.newHashSet();
List<String> popUserIds = StringUtils.str2List(entry.getValue().toString(), StringUtils.SEPARATOR, true, true);
List<String> variableUserIds = StringUtils.str2List(userIds, StringUtils.SEPARATOR, true, true);
hashSet.addAll(popUserIds);
hashSet.addAll(variableUserIds);
// Warm-Flow 会按“状态:变量名”读取办理人,正向和退回都写入同一批候选人。
map.put(TaskStatusEnum.PASS.getStatus() + StringUtils.COLON + entry.getKey(), StringUtils.joinComma(hashSet));
map.put(TaskStatusEnum.BACK.getStatus() + StringUtils.COLON + entry.getKey(), StringUtils.joinComma(hashSet));
}
} else {
map.put(TaskStatusEnum.PASS.getStatus() + StringUtils.COLON + entry.getKey(), entry.getValue());
map.put(TaskStatusEnum.BACK.getStatus() + StringUtils.COLON + entry.getKey(), entry.getValue());
}
}
return map;
}
}
@@ -0,0 +1,60 @@
package org.dromara.workflow.liteflow.instance;
import cn.hutool.core.util.ObjectUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.warm.flow.core.entity.Definition;
import org.dromara.warm.flow.core.entity.Instance;
import org.dromara.warm.flow.core.service.DefService;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.InstanceDeleteContext;
import org.dromara.workflow.handler.FlowProcessEventHandler;
import java.util.Map;
import java.util.function.Function;
/**
* 校验实例删除权限并发布业务删除事件。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@Slf4j
@LiteflowComponent("instanceDeleteEvent")
public class InstanceDeleteEventComponent extends NodeComponent {
private final DefService defService;
private final FlowProcessEventHandler flowProcessEventHandler;
@Override
public void process() {
InstanceDeleteContext context = getContextBean(InstanceDeleteContext.class);
String userId = LoginHelper.getUserIdStr();
context.getFlowInstances().forEach(flowInstance -> {
if (!LoginHelper.isSuperAdmin() && !flowInstance.getCreateBy().equals(userId)) {
throw new ServiceException("权限不足,无法删除流程实例信息!");
}
});
Map<Long, Definition> definitionMap = StreamUtils.toMap(
defService.getByIds(StreamUtils.toList(context.getFlowInstances(), Instance::getDefinitionId)),
Definition::getId,
Function.identity()
);
context.getFlowInstances().forEach(instance -> {
Definition definition = definitionMap.get(instance.getDefinitionId());
if (ObjectUtil.isNull(definition)) {
log.warn("实例 ID: {} 对应的流程定义信息未找到,跳过删除事件触发。", instance.getId());
return;
}
flowProcessEventHandler.processDeleteHandler(definition.getFlowCode(), instance.getBusinessId());
});
}
}
@@ -0,0 +1,79 @@
package org.dromara.workflow.liteflow.instance;
import cn.hutool.core.collection.CollUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.mybatis.core.query.QueryBuilder;
import org.dromara.warm.flow.core.FlowEngine;
import org.dromara.warm.flow.core.service.InsService;
import org.dromara.warm.flow.orm.entity.FlowHisTask;
import org.dromara.warm.flow.orm.entity.FlowTask;
import org.dromara.warm.flow.orm.mapper.FlowHisTaskMapper;
import org.dromara.warm.flow.orm.mapper.FlowTaskMapper;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.TaskStatusEnum;
import org.dromara.workflow.domain.context.InstanceDeleteContext;
import java.util.List;
import java.util.Objects;
/**
* 执行流程实例删除。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("instanceDeleteExecute")
public class InstanceDeleteExecuteComponent extends NodeComponent {
private final InsService insService;
private final FlowHisTaskMapper flowHisTaskMapper;
private final FlowTaskMapper flowTaskMapper;
@Override
public void process() {
InstanceDeleteContext context = getContextBean(InstanceDeleteContext.class);
deleteCopyUsers(context.getDeleteInstanceIds());
if (!context.isHistory()) {
context.setResult(insService.remove(context.getDeleteInstanceIds()));
return;
}
List<FlowTask> flowTaskList = flowTaskMapper.selectList(QueryBuilder.lambda(FlowTask.class)
.in(FlowTask::getInstanceId, context.getDeleteInstanceIds())
.build());
if (CollUtil.isNotEmpty(flowTaskList)) {
FlowEngine.userService().deleteByTaskIds(StreamUtils.toList(flowTaskList, FlowTask::getId));
}
FlowEngine.taskService().deleteByInsIds(context.getDeleteInstanceIds());
FlowEngine.hisTaskService().deleteByInsIds(context.getDeleteInstanceIds());
FlowEngine.insService().removeByIds(context.getDeleteInstanceIds());
context.setResult(true);
}
/**
* 删除流程实例产生的抄送人员关联。
*
* @param instanceIds 流程实例 ID 集合
*/
private void deleteCopyUsers(List<Long> instanceIds) {
List<FlowHisTask> copyTasks = flowHisTaskMapper.selectList(QueryBuilder.lambda(FlowHisTask.class)
.in(FlowHisTask::getInstanceId, instanceIds)
.eq(FlowHisTask::getFlowStatus, TaskStatusEnum.COPY.getStatus())
.build());
if (CollUtil.isEmpty(copyTasks)) {
return;
}
List<Long> copyTaskIds = copyTasks.stream()
.map(FlowHisTask::getTaskId)
.filter(Objects::nonNull)
.distinct()
.toList();
if (CollUtil.isNotEmpty(copyTaskIds)) {
FlowEngine.userService().deleteByTaskIds(copyTaskIds);
}
}
}
@@ -0,0 +1,24 @@
package org.dromara.workflow.liteflow.instance;
import cn.hutool.core.collection.CollUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeBooleanComponent;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.InstanceDeleteContext;
/**
* 判断删除流程实例时是否存在可执行删除的数据。
*
* @author may
*/
@ConditionalOnEnable
@LiteflowComponent("instanceDeleteExists")
public class InstanceDeleteExistsComponent extends NodeBooleanComponent {
@Override
public boolean processBoolean() {
InstanceDeleteContext context = getContextBean(InstanceDeleteContext.class);
return CollUtil.isNotEmpty(context.getFlowInstances());
}
}
@@ -0,0 +1,52 @@
package org.dromara.workflow.liteflow.instance;
import cn.hutool.core.collection.CollUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.mybatis.core.query.QueryBuilder;
import org.dromara.warm.flow.orm.entity.FlowInstance;
import org.dromara.warm.flow.orm.mapper.FlowInstanceMapper;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.InstanceDeleteContext;
import java.util.ArrayList;
import java.util.List;
/**
* 加载待删除流程实例。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@Slf4j
@LiteflowComponent("instanceDeleteLoad")
public class InstanceDeleteLoadComponent extends NodeComponent {
private final FlowInstanceMapper flowInstanceMapper;
@Override
public void process() {
InstanceDeleteContext context = getContextBean(InstanceDeleteContext.class);
List<FlowInstance> flowInstances;
if (CollUtil.isNotEmpty(context.getBusinessIds())) {
flowInstances = flowInstanceMapper.selectList(QueryBuilder.lambda(FlowInstance.class)
.in(FlowInstance::getBusinessId, context.getBusinessIds())
.build());
context.setDeleteInstanceIds(StreamUtils.toList(flowInstances, FlowInstance::getId));
} else {
flowInstances = flowInstanceMapper.selectByIds(context.getInstanceIds());
context.setDeleteInstanceIds(new ArrayList<>(context.getInstanceIds()));
}
if (CollUtil.isEmpty(flowInstances)) {
log.warn("未找到对应的流程实例信息,无法执行删除操作。");
context.setResult(false);
return;
}
context.setFlowInstances(flowInstances);
}
}
@@ -0,0 +1,49 @@
package org.dromara.workflow.liteflow.operation;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.warm.flow.core.service.TaskService;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.TaskStatusEnum;
import org.dromara.workflow.domain.context.TaskOperationContext;
/**
* 执行委派、转办、加签或减签。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("taskOpExecute")
public class TaskOpExecuteComponent extends NodeComponent {
private final TaskService taskService;
@Override
public void process() {
TaskOperationContext context = getContextBean(TaskOperationContext.class);
Long taskId = context.getTaskOperationBo().getTaskId();
boolean result = false;
switch (context.getOperation()) {
case DELEGATE_TASK -> {
context.getFlowParams().hisStatus(TaskStatusEnum.DEPUTE.getStatus());
result = taskService.depute(taskId, context.getFlowParams());
}
case TRANSFER_TASK -> {
context.getFlowParams().hisStatus(TaskStatusEnum.TRANSFER.getStatus());
result = taskService.transfer(taskId, context.getFlowParams());
}
case ADD_SIGNATURE -> {
context.getFlowParams().hisStatus(TaskStatusEnum.SIGN.getStatus());
result = taskService.addSignature(taskId, context.getFlowParams());
}
case REDUCTION_SIGNATURE -> {
context.getFlowParams().hisStatus(TaskStatusEnum.SIGN_OFF.getStatus());
result = taskService.reductionSignature(taskId, context.getFlowParams());
}
}
context.setResult(result);
}
}
@@ -0,0 +1,53 @@
package org.dromara.workflow.liteflow.operation;
import cn.hutool.core.util.ObjectUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.mybatis.core.query.QueryBuilder;
import org.dromara.warm.flow.core.entity.Task;
import org.dromara.warm.flow.core.enums.CooperateType;
import org.dromara.warm.flow.core.service.TaskService;
import org.dromara.warm.flow.orm.entity.FlowNode;
import org.dromara.warm.flow.orm.mapper.FlowNodeMapper;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.TaskOperationEnum;
import org.dromara.workflow.domain.context.TaskOperationContext;
/**
* 加载任务节点并校验操作约束。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("taskOpLoad")
public class TaskOpLoadComponent extends NodeComponent {
private final TaskService taskService;
private final FlowNodeMapper flowNodeMapper;
@Override
public void process() {
TaskOperationContext context = getContextBean(TaskOperationContext.class);
Task task = taskService.getById(context.getTaskOperationBo().getTaskId());
if (ObjectUtil.isNull(task)) {
throw new ServiceException("任务不存在!");
}
FlowNode flowNode = flowNodeMapper.selectOne(QueryBuilder.lambda(FlowNode.class)
.eq(FlowNode::getNodeCode, task.getNodeCode())
.eq(FlowNode::getDefinitionId, task.getDefinitionId())
.build());
if (ObjectUtil.isNull(flowNode)) {
throw new ServiceException("流程节点不存在");
}
if ((context.getOperation() == TaskOperationEnum.ADD_SIGNATURE || context.getOperation() == TaskOperationEnum.REDUCTION_SIGNATURE)
&& CooperateType.isOrSign(flowNode.getNodeRatio())) {
throw new ServiceException(task.getNodeName() + "不是会签或票签节点!");
}
context.setTask(task);
context.setFlowNode(flowNode);
}
}
@@ -0,0 +1,24 @@
package org.dromara.workflow.liteflow.operation;
import cn.hutool.core.collection.CollUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeBooleanComponent;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.TaskOperationContext;
/**
* 判断任务操作后是否进入消息通知分支。
*
* @author may
*/
@ConditionalOnEnable
@LiteflowComponent("taskOpNeedNotify")
public class TaskOpNeedNotifyComponent extends NodeBooleanComponent {
@Override
public boolean processBoolean() {
TaskOperationContext context = getContextBean(TaskOperationContext.class);
return context.isResult() && CollUtil.isNotEmpty(context.getTaskOperationBo().getMessageType());
}
}
@@ -0,0 +1,57 @@
package org.dromara.workflow.liteflow.operation;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.system.api.UserService;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.bo.TaskOperationBo;
import org.dromara.workflow.domain.context.TaskOperationContext;
import org.dromara.workflow.service.IFlwCommonService;
import java.util.ArrayList;
import java.util.List;
import static org.dromara.workflow.common.constant.FlowConstant.PATH_TASK_WAITING;
/**
* 任务操作成功后发送待办消息。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("taskOpNotify")
public class TaskOpNotifyComponent extends NodeComponent {
private final IFlwCommonService flwCommonService;
private final UserService userService;
@Override
public void process() {
TaskOperationContext context = getContextBean(TaskOperationContext.class);
TaskOperationBo bo = context.getTaskOperationBo();
List<Long> userIdList = new ArrayList<>();
if (StringUtils.isNotBlank(bo.getUserId())) {
userIdList.add(Convert.toLong(bo.getUserId()));
}
if (CollUtil.isNotEmpty(bo.getUserIds())) {
userIdList.addAll(bo.getUserIds().stream().map(Convert::toLong).toList());
}
if (CollUtil.isEmpty(userIdList)) {
return;
}
// 转办、委托、加减签等运行时操作,消息接收人统一从“我的待办”进入处理。
flwCommonService.sendMessage(
bo.getMessageType(),
StringUtils.isNotBlank(bo.getMessage()) ? bo.getMessage() : "单据「" + context.getOperation().getDesc() + "」通知",
"单据「" + context.getOperation().getDesc() + "」提醒",
userService.selectListByIds(userIdList),
PATH_TASK_WAITING
);
}
}
@@ -0,0 +1,57 @@
package org.dromara.workflow.liteflow.operation;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.warm.flow.core.dto.FlowParams;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.TaskOperationEnum;
import org.dromara.workflow.domain.bo.TaskOperationBo;
import org.dromara.workflow.domain.context.TaskOperationContext;
import java.util.Collections;
/**
* 准备任务操作参数。
*
* @author may
*/
@ConditionalOnEnable
@Slf4j
@LiteflowComponent("taskOpPrepare")
public class TaskOpPrepareComponent extends NodeComponent {
@Override
public void process() {
TaskOperationContext context = getContextBean(TaskOperationContext.class);
TaskOperationEnum op = TaskOperationEnum.getByCode(context.getTaskOperation());
if (op == null) {
log.error("Invalid operation type:{} ", context.getTaskOperation());
throw new ServiceException("Invalid operation type " + context.getTaskOperation());
}
context.setOperation(op);
TaskOperationBo bo = context.getTaskOperationBo();
switch (op) {
case DELEGATE_TASK, TRANSFER_TASK -> ValidatorUtils.validate(bo, AddGroup.class);
case ADD_SIGNATURE, REDUCTION_SIGNATURE -> ValidatorUtils.validate(bo, EditGroup.class);
}
FlowParams flowParams = FlowParams.build().message(bo.getMessage());
if (LoginHelper.isSuperAdmin()) {
flowParams.ignore(true);
}
switch (op) {
case DELEGATE_TASK, TRANSFER_TASK -> flowParams.addHandlers(Collections.singletonList(bo.getUserId()));
case ADD_SIGNATURE -> flowParams.addHandlers(bo.getUserIds());
case REDUCTION_SIGNATURE -> flowParams.reductionHandlers(bo.getUserIds());
}
context.setFlowParams(flowParams);
}
}
@@ -0,0 +1,67 @@
package org.dromara.workflow.liteflow.start;
import cn.hutool.core.collection.CollUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.warm.flow.core.dto.FlowParams;
import org.dromara.warm.flow.core.entity.Instance;
import org.dromara.warm.flow.core.entity.Task;
import org.dromara.warm.flow.core.service.InsService;
import org.dromara.warm.flow.core.service.TaskService;
import org.dromara.warm.flow.orm.entity.FlowTask;
import org.dromara.workflow.api.domain.StartProcessReturnDTO;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import org.dromara.workflow.domain.context.StartProcessContext;
import org.dromara.workflow.mapper.FlwInstanceBizExtMapper;
import java.util.List;
/**
* 启动流程实例并构建返回结果。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("startExecute")
public class StartExecuteComponent extends NodeComponent {
private final InsService insService;
private final TaskService taskService;
private final FlwInstanceBizExtMapper flwInstanceBizExtMapper;
@Override
public void process() {
StartProcessContext context = getContextBean(StartProcessContext.class);
FlowParams flowParams = FlowParams.build()
.handler(context.getStartProcessBo().getHandler())
.flowCode(context.getStartProcessBo().getFlowCode())
.variable(context.getVariables())
.flowStatus(BusinessStatusEnum.DRAFT.getStatus());
Instance instance = insService.start(context.getBusinessId(), flowParams);
context.setInstance(instance);
saveBizExt(context, instance);
List<Task> taskList = taskService.list(new FlowTask().setInstanceId(instance.getId()));
if (CollUtil.isEmpty(taskList)) {
throw new ServiceException("流程启动失败,未生成任务");
}
if (taskList.size() > 1) {
throw new ServiceException("请检查流程第一个环节是否为申请人!");
}
context.setTaskList(taskList);
context.setStartProcessReturn(new StartProcessReturnDTO(instance.getId(), taskList.getFirst().getId()));
}
private void saveBizExt(StartProcessContext context, Instance instance) {
FlowInstanceBizExt bizExt = context.getBizExt();
bizExt.setInstanceId(instance.getId());
bizExt.setBusinessId(instance.getBusinessId());
flwInstanceBizExtMapper.saveOrUpdateByInstanceId(bizExt);
}
}
@@ -0,0 +1,24 @@
package org.dromara.workflow.liteflow.start;
import cn.hutool.core.util.ObjectUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeBooleanComponent;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.StartProcessContext;
/**
* 判断启动流程时是否走已有实例续提交分支。
*
* @author may
*/
@ConditionalOnEnable
@LiteflowComponent("startExists")
public class StartExistsComponent extends NodeBooleanComponent {
@Override
public boolean processBoolean() {
StartProcessContext context = getContextBean(StartProcessContext.class);
return ObjectUtil.isNotNull(context.getExistingInstance());
}
}
@@ -0,0 +1,55 @@
package org.dromara.workflow.liteflow.start;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.warm.flow.core.FlowEngine;
import org.dromara.warm.flow.core.entity.Definition;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import org.dromara.workflow.domain.context.StartProcessContext;
import java.util.Map;
import static org.dromara.workflow.common.constant.FlowConstant.*;
/**
* 加载流程定义并补齐启动变量。
*
* @author may
*/
@ConditionalOnEnable
@LiteflowComponent("startPrepareInstance")
public class StartPrepareInstanceComponent extends NodeComponent {
@Override
public void process() {
StartProcessContext context = getContextBean(StartProcessContext.class);
Definition definition = FlowEngine.defService().getPublishByFlowCode(context.getStartProcessBo().getFlowCode());
if (ObjectUtil.isNull(definition)) {
throw new ServiceException("流程【" + context.getStartProcessBo().getFlowCode() + "】未发布,请先在流程设计器中发布流程定义");
}
context.setDefinition(definition);
Map<String, Object> variables = context.getVariables();
Dict dict = JsonUtils.parseMap(definition.getExt());
boolean autoPass = !ObjectUtil.isNull(dict) && dict.getBool(AUTO_PASS);
variables.put(AUTO_PASS, autoPass);
variables.put(BUSINESS_CODE, generateBusinessCode(context.getBizExt()));
}
private String generateBusinessCode(FlowInstanceBizExt bizExt) {
if (ObjectUtil.isNull(bizExt.getBusinessCode()) || bizExt.getBusinessCode().isBlank()) {
// TODO: 按照自己业务规则生成编号
String businessCode = Convert.toStr(System.currentTimeMillis());
bizExt.setBusinessCode(businessCode);
return businessCode;
}
return bizExt.getBusinessCode();
}
}
@@ -0,0 +1,53 @@
package org.dromara.workflow.liteflow.start;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.query.QueryBuilder;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.warm.flow.orm.entity.FlowInstance;
import org.dromara.warm.flow.orm.mapper.FlowInstanceMapper;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.StartProcessContext;
import java.util.Map;
import static org.dromara.workflow.common.constant.FlowConstant.*;
/**
* 准备流程启动请求。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("startPrepareRequest")
public class StartPrepareRequestComponent extends NodeComponent {
private final FlowInstanceMapper flowInstanceMapper;
@Override
public void process() {
StartProcessContext context = getContextBean(StartProcessContext.class);
String businessId = context.getStartProcessBo().getBusinessId();
if (StringUtils.isBlank(businessId)) {
throw new ServiceException("启动工作流时必须包含业务ID");
}
context.setBusinessId(businessId);
context.setVariables(context.getStartProcessBo().getVariables());
context.setBizExt(context.getStartProcessBo().getBizExt());
Map<String, Object> variables = context.getVariables();
variables.put(INITIATOR, LoginHelper.getUserIdStr());
variables.put(INITIATOR_DEPT_ID, LoginHelper.getDeptId());
variables.put(BUSINESS_ID, businessId);
FlowInstance flowInstance = flowInstanceMapper.selectOne(QueryBuilder.lambda(FlowInstance.class)
.eq(FlowInstance::getBusinessId, businessId)
.build());
context.setExistingInstance(flowInstance);
}
}
@@ -0,0 +1,56 @@
package org.dromara.workflow.liteflow.start;
import cn.hutool.core.collection.CollUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.warm.flow.core.entity.Task;
import org.dromara.warm.flow.core.service.InsService;
import org.dromara.warm.flow.core.service.TaskService;
import org.dromara.warm.flow.orm.entity.FlowTask;
import org.dromara.workflow.api.domain.StartProcessReturnDTO;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import org.dromara.workflow.domain.context.StartProcessContext;
import org.dromara.workflow.mapper.FlwInstanceBizExtMapper;
import java.util.List;
/**
* 已存在流程实例时执行续提交。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("startResume")
public class StartResumeComponent extends NodeComponent {
private final TaskService taskService;
private final InsService insService;
private final FlwInstanceBizExtMapper flwInstanceBizExtMapper;
@Override
public void process() {
StartProcessContext context = getContextBean(StartProcessContext.class);
BusinessStatusEnum.checkStartStatus(context.getExistingInstance().getFlowStatus());
List<Task> taskList = taskService.list(new FlowTask().setInstanceId(context.getExistingInstance().getId()));
if (CollUtil.isEmpty(taskList)) {
throw new ServiceException("流程实例缺少任务,请检查流程定义配置");
}
taskService.mergeVariable(context.getExistingInstance(), context.getVariables());
insService.updateById(context.getExistingInstance());
saveBizExt(context);
context.setStartProcessReturn(new StartProcessReturnDTO(taskList.getFirst().getInstanceId(), taskList.getFirst().getId()));
}
private void saveBizExt(StartProcessContext context) {
FlowInstanceBizExt bizExt = context.getBizExt();
bizExt.setInstanceId(context.getExistingInstance().getId());
bizExt.setBusinessId(context.getExistingInstance().getBusinessId());
flwInstanceBizExtMapper.saveOrUpdateByInstanceId(bizExt);
}
}
@@ -0,0 +1,46 @@
package org.dromara.workflow.mapper;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.workflow.domain.FlowCategory;
import org.dromara.workflow.domain.vo.FlowCategoryVo;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* 流程分类Mapper接口
*
* @author may
* @date 2023-06-27
*/
public interface FlwCategoryMapper extends BaseMapperPlus<FlowCategory, FlowCategoryVo> {
/**
* 根据父流程分类ID查询其所有子流程分类的列表
*
* @param parentId 父流程分类ID
* @return 包含子流程分类的列表
*/
default List<FlowCategory> selectListByParentId(Long parentId) {
return this.lambda()
.select(FlowCategory::getCategoryId)
.findInSet(parentId, FlowCategory::getAncestors)
.list();
}
/**
* 根据父流程分类ID查询包括父ID及其所有子流程分类ID的列表
*
* @param parentId 父流程分类ID
* @return 包含父ID和子流程分类ID的列表
*/
default List<Long> selectCategoryIdsByParentId(Long parentId) {
return Stream.concat(
this.selectListByParentId(parentId).stream()
.map(FlowCategory::getCategoryId),
Stream.of(parentId)
).collect(Collectors.toList());
}
}
@@ -0,0 +1,69 @@
package org.dromara.workflow.mapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.yulichang.base.MPJBaseMapper;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.common.mybatis.core.query.QueryBuilder;
import org.dromara.warm.flow.core.enums.NodeType;
import org.dromara.warm.flow.orm.entity.FlowDefinition;
import org.dromara.warm.flow.orm.entity.FlowHisTask;
import org.dromara.warm.flow.orm.entity.FlowInstance;
import org.dromara.workflow.common.enums.TaskStatusEnum;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import org.dromara.workflow.domain.bo.FlowTaskBo;
import org.dromara.workflow.domain.vo.FlowHisTaskVo;
import java.util.List;
import java.util.Map;
/**
* 任务信息Mapper接口
*
* @author may
* @date 2024-03-02
*/
public interface FlwHisTaskMapper extends BaseMapperPlus<FlowHisTask, FlowHisTaskVo>, MPJBaseMapper<FlowHisTask> {
/**
* 分页查询已办任务列表。
*
* @param page 分页对象
* @param bo 查询条件
* @param categoryIds 流程分类 ID 集合
* @param userId 当前用户 ID
* @return 已办任务分页结果
*/
default Page<FlowHisTaskVo> getListFinishTask(Page<FlowHisTaskVo> page, FlowTaskBo bo, List<String> categoryIds, String userId) {
Map<String, Object> params = bo.getParams();
return QueryBuilder.lambdaJoin("a", FlowHisTask.class)
.select(FlowHisTask::getId, FlowHisTask::getNodeCode, FlowHisTask::getNodeName,
FlowHisTask::getCooperateType, FlowHisTask::getApprover, FlowHisTask::getCollaborator,
FlowHisTask::getNodeType, FlowHisTask::getTargetNodeCode, FlowHisTask::getTargetNodeName,
FlowHisTask::getDefinitionId, FlowHisTask::getInstanceId)
.selectAs(FlowHisTask::getFlowStatus, FlowHisTaskVo::getFlowTaskStatus)
.select(FlowHisTask::getMessage, FlowHisTask::getExt, FlowHisTask::getCreateTime,
FlowHisTask::getUpdateTime, FlowHisTask::getFormCustom, FlowHisTask::getFormPath)
.select("b", FlowInstance::getFlowStatus, FlowInstance::getBusinessId, FlowInstance::getCreateBy)
.select("c", FlowDefinition::getFlowName, FlowDefinition::getFlowCode, FlowDefinition::getCategory,
FlowDefinition::getVersion)
.select("biz", FlowInstanceBizExt::getBusinessCode, FlowInstanceBizExt::getBusinessTitle)
.leftJoin(FlowInstance.class, "b", FlowInstance::getId, FlowHisTask::getInstanceId)
.leftJoin(FlowDefinition.class, "c", FlowDefinition::getId, FlowHisTask::getDefinitionId)
.leftJoin(FlowInstanceBizExt.class, "biz", FlowInstanceBizExt::getInstanceId, FlowInstance::getId)
.in("a", FlowHisTask::getNodeType, List.of(1, 3, 4))
.ne("a", FlowHisTask::getFlowStatus, TaskStatusEnum.COPY.getStatus())
.likeIfText("a", FlowHisTask::getNodeName, bo.getNodeName())
.likeIfText("c", FlowDefinition::getFlowName, bo.getFlowName())
.likeIfText("c", FlowDefinition::getFlowCode, bo.getFlowCode())
.likeIfText("b", FlowInstance::getFlowStatus, bo.getFlowStatus())
.inIfNotEmpty("b", FlowInstance::getCreateBy, bo.getCreateByIds())
.inIfNotEmpty("c", FlowDefinition::getCategory, categoryIds)
.betweenParams("a", FlowHisTask::getCreateTime, params, "beginTime", "endTime")
.eq(userId != null && !userId.isBlank(), "a", FlowHisTask::getNodeType, NodeType.BETWEEN.getKey())
.eqIfText("a", FlowHisTask::getApprover, userId)
.orderByDesc("a", FlowHisTask::getCreateTime)
.orderByDesc("a", FlowHisTask::getUpdateTime)
.page(page, FlowHisTaskVo.class);
}
}
@@ -0,0 +1,59 @@
package org.dromara.workflow.mapper;
import cn.hutool.core.util.ObjectUtil;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import java.util.Collection;
/**
* 流程实例业务扩展Mapper接口
*
* @author may
* @date 2025-08-05
*/
public interface FlwInstanceBizExtMapper extends BaseMapperPlus<FlowInstanceBizExt, FlowInstanceBizExt> {
/**
* 根据 instanceId 保存或更新流程实例业务扩展
*
* @param entity 流程实例业务扩展实体
* @return 操作是否成功
*/
default int saveOrUpdateByInstanceId(FlowInstanceBizExt entity) {
// 查询是否存在
FlowInstanceBizExt exist = this.lambda()
.eq(FlowInstanceBizExt::getInstanceId, entity.getInstanceId())
.one();
if (ObjectUtil.isNotNull(exist)) {
// 存在就带上主键更新
entity.setId(exist.getId());
return updateById(entity);
} else {
// 不存在就插入
return insert(entity);
}
}
/**
* 按照流程实例ID删除单个流程实例业务扩展
*
* @param instanceId 流程实例ID
* @return 删除的行数
*/
default int deleteByInstId(Long instanceId) {
return this.lambda().eq(FlowInstanceBizExt::getInstanceId, instanceId).deleteCount();
}
/**
* 按照流程实例ID批量删除流程实例业务扩展
*
* @param instanceIds 流程实例ID列表
* @return 删除的行数
*/
default int deleteByInstIds(Collection<Long> instanceIds) {
return this.lambda().in(FlowInstanceBizExt::getInstanceId, instanceIds).deleteCount();
}
}
@@ -0,0 +1,28 @@
package org.dromara.workflow.mapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.yulichang.base.MPJBaseMapper;
import com.github.yulichang.wrapper.MPJLambdaWrapper;
import org.dromara.warm.flow.orm.entity.FlowInstance;
import org.dromara.workflow.domain.vo.FlowInstanceVo;
/**
* 实例信息Mapper接口
*
* @author may
* @date 2024-03-02
*/
public interface FlwInstanceMapper extends MPJBaseMapper<FlowInstance> {
/**
* 流程实例信息
*
* @param page 分页
* @param queryWrapper 条件
* @return 结果
*/
default Page<FlowInstanceVo> selectInstanceList(Page<FlowInstanceVo> page, MPJLambdaWrapper<FlowInstance> queryWrapper) {
return this.selectJoinPage(page, FlowInstanceVo.class, queryWrapper);
}
}
@@ -0,0 +1,15 @@
package org.dromara.workflow.mapper;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.workflow.domain.FlowSpel;
import org.dromara.workflow.domain.vo.FlowSpelVo;
/**
* 流程spel表达式定义Mapper接口
*
* @author Michelle.Chung
* @date 2025-07-04
*/
public interface FlwSpelMapper extends BaseMapperPlus<FlowSpel, FlowSpelVo> {
}
@@ -0,0 +1,103 @@
package org.dromara.workflow.mapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.github.yulichang.base.MPJBaseMapper;
import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.common.mybatis.core.query.QueryBuilder;
import org.dromara.warm.flow.core.enums.NodeType;
import org.dromara.warm.flow.orm.entity.*;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import org.dromara.workflow.domain.bo.FlowTaskBo;
import org.dromara.workflow.domain.vo.FlowTaskVo;
import java.util.List;
import java.util.Map;
/**
* 任务信息Mapper接口
*
* @author may
* @date 2024-03-02
*/
public interface FlwTaskMapper extends BaseMapperPlus<FlowTask, FlowTaskVo>, MPJBaseMapper<FlowTask> {
/**
* 分页查询运行中的待办任务。
*
* @param page 分页对象
* @param bo 查询条件
* @param categoryIds 流程分类 id 列表
* @param userId 当前用户 id
* @return 待办任务分页结果
*/
default Page<FlowTaskVo> getListRunTask(Page<FlowTaskVo> page, FlowTaskBo bo, List<String> categoryIds, String userId) {
Map<String, Object> params = bo.getParams();
return QueryBuilder.lambdaJoin("t", FlowTask.class)
.distinct()
.select(FlowTask::getId, FlowTask::getNodeCode, FlowTask::getNodeName, FlowTask::getNodeType,
FlowTask::getDefinitionId, FlowTask::getInstanceId, FlowTask::getCreateTime, FlowTask::getUpdateTime)
.select("i", FlowInstance::getBusinessId, FlowInstance::getFlowStatus, FlowInstance::getCreateBy)
.select("d", FlowDefinition::getFlowName, FlowDefinition::getFlowCode, FlowDefinition::getFormCustom,
FlowDefinition::getCategory, FlowDefinition::getVersion)
.select("uu", FlowUser::getProcessedBy, FlowUser::getType)
.select("biz", FlowInstanceBizExt::getBusinessCode, FlowInstanceBizExt::getBusinessTitle)
.selectAs("COALESCE(NULLIF(TRIM(t.form_path), ''), NULLIF(TRIM(d.form_path), ''))", FlowTaskVo::getFormPath)
.leftJoin(FlowUser.class, "uu", FlowUser::getAssociated, FlowTask::getId)
.leftJoin(FlowDefinition.class, "d", FlowDefinition::getId, FlowTask::getDefinitionId)
.leftJoin(FlowInstance.class, "i", FlowInstance::getId, FlowTask::getInstanceId)
.leftJoin(FlowInstanceBizExt.class, "biz", FlowInstanceBizExt::getInstanceId, FlowInstance::getId)
.eq("t", FlowTask::getNodeType, NodeType.BETWEEN.getKey())
.in("uu", FlowUser::getType, List.of("1", "2", "3"))
.likeIfText("t", FlowTask::getNodeName, bo.getNodeName())
.likeIfText("d", FlowDefinition::getFlowName, bo.getFlowName())
.likeIfText("d", FlowDefinition::getFlowCode, bo.getFlowCode())
.likeIfText("i", FlowInstance::getFlowStatus, bo.getFlowStatus())
.inIfNotEmpty("i", FlowInstance::getCreateBy, bo.getCreateByIds())
.inIfNotEmpty("d", FlowDefinition::getCategory, categoryIds)
.betweenParams("t", FlowTask::getCreateTime, params, "beginTime", "endTime")
.eqIfText("uu", FlowUser::getProcessedBy, userId)
.eq(userId != null && !userId.isBlank(), "i", FlowInstance::getFlowStatus, BusinessStatusEnum.WAITING.getStatus())
.orderByDesc("t", FlowTask::getCreateTime)
.orderByDesc("t", FlowTask::getUpdateTime)
.page(page, FlowTaskVo.class);
}
/**
* 分页查询抄送任务。
*
* @param page 分页对象
* @param bo 查询条件
* @param categoryIds 流程分类 id 列表
* @param userId 当前用户 id
* @return 抄送任务分页结果
*/
default Page<FlowTaskVo> getTaskCopyByPage(Page<FlowTaskVo> page, FlowTaskBo bo, List<String> categoryIds, String userId) {
Map<String, Object> params = bo.getParams();
return QueryBuilder.lambdaJoin("a", FlowUser.class)
.select(FlowUser::getProcessedBy, FlowUser::getCreateTime)
.select("b", FlowHisTask::getId, FlowHisTask::getUpdateTime, FlowHisTask::getFormCustom,
FlowHisTask::getFormPath, FlowHisTask::getNodeName, FlowHisTask::getNodeCode)
.select("c", FlowInstance::getBusinessId, FlowInstance::getFlowStatus, FlowInstance::getCreateBy)
.select("d", FlowDefinition::getFlowName, FlowDefinition::getFlowCode, FlowDefinition::getCategory,
FlowDefinition::getVersion)
.select("biz", FlowInstanceBizExt::getBusinessCode, FlowInstanceBizExt::getBusinessTitle)
.leftJoin(FlowHisTask.class, "b", FlowHisTask::getTaskId, FlowUser::getAssociated)
.leftJoin(FlowInstance.class, "c", FlowInstance::getId, FlowHisTask::getInstanceId)
.leftJoin(FlowDefinition.class, "d", FlowDefinition::getId, FlowInstance::getDefinitionId)
.leftJoin(FlowInstanceBizExt.class, "biz", FlowInstanceBizExt::getInstanceId, FlowInstance::getId)
.eq("a", FlowUser::getType, "4")
.likeIfText("b", FlowHisTask::getNodeName, bo.getNodeName())
.likeIfText("d", FlowDefinition::getFlowName, bo.getFlowName())
.likeIfText("d", FlowDefinition::getFlowCode, bo.getFlowCode())
.likeIfText("c", FlowInstance::getFlowStatus, bo.getFlowStatus())
.inIfNotEmpty("c", FlowInstance::getCreateBy, bo.getCreateByIds())
.inIfNotEmpty("d", FlowDefinition::getCategory, categoryIds)
.betweenParams("a", FlowUser::getCreateTime, params, "beginTime", "endTime")
.eqIfText("a", FlowUser::getProcessedBy, userId)
.orderByDesc("a", FlowUser::getCreateTime)
.orderByDesc("b", FlowHisTask::getUpdateTime)
.page(page, FlowTaskVo.class);
}
}
@@ -0,0 +1,15 @@
package org.dromara.workflow.mapper;
import com.github.yulichang.base.MPJBaseMapper;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.warm.flow.orm.entity.FlowUser;
/**
* 任务信息Mapper接口
*
* @author may
* @date 2024-03-02
*/
public interface FlwUserMapper extends BaseMapperPlus<FlowUser, FlowUser>, MPJBaseMapper<FlowUser> {
}
@@ -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> {
}
@@ -0,0 +1,41 @@
package org.dromara.workflow.rule;
import cn.hutool.core.util.ObjectUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.system.api.DeptService;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.springframework.stereotype.Component;
/**
* spel表达式规则组件
* <p>
* 通过该组件统一管理流程定义中的spel表达式
* </p>
*
* @author Michelle.Chung
*/
@ConditionalOnEnable
@Slf4j
@Component
@RequiredArgsConstructor
public class SpelRuleComponent {
private final DeptService deptService;
/**
* 通过发起人部门 ID 获取部门负责人。
*
* @param initiatorDeptId 发起人部门 ID
* @return 部门负责人用户 ID
*/
public Long selectDeptLeaderById(Long initiatorDeptId) {
Long leaderId = deptService.selectDeptLeaderById(initiatorDeptId);
if (ObjectUtil.isNull(leaderId)) {
throw new ServiceException("当前部门未设置负责人,请联系管理员操作。");
}
return leaderId;
}
}
@@ -0,0 +1,105 @@
package org.dromara.workflow.service;
import cn.hutool.core.lang.tree.Tree;
import org.dromara.workflow.domain.bo.FlowCategoryBo;
import org.dromara.workflow.domain.vo.FlowCategoryVo;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 流程分类Service接口
*
* @author may
*/
public interface IFlwCategoryService {
/**
* 查询流程分类
*
* @param categoryId 主键
* @return 流程分类
*/
FlowCategoryVo queryById(Long categoryId);
/**
* 根据流程分类ID查询流程分类名称
*
* @param categoryId 流程分类ID
* @return 流程分类名称
*/
String selectCategoryNameById(Long categoryId);
/**
* 根据流程分类ID查询流程分类名称
*
* @param categoryIds 流程分类ID
* @return 流程分类名称
*/
Map<Long, String> selectCategoryNameByIds(Set<Long> categoryIds);
/**
* 查询符合条件的流程分类列表
*
* @param bo 查询条件
* @return 流程分类列表
*/
List<FlowCategoryVo> queryList(FlowCategoryBo bo);
/**
* 查询流程分类树结构信息
*
* @param category 流程分类信息
* @return 流程分类树信息集合
*/
List<Tree<String>> selectCategoryTreeList(FlowCategoryBo category);
/**
* 校验流程分类名称是否唯一
*
* @param category 流程分类信息
* @return 结果
*/
boolean checkCategoryNameUnique(FlowCategoryBo category);
/**
* 查询流程分类是否存在流程定义
*
* @param categoryId 流程分类ID
* @return 结果 true 存在 false 不存在
*/
boolean checkCategoryExistDefinition(Long categoryId);
/**
* 是否存在流程分类子节点
*
* @param categoryId 流程分类ID
* @return 结果
*/
boolean hasChildByCategoryId(Long categoryId);
/**
* 新增流程分类
*
* @param bo 流程分类
* @return 是否新增成功
*/
int insertByBo(FlowCategoryBo bo);
/**
* 修改流程分类
*
* @param bo 流程分类
* @return 是否修改成功
*/
int updateByBo(FlowCategoryBo bo);
/**
* 删除流程分类信息
*
* @param categoryId 主键
* @return 是否删除成功
*/
int deleteWithValidById(Long categoryId);
}
@@ -0,0 +1,63 @@
package org.dromara.workflow.service;
import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.system.api.domain.UserDTO;
import java.util.List;
/**
* 通用 工作流服务
*
* @author LionLi
*/
public interface IFlwCommonService {
/**
* 发送消息
*
* @param flowName 流程定义名称
* @param instId 实例id
* @param messageType 消息类型
* @param message 消息内容,为空则发送默认配置的消息内容
*/
void sendMessage(String flowName, Long instId, List<String> messageType, String message);
/**
* 发送消息
*
* @param messageType 消息类型
* @param message 消息内容
* @param subject 邮件标题
* @param userList 接收用户
*/
void sendMessage(List<String> messageType, String message, String subject, List<UserDTO> userList);
/**
* 发送带跳转路径的消息。
*
* @param messageType 消息类型
* @param message 消息内容
* @param subject 邮件标题
* @param userList 接收用户
* @param path 前端跳转路径
*/
void sendMessage(List<String> messageType, String message, String subject, List<UserDTO> userList, String path);
/**
* 发送工作流结果消息给指定用户。
*
* @param flowName 流程名称
* @param status 流程状态
* @param messageType 消息类型
* @param userList 接收用户
*/
void sendResultMessage(String flowName, BusinessStatusEnum status, List<String> messageType, List<UserDTO> userList);
/**
* 申请人节点编码
*
* @param definitionId 流程定义id
* @return 申请人节点编码
*/
String applyNodeCode(Long definitionId);
}
@@ -0,0 +1,72 @@
package org.dromara.workflow.service;
import jakarta.servlet.http.HttpServletResponse;
import org.dromara.common.core.domain.PageResult;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.warm.flow.orm.entity.FlowDefinition;
import org.dromara.workflow.domain.vo.FlowDefinitionVo;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.Collection;
/**
* 流程定义 服务层
*
* @author may
*/
public interface IFlwDefinitionService {
/**
* 查询流程定义列表
*
* @param flowDefinition 参数
* @param pageQuery 分页
* @return 返回分页列表
*/
PageResult<FlowDefinitionVo> queryList(FlowDefinition flowDefinition, PageQuery pageQuery);
/**
* 查询未发布的流程定义列表
*
* @param flowDefinition 参数
* @param pageQuery 分页
* @return 返回分页列表
*/
PageResult<FlowDefinitionVo> unPublishList(FlowDefinition flowDefinition, PageQuery pageQuery);
/**
* 发布流程定义
*
* @param id 流程定义id
* @return 是否发布成功
*/
boolean publish(Long id);
/**
* 导出流程定义
*
* @param id 流程定义id
* @param response 响应
* @throws IOException 异常
*/
void exportDef(Long id, HttpServletResponse response) throws IOException;
/**
* 导入流程定义
*
* @param file 文件
* @param category 分类
* @return 是否导入成功
*/
boolean importJson(MultipartFile file, String category);
/**
* 删除流程定义
*
* @param ids 流程定义id
* @return 是否删除成功
*/
boolean removeDef(Collection<Long> ids);
}
@@ -0,0 +1,169 @@
package org.dromara.workflow.service;
import org.dromara.common.core.domain.PageResult;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.warm.flow.orm.entity.FlowInstance;
import org.dromara.workflow.domain.bo.FlowCancelBo;
import org.dromara.workflow.domain.bo.FlowInstanceBo;
import org.dromara.workflow.domain.bo.FlowInvalidBo;
import org.dromara.workflow.domain.bo.FlowVariableBo;
import org.dromara.workflow.domain.vo.FlowInstanceVo;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 流程实例 服务层
*
* @author may
*/
public interface IFlwInstanceService {
/**
* 分页查询正在运行的流程实例
*
* @param flowInstanceBo 流程实例
* @param pageQuery 分页
* @return 结果
*/
PageResult<FlowInstanceVo> selectRunningInstanceList(FlowInstanceBo flowInstanceBo, PageQuery pageQuery);
/**
* 分页查询已结束的流程实例
*
* @param flowInstanceBo 流程实例
* @param pageQuery 分页
* @return 结果
*/
PageResult<FlowInstanceVo> selectFinishInstanceList(FlowInstanceBo flowInstanceBo, PageQuery pageQuery);
/**
* 根据业务 id 查询流程实例详情,返回业务侧展示所需信息。
*
* @param businessId 业务id
* @return 结果
*/
FlowInstanceVo queryByBusinessId(Long businessId);
/**
* 按业务 id 查询底层流程实例实体。
*
* @param businessId 业务id
* @return 结果
*/
FlowInstance selectInstByBusinessId(String businessId);
/**
* 按实例 id 查询单个流程实例实体。
*
* @param instanceId 实例id
* @return 结果
*/
FlowInstance selectInstById(Long instanceId);
/**
* 按实例 id 集合批量查询流程实例。
*
* @param instanceIds 实例id
* @return 结果
*/
List<FlowInstance> selectInstListByIdList(Collection<Long> instanceIds);
/**
* 按照业务id删除流程实例
*
* @param businessIds 业务id
* @return 结果
*/
boolean deleteByBusinessIds(List<String> businessIds);
/**
* 按照实例id删除流程实例
*
* @param instanceIds 实例id
* @return 结果
*/
boolean deleteByInstanceIds(Collection<Long> instanceIds);
/**
* 按照实例id删除已完成得流程实例
*
* @param instanceIds 删除的实例id
* @return 删除结果
*/
boolean deleteHisByInstanceIds(Collection<Long> instanceIds);
/**
* 撤销流程
*
* @param bo 参数
* @return 结果
*/
boolean cancelProcessApply(FlowCancelBo bo);
/**
* 获取当前登陆人发起的流程实例
*
* @param instanceBo 流程实例
* @param pageQuery 分页
* @return 结果
*/
PageResult<FlowInstanceVo> selectCurrentInstanceList(FlowInstanceBo instanceBo, PageQuery pageQuery);
/**
* 获取流程图及历史办理记录,供前端展示流转轨迹。
*
* @param businessId 业务id
* @return 结果
*/
Map<String, Object> flowHisTaskList(String businessId);
/**
* 按照实例id更新状态
*
* @param instanceId 实例id
* @param status 状态
*/
void updateStatus(Long instanceId, String status);
/**
* 获取指定实例当前持有的流程变量。
*
* @param instanceId 实例id
* @return 结果
*/
Map<String, Object> instanceVariable(Long instanceId);
/**
* 更新流程变量,支持在运行中修正流程上下文数据。
*
* @param bo 参数
* @return 结果
*/
boolean updateVariable(FlowVariableBo bo);
/**
* 为指定流程实例设置变量集合。
*
* @param instanceId 实例id
* @param variable 流程变量
*/
void setVariable(Long instanceId, Map<String, Object> variable);
/**
* 按任务 id 反查所属流程实例。
*
* @param taskId 任务id
* @return 结果
*/
FlowInstance selectByTaskId(Long taskId);
/**
* 作废流程
*
* @param bo 流程实例
* @return 结果
*/
boolean processInvalid(FlowInvalidBo bo);
}
@@ -0,0 +1,35 @@
package org.dromara.workflow.service;
import org.dromara.workflow.domain.vo.NodeExtVo;
import java.util.Map;
/**
* 流程节点扩展属性 服务层
*
* @author AprilWind
*/
public interface IFlwNodeExtService {
/**
* 解析扩展属性 JSON 并构建 Node 扩展属性对象
* <p>
* 根据传入的 JSON 字符串,将扩展属性分为三类:
* 1. ButtonPermissionEnum:解析为按钮权限列表,标记每个按钮是否勾选
* 2. CopySettingEnum:解析为抄送对象 ID 集合
* 3. VariablesEnum:解析为自定义参数 Map
*
* <p>示例 JSON
* [
* {"code": "ButtonPermissionEnum", "value": "back,termination"},
* {"code": "CopySettingEnum", "value": "1"},
* {"code": "VariablesEnum", "value": "key1=value1,key2=value2"}
* ]
*
* @param ext 扩展属性 JSON 字符串
* @param variable 流程变量
* @return NodeExtVo 对象,封装按钮权限列表、抄送对象集合和自定义参数 Map
*/
NodeExtVo parseNodeExt(String ext, Map<String, Object> variable);
}
@@ -0,0 +1,88 @@
package org.dromara.workflow.service;
import org.dromara.common.core.domain.PageResult;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.system.api.domain.TaskAssigneeDTO;
import org.dromara.system.api.model.TaskAssigneeBody;
import org.dromara.workflow.domain.bo.FlowSpelBo;
import org.dromara.workflow.domain.vo.FlowSpelVo;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/**
* 流程 SpEL 表达式定义服务接口,负责表达式规则管理与动态指派解析。
*
* @author Michelle.Chung
* @date 2025-07-04
*/
public interface IFlwSpelService {
/**
* 根据主键查询流程 SpEL 表达式定义。
*
* @param id 主键
* @return 流程spel表达式定义
*/
FlowSpelVo queryById(Long id);
/**
* 分页查询流程spel表达式定义列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 流程spel表达式定义分页列表
*/
PageResult<FlowSpelVo> queryPageList(FlowSpelBo bo, PageQuery pageQuery);
/**
* 查询符合条件的流程 SpEL 表达式定义列表。
*
* @param bo 查询条件
* @return 流程spel表达式定义列表
*/
List<FlowSpelVo> queryList(FlowSpelBo bo);
/**
* 新增流程 SpEL 表达式定义。
*
* @param bo 流程spel表达式定义
* @return 是否新增成功
*/
Boolean insertByBo(FlowSpelBo bo);
/**
* 修改流程 SpEL 表达式定义。
*
* @param bo 流程spel表达式定义
* @return 是否修改成功
*/
Boolean updateByBo(FlowSpelBo bo);
/**
* 校验并批量删除流程spel表达式定义信息
*
* @param ids 待删除的主键集合
* @param isValid 是否进行有效性校验
* @return 是否删除成功
*/
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
/**
* 按任务指派条件解析 SpEL,并返回可用办理人列表。
*
* @param taskQuery 查询条件
* @return 办理人
*/
TaskAssigneeDTO selectSpelByTaskAssigneeList(TaskAssigneeBody taskQuery);
/**
* 根据视图 SpEL 表达式列表,查询对应的备注信息
*
* @param viewSpels SpEL 表达式列表
* @return 映射表:key 为 SpEL 表达式,value 为对应备注;若为空则返回空 Map
*/
Map<String, String> selectRemarksBySpels(List<String> viewSpels);
}
@@ -0,0 +1,24 @@
package org.dromara.workflow.service;
import org.dromara.system.api.domain.UserDTO;
import java.util.List;
/**
* 流程设计器-获取办理人
*
* @author AprilWind
*/
public interface IFlwTaskAssigneeService {
/**
* 批量解析多个存储标识符(storageIds),按类型分类并合并查询用户列表
* 输入格式支持多个以逗号分隔的标识(如 "user:123,role:456,789"
* 会自动去重返回结果,非法格式的标识将被忽略
*
* @param storageIds 多个存储标识符字符串(逗号分隔)
* @return 合并后的用户列表,去重后返回,非法格式的标识将被跳过
*/
List<UserDTO> fetchUsersByStorageIds(String storageIds);
}
@@ -0,0 +1,218 @@
package org.dromara.workflow.service;
import org.dromara.common.core.domain.PageResult;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.system.api.domain.UserDTO;
import org.dromara.warm.flow.core.entity.Node;
import org.dromara.warm.flow.core.entity.Task;
import org.dromara.warm.flow.orm.entity.FlowHisTask;
import org.dromara.warm.flow.orm.entity.FlowNode;
import org.dromara.warm.flow.orm.entity.FlowTask;
import org.dromara.workflow.api.domain.StartProcessReturnDTO;
import org.dromara.workflow.domain.bo.*;
import org.dromara.workflow.domain.vo.FlowHisTaskVo;
import org.dromara.workflow.domain.vo.FlowTaskVo;
import java.util.Collection;
import java.util.List;
/**
* 任务 服务层
*
* @author may
*/
public interface IFlwTaskService {
/**
* 启动任务
*
* @param startProcessBo 启动流程参数
* @return 结果
*/
StartProcessReturnDTO startWorkFlow(StartProcessBo startProcessBo);
/**
* 办理任务
*
* @param completeTaskBo 办理任务参数
* @return 结果
*/
boolean completeTask(CompleteTaskBo completeTaskBo);
/**
* 添加抄送人
*
* @param task 任务信息
* @param flowCopyList 抄送人
*/
void setCopy(Task task, List<FlowCopyBo> flowCopyList);
/**
* 查询当前用户的待办任务
*
* @param flowTaskBo 参数
* @param pageQuery 分页
* @return 当前用户待办任务分页结果
*/
PageResult<FlowTaskVo> pageByTaskWait(FlowTaskBo flowTaskBo, PageQuery pageQuery);
/**
* 查询当前租户所有待办任务
*
* @param flowTaskBo 参数
* @param pageQuery 分页
* @return 当前用户已办任务分页结果
*/
PageResult<FlowHisTaskVo> pageByTaskFinish(FlowTaskBo flowTaskBo, PageQuery pageQuery);
/**
* 查询待办任务
*
* @param flowTaskBo 参数
* @param pageQuery 分页
* @return 当前租户待办任务分页结果
*/
PageResult<FlowTaskVo> pageByAllTaskWait(FlowTaskBo flowTaskBo, PageQuery pageQuery);
/**
* 查询已办任务
*
* @param flowTaskBo 参数
* @param pageQuery 分页
* @return 当前租户已办任务分页结果
*/
PageResult<FlowHisTaskVo> pageByAllTaskFinish(FlowTaskBo flowTaskBo, PageQuery pageQuery);
/**
* 查询当前用户的抄送
*
* @param flowTaskBo 参数
* @param pageQuery 分页
* @return 当前用户抄送分页结果
*/
PageResult<FlowTaskVo> pageByTaskCopy(FlowTaskBo flowTaskBo, PageQuery pageQuery);
/**
* 修改任务办理人
*
* @param taskIdList 任务id
* @param userId 用户id
* @return 结果
*/
boolean updateAssignee(Collection<Long> taskIdList, String userId);
/**
* 驳回审批
*
* @param bo 参数
* @return 结果
*/
boolean backProcess(BackProcessBo bo);
/**
* 获取可驳回的前置节点
*
* @param taskId 任务id
* @param nowNodeCode 当前节点
* @return 结果
*/
List<Node> getBackTaskNode(Long taskId, String nowNodeCode);
/**
* 终止任务
*
* @param bo 参数
* @return 结果
*/
boolean terminationTask(FlowTerminationBo bo);
/**
* 按照任务id查询任务
*
* @param taskIdList 任务id
* @return 任务列表
*/
List<FlowTask> selectByIdList(Collection<Long> taskIdList);
/**
* 按照任务id查询任务
*
* @param taskId 任务id
* @return 任务详情
*/
FlowTaskVo selectById(Long taskId);
/**
* 获取下一节点信息
*
* @param bo 参数
* @return 下一节点列表
*/
List<FlowNode> getNextNodeList(FlowNextNodeBo bo);
/**
* 按照任务id查询任务
*
* @param taskId 任务id
* @return 历史任务详情
*/
FlowHisTask selectHisTaskById(Long taskId);
/**
* 按照实例id查询任务
*
* @param instanceId 流程实例id
* @return 任务列表
*/
List<FlowTask> selectByInstId(Long instanceId);
/**
* 按照实例id查询任务
*
* @param instanceIds 列表
* @return 任务列表
*/
List<FlowTask> selectByInstIds(Collection<Long> instanceIds);
/**
* 判断流程是否已结束(即该流程实例下是否还有未完成的任务)
*
* @param instanceId 流程实例ID
* @return true 表示任务已全部结束;false 表示仍有任务存在
*/
boolean isTaskEnd(Long instanceId);
/**
* 任务操作
*
* @param bo 参数
* @param taskOperation 操作类型,委派 delegateTask、转办 transferTask、加签 addSignature、减签 reductionSignature
* @return 结果
*/
boolean taskOperation(TaskOperationBo bo, String taskOperation);
/**
* 获取当前任务的所有办理人
*
* @param taskIds 任务id
* @return 办理人列表
*/
List<UserDTO> currentTaskAllUser(List<Long> taskIds);
/**
* 按照节点编码查询节点
*
* @param nodeCode 节点编码
* @param definitionId 流程定义id
* @return 节点
*/
FlowNode getByNodeCode(String nodeCode, Long definitionId);
/**
* 催办任务
*
* @param bo 参数
* @return 是否催办成功
*/
boolean urgeTask(FlowUrgeTaskBo bo);
}
@@ -0,0 +1,75 @@
package org.dromara.workflow.service;
import org.dromara.common.core.domain.PageResult;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.workflow.domain.bo.TestLeaveBo;
import org.dromara.workflow.domain.vo.TestLeaveVo;
import java.util.Collection;
import java.util.List;
/**
* 请假示例服务接口,定义请假单查询、维护与流程发起能力。
*
* @author may
* @date 2023-07-21
*/
public interface ITestLeaveService {
/**
* 根据主键查询请假单。
*
* @param id 主键
* @return 请假详情
*/
TestLeaveVo queryById(Long id);
/**
* 分页查询请假列表。
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 请假分页数据
*/
PageResult<TestLeaveVo> queryPageList(TestLeaveBo bo, PageQuery pageQuery);
/**
* 查询符合条件的请假列表。
*
* @param bo 查询条件
* @return 请假列表
*/
List<TestLeaveVo> queryList(TestLeaveBo bo);
/**
* 新增请假单。
*
* @param bo 请假信息
* @return 新增结果
*/
TestLeaveVo insertByBo(TestLeaveBo bo);
/**
* 提交请假单并发起审批流程。
*
* @param bo 请假信息
* @return 提交结果
*/
TestLeaveVo submitAndFlowStart(TestLeaveBo bo);
/**
* 修改请假单。
*
* @param bo 请假信息
* @return 修改结果
*/
TestLeaveVo updateByBo(TestLeaveBo bo);
/**
* 校验并批量删除请假信息。
*
* @param ids 主键集合
* @return 删除结果
*/
Boolean deleteWithValidByIds(Collection<Long> ids);
}
@@ -0,0 +1,78 @@
package org.dromara.workflow.service.impl;
import cn.hutool.core.convert.Convert;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.translation.annotation.TranslationType;
import org.dromara.common.translation.core.TranslationInterface;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.constant.FlowConstant;
import org.dromara.workflow.service.IFlwCategoryService;
import org.springframework.stereotype.Service;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
/**
* 流程分类名称翻译实现
*
* @author AprilWind
*/
@ConditionalOnEnable
@Slf4j
@RequiredArgsConstructor
@Service
@TranslationType(type = FlowConstant.CATEGORY_ID_TO_NAME)
public class CategoryNameTranslationImpl implements TranslationInterface<String> {
private final IFlwCategoryService flwCategoryService;
/**
* 将流程分类 ID 翻译为分类名称。
*
* @param key 流程分类 ID
* @param other 额外参数
* @return 流程分类名称
*/
@Override
public String translation(Object key, String other) {
return flwCategoryService.selectCategoryNameById(Convert.toLong(key));
}
/**
* 批量将流程分类 ID 翻译为分类名称。
*
* @param keys 流程分类 ID 集合
* @param other 额外参数
* @return 流程分类 ID 与分类名称映射
*/
@Override
public Map<Object, String> translationBatch(Set<Object> keys, String other) {
Set<Long> categoryIds = collectLongIds(keys);
if (categoryIds.isEmpty()) {
return Map.of();
}
Map<Long, String> categoryNames = flwCategoryService.selectCategoryNameByIds(categoryIds);
Map<Object, String> result = new LinkedHashMap<>(keys.size());
for (Object key : keys) {
result.put(key, buildValue(key, categoryNames));
}
return result;
}
/**
* 根据原始键构建流程分类名称翻译值。
*
* @param source 原始键
* @param categoryNames 流程分类 ID 与分类名称映射
* @return 流程分类名称
*/
private String buildValue(Object source, Map<Long, String> categoryNames) {
if (source instanceof String ids) {
return joinMappedValues(ids, categoryNames::get);
}
return source == null ? null : categoryNames.get(Convert.toLong(source));
}
}
@@ -0,0 +1,286 @@
package org.dromara.workflow.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.tree.Tree;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.*;
import org.dromara.warm.flow.core.service.DefService;
import org.dromara.warm.flow.orm.entity.FlowDefinition;
import org.dromara.warm.flow.ui.service.CategoryService;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.constant.FlowConstant;
import org.dromara.workflow.domain.FlowCategory;
import org.dromara.workflow.domain.bo.FlowCategoryBo;
import org.dromara.workflow.domain.vo.FlowCategoryVo;
import org.dromara.workflow.mapper.FlwCategoryMapper;
import org.dromara.workflow.service.IFlwCategoryService;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
/**
* 流程分类Service业务层处理
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@Service
public class FlwCategoryServiceImpl implements IFlwCategoryService, CategoryService {
private final DefService defService;
private final FlwCategoryMapper categoryMapper;
/**
* 查询流程分类
*
* @param categoryId 主键
* @return 流程分类
*/
@Override
public FlowCategoryVo queryById(Long categoryId) {
return categoryMapper.selectVoById(categoryId);
}
/**
* 根据流程分类ID查询流程分类名称
*
* @param categoryId 流程分类ID
* @return 流程分类名称
*/
@Cacheable(cacheNames = FlowConstant.FLOW_CATEGORY_NAME, key = "#categoryId")
@Override
public String selectCategoryNameById(Long categoryId) {
if (ObjectUtil.isNull(categoryId)) {
return null;
}
FlowCategory category = categoryMapper.lambda()
.select(FlowCategory::getCategoryName)
.eq(FlowCategory::getCategoryId, categoryId)
.one();
return ObjectUtils.notNullGetter(category, FlowCategory::getCategoryName);
}
/**
* 根据流程分类ID查询流程分类名称
*
* @param categoryIds 流程分类ID
* @return 流程分类名称
*/
@Override
public Map<Long, String> selectCategoryNameByIds(Set<Long> categoryIds) {
if (CollUtil.isEmpty(categoryIds)) {
return Collections.emptyMap();
}
List<FlowCategory> list = categoryMapper.lambda()
.select(FlowCategory::getCategoryId, FlowCategory::getCategoryName)
.in(FlowCategory::getCategoryId, categoryIds)
.list();
return StreamUtils.toMap(list, FlowCategory::getCategoryId, FlowCategory::getCategoryName);
}
/**
* 查询符合条件的流程分类列表
*
* @param bo 查询条件
* @return 流程分类列表
*/
@Override
public List<FlowCategoryVo> queryList(FlowCategoryBo bo) {
LambdaQueryWrapper<FlowCategory> lqw = buildQueryWrapper(bo);
return categoryMapper.selectVoList(lqw);
}
/**
* 查询流程分类树结构信息
*
* @param category 流程分类信息
* @return 流程分类树信息集合
*/
@Override
public List<Tree<String>> selectCategoryTreeList(FlowCategoryBo category) {
List<FlowCategoryVo> categoryList = this.queryList(category);
if (CollUtil.isEmpty(categoryList)) {
return CollUtil.newArrayList();
}
return TreeBuildUtils.buildMultiRoot(
categoryList,
node -> Convert.toStr(node.getCategoryId()),
node -> Convert.toStr(node.getParentId()),
(node, treeNode) -> treeNode
.setId(Convert.toStr(node.getCategoryId()))
.setParentId(Convert.toStr(node.getParentId()))
.setName(node.getCategoryName())
.setWeight(node.getOrderNum())
);
}
/**
* 工作流查询分类
*
* @return 分类树结构列表
*/
@Override
public List<org.dromara.warm.flow.core.dto.Tree> queryCategory() {
List<FlowCategoryVo> list = this.queryList(new FlowCategoryBo());
return StreamUtils.toList(list, category -> new org.dromara.warm.flow.core.dto.Tree()
.setId(Convert.toStr(category.getCategoryId()))
.setName(category.getCategoryName())
.setParentId(Convert.toStr(category.getParentId()))
);
}
/**
* 校验流程分类名称是否唯一
*
* @param category 流程分类信息
* @return 结果
*/
@Override
public boolean checkCategoryNameUnique(FlowCategoryBo category) {
boolean exist = categoryMapper.lambda()
.eq(FlowCategory::getCategoryName, category.getCategoryName())
.eq(FlowCategory::getParentId, category.getParentId())
.neIfPresent(FlowCategory::getCategoryId, category.getCategoryId())
.exists();
return !exist;
}
/**
* 查询流程分类是否存在流程定义
*
* @param categoryId 流程分类ID
* @return 结果 true 存在 false 不存在
*/
@Override
public boolean checkCategoryExistDefinition(Long categoryId) {
FlowDefinition definition = new FlowDefinition();
definition.setCategory(categoryId.toString());
return defService.exists(definition);
}
/**
* 是否存在流程分类子节点
*
* @param categoryId 流程分类ID
* @return 结果
*/
@Override
public boolean hasChildByCategoryId(Long categoryId) {
return categoryMapper.lambda().eq(FlowCategory::getParentId, categoryId).exists();
}
/**
* 构建流程分类查询条件。
*
* @param bo 查询条件
* @return 查询条件包装器
*/
private LambdaQueryWrapper<FlowCategory> buildQueryWrapper(FlowCategoryBo bo) {
LambdaQueryWrapper<FlowCategory> lqw = Wrappers.lambdaQuery();
lqw.eq(FlowCategory::getDelFlag, SystemConstants.NORMAL);
lqw.eq(ObjectUtil.isNotNull(bo.getCategoryId()), FlowCategory::getCategoryId, bo.getCategoryId());
lqw.eq(ObjectUtil.isNotNull(bo.getParentId()), FlowCategory::getParentId, bo.getParentId());
lqw.like(StringUtils.isNotBlank(bo.getCategoryName()), FlowCategory::getCategoryName, bo.getCategoryName());
lqw.orderByAsc(FlowCategory::getAncestors);
lqw.orderByAsc(FlowCategory::getParentId);
lqw.orderByAsc(FlowCategory::getOrderNum);
lqw.orderByAsc(FlowCategory::getCategoryId);
return lqw;
}
/**
* 新增流程分类
*
* @param bo 流程分类
* @return 是否新增成功
*/
@Override
public int insertByBo(FlowCategoryBo bo) {
FlowCategory info = categoryMapper.selectById(bo.getParentId());
if (ObjectUtil.isNull(info)) {
throw new ServiceException("父级流程分类不存在!");
}
FlowCategory category = MapstructUtils.convert(bo, FlowCategory.class);
category.setAncestors(info.getAncestors() + StringUtils.SEPARATOR + category.getParentId());
return categoryMapper.insert(category);
}
/**
* 修改流程分类
*
* @param bo 流程分类
* @return 是否修改成功
*/
@CacheEvict(cacheNames = FlowConstant.FLOW_CATEGORY_NAME, key = "#bo.categoryId")
@Override
@Transactional(rollbackFor = Exception.class)
public int updateByBo(FlowCategoryBo bo) {
FlowCategory category = MapstructUtils.convert(bo, FlowCategory.class);
FlowCategory oldCategory = categoryMapper.selectById(category.getCategoryId());
if (ObjectUtil.isNull(oldCategory)) {
throw new ServiceException("流程分类不存在,无法修改");
}
if (oldCategory.getParentId() == 0L && category.getParentId() != 0L) {
throw new ServiceException("不允许修改顶级分类的父级节点");
}
if (!oldCategory.getParentId().equals(category.getParentId())) {
FlowCategory newParentCategory = categoryMapper.selectById(category.getParentId());
if (ObjectUtil.isNotNull(newParentCategory)) {
String newAncestors = newParentCategory.getAncestors() + StringUtils.SEPARATOR + newParentCategory.getCategoryId();
String oldAncestors = oldCategory.getAncestors();
category.setAncestors(newAncestors);
updateCategoryChildren(category.getCategoryId(), newAncestors, oldAncestors);
} else {
throw new ServiceException("父级流程分类不存在!");
}
} else {
category.setAncestors(oldCategory.getAncestors());
}
return categoryMapper.updateById(category);
}
/**
* 修改子元素关系
*
* @param categoryId 被修改的流程分类ID
* @param newAncestors 新的父ID集合
* @param oldAncestors 旧的父ID集合
*/
private void updateCategoryChildren(Long categoryId, String newAncestors, String oldAncestors) {
List<FlowCategory> children = categoryMapper.lambda()
.findInSet(categoryId, FlowCategory::getAncestors)
.list();
List<FlowCategory> list = new ArrayList<>();
for (FlowCategory child : children) {
FlowCategory category = new FlowCategory();
category.setCategoryId(child.getCategoryId());
category.setAncestors(child.getAncestors().replaceFirst(oldAncestors, newAncestors));
list.add(category);
}
if (CollUtil.isNotEmpty(list)) {
categoryMapper.updateBatchById(list);
}
}
/**
* 删除流程分类信息
*
* @param categoryId 主键
* @return 是否删除成功
*/
@CacheEvict(cacheNames = FlowConstant.FLOW_CATEGORY_NAME, key = "#categoryId")
@Override
public int deleteWithValidById(Long categoryId) {
return categoryMapper.deleteById(categoryId);
}
}
@@ -0,0 +1,273 @@
package org.dromara.workflow.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.service.DictService;
import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.system.api.DeptService;
import org.dromara.system.api.UserService;
import org.dromara.system.api.domain.UserDTO;
import org.dromara.warm.flow.core.dto.DefJson;
import org.dromara.warm.flow.core.dto.NodeJson;
import org.dromara.warm.flow.core.dto.PromptContent;
import org.dromara.warm.flow.core.enums.NodeType;
import org.dromara.warm.flow.core.utils.MapUtil;
import org.dromara.warm.flow.orm.entity.FlowHisTask;
import org.dromara.warm.flow.orm.mapper.FlowHisTaskMapper;
import org.dromara.warm.flow.ui.service.ChartExtService;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.constant.FlowConstant;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* 流程图提示信息
*
* @author AprilWind
*/
@ConditionalOnEnable
@Slf4j
@RequiredArgsConstructor
@Service
public class FlwChartExtServiceImpl implements ChartExtService {
private final UserService userService;
private final DeptService deptService;
private final FlowHisTaskMapper flowHisTaskMapper;
private final DictService dictService;
@Value("${warm-flow.node-tooltip:true}")
private boolean nodeTooltip;
/**
* 设置流程图提示信息
*
* @param defJson 流程定义json对象
*/
@Override
public void execute(DefJson defJson) {
// 配置关闭,直接返回,不渲染悬浮窗
if (!nodeTooltip) {
return;
}
// 根据流程实例ID查询所有相关的历史任务列表
List<FlowHisTask> flowHisTasks = this.getHisTaskGroupedByNode(defJson.getInstance().getId());
if (CollUtil.isEmpty(flowHisTasks)) {
return;
}
// 按节点编号(nodeCode)对历史任务进行分组
Map<String, List<FlowHisTask>> groupedByNode = StreamUtils.groupByKey(flowHisTasks, FlowHisTask::getNodeCode);
// 批量查询所有审批人的用户信息
List<UserDTO> userDTOList = userService.selectListByIds(StreamUtils.toSet(flowHisTasks, e -> Convert.toLong(e.getApprover())));
// 将查询到的用户列表转换为以用户ID为key的映射
Map<Long, UserDTO> userMap = StreamUtils.toIdentityMap(userDTOList, UserDTO::getUserId);
Set<Long> deptIds = StreamUtils.toSet(userDTOList, UserDTO::getDeptId);
Map<Long, String> deptNameMap = deptService.selectDeptNamesByIds(deptIds);
Map<String, String> dictType = dictService.getAllDictByDictType(FlowConstant.WF_TASK_STATUS);
for (NodeJson nodeJson : defJson.getNodeList()) {
List<FlowHisTask> taskList = groupedByNode.get(nodeJson.getNodeCode());
if (CollUtil.isEmpty(taskList)) {
continue;
}
// 按审批人分组去重,保留最新处理记录,最终转换成 List
List<FlowHisTask> latestPerApprover = taskList.stream()
.collect(Collectors.collectingAndThen(
Collectors.toMap(
FlowHisTask::getApprover,
Function.identity(),
(oldTask, newTask) -> newTask.getUpdateTime().after(oldTask.getUpdateTime()) ? newTask : oldTask,
LinkedHashMap::new
),
map -> new ArrayList<>(map.values())
));
// 处理当前节点的扩展信息
this.processNodeExtInfo(nodeJson, latestPerApprover, userMap, deptNameMap, dictType);
}
}
/**
* 初始化流程图提示信息
*
* @param defJson 流程定义json对象
*/
@Override
public void initPromptContent(DefJson defJson) {
// 配置关闭,直接返回,不渲染悬浮窗
if (!nodeTooltip) {
return;
}
defJson.setTopText("流程名称: " + defJson.getFlowName());
defJson.getNodeList().forEach(nodeJson -> {
nodeJson.setPromptContent(
new PromptContent()
// 提示信息
.setInfo(
CollUtil.newArrayList(
new PromptContent.InfoItem()
.setPrefix("任务名称: ")
.setContent(nodeJson.getNodeName())
.setContentStyle(Map.of(
"border", "1px solid #d1e9ff",
"backgroundColor", "#e8f4ff",
"padding", "4px 8px",
"borderRadius", "4px"
))
.setRowStyle(Map.of(
"fontWeight", "bold",
"margin", "0 0 6px 0",
"padding", "0 0 8px 0",
"borderBottom", "1px solid #ccc"
))
)
)
// 弹窗样式
.setDialogStyle(MapUtil.mergeAll(
"position", "absolute",
"backgroundColor", "#fff",
"border", "1px solid #ccc",
"borderRadius", "4px",
"boxShadow", "0 2px 8px rgba(0, 0, 0, 0.15)",
"padding", "8px 12px",
"fontSize", "14px",
"zIndex", "1000",
"maxWidth", "500px",
"maxHeight", "300px",
"overflowY", "auto",
"overflowX", "hidden",
"color", "#333",
"pointerEvents", "auto",
"scrollbarWidth", "thin"
))
);
});
}
/**
* 处理节点的扩展信息,构建用于流程图悬浮提示的内容
*
* @param nodeJson 当前流程节点对象,包含节点基础信息和提示内容容器
* @param taskList 当前节点关联的历史审批任务列表,用于生成提示信息
* @param userMap 用户信息映射表,key 为用户ID,value 为用户DTO对象,用于获取审批人信息
* @param deptNameMap 部门名称映射表,key 为部门ID,value 为部门名称
* @param dictType 数据字典映射表,key 为字典项编码,value 为对应显示值,用于翻译审批状态等
*/
private void processNodeExtInfo(NodeJson nodeJson, List<FlowHisTask> taskList, Map<Long, UserDTO> userMap,
Map<Long, String> deptNameMap, Map<String, String> dictType) {
// 获取节点提示内容对象中的 info 列表,用于追加提示项
List<PromptContent.InfoItem> info = nodeJson.getPromptContent().getInfo();
// 遍历所有任务记录,构建提示内容
for (FlowHisTask task : taskList) {
UserDTO userDTO = userMap.get(Convert.toLong(task.getApprover()));
if (ObjectUtil.isEmpty(userDTO)) {
continue;
}
String deptName = deptNameMap.getOrDefault(userDTO.getDeptId(), StringUtils.EMPTY);
// 添加标题项,如:👤 张三(市场部)
info.add(new PromptContent.InfoItem()
.setPrefix(StringUtils.format("👥 {}{}", userDTO.getNickName(), deptName))
.setPrefixStyle(Map.of(
"fontWeight", "bold",
"fontSize", "15px",
"color", "#333"
))
.setRowStyle(Map.of(
"margin", "8px 0",
"borderBottom", "1px dashed #ccc"
))
);
// 添加具体信息项:账号、耗时、时间
info.add(buildInfoItem("用户账号", userDTO.getUserName()));
info.add(buildInfoItem("审批状态", dictType.get(task.getFlowStatus())));
info.add(buildInfoItem("审批耗时", DateUtils.formatBetweenBySecond(task.getUpdateTime(), task.getCreateTime())));
info.add(buildInfoItem("办理时间", DateUtils.formatDateTime(task.getUpdateTime())));
}
}
/**
* 构建单条提示内容对象 InfoItem,用于悬浮窗显示(key: value
*
* @param key 字段名(作为前缀)
* @param value 字段值
* @return 提示项对象
*/
private PromptContent.InfoItem buildInfoItem(String key, String value) {
return new PromptContent.InfoItem()
// 前缀
.setPrefix(key + ": ")
// 前缀样式
.setPrefixStyle(Map.of(
"textAlign", "right",
"color", "#444",
"userSelect", "none",
"display", "inline-block",
"width", "100px",
"paddingRight", "8px",
"fontWeight", "500",
"fontSize", "14px",
"lineHeight", "24px",
"verticalAlign", "middle"
))
// 内容
.setContent(value)
// 内容样式
.setContentStyle(Map.of(
"backgroundColor", "#f7faff",
"color", "#005cbf",
"padding", "4px 8px",
"fontSize", "14px",
"borderRadius", "4px",
"whiteSpace", "normal",
"border", "1px solid #d0e5ff",
"userSelect", "text",
"lineHeight", "20px"
))
// 行样式
.setRowStyle(Map.of(
"color", "#222",
"alignItems", "center",
"display", "flex",
"marginBottom", "6px",
"fontWeight", "400",
"fontSize", "14px"
));
}
/**
* 根据流程实例ID获取历史任务列表
*
* @param instanceId 流程实例ID
* @return 按更新时间倒序排列的审批历史任务列表
*/
public List<FlowHisTask> getHisTaskGroupedByNode(Long instanceId) {
LambdaQueryWrapper<FlowHisTask> wrapper = Wrappers.lambdaQuery();
wrapper.eq(FlowHisTask::getInstanceId, instanceId)
.eq(FlowHisTask::getNodeType, NodeType.BETWEEN.getKey())
.orderByDesc(FlowHisTask::getUpdateTime);
return flowHisTaskMapper.selectList(wrapper);
}
}

Some files were not shown because too many files have changed in this diff Show More