Files
ordo/docs/plan-conditionRouting.md
T
0264408andCursor 58ec867eab feat: add explicit conditional routing for approval steps
Require ProcessDefinition transitions so the engine can branch or end by conditionKey, persist them in JDBC, and pin repository text files to LF.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 18:56:59 +08:00

178 lines
11 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.
# Plan: 条件路由 (Conditional Routing / RoutingCondition)
## 已与用户确认的关键决策
1. transitions 挂在 `ProcessDefinition` 级别(独立列表),不改动 `ApprovalStep`。
2. `RoutingCondition` 采用与 `AssigneeResolver` 一致的模式:全局单例 bean,
`boolean matches(String conditionKey, ProcessContext context)`;数据库只存 `conditionKey`
字符串,不持久化任何 Java lambda/表达式(因为 JDBC 场景下引擎重启要从数据库重新装配
`ProcessDefinition`,无法反序列化任意代码逻辑)。
3. **不保留线性回退**:每个 `ProcessDefinition` 必须显式声明 transitions,覆盖每个 step 的
出边;没有 transitions 完全是非法状态(构造期校验报错),不再依赖 `steps()` 列表顺序做
执行期推进(`step_order`/列表顺序仅用于候选人/去重校验和展示,不再决定执行流)。
4. 匹配不到任何 transition 时,fail-fast 抛出 `NoRouteFoundException`(不静默回退)。
## 模型设计 (ordo-api)
### 新增 `StepTransition` record
```
record StepTransition(String fromStepId, String toStepId, String conditionKey, int priority)
```
- `toStepId == null` 表示"终止流程"(当匹配到这条边时,实例被判 APPROVED 并完成)。
- `conditionKey == null/blank` 表示无条件(总是匹配),用作 else/默认分支。
- 便捷工厂:
- `StepTransition.always(fromStepId, toStepId)` — 无条件跳转,priority=0
- `StepTransition.when(fromStepId, toStepId, conditionKey, priority)` — 条件跳转
- `StepTransition.end(fromStepId)` — 无条件终止
- `StepTransition.endWhen(fromStepId, conditionKey, priority)` — 条件终止
- 评估顺序:按 `priority` 升序,第一个匹配的生效(无条件 = 恒真)。
### 新增 `RoutingCondition` functional interface(与 AssigneeResolver 同目录/同风格)
```
@FunctionalInterface
public interface RoutingCondition {
boolean matches(String conditionKey, ProcessContext context);
static RoutingCondition always() { return (key, context) -> true; }
}
```
### `ProcessDefinition` 改为 4 个分量(破坏性变更)
```
record ProcessDefinition(String id, String name, List<ApprovalStep> steps, List<StepTransition> transitions)
```
构造期校验(在现有 id/name/steps 非空+去重校验基础上新增):
- `transitions` 非 null,`List.copyOf` 防御性拷贝。
- 每条 transition 的 `fromStepId` 必须是 `steps` 中存在的 id;`toStepId`(非 null 时)也必须存在。
- 每个 step 的 id 必须至少作为一条 transition 的 `fromStepId` 出现一次(强制显式声明出边),
否则 `IllegalArgumentException("step X has no outgoing transition")`。
- 同一 `(fromStepId, priority)` 不能重复(避免评估顺序有歧义)。
- 新增便捷工厂 `ProcessDefinition.linear(id, name, steps)`:自动生成"上一步无条件指向下一步、
最后一步无条件终止"的 transitions,等价于旧的线性行为,用于降低无需真正条件路由的测试/demo
的迁移成本(生成的仍然是显式 transitions,满足"强制显式"的约束,只是自动拼装)。
### 新增异常 `NoRouteFoundException extends OrdoException`
- message 形如 `"no matching transition for step: <stepId> in instance: <instanceId>"`。
- 放在 `com.jetlumen.ordo.api.exception` 包,命名/风格对齐现有 `DefinitionNotFoundException` 等。
## 引擎改造 (ordo-core)
### `DefaultOrdoEngine`
- 构造函数新增 `RoutingCondition routingCondition` 参数(在 `assigneeResolver` 之后插入,
与 Spring autoconfigure bean 顺序一致)。
- 删除 `indexOf(...)` 私有方法(不再需要基于下标推进)。
- `advanceOrComplete(instance, definition, step, now)` 重写为:
1. 调用新的私有 `resolveTransition(definition, step, context)`:
过滤 `definition.transitions()` 中 `fromStepId == step.id()` 的项,按 `priority` 升序排序,
依次判断 `conditionKey` 为空/blank(无条件恒真)或 `routingCondition.matches(conditionKey, context)`,
返回第一个匹配的 `StepTransition`;若无匹配,抛出 `NoRouteFoundException`。
2. 若匹配结果 `toStepId() == null` → `completeInstance(instance, ProcessStatus.APPROVED, now)`。
3. 否则 → `createStepTasks(instance, requireStep(definition, matched.toStepId()), now)`。
- `advanceAfterDecision` 中原有 ANY/ALL 策略判断逻辑不变,只是"推进到下一步"的调用点改为走上面的
transition 解析,不再用 `stepIndex == size-1` 判断是否最后一步。
- `NoRouteFoundException` 在 `transactionExecutor.execute(...)` 内抛出,天然触发整个 approve/reject
事务回滚(复用现有 `rollsBackTheWholeApprovalWhenTheNextStepCannotBeCreated` 验证过的回滚机制)。
- `start()` 的起始 step 仍取 `definition.steps().getFirst()`(未改变,属于本次范围外的假设,
如需显式声明起始 step 可后续再提出)。
### `InMemoryOrdoEngine`
- 仿照现有 `AssigneeResolver` 构造函数重载模式,新增:
- `InMemoryOrdoEngine(RoutingCondition routingCondition)`
- `InMemoryOrdoEngine(AssigneeResolver assigneeResolver, RoutingCondition routingCondition)`
- `InMemoryOrdoEngine(Clock clock, AssigneeResolver assigneeResolver, RoutingCondition routingCondition)`
- 默认无参/仅 Clock 的构造函数内部使用 `RoutingCondition.always()`。
## 持久化 (ordo-storage-jdbc)
### 新迁移 `V3__add_step_transitions.sql`
```sql
CREATE TABLE ordo_step_transition (
definition_id VARCHAR(64) NOT NULL,
from_step_id VARCHAR(64) NOT NULL,
to_step_id VARCHAR(64),
condition_key VARCHAR(255),
priority INTEGER NOT NULL,
PRIMARY KEY (definition_id, from_step_id, priority),
CONSTRAINT fk_transition_from FOREIGN KEY (definition_id, from_step_id)
REFERENCES ordo_approval_step (definition_id, step_id),
CONSTRAINT fk_transition_to FOREIGN KEY (definition_id, to_step_id)
REFERENCES ordo_approval_step (definition_id, step_id)
);
```
**重要提醒(已踩过的坑,来自 /memories/repo/any-all-multi-approval.md)**:
- 必须同步把这个新迁移文件加进 `ordo-storage-jdbc/src/test/java/.../JdbcTestSupport.java` 的
`MIGRATIONS` 数组,否则测试库缺表导致全部 JDBC 测试失败。
- 注释里不要出现分号(或依赖已有的按行剥离 `--` 注释逻辑,已经是健壮的,无需再担心)。
### 新增 `StepTransitionMapper`(仿照 `ApprovalStepMapper` 的 `StepRow` 模式)
- `readRow(ResultSet) -> TransitionRow(fromStepId, toStepId, conditionKey, priority)`。
### `JdbcProcessDefinitionRepository`
- `insertIfAbsent`:在插入 steps/candidates 之后,遍历 `definition.transitions()` 执行
`INSERT INTO ordo_step_transition (...)`。
- `findById`:新增 `SELECT_TRANSITIONS`(按 `from_step_id, priority` 排序),组装
`List<StepTransition>` 传入新的 4 参 `ProcessDefinition` 构造函数。
## Spring Boot 自动配置 (ordo-spring-boot-autoconfigure)
- `OrdoJdbcAutoConfiguration` 新增:
```java
@Bean
@ConditionalOnMissingBean
public RoutingCondition ordoRoutingCondition() {
return RoutingCondition.always();
}
```
- `ordoEngine(...)` 方法签名新增 `RoutingCondition ordoRoutingCondition` 参数,传给
`DefaultOrdoEngine` 构造函数。
- `OrdoJdbcAutoConfigurationTest`:更新 `LEAVE_REQUEST` 定义使用 `ProcessDefinition.linear(...)`;
新增一个 `honoursUserDefinedRoutingCondition` 测试,仿照现有
`honoursUserDefinedAssigneeResolver` 的写法。
## 调用点迁移(约 29 处 `new ProcessDefinition(...)`)
- 绝大多数纯线性场景(测试/demo 不需要真正的条件路由)直接把
`new ProcessDefinition(id, name, steps)` 换成 `ProcessDefinition.linear(id, name, steps)`。
- 受影响文件:
- `ordo-api/src/test/java/.../ProcessDefinitionTest.java`(同时新增 transitions 相关校验用例:
缺少出边报错、fromStepId/toStepId 引用不存在的 step 报错、重复 priority 报错)
- `ordo-core/src/test/java/.../InMemoryOrdoEngineTest.java`(8 处调用点 + 新增条件路由专项测试)
- `ordo-spring-boot-autoconfigure/src/test/java/.../OrdoJdbcAutoConfigurationTest.java`
- `ordo-storage-jdbc/src/test/java/.../Jdbc*Test.java`(约 10 处,含
`JdbcProcessDefinitionRepositoryTest` 需要新增"读写 transitions 往返"专项用例)
- `ordo-storage-jdbc/src/main/java/.../JdbcProcessDefinitionRepository.java`(`findById` 内部
组装 `ProcessDefinition` 那一处,改为传入真实加载到的 transitions)
- `ordo-example/src/main/java/.../LeaveRequestExample.java`
- `d:\Projects\Personal\java\rhizome\src\main\java\...\LeaveRequestDemoService.java`
## 新增测试(条件路由专项,主要在 `InMemoryOrdoEngineTest` 或新建
`ConditionalRoutingTest`)
1. 基础分支:manager 通过后,根据 `conditionKey`(如 `"amount-gt-1000"`)走向不同的下一 step
(高额 -> director 审批 step;低额 -> 直接 end),验证 `RoutingCondition.matches` 被正确调用
并影响推进结果。
2. 无匹配 transition 时抛出 `NoRouteFoundException`,且整个 approve 事务回滚(沿用现有
rollback 测试模式)。
3. `ProcessDefinition` 构造期校验:
- 某 step 没有任何出边 -> `IllegalArgumentException`。
- transition 引用不存在的 `fromStepId`/`toStepId` -> `IllegalArgumentException`。
- 同一 `(fromStepId, priority)` 重复 -> `IllegalArgumentException`。
4. JDBC 往返测试:`JdbcProcessDefinitionRepositoryTest` 新增用例,插入一个带条件分支的
definition,`findById` 读回后 `transitions()` 内容与写入一致(包括 `toStepId == null` 终止边)。
## 范围外 / 明确不做
- 不支持在 transition 上直接内嵌 Java lambda(会破坏 JDBC 持久化重启场景,已由用户确认)。
- 不引入显式 `startStepId` 概念,起始步骤仍是 `steps().getFirst()`。
- 不做并行分支/合并(fork-join),仅支持"单一当前 step -> 单一下一 step 或终止"的有向图推进,
与现有 ANY/ALL 多候选人机制正交(transition 发生在"某个 step 整体被判定通过/拒绝"之后)。
## 实施顺序(有依赖关系,需按序执行)
1. ordo-api:`StepTransition`、`RoutingCondition`、`ProcessDefinition`(含 `linear` 工厂)、
`NoRouteFoundException`。
2. ordo-core:`DefaultOrdoEngine` 改造、`InMemoryOrdoEngine` 构造函数重载。
3. ordo-storage-jdbc:`V3` 迁移 + `StepTransitionMapper` + `JdbcProcessDefinitionRepository` 改造
+ 同步更新 `JdbcTestSupport.MIGRATIONS`。
4. ordo-spring-boot-autoconfigure:`RoutingCondition` bean 及 `ordoEngine(...)` 装配。
5. 迁移全部 ~29 处旧调用点(`ProcessDefinition.linear(...)`),含 rhizome demo。
6. 新增条件路由专项测试(引擎行为 + 校验 + JDBC 往返)。
## 验证
- `ordo-api`/`ordo-core`/`ordo-storage-jdbc`/`ordo-spring-boot-autoconfigure` 全量单元测试通过。
- 手动确认 rhizome 端到端 curl 流程(线性场景切到 `ProcessDefinition.linear` 后)仍然可用
(非阻塞,可在实现完成后单独验证)。