!1286 2.4.2:工作流的更新

Merge pull request !1286 from 芋道源码/feature/bpm
This commit is contained in:
芋道源码 2025-03-15 05:22:46 +00:00 committed by Gitee
commit 367e3b9880
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
58 changed files with 2179 additions and 606 deletions

View File

@ -149,22 +149,45 @@
### 工作流程 ### 工作流程
| | 功能 | 描述 |
|----|-------|-----------------------------------------|
| 🚀 | 流程模型 | 配置工作流的流程模型,支持 BPMN 和仿钉钉/飞书设计器 |
| 🚀 | 流程表单 | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件 |
| 🚀 | 用户分组 | 自定义用户分组,可用于工作流的审批分组 |
| 🚀 | 我的流程 | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线 |
| 🚀 | 待办任务 | 查看自己【未】审批的工作任务,支持通过、不通过、转派、委派、退回、加减签等操作 |
| 🚀 | 已办任务 | 查看自己【已】审批的工作任务,支持流程预测,展示未来审批人信息 |
| 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 |
![功能图](/.image/common/bpm-feature.png) ![功能图](/.image/common/bpm-feature.png)
基于 Flowable 构建,可支持信创(国产)数据库,满足中国特色流程操作:
| BPMN 设计器 | 钉钉/飞书设计器 | | BPMN 设计器 | 钉钉/飞书设计器 |
|------------------------------|--------------------------------| |------------------------------|--------------------------------|
| ![](/.image/工作流设计器-bpmn.jpg) | ![](/.image/工作流设计器-simple.jpg) | | ![](/.image/工作流设计器-bpmn.jpg) | ![](/.image/工作流设计器-simple.jpg) |
> 历经头部企业生产验证,工作流引擎须标配仿钉钉/飞书 + BPMN 双设计器!!!
>
> 前者支持轻量配置简单流程,后者实现复杂场景深度编排
| 功能列表 | 功能描述 | 是否完成 |
|------------|-------------------------------------------------------------------------------------|------|
| SIMPLE 设计器 | 仿钉钉/飞书设计器支持拖拽搭建表单流程10 分钟快速完成审批流程配置 | ✅ |
| BPMN 设计器 | 基于 BPMN 标准开发,适配复杂业务场景,满足多层级审批及流程自动化需求 | ✅ |
| 会签 | 同一个审批节点设置多个人(如 A、B、C 三人,三人会同时收到待办任务),需全部同意之后,审批才可到下一审批节点 | ✅ |
| 或签 | 同一个审批节点设置多个人,任意一个人处理后,就能进入下一个节点 | ✅ |
| 依次审批 | (顺序会签)同一个审批节点设置多个人(如 A、B、C 三人),三人按顺序依次收到待办,即 A 先审批A 提交后 B 才能审批,需全部同意之后,审批才可到下一审批节点 | ✅ |
| 抄送 | 将审批结果通知给抄送人,同一个审批默认排重,不重复抄送给同一人 | ✅ |
| 驳回 | (退回)将审批重置发送给某节点,重新审批。可驳回至发起人、上一节点、任意节点 | ✅ |
| 转办 | A 转给其 B 审批B 审批后,进入下一节点 | ✅ |
| 委派 | A 转给其 B 审批B 审批后,转给 AA 继续审批后进入下一节点 | ✅ |
| 加签 | 允许当前审批人根据需要,自行增加当前节点的审批人,支持向前、向后加签 | ✅ |
| 减签 | (取消加签)在当前审批人操作之前,减少审批人 | ✅ |
| 撤销 | (取消流程)流程发起人,可以对流程进行撤销处理 | ✅ |
| 终止 | 系统管理员,在任意节点终止流程实例 | ✅ |
| 表单权限 | 支持拖拉拽配置表单,每个审批节点可配置只读、编辑、隐藏权限 | ✅ |
| 超时审批 | 配置超时审批时间,超时后自动触发审批通过、不通过、驳回等操作 | ✅ |
| 自动提醒 | 配置提醒时间,到达时间后自动触发短信、邮箱、站内信等通知提醒,支持自定义重复提醒频次 | ✅ |
| 父子流程 | 主流程设置子流程节点,子流程节点会自动触发子流程。子流程结束后,主流程才会执行(继续往下下执行),支持同步子流程、异步子流程 | ✅ |
| 条件分支 | (排它分支)用于在流程中实现决策,即根据条件选择一个分支执行 | ✅ |
| 并行分支 | 允许将流程分成多条分支,不进行条件判断,所有分支都会执行 | ✅ |
| 包容分支 | (条件分支 + 并行分支的结合体)允许基于条件选择多条分支执行,但如果没有任何一个分支满足条件,则可以选择默认分支 | ✅ |
| 路由分支 | 根据条件选择一个分支执行(重定向到指定配置节点),也可以选择默认分支执行(继续往下执行) | ✅ |
| 触发节点 | 执行到该节点,触发 HTTP 请求、HTTP 回调、更新数据、删除数据等 | ✅ |
| 延迟节点 | 执行到该节点,审批等待一段时间再执行,支持固定时长、固定日期等 | ✅ |
| 拓展设置 | 流程前置/后置通知,节点(任务)前置、后置通知,流程报表,自动审批去重,自定流程编号、标题、摘要,流程报表等 | ✅ |
### 支付系统 ### 支付系统
| | 功能 | 描述 | | | 功能 | 描述 |

View File

@ -1,7 +1,6 @@
package cn.iocoder.yudao.module.bpm.api.task; package cn.iocoder.yudao.module.bpm.api.task;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import jakarta.validation.Valid; import jakarta.validation.Valid;
/** /**
@ -20,4 +19,6 @@ public interface BpmProcessInstanceApi {
*/ */
String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO reqDTO); String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO reqDTO);
} }

View File

@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.bpm.api.task;
import jakarta.validation.constraints.NotEmpty;
/**
* 流程任务 Api 接口
*
* @author jason
*/
public interface BpmProcessTaskApi {
/**
* 触发流程任务的执行
*
* @param processInstanceId 流程实例编号
* @param taskDefineKey 任务 Key
*/
void triggerTask(@NotEmpty(message = "流程实例的编号不能为空") String processInstanceId,
@NotEmpty(message = "任务 Key 不能为空") String taskDefineKey);
}

View File

@ -24,6 +24,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_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_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, "操作流程失败,原因:你不是该流程({})的管理员"); ErrorCode MODEL_UPDATE_FAIL_NOT_MANAGER = new ErrorCode(1_009_002_007, "操作流程失败,原因:你不是该流程({})的管理员");
ErrorCode MODEL_DEPLOY_FAIL_FIRST_USER_TASK_CANDIDATE_STRATEGY_ERROR = new ErrorCode(1_009_002_008, "部署流程失败,原因:首个任务({})的审批人不能是【审批人自选】");
// ========== 流程定义 1-009-003-000 ========== // ========== 流程定义 1-009-003-000 ==========
ErrorCode PROCESS_DEFINITION_KEY_NOT_MATCH = new ErrorCode(1_009_003_000, "流程定义的标识期望是({}),当前是({}),请修改 BPMN 流程图"); ErrorCode PROCESS_DEFINITION_KEY_NOT_MATCH = new ErrorCode(1_009_003_000, "流程定义的标识期望是({}),当前是({}),请修改 BPMN 流程图");
@ -39,6 +40,8 @@ public interface ErrorCodeConstants {
ErrorCode PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_EXISTS = new ErrorCode(1_009_004_004, "任务({})的候选人({})不存在"); 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, "发起流程失败,你没有权限发起该流程"); ErrorCode PROCESS_INSTANCE_START_USER_CAN_START = new ErrorCode(1_009_004_005, "发起流程失败,你没有权限发起该流程");
ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW = new ErrorCode(1_009_004_005, "流程取消失败,该流程不允许取消"); ErrorCode PROCESS_INSTANCE_CANCEL_FAIL_NOT_ALLOW = new ErrorCode(1_009_004_005, "流程取消失败,该流程不允许取消");
ErrorCode PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR = new ErrorCode(1_009_004_006, "流程 Http 触发器请求调用失败");
ErrorCode PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG = new ErrorCode(1_009_004_007, "下一个任务({})的审批人未配置");
// ========== 流程任务 1-009-005-000 ========== // ========== 流程任务 1-009-005-000 ==========
ErrorCode TASK_OPERATE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1_009_005_001, "操作失败,原因:该任务的审批人不是你"); ErrorCode TASK_OPERATE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1_009_005_001, "操作失败,原因:该任务的审批人不是你");

View File

