整理配置和代码入口,增加部分测试。
This commit is contained in:
@@ -188,7 +188,7 @@ class MaterialDetailApiHandler(BaseApiHandler):
|
||||
)
|
||||
# 整组成功
|
||||
for node, _ in group_items:
|
||||
results.append(TaskResult.success(node_id=node.node_id))
|
||||
results.append(TaskResult.in_progress(node_id=node.node_id, task_id=push_id))
|
||||
except Exception as e:
|
||||
# 整组失败
|
||||
error_msg = str(e)
|
||||
|
||||
@@ -18,6 +18,7 @@ PUT (6个更新接口):
|
||||
不支持: CREATE, DELETE
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import List, Dict, Any
|
||||
from ...datasource.api.handler import BaseApiHandler
|
||||
from ...datasource.task_result import TaskResult
|
||||
@@ -35,6 +36,35 @@ from schemas.project.project_base import (
|
||||
from schemas.project.project import ProjectInfoResponse
|
||||
|
||||
|
||||
_PROJECT_EXTENSION_FIELD_NAMES = {
|
||||
"implementation_estimate",
|
||||
"static_total_investment",
|
||||
"completed_investment",
|
||||
"total_contract_quantity",
|
||||
"completed_settlement_quantity",
|
||||
}
|
||||
|
||||
|
||||
def _merge_project_all_info_fields(
|
||||
project_data: Dict[str, Any],
|
||||
all_info: Dict[str, Any],
|
||||
) -> Dict[str, Any]:
|
||||
"""把 all_info 中项目扩展字段拍平到 project 主数据上。"""
|
||||
merged = dict(project_data)
|
||||
|
||||
for key, value in all_info.items():
|
||||
if not key.endswith("_extension") or not isinstance(value, dict):
|
||||
continue
|
||||
for field_name in _PROJECT_EXTENSION_FIELD_NAMES:
|
||||
if field_name not in value:
|
||||
continue
|
||||
current_value = merged.get(field_name)
|
||||
if current_value in (None, ""):
|
||||
merged[field_name] = value.get(field_name)
|
||||
|
||||
return merged
|
||||
|
||||
|
||||
class ProjectApiHandler(BaseApiHandler):
|
||||
"""项目 API Handler"""
|
||||
|
||||
@@ -136,12 +166,15 @@ class ProjectApiHandler(BaseApiHandler):
|
||||
|
||||
# 处理所有项目
|
||||
for project_data in projects:
|
||||
node = self._create_node(project_data.model_dump())
|
||||
# 设置 context
|
||||
node.context["project_id"] = project_data.id
|
||||
|
||||
# 预加载并缓存 all_info,同时存储到 node.context
|
||||
all_info = await self.get_all_info(self.api_client, project_data.id)
|
||||
normalized_project_data = _merge_project_all_info_fields(
|
||||
project_data.model_dump(),
|
||||
all_info,
|
||||
)
|
||||
node = self._create_node(normalized_project_data)
|
||||
# 设置 context
|
||||
node.context["project_id"] = project_data.id
|
||||
node.context["all_info"] = all_info
|
||||
|
||||
nodes.append(node)
|
||||
@@ -217,10 +250,12 @@ class ProjectApiHandler(BaseApiHandler):
|
||||
|
||||
# 执行更新(差异已由 _get_schema_diff 打印)
|
||||
try:
|
||||
push_id = self._generate_push_id()
|
||||
# project 需要使用 _filter_update_data 返回的完整 schema 数据
|
||||
full_update_data = self._filter_update_data(data, original_data, schema)
|
||||
await api_func(self.api_client, full_update_data, push_id, biz_id)
|
||||
await self._submit_update_and_wait(
|
||||
api_func=api_func,
|
||||
data=full_update_data,
|
||||
biz_id=biz_id,
|
||||
)
|
||||
node_updated = True
|
||||
except Exception as e:
|
||||
error = str(e)
|
||||
@@ -249,13 +284,39 @@ class ProjectApiHandler(BaseApiHandler):
|
||||
async def poll_tasks(self, task_ids: List[str]) -> Dict[str, TaskResult]:
|
||||
"""轮询异步任务状态"""
|
||||
return await super().poll_tasks(task_ids)
|
||||
|
||||
async def _submit_update_and_wait(self, *, api_func, data: Dict[str, Any], biz_id: str) -> None:
|
||||
push_id = self._generate_push_id()
|
||||
await api_func(self.api_client, data, push_id, biz_id)
|
||||
|
||||
max_attempts = 20
|
||||
poll_interval = 0.2
|
||||
last_result: TaskResult | None = None
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
polled = await super().poll_tasks([push_id])
|
||||
result = polled.get(push_id)
|
||||
last_result = result
|
||||
|
||||
if result is None:
|
||||
raise RuntimeError(f"Project update poll missing result: push_id={push_id}")
|
||||
if result.status == result.status.SUCCESS:
|
||||
return
|
||||
if result.status == result.status.FAILED:
|
||||
raise RuntimeError(result.error or f"Project update failed: push_id={push_id}")
|
||||
if attempt < max_attempts - 1:
|
||||
await asyncio.sleep(poll_interval)
|
||||
|
||||
raise RuntimeError(
|
||||
f"Project update poll timeout: push_id={push_id}, last_status={getattr(last_result, 'status', None)}"
|
||||
)
|
||||
|
||||
def _create_node(self, data: Dict[str, Any]) -> SyncNode:
|
||||
node = self._create_basic_node(data, depend_ids=[])
|
||||
|
||||
|
||||
# 设置 context
|
||||
node.context["project_id"] = node.data_id
|
||||
|
||||
|
||||
return node
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Project domain registration"""
|
||||
from ...common.registry import DomainRegistry
|
||||
from schemas.project.project_base import ProjectResponseBase
|
||||
from .sync_node import ProjectSyncNode
|
||||
from .sync_node import ProjectSyncNode, ProjectSyncSchema
|
||||
from .sync_strategy import ProjectSyncStrategy
|
||||
from .jsonl_handler import ProjectJsonlHandler
|
||||
from .api_handler import ProjectApiHandler
|
||||
@@ -9,7 +8,7 @@ from .api_handler import ProjectApiHandler
|
||||
# 注册 project domain
|
||||
DomainRegistry.register(
|
||||
node_type="project",
|
||||
schema=ProjectResponseBase,
|
||||
schema=ProjectSyncSchema,
|
||||
node_class=ProjectSyncNode,
|
||||
strategy_class=ProjectSyncStrategy,
|
||||
jsonl_handler_class=ProjectJsonlHandler,
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
from pydantic import Field
|
||||
|
||||
from ...common.sync_node import SyncNode
|
||||
from schemas.project.project_base import ProjectResponseBase
|
||||
|
||||
|
||||
class ProjectSyncNode(SyncNode[ProjectResponseBase]):
|
||||
class ProjectSyncSchema(ProjectResponseBase):
|
||||
"""项目同步内部 schema,补充 all_info 扩展字段。"""
|
||||
|
||||
implementation_estimate: float | None = Field(None, description="执行概算")
|
||||
static_total_investment: float | None = Field(None, description="静态总投资")
|
||||
completed_investment: float | None = Field(None, description="已完成投资")
|
||||
total_contract_quantity: float | None = Field(None, description="合同总量")
|
||||
completed_settlement_quantity: float | None = Field(None, description="已结算量")
|
||||
|
||||
|
||||
class ProjectSyncNode(SyncNode[ProjectSyncSchema]):
|
||||
"""项目同步节点"""
|
||||
node_type = "project"
|
||||
schema = ProjectResponseBase
|
||||
schema = ProjectSyncSchema
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@@ -17,6 +17,7 @@ PUT:
|
||||
不支持: CREATE, DELETE
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import List, Dict, Any, Optional, Tuple, Set
|
||||
from pydantic import ValidationError
|
||||
|
||||
@@ -270,6 +271,8 @@ class ProjectDetailApiHandler(BaseApiHandler):
|
||||
|
||||
try:
|
||||
resolved_data_id = self._resolve_data_id_from_all_info(all_info, project_id=project_id, key=key)
|
||||
if not resolved_data_id:
|
||||
resolved_data_id = await self._resolve_data_id_with_retry(project_id=project_id, key=key)
|
||||
except Exception as exc:
|
||||
self._pending_push_context.pop(task_id, None)
|
||||
results[task_id] = TaskResult.failed(error=f"Poll resolve data_id failed: {exc}", push_id=task_id)
|
||||
@@ -342,6 +345,9 @@ class ProjectDetailApiHandler(BaseApiHandler):
|
||||
if isinstance(all_info, dict):
|
||||
return all_info
|
||||
|
||||
if force_refresh:
|
||||
ProjectApiHandler._all_info_cache.pop(project_id, None)
|
||||
|
||||
all_info = await ProjectApiHandler.get_all_info(self.api_client, project_id)
|
||||
if project_node is not None:
|
||||
project_node.context["all_info"] = all_info
|
||||
@@ -370,6 +376,20 @@ class ProjectDetailApiHandler(BaseApiHandler):
|
||||
return matched[0]
|
||||
return None
|
||||
|
||||
async def _resolve_data_id_with_retry(self, *, project_id: str, key: str) -> Optional[str]:
|
||||
attempts = 6
|
||||
delay = 0.2
|
||||
|
||||
for attempt in range(attempts):
|
||||
all_info = await self._get_project_all_info(project_id, force_refresh=True)
|
||||
resolved = self._resolve_data_id_from_all_info(all_info, project_id=project_id, key=key)
|
||||
if resolved:
|
||||
return resolved
|
||||
if attempt < attempts - 1:
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
return None
|
||||
|
||||
async def _resolve_data_id_from_project_all_info(self, project_id: str, key: str) -> Optional[str]:
|
||||
all_info = await self._get_project_all_info(project_id)
|
||||
return self._resolve_data_id_from_all_info(all_info, project_id=project_id, key=key)
|
||||
|
||||
Reference in New Issue
Block a user