# 外围系统接入服务 该 FastAPI 子应用接入真实业务工单,提供版本化 CRUD、处理反馈和事件调查入口,同时保留独立的历史监测回放与演练数据接口。生产环境挂载在主 Server 的 `/peripheral`,随 `next-backend` 单容器发布;源码仍可独立启动用于本地隔离调试。管理入口位于供水工作台“外围系统”。 外围工单的 `origin` 固定为 `system_data`,前端录入、同步器、任务快照、执行回执和工单查询均使用该来源,不再提供演练来源分支。业务录入代表系统中的上报,不代表投诉内容或故障原因已核实,Agent 仍需查询实际证据。 此次变更不修改表结构;部署时需在每个项目业务库执行 `resources/sql/029_peripheral_work_orders_system_data.sql`,统一修正源工单、修订记录、任务快照和外围反馈证据。API 路径、配置键中的 `sample` 为兼容名称,不代表工单是模拟数据。更新需发布 Server、同步器、Agent 与前端对应版本。 浏览器通过 `TJWATER_PERIPHERAL_SAMPLE_API_BASE_URL` 连接外围服务。服务复用主系统的 Bearer 身份和 `X-Project-Id`,读取需要 `simulation.view`,写入需要 `simulation.run`。权限键沿用现有契约,不用于判断数据来源。 生产环境不需要为外围服务新增域名。前端设置 `TJWATER_PERIPHERAL_SAMPLE_API_BASE_URL=/peripheral`,在现有前端 NPMplus Proxy Host 中增加以下 Custom Location: | 配置项 | 值 | | --- | --- | | Location | `/peripheral`,不添加末尾斜杠 | | Scheme | `http` | | Forward Hostname / IP | `192.168.1.114`,不追加路径或末尾斜杠 | | Forward Port | `8001` | | Forward Path | 留空 | 容器内部仍监听 `8000`,生产发布在宿主机映射为 `8001`,避免与已有服务冲突。该配置保留 `/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 不自动创建表或复制演示数据。 `PERIPHERAL_SAMPLE_PROJECT_CODE` 和 `PERIPHERAL_SAMPLE_DB_NAME` 仅供显式运行的种子、导出等脚本选择默认库,不再限制 HTTP 请求。工单变更在当前项目已有 `workbench.domain_event_log` 时于同一事务发布事件;未配置事件表时不自动建表,实时同步仍需项目工作台事件结构。 后台同步器和调查 worker 支持 `OPERATIONAL_WORKER_PROJECT_IDS`(项目 UUID,逗号分隔),每个项目维护独立凭据、事件游标和会话状态;未配置时继续使用原 `OPERATIONAL_WORKER_PROJECT_ID`。多项目配置只选择运行范围,不新增项目成员权限,服务身份必须已获各项目授权。修改后需重启两个常驻进程,参见 [工作进程配置](operational-task-workers.md)。 主要接口包括: - `/api/sample/v1/context` 与 `/scenario` - `/api/sample/v1/work-orders` - `/api/sample/v1/test-data` - `/api/sample/v1/monitoring/points`、`observations`、`alerts` - `/api/sample/v1/demo-data` SCADA 按月数据集、5 分钟时间定位、主动周期监测、质量校验、爆管调查与定位边界见 [SCADA 外围系统时段回放、主动监测与爆管调查设计](scada-peripheral-replay-design.md)。部署回放存储需在项目 BizDB 执行 `031_scada_replays.sql`;项目通用配置需在 MetaDB 执行 `033_metadata_project_settings.sql`。监测频率保存于 `projects.settings.scada_monitoring_interval_seconds`,压力与出厂边界流量角色保存在类型化 `projects.settings.scada_monitoring` 中;本阶段不注册定时任务,也不部署 SCADA 回放 worker。代码和结构迁移完成不等于月度数据或 Agent 联调已经验收。 独立监测样本和测试数据通过 `scripts/seed_peripheral_sample.py` 写入 `tjwater_v2` 的 `peripheral_sample` schema。固定窗口为 2026-09-17,样本标记为 `synthetic` 并使用 `DEMO-` 前缀;业务工单保持 `system_data` 来源。 ## 工单事件调查 `POST /api/sample/v1/work-orders` 可创建 `work_order_type=water_quality_complaint` 的水质投诉事件,或 `work_order_type=burst_report` 的爆管上报,均默认使用业务来源。二者由外围同步器转换为统一事件调查任务,再由后端 worker 可靠交给 Agent;浏览器不再负责启动调查。事件和执行请求 ID 均由稳定业务身份确定,回执恢复或重试不会重复创建任务。普通业务录入无需绑定演练场景码,也不会向 Agent 注入预设监测结果或邻区投诉情况。 外围服务只保存工单,不直接调用 Agent。`scripts/sync_peripheral_work_orders.py` 使用受认证的后台身份同步外部版本并创建调查任务,`scripts/run_operational_worker.py` 再将持久化的待执行任务交给 Agent,因此浏览器关闭后流程仍可继续。 ## 工单事件主动同步 工单的创建、编辑、现场反馈和删除改为由来源事件触发同步。浏览器继续使用已有工作台 SSE,不需要新增连接或定时工单轮询。 ```text 外围工单写入 → 同一 PostgreSQL 事务保存源记录、历史版本和变更事件 → COMMIT 后 NOTIFY → Server 工作台 SSE 唤醒外围同步器 → 同步器通过授权 API 更新任务投影 → task/action/notification 事件推送给浏览器 ``` ### 项目与存储范围 当前 HTTP 请求按已授权项目的 Metadata 路由访问其 BizDB,源变更事件复用 `workbench.domain_event_log` 的 `workbench.work_order.changed` topic。聚合类型为 `peripheral_work_order`,元数据只包含 `source=peripheral_sample`、工单 ID 和 `created|updated|deleted`,不包含正文。 源记录、历史版本及变更事件在同一事务写入,提交后通过 PostgreSQL NOTIFY 唤醒监听;事务失败整体回滚,幂等重放不再次产生事件。外围服务只保存源事实,任务投影、调查创建和通知仍经 Server 状态机与授权 API 处理。 代码仅在当前项目已有事件表时写事件,不自动创建表、不回退其他项目数据库;表缺失时跳过事件发布,已有表写入失败则使源变更事务失败。HTTP 项目路由、数据库结构是否部署、后台同步器是否配置该项目是三个条件,需要分别核验。早期测试和迁移仅在 `tjwater_v2` 执行的记录不再作为运行时代码的固定库限制。扩展遵循 [数据库变更门禁](workbench-database-rollout.md)。 ### 同步器 从 Server 仓库运行已有命令: ```bash conda run -n server python scripts/sync_peripheral_work_orders.py conda run -n server python scripts/sync_peripheral_work_orders.py --once ``` 继续复用 `OPERATIONAL_WORKER_*` OAuth 工作身份及 `OPERATIONAL_ACTION_PERIPHERAL_URL`,不需要新账号或密钥。启动和游标过期时先读取事件游标,再进行全量对账;扫描期间的事件从该游标继续补读。相同源版本仍由原幂等键去重。源事件按工单 ID 唤醒同步器;动作回执事件只补查该任务关联的工单,避免源创建早于回执保存时漏同步。任务投影事件不再触发全量扫描。 正常状态收到源事件即处理对应 ID:`GET /api/v1/operational-tasks?peripheral_work_order_id=` 只返回同项目的来源任务和动作回执关联任务;外围接口只读取该工单,不再分页遍历全部工单和全部调查详情。重复 ID 合并;目标工单的明确删除只撤回该工单投影,不影响其他任务。 内存待处理 ID 最多 200 个,超过上限合并为一次全量对账,不持续扩张队列。这是突发积压保护,不增加公共配置项。 默认每 300 秒仍进行一次全量对账,处理漏通知、断线、旧批量脚本直接写库及重启。持续收到事件不会推迟全量对账期限。可用 `--reconcile-seconds` 调整这一兜底周期(最小 1 秒),不再复用工况 worker 的 30 秒租约检查周期。同步失败按 5、10、20、40、60 秒退避重试;事件连接断开每 5 秒重试,保留最后游标。扫描开始前取出待处理 ID,扫描期间新到达的 ID 留给下一轮;失败的 ID 会放回队列,重试不依赖新消息。 浏览器收到源事件时不会提前刷新业务快照,而是等待任务投影事件后读取对应任务。工况手动刷新仍只读 Server 权威快照。工单事件不等于 Header 通知;只有需要处理的业务待办或告警才进入通知列表。 游标顺序、共享 Hub 查询与兼容边界见 [工作台事件链路](workbench-event-refinement.md)。新增查询参数已从 Server OpenAPI 同步生成 Agent 类型,不另建手写契约。 ### 验证与启用 ```bash conda run -n server python -m pytest tests/unit/test_peripheral_push_sync.py tests/unit/test_peripheral_work_order_events.py tests/unit/test_peripheral_task_sync.py -q PERIPHERAL_SAMPLE_LIVE_TEST=1 conda run -n server python -m pytest tests/integration/test_peripheral_work_order_events.py -q ``` 数据库测试强制限定 `tjwater_v2`,使用随机项目键和回滚事务,不保留测试工单或事件。覆盖创建、修改、反馈、删除、幂等重放及回滚无事件。HTTP 隔离测试覆盖源事件即时投影、游标重置、断线续传、失败重试和监听取消。 发布时外围服务随主 Server 镜像更新,同步器是独立常驻进程;需按维护者授权的发布流程分别更新 Server、同步器及配套前端。提交代码不证明现有进程已重载。回退应用代码不涉及数据结构回滚,已有事件仍可正常补读;回退到旧同步器后恢复周期对账。