Skip to content
Merged

Dev #221

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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 模块划分

Expand Down
2 changes: 1 addition & 1 deletion docs/Integration/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` 分流机制、请求/响应结构 |
Expand Down
2 changes: 1 addition & 1 deletion docs/Integration/event-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -225,5 +225,5 @@ public class MyFlowRecordUrgeEventHandler implements IHandler<FlowRecordUrgeEven

## 6. Mock 模式标记

- 事件携带 `mock` 标记:有 session 的场景用 `session.isMock()`(等价于 `repositoryHolder instanceof MockRepositoryHolder`);无 session 的服务(`FlowDeleteService`/`FlowUrgeService`/`FlowRevokeService`)直接用 `repositoryHolder instanceof MockRepositoryHolder`。
- 事件携带 `mock` 标记:有 session 的场景用 `session.isMock()`(等价于 `repositoryHolder instanceof MockRepositoryHolder`);无 session 的服务(`FlowDeleteService`/`FlowUrgeService`/`FlowRevokeService`/`FlowSubProcessResetService`)直接用 `repositoryHolder instanceof MockRepositoryHolder`。
- 订阅方可通过 `event.isMock()` 区分测试/演示流量与生产流量(例如不发送真实消息)。
2 changes: 1 addition & 1 deletion docs/Integration/rest-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ form(FlowForm 元数据) / fieldPermissions(字段权限)
todos(当前节点待办 Body 列表) / actions / actionList
mergeable / createOperator / currentOperator / flowState / recordState
histories(审批历史) / nodes(NodeOption 可操作节点) / revoke / urge
resetSubProcess(子流程重置能力标识:当前待办位于开启重置能力、已汇聚完成的子流程下游时为 true)
resetSubProcess(子流程重置能力标识:当前待办位于开启重置能力、已汇聚完成的子流程的锁定合并节点上时为 true;更深层下游节点为 false
```

