Files
next-tjwater-server/README.md
T

232 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# NextTJWater Server
`next-tjwater-server` 是 NextTJWater 供水系统的 FastAPI 后端,提供认证、项目、管网、模拟、爆管、漏损、SCADA、地图服务、工况任务和受控 MCP 能力。业务数据和时序数据按已认证项目路由,后台工作进程也必须使用明确的项目范围。
## 技术栈
- Python 3.12
- FastAPI / Uvicorn
- Pydantic / SQLAlchemy / psycopg
- PostgreSQL、PostGIS、TimescaleDB
- WNTR、EPANET、Cython、科学计算与空间分析依赖
- pytest
## 目录结构
```text
app/main.py FastAPI 入口
app/api/ HTTP API 路由
app/auth/ 认证和权限上下文
app/core/ 配置、日志和基础设施初始化
app/domain/ 领域模型和 Pydantic schema
app/infra/ 数据库、EPANET 和外部集成
app/services/ 业务服务编排
app/algorithms/ 管网算法、模拟、爆管、漏损、清洗和健康分析
app/native/ 本地管网数据读写与转换
tests/ 后端测试
resources/ SQL、模板和示例资源
infra/docker/ Docker Compose 编排
```
## 本地开发
先从样板创建本地配置,再按启用的功能填写:
```bash
cp .env.example .env
```
`.env` 只用于本地或部署主机运行,不得提交。生产配置由部署主机的受控环境文件注入,不将完整应用 `.env` 保存为 Gitea Actions 密钥。
推荐使用已有 conda 环境:
```bash
conda run -n server python -m pytest tests/unit tests/auth -q
conda run -n server python scripts/run_server.py --reload
```
如需要进入环境:
```bash
conda activate server
```
`GET /health` 是无需鉴权的 HTTP 服务存活检查,返回 `200` 和 `{"status":"ok"}`,不查询数据库。
`GET /ready` 检查应用启动状态、元数据库 `SELECT 1` 及已启用的 MCP 传输生命周期;
全部就绪返回 `200`,否则返回 `503`,元数据库探测预算为 2 秒。MCP 关闭时标记为
`disabled`,不阻止就绪。返回结果不包含数据库地址、凭据或原始异常,禁止缓存。
就绪状态不等同于 OAuth、用户权限或真实工具调用成功;指定项目的业务库和时序库仍通过
带身份与 `X-Project-Id` 的 `GET /api/v1/projects/current/database-health` 检查。
## 环境配置
`.env.example` 是唯一应提交的环境配置样板,主要分为以下几组:
- 元数据库、项目业务库、TimescaleDB 及项目数据库连接池。
- GeoServer 项目供应、Keycloak JWT 校验、CORS 和工作台事件。
- 天地图地理编码和地名搜索。
- 调查任务 worker、外围工单同步、数据与动作适配器。
- 可选的 MCP resource、OAuth issuer、宿主和超时预算。
基础数据库变量用于启动和项目供应。运行时的项目请求仍以元数据库中的 `biz_data` 和 `iot_data` 路由为准,不会因为存在默认数据库变量而回退到固定项目。
CORS 通过后端 `.env` 或部署进程环境变量中的 `CORS_ALLOWED_ORIGINS` 配置,进程环境变量优先。
必须使用英文逗号分隔的明确页面来源(协议、域名或 IP、端口,不带路径),不能填写 `*`。
未设置时默认只允许本机 `5173`、`15173` 开发来源。正式部署按实际浏览器地址填写,例如:
```dotenv
CORS_ALLOWED_ORIGINS=https://next-demo.waternetwork.cn,http://192.168.1.133,https://192.168.1.133,http://192.168.1.110,https://192.168.1.110
```
CORS 匹配浏览器页面来源,不是 Nginx 连接 IP 或域名解析结果;非默认端口须逐项列出。
保留跨域凭据支持及 `Authorization`、`X-Project-Id` 请求头,接口认证和权限校验继续生效。
修改后需重启进程;使用 Compose `env_file` 注入时需重新创建容器才能读取新值。
## 业务 Skill 与闭环
[爆管事件处理流程](docs/burst-response-workflow.md)覆盖工单接收、资产关联、隔离阀门、影响说明、审批处置和反馈闭环。Server 按任务 workflow 与回放来源分发独立 Skill,版本以实际返回为准:
| workflow | Skill | 调查重点 |
| --- | --- | --- |
| burst_report(工单) | [爆管事件处理](resources/work-order-skills/burst-event-handling/SKILL.md) | 资产关联、隔离方案、影响说明、处置及恢复闭环 |
| scada_anomaly/历史回放 | [SCADA 异常调查](resources/work-order-skills/scada-anomaly/SKILL.md) | 冻结数据诊断、候选定位与隔离影响分析 |
| water_quality_complaint | [水质投诉](resources/work-order-skills/water-quality-complaint/SKILL.md) | 投诉关联、监测与采样、候选影响、处置及复测回访 |
| general | [预约跟进](resources/work-order-skills/appointment-follow-up/SKILL.md) | 审批与回执、现场履约、反馈复核及与原事件的闭环区别 |
JSONL 整理遵守会话文件权限,后台图表和表格通过既有报告 `ui_blocks` 展示;Skill 不增加审批、执行或原工单关单权限。预约投影不自动启动 Agent 调查,普通协作与后台执行分别遵守其原有工具范围。
## 常用命令
外围接入样板作为 `/peripheral` 子应用随主 API 进程启动,复用 PostgreSQL 且不自动建表;授权边界和契约见 [外围样板说明](docs/peripheral-sample.md)。
外围系统提供业务工单和反馈的版本化 CRUD,工单来源固定为 `system_data`,经同步器投影为 `workbench.operational_tasks` 中的外围来源任务。系统上报与已核实事实分别记录,现场处置通过审批流程执行。独立测试数据和监测演示的固定时窗及造数方式见 [合成样本说明](docs/peripheral-generated-data.md),运行态读取边界见 [工作台运行态说明](docs/workbench-runtime.md)。
```bash
conda run -n server python -m pytest tests -q
conda run -n server python scripts/run_server.py
docker build -t tjwater-server:local .
docker compose -f infra/docker/docker-compose.yml config
```
- `pytest`:运行自动化测试。
- `scripts/run_server.py`:使用项目脚本启动服务。
- `docker build`:构建后端镜像。
- `docker compose config`:检查 compose 配置和变量展开。
开发时使用 `python scripts/run_server.py --reload` 启用进程交接式重载:父进程持有监听端口,
候选进程完成 ASGI 初始化、通过元数据库就绪检查并接管监听后,才通知旧进程停止接收新连接。
旧 HTTP 请求和 SSE 连接继续由旧进程完成,期间新请求由新进程响应;不迁移已建立的 TCP 连接,
也不通过强制取消旧请求来加快重载。候选启动失败或超出 `--startup-timeout`(默认 60 秒)时,
保留旧进程继续服务。快速连续修改最多保留两个正在排空的旧进程,其余修改合并后再重载。
默认监视 `app/` 和 `samples/peripheral/` 下的 Python 文件,可重复传入 `--reload-dir` 指定目录。
直接执行 `uvicorn ... --reload` 仍使用其原有先停后启流程;已经运行的旧启动命令不会自动切换到新机制。
## 开发规范
- Python 文件、函数、变量、Pydantic 字段、JSON body 字段和 query 参数使用 `snake_case`。
- Python 类和 Pydantic 模型使用 `PascalCase`。
- 新 HTTP 路径使用 `kebab-case`,例如 `/api/v1/pressure-status/analyze`。
- 优先复用现有 FastAPI/service/repository 边界。
- 不要把临时数据、数据库 dump、日志或本地运行产物纳入提交。
## 项目数据库路由
项目级 REST 请求通过 `X-Project-Id` 解析元数据中的数据库配置:
- `biz_data` DSN 用于管网业务数据。每个物理业务库使用同名 `_template` 数据库,通过逻辑订阅只同步 `network` schema;模拟临时库从该项目模板克隆。`WNDB_SCHEMA_TEMPLATE_DB_NAME`(当前为 `tjwater_v2_schema_template`)仅用于创建空业务库和 INP 导入暂存库。
- `iot_data` DSN 用于 TimescaleDB,始终使用元数据配置的完整 DSN,不再从项目代码推导数据库名。
- 元数据、业务库和 TimescaleDB 可以部署在同一主机,也可以分别部署。
在线重算会原子替换一个时刻的节点、管段基线。历史分块已经压缩时,写入事务使用
`TIMESCALEDB_REALTIME_MAX_DECOMPRESSED_TUPLES` 设置解压预算,默认 100 万条;配置必须
为正整数,不允许 0。该设置仅在实时快照写入事务内生效,不改变数据库全局设置或普通读取。
预算按需根据受影响压缩批次的规模调整,超过预算仍会失败并回滚当前快照;此前完成的其他
时刻不会一起回滚。同步、异步及单表批量快照入口使用同一预算。
压缩写入回归位于 `tests/integration/test_realtime_compression.py`;仅在设置
`REALTIME_COMPRESSION_TEST_DSN` 时执行,并且只接受库名以 `tjw_test_realtime_` 开头的
独立可丢弃数据库。测试会创建和删除该库内的 `realtime` schema,禁止指向项目数据库。
完整新建供水项目使用 `POST /api/v1/admin/project-provisions`,以
`multipart/form-data` 同时提交 `name`、小写 `code`、可选的
`description`、`gs_workspace`、`map_zoom` 和 INP `file`。工作流会按顺序完成:
1. EPANET 校验 INP,并从空结构模板创建业务库、导入模型;
2. 创建同名 `_template`,复制 32 张 `network` 表并建立逻辑订阅;
3. 从 `TIMESCALEDB_SCHEMA_TEMPLATE_DB_NAME` 创建空时序库;
4. 创建 GeoServer 工作空间、PostGIS 数据存储和 7 个 GIS 图层,将客户端缓存设为 `GEOSERVER_CLIENT_CACHE_SECONDS`;
5. 最后在一个元数据事务中写入项目、两条加密数据库路由和创建者成员关系,并将项目设为 `active`。
基础设施任一步失败时按 GeoServer、时序库、管网模板、业务库的逆序清理;元数据提交失败也执行同样清理。旧 `POST /admin/projects` 仅保留给已经由外部流程创建好的资源登记使用,并已标记为 deprecated。
使用模板复制或临时方案库的模拟功能时,`biz_data` 账号必须具备数据库创建和删除权限;只有显式删除项目时才会终止该项目的现有数据库会话,普通复制不会主动中断复制源会话。
奉贤项目的原始 GIS 与水质监测点使用独立脚本维护。导入脚本读取两份交付目录并写入项目 BizDB;水质脚本调用现有灵敏度分析服务生成 50 个监测点;发布脚本创建 10 个项目图层并刷新 GeoServer 缓存:
```bash
conda run -n server python scripts/import_fengxian_gis.py --database tjwater_v2
conda run -n server python scripts/generate_fengxian_water_quality_points.py --project tjwater_v2
conda run -n server python scripts/publish_fengxian_map_layers.py --database tjwater_v2 --workspace tjwater_next
```
发布命令需要配置 `GEOSERVER_URL`、`GEOSERVER_USERNAME` 和 `GEOSERVER_PASSWORD`。其中 7 个基础管网图层仍由项目供应流程创建,奉贤发布脚本追加 9 个原始 GIS 图层和 1 个水质监测点图层。
## 工况任务工作进程
调查任务 worker 和外围工单同步器是独立进程,不随 API 进程自动启动:
```bash
conda run -n server python scripts/run_operational_worker.py
conda run -n server python scripts/sync_peripheral_work_orders.py
```
`OPERATIONAL_WORKER_PROJECT_IDS` 是逗号分隔的显式项目 UUID 列表,非空时优先于单项目兼容变量 `OPERATIONAL_WORKER_PROJECT_ID`。如需覆盖所有已启用项目,应将这些项目的 UUID 全部明确写入列表。进程不会自动枚举项目,配置列表也不会自动授权;服务身份必须是每个目标项目的有效成员。
每个项目保持独立的事件游标、会话、消息游标和 OAuth 缓存。`OPERATIONAL_WORKER_MAX_CONCURRENCY` 是每个项目的上限,而不是全局上限。更改项目列表后,需要同时重启 worker 和外围同步器。完整配置、授权和重试边界见 [工况工作进程说明](docs/operational-task-workers.md)。
## 测试与发布
提交前根据改动范围运行最小有效测试:
```bash
conda run -n server python -m pytest tests/unit tests/auth -q
```
发布镜像前建议运行:
```bash
docker build -t tjwater-server:local .
```
Dockerfile 的 `test` 阶段会执行 Python 编译检查、应用导入、OpenAPI 检查和完整 pytest。
Gitea Actions 工作流位于 `.gitea/workflows/package.yml`,可手动触发,也会在推送 `v*` tag 时运行。工作流构建 `test` 阶段,推送不可变的 `sha-*` 镜像,然后通过受限 SSH 账号对 `next-backend` 执行候选验证。只有 tag 工作流会在验证通过后提升正式版本。仓库需要以下 Actions secrets:
- `REGISTRY_USERNAME`
- `REGISTRY_PASSWORD`
- `DEV_DEPLOY_SSH_KEY`
这些密钥只用于镜像仓库和受限开发机部署,不替代后端应用的运行环境配置。
## 安全规则
不要提交 `.env`、客户数据、数据库 dump、日志、生成缓存、`db_inp/`、`temp/`、`data/` 或本地密钥。CI/CD 凭据应放在 Gitea secrets 和仓库变量中。
## 产品 MCP 查询
Server 可选启用 MCP 2026-07-28,默认关闭。当前目录包含 67 项类型化工具,其中 42 项查询、12 项计算、13 项预览式写入。MCP 使用专用 OAuth resource Bearer、宿主绑定上下文和现有项目或任务权限,不提供自由命令执行。
写入工具默认只执行 `dry_run`。真实写入必须使用同一 `request_id`,带上预览返回的 `expected_digest`,并通过宿主原生许可和项目权限检查。超时或结果不明时先查询操作状态,不盲目重试。实施、启用和授权边界见 [MCP 实施与启用](docs/mcp-query-implementation.md) 和 [协议与旧 CLI 能力对照](docs/mcp-query-design.md)。
## 后台计算队列
长计算通过项目数据库队列和独立计算 Worker 执行,API 保持4个进程,HTTP超时不延长。接入需要新增数据库迁移和独立服务,详见[计算队列部署与MCP契约](docs/calculation-queue.md)。
后台调查通过 `POST /api/v1/operational-tasks/{task_id}/questions` 保存结构化问题,并沿现有任务通知展示。当前 run 可在没有最终报告时正常结束,任务进入等待人工;具备 `operational_task.write` 权限的用户通过 `questions/{question_id}/answer` 回答,原任务排队新一轮。回答保存为交互记录,不自动转为现场证据或审批。问题支持最多三个单选、多选或自定义回答,版本校验和请求幂等保护重复提交。任务详情返回最近 100 组提问,完整历史由 MCP 的 `get_task_context(section="questions")` 分页读取。
部署任务提问功能前,在各项目业务库执行 `resources/sql/028_task_questions.sql`,再同步 Server、Agent 与前端版本。迁移不修改既有调查记录;更新代码不会自动迁移数据库或重启服务。
外围工单来源统一为系统数据。部署对应版本前,在各项目业务库执行 `resources/sql/029_peripheral_work_orders_system_data.sql`,再同步 Server、同步器、Agent 与前端版本。迁移会修正既有外围源工单、修订记录、任务快照和外围反馈证据,不修改测试数据或模拟监测数据。
多 Agent 后台调查按实例归属运行。部署对应版本前,在每个启用调查 Worker 的项目 BizDB 执行 `resources/sql/030_operational_run_agent_ownership.sql`,再同步 Server、Agent 和 Worker,并为各 Agent 配置唯一且重启稳定的 `AGENT_INSTANCE_ID`。迁移只增加运行归属列和活跃运行索引;旧运行不猜测归属,待其结束或租约恢复后,新一代运行开始记录实例。