"""Client 任务数据库使用的枚举和简单数据对象。 本文件不依赖 Qt、Admin 客户端或 uiautomator2,只表达数据含义。 """ from dataclasses import dataclass, field from enum import Enum from typing import Any, Dict, Mapping, Optional, Tuple 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 execution_mode: str = "dry_run" 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.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") 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 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, ...] @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 shop_name: Optional[str] = None latest_run_status: Optional[RunStatus] = None duration_seconds: Optional[int] = None order_no: Optional[str] = None @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 execution_mode: str = "dry_run" @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]] result_data: 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] @dataclass(frozen=True) class StartedTaskRun: """已进入执行状态的一次采集或采购尝试。""" task: TaskDetail attempt_id: str attempt_no: int @dataclass(frozen=True) class AppSettingRecord: """一条非敏感应用设置。""" setting_key: str value: Any updated_at: str