feat(collect): add product status recheck
This commit is contained in:
@@ -2738,6 +2738,383 @@ class CollectWorker(BaseWorker):
|
||||
def _elapsed_ms(self, started):
|
||||
return int((time.monotonic() - started) * 1000)
|
||||
|
||||
|
||||
class StatusRecheckWorker(CollectWorker):
|
||||
"""Re-read product status snapshots without changing task workflow fields."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
tasks,
|
||||
db_path=None,
|
||||
config=None,
|
||||
preflight=True,
|
||||
diagnostic_log_dir=None,
|
||||
):
|
||||
super().__init__(
|
||||
tasks,
|
||||
db_path=db_path,
|
||||
config=config,
|
||||
preflight=preflight,
|
||||
diagnostic_log_dir=diagnostic_log_dir,
|
||||
collect_scope=product_status.COLLECT_SCOPE_ALL,
|
||||
)
|
||||
|
||||
def execute(self):
|
||||
account_rows = accounts.list_accounts(path=self.db_path, config=self.config)
|
||||
account_by_alias = {
|
||||
str(account.alias).strip(): account
|
||||
for account in account_rows
|
||||
if str(account.alias).strip()
|
||||
}
|
||||
eligible = list(self.tasks)
|
||||
batch_ids = self._batch_ids(eligible)
|
||||
total = len(eligible)
|
||||
rechecked = 0
|
||||
skipped = 0
|
||||
failed = 0
|
||||
done = 0
|
||||
login_skip_reasons = {}
|
||||
login_required_accounts = {}
|
||||
preflight_info = {}
|
||||
skip_reason_counts = empty_skip_reason_counts()
|
||||
product_status_counts = {
|
||||
status: 0 for status in product_status.VALID_PRODUCT_STATUSES
|
||||
}
|
||||
|
||||
self._run_id = self._create_run_log(eligible, batch_ids)
|
||||
self._emit_activity("preflight_started", total=total, step="preflight")
|
||||
self._log_run_event(
|
||||
f"step=preflight result=start detail=商品状态重检开始 total={total}"
|
||||
)
|
||||
|
||||
if self.preflight:
|
||||
blocked, preflight_info = self._preflight_prepare(
|
||||
eligible,
|
||||
account_rows,
|
||||
account_by_alias,
|
||||
)
|
||||
if blocked:
|
||||
self._log_preflight_blocked(blocked)
|
||||
summary = self._summary(
|
||||
ok=False,
|
||||
total=total,
|
||||
done=done,
|
||||
collected=rechecked,
|
||||
skipped=skipped,
|
||||
failed=failed,
|
||||
batch_ids=batch_ids,
|
||||
blocked=True,
|
||||
extra={
|
||||
**blocked,
|
||||
"rechecked": rechecked,
|
||||
"skip_reason_counts": dict(skip_reason_counts),
|
||||
"product_status_counts": dict(product_status_counts),
|
||||
},
|
||||
)
|
||||
self._finish_run_log("blocked", summary)
|
||||
return summary
|
||||
for item in preflight_info.get("logged_out") or []:
|
||||
alias = str(item.get("alias") or "").strip()
|
||||
reason = item.get("reason") or "账号未登录"
|
||||
if alias:
|
||||
login_skip_reasons[alias] = reason
|
||||
login_required_accounts[alias] = item
|
||||
self._log_run_event("step=preflight result=success detail=账号就绪检查完成")
|
||||
else:
|
||||
self._log_run_event(
|
||||
"step=preflight result=skipped detail=测试模式跳过商品状态重检前检查",
|
||||
level="warning",
|
||||
)
|
||||
|
||||
for index, task in enumerate(eligible, start=1):
|
||||
if self.should_cancel():
|
||||
break
|
||||
self._emit_activity(
|
||||
"task_started",
|
||||
task=task,
|
||||
index=index,
|
||||
total=total,
|
||||
step="match_account",
|
||||
)
|
||||
account = account_by_alias.get(str(task.alias).strip())
|
||||
if account is None:
|
||||
skipped += 1
|
||||
done += 1
|
||||
skip_reason_counts[ALIAS_UNMATCHED] += 1
|
||||
reason = "别名未匹配账号"
|
||||
self._log_run_event(
|
||||
"step=match_account result=skipped detail=任务 {task_id} 商品 {item_id} {reason}".format(
|
||||
task_id=task.id,
|
||||
item_id=task.item_id,
|
||||
reason=reason,
|
||||
),
|
||||
task=task,
|
||||
level="warning",
|
||||
)
|
||||
self._emit_activity(
|
||||
"task_finished",
|
||||
task=task,
|
||||
index=index,
|
||||
total=total,
|
||||
step="match_account",
|
||||
result="skipped",
|
||||
)
|
||||
self._emit_recheck_progress(done, total, rechecked, skipped, failed)
|
||||
continue
|
||||
|
||||
alias = str(task.alias).strip()
|
||||
if alias in login_skip_reasons:
|
||||
skipped += 1
|
||||
done += 1
|
||||
skip_reason_counts[LOGIN_REQUIRED] += 1
|
||||
reason = login_skip_reasons[alias]
|
||||
self._log_run_event(
|
||||
"step=login_check result=skipped detail=任务 {task_id} 商品 {item_id} {reason}".format(
|
||||
task_id=task.id,
|
||||
item_id=task.item_id,
|
||||
reason=reason,
|
||||
),
|
||||
task=task,
|
||||
level="warning",
|
||||
)
|
||||
self._emit_activity(
|
||||
"task_finished",
|
||||
task=task,
|
||||
index=index,
|
||||
total=total,
|
||||
step="check_login",
|
||||
result="skipped",
|
||||
)
|
||||
self._emit_recheck_progress(done, total, rechecked, skipped, failed)
|
||||
continue
|
||||
|
||||
self._emit_activity(
|
||||
"task_step",
|
||||
task=task,
|
||||
index=index,
|
||||
total=total,
|
||||
step="check_login",
|
||||
)
|
||||
status = self._confirmed_login_status(account, context="status_recheck", task=task)
|
||||
if self._is_definitive_logged_out(status):
|
||||
skipped += 1
|
||||
done += 1
|
||||
skip_reason_counts[LOGIN_REQUIRED] += 1
|
||||
reason = self._midrun_login_skip_reason(status)
|
||||
login_skip_reasons[alias] = reason
|
||||
login_required_accounts[alias] = self._account_payload(account, reason)
|
||||
self._log_run_event(
|
||||
"step=login_check result=skipped detail=任务 {task_id} 商品 {item_id} {reason}".format(
|
||||
task_id=task.id,
|
||||
item_id=task.item_id,
|
||||
reason=reason,
|
||||
),
|
||||
task=task,
|
||||
level="warning",
|
||||
)
|
||||
self._emit_activity(
|
||||
"task_finished",
|
||||
task=task,
|
||||
index=index,
|
||||
total=total,
|
||||
step="check_login",
|
||||
result="skipped",
|
||||
)
|
||||
self._emit_recheck_progress(done, total, rechecked, skipped, failed)
|
||||
continue
|
||||
if not status.get("logged_in"):
|
||||
self._log_run_event(
|
||||
"step=login_check result=uncertain detail=任务 {task_id} 商品 {item_id} 登录状态检测暂时不稳定,继续尝试读取商品状态: {detail}".format(
|
||||
task_id=task.id,
|
||||
item_id=task.item_id,
|
||||
detail=self._login_status_detail(status),
|
||||
),
|
||||
task=task,
|
||||
level="warning",
|
||||
)
|
||||
|
||||
started = time.monotonic()
|
||||
current_step = "read_product_status"
|
||||
activity_result = "success"
|
||||
|
||||
def on_step(step):
|
||||
nonlocal current_step
|
||||
current_step = str(step)
|
||||
self._emit_activity(
|
||||
"task_step",
|
||||
task=task,
|
||||
index=index,
|
||||
total=total,
|
||||
step=current_step,
|
||||
)
|
||||
self._log_run_event(
|
||||
"step={step} result=start detail=任务 {task_id} 商品 {item_id}".format(
|
||||
step=current_step,
|
||||
task_id=task.id,
|
||||
item_id=task.item_id,
|
||||
),
|
||||
task=task,
|
||||
)
|
||||
|
||||
try:
|
||||
result = editor.recheck_product_status(
|
||||
account,
|
||||
{"item_id": task.item_id},
|
||||
on_step=on_step,
|
||||
)
|
||||
detected_status = product_status.normalize_status(
|
||||
result.get("product_status")
|
||||
)
|
||||
product_status_counts[detected_status] += 1
|
||||
current_step = "db_write"
|
||||
self._emit_activity(
|
||||
"task_step",
|
||||
task=task,
|
||||
index=index,
|
||||
total=total,
|
||||
step="save_result",
|
||||
)
|
||||
self._log_run_event(
|
||||
"step=db_write result=start detail=任务 {task_id} 商品 {item_id} 保存商品状态".format(
|
||||
task_id=task.id,
|
||||
item_id=task.item_id,
|
||||
),
|
||||
task=task,
|
||||
)
|
||||
db.set_product_status(
|
||||
task.id,
|
||||
detected_status,
|
||||
result.get("product_status_note"),
|
||||
path=self.db_path,
|
||||
)
|
||||
rechecked += 1
|
||||
elapsed_ms = self._elapsed_ms(started)
|
||||
self.row_updated.emit(
|
||||
task.id,
|
||||
{
|
||||
"product_status": detected_status,
|
||||
"product_status_note": result.get("product_status_note"),
|
||||
},
|
||||
)
|
||||
self._log_run_event(
|
||||
"step=db_write result=success detail=任务 {task_id} 商品 {item_id} 商品状态重检成功 elapsed_ms={elapsed_ms}".format(
|
||||
task_id=task.id,
|
||||
item_id=task.item_id,
|
||||
elapsed_ms=elapsed_ms,
|
||||
),
|
||||
task=task,
|
||||
)
|
||||
if result.get("close_target_confirmed") is False:
|
||||
self._log_run_event(
|
||||
"step=close_product result=uncertain detail=任务 {task_id} 商品 {item_id} 商品页已请求关闭,但未在短时间内确认关闭;状态结果已保存,继续处理后续任务".format(
|
||||
task_id=task.id,
|
||||
item_id=task.item_id,
|
||||
),
|
||||
task=task,
|
||||
level="warning",
|
||||
)
|
||||
except Exception as exc:
|
||||
activity_result = "failed"
|
||||
failed += 1
|
||||
error = str(exc) or exc.__class__.__name__
|
||||
safe_error = diagnostics.redact_log_text(error)
|
||||
display_error = db.format_failure_error(safe_error, current_step)
|
||||
elapsed_ms = self._elapsed_ms(started)
|
||||
self.failed.emit(task.id, display_error)
|
||||
self._log_run_event(
|
||||
"step={step} result=failed detail=商品状态重检失败: {error} elapsed_ms={elapsed_ms}".format(
|
||||
step=current_step,
|
||||
error=safe_error,
|
||||
elapsed_ms=elapsed_ms,
|
||||
),
|
||||
task=task,
|
||||
level="error",
|
||||
)
|
||||
self._write_diagnostic_log(
|
||||
"商品状态重检失败",
|
||||
level="ERROR",
|
||||
step=current_step,
|
||||
task=task,
|
||||
elapsed_ms=elapsed_ms,
|
||||
payload={"error": safe_error},
|
||||
exc=exc,
|
||||
)
|
||||
finally:
|
||||
done += 1
|
||||
self._emit_activity(
|
||||
"task_finished",
|
||||
task=task,
|
||||
index=index,
|
||||
total=total,
|
||||
step=current_step,
|
||||
result=activity_result,
|
||||
)
|
||||
self._emit_recheck_progress(done, total, rechecked, skipped, failed)
|
||||
|
||||
summary = self._summary(
|
||||
ok=failed == 0,
|
||||
total=total,
|
||||
done=done,
|
||||
collected=rechecked,
|
||||
skipped=skipped,
|
||||
failed=failed,
|
||||
batch_ids=batch_ids,
|
||||
extra={
|
||||
**preflight_info,
|
||||
"rechecked": rechecked,
|
||||
"login_required_accounts": list(login_required_accounts.values()),
|
||||
"skip_reason_counts": dict(skip_reason_counts),
|
||||
"product_status_counts": dict(product_status_counts),
|
||||
},
|
||||
)
|
||||
self._finish_run_log("cancelled" if self.should_cancel() else "done", summary)
|
||||
return summary
|
||||
|
||||
def _emit_recheck_progress(self, done, total, rechecked, skipped, failed):
|
||||
self.progress.emit(
|
||||
{
|
||||
"done": done,
|
||||
"total": total,
|
||||
"rechecked": rechecked,
|
||||
"skipped": skipped,
|
||||
"failed": failed,
|
||||
}
|
||||
)
|
||||
|
||||
def _create_run_log(self, eligible, batch_ids):
|
||||
try:
|
||||
return db.create_run_log(
|
||||
"status_recheck",
|
||||
dry_run=False,
|
||||
total=len(eligible),
|
||||
options={
|
||||
"batch_ids": batch_ids,
|
||||
"preflight": self.preflight,
|
||||
"scope": "current_filters",
|
||||
},
|
||||
path=self.db_path,
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def _finish_run_log(self, status, summary):
|
||||
if self._run_id is None:
|
||||
return
|
||||
try:
|
||||
db.finish_run_log(
|
||||
self._run_id,
|
||||
status=status,
|
||||
done=summary.get("done", 0),
|
||||
success_count=summary.get("rechecked", 0),
|
||||
skipped_count=summary.get("skipped", 0),
|
||||
failed_count=summary.get("failed", 0),
|
||||
summary_json=summary,
|
||||
path=self.db_path,
|
||||
)
|
||||
except Exception:
|
||||
return
|
||||
|
||||
|
||||
class WriteBackWorker(BaseWorker):
|
||||
"""Write Excel fields back in a background thread."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user