init: init proj

This commit is contained in:
2025-11-13 17:11:19 +08:00
commit 6388f00388
1228 changed files with 116748 additions and 0 deletions

View File

@@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-modules</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<packaging>jar</packaging>
<artifactId>ruoyi-workflow</artifactId>
<description>
工作流模块
</description>
<dependencies>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-sse</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>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-web</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-log</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-idempotent</artifactId>
</dependency>
<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-tenant</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-security</artifactId>
</dependency>
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-mybatis-plus-sb3-starter</artifactId>
</dependency>
<dependency>
<groupId>org.dromara.warm</groupId>
<artifactId>warm-flow-plugin-ui-sb-web</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -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 {
}

View File

@@ -0,0 +1,95 @@
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";
/**
* 委托
*/
String DELEGATE_TASK = "delegateTask";
/**
* 转办
*/
String TRANSFER_TASK = "transferTask";
/**
* 加签
*/
String ADD_SIGNATURE = "addSignature";
/**
* 减签
*/
String REDUCTION_SIGNATURE = "reductionSignature";
/**
* 流程分类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 WF_TASK_STATUS = "wf_task_status";
/**
* 自动通过
*/
String AUTO_PASS = "autoPass";
/**
* 业务编码
*/
String BUSINESS_CODE = "businessCode";
}

View File

@@ -0,0 +1,65 @@
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;
}

View File

@@ -0,0 +1,20 @@
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;
}

View File

@@ -0,0 +1,53 @@
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()));
/**
* 根据消息类型 code 获取 MessageTypeEnum
*
* @param code 消息类型code
* @return MessageTypeEnum
*/
public static MessageTypeEnum getByCode(String code) {
return MESSAGE_TYPE_ENUM_MAP.getOrDefault(code, null);
}
}

View File

@@ -0,0 +1,32 @@
package org.dromara.workflow.common.enums;
/**
* 节点扩展属性枚举
*
* @author AprilWind
*/
public interface NodeExtEnum {
/**
* 选项label
*
* @return 选项label
*/
String getLabel();
/**
* 选项值
*
* @return 选项值
*/
String getValue();
/**
* 是否默认选中
*
* @return 是否默认选中
*/
boolean isSelected();
}

View File

@@ -0,0 +1,140 @@
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);
}
/**
* 获取所有办理人类型的描述列表
* <p>
* 获取当前枚举类所有项的描述字段列表,通常用于展示选择项。
* </p>
*
* @return List<String> 返回所有办理人类型的描述列表
*/
public static List<String> getAssigneeTypeList() {
return Arrays.stream(values())
.map(TaskAssigneeEnum::getDesc)
.collect(Collectors.toList());
}
/**
* 获取所有办理人类型的代码列表
* <p>
* 获取当前枚举类所有项的代码字段列表,通常用于程序内部逻辑的判断。
* </p>
*
* @return List<String> 返回所有办理人类型的代码列表
*/
public static List<String> getAssigneeCodeList() {
return Arrays.stream(values())
.map(TaskAssigneeEnum::getCode)
.collect(Collectors.toList());
}
/**
* 判断当前办理人类型是否需要调用部门服务deptService
*
* @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, "#");
}
}

View File

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

View File

@@ -0,0 +1,114 @@
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 final String status;
/**
* 描述
*/
private final String desc;
private static final Map<String, String> STATUS_DESC_MAP = Arrays.stream(values())
.collect(Collectors.toConcurrentMap(TaskStatusEnum::getStatus, TaskStatusEnum::getDesc));
/**
* 任务业务状态
*
* @param status 状态
*/
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);
}
}

View File

@@ -0,0 +1,20 @@
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;
}

View File

@@ -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 {
}

View File

@@ -0,0 +1,134 @@
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.ExcelUtil;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.web.core.BaseController;
import org.dromara.workflow.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;
/**
* 查询流程分类列表
*/
@SaCheckPermission("workflow:category:list")
@GetMapping("/list")
public R<List<FlowCategoryVo>> list(FlowCategoryBo bo) {
List<FlowCategoryVo> list = flwCategoryService.queryList(bo);
return R.ok(list);
}
/**
* 导出流程分类列表
*/
@SaCheckPermission("workflow:category:export")
@Log(title = "流程分类", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(FlowCategoryBo bo, HttpServletResponse response) {
List<FlowCategoryVo> list = flwCategoryService.queryList(bo);
ExcelUtil.exportExcel(list, "流程分类", FlowCategoryVo.class, response);
}
/**
* 获取流程分类详细信息
*
* @param categoryId 主键
*/
@SaCheckPermission("workflow:category:query")
@GetMapping("/{categoryId}")
public R<FlowCategoryVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long categoryId) {
return R.ok(flwCategoryService.queryById(categoryId));
}
/**
* 新增流程分类
*/
@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));
}
/**
* 修改流程分类
*/
@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 主键
*/
@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 流程分类
*/
@GetMapping("/categoryTree")
public R<List<Tree<String>>> categoryTree(FlowCategoryBo categoryBo) {
return R.ok(flwCategoryService.selectCategoryTreeList(categoryBo));
}
}

View File

@@ -0,0 +1,194 @@
package org.dromara.workflow.controller;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
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 分页
*/
@GetMapping("/list")
public TableDataInfo<FlowDefinitionVo> list(FlowDefinition flowDefinition, PageQuery pageQuery) {
return flwDefinitionService.queryList(flowDefinition, pageQuery);
}
/**
* 查询未发布的流程定义列表
*
* @param flowDefinition 参数
* @param pageQuery 分页
*/
@GetMapping("/unPublishList")
public TableDataInfo<FlowDefinitionVo> unPublishList(FlowDefinition flowDefinition, PageQuery pageQuery) {
return flwDefinitionService.unPublishList(flowDefinition, pageQuery);
}
/**
* 获取流程定义详细信息
*
* @param id 流程定义id
*/
@GetMapping(value = "/{id}")
public R<Definition> getInfo(@PathVariable Long id) {
return R.ok(defService.getById(id));
}
/**
* 新增流程定义
*
* @param flowDefinition 参数
*/
@Log(title = "流程定义", businessType = BusinessType.INSERT)
@PostMapping
@RepeatSubmit()
@Transactional(rollbackFor = Exception.class)
public R<Boolean> add(@RequestBody FlowDefinition flowDefinition) {
return R.ok(defService.checkAndSave(flowDefinition));
}
/**
* 修改流程定义
*
* @param flowDefinition 参数
*/
@Log(title = "流程定义", businessType = BusinessType.UPDATE)
@PutMapping
@RepeatSubmit()
@Transactional(rollbackFor = Exception.class)
public R<Boolean> edit(@RequestBody FlowDefinition flowDefinition) {
return R.ok(defService.updateById(flowDefinition));
}
/**
* 发布流程定义
*
* @param id 流程定义id
*/
@Log(title = "流程定义", businessType = BusinessType.INSERT)
@PutMapping("/publish/{id}")
@RepeatSubmit()
public R<Boolean> publish(@PathVariable Long id) {
return R.ok(flwDefinitionService.publish(id));
}
/**
* 取消发布流程定义
*
* @param id 流程定义id
*/
@Log(title = "流程定义", businessType = BusinessType.INSERT)
@PutMapping("/unPublish/{id}")
@RepeatSubmit()
@Transactional(rollbackFor = Exception.class)
public R<Boolean> unPublish(@PathVariable Long id) {
return R.ok(defService.unPublish(id));
}
/**
* 删除流程定义
*/
@Log(title = "流程定义", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}")
public R<Void> remove(@PathVariable List<Long> ids) {
return toAjax(flwDefinitionService.removeDef(ids));
}
/**
* 复制流程定义
*
* @param id 流程定义id
*/
@Log(title = "流程定义", businessType = BusinessType.INSERT)
@PostMapping("/copy/{id}")
@RepeatSubmit()
@Transactional(rollbackFor = Exception.class)
public R<Boolean> copy(@PathVariable Long id) {
return R.ok(defService.copyDef(id));
}
/**
* 导入流程定义
*
* @param file 文件
* @param category 分类
*/
@Log(title = "流程定义", businessType = BusinessType.IMPORT)
@PostMapping("/importDef")
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}")
public void exportDef(@PathVariable Long id, HttpServletResponse response) throws IOException {
flwDefinitionService.exportDef(id, response);
}
/**
* 获取流程定义JSON字符串
*
* @param id 流程定义id
*/
@GetMapping("/xmlString/{id}")
public R<String> xmlString(@PathVariable Long id) {
return R.ok("操作成功", defService.exportJson(id));
}
/**
* 激活/挂起流程定义
*
* @param id 流程定义id
* @param active 激活/挂起
*/
@RepeatSubmit()
@PutMapping("/active/{id}")
@Transactional(rollbackFor = Exception.class)
public R<Boolean> active(@PathVariable Long id, @RequestParam boolean active) {
return R.ok(active ? defService.active(id) : defService.unActive(id));
}
}

View File

@@ -0,0 +1,179 @@
package org.dromara.workflow.controller;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
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 分页
*/
@GetMapping("/pageByRunning")
public TableDataInfo<FlowInstanceVo> selectRunningInstanceList(FlowInstanceBo flowInstanceBo, PageQuery pageQuery) {
return flwInstanceService.selectRunningInstanceList(flowInstanceBo, pageQuery);
}
/**
* 查询已结束的流程实例列表
*
* @param flowInstanceBo 流程实例
* @param pageQuery 分页
*/
@GetMapping("/pageByFinish")
public TableDataInfo<FlowInstanceVo> selectFinishInstanceList(FlowInstanceBo flowInstanceBo, PageQuery pageQuery) {
return flwInstanceService.selectFinishInstanceList(flowInstanceBo, pageQuery);
}
/**
* 根据业务id查询流程实例详细信息
*
* @param businessId 业务id
*/
@GetMapping("/getInfo/{businessId}")
public R<FlowInstanceVo> getInfo(@PathVariable Long businessId) {
return R.ok(flwInstanceService.queryByBusinessId(businessId));
}
/**
* 按照业务id删除流程实例
*
* @param businessIds 业务id
*/
@DeleteMapping("/deleteByBusinessIds/{businessIds}")
public R<Void> deleteByBusinessIds(@PathVariable List<Long> businessIds) {
return toAjax(flwInstanceService.deleteByBusinessIds(businessIds));
}
/**
* 按照实例id删除流程实例
*
* @param instanceIds 实例id
*/
@DeleteMapping("/deleteByInstanceIds/{instanceIds}")
public R<Void> deleteByInstanceIds(@PathVariable List<Long> instanceIds) {
return toAjax(flwInstanceService.deleteByInstanceIds(instanceIds));
}
/**
* 按照实例id删除已完成得流程实例
*
* @param instanceIds 实例id
*/
@DeleteMapping("/deleteHisByInstanceIds/{instanceIds}")
public R<Void> deleteHisByInstanceIds(@PathVariable List<Long> instanceIds) {
return toAjax(flwInstanceService.deleteHisByInstanceIds(instanceIds));
}
/**
* 撤销流程
*
* @param bo 参数
*/
@RepeatSubmit()
@PutMapping("/cancelProcessApply")
public R<Void> cancelProcessApply(@RequestBody FlowCancelBo bo) {
return toAjax(flwInstanceService.cancelProcessApply(bo));
}
/**
* 激活/挂起流程实例
*
* @param id 流程实例id
* @param active 激活/挂起
*/
@RepeatSubmit()
@PutMapping("/active/{id}")
public R<Boolean> active(@PathVariable Long id, @RequestParam boolean active) {
return R.ok(active ? insService.active(id) : insService.unActive(id));
}
/**
* 获取当前登陆人发起的流程实例
*
* @param flowInstanceBo 参数
* @param pageQuery 分页
*/
@GetMapping("/pageByCurrent")
public TableDataInfo<FlowInstanceVo> selectCurrentInstanceList(FlowInstanceBo flowInstanceBo, PageQuery pageQuery) {
return flwInstanceService.selectCurrentInstanceList(flowInstanceBo, pageQuery);
}
/**
* 获取流程图,流程记录
*
* @param businessId 业务id
*/
@GetMapping("/flowHisTaskList/{businessId}")
public R<Map<String, Object>> flowHisTaskList(@PathVariable String businessId) {
return R.ok(flwInstanceService.flowHisTaskList(businessId));
}
/**
* 获取流程变量
*
* @param instanceId 流程实例id
*/
@GetMapping("/instanceVariable/{instanceId}")
public R<Map<String, Object>> instanceVariable(@PathVariable Long instanceId) {
return R.ok(flwInstanceService.instanceVariable(instanceId));
}
/**
* 修改流程变量
*
* @param bo 参数
*/
@RepeatSubmit()
@PutMapping("/updateVariable")
public R<Void> updateVariable(@Validated @RequestBody FlowVariableBo bo) {
return toAjax(flwInstanceService.updateVariable(bo));
}
/**
* 作废流程
*
* @param bo 参数
*/
@Log(title = "流程实例管理", businessType = BusinessType.INSERT)
@RepeatSubmit()
@PostMapping("/invalid")
public R<Boolean> invalid(@Validated @RequestBody FlowInvalidBo bo) {
return R.ok(flwInstanceService.processInvalid(bo));
}
}

View File

@@ -0,0 +1,93 @@
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.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
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达式定义列表
*/
@SaCheckPermission("workflow:spel:list")
@GetMapping("/list")
public TableDataInfo<FlowSpelVo> list(FlowSpelBo bo, PageQuery pageQuery) {
return flwSpelService.queryPageList(bo, pageQuery);
}
/**
* 获取流程spel达式定义详细信息
*
* @param id 主键
*/
@SaCheckPermission("workflow:spel:query")
@GetMapping("/{id}")
public R<FlowSpelVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
return R.ok(flwSpelService.queryById(id));
}
/**
* 新增流程spel达式定义
*/
@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达式定义
*/
@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 主键串
*/
@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));
}
}

View File

@@ -0,0 +1,224 @@
package org.dromara.workflow.controller;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.domain.dto.StartProcessReturnDTO;
import org.dromara.common.core.domain.dto.UserDTO;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.warm.flow.core.entity.Node;
import org.dromara.warm.flow.orm.entity.FlowNode;
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 启动流程参数
*/
@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 办理任务参数
*/
@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 分页
*/
@GetMapping("/pageByTaskWait")
public TableDataInfo<FlowTaskVo> pageByTaskWait(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
return flwTaskService.pageByTaskWait(flowTaskBo, pageQuery);
}
/**
* 查询当前用户的已办任务
*
* @param flowTaskBo 参数
* @param pageQuery 分页
*/
@GetMapping("/pageByTaskFinish")
public TableDataInfo<FlowHisTaskVo> pageByTaskFinish(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
return flwTaskService.pageByTaskFinish(flowTaskBo, pageQuery);
}
/**
* 查询待办任务
*
* @param flowTaskBo 参数
* @param pageQuery 分页
*/
@GetMapping("/pageByAllTaskWait")
public TableDataInfo<FlowTaskVo> pageByAllTaskWait(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
return flwTaskService.pageByAllTaskWait(flowTaskBo, pageQuery);
}
/**
* 查询已办任务
*
* @param flowTaskBo 参数
* @param pageQuery 分页
*/
@GetMapping("/pageByAllTaskFinish")
public TableDataInfo<FlowHisTaskVo> pageByAllTaskFinish(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
return flwTaskService.pageByAllTaskFinish(flowTaskBo, pageQuery);
}
/**
* 查询当前用户的抄送
*
* @param flowTaskBo 参数
* @param pageQuery 分页
*/
@GetMapping("/pageByTaskCopy")
public TableDataInfo<FlowTaskVo> pageByTaskCopy(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
return flwTaskService.pageByTaskCopy(flowTaskBo, pageQuery);
}
/**
* 根据taskId查询代表任务
*
* @param taskId 任务id
*/
@GetMapping("/getTask/{taskId}")
public R<FlowTaskVo> getTask(@PathVariable Long taskId) {
return R.ok(flwTaskService.selectById(taskId));
}
/**
* 获取下一节点信息
*
* @param bo 参数
*/
@PostMapping("/getNextNodeList")
public R<List<FlowNode>> getNextNodeList(@RequestBody FlowNextNodeBo bo) {
return R.ok(flwTaskService.getNextNodeList(bo));
}
/**
* 终止任务
*
* @param bo 参数
*/
@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
*/
@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
*/
@Log(title = "任务管理", businessType = BusinessType.UPDATE)
@RepeatSubmit()
@PutMapping("/updateAssignee/{userId}")
public R<Void> updateAssignee(@RequestBody List<Long> taskIdList, @PathVariable String userId) {
return toAjax(flwTaskService.updateAssignee(taskIdList, userId));
}
/**
* 驳回审批
*
* @param bo 参数
*/
@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 当前节点
*/
@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
*/
@GetMapping("/currentTaskAllUser/{taskId}")
public R<List<UserDTO>> currentTaskAllUser(@PathVariable Long taskId) {
return R.ok(flwTaskService.currentTaskAllUser(List.of(taskId)));
}
/**
* 催办任务
*
* @param bo 参数
* @return 结果
*/
@PostMapping("/urgeTask")
public R<Void> urgeTask(@RequestBody FlowUrgeTaskBo bo) {
return toAjax(flwTaskService.urgeTask(bo));
}
}

View File

@@ -0,0 +1,119 @@
package org.dromara.workflow.controller;
import cn.dev33.satoken.annotation.SaCheckPermission;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.excel.utils.ExcelUtil;
import org.dromara.common.idempotent.annotation.RepeatSubmit;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.web.core.BaseController;
import org.dromara.workflow.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;
/**
* 查询请假列表
*/
@SaCheckPermission("workflow:leave:list")
@GetMapping("/list")
public TableDataInfo<TestLeaveVo> list(TestLeaveBo bo, PageQuery pageQuery) {
return testLeaveService.queryPageList(bo, pageQuery);
}
/**
* 导出请假列表
*/
@SaCheckPermission("workflow:leave:export")
@Log(title = "请假", businessType = BusinessType.EXPORT)
@PostMapping("/export")
public void export(TestLeaveBo bo, HttpServletResponse response) {
List<TestLeaveVo> list = testLeaveService.queryList(bo);
ExcelUtil.exportExcel(list, "请假", TestLeaveVo.class, response);
}
/**
* 获取请假详细信息
*
* @param id 主键
*/
@SaCheckPermission("workflow:leave:query")
@GetMapping("/{id}")
public R<TestLeaveVo> getInfo(@NotNull(message = "主键不能为空")
@PathVariable Long id) {
return R.ok(testLeaveService.queryById(id));
}
/**
* 新增请假
*/
@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));
}
/**
* 提交请假并提交流程
*/
@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));
}
/**
* 修改请假
*/
@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 主键串
*/
@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)));
}
}

View File

@@ -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.tenant.core.TenantEntity;
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 TenantEntity {
@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<>();
}

View File

@@ -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.tenant.core.TenantEntity;
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 TenantEntity {
@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;
}

View File

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

View File

@@ -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.util.Date;
/**
* 请假对象 test_leave
*
* @author may
* @date 2023-07-21
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("test_leave")
public class TestLeave extends BaseEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键
*/
@TableId(value = "id")
private Long id;
/**
* 申请编号
*/
private String applyCode;
/**
* 请假类型
*/
private String leaveType;
/**
* 开始时间
*/
private Date startDate;
/**
* 结束时间
*/
private Date endDate;
/**
* 请假天数
*/
private Integer leaveDays;
/**
* 请假原因
*/
private String remark;
/**
* 状态
*/
private String status;
}

