diff --git a/README.md b/README.md index 115d396..56bd6d6 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ conda activate server ## 常用命令 -外围接入样板是独立进程,复用 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 diff --git a/app/main.py b/app/main.py index f7828b5..d8d0bcf 100644 --- a/app/main.py +++ b/app/main.py @@ -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) diff --git a/docs/operational-task-workers.md b/docs/operational-task-workers.md index c215c9c..585ca06 100644 --- a/docs/operational-task-workers.md +++ b/docs/operational-task-workers.md @@ -72,8 +72,8 @@ OPERATIONAL_WORKER_AGENT_URL=http://127.0.0.1:8787 OPERATIONAL_WORKER_TOKEN_URL=https:///realms//protocol/openid-connect/token OPERATIONAL_WORKER_CLIENT_ID= OPERATIONAL_WORKER_CLIENT_SECRET= -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 优先。不要配置设备命令网关进行本次合成联调。 diff --git a/docs/peripheral-sample.md b/docs/peripheral-sample.md index 3c6b5ad..21a5628 100644 --- a/docs/peripheral-sample.md +++ b/docs/peripheral-sample.md @@ -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 不自动创建表或复制演示数据。 diff --git a/scripts/provision_operational_worker_identity.py b/scripts/provision_operational_worker_identity.py index ac67fd5..5a9e4a7 100644 --- a/scripts/provision_operational_worker_identity.py +++ b/scripts/provision_operational_worker_identity.py @@ -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(): diff --git a/tests/api/test_peripheral_mount.py b/tests/api/test_peripheral_mount.py new file mode 100644 index 0000000..674e352 --- /dev/null +++ b/tests/api/test_peripheral_mount.py @@ -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"