优化sync_system代码质量
This commit is contained in:
@@ -19,7 +19,8 @@ class ConstructionTaskSyncStrategy(DefaultSyncStrategy[ConstructionResponse]):
|
||||
update_direction=UpdateDirection.PUSH,
|
||||
)
|
||||
|
||||
def preprocess_compare_payload(self, payload: dict) -> dict:
|
||||
def normalize_compare_payload(self, data: dict | None, data_id_map=None) -> dict:
|
||||
payload = super().normalize_compare_payload(data, data_id_map)
|
||||
plan_nodes = payload.get("plan_node")
|
||||
if not isinstance(plan_nodes, list):
|
||||
return payload
|
||||
|
||||
@@ -19,7 +19,8 @@ class ContractSettlementSyncStrategy(DefaultSyncStrategy[ContractSettlementRespo
|
||||
update_direction=UpdateDirection.PUSH,
|
||||
)
|
||||
|
||||
def preprocess_compare_payload(self, payload: dict) -> dict:
|
||||
def normalize_compare_payload(self, data: dict | None, data_id_map=None) -> dict:
|
||||
payload = super().normalize_compare_payload(data, data_id_map)
|
||||
plan_nodes = payload.get("plan_node")
|
||||
if not isinstance(plan_nodes, list):
|
||||
return payload
|
||||
|
||||
@@ -19,7 +19,8 @@ class MaterialSyncStrategy(DefaultSyncStrategy[MaterialResponse]):
|
||||
update_direction=UpdateDirection.PUSH,
|
||||
)
|
||||
|
||||
def preprocess_compare_payload(self, payload: dict) -> dict:
|
||||
def normalize_compare_payload(self, data: dict | None, data_id_map=None) -> dict:
|
||||
payload = super().normalize_compare_payload(data, data_id_map)
|
||||
plan_nodes = payload.get("plan_node")
|
||||
if not isinstance(plan_nodes, list):
|
||||
return payload
|
||||
|
||||
@@ -20,16 +20,13 @@ class PreparationSyncStrategy(DefaultSyncStrategy[PreparationDetail]):
|
||||
update_direction=UpdateDirection.PUSH,
|
||||
)
|
||||
|
||||
def _needs_update(self, source_node, target_node, resolved_data=None, data_id_map=None) -> bool:
|
||||
if resolved_data is None:
|
||||
raise ValueError(f"[{self.node_type}] resolved_data is required for differential sync.")
|
||||
|
||||
target_data = target_node.get_data()
|
||||
def should_update_pair(self, source_node, target_node, *, source_data, target_data, data_id_map=None) -> bool:
|
||||
if target_data is None:
|
||||
return False
|
||||
|
||||
# 这里只按 PostPreparation 可写字段判定是否需要 update,避免 response-only 字段差异触发空更新。
|
||||
update_fields = set(PostPreparation.model_fields.keys())
|
||||
source_payload = {key: value for key, value in resolved_data.items() if key in update_fields}
|
||||
source_payload = {key: value for key, value in source_data.items() if key in update_fields}
|
||||
target_payload = {key: value for key, value in target_data.items() if key in update_fields}
|
||||
|
||||
normalized_source = normalized_data_for_compare(source_payload, data_id_map or {})
|
||||
|
||||
@@ -20,16 +20,13 @@ class ProductionSyncStrategy(DefaultSyncStrategy[ProductionDetail]):
|
||||
update_direction=UpdateDirection.PUSH,
|
||||
)
|
||||
|
||||
def _needs_update(self, source_node, target_node, resolved_data=None, data_id_map=None) -> bool:
|
||||
if resolved_data is None:
|
||||
raise ValueError(f"[{self.node_type}] resolved_data is required for differential sync.")
|
||||
|
||||
target_data = target_node.get_data()
|
||||
def should_update_pair(self, source_node, target_node, *, source_data, target_data, data_id_map=None) -> bool:
|
||||
if target_data is None:
|
||||
return False
|
||||
|
||||
# 这里只按 PostProduction 可写字段判定是否需要 update,避免 response-only 字段差异触发空更新。
|
||||
update_fields = set(PostProduction.model_fields.keys())
|
||||
source_payload = {key: value for key, value in resolved_data.items() if key in update_fields}
|
||||
source_payload = {key: value for key, value in source_data.items() if key in update_fields}
|
||||
target_payload = {key: value for key, value in target_data.items() if key in update_fields}
|
||||
|
||||
normalized_source = normalized_data_for_compare(source_payload, data_id_map or {})
|
||||
|
||||
@@ -107,6 +107,8 @@ class ProjectApiHandler(BaseApiHandler):
|
||||
project_id: 可选,指定项目ID列表则只加载这些项目(优先级高于构造函数的filter_project_id)
|
||||
"""
|
||||
try:
|
||||
self.ensure_api_client()
|
||||
self.ensure_collection()
|
||||
# 清空旧缓存
|
||||
self.clear_cache()
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@ class ProjectSyncSchema(ProjectResponseBase):
|
||||
completed_investment: float | None = Field(None, description="已完成投资")
|
||||
total_contract_quantity: float | None = Field(None, description="合同总量")
|
||||
completed_settlement_quantity: float | None = Field(None, description="已结算量")
|
||||
ic_plan_first_production_date: str | None = Field(None, description="集团计划首批投产日期")
|
||||
ic_plan_full_production_date: str | None = Field(None, description="集团计划全容量投产日期")
|
||||
|
||||
|
||||
class ProjectSyncNode(SyncNode[ProjectSyncSchema]):
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
from pydantic import BaseModel
|
||||
|
||||
from ...sync_system.strategy import DefaultSyncStrategy
|
||||
from ...sync_system.config import StrategyConfig, OrphanAction, UpdateDirection
|
||||
from schemas.project.project_base import ProjectResponseBase
|
||||
|
||||
|
||||
class ProjectDomainOption(BaseModel):
|
||||
pull_group_plan_production_date: bool = False
|
||||
|
||||
|
||||
class ProjectSyncStrategy(DefaultSyncStrategy[ProjectResponseBase]):
|
||||
"""
|
||||
Project 同步策略
|
||||
@@ -17,6 +23,12 @@ class ProjectSyncStrategy(DefaultSyncStrategy[ProjectResponseBase]):
|
||||
|
||||
# 类变量:schema 固定为 ProjectResponseBase
|
||||
schema = ProjectResponseBase
|
||||
domain_option_model = ProjectDomainOption
|
||||
|
||||
_PULL_ONLY_FIELDS = [
|
||||
"ic_plan_first_production_date",
|
||||
"ic_plan_full_production_date",
|
||||
]
|
||||
|
||||
# 类变量:默认配置
|
||||
default_config = StrategyConfig(
|
||||
@@ -26,4 +38,60 @@ class ProjectSyncStrategy(DefaultSyncStrategy[ProjectResponseBase]):
|
||||
local_orphan_action=OrphanAction.CREATE_REMOTE,
|
||||
remote_orphan_action=OrphanAction.NONE,
|
||||
update_direction=UpdateDirection.PUSH,
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def domain_option(self) -> ProjectDomainOption:
|
||||
return ProjectDomainOption.model_validate(self.config.domain_option)
|
||||
|
||||
def get_node_update_payload(self, node):
|
||||
payload = dict(node.get_data() or {})
|
||||
context = getattr(node, "context", None)
|
||||
if not isinstance(context, dict):
|
||||
return payload
|
||||
|
||||
all_info = context.get("all_info")
|
||||
if not isinstance(all_info, dict):
|
||||
return payload
|
||||
|
||||
for section_name, section_data in all_info.items():
|
||||
if not section_name.endswith("_extension") or not isinstance(section_data, dict):
|
||||
continue
|
||||
for field_name in self._PULL_ONLY_FIELDS:
|
||||
if payload.get(field_name) not in (None, ""):
|
||||
continue
|
||||
if field_name in section_data:
|
||||
payload[field_name] = section_data.get(field_name)
|
||||
|
||||
return payload
|
||||
|
||||
async def update_pair(self, local_node, remote_node, data_id_map=None):
|
||||
updated_by_id = {}
|
||||
|
||||
if self.domain_option.pull_group_plan_production_date:
|
||||
pull_update = await self.prepare_update_for_direction(
|
||||
local_node,
|
||||
remote_node,
|
||||
UpdateDirection.PULL,
|
||||
data_id_map,
|
||||
include_fields=self._PULL_ONLY_FIELDS,
|
||||
)
|
||||
if pull_update is not None:
|
||||
updated_by_id[pull_update.node_id] = pull_update
|
||||
|
||||
generic_exclude_fields = self._PULL_ONLY_FIELDS
|
||||
else:
|
||||
generic_exclude_fields = None
|
||||
|
||||
if self.config.update_direction != UpdateDirection.NONE:
|
||||
generic_update = await self.prepare_update_for_direction(
|
||||
local_node,
|
||||
remote_node,
|
||||
self.config.update_direction,
|
||||
data_id_map,
|
||||
exclude_fields=generic_exclude_fields,
|
||||
)
|
||||
if generic_update is not None:
|
||||
updated_by_id[generic_update.node_id] = generic_update
|
||||
|
||||
return list(updated_by_id.values())
|
||||
@@ -1,8 +1,16 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from ...sync_system.strategy import DefaultSyncStrategy
|
||||
from ...sync_system.config import StrategyConfig, OrphanAction, UpdateDirection
|
||||
from ...sync_system.strategy_ops.compare_ops import build_data_id_normalization_map
|
||||
from ...sync_system.strategy_ops.update_ops import emit_schema_diff_report, _build_schema_diff_validator
|
||||
from schemas.project_extensions.project_detail import ProjectDetailKey, ProjectDetailProject
|
||||
|
||||
|
||||
class ProjectDetailDomainOption(BaseModel):
|
||||
pull_group_production_plans: bool = Field(True, description="固定将集团保供/爬坡/挑战产能计划按 pull 处理")
|
||||
|
||||
|
||||
class ProjectDetailSyncStrategy(DefaultSyncStrategy[ProjectDetailProject]):
|
||||
"""
|
||||
ProjectDetail 同步策略(项目容量信息)。
|
||||
@@ -13,6 +21,12 @@ class ProjectDetailSyncStrategy(DefaultSyncStrategy[ProjectDetailProject]):
|
||||
"""
|
||||
|
||||
schema = ProjectDetailProject
|
||||
domain_option_model = ProjectDetailDomainOption
|
||||
_GROUP_PRODUCTION_PLAN_KEYS = {
|
||||
"ensure_production",
|
||||
"climb_production",
|
||||
"challenge_production",
|
||||
}
|
||||
|
||||
default_config = StrategyConfig(
|
||||
auto_bind=True,
|
||||
@@ -21,16 +35,54 @@ class ProjectDetailSyncStrategy(DefaultSyncStrategy[ProjectDetailProject]):
|
||||
local_orphan_action=OrphanAction.NONE,
|
||||
remote_orphan_action=OrphanAction.NONE,
|
||||
update_direction=UpdateDirection.PUSH,
|
||||
field_direction_key="key",
|
||||
field_direction_overrides={
|
||||
"ensure_production": UpdateDirection.PULL,
|
||||
"climb_production": UpdateDirection.PULL,
|
||||
"challenge_production": UpdateDirection.PULL,
|
||||
},
|
||||
)
|
||||
|
||||
def preprocess_compare_payload(self, payload: dict) -> dict:
|
||||
normalized = dict(payload or {})
|
||||
async def update(self):
|
||||
self.ensure_runtime()
|
||||
validator = _build_schema_diff_validator(self)
|
||||
self._active_schema_diff_validator = validator
|
||||
data_id_map = await build_data_id_normalization_map(
|
||||
node_type=self.node_type,
|
||||
local_collection=self.local_collection,
|
||||
remote_collection=self.remote_collection,
|
||||
binding_manager=self.binding_manager,
|
||||
)
|
||||
|
||||
pull_pairs = []
|
||||
generic_pairs = []
|
||||
for pair in await self.collect_update_pairs():
|
||||
if self._should_pull_pair(pair.local_node, pair.remote_node):
|
||||
pull_pairs.append(pair)
|
||||
else:
|
||||
generic_pairs.append(pair)
|
||||
|
||||
updated_by_id = {}
|
||||
try:
|
||||
for pair in pull_pairs:
|
||||
updated_node = await self.prepare_update_for_direction(
|
||||
pair.local_node,
|
||||
pair.remote_node,
|
||||
UpdateDirection.PULL,
|
||||
data_id_map,
|
||||
)
|
||||
if updated_node is not None:
|
||||
updated_by_id[updated_node.node_id] = updated_node
|
||||
|
||||
for pair in generic_pairs:
|
||||
for updated_node in await self.update_pair(pair.local_node, pair.remote_node, data_id_map):
|
||||
updated_by_id[updated_node.node_id] = updated_node
|
||||
finally:
|
||||
self._active_schema_diff_validator = None
|
||||
|
||||
emit_schema_diff_report(self, validator)
|
||||
return list(updated_by_id.values())
|
||||
|
||||
@property
|
||||
def domain_option(self) -> ProjectDetailDomainOption:
|
||||
return ProjectDetailDomainOption.model_validate(self.config.domain_option)
|
||||
|
||||
def normalize_compare_payload(self, data: dict | None, data_id_map=None) -> dict:
|
||||
normalized = super().normalize_compare_payload(data, data_id_map)
|
||||
key = normalized.get("key")
|
||||
if isinstance(key, ProjectDetailKey):
|
||||
key = key.value
|
||||
@@ -76,3 +128,20 @@ class ProjectDetailSyncStrategy(DefaultSyncStrategy[ProjectDetailProject]):
|
||||
items.append(normalized_item)
|
||||
|
||||
return items
|
||||
|
||||
@staticmethod
|
||||
def _extract_pair_key(local_node, remote_node) -> str | None:
|
||||
local_data = local_node.get_data() or {}
|
||||
remote_data = remote_node.get_data() or {}
|
||||
|
||||
key = local_data.get("key", remote_data.get("key"))
|
||||
if isinstance(key, ProjectDetailKey):
|
||||
return key.value
|
||||
if isinstance(key, str):
|
||||
return key
|
||||
return None
|
||||
|
||||
def _should_pull_pair(self, local_node, remote_node) -> bool:
|
||||
if not self.domain_option.pull_group_production_plans:
|
||||
return False
|
||||
return self._extract_pair_key(local_node, remote_node) in self._GROUP_PRODUCTION_PLAN_KEYS
|
||||
|
||||
@@ -24,19 +24,16 @@ class SupplierSyncStrategy(DefaultSyncStrategy[SupplierDetailResponse]):
|
||||
update_direction=UpdateDirection.PULL,
|
||||
)
|
||||
|
||||
def _needs_update(
|
||||
def should_update_pair(
|
||||
self,
|
||||
source_node,
|
||||
target_node,
|
||||
resolved_data=None,
|
||||
*,
|
||||
source_data,
|
||||
target_data,
|
||||
data_id_map=None,
|
||||
) -> bool:
|
||||
"""Supplier 更新忽略 id 字段差异。"""
|
||||
if resolved_data is None:
|
||||
raise ValueError(f"[{self.node_type}] resolved_data is required for differential sync.")
|
||||
|
||||
source_data = resolved_data
|
||||
target_data = target_node.get_data()
|
||||
if source_data is None or target_data is None:
|
||||
return False
|
||||
|
||||
|
||||
Reference in New Issue
Block a user