@ -14,7 +14,8 @@ import lombok.Getter;
public enum BpmBoundaryEventTypeEnum { public enum BpmBoundaryEventTypeEnum {
USER_TASK_TIMEOUT(1, "用户任务超时"), USER_TASK_TIMEOUT(1, "用户任务超时"),
DELAY_TIMER_TIMEOUT(2, "延迟器超时"); DELAY_TIMER_TIMEOUT(2, "延迟器超时"),
CHILD_PROCESS_TIMEOUT(3, "子流程超时");
private final Integer type; private final Integer type;
private final String name; private final String name;

View File

@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.bpm.enums.definition;
import cn.hutool.core.util.ArrayUtil;
import cn.iocoder.yudao.framework.common.core.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* BPM 子流程多实例来源类型枚举
*
* @author Lesan
*/
@Getter
@AllArgsConstructor
public enum BpmChildProcessMultiInstanceSourceTypeEnum implements ArrayValuable<Integer> {
FIXED_QUANTITY(1, "固定数量"),
NUMBER_FORM(2, "数字表单"),
MULTIPLE_FORM(3, "多选表单");
private final Integer type;
private final String name;
public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmChildProcessMultiInstanceSourceTypeEnum::getType).toArray(Integer[]::new);
public static BpmChildProcessMultiInstanceSourceTypeEnum typeOf(Integer type) {
return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
}
@Override
public Integer[] 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.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* BPM 当子流程发起人为空时类型枚举
*
* @author Lesan
*/
@Getter
@AllArgsConstructor
public enum BpmChildProcessStartUserEmptyTypeEnum implements ArrayValuable<Integer> {
MAIN_PROCESS_START_USER(1, "同主流程发起人"),
CHILD_PROCESS_ADMIN(2, "子流程管理员"),
MAIN_PROCESS_ADMIN(3, "主流程管理员");
private final Integer type;
private final String name;
public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmChildProcessStartUserEmptyTypeEnum::getType).toArray(Integer[]::new);
public static BpmChildProcessStartUserEmptyTypeEnum typeOf(Integer type) {
return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
}
@Override
public Integer[] 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.ArrayValuable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* BPM 子流程发起人类型枚举
*
* @author Lesan
*/
@Getter
@AllArgsConstructor
public enum BpmChildProcessStartUserTypeEnum implements ArrayValuable<Integer> {
MAIN_PROCESS_START_USER(1, "同主流程发起人"),
FROM_FORM(2, "表单");
private final Integer type;
private final String name;
public static final Integer[] ARRAYS = Arrays.stream(values()).map(BpmChildProcessStartUserTypeEnum::getType).toArray(Integer[]::new);
public static BpmChildProcessStartUserTypeEnum typeOf(Integer type) {
return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
}
@Override
public Integer[] array() {
return ARRAYS;
}
}

View File

@ -25,10 +25,13 @@ public enum BpmSimpleModelNodeTypeEnum implements ArrayValuable<Integer> {
START_USER_NODE(10, "发起人", "userTask"), // 发起人节点前端的开始节点Id 固定 START_USER_NODE(10, "发起人", "userTask"), // 发起人节点前端的开始节点Id 固定
APPROVE_NODE(11, "审批人", "userTask"), APPROVE_NODE(11, "审批人", "userTask"),
COPY_NODE(12, "抄送人", "serviceTask"), COPY_NODE(12, "抄送人", "serviceTask"),
TRANSACTOR_NODE(13, "办理人", "userTask"),
DELAY_TIMER_NODE(14, "延迟器", "receiveTask"), DELAY_TIMER_NODE(14, "延迟器", "receiveTask"),
TRIGGER_NODE(15, "触发器", "serviceTask"), TRIGGER_NODE(15, "触发器", "serviceTask"),
CHILD_PROCESS(20, "子流程", "callActivity"),
// 50 ~ 条件分支 // 50 ~ 条件分支
CONDITION_NODE(50, "条件", "sequenceFlow"), // 用于构建流转条件的表达式 CONDITION_NODE(50, "条件", "sequenceFlow"), // 用于构建流转条件的表达式
CONDITION_BRANCH_NODE(51, "条件分支", "exclusiveGateway"), CONDITION_BRANCH_NODE(51, "条件分支", "exclusiveGateway"),

View File

@ -16,8 +16,12 @@ import java.util.Arrays;
@AllArgsConstructor @AllArgsConstructor
public enum BpmTriggerTypeEnum implements ArrayValuable<Integer> { public enum BpmTriggerTypeEnum implements ArrayValuable<Integer> {
HTTP_REQUEST(1, "发起 HTTP 请求"), HTTP_REQUEST(1, "发起 HTTP 请求"), // BPM => 业务流程继续执行无需等待业务
UPDATE_NORMAL_FORM(2, "更新流程表单"); // TODO @jasonFORM_UPDATE HTTP_CALLBACK(2, "接收 HTTP 回调"), // BPM => 业务 => BPM流程卡主等待业务回调
FORM_UPDATE(10, "更新流程表单数据"),
FORM_DELETE(11, "删除流程表单数据"),
;
/** /**
* 触发器执行动作类型 * 触发器执行动作类型
@ -39,5 +43,4 @@ public enum BpmTriggerTypeEnum implements ArrayValuable<Integer> {
public static BpmTriggerTypeEnum typeOf(Integer type) { public static BpmTriggerTypeEnum typeOf(Integer type) {
return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); return ArrayUtil.firstMatch(item -> item.getType().equals(type), values());
} }
} }

View File

@ -26,6 +26,7 @@ public enum BpmReasonEnum {
TIMEOUT_REJECT("审批超时,系统自动不通过"), TIMEOUT_REJECT("审批超时,系统自动不通过"),
ASSIGN_START_USER_APPROVE("审批人与提交人为同一人时,自动通过"), ASSIGN_START_USER_APPROVE("审批人与提交人为同一人时,自动通过"),
ASSIGN_START_USER_APPROVE_WHEN_SKIP("审批人与提交人为同一人时,自动通过"), ASSIGN_START_USER_APPROVE_WHEN_SKIP("审批人与提交人为同一人时,自动通过"),
ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE("发起人节点首次自动通过"), // 目前仅子流程使用
ASSIGN_START_USER_APPROVE_WHEN_DEPT_LEADER_NOT_FOUND("审批人与提交人为同一人时,找不到部门负责人,自动通过"), ASSIGN_START_USER_APPROVE_WHEN_DEPT_LEADER_NOT_FOUND("审批人与提交人为同一人时,找不到部门负责人,自动通过"),
ASSIGN_START_USER_TRANSFER_DEPT_LEADER("审批人与提交人为同一人时,转交给部门负责人审批"), ASSIGN_START_USER_TRANSFER_DEPT_LEADER("审批人与提交人为同一人时,转交给部门负责人审批"),
ASSIGN_EMPTY_APPROVE("审批人为空,自动通过"), ASSIGN_EMPTY_APPROVE("审批人为空,自动通过"),

View File

@ -2,11 +2,10 @@ package cn.iocoder.yudao.module.bpm.api.task;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
/** /**
* Flowable 流程实例 Api 实现类 * Flowable 流程实例 Api 实现类
@ -25,4 +24,5 @@ public class BpmProcessInstanceApiImpl implements BpmProcessInstanceApi {
public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO reqDTO) { public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO reqDTO) {
return processInstanceService.createProcessInstance(userId, reqDTO); return processInstanceService.createProcessInstance(userId, reqDTO);
} }
} }

View File

@ -0,0 +1,25 @@
package cn.iocoder.yudao.module.bpm.api.task;
import cn.iocoder.yudao.module.bpm.service.task.BpmTaskService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
/**
* 流程任务 Api 实现类
*
* @author jason
*/
@Service
@Validated
public class BpmProcessTaskApiImpl implements BpmProcessTaskApi {
@Resource
private BpmTaskService bpmTaskService;
@Override
public void triggerTask(String processInstanceId, String taskDefineKey) {
bpmTaskService.triggerTask(processInstanceId, taskDefineKey);
}
}

View File

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

View File

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

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model;
import cn.iocoder.yudao.framework.common.core.KeyValue; import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.validation.InEnum; import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmAutoApproveTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmAutoApproveTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
@ -27,8 +28,7 @@ import java.util.List;
@Data @Data
public class BpmModelMetaInfoVO { public class BpmModelMetaInfoVO {
@Schema(description = "流程图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao.jpg") @Schema(description = "流程图标", example = "https://www.iocoder.cn/yudao.jpg")
@NotEmpty(message = "流程图标不能为空")
@URL(message = "流程图标格式不正确") @URL(message = "流程图标格式不正确")
private String icon; private String icon;
@ -46,6 +46,7 @@ public class BpmModelMetaInfoVO {
private Integer formType; private Integer formType;
@Schema(description = "表单编号", example = "1024") @Schema(description = "表单编号", example = "1024")
private Long formId; // formType NORMAL 使用必须非空 private Long formId; // formType NORMAL 使用必须非空
@Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/create") @Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/create")
private String formCustomCreatePath; // 表单类型为 CUSTOM 必须非空 private String formCustomCreatePath; // 表单类型为 CUSTOM 必须非空
@Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/view") @Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/view")
@ -81,6 +82,14 @@ public class BpmModelMetaInfoVO {
@Schema(description = "摘要设置", example = "{}") @Schema(description = "摘要设置", example = "{}")
private SummarySetting summarySetting; private SummarySetting summarySetting;
// TODO @lesanprocessBeforeTriggerSetting要不叫这个主要考虑notify 留给后续的站内信短信邮件这种 notify 通知哈
@Schema(description = "流程前置通知设置", example = "{}")
private HttpRequestSetting PreProcessNotifySetting;
// TODO @lesanprocessAfterTriggerSetting
@Schema(description = "流程后置通知设置", example = "{}")
private HttpRequestSetting PostProcessNotifySetting;
@Schema(description = "流程 ID 规则") @Schema(description = "流程 ID 规则")
@Data @Data
@Valid @Valid
@ -133,4 +142,32 @@ public class BpmModelMetaInfoVO {
} }
@Schema(description = "http 请求通知设置", example = "{}")
@Data
public static class HttpRequestSetting {
@Schema(description = "请求路径", example = "http://127.0.0.1")
@NotEmpty(message = "请求 URL 不能为空")
@URL(message = "请求 URL 格式不正确")
private String url;
@Schema(description = "请求头参数设置", example = "[]")
@Valid
private List<BpmSimpleModelNodeVO.HttpRequestParam> header;
@Schema(description = "请求头参数设置", example = "[]")
@Valid
private List<BpmSimpleModelNodeVO.HttpRequestParam> body;
/**
* 请求返回处理设置用于修改流程表单值
* <p>
* key表示要修改的流程表单字段名(name)
* value接口返回的字段名
*/
@Schema(description = "请求返回处理设置", example = "[]")
private List<KeyValue<String, String>> response;
}
} }

View File

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

View File

@ -4,16 +4,19 @@ import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.validation.InEnum; import cn.iocoder.yudao.framework.common.validation.InEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.*; import cn.iocoder.yudao.module.bpm.enums.definition.*;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.Data; import lombok.Data;
import org.flowable.bpmn.model.IOParameter;
import org.hibernate.validator.constraints.URL; import org.hibernate.validator.constraints.URL;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
@Schema(description = "管理后台 - 仿钉钉流程设计模型节点 VO") @Schema(description = "管理后台 - 仿钉钉流程设计模型节点 VO")
@Data @Data
@ -114,7 +117,8 @@ public class BpmSimpleModelNodeVO {
@Schema(description = "路由分支组", example = "[]") @Schema(description = "路由分支组", example = "[]")
private List<RouterSetting> routerGroups; private List<RouterSetting> routerGroups;
@Schema(description = "路由分支默认分支 ID", example = "Flow_xxx", hidden = true) // 由后端生成所以 hidden = true @Schema(description = "路由分支默认分支 ID", example = "Flow_xxx", hidden = true) // 由后端生成不从前端传递所以 hidden = true
@JsonIgnore
private String routerDefaultFlowId; // 仅用于路由分支节点 BpmSimpleModelNodeType.ROUTER_BRANCH_NODE private String routerDefaultFlowId; // 仅用于路由分支节点 BpmSimpleModelNodeType.ROUTER_BRANCH_NODE
/** /**
@ -122,6 +126,15 @@ public class BpmSimpleModelNodeVO {
*/ */
private TriggerSetting triggerSetting; private TriggerSetting triggerSetting;
@Schema(description = "附加节点 Id", example = "UserTask_xxx", hidden = true) // 由后端生成不从前端传递所以 hidden = true
@JsonIgnore
private String attachNodeId; // 目前用于触发器节点HTTP 回调需要 UserTask ReceiveTask附加节点) 来完成
/**
* 子流程设置
*/
private ChildProcessSetting childProcessSetting;
@Schema(description = "任务监听器") @Schema(description = "任务监听器")
@Valid @Valid
@Data @Data
@ -345,12 +358,10 @@ public class BpmSimpleModelNodeVO {
@Valid @Valid
private HttpRequestTriggerSetting httpRequestSetting; private HttpRequestTriggerSetting httpRequestSetting;
// TODO @jason这个要不直接叫 formSetting更好理解一点哈
// TODO @jason如果搞成 List<NormalFormTriggerSetting>是不是可以做条件组了微信讨论哈
/** /**
* 流程表单触发器设置 * 流程表单触发器设置
*/ */
private NormalFormTriggerSetting normalFormSetting; private List<FormTriggerSetting> formSettings;
@Schema(description = "http 请求触发器设置", example = "{}") @Schema(description = "http 请求触发器设置", example = "{}")
@Data @Data
@ -369,7 +380,6 @@ public class BpmSimpleModelNodeVO {
@Valid @Valid
private List<HttpRequestParam> body; private List<HttpRequestParam> body;
// TODO @json可能未来看情况搞个 HttpResponseParam得看看有没别的业务需要抽象统一
/** /**
* 请求返回处理设置用于修改流程表单值 * 请求返回处理设置用于修改流程表单值
* <p> * <p>
@ -379,15 +389,137 @@ public class BpmSimpleModelNodeVO {
@Schema(description = "请求返回处理设置", example = "[]") @Schema(description = "请求返回处理设置", example = "[]")
private List<KeyValue<String, String>> response; private List<KeyValue<String, String>> response;
/**
* Http 回调请求需要指定回调任务 Key用于回调执行
*/
@Schema(description = "回调任务 Key", example = "xxx", hidden = true)
private String callbackTaskDefineKey;
} }
@Schema(description = "流程表单触发器设置", example = "{}") @Schema(description = "流程表单触发器设置", example = "{}")
@Data @Data
public static class NormalFormTriggerSetting { public static class FormTriggerSetting {
@Schema(description = "修改的表单字段", example = "userName") @Schema(description = "条件类型", example = "1")
@InEnum(BpmSimpleModeConditionTypeEnum.class)
private Integer conditionType;
@Schema(description = "条件表达式", example = "${day>3}")
private String conditionExpression;
@Schema(description = "条件组", example = "{}")
private ConditionGroups conditionGroups;
@Schema(description = "修改的表单字段", example = "{}")
private Map<String, Object> updateFormFields; private Map<String, Object> updateFormFields;
@Schema(description = "删除表单字段", example = "[]")
private Set<String> deleteFields;
}
}
@Schema(description = "子流程节点配置")
@Data
@Valid
public static class ChildProcessSetting {
@Schema(description = "被调用流程", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxx")
@NotEmpty(message = "被调用流程不能为空")
private String calledProcessDefinitionKey;
@Schema(description = "被调用流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "xxx")
@NotEmpty(message = "被调用流程名称不能为空")
private String calledProcessDefinitionName;
@Schema(description = "是否异步", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
@NotNull(message = "是否异步不能为空")
private Boolean async;
@Schema(description = "输入参数(主->子)", example = "[]")
private List<IOParameter> inVariables;
@Schema(description = "输出参数(子->主)", example = "[]")
private List<IOParameter> outVariables;
@Schema(description = "是否自动跳过子流程发起节点", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
@NotNull(message = "是否自动跳过子流程发起节点不能为空")
private Boolean skipStartUserNode;
@Schema(description = "子流程发起人配置", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}")
@NotNull(message = "子流程发起人配置不能为空")
private StartUserSetting startUserSetting;
@Schema(description = "超时设置", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}")
private TimeoutSetting timeoutSetting;
@Schema(description = "多实例设置", requiredMode = Schema.RequiredMode.REQUIRED, example = "{}")
private MultiInstanceSetting multiInstanceSetting;
@Schema(description = "子流程发起人配置")
@Data
@Valid
public static class StartUserSetting {
@Schema(description = "子流程发起人类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "子流程发起人类型")
@InEnum(BpmChildProcessStartUserTypeEnum.class)
private Integer type;
@Schema(description = "表单", example = "xxx")
private String formField;
@Schema(description = "当子流程发起人为空时类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "当子流程发起人为空时类型不能为空")
@InEnum(BpmChildProcessStartUserEmptyTypeEnum.class)
private Integer emptyType;
}
@Schema(description = "超时设置")
@Data
@Valid
public static class TimeoutSetting {
@Schema(description = "是否开启超时设置", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
@NotNull(message = "是否开启超时设置不能为空")
private Boolean enable;
@Schema(description = "时间类型", example = "1")
@InEnum(BpmDelayTimerTypeEnum.class)
private Integer type;
@Schema(description = "时间表达式", example = "PT1H,2025-01-01T00:00:00")
private String timeExpression;
}
@Schema(description = "多实例设置")
@Data
@Valid
public static class MultiInstanceSetting {
@Schema(description = "是否开启多实例", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
@NotNull(message = "是否开启多实例不能为空")
private Boolean enable;
@Schema(description = "是否串行", requiredMode = Schema.RequiredMode.REQUIRED, example = "false")
@NotNull(message = "是否串行不能为空")
private Boolean sequential;
@Schema(description = "完成比例", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
@NotNull(message = "完成比例不能为空")
private Integer approveRatio;
@Schema(description = "多实例来源类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "多实例来源类型不能为空")
@InEnum(BpmChildProcessMultiInstanceSourceTypeEnum.class)
private Integer sourceType;
@Schema(description = "多实例来源", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
@NotNull(message = "多实例来源不能为空")
private String source;
} }
} }

View File

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

View File

@ -1,8 +1,10 @@
package cn.iocoder.yudao.module.bpm.controller.admin.task; package cn.iocoder.yudao.module.bpm.controller.admin.task;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.number.NumberUtils; import cn.iocoder.yudao.framework.common.util.number.NumberUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*; 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.convert.task.BpmProcessInstanceConvert;
@ -30,10 +32,10 @@ import org.springframework.web.bind.annotation.*;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
@Tag(name = "管理后台 - 流程实例") // 流程实例通过流程定义创建的一次申请 @Tag(name = "管理后台 - 流程实例") // 流程实例通过流程定义创建的一次申请
@ -76,8 +78,14 @@ public class BpmProcessInstanceController {
convertSet(processDefinitionMap.values(), ProcessDefinition::getCategory)); convertSet(processDefinitionMap.values(), ProcessDefinition::getCategory));
Map<String, BpmProcessDefinitionInfoDO> processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap( Map<String, BpmProcessDefinitionInfoDO> processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap(
convertSet(pageResult.getList(), HistoricProcessInstance::getProcessDefinitionId)); convertSet(pageResult.getList(), HistoricProcessInstance::getProcessDefinitionId));
Set<Long> userIds = convertSet(pageResult.getList(), processInstance -> NumberUtils.parseLong(processInstance.getStartUserId()));
userIds.addAll(convertSetByFlatMap(taskMap.values(),
tasks -> tasks.stream().map(Task::getAssignee).filter(StrUtil::isNotBlank).map(Long::parseLong)));
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(userIds);
Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(
convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstancePage(pageResult, return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstancePage(pageResult,
processDefinitionMap, categoryMap, taskMap, null, null, processDefinitionInfoMap)); processDefinitionMap, categoryMap, taskMap, userMap, deptMap, processDefinitionInfoMap));
} }
@GetMapping("/manager-page") @GetMapping("/manager-page")
@ -140,6 +148,7 @@ public class BpmProcessInstanceController {
processDefinition, processDefinitionInfo, startUser, dept)); processDefinition, processDefinitionInfo, startUser, dept));
} }
// TODO @lesan子流程子流程如果取消主流程应该是通过还是不通过哈还是禁用掉子流程的取消
@DeleteMapping("/cancel-by-start-user") @DeleteMapping("/cancel-by-start-user")
@Operation(summary = "用户取消流程实例", description = "取消发起的流程") @Operation(summary = "用户取消流程实例", description = "取消发起的流程")
@PreAuthorize("@ss.hasPermission('bpm:process-instance:cancel')") @PreAuthorize("@ss.hasPermission('bpm:process-instance:cancel')")
@ -162,10 +171,25 @@ public class BpmProcessInstanceController {
@Operation(summary = "获得审批详情") @Operation(summary = "获得审批详情")
@Parameter(name = "id", description = "流程实例的编号", required = true) @Parameter(name = "id", description = "流程实例的编号", required = true)
@PreAuthorize("@ss.hasPermission('bpm:process-instance:query')") @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
@SuppressWarnings("unchecked")
public CommonResult<BpmApprovalDetailRespVO> getApprovalDetail(@Valid BpmApprovalDetailReqVO reqVO) { public CommonResult<BpmApprovalDetailRespVO> getApprovalDetail(@Valid BpmApprovalDetailReqVO reqVO) {
if (StrUtil.isNotEmpty(reqVO.getProcessVariablesStr())) {
reqVO.setProcessVariables(JsonUtils.parseObject(reqVO.getProcessVariablesStr(), Map.class));
}
return success(processInstanceService.getApprovalDetail(getLoginUserId(), reqVO)); return success(processInstanceService.getApprovalDetail(getLoginUserId(), reqVO));
} }
@GetMapping("/get-next-approval-nodes")
@Operation(summary = "获取下一个执行的流程节点")
@PreAuthorize("@ss.hasPermission('bpm:process-instance:query')")
@SuppressWarnings("unchecked")
public CommonResult<List<BpmApprovalDetailRespVO.ActivityNode>> getNextApprovalNodes(@Valid BpmApprovalDetailReqVO reqVO) {
if (StrUtil.isNotEmpty(reqVO.getProcessVariablesStr())) {
reqVO.setProcessVariables(JsonUtils.parseObject(reqVO.getProcessVariablesStr(), Map.class));
}
return success(processInstanceService.getNextApprovalNodes(getLoginUserId(), reqVO));
}
@GetMapping("/get-bpmn-model-view") @GetMapping("/get-bpmn-model-view")
@Operation(summary = "获取流程实例的 BPMN 模型视图", description = "在【流程详细】界面中,进行调用") @Operation(summary = "获取流程实例的 BPMN 模型视图", description = "在【流程详细】界面中,进行调用")
@Parameter(name = "id", description = "流程实例的编号", required = true) @Parameter(name = "id", description = "流程实例的编号", required = true)

View File

@ -18,6 +18,9 @@ public class BpmApprovalDetailReqVO {
@Schema(description = "流程变量") @Schema(description = "流程变量")
private Map<String, Object> processVariables; // 使用场景 processDefinitionId用于流程预测 private Map<String, Object> processVariables; // 使用场景 processDefinitionId用于流程预测
@Schema(description = "流程变量")
private String processVariablesStr; // 解决 GET 无法传递对象的问题最终转换成 processVariables 变量
@Schema(description = "流程实例的编号", example = "1024") @Schema(description = "流程实例的编号", example = "1024")
private String processInstanceId; // 使用场景流程已发起时候传流程实例 ID private String processInstanceId; // 使用场景流程已发起时候传流程实例 ID

View File

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

View File

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

View File

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

View File

@ -85,6 +85,9 @@ public class BpmTaskRespVO {
@Schema(description = "是否填写审批意见", example = "false") @Schema(description = "是否填写审批意见", example = "false")
private Boolean reasonRequire; private Boolean reasonRequire;
@Schema(description = "节点类型", example = "10")
private Integer nodeType; // 参见 BpmSimpleModelNodeTypeEnum 枚举
@Data @Data
@Schema(description = "流程实例") @Schema(description = "流程实例")
public static class ProcessInstance { public static class ProcessInstance {

View File

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

View File

@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.bpm.dal.dataobject.definition;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler;
import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmAutoApproveTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmAutoApproveTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
@ -60,6 +59,14 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
*/ */
private Integer modelType; private Integer modelType;
/**
* 流程分类的编码
*
* 关联 {@link BpmCategoryDO#getCode()}
*
* 为什么要存储原因是{@link ProcessDefinition#getCategory()} 无法设置
*/
private String category;
/** /**
* 图标 * 图标
*/ */
@ -149,7 +156,7 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
* *
* 关联 {@link AdminUserRespDTO#getId()} 字段的数组 * 关联 {@link AdminUserRespDTO#getId()} 字段的数组
*/ */
@TableField(typeHandler = StringListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤 @TableField(typeHandler = LongListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤
private List<Long> managerUserIds; private List<Long> managerUserIds;
/** /**
@ -175,11 +182,22 @@ public class BpmProcessDefinitionInfoDO extends BaseDO {
*/ */
@TableField(typeHandler = JacksonTypeHandler.class) @TableField(typeHandler = JacksonTypeHandler.class)
private BpmModelMetaInfoVO.TitleSetting titleSetting; private BpmModelMetaInfoVO.TitleSetting titleSetting;
/** /**
* 摘要设置 * 摘要设置
*/ */
@TableField(typeHandler = JacksonTypeHandler.class) @TableField(typeHandler = JacksonTypeHandler.class)
private BpmModelMetaInfoVO.SummarySetting summarySetting; private BpmModelMetaInfoVO.SummarySetting summarySetting;
// TODO @lesanprocessBeforeTriggerSetting要不叫这个主要考虑notify 留给后续的站内信短信邮件这种 notify 通知哈
/**
* 流程前置通知设置
*/
@TableField(typeHandler = JacksonTypeHandler.class, exist = false) // TODO @芋艿临时注释 exist因为要合并 master-jdk17
private BpmModelMetaInfoVO.HttpRequestSetting PreProcessNotifySetting;
/**
* 流程后置通知设置
*/
@TableField(typeHandler = JacksonTypeHandler.class, exist = false) // TODO @芋艿临时注释 exist因为要合并 master-jdk17
private BpmModelMetaInfoVO.HttpRequestSetting PostProcessNotifySetting;
} }

View File

@ -1,15 +1,21 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior; package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior;
import cn.hutool.core.collection.CollUtil; 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 cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessMultiInstanceSourceTypeEnum;
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 lombok.Setter; import lombok.Setter;
import org.flowable.bpmn.model.Activity; import org.flowable.bpmn.model.Activity;
import org.flowable.bpmn.model.CallActivity;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior;
import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior; import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior;
import java.util.List;
import java.util.Set; import java.util.Set;
/** /**
@ -42,6 +48,8 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
*/ */
@Override @Override
protected int resolveNrOfInstances(DelegateExecution execution) { protected int resolveNrOfInstances(DelegateExecution execution) {
// 情况一UserTask 节点
if (execution.getCurrentFlowElement() instanceof UserTask) {
// 第一步设置 collectionVariable CollectionVariable // 第一步设置 collectionVariable CollectionVariable
// execution.getVariable() 读取所有任务处理人的 key // execution.getVariable() 读取所有任务处理人的 key
super.collectionExpression = null; // collectionExpression collectionVariable 是互斥的 super.collectionExpression = null; // collectionExpression collectionVariable 是互斥的
@ -65,4 +73,19 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav
return assigneeUserIds.size(); return assigneeUserIds.size();
} }
// 情况二CallActivity 节点
if (execution.getCurrentFlowElement() instanceof CallActivity) {
FlowElement flowElement = execution.getCurrentFlowElement();
Integer sourceType = BpmnModelUtils.parseMultiInstanceSourceType(flowElement);
if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType())) {
return execution.getVariable(super.collectionExpression.getExpressionText(), Integer.class);
}
if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) {
return execution.getVariable(super.collectionExpression.getExpressionText(), List.class).size();
}
}
return super.resolveNrOfInstances(execution);
}
} }

View File

@ -2,14 +2,20 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils; import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessMultiInstanceSourceTypeEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; 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.framework.flowable.core.util.FlowableUtils;
import lombok.Setter; import lombok.Setter;
import org.flowable.bpmn.model.Activity; import org.flowable.bpmn.model.Activity;
import org.flowable.bpmn.model.CallActivity;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.AbstractBpmnActivityBehavior;
import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior; import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior;
import java.util.List;
import java.util.Set; import java.util.Set;
/** /**
@ -35,6 +41,8 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
*/ */
@Override @Override
protected int resolveNrOfInstances(DelegateExecution execution) { protected int resolveNrOfInstances(DelegateExecution execution) {
// 情况一UserTask 节点
if (execution.getCurrentFlowElement() instanceof UserTask) {
// 第一步设置 collectionVariable CollectionVariable // 第一步设置 collectionVariable CollectionVariable
// execution.getVariable() 读取所有任务处理人的 key // execution.getVariable() 读取所有任务处理人的 key
super.collectionExpression = null; // collectionExpression collectionVariable 是互斥的 super.collectionExpression = null; // collectionExpression collectionVariable 是互斥的
@ -59,4 +67,19 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB
return assigneeUserIds.size(); return assigneeUserIds.size();
} }
// 情况二CallActivity 节点
if (execution.getCurrentFlowElement() instanceof CallActivity) {
FlowElement flowElement = execution.getCurrentFlowElement();
Integer sourceType = BpmnModelUtils.parseMultiInstanceSourceType(flowElement);
if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType())) {
return execution.getVariable(super.collectionExpression.getExpressionText(), Integer.class);
}
if (sourceType.equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) {
return execution.getVariable(super.collectionExpression.getExpressionText(), List.class).size();
}
}
return super.resolveNrOfInstances(execution);
}
} }

View File

@ -19,6 +19,7 @@ import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.BpmnModel; import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.CallActivity;
import org.flowable.bpmn.model.FlowElement; import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.UserTask; import org.flowable.bpmn.model.UserTask;
import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.delegate.DelegateExecution;
@ -129,8 +130,12 @@ public class BpmTaskCandidateInvoker {
public Set<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId, public Set<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId,
Long startUserId, String processDefinitionId, Map<String, Object> processVariables) { Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
// 审批类型非人工审核时不进行计算候选人原因是后续会自动通过不通过 // 如果是 CallActivity 子流程不进行计算候选人
FlowElement flowElement = BpmnModelUtils.getFlowElementById(bpmnModel, activityId); FlowElement flowElement = BpmnModelUtils.getFlowElementById(bpmnModel, activityId);
if (flowElement instanceof CallActivity) {
return new HashSet<>();
}
// 审批类型非人工审核时不进行计算候选人原因是后续会自动通过不通过
Integer approveType = BpmnModelUtils.parseApproveType(flowElement); Integer approveType = BpmnModelUtils.parseApproveType(flowElement);
if (ObjectUtils.equalsAny(approveType, if (ObjectUtils.equalsAny(approveType,
BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType(), BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType(),

View File

@ -0,0 +1,78 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.lang.Assert;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import com.google.common.collect.Sets;
import jakarta.annotation.Resource;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
/**
* 审批人自选 {@link BpmTaskCandidateUserStrategy} 实现类
* 审批人在审批时选择下一个节点的审批人
*
* @author smallNorthLee
*/
@Component
public class BpmTaskCandidateApproveUserSelectStrategy extends AbstractBpmTaskCandidateDeptLeaderStrategy {
@Resource
@Lazy // 延迟加载避免循环依赖
private BpmProcessInstanceService processInstanceService;
@Override
public BpmTaskCandidateStrategyEnum getStrategy() {
return BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT;
}
@Override
public void validateParam(String param) {}
@Override
public boolean isParamRequired() {
return false;
}
@Override
public LinkedHashSet<Long> calculateUsersByTask(DelegateExecution execution, String param) {
ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId());
Assert.notNull(processInstance, "流程实例({})不能为空", execution.getProcessInstanceId());
Map<String, List<Long>> approveUserSelectAssignees = FlowableUtils.getApproveUserSelectAssignees(processInstance);
Assert.notNull(approveUserSelectAssignees, "流程实例({}) 的下一个执行节点审批人不能为空",
execution.getProcessInstanceId());
if (approveUserSelectAssignees == null) {
return Sets.newLinkedHashSet();
}
// 获得审批人
List<Long> assignees = approveUserSelectAssignees.get(execution.getCurrentActivityId());
return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet();
}
@Override
public LinkedHashSet<Long> calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param,
Long startUserId, String processDefinitionId, Map<String, Object> processVariables) {
if (processVariables == null) {
return Sets.newLinkedHashSet();
}
// 流程预测时会使用允许审批人为空如果为空前端会弹出提示选择下一个节点审批人避免流程无法进行审批时会真正校验节点是否配置审批人
Map<String, List<Long>> approveUserSelectAssignees = FlowableUtils.getApproveUserSelectAssignees(processVariables);
if (approveUserSelectAssignees == null) {
return Sets.newLinkedHashSet();
}
// 获得审批人
List<Long> assignees = approveUserSelectAssignees.get(activityId);
return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet();
}
}

View File

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

View File

@ -24,7 +24,8 @@ public enum BpmTaskCandidateStrategyEnum implements ArrayValuable<Integer> {
MULTI_DEPT_LEADER_MULTI(23, "连续多级部门的负责人"), MULTI_DEPT_LEADER_MULTI(23, "连续多级部门的负责人"),
POST(22, "岗位"), POST(22, "岗位"),
USER(30, "用户"), USER(30, "用户"),
START_USER_SELECT(35, "发起人自选"), // 申请人自己可在提交申请时选择此节点的审批人 APPROVE_USER_SELECT(34, "审批人自身"), // 当前审批人可在审批时选择下一个节点的审批人
START_USER_SELECT(35, "发起人自选"), // 申请人自己可在提交申请时选择此节点的审批人
START_USER(36, "发起人自己"), // 申请人自己, 一般紧挨开始节点常用于发起人信息审核场景 START_USER(36, "发起人自己"), // 申请人自己, 一般紧挨开始节点常用于发起人信息审核场景
START_USER_DEPT_LEADER(37, "发起人部门负责人"), START_USER_DEPT_LEADER(37, "发起人部门负责人"),
START_USER_DEPT_LEADER_MULTI(38, "发起人连续多级部门的负责人"), START_USER_DEPT_LEADER_MULTI(38, "发起人连续多级部门的负责人"),

View File

@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.enums; package cn.iocoder.yudao.module.bpm.framework.flowable.core.enums;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
/** /**
* BPMN XML 常量信息 * BPMN XML 常量信息
* *
@ -66,6 +68,11 @@ public interface BpmnModelConstants {
*/ */
String USER_TASK_APPROVE_METHOD = "approveMethod"; String USER_TASK_APPROVE_METHOD = "approveMethod";
/**
* BPMN Child Process 的扩展属性用于标记多实例来源类型
*/
String CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE = "childProcessMultiInstanceSourceType";
/** /**
* BPMN ExtensionElement 流程表单字段权限元素, 用于标记字段权限 * BPMN ExtensionElement 流程表单字段权限元素, 用于标记字段权限
*/ */
@ -129,4 +136,11 @@ public interface BpmnModelConstants {
*/ */
String REASON_REQUIRE = "reasonRequire"; String REASON_REQUIRE = "reasonRequire";
/**
* 节点类型
*
* 目前只有 {@link BpmModelTypeEnum#SIMPLE} UserTask 节点会设置该属性用于区分是审批节点还是办理节点
*/
String NODE_TYPE = "nodeType";
} }

View File

@ -27,8 +27,16 @@ public class BpmnVariableConstants {
* 流程实例的变量 - 发起用户选择的审批人 Map * 流程实例的变量 - 发起用户选择的审批人 Map
* *
* @see ProcessInstance#getProcessVariables() * @see ProcessInstance#getProcessVariables()
* @see BpmTaskCandidateStrategyEnum#START_USER_SELECT
*/ */
public static final String PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES = "PROCESS_START_USER_SELECT_ASSIGNEES"; public static final String PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES = "PROCESS_START_USER_SELECT_ASSIGNEES";
/**
* 流程实例的变量 - 审批人选择的审批人 Map
*
* @see ProcessInstance#getProcessVariables()
* @see BpmTaskCandidateStrategyEnum#APPROVE_USER_SELECT
*/
public static final String PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES = "PROCESS_APPROVE_USER_SELECT_ASSIGNEES";
/** /**
* 流程实例的变量 - 发起用户 ID * 流程实例的变量 - 发起用户 ID
* *
@ -51,6 +59,13 @@ public class BpmnVariableConstants {
*/ */
public static final String PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED = "_FLOWABLE_SKIP_EXPRESSION_ENABLED"; public static final String PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED = "_FLOWABLE_SKIP_EXPRESSION_ENABLED";
/**
* 流程实例的变量 - 用于判断流程是否需要跳过发起人节点
*
* @see ProcessInstance#getProcessVariables()
*/
public static final String PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE = "PROCESS_SKIP_START_USER_NODE";
/** /**
* 流程实例的变量 - 流程开始时间 * 流程实例的变量 - 流程开始时间
* *

View File

@ -22,6 +22,7 @@ import java.util.Set;
public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEventListener { public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEventListener {
public static final Set<FlowableEngineEventType> PROCESS_INSTANCE_EVENTS = ImmutableSet.<FlowableEngineEventType>builder() public static final Set<FlowableEngineEventType> PROCESS_INSTANCE_EVENTS = ImmutableSet.<FlowableEngineEventType>builder()
.add(FlowableEngineEventType.PROCESS_CREATED)
.add(FlowableEngineEventType.PROCESS_COMPLETED) .add(FlowableEngineEventType.PROCESS_COMPLETED)
.add(FlowableEngineEventType.PROCESS_CANCELLED) .add(FlowableEngineEventType.PROCESS_CANCELLED)
.build(); .build();
@ -34,6 +35,11 @@ public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEvent
super(PROCESS_INSTANCE_EVENTS); super(PROCESS_INSTANCE_EVENTS);
} }
@Override
protected void processCreated(FlowableEngineEntityEvent event) {
processInstanceService.processProcessInstanceCreated((ProcessInstance)event.getEntity());
}
@Override @Override
protected void processCompleted(FlowableEngineEntityEvent event) { protected void processCompleted(FlowableEngineEntityEvent event) {
processInstanceService.processProcessInstanceCompleted((ProcessInstance)event.getEntity()); processInstanceService.processProcessInstanceCompleted((ProcessInstance)event.getEntity());

View File

@ -109,7 +109,11 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener {
// 2.2 延迟器超时处理 // 2.2 延迟器超时处理
} else if (ObjectUtil.equal(bpmTimerBoundaryEventType, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT)) { } else if (ObjectUtil.equal(bpmTimerBoundaryEventType, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT)) {
String taskKey = boundaryEvent.getAttachedToRefId(); String taskKey = boundaryEvent.getAttachedToRefId();
taskService.processDelayTimerTimeout(event.getProcessInstanceId(), taskKey); taskService.triggerTask(event.getProcessInstanceId(), taskKey);
// 2.3 子流程超时处理
} else if (ObjectUtil.equal(bpmTimerBoundaryEventType, BpmBoundaryEventTypeEnum.CHILD_PROCESS_TIMEOUT)) {
String taskKey = boundaryEvent.getAttachedToRefId();
taskService.processChildProcessTimeout(event.getProcessInstanceId(), taskKey);
} }
} }

View File

@ -0,0 +1,159 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmHttpRequestParamTypeEnum;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import com.fasterxml.jackson.core.type.TypeReference;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR;
/**
* 工作流发起 HTTP 请求工具类
*
* @author 芋道源码
*/
@Slf4j
public class BpmHttpRequestUtils {
public static void executeBpmHttpRequest(ProcessInstance processInstance,
String url,
List<BpmSimpleModelNodeVO.HttpRequestParam> headerParams,
List<BpmSimpleModelNodeVO.HttpRequestParam> bodyParams,
Boolean handleResponse,
List<KeyValue<String, String>> response,
// TODO @lesanRestTemplate 直接通过 springUtil 获取好咧
RestTemplate restTemplate,
// TODO @lesanprocessInstanceService 直接通过 springUtil 获取好咧
BpmProcessInstanceService processInstanceService) {
// 1.1 设置请求头
MultiValueMap<String, String> headers = buildHttpHeaders(processInstance, headerParams);
// 1.2 设置请求体
MultiValueMap<String, String> body = buildHttpBody(processInstance, bodyParams);
// 2. 发起请求
ResponseEntity<String> responseEntity = sendHttpRequest(url, headers, body, restTemplate);
// 3. 处理返回
// TODO @lesan可以用 if return让括号小点
if (Boolean.TRUE.equals(handleResponse)) {
// 3.1 判断是否需要解析返回值
if (responseEntity == null
|| StrUtil.isEmpty(responseEntity.getBody())
|| !responseEntity.getStatusCode().is2xxSuccessful()
|| CollUtil.isEmpty(response)) {
return;
}
// 3.2 解析返回值, 返回值必须符合 CommonResult 规范
CommonResult<Map<String, Object>> respResult = JsonUtils.parseObjectQuietly(responseEntity.getBody(),
new TypeReference<>() {});
if (respResult == null || !respResult.isSuccess()) {
return;
}
// 3.3 获取需要更新的流程变量
Map<String, Object> updateVariables = getNeedUpdatedVariablesFromResponse(respResult.getData(), response);
// 3.4 更新流程变量
if (CollUtil.isNotEmpty(updateVariables)) {
processInstanceService.updateProcessInstanceVariables(processInstance.getId(), updateVariables);
}
}
}
public static ResponseEntity<String> sendHttpRequest(String url,
MultiValueMap<String, String> headers,
MultiValueMap<String, String> body,
RestTemplate restTemplate) {
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, headers);
ResponseEntity<String> responseEntity;
try {
responseEntity = restTemplate.exchange(url, HttpMethod.POST, requestEntity, String.class);
log.info("[sendHttpRequest][HTTP 触发器,请求头:{},请求体:{},响应结果:{}]", headers, body, responseEntity);
} catch (RestClientException e) {
log.error("[sendHttpRequest][HTTP 触发器,请求头:{},请求体:{},请求出错:{}]", headers, body, e.getMessage());
throw exception(PROCESS_INSTANCE_HTTP_TRIGGER_CALL_ERROR);
}
return responseEntity;
}
public static MultiValueMap<String, String> buildHttpHeaders(ProcessInstance processInstance,
List<BpmSimpleModelNodeVO.HttpRequestParam> headerSettings) {
Map<String, Object> processVariables = processInstance.getProcessVariables();
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add(HEADER_TENANT_ID, processInstance.getTenantId());
addHttpRequestParam(headers, headerSettings, processVariables);
return headers;
}
public static MultiValueMap<String, String> buildHttpBody(ProcessInstance processInstance,
List<BpmSimpleModelNodeVO.HttpRequestParam> bodySettings) {
Map<String, Object> processVariables = processInstance.getProcessVariables();
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
addHttpRequestParam(body, bodySettings, processVariables);
body.add("processInstanceId", processInstance.getId());
return body;
}
/**
* 从请求返回值获取需要更新的流程变量
*
* @param result 请求返回结果
* @param responseSettings 返回设置
* @return 需要更新的流程变量
*/
public static Map<String, Object> getNeedUpdatedVariablesFromResponse(Map<String, Object> result,
List<KeyValue<String, String>> responseSettings) {
Map<String, Object> updateVariables = new HashMap<>();
if (CollUtil.isEmpty(result)) {
return updateVariables;
}
responseSettings.forEach(responseSetting -> {
if (StrUtil.isNotEmpty(responseSetting.getKey()) && result.containsKey(responseSetting.getValue())) {
updateVariables.put(responseSetting.getKey(), result.get(responseSetting.getValue()));
}
});
return updateVariables;
}
/**
* 添加 HTTP 请求参数请求头或者请求体
*
* @param params HTTP 请求参数
* @param paramSettings HTTP 请求参数设置
* @param processVariables 流程变量
*/
public static void addHttpRequestParam(MultiValueMap<String, String> params,
List<BpmSimpleModelNodeVO.HttpRequestParam> paramSettings,
Map<String, Object> processVariables) {
if (CollUtil.isEmpty(paramSettings)) {
return;
}
paramSettings.forEach(item -> {
if (item.getType().equals(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType())) {
params.add(item.getKey(), item.getValue());
} else if (item.getType().equals(BpmHttpRequestParamTypeEnum.FROM_FORM.getType())) {
params.add(item.getKey(), processVariables.get(item.getValue()).toString());
}
});
}
}

View File

@ -158,6 +158,17 @@ public class BpmnModelUtils {
return NumberUtils.parseInt(parseExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_TYPE)); return NumberUtils.parseInt(parseExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_TYPE));
} }
/**
* 解析子流程多实例来源类型
*
* @see BpmChildProcessMultiInstanceSourceTypeEnum
* @param element 任务节点
* @return 多实例来源类型
*/
public static Integer parseMultiInstanceSourceType(FlowElement element) {
return NumberUtils.parseInt(parseExtensionElement(element, BpmnModelConstants.CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE));
}
/** /**
* 添加任务拒绝处理元素 * 添加任务拒绝处理元素
* *
@ -410,6 +421,26 @@ public class BpmnModelUtils {
return parseExtensionElement(flowElement, TRIGGER_PARAM); return parseExtensionElement(flowElement, TRIGGER_PARAM);
} }
/**
* 给节点添加节点类型
*
* @param nodeType 节点类型
* @param flowElement 节点
*/
public static void addNodeType(Integer nodeType, FlowElement flowElement) {
addExtensionElement(flowElement, BpmnModelConstants.NODE_TYPE, nodeType);
}
/**
* 解析节点类型
*
* @param flowElement 节点
* @return 节点类型
*/
public static Integer parseNodeType(FlowElement flowElement) {
return NumberUtils.parseInt(parseExtensionElement(flowElement, BpmnModelConstants.NODE_TYPE));
}
// ========== BPM 简单查找相关的方法 ========== // ========== BPM 简单查找相关的方法 ==========
/** /**
@ -777,10 +808,116 @@ public class BpmnModelUtils {
// 情况ExclusiveGateway 排它只有一个满足条件的如果没有就走默认的 // 情况ExclusiveGateway 排它只有一个满足条件的如果没有就走默认的
if (currentElement instanceof ExclusiveGateway) { if (currentElement instanceof ExclusiveGateway) {
// 查找满足条件的 SequenceFlow 路径 // 查找满足条件的 SequenceFlow 路径
SequenceFlow matchSequenceFlow = findMatchSequenceFlowByExclusiveGateway((Gateway) currentElement, variables);
// 遍历满足条件的 SequenceFlow 路径
if (matchSequenceFlow != null) {
simulateNextFlowElements(matchSequenceFlow.getTargetFlowElement(), variables, resultElements, visitElements);
}
}
// 情况InclusiveGateway 包容多个满足条件的如果没有就走默认的
else if (currentElement instanceof InclusiveGateway) {
// 查找满足条件的 SequenceFlow 路径
Collection<SequenceFlow> matchSequenceFlows = findMatchSequenceFlowsByInclusiveGateway((Gateway) currentElement, variables);
// 遍历满足条件的 SequenceFlow 路径
matchSequenceFlows.forEach(
flow -> simulateNextFlowElements(flow.getTargetFlowElement(), variables, resultElements, visitElements));
}
// 情况ParallelGateway 并行都满足都走
else if (currentElement instanceof ParallelGateway) {
Gateway gateway = (Gateway) currentElement; Gateway gateway = (Gateway) currentElement;
SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), // 遍历子节点
gateway.getOutgoingFlows().forEach(
nextElement -> simulateNextFlowElements(nextElement.getTargetFlowElement(), variables, resultElements, visitElements));
}
}
/**
* 根据当前节点获取下一个节点
*
* @param currentElement 当前节点
* @param bpmnModel BPMN模型
* @param variables 流程变量
*/
@SuppressWarnings("PatternVariableCanBeUsed")
public static List<FlowNode> getNextFlowNodes(FlowElement currentElement, BpmnModel bpmnModel,
Map<String, Object> variables){
List<FlowNode> nextFlowNodes = new ArrayList<>(); // 下一个执行的流程节点集合
FlowNode currentNode = (FlowNode) currentElement; // 当前执行节点的基本属性
List<SequenceFlow> outgoingFlows = currentNode.getOutgoingFlows(); // 当前节点的关联节点
if (CollUtil.isEmpty(outgoingFlows)) {
log.warn("[getNextFlowNodes][当前节点({}) 的 outgoingFlows 为空]", currentNode.getId());
return nextFlowNodes;
}
// 遍历每个出口流
for (SequenceFlow outgoingFlow : outgoingFlows) {
// 获取目标节点的基本属性
FlowElement targetElement = bpmnModel.getFlowElement(outgoingFlow.getTargetRef());
if (targetElement == null) {
continue;
}
// 如果是结束节点直接返回
if (targetElement instanceof EndEvent) {
break;
}
// 情况一处理不同类型的网关
if (targetElement instanceof Gateway) {
Gateway gateway = (Gateway) targetElement;
if (gateway instanceof ExclusiveGateway) {
handleExclusiveGateway(gateway, bpmnModel, variables, nextFlowNodes);
} else if (gateway instanceof InclusiveGateway) {
handleInclusiveGateway(gateway, bpmnModel, variables, nextFlowNodes);
} else if (gateway instanceof ParallelGateway) {
handleParallelGateway(gateway, bpmnModel, variables, nextFlowNodes);
}
} else {
// 情况二如果不是网关直接添加到下一个节点列表
nextFlowNodes.add((FlowNode) targetElement);
}
}
return nextFlowNodes;
}
/**
* 处理排它网关
*
* @param gateway 排他网关
* @param bpmnModel BPMN模型
* @param variables 流程变量
* @param nextFlowNodes 下一个执行的流程节点集合
*/
private static void handleExclusiveGateway(Gateway gateway, BpmnModel bpmnModel,
Map<String, Object> variables, List<FlowNode> nextFlowNodes) {
// 查找满足条件的 SequenceFlow 路径
SequenceFlow matchSequenceFlow = findMatchSequenceFlowByExclusiveGateway(gateway, variables);
// 遍历满足条件的 SequenceFlow 路径
if (matchSequenceFlow != null) {
FlowElement targetElement = bpmnModel.getFlowElement(matchSequenceFlow.getTargetRef());
if (targetElement instanceof FlowNode) {
nextFlowNodes.add((FlowNode) targetElement);
}
}
}
/**
* 处理排它网关Exclusive Gateway选择符合条件的路径
*
* @param gateway 排他网关
* @param variables 流程变量
* @return 符合条件的路径
*/
private static SequenceFlow findMatchSequenceFlowByExclusiveGateway(Gateway gateway, Map<String, Object> variables) {
// TODO 表单无可编辑字段时variables为空流程走向会出现问题比如流程审批过程中无需要修改的字段值
// TODO @小北是不是还是保证编辑的时候如果计算下一个节点还是 variables 是完整体而不是空的可以微信讨论下
SequenceFlow matchSequenceFlow;
if (CollUtil.isNotEmpty(variables)) {
matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
&& evalConditionExpress(variables, flow.getConditionExpression())); && (evalConditionExpress(variables, flow.getConditionExpression())));
} else {
matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()));
}
if (matchSequenceFlow == null) { if (matchSequenceFlow == null) {
matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(),
flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId())); flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId()));
@ -789,17 +926,39 @@ public class BpmnModelUtils {
matchSequenceFlow = gateway.getOutgoingFlows().get(0); matchSequenceFlow = gateway.getOutgoingFlows().get(0);
} }
} }
// 遍历满足条件的 SequenceFlow 路径 return matchSequenceFlow;
if (matchSequenceFlow != null) {
simulateNextFlowElements(matchSequenceFlow.getTargetFlowElement(), variables, resultElements, visitElements);
}
return;
} }
// 情况InclusiveGateway 包容多个满足条件的如果没有就走默认的 /**
if (currentElement instanceof InclusiveGateway) { * 处理包容网关
*
* @param gateway 排他网关
* @param bpmnModel BPMN模型
* @param variables 流程变量
* @param nextFlowNodes 下一个执行的流程节点集合
*/
private static void handleInclusiveGateway(Gateway gateway, BpmnModel bpmnModel,
Map<String, Object> variables, List<FlowNode> nextFlowNodes) {
// 查找满足条件的 SequenceFlow 路径集合
Collection<SequenceFlow> matchSequenceFlows = findMatchSequenceFlowsByInclusiveGateway(gateway, variables);
// 遍历满足条件的 SequenceFlow 路径获取目标节点
matchSequenceFlows.forEach(flow -> {
FlowElement targetElement = bpmnModel.getFlowElement(flow.getTargetRef());
if (targetElement instanceof FlowNode) {
nextFlowNodes.add((FlowNode) targetElement);
}
});
}
/**
* 处理排它网关Inclusive Gateway选择符合条件的路径
*
* @param gateway 排他网关
* @param variables 流程变量
* @return 符合条件的路径
*/
private static Collection<SequenceFlow> findMatchSequenceFlowsByInclusiveGateway(Gateway gateway, Map<String, Object> variables) {
// 查找满足条件的 SequenceFlow 路径 // 查找满足条件的 SequenceFlow 路径
Gateway gateway = (Gateway) currentElement;
Collection<SequenceFlow> matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(), Collection<SequenceFlow> matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(),
flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())
&& evalConditionExpress(variables, flow.getConditionExpression())); && evalConditionExpress(variables, flow.getConditionExpression()));
@ -811,37 +970,52 @@ public class BpmnModelUtils {
matchSequenceFlows = gateway.getOutgoingFlows(); matchSequenceFlows = gateway.getOutgoingFlows();
} }
} }
// 遍历满足条件的 SequenceFlow 路径 return matchSequenceFlows;
matchSequenceFlows.forEach(
flow -> simulateNextFlowElements(flow.getTargetFlowElement(), variables, resultElements, visitElements));
} }
// 情况ParallelGateway 并行都满足都走
if (currentElement instanceof ParallelGateway) { /**
Gateway gateway = (Gateway) currentElement; * 处理并行网关
// 遍历子节点 *
gateway.getOutgoingFlows().forEach( * @param gateway 排他网关
nextElement -> simulateNextFlowElements(nextElement.getTargetFlowElement(), variables, resultElements, visitElements)); * @param bpmnModel BPMN模型
return; * @param variables 流程变量
* @param nextFlowNodes 下一个执行的流程节点集合
*/
private static void handleParallelGateway(Gateway gateway, BpmnModel bpmnModel,
Map<String, Object> variables, List<FlowNode> nextFlowNodes) {
// 并行网关遍历所有出口路径获取目标节点
gateway.getOutgoingFlows().forEach(flow -> {
FlowElement targetElement = bpmnModel.getFlowElement(flow.getTargetRef());
if (targetElement instanceof FlowNode) {
nextFlowNodes.add((FlowNode) targetElement);
} }
});
} }
/** /**
* 计算条件表达式是否为 true 满足条件 * 计算条件表达式是否为 true 满足条件
* *
* @param variables 流程实例 * @param variables 流程实例
* @param express 条件表达式 * @param expression 条件表达式
* @return 是否满足条件 * @return 是否满足条件
*/ */
public static boolean evalConditionExpress(Map<String, Object> variables, String express) { public static boolean evalConditionExpress(Map<String, Object> variables, String expression) {
if (express == null) { if (expression == null) {
return Boolean.FALSE; return Boolean.FALSE;
} }
// 如果 variables 为空则创建一个的原因可能 expression 的计算不依赖于 variables
if (variables == null) {
variables = new HashMap<>();
}
// 执行计算
try { try {
Object result = FlowableUtils.getExpressionValue(variables, express); Object result = FlowableUtils.getExpressionValue(variables, expression);
return Boolean.TRUE.equals(result); return Boolean.TRUE.equals(result);
} catch (FlowableException ex) { } catch (FlowableException ex) {
log.error("[evalConditionExpress][条件表达式({}) 变量({}) 解析报错]", express, variables, ex); // 为什么使用 info 日志原因是expression 如果从 variables 取不到值会报错实际这种情况下可以忽略
log.info("[evalConditionExpress][条件表达式({}) 变量({}) 解析报错]", expression, variables, ex);
return Boolean.FALSE; return Boolean.FALSE;
} }
} }

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.bpm.framework.flowable.core.util; package cn.iocoder.yudao.module.bpm.framework.flowable.core.util;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.extra.spring.SpringUtil; import cn.hutool.extra.spring.SpringUtil;
import cn.iocoder.yudao.framework.common.core.KeyValue; import cn.iocoder.yudao.framework.common.core.KeyValue;
@ -24,7 +25,10 @@ import org.flowable.engine.impl.util.CommandContextUtil;
import org.flowable.engine.runtime.ProcessInstance; import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.TaskInfo; import org.flowable.task.api.TaskInfo;
import java.util.*; import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -190,12 +194,37 @@ public class FlowableUtils {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public static Map<String, List<Long>> getStartUserSelectAssignees(Map<String, Object> processVariables) { public static Map<String, List<Long>> getStartUserSelectAssignees(Map<String, Object> processVariables) {
if (processVariables == null) { if (processVariables == null) {
return null; return new HashMap<>();
} }
return (Map<String, List<Long>>) processVariables.get( return (Map<String, List<Long>>) processVariables.get(
BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES); BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES);
} }
/**
* 获得流程实例的审批用户选择的下一个节点的审批人 Map
*
* @param processInstance 流程实例
* @return 审批用户选择的下一个节点的审批人Map
*/
public static Map<String, List<Long>> getApproveUserSelectAssignees(ProcessInstance processInstance) {
return processInstance != null ? getApproveUserSelectAssignees(processInstance.getProcessVariables()) : null;
}
/**
* 获得流程实例的审批用户选择的下一个节点的审批人 Map
*
* @param processVariables 流程变量
* @return 审批用户选择的下一个节点的审批人Map Map
*/
@SuppressWarnings("unchecked")
public static Map<String, List<Long>> getApproveUserSelectAssignees(Map<String, Object> processVariables) {
if (processVariables == null) {
return new HashMap<>();
}
return (Map<String, List<Long>>) processVariables.get(
BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES);
}
/** /**
* 获得流程实例的摘要 * 获得流程实例的摘要
* *
@ -240,7 +269,7 @@ public class FlowableUtils {
return formFieldsMap.entrySet().stream() return formFieldsMap.entrySet().stream()
.limit(3) .limit(3)
.map(entry -> new KeyValue<>(entry.getValue().getTitle(), .map(entry -> new KeyValue<>(entry.getValue().getTitle(),
processVariables.getOrDefault(entry.getValue().getField(), "").toString())) MapUtil.getStr(processVariables, entry.getValue().getField(), "")))
.collect(Collectors.toList()); .collect(Collectors.toList());
} }

View File

@ -5,25 +5,27 @@ import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil; import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.*; import cn.hutool.core.util.*;
import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
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;
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.ConditionGroups;
import cn.iocoder.yudao.module.bpm.enums.definition.*; import cn.iocoder.yudao.module.bpm.enums.definition.*;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmCopyTaskDelegate; import cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmCopyTaskDelegate;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmTriggerTaskDelegate; import cn.iocoder.yudao.module.bpm.framework.flowable.core.listener.BpmTriggerTaskDelegate;
import cn.iocoder.yudao.module.bpm.service.task.listener.BpmCallActivityListener;
import cn.iocoder.yudao.module.bpm.service.task.listener.BpmUserTaskListener;
import org.flowable.bpmn.BpmnAutoLayout; import org.flowable.bpmn.BpmnAutoLayout;
import org.flowable.bpmn.constants.BpmnXMLConstants; import org.flowable.bpmn.constants.BpmnXMLConstants;
import org.flowable.bpmn.model.Process; import org.flowable.bpmn.model.Process;
import org.flowable.bpmn.model.*; import org.flowable.bpmn.model.*;
import org.flowable.engine.delegate.ExecutionListener;
import org.flowable.engine.delegate.TaskListener; import org.flowable.engine.delegate.TaskListener;
import org.springframework.util.MultiValueMap;
import java.util.*; import java.util.*;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.*; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.*;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*;
import static cn.iocoder.yudao.module.bpm.service.task.listener.BpmUserTaskListener.DELEGATE_EXPRESSION;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
/** /**
@ -40,9 +42,10 @@ public class SimpleModelUtils {
static { static {
List<NodeConvert> converts = asList(new StartNodeConvert(), new EndNodeConvert(), List<NodeConvert> converts = asList(new StartNodeConvert(), new EndNodeConvert(),
new StartUserNodeConvert(), new ApproveNodeConvert(), new CopyNodeConvert(), new StartUserNodeConvert(), new ApproveNodeConvert(), new CopyNodeConvert(), new TransactorNodeConvert(),
new DelayTimerNodeConvert(), new TriggerNodeConvert(), new DelayTimerNodeConvert(), new TriggerNodeConvert(),
new ConditionBranchNodeConvert(), new ParallelBranchNodeConvert(), new InclusiveBranchNodeConvert(), new RouteBranchNodeConvert()); new ConditionBranchNodeConvert(), new ParallelBranchNodeConvert(), new InclusiveBranchNodeConvert(), new RouteBranchNodeConvert(),
new ChildProcessConvert());
converts.forEach(convert -> NODE_CONVERTS.put(convert.getType(), convert)); converts.forEach(convert -> NODE_CONVERTS.put(convert.getType(), convert));
} }
@ -78,7 +81,7 @@ public class SimpleModelUtils {
traverseNodeToBuildFlowNode(startNode, process); traverseNodeToBuildFlowNode(startNode, process);
// 3. 构建并添加节点之间的连线 Sequence Flow // 3. 构建并添加节点之间的连线 Sequence Flow
EndEvent endEvent = BpmnModelUtils.getEndEvent(bpmnModel); EndEvent endEvent = getEndEvent(bpmnModel);
traverseNodeToBuildSequenceFlow(process, startNode, endEvent.getId()); traverseNodeToBuildSequenceFlow(process, startNode, endEvent.getId());
// 4. 自动布局 // 4. 自动布局
@ -164,8 +167,16 @@ public class SimpleModelUtils {
// 情况一节点则建立连线 // 情况一节点则建立连线
// 情况二没有子节点则直接跟 targetNodeId 建立连线例如说结束节点条件分支分支节点的孩子节点或聚合节点的最后一个节点 // 情况二没有子节点则直接跟 targetNodeId 建立连线例如说结束节点条件分支分支节点的孩子节点或聚合节点的最后一个节点
String finalTargetNodeId = isChildNodeValid ? childNode.getId() : targetNodeId; String finalTargetNodeId = isChildNodeValid ? childNode.getId() : targetNodeId;
// 如果没有附加节点则直接建立连线
if (StrUtil.isEmpty(node.getAttachNodeId())) {
SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), finalTargetNodeId); SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), finalTargetNodeId);
process.addFlowElement(sequenceFlow); process.addFlowElement(sequenceFlow);
} else {
// 如果有附加节点需要先建立和附加节点的连线再建立附加节点和目标节点的连线例如说触发器节点HTTP 回调
List<SequenceFlow> sequenceFlows = buildAttachNodeSequenceFlow(node.getId(), node.getAttachNodeId(), finalTargetNodeId);
sequenceFlows.forEach(process::addFlowElement);
}
// 因为有子节点递归调用后续子节点 // 因为有子节点递归调用后续子节点
if (isChildNodeValid) { if (isChildNodeValid) {
@ -173,6 +184,19 @@ public class SimpleModelUtils {
} }
} }
/**
* 构建有附加节点的连线
*
* @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);
}
/** /**
* 遍历条件节点构建 SequenceFlow 元素 * 遍历条件节点构建 SequenceFlow 元素
* *
@ -337,7 +361,7 @@ public class SimpleModelUtils {
userTask.setName(node.getName()); userTask.setName(node.getName());
// 人工审批 // 人工审批
addExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_TYPE, BpmUserTaskApproveTypeEnum.USER.getType()); addExtensionElement(userTask, USER_TASK_APPROVE_TYPE, BpmUserTaskApproveTypeEnum.USER.getType());
// 候选人策略为发起人自己 // 候选人策略为发起人自己
addCandidateElements(BpmTaskCandidateStrategyEnum.START_USER.getStrategy(), null, userTask); addCandidateElements(BpmTaskCandidateStrategyEnum.START_USER.getStrategy(), null, userTask);
// 添加表单字段权限属性元素 // 添加表单字段权限属性元素
@ -388,24 +412,17 @@ public class SimpleModelUtils {
*/ */
private BoundaryEvent buildUserTaskTimeoutBoundaryEvent(UserTask userTask, private BoundaryEvent buildUserTaskTimeoutBoundaryEvent(UserTask userTask,
BpmSimpleModelNodeVO.TimeoutHandler timeoutHandler) { BpmSimpleModelNodeVO.TimeoutHandler timeoutHandler) {
// 1.1 定时器边界事件 // 1. 创建 Timeout Boundary Event
BoundaryEvent boundaryEvent = new BoundaryEvent(); String timeCycle = null;
boundaryEvent.setId("Event-" + IdUtil.fastUUID());
boundaryEvent.setCancelActivity(false); // 设置关联的任务为不会被中断
boundaryEvent.setAttachedToRef(userTask);
// 1.2 定义超时时间最大提醒次数
TimerEventDefinition eventDefinition = new TimerEventDefinition();
eventDefinition.setTimeDuration(timeoutHandler.getTimeDuration());
if (Objects.equals(BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType(), timeoutHandler.getType()) && if (Objects.equals(BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType(), timeoutHandler.getType()) &&
timeoutHandler.getMaxRemindCount() != null && timeoutHandler.getMaxRemindCount() > 1) { timeoutHandler.getMaxRemindCount() != null && timeoutHandler.getMaxRemindCount() > 1) {
eventDefinition.setTimeCycle(String.format("R%d/%s", timeCycle = String.format("R%d/%s",
timeoutHandler.getMaxRemindCount(), timeoutHandler.getTimeDuration())); timeoutHandler.getMaxRemindCount(), timeoutHandler.getTimeDuration());
} }
boundaryEvent.addEventDefinition(eventDefinition); BoundaryEvent boundaryEvent = buildTimeoutBoundaryEvent(userTask, BpmBoundaryEventTypeEnum.USER_TASK_TIMEOUT.getType(),
timeoutHandler.getTimeDuration(), timeCycle, null);
// 2.1 添加定时器边界事件类型 // 2 添加超时执行动作元素
addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, BpmBoundaryEventTypeEnum.USER_TASK_TIMEOUT.getType());
// 2.2 添加超时执行动作元素
addExtensionElement(boundaryEvent, USER_TASK_TIMEOUT_HANDLER_TYPE, timeoutHandler.getType()); addExtensionElement(boundaryEvent, USER_TASK_TIMEOUT_HANDLER_TYPE, timeoutHandler.getType());
return boundaryEvent; return boundaryEvent;
} }
@ -445,6 +462,8 @@ public class SimpleModelUtils {
addSignEnable(node.getSignEnable(), userTask); addSignEnable(node.getSignEnable(), userTask);
// 审批意见 // 审批意见
addReasonRequire(node.getReasonRequire(), userTask); addReasonRequire(node.getReasonRequire(), userTask);
// 节点类型
addNodeType(node.getType(), userTask);
return userTask; return userTask;
} }
@ -455,7 +474,7 @@ public class SimpleModelUtils {
FlowableListener flowableListener = new FlowableListener(); FlowableListener flowableListener = new FlowableListener();
flowableListener.setEvent(TaskListener.EVENTNAME_CREATE); flowableListener.setEvent(TaskListener.EVENTNAME_CREATE);
flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
flowableListener.setImplementation(DELEGATE_EXPRESSION); flowableListener.setImplementation(BpmUserTaskListener.DELEGATE_EXPRESSION);
addListenerConfig(flowableListener, node.getTaskCreateListener()); addListenerConfig(flowableListener, node.getTaskCreateListener());
flowableListeners.add(flowableListener); flowableListeners.add(flowableListener);
} }
@ -464,7 +483,7 @@ public class SimpleModelUtils {
FlowableListener flowableListener = new FlowableListener(); FlowableListener flowableListener = new FlowableListener();
flowableListener.setEvent(TaskListener.EVENTNAME_ASSIGNMENT); flowableListener.setEvent(TaskListener.EVENTNAME_ASSIGNMENT);
flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
flowableListener.setImplementation(DELEGATE_EXPRESSION); flowableListener.setImplementation(BpmUserTaskListener.DELEGATE_EXPRESSION);
addListenerConfig(flowableListener, node.getTaskAssignListener()); addListenerConfig(flowableListener, node.getTaskAssignListener());
flowableListeners.add(flowableListener); flowableListeners.add(flowableListener);
} }
@ -473,7 +492,7 @@ public class SimpleModelUtils {
FlowableListener flowableListener = new FlowableListener(); FlowableListener flowableListener = new FlowableListener();
flowableListener.setEvent(TaskListener.EVENTNAME_COMPLETE); flowableListener.setEvent(TaskListener.EVENTNAME_COMPLETE);
flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
flowableListener.setImplementation(DELEGATE_EXPRESSION); flowableListener.setImplementation(BpmUserTaskListener.DELEGATE_EXPRESSION);
addListenerConfig(flowableListener, node.getTaskCompleteListener()); addListenerConfig(flowableListener, node.getTaskCompleteListener());
flowableListeners.add(flowableListener); flowableListeners.add(flowableListener);
} }
@ -486,7 +505,7 @@ public class SimpleModelUtils {
BpmUserTaskApproveMethodEnum approveMethodEnum = BpmUserTaskApproveMethodEnum.valueOf(approveMethod); BpmUserTaskApproveMethodEnum approveMethodEnum = BpmUserTaskApproveMethodEnum.valueOf(approveMethod);
Assert.notNull(approveMethodEnum, "审批方式({})不能为空", approveMethodEnum); Assert.notNull(approveMethodEnum, "审批方式({})不能为空", approveMethodEnum);
// 添加审批方式的扩展属性 // 添加审批方式的扩展属性
addExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_METHOD, approveMethod); addExtensionElement(userTask, USER_TASK_APPROVE_METHOD, approveMethod);
if (approveMethodEnum == BpmUserTaskApproveMethodEnum.RANDOM) { if (approveMethodEnum == BpmUserTaskApproveMethodEnum.RANDOM) {
// 随机审批不需要设置多实例属性 // 随机审批不需要设置多实例属性
return; return;
@ -514,6 +533,15 @@ public class SimpleModelUtils {
} }
private static class TransactorNodeConvert extends ApproveNodeConvert {
@Override
public BpmSimpleModelNodeTypeEnum getType() {
return BpmSimpleModelNodeTypeEnum.TRANSACTOR_NODE;
}
}
private static class CopyNodeConvert implements NodeConvert { private static class CopyNodeConvert implements NodeConvert {
@Override @Override
@ -684,20 +712,16 @@ public class SimpleModelUtils {
// 2. 添加接收任务的 Timer Boundary Event // 2. 添加接收任务的 Timer Boundary Event
if (node.getDelaySetting() != null) { if (node.getDelaySetting() != null) {
// 2.1 定时器边界事件 BoundaryEvent boundaryEvent = null;
BoundaryEvent boundaryEvent = new BoundaryEvent();
boundaryEvent.setId("Event-" + IdUtil.fastUUID());
boundaryEvent.setCancelActivity(false);
boundaryEvent.setAttachedToRef(receiveTask);
// 2.2 定义超时时间
TimerEventDefinition eventDefinition = new TimerEventDefinition();
if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())) { if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())) {
eventDefinition.setTimeDuration(node.getDelaySetting().getDelayTime()); boundaryEvent = buildTimeoutBoundaryEvent(receiveTask, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(),
node.getDelaySetting().getDelayTime(), null, null);
} else if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) { } else if (node.getDelaySetting().getDelayType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) {
eventDefinition.setTimeDate(node.getDelaySetting().getDelayTime()); boundaryEvent = buildTimeoutBoundaryEvent(receiveTask, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(),
null, null, node.getDelaySetting().getDelayTime());
} else {
throw new UnsupportedOperationException("不支持的延迟类型:" + node.getDelaySetting());
} }
boundaryEvent.addEventDefinition(eventDefinition);
addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType());
flowElements.add(boundaryEvent); flowElements.add(boundaryEvent);
} }
return flowElements; return flowElements;
@ -712,23 +736,36 @@ public class SimpleModelUtils {
public static class TriggerNodeConvert implements NodeConvert { public static class TriggerNodeConvert implements NodeConvert {
@Override @Override
public ServiceTask convert(BpmSimpleModelNodeVO node) { public List<? extends FlowElement> convertList(BpmSimpleModelNodeVO node) {
Assert.notNull(node.getTriggerSetting(), "触发器节点设置不能为空");
List<FlowElement> flowElements = new ArrayList<>(2);
// HTTP 回调请求需要附加一个 ReceiveTask发起请求后等待回调执行
if (BpmTriggerTypeEnum.HTTP_CALLBACK.getType().equals(node.getTriggerSetting().getType())) {
Assert.notNull(node.getTriggerSetting().getHttpRequestSetting(), "触发器 HTTP 回调请求设置不能为空");
ReceiveTask receiveTask = new ReceiveTask();
receiveTask.setId("Activity_" + IdUtil.fastUUID());
receiveTask.setName("HTTP 回调");
node.setAttachNodeId(receiveTask.getId());
flowElements.add(receiveTask);
// 重要设置 callbackTaskDefineKey用于 HTTP 回调
node.getTriggerSetting().getHttpRequestSetting().setCallbackTaskDefineKey(receiveTask.getId());
}
// 触发器使用 ServiceTask 来实现 // 触发器使用 ServiceTask 来实现
ServiceTask serviceTask = new ServiceTask(); ServiceTask serviceTask = new ServiceTask();
serviceTask.setId(node.getId()); serviceTask.setId(node.getId());
serviceTask.setName(node.getName()); serviceTask.setName(node.getName());
serviceTask.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); serviceTask.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
serviceTask.setImplementation("${" + BpmTriggerTaskDelegate.BEAN_NAME + "}"); serviceTask.setImplementation("${" + BpmTriggerTaskDelegate.BEAN_NAME + "}");
if (node.getTriggerSetting() != null) {
addExtensionElement(serviceTask, TRIGGER_TYPE, node.getTriggerSetting().getType()); addExtensionElement(serviceTask, TRIGGER_TYPE, node.getTriggerSetting().getType());
if (node.getTriggerSetting().getHttpRequestSetting() != null) { if (node.getTriggerSetting().getHttpRequestSetting() != null) {
addExtensionElementJson(serviceTask, TRIGGER_PARAM, node.getTriggerSetting().getHttpRequestSetting()); addExtensionElementJson(serviceTask, TRIGGER_PARAM, node.getTriggerSetting().getHttpRequestSetting());
} }
if (node.getTriggerSetting().getNormalFormSetting() != null) { if (node.getTriggerSetting().getFormSettings() != null) {
addExtensionElementJson(serviceTask, TRIGGER_PARAM, node.getTriggerSetting().getNormalFormSetting()); addExtensionElementJson(serviceTask, TRIGGER_PARAM, node.getTriggerSetting().getFormSettings());
} }
} flowElements.add(serviceTask);
return serviceTask; return flowElements;
} }
@Override @Override
@ -762,10 +799,131 @@ public class SimpleModelUtils {
} }
private static class ChildProcessConvert implements NodeConvert {
@Override
public List<FlowElement> convertList(BpmSimpleModelNodeVO node) {
List<FlowElement> flowElements = new ArrayList<>(2);
BpmSimpleModelNodeVO.ChildProcessSetting childProcessSetting = node.getChildProcessSetting();
List<IOParameter> inVariables = childProcessSetting.getInVariables() == null ?
new ArrayList<>() : new ArrayList<>(childProcessSetting.getInVariables());
CallActivity callActivity = new CallActivity();
callActivity.setId(node.getId());
callActivity.setName(node.getName());
callActivity.setCalledElementType("key");
// 1. 是否异步
if (node.getChildProcessSetting().getAsync()) {
// TODO @lesan: 这里目前测试没有跳过执行call activity 后面的节点
callActivity.setAsynchronous(true);
}
// 2. 调用的子流程
callActivity.setCalledElement(childProcessSetting.getCalledProcessDefinitionKey());
callActivity.setProcessInstanceName(childProcessSetting.getCalledProcessDefinitionName());
// 3. 是否自动跳过子流程发起节点
IOParameter ioParameter = new IOParameter();
ioParameter.setSourceExpression(childProcessSetting.getSkipStartUserNode().toString());
ioParameter.setTarget(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE);
inVariables.add(ioParameter);
// 4. 默认需要传递的一些变量流程状态
ioParameter = new IOParameter();
ioParameter.setSource(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS);
ioParameter.setTarget(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS);
inVariables.add(ioParameter);
// 5. 子变量传递->主变量传递
callActivity.setInParameters(inVariables);
if (ArrayUtil.isNotEmpty(childProcessSetting.getOutVariables()) && ObjUtil.notEqual(childProcessSetting.getAsync(), Boolean.TRUE)) {
callActivity.setOutParameters(childProcessSetting.getOutVariables());
}
// 6. 子流程发起人配置
List<FlowableListener> executionListeners = new ArrayList<>();
FlowableListener flowableListener = new FlowableListener();
flowableListener.setEvent(ExecutionListener.EVENTNAME_START);
flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION);
flowableListener.setImplementation(BpmCallActivityListener.DELEGATE_EXPRESSION);
FieldExtension fieldExtension = new FieldExtension();
fieldExtension.setFieldName("listenerConfig");
fieldExtension.setStringValue(JsonUtils.toJsonString(childProcessSetting.getStartUserSetting()));
flowableListener.getFieldExtensions().add(fieldExtension);
executionListeners.add(flowableListener);
callActivity.setExecutionListeners(executionListeners);
// 7. 超时设置
if (childProcessSetting.getTimeoutSetting() != null && Boolean.TRUE.equals(childProcessSetting.getTimeoutSetting().getEnable())) {
BoundaryEvent boundaryEvent = null;
if (childProcessSetting.getTimeoutSetting().getType().equals(BpmDelayTimerTypeEnum.FIXED_DATE_TIME.getType())) {
boundaryEvent = buildTimeoutBoundaryEvent(callActivity, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT.getType(),
childProcessSetting.getTimeoutSetting().getTimeExpression(), null, null);
} else if (childProcessSetting.getTimeoutSetting().getType().equals(BpmDelayTimerTypeEnum.FIXED_TIME_DURATION.getType())) {
boundaryEvent = buildTimeoutBoundaryEvent(callActivity, BpmBoundaryEventTypeEnum.CHILD_PROCESS_TIMEOUT.getType(),
null, null, childProcessSetting.getTimeoutSetting().getTimeExpression());
}
flowElements.add(boundaryEvent);
}
// 8. 多实例
if (childProcessSetting.getMultiInstanceSetting() != null && Boolean.TRUE.equals(childProcessSetting.getMultiInstanceSetting().getEnable())) {
MultiInstanceLoopCharacteristics multiInstanceCharacteristics = new MultiInstanceLoopCharacteristics();
multiInstanceCharacteristics.setSequential(childProcessSetting.getMultiInstanceSetting().getSequential());
if (childProcessSetting.getMultiInstanceSetting().getSourceType().equals(BpmChildProcessMultiInstanceSourceTypeEnum.FIXED_QUANTITY.getType())) {
multiInstanceCharacteristics.setLoopCardinality(childProcessSetting.getMultiInstanceSetting().getSource());
}
if (childProcessSetting.getMultiInstanceSetting().getSourceType().equals(BpmChildProcessMultiInstanceSourceTypeEnum.NUMBER_FORM.getType()) ||
childProcessSetting.getMultiInstanceSetting().getSourceType().equals(BpmChildProcessMultiInstanceSourceTypeEnum.MULTIPLE_FORM.getType())) {
multiInstanceCharacteristics.setInputDataItem(childProcessSetting.getMultiInstanceSetting().getSource());
}
multiInstanceCharacteristics.setCompletionCondition(String.format(BpmUserTaskApproveMethodEnum.RATIO.getCompletionCondition(),
String.format("%.2f", childProcessSetting.getMultiInstanceSetting().getApproveRatio() / 100D)));
callActivity.setLoopCharacteristics(multiInstanceCharacteristics);
addExtensionElement(callActivity, CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE, childProcessSetting.getMultiInstanceSetting().getSourceType());
}
// 添加节点类型
addNodeType(node.getType(), callActivity);
flowElements.add(callActivity);
return flowElements;
}
@Override
public BpmSimpleModelNodeTypeEnum getType() {
return BpmSimpleModelNodeTypeEnum.CHILD_PROCESS;
}
}
private static String buildGatewayJoinId(String id) { private static String buildGatewayJoinId(String id) {
return id + "_join"; return id + "_join";
} }
private static BoundaryEvent buildTimeoutBoundaryEvent(Activity attachedToRef, Integer type,
String timeDuration, String timeCycle, String timeDate) {
// 1.1 定时器边界事件
BoundaryEvent boundaryEvent = new BoundaryEvent();
boundaryEvent.setId("Event-" + IdUtil.fastUUID());
boundaryEvent.setCancelActivity(false); // 设置关联的任务为不会被中断
boundaryEvent.setAttachedToRef(attachedToRef);
// 1.2 定义超时时间表达式
TimerEventDefinition eventDefinition = new TimerEventDefinition();
if (ObjUtil.isNotNull(timeDuration)) {
eventDefinition.setTimeDuration(timeDuration);
}
if (ObjUtil.isNotNull(timeDuration)) {
eventDefinition.setTimeCycle(timeCycle);
}
if (ObjUtil.isNotNull(timeDate)) {
eventDefinition.setTimeDate(timeDate);
}
boundaryEvent.addEventDefinition(eventDefinition);
// 2. 添加定时器边界事件类型
addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, type);
return boundaryEvent;
}
// ========== SIMPLE 流程预测相关的方法 ========== // ========== SIMPLE 流程预测相关的方法 ==========
public static List<BpmSimpleModelNodeVO> simulateProcess(BpmSimpleModelNodeVO rootNode, Map<String, Object> variables) { public static List<BpmSimpleModelNodeVO> simulateProcess(BpmSimpleModelNodeVO rootNode, Map<String, Object> variables) {
@ -785,11 +943,13 @@ public class SimpleModelUtils {
BpmSimpleModelNodeTypeEnum nodeType = BpmSimpleModelNodeTypeEnum.valueOf(currentNode.getType()); BpmSimpleModelNodeTypeEnum nodeType = BpmSimpleModelNodeTypeEnum.valueOf(currentNode.getType());
Assert.notNull(nodeType, "模型节点类型不支持"); Assert.notNull(nodeType, "模型节点类型不支持");
// 情况START_NODE/START_USER_NODE/APPROVE_NODE/COPY_NODE/END_NODE // 情况START_NODE/START_USER_NODE/APPROVE_NODE/COPY_NODE/END_NODE/TRANSACTOR_NODE
if (nodeType == BpmSimpleModelNodeTypeEnum.START_NODE if (nodeType == BpmSimpleModelNodeTypeEnum.START_NODE
|| nodeType == BpmSimpleModelNodeTypeEnum.START_USER_NODE || nodeType == BpmSimpleModelNodeTypeEnum.START_USER_NODE
|| nodeType == BpmSimpleModelNodeTypeEnum.APPROVE_NODE || nodeType == BpmSimpleModelNodeTypeEnum.APPROVE_NODE
|| nodeType == BpmSimpleModelNodeTypeEnum.TRANSACTOR_NODE
|| nodeType == BpmSimpleModelNodeTypeEnum.COPY_NODE || nodeType == BpmSimpleModelNodeTypeEnum.COPY_NODE
|| nodeType == BpmSimpleModelNodeTypeEnum.CHILD_PROCESS
|| nodeType == BpmSimpleModelNodeTypeEnum.END_NODE) { || nodeType == BpmSimpleModelNodeTypeEnum.END_NODE) {
// 添加元素 // 添加元素
resultNodes.add(currentNode); resultNodes.add(currentNode);
@ -841,27 +1001,4 @@ public class SimpleModelUtils {
return BpmnModelUtils.evalConditionExpress(variables, buildConditionExpression(conditionSetting)); return BpmnModelUtils.evalConditionExpress(variables, buildConditionExpression(conditionSetting));
} }
// TODO @芋艿要不要优化下抽个 HttpUtils
/**
* 添加 HTTP 请求参数请求头或者请求体
*
* @param params HTTP 请求参数
* @param paramSettings HTTP 请求参数设置
* @param processVariables 流程变量
*/
public static void addHttpRequestParam(MultiValueMap<String, String> params,
List<BpmSimpleModelNodeVO.HttpRequestParam> paramSettings,
Map<String, Object> processVariables) {
if (CollUtil.isEmpty(paramSettings)) {
return;
}
paramSettings.forEach(item -> {
if (item.getType().equals(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType())) {
params.add(item.getKey(), item.getValue());
} else if (item.getType().equals(BpmHttpRequestParamTypeEnum.FROM_FORM.getType())) {
params.add(item.getKey(), processVariables.get(item.getValue()).toString());
}
});
}
} }

View File

@ -16,6 +16,7 @@ import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
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.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils;
@ -23,9 +24,7 @@ import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceCopyService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import jakarta.validation.Valid; import jakarta.validation.Valid;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.flowable.bpmn.model.BpmnModel; import org.flowable.bpmn.model.*;
import org.flowable.bpmn.model.StartEvent;
import org.flowable.bpmn.model.UserTask;
import org.flowable.common.engine.impl.db.SuspensionState; import org.flowable.common.engine.impl.db.SuspensionState;
import org.flowable.engine.HistoryService; import org.flowable.engine.HistoryService;
import org.flowable.engine.RepositoryService; import org.flowable.engine.RepositoryService;
@ -41,13 +40,12 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import java.util.List; import java.util.*;
import java.util.Map;
import java.util.Objects;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseCandidateStrategy;
/** /**
* 流程模型实现主要进行 Flowable {@link Model} 的维护 * 流程模型实现主要进行 Flowable {@link Model} 的维护
@ -209,11 +207,11 @@ public class BpmModelServiceImpl implements BpmModelService {
public void deployModel(Long userId, String id) { public void deployModel(Long userId, String id) {
// 1.1 校验流程模型存在 // 1.1 校验流程模型存在
Model model = validateModelManager(id, userId); Model model = validateModelManager(id, userId);
BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model);
// 1.2 校验流程图 // 1.2 校验流程图
byte[] bpmnBytes = getModelBpmnXML(model.getId()); byte[] bpmnBytes = getModelBpmnXML(model.getId());
validateBpmnXml(bpmnBytes); validateBpmnXml(bpmnBytes, metaInfo.getType());
// 1.3 校验表单已配 // 1.3 校验表单已配
BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model);
BpmFormDO form = validateFormConfig(metaInfo); BpmFormDO form = validateFormConfig(metaInfo);
// 1.4 校验任务分配规则已配置 // 1.4 校验任务分配规则已配置
taskCandidateInvoker.validateBpmnConfig(bpmnBytes); taskCandidateInvoker.validateBpmnConfig(bpmnBytes);
@ -233,7 +231,7 @@ public class BpmModelServiceImpl implements BpmModelService {
repositoryService.saveModel(model); repositoryService.saveModel(model);
} }
private void validateBpmnXml(byte[] bpmnBytes) { private void validateBpmnXml(byte[] bpmnBytes, Integer type) {
BpmnModel bpmnModel = BpmnModelUtils.getBpmnModel(bpmnBytes); BpmnModel bpmnModel = BpmnModelUtils.getBpmnModel(bpmnBytes);
if (bpmnModel == null) { if (bpmnModel == null) {
throw exception(MODEL_NOT_EXISTS); throw exception(MODEL_NOT_EXISTS);
@ -250,6 +248,17 @@ public class BpmModelServiceImpl implements BpmModelService {
throw exception(MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS, userTask.getId()); throw exception(MODEL_DEPLOY_FAIL_BPMN_USER_TASK_NAME_NOT_EXISTS, userTask.getId());
} }
}); });
// TODO @小北是不是可以 UserTask firUserTask = CollUtil.get(userTasks, BpmModelTypeEnum.BPMN.getType().equals(type) ? 0 : 1)然后最好判空极端情况下 usertask 哈哈哈哈
// 3. 校验第一个用户任务节点的规则类型是否为审批人自选
Map<Integer, UserTask> userTaskMap = new HashMap<>();
// BPMN 设计器校验第一个用户任务节点
userTaskMap.put(BpmModelTypeEnum.BPMN.getType(), userTasks.get(0));
// SIMPLE 设计器第一个节点固定为发起人所以校验第二个用户任务节点
userTaskMap.put(BpmModelTypeEnum.SIMPLE.getType(), userTasks.get(1));
Integer candidateStrategy = parseCandidateStrategy(userTaskMap.get(type));
if (Objects.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy())) {
throw exception(MODEL_DEPLOY_FAIL_FIRST_USER_TASK_CANDIDATE_STRATEGY_ERROR, userTaskMap.get(type).getName());
}
} }
@Override @Override

View File

@ -28,8 +28,7 @@ import java.util.*;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.addIfNotNull; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.addIfNotNull;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_DEFINITION_KEY_NOT_MATCH; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.PROCESS_DEFINITION_NAME_NOT_MATCH;
import static java.util.Collections.emptyList; import static java.util.Collections.emptyList;
/** /**
@ -144,9 +143,8 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
// 插入拓展表 // 插入拓展表
BpmProcessDefinitionInfoDO definitionDO = BeanUtils.toBean(modelMetaInfo, BpmProcessDefinitionInfoDO.class) BpmProcessDefinitionInfoDO definitionDO = BeanUtils.toBean(modelMetaInfo, BpmProcessDefinitionInfoDO.class)
.setModelId(model.getId()).setProcessDefinitionId(definition.getId()) .setModelId(model.getId()).setCategory(model.getCategory()).setProcessDefinitionId(definition.getId())
.setModelType(modelMetaInfo.getType()).setSimpleModel(simpleJson); .setModelType(modelMetaInfo.getType()).setSimpleModel(simpleJson);
if (form != null) { if (form != null) {
definitionDO.setFormFields(form.getFields()).setFormConf(form.getConf()); definitionDO.setFormFields(form.getFields()).setFormConf(form.getConf());
} }
@ -156,16 +154,25 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ
@Override @Override
public void updateProcessDefinitionState(String id, Integer state) { public void updateProcessDefinitionState(String id, Integer state) {
ProcessDefinition processDefinition = repositoryService.getProcessDefinition(id);
if (processDefinition == null) {
throw exception(PROCESS_DEFINITION_NOT_EXISTS);
}
// 激活 // 激活
if (Objects.equals(SuspensionState.ACTIVE.getStateCode(), state)) { if (Objects.equals(SuspensionState.ACTIVE.getStateCode(), state)) {
if (processDefinition.isSuspended()) {
repositoryService.activateProcessDefinitionById(id, false, null); repositoryService.activateProcessDefinitionById(id, false, null);
}
return; return;
} }
// 挂起 // 挂起
if (Objects.equals(SuspensionState.SUSPENDED.getStateCode(), state)) { if (Objects.equals(SuspensionState.SUSPENDED.getStateCode(), state)) {
// suspendProcessInstances = false进行中的任务不进行挂起 // suspendProcessInstances = false进行中的任务不进行挂起
// 原因只要新的流程不允许发起即可老流程继续可以执行 // 原因只要新的流程不允许发起即可老流程继续可以执行
if (!processDefinition.isSuspended()) {
repositoryService.suspendProcessDefinitionById(id, false, null); repositoryService.suspendProcessDefinitionById(id, false, null);
}
return; return;
} }
log.error("[updateProcessDefinitionState][流程定义({}) 修改未知状态({})]", id, state); log.error("[updateProcessDefinitionState][流程定义({}) 修改未知状态({})]", id, state);

View File

@ -7,6 +7,7 @@ import jakarta.validation.Valid;
import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.runtime.ProcessInstance; import org.flowable.engine.runtime.ProcessInstance;
import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
@ -84,7 +85,6 @@ public interface BpmProcessInstanceService {
PageResult<HistoricProcessInstance> getProcessInstancePage(Long userId, PageResult<HistoricProcessInstance> getProcessInstancePage(Long userId,
@Valid BpmProcessInstancePageReqVO pageReqVO); @Valid BpmProcessInstancePageReqVO pageReqVO);
// TODO @芋艿重点在 review
/** /**
* 获取审批详情 * 获取审批详情
* <p> * <p>
@ -96,6 +96,15 @@ public interface BpmProcessInstanceService {
*/ */
BpmApprovalDetailRespVO getApprovalDetail(Long loginUserId, @Valid BpmApprovalDetailReqVO reqVO); BpmApprovalDetailRespVO getApprovalDetail(Long loginUserId, @Valid BpmApprovalDetailReqVO reqVO);
/**
* 获取下一个执行节点信息
*
* @param loginUserId 登录人的用户编号
* @param reqVO 请求信息
* @return 下一个执行节点信息
*/
List<BpmApprovalDetailRespVO.ActivityNode> getNextApprovalNodes(Long loginUserId, @Valid BpmApprovalDetailReqVO reqVO);
/** /**
* 获取流程实例的 BPMN 模型视图 * 获取流程实例的 BPMN 模型视图
* *
@ -148,6 +157,22 @@ public interface BpmProcessInstanceService {
*/ */
void updateProcessInstanceReject(ProcessInstance processInstance, String reason); void updateProcessInstanceReject(ProcessInstance processInstance, String reason);
/**
* 更新 ProcessInstance 的变量
*
* @param id 流程编号
* @param variables 流程变量
*/
void updateProcessInstanceVariables(String id, Map<String, Object> variables);
/**
* 删除 ProcessInstance 的变量
*
* @param id 流程编号
* @param variableNames 流程变量名
*/
void removeProcessInstanceVariables(String id, Collection<String> variableNames);
// ========== Event 事件相关方法 ========== // ========== Event 事件相关方法 ==========
/** /**
@ -158,11 +183,9 @@ public interface BpmProcessInstanceService {
void processProcessInstanceCompleted(ProcessInstance instance); void processProcessInstanceCompleted(ProcessInstance instance);
/** /**
* 更新 ProcessInstance 的变量 * 处理 ProcessInstance 开始事件例如说流程前置通知
* *
* @param id 流程编号 * @param instance 流程任务
* @param variables 流程变量
*/ */
void updateProcessInstanceVariables(String id, Map<String, Object> variables); void processProcessInstanceCreated(ProcessInstance instance);
} }

