Skip to content
Merged

Dev #238

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions docs/capabilities/flow-engine-framework/sub-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ symbols:
- SubProcessResultScript
- FlowSubProcessResultService
- FlowSubProcessResetService
- SubProcessSessionFactory
- FlowSubProcessResetRequest
- FlowSubProcessResetEvent
- FlowSession.findSubProcessRecords
Expand Down Expand Up @@ -89,7 +90,9 @@ def run(request){
### 接口与定位方式

- 请求 `FlowSubProcessResetRequest`:`recordId`(当前操作的待办记录)+ `resetInstanceProcessIds`(选中重建的实例流程id)+ `advice`(重置说明)。
- **不需要指定子流程节点**:由选中实例的流程id定位其所属聚合组(全部选中实例须同属一个未取代聚合组),重置始终是整个子流程**从头重走一次**——重建实例经由子流程节点配置的创建脚本与自动提交策略重新发起,不跳转、不迁移、不自动跳过任何历史数据。
- **不需要指定子流程节点**:由选中实例的流程id定位其所属聚合组(全部选中实例须同属一个未取代聚合组),重置始终是整个子流程**从头重走一次**——重建时重新执行子流程节点配置的创建脚本,按旧实例在聚合组中的创建顺序取对应的 `FlowCreateRequest`,再按创建脚本的自动提交策略提交;不跳转、不迁移、不自动跳过任何历史数据。
- **参数与人员设定由脚本决定**:重置接口不接受表单、发起人、流程标题或人员设定参数,这些均以创建脚本重新执行的输出为准(`FlowCreateRequest.formData` / `operatorId` / `operatorSelectMap` / `workTitle` / `actionId`),与首次发起完全一致;「发起人设定 / 审批人设定」(`INITIATOR_SELECT` / `APPROVER_SELECT`)类节点在重置后仍由脚本指定的人员接收待办。
- **数量一致性校验**:创建脚本返回的创建请求数量必须与旧聚合组实例数一致,否则抛出 `state.subProcess.resetScriptMismatch` 并拒绝重置,避免按错位请求重建、打乱多实例的创建顺序与数据组装。
- 前置约束:当前记录为待办且操作人匹配;目标聚合组已放行(PASSED)、未被取代、全部实例已结束;当前流程无等待中聚合组;当前记录位于**锁定合并节点**(见下)。

### 锁定合并节点(复杂下游拓扑支持)
Expand All @@ -106,7 +109,8 @@ def run(request){

- **聚合组建模**:旧聚合组标记 `superseded`(聚合状态保持不变,历史记录保留有效可查),新建一条聚合组取代其成为当前有效数据。新组由两类实例组成:
- **继承实例**(未选中,`Instance.inherited=true`):沿用原实例的流程id与最终状态,不重新执行;
- **重建实例**(选中):基于旧实例的启动记录反推创建请求(沿用原子流程定义、表单数据、流程标题与原发起人),创建全新子流程(新流程id),`Instance.sourceProcessId` 记录其替换的旧实例流程id,供订阅方完成旧 → 新映射。
- **重建实例**(选中):重新执行子流程创建脚本,按旧实例在聚合组中的创建顺序取对应的 `FlowCreateRequest`(表单数据、发起人、人员设定、流程标题与开始动作均以脚本输出为准,不再基于旧启动记录反推请求),创建全新子流程(新流程id),`Instance.sourceProcessId` 记录其替换的旧实例流程id,供订阅方完成旧 → 新映射。
- **脚本上下文一致**:重置与结果判定共用 `SubProcessSessionFactory` 以锚点记录重建父流程会话(当前节点为子流程节点、表单/审批意见/动作均取自锚点记录),保证重置时创建脚本拿到的 `request` 与首次触发子流程时一致。
- **锚点不变**:新组沿用旧组的 `parentRecordId`(原触发记录),保证结果判定后主流程恢复位置不变。
- **记录链作废**:触发锚点之后的主流程记录链以撤销语义作废(含执行重置的当前待办,重置说明写入该记录供审计),主流程退回子流程节点重新等待——**原待办从待办列表消失,新聚合组放行后生成全新的下游待办**;作废链路与 `FlowRevokeService` 一致,逐条推送 `FlowRecordRevokeEvent`。
- **结果判定自愈**:重建实例全部结束后,`FlowSubProcessResultService` 按新组判定(结果脚本经上下文可见继承 + 重建实例的完整最终记录集),放行后生成新的下游待办;`complete()` 定位聚合组时跳过已取代组,避免继承实例同属新旧两组时命中旧组。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,19 @@ public static FlowStateException subProcessResetMergeNodeOnly() {
"Sub process reset only allowed on the todo record of the locked merge node");
}

/**
* Sub process reset script result does not match the original group instance count
*
* @param expected original group instance count
* @param actual create request count returned by the creation script
* @return exception
*/
public static FlowStateException subProcessResetScriptMismatch(int expected, int actual) {
return new FlowStateException("state.subProcess.resetScriptMismatch",
String.format("Sub process reset creation script returned %d create requests, "
+ "but the original group has %d instances", actual, expected));
}

/**
* Workflow script not found when executing a node or action
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import com.codingapi.flow.service.FlowRecordService;
import com.codingapi.flow.service.FlowService;
import com.codingapi.flow.service.WorkflowService;
import com.codingapi.flow.session.FlowSession;
import com.codingapi.flow.session.IRepositoryHolder;
import com.codingapi.flow.strategy.node.SubProcessStrategy;
import com.codingapi.flow.workflow.Workflow;
Expand Down Expand Up @@ -112,15 +113,27 @@ public void reset() {
throw FlowStateException.subProcessResetMergeNodeOnly();
}

// 重建与首次发起共用同一套创建脚本与数据组装过程:重新执行子流程创建脚本,
// 按旧实例在聚合组中的创建顺序取对应请求,保证参数、发起人、发起人设定的人员
// 与首次发起一致,且不会打乱多实例的创建顺序
FlowSession parentSession = SubProcessSessionFactory.createParentSession(
currentGroup, repositoryHolder, workflowService);
List<FlowCreateRequest> scriptRequests = strategy.getSubProcessScript().execute(parentSession);
List<SubProcessRecord.Instance> oldInstances = currentGroup.getInstances();
if (scriptRequests.size() != oldInstances.size()) {
throw FlowStateException.subProcessResetScriptMismatch(oldInstances.size(), scriptRequests.size());
}

// 先创建重建实例(不触发自动提交),落库新聚合组后再统一提交,
// 避免重建实例在新组落库前结束导致结果判定找不到聚合组
boolean submit = strategy.isSubmit();
FlowService flowService = repositoryHolder.createFlowService();
List<SubProcessRecord.Instance> newInstances = new ArrayList<>();
List<FlowActionRequest> submitRequests = new ArrayList<>();
for (SubProcessRecord.Instance instance : currentGroup.getInstances()) {
for (int index = 0; index < oldInstances.size(); index++) {
SubProcessRecord.Instance instance = oldInstances.get(index);
if (request.getResetInstanceProcessIds().contains(instance.getProcessId())) {
FlowCreateRequest createRequest = buildRebuildRequest(instance, anchorRecord);
FlowCreateRequest createRequest = scriptRequests.get(index);
long newRecordId = flowService.create(createRequest);
FlowRecord newStartRecord = repositoryHolder.getRecordById(newRecordId);
newInstances.add(SubProcessRecord.Instance.rebuiltFrom(newStartRecord, instance.getProcessId()));
Expand Down Expand Up @@ -236,29 +249,6 @@ private FlowRecord findLockedMergeRecord(String processId, long anchorId) {
.orElse(null);
}

/**
* 依据旧实例的启动记录反推重建请求:沿用原子流程定义、表单数据、流程标题与原发起人,
* 锚定到原触发记录。
*
* @param oldInstance 被重置的旧实例
* @param anchorRecord 父流程中子流程节点的触发记录
* @return 重建子流程的创建请求
*/
private FlowCreateRequest buildRebuildRequest(SubProcessRecord.Instance oldInstance, FlowRecord anchorRecord) {
FlowRecord oldStartRecord = repositoryHolder.getRecordById(oldInstance.getStartRecordId());
if (oldStartRecord == null) {
throw FlowNotFoundException.record(oldInstance.getStartRecordId());
}
FlowCreateRequest request = new FlowCreateRequest();
request.setWorkCode(oldStartRecord.getWorkCode());
request.setOperatorId(oldStartRecord.getCreateOperatorId());
request.setActionId(oldStartRecord.getActionId());
request.setFormData(oldStartRecord.getFormData());
request.setWorkTitle(oldInstance.getWorkTitle());
request.setParentRecordId(anchorRecord.getId());
return request;
}

/**
* 判断指定记录当前是否可以执行子流程重置(供详情数据计算重置标识)。
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,21 @@
package com.codingapi.flow.service.impl;

import com.codingapi.flow.action.BaseAction;
import com.codingapi.flow.action.IFlowAction;
import com.codingapi.flow.action.actions.PassAction;
import com.codingapi.flow.cache.FlowRuntimeScriptLocalCache;
import com.codingapi.flow.domain.SubProcessContext;
import com.codingapi.flow.domain.SubProcessRecord;
import com.codingapi.flow.error.ErrorThrow;
import com.codingapi.flow.event.FlowRecordDoneEvent;
import com.codingapi.flow.event.FlowRecordTodoEvent;
import com.codingapi.flow.event.IFlowEvent;
import com.codingapi.flow.exception.FlowNotFoundException;
import com.codingapi.flow.exception.FlowValidationException;
import com.codingapi.flow.form.FormData;
import com.codingapi.flow.node.BaseFlowNode;
import com.codingapi.flow.node.IFlowNode;
import com.codingapi.flow.node.nodes.SubProcessNode;
import com.codingapi.flow.operator.IFlowOperator;
import com.codingapi.flow.record.FlowRecord;
import com.codingapi.flow.repository.SubProcessRepository;
import com.codingapi.flow.service.WorkflowService;
import com.codingapi.flow.session.FlowAdvice;
import com.codingapi.flow.session.FlowSession;
import com.codingapi.flow.session.IRepositoryHolder;
import com.codingapi.flow.strategy.node.SubProcessStrategy;
import com.codingapi.flow.workflow.Workflow;
import com.codingapi.flow.workflow.runtime.WorkflowRuntime;
import com.codingapi.springboot.framework.event.EventPusher;

import java.util.ArrayList;
Expand All @@ -39,13 +29,11 @@ public class FlowSubProcessResultService {
private final FlowRecord childFinalRecord;
private final IRepositoryHolder repositoryHolder;
private final SubProcessRepository subProcessRepository;
private final WorkflowService workflowService;

public FlowSubProcessResultService(FlowRecord childFinalRecord, IRepositoryHolder repositoryHolder) {
this.childFinalRecord = childFinalRecord;
this.repositoryHolder = repositoryHolder;
this.subProcessRepository = repositoryHolder.getSubProcessRepository();
this.workflowService = repositoryHolder.getWorkflowService();
}

public void complete() {
Expand Down Expand Up @@ -90,42 +78,8 @@ public void complete() {
}

private FlowSession createParentSession(SubProcessRecord subProcessRecord) {
FlowRecord parentRecord = repositoryHolder.getRecordById(subProcessRecord.getParentRecordId());
if (parentRecord == null) {
throw FlowNotFoundException.record(subProcessRecord.getParentRecordId());
}
WorkflowRuntime workflowRuntime = workflowService.getWorkflowRuntime(subProcessRecord.getParentWorkRuntimeId());
if (workflowRuntime == null) {
throw FlowNotFoundException.workflow(subProcessRecord.getParentWorkRuntimeId() + " not found");
}
FlowRuntimeScriptLocalCache.getInstance().set(workflowRuntime.getScripts());
Workflow workflow = workflowRuntime.toWorkflow();
IFlowNode sourceNode = workflow.getFlowNode(parentRecord.getNodeId());
IFlowAction action = sourceNode.actionManager().getActionById(parentRecord.getActionId());
// 前驱记录可能为相同人员自动通过的留痕记录(无审批动作,actionId=null,issue #226):
// 恢复下游以来源节点的通过动作兜底,避免空动作会话导致 NPE 使主流程永久停滞
if (action == null && parentRecord.isAutoDone()) {
action = sourceNode.actionManager().getAction(PassAction.class);
}
IFlowOperator currentOperator = repositoryHolder.getOperatorById(parentRecord.getCurrentOperatorId());
IFlowOperator createdOperator = repositoryHolder.getOperatorById(parentRecord.getCreateOperatorId());
IFlowOperator submitOperator = repositoryHolder.getOperatorById(parentRecord.getSubmitOperatorId());
FormData formData = new FormData(workflow.getForm());
formData.reset(parentRecord.getFormData());
FlowAdvice advice = parentRecord.toAdvice(workflow);
IFlowNode subProcessNode = workflow.getFlowNode(subProcessRecord.getNodeId());
FlowSession session = new FlowSession(repositoryHolder,
currentOperator,
createdOperator,
submitOperator,
workflow,
subProcessNode,
action,
formData,
parentRecord,
List.of(),
parentRecord.getWorkRuntimeId(),
advice);
FlowSession session = SubProcessSessionFactory.createParentSession(
subProcessRecord, repositoryHolder, repositoryHolder.getWorkflowService());
session.setSubProcessContext(new SubProcessContext(subProcessRecord, childFinalRecord));
return session;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package com.codingapi.flow.service.impl;

import com.codingapi.flow.action.IFlowAction;
import com.codingapi.flow.action.actions.PassAction;
import com.codingapi.flow.cache.FlowRuntimeScriptLocalCache;
import com.codingapi.flow.domain.SubProcessRecord;
import com.codingapi.flow.exception.FlowNotFoundException;
import com.codingapi.flow.form.FormData;
import com.codingapi.flow.node.IFlowNode;
import com.codingapi.flow.operator.IFlowOperator;
import com.codingapi.flow.record.FlowRecord;
import com.codingapi.flow.service.WorkflowService;
import com.codingapi.flow.session.FlowAdvice;
import com.codingapi.flow.session.FlowSession;
import com.codingapi.flow.session.IRepositoryHolder;
import com.codingapi.flow.workflow.Workflow;
import com.codingapi.flow.workflow.runtime.WorkflowRuntime;

import java.util.List;

/**
* 子流程父流程会话工厂。
*
* <p>以子流程聚合记录的锚点记录为来源重建父流程会话,供子流程结果判定脚本与
* 子流程重置重建脚本复用,保证两处脚本执行时的父流程上下文、表单数据与动作信息一致。</p>
*/
public class SubProcessSessionFactory {

private SubProcessSessionFactory() {
}

/**
* 以子流程聚合记录的锚点记录重建父流程会话。
*
* <p>当前记录为锚点记录、当前节点为子流程节点,表单数据与审批意见均来自锚点记录,
* 因而与首次触发子流程时的脚本上下文保持一致。</p>
*
* @param subProcessRecord 子流程聚合记录
* @param repositoryHolder 仓储持有者
* @param workflowService 流程服务
* @return 父流程会话
*/
public static FlowSession createParentSession(SubProcessRecord subProcessRecord,
IRepositoryHolder repositoryHolder,
WorkflowService workflowService) {
FlowRecord parentRecord = repositoryHolder.getRecordById(subProcessRecord.getParentRecordId());
if (parentRecord == null) {
throw FlowNotFoundException.record(subProcessRecord.getParentRecordId());
}
WorkflowRuntime workflowRuntime = workflowService.getWorkflowRuntime(subProcessRecord.getParentWorkRuntimeId());
if (workflowRuntime == null) {
throw FlowNotFoundException.workflow(subProcessRecord.getParentWorkRuntimeId() + " not found");
}
FlowRuntimeScriptLocalCache.getInstance().set(workflowRuntime.getScripts());
Workflow workflow = workflowRuntime.toWorkflow();
IFlowNode sourceNode = workflow.getFlowNode(parentRecord.getNodeId());
IFlowAction action = sourceNode.actionManager().getActionById(parentRecord.getActionId());
// 前驱记录可能为相同人员自动通过的留痕记录(无审批动作,actionId=null,issue #226):
// 恢复下游以来源节点的通过动作兜底,避免空动作会话导致 NPE 使主流程永久停滞
if (action == null && parentRecord.isAutoDone()) {
action = sourceNode.actionManager().getAction(PassAction.class);
}
IFlowOperator currentOperator = repositoryHolder.getOperatorById(parentRecord.getCurrentOperatorId());
IFlowOperator createdOperator = repositoryHolder.getOperatorById(parentRecord.getCreateOperatorId());
IFlowOperator submitOperator = repositoryHolder.getOperatorById(parentRecord.getSubmitOperatorId());
FormData formData = new FormData(workflow.getForm());
formData.reset(parentRecord.getFormData());
FlowAdvice advice = parentRecord.toAdvice(workflow);
IFlowNode subProcessNode = workflow.getFlowNode(subProcessRecord.getNodeId());
return new FlowSession(repositoryHolder,
currentOperator,
createdOperator,
submitOperator,
workflow,
subProcessNode,
action,
formData,
parentRecord,
List.of(),
parentRecord.getWorkRuntimeId(),
advice);
}
}
Loading
Loading