Skip to content

Commit 48afaea

Browse files
xlorneclaude
andcommitted
feat: 新增删除流程能力与撤销事件
- 新增 FlowDeleteService: 仅允许删除位于开始节点、待办且未流转的流程实例 - 新增 FlowDeleteRequest 请求对象与 FlowRecordDeleteEvent 事件 - FlowRecordService 新增 deleteFlowRecord, 清理合并关系/待办/流程记录 - FlowRecordController 暴露 POST /api/cmd/record/delete - FlowRevokeService 撤销时对每条被撤销记录推送 FlowRecordRevokeEvent - FlowStateException 扩展删除相关异常方法 - 同步 README.md 与 CLAUDE.md 核心架构说明 Co-Authored-By: Claude <noreply@anthropic.com>
1 parent b0263ed commit 48afaea

12 files changed

Lines changed: 507 additions & 41 deletions

File tree

CLAUDE.md

Lines changed: 42 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,48 @@ Flow Engine 是一个企业级工作流引擎,基于 Java 17 和 Spring Boot 3
3030
| `com.codingapi.flow.manager` | 业务管理器 |
3131
| `com.codingapi.flow.service` | 业务服务层 |
3232

33+
## 核心架构
34+
35+
理解以下跨文件的模式后再动手改动,可避免破坏引擎一致性。
36+
37+
### 服务编排模式(FlowService)
38+
39+
`FlowService``flow-engine-framework/.../service/FlowService.java`)是 `@Transactional` 业务入口。**每个流程操作对应一个独立的 `FlowXxxService`**`FlowCreateService` / `FlowActionService` / `FlowRevokeService` / `FlowDeleteService` / `FlowUrgeService` / `FlowDetailService`),通过 `IRepositoryHolder` 注入依赖,且每次操作前调用 `FlowOperatorLocalThreadCache.getInstance().clear()` 清理线程缓存。新增流程操作应沿用此模式:新建 `FlowXxxService(request, repositoryHolder)`,并在 `FlowService` 暴露一个入口方法。
40+
41+
### 仓储持有者(IRepositoryHolder)
42+
43+
服务层不直接注入仓储,而是通过 `IRepositoryHolder` 获取 `FlowRecordService` / `WorkflowService` / `FlowOperatorGateway` 等依赖。两个实现:
44+
- `RepositoryHolderContext` — 生产,单例,由 `flow-engine-starter` 自动配置注册
45+
- `MockRepositoryHolder` — 测试 / Mock 场景
46+
47+
REST 层通过请求参数 `mockKey` 分流到 Mock 实例(见 `FlowRecordController.loadFlowService()` / `loadCurrentOperatorId()`);`operatorId` 也可由该参数覆盖,便于无登录态测试。
48+
49+
### 流程记录生命周期
50+
51+
三类记录紧密绑定,须保持一致:
52+
- `FlowRecord` — 流程执行记录(TODO / DONE 状态,`isTodo()` / `isDone()` / `isFinish()`
53+
- `FlowTodoRecord` — 待办记录
54+
- `FlowTodoMerge` — 待办合并关系(开启记录合并 `isMergeable()` 时产生)
55+
56+
保存走 `FlowRecordSaveService.saveAll()`,单次调用顺序执行 `saveRecords → saveTodoMargeRecords → removeTodoMergeRecords`,保证三者一致。删除整条流程走 `FlowRecordService.deleteFlowRecord()`,按 合并关系 → 待办 → 流程记录 顺序清理。
57+
58+
### 节点 / 动作 / 策略三元组
59+
60+
19 种节点(`node/nodes/`)× 8 种动作(`action/actions/`),行为由策略(`strategy/node/``strategy/workflow/`)驱动而非继承扩展:
61+
- `NodeStrategyManager` 持有节点策略(如 `RevokeStrategy``OperatorLoadStrategy``RecordMergeStrategy`
62+
- `ActionManager` 持有节点动作
63+
- 节点类型常量见 `StartNode.NODE_TYPE`(= `NodeType.START.name()`),`FlowRecord.isNodeType(String)` 用于按类型判定
64+
65+
### 事件(IFlowEvent + EventPusher)
66+
67+
7 种事件:开始 / 待办 / 已办 / 完成 / 催办 / 撤销(`FlowRecordRevokeEvent`)/ 删除(`FlowRecordDeleteEvent`)。操作完成后用 `EventPusher.push(event)` 推送;事件通常携带 `FlowRecord` + `mock` 标记。外部可用 Spring `@EventListener` 订阅。
68+
69+
### API 模块划分
70+
71+
- **命令操作**`flow-engine-starter-api``FlowRecordController``/api/cmd/record`:create / action / revoke / delete / urge / detail)与 `WorkflowController``/api/cmd/workflow`:流程设计 CRUD)
72+
- **查询**`flow-engine-starter-query``FlowRecordQueryController``/api/query/record`:list / todo / done / notify)
73+
- **持久化**`flow-engine-starter-infra` — JPA 实体与仓储实现(含达梦方言)
74+
3375
## 常用命令
3476

