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,6 @@
"""Project domain"""
from .sync_node import ProjectSyncNode
from .sync_strategy import ProjectSyncStrategy
from .jsonl_handler import ProjectJsonlHandler
__all__ = ["ProjectSyncNode", "ProjectSyncStrategy", "ProjectJsonlHandler"]
@@ -0,0 +1,395 @@
"""
Project API Handler
实现的接口:
GET:
- /project/list - 获取项目列表
- /project - 获取项目详情
- /project/all_info - 获取项目完整信息
PUT (6个更新接口):
- /project - 更新项目基本信息
- /project/key_constraints - 关键节点
- /project/complete_investment - 完成投资
- /project/investment_decision - 投资决策
- /project/dynamic_investment - 动态投资
- /project/settlement_investment - 结算投资
不支持: CREATE, DELETE
"""
from typing import List, Dict, Any
from ...datasource.api.handler import BaseApiHandler
from ...datasource.task_result import TaskResult
from ...common.sync_node import SyncNode
from .sync_node import ProjectSyncNode
from schemas.project.project_base import (
ProjectResponseBase,
ProjectBaseInfoUpdate,
ProjectKeyConstraints,
ProjectCompleteInvestment,
ProjectInvestmentDecision,
ProjectDynamicInvestment,
ProjectSettlementInvestment
)
from schemas.project.project import ProjectInfoResponse
class ProjectApiHandler(BaseApiHandler):
"""项目 API Handler"""
# 类级别缓存:存储 all_info 数据
_all_info_cache: Dict[str, Dict[str, Any]] = {}
def __init__(self, filter_project_id: str | List[str] | None = None):
"""
初始化 Project Handler
Args:
filter_project_id: 指定只加载的项目ID列表(可选,用于多项目测试)
"""
super().__init__()
self._node_type = "project"
self._node_class = ProjectSyncNode
self._schema = ProjectResponseBase
if filter_project_id is None:
self._filter_project_id = None
elif isinstance(filter_project_id, str):
self._filter_project_id = [filter_project_id]
else:
self._filter_project_id = filter_project_id # 保存过滤条件
def set_target_project_ids(self, target_project_ids: List[str]) -> None:
self._filter_project_id = list(target_project_ids) if target_project_ids else None
@classmethod
async def get_all_info(cls, client, project_id: str) -> Dict[str, Any]:
"""获取项目完整信息(带缓存)
Args:
client: API client
project_id: 项目ID
Returns:
项目完整信息字典
"""
if project_id in cls._all_info_cache:
return cls._all_info_cache[project_id]
all_info = await api_get_project_all_info(client, project_id)
cls._all_info_cache[project_id] = all_info
return all_info
@classmethod
def clear_cache(cls):
"""清空缓存(每次同步开始前调用)"""
cls._all_info_cache.clear()
async def load(self) -> List[SyncNode]:
"""加载项目列表并预加载 all_info(支持分页)
Args:
project_id: 可选,指定项目ID列表则只加载这些项目(优先级高于构造函数的filter_project_id
"""
try:
# 清空旧缓存
self.clear_cache()
nodes = []
# 确定要加载的项目ID列表:参数 > 构造函数配置
target_project_ids = self._filter_project_id or []
if isinstance(target_project_ids, str):
target_project_ids = [target_project_ids]
if target_project_ids:
# 只加载指定项目列表
projects : List[ProjectResponseBase]= []
for project_id in target_project_ids:
project_response = await api_get_project(self.api_client, project_id)
projects.append(project_response)
else:
# 加载所有项目(分页)
projects : List[ProjectResponseBase]= []
page = 1
page_size = 100
while True:
# 获取一页数据
page_projects, total = await api_get_project_list(
self.api_client,
page=page,
page_size=page_size
)
# 没有数据了,退出循环
if not page_projects:
break
projects.extend(page_projects)
# 检查是否还有下一页
if page * page_size >= total:
break
page += 1
# 处理所有项目
for project_data in projects:
node = self._create_node(project_data.model_dump())
# 设置 context
node.context["project_id"] = project_data.id
# 预加载并缓存 all_info,同时存储到 node.context
all_info = await self.get_all_info(self.api_client, project_data.id)
node.context["all_info"] = all_info
nodes.append(node)
return nodes
except Exception as e:
# 打印详细错误信息并重新抛出,让上层处理
print(f"\n{'='*70}")
print("❌ 项目加载失败")
print(f"{'='*70}")
print(f"错误类型: {type(e).__name__}")
print(f"错误信息: {str(e)}")
print(f"{'='*70}")
import traceback
traceback.print_exc()
print(f"{'='*70}\n")
# 重新抛出异常,让DataSource能正确处理
raise
async def create_all(self, nodes: List[SyncNode]) -> List[TaskResult]:
"""不支持创建"""
return [
TaskResult.failed(node_id=node.node_id, error="Project creation not supported")
for node in nodes
]
async def update_all(self, nodes: List[SyncNode]) -> List[TaskResult]:
"""
批量更新项目(支持多种更新类型)
支持 6 种更新 schema
1. ProjectBaseInfoUpdate - 基础信息
2. ProjectKeyConstraints - 关键约束
3. ProjectCompleteInvestment - 竣工投资
4. ProjectInvestmentDecision - 投资决策
5. ProjectDynamicInvestment - 动态投资
6. ProjectSettlementInvestment - 结算投资
"""
results = []
# 定义所有更新 schema 和对应的 API 函数
update_configs = [
(ProjectBaseInfoUpdate, api_update_project_base_info),
(ProjectKeyConstraints, api_update_project_key_constraints),
(ProjectCompleteInvestment, api_update_project_complete_investment),
(ProjectInvestmentDecision, api_update_project_investment_decision),
(ProjectDynamicInvestment, api_update_project_dynamic_investment),
(ProjectSettlementInvestment, api_update_project_settlement_investment),
]
for node in nodes:
data = node.get_data()
if not data:
error_msg = node.error or "Node data is missing or invalid"
results.append(TaskResult.failed(node_id=node.node_id, error=f"Cannot update project: {error_msg}"))
continue
original_data = node.get_origin_data() or {}
biz_id = node.data_id
if not biz_id:
results.append(TaskResult.failed(node_id=node.node_id, error="Missing biz_id for update"))
continue
node_updated = False
error = None
# 遍历所有更新 schema,检查差异并更新
for schema, api_func in update_configs:
update_data = self._get_schema_diff(schema, data, original_data, node.node_id)
if not update_data:
continue
# 执行更新(差异已由 _get_schema_diff 打印)
try:
push_id = self._generate_push_id()
# project 需要使用 _filter_update_data 返回的完整 schema 数据
full_update_data = self._filter_update_data(data, original_data, schema)
await api_func(self.api_client, full_update_data, push_id, biz_id)
node_updated = True
except Exception as e:
error = str(e)
break
# 汇总结果
if error:
results.append(TaskResult.failed(node_id=node.node_id, error=error))
elif node_updated:
results.append(TaskResult.success(node_id=node.node_id))
else:
# 没有任何字段需要更新
reason = f"No physical diff found across all 6 sub-schemas for project {biz_id} despite strategy update request (Normalization Discrepancy)."
print(f" [WARNING] {reason}")
results.append(TaskResult.skipped(node_id=node.node_id, reason=reason))
return results
async def delete_all(self, nodes: List[SyncNode]) -> List[TaskResult]:
"""不支持删除"""
return [
TaskResult.failed(node_id=node.node_id, error="Project deletion not supported")
for node in nodes
]
async def poll_tasks(self, task_ids: List[str]) -> Dict[str, TaskResult]:
"""轮询异步任务状态"""
return await super().poll_tasks(task_ids)
def _create_node(self, data: Dict[str, Any]) -> SyncNode:
node = self._create_basic_node(data, depend_ids=[])
# 设置 context
node.context["project_id"] = node.data_id
return node
# ========== 静态 API 函数(可抽成 SDK ==========
async def api_get_project_list(client, page: int = 1, page_size: int = 100) -> tuple[List[ProjectResponseBase], int]:
"""GET /project/list - 获取项目列表
Args:
client: API客户端
page: 页码,从1开始
page_size: 每页大小
Returns:
tuple[List[ProjectResponseBase], int]: (验证过的项目列表, 总数)
"""
response = await client.request(
method="GET",
endpoint="/project/list",
params={"page": page, "page_size": page_size}
)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
data = response.get("data", {})
list_data = data.get("list", [])
total = data.get("total", 0)
# 验证每个返回项
validated_items = []
for item in list_data:
validated_item = ProjectResponseBase.model_validate(item)
validated_items.append(validated_item)
return validated_items, total
async def api_get_project(client, project_id: str) -> ProjectResponseBase:
"""GET /project - 获取项目详情"""
response = await client.request(
method="GET",
endpoint="/project",
params={"project_id": project_id}
)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
return ProjectResponseBase.model_validate(response.get("data"))
async def api_get_project_all_info(client, project_id: str) -> Dict[str, Any]:
"""GET /project/all_info - 获取项目完整信息(校验为 ProjectInfoResponse"""
response = await client.request(
method="GET",
endpoint="/project/all_info",
params={"project_id": project_id}
)
# 检查响应状态
code = response.get('code')
if code != 200:
error_msg = response.get('message', 'Unknown error')
# 422 验证错误时打印详细信息
if code == 422:
print(f"\n{'='*70}")
print("❌ API 422 验证错误")
print(f"{'='*70}")
print("接口: GET /project/all_info")
print(f"项目ID: {project_id}")
print(f"错误信息: {error_msg}")
print("完整响应:")
import json
print(json.dumps(response, indent=2, ensure_ascii=False))
print(f"{'='*70}\n")
raise Exception(f"API error {code}: {error_msg}")
data = response.get('data', {})
validated = ProjectInfoResponse.model_validate(data)
return validated.model_dump()
async def api_update_project_base_info(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
"""PUT /project - 更新项目基本信息"""
ProjectBaseInfoUpdate.model_validate(data)
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
response = await client.request(method="PUT", endpoint="/project", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
async def api_update_project_key_constraints(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
"""PUT /project/key_constraints - 更新关键节点"""
ProjectKeyConstraints.model_validate(data)
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
response = await client.request(method="PUT", endpoint="/project/key_constraints", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
async def api_update_project_complete_investment(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
"""PUT /project/complete_investment - 更新完成投资"""
ProjectCompleteInvestment.model_validate(data)
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
response = await client.request(method="PUT", endpoint="/project/complete_investment", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
async def api_update_project_investment_decision(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
"""PUT /project/investment_decision - 更新投资决策"""
ProjectInvestmentDecision.model_validate(data)
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
response = await client.request(method="PUT", endpoint="/project/investment_decision", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
async def api_update_project_dynamic_investment(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
"""PUT /project/dynamic_investment - 更新动态投资"""
ProjectDynamicInvestment.model_validate(data)
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
response = await client.request(method="PUT", endpoint="/project/dynamic_investment", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
async def api_update_project_settlement_investment(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
"""PUT /project/settlement_investment - 更新结算投资"""
ProjectSettlementInvestment.model_validate(data)
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
response = await client.request(method="PUT", endpoint="/project/settlement_investment", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
@@ -0,0 +1,12 @@
"""
Project JSONL Handler
"""
from ...datasource import BaseJsonlHandler, JsonlDataSource
from .sync_node import ProjectSyncNode
from schemas.project.project_base import ProjectResponseBase
class ProjectJsonlHandler(BaseJsonlHandler):
"""项目 JSONL Handler"""
def __init__(self, datasource: JsonlDataSource):
super().__init__(datasource, "project", ProjectSyncNode, ProjectResponseBase)
@@ -0,0 +1,17 @@
"""Project domain registration"""
from ...common.registry import DomainRegistry
from schemas.project.project_base import ProjectResponseBase
from .sync_node import ProjectSyncNode
from .sync_strategy import ProjectSyncStrategy
from .jsonl_handler import ProjectJsonlHandler
from .api_handler import ProjectApiHandler
# 注册 project domain
DomainRegistry.register(
node_type="project",
schema=ProjectResponseBase,
node_class=ProjectSyncNode,
strategy_class=ProjectSyncStrategy,
jsonl_handler_class=ProjectJsonlHandler,
api_handler_class=ProjectApiHandler,
)
@@ -0,0 +1,11 @@
from ...common.sync_node import SyncNode
from schemas.project.project_base import ProjectResponseBase
class ProjectSyncNode(SyncNode[ProjectResponseBase]):
"""项目同步节点"""
node_type = "project"
schema = ProjectResponseBase
def __init__(self, **kwargs):
super().__init__(**kwargs)
@@ -0,0 +1,29 @@
from ...sync_system.strategy import DefaultSyncStrategy
from ...sync_system.config import StrategyConfig, OrphanAction, UpdateDirection
from schemas.project.project_base import ProjectResponseBase
class ProjectSyncStrategy(DefaultSyncStrategy[ProjectResponseBase]):
"""
Project 同步策略
业务规则:
1. 手动绑定(根节点)
2. 允许拉取更新(不自动创建)
3. 依赖公司(company
schema 已在类定义中设置,禁止在初始化时传入其他 schema。
"""
# 类变量:schema 固定为 ProjectResponseBase
schema = ProjectResponseBase
# 类变量:默认配置
default_config = StrategyConfig(
auto_bind=False,
auto_bind_fields=[],
depend_fields={"company_id": "company"},
local_orphan_action=OrphanAction.CREATE_REMOTE,
remote_orphan_action=OrphanAction.NONE,
update_direction=UpdateDirection.PUSH,
)