255 lines
9.0 KiB
Python
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())
|