View File

@@ -0,0 +1,70 @@
package org.dromara.workflow.domain.bo;
import jakarta.validation.constraints.NotBlank;
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) {
return new HashMap<>(16);
}
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables;
}
}

View File

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

View File

@@ -0,0 +1,31 @@
package org.dromara.workflow.domain.bo;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import org.dromara.common.core.validate.AddGroup;
import java.io.Serial;
import java.io.Serializable;
/**
* 撤销任务请求对象
*
* @author may
*/
@Data
public class FlowCancelBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 任务ID
*/
@NotBlank(message = "业务ID不能为空", groups = AddGroup.class)
private String businessId;
/**
* 办理意见
*/
private String message;
}

View File

@@ -0,0 +1,47 @@
package org.dromara.workflow.domain.bo;
import io.github.linpeilie.annotations.AutoMapper;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.workflow.domain.FlowCategory;
/**
* 流程分类业务对象 wf_category
*
* @author may
* @date 2023-06-27
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = FlowCategory.class, reverseConvertGenerate = false)
public class FlowCategoryBo extends BaseEntity {
/**
* 流程分类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;
}

View File

@@ -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 userName;
}

View File

@@ -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<Long> createByIds;
}

View File

@@ -0,0 +1,31 @@
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;
/**
* 作废请求对象
*
* @author may
*/
@Data
public class FlowInvalidBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 流程实例id
*/
@NotNull(message = "流程实例id为空", groups = AddGroup.class)
private Long id;
/**
* 审批意见
*/
private String comment;
}

View File

@@ -0,0 +1,38 @@
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;
public Map<String, Object> getVariables() {
if (variables == null) {
return new HashMap<>(16);
}
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables;
}
}

View File

@@ -0,0 +1,60 @@
package org.dromara.workflow.domain.bo;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import lombok.EqualsAndHashCode;
import jakarta.validation.constraints.*;
import org.dromara.workflow.domain.FlowSpel;
/**
* 流程spel达式定义业务对象 flow_spel
*
* @author Michelle.Chung
* @date 2025-07-04
*/
@Data
@EqualsAndHashCode(callSuper = true)
@AutoMapper(target = FlowSpel.class, reverseConvertGenerate = false)
public class FlowSpelBo extends BaseEntity {
/**
* 主键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;
}

View File

@@ -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 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 List<String> permissionList;
/**
* 申请人Ids
*/
private List<Long> createByIds;
}

View File

@@ -0,0 +1,31 @@
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;
/**
* 终止任务请求对象
*
* @author may
*/
@Data
public class FlowTerminationBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 任务id
*/
@NotNull(message = "任务id为空", groups = AddGroup.class)
private Long taskId;
/**
* 审批意见
*/
private String comment;
}

View File

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

View File

@@ -0,0 +1,39 @@
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;
/**
* 流程变量参数
*
* @author may
*/
@Data
public class FlowVariableBo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 流程实例id
*/
@NotNull(message = "流程实例id为空", groups = AddGroup.class)
private Long instanceId;
/**
* 流程变量key
*/
@NotNull(message = "流程变量key为空", groups = AddGroup.class)
private String key;
/**
* 流程变量value
*/
@NotNull(message = "流程变量value为空", groups = AddGroup.class)
private String value;
}

View File

@@ -0,0 +1,68 @@
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) {
return new HashMap<>(16);
}
variables.entrySet().removeIf(entry -> Objects.isNull(entry.getValue()));
return variables;
}
public FlowInstanceBizExt getBizExt() {
if (ObjectUtil.isNull(bizExt)) {
bizExt = new FlowInstanceBizExt();
}
return bizExt;
}
}

View File

@@ -0,0 +1,48 @@
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 String message;
}

View File

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

View File

@@ -0,0 +1,43 @@
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() {
}
public ButtonPermissionVo(String code, Boolean show) {
this.code = code;
this.show = show;
}
}

View File

@@ -0,0 +1,69 @@
package org.dromara.workflow.domain.vo;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
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.util.Date;
/**
* 流程分类视图对象 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 Date createTime;
}

View File

@@ -0,0 +1,36 @@
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 userName;
public FlowCopyVo(Long userId) {
this.userId = userId;
}
}

View File

@@ -0,0 +1,104 @@
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.util.Date;
/**
* 流程定义视图
*
* @author may
*/
@Data
public class FlowDefinitionVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long id;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 租户ID
*/
private String tenantId;
/**
* 删除标记
*/
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;
}

View File

@@ -0,0 +1,256 @@
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.util.Date;
import java.util.List;
/**
* 历史任务视图
*
* @author may
*/
@Data
public class FlowHisTaskVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long id;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 租户ID
*/
private String tenantId;
/**
* 删除标记
*/
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 approveName;
/**
* 协作人(只有转办、会签、票签、委派)
*/
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(Date createTime) {
this.createTime = createTime;
updateRunDuration();
}
/**
* 设置更新时间并计算任务运行时长
*
* @param updateTime 更新时间
*/
public void setUpdateTime(Date updateTime) {
this.updateTime = updateTime;
updateRunDuration();
}
/**
* 更新运行时长
*/
private void updateRunDuration() {
// 如果创建时间和更新时间均不为空,计算它们之间的时长
if (this.updateTime != null && this.createTime != null) {
this.runDuration = DateUtils.getTimeDifference(this.updateTime, this.createTime);
}
}
/**
* 设置协作方式,并通过协作方式获取名称
*/
public void setCooperateType(Integer cooperateType) {
this.cooperateType = cooperateType;
this.cooperateTypeName = CooperateType.getValueByKey(cooperateType);
}
}

View File

@@ -0,0 +1,149 @@
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.util.Date;
/**
* 流程实例视图
*
* @author may
*/
@Data
public class FlowInstanceVo {
private Long id;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 租户ID
*/
private String tenantId;
/**
* 删除标记
*/
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;
/**
* 流程状态0待提交 1审批中 2 审批通过 3自动通过 8已完成 9已退回 10失效
*/
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;
//业务扩展信息结束
}

View File

@@ -0,0 +1,79 @@
package org.dromara.workflow.domain.vo;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import org.dromara.common.excel.annotation.ExcelDictFormat;
import org.dromara.common.excel.convert.ExcelDictConvert;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.dromara.workflow.domain.FlowSpel;
import java.io.Serial;
import java.io.Serializable;
import java.util.Date;
/**
* 流程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 Date createTime;
}

View File

@@ -0,0 +1,215 @@
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.math.BigDecimal;
import java.util.Date;
import java.util.List;
import java.util.Map;
/**
* 任务视图
*
* @author may
*/
@Data
public class FlowTaskVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long id;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 租户ID
*/
private String tenantId;
/**
* 删除标记
*/
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 BigDecimal 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;
//业务扩展信息结束
}

View File

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

View File

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

View File

@@ -0,0 +1,95 @@
package org.dromara.workflow.handler;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.event.ProcessTaskEvent;
import org.dromara.common.core.domain.event.ProcessDeleteEvent;
import org.dromara.common.core.domain.event.ProcessEvent;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.tenant.helper.TenantHelper;
import org.dromara.warm.flow.core.entity.Instance;
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) {
String tenantId = TenantHelper.getTenantId();
log.info("【流程事件发布】租户ID: {}, 流程编码: {}, 业务ID: {}, 流程状态: {}, 节点类型: {}, 节点编码: {}, 节点名称: {}, 是否申请人节点: {}, 参数: {}",
tenantId, flowCode, instance.getBusinessId(), status, instance.getNodeType(), instance.getNodeCode(), instance.getNodeName(), submit, params);
ProcessEvent processEvent = new ProcessEvent();
processEvent.setTenantId(tenantId);
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 taskId 任务id
* @param params 上一个任务的办理参数
*/
public void processTaskHandler(String flowCode, Instance instance, Long taskId, Map<String, Object> params) {
String tenantId = TenantHelper.getTenantId();
log.info("【流程任务事件发布】租户ID: {}, 流程编码: {}, 业务ID: {}, 节点类型: {}, 节点编码: {}, 节点名称: {}, 任务ID: {}",
tenantId, flowCode, instance.getBusinessId(), instance.getNodeType(), instance.getNodeCode(), instance.getNodeName(), taskId);
ProcessTaskEvent processTaskEvent = new ProcessTaskEvent();
processTaskEvent.setTenantId(tenantId);
processTaskEvent.setFlowCode(flowCode);
processTaskEvent.setInstanceId(instance.getId());
processTaskEvent.setBusinessId(instance.getBusinessId());
processTaskEvent.setNodeType(instance.getNodeType());
processTaskEvent.setNodeCode(instance.getNodeCode());
processTaskEvent.setNodeName(instance.getNodeName());
processTaskEvent.setTaskId(taskId);
processTaskEvent.setStatus(instance.getFlowStatus());
processTaskEvent.setParams(params);
SpringUtils.context().publishEvent(processTaskEvent);
}
/**
* 删除流程监听
*
* @param flowCode 流程定义编码
* @param businessId 业务ID
*/
public void processDeleteHandler(String flowCode, String businessId) {
String tenantId = TenantHelper.getTenantId();
log.info("【流程删除事件发布】租户ID: {}, 流程编码: {}, 业务ID: {}", tenantId, flowCode, businessId);
ProcessDeleteEvent processDeleteEvent = new ProcessDeleteEvent();
processDeleteEvent.setTenantId(tenantId);
processDeleteEvent.setFlowCode(flowCode);
processDeleteEvent.setBusinessId(businessId);
SpringUtils.context().publishEvent(processDeleteEvent);
}
}

View File

@@ -0,0 +1,65 @@
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.domain.dto.UserDTO;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.satoken.utils.LoginHelper;
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} 中获取
* 返回当前用户权限集合
*/
@Override
public List<String> permissions() {
return Collections.singletonList(LoginHelper.getUserIdStr());
}
/**
* 获取当前办理人
*
* @return 当前办理人
*/
@Override
public String getHandler() {
return LoginHelper.getUserIdStr();
}
/**
* 转换办理人比如设计器中预设了能办理的人如果其中包含角色或者部门id等可以通过此接口进行转换成用户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()));
}
}

View File

@@ -0,0 +1,226 @@
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.service.UserService;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
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.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();
NodeExtVo nodeExt = nodeExtService.parseNodeExt(ext, variable);
Set<String> copyList = nodeExt.getCopySettings();
if (CollUtil.isNotEmpty(copyList)) {
List<FlowCopyBo> list = StreamUtils.toList(copyList, x -> {
FlowCopyBo bo = new FlowCopyBo();
Long id = Convert.toLong(x);
bo.setUserId(id);
bo.setUserName(userService.selectUserNameById(id));
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());
for (Task flowTask : nextTasks) {
// 如果办理或者退回并行存在需要指定办理人,则直接覆盖办理人
if (variable.containsKey(flowTask.getNodeCode()) && TaskStatusEnum.isPassOrBack(flowParams.getHisStatus())) {
String userIds = variable.get(flowTask.getNodeCode()).toString();
flowTask.setPermissionList(List.of(userIds.split(StringUtils.SEPARATOR)));
variable.remove(flowTask.getNodeCode());
}
// 如果是申请节点,则把启动人添加到办理人
if (flowTask.getNodeCode().equals(applyNodeCode)) {
flowTask.setPermissionList(List.of(instance.getCreateBy()));
}
}
}
/**
* 完成监听器,当前任务完成后执行
*
* @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) {
flowProcessEventHandler.processHandler(definition.getFlowCode(), instance, instance.getFlowStatus(), variable, true);
} else {
// 判断流程状态(发布:撤销,退回,作废,终止,已完成事件)
String status = determineFlowStatus(instance);
if (StringUtils.isNotBlank(status)) {
flowProcessEventHandler.processHandler(definition.getFlowCode(), instance, status, params, false);
}
if (!BusinessStatusEnum.initialState(instance.getFlowStatus())) {
if (task != null && CollUtil.isNotEmpty(nextTasks) && nextTasks.size() == 1
&& flwCommonService.applyNodeCode(definition.getId()).equals(nextTasks.get(0).getNodeCode())) {
// 如果为画线指定驳回 线条指定为驳回 驳回得节点为申请人节点 则修改流程状态为退回
flowProcessEventHandler.processHandler(definition.getFlowCode(), instance, BusinessStatusEnum.BACK.getStatus(), params, false);
// 修改流程实例状态
instance.setFlowStatus(BusinessStatusEnum.BACK.getStatus());
FlowEngine.insService().updateById(instance);
}
}
}
//发布任务事件
if (CollUtil.isNotEmpty(nextTasks)) {
for (Task nextTask : nextTasks) {
flowProcessEventHandler.processTaskHandler(definition.getFlowCode(), instance, nextTask.getId(), 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<>() {
});
// 添加抄送人
flwTaskService.setCopy(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);
flwCommonService.sendMessage(definition.getFlowName(), instance.getId(), messageType, notice);
}
FlowEngine.insService().removeVariables(instance.getId(),
FlowConstant.FLOW_COPY_LIST,
FlowConstant.MESSAGE_TYPE,
FlowConstant.MESSAGE_NOTICE,
FlowConstant.SUBMIT
);
}
/**
* 根据流程实例确定最终状态
*
* @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;
}
}
}

View File

@@ -0,0 +1,49 @@
package org.dromara.workflow.mapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.dromara.common.mybatis.annotation.DataColumn;
import org.dromara.common.mybatis.annotation.DataPermission;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.common.mybatis.helper.DataBaseHelper;
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.selectList(new LambdaQueryWrapper<FlowCategory>()
.select(FlowCategory::getCategoryId)
.apply(DataBaseHelper.findInSet(parentId, "ancestors")));
}
/**
* 根据父流程分类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());
}
}

View File

@@ -0,0 +1,61 @@
package org.dromara.workflow.mapper;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import java.util.List;
/**
* 流程实例业务扩展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.selectOne(new LambdaQueryWrapper<FlowInstanceBizExt>()
.eq(FlowInstanceBizExt::getInstanceId, entity.getInstanceId()));
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.delete(new LambdaQueryWrapper<FlowInstanceBizExt>()
.eq(FlowInstanceBizExt::getInstanceId, instanceId));
}
/**
* 按照流程实例ID批量删除流程实例业务扩展
*
* @param instanceIds 流程实例ID列表
* @return 删除的行数
*/
default int deleteByInstIds(List<Long> instanceIds) {
return this.delete(new LambdaQueryWrapper<FlowInstanceBizExt>()
.in(FlowInstanceBizExt::getInstanceId, instanceIds));
}
}

View File

@@ -0,0 +1,27 @@
package org.dromara.workflow.mapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
import org.dromara.workflow.domain.bo.FlowInstanceBo;
import org.dromara.workflow.domain.vo.FlowInstanceVo;
/**
* 实例信息Mapper接口
*
* @author may
* @date 2024-03-02
*/
public interface FlwInstanceMapper {
/**
* 流程实例信息
*
* @param page 分页
* @param queryWrapper 条件
* @return 结果
*/
Page<FlowInstanceVo> selectInstanceList(@Param("page") Page<FlowInstanceVo> page, @Param(Constants.WRAPPER) Wrapper<FlowInstanceBo> queryWrapper);
}

View File

@@ -0,0 +1,15 @@
package org.dromara.workflow.mapper;
import org.dromara.workflow.domain.FlowSpel;
import org.dromara.workflow.domain.vo.FlowSpelVo;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
/**
* 流程spel达式定义Mapper接口
*
* @author Michelle.Chung
* @date 2025-07-04
*/
public interface FlwSpelMapper extends BaseMapperPlus<FlowSpel, FlowSpelVo> {
}

View File

@@ -0,0 +1,47 @@
package org.dromara.workflow.mapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Param;
import org.dromara.workflow.domain.bo.FlowTaskBo;
import org.dromara.workflow.domain.vo.FlowHisTaskVo;
import org.dromara.workflow.domain.vo.FlowTaskVo;
/**
* 任务信息Mapper接口
*
* @author may
* @date 2024-03-02
*/
public interface FlwTaskMapper {
/**
* 获取待办信息
*
* @param page 分页
* @param queryWrapper 条件
* @return 结果
*/
Page<FlowTaskVo> getListRunTask(@Param("page") Page<FlowTaskVo> page, @Param(Constants.WRAPPER) Wrapper<FlowTaskBo> queryWrapper);
/**
* 获取已办
*
* @param page 分页
* @param queryWrapper 条件
* @return 结果
*/
Page<FlowHisTaskVo> getListFinishTask(@Param("page") Page<FlowTaskVo> page, @Param(Constants.WRAPPER) Wrapper<FlowTaskBo> queryWrapper);
/**
* 查询当前用户的抄送
*
* @param page 分页
* @param queryWrapper 条件
* @return 结果
*/
Page<FlowTaskVo> getTaskCopyByPage(@Param("page") Page<FlowTaskVo> page, @Param(Constants.WRAPPER) QueryWrapper<FlowTaskBo> queryWrapper);
}

View File

@@ -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> {
}

View File

@@ -0,0 +1,38 @@
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.common.core.service.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获取部门负责人
*/
public Long selectDeptLeaderById(Long initiatorDeptId) {
Long leaderId = deptService.selectDeptLeaderById(initiatorDeptId);
if (ObjectUtil.isNull(leaderId)) {
throw new ServiceException("当前部门未设置负责人,请联系管理员操作。");
}
return leaderId;
}
}

View File

@@ -0,0 +1,95 @@
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;
/**
* 流程分类Service接口
*
* @author may
*/
public interface IFlwCategoryService {
/**
* 查询流程分类
*
* @param categoryId 主键
* @return 流程分类
*/
FlowCategoryVo queryById(Long categoryId);
/**
* 根据流程分类ID查询流程分类名称
*
* @param categoryId 流程分类ID
* @return 流程分类名称
*/
String selectCategoryNameById(Long categoryId);
/**
* 查询符合条件的流程分类列表
*
* @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);
}

View File

@@ -0,0 +1,41 @@
package org.dromara.workflow.service;
import org.dromara.common.core.domain.dto.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 definitionId 流程定义id
* @return 申请人节点编码
*/
String applyNodeCode(Long definitionId);
}

View File

@@ -0,0 +1,78 @@
package org.dromara.workflow.service;
import jakarta.servlet.http.HttpServletResponse;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
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.List;
/**
* 流程定义 服务层
*
* @author may
*/
public interface IFlwDefinitionService {
/**
* 查询流程定义列表
*
* @param flowDefinition 参数
* @param pageQuery 分页
* @return 返回分页列表
*/
TableDataInfo<FlowDefinitionVo> queryList(FlowDefinition flowDefinition, PageQuery pageQuery);
/**
* 查询未发布的流程定义列表
*
* @param flowDefinition 参数
* @param pageQuery 分页
* @return 返回分页列表
*/
TableDataInfo<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(List<Long> ids);
/**
* 新增租户流程定义
*
* @param tenantId 租户id
*/
void syncDef(String tenantId);
}

View File

