diff --git a/CLAUDE.md b/CLAUDE.md index d0a9ac6f..36ac241d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,7 +64,7 @@ REST 层通过请求参数 `mockKey` 分流到 Mock 实例(见 `FlowRecordCont ### 事件(IFlowEvent + EventPusher) -7 种事件:开始 / 待办 / 已办 / 完成 / 催办 / 撤销(`FlowRecordRevokeEvent`)/ 删除(`FlowRecordDeleteEvent`)。操作完成后用 `EventPusher.push(event)` 推送;事件通常携带 `FlowRecord` + `mock` 标记。外部可用 Spring `@EventListener` 订阅。 +8 种事件:开始 / 待办 / 已办 / 完成 / 催办 / 撤销(`FlowRecordRevokeEvent`)/ 删除(`FlowRecordDeleteEvent`)/ 子流程重置(`FlowSubProcessResetEvent`)。操作完成后用 `EventPusher.push(event)` 推送;记录类事件携带 `FlowRecord` + `mock` 标记,重置事件携带新旧聚合组快照。外部可用 Spring `@EventListener` 订阅。 ### API 模块划分 diff --git a/docs/Integration/README.md b/docs/Integration/README.md index 78ec9a92..618bd511 100644 --- a/docs/Integration/README.md +++ b/docs/Integration/README.md @@ -32,7 +32,7 @@ | [快速开始](./quick-start.md) | 最小集成步骤:引入依赖 → 提供 Bean → 编写流程 → 发起流程 | | [流程用户体系](./user-integration.md) | `FlowOperatorGateway` / `IFlowOperator` / `GatewayContext` / 线程缓存 / 当前登录人 | | [流程默认脚本](./script-integration.md) | 12 种脚本类型、`GroovyScriptRequest` 完整 API、`$bind`、默认脚本替换、脚本生命周期 | -| [流程事件机制](./event-integration.md) | 7 种事件、`EventPusher`、`IHandler` 订阅、异步分发管道、事务变体、推送时机 | +| [流程事件机制](./event-integration.md) | 8 种事件、`EventPusher`、`IHandler` 订阅、异步分发管道、事务变体、推送时机 | | [仓储抽象与持久化](./repository-integration.md) | `IRepositoryHolder`、12 个仓储接口(含 11 个 JPA 实现 + 需自行实现的 `NodeViewJavaScriptRepository`)、脚本仓储、锁机制 | | [Spring Boot 自动配置](./auto-configuration.md) | `AutoConfiguration` 8 个 Bean、4 个 Register、必选/可选 Bean 清单 | | [REST API](./rest-api.md) | 4 个 Controller、`mockKey`/`operatorId` 分流机制、请求/响应结构 | diff --git a/docs/Integration/event-integration.md b/docs/Integration/event-integration.md index f866fc3c..9adf1afc 100644 --- a/docs/Integration/event-integration.md +++ b/docs/Integration/event-integration.md @@ -225,5 +225,5 @@ public class MyFlowRecordUrgeEventHandler implements IHandler5. 后端:扩展点 - + diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/FlowRecordRepositoryMockImpl.java b/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/FlowRecordRepositoryMockImpl.java index 3c832266..3696516c 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/FlowRecordRepositoryMockImpl.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/mock/repository/FlowRecordRepositoryMockImpl.java @@ -33,11 +33,13 @@ public List findTodoByOperator(long operatorId) { } public List findDoneByOperator(long operatorId) { - return cache.values().stream().filter(flowRecord -> flowRecord.getCurrentOperatorId() == operatorId && !flowRecord.isTodo()).toList(); + // 与 JPA 口径一致:已办列表过滤已作废与隐藏记录(撤销/重置产生的作废记录不进入已办) + return cache.values().stream().filter(flowRecord -> flowRecord.getCurrentOperatorId() == operatorId && flowRecord.isDone()).toList(); } public List findNotifyByOperator(long operatorId) { - return cache.values().stream().filter(flowRecord -> flowRecord.getCurrentOperatorId() == operatorId && flowRecord.isNotify()).toList(); + // 与 JPA 口径一致:抄送列表过滤已作废与隐藏记录 + return cache.values().stream().filter(flowRecord -> flowRecord.getCurrentOperatorId() == operatorId && flowRecord.isNotify() && !flowRecord.isRevoked() && !flowRecord.isHidden()).toList(); } diff --git a/flow-engine-framework/src/main/java/com/codingapi/flow/service/impl/FlowSubProcessResetService.java b/flow-engine-framework/src/main/java/com/codingapi/flow/service/impl/FlowSubProcessResetService.java index a65e0e6a..e5d085f8 100644 --- a/flow-engine-framework/src/main/java/com/codingapi/flow/service/impl/FlowSubProcessResetService.java +++ b/flow-engine-framework/src/main/java/com/codingapi/flow/service/impl/FlowSubProcessResetService.java @@ -141,13 +141,12 @@ public void reset() { newInstances); subProcessRepository.save(newGroup); - for (FlowActionRequest submitRequest : submitRequests) { - flowService.action(submitRequest); - } - - // 作废触发锚点之后的记录链(复用撤销语义):重置后主流程退回子流程节点等待, + // 作废触发锚点之后的旧记录链(复用撤销语义):重置后主流程退回子流程节点等待, // 锚点之后的旧记录不再代表有效路径,且其 fromId + 节点与恢复后新建记录相同, - // 不作废会被同节点记录查询误判为多人审批 + // 不作废会被同节点记录查询误判为多人审批。 + // 作废必须先于重建实例提交:纯自动子流程会在提交中同步跑完并触发结果判定 + // 恢复主流程(新记录 fromId == 锚点id),之后再作废会把刚恢复的新记录一并作废, + // 导致主流程卡死 List invalidatedRecords = new ArrayList<>(); List afterRecords = repositoryHolder .findAfterRecords(currentRecord.getProcessId(), anchorRecord.getId()); @@ -164,6 +163,10 @@ public void reset() { // 避免重走再次触发下游节点(如抄送)时被被动式环检测误判为循环 LoopTriggerTraceContext.getInstance().clearByProcess(currentRecord.getProcessId()); + for (FlowActionRequest submitRequest : submitRequests) { + flowService.action(submitRequest); + } + boolean mock = repositoryHolder instanceof MockRepositoryHolder; IFlowOperator resetOperator = repositoryHolder.getOperatorById(request.getOperatorId()); for (FlowRecord invalidatedRecord : invalidatedRecords) { diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/node/factory/NodeFactoryTest.java b/flow-engine-framework/src/test/java/com/codingapi/flow/node/factory/NodeFactoryTest.java index aa2bd989..a69e30d7 100644 --- a/flow-engine-framework/src/test/java/com/codingapi/flow/node/factory/NodeFactoryTest.java +++ b/flow-engine-framework/src/test/java/com/codingapi/flow/node/factory/NodeFactoryTest.java @@ -139,7 +139,7 @@ void shouldPreserveSubProcessConfigurationWhenConvertingNode() { .name("子流程") .strategies(NodeStrategyBuilder.builder() .addStrategy(new SubProcessStrategy( - "create-script", false, "result-script", true)) + "create-script", false, "result-script", true, true)) .build()) .build(); Map data = JSON.parseObject(JSON.toJSONString(source.toMap())); @@ -152,6 +152,7 @@ void shouldPreserveSubProcessConfigurationWhenConvertingNode() { assertEquals("result-script", strategy.getResultScript().getScript()); assertEquals(false, strategy.isSubmit()); assertTrue(strategy.isShowParentProcessRecords()); + assertTrue(strategy.isResettable()); } /** @@ -180,6 +181,32 @@ void shouldDisableParentProcessRecordsForLegacySubProcessConfiguration() { assertFalse(restoredStrategy.isShowParentProcessRecords()); } + /** + * 测试目标:验证历史流程定义没有重置能力配置时保持默认关闭(issue #219)。 + * 前置条件:子流程策略 Map 中不包含 resettable。 + * 执行步骤:通过节点工厂反序列化历史节点定义。 + * 期望断言:重置能力开关为 false。 + */ + @Test + void shouldDisableResettableForLegacySubProcessConfiguration() { + SubProcessNode source = SubProcessNode.builder() + .name("历史子流程") + .strategies(NodeStrategyBuilder.builder() + .addStrategy(new SubProcessStrategy("create-script", true, "result-script")) + .build()) + .build(); + Map data = JSON.parseObject(JSON.toJSONString(source.toMap())); + @SuppressWarnings("unchecked") + List> strategies = (List>) data.get("strategies"); + strategies.get(0).remove("resettable"); + + SubProcessNode restored = (SubProcessNode) NodeFactory.getInstance().createNode(data); + SubProcessStrategy restoredStrategy = restored.strategyManager().getStrategy(SubProcessStrategy.class); + + assertNotNull(restoredStrategy); + assertFalse(restoredStrategy.isResettable()); + } + @Test void createTriggerNode() { diff --git a/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowSubProcessResetServiceTest.java b/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowSubProcessResetServiceTest.java index c4d9d296..7ac9eab8 100644 --- a/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowSubProcessResetServiceTest.java +++ b/flow-engine-framework/src/test/java/com/codingapi/flow/service/FlowSubProcessResetServiceTest.java @@ -786,6 +786,68 @@ void shouldAllowResetWhenNotifyNodeBeforeMergeNode() { "重走后流程应正常结束"); } + /** + * 测试目标:验证重建实例同步跑完(纯自动子流程)时,主流程不会因作废范围 + * 误伤新恢复的记录而卡死。 + * 前置条件:主流程 开始 -> 子流程(可重置,单实例) -> 最终审批 -> 结束; + * 子流程为纯自动流程(开始 -> 结束),放行后同步完成。 + * 执行步骤:主流程停在最终审批待办后调用重置接口。 + * 期望断言:重置后主流程恢复出有效的最终审批待办且未被作废,新聚合组放行,流程不卡死。 + */ + @Test + void shouldNotInvalidateResumedRecordsWhenRebuiltChildCompletesSynchronously() { + StartNode autoStart = writableStart("自动子流程开始"); + Workflow autoWorkflow = WorkflowBuilder.builder() + .title("子流程重置测试-自动子流程") + .code(CHILD_CODE + "-auto") + .createdOperator(initiator) + .form(form) + .addNode(autoStart) + .addNode(EndNode.builder().name("自动子流程结束").build()) + .build(); + factory.workflowService.saveWorkflow(autoWorkflow); + String autoScript = """ + def run(request){ + return request.toCreateRequest('%s', %d, '%s', [content:'auto-child']) + } + """.formatted(CHILD_CODE + "-auto", initiator.getUserId(), passAction(autoStart).id()); + + StartNode start = writableStart("同步开始"); + SubProcessNode subProcess = resettableSubProcess(autoScript); + ApprovalNode syncFinalApproval = approvalNode("同步最终审批", finalOperator); + Workflow workflow = WorkflowBuilder.builder() + .title("子流程重置测试-同步完成") + .code(PARENT_CODE + "-sync") + .createdOperator(initiator) + .form(form) + .addNode(start) + .addNode(subProcess) + .addNode(syncFinalApproval) + .addNode(EndNode.builder().name("同步结束").build()) + .build(); + factory.workflowService.saveWorkflow(workflow); + + submitWorkflow(PARENT_CODE + "-sync", start, Map.of("content", "sync")); + FlowRecord finalTodo = todos(finalOperator, syncFinalApproval).get(0); + + SubProcessRecord group = factory.subProcessRepository + .findByParentProcessId(finalTodo.getProcessId()).get(0); + factory.flowService.resetSubProcess(new FlowSubProcessResetRequest( + finalTodo.getId(), finalOperator.getUserId(), + List.of(group.getInstances().get(0).getProcessId()))); + + List resumedTodos = todos(finalOperator, syncFinalApproval); + assertEquals(1, resumedTodos.size(), + "重建实例同步完成后主流程应恢复出有效待办,不能卡死"); + assertFalse(resumedTodos.get(0).isRevoked(), "新恢复的记录不能被误作废"); + SubProcessRecord latestGroup = factory.subProcessRepository + .findByParentProcessIdAndNodeId(finalTodo.getProcessId(), subProcess.getId()).stream() + .filter(record -> !record.isSuperseded()) + .findFirst().orElseThrow(); + assertEquals(SubProcessRecord.State.PASSED, latestGroup.getState(), + "同步完成的新聚合组应放行"); + } + // ==================== 流程构建与操作辅助 ==================== private SubProcessNode resettableSubProcess(String script) { diff --git a/flow-frontend b/flow-frontend index 31a0a0f1..a42c3515 160000 --- a/flow-frontend +++ b/flow-frontend @@ -1 +1 @@ -Subproject commit 31a0a0f1443b031adeed1ad39d5381aaf55b0487 +Subproject commit a42c35157994f3d00d1b61644f8b30a2c5376830
扩展点说明
事件订阅实现 IHandler<E> 订阅 7 种事件:Start / Todo / Done / Finish / Urge / Revoke / Delete,自动被 Spring 扫描
事件订阅实现 IHandler<E> 订阅 8 种事件:Start / Todo / Done / Finish / Urge / Revoke / Delete / SubProcessReset,自动被 Spring 扫描
节点策略15 种策略(OperatorLoad / RecordMerge / Revoke / Router / FormFieldPermission 等)由 NodeStrategyManager 持有(工厂固定注册,不支持新增策略类型;业务方通过策略配置 + Groovy 脚本表达差异)
自定义动作8 种内置动作(PASS/REJECT/RETURN/TRANSFER/DELEGATE/ADD_AUDIT/SAVE/CUSTOM)由 FlowActionFactory 固定注册(不支持新增动作类型);业务方通过 CUSTOM 动作 + Groovy 脚本表达自定义行为
自定义脚本注册ScriptRegistryContext.getInstance().setRegistry(new CustomScriptRegistry()) 替换默认 Groovy 脚本