392 lines
14 KiB
Python
392 lines
14 KiB
Python
"""
|
|
Material 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 MaterialSyncNode
|
|
from schemas.material.material import MaterialResponse, MaterialCreate, MaterialUpdate, MaterialPlanUpdate
|
|
|
|
|
|
class MaterialApiHandler(BaseApiHandler):
|
|
"""物资 API Handler"""
|
|
|
|
def __init__(self):
|
|
super().__init__(node_class=MaterialSyncNode, schema=MaterialResponse)
|
|
self.update_schemas = [MaterialCreate, MaterialUpdate, MaterialPlanUpdate]
|
|
|
|
async def load(self) -> List[SyncNode]:
|
|
"""
|
|
加载物资列表
|
|
|
|
依赖 Project
|
|
使用 GET /material/list?project_id={id} 接口
|
|
"""
|
|
try:
|
|
projects = self._collection.filter(node_type="project")
|
|
project_ids = [p.data_id for p in projects if p.data_id]
|
|
|
|
if not project_ids:
|
|
print("⚠️ No projects found, skipping material load")
|
|
return []
|
|
|
|
all_materials = []
|
|
for project_id in project_ids:
|
|
try:
|
|
materials = await api_get_material_list(self.api_client, project_id)
|
|
|
|
for material in materials:
|
|
# 已验证的 Pydantic model,转换为 dict
|
|
material_data = material.model_dump()
|
|
all_materials.append(self._create_node(material_data))
|
|
|
|
except Exception as e:
|
|
print(f"❌ Failed to load materials for project {project_id}: {e}")
|
|
continue
|
|
|
|
return all_materials
|
|
|
|
except Exception as e:
|
|
print(f"❌ Failed to load materials: {e}")
|
|
return []
|
|
|
|
async def create_all(self, nodes: List[SyncNode]) -> List[TaskResult]:
|
|
"""批量创建物资"""
|
|
results = []
|
|
for node in nodes:
|
|
result = await self._create_single(node)
|
|
results.append(result)
|
|
return results
|
|
|
|
async def update_all(self, nodes: List[SyncNode]) -> List[TaskResult]:
|
|
"""
|
|
批量更新物资
|
|
|
|
支持两种更新类型:
|
|
1. MaterialUpdate - 基础信息更新
|
|
2. MaterialPlanUpdate - 计划信息更新
|
|
|
|
更新逻辑:
|
|
- 对每种 schema,检查是否有差异
|
|
- 如果有差异,调用对应 API
|
|
"""
|
|
from pydantic import ValidationError
|
|
|
|
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
|
|
|
|
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
|
|
steps_payload = self._get_request_steps(node)
|
|
steps_sent = False
|
|
|
|
# 1. 基础信息更新 (MaterialUpdate)
|
|
force_steps_update = self._steps_changed(node)
|
|
base_diff = self.get_update_schema_diff(
|
|
MaterialUpdate,
|
|
data,
|
|
origin_data,
|
|
node.node_id,
|
|
force_full_schema=force_steps_update,
|
|
)
|
|
if base_diff:
|
|
try:
|
|
update_model = MaterialUpdate.model_validate(data)
|
|
push_id = self._generate_push_id()
|
|
await api_update_material(
|
|
self.api_client,
|
|
update_model,
|
|
push_id,
|
|
biz_id,
|
|
steps=steps_payload if not steps_sent else [],
|
|
)
|
|
self._mark_approval_snapshot_synced(node)
|
|
steps_sent = steps_sent or bool(steps_payload)
|
|
node_updated = True
|
|
except ValidationError as e:
|
|
error = f"{ERROR_VALIDATION_FAILED}: {e}"
|
|
|
|
# 2. 计划信息更新 (MaterialPlanUpdate)
|
|
plan_diff = self.get_update_schema_diff(MaterialPlanUpdate, data, origin_data, node.node_id)
|
|
if not error and plan_diff:
|
|
try:
|
|
plan_model = MaterialPlanUpdate.model_validate(data)
|
|
push_id = self._generate_push_id()
|
|
# 需要调用对应的计划更新 API
|
|
await api_update_material_plan(
|
|
self.api_client,
|
|
plan_model.model_dump(exclude_unset=True),
|
|
push_id,
|
|
biz_id,
|
|
steps=steps_payload if not steps_sent else [],
|
|
)
|
|
self._mark_approval_snapshot_synced(node)
|
|
steps_sent = steps_sent or bool(steps_payload)
|
|
node_updated = True
|
|
except ValidationError as e:
|
|
error = f"{ERROR_VALIDATION_FAILED}: {e}"
|
|
|
|
# 汇总结果
|
|
if error:
|
|
results.append(TaskResult.failed(node_id=node.node_id, error=error))
|
|
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"))
|
|
|
|
return results
|
|
|
|
async def delete_all(self, nodes: List[SyncNode]) -> List[TaskResult]:
|
|
"""批量删除物资"""
|
|
results = []
|
|
for node in nodes:
|
|
result = await self._delete_single(node)
|
|
results.append(result)
|
|
return results
|
|
|
|
async def _create_single(self, node: SyncNode) -> TaskResult:
|
|
"""创建物资"""
|
|
try:
|
|
data = node.get_data()
|
|
if not data:
|
|
return TaskResult.failed(
|
|
node_id=node.node_id,
|
|
error=ERROR_NODE_DATA_NONE
|
|
)
|
|
|
|
push_id = self._generate_push_id()
|
|
|
|
project_id = node.context.get("project_id")
|
|
if not project_id:
|
|
project_id = data.get("project_id")
|
|
|
|
if not project_id:
|
|
return TaskResult.failed(
|
|
node_id=node.node_id,
|
|
error=ERROR_PROJECT_ID_NOT_FOUND
|
|
)
|
|
|
|
# 验证并转换为 Pydantic model
|
|
from pydantic import ValidationError
|
|
create_data = {"project_id": project_id, **data}
|
|
try:
|
|
create_model = MaterialCreate.model_validate(create_data)
|
|
except ValidationError as e:
|
|
return TaskResult.failed(
|
|
node_id=node.node_id,
|
|
error=f"{ERROR_VALIDATION_FAILED}: {e}"
|
|
)
|
|
|
|
response = await api_create_material(
|
|
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:
|
|
return TaskResult.failed(
|
|
node_id=node.node_id,
|
|
error="Missing created_id in create response"
|
|
)
|
|
return TaskResult.success(node_id=node.node_id, data_id=created_id)
|
|
else:
|
|
return TaskResult.in_progress(node_id=node.node_id, task_id=push_id)
|
|
|
|
except Exception as e:
|
|
return TaskResult.failed(
|
|
node_id=node.node_id,
|
|
error=f"Create failed: {str(e)}"
|
|
)
|
|
|
|
async def _update_single(self, node: SyncNode) -> TaskResult:
|
|
"""更新物资"""
|
|
if not self.api_client:
|
|
return TaskResult.failed(
|
|
node_id=node.node_id,
|
|
error="api_client not initialized"
|
|
)
|
|
|
|
try:
|
|
data = node.get_data()
|
|
if not data:
|
|
return TaskResult.failed(
|
|
node_id=node.node_id,
|
|
error="Node data is None or validation failed"
|
|
)
|
|
|
|
push_id = self._generate_push_id()
|
|
biz_id = node.data_id or data.get("id")
|
|
if not biz_id:
|
|
return TaskResult.failed(node_id=node.node_id, error="Missing biz_id for update")
|
|
|
|
# 验证并转换为 Pydantic model
|
|
from pydantic import ValidationError
|
|
try:
|
|
update_model = MaterialUpdate.model_validate(data)
|
|
except ValidationError as e:
|
|
return TaskResult.failed(
|
|
node_id=node.node_id,
|
|
error=f"{ERROR_VALIDATION_FAILED}: {e}"
|
|
)
|
|
|
|
await api_update_material(self.api_client, update_model, push_id, biz_id)
|
|
return TaskResult.success(node_id=node.node_id)
|
|
|
|
except Exception as e:
|
|
return TaskResult.failed(
|
|
node_id=node.node_id,
|
|
error=f"Update failed: {str(e)}"
|
|
)
|
|
|
|
async def _delete_single(self, node: SyncNode) -> TaskResult:
|
|
"""删除物资"""
|
|
try:
|
|
push_id = self._generate_push_id()
|
|
biz_id = node.data_id
|
|
if not biz_id:
|
|
return TaskResult.failed(node_id=node.node_id, error="Missing biz_id for delete")
|
|
|
|
request_data = {
|
|
"push_id": push_id,
|
|
"biz_id": biz_id,
|
|
}
|
|
|
|
response = await self.api_client.request(
|
|
method="DELETE",
|
|
endpoint="/material/delete",
|
|
data=request_data
|
|
)
|
|
|
|
if response.get("code") == 200:
|
|
return TaskResult.success(node_id=node.node_id)
|
|
else:
|
|
return TaskResult.failed(
|
|
node_id=node.node_id,
|
|
error=f"API returned error: {response.get('message', 'Unknown error')}"
|
|
)
|
|
|
|
except Exception as e:
|
|
return TaskResult.failed(
|
|
node_id=node.node_id,
|
|
error=f"Delete failed: {str(e)}"
|
|
)
|
|
|
|
def _create_node(self, data: Dict[str, Any]) -> SyncNode:
|
|
"""
|
|
创建物资同步节点
|
|
|
|
设置 project_id 和 material_id 到 context
|
|
"""
|
|
node = self._create_basic_node(data, depend_ids=[])
|
|
|
|
if data.get("project_id"):
|
|
node.context["project_id"] = data["project_id"]
|
|
node.context["material_id"] = node.data_id
|
|
|
|
return node
|
|
|
|
|
|
# ========== 静态 API 函数 ==========
|
|
|
|
async def api_get_material(client, material_id: str) -> Dict[str, Any]:
|
|
"""获取单个物资数据 GET /material"""
|
|
response = await client.request(
|
|
method="GET",
|
|
endpoint="/material",
|
|
params={"material_id": material_id}
|
|
)
|
|
if response.get("code") != 200:
|
|
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
|
|
return response.get("data", {})
|
|
|
|
|
|
# ========== 静态 API 函数 ==========
|
|
|
|
async def api_get_material_list(client, project_id: str) -> List[MaterialResponse]:
|
|
"""GET /material/list - 获取物资列表"""
|
|
response = await client.request(
|
|
method="GET",
|
|
endpoint="/material/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 = MaterialResponse.model_validate(item)
|
|
validated_items.append(validated_item)
|
|
except ValidationError as e:
|
|
print(f"⚠️ Skipping invalid material item: {e}")
|
|
continue
|
|
|
|
return validated_items
|
|
|
|
|
|
async def api_create_material(client, data: MaterialCreate, push_id: str, *, steps: List[Dict[str, Any]] | None = None) -> Dict[str, Any]:
|
|
"""创建物资 POST /material/create"""
|
|
request_data = {"push_id": push_id, "data": data.model_dump(), "steps": steps or []}
|
|
response = await client.request(method="POST", endpoint="/material/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_material(
|
|
client,
|
|
data: MaterialUpdate,
|
|
push_id: str,
|
|
biz_id: str,
|
|
*,
|
|
steps: List[Dict[str, Any]] | None = None,
|
|
) -> None:
|
|
"""更新物资 PUT /material/update"""
|
|
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="/material/update", data=request_data)
|
|
if response.get("code") != 200:
|
|
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
|
|
|
|
|
|
async def api_update_material_plan(
|
|
client,
|
|
data: Dict[str, Any],
|
|
push_id: str,
|
|
biz_id: str,
|
|
*,
|
|
steps: List[Dict[str, Any]] | None = None,
|
|
) -> None:
|
|
"""更新物资计划 PUT /material/plan"""
|
|
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
|
|
response = await client.request(method="PUT", endpoint="/material/plan", data=request_data)
|
|
if response.get("code") != 200:
|
|
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
|