@@ -0,0 +1,168 @@
package org.dromara.workflow.service;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
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.List;
import java.util.Map;
/**
* 流程实例 服务层
*
* @author may
*/
public interface IFlwInstanceService {
/**
* 分页查询正在运行的流程实例
*
* @param flowInstanceBo 流程实例
* @param pageQuery 分页
* @return 结果
*/
TableDataInfo<FlowInstanceVo> selectRunningInstanceList(FlowInstanceBo flowInstanceBo, PageQuery pageQuery);
/**
* 分页查询已结束的流程实例
*
* @param flowInstanceBo 流程实例
* @param pageQuery 分页
* @return 结果
*/
TableDataInfo<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(List<Long> instanceIds);
/**
* 按照业务id删除流程实例
*
* @param businessIds 业务id
* @return 结果
*/
boolean deleteByBusinessIds(List<Long> businessIds);
/**
* 按照实例id删除流程实例
*
* @param instanceIds 实例id
* @return 结果
*/
boolean deleteByInstanceIds(List<Long> instanceIds);
/**
* 按照实例id删除已完成得流程实例
*
* @param instanceIds 删除的实例id
* @return 删除结果
*/
boolean deleteHisByInstanceIds(List<Long> instanceIds);
/**
* 撤销流程
*
* @param bo 参数
* @return 结果
*/
boolean cancelProcessApply(FlowCancelBo bo);
/**
* 获取当前登陆人发起的流程实例
*
* @param instanceBo 流程实例
* @param pageQuery 分页
* @return 结果
*/
TableDataInfo<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);
}

View File

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

View File

@@ -0,0 +1,88 @@
package org.dromara.workflow.service;
import org.dromara.common.core.domain.dto.TaskAssigneeDTO;
import org.dromara.common.core.domain.model.TaskAssigneeBody;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
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达式定义Service接口
*
* @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达式定义分页列表
*/
TableDataInfo<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);
}

View File

@@ -0,0 +1,24 @@
package org.dromara.workflow.service;
import org.dromara.common.core.domain.dto.UserDTO;
import java.util.List;
/**
* 流程设计器-获取办理人
*
* @author AprilWind
*/
public interface IFlwTaskAssigneeService {
/**
* 批量解析多个存储标识符storageIds按类型分类并合并查询用户列表
* 输入格式支持多个以逗号分隔的标识(如 "user:123,role:456,789"
* 会自动去重返回结果,非法格式的标识将被忽略
*
* @param storageIds 多个存储标识符字符串(逗号分隔)
* @return 合并后的用户列表,去重后返回,非法格式的标识将被跳过
*/
List<UserDTO> fetchUsersByStorageIds(String storageIds);
}

View File

@@ -0,0 +1,217 @@
package org.dromara.workflow.service;
import org.dromara.common.core.domain.dto.StartProcessReturnDTO;
import org.dromara.common.core.domain.dto.UserDTO;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
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.domain.bo.*;
import org.dromara.workflow.domain.vo.FlowHisTaskVo;
import org.dromara.workflow.domain.vo.FlowTaskVo;
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 结果
*/
TableDataInfo<FlowTaskVo> pageByTaskWait(FlowTaskBo flowTaskBo, PageQuery pageQuery);
/**
* 查询当前租户所有待办任务
*
* @param flowTaskBo 参数
* @param pageQuery 分页
* @return 结果
*/
TableDataInfo<FlowHisTaskVo> pageByTaskFinish(FlowTaskBo flowTaskBo, PageQuery pageQuery);
/**
* 查询待办任务
*
* @param flowTaskBo 参数
* @param pageQuery 分页
* @return 结果
*/
TableDataInfo<FlowTaskVo> pageByAllTaskWait(FlowTaskBo flowTaskBo, PageQuery pageQuery);
/**
* 查询已办任务
*
* @param flowTaskBo 参数
* @param pageQuery 分页
* @return 结果
*/
TableDataInfo<FlowHisTaskVo> pageByAllTaskFinish(FlowTaskBo flowTaskBo, PageQuery pageQuery);
/**
* 查询当前用户的抄送
*
* @param flowTaskBo 参数
* @param pageQuery 分页
* @return 结果
*/
TableDataInfo<FlowTaskVo> pageByTaskCopy(FlowTaskBo flowTaskBo, PageQuery pageQuery);
/**
* 修改任务办理人
*
* @param taskIdList 任务id
* @param userId 用户id
* @return 结果
*/
boolean updateAssignee(List<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(List<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(List<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);
}

View File

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

View File

@@ -0,0 +1,31 @@
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;
/**
* 流程分类名称翻译实现
*
* @author AprilWind
*/
@ConditionalOnEnable
@Slf4j
@RequiredArgsConstructor
@Service
@TranslationType(type = FlowConstant.CATEGORY_ID_TO_NAME)
public class CategoryNameTranslationImpl implements TranslationInterface<String> {
private final IFlwCategoryService flwCategoryService;
@Override
public String translation(Object key, String other) {
return flwCategoryService.selectCategoryNameById(Convert.toLong(key));
}
}

View File

@@ -0,0 +1,258 @@
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.common.mybatis.helper.DataBaseHelper;
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.ArrayList;
import java.util.List;
/**
* 流程分类Service业务层处理
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@Service
public class FlwCategoryServiceImpl implements IFlwCategoryService, CategoryService {
private final DefService defService;
private final FlwCategoryMapper baseMapper;
/**
* 查询流程分类
*
* @param categoryId 主键
* @return 流程分类
*/
@Override
public FlowCategoryVo queryById(Long categoryId) {
return baseMapper.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 = baseMapper.selectOne(new LambdaQueryWrapper<FlowCategory>()
.select(FlowCategory::getCategoryName).eq(FlowCategory::getCategoryId, categoryId));
return ObjectUtils.notNullGetter(category, FlowCategory::getCategoryName);
}
/**
* 查询符合条件的流程分类列表
*
* @param bo 查询条件
* @return 流程分类列表
*/
@Override
public List<FlowCategoryVo> queryList(FlowCategoryBo bo) {
LambdaQueryWrapper<FlowCategory> lqw = buildQueryWrapper(bo);
return baseMapper.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 = baseMapper.exists(new LambdaQueryWrapper<FlowCategory>()
.eq(FlowCategory::getCategoryName, category.getCategoryName())
.eq(FlowCategory::getParentId, category.getParentId())
.ne(ObjectUtil.isNotNull(category.getCategoryId()), FlowCategory::getCategoryId, category.getCategoryId()));
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 baseMapper.exists(new LambdaQueryWrapper<FlowCategory>()
.eq(FlowCategory::getParentId, categoryId));
}
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 = baseMapper.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 baseMapper.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 = baseMapper.selectById(category.getCategoryId());
if (ObjectUtil.isNull(oldCategory)) {
throw new ServiceException("流程分类不存在,无法修改");
}
if (!oldCategory.getParentId().equals(category.getParentId())) {
FlowCategory newParentCategory = baseMapper.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 baseMapper.updateById(category);
}
/**
* 修改子元素关系
*
* @param categoryId 被修改的流程分类ID
* @param newAncestors 新的父ID集合
* @param oldAncestors 旧的父ID集合
*/
private void updateCategoryChildren(Long categoryId, String newAncestors, String oldAncestors) {
List<FlowCategory> children = baseMapper.selectList(new LambdaQueryWrapper<FlowCategory>()
.apply(DataBaseHelper.findInSet(categoryId, "ancestors")));
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)) {
baseMapper.updateBatchById(list);
}
}
/**
* 删除流程分类信息
*
* @param categoryId 主键
* @return 是否删除成功
*/
@CacheEvict(cacheNames = FlowConstant.FLOW_CATEGORY_NAME, key = "#categoryId")
@Override
public int deleteWithValidById(Long categoryId) {
return baseMapper.deleteById(categoryId);
}
}

View File

@@ -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.domain.dto.UserDTO;
import org.dromara.common.core.service.DeptService;
import org.dromara.common.core.service.DictService;
import org.dromara.common.core.service.UserService;
import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
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.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
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.toList(flowHisTasks, e -> Convert.toLong(e.getApprover())));
// 将查询到的用户列表转换为以用户ID为key的映射
Map<Long, UserDTO> userMap = StreamUtils.toIdentityMap(userDTOList, UserDTO::getUserId);
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, 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 为用户IDvalue 为用户DTO对象用于获取审批人信息
* @param dictType 数据字典映射表key 为字典项编码value 为对应显示值,用于翻译审批状态等
*/
private void processNodeExtInfo(NodeJson nodeJson, List<FlowHisTask> taskList, Map<Long, UserDTO> userMap, 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 = deptService.selectDeptNameByIds(Convert.toStr(userDTO.getDeptId()));
// 添加标题项,如:👤 张三(市场部)
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.getTimeDifference(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);
}
}

View File

@@ -0,0 +1,119 @@
package org.dromara.workflow.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.dto.UserDTO;
import org.dromara.common.core.utils.SpringUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mail.utils.MailUtils;
import org.dromara.common.sse.dto.SseMessageDto;
import org.dromara.common.sse.utils.SseMessageUtils;
import org.dromara.warm.flow.core.FlowEngine;
import org.dromara.warm.flow.core.entity.Node;
import org.dromara.warm.flow.orm.entity.FlowTask;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.MessageTypeEnum;
import org.dromara.workflow.service.IFlwCommonService;
import org.dromara.workflow.service.IFlwTaskService;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
/**
* 工作流工具
*
* @author LionLi
*/
@ConditionalOnEnable
@Slf4j
@RequiredArgsConstructor
@Service
public class FlwCommonServiceImpl implements IFlwCommonService {
private static final String DEFAULT_SUBJECT = "单据审批提醒";
/**
* 根据流程实例发送消息给当前处理人
*
* @param flowName 流程定义名称
* @param instId 流程实例ID
* @param messageType 消息类型列表
* @param message 消息内容,为空则使用默认消息
*/
@Override
public void sendMessage(String flowName, Long instId, List<String> messageType, String message) {
if (CollUtil.isEmpty(messageType)) {
return;
}
IFlwTaskService flwTaskService = SpringUtils.getBean(IFlwTaskService.class);
List<FlowTask> list = flwTaskService.selectByInstId(instId);
if (CollUtil.isEmpty(list)) {
return;
}
if (StringUtils.isBlank(message)) {
message = "有新的【" + flowName + "】单据已经提交至您,请您及时处理。";
}
List<UserDTO> userList = flwTaskService.currentTaskAllUser(StreamUtils.toList(list, FlowTask::getId));
if (CollUtil.isEmpty(userList)) {
return;
}
sendMessage(messageType, message, DEFAULT_SUBJECT, userList);
}
/**
* 发送消息给指定用户列表
*
* @param messageType 消息类型列表
* @param message 消息内容
* @param subject 邮件标题
* @param userList 接收用户列表
*/
@Override
public void sendMessage(List<String> messageType, String message, String subject, List<UserDTO> userList) {
if (CollUtil.isEmpty(messageType) || CollUtil.isEmpty(userList)) {
return;
}
List<Long> userIds = new ArrayList<>(StreamUtils.toSet(userList, UserDTO::getUserId));
Set<String> emails = StreamUtils.toSet(userList, UserDTO::getEmail);
for (String code : messageType) {
MessageTypeEnum messageTypeEnum = MessageTypeEnum.getByCode(code);
if (ObjectUtil.isEmpty(messageTypeEnum)) {
continue;
}
switch (messageTypeEnum) {
case SYSTEM_MESSAGE -> {
SseMessageDto dto = new SseMessageDto();
dto.setUserIds(userIds);
dto.setMessage(message);
SseMessageUtils.publishMessage(dto);
}
case EMAIL_MESSAGE -> MailUtils.sendText(emails, subject, message);
case SMS_MESSAGE -> {
//todo 短信发送
}
default -> throw new IllegalStateException("Unexpected value: " + messageTypeEnum);
}
}
}
/**
* 申请人节点编码
*
* @param definitionId 流程定义id
* @return 申请人节点编码
*/
@Override
public String applyNodeCode(Long definitionId) {
List<Node> firstBetweenNode = FlowEngine.nodeService().getFirstBetweenNode(definitionId, new HashMap<>());
return firstBetweenNode.get(0).getNodeCode();
}
}

View File

