271 lines
10 KiB
Python
271 lines
10 KiB
Python
"""Idempotently seed the workbench's original demo reservation tasks."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from datetime import datetime, timedelta
|
|
from pathlib import Path
|
|
from uuid import UUID, uuid4
|
|
from zoneinfo import ZoneInfo
|
|
|
|
from psycopg.types.json import Jsonb
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from app.domain.schemas.workbench_runtime import WorkbenchRuntimeWorkOrder
|
|
from app.native.wndb.core.connection import project_connection
|
|
|
|
|
|
SCHEMA_SQL = ROOT / "resources/sql/011_workbench_work_orders.sql"
|
|
SHANGHAI = ZoneInfo("Asia/Shanghai")
|
|
DATASET_ID = "workbench-demo-orders@1"
|
|
DEMO_DATE = datetime(2026, 9, 17, tzinfo=SHANGHAI)
|
|
SEEDED_WORK_ORDER_PREFIX = "mock-work-order-"
|
|
LEGACY_WORK_ORDER_SOURCES = (
|
|
"合成数据预约工单",
|
|
"外围样板预约工单",
|
|
)
|
|
|
|
WORK_ORDER_TEMPLATES = (
|
|
{
|
|
"id": "incident-valve-dispatch",
|
|
"code": "DEMO-WO-20260917-001",
|
|
"title": "演示工单:华山路阀门边界确认",
|
|
"summary": "异常事件分析后生成阀门边界确认任务,派发前需调度员复核影响范围。",
|
|
"detail": "该条目为工作台演示数据,用于展示人工确认界面,不代表真实提交或岗位通知。",
|
|
"recommendation": "确认影响用户、上下游压力和应急隔离边界后,再通知执行岗位到场。",
|
|
"source": "异常事件分析",
|
|
"location": "华山路沿线阀门组 VG-11",
|
|
"dispatcher": "调度中心值班长",
|
|
"assignee": "调度执行岗",
|
|
"priority": "urgent",
|
|
"risk_level": "attention",
|
|
"phase": "dispatch",
|
|
"start_hour": 10,
|
|
"start_minute": 30,
|
|
"duration_minutes": 40,
|
|
"dispatch_lead_minutes": 30,
|
|
"reply_window_minutes": 20,
|
|
"reply_requirements": [
|
|
"确认阀门当前开度",
|
|
"记录影响分区和用户范围",
|
|
"回传派发确认截图",
|
|
],
|
|
},
|
|
{
|
|
"id": "valve-operation-execution",
|
|
"code": "DEMO-WO-20260917-002",
|
|
"title": "演示工单:北辰分区阀门开度调整",
|
|
"summary": "根据管网智能调度方案,现场调整北辰分区边界阀门开度并复核压力。",
|
|
"detail": "该工单模拟系统派发后的执行阶段,班组正在按方案对阀门要素进行操作。",
|
|
"recommendation": "执行过程中保持调度语音联络,调整后立即复核末端压力和流量变化。",
|
|
"source": "调度方案确认",
|
|
"location": "北辰分区边界阀门 BV-07",
|
|
"dispatcher": "管网调度岗",
|
|
"assignee": "北辰抢修二组",
|
|
"priority": "urgent",
|
|
"risk_level": "attention",
|
|
"phase": "execution",
|
|
"start_hour": 11,
|
|
"start_minute": 0,
|
|
"duration_minutes": 55,
|
|
"dispatch_lead_minutes": 25,
|
|
"reply_window_minutes": 20,
|
|
"reply_requirements": [
|
|
"上传阀门操作前后照片",
|
|
"记录最终开度",
|
|
"复核北辰末端压力不低于 0.22MPa",
|
|
],
|
|
},
|
|
{
|
|
"id": "pump-boundary-reply",
|
|
"code": "DEMO-WO-20260917-003",
|
|
"title": "演示工单:泵站边界复核复令",
|
|
"summary": "泵站出水边界复核已完成,等待班组复令和调度确认闭环。",
|
|
"detail": "该工单模拟执行完成后的复令阶段,用于检查处置结果、照片和指标回传。",
|
|
"recommendation": "核对泵站出水压力、频率曲线和分区边界模拟结果后完成复令。",
|
|
"source": "模型复核建议",
|
|
"location": "北辰二级泵站 P-02",
|
|
"dispatcher": "模型复核岗",
|
|
"assignee": "泵站运行班组",
|
|
"priority": "normal",
|
|
"risk_level": "normal",
|
|
"phase": "reply",
|
|
"start_hour": 13,
|
|
"start_minute": 0,
|
|
"duration_minutes": 45,
|
|
"dispatch_lead_minutes": 20,
|
|
"reply_window_minutes": 35,
|
|
"reply_requirements": [
|
|
"上传泵站运行截图",
|
|
"填写出水压力复核结果",
|
|
"确认模型边界已同步更新",
|
|
],
|
|
},
|
|
)
|
|
|
|
|
|
def _build_activity(template: dict, scheduled_at: datetime, updated_at: datetime) -> list[dict]:
|
|
created_at = scheduled_at - timedelta(minutes=template["dispatch_lead_minutes"])
|
|
activity = [{"sequence": 1, "occurred_at": created_at,
|
|
"actor": template["dispatcher"], "kind": "created",
|
|
"title": "演示工单创建", "detail": template["summary"]}]
|
|
if template["phase"] != "dispatch":
|
|
activity.append({"sequence": 2, "occurred_at": updated_at,
|
|
"actor": template["assignee"], "kind": "updated",
|
|
"title": "演示状态更新", "detail": template["detail"]})
|
|
return activity
|
|
|
|
|
|
def build_demo_work_orders() -> list[dict]:
|
|
"""Build the three reservation tasks previously provided by the frontend mock."""
|
|
documents = []
|
|
for template in WORK_ORDER_TEMPLATES:
|
|
scheduled_at = DEMO_DATE.replace(
|
|
hour=template["start_hour"],
|
|
minute=template["start_minute"],
|
|
)
|
|
status = {
|
|
"dispatch": "pending",
|
|
"execution": "running",
|
|
"reply": "awaiting_reply",
|
|
}[template["phase"]]
|
|
updated_at = {
|
|
"dispatch": scheduled_at - timedelta(minutes=5),
|
|
"execution": scheduled_at + timedelta(minutes=20),
|
|
"reply": scheduled_at + timedelta(minutes=template["duration_minutes"]),
|
|
}[template["phase"]]
|
|
projected = {
|
|
"id": f"{SEEDED_WORK_ORDER_PREFIX}{template['id']}",
|
|
"code": template["code"],
|
|
"scheduled_at": scheduled_at,
|
|
"title": template["title"],
|
|
"summary": template["summary"],
|
|
"status": status,
|
|
"risk_level": template["risk_level"],
|
|
"updated_at": updated_at,
|
|
"detail": template["detail"],
|
|
"recommendation": template["recommendation"],
|
|
"duration_minutes": template["duration_minutes"],
|
|
"source": template["source"],
|
|
"dispatcher": template["dispatcher"],
|
|
"priority": template["priority"],
|
|
"location": template["location"],
|
|
"assignee": template["assignee"],
|
|
"feedback_requirements": template["reply_requirements"],
|
|
"activity": _build_activity(template, scheduled_at, updated_at),
|
|
}
|
|
documents.append(
|
|
WorkbenchRuntimeWorkOrder.model_validate(projected).model_dump(mode="json")
|
|
)
|
|
return documents
|
|
|
|
|
|
def replace_demo_work_orders(conn, project_id: UUID, documents: list[dict]) -> None:
|
|
"""Remove the obsolete synthetic projection and replace this seed dataset."""
|
|
conn.execute(
|
|
"""
|
|
DELETE FROM workbench.work_orders
|
|
WHERE project_id = %s
|
|
AND (
|
|
work_order_id LIKE 'demo-work-order-%%'
|
|
OR work_order_id LIKE %s
|
|
OR document->>'source' = ANY(%s)
|
|
)
|
|
""",
|
|
(
|
|
project_id,
|
|
f"{SEEDED_WORK_ORDER_PREFIX}%",
|
|
list(LEGACY_WORK_ORDER_SOURCES),
|
|
),
|
|
)
|
|
for document in documents:
|
|
conn.execute(
|
|
"""
|
|
INSERT INTO workbench.work_orders
|
|
(project_id, work_order_id, scheduled_at, updated_at, document)
|
|
VALUES (%s, %s, %s, %s, %s)
|
|
ON CONFLICT (project_id, work_order_id) DO UPDATE
|
|
SET scheduled_at = EXCLUDED.scheduled_at,
|
|
updated_at = EXCLUDED.updated_at,
|
|
document = EXCLUDED.document
|
|
""",
|
|
(
|
|
project_id,
|
|
document["id"],
|
|
document["scheduled_at"],
|
|
document["updated_at"],
|
|
Jsonb(document),
|
|
),
|
|
)
|
|
|
|
|
|
def publish_demo_work_order_batch_event(conn, project_id: UUID, count: int) -> None:
|
|
"""Invalidate the demo projection once, never once per seeded row."""
|
|
cursor = conn.execute(
|
|
"""INSERT INTO workbench.domain_event_log
|
|
(project_id,event_id,topic,aggregate_type,aggregate_id,
|
|
aggregate_revision,occurred_at,metadata)
|
|
SELECT %s,%s,'workbench.work_order.changed','demo_work_order_dataset',%s,
|
|
COALESCE(max(aggregate_revision),0)+1,now(),%s
|
|
FROM workbench.domain_event_log
|
|
WHERE project_id=%s AND aggregate_type='demo_work_order_dataset'
|
|
AND aggregate_id=%s
|
|
RETURNING event_seq""",
|
|
(
|
|
project_id, uuid4(), DATASET_ID,
|
|
Jsonb({"namespace": "demo-work-order", "count": count}),
|
|
project_id, DATASET_ID,
|
|
),
|
|
)
|
|
row = cursor.fetchone()
|
|
conn.execute(
|
|
"SELECT pg_notify('workbench_events', %s)",
|
|
(f'{project_id}:{row["event_seq"]}',),
|
|
)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--database-name", default="tjwater_v2")
|
|
parser.add_argument("--project-id", required=True, type=UUID)
|
|
args = parser.parse_args()
|
|
documents = build_demo_work_orders()
|
|
|
|
with project_connection(args.database_name) as conn, conn.transaction():
|
|
conn.execute(SCHEMA_SQL.read_text(encoding="utf-8"))
|
|
replace_demo_work_orders(conn, args.project_id, documents)
|
|
publish_demo_work_order_batch_event(conn, args.project_id, len(documents))
|
|
|
|
print(
|
|
json.dumps(
|
|
{
|
|
"database": args.database_name,
|
|
"project_id": str(args.project_id),
|
|
"dataset_id": DATASET_ID,
|
|
"work_orders": [
|
|
{
|
|
"id": document["id"],
|
|
"code": document["code"],
|
|
"title": document["title"],
|
|
"status": document["status"],
|
|
"feedback_requirement_count": len(
|
|
document["feedback_requirements"]
|
|
),
|
|
}
|
|
for document in documents
|
|
],
|
|
},
|
|
ensure_ascii=False,
|
|
indent=2,
|
|
)
|
|
)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|