feat(investigations): add event impact and work-order feedback flows

Add isolated burst and pollution impact evidence, parallel work-order workflows, persisted notifications and graph updates.

Checkpoint current code. Known follow-ups remain for large pollution result previews and graceful geometry failure.
This commit is contained in:
2026-11-14 15:14:02 +08:00
parent 471ad92a47
commit 2e4f6cb93b
49 changed files with 1826 additions and 93 deletions
+46 -5
View File
@@ -56,6 +56,7 @@ from app.domain.schemas.operational_task import (
OperationalTaskCreate,
OperationalTaskDetail,
OperationalTaskNotification,
WorkOrderNotificationReference,
OperationalTaskPermissions,
OperationalTaskSchedule,
OperationalTaskScheduleCreate,
@@ -874,7 +875,7 @@ class OperationalTaskService:
diagram = update_investigation_graph(saved_diagram, command.progress.graph_update)
elif saved_diagram is not None:
require(diagram is None or saved_diagram.edges is None, "GRAPH_UPDATE_REQUIRED",
"Use graph_update to extend the saved investigation graph", status=422)
"Use graph_update to patch saved nodes or extend the investigation graph", status=422)
if diagram is None:
diagram = adopt_legacy_investigation_graph(saved_diagram)
if diagram:
@@ -1258,6 +1259,7 @@ class OperationalTaskService:
task_id, "action_approval_pending", task["title"], command.reason, now,
required_permission=approval_permission,
subject_type="action", subject_id=action.action_id,
work_order_action=action if action.kind == "create_work_order" else None,
)
await self._save_request(
scope, command.request_id, body, {"action_id": str(action.action_id)}
@@ -1743,6 +1745,29 @@ class OperationalTaskService:
task_id, "task_failed",
task["title"], command.detail, now,
)
if command.source_revision > 1 and task["task_type"] in {"external_work_order", "event_investigation"}:
# The feedback, its notification and outbox event commit together.
# Duplicate request/source versions return above, before publishing.
work_order_action = next((item for item in await self.repo.list_actions(str(task_id))
if item["kind"] == "create_work_order" and item.get("receipt")
and command.source_event_id == f"{item['receipt']['adapter']}:{item['receipt']['receipt_id']}"), None)
is_work_order_feedback = (task["task_type"] == "external_work_order"
or work_order_action is not None
or (task["task_type"] == "event_investigation" and matching_source))
if is_work_order_feedback:
await self._notify(
task_id, "work_order_feedback", "收到工单反馈",
command.evidence.summary if command.evidence else command.detail, now,
subject_type="action" if work_order_action else "task",
subject_id=UUID(str(work_order_action["action_id"])) if work_order_action else task_id,
work_order_action=HumanApprovalAction.model_validate(work_order_action) if work_order_action else None,
work_order_reference=WorkOrderNotificationReference(
source_event_id=command.source_event_id,
source_revision=command.source_revision,
evidence_id=command.evidence.evidence_id if command.evidence else None,
status=command.status,
),
)
response = {
"source_event_id": command.source_event_id,
"source_revision": command.source_revision,
@@ -2161,12 +2186,24 @@ class OperationalTaskService:
lifecycle = self._investigation_status(actions, task)
recovery = task.get("handling_context") or {}
if lifecycle == "completed" and recovery:
require(recovery.get("verification_evidence_id") in report.report.evidence_refs,
"RECOVERY_EVIDENCE_REQUIRED", "最终复核报告必须引用恢复监测证据", status=409)
if task.get("workflow") == "water_pollution":
orders = [item for item in recovery.get("work_orders", {}).values()
if not item.get("superseded_by")]
feedback_refs = [item.get("feedback_evidence_id") for item in orders]
require(recovery.get("phase") in {"field_review", "completed"} and bool(orders)
and all(item["status"] == "completed" for item in orders)
and all(ref and ref in report.report.evidence_refs for ref in feedback_refs),
"POLLUTION_FEEDBACK_REQUIRED", "水污染最终报告必须复核并引用全部最新现场反馈证据", status=409)
verification_id = feedback_refs[-1]
else:
verification_id = recovery.get("verification_evidence_id")
require(verification_id in report.report.evidence_refs,
"RECOVERY_EVIDENCE_REQUIRED", "最终复核报告必须引用恢复监测证据", status=409)
require(report.report.outcome == "concluded", "RECOVERY_EVIDENCE_REQUIRED",
"恢复复核通过后才能结束事件", status=409)
recovery.update(phase="completed", title="恢复供水复核成功", completed_at=now.isoformat())
await self._resolve_investigation_alerts(str(task_id), recovery["verification_evidence_id"])
recovery.update(phase="completed", title=("水污染处置反馈已复核" if task.get("workflow") == "water_pollution"
else "恢复供水复核成功"), completed_at=now.isoformat())
await self._resolve_investigation_alerts(str(task_id), verification_id)
elif report.report.outcome == "insufficient_data":
terminal = all(node.status in {"completed", "skipped", "cancelled"}
for node in plan.plan.nodes)
@@ -2519,6 +2556,8 @@ class OperationalTaskService:
*, required_permission: str | None = None,
subject_type: str = "task", subject_id: UUID | None = None,
report_reference: InvestigationReportReference | None = None,
work_order_reference: WorkOrderNotificationReference | None = None,
work_order_action: HumanApprovalAction | None = None,
) -> None:
notification = OperationalTaskNotification(
notification_id=uuid4(), task_id=task_id, kind=kind,
@@ -2527,6 +2566,8 @@ class OperationalTaskService:
lifecycle_status="active", subject_type=subject_type,
subject_id=subject_id or task_id,
report_reference=report_reference,
work_order_reference=work_order_reference,
work_order_action=work_order_action,
)
await self.repo.insert_notification(_json(notification))
await self._append_event(