View File

@ -5,6 +5,7 @@ import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.DateUtil; import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.pojo.PageResult;
@ -13,6 +14,7 @@ import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.framework.common.util.object.ObjectUtils; import cn.iocoder.yudao.framework.common.util.object.ObjectUtils;
import cn.iocoder.yudao.framework.common.util.object.PageUtils; import cn.iocoder.yudao.framework.common.util.object.PageUtils;
import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO;
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.BpmModelMetaInfoVO;
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;
import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*; import cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.*;
@ -28,10 +30,11 @@ import cn.iocoder.yudao.module.bpm.enums.task.BpmProcessInstanceStatusEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.BpmTaskCandidateInvoker;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.dept.BpmTaskCandidateStartUserSelectStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.event.BpmProcessInstanceEventPublisher; import cn.iocoder.yudao.module.bpm.framework.flowable.core.event.BpmProcessInstanceEventPublisher;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; 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.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils;
@ -54,11 +57,13 @@ import org.flowable.engine.history.HistoricProcessInstanceQuery;
import org.flowable.engine.repository.ProcessDefinition; import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.runtime.ProcessInstance; import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.engine.runtime.ProcessInstanceBuilder; import org.flowable.engine.runtime.ProcessInstanceBuilder;
import org.flowable.task.api.Task;
import org.flowable.task.api.history.HistoricTaskInstance; import org.flowable.task.api.history.HistoricTaskInstance;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.client.RestTemplate;
import java.util.*; import java.util.*;
@ -67,6 +72,7 @@ import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.
import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ActivityNode; import static cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance.BpmApprovalDetailRespVO.ActivityNode;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseNodeType;
import static java.util.Arrays.asList; import static java.util.Arrays.asList;
import static java.util.Collections.singletonList; import static java.util.Collections.singletonList;
import static org.flowable.bpmn.constants.BpmnXMLConstants.*; import static org.flowable.bpmn.constants.BpmnXMLConstants.*;
@ -116,6 +122,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
@Resource @Resource
private BpmProcessIdRedisDAO processIdRedisDAO; private BpmProcessIdRedisDAO processIdRedisDAO;
@Resource
private RestTemplate restTemplate;
// ========== Query 查询相关方法 ========== // ========== Query 查询相关方法 ==========
@Override @Override
@ -164,7 +173,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
Long startUserId = loginUserId; // 流程发起人 Long startUserId = loginUserId; // 流程发起人
HistoricProcessInstance historicProcessInstance = null; // 流程实例 HistoricProcessInstance historicProcessInstance = null; // 流程实例
Integer processInstanceStatus = BpmProcessInstanceStatusEnum.NOT_START.getStatus(); // 流程状态 Integer processInstanceStatus = BpmProcessInstanceStatusEnum.NOT_START.getStatus(); // 流程状态
Map<String, Object> processVariables = reqVO.getProcessVariables(); // 流程变量 Map<String, Object> processVariables = new HashMap<>(); // 流程变量
// 1.2 如果是流程已发起的场景则使用流程实例的数据 // 1.2 如果是流程已发起的场景则使用流程实例的数据
if (reqVO.getProcessInstanceId() != null) { if (reqVO.getProcessInstanceId() != null) {
historicProcessInstance = getHistoricProcessInstance(reqVO.getProcessInstanceId()); historicProcessInstance = getHistoricProcessInstance(reqVO.getProcessInstanceId());
@ -173,7 +182,13 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
} }
startUserId = Long.valueOf(historicProcessInstance.getStartUserId()); startUserId = Long.valueOf(historicProcessInstance.getStartUserId());
processInstanceStatus = FlowableUtils.getProcessInstanceStatus(historicProcessInstance); processInstanceStatus = FlowableUtils.getProcessInstanceStatus(historicProcessInstance);
processVariables = historicProcessInstance.getProcessVariables(); // 合并 DB 和前端传递的流量变量以前端的为主
if (CollUtil.isNotEmpty(historicProcessInstance.getProcessVariables())) {
processVariables.putAll(historicProcessInstance.getProcessVariables());
}
}
if (CollUtil.isNotEmpty(reqVO.getProcessVariables())) {
processVariables.putAll(reqVO.getProcessVariables());
} }
// 1.3 读取其它相关数据 // 1.3 读取其它相关数据
ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition( ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition(
@ -205,20 +220,78 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
} }
// 3.1 计算当前登录用户的待办任务 // 3.1 计算当前登录用户的待办任务
// TODO @jason有一个极端情况如果一个用户有 2 task A BA 已经通过B 需要审核这个时通过 A 进来todo 拿到 BpmTaskRespVO todoTask = taskService.getTodoTask(loginUserId, reqVO.getTaskId(), reqVO.getProcessInstanceId());
// B会不会表单权限不一致哈
BpmTaskRespVO todoTask = taskService.getFirstTodoTask(loginUserId, reqVO.getProcessInstanceId());
// 3.2 预测未运行节点的审批信息 // 3.2 预测未运行节点的审批信息
List<ActivityNode> simulateActivityNodes = getSimulateApproveNodeList(startUserId, bpmnModel, List<ActivityNode> simulateActivityNodes = getSimulateApproveNodeList(startUserId, bpmnModel,
processDefinitionInfo, processDefinitionInfo,
processVariables, activities); processVariables, activities);
// 3.3 如果是发起动作activityId 为开始节点不校验审批人自选节点
if (ObjUtil.equals(reqVO.getActivityId(), BpmnModelConstants.START_USER_NODE_ID)) {
simulateActivityNodes.removeIf(node ->
BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy().equals(node.getCandidateStrategy()));
}
// 4. 拼接最终数据 // 4. 拼接最终数据
return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance, return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance,
processInstanceStatus, endActivityNodes, runActivityNodes, simulateActivityNodes, todoTask); processInstanceStatus, endActivityNodes, runActivityNodes, simulateActivityNodes, todoTask);
} }
@Override
public List<ActivityNode> getNextApprovalNodes(Long loginUserId, BpmApprovalDetailReqVO reqVO) {
// 1.1 校验任务存在且是当前用户的
Task task = taskService.validateTask(loginUserId, reqVO.getTaskId());
// 1.2 校验流程实例存在
ProcessInstance instance = getProcessInstance(task.getProcessInstanceId());
if (instance == null) {
throw exception(PROCESS_INSTANCE_NOT_EXISTS);
}
HistoricProcessInstance historicProcessInstance = getHistoricProcessInstance(task.getProcessInstanceId());
if (historicProcessInstance == null) {
throw exception(ErrorCodeConstants.PROCESS_INSTANCE_NOT_EXISTS);
}
// 1.3 校验BpmnModel
BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(task.getProcessDefinitionId());
if (bpmnModel == null) {
return null;
}
// 2. 设置流程变量
Map<String, Object> processVariables = new HashMap<>();
// 2.1 获取历史中流程变量
if (CollUtil.isNotEmpty(historicProcessInstance.getProcessVariables())) {
processVariables.putAll(historicProcessInstance.getProcessVariables());
}
// 2.2 合并前端传递的流程变量以前端为准
if (CollUtil.isNotEmpty(reqVO.getProcessVariables())) {
processVariables.putAll(reqVO.getProcessVariables());
}
// 3 获取当前任务节点的信息
// 3.1 获取下一个将要执行的节点集合
FlowElement flowElement = bpmnModel.getFlowElement(task.getTaskDefinitionKey());
List<FlowNode> nextFlowNodes = BpmnModelUtils.getNextFlowNodes(flowElement, bpmnModel, processVariables);
return convertList(nextFlowNodes, node -> {
List<Long> candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(),
loginUserId, historicProcessInstance.getProcessDefinitionId(), processVariables);
// 3.2 获取节点的审批人信息
Map<Long, AdminUserRespDTO> userMap = adminUserApi.getUserMap(candidateUserIds);
// 3.3 获取节点的审批人部门信息
Map<Long, DeptRespDTO> deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId));
// 3.4 存在一个节点多人审批的情况组装审批人信息
List<UserSimpleBaseVO> candidateUsers = new ArrayList<>();
userMap.forEach((key, value) -> candidateUsers.add(BpmProcessInstanceConvert.INSTANCE.buildUser(key, userMap, deptMap)));
return new ActivityNode().setNodeType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())
.setId(node.getId())
.setName(node.getName())
.setStatus(BpmTaskStatusEnum.RUNNING.getStatus())
.setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(node))
// TODO @小北先把 candidateUserIds 设置完然后最后拼接 candidateUsers 信息这样如果有多个节点就不用重复查询啦类似 buildApprovalDetail 思路
// TODO 先拼接处 List ActivityNode
// TODO 接着再起一段处理 adminUserApi.getUserMap(candidateUserIds)deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId))
.setCandidateUsers(candidateUsers);
});
}
@Override @Override
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public PageResult<HistoricProcessInstance> getProcessInstancePage(Long userId, public PageResult<HistoricProcessInstance> getProcessInstancePage(Long userId,
@ -317,15 +390,17 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
HistoricProcessInstance historicProcessInstance, Integer processInstanceStatus, HistoricProcessInstance historicProcessInstance, Integer processInstanceStatus,
List<HistoricActivityInstance> activities, List<HistoricTaskInstance> tasks) { List<HistoricActivityInstance> activities, List<HistoricTaskInstance> tasks) {
// 遍历 tasks 列表只处理已结束的 UserTask // 遍历 tasks 列表只处理已结束的 UserTask
// 为什么不通过 activities 因为加签场景下它只存在于 tasks没有 activities导致如果遍历 activities // 为什么不通过 activities 因为加签场景下它只存在于 tasks没有 activities导致如果遍历 activities 的话它无法成为一个节点
// 的话它无法成为一个节点 // TODO @芋艿子流程只有activity这里获取不到已结束的子流程
// TODO @lesan子流程基于 activities 查询出 usertaskcallactivity然后拼接如果是子流程就是可以点击过去
List<HistoricTaskInstance> endTasks = filterList(tasks, task -> task.getEndTime() != null); List<HistoricTaskInstance> endTasks = filterList(tasks, task -> task.getEndTime() != null);
List<ActivityNode> approvalNodes = convertList(endTasks, task -> { List<ActivityNode> approvalNodes = convertList(endTasks, task -> {
FlowElement flowNode = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); FlowElement flowNode = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
ActivityNode activityNode = new ActivityNode().setId(task.getTaskDefinitionKey()).setName(task.getName()) ActivityNode activityNode = new ActivityNode().setId(task.getTaskDefinitionKey()).setName(task.getName())
.setNodeType(START_USER_NODE_ID.equals(task.getTaskDefinitionKey()) .setNodeType(START_USER_NODE_ID.equals(task.getTaskDefinitionKey())
? BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType() ? BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType()
: BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()) : ObjUtil.defaultIfNull(parseNodeType(flowNode), // 目的解决办理节点的识别
BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()))
.setStatus(FlowableUtils.getTaskStatus(task)) .setStatus(FlowableUtils.getTaskStatus(task))
.setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode)) .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode))
.setStartTime(DateUtils.of(task.getCreateTime())).setEndTime(DateUtils.of(task.getEndTime())) .setStartTime(DateUtils.of(task.getCreateTime())).setEndTime(DateUtils.of(task.getEndTime()))
@ -386,13 +461,14 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
Map<String, Object> processVariables, Map<String, Object> processVariables,
List<HistoricActivityInstance> activities, List<HistoricActivityInstance> activities,
List<HistoricTaskInstance> tasks) { List<HistoricTaskInstance> tasks) {
// 构建运行中的任务基于 activityId 分组 // 构建运行中的任务子流程基于 activityId 分组
List<HistoricActivityInstance> runActivities = filterList(activities, activity -> activity.getEndTime() == null List<HistoricActivityInstance> runActivities = filterList(activities, activity -> activity.getEndTime() == null
&& (StrUtil.equalsAny(activity.getActivityType(), ELEMENT_TASK_USER))); && (StrUtil.equalsAny(activity.getActivityType(), ELEMENT_TASK_USER, ELEMENT_CALL_ACTIVITY)));
Map<String, List<HistoricActivityInstance>> runningTaskMap = convertMultiMap(runActivities, Map<String, List<HistoricActivityInstance>> runningTaskMap = convertMultiMap(runActivities,
HistoricActivityInstance::getActivityId); HistoricActivityInstance::getActivityId);
// 按照 activityId 分组构建 ApprovalNodeInfo 节点 // 按照 activityId 分组构建 ApprovalNodeInfo 节点
// TODO @lesan子流程在子流程进行审批的时候HistoricActivityInstance 里面可以拿到 runActivities.get(0).getCalledProcessInstanceId()要不要支持跳转
Map<String, HistoricTaskInstance> taskMap = convertMap(tasks, HistoricTaskInstance::getId); Map<String, HistoricTaskInstance> taskMap = convertMap(tasks, HistoricTaskInstance::getId);
return convertList(runningTaskMap.entrySet(), entry -> { return convertList(runningTaskMap.entrySet(), entry -> {
String activityId = entry.getKey(); String activityId = entry.getKey();
@ -402,7 +478,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
HistoricActivityInstance firstActivity = CollUtil.getFirst(taskActivities); // 取第一个任务会签/或签的任务开始时间相同 HistoricActivityInstance firstActivity = CollUtil.getFirst(taskActivities); // 取第一个任务会签/或签的任务开始时间相同
ActivityNode activityNode = new ActivityNode().setId(firstActivity.getActivityId()) ActivityNode activityNode = new ActivityNode().setId(firstActivity.getActivityId())
.setName(firstActivity.getActivityName()) .setName(firstActivity.getActivityName())
.setNodeType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()) .setNodeType(ObjUtil.defaultIfNull(parseNodeType(flowNode), // 目的解决办理节点"子流程"的识别
BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()))
.setStatus(BpmTaskStatusEnum.RUNNING.getStatus()) .setStatus(BpmTaskStatusEnum.RUNNING.getStatus())
.setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode)) .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode))
.setStartTime(DateUtils.of(CollUtil.getFirst(taskActivities).getStartTime())) .setStartTime(DateUtils.of(CollUtil.getFirst(taskActivities).getStartTime()))
@ -410,6 +487,11 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
// 处理每个任务的 tasks 属性 // 处理每个任务的 tasks 属性
for (HistoricActivityInstance activity : taskActivities) { for (HistoricActivityInstance activity : taskActivities) {
HistoricTaskInstance task = taskMap.get(activity.getTaskId()); HistoricTaskInstance task = taskMap.get(activity.getTaskId());
// 特殊情况子流程节点 ChildProcess 仅存在于 activity 并且没有自身的 task需要跳过执行
// TODO @芋艿后续看看怎么优化
if (task == null) {
continue;
}
activityNode.getTasks().add(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task)); activityNode.getTasks().add(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task));
// 加签子任务需要过滤掉已经完成的加签子任务 // 加签子任务需要过滤掉已经完成的加签子任务
List<HistoricTaskInstance> childrenTasks = filterList( List<HistoricTaskInstance> childrenTasks = filterList(
@ -479,7 +561,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
// 1. 开始节点/审批节点 // 1. 开始节点/审批节点
if (ObjectUtils.equalsAny(node.getType(), if (ObjectUtils.equalsAny(node.getType(),
BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType(), BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType(),
BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())) { BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType(),
BpmSimpleModelNodeTypeEnum.TRANSACTOR_NODE.getType())) {
List<Long> candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(), List<Long> candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(),
startUserId, processDefinitionInfo.getProcessDefinitionId(), processVariables); startUserId, processDefinitionInfo.getProcessDefinitionId(), processVariables);
activityNode.setCandidateUserIds(candidateUserIds); activityNode.setCandidateUserIds(candidateUserIds);
@ -494,6 +577,14 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
// 3. 抄送节点 // 3. 抄送节点
if (CollUtil.isEmpty(runActivityIds) && // 流程发起时需要展示抄送节点用于选择抄送人 if (CollUtil.isEmpty(runActivityIds) && // 流程发起时需要展示抄送节点用于选择抄送人
BpmSimpleModelNodeTypeEnum.COPY_NODE.getType().equals(node.getType())) { BpmSimpleModelNodeTypeEnum.COPY_NODE.getType().equals(node.getType())) {
List<Long> candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(),
startUserId, processDefinitionInfo.getProcessDefinitionId(), processVariables);
activityNode.setCandidateUserIds(candidateUserIds);
return activityNode;
}
// 4. 子流程节点
if (BpmSimpleModelNodeTypeEnum.CHILD_PROCESS.getType().equals(node.getType())) {
return activityNode; return activityNode;
} }
return null; return null;
@ -643,7 +734,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
throw exception(PROCESS_INSTANCE_START_USER_CAN_START); throw exception(PROCESS_INSTANCE_START_USER_CAN_START);
} }
// 1.3 校验发起人自选审批人 // 1.3 校验发起人自选审批人
validateStartUserSelectAssignees(definition, startUserSelectAssignees); validateStartUserSelectAssignees(userId, definition, startUserSelectAssignees, variables);
// 2. 创建流程实例 // 2. 创建流程实例
if (variables == null) { if (variables == null) {
@ -653,10 +744,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, userId); // 设置流程变量发起人 ID variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_ID, userId); // 设置流程变量发起人 ID
variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, // 流程实例状态审批中 variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_STATUS, // 流程实例状态审批中
BpmProcessInstanceStatusEnum.RUNNING.getStatus()); BpmProcessInstanceStatusEnum.RUNNING.getStatus());
variables.put(BpmnVariableConstants.PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED, true); // 跳过表达式需要添加此变量为 variables.put(BpmnVariableConstants.PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED, true); // 跳过表达式需要添加此变量为 true不影响没配置 skipExpression 的节点
// true不影响没配置
// skipExpression 的节点
if (CollUtil.isNotEmpty(startUserSelectAssignees)) { if (CollUtil.isNotEmpty(startUserSelectAssignees)) {
// 设置流程变量发起人自选审批人
variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES,
startUserSelectAssignees); startUserSelectAssignees);
} }
@ -688,17 +778,23 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
return instance.getId(); return instance.getId();
} }
private void validateStartUserSelectAssignees(ProcessDefinition definition, private void validateStartUserSelectAssignees(Long userId, ProcessDefinition definition,
Map<String, List<Long>> startUserSelectAssignees) { Map<String, List<Long>> startUserSelectAssignees,
// 1. 获得发起人自选审批人的 UserTask/ServiceTask 列表 Map<String, Object> variables) {
BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(definition.getId()); // 1. 获取预测的节点信息
List<Task> tasks = BpmTaskCandidateStartUserSelectStrategy.getStartUserSelectTaskList(bpmnModel); BpmApprovalDetailRespVO detailRespVO = getApprovalDetail(userId, new BpmApprovalDetailReqVO()
if (CollUtil.isEmpty(tasks)) { .setProcessDefinitionId(definition.getId())
.setProcessVariables(variables));
List<ActivityNode> activityNodes = detailRespVO.getActivityNodes();
if (CollUtil.isEmpty(activityNodes)) {
return; return;
} }
// 2. 校验发起人自选审批人的审批人和抄送人是否都配置了 // 2.1 移除掉不是发起人自选审批人节点
tasks.forEach(task -> { activityNodes.removeIf(task ->
ObjectUtil.notEqual(BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy(), task.getCandidateStrategy()));
// 2.2 流程发起时要先获取当前流程的预测走向节点发起时只校验预测的节点发起人自选审批人的审批人和抄送人是否都配置了
activityNodes.forEach(task -> {
List<Long> assignees = startUserSelectAssignees != null ? startUserSelectAssignees.get(task.getId()) : null; List<Long> assignees = startUserSelectAssignees != null ? startUserSelectAssignees.get(task.getId()) : null;
if (CollUtil.isEmpty(assignees)) { if (CollUtil.isEmpty(assignees)) {
throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, task.getName()); throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, task.getName());
@ -771,6 +867,16 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
BpmReasonEnum.REJECT_TASK.format(reason)); BpmReasonEnum.REJECT_TASK.format(reason));
} }
@Override
public void updateProcessInstanceVariables(String id, Map<String, Object> variables) {
runtimeService.setVariables(id, variables);
}
@Override
public void removeProcessInstanceVariables(String id, Collection<String> variableNames) {
runtimeService.removeVariables(id, variableNames);
}
// ========== Event 事件相关方法 ========== // ========== Event 事件相关方法 ==========
@Override @Override
@ -802,12 +908,47 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService
// 3. 发送流程实例的状态事件 // 3. 发送流程实例的状态事件
processInstanceEventPublisher.sendProcessInstanceResultEvent( processInstanceEventPublisher.sendProcessInstanceResultEvent(
BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceStatusEvent(this, instance, status)); BpmProcessInstanceConvert.INSTANCE.buildProcessInstanceStatusEvent(this, instance, status));
// 4. 流程后置通知
if (Objects.equals(status, BpmProcessInstanceStatusEnum.APPROVE.getStatus())) {
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.
getProcessDefinitionInfo(instance.getProcessDefinitionId());
if (ObjUtil.isNotNull(processDefinitionInfo) &&
ObjUtil.isNotNull(processDefinitionInfo.getPostProcessNotifySetting())) {
BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getPostProcessNotifySetting();
BpmHttpRequestUtils.executeBpmHttpRequest(instance,
setting.getUrl(),
setting.getHeader(),
setting.getBody(),
true, setting.getResponse(),
restTemplate,
this);
}
}
}); });
} }
@Override @Override
public void updateProcessInstanceVariables(String id, Map<String, Object> variables) { public void processProcessInstanceCreated(ProcessInstance instance) {
runtimeService.setVariables(id, variables); // 注意需要基于 instance 设置租户编号避免 Flowable 内部异步时丢失租户编号
FlowableUtils.execute(instance.getTenantId(), () -> {
// 流程前置通知
BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService.
getProcessDefinitionInfo(instance.getProcessDefinitionId());
// TODO @lesanif return 减少括号
if (ObjUtil.isNotNull(processDefinitionInfo) &&
ObjUtil.isNotNull(processDefinitionInfo.getPreProcessNotifySetting())) {
BpmModelMetaInfoVO.HttpRequestSetting setting = processDefinitionInfo.getPreProcessNotifySetting();
BpmHttpRequestUtils.executeBpmHttpRequest(instance,
setting.getUrl(),
setting.getHeader(),
setting.getBody(),
true, setting.getResponse(),
restTemplate,
this);
}
});
} }
} }

