diff --git a/README.md b/README.md index 935c0caad8..f326bcadf4 100644 --- a/README.md +++ b/README.md @@ -149,22 +149,45 @@ ### 工作流程 -| | 功能 | 描述 | -|----|-------|-----------------------------------------| -| 🚀 | 流程模型 | 配置工作流的流程模型,支持 BPMN 和仿钉钉/飞书设计器 | -| 🚀 | 流程表单 | 拖动表单元素生成相应的工作流表单,覆盖 Element UI 所有的表单组件 | -| 🚀 | 用户分组 | 自定义用户分组,可用于工作流的审批分组 | -| 🚀 | 我的流程 | 查看我发起的工作流程,支持新建、取消流程等操作,高亮流程图、审批时间线 | -| 🚀 | 待办任务 | 查看自己【未】审批的工作任务,支持通过、不通过、转派、委派、退回、加减签等操作 | -| 🚀 | 已办任务 | 查看自己【已】审批的工作任务,支持流程预测,展示未来审批人信息 | -| 🚀 | OA 请假 | 作为业务自定义接入工作流的使用示例,只需创建请求对应的工作流程,即可进行审批 | - ![功能图](/.image/common/bpm-feature.png) +基于 Flowable 构建,可支持信创(国产)数据库,满足中国特色流程操作: + | BPMN 设计器 | 钉钉/飞书设计器 | |------------------------------|--------------------------------| | ![](/.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 审批后,转给 A,A 继续审批后进入下一节点 | ✅ | +| 加签 | 允许当前审批人根据需要,自行增加当前节点的审批人,支持向前、向后加签 | ✅ | +| 减签 | (取消加签)在当前审批人操作之前,减少审批人 | ✅ | +| 撤销 | (取消流程)流程发起人,可以对流程进行撤销处理 | ✅ | +| 终止 | 系统管理员,在任意节点终止流程实例 | ✅ | +| 表单权限 | 支持拖拉拽配置表单,每个审批节点可配置只读、编辑、隐藏权限 | ✅ | +| 超时审批 | 配置超时审批时间,超时后自动触发审批通过、不通过、驳回等操作 | ✅ | +| 自动提醒 | 配置提醒时间,到达时间后自动触发短信、邮箱、站内信等通知提醒,支持自定义重复提醒频次 | ✅ | +| 父子流程 | 主流程设置子流程节点,子流程节点会自动触发子流程。子流程结束后,主流程才会执行(继续往下下执行),支持同步子流程、异步子流程 | ✅ | +| 条件分支 | (排它分支)用于在流程中实现决策,即根据条件选择一个分支执行 | ✅ | +| 并行分支 | 允许将流程分成多条分支,不进行条件判断,所有分支都会执行 | ✅ | +| 包容分支 | (条件分支 + 并行分支的结合体)允许基于条件选择多条分支执行,但如果没有任何一个分支满足条件,则可以选择默认分支 | ✅ | +| 路由分支 | 根据条件选择一个分支执行(重定向到指定配置节点),也可以选择默认分支执行(继续往下执行) | ✅ | +| 触发节点 | 执行到该节点,触发 HTTP 请求、HTTP 回调、更新数据、删除数据等 | ✅ | +| 延迟节点 | 执行到该节点,审批等待一段时间再执行,支持固定时长、固定日期等 | ✅ | +| 拓展设置 | 流程前置/后置通知,节点(任务)前置、后置通知,流程报表,自动审批去重,自定流程编号、标题、摘要,流程报表等 | ✅ | + ### 支付系统 | | 功能 | 描述 | diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java index e4eddf6ebd..b8ccad64b1 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApi.java @@ -1,7 +1,6 @@ package cn.iocoder.yudao.module.bpm.api.task; import cn.iocoder.yudao.module.bpm.api.task.dto.BpmProcessInstanceCreateReqDTO; - import jakarta.validation.Valid; /** @@ -20,4 +19,6 @@ public interface BpmProcessInstanceApi { */ String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO reqDTO); + + } diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApi.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApi.java new file mode 100644 index 0000000000..8f6f4258c8 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApi.java @@ -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); + +} diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java index 7fbc7ba223..65605142c2 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/ErrorCodeConstants.java @@ -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_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_DEPLOY_FAIL_FIRST_USER_TASK_CANDIDATE_STRATEGY_ERROR = new ErrorCode(1_009_002_008, "部署流程失败,原因:首个任务({})的审批人不能是【审批人自选】"); // ========== 流程定义 1-009-003-000 ========== 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_CAN_START = 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 ========== ErrorCode TASK_OPERATE_FAIL_ASSIGN_NOT_SELF = new ErrorCode(1_009_005_001, "操作失败,原因:该任务的审批人不是你"); diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventTypeEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventTypeEnum.java index a63804de32..69c5512599 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventTypeEnum.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmBoundaryEventTypeEnum.java @@ -14,7 +14,8 @@ import lombok.Getter; public enum BpmBoundaryEventTypeEnum { USER_TASK_TIMEOUT(1, "用户任务超时"), - DELAY_TIMER_TIMEOUT(2, "延迟器超时"); + DELAY_TIMER_TIMEOUT(2, "延迟器超时"), + CHILD_PROCESS_TIMEOUT(3, "子流程超时"); private final Integer type; private final String name; diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessMultiInstanceSourceTypeEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessMultiInstanceSourceTypeEnum.java new file mode 100644 index 0000000000..fab0ddfd71 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessMultiInstanceSourceTypeEnum.java @@ -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 { + + 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; + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserEmptyTypeEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserEmptyTypeEnum.java new file mode 100644 index 0000000000..55e6e02e87 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserEmptyTypeEnum.java @@ -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 { + + 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; + } +} diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserTypeEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserTypeEnum.java new file mode 100644 index 0000000000..10d04ea4f4 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmChildProcessStartUserTypeEnum.java @@ -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 { + + 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; + } +} diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java index 238a9347c3..e5ffa12025 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmSimpleModelNodeTypeEnum.java @@ -25,10 +25,13 @@ public enum BpmSimpleModelNodeTypeEnum implements ArrayValuable { START_USER_NODE(10, "发起人", "userTask"), // 发起人节点。前端的开始节点,Id 固定 APPROVE_NODE(11, "审批人", "userTask"), COPY_NODE(12, "抄送人", "serviceTask"), + TRANSACTOR_NODE(13, "办理人", "userTask"), DELAY_TIMER_NODE(14, "延迟器", "receiveTask"), TRIGGER_NODE(15, "触发器", "serviceTask"), + CHILD_PROCESS(20, "子流程", "callActivity"), + // 50 ~ 条件分支 CONDITION_NODE(50, "条件", "sequenceFlow"), // 用于构建流转条件的表达式 CONDITION_BRANCH_NODE(51, "条件分支", "exclusiveGateway"), diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTriggerTypeEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTriggerTypeEnum.java index 9a2f073726..13f997c7b4 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTriggerTypeEnum.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/definition/BpmTriggerTypeEnum.java @@ -16,8 +16,12 @@ import java.util.Arrays; @AllArgsConstructor public enum BpmTriggerTypeEnum implements ArrayValuable { - HTTP_REQUEST(1, "发起 HTTP 请求"), - UPDATE_NORMAL_FORM(2, "更新流程表单"); // TODO @jason:FORM_UPDATE + HTTP_REQUEST(1, "发起 HTTP 请求"), // BPM => 业务,流程继续执行,无需等待业务 + HTTP_CALLBACK(2, "接收 HTTP 回调"), // BPM => 业务 => BPM,流程卡主,等待业务回调 + + FORM_UPDATE(10, "更新流程表单数据"), + FORM_DELETE(11, "删除流程表单数据"), + ; /** * 触发器执行动作类型 @@ -39,5 +43,4 @@ public enum BpmTriggerTypeEnum implements ArrayValuable { public static BpmTriggerTypeEnum typeOf(Integer type) { return ArrayUtil.firstMatch(item -> item.getType().equals(type), values()); } - } diff --git a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java index d41c0c3133..b0ade75299 100644 --- a/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java +++ b/yudao-module-bpm/yudao-module-bpm-api/src/main/java/cn/iocoder/yudao/module/bpm/enums/task/BpmReasonEnum.java @@ -26,6 +26,7 @@ public enum BpmReasonEnum { TIMEOUT_REJECT("审批超时,系统自动不通过"), ASSIGN_START_USER_APPROVE("审批人与提交人为同一人时,自动通过"), 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_TRANSFER_DEPT_LEADER("审批人与提交人为同一人时,转交给部门负责人审批"), ASSIGN_EMPTY_APPROVE("审批人为空,自动通过"), diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java index f8aea4d74e..5ebb12f0f5 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessInstanceApiImpl.java @@ -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.service.task.BpmProcessInstanceService; -import org.springframework.stereotype.Service; -import org.springframework.validation.annotation.Validated; - import jakarta.annotation.Resource; import jakarta.validation.Valid; +import org.springframework.stereotype.Service; +import org.springframework.validation.annotation.Validated; /** * Flowable 流程实例 Api 实现类 @@ -25,4 +24,5 @@ public class BpmProcessInstanceApiImpl implements BpmProcessInstanceApi { public String createProcessInstance(Long userId, @Valid BpmProcessInstanceCreateReqDTO reqDTO) { return processInstanceService.createProcessInstance(userId, reqDTO); } + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApiImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApiImpl.java new file mode 100644 index 0000000000..d99aa03c6a --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/api/task/BpmProcessTaskApiImpl.java @@ -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); + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java index 6483ee0e39..5578114f9d 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmModelController.java @@ -57,7 +57,7 @@ public class BpmModelController { @GetMapping("/list") @Operation(summary = "获得模型分页") @Parameter(name = "name", description = "模型名称", example = "芋艿") - public CommonResult> getModelPage(@RequestParam(value = "name", required = false) String name) { + public CommonResult> getModelList(@RequestParam(value = "name", required = false) String name) { List list = modelService.getModelList(name); if (CollUtil.isEmpty(list)) { return success(Collections.emptyList()); diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java index 28cbd0ab98..90c38886a7 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/BpmProcessDefinitionController.java @@ -17,6 +17,7 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import org.flowable.bpmn.model.BpmnModel; +import org.flowable.common.engine.impl.db.SuspensionState; import org.flowable.engine.repository.Deployment; import org.flowable.engine.repository.ProcessDefinition; import org.springframework.security.access.prepost.PreAuthorize; @@ -31,6 +32,7 @@ import java.util.List; import java.util.Map; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @@ -99,6 +101,17 @@ public class BpmProcessDefinitionController { list, null, processDefinitionMap, null, null)); } + @GetMapping("/simple-list") + @Operation(summary = "获得流程定义精简列表", description = "只包含未挂起的流程,主要用于前端的下拉选项") + public CommonResult> getSimpleProcessDefinitionList() { + // 只查询未挂起的流程 + List list = processDefinitionService.getProcessDefinitionListBySuspensionState( + SuspensionState.ACTIVE.getStateCode()); + // 拼接 VO 返回,只返回 id、name、key + return success(convertList(list, definition -> new BpmProcessDefinitionRespVO() + .setId(definition.getId()).setName(definition.getName()).setKey(definition.getKey()))); + } + @GetMapping ("/get") @Operation(summary = "获得流程定义") @Parameter(name = "id", description = "流程编号", required = true, example = "1024") diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java index 7bdc963c8a..254a5178f0 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelMetaInfoVO.java @@ -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.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.BpmModelFormTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum; @@ -27,8 +28,7 @@ import java.util.List; @Data public class BpmModelMetaInfoVO { - @Schema(description = "流程图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao.jpg") - @NotEmpty(message = "流程图标不能为空") + @Schema(description = "流程图标", example = "https://www.iocoder.cn/yudao.jpg") @URL(message = "流程图标格式不正确") private String icon; @@ -46,6 +46,7 @@ public class BpmModelMetaInfoVO { private Integer formType; @Schema(description = "表单编号", example = "1024") private Long formId; // formType 为 NORMAL 使用,必须非空 + @Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/create") private String formCustomCreatePath; // 表单类型为 CUSTOM 时,必须非空 @Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址", example = "/bpm/oa/leave/view") @@ -81,6 +82,14 @@ public class BpmModelMetaInfoVO { @Schema(description = "摘要设置", example = "{}") private SummarySetting summarySetting; + // TODO @lesan:processBeforeTriggerSetting;要不叫这个?主要考虑,notify 留给后续的站内信、短信、邮件这种 notify 通知哈。 + @Schema(description = "流程前置通知设置", example = "{}") + private HttpRequestSetting PreProcessNotifySetting; + + // TODO @lesan:processAfterTriggerSetting + @Schema(description = "流程后置通知设置", example = "{}") + private HttpRequestSetting PostProcessNotifySetting; + @Schema(description = "流程 ID 规则") @Data @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 header; + + @Schema(description = "请求头参数设置", example = "[]") + @Valid + private List body; + + /** + * 请求返回处理设置,用于修改流程表单值 + *

