Files
ecm_sync_system/sync_state_machine/sync_system/resolve_ids.py
T
strepsiades 4a9c444b10 first commit
2026-03-09 16:31:42 +08:00

359 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
ID 解析模块:处理本地 data_id 到远程 data_id 的转换
"""
from typing import TYPE_CHECKING, Optional, List, Dict, Any, cast
from ..common.collection import DataCollection
from ..common.binding import BindingManager
if TYPE_CHECKING:
from ..common.sync_node import SyncNode
class IDResolver:
"""
ID 解析器:将本地业务 ID 转换为远程业务 ID
用于自动绑定场景,解决跨系统的 ID 引用问题。
例如:本地 Contract 的 project_id="P1" 需要转换为远程的 project_id="RP_99"
"""
def __init__(
self,
local_collection: DataCollection,
remote_collection: DataCollection,
binding_manager: BindingManager
):
self.local_collection = local_collection
self.remote_collection = remote_collection
self.binding_manager = binding_manager
async def resolve_id(
self,
source_data_id: str,
node_type_hint: Optional[str] = None,
to_remote: bool = True
) -> Optional[str]:
"""
将源系统的 data_id 转换为目标系统的 data_id。
Args:
source_data_id: 源系统的业务 ID
node_type_hint: 节点类型提示
to_remote: True 表示从本地转换到远程,False 表示从远程转换到本地
Returns:
目标系统的业务 ID,如果转换失败则返回 None
"""
if to_remote:
# 1. 查找源节点(本地)
src_node = self._find_node_by_data_id(
self.local_collection,
source_data_id,
node_type_hint
)
if not src_node:
return None
# 2. 查找目标 node_id (远程)
target_node_id = await self.binding_manager.get_remote_id(
src_node.node_type,
src_node.node_id
)
if not target_node_id:
return None
# 3. 查找远程节点并提取 data_id
target_node = self.remote_collection.get(target_node_id)
if not target_node:
return None
return target_node.data_id
else:
# 1. 查找源节点(远程)
src_node = self._find_node_by_data_id(
self.remote_collection,
source_data_id,
node_type_hint
)
if not src_node:
return None
# 2. 查找目标 node_id (本地)
target_node_id = await self.binding_manager.get_local_id(
src_node.node_type,
src_node.node_id
)
if not target_node_id:
return None
# 3. 查找本地节点并提取 data_id
target_node = self.local_collection.get(target_node_id)
if not target_node:
return None
return target_node.data_id
async def resolve_ids(
self,
local_data_ids: List[str],
node_type_hints: Optional[List[Optional[str]]] = None
) -> List[Optional[str]]:
"""
批量转换本地 data_id 列表为远程 data_id 列表。
Args:
local_data_ids: 本地业务 ID 列表
node_type_hints: 节点类型提示列表(可选)
Returns:
远程业务 ID 列表,保持顺序,转换失败的位置为 None
"""
if node_type_hints is None:
node_type_hints = cast(List[Optional[str]], [None] * len(local_data_ids))
results: List[Optional[str]] = []
for local_id, hint in zip(local_data_ids, node_type_hints):
remote_id = await self.resolve_id(local_id, hint)
results.append(remote_id)
return results
def _find_node_by_data_id(
self,
collection: DataCollection,
data_id: str,
node_type_hint: Optional[str] = None
):
"""
在 collection 中查找指定 data_id 的节点。
如果提供了 node_type_hint,则只在该类型中查找。
否则通过 collection 内部的 data_id 索引进行 O(1) 查找。
Args:
collection: 数据集合
data_id: 业务数据 ID
node_type_hint: 节点类型提示
Returns:
找到的节点,或 None
"""
if node_type_hint:
# 有类型提示,直接使用高效查找
return collection.get_by_data_id(node_type_hint, data_id)
return collection.get_by_data_id_any(data_id)
async def resolve_business_key_ids(
self,
business_key_fields: List[str],
local_data: Dict[str, Any],
node_type_hints: Optional[Dict[str, str]] = None
) -> Dict[str, Optional[str]]:
"""
解析业务键中的所有 ID 字段,转换为远程 ID。
识别规则:
- 字段名以 "_id" 结尾
- 字段名为 "id"
Args:
business_key_fields: 业务键字段列表
local_data: 本地数据对象
node_type_hints: ID 字段到 node_type 的映射(可选)
Returns:
字段名到远程 ID 的映射,转换失败的为 None
"""
if node_type_hints is None:
node_type_hints = {}
resolved = {}
for field_name in business_key_fields:
# 判断是否是 ID 字段
is_id_field = field_name.endswith("_id") or field_name == "id"
if not is_id_field:
# 非 ID 字段,不需要转换
resolved[field_name] = local_data.get(field_name)
continue
# ID 字段,需要转换
local_id = local_data.get(field_name)
if local_id is None:
resolved[field_name] = None
continue
# 转换为远程 ID
hint = node_type_hints.get(field_name)
remote_id = await self.resolve_id(local_id, hint)
resolved[field_name] = remote_id
return resolved
async def replace_ids_in_data(
self,
node: "SyncNode",
depend_field_names: set[str],
node_type: str,
id_field_hints: Dict[str, str],
to_remote: bool = True
) -> None:
"""
替换节点data中的所有ID字段(源系统ID -> 目标系统ID)。
Args:
node: 待处理的节点
depend_field_names: 依赖字段名集合
node_type: 节点类型(用于日志)
id_field_hints: ID字段到node_type的提示映射
to_remote: 转换方向
"""
import logging
logger = logging.getLogger(__name__)
data_dict = node.get_data()
if not data_dict:
return
# 遍历所有字段
for field_name, value in list(data_dict.items()):
# 判断是否是ID字段
is_id_field = field_name.endswith("_id") or field_name == "id"
is_primary_id_field = field_name in ("id", "_id")
if not is_id_field:
continue
if value is None:
continue
if value == "":
continue
# 判断是否是依赖字段
is_depend_field = field_name in depend_field_names
# 获取node_type提示
hint = id_field_hints.get(field_name)
# 如果是主 ID 字段且没有提示,则使用当前节点类型作为提示
if is_primary_id_field and not hint:
hint = node_type
# 解析ID
target_id = await self.resolve_id(str(value), hint, to_remote=to_remote)
if target_id is None:
# to_remote=True表示从local到remote,所以node是local节点
# to_remote=False表示从remote到local,所以node是remote节点
collection_name = "local" if to_remote else "remote"
node_data_id = node.data_id or "N/A"
direction = "remote" if to_remote else "local"
if is_depend_field:
logger.warning(
f"[{node_type}] {collection_name.capitalize()} node {node.node_id} "
f"(data_id={node_data_id}): Dependency field '{field_name}' "
f"with value '{value}' failed to resolve to {direction} ID"
)
else:
if not is_primary_id_field:
logger.warning(
f"[{node_type}] {collection_name.capitalize()} node {node.node_id} "
f"(data_id={node_data_id}): Non-dependency ID field '{field_name}' "
f"with value '{value}' failed to resolve to {direction} ID, setting to empty"
)
data_dict[field_name] = ""
else:
# 成功解析,更新字段值
data_dict[field_name] = target_id
# 将修改后的数据写回节点
node.set_data(data_dict)
async def resolve_and_validate(
self,
data: Dict[str, Any],
node_type: str,
depend_fields: Dict[str, str],
to_remote: bool,
) -> tuple[Optional[Dict[str, Any]], Optional[str]]:
"""
ID 替换 + 严格验证(一站式方法)
用途:CREATE/UPDATE 时将源端的依赖 ID 转换为目标端 ID,并验证转换结果
流程:
1. 无依赖字段 → 直接返回原数据
2. 创建临时节点 → 调用 replace_ids_in_data 执行 ID 替换
3. 严格验证:检查所有依赖字段是否成功转换
验证逻辑(与 bind 阶段依赖检查的区别):
- bind 阶段:检查同端依赖(本地 material 的 contract_id 在本地是否存在)
- create 阶段:检查能否转换为目标端 ID(本地 contract_id 能否找到远程 ID
触发场景示例:
本地 material: {"contract_id": "local_c123"}
bind 检查:✅ 本地 contract(data_id=local_c123) 存在
create 转换:❌ 远程 contract 还没创建,找不到远程 ID
结果:虽然本地依赖满足,但无法推送到远程
Args:
data: 源数据字典
node_type: 节点类型
depend_fields: 依赖字段映射 {field_name: dep_node_type}
to_remote: True=本地→远程, False=远程→本地
Returns:
(resolved_data, None): 成功
(None, error_msg): 失败
"""
from ..common.registry import DomainRegistry
# 无依赖字段,直接返回原数据
if not depend_fields:
return data, None
depend_field_names = set(depend_fields.keys())
# id_field_hints 与 depend_fields 相同(field_name -> node_type
id_field_hints = dict(depend_fields)
# 创建临时节点用于 ID 替换
temp_node_class = DomainRegistry.get_node_class(node_type)
temp_node = temp_node_class(
node_id="temp",
data_id="",
data=data
)
# 执行 ID 替换
await self.replace_ids_in_data(
temp_node,
depend_field_names,
node_type,
id_field_hints,
to_remote=to_remote
)
resolved_data = temp_node.get_data()
if not resolved_data:
return None, "ID 解析失败(数据为空)"
# 严格验证:检查所有依赖字段是否成功转换
failed_fields = []
for field_name, dep_type in depend_fields.items():
original_value = data.get(field_name)
resolved_value = resolved_data.get(field_name)
# 原本有值(源端 ID),但解析后为空(找不到目标端 ID)
# 说明依赖节点虽然在源端存在,但在目标端还没就绪
if original_value and not resolved_value:
failed_fields.append(f"{field_name}({dep_type})={original_value}")
if failed_fields:
direction = "远程" if to_remote else "本地"
error_msg = f"依赖字段无法解析到{direction} ID: {', '.join(failed_fields)}"
return None, error_msg
return resolved_data, None