From 2d14108373e84f890b12173b256f8471c27cb856 Mon Sep 17 00:00:00 2001 From: lorne <1991wangliang@gmail.com> Date: Mon, 24 Aug 2026 20:47:24 +0800 Subject: [PATCH 1/3] =?UTF-8?q?chore:=20=E5=89=8D=E7=AB=AF=E5=8D=87?= =?UTF-8?q?=E7=BA=A7=E8=87=B3=200.2.6=EF=BC=88=E5=AD=90=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E9=85=8D=E7=BD=AE=E6=96=B0=E5=A2=9E=E9=87=8D?= =?UTF-8?q?=E7=BD=AE=E5=BC=80=E5=85=B3=EF=BC=89=20issue=20#219?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 子流程策略配置面板新增「允许重置子流程」开关(resettable,默认关闭), 为子流程数据重置能力提供设计器配置入口。 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- flow-frontend | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flow-frontend b/flow-frontend index 31a0a0f1..fce6370a 160000 --- a/flow-frontend +++ b/flow-frontend @@ -1 +1 @@ -Subproject commit 31a0a0f1443b031adeed1ad39d5381aaf55b0487 +Subproject commit fce6370a72460b620e23bb2b71681f71b9ef8321 From 4a14fae4761b500e1e4b67c172e60b2f75770543 Mon Sep 17 00:00:00 2001 From: lorne <1991wangliang@gmail.com> Date: Mon, 24 Aug 2026 21:47:50 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=E5=8F=91=E5=B8=83=E5=89=8D=E5=AE=A1?= =?UTF-8?q?=E8=AE=A1=E4=BF=AE=E5=A4=8D=E2=80=94=E2=80=94=E5=AD=90=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=E9=87=8D=E7=BD=AE=E9=A1=BA=E5=BA=8F=E7=BC=BA=E9=99=B7?= =?UTF-8?q?=E4=B8=8E=E9=85=8D=E5=A5=97=E5=AE=8C=E5=96=84=20issue=20#219?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 修复阻断缺陷:重置时先作废锚点之后旧记录链、清除循环触发标记,再提交重建实例; 原顺序下纯自动子流程同步跑完会触发主流程恢复,随后作废把新恢复记录一并作废导致流程卡死 - 新增回归测试:纯自动子流程同步完成场景(修复前红灯复现卡死,修复后通过) - NodeFactoryTest 补充 resettable 序列化往返与历史配置缺省关闭用例 - Mock 已办/抄送查询对齐 JPA 口径(过滤已作废与隐藏记录) - 文档修正:事件体系 8 种事件表述(README/CLAUDE/ioc/集成手册)、无 session 服务清单、 重置标识措辞精确为锁定合并节点、能力索引描述同步 - 前端子模块升级至 24a8f75(审批上下文暴露重置能力、视图类型同步) 后端 284 个测试全部通过,前端构建与测试通过。 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CLAUDE.md | 2 +- docs/Integration/README.md | 2 +- docs/Integration/event-integration.md | 2 +- docs/Integration/rest-api.md | 2 +- .../flow-engine-framework/sub-process.md | 4 +- docs/capabilities/index.md | 2 +- docs/capabilities/springboot/ioc.md | 2 +- docs/manual/integration-manual.html | 2 +- .../FlowRecordRepositoryMockImpl.java | 6 +- .../impl/FlowSubProcessResetService.java | 15 +++-- .../flow/node/factory/NodeFactoryTest.java | 29 ++++++++- .../FlowSubProcessResetServiceTest.java | 62 +++++++++++++++++++ flow-frontend | 2 +- 13 files changed, 113 insertions(+), 19 deletions(-) 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 fce6370a..24a8f75f 160000 --- a/flow-frontend +++ b/flow-frontend @@ -1 +1 @@ -Subproject commit fce6370a72460b620e23bb2b71681f71b9ef8321 +Subproject commit 24a8f75f147407e67caa22fa0cee9a0b75a78772 From 2b3c506b98a4eb7d98d6acce12afaec10cb65d93 Mon Sep 17 00:00:00 2001 From: lorne <1991wangliang@gmail.com> Date: Tue, 25 Aug 2026 14:05:09 +0800 Subject: [PATCH 3/3] Update flow-frontend to latest main --- flow-frontend | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flow-frontend b/flow-frontend index 24a8f75f..a42c3515 160000 --- a/flow-frontend +++ b/flow-frontend @@ -1 +1 @@ -Subproject commit 24a8f75f147407e67caa22fa0cee9a0b75a78772 +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 脚本