""" 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