Files
KSP_project/docs/superpowers/specs/2026-07-09-guanghan-topology-database-design.md

22 KiB
Raw Permalink Blame History

广寒基地 Wiki 时态拓扑数据库设计

日期:2026-07-09

状态:设计确认稿

适用范围:广寒基地拓扑图、模块详情和历史状态查询

1. 目标

为广寒基地 Wiki 拓扑页面建立独立的数据源,使页面能够:

  • 根据明确时间点还原当时的完整基地拓扑与模块状态;
  • 根据任务编号(如 GHC-05)定位该任务最后一条记录的时间,并展示该时刻的完整基地状态;
  • 在页面内调整时间,查看拓扑、连接、模块信息和状态的变化;
  • 保存任务的“已发生 / 未发生(已规划)”状态,为未来展示规划拓扑预留能力;
  • 保证所有拓扑和状态变化都能追溯到具体任务及任务事件。

本数据库不包含“全局模拟时间”。查询时间只属于单次页面请求或页面当前视图,不会写回数据库,也不会影响其他访问者。

2. 系统边界

2.1 独立数据库

在现有 PostgreSQL 服务实例中创建独立数据库:

建议数据库名:ksp_wiki_topology
建议 Schematopology

该数据库与以下系统保持逻辑和数据隔离:

  • Wiki.js 自身数据库;
  • Operation Hangar 业务数据库;
  • Operation Hangar 的当前时间或模拟时间设置。

PostgreSQL 服务地址、端口和基础设施可以复用,但数据库、用户权限、迁移记录和备份策略应独立。

2.2 访问方式

Wiki 页面不直接连接 PostgreSQL,而是通过只读 HTTP API 获取快照:

Wiki 页面 → 拓扑只读 API → ksp_wiki_topology

数据库写入应由独立的导入、管理或迁移流程完成。API 使用只读数据库账号。

3. 核心设计选择

采用“稳定实体 + 时效区间 + 任务事件”的时态图模型。

  • 稳定实体:基地、模块、设备、车辆、安装口等长期身份;
  • 时效区间:名称、尺寸、状态、安装关系和连接关系在什么时间有效;
  • 任务事件:哪一个任务在什么时间造成了变化;
  • 可选缓存:未来可以物化完整快照,但缓存不是事实来源。

不采用“每个任务保存一份完整快照”,以免大量重复和历史修正不一致;也不采用“查询时从头重放全部事件”,以免页面查询复杂且性能不可预测。

3.1 时间区间约定

所有有效期统一采用左闭右开区间:

[valid_from, valid_to)

例如某连接在 2035-01-01T08:00:00Z 建立,在 2035-02-01T08:00:00Z 拆除,则它在起始时刻有效,在拆除时刻不再有效。

valid_to IS NULL 表示尚未失效。

所有业务时间使用 PostgreSQL timestamptzAPI 使用带时区的 ISO 8601 字符串。建议以 UTC 存储和交换,前端按显示需要转换。

4. 查询语义

4.1 参数优先级

建议快照接口:

GET /api/v1/bases/guanghan/snapshot
GET /api/v1/bases/guanghan/snapshot?mission=GHC-05
GET /api/v1/bases/guanghan/snapshot?at=2035-01-01T08:00:00Z
GET /api/v1/bases/guanghan/snapshot?mission=GHC-05&at=2035-02-01T08:00:00Z

时间解析规则:

请求参数 快照时间 任务编号的作用
at + mission 严格采用 at 页面上下文、标注和导航,不限制快照数据
at 严格采用 at
mission 该任务最后一条有效事件的时间 决定默认快照时间
均未提供 所有已发生任务中最新一条有效事件的时间

关键约束:

  • 指定任务时,快照仍是该时刻的完整基地状态,不是“只展示该任务产生的数据”;
  • 同时指定任务和时间时,即使时间早于或晚于该任务,仍按时间展示;
  • Wiki 页面生成链接或请求时应尽可能携带任务编号;
  • 不存在任何可由页面读取或修改的全局当前时间。

4.2 已发生与规划任务

第一阶段仅录入和查询已发生任务。

任务状态预留:

occurred  已发生
planned   未发生 / 已规划

默认查询只纳入 occurred。未来启用规划展示时,应增加显式参数,例如:

GET /api/v1/bases/guanghan/snapshot?mission=GHC-12&view=planned

