add: 新增工作流 LiteFlow 规则链

This commit is contained in:
疯狂的狮子Li
2026-07-02 14:39:10 +08:00
parent 53f857714d
commit c8f755cbde
26 changed files with 1279 additions and 0 deletions
@@ -245,6 +245,15 @@ warm-flow:
# 默认Authorization,如果有多个token,用逗号分隔
token-name: ${sa-token.token-name},clientid
--- # LiteFlow 业务规则编排
liteflow:
# 跟随工作流模块开关,避免禁用工作流时仍加载任务办理链
enable: ${warm-flow.enabled:true}
# 工作流模块用于编排任务办理链路
rule-source: classpath:liteflow/*.el.xml
# 开启虚拟线程
enable-virtual-thread: true
--- # mqtt 配置
# 具体配置还需查看文档
# https://mica-mqtt.dreamlu.net/guide/spring/client.html
+6
View File
@@ -96,6 +96,12 @@
<artifactId>warm-flow-plugin-ui-sb-web</artifactId>
</dependency>
<!-- LiteFlow 规则编排 -->
<dependency>
<groupId>org.dromara</groupId>
<artifactId>ruoyi-common-liteflow</artifactId>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,44 @@
package org.dromara.workflow.domain.context;
import lombok.Data;
import org.dromara.warm.flow.core.dto.FlowParams;
import org.dromara.warm.flow.core.entity.Instance;
import org.dromara.warm.flow.orm.entity.FlowTask;
import org.dromara.workflow.domain.bo.CompleteTaskBo;
/**
* 办理任务 LiteFlow 上下文。
* <p>
* 链路内各节点通过该对象传递任务、实例、办理参数等中间结果,避免组件之间重复查询。
*
* @author may
*/
@Data
public class CompleteTaskContext {
/**
* 办理任务入参。
*/
private final CompleteTaskBo completeTaskBo;
/**
* 当前待办理的运行任务,由校验节点加载。
*/
private FlowTask flowTask;
/**
* 当前任务所属流程实例,由校验节点加载。
*/
private Instance instance;
/**
* Warm-Flow 办理参数,由参数构建节点生成并供执行、自动审批节点复用。
*/
private FlowParams flowParams;
/**
* 流程实例是否开启自动审批,由参数构建节点从实例变量中解析。
*/
private Boolean autoPass;
}
@@ -0,0 +1,59 @@
package org.dromara.workflow.domain.context;
import lombok.Data;
import org.dromara.warm.flow.orm.entity.FlowInstance;
import java.util.Collection;
import java.util.List;
/**
* 删除流程实例 LiteFlow 上下文。
*
* @author may
*/
@Data
public class InstanceDeleteContext {
/**
* 按业务 id 删除时的业务 id 集合。
*/
private final List<String> businessIds;
/**
* 按实例 id 删除时的实例 id 集合。
*/
private final Collection<Long> instanceIds;
/**
* 是否删除历史实例及其历史任务数据。
*/
private final boolean history;
/**
* 待删除流程实例。
*/
private List<FlowInstance> flowInstances;
/**
* 实际执行删除的实例 id。
*/
private List<Long> deleteInstanceIds;
/**
* 删除结果。
*/
private boolean result;
public static InstanceDeleteContext byBusinessIds(List<String> businessIds) {
return new InstanceDeleteContext(businessIds, null, false);
}
public static InstanceDeleteContext byInstanceIds(Collection<Long> instanceIds) {
return new InstanceDeleteContext(null, instanceIds, false);
}
public static InstanceDeleteContext byHistoryInstanceIds(Collection<Long> instanceIds) {
return new InstanceDeleteContext(null, instanceIds, true);
}
}
@@ -0,0 +1,70 @@
package org.dromara.workflow.domain.context;
import lombok.Data;
import org.dromara.warm.flow.core.entity.Definition;
import org.dromara.warm.flow.core.entity.Instance;
import org.dromara.warm.flow.core.entity.Task;
import org.dromara.warm.flow.orm.entity.FlowInstance;
import org.dromara.workflow.api.domain.StartProcessReturnDTO;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import org.dromara.workflow.domain.bo.StartProcessBo;
import java.util.List;
import java.util.Map;
/**
* 启动流程 LiteFlow 上下文。
* <p>
* 链路内保存启动参数、流程定义、流程实例和首个任务等中间结果。
*
* @author may
*/
@Data
public class StartProcessContext {
/**
* 启动流程入参。
*/
private final StartProcessBo startProcessBo;
/**
* 业务唯一标识。
*/
private String businessId;
/**
* 流程变量。
*/
private Map<String, Object> variables;
/**
* 流程实例业务扩展信息。
*/
private FlowInstanceBizExt bizExt;
/**
* 已存在的流程实例,非空时走续提交逻辑。
*/
private FlowInstance existingInstance;
/**
* 已发布的流程定义。
*/
private Definition definition;
/**
* 新启动的流程实例。
*/
private Instance instance;
/**
* 当前流程实例下生成的任务列表。
*/
private List<Task> taskList;
/**
* 启动流程返回结果。
*/
private StartProcessReturnDTO startProcessReturn;
}
@@ -0,0 +1,55 @@
package org.dromara.workflow.domain.context;
import lombok.Data;
import org.dromara.warm.flow.core.dto.FlowParams;
import org.dromara.warm.flow.core.entity.Task;
import org.dromara.warm.flow.orm.entity.FlowNode;
import org.dromara.workflow.common.enums.TaskOperationEnum;
import org.dromara.workflow.domain.bo.TaskOperationBo;
/**
* 任务操作 LiteFlow 上下文。
* <p>
* 用于在委派、转办、加签、减签链路中传递操作类型、任务、节点和办理参数。
*
* @author may
*/
@Data
public class TaskOperationContext {
/**
* 任务操作入参。
*/
private final TaskOperationBo taskOperationBo;
/**
* 操作类型编码。
*/
private final String taskOperation;
/**
* 解析后的操作枚举。
*/
private TaskOperationEnum operation;
/**
* Warm-Flow 操作参数。
*/
private FlowParams flowParams;
/**
* 当前任务。
*/
private Task task;
/**
* 当前任务对应的流程节点。
*/
private FlowNode flowNode;
/**
* 操作执行结果。
*/
private boolean result;
}
@@ -0,0 +1,95 @@
package org.dromara.workflow.liteflow.complete;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.query.QueryBuilder;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.warm.flow.core.FlowEngine;
import org.dromara.warm.flow.core.dto.FlowParams;
import org.dromara.warm.flow.core.entity.User;
import org.dromara.warm.flow.core.service.TaskService;
import org.dromara.warm.flow.orm.entity.FlowTask;
import org.dromara.warm.flow.orm.mapper.FlowTaskMapper;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.CompleteTaskContext;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.dromara.workflow.common.constant.FlowConstant.*;
/**
* 执行当前处理人的后续自动审批。
* <p>
* 仅当流程实例开启自动审批时生效,用于连续跳过同一登录人负责的后续待办任务。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("completeAutoPass")
public class CompleteAutoPassComponent extends NodeComponent {
private final TaskService taskService;
private final FlowTaskMapper flowTaskMapper;
@Override
public void process() {
CompleteTaskContext context = getRequestData();
FlowTask flowTask = context.getFlowTask();
autoPass(flowTask.getId(), context.getFlowParams(), flowTask.getInstanceId());
}
/**
* 递归跳过当前处理人的后续任务。
*
* @param taskId 本轮已处理任务 id,用于避免重复跳过同一任务
* @param flowParams 当前办理参数,会在自动审批时覆盖为自动审批消息和空抄送变量
* @param instanceId 流程实例 id
*/
private void autoPass(Long taskId, FlowParams flowParams, Long instanceId) {
List<FlowTask> flowTaskList = selectByInstId(instanceId);
if (CollUtil.isEmpty(flowTaskList)) {
return;
}
List<User> userList = FlowEngine.userService()
.getByAssociateds(StreamUtils.toList(flowTaskList, FlowTask::getId));
if (CollUtil.isEmpty(userList)) {
return;
}
for (FlowTask task : flowTaskList) {
if (task.getId().equals(taskId)) {
continue;
}
// 自动审批只处理当前登录人仍是办理人的后续任务,避免替其他候选人或并行分支误审批。
List<User> users = StreamUtils.filter(userList,
e -> ObjectUtil.equals(task.getId(), e.getAssociated()) && ObjectUtil.equal(e.getProcessedBy(), LoginHelper.getUserIdStr()));
if (CollUtil.isEmpty(users)) {
continue;
}
// 自动审批不继承人工办理时的抄送和消息通知,避免重复发送业务消息。
flowParams
.message("流程引擎自动审批!")
.variable(Map.of(
SUBMIT, false,
FLOW_COPY_LIST, Collections.emptyList(),
MESSAGE_NOTICE, StringUtils.EMPTY));
taskService.skip(task.getId(), flowParams);
// 跳过后可能生成新的后续任务,继续扫描直到当前登录人不再有可自动办理任务。
autoPass(task.getId(), flowParams, instanceId);
}
}
private List<FlowTask> selectByInstId(Long instanceId) {
return flowTaskMapper.selectList(QueryBuilder.lambda(FlowTask.class)
.eq(FlowTask::getInstanceId, instanceId)
.build());
}
}
@@ -0,0 +1,54 @@
package org.dromara.workflow.liteflow.complete;
import cn.hutool.core.convert.Convert;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.warm.flow.core.dto.FlowParams;
import org.dromara.warm.flow.core.enums.SkipType;
import org.dromara.warm.flow.core.service.TaskService;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.TaskStatusEnum;
import org.dromara.workflow.domain.bo.CompleteTaskBo;
import org.dromara.workflow.domain.context.CompleteTaskContext;
import java.util.Map;
import static org.dromara.workflow.common.constant.FlowConstant.*;
/**
* 构建办理参数并执行当前任务跳转。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("completeExecute")
public class CompleteExecuteComponent extends NodeComponent {
private final TaskService taskService;
@Override
public void process() {
CompleteTaskContext context = getRequestData();
CompleteTaskBo completeTaskBo = context.getCompleteTaskBo();
Map<String, Object> variables = completeTaskBo.getVariables();
FlowParams flowParams = FlowParams.build()
.handler(completeTaskBo.getHandler())
.variable(variables)
.ignore(Convert.toBool(variables.getOrDefault(VAR_IGNORE, false)))
.ignoreDepute(Convert.toBool(variables.getOrDefault(VAR_IGNORE_DEPUTE, false)))
.ignoreCooperate(Convert.toBool(variables.getOrDefault(VAR_IGNORE_COOPERATE, false)))
.skipType(SkipType.PASS.getKey())
.message(completeTaskBo.getMessage())
.flowStatus(BusinessStatusEnum.WAITING.getStatus())
.hisStatus(TaskStatusEnum.PASS.getStatus())
.hisTaskExt(completeTaskBo.getFileId());
context.setFlowParams(flowParams);
context.setAutoPass(Convert.toBool(context.getInstance().getVariableMap().getOrDefault(AUTO_PASS, false)));
taskService.skip(context.getFlowTask().getId(), flowParams);
}
}
@@ -0,0 +1,23 @@
package org.dromara.workflow.liteflow.complete;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeBooleanComponent;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.CompleteTaskContext;
/**
* 判断办理任务后是否进入自动审批分支。
*
* @author may
*/
@ConditionalOnEnable
@LiteflowComponent("completeNeedAutoPass")
public class CompleteNeedAutoPassComponent extends NodeBooleanComponent {
@Override
public boolean processBoolean() {
CompleteTaskContext context = getRequestData();
return Boolean.TRUE.equals(context.getAutoPass());
}
}
@@ -0,0 +1,100 @@
package org.dromara.workflow.liteflow.complete;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.warm.flow.core.entity.Instance;
import org.dromara.warm.flow.core.service.InsService;
import org.dromara.warm.flow.orm.entity.FlowTask;
import org.dromara.warm.flow.orm.mapper.FlowTaskMapper;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.TaskStatusEnum;
import org.dromara.workflow.domain.bo.CompleteTaskBo;
import org.dromara.workflow.domain.context.CompleteTaskContext;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static org.dromara.workflow.common.constant.FlowConstant.*;
/**
* 准备任务办理上下文。
* <p>
* 负责加载任务实例、补齐流程变量,并解析弹窗选择的后续办理人。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("completePrepare")
public class CompletePrepareComponent extends NodeComponent {
private final FlowTaskMapper flowTaskMapper;
private final InsService insService;
@Override
public void process() {
CompleteTaskContext context = getRequestData();
CompleteTaskBo completeTaskBo = context.getCompleteTaskBo();
FlowTask flowTask = flowTaskMapper.selectById(completeTaskBo.getTaskId());
if (ObjectUtil.isNull(flowTask)) {
throw new ServiceException("流程任务不存在或任务已审批!");
}
Instance instance = insService.getById(flowTask.getInstanceId());
if (ObjectUtil.isNull(instance)) {
throw new ServiceException("流程实例不存在");
}
context.setFlowTask(flowTask);
context.setInstance(instance);
Map<String, Object> variables = completeTaskBo.getVariables();
variables.put(FLOW_COPY_LIST, completeTaskBo.getFlowCopyList());
variables.put(MESSAGE_TYPE, completeTaskBo.getMessageType());
variables.put(MESSAGE_NOTICE, completeTaskBo.getNotice());
if (BusinessStatusEnum.isDraftOrCancelOrBack(instance.getFlowStatus())) {
// 草稿、撤销、驳回状态再次办理时等同于重新提交,条件表达式通过 SUBMIT 识别提交分支。
variables.put(SUBMIT, true);
}
Map<String, Object> assigneeMap = setPopAssigneeMap(completeTaskBo.getAssigneeMap(), instance.getVariableMap());
if (CollUtil.isNotEmpty(assigneeMap)) {
variables.putAll(assigneeMap);
}
}
/**
* 将弹窗处理人映射成通过/退回状态下的办理人变量。
*/
private Map<String, Object> setPopAssigneeMap(Map<String, Object> assigneeMap, Map<String, Object> variablesMap) {
Map<String, Object> map = new HashMap<>();
if (CollUtil.isEmpty(assigneeMap)) {
return map;
}
for (Map.Entry<String, Object> entry : assigneeMap.entrySet()) {
if (variablesMap.containsKey(entry.getKey())) {
String userIds = variablesMap.get(entry.getKey()).toString();
if (StringUtils.isNotBlank(userIds)) {
Set<String> hashSet = CollUtil.newHashSet();
List<String> popUserIds = StringUtils.str2List(entry.getValue().toString(), StringUtils.SEPARATOR, true, true);
List<String> variableUserIds = StringUtils.str2List(userIds, StringUtils.SEPARATOR, true, true);
hashSet.addAll(popUserIds);
hashSet.addAll(variableUserIds);
// Warm-Flow 会按“状态:变量名”读取办理人,正向和退回都写入同一批候选人。
map.put(TaskStatusEnum.PASS.getStatus() + StringUtils.COLON + entry.getKey(), StringUtils.joinComma(hashSet));
map.put(TaskStatusEnum.BACK.getStatus() + StringUtils.COLON + entry.getKey(), StringUtils.joinComma(hashSet));
}
} else {
map.put(TaskStatusEnum.PASS.getStatus() + StringUtils.COLON + entry.getKey(), entry.getValue());
map.put(TaskStatusEnum.BACK.getStatus() + StringUtils.COLON + entry.getKey(), entry.getValue());
}
}
return map;
}
}
@@ -0,0 +1,60 @@
package org.dromara.workflow.liteflow.instance;
import cn.hutool.core.util.ObjectUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.warm.flow.core.entity.Definition;
import org.dromara.warm.flow.core.entity.Instance;
import org.dromara.warm.flow.core.service.DefService;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.InstanceDeleteContext;
import org.dromara.workflow.handler.FlowProcessEventHandler;
import java.util.Map;
import java.util.function.Function;
/**
* 校验实例删除权限并发布业务删除事件。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@Slf4j
@LiteflowComponent("instanceDeleteEvent")
public class InstanceDeleteEventComponent extends NodeComponent {
private final DefService defService;
private final FlowProcessEventHandler flowProcessEventHandler;
@Override
public void process() {
InstanceDeleteContext context = getRequestData();
String userId = LoginHelper.getUserIdStr();
context.getFlowInstances().forEach(flowInstance -> {
if (!LoginHelper.isSuperAdmin() && !flowInstance.getCreateBy().equals(userId)) {
throw new ServiceException("权限不足,无法删除流程实例信息!");
}
});
Map<Long, Definition> definitionMap = StreamUtils.toMap(
defService.getByIds(StreamUtils.toList(context.getFlowInstances(), Instance::getDefinitionId)),
Definition::getId,
Function.identity()
);
context.getFlowInstances().forEach(instance -> {
Definition definition = definitionMap.get(instance.getDefinitionId());
if (ObjectUtil.isNull(definition)) {
log.warn("实例 ID: {} 对应的流程定义信息未找到,跳过删除事件触发。", instance.getId());
return;
}
flowProcessEventHandler.processDeleteHandler(definition.getFlowCode(), instance.getBusinessId());
});
}
}
@@ -0,0 +1,50 @@
package org.dromara.workflow.liteflow.instance;
import cn.hutool.core.collection.CollUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.mybatis.core.query.QueryBuilder;
import org.dromara.warm.flow.core.FlowEngine;
import org.dromara.warm.flow.core.service.InsService;
import org.dromara.warm.flow.orm.entity.FlowTask;
import org.dromara.warm.flow.orm.mapper.FlowTaskMapper;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.InstanceDeleteContext;
import java.util.List;
/**
* 执行流程实例删除。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("instanceDeleteExecute")
public class InstanceDeleteExecuteComponent extends NodeComponent {
private final InsService insService;
private final FlowTaskMapper flowTaskMapper;
@Override
public void process() {
InstanceDeleteContext context = getRequestData();
if (!context.isHistory()) {
context.setResult(insService.remove(context.getDeleteInstanceIds()));
return;
}
List<FlowTask> flowTaskList = flowTaskMapper.selectList(QueryBuilder.lambda(FlowTask.class)
.in(FlowTask::getInstanceId, context.getDeleteInstanceIds())
.build());
if (CollUtil.isNotEmpty(flowTaskList)) {
FlowEngine.userService().deleteByTaskIds(StreamUtils.toList(flowTaskList, FlowTask::getId));
}
FlowEngine.taskService().deleteByInsIds(context.getDeleteInstanceIds());
FlowEngine.hisTaskService().deleteByInsIds(context.getDeleteInstanceIds());
FlowEngine.insService().removeByIds(context.getDeleteInstanceIds());
context.setResult(true);
}
}
@@ -0,0 +1,24 @@
package org.dromara.workflow.liteflow.instance;
import cn.hutool.core.collection.CollUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeBooleanComponent;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.InstanceDeleteContext;
/**
* 判断删除流程实例时是否存在可执行删除的数据。
*
* @author may
*/
@ConditionalOnEnable
@LiteflowComponent("instanceDeleteExists")
public class InstanceDeleteExistsComponent extends NodeBooleanComponent {
@Override
public boolean processBoolean() {
InstanceDeleteContext context = getRequestData();
return CollUtil.isNotEmpty(context.getFlowInstances());
}
}
@@ -0,0 +1,52 @@
package org.dromara.workflow.liteflow.instance;
import cn.hutool.core.collection.CollUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.mybatis.core.query.QueryBuilder;
import org.dromara.warm.flow.orm.entity.FlowInstance;
import org.dromara.warm.flow.orm.mapper.FlowInstanceMapper;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.InstanceDeleteContext;
import java.util.ArrayList;
import java.util.List;
/**
* 加载待删除流程实例。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@Slf4j
@LiteflowComponent("instanceDeleteLoad")
public class InstanceDeleteLoadComponent extends NodeComponent {
private final FlowInstanceMapper flowInstanceMapper;
@Override
public void process() {
InstanceDeleteContext context = getRequestData();
List<FlowInstance> flowInstances;
if (CollUtil.isNotEmpty(context.getBusinessIds())) {
flowInstances = flowInstanceMapper.selectList(QueryBuilder.lambda(FlowInstance.class)
.in(FlowInstance::getBusinessId, context.getBusinessIds())
.build());
context.setDeleteInstanceIds(StreamUtils.toList(flowInstances, FlowInstance::getId));
} else {
flowInstances = flowInstanceMapper.selectByIds(context.getInstanceIds());
context.setDeleteInstanceIds(new ArrayList<>(context.getInstanceIds()));
}
if (CollUtil.isEmpty(flowInstances)) {
log.warn("未找到对应的流程实例信息,无法执行删除操作。");
context.setResult(false);
return;
}
context.setFlowInstances(flowInstances);
}
}
@@ -0,0 +1,49 @@
package org.dromara.workflow.liteflow.operation;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.warm.flow.core.service.TaskService;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.TaskStatusEnum;
import org.dromara.workflow.domain.context.TaskOperationContext;
/**
* 执行委派、转办、加签或减签。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("taskOpExecute")
public class TaskOpExecuteComponent extends NodeComponent {
private final TaskService taskService;
@Override
public void process() {
TaskOperationContext context = getRequestData();
Long taskId = context.getTaskOperationBo().getTaskId();
boolean result = false;
switch (context.getOperation()) {
case DELEGATE_TASK -> {
context.getFlowParams().hisStatus(TaskStatusEnum.DEPUTE.getStatus());
result = taskService.depute(taskId, context.getFlowParams());
}
case TRANSFER_TASK -> {
context.getFlowParams().hisStatus(TaskStatusEnum.TRANSFER.getStatus());
result = taskService.transfer(taskId, context.getFlowParams());
}
case ADD_SIGNATURE -> {
context.getFlowParams().hisStatus(TaskStatusEnum.SIGN.getStatus());
result = taskService.addSignature(taskId, context.getFlowParams());
}
case REDUCTION_SIGNATURE -> {
context.getFlowParams().hisStatus(TaskStatusEnum.SIGN_OFF.getStatus());
result = taskService.reductionSignature(taskId, context.getFlowParams());
}
}
context.setResult(result);
}
}
@@ -0,0 +1,53 @@
package org.dromara.workflow.liteflow.operation;
import cn.hutool.core.util.ObjectUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.mybatis.core.query.QueryBuilder;
import org.dromara.warm.flow.core.entity.Task;
import org.dromara.warm.flow.core.enums.CooperateType;
import org.dromara.warm.flow.core.service.TaskService;
import org.dromara.warm.flow.orm.entity.FlowNode;
import org.dromara.warm.flow.orm.mapper.FlowNodeMapper;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.TaskOperationEnum;
import org.dromara.workflow.domain.context.TaskOperationContext;
/**
* 加载任务节点并校验操作约束。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("taskOpLoad")
public class TaskOpLoadComponent extends NodeComponent {
private final TaskService taskService;
private final FlowNodeMapper flowNodeMapper;
@Override
public void process() {
TaskOperationContext context = getRequestData();
Task task = taskService.getById(context.getTaskOperationBo().getTaskId());
if (ObjectUtil.isNull(task)) {
throw new ServiceException("任务不存在!");
}
FlowNode flowNode = flowNodeMapper.selectOne(QueryBuilder.lambda(FlowNode.class)
.eq(FlowNode::getNodeCode, task.getNodeCode())
.eq(FlowNode::getDefinitionId, task.getDefinitionId())
.build());
if (ObjectUtil.isNull(flowNode)) {
throw new ServiceException("流程节点不存在");
}
if ((context.getOperation() == TaskOperationEnum.ADD_SIGNATURE || context.getOperation() == TaskOperationEnum.REDUCTION_SIGNATURE)
&& CooperateType.isOrSign(flowNode.getNodeRatio())) {
throw new ServiceException(task.getNodeName() + "不是会签或票签节点!");
}
context.setTask(task);
context.setFlowNode(flowNode);
}
}
@@ -0,0 +1,24 @@
package org.dromara.workflow.liteflow.operation;
import cn.hutool.core.collection.CollUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeBooleanComponent;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.TaskOperationContext;
/**
* 判断任务操作后是否进入消息通知分支。
*
* @author may
*/
@ConditionalOnEnable
@LiteflowComponent("taskOpNeedNotify")
public class TaskOpNeedNotifyComponent extends NodeBooleanComponent {
@Override
public boolean processBoolean() {
TaskOperationContext context = getRequestData();
return context.isResult() && CollUtil.isNotEmpty(context.getTaskOperationBo().getMessageType());
}
}
@@ -0,0 +1,57 @@
package org.dromara.workflow.liteflow.operation;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.system.api.UserService;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.bo.TaskOperationBo;
import org.dromara.workflow.domain.context.TaskOperationContext;
import org.dromara.workflow.service.IFlwCommonService;
import java.util.ArrayList;
import java.util.List;
import static org.dromara.workflow.common.constant.FlowConstant.PATH_TASK_WAITING;
/**
* 任务操作成功后发送待办消息。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("taskOpNotify")
public class TaskOpNotifyComponent extends NodeComponent {
private final IFlwCommonService flwCommonService;
private final UserService userService;
@Override
public void process() {
TaskOperationContext context = getRequestData();
TaskOperationBo bo = context.getTaskOperationBo();
List<Long> userIdList = new ArrayList<>();
if (StringUtils.isNotBlank(bo.getUserId())) {
userIdList.add(Convert.toLong(bo.getUserId()));
}
if (CollUtil.isNotEmpty(bo.getUserIds())) {
userIdList.addAll(bo.getUserIds().stream().map(Convert::toLong).toList());
}
if (CollUtil.isEmpty(userIdList)) {
return;
}
// 转办、委托、加减签等运行时操作,消息接收人统一从“我的待办”进入处理。
flwCommonService.sendMessage(
bo.getMessageType(),
StringUtils.isNotBlank(bo.getMessage()) ? bo.getMessage() : "单据「" + context.getOperation().getDesc() + "」通知",
"单据「" + context.getOperation().getDesc() + "」提醒",
userService.selectListByIds(userIdList),
PATH_TASK_WAITING
);
}
}
@@ -0,0 +1,57 @@
package org.dromara.workflow.liteflow.operation;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.extern.slf4j.Slf4j;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.ValidatorUtils;
import org.dromara.common.core.validate.AddGroup;
import org.dromara.common.core.validate.EditGroup;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.warm.flow.core.dto.FlowParams;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.common.enums.TaskOperationEnum;
import org.dromara.workflow.domain.bo.TaskOperationBo;
import org.dromara.workflow.domain.context.TaskOperationContext;
import java.util.Collections;
/**
* 准备任务操作参数。
*
* @author may
*/
@ConditionalOnEnable
@Slf4j
@LiteflowComponent("taskOpPrepare")
public class TaskOpPrepareComponent extends NodeComponent {
@Override
public void process() {
TaskOperationContext context = getRequestData();
TaskOperationEnum op = TaskOperationEnum.getByCode(context.getTaskOperation());
if (op == null) {
log.error("Invalid operation type:{} ", context.getTaskOperation());
throw new ServiceException("Invalid operation type " + context.getTaskOperation());
}
context.setOperation(op);
TaskOperationBo bo = context.getTaskOperationBo();
switch (op) {
case DELEGATE_TASK, TRANSFER_TASK -> ValidatorUtils.validate(bo, AddGroup.class);
case ADD_SIGNATURE, REDUCTION_SIGNATURE -> ValidatorUtils.validate(bo, EditGroup.class);
}
FlowParams flowParams = FlowParams.build().message(bo.getMessage());
if (LoginHelper.isSuperAdmin()) {
flowParams.ignore(true);
}
switch (op) {
case DELEGATE_TASK, TRANSFER_TASK -> flowParams.addHandlers(Collections.singletonList(bo.getUserId()));
case ADD_SIGNATURE -> flowParams.addHandlers(bo.getUserIds());
case REDUCTION_SIGNATURE -> flowParams.reductionHandlers(bo.getUserIds());
}
context.setFlowParams(flowParams);
}
}
@@ -0,0 +1,67 @@
package org.dromara.workflow.liteflow.start;
import cn.hutool.core.collection.CollUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.warm.flow.core.dto.FlowParams;
import org.dromara.warm.flow.core.entity.Instance;
import org.dromara.warm.flow.core.entity.Task;
import org.dromara.warm.flow.core.service.InsService;
import org.dromara.warm.flow.core.service.TaskService;
import org.dromara.warm.flow.orm.entity.FlowTask;
import org.dromara.workflow.api.domain.StartProcessReturnDTO;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import org.dromara.workflow.domain.context.StartProcessContext;
import org.dromara.workflow.mapper.FlwInstanceBizExtMapper;
import java.util.List;
/**
* 启动流程实例并构建返回结果。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("startExecute")
public class StartExecuteComponent extends NodeComponent {
private final InsService insService;
private final TaskService taskService;
private final FlwInstanceBizExtMapper flwInstanceBizExtMapper;
@Override
public void process() {
StartProcessContext context = getRequestData();
FlowParams flowParams = FlowParams.build()
.handler(context.getStartProcessBo().getHandler())
.flowCode(context.getStartProcessBo().getFlowCode())
.variable(context.getVariables())
.flowStatus(BusinessStatusEnum.DRAFT.getStatus());
Instance instance = insService.start(context.getBusinessId(), flowParams);
context.setInstance(instance);
saveBizExt(context, instance);
List<Task> taskList = taskService.list(new FlowTask().setInstanceId(instance.getId()));
if (CollUtil.isEmpty(taskList)) {
throw new ServiceException("流程启动失败,未生成任务");
}
if (taskList.size() > 1) {
throw new ServiceException("请检查流程第一个环节是否为申请人!");
}
context.setTaskList(taskList);
context.setStartProcessReturn(new StartProcessReturnDTO(instance.getId(), taskList.getFirst().getId()));
}
private void saveBizExt(StartProcessContext context, Instance instance) {
FlowInstanceBizExt bizExt = context.getBizExt();
bizExt.setInstanceId(instance.getId());
bizExt.setBusinessId(instance.getBusinessId());
flwInstanceBizExtMapper.saveOrUpdateByInstanceId(bizExt);
}
}
@@ -0,0 +1,24 @@
package org.dromara.workflow.liteflow.start;
import cn.hutool.core.util.ObjectUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeBooleanComponent;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.StartProcessContext;
/**
* 判断启动流程时是否走已有实例续提交分支。
*
* @author may
*/
@ConditionalOnEnable
@LiteflowComponent("startExists")
public class StartExistsComponent extends NodeBooleanComponent {
@Override
public boolean processBoolean() {
StartProcessContext context = getRequestData();
return ObjectUtil.isNotNull(context.getExistingInstance());
}
}
@@ -0,0 +1,55 @@
package org.dromara.workflow.liteflow.start;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Dict;
import cn.hutool.core.util.ObjectUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.json.utils.JsonUtils;
import org.dromara.warm.flow.core.FlowEngine;
import org.dromara.warm.flow.core.entity.Definition;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import org.dromara.workflow.domain.context.StartProcessContext;
import java.util.Map;
import static org.dromara.workflow.common.constant.FlowConstant.*;
/**
* 加载流程定义并补齐启动变量。
*
* @author may
*/
@ConditionalOnEnable
@LiteflowComponent("startPrepareInstance")
public class StartPrepareInstanceComponent extends NodeComponent {
@Override
public void process() {
StartProcessContext context = getRequestData();
Definition definition = FlowEngine.defService().getPublishByFlowCode(context.getStartProcessBo().getFlowCode());
if (ObjectUtil.isNull(definition)) {
throw new ServiceException("流程【" + context.getStartProcessBo().getFlowCode() + "】未发布,请先在流程设计器中发布流程定义");
}
context.setDefinition(definition);
Map<String, Object> variables = context.getVariables();
Dict dict = JsonUtils.parseMap(definition.getExt());
boolean autoPass = !ObjectUtil.isNull(dict) && dict.getBool(AUTO_PASS);
variables.put(AUTO_PASS, autoPass);
variables.put(BUSINESS_CODE, generateBusinessCode(context.getBizExt()));
}
private String generateBusinessCode(FlowInstanceBizExt bizExt) {
if (ObjectUtil.isNull(bizExt.getBusinessCode()) || bizExt.getBusinessCode().isBlank()) {
// TODO: 按照自己业务规则生成编号
String businessCode = Convert.toStr(System.currentTimeMillis());
bizExt.setBusinessCode(businessCode);
return businessCode;
}
return bizExt.getBusinessCode();
}
}
@@ -0,0 +1,53 @@
package org.dromara.workflow.liteflow.start;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.query.QueryBuilder;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.warm.flow.orm.entity.FlowInstance;
import org.dromara.warm.flow.orm.mapper.FlowInstanceMapper;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.context.StartProcessContext;
import java.util.Map;
import static org.dromara.workflow.common.constant.FlowConstant.*;
/**
* 准备流程启动请求。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("startPrepareRequest")
public class StartPrepareRequestComponent extends NodeComponent {
private final FlowInstanceMapper flowInstanceMapper;
@Override
public void process() {
StartProcessContext context = getRequestData();
String businessId = context.getStartProcessBo().getBusinessId();
if (StringUtils.isBlank(businessId)) {
throw new ServiceException("启动工作流时必须包含业务ID");
}
context.setBusinessId(businessId);
context.setVariables(context.getStartProcessBo().getVariables());
context.setBizExt(context.getStartProcessBo().getBizExt());
Map<String, Object> variables = context.getVariables();
variables.put(INITIATOR, LoginHelper.getUserIdStr());
variables.put(INITIATOR_DEPT_ID, LoginHelper.getDeptId());
variables.put(BUSINESS_ID, businessId);
FlowInstance flowInstance = flowInstanceMapper.selectOne(QueryBuilder.lambda(FlowInstance.class)
.eq(FlowInstance::getBusinessId, businessId)
.build());
context.setExistingInstance(flowInstance);
}
}
@@ -0,0 +1,56 @@
package org.dromara.workflow.liteflow.start;
import cn.hutool.core.collection.CollUtil;
import com.yomahub.liteflow.annotation.LiteflowComponent;
import com.yomahub.liteflow.core.NodeComponent;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.enums.BusinessStatusEnum;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.warm.flow.core.entity.Task;
import org.dromara.warm.flow.core.service.InsService;
import org.dromara.warm.flow.core.service.TaskService;
import org.dromara.warm.flow.orm.entity.FlowTask;
import org.dromara.workflow.api.domain.StartProcessReturnDTO;
import org.dromara.workflow.common.ConditionalOnEnable;
import org.dromara.workflow.domain.FlowInstanceBizExt;
import org.dromara.workflow.domain.context.StartProcessContext;
import org.dromara.workflow.mapper.FlwInstanceBizExtMapper;
import java.util.List;
/**
* 已存在流程实例时执行续提交。
*
* @author may
*/
@ConditionalOnEnable
@RequiredArgsConstructor
@LiteflowComponent("startResume")
public class StartResumeComponent extends NodeComponent {
private final TaskService taskService;
private final InsService insService;
private final FlwInstanceBizExtMapper flwInstanceBizExtMapper;
@Override
public void process() {
StartProcessContext context = getRequestData();
BusinessStatusEnum.checkStartStatus(context.getExistingInstance().getFlowStatus());
List<Task> taskList = taskService.list(new FlowTask().setInstanceId(context.getExistingInstance().getId()));
if (CollUtil.isEmpty(taskList)) {
throw new ServiceException("流程实例缺少任务,请检查流程定义配置");
}
taskService.mergeVariable(context.getExistingInstance(), context.getVariables());
insService.updateById(context.getExistingInstance());
saveBizExt(context);
context.setStartProcessReturn(new StartProcessReturnDTO(taskList.getFirst().getInstanceId(), taskList.getFirst().getId()));
}
private void saveBizExt(StartProcessContext context) {
FlowInstanceBizExt bizExt = context.getBizExt();
bizExt.setInstanceId(context.getExistingInstance().getId());
bizExt.setBusinessId(context.getExistingInstance().getBusinessId());
flwInstanceBizExtMapper.saveOrUpdateByInstanceId(bizExt);
}
}
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<flow>
<!--
删除流程实例链路:
1. 按业务 id 或实例 id 加载待删除实例
2. 存在待删除实例时校验权限并发布业务删除事件
3. 按运行实例或历史实例模式执行删除;不存在实例时直接返回 false
-->
<chain name="deleteInstanceChain">
THEN(
instanceDeleteLoad,
IF(
instanceDeleteExists,
THEN(
instanceDeleteEvent,
instanceDeleteExecute
),
noop
)
);
</chain>
</flow>
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<flow>
<!--
启动流程链路:
1. 初始化请求并查询已有实例
2. 已存在实例时走续提交并返回当前任务
3. 不存在实例时加载流程定义并补齐变量
4. 新启动实例、保存业务扩展并构建返回结果
-->
<chain name="startProcessChain">
THEN(
startPrepareRequest,
IF(
startExists,
startResume,
THEN(
startPrepareInstance,
startExecute
)
)
);
</chain>
<!--
办理任务链路:
1. 加载任务实例、补齐变量并解析办理人
2. 构建参数并执行当前任务跳转
3. 开启自动审批时继续办理当前处理人的后续任务
-->
<chain name="completeTaskChain">
THEN(
completePrepare,
completeExecute,
IF(
completeNeedAutoPass,
completeAutoPass,
noop
)
);
</chain>
<!--
任务操作链路:
1. 解析操作类型、校验入参并构建参数
2. 加载任务节点并校验操作约束
3. 执行任务操作
4. 操作成功且配置消息类型时发送通知
-->
<chain name="taskOperationChain">
THEN(
taskOpPrepare,
taskOpLoad,
taskOpExecute,
IF(
taskOpNeedNotify,
taskOpNotify,
noop
)
);
</chain>
</flow>