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

14 KiB
Raw Blame History

Ordo 使用说明

本文档是 Ordo 对外行为的使用说明真源。新增或变更对外 API、JSON 定义、扩展点、异常或运行时语义时,必须同步更新本文。 未实现能力写在 roadmap.md,不要在这里当已支持功能描述。

版本:0.0.1-SNAPSHOT。语言:Java 17+。

1. 定位

Ordo 是嵌入宿主进程的审批引擎,入口是 OrdoEngine。

做:流程定义、实例推进、待办任务、条件路由、ACTION 副作用、审计事件、分页查询。

产品边界:不做业务表单、用户体系、多租户;不内置设计器 UI。业务字段放在 ProcessContext(不可变 Map<String, Object>)。当前也没有 REST;HTTP 仍由宿主自建。计划中的可选 REST 与独立设计器见 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 内存引擎

<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
  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 代码构建

线性(每步无条件进下一步,最后一步结束):

ordo.register(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", "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。

{
  "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
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 自行补发。

@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):到期升级、CANCELLED、定义不可变多版本、MySQL 方言、可选 REST + 目录 SPI。独立设计器不进本仓库,等 REST、目录与多版本定义之后再做。

暂不在计划中:多租户、设计器 UI。当前 REST 由宿主自建。