Skip to content
Merged

Dev #220

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
18 changes: 16 additions & 2 deletions docs/Integration/event-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,9 @@ public interface IAsyncEvent extends IEvent {} // 异步事件标记 ←

同步/异步由 `DomainEventContext.push` 的 `instanceof` 判定:`IAsyncEvent` → 异步(线程池执行);`ISyncEvent`/裸 `IEvent` → 同步(发布线程内联执行)。

### 1.2 七种事件
### 1.2 八种事件

所有事件均携带 `FlowRecord`(或当前记录)与 `boolean mock` 标记(Mock 模式为 `true`)。
记录类事件均携带 `FlowRecord`(或当前记录)与 `boolean mock` 标记(Mock 模式为 `true`)。

| 事件类 | 触发时机 | 构造参数 | 访问器 |
|---|---|---|---|
Expand All @@ -40,6 +40,19 @@ public interface IAsyncEvent extends IEvent {} // 异步事件标记 ←
| `FlowRecordRevokeEvent` | 撤销(被撤销的后置记录) | `FlowRecord currentRecord, boolean mock` | `getCurrentRecord()` / `isMock()` |
| `FlowRecordUrgeEvent` | 催办(每条待办) | `FlowRecord, IFlowOperator urgeOperator, boolean mock` | `getFlowRecord()` / `getUrgeOperator()` / `isMock()` |
| `FlowRecordDeleteEvent` | 删除未流转实例(开始节点) | `FlowRecord, boolean mock` | `getFlowRecord()` / `isMock()` |
| `FlowSubProcessResetEvent` | 子流程数据重置(issue #219) | `SubProcessRecord oldRecord, SubProcessRecord newRecord, long resetRecordId, IFlowOperator resetOperator, boolean mock` | `getOldRecord()` / `getNewRecord()` / `getResetRecordId()` / `getResetOperator()` / `isMock()` |

**子流程重置事件订阅须知**:

- 事件提醒业务方子流程数据已被重置:`oldRecord` 为被取代的旧聚合组快照(含全部旧实例流程id),
`newRecord` 为重置后的新聚合组——继承实例沿用原流程id,重建实例为新流程id,
重建实例的 `getSourceProcessId()` 记录其替换的旧实例流程id,据此完成旧 → 新流程id映射;
- 事件经异步线程池分发、到达顺序不保证(重置同时伴随被作废记录的 `RevokeEvent`
与重建实例的 `StartEvent`/`TodoEvent`),订阅方应按「同一 `parentRecordId` + 节点维度下
**最大聚合组 id 为活跃组**」做幂等覆盖式收敛,禁止增量累加;
- 事件的 `oldRecord` id 与订阅方当前活跃组 id 不一致时,说明存在丢失的重置事件,
应通过节点记录查询(`/api/cmd/record/processNodes`)全量对账;
- 旧实例记录不会被删除(仅聚合组标记已取代),订阅方收到事件后应自行将旧流程id的数据标记失效。

范例(`FlowRecordUrgeEvent.java`,唯一带三个字段的事件):

Expand Down Expand Up @@ -107,6 +120,7 @@ EventPusher.push(new FlowRecordStartEvent(flowRecord, session.isMock()));
| `FlowUrgeService.urge()` | 每条待办 `UrgeEvent` |
| `FlowRevokeService.revoke()` | 当前记录恢复待办 `TodoEvent` + 被撤销后置记录 `RevokeEvent` |
| `FlowDeleteService.delete()` | `DeleteEvent` |
| `FlowSubProcessResetService.reset()` | 被作废记录链逐条 `RevokeEvent` + 子流程重置 `SubProcessResetEvent`;重建实例经 `FlowCreateService` 推送各自的 `StartEvent` + `TodoEvent` |

> 事件在 `repositoryHolder.saveRecords(...)` **落库之后**推送,保证订阅方看到的记录已持久化。

Expand Down
16 changes: 16 additions & 0 deletions docs/Integration/rest-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ operatorId 请求参数 → 存在 → 使用该 id 作为当前操作人
| POST | `/api/cmd/record/processNodes` | `FlowProcessNodeRequest` | `MultiResponse<ProcessNode>` | 流程流转节点预览 |
| POST | `/api/cmd/record/create` | `FlowCreateRequest` | `SingleResponse<Long>` | 发起流程,返回首条记录 id |
| POST | `/api/cmd/record/action` | `FlowActionRequest` | `SingleResponse<ActionResponse>` | 执行动作(通过/拒绝/加签/委派/退回/转办/自定义/保存) |
| POST | `/api/cmd/record/subProcess/reset` | `FlowSubProcessResetRequest` | `Response` | 子流程数据重置(独立接口,需子流程节点开启重置能力) |
| POST | `/api/cmd/record/urge` | `IdRequest` | `Response` | 催办 |
| POST | `/api/cmd/record/revoke` | `IdRequest` | `Response` | 撤销 |
| POST | `/api/cmd/record/delete` | `IdRequest` | `Response` | 删除(仅未流转实例,开始节点) |
Expand Down Expand Up @@ -139,6 +140,20 @@ public interface FlowRecordQueryService {
- `forwardOperatorIds`:加签/委派/转办的目标操作人。
- `backNodeId`:退回动作的目标节点(仅 Start/Approval/Handle 可退回)。

### `FlowSubProcessResetRequest`(子流程数据重置,独立接口)

```json
{
"recordId": 456,
"resetInstanceProcessIds": ["选中重建的子流程实例流程id"],
"advice": "重置说明"
}
```

- 重置不属于审批动作,不经过 `/action` 接口;仅当子流程节点开启 `resettable` 能力且当前待办位于其下游时可调用。
- 无需指定子流程节点:由选中实例流程id定位其所属聚合组,选中实例须同属一个已放行且未取代的聚合组。
- 流程详情 `FlowContent.resetSubProcess` 标识字段表明当前记录是否具备该能力,前端据此提供交互。

### `ActionResponse`

```json
Expand All @@ -165,6 +180,7 @@ form(FlowForm 元数据) / fieldPermissions(字段权限)
todos(当前节点待办 Body 列表) / actions / actionList
mergeable / createOperator / currentOperator / flowState / recordState
histories(审批历史) / nodes(NodeOption 可操作节点) / revoke / urge
resetSubProcess(子流程重置能力标识:当前待办位于开启重置能力、已汇聚完成的子流程下游时为 true)
```

### `FlowRecordContent`(列表响应,字段摘要)
Expand Down
52 changes: 52 additions & 0 deletions docs/capabilities/flow-engine-framework/sub-process.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ symbols:
- SubProcessScript
- SubProcessResultScript
- FlowSubProcessResultService
- FlowSubProcessResetService
- FlowSubProcessResetRequest
- FlowSubProcessResetEvent
- FlowSession.findSubProcessRecords
- ProcessNode.SubProcessBody
---
Expand Down Expand Up @@ -75,8 +78,57 @@ def run(request){
- 拼接的祖先流程节点统一返回 `ProcessNode.parentProcessRecord=true`,当前子流程节点为 `false`,供展示层区分记录来源。
- 开关开启时主流程历史包含审批人和审批意见,因此应由流程设计者按数据可见性要求显式配置。

## 子流程数据重置(issue #219)

子流程汇聚放行后、下游审批发现数据有误时,可对**已完成的子流程聚合记录**执行退回重走。重置是**独立接口**(不属于常规审批动作),能力完全由业务配置控制:

### 能力开关(子流程节点配置)

`SubProcessStrategy` 新增 `resettable` 布尔配置,**默认关闭**。仅当子流程节点开启该能力时,其汇聚完成后的下游待办记录才具备重置能力,`/api/cmd/record/subProcess/reset` 接口才允许调用;未开启时接口直接拒绝。历史流程定义无该字段时按关闭处理(`fromMap` 兼容)。

### 接口与定位方式

- 请求 `FlowSubProcessResetRequest`:`recordId`(当前操作的待办记录)+ `resetInstanceProcessIds`(选中重建的实例流程id)+ `advice`(重置说明)。
- **不需要指定子流程节点**:由选中实例的流程id定位其所属聚合组(全部选中实例须同属一个未取代聚合组),重置始终是整个子流程**从头重走一次**——重建实例经由子流程节点配置的创建脚本与自动提交策略重新发起,不跳转、不迁移、不自动跳过任何历史数据。
- 前置约束:当前记录为待办且操作人匹配;目标聚合组已放行(PASSED)、未被取代、全部实例已结束;当前流程无等待中聚合组;当前记录位于**锁定合并节点**(见下)。

### 锁定合并节点(复杂下游拓扑支持)

合并节点 = 子流程放行恢复后,锚点记录之后**首个产生业务记录的节点**。恢复遍历以锚点为来源记录,合并点首代记录均满足 `fromId == 锚点id`,据此从真实执行数据判定(抄送记录不参与判定):

- 串联 / 条件分支 / 触发节点 / 抄送直通:合并节点唯一——条件分支取**实际命中分支**的首个节点,触发与抄送节点不阻断、不产生合并点;
- 并行 / 包容分支扇出:多个分支首节点记录并存时,按记录产生顺序**锁定第一个分支的节点**为合并节点,兄弟分支不可重置;
- **重置仅允许在锁定合并节点的待办记录上发起**,合并节点之后的更深层节点不可重置(重置语义为退回子流程重走,入口固定在汇聚处)。

重置执行时同步清除该流程实例的循环触发标记(`LoopTriggerTraceContext.clearByProcess`),保证重走再次经过下游节点(如抄送)时不被被动式环检测误判为循环。

### 重置语义

- **聚合组建模**:旧聚合组标记 `superseded`(聚合状态保持不变,历史记录保留有效可查),新建一条聚合组取代其成为当前有效数据。新组由两类实例组成:
- **继承实例**(未选中,`Instance.inherited=true`):沿用原实例的流程id与最终状态,不重新执行;
- **重建实例**(选中):基于旧实例的启动记录反推创建请求(沿用原子流程定义、表单数据、流程标题与原发起人),创建全新子流程(新流程id),`Instance.sourceProcessId` 记录其替换的旧实例流程id,供订阅方完成旧 → 新映射。
- **锚点不变**:新组沿用旧组的 `parentRecordId`(原触发记录),保证结果判定后主流程恢复位置不变。
- **记录链作废**:触发锚点之后的主流程记录链以撤销语义作废(含执行重置的当前待办,重置说明写入该记录供审计),主流程退回子流程节点重新等待——**原待办从待办列表消失,新聚合组放行后生成全新的下游待办**;作废链路与 `FlowRevokeService` 一致,逐条推送 `FlowRecordRevokeEvent`。
- **结果判定自愈**:重建实例全部结束后,`FlowSubProcessResultService` 按新组判定(结果脚本经上下文可见继承 + 重建实例的完整最终记录集),放行后生成新的下游待办;`complete()` 定位聚合组时跳过已取代组,避免继承实例同属新旧两组时命中旧组。
- **脚本视野**:`FlowSession.findSubProcessRecords` 过滤已取代组,重置后脚本仅见当前有效组。
- **事件通知**:推送 `FlowSubProcessResetEvent`(旧组快照 + 新组快照 + 重置记录id + 重置操作人 + mock),提醒业务订阅方子流程数据已被重置;订阅方收敛规则见 `docs/Integration/event-integration.md`。

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

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

### 数据结构兼容性

- `SubProcessRecord` 新增 `superseded` 标记、`Instance` 新增 `inherited`/`sourceProcessId` 字段,均为纯增量:持久化实例 JSON 由 fastjson2 按字段读写,旧数据缺失字段按 `false`/`null` 处理;实体新增可空列 `superseded`,存量行为 `null` 时按未取代处理;
- `SubProcessStrategy.resettable`、`FlowContent.resetSubProcess` 均为新增字段,旧数据/旧客户端不受影响;
- 重置不删除任何历史记录,旧聚合组与旧实例记录在其原流程内保持有效可查。

## 运行约束

- 主流程等待期间不允许撤销子流程触发记录。
- 未自动提交的子流程草稿在主流程等待期间不允许删除。
- 结果失败的异常策略仅支持跳转节点,不支持返回兜底操作人。
- 子流程重置仅在节点开启 `resettable` 能力时可用,默认关闭。
- 主流程等待期间(存在等待中聚合组)不允许执行子流程重置。
- 结果未放行(ERROR)的聚合组不允许重置。
- 重置仅允许在锁定合并节点的待办上发起;并行/包容分支的兄弟分支与合并节点之后的更深层节点不可重置。
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,18 @@ public void clearExpired() {
this.traces.entrySet().removeIf(entry -> now - entry.getValue() > DEFAULT_WINDOW_MILLIS);
}

/**
* 清除指定流程实例的全部触发标记。
* <p>子流程重置等"退回重走"场景会作废既有记录链并重新触发下游节点,
* 旧执行留下的标记不应把合法的重走误判为循环。</p>
*
* @param processId 流程实例标识
*/
public void clearByProcess(String processId) {
String prefix = processId + ":";
this.traces.keySet().removeIf(key -> key.startsWith(prefix));
}

/**
* 清空全部标记(测试隔离用)。
*/
Expand Down
Loading
Loading