2026-08-06 16:21:39 +08:00
|
|
|
"""Client 任务数据库使用的枚举和简单数据对象。
|
|
|
|
|
|
|
|
|
|
本文件不依赖 Qt、Admin 客户端或 uiautomator2,只表达数据含义。
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from dataclasses import dataclass, field
|
|
|
|
|
from enum import Enum
|
2026-08-11 10:42:51 +08:00
|
|
|
from typing import Any, Dict, Mapping, Optional, Tuple
|
2026-08-06 16:21:39 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-08-10 16:35:38 +08:00
|
|
|
execution_mode: str = "dry_run"
|
2026-08-06 16:21:39 +08:00
|
|
|
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 不能为空")
|
2026-08-10 16:35:38 +08:00
|
|
|
if self.execution_mode not in {"dry_run", "live"}:
|
|
|
|
|
raise ValueError("execution_mode 只允许 dry_run 或 live")
|
|
|
|
|
if (
|
|
|
|
|
self.task_type is not TaskType.PURCHASE
|
|
|
|
|
and self.execution_mode != "dry_run"
|
|
|
|
|
):
|
|
|
|
|
raise ValueError("只有采购任务允许 execution_mode=live")
|
2026-08-06 16:21:39 +08:00
|
|
|
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 = ""
|
|
|
|
|
|
|
|
|
|
|
2026-08-11 10:42:51 +08:00
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class TaskRerunPlan:
|
|
|
|
|
"""批量重新执行预检结果;界面只展示计数,不自行判断安全条件。"""
|
|
|
|
|
|
|
|
|
|
target_type: TaskType
|
|
|
|
|
selected_count: int
|
|
|
|
|
eligible_task_ids: Tuple[str, ...]
|
|
|
|
|
filtered_task_ids: Tuple[str, ...]
|
|
|
|
|
blocked: Tuple[Tuple[str, str], ...]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
|
|
|
class TaskRerunBatch:
|
|
|
|
|
"""交给固定设备线程串行处理的批量命令。"""
|
|
|
|
|
|
|
|
|
|
target_type: TaskType
|
|
|
|
|
task_ids: Tuple[str, ...]
|
|
|
|
|
|
|
|
|
|
|
2026-08-06 16:21:39 +08:00
|
|
|
@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
|
2026-08-11 11:17:44 +08:00
|
|
|
shop_name: Optional[str] = None
|
|
|
|
|
latest_run_status: Optional[RunStatus] = None
|
|
|
|
|
duration_seconds: Optional[int] = None
|
2026-08-11 16:46:49 +08:00
|
|
|
order_no: Optional[str] = None
|
2026-08-06 16:21:39 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@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
|
2026-08-10 16:35:38 +08:00
|
|
|
execution_mode: str = "dry_run"
|
2026-08-06 16:21:39 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@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]]
|
2026-08-09 21:44:29 +08:00
|
|
|
result_data: Optional[Dict[str, Any]]
|
2026-08-06 16:21:39 +08:00
|
|
|
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:
|
2026-08-09 23:39:37 +08:00
|
|
|
"""已进入执行状态的一次采集或采购尝试。"""
|
2026-08-07 17:38:29 +08:00
|
|
|
|
|
|
|
|
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
|