Merge remote-tracking branch 'origin/feature/bpm' into feature/bpm

This commit is contained in:
jason 2025-03-04 23:17:52 +08:00
commit d1fead11da
23 changed files with 288 additions and 83 deletions

View File

@ -17,8 +17,8 @@ import java.util.Arrays;
public enum BpmChildProcessMultiInstanceSourceTypeEnum implements ArrayValuable<Integer> {
FIXED_QUANTITY(1, "固定数量"),
DIGITAL_FORM(2, "数字表单"),
MULTI_FORM(3, "表单");
NUMBER_FORM(2, "数字表单"),
MULTIPLE_FORM(3, "表单");
private final Integer type;
private final String name;

View File

@ -16,8 +16,9 @@ import java.util.Arrays;
@AllArgsConstructor
public enum BpmTriggerTypeEnum implements ArrayValuable<Integer> {
HTTP_REQUEST(1, "发起 HTTP 请求"),
HTTP_CALLBACK(2, "发起 HTTP 回调"),
HTTP_REQUEST(1, "发起 HTTP 请求"), // BPM => 业务流程继续执行无需等待业务
HTTP_CALLBACK(2, "接收 HTTP 回调"), // BPM => 业务 => BPM流程卡主等待业务回调
FORM_UPDATE(10, "更新流程表单数据"),
FORM_DELETE(11, "删除流程表单数据"),
;

View File

@ -57,7 +57,7 @@ public class BpmModelController {
@GetMapping("/list")
@Operation(summary = "获得模型分页")
@Parameter(name = "name", description = "模型名称", example = "芋艿")
public CommonResult<List<BpmModelRespVO>> getModelPage(@RequestParam(value = "name", required = false) String name) {
public CommonResult<List<BpmModelRespVO>> getModelList(@RequestParam(value = "name", required = false) String name) {
List<Model> list = modelService.getModelList(name);
if (CollUtil.isEmpty(list)) {
return success(Collections.emptyList());

View File

@ -17,6 +17,7 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.common.engine.impl.db.SuspensionState;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.ProcessDefinition;
import org.springframework.security.access.prepost.PreAuthorize;
@ -31,6 +32,7 @@ import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@ -99,6 +101,17 @@ public class BpmProcessDefinitionController {
list, null, processDefinitionMap, null, null));
}
@GetMapping("/simple-list")
@Operation(summary = "获得流程定义精简列表", description = "只包含未挂起的流程,主要用于前端的下拉选项")
public CommonResult<List<BpmProcessDefinitionRespVO>> getSimpleProcessDefinitionList() {
// 只查询未挂起的流程
List<ProcessDefinition> list = processDefinitionService.getProcessDefinitionListBySuspensionState(
SuspensionState.ACTIVE.getStateCode());
// 拼接 VO 返回只返回 idnamekey
return success(convertList(list, definition -> new BpmProcessDefinitionRespVO()
.setId(definition.getId()).setName(definition.getName()).setKey(definition.getKey())));
}
@GetMapping ("/get")
@Operation(summary = "获得流程定义")
@Parameter(name = "id", description = "流程编号", required = true, example = "1024")

View File

@ -25,7 +25,7 @@ public class BpmModelRespVO extends BpmModelMetaInfoVO {
@Schema(description = "流程图标", example = "https://www.iocoder.cn/yudao.jpg")
private String icon;
@Schema(description = "流程分类编", example = "1")
@Schema(description = "流程分类编", example = "1")
private String category;
@Schema(description = "流程分类名字", example = "请假")
private String categoryName;

View File

@ -509,7 +509,7 @@ public class BpmSimpleModelNodeVO {
@Schema(description = "完成比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@NotNull(message = "完成比例不能为空")
private Integer completeRatio; // TODO @lesanapproveRatio 要不这个和上面保持一致
private Integer approveRatio;
@Schema(description = "多实例来源类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "多实例来源类型不能为空")

View File

@ -1,14 +1,14 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - 流程定义 Response VO")
@Data
public class BpmProcessDefinitionRespVO {
public class BpmProcessDefinitionRespVO extends BpmModelMetaInfoVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private String id;
@ -22,36 +22,19 @@ public class BpmProcessDefinitionRespVO {
@Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao")
private String key;
@Schema(description = "流程图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao.jpg")
private String icon;
@Schema(description = "流程描述", example = "我是描述")
private String description;
@Schema(description = "流程分类", example = "1")
private String category;
@Schema(description = "流程分类名字", example = "请假")
private String categoryName;
@Schema(description = "流程模型的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABC")
private String modelId;
@Schema(description = "流程模型的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Integer modelType; // 参见 BpmModelTypeEnum 枚举类
@Schema(description = "表单类型-参见 bpm_model_form_type 数据字典", example = "1")
private Integer formType;
@Schema(description = "表单编号-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", example = "1024")
private Long formId;
@Schema(description = "表单名字", example = "请假表单")
private String formName;
@Schema(description = "表单的配置-JSON 字符串。在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", requiredMode = Schema.RequiredMode.REQUIRED)
private String formConf;
@Schema(description = "表单项的数组-JSON 字符串的数组。在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", requiredMode = Schema.RequiredMode.REQUIRED)
private List<String> formFields;
@Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空",
example = "/bpm/oa/leave/create")
private String formCustomCreatePath;
@Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空",
example = "/bpm/oa/leave/view")
private String formCustomViewPath;
@Schema(description = "中断状态-参见 SuspensionState 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer suspensionState; // 参见 SuspensionState 枚举

View File

@ -32,10 +32,10 @@ import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - 流程实例") // 流程实例通过流程定义创建的一次申请
@ -78,8 +78,14 @@ public class BpmProcessInstanceController {
convertSet(processDefinitionMap.values(), ProcessDefinition::getCategory));
Map<String, BpmProcessDefinitionInfoDO> processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap(
convertSet(pageResult.getList(), HistoricProcessInstance::getProcessDefinitionId));
Set<Long> userIds = convertSet(pageResult.getList(), processInstance -> NumberUtils.parseLong(processInstance.getStartUserId()));
userIds.addAll(convertSetByFlatMap(taskMap.values(),
tasks -> tasks.stream().map(Task::getAssignee).filter(StrUtil::isNotBlank).map(Long::parseLong)));
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstancePage(pageResult,
processDefinitionMap, categoryMap, taskMap, null, null, processDefinitionInfoMap));
processDefinitionMap, categoryMap, taskMap, userMap, deptMap, processDefinitionInfoMap));
}
@GetMapping("/manager-page")

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@ -73,6 +74,13 @@ public class BpmProcessInstanceRespVO {
@Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道")
private String name;
@Schema(description = "任务分配人编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2048")
@JsonIgnore // 不返回只是方便后续读取赋值给 assigneeUser
private Long assignee;
@Schema(description = "任务分配人", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2048")
private UserSimpleBaseVO assigneeUser;
}
}

View File

@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Schema(description = "管理后台 - 通过流程任务的 Request VO")
@ -23,4 +24,7 @@ public class BpmTaskApproveReqVO {
@Schema(description = "变量实例(动态表单)", requiredMode = Schema.RequiredMode.REQUIRED)
private Map<String, Object> variables;
@Schema(description = "下一个节点审批人", example = "{nodeId:[1, 2]}")
private Map<String, List<Long>> nextAssignees; // 为什么是 Map而不是 List 因为下一个节点可能是多个例如说并行网关的情况
}

View File

@ -18,6 +18,9 @@ public class BpmTaskPageReqVO extends PageParam {
@Schema(description = "流程分类", example = "1")
private String category;
@Schema(description = "流程定义的标识", example = "2048")
private String processDefinitionKey; // 精准匹配
@Schema(description = "创建时间")
@DateTimeFormat(pattern = DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] createTime;

View File

@ -76,6 +76,15 @@ public interface BpmProcessInstanceConvert {
respVO.setStartUser(BeanUtils.toBean(startUser, UserSimpleBaseVO.class));
MapUtils.findAndThen(deptMap, startUser.getDeptId(), dept -> respVO.getStartUser().setDeptName(dept.getName()));
}
if (CollUtil.isNotEmpty(respVO.getTasks())) {
respVO.getTasks().forEach(task -> {
AdminUserRespDTO assigneeUser = userMap.get(task.getAssignee());
if (assigneeUser!= null) {
task.setAssigneeUser(BeanUtils.toBean(assigneeUser, UserSimpleBaseVO.class));
MapUtils.findAndThen(deptMap, assigneeUser.getDeptId(), dept -> task.getAssigneeUser().setDeptName(dept.getName()));
}
});
}
}
// 摘要
respVO.setSummary(FlowableUtils.getSummary(processDefinitionInfoMap.get(respVO.getProcessDefinitionId()),

View File

@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.bpm.dal.dataobject.definition;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmAutoApproveTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
@ -60,6 +59,14 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
*/
private Integer modelType;
/**
* 流程分类的编码
*
* 关联 {@link BpmCategoryDO#getCode()}
*
* 为什么要存储原因是{@link ProcessDefinition#getCategory()} 无法设置
*/
private String category;
/**
* 图标
*/
@ -149,7 +156,7 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
*
* 关联 {@link AdminUserRespDTO#getId()} 字段的数组
*/
@TableField(typeHandler = StringListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤
@TableField(typeHandler = LongListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤
private List<Long> managerUserIds;
/**

View File

@ -77,10 +77,10 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
if (execution.getCurrentFlowElement() instanceof CallActivity) {
FlowElement flowElement = execution.getCurrentFlowElement();
Integer sourceType = BpmnModelUtils.parseMultiInstanceSourceType(flowElement);
if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.DIGITAL_FORM.getType())) {
if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType())) {
return execution.getVariable(super.collectionExpression.getExpressionText(), Integer.class);
}
if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTI_FORM.getType())) {
if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) {
return execution.getVariable(super.collectionExpression.getExpressionText(), List.class).size();
}
}

View File

@ -71,10 +71,10 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
if (execution.getCurrentFlowElement() instanceof CallActivity) {
FlowElement flowElement = execution.getCurrentFlowElement();
Integer sourceType = BpmnModelUtils.parseMultiInstanceSourceType(flowElement);
if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.DIGITAL_FORM.getType())) {
if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType())) {
return execution.getVariable(super.collectionExpression.getExpressionText(), Integer.class);
}
if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTI_FORM.getType())) {
if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) {
return execution.getVariable(super.collectionExpression.getExpressionText(), List.class).size();
}
}

View File

@ -2,24 +2,21 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.d
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import com.google.common.collect.Sets;
import jakarta.annotation.Resource;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.ServiceTask;
import org.flowable.bpmn.model.Task;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
/**
* 发起人自选 {@link BpmTaskCandidateUserStrategy} 实现类
@ -55,7 +52,7 @@ public class BpmTaskCandidateStartUserSelectStrategy extends AbstractBpmTaskCand
execution.getProcessInstanceId());
// 获得审批人
List<Long> assignees = startUserSelectAssignees.get(execution.getCurrentActivityId());
return new LinkedHashSet<>(assignees);
return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet();
}
@Override
@ -70,28 +67,7 @@ public class BpmTaskCandidateStartUserSelectStrategy extends AbstractBpmTaskCand
}
// 获得审批人
List<Long> assignees = startUserSelectAssignees.get(activityId);
return new LinkedHashSet<>(assignees);
}
/**
* 获得发起人自选审批人或抄送人的 Task 列表
*
* @param bpmnModel BPMN 模型
* @return Task 列表
*/
public static List<Task> getStartUserSelectTaskList(BpmnModel bpmnModel) {
if (bpmnModel == null) {
return Collections.emptyList();
}
List<Task> tasks = new ArrayList<>();
tasks.addAll(BpmnModelUtils.getBpmnModelElements(bpmnModel, UserTask.class));
tasks.addAll(BpmnModelUtils.getBpmnModelElements(bpmnModel, ServiceTask.class));
if (CollUtil.isEmpty(tasks)) {
return Collections.emptyList();
}
tasks.removeIf(task -> ObjectUtil.notEqual(BpmnModelUtils.parseCandidateStrategy(task),
BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy()));
return tasks;
return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet();
}
}

View File

@ -809,6 +809,7 @@ public class BpmnModelUtils {
if (currentElement instanceof ExclusiveGateway) {
// 查找满足条件的 SequenceFlow 路径
Gateway gateway = (Gateway) currentElement;
// TODO @小北当一个网关节点下存在多个满足的并行节点时只查询一个节点流程流转会存在问题需要优化具体见issuehttps://github.com/YunaiV/ruoyi-vue-pro/issues/761
SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
&& (evalConditionExpress(variables, flow.getConditionExpression())));
@ -857,6 +858,130 @@ public class BpmnModelUtils {
}
}
/**
* 根据当前节点获取下一个节点
*
* @param currentElement 当前节点
* @param bpmnModel BPMN模型
* @param variables 流程变量
*/
@SuppressWarnings("PatternVariableCanBeUsed")
public static List<FlowNode> getNextFlowNodes(FlowElement currentElement, BpmnModel bpmnModel,
Map<String, Object> variables){
List<FlowNode> nextFlowNodes = new ArrayList<>(); // 下一个执行的流程节点集合
FlowNode currentNode = (FlowNode) currentElement; // 当前执行节点的基本属性
List<SequenceFlow> outgoingFlows = currentNode.getOutgoingFlows(); // 当前节点的关联节点
if (CollUtil.isEmpty(outgoingFlows)) {
log.warn("[getNextFlowNodes][当前节点({}) 的 outgoingFlows 为空]", currentNode.getId());
return nextFlowNodes;
}
// 遍历每个出口流
for (SequenceFlow outgoingFlow : outgoingFlows) {
// 获取目标节点的基本属性
FlowElement targetElement = bpmnModel.getFlowElement(outgoingFlow.getTargetRef());
if (targetElement == null) {
continue;
}
// 情况一处理不同类型的网关
if (targetElement instanceof Gateway) {
Gateway gateway = (Gateway) targetElement;
if (gateway instanceof ExclusiveGateway) {
handleExclusiveGateway(gateway, bpmnModel, variables, nextFlowNodes);
} else if (gateway instanceof InclusiveGateway) {
handleInclusiveGateway(gateway, bpmnModel, variables, nextFlowNodes);
} else if (gateway instanceof ParallelGateway) {
handleParallelGateway(gateway, bpmnModel, variables, nextFlowNodes);
}
} else {
// 情况二如果不是网关直接添加到下一个节点列表
nextFlowNodes.add((FlowNode) targetElement);
}
}
return nextFlowNodes;
}
/**
* 处理排它网关
*
* @param gateway 排他网关
* @param bpmnModel BPMN模型
* @param variables 流程变量
* @param nextFlowNodes 下一个执行的流程节点集合
*/
private static void handleExclusiveGateway(Gateway gateway, BpmnModel bpmnModel,
Map<String, Object> variables, List<FlowNode> nextFlowNodes) {
// TODO @小北 这里和 simulateNextFlowElements 中有重复代码是否重构每个网关节点拆分出方法应该比较合理化@芋艿
// TODO @小北ok simulateNextFlowElements 里面处理网关的复用这个方法可以么
SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
&& (evalConditionExpress(variables, flow.getConditionExpression())));
if (matchSequenceFlow == null) {
matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId()));
// 特殊没有默认的情况下并且只有 1 个条件则认为它是默认的
if (matchSequenceFlow == null && gateway.getOutgoingFlows().size() == 1) {
matchSequenceFlow = gateway.getOutgoingFlows().get(0);
}
}
// 遍历满足条件的 SequenceFlow 路径
if (matchSequenceFlow != null) {
FlowElement targetElement = bpmnModel.getFlowElement(matchSequenceFlow.getTargetRef());
if (targetElement instanceof FlowNode) {
nextFlowNodes.add((FlowNode) targetElement);
}
}
}
/**
* 处理包容网关
*
* @param gateway 排他网关
* @param bpmnModel BPMN模型
* @param variables 流程变量
* @param nextFlowNodes 下一个执行的流程节点集合
*/
private static void handleInclusiveGateway(Gateway gateway, BpmnModel bpmnModel,
Map<String, Object> variables, List<FlowNode> nextFlowNodes) {
Collection<SequenceFlow> matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(),
flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
&& evalConditionExpress(variables, flow.getConditionExpression()));
if (CollUtil.isEmpty(matchSequenceFlows)) {
matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(),
flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId()));
// 特殊没有默认的情况下并且只有 1 个条件则认为它是默认的
if (CollUtil.isEmpty(matchSequenceFlows) && gateway.getOutgoingFlows().size() == 1) {
matchSequenceFlows = gateway.getOutgoingFlows();
}
}
// 遍历满足条件的 SequenceFlow 路径获取目标节点
matchSequenceFlows.forEach(flow -> {
FlowElement targetElement = bpmnModel.getFlowElement(flow.getTargetRef());
if (targetElement instanceof FlowNode) {
nextFlowNodes.add((FlowNode) targetElement);
}
});
}
/**
* 处理并行网关
*
* @param gateway 排他网关
* @param bpmnModel BPMN模型
* @param variables 流程变量
* @param nextFlowNodes 下一个执行的流程节点集合
*/
private static void handleParallelGateway(Gateway gateway, BpmnModel bpmnModel,
Map<String, Object> variables, List<FlowNode> nextFlowNodes) {
// 并行网关遍历所有出口路径获取目标节点
gateway.getOutgoingFlows().forEach(flow -> {
FlowElement targetElement = bpmnModel.getFlowElement(flow.getTargetRef());
if (targetElement instanceof FlowNode) {
nextFlowNodes.add((FlowNode) targetElement);
}
});
}
/**
* 计算条件表达式是否为 true 满足条件
*

View File

@ -25,7 +25,6 @@ import org.springframework.util.MultiValueMap;
import java.util.*;
import static cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum.HTTP_CALLBACK;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.*;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*;
import static java.util.Arrays.asList;
@ -737,13 +736,12 @@ public class SimpleModelUtils {
public static class TriggerNodeConvert implements NodeConvert {
// TODO @芋艿回调在看看
@Override
public List<? extends FlowElement> convertList(BpmSimpleModelNodeVO node) {
Assert.notNull(node.getTriggerSetting(), "触发器节点设置不能为空");
List<FlowElement> flowElements = new ArrayList<>(2);
// HTTP 回调请求需要附加一个 ReceiveTask发起请求后等待回调执行
if (HTTP_CALLBACK.getType().equals(node.getTriggerSetting().getType())) {
if (BpmTriggerTypeEnum.HTTP_CALLBACK.getType().equals(node.getTriggerSetting().getType())) {
Assert.notNull(node.getTriggerSetting().getHttpRequestSetting(), "触发器 HTTP 回调请求设置不能为空");
ReceiveTask receiveTask = new ReceiveTask();
receiveTask.setId("Activity_" + IdUtil.fastUUID());
@ -875,13 +873,12 @@ public class SimpleModelUtils {
if (childProcessSetting.getMultiInstanceSetting().getSourceType().equals(BpmChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY.getType())) {
multiInstanceCharacteristics.setLoopCardinality(childProcessSetting.getMultiInstanceSetting().getSource());
}
if (childProcessSetting.getMultiInstanceSetting().getSourceType().equals(BpmChildProcessMultiInstanceSourceTypeEnum.DIGITAL_FORM.getType()) ||
childProcessSetting.getMultiInstanceSetting().getSourceType().equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTI_FORM.getType())) {
if (childProcessSetting.getMultiInstanceSetting().getSourceType().equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType()) ||
childProcessSetting.getMultiInstanceSetting().getSourceType().equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) {
multiInstanceCharacteristics.setInputDataItem(childProcessSetting.getMultiInstanceSetting().getSource());
}
// TODO @lesanString.format(approveMethodEnum.getCompletionCondition(), String.format("%.2f", approveRatio / 100D)));
multiInstanceCharacteristics.setCompletionCondition(String.format("${ nrOfCompletedInstances/nrOfInstances >= %s}",
String.format("%.2f", childProcessSetting.getMultiInstanceSetting().getCompleteRatio() / 100D)));
multiInstanceCharacteristics.setCompletionCondition(String.format(BpmUserTaskApproveMethodEnum.RATIO.getCompletionCondition(),
String.format("%.2f", childProcessSetting.getMultiInstanceSetting().getApproveRatio() / 100D)));
callActivity.setLoopCharacteristics(multiInstanceCharacteristics);
addExtensionElement(callActivity, CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE, childProcessSetting.getMultiInstanceSetting().getSourceType());
}

View File

@ -143,9 +143,8 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
// 插入拓展表
BpmProcessDefinitionInfoDO definitionDO = BeanUtils.toBean(modelMetaInfo, BpmProcessDefinitionInfoDO.class)
.setModelId(model.getId()).setProcessDefinitionId(definition.getId())
.setModelId(model.getId()).setCategory(model.getCategory()).setProcessDefinitionId(definition.getId())
.setModelType(modelMetaInfo.getType()).setSimpleModel(simpleJson);
if (form != null) {
definitionDO.setFormFields(form.getFields()).setFormConf(form.getConf());
}

View File

@ -85,7 +85,6 @@ public interface BpmProcessInstanceService {
PageResult<HistoricProcessInstance> getProcessInstancePage(Long userId,
@Valid BpmProcessInstancePageReqVO pageReqVO);
// TODO @芋艿重点在 review
/**
* 获取审批详情
* <p>

View File

@ -175,7 +175,12 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
}
startUserId = Long.valueOf(historicProcessInstance.getStartUserId());
processInstanceStatus = FlowableUtils.getProcessInstanceStatus(historicProcessInstance);
processVariables = historicProcessInstance.getProcessVariables();
// 合并 DB 和前端传递的流量变量以前端的为主
Map<String, Object> historicVariables = historicProcessInstance.getProcessVariables();
if (CollUtil.isNotEmpty(processVariables)) {
historicVariables.putAll(processVariables);
}
processVariables = historicVariables;
}
// 1.3 读取其它相关数据
ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition(

View File

@ -20,6 +20,8 @@ import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskSignTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
@ -120,6 +122,9 @@ public class BpmTaskServiceImpl implements BpmTaskService {
if (StrUtil.isNotEmpty(pageVO.getCategory())) {
taskQuery.taskCategory(pageVO.getCategory());
}
if (StrUtil.isNotEmpty(pageVO.getProcessDefinitionKey())) {
taskQuery.processDefinitionKey(pageVO.getProcessDefinitionKey());
}
if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) {
taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0]));
taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1]));
@ -548,7 +553,21 @@ public class BpmTaskServiceImpl implements BpmTaskService {
// 其中variables 是存储动态表单到 local 任务级别过滤一下避免 ProcessInstance 系统级的变量被占用
if (CollUtil.isNotEmpty(reqVO.getVariables())) {
Map<String, Object> variables = FlowableUtils.filterTaskFormVariable(reqVO.getVariables());
// 修改表单的值需要存储到 ProcessInstance 变量
// 校验传递的参数中是否为下一个将要执行的任务节点
validateNextAssignees(task.getTaskDefinitionKey(), reqVO.getVariables(), bpmnModel, reqVO.getNextAssignees(), instance);
// 如果有下一个审批人则设置到流程变量中
// TODO @小北validateNextAssignees 升级成 validateAndSetNextAssignees然后里面吧下面这一小段逻辑抽进去如何
if (CollUtil.isNotEmpty(reqVO.getNextAssignees())) {
// 获取实例中的全部节点数据避免后续节点的审批人被覆盖
// TODO @小北这里有个需要讨论的点微信哈
// TODO 因为 PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES 定位是发起人那么审批人选择的放在 PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES目前想到两个方案
// TODO 方案一增加一个 PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES然后设置到这里面然后BpmTaskCandidateStartUserSelectStrategy 也从这里读
// TODO 方案二也是增加一个 PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES根据节点审批人类型放到 PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEESPROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES
// TODO 方案三融合成 PROCESS_INSTANCE_VARIABLE_USER_SELECT_ASSIGNEES不再区分是发起人选择还是审批人选择
Map<String, List<Long>> hisProcessVariables = FlowableUtils.getStartUserSelectAssignees(instance.getProcessVariables());
hisProcessVariables.putAll(reqVO.getNextAssignees());
variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, hisProcessVariables);
}
runtimeService.setVariables(task.getProcessInstanceId(), variables);
taskService.complete(task.getId(), variables, true);
} else {
@ -559,6 +578,57 @@ public class BpmTaskServiceImpl implements BpmTaskService {
handleParentTaskIfSign(task.getParentTaskId());
}
/**
* 校验选择的下一个节点的审批人是否合法
*
* 1. 是否有漏选没有选择审批人
* 2. 是否有多选非下一个节点
*
* @param taskDefinitionKey 当前任务节点标识
* @param variables 流程变量
* @param bpmnModel 流程模型
* @param nextAssignees 下一个节点审批人集合参数
* @param processInstance 流程实例
*/
private void validateNextAssignees(String taskDefinitionKey, Map<String, Object> variables, BpmnModel bpmnModel,
Map<String, List<Long>> nextAssignees, ProcessInstance processInstance) {
// 1. 获取当前任务节点的信息
FlowElement flowElement = bpmnModel.getFlowElement(taskDefinitionKey);
// 2. 获取下一个将要执行的节点集合
List<FlowNode> nextFlowNodes = getNextFlowNodes(flowElement, bpmnModel, variables);
// 3. 循环下一个将要执行的节点集合
for (FlowNode nextFlowNode : nextFlowNodes) {
// 3.1 获取下一个将要执行节点的属性是否为自选审批人等
// TODO @小北public static Integer parseCandidateStrategy(FlowElement userTask) 使用这个工具方法哈
Map<String, List<ExtensionElement>> extensionElements = nextFlowNode.getExtensionElements();
List<ExtensionElement> elements = extensionElements.get(BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY);
if (CollUtil.isEmpty(elements)) {
continue;
}
// 3.2 获取节点中的审批人策略
Integer candidateStrategy = Integer.valueOf(elements.get(0).getElementText());
// 3.3 获取流程实例中的发起人自选审批人
Map<String, List<Long>> startUserSelectAssignees = FlowableUtils.getStartUserSelectAssignees(processInstance.getProcessVariables());
List<Long> startUserSelectAssignee = startUserSelectAssignees.get(nextFlowNode.getId());
// 3.4 如果节点中的审批人策略为 发起人自选并且该节点的审批人为空
if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy()) && CollUtil.isEmpty(startUserSelectAssignee)) {
// 先判断前端传递的参数节点节点是否为将要执行的节点
// TODO @小北!nextAssignees.containsKey(nextFlowNode.getId()) CollUtil.isEmpty(nextAssignees.get(nextFlowNode.getId()))) 是不是等价的
if (!nextAssignees.containsKey(nextFlowNode.getId())) {
throw exception(TASK_TARGET_NODE_NOT_EXISTS, nextFlowNode.getName());
}
// 如果前端传递的节点为空则抛出异常
// TODO @小北换一个错误码哈
if (CollUtil.isEmpty(nextAssignees.get(nextFlowNode.getId()))) {
throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, nextFlowNode.getName());
}
}
// TODO @小北加一个审批人选择的校验
}
}
/**
* 审批通过存在后加签的任务
* <p>

View File

@ -42,8 +42,8 @@ public class BpmHttpCallbackTrigger extends BpmAbstractHttpRequestTrigger {
MultiValueMap<String, String> headers = buildHttpHeaders(processInstance, setting.getHeader());
// 2.2 设置请求体
MultiValueMap<String, String> body = buildHttpBody(processInstance, setting.getBody());
// TODO @芋艿回调在看看
body.add("callbackId", setting.getCallbackTaskDefineKey()); // 回调请求 callbackId 需要传给被调用方用于回调执行
// 重要回调请求 taskDefineKey 需要传给被调用方用于回调执行
body.add("taskDefineKey", setting.getCallbackTaskDefineKey());
// 3. 发起请求
sendHttpRequest(setting.getUrl(), headers, body);