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>
11 KiB
11 KiB
Plan: 条件路由 (Conditional Routing / RoutingCondition)
已与用户确认的关键决策
- transitions 挂在
ProcessDefinition级别(独立列表),不改动ApprovalStep。 RoutingCondition采用与AssigneeResolver一致的模式:全局单例 bean,boolean matches(String conditionKey, ProcessContext context);数据库只存conditionKey字符串,不持久化任何 Java lambda/表达式(因为 JDBC 场景下引擎重启要从数据库重新装配ProcessDefinition,无法反序列化任意代码逻辑)。- 不保留线性回退:每个
ProcessDefinition必须显式声明 transitions,覆盖每个 step 的 出边;没有 transitions 完全是非法状态(构造期校验报错),不再依赖steps()列表顺序做 执行期推进(step_order/列表顺序仅用于候选人/去重校验和展示,不再决定执行流)。 - 匹配不到任何 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=0StepTransition.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)重写为:- 调用新的私有
resolveTransition(definition, step, context): 过滤definition.transitions()中fromStepId == step.id()的项,按priority升序排序, 依次判断conditionKey为空/blank(无条件恒真)或routingCondition.matches(conditionKey, context), 返回第一个匹配的StepTransition;若无匹配,抛出NoRouteFoundException。 - 若匹配结果
toStepId() == null→completeInstance(instance, ProcessStatus.APPROVED, now)。 - 否则 →
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
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新增:@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.javaordo-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.javad:\Projects\Personal\java\rhizome\src\main\java\...\LeaveRequestDemoService.java
新增测试(条件路由专项,主要在 InMemoryOrdoEngineTest 或新建
ConditionalRoutingTest)
- 基础分支:manager 通过后,根据
conditionKey(如"amount-gt-1000")走向不同的下一 step (高额 -> director 审批 step;低额 -> 直接 end),验证RoutingCondition.matches被正确调用 并影响推进结果。 - 无匹配 transition 时抛出
NoRouteFoundException,且整个 approve 事务回滚(沿用现有 rollback 测试模式)。 ProcessDefinition构造期校验:- 某 step 没有任何出边 ->
IllegalArgumentException。 - transition 引用不存在的
fromStepId/toStepId->IllegalArgumentException。 - 同一
(fromStepId, priority)重复 ->IllegalArgumentException。
- 某 step 没有任何出边 ->
- JDBC 往返测试:
JdbcProcessDefinitionRepositoryTest新增用例,插入一个带条件分支的 definition,findById读回后transitions()内容与写入一致(包括toStepId == null终止边)。
范围外 / 明确不做
- 不支持在 transition 上直接内嵌 Java lambda(会破坏 JDBC 持久化重启场景,已由用户确认)。
- 不引入显式
startStepId概念,起始步骤仍是steps().getFirst()。 - 不做并行分支/合并(fork-join),仅支持"单一当前 step -> 单一下一 step 或终止"的有向图推进, 与现有 ANY/ALL 多候选人机制正交(transition 发生在"某个 step 整体被判定通过/拒绝"之后)。
实施顺序(有依赖关系,需按序执行)
- ordo-api:
StepTransition、RoutingCondition、ProcessDefinition(含linear工厂)、NoRouteFoundException。 - ordo-core:
DefaultOrdoEngine改造、InMemoryOrdoEngine构造函数重载。 - ordo-storage-jdbc:
V3迁移 +StepTransitionMapper+JdbcProcessDefinitionRepository改造- 同步更新
JdbcTestSupport.MIGRATIONS。
- 同步更新
- ordo-spring-boot-autoconfigure:
RoutingConditionbean 及ordoEngine(...)装配。 - 迁移全部 ~29 处旧调用点(
ProcessDefinition.linear(...)),含 rhizome demo。 - 新增条件路由专项测试(引擎行为 + 校验 + JDBC 往返)。
验证
ordo-api/ordo-core/ordo-storage-jdbc/ordo-spring-boot-autoconfigure全量单元测试通过。- 手动确认 rhizome 端到端 curl 流程(线性场景切到
ProcessDefinition.linear后)仍然可用 (非阻塞,可在实现完成后单独验证)。