docs: design Guanghan temporal topology database
This commit is contained in:
@@ -0,0 +1,666 @@
|
|||||||
|
# 广寒基地 Wiki 时态拓扑数据库设计
|
||||||
|
|
||||||
|
日期:2026-07-09
|
||||||
|
|
||||||
|
状态:设计确认稿
|
||||||
|
|
||||||
|
适用范围:广寒基地拓扑图、模块详情和历史状态查询
|
||||||
|
|
||||||
|
## 1. 目标
|
||||||
|
|
||||||
|
为广寒基地 Wiki 拓扑页面建立独立的数据源,使页面能够:
|
||||||
|
|
||||||
|
- 根据明确时间点还原当时的完整基地拓扑与模块状态;
|
||||||
|
- 根据任务编号(如 `GHC-05`)定位该任务最后一条记录的时间,并展示该时刻的完整基地状态;
|
||||||
|
- 在页面内调整时间,查看拓扑、连接、模块信息和状态的变化;
|
||||||
|
- 保存任务的“已发生 / 未发生(已规划)”状态,为未来展示规划拓扑预留能力;
|
||||||
|
- 保证所有拓扑和状态变化都能追溯到具体任务及任务事件。
|
||||||
|
|
||||||
|
本数据库不包含“全局模拟时间”。查询时间只属于单次页面请求或页面当前视图,不会写回数据库,也不会影响其他访问者。
|
||||||
|
|
||||||
|
## 2. 系统边界
|
||||||
|
|
||||||
|
### 2.1 独立数据库
|
||||||
|
|
||||||
|
在现有 PostgreSQL 服务实例中创建独立数据库:
|
||||||
|
|
||||||
|
```text
|
||||||
|
建议数据库名:ksp_wiki_topology
|
||||||
|
建议 Schema:topology
|
||||||
|
```
|
||||||
|
|
||||||
|
该数据库与以下系统保持逻辑和数据隔离:
|
||||||
|
|
||||||
|
- Wiki.js 自身数据库;
|
||||||
|
- Operation Hangar 业务数据库;
|
||||||
|
- Operation Hangar 的当前时间或模拟时间设置。
|
||||||
|
|
||||||
|
PostgreSQL 服务地址、端口和基础设施可以复用,但数据库、用户权限、迁移记录和备份策略应独立。
|
||||||
|
|
||||||
|
### 2.2 访问方式
|
||||||
|
|
||||||
|
Wiki 页面不直接连接 PostgreSQL,而是通过只读 HTTP API 获取快照:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Wiki 页面 → 拓扑只读 API → ksp_wiki_topology
|
||||||
|
```
|
||||||
|
|
||||||
|
数据库写入应由独立的导入、管理或迁移流程完成。API 使用只读数据库账号。
|
||||||
|
|
||||||
|
## 3. 核心设计选择
|
||||||
|
|
||||||
|
采用“稳定实体 + 时效区间 + 任务事件”的时态图模型。
|
||||||
|
|
||||||
|
- 稳定实体:基地、模块、设备、车辆、安装口等长期身份;
|
||||||
|
- 时效区间:名称、尺寸、状态、安装关系和连接关系在什么时间有效;
|
||||||
|
- 任务事件:哪一个任务在什么时间造成了变化;
|
||||||
|
- 可选缓存:未来可以物化完整快照,但缓存不是事实来源。
|
||||||
|
|
||||||
|
不采用“每个任务保存一份完整快照”,以免大量重复和历史修正不一致;也不采用“查询时从头重放全部事件”,以免页面查询复杂且性能不可预测。
|
||||||
|
|
||||||
|
### 3.1 时间区间约定
|
||||||
|
|
||||||
|
所有有效期统一采用左闭右开区间:
|
||||||
|
|
||||||
|
```text
|
||||||
|
[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 参数优先级
|
||||||
|
|
||||||
|
建议快照接口:
|
||||||
|
|
||||||
|
```http
|
||||||
|
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 已发生与规划任务
|
||||||
|
|
||||||
|
第一阶段仅录入和查询已发生任务。
|
||||||
|
|
||||||
|
任务状态预留:
|
||||||
|
|
||||||
|
```text
|
||||||
|
occurred 已发生
|
||||||
|
planned 未发生 / 已规划
|
||||||
|
```
|
||||||
|
|
||||||
|
默认查询只纳入 `occurred`。未来启用规划展示时,应增加显式参数,例如:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/v1/bases/guanghan/snapshot?mission=GHC-12&view=planned
|
||||||
|
```
|
||||||
|
|
||||||
|
规划数据不得在默认查询中混入已发生事实。若未来需要多套互斥规划,应在后续版本增加规划场景表,而不是让冲突规划共用同一时间线。
|
||||||
|
|
||||||
|
在规划场景投影功能实现前,可以保存 `planned` 任务及其事件,但这些事件不得关闭或改写已发生时间线中的有效区间。未来开始保存规划拓扑时,必须先为时态事实增加 `scenario_id`,以“已发生时间线 + 指定规划场景增量”的方式查询;不能直接用规划事件填写实际记录的 `valid_to`。
|
||||||
|
|
||||||
|
## 5. 概念模型
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
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 扩展
|
||||||
|
|
||||||
|
```sql
|
||||||
|
CREATE EXTENSION IF NOT EXISTS pgcrypto;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS citext;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS btree_gist;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7.2 有效期
|
||||||
|
|
||||||
|
建议在时态表中增加生成列:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
valid_period tstzrange
|
||||||
|
GENERATED ALWAYS AS (
|
||||||
|
tstzrange(valid_from, valid_to, '[)')
|
||||||
|
) STORED
|
||||||
|
```
|
||||||
|
|
||||||
|
以模块信息版本为例,禁止同一组件的版本重叠:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
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 确定锚点时间
|
||||||
|
|
||||||
|
伪代码:
|
||||||
|
|
||||||
|
```text
|
||||||
|
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 选择有效数据
|
||||||
|
|
||||||
|
对每张时态表采用同一条件:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
valid_from <= :snapshot_at
|
||||||
|
AND (valid_to IS NULL OR :snapshot_at < valid_to)
|
||||||
|
```
|
||||||
|
|
||||||
|
组件存在条件由引入、退役事件时间计算。查询结果必须包含:
|
||||||
|
|
||||||
|
- 在该时刻存在的组件及有效信息版本;
|
||||||
|
- 每个组件当时有效的各维度状态;
|
||||||
|
- 两端组件均存在且当时有效的连接;
|
||||||
|
- 默认布局中的节点信息;
|
||||||
|
- 任务上下文和实际采用的快照时间。
|
||||||
|
|
||||||
|
### 8.3 任务筛选不是数据过滤
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /snapshot?mission=GHC-05
|
||||||
|
```
|
||||||
|
|
||||||
|
先取得 GHC-05 最后事件时间,再查询截至该时间的完整历史结果。因此快照会包含 GHC-01 至 GHC-05 留存下来的全部有效设施。
|
||||||
|
|
||||||
|
例如:
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /snapshot?mission=GHC-05&at=<GHC-06之后的时间>
|
||||||
|
```
|
||||||
|
|
||||||
|
仍按明确时间展示 GHC-06 之后的基地;`GHC-05` 仅作为页面当前任务上下文。
|
||||||
|
|
||||||
|
## 9. API 返回建议
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"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. 索引
|
||||||
|
|
||||||
|
第一阶段至少建立:
|
||||||
|
|
||||||
|
```text
|
||||||
|
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 的时间设置。
|
||||||
Reference in New Issue
Block a user