整理配置和代码入口,增加部分测试。

This commit is contained in:
strepsiades
2026-03-10 12:22:54 +08:00
parent 515c3d9388
commit a638e4203b
35 changed files with 1314 additions and 199 deletions
@@ -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,
+14 -2
View File
@@ -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)