first commit
This commit is contained in:
@@ -0,0 +1,358 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user