Files
ecm_sync_system/sync_state_machine/domain/construction/api_handler.py
T
strepsiades f5729d5a18 整理代码
2026-04-03 09:18:44 +08:00

381 lines
16 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Construction Task API Handler (V2)
职责:
- 加载施工任务列表(依赖 Project)
- 施工任务创建/更新/删除(批量接口)
"""
from typing import List, Dict, Any
from ...datasource.api.handler import BaseApiHandler
from ...datasource.task_result import TaskResult
from ...common.sync_node import SyncNode
from ...datasource.api.errors import (
ERROR_NODE_DATA_NONE,
ERROR_VALIDATION_FAILED,
ERROR_PROJECT_ID_NOT_FOUND
)
from .sync_node import ConstructionTaskSyncNode
from schemas.construction.construction import (
ConstructionResponse,
ConstructionCreate,
ConstructionUpdate,
ConstructionPlanUpdate
)
class ConstructionTaskApiHandler(BaseApiHandler):
"""施工任务 API Handler"""
def __init__(self):
super().__init__(node_class=ConstructionTaskSyncNode, schema=ConstructionResponse)
self.update_schemas = [ConstructionCreate, ConstructionUpdate, ConstructionPlanUpdate]
async def load(self) -> List[SyncNode]:
"""加载施工任务列表(依赖 Project"""
projects = self._collection.filter(node_type="project")
project_ids = [p.data_id for p in projects if p.data_id]
if not project_ids:
return []
all_tasks = []
for project_id in project_ids:
try:
tasks = await api_get_construction_task_list(self.api_client, project_id)
for task in tasks:
# 已验证的 Pydantic model,转换为 dict
task_data = task.model_dump()
all_tasks.append(self._create_node(task_data))
except Exception as e:
print(f"❌ Failed to load construction tasks for project {project_id}: {e}")
return all_tasks
async def create_all(self, nodes: List[SyncNode]) -> List[TaskResult]:
"""批量创建施工任务"""
results = []
for node in nodes:
data = node.get_data()
if not data:
results.append(TaskResult.failed(node_id=node.node_id, error=ERROR_NODE_DATA_NONE))
continue
project_id = node.context.get("project_id") or data.get("project_id")
if not project_id:
results.append(TaskResult.failed(node_id=node.node_id, error=ERROR_PROJECT_ID_NOT_FOUND))
continue
push_id = self._generate_push_id()
try:
# 验证并转换为 Pydantic model
from pydantic import ValidationError
create_data = {"project_id": project_id, **data}
try:
create_model = ConstructionCreate.model_validate(create_data)
except ValidationError as e:
results.append(TaskResult.failed(node_id=node.node_id, error=f"{ERROR_VALIDATION_FAILED}: {e}"))
continue
response = await api_create_construction_task(
self.api_client,
create_model,
push_id,
steps=self._get_request_steps(node),
)
if self.poll_mode == "sync":
created_id = self._extract_created_id_from_response(response)
if not created_id:
results.append(
TaskResult.failed(
node_id=node.node_id,
error="Missing created_id in create response"
)
)
continue
results.append(TaskResult.success(node_id=node.node_id, data_id=created_id))
else:
results.append(TaskResult.in_progress(node_id=node.node_id, task_id=push_id))
except Exception as e:
results.append(TaskResult.failed(node_id=node.node_id, error=str(e), push_id=push_id))
return results
async def update_all(self, nodes: List[SyncNode]) -> List[TaskResult]:
"""
批量更新施工任务
支持两种更新类型:
1. ConstructionUpdate - 基础信息更新 (PUT /construction/update)
业务逻辑:如果show_name字段不同,那么仅当type == 'QT'时才更新
2. ConstructionPlanUpdate - 计划信息更新 (PUT /construction/plan)
更新逻辑:
- 对每种 schema,将原始数据和新数据都转换为 schema 格式
- dump 成字典后比较
- 如果有差异且满足业务条件,调用对应 API
"""
results = []
for node in nodes:
data = node.get_data()
if not data:
results.append(TaskResult.failed(node_id=node.node_id, error="node data is None"))
continue
origin_data = node.get_origin_data() or {}
biz_id = node.data_id or data.get("id")
if not biz_id:
results.append(TaskResult.failed(node_id=node.node_id, error="Missing biz_id for update"))
continue
node_updated = False
error = None
push_failure_id = None
schema_status_lines: List[str] = []
# 1. 基础信息更新 (ConstructionUpdate)
# 业务逻辑:如果 show_name 字段不同,那么仅当 type == 'QT' 时才更新
force_steps_update = self._steps_changed(node)
steps_payload = self._get_request_steps(node)
steps_sent = False
base_new_schema_error = self._validate_schema(data, ConstructionUpdate)
base_changed_fields = self._schema_changed_fields(ConstructionUpdate, data, origin_data)
if force_steps_update and not base_new_schema_error and not base_changed_fields:
base_changed_fields = ["__steps__"]
base_update_called = False
base_update_error: str | None = None
if base_new_schema_error:
base_update_error = base_new_schema_error
elif base_changed_fields:
should_update = True
if "show_name" in base_changed_fields:
task_type = data.get("task_type") or data.get("type")
if task_type != "QT":
should_update = False
if should_update:
try:
update_payload = data if (force_steps_update and base_changed_fields == ["__steps__"]) else data
update_model = ConstructionUpdate.model_validate(update_payload)
push_id = self._generate_push_id()
request_steps = steps_payload if not steps_sent else []
if request_steps:
await api_update_construction_task(
self.api_client,
update_model,
push_id,
biz_id,
steps=request_steps,
)
else:
await api_update_construction_task(
self.api_client,
update_model,
push_id,
biz_id,
)
node_updated = True
base_update_called = True
steps_sent = steps_sent or bool(steps_payload)
self._mark_approval_snapshot_synced(node)
except Exception as e:
error = str(e)
base_update_error = str(e)
push_failure_id = push_id
else:
base_update_error = "show_name changed but task_type != QT"
schema_status_lines.append(
self.build_update_schema_status(
schema_name="ConstructionUpdate",
schema=ConstructionUpdate,
new_data=data,
origin_data=origin_data,
will_update=base_update_called,
update_error=base_update_error,
)
)
# 2. 计划信息更新 (ConstructionPlanUpdate)
plan_new_schema_error = self._validate_schema(data, ConstructionPlanUpdate)
plan_changed_fields = self._schema_changed_fields(ConstructionPlanUpdate, data, origin_data)
plan_update_called = False
plan_update_error: str | None = None
if not error:
if plan_new_schema_error:
plan_update_error = plan_new_schema_error
elif plan_changed_fields:
try:
plan_model = ConstructionPlanUpdate.model_validate(data)
plan_dict = plan_model.model_dump(exclude_unset=True)
push_id = self._generate_push_id()
request_steps = steps_payload if not steps_sent else []
if request_steps:
await api_update_construction_task_plan(
self.api_client,
plan_dict,
push_id,
biz_id,
steps=request_steps,
)
else:
await api_update_construction_task_plan(
self.api_client,
plan_dict,
push_id,
biz_id,
)
node_updated = True
plan_update_called = True
steps_sent = steps_sent or bool(steps_payload)
self._mark_approval_snapshot_synced(node)
except Exception as e:
error = str(e)
plan_update_error = str(e)
push_failure_id = push_id
schema_status_lines.append(
self.build_update_schema_status(
schema_name="ConstructionPlanUpdate",
schema=ConstructionPlanUpdate,
new_data=data,
origin_data=origin_data,
will_update=plan_update_called,
update_error=plan_update_error,
)
)
for line in schema_status_lines:
node.append_log(f"UPDATE_SCHEMA: {line}")
# 汇总结果
if error:
results.append(TaskResult.failed(node_id=node.node_id, error=error, push_id=push_failure_id))
elif node_updated:
results.append(TaskResult.success(node_id=node.node_id))
else:
# 没有任何字段需要更新
results.append(TaskResult.skipped(
node_id=node.node_id,
reason="No updatable fields changed | " + "; ".join(schema_status_lines)
))
return results
async def delete_all(self, nodes: List[SyncNode]) -> List[TaskResult]:
"""批量删除施工任务"""
results = []
for node in nodes:
push_id = self._generate_push_id()
biz_id = node.data_id
if not biz_id:
results.append(TaskResult.failed(node_id=node.node_id, error="Missing biz_id for delete"))
continue
try:
await api_delete_construction_task(self.api_client, biz_id, push_id)
results.append(TaskResult.success(node_id=node.node_id))
except Exception as e:
results.append(TaskResult.failed(node_id=node.node_id, error=str(e), push_id=push_id))
return results
async def poll_tasks(self, task_ids: List[str]) -> Dict[str, TaskResult]:
"""轮询异步任务状态"""
return await super().poll_tasks(task_ids)
def _create_node(self, data: Dict[str, Any]) -> SyncNode:
node = self._create_basic_node(data, depend_ids=[])
if data.get("project_id"):
node.context["project_id"] = data["project_id"]
node.context["task_id"] = node.data_id
return node
# ========== 静态 API 函数 ==========
async def api_get_construction_task_list(client, project_id: str) -> List[ConstructionResponse]:
response = await client.request(
method="GET",
endpoint="/construction/list",
params={"project_id": project_id}
)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
items = response.get("data", [])
# 验证每个返回项
from pydantic import ValidationError
validated_items = []
for item in items:
try:
validated_item = ConstructionResponse.model_validate(item)
validated_items.append(validated_item)
except ValidationError as e:
print(f"⚠️ Skipping invalid item: {e}")
continue
return validated_items
async def api_create_construction_task(client, data: ConstructionCreate, push_id: str, *, steps: List[Dict[str, Any]] | None = None) -> Dict[str, Any]:
request_data = {"push_id": push_id, "data": data.model_dump(), "steps": steps or []}
response = await client.request(method="POST", endpoint="/construction/create", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
return response
async def api_update_construction_task(
client,
data: ConstructionUpdate,
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True), "steps": steps or []}
response = await client.request(method="PUT", endpoint="/construction/update", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
async def api_get_construction_task(client, task_id: str) -> Dict[str, Any]:
"""GET /construction - 获取单个施工任务"""
response = await client.request(
method="GET",
endpoint="/construction",
params={"task_id": task_id}
)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
return response.get("data", {})
async def api_update_construction_task_plan(
client,
data: Dict[str, Any],
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
"""PUT /construction/plan - 更新施工计划"""
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
response = await client.request(method="PUT", endpoint="/construction/plan", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
async def api_delete_construction_task(client, task_id: str, push_id: str) -> None:
request_data = {"push_id": push_id, "biz_id": task_id}
response = await client.request(method="DELETE", endpoint="/construction/delete", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")