22 KiB
广寒基地 Wiki 时态拓扑数据库设计
日期:2026-07-09
状态:设计确认稿
适用范围:广寒基地拓扑图、模块详情和历史状态查询
1. 目标
为广寒基地 Wiki 拓扑页面建立独立的数据源,使页面能够:
- 根据明确时间点还原当时的完整基地拓扑与模块状态;
- 根据任务编号(如
GHC-05)定位该任务最后一条记录的时间,并展示该时刻的完整基地状态; - 在页面内调整时间,查看拓扑、连接、模块信息和状态的变化;
- 保存任务的“已发生 / 未发生(已规划)”状态,为未来展示规划拓扑预留能力;
- 保证所有拓扑和状态变化都能追溯到具体任务及任务事件。
本数据库不包含“全局模拟时间”。查询时间只属于单次页面请求或页面当前视图,不会写回数据库,也不会影响其他访问者。
2. 系统边界
2.1 独立数据库
在现有 PostgreSQL 服务实例中创建独立数据库:
建议数据库名:ksp_wiki_topology
建议 Schema:topology
该数据库与以下系统保持逻辑和数据隔离:
- 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 timestamptz,API 使用带时区的 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_at 和 updated_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 |
occurred 或 planned |
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_installed、connection_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 |
module、node、vehicle、equipment 等 |
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 |
installed、retired |
operation |
operational、standby、offline、fault |
occupancy |
uncrewed、crewed |
construction |
complete、under_construction |
同一组件、同一状态维度的有效期不得重叠。允许某个维度没有记录,此时 API 返回 unknown,不得擅自推断正常。
6.7 ports
组件安装口或逻辑连接端点。
| 字段 | 类型 | 约束 / 含义 |
|---|---|---|
id |
uuid |
主键 |
component_id |
uuid |
非空 |
port_key |
text |
组件内稳定标识,如 north |
display_name |
text |
如“北部安装口” |
direction |
text |
north、east、south、west 或自定义 |
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_layouts 与 diagram_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 |
对象 |
x、y |
numeric |
中心坐标 |
width、height |
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. 首期数据导入
建议按以下顺序导入:
- 创建广寒基地;
- 创建已发生任务及其事件,至少覆盖现有 Timeline 中的 GHC 系列记录;
- 创建稳定组件和端口;
- 写入组件信息版本与尺寸;
- 写入组件状态区间;
- 写入结构直连、加压走道和其他连接;
- 写入当前工程总图布局;
- 对每个任务编号生成快照,与 Timeline 和现有拓扑图逐项核对。
GHC-05 验收快照至少应验证:
- 核心舱是四向安装节点;
- 居住舱 #2 位于核心舱北部,居住舱 #1 位于南部;
- 居住舱 #1 通过加压走道连接电力模块北部舱段;
- 温室模块通过加压走道连接吴刚二号;
- 电力模块为独立十字结构,其北、南、东、西模块关系正确;
- 电力模块相关四个大型模块均显示散热器信息;
- 加压走道在页面渲染为绿色实线,不显示重复标签;
- 页面不显示尚未发生的未来扩展标记。
13. 测试与验收
13.1 数据库约束测试
- 同一组件同一状态维度的重叠区间必须写入失败;
- 同一组件的重叠信息版本必须写入失败;
- 无任务事件来源的变化必须写入失败;
- 结束时间早于开始时间必须写入失败;
- 重复任务编号、组件键和端口键必须写入失败。
13.2 查询测试
- 仅任务编号:采用该任务最大事件时间;
- 仅明确时间:采用明确时间;
- 时间与任务同时存在:采用明确时间,任务不裁剪数据;
- 无参数:采用已发生任务的全库最大事件时间;
- 明确时间早于首个任务:返回空拓扑;
- 规划任务默认不进入事实快照;
- 任意历史时间点查询不得依赖服务器当前时间。
13.3 页面验收
- URL 可携带任务编号和时间;
- 页面调整时间后,拓扑和模块弹窗信息同步变化;
- 刷新或复制 URL 能还原同一视图;
- 两位用户选择不同时间互不影响;
- API 异常时页面显示明确错误,不使用过期数据冒充当前结果。
14. 明确排除项
本阶段只形成数据库与查询契约设计,不实施:
- PostgreSQL 建库和迁移脚本;
- 数据导入程序;
- 拓扑 API;
- Wiki 页面动态加载和时间控件;
- 规划场景分支系统;
- 完整双时态历史查询;
- 快照物化缓存。
这些功能应以本文档的数据语义为后续实现依据,尤其不得重新引入全局模拟时间,或直接复用 Operation Hangar 的时间设置。