feat(douyu): 新增和平小店查询模块(绑定角色/商品列表/点券余额)
- activity_client: getIframeUrl 签发 code/sig,道聚城 getrole.out/recommend/balance 接口 (2026-08 起需 isCode=1 + authType=delegate + sAnchorId 才能通过 Livelink 校验) - 迁移 0016: accounts xpd_* 字段、douyu_config 小店配置(actAlias/actId/rid)、 douyu_xpd_goods_snapshot 商品快照表 - runner/service/router/schema 注册 query_xpd_role/refresh_xpd_goods/query_xpd_balance 三个任务,任务结果剥离 role.raw 防 openid 泄露 - 前端: 和平小店路由/菜单/DouyuTasksPage 三态/商品下拉/配置弹窗
This commit is contained in:
@@ -15,7 +15,7 @@ from sqlalchemy.orm import Session, joinedload
|
||||
from core.douyu import DouyuActivityClient, DouyuActivityError
|
||||
|
||||
from ..database import SessionLocal
|
||||
from ..models import Account, DouyuEsportsGoodsSnapshot, DouyuGoodsSnapshot, DouyuTask
|
||||
from ..models import Account, DouyuEsportsGoodsSnapshot, DouyuGoodsSnapshot, DouyuTask, DouyuXpdGoodsSnapshot
|
||||
from .douyu_service import (
|
||||
DOUYU_CONFIG_FIELDS,
|
||||
account_uid,
|
||||
@@ -415,6 +415,30 @@ class DouyuBatchRunner:
|
||||
row.updated_at = now
|
||||
db.commit()
|
||||
|
||||
def _upsert_xpd_goods(self, db: Session, goods: list[dict]) -> None:
|
||||
"""写入和平小店商品快照。"""
|
||||
now = datetime.now(timezone.utc)
|
||||
for raw in goods:
|
||||
commodity_id = str(raw.get("commodity_id") or raw.get("iGoodsId") or "")
|
||||
if not commodity_id:
|
||||
continue
|
||||
row = (
|
||||
db.query(DouyuXpdGoodsSnapshot)
|
||||
.filter(DouyuXpdGoodsSnapshot.commodity_id == commodity_id)
|
||||
.first()
|
||||
)
|
||||
if row is None:
|
||||
row = DouyuXpdGoodsSnapshot(commodity_id=commodity_id)
|
||||
db.add(row)
|
||||
row.name = str(raw.get("name") or raw.get("sGoodsName") or "")
|
||||
row.price = self._to_int(raw.get("price") or raw.get("iPrice"))
|
||||
row.org_price = self._to_int(raw.get("org_price") or raw.get("iOrgPrice"))
|
||||
row.category = str(raw.get("category") or raw.get("iCategoryId") or "")
|
||||
row.goods_left = self._to_int(raw.get("goods_left") or raw.get("iGoodsLeft"))
|
||||
row.raw = raw
|
||||
row.updated_at = now
|
||||
db.commit()
|
||||
|
||||
def _config_info(self, db: Session) -> dict:
|
||||
config = ensure_douyu_config(db)
|
||||
return {field: douyu_config_value(field, getattr(config, field, None)) for field in DOUYU_CONFIG_FIELDS}
|
||||
@@ -993,6 +1017,141 @@ class DouyuBatchRunner:
|
||||
{"goods_count": len(goods), "esports_store_score": result["score"], "goods": goods},
|
||||
)
|
||||
|
||||
def _xpd_role_context(self, client: DouyuActivityClient, config: dict) -> dict:
|
||||
"""获取小店 H5 参数 + 绑定角色信息,小店任务共用。"""
|
||||
embed = client.xpd_embed_query(
|
||||
act_alias=str(config["xpd_act_alias"]),
|
||||
rid=str(config["xpd_rid"]),
|
||||
)
|
||||
role = client.xpd_get_role(
|
||||
embed_query=embed["query"],
|
||||
act_id=str(config["xpd_act_id"]),
|
||||
rid=str(config["xpd_rid"]),
|
||||
)
|
||||
return {"embed": embed, "role": role}
|
||||
|
||||
def _xpd_area_id(self, role: dict, account: Account) -> int:
|
||||
"""角色大区: 微信=1, 手Q=2, 未知回退账号已存值或 1。"""
|
||||
role_type = str(role.get("type") or "")
|
||||
if role_type == "wx":
|
||||
return 1
|
||||
if role_type == "qq":
|
||||
return 2
|
||||
return account.xpd_area_id or 1
|
||||
|
||||
def _apply_xpd_role_to_account(self, account: Account, role: dict, area_id: int) -> None:
|
||||
account.xpd_game_name = str(role.get("role_name") or "") or account.xpd_game_name
|
||||
account.xpd_openid = str(role.get("game_open_id") or "") or account.xpd_openid
|
||||
account.xpd_role_id = str(role.get("role_id") or "") or account.xpd_role_id
|
||||
account.xpd_plat_id = self._to_int(role.get("plat_id"))
|
||||
account.xpd_area_id = area_id
|
||||
account.updated_at = datetime.now(timezone.utc)
|
||||
|
||||
def _execute_query_xpd_role(
|
||||
self,
|
||||
db: Session,
|
||||
task: DouyuTask,
|
||||
account: Account,
|
||||
cookie: str,
|
||||
config: dict,
|
||||
):
|
||||
"""查询和平小店绑定角色。"""
|
||||
client = self._client(cookie)
|
||||
ctx = self._xpd_role_context(client, config)
|
||||
role = ctx["role"]
|
||||
if not role.get("role_id"):
|
||||
self._mark_task(db, task, "failed", "未获取到小店绑定角色")
|
||||
return
|
||||
area_id = self._xpd_area_id(role, account)
|
||||
self._apply_xpd_role_to_account(account, role, area_id)
|
||||
account.xpd_bind_status = "xpd_bound"
|
||||
db.commit()
|
||||
role_text = str(role.get("role_name") or "-")
|
||||
channel = "微信" if role.get("type") == "wx" else ("手Q" if role.get("type") == "qq" else str(role.get("type") or "-"))
|
||||
self._mark_task(
|
||||
db,
|
||||
task,
|
||||
"success",
|
||||
f"小店角色: {role_text}({channel})",
|
||||
{"role": role, "area_id": area_id},
|
||||
)
|
||||
|
||||
def _execute_refresh_xpd_goods(
|
||||
self,
|
||||
db: Session,
|
||||
task: DouyuTask,
|
||||
account: Account,
|
||||
cookie: str,
|
||||
config: dict,
|
||||
):
|
||||
"""刷新和平小店商品列表快照(全局数据,任一可用 CK 即可)。"""
|
||||
client = self._client(cookie)
|
||||
ctx = self._xpd_role_context(client, config)
|
||||
role = ctx["role"]
|
||||
if not role.get("role_id"):
|
||||
self._mark_task(db, task, "failed", "未获取到小店绑定角色")
|
||||
return
|
||||
area_id = self._xpd_area_id(role, account)
|
||||
result = client.xpd_list_goods(
|
||||
embed_query=ctx["embed"]["query"],
|
||||
act_id=str(config["xpd_act_id"]),
|
||||
openid=str(role.get("game_open_id") or ""),
|
||||
roleid=str(role.get("role_id") or ""),
|
||||
areaid=str(area_id),
|
||||
)
|
||||
goods = result["goods"]
|
||||
self._upsert_xpd_goods(db, goods)
|
||||
self._apply_xpd_role_to_account(account, role, area_id)
|
||||
account.xpd_bind_status = "xpd_goods_refreshed"
|
||||
db.commit()
|
||||
self._mark_task(
|
||||
db,
|
||||
task,
|
||||
"success",
|
||||
f"已刷新小店商品 {len(goods)} 个",
|
||||
{"goods_count": len(goods), "goods": goods},
|
||||
)
|
||||
|
||||
def _execute_query_xpd_balance(
|
||||
self,
|
||||
db: Session,
|
||||
task: DouyuTask,
|
||||
account: Account,
|
||||
cookie: str,
|
||||
config: dict,
|
||||
):
|
||||
"""查询和平小店点券余额。"""
|
||||
client = self._client(cookie)
|
||||
ctx = self._xpd_role_context(client, config)
|
||||
role = ctx["role"]
|
||||
if not role.get("role_id"):
|
||||
self._mark_task(db, task, "failed", "未获取到小店绑定角色")
|
||||
return
|
||||
area_id = self._xpd_area_id(role, account)
|
||||
result = client.xpd_balance(
|
||||
embed_query=ctx["embed"]["query"],
|
||||
act_id=str(config["xpd_act_id"]),
|
||||
openid=str(role.get("game_open_id") or ""),
|
||||
roleid=str(role.get("role_id") or ""),
|
||||
plat=str(role.get("plat_id") or "1"),
|
||||
areaid=str(area_id),
|
||||
)
|
||||
balance = result.get("balance")
|
||||
self._apply_xpd_role_to_account(account, role, area_id)
|
||||
account.xpd_balance = balance
|
||||
account.xpd_bind_status = "xpd_balance_queried"
|
||||
db.commit()
|
||||
if balance is None:
|
||||
self._mark_task(db, task, "failed", "未获取到小店点券余额")
|
||||
return
|
||||
self._mark_task(
|
||||
db,
|
||||
task,
|
||||
"success",
|
||||
f"小店点券余额: {balance}",
|
||||
{"balance": balance, "role": role, "area_id": area_id},
|
||||
)
|
||||
|
||||
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)
|
||||
@@ -2284,6 +2443,9 @@ class DouyuBatchRunner:
|
||||
"query_gold_balance": self._execute_query_gold_balance,
|
||||
"query_exchange_records": self._execute_query_exchange_records,
|
||||
"prefetch_csrf_token": self._execute_prefetch_csrf_token,
|
||||
"query_xpd_role": self._execute_query_xpd_role,
|
||||
"refresh_xpd_goods": self._execute_refresh_xpd_goods,
|
||||
"query_xpd_balance": self._execute_query_xpd_balance,
|
||||
}.get(task.task_type)
|
||||
if handler is None:
|
||||
self._mark_task(worker_db, task, "failed", "不支持的任务类型")
|
||||
|
||||
@@ -38,6 +38,9 @@ SUPPORTED_DOUYU_TASK_TYPES = {
|
||||
"refresh_goods": "刷新商品列表",
|
||||
"query_exchange_records": "一键查询兑换记录",
|
||||
"prefetch_csrf_token": "一键获取兑换 CSRF Token",
|
||||
"query_xpd_role": "查询小店绑定角色",
|
||||
"refresh_xpd_goods": "刷新小店商品列表",
|
||||
"query_xpd_balance": "查询小店点券余额",
|
||||
}
|
||||
|
||||
|
||||
@@ -59,6 +62,9 @@ DOUYU_CONFIG_DEFAULTS = {
|
||||
"gold_pay_type": 1,
|
||||
"gift_id": "23643",
|
||||
"skin_id": "2942",
|
||||
"xpd_act_alias": "20260623KDQFH",
|
||||
"xpd_act_id": "46195",
|
||||
"xpd_rid": "9263298",
|
||||
}
|
||||
|
||||
DOUYU_CONFIG_FIELDS = tuple(DOUYU_CONFIG_DEFAULTS.keys())
|
||||
@@ -169,7 +175,7 @@ def create_douyu_planned_tasks(
|
||||
raise ValueError("不支持的任务类型")
|
||||
|
||||
accounts = visible_douyu_task_accounts(db, account_ids)
|
||||
if task_type in {"refresh_goods", "refresh_esports_goods"} and accounts:
|
||||
if task_type in {"refresh_goods", "refresh_esports_goods", "refresh_xpd_goods"} and accounts:
|
||||
# 商品快照是全局数据,一个可用 CK 足够;没有 CK 时前端无法选账号创建任务。
|
||||
accounts = accounts[:1]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user