@@ -0,0 +1,269 @@
package org.dromara.workflow.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IoUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.servlet.http.HttpServletResponse;
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.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.warm.flow.core.dto.DefJson;
import org.dromara.warm.flow.core.enums.NodeType;
import org.dromara.warm.flow.core.enums.PublishStatus;
import org.dromara.warm.flow.core.service.DefService;
import org.dromara.warm.flow.orm.entity.FlowDefinition;
import org.dromara.warm.flow.orm.entity.FlowHisTask;
import org.dromara.warm.flow.orm.entity.FlowNode;
import org.dromara.warm.flow.orm.entity.FlowSkip;
import org.dromara.warm.flow.orm.mapper.FlowDefinitionMapper;
import org.dromara.warm.flow.orm.mapper.FlowHisTaskMapper;
import org.dromara.warm.flow.orm.mapper.FlowNodeMapper;
import org.dromara.warm.flow.orm.mapper.FlowSkipMapper;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.constant.FlowConstant;
import org.dromara.workflow.domain.FlowCategory;
import org.dromara.workflow.domain.vo.FlowDefinitionVo;
import org.dromara.workflow.mapper.FlwCategoryMapper;
import org.dromara.workflow.service.IFlwCommonService;
import org.dromara.workflow.service.IFlwDefinitionService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static org.dromara.common.core.constant.TenantConstants.DEFAULT_TENANT_ID;
/**
* 流程定义 服务层实现
*
* @author may
*/
@ConditionalOnEnable
@Slf4j
@RequiredArgsConstructor
@Service
public class FlwDefinitionServiceImpl implements IFlwDefinitionService {
private final DefService defService;
private final FlowDefinitionMapper flowDefinitionMapper;
private final FlowHisTaskMapper flowHisTaskMapper;
private final FlowNodeMapper flowNodeMapper;
private final FlowSkipMapper flowSkipMapper;
private final FlwCategoryMapper flwCategoryMapper;
private final IFlwCommonService flwCommonService;
/**
* 查询流程定义列表
*
* @param flowDefinition 流程定义信息
* @param pageQuery 分页
* @return 返回分页列表
*/
@Override
public TableDataInfo<FlowDefinitionVo> queryList(FlowDefinition flowDefinition, PageQuery pageQuery) {
LambdaQueryWrapper<FlowDefinition> wrapper = buildQueryWrapper(flowDefinition);
wrapper.eq(FlowDefinition::getIsPublish, PublishStatus.PUBLISHED.getKey());
Page<FlowDefinition> page = flowDefinitionMapper.selectPage(pageQuery.build(), wrapper);
List<FlowDefinitionVo> list = BeanUtil.copyToList(page.getRecords(), FlowDefinitionVo.class);
return new TableDataInfo<>(list, page.getTotal());
}
/**
* 查询未发布的流程定义列表
*
* @param flowDefinition 流程定义信息
* @param pageQuery 分页
* @return 返回分页列表
*/
@Override
public TableDataInfo<FlowDefinitionVo> unPublishList(FlowDefinition flowDefinition, PageQuery pageQuery) {
LambdaQueryWrapper<FlowDefinition> wrapper = buildQueryWrapper(flowDefinition);
wrapper.in(FlowDefinition::getIsPublish, Arrays.asList(PublishStatus.UNPUBLISHED.getKey(), PublishStatus.EXPIRED.getKey()));
Page<FlowDefinition> page = flowDefinitionMapper.selectPage(pageQuery.build(), wrapper);
List<FlowDefinitionVo> list = BeanUtil.copyToList(page.getRecords(), FlowDefinitionVo.class);
return new TableDataInfo<>(list, page.getTotal());
}
private LambdaQueryWrapper<FlowDefinition> buildQueryWrapper(FlowDefinition flowDefinition) {
LambdaQueryWrapper<FlowDefinition> wrapper = Wrappers.lambdaQuery();
wrapper.like(StringUtils.isNotBlank(flowDefinition.getFlowCode()), FlowDefinition::getFlowCode, flowDefinition.getFlowCode());
wrapper.like(StringUtils.isNotBlank(flowDefinition.getFlowName()), FlowDefinition::getFlowName, flowDefinition.getFlowName());
if (StringUtils.isNotBlank(flowDefinition.getCategory())) {
List<Long> categoryIds = flwCategoryMapper.selectCategoryIdsByParentId(Convert.toLong(flowDefinition.getCategory()));
wrapper.in(FlowDefinition::getCategory, StreamUtils.toList(categoryIds, Convert::toStr));
}
wrapper.orderByDesc(FlowDefinition::getCreateTime);
return wrapper;
}
/**
* 发布流程定义
*
* @param id 流程定义id
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean publish(Long id) {
List<FlowNode> flowNodes = flowNodeMapper.selectList(new LambdaQueryWrapper<FlowNode>().eq(FlowNode::getDefinitionId, id));
List<String> errorMsg = new ArrayList<>();
if (CollUtil.isNotEmpty(flowNodes)) {
String applyNodeCode = flwCommonService.applyNodeCode(id);
for (FlowNode flowNode : flowNodes) {
if (StringUtils.isBlank(flowNode.getPermissionFlag()) && !applyNodeCode.equals(flowNode.getNodeCode()) && NodeType.BETWEEN.getKey().equals(flowNode.getNodeType())) {
errorMsg.add(flowNode.getNodeName());
}
}
if (CollUtil.isNotEmpty(errorMsg)) {
throw new ServiceException("节点【{}】未配置办理人!", StringUtils.joinComma(errorMsg));
}
}
return defService.publish(id);
}
/**
* 导入流程定义
*
* @param file 文件
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean importJson(MultipartFile file, String category) {
try {
DefJson defJson = JsonUtils.parseObject(file.getBytes(), DefJson.class);
defJson.setCategory(category);
defService.importDef(defJson);
} catch (IOException e) {
log.error("读取文件流错误: {}", e.getMessage(), e);
throw new IllegalStateException("文件读取失败,请检查文件内容", e);
}
return true;
}
/**
* 导出流程定义
*
* @param id 流程定义id
* @param response 响应
* @throws IOException 异常
*/
@Override
public void exportDef(Long id, HttpServletResponse response) throws IOException {
byte[] data = defService.exportJson(id).getBytes(StandardCharsets.UTF_8);
// 设置响应头和内容类型
response.reset();
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType("application/text");
response.setHeader("Content-Disposition", "attachment;");
response.addHeader("Content-Length", "" + data.length);
IoUtil.write(response.getOutputStream(), false, data);
}
/**
* 删除流程定义
*
* @param ids 流程定义id
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean removeDef(List<Long> ids) {
LambdaQueryWrapper<FlowHisTask> wrapper = Wrappers.lambdaQuery();
wrapper.in(FlowHisTask::getDefinitionId, ids);
List<FlowHisTask> flowHisTasks = flowHisTaskMapper.selectList(wrapper);
if (CollUtil.isNotEmpty(flowHisTasks)) {
List<FlowDefinition> flowDefinitions = flowDefinitionMapper.selectByIds(StreamUtils.toList(flowHisTasks, FlowHisTask::getDefinitionId));
if (CollUtil.isNotEmpty(flowDefinitions)) {
String join = StreamUtils.join(flowDefinitions, FlowDefinition::getFlowCode);
log.info("流程定义【{}】已被使用不可被删除!", join);
throw new ServiceException("流程定义【{}】已被使用不可被删除!", join);
}
}
try {
defService.removeDef(ids);
} catch (Exception e) {
log.error("Error removing flow definitions: {}", e.getMessage(), e);
throw new RuntimeException("Failed to remove flow definitions", e);
}
return true;
}
/**
* 新增租户流程定义
*
* @param tenantId 租户id
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void syncDef(String tenantId) {
List<FlowDefinition> flowDefinitions = flowDefinitionMapper.selectList(new LambdaQueryWrapper<FlowDefinition>().eq(FlowDefinition::getTenantId, DEFAULT_TENANT_ID));
if (CollUtil.isEmpty(flowDefinitions)) {
return;
}
FlowCategory flowCategory = flwCategoryMapper.selectOne(new LambdaQueryWrapper<FlowCategory>()
.eq(FlowCategory::getTenantId, DEFAULT_TENANT_ID)
.eq(FlowCategory::getCategoryId, FlowConstant.FLOW_CATEGORY_ID));
flowCategory.setCategoryId(null);
flowCategory.setTenantId(tenantId);
flowCategory.setCreateDept(null);
flowCategory.setCreateBy(null);
flowCategory.setCreateTime(null);
flowCategory.setUpdateBy(null);
flowCategory.setUpdateTime(null);
flwCategoryMapper.insert(flowCategory);
List<Long> defIds = StreamUtils.toList(flowDefinitions, FlowDefinition::getId);
List<FlowNode> flowNodes = flowNodeMapper.selectList(new LambdaQueryWrapper<FlowNode>().in(FlowNode::getDefinitionId, defIds));
List<FlowSkip> flowSkips = flowSkipMapper.selectList(new LambdaQueryWrapper<FlowSkip>().in(FlowSkip::getDefinitionId, defIds));
for (FlowDefinition definition : flowDefinitions) {
FlowDefinition flowDefinition = BeanUtil.toBean(definition, FlowDefinition.class);
flowDefinition.setId(null);
flowDefinition.setTenantId(tenantId);
flowDefinition.setIsPublish(0);
flowDefinition.setCategory(Convert.toStr(flowCategory.getCategoryId()));
int insert = flowDefinitionMapper.insert(flowDefinition);
if (insert <= 0) {
log.info("同步流程定义【{}】失败!", definition.getFlowCode());
continue;
}
log.info("同步流程定义【{}】成功!", definition.getFlowCode());
Long definitionId = flowDefinition.getId();
if (CollUtil.isNotEmpty(flowNodes)) {
List<FlowNode> nodes = StreamUtils.filter(flowNodes, node -> node.getDefinitionId().equals(definition.getId()));
if (CollUtil.isNotEmpty(nodes)) {
List<FlowNode> flowNodeList = BeanUtil.copyToList(nodes, FlowNode.class);
flowNodeList.forEach(e -> {
e.setId(null);
e.setDefinitionId(definitionId);
e.setTenantId(tenantId);
e.setPermissionFlag(null);
});
flowNodeMapper.insertOrUpdate(flowNodeList);
}
}
if (CollUtil.isNotEmpty(flowSkips)) {
List<FlowSkip> skips = StreamUtils.filter(flowSkips, skip -> skip.getDefinitionId().equals(definition.getId()));
if (CollUtil.isNotEmpty(skips)) {
List<FlowSkip> flowSkipList = BeanUtil.copyToList(skips, FlowSkip.class);
flowSkipList.forEach(e -> {
e.setId(null);
e.setDefinitionId(definitionId);
e.setTenantId(tenantId);
});
flowSkipMapper.insertOrUpdate(flowSkipList);
}
}
}
}
}

View File

@@ -0,0 +1,500 @@
package org.dromara.workflow.service.impl;
import cn.hutool.core.bean.BeanUtil;
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.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.warm.flow.core.FlowEngine;
import org.dromara.warm.flow.core.constant.ExceptionCons;
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.entity.User;
import org.dromara.warm.flow.core.enums.NodeType;
import org.dromara.warm.flow.core.service.DefService;
import org.dromara.warm.flow.core.service.InsService;
import org.dromara.warm.flow.core.service.TaskService;
import org.dromara.warm.flow.orm.entity.FlowHisTask;
import org.dromara.warm.flow.orm.entity.FlowInstance;
import org.dromara.warm.flow.orm.entity.FlowTask;
import org.dromara.warm.flow.orm.mapper.FlowHisTaskMapper;
import org.dromara.warm.flow.orm.mapper.FlowInstanceMapper;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.TaskStatusEnum;
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.FlowHisTaskVo;
import org.dromara.workflow.domain.vo.FlowInstanceVo;
import org.dromara.workflow.handler.FlowProcessEventHandler;
import org.dromara.workflow.mapper.FlwCategoryMapper;
import org.dromara.workflow.mapper.FlwInstanceMapper;
import org.dromara.workflow.service.IFlwInstanceService;
import org.dromara.workflow.service.IFlwTaskService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.*;
import java.util.function.Function;
/**
* 流程实例 服务层实现
*
* @author may
*/
@ConditionalOnEnable
@Slf4j
@RequiredArgsConstructor
@Service
public class FlwInstanceServiceImpl implements IFlwInstanceService {
private final InsService insService;
private final DefService defService;
private final TaskService taskService;
private final FlowHisTaskMapper flowHisTaskMapper;
private final FlowInstanceMapper flowInstanceMapper;
private final FlowProcessEventHandler flowProcessEventHandler;
private final IFlwTaskService flwTaskService;
private final FlwInstanceMapper flwInstanceMapper;
private final FlwCategoryMapper flwCategoryMapper;
/**
* 分页查询正在运行的流程实例
*
* @param flowInstanceBo 流程实例
* @param pageQuery 分页
*/
@Override
public TableDataInfo<FlowInstanceVo> selectRunningInstanceList(FlowInstanceBo flowInstanceBo, PageQuery pageQuery) {
QueryWrapper<FlowInstanceBo> queryWrapper = buildQueryWrapper(flowInstanceBo);
queryWrapper.in("fi.flow_status", BusinessStatusEnum.runningStatus());
Page<FlowInstanceVo> page = flwInstanceMapper.selectInstanceList(pageQuery.build(), queryWrapper);
return TableDataInfo.build(page);
}
/**
* 分页查询已结束的流程实例
*
* @param flowInstanceBo 流程实例
* @param pageQuery 分页
*/
@Override
public TableDataInfo<FlowInstanceVo> selectFinishInstanceList(FlowInstanceBo flowInstanceBo, PageQuery pageQuery) {
QueryWrapper<FlowInstanceBo> queryWrapper = buildQueryWrapper(flowInstanceBo);
queryWrapper.in("fi.flow_status", BusinessStatusEnum.finishStatus());
Page<FlowInstanceVo> page = flwInstanceMapper.selectInstanceList(pageQuery.build(), queryWrapper);
return TableDataInfo.build(page);
}
/**
* 根据业务id查询流程实例详细信息
*
* @param businessId 业务id
* @return 结果
*/
@Override
public FlowInstanceVo queryByBusinessId(Long businessId) {
FlowInstance instance = this.selectInstByBusinessId(Convert.toStr(businessId));
FlowInstanceVo instanceVo = BeanUtil.toBean(instance, FlowInstanceVo.class);
Definition definition = defService.getById(instanceVo.getDefinitionId());
instanceVo.setFlowName(definition.getFlowName());
instanceVo.setFlowCode(definition.getFlowCode());
instanceVo.setVersion(definition.getVersion());
instanceVo.setFormCustom(definition.getFormCustom());
instanceVo.setFormPath(definition.getFormPath());
instanceVo.setCategory(definition.getCategory());
return instanceVo;
}
/**
* 通用查询条件
*
* @param flowInstanceBo 查询条件
* @return 查询条件构造方法
*/
private QueryWrapper<FlowInstanceBo> buildQueryWrapper(FlowInstanceBo flowInstanceBo) {
QueryWrapper<FlowInstanceBo> queryWrapper = Wrappers.query();
queryWrapper.like(StringUtils.isNotBlank(flowInstanceBo.getNodeName()), "fi.node_name", flowInstanceBo.getNodeName());
queryWrapper.like(StringUtils.isNotBlank(flowInstanceBo.getFlowName()), "fd.flow_name", flowInstanceBo.getFlowName());
queryWrapper.like(StringUtils.isNotBlank(flowInstanceBo.getFlowCode()), "fd.flow_code", flowInstanceBo.getFlowCode());
if (StringUtils.isNotBlank(flowInstanceBo.getCategory())) {
List<Long> categoryIds = flwCategoryMapper.selectCategoryIdsByParentId(Convert.toLong(flowInstanceBo.getCategory()));
queryWrapper.in("fd.category", StreamUtils.toList(categoryIds, Convert::toStr));
}
queryWrapper.eq(StringUtils.isNotBlank(flowInstanceBo.getBusinessId()), "fi.business_id", flowInstanceBo.getBusinessId());
queryWrapper.in(CollUtil.isNotEmpty(flowInstanceBo.getCreateByIds()), "fi.create_by", flowInstanceBo.getCreateByIds());
queryWrapper.eq("fi.del_flag", "0");
queryWrapper.orderByDesc("fi.create_time");
return queryWrapper;
}
/**
* 根据业务id查询流程实例
*
* @param businessId 业务id
*/
@Override
public FlowInstance selectInstByBusinessId(String businessId) {
return flowInstanceMapper.selectOne(new LambdaQueryWrapper<FlowInstance>().eq(FlowInstance::getBusinessId, businessId));
}
/**
* 按照实例id查询流程实例
*
* @param instanceId 实例id
*/
@Override
public FlowInstance selectInstById(Long instanceId) {
return flowInstanceMapper.selectById(instanceId);
}
/**
* 按照实例id查询流程实例
*
* @param instanceIds 实例id
*/
@Override
public List<FlowInstance> selectInstListByIdList(List<Long> instanceIds) {
return flowInstanceMapper.selectByIds(instanceIds);
}
/**
* 按照业务id删除流程实例
*
* @param businessIds 业务id
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteByBusinessIds(List<Long> businessIds) {
List<FlowInstance> flowInstances = flowInstanceMapper.selectList(new LambdaQueryWrapper<FlowInstance>().in(FlowInstance::getBusinessId, StreamUtils.toList(businessIds, Convert::toStr)));
if (CollUtil.isEmpty(flowInstances)) {
log.warn("未找到对应的流程实例信息,无法执行删除操作。");
return false;
}
return insService.remove(StreamUtils.toList(flowInstances, FlowInstance::getId));
}
/**
* 按照实例id删除流程实例
*
* @param instanceIds 实例id
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteByInstanceIds(List<Long> instanceIds) {
// 获取实例信息
List<Instance> instances = insService.getByIds(instanceIds);
if (CollUtil.isEmpty(instances)) {
log.warn("未找到对应的流程实例信息,无法执行删除操作。");
return false;
}
// 获取定义信息
Map<Long, Definition> definitionMap = StreamUtils.toMap(
defService.getByIds(StreamUtils.toList(instances, Instance::getDefinitionId)),
Definition::getId,
Function.identity()
);
try {
// 逐一触发删除事件
instances.forEach(instance -> {
Definition definition = definitionMap.get(instance.getDefinitionId());
if (ObjectUtil.isNull(definition)) {
log.warn("实例 ID: {} 对应的流程定义信息未找到,跳过删除事件触发。", instance.getId());
return;
}
flowProcessEventHandler.processDeleteHandler(definition.getFlowCode(), instance.getBusinessId());
});
// 删除实例
boolean remove = insService.remove(instanceIds);
if (!remove) {
log.warn("删除流程实例失败!");
throw new ServiceException("删除流程实例失败");
}
} catch (Exception e) {
log.warn("操作失败!{}", e.getMessage());
throw new ServiceException(e.getMessage());
}
return true;
}
/**
* 按照实例id删除已完成的流程实例
*
* @param instanceIds 实例id
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean deleteHisByInstanceIds(List<Long> instanceIds) {
// 获取实例信息
List<Instance> instances = insService.getByIds(instanceIds);
if (CollUtil.isEmpty(instances)) {
log.warn("未找到对应的流程实例信息,无法执行删除操作。");
return false;
}
// 获取定义信息
Map<Long, Definition> definitionMap = StreamUtils.toMap(
defService.getByIds(StreamUtils.toList(instances, Instance::getDefinitionId)),
Definition::getId,
Function.identity()
);
try {
// 逐一触发删除事件
instances.forEach(instance -> {
Definition definition = definitionMap.get(instance.getDefinitionId());
if (ObjectUtil.isNull(definition)) {
log.warn("实例 ID: {} 对应的流程定义信息未找到,跳过删除事件触发。", instance.getId());
return;
}
flowProcessEventHandler.processDeleteHandler(definition.getFlowCode(), instance.getBusinessId());
});
List<FlowTask> flowTaskList = flwTaskService.selectByInstIds(instanceIds);
if (CollUtil.isNotEmpty(flowTaskList)) {
FlowEngine.userService().deleteByTaskIds(StreamUtils.toList(flowTaskList, FlowTask::getId));
}
FlowEngine.taskService().deleteByInsIds(instanceIds);
FlowEngine.hisTaskService().deleteByInsIds(instanceIds);
FlowEngine.insService().removeByIds(instanceIds);
} catch (Exception e) {
log.warn("操作失败!{}", e.getMessage());
throw new ServiceException(e.getMessage());
}
return true;
}
/**
* 撤销流程
*
* @param bo 参数
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean cancelProcessApply(FlowCancelBo bo) {
try {
Instance instance = selectInstByBusinessId(bo.getBusinessId());
if (instance == null) {
throw new ServiceException(ExceptionCons.NOT_FOUNT_INSTANCE);
}
Definition definition = defService.getById(instance.getDefinitionId());
if (definition == null) {
throw new ServiceException(ExceptionCons.NOT_FOUNT_DEF);
}
String message = bo.getMessage();
String userIdStr = LoginHelper.getUserIdStr();
BusinessStatusEnum.checkCancelStatus(instance.getFlowStatus());
FlowParams flowParams = FlowParams.build()
.message(message)
.flowStatus(BusinessStatusEnum.CANCEL.getStatus())
.hisStatus(BusinessStatusEnum.CANCEL.getStatus())
.handler(userIdStr)
.ignore(true);
taskService.revoke(instance.getId(), flowParams);
} catch (Exception e) {
log.error("撤销失败: {}", e.getMessage(), e);
throw new ServiceException(e.getMessage());
}
return true;
}
/**
* 获取当前登陆人发起的流程实例
*
* @param instanceBo 流程实例
* @param pageQuery 分页
*/
@Override
public TableDataInfo<FlowInstanceVo> selectCurrentInstanceList(FlowInstanceBo instanceBo, PageQuery pageQuery) {
QueryWrapper<FlowInstanceBo> queryWrapper = buildQueryWrapper(instanceBo);
queryWrapper.eq("fi.create_by", LoginHelper.getUserIdStr());
Page<FlowInstanceVo> page = flwInstanceMapper.selectInstanceList(pageQuery.build(), queryWrapper);
return TableDataInfo.build(page);
}
/**
* 获取流程图,流程记录
*
* @param businessId 业务id
*/
@Override
public Map<String, Object> flowHisTaskList(String businessId) {
FlowInstance flowInstance = this.selectInstByBusinessId(businessId);
if (ObjectUtil.isNull(flowInstance)) {
throw new ServiceException(ExceptionCons.NOT_FOUNT_INSTANCE);
}
Long instanceId = flowInstance.getId();
// 先组装待审批任务(运行中的任务)
List<FlowHisTaskVo> runningTaskVos = new ArrayList<>();
List<FlowTask> runningTasks = flwTaskService.selectByInstId(instanceId);
if (CollUtil.isNotEmpty(runningTasks)) {
runningTaskVos = BeanUtil.copyToList(runningTasks, FlowHisTaskVo.class);
List<User> associatedUsers = FlowEngine.userService()
.getByAssociateds(StreamUtils.toList(runningTasks, FlowTask::getId));
Map<Long, List<User>> taskUserMap = StreamUtils.groupByKey(associatedUsers, User::getAssociated);
for (FlowHisTaskVo vo : runningTaskVos) {
vo.setFlowStatus(TaskStatusEnum.WAITING.getStatus());
vo.setUpdateTime(null);
vo.setRunDuration(null);
List<User> users = taskUserMap.get(vo.getId());
if (CollUtil.isNotEmpty(users)) {
vo.setApprover(StreamUtils.join(users, User::getProcessedBy));
}
}
}
// 再组装历史任务(已处理任务)
List<FlowHisTaskVo> hisTaskVos = new ArrayList<>();
List<FlowHisTask> hisTasks = flowHisTaskMapper.selectList(
new LambdaQueryWrapper<FlowHisTask>()
.eq(FlowHisTask::getInstanceId, instanceId)
.eq(FlowHisTask::getNodeType, NodeType.BETWEEN.getKey())
.orderByDesc(FlowHisTask::getUpdateTime)
);
if (CollUtil.isNotEmpty(hisTasks)) {
hisTaskVos = BeanUtil.copyToList(hisTasks, FlowHisTaskVo.class);
}
// 结果列表,待审批任务在前,历史任务在后
List<FlowHisTaskVo> combinedList = new ArrayList<>();
combinedList.addAll(runningTaskVos);
combinedList.addAll(hisTaskVos);
return Map.of("list", combinedList, "instanceId", instanceId);
}
/**
* 按照实例id更新状态
*
* @param instanceId 实例id
* @param status 状态
*/
@Override
public void updateStatus(Long instanceId, String status) {
LambdaUpdateWrapper<FlowInstance> wrapper = new LambdaUpdateWrapper<>();
wrapper.set(FlowInstance::getFlowStatus, status);
wrapper.eq(FlowInstance::getId, instanceId);
flowInstanceMapper.update(wrapper);
}
/**
* 获取流程变量
*
* @param instanceId 实例id
*/
@Override
public Map<String, Object> instanceVariable(Long instanceId) {
FlowInstance flowInstance = flowInstanceMapper.selectById(instanceId);
Map<String, Object> variableMap = Optional.ofNullable(flowInstance.getVariableMap()).orElse(Collections.emptyMap());
List<Map<String, Object>> variableList = variableMap.entrySet().stream()
.map(entry -> Map.of("key", entry.getKey(), "value", entry.getValue()))
.toList();
return Map.of("variableList", variableList, "variable", flowInstance.getVariable());
}
/**
* 设置流程变量
*
* @param bo 参数
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateVariable(FlowVariableBo bo) {
FlowInstance flowInstance = flowInstanceMapper.selectById(bo.getInstanceId());
if (flowInstance == null) {
throw new ServiceException(ExceptionCons.NOT_FOUNT_INSTANCE);
}
try {
Map<String, Object> variableMap = new HashMap<>(Optional.ofNullable(flowInstance.getVariableMap()).orElse(Collections.emptyMap()));
if (!variableMap.containsKey(bo.getKey())) {
log.error("变量不存在: {}", bo.getKey());
return false;
}
variableMap.put(bo.getKey(), bo.getValue());
flowInstance.setVariable(FlowEngine.jsonConvert.objToStr(variableMap));
flowInstanceMapper.updateById(flowInstance);
} catch (Exception e) {
log.error("设置流程变量失败: {}", e.getMessage(), e);
throw new ServiceException(e.getMessage());
}
return true;
}
/**
* 设置流程变量
*
* @param instanceId 实例id
* @param variable 流程变量
*/
@Override
public void setVariable(Long instanceId, Map<String, Object> variable) {
Instance instance = insService.getById(instanceId);
if (instance != null) {
taskService.mergeVariable(instance, variable);
insService.updateById(instance);
}
}
/**
* 按任务id查询实例
*
* @param taskId 任务id
*/
@Override
public FlowInstance selectByTaskId(Long taskId) {
Task task = taskService.getById(taskId);
if (task == null) {
FlowHisTask flowHisTask = flwTaskService.selectHisTaskById(taskId);
if (flowHisTask != null) {
return this.selectInstById(flowHisTask.getInstanceId());
}
} else {
return this.selectInstById(task.getInstanceId());
}
return null;
}
/**
* 作废流程
*
* @param bo 参数
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean processInvalid(FlowInvalidBo bo) {
try {
Instance instance = insService.getById(bo.getId());
if (instance != null) {
BusinessStatusEnum.checkInvalidStatus(instance.getFlowStatus());
}
FlowParams flowParams = FlowParams.build()
.message(bo.getComment())
.flowStatus(BusinessStatusEnum.INVALID.getStatus())
.hisStatus(TaskStatusEnum.INVALID.getStatus())
.ignore(true);
taskService.terminationByInsId(bo.getId(), flowParams);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new ServiceException(e.getMessage());
}
}
}

View File

@@ -0,0 +1,354 @@
package org.dromara.workflow.service.impl;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.dto.DictTypeDTO;
import org.dromara.common.core.service.DictService;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.warm.flow.core.FlowEngine;
import org.dromara.warm.flow.core.utils.CollUtil;
import org.dromara.warm.flow.core.utils.ExpressionUtil;
import org.dromara.warm.flow.ui.service.NodeExtService;
import org.dromara.warm.flow.ui.vo.NodeExt;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.ButtonPermissionEnum;
import org.dromara.workflow.common.enums.CopySettingEnum;
import org.dromara.workflow.common.enums.NodeExtEnum;
import org.dromara.workflow.common.enums.VariablesEnum;
import org.dromara.workflow.domain.vo.ButtonPermissionVo;
import org.dromara.workflow.domain.vo.NodeExtVo;
import org.dromara.workflow.service.IFlwNodeExtService;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
/**
* 流程设计器-节点扩展属性
*
* @author AprilWind
*/
@ConditionalOnEnable
@Slf4j
@RequiredArgsConstructor
@Service
public class FlwNodeExtServiceImpl implements NodeExtService, IFlwNodeExtService {
/**
* 存储不同 dictType 对应的配置信息
*/
private static final Map<String, Map<String, Object>> CHILD_NODE_MAP;
static {
CHILD_NODE_MAP = Map.of(
CopySettingEnum.class.getSimpleName(),
Map.of(
"label", "抄送对象",
"type", 5,
"must", false,
"multiple", false,
"desc", "设置该节点的抄送办理人"
),
VariablesEnum.class.getSimpleName(),
Map.of(
"label", "自定义参数",
"type", 2,
"must", false,
"multiple", false,
"desc", "节点执行时可设置自定义参数多个参数以逗号分隔key1=value1,key2=value2"
),
ButtonPermissionEnum.class.getSimpleName(),
Map.of(
"label", "权限按钮",
"type", 4,
"must", false,
"multiple", true,
"desc", "控制该节点的按钮权限"
)
);
}
private final DictService dictService;
/**
* 获取节点扩展属性
*
* @return 节点扩展属性列表
*/
@Override
public List<NodeExt> getNodeExt() {
List<NodeExt> nodeExtList = new ArrayList<>();
// 构建基础设置页面
nodeExtList.add(buildNodeExt("wf_basic_tab", "基础设置", 1,
List.of(CopySettingEnum.class, VariablesEnum.class)));
// 构建按钮权限页面
nodeExtList.add(buildNodeExt("wf_button_tab", "权限", 2,
List.of(ButtonPermissionEnum.class)));
// 自定义构建 规则参考 NodeExt 与 warm-flow文档说明
// nodeExtList.add(buildNodeExt("xxx_xxx", "xxx", 1, List);
return nodeExtList;
}
/**
* 构建一个 `NodeExt` 对象
*
* @param code 唯一编码
* @param name 名称(新页签时,作为页签名称)
* @param type 节点类型1: 基础设置2: 新页签)
* @param sources 数据来源(枚举类或字典类型)
* @return 构建的 `NodeExt` 对象
*/
@SuppressWarnings("unchecked cast")
private NodeExt buildNodeExt(String code, String name, int type, List<Object> sources) {
NodeExt nodeExt = new NodeExt();
nodeExt.setCode(code);
nodeExt.setType(type);
nodeExt.setName(name);
nodeExt.setChilds(sources.stream()
.map(source -> {
if (source instanceof Class<?> clazz && NodeExtEnum.class.isAssignableFrom(clazz)) {
return buildChildNode((Class<? extends NodeExtEnum>) clazz);
} else if (source instanceof String dictType) {
return buildChildNode(dictType);
}
return null;
})
.filter(ObjectUtil::isNotNull)
.toList()
);
return nodeExt;
}
/**
* 根据枚举类型构建一个 `ChildNode` 对象
*
* @param enumClass 枚举类,必须实现 `NodeExtEnum` 接口
* @return 构建的 `ChildNode` 对象
*/
private NodeExt.ChildNode buildChildNode(Class<? extends NodeExtEnum> enumClass) {
if (!enumClass.isEnum()) {
return null;
}
String simpleName = enumClass.getSimpleName();
NodeExt.ChildNode childNode = new NodeExt.ChildNode();
Map<String, Object> map = CHILD_NODE_MAP.get(simpleName);
// 编码此json中唯
childNode.setCode(simpleName);
// label名称
childNode.setLabel(Convert.toStr(map.get("label")));
// 1输入框 2文本域 3下拉框 4选择框 5用户选择器
childNode.setType(Convert.toInt(map.get("type"), 1));
// 是否必填
childNode.setMust(Convert.toBool(map.get("must"), false));
// 是否多选
childNode.setMultiple(Convert.toBool(map.get("multiple"), true));
// 描述
childNode.setDesc(Convert.toStr(map.get("desc"), null));
// 字典,下拉框和复选框时用到
childNode.setDict(Arrays.stream(enumClass.getEnumConstants())
.map(NodeExtEnum.class::cast)
.map(x ->
new NodeExt.DictItem(x.getLabel(), x.getValue(), x.isSelected())
).toList());
return childNode;
}
/**
* 根据字典类型构建 `ChildNode` 对象
*
* @param dictType 字典类型
* @return 构建的 `ChildNode` 对象
*/
private NodeExt.ChildNode buildChildNode(String dictType) {
DictTypeDTO dictTypeDTO = dictService.getDictType(dictType);
if (ObjectUtil.isNull(dictTypeDTO)) {
return null;
}
NodeExt.ChildNode childNode = new NodeExt.ChildNode();
// 编码此json中唯一
childNode.setCode(dictType);
// label名称
childNode.setLabel(dictTypeDTO.getDictName());
// 1输入框 2文本域 3下拉框 4选择框 5用户选择器
childNode.setType(3);
// 是否必填
childNode.setMust(false);
// 是否多选
childNode.setMultiple(true);
// 描述 (可根据描述参数解析更多配置如typemustmultiple等)
childNode.setDesc(dictTypeDTO.getRemark());
// 字典,下拉框和复选框时用到
childNode.setDict(dictService.getDictData(dictType)
.stream().map(x ->
new NodeExt.DictItem(x.getDictLabel(), x.getDictValue(), Convert.toBool(x.getIsDefault(), false))
).toList());
return childNode;
}
/**
* 解析扩展属性 JSON 并构建 Node 扩展属性对象
* <p>
* 根据传入的 JSON 字符串,将扩展属性分为三类:
* 1. ButtonPermissionEnum解析为按钮权限列表标记每个按钮是否勾选
* 2. CopySettingEnum解析为抄送对象 ID 集合
* 3. VariablesEnum解析为自定义参数 Map
*
* <p>示例 JSON
* [
* {"code": "ButtonPermissionEnum", "value": "back,termination"},
* {"code": "CopySettingEnum", "value": "1,3,4,#{@spelRuleComponent.selectDeptLeaderById(#deptId", "#roleId)}"},
* {"code": "VariablesEnum", "value": "key1=value1,key2=value2"}
* ]
*
* @param ext 扩展属性 JSON 字符串
* @param variable 流程变量
* @return NodeExtVo 对象,封装按钮权限列表、抄送对象集合和自定义参数 Map
*/
@Override
public NodeExtVo parseNodeExt(String ext, Map<String, Object> variable) {
NodeExtVo nodeExtVo = new NodeExtVo();
// 解析 JSON 为 Dict 列表
List<Dict> nodeExtMap = JsonUtils.parseArrayMap(ext);
if (ObjectUtil.isEmpty(nodeExtMap)) {
return nodeExtVo;
}
for (Dict nodeExt : nodeExtMap) {
String code = nodeExt.getStr("code");
String value = nodeExt.getStr("value");
if (ButtonPermissionEnum.class.getSimpleName().equals(code)) {
// 解析按钮权限
// 将 value 拆分为 Set<String>,便于精确匹配
Set<String> buttonSet = StringUtils.str2Set(value, StringUtils.SEPARATOR);
// 获取按钮字典配置
NodeExt.ChildNode childNode = buildChildNode(ButtonPermissionEnum.class);
// 构建 ButtonPermissionVo 列表
List<ButtonPermissionVo> buttonList = Optional.ofNullable(childNode)
.map(NodeExt.ChildNode::getDict)
.orElse(List.of())
.stream()
.map(dict -> new ButtonPermissionVo(dict.getValue(), buttonSet.contains(dict.getValue())))
.toList();
nodeExtVo.setButtonPermissions(buttonList);
} else if (CopySettingEnum.class.getSimpleName().equals(code)) {
List<String> permissions = spelSmartSplit(value).stream()
.map(s -> {
List<String> result = ExpressionUtil.evalVariable(s, variable);
if (CollUtil.isNotEmpty(result)) {
return result;
}
return Collections.singletonList(s);
}).filter(Objects::nonNull)
.flatMap(List::stream)
.distinct()
.collect(Collectors.toList());
List<String> copySettings = FlowEngine.permissionHandler().convertPermissions(permissions);
// 解析抄送对象 ID 集合
nodeExtVo.setCopySettings(new HashSet<>(copySettings));
} else if (VariablesEnum.class.getSimpleName().equals(code)) {
// 解析自定义参数
// 将 key=value 字符串拆分为 Map
Map<String, String> variables = Arrays.stream(StringUtils.split(value, StringUtils.SEPARATOR))
.map(s -> StringUtils.split(s, "="))
.filter(arr -> arr.length == 2)
.collect(Collectors.toMap(arr -> arr[0], arr -> arr[1]));
nodeExtVo.setVariables(variables);
} else {
// 未知扩展类型,记录日志
log.warn("未知扩展类型code={}, value={}", code, value);
}
}
return nodeExtVo;
}
/**
* 按逗号分割字符串,但保留 #{...} 表达式和字符串常量中的逗号
*/
private static List<String> spelSmartSplit(String str) {
List<String> result = new ArrayList<>();
if (str == null || str.trim().isEmpty()) {
return result;
}
StringBuilder token = new StringBuilder();
// #{...} 的嵌套深度
int depth = 0;
// 是否在字符串常量中(" 或 '
boolean inString = false;
// 当前字符串引号类型
char stringQuote = 0;
for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
// 检测进入 SpEL 表达式 #{...}
if (!inString && c == '#' && depth == 0 && checkNext(str, i, '{')) {
depth++;
token.append("#{");
// 跳过 {
i++;
continue;
}
// 在表达式中遇到 { 或 } 改变嵌套深度
if (!inString && depth > 0) {
if (c == '{') {
depth++;
} else if (c == '}') {
depth--;
}
token.append(c);
continue;
}
// 检测字符串开始/结束
if (depth > 0 && (c == '"' || c == '\'')) {
if (!inString) {
inString = true;
stringQuote = c;
} else if (stringQuote == c) {
inString = false;
}
token.append(c);
continue;
}
// 外层逗号才分割
if (c == ',' && depth == 0 && !inString) {
String part = token.toString().trim();
if (!part.isEmpty()) {
result.add(part);
}
token.setLength(0);
continue;
}
token.append(c);
}
// 添加最后一个
String part = token.toString().trim();
if (!part.isEmpty()) {
result.add(part);
}
return result;
}
private static boolean checkNext(String str, int index, char expected) {
return index + 1 < str.length() && str.charAt(index + 1) == expected;
}
}

View File

@@ -0,0 +1,190 @@
package org.dromara.workflow.service.impl;
import cn.hutool.core.collection.CollUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.constant.SystemConstants;
import org.dromara.common.core.domain.dto.TaskAssigneeDTO;
import org.dromara.common.core.domain.model.TaskAssigneeBody;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.FlowSpel;
import org.dromara.workflow.domain.bo.FlowSpelBo;
import org.dromara.workflow.domain.vo.FlowSpelVo;
import org.dromara.workflow.mapper.FlwSpelMapper;
import org.dromara.workflow.service.IFlwSpelService;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* 流程spel达式定义Service业务层处理
*
* @author Michelle.Chung
* @date 2025-07-04
*/
@ConditionalOnEnable
@Slf4j
@RequiredArgsConstructor
@Service
public class FlwSpelServiceImpl implements IFlwSpelService {
private final FlwSpelMapper baseMapper;
/**
* 查询流程spel达式定义
*
* @param id 主键
* @return 流程spel达式定义
*/
@Override
public FlowSpelVo queryById(Long id){
return baseMapper.selectVoById(id);
}
/**
* 分页查询流程spel达式定义列表
*
* @param bo 查询条件
* @param pageQuery 分页参数
* @return 流程spel达式定义分页列表
*/
@Override
public TableDataInfo<FlowSpelVo> queryPageList(FlowSpelBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<FlowSpel> lqw = buildQueryWrapper(bo);
Page<FlowSpelVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询符合条件的流程spel达式定义列表
*
* @param bo 查询条件
* @return 流程spel达式定义列表
*/
@Override
public List<FlowSpelVo> queryList(FlowSpelBo bo) {
LambdaQueryWrapper<FlowSpel> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<FlowSpel> buildQueryWrapper(FlowSpelBo bo) {
Map<String, Object> params = bo.getParams();
LambdaQueryWrapper<FlowSpel> lqw = Wrappers.lambdaQuery();
lqw.orderByAsc(FlowSpel::getId);
lqw.like(StringUtils.isNotBlank(bo.getComponentName()), FlowSpel::getComponentName, bo.getComponentName());
lqw.like(StringUtils.isNotBlank(bo.getMethodName()), FlowSpel::getMethodName, bo.getMethodName());
lqw.eq(StringUtils.isNotBlank(bo.getMethodParams()), FlowSpel::getMethodParams, bo.getMethodParams());
lqw.eq(StringUtils.isNotBlank(bo.getViewSpel()), FlowSpel::getViewSpel, bo.getViewSpel());
lqw.eq(StringUtils.isNotBlank(bo.getStatus()), FlowSpel::getStatus, bo.getStatus());
lqw.like(StringUtils.isNotBlank(bo.getRemark()), FlowSpel::getRemark, bo.getRemark());
return lqw;
}
/**
* 新增流程spel达式定义
*
* @param bo 流程spel达式定义
* @return 是否新增成功
*/
@Override
public Boolean insertByBo(FlowSpelBo bo) {
FlowSpel add = MapstructUtils.convert(bo, FlowSpel.class);
validEntityBeforeSave(add);
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
}
return flag;
}
/**
* 修改流程spel达式定义
*
* @param bo 流程spel达式定义
* @return 是否修改成功
*/
@Override
public Boolean updateByBo(FlowSpelBo bo) {
FlowSpel update = MapstructUtils.convert(bo, FlowSpel.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(FlowSpel entity){
//TODO 做一些数据校验,如唯一约束
}
/**
* 校验并批量删除流程spel达式定义信息
*
* @param ids 待删除的主键集合
* @param isValid 是否进行有效性校验
* @return 是否删除成功
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if(isValid){
//TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteByIds(ids) > 0;
}
/**
* 查询spel并返回任务指派的列表支持分页
*
* @param taskQuery 查询条件
* @return 办理人
*/
@Override
public TaskAssigneeDTO selectSpelByTaskAssigneeList(TaskAssigneeBody taskQuery) {
PageQuery pageQuery = new PageQuery(taskQuery.getPageSize(), taskQuery.getPageNum());
FlowSpelBo bo = new FlowSpelBo();
bo.setViewSpel(taskQuery.getHandlerCode());
bo.setRemark(taskQuery.getHandlerName());
bo.setStatus(SystemConstants.NORMAL);
Map<String, Object> params = bo.getParams();
params.put("beginTime", taskQuery.getBeginTime());
params.put("endTime", taskQuery.getEndTime());
TableDataInfo<FlowSpelVo> page = this.queryPageList(bo, pageQuery);
// 使用封装的字段映射方法进行转换
List<TaskAssigneeDTO.TaskHandler> handlers = TaskAssigneeDTO.convertToHandlerList(page.getRows(),
FlowSpelVo::getViewSpel, item -> "", FlowSpelVo::getRemark, item -> "", FlowSpelVo::getCreateTime);
return new TaskAssigneeDTO(page.getTotal(), handlers);
}
/**
* 根据视图 SpEL 表达式列表,查询对应的备注信息
*
* @param viewSpels SpEL 表达式列表
* @return 映射表key 为 SpEL 表达式value 为对应备注;若为空则返回空 Map
*/
@Override
public Map<String, String> selectRemarksBySpels(List<String> viewSpels) {
if (CollUtil.isEmpty(viewSpels)) {
return Collections.emptyMap();
}
List<FlowSpel> list = baseMapper.selectList(
new LambdaQueryWrapper<FlowSpel>()
.select(FlowSpel::getViewSpel, FlowSpel::getRemark)
.in(FlowSpel::getViewSpel, viewSpels)
);
return StreamUtils.toMap(list, FlowSpel::getViewSpel, x ->
StringUtils.isEmpty(x.getRemark()) ? "" : x.getRemark()
);
}
}

View File

@@ -0,0 +1,291 @@
package org.dromara.workflow.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Pair;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.dto.DeptDTO;
import org.dromara.common.core.domain.dto.TaskAssigneeDTO;
import org.dromara.common.core.domain.dto.UserDTO;
import org.dromara.common.core.domain.model.TaskAssigneeBody;
import org.dromara.common.core.enums.FormatsType;
import org.dromara.common.core.service.*;
import org.dromara.common.core.utils.DateUtils;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.warm.flow.ui.dto.HandlerFunDto;
import org.dromara.warm.flow.ui.dto.HandlerQuery;
import org.dromara.warm.flow.ui.dto.TreeFunDto;
import org.dromara.warm.flow.ui.service.HandlerSelectService;
import org.dromara.warm.flow.ui.vo.HandlerFeedBackVo;
import org.dromara.warm.flow.ui.vo.HandlerSelectVo;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.TaskAssigneeEnum;
import org.dromara.workflow.service.IFlwSpelService;
import org.dromara.workflow.service.IFlwTaskAssigneeService;
import org.springframework.stereotype.Service;
import java.util.*;
import java.util.stream.Collectors;
/**
* 流程设计器-获取办理人权限设置列表
*
* @author AprilWind
*/
@ConditionalOnEnable
@Slf4j
@RequiredArgsConstructor
@Service
public class FlwTaskAssigneeServiceImpl implements IFlwTaskAssigneeService, HandlerSelectService {
private static final String DEFAULT_GROUP_NAME = "默认分组";
private final TaskAssigneeService taskAssigneeService;
private final UserService userService;
private final DeptService deptService;
private final RoleService roleService;
private final PostService postService;
private final IFlwSpelService spelService;
/**
* 获取办理人权限设置列表tabs页签
*
* @return tabs页签
*/
@Override
public List<String> getHandlerType() {
return TaskAssigneeEnum.getAssigneeTypeList();
}
/**
* 获取办理列表, 同时构建左侧部门树状结构
*
* @param query 查询条件
* @return HandlerSelectVo
*/
@Override
public HandlerSelectVo getHandlerSelect(HandlerQuery query) {
// 获取任务办理类型
TaskAssigneeEnum type = TaskAssigneeEnum.fromDesc(query.getHandlerType());
// 转换查询条件为 TaskAssigneeBody
TaskAssigneeBody taskQuery = BeanUtil.toBean(query, TaskAssigneeBody.class);
// 统一查询并构建业务数据
TaskAssigneeDTO dto = fetchTaskAssigneeData(type, taskQuery);
List<DeptDTO> depts = fetchDeptData(type);
return getHandlerSelectVo(buildHandlerData(dto, type), buildDeptTree(depts));
}
/**
* 办理人权限名称回显
*
* @param storageIds 入库主键集合
* @return 结果
*/
@Override
public List<HandlerFeedBackVo> handlerFeedback(List<String> storageIds) {
if (CollUtil.isEmpty(storageIds)) {
return Collections.emptyList();
}
// 解析并归类 ID同时记录原始顺序和对应解析结果
Map<TaskAssigneeEnum, List<String>> typeIdMap = new EnumMap<>(TaskAssigneeEnum.class);
Map<String, Pair<TaskAssigneeEnum, String>> parsedMap = new LinkedHashMap<>();
for (String storageId : storageIds) {
Pair<TaskAssigneeEnum, String> parsed = this.parseStorageId(storageId);
parsedMap.put(storageId, parsed);
if (parsed != null) {
typeIdMap.computeIfAbsent(parsed.getKey(), k -> new ArrayList<>()).add(parsed.getValue());
}
}
// 查询所有类型对应的 ID 名称映射
Map<TaskAssigneeEnum, Map<String, String>> nameMap = new EnumMap<>(TaskAssigneeEnum.class);
typeIdMap.forEach((type, ids) -> nameMap.put(type, this.getNamesByType(type, ids)));
// 组装返回结果,保持原始顺序
return parsedMap.entrySet().stream()
.map(entry -> {
String storageId = entry.getKey();
Pair<TaskAssigneeEnum, String> parsed = entry.getValue();
String handlerName = (parsed == null) ? null
: nameMap.getOrDefault(parsed.getKey(), Collections.emptyMap())
.get(parsed.getValue());
return new HandlerFeedBackVo(storageId, handlerName);
}).toList();
}
/**
* 根据任务办理类型查询对应的数据
*/
private TaskAssigneeDTO fetchTaskAssigneeData(TaskAssigneeEnum type, TaskAssigneeBody taskQuery) {
return switch (type) {
case USER -> taskAssigneeService.selectUsersByTaskAssigneeList(taskQuery);
case ROLE -> taskAssigneeService.selectRolesByTaskAssigneeList(taskQuery);
case DEPT -> taskAssigneeService.selectDeptsByTaskAssigneeList(taskQuery);
case POST -> taskAssigneeService.selectPostsByTaskAssigneeList(taskQuery);
case SPEL -> spelService.selectSpelByTaskAssigneeList(taskQuery);
};
}
/**
* 根据任务办理类型获取部门数据
*/
private List<DeptDTO> fetchDeptData(TaskAssigneeEnum type) {
if (type.needsDeptService()) {
return deptService.selectDeptsByList();
}
return new ArrayList<>();
}
/**
* 获取权限分组名称
*
* @param type 任务分配人枚举
* @param groupName 权限分组
* @return 权限分组名称
*/
private String getGroupName(TaskAssigneeEnum type, String groupName) {
if (StringUtils.isEmpty(groupName)) {
return DEFAULT_GROUP_NAME;
}
if (type.needsDeptService()) {
return deptService.selectDeptNameByIds(groupName);
}
return DEFAULT_GROUP_NAME;
}
/**
* 构建部门树状结构
*/
private TreeFunDto<DeptDTO> buildDeptTree(List<DeptDTO> depts) {
return new TreeFunDto<>(depts)
.setId(dept -> Convert.toStr(dept.getDeptId()))
.setName(DeptDTO::getDeptName)
.setParentId(dept -> Convert.toStr(dept.getParentId()));
}
/**
* 构建任务办理人数据
*/
private HandlerFunDto<TaskAssigneeDTO.TaskHandler> buildHandlerData(TaskAssigneeDTO dto, TaskAssigneeEnum type) {
return new HandlerFunDto<>(dto.getList(), dto.getTotal())
.setStorageId(assignee -> type.getCode() + assignee.getStorageId())
.setHandlerCode(assignee -> StringUtils.blankToDefault(assignee.getHandlerCode(), ""))
.setHandlerName(assignee -> StringUtils.blankToDefault(assignee.getHandlerName(), ""))
.setGroupName(assignee -> this.getGroupName(type, assignee.getGroupName()))
.setCreateTime(assignee -> DateUtils.parseDateToStr(FormatsType.YYYY_MM_DD_HH_MM_SS, assignee.getCreateTime()));
}
/**
* 批量解析多个存储标识符storageIds按类型分类并合并查询用户列表
* 输入格式支持多个以逗号分隔的标识(如 "user:123,role:456,789"
* 会自动去重返回结果,非法格式的标识将被忽略
*
* @param storageIds 多个存储标识符字符串(逗号分隔)
* @return 合并后的用户列表,去重后返回,非法格式的标识将被跳过
*/
@Override
public List<UserDTO> fetchUsersByStorageIds(String storageIds) {
if (StringUtils.isEmpty(storageIds)) {
return List.of();
}
Map<TaskAssigneeEnum, List<String>> typeIdMap = new EnumMap<>(TaskAssigneeEnum.class);
for (String storageId : storageIds.split(StringUtils.SEPARATOR)) {
Pair<TaskAssigneeEnum, String> parsed = this.parseStorageId(storageId);
if (parsed != null) {
typeIdMap.computeIfAbsent(parsed.getKey(), k -> new ArrayList<>()).add(parsed.getValue());
}
}
return typeIdMap.entrySet().stream()
.flatMap(entry -> this.getUsersByType(entry.getKey(), entry.getValue()).stream())
.distinct()
.toList();
}
/**
* 根据指定的任务分配类型TaskAssigneeEnum和 ID 列表,获取对应的用户信息列表
*
* @param type 任务分配类型表示用户、角色、部门或其他TaskAssigneeEnum 枚举值)
* @param ids 与指定分配类型关联的 ID 列表例如用户ID、角色ID、部门ID等
* @return 返回包含用户信息的列表。如果类型为用户USER则通过用户ID列表查询
* 如果类型为角色ROLE则通过角色ID列表查询
* 如果类型为部门DEPT则通过部门ID列表查询
* 如果类型为岗位POST或无法识别的类型则返回空列表
*/
private List<UserDTO> getUsersByType(TaskAssigneeEnum type, List<String> ids) {
if (type == TaskAssigneeEnum.SPEL) {
return new ArrayList<>();
}
List<Long> longIds = StreamUtils.toList(ids, Convert::toLong);
return switch (type) {
case USER -> userService.selectListByIds(longIds);
case ROLE -> userService.selectUsersByRoleIds(longIds);
case DEPT -> userService.selectUsersByDeptIds(longIds);
case POST -> userService.selectUsersByPostIds(longIds);
default -> new ArrayList<>();
};
}
/**
* 根据任务分配类型和对应 ID 列表,批量查询名称映射关系
*
* @param type 分配类型(用户、角色、部门、岗位)
* @param ids ID 列表如用户ID、角色ID等
* @return 返回 Map其中 key 为 IDvalue 为对应的名称
*/
private Map<String, String> getNamesByType(TaskAssigneeEnum type, List<String> ids) {
if (type == TaskAssigneeEnum.SPEL) {
return spelService.selectRemarksBySpels(ids);
}
List<Long> longIds = StreamUtils.toList(ids, Convert::toLong);
Map<Long, String> rawMap = switch (type) {
case USER -> userService.selectUserNamesByIds(longIds);
case ROLE -> roleService.selectRoleNamesByIds(longIds);
case DEPT -> deptService.selectDeptNamesByIds(longIds);
case POST -> postService.selectPostNamesByIds(longIds);
default -> Collections.emptyMap();
};
if (MapUtil.isEmpty(rawMap)) {
return Collections.emptyMap();
}
return rawMap.entrySet()
.stream()
.collect(Collectors.toMap(
e -> Convert.toStr(e.getKey()),
Map.Entry::getValue
));
}
/**
* 解析 storageId 字符串返回类型和ID的组合
*
* @param storageId 例如 "user:123" 或 "456"
* @return Pair(TaskAssigneeEnum, Long),如果格式非法返回 null
*/
private Pair<TaskAssigneeEnum, String> parseStorageId(String storageId) {
if (StringUtils.isBlank(storageId)) {
return null;
}
if (TaskAssigneeEnum.isSpelExpression(storageId)) {
return Pair.of(TaskAssigneeEnum.SPEL, storageId);
}
try {
String[] parts = storageId.split(StrUtil.COLON, 2);
if (parts.length < 2) {
return Pair.of(TaskAssigneeEnum.USER, parts[0]);
} else {
TaskAssigneeEnum type = TaskAssigneeEnum.fromCode(parts[0] + StrUtil.COLON);
return Pair.of(type, parts[1]);
}
} catch (Exception e) {
log.warn("解析 storageId 失败,格式非法:{},错误信息:{}", storageId, e.getMessage());
return null;
}
}
}

View File

@@ -0,0 +1,860 @@
package org.dromara.workflow.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.dto.StartProcessReturnDTO;
import org.dromara.common.core.domain.dto.UserDTO;
import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.service.UserService;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
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.json.utils.JsonUtils;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.warm.flow.core.FlowEngine;
import org.dromara.warm.flow.core.dto.FlowParams;
import org.dromara.warm.flow.core.entity.*;
import org.dromara.warm.flow.core.enums.NodeType;
import org.dromara.warm.flow.core.enums.SkipType;
import org.dromara.warm.flow.core.enums.UserType;
import org.dromara.warm.flow.core.service.*;
import org.dromara.warm.flow.core.utils.ExpressionUtil;
import org.dromara.warm.flow.core.utils.MapUtil;
import org.dromara.warm.flow.orm.entity.*;
import org.dromara.warm.flow.orm.mapper.FlowHisTaskMapper;
import org.dromara.warm.flow.orm.mapper.FlowInstanceMapper;
import org.dromara.warm.flow.orm.mapper.FlowNodeMapper;
import org.dromara.warm.flow.orm.mapper.FlowTaskMapper;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.constant.FlowConstant;
import org.dromara.workflow.common.enums.TaskAssigneeType;
import org.dromara.workflow.common.enums.TaskStatusEnum;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import org.dromara.workflow.domain.bo.*;
import org.dromara.workflow.domain.vo.FlowCopyVo;
import org.dromara.workflow.domain.vo.FlowHisTaskVo;
import org.dromara.workflow.domain.vo.FlowTaskVo;
import org.dromara.workflow.domain.vo.NodeExtVo;
import org.dromara.workflow.mapper.FlwCategoryMapper;
import org.dromara.workflow.mapper.FlwInstanceBizExtMapper;
import org.dromara.workflow.mapper.FlwTaskMapper;
import org.dromara.workflow.service.IFlwCommonService;
import org.dromara.workflow.service.IFlwNodeExtService;
import org.dromara.workflow.service.IFlwTaskAssigneeService;
import org.dromara.workflow.service.IFlwTaskService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.*;
import static org.dromara.workflow.common.constant.FlowConstant.*;
/**
* 任务 服务层实现
*
* @author may
*/
@ConditionalOnEnable
@Slf4j
@RequiredArgsConstructor
@Service
public class FlwTaskServiceImpl implements IFlwTaskService {
private final TaskService taskService;
private final InsService insService;
private final DefService defService;
private final HisTaskService hisTaskService;
private final NodeService nodeService;
private final FlowInstanceMapper flowInstanceMapper;
private final FlowTaskMapper flowTaskMapper;
private final FlowHisTaskMapper flowHisTaskMapper;
private final IdentifierGenerator identifierGenerator;
private final UserService userService;
private final FlwTaskMapper flwTaskMapper;
private final FlwCategoryMapper flwCategoryMapper;
private final FlowNodeMapper flowNodeMapper;
private final IFlwTaskAssigneeService flwTaskAssigneeService;
private final IFlwCommonService flwCommonService;
private final IFlwNodeExtService flwNodeExtService;
private final FlwInstanceBizExtMapper flwInstanceBizExtMapper;
/**
* 启动任务
*
* @param startProcessBo 启动流程参数
*/
@Override
@Transactional(rollbackFor = Exception.class)
public StartProcessReturnDTO startWorkFlow(StartProcessBo startProcessBo) {
String businessId = startProcessBo.getBusinessId();
if (StringUtils.isBlank(businessId)) {
throw new ServiceException("启动工作流时必须包含业务ID");
}
// 启动流程实例(提交申请)
Map<String, Object> variables = startProcessBo.getVariables();
// 流程发起人
variables.put(INITIATOR, LoginHelper.getUserIdStr());
// 发起人部门id
variables.put(INITIATOR_DEPT_ID, LoginHelper.getDeptId());
// 业务id
variables.put(BUSINESS_ID, businessId);
FlowInstanceBizExt bizExt = startProcessBo.getBizExt();
// 获取已有流程实例
FlowInstance flowInstance = flowInstanceMapper.selectOne(new LambdaQueryWrapper<>(FlowInstance.class)
.eq(FlowInstance::getBusinessId, businessId));
if (ObjectUtil.isNotNull(flowInstance)) {
// 已存在流程
BusinessStatusEnum.checkStartStatus(flowInstance.getFlowStatus());
List<Task> taskList = taskService.list(new FlowTask().setInstanceId(flowInstance.getId()));
taskService.mergeVariable(flowInstance, variables);
insService.updateById(flowInstance);
StartProcessReturnDTO dto = new StartProcessReturnDTO();
dto.setProcessInstanceId(taskList.get(0).getInstanceId());
dto.setTaskId(taskList.get(0).getId());
// 保存流程实例业务信息
this.buildFlowInstanceBizExt(flowInstance, bizExt);
return dto;
}
// 将流程定义内的扩展参数设置到变量中
Definition definition = FlowEngine.defService().getPublishByFlowCode(startProcessBo.getFlowCode());
Dict dict = JsonUtils.parseMap(definition.getExt());
boolean autoPass = !ObjectUtil.isNull(dict) && dict.getBool(FlowConstant.AUTO_PASS);
variables.put(FlowConstant.AUTO_PASS, autoPass);
variables.put(FlowConstant.BUSINESS_CODE, this.generateBusinessCode(bizExt));
FlowParams flowParams = FlowParams.build()
.handler(startProcessBo.getHandler())
.flowCode(startProcessBo.getFlowCode())
.variable(startProcessBo.getVariables())
.flowStatus(BusinessStatusEnum.DRAFT.getStatus());
Instance instance;
try {
instance = insService.start(businessId, flowParams);
} catch (Exception e) {
throw new ServiceException(e.getMessage());
}
// 保存流程实例业务信息
this.buildFlowInstanceBizExt(instance, bizExt);
// 申请人执行流程
List<Task> taskList = taskService.list(new FlowTask().setInstanceId(instance.getId()));
if (taskList.size() > 1) {
throw new ServiceException("请检查流程第一个环节是否为申请人!");
}
StartProcessReturnDTO dto = new StartProcessReturnDTO();
dto.setProcessInstanceId(instance.getId());
dto.setTaskId(taskList.get(0).getId());
return dto;
}
/**
* 生成业务编号,如果已有则直接返回已有值
*/
private String generateBusinessCode(FlowInstanceBizExt bizExt) {
if (StringUtils.isBlank(bizExt.getBusinessCode())) {
// TODO: 按照自己业务规则生成编号
String businessCode = Convert.toStr(System.currentTimeMillis());
bizExt.setBusinessCode(businessCode);
return businessCode;
}
return bizExt.getBusinessCode();
}
/**
* 构建流程实例业务信息
*
* @param instance 流程实例
* @param bizExt 流程业务扩展信息
*/
private void buildFlowInstanceBizExt(Instance instance, FlowInstanceBizExt bizExt) {
bizExt.setInstanceId(instance.getId());
bizExt.setBusinessId(instance.getBusinessId());
flwInstanceBizExtMapper.saveOrUpdateByInstanceId(bizExt);
}
/**
* 办理任务
*
* @param completeTaskBo 办理任务参数
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean completeTask(CompleteTaskBo completeTaskBo) {
try {
// 获取任务ID并查询对应的流程任务和实例信息
Long taskId = completeTaskBo.getTaskId();
List<String> messageType = completeTaskBo.getMessageType();
String notice = completeTaskBo.getNotice();
// 获取抄送人
List<FlowCopyBo> flowCopyList = completeTaskBo.getFlowCopyList();
// 设置抄送人
Map<String, Object> variables = completeTaskBo.getVariables();
variables.put(FlowConstant.FLOW_COPY_LIST, flowCopyList);
// 消息类型
variables.put(FlowConstant.MESSAGE_TYPE, messageType);
// 消息通知
variables.put(FlowConstant.MESSAGE_NOTICE, notice);
FlowTask flowTask = flowTaskMapper.selectById(taskId);
if (ObjectUtil.isNull(flowTask)) {
throw new ServiceException("流程任务不存在或任务已审批!");
}
Instance ins = insService.getById(flowTask.getInstanceId());
// 检查流程状态是否为草稿、已撤销或已退回状态,若是则执行流程提交监听
if (BusinessStatusEnum.isDraftOrCancelOrBack(ins.getFlowStatus())) {
variables.put(FlowConstant.SUBMIT, true);
}
// 设置弹窗处理人
Map<String, Object> assigneeMap = setPopAssigneeMap(completeTaskBo.getAssigneeMap(), ins.getVariableMap());
if (CollUtil.isNotEmpty(assigneeMap)) {
variables.putAll(assigneeMap);
}
// 构建流程参数,包括变量、跳转类型、消息、处理人、权限等信息
FlowParams flowParams = FlowParams.build()
.handler(completeTaskBo.getHandler())
.variable(variables)
.skipType(SkipType.PASS.getKey())
.message(completeTaskBo.getMessage())
.flowStatus(BusinessStatusEnum.WAITING.getStatus())
.hisStatus(TaskStatusEnum.PASS.getStatus())
.hisTaskExt(completeTaskBo.getFileId());
Boolean autoPass = Convert.toBool(variables.getOrDefault(AUTO_PASS, false));
skipTask(taskId, flowParams, flowTask.getInstanceId(), autoPass);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new ServiceException(e.getMessage());
}
}
/**
* 流程办理
*
* @param taskId 任务ID
* @param flowParams 参数
* @param instanceId 实例ID
* @param autoPass 自动审批
*/
private void skipTask(Long taskId, FlowParams flowParams, Long instanceId, Boolean autoPass) {
// 执行任务跳转,并根据返回的处理人设置下一步处理人
taskService.skip(taskId, flowParams);
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) && autoPass) {
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(
FlowConstant.SUBMIT, false,
FlowConstant.FLOW_COPY_LIST, Collections.emptyList(),
FlowConstant.MESSAGE_NOTICE, StringUtils.EMPTY));
skipTask(task.getId(), flowParams, instanceId, true);
}
}
}
/**
* 设置弹窗处理人
*
* @param assigneeMap 处理人
* @param variablesMap 变量
*/
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 = new HashSet<>();
//弹窗传入的选人
List<String> popUserIds = Arrays.asList(entry.getValue().toString().split(StringUtils.SEPARATOR));
//已有的选人
List<String> variableUserIds = Arrays.asList(userIds.split(StringUtils.SEPARATOR));
hashSet.addAll(popUserIds);
hashSet.addAll(variableUserIds);
map.put(entry.getKey(), StringUtils.joinComma(hashSet));
}
} else {
map.put(entry.getKey(), entry.getValue());
}
}
return map;
}
/**
* 添加抄送人
*
* @param task 任务信息
* @param flowCopyList 抄送人
*/
@Override
public void setCopy(Task task, List<FlowCopyBo> flowCopyList) {
if (CollUtil.isEmpty(flowCopyList)) {
return;
}
// 添加抄送人记录
FlowHisTask flowHisTask = flowHisTaskMapper.selectList(
new LambdaQueryWrapper<>(FlowHisTask.class)
.eq(FlowHisTask::getTaskId, task.getId())).get(0);
FlowNode flowNode = new FlowNode();
flowNode.setNodeCode(flowHisTask.getTargetNodeCode());
flowNode.setNodeName(flowHisTask.getTargetNodeName());
//生成新的任务id
long taskId = identifierGenerator.nextId(null).longValue();
task.setId(taskId);
task.setNodeName("【抄送】" + task.getNodeName());
Date updateTime = new Date(flowHisTask.getUpdateTime().getTime() - 1000);
FlowParams flowParams = FlowParams.build()
.skipType(SkipType.NONE.getKey())
.hisStatus(TaskStatusEnum.COPY.getStatus())
.message("【抄送给】" + StreamUtils.join(flowCopyList, FlowCopyBo::getUserName));
HisTask hisTask = hisTaskService.setSkipHisTask(task, flowNode, flowParams);
hisTask.setCreateTime(updateTime);
hisTask.setUpdateTime(updateTime);
hisTaskService.save(hisTask);
List<User> userList = StreamUtils.toList(flowCopyList, x ->
new FlowUser()
.setType(TaskAssigneeType.COPY.getCode())
.setProcessedBy(Convert.toStr(x.getUserId()))
.setAssociated(taskId));
// 批量保存抄送人员
FlowEngine.userService().saveBatch(userList);
}
/**
* 查询当前用户的待办任务
*
* @param flowTaskBo 参数
* @param pageQuery 分页
*/
@Override
public TableDataInfo<FlowTaskVo> pageByTaskWait(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
QueryWrapper<FlowTaskBo> queryWrapper = buildQueryWrapper(flowTaskBo);
queryWrapper.eq("t.node_type", NodeType.BETWEEN.getKey());
queryWrapper.in("t.processed_by", LoginHelper.getUserIdStr());
queryWrapper.in("t.flow_status", BusinessStatusEnum.WAITING.getStatus());
Page<FlowTaskVo> page = flwTaskMapper.getListRunTask(pageQuery.build(), queryWrapper);
this.wrapAssigneeInfo(page.getRecords());
return TableDataInfo.build(page);
}
/**
* 查询当前用户的已办任务
*
* @param flowTaskBo 参数
* @param pageQuery 分页
*/
@Override
public TableDataInfo<FlowHisTaskVo> pageByTaskFinish(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
QueryWrapper<FlowTaskBo> queryWrapper = buildQueryWrapper(flowTaskBo);
queryWrapper.eq("t.node_type", NodeType.BETWEEN.getKey());
queryWrapper.in("t.approver", LoginHelper.getUserIdStr());
Page<FlowHisTaskVo> page = flwTaskMapper.getListFinishTask(pageQuery.build(), queryWrapper);
return TableDataInfo.build(page);
}
/**
* 查询待办任务
*
* @param flowTaskBo 参数
* @param pageQuery 分页
*/
@Override
public TableDataInfo<FlowTaskVo> pageByAllTaskWait(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
QueryWrapper<FlowTaskBo> queryWrapper = buildQueryWrapper(flowTaskBo);
queryWrapper.eq("t.node_type", NodeType.BETWEEN.getKey());
Page<FlowTaskVo> page = flwTaskMapper.getListRunTask(pageQuery.build(), queryWrapper);
this.wrapAssigneeInfo(page.getRecords());
return TableDataInfo.build(page);
}
/**
* 为流程任务列表封装处理人 IDassigneeIds
*
* @param taskList 流程任务列表
*/
private void wrapAssigneeInfo(List<FlowTaskVo> taskList) {
if (CollUtil.isEmpty(taskList)) {
return;
}
List<User> associatedUsers = FlowEngine.userService().getByAssociateds(StreamUtils.toList(taskList, FlowTaskVo::getId));
Map<Long, List<User>> taskUserMap = StreamUtils.groupByKey(associatedUsers, User::getAssociated);
// 组装用户数据回任务列表
for (FlowTaskVo task : taskList) {
List<User> users = taskUserMap.get(task.getId());
task.setAssigneeIds(StreamUtils.join(users, User::getProcessedBy));
}
}
/**
* 查询已办任务
*
* @param flowTaskBo 参数
* @param pageQuery 分页
*/
@Override
public TableDataInfo<FlowHisTaskVo> pageByAllTaskFinish(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
QueryWrapper<FlowTaskBo> queryWrapper = buildQueryWrapper(flowTaskBo);
Page<FlowHisTaskVo> page = flwTaskMapper.getListFinishTask(pageQuery.build(), queryWrapper);
return TableDataInfo.build(page);
}
/**
* 查询当前用户的抄送
*
* @param flowTaskBo 参数
* @param pageQuery 分页
*/
@Override
public TableDataInfo<FlowTaskVo> pageByTaskCopy(FlowTaskBo flowTaskBo, PageQuery pageQuery) {
QueryWrapper<FlowTaskBo> queryWrapper = buildQueryWrapper(flowTaskBo);
queryWrapper.in("t.processed_by", LoginHelper.getUserIdStr());
Page<FlowTaskVo> page = flwTaskMapper.getTaskCopyByPage(pageQuery.build(), queryWrapper);
return TableDataInfo.build(page);
}
private QueryWrapper<FlowTaskBo> buildQueryWrapper(FlowTaskBo flowTaskBo) {
QueryWrapper<FlowTaskBo> wrapper = Wrappers.query();
wrapper.like(StringUtils.isNotBlank(flowTaskBo.getNodeName()), "t.node_name", flowTaskBo.getNodeName());
wrapper.like(StringUtils.isNotBlank(flowTaskBo.getFlowName()), "t.flow_name", flowTaskBo.getFlowName());
wrapper.like(StringUtils.isNotBlank(flowTaskBo.getFlowCode()), "t.flow_code", flowTaskBo.getFlowCode());
wrapper.in(CollUtil.isNotEmpty(flowTaskBo.getCreateByIds()), "t.create_by", flowTaskBo.getCreateByIds());
if (StringUtils.isNotBlank(flowTaskBo.getCategory())) {
List<Long> categoryIds = flwCategoryMapper.selectCategoryIdsByParentId(Convert.toLong(flowTaskBo.getCategory()));
wrapper.in("t.category", StreamUtils.toList(categoryIds, Convert::toStr));
}
wrapper.orderByDesc("t.create_time").orderByDesc("t.update_time");
return wrapper;
}
/**
* 驳回任务
*
* @param bo 参数
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean backProcess(BackProcessBo bo) {
try {
Long taskId = bo.getTaskId();
String notice = bo.getNotice();
List<String> messageType = bo.getMessageType();
String message = bo.getMessage();
FlowTask task = flowTaskMapper.selectById(taskId);
if (ObjectUtil.isNull(task)) {
throw new ServiceException("任务不存在!");
}
Instance inst = insService.getById(task.getInstanceId());
BusinessStatusEnum.checkBackStatus(inst.getFlowStatus());
Long definitionId = task.getDefinitionId();
String applyNodeCode = flwCommonService.applyNodeCode(definitionId);
Map<String, Object> variable = new HashMap<>();
// 消息类型
variable.put(FlowConstant.MESSAGE_TYPE, messageType);
// 消息通知
variable.put(FlowConstant.MESSAGE_NOTICE, notice);
FlowParams flowParams = FlowParams.build()
.nodeCode(bo.getNodeCode())
.variable(variable)
.message(message)
.skipType(SkipType.REJECT.getKey())
.flowStatus(applyNodeCode.equals(bo.getNodeCode()) ? TaskStatusEnum.BACK.getStatus() : TaskStatusEnum.WAITING.getStatus())
.hisStatus(TaskStatusEnum.BACK.getStatus())
.hisTaskExt(bo.getFileId());
taskService.skip(task.getId(), flowParams);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new ServiceException(e.getMessage());
}
}
/**
* 获取可驳回的前置节点
*
* @param taskId 任务id
* @param nowNodeCode 当前节点
*/
@Override
public List<Node> getBackTaskNode(Long taskId, String nowNodeCode) {
FlowTask task = flowTaskMapper.selectById(taskId);
List<Node> nodeCodes = nodeService.getByNodeCodes(Collections.singletonList(nowNodeCode), task.getDefinitionId());
if (!CollUtil.isNotEmpty(nodeCodes)) {
return nodeCodes;
}
List<User> userList = FlowEngine.userService()
.getByAssociateds(Collections.singletonList(task.getId()), UserType.DEPUTE.getKey());
if (CollUtil.isNotEmpty(userList)) {
return nodeCodes;
}
//判断是否配置了固定驳回节点
Node node = nodeCodes.get(0);
if (StringUtils.isNotBlank(node.getAnyNodeSkip())) {
return nodeService.getByNodeCodes(Collections.singletonList(node.getAnyNodeSkip()), task.getDefinitionId());
}
//获取可驳回的前置节点
List<Node> nodes = nodeService.previousNodeList(task.getDefinitionId(), nowNodeCode);
if (CollUtil.isNotEmpty(nodes)) {
return StreamUtils.filter(nodes, e -> NodeType.BETWEEN.getKey().equals(e.getNodeType()));
}
return nodes;
}
/**
* 终止任务
*
* @param bo 参数
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean terminationTask(FlowTerminationBo bo) {
try {
Long taskId = bo.getTaskId();
Task task = taskService.getById(taskId);
if (task == null) {
throw new ServiceException("任务不存在!");
}
Instance instance = insService.getById(task.getInstanceId());
if (ObjectUtil.isNotNull(instance)) {
BusinessStatusEnum.checkInvalidStatus(instance.getFlowStatus());
}
FlowParams flowParams = FlowParams.build()
.message(bo.getComment())
.flowStatus(BusinessStatusEnum.TERMINATION.getStatus())
.hisStatus(TaskStatusEnum.TERMINATION.getStatus());
taskService.termination(taskId, flowParams);
return true;
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new ServiceException(e.getMessage());
}
}
/**
* 按照任务id查询任务
*
* @param taskIdList 任务id
*/
@Override
public List<FlowTask> selectByIdList(List<Long> taskIdList) {
return flowTaskMapper.selectList(new LambdaQueryWrapper<>(FlowTask.class).in(FlowTask::getId, taskIdList));
}
/**
* 按照任务id查询任务
*
* @param taskId 任务id
*/
@Override
public FlowTaskVo selectById(Long taskId) {
Task task = taskService.getById(taskId);
if (ObjectUtil.isNull(task)) {
return null;
}
FlowTaskVo flowTaskVo = BeanUtil.toBean(task, FlowTaskVo.class);
Instance instance = insService.getById(task.getInstanceId());
Definition definition = defService.getById(task.getDefinitionId());
flowTaskVo.setFlowStatus(instance.getFlowStatus());
flowTaskVo.setVersion(definition.getVersion());
flowTaskVo.setFlowCode(definition.getFlowCode());
flowTaskVo.setFlowName(definition.getFlowName());
flowTaskVo.setBusinessId(instance.getBusinessId());
FlowNode flowNode = this.getByNodeCode(flowTaskVo.getNodeCode(), instance.getDefinitionId());
if (ObjectUtil.isNull(flowNode)) {
throw new NullPointerException("当前【" + flowTaskVo.getNodeCode() + "】节点编码不存在");
}
NodeExtVo nodeExtVo = flwNodeExtService.parseNodeExt(flowNode.getExt(), instance.getVariableMap());
//设置按钮权限
if (CollUtil.isNotEmpty(nodeExtVo.getButtonPermissions())) {
flowTaskVo.setButtonList(nodeExtVo.getButtonPermissions());
} else {
flowTaskVo.setButtonList(new ArrayList<>());
}
if (CollUtil.isNotEmpty(nodeExtVo.getCopySettings())) {
List<FlowCopyVo> list = StreamUtils.toList(nodeExtVo.getCopySettings(), x -> new FlowCopyVo(Convert.toLong(x)));
flowTaskVo.setCopyList(list);
} else {
flowTaskVo.setCopyList(new ArrayList<>());
}
if (CollUtil.isNotEmpty(nodeExtVo.getVariables())) {
flowTaskVo.setVarList(nodeExtVo.getVariables());
} else {
flowTaskVo.setVarList(new HashMap<>());
}
flowTaskVo.setNodeRatio(flowNode.getNodeRatio());
flowTaskVo.setApplyNode(flowNode.getNodeCode().equals(flwCommonService.applyNodeCode(task.getDefinitionId())));
return flowTaskVo;
}
/**
* 获取下一节点信息
*
* @param bo 参数
*/
@Override
public List<FlowNode> getNextNodeList(FlowNextNodeBo bo) {
Long taskId = bo.getTaskId();
Map<String, Object> variables = bo.getVariables();
Task task = taskService.getById(taskId);
Instance instance = insService.getById(task.getInstanceId());
Definition definition = defService.getById(task.getDefinitionId());
Map<String, Object> mergeVariable = MapUtil.mergeAll(instance.getVariableMap(), variables);
// 获取下一节点列表
List<Node> nextNodeList = nodeService.getNextNodeList(task.getDefinitionId(), task.getNodeCode(), null, SkipType.PASS.getKey(), mergeVariable);
List<FlowNode> nextFlowNodes = BeanUtil.copyToList(nextNodeList, FlowNode.class);
// 只获取中间节点
nextFlowNodes = StreamUtils.filter(nextFlowNodes, node -> NodeType.BETWEEN.getKey().equals(node.getNodeType()));
if (CollUtil.isNotEmpty(nextNodeList)) {
//构建以下节点数据
List<Task> buildNextTaskList = StreamUtils.toList(nextNodeList, node -> taskService.addTask(node, instance, definition, FlowParams.build()));
//办理人变量替换
ExpressionUtil.evalVariable(buildNextTaskList, FlowParams.build().variable(mergeVariable));
for (FlowNode flowNode : nextFlowNodes) {
StreamUtils.findFirst(buildNextTaskList, t -> t.getNodeCode().equals(flowNode.getNodeCode()))
.ifPresent(first -> {
List<UserDTO> users;
if (CollUtil.isNotEmpty(first.getPermissionList())
&& CollUtil.isNotEmpty(users = flwTaskAssigneeService.fetchUsersByStorageIds(StringUtils.joinComma(first.getPermissionList())))) {
flowNode.setPermissionFlag(StreamUtils.join(users, e -> Convert.toStr(e.getUserId())));
}
});
}
}
return nextFlowNodes;
}
/**
* 按照任务id查询任务
*
* @param taskId 任务id
* @return 结果
*/
@Override
public FlowHisTask selectHisTaskById(Long taskId) {
return flowHisTaskMapper.selectOne(new LambdaQueryWrapper<>(FlowHisTask.class).eq(FlowHisTask::getId, taskId));
}
/**
* 按照实例id查询任务
*
* @param instanceId 流程实例id
*/
@Override
public List<FlowTask> selectByInstId(Long instanceId) {
return flowTaskMapper.selectList(new LambdaQueryWrapper<>(FlowTask.class).eq(FlowTask::getInstanceId, instanceId));
}
/**
* 按照实例id查询任务
*
* @param instanceIds 流程实例id
*/
@Override
public List<FlowTask> selectByInstIds(List<Long> instanceIds) {
return flowTaskMapper.selectList(new LambdaQueryWrapper<>(FlowTask.class).in(FlowTask::getInstanceId, instanceIds));
}
/**
* 判断流程是否已结束(即该流程实例下是否还有未完成的任务)
*
* @param instanceId 流程实例ID
* @return true 表示任务已全部结束false 表示仍有任务存在
*/
@Override
public boolean isTaskEnd(Long instanceId) {
boolean exists = flowTaskMapper.exists(new LambdaQueryWrapper<FlowTask>().eq(FlowTask::getInstanceId, instanceId));
return !exists;
}
/**
* 任务操作
*
* @param bo 参数
* @param taskOperation 操作类型,委派 delegateTask、转办 transferTask、加签 addSignature、减签 reductionSignature
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean taskOperation(TaskOperationBo bo, String taskOperation) {
FlowParams flowParams = FlowParams.build().message(bo.getMessage());
if (LoginHelper.isSuperAdmin() || LoginHelper.isTenantAdmin()) {
flowParams.ignore(true);
}
// 根据操作类型构建 FlowParams
switch (taskOperation) {
case DELEGATE_TASK, TRANSFER_TASK -> {
ValidatorUtils.validate(bo, AddGroup.class);
flowParams.addHandlers(Collections.singletonList(bo.getUserId()));
}
case ADD_SIGNATURE -> {
ValidatorUtils.validate(bo, EditGroup.class);
flowParams.addHandlers(bo.getUserIds());
}
case REDUCTION_SIGNATURE -> {
ValidatorUtils.validate(bo, EditGroup.class);
flowParams.reductionHandlers(bo.getUserIds());
}
default -> {
log.error("Invalid operation type:{} ", taskOperation);
throw new ServiceException("Invalid operation type " + taskOperation);
}
}
Long taskId = bo.getTaskId();
Task task = taskService.getById(taskId);
FlowNode flowNode = getByNodeCode(task.getNodeCode(), task.getDefinitionId());
if (ADD_SIGNATURE.equals(taskOperation) || REDUCTION_SIGNATURE.equals(taskOperation)) {
if (flowNode.getNodeRatio().compareTo(BigDecimal.ZERO) == 0) {
throw new ServiceException(task.getNodeName() + "不是会签节点!");
}
}
// 设置任务状态并执行对应的任务操作
switch (taskOperation) {
//委派任务
case DELEGATE_TASK -> {
flowParams.hisStatus(TaskStatusEnum.DEPUTE.getStatus());
return taskService.depute(taskId, flowParams);
}
//转办任务
case TRANSFER_TASK -> {
flowParams.hisStatus(TaskStatusEnum.TRANSFER.getStatus());
return taskService.transfer(taskId, flowParams);
}
//加签,增加办理人
case ADD_SIGNATURE -> {
flowParams.hisStatus(TaskStatusEnum.SIGN.getStatus());
return taskService.addSignature(taskId, flowParams);
}
//减签,减少办理人
case REDUCTION_SIGNATURE -> {
flowParams.hisStatus(TaskStatusEnum.SIGN_OFF.getStatus());
return taskService.reductionSignature(taskId, flowParams);
}
default -> {
log.error("Invalid operation type:{} ", taskOperation);
throw new ServiceException("Invalid operation type " + taskOperation);
}
}
}
/**
* 修改任务办理人(此方法将会批量修改所有任务的办理人)
*
* @param taskIdList 任务id
* @param userId 用户id
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean updateAssignee(List<Long> taskIdList, String userId) {
if (CollUtil.isEmpty(taskIdList)) {
return false;
}
try {
List<FlowTask> flowTasks = this.selectByIdList(taskIdList);
// 批量删除现有任务的办理人记录
if (CollUtil.isNotEmpty(flowTasks)) {
FlowEngine.userService().deleteByTaskIds(StreamUtils.toList(flowTasks, FlowTask::getId));
List<User> userList = StreamUtils.toList(flowTasks, flowTask ->
new FlowUser()
.setType(TaskAssigneeType.APPROVER.getCode())
.setProcessedBy(userId)
.setAssociated(flowTask.getId()));
if (CollUtil.isNotEmpty(userList)) {
FlowEngine.userService().saveBatch(userList);
}
}
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new ServiceException(e.getMessage());
}
return true;
}
/**
* 获取当前任务的所有办理人
*
* @param taskIds 任务id
*/
@Override
public List<UserDTO> currentTaskAllUser(List<Long> taskIds) {
// 获取与当前任务关联的用户列表
List<User> userList = FlowEngine.userService().getByAssociateds(taskIds);
if (CollUtil.isEmpty(userList)) {
return Collections.emptyList();
}
return userService.selectListByIds(StreamUtils.toList(userList, e -> Convert.toLong(e.getProcessedBy())));
}
/**
* 按照节点编码查询节点
*
* @param nodeCode 节点编码
* @param definitionId 流程定义id
*/
@Override
public FlowNode getByNodeCode(String nodeCode, Long definitionId) {
return flowNodeMapper.selectOne(new LambdaQueryWrapper<FlowNode>()
.eq(FlowNode::getNodeCode, nodeCode)
.eq(FlowNode::getDefinitionId, definitionId));
}
/**
* 催办任务
*
* @param bo 参数
*/
@Override
public boolean urgeTask(FlowUrgeTaskBo bo) {
try {
if (CollUtil.isEmpty(bo.getTaskIdList())) {
return false;
}
List<UserDTO> userList = this.currentTaskAllUser(bo.getTaskIdList());
if (CollUtil.isEmpty(userList)) {
return false;
}
List<String> messageType = bo.getMessageType();
String message = bo.getMessage();
flwCommonService.sendMessage(messageType, message, "单据审批提醒", userList);
} catch (Exception e) {
log.error(e.getMessage(), e);
throw new ServiceException(e.getMessage());
}
return true;
}
}