3577
### 构建与测试
@@ -70,24 +112,6 @@ cd flow-engine-example && mvn spring-boot:run
70112
- **与用户沟通及编写文档时,所有内容必须使用中文表述**
71113
- **在每次修改代码以后,要执行本地化的编译验证确保代码没有错误**
72114
- Java 代码遵循 Spring Boot 规范
73-
- 设计涉及流程或 UML 图形的解决方案时,使用纯文本线框图表示
74-
- 在设计计划方案或执行方案过程中,对于代码的设计规划与调整修改要遵循本项目的代码风格和架构设计规则
75-
- 设计的计划要保存到本地的 `docs/` 目录下:
76-
- `docs/plan/` - 存放详细可执行开发计划,文件命名格式为 `yyyy-mm-dd-标题.md`
77-
- `docs/architecture/` - 存放架构设计文档
78-
- `docs/todo/` - 存放阶段任务清单
79-
80-
### Git 工作流
81-
82-
**代码提交路径:**
83-
84-
```
85-
feature/{task-name} → PR → dev → (用户审核) → main
86-
```
87-
88-
- **禁止直接向 main 分支提交**,main 分支由用户手动合并管理
89-
- **dev 分支为集成分支**,所有 PR 的目标分支均为 dev
90-
- **所有代码开发必须在 git worktree 中进行**,不在主工作区直接改代码
91115

92116
### 测试规范
93117

