refactor(db)!: adopt project-routed pooled databases
Reorganize WNDB by responsibility and remove legacy scheme endpoints.\n\nRoute analysis and time-series access through project pools, preserve transactional realtime replacement, and refresh GIS materialized views after writes.\n\nAdd database architecture documentation, live pooling coverage, API contract updates, and executable container verification.\n\nBREAKING CHANGE: legacy scheme APIs and flat app.native.wndb module imports are removed.
This commit is contained in:
@@ -0,0 +1,454 @@
|
||||
# TJWater 数据库改造说明与当前结构
|
||||
|
||||
> 本文记录 2026-08-25 的数据库实际状态。结构、约束、行数、TimescaleDB chunk 和策略均直接读取数据库,不以仓库中的 SQL 脚本为依据。文中不包含主机、端口、账号、密码或 DSN。
|
||||
|
||||
## 改造范围与当前状态
|
||||
|
||||
本次改造保留原 `tjwater` 业务库和时序库,新建 `tjwater_next` 作为隔离验证环境。元数据库仍为 `system_hub`,新项目通过 `biz_data` 和 `iot_data` 两条路由分别关联新业务库与新时序库。项目完成迁移和联调后已切换为 `active`,原数据库没有被覆盖,仍可用于对照和回退。
|
||||
|
||||
已经完成的数据库修改包括:
|
||||
|
||||
- 新建并迁移 `tjwater_next` 业务库,将旧 `public` 中混合存放的管网、GIS、SCADA 配置和分析数据按领域拆分。
|
||||
- 新建并迁移 `tjwater_next` 时序库,将 SCADA、实时计算和分析计算结果分开存放。
|
||||
- `realtime` 采用冷热数据策略,72 小时后的 chunk 自动转为有序列存。
|
||||
- `analysis` 按 `stored_at` 分区,入库满 24 小时的 chunk 自动转为有序列存。
|
||||
- GIS 查询层改用物化视图,当前 7 张物化视图均已填充。
|
||||
- GeoServer 已建立 `tjwater_next` 工作空间和同名数据存储,从业务库 `gis` schema 发布 7 个图层。GeoWebCache 的服务端与客户端缓存有效期均为 300 秒。
|
||||
- 旧库中的 `operation`、`current_operation`、`batch_operation`、`operation_table`、`restore_operation` 和 `snapshot_operation` 没有进入新业务库。
|
||||
- `system_hub.public` 补充了项目数据库外键、数据库路由约束、连接池约束、必要的非空约束,以及 5 张表和 44 个字段的中文数据库注释。
|
||||
- 用户角色和项目角色仍是可扩展字符串,没有增加枚举检查约束。
|
||||
- `audit_logs.user_id` 和 `audit_logs.project_id` 仍为逻辑关联,没有增加外键。
|
||||
- 业务库 48 个表或物化视图、192 个字段,以及时序库 7 张表、47 个字段均已写入中文数据库注释。
|
||||
- 后端已对接新 schema。WNDB、PostgreSQL 管理连接和同步 TimescaleDB 访问均使用有界连接池,闲置项目按最近使用顺序回收,实时覆盖写入使用单一事务。
|
||||
- 后端批量元素查询读取 GIS 物化视图,模型增删改和 INP 导入提交后执行并发刷新;批量事务只刷新一次。
|
||||
- `pattern_values`、`pattern_flow_samples`、`curve_points`、`demands` 和 `link_vertices` 的顺序号按所属父对象编号,主键已改为父对象 ID 与 `sequence_no` 的复合键。
|
||||
|
||||
`tjwater_next` 当前为 `active`。排水项目 `lingang` 已迁入 `system_hub.public`,供水和排水后端共用同一套项目、成员、数据库路由和审计表。
|
||||
|
||||
## 数据库总体关系
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph META["system_hub 元数据库"]
|
||||
MP["public<br/>统一元数据<br/>5 个项目"]
|
||||
end
|
||||
|
||||
subgraph OLD["原数据库,保持不变"]
|
||||
OB["tjwater 业务库<br/>public"]
|
||||
OT["tjwater 时序库<br/>scada / realtime / scheme"]
|
||||
end
|
||||
|
||||
subgraph NEXT["隔离验证数据库"]
|
||||
NB["tjwater_next 业务库<br/>network / gis / asset / analysis"]
|
||||
NT["tjwater_next 时序库<br/>scada / realtime / analysis"]
|
||||
end
|
||||
|
||||
GS["GeoServer<br/>tjwater_next 工作空间"]
|
||||
WEB["供水前端<br/>tjwater_next 图层配置"]
|
||||
|
||||
MP -->|"tjwater 的 biz_data"| OB
|
||||
MP -->|"tjwater 的 iot_data"| OT
|
||||
MP -->|"tjwater_next 的 biz_data"| NB
|
||||
MP -->|"tjwater_next 的 iot_data"| NT
|
||||
MP -->|"lingang 的两条数据库路由"| DRAIN["排水项目数据库"]
|
||||
NB -->|"gis 物化视图"| GS -->|"WFS / WMTS"| WEB
|
||||
```
|
||||
|
||||
一个项目对应一个业务数据库和一个时序数据库。项目本地表不保存 `project_id`,项目边界由元数据库路由和数据库连接共同确定。
|
||||
|
||||
## 元数据库 system_hub
|
||||
|
||||
### public:当前主元数据
|
||||
|
||||
`public` 当前有 5 个项目、1 个用户、5 条成员关系和 10 条数据库路由。5 个项目均配置了一条 `biz_data` 和一条 `iot_data` 路由。审计日志数量会随接口请求持续增加,不在文档中固化行数。
|
||||
|
||||
| 表 | 用途 | 主要字段 |
|
||||
| --- | --- | --- |
|
||||
| `users` | Keycloak 用户身份快照和系统授权状态 | `id`、`keycloak_id`、`username`、`email`、`role`、`is_active`、`is_superuser`、`attributes`、时间字段 |
|
||||
| `projects` | 项目基本信息和地图配置 | `id`、`name`、`code`、`gs_workspace`、`map_extent`、`map_config`、`status`、时间字段 |
|
||||
| `user_project_membership` | 用户和项目的成员关系 | `id`、`user_id`、`project_id`、`project_role` |
|
||||
| `project_databases` | 项目业务库和时序库路由 | `id`、`project_id`、`db_role`、`db_type`、`dsn_encrypted`、`pool_min_size`、`pool_max_size` |
|
||||
| `audit_logs` | 独立保留的操作审计 | `id`、`user_id`、`project_id`、`action`、资源字段、请求字段、`response_status`、`timestamp` |
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
USERS {
|
||||
uuid id PK
|
||||
uuid keycloak_id UK
|
||||
varchar username UK
|
||||
varchar email UK
|
||||
varchar role
|
||||
boolean is_active
|
||||
boolean is_superuser
|
||||
}
|
||||
|
||||
PROJECTS {
|
||||
uuid id PK
|
||||
varchar code UK
|
||||
varchar name
|
||||
varchar gs_workspace UK
|
||||
jsonb map_extent
|
||||
jsonb map_config
|
||||
varchar status
|
||||
}
|
||||
|
||||
USER_PROJECT_MEMBERSHIP {
|
||||
uuid id PK
|
||||
uuid user_id FK
|
||||
uuid project_id FK
|
||||
varchar project_role
|
||||
}
|
||||
|
||||
PROJECT_DATABASES {
|
||||
uuid id PK
|
||||
uuid project_id FK
|
||||
varchar db_role
|
||||
varchar db_type
|
||||
text dsn_encrypted
|
||||
int pool_min_size
|
||||
int pool_max_size
|
||||
}
|
||||
|
||||
AUDIT_LOGS {
|
||||
uuid id PK
|
||||
uuid user_id
|
||||
uuid project_id
|
||||
varchar action
|
||||
timestamptz timestamp
|
||||
}
|
||||
|
||||
USERS ||--o{ USER_PROJECT_MEMBERSHIP : "参与项目"
|
||||
PROJECTS ||--o{ USER_PROJECT_MEMBERSHIP : "包含成员"
|
||||
PROJECTS ||--o{ PROJECT_DATABASES : "配置路由"
|
||||
```
|
||||
|
||||
数据库强制执行以下关系和约束:
|
||||
|
||||
- 成员表的 `user_id`、`project_id` 分别引用用户和项目,删除用户或项目时级联删除成员关系。
|
||||
- `project_databases.project_id` 引用项目,删除项目时级联删除数据库路由。
|
||||
- 同一项目的 `db_role` 唯一。
|
||||
- `biz_data` 必须使用 `postgresql`,`iot_data` 必须使用 `timescaledb`。
|
||||
- `pool_min_size` 不小于 1,`pool_max_size` 不小于 `pool_min_size`。
|
||||
- 项目状态限定为 `active`、`inactive` 或 `archived`。
|
||||
- 审计表中的用户和项目 ID 不设置外键,删除业务对象不会连带删除历史日志。
|
||||
|
||||
### 排水元数据合并
|
||||
|
||||
排水后端原先使用独立的 `hub` schema,其中有 1 个 `lingang` 项目、2 条数据库路由和 203 条审计记录,没有用户或成员关系。项目 UUID 保持不变,两条路由转换为 `public.project_databases` 使用的 Fernet 加密格式,连接池参数由 `pool_size + max_overflow` 映射为 `pool_min_size + pool_max_size`。原审计记录已迁入 `public.audit_logs`。
|
||||
|
||||
排水后端已改用 `public.projects`、`public.project_databases`、`public.user_project_membership`、`public.users` 和 `public.audit_logs`。供水和排水服务读取同一份项目状态、Keycloak 身份、项目权限及数据库路由。原 `hub` schema 已在迁移校验和连接测试通过后删除。
|
||||
|
||||
## 原业务库 tjwater
|
||||
|
||||
原业务库将大部分业务对象放在 `public`,当前有 68 张表、9 张视图和 2 张物化视图。管网模型、GIS、SCADA 配置、分析结果、方案数据、临时表和操作记录混在同一命名空间中。
|
||||
|
||||
其中还安装了 `postgis_tiger_geocoder` 和 `postgis_topology`,因此存在 `tiger` 和 `topology` 扩展 schema。新业务库只保留当前实际使用的 PostGIS 能力,没有继续安装这两个扩展。
|
||||
|
||||
## 相对原数据库的结构变化
|
||||
|
||||
以下对比以当前仍保留的原业务库 `tjwater`、原时序库 `tjwater` 与新库 `tjwater_next` 的实际对象为准。对象名称的对应关系表示业务实体或数据职责的迁移方向,不表示所有字段均一对一复制。
|
||||
|
||||
### 归并和调整
|
||||
|
||||
| 原库对象或职责 | 新库对象或职责 | 调整内容 |
|
||||
| --- | --- | --- |
|
||||
| `_node`、`junctions`、`reservoirs`、`tanks` | `network.nodes` 及节点类型子表 | 节点统一由主表管理,类型专有字段保留在共享主键子表。 |
|
||||
| `_link`、`pipes`、`pumps`、`valves` | `network.links` 及连接类型子表 | 连接统一记录端点和类型,管道、泵、阀门参数移入对应子表。 |
|
||||
| `coordinates`、`vertices` | `gis.node_geometries`、`gis.link_vertices` | 管网几何从模型参数中拆出,分别保存节点位置和连接折点。 |
|
||||
| 旧的 GIS 视图和物化视图 | `gis` 下 7 张物化视图 | 前端查询层统一为节点、连接和设备的物化视图,底层仍读取 `network`、`gis`、`asset` 的规范化表。 |
|
||||
| SCADA 设备配置表 | `asset.scada_devices` | 设备配置集中到资产域,并以外键关联一个节点或一条连接。当前已迁入 118 台设备。 |
|
||||
| `scheme_list` 及方案结果相关表 | `analysis.runs`、`analysis.results` | 运行批次与非时序摘要结果留在业务库,逐时逐元素结果迁入时序库。当前已迁入 124 次运行和 17 条非时序结果。 |
|
||||
| `scada.scada_data` | `scada.measurements` | SCADA 测量值迁入新时序库并按设备和时间保存。 |
|
||||
| `realtime.node_simulation`、`realtime.link_simulation` | `realtime.node_results`、`realtime.link_results` | 实时仿真结果保留为节点和连接两类时序数据。 |
|
||||
| `scheme.node_simulation`、`scheme.link_simulation` | `analysis.node_results`、`analysis.link_results` | 方案或分析的元素时序结果以 `run_id` 区分批次,和业务库中的 `analysis.runs` 形成逻辑关联。 |
|
||||
|
||||
### 已删减且未迁入的对象
|
||||
|
||||
- 云端操作记录相关的 `operation`、`current_operation`、`batch_operation`、`operation_table`、`restore_operation`、`snapshot_operation` 未进入新业务库。该设计不再作为业务数据模型的一部分。
|
||||
- 临时处理表 `temp_link_1`、`temp_link_2`、`temp_node`、`temp_region`、`temp_vd_topology` 未迁入。它们属于历史处理过程的中间对象,不应成为长期库结构。
|
||||
- 原库中的 `_node`、`_link`、`_pattern`、`_curve`、`_region` 等内部或过渡表不再单独存在。新库以明确的领域表和外键关系表达同一类数据。
|
||||
- 原库的 `tiger`、`topology` 扩展 schema 未在新库安装,`tjwater_next` 仅保留 PostGIS 及其 `public` 系统对象。
|
||||
|
||||
### 尚未完整承接的范围
|
||||
|
||||
原库有 `region`、`region_dma`、`region_sa`、`region_vd`、`region_wda` 五张区域细分表。检查时这些表均无数据,因此当前新库只建立了通用的 `gis.regions`、`gis.region_nodes`,没有为 DMA、分区计量、分区调度或用水分区固化专用结构。
|
||||
|
||||
这不是删除已有业务数据,而是暂缓固化尚未使用的模型。后续确认 DMA 和 `VA` 的业务含义后,再决定是在 `gis` 中增加区域类型专有表,还是放入独立的业务 schema。当前库中没有名为 `va` 的表;若该名称指阀门,则对应 `network.valves`,若指 `region_vd`,则属于上述尚未迁入的区域细分模型。
|
||||
|
||||
### 新增的结构能力
|
||||
|
||||
- 业务库由单一 `public` 命名空间拆为 `network`、`gis`、`asset`、`analysis` 四个领域 schema,降低模型、空间数据、设备配置和分析记录之间的耦合。
|
||||
- `gis` 增加了面向前端的 7 张物化视图,以及 `gis.refresh_all_materialized_views` 刷新过程。后端在模型批量修改和 INP 导入提交后统一刷新,不使用数据库触发器或定时任务。
|
||||
- 新时序库增加 `migration` schema,用于保留迁移过程的设置和日志,不与业务时序数据混放。
|
||||
- `realtime` 两张 hypertable 已启用 72 小时后的列存压缩策略,`analysis` 两张 hypertable 按入库时间执行 24 小时冷热转换。`scada` 保持独立行存。
|
||||
- 元数据库的 `public.project_databases` 增加项目外键、数据库角色和类型约束,以及连接池上下限约束,用于保证每个项目的业务库和时序库路由有效。
|
||||
|
||||
## 新业务库 tjwater_next
|
||||
|
||||
新业务库使用 PostgreSQL 和 PostGIS。业务对象分布在 4 个 schema 中,`public` 只保留 PostGIS 提供的系统对象。
|
||||
|
||||
| Schema | 普通表 | 物化视图 | 用途 |
|
||||
| --- | ---: | ---: | --- |
|
||||
| `network` | 32 | 0 | 供水管网模型、仿真参数、规则和曲线 |
|
||||
| `gis` | 6 | 7 | 原始空间数据、区域关系和前端查询层 |
|
||||
| `asset` | 1 | 0 | SCADA 设备配置及其管网关联 |
|
||||
| `analysis` | 2 | 0 | 分析运行记录和非时序结果 |
|
||||
|
||||
当前迁移数据包含 87,907 个节点、91,054 条连接、118 个 SCADA 设备、124 次分析运行和 17 条非时序分析结果。其中有 87,894 个普通节点、13 个水源、91,052 条管道和 2 个阀门。
|
||||
|
||||
### network:管网模型
|
||||
|
||||
`network.nodes` 和 `network.links` 是节点、连接的统一主表。具体类型通过共享主键的一对一子表扩展:
|
||||
|
||||
- 节点:`junctions`、`reservoirs`、`tanks`。
|
||||
- 连接:`pipes`、`pumps`、`valves`。
|
||||
- 模式和曲线:`patterns`、`pattern_values`、`pattern_flow_samples`、`curves`、`curve_points`。
|
||||
- 节点附属数据:`demands`、`emitters`、`sources`、`initial_quality`、`tank_mixing`、`node_tags`。
|
||||
- 连接附属数据:`link_initial_settings`、`link_tags`、`pump_energy_settings`、`pipe_reaction_coefficients`、`tank_reaction_coefficients`。
|
||||
- 模型配置:`controls`、`rules`、`simulation_settings`、`time_settings`、`report_settings`、`energy_settings`、`reaction_settings`、`model_titles`。
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
NODES {
|
||||
text id PK
|
||||
text node_type
|
||||
}
|
||||
JUNCTIONS {
|
||||
text node_id PK,FK
|
||||
float elevation
|
||||
}
|
||||
RESERVOIRS {
|
||||
text node_id PK,FK
|
||||
float head
|
||||
text pattern_id FK
|
||||
}
|
||||
TANKS {
|
||||
text node_id PK,FK
|
||||
float elevation
|
||||
float initial_level
|
||||
text volume_curve_id FK
|
||||
}
|
||||
LINKS {
|
||||
text id PK
|
||||
text link_type
|
||||
text start_node_id FK
|
||||
text end_node_id FK
|
||||
}
|
||||
PIPES {
|
||||
text link_id PK,FK
|
||||
float length
|
||||
float diameter
|
||||
text status
|
||||
}
|
||||
PUMPS {
|
||||
text link_id PK,FK
|
||||
float power
|
||||
text head_curve_id FK
|
||||
text pattern_id FK
|
||||
}
|
||||
VALVES {
|
||||
text link_id PK,FK
|
||||
float diameter
|
||||
text valve_type
|
||||
text setting
|
||||
}
|
||||
PATTERNS {
|
||||
text id PK
|
||||
}
|
||||
CURVES {
|
||||
text id PK
|
||||
text curve_type
|
||||
}
|
||||
|
||||
NODES ||--o| JUNCTIONS : "节点类型"
|
||||
NODES ||--o| RESERVOIRS : "节点类型"
|
||||
NODES ||--o| TANKS : "节点类型"
|
||||
NODES ||--o{ LINKS : "起点"
|
||||
NODES ||--o{ LINKS : "终点"
|
||||
LINKS ||--o| PIPES : "连接类型"
|
||||
LINKS ||--o| PUMPS : "连接类型"
|
||||
LINKS ||--o| VALVES : "连接类型"
|
||||
PATTERNS ||--o{ RESERVOIRS : "水位模式"
|
||||
PATTERNS ||--o{ PUMPS : "运行模式"
|
||||
CURVES ||--o{ PUMPS : "扬程曲线"
|
||||
CURVES ||--o{ TANKS : "容积曲线"
|
||||
```
|
||||
|
||||
删除节点或连接时,对应类型子表、GIS 几何、标签和关联参数按外键规则同步删除。连接的起点和终点必须引用已有节点,且不能是同一个节点。
|
||||
|
||||
`pattern_values`、`pattern_flow_samples` 和 `curve_points` 的 `sequence_no` 分别表示同一模式或曲线内部的顺序,`demands.sequence_no` 表示同一节点下多条需水记录的顺序。因此,这四张明细表使用父对象 ID 与 `sequence_no` 组成主键,不要求顺序号在全表唯一。该约束与 WNDB 的局部编号、INP 导入和按父对象排序查询一致。
|
||||
|
||||
### gis:空间数据和查询层
|
||||
|
||||
基础空间表包括:
|
||||
|
||||
| 表 | 用途 | 主要关系 |
|
||||
| --- | --- | --- |
|
||||
| `node_geometries` | 节点点位 | `node_id` 引用 `network.nodes` |
|
||||
| `link_vertices` | 连接中间折点 | `link_id` 引用 `network.links` |
|
||||
| `labels` | 地图标注 | 可选关联节点 |
|
||||
| `backdrops` | 模型背景配置 | 单条配置记录 |
|
||||
| `regions` | 区域边界 | 保存区域类型和面几何 |
|
||||
| `region_nodes` | 区域和节点的多对多关系 | 同时引用区域和节点 |
|
||||
|
||||
`link_vertices` 使用 `(link_id, sequence_no)` 复合主键。折点顺序只在同一条连接内部有效,不同连接可以从相同的顺序号开始编号。
|
||||
|
||||
前端查询使用 7 张物化视图:`junctions`、`reservoirs`、`tanks`、`pipes`、`pumps`、`valves` 和 `scada_devices`。这些视图把模型属性和几何合并,避免前端与 GeoServer 每次重复执行跨表计算。当前 7 张视图均已填充,并为元素 ID 建立唯一索引,为几何建立 GiST 索引。
|
||||
|
||||
原始模型几何继续使用项目坐标系 `EPSG:900914`。点要素物化视图中的 `x`、`y` 仍保存该坐标系下的模型坐标,供 WNDB 和 INP 读写使用;提供给 GeoServer 的 `geom` 统一转换为 `EPSG:3857`。管道发布为线,水泵和阀门使用连接线中点发布为点,SCADA 设备按设备自有位置、关联节点位置或关联连接中点依次取值。当前数据库中的 7 个 `geom` 字段均已核对为 `EPSG:3857`。
|
||||
|
||||
数据库中的 `gis.refresh_all_materialized_views(boolean)` 存储过程统一刷新全部物化视图。后端批量读取普通节点、水源、水箱、管道、水泵、阀门和 SCADA 设备时直接查询这些视图;单条模型增删改提交后立即刷新,批量修改和 INP 导入成功提交后只刷新一次。实际库中一次全量并发刷新约 6 秒。视图已有数据和唯一索引,因此刷新期间查询仍可继续。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
N["network 节点和连接"] --> G["gis 原始几何"]
|
||||
A["asset.scada_devices"] --> G
|
||||
N --> MV["gis 物化视图"]
|
||||
G --> MV
|
||||
A --> MV
|
||||
MV --> GS["GeoServer tjwater_next"]
|
||||
GS --> WFS["WFS 要素查询"]
|
||||
GS --> GWC["GeoWebCache WMTS"]
|
||||
WFS --> F["供水前端"]
|
||||
GWC --> F
|
||||
R["refresh_all_materialized_views"] --> MV
|
||||
```
|
||||
|
||||
### GeoServer 与前端图层
|
||||
|
||||
`system_hub.public.projects` 中的 `tjwater_next` 项目已配置 `gs_workspace=tjwater_next`,当前状态为 `active`。GeoServer 的 `tjwater_next` 数据存储连接同名业务库并限定到 `gis` schema,图层名称直接采用物化视图名称。7 个图层使用相同的项目管网发布边界,空图层和视口内没有要素的瓦片会返回空 MVT,不会产生越界错误。
|
||||
|
||||
| 前端数据源 | GeoServer 图层 | 几何 | 当前要素数 |
|
||||
| --- | --- | --- | ---: |
|
||||
| `junctions` | `tjwater_next:junctions` | Point,`EPSG:3857` | 87,894 |
|
||||
| `reservoirs` | `tjwater_next:reservoirs` | Point,`EPSG:3857` | 13 |
|
||||
| `tanks` | `tjwater_next:tanks` | Point,`EPSG:3857` | 0 |
|
||||
| `pipes` | `tjwater_next:pipes` | LineString,`EPSG:3857` | 91,052 |
|
||||
| `pumps` | `tjwater_next:pumps` | Point,`EPSG:3857` | 0 |
|
||||
| `valves` | `tjwater_next:valves` | Point,`EPSG:3857` | 2 |
|
||||
| `scada` | `tjwater_next:scada_devices` | Point,`EPSG:3857` | 118 |
|
||||
|
||||
供水前端的 `refactor/tjwater-next-integration` 分支已切换到这些图层和新字段名。地图切片走 `WebMercatorQuad` 的 MVT,定位和详情查询走 WFS。旧 `geo_pipes_mat`、`geo_junctions_mat` 和其他 `geo_*` 图层名不再作为新前端的兼容别名。
|
||||
|
||||
方案查询统一读取 `/api/v1/analysis/runs`,详情和时序结果按 UUID `run_id` 关联。时间轴读取 `/api/v1/timeseries/analysis/runs/{run_id}/values`,爆管定位的模拟数据源也传递 `simulation_run_id`。监测点优化使用 `/api/v1/sensor-placement-runs`。前端不再调用旧的 `/schemes`、`/timeseries/schemes` 和 `/sensor-placement-schemes` 接口。
|
||||
|
||||
模型修改提交后,后端先调用 `gis.refresh_all_materialized_views(boolean)` 刷新数据库查询层。GeoWebCache 不会感知 PostgreSQL 物化视图刷新,因此 7 个新图层配置了 300 秒的服务端和客户端缓存有效期,前端最迟在 5 分钟后读到新瓦片。部署或批量迁移完成后仍可执行一次图层缓存清空,避免等待已有瓦片自然过期。
|
||||
|
||||
### asset:SCADA 设备配置
|
||||
|
||||
`asset.scada_devices` 保存设备类型、采集接口标识、传输模式、频率、可靠性和可选几何。每台设备必须关联一个节点或一条连接,不能同时关联两者。设备的历史测量值不放在业务库,保存在时序库 `scada.measurements`。
|
||||
|
||||
### analysis:分析运行和非时序结果
|
||||
|
||||
`analysis.runs` 表示一次实际执行,保存运行名称、类型、创建人、开始时间、状态和参数。当前没有单独的 `scenarios` 模板表。
|
||||
|
||||
每次执行都会创建新的 `run_id`,名称和业务时间相同也不会覆盖旧结果。扩展仿真开始写结果前,业务库先记录 `running`;时序库写入完成后更新为 `completed`,写入失败则保留为 `failed`。业务库和时序库据此共用同一个执行标识。
|
||||
|
||||
`analysis.results` 保存不适合放入时序表的结果摘要或结构化业务结果。每条结果必须属于一个运行,可以关联一个节点、一条连接,或者不关联任何管网元素。元素级时序结果保存在时序库,某次分析只有摘要结果、只有元素时序结果或两者都有都是合法状态。
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
RUNS {
|
||||
uuid run_id PK
|
||||
text name
|
||||
text run_type
|
||||
text created_by
|
||||
timestamptz started_at
|
||||
text status
|
||||
jsonb parameters
|
||||
}
|
||||
RESULTS {
|
||||
uuid result_id PK
|
||||
uuid run_id FK
|
||||
text result_type
|
||||
text node_id FK
|
||||
text link_id FK
|
||||
jsonb payload
|
||||
}
|
||||
RUNS ||--o{ RESULTS : "产生可选结果"
|
||||
```
|
||||
|
||||
### 业务表如何扩展
|
||||
|
||||
当前没有预建空的 `business` schema。管网模型放在 `network`,空间区域和 DMA 放在 `gis`,设备配置放在 `asset`,运行和结果放在 `analysis`。后续出现工单、巡检、告警或资产养护等明确业务实体时,再按稳定的业务边界增加独立 schema;不把新业务表重新堆回 `public`,也不为尚未确定的角色或流程提前建表。
|
||||
|
||||
## 原时序库 tjwater
|
||||
|
||||
原时序库运行 TimescaleDB 2.21.3,共有 5 张 hypertable:
|
||||
|
||||
- `scada.scada_data`
|
||||
- `realtime.node_simulation`
|
||||
- `realtime.link_simulation`
|
||||
- `scheme.node_simulation`
|
||||
- `scheme.link_simulation`
|
||||
|
||||
这些表均为行存,没有启用压缩策略。方案结果使用 `scheme_name` 关联业务库中的方案记录。
|
||||
|
||||
## 新时序库 tjwater_next
|
||||
|
||||
新时序库仍运行 TimescaleDB 2.21.3。业务时序数据分为 `scada`、`realtime` 和 `analysis`,迁移过程状态单独放在 `migration`。
|
||||
|
||||
| 表 | 主键 | Chunk 间隔 | 当前行数 | 当前占用 |
|
||||
| --- | --- | --- | ---: | ---: |
|
||||
| `scada.measurements` | `(time, device_id)` | 7 天 | 1,249,602 | 约 0.18 GiB |
|
||||
| `realtime.node_results` | `(time, node_id)` | 1 天 | 159,111,670 | 约 1.89 GiB |
|
||||
| `realtime.link_results` | `(time, link_id)` | 1 天 | 164,807,740 | 约 2.18 GiB |
|
||||
| `analysis.node_results` | `(stored_at, time, run_id, node_id)` | 1 天 | 21,800,936 | 约 2.93 GiB |
|
||||
| `analysis.link_results` | `(stored_at, time, run_id, link_id)` | 1 天 | 22,581,392 | 约 4.82 GiB |
|
||||
|
||||
### scada
|
||||
|
||||
`scada.measurements` 保存设备测量值和清洗值。`device_id` 与业务库的 `asset.scada_devices.device_id` 是跨数据库逻辑关联,数据库不能建立物理外键。
|
||||
|
||||
时序表当前有 118 个设备标识,与业务库的 118 台设备一一对应。迁移后曾发现设备标识 `11`、`12`、`13`、`14` 没有对应的 SCADA 点位配置;确认属于无效孤立数据后,已从新时序库删除其 5,376 条记录。原 `tjwater` 时序库保持不变,必要时仍可追溯迁移前数据。
|
||||
|
||||
### realtime
|
||||
|
||||
`realtime.node_results` 和 `realtime.link_results` 保存当前实时计算窗口。主键保证同一时刻、同一元素只能有一条记录。相同时间窗口由后端先删除、再通过 `COPY` 批量插入;节点和连接两次替换位于同一个最外层事务,其中任一步失败都会整体回滚。
|
||||
|
||||
两张表当前各有 19 个 chunk,均已转为列存,因为现有数据都早于 72 小时热窗口。数据库每小时执行一次策略检查,将 72 小时以前的 chunk 转为有序列存。节点结果按 `node_id, time DESC` 排序,连接结果按 `link_id, time DESC` 排序。新写入的数据使用 1 天 chunk,并在 72 小时内保持行存。
|
||||
|
||||
### analysis
|
||||
|
||||
`analysis.node_results` 和 `analysis.link_results` 保存每次分析运行的元素级时间序列,通过 `run_id` 与业务库 `analysis.runs` 逻辑关联。`time` 是仿真业务时刻,继续用于曲线和时间范围查询;`stored_at` 是该批结果的入库完成时间,作为 hypertable 的分区时间。
|
||||
|
||||
两张表按 1 天创建 chunk,最近 24 小时保持热数据。超过 24 小时的 chunk 会由压缩策略转为列存冷数据,策略按 `run_id` 与元素 ID 分段、按仿真 `time DESC` 排序。重新计算相同历史时间段时,新结果写入当前的 `stored_at` 热分区,不需要改写历史冷分区。当前各有 20 个 chunk,均因入库时间超过 24 小时而完成压缩。
|
||||
|
||||
迁移期间曾保留按仿真时间分区的 `*_time_partitioned_legacy` 表用于回退校验。清理前已核对 46 个 `run_id`:节点 21,800,936 行、连接 22,581,392 行的总数、逐运行数量、时间范围和全字段校验值均一致。两张旧表随后删除,释放约 12.16 GiB;原 `tjwater` 时序库仍保留,不受此次清理影响。
|
||||
|
||||
### migration
|
||||
|
||||
`migration.copy_log` 记录每个来源表、迁移时间窗口和源行数,当前有 150 条记录。`migration.settings` 保存迁移截止时间等运行参数,当前有 1 条记录。它们是迁移审计和断点信息,不是业务数据,也不是仍待处理的中间结果。正式数据迁移已经完成。
|
||||
|
||||
## 后端连接与事务
|
||||
|
||||
元数据库通过 SQLAlchemy 异步连接池访问;项目请求按 `system_hub.public.project_databases` 路由到业务库和时序库。异步业务查询和异步时序查询由项目级动态池管理,原生 WNDB 同步访问使用按数据库缓存的 `psycopg_pool.ConnectionPool`,数据库创建、复制和删除使用独立的 PostgreSQL 管理池,同步 TimescaleDB 访问也使用按数据库缓存的连接池。应用目录中已没有直接调用 `psycopg.connect` 的业务代码。
|
||||
|
||||
WNDB 批量修改和 INP 数据导入在同一条池连接和同一事务中执行,提交后再刷新 GIS 物化视图。实时节点和连接结果也在一个事务中执行先删后写,同一结果时间使用事务级锁避免并发覆盖竞态;分析结果按 `run_id` 加事务级锁,防止同一运行被并发写入两次。
|
||||
|
||||
自动化真实数据库测试分别执行 64 次业务库和 64 次时序库并发借用,查询结果一致,连接均能归还池中。嵌套 WNDB 写入和分析运行生命周期测试会在外层强制回滚,数据库没有残留记录。`DatabaseCommand` 的 pattern 新增、修改、删除也在同一池化事务中完成,并验证了五张明细表的复合主键、级联解除需求模式关联、结果变更和整体回滚。实时覆盖测试确认第二批数据替换第一批数据,外层回滚后测试记录为 0。
|
||||
|
||||
## 跨数据库逻辑关系
|
||||
|
||||
PostgreSQL 不能对另一个数据库中的表建立外键。业务库与时序库之间通过稳定 ID 保持逻辑一致性,完整性由迁移校验和后端事务负责。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
P["system_hub.public.projects"] -->|"biz_data 路由"| B["项目业务库"]
|
||||
P -->|"iot_data 路由"| T["项目时序库"]
|
||||
|
||||
BN["network.nodes.id"] -. "node_id" .-> RN["realtime.node_results"]
|
||||
BN -. "node_id" .-> AN["analysis.node_results"]
|
||||
BL["network.links.id"] -. "link_id" .-> RL["realtime.link_results"]
|
||||
BL -. "link_id" .-> AL["analysis.link_results"]
|
||||
SD["asset.scada_devices.device_id"] -. "device_id" .-> SM["scada.measurements"]
|
||||
AR["analysis.runs.run_id"] -. "run_id" .-> AN
|
||||
AR -. "run_id" .-> AL
|
||||
```
|
||||
|
||||
实线表示元数据库保存的项目路由,虚线表示跨数据库逻辑关联。每个项目使用独立业务库和时序库,不需要在项目本地表中重复保存 `project_id`。
|
||||
|
||||
## 仍需处理的事项
|
||||
|
||||
- 当前没有修改 SCADA 设备配置的后端接口。以后增加这类写接口时,也要在提交后调用统一的物化视图刷新函数。
|
||||
- 用户角色和项目角色还未定型,数据库只要求字段非空,不限制具体取值。
|
||||
@@ -0,0 +1,168 @@
|
||||
# WNDB 文件结构说明
|
||||
|
||||
本文记录 2026-08-25 完成的 WNDB 目录与命令结构重构。检查对象是当前代码,不以旧 SQL 脚本或历史目录为依据。
|
||||
|
||||
## 结论
|
||||
|
||||
当前结构适合继续维护。WNDB 已按连接基础设施、管网模型、GIS、INP 和命令执行分组,原来的编号文件名、根目录聚合门面和星号导入已经移除。WDA、SCADA 资产查询和测压点选址也已离开底层模型目录。
|
||||
|
||||
本次调整了代码文件、导入关系、命令分派和 WNDB 内部命令对象,没有改变 HTTP 接口。历史撤销日志已从数据库中移除,内部接口不再保留无效的兼容字段。真实库回归时发现五张明细表错误地把局部顺序号设成全局主键,已在 `tjwater_next` 中改为父对象 ID 与 `sequence_no` 的复合主键。
|
||||
|
||||
## 当前目录
|
||||
|
||||
```text
|
||||
app/native/wndb/
|
||||
├── __init__.py
|
||||
├── core/
|
||||
│ ├── connection.py
|
||||
│ ├── database.py
|
||||
│ └── projects.py
|
||||
├── model/
|
||||
│ ├── elements.py
|
||||
│ ├── junctions.py
|
||||
│ ├── reservoirs.py
|
||||
│ ├── tanks.py
|
||||
│ ├── pipes.py
|
||||
│ ├── pumps.py
|
||||
│ ├── valves.py
|
||||
│ ├── patterns.py
|
||||
│ ├── curves.py
|
||||
│ ├── options.py
|
||||
│ └── 其他 EPANET 模型模块
|
||||
├── gis/
|
||||
│ ├── coordinates.py
|
||||
│ ├── vertices.py
|
||||
│ ├── labels.py
|
||||
│ ├── backdrop.py
|
||||
│ ├── regions.py
|
||||
│ └── region_geometry.py
|
||||
├── inp/
|
||||
│ ├── sections.py
|
||||
│ ├── importer.py
|
||||
│ └── exporter.py
|
||||
└── commands/
|
||||
├── api.py
|
||||
├── cascade.py
|
||||
└── executor.py
|
||||
```
|
||||
|
||||
目录内共有 47 个 Python 文件,约 7,100 行。`app/native/wndb/__init__.py` 只保留包说明,不再统一导出所有函数。调用方需要从具体职责模块导入,依赖来源可以直接从文件头确认。
|
||||
|
||||
## 各目录的职责
|
||||
|
||||
### core:连接、事务和数据库基础能力
|
||||
|
||||
`connection.py` 管理项目连接池、管理连接池和项目事务上下文。连接池按路由后的 DSN 复用,并限制缓存规模。
|
||||
|
||||
`database.py` 提供 `ChangeSet`、`DatabaseCommand`、参数化查询和物化视图刷新。`DatabaseCommand` 只保存待执行的 SQL 和执行成功后返回给调用方的变更列表,不再生成或保存撤销 SQL。模型直接修改时按需刷新视图;批量命令在外层事务提交后只刷新一次。物化视图保留模型坐标 `x`、`y`,同时将供 GeoServer 使用的 `geom` 转换为 `EPSG:3857`,WNDB 查询不会把发布坐标误当成模型坐标。
|
||||
|
||||
`projects.py` 只负责项目数据库的创建、复制、打开、关闭和删除,不再混入模型查询。
|
||||
|
||||
### model:管网模型和仿真配置
|
||||
|
||||
`model` 按业务实体命名,不再使用 `s2_junctions.py` 这类 INP 章节编号。节点、连接、模式、曲线、需求、规则和仿真设置都能从文件名直接定位。
|
||||
|
||||
每个实体模块保留三类紧密相关的函数:读取实体、生成并执行实体变更、转换该实体对应的一行或一段 INP 内容。完整文件的读取顺序、事务和项目生命周期由 `inp` 目录负责。因此,实体级编解码仍靠近实体定义,跨章节编排已经集中。
|
||||
|
||||
`elements.py` 保存节点、连接、模式、曲线和区域的通用类型判断及拓扑查询。它不再承担业务算法。
|
||||
|
||||
### gis:空间数据和区域几何
|
||||
|
||||
`coordinates.py`、`vertices.py`、`labels.py` 和 `backdrop.py` 对应原始 GIS 数据。`regions.py` 负责区域持久化,`region_geometry.py` 负责边界、凸包、膨胀和区域内元素查询。
|
||||
|
||||
管网实体修改会调用坐标 SQL 辅助函数,区域几何也会读取管网拓扑。这里存在明确的模型与 GIS 协作,但没有模块导入环。现阶段继续拆出抽象接口只会增加层级,没有实际收益。
|
||||
|
||||
### inp:文件级导入导出
|
||||
|
||||
`sections.py` 只保存 INP 章节名称和输出顺序。旧文件中混放的 `s1_title`、`s2_junction` 等命令类型常量已经移除。
|
||||
|
||||
`importer.py` 负责文件分段、导入顺序、项目事务、版本转换和导入后的物化视图刷新。`exporter.py` 负责按 EPANET 版本组织各章节并写出文件或 `ChangeSet`。
|
||||
|
||||
### commands:批量修改和级联关系
|
||||
|
||||
`api.py` 为级联删除和选项同步补齐命令元数据。`cascade.py` 将删除节点、连接、模式和曲线的请求展开为完整的关联修改。`executor.py` 在一个项目事务中执行展开后的命令。
|
||||
|
||||
元素命令分派已经由类型到处理函数的显式注册表实现。新增元素时,只需把受支持的新增、修改或删除处理函数登记到对应注册表。没有处理函数的命令保持空操作,行为与改造前一致。
|
||||
|
||||
### 命令执行与事务
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
REQUEST[ChangeSet 请求]
|
||||
EXPAND[展开级联修改]
|
||||
BUILD[实体模块生成 DatabaseCommand]
|
||||
EXECUTE[执行 SQL]
|
||||
RESULT[返回 ChangeSet]
|
||||
COMMIT[提交项目事务]
|
||||
REFRESH[刷新物化视图]
|
||||
|
||||
REQUEST --> EXPAND --> BUILD --> EXECUTE --> RESULT
|
||||
EXECUTE --> COMMIT --> REFRESH
|
||||
```
|
||||
|
||||
实体模块根据请求生成 `DatabaseCommand`,其中 `sql` 是要执行的语句,`changes` 是成功后的变更结果。批量命令先在同一个项目事务中展开级联关系,再依次执行 SQL;任何一步失败都会回滚整个事务。事务提交后统一刷新物化视图,避免一次批量修改触发多次刷新。直接调用单个实体修改时,如果当前没有外层项目事务,则由 `execute_command` 完成提交并按影响范围刷新视图。
|
||||
|
||||
旧实现中的 `DbChangeSet` 同时保存执行和撤销两套 SQL、两套变更结果,但数据库已经不再提供 operation 或 snapshot 撤销日志,这些字段没有消费者。当前代码已经删除 `undo_sql`、`undo_cs` 及各实体模块中的撤销 SQL 构造,也删除了只为撤销结果读取旧记录的查询和辅助方法。局部修改仍会读取一次当前记录,用于补齐请求中未提供的字段;这类读取属于更新语义,不是撤销机制。
|
||||
|
||||
## WNDB 之外的业务代码
|
||||
|
||||
以下代码不再放在 `app/native/wndb`:
|
||||
|
||||
| 职责 | 当前路径 | 原因 |
|
||||
| --- | --- | --- |
|
||||
| 用水量分配计算 | `app/algorithms/water_demand/` | 属于业务算法,WNDB 只提供节点、需求和区域查询 |
|
||||
| SCADA 资产查询 | `app/infra/db/postgresql/scada_assets.py` | 对应 `asset.scada_devices` 的 PostgreSQL 仓储 |
|
||||
| 测压点选址结果 | `app/infra/db/postgresql/sensor_placement.py` | 对应 `analysis.runs` 和 `analysis.results` 的仓储 |
|
||||
| 服务组合入口 | `app/services/tjnetwork.py` | 组合 WNDB、EPANET 和业务仓储,供接口与算法层调用 |
|
||||
|
||||
`tjnetwork.py` 从约 995 行缩减到约 320 行。它不再通过 WNDB 根包获得全部函数,只显式导入当前服务使用的能力。过时的 `scripts/test_tjnetwork.py` 依赖已移除的 operation、snapshot、DMA 和旧 SCADA API,已经一并删除。
|
||||
|
||||
## 依赖方向
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
API[HTTP 接口与业务服务]
|
||||
SERVICE[services.tjnetwork]
|
||||
ALGORITHM[业务算法]
|
||||
REPOSITORY[PostgreSQL 业务仓储]
|
||||
COMMANDS[wndb.commands]
|
||||
INP[wndb.inp]
|
||||
MODEL[wndb.model]
|
||||
GIS[wndb.gis]
|
||||
CORE[wndb.core]
|
||||
DB[(项目业务数据库)]
|
||||
|
||||
API --> SERVICE
|
||||
API --> ALGORITHM
|
||||
API --> REPOSITORY
|
||||
SERVICE --> COMMANDS
|
||||
SERVICE --> INP
|
||||
SERVICE --> MODEL
|
||||
SERVICE --> GIS
|
||||
SERVICE --> REPOSITORY
|
||||
COMMANDS --> MODEL
|
||||
COMMANDS --> GIS
|
||||
COMMANDS --> CORE
|
||||
INP --> MODEL
|
||||
INP --> GIS
|
||||
INP --> CORE
|
||||
MODEL --> CORE
|
||||
GIS --> CORE
|
||||
MODEL --> GIS
|
||||
GIS --> MODEL
|
||||
CORE --> DB
|
||||
REPOSITORY --> DB
|
||||
```
|
||||
|
||||
WNDB 根包不再作为依赖汇聚点。上层若只需要管道查询,应直接依赖 `model.pipes`;需要区域几何时依赖 `gis.region_geometry`;需要项目连接时依赖 `core.connection`。
|
||||
|
||||
## 仍需留意的文件规模
|
||||
|
||||
`importer.py` 和 `region_geometry.py` 行数较多,但函数仍围绕单一职责。只有在继续增加 INP 格式或区域算法时,才需要分别拆出版本转换器或边界算法模块。目前没有必要为了控制文件行数继续分层。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- 本地 conda 环境全量测试:239 项通过,10 项按条件跳过。
|
||||
- Docker 镜像构建成功,镜像内全量测试结果一致。
|
||||
- `tjwater_next` 真实数据库测试:8 项通过,覆盖业务库和时序库并发借用、嵌套事务回滚、分析运行生命周期、恶意标识符转义、五张明细表的复合主键,以及 WNDB pattern 增删改、级联解除需求关联和整体回滚。
|
||||
- Python 编译、未使用导入扫描、撤销字段残留扫描和 `git diff --check` 均通过。
|
||||
Reference in New Issue
Block a user