Files
ecm_sync_system/sync_state_machine/domain/contract/api_handler.py
T

362 lines
13 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.
"""
Contract API Handler
职责:
- 加载合同列表(依赖 Project)
- 合同创建(POST /contract/create,异步)
- 合同更新(PUT /contract/update,同步)
- 合同删除(DELETE /contract/delete,同步)
"""
from typing import List, Dict, Any, Optional
from ...datasource.api.handler import BaseApiHandler
from ...datasource.task_result import TaskResult
from ...common.sync_node import SyncNode
from ...datasource.api.client import ApiClient
from .sync_node import ContractSyncNode
from schemas.contract.contract import ContractResponse, ContractCreate, ContractUpdate
# ========== 静态 API 函数 ==========
async def api_get_contract_list(client: ApiClient, project_id: str) -> List[ContractResponse]:
"""GET /contract/list - 获取项目合同列表"""
response = await client.request(
method="GET",
endpoint="/contract/list",
params={"project_id": project_id}
)
if response.get("code") != 200:
raise Exception(f"Failed to get contract list: {response.get('message', 'Unknown error')}")
# V2 API 返回格式:{code: 200, data: [...]}
items = response.get("data", [])
return [ContractResponse.model_validate(item) for item in items]
async def api_get_contract(client: ApiClient, contract_id: str) -> ContractResponse:
"""GET /contract - 获取合同详情"""
response = await client.request(
method="GET",
endpoint="/contract",
params={"contract_id": contract_id}
)
if response.get("code") != 200:
raise Exception(f"Failed to get contract: {response.get('message', 'Unknown error')}")
return ContractResponse.model_validate(response["data"])
async def api_create_contract(
client: ApiClient,
data: Dict[str, Any],
push_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> Dict[str, Any]:
"""POST /contract/create - 创建合同(异步)"""
validated_data = ContractCreate.model_validate(data)
request_data = {"push_id": push_id, "data": validated_data.model_dump(exclude_none=True), "steps": steps or []}
response = await client.request("POST", "/contract/create", data=request_data)
if response.get("code") != 200:
raise Exception(f"Failed to create contract: {response.get('message', 'Unknown error')}")
return response
async def api_update_contract(
client: ApiClient,
data_id: str,
data: Dict[str, Any],
push_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> Dict[str, Any]:
"""PUT /contract/update - 更新合同(同步)"""
validated_data = ContractUpdate.model_validate(data)
request_data = {
"push_id": push_id,
"biz_id": data_id,
"data": validated_data.model_dump(exclude_none=True),
"steps": steps or [],
}
response = await client.request("PUT", "/contract/update", data=request_data)
if response.get("code") != 200:
raise Exception(f"Failed to update contract: {response.get('message', 'Unknown error')}")
return response
async def api_delete_contract(client: ApiClient, data_id: str, push_id: str) -> Dict[str, Any]:
"""DELETE /contract/delete - 删除合同(同步)"""
request_data = {"push_id": push_id, "biz_id": data_id}
response = await client.request("DELETE", "/contract/delete", data=request_data)
if response.get("code") != 200:
raise Exception(f"Failed to delete contract: {response.get('message', 'Unknown error')}")
return response
# ========== Handler ==========
class ContractApiHandler(BaseApiHandler):
"""合同 API Handler"""
def __init__(self):
super().__init__()
self._node_type = "contract"
self._node_class = ContractSyncNode
self._schema = ContractResponse
self.update_schemas = [ContractCreate, ContractUpdate]
# ========== Handler 接口实现 ==========
async def load(self) -> List[SyncNode]:
"""
加载合同列表
依赖 Project:从 collection 获取项目列表,然后加载这些项目的合同
Returns:
合同数据列表
"""
try:
# 从 collection 获取项目列表
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 contract load")
return []
# 为每个项目加载合同
all_nodes = []
for project_id in project_ids:
try:
contracts = await api_get_contract_list(self.api_client, project_id)
# 已验证的 Pydantic model,转换为 dict 并创建 node
for contract in contracts:
contract_data = contract.model_dump()
all_nodes.append(self._create_node(contract_data))
except Exception as e:
print(f"❌ Failed to load contracts for project {project_id}: {e}")
continue
return all_nodes
except Exception as e:
print(f"❌ Failed to load contracts: {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]:
"""批量更新合同"""
results = []
for node in nodes:
result = await self._update_single(node)
results.append(result)
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 poll_tasks(self, task_ids: List[str]) -> Dict[str, TaskResult]:
"""轮询异步任务状态"""
return await super().poll_tasks(task_ids)
async def _create_single(self, node: SyncNode) -> TaskResult:
"""
创建合同(异步)
Args:
node: 同步节点
Returns:
TaskResult (IN_PROGRESS with push_id)
"""
push_id: str | None = None
try:
# 获取数据
data = node.get_data()
if not data:
return TaskResult.failed(node_id=node.node_id, error="Node data is None or validation failed")
# 从 context 获取 project_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="project_id not found in context or data"
)
# 构建请求数据(确保包含 project_id)
request_data = {
"project_id": project_id,
**data
}
# 调用 API(直接调用顶层函数)
push_id = self._generate_push_id()
response = await api_create_contract(
self.api_client,
request_data,
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)}",
push_id=push_id,
)
async def _update_single(self, node: SyncNode) -> TaskResult:
"""
更新合同(同步)
自动选择需要更新的字段(基于 update_schema
Args:
node: 同步节点
Returns:
TaskResult (SUCCESS or FAILED)
"""
try:
data = node.get_data()
if not data:
return TaskResult.failed(node_id=node.node_id, error="Missing data for update")
# 过滤出需要更新的字段(排除空值、未变化的字段)
origin_data = node.get_origin_data() or {}
force_steps_update = self._steps_changed(node)
update_data = self.build_update_request_data(
new_data=data,
original_data=origin_data,
schema=ContractUpdate,
force_full_schema=force_steps_update,
)
# 如果没有需要更新的字段,降级 action 并返回跳过
if not update_data:
return TaskResult.skipped(node_id=node.node_id, reason="No updatable fields changed")
# Schema 验证
error = self._validate_schema(update_data, ContractUpdate)
if error:
return TaskResult.failed(
node_id=node.node_id,
error=f"Schema validation failed: {error}"
)
# 生成 push_id 和 biz_id
push_id = self._generate_push_id()
biz_id = node.data_id or update_data.get("id")
if not biz_id:
return TaskResult.failed(node_id=node.node_id, error="Missing biz_id for update")
# 调用 API
response = await api_update_contract(
self.api_client,
biz_id,
update_data,
push_id,
steps=self._get_request_steps(node),
)
# 检查响应状态(更新接口是同步的)
if response.get("code") == 200:
self._mark_approval_snapshot_synced(node)
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"Update failed: {str(e)}"
)
async def _delete_single(self, node: SyncNode) -> TaskResult:
"""
删除合同(同步)
Args:
node: 同步节点
Returns:
TaskResult (SUCCESS or FAILED)
"""
if not self.api_client:
return TaskResult.failed(
node_id=node.node_id,
error="api_client not initialized"
)
try:
# 生成 push_id 和 biz_id
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")
# 调用 API
response = await api_delete_contract(self.api_client, biz_id, push_id)
# 检查响应状态
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 到 context"""
node = self._create_basic_node(data, depend_ids=[])
# 设置 project_id 到 context(供子节点使用)
if data.get("project_id"):
node.context["project_id"] = data["project_id"]
return node