Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d31f456e6 | ||
|
|
cddfc8647d |
@@ -1,6 +1,6 @@
|
||||
# TJWaterServerBinary 内部后端
|
||||
# NextTJWater Server
|
||||
|
||||
`TJWaterServerBinary` 是 TJWater 内部版 Python 后端,基于 FastAPI 提供认证、项目、管网、模拟、爆管、漏损、SCADA 和地图服务集成能力。该仓库用于内部开发和完整功能维护。
|
||||
`next-tjwater-server` 是 NextTJWater 供水系统的 FastAPI 后端,提供认证、项目、管网、模拟、爆管、漏损、SCADA、地图服务、工况任务和受控 MCP 能力。业务数据和时序数据按已认证项目路由,后台工作进程也必须使用明确的项目范围。
|
||||
|
||||
## 技术栈
|
||||
|
||||
@@ -30,6 +30,14 @@ infra/docker/ Docker Compose 编排
|
||||
|
||||
## 本地开发
|
||||
|
||||
先从样板创建本地配置,再按启用的功能填写:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
`.env` 只用于本地或部署主机运行,不得提交。生产配置由部署主机的受控环境文件注入,不将完整应用 `.env` 保存为 Gitea Actions 密钥。
|
||||
|
||||
推荐使用已有 conda 环境:
|
||||
|
||||
```bash
|
||||
@@ -50,9 +58,21 @@ conda activate server
|
||||
就绪状态不等同于 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` 路由为准,不会因为存在默认数据库变量而回退到固定项目。
|
||||
|
||||
## 常用命令
|
||||
|
||||
外围接入样板是独立进程,复用 PostgreSQL,不自动建表;启动、授权边界和契约见 [外围样板说明](docs/peripheral-sample.md)。
|
||||
外围接入样板作为 `/peripheral` 子应用随主 API 进程启动,复用 PostgreSQL 且不自动建表;授权边界和契约见 [外围样板说明](docs/peripheral-sample.md)。
|
||||
外围样板提供 synthetic 测试工单和测试数据的版本化 CRUD;工单经同步器投影为 `workbench.operational_tasks` 中的外围来源任务。固定演示时窗和造数方式见 [合成样本说明](docs/peripheral-generated-data.md),运行态读取边界见 [工作台运行态说明](docs/workbench-runtime.md)。这些数据不代表现场事实,也不会触发设备控制。
|
||||
|
||||
```bash
|
||||
@@ -117,6 +137,19 @@ conda run -n server python scripts/publish_fengxian_map_layers.py --database tjw
|
||||
|
||||
发布命令需要配置 `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)。
|
||||
|
||||
## 测试与发布
|
||||
|
||||
提交前根据改动范围运行最小有效测试:
|
||||
@@ -131,7 +164,15 @@ conda run -n server python -m pytest tests/unit tests/auth -q
|
||||
docker build -t tjwater-server:local .
|
||||
```
|
||||
|
||||
Gitea 包工作流位于 `.gitea/workflows/package.yml`,通常由 tag 触发构建、推送镜像并通知部署 webhook。
|
||||
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`
|
||||
|
||||
这些密钥只用于镜像仓库和受限开发机部署,不替代后端应用的运行环境配置。
|
||||
|
||||
## 安全规则
|
||||
|
||||
@@ -139,4 +180,6 @@ Gitea 包工作流位于 `.gitea/workflows/package.yml`,通常由 tag 触发
|
||||
|
||||
## 产品 MCP 查询
|
||||
|
||||
Server 可选启用 MCP 2026-07-28 的25项项目查询及10项受控分析,默认关闭。它使用专用 OAuth resource Bearer、宿主绑定上下文及现有项目/任务权限,调查查询返回前持久化证据。启用需更新依赖、应用项目 SQL 迁移和配置 Keycloak token exchange;这些生产操作尚未执行。详见 [实施与启用](docs/mcp-query-implementation.md) 和 [协议/旧 CLI 能力对照](docs/mcp-query-design.md)。
|
||||
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)。
|
||||
|
||||
@@ -14,6 +14,7 @@ from app.infra.db.metadb.database import close_metadata_engine
|
||||
from app.infra.db.timescaledb.sync_pool import close_all_timescale_pools
|
||||
from app.native.wndb.core.connection import close_all_project_pools
|
||||
from app.core.config import settings
|
||||
from samples.peripheral.api import create_app as create_peripheral_app
|
||||
|
||||
# 导入审计中间件
|
||||
from app.infra.audit.middleware import AuditMiddleware
|
||||
@@ -80,6 +81,7 @@ install_problem_details_handlers(app)
|
||||
from app.api.v1.endpoints.mcp_queries import router as mcp_context_router
|
||||
app.state.mcp_runtime = mcp_runtime
|
||||
app.include_router(mcp_context_router, prefix="/api/v1")
|
||||
app.mount("/peripheral", create_peripheral_app(), name="peripheral")
|
||||
# Legacy routers without version prefix
|
||||
# app.include_router(api_router)
|
||||
|
||||
|
||||
@@ -72,8 +72,8 @@ OPERATIONAL_WORKER_AGENT_URL=http://127.0.0.1:8787
|
||||
OPERATIONAL_WORKER_TOKEN_URL=https://<identity-host>/realms/<realm>/protocol/openid-connect/token
|
||||
OPERATIONAL_WORKER_CLIENT_ID=<dedicated-service-client>
|
||||
OPERATIONAL_WORKER_CLIENT_SECRET=<inject-securely>
|
||||
OPERATIONAL_ACTION_PERIPHERAL_URL=http://127.0.0.1:8011
|
||||
OPERATIONAL_DATA_PERIPHERAL_URL=http://127.0.0.1:8011
|
||||
OPERATIONAL_ACTION_PERIPHERAL_URL=http://127.0.0.1:8000/peripheral
|
||||
OPERATIONAL_DATA_PERIPHERAL_URL=http://127.0.0.1:8000/peripheral
|
||||
```
|
||||
|
||||
选择 OAuth 时不要同时配置 `OPERATIONAL_WORKER_BEARER`,否则固定 Bearer 优先。不要配置设备命令网关进行本次合成联调。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 外围样板服务
|
||||
|
||||
该独立 FastAPI 服务提供历史监测回放、固定合成数据,以及 synthetic 测试工单和测试数据的版本化 CRUD。管理入口位于供水工作台“外围样板”。它不控制设备,也不修改正式业务资产。
|
||||
该 FastAPI 子应用提供历史监测回放、固定合成数据,以及 synthetic 测试工单和测试数据的版本化 CRUD。生产环境挂载在主 Server 的 `/peripheral`,随 `next-backend` 单容器发布;源码仍可独立启动用于本地隔离调试。管理入口位于供水工作台“外围样板”。它不控制设备,也不修改正式业务资产。
|
||||
|
||||
浏览器通过 `TJWATER_PERIPHERAL_SAMPLE_API_BASE_URL` 连接样板服务。服务复用主系统的 Bearer 身份和 `X-Project-Id`,读取需要 `simulation.view`,写入需要 `simulation.run`。
|
||||
|
||||
@@ -10,17 +10,16 @@ Custom Location:
|
||||
|
||||
| 配置项 | 值 |
|
||||
| --- | --- |
|
||||
| Location | `/peripheral/`,必须保留末尾斜杠 |
|
||||
| Location | `/peripheral`,不添加末尾斜杠 |
|
||||
| Scheme | `http` |
|
||||
| Forward Hostname / IP | NPMplus 能访问的应用机地址,通常与现有前端上游相同 |
|
||||
| Forward Port | `8011` |
|
||||
| Forward Hostname / IP | `192.168.1.114`,不追加路径或末尾斜杠 |
|
||||
| Forward Port | `8000` |
|
||||
| Forward Path | 留空 |
|
||||
|
||||
NPMplus 会去掉 `/peripheral/` 前缀,因此浏览器请求
|
||||
`/peripheral/api/sample/v1/context` 时,上游收到 `/api/sample/v1/context`。不要在 Forward
|
||||
Hostname 后再追加 `/api/sample/v1`。外围服务应只向 NPMplus 所在网络开放 `8011`,不要直接开放公网。
|
||||
本机开发和预览模式也提供等价的 `/peripheral` 代理,可通过
|
||||
`TJWATER_PERIPHERAL_SAMPLE_UPSTREAM` 覆盖默认上游 `http://127.0.0.1:8011`。
|
||||
该配置保留 `/peripheral` 前缀,因此浏览器请求
|
||||
`/peripheral/api/sample/v1/context` 时,主 Server 收到相同路径并交给挂载的子应用。不要在
|
||||
Forward Hostname 后追加 `/api/sample/v1`。本机开发和预览模式也将 `/peripheral` 代理到
|
||||
`TJWATER_SERVER_API_BASE_URL`,默认是 `http://127.0.0.1:8000`。
|
||||
|
||||
外围 API 支持已授权项目切换。每次请求先由主系统验证项目成员身份和权限,再从 Metadata DB 解析该项目的 `biz_data` 路由;历史观测另解析 `iot_data` 路由。逻辑项目代码、业务物理库名和时序物理库名可以不同,连接沿用各自完整 DSN。路由缺失返回 `503 PROJECT_STORAGE_NOT_CONFIGURED`,不会回退到其他项目数据库。未配置存储表的项目仍需单独完成结构部署;API 不自动创建表或复制演示数据。
|
||||
|
||||
|
||||
@@ -309,8 +309,8 @@ def _update_worker_environment(
|
||||
"OPERATIONAL_WORKER_SERVER_URL": "http://127.0.0.1:8000",
|
||||
"OPERATIONAL_WORKER_AGENT_URL": "http://127.0.0.1:18787",
|
||||
"OPERATIONAL_WORKER_TOKEN_REFRESH_SECONDS": "300",
|
||||
"OPERATIONAL_ACTION_PERIPHERAL_URL": "http://127.0.0.1:8011",
|
||||
"OPERATIONAL_DATA_PERIPHERAL_URL": "http://127.0.0.1:8011",
|
||||
"OPERATIONAL_ACTION_PERIPHERAL_URL": "http://127.0.0.1:8000/peripheral",
|
||||
"OPERATIONAL_DATA_PERIPHERAL_URL": "http://127.0.0.1:8000/peripheral",
|
||||
"OPERATIONAL_DATA_SERVER_URL": "http://127.0.0.1:8000",
|
||||
}
|
||||
for key, value in values.items():
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app.main import app
|
||||
|
||||
|
||||
def test_main_server_mounts_peripheral_health_endpoint():
|
||||
response = TestClient(app).get("/peripheral/health")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json() == {
|
||||
"service": "peripheral-sample",
|
||||
"origin": "synthetic",
|
||||
"storage": "postgresql",
|
||||
"status": "up",
|
||||
"storage_verified": False,
|
||||
}
|
||||
|
||||
|
||||
def test_mounted_peripheral_api_preserves_its_authentication_boundary():
|
||||
response = TestClient(app).get("/peripheral/api/sample/v1/context")
|
||||
|
||||
assert response.status_code == 401
|
||||
assert response.json()["code"] == "AUTHENTICATION_REQUIRED"
|
||||
Reference in New Issue
Block a user