View File

@@ -0,0 +1,238 @@
package org.dromara.workflow.service.impl;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.domain.dto.StartProcessDTO;
import org.dromara.common.core.domain.event.ProcessDeleteEvent;
import org.dromara.common.core.domain.event.ProcessEvent;
import org.dromara.common.core.domain.event.ProcessTaskEvent;
import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.service.WorkflowService;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.domain.BaseEntity;
import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.constant.FlowConstant;
import org.dromara.workflow.domain.TestLeave;
import org.dromara.workflow.domain.bo.TestLeaveBo;
import org.dromara.workflow.domain.vo.TestLeaveVo;
import org.dromara.workflow.mapper.TestLeaveMapper;
import org.dromara.workflow.service.ITestLeaveService;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
/**
* 请假Service业务层处理
*
* @author may
* @date 2023-07-21
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@Service
@Slf4j
public class TestLeaveServiceImpl implements ITestLeaveService {
private final TestLeaveMapper baseMapper;
private final WorkflowService workflowService;
/**
* spel条件表达判断小于2
*
* @param leaveDays 待判断的变量可不传自行返回true或false
* @return boolean
*/
public boolean eval(Integer leaveDays) {
if (leaveDays <= 2) {
return true;
}
return false;
}
/**
* 查询请假
*/
@Override
public TestLeaveVo queryById(Long id) {
return baseMapper.selectVoById(id);
}
/**
* 查询请假列表
*/
@Override
public TableDataInfo<TestLeaveVo> queryPageList(TestLeaveBo bo, PageQuery pageQuery) {
LambdaQueryWrapper<TestLeave> lqw = buildQueryWrapper(bo);
Page<TestLeaveVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
return TableDataInfo.build(result);
}
/**
* 查询请假列表
*/
@Override
public List<TestLeaveVo> queryList(TestLeaveBo bo) {
LambdaQueryWrapper<TestLeave> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
private LambdaQueryWrapper<TestLeave> buildQueryWrapper(TestLeaveBo bo) {
LambdaQueryWrapper<TestLeave> lqw = Wrappers.lambdaQuery();
lqw.eq(StringUtils.isNotBlank(bo.getLeaveType()), TestLeave::getLeaveType, bo.getLeaveType());
lqw.ge(bo.getStartLeaveDays() != null, TestLeave::getLeaveDays, bo.getStartLeaveDays());
lqw.le(bo.getEndLeaveDays() != null, TestLeave::getLeaveDays, bo.getEndLeaveDays());
lqw.orderByDesc(BaseEntity::getCreateTime);
return lqw;
}
/**
* 新增请假
*/
@Override
public TestLeaveVo insertByBo(TestLeaveBo bo) {
long day = DateUtil.betweenDay(bo.getStartDate(), bo.getEndDate(), true);
// 截止日期也算一天
bo.setLeaveDays((int) day + 1);
bo.setApplyCode(System.currentTimeMillis() + StrUtil.EMPTY);
TestLeave add = MapstructUtils.convert(bo, TestLeave.class);
if (StringUtils.isBlank(add.getStatus())) {
add.setStatus(BusinessStatusEnum.DRAFT.getStatus());
}
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setId(add.getId());
}
return MapstructUtils.convert(add, TestLeaveVo.class);
}
@Transactional(rollbackFor = Exception.class)
@Override
public TestLeaveVo submitAndFlowStart(TestLeaveBo bo) {
long day = DateUtil.betweenDay(bo.getStartDate(), bo.getEndDate(), true);
// 截止日期也算一天
bo.setLeaveDays((int) day + 1);
if (ObjectUtil.isNull(bo.getId())) {
bo.setApplyCode(System.currentTimeMillis() + StrUtil.EMPTY);
}
TestLeave leave = MapstructUtils.convert(bo, TestLeave.class);
boolean flag = baseMapper.insertOrUpdate(leave);
if (flag) {
bo.setId(leave.getId());
// 后端发起需要忽略权限
bo.getParams().put("ignore", true);
StartProcessDTO startProcess = new StartProcessDTO();
startProcess.setBusinessId(leave.getId().toString());
startProcess.setFlowCode(StringUtils.isEmpty(bo.getFlowCode()) ? "leave1" : bo.getFlowCode());
startProcess.setVariables(bo.getParams());
// 后端发起 如果没有登录用户 比如定时任务 可以手动设置一个处理人id
// startProcess.setHandler("0");
boolean flag1 = workflowService.startCompleteTask(startProcess);
if (!flag1) {
throw new ServiceException("流程发起异常");
}
}
return MapstructUtils.convert(leave, TestLeaveVo.class);
}
/**
* 修改请假
*/
@Override
public TestLeaveVo updateByBo(TestLeaveBo bo) {
TestLeave update = MapstructUtils.convert(bo, TestLeave.class);
baseMapper.updateById(update);
return MapstructUtils.convert(update, TestLeaveVo.class);
}
/**
* 批量删除请假
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean deleteWithValidByIds(List<Long> ids) {
workflowService.deleteInstance(ids);
return baseMapper.deleteByIds(ids) > 0;
}
/**
* 总体流程监听(例如: 草稿,撤销,退回,作废,终止,已完成等)
* 正常使用只需#processEvent.flowCode=='leave1'
* 示例为了方便则使用startsWith匹配了全部示例key
*
* @param processEvent 参数
*/
@EventListener(condition = "#processEvent.flowCode.startsWith('leave')")
public void processHandler(ProcessEvent processEvent) {
log.info("当前任务执行了{}", processEvent.toString());
TestLeave testLeave = baseMapper.selectById(Convert.toLong(processEvent.getBusinessId()));
testLeave.setStatus(processEvent.getStatus());
// 用于例如审批附件 审批意见等 存储到业务表内 自行根据业务实现存储流程
Map<String, Object> params = processEvent.getParams();
if (MapUtil.isNotEmpty(params)) {
// 历史任务扩展(通常为附件)
String hisTaskExt = Convert.toStr(params.get("hisTaskExt"));
// 办理人
String handler = Convert.toStr(params.get("handler"));
// 办理意见
String message = Convert.toStr(params.get("message"));
}
if (processEvent.getSubmit()) {
if (StringUtils.isBlank(testLeave.getApplyCode())) {
String businessCode = MapUtil.getStr(params, FlowConstant.BUSINESS_CODE, StrUtil.EMPTY);
testLeave.setApplyCode(businessCode);
}
testLeave.setStatus(BusinessStatusEnum.WAITING.getStatus());
}
baseMapper.updateById(testLeave);
}
/**
* 执行任务创建监听(也代表上一条任务完成事件)
* 示例:也可通过 @EventListener(condition = "#processTaskEvent.flowCode=='leave1'")进行判断
* 在方法中判断流程节点key
* if ("xxx".equals(processTaskEvent.getNodeCode())) {
* //执行业务逻辑
* }
*
* @param processTaskEvent 参数
*/
@EventListener(condition = "#processTaskEvent.flowCode.startsWith('leave')")
public void processTaskHandler(ProcessTaskEvent processTaskEvent) {
log.info("当前任务创建了{}", processTaskEvent.toString());
}
/**
* 监听删除流程事件
* 正常使用只需#processDeleteEvent.flowCode=='leave1'
* 示例为了方便则使用startsWith匹配了全部示例key
*
* @param processDeleteEvent 参数
*/
@EventListener(condition = "#processDeleteEvent.flowCode.startsWith('leave')")
public void processDeleteHandler(ProcessDeleteEvent processDeleteEvent) {
log.info("监听删除流程事件,当前任务执行了{}", processDeleteEvent.toString());
TestLeave testLeave = baseMapper.selectById(Convert.toLong(processDeleteEvent.getBusinessId()));
if (ObjectUtil.isNull(testLeave)) {
return;
}
baseMapper.deleteById(testLeave.getId());
}
}

