【功能新增】工作流:支持审批人为空时,根据配置进行自动通过、自动拒绝、指定人审批的效果

This commit is contained in:
YunaiV 2024-08-17 16:24:25 +08:00
parent d4306846f9
commit 0d738fa397
13 changed files with 197 additions and 22 deletions

View File

@ -0,0 +1,29 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* BPM 用户任务的审批人为空时处理类型枚举
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Getter
public enum BpmUserTaskAssignEmptyHandlerTypeEnum implements IntArrayValuable {
APPROVE(1), // 自动通过
REJECT(2), // 自动拒绝
ASSIGN_USER(3), // 指定人员审批
ASSIGN_ADMIN(4), // 转交给流程管理员
;
private final Integer type;
@Override
public int[] array() {
return new int[0];
}
}

View File

@ -28,6 +28,8 @@ public enum BpmReasonEnum {
ASSIGN_START_USER_APPROVE_WHEN_SKIP("审批人与提交人为同一人时,自动通过"),
ASSIGN_START_USER_APPROVE_WHEN_DEPT_LEADER_NOT_FOUND("审批人与提交人为同一人时,找不到部门负责人,自动通过"),
ASSIGN_START_USER_TRANSFER_DEPT_LEADER("审批人与提交人为同一人时,转交给部门负责人审批"),
ASSIGN_EMPTY_APPROVE("审批人为空,自动通过"),
ASSIGN_EMPTY_REJECT("审批人为空,自动不通过"),
;
private final String reason;

View File

@ -86,6 +86,11 @@ public class BpmSimpleModelNodeVO {
@InEnum(BpmUserTaskAssignStartUserHandlerTypeEnum.class)
private Integer assignStartUserHandlerType;
/**
* 空处理策略
*/
private AssignEmptyHandler assignEmptyHandler;
@Data
@Schema(description = "审批节点拒绝处理策略")
public static class RejectHandler {
@ -121,6 +126,21 @@ public class BpmSimpleModelNodeVO {
}
@Data
@Schema(description = "空处理策略")
@Valid
public static class AssignEmptyHandler {
@Schema(description = "空处理类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "空处理类型不能为空")
@InEnum(BpmUserTaskAssignEmptyHandlerTypeEnum.class)
private Integer type;
@Schema(description = "指定人员审批的用户编号数组", example = "1")
private List<Long> userIds;
}
@Data
@Schema(description = "操作按钮设置")
public static class OperationButtonSetting {

View File

@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
import lombok.Setter;
@ -49,6 +51,10 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
// 第二步获取任务的所有处理人
Set<Long> assigneeUserIds = taskCandidateInvoker.calculateUsers(execution);
if (CollUtil.isEmpty(assigneeUserIds)) {
// 特殊如果没有处理人的情况下至少有一个 null 空元素保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务避免自动通过
assigneeUserIds = SetUtils.asSet((Long) null);
}
execution.setVariable(super.collectionVariable, assigneeUserIds);
return assigneeUserIds.size();
}

View File

@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
import lombok.Setter;
@ -43,6 +45,10 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
// 第二步获取任务的所有处理人
Set<Long> assigneeUserIds = new LinkedHashSet<>(taskCandidateInvoker.calculateUsers(execution)); // 保证有序
if (CollUtil.isEmpty(assigneeUserIds)) {
// 特殊如果没有处理人的情况下至少有一个 null 空元素保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务避免自动通过
assigneeUserIds = SetUtils.asSet((Long) null);
}
execution.setVariable(super.collectionVariable, assigneeUserIds);
return assigneeUserIds.size();
}

View File

@ -1,9 +1,16 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskApproveReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRejectReqVO;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignEmptyHandlerTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.UserTask;
@ -14,6 +21,8 @@ import org.flowable.engine.impl.cfg.ProcessEngineConfigurationImpl;
import org.flowable.engine.impl.util.TaskHelper;
import org.flowable.task.service.TaskService;
import org.flowable.task.service.impl.persistence.entity.TaskEntity;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.util.List;
import java.util.Set;
@ -41,9 +50,29 @@ public class BpmUserTaskActivityBehavior extends UserTaskActivityBehavior {
DelegateExecution execution, ProcessEngineConfigurationImpl processEngineConfiguration) {
// 第一步获得任务的候选用户
Long assigneeUserId = calculateTaskCandidateUsers(execution);
Assert.notNull(assigneeUserId, "任务处理人不能为空");
// 第二步设置作为负责人
TaskHelper.changeTaskAssignee(task, String.valueOf(assigneeUserId));
if (assigneeUserId != null) {
TaskHelper.changeTaskAssignee(task, String.valueOf(assigneeUserId));
return;
}
// 特殊审批人为空根据配置是否要自动通过自动拒绝
Integer assignEmptyHandlerType = BpmnModelUtils.parseAssignEmptyHandlerType(userTask);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
if (ObjectUtil.equal(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.APPROVE.getType())) {
SpringUtil.getBean(BpmTaskService.class).approveTask(null, new BpmTaskApproveReqVO()
.setId(task.getId()).setReason(BpmReasonEnum.ASSIGN_EMPTY_APPROVE.getReason()));
} else if (ObjectUtil.equal(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.REJECT.getType())) {
SpringUtil.getBean(BpmTaskService.class).rejectTask(null, new BpmTaskRejectReqVO()
.setId(task.getId()).setReason(BpmReasonEnum.ASSIGN_EMPTY_REJECT.getReason()));
}
}
});
}
private Long calculateTaskCandidateUsers(DelegateExecution execution) {
@ -56,6 +85,9 @@ public class BpmUserTaskActivityBehavior extends UserTaskActivityBehavior {
// 情况二如果非多实例的任务则计算任务处理人
// 第一步先计算可处理该任务的处理人们
Set<Long> candidateUserIds = taskCandidateInvoker.calculateUsers(execution);
if (CollUtil.isEmpty(candidateUserIds)) {
return null;
}
// 第二步后随机选择一个任务的处理人
// 疑问为什么一定要选择一个任务处理人
// 解答项目对 bpm 的任务是责任到人所以每个任务有且仅有一个处理人

View File

@ -27,7 +27,6 @@ import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.MODEL_DEPLOY_FAIL_TASK_CANDIDATE_NOT_CONFIG;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.TASK_CREATE_FAIL_NO_CANDIDATE_USER;
/**
* {@link BpmTaskCandidateStrategy} 的调用者用于调用对应的策略实现任务的候选人的计算
@ -89,17 +88,16 @@ public class BpmTaskCandidateInvoker {
String param = BpmnModelUtils.parseCandidateParam(execution.getCurrentFlowElement());
// 1.1 计算任务的候选人
Set<Long> userIds = getCandidateStrategy(strategy).calculateUsers(execution, param);
// 1.2 移除被禁用的用户
removeDisableUsers(userIds);
// 1.2 候选人为空时根据审批人为空的配置补充
if (CollUtil.isEmpty(userIds)) {
userIds = getCandidateStrategy(BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY.getStrategy())
.calculateUsers(execution, param);
}
// 1.3 移除发起人的用户
removeStartUserIfSkip(execution, userIds);
// 2. 校验是否有候选人
if (CollUtil.isEmpty(userIds)) {
log.error("[calculateUsers][流程任务({}/{}/{}) 任务规则({}/{}) 找不到候选人]", execution.getId(),
execution.getProcessDefinitionId(), execution.getCurrentActivityId(), strategy, param);
throw exception(TASK_CREATE_FAIL_NO_CANDIDATE_USER);
}
// 2. 移除被禁用的用户
removeDisableUsers(userIds);
return userIds;
}

View File

@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignEmptyHandlerTypeEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
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.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.flowable.engine.delegate.DelegateExecution;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
/**
* 审批人为空 {@link BpmTaskCandidateStrategy} 实现类
*
* @author kyle
*/
@Component
public class BpmTaskCandidateAssignEmptyStrategy implements BpmTaskCandidateStrategy {
@Resource
private AdminUserApi adminUserApi;
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
return BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY;
}
@Override
public void validateParam(String param) {
}
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
// 情况一指定人员审批
Integer assignEmptyHandlerType = BpmnModelUtils.parseAssignEmptyHandlerType(execution.getCurrentFlowElement());
if (Objects.equals(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.ASSIGN_USER.getType())) {
return new HashSet<>(BpmnModelUtils.parseAssignEmptyHandlerUserIds(execution.getCurrentFlowElement()));
}
// 情况二流程管理员
if (Objects.equals(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.ASSIGN_ADMIN.getType())) {
// TODO 芋艿需要等待流程实例的管理员支持
throw new UnsupportedOperationException("暂时实现!!!");
}
// 都不满足还是返回空
return new HashSet<>();
}
}

