Files
ecm_sync_system/sync_state_machine/config/strategy_config.py
T
2026-04-01 11:52:57 +08:00

181 lines
6.8 KiB
Python

from __future__ import annotations
import logging
from enum import Enum
from typing import Any, Dict, List, Mapping, Type
from pydantic import BaseModel, ConfigDict, Field
def resolve_domain_option_config(
domain_option_model: Type[BaseModel] | None,
domain_option: Mapping[str, Any] | None,
) -> Dict[str, Any]:
if domain_option_model is None:
return dict(domain_option or {})
return domain_option_model.model_validate(domain_option or {}).model_dump()
class OrphanAction(str, Enum):
CREATE_REMOTE = "create_remote"
CREATE_LOCAL = "create_local"
DELETE_LOCAL = "delete_local"
DELETE_REMOTE = "delete_remote"
NONE = "none"
class UpdateDirection(str, Enum):
PUSH = "push"
PULL = "pull"
NONE = "none"
class PreBindDataIdPair(BaseModel):
model_config = ConfigDict(validate_assignment=True, extra="forbid")
local_data_id: str
remote_data_id: str
class StrategyConfig(BaseModel):
model_config = ConfigDict(validate_assignment=True, extra="forbid")
auto_bind: bool = True
auto_bind_fields: List[str] = Field(default_factory=list)
pre_bind_data_id: List[PreBindDataIdPair] = Field(default_factory=list)
depend_fields: Dict[str, str] = Field(default_factory=dict)
local_orphan_action: OrphanAction = OrphanAction.NONE
remote_orphan_action: OrphanAction = OrphanAction.NONE
update_direction: UpdateDirection = UpdateDirection.PUSH
# post-check 一致性比较时忽略的顶层字段。
# 只影响最终一致性评估,不影响 create/update 的差异检测。
post_check_ignore_fields: List[str] = Field(default_factory=list)
# post-check 一致性比较时,忽略列表型字段内各元素的特定子键。
# 格式: {字段名: [子键, ...]},如 {"plan_node": ["is_audit"]}。
# 用于排除后端硬写死、永远与本地不一致的字段,避免误报。
post_check_ignore_list_item_fields: Dict[str, List[str]] = Field(default_factory=dict)
# schema diff / validate 时忽略的顶层字段。
compare_ignore_fields: List[str] = Field(default_factory=list)
# 当 auto-bind 命中“多对一”候选时,允许稳定选择一条进行绑定,剩余候选按 orphan/create 流程继续处理。
# 默认关闭,避免在一般场景下引入隐式匹配。
allow_many_to_one_auto_bind: bool = False
# 是否跳过该类型的 create/update 阶段。
skip_sync: bool = False
# 是否跳过该类型的 post-check reload 与一致性校验。
skip_post_check: bool = False
# 领域策略运行参数,由各 strategy 的 domain_option_model 负责解释与校验。
domain_option: Dict[str, Any] = Field(default_factory=dict)
def log_logic_warnings(self, *, node_type: str, logger: logging.Logger) -> None:
direction = self.update_direction
local_action = self.local_orphan_action
remote_action = self.remote_orphan_action
if direction == UpdateDirection.PUSH:
if local_action == OrphanAction.CREATE_LOCAL:
logger.warning(
f"[{node_type}] Config inconsistency: "
f"update_direction=PUSH but local_orphan_action=CREATE_LOCAL (本地创建)。"
f"应使用 CREATE_REMOTE (推送到远程)"
)
if remote_action == OrphanAction.CREATE_LOCAL:
logger.warning(
f"[{node_type}] Config inconsistency: "
f"update_direction=PUSH but remote_orphan_action=CREATE_LOCAL (本地创建)。"
f"PUSH 模式应设为 NONE 或 DELETE_REMOTE"
)
elif direction == UpdateDirection.PULL:
if local_action == OrphanAction.CREATE_REMOTE:
logger.warning(
f"[{node_type}] Config inconsistency: "
f"update_direction=PULL but local_orphan_action=CREATE_REMOTE (推送到远程)。"
f"PULL 模式应设为 NONE 或 DELETE_LOCAL"
)
if remote_action in {OrphanAction.CREATE_REMOTE, OrphanAction.DELETE_LOCAL}:
logger.warning(
f"[{node_type}] Config inconsistency: "
f"update_direction=PULL but remote_orphan_action={remote_action}。"
f"PULL 模式建议使用 CREATE_LOCAL 或 NONE"
)
elif direction == UpdateDirection.NONE:
if local_action != OrphanAction.NONE:
logger.warning(
f"[{node_type}] Config inconsistency: "
f"update_direction=NONE but local_orphan_action={local_action}。"
f"应设为 NONE"
)
if remote_action != OrphanAction.NONE:
logger.warning(
f"[{node_type}] Config inconsistency: "
f"update_direction=NONE but remote_orphan_action={remote_action}。"
f"应设为 NONE"
)
class StrategyRuntimeConfig(BaseModel):
"""Pipeline 层策略运行配置(按 node_type)。"""
model_config = ConfigDict(validate_assignment=True, extra="forbid")
node_type: str
config: StrategyConfig = Field(default_factory=StrategyConfig)
class ConfigPresets:
@staticmethod
def first_sync() -> StrategyConfig:
return StrategyConfig(
local_orphan_action=OrphanAction.CREATE_REMOTE,
remote_orphan_action=OrphanAction.CREATE_LOCAL,
update_direction=UpdateDirection.PUSH,
)
@staticmethod
def daily_sync() -> StrategyConfig:
return ConfigPresets.push_only()
@staticmethod
def repair_sync() -> StrategyConfig:
return StrategyConfig(
local_orphan_action=OrphanAction.NONE,
remote_orphan_action=OrphanAction.NONE,
update_direction=UpdateDirection.NONE,
)
@staticmethod
def pull_only() -> StrategyConfig:
return StrategyConfig(
local_orphan_action=OrphanAction.NONE,
remote_orphan_action=OrphanAction.CREATE_LOCAL,
update_direction=UpdateDirection.PULL,
)
@staticmethod
def push_only() -> StrategyConfig:
return StrategyConfig(
local_orphan_action=OrphanAction.CREATE_REMOTE,
remote_orphan_action=OrphanAction.NONE,
update_direction=UpdateDirection.PUSH,
)
@staticmethod
def get_preset(name: str) -> StrategyConfig:
presets = {
"first_sync": ConfigPresets.first_sync,
"daily_sync": ConfigPresets.daily_sync,
"repair_sync": ConfigPresets.repair_sync,
"pull_only": ConfigPresets.pull_only,
"push_only": ConfigPresets.push_only,
}
if name not in presets:
raise ValueError(f"Unknown preset: {name}. Available: {list(presets.keys())}")
return presets[name]()