View File

@@ -0,0 +1,188 @@
package org.dromara.workflow.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.dto.CompleteTaskDTO;
import org.dromara.common.core.domain.dto.StartProcessDTO;
import org.dromara.common.core.domain.dto.StartProcessReturnDTO;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.service.WorkflowService;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.warm.flow.orm.entity.FlowInstance;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.MessageTypeEnum;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import org.dromara.workflow.domain.bo.CompleteTaskBo;
import org.dromara.workflow.domain.bo.StartProcessBo;
import org.dromara.workflow.service.IFlwDefinitionService;
import org.dromara.workflow.service.IFlwInstanceService;
import org.dromara.workflow.service.IFlwTaskService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* 通用 工作流服务实现
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@Service
public class WorkflowServiceImpl implements WorkflowService {
private final IFlwInstanceService flwInstanceService;
private final IFlwDefinitionService flwDefinitionService;
private final IFlwTaskService flwTaskService;
/**
* 删除流程实例
*
* @param businessIds 业务id
* @return 结果
*/
@Override
public boolean deleteInstance(List<Long> businessIds) {
return flwInstanceService.deleteByBusinessIds(businessIds);
}
/**
* 获取当前流程状态
*
* @param taskId 任务id
*/
@Override
public String getBusinessStatusByTaskId(Long taskId) {
FlowInstance flowInstance = flwInstanceService.selectByTaskId(taskId);
return ObjectUtil.isNotNull(flowInstance) ? flowInstance.getFlowStatus() : StringUtils.EMPTY;
}
/**
* 获取当前流程状态
*
* @param businessId 业务id
*/
@Override
public String getBusinessStatus(String businessId) {
FlowInstance flowInstance = flwInstanceService.selectInstByBusinessId(businessId);
return ObjectUtil.isNotNull(flowInstance) ? flowInstance.getFlowStatus() : StringUtils.EMPTY;
}
/**
* 设置流程变量
*
* @param instanceId 流程实例id
* @param variables 流程变量
*/
@Override
public void setVariable(Long instanceId, Map<String, Object> variables) {
flwInstanceService.setVariable(instanceId, variables);
}
/**
* 获取流程变量
*
* @param instanceId 流程实例id
*/
@Override
public Map<String, Object> instanceVariable(Long instanceId) {
return flwInstanceService.instanceVariable(instanceId);
}
/**
* 按照业务id查询流程实例id
*
* @param businessId 业务id
* @return 结果
*/
@Override
public Long getInstanceIdByBusinessId(String businessId) {
FlowInstance flowInstance = flwInstanceService.selectInstByBusinessId(businessId);
return ObjectUtil.isNotNull(flowInstance) ? flowInstance.getId() : null;
}
/**
* 新增租户流程定义
*
* @param tenantId 租户id
*/
@Override
public void syncDef(String tenantId) {
flwDefinitionService.syncDef(tenantId);
}
/**
* 启动流程
*
* @param startProcess 参数
*/
@Override
public StartProcessReturnDTO startWorkFlow(StartProcessDTO startProcess) {
return flwTaskService.startWorkFlow(BeanUtil.toBean(startProcess, StartProcessBo.class));
}
/**
* 办理任务
* 系统后台发起审批 无用户信息 需要忽略权限
* completeTask.getVariables().put("ignore", true);
*
* @param completeTask 参数
*/
@Override
public boolean completeTask(CompleteTaskDTO completeTask) {
return flwTaskService.completeTask(BeanUtil.toBean(completeTask, CompleteTaskBo.class));
}
/**
* 办理任务
*
* @param taskId 任务ID
* @param message 办理意见
*/
@Override
public boolean completeTask(Long taskId, String message) {
CompleteTaskBo completeTask = new CompleteTaskBo();
completeTask.setTaskId(taskId);
completeTask.setMessage(message);
// 忽略权限(系统后台发起审批 无用户信息 需要忽略权限)
completeTask.getVariables().put("ignore", true);
return flwTaskService.completeTask(completeTask);
}
/**
* 启动流程并办理第一个任务
*
* @param startProcess 参数
*/
@Override
@Transactional(rollbackFor = Exception.class)
public boolean startCompleteTask(StartProcessDTO startProcess) {
try {
StartProcessBo processBo = new StartProcessBo();
processBo.setBusinessId(startProcess.getBusinessId());
processBo.setFlowCode(startProcess.getFlowCode());
processBo.setVariables(startProcess.getVariables());
processBo.setHandler(startProcess.getHandler());
processBo.setBizExt(BeanUtil.toBean(startProcess.getBizExt(), FlowInstanceBizExt.class));
StartProcessReturnDTO result = flwTaskService.startWorkFlow(processBo);
CompleteTaskBo taskBo = new CompleteTaskBo();
taskBo.setTaskId(result.getTaskId());
taskBo.setMessageType(Collections.singletonList(MessageTypeEnum.SYSTEM_MESSAGE.getCode()));
taskBo.setVariables(startProcess.getVariables());
taskBo.setHandler(startProcess.getHandler());
boolean flag = flwTaskService.completeTask(taskBo);
if (!flag) {
throw new ServiceException("流程发起异常");
}
return true;
} catch (Exception e) {
throw new ServiceException(e.getMessage());
}
}
}

