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