first commit

This commit is contained in:
strepsiades
2026-03-09 16:31:42 +08:00
commit 4a9c444b10
486 changed files with 52479 additions and 0 deletions
@@ -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