View File

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

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.workflow.mapper.FlwCategoryMapper">
</mapper>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.workflow.mapper.FlwInstanceBizExtMapper">
</mapper>

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.workflow.mapper.FlwInstanceMapper">
<resultMap type="org.dromara.workflow.domain.vo.FlowInstanceVo" id="FlowInstanceResult">
</resultMap>
<select id="selectInstanceList" resultMap="FlowInstanceResult">
select fi.id,
fi.create_time,
fi.update_time,
fi.tenant_id,
fi.del_flag,
fi.definition_id,
fi.business_id,
fi.node_type,
fi.node_code,
fi.node_name,
fi.variable,
fi.flow_status,
fi.activity_status,
fi.create_by,
fi.ext,
fd.flow_name,
fd.flow_code,
fd.version,
fd.form_custom,
fd.form_path,
fd.category,
biz.business_code,
biz.business_title
from flow_instance fi
left join flow_definition fd on fi.definition_id = fd.id
left join flow_instance_biz_ext biz on biz.instance_id = fi.id
${ew.getCustomSqlSegment}
</select>
</mapper>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.workflow.mapper.FlwSpelMapper">
</mapper>

View File

@@ -0,0 +1,124 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.workflow.mapper.FlwTaskMapper">
<resultMap type="org.dromara.workflow.domain.vo.FlowTaskVo" id="FlowTaskResult">
</resultMap>
<resultMap type="org.dromara.workflow.domain.vo.FlowHisTaskVo" id="FlowHisTaskResult">
</resultMap>
<select id="getListRunTask" resultMap="FlowTaskResult">
select * from (
select distinct
t.id,
t.node_code,
t.node_name,
t.node_type,
t.definition_id,
t.instance_id,
t.create_time,
t.update_time,
t.tenant_id,
i.business_id,
i.flow_status,
i.create_by,
d.flow_name,
d.flow_code,
d.form_custom,
d.category,
COALESCE(t.form_path, d.form_path) as form_path,
d.version,
uu.processed_by,
uu.type,
biz.business_code,
biz.business_title
from flow_task t
left join flow_user uu on uu.associated = t.id
left join flow_definition d on t.definition_id = d.id
left join flow_instance i on t.instance_id = i.id
left join flow_instance_biz_ext biz on biz.instance_id = i.id
where t.node_type = 1
and t.del_flag = '0'
and uu.del_flag = '0'
and uu.type in ('1','2','3')
) t
${ew.getCustomSqlSegment}
</select>
<select id="getListFinishTask" resultMap="FlowHisTaskResult">
select * from (
select
a.id,
a.node_code,
a.node_name,
a.cooperate_type,
a.approver,
a.collaborator,
a.node_type,
a.target_node_code,
a.target_node_name,
a.definition_id,
a.instance_id,
a.flow_status flow_task_status,
a.message,
a.ext,
a.create_time,
a.update_time,
a.tenant_id,
a.form_custom,
a.form_path,
b.flow_status,
b.business_id,
b.create_by,
c.flow_name,
c.flow_code,
c.category,
c.version,
biz.business_code,
biz.business_title
from flow_his_task a
left join flow_instance b on a.instance_id = b.id
left join flow_definition c on a.definition_id = c.id
left join flow_instance_biz_ext biz on biz.instance_id = b.id
where a.del_flag ='0'
and b.del_flag = '0'
and c.del_flag = '0'
and a.node_type in ('1','3','4')
) t
${ew.getCustomSqlSegment}
</select>
<select id="getTaskCopyByPage" resultMap="FlowTaskResult">
select * from (
select
b.id,
b.update_time,
c.business_id,
c.flow_status,
c.create_by,
a.processed_by,
a.create_time,
b.form_custom,
b.form_path,
b.node_name,
b.node_code,
d.flow_name,
d.flow_code,
d.category,
d.version,
biz.business_code,
biz.business_title
from flow_user a
left join flow_his_task b on a.associated = b.task_id
left join flow_instance c on b.instance_id = c.id
left join flow_definition d on c.definition_id=d.id
left join flow_instance_biz_ext biz on biz.instance_id = c.id
where a.type = '4'
and a.del_flag = '0'
and b.del_flag = '0'
and d.del_flag = '0'
) t
${ew.getCustomSqlSegment}
</select>
</mapper>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.dromara.workflow.mapper.TestLeaveMapper">
</mapper>