feat: 执行并提交 PDD 采集任务 (#32)
This commit is contained in:
+46
-46
@@ -13,7 +13,7 @@
|
||||
- 窗口关闭时要断开信号并置标志位,否则迟到的后台结果会访问
|
||||
已经销毁的控件、直接崩溃。做法见同文档 §5.2。
|
||||
- 数据库读写走 Repository,**不要在这里拼业务 SQL**。
|
||||
- “获取任务”会真的去操作手机、可能下单;“搜索”只读本地数据库。
|
||||
- “获取任务”会真的去操作手机采集商品;“搜索”只读本地数据库。
|
||||
两者必须分开,不得共用入口。
|
||||
- 普通成功不弹窗,更新界面即可;可恢复错误用 `InfoBar`
|
||||
(模板见 `docs/client/05-ui-specification.md` §9.1);
|
||||
@@ -28,11 +28,10 @@ from qfluentwidgets import InfoBar, InfoBarPosition
|
||||
from .admin_gateway import (
|
||||
AdminGatewayError,
|
||||
AdminTask,
|
||||
AndroidDeviceInfo,
|
||||
ClaimCapabilities,
|
||||
ClientInfo,
|
||||
TaskClaimGateway,
|
||||
AdminGateway,
|
||||
)
|
||||
from .collect_task_service import CollectServiceFactory, CollectTaskService
|
||||
from .current_client_service import CurrentClientService
|
||||
from .http_admin_gateway import DEFAULT_ADMIN_BASE_URL, HttpAdminGateway
|
||||
from .pdd_ui import PDDTaskPage, TaskRow
|
||||
@@ -45,7 +44,7 @@ from .task_models import (
|
||||
TaskSummary,
|
||||
TaskType,
|
||||
)
|
||||
from .task_repository import DuplicateTaskError, TaskRepository
|
||||
from .task_repository import TaskRepository
|
||||
|
||||
|
||||
TASK_TYPE_BY_TEXT = {
|
||||
@@ -116,21 +115,23 @@ def admin_task_to_new_claimed_task(task: AdminTask) -> NewClaimedTask:
|
||||
|
||||
|
||||
class ClaimTaskWorker(QObject):
|
||||
"""在后台领取至多一个采集任务,并先写入本地数据库。"""
|
||||
"""在后台补交或执行至多一条采集任务。"""
|
||||
|
||||
noTask = pyqtSignal()
|
||||
taskSaved = pyqtSignal(str)
|
||||
duplicateTask = pyqtSignal(str)
|
||||
localSaveFailed = pyqtSignal(str, str)
|
||||
failed = pyqtSignal(str)
|
||||
outcome = pyqtSignal(str, str, str)
|
||||
completed = pyqtSignal()
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
gateway: TaskClaimGateway,
|
||||
gateway: AdminGateway,
|
||||
task_repository: TaskRepository,
|
||||
client_service: CurrentClientService,
|
||||
android_device_service: SelectedAndroidDeviceService,
|
||||
collect_service_factory: Optional[CollectServiceFactory] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self._gateway = gateway
|
||||
@@ -138,6 +139,7 @@ class ClaimTaskWorker(QObject):
|
||||
self._client_service = client_service
|
||||
self._android_device_service = android_device_service
|
||||
self._cancelled = False
|
||||
self._collect_service_factory = collect_service_factory
|
||||
|
||||
def cancel(self) -> None:
|
||||
"""阻止尚未开始的领取;已领取的任务仍必须保存到本地。"""
|
||||
@@ -146,7 +148,6 @@ class ClaimTaskWorker(QObject):
|
||||
|
||||
@pyqtSlot()
|
||||
def run(self) -> None:
|
||||
claimed_task: Optional[AdminTask] = None
|
||||
try:
|
||||
if self._cancelled:
|
||||
return
|
||||
@@ -155,42 +156,20 @@ class ClaimTaskWorker(QObject):
|
||||
self.failed.emit("请先在设置页保存当前设备号和设备名")
|
||||
return
|
||||
android_serial = self._android_device_service.load()
|
||||
if not android_serial:
|
||||
self.failed.emit("请先在设置页选择并保存 Android 设备")
|
||||
return
|
||||
|
||||
capabilities = ClaimCapabilities(
|
||||
device=AndroidDeviceInfo(android_serial),
|
||||
supported_types=(TaskType.COLLECT,),
|
||||
purchase_mode="dry_run",
|
||||
schema_versions=(1,),
|
||||
)
|
||||
claimed_task = self._gateway.claim_next(
|
||||
result = CollectTaskService(
|
||||
self._gateway,
|
||||
self._task_repository,
|
||||
ClientInfo(
|
||||
client_settings.client_id,
|
||||
client_settings.client_name,
|
||||
),
|
||||
capabilities,
|
||||
)
|
||||
if claimed_task is None:
|
||||
if not self._cancelled:
|
||||
self.noTask.emit()
|
||||
return
|
||||
|
||||
local_task = admin_task_to_new_claimed_task(claimed_task)
|
||||
try:
|
||||
self._task_repository.add_claimed_task(local_task)
|
||||
except DuplicateTaskError:
|
||||
if not self._cancelled:
|
||||
self.duplicateTask.emit(claimed_task.task_id)
|
||||
return
|
||||
except Exception as exc:
|
||||
if not self._cancelled:
|
||||
self.localSaveFailed.emit(claimed_task.task_id, str(exc))
|
||||
return
|
||||
|
||||
if not self._cancelled:
|
||||
self.taskSaved.emit(claimed_task.task_id)
|
||||
android_serial or "",
|
||||
cancelled=lambda: self._cancelled,
|
||||
collect_service_factory=self._collect_service_factory,
|
||||
).execute_one()
|
||||
if not self._cancelled or result.kind == "cancelled":
|
||||
self.outcome.emit(result.kind, result.message, result.task_id)
|
||||
except AdminGatewayError as exc:
|
||||
if not self._cancelled:
|
||||
request_hint = (
|
||||
@@ -199,10 +178,7 @@ class ClaimTaskWorker(QObject):
|
||||
self.failed.emit(f"{exc}{request_hint}")
|
||||
except Exception as exc:
|
||||
if not self._cancelled:
|
||||
if claimed_task is not None:
|
||||
self.localSaveFailed.emit(claimed_task.task_id, str(exc))
|
||||
else:
|
||||
self.failed.emit(f"领取任务失败:{exc}")
|
||||
self.failed.emit(f"执行采集任务失败:{exc}")
|
||||
finally:
|
||||
self.completed.emit()
|
||||
|
||||
@@ -215,8 +191,9 @@ class PDDTaskPageEvent(QObject):
|
||||
page: PDDTaskPage,
|
||||
repository: Optional[TaskRepository] = None,
|
||||
parent=None,
|
||||
claim_gateway: Optional[TaskClaimGateway] = None,
|
||||
claim_gateway: Optional[AdminGateway] = None,
|
||||
settings_repository: Optional[SettingsRepository] = None,
|
||||
collect_service_factory: Optional[CollectServiceFactory] = None,
|
||||
):
|
||||
super().__init__(parent or page)
|
||||
self._page = page
|
||||
@@ -226,6 +203,13 @@ class PDDTaskPageEvent(QObject):
|
||||
self._claim_busy = False
|
||||
self._claim_thread: Optional[QThread] = None
|
||||
self._claim_worker: Optional[ClaimTaskWorker] = None
|
||||
self._collect_service_factory = collect_service_factory
|
||||
|
||||
try:
|
||||
self._repository.recover_interrupted_work()
|
||||
except AttributeError:
|
||||
# 测试用的只读 Repository 可以不实现恢复接口。
|
||||
pass
|
||||
|
||||
settings = settings_repository or SettingsRepository()
|
||||
self._client_service = CurrentClientService(settings)
|
||||
@@ -242,6 +226,7 @@ class PDDTaskPageEvent(QObject):
|
||||
self._claim_gateway = HttpAdminGateway(
|
||||
base_url if isinstance(base_url, str) else "",
|
||||
timeout_seconds=timeout_seconds,
|
||||
client_id=self._client_service.load().client_id,
|
||||
)
|
||||
except (TypeError, ValueError) as exc:
|
||||
self._claim_gateway_error = str(exc)
|
||||
@@ -290,7 +275,7 @@ class PDDTaskPageEvent(QObject):
|
||||
|
||||
self._claim_busy = True
|
||||
self._page.autoFetchButton.setEnabled(False)
|
||||
self._page.set_engine_status("正在领取一个采集任务,请稍候…")
|
||||
self._page.set_engine_status("正在处理一条采集任务,请稍候…")
|
||||
|
||||
thread = QThread(self)
|
||||
worker = ClaimTaskWorker(
|
||||
@@ -298,6 +283,7 @@ class PDDTaskPageEvent(QObject):
|
||||
self._repository,
|
||||
self._client_service,
|
||||
self._selected_android_device_service,
|
||||
self._collect_service_factory,
|
||||
)
|
||||
worker.moveToThread(thread)
|
||||
thread.started.connect(worker.run)
|
||||
@@ -306,6 +292,7 @@ class PDDTaskPageEvent(QObject):
|
||||
worker.duplicateTask.connect(self._on_duplicate_claimed_task)
|
||||
worker.localSaveFailed.connect(self._on_claimed_task_save_failed)
|
||||
worker.failed.connect(self._on_claim_failed)
|
||||
worker.outcome.connect(self._on_collect_outcome)
|
||||
worker.completed.connect(thread.quit)
|
||||
worker.completed.connect(worker.deleteLater)
|
||||
thread.finished.connect(thread.deleteLater)
|
||||
@@ -352,6 +339,15 @@ class PDDTaskPageEvent(QObject):
|
||||
self._page.set_engine_status(content)
|
||||
self._show_claim_error("领取任务失败", content)
|
||||
|
||||
@pyqtSlot(str, str, str)
|
||||
def _on_collect_outcome(self, kind: str, message: str, _task_id: str) -> None:
|
||||
if self._closing:
|
||||
return
|
||||
self._page.set_engine_status(message)
|
||||
self._reload()
|
||||
if kind in {"result_pending", "manual_review", "failed"}:
|
||||
self._show_claim_error("采集任务需要处理", message)
|
||||
|
||||
def _show_claim_error(self, title: str, content: str) -> None:
|
||||
"""显示不会自动消失的可恢复错误,同时保留底部状态文字。"""
|
||||
|
||||
@@ -413,6 +409,7 @@ class PDDTaskPageEvent(QObject):
|
||||
self._on_claimed_task_save_failed,
|
||||
),
|
||||
(worker.failed, self._on_claim_failed),
|
||||
(worker.outcome, self._on_collect_outcome),
|
||||
)
|
||||
except RuntimeError:
|
||||
signal_slots = ()
|
||||
@@ -424,7 +421,10 @@ class PDDTaskPageEvent(QObject):
|
||||
|
||||
if thread is not None and thread.isRunning():
|
||||
thread.quit()
|
||||
thread.wait(10_000)
|
||||
# uiautomator2/ADB 的单次调用可能需要数秒才返回。先通过
|
||||
# cancelled 标志让采集在下一个安全点退出,再等待工作线程收尾,
|
||||
# 避免窗口销毁时出现 "QThread destroyed while running"。
|
||||
thread.wait(60_000)
|
||||
|
||||
|
||||
def summary_to_row(summary: TaskSummary) -> TaskRow:
|
||||
|
||||
Reference in New Issue
Block a user