Files
next-tjwater-server/scripts/seed_workbench_work_orders.py
T

255 lines
9.0 KiB
Python

"""Project peripheral demo orders into the backend workbench order store."""
from __future__ import annotations
import argparse
import json
import sys
from datetime import datetime, timedelta
from pathlib import Path
from uuid import UUID
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")
PROJECTION_SOURCE = "合成数据预约工单"
DEMO_WORK_ORDER_ID = "demo-work-order-peripheral-inspection"
ORDER_DETAILS = {
"合成通信核查": {
"location": "DEMO-P001",
"duration_minutes": 30,
"reply_requirements": ["记录通信恢复时间", "确认缺口数据是否补传"],
},
"合成压力偏差复核": {
"location": "DEMO-P001 / DEMO-P002",
"duration_minutes": 45,
"reply_requirements": ["回填相邻测点对照结果", "确认合成样本复核结论"],
},
"合成零流量样本确认": {
"location": "DEMO-F001",
"duration_minutes": 20,
"reply_requirements": ["确认零值显示", "确认缺测值显示"],
},
}
def _parse_timestamp(value: str) -> datetime:
return datetime.fromisoformat(value.replace("Z", "+00:00"))
def _time_label(value: datetime) -> str:
return value.astimezone(SHANGHAI).strftime("%H:%M")
def _time_range(start: datetime, end: datetime) -> str:
return f"{_time_label(start)}-{_time_label(end)}"
def _subtask_status(status: str) -> str:
mapped = {
"open": "pending",
"in_progress": "running",
"completed": "completed",
}.get(status)
if mapped is None:
raise ValueError(f"unsupported peripheral work-order status: {status}")
return mapped
def _stage_statuses(status: str) -> tuple[str, str, str]:
if status == "completed":
return "done", "done", "done"
if status == "running":
return "done", "current", "pending"
return "current", "pending", "pending"
def project_peripheral_subtask(document: dict) -> dict:
status = _subtask_status(document.get("status"))
scheduled_at = _parse_timestamp(document["scheduled_at"])
details = ORDER_DETAILS.get(document["title"], {})
duration_minutes = int(details.get("duration_minutes", 30))
execution_end = scheduled_at + timedelta(minutes=duration_minutes)
reply_end = execution_end + timedelta(minutes=15)
dispatch_status, execution_status, reply_status = _stage_statuses(status)
assignee = document.get("assignee") or "待分派班组"
return {
"id": document["work_order_id"],
"title": document["title"],
"summary": document["summary"],
"status": status,
"scheduled_at": document["scheduled_at"],
"location": details.get("location", "演示点位"),
"assignee": assignee,
"duration_minutes": duration_minutes,
"reply_window_minutes": 15,
"stages": [
{
"id": "dispatch",
"sequence": 1,
"title": "派发",
"status": dispatch_status,
"owner": "演示调度",
"time_label": _time_label(scheduled_at - timedelta(minutes=15)),
"description": "预约工单子任务已进入演示调度流程。",
},
{
"id": "execution",
"sequence": 2,
"title": "执行",
"status": execution_status,
"owner": assignee,
"time_label": _time_range(scheduled_at, execution_end),
"description": "按外围合成场景执行本项核查。",
},
{
"id": "reply",
"sequence": 3,
"title": "复令",
"status": reply_status,
"owner": assignee,
"time_label": _time_range(execution_end, reply_end),
"description": "回传本项核查结果并完成子任务闭环。",
},
],
"reply_requirements": details.get(
"reply_requirements", ["回传演示核查结果"]
),
}
def project_peripheral_work_order(source_documents: list[dict]) -> dict:
if not source_documents:
raise ValueError("source documents must not be empty")
subtasks = [project_peripheral_subtask(document) for document in source_documents]
scheduled_values = [_parse_timestamp(item["scheduled_at"]) for item in source_documents]
updated_values = [_parse_timestamp(item["updated_at"]) for item in source_documents]
end_values = [
scheduled + timedelta(minutes=subtask["duration_minutes"])
for scheduled, subtask in zip(scheduled_values, subtasks, strict=True)
]
statuses = {subtask["status"] for subtask in subtasks}
if statuses == {"completed"}:
status = "completed"
elif statuses == {"pending"}:
status = "pending"
else:
status = "running"
priority = "urgent" if any(
document.get("priority") in {"high", "urgent"}
for document in source_documents
) else "normal"
projected = {
"id": DEMO_WORK_ORDER_ID,
"code": "DEMO-WO-001",
"scheduled_at": min(scheduled_values),
"title": "外围合成数据核查",
"summary": "依次核查合成通信、压力偏差和零流量样本。",
"status": status,
"risk_level": "attention" if priority == "urgent" else "normal",
"updated_at": max(updated_values),
"detail": "外围样板中的三项 synthetic 核查已组合为一张预约工单,仅用于演示。",
"recommendation": "按子任务逐项核查并分别完成阶段回传。",
"duration_minutes": max(
1,
int((max(end_values) - min(scheduled_values)).total_seconds() // 60),
),
"source": PROJECTION_SOURCE,
"dispatcher": "演示调度",
"priority": priority,
"subtasks": subtasks,
}
return WorkbenchRuntimeWorkOrder.model_validate(projected).model_dump(mode="json")
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()
with project_connection(args.database_name) as conn, conn.transaction():
conn.execute(SCHEMA_SQL.read_text(encoding="utf-8"))
source_rows = conn.execute(
"""
SELECT document
FROM peripheral_sample.work_orders
WHERE project_id = %s
AND deleted_at IS NULL
AND document->>'origin' = 'synthetic'
AND document->>'scheduled_at' IS NOT NULL
AND document->>'status' IN ('open', 'in_progress', 'completed')
ORDER BY (document->>'scheduled_at')::timestamptz, work_order_id
""",
(args.project_id,),
).fetchall()
source_documents = [row["document"] for row in source_rows]
if not source_documents:
raise RuntimeError("no eligible peripheral demo work orders found")
documents = [project_peripheral_work_order(source_documents)]
conn.execute(
"""
DELETE FROM workbench.work_orders
WHERE project_id = %s
AND (
work_order_id LIKE 'demo-work-order-%%'
OR document->>'source' = %s
OR document->>'source' = '外围样板预约工单'
)
""",
(args.project_id, PROJECTION_SOURCE),
)
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
""",
(
args.project_id,
document["id"],
document["scheduled_at"],
document["updated_at"],
Jsonb(document),
),
)
print(json.dumps({
"database": args.database_name,
"project_id": str(args.project_id),
"source": PROJECTION_SOURCE,
"work_orders": [
{
"id": document["id"],
"code": document["code"],
"title": document["title"],
"subtask_count": len(document["subtasks"]),
}
for document in documents
],
}, ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())