BPM:增加「发起人自选」的任务审批人的分配策略

This commit is contained in:
YunaiV 2024-03-23 00:54:26 +08:00
parent acea73c991
commit 528a321f0a
33 changed files with 305 additions and 189 deletions

View File

@ -32,14 +32,13 @@ public class BpmProcessInstanceCreateReqDTO {
@NotEmpty(message = "业务的唯一标识")
private String businessKey;
// TODO @haiassignees 复数
/**
* 提前指派的审批人
* 发起人自选审批人 Map
*
* keytaskKey 任务编码
* value审批人的数组
* 例如 { taskKey1 :[1, 2] }则表示 taskKey1 这个任务提前设定了 userId 1,2 的用户进行审批
* 例如{ taskKey1 :[1, 2] }则表示 taskKey1 这个任务提前设定了 userId 1,2 的用户进行审批
*/
private Map<String, List<Long>> assignee;
private Map<String, List<Long>> startUserSelectAssignees;
}

View File

@ -29,12 +29,13 @@ public interface ErrorCodeConstants {
ErrorCode PROCESS_DEFINITION_NAME_NOT_MATCH = new ErrorCode(1_009_003_001, "流程定义的名字期望是({}),当前是({}),请修改 BPMN 流程图");
ErrorCode PROCESS_DEFINITION_NOT_EXISTS = new ErrorCode(1_009_003_002, "流程定义不存在");
ErrorCode PROCESS_DEFINITION_IS_SUSPENDED = new ErrorCode(1_009_003_003, "流程定义处于挂起状态");
ErrorCode PROCESS_DEFINITION_BPMN_MODEL_NOT_EXISTS = new ErrorCode(1_009_003_004, "流程定义的模型不存在");
// ========== 流程实例 1-009-004-000 ==========
ErrorCode PROCESS_INSTANCE_NOT_EXISTS = new ErrorCode(1_009_004_000, "流程实例不存在");
ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_EXISTS = new ErrorCode(1_009_004_001, "流程取消失败,流程不处于运行中");
ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_SELF = new ErrorCode(1_009_004_002, "流程取消失败,该流程不是你发起的");
ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_003, "审批任务({})的审批人未配置");
ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS = new ErrorCode(1_009_004_004, "审批任务({})的审批人({})不存在");
// ========== 流程任务 1-009-005-000 ==========
ErrorCode TASK_OPERATE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1_009_005_001, "操作失败,原因:该任务的审批人不是你");
@ -50,8 +51,6 @@ public interface ErrorCodeConstants {
ErrorCode TASK_SIGN_DELETE_NO_PARENT = new ErrorCode(1_009_005_012, "任务减签失败,被减签的任务必须是通过加签生成的任务");
ErrorCode TASK_TRANSFER_FAIL_USER_REPEAT = new ErrorCode(1_009_005_013, "任务转办失败,转办人和当前审批人为同一人");
ErrorCode TASK_TRANSFER_FAIL_USER_NOT_EXISTS = new ErrorCode(1_009_005_014, "任务转办失败,转办人不存在");
// ========== 流程任务分配规则 1-009-006-000 TODO 芋艿这里要改下 ==========
ErrorCode TASK_CREATE_FAIL_NO_CANDIDATE_USER = new ErrorCode(1_009_006_003, "操作失败,原因:找不到任务的审批人!");
// ========== 动态表单模块 1-009-010-000 ==========

View File

@ -9,6 +9,7 @@ import cn.iocoder.yudao.module.bpm.convert.definition.BpmProcessDefinitionConver
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.BpmTaskCandidateStartUserSelectStrategy;
import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmFormService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
@ -16,6 +17,8 @@ import io.swagger.v3.oas.annotations.Operation;
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.bpmn.model.UserTask;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.ProcessDefinition;
import org.springframework.security.access.prepost.PreAuthorize;
@ -89,13 +92,23 @@ public class BpmProcessDefinitionController {
list, null, processDefinitionMap, null, null));
}
@GetMapping ("/get-bpmn-xml")
@Operation(summary = "获得流程定义的 BPMN XML")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@GetMapping ("/get")
@Operation(summary = "获得流程定义")
@Parameter(name = "id", description = "流程编号", required = true, example = "1024")
@Parameter(name = "key", description = "流程定义标识", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('bpm:process-definition:query')")
public CommonResult<String> getProcessDefinitionBpmnXML(@RequestParam("id") String id) {
String bpmnXML = processDefinitionService.getProcessDefinitionBpmnXML(id);
return success(bpmnXML);
public CommonResult<BpmProcessDefinitionRespVO> getProcessDefinition(
@RequestParam(value = "id", required = false) String id,
@RequestParam(value = "key", required = false) String key) {
ProcessDefinition processDefinition = id != null ? processDefinitionService.getProcessDefinition(id)
: processDefinitionService.getActiveProcessDefinition(key);
if (processDefinition == null) {
return success(null);
}
BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(processDefinition.getId());
List<UserTask> userTaskList = BpmTaskCandidateStartUserSelectStrategy.getStartUserSelectUserTaskList(bpmnModel);
return success(BpmProcessDefinitionConvert.INSTANCE.buildProcessDefinition(
processDefinition, null, null, null, null, bpmnModel, userTaskList));
}
}

View File

@ -18,6 +18,9 @@ public class BpmCategorySaveReqVO {
@NotEmpty(message = "分类名不能为空")
private String name;
@Schema(description = "分类描述", example = "你猜")
private String description;
@Schema(description = "分类标志", requiredMode = Schema.RequiredMode.REQUIRED, example = "OA")
@NotEmpty(message = "分类标志不能为空")
private String code;

View File

@ -48,7 +48,7 @@ public class BpmProcessDefinitionRespVO {
private String formCustomViewPath;
@Schema(description = "中断状态-参见 SuspensionState 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer suspensionState;
private Integer suspensionState; // 参见 SuspensionState 枚举
@Schema(description = "部署时间")
private LocalDateTime deploymentTime; // 需要从对应的 Deployment 读取非必须返回
@ -56,4 +56,19 @@ public class BpmProcessDefinitionRespVO {
@Schema(description = "BPMN XML")
private String bpmnXml; // 需要从对应的 BpmnModel 读取非必须返回
@Schema(description = "发起用户需要选择审批人的任务数组")
private List<UserTask> startUserSelectTasks; // 需要从对应的 BpmnModel 读取非必须返回
@Schema(description = "BPMN UserTask 用户任务")
@Data
public static class UserTask {
@Schema(description = "任务标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "sudo")
private String id;
@Schema(description = "任务名", requiredMode = Schema.RequiredMode.REQUIRED, example = "王五")
private String name;
}
}

View File

@ -7,6 +7,8 @@ import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
@ -30,6 +32,9 @@ public class BpmOALeaveCreateReqVO {
@Schema(description = "原因", requiredMode = Schema.RequiredMode.REQUIRED, example = "阅读芋道源码")
private String reason;
@Schema(description = "发起人自选审批人 Map", example = "{taskKey1: [1, 2]}")
private Map<String, List<Long>> startUserSelectAssignees;
@AssertTrue(message = "结束时间,需要在开始时间之后")
public boolean isEndTimeValid() {
return !getEndTime().isBefore(getStartTime());

View File

@ -11,6 +11,7 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessI
import cn.iocoder.yudao.module.bpm.convert.task.BpmProcessInstanceConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
@ -130,7 +131,8 @@ public class BpmProcessInstanceController {
processInstance.getProcessDefinitionId());
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.getProcessDefinitionInfo(
processInstance.getProcessDefinitionId());
String bpmnXml = processDefinitionService.getProcessDefinitionBpmnXML(processInstance.getProcessDefinitionId());
String bpmnXml = BpmnModelUtils.getBpmnXml(
processDefinitionService.getProcessDefinitionBpmnModel(processInstance.getProcessDefinitionId()));
AdminUserRespDTO startUser = adminUserApi.getUser(NumberUtils.parseLong(processInstance.getStartUserId()));
DeptRespDTO dept = null;
if (startUser != null) {

View File

@ -18,8 +18,7 @@ public class BpmProcessInstanceCreateReqVO {
@Schema(description = "变量实例(动态表单)")
private Map<String, Object> variables;
// TODO @haiassignees 复数
@Schema(description = "提前指派的审批人", requiredMode = Schema.RequiredMode.REQUIRED, example = "{taskKey1: [1, 2]}")
private Map<String, List<Long>> assignee;
@Schema(description = "发起人自选审批人 Map", example = "{taskKey1: [1, 2]}")
private Map<String, List<Long>> startUserSelectAssignees;
}

View File

@ -4,12 +4,14 @@ import cn.hutool.core.date.LocalDateTimeUtil;
import cn.hutool.core.map.MapUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.UserTask;
import org.flowable.common.engine.impl.db.SuspensionState;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.ProcessDefinition;
@ -47,27 +49,50 @@ public interface BpmProcessDefinitionConvert {
Map<Long, BpmFormDO> formMap,
Map<String, BpmCategoryDO> categoryMap) {
return CollectionUtils.convertList(list, definition -> {
BpmProcessDefinitionRespVO respVO = BeanUtils.toBean(definition, BpmProcessDefinitionRespVO.class);
respVO.setSuspensionState(definition.isSuspended() ? SuspensionState.SUSPENDED.getStateCode() : SuspensionState.ACTIVE.getStateCode());
// Deployment
MapUtils.findAndThen(deploymentMap, definition.getDeploymentId(),
deployment -> respVO.setDeploymentTime(LocalDateTimeUtil.of(deployment.getDeploymentTime())));
// BpmProcessDefinitionInfoDO
Deployment deployment = MapUtil.get(deploymentMap, definition.getDeploymentId(), Deployment.class);
BpmProcessDefinitionInfoDO processDefinitionInfo = MapUtil.get(processDefinitionInfoMap, definition.getId(), BpmProcessDefinitionInfoDO.class);
BpmFormDO form = null;
if (processDefinitionInfo != null) {
copyTo(processDefinitionInfo, respVO);
// Form
BpmFormDO form = MapUtil.get(formMap, processDefinitionInfo.getFormId(), BpmFormDO.class);
if (form != null) {
respVO.setFormName(form.getName());
}
form = MapUtil.get(formMap, processDefinitionInfo.getFormId(), BpmFormDO.class);
}
// Category
MapUtils.findAndThen(categoryMap, definition.getCategory(), category -> respVO.setCategoryName(category.getName()));
return respVO;
BpmCategoryDO category = MapUtil.get(categoryMap, definition.getCategory(), BpmCategoryDO.class);
return buildProcessDefinition(definition, deployment, processDefinitionInfo, form, category, null, null);
});
}
default BpmProcessDefinitionRespVO buildProcessDefinition(ProcessDefinition definition,
Deployment deployment,
BpmProcessDefinitionInfoDO processDefinitionInfo,
BpmFormDO form,
BpmCategoryDO category,
BpmnModel bpmnModel,
List<UserTask> startUserSelectUserTaskList) {
BpmProcessDefinitionRespVO respVO = BeanUtils.toBean(definition, BpmProcessDefinitionRespVO.class);
respVO.setSuspensionState(definition.isSuspended() ? SuspensionState.SUSPENDED.getStateCode() : SuspensionState.ACTIVE.getStateCode());
// Deployment
if (deployment != null) {
respVO.setDeploymentTime(LocalDateTimeUtil.of(deployment.getDeploymentTime()));
}
// BpmProcessDefinitionInfoDO
if (processDefinitionInfo != null) {
copyTo(processDefinitionInfo, respVO);
// Form
if (form != null) {
respVO.setFormName(form.getName());
}
}
// Category
if (category != null) {
respVO.setCategoryName(category.getName());
}
// BpmnModel
if (bpmnModel != null) {
respVO.setBpmnXml(BpmnModelUtils.getBpmnXml(bpmnModel));
respVO.setStartUserSelectTasks(BeanUtils.toBean(startUserSelectUserTaskList, BpmProcessDefinitionRespVO.UserTask.class));
}
return respVO;
}
@Mapping(source = "from.id", target = "to.id", ignore = true)
void copyTo(BpmProcessDefinitionInfoDO from, @MappingTarget BpmProcessDefinitionRespVO to);

View File

@ -68,7 +68,7 @@ public interface BpmProcessInstanceConvert {
DeptRespDTO dept) {
BpmProcessInstanceRespVO respVO = BeanUtils.toBean(processInstance, BpmProcessInstanceRespVO.class);
respVO.setStatus(FlowableUtils.getProcessInstanceStatus(processInstance));
respVO.setFormVariables(FlowableUtils.filterProcessInstanceFormVariable(processInstance.getProcessVariables()));
respVO.setFormVariables(FlowableUtils.getProcessInstanceFormVariable(processInstance));
// definition
respVO.setProcessDefinition(BeanUtils.toBean(processDefinition, BpmProcessDefinitionRespVO.class));
copyTo(processDefinitionExt, respVO.getProcessDefinition());

View File

@ -1,11 +1,10 @@
package cn.iocoder.yudao.module.bpm.dal.dataobject.definition;
import lombok.*;
import java.util.*;
import java.time.LocalDateTime;
import java.time.LocalDateTime;
import com.baomidou.mybatisplus.annotation.*;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
/**
* BPM 流程分类 DO
@ -42,7 +41,7 @@ public class BpmCategoryDO extends BaseDO {
/**
* 分类状态
*
* 枚举 {@link TODO common_status 对应的类}
* 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
*/
private Integer status;
/**

View File

@ -21,7 +21,7 @@ public interface BpmCategoryMapper extends BaseMapperX<BpmCategoryDO> {
default PageResult<BpmCategoryDO> selectPage(BpmCategoryPageReqVO reqVO) {
return selectPage(reqVO, new LambdaQueryWrapperX<BpmCategoryDO>()
.likeIfPresent(BpmCategoryDO::getName, reqVO.getName())
.eqIfPresent(BpmCategoryDO::getCode, reqVO.getCode())
.likeIfPresent(BpmCategoryDO::getCode, reqVO.getCode())
.eqIfPresent(BpmCategoryDO::getStatus, reqVO.getStatus())
.betweenIfPresent(BpmCategoryDO::getCreateTime, reqVO.getCreateTime())
.orderByAsc(BpmCategoryDO::getSort));

View File

@ -1,41 +0,0 @@
package cn.iocoder.yudao.module.bpm.framework;
import cn.hutool.core.collection.CollUtil;
import com.alibaba.ttl.TransmittableThreadLocal;
import java.util.Collections;
import java.util.List;
import java.util.Map;
/**
* 工作流--用户用到的上下文相关信息
*/
@Deprecated // TODO 芋艿找个方式去掉这个上下文
public class FlowableContextHolder {
private static final ThreadLocal<Map<String, List<Long>>> ASSIGNEE = new TransmittableThreadLocal<>();
/**
* 通过流程任务的定义 key 拿到提前选好的审批人
* 此方法目的首次创建流程实例时数据库中还查询不到 assignee 字段所以存入上下文中获取
*
* @param taskDefinitionKey 流程任务 key
* @return 审批人 ID 集合
*/
public static List<Long> getAssigneeByTaskDefinitionKey(String taskDefinitionKey) {
if (CollUtil.isNotEmpty(ASSIGNEE.get())) {
return ASSIGNEE.get().get(taskDefinitionKey);
}
return Collections.emptyList();
}
/**
* 存入提前选好的审批人到上下文线程变量中
*
* @param assignee 流程任务 key -> 审批人 ID 炅和
*/
public static void setAssignee(Map<String, List<Long>> assignee) {
ASSIGNEE.set(assignee);
}
}

View File

@ -4,16 +4,13 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
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.util.BpmnModelUtils;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.delegate.DelegateExecution;
@ -60,9 +57,13 @@ public class BpmTaskCandidateInvoker {
// 遍历所有的 UserTask校验审批人配置
userTaskList.forEach(userTask -> {
// 1. 非空校验
Integer strategy = parseCandidateStrategy(userTask);
String param = parseCandidateParam(userTask);
if (strategy == null || StrUtil.isBlank(param)) {
Integer strategy = BpmnModelUtils.parseCandidateStrategy(userTask);
String param = BpmnModelUtils.parseCandidateParam(userTask);
if (strategy == null) {
throw exception(MODEL_DEPLOY_FAIL_TASK_CANDIDATE_NOT_CONFIG, userTask.getName());
}
BpmTaskCandidateStrategy candidateStrategy = getCandidateStrategy(strategy);
if (candidateStrategy.isParamRequired() && StrUtil.isBlank(param)) {
throw exception(MODEL_DEPLOY_FAIL_TASK_CANDIDATE_NOT_CONFIG, userTask.getName());
}
// 2. 具体策略校验
@ -77,16 +78,8 @@ public class BpmTaskCandidateInvoker {
* @return 用户编号集合
*/
public Set<Long> calculateUsers(DelegateExecution execution) {
// TODO 芋艿这里需要重构
// // 1. 先从提前选好的审批人中获取
// List<Long> assignee = processInstanceService.getAssigneeByProcessInstanceIdAndTaskDefinitionKey(
// execution.getProcessInstanceId(), execution.getCurrentActivityId());
// if (CollUtil.isNotEmpty(assignee)) {
// // TODO @hainew HashSet 即可
// return convertSet(assignee, Function.identity());
// }
Integer strategy = parseCandidateStrategy(execution.getCurrentFlowElement());
String param = parseCandidateParam(execution.getCurrentFlowElement());
Integer strategy = BpmnModelUtils.parseCandidateStrategy(execution.getCurrentFlowElement());
String param = BpmnModelUtils.parseCandidateParam(execution.getCurrentFlowElement());
// 1.1 计算任务的候选人
Set<Long> userIds = getCandidateStrategy(strategy).calculateUsers(execution, param);
// 1.2 移除被禁用的用户
@ -113,16 +106,6 @@ public class BpmTaskCandidateInvoker {
});
}
private static Integer parseCandidateStrategy(FlowElement userTask) {
return NumberUtils.parseInt(userTask.getAttributeValue(
BpmnModelConstants.NAMESPACE, BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY));
}
private static String parseCandidateParam(FlowElement userTask) {
return userTask.getAttributeValue(
BpmnModelConstants.NAMESPACE, BpmnModelConstants.USER_TASK_CANDIDATE_PARAM);
}
private BpmTaskCandidateStrategy getCandidateStrategy(Integer strategy) {
BpmTaskCandidateStrategyEnum strategyEnum = BpmTaskCandidateStrategyEnum.valueOf(strategy);
Assert.notNull(strategyEnum, "策略(%s) 不存在", strategy);

View File

@ -36,4 +36,13 @@ public interface BpmTaskCandidateStrategy {
*/
Set<Long> calculateUsers(DelegateExecution execution, String param);
/**
* 是否一定要输入参数
*
* @return 是否
*/
default boolean isParamRequired() {
return true;
}
}

View File

@ -32,12 +32,12 @@ public class BpmTaskAssignLeaderExpression {
private DeptApi deptApi;
@Resource
private BpmProcessInstanceService bpmProcessInstanceService;
private BpmProcessInstanceService processInstanceService;
protected Set<Long> calculateUsers(DelegateExecution execution, int level) {
Assert.isTrue(level > 0, "level 必须大于 0");
// 获得发起人
ProcessInstance processInstance = bpmProcessInstanceService.getProcessInstance(execution.getProcessInstanceId());
ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId());
Long startUserId = NumberUtils.parseLong(processInstance.getStartUserId());
// 获得对应 leve 的部门
DeptRespDTO dept = null;

View File

@ -19,10 +19,10 @@ import java.util.Set;
public class BpmTaskAssignStartUserExpression {
@Resource
private BpmProcessInstanceService bpmProcessInstanceService;
private BpmProcessInstanceService processInstanceService;
public Set<Long> calculateUsers(DelegateExecution execution) {
ProcessInstance processInstance = bpmProcessInstanceService.getProcessInstance(execution.getProcessInstanceId());
ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId());
Long startUserId = NumberUtils.parseLong(processInstance.getStartUserId());
return SetUtils.asSet(startUserId);
}

View File

@ -0,0 +1,76 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
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.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import jakarta.annotation.Resource;
import org.flowable.bpmn.model.BpmnModel;
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.*;
/**
* 发起人自选 {@link BpmTaskCandidateUserStrategy} 实现类
*
* @author 芋道源码
*/
@Component
public class BpmTaskCandidateStartUserSelectStrategy implements BpmTaskCandidateStrategy {
@Resource
@Lazy // 延迟加载避免循环依赖
private BpmProcessInstanceService processInstanceService;
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
return BpmTaskCandidateStrategyEnum.START_USER_SELECT;
}
@Override
public void validateParam(String param) {}
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId());
Assert.notNull(processInstance, "流程实例({})不能为空", execution.getProcessInstanceId());
Map<String, List<Long>> startUserSelectAssignees = FlowableUtils.getStartUserSelectAssignees(processInstance);
Assert.notNull(startUserSelectAssignees, "流程实例({}) 的发起人自选审批人不能为空",
execution.getProcessInstanceId());
// 获得审批人
List<Long> assignees = startUserSelectAssignees.get(execution.getCurrentActivityId());
return new LinkedHashSet<>(assignees);
}
@Override
public boolean isParamRequired() {
return false;
}
/**
* 获得发起人自选审批人的 UserTask 列表
*
* @param bpmnModel BPMN 模型
* @return UserTask 列表
*/
public static List<UserTask> getStartUserSelectUserTaskList(BpmnModel bpmnModel) {
if (bpmnModel == null) {
return null;
}
List<UserTask> userTaskList = BpmnModelUtils.getBpmnModelElements(bpmnModel, UserTask.class);
if (CollUtil.isEmpty(userTaskList)) {
return null;
}
userTaskList.removeIf(userTask -> !Objects.equals(BpmnModelUtils.parseCandidateStrategy(userTask),
BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy()));
return userTaskList;
}
}

View File

@ -15,6 +15,12 @@ public class BpmConstants {
* @see ProcessInstance#getProcessVariables()
*/
public static final String PROCESS_INSTANCE_VARIABLE_STATUS = "PROCESS_STATUS";
/**
* 流程实例的变量 - 发起用户选择的审批人 Map
*
* @see ProcessInstance#getProcessVariables()
*/
public static final String PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES = "PROCESS_START_USER_SELECT_ASSIGNEES";
/**
* 任务的变量 - 状态

View File

@ -20,6 +20,7 @@ public enum BpmTaskCandidateStrategyEnum {
DEPT_LEADER(21, "部门的负责人"),
POST(22, "岗位"),
USER(30, "用户"),
START_USER_SELECT(35, "发起人自选"), // 申请人自己可在提交申请时选择此节点的审批人
USER_GROUP(40, "用户组"),
EXPRESSION(60, "流程表达式"), // 表达式 ExpressionManager
;

View File

@ -2,6 +2,8 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
import org.flowable.bpmn.converter.BpmnXMLConverter;
import org.flowable.bpmn.model.Process;
import org.flowable.bpmn.model.*;
@ -14,6 +16,16 @@ import java.util.*;
*/
public class BpmnModelUtils {
public static Integer parseCandidateStrategy(FlowElement userTask) {
return NumberUtils.parseInt(userTask.getAttributeValue(
BpmnModelConstants.NAMESPACE, BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY));
}
public static String parseCandidateParam(FlowElement userTask) {
return userTask.getAttributeValue(
BpmnModelConstants.NAMESPACE, BpmnModelConstants.USER_TASK_CANDIDATE_PARAM);
}
/**
* 根据节点获取入口连线
*
@ -91,6 +103,14 @@ public class BpmnModelUtils {
return converter.convertToBpmnModel(new BytesStreamSource(bpmnBytes), false, false);
}
public static String getBpmnXml(BpmnModel model) {
if (model == null) {
return null;
}
BpmnXMLConverter converter = new BpmnXMLConverter();
return new String(converter.convertToXML(model));
}
// ========== 遍历相关的方法 ==========
/**

View File

@ -12,6 +12,7 @@ import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.TaskInfo;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
@ -79,7 +80,7 @@ public class FlowableUtils {
* @param processInstance 流程实例
* @return 表单
*/
public static Map<String, Object> getProcessInstanceFormVariable(ProcessInstance processInstance) {
public static Map<String, Object> getProcessInstanceFormVariable(HistoricProcessInstance processInstance) {
Map<String, Object> formVariables = new HashMap<>(processInstance.getProcessVariables());
filterProcessInstanceFormVariable(formVariables);
return formVariables;
@ -98,6 +99,18 @@ public class FlowableUtils {
return processVariables;
}
/**
* 获得流程实例的发起用户选择的审批人 Map
*
* @param processInstance 流程实例
* @return 发起用户选择的审批人 Map
*/
@SuppressWarnings("unchecked")
public static Map<String, List<Long>> getStartUserSelectAssignees(ProcessInstance processInstance) {
return (Map<String, List<Long>>) processInstance.getProcessVariables().get(
BpmConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES);
}
// ========== Task 相关的工具方法 ==========
/**

View File

@ -33,7 +33,7 @@ public class BpmFormServiceImpl implements BpmFormService {
@Override
public Long createForm(BpmFormSaveReqVO createReqVO) {
this.vadateFields(createReqVO.getFields());
this.validateFields(createReqVO.getFields());
// 插入
BpmFormDO form = BeanUtils.toBean(createReqVO, BpmFormDO.class);
formMapper.insert(form);
@ -43,7 +43,7 @@ public class BpmFormServiceImpl implements BpmFormService {
@Override
public void updateForm(BpmFormSaveReqVO updateReqVO) {
vadateFields(updateReqVO.getFields());
validateFields(updateReqVO.getFields());
// 校验存在
validateFormExists(updateReqVO.getId());
// 更新
@ -93,7 +93,7 @@ public class BpmFormServiceImpl implements BpmFormService {
*
* @param fields field 数组
*/
private void vadateFields(List<String> fields) {
private void validateFields(List<String> fields) {
if (true) { // TODO 芋艿兼容 Vue3 工作流因为采用了新的表单设计器所以暂时不校验
return;
}

View File

@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmPro
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.Model;
import org.flowable.engine.repository.ProcessDefinition;
@ -61,12 +62,12 @@ public interface BpmProcessDefinitionService {
void updateProcessDefinitionState(String id, Integer state);
/**
* 获得流程定义对应的 BPMN XML
* 获得流程定义对应的 BPMN
*
* @param id 流程定义编号
* @return BPMN XML
* @return BPMN
*/
String getProcessDefinitionBpmnXML(String id);
BpmnModel getProcessDefinitionBpmnModel(String id);
/**
* 获得流程定义的信息
@ -89,9 +90,9 @@ public interface BpmProcessDefinitionService {
}
/**
* 获得编号对应的 ProcessDefinition
* 获得流程定义编号对应的 ProcessDefinition
*
* @param id 编号
* @param id 流程定义编号
* @return 流程定义
*/
ProcessDefinition getProcessDefinition(String id);
@ -139,7 +140,7 @@ public interface BpmProcessDefinitionService {
* @return 流程部署 Map
*/
default Map<String, Deployment> getDeploymentMap(Set<String> ids) {
return convertMap(getDeployments(ids), Deployment::getId);
return convertMap(getDeploymentList(ids), Deployment::getId);
}
/**
@ -148,7 +149,7 @@ public interface BpmProcessDefinitionService {
* @param ids 部署编号的数组
* @return 流程部署的数组
*/
List<Deployment> getDeployments(Set<String> ids);
List<Deployment> getDeploymentList(Set<String> ids);
/**
* 获得 id 对应的 Deployment

View File

@ -14,7 +14,6 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConsta
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.converter.BpmnXMLConverter;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.common.engine.impl.db.SuspensionState;
import org.flowable.engine.RepositoryService;
@ -84,7 +83,7 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
}
@Override
public List<Deployment> getDeployments(Set<String> ids) {
public List<Deployment> getDeploymentList(Set<String> ids) {
if (CollUtil.isEmpty(ids)) {
return emptyList();
}
@ -156,13 +155,8 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
}
@Override
public String getProcessDefinitionBpmnXML(String id) {
BpmnModel bpmnModel = repositoryService.getBpmnModel(id);
if (bpmnModel == null) {
return null;
}
BpmnXMLConverter converter = new BpmnXMLConverter();
return StrUtil.utf8Str(converter.convertToXML(bpmnModel));
public BpmnModel getProcessDefinitionBpmnModel(String id) {
return repositoryService.getBpmnModel(id);
}
@Override

View File

@ -56,7 +56,8 @@ public class BpmOALeaveServiceImpl implements BpmOALeaveService {
processInstanceVariables.put("day", day);
String processInstanceId = processInstanceApi.createProcessInstance(userId,
new BpmProcessInstanceCreateReqDTO().setProcessDefinitionKey(PROCESS_KEY)
.setVariables(processInstanceVariables).setBusinessKey(String.valueOf(leave.getId())));
.setVariables(processInstanceVariables).setBusinessKey(String.valueOf(leave.getId()))
.setStartUserSelectAssignees(createReqVO.getStartUserSelectAssignees()));
// 将工作流的编号更新到 OA 请假单中
leaveMapper.updateById(new BpmOALeaveDO().setId(leave.getId()).setProcessInstanceId(processInstanceId));

View File

@ -49,17 +49,6 @@ public interface BpmProcessInstanceService {
return convertMap(getProcessInstances(ids), ProcessInstance::getProcessInstanceId);
}
/**
* 获得流程实例名字 Map
*
* @param ids 流程实例的编号集合
* @return 对应的映射关系
*/
default Map<String, String> getProcessInstanceNameMap(Set<String> ids) {
return convertMap(getProcessInstances(ids),
ProcessInstance::getProcessInstanceId, ProcessInstance::getName);
}
/**
* 获得历史的流程实例
*
@ -152,14 +141,4 @@ public interface BpmProcessInstanceService {
*/
void updateProcessInstanceReject(String id, String reason);
// TODO @hai改成 getProcessInstanceAssigneesByTaskDefinitionKey(String id, String taskDefinitionKey)
/**
* 获取流程实例中取出指定流程任务提前指定的审批人
*
* @param processInstanceId 流程实例的编号
* @param taskDefinitionKey 流程任务定义的 key
* @return 审批人集合
*/
List<Long> getAssigneeByProcessInstanceIdAndTaskDefinitionKey(String processInstanceId, String taskDefinitionKey);
}

View File

@ -30,8 +30,9 @@ public class BpmTaskAssignLeaderExpressionTest extends BaseMockitoUnitTest {
private AdminUserApi adminUserApi;
@Mock
private DeptApi deptApi;
@Mock
private BpmProcessInstanceService bpmProcessInstanceService;
private BpmProcessInstanceService processInstanceService;
@Test
public void testCalculateUsers_noDept() {
@ -96,7 +97,7 @@ public class BpmTaskAssignLeaderExpressionTest extends BaseMockitoUnitTest {
// mock 返回 startUserId
ExecutionEntityImpl processInstance = new ExecutionEntityImpl();
processInstance.setStartUserId(String.valueOf(startUserId));
when(bpmProcessInstanceService.getProcessInstance(eq(execution.getProcessInstanceId())))
when(processInstanceService.getProcessInstance(eq(execution.getProcessInstanceId())))
.thenReturn(processInstance);
return execution;
}

View File

@ -1,26 +1,24 @@
package cn.iocoder.yudao.module.bpm.service.category;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.category.BpmCategoryPageReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.category.BpmCategorySaveReqVO;
import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryServiceImpl;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import jakarta.annotation.Resource;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
import cn.iocoder.yudao.module.bpm.dal.mysql.category.BpmCategoryMapper;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.bpm.service.definition.BpmCategoryServiceImpl;
import jakarta.annotation.Resource;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.Import;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.*;
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildBetweenTime;
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.buildTime;
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.cloneIgnoreId;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertPojoEquals;
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
import static cn.iocoder.yudao.framework.test.core.util.RandomUtils.*;
import static cn.iocoder.yudao.framework.common.util.date.LocalDateTimeUtils.*;
import static cn.iocoder.yudao.framework.common.util.object.ObjectUtils.*;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.CATEGORY_NOT_EXISTS;
import static org.junit.jupiter.api.Assertions.*;
/**
@ -40,7 +38,8 @@ public class BpmCategoryServiceImplTest extends BaseDbUnitTest {
@Test
public void testCreateCategory_success() {
// 准备参数
BpmCategorySaveReqVO createReqVO = randomPojo(BpmCategorySaveReqVO.class).setId(null);
BpmCategorySaveReqVO createReqVO = randomPojo(BpmCategorySaveReqVO.class).setId(null)
.setStatus(randomCommonStatus());
// 调用
Long categoryId = categoryService.createCategory(createReqVO);
@ -59,6 +58,7 @@ public class BpmCategoryServiceImplTest extends BaseDbUnitTest {
// 准备参数
BpmCategorySaveReqVO updateReqVO = randomPojo(BpmCategorySaveReqVO.class, o -> {
o.setId(dbCategory.getId()); // 设置更新的 ID
o.setStatus(randomCommonStatus());
});
// 调用
@ -101,29 +101,28 @@ public class BpmCategoryServiceImplTest extends BaseDbUnitTest {
}
@Test
@Disabled // TODO 请修改 null 为需要的值然后删除 @Disabled 注解
public void testGetCategoryPage() {
// mock 数据
BpmCategoryDO dbCategory = randomPojo(BpmCategoryDO.class, o -> { // 等会查询到
o.setName(null);
o.setCode(null);
o.setStatus(null);
o.setCreateTime(null);
o.setName("芋头");
o.setCode("xiaodun");
o.setStatus(CommonStatusEnum.ENABLE.getStatus());
o.setCreateTime(buildTime(2023, 2, 2));
});
categoryMapper.insert(dbCategory);
// 测试 name 不匹配
categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setName(null)));
categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setName("小盾")));
// 测试 code 不匹配
categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setCode(null)));
categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setCode("tudou")));
// 测试 status 不匹配
categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setStatus(null)));
categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setStatus(CommonStatusEnum.DISABLE.getStatus())));
// 测试 createTime 不匹配
categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setCreateTime(null)));
categoryMapper.insert(cloneIgnoreId(dbCategory, o -> o.setCreateTime(buildTime(2024, 2, 2))));
// 准备参数
BpmCategoryPageReqVO reqVO = new BpmCategoryPageReqVO();
reqVO.setName(null);
reqVO.setCode(null);
reqVO.setStatus(null);
reqVO.setName("");
reqVO.setCode("xiao");
reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
reqVO.setCreateTime(buildBetweenTime(2023, 2, 1, 2023, 2, 28));
// 调用

View File

@ -6,7 +6,6 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormSaveReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormPageReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.form.BpmFormUpdateReqVO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
import cn.iocoder.yudao.module.bpm.dal.mysql.definition.BpmFormMapper;
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmFormFieldRespDTO;
@ -66,7 +65,7 @@ public class BpmFormServiceTest extends BaseDbUnitTest {
});
formMapper.insert(dbForm);// @Sql: 先插入出一条存在的数据
// 准备参数
BpmFormUpdateReqVO reqVO = randomPojo(BpmFormUpdateReqVO.class, o -> {
BpmFormSaveReqVO reqVO = randomPojo(BpmFormSaveReqVO.class, o -> {
o.setId(dbForm.getId()); // 设置更新的 ID
o.setConf("{'yudao': 'yuanma'}");
o.setFields(randomFields());
@ -82,7 +81,7 @@ public class BpmFormServiceTest extends BaseDbUnitTest {
@Test
public void testUpdateForm_notExists() {
// 准备参数
BpmFormUpdateReqVO reqVO = randomPojo(BpmFormUpdateReqVO.class, o -> {
BpmFormSaveReqVO reqVO = randomPojo(BpmFormSaveReqVO.class, o -> {
o.setConf("{'yudao': 'yuanma'}");
o.setFields(randomFields());
});

View File

@ -1,2 +1,3 @@
DELETE FROM "bpm_form";
DELETE FROM "bpm_user_group";
DELETE FROM "bpm_category";

View File

@ -12,6 +12,21 @@ CREATE TABLE IF NOT EXISTS "bpm_user_group" (
PRIMARY KEY ("id")
) COMMENT '用户组';
CREATE TABLE IF NOT EXISTS "bpm_category" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"name" varchar(63) NOT NULL,
"code" varchar(63) NOT NULL,
"description" varchar(255) NOT NULL,
"status" tinyint NOT NULL,
"sort" int NOT NULL,
"creator" varchar(64) DEFAULT '',
"create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updater" varchar(64) DEFAULT '',
"update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deleted" bit NOT NULL DEFAULT FALSE,
PRIMARY KEY ("id")
) COMMENT '分类';
CREATE TABLE IF NOT EXISTS "bpm_form" (
"id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY,
"name" varchar(63) NOT NULL,