Skip to content

Ordered Tree Tables CN

JAVA-Lw edited this page Oct 7, 2026 · 1 revision

树状表:接口说明与组织表示例

English

本文介绍 core/ordered_tree/v1,按 2026-10-07 的 dev 源码核对。示例是模拟契约数据,不是生产环境抓包;记录省略审计等其他模型字段,保留 HTTP 的 data / meta 外层。调用时请换成当前工作区的真实 ID。

1. 先选对接口

  • 内置 departments 在主数据源(/settings/data-models?source=main)可见,通用运行时接口只读。创建、编辑、删除部门、关联角色和维护成员归属,应使用组织控制台专用接口。
  • 自建的可写树状模板表按权限开放下列通用操作。为完整演示,请创建模型编码为 org_demo、含必填字符串业务字段 name 的树状表。它是组织层级示例表,不会自动成为系统部门目录。
  • 调用需要认证及当前工作区权限;使用 Cookie 认证的写请求沿用现有 CSRF 协议(x-csrf-token)。示例省略凭证。游标不能替代授权。

2. 示例层级与存储

flowchart TD
    Root["组织 / Organization: virtual root"] --> A["研发中心 / R&D"]
    A --> B["平台部 / Platform"]
    A --> C["应用部 / Applications"]
    B --> D["基础设施组 / Infrastructure"]
Loading
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 路径字典序;移动子树会在同一事务中更新后代路径。

3. 通用接口清单

统一前缀:/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 权限。即使模板描述了写操作,内置只读模型的能力限制仍然生效。

4. 通用请求与返回

4.1 普通列表

GET /api/runtime/models/org_demo/list?page=1&page_size=20

HTTP 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
}

4.2 单条读取

GET /api/runtime/models/org_demo/get/00000000-0000-4000-8000-000000000002

HTTP 200

{
  "data": {
    "id": "00000000-0000-4000-8000-000000000002",
    "name": "平台部",
    "parent_id": "00000000-0000-4000-8000-000000000001"
  },
  "meta": null
}

4.3 新增节点

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 创建顶层节点;不指定相邻位置则追加到同级末尾。

4.4 修改业务字段

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。各示例独立,不按修改后的名称串联。

4.5 顶层节点

GET /api/runtime/models/org_demo/tree/roots?limit=100

HTTP 200

{
  "data": {
    "items": [
      {
        "id": "00000000-0000-4000-8000-000000000001",
        "name": "研发中心",
        "parent_id": null
      }
    ],
    "has_more": false,
    "next_cursor": null
  },
  "meta": null
}

4.6 直接子节点

GET /api/runtime/models/org_demo/tree/children/00000000-0000-4000-8000-000000000001?limit=100

HTTP 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 包装;组织专用列表会补充该字段。

4.7 祖先链

GET /api/runtime/models/org_demo/tree/ancestors/00000000-0000-4000-8000-000000000004

HTTP 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
}

4.8 后代与路径

GET /api/runtime/models/org_demo/tree/descendants/00000000-0000-4000-8000-000000000001?limit=100&include_path=true

HTTP 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 则按页遍历全部深度。

4.9 前缀搜索

GET /api/runtime/models/org_demo/tree/search?prefix=%E5%9F%BA%E7%A1%80&limit=20

HTTP 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。内置组织目录搜索使用下方的组织专用入口。

4.10 移动与同级排序

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 移到顶层,省略此字段也解析为顶层,所以同级排序请显式传当前父节点。不传两个位置锚点则追加。不能移到自己或自己的后代下面。

4.11 删除叶节点

DELETE /api/runtime/models/org_demo/delete/00000000-0000-4000-8000-000000000004

HTTP 200

{
  "data": {
    "deleted": true
  },
  "meta": null
}

4.12 删除整棵子树

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,不能把单页长度当总数。并发变化导致数量不符时拒绝执行;这是数量保护,不是节点集合的快照保证。普通删除遇到非叶节点会拒绝。

5. 分页与错误处理

{
  "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 事务。跨请求是实时视图,不是长快照;并发插入/跨已读边界移动可能漏读或重复,重载取得当前视图。一致性导出需要专门的快照/导出能力。

6. 真实组织专用接口

方法 路径 作用
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} 查询该部门及全部下级部门的用户

6.1 新增顶层部门

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 为示意)。

6.2 展开直接子部门

GET /api/console/settings/departments?parent_id=00000000-0000-4000-8000-000000000001&limit=100

HTTP 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。

6.3 编辑部门

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),不要仅传局部变更字段。

6.4 设置多部门与主部门

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 是否展开下级节点。

6.5 删除空叶部门

DELETE /api/console/settings/departments/00000000-0000-4000-8000-000000000004

HTTP 204,无响应体。要求该部门没有子部门、没有成员绑定,否则拒绝。组织控制台没有把通用子树删除命令开放成组织批量删除接口。

6.6 查询可用操作

GET /api/console/settings/departments/access

HTTP 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
}

以上布尔值为示例,以当前用户实际返回值为准;每个操作仍由后端鉴权。

7. 推荐加载流程

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"]
Loading

8. 实现依据

Clone this wiki locally