### `FlowRecordContent`(列表响应,字段摘要)
Expand Down
4 changes: 2 additions & 2 deletions docs/capabilities/flow-engine-framework/sub-process.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: flow-engine-framework/sub-process
module: flow-engine-framework
description: 子流程聚合执行能力,支持批量创建、主流程等待、脚本结果判定与异常跳转
description: 子流程聚合执行能力,支持批量创建、主流程等待、脚本结果判定、异常跳转与数据重置(退回重走)
status: 已实现
scope: 后端
source: 项目自有
Expand Down Expand Up @@ -115,7 +115,7 @@ def run(request){

### 可见性(详情标识字段)

重置不是节点动作,不出现在动作列表中。流程详情 `FlowContent` 新增 `resetSubProcess` 布尔标识:当前待办位于「开启重置能力、已汇聚完成」的子流程下游时为 `true`。前端解析到该标识即可自行提供重置交互(呈现方式由使用方决定,不限定于操作列);节点视图中新旧聚合组同时呈现,`ProcessNode.SubProcessBody.superseded` 与 `SubProcessInstanceBody.inherited/sourceProcessId` 供前端区分。
重置不是节点动作,不出现在动作列表中。流程详情 `FlowContent` 新增 `resetSubProcess` 布尔标识:当前待办位于「开启重置能力、已汇聚完成」的子流程的**锁定合并节点**上时为 `true`(更深层下游节点为 `false`)。前端解析到该标识即可自行提供重置交互(呈现方式由使用方决定,不限定于操作列);节点视图中新旧聚合组同时呈现,`ProcessNode.SubProcessBody.superseded` 与 `SubProcessInstanceBody.inherited/sourceProcessId` 供前端区分。

### 数据结构兼容性

Expand Down
2 changes: 1 addition & 1 deletion docs/capabilities/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
| [flow-engine-framework/operator-assignment](./flow-engine-framework/operator-assignment.md) | flow-engine-framework | 操作人分配能力,支持发起人/审批人设定的分配校验与落库 | 后端 | 项目自有 |
| [flow-engine-framework/repository](./flow-engine-framework/repository.md) | flow-engine-framework | 仓储抽象层,定义流程引擎的持久化接口和仓储持有者上下文 | 后端 | 项目自有 |
| [flow-engine-framework/script](./flow-engine-framework/script.md) | flow-engine-framework | Groovy 脚本运行时,包含脚本注册表、脚本类型封装和脚本执行上下文 | 后端 | 项目自有 |
| [flow-engine-framework/sub-process](./flow-engine-framework/sub-process.md) | flow-engine-framework | 子流程聚合执行能力,支持批量创建、主流程等待、脚本结果判定与异常跳转 | 后端 | 项目自有 |
| [flow-engine-framework/sub-process](./flow-engine-framework/sub-process.md) | flow-engine-framework | 子流程聚合执行能力,支持批量创建、主流程等待、脚本结果判定、异常跳转与数据重置(退回重走) | 后端 | 项目自有 |
| [flow-engine-framework/workflow-strategy](./flow-engine-framework/workflow-strategy.md) | flow-engine-framework | 工作流级别策略体系,包含策略接口、工厂和具体策略实现 | 后端 | 项目自有 |
| [flow-engine-framework/workflow](./flow-engine-framework/workflow.md) | flow-engine-framework | 工作流引擎核心模型,包含流程定义、构建器、版本管理和运行时快照 | 后端 | 项目自有 |
| [flow-engine-starter/auto-configuration](./flow-engine-starter/auto-configuration.md) | flow-engine-starter | Spring Boot 自动配置入口,注册流程引擎的核心组件和仓储上下文 | 后端 | 项目自有 |
Expand Down
2 changes: 1 addition & 1 deletion docs/capabilities/springboot/ioc.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ framework_version: "3.5.9"
- **依赖注入**:容器管理 Bean 之间的依赖关系。本项目不使用 `@Autowired` / `@Inject` 注解,统一通过 Lombok `@AllArgsConstructor` 构造器注入(如 `WorkflowController`、`WorkflowService`、各 `RepositoryImpl`)
- **Bean 生命周期管理**:容器统一管理 Bean 的创建、初始化和销毁
- **AOP 支持**:框架提供 `@Aspect` 横切关注点能力(本项目未使用)
- **事件发布**:本项目不使用 Spring `ApplicationEventPublisher`,事件系统为自研 `IFlowEvent` + `EventPusher`(`flow-engine-framework/event/` 下 7 种事件类,操作完成后通过 `EventPusher.push(event)` 推送)
- **事件发布**:本项目不使用 Spring `ApplicationEventPublisher`,事件系统为自研 `IFlowEvent` + `EventPusher`(`flow-engine-framework/event/` 下 8 种事件类,操作完成后通过 `EventPusher.push(event)` 推送)
- **自动配置**:通过 `@Configuration` + `@Conditional` 实现条件化 Bean 注册

## 如何使用
Expand Down
2 changes: 1 addition & 1 deletion docs/manual/integration-manual.html
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ <h2 id="be-extends">5. 后端:扩展点</h2>

<table>
<tr><th>扩展点</th><th>说明</th></tr>
<tr><td>事件订阅</td><td>实现 <code>IHandler&lt;E&gt;</code> 订阅 7 种事件:Start / Todo / Done / Finish / Urge / Revoke / Delete,自动被 Spring 扫描</td></tr>
<tr><td>事件订阅</td><td>实现 <code>IHandler&lt;E&gt;</code> 订阅 8 种事件:Start / Todo / Done / Finish / Urge / Revoke / Delete / SubProcessReset,自动被 Spring 扫描</td></tr>
<tr><td>节点策略</td><td>15 种策略(OperatorLoad / RecordMerge / Revoke / Router / FormFieldPermission 等)由 <code>NodeStrategyManager</code> 持有(工厂固定注册,不支持新增策略类型;业务方通过策略配置 + Groovy 脚本表达差异)</td></tr>
<tr><td>自定义动作</td><td>8 种内置动作(PASS/REJECT/RETURN/TRANSFER/DELEGATE/ADD_AUDIT/SAVE/CUSTOM)由 <code>FlowActionFactory</code> 固定注册(不支持新增动作类型);业务方通过 CUSTOM 动作 + Groovy 脚本表达自定义行为</td></tr>
<tr><td>自定义脚本注册</td><td><code>ScriptRegistryContext.getInstance().setRegistry(new CustomScriptRegistry())</code> 替换默认 Groovy 脚本</td></tr>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,13 @@ public List<FlowRecord> findTodoByOperator(long operatorId) {
}

public List<FlowRecord> 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<FlowRecord> 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();
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,13 +141,12 @@ public void reset() {
newInstances);
subProcessRepository.save(newGroup);

for (FlowActionRequest submitRequest : submitRequests) {
flowService.action(submitRequest);
}

// 作废触发锚点之后的记录链(复用撤销语义):重置后主流程退回子流程节点等待,
// 作废触发锚点之后的旧记录链(复用撤销语义):重置后主流程退回子流程节点等待,
// 锚点之后的旧记录不再代表有效路径,且其 fromId + 节点与恢复后新建记录相同,
// 不作废会被同节点记录查询误判为多人审批
// 不作废会被同节点记录查询误判为多人审批。
// 作废必须先于重建实例提交:纯自动子流程会在提交中同步跑完并触发结果判定
// 恢复主流程(新记录 fromId == 锚点id),之后再作废会把刚恢复的新记录一并作废,
// 导致主流程卡死
List<FlowRecord> invalidatedRecords = new ArrayList<>();
List<FlowRecord> afterRecords = repositoryHolder
.findAfterRecords(currentRecord.getProcessId(), anchorRecord.getId());
Expand All @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, Object> data = JSON.parseObject(JSON.toJSONString(source.toMap()));
Expand All @@ -152,6 +152,7 @@ void shouldPreserveSubProcessConfigurationWhenConvertingNode() {
assertEquals("result-script", strategy.getResultScript().getScript());
assertEquals(false, strategy.isSubmit());
assertTrue(strategy.isShowParentProcessRecords());
assertTrue(strategy.isResettable());
}

/**
Expand Down Expand Up @@ -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<String, Object> data = JSON.parseObject(JSON.toJSONString(source.toMap()));
@SuppressWarnings("unchecked")
List<Map<String, Object>> strategies = (List<Map<String, Object>>) 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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<FlowRecord> 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) {
Expand Down
2 changes: 1 addition & 1 deletion flow-frontend
Submodule flow-frontend updated 29 files
+3 −2 docs/capabilities/flow-design/sub-process-config.md
+1 −1 docs/capabilities/index.md
+7 −0 packages/flow-approval-presenter/CHANGELOG.md
+1 −1 packages/flow-approval-presenter/package.json
+23 −1 packages/flow-approval-presenter/src/presenters/action.ts
+10 −0 packages/flow-approval-presenter/src/presenters/index.ts
+6 −0 packages/flow-core/CHANGELOG.md
+1 −1 packages/flow-core/package.json
+6 −0 packages/flow-design/CHANGELOG.md
+1 −1 packages/flow-design/package.json
+22 −0 packages/flow-design/src/components/design-editor/node-components/strategy/sub-process.tsx
+12 −1 packages/flow-design/tests/script/sub_process_strategy.test.ts
+6 −0 packages/flow-icons/CHANGELOG.md
+1 −1 packages/flow-icons/package.json
+6 −0 packages/flow-mobile/flow-mobile-approval/CHANGELOG.md
+1 −1 packages/flow-mobile/flow-mobile-approval/package.json
+6 −0 packages/flow-mobile/flow-mobile-form/CHANGELOG.md
+1 −1 packages/flow-mobile/flow-mobile-form/package.json
+6 −0 packages/flow-mobile/flow-mobile-ui/CHANGELOG.md
+1 −1 packages/flow-mobile/flow-mobile-ui/package.json
+6 −0 packages/flow-pc/flow-pc-approval/CHANGELOG.md
+1 −1 packages/flow-pc/flow-pc-approval/package.json
+6 −0 packages/flow-pc/flow-pc-form/CHANGELOG.md
+1 −1 packages/flow-pc/flow-pc-form/package.json
+6 −0 packages/flow-pc/flow-pc-ui/CHANGELOG.md
+1 −1 packages/flow-pc/flow-pc-ui/package.json
+7 −0 packages/flow-types/CHANGELOG.md
+1 −1 packages/flow-types/package.json
+6 −0 packages/flow-types/src/types/flow-approval.ts
Loading