diff --git a/.idea/misc.xml b/.idea/misc.xml index de41ab4..064e351 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -8,7 +8,7 @@ - + \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..c29f973 --- /dev/null +++ b/README.md @@ -0,0 +1,145 @@ +# Ordo + +轻量审批流程引擎。宿主通过 `OrdoEngine` 注册流程定义、发起实例、审批/驳回/撤回,并查询任务、实例与审计历史。引擎不绑定业务表单,也不自带 REST;业务数据放在 `ProcessContext` 里。 + +要求 **Java 17+**。当前版本 `0.0.1-SNAPSHOT`。 + +## 模块 + +| 模块 | 作用 | +|---|---| +| `ordo-api` | 公共模型与 `OrdoEngine` 端口 | +| `ordo-core` | 运行时(`DefaultOrdoEngine` / `InMemoryOrdoEngine`) | +| `ordo-storage-jdbc` | JDBC 存储 + Flyway 迁移(V1–V5) | +| `ordo-spring-boot-starter` | Spring Boot 自动装配(JDBC + Flyway) | +| `ordo-example` | 内存引擎示例 | + +存储实现面向 **PostgreSQL**(测试可用 H2 PostgreSQL 兼容模式)。Starter **不携带** JDBC 驱动,宿主自行加入 `postgresql` 或 `h2`。Spring Boot 4 还需额外引入 `spring-boot-starter-flyway`,否则迁移不会跑。 + +## 能力 + +- 线性或多步图:`StepTransition` + 可选 `RoutingCondition` +- 会签/或签:`ApprovalPolicy.ALL` / `ANY`(多候选人) +- ACTION 步骤:事务提交后调用宿主 `ActionHandler` +- 发起人撤回:`WITHDRAWN`,待办任务 `SKIPPED` +- 分页查询:任务 / 实例 / 流程定义 +- 审计时间线:`ProcessEvent` + `queryHistory` +- 扩展点:`AssigneeResolver`、`RoutingCondition`、`ActionHandler`、`OrdoEventListener` + +开发计划:任务转派、到期升级、`CANCELLED`、定义不可变多版本、MySQL 方言。多租户与官方 REST Starter **暂不在计划中**。见 [docs/roadmap.md](docs/roadmap.md)。 + +详细用法(定义 JSON、扩展点、异常、查询、ACTION/审计语义)见 **[docs/usage.md](docs/usage.md)**。对外行为变更时同步更新该文档。 + +## 内存快速开始 + +```xml + + com.jetlumen + ordo-core + 0.0.1-SNAPSHOT + +``` + +```java +OrdoEngine ordo = new InMemoryOrdoEngine(); +ordo.register(ProcessDefinition.linear("leave-request", "Leave request", List.of( + ApprovalStep.single("manager", "Manager approval", "maria"), + ApprovalStep.single("hr", "HR approval", "henry") +))); + +ProcessInstance instance = ordo.start("leave-request", "alice", + new ProcessContext(Map.of("requestId", "LEAVE-2026-001"))); +ApprovalTask manager = ordo.findPendingTasksByInstanceId(instance.id()).get(0); +ordo.approve(manager.id(), "maria", "ok"); +``` + +完整示例:`ordo-example` 的 `LeaveRequestExample`。 + +## JSON 流程定义 + +```json +{ + "id": "leave-request-routed", + "name": "Leave request", + "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 } + ] +} +``` + +- `kind` 默认 `APPROVAL`;ACTION 用 `"action"` 作为 handler 查找键。 +- 转移按 `priority` 升序匹配;`when` 为空或条件命中则走该边;`to: null` 表示结束。 +- 代码侧可用 `ProcessDefinitionParser.fromJson(...)`。 +- `replace` 会整体替换同 id 定义;存在 `RUNNING` 实例时拒绝替换。 + +## Spring Boot + +```xml + + com.jetlumen + ordo-spring-boot-starter + 0.0.1-SNAPSHOT + +``` + +需要 `DataSource`。存在 `DataSource` 且 `ordo.enabled` 不为 `false` 时装配 JDBC 引擎。 + +```yaml +ordo: + enabled: true + definitions: + location: classpath*:ordo/*.json # 默认值;启动时 replace 加载 +``` + +宿主提供 Bean 即可覆盖默认值: + +| Bean | 默认 | +|---|---| +| `AssigneeResolver` | 候选人即办理人 | +| `RoutingCondition` | 始终匹配 | +| `ActionHandler` | 空操作 | +| `OrdoEventListener` | 可注册多个,提交后按顺序调用 | + +## 运行时约定 + +**办理人** 必须等于任务 `assignee`,否则 `UnauthorizedTaskOperationException`。 +**撤回** 仅发起人可操作,且实例须为 `RUNNING`。 + +实例状态:`RUNNING` / `APPROVED` / `REJECTED` / `WITHDRAWN`。 +任务状态:`PENDING` / `APPROVED` / `REJECTED` / `SKIPPED`。 + +**ACTION** 在审批事务提交之后执行。失败只记 `FAILED`、打日志、发 `ACTION_FAILED`,**不回滚已生效审批、不阻塞后续步骤**。可重试策略留给宿主(listener 或 `queryActionExecutions`)。 + +**历史** `queryHistory(instanceId, page)` 按时间升序。事件类型包括实例起止、任务创建/审批/跳过、ACTION 成败。 +`OrdoEventListener.onEvent` 在提交后派发;单个 listener 抛错不影响流程和其他 listener。 + +## 查询 + +```java +ordo.queryTasks(TaskQuery.any().withAssignee("maria").withStatus(TaskStatus.PENDING), + PageRequest.of(0, 20)); +ordo.queryInstances(InstanceQuery.any().withInitiator("alice"), PageRequest.of(0, 20)); +ordo.queryDefinitions(PageRequest.of(0, 20)); +ordo.queryHistory(instanceId, PageRequest.of(0, 50)); +ordo.queryActionExecutions(instanceId, PageRequest.of(0, 20)); +``` + +任务/实例列表默认最新优先;历史与 ACTION 执行记录按发生时间升序。 + +## 构建 + +```bash +mvn -pl ordo-api,ordo-core,ordo-storage-jdbc,ordo-spring-boot-autoconfigure,ordo-spring-boot-starter install +``` + +先 `install` 再给外部工程(如宿主应用)引用。 diff --git a/docs/roadmap.md b/docs/roadmap.md index 893e9fe..56385db 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,45 +1,50 @@ # Ordo Roadmap -记录当前已完成能力之后,后续要做的开发计划。按优先级分组,供后续排期/立项参考。 +本文档是后续开发计划。「开发计划」中的项会做。「暂不在计划中」的项在另有决定前不做。除此之外仍可能立项其他新特性。 -## 已完成(背景,非本文档重点) +对外行为落地后同步 [usage.md](usage.md)。 -- ANY/ALL 多候选人会签/或签(见 `/memories/repo/any-all-multi-approval.md`) -- JDBC 存储模块 + Flyway 迁移(V1~V4) +## 已完成 + +- ANY/ALL 会签/或签 +- JDBC 存储(PostgreSQL)+ Flyway V1–V5 - 条件路由 `StepTransition` + `RoutingCondition` -- ACTION 步骤 + `ActionHandler` -- 流程撤回(`WITHDRAWN`) -- **任务/实例/流程定义分页过滤查询 API**(2026-09-15 完成):`ApprovalTaskRepository.query`、 -`ProcessInstanceRepository.query`、`ProcessDefinitionRepository.findAll` + `OrdoEngine` 对应的 -`queryTasks`/`queryInstances`/`listDefinitions`,均支持 `PageRequest`/`Page` 分页与按 -assignee/instanceId/definitionId/status/initiator/时间范围过滤,默认按时间降序(最新优先)。 +- ACTION + `ActionHandler`;执行记录持久化 +- 发起人撤回 `WITHDRAWN` +- 分页查询:任务 / 实例 / 定义 +- 审计 `ProcessEvent` / `queryHistory`;`OrdoEventListener` -## P0 — 审计与扩展点 +## 开发计划(确定要做) -- [x] **独立历史/审计事件模型**:现在历史只能靠 `ApprovalTask.action` 字段拼凑,没有独立的流程事件表 - (谁在何时对哪个实例做了什么)。建议新增 `ProcessEvent`/`ProcessHistoryRepository`。 -- [x] **状态变更事件监听器**:`OrdoEngine` 目前没有任何 listener/hook,无法在任务创建、审批、实例完成时 - 被外部感知(做通知、写审计日志等)。可加 `OrdoEventListener` 扩展点,风格与 `AssigneeResolver`/ - `RoutingCondition` 一致。 -- [x] **ACTION 步骤执行记录持久化**:目前 `ActionHandler` 执行结果只在宿主内存里记(如 rhizome 的 - `LeaveActionHandler`),重启即丢失,且失败只打日志不影响流程状态,需要设计重试/失败处理策略。 +下列能力已纳入计划,尚未实现。实现顺序可按依赖调整,但范围本身不从计划中拿掉。 -## P1 — 任务生命周期完善 +### 任务转派 -- [ ] **任务委托/转派(delegate/reassign)**:任务创建后 assignee 不可变,无法转交他人处理。 -- [ ] **超时/升级(SLA/escalation)**:无到期时间、定时器、自动升级机制。 -- [ ] **流程实例取消 vs 撤回**:目前只有 `WITHDRAWN`(仅发起人可操作),没有管理员/系统层面的 - `CANCELLED` 语义。 +任务创建后当前 `assignee` 不可变。计划提供委托/转派(delegate/reassign),把待办转给他人办理,并写入审计。 -## P2 — 架构级演进(范围较大,放在后面) +### 到期升级 -- [ ] **流程定义版本化**:目前同 id 直接整体替换(`replace`),建议演进为不可变多版本 + 运行中实例 - 锁定所用版本。 -- [ ] **多租户支持**:数据模型无 tenant 隔离字段。 -- [ ] **JDBC 多方言支持**:目前 DDL/实现明显偏向 PostgreSQL(唯一键冲突处理等),无 MySQL/Testcontainers - 测试,若要支持更多数据库需要抽象 dialect 层。 -- [ ] **通用 REST Starter**:现在 REST 层完全是 rhizome 自己写的 demo,可考虑提供一个可选的 - `ordo-spring-boot-starter-web` 暴露标准 REST 接口。 -- [ ] **表单/UI schema、子流程、并行 fork-join**:属于更大的引擎能力扩展,优先级最低,等基础能力稳定后 - 再评估是否需要。 +目前无到期时间与定时器。计划支持 SLA/超时:到期后升级(改办理人、通知或进入指定步骤),即「到期升级」。 +### CANCELLED + +目前终态只有 `APPROVED` / `REJECTED` / `WITHDRAWN`(撤回仅发起人)。计划增加管理员/系统取消语义 `CANCELLED`,与撤回区分。 + +### 定义不可变多版本 + +当前同 `id` 用 `replace` 整体替换;存在 `RUNNING` 实例时拒绝。计划改为定义不可变多版本:新版本不改写旧版本;运行中实例锁定发起时所用版本。 + +### MySQL 方言 + +当前 JDBC DDL/冲突处理面向 PostgreSQL。计划增加 MySQL 方言(及对应测试),通过 dialect 层扩展,而不是只支持一种库。 + +## 暂不在计划中 + +- **多租户**(数据模型无 tenant 隔离) +- **官方 REST Starter**(不提供 `ordo-spring-boot-starter-web`;REST 由宿主自建) + +## 后续新特性 + +上表「确定要做」之外,仍可能立项其他能力(例如表单/UI schema、子流程、并行 fork-join)。**多租户与官方 REST 在另有明确决定前不进入计划。** + +新特性立项时写入「开发计划」对应小节;完成后移到「已完成」,并更新 [usage.md](usage.md)。 diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..84576e6 --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,344 @@ +# Ordo 使用说明 + +本文档是 Ordo 对外行为的使用说明真源。**新增或变更对外 API、JSON 定义、扩展点、异常或运行时语义时,必须同步更新本文。** 未实现能力写在 [roadmap.md](roadmap.md),不要在这里当已支持功能描述。 + +版本:`0.0.1-SNAPSHOT`。语言:Java 17+。 + +## 1. 定位 + +Ordo 是嵌入宿主进程的审批引擎,入口是 `OrdoEngine`。 + +做:流程定义、实例推进、待办任务、条件路由、ACTION 副作用、审计事件、分页查询。 + +产品边界(不做,且暂不在开发计划):业务表单、用户体系、官方 REST、多租户。业务字段放在 `ProcessContext`(不可变 `Map`)。 + +开发计划(尚未提供,见 [roadmap.md](roadmap.md)):任务转派、到期升级、`CANCELLED`、定义不可变多版本、MySQL 方言。 + +## 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 + + 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 + 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 pending = ordo.findPendingTasksByInstanceId(instance.id()); +ordo.approve(pending.get(0).id(), "maria", "ok"); + +ordo.reject(taskId, "maria", "额度不足"); +ordo.withdraw(instance.id(), "alice", "计划有变"); +``` + +`start` 的 `initiator`、办理人 `actor` 均不可空白。 + +### 4.2 权限 + +- `approve` / `reject`:`actor` 必须等于该任务当前 `assignee`,否则 `UnauthorizedTaskOperationException`。 +- 任务非 `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`,看不到任务意见。上下文在 `start` 时写入,运行中引擎**不会**改 context。 + +## 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` +- `ACTION_SUCCEEDED` / `ACTION_FAILED` + +字段:`id`、`instanceId`、可选 `taskId`/`stepId`/`actor`/`detail`、`occurredAt`。系统完成类事件 `actor` 可为空。 + +`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 任务 | +| `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 | +| `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 Starter。REST 由宿主自建。 diff --git a/ordo-api/src/main/java/com/jetlumen/ordo/api/OrdoEngine.java b/ordo-api/src/main/java/com/jetlumen/ordo/api/OrdoEngine.java index 5971a2f..76d3368 100644 --- a/ordo-api/src/main/java/com/jetlumen/ordo/api/OrdoEngine.java +++ b/ordo-api/src/main/java/com/jetlumen/ordo/api/OrdoEngine.java @@ -46,7 +46,7 @@ public interface OrdoEngine { Page queryInstances(InstanceQuery query, PageRequest pageRequest); /** Paginated listing of all registered process definitions. */ - Page listDefinitions(PageRequest pageRequest); + Page queryDefinitions(PageRequest pageRequest); /** Instance timeline, oldest-first; see {@link com.jetlumen.ordo.api.repository.ProcessHistoryRepository#query}. */ Page queryHistory(String instanceId, PageRequest pageRequest); diff --git a/ordo-api/src/main/java/com/jetlumen/ordo/api/ProcessDefinition.java b/ordo-api/src/main/java/com/jetlumen/ordo/api/ProcessDefinition.java index bd14f41..17d0839 100644 --- a/ordo-api/src/main/java/com/jetlumen/ordo/api/ProcessDefinition.java +++ b/ordo-api/src/main/java/com/jetlumen/ordo/api/ProcessDefinition.java @@ -59,7 +59,7 @@ public record ProcessDefinition(String id, String name, List steps transitions.add(StepTransition.always(steps.get(i).id(), steps.get(i + 1).id())); } if (!steps.isEmpty()) { - transitions.add(StepTransition.end(steps.getLast().id())); + transitions.add(StepTransition.end(steps.get(steps.size() - 1).id())); } return new ProcessDefinition(id, name, steps, transitions); } diff --git a/ordo-api/src/test/java/com/jetlumen/ordo/api/ProcessDefinitionParserTest.java b/ordo-api/src/test/java/com/jetlumen/ordo/api/ProcessDefinitionParserTest.java index 3e22279..ba98e4b 100644 --- a/ordo-api/src/test/java/com/jetlumen/ordo/api/ProcessDefinitionParserTest.java +++ b/ordo-api/src/test/java/com/jetlumen/ordo/api/ProcessDefinitionParserTest.java @@ -59,7 +59,7 @@ class ProcessDefinitionParserTest { """; ProcessDefinition definition = ProcessDefinitionParser.fromJson(json); - assertEquals("manager", definition.steps().getFirst().id()); + assertEquals("manager", definition.steps().get(0).id()); assertEquals("hr", definition.steps().get(1).id()); } @@ -171,7 +171,7 @@ class ProcessDefinitionParserTest { } """; ProcessDefinition definition = ProcessDefinitionParser.fromJson(json); - assertEquals(StepKind.APPROVAL, definition.steps().getFirst().kind()); + assertEquals(StepKind.APPROVAL, definition.steps().get(0).kind()); assertEquals(ApprovalStep.action("notify", "Notify HR", "leave-approved-mail"), definition.steps().get(1)); } } diff --git a/ordo-api/src/test/java/com/jetlumen/ordo/api/ProcessDefinitionTest.java b/ordo-api/src/test/java/com/jetlumen/ordo/api/ProcessDefinitionTest.java index 2178b1e..a50e5e8 100644 --- a/ordo-api/src/test/java/com/jetlumen/ordo/api/ProcessDefinitionTest.java +++ b/ordo-api/src/test/java/com/jetlumen/ordo/api/ProcessDefinitionTest.java @@ -115,7 +115,7 @@ class ProcessDefinitionTest { ProcessDefinition definition = new ProcessDefinition("notify", "Notify", List.of( ApprovalStep.action("mail", "Send mail", "leave-approved-mail")), List.of(StepTransition.end("mail"))); - assertEquals(StepKind.ACTION, definition.steps().getFirst().kind()); - assertEquals("leave-approved-mail", definition.steps().getFirst().actionKey()); + assertEquals(StepKind.ACTION, definition.steps().get(0).kind()); + assertEquals("leave-approved-mail", definition.steps().get(0).actionKey()); } } diff --git a/ordo-core/src/main/java/com/jetlumen/ordo/core/DefaultOrdoEngine.java b/ordo-core/src/main/java/com/jetlumen/ordo/core/DefaultOrdoEngine.java index 6827841..60e478b 100644 --- a/ordo-core/src/main/java/com/jetlumen/ordo/core/DefaultOrdoEngine.java +++ b/ordo-core/src/main/java/com/jetlumen/ordo/core/DefaultOrdoEngine.java @@ -136,7 +136,7 @@ public final class DefaultOrdoEngine implements OrdoEngine { ProcessStatus.RUNNING, now, null, context); instanceRepository.insert(started); record(events, started.id(), null, null, ProcessEventType.INSTANCE_STARTED, initiator, null, now); - enterStep(started, definition, definition.steps().getFirst(), now, queued, events); + enterStep(started, definition, definition.steps().get(0), now, queued, events); return started; }); finishCommittedWork(queued, events); @@ -252,7 +252,7 @@ public final class DefaultOrdoEngine implements OrdoEngine { } @Override - public synchronized Page listDefinitions(PageRequest pageRequest) { + public synchronized Page queryDefinitions(PageRequest pageRequest) { Objects.requireNonNull(pageRequest, "pageRequest must not be null"); return definitionRepository.findAll(pageRequest); } diff --git a/ordo-core/src/main/java/com/jetlumen/ordo/core/InMemoryOrdoEngine.java b/ordo-core/src/main/java/com/jetlumen/ordo/core/InMemoryOrdoEngine.java index ac20939..f33819c 100644 --- a/ordo-core/src/main/java/com/jetlumen/ordo/core/InMemoryOrdoEngine.java +++ b/ordo-core/src/main/java/com/jetlumen/ordo/core/InMemoryOrdoEngine.java @@ -141,8 +141,8 @@ public final class InMemoryOrdoEngine implements OrdoEngine { } @Override - public Page listDefinitions(PageRequest pageRequest) { - return delegate.listDefinitions(pageRequest); + public Page queryDefinitions(PageRequest pageRequest) { + return delegate.queryDefinitions(pageRequest); } @Override diff --git a/ordo-core/src/test/java/com/jetlumen/ordo/core/InMemoryOrdoEngineTest.java b/ordo-core/src/test/java/com/jetlumen/ordo/core/InMemoryOrdoEngineTest.java index 127b281..6c44d1c 100644 --- a/ordo-core/src/test/java/com/jetlumen/ordo/core/InMemoryOrdoEngineTest.java +++ b/ordo-core/src/test/java/com/jetlumen/ordo/core/InMemoryOrdoEngineTest.java @@ -57,7 +57,7 @@ class InMemoryOrdoEngineTest { @Test void completesASequentialApprovalProcess() { var instance = engine.start("leave", "alice"); - ApprovalTask managerTask = engine.findTasks(instance.id()).getFirst(); + ApprovalTask managerTask = engine.findTasks(instance.id()).get(0); assertEquals(TaskStatus.APPROVED, engine.approve(managerTask.id(), "maria").status()); ApprovalTask hrTask = engine.findTasks(instance.id()).get(1); @@ -70,7 +70,7 @@ class InMemoryOrdoEngineTest { @Test void rejectionTerminatesTheProcess() { var instance = engine.start("leave", "alice"); - ApprovalTask task = engine.findTasks(instance.id()).getFirst(); + ApprovalTask task = engine.findTasks(instance.id()).get(0); ApprovalTask rejectedTask = engine.reject(task.id(), "maria", "Insufficient leave balance"); @@ -86,7 +86,7 @@ class InMemoryOrdoEngineTest { @Test void rejectsDuplicateAndUnauthorizedOperations() { var instance = engine.start("leave", "alice"); - ApprovalTask task = engine.findTasks(instance.id()).getFirst(); + ApprovalTask task = engine.findTasks(instance.id()).get(0); assertThrows(UnauthorizedTaskOperationException.class, () -> engine.approve(task.id(), "mallory")); engine.approve(task.id(), "maria"); @@ -108,7 +108,7 @@ class InMemoryOrdoEngineTest { ApprovalStep.single("director", "Director approval", "diana")))); var instance = empty.start("expense", "alice"); - assertEquals("diana", empty.findTasks(instance.id()).getFirst().assignee()); + assertEquals("diana", empty.findTasks(instance.id()).get(0).assignee()); } @Test @@ -117,7 +117,7 @@ class InMemoryOrdoEngineTest { ApprovalStep.single("director", "Director approval", "diana")))); var instance = engine.start("leave", "alice"); - ApprovalTask task = engine.findTasks(instance.id()).getFirst(); + ApprovalTask task = engine.findTasks(instance.id()).get(0); assertEquals("director", task.stepId()); assertEquals("diana", task.assignee()); } @@ -132,19 +132,19 @@ class InMemoryOrdoEngineTest { @Test void replaceSucceedsAfterInstancesReachATerminalStatus() { var instance = engine.start("leave", "alice"); - engine.reject(engine.findTasks(instance.id()).getFirst().id(), "maria"); + engine.reject(engine.findTasks(instance.id()).get(0).id(), "maria"); engine.replace(ProcessDefinition.linear("leave", "Leave request v2", List.of( ApprovalStep.single("director", "Director approval", "diana")))); var next = engine.start("leave", "bob"); - assertEquals("diana", engine.findTasks(next.id()).getFirst().assignee()); + assertEquals("diana", engine.findTasks(next.id()).get(0).assignee()); } @Test void initiatorCanWithdrawWhileALaterStepIsPending() { var instance = engine.start("leave", "alice"); - engine.approve(engine.findTasks(instance.id()).getFirst().id(), "maria"); + engine.approve(engine.findTasks(instance.id()).get(0).id(), "maria"); ProcessInstance withdrawn = engine.withdraw(instance.id(), "alice", "changed plans"); @@ -172,7 +172,7 @@ class InMemoryOrdoEngineTest { () -> engine.withdraw(instance.id(), "mallory")); assertThrows(InstanceNotFoundException.class, () -> engine.withdraw("missing", "alice")); - engine.reject(engine.findTasks(instance.id()).getFirst().id(), "maria"); + engine.reject(engine.findTasks(instance.id()).get(0).id(), "maria"); assertThrows(InstanceAlreadyCompletedException.class, () -> engine.withdraw(instance.id(), "alice")); } @@ -182,7 +182,7 @@ class InMemoryOrdoEngineTest { assertThrows(DefinitionNotFoundException.class, () -> engine.start(" ", "alice")); var instance = engine.start("leave", "alice"); - ApprovalTask task = engine.findTasks(instance.id()).getFirst(); + ApprovalTask task = engine.findTasks(instance.id()).get(0); assertThrows(IllegalArgumentException.class, () -> engine.approve(task.id(), " ")); assertThrows(IllegalArgumentException.class, () -> engine.reject(task.id(), " ")); } @@ -191,7 +191,7 @@ class InMemoryOrdoEngineTest { void keepsSeparateInstancesIndependent() { var aliceInstance = engine.start("leave", "alice"); var bobInstance = engine.start("leave", "bob"); - ApprovalTask aliceTask = engine.findTasks(aliceInstance.id()).getFirst(); + ApprovalTask aliceTask = engine.findTasks(aliceInstance.id()).get(0); engine.approve(aliceTask.id(), "maria"); @@ -199,7 +199,7 @@ class InMemoryOrdoEngineTest { assertEquals(1, engine.findTasks(bobInstance.id()).size()); assertEquals(ProcessStatus.RUNNING, engine.findInstance(aliceInstance.id()).orElseThrow().status()); assertEquals(ProcessStatus.RUNNING, engine.findInstance(bobInstance.id()).orElseThrow().status()); - assertEquals("maria", engine.findTasks(bobInstance.id()).getFirst().assignee()); + assertEquals("maria", engine.findTasks(bobInstance.id()).get(0).assignee()); assertTrue(engine.findTasks("missing-instance").isEmpty()); } @@ -207,14 +207,14 @@ class InMemoryOrdoEngineTest { void findsOnlyCurrentPendingTasksByAssigneeAndInstance() { var aliceInstance = engine.start("leave", "alice"); var bobInstance = engine.start("leave", "bob"); - ApprovalTask aliceManagerTask = engine.findPendingTasksByInstanceId(aliceInstance.id()).getFirst(); + ApprovalTask aliceManagerTask = engine.findPendingTasksByInstanceId(aliceInstance.id()).get(0); assertEquals(2, engine.findPendingTasksByAssignee("maria").size()); assertEquals(List.of(aliceManagerTask), engine.findPendingTasksByInstanceId(aliceInstance.id())); assertTrue(engine.findPendingTasksByAssignee("henry").isEmpty()); engine.approve(aliceManagerTask.id(), "maria"); - ApprovalTask hrTask = engine.findPendingTasksByInstanceId(aliceInstance.id()).getFirst(); + ApprovalTask hrTask = engine.findPendingTasksByInstanceId(aliceInstance.id()).get(0); assertEquals("hr", hrTask.stepId()); assertEquals("henry", hrTask.assignee()); @@ -230,7 +230,7 @@ class InMemoryOrdoEngineTest { @Test void recordsApprovalAuditDataAndLeavesPendingTasksWithoutAnAction() { var instance = engine.start("leave", "alice"); - ApprovalTask managerTask = engine.findPendingTasksByInstanceId(instance.id()).getFirst(); + ApprovalTask managerTask = engine.findPendingTasksByInstanceId(instance.id()).get(0); assertNull(managerTask.action()); ApprovalTask approvedTask = engine.approve(managerTask.id(), "maria", " Approved for the requested dates. "); @@ -262,11 +262,11 @@ class InMemoryOrdoEngineTest { "manager", "david", "hr", "helena" ))); - ApprovalTask managerTask = contextAwareEngine.findPendingTasksByInstanceId(instance.id()).getFirst(); + ApprovalTask managerTask = contextAwareEngine.findPendingTasksByInstanceId(instance.id()).get(0); assertEquals("david", managerTask.assignee()); contextAwareEngine.approve(managerTask.id(), "david"); - assertEquals("helena", contextAwareEngine.findPendingTasksByInstanceId(instance.id()).getFirst().assignee()); + assertEquals("helena", contextAwareEngine.findPendingTasksByInstanceId(instance.id()).get(0).assignee()); assertEquals("david", instance.context().value("manager").orElseThrow()); } @@ -293,7 +293,7 @@ class InMemoryOrdoEngineTest { .filter(t -> t.id().equals(mikeTask.id())).findFirst().orElseThrow().status()); assertThrows(TaskAlreadyCompletedException.class, () -> anyEngine.approve(mikeTask.id(), "mike")); - ApprovalTask hrTask = anyEngine.findPendingTasksByInstanceId(instance.id()).getFirst(); + ApprovalTask hrTask = anyEngine.findPendingTasksByInstanceId(instance.id()).get(0); assertEquals("hr", hrTask.stepId()); anyEngine.approve(hrTask.id(), "henry"); assertEquals(ProcessStatus.APPROVED, anyEngine.findInstance(instance.id()).orElseThrow().status()); @@ -338,7 +338,7 @@ class InMemoryOrdoEngineTest { assertEquals(ProcessStatus.RUNNING, allEngine.findInstance(instance.id()).orElseThrow().status()); allEngine.approve(mikeTask.id(), "mike"); - ApprovalTask hrTask = allEngine.findPendingTasksByInstanceId(instance.id()).getFirst(); + ApprovalTask hrTask = allEngine.findPendingTasksByInstanceId(instance.id()).get(0); assertEquals("hr", hrTask.stepId()); allEngine.approve(hrTask.id(), "henry"); assertEquals(ProcessStatus.APPROVED, allEngine.findInstance(instance.id()).orElseThrow().status()); @@ -385,13 +385,13 @@ class InMemoryOrdoEngineTest { StepTransition.end("director")))); var high = routingEngine.start("expense", "alice", new ProcessContext(Map.of("amount", 2500))); - routingEngine.approve(routingEngine.findPendingTasksByInstanceId(high.id()).getFirst().id(), "maria"); - assertEquals("director", routingEngine.findPendingTasksByInstanceId(high.id()).getFirst().stepId()); - routingEngine.approve(routingEngine.findPendingTasksByInstanceId(high.id()).getFirst().id(), "diana"); + routingEngine.approve(routingEngine.findPendingTasksByInstanceId(high.id()).get(0).id(), "maria"); + assertEquals("director", routingEngine.findPendingTasksByInstanceId(high.id()).get(0).stepId()); + routingEngine.approve(routingEngine.findPendingTasksByInstanceId(high.id()).get(0).id(), "diana"); assertEquals(ProcessStatus.APPROVED, routingEngine.findInstance(high.id()).orElseThrow().status()); var low = routingEngine.start("expense", "bob", new ProcessContext(Map.of("amount", 200))); - routingEngine.approve(routingEngine.findPendingTasksByInstanceId(low.id()).getFirst().id(), "maria"); + routingEngine.approve(routingEngine.findPendingTasksByInstanceId(low.id()).get(0).id(), "maria"); assertEquals(ProcessStatus.APPROVED, routingEngine.findInstance(low.id()).orElseThrow().status()); assertTrue(routingEngine.findPendingTasksByInstanceId(low.id()).isEmpty()); } @@ -411,10 +411,10 @@ class InMemoryOrdoEngineTest { StepTransition.end("hr")))); var instance = actionEngine.start("leave", "alice"); - actionEngine.approve(actionEngine.findPendingTasksByInstanceId(instance.id()).getFirst().id(), "maria"); + actionEngine.approve(actionEngine.findPendingTasksByInstanceId(instance.id()).get(0).id(), "maria"); assertEquals(List.of("leave-approved-mail"), executed); - ApprovalTask hrTask = actionEngine.findPendingTasksByInstanceId(instance.id()).getFirst(); + ApprovalTask hrTask = actionEngine.findPendingTasksByInstanceId(instance.id()).get(0); assertEquals("hr", hrTask.stepId()); assertEquals(ProcessStatus.RUNNING, actionEngine.findInstance(instance.id()).orElseThrow().status()); } @@ -433,7 +433,7 @@ class InMemoryOrdoEngineTest { StepTransition.end("notify")))); var instance = actionEngine.start("leave", "alice"); - actionEngine.approve(actionEngine.findPendingTasksByInstanceId(instance.id()).getFirst().id(), "maria"); + actionEngine.approve(actionEngine.findPendingTasksByInstanceId(instance.id()).get(0).id(), "maria"); assertEquals(ProcessStatus.APPROVED, actionEngine.findInstance(instance.id()).orElseThrow().status()); } @@ -451,7 +451,7 @@ class InMemoryOrdoEngineTest { var instance = actionEngine.start("leave", "alice"); assertEquals(List.of("leave-submitted-mail"), executed); - assertEquals("manager", actionEngine.findPendingTasksByInstanceId(instance.id()).getFirst().stepId()); + assertEquals("manager", actionEngine.findPendingTasksByInstanceId(instance.id()).get(0).stepId()); assertEquals(ProcessStatus.RUNNING, instance.status()); } @@ -463,7 +463,7 @@ class InMemoryOrdoEngineTest { List.of(StepTransition.endWhen("manager", "never", 0)))); var instance = routingEngine.start("expense", "alice"); - ApprovalTask task = routingEngine.findPendingTasksByInstanceId(instance.id()).getFirst(); + ApprovalTask task = routingEngine.findPendingTasksByInstanceId(instance.id()).get(0); assertThrows(NoRouteFoundException.class, () -> routingEngine.approve(task.id(), "maria")); } @@ -476,12 +476,12 @@ class InMemoryOrdoEngineTest { Page byAssignee = engine.queryTasks(TaskQuery.any().withAssignee("maria"), new PageRequest(0, 10)); assertEquals(1, byAssignee.totalElements()); - assertEquals("maria", byAssignee.content().getFirst().assignee()); + assertEquals("maria", byAssignee.content().get(0).assignee()); Page byDefinition = engine.queryTasks(TaskQuery.any().withDefinitionId("leave"), new PageRequest(0, 10)); assertEquals(1, byDefinition.totalElements()); - assertEquals(leaveInstance.id(), byDefinition.content().getFirst().instanceId()); + assertEquals(leaveInstance.id(), byDefinition.content().get(0).instanceId()); Page firstPage = engine.queryTasks(TaskQuery.any(), new PageRequest(0, 1)); assertEquals(2, firstPage.totalElements()); @@ -499,20 +499,20 @@ class InMemoryOrdoEngineTest { Page runningOnly = engine.queryInstances( InstanceQuery.any().withStatus(ProcessStatus.RUNNING), new PageRequest(0, 10)); assertEquals(1, runningOnly.totalElements()); - assertEquals(running.id(), runningOnly.content().getFirst().id()); + assertEquals(running.id(), runningOnly.content().get(0).id()); Page byInitiator = engine.queryInstances(InstanceQuery.any().withInitiator("bob"), new PageRequest(0, 10)); assertEquals(1, byInitiator.totalElements()); - assertEquals(ProcessStatus.WITHDRAWN, byInitiator.content().getFirst().status()); + assertEquals(ProcessStatus.WITHDRAWN, byInitiator.content().get(0).status()); } @Test - void listDefinitionsPaginatesRegisteredDefinitions() { + void queryDefinitionsPaginatesRegisteredDefinitions() { engine.register(ProcessDefinition.linear("expense", "Expense request", List.of( ApprovalStep.single("finance", "Finance approval", "frank")))); - Page all = engine.listDefinitions(new PageRequest(0, 10)); + Page all = engine.queryDefinitions(new PageRequest(0, 10)); assertEquals(2, all.totalElements()); assertEquals(List.of("expense", "leave"), all.content().stream().map(ProcessDefinition::id).toList()); } @@ -529,7 +529,7 @@ class InMemoryOrdoEngineTest { ApprovalTask mariaTask = anyEngine.findTasks(instance.id()).stream() .filter(task -> task.assignee().equals("maria")).findFirst().orElseThrow(); anyEngine.approve(mariaTask.id(), "maria"); - anyEngine.approve(anyEngine.findPendingTasksByInstanceId(instance.id()).getFirst().id(), "henry"); + anyEngine.approve(anyEngine.findPendingTasksByInstanceId(instance.id()).get(0).id(), "henry"); List types = anyEngine.queryHistory(instance.id(), new PageRequest(0, 50)).content() .stream().map(ProcessEvent::type).toList(); @@ -574,7 +574,7 @@ class InMemoryOrdoEngineTest { ApprovalStep.single("manager", "Manager approval", "maria")))); var instance = listening.start("leave", "alice"); - listening.approve(listening.findPendingTasksByInstanceId(instance.id()).getFirst().id(), "maria"); + listening.approve(listening.findPendingTasksByInstanceId(instance.id()).get(0).id(), "maria"); assertEquals(ProcessStatus.APPROVED, listening.findInstance(instance.id()).orElseThrow().status()); assertEquals(List.of( @@ -601,7 +601,7 @@ class InMemoryOrdoEngineTest { StepTransition.end("fail")))); var instance = actionEngine.start("leave", "alice"); - actionEngine.approve(actionEngine.findPendingTasksByInstanceId(instance.id()).getFirst().id(), "maria"); + actionEngine.approve(actionEngine.findPendingTasksByInstanceId(instance.id()).get(0).id(), "maria"); assertEquals(ProcessStatus.APPROVED, actionEngine.findInstance(instance.id()).orElseThrow().status()); List executions = actionEngine.queryActionExecutions(instance.id(), new PageRequest(0, 10)) diff --git a/ordo-example/src/main/java/com/jetlumen/ordo/example/LeaveRequestExample.java b/ordo-example/src/main/java/com/jetlumen/ordo/example/LeaveRequestExample.java index 4866028..b6cfaca 100644 --- a/ordo-example/src/main/java/com/jetlumen/ordo/example/LeaveRequestExample.java +++ b/ordo-example/src/main/java/com/jetlumen/ordo/example/LeaveRequestExample.java @@ -23,7 +23,7 @@ public final class LeaveRequestExample { var instance = ordo.start("leave-request", "alice", new ProcessContext(Map.of( "requestId", "LEAVE-2026-001" ))); - var managerTask = ordo.findTasks(instance.id()).getFirst(); + var managerTask = ordo.findTasks(instance.id()).get(0); ordo.approve(managerTask.id(), "maria", "Approved by the manager."); var hrTask = ordo.findTasks(instance.id()).get(1); ordo.approve(hrTask.id(), "henry", "Leave record updated."); diff --git a/ordo-spring-boot-autoconfigure/pom.xml b/ordo-spring-boot-autoconfigure/pom.xml index 79dd6ad..c456e66 100644 --- a/ordo-spring-boot-autoconfigure/pom.xml +++ b/ordo-spring-boot-autoconfigure/pom.xml @@ -12,8 +12,8 @@ ordo-spring-boot-autoconfigure - 21 - 21 + 17 + 17 UTF-8 3.3.5 diff --git a/ordo-spring-boot-autoconfigure/src/test/java/com/jetlumen/ordo/spring/OrdoJdbcAutoConfigurationTest.java b/ordo-spring-boot-autoconfigure/src/test/java/com/jetlumen/ordo/spring/OrdoJdbcAutoConfigurationTest.java index e982f58..7e0c7d6 100644 --- a/ordo-spring-boot-autoconfigure/src/test/java/com/jetlumen/ordo/spring/OrdoJdbcAutoConfigurationTest.java +++ b/ordo-spring-boot-autoconfigure/src/test/java/com/jetlumen/ordo/spring/OrdoJdbcAutoConfigurationTest.java @@ -54,7 +54,7 @@ class OrdoJdbcAutoConfigurationTest { List pending = engine.findPendingTasksByAssignee("maria"); assertThat(pending).hasSize(1); - ApprovalTask approved = engine.approve(pending.getFirst().id(), "maria", "looks good"); + ApprovalTask approved = engine.approve(pending.get(0).id(), "maria", "looks good"); assertThat(approved.status().name()).isEqualTo("APPROVED"); assertThat(engine.findInstance(instance.id())).isPresent(); }); @@ -162,7 +162,7 @@ class OrdoJdbcAutoConfigurationTest { OrdoEngine engine = context.getBean(OrdoEngine.class); engine.register(LEAVE_REQUEST); ProcessInstance instance = engine.start("leave-request", "alice"); - engine.approve(engine.findPendingTasksByAssignee("maria").getFirst().id(), "maria"); + engine.approve(engine.findPendingTasksByAssignee("maria").get(0).id(), "maria"); RecordingListener listener = context.getBean(RecordingListener.class); assertThat(listener.types).contains( @@ -170,7 +170,7 @@ class OrdoJdbcAutoConfigurationTest { ProcessEventType.TASK_CREATED, ProcessEventType.TASK_APPROVED, ProcessEventType.INSTANCE_APPROVED); - assertThat(listener.types.getFirst()).isEqualTo(ProcessEventType.INSTANCE_STARTED); + assertThat(listener.types.get(0)).isEqualTo(ProcessEventType.INSTANCE_STARTED); assertThat(engine.queryHistory(instance.id(), new PageRequest(0, 20)) .totalElements()).isGreaterThan(0); }); diff --git a/ordo-spring-boot-starter/pom.xml b/ordo-spring-boot-starter/pom.xml index dd7f280..8cb0735 100644 --- a/ordo-spring-boot-starter/pom.xml +++ b/ordo-spring-boot-starter/pom.xml @@ -12,8 +12,8 @@ ordo-spring-boot-starter - 21 - 21 + 17 + 17 UTF-8 3.3.5 diff --git a/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcActionExecutionRepositoryTest.java b/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcActionExecutionRepositoryTest.java index 6f6b5c3..5934b80 100644 --- a/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcActionExecutionRepositoryTest.java +++ b/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcActionExecutionRepositoryTest.java @@ -43,7 +43,7 @@ class JdbcActionExecutionRepositoryTest { assertTrue(repository.complete("ex-1", ActionExecutionStatus.SUCCESS, null, T0.plusSeconds(2))); Page page = repository.query("inst-1", new PageRequest(0, 10)); assertEquals(1, page.totalElements()); - ActionExecution stored = page.content().getFirst(); + ActionExecution stored = page.content().get(0); assertEquals(ActionExecutionStatus.SUCCESS, stored.status()); assertEquals(T0.plusSeconds(2), stored.finishedAt()); assertFalse(repository.complete("ex-1", ActionExecutionStatus.FAILED, "nope", T0.plusSeconds(3))); diff --git a/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcOrdoEngineIntegrationTest.java b/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcOrdoEngineIntegrationTest.java index a4fc069..3e0a9ca 100644 --- a/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcOrdoEngineIntegrationTest.java +++ b/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcOrdoEngineIntegrationTest.java @@ -55,11 +55,11 @@ class JdbcOrdoEngineIntegrationTest { void completesASequentialApprovalProcess() { ProcessInstance instance = engine.start("leave", "alice", new ProcessContext(Map.of("requestId", "LEAVE-2026-001"))); - ApprovalTask managerTask = engine.findPendingTasksByInstanceId(instance.id()).getFirst(); + ApprovalTask managerTask = engine.findPendingTasksByInstanceId(instance.id()).get(0); assertEquals("maria", managerTask.assignee()); engine.approve(managerTask.id(), "maria", "ok"); - ApprovalTask hrTask = engine.findPendingTasksByInstanceId(instance.id()).getFirst(); + ApprovalTask hrTask = engine.findPendingTasksByInstanceId(instance.id()).get(0); assertEquals("henry", hrTask.assignee()); assertEquals(ProcessStatus.RUNNING, engine.findInstance(instance.id()).orElseThrow().status()); @@ -74,7 +74,7 @@ class JdbcOrdoEngineIntegrationTest { @Test void rejectionTerminatesTheProcess() { ProcessInstance instance = engine.start("leave", "alice"); - ApprovalTask task = engine.findPendingTasksByInstanceId(instance.id()).getFirst(); + ApprovalTask task = engine.findPendingTasksByInstanceId(instance.id()).get(0); ApprovalTask rejectedTask = engine.reject(task.id(), "maria", "Insufficient leave balance"); @@ -87,14 +87,14 @@ class JdbcOrdoEngineIntegrationTest { @Test void rejectsRepeatedApprovalsWithoutDuplicatingTheFlow() { ProcessInstance instance = engine.start("leave", "alice"); - ApprovalTask managerTask = engine.findPendingTasksByInstanceId(instance.id()).getFirst(); + ApprovalTask managerTask = engine.findPendingTasksByInstanceId(instance.id()).get(0); engine.approve(managerTask.id(), "maria"); assertThrows(TaskAlreadyCompletedException.class, () -> engine.approve(managerTask.id(), "maria")); assertEquals(2, engine.findTasks(instance.id()).size()); assertEquals(1, engine.findPendingTasksByInstanceId(instance.id()).size()); - assertEquals("hr", engine.findPendingTasksByInstanceId(instance.id()).getFirst().stepId()); + assertEquals("hr", engine.findPendingTasksByInstanceId(instance.id()).get(0).stepId()); } @Test @@ -110,7 +110,7 @@ class JdbcOrdoEngineIntegrationTest { ApprovalStep.single("hr", "HR approval", "henry")))); ProcessInstance instance = failingEngine.start("leave2", "alice"); - ApprovalTask managerTask = failingEngine.findPendingTasksByInstanceId(instance.id()).getFirst(); + ApprovalTask managerTask = failingEngine.findPendingTasksByInstanceId(instance.id()).get(0); assertThrows(IllegalStateException.class, () -> failingEngine.approve(managerTask.id(), "maria")); // the conditional task completion must have been rolled back @@ -137,7 +137,7 @@ class JdbcOrdoEngineIntegrationTest { List.of(StepTransition.endWhen("manager", "never", 0)))); ProcessInstance instance = failingEngine.start("leave-noroute", "alice"); - ApprovalTask managerTask = failingEngine.findPendingTasksByInstanceId(instance.id()).getFirst(); + ApprovalTask managerTask = failingEngine.findPendingTasksByInstanceId(instance.id()).get(0); assertThrows(NoRouteFoundException.class, () -> failingEngine.approve(managerTask.id(), "maria")); ApprovalTask storedTask = failingEngine.findTask(managerTask.id()).orElseThrow(); @@ -155,11 +155,11 @@ class JdbcOrdoEngineIntegrationTest { OrdoEngine secondEngine = newEngine(AssigneeResolver.direct()); assertEquals(instance, secondEngine.findInstance(instance.id()).orElseThrow()); - ApprovalTask managerTask = secondEngine.findPendingTasksByInstanceId(instance.id()).getFirst(); + ApprovalTask managerTask = secondEngine.findPendingTasksByInstanceId(instance.id()).get(0); secondEngine.approve(managerTask.id(), "maria"); assertEquals(ProcessStatus.RUNNING, engine.findInstance(instance.id()).orElseThrow().status()); - assertEquals("henry", engine.findPendingTasksByInstanceId(instance.id()).getFirst().assignee()); + assertEquals("henry", engine.findPendingTasksByInstanceId(instance.id()).get(0).assignee()); } @Test @@ -168,7 +168,7 @@ class JdbcOrdoEngineIntegrationTest { ApprovalStep.single("director", "Director approval", "diana")))); ProcessInstance instance = engine.start("leave", "alice"); - assertEquals("diana", engine.findPendingTasksByInstanceId(instance.id()).getFirst().assignee()); + assertEquals("diana", engine.findPendingTasksByInstanceId(instance.id()).get(0).assignee()); } @Test @@ -181,7 +181,7 @@ class JdbcOrdoEngineIntegrationTest { @Test void initiatorCanWithdrawWhileALaterStepIsPending() { ProcessInstance instance = engine.start("leave", "alice"); - engine.approve(engine.findPendingTasksByInstanceId(instance.id()).getFirst().id(), "maria"); + engine.approve(engine.findPendingTasksByInstanceId(instance.id()).get(0).id(), "maria"); ProcessInstance withdrawn = engine.withdraw(instance.id(), "alice", "changed plans"); @@ -202,7 +202,7 @@ class JdbcOrdoEngineIntegrationTest { ProcessInstance instance = engine.start("leave", "alice"); assertThrows(UnauthorizedInstanceOperationException.class, () -> engine.withdraw(instance.id(), "mallory")); - engine.reject(engine.findPendingTasksByInstanceId(instance.id()).getFirst().id(), "maria"); + engine.reject(engine.findPendingTasksByInstanceId(instance.id()).get(0).id(), "maria"); assertThrows(InstanceAlreadyCompletedException.class, () -> engine.withdraw(instance.id(), "alice")); } @@ -232,11 +232,11 @@ class JdbcOrdoEngineIntegrationTest { StepTransition.end("fail")))); ProcessInstance instance = actionEngine.start("leave-action", "alice"); - actionEngine.approve(actionEngine.findPendingTasksByInstanceId(instance.id()).getFirst().id(), "maria"); + actionEngine.approve(actionEngine.findPendingTasksByInstanceId(instance.id()).get(0).id(), "maria"); assertEquals(ProcessStatus.APPROVED, actionEngine.findInstance(instance.id()).orElseThrow().status()); assertEquals(ActionExecutionStatus.SUCCESS, - actionEngine.queryActionExecutions(instance.id(), new PageRequest(0, 10)).content().getFirst().status()); + actionEngine.queryActionExecutions(instance.id(), new PageRequest(0, 10)).content().get(0).status()); assertEquals(ActionExecutionStatus.FAILED, actionEngine.queryActionExecutions(instance.id(), new PageRequest(0, 10)).content().get(1).status()); List types = actionEngine.queryHistory(instance.id(), new PageRequest(0, 50)).content() diff --git a/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcPostgresIntegrationTest.java b/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcPostgresIntegrationTest.java index 5ea1bd7..6831548 100644 --- a/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcPostgresIntegrationTest.java +++ b/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcPostgresIntegrationTest.java @@ -110,9 +110,9 @@ class JdbcPostgresIntegrationTest { ProcessInstance instance = engine.start("leave-pg", "alice", new ProcessContext(Map.of("requestId", "LEAVE-2026-001", "days", 5))); - ApprovalTask managerTask = engine.findPendingTasksByInstanceId(instance.id()).getFirst(); + ApprovalTask managerTask = engine.findPendingTasksByInstanceId(instance.id()).get(0); engine.approve(managerTask.id(), "maria", "ok"); - ApprovalTask hrTask = engine.findPendingTasksByInstanceId(instance.id()).getFirst(); + ApprovalTask hrTask = engine.findPendingTasksByInstanceId(instance.id()).get(0); engine.approve(hrTask.id(), "henry", "ok"); ProcessInstance finished = engine.findInstance(instance.id()).orElseThrow(); @@ -207,7 +207,7 @@ class JdbcPostgresIntegrationTest { ApprovalStep.single("hr", "HR approval", "henry")))); ProcessInstance instance = failingEngine.start("leave-rollback-pg", "alice"); - ApprovalTask managerTask = failingEngine.findPendingTasksByInstanceId(instance.id()).getFirst(); + ApprovalTask managerTask = failingEngine.findPendingTasksByInstanceId(instance.id()).get(0); assertThrows(IllegalStateException.class, () -> failingEngine.approve(managerTask.id(), "maria")); // the conditional task completion must have been rolled back diff --git a/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcProcessHistoryRepositoryTest.java b/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcProcessHistoryRepositoryTest.java index 618d938..6baea40 100644 --- a/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcProcessHistoryRepositoryTest.java +++ b/ordo-storage-jdbc/src/test/java/com/jetlumen/ordo/storage/jdbc/JdbcProcessHistoryRepositoryTest.java @@ -50,7 +50,7 @@ class JdbcProcessHistoryRepositoryTest { Page first = repository.query("inst-1", new PageRequest(0, 1)); assertEquals(1, first.content().size()); - assertEquals("e1", first.content().getFirst().id()); + assertEquals("e1", first.content().get(0).id()); assertTrue(first.hasNext()); } diff --git a/pom.xml b/pom.xml index 0e28a24..be03a59 100644 --- a/pom.xml +++ b/pom.xml @@ -23,8 +23,8 @@ 0.0.1-SNAPSHOT - 21 - 21 + 17 + 17 UTF-8 5.11.4 2.18.2