Host examples and README still described the pre-split packages and omitted optional REST. Co-authored-by: Cursor <cursoragent@cursor.com>
22 KiB
Ordo 使用说明
本文档是 Ordo 对外行为的使用说明真源。新增或变更对外 API、JSON 定义、扩展点、异常或运行时语义时,必须同步更新本文。 未实现能力写在 roadmap.md,不要在这里当已支持功能描述。
版本:0.0.1-SNAPSHOT。语言:Java 17+。
1. 定位
Ordo 是嵌入宿主进程的审批引擎,入口是 OrdoEngine。
做:流程定义、实例推进、待办任务、条件路由、ACTION 副作用、审计事件、分页查询。
产品边界:不做业务表单、用户体系、多租户;不内置设计器 UI。业务字段放在 ProcessContext(不可变 Map<String, Object>)。可选 HTTP 见 §2.3;独立设计器见 roadmap.md。
2. 模块与接入
| 模块 | 何时用 |
|---|---|
ordo-api |
始终:模型与 OrdoEngine |
ordo-core |
内存引擎 / 自己装配 DefaultOrdoEngine |
ordo-storage-jdbc |
JDBC 持久化(PostgreSQL、MySQL;测试可用 H2 PostgreSQL 模式) |
ordo-spring-boot-starter |
Spring Boot 自动装配 |
ordo-example |
LeaveRequestExample 内存演示 |
Java 包(模块未变):
| 包 | 内容 |
|---|---|
com.jetlumen.ordo.api |
OrdoEngine、TransactionExecutor |
com.jetlumen.ordo.api.definition |
图:定义、步骤、边、when、PARALLEL |
com.jetlumen.ordo.api.runtime |
实例、任务、事件、ACTION 执行、token、ProcessRuntime |
com.jetlumen.ordo.api.spi |
ActionHandler、NamedAction、RoutingCondition、NamedCondition、AssigneeResolver、OrdoCatalog、OrdoEventListener |
com.jetlumen.ordo.api.util |
Texts、Jsons |
com.jetlumen.ordo.api.exception / query / repository |
异常、分页查询、存储端口 |
com.jetlumen.ordo.core.spi |
DispatchingActionHandler、DispatchingRoutingCondition、RegistryOrdoCatalog |
Starter 不携带 JDBC 驱动。生产按库添加 org.postgresql:postgresql 或 com.mysql:mysql-connector-j。Spring Boot 4 还需 spring-boot-starter-flyway,否则 Flyway 迁移不会执行。
先 mvn install 本仓库,宿主再依赖 0.0.1-SNAPSHOT。
2.1 内存引擎
<dependency>
<groupId>com.jetlumen</groupId>
<artifactId>ordo-core</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
OrdoEngine ordo = new InMemoryOrdoEngine();
// 可选:new InMemoryOrdoEngine(clock, assigneeResolver, routingCondition, actionHandler, listeners)
进程退出数据即丢失。适合单测和本地试跑。
2.2 Spring Boot(JDBC)
<dependency>
<groupId>com.jetlumen</groupId>
<artifactId>ordo-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
需要已有 DataSource。ordo.enabled 缺省为 true。
ordo:
enabled: true
jdbc:
dialect: # 可选 postgresql / mysql;空则按 DataSource 探测
definitions:
location: classpath*:ordo/*.json # 启动时对每个 JSON 调用 publish
due:
poll-ms: 0 # >0 时轮询 processDue;默认不调度
rest:
enabled: false
base-path: /ordo
启动加载使用 publish:图与 latest 相同则不升版本;不同则写入新版本。运行中实例继续锁定发起时所用版本。
宿主用 @Bean 覆盖默认扩展点:
| Bean | 默认 |
|---|---|
AssigneeResolver |
候选人字符串即办理人 |
NamedCondition |
可有多个;无 RoutingCondition 门面且列表非空时装配 DispatchingRoutingCondition |
NamedAction |
可有多个;无 ActionHandler 门面且列表非空时装配 DispatchingActionHandler |
RoutingCondition |
无 NamedCondition 时 ref 边恒 true;有则按 key 分发,未知 ref 为 false |
ActionHandler |
无 NamedAction 时空操作;有则按 key 分发,未知 key 抛 IllegalArgumentException |
OrdoEventListener |
可有多个,提交后按 Spring 顺序调用 |
OrdoCatalog |
REST 打开时:从 NamedCondition / NamedAction 投影;皆空则为空列表。assignees 仍空 |
未提供 NamedAction 且未覆盖 ActionHandler 时,ACTION 步骤仍会推进流程,但 handler 什么都不做。不要同时提供门面 Bean 与对应 Named*(门面优先,具名 Bean 不参与运行时)。
内存引擎把 com.jetlumen.ordo.core.spi.DispatchingRoutingCondition.of(...) / DispatchingActionHandler.of(...) 传入 InMemoryOrdoEngine 即可。
2.3 可选 REST
spring-boot-starter-web 对 autoconfigure 为 optional,starter 不传递 Web。宿主已有 Web 且 ordo.rest.enabled=true 时注册 com.jetlumen.ordo.spring.rest 下的接口。不包含鉴权;actor / initiator 放在 JSON 体。OpenAPI:ordo-rest.openapi.yaml。
默认前缀 /ordo:
| 方法 | 路径 | 引擎 |
|---|---|---|
| POST | /definitions/parse |
ProcessDefinitionParser.fromJson |
| POST | /definitions |
publish |
| GET | /definitions |
queryDefinitions |
| GET | /definitions/{id} |
findDefinition(latest) |
| GET | /definitions/{id}/versions |
queryDefinitionVersions |
| GET | /definitions/{id}/versions/{version} |
findDefinition(id, version) |
| POST | /instances |
start |
| GET | /instances |
queryInstances |
| GET | /instances/{id} |
findInstance |
| POST | /instances/{id}/withdraw |
withdraw |
| POST | /instances/{id}/cancel |
cancel |
| GET | /instances/{id}/tasks |
findTasks |
| GET | /instances/{id}/tasks/pending |
findPendingTasksByInstanceId |
| GET | /instances/{id}/history |
queryHistory |
| GET | /instances/{id}/action-executions |
queryActionExecutions |
| GET | /tasks |
queryTasks |
| GET | /tasks/pending?assignee= |
findPendingTasksByAssignee |
| GET | /tasks/{id} |
findTask |
| POST | /tasks/{id}/approve |
approve |
| POST | /tasks/{id}/reject |
reject |
| POST | /tasks/{id}/reassign |
reassign |
| POST | /due |
processDue(默认 limit 100) |
| GET | /catalog/conditions|actions|assignees |
OrdoCatalog |
定义读写 JSON 与 §3.2 相同。HTTP:404 找不到;403 越权;409 已完成/无路由;400 非法参数。
3. 流程定义
每个定义有 id、引擎分配的 version、name、步骤列表、转移列表。步骤 id 在定义内唯一。每个步骤必须至少有一条出边(结束用 to = null)。同一 from 上 priority 不能重复。
3.1 代码构建
线性(每步无条件进下一步,最后一步结束):
ordo.publish(ProcessDefinition.linear("leave-request", "Leave request", List.of(
ApprovalStep.single("manager", "Manager approval", "maria"),
ApprovalStep.single("hr", "HR approval", "henry")
)));
图(条件边 + ACTION):
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", RoutingPredicate.gt("days", 3), 0),
new StepTransition("manager", "notify-approved", null, 1),
StepTransition.always("hr", "notify-approved"),
StepTransition.end("notify-approved")
));
publish:该 id 尚无版本则写入 v1;与 latest 的 id/name/steps/transitions 相同则返回 latest 不插入;否则插入 latest + 1。运行中实例不阻止发布。调用方构造的 ProcessDefinition 版本为 0;入库后由引擎分配从 1 起的单调版本。JSON 不要写 version,出现则忽略。
start(definitionId) 使用 latest。实例带 definitionVersion;审批、到期、撤回、取消均按该版本取图,不跟随后续 publish。
3.2 JSON
ProcessDefinitionParser.fromJson(String|InputStream) / toJson(ProcessDefinition)。Spring 默认扫 classpath*:ordo/*.json。toJson 写出 version 与 startStep(当前步骤列表首位);fromJson 仍忽略 JSON 里的 version。
{
"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": { "gt": ["days", 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 或 PARALLEL。 |
branches |
仅 PARALLEL。至少 2 条分支;每条含 id、steps、transitions。禁止嵌套 PARALLEL。 |
candidates / policy |
仅审批步。policy 默认 ANY。审批步至少一名候选人,禁止重复。 |
action |
仅 ACTION 步,对应 ActionHandler.execute 的 key。审批步禁止带 action。 |
due |
仅审批步。可选。after 为 ISO-8601 时长;then 为 reassign / notify / goto。进入该步时任务 dueAt = now + after。 |
from / to / when / priority |
to 为 null 或 JSON null 表示结束。when 省略则无条件(else)。priority 默认 0,仅在同类边之间越小越先匹配(条件边一组,无条件边一组)。 |
4. 实例与任务
4.1 生命周期
start → RUNNING
审批步:为每个候选人建 PENDING 任务
PARALLEL 步:为每条分支建立令牌并同时进入分支起点
ACTION 步:事务内记 PENDING 执行记录,提交后调 ActionHandler
approve / 路由结束 → APPROVED
reject(步被否决)→ REJECTED
withdraw(仅发起人)→ WITHDRAWN,剩余 PENDING 任务 → SKIPPED
cancel(任意非空 actor)→ CANCELLED,剩余 PENDING 任务 → SKIPPED
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", "计划有变");
ordo.cancel(instance.id(), "admin", "政策变更");
start 的 initiator、办理人 actor 均不可空白。
4.2 权限
approve/reject/reassign:actor必须等于该任务当前assignee,否则UnauthorizedTaskOperationException。reassign:仅PENDING任务;同一任务 id,办理人改为newAssignee,不推进步骤。newAssignee不可空白、不可等于当前assignee,且同一步不能已有该人的PENDING任务,否则IllegalArgumentException。不经过AssigneeResolver。人工转派不改dueAt。processDue(limit):认领dueAt <= now的 PENDING 任务(limit > 0),按步上due.then执行:reassign换办理人(to走AssigneeResolver)、notify可选ActionHandler、goto跳过当前步 PENDING 并进入to步骤(to必须是步骤 id)。每种策略对一张任务最多成功一次(清空dueAt)。引擎无后台线程;Spring 下ordo.due.poll-ms > 0才轮询。- 任务非
PENDING:TaskAlreadyCompletedException。 withdraw:仅initiator,否则UnauthorizedInstanceOperationException;实例非RUNNING:InstanceAlreadyCompletedException。cancel:actor非空即可,不校验是否发起人;实例须为RUNNING,否则InstanceAlreadyCompletedException。谁能调用由宿主决定。
没有管理员代批。
4.3 状态
实例 ProcessStatus:RUNNING、APPROVED、REJECTED、WITHDRAWN、CANCELLED。
任务 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, runtime),例如把角色名解析成用户 id。默认实现原样返回 candidate。ProcessRuntime 含 instanceId、definitionId、definitionVersion、initiator、当前 stepId 和业务 context。发起人等引擎元数据不写入 ProcessContext.variables。
连续 ACTION 步会在同一次提交后依次执行,上限 32 跳(每个 PARALLEL 分支各自计数),超出抛 IllegalStateException。
5.1 结构化并行(PARALLEL)
主图仍是单线。kind: PARALLEL 是一个步骤,块内多条分支同时推进;全部完成后走该步在父图上的出边。不是 BPMN fork/join 网关。
JSON 嵌套写,引擎拍平存储。分支内边 to: null 表示该分支完成并等待 join,不是实例通过。PARALLEL 步自己的出边 to: null 才结束实例。
v1:至少 2 条分支;禁止套娃 PARALLEL;join 固定 ALL;任一分支按现有规则否决则整单 REJECTED 并 SKIPPED 其余 PENDING;due.goto 只能指向同一分支内的步骤。会签仍用单步 candidates + ANY/ALL。
事件:PARALLEL_ENTERED、BRANCH_COMPLETED(detail 为 branch id)、PARALLEL_JOINED。
6. 条件路由
离开当前步时,取出全部 from = 当前步 的边。先按 priority 升序匹配带 when 的边(谓词为真,或 ref 且 routingCondition.matches 为真);都未命中再按 priority 走无 when 的边(else)。无条件边不再与条件边抢数字顺序。
when 必须是 JSON 对象,不能再写字符串 key。谓词闭集:eq/ne/gt/gte/lt/lte、in、and/or/not(深度 ≤ 8、叶子 ≤ 32)。宿主复杂逻辑用 ref + args,例如 { "ref": "amountGt", "args": { "threshold": 50000 } }。
没有匹配边:NoRouteFoundException,整次 approve/start 事务回滚(JDBC 下任务/实例都不会半更新)。
无条件边是默认分支;priority 只在同类边之间比较(多条条件边之间,或多条无条件边之间)。
RoutingCondition.matches 看到 args 与 ProcessRuntime(含业务 context、发起人、定义与当前步)。看不到任务意见。上下文在 start 时写入,运行中引擎不会改 context。内置谓词仍只读 ProcessContext 变量。
引擎仍只注入一个 RoutingCondition。Spring 下注册多个 NamedCondition Bean(key() 对应 when.ref);无门面且列表非空时装配官方分发器。未知 ref 为 false,该边不匹配。重复 key 启动失败。也可继续提供单个 RoutingCondition 门面自行分发(不要与 NamedCondition 混用)。无 NamedCondition 时默认仍是恒 true。
7. ACTION 步骤
进入 ACTION 步时:
- 事务内插入
ActionExecution,状态PENDING。 - 按转移进入下一步或结束实例(仍在同一事务)。
- 事务提交后调用
ActionHandler.execute(actionKey, runtime)。 - 成功 →
SUCCESS+ 事件ACTION_SUCCEEDED;失败 →FAILED(errorMessage)+ACTION_FAILED+ 日志 WARNING。
失败不回滚已提交的审批,不阻塞后续步骤,引擎不做重试。 宿主用 queryActionExecutions 或 listener 自行补发。分发器遇到未知 actionKey 会抛 IllegalArgumentException,记为该次 ACTION FAILED。
import com.jetlumen.ordo.api.runtime.ProcessRuntime;
import com.jetlumen.ordo.api.spi.NamedAction;
import org.springframework.stereotype.Component;
@Component
public class LeaveApprovedMail implements NamedAction {
@Override
public String key() {
return "leave-approved-mail";
}
@Override
public void execute(ProcessRuntime runtime) {
/* ... */
}
}
仍可提供单个 ActionHandler 按 key 自行分发;不要与 NamedAction 混用。
8. 审计与监听
queryHistory(instanceId, page):该实例事件,发生时间升序。
ProcessEventType:
INSTANCE_STARTED/INSTANCE_APPROVED/INSTANCE_REJECTED/INSTANCE_WITHDRAWN/INSTANCE_CANCELLEDTASK_CREATED/TASK_APPROVED/TASK_REJECTED/TASK_SKIPPED/TASK_REASSIGNED/TASK_ESCALATEDACTION_SUCCEEDED/ACTION_FAILED
字段:id、instanceId、可选 taskId/stepId/actor/detail、occurredAt。系统完成类事件 actor 可为空。TASK_REASSIGNED 的 actor 为转出人,detail 为转入人。TASK_ESCALATED 的 actor 为空,detail 为新办理人 / notify 的 action key / goto 目标步 id。
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 的 latest | 定义 id 升序 |
queryDefinitionVersions(id, PageRequest) |
单 id 全部版本 | version 降序 |
queryHistory(instanceId, PageRequest) |
单实例 | 发生时间升序 |
queryActionExecutions(instanceId, PageRequest) |
单实例 | 开始时间升序 |
TaskQuery.any().withAssignee("maria").withStatus(TaskStatus.PENDING) 等 with 方法返回新对象。字段 null 表示不按该维过滤。
便捷方法(不分页):findInstance、findTask、findTasks、findPendingTasksByAssignee、findPendingTasksByInstanceId、findDefinition(id) / findDefinition(id, version)。
10. OrdoEngine 一览
| 方法 | 说明 |
|---|---|
publish |
发布不可变图版本(相等则 no-op) |
start |
发起;可选 ProcessContext |
approve / reject |
办理当前 PENDING 任务 |
reassign |
当前办理人把 PENDING 任务转给他人 |
processDue |
认领并处理已到期 PENDING 任务 |
withdraw |
发起人撤回 |
cancel |
管理员/系统取消(引擎不鉴权角色) |
find* |
按 id / 待办索引读取 |
queryTasks / queryInstances / queryDefinitions / queryDefinitionVersions |
分页列表 |
queryHistory / queryActionExecutions |
实例审计与 ACTION 记录 |
11. 异常
均继承 OrdoException(unchecked)。
| 类型 | 何时 |
|---|---|
DefinitionNotFoundException |
start 等找不到 latest,或实例锁定的 version 不存在 |
InstanceNotFoundException |
撤回/取消等找不到实例 |
InstanceAlreadyCompletedException |
对非 RUNNING 实例完成/撤回/取消 |
TaskNotFoundException |
任务 id 不存在 |
TaskAlreadyCompletedException |
重复办理或已被 SKIPPED |
UnauthorizedTaskOperationException |
actor ≠ 当前 assignee(approve / reject / reassign) |
UnauthorizedInstanceOperationException |
非发起人撤回 |
NoRouteFoundException |
当前步没有匹配转移 |
参数空白等会抛 IllegalArgumentException,不属于 OrdoException。
12. 存储
Flyway 脚本按方言分目录:db/postgresql/migration、db/mysql/migration。未设置 spring.flyway.locations 时,starter 按探测到的方言指向对应目录。已有 flyway_schema_history 的开发库按版本追加迁移(PARALLEL 为 V3__parallel_blocks.sql)。表包括流程头 ordo_process、按 (id, version) 存储的定义/步骤(含 parent_step_id / branch_id)/候选人/转移、实例(含 definition_version)、任务、并行令牌 ordo_instance_token、ordo_process_event、ordo_action_execution。
自定义方言:实现 SqlDialect 并用 META-INF/services 注册,或提供 SqlDialect Bean。新增列时每个已支持方言目录各加一条迁移。
多 JVM 共享同一库时,多步写入走 TransactionExecutor,完成任务/实例用条件更新(仍 PENDING / 仍 RUNNING 才改),避免双花。
13. 未提供能力
独立设计器不进本仓库,等 REST 与目录之后由单独产品消费(见 roadmap.md)。
暂不在计划中:多租户、设计器 UI。