feat(workbench): persist operational workflows and events
This commit is contained in:
@@ -5,6 +5,7 @@ from uuid import UUID
|
||||
from pydantic import BaseModel, ConfigDict, Field, StrictBool, StrictFloat, StrictInt, model_validator
|
||||
|
||||
from app.domain.schemas.shared import ObjectRef, TimeRange, WireDateTime
|
||||
from app.domain.schemas.workbench_runtime import WorkbenchRuntimeWorkOrderSubtask
|
||||
|
||||
Text = Annotated[str, Field(min_length=1, max_length=4000)]
|
||||
PositiveInt = Annotated[StrictInt, Field(ge=1)]
|
||||
@@ -116,8 +117,21 @@ class TaskPage(Model):
|
||||
next_cursor: UUID | None
|
||||
|
||||
|
||||
WorkOrderStatus = Literal["draft", "open", "in_progress", "completed", "cancelled"]
|
||||
WorkOrderStatus = Literal[
|
||||
"draft", "open", "in_progress", "awaiting_reply", "completed", "cancelled"
|
||||
]
|
||||
Priority = Literal["low", "normal", "high", "urgent"]
|
||||
WorkOrderType = Literal["general", "water_quality_complaint", "burst_report"]
|
||||
WorkOrderSubtasks = Annotated[
|
||||
list[WorkbenchRuntimeWorkOrderSubtask], Field(min_length=1, max_length=50)
|
||||
]
|
||||
|
||||
|
||||
def _validate_unique_subtasks(
|
||||
subtasks: list[WorkbenchRuntimeWorkOrderSubtask] | None,
|
||||
) -> None:
|
||||
if subtasks is not None and len({item.id for item in subtasks}) != len(subtasks):
|
||||
raise ValueError("work order subtask ids must be unique")
|
||||
|
||||
|
||||
class CreateWorkOrder(Model):
|
||||
@@ -128,11 +142,16 @@ class CreateWorkOrder(Model):
|
||||
priority: Priority = "normal"
|
||||
assignee: Annotated[str, Field(min_length=1, max_length=200)] | None = None
|
||||
scheduled_at: WireDateTime | None = None
|
||||
work_order_type: WorkOrderType = "general"
|
||||
location: Annotated[str, Field(min_length=1, max_length=200)] | None = None
|
||||
scenario_code: Annotated[str, Field(min_length=1, max_length=128)] | None = None
|
||||
subtasks: WorkOrderSubtasks | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def aware_schedule(self):
|
||||
if self.scheduled_at is not None and self.scheduled_at.tzinfo is None:
|
||||
raise ValueError("scheduled_at must include timezone")
|
||||
_validate_unique_subtasks(self.subtasks)
|
||||
return self
|
||||
|
||||
|
||||
@@ -147,6 +166,12 @@ class UpdateWorkOrder(Model):
|
||||
clear_assignee: StrictBool = False
|
||||
scheduled_at: WireDateTime | None = None
|
||||
clear_scheduled_at: StrictBool = False
|
||||
location: Annotated[str, Field(min_length=1, max_length=200)] | None = None
|
||||
clear_location: StrictBool = False
|
||||
scenario_code: Annotated[str, Field(min_length=1, max_length=128)] | None = None
|
||||
clear_scenario_code: StrictBool = False
|
||||
subtasks: WorkOrderSubtasks | None = None
|
||||
clear_subtasks: StrictBool = False
|
||||
|
||||
@model_validator(mode="after")
|
||||
def valid_update(self):
|
||||
@@ -157,8 +182,15 @@ class UpdateWorkOrder(Model):
|
||||
raise ValueError("clear_assignee conflicts with assignee")
|
||||
if self.clear_scheduled_at and self.scheduled_at is not None:
|
||||
raise ValueError("clear_scheduled_at conflicts with scheduled_at")
|
||||
if self.clear_location and self.location is not None:
|
||||
raise ValueError("clear_location conflicts with location")
|
||||
if self.clear_scenario_code and self.scenario_code is not None:
|
||||
raise ValueError("clear_scenario_code conflicts with scenario_code")
|
||||
if self.clear_subtasks and self.subtasks is not None:
|
||||
raise ValueError("clear_subtasks conflicts with subtasks")
|
||||
if self.scheduled_at is not None and self.scheduled_at.tzinfo is None:
|
||||
raise ValueError("scheduled_at must include timezone")
|
||||
_validate_unique_subtasks(self.subtasks)
|
||||
return self
|
||||
|
||||
|
||||
@@ -182,6 +214,15 @@ class WorkOrder(Model):
|
||||
updated_at: WireDateTime
|
||||
created_by: str
|
||||
updated_by: str
|
||||
work_order_type: WorkOrderType = "general"
|
||||
location: str | None = None
|
||||
scenario_code: str | None = None
|
||||
subtasks: WorkOrderSubtasks | None = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def unique_subtasks(self):
|
||||
_validate_unique_subtasks(self.subtasks)
|
||||
return self
|
||||
|
||||
|
||||
class WorkOrderPage(Model):
|
||||
|
||||
@@ -263,7 +263,13 @@ class PostgresStore:
|
||||
f"SELECT document FROM peripheral_sample.{table} WHERE project_id=%s AND {id_column}=%s AND deleted_at IS NULL",
|
||||
(who.project_id, resource_id)).fetchone()
|
||||
ensure(row is not None, missing_code, missing_message, 404)
|
||||
return row["document"]
|
||||
return self._active_managed_document(row["document"])
|
||||
|
||||
@staticmethod
|
||||
def _active_managed_document(document: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Project a current active row without legacy soft-delete metadata."""
|
||||
return {key: value for key, value in document.items()
|
||||
if key not in {"deleted_at", "deleted_by"}}
|
||||
|
||||
def _list_managed(self, who: Identity, table: str, id_column: str,
|
||||
limit: int, cursor: UUID | None) -> dict[str, Any]:
|
||||
@@ -277,7 +283,7 @@ class PostgresStore:
|
||||
sql += f" AND {id_column}>%s"
|
||||
params.append(cursor)
|
||||
rows = conn.execute(sql + f" ORDER BY {id_column} LIMIT %s", params + [limit + 1]).fetchall()
|
||||
docs = [row["document"] for row in rows[:limit]]
|
||||
docs = [self._active_managed_document(row["document"]) for row in rows[:limit]]
|
||||
return {"items": docs, "next_cursor": docs[-1][id_column] if len(rows) > limit else None}
|
||||
|
||||
def _save_work_order(self, conn, who: Identity, document: dict[str, Any]) -> dict[str, Any]:
|
||||
@@ -319,11 +325,18 @@ class PostgresStore:
|
||||
"WORK_ORDER_NOT_FOUND", "测试工单不存在")
|
||||
ensure(document["revision"] == command.expected_revision, "REVISION_CONFLICT", "工单版本已变化,请重新读取", 409)
|
||||
changes = command.model_dump(mode="json", exclude_unset=True,
|
||||
exclude={"request_id", "expected_revision", "clear_assignee", "clear_scheduled_at"})
|
||||
exclude={"request_id", "expected_revision", "clear_assignee", "clear_scheduled_at",
|
||||
"clear_location", "clear_scenario_code", "clear_subtasks"})
|
||||
if command.clear_assignee:
|
||||
changes["assignee"] = None
|
||||
if command.clear_scheduled_at:
|
||||
changes["scheduled_at"] = None
|
||||
if command.clear_location:
|
||||
changes["location"] = None
|
||||
if command.clear_scenario_code:
|
||||
changes["scenario_code"] = None
|
||||
if command.clear_subtasks:
|
||||
changes["subtasks"] = None
|
||||
document.update(changes)
|
||||
document.update(revision=document["revision"] + 1, updated_at=datetime.now(timezone.utc).isoformat(),
|
||||
updated_by=who.user_id)
|
||||
|
||||
Reference in New Issue
Block a user