@@ -152,27 +176,6 @@ class ScriptRuntimeContextTest {
152176
}
153177
```
154178

155-
## 架构设计文档
156-
157-
详细架构设计见 `docs/architecture/` 目录:
158-
159-
| 文档 | 说明 |
160-
|------|------|
161-
| `01-项目概览.md` | 项目背景、技术栈 |
162-
| `02-模块总览.md` | Maven 模块划分 |
163-
| `03-节点模块.md` | 节点接口、节点类型 |
164-
| `04-动作模块.md` | 动作接口、动作执行 |
165-
| `05-策略模块.md` | 节点策略、工作流策略 |
166-
| `06-脚本模块.md` | Groovy 脚本运行时 |
167-
| `07-仓储模块.md` | 仓储接口、基础设施实现 |
168-
| `08-构建器模块.md` | 建造者模式实现 |
169-
| `09-管理器模块.md` | 业务管理器 |
170-
| `10-服务模块.md` | 业务服务层 |
171-
| `11-网关与上下文.md` | 网关接口、上下文 |
172-
| `12-数据库设计.md` | 实体设计、表结构 |
173-
| `13-API接口设计.md` | REST API 设计 |
174-
175-
176179
<!-- PKR-START -->
177180
## PKR 知识查阅(编码前必须)
178181

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,13 +62,15 @@ flow-engine
6262
│ │ └── UrgeInterval # 催办间隔
6363
│ ├── error # 错误处理
6464
│ │ └── ErrorThrow # 错误抛出器
65-
│ ├── event # 事件系统(5种
65+
│ ├── event # 事件系统(7种
6666
│ │ ├── IFlowEvent # 事件接口
6767
│ │ ├── FlowRecordStartEvent # 流程开始事件
6868
│ │ ├── FlowRecordTodoEvent # 待办事件
6969
│ │ ├── FlowRecordDoneEvent # 已办事件
7070
│ │ ├── FlowRecordFinishEvent # 流程完成事件
71-
│ │ └── FlowRecordUrgeEvent # 催办事件
71+
│ │ ├── FlowRecordUrgeEvent # 催办事件
72+
│ │ ├── FlowRecordRevokeEvent # 流程撤销事件
73+
│ │ └── FlowRecordDeleteEvent # 流程删除事件
7274
│ ├── exception # 异常系统(6种)
7375
│ │ ├── FlowException # 异常基类
7476
│ │ ├── FlowValidationException # 参数验证异常
@@ -132,6 +134,7 @@ flow-engine
132134
│ │ │ ├── FlowActionRequest # 动作请求
133135
│ │ │ ├── FlowCreateRequest # 创建请求
134136
│ │ │ ├── FlowRevokeRequest # 撤回请求
137+
│ │ │ ├── FlowDeleteRequest # 删除请求
135138
│ │ │ └── FlowUrgeRequest # 催办请求
136139
│ │ └── response # 响应对象
137140
│ │ ├── FlowOperator # 流程操作者
@@ -172,6 +175,7 @@ flow-engine
172175
│ │ │ ├── FlowActionService # 流程动作服务
173176
│ │ │ ├── FlowDelayTriggerService # 延迟触发服务
174177
│ │ │ ├── FlowRevokeService # 流程撤回服务
178+
│ │ │ ├── FlowDeleteService # 流程删除服务
175179
│ │ │ ├── FlowUrgeService # 流程催办服务
176180
│ │ │ └── FlowDetailService # 流程详情服务
177181
│ │ └── FlowService # 服务接口
@@ -474,6 +478,8 @@ category.subcategory.errorType
474478
- `notFound.workflow.definition` - 流程定义未找到
475479
- `permission.field.readOnly` - 字段只读
476480
- `state.record.alreadyDone` - 记录已完成
481+
- `state.record.notSupportDelete` - 记录不允许删除
482+
- `state.node.notStart` - 记录不在开始节点
477483
- `validation.field.required` - 必填字段为空
478484
- `execution.script.error` - 脚本执行错误
479485

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.codingapi.flow.event;
2+
3+
import com.codingapi.flow.record.FlowRecord;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
7+
/**
8+
* 流程删除事件
9+
* <p>
10+
* 仅当删除位于开始节点且尚未流转的流程实例时触发
11+
*/
12+
@Getter
13+
@AllArgsConstructor
14+
public class FlowRecordDeleteEvent implements IFlowEvent {
15+
16+
private final FlowRecord flowRecord;
17+
18+
private final boolean mock;
19+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.codingapi.flow.event;
2+
3+
import com.codingapi.flow.record.FlowRecord;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
7+
/**
8+
* 流程撤销事件
9+
* <p>
10+
* 撤销流程时触发,登记被撤销的下级记录数据
11+
*/
12+
@Getter
13+
@AllArgsConstructor
14+
public class FlowRecordRevokeEvent implements IFlowEvent {
15+
16+
/**
17+
* 被撤销的待办记录
18+
*/
19+
private final FlowRecord currentRecord;
20+
21+
/**
22+
* 是否为模拟环境
23+
*/
24+
private final boolean mock;
25+
}

flow-engine-framework/src/main/java/com/codingapi/flow/exception/FlowStateException.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,31 @@ public static FlowStateException nodeNotSupportRevoke() {
117117
public static FlowStateException recordLimitUrgeError() {
118118
return new FlowStateException("record.urge.limit", "record urge limit error");
119119
}
120+
121+
/**
122+
* Record not support delete (not a todo / finished / revoked)
123+
*
124+
* @return exception
125+
*/
126+
public static FlowStateException recordNotSupportDelete() {
127+
return new FlowStateException("state.record.notSupportDelete", "Flow record is not a todo, delete not allowed");
128+
}
129+
130+
/**
131+
* Record is not on the start node, delete not allowed
132+
*
133+
* @return exception
134+
*/
135+
public static FlowStateException nodeNotStartNode() {
136+
return new FlowStateException("state.node.notStart", "Flow record is not on the start node, delete not allowed");
137+
}
138+
139+
/**
140+
* Flow already running (has subsequent records), delete not allowed
141+
*
142+
* @return exception
143+
*/
144+
public static FlowStateException recordAlreadyRunning() {
145+
return new FlowStateException("state.record.alreadyRunning", "Flow is already running, delete not allowed");
146+
}
120147
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.codingapi.flow.pojo.request;
2+
3+
import com.codingapi.flow.exception.FlowValidationException;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
8+
/**
9+
* 流程删除请求
10+
* <p>
11+
* 仅用于删除位于开始节点且尚未流转的流程实例
12+
*/
13+
@Data
14+
@NoArgsConstructor
15+
@AllArgsConstructor
16+
public class FlowDeleteRequest {
17+
18+
/**
19+
* 记录id
20+
*/
21+
private long recordId;
22+
/**
23+
* 操作者
24+
*/
25+
private long operatorId;
26+
27+
public void verify() {
28+
if (recordId <= 0) {
29+
throw FlowValidationException.required("recordId");
30+
}
31+
if (operatorId <= 0) {
32+
throw FlowValidationException.required("operatorId");
33+
}
34+
}
35+
}

flow-engine-framework/src/main/java/com/codingapi/flow/service/FlowRecordService.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,4 +120,28 @@ public List<FlowRecord> findFlowRecordCurrentNodeRecords(long fromId, String nod
120120
return this.flowRecordRepository.findCurrentNodeRecords(fromId,nodeId);
121121
}
122122

123+
124+
/**
125+
* 删除流程记录及其关联的待办数据
126+
* <p>
127+
* 仅用于作废尚未流转的流程实例,会同时清理该流程记录对应的待办记录与合并关系
128+
* @param flowRecord 流程记录
129+
*/
130+
public void deleteFlowRecord(FlowRecord flowRecord) {
131+
FlowTodoRecord flowTodoRecord = flowTodoRecordRepository.getByTodoKey(flowRecord.getTodoKey());
132+
if (flowTodoRecord != null) {
133+
// 清理待办合并关系
134+
List<FlowTodoMerge> todoMerges = flowTodoMergeRepository.findByTodoId(flowTodoRecord.getId());
135+
if (todoMerges != null) {
136+
for (FlowTodoMerge todoMerge : todoMerges) {
137+
flowTodoMergeRepository.delete(todoMerge);
138+
}
139+
}
140+
// 清理待办记录
141+
flowTodoRecordRepository.delete(flowTodoRecord);
142+
}
143+
// 删除流程记录
144+
flowRecordRepository.delete(flowRecord);
145+
}
146+
123147
}

flow-engine-framework/src/main/java/com/codingapi/flow/service/FlowService.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,19 @@ public void revoke(FlowRevokeRequest request) {
8585
}
8686

8787

88+
/**
89+
* 删除流程
90+
* <p>
91+
* 仅用于作废尚未流转(位于开始节点且无后续记录)的流程实例
92+
* @param request 删除请求
93+
*/
94+
public void delete(FlowDeleteRequest request) {
95+
FlowOperatorLocalThreadCache.getInstance().clear();
96+
FlowDeleteService flowDeleteService = new FlowDeleteService(request, this.repositoryHolder);
97+
flowDeleteService.delete();
98+
}
99+
100+
88101
/**
89102
* 催办
90103
*/
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package com.codingapi.flow.service.impl;
2+
3+
import com.codingapi.flow.event.FlowRecordDeleteEvent;
4+
import com.codingapi.flow.exception.FlowNotFoundException;
5+
import com.codingapi.flow.exception.FlowStateException;
6+
import com.codingapi.flow.mock.MockRepositoryHolder;
7+
import com.codingapi.flow.node.nodes.StartNode;
8+
import com.codingapi.flow.pojo.request.FlowDeleteRequest;
9+
import com.codingapi.flow.record.FlowRecord;
10+
import com.codingapi.flow.service.FlowRecordService;
11+
import com.codingapi.flow.session.IRepositoryHolder;
12+
import com.codingapi.springboot.framework.event.EventPusher;
13+
14+
import java.util.List;
15+
16+
/**
17+
* 删除流程服务
18+
* <p>
19+
* 仅允许删除位于开始节点、且尚未流转的待办记录,等价于作废整个尚未发起的流程实例。
20+
* 若流程已流转到后续节点,需先撤销退回开始节点后再删除。
21+
*/
22+
public class FlowDeleteService {
23+
24+
private final FlowDeleteRequest request;
25+
private final FlowRecordService flowRecordService;
26+
private final IRepositoryHolder repositoryHolder;
27+
28+
public FlowDeleteService(FlowDeleteRequest request, IRepositoryHolder repositoryHolder) {
29+
this.request = request;
30+
this.flowRecordService = repositoryHolder.getFlowRecordService();
31+
this.repositoryHolder = repositoryHolder;
32+
}
33+
34+
public void delete() {
35+
request.verify();
36+
FlowRecord currentRecord = flowRecordService.getFlowRecord(request.getRecordId());
37+
if (currentRecord == null) {
38+
throw FlowNotFoundException.record(request.getRecordId());
39+
}
40+
// 必须是待办状态(未办理、未撤销、未结束)
41+
if (!currentRecord.isTodo()) {
42+
throw FlowStateException.recordNotSupportDelete();
43+
}
44+
// 必须位于开始节点
45+
if (!currentRecord.isNodeType(StartNode.NODE_TYPE)) {
46+
throw FlowStateException.nodeNotStartNode();
47+
}
48+
// 必须无后续流转记录(流程尚未执行)
49+
List<FlowRecord> afterRecords = flowRecordService.findFlowRecordAfterRecords(
50+
currentRecord.getProcessId(), currentRecord.getId());
51+
if (afterRecords != null && !afterRecords.isEmpty()) {
52+
throw FlowStateException.recordAlreadyRunning();
53+
}
54+
// 权限校验:仅允许当前待办处理人删除
55+
if (currentRecord.getCurrentOperatorId() != request.getOperatorId()) {
56+
throw FlowStateException.operatorNotMatch();
57+
}
58+
// 删除流程记录及其关联待办
59+
flowRecordService.deleteFlowRecord(currentRecord);
60+
// 推送删除事件
61+
EventPusher.push(new FlowRecordDeleteEvent(currentRecord, repositoryHolder instanceof MockRepositoryHolder));
62+
}
63+
}

flow-engine-framework/src/main/java/com/codingapi/flow/service/impl/FlowRevokeService.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.codingapi.flow.service.impl;
22

3+
import com.codingapi.flow.event.FlowRecordRevokeEvent;
34
import com.codingapi.flow.event.FlowRecordTodoEvent;
45
import com.codingapi.flow.event.IFlowEvent;
56
import com.codingapi.flow.exception.FlowNotFoundException;
@@ -94,6 +95,8 @@ public void revoke() {
9495
for (FlowRecord afterRecord : afterRecords) {
9596
afterRecord.revoke();
9697
recordList.add(afterRecord);
98+
// 撤销事件,登记撤销发起的记录数据
99+
flowEvents.add(new FlowRecordRevokeEvent(afterRecord, repositoryHolder instanceof MockRepositoryHolder));
97100
}
98101

99102
repositoryHolder.saveRecords(recordList);

0 commit comments

Comments
 (0)