Files
cmautobuy/client/src/admin_gateway.py
T

165 lines
4.8 KiB
Python
Raw Normal View History

"""Client 访问 Admin 的稳定边界和简单数据对象。
2026-08-06 17:57:49 +08:00
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:
2026-08-06 17:57:49 +08:00
"""Client 身份和可选名称,不保存访问令牌。"""
client_id: str
2026-08-06 17:57:49 +08:00
name: str = ""
def __post_init__(self) -> None:
if not self.client_id.strip():
raise ValueError("client_id 不能为空")
2026-08-06 17:57:49 +08:00
if len(self.name.strip()) > 50:
raise ValueError("client.name 最多 50 个字")
@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 领取任务时声明的设备与执行能力。"""
2026-08-06 17:57:49 +08:00
device: Optional[AndroidDeviceInfo] = None
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
2026-08-06 17:57:49 +08:00
@dataclass(frozen=True)
class RegistrationReceipt:
"""Admin 已登记当前 Client 的确认。"""
registered: bool
client_id: str
registered_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 {})
2026-08-06 17:57:49 +08:00
class ClientRegistrationGateway(ABC):
"""设置页只依赖登记能力,不依赖任务领取和提交。"""
@abstractmethod
def register_client(
self, client: ClientInfo, capabilities: ClaimCapabilities
) -> RegistrationReceipt:
"""幂等登记或更新当前 Client。"""
class AdminGateway(ClientRegistrationGateway):
"""完整 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:
"""幂等提交失败、取消或人工处理结果。"""