17 KiB
WNDB 文件结构说明
本文记录 2026-08-25 完成的 WNDB 目录与命令结构重构。检查对象是当前代码,不以旧 SQL 脚本或历史目录为依据。
结论
当前结构适合继续维护。WNDB 已按连接基础设施、管网模型、GIS、INP 和命令执行分组,原来的编号文件名、根目录聚合门面和星号导入已经移除。WDA、SCADA 资产查询和测压点选址也已离开底层模型目录。
本次调整了代码文件、导入关系、命令分派、项目生命周期接口和 WNDB 内部命令对象。无状态服务不再发布“打开、关闭、是否打开项目”三个旧 HTTP 操作,数据库连接在请求中按需从池借用。历史撤销日志已从数据库中移除,内部接口不再保留无效的兼容字段。真实库回归时发现五张明细表错误地把局部顺序号设成全局主键,已在 tjwater_v2 中改为父对象 ID 与 sequence_no 的复合主键。
tjwater_v2 是 v2 业务库和时序库的正式物理库名,元数据库中的逻辑项目代码也为 tjwater_v2;GeoServer 工作空间仍为 tjwater_next。每个物理业务库动态派生同名 _template 管网镜像,逻辑订阅只同步 network schema。独立的 tjwater_v2_schema_template 保持空数据并禁止普通连接,只提供统一 v2 结构。
五个源业务库(包括 zjb)都在各自的数据库命名空间内使用 publication wndb_network_pub,五个模板库都使用 subscription wndb_network_sub。PostgreSQL 实例级的 replication slot 按物理库唯一命名,例如 tjwater_v2_network_slot、md_v2_network_slot 和 zjb_network_slot,因此同名发布/订阅不会串库。逻辑复制只跟踪 32 张 network 表的 DML;GIS、SCADA、分析和其他业务数据不进入发布,DDL 与序列状态也需显式维护。
当前目录
app/native/wndb/
├── __init__.py
├── core/
│ ├── connection.py
│ ├── database.py
│ ├── model_replace.py
│ └── projects.py
├── model/
│ ├── elements.py
│ ├── junctions.py
│ ├── reservoirs.py
│ ├── tanks.py
│ ├── pipes.py
│ ├── pumps.py
│ ├── valves.py
│ ├── patterns.py
│ ├── curves.py
│ ├── options.py
│ ├── options_v2.py
│ ├── options_v3.py
│ └── 其他 EPANET 模型模块
├── gis/
│ ├── coordinates.py
│ ├── vertices.py
│ ├── labels.py
│ ├── backdrop.py
│ ├── regions.py
│ ├── network_views.py
│ └── region_geometry.py
├── inp/
│ ├── sections.py
│ ├── importer.py
│ └── exporter.py
└── commands/
├── api.py
├── cascade.py
└── executor.py
目录内共有 49 个 Python 文件。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 只负责项目数据库的创建、复制、删除和异常安全的临时库上下文,不再保存“项目已打开”状态,也不混入模型查询。postgres、空结构模板、所有 _template 项目模板和元数据库属于保护对象。旧 WNDB 空模板库 project 已退出架构并从业务 PostgreSQL 与 TimescaleDB 实例删除。每个物理业务库对应一个同名 _template 管网模板,逻辑订阅只同步 network schema;空结构模板由 WNDB_SCHEMA_TEMPLATE_DB_NAME 配置,用于创建业务库和 INP 暂存库。批量清理不再扫描并删除服务器上的未知数据库,调用方必须显式提供目标。数据库级 advisory lock 与 datallowconn 串行化生命周期操作;克隆项目模板时仅临时禁止连接,完成后恢复,以便订阅工作进程继续同步。
project_templates.py 管理正式项目与其 _template 的一对一关系:先为源库 32 张 network 表创建 publication 和唯一 replication slot,再从空结构模板建立目标库、做一次一致性网络数据复制,最后以 copy_data=false 接续逻辑订阅。建库前检查复制 worker 余量,订阅的 worker、32 张关系状态均 ready 后才允许工作流继续。删除或失败回滚时先移除 subscription 和 slot,再删除模板库及 publication,不遗留 WAL slot。
model_replace.py 在源库可重复读快照中读取 network、gis 基表,并按外键拓扑顺序复制到目标业务库。替换在单一事务内完成,不再删除并重建整个业务库;普通模型修改和整体替换共用同一项目级事务锁。INP 暂存库与目标项目使用不同原始坐标 SRID 时,复制阶段保留坐标数值并将几何重新标记为目标列的 SRID,支持 zjb 等使用项目自定义工程坐标系的业务库。INP 替换时,analysis.results 保留历史记录,只有新模型中不存在的元素引用会置空,asset.scada_devices 保留仍能匹配新节点或管段的设备。临时分析库只需要订阅模板中的 network 数据;GIS 只是 INP 的可选展示章节,SCADA 也不是 EPANET 求解器的输入表。
model:管网模型和仿真配置
model 按业务实体命名,不再使用 s2_junctions.py 这类 INP 章节编号。节点、连接、模式、曲线、需求、规则和仿真设置都能从文件名直接定位。
options_v2.py 和 options_v3.py 分别负责 EPANET V2、V3 的 [OPTIONS] 章节导入导出;数据库中的 engine_version = 'legacy' 仍表示 V2 配置,仅作为现有存储标识保留。通过 V3 入口导入标准 EPANET INP 时会同时保存 legacy 原值和映射后的 V3 值,保证数据库再次导出的 V2 INP 不会引用模板遗留的 pattern。
每个实体模块保留三类紧密相关的函数:读取实体、生成并执行实体变更、转换该实体对应的一行或一段 INP 内容。完整文件的读取顺序、事务和项目生命周期由 inp 目录负责。因此,实体级编解码仍靠近实体定义,跨章节编排已经集中。
elements.py 保存节点、连接、模式、曲线和区域的通用类型判断及拓扑查询。它不再承担业务算法。
gis:空间数据和区域几何
coordinates.py、vertices.py、labels.py 和 backdrop.py 对应原始 GIS 数据。regions.py 负责区域持久化,region_geometry.py 负责边界、凸包、膨胀和区域内元素查询。network_views.py 是 GIS 查询视图的只读适配层,负责统一节点、链路、拓扑和需求投影的批量读取。坐标读取不会补写默认几何;缺少坐标时仅向调用方返回 (0, 0),真实坐标的初始化仍由新增、导入或修复命令负责。
管网实体修改会调用坐标 SQL 辅助函数,区域几何也会读取管网拓扑。这里存在明确的模型与 GIS 协作,但没有模块导入环。现阶段继续拆出抽象接口只会增加层级,没有实际收益。
GIS 统一查询视图
tjwater_v2 以现有 GIS 物化视图为基础增加了两个不保存重复数据的普通视图:
| 视图 | 来源 | 用途 |
|---|---|---|
gis.network_nodes |
gis.junctions、gis.reservoirs、gis.tanks |
统一返回 id、模型坐标 x/y 和 node_type |
gis.network_links |
gis.pipes、gis.pumps、gis.valves |
统一返回 id、起止节点和 link_type |
普通视图始终读取底层物化视图当前内容,不需要单独刷新。管网修改提交后仍由 gis.refresh_all_materialized_views 刷新节点、管道、泵、阀门等物化视图,两个统一视图会随之得到最新结果。视图和全部字段均已在数据库中写入说明。
旧服务先读取节点或链路 ID,再逐个查询坐标、类型和端点,会对完整管网产生数万次往返。当前节点坐标、主干节点、全部链路、主干管道、区域边界链路、区域拓扑和用水量汇总均改为视图批量查询。没有调用方的 get_nodes_in_extent、get_links_in_extent,以及仅服务于旧聚合过程的四个模型辅助函数已删除;单元素详情接口仍保留原始模型查询。
inp:文件级导入导出
sections.py 只保存 INP 章节名称和输出顺序。旧文件中混放的 s1_title、s2_junction 等命令类型常量已经移除。
importer.py 负责文件分段、导入顺序、项目事务、版本转换和导入后的物化视图刷新。INP 更新先从 tjwater_v2_schema_template 创建唯一空暂存库并完成解析,再在当前业务库中事务替换模型表。模型提交后即使暂存库清理失败也仍会刷新物化视图;清理失败会记录日志,不再遮蔽主操作。ChangeSet 导入使用每请求唯一临时文件并在 finally 删除,避免同项目并发导入互相覆盖。exporter.py 负责按 EPANET 版本组织各章节并写出文件或 ChangeSet。
commands:批量修改和级联关系
api.py 为级联删除和选项同步补齐命令元数据。cascade.py 将删除节点、连接、模式和曲线的请求展开为完整的关联修改。executor.py 在一个项目事务中执行展开后的命令。
元素命令分派已经由类型到处理函数的显式注册表实现。新增元素时,只需把受支持的新增、修改或删除处理函数登记到对应注册表。没有处理函数的命令保持空操作,行为与改造前一致。
命令执行与事务
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 完成提交并按影响范围刷新视图。
物化视图采用并发刷新,因此刷新位于模型事务提交之后。若刷新失败,模型修改已经持久化,代码会抛出 MaterializedViewRefreshAfterCommitError。HTTP 层返回专用 Problem Details、503 和 X-TJWater-Changes-Committed: true,明确提示不能盲目重放原始写入。
旧实现中的 DbChangeSet 同时保存执行和撤销两套 SQL、两套变更结果,但数据库已经不再提供 operation 或 snapshot 撤销日志,这些字段没有消费者。当前代码已经删除 undo_sql、undo_cs 及各实体模块中的撤销 SQL 构造,也删除了只为撤销结果读取旧记录的查询和辅助方法。局部修改仍会读取一次当前记录,用于补齐请求中未提供的字段;这类读取属于更新语义,不是撤销机制。
WNDB 之外的业务代码
以下代码不再放在 app/native/wndb:
| 职责 | 当前路径 | 原因 |
|---|---|---|
| 用水量分配计算 | app/algorithms/demand_allocation/ |
只接收普通拓扑数据;数据库装载位于 app/services/demand_allocation.py |
| SCADA 资产查询 | app/infra/db/postgresql/scada.py |
对应 asset.scada_devices 的 PostgreSQL 仓储 |
| 测压点选址结果 | app/infra/db/postgresql/sensor_placement.py |
对应 analysis.runs 和 analysis.results 的仓储 |
| 网络 API 门面 | app/services/tjnetwork.py |
暂时组合 WNDB 与 EPANET 供 HTTP 接口调用;算法层不再依赖该门面 |
tjnetwork.py 从约 995 行缩减到约 285 行。它不再通过 WNDB 根包获得全部函数,只显式导入当前接口使用的能力。过时的 scripts/test_tjnetwork.py 依赖已移除的 operation、snapshot、DMA 和旧 SCADA API,已经一并删除。
HTTP 执行边界
WNDB 当前使用同步 psycopg 连接池。network/、components/ 和同步 EPANET 仿真接口统一声明为同步处理函数;公开 REST 路由的异步适配器把这些函数送入线程池,并把项目路由上下文传入工作线程,不会在事件循环线程上阻塞数据库或求解器。异步业务库和时序库访问使用带借用计数和代际切换的项目池:活跃旧池不会被 LRU 淘汰或强制关闭,配置变化后新请求立即使用新池,旧池在已有借用归还后关闭。元数据库保持独立 SQLAlchemy 异步池。
临时分析库直接从当前物理业务库的 _template 管网镜像创建,模板中的 network 数据由逻辑订阅维护。扩展仿真只在临时库修改管网参数、导出 INP 并调用 EPANET;GIS 和 SCADA 都不复制到临时库。当前 v2 项目的 SCADA 设备均为 non_realtime,扩展仿真使用接口显式传入的参数。V3→V2 格式转换不需要项目模型,单独使用空结构模板临时库。旧 online_Analysis.py、restore 和 open/close 项目脚本已经删除,不再保留 operation 恢复入口。
监测点选址、爆管定位和 DMA 漏损识别统一通过应用服务按请求导出唯一的临时 INP。算法运行期间文件有效,成功或异常退出时都会删除;不再复用按项目命名的固定 INP 缓存,避免模型更新后算法继续读取旧文件,也避免并发请求覆盖彼此的输入。
依赖方向
flowchart TD
API[HTTP 接口]
SERVICE[应用服务]
ALGORITHM[业务算法]
REPOSITORY[PostgreSQL 业务仓储]
COMMANDS[wndb.commands]
INP[wndb.inp]
MODEL[wndb.model]
GIS[wndb.gis]
CORE[wndb.core]
DB[(项目业务数据库)]
API --> SERVICE
API --> REPOSITORY
SERVICE --> ALGORITHM
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。
app/algorithms 已按业务能力改为 burst_detection、burst_localization、scada_cleaning、pipe_health_prediction、valve_isolation、dma_leakage_estimation、pressure_sensor_placement 和 demand_allocation。自动化架构测试禁止算法层反向依赖 service、infra、native 或 API,也禁止 infra 依赖 service/algorithm。少量只做仓储转发的 HTTP 接口仍直接依赖 repository,后续按业务聚合需要迁入应用服务,不为机械包装而增加空壳层。
仍需留意的文件规模
importer.py 和 region_geometry.py 行数较多,但函数仍围绕单一职责。只有在继续增加 INP 格式或区域算法时,才需要分别拆出版本转换器或边界算法模块。目前没有必要为了控制文件行数继续分层。
验证结果
- 本地 conda 环境单元、鉴权和 API 测试:313 项通过,2 项按条件跳过。
- 一次性实库从空结构模板创建后,通过 INP 暂存解析和事务替换得到 11 个节点、13 条连接、11 条坐标及 9 条 junction 物化视图记录,验证后已完整删除。
tjwater_v2统一视图覆盖 87,907 个节点和 91,054 条链路,与六个来源物化视图的合计数量一致。实测完整节点读取约 0.17 秒、完整链路读取约 0.10 秒、完整拓扑两次批量查询约 1.12 秒;耗时仅作为当前环境基线,不作为固定性能承诺。tjwater_v2真实数据库测试覆盖业务库和时序库并发借用、失效连接自动重建、只含network数据的临时库仿真与清理、嵌套事务回滚、分析运行生命周期、恶意标识符转义、明细表复合主键、统一 GIS 查询视图,以及 WNDB pattern 增删改、级联解除需求关联和整体回滚。- Python 编译、未使用导入扫描、撤销字段残留扫描和
git diff --check均通过。