feat(workbench): add runtime GIS and sample integrations
This commit is contained in:
@@ -0,0 +1,254 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user