View File

@ -35,13 +35,16 @@ public interface BpmTaskService {
PageResult<Task> getTaskTodoPage(Long userId, BpmTaskPageReqVO pageReqVO); PageResult<Task> getTaskTodoPage(Long userId, BpmTaskPageReqVO pageReqVO);
/** /**
* 获得用户在指定流程下首个需要处理待办的任务 * 获得用户待办的任务
* 1. 根据 taskId 查询待办任务
* 2. 如果任务不存在或者已审核获取指定流程下首个需要处理任务
* *
* @param userId 用户编号 * @param userId 用户编号
* @param taskId 任务编号
* @param processInstanceId 流程实例编号 * @param processInstanceId 流程实例编号
* @return 待办任务 * @return 待办任务
*/ */
BpmTaskRespVO getFirstTodoTask(Long userId, String processInstanceId); BpmTaskRespVO getTodoTask(Long userId, String taskId, String processInstanceId);
/** /**
* 获得已办的流程任务分页 * 获得已办的流程任务分页
@ -89,6 +92,14 @@ public interface BpmTaskService {
*/ */
List<HistoricTaskInstance> getTaskListByProcessInstanceId(String processInstanceId, Boolean asc); List<HistoricTaskInstance> getTaskListByProcessInstanceId(String processInstanceId, Boolean asc);
/**
* 校验任务是否存在并且是否是分配给自己的任务
*
* @param userId 用户 id
* @param taskId task id
*/
Task validateTask(Long userId, String taskId);
/** /**
* 获取任务 * 获取任务
* *
@ -277,11 +288,22 @@ public interface BpmTaskService {
void processTaskTimeout(String processInstanceId, String taskDefineKey, Integer handlerType); void processTaskTimeout(String processInstanceId, String taskDefineKey, Integer handlerType);
/** /**
* 处理 延迟器 超时事件 * 处理 ChildProcess 子流程的审批超时事件
* *
* @param processInstanceId 流程示例编号 * @param processInstanceId 流程示例编号
* @param taskDefineKey 任务 Key * @param taskDefineKey 任务 Key
*/ */
void processDelayTimerTimeout(String processInstanceId, String taskDefineKey); void processChildProcessTimeout(String processInstanceId, String taskDefineKey);
/**
* 触发流程任务 (ReceiveTask) 的执行
* <p>
* 1. Simple 模型 HTTP 回调请求触发器节点的回调触发流程继续执行
* 2. Simple 模型延迟器节点到时触发流程继续执行
*
* @param processInstanceId 流程示例编号
* @param taskDefineKey 任务 Key
*/
void triggerTask(String processInstanceId, String taskDefineKey);
} }

