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:
yml2213
2026-08-06 20:21:28 +08:00
parent c05c0e044e
commit a515ea6e0a
10 changed files with 394 additions and 178 deletions
+3 -91
View File
@@ -32,6 +32,7 @@ from ..services.douyu_service import (
cookie_account_ids_query,
create_douyu_planned_tasks,
douyu_config_value,
douyu_task_payload,
ensure_douyu_config,
)
@@ -120,104 +121,15 @@ def _account_out(account: Account) -> DouyuTaskAccountOut:
xpd_plat_id=account.xpd_plat_id,
xpd_area_id=account.xpd_area_id,
xpd_balance=account.xpd_balance,
xpd_fragments=account.xpd_fragments,
xpd_bind_status=account.xpd_bind_status or "",
assigned_to=account.assigned_to,
assigned_username=account.assigned_user.username if account.assigned_user else None,
)
def _slim_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_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 _sanitize_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 = dict(result)
for key in (
"raw",
"bind_info",
"before_bind_info",
"bind_candidates",
"cooldown_bind_info",
"after_bind_info",
"activity_bind_snapshot",
"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"] = {key: value for key, value in role.items() if key != "raw"}
goods = data.get("goods")
if isinstance(goods, list):
data.pop("goods", None)
elif isinstance(goods, dict):
data["goods"] = _slim_goods(goods)
if "limited_goods" in data:
data["limited_goods"] = _slim_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 _task_out(task: DouyuTask, *, include_detail: bool = False) -> DouyuTaskOut:
account = task.account
return DouyuTaskOut(
id=task.id,
batch_id=task.batch_id,
account_id=task.account_id,
account_username=account.username if account else "",
account_uid=account.uid if account else "",
account_nickname=account.nickname if account else "",
task_type=task.task_type,
status=task.status or "",
message=task.message or "",
result=_sanitize_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=task.created_at,
finished_at=task.finished_at,
)
return DouyuTaskOut(**douyu_task_payload(task, include_detail=include_detail))
def _config_out(config: DouyuConfig) -> DouyuConfigOut: