refactor(douyu): runner 按功能域拆分 Mixin + 精英手册兑换对齐真机流程 (HAR 审核通过)
拆分: douyu_runner.py (-3186行) → core/bind/manual/gold/donate/goods/xpd 七个 Mixin + 入口聚合类, douyu_batch_registry 供 routers 重导出 精英手册兑换对齐 HAR 抓包 (activity_client +379 行): - csrf 复用 Cookie 值不再每次 generateCsrf (HAR 实测全程零 csrf 请求), 仅服务端报 csrf 错误时 force_refresh 重试一次 - pay Referer 补 roomId 对齐浏览器 - 新增手册链路接口: elite_user_info/storedetail/pre_exchange check+confirm/ subscribe/batch_limit/赠品兑换/自动转换 - resolve_exchange_plan: 浏览器兑换按钮状态机路由 (normal/subscribe/ pre_exchange/wait/blocked) - 兑换执行: 人类节奏抖动 → 锁单(火爆重试1次) → 支付分级退避 (频控 30/60/90s 4次上限, 普错 2/5/10/20s 5次上限, 300s 锁单期 15s 余量, 可中断睡眠) 测试: 新增 17 项 (csrf 复用语义/状态机路由/Referer), 全量 94 通过; 未夹带代理改动
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
"""斗鱼任务执行器:手册开通与积分(由 douyu_runner.py 按功能域拆分)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.douyu import DouyuActivityClient, DouyuActivityError
|
||||
from ..models import Account, DouyuTask
|
||||
from .douyu_service import account_uid, update_account_profile_from_cookie
|
||||
|
||||
class ManualMixin:
|
||||
"""手册域:精英/电竞手册开通支付、积分查询与到账轮询。"""
|
||||
def _refresh_account_points(
|
||||
self,
|
||||
client: DouyuActivityClient,
|
||||
account: Account,
|
||||
cookie: str,
|
||||
*,
|
||||
ctn: str | None = None,
|
||||
) -> dict:
|
||||
"""刷新账号积分并写回账号表。"""
|
||||
uid = account_uid(account, cookie)
|
||||
if not uid:
|
||||
raise DouyuActivityError("Cookie 中没有 acf_uid,无法查询积分")
|
||||
ctn_value = ctn or client.acf_ccn(refresh_subscribe=False)
|
||||
result = client.query_points(uid=uid, ctn=ctn_value)
|
||||
points = self._to_int(result.get("points"))
|
||||
account.uid = uid
|
||||
account.points = points
|
||||
update_account_profile_from_cookie(account, cookie)
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
return {"points": points, "points_query": result}
|
||||
|
||||
def _wait_points_after_payment(
|
||||
self,
|
||||
db: Session,
|
||||
task: DouyuTask,
|
||||
account: Account,
|
||||
client: DouyuActivityClient,
|
||||
cookie: str,
|
||||
ctn: str,
|
||||
result: dict,
|
||||
) -> bool:
|
||||
"""等待宝典支付到账;积分达到 300 视为开通成功。"""
|
||||
deadline = time.monotonic() + DOUYU_PAYMENT_POLL_SECONDS
|
||||
result["payment_polling"] = True
|
||||
result["payment_target_points"] = 300
|
||||
poll_count = 0
|
||||
last_points = None
|
||||
while not self._stop.is_set() and time.monotonic() <= deadline:
|
||||
try:
|
||||
points_result = self._refresh_account_points(client, account, cookie, ctn=ctn)
|
||||
db.commit()
|
||||
poll_count += 1
|
||||
last_points = points_result["points"]
|
||||
result.update(points_result)
|
||||
result["payment_poll_count"] = poll_count
|
||||
result["payment_polling"] = True
|
||||
if last_points is not None and last_points >= 300:
|
||||
result["payment_polling"] = False
|
||||
result["elite_opened"] = True
|
||||
return True
|
||||
self._update_task_progress(
|
||||
db,
|
||||
task,
|
||||
"running",
|
||||
f"精英宝典支付码已生成,等待开通到账(当前积分 {last_points if last_points is not None else '-'})",
|
||||
result,
|
||||
)
|
||||
except Exception as exc:
|
||||
poll_count += 1
|
||||
result["payment_poll_count"] = poll_count
|
||||
result["payment_poll_error"] = str(exc)
|
||||
self._update_task_progress(db, task, "running", f"等待开通到账: {exc}", result)
|
||||
if self._stop.wait(DOUYU_PAYMENT_POLL_INTERVAL):
|
||||
break
|
||||
result["payment_polling"] = False
|
||||
result["elite_opened"] = False
|
||||
result["points"] = last_points
|
||||
return False
|
||||
|
||||
def _refresh_esports_handbook(
|
||||
self,
|
||||
client: DouyuActivityClient,
|
||||
account: Account,
|
||||
*,
|
||||
manual_id: str,
|
||||
) -> dict:
|
||||
"""刷新电竞手册开通状态并将积分写回账号。"""
|
||||
result = client.esports_user_info(manual_id=manual_id)
|
||||
manual_type = self._to_int(result.get("manual_type"))
|
||||
manual_score = self._to_int(result.get("manual_score"))
|
||||
account.esports_points = manual_score
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
return {
|
||||
"esports_manual_type": manual_type,
|
||||
"esports_manual_score": manual_score,
|
||||
"esports_expire_time": result.get("expire_time"),
|
||||
"esports_user_info": result,
|
||||
"esports_points": manual_score,
|
||||
"points": manual_score,
|
||||
}
|
||||
|
||||
def _wait_esports_open_after_payment(
|
||||
self,
|
||||
db: Session,
|
||||
task: DouyuTask,
|
||||
account: Account,
|
||||
client: DouyuActivityClient,
|
||||
*,
|
||||
manual_id: str,
|
||||
result: dict,
|
||||
baseline_manual_type: int | None,
|
||||
baseline_manual_score: int | None,
|
||||
) -> bool:
|
||||
"""等待电竞手册支付到账,以 manualType=1 或积分变化作为成功条件。"""
|
||||
deadline = time.monotonic() + DOUYU_PAYMENT_POLL_SECONDS
|
||||
result["payment_polling"] = True
|
||||
result["esports_manual_type_baseline"] = baseline_manual_type
|
||||
result["esports_manual_score_baseline"] = baseline_manual_score
|
||||
poll_count = 0
|
||||
last_manual_type = baseline_manual_type
|
||||
last_manual_score = baseline_manual_score
|
||||
|
||||
while not self._stop.is_set() and time.monotonic() <= deadline:
|
||||
try:
|
||||
handbook_result = self._refresh_esports_handbook(
|
||||
client,
|
||||
account,
|
||||
manual_id=manual_id,
|
||||
)
|
||||
db.commit()
|
||||
poll_count += 1
|
||||
last_manual_type = handbook_result["esports_manual_type"]
|
||||
last_manual_score = handbook_result["esports_manual_score"]
|
||||
result.update(handbook_result)
|
||||
result["payment_poll_count"] = poll_count
|
||||
result["payment_polling"] = True
|
||||
opened = (
|
||||
last_manual_type is not None
|
||||
and last_manual_type >= 1
|
||||
) or (
|
||||
baseline_manual_score is not None
|
||||
and last_manual_score is not None
|
||||
and last_manual_score > baseline_manual_score
|
||||
)
|
||||
if opened:
|
||||
result["payment_polling"] = False
|
||||
result["esports_opened"] = True
|
||||
return True
|
||||
self._update_task_progress(
|
||||
db,
|
||||
task,
|
||||
"running",
|
||||
"电竞手册支付码已生成,等待开通到账"
|
||||
f"(类型 {last_manual_type if last_manual_type is not None else '-'},"
|
||||
f"积分 {last_manual_score if last_manual_score is not None else '-'})",
|
||||
result,
|
||||
)
|
||||
except Exception as exc:
|
||||
poll_count += 1
|
||||
result["payment_poll_count"] = poll_count
|
||||
result["payment_poll_error"] = str(exc)
|
||||
self._update_task_progress(db, task, "running", f"等待电竞手册到账: {exc}", result)
|
||||
if self._stop.wait(DOUYU_PAYMENT_POLL_INTERVAL):
|
||||
break
|
||||
|
||||
result["payment_polling"] = False
|
||||
result["esports_opened"] = False
|
||||
result["esports_manual_type"] = last_manual_type
|
||||
result["esports_manual_score"] = last_manual_score
|
||||
result["esports_points"] = last_manual_score
|
||||
result["points"] = last_manual_score
|
||||
return False
|
||||
|
||||
def _execute_create_elite_qr(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
|
||||
client = self._client(cookie)
|
||||
ctn = str(self._task_payload(task).get("ctn") or "")
|
||||
if not ctn:
|
||||
ctn = client.acf_ccn(refresh_subscribe=True)
|
||||
act_alias = self._confirm_act_alias(config) or self._bind_qr_act_alias(config)
|
||||
if not act_alias:
|
||||
self._mark_task(db, task, "failed", "请先配置开通宝典活动 actAlias")
|
||||
return
|
||||
result = client.create_elite_qr(
|
||||
ctn=ctn,
|
||||
act_alias=act_alias,
|
||||
amount=int(config["elite_amount"]),
|
||||
room_id=str(config["room_id"]),
|
||||
)
|
||||
account.bind_status = "elite_qr_created"
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
self._update_task_progress(db, task, "running", "精英宝典支付码已生成,等待开通到账", result)
|
||||
opened = self._wait_points_after_payment(db, task, account, client, cookie, ctn, result)
|
||||
if self._stop.is_set():
|
||||
self._mark_task(db, task, "stopped", "任务已停止", result)
|
||||
return
|
||||
if opened:
|
||||
account.bind_status = "elite_opened"
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
self._mark_task(db, task, "success", f"精英宝典已开通,积分: {account.points}", result)
|
||||
return
|
||||
self._mark_task(
|
||||
db,
|
||||
task,
|
||||
"failed",
|
||||
f"未检测到精英宝典开通到账,当前积分: {account.points if account.points is not None else '-'}",
|
||||
result,
|
||||
)
|
||||
|
||||
def _execute_create_esports_qr(
|
||||
self,
|
||||
db: Session,
|
||||
task: DouyuTask,
|
||||
account: Account,
|
||||
cookie: str,
|
||||
config: dict,
|
||||
):
|
||||
"""生成电竞手册支付二维码,并通过活动状态确认开通到账。"""
|
||||
client = self._client(cookie)
|
||||
ctn = str(self._task_payload(task).get("ctn") or "")
|
||||
if not ctn:
|
||||
ctn = client.acf_ccn(refresh_subscribe=True)
|
||||
act_alias = str(config.get("esports_act_alias") or "").strip()
|
||||
manual_id = str(config.get("esports_manual_id") or "").strip()
|
||||
if not act_alias or not manual_id:
|
||||
self._mark_task(db, task, "failed", "请先配置电竞手册活动 actAlias 和 manualID")
|
||||
return
|
||||
|
||||
baseline_manual_type = None
|
||||
baseline_manual_score = None
|
||||
baseline_result: dict = {}
|
||||
try:
|
||||
baseline_result = self._refresh_esports_handbook(client, account, manual_id=manual_id)
|
||||
baseline_manual_type = baseline_result["esports_manual_type"]
|
||||
baseline_manual_score = baseline_result["esports_manual_score"]
|
||||
db.commit()
|
||||
except Exception as exc:
|
||||
self._push_log("warning", f"生成电竞手册支付码前查询活动状态失败: {exc}")
|
||||
|
||||
if baseline_manual_type is not None and baseline_manual_type >= 1:
|
||||
account.esports_bind_status = "esports_opened"
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
self._mark_task(
|
||||
db,
|
||||
task,
|
||||
"success",
|
||||
f"电竞手册已开通,积分: {baseline_manual_score if baseline_manual_score is not None else '-'}",
|
||||
{**baseline_result, "esports_opened": True, "payment_polling": False},
|
||||
)
|
||||
return
|
||||
|
||||
result = client.create_esports_qr(
|
||||
ctn=ctn,
|
||||
act_alias=act_alias,
|
||||
amount=int(config["esports_amount"]),
|
||||
room_id=str(config["room_id"]),
|
||||
)
|
||||
result.update(baseline_result)
|
||||
account.esports_bind_status = "esports_qr_created"
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
self._update_task_progress(db, task, "running", "电竞手册支付码已生成,等待开通到账", result)
|
||||
opened = self._wait_esports_open_after_payment(
|
||||
db,
|
||||
task,
|
||||
account,
|
||||
client,
|
||||
manual_id=manual_id,
|
||||
result=result,
|
||||
baseline_manual_type=baseline_manual_type,
|
||||
baseline_manual_score=baseline_manual_score,
|
||||
)
|
||||
if self._stop.is_set():
|
||||
self._mark_task(db, task, "stopped", "任务已停止", result)
|
||||
return
|
||||
if opened:
|
||||
account.esports_bind_status = "esports_opened"
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
self._mark_task(
|
||||
db,
|
||||
task,
|
||||
"success",
|
||||
f"电竞手册已开通,积分: {account.esports_points if account.esports_points is not None else '-'}",
|
||||
result,
|
||||
)
|
||||
return
|
||||
self._mark_task(
|
||||
db,
|
||||
task,
|
||||
"failed",
|
||||
"未检测到电竞手册开通到账"
|
||||
f",类型: {result.get('esports_manual_type', '-')},"
|
||||
f"积分: {result.get('esports_manual_score', '-')}",
|
||||
result,
|
||||
)
|
||||
|
||||
def _execute_query_esports_points(
|
||||
self,
|
||||
db: Session,
|
||||
task: DouyuTask,
|
||||
account: Account,
|
||||
cookie: str,
|
||||
config: dict,
|
||||
):
|
||||
"""查询电竞手册积分。"""
|
||||
manual_id = str(config.get("esports_manual_id") or "").strip()
|
||||
if not manual_id:
|
||||
self._mark_task(db, task, "failed", "请先配置电竞手册 manualID")
|
||||
return
|
||||
client = self._client(cookie)
|
||||
result = self._refresh_esports_handbook(client, account, manual_id=manual_id)
|
||||
account.esports_bind_status = "esports_points_queried"
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
points = result["esports_points"]
|
||||
self._mark_task(db, task, "success", f"电竞积分: {points if points is not None else '-'}", result)
|
||||
|
||||
def _execute_query_points(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
|
||||
client = self._client(cookie)
|
||||
ctn = client.acf_ccn(refresh_subscribe=False)
|
||||
result = self._refresh_account_points(client, account, cookie, ctn=ctn)
|
||||
account.bind_status = "points_queried"
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
points = result["points"]
|
||||
self._mark_task(db, task, "success", f"积分: {points if points is not None else '-'}", result)
|
||||
|
||||
Reference in New Issue
Block a user