+ * key:表示要修改的流程表单字段名(name) + * value:接口返回的字段名 + */ + @Schema(description = "请求返回处理设置", example = "[]") + private List> response; + + } + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelRespVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelRespVO.java index 278291483d..275368c7c6 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelRespVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/BpmModelRespVO.java @@ -25,7 +25,7 @@ public class BpmModelRespVO extends BpmModelMetaInfoVO { @Schema(description = "流程图标", example = "https://www.iocoder.cn/yudao.jpg") private String icon; - @Schema(description = "流程分类编码", example = "1") + @Schema(description = "流程分类编号", example = "1") private String category; @Schema(description = "流程分类名字", example = "请假") private String categoryName; diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java index 291cf4d830..f002e6894a 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/model/simple/BpmSimpleModelNodeVO.java @@ -4,16 +4,19 @@ import cn.iocoder.yudao.framework.common.core.KeyValue; import cn.iocoder.yudao.framework.common.validation.InEnum; import cn.iocoder.yudao.module.bpm.enums.definition.*; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; +import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonInclude; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; +import org.flowable.bpmn.model.IOParameter; import org.hibernate.validator.constraints.URL; import java.util.List; import java.util.Map; +import java.util.Set; @Schema(description = "管理后台 - 仿钉钉流程设计模型节点 VO") @Data @@ -114,7 +117,8 @@ public class BpmSimpleModelNodeVO { @Schema(description = "路由分支组", example = "[]") private List 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 /** @@ -122,6 +126,15 @@ public class BpmSimpleModelNodeVO { */ 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 = "任务监听器") @Valid @Data @@ -345,12 +358,10 @@ public class BpmSimpleModelNodeVO { @Valid private HttpRequestTriggerSetting httpRequestSetting; - // TODO @jason:这个要不直接叫 formSetting,更好理解一点哈 - // TODO @jason:如果搞成 List,是不是可以做条件组了?微信讨论哈 /** * 流程表单触发器设置 */ - private NormalFormTriggerSetting normalFormSetting; + private List formSettings; @Schema(description = "http 请求触发器设置", example = "{}") @Data @@ -369,7 +380,6 @@ public class BpmSimpleModelNodeVO { @Valid private List body; - // TODO @json:可能未来看情况,搞个 HttpResponseParam;得看看有没别的业务需要,抽象统一 /** * 请求返回处理设置,用于修改流程表单值 *

@@ -379,15 +389,137 @@ public class BpmSimpleModelNodeVO { @Schema(description = "请求返回处理设置", example = "[]") private List> response; + /** + * Http 回调请求,需要指定回调任务 Key,用于回调执行 + */ + @Schema(description = "回调任务 Key", example = "xxx", hidden = true) + private String callbackTaskDefineKey; + } @Schema(description = "流程表单触发器设置", example = "{}") @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 updateFormFields; + @Schema(description = "删除表单字段", example = "[]") + private Set 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 inVariables; + + @Schema(description = "输出参数(子->主)", example = "[]") + private List 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; + } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java index 1e9dfc8207..0ec737174a 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/definition/vo/process/BpmProcessDefinitionRespVO.java @@ -1,5 +1,6 @@ package cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process; +import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -8,7 +9,7 @@ import java.util.List; @Schema(description = "管理后台 - 流程定义 Response VO") @Data -public class BpmProcessDefinitionRespVO { +public class BpmProcessDefinitionRespVO extends BpmModelMetaInfoVO { @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private String id; @@ -19,15 +20,9 @@ public class BpmProcessDefinitionRespVO { @Schema(description = "流程名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") private String name; - @Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao") + @Schema(description = "流程标识", requiredMode = Schema.RequiredMode.REQUIRED, example = "youdao") private String key; - @Schema(description = "流程图标", requiredMode = Schema.RequiredMode.REQUIRED, example = "https://www.iocoder.cn/yudao.jpg") - private String icon; - - @Schema(description = "流程描述", example = "我是描述") - private String description; - @Schema(description = "流程分类", example = "1") private String category; @Schema(description = "流程分类名字", example = "请假") @@ -36,22 +31,15 @@ public class BpmProcessDefinitionRespVO { @Schema(description = "流程模型的类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "10") private Integer modelType; // 参见 BpmModelTypeEnum 枚举类 - @Schema(description = "表单类型-参见 bpm_model_form_type 数据字典", example = "1") - private Integer formType; - @Schema(description = "表单编号-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", example = "1024") - private Long formId; - @Schema(description = "表单名字", example = "请假表单") - private String formName; + @Schema(description = "流程模型的编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "ABC") + private String modelId; + @Schema(description = "表单的配置-JSON 字符串。在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", requiredMode = Schema.RequiredMode.REQUIRED) private String formConf; @Schema(description = "表单项的数组-JSON 字符串的数组。在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", requiredMode = Schema.RequiredMode.REQUIRED) private List formFields; - @Schema(description = "自定义表单的提交路径,使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", - example = "/bpm/oa/leave/create") - private String formCustomCreatePath; - @Schema(description = "自定义表单的查看路径,使用 Vue 的路由地址-在表单类型为 {@link BpmModelFormTypeEnum#CUSTOM} 时,必须非空", - example = "/bpm/oa/leave/view") - private String formCustomViewPath; + @Schema(description = "表单名字", example = "请假表单") + private String formName; @Schema(description = "中断状态-参见 SuspensionState 枚举", requiredMode = Schema.RequiredMode.REQUIRED, example = "1") private Integer suspensionState; // 参见 SuspensionState 枚举 @@ -67,7 +55,7 @@ public class BpmProcessDefinitionRespVO { @Schema(description = "流程定义排序", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024") private Long sort; - + @Schema(description = "BPMN UserTask 用户任务") @Data public static class UserTask { diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java index 3a847ce4e8..9316261678 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/BpmProcessInstanceController.java @@ -1,8 +1,10 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task; 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.PageResult; +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; 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.convert.task.BpmProcessInstanceConvert; @@ -30,10 +32,10 @@ import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; +import java.util.Set; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertList; -import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet; +import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.*; import static cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId; @Tag(name = "管理后台 - 流程实例") // 流程实例,通过流程定义创建的一次“申请” @@ -76,8 +78,14 @@ public class BpmProcessInstanceController { convertSet(processDefinitionMap.values(), ProcessDefinition::getCategory)); Map processDefinitionInfoMap = processDefinitionService.getProcessDefinitionInfoMap( convertSet(pageResult.getList(), HistoricProcessInstance::getProcessDefinitionId)); + Set 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 userMap = adminUserApi.getUserMap(userIds); + Map deptMap = deptApi.getDeptMap( + convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); return success(BpmProcessInstanceConvert.INSTANCE.buildProcessInstancePage(pageResult, - processDefinitionMap, categoryMap, taskMap, null, null, processDefinitionInfoMap)); + processDefinitionMap, categoryMap, taskMap, userMap, deptMap, processDefinitionInfoMap)); } @GetMapping("/manager-page") @@ -140,6 +148,7 @@ public class BpmProcessInstanceController { processDefinition, processDefinitionInfo, startUser, dept)); } + // TODO @lesan:【子流程】子流程如果取消,主流程应该是通过、还是不通过哈?还是禁用掉子流程的取消? @DeleteMapping("/cancel-by-start-user") @Operation(summary = "用户取消流程实例", description = "取消发起的流程") @PreAuthorize("@ss.hasPermission('bpm:process-instance:cancel')") @@ -162,10 +171,25 @@ public class BpmProcessInstanceController { @Operation(summary = "获得审批详情") @Parameter(name = "id", description = "流程实例的编号", required = true) @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')") + @SuppressWarnings("unchecked") public CommonResult getApprovalDetail(@Valid BpmApprovalDetailReqVO reqVO) { + if (StrUtil.isNotEmpty(reqVO.getProcessVariablesStr())) { + reqVO.setProcessVariables(JsonUtils.parseObject(reqVO.getProcessVariablesStr(), Map.class)); + } return success(processInstanceService.getApprovalDetail(getLoginUserId(), reqVO)); } + @GetMapping("/get-next-approval-nodes") + @Operation(summary = "获取下一个执行的流程节点") + @PreAuthorize("@ss.hasPermission('bpm:process-instance:query')") + @SuppressWarnings("unchecked") + public CommonResult> 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") @Operation(summary = "获取流程实例的 BPMN 模型视图", description = "在【流程详细】界面中,进行调用") @Parameter(name = "id", description = "流程实例的编号", required = true) diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java index 9121f10362..069ec2000e 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmApprovalDetailReqVO.java @@ -18,6 +18,9 @@ public class BpmApprovalDetailReqVO { @Schema(description = "流程变量") private Map processVariables; // 使用场景:同 processDefinitionId,用于流程预测 + @Schema(description = "流程变量") + private String processVariablesStr; // 解决 GET 无法传递对象的问题,最终转换成 processVariables 变量 + @Schema(description = "流程实例的编号", example = "1024") private String processInstanceId; // 使用场景:流程已发起时候传流程实例 ID diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java index 76dca606a2..f73c490a54 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/instance/BpmProcessInstanceRespVO.java @@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.bpm.controller.admin.task.vo.instance; import cn.iocoder.yudao.framework.common.core.KeyValue; import cn.iocoder.yudao.module.bpm.controller.admin.base.user.UserSimpleBaseVO; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.process.BpmProcessDefinitionRespVO; +import com.fasterxml.jackson.annotation.JsonIgnore; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @@ -73,6 +74,13 @@ public class BpmProcessInstanceRespVO { @Schema(description = "任务名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "芋道") private String name; + @Schema(description = "任务分配人编号", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2048") + @JsonIgnore // 不返回,只是方便后续读取,赋值给 assigneeUser + private Long assignee; + + @Schema(description = "任务分配人", requiredMode = Schema.RequiredMode.NOT_REQUIRED, example = "2048") + private UserSimpleBaseVO assigneeUser; + } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java index 40df86efc8..0969fda135 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskApproveReqVO.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import lombok.Data; +import java.util.List; import java.util.Map; @Schema(description = "管理后台 - 通过流程任务的 Request VO") @@ -23,4 +24,7 @@ public class BpmTaskApproveReqVO { @Schema(description = "变量实例(动态表单)", requiredMode = Schema.RequiredMode.REQUIRED) private Map variables; + @Schema(description = "下一个节点审批人", example = "{nodeId:[1, 2]}") + private Map> nextAssignees; // 为什么是 Map,而不是 List 呢?因为下一个节点可能是多个,例如说并行网关的情况 + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskPageReqVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskPageReqVO.java index 11c59ce3ed..f129e5a315 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskPageReqVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskPageReqVO.java @@ -18,6 +18,9 @@ public class BpmTaskPageReqVO extends PageParam { @Schema(description = "流程分类", example = "1") private String category; + @Schema(description = "流程定义的标识", example = "2048") + private String processDefinitionKey; // 精准匹配 + @Schema(description = "创建时间") @DateTimeFormat(pattern = DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND) private LocalDateTime[] createTime; diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java index 83812ee1ab..fce72a490a 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/controller/admin/task/vo/task/BpmTaskRespVO.java @@ -85,6 +85,9 @@ public class BpmTaskRespVO { @Schema(description = "是否填写审批意见", example = "false") private Boolean reasonRequire; + @Schema(description = "节点类型", example = "10") + private Integer nodeType; // 参见 BpmSimpleModelNodeTypeEnum 枚举。 + @Data @Schema(description = "流程实例") public static class ProcessInstance { diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java index 6e798e77f2..ccd7f06e74 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/convert/task/BpmProcessInstanceConvert.java @@ -76,6 +76,15 @@ public interface BpmProcessInstanceConvert { respVO.setStartUser(BeanUtils.toBean(startUser, UserSimpleBaseVO.class)); MapUtils.findAndThen(deptMap, startUser.getDeptId(), dept -> respVO.getStartUser().setDeptName(dept.getName())); } + if (CollUtil.isNotEmpty(respVO.getTasks())) { + respVO.getTasks().forEach(task -> { + AdminUserRespDTO assigneeUser = userMap.get(task.getAssignee()); + if (assigneeUser!= null) { + task.setAssigneeUser(BeanUtils.toBean(assigneeUser, UserSimpleBaseVO.class)); + MapUtils.findAndThen(deptMap, assigneeUser.getDeptId(), dept -> task.getAssigneeUser().setDeptName(dept.getName())); + } + }); + } } // 摘要 respVO.setSummary(FlowableUtils.getSummary(processDefinitionInfoMap.get(respVO.getProcessDefinitionId()), diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java index ccab7b0435..549889e3aa 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/dal/dataobject/definition/BpmProcessDefinitionInfoDO.java @@ -2,7 +2,6 @@ package cn.iocoder.yudao.module.bpm.dal.dataobject.definition; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.type.LongListTypeHandler; -import cn.iocoder.yudao.framework.mybatis.core.type.StringListTypeHandler; import cn.iocoder.yudao.module.bpm.controller.admin.definition.vo.model.BpmModelMetaInfoVO; import cn.iocoder.yudao.module.bpm.enums.definition.BpmAutoApproveTypeEnum; import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelFormTypeEnum; @@ -60,6 +59,14 @@ public class BpmProcessDefinitionInfoDO extends BaseDO { */ private Integer modelType; + /** + * 流程分类的编码 + * + * 关联 {@link BpmCategoryDO#getCode()} + * + * 为什么要存储?原因是,{@link ProcessDefinition#getCategory()} 无法设置 + */ + private String category; /** * 图标 */ @@ -149,7 +156,7 @@ public class BpmProcessDefinitionInfoDO extends BaseDO { * * 关联 {@link AdminUserRespDTO#getId()} 字段的数组 */ - @TableField(typeHandler = StringListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤 + @TableField(typeHandler = LongListTypeHandler.class) // 为了可以使用 find_in_set 进行过滤 private List managerUserIds; /** @@ -175,11 +182,22 @@ public class BpmProcessDefinitionInfoDO extends BaseDO { */ @TableField(typeHandler = JacksonTypeHandler.class) private BpmModelMetaInfoVO.TitleSetting titleSetting; - /** * 摘要设置 */ @TableField(typeHandler = JacksonTypeHandler.class) private BpmModelMetaInfoVO.SummarySetting summarySetting; + // TODO @lesan:processBeforeTriggerSetting;要不叫这个?主要考虑,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; + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java index d239dbe3ff..57f4d393f3 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmParallelMultiInstanceBehavior.java @@ -1,15 +1,21 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior; 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.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 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.impl.bpmn.behavior.AbstractBpmnActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.ParallelMultiInstanceBehavior; +import java.util.List; import java.util.Set; /** @@ -42,27 +48,44 @@ public class BpmParallelMultiInstanceBehavior extends ParallelMultiInstanceBehav */ @Override protected int resolveNrOfInstances(DelegateExecution execution) { - // 第一步,设置 collectionVariable 和 CollectionVariable - // 从 execution.getVariable() 读取所有任务处理人的 key - super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的 - super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId()); - // 从 execution.getVariable() 读取当前所有任务处理的人的 key - super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId()); + // 情况一:UserTask 节点 + if (execution.getCurrentFlowElement() instanceof UserTask) { + // 第一步,设置 collectionVariable 和 CollectionVariable + // 从 execution.getVariable() 读取所有任务处理人的 key + super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的 + super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId()); + // 从 execution.getVariable() 读取当前所有任务处理的人的 key + super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId()); - // 第二步,获取任务的所有处理人 - @SuppressWarnings("unchecked") - Set assigneeUserIds = (Set) execution.getVariable(super.collectionVariable, Set.class); - if (assigneeUserIds == null) { - assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution); - if (CollUtil.isEmpty(assigneeUserIds)) { - // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过! - // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务 - // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时 - assigneeUserIds = SetUtils.asSet((Long) null); + // 第二步,获取任务的所有处理人 + @SuppressWarnings("unchecked") + Set assigneeUserIds = (Set) execution.getVariable(super.collectionVariable, Set.class); + if (assigneeUserIds == null) { + assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution); + if (CollUtil.isEmpty(assigneeUserIds)) { + // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过! + // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务 + // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时 + assigneeUserIds = SetUtils.asSet((Long) null); + } + execution.setVariableLocal(super.collectionVariable, assigneeUserIds); } - execution.setVariableLocal(super.collectionVariable, assigneeUserIds); + 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); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java index b3a3a24f80..cb748182ed 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/behavior/BpmSequentialMultiInstanceBehavior.java @@ -2,14 +2,20 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.behavior; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.util.collection.SetUtils; +import cn.iocoder.yudao.module.bpm.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 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.impl.bpmn.behavior.AbstractBpmnActivityBehavior; import org.flowable.engine.impl.bpmn.behavior.SequentialMultiInstanceBehavior; +import java.util.List; import java.util.Set; /** @@ -35,28 +41,45 @@ public class BpmSequentialMultiInstanceBehavior extends SequentialMultiInstanceB */ @Override protected int resolveNrOfInstances(DelegateExecution execution) { - // 第一步,设置 collectionVariable 和 CollectionVariable - // 从 execution.getVariable() 读取所有任务处理人的 key - super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的 - super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId()); - // 从 execution.getVariable() 读取当前所有任务处理的人的 key - super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId()); + // 情况一:UserTask 节点 + if (execution.getCurrentFlowElement() instanceof UserTask) { + // 第一步,设置 collectionVariable 和 CollectionVariable + // 从 execution.getVariable() 读取所有任务处理人的 key + super.collectionExpression = null; // collectionExpression 和 collectionVariable 是互斥的 + super.collectionVariable = FlowableUtils.formatExecutionCollectionVariable(execution.getCurrentActivityId()); + // 从 execution.getVariable() 读取当前所有任务处理的人的 key + super.collectionElementVariable = FlowableUtils.formatExecutionCollectionElementVariable(execution.getCurrentActivityId()); - // 第二步,获取任务的所有处理人 - // 不使用 execution.getVariable 原因:目前依次审批任务回退后 collectionVariable 变量没有清理, 如果重新进入该任务不会重新分配审批人 - @SuppressWarnings("unchecked") - Set assigneeUserIds = (Set) execution.getVariableLocal(super.collectionVariable, Set.class); - if (assigneeUserIds == null) { - assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution); - if (CollUtil.isEmpty(assigneeUserIds)) { - // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过! - // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务 - // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时 - assigneeUserIds = SetUtils.asSet((Long) null); + // 第二步,获取任务的所有处理人 + // 不使用 execution.getVariable 原因:目前依次审批任务回退后 collectionVariable 变量没有清理, 如果重新进入该任务不会重新分配审批人 + @SuppressWarnings("unchecked") + Set assigneeUserIds = (Set) execution.getVariableLocal(super.collectionVariable, Set.class); + if (assigneeUserIds == null) { + assigneeUserIds = taskCandidateInvoker.calculateUsersByTask(execution); + if (CollUtil.isEmpty(assigneeUserIds)) { + // 特殊:如果没有处理人的情况下,至少有一个 null 空元素,避免自动通过! + // 这样,保证在 BpmUserTaskActivityBehavior 至少创建出一个 Task 任务 + // 用途:1)审批人为空时;2)审批类型为自动通过、自动拒绝时 + assigneeUserIds = SetUtils.asSet((Long) null); + } + execution.setVariableLocal(super.collectionVariable, assigneeUserIds); } - execution.setVariableLocal(super.collectionVariable, assigneeUserIds); + 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); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java index 952f0f1bea..7f66b29d3b 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/BpmTaskCandidateInvoker.java @@ -19,6 +19,7 @@ import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; import com.google.common.annotations.VisibleForTesting; import lombok.extern.slf4j.Slf4j; import org.flowable.bpmn.model.BpmnModel; +import org.flowable.bpmn.model.CallActivity; import org.flowable.bpmn.model.FlowElement; import org.flowable.bpmn.model.UserTask; import org.flowable.engine.delegate.DelegateExecution; @@ -129,8 +130,12 @@ public class BpmTaskCandidateInvoker { public Set calculateUsersByActivity(BpmnModel bpmnModel, String activityId, Long startUserId, String processDefinitionId, Map processVariables) { - // 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过 + // 如果是 CallActivity 子流程,不进行计算候选人 FlowElement flowElement = BpmnModelUtils.getFlowElementById(bpmnModel, activityId); + if (flowElement instanceof CallActivity) { + return new HashSet<>(); + } + // 审批类型非人工审核时,不进行计算候选人。原因是:后续会自动通过、不通过 Integer approveType = BpmnModelUtils.parseApproveType(flowElement); if (ObjectUtils.equalsAny(approveType, BpmUserTaskApproveTypeEnum.AUTO_APPROVE.getType(), diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateApproveUserSelectStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateApproveUserSelectStrategy.java new file mode 100644 index 0000000000..a316925655 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateApproveUserSelectStrategy.java @@ -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 calculateUsersByTask(DelegateExecution execution, String param) { + ProcessInstance processInstance = processInstanceService.getProcessInstance(execution.getProcessInstanceId()); + Assert.notNull(processInstance, "流程实例({})不能为空", execution.getProcessInstanceId()); + Map> approveUserSelectAssignees = FlowableUtils.getApproveUserSelectAssignees(processInstance); + Assert.notNull(approveUserSelectAssignees, "流程实例({}) 的下一个执行节点审批人不能为空", + execution.getProcessInstanceId()); + if (approveUserSelectAssignees == null) { + return Sets.newLinkedHashSet(); + } + // 获得审批人 + List assignees = approveUserSelectAssignees.get(execution.getCurrentActivityId()); + return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet(); + } + + @Override + public LinkedHashSet calculateUsersByActivity(BpmnModel bpmnModel, String activityId, String param, + Long startUserId, String processDefinitionId, Map processVariables) { + if (processVariables == null) { + return Sets.newLinkedHashSet(); + } + // 流程预测时会使用,允许审批人为空,如果为空前端会弹出提示选择下一个节点审批人,避免流程无法进行,审批时会真正校验节点是否配置审批人 + Map> approveUserSelectAssignees = FlowableUtils.getApproveUserSelectAssignees(processVariables); + if (approveUserSelectAssignees == null) { + return Sets.newLinkedHashSet(); + } + // 获得审批人 + List assignees = approveUserSelectAssignees.get(activityId); + return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet(); + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategy.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategy.java index 9fd14d6ded..9304d288ac 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategy.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/candidate/strategy/dept/BpmTaskCandidateStartUserSelectStrategy.java @@ -2,24 +2,21 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.d import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; -import cn.hutool.core.util.ObjectUtil; import cn.iocoder.yudao.module.bpm.framework.flowable.core.candidate.strategy.user.BpmTaskCandidateUserStrategy; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; -import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.FlowableUtils; import cn.iocoder.yudao.module.bpm.service.task.BpmProcessInstanceService; import com.google.common.collect.Sets; import jakarta.annotation.Resource; import org.flowable.bpmn.model.BpmnModel; -import org.flowable.bpmn.model.ServiceTask; -import org.flowable.bpmn.model.Task; -import org.flowable.bpmn.model.UserTask; import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.runtime.ProcessInstance; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Component; -import java.util.*; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; /** * 发起人自选 {@link BpmTaskCandidateUserStrategy} 实现类 @@ -55,7 +52,7 @@ public class BpmTaskCandidateStartUserSelectStrategy extends AbstractBpmTaskCand execution.getProcessInstanceId()); // 获得审批人 List assignees = startUserSelectAssignees.get(execution.getCurrentActivityId()); - return new LinkedHashSet<>(assignees); + return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet(); } @Override @@ -70,28 +67,7 @@ public class BpmTaskCandidateStartUserSelectStrategy extends AbstractBpmTaskCand } // 获得审批人 List assignees = startUserSelectAssignees.get(activityId); - return new LinkedHashSet<>(assignees); - } - - /** - * 获得发起人自选审批人或抄送人的 Task 列表 - * - * @param bpmnModel BPMN 模型 - * @return Task 列表 - */ - public static List getStartUserSelectTaskList(BpmnModel bpmnModel) { - if (bpmnModel == null) { - return Collections.emptyList(); - } - List tasks = new ArrayList<>(); - tasks.addAll(BpmnModelUtils.getBpmnModelElements(bpmnModel, UserTask.class)); - tasks.addAll(BpmnModelUtils.getBpmnModelElements(bpmnModel, ServiceTask.class)); - if (CollUtil.isEmpty(tasks)) { - return Collections.emptyList(); - } - tasks.removeIf(task -> ObjectUtil.notEqual(BpmnModelUtils.parseCandidateStrategy(task), - BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy())); - return tasks; + return CollUtil.isNotEmpty(assignees) ? new LinkedHashSet<>(assignees) : Sets.newLinkedHashSet(); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java index 990bc6303a..2a45e3a10c 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmTaskCandidateStrategyEnum.java @@ -24,7 +24,8 @@ public enum BpmTaskCandidateStrategyEnum implements ArrayValuable { MULTI_DEPT_LEADER_MULTI(23, "连续多级部门的负责人"), POST(22, "岗位"), USER(30, "用户"), - START_USER_SELECT(35, "发起人自选"), // 申请人自己,可在提交申请时选择此节点的审批人 + APPROVE_USER_SELECT(34, "审批人自身"), // 当前审批人,可在审批时,选择下一个节点的审批人 + START_USER_SELECT(35, "发起人自选"), // 申请人自己,可在提交申请时,选择此节点的审批人 START_USER(36, "发起人自己"), // 申请人自己, 一般紧挨开始节点,常用于发起人信息审核场景 START_USER_DEPT_LEADER(37, "发起人部门负责人"), START_USER_DEPT_LEADER_MULTI(38, "发起人连续多级部门的负责人"), diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java index 9dce8a2eae..e416428c4e 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnModelConstants.java @@ -1,5 +1,7 @@ package cn.iocoder.yudao.module.bpm.framework.flowable.core.enums; +import cn.iocoder.yudao.module.bpm.enums.definition.BpmModelTypeEnum; + /** * BPMN XML 常量信息 * @@ -66,6 +68,11 @@ public interface BpmnModelConstants { */ String USER_TASK_APPROVE_METHOD = "approveMethod"; + /** + * BPMN Child Process 的扩展属性,用于标记多实例来源类型 + */ + String CHILD_PROCESS_MULTI_INSTANCE_SOURCE_TYPE = "childProcessMultiInstanceSourceType"; + /** * BPMN ExtensionElement 流程表单字段权限元素, 用于标记字段权限 */ @@ -129,4 +136,11 @@ public interface BpmnModelConstants { */ String REASON_REQUIRE = "reasonRequire"; + /** + * 节点类型 + * + * 目前只有 {@link BpmModelTypeEnum#SIMPLE} 的 UserTask 节点会设置该属性,用于区分是审批节点、还是办理节点 + */ + String NODE_TYPE = "nodeType"; + } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java index 8172cf59a5..893c4d053c 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/enums/BpmnVariableConstants.java @@ -27,8 +27,16 @@ public class BpmnVariableConstants { * 流程实例的变量 - 发起用户选择的审批人 Map * * @see ProcessInstance#getProcessVariables() + * @see BpmTaskCandidateStrategyEnum#START_USER_SELECT */ 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 * @@ -51,6 +59,13 @@ public class BpmnVariableConstants { */ 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"; + /** * 流程实例的变量 - 流程开始时间 * diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java index 5251aea64f..ba2aaa6bcb 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmProcessInstanceEventListener.java @@ -22,6 +22,7 @@ import java.util.Set; public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEventListener { public static final Set PROCESS_INSTANCE_EVENTS = ImmutableSet.builder() + .add(FlowableEngineEventType.PROCESS_CREATED) .add(FlowableEngineEventType.PROCESS_COMPLETED) .add(FlowableEngineEventType.PROCESS_CANCELLED) .build(); @@ -34,6 +35,11 @@ public class BpmProcessInstanceEventListener extends AbstractFlowableEngineEvent super(PROCESS_INSTANCE_EVENTS); } + @Override + protected void processCreated(FlowableEngineEntityEvent event) { + processInstanceService.processProcessInstanceCreated((ProcessInstance)event.getEntity()); + } + @Override protected void processCompleted(FlowableEngineEntityEvent event) { processInstanceService.processProcessInstanceCompleted((ProcessInstance)event.getEntity()); diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java index ecef8fdb48..329241f799 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/listener/BpmTaskEventListener.java @@ -109,7 +109,11 @@ public class BpmTaskEventListener extends AbstractFlowableEngineEventListener { // 2.2 延迟器超时处理 } else if (ObjectUtil.equal(bpmTimerBoundaryEventType, BpmBoundaryEventTypeEnum.DELAY_TIMER_TIMEOUT)) { 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); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java new file mode 100644 index 0000000000..7f4428aac3 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmHttpRequestUtils.java @@ -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 headerParams, + List bodyParams, + Boolean handleResponse, + List> response, + // TODO @lesan:RestTemplate 直接通过 springUtil 获取好咧; + RestTemplate restTemplate, + // TODO @lesan:processInstanceService 直接通过 springUtil 获取好咧; + BpmProcessInstanceService processInstanceService) { + + // 1.1 设置请求头 + MultiValueMap headers = buildHttpHeaders(processInstance, headerParams); + // 1.2 设置请求体 + MultiValueMap body = buildHttpBody(processInstance, bodyParams); + + // 2. 发起请求 + ResponseEntity 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> respResult = JsonUtils.parseObjectQuietly(responseEntity.getBody(), + new TypeReference<>() {}); + if (respResult == null || !respResult.isSuccess()) { + return; + } + // 3.3 获取需要更新的流程变量 + Map updateVariables = getNeedUpdatedVariablesFromResponse(respResult.getData(), response); + // 3.4 更新流程变量 + if (CollUtil.isNotEmpty(updateVariables)) { + processInstanceService.updateProcessInstanceVariables(processInstance.getId(), updateVariables); + } + } + } + + public static ResponseEntity sendHttpRequest(String url, + MultiValueMap headers, + MultiValueMap body, + RestTemplate restTemplate) { + HttpEntity> requestEntity = new HttpEntity<>(body, headers); + ResponseEntity 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 buildHttpHeaders(ProcessInstance processInstance, + List headerSettings) { + Map processVariables = processInstance.getProcessVariables(); + MultiValueMap headers = new LinkedMultiValueMap<>(); + headers.add(HEADER_TENANT_ID, processInstance.getTenantId()); + addHttpRequestParam(headers, headerSettings, processVariables); + return headers; + } + + public static MultiValueMap buildHttpBody(ProcessInstance processInstance, + List bodySettings) { + Map processVariables = processInstance.getProcessVariables(); + MultiValueMap body = new LinkedMultiValueMap<>(); + addHttpRequestParam(body, bodySettings, processVariables); + body.add("processInstanceId", processInstance.getId()); + return body; + } + + /** + * 从请求返回值获取需要更新的流程变量 + * + * @param result 请求返回结果 + * @param responseSettings 返回设置 + * @return 需要更新的流程变量 + */ + public static Map getNeedUpdatedVariablesFromResponse(Map result, + List> responseSettings) { + Map 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 params, + List paramSettings, + Map 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()); + } + }); + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java index 3f22c7c259..2ec26b407e 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/BpmnModelUtils.java @@ -158,6 +158,17 @@ public class BpmnModelUtils { 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); } + /** + * 给节点添加节点类型 + * + * @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 简单查找相关的方法 ========== /** @@ -777,71 +808,214 @@ public class BpmnModelUtils { // 情况:ExclusiveGateway 排它,只有一个满足条件的。如果没有,就走默认的 if (currentElement instanceof ExclusiveGateway) { // 查找满足条件的 SequenceFlow 路径 - Gateway gateway = (Gateway) currentElement; - SequenceFlow matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), - flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) - && evalConditionExpress(variables, flow.getConditionExpression())); - if (matchSequenceFlow == null) { - matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), - flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId())); - // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的 - if (matchSequenceFlow == null && gateway.getOutgoingFlows().size() == 1) { - matchSequenceFlow = gateway.getOutgoingFlows().get(0); - } - } + SequenceFlow matchSequenceFlow = findMatchSequenceFlowByExclusiveGateway((Gateway) currentElement, variables); // 遍历满足条件的 SequenceFlow 路径 if (matchSequenceFlow != null) { simulateNextFlowElements(matchSequenceFlow.getTargetFlowElement(), variables, resultElements, visitElements); } - return; } - // 情况:InclusiveGateway 包容,多个满足条件的。如果没有,就走默认的 - if (currentElement instanceof InclusiveGateway) { + else if (currentElement instanceof InclusiveGateway) { // 查找满足条件的 SequenceFlow 路径 - Gateway gateway = (Gateway) currentElement; - Collection matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(), - flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) - && evalConditionExpress(variables, flow.getConditionExpression())); - if (CollUtil.isEmpty(matchSequenceFlows)) { - matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(), - flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId())); - // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的 - if (CollUtil.isEmpty(matchSequenceFlows) && gateway.getOutgoingFlows().size() == 1) { - matchSequenceFlows = gateway.getOutgoingFlows(); - } - } + Collection matchSequenceFlows = findMatchSequenceFlowsByInclusiveGateway((Gateway) currentElement, variables); // 遍历满足条件的 SequenceFlow 路径 matchSequenceFlows.forEach( flow -> simulateNextFlowElements(flow.getTargetFlowElement(), variables, resultElements, visitElements)); } - // 情况:ParallelGateway 并行,都满足,都走 - if (currentElement instanceof ParallelGateway) { + else if (currentElement instanceof ParallelGateway) { Gateway gateway = (Gateway) currentElement; // 遍历子节点 gateway.getOutgoingFlows().forEach( nextElement -> simulateNextFlowElements(nextElement.getTargetFlowElement(), variables, resultElements, visitElements)); - return; } } + /** + * 根据当前节点,获取下一个节点 + * + * @param currentElement 当前节点 + * @param bpmnModel BPMN模型 + * @param variables 流程变量 + */ + @SuppressWarnings("PatternVariableCanBeUsed") + public static List getNextFlowNodes(FlowElement currentElement, BpmnModel bpmnModel, + Map variables){ + List nextFlowNodes = new ArrayList<>(); // 下一个执行的流程节点集合 + FlowNode currentNode = (FlowNode) currentElement; // 当前执行节点的基本属性 + List 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 variables, List 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 variables) { + // TODO 表单无可编辑字段时variables为空,流程走向会出现问题,比如流程审批过程中无需要修改的字段值, + // TODO @小北:是不是还是保证,编辑的时候,如果计算下一个节点,还是 variables 是完整体?而不是空的!!!(可以微信讨论下) + SequenceFlow matchSequenceFlow; + if (CollUtil.isNotEmpty(variables)) { + matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), + flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) + && (evalConditionExpress(variables, flow.getConditionExpression()))); + } else { + matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), + flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId())); + } + if (matchSequenceFlow == null) { + matchSequenceFlow = CollUtil.findOne(gateway.getOutgoingFlows(), + flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId())); + // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的 + if (matchSequenceFlow == null && gateway.getOutgoingFlows().size() == 1) { + matchSequenceFlow = gateway.getOutgoingFlows().get(0); + } + } + return matchSequenceFlow; + } + + /** + * 处理包容网关 + * + * @param gateway 排他网关 + * @param bpmnModel BPMN模型 + * @param variables 流程变量 + * @param nextFlowNodes 下一个执行的流程节点集合 + */ + private static void handleInclusiveGateway(Gateway gateway, BpmnModel bpmnModel, + Map variables, List nextFlowNodes) { + // 查找满足条件的 SequenceFlow 路径集合 + Collection 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 findMatchSequenceFlowsByInclusiveGateway(Gateway gateway, Map variables) { + // 查找满足条件的 SequenceFlow 路径 + Collection matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(), + flow -> ObjUtil.notEqual(gateway.getDefaultFlow(), flow.getId()) + && evalConditionExpress(variables, flow.getConditionExpression())); + if (CollUtil.isEmpty(matchSequenceFlows)) { + matchSequenceFlows = CollUtil.filterNew(gateway.getOutgoingFlows(), + flow -> ObjUtil.equal(gateway.getDefaultFlow(), flow.getId())); + // 特殊:没有默认的情况下,并且只有 1 个条件,则认为它是默认的 + if (CollUtil.isEmpty(matchSequenceFlows) && gateway.getOutgoingFlows().size() == 1) { + matchSequenceFlows = gateway.getOutgoingFlows(); + } + } + return matchSequenceFlows; + } + + + /** + * 处理并行网关 + * + * @param gateway 排他网关 + * @param bpmnModel BPMN模型 + * @param variables 流程变量 + * @param nextFlowNodes 下一个执行的流程节点集合 + */ + private static void handleParallelGateway(Gateway gateway, BpmnModel bpmnModel, + Map variables, List nextFlowNodes) { + // 并行网关,遍历所有出口路径,获取目标节点 + gateway.getOutgoingFlows().forEach(flow -> { + FlowElement targetElement = bpmnModel.getFlowElement(flow.getTargetRef()); + if (targetElement instanceof FlowNode) { + nextFlowNodes.add((FlowNode) targetElement); + } + }); + } + /** * 计算条件表达式是否为 true 满足条件 * * @param variables 流程实例 - * @param express 条件表达式 + * @param expression 条件表达式 * @return 是否满足条件 */ - public static boolean evalConditionExpress(Map variables, String express) { - if (express == null) { + public static boolean evalConditionExpress(Map variables, String expression) { + if (expression == null) { return Boolean.FALSE; } + // 如果 variables 为空,则创建一个的原因?可能 expression 的计算,不依赖于 variables + if (variables == null) { + variables = new HashMap<>(); + } + + // 执行计算 try { - Object result = FlowableUtils.getExpressionValue(variables, express); + Object result = FlowableUtils.getExpressionValue(variables, expression); return Boolean.TRUE.equals(result); } catch (FlowableException ex) { - log.error("[evalConditionExpress][条件表达式({}) 变量({}) 解析报错]", express, variables, ex); + // 为什么使用 info 日志?原因是,expression 如果从 variables 取不到值,会报错。实际这种情况下,可以忽略 + log.info("[evalConditionExpress][条件表达式({}) 变量({}) 解析报错]", expression, variables, ex); return Boolean.FALSE; } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java index a458567d81..67c24bb9f4 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/FlowableUtils.java @@ -1,5 +1,6 @@ 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.extra.spring.SpringUtil; 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.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.stream.Collectors; @@ -190,12 +194,37 @@ public class FlowableUtils { @SuppressWarnings("unchecked") public static Map> getStartUserSelectAssignees(Map processVariables) { if (processVariables == null) { - return null; + return new HashMap<>(); } return (Map>) processVariables.get( BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES); } + /** + * 获得流程实例的审批用户选择的下一个节点的审批人 Map + * + * @param processInstance 流程实例 + * @return 审批用户选择的下一个节点的审批人Map + */ + public static Map> getApproveUserSelectAssignees(ProcessInstance processInstance) { + return processInstance != null ? getApproveUserSelectAssignees(processInstance.getProcessVariables()) : null; + } + + /** + * 获得流程实例的审批用户选择的下一个节点的审批人 Map + * + * @param processVariables 流程变量 + * @return 审批用户选择的下一个节点的审批人Map Map + */ + @SuppressWarnings("unchecked") + public static Map> getApproveUserSelectAssignees(Map processVariables) { + if (processVariables == null) { + return new HashMap<>(); + } + return (Map>) processVariables.get( + BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_APPROVE_USER_SELECT_ASSIGNEES); + } + /** * 获得流程实例的摘要 * @@ -240,7 +269,7 @@ public class FlowableUtils { return formFieldsMap.entrySet().stream() .limit(3) .map(entry -> new KeyValue<>(entry.getValue().getTitle(), - processVariables.getOrDefault(entry.getValue().getField(), "").toString())) + MapUtil.getStr(processVariables, entry.getValue().getField(), ""))) .collect(Collectors.toList()); } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java index 309c11cf83..3b5bad52c7 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/framework/flowable/core/util/SimpleModelUtils.java @@ -5,25 +5,27 @@ import cn.hutool.core.lang.Assert; import cn.hutool.core.map.MapUtil; import cn.hutool.core.util.*; import cn.iocoder.yudao.framework.common.util.collection.CollectionUtils; +import cn.iocoder.yudao.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.ConditionGroups; 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.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.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.constants.BpmnXMLConstants; import org.flowable.bpmn.model.Process; import org.flowable.bpmn.model.*; +import org.flowable.engine.delegate.ExecutionListener; import org.flowable.engine.delegate.TaskListener; -import org.springframework.util.MultiValueMap; 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.util.BpmnModelUtils.*; -import static cn.iocoder.yudao.module.bpm.service.task.listener.BpmUserTaskListener.DELEGATE_EXPRESSION; import static java.util.Arrays.asList; /** @@ -40,9 +42,10 @@ public class SimpleModelUtils { static { List 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 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)); } @@ -78,7 +81,7 @@ public class SimpleModelUtils { traverseNodeToBuildFlowNode(startNode, process); // 3. 构建并添加节点之间的连线 Sequence Flow - EndEvent endEvent = BpmnModelUtils.getEndEvent(bpmnModel); + EndEvent endEvent = getEndEvent(bpmnModel); traverseNodeToBuildSequenceFlow(process, startNode, endEvent.getId()); // 4. 自动布局 @@ -164,8 +167,16 @@ public class SimpleModelUtils { // 情况一:有“子”节点,则建立连线 // 情况二:没有“子节点”,则直接跟 targetNodeId 建立连线。例如说,结束节点、条件分支(分支节点的孩子节点或聚合节点)的最后一个节点 String finalTargetNodeId = isChildNodeValid ? childNode.getId() : targetNodeId; - SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), finalTargetNodeId); - process.addFlowElement(sequenceFlow); + + // 如果没有附加节点:则直接建立连线 + if (StrUtil.isEmpty(node.getAttachNodeId())) { + SequenceFlow sequenceFlow = buildBpmnSequenceFlow(node.getId(), finalTargetNodeId); + process.addFlowElement(sequenceFlow); + } else { + // 如果有附加节点:需要先建立和附加节点的连线,再建立附加节点和目标节点的连线。例如说,触发器节点(HTTP 回调) + List sequenceFlows = buildAttachNodeSequenceFlow(node.getId(), node.getAttachNodeId(), finalTargetNodeId); + sequenceFlows.forEach(process::addFlowElement); + } // 因为有子节点,递归调用后续子节点 if (isChildNodeValid) { @@ -173,6 +184,19 @@ public class SimpleModelUtils { } } + /** + * 构建有附加节点的连线 + * + * @param nodeId 当前节点 ID + * @param attachNodeId 附属节点 ID + * @param targetNodeId 目标节点 ID + */ + private static List 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 元素 * @@ -337,7 +361,7 @@ public class SimpleModelUtils { 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); // 添加表单字段权限属性元素 @@ -388,24 +412,17 @@ public class SimpleModelUtils { */ private BoundaryEvent buildUserTaskTimeoutBoundaryEvent(UserTask userTask, BpmSimpleModelNodeVO.TimeoutHandler timeoutHandler) { - // 1.1 定时器边界事件 - BoundaryEvent boundaryEvent = new BoundaryEvent(); - boundaryEvent.setId("Event-" + IdUtil.fastUUID()); - boundaryEvent.setCancelActivity(false); // 设置关联的任务为不会被中断 - boundaryEvent.setAttachedToRef(userTask); - // 1.2 定义超时时间、最大提醒次数 - TimerEventDefinition eventDefinition = new TimerEventDefinition(); - eventDefinition.setTimeDuration(timeoutHandler.getTimeDuration()); + // 1. 创建 Timeout Boundary Event + String timeCycle = null; if (Objects.equals(BpmUserTaskTimeoutHandlerTypeEnum.REMINDER.getType(), timeoutHandler.getType()) && timeoutHandler.getMaxRemindCount() != null && timeoutHandler.getMaxRemindCount() > 1) { - eventDefinition.setTimeCycle(String.format("R%d/%s", - timeoutHandler.getMaxRemindCount(), timeoutHandler.getTimeDuration())); + timeCycle = String.format("R%d/%s", + timeoutHandler.getMaxRemindCount(), timeoutHandler.getTimeDuration()); } - boundaryEvent.addEventDefinition(eventDefinition); + BoundaryEvent boundaryEvent = buildTimeoutBoundaryEvent(userTask, BpmBoundaryEventTypeEnum.USER_TASK_TIMEOUT.getType(), + timeoutHandler.getTimeDuration(), timeCycle, null); - // 2.1 添加定时器边界事件类型 - addExtensionElement(boundaryEvent, BOUNDARY_EVENT_TYPE, BpmBoundaryEventTypeEnum.USER_TASK_TIMEOUT.getType()); - // 2.2 添加超时执行动作元素 + // 2 添加超时执行动作元素 addExtensionElement(boundaryEvent, USER_TASK_TIMEOUT_HANDLER_TYPE, timeoutHandler.getType()); return boundaryEvent; } @@ -445,6 +462,8 @@ public class SimpleModelUtils { addSignEnable(node.getSignEnable(), userTask); // 审批意见 addReasonRequire(node.getReasonRequire(), userTask); + // 节点类型 + addNodeType(node.getType(), userTask); return userTask; } @@ -455,7 +474,7 @@ public class SimpleModelUtils { FlowableListener flowableListener = new FlowableListener(); flowableListener.setEvent(TaskListener.EVENTNAME_CREATE); flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); - flowableListener.setImplementation(DELEGATE_EXPRESSION); + flowableListener.setImplementation(BpmUserTaskListener.DELEGATE_EXPRESSION); addListenerConfig(flowableListener, node.getTaskCreateListener()); flowableListeners.add(flowableListener); } @@ -464,7 +483,7 @@ public class SimpleModelUtils { FlowableListener flowableListener = new FlowableListener(); flowableListener.setEvent(TaskListener.EVENTNAME_ASSIGNMENT); flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); - flowableListener.setImplementation(DELEGATE_EXPRESSION); + flowableListener.setImplementation(BpmUserTaskListener.DELEGATE_EXPRESSION); addListenerConfig(flowableListener, node.getTaskAssignListener()); flowableListeners.add(flowableListener); } @@ -473,7 +492,7 @@ public class SimpleModelUtils { FlowableListener flowableListener = new FlowableListener(); flowableListener.setEvent(TaskListener.EVENTNAME_COMPLETE); flowableListener.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); - flowableListener.setImplementation(DELEGATE_EXPRESSION); + flowableListener.setImplementation(BpmUserTaskListener.DELEGATE_EXPRESSION); addListenerConfig(flowableListener, node.getTaskCompleteListener()); flowableListeners.add(flowableListener); } @@ -486,7 +505,7 @@ public class SimpleModelUtils { BpmUserTaskApproveMethodEnum approveMethodEnum = BpmUserTaskApproveMethodEnum.valueOf(approveMethod); Assert.notNull(approveMethodEnum, "审批方式({})不能为空", approveMethodEnum); // 添加审批方式的扩展属性 - addExtensionElement(userTask, BpmnModelConstants.USER_TASK_APPROVE_METHOD, approveMethod); + addExtensionElement(userTask, USER_TASK_APPROVE_METHOD, approveMethod); if (approveMethodEnum == BpmUserTaskApproveMethodEnum.RANDOM) { // 随机审批,不需要设置多实例属性 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 { @Override @@ -684,20 +712,16 @@ public class SimpleModelUtils { // 2. 添加接收任务的 Timer Boundary Event if (node.getDelaySetting() != null) { - // 2.1 定时器边界事件 - BoundaryEvent boundaryEvent = new BoundaryEvent(); - boundaryEvent.setId("Event-" + IdUtil.fastUUID()); - boundaryEvent.setCancelActivity(false); - boundaryEvent.setAttachedToRef(receiveTask); - // 2.2 定义超时时间 - TimerEventDefinition eventDefinition = new TimerEventDefinition(); + BoundaryEvent boundaryEvent = null; 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())) { - 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); } return flowElements; @@ -712,23 +736,36 @@ public class SimpleModelUtils { public static class TriggerNodeConvert implements NodeConvert { @Override - public ServiceTask convert(BpmSimpleModelNodeVO node) { + public List convertList(BpmSimpleModelNodeVO node) { + Assert.notNull(node.getTriggerSetting(), "触发器节点设置不能为空"); + List 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 = new ServiceTask(); serviceTask.setId(node.getId()); serviceTask.setName(node.getName()); serviceTask.setImplementationType(ImplementationType.IMPLEMENTATION_TYPE_DELEGATEEXPRESSION); serviceTask.setImplementation("${" + BpmTriggerTaskDelegate.BEAN_NAME + "}"); - if (node.getTriggerSetting() != null) { - addExtensionElement(serviceTask, TRIGGER_TYPE, node.getTriggerSetting().getType()); - if (node.getTriggerSetting().getHttpRequestSetting() != null) { - addExtensionElementJson(serviceTask, TRIGGER_PARAM, node.getTriggerSetting().getHttpRequestSetting()); - } - if (node.getTriggerSetting().getNormalFormSetting() != null) { - addExtensionElementJson(serviceTask, TRIGGER_PARAM, node.getTriggerSetting().getNormalFormSetting()); - } + addExtensionElement(serviceTask, TRIGGER_TYPE, node.getTriggerSetting().getType()); + if (node.getTriggerSetting().getHttpRequestSetting() != null) { + addExtensionElementJson(serviceTask, TRIGGER_PARAM, node.getTriggerSetting().getHttpRequestSetting()); } - return serviceTask; + if (node.getTriggerSetting().getFormSettings() != null) { + addExtensionElementJson(serviceTask, TRIGGER_PARAM, node.getTriggerSetting().getFormSettings()); + } + flowElements.add(serviceTask); + return flowElements; } @Override @@ -762,10 +799,131 @@ public class SimpleModelUtils { } + private static class ChildProcessConvert implements NodeConvert { + + @Override + public List convertList(BpmSimpleModelNodeVO node) { + List flowElements = new ArrayList<>(2); + BpmSimpleModelNodeVO.ChildProcessSetting childProcessSetting = node.getChildProcessSetting(); + List 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 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) { 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 流程预测相关的方法 ========== public static List simulateProcess(BpmSimpleModelNodeVO rootNode, Map variables) { @@ -785,11 +943,13 @@ public class SimpleModelUtils { BpmSimpleModelNodeTypeEnum nodeType = BpmSimpleModelNodeTypeEnum.valueOf(currentNode.getType()); 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 || nodeType == BpmSimpleModelNodeTypeEnum.START_USER_NODE || nodeType == BpmSimpleModelNodeTypeEnum.APPROVE_NODE + || nodeType == BpmSimpleModelNodeTypeEnum.TRANSACTOR_NODE || nodeType == BpmSimpleModelNodeTypeEnum.COPY_NODE + || nodeType == BpmSimpleModelNodeTypeEnum.CHILD_PROCESS || nodeType == BpmSimpleModelNodeTypeEnum.END_NODE) { // 添加元素 resultNodes.add(currentNode); @@ -799,8 +959,8 @@ public class SimpleModelUtils { if (nodeType == BpmSimpleModelNodeTypeEnum.CONDITION_BRANCH_NODE) { // 查找满足条件的 BpmSimpleModelNodeVO 节点 BpmSimpleModelNodeVO matchConditionNode = CollUtil.findOne(currentNode.getConditionNodes(), - conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow()) - && evalConditionExpress(variables, conditionNode.getConditionSetting())); + conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow()) + && evalConditionExpress(variables, conditionNode.getConditionSetting())); if (matchConditionNode == null) { matchConditionNode = CollUtil.findOne(currentNode.getConditionNodes(), conditionNode -> BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow())); @@ -814,8 +974,8 @@ public class SimpleModelUtils { if (nodeType == BpmSimpleModelNodeTypeEnum.INCLUSIVE_BRANCH_NODE) { // 查找满足条件的 BpmSimpleModelNodeVO 节点 Collection matchConditionNodes = CollUtil.filterNew(currentNode.getConditionNodes(), - conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow()) - && evalConditionExpress(variables, conditionNode.getConditionSetting())); + conditionNode -> !BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow()) + && evalConditionExpress(variables, conditionNode.getConditionSetting())); if (CollUtil.isEmpty(matchConditionNodes)) { matchConditionNodes = CollUtil.filterNew(currentNode.getConditionNodes(), conditionNode -> BooleanUtil.isTrue(conditionNode.getConditionSetting().getDefaultFlow())); @@ -841,27 +1001,4 @@ public class SimpleModelUtils { return BpmnModelUtils.evalConditionExpress(variables, buildConditionExpression(conditionSetting)); } - // TODO @芋艿:【高】要不要优化下,抽个 HttpUtils - - /** - * 添加 HTTP 请求参数。请求头或者请求体 - * - * @param params HTTP 请求参数 - * @param paramSettings HTTP 请求参数设置 - * @param processVariables 流程变量 - */ - public static void addHttpRequestParam(MultiValueMap params, - List paramSettings, - Map 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()); - } - }); - } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java index 9ccc2f2c92..6f3162683f 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmModelServiceImpl.java @@ -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.task.BpmReasonEnum; 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.FlowableUtils; 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.validation.Valid; import lombok.extern.slf4j.Slf4j; -import org.flowable.bpmn.model.BpmnModel; -import org.flowable.bpmn.model.StartEvent; -import org.flowable.bpmn.model.UserTask; +import org.flowable.bpmn.model.*; import org.flowable.common.engine.impl.db.SuspensionState; import org.flowable.engine.HistoryService; import org.flowable.engine.RepositoryService; @@ -41,13 +40,12 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; 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.module.bpm.enums.ErrorCodeConstants.*; +import static cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils.parseCandidateStrategy; /** * 流程模型实现:主要进行 Flowable {@link Model} 的维护 @@ -209,11 +207,11 @@ public class BpmModelServiceImpl implements BpmModelService { public void deployModel(Long userId, String id) { // 1.1 校验流程模型存在 Model model = validateModelManager(id, userId); + BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model); // 1.2 校验流程图 byte[] bpmnBytes = getModelBpmnXML(model.getId()); - validateBpmnXml(bpmnBytes); + validateBpmnXml(bpmnBytes, metaInfo.getType()); // 1.3 校验表单已配 - BpmModelMetaInfoVO metaInfo = BpmModelConvert.INSTANCE.parseMetaInfo(model); BpmFormDO form = validateFormConfig(metaInfo); // 1.4 校验任务分配规则已配置 taskCandidateInvoker.validateBpmnConfig(bpmnBytes); @@ -233,7 +231,7 @@ public class BpmModelServiceImpl implements BpmModelService { repositoryService.saveModel(model); } - private void validateBpmnXml(byte[] bpmnBytes) { + private void validateBpmnXml(byte[] bpmnBytes, Integer type) { BpmnModel bpmnModel = BpmnModelUtils.getBpmnModel(bpmnBytes); if (bpmnModel == null) { 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()); } }); + // TODO @小北:是不是可以 UserTask firUserTask = CollUtil.get(userTasks, BpmModelTypeEnum.BPMN.getType().equals(type) ? 0 : 1);然后,最好判空。。。极端情况下,没 usertask ,哈哈哈哈。 + // 3. 校验第一个用户任务节点的规则类型是否为“审批人自选” + Map 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 diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java index c6a178c6c8..14ee084063 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/definition/BpmProcessDefinitionServiceImpl.java @@ -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.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.PROCESS_DEFINITION_NAME_NOT_MATCH; +import static cn.iocoder.yudao.module.bpm.enums.ErrorCodeConstants.*; import static java.util.Collections.emptyList; /** @@ -144,9 +143,8 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ // 插入拓展表 BpmProcessDefinitionInfoDO definitionDO = BeanUtils.toBean(modelMetaInfo, BpmProcessDefinitionInfoDO.class) - .setModelId(model.getId()).setProcessDefinitionId(definition.getId()) + .setModelId(model.getId()).setCategory(model.getCategory()).setProcessDefinitionId(definition.getId()) .setModelType(modelMetaInfo.getType()).setSimpleModel(simpleJson); - if (form != null) { definitionDO.setFormFields(form.getFields()).setFormConf(form.getConf()); } @@ -156,16 +154,25 @@ public class BpmProcessDefinitionServiceImpl implements BpmProcessDefinitionServ @Override 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)) { - repositoryService.activateProcessDefinitionById(id, false, null); + if (processDefinition.isSuspended()) { + repositoryService.activateProcessDefinitionById(id, false, null); + } return; } // 挂起 if (Objects.equals(SuspensionState.SUSPENDED.getStateCode(), state)) { // suspendProcessInstances = false,进行中的任务,不进行挂起。 // 原因:只要新的流程不允许发起即可,老流程继续可以执行。 - repositoryService.suspendProcessDefinitionById(id, false, null); + if (!processDefinition.isSuspended()) { + repositoryService.suspendProcessDefinitionById(id, false, null); + } return; } log.error("[updateProcessDefinitionState][流程定义({}) 修改未知状态({})]", id, state); diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java index 55b86a5514..abba2245e2 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceService.java @@ -7,6 +7,7 @@ import jakarta.validation.Valid; import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.runtime.ProcessInstance; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; @@ -84,7 +85,6 @@ public interface BpmProcessInstanceService { PageResult getProcessInstancePage(Long userId, @Valid BpmProcessInstancePageReqVO pageReqVO); - // TODO @芋艿:重点在 review 下 /** * 获取审批详情。 *

@@ -96,6 +96,15 @@ public interface BpmProcessInstanceService { */ BpmApprovalDetailRespVO getApprovalDetail(Long loginUserId, @Valid BpmApprovalDetailReqVO reqVO); + /** + * 获取下一个执行节点信息 + * + * @param loginUserId 登录人的用户编号 + * @param reqVO 请求信息 + * @return 下一个执行节点信息 + */ + List getNextApprovalNodes(Long loginUserId, @Valid BpmApprovalDetailReqVO reqVO); + /** * 获取流程实例的 BPMN 模型视图 * @@ -148,6 +157,22 @@ public interface BpmProcessInstanceService { */ void updateProcessInstanceReject(ProcessInstance processInstance, String reason); + /** + * 更新 ProcessInstance 的变量 + * + * @param id 流程编号 + * @param variables 流程变量 + */ + void updateProcessInstanceVariables(String id, Map variables); + + /** + * 删除 ProcessInstance 的变量 + * + * @param id 流程编号 + * @param variableNames 流程变量名 + */ + void removeProcessInstanceVariables(String id, Collection variableNames); + // ========== Event 事件相关方法 ========== /** @@ -158,11 +183,9 @@ public interface BpmProcessInstanceService { void processProcessInstanceCompleted(ProcessInstance instance); /** - * 更新 ProcessInstance 的变量 + * 处理 ProcessInstance 开始事件,例如说:流程前置通知 * - * @param id 流程编号 - * @param variables 流程变量 + * @param instance 流程任务 */ - void updateProcessInstanceVariables(String id, Map variables); - + void processProcessInstanceCreated(ProcessInstance instance); } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java index ca50baef80..1bd568c8e5 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmProcessInstanceServiceImpl.java @@ -5,6 +5,7 @@ import cn.hutool.core.collection.ListUtil; import cn.hutool.core.date.DateUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.StrUtil; 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.PageUtils; 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.simple.BpmSimpleModelNodeVO; 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.BpmTaskStatusEnum; 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.BpmnVariableConstants; 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.FlowableUtils; 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.runtime.ProcessInstance; import org.flowable.engine.runtime.ProcessInstanceBuilder; +import org.flowable.task.api.Task; import org.flowable.task.api.history.HistoricTaskInstance; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; +import org.springframework.web.client.RestTemplate; 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.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.util.BpmnModelUtils.parseNodeType; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.flowable.bpmn.constants.BpmnXMLConstants.*; @@ -116,6 +122,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService @Resource private BpmProcessIdRedisDAO processIdRedisDAO; + @Resource + private RestTemplate restTemplate; + // ========== Query 查询相关方法 ========== @Override @@ -144,7 +153,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService } private Map getFormFieldsPermission(BpmnModel bpmnModel, - String activityId, String taskId) { + String activityId, String taskId) { // 1. 获取流程活动编号。流程活动 Id 为空事,从流程任务中获取流程活动 Id if (StrUtil.isEmpty(activityId) && StrUtil.isNotEmpty(taskId)) { activityId = Optional.ofNullable(taskService.getHistoricTask(taskId)) @@ -164,7 +173,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService Long startUserId = loginUserId; // 流程发起人 HistoricProcessInstance historicProcessInstance = null; // 流程实例 Integer processInstanceStatus = BpmProcessInstanceStatusEnum.NOT_START.getStatus(); // 流程状态 - Map processVariables = reqVO.getProcessVariables(); // 流程变量 + Map processVariables = new HashMap<>(); // 流程变量 // 1.2 如果是流程已发起的场景,则使用流程实例的数据 if (reqVO.getProcessInstanceId() != null) { historicProcessInstance = getHistoricProcessInstance(reqVO.getProcessInstanceId()); @@ -173,7 +182,13 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService } startUserId = Long.valueOf(historicProcessInstance.getStartUserId()); 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 读取其它相关数据 ProcessDefinition processDefinition = processDefinitionService.getProcessDefinition( @@ -205,20 +220,78 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService } // 3.1 计算当前登录用户的待办任务 - // TODO @jason:有一个极端情况,如果一个用户有 2 个 task A 和 B,A 已经通过,B 需要审核。这个时,通过 A 进来,todo 拿到 - // B,会不会表单权限不一致哈。 - BpmTaskRespVO todoTask = taskService.getFirstTodoTask(loginUserId, reqVO.getProcessInstanceId()); - + BpmTaskRespVO todoTask = taskService.getTodoTask(loginUserId, reqVO.getTaskId(), reqVO.getProcessInstanceId()); // 3.2 预测未运行节点的审批信息 List simulateActivityNodes = getSimulateApproveNodeList(startUserId, bpmnModel, processDefinitionInfo, 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. 拼接最终数据 return buildApprovalDetail(reqVO, bpmnModel, processDefinition, processDefinitionInfo, historicProcessInstance, processInstanceStatus, endActivityNodes, runActivityNodes, simulateActivityNodes, todoTask); } + @Override + public List 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 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 nextFlowNodes = BpmnModelUtils.getNextFlowNodes(flowElement, bpmnModel, processVariables); + return convertList(nextFlowNodes, node -> { + List candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(), + loginUserId, historicProcessInstance.getProcessDefinitionId(), processVariables); + // 3.2 获取节点的审批人信息 + Map userMap = adminUserApi.getUserMap(candidateUserIds); + // 3.3 获取节点的审批人部门信息 + Map deptMap = deptApi.getDeptMap(convertSet(userMap.values(), AdminUserRespDTO::getDeptId)); + // 3.4 存在一个节点多人审批的情况,组装审批人信息 + List 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 @SuppressWarnings("unchecked") public PageResult getProcessInstancePage(Long userId, @@ -283,15 +356,15 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService * 主要是,拼接审批人的用户信息、部门信息 */ private BpmApprovalDetailRespVO buildApprovalDetail(BpmApprovalDetailReqVO reqVO, - BpmnModel bpmnModel, - ProcessDefinition processDefinition, - BpmProcessDefinitionInfoDO processDefinitionInfo, - HistoricProcessInstance processInstance, - Integer processInstanceStatus, - List endApprovalNodeInfos, - List runningApprovalNodeInfos, - List simulateApprovalNodeInfos, - BpmTaskRespVO todoTask) { + BpmnModel bpmnModel, + ProcessDefinition processDefinition, + BpmProcessDefinitionInfoDO processDefinitionInfo, + HistoricProcessInstance processInstance, + Integer processInstanceStatus, + List endApprovalNodeInfos, + List runningApprovalNodeInfos, + List simulateApprovalNodeInfos, + BpmTaskRespVO todoTask) { // 1. 获取所有需要读取用户信息的 userIds List approveNodes = newArrayList( asList(endApprovalNodeInfos, runningApprovalNodeInfos, simulateApprovalNodeInfos)); @@ -313,19 +386,21 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService * 获得【已结束】的活动节点们 */ private List getEndActivityNodeList(Long startUserId, BpmnModel bpmnModel, - BpmProcessDefinitionInfoDO processDefinitionInfo, - HistoricProcessInstance historicProcessInstance, Integer processInstanceStatus, - List activities, List tasks) { + BpmProcessDefinitionInfoDO processDefinitionInfo, + HistoricProcessInstance historicProcessInstance, Integer processInstanceStatus, + List activities, List tasks) { // 遍历 tasks 列表,只处理已结束的 UserTask - // 为什么不通过 activities 呢?因为,加签场景下,它只存在于 tasks,没有 activities,导致如果遍历 activities - // 的话,它无法成为一个节点 + // 为什么不通过 activities 呢?因为,加签场景下,它只存在于 tasks,没有 activities,导致如果遍历 activities 的话,它无法成为一个节点 + // TODO @芋艿:子流程只有activity,这里获取不到已结束的子流程; + // TODO @lesan:【子流程】基于 activities 查询出 usertask、callactivity,然后拼接?如果是子流程,就是可以点击过去? List endTasks = filterList(tasks, task -> task.getEndTime() != null); List approvalNodes = convertList(endTasks, task -> { FlowElement flowNode = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); ActivityNode activityNode = new ActivityNode().setId(task.getTaskDefinitionKey()).setName(task.getName()) .setNodeType(START_USER_NODE_ID.equals(task.getTaskDefinitionKey()) ? BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType() - : BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()) + : ObjUtil.defaultIfNull(parseNodeType(flowNode), // 目的:解决“办理节点”的识别 + BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())) .setStatus(FlowableUtils.getTaskStatus(task)) .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode)) .setStartTime(DateUtils.of(task.getCreateTime())).setEndTime(DateUtils.of(task.getEndTime())) @@ -381,18 +456,19 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService * 获得【进行中】的活动节点们 */ private List getRunApproveNodeList(Long startUserId, - BpmnModel bpmnModel, - ProcessDefinition processDefinition, - Map processVariables, - List activities, - List tasks) { - // 构建运行中的任务,基于 activityId 分组 + BpmnModel bpmnModel, + ProcessDefinition processDefinition, + Map processVariables, + List activities, + List tasks) { + // 构建运行中的任务、子流程,基于 activityId 分组 List 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> runningTaskMap = convertMultiMap(runActivities, HistoricActivityInstance::getActivityId); // 按照 activityId 分组,构建 ApprovalNodeInfo 节点 + // TODO @lesan:【子流程】在子流程进行审批的时候,HistoricActivityInstance 里面可以拿到 runActivities.get(0).getCalledProcessInstanceId()。要不要支持跳转??? Map taskMap = convertMap(tasks, HistoricTaskInstance::getId); return convertList(runningTaskMap.entrySet(), entry -> { String activityId = entry.getKey(); @@ -402,7 +478,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService HistoricActivityInstance firstActivity = CollUtil.getFirst(taskActivities); // 取第一个任务,会签/或签的任务,开始时间相同 ActivityNode activityNode = new ActivityNode().setId(firstActivity.getActivityId()) .setName(firstActivity.getActivityName()) - .setNodeType(BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType()) + .setNodeType(ObjUtil.defaultIfNull(parseNodeType(flowNode), // 目的:解决“办理节点”和"子流程"的识别 + BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())) .setStatus(BpmTaskStatusEnum.RUNNING.getStatus()) .setCandidateStrategy(BpmnModelUtils.parseCandidateStrategy(flowNode)) .setStartTime(DateUtils.of(CollUtil.getFirst(taskActivities).getStartTime())) @@ -410,6 +487,11 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService // 处理每个任务的 tasks 属性 for (HistoricActivityInstance activity : taskActivities) { HistoricTaskInstance task = taskMap.get(activity.getTaskId()); + // 特殊情况:子流程节点 ChildProcess 仅存在于 activity 中,并且没有自身的 task,需要跳过执行 + // TODO @芋艿:后续看看怎么优化! + if (task == null) { + continue; + } activityNode.getTasks().add(BpmProcessInstanceConvert.INSTANCE.buildApprovalTaskInfo(task)); // 加签子任务,需要过滤掉已经完成的加签子任务 List childrenTasks = filterList( @@ -440,9 +522,9 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService * 获得【预测(未来)】的活动节点们 */ private List getSimulateApproveNodeList(Long startUserId, BpmnModel bpmnModel, - BpmProcessDefinitionInfoDO processDefinitionInfo, - Map processVariables, - List activities) { + BpmProcessDefinitionInfoDO processDefinitionInfo, + Map processVariables, + List activities) { // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance // 包括了历史的操作,不是只有 startEvent 到当前节点的记录 Set runActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId); @@ -464,8 +546,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService } private ActivityNode buildNotRunApproveNodeForSimple(Long startUserId, BpmnModel bpmnModel, - BpmProcessDefinitionInfoDO processDefinitionInfo, Map processVariables, - BpmSimpleModelNodeVO node, Set runActivityIds) { + BpmProcessDefinitionInfoDO processDefinitionInfo, Map processVariables, + BpmSimpleModelNodeVO node, Set runActivityIds) { // TODO @芋艿:【可优化】在驳回场景下,未来的预测准确性不高。原因是,驳回后,HistoricActivityInstance // 包括了历史的操作,不是只有 startEvent 到当前节点的记录 if (runActivityIds.contains(node.getId())) { @@ -479,7 +561,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService // 1. 开始节点/审批节点 if (ObjectUtils.equalsAny(node.getType(), BpmSimpleModelNodeTypeEnum.START_USER_NODE.getType(), - BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType())) { + BpmSimpleModelNodeTypeEnum.APPROVE_NODE.getType(), + BpmSimpleModelNodeTypeEnum.TRANSACTOR_NODE.getType())) { List candidateUserIds = getTaskCandidateUserList(bpmnModel, node.getId(), startUserId, processDefinitionInfo.getProcessDefinitionId(), processVariables); activityNode.setCandidateUserIds(candidateUserIds); @@ -494,14 +577,22 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService // 3. 抄送节点 if (CollUtil.isEmpty(runActivityIds) && // 流程发起时:需要展示抄送节点,用于选择抄送人 BpmSimpleModelNodeTypeEnum.COPY_NODE.getType().equals(node.getType())) { + List 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 null; } private ActivityNode buildNotRunApproveNodeForBpmn(Long startUserId, BpmnModel bpmnModel, - BpmProcessDefinitionInfoDO processDefinitionInfo, Map processVariables, - FlowElement node, Set runActivityIds) { + BpmProcessDefinitionInfoDO processDefinitionInfo, Map processVariables, + FlowElement node, Set runActivityIds) { if (runActivityIds.contains(node.getId())) { return null; } @@ -532,7 +623,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService } private List getTaskCandidateUserList(BpmnModel bpmnModel, String activityId, - Long startUserId, String processDefinitionId, Map processVariables) { + Long startUserId, String processDefinitionId, Map processVariables) { Set userIds = taskCandidateInvoker.calculateUsersByActivity(bpmnModel, activityId, startUserId, processDefinitionId, processVariables); return new ArrayList<>(userIds); @@ -568,11 +659,11 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService Set finishedTaskActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId, activityInstance -> activityInstance.getEndTime() != null && ObjectUtil.notEqual(activityInstance.getActivityType(), - BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW)); + BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW)); Set finishedSequenceFlowActivityIds = convertSet(activities, HistoricActivityInstance::getActivityId, activityInstance -> activityInstance.getEndTime() != null && ObjectUtil.equals(activityInstance.getActivityType(), - BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW)); + BpmnXMLConstants.ELEMENT_SEQUENCE_FLOW)); // 特殊:会签情况下,会有部分已完成(审批)、部分未完成(待审批),此时需要 finishedTaskActivityIds 移除掉 finishedTaskActivityIds.removeAll(unfinishedTaskActivityIds); // 特殊:如果流程实例被拒绝,则需要计算是哪个活动节点。 @@ -624,8 +715,8 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService } private String createProcessInstance0(Long userId, ProcessDefinition definition, - Map variables, String businessKey, - Map> startUserSelectAssignees) { + Map variables, String businessKey, + Map> startUserSelectAssignees) { // 1.1 校验流程定义 if (definition == null) { throw exception(PROCESS_DEFINITION_NOT_EXISTS); @@ -643,7 +734,7 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService throw exception(PROCESS_INSTANCE_START_USER_CAN_START); } // 1.3 校验发起人自选审批人 - validateStartUserSelectAssignees(definition, startUserSelectAssignees); + validateStartUserSelectAssignees(userId, definition, startUserSelectAssignees, variables); // 2. 创建流程实例 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_STATUS, // 流程实例状态:审批中 BpmProcessInstanceStatusEnum.RUNNING.getStatus()); - variables.put(BpmnVariableConstants.PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED, true); // 跳过表达式需要添加此变量为 - // true,不影响没配置 - // skipExpression 的节点 + variables.put(BpmnVariableConstants.PROCESS_INSTANCE_SKIP_EXPRESSION_ENABLED, true); // 跳过表达式需要添加此变量为 true,不影响没配置 skipExpression 的节点 if (CollUtil.isNotEmpty(startUserSelectAssignees)) { + // 设置流程变量,发起人自选审批人 variables.put(BpmnVariableConstants.PROCESS_INSTANCE_VARIABLE_START_USER_SELECT_ASSIGNEES, startUserSelectAssignees); } @@ -688,17 +778,23 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService return instance.getId(); } - private void validateStartUserSelectAssignees(ProcessDefinition definition, - Map> startUserSelectAssignees) { - // 1. 获得发起人自选审批人的 UserTask/ServiceTask 列表 - BpmnModel bpmnModel = processDefinitionService.getProcessDefinitionBpmnModel(definition.getId()); - List tasks = BpmTaskCandidateStartUserSelectStrategy.getStartUserSelectTaskList(bpmnModel); - if (CollUtil.isEmpty(tasks)) { + private void validateStartUserSelectAssignees(Long userId, ProcessDefinition definition, + Map> startUserSelectAssignees, + Map variables) { + // 1. 获取预测的节点信息 + BpmApprovalDetailRespVO detailRespVO = getApprovalDetail(userId, new BpmApprovalDetailReqVO() + .setProcessDefinitionId(definition.getId()) + .setProcessVariables(variables)); + List activityNodes = detailRespVO.getActivityNodes(); + if (CollUtil.isEmpty(activityNodes)) { return; } - // 2. 校验发起人自选审批人的审批人和抄送人是否都配置了 - tasks.forEach(task -> { + // 2.1 移除掉不是发起人自选审批人节点 + activityNodes.removeIf(task -> + ObjectUtil.notEqual(BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy(), task.getCandidateStrategy())); + // 2.2 流程发起时要先获取当前流程的预测走向节点,发起时只校验预测的节点发起人自选审批人的审批人和抄送人是否都配置了 + activityNodes.forEach(task -> { List assignees = startUserSelectAssignees != null ? startUserSelectAssignees.get(task.getId()) : null; if (CollUtil.isEmpty(assignees)) { 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)); } + @Override + public void updateProcessInstanceVariables(String id, Map variables) { + runtimeService.setVariables(id, variables); + } + + @Override + public void removeProcessInstanceVariables(String id, Collection variableNames) { + runtimeService.removeVariables(id, variableNames); + } + // ========== Event 事件相关方法 ========== @Override @@ -802,12 +908,47 @@ public class BpmProcessInstanceServiceImpl implements BpmProcessInstanceService // 3. 发送流程实例的状态事件 processInstanceEventPublisher.sendProcessInstanceResultEvent( 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 - public void updateProcessInstanceVariables(String id, Map variables) { - runtimeService.setVariables(id, variables); + public void processProcessInstanceCreated(ProcessInstance instance) { + // 注意:需要基于 instance 设置租户编号,避免 Flowable 内部异步时,丢失租户编号 + FlowableUtils.execute(instance.getTenantId(), () -> { + // 流程前置通知 + BpmProcessDefinitionInfoDO processDefinitionInfo = processDefinitionService. + getProcessDefinitionInfo(instance.getProcessDefinitionId()); + // TODO @lesan:if 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); + } + }); } } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java index bff859b063..e99d974356 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskService.java @@ -35,13 +35,16 @@ public interface BpmTaskService { PageResult getTaskTodoPage(Long userId, BpmTaskPageReqVO pageReqVO); /** - * 获得用户在指定流程下,首个需要处理(待办)的任务 + * 获得用户(待办)的任务: + * 1. 根据 taskId 查询待办任务 + * 2. 如果任务不存在(或者已审核),获取指定流程下,首个需要处理任务 * * @param userId 用户编号 + * @param taskId 任务编号 * @param processInstanceId 流程实例编号 * @return 待办任务 */ - BpmTaskRespVO getFirstTodoTask(Long userId, String processInstanceId); + BpmTaskRespVO getTodoTask(Long userId, String taskId, String processInstanceId); /** * 获得已办的流程任务分页 @@ -89,6 +92,14 @@ public interface BpmTaskService { */ List 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); /** - * 处理 延迟器 超时事件 + * 处理 ChildProcess 子流程的审批超时事件 * * @param processInstanceId 流程示例编号 * @param taskDefineKey 任务 Key */ - void processDelayTimerTimeout(String processInstanceId, String taskDefineKey); + void processChildProcessTimeout(String processInstanceId, String taskDefineKey); + + /** + * 触发流程任务 (ReceiveTask) 的执行 + *

+ * 1. Simple 模型 HTTP 回调请求触发器节点的回调,触发流程继续执行 + * 2. Simple 模型延迟器节点,到时触发流程继续执行 + * + * @param processInstanceId 流程示例编号 + * @param taskDefineKey 任务 Key + */ + void triggerTask(String processInstanceId, String taskDefineKey); } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java index 76c7771035..f96d0cb392 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/BpmTaskServiceImpl.java @@ -1,6 +1,7 @@ package cn.iocoder.yudao.module.bpm.service.task; import cn.hutool.core.collection.CollUtil; +import cn.hutool.core.convert.Convert; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.*; 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.BpmTaskSignTypeEnum; import cn.iocoder.yudao.module.bpm.enums.task.BpmTaskStatusEnum; +import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmTaskCandidateStrategyEnum; import cn.iocoder.yudao.module.bpm.framework.flowable.core.enums.BpmnVariableConstants; import cn.iocoder.yudao.module.bpm.framework.flowable.core.util.BpmnModelUtils; 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.TaskService; import org.flowable.engine.history.HistoricActivityInstance; +import org.flowable.engine.runtime.ActivityInstance; import org.flowable.engine.runtime.Execution; import org.flowable.engine.runtime.ProcessInstance; 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.util.collection.CollectionUtils.*; 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_SKIP_START_USER_NODE; 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())) { taskQuery.taskCategory(pageVO.getCategory()); } + if (StrUtil.isNotEmpty(pageVO.getProcessDefinitionKey())) { + taskQuery.processDefinitionKey(pageVO.getProcessDefinitionKey()); + } if (ArrayUtil.isNotEmpty(pageVO.getCreateTime())) { taskQuery.taskCreatedAfter(DateUtils.of(pageVO.getCreateTime()[0])); taskQuery.taskCreatedBefore(DateUtils.of(pageVO.getCreateTime()[1])); @@ -129,7 +137,67 @@ public class BpmTaskServiceImpl implements BpmTaskService { } @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 childrenTasks = getAllChildrenTaskListByParentTaskId(todoTask.getId(), CollUtil.newArrayList(todoTask)); + + // 3. 转换返回 + BpmnModel bpmnModel = bpmProcessDefinitionService.getProcessDefinitionBpmnModel(todoTask.getProcessDefinitionId()); + Map 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) { return null; } @@ -141,37 +209,12 @@ public class BpmTaskServiceImpl implements BpmTaskService { .includeProcessVariables() .orderByTaskCreateTime().asc() // 按创建时间升序 .list(); - if (CollUtil.isEmpty(tasks)) { - return null; - } - // 2.1 查询我的首个任务 - Task todoTask = CollUtil.findOne(tasks, task -> { + // 2. 查询我的首个任务 + return CollUtil.findOne(tasks, task -> { return isAssignUserTask(userId, task) // 当前用户为审批人 || isAddSignUserTask(userId, task); // 当前用户为加签人(为了减签) }); - if (todoTask == null) { - return null; - } - // 2.2 查询该任务的子任务 - List childrenTasks = getAllChildrenTaskListByParentTaskId(todoTask.getId(), tasks); - - // 3. 转换返回 - BpmnModel bpmnModel = bpmProcessDefinitionService.getProcessDefinitionBpmnModel(todoTask.getProcessDefinitionId()); - Map 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 @@ -194,6 +237,10 @@ public class BpmTaskServiceImpl implements BpmTaskService { return PageResult.empty(); } List tasks = taskQuery.listPage(PageUtils.getStart(pageVO), pageVO.getPageSize()); + + // 特殊:强制移除自动完成的“发起人”节点 + // 补充说明:由于 taskQuery 无法方面的过滤,所以暂时通过内存过滤 + tasks.removeIf(task -> task.getTaskDefinitionKey().equals(START_USER_NODE_ID)); return new PageResult<>(tasks, count); } @@ -243,13 +290,8 @@ public class BpmTaskServiceImpl implements BpmTaskService { return query.list(); } - /** - * 校验任务是否存在,并且是否是分配给自己的任务 - * - * @param userId 用户 id - * @param taskId task id - */ - private Task validateTask(Long userId, String taskId) { + @Override + public Task validateTask(Long userId, String taskId) { Task task = validateTaskExist(taskId); // 为什么判断 assignee 非空的情况下? // 例如说:在审批人为空时,我们会有“自动审批通过”的策略,此时 userId 为 null,允许通过 @@ -515,21 +557,87 @@ public class BpmTaskServiceImpl implements BpmTaskService { // 2.2 添加评论 taskService.addComment(task.getId(), task.getProcessInstanceId(), BpmCommentTypeEnum.APPROVE.getType(), BpmCommentTypeEnum.APPROVE.formatComment(reqVO.getReason())); - // 2.3 调用 BPM complete 去完成任务 - // 其中,variables 是存储动态表单到 local 任务级别。过滤一下,避免 ProcessInstance 系统级的变量被占用 - if (CollUtil.isNotEmpty(reqVO.getVariables())) { - Map variables = FlowableUtils.filterTaskFormVariable(reqVO.getVariables()); - // 修改表单的值需要存储到 ProcessInstance 变量 - runtimeService.setVariables(task.getProcessInstanceId(), variables); - taskService.complete(task.getId(), variables, true); - } else { - taskService.complete(task.getId()); - } + // 2.3 校验并处理 APPROVE_USER_SELECT 当前审批人,选择下一节点审批人的逻辑 + Map variables = validateAndSetNextAssignees(task.getTaskDefinitionKey(), reqVO.getVariables(), + bpmnModel, reqVO.getNextAssignees(), instance); + runtimeService.setVariables(task.getProcessInstanceId(), variables); + // 2.4 调用 BPM complete 去完成任务 + taskService.complete(task.getId(), variables, true); // 【加签专属】处理加签任务 handleParentTaskIfSign(task.getParentTaskId()); } + + /** + * 校验选择的下一个节点的审批人,是否合法 + * + * 1. 是否有漏选:没有选择审批人 + * 2. 是否有多选:非下一个节点 + * + * @param taskDefinitionKey 当前任务节点标识 + * @param variables 流程变量 + * @param bpmnModel 流程模型 + * @param nextAssignees 下一个节点审批人集合(参数) + * @param processInstance 流程实例 + */ + private Map validateAndSetNextAssignees(String taskDefinitionKey, Map variables, BpmnModel bpmnModel, + Map> nextAssignees, ProcessInstance processInstance) { + // 1. 获取下一个将要执行的节点集合 + FlowElement flowElement = bpmnModel.getFlowElement(taskDefinitionKey); + List nextFlowNodes = getNextFlowNodes(flowElement, bpmnModel, variables); + + // 2. 校验选择的下一个节点的审批人,是否合法 + Map> processVariables; + for (FlowNode nextFlowNode : nextFlowNodes) { + Integer candidateStrategy = parseCandidateStrategy(nextFlowNode); + // 2.1 情况一:如果节点中的审批人策略为 发起人自选 + if (ObjUtil.equals(candidateStrategy, BpmTaskCandidateStrategyEnum.START_USER_SELECT.getStrategy())) { + // 如果节点存在,但未配置审批人 + List 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 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 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; + } + /** * 审批通过存在“后加签”的任务。 *

@@ -991,6 +1099,7 @@ public class BpmTaskServiceImpl implements BpmTaskService { @Override @Transactional(rollbackFor = Exception.class) + @SuppressWarnings("DataFlowIssue") public void deleteSignTask(Long userId, BpmTaskSignDeleteReqVO reqVO) { // 1.1 校验 task 可以被减签 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); + // 获取发起人节点 + BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId()); + if (bpmnModel == null) { + log.error("[processTaskAssigned][taskId({}) 没有找到流程模型]", task.getId()); + return; + } + 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)) { - BpmnModel bpmnModel = modelService.getBpmnModelByDefinitionId(processInstance.getProcessDefinitionId()); - if (bpmnModel == null) { - log.error("[processTaskAssigned][taskId({}) 没有找到流程模型]", task.getId()); - return; - } - FlowElement userTaskElement = BpmnModelUtils.getFlowElementById(bpmnModel, task.getTaskDefinitionKey()); Integer assignStartUserHandlerType = BpmnModelUtils.parseAssignStartUserHandlerType(userTaskElement); // 情况一:自动跳过 @@ -1304,14 +1425,23 @@ public class BpmTaskServiceImpl implements BpmTaskService { } @Override - public void processDelayTimerTimeout(String processInstanceId, String taskDefineKey) { + @Transactional(rollbackFor = Exception.class) + public void processChildProcessTimeout(String processInstanceId, String taskDefineKey) { + List 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() .processInstanceId(processInstanceId) .activityId(taskDefineKey) .singleResult(); if (execution == null) { - log.error("[processDelayTimerTimeout][processInstanceId({}) activityId({}) 没有找到执行活动]", - processInstanceId, taskDefineKey); + log.error("[triggerTask][processInstanceId({}) activityId({}) 没有找到执行活动]", processInstanceId, taskDefineKey); return; } diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmCallActivityListener.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmCallActivityListener.java new file mode 100644 index 0000000000..40313a9663 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmCallActivityListener.java @@ -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 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 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())); + } + } + } + +} \ No newline at end of file diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java index 97c103c143..a0e21559a6 100644 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/listener/BpmUserTaskListener.java @@ -1,29 +1,20 @@ 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.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 jakarta.annotation.Resource; import lombok.Setter; import lombok.extern.slf4j.Slf4j; import org.flowable.engine.delegate.TaskListener; -import org.flowable.engine.history.HistoricProcessInstance; import org.flowable.engine.impl.el.FixedValue; +import org.flowable.engine.runtime.ProcessInstance; import org.flowable.task.service.delegate.DelegateTask; 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.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.client.RestClientException; 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; // TODO @芋艿:可能会想换个包地址 @@ -51,46 +42,27 @@ public class BpmUserTaskListener implements TaskListener { @Override public void notify(DelegateTask delegateTask) { // 1. 获取所需基础信息 - HistoricProcessInstance processInstance = processInstanceService.getHistoricProcessInstance(delegateTask.getProcessInstanceId()); + ProcessInstance processInstance = processInstanceService.getProcessInstance(delegateTask.getProcessInstanceId()); BpmSimpleModelNodeVO.ListenerHandler listenerHandler = parseListenerConfig(listenerConfig); - // 2. 获取请求头和请求体 - Map processVariables = processInstance.getProcessVariables(); - MultiValueMap headers = new LinkedMultiValueMap<>(); - MultiValueMap 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 请求体默认参数 + // 2. 发起请求 // TODO @芋艿:哪些默认参数,后续再调研下;感觉可以搞个 task 字段,把整个 delegateTask 放进去; - body.add("processInstanceId", delegateTask.getProcessInstanceId()); - body.add("assignee", delegateTask.getAssignee()); - body.add("taskDefinitionKey", delegateTask.getTaskDefinitionKey()); - body.add("taskId", delegateTask.getId()); + listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("processInstanceId") + .setType(BpmHttpRequestParamTypeEnum.FIXED_VALUE.getType()).setValue(delegateTask.getProcessInstanceId())); + listenerHandler.getBody().add(new BpmSimpleModelNodeVO.HttpRequestParam().setKey("assignee") + .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. 异步发起请求 - // TODO @芋艿:确认要同步,还是异步 - HttpEntity> requestEntity = new HttpEntity<>(body, headers); - try { - ResponseEntity 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 芋艿:待定! + // 3. 是否需要后续操作?TODO 芋艿:待定! } } \ No newline at end of file diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmHttpRequestTrigger.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmHttpRequestTrigger.java deleted file mode 100644 index 52936f93f9..0000000000 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmHttpRequestTrigger.java +++ /dev/null @@ -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 processVariables = processInstance.getProcessVariables(); - MultiValueMap headers = new LinkedMultiValueMap<>(); - headers.add(HEADER_TENANT_ID, processInstance.getTenantId()); - SimpleModelUtils.addHttpRequestParam(headers, setting.getHeader(), processVariables); - // 2.2 设置请求体 - MultiValueMap body = new LinkedMultiValueMap<>(); - SimpleModelUtils.addHttpRequestParam(body, setting.getBody(), processVariables); - body.add("processInstanceId", processInstanceId); - - // TODO @芋艿:要不要抽象一个 Http 请求的工具类,方便复用呢? - // 3. 发起请求 - HttpEntity> requestEntity = new HttpEntity<>(body, headers); - ResponseEntity 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> respResult = JsonUtils.parseObjectQuietly( - responseEntity.getBody(), new TypeReference<>() {}); - if (respResult == null || !respResult.isSuccess()){ - return; - } - // 4.3 获取需要更新的流程变量 - Map updateVariables = getNeedUpdatedVariablesFromResponse(respResult.getData(), setting.getResponse()); - // 4.4 更新流程变量 - if (CollUtil.isNotEmpty(updateVariables)) { - processInstanceService.updateProcessInstanceVariables(processInstanceId, updateVariables); - } - } - - /** - * 从请求返回值获取需要更新的流程变量 - * - * @param result 请求返回结果 - * @param responseSettings 返回设置 - * @return 需要更新的流程变量 - */ - private Map getNeedUpdatedVariablesFromResponse(Map result, - List> responseSettings) { - Map 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; - } - -} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmUpdateNormalFormTrigger.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmUpdateNormalFormTrigger.java deleted file mode 100644 index deab1f5e36..0000000000 --- a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/BpmUpdateNormalFormTrigger.java +++ /dev/null @@ -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()); - } - } - -} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormDeleteTrigger.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormDeleteTrigger.java new file mode 100644 index 0000000000..c441c5f9b6 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormDeleteTrigger.java @@ -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 settings = JsonUtils.parseObject(param, new TypeReference<>() {}); + if (CollUtil.isEmpty(settings)) { + log.error("[execute][流程({}) 删除流程表单数据触发器配置为空]", processInstanceId); + return; + } + + // 2. 获取流程变量 + Map processVariables = processInstanceService.getProcessInstance(processInstanceId).getProcessVariables(); + + // 3.1 获取需要删除的表单字段 + Set 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); + } + } +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormUpdateTrigger.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormUpdateTrigger.java new file mode 100644 index 0000000000..4940d697d4 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/form/BpmFormUpdateTrigger.java @@ -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 settings = JsonUtils.parseObject(param, new TypeReference<>() {}); + if (CollUtil.isEmpty(settings)) { + log.error("[execute][流程({}) 更新流程表单触发器配置为空]", processInstanceId); + return; + } + + // 2. 获取流程变量 + Map 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()); + } + } + } +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmAbstractHttpRequestTrigger.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmAbstractHttpRequestTrigger.java new file mode 100644 index 0000000000..b1d81bc146 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmAbstractHttpRequestTrigger.java @@ -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 { + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmHttpCallbackTrigger.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmHttpCallbackTrigger.java new file mode 100644 index 0000000000..ab1f54abd4 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmHttpCallbackTrigger.java @@ -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); + } + +} diff --git a/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmSyncHttpRequestTrigger.java b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmSyncHttpRequestTrigger.java new file mode 100644 index 0000000000..6c74b066f0 --- /dev/null +++ b/yudao-module-bpm/yudao-module-bpm-biz/src/main/java/cn/iocoder/yudao/module/bpm/service/task/trigger/http/BpmSyncHttpRequestTrigger.java @@ -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); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImpl.java index 9483e8f738..4433fc2a75 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImpl.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiAccessLogServiceImpl.java @@ -1,8 +1,8 @@ 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.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.util.TenantUtils; import cn.iocoder.yudao.module.infra.api.logger.dto.ApiAccessLogCreateReqDTO; @@ -35,8 +35,8 @@ public class ApiAccessLogServiceImpl implements ApiAccessLogService { @Override public void createApiAccessLog(ApiAccessLogCreateReqDTO createDTO) { ApiAccessLogDO apiAccessLog = BeanUtils.toBean(createDTO, ApiAccessLogDO.class); - apiAccessLog.setRequestParams(StrUtil.maxLength(apiAccessLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH)); - apiAccessLog.setResultMsg(StrUtil.maxLength(apiAccessLog.getResultMsg(), RESULT_MSG_MAX_LENGTH)); + apiAccessLog.setRequestParams(StrUtils.maxLength(apiAccessLog.getRequestParams(), REQUEST_PARAMS_MAX_LENGTH)); + apiAccessLog.setResultMsg(StrUtils.maxLength(apiAccessLog.getResultMsg(), RESULT_MSG_MAX_LENGTH)); if (TenantContextHolder.getTenantId() != null) { apiAccessLogMapper.insert(apiAccessLog); } else { diff --git a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java index 747b220b5c..1fcbc8dd78 100644 --- a/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java +++ b/yudao-module-infra/yudao-module-infra-biz/src/main/java/cn/iocoder/yudao/module/infra/service/logger/ApiErrorLogServiceImpl.java @@ -1,8 +1,8 @@ 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.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.util.TenantUtils; import cn.iocoder.yudao.module.infra.api.logger.dto.ApiErrorLogCreateReqDTO; @@ -39,7 +39,7 @@ public class ApiErrorLogServiceImpl implements ApiErrorLogService { public void createApiErrorLog(ApiErrorLogCreateReqDTO createDTO) { ApiErrorLogDO apiErrorLog = BeanUtils.toBean(createDTO, ApiErrorLogDO.class) .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) { apiErrorLogMapper.insert(apiErrorLog); } else {