整理update逻辑

This commit is contained in:
strepsiades
2026-03-31 08:48:34 +08:00
parent bdc7bd5069
commit 14c20a6123
5 changed files with 210 additions and 2 deletions
+1 -1
View File
@@ -118,7 +118,7 @@ class LarApiHandler(BaseApiHandler):
for schema, api_func in update_configs:
# 获取过滤后的更新数据
update_data = self.build_update_request_data(
update_data = self.build_update_request_data(
node_data,
origin_data,
schema,
@@ -1,6 +1,6 @@
from ...sync_system.strategy import DefaultSyncStrategy
from ...sync_system.config import StrategyConfig, OrphanAction, UpdateDirection
from schemas.project_extensions.project_detail import ProjectDetailProject
from schemas.project_extensions.project_detail import ProjectDetailKey, ProjectDetailProject
class ProjectDetailSyncStrategy(DefaultSyncStrategy[ProjectDetailProject]):
@@ -28,3 +28,51 @@ class ProjectDetailSyncStrategy(DefaultSyncStrategy[ProjectDetailProject]):
"challenge_production": UpdateDirection.PULL,
},
)
def preprocess_compare_payload(self, payload: dict) -> dict:
normalized = dict(payload or {})
key = normalized.get("key")
if isinstance(key, ProjectDetailKey):
key = key.value
normalized["key"] = key
if isinstance(key, str):
normalized["value"] = self._normalize_detail_value_for_compare(key, normalized.get("value"))
return normalized
@staticmethod
def _normalize_detail_value_for_compare(key: str, value: object) -> list[dict[str, object]]:
if not isinstance(value, list):
return []
items: list[dict[str, object]] = []
for item in value:
if not isinstance(item, dict):
continue
normalized_item: dict[str, object] = {}
if "capacity" in item:
normalized_item["capacity"] = item.get("capacity")
if key in {"plan_construction", "actual_construction", "production"}:
date_value = item.get("date")
if date_value in (None, ""):
continue
normalized_item["date"] = date_value
elif key in {"ensure_production", "climb_production", "challenge_production"}:
year_value = item.get("year")
if year_value in (None, ""):
date_value = item.get("date")
if isinstance(date_value, str) and len(date_value) >= 4 and date_value[:4].isdigit():
year_value = int(date_value[:4])
if year_value in (None, ""):
continue
normalized_item["year"] = year_value
else:
if item.get("year") not in (None, ""):
normalized_item["year"] = item.get("year")
if item.get("date") not in (None, ""):
normalized_item["date"] = item.get("date")
items.append(normalized_item)
return items
@@ -0,0 +1,59 @@
from __future__ import annotations
from typing import Any
import pytest
from sync_state_machine.datasource.task_result import TaskStatus
from sync_state_machine.domain.contract.api_handler import ContractApiHandler
from sync_state_machine.domain.contract.sync_node import ContractSyncNode
class _MockApiClient:
def __init__(self) -> None:
self.requests: list[dict[str, Any]] = []
async def request(self, method: str, endpoint: str, **kwargs: Any) -> dict[str, Any]:
self.requests.append({"method": method, "endpoint": endpoint, **kwargs})
return {"code": 200, "message": "OK", "data": {"biz_id": "contract-created-1"}}
@pytest.mark.asyncio
async def test_contract_create_passes_name_and_qt_code_to_api() -> None:
handler = ContractApiHandler()
handler.api_client = _MockApiClient()
handler.set_poll_mode("sync")
node = ContractSyncNode(
node_id="contract-node-1",
data={
"id": "contract-local-1",
"project_id": "project-1",
"name": "电缆采购合同",
"short_name": "电缆采购合同",
"code": "QT",
"contract_type": "product",
"currency": "CNY",
"manager_name": "张三",
"manager_phone": "13800000000",
"sign_company": "测试供应商",
"company_id": "company-1",
"sign_date": "2026-03-01",
"credit_code": "91350000123456789X",
"total_price": 1200.5,
"sub_name": None,
},
context={"project_id": "project-1"},
)
results = await handler.create_all([node])
assert len(results) == 1
assert results[0].status == TaskStatus.SUCCESS
assert results[0].data_id == "contract-created-1"
request = handler.api_client.requests[0]
assert request["endpoint"] == "/contract/create"
assert request["data"]["data"]["name"] == "电缆采购合同"
assert request["data"]["data"]["code"] == "QT"
assert request["data"]["data"]["project_id"] == "project-1"
@@ -0,0 +1,57 @@
from __future__ import annotations
import pytest
from sync_state_machine.common.collection import DataCollection
from sync_state_machine.domain.project.api_handler import ProjectApiHandler
class _FakeProjectResponse:
def __init__(self, project_id: str):
self.id = project_id
def model_dump(self) -> dict[str, str]:
return {"id": self.id, "name": f"project-{self.id}"}
@pytest.mark.asyncio
async def test_project_handler_purge_project_data_requires_data_id_filter() -> None:
handler = ProjectApiHandler()
handler.set_handler_config({"purge_project_data": True})
with pytest.raises(ValueError, match="requires non-empty data_id_filter"):
await handler.load()
@pytest.mark.asyncio
async def test_project_handler_purge_project_data_calls_remote_endpoint(monkeypatch: pytest.MonkeyPatch) -> None:
handler = ProjectApiHandler()
handler.set_handler_config(
{
"purge_project_data": True,
"data_id_filter": ["project-1"],
}
)
handler.api_client = object()
handler.set_collection(DataCollection("remote"))
purged: list[str] = []
async def fake_purge(client, project_id: str):
purged.append(project_id)
return {"project_id": project_id}
async def fake_get_project(client, project_id: str):
return _FakeProjectResponse(project_id)
async def fake_get_all_info(cls, client, project_id: str):
return {}
monkeypatch.setattr("sync_state_machine.domain.project.api_handler.api_purge_project_data", fake_purge)
monkeypatch.setattr("sync_state_machine.domain.project.api_handler.api_get_project", fake_get_project)
monkeypatch.setattr(ProjectApiHandler, "get_all_info", classmethod(fake_get_all_info))
nodes = await handler.load()
assert purged == ["project-1"]
assert [node.data_id for node in nodes] == ["project-1"]
+44
View File
@@ -4,6 +4,8 @@ from sync_state_machine.domain.company.sync_strategy import CompanySyncStrategy
from sync_state_machine.sync_system.config import UpdateDirection
from sync_state_machine.sync_system.strategy_ops.update_ops import run_update
from sync_state_machine.domain.construction.sync_strategy import ConstructionTaskSyncStrategy
from sync_state_machine.domain.project_detail.sync_strategy import ProjectDetailSyncStrategy
from schemas.project_extensions.project_detail import ProjectDetailKey
@pytest.mark.asyncio
async def test_run_update_with_direction_overrides():
@@ -107,3 +109,45 @@ def test_default_strategy_exposes_normalize_compare_payload():
}
assert strategy.normalize_compare_payload(local_payload) == strategy.normalize_compare_payload(remote_payload)
def test_project_detail_compare_payload_ignores_response_only_year_and_is_audit_noise():
strategy = ProjectDetailSyncStrategy(
"project_detail_data",
MagicMock(),
MagicMock(),
MagicMock(),
)
local_payload = {
"project_id": "project-1",
"key": "actual_construction",
"value": [{"date": "2023-08-23", "capacity": 4.0, "is_audit": False, "year": None}],
}
remote_payload = {
"project_id": "project-1",
"key": "actual_construction",
"value": [{"date": "2023-08-23", "capacity": 4.0, "is_audit": True, "year": 2023}],
}
assert strategy.normalize_compare_payload(local_payload) == strategy.normalize_compare_payload(remote_payload)
def test_project_detail_compare_payload_normalizes_enum_key_values():
strategy = ProjectDetailSyncStrategy(
"project_detail_data",
MagicMock(),
MagicMock(),
MagicMock(),
)
remote_payload = {
"project_id": "project-1",
"key": ProjectDetailKey.PLAN_CONSTRUCTION,
"value": [{"date": "2023-06-28", "capacity": 4.0, "year": 2023, "is_audit": True}],
}
normalized = strategy.normalize_compare_payload(remote_payload)
assert normalized["key"] == "plan_construction"
assert normalized["value"] == [{"date": "2023-06-28", "capacity": 4.0}]