"""Client 访问 Admin 的稳定边界和简单数据对象。 AdminGateway 只有领取任务、提交成功结果、提交失败结果三个业务方法。 业务层不应直接依赖 HTTP 请求或 Mock 的内部实现。 """ from abc import ABC, abstractmethod from dataclasses import dataclass, field from typing import Any, Mapping, Optional, Tuple from .task_models import TaskType @dataclass(frozen=True) class ClientInfo: """发起领取请求的 Client 身份,不保存访问令牌。""" client_id: str def __post_init__(self) -> None: if not self.client_id.strip(): raise ValueError("client_id 不能为空") @dataclass(frozen=True) class AndroidDeviceInfo: """领取任务时上报的 Android 设备信息。""" address: str platform: str = "android" pdd_package: str = "com.xunmeng.pinduoduo" def __post_init__(self) -> None: if not self.address.strip(): raise ValueError("设备地址不能为空") if self.platform != "android": raise ValueError("当前只支持 android 平台") if not self.pdd_package.strip(): raise ValueError("PDD 包名不能为空") @dataclass(frozen=True) class ClaimCapabilities: """Client 领取任务时声明的设备与执行能力。""" device: AndroidDeviceInfo supported_types: Tuple[TaskType, ...] = ( TaskType.COLLECT, TaskType.PURCHASE, ) purchase_mode: str = "dry_run" schema_versions: Tuple[int, ...] = (1,) def __post_init__(self) -> None: if not self.supported_types: raise ValueError("supported_types 不能为空") if any(not isinstance(value, TaskType) for value in self.supported_types): raise ValueError("supported_types 必须使用 TaskType") if self.purchase_mode not in {"dry_run", "live"}: raise ValueError("purchase_mode 只能是 dry_run 或 live") if not self.schema_versions or any( version <= 0 for version in self.schema_versions ): raise ValueError("schema_versions 必须是正整数") @dataclass(frozen=True) class AdminTask: """Admin 派发给 Client 的一个任务。""" task_id: str task_type: TaskType version: int priority: int payload: Mapping[str, Any] = field(default_factory=dict) created_at: str = "" updated_at: str = "" def __post_init__(self) -> None: if not self.task_id.strip(): raise ValueError("task_id 不能为空") if not isinstance(self.task_type, TaskType): raise ValueError("task_type 必须使用 TaskType") if self.version <= 0: raise ValueError("version 必须大于 0") if not isinstance(self.payload, Mapping): raise ValueError("payload 必须是对象") @dataclass(frozen=True) class SubmissionReceipt: """Admin 已接收并保存一次提交的确认。""" accepted: bool result_id: str accepted_at: str class AdminGatewayError(RuntimeError): """带稳定错误代码和可重试标志的 Admin 边界错误。""" def __init__( self, code: str, message: str, retryable: bool, request_id: str = "", details: Optional[Mapping[str, Any]] = None, ): super().__init__(message) self.code = code self.retryable = retryable self.request_id = request_id self.details = dict(details or {}) class AdminGateway(ABC): """Admin 边界;不得增加状态查询、心跳或租约方法。""" @abstractmethod def claim_next( self, client: ClientInfo, capabilities: ClaimCapabilities ) -> Optional[AdminTask]: """领取至多一个分配给当前 Client 的任务。""" @abstractmethod def submit_result( self, task_id: str, idempotency_key: str, result: Mapping[str, Any], ) -> SubmissionReceipt: """幂等提交采集或采购成功结果。""" @abstractmethod def submit_failure( self, task_id: str, idempotency_key: str, failure: Mapping[str, Any], ) -> SubmissionReceipt: """幂等提交失败、取消或人工处理结果。"""