View File

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.bpm.service.task; package cn.iocoder.yudao.module.bpm.service.task;
import cn.hutool.core.collection.CollUtil; import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.Assert;
import cn.hutool.core.util.*; import cn.hutool.core.util.*;
import cn.hutool.extra.spring.SpringUtil; import cn.hutool.extra.spring.SpringUtil;
@ -19,6 +20,7 @@ import cn.iocoder.yudao.module.bpm.enums.task.BpmCommentTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmReasonEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskSignTypeEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskSignTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; 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.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
@ -40,6 +42,7 @@ import org.flowable.engine.ManagementService;
import org.flowable.engine.RuntimeService; import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService; import org.flowable.engine.TaskService;
import org.flowable.engine.history.HistoricActivityInstance; import org.flowable.engine.history.HistoricActivityInstance;
import org.flowable.engine.runtime.ActivityInstance;
import org.flowable.engine.runtime.Execution; import org.flowable.engine.runtime.Execution;
import org.flowable.engine.runtime.ProcessInstance; import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.DelegationState; import org.flowable.task.api.DelegationState;
@ -61,7 +64,9 @@ import java.util.stream.Stream;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; 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.framework.common.util.collection.CollectionUtils.*;
import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnModelConstants.START_USER_NODE_ID;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_RETURN_FLAG;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.*;
/** /**
@ -116,6 +121,9 @@ public class BpmTaskServiceImpl implements BpmTaskService {
if (StrUtil.isNotEmpty(pageVO.getCategory())) { if (StrUtil.isNotEmpty(pageVO.getCategory())) {
taskQuery.taskCategory(pageVO.getCategory()); taskQuery.taskCategory(pageVO.getCategory());
} }
if (StrUtil.isNotEmpty(pageVO.getProcessDefinitionKey())) {
taskQuery.processDefinitionKey(pageVO.getProcessDefinitionKey());
}
if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) {
taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0]));
taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1]));
@ -129,7 +137,67 @@ public class BpmTaskServiceImpl implements BpmTaskService {
} }
@Override @Override
public BpmTaskRespVO getFirstTodoTask(Long userId, String processInstanceId) { public BpmTaskRespVO getTodoTask(Long userId, String taskId, String processInstanceId) {
// 1.1 获取指定的用户待办任务
Task todoTask = getMyTodoTask(userId, taskId);
// 1.2 获取不到则获取该流程实例下第一个用户的待办任务
if (todoTask == null) {
todoTask = getMyFirstTodoTask(userId, processInstanceId);
}
if (todoTask == null) {
return null;
}
// 2. 查询该任务的子任务
List<Task> childrenTasks = getAllChildrenTaskListByParentTaskId(todoTask.getId(), CollUtil.newArrayList(todoTask));
// 3. 转换返回
BpmnModel bpmnModel = bpmProcessDefinitionService.getProcessDefinitionBpmnModel(todoTask.getProcessDefinitionId());
Map<Integer, BpmTaskRespVO.OperationButtonSetting> buttonsSetting = BpmnModelUtils.parseButtonsSetting(
bpmnModel, todoTask.getTaskDefinitionKey());
Boolean signEnable = parseSignEnable(bpmnModel, todoTask.getTaskDefinitionKey());
Boolean reasonRequire = parseReasonRequire(bpmnModel, todoTask.getTaskDefinitionKey());
Integer nodeType = parseNodeType(BpmnModelUtils.getFlowElementById(bpmnModel, todoTask.getTaskDefinitionKey()));
// 4. 任务表单
BpmFormDO taskForm = null;
if (StrUtil.isNotBlank(todoTask.getFormKey())) {
taskForm = formService.getForm(NumberUtils.parseLong(todoTask.getFormKey()));
}
return BpmTaskConvert.INSTANCE.buildTodoTask(todoTask, childrenTasks, buttonsSetting, taskForm)
.setNodeType(nodeType).setSignEnable(signEnable).setReasonRequire(reasonRequire);
}
/**
* 获得用户指定 taskId 任务编号的待办未审批且可审核的任务
*
* @param userId 用户编号
* @param taskId 任务编号
* @return 任务
*/
private Task getMyTodoTask(Long userId, String taskId) {
if (StrUtil.isEmpty(taskId)) {
return null;
}
Task task = getTask(taskId);
if (task == null) {
return null;
}
if (!isAssignUserTask(userId, task) && !isAddSignUserTask(userId, task)) {
return null;
}
return task;
}
/**
* 获得用户指定 processInstanceId 流程编号下的首个待办未审批且可审核的任务
*
* @param userId 用户编号
* @param processInstanceId 流程编号
* @return 任务
*/
private Task getMyFirstTodoTask(Long userId, String processInstanceId) {
if (processInstanceId == null) { if (processInstanceId == null) {
return null; return null;
} }
@ -141,37 +209,12 @@ public class BpmTaskServiceImpl implements BpmTaskService {
.includeProcessVariables() .includeProcessVariables()
.orderByTaskCreateTime().asc() // 按创建时间升序 .orderByTaskCreateTime().asc() // 按创建时间升序
.list(); .list();
if (CollUtil.isEmpty(tasks)) {
return null;
}
// 2.1 查询我的首个任务 // 2. 查询我的首个任务
Task todoTask = CollUtil.findOne(tasks, task -> { return CollUtil.findOne(tasks, task -> {
return isAssignUserTask(userId, task) // 当前用户为审批人 return isAssignUserTask(userId, task) // 当前用户为审批人
|| isAddSignUserTask(userId, task); // 当前用户为加签人为了减签 || isAddSignUserTask(userId, task); // 当前用户为加签人为了减签
}); });
if (todoTask == null) {
return null;
}
// 2.2 查询该任务的子任务
List<Task> childrenTasks = getAllChildrenTaskListByParentTaskId(todoTask.getId(), tasks);
// 3. 转换返回
BpmnModel bpmnModel = bpmProcessDefinitionService.getProcessDefinitionBpmnModel(todoTask.getProcessDefinitionId());
Map<Integer, BpmTaskRespVO.OperationButtonSetting> buttonsSetting = BpmnModelUtils.parseButtonsSetting(
bpmnModel, todoTask.getTaskDefinitionKey());
Boolean signEnable = parseSignEnable(bpmnModel, todoTask.getTaskDefinitionKey());
Boolean reasonRequire = parseReasonRequire(bpmnModel, todoTask.getTaskDefinitionKey());
// 4. 任务表单
BpmFormDO taskForm = null;
if (StrUtil.isNotBlank(todoTask.getFormKey())) {
taskForm = formService.getForm(NumberUtils.parseLong(todoTask.getFormKey()));
}
return BpmTaskConvert.INSTANCE.buildTodoTask(todoTask, childrenTasks, buttonsSetting, taskForm)
.setSignEnable(signEnable)
.setReasonRequire(reasonRequire);
} }
@Override @Override
@ -194,6 +237,10 @@ public class BpmTaskServiceImpl implements BpmTaskService {
return PageResult.empty(); return PageResult.empty();
} }
List<HistoricTaskInstance> tasks = taskQuery.listPage(PageUtils.getStart(pageVO), pageVO.getPageSize()); List<HistoricTaskInstance> tasks = taskQuery.listPage(PageUtils.getStart(pageVO), pageVO.getPageSize());
// 特殊强制移除自动完成的发起人节点
// 补充说明由于 taskQuery 无法方面的过滤所以暂时通过内存过滤
tasks.removeIf(task -> task.getTaskDefinitionKey().equals(START_USER_NODE_ID));
return new PageResult<>(tasks, count); return new PageResult<>(tasks, count);
} }
@ -243,13 +290,8 @@ public class BpmTaskServiceImpl implements BpmTaskService {
return query.list(); return query.list();
} }
/** @Override
* 校验任务是否存在并且是否是分配给自己的任务 public Task validateTask(Long userId, String taskId) {
*
* @param userId 用户 id
* @param taskId task id
*/
private Task validateTask(Long userId, String taskId) {
Task task = validateTaskExist(taskId); Task task = validateTaskExist(taskId);
// 为什么判断 assignee 非空的情况下 // 为什么判断 assignee 非空的情况下
// 例如说在审批人为空时我们会有自动审批通过的策略此时 userId null允许通过 // 例如说在审批人为空时我们会有自动审批通过的策略此时 userId null允许通过
@ -515,21 +557,87 @@ public class BpmTaskServiceImpl implements BpmTaskService {
// 2.2 添加评论 // 2.2 添加评论
taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.APPROVE.getType(), taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.APPROVE.getType(),
BpmCommentTypeEnum.APPROVE.formatComment(reqVO.getReason())); BpmCommentTypeEnum.APPROVE.formatComment(reqVO.getReason()));
// 2.3 调用 BPM complete 去完成任务 // 2.3 校验并处理 APPROVE_USER_SELECT 当前审批人选择下一节点审批人的逻辑
// 其中variables 是存储动态表单到 local 任务级别过滤一下避免 ProcessInstance 系统级的变量被占用 Map<String, Object> variables = validateAndSetNextAssignees(task.getTaskDefinitionKey(), reqVO.getVariables(),
if (CollUtil.isNotEmpty(reqVO.getVariables())) { bpmnModel, reqVO.getNextAssignees(), instance);
Map<String, Object> variables = FlowableUtils.filterTaskFormVariable(reqVO.getVariables());
// 修改表单的值需要存储到 ProcessInstance 变量
runtimeService.setVariables(task.getProcessInstanceId(), variables); runtimeService.setVariables(task.getProcessInstanceId(), variables);
// 2.4 调用 BPM complete 去完成任务
taskService.complete(task.getId(), variables, true); taskService.complete(task.getId(), variables, true);
} else {
taskService.complete(task.getId());
}
// 加签专属处理加签任务 // 加签专属处理加签任务
handleParentTaskIfSign(task.getParentTaskId()); handleParentTaskIfSign(task.getParentTaskId());
} }
/**
* 校验选择的下一个节点的审批人是否合法
*
* 1. 是否有漏选没有选择审批人
* 2. 是否有多选非下一个节点
*
* @param taskDefinitionKey 当前任务节点标识
* @param variables 流程变量
* @param bpmnModel 流程模型
* @param nextAssignees 下一个节点审批人集合参数
* @param processInstance 流程实例
*/
private Map<String, Object> validateAndSetNextAssignees(String taskDefinitionKey, Map<String, Object> variables, BpmnModel bpmnModel,
Map<String, List<Long>> nextAssignees, ProcessInstance processInstance) {
// 1. 获取下一个将要执行的节点集合
FlowElement flowElement = bpmnModel.getFlowElement(taskDefinitionKey);
List<FlowNode> nextFlowNodes = getNextFlowNodes(flowElement, bpmnModel, variables);
// 2. 校验选择的下一个节点的审批人是否合法
Map<String, List<Long>> processVariables;
for (FlowNode nextFlowNode : nextFlowNodes) {
Integer candidateStrategy = parseCandidateStrategy(nextFlowNode);
// 2.1 情况一如果节点中的审批人策略为 发起人自选
if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy())) {
// 如果节点存在但未配置审批人
List<Long> assignees = nextAssignees != null ? nextAssignees.get(nextFlowNode.getId()) : null;
if (CollUtil.isEmpty(assignees)) {
throw exception(PROCESS_INSTANCE_START_USER_SELECT_ASSIGNEES_NOT_CONFIG, nextFlowNode.getName());
}
processVariables = FlowableUtils.getStartUserSelectAssignees(processInstance.getProcessVariables());
// 特殊如果当前节点已经存在审批人则不允许覆盖
// TODO @小北不用改通过 if return让逻辑更简洁一点虽然会多判断一次 processVariables但是 if else 层级更少
if (processVariables != null
&& CollUtil.isNotEmpty(processVariables.get(nextFlowNode.getId()))) {
continue;
}
// 设置 PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES
if (processVariables == null) {
processVariables = new HashMap<>();
}
processVariables.put(nextFlowNode.getId(), assignees);
variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, processVariables);
}
// 2.2 情况二如果节点中的审批人策略为 审批人在审批时选择下一个节点的审批人并且该节点的审批人为空
if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.APPROVE_USER_SELECT.getStrategy())) {
// 如果节点存在但未配置审批人
List<Long> assignees = nextAssignees != null ? nextAssignees.get(nextFlowNode.getId()) : null;
if (CollUtil.isEmpty(assignees)) {
throw exception(PROCESS_INSTANCE_APPROVE_USER_SELECT_ASSIGNEES_NOT_CONFIG, nextFlowNode.getName());
}
processVariables = FlowableUtils.getApproveUserSelectAssignees(processInstance.getProcessVariables());
if (processVariables == null) {
processVariables = new HashMap<>();
} else {
List<Long> approveUserSelectAssignee = processVariables.get(nextFlowNode.getId());
// 特殊如果当前节点已经存在审批人则不允许覆盖
// TODO @小北这种应该可以覆盖呢
if (CollUtil.isNotEmpty(approveUserSelectAssignee)) {
continue;
}
}
// 设置 PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES
processVariables.put(nextFlowNode.getId(), assignees);
variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES, processVariables);
}
}
return variables;
}
/** /**
* 审批通过存在后加签的任务 * 审批通过存在后加签的任务
* <p> * <p>
@ -991,6 +1099,7 @@ public class BpmTaskServiceImpl implements BpmTaskService {
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
@SuppressWarnings("DataFlowIssue")
public void deleteSignTask(Long userId, BpmTaskSignDeleteReqVO reqVO) { public void deleteSignTask(Long userId, BpmTaskSignDeleteReqVO reqVO) {
// 1.1 校验 task 可以被减签 // 1.1 校验 task 可以被减签
Task task = validateTaskCanSignDelete(reqVO.getId()); Task task = validateTaskCanSignDelete(reqVO.getId());
@ -1207,19 +1316,31 @@ public class BpmTaskServiceImpl implements BpmTaskService {
} }
} }
// 审批人与提交人为同一人时根据 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()); BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId());
if (bpmnModel == null) { if (bpmnModel == null) {
log.error("[processTaskAssigned][taskId({}) 没有找到流程模型]", task.getId()); log.error("[processTaskAssigned][taskId({}) 没有找到流程模型]", task.getId());
return; return;
} }
FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey());
// 判断是否为退回或者驳回如果是退回或者驳回不走这个策略
// TODO 芋艿优化未来有没更好的判断方式另外还要考虑清理机制就是说下次处理了之后就移除这个标识
Boolean returnTaskFlag = runtimeService.getVariable(processInstance.getProcessInstanceId(),
String.format(PROCESS_INSTANCE_VARIABLE_RETURN_FLAG, task.getTaskDefinitionKey()), Boolean.class);
Boolean skipStartUserNodeFlag = Convert.toBool(runtimeService.getVariable(processInstance.getProcessInstanceId(),
PROCESS_INSTANCE_VARIABLE_SKIP_START_USER_NODE, String.class));
if (userTaskElement.getId().equals(START_USER_NODE_ID)
&& (skipStartUserNodeFlag == null // 目的一般是主流程发起人节点自动通过审核
|| Boolean.TRUE.equals(skipStartUserNodeFlag)) // 目的一般是子流程发起人节点按配置自动通过审核
&& ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) {
getSelf().approveTask(Long.valueOf(task.getAssignee()), new BpmTaskApproveReqVO().setId(task.getId())
.setReason(BpmReasonEnum.ASSIGN_START_USER_APPROVE_WHEN_SKIP_START_USER_NODE.getReason()));
return;
}
// 当不为发起人节点时审批人与提交人为同一人时根据 BpmUserTaskAssignStartUserHandlerTypeEnum 策略进行处理
if (ObjectUtil.notEqual(userTaskElement.getId(), START_USER_NODE_ID)
&& StrUtil.equals(task.getAssignee(), processInstance.getStartUserId())) {
if (ObjUtil.notEqual(returnTaskFlag, Boolean.TRUE)) {
Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(userTaskElement); Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(userTaskElement);
// 情况一自动跳过 // 情况一自动跳过
@ -1304,14 +1425,23 @@ public class BpmTaskServiceImpl implements BpmTaskService {
} }
@Override @Override
public void processDelayTimerTimeout(String processInstanceId, String taskDefineKey) { @Transactional(rollbackFor = Exception.class)
public void processChildProcessTimeout(String processInstanceId, String taskDefineKey) {
List<ActivityInstance> activityInstances = runtimeService.createActivityInstanceQuery()
.processInstanceId(processInstanceId)
.activityId(taskDefineKey).list();
activityInstances.forEach(activityInstance -> FlowableUtils.execute(activityInstance.getTenantId(),
() -> moveTaskToEnd(activityInstance.getCalledProcessInstanceId(), BpmReasonEnum.TIMEOUT_APPROVE.getReason())));
}
@Override
public void triggerTask(String processInstanceId, String taskDefineKey) {
Execution execution = runtimeService.createExecutionQuery() Execution execution = runtimeService.createExecutionQuery()
.processInstanceId(processInstanceId) .processInstanceId(processInstanceId)
.activityId(taskDefineKey) .activityId(taskDefineKey)
.singleResult(); .singleResult();
if (execution == null) { if (execution == null) {
log.error("[processDelayTimerTimeout][processInstanceId({}) activityId({}) 没有找到执行活动]", log.error("[triggerTask][processInstanceId({}) activityId({}) 没有找到执行活动]", processInstanceId, taskDefineKey);
processInstanceId, taskDefineKey);
return; return;
} }

View File

@ -0,0 +1,96 @@
package cn.iocoder.yudao.module.bpm.service.task.listener;
import cn.hutool.core.lang.Assert;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
import cn.iocoder.yudao.module.bpm.dal.dataobject.definition.BpmProcessDefinitionInfoDO;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessStartUserEmptyTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmChildProcessStartUserTypeEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils;
import cn.iocoder.yudao.module.bpm.service.definition.BpmProcessDefinitionService;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import jakarta.annotation.Resource;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.ExecutionListener;
import org.flowable.engine.impl.el.FixedValue;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* BPM 子流程监听器设置流程的发起人
*
* @author Lesan
*/
@Component
@Slf4j
public class BpmCallActivityListener implements ExecutionListener {
public static final String DELEGATE_EXPRESSION = "${bpmCallActivityListener}";
@Setter
private FixedValue listenerConfig;
@Resource
private BpmProcessDefinitionService processDefinitionService;
@Resource
private BpmProcessInstanceService processInstanceService;
@Override
public void notify(DelegateExecution execution) {
String expressionText = listenerConfig.getExpressionText();
Assert.notNull(expressionText, "监听器扩展字段({})不能为空", expressionText);
BpmSimpleModelNodeVO.ChildProcessSetting.StartUserSetting startUserSetting = JsonUtils.parseObject(
expressionText, BpmSimpleModelNodeVO.ChildProcessSetting.StartUserSetting.class);
ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getRootProcessInstanceId());
// 1. 当发起人来源为主流程发起人时并兜底 startUserSetting 为空时
if (startUserSetting == null
|| startUserSetting.getType().equals(BpmChildProcessStartUserTypeEnum.MAIN_PROCESS_START_USER.getType())) {
FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId()));
return;
}
// 2. 当发起人来源为表单时
if (startUserSetting.getType().equals(BpmChildProcessStartUserTypeEnum.FROM_FORM.getType())) {
String formFieldValue = MapUtil.getStr(processInstance.getProcessVariables(), startUserSetting.getFormField());
// 2.1 当表单值为空时
if (StrUtil.isEmpty(formFieldValue)) {
// 2.1.1 来自主流程发起人
if (startUserSetting.getEmptyType().equals(BpmChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_START_USER.getType())) {
FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId()));
return;
}
// 2.1.2 来自子流程管理员
if (startUserSetting.getEmptyType().equals(BpmChildProcessStartUserEmptyTypeEnum.CHILD_PROCESS_ADMIN.getType())) {
BpmProcessDefinitionInfoDO processDefinition = processDefinitionService.getProcessDefinitionInfo(execution.getProcessDefinitionId());
List<Long> managerUserIds = processDefinition.getManagerUserIds();
FlowableUtils.setAuthenticatedUserId(managerUserIds.get(0));
return;
}
// 2.1.3 来自主流程管理员
if (startUserSetting.getEmptyType().equals(BpmChildProcessStartUserEmptyTypeEnum.MAIN_PROCESS_ADMIN.getType())) {
BpmProcessDefinitionInfoDO processDefinition = processDefinitionService.getProcessDefinitionInfo(processInstance.getProcessDefinitionId());
List<Long> managerUserIds = processDefinition.getManagerUserIds();
FlowableUtils.setAuthenticatedUserId(managerUserIds.get(0));
return;
}
}
// 2.2 使用表单值并兜底字符串转 Long 失败时使用主流程发起人
try {
FlowableUtils.setAuthenticatedUserId(Long.parseLong(formFieldValue));
} catch (Exception e) {
log.error("[notify][监听器:{},子流程监听器设置流程的发起人字符串转 Long 失败,字符串:{}]",
DELEGATE_EXPRESSION, formFieldValue);
FlowableUtils.setAuthenticatedUserId(Long.parseLong(processInstance.getStartUserId()));
}
}
}
}

