2026-08-06 16:21:39 +08:00
|
|
|
"""Client 任务数据库使用的枚举和简单数据对象。
|
|
|
|
|
|
|
|
|
|
本文件不依赖 Qt、Admin 客户端或 uiautomator2,只表达数据含义。
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
from enum import Enum
|
|
|
|
|
from typing import Any, Dict, Mapping, Optional
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TaskType(str, Enum):
|
|
|
|
|
"""PDD 任务类型。"""
|
|
|
|
|
|
|
|
|
|
COLLECT = "collect"
|
|
|
|
|
PURCHASE = "purchase"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TaskStatus(str, Enum):
|
|
|
|
|
"""PDD 任务本地状态。"""
|
|
|
|
|
|
|
|
|
|
CLAIMED = "claimed"
|
|
|
|
|
RUNNING = "running"
|
|
|
|
|
RESULT_PENDING = "result_pending"
|
|
|
|
|
RETRY_WAIT = "retry_wait"
|
|
|
|
|
MANUAL_REVIEW = "manual_review"
|
|
|
|
|
SUCCEEDED = "succeeded"
|
|
|
|
|
FAILED = "failed"
|
|
|
|
|
CANCELLED = "cancelled"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class RunStatus(str, Enum):
|
|
|
|
|
"""单次任务执行状态。"""
|
|
|
|
|
|
|
|
|
|
RUNNING = "running"
|
|
|
|
|
SUCCEEDED = "succeeded"
|
|
|
|
|
FAILED = "failed"
|
|
|
|
|
CANCELLED = "cancelled"
|
|
|
|
|
MANUAL_REVIEW = "manual_review"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OutboxEventType(str, Enum):
|
|
|
|
|
"""等待提交 Admin 的事件类型。"""
|
|
|
|
|
|
|
|
|
|
COLLECT_RESULT = "collect_result"
|
|
|
|
|
PURCHASE_RESULT = "purchase_result"
|
|
|
|
|
TASK_FAILURE = "task_failure"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class OutboxStatus(str, Enum):
|
|
|
|
|
"""Outbox 事件发送状态。"""
|
|
|
|
|
|
|
|
|
|
PENDING = "pending"
|
|
|
|
|
SENDING = "sending"
|
|
|
|
|
SENT = "sent"
|
|
|
|
|
FAILED = "failed"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class NewClaimedTask:
|
|
|
|
|
"""刚从 Admin 领取、准备写入本地数据库的任务。"""
|
|
|
|
|
|
|
|
|
|
remote_task_id: str
|
|
|
|
|
task_type: TaskType
|
|
|
|
|
goods_url: str
|
|
|
|
|
goods_id: Optional[str] = None
|
|
|
|
|
title: Optional[str] = None
|
|
|
|
|
target_color: Optional[str] = None
|
|
|
|
|
target_size: Optional[str] = None
|
|
|
|
|
price_cent: Optional[int] = None
|
|
|
|
|
quantity: Optional[int] = None
|
|
|
|
|
priority: int = 0
|
|
|
|
|
version: int = 1
|
|
|
|
|
admin_payload: Mapping[str, Any] = field(default_factory=dict)
|
|
|
|
|
|
|
|
|
|
def __post_init__(self) -> None:
|
|
|
|
|
if not isinstance(self.task_type, TaskType):
|
|
|
|
|
raise ValueError("task_type 必须是 TaskType")
|
|
|
|
|
if not self.remote_task_id.strip():
|
|
|
|
|
raise ValueError("remote_task_id 不能为空")
|
|
|
|
|
if not self.goods_url.strip():
|
|
|
|
|
raise ValueError("goods_url 不能为空")
|
|
|
|
|
if self.price_cent is not None and self.price_cent < 0:
|
|
|
|
|
raise ValueError("price_cent 不能小于 0")
|
|
|
|
|
if self.quantity is not None and self.quantity <= 0:
|
|
|
|
|
raise ValueError("quantity 必须大于 0")
|
|
|
|
|
if self.version <= 0:
|
|
|
|
|
raise ValueError("version 必须大于 0")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class TaskFilters:
|
|
|
|
|
"""本地任务列表的可选筛选条件。"""
|
|
|
|
|
|
|
|
|
|
task_type: Optional[TaskType] = None
|
|
|
|
|
status: Optional[TaskStatus] = None
|
|
|
|
|
keyword: str = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class TaskSummary:
|
|
|
|
|
"""任务表格使用的轻量数据,不包含完整 JSON。"""
|
|
|
|
|
|
|
|
|
|
id: int
|
|
|
|
|
remote_task_id: str
|
|
|
|
|
task_type: TaskType
|
|
|
|
|
goods_id: Optional[str]
|
|
|
|
|
title: Optional[str]
|
|
|
|
|
target_color: Optional[str]
|
|
|
|
|
target_size: Optional[str]
|
|
|
|
|
price_cent: Optional[int]
|
|
|
|
|
quantity: Optional[int]
|
|
|
|
|
status: TaskStatus
|
|
|
|
|
updated_at: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class TaskDetail:
|
|
|
|
|
"""一条 PDD 任务的完整本地记录。"""
|
|
|
|
|
|
|
|
|
|
id: int
|
|
|
|
|
remote_task_id: str
|
|
|
|
|
task_type: TaskType
|
|
|
|
|
goods_id: Optional[str]
|
|
|
|
|
goods_url: str
|
|
|
|
|
title: Optional[str]
|
|
|
|
|
target_color: Optional[str]
|
|
|
|
|
target_size: Optional[str]
|
|
|
|
|
price_cent: Optional[int]
|
|
|
|
|
quantity: Optional[int]
|
|
|
|
|
status: TaskStatus
|
|
|
|
|
current_step: Optional[str]
|
|
|
|
|
priority: int
|
|
|
|
|
version: int
|
|
|
|
|
admin_payload: Dict[str, Any]
|
|
|
|
|
pdd_data: Optional[Dict[str, Any]]
|
|
|
|
|
retry_count: int
|
|
|
|
|
last_error_code: Optional[str]
|
|
|
|
|
last_error_message: Optional[str]
|
|
|
|
|
received_at: str
|
|
|
|
|
started_at: Optional[str]
|
|
|
|
|
finished_at: Optional[str]
|
|
|
|
|
created_at: str
|
|
|
|
|
updated_at: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class TaskRunRecord:
|
|
|
|
|
"""一次任务执行的数据库记录。"""
|
|
|
|
|
|
|
|
|
|
id: int
|
|
|
|
|
task_id: int
|
|
|
|
|
attempt_id: str
|
|
|
|
|
attempt_no: int
|
|
|
|
|
device_address: str
|
|
|
|
|
run_status: RunStatus
|
|
|
|
|
current_step: Optional[str]
|
|
|
|
|
started_at: str
|
|
|
|
|
finished_at: Optional[str]
|
|
|
|
|
irreversible_action_at: Optional[str]
|
|
|
|
|
order_submitted_at: Optional[str]
|
|
|
|
|
error_code: Optional[str]
|
|
|
|
|
error_message: Optional[str]
|
|
|
|
|
diagnostics_json: Optional[Dict[str, Any]]
|
|
|
|
|
artifact_directory: Optional[str]
|
|
|
|
|
created_at: str
|
|
|
|
|
updated_at: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class OutboxEventRecord:
|
|
|
|
|
"""一条等待提交 Admin 的可靠事件记录。"""
|
|
|
|
|
|
|
|
|
|
id: int
|
|
|
|
|
task_id: int
|
|
|
|
|
event_type: OutboxEventType
|
|
|
|
|
idempotency_key: str
|
|
|
|
|
payload_json: Dict[str, Any]
|
|
|
|
|
status: OutboxStatus
|
|
|
|
|
attempt_count: int
|
|
|
|
|
next_retry_at: Optional[str]
|
|
|
|
|
last_error: Optional[str]
|
|
|
|
|
created_at: str
|
|
|
|
|
updated_at: str
|
|
|
|
|
sent_at: Optional[str]
|
|
|
|
|
|
|
|
|
|
|
2026-08-07 17:38:29 +08:00
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class StartedTaskRun:
|
|
|
|
|
"""已进入执行状态的一次采集尝试。"""
|
|
|
|
|
|
|
|
|
|
task: TaskDetail
|
|
|
|
|
attempt_id: str
|
|
|
|
|
attempt_no: int
|
|
|
|
|
|
|
|
|
|
|
2026-08-06 16:21:39 +08:00
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class AppSettingRecord:
|
|
|
|
|
"""一条非敏感应用设置。"""
|
|
|
|
|
|
|
|
|
|
setting_key: str
|
|
|
|
|
value: Any
|
|
|
|
|
updated_at: str
|