Repository navigation
Ordered Tree Tables CN
本文介绍 core/ordered_tree/v1,按 2026-10-07 的 dev 源码核对。示例是模拟契约数据,不是生产环境抓包;记录省略审计等其他模型字段,保留 HTTP 的 data / meta 外层。调用时请换成当前工作区的真实 ID。
- 内置
departments在主数据源(/settings/data-models?source=main)可见,通用运行时接口只读。创建、编辑、删除部门、关联角色和维护成员归属,应使用组织控制台专用接口。 - 自建的可写树状模板表按权限开放下列通用操作。为完整演示,请创建模型编码为
org_demo、含必填字符串业务字段name的树状表。它是组织层级示例表,不会自动成为系统部门目录。 - 调用需要认证及当前工作区权限;使用 Cookie 认证的写请求沿用现有 CSRF 协议(
x-csrf-token)。示例省略凭证。游标不能替代授权。
flowchart TD
Root["组织 / Organization: virtual root"] --> A["研发中心 / R&D"]
A --> B["平台部 / Platform"]
A --> C["应用部 / Applications"]
B --> D["基础设施组 / Infrastructure"]
| Node / 节点 | ID |
|---|---|
| 研发中心 | 00000000-0000-4000-8000-000000000001 |
| 平台部 | 00000000-0000-4000-8000-000000000002 |
| 应用部 | 00000000-0000-4000-8000-000000000003 |
| 基础设施组 | 00000000-0000-4000-8000-000000000004 |
“组织”是虚拟浏览根,不是数据库记录。顶层部门的 parent_id: null,未填上级时挂在这个浏览根下面。选中“组织”查看所有用户(包括未分配部门的用户);选中某部门查看该部门及全部下级部门的用户,由后端去重。通用树状表本身可以有多条顶层记录。
| 字段 | 职责 |
|---|---|
parent_id |
父子关系真值;null 表示顶层 |
sibling_rank |
底座维护同级顺序,不直接写入 |
tree_path |
隐藏的 PostgreSQL ltree 派生列,不是公开业务字段 |
scope_id、tree_partition_id
|
底座维护作用域与树分区隔离 |
数据库在移动时原子维护路径并防止成环。后代范围用 ltree 路径谓词和 ARRAY[tree_path] 上的 GiST 表达式索引筛选,不需要客户端按层多次请求才能找齐子树。但这不代表每个接口只执行一条 SQL:授权、游标校验、搜索祖先上下文及业务补充读取可能需要额外查询。业务排序按同级顺序,不按 UUID 路径字典序;移动子树会在同一事务中更新后代路径。
统一前缀:/api/runtime/models/{model_code},示例使用 org_demo。
| Method | Path | 作用 |
|---|---|---|
| GET | /list |
普通分页列表;不自动展开后代 |
| GET | /get/{id} |
读取单条记录 |
| POST | /create |
按父节点和相邻位置创建 |
| PATCH | /update/{id} |
只改业务字段 |
| DELETE | /delete/{id} |
仅删除叶节点 |
| GET | /tree/roots |
分页读取顶层节点 |
| GET | /tree/children/{id} |
分页读取直接子节点 |
| GET | /tree/ancestors/{id} |
根到直接父节点,不含自身 |
| GET | /tree/descendants/{id} |
深度优先分页读取后代,不含自身 |
| GET | /tree/search |
业务文本前缀匹配,带祖先上下文 |
| POST | /tree/move/{id} |
换父节点或调整同级位置 |
| POST | /tree/delete-subtree/{id} |
核对数量后删除节点及全部后代 |
读取需要 view,新增需要 create,修改/移动需要 update,两类删除需要 delete 权限。即使模板描述了写操作,内置只读模型的能力限制仍然生效。
GET /api/runtime/models/org_demo/list?page=1&page_size=20HTTP 200
{
"data": {
"items": [
{
"id": "00000000-0000-4000-8000-000000000001",
"name": "研发中心",
"parent_id": null
},
{
"id": "00000000-0000-4000-8000-000000000002",
"name": "平台部",
"parent_id": "00000000-0000-4000-8000-000000000001"
},
{
"id": "00000000-0000-4000-8000-000000000003",
"name": "应用部",
"parent_id": "00000000-0000-4000-8000-000000000001"
},
{
"id": "00000000-0000-4000-8000-000000000004",
"name": "基础设施组",
"parent_id": "00000000-0000-4000-8000-000000000002"
}
],
"total": 4
},
"meta": null
}GET /api/runtime/models/org_demo/get/00000000-0000-4000-8000-000000000002HTTP 200
{
"data": {
"id": "00000000-0000-4000-8000-000000000002",
"name": "平台部",
"parent_id": "00000000-0000-4000-8000-000000000001"
},
"meta": null
}POST /api/runtime/models/org_demo/create
Content-Type: application/json
{
"name": "基础设施组",
"parent_id": "00000000-0000-4000-8000-000000000002"
}HTTP 201
{
"data": {
"id": "00000000-0000-4000-8000-000000000004",
"name": "基础设施组",
"parent_id": "00000000-0000-4000-8000-000000000002"
},
"meta": null
}省略 parent_id 或传 null 创建顶层节点;不指定相邻位置则追加到同级末尾。
PATCH /api/runtime/models/org_demo/update/00000000-0000-4000-8000-000000000004
Content-Type: application/json
{
"name": "基础平台组"
}HTTP 200
{
"data": {
"id": "00000000-0000-4000-8000-000000000004",
"name": "基础平台组",
"parent_id": "00000000-0000-4000-8000-000000000002"
},
"meta": null
}此接口不能传 parent_id 或 sibling_rank,调整结构用 tree/move。各示例独立,不按修改后的名称串联。
GET /api/runtime/models/org_demo/tree/roots?limit=100HTTP 200
{
"data": {
"items": [
{
"id": "00000000-0000-4000-8000-000000000001",
"name": "研发中心",
"parent_id": null
}
],
"has_more": false,
"next_cursor": null
},
"meta": null
}GET /api/runtime/models/org_demo/tree/children/00000000-0000-4000-8000-000000000001?limit=100HTTP 200
{
"data": {
"items": [
{
"id": "00000000-0000-4000-8000-000000000002",
"name": "平台部",
"parent_id": "00000000-0000-4000-8000-000000000001"
},
{
"id": "00000000-0000-4000-8000-000000000003",
"name": "应用部",
"parent_id": "00000000-0000-4000-8000-000000000001"
}
],
"has_more": false,
"next_cursor": null
},
"meta": null
}通用 roots/children 的 items 直接放记录,没有统一的 has_children 包装;组织专用列表会补充该字段。
GET /api/runtime/models/org_demo/tree/ancestors/00000000-0000-4000-8000-000000000004HTTP 200
{
"data": [
{
"id": "00000000-0000-4000-8000-000000000001",
"name": "研发中心",
"parent_id": null
},
{
"id": "00000000-0000-4000-8000-000000000002",
"name": "平台部",
"parent_id": "00000000-0000-4000-8000-000000000001"
}
],
"meta": null
}GET /api/runtime/models/org_demo/tree/descendants/00000000-0000-4000-8000-000000000001?limit=100&include_path=trueHTTP 200
{
"data": {
"items": [
{
"record": {
"id": "00000000-0000-4000-8000-000000000002",
"name": "平台部",
"parent_id": "00000000-0000-4000-8000-000000000001"
},
"depth": 1,
"has_children": true,
"path": [
"00000000-0000-4000-8000-000000000001",
"00000000-0000-4000-8000-000000000002"
]
},
{
"record": {
"id": "00000000-0000-4000-8000-000000000004",
"name": "基础设施组",
"parent_id": "00000000-0000-4000-8000-000000000002"
},
"depth": 2,
"has_children": false,
"path": [
"00000000-0000-4000-8000-000000000001",
"00000000-0000-4000-8000-000000000002",
"00000000-0000-4000-8000-000000000004"
]
},
{
"record": {
"id": "00000000-0000-4000-8000-000000000003",
"name": "应用部",
"parent_id": "00000000-0000-4000-8000-000000000001"
},
"depth": 1,
"has_children": false,
"path": [
"00000000-0000-4000-8000-000000000001",
"00000000-0000-4000-8000-000000000003"
]
}
],
"has_more": false,
"next_cursor": null
},
"meta": null
}depth 相对被查询节点计算(直接子节点为 1)。path 从被查询节点开始,包含结果节点,是 ID 数组,不是原始 ltree 字段。未开启 include_path=true 时 path 为 null。max_depth=1 只读直接子节点;省略 max_depth 则按页遍历全部深度。
GET /api/runtime/models/org_demo/tree/search?prefix=%E5%9F%BA%E7%A1%80&limit=20HTTP 200
{
"data": {
"items": [
{
"record": {
"id": "00000000-0000-4000-8000-000000000001",
"name": "研发中心",
"parent_id": null
},
"is_match": false
},
{
"record": {
"id": "00000000-0000-4000-8000-000000000002",
"name": "平台部",
"parent_id": "00000000-0000-4000-8000-000000000001"
},
"is_match": false
},
{
"record": {
"id": "00000000-0000-4000-8000-000000000004",
"name": "基础设施组",
"parent_id": "00000000-0000-4000-8000-000000000002"
},
"is_match": true
}
],
"has_more": false,
"next_cursor": null
},
"meta": null
}这里的前缀为“基础”。匹配非系统 String/Enum/Text 业务字段,忽略大小写,是前缀匹配,不是任意位置模糊包含。limit 只计算命中节点,items 还包含祖先;跨页按 ID 合并并保留 is_match=true。内置组织目录搜索使用下方的组织专用入口。
POST /api/runtime/models/org_demo/tree/move/00000000-0000-4000-8000-000000000003
Content-Type: application/json
{
"new_parent_id": "00000000-0000-4000-8000-000000000001",
"before_id": "00000000-0000-4000-8000-000000000002"
}HTTP 200
{
"data": {
"moved": true
},
"meta": null
}这里将应用部放到同一研发中心下的平台部之前。后插使用 after_id,不能同时传两个锚点;锚点必须属于目标父节点。new_parent_id: null 移到顶层,省略此字段也解析为顶层,所以同级排序请显式传当前父节点。不传两个位置锚点则追加。不能移到自己或自己的后代下面。
DELETE /api/runtime/models/org_demo/delete/00000000-0000-4000-8000-000000000004HTTP 200
{
"data": {
"deleted": true
},
"meta": null
}POST /api/runtime/models/org_demo/tree/delete-subtree/00000000-0000-4000-8000-000000000002
Content-Type: application/json
{
"expected_affected_count": 2
}HTTP 200
{
"data": {
"deleted_count": 2
},
"meta": null
}预计数量包含被选节点,这里平台部 + 基础设施组 = 2。可完整遍历后代后加 1,不能把单页长度当总数。并发变化导致数量不符时拒绝执行;这是数量保护,不是节点集合的快照保证。普通删除遇到非叶节点会拒绝。
{
"data": {
"items": [
{
"id": "00000000-0000-4000-8000-000000000002",
"name": "平台部",
"parent_id": "00000000-0000-4000-8000-000000000001"
}
],
"has_more": true,
"next_cursor": "<opaque-cursor-from-server>"
},
"meta": null
}上方游标是占位符,不可直接调用。把服务端实际返回的 next_cursor 原样进行 URL 编码,作为同一查询的 cursor。模型、作用域、分区和过滤条件保持一致。has_more=false 时结束,此时 next_cursor=null。
- roots/children/descendants 默认
limit=100,通用 search 默认20,都是批次大小,不是树容量限制。 - 已移除应用层的 256 层、1000 条结果、100 个搜索命中限制;仍须符合输入整数类型与 PostgreSQL 存储本身的边界。
- 无效或跨查询游标返回 HTTP 400;锚点删除、移动或相关排序变化返回 HTTP 409、
tree_stale_cursor,丢弃游标重新加载。 - 权限不足、节点不存在、成环、相邻位置无效、删除数量不符均属于失败,不是部分成功;不要把错误当成空页。
- 锚点校验与单页查询处于同一只读 repeatable-read 事务。跨请求是实时视图,不是长快照;并发插入/跨已读边界移动可能漏读或重复,重载取得当前视图。一致性导出需要专门的快照/导出能力。
| 方法 | 路径 | 作用 |
|---|---|---|
| GET | /api/console/settings/departments/access |
当前用户可执行的组织操作 |
| GET | /api/console/settings/departments |
顶层 / 子部门 / 搜索 / 已选 ID 补查 |
| POST | /api/console/settings/departments |
新增部门并关联角色 |
| PATCH | /api/console/settings/departments/{id} |
保存名称、上级与角色关联 |
| DELETE | /api/console/settings/departments/{id} |
按组织业务约束删除部门 |
| PUT | /api/console/settings/members/{id}/departments |
全量替换成员部门与主部门 |
| GET | /api/console/settings/members?department_id={id} |
查询该部门及全部下级部门的用户 |
POST /api/console/settings/departments
Content-Type: application/json
{
"name": "研发中心",
"parent_id": null,
"role_codes": []
}HTTP 201
{
"data": {
"id": "00000000-0000-4000-8000-000000000001",
"name": "研发中心",
"parent_id": null,
"role_codes": [],
"member_count": 0
},
"meta": null
}role_codes 必填,不关联角色传 [];关联时传已有角色编码,不会新建角色记录。建好下级后可如下查询(人数 3 为示意)。
GET /api/console/settings/departments?parent_id=00000000-0000-4000-8000-000000000001&limit=100HTTP 200
{
"data": {
"items": [
{
"id": "00000000-0000-4000-8000-000000000002",
"name": "平台部",
"parent_id": "00000000-0000-4000-8000-000000000001",
"role_codes": [],
"member_count": 3,
"has_children": true,
"is_match": true
},
{
"id": "00000000-0000-4000-8000-000000000003",
"name": "应用部",
"parent_id": "00000000-0000-4000-8000-000000000001",
"role_codes": [],
"member_count": 0,
"has_children": false,
"is_match": true
}
],
"has_more": false,
"next_cursor": null
},
"meta": null
}不传过滤条件读顶层;prefix=基础 按名称前缀搜索;ids=<id1>,<id2> 补查已有选择。parent_id、prefix、ids 三者互斥。各模式均使用分页外层,默认 limit=100(包括组织专用搜索)。部门字段直接平铺,不包装在 record 中;member_count 是该部门子树内的去重用户数。普通列表节点为命中项,仅搜索补充的祖先上下文为 is_match=false。
PATCH /api/console/settings/departments/00000000-0000-4000-8000-000000000002
Content-Type: application/json
{
"name": "平台部",
"parent_id": "00000000-0000-4000-8000-000000000001",
"role_codes": []
}HTTP 200
{
"data": {
"id": "00000000-0000-4000-8000-000000000002",
"name": "平台部",
"parent_id": "00000000-0000-4000-8000-000000000001",
"role_codes": [],
"member_count": 3
},
"meta": null
}这里传完整保存对象(name、parent_id、role_codes),不要仅传局部变更字段。
PUT /api/console/settings/members/00000000-0000-4000-8000-000000000005/departments
Content-Type: application/json
{
"department_ids": [
"00000000-0000-4000-8000-000000000002",
"00000000-0000-4000-8000-000000000003"
],
"primary_department_id": "00000000-0000-4000-8000-000000000002"
}HTTP 204
(No response body.)
允许多个部门;有部门归属时必须恰好设置一个主部门,且主部门在 department_ids 中。清空归属传 department_ids: []、primary_department_id: null。直接加入的部门关联角色补充用户已有权限,不隐式继承祖先部门角色。浏览某棵子树不等于加入部门或获得权限。
选中虚拟根时,成员请求省略 department_id;选中具体部门时由后端做子树过滤,不依赖 UI 是否展开下级节点。
DELETE /api/console/settings/departments/00000000-0000-4000-8000-000000000004HTTP 204,无响应体。要求该部门没有子部门、没有成员绑定,否则拒绝。组织控制台没有把通用子树删除命令开放成组织批量删除接口。
GET /api/console/settings/departments/accessHTTP 200
{
"data": {
"can_list": true,
"can_create": true,
"can_update": true,
"can_delete": true,
"can_replace_member_departments": true,
"can_assign_roles": true
},
"meta": null
}以上布尔值为示例,以当前用户实际返回值为准;每个操作仍由后端鉴权。
flowchart TD
A["Open tree"] --> B["Load roots page"]
B --> C{"User action"}
C -->|Expand| D["Load direct children"]
C -->|Search| E["Prefix matches + ancestors"]
C -->|More| F["Same query + next_cursor"]
C -->|Select department| G["Members filtered by subtree"]
C -->|Move custom node| H["Validate parent / position / cycle"]
H --> I["Atomically update parent, rank and descendant paths"]
F --> J{"Cursor valid?"}
J -->|Yes| K["Return page"]
J -->|Stale: 409| L["Discard cursor and reload"]