diff --git a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/constant/ProcessInstanceConstant.java b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/constant/ProcessInstanceConstant.java index c64c3f40c..2ab6f372c 100644 --- a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/constant/ProcessInstanceConstant.java +++ b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/constant/ProcessInstanceConstant.java @@ -50,6 +50,16 @@ public class ProcessInstanceConstant { */ public static final String USER_TASK_REFUSE_TYPE_TO_NODE = "TO_NODE"; + /** + * 拒绝处理策略:驳回到发起人 审批人拒绝后,流程跳转回发起人节点,发起人可重新编辑提交 + */ + public static final String USER_TASK_REFUSE_TYPE_TO_START = "TO_START"; + + /** + * 流程变量:驳回到发起人标识 用于标记流程实例是否处于驳回待重新提交状态 + */ + public static final String REJECT_TO_STARTER_VAR = "rejectToStarter"; + /** * 多人审批模式:会签 所有审批人都必须同意才能通过,任何一人拒绝则不通过 */ diff --git a/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/vo/ProcessInstanceRecordVo.java b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/vo/ProcessInstanceRecordVo.java new file mode 100644 index 000000000..43b39975a --- /dev/null +++ b/pigx-flow/pigx-flow-api/src/main/java/com/pig4cloud/pigx/flow/vo/ProcessInstanceRecordVo.java @@ -0,0 +1,38 @@ +package com.pig4cloud.pigx.flow.vo; + +import com.pig4cloud.pigx.flow.entity.ProcessInstanceRecord; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 流程实例记录视图对象 + *

+ * 继承自ProcessInstanceRecord实体类,用于扩展查询返回的额外信息。 + * 包含流程实例的驳回状态等运行时信息。 + *

+ * + * @author pigx + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ProcessInstanceRecordVo extends ProcessInstanceRecord { + + /** + * 是否驳回到发起人状态 + *

+ * true表示流程已被驳回到发起人,等待发起人重新编辑提交; + * false或null表示流程正常运行中 + *

+ */ + private Boolean rejectToStarter; + + /** + * 重新提交的任务ID + *

+ * 当rejectToStarter为true时,此字段保存发起人节点(root)的任务ID, + * 用于前端调用重新提交接口 + *

+ */ + private String resubmitTaskId; + +} diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/controller/ProcessTaskController.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/controller/ProcessTaskController.java index 62d5be7df..94f0116d7 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/controller/ProcessTaskController.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/controller/ProcessTaskController.java @@ -159,4 +159,23 @@ public class ProcessTaskController { return taskService.back(taskParamDto); } + /** + * 重新提交任务(驳回到发起人后) + *

+ * 当流程被驳回到发起人后,发起人可使用此接口重新编辑表单并提交。 + * 与completeTask不同: + * 1. 不需要填写审批意见 + * 2. 会自动清理驳回标识 + * 3. 提交后流程继续到下一个审批节点 + *

+ * @param taskParamDto 重新提交参数,包含: + * - taskId: 发起人节点的任务ID + * - formData: 更新后的表单数据 + * @return R 统一响应对象,操作成功返回提交结果 + */ + @PostMapping("resubmitTask") + public R resubmitTask(@RequestBody TaskParamDto taskParamDto) { + return taskService.resubmitTask(taskParamDto); + } + } diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/ITaskService.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/ITaskService.java index bff9632bd..b15de35a5 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/ITaskService.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/ITaskService.java @@ -106,4 +106,18 @@ public interface ITaskService { */ R queryTaskData(); + /** + * 重新提交任务(驳回到发起人后) + *

+ * 当流程被驳回到发起人时,发起人使用此方法重新编辑表单并提交。 + * 与普通completeTask不同: + * 1. 不需要填写审批意见 + * 2. 会清理 rejectToStarter 流程变量 + * 3. 完成后流程继续到下一个审批节点 + *

+ * @param taskParamDto 任务参数,包含taskId和更新后的formData + * @return 操作结果 + */ + R resubmitTask(TaskParamDto taskParamDto); + } diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImpl.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImpl.java index 00fdd60e8..e3b09edd1 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImpl.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/ProcessInstanceServiceImpl.java @@ -29,6 +29,7 @@ import com.pig4cloud.pigx.flow.support.utils.NodeUtil; import com.pig4cloud.pigx.flow.vo.NodeFormatParamVo; import com.pig4cloud.pigx.flow.vo.NodeVo; import com.pig4cloud.pigx.flow.vo.ProcessCopyVo; +import com.pig4cloud.pigx.flow.vo.ProcessInstanceRecordVo; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -364,7 +365,39 @@ public class ProcessInstanceServiceImpl implements IProcessInstanceService { Page instanceRecordPage = processInstanceRecordService.lambdaQuery().eq(ProcessInstanceRecord::getUserId, userId).eq(ProcessInstanceRecord::getStatus, taskQueryParamDto.getStatus()).between(ArrayUtil.isNotEmpty(taskQueryParamDto.getTaskTime()), ProcessInstanceRecord::getCreateTime, ArrayUtil.isNotEmpty(taskQueryParamDto.getTaskTime()) ? taskQueryParamDto.getTaskTime()[0] : null, ArrayUtil.isNotEmpty(taskQueryParamDto.getTaskTime()) ? taskQueryParamDto.getTaskTime()[1] : null).orderByDesc(ProcessInstanceRecord::getCreateTime).page(new Page<>(taskQueryParamDto.getCurrent(), taskQueryParamDto.getSize())); - return R.ok(instanceRecordPage); + // 转换为 VO 并添加驳回状态信息 + List voList = BeanUtil.copyToList(instanceRecordPage.getRecords(), ProcessInstanceRecordVo.class); + + for (ProcessInstanceRecordVo vo : voList) { + try { + // 从 Flowable 变量中获取 rejectToStarter 状态 + Object rejectVar = runtimeService.getVariable( + vo.getProcessInstanceId(), + "rejectToStarter" + ); + vo.setRejectToStarter(Boolean.TRUE.equals(rejectVar)); + + // 如果是驳回状态,获取 root 任务 ID 用于重新提交 + if (Boolean.TRUE.equals(vo.getRejectToStarter())) { + Task task = taskService.createTaskQuery() + .processInstanceId(vo.getProcessInstanceId()) + .taskDefinitionKey("root") + .singleResult(); + if (task != null) { + vo.setResubmitTaskId(task.getId()); + } + } + } catch (Exception e) { + // 流程可能已结束,设置为 false + vo.setRejectToStarter(false); + } + } + + // 构建返回的分页对象 + Page resultPage = new Page<>(instanceRecordPage.getCurrent(), instanceRecordPage.getSize(), instanceRecordPage.getTotal()); + resultPage.setRecords(voList); + + return R.ok(resultPage); } /** diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImpl.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImpl.java index de2f5b541..daf3163fd 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImpl.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/service/impl/TaskServiceImpl.java @@ -10,6 +10,7 @@ import com.pig4cloud.pigx.common.core.util.R; import com.pig4cloud.pigx.common.data.tenant.TenantContextHolder; import com.pig4cloud.pigx.common.security.util.SecurityUtils; import com.pig4cloud.pigx.flow.constant.NodeStatusEnum; +import com.pig4cloud.pigx.flow.constant.ProcessInstanceConstant; import com.pig4cloud.pigx.flow.dto.IndexPageStatistics; import com.pig4cloud.pigx.flow.dto.Node; import com.pig4cloud.pigx.flow.dto.TaskParamDto; @@ -483,6 +484,79 @@ public class TaskServiceImpl implements ITaskService { return flowableEngineService.back(taskParamDto); } + /** + * 重新提交任务(驳回到发起人后) + *

+ * 当流程被驳回到发起人时,发起人使用此方法重新编辑表单并提交。 + * 该方法会: + * 1. 验证任务存在且属于当前用户 + * 2. 验证任务是 root 节点任务 + * 3. 更新表单数据 + * 4. 清理 rejectToStarter 流程变量 + * 5. 完成任务,流程继续到下一个审批节点 + *

+ * + * @param taskParamDto 任务参数,包含taskId和更新后的formData + * @return R 操作结果 + */ + @Override + @SneakyThrows + @Transactional(rollbackFor = Exception.class) + public R resubmitTask(TaskParamDto taskParamDto) { + long userId = SecurityUtils.getUser().getId(); + + // 1. 获取任务并验证 + Task task = taskService.createTaskQuery() + .taskId(taskParamDto.getTaskId()) + .taskTenantId(TenantContextHolder.getTenantId().toString()) + .taskAssignee(String.valueOf(userId)) + .singleResult(); + + if (task == null) { + return R.failed("任务不存在或无权限操作"); + } + + // 2. 验证是 root 任务(发起人节点) + if (!StrUtil.equals(task.getTaskDefinitionKey(), "root")) { + return R.failed("非重新提交任务,无法操作"); + } + + String processInstanceId = task.getProcessInstanceId(); + + // 3. 更新表单数据 + if (CollUtil.isNotEmpty(taskParamDto.getFormData())) { + runtimeService.setVariables(task.getExecutionId(), taskParamDto.getFormData()); + + // 同步更新到 ProcessInstanceRecord + ProcessInstanceRecord processInstanceRecord = processInstanceRecordService.lambdaQuery() + .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) + .one(); + + if (processInstanceRecord != null) { + Map existingFormData = new HashMap<>(); + if (StrUtil.isNotBlank(processInstanceRecord.getFormData())) { + existingFormData = objectMapper.readValue(processInstanceRecord.getFormData(), + new TypeReference>() {}); + } + existingFormData.putAll(taskParamDto.getFormData()); + String updatedFormData = objectMapper.writeValueAsString(existingFormData); + + processInstanceRecordService.lambdaUpdate() + .set(ProcessInstanceRecord::getFormData, updatedFormData) + .eq(ProcessInstanceRecord::getProcessInstanceId, processInstanceId) + .update(); + } + } + + // 4. 清理 rejectToStarter 变量 + runtimeService.removeVariable(processInstanceId, "rejectToStarter"); + + // 5. 完成任务,流程继续 + taskService.complete(taskParamDto.getTaskId()); + + return R.ok("重新提交成功"); + } + /** * 递归获取所有需要停止的子流程实例ID列表 *

diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/listeners/RootTaskCreateListener.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/listeners/RootTaskCreateListener.java new file mode 100644 index 000000000..d5bc542bb --- /dev/null +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/listeners/RootTaskCreateListener.java @@ -0,0 +1,66 @@ +package com.pig4cloud.pigx.flow.support.listeners; + +import cn.hutool.extra.spring.SpringUtil; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.RuntimeService; +import org.flowable.engine.TaskService; +import org.flowable.task.service.delegate.DelegateTask; +import org.flowable.task.service.delegate.TaskListener; + +/** + * 发起人任务创建监听器 + *

+ * 用于处理发起人节点(root节点)的任务创建事件。 + * 该监听器的核心职责是区分"正常发起流程"和"驳回到发起人"两种场景: + *

    + *
  • 正常发起流程:自动完成root任务,让流程继续到第一个审批节点
  • + *
  • 驳回到发起人:保留任务,让发起人可以在待办任务中看到并处理
  • + *
+ *

+ *

+ * 判断逻辑: + * 通过检查流程变量 rejectToStarter 是否为 true 来判断是否是驳回场景。 + * 该变量在 ApproveServiceTask 处理驳回到发起人时设置。 + *

+ * + * @author pigx + * @see com.pig4cloud.pigx.flow.support.tasks.ApproveServiceTask + */ +@Slf4j +public class RootTaskCreateListener implements TaskListener { + + /** + * 任务创建事件处理方法 + *

+ * 当root任务被创建时触发。根据是否是驳回场景决定是否自动完成任务。 + *

+ * + * @param delegateTask 委派的任务对象 + */ + @Override + public void notify(DelegateTask delegateTask) { + RuntimeService runtimeService = SpringUtil.getBean(RuntimeService.class); + TaskService taskService = SpringUtil.getBean(TaskService.class); + + String processInstanceId = delegateTask.getProcessInstanceId(); + String taskId = delegateTask.getId(); + String assignee = delegateTask.getAssignee(); + + log.debug("发起人任务创建 - 任务ID: {}, 执行人: {}, 流程实例ID: {}", taskId, assignee, processInstanceId); + + // 检查是否是驳回场景 + Object rejectToStarter = runtimeService.getVariable(processInstanceId, "rejectToStarter"); + + if (!Boolean.TRUE.equals(rejectToStarter)) { + // 非驳回场景:自动完成 root 任务,让流程继续到下一个节点 + log.debug("非驳回场景,自动完成发起人任务 - 任务ID: {}", taskId); + taskService.complete(taskId); + } + else { + // 驳回场景:保留任务给发起人处理 + // 发起人可以在待办任务列表中看到这个任务,并进行编辑重新提交 + log.info("驳回场景,保留任务给发起人 - 任务ID: {}, 执行人: {}", taskId, assignee); + } + } + +} diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/node/RootTaskHandler.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/node/RootTaskHandler.java new file mode 100644 index 000000000..c2da4e999 --- /dev/null +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/node/RootTaskHandler.java @@ -0,0 +1,67 @@ +package com.pig4cloud.pigx.flow.support.node; + +import cn.hutool.extra.spring.SpringUtil; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.pig4cloud.pigx.flow.constant.ProcessInstanceConstant; +import com.pig4cloud.pigx.flow.dto.NodeUser; +import lombok.extern.slf4j.Slf4j; +import org.flowable.engine.delegate.DelegateExecution; +import org.springframework.stereotype.Component; + +import java.util.List; + +/** + * 发起人任务处理器 + *

+ * 用于处理驳回到发起人场景下,发起人任务的执行人解析。 + * 该处理器作为Spring Bean注入,在BPMN模型中通过EL表达式调用。 + *

+ *

+ * 使用方式: + * 在UserTask的assignee属性中配置:${rootTaskHandler.resolveAssignee(execution)} + *

+ * + * @author pigx + */ +@Component("rootTaskHandler") +@Slf4j +public class RootTaskHandler { + + /** + * 解析发起人作为任务执行人 + *

+ * 从流程变量中获取发起人信息(root变量),返回发起人的用户ID作为任务执行人。 + * 该方法在任务创建时由Flowable引擎通过EL表达式调用。 + *

+ * + * @param execution 流程执行上下文,包含流程变量等信息 + * @return 发起人用户ID字符串,如果未找到则返回默认空执行人标识 + */ + public String resolveAssignee(DelegateExecution execution) { + // 从流程变量中获取发起人信息 + Object rootUserObj = execution.getVariable("root"); + if (rootUserObj == null) { + log.warn("未找到发起人信息,流程实例ID: {}", execution.getProcessInstanceId()); + return ProcessInstanceConstant.DEFAULT_EMPTY_ASSIGN.toString(); + } + + ObjectMapper objectMapper = SpringUtil.getBean(ObjectMapper.class); + try { + // 发起人信息存储为List格式 + List nodeUsers = objectMapper.readValue(objectMapper.writeValueAsString(rootUserObj), + new TypeReference>() { + }); + if (!nodeUsers.isEmpty()) { + Long userId = nodeUsers.get(0).getId(); + log.debug("解析发起人成功,用户ID: {}", userId); + return userId.toString(); + } + } + catch (Exception e) { + log.error("解析发起人信息失败,流程实例ID: {}", execution.getProcessInstanceId(), e); + } + return ProcessInstanceConstant.DEFAULT_EMPTY_ASSIGN.toString(); + } + +} diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/ApproveServiceTask.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/ApproveServiceTask.java index f1d2e4ad1..7a0a22db1 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/ApproveServiceTask.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/tasks/ApproveServiceTask.java @@ -105,6 +105,11 @@ public class ApproveServiceTask implements JavaDelegate { // 根据拒绝设置重定向到特定节点 stateChangeBuilder.moveActivityIdTo(currentActivityId, refuseConfig.getNodeId()).changeState(); } + else if (StrUtil.equals(rejectionHandler, "TO_START")) { + // 驳回到发起人:设置驳回标识变量,跳转到 root 节点 + runtimeService.setVariable(processInstanceId, "rejectToStarter", true); + stateChangeBuilder.moveActivityIdTo(currentActivityId, "root").changeState(); + } else { // 默认行为:重定向到结束节点 stateChangeBuilder.moveActivityIdTo(currentActivityId, "end").changeState(); diff --git a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/ModelUtil.java b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/ModelUtil.java index a126071a2..d9fdd3d89 100644 --- a/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/ModelUtil.java +++ b/pigx-flow/pigx-flow-biz/src/main/java/com/pig4cloud/pigx/flow/support/utils/ModelUtil.java @@ -13,6 +13,7 @@ import com.pig4cloud.pigx.flow.service.IProcessNodeDataService; import com.pig4cloud.pigx.flow.support.expression.condition.NodeExpressionStrategyFactory; import com.pig4cloud.pigx.flow.support.listeners.ApprovalCreateListener; import com.pig4cloud.pigx.flow.support.listeners.FlowProcessEventListener; +import com.pig4cloud.pigx.flow.support.listeners.RootTaskCreateListener; import com.pig4cloud.pigx.flow.support.tasks.ApproveServiceTask; import com.pig4cloud.pigx.flow.support.tasks.CopyServiceTask; import lombok.extern.slf4j.Slf4j; @@ -352,22 +353,53 @@ public class ModelUtil { /** * 构建开始节点 *

- * 创建BPMN流程的开始事件节点。 - * 每个流程必须有且只有一个开始节点。 + * 创建BPMN流程的开始事件节点和发起人任务节点。 + * 流程结构:StartEvent(start) → UserTask(root) → 第一个审批节点 + *

+ *

+ * 发起人任务节点的作用: + *

    + *
  • 正常发起流程时,通过RootTaskCreateListener自动完成,流程继续到下一个节点
  • + *
  • 驳回到发起人时,任务保留给发起人,发起人可以在待办任务中编辑并重新提交
  • + *
+ *

* * @param node 前端传输的开始节点数据 - * @return 包含开始事件的流程元素列表 + * @return 包含开始事件和发起人任务的流程元素列表 */ private static List buildStartNode(Node node) { List flowElementList = new ArrayList<>(); + // 1. 创建真正的开始事件(ID 为 start) StartEvent startEvent = new StartEvent(); - startEvent.setId(node.getId()); - startEvent.setName(node.getName()); - + startEvent.setId("start"); + startEvent.setName("开始"); flowElementList.add(startEvent); + // 2. 创建发起人任务节点(ID 保持为 root) + // 配置任务监听器,用于处理驳回场景 + FlowableListener createListener = new FlowableListener(); + createListener.setImplementation(RootTaskCreateListener.class.getCanonicalName()); + createListener.setImplementationType("class"); + createListener.setEvent("create"); + + UserTask rootTask = new UserTask(); + rootTask.setId(node.getId()); // 保持 "root" 作为 ID + rootTask.setName(node.getName()); + // 动态获取发起人作为执行人 + rootTask.setAssignee("${rootTaskHandler.resolveAssignee(execution)}"); + + List taskListeners = new ArrayList<>(); + taskListeners.add(createListener); + rootTask.setTaskListeners(taskListeners); + + flowElementList.add(rootTask); + + // 3. 更新节点的 head/tail 信息 + node.setHeadId("start"); // 头部是 StartEvent + node.setTailId(node.getId()); // 尾部是 root UserTask + return flowElementList; } @@ -655,7 +687,11 @@ public class ModelUtil { /** * 创建节点内部连接线 *

- * 创建节点内部的连接线,例如审批节点与其后续服务任务之间的连接。 + * 创建节点内部的连接线,例如: + *

    + *
  • 开始节点:start → root(StartEvent到发起人UserTask)
  • + *
  • 审批节点:审批任务 → 服务任务
  • + *
* * @param node 节点数据 * @param flowId 流程定义ID @@ -673,6 +709,13 @@ public class ModelUtil { return sequenceFlowList; } + // 开始节点:创建 start → root 的连线 + if (node.getType() == NodeTypeEnum.ROOT.getValue().intValue()) { + SequenceFlow sequenceFlow = buildSingleSequenceFlow("start", nodeId, null, "start->root"); + sequenceFlowList.add(sequenceFlow); + } + + // 审批节点:创建审批任务到服务任务的连线 if (node.getType() == NodeTypeEnum.APPROVAL.getValue().intValue()) { String gatewayId = StrUtil.format("approve_service_task_{}", nodeId);