!978 BPM 仿钉钉/飞书模式的草稿 PR

Merge pull request !978 from 芋道源码/feature/bpm
This commit is contained in:
芋道源码 2024-10-04 09:11:39 +00:00 committed by Gitee
commit 76e1d0e6f7
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
97 changed files with 3822 additions and 669 deletions

View File

@ -16,7 +16,7 @@
<module>yudao-module-system</module>
<module>yudao-module-infra</module>
<!-- <module>yudao-module-member</module>-->
<!-- <module>yudao-module-bpm</module>-->
<module>yudao-module-bpm</module>
<!-- <module>yudao-module-report</module>-->
<!-- <module>yudao-module-mp</module>-->
<!-- <module>yudao-module-pay</module>-->

11
sql/mysql/bpm_update.sql Normal file
View File

@ -0,0 +1,11 @@
-- ----------------------------
-- 流程抄送表新加流程活动编号
-- ----------------------------
ALTER TABLE `pro-test`.`bpm_process_instance_copy`
ADD COLUMN `activity_id` varchar(64) NULL COMMENT '流程活动编号' AFTER `category`,
MODIFY COLUMN `task_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '任务编号' AFTER `category`;
ALTER TABLE `pro-test`.`bpm_process_definition_info`
ADD COLUMN `model_type` tinyint NOT NULL DEFAULT 10 COMMENT '流程模型的类型' AFTER `model_id`,
ADD COLUMN `simple_model` json NULL COMMENT 'SIMPLE 设计器模型数据' AFTER `form_custom_view_path`,
ADD COLUMN `visible` bit(1) NOT NULL DEFAULT 1 COMMENT '是否可见' AFTER `simple_model`;

View File

@ -59,4 +59,11 @@ public class BeanUtils {
return new PageResult<>(list, source.getTotal());
}
public static void copyProperties(Object source, Object target) {
if (source == null || target == null) {
return;
}
BeanUtil.copyProperties(source, target, false);
}
}

View File

@ -23,6 +23,7 @@ public interface ErrorCodeConstants {
"原因:用户任务({})未配置审批人,请点击【流程设计】按钮,选择该它的【任务(审批人)】进行配置");
ErrorCode MODEL_DEPLOY_FAIL_BPMN_START_EVENT_NOT_EXISTS = new ErrorCode(1_009_002_005, "部署流程失败原因BPMN 流程图中,没有开始事件");
ErrorCode MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS = new ErrorCode(1_009_002_006, "部署流程失败原因BPMN 流程图中,用户任务({})的名字不存在");
ErrorCode MODEL_UPDATE_FAIL_NOT_MANAGER = new ErrorCode(1_009_002_007, "操作流程失败,原因:你不是该流程的管理员");
// ========== 流程定义 1-009-003-000 ==========
ErrorCode PROCESS_DEFINITION_KEY_NOT_MATCH = new ErrorCode(1_009_003_000, "流程定义的标识期望是({}),当前是({}),请修改 BPMN 流程图");
@ -36,6 +37,7 @@ public interface ErrorCodeConstants {
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, "审批任务({})的审批人({})不存在");
ErrorCode PROCESS_INSTANCE_START_USER_CAN_START = new ErrorCode(1_009_004_005, "发起流程失败,你没有权限发起该流程");
// ========== 流程任务 1-009-005-000 ==========
ErrorCode TASK_OPERATE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1_009_005_001, "操作失败,原因:该任务的审批人不是你");

View File

@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.hutool.core.util.ArrayUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* BPM 边界事件 (boundary event) 自定义类型枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum BpmBoundaryEventType {
USER_TASK_TIMEOUT(1,"用户任务超时");
private final Integer type;
private final String name;
public static BpmBoundaryEventType typeOf(Integer type) {
return ArrayUtil.firstMatch(eventType -> eventType.getType().equals(type), values());
}
}

View File

@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.hutool.core.util.ArrayUtil;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* BPM 表单权限的枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum BpmFieldPermissionEnum {
READ(1, "只读"),
WRITE(2, "可编辑"),
NONE(3, "隐藏");
/**
* 权限
*/
private final Integer permission;
/**
* 名字
*/
private final String name;
public static BpmFieldPermissionEnum valueOf(Integer permission) {
return ArrayUtil.firstMatch(item -> item.getPermission().equals(permission), values());
}
}

View File

@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* BPM 模型的类型的枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum BpmModelTypeEnum implements IntArrayValuable {
BPMN(10, "BPMN 设计器"), // https://bpmn.io/toolkit/bpmn-js/
SIMPLE(20, "SIMPLE 设计器"); // 参考钉钉飞书工作流的设计器
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmModelTypeEnum::getType).toArray();
private final Integer type;
private final String name;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 仿钉钉的流程器设计器条件节点的条件类型
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum BpmSimpleModeConditionType implements IntArrayValuable {
EXPRESSION(1, "条件表达式"),
RULE(2, "条件规则");
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmSimpleModeConditionType::getType).toArray();
private final Integer type;
private final String name;
public static BpmSimpleModeConditionType valueOf(Integer type) {
return ArrayUtil.firstMatch(nodeType -> nodeType.getType().equals(type), values());
}
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,76 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
import java.util.Objects;
/**
* 仿钉钉的流程器设计器的模型节点类型
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum BpmSimpleModelNodeType implements IntArrayValuable {
// 0 ~ 1 开始和结束
START_NODE(0, "startEvent", "开始节点"),
END_NODE(1, "endEvent", "结束节点"),
// 10 ~ 49 各种节点
START_USER_NODE(10, "userTask", "发起人节点"), // 发起人节点前端的开始节点Id 固定
APPROVE_NODE(11, "userTask", "审批人节点"),
COPY_NODE(12, "serviceTask", "抄送人节点"),
// 50 ~ 条件分支
CONDITION_NODE(50, "sequenceFlow", "条件节点"), // 用于构建流转条件的表达式
CONDITION_BRANCH_NODE(51, " “parallelGateway”", "条件分支节点"), // TODO @jason是不是改成叫 条件分支
PARALLEL_BRANCH_NODE(52, "exclusiveGateway", "并行分支节点"), // TODO @jason是不是一个 并行分支 就可以啦 后面是否去掉并行网关只用包容网关
INCLUSIVE_BRANCH_NODE(53, "inclusiveGateway", "包容分支节点"),
// TODO @jason建议整合 join最终只有 条件分支并行分支包容分支三种~
// TODO @芋艿 感觉还是分开好理解一点,也好处理一点前端结构中把聚合节点显示并传过来
;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmSimpleModelNodeType::getType).toArray();
public static final String BPMN_USER_TASK_TYPE = "userTask";
private final Integer type;
private final String bpmnType;
private final String name;
/**
* 判断是否为分支节点
*
* @param type 节点类型
*/
public static boolean isBranchNode(Integer type) {
return Objects.equals(CONDITION_BRANCH_NODE.getType(), type)
|| Objects.equals(PARALLEL_BRANCH_NODE.getType(), type)
|| Objects.equals(INCLUSIVE_BRANCH_NODE.getType(), type);
}
/**
* 判断是否需要记录的节点
*
* @param bpmnType bpmn节点类型
*/
public static boolean isRecordNode(String bpmnType) {
return Objects.equals(APPROVE_NODE.getBpmnType(), bpmnType)
|| Objects.equals(END_NODE.getBpmnType(), bpmnType);
}
public static BpmSimpleModelNodeType valueOf(Integer type) {
return ArrayUtil.firstMatch(nodeType -> nodeType.getType().equals(type), values());
}
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* BPM 多人审批方式的枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum BpmUserTaskApproveMethodEnum implements IntArrayValuable {
RANDOM(1, "随机挑选一人审批"),
RATIO(2, "多人会签(按通过比例)"), // 会签按通过比例
ANY(3, "多人或签(一人通过或拒绝)"), // 或签通过只需一人拒绝只需一人
SEQUENTIAL(4, "依次审批"); // 依次审批
/**
* 审批方式
*/
private final Integer method;
/**
* 名字
*/
private final String name;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskApproveMethodEnum::getMethod).toArray();
public static BpmUserTaskApproveMethodEnum valueOf(Integer method) {
return ArrayUtil.firstMatch(item -> item.getMethod().equals(method), values());
}
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 用户任务的审批类型枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum BpmUserTaskApproveTypeEnum implements IntArrayValuable {
USER(1), // 人工审批
AUTO_APPROVE(2), // 自动通过
AUTO_REJECT(3); // 自动拒绝
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskApproveTypeEnum::getType).toArray();
private final Integer type;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

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

View File

@ -0,0 +1,31 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Arrays;
/**
* BPM 用户任务的审批人与发起人相同时处理类型枚举
*
* @author 芋道源码
*/
@RequiredArgsConstructor
@Getter
public enum BpmUserTaskAssignStartUserHandlerTypeEnum implements IntArrayValuable {
START_USER_AUDIT(1), // 由发起人对自己审批
SKIP(2), // 自动跳过参考飞书1如果当前节点还有其他审批人则交由其他审批人进行审批2如果当前节点没有其他审批人则该节点自动通过
TRANSFER_DEPT_LEADER(3); // 转交给部门负责人审批参考飞书若部门负责人为空则自动通过
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskAssignStartUserHandlerTypeEnum::getType).toArray();
private final Integer type;
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,35 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* BPM 用户任务拒绝处理类型枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum BpmUserTaskRejectHandlerType implements IntArrayValuable {
FINISH_PROCESS_INSTANCE(1, "终止流程"),
RETURN_USER_TASK(2, "驳回到指定任务节点");
private final Integer type;
private final String name;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskRejectHandlerType::getType).toArray();
public static BpmUserTaskRejectHandlerType typeOf(Integer type) {
return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
}
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* 用户任务超时处理类型枚举
*
* @author jason
*/
@Getter
@AllArgsConstructor
public enum BpmUserTaskTimeoutHandlerTypeEnum implements IntArrayValuable {
REMINDER(1,"自动提醒"),
APPROVE(2, "自动同意"),
REJECT(3, "自动拒绝");
private final Integer type;
private final String name;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmUserTaskTimeoutHandlerTypeEnum::getType).toArray();
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -14,7 +14,8 @@ public enum BpmMessageEnum {
PROCESS_INSTANCE_APPROVE("bpm_process_instance_approve"), // 流程任务被审批通过时发送给申请人
PROCESS_INSTANCE_REJECT("bpm_process_instance_reject"), // 流程任务被审批不通过时发送给申请人
TASK_ASSIGNED("bpm_task_assigned"); // 任务被分配时发送给审批人
TASK_ASSIGNED("bpm_task_assigned"), // 任务被分配时发送给审批人
TASK_TIMEOUT("bpm_task_timeout"); // 任务审批超时时发送给审批人
/**
* 短信模板的标识

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.bpm.enums.task;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -15,6 +16,7 @@ import java.util.Arrays;
@AllArgsConstructor
public enum BpmProcessInstanceStatusEnum implements IntArrayValuable {
NOT_START(-1, "未开始"),
RUNNING(1, "审批中"),
APPROVE(2, "审批通过"),
REJECT(3, "审批不通过"),
@ -36,4 +38,9 @@ public enum BpmProcessInstanceStatusEnum implements IntArrayValuable {
return ARRAYS;
}
public static boolean isProcessEndStatus(Integer status) {
return ObjectUtils.equalsAny(status,
APPROVE.getStatus(), REJECT.getStatus(), CANCEL.getStatus());
}
}

View File

@ -5,13 +5,13 @@ import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 流程实例/任务的删除原因枚举
* 流程实例/任务的的处理原因枚举
*
* @author 芋道源码
*/
@Getter
@AllArgsConstructor
public enum BpmDeleteReasonEnum {
public enum BpmReasonEnum {
// ========== 流程实例的独有原因 ==========
@ -22,6 +22,16 @@ public enum BpmDeleteReasonEnum {
// ========== 流程任务的独有原因 ==========
CANCEL_BY_SYSTEM("系统自动取消"), // 场景非常多比如说1多任务审批已经满足条件无需审批该任务2流程实例被取消无需审批该任务等等
TIMEOUT_APPROVE("审批超时,系统自动通过"),
TIMEOUT_REJECT("审批超时,系统自动不通过"),
ASSIGN_START_USER_APPROVE("审批人与提交人为同一人时,自动通过"),
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("审批人为空,自动不通过"),
APPROVE_TYPE_AUTO_APPROVE("非人工审核,自动通过"),
APPROVE_TYPE_AUTO_REJECT("非人工审核,自动不通过"),
;
private final String reason;
@ -36,10 +46,4 @@ public enum BpmDeleteReasonEnum {
return StrUtil.format(reason, args);
}
// ========== 逻辑 ==========
public static boolean isRejectReason(String reason) {
return StrUtil.startWith(reason, "审批不通过任务,原因:");
}
}

View File

@ -13,6 +13,7 @@ import lombok.Getter;
@AllArgsConstructor
public enum BpmTaskStatusEnum {
NOT_START(-1, "未开始"),
RUNNING(1, "审批中"),
APPROVE(2, "审批通过"),
REJECT(3, "审批不通过"),

View File

@ -0,0 +1,4 @@
/**
* 基础包放一些通用的 VO
*/
package cn.iocoder.yudao.module.bpm.controller.admin.base;

View File

@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.bpm.controller.admin.base.user;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Schema(description = "用户精简信息 VO")
@Data
public class UserSimpleBaseVO {
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
private String nickname;
@Schema(description = "用户头像", example = "https://www.iocoder.cn/1.png")
private String avatar;
}

View File

@ -4,10 +4,9 @@ import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.io.IoUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.*;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelUpdateReqVO;
import cn.iocoder.yudao.module.bpm.convert.definition.BpmModelConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmCategoryDO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
@ -15,7 +14,8 @@ 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.BpmModelService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@ -28,15 +28,15 @@ import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.io.IOException;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
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 = "管理后台 - 流程模型")
@RestController
@ -53,6 +53,9 @@ public class BpmModelController {
@Resource
private BpmProcessDefinitionService processDefinitionService;
@Resource
private AdminUserApi adminUserApi;
@GetMapping("/page")
@Operation(summary = "获得模型分页")
public CommonResult<PageResult<BpmModelRespVO>> getModelPage(BpmModelPageReqVO pageVO) {
@ -64,7 +67,7 @@ public class BpmModelController {
// 拼接数据
// 获得 Form 表单
Set<Long> formIds = convertSet(pageResult.getList(), model -> {
BpmModelMetaInfoRespDTO metaInfo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoRespDTO.class);
BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model);
return metaInfo != null ? metaInfo.getFormId() : null;
});
Map<Long, BpmFormDO> formMap = formService.getFormMap(formIds);
@ -78,7 +81,14 @@ public class BpmModelController {
// 获得 ProcessDefinition Map
List<ProcessDefinition> processDefinitions = processDefinitionService.getProcessDefinitionListByDeploymentIds(deploymentIds);
Map<String, ProcessDefinition> processDefinitionMap = convertMap(processDefinitions, ProcessDefinition::getDeploymentId);
return success(BpmModelConvert.INSTANCE.buildModelPage(pageResult, formMap, categoryMap, deploymentMap, processDefinitionMap));
// 获得 User Map
Set<Long> userIds = convertSetByFlatMap(pageResult.getList(), model -> {
BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model);
return metaInfo != null ? metaInfo.getStartUserIds().stream() : Stream.empty();
});
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
return success(BpmModelConvert.INSTANCE.buildModelPage(pageResult,
formMap, categoryMap, deploymentMap, processDefinitionMap, userMap));
}
@GetMapping("/get")
@ -97,34 +107,24 @@ public class BpmModelController {
@PostMapping("/create")
@Operation(summary = "新建模型")
@PreAuthorize("@ss.hasPermission('bpm:model:create')")
public CommonResult<String> createModel(@Valid @RequestBody BpmModelCreateReqVO createRetVO) {
return success(modelService.createModel(createRetVO, null));
public CommonResult<String> createModel(@Valid @RequestBody BpmModelSaveReqVO createRetVO) {
return success(modelService.createModel(createRetVO));
}
@PutMapping("/update")
@Operation(summary = "修改模型")
@PreAuthorize("@ss.hasPermission('bpm:model:update')")
public CommonResult<Boolean> updateModel(@Valid @RequestBody BpmModelUpdateReqVO modelVO) {
modelService.updateModel(modelVO);
public CommonResult<Boolean> updateModel(@Valid @RequestBody BpmModelSaveReqVO modelVO) {
modelService.updateModel(getLoginUserId(), modelVO);
return success(true);
}
@PostMapping("/import")
@Operation(summary = "导入模型")
@PreAuthorize("@ss.hasPermission('bpm:model:import')")
public CommonResult<String> importModel(@Valid BpmModeImportReqVO importReqVO) throws IOException {
BpmModelCreateReqVO createReqVO = BeanUtils.toBean(importReqVO, BpmModelCreateReqVO.class);
// 读取文件
String bpmnXml = IoUtils.readUtf8(importReqVO.getBpmnFile().getInputStream(), false);
return success(modelService.createModel(createReqVO, bpmnXml));
}
@PostMapping("/deploy")
@Operation(summary = "部署模型")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('bpm:model:deploy')")
public CommonResult<Boolean> deployModel(@RequestParam("id") String id) {
modelService.deployModel(id);
modelService.deployModel(getLoginUserId(), id);
return success(true);
}
@ -132,7 +132,15 @@ public class BpmModelController {
@Operation(summary = "修改模型的状态", description = "实际更新的部署的流程定义的状态")
@PreAuthorize("@ss.hasPermission('bpm:model:update')")
public CommonResult<Boolean> updateModelState(@Valid @RequestBody BpmModelUpdateStateReqVO reqVO) {
modelService.updateModelState(reqVO.getId(), reqVO.getState());
modelService.updateModelState(getLoginUserId(), reqVO.getId(), reqVO.getState());
return success(true);
}
@PutMapping("/update-bpmn")
@Operation(summary = "修改模型的 BPMN")
@PreAuthorize("@ss.hasPermission('bpm:model:update')")
public CommonResult<Boolean> updateModelBpmn(@Valid @RequestBody BpmModeUpdateBpmnReqVO reqVO) {
modelService.updateModelBpmnXml(reqVO.getId(), reqVO.getBpmnXml());
return success(true);
}
@ -141,8 +149,25 @@ public class BpmModelController {
@Parameter(name = "id", description = "编号", required = true, example = "1024")
@PreAuthorize("@ss.hasPermission('bpm:model:delete')")
public CommonResult<Boolean> deleteModel(@RequestParam("id") String id) {
modelService.deleteModel(id);
modelService.deleteModel(getLoginUserId(), id);
return success(true);
}
// ========== 仿钉钉/飞书的精简模型 =========
@GetMapping("/simple/get")
@Operation(summary = "获得仿钉钉流程设计模型")
@Parameter(name = "modelId", description = "流程模型编号", required = true, example = "a2c5eee0-eb6c-11ee-abf4-0c37967c420a")
public CommonResult<BpmSimpleModelNodeVO> getSimpleModel(@RequestParam("id") String modelId){
return success(modelService.getSimpleModel(modelId));
}
@PostMapping("/simple/update")
@Operation(summary = "保存仿钉钉流程设计模型")
@PreAuthorize("@ss.hasPermission('bpm:model:update')")
public CommonResult<Boolean> updateSimpleModel(@Valid @RequestBody BpmSimpleModelUpdateReqVO reqVO) {
modelService.updateSimpleModel(getLoginUserId(), reqVO);
return success(Boolean.TRUE);
}
}

View File

@ -34,6 +34,7 @@ import java.util.Map;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - 流程定义")
@RestController
@ -79,14 +80,23 @@ public class BpmProcessDefinitionController {
@Parameter(name = "suspensionState", description = "挂起状态", required = true, example = "1") // 参见 Flowable SuspensionState 枚举
public CommonResult<List<BpmProcessDefinitionRespVO>> getProcessDefinitionList(
@RequestParam("suspensionState") Integer suspensionState) {
// 1.1 获得开启的流程定义
List<ProcessDefinition> list = processDefinitionService.getProcessDefinitionListBySuspensionState(suspensionState);
if (CollUtil.isEmpty(list)) {
return success(Collections.emptyList());
}
// 获得 BpmProcessDefinitionInfoDO Map
// 1.2 移除不可见的流程定义
Map<String, BpmProcessDefinitionInfoDO> processDefinitionMap = processDefinitionService.getProcessDefinitionInfoMap(
convertSet(list, ProcessDefinition::getId));
Long userId = getLoginUserId();
list.removeIf(processDefinition -> {
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionMap.get(processDefinition.getId());
return processDefinitionInfo == null // 不存在
|| Boolean.FALSE.equals(processDefinitionInfo.getVisible()) // visible 不可见
|| !processDefinitionService.canUserStartProcessDefinition(processDefinitionInfo, userId); // 无权限发起
});
// 2. 拼接 VO 返回
return success(BpmProcessDefinitionConvert.INSTANCE.buildProcessDefinitionList(
list, null, processDefinitionMap, null, null));
}

View File

@ -1,19 +0,0 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.web.multipart.MultipartFile;
import jakarta.validation.constraints.NotNull;
@Schema(description = "管理后台 - 流程模型的导入 Request VO 相比流程模型的新建来说,只是多了一个 bpmnFile 文件")
@Data
public class BpmModeImportReqVO extends BpmModelCreateReqVO {
@Schema(description = "BPMN 文件", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "BPMN 文件不能为空")
private MultipartFile bpmnFile;
}

View File

@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
@Schema(description = "管理后台 - 流程模型的更新 BPMN XML Request VO")
@Data
public class BpmModeUpdateBpmnReqVO {
@Schema(description = "流程编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotEmpty(message = "流程编号不能为空")
private String id;
@Schema(description = "BPMN XML", requiredMode = Schema.RequiredMode.REQUIRED)
@NotEmpty(message = "BPMN XML 不能为空")
private String bpmnXml;
}

View File

@ -0,0 +1,62 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
import java.util.List;
/**
* BPM 流程 MetaInfo Response DTO
* 主要用于 { Model#setMetaInfo(String)} 的存储
*
* 最终它的字段和 {@link cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO} 是一致的
*
* @author 芋道源码
*/
@Data
public class BpmModelMetaInfoVO {
@Schema(description = "流程图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao.jpg")
@NotEmpty(message = "流程图标不能为空")
@URL(message = "流程图标格式不正确")
private String icon;
@Schema(description = "流程描述", example = "我是描述")
private String description;
@Schema(description = "流程类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@InEnum(BpmModelTypeEnum.class)
@NotNull(message = "流程类型不能为空")
private Integer type;
@Schema(description = "表单类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
@InEnum(BpmModelFormTypeEnum.class)
@NotNull(message = "表单类型不能为空")
private Integer formType;
@Schema(description = "表单编号", example = "1024")
private Long formId; // formType NORMAL 使用必须非空
@Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址",
example = "/bpm/oa/leave/create")
private String formCustomCreatePath; // 表单类型为 CUSTOM 必须非空
@Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址",
example = "/bpm/oa/leave/view")
private String formCustomViewPath; // 表单类型为 CUSTOM 必须非空
@Schema(description = "是否可见", requiredMode = Schema.RequiredMode.REQUIRED, example = "true")
@NotNull(message = "是否可见不能为空")
private Boolean visible;
@Schema(description = "可发起用户编号数组", example = "[1,2,3]")
private List<Long> startUserIds;
@Schema(description = "可管理用户编号数组", requiredMode = Schema.RequiredMode.REQUIRED, example = "[2,4,6]")
@NotEmpty(message = "可管理用户编号数组不能为空")
private List<Long> managerUserIds;
}

View File

@ -1,14 +1,16 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO;
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 BpmModelRespVO {
public class BpmModelRespVO extends BpmModelMetaInfoVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
private String id;
@ -22,33 +24,23 @@ public class BpmModelRespVO {
@Schema(description = "流程图标", 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 = "表单类型-参见 bpm_model_form_type 数据字典", example = "1")
private Integer formType;
@Schema(description = "表单编号", example = "1024")
private Long formId; // 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 必须非空
@Schema(description = "表单名字", example = "请假表单")
private String formName;
@Schema(description = "自定义表单的提交路径", example = "/bpm/oa/leave/create")
private String formCustomCreatePath; // 使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 必须非空
@Schema(description = "自定义表单的查看路径", example = "/bpm/oa/leave/view")
private String formCustomViewPath; // 使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 必须非空
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;
@Schema(description = "BPMN XML", requiredMode = Schema.RequiredMode.REQUIRED)
private String bpmnXml;
@Schema(description = "可发起的用户数组")
private List<UserSimpleBaseVO> startUsers;
/**
* 最新部署的流程定义
*/

View File

@ -1,15 +1,15 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
@Schema(description = "管理后台 - 流程模型的创建 Request VO")
@Schema(description = "管理后台 - 流程模型的保存 Request VO")
@Data
public class BpmModelCreateReqVO {
public class BpmModelSaveReqVO extends BpmModelMetaInfoVO {
@Schema(description = "编号", example = "1024")
private String id;
@Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "process_yudao")
@NotEmpty(message = "流程标识不能为空")
@ -19,7 +19,7 @@ public class BpmModelCreateReqVO {
@NotEmpty(message = "流程名称不能为空")
private String name;
@Schema(description = "流程描述", example = "我是描述")
private String description;
@Schema(description = "流程分类", example = "1")
private String category;
}

View File

@ -1,46 +0,0 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import org.hibernate.validator.constraints.URL;
@Schema(description = "管理后台 - 流程模型的更新 Request VO")
@Data
public class BpmModelUpdateReqVO {
@Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
@NotEmpty(message = "编号不能为空")
private String id;
@Schema(description = "流程名称", example = "芋道")
private String name;
@Schema(description = "流程图标", example = "https://www.iocoder.cn/yudao.jpg")
@URL(message = "流程图标格式不正确")
private String icon;
@Schema(description = "流程描述", example = "我是描述")
private String description;
@Schema(description = "流程分类", example = "1")
private String category;
@Schema(description = "BPMN XML", requiredMode = Schema.RequiredMode.REQUIRED)
private String bpmnXml;
@Schema(description = "表单类型-参见 bpm_model_form_type 数据字典", example = "1")
@InEnum(BpmModelFormTypeEnum.class)
private Integer formType;
@Schema(description = "表单编号-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", example = "1024")
private Long formId;
@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;
}

View File

@ -0,0 +1,220 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.*;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Schema(description = "管理后台 - 仿钉钉流程设计模型节点 VO")
@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class BpmSimpleModelNodeVO {
@Schema(description = "模型节点编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "StartEvent_1")
@NotEmpty(message = "模型节点编号不能为空")
private String id;
@Schema(description = "模型节点类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "模型节点类型不能为空")
@InEnum(BpmSimpleModelNodeType.class)
private Integer type;
@Schema(description = "模型节点名称", example = "领导审批")
private String name;
// TODO @jason gpt 大模型对了下这个字段的命名貌似叫 displayText 合适点可以等最后我们全局替换下优先级
@Schema(description = "节点展示内容", example = "指定成员: 芋道源码")
private String showText;
@Schema(description = "子节点")
private BpmSimpleModelNodeVO childNode; // 补充说明在该模型下子节点有且仅有一个不会有多个
@Schema(description = "条件节点")
private List<BpmSimpleModelNodeVO> conditionNodes; // 补充说明有且仅有条件并行包容等分支会使用
@Schema(description = "条件类型", example = "1")
@InEnum(BpmSimpleModeConditionType.class)
private Integer conditionType; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE
@Schema(description = "条件表达式", example = "${day>3}")
private String conditionExpression; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE
@Schema(description = "是否默认条件", example = "true")
private Boolean defaultFlow; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE
/**
* 条件组
*/
private ConditionGroups conditionGroups; // 仅用于条件节点 BpmSimpleModelNodeType.CONDITION_NODE
@Schema(description = "候选人策略", example = "30")
@InEnum(BpmTaskCandidateStrategyEnum.class)
private Integer candidateStrategy; // 用于审批抄送节点
@Schema(description = "候选人参数")
private String candidateParam; // 用于审批抄送节点
@Schema(description = "审批节点类型", example = "1")
@InEnum(BpmUserTaskApproveTypeEnum.class)
private Integer approveType; // 用于审批节点
@Schema(description = "多人审批方式", example = "1")
@InEnum(BpmUserTaskApproveMethodEnum.class)
private Integer approveMethod; // 用于审批节点
@Schema(description = "通过比例", example = "100")
private Integer approveRatio; // 通过比例当多人审批方式为多人会签(按通过比例) 需要设置
@Schema(description = "表单权限", example = "[]")
private List<Map<String, String>> fieldsPermission;
@Schema(description = "操作按钮设置", example = "[]")
private List<OperationButtonSetting> buttonsSetting; // 用于审批节点
// TODO @jason看看是不是可以简化@芋艿 暂时先放着不知道后面是否会用到
/**
* 附加节点 Id, 该节点不从前端传入 由程序生成. 由于当个节点无法完成功能 需要附加节点来完成
*/
@JsonIgnore
private String attachNodeId;
/**
* 审批节点拒绝处理
*/
private RejectHandler rejectHandler;
/**
* 审批节点超时处理
*/
private TimeoutHandler timeoutHandler;
@Schema(description = "审批节点的审批人与发起人相同时,对应的处理类型", example = "1")
@InEnum(BpmUserTaskAssignStartUserHandlerTypeEnum.class)
private Integer assignStartUserHandlerType;
/**
* 空处理策略
*/
private AssignEmptyHandler assignEmptyHandler;
@Schema(description = "审批节点拒绝处理策略")
@Data
public static class RejectHandler {
@Schema(description = "拒绝处理类型", example = "1")
@InEnum(BpmUserTaskRejectHandlerType.class)
private Integer type;
@Schema(description = "任务拒绝后驳回的节点 Id", example = "Activity_1")
private String returnNodeId;
}
@Schema(description = "审批节点超时处理策略")
@Valid
@Data
public static class TimeoutHandler {
@Schema(description = "是否开启超时处理", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
@NotNull(message = "是否开启超时处理不能为空")
private Boolean enable;
@Schema(description = "任务超时未处理的行为", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "任务超时未处理的行为不能为空")
@InEnum(BpmUserTaskTimeoutHandlerTypeEnum.class)
private Integer type;
@Schema(description = "超时时间", requiredMode = Schema.RequiredMode.REQUIRED, example = "PT6H")
@NotEmpty(message = "超时时间不能为空")
private String timeDuration;
@Schema(description = "最大提醒次数", example = "1")
private Integer maxRemindCount;
}
@Schema(description = "空处理策略")
@Data
@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;
}
@Schema(description = "操作按钮设置")
@Data
@Valid
public static class OperationButtonSetting {
// TODO @jason是不是按钮的标识id 会和数据库的 id 自增有点模糊key 标识会更合理一点点哈
@Schema(description = "按钮 Id", example = "1")
private Integer id;
@Schema(description = "显示名称", example = "审批")
private String displayName;
@Schema(description = "是否启用", example = "true")
private Boolean enable;
}
@Schema(description = "条件组")
@Data
@Valid
public static class ConditionGroups {
@Schema(description = "条件组下的条件关系是否为与关系", example = "true")
@NotNull(message = "条件关系不能为空")
private Boolean and;
@Schema(description = "条件组下的条件", example = "[]")
@NotEmpty(message = "条件不能为空")
private List<Condition> conditions;
}
@Schema(description = "条件")
@Data
@Valid
public static class Condition {
@Schema(description = "条件下的规则关系是否为与关系", example = "true")
@NotNull(message = "规则关系不能为空")
private Boolean and;
@Schema(description = "条件下的规则", example = "[]")
@NotEmpty(message = "规则不能为空")
private List<ConditionRule> rules;
}
@Schema(description = "条件规则")
@Data
@Valid
public static class ConditionRule {
@Schema(description = "运行符号", example = "==")
@NotEmpty(message = "运行符号不能为空")
private String opCode;
@Schema(description = "运算符左边的值,例如某个流程变量", example = "startUserId")
@NotEmpty(message = "运算符左边的值不能为空")
private String leftSide;
@Schema(description = "运算符右边的值", example = "1")
@NotEmpty(message = "运算符右边的值不能为空")
private String rightSide;
}
// TODO @芋艿条件建议可以固化的一些选项然后有个表达式兜底要支持
}

View File

@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
// TODO @jason需要考虑如果某个节点的配置不正确需要有提示具体怎么实现可以讨论下
@Schema(description = "管理后台 - 仿钉钉流程设计模型的新增/修改 Request VO")
@Data
public class BpmSimpleModelUpdateReqVO {
@Schema(description = "流程模型编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotEmpty(message = "流程模型编号不能为空")
private String id; // 对应 Flowable act_re_model ID_ 字段
@Schema(description = "仿钉钉流程设计模型对象", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "仿钉钉流程设计模型对象不能为空")
@Valid
private BpmSimpleModelNodeVO simpleModel;
}

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.activity.BpmActivityRespVO;
import cn.iocoder.yudao.module.bpm.convert.task.BpmActivityConvert;
import cn.iocoder.yudao.module.bpm.service.task.BpmActivityService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Parameter;
@ -34,6 +35,6 @@ public class BpmActivityController {
@PreAuthorize("@ss.hasPermission('bpm:task:query')")
public CommonResult<List<BpmActivityRespVO>> getActivityList(
@RequestParam("processInstanceId") String processInstanceId) {
return success(activityService.getActivityListByProcessInstanceId(processInstanceId));
return success(BpmActivityConvert.INSTANCE.convertList(activityService.getActivityListByProcessInstanceId(processInstanceId)));
}
}

View File

@ -4,10 +4,7 @@ import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCancelReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCreateReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstancePageReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceRespVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
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;
@ -160,4 +157,20 @@ public class BpmProcessInstanceController {
return success(true);
}
@GetMapping("/get-form-fields-permission")
@Operation(summary = "获得表单字段权限")
@PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
public CommonResult<Map<String, String>> getFormFieldsPermission(
@Valid BpmFormFieldsPermissionReqVO reqVO) {
return success(processInstanceService.getFormFieldsPermission(reqVO));
}
@GetMapping("/get-approval-detail")
@Operation(summary = "获得审批详情")
@Parameter(name = "id", description = "流程实例的编号", required = true)
@PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
public CommonResult<BpmApprovalDetailRespVO> getApprovalDetail(@Valid BpmApprovalDetailReqVO reqVO) {
return success(processInstanceService.getApprovalDetail(getLoginUserId(), reqVO));
}
}

View File

@ -11,7 +11,6 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessI
import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
@ -43,8 +42,6 @@ public class BpmProcessInstanceCopyController {
private BpmProcessInstanceCopyService processInstanceCopyService;
@Resource
private BpmProcessInstanceService processInstanceService;
@Resource
private BpmTaskService taskService;
@Resource
private AdminUserApi adminUserApi;
@ -61,8 +58,6 @@ public class BpmProcessInstanceCopyController {
}
// 拼接返回
Map<String, String> taskNameMap = taskService.getTaskNameByTaskIds(
convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getTaskId));
Map<String, HistoricProcessInstance> processInstanceMap = processInstanceService.getHistoricProcessInstanceMap(
convertSet(pageResult.getList(), BpmProcessInstanceCopyDO::getProcessInstanceId));
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(convertListByFlatMap(pageResult.getList(),
@ -70,7 +65,6 @@ public class BpmProcessInstanceCopyController {
return success(BeanUtils.toBean(pageResult, BpmProcessInstanceCopyRespVO.class, copyVO -> {
MapUtils.findAndThen(userMap, Long.valueOf(copyVO.getCreator()), user -> copyVO.setCreatorName(user.getNickname()));
MapUtils.findAndThen(userMap, copyVO.getStartUserId(), user -> copyVO.setStartUserName(user.getNickname()));
MapUtils.findAndThen(taskNameMap, copyVO.getTaskId(), copyVO::setTaskName);
MapUtils.findAndThen(processInstanceMap, copyVO.getProcessInstanceId(),
processInstance -> copyVO.setProcessInstanceStartTime(DateUtils.of(processInstance.getStartTime())));
}));

View File

@ -8,6 +8,7 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*;
import cn.iocoder.yudao.module.bpm.convert.task.BpmTaskConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
import cn.iocoder.yudao.module.bpm.service.definition.BpmFormService;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
@ -19,6 +20,7 @@ import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.runtime.ProcessInstance;
@ -50,6 +52,8 @@ public class BpmTaskController {
private BpmProcessInstanceService processInstanceService;
@Resource
private BpmFormService formService;
@Resource
private BpmProcessDefinitionService bpmProcessDefinitionService;
@Resource
private AdminUserApi adminUserApi;
@ -134,8 +138,10 @@ public class BpmTaskController {
// 获得 Form Map
Map<Long, BpmFormDO> formMap = formService.getFormMap(
convertSet(taskList, task -> NumberUtils.parseLong(task.getFormKey())));
// 获得 BpmnModel
BpmnModel bpmnModel = bpmProcessDefinitionService.getProcessDefinitionBpmnModel(processInstance.getProcessDefinitionId());
return success(BpmTaskConvert.INSTANCE.buildTaskListByProcessInstanceId(taskList, processInstance,
formMap, userMap, deptMap));
formMap, userMap, deptMap, bpmnModel));
}
@PutMapping("/approve")

View File

@ -24,6 +24,8 @@ public class BpmProcessInstanceCopyRespVO {
@Schema(description = "流程实例的发起时间")
private LocalDateTime processInstanceStartTime;
@Schema(description = "抄送的节点的活动编号")
private String activityId;
@Schema(description = "发起抄送的任务编号")
private String taskId;
@Schema(description = "发起抄送的任务名称")

View File

@ -0,0 +1,26 @@
package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.AssertTrue;
import lombok.Data;
// TODO @jason这个可以简化下使用 @RequestParam嘿嘿主要 VO 项不要太多
@Schema(description = "管理后台 - 审批详情 Request VO")
@Data
public class BpmApprovalDetailReqVO {
@Schema(description = "流程定义的编号", example = "1024")
private String processDefinitionId;
@Schema(description = "流程实例的编号", example = "1024")
private String processInstanceId;
@AssertTrue(message = "流程定义的编号和流程实例的编号不能同时为空")
@JsonIgnore
public boolean isValidProcessParam() {
return StrUtil.isNotEmpty(processDefinitionId) || StrUtil.isNotEmpty(processInstanceId);
}
}

View File

@ -0,0 +1,87 @@
package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
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 BpmApprovalDetailRespVO {
@Schema(description = "流程实例的状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer status; // 参见 BpmProcessInstanceStatusEnum 枚举
@Schema(description = "审批信息列表", requiredMode = Schema.RequiredMode.REQUIRED)
private List<ApprovalNodeInfo> approveNodes;
@Schema(description = "审批节点信息")
@Data
public static class ApprovalNodeInfo {
@Schema(description = "节点编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "StartUserNode")
private String id;
@Schema(description = "节点名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "发起人")
private String name;
@Schema(description = "节点类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer nodeType; // 参见 BpmSimpleModelNodeType 枚举
@Schema(description = "节点状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "0")
private Integer status; // 参见 BpmTaskStatusEnum 枚举
@Schema(description = "节点的开始时间")
private LocalDateTime startTime;
@Schema(description = "节点的结束时间")
private LocalDateTime endTime;
@Schema(description = "审批节点的任务信息")
private List<ApprovalTaskInfo> tasks;
@Schema(description = "候选人用户列表")
// TODO @jasoncandidateUserList => candidateUsers保持和 tasks 的命名风格一致哈
private List<User> candidateUserList; // 用于未运行任务节点
}
// TODO @jason可以替换成 UserSimpleBaseVO简化下
@Schema(description = "用户信息")
@Data
public static class User {
@Schema(description = "用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Long id;
@Schema(description = "用户昵称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋艿")
private String nickname;
@Schema(description = "用户头像", example = "https://www.iocoder.cn/1.png")
private String avatar;
}
@Schema(description = "审批任务信息")
@Data
public static class ApprovalTaskInfo {
@Schema(description = "任务编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private String id;
@Schema(description = "任务所属人", example = "1024")
private User ownerUser;
@Schema(description = "任务分配人", example = "2048")
private User assigneeUser;
@Schema(description = "任务状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
private Integer status; // 参见 BpmTaskStatusEnum 枚举
@Schema(description = "审批意见", example = "同意")
private String reason;
}
}

View File

@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance;
import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.AssertTrue;
import lombok.Data;
@Schema(description = "管理后台 - 表单字段权限 Request VO")
@Data
public class BpmFormFieldsPermissionReqVO {
@Schema(description = "流程定义的编号", example = "1024")
private String processDefinitionId;
@Schema(description = "流程实例的编号", example = "1024")
private String processInstanceId;
@Schema(description = "流程活动编号", example = "StartUserNode")
private String activityId; // 对应 BPMN XML 节点 Id
@Schema(description = "流程任务编号", example = "95f2f08b-621b-11ef-bf39-00ff4722db8b")
private String taskId; // UserTask 对应的Id
@AssertTrue(message = "流程定义的编号和流程实例的编号不能同时为空")
@JsonIgnore
public boolean isValidProcessParam() {
return StrUtil.isNotEmpty(processDefinitionId) || StrUtil.isNotEmpty(processInstanceId);
}
@AssertTrue(message = "流程活动编号和流程任务编号编号不能同时为空")
@JsonIgnore
public boolean isValidActivityParam() {
return StrUtil.isNotEmpty(activityId) || StrUtil.isNotEmpty(taskId);
}
}

View File

@ -61,12 +61,17 @@ public class BpmTaskRespVO {
private Long formId;
@Schema(description = "表单名字", example = "请假表单")
private String formName;
@Schema(description = "表单的配置-JSON 字符串")
@Schema(description = "表单的配置JSON 字符串")
private String formConf;
@Schema(description = "表单项的数组")
private List<String> formFields;
@Schema(description = "提交的表单值", requiredMode = Schema.RequiredMode.REQUIRED)
private Map<String, Object> formVariables;
// @芋艿 都改成了 fieldsPermission buttonsSetting BpmSimpleModelNodeVO 统一
@Schema(description = "表单字段权限值")
private Map<String, String> fieldsPermission;
@Schema(description = "操作按钮设置值")
private Map<Integer, OperationButtonSetting> buttonsSetting;
@Data
@Schema(description = "流程实例")
@ -91,4 +96,15 @@ public class BpmTaskRespVO {
}
@Data
@Schema(description = "操作按钮设置")
public static class OperationButtonSetting {
@Schema(description = "显示名称", example = "审批")
private String displayName;
@Schema(description = "是否启用", example = "true")
private Boolean enable;
}
}

View File

@ -1,20 +1,19 @@
package cn.iocoder.yudao.module.bpm.convert.definition;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelCreateReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelRespVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelUpdateReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO;
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.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import org.flowable.common.engine.impl.db.SuspensionState;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.Model;
@ -22,9 +21,11 @@ import org.flowable.engine.repository.ProcessDefinition;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
/**
* 流程模型 Convert
@ -39,22 +40,24 @@ public interface BpmModelConvert {
default PageResult<BpmModelRespVO> buildModelPage(PageResult<Model> pageResult,
Map<Long, BpmFormDO> formMap,
Map<String, BpmCategoryDO> categoryMap, Map<String, Deployment> deploymentMap,
Map<String, ProcessDefinition> processDefinitionMap) {
List<BpmModelRespVO> list = CollectionUtils.convertList(pageResult.getList(), model -> {
BpmModelMetaInfoRespDTO metaInfo = buildMetaInfo(model);
Map<String, ProcessDefinition> processDefinitionMap,
Map<Long, AdminUserRespDTO> userMap) {
List<BpmModelRespVO> list = convertList(pageResult.getList(), model -> {
BpmModelMetaInfoVO metaInfo = parseMetaInfo(model);
BpmFormDO form = metaInfo != null ? formMap.get(metaInfo.getFormId()) : null;
BpmCategoryDO category = categoryMap.get(model.getCategory());
Deployment deployment = model.getDeploymentId() != null ? deploymentMap.get(model.getDeploymentId()) : null;
ProcessDefinition processDefinition = model.getDeploymentId() != null ? processDefinitionMap.get(model.getDeploymentId()) : null;
return buildModel0(model, metaInfo, form, category, deployment, processDefinition);
List<AdminUserRespDTO> startUsers = metaInfo != null ? convertList(metaInfo.getStartUserIds(), userMap::get) : null;
return buildModel0(model, metaInfo, form, category, deployment, processDefinition, startUsers);
});
return new PageResult<>(list, pageResult.getTotal());
}
default BpmModelRespVO buildModel(Model model,
byte[] bpmnBytes) {
BpmModelMetaInfoRespDTO metaInfo = buildMetaInfo(model);
BpmModelRespVO modelVO = buildModel0(model, metaInfo, null, null, null, null);
BpmModelMetaInfoVO metaInfo = parseMetaInfo(model);
BpmModelRespVO modelVO = buildModel0(model, metaInfo, null, null, null, null, null);
if (ArrayUtil.isNotEmpty(bpmnBytes)) {
modelVO.setBpmnXml(BpmnModelUtils.getBpmnXml(bpmnBytes));
}
@ -62,20 +65,16 @@ public interface BpmModelConvert {
}
default BpmModelRespVO buildModel0(Model model,
BpmModelMetaInfoRespDTO metaInfo, BpmFormDO form, BpmCategoryDO category,
Deployment deployment, ProcessDefinition processDefinition) {
BpmModelMetaInfoVO metaInfo, BpmFormDO form, BpmCategoryDO category,
Deployment deployment, ProcessDefinition processDefinition,
List<AdminUserRespDTO> startUsers) {
BpmModelRespVO modelRespVO = new BpmModelRespVO().setId(model.getId()).setName(model.getName())
.setKey(model.getKey()).setCategory(model.getCategory())
.setCreateTime(DateUtils.of(model.getCreateTime()));
// Form
if (metaInfo != null) {
modelRespVO.setFormType(metaInfo.getFormType()).setFormId(metaInfo.getFormId())
.setFormCustomCreatePath(metaInfo.getFormCustomCreatePath())
.setFormCustomViewPath(metaInfo.getFormCustomViewPath());
modelRespVO.setIcon(metaInfo.getIcon()).setDescription(metaInfo.getDescription());
}
BeanUtils.copyProperties(metaInfo, modelRespVO);
if (form != null) {
modelRespVO.setFormId(form.getId()).setFormName(form.getName());
modelRespVO.setFormName(form.getName());
}
// Category
if (category != null) {
@ -90,49 +89,30 @@ public interface BpmModelConvert {
modelRespVO.getProcessDefinition().setDeploymentTime(DateUtils.of(deployment.getDeploymentTime()));
}
}
// User
modelRespVO.setStartUsers(BeanUtils.toBean(startUsers, UserSimpleBaseVO.class));
return modelRespVO;
}
default void copyToCreateModel(Model model, BpmModelCreateReqVO bean) {
model.setName(bean.getName());
model.setKey(bean.getKey());
model.setMetaInfo(buildMetaInfoStr(null,
null, bean.getDescription(),
null, null, null, null));
default void copyToModel(Model model, BpmModelSaveReqVO reqVO) {
model.setName(reqVO.getName());
model.setKey(reqVO.getKey());
model.setCategory(reqVO.getCategory());
model.setMetaInfo(JsonUtils.toJsonString(BeanUtils.toBean(reqVO, BpmModelMetaInfoVO.class)));
}
default void copyToUpdateModel(Model model, BpmModelUpdateReqVO bean) {
model.setName(bean.getName());
model.setCategory(bean.getCategory());
model.setMetaInfo(buildMetaInfoStr(buildMetaInfo(model),
bean.getIcon(), bean.getDescription(),
bean.getFormType(), bean.getFormId(), bean.getFormCustomCreatePath(), bean.getFormCustomViewPath()));
default BpmModelMetaInfoVO parseMetaInfo(Model model) {
BpmModelMetaInfoVO vo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoVO.class);
if (vo == null) {
return null;
}
default String buildMetaInfoStr(BpmModelMetaInfoRespDTO metaInfo,
String icon, String description,
Integer formType, Long formId, String formCustomCreatePath, String formCustomViewPath) {
if (metaInfo == null) {
metaInfo = new BpmModelMetaInfoRespDTO();
if (vo.getManagerUserIds() == null) {
vo.setManagerUserIds(Collections.emptyList());
}
// 只有非空才进行设置避免更新时的覆盖
if (StrUtil.isNotEmpty(icon)) {
metaInfo.setIcon(icon);
if (vo.getStartUserIds() == null) {
vo.setStartUserIds(Collections.emptyList());
}
if (StrUtil.isNotEmpty(description)) {
metaInfo.setDescription(description);
}
if (Objects.nonNull(formType)) {
metaInfo.setFormType(formType);
metaInfo.setFormId(formId);
metaInfo.setFormCustomCreatePath(formCustomCreatePath);
metaInfo.setFormCustomViewPath(formCustomViewPath);
}
return JsonUtils.toJsonString(metaInfo);
}
default BpmModelMetaInfoRespDTO buildMetaInfo(Model model) {
return JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoRespDTO.class);
return vo;
}
}

View File

@ -89,11 +89,6 @@ public interface BpmProcessInstanceConvert {
@Mapping(source = "from.id", target = "to.id", ignore = true)
void copyTo(BpmProcessDefinitionInfoDO from, @MappingTarget BpmProcessDefinitionRespVO to);
default BpmProcessInstanceStatusEvent buildProcessInstanceStatusEvent(Object source, HistoricProcessInstance instance, Integer status) {
return new BpmProcessInstanceStatusEvent(source).setId(instance.getId()).setStatus(status)
.setProcessDefinitionKey(instance.getProcessDefinitionKey()).setBusinessKey(instance.getBusinessKey());
}
default BpmProcessInstanceStatusEvent buildProcessInstanceStatusEvent(Object source, ProcessInstance instance, Integer status) {;
return new BpmProcessInstanceStatusEvent(source).setId(instance.getId()).setStatus(status)
.setProcessDefinitionKey(instance.getProcessDefinitionKey()).setBusinessKey(instance.getBusinessKey());

View File

@ -9,10 +9,13 @@ import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceRespVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.BpmTaskRespVO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
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.message.dto.BpmMessageSendWhenTaskCreatedReqDTO;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.Task;
@ -81,10 +84,12 @@ public interface BpmTaskConvert {
HistoricProcessInstance processInstance,
Map<Long, BpmFormDO> formMap,
Map<Long, AdminUserRespDTO> userMap,
Map<Long, DeptRespDTO> deptMap) {
Map<Long, DeptRespDTO> deptMap,
BpmnModel bpmnModel) {
List<BpmTaskRespVO> taskVOList = CollectionUtils.convertList(taskList, task -> {
BpmTaskRespVO taskVO = BeanUtils.toBean(task, BpmTaskRespVO.class);
taskVO.setStatus(FlowableUtils.getTaskStatus(task)).setReason(FlowableUtils.getTaskReason(task));
Integer taskStatus = FlowableUtils.getTaskStatus(task);
taskVO.setStatus(taskStatus).setReason(FlowableUtils.getTaskReason(task));
// 流程实例
AdminUserRespDTO startUser = userMap.get(NumberUtils.parseLong(processInstance.getStartUserId()));
taskVO.setProcessInstance(BeanUtils.toBean(processInstance, BpmTaskRespVO.ProcessInstance.class));
@ -106,6 +111,14 @@ public interface BpmTaskConvert {
taskVO.setOwnerUser(BeanUtils.toBean(ownerUser, BpmProcessInstanceRespVO.User.class));
findAndThen(deptMap, ownerUser.getDeptId(), dept -> taskVO.getOwnerUser().setDeptName(dept.getName()));
}
if (BpmTaskStatusEnum.RUNNING.getStatus().equals(taskStatus)){
// 设置表单权限 TODO @芋艿 是不是还要加一个全局的权限 基于 processInstance 的权限回复可能不需要但是发起人需要有个权限配置
// TODO @jason貌似这么返回主要解决当前审批 task 的表单权限但是不同抄送人的表单权限可能不太对例如说 A 抄送人是隐藏某个字段
// @芋艿 表单权限需要分离开单独的接口来获取了 BpmProcessInstanceService.getProcessInstanceFormFieldsPermission
taskVO.setFieldsPermission(BpmnModelUtils.parseFormFieldsPermission(bpmnModel, task.getTaskDefinitionKey()));
// 操作按钮设置
taskVO.setButtonsSetting(BpmnModelUtils.parseButtonsSetting(bpmnModel, task.getTaskDefinitionKey()));
}
return taskVO;
});
@ -151,7 +164,7 @@ public interface BpmTaskConvert {
/**
* 将父任务的属性拷贝到子任务加签任务
*
* <p>
* 为什么不使用 mapstruct 映射因为 TaskEntityImpl 还有很多其他属性这里我们只设置我们需要的
* 使用 mapstruct 会将里面嵌套的各个属性值都设置进去会出现意想不到的问题
*
@ -165,7 +178,6 @@ public interface BpmTaskConvert {
childTask.setParentTaskId(parentTask.getId());
childTask.setProcessDefinitionId(parentTask.getProcessDefinitionId());
childTask.setProcessInstanceId(parentTask.getProcessInstanceId());
// childTask.setExecutionId(parentTask.getExecutionId()); // TODO 芋艿新加的不太确定尴尬不加时子任务不通过会失败报错加了子任务审批通过会失败报错
childTask.setTaskDefinitionKey(parentTask.getTaskDefinitionKey());
childTask.setTaskDefinitionId(parentTask.getTaskDefinitionId());
childTask.setPriority(parentTask.getPriority());

View File

@ -1,12 +1,20 @@
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.StringListTypeHandler;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import lombok.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.flowable.engine.repository.Model;
import org.flowable.engine.repository.ProcessDefinition;
import java.util.List;
@ -31,15 +39,21 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
/**
* 流程定义的编号
*
* 关联 ProcessDefinition id 属性
* 关联 {@link ProcessDefinition#getId()} 属性
*/
private String processDefinitionId;
/**
* 流程模型的编号
*
* 关联 Model id 属性
* 关联 {@link Model#getId()} 属性
*/
private String modelId;
/**
* 流程模型的类型
*
* 枚举 {@link BpmModelTypeEnum}
*/
private Integer modelType;
/**
* 图标
@ -53,11 +67,12 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
/**
* 表单类型
*
* 关联 {@link BpmModelFormTypeEnum}
* 枚举 {@link BpmModelFormTypeEnum}
*/
private Integer formType;
/**
* 动态表单编号
*
* 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL}
*
* 关联 {@link BpmFormDO#getId()}
@ -65,6 +80,7 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
private Long formId;
/**
* 表单的配置
*
* 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL}
*
* 冗余 {@link BpmFormDO#getConf()}
@ -72,21 +88,59 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
private String formConf;
/**
* 表单项的数组
*
* 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL}
*
* 冗余 {@link BpmFormDO#getFields()} ()}
* 冗余 {@link BpmFormDO#getFields()}
*/
@TableField(typeHandler = JacksonTypeHandler.class)
private List<String> formFields;
/**
* 自定义表单的提交路径使用 Vue 的路由地址
*
* 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM}
*/
private String formCustomCreatePath;
/**
* 自定义表单的查看路径使用 Vue 的路由地址
*
* 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM}
*/
private String formCustomViewPath;
/**
* SIMPLE 设计器模型数据 json 格式
*
* 目的当使用仿钉钉设计器时流程模型发布的时候需要保存流程模型设计器的快照数据
*/
private String simpleModel;
/**
* 是否可见
*
* 目的如果 false 不可见则不展示在发起流程的列表里
*/
private Boolean visible;
/**
* 可发起用户编号数组
*
* 关联 {@link AdminUserRespDTO#getId()} 字段的数组
*
* 如果为空则表示全部可以发起
*
* 它和 {@link #visible} 的区别在于
* 1. {@link #visible} 只是决定是否可见即使不可见还是可以发起
* 2. startUserIds 决定某个用户是否可以发起如果该用户不可发起则他也是不可见的
*/
@TableField(typeHandler = StringListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤
private List<Long> startUserIds;
/**
* 可管理用户编号数组
*
* 关联 {@link AdminUserRespDTO#getId()} 字段的数组
*/
@TableField(typeHandler = StringListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤
private List<Long> managerUserIds;
}

View File

@ -48,10 +48,15 @@ public class BpmProcessInstanceCopyDO extends BaseDO {
* 冗余 ProcessInstance category 字段
*/
private String category;
/**
* 流程活动编号
* <p/>
* 对应 BPMN XML 节点编号用于查询抄送节点的表单字段权限
* 这里冗余的原因如果是钉钉易搭的抄送节点 (ServiceTask)使用 taskId 可能查不到对应的 activityId
*/
private String activityId;
/**
* 任务主键
*
* 关联 Task id 属性
*/
private String taskId;

View File

@ -7,6 +7,8 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessI
import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface BpmProcessInstanceCopyMapper extends BaseMapperX<BpmProcessInstanceCopyDO> {
@ -18,4 +20,9 @@ public interface BpmProcessInstanceCopyMapper extends BaseMapperX<BpmProcessInst
.orderByDesc(BpmProcessInstanceCopyDO::getId));
}
default List<BpmProcessInstanceCopyDO> selectListByProcessInstanceIdAndActivityId(String processInstanceId, String activityId) {
return selectList(BpmProcessInstanceCopyDO::getProcessInstanceId, processInstanceId,
BpmProcessInstanceCopyDO::getActivityId, activityId);
}
}

View File

@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCand
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.event.BpmProcessInstanceEventPublisher;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import org.flowable.common.engine.api.delegate.FlowableFunctionDelegate;
import org.flowable.common.engine.api.delegate.event.FlowableEventListener;
import org.flowable.spring.SpringProcessEngineConfiguration;
import org.flowable.spring.boot.EngineConfigurationConfigurer;
@ -56,12 +57,15 @@ public class BpmFlowableConfiguration {
@Bean
public EngineConfigurationConfigurer<SpringProcessEngineConfiguration> bpmProcessEngineConfigurationConfigurer(
ObjectProvider<FlowableEventListener> listeners,
ObjectProvider<FlowableFunctionDelegate> customFlowableFunctionDelegates,
BpmActivityBehaviorFactory bpmActivityBehaviorFactory) {
return configuration -> {
// 注册监听器例如说 BpmActivityEventListener
configuration.setEventListeners(ListUtil.toList(listeners.iterator()));
// 设置 ActivityBehaviorFactory 实现类用于流程任务的审核人的自定义
configuration.setActivityBehaviorFactory(bpmActivityBehaviorFactory);
// 设置自定义的函数
configuration.setCustomFlowableFunctionDelegates(ListUtil.toList(customFlowableFunctionDelegates.stream().iterator()));
};
}

View File

@ -1,7 +1,9 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import lombok.Setter;
import org.flowable.bpmn.model.Activity;
import org.flowable.engine.delegate.DelegateExecution;
@ -48,12 +50,17 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
// 第二步获取任务的所有处理人
// 由于每次审批会签或签等情况后都会执行一次所以 variable 已经有结果不重复计算
@SuppressWarnings("unchecked")
Set<Long> assigneeUserIds = (Set<Long>) execution.getVariable(super.collectionVariable, Set.class);
if (assigneeUserIds == null) {
assigneeUserIds = taskCandidateInvoker.calculateUsers(execution);
execution.setVariable(super.collectionVariable, assigneeUserIds);
if (CollUtil.isEmpty(assigneeUserIds)) {
// 特殊如果没有处理人的情况下至少有一个 null 空元素避免自动通过
// 这样保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
// 用途1审批人为空时2审批类型为自动通过自动拒绝时
assigneeUserIds = SetUtils.asSet((Long) null);
}
}
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.candidate.BpmTaskCandidateInvoker;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import lombok.Setter;
@ -41,12 +43,17 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId());
// 第二步获取任务的所有处理人
// 由于每次审批会签或签等情况后都会执行一次所以 variable 已经有结果不重复计算
@SuppressWarnings("unchecked")
Set<Long> assigneeUserIds = (Set<Long>) execution.getVariable(super.collectionVariable, Set.class);
if (assigneeUserIds == null) {
assigneeUserIds = taskCandidateInvoker.calculateUsers(execution);
execution.setVariable(super.collectionVariable, assigneeUserIds);
if (CollUtil.isEmpty(assigneeUserIds)) {
// 特殊如果没有处理人的情况下至少有一个 null 空元素避免自动通过
// 这样保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务
// 用途1审批人为空时2审批类型为自动通过自动拒绝时
assigneeUserIds = SetUtils.asSet((Long) null);
}
}
return assigneeUserIds.size();
}

View File

@ -1,7 +1,6 @@
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.RandomUtil;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
import lombok.Setter;
@ -14,6 +13,7 @@ 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.annotation.Transactional;
import java.util.List;
import java.util.Set;
@ -36,15 +36,17 @@ public class BpmUserTaskActivityBehavior extends UserTaskActivityBehavior {
}
@Override
@Transactional(rollbackFor = Exception.class)
protected void handleAssignments(TaskService taskService, String assignee, String owner,
List<String> candidateUsers, List<String> candidateGroups, TaskEntity task, ExpressionManager expressionManager,
DelegateExecution execution, ProcessEngineConfigurationImpl processEngineConfiguration) {
// 第一步获得任务的候选用户
Long assigneeUserId = calculateTaskCandidateUsers(execution);
Assert.notNull(assigneeUserId, "任务处理人不能为空");
// 第二步设置作为负责人
if (assigneeUserId != null) {
TaskHelper.changeTaskAssignee(task, String.valueOf(assigneeUserId));
}
}
private Long calculateTaskCandidateUsers(DelegateExecution execution) {
// 情况一如果是多实例的任务例如说会签或签等情况则从 Variable 中获取
@ -56,6 +58,9 @@ public class BpmUserTaskActivityBehavior extends UserTaskActivityBehavior {
// 情况二如果非多实例的任务则计算任务处理人
// 第一步先计算可处理该任务的处理人们
Set<Long> candidateUserIds = taskCandidateInvoker.calculateUsers(execution);
if (CollUtil.isEmpty(candidateUserIds)) {
return null;
}
// 第二步后随机选择一个任务的处理人
// 疑问为什么一定要选择一个任务处理人
// 解答项目对 bpm 的任务是责任到人所以每个任务有且仅有一个处理人

View File

@ -2,11 +2,17 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.framework.datapermission.core.annotation.DataPermission;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignStartUserHandlerTypeEnum;
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.service.task.BpmProcessInstanceService;
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;
@ -14,15 +20,12 @@ import lombok.extern.slf4j.Slf4j;
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 java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.*;
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} 的调用者用于调用对应的策略实现任务的候选人的计算
@ -57,7 +60,14 @@ public class BpmTaskCandidateInvoker {
List<UserTask> userTaskList = BpmnModelUtils.getBpmnModelElements(bpmnModel, UserTask.class);
// 遍历所有的 UserTask校验审批人配置
userTaskList.forEach(userTask -> {
// 1. 非空校验
// 1.1 非人工审批无需校验审批人配置
Integer approveType = BpmnModelUtils.parseApproveType(userTask);
if (ObjectUtils.equalsAny(approveType,
BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType(),
BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType())) {
return;
}
// 1.2 非空校验
Integer strategy = BpmnModelUtils.parseCandidateStrategy(userTask);
String param = BpmnModelUtils.parseCandidateParam(userTask);
if (strategy == null) {
@ -80,19 +90,31 @@ public class BpmTaskCandidateInvoker {
*/
@DataPermission(enable = false) // 忽略数据权限避免因为过滤导致找不到候选人
public Set<Long> calculateUsers(DelegateExecution execution) {
// 审批类型非人工审核时不进行计算候选人原因是后续会自动通过不通过
Integer approveType = BpmnModelUtils.parseApproveType(execution.getCurrentFlowElement());
if (ObjectUtils.equalsAny(approveType,
BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType(),
BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType())) {
return new HashSet<>();
}
Integer strategy = BpmnModelUtils.parseCandidateStrategy(execution.getCurrentFlowElement());
String param = BpmnModelUtils.parseCandidateParam(execution.getCurrentFlowElement());
// 1.1 计算任务的候选人
Set<Long> userIds = getCandidateStrategy(strategy).calculateUsers(execution, param);
removeDisableUsers(userIds);
// 1.2 移除被禁用的用户
removeDisableUsers(userIds);
// 2. 校验是否有候选人
// 2. 候选人为空时根据审批人为空的配置补充
if (CollUtil.isEmpty(userIds)) {
log.error("[calculateUsers][流程任务({}/{}/{}) 任务规则({}/{}) 找不到候选人]", execution.getId(),
execution.getProcessDefinitionId(), execution.getCurrentActivityId(), strategy, param);
throw exception(TASK_CREATE_FAIL_NO_CANDIDATE_USER);
userIds = getCandidateStrategy(BpmTaskCandidateStrategyEnum.ASSIGN_EMPTY.getStrategy())
.calculateUsers(execution, param);
// ASSIGN_EMPTY 策略不需要移除被禁用的用户原因是再移除可能会出现更没审批人了
}
// 3. 移除发起人的用户
removeStartUserIfSkip(execution, userIds);
return userIds;
}
@ -108,7 +130,30 @@ public class BpmTaskCandidateInvoker {
});
}
private BpmTaskCandidateStrategy getCandidateStrategy(Integer strategy) {
/**
* 如果审批人与发起人相同时配置了 SKIP 跳过则移除发起人
*
* 注意如果只有一个候选人则不处理避免无法审批
*
* @param execution 执行中的任务
* @param assigneeUserIds 当前分配的候选人
*/
@VisibleForTesting
void removeStartUserIfSkip(DelegateExecution execution, Set<Long> assigneeUserIds) {
if (CollUtil.size(assigneeUserIds) <= 1) {
return;
}
Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(execution.getCurrentFlowElement());
if (ObjectUtil.notEqual(assignStartUserHandlerType, BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType())) {
return;
}
ProcessInstance processInstance = SpringUtil.getBean(BpmProcessInstanceService.class)
.getProcessInstance(execution.getProcessInstanceId());
Assert.notNull(processInstance, "流程实例({}) 不存在", execution.getProcessInstanceId());
assigneeUserIds.remove(Long.valueOf(processInstance.getStartUserId()));
}
public BpmTaskCandidateStrategy getCandidateStrategy(Integer strategy) {
BpmTaskCandidateStrategyEnum strategyEnum = BpmTaskCandidateStrategyEnum.valueOf(strategy);
Assert.notNull(strategyEnum, "策略(%s) 不存在", strategy);
BpmTaskCandidateStrategy strategyObj = strategyMap.get(strategyEnum);

View File

@ -2,12 +2,14 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.runtime.ProcessInstance;
import java.util.Collections;
import java.util.Set;
/**
* BPM 任务的候选人的策略接口
*
* <p>
* 例如说分配审批人
*
* @author 芋道源码
@ -28,14 +30,6 @@ public interface BpmTaskCandidateStrategy {
*/
void validateParam(String param);
/**
* 基于执行任务获得任务的候选用户们
*
* @param execution 执行任务
* @return 用户编号集合
*/
Set<Long> calculateUsers(DelegateExecution execution, String param);
/**
* 是否一定要输入参数
*
@ -45,4 +39,50 @@ public interface BpmTaskCandidateStrategy {
return true;
}
/**
* 基于候选人参数获得任务的候选用户们
*
* @param param 执行任务
* @return 用户编号集合
*/
default Set<Long> calculateUsers(String param) {
return Collections.emptySet();
}
/**
* 基于执行任务获得任务的候选用户们
*
* @param execution 执行任务
* @return 用户编号集合
*/
default Set<Long> calculateUsers(DelegateExecution execution, String param) {
Set<Long> users = calculateUsers(param);
removeDisableUsers(users);
return users;
}
/**
* 基于流程实例获得任务的候选用户们
* <p>
* 目的用于获取未执行节点的候选用户们
*
* @param startUserId 流程发起人编号
* @param processInstance 流程实例编号
* @param activityId 活动 Id (对应 Bpmn XML id)
* @param param 节点的参数
* @return 用户编号集合
*/
default Set<Long> calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) {
Set<Long> users = calculateUsers(param);
removeDisableUsers(users);
return users;
}
/**
* 移除被禁用的用户
*
* @param users 用户 Ids
*/
void removeDisableUsers(Set<Long> users);
}

View File

@ -0,0 +1,78 @@
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.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import java.util.*;
/**
* 部门的负责人 {@link BpmTaskCandidateStrategy} 抽象类
*
* @author jason
*/
public abstract class BpmTaskCandidateAbstractDeptLeaderStrategy extends BpmTaskCandidateAbstractStrategy {
protected DeptApi deptApi;
public BpmTaskCandidateAbstractDeptLeaderStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
super(adminUserApi);
this.deptApi = deptApi;
}
/**
* 获得指定层级的部门负责人只有第 level 的负责人
*
* @param dept 指定部门
* @param level 第几级
* @return 部门负责人的编号
*/
protected Long getAssignLevelDeptLeaderId(DeptRespDTO dept, Integer level) {
Assert.isTrue(level > 0, "level 必须大于 0");
if (dept == null) {
return null;
}
DeptRespDTO currentDept = dept;
for (int i = 1; i < level; i++) {
DeptRespDTO parentDept = deptApi.getDept(currentDept.getParentId());
if (parentDept == null) { // 找不到父级部门到了最高级返回最高级的部门负责人
break;
}
currentDept = parentDept;
}
return currentDept.getLeaderUserId();
}
/**
* 获得连续层级的部门负责人包含 [1, level] 的负责人
*
* @param deptIds 指定部门编号数组
* @param level 最大层级
* @return 连续部门负责人 Id
*/
protected Set<Long> getMultiLevelDeptLeaderIds(List<Long> deptIds, Integer level) {
Assert.isTrue(level > 0, "level 必须大于 0");
if (CollUtil.isEmpty(deptIds)) {
return new HashSet<>();
}
Set<Long> deptLeaderIds = new LinkedHashSet<>(); // 保证有序
for (Long deptId : deptIds) {
DeptRespDTO dept = deptApi.getDept(deptId);
for (int i = 0; i < level; i++) {
if (dept.getLeaderUserId() != null) {
deptLeaderIds.add(dept.getLeaderUserId());
}
DeptRespDTO parentDept = deptApi.getDept(dept.getParentId());
if (parentDept == null) { // 找不到父级部门. 已经到了最高层级了
break;
}
dept = parentDept;
}
}
return deptLeaderIds;
}
}

View File

@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateStrategy;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import java.util.Map;
import java.util.Set;
/**
* {@link BpmTaskCandidateStrategy} 抽象类
*
* @author jason
*/
public abstract class BpmTaskCandidateAbstractStrategy implements BpmTaskCandidateStrategy {
protected AdminUserApi adminUserApi;
public BpmTaskCandidateAbstractStrategy(AdminUserApi adminUserApi) {
this.adminUserApi = adminUserApi;
}
@Override
public void removeDisableUsers(Set<Long> users) {
if (CollUtil.isEmpty(users)) {
return;
}
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(users);
users.removeIf(id -> {
AdminUserRespDTO user = userMap.get(id);
return user == null || !CommonStatusEnum.ENABLE.getStatus().equals(user.getStatus());
});
}
}

View File

@ -0,0 +1,66 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
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.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.flowable.engine.delegate.DelegateExecution;
import org.springframework.context.annotation.Lazy;
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 extends BpmTaskCandidateAbstractStrategy {
@Resource
@Lazy // 延迟加载避免循环依赖
private BpmProcessDefinitionService processDefinitionService;
public BpmTaskCandidateAssignEmptyStrategy(AdminUserApi adminUserApi) {
super(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())) {
Set<Long> users = new HashSet<>(BpmnModelUtils.parseAssignEmptyHandlerUserIds(execution.getCurrentFlowElement()));
removeDisableUsers(users);
return users;
}
// 情况二流程管理员
if (Objects.equals(assignEmptyHandlerType, BpmUserTaskAssignEmptyHandlerTypeEnum.ASSIGN_ADMIN.getType())) {
BpmProcessDefinitionInfoDO processDefinition = processDefinitionService.getProcessDefinitionInfo(execution.getProcessDefinitionId());
Assert.notNull(processDefinition, "流程定义({})不存在", execution.getProcessDefinitionId());
return new HashSet<>(processDefinition.getManagerUserIds());
}
// 都不满足还是返回空
return new HashSet<>();
}
}

View File

@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.util.string.StrUtils;
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.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import org.springframework.stereotype.Component;
import java.util.Set;
/**
* 连续多级部门的负责人 {@link BpmTaskCandidateStrategy} 实现类
*
* @author jason
*/
@Component
public class BpmTaskCandidateDeptLeaderMultiStrategy extends BpmTaskCandidateAbstractDeptLeaderStrategy {
public BpmTaskCandidateDeptLeaderMultiStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
super(adminUserApi, deptApi);
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
return BpmTaskCandidateStrategyEnum.MULTI_DEPT_LEADER_MULTI;
}
@Override
public void validateParam(String param) {
// 参数格式: | 分隔1左边为部门多个部门用 , 分隔2右边为部门层级
String[] params = param.split("\\|");
Assert.isTrue(params.length == 2, "参数格式不匹配");
deptApi.validateDeptList(StrUtils.splitToLong(params[0], ","));
Assert.isTrue(Integer.parseInt(params[1]) > 0, "部门层级必须大于 0");
}
@Override
public Set<Long> calculateUsers(String param) {
String[] params = param.split("\\|");
return getMultiLevelDeptLeaderIds(StrUtils.splitToLong(params[0], ","), Integer.valueOf(params[1]));
}
}

View File

@ -5,8 +5,7 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCand
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import jakarta.annotation.Resource;
import org.flowable.engine.delegate.DelegateExecution;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import org.springframework.stereotype.Component;
import java.util.List;
@ -20,10 +19,14 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
* @author kyle
*/
@Component
public class BpmTaskCandidateDeptLeaderStrategy implements BpmTaskCandidateStrategy {
public class BpmTaskCandidateDeptLeaderStrategy extends BpmTaskCandidateAbstractStrategy {
@Resource
private DeptApi deptApi;
private final DeptApi deptApi;
public BpmTaskCandidateDeptLeaderStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
super(adminUserApi);
this.deptApi = deptApi;
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
@ -37,7 +40,7 @@ public class BpmTaskCandidateDeptLeaderStrategy implements BpmTaskCandidateStrat
}
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
public Set<Long> calculateUsers(String param) {
Set<Long> deptIds = StrUtils.splitToLongSet(param);
List<DeptRespDTO> depts = deptApi.getDeptList(deptIds);
return convertSet(depts, DeptRespDTO::getLeaderUserId);

View File

@ -6,8 +6,6 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidat
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import org.flowable.engine.delegate.DelegateExecution;
import org.springframework.stereotype.Component;
import java.util.List;
@ -21,12 +19,14 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
* @author kyle
*/
@Component
public class BpmTaskCandidateDeptMemberStrategy implements BpmTaskCandidateStrategy {
public class BpmTaskCandidateDeptMemberStrategy extends BpmTaskCandidateAbstractStrategy {
@Resource
private DeptApi deptApi;
@Resource
private AdminUserApi adminUserApi;
private final DeptApi deptApi;
public BpmTaskCandidateDeptMemberStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
super(adminUserApi);
this.deptApi = deptApi;
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
@ -40,7 +40,7 @@ public class BpmTaskCandidateDeptMemberStrategy implements BpmTaskCandidateStrat
}
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
public Set<Long> calculateUsers(String param) {
Set<Long> deptIds = StrUtils.splitToLongSet(param);
List<AdminUserRespDTO> users = adminUserApi.getUserListByDeptIds(deptIds);
return convertSet(users, AdminUserRespDTO::getId);

View File

@ -4,6 +4,7 @@ import cn.hutool.core.convert.Convert;
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.FlowableUtils;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import org.flowable.engine.delegate.DelegateExecution;
import org.springframework.stereotype.Component;
@ -15,7 +16,11 @@ import java.util.Set;
* @author 芋道源码
*/
@Component
public class BpmTaskCandidateExpressionStrategy implements BpmTaskCandidateStrategy {
public class BpmTaskCandidateExpressionStrategy extends BpmTaskCandidateAbstractStrategy {
public BpmTaskCandidateExpressionStrategy(AdminUserApi adminUserApi) {
super(adminUserApi);
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
@ -30,7 +35,9 @@ public class BpmTaskCandidateExpressionStrategy implements BpmTaskCandidateStrat
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
Object result = FlowableUtils.getExpressionValue(execution, param);
return Convert.toSet(Long.class, result);
Set<Long> users = Convert.toSet(Long.class, result);
removeDisableUsers(users);
return users;
}
}

View File

@ -5,8 +5,7 @@ import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmUserGroupDO;
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.service.definition.BpmUserGroupService;
import jakarta.annotation.Resource;
import org.flowable.engine.delegate.DelegateExecution;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import org.springframework.stereotype.Component;
import java.util.Collection;
@ -21,10 +20,14 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
* @author kyle
*/
@Component
public class BpmTaskCandidateGroupStrategy implements BpmTaskCandidateStrategy {
public class BpmTaskCandidateGroupStrategy extends BpmTaskCandidateAbstractStrategy {
@Resource
private BpmUserGroupService userGroupService;
private final BpmUserGroupService userGroupService;
public BpmTaskCandidateGroupStrategy(AdminUserApi adminUserApi, BpmUserGroupService userGroupService) {
super(adminUserApi);
this.userGroupService = userGroupService;
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
@ -38,7 +41,7 @@ public class BpmTaskCandidateGroupStrategy implements BpmTaskCandidateStrategy {
}
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
public Set<Long> calculateUsers(String param) {
Set<Long> groupIds = StrUtils.splitToLongSet(param);
List<BpmUserGroupDO> groups = userGroupService.getUserGroupList(groupIds);
return convertSetByFlatMap(groups, BpmUserGroupDO::getUserIds, Collection::stream);

View File

@ -6,8 +6,6 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidat
import cn.iocoder.yudao.module.system.api.dept.PostApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import org.flowable.engine.delegate.DelegateExecution;
import org.springframework.stereotype.Component;
import java.util.List;
@ -21,12 +19,14 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
* @author kyle
*/
@Component
public class BpmTaskCandidatePostStrategy implements BpmTaskCandidateStrategy {
public class BpmTaskCandidatePostStrategy extends BpmTaskCandidateAbstractStrategy {
@Resource
private PostApi postApi;
@Resource
private AdminUserApi adminUserApi;
private final PostApi postApi;
public BpmTaskCandidatePostStrategy(AdminUserApi adminUserApi, PostApi postApi) {
super(adminUserApi);
this.postApi = postApi;
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
@ -40,7 +40,7 @@ public class BpmTaskCandidatePostStrategy implements BpmTaskCandidateStrategy {
}
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
public Set<Long> calculateUsers(String param) {
Set<Long> postIds = StrUtils.splitToLongSet(param);
List<AdminUserRespDTO> users = adminUserApi.getUserListByPostIds(postIds);
return convertSet(users, AdminUserRespDTO::getId);

View File

@ -5,8 +5,8 @@ import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCand
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
import cn.iocoder.yudao.module.system.api.permission.RoleApi;
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.Set;
@ -17,13 +17,17 @@ import java.util.Set;
* @author kyle
*/
@Component
public class BpmTaskCandidateRoleStrategy implements BpmTaskCandidateStrategy {
public class BpmTaskCandidateRoleStrategy extends BpmTaskCandidateAbstractStrategy {
@Resource
private RoleApi roleApi;
@Resource
private PermissionApi permissionApi;
public BpmTaskCandidateRoleStrategy(AdminUserApi adminUserApi) {
super(adminUserApi);
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
return BpmTaskCandidateStrategyEnum.ROLE;
@ -36,7 +40,7 @@ public class BpmTaskCandidateRoleStrategy implements BpmTaskCandidateStrategy {
}
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
public Set<Long> calculateUsers(String param) {
Set<Long> roleIds = StrUtils.splitToLongSet(param);
return permissionApi.getUserRoleIdListByRoleIds(roleIds);
}

View File

@ -0,0 +1,90 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
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.service.task.BpmProcessInstanceService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
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.HashSet;
import java.util.Set;
import static cn.hutool.core.collection.ListUtil.toList;
/**
* 发起人连续多级部门的负责人 {@link BpmTaskCandidateStrategy} 实现类
*
* @author jason
*/
@Component
public class BpmTaskCandidateStartUserDeptLeaderMultiStrategy extends BpmTaskCandidateAbstractDeptLeaderStrategy {
@Resource
@Lazy
private BpmProcessInstanceService processInstanceService;
public BpmTaskCandidateStartUserDeptLeaderMultiStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
super(adminUserApi, deptApi);
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
return BpmTaskCandidateStrategyEnum.START_USER_DEPT_LEADER_MULTI;
}
@Override
public void validateParam(String param) {
// 参数是部门的层级
Assert.isTrue(Integer.parseInt(param) > 0, "部门的层级必须大于 0");
}
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
// 获得流程发起人
ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId());
Long startUserId = NumberUtils.parseLong(processInstance.getStartUserId());
// 获取发起人的 multi 部门负责人
DeptRespDTO dept = getStartUserDept(startUserId);
if (dept == null) {
return new HashSet<>();
}
Set<Long> users = getMultiLevelDeptLeaderIds(toList(dept.getId()), Integer.valueOf(param)); // 参数是部门的层级
// TODO @jason这里 removeDisableUsers 的原因是啥呀
removeDisableUsers(users);
return users;
}
@Override
public Set<Long> calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) {
DeptRespDTO dept = getStartUserDept(startUserId);
if (dept == null) {
return new HashSet<>();
}
Set<Long> users = getMultiLevelDeptLeaderIds(toList(dept.getId()), Integer.valueOf(param)); // 参数是部门的层级
removeDisableUsers(users);
return users;
}
/**
* 获取发起人的部门
*
* @param startUserId 发起人 Id
*/
protected DeptRespDTO getStartUserDept(Long startUserId) {
AdminUserRespDTO startUser = adminUserApi.getUser(startUserId);
if (startUser.getDeptId() == null) { // 找不到部门
return null;
}
return deptApi.getDept(startUser.getDeptId());
}
}

View File

@ -0,0 +1,91 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
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.service.task.BpmProcessInstanceService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
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.HashSet;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.util.collection.SetUtils.asSet;
/**
* 发起人的部门负责人, 可以是上级部门负责人 {@link BpmTaskCandidateStrategy} 实现类
*
* @author jason
*/
@Component
public class BpmTaskCandidateStartUserDeptLeaderStrategy extends BpmTaskCandidateAbstractDeptLeaderStrategy {
@Resource
@Lazy // 避免循环依赖
private BpmProcessInstanceService processInstanceService;
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
return BpmTaskCandidateStrategyEnum.START_USER_DEPT_LEADER;
}
public BpmTaskCandidateStartUserDeptLeaderStrategy(AdminUserApi adminUserApi, DeptApi deptApi) {
super(adminUserApi, deptApi);
}
@Override
public void validateParam(String param) {
// 参数是部门的层级
Assert.isTrue(Integer.parseInt(param) > 0, "部门的层级必须大于 0");
}
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
// 获得流程发起人
ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId());
Long startUserId = NumberUtils.parseLong(processInstance.getStartUserId());
// 获取发起人的部门负责人
Set<Long> users = getStartUserDeptLeader(startUserId, param);
removeDisableUsers(users);
return users;
}
@Override
public Set<Long> calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) {
// 获取发起人的部门负责人
Set<Long> users = getStartUserDeptLeader(startUserId, param);
removeDisableUsers(users);
return users;
}
private Set<Long> getStartUserDeptLeader(Long startUserId, String param) {
DeptRespDTO dept = getStartUserDept(startUserId);
if (dept == null) {
return new HashSet<>();
}
Long deptLeaderId = getAssignLevelDeptLeaderId(dept, Integer.valueOf(param)); // 参数是部门的层级
return deptLeaderId != null ? asSet(deptLeaderId) : new HashSet<>();
}
/**
* 获取发起人的部门
*
* @param startUserId 发起人 Id
*/
protected DeptRespDTO getStartUserDept(Long startUserId) {
AdminUserRespDTO startUser = adminUserApi.getUser(startUserId);
if (startUser.getDeptId() == null) { // 找不到部门
return null;
}
return deptApi.getDept(startUser.getDeptId());
}
}

View File

@ -2,11 +2,11 @@ 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 cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.UserTask;
@ -23,12 +23,16 @@ import java.util.*;
* @author 芋道源码
*/
@Component
public class BpmTaskCandidateStartUserSelectStrategy implements BpmTaskCandidateStrategy {
public class BpmTaskCandidateStartUserSelectStrategy extends BpmTaskCandidateAbstractStrategy {
@Resource
@Lazy // 延迟加载避免循环依赖
private BpmProcessInstanceService processInstanceService;
public BpmTaskCandidateStartUserSelectStrategy(AdminUserApi adminUserApi) {
super(adminUserApi);
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
return BpmTaskCandidateStrategyEnum.START_USER_SELECT;
@ -46,7 +50,23 @@ public class BpmTaskCandidateStartUserSelectStrategy implements BpmTaskCandidate
execution.getProcessInstanceId());
// 获得审批人
List<Long> assignees = startUserSelectAssignees.get(execution.getCurrentActivityId());
return new LinkedHashSet<>(assignees);
Set<Long> users = new LinkedHashSet<>(assignees);
removeDisableUsers(users);
return users;
}
@Override
public Set<Long> calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) {
if (processInstance == null) {
return Collections.emptySet();
}
Map<String, List<Long>> startUserSelectAssignees = FlowableUtils.getStartUserSelectAssignees(processInstance);
Assert.notNull(startUserSelectAssignees, "流程实例({}) 的发起人自选审批人不能为空", processInstance.getId());
// 获得审批人
List<Long> assignees = startUserSelectAssignees.get(activityId);
Set<Long> users = new LinkedHashSet<>(assignees);
removeDisableUsers(users);
return users;
}
@Override

View File

@ -0,0 +1,62 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
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.Set;
/**
* 发起人自己 {@link BpmTaskCandidateUserStrategy} 实现类
* <p>
* 适合场景用于需要发起人信息复核等场景
*
* @author jason
*/
@Component
public class BpmTaskCandidateStartUserStrategy extends BpmTaskCandidateAbstractStrategy {
@Resource
@Lazy // 延迟加载避免循环依赖
private BpmProcessInstanceService processInstanceService;
public BpmTaskCandidateStartUserStrategy(AdminUserApi adminUserApi) {
super(adminUserApi);
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
return BpmTaskCandidateStrategyEnum.START_USER;
}
@Override
public void validateParam(String param) {
}
@Override
public boolean isParamRequired() {
return false;
}
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId());
Set<Long> users = SetUtils.asSet(Long.valueOf(processInstance.getStartUserId()));
removeDisableUsers(users);
return users;
}
@Override
public Set<Long> calculateUsers(Long startUserId, ProcessInstance processInstance, String activityId, String param) {
Set<Long> users = SetUtils.asSet(startUserId);
removeDisableUsers(users);
return users;
}
}

View File

@ -1,13 +1,13 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy;
import cn.hutool.core.text.StrPool;
import cn.iocoder.yudao.framework.common.util.string.StrUtils;
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.system.api.user.AdminUserApi;
import jakarta.annotation.Resource;
import org.flowable.engine.delegate.DelegateExecution;
import org.springframework.stereotype.Component;
import java.util.LinkedHashSet;
import java.util.Set;
/**
@ -16,10 +16,11 @@ import java.util.Set;
* @author kyle
*/
@Component
public class BpmTaskCandidateUserStrategy implements BpmTaskCandidateStrategy {
public class BpmTaskCandidateUserStrategy extends BpmTaskCandidateAbstractStrategy {
@Resource
private AdminUserApi adminUserApi;
public BpmTaskCandidateUserStrategy(AdminUserApi adminUserApi) {
super(adminUserApi);
}
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
@ -32,8 +33,8 @@ public class BpmTaskCandidateUserStrategy implements BpmTaskCandidateStrategy {
}
@Override
public Set<Long> calculateUsers(DelegateExecution execution, String param) {
return StrUtils.splitToLongSet(param);
public Set<Long> calculateUsers(String param) {
return new LinkedHashSet<>(StrUtils.splitToLong(param, StrPool.COMMA));
}
}

View File

@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.el;
import org.flowable.common.engine.api.variable.VariableContainer;
import org.flowable.common.engine.impl.el.function.AbstractFlowableVariableExpressionFunction;
import org.springframework.stereotype.Component;
// TODO @jason这个自定义转换的原因是啥呀
/**
* 根据流程变量 variable 的类型, 转换参数的值
*
* @author jason
*/
@Component
public class VariableConvertByTypeExpressionFunction extends AbstractFlowableVariableExpressionFunction {
public VariableConvertByTypeExpressionFunction() {
super("convertByType");
}
public static Object convertByType(VariableContainer variableContainer, String variableName, Object parmaValue) {
Object variable = variableContainer.getVariable(variableName);
if (variable != null && parmaValue != null) {
// 如果值不是字符串类型, 流程变量的类型是字符串 把值转成字符串
if (!(parmaValue instanceof String) && variable instanceof String ) {
return parmaValue.toString();
}
}
return parmaValue;
}
}

View File

@ -1,9 +1,12 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.enums;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.core.IntArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* BPM 任务的候选人策略枚举
*
@ -13,18 +16,25 @@ import lombok.Getter;
*/
@Getter
@AllArgsConstructor
public enum BpmTaskCandidateStrategyEnum {
public enum BpmTaskCandidateStrategyEnum implements IntArrayValuable {
ROLE(10, "角色"),
DEPT_MEMBER(20, "部门的成员"), // 包括负责人
DEPT_LEADER(21, "部门的负责人"),
MULTI_DEPT_LEADER_MULTI(23, "连续多级部门的负责人"),
POST(22, "岗位"),
USER(30, "用户"),
START_USER_SELECT(35, "发起人自选"), // 申请人自己可在提交申请时选择此节点的审批人
START_USER(36, "发起人自己"), // 申请人自己, 一般紧挨开始节点常用于发起人信息审核场景
START_USER_DEPT_LEADER(37, "发起人部门负责人"),
START_USER_DEPT_LEADER_MULTI(38, "发起人连续多级部门的负责人"),
USER_GROUP(40, "用户组"),
EXPRESSION(60, "流程表达式"), // 表达式 ExpressionManager
ASSIGN_EMPTY(1, "审批人为空"),
;
public static final int[] ARRAYS = Arrays.stream(values()).mapToInt(BpmTaskCandidateStrategyEnum::getStrategy).toArray();
/**
* 类型
*/
@ -38,4 +48,9 @@ public enum BpmTaskCandidateStrategyEnum {
return ArrayUtil.firstMatch(o -> o.getStrategy().equals(strategy), values());
}
@Override
public int[] array() {
return ARRAYS;
}
}

View File

@ -23,4 +23,95 @@ public interface BpmnModelConstants {
*/
String USER_TASK_CANDIDATE_PARAM = "candidateParam";
/**
* BPMN ExtensionElement 的扩展属性用于标记边界事件类型
*/
String BOUNDARY_EVENT_TYPE = "boundaryEventType";
/**
* BPMN ExtensionElement 的扩展属性用于标记用户任务超时执行动作
*/
String USER_TASK_TIMEOUT_HANDLER_TYPE = "timeoutHandlerType";
/**
* BPMN ExtensionElement 的扩展属性用于标记用户任务的审批人与发起人相同时对应的处理类型
*/
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 的扩展属性用于标记用户任务拒绝处理类型
*/
String USER_TASK_REJECT_HANDLER_TYPE = "rejectHandlerType";
/**
* BPMN ExtensionElement 的扩展属性用于标记用户任务拒绝后的回退的任务 Id
*/
String USER_TASK_REJECT_RETURN_TASK_ID = "rejectReturnTaskId";
/**
* BPMN UserTask 的扩展属性用于标记用户任务的审批类型
*/
String USER_TASK_APPROVE_TYPE = "approveType";
/**
* BPMN UserTask 的扩展属性用于标记用户任务的审批方式
*/
String USER_TASK_APPROVE_METHOD = "approveMethod";
/**
* BPMN ExtensionElement 流程表单字段权限元素, 用于标记字段权限
*/
String FORM_FIELD_PERMISSION_ELEMENT = "fieldsPermission";
/**
* BPMN ExtensionElement Attribute, 用于标记表单字段
*/
String FORM_FIELD_PERMISSION_ELEMENT_FIELD_ATTRIBUTE = "field";
/**
* BPMN ExtensionElement Attribute, 用于标记表单权限
*/
String FORM_FIELD_PERMISSION_ELEMENT_PERMISSION_ATTRIBUTE = "permission";
/**
* BPMN ExtensionElement 操作按钮设置元素, 用于审批节点操作按钮设置
*/
String BUTTON_SETTING_ELEMENT = "buttonsSetting";
/**
* BPMN ExtensionElement Attribute, 用于标记按钮编号
*/
String BUTTON_SETTING_ELEMENT_ID_ATTRIBUTE = "id";
/**
* BPMN ExtensionElement Attribute, 用于标记按钮显示名称
*/
String BUTTON_SETTING_ELEMENT_DISPLAY_NAME_ATTRIBUTE = "displayName";
/**
* BPMN ExtensionElement Attribute, 用于标记按钮是否启用
*/
String BUTTON_SETTING_ELEMENT_ENABLE_ATTRIBUTE = "enable";
/**
* BPMN Start Event Node Id
*/
String START_EVENT_NODE_ID = "StartEvent";
/**
* BPMN Start Event Node Name
*/
String START_EVENT_NODE_NAME = "开始";
/**
* 发起人节点 ID
*/
String START_USER_NODE_ID = "StartUserNode";
}

View File

@ -3,11 +3,11 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.enums;
import org.flowable.engine.runtime.ProcessInstance;
/**
* BPM 通用常量
* BPM Variable 通用常量
*
* @author 芋道源码
*/
public class BpmConstants {
public class BpmnVariableConstants {
/**
* 流程实例的变量 - 状态
@ -15,6 +15,14 @@ public class BpmConstants {
* @see ProcessInstance#getProcessVariables()
*/
public static final String PROCESS_INSTANCE_VARIABLE_STATUS = "PROCESS_STATUS";
/**
* 流程实例的变量 - 理由
*
* 例如说审批不通过的理由目前审核通过暂时不会记录
*
* @see ProcessInstance#getProcessVariables()
*/
public static final String PROCESS_INSTANCE_VARIABLE_REASON = "PROCESS_REASON";
/**
* 流程实例的变量 - 发起用户选择的审批人 Map
*
@ -22,6 +30,15 @@ public class BpmConstants {
*/
public static final String PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES = "PROCESS_START_USER_SELECT_ASSIGNEES";
/**
* 流程实例的变量 - 用于判断流程实例变量节点是否驳回. 格式 RETURN_FLAG_{节点 id}
*
* 目的是驳回到发起节点时因为审批人与发起人相同所以被自动通过但是此时还是希望不要自动通过
*
* @see ProcessInstance#getProcessVariables()
*/
public static final String PROCESS_INSTANCE_VARIABLE_RETURN_FLAG = "RETURN_FLAG_%s";
/**
* 任务的变量 - 状态
*

View File

@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.enums;
// TODO @jason要不合并到 BpmnModelConstants
/**
* 仿钉钉快搭 JSON 常量信息
*
* @author jason
*/
public interface SimpleModelConstants {
// TODO @芋艿条件表达式的字段名
/**
* 网关节点默认序列流属性
*/
String DEFAULT_FLOW_ATTRIBUTE = "defaultFlow";
/**
* 条件节点的条件类型属性
*/
String CONDITION_TYPE_ATTRIBUTE = "conditionType";
/**
* 条件节点条件表达式属性
*/
String CONDITION_EXPRESSION_ATTRIBUTE = "conditionExpression";
/**
* 条件规则的条件组属性
*/
String CONDITION_GROUPS_ATTRIBUTE = "conditionGroups";
}

View File

@ -0,0 +1,47 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService;
import jakarta.annotation.Resource;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.JavaDelegate;
import org.springframework.stereotype.Component;
import java.util.Set;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmCopyTaskDelegate.BEAN_NAME;
/**
* 处理抄送用户的 {@link JavaDelegate} 的实现类
*
* 目前只有快搭模式的抄送节点使用
*
* @author jason
*/
@Component(BEAN_NAME)
public class BpmCopyTaskDelegate implements JavaDelegate {
public static final String BEAN_NAME = "bpmCopyTaskDelegate";
@Resource
private BpmTaskCandidateInvoker taskCandidateInvoker;
@Resource
private BpmProcessInstanceCopyService processInstanceCopyService;
@Override
public void execute(DelegateExecution execution) {
// 1. 获得抄送人
Set<Long> userIds = taskCandidateInvoker.calculateUsers(execution);
if (CollUtil.isEmpty(userIds)) {
return;
}
// 2. 执行抄送
FlowElement currentFlowElement = execution.getCurrentFlowElement();
processInstanceCopyService.createProcessInstanceCopy(userIds, execution.getProcessInstanceId(),
currentFlowElement.getId(), null, currentFlowElement.getName());
}
}

View File

@ -6,7 +6,6 @@ import jakarta.annotation.Resource;
import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent;
import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType;
import org.flowable.engine.delegate.event.AbstractFlowableEngineEventListener;
import org.flowable.engine.delegate.event.FlowableCancelledEvent;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
@ -21,27 +20,21 @@ import java.util.Set;
@Component
public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEventListener {
@Resource
@Lazy
private BpmProcessInstanceService processInstanceService;
public static final Set<FlowableEngineEventType> PROCESS_INSTANCE_EVENTS = ImmutableSet.<FlowableEngineEventType>builder()
.add(FlowableEngineEventType.PROCESS_CANCELLED)
.add(FlowableEngineEventType.PROCESS_COMPLETED)
.build();
@Resource
@Lazy // 延迟加载避免循环依赖
private BpmProcessInstanceService processInstanceService;
public BpmProcessInstanceEventListener(){
super(PROCESS_INSTANCE_EVENTS);
}
@Override
protected void processCancelled(FlowableCancelledEvent event) {
processInstanceService.updateProcessInstanceWhenCancel(event);
}
@Override
protected void processCompleted(FlowableEngineEntityEvent event) {
processInstanceService.updateProcessInstanceWhenApprove((ProcessInstance)event.getEntity());
processInstanceService.processProcessInstanceCompleted((ProcessInstance)event.getEntity());
}
}

View File

@ -1,21 +1,31 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.listener;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventType;
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.bpm.service.definition.BpmModelService;
import cn.iocoder.yudao.module.bpm.service.task.BpmActivityService;
import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
import com.google.common.collect.ImmutableSet;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.BoundaryEvent;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.common.engine.api.delegate.event.FlowableEngineEntityEvent;
import org.flowable.common.engine.api.delegate.event.FlowableEngineEventType;
import org.flowable.engine.delegate.event.AbstractFlowableEngineEventListener;
import org.flowable.engine.delegate.event.FlowableActivityCancelledEvent;
import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.job.api.Job;
import org.flowable.task.api.Task;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import jakarta.annotation.Resource;
import java.util.List;
import java.util.Set;
@ -28,6 +38,9 @@ import java.util.Set;
@Slf4j
public class BpmTaskEventListener extends AbstractFlowableEngineEventListener {
@Resource
@Lazy // 延迟加载避免循环依赖
private BpmModelService modelService;
@Resource
@Lazy // 解决循环依赖
private BpmTaskService taskService;
@ -40,20 +53,21 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener {
.add(FlowableEngineEventType.TASK_ASSIGNED)
// .add(FlowableEngineEventType.TASK_COMPLETED) // 由于审批通过时已经记录了 task status 为通过所以不需要监听了
.add(FlowableEngineEventType.ACTIVITY_CANCELLED)
.add(FlowableEngineEventType.TIMER_FIRED) // 监听审批超时
.build();
public BpmTaskEventListener(){
public BpmTaskEventListener() {
super(TASK_EVENTS);
}
@Override
protected void taskCreated(FlowableEngineEntityEvent event) {
taskService.updateTaskStatusWhenCreated((Task) event.getEntity());
taskService.processTaskCreated((Task) event.getEntity());
}
@Override
protected void taskAssigned(FlowableEngineEntityEvent event) {
taskService.updateTaskExtAssign((Task)event.getEntity());
taskService.processTaskAssigned((Task) event.getEntity());
}
@Override
@ -68,8 +82,34 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener {
if (StrUtil.isEmpty(activity.getTaskId())) {
return;
}
taskService.updateTaskStatusWhenCanceled(activity.getTaskId());
taskService.processTaskCanceled(activity.getTaskId());
});
}
@Override
protected void timerFired(FlowableEngineEntityEvent event) {
// 1.1 只处理 BoundaryEvent 边界计时时间
String processDefinitionId = event.getProcessDefinitionId();
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processDefinitionId);
Job entity = (Job) event.getEntity();
FlowElement element = BpmnModelUtils.getFlowElementById(bpmnModel, entity.getElementId());
if (!(element instanceof BoundaryEvent)) {
return;
}
// 1.2 判断是否为超时处理
BoundaryEvent boundaryEvent = (BoundaryEvent) element;
String boundaryEventType = BpmnModelUtils.parseBoundaryEventExtensionElement(boundaryEvent,
BpmnModelConstants.BOUNDARY_EVENT_TYPE);
BpmBoundaryEventType bpmTimerBoundaryEventType = BpmBoundaryEventType.typeOf(NumberUtils.parseInt(boundaryEventType));
if (ObjectUtil.notEqual(bpmTimerBoundaryEventType, BpmBoundaryEventType.USER_TASK_TIMEOUT)) {
return;
}
// 2. 处理超时
String timeoutHandlerType = BpmnModelUtils.parseBoundaryEventExtensionElement(boundaryEvent,
BpmnModelConstants.USER_TASK_TIMEOUT_HANDLER_TYPE);
String taskKey = boundaryEvent.getAttachedToRefId();
taskService.processTaskTimeout(event.getProcessInstanceId(), taskKey, NumberUtils.parseInt(timeoutHandlerType));
}
}

View File

@ -1,9 +1,13 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
import cn.hutool.core.collection.CollUtil;
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;
import org.flowable.bpmn.converter.BpmnXMLConverter;
import org.flowable.bpmn.model.Process;
@ -12,19 +16,111 @@ import org.flowable.common.engine.impl.util.io.BytesStreamSource;
import java.util.*;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.*;
import static org.flowable.bpmn.constants.BpmnXMLConstants.FLOWABLE_EXTENSIONS_NAMESPACE;
/**
* 流程模型转操作工具类
*/
public class BpmnModelUtils {
public static Integer parseCandidateStrategy(FlowElement userTask) {
return NumberUtils.parseInt(userTask.getAttributeValue(
Integer candidateStrategy = NumberUtils.parseInt(userTask.getAttributeValue(
BpmnModelConstants.NAMESPACE, BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY));
// TODO @芋艿 尝试从 ExtensionElement . 后续相关扩展是否都可以 extensionElement 如表单权限 按钮权限
if (candidateStrategy == null) {
ExtensionElement element = CollUtil.getFirst(userTask.getExtensionElements().get(BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY));
candidateStrategy = element != null ? NumberUtils.parseInt(element.getElementText()) : null;
}
return candidateStrategy;
}
public static String parseCandidateParam(FlowElement userTask) {
return userTask.getAttributeValue(
String candidateParam = userTask.getAttributeValue(
BpmnModelConstants.NAMESPACE, BpmnModelConstants.USER_TASK_CANDIDATE_PARAM);
if (candidateParam == null) {
ExtensionElement element = CollUtil.getFirst(userTask.getExtensionElements().get(BpmnModelConstants.USER_TASK_CANDIDATE_PARAM));
candidateParam = element != null ? element.getElementText() : null;
}
return candidateParam;
}
public static Integer parseApproveType(FlowElement userTask) {
return NumberUtils.parseInt(parseExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_TYPE));
}
public static BpmUserTaskRejectHandlerType parseRejectHandlerType(FlowElement userTask) {
Integer rejectHandlerType = NumberUtils.parseInt(parseExtensionElement(userTask, USER_TASK_REJECT_HANDLER_TYPE));
return BpmUserTaskRejectHandlerType.typeOf(rejectHandlerType);
}
public static String parseReturnTaskId(FlowElement flowElement) {
return parseExtensionElement(flowElement, USER_TASK_REJECT_RETURN_TASK_ID);
}
public static Integer parseAssignStartUserHandlerType(FlowElement userTask) {
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) {
if (flowElement == null) {
return null;
}
ExtensionElement element = CollUtil.getFirst(flowElement.getExtensionElements().get(elementName));
return element != null ? element.getElementText() : null;
}
public static Map<String, String> parseFormFieldsPermission(BpmnModel bpmnModel, String flowElementId) {
if (bpmnModel == null || StrUtil.isEmpty(flowElementId)) {
return null;
}
FlowElement flowElement = getFlowElementById(bpmnModel, flowElementId);
if (flowElement == null) {
return null;
}
List<ExtensionElement> extensionElements = flowElement.getExtensionElements().get(FORM_FIELD_PERMISSION_ELEMENT);
if (CollUtil.isEmpty(extensionElements)) {
return null;
}
Map<String, String> fieldsPermission = MapUtil.newHashMap();
extensionElements.forEach(element -> {
String field = element.getAttributeValue(FLOWABLE_EXTENSIONS_NAMESPACE, FORM_FIELD_PERMISSION_ELEMENT_FIELD_ATTRIBUTE);
String permission = element.getAttributeValue(FLOWABLE_EXTENSIONS_NAMESPACE, FORM_FIELD_PERMISSION_ELEMENT_PERMISSION_ATTRIBUTE);
if (StrUtil.isNotEmpty(field) && StrUtil.isNotEmpty(permission)) {
fieldsPermission.put(field, permission);
}
});
return fieldsPermission;
}
public static Map<Integer, BpmTaskRespVO.OperationButtonSetting> parseButtonsSetting(BpmnModel bpmnModel, String flowElementId) {
FlowElement flowElement = getFlowElementById(bpmnModel, flowElementId);
if (flowElement == null) {
return null;
}
List<ExtensionElement> extensionElements = flowElement.getExtensionElements().get(BUTTON_SETTING_ELEMENT);
if (CollUtil.isEmpty(extensionElements)) {
return null;
}
Map<Integer, BpmTaskRespVO.OperationButtonSetting> buttonSettings = MapUtil.newHashMap(16);
extensionElements.forEach(element -> {
String id = element.getAttributeValue(FLOWABLE_EXTENSIONS_NAMESPACE, BUTTON_SETTING_ELEMENT_ID_ATTRIBUTE);
String displayName = element.getAttributeValue(FLOWABLE_EXTENSIONS_NAMESPACE, BUTTON_SETTING_ELEMENT_DISPLAY_NAME_ATTRIBUTE);
String enable = element.getAttributeValue(FLOWABLE_EXTENSIONS_NAMESPACE, BUTTON_SETTING_ELEMENT_ENABLE_ATTRIBUTE);
if (StrUtil.isNotEmpty(id)) {
BpmTaskRespVO.OperationButtonSetting setting = new BpmTaskRespVO.OperationButtonSetting();
buttonSettings.put(Integer.valueOf(id), setting.setDisplayName(displayName).setEnable(Boolean.parseBoolean(enable)));
}
});
return buttonSettings;
}
/**
@ -95,6 +191,12 @@ public class BpmnModelUtils {
return (StartEvent) CollUtil.findOne(process.getFlowElements(), flowElement -> flowElement instanceof StartEvent);
}
public static EndEvent getEndEvent(BpmnModel model) {
Process process = model.getMainProcess();
// flowElementList endEvent. TODO 多个 EndEvent 会有问题
return (EndEvent) CollUtil.findOne(process.getFlowElements(), flowElement -> flowElement instanceof EndEvent);
}
public static BpmnModel getBpmnModel(byte[] bpmnBytes) {
if (ArrayUtil.isEmpty(bpmnBytes)) {
return null;
@ -334,4 +436,11 @@ public class BpmnModelUtils {
return userTaskList;
}
public static String parseBoundaryEventExtensionElement(BoundaryEvent boundaryEvent, String customElement) {
if (boundaryEvent == null) {
return null;
}
ExtensionElement extensionElement = CollUtil.getFirst(boundaryEvent.getExtensionElements().get(customElement));
return Optional.ofNullable(extensionElement).map(ExtensionElement::getElementText).orElse(null);
}
}

View File

@ -1,7 +1,9 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmConstants;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
import org.flowable.common.engine.api.delegate.Expression;
import org.flowable.common.engine.api.variable.VariableContainer;
import org.flowable.common.engine.impl.el.ExpressionManager;
@ -16,6 +18,7 @@ import org.flowable.task.api.TaskInfo;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Flowable 相关的工具方法
@ -39,6 +42,16 @@ public class FlowableUtils {
return tenantId != null ? String.valueOf(tenantId) : ProcessEngineConfiguration.NO_TENANT_ID;
}
public static void execute(String tenantIdStr, Runnable runnable) {
if (ObjectUtil.isEmpty(tenantIdStr)
|| Objects.equals(tenantIdStr, ProcessEngineConfiguration.NO_TENANT_ID)) {
runnable.run();
} else {
Long tenantId = Long.valueOf(tenantIdStr);
TenantUtils.execute(tenantId, runnable);
}
}
// ========== Execution 相关的工具方法 ==========
/**
@ -78,7 +91,7 @@ public class FlowableUtils {
* @return 状态
*/
private static Integer getProcessInstanceStatus(Map<String, Object> processVariables) {
return (Integer) processVariables.get(BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS);
return (Integer) processVariables.get(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS);
}
/**
@ -102,7 +115,7 @@ public class FlowableUtils {
* @return 过滤后的表单
*/
public static Map<String, Object> filterProcessInstanceFormVariable(Map<String, Object> processVariables) {
processVariables.remove(BpmConstants.PROCESS_INSTANCE_VARIABLE_STATUS);
processVariables.remove(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS);
return processVariables;
}
@ -115,7 +128,7 @@ public class FlowableUtils {
@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);
BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES);
}
// ========== Task 相关的工具方法 ==========
@ -127,7 +140,7 @@ public class FlowableUtils {
* @return 状态
*/
public static Integer getTaskStatus(TaskInfo task) {
return (Integer) task.getTaskLocalVariables().get(BpmConstants.TASK_VARIABLE_STATUS);
return (Integer) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS);
}
/**
@ -137,7 +150,7 @@ public class FlowableUtils {
* @return 审批原因
*/
public static String getTaskReason(TaskInfo task) {
return (String) task.getTaskLocalVariables().get(BpmConstants.TASK_VARIABLE_REASON);
return (String) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_REASON);
}
/**
@ -161,8 +174,8 @@ public class FlowableUtils {
* @return 过滤后的表单
*/
public static Map<String, Object> filterTaskFormVariable(Map<String, Object> taskLocalVariables) {
taskLocalVariables.remove(BpmConstants.TASK_VARIABLE_STATUS);
taskLocalVariables.remove(BpmConstants.TASK_VARIABLE_REASON);
taskLocalVariables.remove(BpmnVariableConstants.TASK_VARIABLE_STATUS);
taskLocalVariables.remove(BpmnVariableConstants.TASK_VARIABLE_REASON);
return taskLocalVariables;
}

View File

@ -0,0 +1,631 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.*;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.ConditionGroups;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.RejectHandler;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmBoundaryEventType;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModeConditionType;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeType;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveMethodEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmCopyTaskDelegate;
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 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;
import static cn.iocoder.yudao.module.bpm.enums.definition.BpmSimpleModelNodeType.*;
import static cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveMethodEnum.*;
import static cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskApproveTypeEnum.USER;
import static cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP;
import static cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerTypeEnum.REMINDER;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum.START_USER;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.*;
import static org.flowable.bpmn.constants.BpmnXMLConstants.*;
/**
* 仿钉钉快搭模型相关的工具方法
*
* @author jason
*/
public class SimpleModelUtils {
/**
* 聚合网关节点 Id 后缀
*/
public static final String JOIN_GATE_WAY_NODE_ID_SUFFIX = "_join";
/**
* 所有审批人同意的表达式
*/
public static final String ALL_APPROVE_COMPLETE_EXPRESSION = "${ nrOfCompletedInstances >= nrOfInstances }";
/**
* 任一一名审批人同意的表达式
*/
public static final String ANY_OF_APPROVE_COMPLETE_EXPRESSION = "${ nrOfCompletedInstances > 0 }";
/**
* 按通过比例完成表达式
*/
public static final String APPROVE_BY_RATIO_COMPLETE_EXPRESSION = "${ nrOfCompletedInstances/nrOfInstances >= %s}";
// TODO @yunai注释需要完善下
/**
* 仿钉钉流程设计模型数据结构(json) 转换成 Bpmn Model (待完善
*
* @param processId 流程标识
* @param processName 流程名称
* @param simpleModelNode 仿钉钉流程设计模型数据结构
* @return Bpmn Model
*/
public static BpmnModel buildBpmnModel(String processId, String processName, BpmSimpleModelNodeVO simpleModelNode) {
BpmnModel bpmnModel = new BpmnModel();
// 不加这个 解析 Message 会报 NPE 异常 .
bpmnModel.setTargetNamespace(BPMN2_NAMESPACE); // TODO @jason待定是不是搞个自定义的 namespace
// TODO 芋艿后续在 review
Process process = new Process();
process.setId(processId);
process.setName(processName);
process.setExecutable(Boolean.TRUE); // TODO @jason这个是必须设置的么
bpmnModel.addProcess(process);
// TODO 芋艿这里可能纠结下到底前端传递还是后端创建出来
// 目前前端的第一个节点是发起人节点这里构建一个 StartNode用于创建 Bpmn StartEvent 节点
BpmSimpleModelNodeVO startNode = buildStartSimpleModelNode();
startNode.setChildNode(simpleModelNode);
// 前端模型数据结构 SimpleModel 构建 FlowNode 并添加到 Main Process
traverseNodeToBuildFlowNode(startNode, process);
// 找到 end event
EndEvent endEvent = (EndEvent) CollUtil.findOne(process.getFlowElements(), item -> item instanceof EndEvent);
// 构建并添加节点之间的连线 Sequence Flow
traverseNodeToBuildSequenceFlow(process, startNode, endEvent.getId());
// 自动布局
new BpmnAutoLayout(bpmnModel).execute();
return bpmnModel;
}
private static BpmSimpleModelNodeVO buildStartSimpleModelNode() {
BpmSimpleModelNodeVO startNode = new BpmSimpleModelNodeVO();
startNode.setId(START_EVENT_NODE_ID);
startNode.setName(START_EVENT_NODE_NAME);
startNode.setType(START_NODE.getType());
return startNode;
}
// TODO @芋艿在优化下这个注释
private static void traverseNodeToBuildSequenceFlow(Process process, BpmSimpleModelNodeVO node, String targetNodeId) {
// 1.1 无效节点返回
if (!isValidNode(node)) {
return;
}
// 1.2 END_NODE 直接返回
BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(node.getType());
Assert.notNull(nodeType, "模型节点类型不支持");
if (nodeType == END_NODE) {
return;
}
// 2.1 情况一普通节点
BpmSimpleModelNodeVO childNode = node.getChildNode();
if (!BpmSimpleModelNodeType.isBranchNode(node.getType())) {
if (!isValidNode(childNode)) {
// 2.1.1 普通节点且无孩子节点分两种情况
// a.结束节点 b. 条件分支的最后一个节点.与分支节点的孩子节点或聚合节点建立连线
if (StrUtil.isNotEmpty(node.getAttachNodeId())) {
// 2.1.1.1 如果有附加节点. 需要先建立和附加节点的连线再建立附加节点和目标节点的连线
List<SequenceFlow> sequenceFlows = buildAttachNodeSequenceFlow(node.getId(), node.getAttachNodeId(), targetNodeId);
sequenceFlows.forEach(process::addFlowElement);
} else {
SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), targetNodeId, null, null, null);
process.addFlowElement(sequenceFlow);
}
} else {
// 2.1.2 普通节点且有孩子节点建立连线
if (StrUtil.isNotEmpty(node.getAttachNodeId())) {
// 2.1.1.2 如果有附加节点. 需要先建立和附加节点的连线再建立附加节点和目标节点的连线
List<SequenceFlow> sequenceFlows = buildAttachNodeSequenceFlow(node.getId(), node.getAttachNodeId(), childNode.getId());
sequenceFlows.forEach(process::addFlowElement);
} else {
SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), childNode.getId(), null, null, null);
process.addFlowElement(sequenceFlow);
}
// 递归调用后续节点
traverseNodeToBuildSequenceFlow(process, childNode, targetNodeId);
}
} else {
// 2.2 情况二分支节点
List<BpmSimpleModelNodeVO> conditionNodes = node.getConditionNodes();
Assert.notEmpty(conditionNodes, "分支节点的条件节点不能为空");
// 分支终点节点 Id
String branchEndNodeId = null;
if (nodeType == CONDITION_BRANCH_NODE) { // 条件分支
// 分两种情况 1. 分支节点有孩子节点为孩子节点 Id 2. 分支节点孩子为无效节点时 (分支嵌套且为分支最后一个节点) 为分支终点节点Id
branchEndNodeId = isValidNode(childNode) ? childNode.getId() : targetNodeId;
} else if (nodeType == PARALLEL_BRANCH_NODE) { // 并行分支
// 分支节点分支终点节点 Id 为程序创建的网关集合节点目前不会从前端传入
branchEndNodeId = node.getId() + JOIN_GATE_WAY_NODE_ID_SUFFIX;
}
// TODO 包容网关待实现
Assert.notEmpty(branchEndNodeId, "分支终点节点 Id 不能为空");
// 3.1 遍历分支节点. 如下情况:
// 分支1A->B->C->D->E 分支2A->D->E A为分支节点, D为A孩子节点
for (BpmSimpleModelNodeVO item : conditionNodes) {
// TODO @jason条件分支的情况下需要分 item 搞的条件 conditionNodes 搞的条件
// @芋艿 这个是啥意思 这里的 item 的节点类型为 BpmSimpleModelNodeType.CONDITION_NODE 类型没有对应的 bpmn 的节点 仅仅用于构建条件表达式
Assert.isTrue(Objects.equals(item.getType(), CONDITION_NODE.getType()), "条件节点类型不符合");
// 构建表达式,可以为空. 并行分支为空
String conditionExpression = buildConditionExpression(item);
BpmSimpleModelNodeVO nextNodeOnCondition = item.getChildNode();
// 3.2 分支有后续节点, 分支1: A->B->C->D
if (isValidNode(nextNodeOnCondition)) {
// 3.2.1 建立 A->B
SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), nextNodeOnCondition.getId(),
item.getId(), item.getName(), conditionExpression);
process.addFlowElement(sequenceFlow);
// 3.2.2 递归调用后续节点连线 建立 B->C->D 的连线
traverseNodeToBuildSequenceFlow(process, nextNodeOnCondition, branchEndNodeId);
} else {
// 3.3 分支无后续节点 建立 A->D
SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), branchEndNodeId,
item.getId(), item.getName(), conditionExpression);
process.addFlowElement(sequenceFlow);
}
}
// 如果是并行分支由于是程序创建的聚合网关需要手工创建聚合网关和下一个节点的连线
if (nodeType == PARALLEL_BRANCH_NODE) {
String nextNodeId = isValidNode(childNode) ? childNode.getId() : targetNodeId;
SequenceFlow sequenceFlow = buildBpmnSequenceFlow(branchEndNodeId, nextNodeId, null, null, null);
process.addFlowElement(sequenceFlow);
}
// 4.递归调用后续节点 继续递归建立 D->E 的连线
traverseNodeToBuildSequenceFlow(process, childNode, targetNodeId);
}
}
/**
* 构建有附加节点的连线
*
* @param nodeId 当前节点 Id
* @param attachNodeId 附属节点 Id
* @param targetNodeId 目标节点 Id
*/
private static List<SequenceFlow> buildAttachNodeSequenceFlow(String nodeId, String attachNodeId, String targetNodeId) {
SequenceFlow sequenceFlow = buildBpmnSequenceFlow(nodeId, attachNodeId, null, null, null);
SequenceFlow attachSequenceFlow = buildBpmnSequenceFlow(attachNodeId, targetNodeId, null, null, null);
return CollUtil.newArrayList(sequenceFlow, attachSequenceFlow);
}
/**
* 构造条件表达式
*
* @param conditionNode 条件节点
*/
public static String buildConditionExpression(BpmSimpleModelNodeVO conditionNode) {
BpmSimpleModeConditionType conditionTypeEnum = BpmSimpleModeConditionType.valueOf(conditionNode.getConditionType());
String conditionExpression = null;
if (conditionTypeEnum == BpmSimpleModeConditionType.EXPRESSION) {
conditionExpression = conditionNode.getConditionExpression();
} else if (conditionTypeEnum == BpmSimpleModeConditionType.RULE) {
ConditionGroups conditionGroups = conditionNode.getConditionGroups();
if (conditionGroups != null && CollUtil.isNotEmpty(conditionGroups.getConditions())) {
List<String> strConditionGroups = conditionGroups.getConditions().stream().map(item -> {
if (CollUtil.isNotEmpty(item.getRules())) {
Boolean and = item.getAnd();
List<String> list = CollectionUtils.convertList(item.getRules(), (rule) -> {
// 如果非数值类型加引号
String rightSide = NumberUtil.isNumber(rule.getRightSide()) ? rule.getRightSide() : "\"" + rule.getRightSide() + "\"";
return String.format(" %s %s var:convertByType(%s,%s)", rule.getLeftSide(), rule.getOpCode(), rule.getLeftSide(), rightSide);
});
return "(" + CollUtil.join(list, and ? " && " : " || ") + ")";
} else {
return "";
}
}).toList();
conditionExpression = String.format("${%s}", CollUtil.join(strConditionGroups, conditionGroups.getAnd() ? " && " : " || "));
}
}
// TODO 待增加其它类型
return conditionExpression;
}
private static SequenceFlow buildBpmnSequenceFlow(String sourceId, String targetId, String seqFlowId, String seqName, String conditionExpression) {
Assert.notEmpty(sourceId, "sourceId 不能为空");
Assert.notEmpty(targetId, "targetId 不能为空");
// TODO @jason如果 seqFlowId 不存在的时候是不是要生成一个默认的 seqFlowId @芋艿 貌似不需要,Flowable 会默认生成
// TODO @jason如果 name 不存在的时候是不是要生成一个默认的 name @芋艿 不需要生成默认的吧 这个会在流程图展示的 一般用户填写的不好生成默认的吧
SequenceFlow sequenceFlow = new SequenceFlow(sourceId, targetId);
if (StrUtil.isNotEmpty(conditionExpression)) {
sequenceFlow.setConditionExpression(conditionExpression);
}
if (StrUtil.isNotEmpty(seqFlowId)) {
sequenceFlow.setId(seqFlowId);
}
if (StrUtil.isNotEmpty(seqName)) {
sequenceFlow.setName(seqName);
}
return sequenceFlow;
}
// TODO @芋艿 改成了 traverseNodeToBuildFlowNode 连线的叫 traverseNodeToBuildSequenceFlow
private static void traverseNodeToBuildFlowNode(BpmSimpleModelNodeVO node, Process process) {
// 判断是否有效节点
if (!isValidNode(node)) {
return;
}
BpmSimpleModelNodeType nodeType = BpmSimpleModelNodeType.valueOf(node.getType());
Assert.notNull(nodeType, "模型节点类型不支持");
List<FlowElement> flowElements = buildFlowNode(node, nodeType);
flowElements.forEach(process::addFlowElement);
// 如果不是网关类型的接口 并且chileNode为空退出
// 如果是分支节点则递归处理条件
if (BpmSimpleModelNodeType.isBranchNode(node.getType())
&& ArrayUtil.isNotEmpty(node.getConditionNodes())) {
node.getConditionNodes().forEach(item -> traverseNodeToBuildFlowNode(item.getChildNode(), process));
}
// 如果有节点则递归处理子节点
traverseNodeToBuildFlowNode(node.getChildNode(), process);
}
public static boolean isValidNode(BpmSimpleModelNodeVO node) {
return node != null && node.getId() != null;
}
public static boolean isSequentialApproveNode(BpmSimpleModelNodeVO node) {
return APPROVE_NODE.getType().equals(node.getType()) && SEQUENTIAL.getMethod().equals(node.getApproveMethod());
}
private static List<FlowElement> buildFlowNode(BpmSimpleModelNodeVO node, BpmSimpleModelNodeType nodeType) {
List<FlowElement> list = new ArrayList<>();
switch (nodeType) {
case START_NODE: { // 开始节点
StartEvent startEvent = convertStartNode(node);
list.add(startEvent);
break;
}
case END_NODE: { // 结束节点
EndEvent endEvent = convertEndNode(node);
list.add(endEvent);
break;
}
case START_USER_NODE: { // 发起人节点
UserTask userTask = convertStartUserNode(node);
list.add(userTask);
break;
}
case APPROVE_NODE: { // 审批节点
List<FlowElement> flowElements = convertApproveNode(node);
list.addAll(flowElements);
break;
}
case COPY_NODE: { // 抄送节点
ServiceTask serviceTask = convertCopyNode(node);
list.add(serviceTask);
break;
}
case CONDITION_BRANCH_NODE: {
ExclusiveGateway exclusiveGateway = convertConditionBranchNode(node);
list.add(exclusiveGateway);
break;
}
case PARALLEL_BRANCH_NODE: {
List<ParallelGateway> parallelGateways = convertParallelBranchNode(node);
list.addAll(parallelGateways);
break;
}
case INCLUSIVE_BRANCH_NODE: {
// TODO jason 待实现
break;
}
default: {
// TODO 其它节点类型的实现
}
}
return list;
}
private static UserTask convertStartUserNode(BpmSimpleModelNodeVO node) {
return buildBpmnStartUserTask(node);
}
private static List<FlowElement> convertApproveNode(BpmSimpleModelNodeVO node) {
List<FlowElement> flowElements = new ArrayList<>();
UserTask userTask = buildBpmnUserTask(node);
flowElements.add(userTask);
// 添加用户任务的 Timer Boundary Event, 用于任务的审批超时处理
if (node.getTimeoutHandler() != null && node.getTimeoutHandler().getEnable()) {
BoundaryEvent boundaryEvent = buildUserTaskTimeoutBoundaryEvent(userTask, node.getTimeoutHandler());
flowElements.add(boundaryEvent);
}
return flowElements;
}
/**
* 添加 UserTask 用户的审批超时 BoundaryEvent 事件
*
* @param userTask 审批任务
* @param timeoutHandler 超时处理器
* @return BoundaryEvent 超时事件
*/
private static BoundaryEvent buildUserTaskTimeoutBoundaryEvent(UserTask userTask, TimeoutHandler timeoutHandler) {
// 1.1 定时器边界事件
BoundaryEvent boundaryEvent = new BoundaryEvent();
boundaryEvent.setId("Event-" + IdUtil.fastUUID());
boundaryEvent.setCancelActivity(false); // 设置关联的任务为不会被中断
boundaryEvent.setAttachedToRef(userTask);
// 1.2 定义超时时间最大提醒次数
TimerEventDefinition eventDefinition = new TimerEventDefinition();
eventDefinition.setTimeDuration(timeoutHandler.getTimeDuration());
if (Objects.equals(REMINDER.getType(), timeoutHandler.getType()) &&
timeoutHandler.getMaxRemindCount() != null && timeoutHandler.getMaxRemindCount() > 1) {
eventDefinition.setTimeCycle(String.format("R%d/%s",
timeoutHandler.getMaxRemindCount(), timeoutHandler.getTimeDuration()));
}
boundaryEvent.addEventDefinition(eventDefinition);
// 2.1 添加定时器边界事件类型
addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, BpmBoundaryEventType.USER_TASK_TIMEOUT.getType().toString());
// 2.2 添加超时执行动作元素
addExtensionElement(boundaryEvent, USER_TASK_TIMEOUT_HANDLER_TYPE, StrUtil.toStringOrNull(timeoutHandler.getType()));
return boundaryEvent;
}
private static List<ParallelGateway> convertParallelBranchNode(BpmSimpleModelNodeVO node) {
ParallelGateway parallelGateway = new ParallelGateway();
parallelGateway.setId(node.getId());
// TODO @jasonsetName
// TODO @芋艿 + jason合并网关是不是要有条件啥的微信讨论
// 并行聚合网关有程序创建前端不需要传入
ParallelGateway joinParallelGateway = new ParallelGateway();
joinParallelGateway.setId(node.getId() + JOIN_GATE_WAY_NODE_ID_SUFFIX);
return CollUtil.newArrayList(parallelGateway, joinParallelGateway);
}
private static ServiceTask convertCopyNode(BpmSimpleModelNodeVO node) {
ServiceTask serviceTask = new ServiceTask();
serviceTask.setId(node.getId());
serviceTask.setName(node.getName());
serviceTask.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
serviceTask.setImplementation("${" + BpmCopyTaskDelegate.BEAN_NAME + "}");
// 添加抄送候选人元素
addCandidateElements(node.getCandidateStrategy(), node.getCandidateParam(), serviceTask);
// 添加表单字段权限属性元素
addFormFieldsPermission(node.getFieldsPermission(), serviceTask);
return serviceTask;
}
/**
* 给节点添加候选人元素
*/
private static void addCandidateElements(Integer candidateStrategy, String candidateParam, FlowElement flowElement) {
addExtensionElement(flowElement, BpmnModelConstants.USER_TASK_CANDIDATE_STRATEGY,
candidateStrategy == null ? null : candidateStrategy.toString());
addExtensionElement(flowElement, BpmnModelConstants.USER_TASK_CANDIDATE_PARAM, candidateParam);
}
private static ExclusiveGateway convertConditionBranchNode(BpmSimpleModelNodeVO node) {
Assert.notEmpty(node.getConditionNodes(), "条件分支节点不能为空");
ExclusiveGateway exclusiveGateway = new ExclusiveGateway();
exclusiveGateway.setId(node.getId());
// 寻找默认的序列流
BpmSimpleModelNodeVO defaultSeqFlow = CollUtil.findOne(node.getConditionNodes(),
item -> BooleanUtil.isTrue(item.getDefaultFlow()));
if (defaultSeqFlow != null) {
exclusiveGateway.setDefaultFlow(defaultSeqFlow.getId());
}
return exclusiveGateway;
}
private static InclusiveGateway convertInclusiveBranchNode(BpmSimpleModelNodeVO node, Boolean isFork) {
InclusiveGateway inclusiveGateway = new InclusiveGateway();
inclusiveGateway.setId(node.getId());
// TODO @jason这里是不是 setName
// TODO @芋艿 + jason是不是搞个合并网关这里微信讨论下有点奇怪
// @芋艿 isFork false 就是合并网关由前端传入这个前端暂时还未实现
if (isFork) {
Assert.notEmpty(node.getConditionNodes(), "条件节点不能为空");
// 寻找默认的序列流
BpmSimpleModelNodeVO defaultSeqFlow = CollUtil.findOne(
node.getConditionNodes(), item -> BooleanUtil.isTrue(item.getDefaultFlow()));
if (defaultSeqFlow != null) {
inclusiveGateway.setDefaultFlow(defaultSeqFlow.getId());
}
}
return inclusiveGateway;
}
private static UserTask buildBpmnUserTask(BpmSimpleModelNodeVO node) {
UserTask userTask = new UserTask();
userTask.setId(node.getId());
userTask.setName(node.getName());
// 如果不是审批人节点则直接返回
addExtensionElement(userTask, USER_TASK_APPROVE_TYPE, StrUtil.toStringOrNull(node.getApproveType()));
if (ObjectUtil.notEqual(node.getApproveType(), USER.getType())) {
return userTask;
}
// 添加候选人元素
addCandidateElements(node.getCandidateStrategy(), node.getCandidateParam(), userTask);
// 添加表单字段权限属性元素
addFormFieldsPermission(node.getFieldsPermission(), userTask);
// 添加操作按钮配置属性元素
addButtonsSetting(node.getButtonsSetting(), userTask);
// 处理多实例审批方式
processMultiInstanceLoopCharacteristics(node.getApproveMethod(), node.getApproveRatio(), userTask);
// 添加任务被拒绝的处理元素
addTaskRejectElements(node.getRejectHandler(), userTask);
// 添加用户任务的审批人与发起人相同时的处理元素
addAssignStartUserHandlerType(node.getAssignStartUserHandlerType(), userTask);
// 添加用户任务的空处理元素
addAssignEmptyHandlerType(node.getAssignEmptyHandler(), userTask);
// 设置审批任务的截止时间
if (node.getTimeoutHandler() != null && node.getTimeoutHandler().getEnable()) {
userTask.setDueDate(node.getTimeoutHandler().getTimeDuration());
}
return userTask;
}
private static void addTaskRejectElements(RejectHandler rejectHandler, UserTask userTask) {
if (rejectHandler == null) {
return;
}
addExtensionElement(userTask, USER_TASK_REJECT_HANDLER_TYPE, StrUtil.toStringOrNull(rejectHandler.getType()));
addExtensionElement(userTask, USER_TASK_REJECT_RETURN_TASK_ID, rejectHandler.getReturnNodeId());
}
private static void addAssignStartUserHandlerType(Integer assignStartUserHandlerType, UserTask userTask) {
if (assignStartUserHandlerType == null) {
return;
}
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) {
BpmUserTaskApproveMethodEnum approveMethodEnum = BpmUserTaskApproveMethodEnum.valueOf(approveMethod);
// TODO @jason这种枚举最终不要去掉哈 BpmUserTaskApproveMethodEnum因为容易不经意重叠
if (approveMethodEnum == null || approveMethodEnum == RANDOM) {
return;
}
// 添加审批方式的扩展属性
addExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_METHOD,
approveMethod == null ? null : approveMethod.toString());
MultiInstanceLoopCharacteristics multiInstanceCharacteristics = new MultiInstanceLoopCharacteristics();
// 设置 collectionVariable本系统用不到仅仅为了 Flowable 校验不报错
multiInstanceCharacteristics.setInputDataItem("${coll_userList}");
if (approveMethodEnum == BpmUserTaskApproveMethodEnum.ANY) {
multiInstanceCharacteristics.setCompletionCondition(ANY_OF_APPROVE_COMPLETE_EXPRESSION);
multiInstanceCharacteristics.setSequential(false);
userTask.setLoopCharacteristics(multiInstanceCharacteristics);
} else if (approveMethodEnum == SEQUENTIAL) {
multiInstanceCharacteristics.setCompletionCondition(ALL_APPROVE_COMPLETE_EXPRESSION);
multiInstanceCharacteristics.setSequential(true);
multiInstanceCharacteristics.setLoopCardinality("1");
userTask.setLoopCharacteristics(multiInstanceCharacteristics);
} else if (approveMethodEnum == RATIO) {
Assert.notNull(approveRatio, "通过比例不能为空");
multiInstanceCharacteristics.setCompletionCondition(
String.format(APPROVE_BY_RATIO_COMPLETE_EXPRESSION, String.format("%.2f", approveRatio / (double) 100)));
multiInstanceCharacteristics.setSequential(false);
}
userTask.setLoopCharacteristics(multiInstanceCharacteristics);
}
/**
* 给节点添加操作按钮设置元素
*/
private static void addButtonsSetting(List<OperationButtonSetting> buttonsSetting, UserTask userTask) {
if (CollUtil.isNotEmpty(buttonsSetting)) {
List<Map<String, String>> list = CollectionUtils.convertList(buttonsSetting, item -> {
Map<String, String> settingMap = MapUtil.newHashMap(16);
settingMap.put(BUTTON_SETTING_ELEMENT_ID_ATTRIBUTE, String.valueOf(item.getId()));
settingMap.put(BUTTON_SETTING_ELEMENT_DISPLAY_NAME_ATTRIBUTE, item.getDisplayName());
settingMap.put(BUTTON_SETTING_ELEMENT_ENABLE_ATTRIBUTE, String.valueOf(item.getEnable()));
return settingMap;
});
list.forEach(item -> addExtensionElement(userTask, BUTTON_SETTING_ELEMENT, item));
}
}
/**
* 给节点添加表单字段权限元素
*/
private static void addFormFieldsPermission(List<Map<String, String>> fieldsPermissions, FlowElement flowElement) {
if (CollUtil.isNotEmpty(fieldsPermissions)) {
fieldsPermissions.forEach(item -> addExtensionElement(flowElement, FORM_FIELD_PERMISSION_ELEMENT, item));
}
}
private static void addExtensionElement(FlowElement element, String name, Map<String, String> attributes) {
if (attributes == null) {
return;
}
ExtensionElement extensionElement = new ExtensionElement();
extensionElement.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE);
extensionElement.setNamespacePrefix(FLOWABLE_EXTENSIONS_PREFIX);
extensionElement.setName(name);
attributes.forEach((key, value) -> {
ExtensionAttribute extensionAttribute = new ExtensionAttribute(key, value);
extensionAttribute.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE);
extensionElement.addAttribute(extensionAttribute);
});
element.addExtensionElement(extensionElement);
}
private static void addExtensionElement(FlowElement element, String name, String value) {
if (value == null) {
return;
}
ExtensionElement extensionElement = new ExtensionElement();
extensionElement.setNamespace(FLOWABLE_EXTENSIONS_NAMESPACE);
extensionElement.setNamespacePrefix(FLOWABLE_EXTENSIONS_PREFIX);
extensionElement.setElementText(value);
extensionElement.setName(name);
element.addExtensionElement(extensionElement);
}
// ========== 各种 build 节点的方法 ==========
private static StartEvent convertStartNode(BpmSimpleModelNodeVO node) {
StartEvent startEvent = new StartEvent();
startEvent.setId(node.getId());
startEvent.setName(node.getName());
return startEvent;
}
private static UserTask buildBpmnStartUserTask(BpmSimpleModelNodeVO node) {
UserTask userTask = new UserTask();
userTask.setId(node.getId());
userTask.setName(node.getName());
// 人工审批
addExtensionElement(userTask, USER_TASK_APPROVE_TYPE, USER.getType().toString());
// 候选人策略为发起人自己
addCandidateElements(START_USER.getStrategy(), null, userTask);
// 添加表单字段权限属性元素
addFormFieldsPermission(node.getFieldsPermission(), userTask);
// 添加操作按钮配置属性元素
addButtonsSetting(node.getButtonsSetting(), userTask);
// 使用自动通过策略 TODO @芋艿 复用了SKIP 是否需要新加一个策略TODO @芋艿回复是不是应该类似飞书搞个草稿状态待定还有一种策略不标记自动通过而是首次发起后第一个节点自动通过
addAssignStartUserHandlerType(SKIP.getType(), userTask);
return userTask;
}
private static EndEvent convertEndNode(BpmSimpleModelNodeVO node) {
EndEvent endEvent = new EndEvent();
endEvent.setId(node.getId());
endEvent.setName(node.getName());
// TODO @芋艿 + jason要不要加一个终止定义
return endEvent;
}
}

View File

@ -1,7 +1,10 @@
package cn.iocoder.yudao.module.bpm.service.definition;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.*;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelPageReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelUpdateReqVO;
import jakarta.validation.Valid;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.engine.repository.Model;
@ -25,10 +28,9 @@ public interface BpmModelService {
* 创建流程模型
*
* @param modelVO 创建信息
* @param bpmnXml BPMN XML
* @return 创建的流程模型的编号
*/
String createModel(@Valid BpmModelCreateReqVO modelVO, String bpmnXml);
String createModel(@Valid BpmModelSaveReqVO modelVO);
/**
* 获得流程模块
@ -46,34 +48,46 @@ public interface BpmModelService {
*/
byte[] getModelBpmnXML(String id);
/**
* 修改流程模型的 BPMN XML
*
* @param id 编号
* @param bpmnXml BPMN XML
*/
void updateModelBpmnXml(String id, String bpmnXml);
/**
* 修改流程模型
*
* @param userId 用户编号
* @param updateReqVO 更新信息
*/
void updateModel(@Valid BpmModelUpdateReqVO updateReqVO);
void updateModel(Long userId, @Valid BpmModelSaveReqVO updateReqVO);
/**
* 将流程模型部署成一个流程定义
*
* @param userId 用户编号
* @param id 编号
*/
void deployModel(String id);
void deployModel(Long userId, String id);
/**
* 删除模型
*
* @param userId 用户编号
* @param id 编号
*/
void deleteModel(String id);
void deleteModel(Long userId, String id);
/**
* 修改模型的状态实际更新的部署的流程定义的状态
*
* @param userId 用户编号
* @param id 编号
* @param state 状态
*/
void updateModelState(String id, Integer state);
void updateModelState(Long userId, String id, Integer state);
/**
* 获得流程定义编号对应的 BPMN Model
@ -83,4 +97,22 @@ public interface BpmModelService {
*/
BpmnModel getBpmnModelByDefinitionId(String processDefinitionId);
// ========== 仿钉钉/飞书的精简模型 =========
/**
* 获取仿钉钉流程设计模型结构
*
* @param modelId 流程模型编号
* @return 仿钉钉流程设计模型结构
*/
BpmSimpleModelNodeVO getSimpleModel(String modelId);
/**
* 更新仿钉钉流程设计模型
*
* @param userId 用户编号
* @param reqVO 请求信息
*/
void updateSimpleModel(Long userId, @Valid BpmSimpleModelUpdateReqVO reqVO);
}

View File

@ -1,20 +1,24 @@
package cn.iocoder.yudao.module.bpm.service.definition;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.object.PageUtils;
import cn.iocoder.yudao.framework.common.util.validation.ValidationUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelCreateReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelPageReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelUpdateReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelSaveReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelUpdateReqVO;
import cn.iocoder.yudao.module.bpm.convert.definition.BpmModelConvert;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmFormDO;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
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.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
@ -28,7 +32,6 @@ import org.flowable.engine.repository.ModelQuery;
import org.flowable.engine.repository.ProcessDefinition;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import org.springframework.validation.annotation.Validated;
import java.util.List;
@ -86,63 +89,79 @@ public class BpmModelServiceImpl implements BpmModelService {
@Override
@Transactional(rollbackFor = Exception.class)
public String createModel(@Valid BpmModelCreateReqVO createReqVO, String bpmnXml) {
public String createModel(@Valid BpmModelSaveReqVO createReqVO) {
if (!ValidationUtils.isXmlNCName(createReqVO.getKey())) {
throw exception(MODEL_KEY_VALID);
}
// 校验流程标识已经存在
// 1. 校验流程标识已经存在
Model keyModel = getModelByKey(createReqVO.getKey());
if (keyModel != null) {
throw exception(MODEL_KEY_EXISTS, createReqVO.getKey());
}
// 创建流程定义
// 2.1 创建流程定义
Model model = repositoryService.newModel();
BpmModelConvert.INSTANCE.copyToCreateModel(model, createReqVO);
BpmModelConvert.INSTANCE.copyToModel(model, createReqVO);
model.setTenantId(FlowableUtils.getTenantId());
// 保存流程定义
// 2.2 保存流程定义
repositoryService.saveModel(model);
// 保存 BPMN XML
saveModelBpmnXml(model, bpmnXml);
return model.getId();
}
@Override
@Transactional(rollbackFor = Exception.class) // 因为进行多个操作所以开启事务
public void updateModel(@Valid BpmModelUpdateReqVO updateReqVO) {
// 校验流程模型存在
Model model = getModel(updateReqVO.getId());
public void updateModel(Long userId, @Valid BpmModelSaveReqVO updateReqVO) {
// 1. 校验流程模型存在
Model model = validateModelManager(updateReqVO.getId(), userId);
// 修改流程定义
BpmModelConvert.INSTANCE.copyToModel(model, updateReqVO);
// 更新模型
repositoryService.saveModel(model);
}
private Model validateModelExists(String id) {
Model model = repositoryService.getModel(id);
if (model == null) {
throw exception(MODEL_NOT_EXISTS);
}
return model;
}
// 修改流程定义
BpmModelConvert.INSTANCE.copyToUpdateModel(model, updateReqVO);
// 更新模型
repositoryService.saveModel(model);
// 更新 BPMN XML
saveModelBpmnXml(model, updateReqVO.getBpmnXml());
/**
* 校验是否有流程模型的管理权限
*
* @param id 流程模型编号
* @param userId 用户编号
* @return 流程模型
*/
private Model validateModelManager(String id, Long userId) {
Model model = validateModelExists(id);
BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model);
if (metaInfo == null || !CollUtil.contains(metaInfo.getManagerUserIds(), userId)) {
throw exception(MODEL_UPDATE_FAIL_NOT_MANAGER);
}
return model;
}
@Override
@Transactional(rollbackFor = Exception.class) // 因为进行多个操作所以开启事务
public void deployModel(String id) {
public void deployModel(Long userId, String id) {
// 1.1 校验流程模型存在
Model model = getModel(id);
if (ObjectUtils.isEmpty(model)) {
throw exception(MODEL_NOT_EXISTS);
}
Model model = validateModelManager(id, userId);
// 1.2 校验流程图
byte[] bpmnBytes = getModelBpmnXML(model.getId());
validateBpmnXml(bpmnBytes);
// 1.3 校验表单已配
BpmModelMetaInfoRespDTO metaInfo = JsonUtils.parseObject(model.getMetaInfo(), BpmModelMetaInfoRespDTO.class);
BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model);
BpmFormDO form = validateFormConfig(metaInfo);
// 1.4 校验任务分配规则已配置
taskCandidateInvoker.validateBpmnConfig(bpmnBytes);
// 1.5 获取仿钉钉流程设计器模型数据
byte[] simpleBytes = getModelSimpleJson(model.getId());
// 2.1 创建流程定义
String definitionId = processDefinitionService.createProcessDefinition(model, metaInfo, bpmnBytes, form);
String definitionId = processDefinitionService.createProcessDefinition(model, metaInfo, bpmnBytes, simpleBytes, form);
// 2.2 将老的流程定义进行挂起也就是说只有最新部署的流程定义才可以发起任务
updateProcessDefinitionSuspended(model.getDeploymentId());
@ -174,12 +193,10 @@ public class BpmModelServiceImpl implements BpmModelService {
@Override
@Transactional(rollbackFor = Exception.class)
public void deleteModel(String id) {
public void deleteModel(Long userId, String id) {
// 校验流程模型存在
Model model = getModel(id);
if (model == null) {
throw exception(MODEL_NOT_EXISTS);
}
Model model = validateModelManager(id, userId);
// 执行删除
repositoryService.deleteModel(id);
// 禁用流程定义
@ -187,12 +204,9 @@ public class BpmModelServiceImpl implements BpmModelService {
}
@Override
public void updateModelState(String id, Integer state) {
public void updateModelState(Long userId, String id, Integer state) {
// 1.1 校验流程模型存在
Model model = getModel(id);
if (model == null) {
throw exception(MODEL_NOT_EXISTS);
}
Model model = validateModelManager(id, userId);
// 1.2 校验流程定义存在
ProcessDefinition definition = processDefinitionService.getProcessDefinitionByDeploymentId(model.getDeploymentId());
if (definition == null) {
@ -208,13 +222,34 @@ public class BpmModelServiceImpl implements BpmModelService {
return repositoryService.getBpmnModel(processDefinitionId);
}
@Override
public BpmSimpleModelNodeVO getSimpleModel(String modelId) {
Model model = validateModelExists(modelId);
// 通过 ACT_RE_MODEL EDITOR_SOURCE_EXTRA_VALUE_ID_ 获取仿钉钉快搭模型的 JSON 数据
byte[] jsonBytes = getModelSimpleJson(model.getId());
return JsonUtils.parseObject(jsonBytes, BpmSimpleModelNodeVO.class);
}
@Override
public void updateSimpleModel(Long userId, BpmSimpleModelUpdateReqVO reqVO) {
// 1. 校验流程模型存在
Model model = validateModelManager(reqVO.getId(), userId);
// 2.1 JSON 转换成 bpmnModel
BpmnModel bpmnModel = SimpleModelUtils.buildBpmnModel(model.getKey(), model.getName(), reqVO.getSimpleModel());
// 2.2 保存 Bpmn XML
updateModelBpmnXml(model.getId(), BpmnModelUtils.getBpmnXml(bpmnModel));
// 2.3 保存 JSON 数据
saveModelSimpleJson(model.getId(), JsonUtils.toJsonByte(reqVO.getSimpleModel()));
}
/**
* 校验流程表单已配置
*
* @param metaInfo 流程模型元数据
* @return 表单配置
*/
private BpmFormDO validateFormConfig(BpmModelMetaInfoRespDTO metaInfo) {
private BpmFormDO validateFormConfig(BpmModelMetaInfoVO metaInfo) {
if (metaInfo == null || metaInfo.getFormType() == null) {
throw exception(MODEL_DEPLOY_FAIL_FORM_NOT_CONFIG);
}
@ -236,16 +271,28 @@ public class BpmModelServiceImpl implements BpmModelService {
}
}
private void saveModelBpmnXml(Model model, String bpmnXml) {
@Override
public void updateModelBpmnXml(String id, String bpmnXml) {
if (StrUtil.isEmpty(bpmnXml)) {
return;
}
repositoryService.addModelEditorSource(model.getId(), StrUtil.utf8Bytes(bpmnXml));
repositoryService.addModelEditorSource(id, StrUtil.utf8Bytes(bpmnXml));
}
private byte[] getModelSimpleJson(String id) {
return repositoryService.getModelEditorSourceExtra(id);
}
private void saveModelSimpleJson(String id, byte[] jsonBytes) {
if (ArrayUtil.isEmpty(jsonBytes)) {
return;
}
repositoryService.addModelEditorSourceExtra(id, jsonBytes);
}
/**
* 挂起 deploymentId 对应的流程定义
*
* <p>
* 注意这里一个 deploymentId 只关联一个流程定义
*
* @param deploymentId 流程发布Id

View File

@ -4,7 +4,7 @@ import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageReqVO;
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 cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.Model;
@ -48,10 +48,12 @@ public interface BpmProcessDefinitionService {
* @param model 流程模型
* @param modelMetaInfo 流程模型元信息
* @param bpmnBytes BPMN XML 字节数组
* @param simpleBytes SIMPLE Model JSON 字节数组
* @param form 表单
* @return 流程编号
*/
String createProcessDefinition(Model model, BpmModelMetaInfoRespDTO modelMetaInfo, byte[] bpmnBytes, BpmFormDO form);
String createProcessDefinition(Model model, BpmModelMetaInfoVO modelMetaInfo,
byte[] bpmnBytes, byte[] simpleBytes, BpmFormDO form);
/**
* 更新流程定义状态
@ -133,6 +135,15 @@ public interface BpmProcessDefinitionService {
*/
ProcessDefinition getActiveProcessDefinition(String key);
/**
* 判断用户是否可以使用该流程定义进行流程的发起
*
* @param processDefinition 流程定义
* @param userId 用户编号
* @return 是否可以发起流程
*/
boolean canUserStartProcessDefinition(BpmProcessDefinitionInfoDO processDefinition, Long userId);
/**
* 获得 ids 对应的 Deployment Map
*

View File

@ -5,13 +5,13 @@ import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.framework.common.util.object.PageUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionPageReqVO;
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.dal.mysql.definition.BpmProcessDefinitionInfoMapper;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.bpm.service.definition.dto.BpmModelMetaInfoRespDTO;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.BpmnModel;
@ -24,6 +24,7 @@ import org.flowable.engine.repository.ProcessDefinitionQuery;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.nio.charset.StandardCharsets;
import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@ -84,6 +85,19 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
.processDefinitionKey(key).active().singleResult();
}
@Override
public boolean canUserStartProcessDefinition(BpmProcessDefinitionInfoDO processDefinition, Long userId) {
if (processDefinition == null) {
return false;
}
// 为空则所有人都可以发起
if (CollUtil.isEmpty(processDefinition.getStartUserIds())) {
return true;
}
// 不为空则需要存在里面
return processDefinition.getStartUserIds().contains(userId);
}
@Override
public List<Deployment> getDeploymentList(Set<String> ids) {
if (CollUtil.isEmpty(ids)) {
@ -105,8 +119,8 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
}
@Override
public String createProcessDefinition(Model model, BpmModelMetaInfoRespDTO modelMetaInfo,
byte[] bpmnBytes, BpmFormDO form) {
public String createProcessDefinition(Model model, BpmModelMetaInfoVO modelMetaInfo,
byte[] bpmnBytes, byte[] simpleBytes, BpmFormDO form) {
// 创建 Deployment 部署
Deployment deploy = repositoryService.createDeployment()
.key(model.getKey()).name(model.getName()).category(model.getCategory())
@ -131,7 +145,9 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
// 插入拓展表
BpmProcessDefinitionInfoDO definitionDO = BeanUtils.toBean(modelMetaInfo, BpmProcessDefinitionInfoDO.class)
.setModelId(model.getId()).setProcessDefinitionId(definition.getId());
.setModelId(model.getId()).setProcessDefinitionId(definition.getId()).setModelType(modelMetaInfo.getType())
.setSimpleModel(StrUtil.str(simpleBytes, StandardCharsets.UTF_8));
if (form != null) {
definitionDO.setFormFields(form.getFields()).setFormConf(form.getConf());
}

View File

@ -1,46 +0,0 @@
package cn.iocoder.yudao.module.bpm.service.definition.dto;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
import lombok.Data;
/**
* BPM 流程 MetaInfo Response DTO
* 主要用于 { Model#setMetaInfo(String)} 的存储
*
* 最终它的字段和 {@link cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO} 是一致的
*
* @author 芋道源码
*/
@Data
public class BpmModelMetaInfoRespDTO {
/**
* 流程图标
*/
private String icon;
/**
* 流程描述
*/
private String description;
/**
* 表单类型
*/
private Integer formType;
/**
* 表单编号
* 在表单类型为 {@link BpmModelFormTypeEnum#NORMAL}
*/
private Long formId;
/**
* 自定义表单的提交路径使用 Vue 的路由地址
* 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM}
*/
private String formCustomCreatePath;
/**
* 自定义表单的查看路径使用 Vue 的路由地址
* 在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM}
*/
private String formCustomViewPath;
}

View File

@ -3,7 +3,7 @@ package cn.iocoder.yudao.module.bpm.service.message;
import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO;
import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceRejectReqDTO;
import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO;
import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskTimeoutReqDTO;
import jakarta.validation.Valid;
/**
@ -36,4 +36,11 @@ public interface BpmMessageService {
*/
void sendMessageWhenTaskAssigned(@Valid BpmMessageSendWhenTaskCreatedReqDTO reqDTO);
/**
* 发送任务审批超时的消息
*
* @param reqDTO 发送信息
*/
void sendMessageWhenTaskTimeout(@Valid BpmMessageSendWhenTaskTimeoutReqDTO reqDTO);
}

View File

@ -6,6 +6,7 @@ import cn.iocoder.yudao.module.bpm.enums.message.BpmMessageEnum;
import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceApproveReqDTO;
import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenProcessInstanceRejectReqDTO;
import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskCreatedReqDTO;
import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskTimeoutReqDTO;
import cn.iocoder.yudao.module.system.api.sms.SmsSendApi;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@ -61,6 +62,16 @@ public class BpmMessageServiceImpl implements BpmMessageService {
BpmMessageEnum.TASK_ASSIGNED.getSmsTemplateCode(), templateParams));
}
@Override
public void sendMessageWhenTaskTimeout(BpmMessageSendWhenTaskTimeoutReqDTO reqDTO) {
Map<String, Object> templateParams = new HashMap<>();
templateParams.put("processInstanceName", reqDTO.getProcessInstanceName());
templateParams.put("taskName", reqDTO.getTaskName());
templateParams.put("detailUrl", getProcessInstanceDetailUrl(reqDTO.getProcessInstanceId()));
smsSendApi.sendSingleSmsToAdmin(BpmMessageConvert.INSTANCE.convert(reqDTO.getAssigneeUserId(),
BpmMessageEnum.TASK_TIMEOUT.getSmsTemplateCode(), templateParams));
}
private String getProcessInstanceDetailUrl(String taskId) {
return webProperties.getAdminUi().getUrl() + "/bpm/process-instance/detail?id=" + taskId;
}

View File

@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.bpm.service.message.dto;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* BPM 发送任务审批超时 Request DTO
*/
@Data
public class BpmMessageSendWhenTaskTimeoutReqDTO {
/**
* 流程实例的编号
*/
@NotEmpty(message = "流程实例的编号不能为空")
private String processInstanceId;
/**
* 流程实例的名字
*/
@NotEmpty(message = "流程实例的名字不能为空")
private String processInstanceName;
/**
* 流程任务的编号
*/
@NotEmpty(message = "流程任务的编号不能为空")
private String taskId;
/**
* 流程任务的名字
*/
@NotEmpty(message = "流程任务的名字不能为空")
private String taskName;
/**
* 审批人的用户编号
*/
@NotNull(message = "审批人的用户编号不能为空")
private Long assigneeUserId;
}

View File

@ -1,6 +1,5 @@
package cn.iocoder.yudao.module.bpm.service.task;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.activity.BpmActivityRespVO;
import org.flowable.engine.history.HistoricActivityInstance;
import java.util.List;
@ -18,7 +17,7 @@ public interface BpmActivityService {
* @param processInstanceId 流程实例的编号
* @return 活动实例列表
*/
List<BpmActivityRespVO> getActivityListByProcessInstanceId(String processInstanceId);
List<HistoricActivityInstance> getActivityListByProcessInstanceId(String processInstanceId);
/**
* 获得执行编号对应的活动实例

View File

@ -1,14 +1,12 @@
package cn.iocoder.yudao.module.bpm.service.task;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.activity.BpmActivityRespVO;
import cn.iocoder.yudao.module.bpm.convert.task.BpmActivityConvert;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.HistoryService;
import org.flowable.engine.history.HistoricActivityInstance;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource;
import java.util.List;
@ -26,10 +24,9 @@ public class BpmActivityServiceImpl implements BpmActivityService {
private HistoryService historyService;
@Override
public List<BpmActivityRespVO> getActivityListByProcessInstanceId(String processInstanceId) {
List<HistoricActivityInstance> activityList = historyService.createHistoricActivityInstanceQuery()
.processInstanceId(processInstanceId).list();
return BpmActivityConvert.INSTANCE.convertList(activityList);
public List<HistoricActivityInstance> getActivityListByProcessInstanceId(String processInstanceId) {
return historyService.createHistoricActivityInstanceQuery().processInstanceId(processInstanceId)
.orderByHistoricActivityInstanceStartTime().asc().list();
}
@Override

View File

@ -5,6 +5,7 @@ import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessI
import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO;
import java.util.Collection;
import java.util.Set;
/**
* 流程抄送 Service 接口
@ -21,6 +22,18 @@ public interface BpmProcessInstanceCopyService {
*/
void createProcessInstanceCopy(Collection<Long> userIds, String taskId);
/**
* 流程实例的抄送
*
* @param userIds 抄送的用户编号
* @param processInstanceId 流程编号
* @param activityId 流程活动编号 id (对应 BPMN XML 节点 Id)
* // TODO 芋艿这个 taskId 是不是可以不要了
* @param taskId 任务编号
* @param taskName 任务名称
*/
void createProcessInstanceCopy(Collection<Long> userIds, String processInstanceId, String activityId, String taskId, String taskName);
/**
* 获得抄送的流程的分页
*
@ -30,5 +43,14 @@ public interface BpmProcessInstanceCopyService {
*/
PageResult<BpmProcessInstanceCopyDO> getProcessInstanceCopyPage(Long userId,
BpmProcessInstanceCopyPageReqVO pageReqVO);
// TODO @芋艿重点在 review
/**
* 通过流程实例和流程活动编号获取抄送人的 Id
*
* @param processInstanceId 流程实例 Id
* @param activityId 流程活动编号 Id
* @return 抄送人 Ids
*/
Set<Long> getCopyUserIds(String processInstanceId, String activityId);
}

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.service.task;
import cn.hutool.core.util.ObjectUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCopyPageReqVO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.task.BpmProcessInstanceCopyDO;
import cn.iocoder.yudao.module.bpm.dal.mysql.task.BpmProcessInstanceCopyMapper;
@ -18,6 +19,7 @@ import org.springframework.validation.annotation.Validated;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList;
@ -48,18 +50,22 @@ public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopy
@Override
public void createProcessInstanceCopy(Collection<Long> userIds, String taskId) {
// 1.1 校验任务存在
Task task = taskService.getTask(taskId);
if (ObjectUtil.isNull(task)) {
throw exception(ErrorCodeConstants.TASK_NOT_EXISTS);
}
// 1.2 校验流程实例存在
String processInstanceId = task.getProcessInstanceId();
createProcessInstanceCopy(userIds, processInstanceId, task.getTaskDefinitionKey(), task.getId(), task.getName());
}
@Override
public void createProcessInstanceCopy(Collection<Long> userIds, String processInstanceId, String activityId, String taskId, String taskName) {
// 1.1 校验流程实例存在
ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId);
if (processInstance == null) {
throw exception(ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS);
}
// 1.3 校验流程定义存在
// 1.2 校验流程定义存在
ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition(
processInstance.getProcessDefinitionId());
if (processDefinition == null) {
@ -70,7 +76,8 @@ public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopy
List<BpmProcessInstanceCopyDO> copyList = convertList(userIds, userId -> new BpmProcessInstanceCopyDO()
.setUserId(userId).setStartUserId(Long.valueOf(processInstance.getStartUserId()))
.setProcessInstanceId(processInstanceId).setProcessInstanceName(processInstance.getName())
.setCategory(processDefinition.getCategory()).setTaskId(taskId).setTaskName(task.getName()));
.setCategory(processDefinition.getCategory()).setActivityId(activityId)
.setTaskId(taskId).setTaskName(taskName));
processInstanceCopyMapper.insertBatch(copyList);
}
@ -80,4 +87,10 @@ public class BpmProcessInstanceCopyServiceImpl implements BpmProcessInstanceCopy
return processInstanceCopyMapper.selectPage(userId, pageReqVO);
}
@Override
public Set<Long> getCopyUserIds(String processInstanceId, String activityId) {
return CollectionUtils.convertSet(processInstanceCopyMapper.selectListByProcessInstanceIdAndActivityId(processInstanceId, activityId),
BpmProcessInstanceCopyDO::getUserId);
}
}

View File

@ -2,11 +2,8 @@ package cn.iocoder.yudao.module.bpm.service.task;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCancelReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstanceCreateReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmProcessInstancePageReqVO;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
import jakarta.validation.Valid;
import org.flowable.engine.delegate.event.FlowableCancelledEvent;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.runtime.ProcessInstance;
@ -23,6 +20,8 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
*/
public interface BpmProcessInstanceService {
// ========== Query 查询相关方法 ==========
/**
* 获得流程实例
*
@ -85,6 +84,28 @@ public interface BpmProcessInstanceService {
PageResult<HistoricProcessInstance> getProcessInstancePage(Long userId,
@Valid BpmProcessInstancePageReqVO pageReqVO);
/**
* 获得表单字段权限
*
* @param reqVO 请求消息
* @return 表单字段权限
*/
Map<String, String> getFormFieldsPermission(@Valid BpmFormFieldsPermissionReqVO reqVO);
// TODO @芋艿重点在 review
/**
* 获取审批详情
* <p>
* 可以是准备发起的流程进行中的流程已经结束的流程
*
* @param loginUserId 登录人的用户编号
* @param reqVO 请求信息
* @return 流程实例的进度
*/
BpmApprovalDetailRespVO getApprovalDetail(Long loginUserId, @Valid BpmApprovalDetailReqVO reqVO);
// ========== Update 写入相关方法 ==========
/**
* 创建流程实例提供给前端
*
@ -120,25 +141,21 @@ public interface BpmProcessInstanceService {
void cancelProcessInstanceByAdmin(Long userId, BpmProcessInstanceCancelReqVO cancelReqVO);
/**
* 更新 ProcessInstance 拓展记录为取消
* 更新 ProcessInstance 为不通过
*
* @param event 流程取消事件
* @param processInstance 流程实例
* @param reason 理由例如说审批不通过时需要传递该值
*/
void updateProcessInstanceWhenCancel(FlowableCancelledEvent event);
void updateProcessInstanceReject(ProcessInstance processInstance, String reason);
// ========== Event 事件相关方法 ==========
/**
* 更新 ProcessInstance 拓展记录为完成
* 处理 ProcessInstance 完成事件例如说审批通过不通过取消
*
* @param instance 流程任务
*/
void updateProcessInstanceWhenApprove(ProcessInstance instance);
void processProcessInstanceCompleted(ProcessInstance instance);
/**
* 更新 ProcessInstance 拓展记录为不通过
*
* @param id 流程编号
* @param reason 理由例如说审批不通过时需要传递该值
*/
void updateProcessInstanceReject(String id, String reason);
}

View File

@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.bpm.service.task;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmUserTaskTimeoutHandlerTypeEnum;
import jakarta.validation.Valid;
import org.flowable.bpmn.model.UserTask;
import org.flowable.task.api.Task;
@ -20,6 +21,8 @@ import java.util.Map;
*/
public interface BpmTaskService {
// ========== Query 查询相关方法 ==========
/**
* 获得待办的流程任务分页
*
@ -74,6 +77,67 @@ public interface BpmTaskService {
*/
List<HistoricTaskInstance> getTaskListByProcessInstanceId(String processInstanceId);
/**
* 获取任务
*
* @param id 任务编号
* @return 任务
*/
Task getTask(String id);
/**
* 获取历史任务
*
* @param id 任务编号
* @return 历史任务
*/
HistoricTaskInstance getHistoricTask(String id);
/**
* 获取历史任务列表
*
* @param taskIds 任务编号集合
* @return 历史任务列表
*/
List<HistoricTaskInstance> getHistoricTasks(Collection<String> taskIds);
/**
* 根据条件查询正在进行中的任务
*
* @param processInstanceId 流程实例编号不允许为空
* @param assigned 是否分配了审批人允许空
* @param taskDefineKey 任务定义 Key允许空
*/
List<Task> getRunningTaskListByProcessInstanceId(String processInstanceId,
Boolean assigned,
String taskDefineKey);
/**
* 获取当前任务的可回退的 UserTask 集合
*
* @param id 当前的任务 ID
* @return 可以回退的节点列表
*/
List<UserTask> getUserTaskListByReturn(String id);
/**
* 获取指定任务的子任务列表
*
* @param parentTaskId 父任务ID
* @return 子任务列表
*/
List<Task> getTaskListByParentTaskId(String parentTaskId);
/**
* 通过任务 ID查询任务名 Map
*
* @param taskIds 任务 ID
* @return 任务 ID 与名字的 Map
*/
Map<String, String> getTaskNameByTaskIds(Collection<String> taskIds);
// ========== Update 写入相关方法 ==========
/**
* 通过任务
*
@ -99,41 +163,11 @@ public interface BpmTaskService {
void transferTask(Long userId, BpmTaskTransferReqVO reqVO);
/**
* 更新 Task 状态在创建时
* 将指定流程实例的进行中的流程任务移动到结束节点
*
* @param task 任务实体
* @param processInstanceId 流程编号
*/
void updateTaskStatusWhenCreated(Task task);
/**
* 更新 Task 状态在取消时
*
* @param taskId 任务的编号
*/
void updateTaskStatusWhenCanceled(String taskId);
/**
* 更新 Task 拓展记录并发送通知
*
* @param task 任务实体
*/
void updateTaskExtAssign(Task task);
/**
* 获取任务
*
* @param id 任务编号
* @return 任务
*/
Task getTask(String id);
/**
* 获取当前任务的可回退的 UserTask 集合
*
* @param id 当前的任务 ID
* @return 可以回退的节点列表
*/
List<UserTask> getUserTaskListByReturn(String id);
void moveTaskToEnd(String processInstanceId);
/**
* 将任务回退到指定的 targetDefinitionKey 位置
@ -167,20 +201,41 @@ public interface BpmTaskService {
*/
void deleteSignTask(Long userId, BpmTaskSignDeleteReqVO reqVO);
/**
* 获取指定任务的子任务列表
*
* @param parentTaskId 父任务ID
* @return 子任务列表
*/
List<Task> getTaskListByParentTaskId(String parentTaskId);
// ========== Event 事件相关方法 ==========
/**
* 通过任务 ID查询任务名 Map
* 处理 Task 创建事件目前是
*
* @param taskIds 任务 ID
* @return 任务 ID 与名字的 Map
* 1. 更新它的状态为审批中
* 2. 处理自动通过的情况例如说1无审批人时是否自动通过不通过2人工审核是否自动通过不通过
*
* 注意它的触发时机晚于 {@link #processTaskAssigned(Task)} 之后
*
* @param task 任务实体
*/
Map<String, String> getTaskNameByTaskIds(Collection<String> taskIds);
void processTaskCreated(Task task);
/**
* 处理 Task 取消事件目前是更新它的状态为已取消
*
* @param taskId 任务的编号
*/
void processTaskCanceled(String taskId);
/**
* 处理 Task 设置审批人事件目前是发送审批消息
*
* @param task 任务实体
*/
void processTaskAssigned(Task task);
/**
* 处理 Task 审批超时事件可能会处理多个当前审批中的任务
*
* @param processInstanceId 流程示例编号
* @param taskDefineKey 任务 Key
* @param handlerType 处理类型参见 {@link BpmUserTaskTimeoutHandlerTypeEnum}
*/
void processTaskTimeout(String processInstanceId, String taskDefineKey, Integer handlerType);
}

View File

@ -1,32 +1,37 @@
package cn.iocoder.yudao.module.bpm.service.task;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.*;
import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.framework.common.util.object.PageUtils;
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.framework.web.core.util.WebFrameworkUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.task.*;
import cn.iocoder.yudao.module.bpm.convert.task.BpmTaskConvert;
import cn.iocoder.yudao.module.bpm.enums.definition.*;
import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmDeleteReasonEnum;
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.BpmConstants;
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;
import cn.iocoder.yudao.module.bpm.service.definition.BpmModelService;
import cn.iocoder.yudao.module.bpm.service.message.BpmMessageService;
import cn.iocoder.yudao.module.bpm.service.message.dto.BpmMessageSendWhenTaskTimeoutReqDTO;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.EndEvent;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.HistoryService;
@ -45,7 +50,6 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.Assert;
import java.util.*;
import java.util.stream.Stream;
@ -53,6 +57,7 @@ import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG;
/**
* 流程任务实例 Service 实现类
@ -78,12 +83,16 @@ public class BpmTaskServiceImpl implements BpmTaskService {
@Resource
private BpmProcessInstanceCopyService processInstanceCopyService;
@Resource
private BpmModelService bpmModelService;
private BpmModelService modelService;
@Resource
private BpmMessageService messageService;
@Resource
private AdminUserApi adminUserApi;
@Resource
private DeptApi deptApi;
// ========== Query 查询相关方法 ==========
@Override
public PageResult<Task> getTaskTodoPage(Long userId, BpmTaskPageReqVO pageVO) {
@ -173,6 +182,165 @@ public class BpmTaskServiceImpl implements BpmTaskService {
return tasks;
}
/**
* 校验任务是否存在并且是否是分配给自己的任务
*
* @param userId 用户 id
* @param taskId task id
*/
private Task validateTask(Long userId, String taskId) {
Task task = validateTaskExist(taskId);
// 为什么判断 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;
}
private Task validateTaskExist(String id) {
Task task = getTask(id);
if (task == null) {
throw exception(TASK_NOT_EXISTS);
}
return task;
}
@Override
public Task getTask(String id) {
return taskService.createTaskQuery().taskId(id).includeTaskLocalVariables().singleResult();
}
@Override
public HistoricTaskInstance getHistoricTask(String id) {
return historyService.createHistoricTaskInstanceQuery().taskId(id).includeTaskLocalVariables().singleResult();
}
@Override
public List<HistoricTaskInstance> getHistoricTasks(Collection<String> taskIds) {
return historyService.createHistoricTaskInstanceQuery().taskIds(taskIds).includeTaskLocalVariables().list();
}
@Override
public List<Task> getRunningTaskListByProcessInstanceId(String processInstanceId, Boolean assigned, String defineKey) {
Assert.notNull(processInstanceId, "processInstanceId 不能为空");
TaskQuery taskQuery = taskService.createTaskQuery().processInstanceId(processInstanceId).active()
.includeTaskLocalVariables();
if (BooleanUtil.isTrue(assigned)) {
taskQuery.taskAssigned();
} else if (BooleanUtil.isFalse(assigned)) {
taskQuery.taskUnassigned();
}
if (StrUtil.isNotEmpty(defineKey)) {
taskQuery.taskDefinitionKey(defineKey);
}
return taskQuery.list();
}
@Override
public List<UserTask> getUserTaskListByReturn(String id) {
// 1.1 校验当前任务 task 存在
Task task = validateTaskExist(id);
// 1.2 根据流程定义获取流程模型信息
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId());
FlowElement source = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
if (source == null) {
throw exception(TASK_NOT_EXISTS);
}
// 2.1 查询该任务的前置任务节点的 key 集合
List<UserTask> previousUserList = BpmnModelUtils.getPreviousUserTaskList(source, null, null);
if (CollUtil.isEmpty(previousUserList)) {
return Collections.emptyList();
}
// 2.2 过滤只有串行可到达的节点才可以回退类似非串行子流程无法退回
previousUserList.removeIf(userTask -> !BpmnModelUtils.isSequentialReachable(source, userTask, null));
return previousUserList;
}
/**
* 获得所有子任务列表
*
* @param parentTask 父任务
* @return 所有子任务列表
*/
private List<Task> getAllChildTaskList(Task parentTask) {
List<Task> result = new ArrayList<>();
// 1. 递归获取子级
Stack<Task> stack = new Stack<>();
stack.push(parentTask);
// 2. 递归遍历
for (int i = 0; i < Short.MAX_VALUE; i++) {
if (stack.isEmpty()) {
break;
}
// 2.1 获取子任务们
Task task = stack.pop();
List<Task> childTaskList = getTaskListByParentTaskId(task.getId());
// 2.2 如果非空则添加到 stack 进一步递归
if (CollUtil.isNotEmpty(childTaskList)) {
stack.addAll(childTaskList);
result.addAll(childTaskList);
}
}
return result;
}
@Override
public List<Task> getTaskListByParentTaskId(String parentTaskId) {
String tableName = managementService.getTableName(TaskEntity.class);
// taskService.createTaskQuery() 没有 parentId 参数所以写 sql 查询
String sql = "select ID_,NAME_,OWNER_,ASSIGNEE_ from " + tableName + " where PARENT_TASK_ID_=#{parentTaskId}";
return taskService.createNativeTaskQuery().sql(sql).parameter("parentTaskId", parentTaskId).list();
}
/**
* 获取子任务个数
*
* @param parentTaskId 父任务 ID
* @return 剩余子任务个数
*/
private Long getTaskCountByParentTaskId(String parentTaskId) {
String tableName = managementService.getTableName(TaskEntity.class);
String sql = "SELECT COUNT(1) from " + tableName + " WHERE PARENT_TASK_ID_=#{parentTaskId}";
return taskService.createNativeTaskQuery().sql(sql).parameter("parentTaskId", parentTaskId).count();
}
/**
* 获得任务根任务的父任务编号
*
* @param task 任务
* @return 根任务的父任务编号
*/
private String getTaskRootParentId(Task task) {
if (task == null || task.getParentTaskId() == null) {
return null;
}
for (int i = 0; i < Short.MAX_VALUE; i++) {
Task parentTask = getTask(task.getParentTaskId());
if (parentTask == null) {
return null;
}
if (parentTask.getParentTaskId() == null) {
return parentTask.getId();
}
task = parentTask;
}
throw new IllegalArgumentException(String.format("Task(%s) 层级过深,无法获取父节点编号", task.getId()));
}
@Override
public Map<String, String> getTaskNameByTaskIds(Collection<String> taskIds) {
if (CollUtil.isEmpty(taskIds)) {
return Collections.emptyMap();
}
List<Task> tasks = taskService.createTaskQuery().taskIds(taskIds).list();
return convertMap(tasks, Task::getId, Task::getName);
}
// ========== Update 写入相关方法 ==========
@Override
@Transactional(rollbackFor = Exception.class)
public void approveTask(Long userId, @Valid BpmTaskApproveReqVO reqVO) {
@ -211,6 +379,8 @@ public class BpmTaskServiceImpl implements BpmTaskService {
// 其中variables 是存储动态表单到 local 任务级别过滤一下避免 ProcessInstance 系统级的变量被占用
if (CollUtil.isNotEmpty(reqVO.getVariables())) {
Map<String, Object> variables = FlowableUtils.filterTaskFormVariable(reqVO.getVariables());
// 修改表单的值需要存储到 ProcessInstance 变量
runtimeService.setVariables(task.getProcessInstanceId(), variables);
taskService.complete(task.getId(), variables, true);
} else {
taskService.complete(task.getId());
@ -244,7 +414,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
/**
* 如果父任务是有前后加签的任务如果它加签出来的子任务都被处理需要处理父任务
*
* <p>
* 1. 如果是向前加签则需要重新激活父任务让它可以被审批
* 2. 如果是向后加签则需要完成父任务让它完成审批
*
@ -281,7 +451,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
} else if (BpmTaskSignTypeEnum.AFTER.getType().equals(scopeType)) {
// 只有 parentTask 处于 APPROVING 的情况下才可以继续 complete 完成
// 否则一个未审批的 parentTask 任务在加签出来的任务都被减签的情况下就直接完成审批这样会存在问题
Integer status = (Integer) parentTask.getTaskLocalVariables().get(BpmConstants.TASK_VARIABLE_STATUS);
Integer status = (Integer) parentTask.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS);
if (ObjectUtil.notEqual(status, BpmTaskStatusEnum.APPROVING.getStatus())) {
return;
}
@ -326,14 +496,37 @@ public class BpmTaskServiceImpl implements BpmTaskService {
throw exception(PROCESS_INSTANCE_NOT_EXISTS);
}
// 2.1 更新流程实例为不通过
// 2.1 更新流程任务为不通过
updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.REJECT.getStatus(), reqVO.getReason());
// 2.2 添加评论
// 2.2 添加流程评论
taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.REJECT.getType(),
BpmCommentTypeEnum.REJECT.formatComment(reqVO.getReason()));
// 2.3 如果当前任务时被加签的则加它的根任务也标记成未通过
// 疑问为什么要标记未通过呢
// 回答例如说 A 任务被向前加签除 B 任务时B 任务被审批不通过此时 A 会被取消 yudao-ui-admin-vue3 不展示已取消的任务导致展示不出审批不通过的细节
if (task.getParentTaskId() != null) {
String rootParentId = getTaskRootParentId(task);
updateTaskStatusAndReason(rootParentId, BpmTaskStatusEnum.REJECT.getStatus(),
BpmCommentTypeEnum.REJECT.formatComment("加签任务不通过"));
taskService.addComment(rootParentId, task.getProcessInstanceId(), BpmCommentTypeEnum.REJECT.getType(),
BpmCommentTypeEnum.REJECT.formatComment("加签任务不通过"));
}
// 3. 更新流程实例审批不通过
processInstanceService.updateProcessInstanceReject(instance.getProcessInstanceId(), reqVO.getReason());
// 3. 根据不同的 RejectHandler 处理策略
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId());
FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
// 3.1 情况一驳回到指定的任务节点
BpmUserTaskRejectHandlerType userTaskRejectHandlerType = BpmnModelUtils.parseRejectHandlerType(userTaskElement);
if (userTaskRejectHandlerType == BpmUserTaskRejectHandlerType.RETURN_USER_TASK) {
String returnTaskId = BpmnModelUtils.parseReturnTaskId(userTaskElement);
Assert.notNull(returnTaskId, "回退的节点不能为空");
returnTask(userId, new BpmTaskReturnReqVO().setId(task.getId())
.setTargetTaskDefinitionKey(returnTaskId).setReason(reqVO.getReason()));
return;
}
// 3.2 情况二直接结束审批不通过
processInstanceService.updateProcessInstanceReject(instance, reqVO.getReason()); // 标记不通过
moveTaskToEnd(task.getProcessInstanceId()); // 结束流程
}
/**
@ -343,7 +536,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
* @param status 状态
*/
private void updateTaskStatus(String id, Integer status) {
taskService.setVariableLocal(id, BpmConstants.TASK_VARIABLE_STATUS, status);
taskService.setVariableLocal(id, BpmnVariableConstants.TASK_VARIABLE_STATUS, status);
}
/**
@ -355,106 +548,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
*/
private void updateTaskStatusAndReason(String id, Integer status, String reason) {
updateTaskStatus(id, status);
taskService.setVariableLocal(id, BpmConstants.TASK_VARIABLE_REASON, reason);
}
/**
* 校验任务是否存在并且是否是分配给自己的任务
*
* @param userId 用户 id
* @param taskId task id
*/
private Task validateTask(Long userId, String taskId) {
Task task = validateTaskExist(taskId);
if (!Objects.equals(userId, NumberUtils.parseLong(task.getAssignee()))) {
throw exception(TASK_OPERATE_FAIL_ASSIGN_NOT_SELF);
}
return task;
}
@Override
public void updateTaskStatusWhenCreated(Task task) {
Integer status = (Integer) task.getTaskLocalVariables().get(BpmConstants.TASK_VARIABLE_STATUS);
if (status != null) {
log.error("[updateTaskStatusWhenCreated][taskId({}) 已经有状态({})]", task.getId(), status);
return;
}
updateTaskStatus(task.getId(), BpmTaskStatusEnum.RUNNING.getStatus());
}
@Override
public void updateTaskStatusWhenCanceled(String taskId) {
Task task = getTask(taskId);
// 1. 可能只是活动不是任务所以查询不到
if (task == null) {
log.error("[updateTaskStatusWhenCanceled][taskId({}) 任务不存在]", taskId);
return;
}
// 2. 更新 task 状态 + 原因
Integer status = (Integer) task.getTaskLocalVariables().get(BpmConstants.TASK_VARIABLE_STATUS);
if (BpmTaskStatusEnum.isEndStatus(status)) {
log.error("[updateTaskStatusWhenCanceled][taskId({}) 处于结果({}),无需进行更新]", taskId, status);
return;
}
updateTaskStatusAndReason(taskId, BpmTaskStatusEnum.CANCEL.getStatus(), BpmDeleteReasonEnum.CANCEL_BY_SYSTEM.getReason());
// 补充说明由于 Task 被删除成 HistoricTask 无法通过 taskService.addComment 添加理由所以无法存储具体的取消理由
}
@Override
public void updateTaskExtAssign(Task task) {
// 发送通知在事务提交时批量执行操作所以直接查询会无法查询到 ProcessInstance所以这里是通过监听事务的提交来实现
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
if (StrUtil.isEmpty(task.getAssignee())) {
return;
}
ProcessInstance processInstance = processInstanceService.getProcessInstance(task.getProcessInstanceId());
AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId()));
messageService.sendMessageWhenTaskAssigned(BpmTaskConvert.INSTANCE.convert(processInstance, startUser, task));
}
});
}
private Task validateTaskExist(String id) {
Task task = getTask(id);
if (task == null) {
throw exception(TASK_NOT_EXISTS);
}
return task;
}
@Override
public Task getTask(String id) {
return taskService.createTaskQuery().taskId(id).includeTaskLocalVariables().singleResult();
}
private HistoricTaskInstance getHistoricTask(String id) {
return historyService.createHistoricTaskInstanceQuery().taskId(id).includeTaskLocalVariables().singleResult();
}
@Override
public List<UserTask> getUserTaskListByReturn(String id) {
// 1.1 校验当前任务 task 存在
Task task = validateTaskExist(id);
// 1.2 根据流程定义获取流程模型信息
BpmnModel bpmnModel = bpmModelService.getBpmnModelByDefinitionId(task.getProcessDefinitionId());
FlowElement source = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
if (source == null) {
throw exception(TASK_NOT_EXISTS);
}
// 2.1 查询该任务的前置任务节点的 key 集合
List<UserTask> previousUserList = BpmnModelUtils.getPreviousUserTaskList(source, null, null);
if (CollUtil.isEmpty(previousUserList)) {
return Collections.emptyList();
}
// 2.2 过滤只有串行可到达的节点才可以回退类似非串行子流程无法退回
previousUserList.removeIf(userTask -> !BpmnModelUtils.isSequentialReachable(source, userTask, null));
return previousUserList;
taskService.setVariableLocal(id, BpmnVariableConstants.TASK_VARIABLE_REASON, reason);
}
@Override
@ -483,7 +577,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
*/
private FlowElement validateTargetTaskCanReturn(String sourceKey, String targetKey, String processDefinitionId) {
// 1.1 获取流程模型信息
BpmnModel bpmnModel = bpmModelService.getBpmnModelByDefinitionId(processDefinitionId);
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processDefinitionId);
// 1.3 获取当前任务节点元素
FlowElement source = BpmnModelUtils.getFlowElementById(bpmnModel, sourceKey);
// 1.3 获取跳转的节点元素
@ -529,7 +623,11 @@ public class BpmTaskServiceImpl implements BpmTaskService {
updateTaskStatusAndReason(task.getId(), BpmTaskStatusEnum.RETURN.getStatus(), reqVO.getReason());
});
// 3. 执行驳回
// 3. 设置流程变量节点驳回标记用于驳回到节点不执行 BpmUserTaskAssignStartUserHandlerTypeEnum 策略导致自动通过
runtimeService.setVariable(currentTask.getProcessInstanceId(),
String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, reqVO.getTargetTaskDefinitionKey()), Boolean.TRUE);
// 4. 执行驳回
runtimeService.createChangeActivityStateBuilder()
.processInstanceId(currentTask.getProcessInstanceId())
.moveActivityIdsToSingleActivityId(returnTaskKeyList, // 当前要跳转的节点列表( 1 或多)
@ -592,6 +690,35 @@ public class BpmTaskServiceImpl implements BpmTaskService {
taskService.setAssignee(taskId, reqVO.getAssigneeUserId().toString());
}
@Override
public void moveTaskToEnd(String processInstanceId) {
List<Task> taskList = getRunningTaskListByProcessInstanceId(processInstanceId, null, null);
if (CollUtil.isEmpty(taskList)) {
return;
}
// 1. 其它未结束的任务直接取消
// 疑问为什么不通过 updateTaskStatusWhenCanceled 监听取消而是直接提前调用呢
// 回答详细见 updateTaskStatusWhenCanceled 的方法加签的场景
taskList.forEach(task -> {
Integer otherTaskStatus = (Integer) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS);
if (BpmTaskStatusEnum.isEndStatus(otherTaskStatus)) {
return;
}
processTaskCanceled(task.getId());
});
// 2. 终止流程
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(taskList.get(0).getProcessDefinitionId());
List<String> activityIds = CollUtil.newArrayList(convertSet(taskList, Task::getTaskDefinitionKey));
EndEvent endEvent = BpmnModelUtils.getEndEvent(bpmnModel);
Assert.notNull(endEvent, "结束节点不能未空");
runtimeService.createChangeActivityStateBuilder()
.processInstanceId(processInstanceId)
.moveActivityIdsToSingleActivityId(activityIds, endEvent.getId())
.changeState();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void createSignTask(Long userId, BpmTaskSignCreateReqVO reqVO) {
@ -656,7 +783,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
List<Long> currentAssigneeList = convertListByFlatMap(taskList, task -> // 需要考虑 owner 的情况因为向后加签时它暂时没 assignee 而是 owner
Stream.of(NumberUtils.parseLong(task.getAssignee()), NumberUtils.parseLong(task.getOwner())));
if (CollUtil.containsAny(currentAssigneeList, reqVO.getUserIds())) {
List<AdminUserRespDTO> userList = adminUserApi.getUserList( CollUtil.intersection(currentAssigneeList, reqVO.getUserIds()));
List<AdminUserRespDTO> userList = adminUserApi.getUserList(CollUtil.intersection(currentAssigneeList, reqVO.getUserIds()));
throw exception(TASK_SIGN_CREATE_USER_REPEAT, String.join(",", convertList(userList, AdminUserRespDTO::getNickname)));
}
return taskEntity;
@ -762,61 +889,203 @@ public class BpmTaskServiceImpl implements BpmTaskService {
return task;
}
/**
* 获得所有子任务列表
*
* @param parentTask 父任务
* @return 所有子任务列表
*/
private List<Task> getAllChildTaskList(Task parentTask) {
List<Task> result = new ArrayList<>();
// 1. 递归获取子级
Stack<Task> stack = new Stack<>();
stack.push(parentTask);
// 2. 递归遍历
for (int i = 0; i < Short.MAX_VALUE; i++) {
if (stack.isEmpty()) {
break;
}
// 2.1 获取子任务们
Task task = stack.pop();
List<Task> childTaskList = getTaskListByParentTaskId(task.getId());
// 2.2 如果非空则添加到 stack 进一步递归
if (CollUtil.isNotEmpty(childTaskList)) {
stack.addAll(childTaskList);
result.addAll(childTaskList);
}
}
return result;
}
// ========== Event 事件相关方法 ==========
@Override
public List<Task> getTaskListByParentTaskId(String parentTaskId) {
String tableName = managementService.getTableName(TaskEntity.class);
// taskService.createTaskQuery() 没有 parentId 参数所以写 sql 查询
String sql = "select ID_,NAME_,OWNER_,ASSIGNEE_ from " + tableName + " where PARENT_TASK_ID_=#{parentTaskId}";
return taskService.createNativeTaskQuery().sql(sql).parameter("parentTaskId", parentTaskId).list();
public void processTaskCreated(Task task) {
// 1. 设置为待办中
Integer status = (Integer) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS);
if (status != null) {
log.error("[updateTaskStatusWhenCreated][taskId({}) 已经有状态({})]", task.getId(), status);
return;
}
updateTaskStatus(task.getId(), BpmTaskStatusEnum.RUNNING.getStatus());
// 2. 处理自动通过的情况例如说1无审批人时是否自动通过不通过2人工审核是否自动通过不通过
ProcessInstance processInstance = processInstanceService.getProcessInstance(task.getProcessInstanceId());
if (processInstance == null) {
log.error("[processTaskCreated][taskId({}) 没有找到流程实例]", task.getId());
return;
}
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId());
FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
Integer approveType = BpmnModelUtils.parseApproveType(userTaskElement);
Integer assignEmptyHandlerType = BpmnModelUtils.parseAssignEmptyHandlerType(userTaskElement);
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCompletion(int transactionStatus) {
// 特殊情况部分情况下TransactionSynchronizationManager 注册 afterCommit 监听时不会被调用但是 afterCompletion 可以
// 例如说第一个 task 就是配置自动通过或者自动拒绝
if (ObjectUtil.notEqual(transactionStatus, TransactionSynchronization.STATUS_COMMITTED)) {
return;
}
// 特殊情况一人工审核审批人为空根据配置是否要自动通过自动拒绝
if (ObjectUtil.equal(approveType, BpmUserTaskApproveTypeEnum.USER.getType())) {
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()));
}
// 特殊情况二自动审核审批类型为自动通过不通过
} else {
if (ObjectUtil.equal(approveType, BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType())) {
SpringUtil.getBean(BpmTaskService.class).approveTask(null, new BpmTaskApproveReqVO()
.setId(task.getId()).setReason(BpmReasonEnum.APPROVE_TYPE_AUTO_APPROVE.getReason()));
} else if (ObjectUtil.equal(approveType, BpmUserTaskApproveTypeEnum.AUTO_REJECT.getType())) {
SpringUtil.getBean(BpmTaskService.class).rejectTask(null, new BpmTaskRejectReqVO()
.setId(task.getId()).setReason(BpmReasonEnum.APPROVE_TYPE_AUTO_REJECT.getReason()));
}
}
}
});
}
/**
* 获取子任务个数
*
* @param parentTaskId 父任务 ID
* @return 剩余子任务个数
* 重要补充说明该方法目前主要有两个情况会调用到
* <p>
* 1. 或签场景 + 审批通过一个或签有多个审批时如果 A 审批通过其它或签 BC 等任务会被 Flowable 自动删除此时需要通过该方法更新状态为已取消
* 2. 审批不通过 {@link #rejectTask(Long, BpmTaskRejectReqVO)} 不通过时对于加签的任务不会被 Flowable 删除此时需要通过该方法更新状态为已取消
*/
private Long getTaskCountByParentTaskId(String parentTaskId) {
String tableName = managementService.getTableName(TaskEntity.class);
String sql = "SELECT COUNT(1) from " + tableName + " WHERE PARENT_TASK_ID_=#{parentTaskId}";
return taskService.createNativeTaskQuery().sql(sql).parameter("parentTaskId", parentTaskId).count();
@Override
public void processTaskCanceled(String taskId) {
Task task = getTask(taskId);
// 1. 可能只是活动不是任务所以查询不到
if (task == null) {
log.error("[updateTaskStatusWhenCanceled][taskId({}) 任务不存在]", taskId);
return;
}
// 2. 更新 task 状态 + 原因
Integer status = (Integer) task.getTaskLocalVariables().get(BpmnVariableConstants.TASK_VARIABLE_STATUS);
if (BpmTaskStatusEnum.isEndStatus(status)) {
log.error("[updateTaskStatusWhenCanceled][taskId({}) 处于结果({}),无需进行更新]", taskId, status);
return;
}
updateTaskStatusAndReason(taskId, BpmTaskStatusEnum.CANCEL.getStatus(), BpmReasonEnum.CANCEL_BY_SYSTEM.getReason());
// 补充说明由于 Task 被删除成 HistoricTask 无法通过 taskService.addComment 添加理由所以无法存储具体的取消理由
}
@Override
public Map<String, String> getTaskNameByTaskIds(Collection<String> taskIds) {
if (CollUtil.isEmpty(taskIds)) {
return Collections.emptyMap();
public void processTaskAssigned(Task task) {
// 发送通知在事务提交时批量执行操作所以直接查询会无法查询到 ProcessInstance所以这里是通过监听事务的提交来实现
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
if (StrUtil.isEmpty(task.getAssignee())) {
log.error("[processTaskAssigned][taskId({}) 没有分配到负责人]", task.getId());
return;
}
List<Task> tasks = taskService.createTaskQuery().taskIds(taskIds).list();
return convertMap(tasks, Task::getId, Task::getName);
ProcessInstance processInstance = processInstanceService.getProcessInstance(task.getProcessInstanceId());
if (processInstance == null) {
log.error("[processTaskAssigned][taskId({}) 没有找到流程实例]", task.getId());
return;
}
// 审批人与提交人为同一人时根据 BpmUserTaskAssignStartUserHandlerTypeEnum 策略进行处理
if (StrUtil.equals(task.getAssignee(), processInstance.getStartUserId())) {
// 判断是否为回退或者驳回如果是回退或者驳回不走这个策略
// TODO 芋艿优化未来有没更好的判断方式另外还要考虑清理机制就是说下次处理了之后就移除这个标识
Boolean returnTaskFlag = runtimeService.getVariable(processInstance.getProcessInstanceId(),
String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class);
if (ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) {
BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId());
if (bpmnModel == null) {
log.error("[processTaskAssigned][taskId({}) 没有找到流程模型]", task.getId());
return;
}
FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(userTaskElement);
// 情况一自动跳过
if (ObjectUtils.equalsAny(assignStartUserHandlerType,
BpmUserTaskAssignStartUserHandlerTypeEnum.SKIP.getType())) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP.getReason()));
return;
}
// 情况二转交给部门负责人审批
if (ObjectUtils.equalsAny(assignStartUserHandlerType,
BpmUserTaskAssignStartUserHandlerTypeEnum.TRANSFER_DEPT_LEADER.getType())) {
AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId()));
Assert.notNull(startUser, "提交人({})信息为空", processInstance.getStartUserId());
DeptRespDTO dept = startUser.getDeptId() != null ? deptApi.getDept(startUser.getDeptId()) : null;
Assert.notNull(dept, "提交人({})部门({})信息为空", processInstance.getStartUserId(), startUser.getDeptId());
// 找不到部门负责人的情况下自动审批通过
// noinspection DataFlowIssue
if (dept.getLeaderUserId() == null) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_DEPT_LEADER_NOT_FOUND.getReason()));
return;
}
// 找得到部门负责人的情况下修改负责人
if (ObjectUtil.notEqual(dept.getLeaderUserId(), startUser.getId())) {
getSelf().transferTask(Long.valueOf(task.getAssignee()), new BpmTaskTransferReqVO()
.setId(task.getId()).setAssigneeUserId(dept.getLeaderUserId())
.setReason(BpmReasonEnum.ASSIGN_START_USER_TRANSFER_DEPT_LEADER.getReason()));
return;
}
// 如果部门负责人是自己还是自己审批吧~
}
}
}
AdminUserRespDTO startUser = adminUserApi.getUser(Long.valueOf(processInstance.getStartUserId()));
messageService.sendMessageWhenTaskAssigned(BpmTaskConvert.INSTANCE.convert(processInstance, startUser, task));
}
});
}
@Override
@Transactional(rollbackFor = Exception.class)
public void processTaskTimeout(String processInstanceId, String taskDefineKey, Integer handlerType) {
ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId);
if (processInstance == null) {
log.error("[processTaskTimeout][processInstanceId({}) 没有找到流程实例]", processInstanceId);
return;
}
List<Task> taskList = getRunningTaskListByProcessInstanceId(processInstanceId, true, taskDefineKey);
// TODO 优化未来需要考虑加签的情况
if (CollUtil.isEmpty(taskList)) {
log.error("[processTaskTimeout][processInstanceId({}) 定义Key({}) 没有找到任务]", processInstanceId, taskDefineKey);
return;
}
taskList.forEach(task -> FlowableUtils.execute(task.getTenantId(), () -> {
// 情况一自动提醒
if (Objects.equals(handlerType, BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType())) {
messageService.sendMessageWhenTaskTimeout(new BpmMessageSendWhenTaskTimeoutReqDTO()
.setProcessInstanceId(processInstanceId).setProcessInstanceName(processInstance.getName())
.setTaskId(task.getId()).setTaskName(task.getName()).setAssigneeUserId(Long.parseLong(task.getAssignee())));
return;
}
// 情况二自动同意
if (Objects.equals(handlerType, BpmUserTaskTimeoutHandlerTypeEnum.APPROVE.getType())) {
approveTask(Long.parseLong(task.getAssignee()),
new BpmTaskApproveReqVO().setId(task.getId()).setReason(BpmReasonEnum.TIMEOUT_APPROVE.getReason()));
return;
}
// 情况三自动拒绝
if (Objects.equals(handlerType, BpmUserTaskTimeoutHandlerTypeEnum.REJECT.getType())) {
rejectTask(Long.parseLong(task.getAssignee()),
new BpmTaskRejectReqVO().setId(task.getId()).setReason(BpmReasonEnum.REJECT_TASK.getReason()));
}
}));
}
/**
* 获得自身的代理对象解决 AOP 生效问题
*
* @return 自己
*/
private BpmTaskServiceImpl getSelf() {
return SpringUtil.getBean(getClass());
}
}

View File

@ -0,0 +1,36 @@
package cn.iocoder.yudao.module.bpm.service.task.bo;
import lombok.Data;
import java.util.List;
import java.util.Map;
import java.util.Set;
import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ApprovalNodeInfo;
/**
* 已经进行中的审批节点 Response BO
*
* @author jason
*/
@Data
public class AlreadyRunApproveNodeRespBO {
/**
* 审批节点信息数组
*/
private List<ApprovalNodeInfo> approveNodes;
/**
* 已运行的节点 ID 数组 (对应 Bpmn XML 节点 id)
*/
private Set<String> runNodeIds;
/**
* 正在运行的节点的审批信息key: activityId, value: 审批信息
* <p>
* 用于依次审批需要加上候选人信息
*/
private Map<String, ApprovalNodeInfo> runningApprovalNodes;
}

View File

@ -10,8 +10,8 @@ import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.delegate.DelegateExecution;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
@ -34,15 +34,23 @@ import static org.mockito.Mockito.when;
*/
public class BpmTaskCandidateInvokerTest extends BaseMockitoUnitTest {
@InjectMocks
private BpmTaskCandidateInvoker taskCandidateInvoker;
@Mock
private AdminUserApi adminUserApi;
@Spy
private BpmTaskCandidateStrategy strategy = new BpmTaskCandidateUserStrategy();
private BpmTaskCandidateStrategy strategy ;
@Spy
private List<BpmTaskCandidateStrategy> strategyList = Collections.singletonList(strategy);
private List<BpmTaskCandidateStrategy> strategyList ;
@BeforeEach
public void setUp() {
strategy = new BpmTaskCandidateUserStrategy(adminUserApi); // 创建strategy实例
strategyList = Collections.singletonList(strategy); // 创建strategyList
taskCandidateInvoker = new BpmTaskCandidateInvoker(strategyList, adminUserApi);
}
@Test
public void testCalculateUsers() {

View File

@ -46,11 +46,11 @@
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<!-- 工作流。默认注释,保证编译速度 -->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->
<!-- <artifactId>yudao-module-bpm-biz</artifactId>-->
<!-- <version>${revision}</version>-->
<!-- </dependency>-->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-bpm-biz</artifactId>
<version>${revision}</version>
</dependency>
<!-- 支付服务。默认注释,保证编译速度 -->
<!-- <dependency>-->
<!-- <groupId>cn.iocoder.boot</groupId>-->