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:
yml2213
2026-08-06 17:23:59 +08:00
parent f8de495029
commit fdc74eba8c
12 changed files with 702 additions and 25 deletions
+163 -1
View File
@@ -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", "不支持的任务类型")
+7 -1
View File
@@ -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]