feat(douyu): 和平小店扭蛋碎片查询 + 深色模式修复 + 任务状态 WS 即时推送
- 碎片: 新增 query_xpd_fragments 任务, 复用 e语言 gamecoin 接口取扭蛋币 jb2, 账号已存角色回退(规避 Livelink 风控), accounts.xpd_fragments 字段与迁移 - 列表: 和平小店移除鱼翅/限制兑换/换绑时间列, 新增扭蛋碎片列; 操作区硬编码白底改用 theme token, 深色模式适配(三个手册共用) - 轮询: 任务状态经 WS(level=task)即时推送(二维码/绑定进度即时展示), pending/running/终态全覆盖, REST/WS 载荷统一 douyu_task_payload, raw 快照递归剥离, HTTP 轮询降频 5s/30s 兜底
This commit is contained in:
@@ -22,6 +22,7 @@ from .douyu_service import (
|
||||
douyu_config_value,
|
||||
ensure_douyu_config,
|
||||
latest_success_cookie,
|
||||
douyu_task_payload,
|
||||
update_account_profile_from_cookie,
|
||||
)
|
||||
|
||||
@@ -345,6 +346,22 @@ class DouyuBatchRunner:
|
||||
return ""
|
||||
return f"{role_name}({channel})" if channel else role_name
|
||||
|
||||
def _push_task_event(self, task: DouyuTask) -> None:
|
||||
"""向批次 WS 推送任务状态事件(level=task),前端即时更新不依赖轮询。"""
|
||||
if not self.log_queue or not self.loop:
|
||||
return
|
||||
try:
|
||||
payload = douyu_task_payload(task)
|
||||
except Exception:
|
||||
logger.exception("[douyu] 推送任务状态失败: task_id={}", task.id)
|
||||
return
|
||||
event = {
|
||||
"level": "task",
|
||||
"message": "",
|
||||
"task": payload,
|
||||
}
|
||||
asyncio.run_coroutine_threadsafe(self.log_queue.put(event), self.loop)
|
||||
|
||||
def _mark_task(
|
||||
self,
|
||||
db: Session,
|
||||
@@ -359,6 +376,7 @@ class DouyuBatchRunner:
|
||||
task.result = result
|
||||
task.finished_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
self._push_task_event(task)
|
||||
|
||||
def _update_task_progress(
|
||||
self,
|
||||
@@ -373,6 +391,7 @@ class DouyuBatchRunner:
|
||||
if result is not None:
|
||||
task.result = result
|
||||
db.commit()
|
||||
self._push_task_event(task)
|
||||
|
||||
def _upsert_goods(self, db: Session, goods: list[dict]) -> None:
|
||||
now = datetime.now(timezone.utc)
|
||||
@@ -1334,6 +1353,67 @@ class DouyuBatchRunner:
|
||||
{"balance": balance, "role": role, "area_id": area_id},
|
||||
)
|
||||
|
||||
def _execute_query_xpd_fragments(
|
||||
self,
|
||||
db: Session,
|
||||
task: DouyuTask,
|
||||
account: Account,
|
||||
cookie: str,
|
||||
config: dict,
|
||||
):
|
||||
"""查询和平小店扭蛋碎片数量。
|
||||
|
||||
优先现查角色;getrole 受限(Livelink 风控/失效)时回退账号已存角色,
|
||||
保证已绑定账号仍可查询。
|
||||
"""
|
||||
client = self._client(cookie)
|
||||
act_id = str(config["xpd_act_id"])
|
||||
embed_query: dict = {}
|
||||
openid = str(account.xpd_openid or "")
|
||||
roleid = str(account.xpd_role_id or "")
|
||||
plat = str(account.xpd_plat_id or "1")
|
||||
areaid = str(account.xpd_area_id or 1)
|
||||
role: dict = {}
|
||||
try:
|
||||
ctx = self._xpd_role_context(client, config)
|
||||
embed_query = ctx["embed"]["query"]
|
||||
role = ctx["role"] if isinstance(ctx.get("role"), dict) else {}
|
||||
if role.get("role_id"):
|
||||
role_area = self._xpd_area_id(role, account)
|
||||
openid = str(role.get("game_open_id") or "") or openid
|
||||
roleid = str(role.get("role_id") or "") or roleid
|
||||
plat = str(role.get("plat_id") or "1") or plat
|
||||
areaid = str(role_area) or areaid
|
||||
self._apply_xpd_role_to_account(account, role, role_area)
|
||||
except Exception:
|
||||
pass
|
||||
if not openid or not roleid:
|
||||
self._mark_task(db, task, "failed", "未获取到小店绑定角色,请先生成二维码扫码绑定")
|
||||
return
|
||||
result = client.xpd_fragments(
|
||||
embed_query=embed_query,
|
||||
act_id=act_id,
|
||||
openid=openid,
|
||||
roleid=roleid,
|
||||
plat=plat,
|
||||
areaid=areaid,
|
||||
)
|
||||
fragments = result.get("fragments")
|
||||
account.xpd_fragments = fragments
|
||||
account.xpd_bind_status = "xpd_fragments_queried"
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
db.commit()
|
||||
if fragments is None:
|
||||
self._mark_task(db, task, "failed", "未获取到小店扭蛋碎片数量")
|
||||
return
|
||||
self._mark_task(
|
||||
db,
|
||||
task,
|
||||
"success",
|
||||
f"小店扭蛋碎片: {fragments}",
|
||||
{"fragments": fragments, "role": role, "area_id": int(areaid)},
|
||||
)
|
||||
|
||||
def _execute_get_bind_qr(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
|
||||
client = self._client(cookie)
|
||||
qr_act_alias = self._bind_qr_act_alias(config)
|
||||
@@ -2584,9 +2664,7 @@ class DouyuBatchRunner:
|
||||
if not task or self._stop.is_set():
|
||||
return
|
||||
account = task.account
|
||||
task.status = "running"
|
||||
task.message = "执行中"
|
||||
worker_db.commit()
|
||||
self._update_task_progress(worker_db, task, "running", "执行中")
|
||||
|
||||
with self._counter_lock:
|
||||
self._started += 1
|
||||
@@ -2631,6 +2709,7 @@ class DouyuBatchRunner:
|
||||
"query_xpd_role": self._execute_query_xpd_role,
|
||||
"refresh_xpd_goods": self._execute_refresh_xpd_goods,
|
||||
"query_xpd_balance": self._execute_query_xpd_balance,
|
||||
"query_xpd_fragments": self._execute_query_xpd_fragments,
|
||||
}.get(task.task_type)
|
||||
if handler is None:
|
||||
self._mark_task(worker_db, task, "failed", "不支持的任务类型")
|
||||
@@ -2669,6 +2748,8 @@ class DouyuBatchRunner:
|
||||
task.status = "pending"
|
||||
task.message = "等待执行"
|
||||
self.db.commit()
|
||||
for task in tasks:
|
||||
self._push_task_event(task)
|
||||
|
||||
total = len(tasks)
|
||||
with ThreadPoolExecutor(max_workers=self.concurrency) as executor:
|
||||
|
||||
@@ -13,8 +13,7 @@ from core.douyu.cookie_utils import cookie_value
|
||||
from ..models import Account, DouyuConfig, DouyuTask, LoginTask
|
||||
|
||||
|
||||
SUPPORTED_DOUYU_TASK_TYPES = {
|
||||
"get_bind_qr": "获取绑定二维码",
|
||||
SUPPORTED_DOUYU_TASK_TYPES = { "get_bind_qr": "获取绑定二维码",
|
||||
"confirm_bind": "确认绑定",
|
||||
"create_elite_qr": "开通精英宝典30",
|
||||
"prepare_esports_bind": "绑定电竞手册角色",
|
||||
@@ -44,11 +43,11 @@ SUPPORTED_DOUYU_TASK_TYPES = {
|
||||
"query_xpd_role": "查询小店绑定角色",
|
||||
"refresh_xpd_goods": "刷新小店商品列表",
|
||||
"query_xpd_balance": "查询小店点券余额",
|
||||
"query_xpd_fragments": "查询小店扭蛋碎片",
|
||||
}
|
||||
|
||||
|
||||
DOUYU_CONFIG_DEFAULTS = {
|
||||
"manual_id": "G4KA4Qnz4LDp7",
|
||||
DOUYU_CONFIG_DEFAULTS = { "manual_id": "G4KA4Qnz4LDp7",
|
||||
"rid": "9263298",
|
||||
"bind_act_alias": "20260120QYOOB",
|
||||
"confirm_act_alias": "20260120QYOOB",
|
||||
@@ -223,3 +222,121 @@ def cleanup_orphan_douyu_tasks(
|
||||
task.finished_at = now
|
||||
db.commit()
|
||||
return len(tasks)
|
||||
|
||||
|
||||
def slim_douyu_goods(goods: object) -> object:
|
||||
"""保留兑换图片和列表展示需要的商品小字段。"""
|
||||
if not isinstance(goods, dict):
|
||||
return None
|
||||
return {
|
||||
key: value
|
||||
for key, value in goods.items()
|
||||
if key in {"commodityId", "commodity_id", "commodityName", "name", "webPic", "pic", "score", "status"}
|
||||
}
|
||||
|
||||
|
||||
def slim_douyu_limited_goods(goods: object) -> list[dict[str, object]]:
|
||||
"""限兑列表只返回前几个商品名,避免任务列表携带完整原始数组。"""
|
||||
if not isinstance(goods, list):
|
||||
return []
|
||||
result: list[dict[str, object]] = []
|
||||
for item in goods[:5]:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
result.append({
|
||||
key: value
|
||||
for key, value in item.items()
|
||||
if key in {"commodityId", "commodity_id", "commodityName", "name"}
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def strip_douyu_raw_snapshots(value: object) -> object:
|
||||
"""递归移除任务结果中的原始接口快照。"""
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
key: strip_douyu_raw_snapshots(item)
|
||||
for key, item in value.items()
|
||||
if key != "raw"
|
||||
}
|
||||
if isinstance(value, list):
|
||||
return [strip_douyu_raw_snapshots(item) for item in value]
|
||||
return value
|
||||
|
||||
|
||||
def sanitize_douyu_task_result(result: dict | None, task_type: str, *, include_detail: bool = False) -> dict | None:
|
||||
"""列表/实时推送接口剥离原始快照与大数组;详情接口保留完整 result。"""
|
||||
if not isinstance(result, dict):
|
||||
return result
|
||||
if include_detail:
|
||||
return result
|
||||
|
||||
data = strip_douyu_raw_snapshots(result)
|
||||
if not isinstance(data, dict):
|
||||
return None
|
||||
for key in (
|
||||
"bind_info",
|
||||
"before_bind_info",
|
||||
"bind_candidates",
|
||||
"cooldown_bind_info",
|
||||
"after_bind_info",
|
||||
"activity_bind_snapshot",
|
||||
"activity_bind_info",
|
||||
"points_query",
|
||||
"exchange_balance_query",
|
||||
"query_act_aliases",
|
||||
"records",
|
||||
):
|
||||
data.pop(key, None)
|
||||
|
||||
# 和平小店任务 result 内嵌 role(含 gameOpenId 等),列表接口剥离其原始快照
|
||||
role = data.get("role")
|
||||
if isinstance(role, dict):
|
||||
data["role"] = strip_douyu_raw_snapshots(role)
|
||||
|
||||
goods = data.get("goods")
|
||||
if isinstance(goods, list):
|
||||
data.pop("goods", None)
|
||||
elif isinstance(goods, dict):
|
||||
data["goods"] = slim_douyu_goods(goods)
|
||||
|
||||
if "limited_goods" in data:
|
||||
data["limited_goods"] = slim_douyu_limited_goods(data.get("limited_goods"))
|
||||
|
||||
if task_type not in {"get_bind_qr", "prepare_esports_bind", "get_esports_bind_qr", "get_xpd_bind_qr"}:
|
||||
data.pop("url", None)
|
||||
if task_type not in {"create_elite_qr", "create_esports_qr", "create_gold_qr"}:
|
||||
data.pop("pay_url", None)
|
||||
return data
|
||||
|
||||
|
||||
def douyu_task_payload(task: DouyuTask, *, include_detail: bool = False) -> dict:
|
||||
"""生成 REST 与 WebSocket 共用的斗鱼任务载荷。"""
|
||||
account = task.account
|
||||
|
||||
def isoformat(value: datetime | None) -> str | None:
|
||||
if value is None:
|
||||
return None
|
||||
if value.tzinfo is None:
|
||||
value = value.replace(tzinfo=timezone.utc)
|
||||
return value.isoformat()
|
||||
|
||||
return {
|
||||
"id": task.id,
|
||||
"batch_id": task.batch_id,
|
||||
"account_id": task.account_id,
|
||||
"account_username": (account.username if account else "") or "",
|
||||
"account_uid": (account.uid if account else "") or "",
|
||||
"account_nickname": (account.nickname if account else "") or "",
|
||||
"task_type": task.task_type or "",
|
||||
"status": task.status or "",
|
||||
"message": task.message or "",
|
||||
"result": sanitize_douyu_task_result(
|
||||
task.result if isinstance(task.result, dict) else None,
|
||||
task.task_type or "",
|
||||
include_detail=include_detail,
|
||||
),
|
||||
"created_by": task.created_by,
|
||||
"created_at": isoformat(task.created_at),
|
||||
"finished_at": isoformat(task.finished_at),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user