View File

@ -1,29 +1,20 @@
package cn.iocoder.yudao.module.bpm.service.task.listener; package cn.iocoder.yudao.module.bpm.service.task.listener;
import cn.hutool.core.util.StrUtil;
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;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils; import cn.iocoder.yudao.module.bpm.enums.definition.BpmHttpRequestParamTypeEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.Setter; import lombok.Setter;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.delegate.TaskListener; import org.flowable.engine.delegate.TaskListener;
import org.flowable.engine.history.HistoricProcessInstance;
import org.flowable.engine.impl.el.FixedValue; import org.flowable.engine.impl.el.FixedValue;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.service.delegate.DelegateTask; import org.flowable.task.service.delegate.DelegateTask;
import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.Scope;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import java.util.Map;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseListenerConfig; import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseListenerConfig;
// TODO @芋艿可能会想换个包地址 // TODO @芋艿可能会想换个包地址
@ -51,46 +42,27 @@ public class BpmUserTaskListener implements TaskListener {
@Override @Override
public void notify(DelegateTask delegateTask) { public void notify(DelegateTask delegateTask) {
// 1. 获取所需基础信息 // 1. 获取所需基础信息
HistoricProcessInstance processInstance = processInstanceService.getHistoricProcessInstance(delegateTask.getProcessInstanceId()); ProcessInstance processInstance = processInstanceService.getProcessInstance(delegateTask.getProcessInstanceId());
BpmSimpleModelNodeVO.ListenerHandler listenerHandler = parseListenerConfig(listenerConfig); BpmSimpleModelNodeVO.ListenerHandler listenerHandler = parseListenerConfig(listenerConfig);
// 2. 获取请求头和请求体 // 2. 发起请求
Map<String, Object> processVariables = processInstance.getProcessVariables();
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
SimpleModelUtils.addHttpRequestParam(headers, listenerHandler.getHeader(), processVariables);
SimpleModelUtils.addHttpRequestParam(body, listenerHandler.getBody(), processVariables);
// 2.1 请求头默认参数
if (StrUtil.isNotEmpty(delegateTask.getTenantId())) {
headers.add(HEADER_TENANT_ID, delegateTask.getTenantId());
}
// 2.2 请求体默认参数
// TODO @芋艿哪些默认参数后续再调研下感觉可以搞个 task 字段把整个 delegateTask 放进去 // TODO @芋艿哪些默认参数后续再调研下感觉可以搞个 task 字段把整个 delegateTask 放进去
body.add("processInstanceId", delegateTask.getProcessInstanceId()); listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("processInstanceId")
body.add("assignee", delegateTask.getAssignee()); .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getProcessInstanceId()));
body.add("taskDefinitionKey", delegateTask.getTaskDefinitionKey()); listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("assignee")
body.add("taskId", delegateTask.getId()); .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getAssignee()));
listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("taskDefinitionKey")
.setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getTaskDefinitionKey()));
listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("taskId")
.setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getId()));
BpmHttpRequestUtils.executeBpmHttpRequest(processInstance,
listenerHandler.getPath(),
listenerHandler.getHeader(),
listenerHandler.getBody(),
false, null,
restTemplate,
processInstanceService);
// 3. 异步发起请求 // 3. 是否需要后续操作TODO 芋艿待定
// TODO @芋艿确认要同步还是异步
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, headers);
try {
ResponseEntity<String> responseEntity = restTemplate.exchange(listenerHandler.getPath(), HttpMethod.POST,
requestEntity, String.class);
log.info("[notify][监听器:{},事件类型:{},请求头:{},请求体:{},响应结果:{}]",
DELEGATE_EXPRESSION,
delegateTask.getEventName(),
headers,
body,
responseEntity);
} catch (RestClientException e) {
log.error("[error][监听器:{},事件类型:{},请求头:{},请求体:{},请求出错:{}]",
DELEGATE_EXPRESSION,
delegateTask.getEventName(),
headers,
body,
e.getMessage());
}
// 4. 是否需要后续操作TODO 芋艿待定
} }
} }

