Files
cmautobuy/client/src/admin_gateway.py
T

143 lines
4.2 KiB
Python

"""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:
"""幂等提交失败、取消或人工处理结果。"""