增加了项目侧关于steps的处理。允许保存和提取steps信息。

This commit is contained in:
strepsiades
2026-03-11 10:34:49 +08:00
parent 291aa0b4b7
commit 1f68bd5ca8
22 changed files with 759 additions and 142 deletions
+52 -3
View File
@@ -15,6 +15,7 @@ BaseApiHandler - API Handler 基类
import uuid
import asyncio
from abc import abstractmethod
from copy import deepcopy
from typing import ClassVar, Dict, List, Any, Optional, TYPE_CHECKING, TypeVar
from pydantic import BaseModel
@@ -139,6 +140,48 @@ class BaseApiHandler(NodeHandler[T]):
"""默认节点创建(子类可覆盖并补充 context)"""
return self._create_basic_node(data)
def _get_committed_approval_snapshot(self, node: 'SyncNode') -> Dict[str, Any]:
snapshot = node.context.get("approval_snapshot")
return snapshot if isinstance(snapshot, dict) else {}
def _get_pending_approval_snapshot(self, node: 'SyncNode') -> Dict[str, Any]:
snapshot = node.context.get("pending_approval_snapshot")
return snapshot if isinstance(snapshot, dict) else {}
def _get_active_approval_snapshot(self, node: 'SyncNode') -> Dict[str, Any]:
pending_snapshot = self._get_pending_approval_snapshot(node)
if pending_snapshot:
return pending_snapshot
return self._get_committed_approval_snapshot(node)
def _get_request_steps(self, node: 'SyncNode') -> List[Dict[str, Any]]:
snapshot = self._get_active_approval_snapshot(node)
steps = snapshot.get("steps")
if isinstance(steps, list):
return deepcopy(steps)
return []
def _steps_changed(self, node: 'SyncNode') -> bool:
snapshot = self._get_active_approval_snapshot(node)
return bool(snapshot.get("steps_changed"))
def _attach_request_steps(self, request_data: Dict[str, Any], node: Optional['SyncNode']) -> Dict[str, Any]:
payload = dict(request_data)
payload["steps"] = self._get_request_steps(node) if node is not None else []
return payload
def _build_full_update_data(self, new_data: Dict[str, Any], schema: Any) -> Dict[str, Any]:
validated = schema.model_validate(new_data)
return validated.model_dump(exclude_none=True)
def _mark_approval_snapshot_synced(self, node: 'SyncNode') -> None:
snapshot = self._get_active_approval_snapshot(node)
if snapshot:
committed_snapshot = deepcopy(snapshot)
committed_snapshot.pop("steps_changed", None)
node.context["approval_snapshot"] = committed_snapshot
node.context.pop("pending_approval_snapshot", None)
def extract_id(self, data: Dict[str, Any]) -> str:
"""从原始数据提取 ID(默认优先 id,其次 _id)"""
return str(data.get("id") or data.get("_id") or "")
@@ -221,7 +264,8 @@ class BaseApiHandler(NodeHandler[T]):
schema,
data: Dict[str, Any],
origin_data: Dict[str, Any],
node_id: Optional[str] = None
node_id: Optional[str] = None,
force_full_schema: bool = False,
) -> Dict[str, Any] | None:
"""
获取数据在指定 schema 下的差异字段
@@ -245,6 +289,8 @@ class BaseApiHandler(NodeHandler[T]):
origin_dict = origin_model.model_dump(exclude_unset=True)
if new_dict == origin_dict:
if force_full_schema:
return new_dict
return None
# 返回有差异的字段
@@ -268,7 +314,8 @@ class BaseApiHandler(NodeHandler[T]):
self,
new_data: Dict[str, Any],
original_data: Dict[str, Any],
schema: Optional[Any] = None
schema: Optional[Any] = None,
force_full_schema: bool = False,
) -> Dict[str, Any]:
"""
过滤出需要更新的字段
@@ -320,7 +367,9 @@ class BaseApiHandler(NodeHandler[T]):
has_change = True
# 2. 只有存在变化时才返回数据
return schema_data if has_change else {}
if has_change or force_full_schema:
return schema_data
return {}
def _schema_changed_fields(
self,
@@ -21,7 +21,7 @@ class CompanySyncStrategy(DefaultSyncStrategy[CompanyResponse]):
# 类变量:默认配置
default_config = StrategyConfig(
auto_bind=True,
auto_bind_fields=["credit_code"],
auto_bind_fields=["code"],
depend_fields={},
local_orphan_action=OrphanAction.NONE,
remote_orphan_action=OrphanAction.CREATE_LOCAL,
@@ -81,7 +81,12 @@ class ConstructionTaskApiHandler(BaseApiHandler):
results.append(TaskResult.failed(node_id=node.node_id, error=f"{ERROR_VALIDATION_FAILED}: {e}"))
continue
response = await api_create_construction_task(self.api_client, create_model, push_id)
response = await api_create_construction_task(
self.api_client,
create_model,
push_id,
steps=self._get_request_steps(node),
)
if self.poll_mode == "sync":
created_id = self._extract_created_id_from_response(response)
if not created_id:
@@ -134,8 +139,13 @@ class ConstructionTaskApiHandler(BaseApiHandler):
# 1. 基础信息更新 (ConstructionUpdate)
# 业务逻辑:如果 show_name 字段不同,那么仅当 type == 'QT' 时才更新
force_steps_update = self._steps_changed(node)
steps_payload = self._get_request_steps(node)
steps_sent = False
base_new_schema_error = self._validate_schema(data, ConstructionUpdate)
base_changed_fields = self._schema_changed_fields(ConstructionUpdate, data, origin_data)
if force_steps_update and not base_new_schema_error and not base_changed_fields:
base_changed_fields = ["__steps__"]
base_update_called = False
base_update_error: str | None = None
if base_new_schema_error:
@@ -150,11 +160,29 @@ class ConstructionTaskApiHandler(BaseApiHandler):
if should_update:
try:
update_model = ConstructionUpdate.model_validate(data)
update_payload = data if (force_steps_update and base_changed_fields == ["__steps__"]) else data
update_model = ConstructionUpdate.model_validate(update_payload)
push_id = self._generate_push_id()
await api_update_construction_task(self.api_client, update_model, push_id, biz_id)
request_steps = steps_payload if not steps_sent else []
if request_steps:
await api_update_construction_task(
self.api_client,
update_model,
push_id,
biz_id,
steps=request_steps,
)
else:
await api_update_construction_task(
self.api_client,
update_model,
push_id,
biz_id,
)
node_updated = True
base_update_called = True
steps_sent = steps_sent or bool(steps_payload)
self._mark_approval_snapshot_synced(node)
except Exception as e:
error = str(e)
base_update_error = str(e)
@@ -185,9 +213,26 @@ class ConstructionTaskApiHandler(BaseApiHandler):
plan_model = ConstructionPlanUpdate.model_validate(data)
plan_dict = plan_model.model_dump(exclude_unset=True)
push_id = self._generate_push_id()
await api_update_construction_task_plan(self.api_client, plan_dict, push_id, biz_id)
request_steps = steps_payload if not steps_sent else []
if request_steps:
await api_update_construction_task_plan(
self.api_client,
plan_dict,
push_id,
biz_id,
steps=request_steps,
)
else:
await api_update_construction_task_plan(
self.api_client,
plan_dict,
push_id,
biz_id,
)
node_updated = True
plan_update_called = True
steps_sent = steps_sent or bool(steps_payload)
self._mark_approval_snapshot_synced(node)
except Exception as e:
error = str(e)
plan_update_error = str(e)
@@ -279,16 +324,23 @@ async def api_get_construction_task_list(client, project_id: str) -> List[Constr
return validated_items
async def api_create_construction_task(client, data: ConstructionCreate, push_id: str) -> Dict[str, Any]:
request_data = {"push_id": push_id, "data": data.model_dump()}
async def api_create_construction_task(client, data: ConstructionCreate, push_id: str, *, steps: List[Dict[str, Any]] | None = None) -> Dict[str, Any]:
request_data = {"push_id": push_id, "data": data.model_dump(), "steps": steps or []}
response = await client.request(method="POST", endpoint="/construction/create", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
return response
async def api_update_construction_task(client, data: ConstructionUpdate, push_id: str, biz_id: str) -> None:
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True)}
async def api_update_construction_task(
client,
data: ConstructionUpdate,
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True), "steps": steps or []}
response = await client.request(method="PUT", endpoint="/construction/update", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
@@ -306,9 +358,16 @@ async def api_get_construction_task(client, task_id: str) -> Dict[str, Any]:
return response.get("data", {})
async def api_update_construction_task_plan(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
async def api_update_construction_task_plan(
client,
data: Dict[str, Any],
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
"""PUT /construction/plan - 更新施工计划"""
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
response = await client.request(method="PUT", endpoint="/construction/plan", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
@@ -78,7 +78,12 @@ class ConstructionTaskDetailApiHandler(BaseApiHandler):
push_id = self._generate_push_id()
try:
response = await api_create_construction_task_detail(self.api_client, create_model, push_id)
response = await api_create_construction_task_detail(
self.api_client,
create_model,
push_id,
steps=self._get_request_steps(node),
)
if self.poll_mode == "sync":
created_id = self._extract_created_id_from_response(response)
if not created_id:
@@ -120,7 +125,14 @@ class ConstructionTaskDetailApiHandler(BaseApiHandler):
origin_data = node.get_origin_data() or {}
# 使用 _get_schema_diff 检查差异并打印日志
diff_fields = self._get_schema_diff(ConstructionDetailUpdate, data, origin_data, node.node_id)
force_steps_update = self._steps_changed(node)
diff_fields = self._get_schema_diff(
ConstructionDetailUpdate,
data,
origin_data,
node.node_id,
force_full_schema=force_steps_update,
)
if not diff_fields:
from ...common.types import SyncAction
results.append(TaskResult.skipped(node_id=node.node_id, reason="No updatable fields changed"))
@@ -133,7 +145,12 @@ class ConstructionTaskDetailApiHandler(BaseApiHandler):
continue
# 需要完整的 schema 数据(包括必填字段)
update_data = self._filter_update_data(data, origin_data, ConstructionDetailUpdate)
update_data = self._filter_update_data(
data,
origin_data,
ConstructionDetailUpdate,
force_full_schema=force_steps_update,
)
# 验证并转换为 Pydantic model
try:
@@ -148,6 +165,7 @@ class ConstructionTaskDetailApiHandler(BaseApiHandler):
# 准备批量数据
detail_list = [model.model_dump(mode='json', exclude_unset=True) for node, model in group_items]
steps = self._get_request_steps(group_items[0][0]) if group_items else []
# 使用 parent_idtask_id)作为 biz_id
biz_id = task_id
@@ -159,9 +177,11 @@ class ConstructionTaskDetailApiHandler(BaseApiHandler):
{"detail_list": detail_list},
push_id,
biz_id,
steps=steps,
)
# 整组成功
for node, _ in group_items:
self._mark_approval_snapshot_synced(node)
if self.poll_mode == "sync":
results.append(TaskResult.success(node_id=node.node_id))
else:
@@ -243,7 +263,13 @@ async def api_get_construction_task_detail_list(client, task_id: str) -> List[Co
return validated_items
async def api_create_construction_task_detail(client, data: ConstructionDetailCreate, push_id: str) -> Dict[str, Any]:
async def api_create_construction_task_detail(
client,
data: ConstructionDetailCreate,
push_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> Dict[str, Any]:
"""
创建施工明细 POST /construction/detail/create
@@ -253,14 +279,21 @@ async def api_create_construction_task_detail(client, data: ConstructionDetailCr
push_id: 推送ID
biz_id: 业务ID
"""
request_data = {"push_id": push_id, "data": data.model_dump(mode='json')}
request_data = {"push_id": push_id, "data": data.model_dump(mode='json'), "steps": steps or []}
response = await client.request(method="POST", endpoint="/construction/detail/create", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
return response
async def api_update_construction_task_detail(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
async def api_update_construction_task_detail(
client,
data: Dict[str, Any],
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
"""
更新施工明细 PUT /construction/detail/update
@@ -270,7 +303,7 @@ async def api_update_construction_task_detail(client, data: Dict[str, Any], push
push_id: 推送ID
biz_id: 业务ID
"""
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
response = await client.request(method="PUT", endpoint="/construction/detail/update", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
@@ -48,20 +48,38 @@ async def api_get_contract(client: ApiClient, contract_id: str) -> ContractRespo
return ContractResponse.model_validate(response["data"])
async def api_create_contract(client: ApiClient, data: Dict[str, Any], push_id: str) -> Dict[str, Any]:
async def api_create_contract(
client: ApiClient,
data: Dict[str, Any],
push_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> Dict[str, Any]:
"""POST /contract/create - 创建合同(异步)"""
validated_data = ContractCreate.model_validate(data)
request_data = {"push_id": push_id, "data": validated_data.model_dump(exclude_none=True)}
request_data = {"push_id": push_id, "data": validated_data.model_dump(exclude_none=True), "steps": steps or []}
response = await client.request("POST", "/contract/create", data=request_data)
if response.get("code") != 200:
raise Exception(f"Failed to create contract: {response.get('message', 'Unknown error')}")
return response
async def api_update_contract(client: ApiClient, data_id: str, data: Dict[str, Any], push_id: str) -> Dict[str, Any]:
async def api_update_contract(
client: ApiClient,
data_id: str,
data: Dict[str, Any],
push_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> Dict[str, Any]:
"""PUT /contract/update - 更新合同(同步)"""
validated_data = ContractUpdate.model_validate(data)
request_data = {"push_id": push_id, "biz_id": data_id, "data": validated_data.model_dump(exclude_none=True)}
request_data = {
"push_id": push_id,
"biz_id": data_id,
"data": validated_data.model_dump(exclude_none=True),
"steps": steps or [],
}
response = await client.request("PUT", "/contract/update", data=request_data)
if response.get("code") != 200:
raise Exception(f"Failed to update contract: {response.get('message', 'Unknown error')}")
@@ -194,7 +212,12 @@ class ContractApiHandler(BaseApiHandler):
# 调用 API(直接调用顶层函数)
push_id = self._generate_push_id()
response = await api_create_contract(self.api_client, request_data, push_id)
response = await api_create_contract(
self.api_client,
request_data,
push_id,
steps=self._get_request_steps(node),
)
if self.poll_mode == "sync":
created_id = self._extract_created_id_from_response(response)
if not created_id:
@@ -232,10 +255,12 @@ class ContractApiHandler(BaseApiHandler):
# 过滤出需要更新的字段(排除空值、未变化的字段)
origin_data = node.get_origin_data() or {}
force_steps_update = self._steps_changed(node)
update_data = self._filter_update_data(
new_data=data,
original_data=origin_data,
schema=ContractUpdate,
force_full_schema=force_steps_update,
)
# 如果没有需要更新的字段,降级 action 并返回跳过
@@ -257,10 +282,17 @@ class ContractApiHandler(BaseApiHandler):
return TaskResult.failed(node_id=node.node_id, error="Missing biz_id for update")
# 调用 API
response = await api_update_contract(self.api_client, biz_id, update_data, push_id)
response = await api_update_contract(
self.api_client,
biz_id,
update_data,
push_id,
steps=self._get_request_steps(node),
)
# 检查响应状态(更新接口是同步的)
if response.get("code") == 200:
self._mark_approval_snapshot_synced(node)
return TaskResult.success(node_id=node.node_id)
else:
return TaskResult.failed(
@@ -55,7 +55,12 @@ class ContractChangeApiHandler(BaseApiHandler):
push_id = self._generate_push_id()
try:
response = await api_create_contract_change(self.api_client, create_model, push_id)
response = await api_create_contract_change(
self.api_client,
create_model,
push_id,
steps=self._get_request_steps(node),
)
if self.poll_mode == "sync":
created_id = self._extract_created_id_from_response(response)
if not created_id:
@@ -87,7 +92,13 @@ class ContractChangeApiHandler(BaseApiHandler):
continue
origin_data = node.get_origin_data() or {}
update_data = self._filter_update_data(node_data, origin_data, PostContractChange)
force_steps_update = self._steps_changed(node)
update_data = self._filter_update_data(
node_data,
origin_data,
PostContractChange,
force_full_schema=force_steps_update,
)
if not update_data:
from ...common.types import SyncAction
results.append(TaskResult.skipped(node_id=node.node_id, reason="No updatable fields changed"))
@@ -107,7 +118,14 @@ class ContractChangeApiHandler(BaseApiHandler):
continue
try:
await api_update_contract_change(self.api_client, update_model, push_id, biz_id)
await api_update_contract_change(
self.api_client,
update_model,
push_id,
biz_id,
steps=self._get_request_steps(node),
)
self._mark_approval_snapshot_synced(node)
results.append(TaskResult.success(node_id=node.node_id))
except Exception as e:
results.append(TaskResult.failed(node_id=node.node_id, error=str(e)))
@@ -138,7 +156,13 @@ class ContractChangeApiHandler(BaseApiHandler):
# ========== 静态 API 函数 ==========
async def api_create_contract_change(client, data: CreateContractChange, push_id: str) -> Dict[str, Any]:
async def api_create_contract_change(
client,
data: CreateContractChange,
push_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> Dict[str, Any]:
"""
POST /contract_change/create - 创建合同变更
@@ -148,14 +172,21 @@ async def api_create_contract_change(client, data: CreateContractChange, push_id
push_id: 推送ID
biz_id: 业务ID
"""
request_data = {"push_id": push_id, "data": data.model_dump()}
request_data = {"push_id": push_id, "data": data.model_dump(), "steps": steps or []}
response = await client.request(method="POST", endpoint="/contract_change/create", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
return response
async def api_update_contract_change(client, data: PostContractChange, push_id: str, biz_id: str) -> None:
async def api_update_contract_change(
client,
data: PostContractChange,
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
"""
PUT /contract_change/update - 更新合同变更
@@ -165,7 +196,7 @@ async def api_update_contract_change(client, data: PostContractChange, push_id:
push_id: 推送ID
biz_id: 业务ID
"""
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True)}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True), "steps": steps or []}
response = await client.request(method="PUT", endpoint="/contract_change/update", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
@@ -82,7 +82,12 @@ class ContractSettlementApiHandler(BaseApiHandler):
push_id = self._generate_push_id()
try:
response = await api_create_contract_settlement(self.api_client, create_model, push_id)
response = await api_create_contract_settlement(
self.api_client,
create_model,
push_id,
steps=self._get_request_steps(node),
)
if self.poll_mode == "sync":
created_id = self._extract_created_id_from_response(response)
if not created_id:
@@ -130,14 +135,31 @@ class ContractSettlementApiHandler(BaseApiHandler):
node_updated = False
error = None
steps_payload = self._get_request_steps(node)
steps_sent = False
# 1. 基础信息更新 (ContractSettlementUpdate)
base_diff = self._get_schema_diff(ContractSettlementUpdate, data, origin_data, node.node_id)
force_steps_update = self._steps_changed(node)
base_diff = self._get_schema_diff(
ContractSettlementUpdate,
data,
origin_data,
node.node_id,
force_full_schema=force_steps_update,
)
if base_diff:
try:
update_model = ContractSettlementUpdate.model_validate(data)
push_id = self._generate_push_id()
await api_update_contract_settlement(self.api_client, update_model, push_id, biz_id)
await api_update_contract_settlement(
self.api_client,
update_model,
push_id,
biz_id,
steps=steps_payload if not steps_sent else [],
)
self._mark_approval_snapshot_synced(node)
steps_sent = steps_sent or bool(steps_payload)
node_updated = True
except ValidationError as e:
error = f"Validation failed: {e}"
@@ -151,7 +173,15 @@ class ContractSettlementApiHandler(BaseApiHandler):
plan_model = ContractSettlementPlanUpdate.model_validate(data)
push_id = self._generate_push_id()
# 使用计划更新专用 API
await api_update_settlement_plan(self.api_client, plan_model.model_dump(exclude_unset=True), push_id, biz_id)
await api_update_settlement_plan(
self.api_client,
plan_model.model_dump(exclude_unset=True),
push_id,
biz_id,
steps=steps_payload if not steps_sent else [],
)
self._mark_approval_snapshot_synced(node)
steps_sent = steps_sent or bool(steps_payload)
node_updated = True
except ValidationError as e:
error = f"Validation failed: {e}"
@@ -255,7 +285,13 @@ async def api_get_contract_settlement_list(client, project_id: str) -> List[Cont
return validated_items
async def api_create_contract_settlement(client, data: ContractSettlementCreate, push_id: str) -> Dict[str, Any]:
async def api_create_contract_settlement(
client,
data: ContractSettlementCreate,
push_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> Dict[str, Any]:
"""
创建合同结算 POST /contract_settlement/create
@@ -271,14 +307,21 @@ async def api_create_contract_settlement(client, data: ContractSettlementCreate,
Raises:
Exception: API 返回错误
"""
request_data = {"push_id": push_id, "data": data.model_dump()}
request_data = {"push_id": push_id, "data": data.model_dump(), "steps": steps or []}
response = await client.request(method="POST", endpoint="/contract_settlement/create", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
return response
async def api_update_contract_settlement(client, data: ContractSettlementUpdate, push_id: str, biz_id: str) -> None:
async def api_update_contract_settlement(
client,
data: ContractSettlementUpdate,
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
"""
更新合同结算 PUT /contract_settlement/update
@@ -291,7 +334,7 @@ async def api_update_contract_settlement(client, data: ContractSettlementUpdate,
Raises:
Exception: API 返回错误
"""
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True)}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True), "steps": steps or []}
response = await client.request(method="PUT", endpoint="/contract_settlement/update", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
@@ -305,9 +348,16 @@ async def api_delete_contract_settlement(client, settlement_id: str, push_id: st
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
async def api_update_settlement_plan(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
async def api_update_settlement_plan(
client,
data: Dict[str, Any],
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
"""更新合同结算计划 PUT /contract_settlement/plan"""
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
response = await client.request(method="PUT", endpoint="/contract_settlement/plan", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
@@ -76,7 +76,12 @@ class ContractSettlementDetailApiHandler(BaseApiHandler):
push_id = self._generate_push_id()
try:
response = await api_create_contract_settlement_detail(self.api_client, create_model, push_id)
response = await api_create_contract_settlement_detail(
self.api_client,
create_model,
push_id,
steps=self._get_request_steps(node),
)
if self.poll_mode == "sync":
created_id = self._extract_created_id_from_response(response)
if not created_id:
@@ -118,7 +123,14 @@ class ContractSettlementDetailApiHandler(BaseApiHandler):
origin_data = node.get_origin_data() or {}
# 使用 _get_schema_diff 检查差异并打印日志
diff_fields = self._get_schema_diff(ContractSettlementDetailUpdate, data, origin_data, node.node_id)
force_steps_update = self._steps_changed(node)
diff_fields = self._get_schema_diff(
ContractSettlementDetailUpdate,
data,
origin_data,
node.node_id,
force_full_schema=force_steps_update,
)
if not diff_fields:
from ...common.types import SyncAction
results.append(TaskResult.skipped(node_id=node.node_id, reason="No updatable fields changed"))
@@ -131,7 +143,12 @@ class ContractSettlementDetailApiHandler(BaseApiHandler):
continue
# 需要完整的 schema 数据(包括必填字段)
update_data = self._filter_update_data(data, origin_data, ContractSettlementDetailUpdate)
update_data = self._filter_update_data(
data,
origin_data,
ContractSettlementDetailUpdate,
force_full_schema=force_steps_update,
)
try:
update_model = ContractSettlementDetailUpdate.model_validate(update_data)
@@ -145,6 +162,7 @@ class ContractSettlementDetailApiHandler(BaseApiHandler):
# 准备批量数据
detail_list = [model.model_dump(mode='json', exclude_unset=True) for node, model in group_items]
steps = self._get_request_steps(group_items[0][0]) if group_items else []
# 使用第一个节点的 biz_id(对于批量更新,通常使用 parent_id)
biz_id = settlement_id
@@ -156,9 +174,11 @@ class ContractSettlementDetailApiHandler(BaseApiHandler):
{"detail_list": detail_list},
push_id,
biz_id,
steps=steps,
)
# 整组成功
for node, _ in group_items:
self._mark_approval_snapshot_synced(node)
results.append(TaskResult.success(node_id=node.node_id, push_id=push_id))
except Exception as e:
# 整组失败
@@ -239,7 +259,13 @@ async def api_get_contract_settlement_detail_list(client, settlement_id: str) ->
return validated_items
async def api_create_contract_settlement_detail(client, data: ContractSettlementDetailCreate, push_id: str) -> Dict[str, Any]:
async def api_create_contract_settlement_detail(
client,
data: ContractSettlementDetailCreate,
push_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> Dict[str, Any]:
"""
创建合同结算明细
@@ -255,14 +281,21 @@ async def api_create_contract_settlement_detail(client, data: ContractSettlement
Raises:
Exception: API 返回错误
"""
request_data = {"push_id": push_id, "data": data.model_dump(mode='json')}
request_data = {"push_id": push_id, "data": data.model_dump(mode='json'), "steps": steps or []}
response = await client.request(method="POST", endpoint="/contract_settlement/detail/create", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
return response
async def api_update_contract_settlement_detail(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
async def api_update_contract_settlement_detail(
client,
data: Dict[str, Any],
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
"""
更新合同结算明细
@@ -275,7 +308,7 @@ async def api_update_contract_settlement_detail(client, data: Dict[str, Any], pu
Raises:
Exception: API 返回错误
"""
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
response = await client.request(method="PUT", endpoint="/contract_settlement/detail/update", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
+47 -8
View File
@@ -111,10 +111,19 @@ class LarApiHandler(BaseApiHandler):
origin_data = node.get_origin_data() or {}
node_updated = False
force_steps_update = self._steps_changed(node)
steps_payload = self._get_request_steps(node)
steps_sent = False
allow_force_steps = True
for schema, api_func in update_configs:
# 获取过滤后的更新数据
update_data = self._filter_update_data(node_data, origin_data, schema)
update_data = self._filter_update_data(
node_data,
origin_data,
schema,
force_full_schema=force_steps_update and allow_force_steps,
)
if update_data:
# 验证并转换为 Pydantic model
try:
@@ -131,8 +140,17 @@ class LarApiHandler(BaseApiHandler):
break
try:
await api_func(self.api_client, update_model, push_id, biz_id)
await api_func(
self.api_client,
update_model,
push_id,
biz_id,
steps=steps_payload if not steps_sent else [],
)
node_updated = True
steps_sent = steps_sent or bool(steps_payload)
allow_force_steps = False
self._mark_approval_snapshot_synced(node)
except Exception as e:
results.append(TaskResult.failed(node_id=node.node_id, error=str(e)))
break
@@ -170,7 +188,14 @@ class LarApiHandler(BaseApiHandler):
# ========== 静态 API 函数 ==========
async def api_update_lar_main(client, data: LarMainInfoUpdateSchema, push_id: str, biz_id: str) -> None:
async def api_update_lar_main(
client,
data: LarMainInfoUpdateSchema,
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
"""
PUT /lar/main - 更新主数据
@@ -180,13 +205,20 @@ async def api_update_lar_main(client, data: LarMainInfoUpdateSchema, push_id: st
push_id: 推送ID
biz_id: 业务ID
"""
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True)}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True), "steps": steps or []}
response = await client.request(method="PUT", endpoint="/lar/main", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
async def api_update_lar_resettlement(client, data: LarResettlementSchema, push_id: str, biz_id: str) -> None:
async def api_update_lar_resettlement(
client,
data: LarResettlementSchema,
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
"""
PUT /lar/resettlement - 更新搬迁信息
@@ -196,13 +228,20 @@ async def api_update_lar_resettlement(client, data: LarResettlementSchema, push_
push_id: 推送ID
biz_id: 业务ID
"""
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True)}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True), "steps": steps or []}
response = await client.request(method="PUT", endpoint="/lar/resettlement", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
async def api_update_lar_investment(client, data: LarInvestmentSchema, push_id: str, biz_id: str) -> None:
async def api_update_lar_investment(
client,
data: LarInvestmentSchema,
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
"""
PUT /lar/investment - 更新投资信息
@@ -212,7 +251,7 @@ async def api_update_lar_investment(client, data: LarInvestmentSchema, push_id:
push_id: 推送ID
biz_id: 业务ID
"""
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True)}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True), "steps": steps or []}
response = await client.request(method="PUT", endpoint="/lar/investment", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
@@ -67,7 +67,12 @@ class LarChangeApiHandler(BaseApiHandler):
push_id = self._generate_push_id()
try:
response = await api_create_lar_change(self.api_client, create_model, push_id)
response = await api_create_lar_change(
self.api_client,
create_model,
push_id,
steps=self._get_request_steps(node),
)
if self.poll_mode == "sync":
created_id = self._extract_created_id_from_response(response)
if not created_id:
@@ -98,7 +103,13 @@ class LarChangeApiHandler(BaseApiHandler):
results.append(TaskResult.failed(node_id=node.node_id, error=f"Cannot update: {error_msg}"))
continue
origin_data = node.get_origin_data() or {}
update_data = self._filter_update_data(node_data, origin_data, LarChangeUpdateSchema)
force_steps_update = self._steps_changed(node)
update_data = self._filter_update_data(
node_data,
origin_data,
LarChangeUpdateSchema,
force_full_schema=force_steps_update,
)
if not update_data:
from ...common.types import SyncAction
results.append(TaskResult.skipped(node_id=node.node_id, reason="No updatable fields changed"))
@@ -118,7 +129,14 @@ class LarChangeApiHandler(BaseApiHandler):
continue
try:
await api_update_lar_change(self.api_client, update_model, push_id, biz_id)
await api_update_lar_change(
self.api_client,
update_model,
push_id,
biz_id,
steps=self._get_request_steps(node),
)
self._mark_approval_snapshot_synced(node)
results.append(TaskResult.success(node_id=node.node_id))
except Exception as e:
results.append(TaskResult.failed(node_id=node.node_id, error=str(e)))
@@ -147,7 +165,13 @@ class LarChangeApiHandler(BaseApiHandler):
# ========== 静态 API 函数 ==========
async def api_create_lar_change(client, data: LarChangeCreateSchema, push_id: str) -> Dict[str, Any]:
async def api_create_lar_change(
client,
data: LarChangeCreateSchema,
push_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> Dict[str, Any]:
"""
POST /lar/change - 创建移民变更
@@ -157,14 +181,21 @@ async def api_create_lar_change(client, data: LarChangeCreateSchema, push_id: st
push_id: 推送ID
biz_id: 业务ID
"""
request_data = {"push_id": push_id, "data": data.model_dump()}
request_data = {"push_id": push_id, "data": data.model_dump(), "steps": steps or []}
response = await client.request(method="POST", endpoint="/lar/change", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
return response
async def api_update_lar_change(client, data: LarChangeUpdateSchema, push_id: str, biz_id: str) -> None:
async def api_update_lar_change(
client,
data: LarChangeUpdateSchema,
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
"""
PUT /lar/change - 更新移民变更
@@ -174,7 +205,7 @@ async def api_update_lar_change(client, data: LarChangeUpdateSchema, push_id: st
push_id: 推送ID
biz_id: 业务ID
"""
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True)}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True), "steps": steps or []}
response = await client.request(method="PUT", endpoint="/lar/change", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
@@ -101,14 +101,31 @@ class MaterialApiHandler(BaseApiHandler):
node_updated = False
error = None
steps_payload = self._get_request_steps(node)
steps_sent = False
# 1. 基础信息更新 (MaterialUpdate)
base_diff = self._get_schema_diff(MaterialUpdate, data, origin_data, node.node_id)
force_steps_update = self._steps_changed(node)
base_diff = self._get_schema_diff(
MaterialUpdate,
data,
origin_data,
node.node_id,
force_full_schema=force_steps_update,
)
if base_diff:
try:
update_model = MaterialUpdate.model_validate(data)
push_id = self._generate_push_id()
await api_update_material(self.api_client, update_model, push_id, biz_id)
await api_update_material(
self.api_client,
update_model,
push_id,
biz_id,
steps=steps_payload if not steps_sent else [],
)
self._mark_approval_snapshot_synced(node)
steps_sent = steps_sent or bool(steps_payload)
node_updated = True
except ValidationError as e:
error = f"{ERROR_VALIDATION_FAILED}: {e}"
@@ -124,8 +141,11 @@ class MaterialApiHandler(BaseApiHandler):
self.api_client,
plan_model.model_dump(exclude_unset=True),
push_id,
biz_id
biz_id,
steps=steps_payload if not steps_sent else [],
)
self._mark_approval_snapshot_synced(node)
steps_sent = steps_sent or bool(steps_payload)
node_updated = True
except ValidationError as e:
error = f"{ERROR_VALIDATION_FAILED}: {e}"
@@ -182,7 +202,12 @@ class MaterialApiHandler(BaseApiHandler):
error=f"{ERROR_VALIDATION_FAILED}: {e}"
)
response = await api_create_material(self.api_client, create_model, push_id)
response = await api_create_material(
self.api_client,
create_model,
push_id,
steps=self._get_request_steps(node),
)
if self.poll_mode == "sync":
created_id = self._extract_created_id_from_response(response)
if not created_id:
@@ -330,26 +355,40 @@ async def api_get_material_list(client, project_id: str) -> List[MaterialRespons
return validated_items
async def api_create_material(client, data: MaterialCreate, push_id: str) -> Dict[str, Any]:
async def api_create_material(client, data: MaterialCreate, push_id: str, *, steps: List[Dict[str, Any]] | None = None) -> Dict[str, Any]:
"""创建物资 POST /material/create"""
request_data = {"push_id": push_id, "data": data.model_dump()}
request_data = {"push_id": push_id, "data": data.model_dump(), "steps": steps or []}
response = await client.request(method="POST", endpoint="/material/create", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
return response
async def api_update_material(client, data: MaterialUpdate, push_id: str, biz_id: str) -> None:
async def api_update_material(
client,
data: MaterialUpdate,
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
"""更新物资 PUT /material/update"""
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True)}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True), "steps": steps or []}
response = await client.request(method="PUT", endpoint="/material/update", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
async def api_update_material_plan(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
async def api_update_material_plan(
client,
data: Dict[str, Any],
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
"""更新物资计划 PUT /material/plan"""
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
response = await client.request(method="PUT", endpoint="/material/plan", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
@@ -102,6 +102,7 @@ class MaterialDetailApiHandler(BaseApiHandler):
self.api_client,
create_model,
push_id,
steps=self._get_request_steps(node),
)
if self.poll_mode == "sync":
created_id = self._extract_created_id_from_response(response)
@@ -144,7 +145,14 @@ class MaterialDetailApiHandler(BaseApiHandler):
origin_data = node.get_origin_data() or {}
# 使用 _get_schema_diff 检查差异并打印日志
diff_fields = self._get_schema_diff(MaterialDetailUpdate, data, origin_data, node.node_id)
force_steps_update = self._steps_changed(node)
diff_fields = self._get_schema_diff(
MaterialDetailUpdate,
data,
origin_data,
node.node_id,
force_full_schema=force_steps_update,
)
if not diff_fields:
from ...common.types import SyncAction
results.append(TaskResult.skipped(node_id=node.node_id, reason="No updatable fields changed"))
@@ -157,7 +165,12 @@ class MaterialDetailApiHandler(BaseApiHandler):
continue
# 需要完整的 schema 数据(包括必填字段)
update_data = self._filter_update_data(data, origin_data, MaterialDetailUpdate)
update_data = self._filter_update_data(
data,
origin_data,
MaterialDetailUpdate,
force_full_schema=force_steps_update,
)
# 验证并转换为 Pydantic model
try:
@@ -174,6 +187,7 @@ class MaterialDetailApiHandler(BaseApiHandler):
# 准备批量数据
detail_list = [model.model_dump(mode='json', exclude_unset=True) for node, model in group_items]
steps = self._get_request_steps(group_items[0][0]) if group_items else []
# 使用 parent_idmaterial_id)作为 biz_id
biz_id = material_id
@@ -184,10 +198,12 @@ class MaterialDetailApiHandler(BaseApiHandler):
self.api_client,
{"detail_list": detail_list},
push_id,
biz_id
biz_id,
steps=steps,
)
# 整组成功
for node, _ in group_items:
self._mark_approval_snapshot_synced(node)
results.append(TaskResult.in_progress(node_id=node.node_id, task_id=push_id))
except Exception as e:
# 整组失败
@@ -271,7 +287,13 @@ async def api_get_material_detail_list(client, material_id: str) -> List[Materia
return validated_items
async def api_create_material_detail(client, data: MaterialDetailCreate, push_id: str) -> Dict[str, Any]:
async def api_create_material_detail(
client,
data: MaterialDetailCreate,
push_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> Dict[str, Any]:
"""
创建物资明细 POST /material_detail/create
@@ -281,7 +303,7 @@ async def api_create_material_detail(client, data: MaterialDetailCreate, push_id
push_id: 推送ID
biz_id: 业务ID
"""
request_data = {"push_id": push_id, "data": data.model_dump(mode='json')}
request_data = {"push_id": push_id, "data": data.model_dump(mode='json'), "steps": steps or []}
response = await client.request(
method="POST",
endpoint="/material/detail/create",
@@ -292,7 +314,14 @@ async def api_create_material_detail(client, data: MaterialDetailCreate, push_id
return response
async def api_update_material_detail(client, data: Dict[str, Any], push_id: str, biz_id: str):
async def api_update_material_detail(
client,
data: Dict[str, Any],
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
):
"""
更新物资明细 PUT /material_detail/update
@@ -305,7 +334,8 @@ async def api_update_material_detail(client, data: Dict[str, Any], push_id: str,
request_data = {
"push_id": push_id,
"biz_id": biz_id,
"data": data
"data": data,
"steps": steps or [],
}
response = await client.request(
method="PUT",
@@ -63,7 +63,12 @@ class PersonnelApiHandler(BaseApiHandler):
try:
request_data = {"project_id": project_id, **data}
PowerCreate.model_validate(request_data)
response = await api_create_personnel(self.api_client, request_data, push_id)
response = await api_create_personnel(
self.api_client,
request_data,
push_id,
steps=self._get_request_steps(node),
)
if self.poll_mode == "sync":
created_id = self._extract_created_id_from_response(response)
if not created_id:
@@ -111,14 +116,31 @@ class PersonnelApiHandler(BaseApiHandler):
node_updated = False
error = None
steps_payload = self._get_request_steps(node)
steps_sent = False
# 1. 基础信息更新 (PowerUpdate)
base_diff = self._get_schema_diff(PowerUpdate, data, origin_data, node.node_id)
force_steps_update = self._steps_changed(node)
base_diff = self._get_schema_diff(
PowerUpdate,
data,
origin_data,
node.node_id,
force_full_schema=force_steps_update,
)
if base_diff:
try:
update_model = PowerUpdate.model_validate(data)
push_id = self._generate_push_id()
await api_update_personnel(self.api_client, update_model.model_dump(), push_id, biz_id)
await api_update_personnel(
self.api_client,
update_model.model_dump(),
push_id,
biz_id,
steps=steps_payload if not steps_sent else [],
)
self._mark_approval_snapshot_synced(node)
steps_sent = steps_sent or bool(steps_payload)
node_updated = True
except ValidationError as e:
error = f"Validation failed: {e}"
@@ -144,7 +166,10 @@ class PersonnelApiHandler(BaseApiHandler):
plan_model.model_dump(),
push_id,
biz_id,
steps=steps_payload if not steps_sent else [],
)
self._mark_approval_snapshot_synced(node)
steps_sent = steps_sent or bool(steps_payload)
node_updated = True
except ValidationError as e:
error = f"Validation failed: {e}"
@@ -207,18 +232,31 @@ async def api_get_personnel(client, personnel_id: str) -> Dict[str, Any]:
return response.get("data", {})
async def api_create_personnel(client, data: Dict[str, Any], push_id: str) -> Dict[str, Any]:
async def api_create_personnel(
client,
data: Dict[str, Any],
push_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> Dict[str, Any]:
"""创建力能 POST /personnel/create"""
request_data = {"push_id": push_id, "data": data}
request_data = {"push_id": push_id, "data": data, "steps": steps or []}
response = await client.request(method="POST", endpoint="/personnel/create", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
return response
async def api_update_personnel(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
async def api_update_personnel(
client,
data: Dict[str, Any],
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
"""更新力能 PUT /personnel/update"""
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
response = await client.request(method="PUT", endpoint="/personnel/update", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
@@ -252,9 +290,16 @@ async def api_delete_personnel(client, personnel_id: str, push_id: str) -> None:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
async def api_update_personnel_plan(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
async def api_update_personnel_plan(
client,
data: Dict[str, Any],
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
"""更新力能计划 PUT /personnel/plan"""
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
response = await client.request(method="PUT", endpoint="/personnel/plan", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
@@ -65,7 +65,12 @@ class PersonnelDetailApiHandler(BaseApiHandler):
try:
request_data = {"personnel_id": personnel_id, **data}
PowerDetailCreate.model_validate(request_data)
response = await api_create_personnel_detail(self.api_client, request_data, push_id)
response = await api_create_personnel_detail(
self.api_client,
request_data,
push_id,
steps=self._get_request_steps(node),
)
if self.poll_mode == "sync":
created_id = self._extract_created_id_from_response(response)
if not created_id:
@@ -107,7 +112,14 @@ class PersonnelDetailApiHandler(BaseApiHandler):
origin_data = node.get_origin_data() or {}
# 使用 _get_schema_diff 检查差异并打印日志
diff_fields = self._get_schema_diff(PowerDetailUpdate, data, origin_data, node.node_id)
force_steps_update = self._steps_changed(node)
diff_fields = self._get_schema_diff(
PowerDetailUpdate,
data,
origin_data,
node.node_id,
force_full_schema=force_steps_update,
)
if not diff_fields:
from ...common.types import SyncAction
results.append(TaskResult.skipped(node_id=node.node_id, reason="No updatable fields changed"))
@@ -120,7 +132,12 @@ class PersonnelDetailApiHandler(BaseApiHandler):
continue
# 需要完整的 schema 数据(包括必填字段)
update_data = self._filter_update_data(data, origin_data, PowerDetailUpdate)
update_data = self._filter_update_data(
data,
origin_data,
PowerDetailUpdate,
force_full_schema=force_steps_update,
)
# 验证并转换为 Pydantic model
try:
@@ -135,6 +152,7 @@ class PersonnelDetailApiHandler(BaseApiHandler):
# 准备批量数据
detail_list = [model.model_dump(exclude_unset=True) for node, model in group_items]
steps = self._get_request_steps(group_items[0][0]) if group_items else []
# 使用 parent_idpersonnel_id)作为 biz_id
biz_id = personnel_id
@@ -146,9 +164,11 @@ class PersonnelDetailApiHandler(BaseApiHandler):
{"detail_list": detail_list},
push_id,
biz_id,
steps=steps,
)
# 整组成功
for node, _ in group_items:
self._mark_approval_snapshot_synced(node)
results.append(TaskResult.success(node_id=node.node_id, push_id=push_id))
except Exception as e:
# 整组失败
@@ -202,16 +222,29 @@ async def api_get_personnel_detail_list(client, personnel_id: str) -> List[Dict[
return response.get("data", [])
async def api_create_personnel_detail(client, data: Dict[str, Any], push_id: str) -> Dict[str, Any]:
request_data = {"push_id": push_id, "data": data}
async def api_create_personnel_detail(
client,
data: Dict[str, Any],
push_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> Dict[str, Any]:
request_data = {"push_id": push_id, "data": data, "steps": steps or []}
response = await client.request(method="POST", endpoint="/personnel/detail/create", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
return response
async def api_update_personnel_detail(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
async def api_update_personnel_detail(
client,
data: Dict[str, Any],
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
response = await client.request(method="PUT", endpoint="/personnel/detail/update", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
@@ -102,7 +102,13 @@ class PreparationApiHandler(BaseApiHandler):
continue
origin_data = node.get_origin_data() or {}
update_data = self._filter_update_data(node_data, origin_data, PostPreparation)
force_steps_update = self._steps_changed(node)
update_data = self._filter_update_data(
node_data,
origin_data,
PostPreparation,
force_full_schema=force_steps_update,
)
if not update_data:
reason = f"No physical diff found for preparation {node.data_id} despite strategy update request (Normalization Discrepancy)."
print(f" [WARNING] [preparation] {reason}")
@@ -123,7 +129,14 @@ class PreparationApiHandler(BaseApiHandler):
continue
try:
await api_update_preparation(self.api_client, update_model, push_id, biz_id)
await api_update_preparation(
self.api_client,
update_model,
push_id,
biz_id,
steps=self._get_request_steps(node),
)
self._mark_approval_snapshot_synced(node)
results.append(TaskResult.success(node_id=node.node_id))
except Exception as e:
results.append(TaskResult.failed(node_id=node.node_id, error=str(e)))
@@ -143,11 +156,18 @@ class PreparationApiHandler(BaseApiHandler):
# ========== 静态 API 函数 ==========
async def api_update_preparation(client, data: PostPreparation, push_id: str, biz_id: str) -> None:
async def api_update_preparation(
client,
data: PostPreparation,
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
"""
PUT /preparation_nodes - 更新里程碑数据
"""
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True)}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True), "steps": steps or []}
response = await client.request(method="PUT", endpoint="/preparation_nodes", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
@@ -98,7 +98,13 @@ class ProductionApiHandler(BaseApiHandler):
origin_data = node.get_origin_data() or {}
# 获取过滤后的更新数据
update_data = self._filter_update_data(node_data, origin_data, PostProduction)
force_steps_update = self._steps_changed(node)
update_data = self._filter_update_data(
node_data,
origin_data,
PostProduction,
force_full_schema=force_steps_update,
)
if not update_data:
reason = f"No physical diff found for production {node.data_id} despite strategy update request (Normalization Discrepancy)."
@@ -121,7 +127,14 @@ class ProductionApiHandler(BaseApiHandler):
continue
try:
await api_update_production(self.api_client, update_model, push_id, biz_id)
await api_update_production(
self.api_client,
update_model,
push_id,
biz_id,
steps=self._get_request_steps(node),
)
self._mark_approval_snapshot_synced(node)
results.append(TaskResult.success(node_id=node.node_id))
except Exception as e:
results.append(TaskResult.failed(node_id=node.node_id, error=str(e)))
@@ -150,7 +163,14 @@ class ProductionApiHandler(BaseApiHandler):
# ========== 静态 API 函数 ==========
async def api_update_production(client, data: PostProduction, push_id: str, biz_id: str) -> None:
async def api_update_production(
client,
data: PostProduction,
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
"""
PUT /production - 更新投产节点数据
@@ -160,7 +180,7 @@ async def api_update_production(client, data: PostProduction, push_id: str, biz_
push_id: 推送ID
biz_id: 业务ID
"""
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True)}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True), "steps": steps or []}
response = await client.request(method="PUT", endpoint="/production_nodes", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
@@ -241,22 +241,41 @@ class ProjectApiHandler(BaseApiHandler):
node_updated = False
error = None
force_steps_update = self._steps_changed(node)
steps_payload = self._get_request_steps(node)
steps_sent = False
allow_force_steps = True
# 遍历所有更新 schema,检查差异并更新
for schema, api_func in update_configs:
update_data = self._get_schema_diff(schema, data, original_data, node.node_id)
update_data = self._get_schema_diff(
schema,
data,
original_data,
node.node_id,
force_full_schema=force_steps_update and allow_force_steps,
)
if not update_data:
continue
# 执行更新(差异已由 _get_schema_diff 打印)
try:
full_update_data = self._filter_update_data(data, original_data, schema)
full_update_data = self._filter_update_data(
data,
original_data,
schema,
force_full_schema=force_steps_update and allow_force_steps,
)
await self._submit_update_and_wait(
api_func=api_func,
data=full_update_data,
biz_id=biz_id,
steps=steps_payload if not steps_sent else [],
)
node_updated = True
steps_sent = steps_sent or bool(steps_payload)
allow_force_steps = False
self._mark_approval_snapshot_synced(node)
except Exception as e:
error = str(e)
break
@@ -285,9 +304,9 @@ class ProjectApiHandler(BaseApiHandler):
"""轮询异步任务状态"""
return await super().poll_tasks(task_ids)
async def _submit_update_and_wait(self, *, api_func, data: Dict[str, Any], biz_id: str) -> None:
async def _submit_update_and_wait(self, *, api_func, data: Dict[str, Any], biz_id: str, steps: List[Dict[str, Any]] | None = None) -> None:
push_id = self._generate_push_id()
await api_func(self.api_client, data, push_id, biz_id)
await api_func(self.api_client, data, push_id, biz_id, steps=steps or [])
max_attempts = 20
poll_interval = 0.2
@@ -400,46 +419,46 @@ async def api_get_project_all_info(client, project_id: str) -> Dict[str, Any]:
return validated.model_dump()
async def api_update_project_base_info(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
async def api_update_project_base_info(client, data: Dict[str, Any], push_id: str, biz_id: str, *, steps: List[Dict[str, Any]] | None = None) -> None:
"""PUT /project - 更新项目基本信息"""
ProjectBaseInfoUpdate.model_validate(data)
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
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:
async def api_update_project_key_constraints(client, data: Dict[str, Any], push_id: str, biz_id: str, *, steps: List[Dict[str, Any]] | None = None) -> None:
"""PUT /project/key_constraints - 更新关键节点"""
ProjectKeyConstraints.model_validate(data)
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
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:
async def api_update_project_complete_investment(client, data: Dict[str, Any], push_id: str, biz_id: str, *, steps: List[Dict[str, Any]] | None = None) -> None:
"""PUT /project/complete_investment - 更新完成投资"""
ProjectCompleteInvestment.model_validate(data)
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
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:
async def api_update_project_investment_decision(client, data: Dict[str, Any], push_id: str, biz_id: str, *, steps: List[Dict[str, Any]] | None = None) -> None:
"""PUT /project/investment_decision - 更新投资决策"""
ProjectInvestmentDecision.model_validate(data)
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
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:
async def api_update_project_dynamic_investment(client, data: Dict[str, Any], push_id: str, biz_id: str, *, steps: List[Dict[str, Any]] | None = None) -> None:
"""PUT /project/dynamic_investment - 更新动态投资"""
ProjectDynamicInvestment.model_validate(data)
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
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')}")
@@ -447,10 +466,10 @@ async def api_update_project_dynamic_investment(client, data: Dict[str, Any], pu
async def api_update_project_settlement_investment(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
async def api_update_project_settlement_investment(client, data: Dict[str, Any], push_id: str, biz_id: str, *, steps: List[Dict[str, Any]] | None = None) -> None:
"""PUT /project/settlement_investment - 更新结算投资"""
ProjectSettlementInvestment.model_validate(data)
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
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')}")
@@ -171,7 +171,9 @@ class ProjectDetailApiHandler(BaseApiHandler):
if origin_payload is None:
origin_payload = {}
if not force_execute and not self._payload_changed(schema, new_payload, origin_payload):
force_steps_update = self._steps_changed(node)
steps_payload = self._get_request_steps(node)
if not force_execute and not force_steps_update and not self._payload_changed(schema, new_payload, origin_payload):
reason = f"No physical diff found for project_detail {key} despite strategy update request (Normalization Discrepancy)."
print(f" [WARNING] {reason}")
# 记录为跳过
@@ -192,7 +194,7 @@ class ProjectDetailApiHandler(BaseApiHandler):
}
try:
await api_func(self.api_client, new_payload, push_id, biz_id)
await api_func(self.api_client, new_payload, push_id, biz_id, steps=steps_payload)
results.append(
TaskResult.in_progress(
node_id=node.node_id,
@@ -202,6 +204,7 @@ class ProjectDetailApiHandler(BaseApiHandler):
key=str(key),
)
)
self._mark_approval_snapshot_synced(node)
except Exception as e:
self._pending_push_context.pop(push_id, None)
results.append(TaskResult.failed(node_id=node.node_id, error=str(e)))
@@ -501,55 +504,55 @@ class ProjectDetailApiHandler(BaseApiHandler):
# ========== 静态 API 函数(可抽成 SDK ==========
async def api_update_project_plan_construction(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
async def api_update_project_plan_construction(client, data: Dict[str, Any], push_id: str, biz_id: str, *, steps: List[Dict[str, Any]] | None = None) -> None:
"""PUT /project/plan_construction - 更新计划施工"""
ProjectPlanConstruction.model_validate(data)
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
response = await client.request(method="PUT", endpoint="/project/plan_construction", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
async def api_update_project_actual_construction(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
async def api_update_project_actual_construction(client, data: Dict[str, Any], push_id: str, biz_id: str, *, steps: List[Dict[str, Any]] | None = None) -> None:
"""PUT /project/actual_construction - 更新实际施工"""
ProjectActualConstruction.model_validate(data)
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
response = await client.request(method="PUT", endpoint="/project/actual_construction", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
async def api_update_project_ensure_capacity(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
async def api_update_project_ensure_capacity(client, data: Dict[str, Any], push_id: str, biz_id: str, *, steps: List[Dict[str, Any]] | None = None) -> None:
"""PUT /project/ensure_capacity - 更新确保产能"""
ProjectEnsureCapacity.model_validate(data)
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
response = await client.request(method="PUT", endpoint="/project/ensure_capacity", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
async def api_update_project_climb_capacity(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
async def api_update_project_climb_capacity(client, data: Dict[str, Any], push_id: str, biz_id: str, *, steps: List[Dict[str, Any]] | None = None) -> None:
"""PUT /project/climb_capacity - 更新登高产能"""
ProjectClimbCapacity.model_validate(data)
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
response = await client.request(method="PUT", endpoint="/project/climb_capacity", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
async def api_update_project_challenge_capacity(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
async def api_update_project_challenge_capacity(client, data: Dict[str, Any], push_id: str, biz_id: str, *, steps: List[Dict[str, Any]] | None = None) -> None:
"""PUT /project/challenge_capacity - 更新揭榜产能"""
ProjectChallengeCapacity.model_validate(data)
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
response = await client.request(method="PUT", endpoint="/project/challenge_capacity", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
async def api_update_project_production_capacity(client, data: Dict[str, Any], push_id: str, biz_id: str) -> None:
async def api_update_project_production_capacity(client, data: Dict[str, Any], push_id: str, biz_id: str, *, steps: List[Dict[str, Any]] | None = None) -> None:
"""PUT /project/production_capacity - 更新投产数据"""
ProjectProductionCapacity.model_validate(data)
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data, "steps": steps or []}
response = await client.request(method="PUT", endpoint="/project/production_capacity", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
@@ -17,7 +17,7 @@ class SupplierSyncStrategy(DefaultSyncStrategy[SupplierDetailResponse]):
default_config = StrategyConfig(
auto_bind=True,
auto_bind_fields=["credit_code"],
auto_bind_fields=["code"],
depend_fields={},
local_orphan_action=OrphanAction.NONE,
remote_orphan_action=OrphanAction.CREATE_LOCAL,
@@ -29,6 +29,7 @@ class SupplierSyncStrategy(DefaultSyncStrategy[SupplierDetailResponse]):
source_node,
target_node,
resolved_data=None,
data_id_map=None,
) -> bool:
"""Supplier 更新忽略 id 字段差异。"""
if resolved_data is None:
+24 -4
View File
@@ -93,7 +93,13 @@ class UnitsApiHandler(BaseApiHandler):
continue
origin_data = node.get_origin_data() or {}
update_data = self._filter_update_data(node_data, origin_data, PostGeneratorUnits)
force_steps_update = self._steps_changed(node)
update_data = self._filter_update_data(
node_data,
origin_data,
PostGeneratorUnits,
force_full_schema=force_steps_update,
)
if not update_data:
reason = f"No physical diff found for units {node.data_id} despite strategy update request (Normalization Discrepancy)."
print(f" [WARNING] [units] {reason}")
@@ -114,7 +120,14 @@ class UnitsApiHandler(BaseApiHandler):
continue
try:
await api_update_units(self.api_client, update_model, push_id, biz_id)
await api_update_units(
self.api_client,
update_model,
push_id,
biz_id,
steps=self._get_request_steps(node),
)
self._mark_approval_snapshot_synced(node)
results.append(TaskResult.success(node_id=node.node_id))
except Exception as e:
results.append(TaskResult.failed(node_id=node.node_id, error=str(e)))
@@ -143,7 +156,14 @@ class UnitsApiHandler(BaseApiHandler):
# ========== 静态 API 函数 ==========
async def api_update_units(client, data: PostGeneratorUnits, push_id: str, biz_id: str) -> None:
async def api_update_units(
client,
data: PostGeneratorUnits,
push_id: str,
biz_id: str,
*,
steps: List[Dict[str, Any]] | None = None,
) -> None:
"""
PUT /units - 更新机组数据
@@ -153,7 +173,7 @@ async def api_update_units(client, data: PostGeneratorUnits, push_id: str, biz_i
push_id: 推送ID
biz_id: 业务ID
"""
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True)}
request_data = {"push_id": push_id, "biz_id": biz_id, "data": data.model_dump(exclude_unset=True), "steps": steps or []}
response = await client.request(method="PUT", endpoint="/units", data=request_data)
if response.get("code") != 200:
raise Exception(f"API error: {response.get('message', 'Unknown error')}")
@@ -1,5 +1,6 @@
from __future__ import annotations
import copy
import logging
from typing import TYPE_CHECKING, List
@@ -77,6 +78,8 @@ async def add_create_action(
binding_status=BindingStatus.NORMAL,
action=SyncAction.CREATE,
)
if src_node.context:
new_node.context.update(copy.deepcopy(src_node.context))
await to_collection.add(new_node)
if source_is_local:
@@ -1,5 +1,6 @@
from __future__ import annotations
import copy
import logging
from typing import TYPE_CHECKING, Callable, List, Optional
@@ -20,6 +21,17 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
def _extract_snapshot_steps(snapshot) -> List:
if not isinstance(snapshot, dict):
return []
steps = snapshot.get("steps")
return copy.deepcopy(steps) if isinstance(steps, list) else []
def _steps_payload_changed(source_snapshot, target_snapshot) -> bool:
return _extract_snapshot_steps(source_snapshot) != _extract_snapshot_steps(target_snapshot)
def _safe_repr(value, *, max_len: int = 120) -> str:
text = repr(value)
if len(text) <= max_len:
@@ -151,6 +163,17 @@ async def add_update_action(
update_id_resolve_ok = data is not None
has_diff = False
diff_descriptions: List[str] = []
source_snapshot = src_node.context.get("approval_snapshot") if isinstance(src_node.context, dict) else None
target_snapshot = target_node.context.get("approval_snapshot") if isinstance(target_node.context, dict) else None
steps_changed = False
if isinstance(source_snapshot, dict):
steps_changed = _steps_payload_changed(source_snapshot, target_snapshot)
pending_snapshot = copy.deepcopy(source_snapshot)
pending_snapshot["steps_changed"] = steps_changed
target_node.context["pending_approval_snapshot"] = pending_snapshot
else:
target_node.context.pop("pending_approval_snapshot", None)
if update_id_resolve_ok:
try:
should_update = needs_update_check(src_node, target_node, resolved_data=data, data_id_map=data_id_map)
@@ -163,6 +186,10 @@ async def add_update_action(
if has_diff:
diff_descriptions = _collect_diff_descriptions(data, target_node.get_data(), data_id_map=data_id_map or {})
if steps_changed:
has_diff = True
diff_descriptions.append("steps: approval flow changed")
if target_has_data_id and update_id_resolve_ok and not has_diff:
continue