feat: add optional Spring REST adapter and catalog SPI

Expose OrdoEngine over conditionally registered HTTP endpoints in autoconfigure, keep JDBC wiring in spring.jdbc, and share definition JSON via ProcessDefinitionParser.toJson.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
0264408
2026-09-17 10:30:09 +08:00
co-authored by Cursor
parent 7d17157633
commit 0288a8b3dd
29 changed files with 1295 additions and 27 deletions
+490
View File
@@ -0,0 +1,490 @@
openapi: 3.0.3
info:
title: Ordo REST
version: 0.0.1-SNAPSHOT
description: Optional Spring adapter over OrdoEngine. No authentication. Default base path `/ordo`.
servers:
- url: /ordo
paths:
/definitions/parse:
post:
summary: Parse definition JSON without publishing
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ProcessDefinitionDocument'
responses:
'200':
description: Parsed graph (version 0)
content:
application/json:
schema:
$ref: '#/components/schemas/ProcessDefinitionDocument'
'400':
$ref: '#/components/responses/Error'
/definitions:
post:
summary: Parse and publish
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ProcessDefinitionDocument'
responses:
'201':
description: Published definition
content:
application/json:
schema:
$ref: '#/components/schemas/ProcessDefinitionDocument'
'400':
$ref: '#/components/responses/Error'
get:
summary: Latest version of each definition
parameters:
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/size'
responses:
'200':
description: Page of definitions
/definitions/{definitionId}:
get:
parameters:
- $ref: '#/components/parameters/definitionId'
responses:
'200':
description: Latest definition
content:
application/json:
schema:
$ref: '#/components/schemas/ProcessDefinitionDocument'
'404':
description: Not found
/definitions/{definitionId}/versions:
get:
parameters:
- $ref: '#/components/parameters/definitionId'
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/size'
responses:
'200':
description: Versions newest first
/definitions/{definitionId}/versions/{version}:
get:
parameters:
- $ref: '#/components/parameters/definitionId'
- name: version
in: path
required: true
schema:
type: integer
minimum: 1
responses:
'200':
description: Specific version
'404':
description: Not found
/instances:
post:
summary: Start an instance
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [definitionId, initiator]
properties:
definitionId:
type: string
initiator:
type: string
context:
type: object
properties:
variables:
type: object
additionalProperties: true
responses:
'200':
description: Started instance
'404':
$ref: '#/components/responses/Error'
get:
parameters:
- name: definitionId
in: query
schema:
type: string
- name: status
in: query
schema:
type: string
- name: initiator
in: query
schema:
type: string
- name: startedFrom
in: query
schema:
type: string
format: date-time
- name: startedTo
in: query
schema:
type: string
format: date-time
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/size'
responses:
'200':
description: Page of instances
/instances/{instanceId}:
get:
parameters:
- $ref: '#/components/parameters/instanceId'
responses:
'200':
description: Instance
'404':
description: Not found
/instances/{instanceId}/withdraw:
post:
parameters:
- $ref: '#/components/parameters/instanceId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ActorComment'
responses:
'200':
description: Withdrawn
'403':
$ref: '#/components/responses/Error'
'404':
$ref: '#/components/responses/Error'
'409':
$ref: '#/components/responses/Error'
/instances/{instanceId}/cancel:
post:
parameters:
- $ref: '#/components/parameters/instanceId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ActorComment'
responses:
'200':
description: Cancelled
'404':
$ref: '#/components/responses/Error'
'409':
$ref: '#/components/responses/Error'
/instances/{instanceId}/tasks:
get:
parameters:
- $ref: '#/components/parameters/instanceId'
responses:
'200':
description: All tasks
/instances/{instanceId}/tasks/pending:
get:
parameters:
- $ref: '#/components/parameters/instanceId'
responses:
'200':
description: Pending tasks
/instances/{instanceId}/history:
get:
parameters:
- $ref: '#/components/parameters/instanceId'
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/size'
responses:
'200':
description: Timeline
/instances/{instanceId}/action-executions:
get:
parameters:
- $ref: '#/components/parameters/instanceId'
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/size'
responses:
'200':
description: ACTION executions
/tasks:
get:
parameters:
- name: assignee
in: query
schema:
type: string
- name: instanceId
in: query
schema:
type: string
- name: definitionId
in: query
schema:
type: string
- name: status
in: query
schema:
type: string
- name: createdFrom
in: query
schema:
type: string
format: date-time
- name: createdTo
in: query
schema:
type: string
format: date-time
- $ref: '#/components/parameters/page'
- $ref: '#/components/parameters/size'
responses:
'200':
description: Page of tasks
/tasks/pending:
get:
parameters:
- name: assignee
in: query
required: true
schema:
type: string
responses:
'200':
description: Pending tasks for assignee
/tasks/{taskId}:
get:
parameters:
- $ref: '#/components/parameters/taskId'
responses:
'200':
description: Task
'404':
description: Not found
/tasks/{taskId}/approve:
post:
parameters:
- $ref: '#/components/parameters/taskId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ActorComment'
responses:
'200':
description: Approved
'403':
$ref: '#/components/responses/Error'
'409':
$ref: '#/components/responses/Error'
/tasks/{taskId}/reject:
post:
parameters:
- $ref: '#/components/parameters/taskId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ActorComment'
responses:
'200':
description: Rejected
'403':
$ref: '#/components/responses/Error'
'409':
$ref: '#/components/responses/Error'
/tasks/{taskId}/reassign:
post:
parameters:
- $ref: '#/components/parameters/taskId'
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [actor, newAssignee]
properties:
actor:
type: string
newAssignee:
type: string
responses:
'200':
description: Reassigned
'403':
$ref: '#/components/responses/Error'
'409':
$ref: '#/components/responses/Error'
/due:
post:
parameters:
- name: limit
in: query
schema:
type: integer
minimum: 1
default: 100
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
limit:
type: integer
minimum: 1
responses:
'200':
description: Batch result
content:
application/json:
schema:
type: object
properties:
processed:
type: integer
'400':
$ref: '#/components/responses/Error'
/catalog/conditions:
get:
responses:
'200':
description: conditionKey catalog
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/CatalogItem'
/catalog/actions:
get:
responses:
'200':
description: actionKey catalog
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/CatalogItem'
/catalog/assignees:
get:
responses:
'200':
description: Assignee catalog
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/CatalogItem'
components:
parameters:
page:
name: page
in: query
schema:
type: integer
minimum: 0
default: 0
size:
name: size
in: query
schema:
type: integer
minimum: 1
default: 20
definitionId:
name: definitionId
in: path
required: true
schema:
type: string
instanceId:
name: instanceId
in: path
required: true
schema:
type: string
taskId:
name: taskId
in: path
required: true
schema:
type: string
responses:
Error:
description: Error body
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorBody'
schemas:
ErrorBody:
type: object
properties:
error:
type: string
message:
type: string
ActorComment:
type: object
required: [actor]
properties:
actor:
type: string
comment:
type: string
CatalogItem:
type: object
properties:
key:
type: string
label:
type: string
ProcessDefinitionDocument:
type: object
required: [id, name, steps, transitions]
properties:
id:
type: string
name:
type: string
version:
type: integer
startStep:
type: string
steps:
type: array
items:
type: object
transitions:
type: array
items:
type: object
properties:
from:
type: string
to:
type: string
nullable: true
when:
type: string
priority:
type: integer
+1 -8
View File
@@ -17,19 +17,12 @@
- 任务转派 `reassign` / `TASK_REASSIGNED`
- 到期升级 `processDue` / `TASK_ESCALATED`
- 管理员/系统取消 `cancel` / `CANCELLED`
- 可选 REST(autoconfigure 条件装配)+ 目录 SPI `OrdoCatalog`
## 开发计划(确定要做)
下列能力已纳入计划,尚未实现。实现顺序可按依赖调整,但范围本身不从计划中拿掉。
### 可选 REST + 目录 SPI
引擎入口仍是 `OrdoEngine`。计划提供**可选、极薄**的 REST 适配(例如独立 starter),带 OpenAPI,覆盖定义读写/解析校验、实例与任务查询,不包含鉴权、RBAC、业务表单。
配套 **目录 SPI**:宿主登记可用的 `conditionKey` / `actionKey` / 候选人(或角色)项,供 REST 与外部设计器下拉,而不是在 JSON 里写引擎无法执行的表达式。
`RoutingCondition` / `ActionHandler` / `AssigneeResolver` 保持全局单例;流程隔离由宿主用 key 约定(建议前缀)+ 门面分发,引擎不按流程定义拆 bean。
### 独立设计器(不进本仓库)
流程设计器是**单独产品**,消费上述 REST 与目录,不做成 ordo 模块。画布对齐引擎图(审批步、ACTION 步、边上的 `when`/`priority`,结束为 `to: null`),不引入 BPMN 网关/并行等引擎没有的语义。节点坐标等 layout 由设计器自存,不进入 `ProcessDefinition`。
+42 -6
View File
@@ -10,9 +10,7 @@ Ordo 是嵌入宿主进程的审批引擎,入口是 `OrdoEngine`。
做:流程定义、实例推进、待办任务、条件路由、ACTION 副作用、审计事件、分页查询。
产品边界:不做业务表单、用户体系、多租户;不内置设计器 UI。业务字段放在 `ProcessContext`(不可变 `Map<String, Object>`)。当前也**没有** REST;HTTP 仍由宿主自建。计划中的可选 REST 与独立设计器见 [roadmap.md](roadmap.md)。
开发计划(尚未提供,见 [roadmap.md](roadmap.md)):可选 REST + 目录 SPI。
产品边界:不做业务表单、用户体系、多租户;不内置设计器 UI。业务字段放在 `ProcessContext`(不可变 `Map<String, Object>`)。可选 HTTP 见 §2.3;独立设计器见 [roadmap.md](roadmap.md)。
## 2. 模块与接入
@@ -66,6 +64,9 @@ ordo:
location: classpath*:ordo/*.json # 启动时对每个 JSON 调用 publish
due:
poll-ms: 0 # >0 时轮询 processDue;默认不调度
rest:
enabled: false
base-path: /ordo
```
启动加载使用 `publish`:图与 latest 相同则不升版本;不同则写入新版本。运行中实例继续锁定发起时所用版本。
@@ -78,9 +79,44 @@ ordo:
| `RoutingCondition` | 始终匹配(无条件边) |
| `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` 不能重复。
@@ -121,7 +157,7 @@ new ProcessDefinition("leave-request-routed", "Leave request",
### 3.2 JSON
`ProcessDefinitionParser.fromJson(String|InputStream)`。Spring 默认扫 `classpath*:ordo/*.json`。
`ProcessDefinitionParser.fromJson(String|InputStream)` / `toJson(ProcessDefinition)`。Spring 默认扫 `classpath*:ordo/*.json`。`toJson` 写出 `version` 与 `startStep`(当前步骤列表首位);`fromJson` 仍忽略 JSON 里的 `version`。
```json
{
@@ -355,6 +391,6 @@ Flyway 脚本按方言分目录:`db/postgresql/migration`、`db/mysql/migratio
## 13. 未提供能力
开发计划中(见 [roadmap.md](roadmap.md)):可选 REST + 目录 SPI。独立设计器不进本仓库,等 REST 与目录之后再做。
独立设计器不进本仓库,等 REST 与目录之后由单独产品消费(见 [roadmap.md](roadmap.md))。
暂不在计划中:多租户、设计器 UI。当前 REST 由宿主自建。
暂不在计划中:多租户、设计器 UI。