# TJWater 数据库改造说明与当前结构 > 本文记录截至 2026-09-14 的数据库实际状态。结构、约束、行数、TimescaleDB chunk 和策略均直接读取数据库,不以仓库中的 SQL 脚本为依据。文中不包含主机、端口、账号、密码或 DSN。 ## 改造范围与当前状态 本次改造保留原 `tjwater` 业务库和时序库,新建的隔离库完成验证后已正式重命名为 `tjwater_v2`。元数据库仍为 `system_hub`,逻辑项目 `tjwater_v2` 通过 `biz_data` 和 `iot_data` 两条路由分别关联 `tjwater_v2` 业务库与时序库。项目已切换为 `active`,原数据库没有被覆盖,仍可用于对照和回退。 命名约定:正式物理库名和元数据项目代码均为 `tjwater_v2`,其管网镜像模板为 `tjwater_v2_template`;其他物理业务库同样使用同名 `_template`。项目模板通过逻辑订阅只同步 `network` schema。数据为空且禁止普通连接的 `tjwater_v2_schema_template` 仅用于创建业务库和 INP 暂存库。GeoServer 工作空间仍使用 `tjwater_next`,它是图层限定名,不是项目代码或物理库名。 已经完成的数据库修改包括: - 新建并迁移 `tjwater_v2` 业务库,将旧 `public` 中混合存放的管网、GIS、SCADA 配置和分析数据按领域拆分。 - 新建并迁移 `tjwater_v2` 时序库,将 SCADA、实时计算和分析计算结果分开存放。 - `realtime` 采用冷热数据策略,72 小时后的 chunk 自动转为有序列存。 - `analysis` 按 `stored_at` 分区,入库满 24 小时的 chunk 自动转为有序列存。 - GIS 查询层改用物化视图,当前 7 张物化视图均已填充。 - GeoServer 已建立 `tjwater_next` 工作空间和同名数据存储,数据存储连接 `tjwater_v2` 业务库的 `gis` schema 并发布 7 个图层。全部 TJWater 业务工作空间的 GeoWebCache 客户端缓存有效期统一为 30 天(`2,592,000` 秒);服务端瓦片在 GIS 物化视图刷新或坐标修正后显式清理。 - 旧库中的 `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 个表或物化视图、194 个字段,以及时序库 7 张表、47 个字段均已写入中文数据库注释。 - 后端已对接新 schema。WNDB、PostgreSQL 管理连接和同步 TimescaleDB 访问均使用有界连接池,闲置项目按最近使用顺序回收;动态异步池采用代际切换,配置更新不会中断旧借用或阻塞新请求。 - 后端批量元素查询读取 GIS 物化视图,模型增删改和 INP 导入提交后执行并发刷新;批量事务只刷新一次。 - `pattern_values`、`pattern_flow_samples`、`curve_points`、`demands` 和 `link_vertices` 的顺序号按所属父对象编号,主键已改为父对象 ID 与 `sequence_no` 的复合键。 - 5 个活跃业务库、5 个对应管网模板库和 `tjwater_v2_schema_template` 均已增加 `asset.scada_devices.measurement_unit`;压力设备回填为 `m`,流量设备回填为 `m3/h`,`gis.scada_devices` 物化视图同步暴露该字段。 逻辑项目 `tjwater_v2` 当前为 `active`。 ## 数据库总体关系 ```mermaid flowchart LR subgraph META["system_hub 元数据库"] MP["public
统一元数据
8 个项目"] end subgraph OLD["原数据库,保持不变"] OB["tjwater 业务库
public"] OT["tjwater 时序库
scada / realtime / scheme"] end subgraph V2["v2 正式数据库"] NB["tjwater_v2 业务库
network / gis / asset / analysis"] NT["tjwater_v2 时序库
scada / realtime / analysis"] end GS["GeoServer
tjwater_next 工作空间"] WEB["供水前端
tjwater_next 图层配置"] MP -->|"tjwater 的 biz_data"| OB MP -->|"tjwater 的 iot_data"| OT MP -->|"项目 tjwater_v2 的 biz_data"| NB MP -->|"项目 tjwater_v2 的 iot_data"| NT NB -->|"gis 物化视图"| GS -->|"WFS / WMTS"| WEB ``` 一个项目对应一个业务数据库和一个时序数据库。项目本地表不保存 `project_id`,项目边界由元数据库路由和数据库连接共同确定。 ## 元数据库 system_hub ### public:当前主元数据 `public` 当前有 8 个项目(5 个 `active`、3 个 `inactive`)、1 个用户、5 条成员关系和 16 条数据库路由。8 个项目均配置了一条 `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。迁移校验后确认 `lingang` 项目属于误入数据,其项目和数据库路由已从当前 `public` 元数据中移除。 排水后端已改用 `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_v2` 的实际对象为准。对象名称的对应关系表示业务实体或数据职责的迁移方向,不表示所有字段均一对一复制。 ### 归并和调整 | 原库对象或职责 | 新库对象或职责 | 调整内容 | | --- | --- | --- | | `_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_v2` 仅保留 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_v2 新业务库使用 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_v2` 项目已配置 `gs_workspace=tjwater_next`,当前状态为 `active`。GeoServer 的 `tjwater_next` 数据存储连接 `tjwater_v2` 业务库并限定到 `gis` schema,图层名称直接采用物化视图名称。7 个图层使用相同的项目管网发布边界,空图层和视口内没有要素的瓦片会返回空 MVT,不会产生越界错误。 GeoWebCache 对 TJWater 的 11 个业务工作空间、80 个已发布图层统一返回 `Cache-Control: max-age=2592000, must-revalidate`。GIS 数据更新后必须先刷新物化视图、重算 GeoServer 的 native/latLon 边界,再显式清理对应图层的服务端瓦片;30 天客户端缓存不会替代这套更新流程。 ### 高铁湛江北站项目 `zjb` 是高铁湛江北站供水项目的逻辑项目代码、业务库、时序库、GeoServer 工作空间和数据存储名称。业务库已使用纠正版 `zhanjiangbei_water_network_pump_station_corrected.inp` 重新导入,当前有 272 个节点和 285 条连接,其中包括 6 台泵和 3 个阀门;`zjb_template` 通过逻辑订阅同步 32 张 `network` 表。时序库使用与 v2 项目一致的 `analysis`、`realtime`、`scada` 结构,目前数据为空。元数据库中项目状态为 `active`,`biz_data` 与 `iot_data` 均使用 1~4 条项目连接池。 源文件使用尚未取得测量定义的 DXF 工程坐标。`zjb.public.spatial_ref_sys` 将其登记为 `TJWater:990001`,原始几何数值保存在该 SRID 下;`gis.to_web_mercator` 根据四个 OSM 控制点执行二维四参数拟合,7 张 GeoServer 物化视图输出 `EPSG:3857`。控制点均方根残差约 4.62 米,适合在线地图展示,不可用于施工或测量放样,取得正式控制点后应重新拟合并刷新物化视图与瓦片。当前发布范围约为 `110.3513°E~110.3573°E、21.2185°N~21.2225°N`。 ### 新项目供应工作流 后端通过 `POST /api/v1/admin/project-provisions` 接收项目元数据和 INP 文件。项目在全部外部资源通过校验后才写入元数据库并变为 `active`,因此普通项目列表不会看到创建中的半成品。 ```mermaid flowchart LR INP["INP 校验"] --> BIZ["创建 BizDB
导入 network / gis"] BIZ --> TMPL["创建 code_template
复制 network 并订阅"] TMPL --> TS["从空模板创建 TimescaleDB"] TS --> GS["创建 GeoServer
7 图层 + 30 天客户端缓存"] GS --> META["元数据单事务
项目 + 2 路由 + 创建者成员"] META --> ACTIVE["active"] ``` 任一步失败都只清理由本次请求新建的资源,并按 GeoServer、TimescaleDB、项目模板、业务库逆序回滚。业务 PostgreSQL 必须为每个活动模板提供一个逻辑复制 worker;工作流在创建前保留一个 worker 余量,容量不足会在资源创建前失败。容器部署基线为 `max_worker_processes=32`、`max_logical_replication_workers=24`、`max_replication_slots=24` 和 `max_wal_senders=24`。旧版非活动项目的三个模板订阅已停用,不再占用 worker。 TimescaleDB 的空结构模板为 `tjwater_v2_timescale_template`,普通连接关闭;当前结构包含空的 `analysis`、`realtime`、`scada` 表、hypertable 与压缩策略。新时序库直接从该模板克隆,不从任一业务项目复制数据。 | 前端数据源 | 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` 接口。 管网元素历史查询统一使用 `POST /api/v1/timeseries/views/element-history/query`。单次请求批量返回元素关联的原始/清洗 SCADA、实时模拟或指定分析运行时序,并显式返回 `source_unit`、`display_unit` 和 `unit_inferred`。点值仍使用 `source_unit`,前端由共享单位模块统一换算为流量 `m³/h`、压力 `m` 和流速 `m/s`。旧的 `element-simulations`、`element-scada-readings` 和 `scada-simulations` 组合接口已移除。 当前业务库 `network.simulation_settings` 中的模型结果源单位如下。`LPS` 表示 L/s,`MLD` 表示百万升/日;公制模型的流速源单位为 m/s。前端不得再假定所有项目都是 `LPS`,而应读取项目设置并通过共享单位模块换算。 | 业务库 | 流量源单位 | 压力源单位 | 前端显示单位 | | --- | --- | --- | --- | | `fengyang_v2` | `LPS` | `METERS` | m³/h、m、m/s | | `md_v2` | `LPS` | `METERS` | m³/h、m、m/s | | `szh_v2` | `MLD` | `METERS` | m³/h、m、m/s | | `tjwater_v2` | `LPS` | `METERS` | m³/h、m、m/s | | `zjb` | `LPS` | `METERS` | m³/h、m、m/s | 模型修改提交后,后端先调用 `gis.refresh_all_materialized_views(boolean)` 刷新数据库查询层。GeoWebCache 不会感知 PostgreSQL 物化视图刷新,因此 7 个新图层配置了 300 秒的服务端和客户端缓存有效期,前端最迟在 5 分钟后读到新瓦片。部署或批量迁移完成后仍可执行一次图层缓存清空,避免等待已有瓦片自然过期。 ### asset:SCADA 设备配置 `asset.scada_devices` 保存设备类型、采集接口标识、传输模式、频率、可靠性、测量源单位 `measurement_unit` 和可选几何。单位字段非空且不允许空字符串。每台设备必须关联一个节点或一条连接,不能同时关联两者。设备的历史测量值不放在业务库,保存在时序库 `scada.measurements`。 ### analysis:分析运行和非时序结果 `analysis.runs` 表示一次实际执行,保存运行名称、类型、创建人、开始时间、状态和参数。新执行会在 `parameters.result_units` 中保存 EPANET 输出的流量和压力源单位,避免日后调整模型设置时误读历史值。旧运行没有该元数据时,API 使用当前模型单位并返回 `unit_inferred=true`。当前没有单独的 `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_v2 新时序库仍运行 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 管理池及数据库级 advisory lock,同步 TimescaleDB 访问也使用按数据库缓存的连接池。应用目录中已没有直接调用 `psycopg.connect` 的业务代码。 WNDB 批量修改和模拟参数准备在同一条池连接和同一事务中执行,提交后只刷新一次 GIS 物化视图。普通写入和 INP 整体替换使用同一项目级事务锁;INP 先在唯一暂存库校验,再从一致性快照事务替换当前库的 `network/gis` 模型表。临时分析库从当前项目的 `_template` 管网镜像克隆,只使用 `network` 数据修改参数、导出 INP 并运行 EPANET;GIS 和 SCADA 都不进入临时库。实时节点和连接结果在一个事务中执行整体先删后写,同一结果时间使用事务级锁;分析结果按 `run_id` 加事务级锁。Timescale 复合查询按节点/管段批量读取,SCADA 清洗使用单条集合更新,不再逐点往返。 自动化真实数据库测试分别执行 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 设备配置的后端接口。以后增加这类写接口时,也要在提交后调用统一的物化视图刷新函数。 - 用户角色和项目角色还未定型,数据库只要求字段非空,不限制具体取值。