View File

@ -30,6 +30,7 @@ public enum BpmTaskCandidateStrategyEnum implements IntArrayValuable {
START_USER_MULTI_LEVEL_DEPT_LEADER(38, "发起人连续多级部门的负责人"),
USER_GROUP(40, "用户组"),
EXPRESSION(60, "流程表达式"), // 表达式 ExpressionManager
ASSIGN_EMPTY(1, "审批人为空"),
;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmTaskCandidateStrategyEnum::getStrategy).toArray();

View File

@ -38,6 +38,15 @@ public interface BpmnModelConstants {
*/
String USER_TASK_ASSIGN_START_USER_HANDLER_TYPE = "assignStartUserHandlerType";
/**
* BPMN ExtensionElement 的扩展属性用于标记用户任务的空处理类型
*/
String USER_TASK_ASSIGN_EMPTY_HANDLER_TYPE = "assignEmptyHandlerType";
/**
* BPMN ExtensionElement 的扩展属性用于标记用户任务的空处理的指定用户编号数组
*/
String USER_TASK_ASSIGN_USER_IDS = "assignEmptyUserIds";
/**
* BPMN ExtensionElement 的扩展属性用于标记用户任务拒绝处理类型
*/

View File

@ -5,6 +5,7 @@ import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.framework.common.util.string.StrUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskRejectHandlerType;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
@ -45,16 +46,24 @@ public class BpmnModelUtils {
}
public static BpmUserTaskRejectHandlerType parseRejectHandlerType(FlowElement userTask) {
Integer rejectHandlerType = NumberUtils.parseInt(BpmnModelUtils.parseExtensionElement(userTask, USER_TASK_REJECT_HANDLER_TYPE));
Integer rejectHandlerType = NumberUtils.parseInt(parseExtensionElement(userTask, USER_TASK_REJECT_HANDLER_TYPE));
return BpmUserTaskRejectHandlerType.typeOf(rejectHandlerType);
}
public static String parseReturnTaskId(FlowElement flowElement) {
return BpmnModelUtils.parseExtensionElement(flowElement, USER_TASK_REJECT_RETURN_TASK_ID);
return parseExtensionElement(flowElement, USER_TASK_REJECT_RETURN_TASK_ID);
}
public static Integer parseAssignStartUserHandlerType(FlowElement userTask) {
return NumberUtils.parseInt(BpmnModelUtils.parseExtensionElement(userTask, USER_TASK_ASSIGN_START_USER_HANDLER_TYPE));
return NumberUtils.parseInt(parseExtensionElement(userTask, USER_TASK_ASSIGN_START_USER_HANDLER_TYPE));
}
public static Integer parseAssignEmptyHandlerType(FlowElement userTask) {
return NumberUtils.parseInt(parseExtensionElement(userTask, USER_TASK_ASSIGN_EMPTY_HANDLER_TYPE));
}
public static List<Long> parseAssignEmptyHandlerUserIds(FlowElement userTask) {
return StrUtils.splitToLong(parseExtensionElement(userTask, USER_TASK_ASSIGN_USER_IDS), ",");
}
public static String parseExtensionElement(FlowElement flowElement, String elementName) {

View File

@ -19,10 +19,7 @@ import org.flowable.bpmn.BpmnAutoLayout;
import org.flowable.bpmn.model.Process;
import org.flowable.bpmn.model.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.*;
import static cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.OperationButtonSetting;
import static cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.TimeoutHandler;
@ -456,7 +453,6 @@ public class SimpleModelUtils {
userTask.setDueDate(node.getTimeoutHandler().getTimeDuration());
}
// TODO @jasonaddCandidateElementsprocessMultiInstanceLoopCharacteristics 建议一起搞哈
// 添加候选人元素
addCandidateElements(node.getCandidateStrategy(), node.getCandidateParam(), userTask);
// 添加表单字段权限属性元素
@ -469,6 +465,8 @@ public class SimpleModelUtils {
addTaskRejectElements(node.getRejectHandler(), userTask);
// 添加用户任务的审批人与发起人相同时的处理元素
addAssignStartUserHandlerType(node.getAssignStartUserHandlerType(), userTask);
// 添加用户任务的空处理元素
addAssignEmptyHandlerType(node.getAssignEmptyHandler(), userTask);
return userTask;
}
@ -487,6 +485,14 @@ public class SimpleModelUtils {
addExtensionElement(userTask, USER_TASK_ASSIGN_START_USER_HANDLER_TYPE, assignStartUserHandlerType.toString());
}
private static void addAssignEmptyHandlerType(BpmSimpleModelNodeVO.AssignEmptyHandler emptyHandler, UserTask userTask) {
if (emptyHandler == null) {
return;
}
addExtensionElement(userTask, USER_TASK_ASSIGN_EMPTY_HANDLER_TYPE, StrUtil.toStringOrNull(emptyHandler.getType()));
addExtensionElement(userTask, USER_TASK_ASSIGN_USER_IDS, StrUtil.join(",", emptyHandler.getUserIds()));
}
private static void processMultiInstanceLoopCharacteristics(Integer approveMethod, Integer approveRatio, UserTask userTask) {
BpmApproveMethodEnum bpmApproveMethodEnum = BpmApproveMethodEnum.valueOf(approveMethod);
if (bpmApproveMethodEnum == null || bpmApproveMethodEnum == BpmApproveMethodEnum.RANDOM) {
@ -496,7 +502,7 @@ public class SimpleModelUtils {
addExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_METHOD,
approveMethod == null ? null : approveMethod.toString());
MultiInstanceLoopCharacteristics multiInstanceCharacteristics = new MultiInstanceLoopCharacteristics();
// 设置 collectionVariable本系统用不到会在 仅仅为了校验
// 设置 collectionVariable本系统用不到仅仅为了 Flowable 校验不报错
multiInstanceCharacteristics.setInputDataItem("${coll_userList}");
if (bpmApproveMethodEnum == BpmApproveMethodEnum.ANY) {
multiInstanceCharacteristics.setCompletionCondition(ANY_OF_APPROVE_COMPLETE_EXPRESSION);

View File

@ -191,7 +191,10 @@ public class BpmTaskServiceImpl implements BpmTaskService {
*/
private Task validateTask(Long userId, String taskId) {
Task task = validateTaskExist(taskId);
if (!Objects.equals(userId, NumberUtils.parseLong(task.getAssignee()))) {
// 为什么判断 assignee 非空的情况下
// 例如说在审批人为空时我们会有自动审批通过的策略此时 userId null允许通过
if (StrUtil.isNotBlank(task.getAssignee())
&& ObjectUtil.notEqual(userId, NumberUtils.parseLong(task.getAssignee()))) {
throw exception(TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
}
return task;