139 lines
4.7 KiB
Python
139 lines
4.7 KiB
Python
"""不可逆阶段中断后的只读采购结果核对。"""
|
|||
|
|
|
||
|
|
from dataclasses import dataclass
|
||
|
|
from typing import Callable, Mapping
|
||
|
|
|
||
|
|
from .pdd_purchase_reconcile_adapter import (
|
||
|
|
PddPurchaseReconcileAdapter,
|
||
|
|
PurchaseReconcileObservation,
|
||
|
|
PurchaseReconcileQuery,
|
||
|
|
)
|
||
|
|
from .task_models import TaskDetail
|
||
|
|
from .task_repository import TaskRepository
|
||
|
|
|
||
|
|
|
||
|
|
PurchaseReconcileFactory = Callable[
|
||
|
|
[str, Callable[[], bool]], PddPurchaseReconcileAdapter
|
||
|
|
]
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass(frozen=True)
|
||
|
|
class PurchaseReconcileOutcome:
|
||
|
|
"""只读核对的简短结果。"""
|
||
|
|
|
||
|
|
kind: str
|
||
|
|
message: str
|
||
|
|
task_id: str = ""
|
||
|
|
|
||
|
|
|
||
|
|
class PurchaseReconcileService:
|
||
|
|
"""只读核对一条已进入不可逆阶段的采购运行。"""
|
||
|
|
|
||
|
|
def __init__(
|
||
|
|
self,
|
||
|
|
repository: TaskRepository,
|
||
|
|
device_address: str,
|
||
|
|
adapter_factory: PurchaseReconcileFactory,
|
||
|
|
*,
|
||
|
|
cancelled: Callable[[], bool] = lambda: False,
|
||
|
|
) -> None:
|
||
|
|
self._repository = repository
|
||
|
|
self._device_address = str(device_address or "").strip()
|
||
|
|
self._factory = adapter_factory
|
||
|
|
self._cancelled = cancelled
|
||
|
|
|
||
|
|
def execute_selected(
|
||
|
|
self, remote_task_id: str
|
||
|
|
) -> PurchaseReconcileOutcome:
|
||
|
|
"""执行一次只读核对,任何结果都交给人工最终确认。"""
|
||
|
|
|
||
|
|
if not self._device_address:
|
||
|
|
raise ValueError("请先在设置页选择并保存 Android 设备")
|
||
|
|
if self._cancelled():
|
||
|
|
return PurchaseReconcileOutcome(
|
||
|
|
"cancelled", "采购结果核对已取消", remote_task_id
|
||
|
|
)
|
||
|
|
task = self._repository.get_task(remote_task_id)
|
||
|
|
run = self._repository.latest_task_run(remote_task_id)
|
||
|
|
if task is None or run is None:
|
||
|
|
raise ValueError(f"任务 {remote_task_id} 或执行记录不存在")
|
||
|
|
if run.irreversible_action_at is None:
|
||
|
|
raise ValueError("未进入不可逆阶段,不应启动订单核对")
|
||
|
|
|
||
|
|
query = self._query(task, run.irreversible_action_at)
|
||
|
|
adapter = None
|
||
|
|
close_error = ""
|
||
|
|
try:
|
||
|
|
adapter = self._factory(self._device_address, self._cancelled)
|
||
|
|
observation = adapter.read_order_match(query)
|
||
|
|
if not isinstance(observation, PurchaseReconcileObservation):
|
||
|
|
raise TypeError("采购核对 Adapter 返回值无效")
|
||
|
|
except Exception as exc:
|
||
|
|
observation = PurchaseReconcileObservation(
|
||
|
|
"unknown", diagnostics={"error": str(exc)}
|
||
|
|
)
|
||
|
|
finally:
|
||
|
|
if adapter is not None:
|
||
|
|
try:
|
||
|
|
adapter.close()
|
||
|
|
except Exception as exc:
|
||
|
|
close_error = str(exc)
|
||
|
|
|
||
|
|
diagnostics = dict(observation.diagnostics)
|
||
|
|
diagnostics.update(
|
||
|
|
{
|
||
|
|
"order_no": observation.order_no,
|
||
|
|
"ordered_at": observation.ordered_at,
|
||
|
|
"mode": "reconcile_only",
|
||
|
|
}
|
||
|
|
)
|
||
|
|
if close_error:
|
||
|
|
diagnostics["close_error"] = close_error
|
||
|
|
self._repository.save_purchase_reconciliation(
|
||
|
|
remote_task_id,
|
||
|
|
run.attempt_id,
|
||
|
|
observation.match_status,
|
||
|
|
diagnostics,
|
||
|
|
)
|
||
|
|
if observation.match_status == "matched":
|
||
|
|
message = (
|
||
|
|
f"任务 {remote_task_id} 仅核对到唯一候选订单;"
|
||
|
|
"请人工确认,程序不会重新下单"
|
||
|
|
)
|
||
|
|
else:
|
||
|
|
message = (
|
||
|
|
f"任务 {remote_task_id} 核对结果不确定;"
|
||
|
|
"需人工处理,程序不会重新下单"
|
||
|
|
)
|
||
|
|
return PurchaseReconcileOutcome(
|
||
|
|
"manual_review", message, remote_task_id
|
||
|
|
)
|
||
|
|
|
||
|
|
@staticmethod
|
||
|
|
def _query(
|
||
|
|
task: TaskDetail, irreversible_action_at: str
|
||
|
|
) -> PurchaseReconcileQuery:
|
||
|
|
payload_root = task.admin_payload
|
||
|
|
payload = payload_root.get("payload")
|
||
|
|
if not isinstance(payload, Mapping):
|
||
|
|
raise ValueError("采购任务缺少 payload")
|
||
|
|
options = payload.get("options")
|
||
|
|
if not isinstance(options, Mapping) or not options:
|
||
|
|
raise ValueError("采购任务缺少 options")
|
||
|
|
goods_id = str(payload.get("goods_id") or "").strip()
|
||
|
|
quantity = payload.get("quantity")
|
||
|
|
if not goods_id:
|
||
|
|
raise ValueError("采购任务缺少 goods_id")
|
||
|
|
if (
|
||
|
|
isinstance(quantity, bool)
|
||
|
|
or not isinstance(quantity, int)
|
||
|
|
or quantity <= 0
|
||
|
|
):
|
||
|
|
raise ValueError("采购任务缺少有效 quantity")
|
||
|
|
return PurchaseReconcileQuery(
|
||
|
|
goods_id=goods_id,
|
||
|
|
options={str(k): str(v) for k, v in options.items()},
|
||
|
|
quantity=quantity,
|
||
|
|
irreversible_action_at=irreversible_action_at,
|
||
|
|
)
|