View File

@ -1,124 +0,0 @@
package cn.iocoder.yudao.module.bpm.service.task.trigger;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.core.KeyValue;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import com.fasterxml.jackson.core.type.TypeReference;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils.HEADER_TENANT_ID;
/**
* BPM 发送 HTTP 请求触发器
*
* @author jason
*/
@Component
@Slf4j
public class BpmHttpRequestTrigger implements BpmTrigger {
@Resource
private BpmProcessInstanceService processInstanceService;
@Resource
private RestTemplate restTemplate;
@Override
public BpmTriggerTypeEnum getType() {
return BpmTriggerTypeEnum.HTTP_REQUEST;
}
@Override
public void execute(String processInstanceId, String param) {
// 1. 解析 http 请求配置
HttpRequestTriggerSetting setting = JsonUtils.parseObject(param, HttpRequestTriggerSetting.class);
if (setting == null) {
log.error("[execute][流程({}) HTTP 触发器请求配置为空]", processInstanceId);
return;
}
// 2.1 设置请求头
ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId);
Map<String, Object> processVariables = processInstance.getProcessVariables();
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add(HEADER_TENANT_ID, processInstance.getTenantId());
SimpleModelUtils.addHttpRequestParam(headers, setting.getHeader(), processVariables);
// 2.2 设置请求体
MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
SimpleModelUtils.addHttpRequestParam(body, setting.getBody(), processVariables);
body.add("processInstanceId", processInstanceId);
// TODO @芋艿要不要抽象一个 Http 请求的工具类方便复用呢
// 3. 发起请求
HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(body, headers);
ResponseEntity<String> responseEntity;
try {
responseEntity = restTemplate.exchange(setting.getUrl(), HttpMethod.POST,
requestEntity, String.class);
log.info("[execute][HTTP 触发器,请求头:{},请求体:{},响应结果:{}]", headers, body, responseEntity);
} catch (RestClientException e) {
log.error("[execute][HTTP 触发器,请求头:{},请求体:{},请求出错:{}]", headers, body, e.getMessage());
return;
}
// 4.1 判断是否需要解析返回值
if (StrUtil.isEmpty(responseEntity.getBody())
|| !responseEntity.getStatusCode().is2xxSuccessful()
|| CollUtil.isEmpty(setting.getResponse())) {
return;
}
// 4.2 解析返回值, 返回值必须符合 CommonResult 规范
CommonResult<Map<String, Object>> respResult = JsonUtils.parseObjectQuietly(
responseEntity.getBody(), new TypeReference<>() {});
if (respResult == null || !respResult.isSuccess()){
return;
}
// 4.3 获取需要更新的流程变量
Map<String, Object> updateVariables = getNeedUpdatedVariablesFromResponse(respResult.getData(), setting.getResponse());
// 4.4 更新流程变量
if (CollUtil.isNotEmpty(updateVariables)) {
processInstanceService.updateProcessInstanceVariables(processInstanceId, updateVariables);
}
}
/**
* 从请求返回值获取需要更新的流程变量
*
* @param result 请求返回结果
* @param responseSettings 返回设置
* @return 需要更新的流程变量
*/
private Map<String, Object> getNeedUpdatedVariablesFromResponse(Map<String,Object> result,
List<KeyValue<String, String>> responseSettings) {
Map<String, Object> updateVariables = new HashMap<>();
if (CollUtil.isEmpty(result)) {
return updateVariables;
}
responseSettings.forEach(responseSetting -> {
if (StrUtil.isNotEmpty(responseSetting.getKey()) && result.containsKey(responseSetting.getValue())) {
updateVariables.put(responseSetting.getKey(), result.get(responseSetting.getValue()));
}
});
return updateVariables;
}
}

