Files
ordo/docs/usage.md
T
0264408andCursor 850583f326 feat: let the current assignee reassign a pending task
Keep the same task id, move the pending inbox, and record TASK_REASSIGNED without advancing the step.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 08:40:25 +08:00

350 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Ordo 使用说明
本文档是 Ordo 对外行为的使用说明真源。**新增或变更对外 API、JSON 定义、扩展点、异常或运行时语义时,必须同步更新本文。** 未实现能力写在 [roadmap.md](roadmap.md),不要在这里当已支持功能描述。
版本:`0.0.1-SNAPSHOT`。语言:Java 17+。
## 1. 定位
Ordo 是嵌入宿主进程的审批引擎,入口是 `OrdoEngine`。
做:流程定义、实例推进、待办任务、条件路由、ACTION 副作用、审计事件、分页查询。
产品边界:不做业务表单、用户体系、多租户;不内置设计器 UI。业务字段放在 `ProcessContext`(不可变 `Map<String, Object>`)。当前也**没有** REST;HTTP 仍由宿主自建。计划中的可选 REST 与独立设计器见 [roadmap.md](roadmap.md)。
开发计划(尚未提供,见 [roadmap.md](roadmap.md)):到期升级、`CANCELLED`、定义不可变多版本、MySQL 方言、可选 REST + 目录 SPI。
## 2. 模块与接入
| 模块 | 何时用 |
|---|---|
| `ordo-api` | 始终:模型与 `OrdoEngine` |
| `ordo-core` | 内存引擎 / 自己装配 `DefaultOrdoEngine` |
| `ordo-storage-jdbc` | JDBC 持久化(PostgreSQL;测试可用 H2 PostgreSQL 模式) |
| `ordo-spring-boot-starter` | Spring Boot 自动装配 |
| `ordo-example` | `LeaveRequestExample` 内存演示 |
Starter **不携带** JDBC 驱动。生产加 `org.postgresql:postgresql`。Spring Boot 4 还需 `spring-boot-starter-flyway`,否则 Flyway 迁移不会执行。
先 `mvn install` 本仓库,宿主再依赖 `0.0.1-SNAPSHOT`。
### 2.1 内存引擎
```xml
<dependency>
<groupId>com.jetlumen</groupId>
<artifactId>ordo-core</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
```
```java
OrdoEngine ordo = new InMemoryOrdoEngine();
// 可选:new InMemoryOrdoEngine(clock, assigneeResolver, routingCondition, actionHandler, listeners)
```
进程退出数据即丢失。适合单测和本地试跑。
### 2.2 Spring Boot(JDBC)
```xml
<dependency>
<groupId>com.jetlumen</groupId>
<artifactId>ordo-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
```
需要已有 `DataSource`。`ordo.enabled` 缺省为 `true`。
```yaml
ordo:
enabled: true
definitions:
location: classpath*:ordo/*.json # 启动时对每个 JSON 调用 replace
```
启动加载使用 `replace`:无 `RUNNING` 实例则整图替换;有运行中实例则保留库里的定义。
宿主用 `@Bean` 覆盖默认扩展点:
| Bean | 默认 |
|---|---|
| `AssigneeResolver` | 候选人字符串即办理人 |
| `RoutingCondition` | 始终匹配(无条件边) |
| `ActionHandler` | 空操作 |
| `OrdoEventListener` | 可有多个,提交后按 Spring 顺序调用 |
未提供自定义 Bean 时,ACTION 步骤仍会推进流程,但 handler 什么都不做。
## 3. 流程定义
每个定义有 `id`、`name`、步骤列表、转移列表。步骤 id 在定义内唯一。每个步骤必须至少有一条出边(结束用 `to = null`)。同一 `from` 上 `priority` 不能重复。
### 3.1 代码构建
线性(每步无条件进下一步,最后一步结束):
```java
ordo.register(ProcessDefinition.linear("leave-request", "Leave request", List.of(
ApprovalStep.single("manager", "Manager approval", "maria"),
ApprovalStep.single("hr", "HR approval", "henry")
)));
```
图(条件边 + ACTION):
```java
new ProcessDefinition("leave-request-routed", "Leave request",
List.of(
ApprovalStep.action("notify-submitted", "Notify submitted", "leave-submitted-mail"),
ApprovalStep.single("manager", "Manager approval", "maria"),
new ApprovalStep("hr", "HR approval", List.of("henry"), ApprovalPolicy.ANY),
ApprovalStep.action("notify-approved", "Notify approved", "leave-approved-mail")
),
List.of(
StepTransition.always("notify-submitted", "manager"),
StepTransition.when("manager", "hr", "days-gt-3", 0),
StepTransition.when("manager", "notify-approved", null, 1),
StepTransition.always("hr", "notify-approved"),
StepTransition.end("notify-approved")
));
```
`register`:同 id 已存在则 `DefinitionAlreadyExistsException`。
`replace`:整图覆盖;存在该定义的 `RUNNING` 实例则 `DefinitionInUseException`。
当前**没有**运行中实例锁定所用版本:`replace` 成功后新实例用新图,旧已结束实例仍按当时落库的任务理解历史。
### 3.2 JSON
`ProcessDefinitionParser.fromJson(String|InputStream)`。Spring 默认扫 `classpath*:ordo/*.json`。
```json
{
"id": "leave-request-routed",
"name": "Leave request",
"startStep": "notify-submitted",
"steps": [
{
"id": "notify-submitted",
"name": "Notify submitted",
"kind": "ACTION",
"action": "leave-submitted-mail"
},
{
"id": "manager",
"name": "Manager approval",
"candidates": ["maria"],
"policy": "ANY"
},
{
"id": "hr",
"name": "HR approval",
"candidates": ["henry"],
"policy": "ANY"
},
{
"id": "notify-approved",
"name": "Notify approved",
"kind": "ACTION",
"action": "leave-approved-mail"
}
],
"transitions": [
{ "from": "notify-submitted", "to": "manager" },
{ "from": "manager", "to": "hr", "when": "days-gt-3", "priority": 0 },
{ "from": "manager", "to": "notify-approved", "priority": 1 },
{ "from": "hr", "to": "notify-approved" },
{ "from": "notify-approved", "to": null }
]
}
```
字段:
| 字段 | 说明 |
|---|---|
| `startStep` | 可选。指定起始步骤 id;缺省为 `steps` 数组第一项。解析时会把该步旋到列表首位。 |
| `kind` | `APPROVAL`(默认)或 `ACTION`。 |
| `candidates` / `policy` | 仅审批步。`policy` 默认 `ANY`。审批步至少一名候选人,禁止重复。 |
| `action` | 仅 ACTION 步,对应 `ActionHandler.execute` 的 key。审批步禁止带 `action`。 |
| `from` / `to` / `when` / `priority` | `to` 为 `null` 或 JSON `null` 表示结束。`when` 空则无条件。`priority` 默认 0,**越小越先匹配**。 |
## 4. 实例与任务
### 4.1 生命周期
```
start → RUNNING
审批步:为每个候选人建 PENDING 任务
ACTION 步:事务内记 PENDING 执行记录,提交后调 ActionHandler
approve / 路由结束 → APPROVED
reject(步被否决)→ REJECTED
withdraw(仅发起人)→ WITHDRAWN,剩余 PENDING 任务 → SKIPPED
```
```java
ProcessInstance instance = ordo.start("leave-request", "alice",
new ProcessContext(Map.of("requestId", "LEAVE-001", "days", 2)));
List<ApprovalTask> pending = ordo.findPendingTasksByInstanceId(instance.id());
ordo.reassign(pending.get(0).id(), "maria", "diana");
ordo.approve(pending.get(0).id(), "diana", "ok");
ordo.reject(taskId, "maria", "额度不足");
ordo.withdraw(instance.id(), "alice", "计划有变");
```
`start` 的 `initiator`、办理人 `actor` 均不可空白。
### 4.2 权限
- `approve` / `reject` / `reassign`:`actor` 必须等于该任务当前 `assignee`,否则 `UnauthorizedTaskOperationException`。
- `reassign`:仅 `PENDING` 任务;同一任务 id,办理人改为 `newAssignee`,不推进步骤。`newAssignee` 不可空白、不可等于当前 `assignee`,且同一步不能已有该人的 `PENDING` 任务,否则 `IllegalArgumentException`。不经过 `AssigneeResolver`。
- 任务非 `PENDING`:`TaskAlreadyCompletedException`。
- `withdraw`:仅 `initiator`,否则 `UnauthorizedInstanceOperationException`;实例非 `RUNNING`:`InstanceAlreadyCompletedException`。
没有管理员代批、没有系统取消。
### 4.3 状态
实例 `ProcessStatus`:`RUNNING`、`APPROVED`、`REJECTED`、`WITHDRAWN`。
任务 `TaskStatus`:`PENDING`、`APPROVED`、`REJECTED`、`SKIPPED`(同一步其它候选人已决定结果,或实例被撤回)。
`ApprovalTask.action`(`TaskAction`)仍表示**该任务**上的办理人/意见/时间;实例级时间线用 `ProcessEvent`,不要靠拼任务 action 还原流程史。
## 5. 会签 / 或签
一步多个 `candidates` 时由 `ApprovalPolicy` 决定:
| | ANY | ALL |
|---|---|---|
| 通过 | 任一人 `approve` 即过步,其余 PENDING 变 `SKIPPED` | 全部 `approve` 才过步 |
| 驳回 | 所有候选人都 `reject` 才否决实例 | 任一人 `reject` 即否决实例,其余 PENDING 变 `SKIPPED` |
候选人创建任务前会经过 `AssigneeResolver.resolve(candidate, step, context)`,例如把角色名解析成用户 id。默认实现原样返回 candidate。
连续 ACTION 步会在同一次提交后依次执行,上限 32 跳,超出抛 `IllegalStateException`。
## 6. 条件路由
离开当前步时,取出全部 `from = 当前步` 的边,按 `priority` 升序。第一条满足下列条件的边生效:
- `conditionKey` 为空,或
- `routingCondition.matches(conditionKey, instance.context()) == true`
没有匹配边:`NoRouteFoundException`,**整次 approve/start 事务回滚**(JDBC 下任务/实例都不会半更新)。
无条件边通常作为默认分支,`priority` 应大于带 `when` 的边。
`RoutingCondition` 只看到 `ProcessContext`,看不到任务意见,也**没有**流程定义 id。上下文在 `start` 时写入,运行中引擎**不会**改 context。
引擎只注入**一个** `RoutingCondition`(与 `ActionHandler` 相同)。Spring 下多个该类型 Bean 会冲突。宿主用一个门面按 `conditionKey` 分发到多套规则;不同流程靠 key 约定隔离(例如 `leave.days-gt-3`),不要指望引擎按定义拆 bean。
## 7. ACTION 步骤
进入 ACTION 步时:
1. 事务内插入 `ActionExecution`,状态 `PENDING`。
2. 按转移进入下一步或结束实例(仍在同一事务)。
3. 事务提交后调用 `ActionHandler.execute(actionKey, context)`。
4. 成功 → `SUCCESS` + 事件 `ACTION_SUCCEEDED`;失败 → `FAILED`(`errorMessage`)+ `ACTION_FAILED` + 日志 WARNING。
**失败不回滚已提交的审批,不阻塞后续步骤,引擎不做重试。** 宿主用 `queryActionExecutions` 或 listener 自行补发。
```java
@Component
public class MailActions implements ActionHandler {
@Override
public void execute(String actionKey, ProcessContext context) {
switch (actionKey) {
case "leave-submitted-mail" -> { /* ... */ }
case "leave-approved-mail" -> { /* ... */ }
default -> throw new IllegalStateException("unknown action: " + actionKey);
}
}
}
```
## 8. 审计与监听
`queryHistory(instanceId, page)`:该实例事件,**发生时间升序**。
`ProcessEventType`:
- `INSTANCE_STARTED` / `INSTANCE_APPROVED` / `INSTANCE_REJECTED` / `INSTANCE_WITHDRAWN`
- `TASK_CREATED` / `TASK_APPROVED` / `TASK_REJECTED` / `TASK_SKIPPED` / `TASK_REASSIGNED`
- `ACTION_SUCCEEDED` / `ACTION_FAILED`
字段:`id`、`instanceId`、可选 `taskId`/`stepId`/`actor`/`detail`、`occurredAt`。系统完成类事件 `actor` 可为空。`TASK_REASSIGNED` 的 `actor` 为转出人,`detail` 为转入人。
`OrdoEventListener.onEvent(ProcessEvent)` 在**事务提交之后**按事件顺序调用(含本轮 ACTION 结果)。单个 listener 抛错只打日志,不影响流程和其他 listener。
短假(提交邮件 → 经理批 → 通过邮件)一类时间线示例:
`INSTANCE_STARTED` → `TASK_CREATED` → `ACTION_SUCCEEDED` → `TASK_APPROVED` → `INSTANCE_APPROVED` → `ACTION_SUCCEEDED`
ACTION 成功事件发生在提交之后,因此排在同轮事务内写入的任务/实例事件后面。
## 9. 查询
`PageRequest.of(page, size)`:`page` 从 0 起,`size > 0`。`Page` 含 `content`、`totalElements`、`page`、`size`,以及 `totalPages()` / `hasNext()`。
| 方法 | 过滤 | 排序 |
|---|---|---|
| `queryTasks(TaskQuery, PageRequest)` | assignee、instanceId、definitionId、status、createdFrom/To | 创建时间降序 |
| `queryInstances(InstanceQuery, PageRequest)` | definitionId、status、initiator、startedFrom/To | 开始时间降序 |
| `queryDefinitions(PageRequest)` | 无过滤 | 定义 id 升序 |
| `queryHistory(instanceId, PageRequest)` | 单实例 | 发生时间升序 |
| `queryActionExecutions(instanceId, PageRequest)` | 单实例 | 开始时间升序 |
`TaskQuery.any().withAssignee("maria").withStatus(TaskStatus.PENDING)` 等 with 方法返回新对象。字段 `null` 表示不按该维过滤。
便捷方法(不分页):`findInstance`、`findTask`、`findTasks`、`findPendingTasksByAssignee`、`findPendingTasksByInstanceId`。
## 10. `OrdoEngine` 一览
| 方法 | 说明 |
|---|---|
| `register` / `replace` | 登记 / 整图替换定义 |
| `start` | 发起;可选 `ProcessContext` |
| `approve` / `reject` | 办理当前 PENDING 任务 |
| `reassign` | 当前办理人把 PENDING 任务转给他人 |
| `withdraw` | 发起人撤回 |
| `find*` | 按 id / 待办索引读取 |
| `queryTasks` / `queryInstances` / `queryDefinitions` | 分页列表 |
| `queryHistory` / `queryActionExecutions` | 实例审计与 ACTION 记录 |
## 11. 异常
均继承 `OrdoException`(unchecked)。
| 类型 | 何时 |
|---|---|
| `DefinitionAlreadyExistsException` | `register` 撞 id |
| `DefinitionNotFoundException` | `start` 等找不到定义 |
| `DefinitionInUseException` | `replace` 时仍有 RUNNING 实例 |
| `InstanceNotFoundException` | 撤回等找不到实例 |
| `InstanceAlreadyCompletedException` | 对非 RUNNING 实例完成/撤回 |
| `TaskNotFoundException` | 任务 id 不存在 |
| `TaskAlreadyCompletedException` | 重复办理或已被 SKIPPED |
| `UnauthorizedTaskOperationException` | actor ≠ 当前 assignee(approve / reject / reassign) |
| `UnauthorizedInstanceOperationException` | 非发起人撤回 |
| `NoRouteFoundException` | 当前步没有匹配转移 |
参数空白等会抛 `IllegalArgumentException`,不属于 `OrdoException`。
## 12. 存储
Flyway 脚本在 `ordo-storage-jdbc` 的 `db/migration`(V1–V5)。表包括定义/步骤/候选人/转移、实例、任务、`ordo_process_event`、`ordo_action_execution`。
多 JVM 共享同一库时,多步写入走 `TransactionExecutor`,完成任务/实例用条件更新(仍 PENDING / 仍 RUNNING 才改),避免双花。
## 13. 未提供能力
开发计划中(见 [roadmap.md](roadmap.md)):到期升级、`CANCELLED`、定义不可变多版本、MySQL 方言、可选 REST + 目录 SPI。独立设计器不进本仓库,等 REST、目录与多版本定义之后再做。
暂不在计划中:多租户、设计器 UI。当前 REST 由宿主自建。