规划数据不得在默认查询中混入已发生事实。若未来需要多套互斥规划,应在后续版本增加规划场景表,而不是让冲突规划共用同一时间线。

在规划场景投影功能实现前,可以保存 planned 任务及其事件,但这些事件不得关闭或改写已发生时间线中的有效区间。未来开始保存规划拓扑时,必须先为时态事实增加 scenario_id,以“已发生时间线 + 指定规划场景增量”的方式查询;不能直接用规划事件填写实际记录的 valid_to

5. 概念模型

erDiagram
    BASES ||--o{ COMPONENTS : contains
    BASES ||--o{ MISSIONS : scopes
    MISSIONS ||--|{ MISSION_EVENTS : contains
    COMPONENTS ||--o{ COMPONENT_VERSIONS : describes
    COMPONENTS ||--o{ COMPONENT_STATES : has
    COMPONENTS ||--|{ PORTS : exposes
    PORTS ||--o{ TOPOLOGY_CONNECTIONS : endpoint_a
    PORTS ||--o{ TOPOLOGY_CONNECTIONS : endpoint_b
    MISSION_EVENTS ||--o{ COMPONENTS : introduces
    MISSION_EVENTS ||--o{ COMPONENT_VERSIONS : starts
    MISSION_EVENTS ||--o{ COMPONENT_STATES : starts
    MISSION_EVENTS ||--o{ TOPOLOGY_CONNECTIONS : establishes
    DIAGRAM_LAYOUTS ||--o{ DIAGRAM_NODES : contains
    COMPONENTS ||--o{ DIAGRAM_NODES : renders

    BASES {
        uuid id PK
        text code UK
        text name
    }
    MISSIONS {
        uuid id PK
        uuid base_id FK
        citext code UK
        text occurrence_status
        text title
    }
    MISSION_EVENTS {
        uuid id PK
        uuid mission_id FK
        timestamptz effective_at
        integer event_order
        text event_type
    }
    COMPONENTS {
        uuid id PK
        uuid base_id FK
        text component_key UK
        text component_type
    }
    PORTS {
        uuid id PK
        uuid component_id FK
        text port_key
        text direction
    }
    TOPOLOGY_CONNECTIONS {
        uuid id PK
        uuid port_a_id FK
        uuid port_b_id FK
        text connection_type
        timestamptz valid_from
        timestamptz valid_to
    }

6. 表设计

以下字段为第一阶段必须字段。所有表建议包含 created_atupdated_at;写入账号负责维护,API 不得修改。

6.1 bases

基地身份。虽然首期只有广寒基地,仍保留基地层级,避免所有表写死单一基地。

字段 类型 约束 / 含义
id uuid 主键,默认 gen_random_uuid()
code text 唯一,如 guanghan
name text 如“广寒基地”
description text 可空

6.2 missions

任务主表。

字段 类型 约束 / 含义
id uuid 主键
base_id uuid 外键 bases.id
code citext 任务编号,如 GHC-05
title text 任务名称
occurrence_status text occurredplanned
description text 可空
source_ref text Timeline、Wiki 或其他来源标识

约束:

  • UNIQUE (base_id, code)
  • occurrence_status 使用检查约束或枚举;
  • 不在任务表中保存“当前时间”;
  • 不要求额外的“正式完成时间”。任务默认时间由其事件的最大业务时间计算。

6.3 mission_events

任务内具有业务时间的原子事件。所有模块、状态和连接变化必须追溯到事件,事件必须归属任务。

字段 类型 约束 / 含义
id uuid 主键
mission_id uuid 非空,外键 missions.id
effective_at timestamptz 事件实际生效时间
event_order integer 同一时刻内的稳定排序,从 0 开始
event_type text component_installedconnection_established
title text 简短说明
details jsonb 可选的结构化补充信息
source_ref text 原始记录位置或文档引用
recorded_at timestamptz 数据录入时间,不参与历史快照判断

约束:

  • UNIQUE (mission_id, effective_at, event_order)
  • 任务“最后一条记录时间”为 MAX(effective_at)
  • recorded_at 只表达何时录入,不能替代 effective_at

如果历史资料只能精确到日期,可在 details.time_precision 中记录 date,同时将 effective_at 统一落在约定时刻。不得用无时区时间或模糊字符串参与排序。

6.4 components

基地中可独立识别和点击的对象,包括舱段、结构节点、设备、着陆器、漫游车等。

字段 类型 约束 / 含义
id uuid 主键
base_id uuid 非空,外键 bases.id
component_key text 稳定机器标识,如 core_cabin
component_type text modulenodevehicleequipment
introduced_by_event_id uuid 非空,首次出现事件
retired_by_event_id uuid 可空,移除或退役事件
metadata jsonb 不参与核心查询的扩展字段

约束:

  • UNIQUE (base_id, component_key)
  • component_key 一旦发布不得因改名而变化;
  • 对象是否存在由引入和退役事件的 effective_at 判断;
  • 首期所有组件必须由已发生任务事件引入。

6.5 component_versions

模块信息的时态版本。名称、简介、尺寸或详情链接发生变化时新增版本,不覆盖历史。

字段 类型 约束 / 含义
id uuid 主键
component_id uuid 非空,外键 components.id
display_name text 页面显示名称
summary text 弹窗摘要
description text 详细信息
length_m numeric(10,3) 可空
width_m numeric(10,3) 可空
height_m numeric(10,3) 可空
diameter_m numeric(10,3) 可空
mass_t numeric(12,3) 可空
detail_url text Wiki 详情页地址,可空
properties jsonb 特有参数,如乘员、功率、容量
valid_from timestamptz 非空
valid_to timestamptz 可空
started_by_event_id uuid 非空,版本开始事件
ended_by_event_id uuid 可空,版本结束事件

同一组件的版本有效期不得重叠。尺寸字段使用 SI 单位;页面负责格式化,不在数据库中保存“约 6 米”之类不可计算文本。

6.6 component_states

模块状态按维度分别保存,避免任一小状态变化都复制整份模块信息。

字段 类型 约束 / 含义
id uuid 主键
component_id uuid 非空
state_kind text 状态维度
state_value text 状态值
details jsonb 可选读数、原因或备注
valid_from timestamptz 非空
valid_to timestamptz 可空
started_by_event_id uuid 非空
ended_by_event_id uuid 可空

建议首期状态维度:

state_kind 示例值
lifecycle installedretired
operation operationalstandbyofflinefault
occupancy uncrewedcrewed
construction completeunder_construction

同一组件、同一状态维度的有效期不得重叠。允许某个维度没有记录,此时 API 返回 unknown,不得擅自推断正常。

6.7 ports

组件安装口或逻辑连接端点。

字段 类型 约束 / 含义
id uuid 主键
component_id uuid 非空
port_key text 组件内稳定标识,如 north
display_name text 如“北部安装口”
direction text northeastsouthwest 或自定义
interface_type text 安装、对接、管线等接口类型
properties jsonb 可空

约束:

  • UNIQUE (component_id, port_key)
  • 十字结构必须明确建立四个端口,不能把“温室模块”等普通舱段误建为四向节点;
  • 没有物理安装口但需要连接的对象,可以建立命名明确的逻辑端口。

6.8 topology_connections

拓扑图中的边。结构直连、加压走道、燃料/供电连接等都使用同一表,通过类型区分。

字段 类型 约束 / 含义
id uuid 主键
base_id uuid 非空
port_a_id uuid 非空,外键 ports.id
port_b_id uuid 非空,外键 ports.id
connection_type text 连接类型
properties jsonb 方向、容量等扩展信息
valid_from timestamptz 非空
valid_to timestamptz 可空
established_by_event_id uuid 非空
ended_by_event_id uuid 可空

建议连接类型:

类型 页面语义
structural_mount 结构直连
pressurized_passage 加压走道,渲染为绿色实线
docking 临时或长期对接
power 供电关系
fuel 燃料关系
cooling 冷却回路

同一端口在同一时间能否存在多个连接,应由 interface_type 决定。结构安装口默认只允许一个有效结构连接;逻辑管线端口可以允许多个。

6.9 diagram_layoutsdiagram_nodes

拓扑事实和视觉排版分离。数据库可以保存页面布局,但坐标不是物理位置,也不改变基地历史。

diagram_layouts

字段 类型 含义
id uuid 主键
base_id uuid 基地
layout_key text engineering_overview_v1
name text 布局名称
canvas_width numeric 逻辑画布宽度
canvas_height numeric 逻辑画布高度
is_default boolean 默认布局

diagram_nodes

字段 类型 含义
layout_id uuid 布局
component_id uuid 对象
xy numeric 中心坐标
widthheight numeric 图形尺寸
rotation_deg numeric 旋转角
style_key text 样式类别
label_override text 可空,原则上使用版本名称
z_index integer 层级

连接线由端口和组件坐标计算。加压走道不作为独立外形组件,仅作为 pressurized_passage 类型连接渲染;顶部图例负责说明,无需在线上重复标签。

布局中不显示未来扩展标记。未来模块真正录入并在所选时间有效后,才会出现在快照中。

7. 数据完整性

7.1 推荐 PostgreSQL 扩展

CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE EXTENSION IF NOT EXISTS citext;
CREATE EXTENSION IF NOT EXISTS btree_gist;

7.2 有效期

建议在时态表中增加生成列:

valid_period tstzrange
  GENERATED ALWAYS AS (
    tstzrange(valid_from, valid_to, '[)')
  ) STORED

以模块信息版本为例,禁止同一组件的版本重叠:

EXCLUDE USING gist (
  component_id WITH =,
  valid_period WITH &&
);

component_states 使用 (component_id, state_kind, valid_period) 排斥重叠;结构连接根据端口占用规则设置部分排斥约束。

7.3 事件一致性

写入服务必须验证:

  • started_by_event_id 的时间等于对应 valid_from
  • ended_by_event_id 的时间等于对应 valid_to
  • 结束事件不得早于开始事件;
  • 引用的任务与组件属于同一基地;
  • 所有变化都有任务事件,不允许“无来源修改”;
  • 已发生事实默认不得引用 planned 任务事件。

复杂的跨表、跨事件验证建议通过事务写入服务实现,并辅以延迟约束触发器。不要仅依赖前端表单。

7.4 历史纠错

业务时间与录入时间必须分开:

  • effective_at / valid_from:事情何时发生;
  • recorded_at / created_at:何时写入数据库。

历史资料纠错不应直接静默覆盖。建议保留审计表,记录表名、记录 ID、旧值、新值、操作者、原因和修改时间。第一阶段不必实现完整双时态查询,但必须具备审计能力。

8. 快照查询

8.1 确定锚点时间

伪代码:

if at is provided:
    snapshot_at = parse(at)
    anchor_source = "explicit_time"
else if mission is provided:
    assert mission exists
    snapshot_at = max(effective_at of events in that mission)
    anchor_source = "mission_latest_event"
else:
    snapshot_at = max(effective_at of events whose mission is occurred)
    anchor_source = "latest_recorded_state"

若指定任务不存在,返回 404 mission_not_found

若任务存在但没有事件,返回 409 mission_has_no_timed_event

若数据库完全没有有效事件,返回空基地快照,而不是服务器当前时间。

8.2 选择有效数据

对每张时态表采用同一条件:

valid_from <= :snapshot_at
AND (valid_to IS NULL OR :snapshot_at < valid_to)

组件存在条件由引入、退役事件时间计算。查询结果必须包含:

  • 在该时刻存在的组件及有效信息版本;
  • 每个组件当时有效的各维度状态;
  • 两端组件均存在且当时有效的连接;
  • 默认布局中的节点信息;
  • 任务上下文和实际采用的快照时间。

8.3 任务筛选不是数据过滤

例如:

GET /snapshot?mission=GHC-05

先取得 GHC-05 最后事件时间,再查询截至该时间的完整历史结果。因此快照会包含 GHC-01 至 GHC-05 留存下来的全部有效设施。

例如:

GET /snapshot?mission=GHC-05&at=<GHC-06之后的时间>

仍按明确时间展示 GHC-06 之后的基地;GHC-05 仅作为页面当前任务上下文。

9. API 返回建议

{
  "base": {
    "code": "guanghan",
    "name": "广寒基地"
  },
  "query": {
    "requestedAt": null,
    "requestedMission": "GHC-05",
    "snapshotAt": "2035-01-01T08:00:00Z",
    "anchorSource": "mission_latest_event",
    "view": "occurred"
  },
  "components": [
    {
      "key": "core_cabin",
      "type": "module",
      "name": "广寒核心舱",
      "dimensions": {
        "lengthM": 8.0,
        "widthM": 4.0,
        "heightM": 4.0
      },
      "states": {
        "lifecycle": "installed",
        "operation": "operational"
      },
      "properties": {},
      "ports": []
    }
  ],
  "connections": [
    {
      "type": "pressurized_passage",
      "fromPort": "habitat_1.south",
      "toPort": "power_north.north"
    }
  ],
  "layout": {
    "key": "engineering_overview_v1",
    "nodes": []
  }
}

响应应返回最终 snapshotAt,使页面时间控件准确定位。页面切换时间只重新请求快照,不修改服务端状态。

10. 索引

第一阶段至少建立:

missions(base_id, code) UNIQUE
missions(base_id, occurrence_status)
mission_events(mission_id, effective_at DESC, event_order DESC)
mission_events(effective_at DESC)
components(base_id, component_key) UNIQUE
component_versions(component_id, valid_from DESC)
component_states(component_id, state_kind, valid_from DESC)
topology_connections(base_id, valid_from DESC)
ports(component_id, port_key) UNIQUE
diagram_nodes(layout_id, component_id) UNIQUE

时态表的 GiST 排斥约束会同时提供区间索引能力。数据量增长后,可针对“当前仍有效”的记录增加 WHERE valid_to IS NULL 部分索引。

11. 权限、部署与备份

建议角色:

角色 权限
topology_owner 数据库与迁移所有者,不供应用日常使用
topology_writer 管理和导入流程读写业务表
topology_reader API 只读访问

部署要求:

  • 使用独立连接串,例如 WIKI_TOPOLOGY_DATABASE_URL
  • Wiki.js 不持有数据库密码,只访问 API;
  • API 只向允许的 Wiki 来源开放 CORS;
  • 数据库迁移使用独立版本表;
  • 数据库备份可复用现有 PostgreSQL 基础设施,但必须能够单独恢复;
  • 日志不得输出完整连接串或密码。

12. 首期数据导入

建议按以下顺序导入:

  1. 创建广寒基地;
  2. 创建已发生任务及其事件,至少覆盖现有 Timeline 中的 GHC 系列记录;
  3. 创建稳定组件和端口;
  4. 写入组件信息版本与尺寸;
  5. 写入组件状态区间;
  6. 写入结构直连、加压走道和其他连接;
  7. 写入当前工程总图布局;
  8. 对每个任务编号生成快照,与 Timeline 和现有拓扑图逐项核对。

GHC-05 验收快照至少应验证:

  • 核心舱是四向安装节点;
  • 居住舱 #2 位于核心舱北部,居住舱 #1 位于南部;
  • 居住舱 #1 通过加压走道连接电力模块北部舱段;
  • 温室模块通过加压走道连接吴刚二号;
  • 电力模块为独立十字结构,其北、南、东、西模块关系正确;
  • 电力模块相关四个大型模块均显示散热器信息;
  • 加压走道在页面渲染为绿色实线,不显示重复标签;
  • 页面不显示尚未发生的未来扩展标记。

13. 测试与验收

13.1 数据库约束测试

  • 同一组件同一状态维度的重叠区间必须写入失败;
  • 同一组件的重叠信息版本必须写入失败;
  • 无任务事件来源的变化必须写入失败;
  • 结束时间早于开始时间必须写入失败;
  • 重复任务编号、组件键和端口键必须写入失败。

13.2 查询测试

  • 仅任务编号:采用该任务最大事件时间;
  • 仅明确时间:采用明确时间;
  • 时间与任务同时存在:采用明确时间,任务不裁剪数据;
  • 无参数:采用已发生任务的全库最大事件时间;
  • 明确时间早于首个任务:返回空拓扑;
  • 规划任务默认不进入事实快照;
  • 任意历史时间点查询不得依赖服务器当前时间。

13.3 页面验收

  • URL 可携带任务编号和时间;
  • 页面调整时间后,拓扑和模块弹窗信息同步变化;
  • 刷新或复制 URL 能还原同一视图;
  • 两位用户选择不同时间互不影响;
  • API 异常时页面显示明确错误,不使用过期数据冒充当前结果。

14. 明确排除项

本阶段只形成数据库与查询契约设计,不实施:

  • PostgreSQL 建库和迁移脚本;
  • 数据导入程序;
  • 拓扑 API
  • Wiki 页面动态加载和时间控件;
  • 规划场景分支系统;
  • 完整双时态历史查询;
  • 快照物化缓存。

这些功能应以本文档的数据语义为后续实现依据,尤其不得重新引入全局模拟时间,或直接复用 Operation Hangar 的时间设置。