View File

@ -1,44 +0,0 @@
package cn.iocoder.yudao.module.bpm.service.task.trigger;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.TriggerSetting.NormalFormTriggerSetting;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
// TODO @jason改成 BpmFormUpdateTrigger
/**
* BPM 更新流程表单触发器
*
* @author jason
*/
@Component
@Slf4j
public class BpmUpdateNormalFormTrigger implements BpmTrigger {
@Resource
private BpmProcessInstanceService processInstanceService;
@Override
public BpmTriggerTypeEnum getType() {
return BpmTriggerTypeEnum.UPDATE_NORMAL_FORM;
}
@Override
public void execute(String processInstanceId, String param) {
// 1. 解析更新流程表单配置
NormalFormTriggerSetting setting = JsonUtils.parseObject(param, NormalFormTriggerSetting.class);
if (setting == null) {
log.error("[execute][流程({}) 更新流程表单触发器配置为空]", processInstanceId);
return;
}
// 2.更新流程变量
if (CollUtil.isNotEmpty(setting.getUpdateFormFields())) {
processInstanceService.updateProcessInstanceVariables(processInstanceId, setting.getUpdateFormFields());
}
}
}

View File

@ -0,0 +1,73 @@
package cn.iocoder.yudao.module.bpm.service.task.trigger.form;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import cn.iocoder.yudao.module.bpm.service.task.trigger.BpmTrigger;
import com.fasterxml.jackson.core.type.TypeReference;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* BPM 删除流程表单数据触发器
*
* @author jason
*/
@Component
@Slf4j
public class BpmFormDeleteTrigger implements BpmTrigger {
@Resource
private BpmProcessInstanceService processInstanceService;
@Override
public BpmTriggerTypeEnum getType() {
return BpmTriggerTypeEnum.FORM_DELETE;
}
@Override
public void execute(String processInstanceId, String param) {
// 1. 解析删除流程表单数据配置
List<BpmSimpleModelNodeVO.TriggerSetting.FormTriggerSetting> settings = JsonUtils.parseObject(param, new TypeReference<>() {});
if (CollUtil.isEmpty(settings)) {
log.error("[execute][流程({}) 删除流程表单数据触发器配置为空]", processInstanceId);
return;
}
// 2. 获取流程变量
Map<String, Object> processVariables = processInstanceService.getProcessInstance(processInstanceId).getProcessVariables();
// 3.1 获取需要删除的表单字段
Set<String> deleteFields = new HashSet<>();
settings.forEach(setting -> {
if (CollUtil.isEmpty(setting.getDeleteFields())) {
return;
}
// 配置了条件判断条件是否满足
boolean isFieldDeletedNeeded = true;
if (setting.getConditionType() != null) {
String conditionExpression = SimpleModelUtils.buildConditionExpression(
setting.getConditionType(), setting.getConditionExpression(), setting.getConditionGroups());
isFieldDeletedNeeded = BpmnModelUtils.evalConditionExpress(processVariables, conditionExpression);
}
if (isFieldDeletedNeeded) {
deleteFields.addAll(setting.getDeleteFields());
}
});
// 3.2 删除流程变量
if (CollUtil.isNotEmpty(deleteFields)) {
processInstanceService.removeProcessInstanceVariables(processInstanceId, deleteFields);
}
}
}

View File

@ -0,0 +1,66 @@
package cn.iocoder.yudao.module.bpm.service.task.trigger.form;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.TriggerSetting.FormTriggerSetting;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.SimpleModelUtils;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import cn.iocoder.yudao.module.bpm.service.task.trigger.BpmTrigger;
import com.fasterxml.jackson.core.type.TypeReference;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
/**
* BPM 更新流程表单触发器
*
* @author jason
*/
@Component
@Slf4j
public class BpmFormUpdateTrigger implements BpmTrigger {
@Resource
private BpmProcessInstanceService processInstanceService;
@Override
public BpmTriggerTypeEnum getType() {
return BpmTriggerTypeEnum.FORM_UPDATE;
}
@Override
public void execute(String processInstanceId, String param) {
// 1. 解析更新流程表单配置
List<FormTriggerSetting> settings = JsonUtils.parseObject(param, new TypeReference<>() {});
if (CollUtil.isEmpty(settings)) {
log.error("[execute][流程({}) 更新流程表单触发器配置为空]", processInstanceId);
return;
}
// 2. 获取流程变量
Map<String, Object> processVariables = processInstanceService.getProcessInstance(processInstanceId).getProcessVariables();
// 3. 更新流程变量
for (FormTriggerSetting setting : settings) {
if (CollUtil.isEmpty(setting.getUpdateFormFields())) {
continue;
}
// 配置了条件判断条件是否满足
boolean isFormUpdateNeeded = true;
if (setting.getConditionType() != null) {
String conditionExpression = SimpleModelUtils.buildConditionExpression(
setting.getConditionType(), setting.getConditionExpression(), setting.getConditionGroups());
isFormUpdateNeeded = BpmnModelUtils.evalConditionExpress(processVariables, conditionExpression);
}
// 更新流程表单
if (isFormUpdateNeeded) {
processInstanceService.updateProcessInstanceVariables(processInstanceId, setting.getUpdateFormFields());
}
}
}
}

View File

@ -0,0 +1,14 @@
package cn.iocoder.yudao.module.bpm.service.task.trigger.http;
import cn.iocoder.yudao.module.bpm.service.task.trigger.BpmTrigger;
import lombok.extern.slf4j.Slf4j;
/**
* BPM 发送 HTTP 请求触发器抽象类
*
* @author jason
*/
@Slf4j
public abstract class BpmAbstractHttpRequestTrigger implements BpmTrigger {
}

View File

@ -0,0 +1,59 @@
package cn.iocoder.yudao.module.bpm.service.task.trigger.http;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmHttpRequestParamTypeEnum;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
/**
* BPM HTTP 回调触发器
*
* @author jason
*/
@Component
@Slf4j
public class BpmHttpCallbackTrigger extends BpmAbstractHttpRequestTrigger {
@Resource
private RestTemplate restTemplate;
@Resource
private BpmProcessInstanceService processInstanceService;
@Override
public BpmTriggerTypeEnum getType() {
return BpmTriggerTypeEnum.HTTP_CALLBACK;
}
@Override
public void execute(String processInstanceId, String param) {
// 1. 解析 http 请求配置
BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting setting = JsonUtils.parseObject(param,
BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting.class);
if (setting == null) {
log.error("[execute][流程({}) HTTP 回调触发器配置为空]", processInstanceId);
return;
}
// 2. 发起请求
ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId);
setting.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam()
.setKey("taskDefineKey") // 重要回调请求 taskDefineKey 需要传给被调用方用于回调执行
.setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(setting.getCallbackTaskDefineKey()));
BpmHttpRequestUtils.executeBpmHttpRequest(processInstance,
setting.getUrl(),
setting.getHeader(),
setting.getBody(),
false, null,
restTemplate,
processInstanceService);
}
}

View File

@ -0,0 +1,54 @@
package cn.iocoder.yudao.module.bpm.service.task.trigger.http;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.simple.BpmSimpleModelNodeVO.TriggerSetting.HttpRequestTriggerSetting;
import cn.iocoder.yudao.module.bpm.enums.definition.BpmTriggerTypeEnum;
import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmHttpRequestUtils;
import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.runtime.ProcessInstance;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
/**
* BPM 发送同步 HTTP 请求触发器
*
* @author jason
*/
@Component
@Slf4j
public class BpmSyncHttpRequestTrigger extends BpmAbstractHttpRequestTrigger {
@Resource
private RestTemplate restTemplate;
@Resource
private BpmProcessInstanceService processInstanceService;
@Override
public BpmTriggerTypeEnum getType() {
return BpmTriggerTypeEnum.HTTP_REQUEST;
}
@Override
public void execute(String processInstanceId, String param) {
// 1. 解析 http 请求配置
HttpRequestTriggerSetting setting = JsonUtils.parseObject(param, HttpRequestTriggerSetting.class);
if (setting == null) {
log.error("[execute][流程({}) HTTP 触发器请求配置为空]", processInstanceId);
return;
}
// 2. 发起请求
ProcessInstance processInstance = processInstanceService.getProcessInstance(processInstanceId);
BpmHttpRequestUtils.executeBpmHttpRequest(processInstance,
setting.getUrl(),
setting.getHeader(),
setting.getBody(),
true, setting.getResponse(),
restTemplate,
processInstanceService);
}
}

View File

@ -1,8 +1,8 @@
package cn.iocoder.yudao.module.infra.service.logger; package cn.iocoder.yudao.module.infra.service.logger;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult; 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.BeanUtils;
import cn.iocoder.yudao.framework.common.util.string.StrUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO;
@ -35,8 +35,8 @@ public class ApiAccessLogServiceImpl implements ApiAccessLogService {
@Override @Override
public void createApiAccessLog(ApiAccessLogCreateReqDTO createDTO) { public void createApiAccessLog(ApiAccessLogCreateReqDTO createDTO) {
ApiAccessLogDO apiAccessLog = BeanUtils.toBean(createDTO, ApiAccessLogDO.class); ApiAccessLogDO apiAccessLog = BeanUtils.toBean(createDTO, ApiAccessLogDO.class);
apiAccessLog.setRequestParams(StrUtil.maxLength(apiAccessLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH)); apiAccessLog.setRequestParams(StrUtils.maxLength(apiAccessLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH));
apiAccessLog.setResultMsg(StrUtil.maxLength(apiAccessLog.getResultMsg(), RESULT_MSG_MAX_LENGTH)); apiAccessLog.setResultMsg(StrUtils.maxLength(apiAccessLog.getResultMsg(), RESULT_MSG_MAX_LENGTH));
if (TenantContextHolder.getTenantId() != null) { if (TenantContextHolder.getTenantId() != null) {
apiAccessLogMapper.insert(apiAccessLog); apiAccessLogMapper.insert(apiAccessLog);
} else { } else {

View File

@ -1,8 +1,8 @@
package cn.iocoder.yudao.module.infra.service.logger; package cn.iocoder.yudao.module.infra.service.logger;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult; 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.BeanUtils;
import cn.iocoder.yudao.framework.common.util.string.StrUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder; import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils; import cn.iocoder.yudao.framework.tenant.core.util.TenantUtils;
import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO;
@ -39,7 +39,7 @@ public class ApiErrorLogServiceImpl implements ApiErrorLogService {
public void createApiErrorLog(ApiErrorLogCreateReqDTO createDTO) { public void createApiErrorLog(ApiErrorLogCreateReqDTO createDTO) {
ApiErrorLogDO apiErrorLog = BeanUtils.toBean(createDTO, ApiErrorLogDO.class) ApiErrorLogDO apiErrorLog = BeanUtils.toBean(createDTO, ApiErrorLogDO.class)
.setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus()); .setProcessStatus(ApiErrorLogProcessStatusEnum.INIT.getStatus());
apiErrorLog.setRequestParams(StrUtil.maxLength(apiErrorLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH)); apiErrorLog.setRequestParams(StrUtils.maxLength(apiErrorLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH));
if (TenantContextHolder.getTenantId() != null) { if (TenantContextHolder.getTenantId() != null) {
apiErrorLogMapper.insert(apiErrorLog); apiErrorLogMapper.insert(apiErrorLog);
} else { } else {