# Ordo 使用说明 本文档是 Ordo 对外行为的使用说明真源。**新增或变更对外 API、JSON 定义、扩展点、异常或运行时语义时,必须同步更新本文。** 未实现能力写在 [roadmap.md](roadmap.md),不要在这里当已支持功能描述。 版本:`0.0.1-SNAPSHOT`。语言:Java 17+。 ## 1. 定位 Ordo 是嵌入宿主进程的审批引擎,入口是 `OrdoEngine`。 做:流程定义、实例推进、待办任务、条件路由、ACTION 副作用、审计事件、分页查询。 产品边界:不做业务表单、用户体系、多租户;不内置设计器 UI。业务字段放在 `ProcessContext`(不可变 `Map`)。可选 HTTP 见 §2.3;独立设计器见 [roadmap.md](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` 内存演示 | 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 内存引擎 ```xml com.jetlumen ordo-core 0.0.1-SNAPSHOT ``` ```java OrdoEngine ordo = new InMemoryOrdoEngine(); // 可选:new InMemoryOrdoEngine(clock, assigneeResolver, routingCondition, actionHandler, listeners) ``` 进程退出数据即丢失。适合单测和本地试跑。 ### 2.2 Spring Boot(JDBC) ```xml com.jetlumen ordo-spring-boot-starter 0.0.1-SNAPSHOT ``` 需要已有 `DataSource`。`ordo.enabled` 缺省为 `true`。 ```yaml 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` | 候选人字符串即办理人 | | `RoutingCondition` | `ref` 边默认恒 true;无 `when` 的边不经过该 Bean | | `ActionHandler` | 空操作 | | `OrdoEventListener` | 可有多个,提交后按 Spring 顺序调用 | | `OrdoCatalog` | 空列表(仅 REST 打开时装配) | 未提供自定义 Bean 时,ACTION 步骤仍会推进流程,但 handler 什么都不做。 ### 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-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 代码构建 线性(每步无条件进下一步,最后一步结束): ```java ordo.publish(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", 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`。 ```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": { "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 ``` ```java ProcessInstance instance = ordo.start("leave-request", "alice", new ProcessContext(Map.of("requestId", "LEAVE-001", "days", 2))); List 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。未知 `ref` 由宿主返回 `false`,该边不匹配。内置谓词仍只读 `ProcessContext` 变量。 引擎只注入**一个** `RoutingCondition`。Spring 下多个该类型 Bean 会冲突。宿主用一个门面按 `ref` 分发;不要指望引擎按定义拆 bean。 ## 7. ACTION 步骤 进入 ACTION 步时: 1. 事务内插入 `ActionExecution`,状态 `PENDING`。 2. 按转移进入下一步或结束实例(仍在同一事务)。 3. 事务提交后调用 `ActionHandler.execute(actionKey, runtime)`。 4. 成功 → `SUCCESS` + 事件 `ACTION_SUCCEEDED`;失败 → `FAILED`(`errorMessage`)+ `ACTION_FAILED` + 日志 WARNING。 **失败不回滚已提交的审批,不阻塞后续步骤,引擎不做重试。** 宿主用 `queryActionExecutions` 或 listener 自行补发。 ```java @Component public class MailActions implements ActionHandler { @Override public void execute(String actionKey, ProcessRuntime runtime) { 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` / `INSTANCE_CANCELLED` - `TASK_CREATED` / `TASK_APPROVED` / `TASK_REJECTED` / `TASK_SKIPPED` / `TASK_REASSIGNED` / `TASK_ESCALATED` - `ACTION_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](roadmap.md))。 暂不在计划中:多租户、设计器 UI。