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:
@@ -4,10 +4,12 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
from urllib.parse import unquote
|
||||
from urllib.parse import parse_qs, unquote, urlsplit
|
||||
|
||||
import requests
|
||||
|
||||
@@ -813,3 +815,230 @@ class DouyuActivityClient:
|
||||
)
|
||||
data = payload.get("data") or {}
|
||||
return {"count": data.get("count"), "raw": payload}
|
||||
|
||||
# ---- 和平小店(腾讯道聚城 H5)----
|
||||
XPD_EMBED_API = "https://www.douyu.com/japi/carnival/nc/txEmbed/getIframeUrl"
|
||||
XPD_H5_REFERER = "https://www.douyu.com/topic/h5/hpxd01"
|
||||
XPD_DAOJU_REFERER = "https://app.daoju.qq.com/"
|
||||
XPD_GET_ROLE_API = "https://apps.game.qq.com/daoju/igw/live"
|
||||
# 2026-08 抓包(8.6-和平小店)实证:本期走 xn_live_cjm 变体(旧期为 recommend_live/common)
|
||||
XPD_RECOMMEND_API = "https://apps.game.qq.com/daoju/v3/recommend_xn_live_cjm/common"
|
||||
XPD_BALANCE_API = "https://apps.game.qq.com/daoju/igw/live/"
|
||||
XPD_UA = (
|
||||
"Mozilla/5.0 (Linux; Android 12; HBN-AL00 Build/V417IR; wv) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 "
|
||||
"Chrome/101.0.4951.61 Mobile Safari/537.36, Douyu_Android"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _xpd_int(value: Any) -> int | None:
|
||||
try:
|
||||
return int(value)
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _xpd_parse_var(text: str, varname: str) -> dict[str, Any]:
|
||||
"""解析道聚城 `var xxx={...};` 形式的响应(容忍等号两侧空白)。"""
|
||||
match = re.search(rf"var\s+{varname}\s*=", text)
|
||||
if not match:
|
||||
raise DouyuActivityError(f"响应中找不到 var {varname}=: {text[:200]}")
|
||||
chunk = text[match.end():].strip()
|
||||
if chunk.endswith(";"):
|
||||
chunk = chunk[:-1]
|
||||
try:
|
||||
return json.loads(chunk)
|
||||
except json.JSONDecodeError as exc:
|
||||
raise DouyuActivityError(f"var {varname}= 解析失败: {chunk[:200]}") from exc
|
||||
|
||||
@classmethod
|
||||
def _xpd_daoju_headers(cls) -> dict[str, str]:
|
||||
return {
|
||||
"User-Agent": cls.XPD_UA,
|
||||
"Accept": "*/*",
|
||||
"Accept-Language": "zh-CN,zh;q=0.9",
|
||||
"X-Requested-With": "air.tv.douyu.android",
|
||||
"Sec-Fetch-Site": "same-site",
|
||||
"Sec-Fetch-Mode": "no-cors",
|
||||
"Sec-Fetch-Dest": "script",
|
||||
"Referer": cls.XPD_DAOJU_REFERER,
|
||||
}
|
||||
|
||||
def _xpd_token(self) -> str:
|
||||
"""拼斗鱼 token: acf_uid_1_acf_stk_0_acf_ltkid。"""
|
||||
uid = cookie_value(self.cookie, "acf_uid")
|
||||
stk = cookie_value(self.cookie, "acf_stk")
|
||||
ltkid = cookie_value(self.cookie, "acf_ltkid")
|
||||
if not uid or not stk or not ltkid:
|
||||
raise DouyuActivityError("Cookie 缺少 acf_uid/acf_stk/acf_ltkid,无法获取小店参数")
|
||||
return f"{uid}_1_{stk}_0_{ltkid}"
|
||||
|
||||
def xpd_embed_query(self, *, act_alias: str, rid: str) -> dict[str, Any]:
|
||||
"""从斗鱼换取和平小店 H5 参数(code/sig/timestamp/actId 等,每次现签)。"""
|
||||
payload = self._request_json(
|
||||
"get",
|
||||
self.XPD_EMBED_API,
|
||||
"获取小店 H5 参数",
|
||||
params={"actAlias": act_alias, "rid": rid, "token": self._xpd_token()},
|
||||
headers={"Referer": self.XPD_H5_REFERER},
|
||||
)
|
||||
if payload.get("error") not in (0, "0", None):
|
||||
raise DouyuActivityError(payload.get("msg") or "获取小店 H5 参数失败")
|
||||
data = payload.get("data") or {}
|
||||
txurl_h5 = str(data.get("txurlH5") or "")
|
||||
if not txurl_h5:
|
||||
raise DouyuActivityError(f"获取小店 H5 参数失败: {payload}")
|
||||
query = parse_qs(urlsplit(txurl_h5).query)
|
||||
embed_query = {key: values[0] for key, values in query.items() if values}
|
||||
return {"query": embed_query, "txurl_h5": txurl_h5, "raw": payload}
|
||||
|
||||
def xpd_get_role(
|
||||
self,
|
||||
*,
|
||||
embed_query: dict[str, str],
|
||||
act_id: str,
|
||||
rid: str = "",
|
||||
) -> dict[str, Any]:
|
||||
"""用小店 code/sig 换取当前绑定角色信息。
|
||||
|
||||
2026-08 起道聚城校验升级:必须带 isCode=1 + authType=delegate + sAnchorId(房间号)。
|
||||
"""
|
||||
params = {
|
||||
"acctype": "livelink",
|
||||
"_jsvar": "info",
|
||||
"_service": "other.livelink.getrole.out",
|
||||
"_biz_code": "cjm",
|
||||
"_act_id": act_id,
|
||||
"_app_id": "2123",
|
||||
"isCode": "1",
|
||||
"gameId": embed_query.get("gameId") or "cjm",
|
||||
"actId": embed_query.get("actId") or "",
|
||||
"appId": embed_query.get("appId") or "bp_cf",
|
||||
"livePlatId": embed_query.get("livePlatId") or "douyu",
|
||||
"code": embed_query.get("code") or "",
|
||||
"timestamp": embed_query.get("timestamp") or "",
|
||||
"v": embed_query.get("v") or "",
|
||||
"sig": embed_query.get("sig") or "",
|
||||
"authType": "delegate",
|
||||
"sAnchorId": rid,
|
||||
"sVideoId": "",
|
||||
}
|
||||
response = self._request(
|
||||
"get",
|
||||
self.XPD_GET_ROLE_API,
|
||||
source="查询小店绑定角色",
|
||||
params=params,
|
||||
headers=self._xpd_daoju_headers(),
|
||||
)
|
||||
info = self._xpd_parse_var(response.text, "info")
|
||||
return {
|
||||
"game_open_id": str(info.get("gameOpenId") or ""),
|
||||
"role_id": str(info.get("roleId") or ""),
|
||||
"role_name": str(info.get("roleName") or ""),
|
||||
"type": str(info.get("type") or ""),
|
||||
"plat_id": str(info.get("platId") or ""),
|
||||
"area": str(info.get("area") or ""),
|
||||
"raw": info,
|
||||
}
|
||||
|
||||
def xpd_list_goods(
|
||||
self,
|
||||
*,
|
||||
embed_query: dict[str, str],
|
||||
act_id: str,
|
||||
openid: str,
|
||||
roleid: str,
|
||||
areaid: str = "1",
|
||||
) -> dict[str, Any]:
|
||||
"""查询和平小店推荐商品(category=76 全部商品)。"""
|
||||
params = {
|
||||
"_service": "act.recommend.query",
|
||||
"_biz_code": "cjm",
|
||||
"_app_id": "2123",
|
||||
"app_id": "2123",
|
||||
"page_begin": "0",
|
||||
"page_num": "1",
|
||||
"page_size": "200",
|
||||
"fields": (
|
||||
"iRelationGoodsid,dtModifyTime,sBuyLimitInfo,sGoodsWaterMark,dtShowBeginTime,"
|
||||
"dtShowEndTime,iSort,iGoodsId,sGoodsName,iJbPrice,iJbOrgPrice,iJb2Price,"
|
||||
"iJb2OrgPrice,iPrice,iOrgPrice,sGoodsPic,sGoodsDesc,sPayType,dtBeginTime,"
|
||||
"dtEndTime,iActionId,sExtShowInfo,sExtInfo,iCategoryId,dtRushBegin,dtRushEnd"
|
||||
),
|
||||
"actionFields": "iActionId,sActionName,dtBeginTime,dtEndTime",
|
||||
"order_by": "dtShowBeginTime",
|
||||
"orderColumnBatch": '[{"key":"iSort","sort":"desc"},{"key":"dtShowBeginTime","sort":"desc"},{"key":"iGoodsId","sort":"asc"}]',
|
||||
"desc": "1",
|
||||
"_act_id": act_id,
|
||||
"actid": act_id,
|
||||
"category": "76",
|
||||
"userid": openid,
|
||||
"roleid": roleid,
|
||||
"area": "1",
|
||||
"areaid": str(areaid),
|
||||
"need_limit": "1",
|
||||
}
|
||||
response = self._request(
|
||||
"get",
|
||||
self.XPD_RECOMMEND_API,
|
||||
source="刷新小店商品",
|
||||
params=params,
|
||||
headers=self._xpd_daoju_headers(),
|
||||
)
|
||||
recommend = self._xpd_parse_var(response.text, "recommend")
|
||||
items = (((recommend.get("data") or {}).get("client_data") or {}).get("itemsdetail")) or []
|
||||
goods = []
|
||||
for item in items:
|
||||
goods.append(
|
||||
{
|
||||
"commodity_id": str(item.get("iGoodsId") or ""),
|
||||
"name": str(item.get("sGoodsName") or ""),
|
||||
"price": self._xpd_int(item.get("iPrice")),
|
||||
"org_price": self._xpd_int(item.get("iOrgPrice")),
|
||||
"category": str(item.get("iCategoryId") or ""),
|
||||
"goods_left": self._xpd_int(item.get("iGoodsLeft")),
|
||||
"raw": item,
|
||||
}
|
||||
)
|
||||
return {"goods": goods, "raw": recommend}
|
||||
|
||||
def xpd_balance(
|
||||
self,
|
||||
*,
|
||||
embed_query: dict[str, str],
|
||||
act_id: str,
|
||||
openid: str,
|
||||
roleid: str,
|
||||
plat: str,
|
||||
areaid: str = "1",
|
||||
) -> dict[str, Any]:
|
||||
"""查询和平小店角色点券余额。"""
|
||||
params = {
|
||||
"_jsvar": "banlanceInfo",
|
||||
"_service": "pay.midas.dq.get.ttpp",
|
||||
"_app_id": "2123",
|
||||
"acctype": "ttpp",
|
||||
"areaid": str(areaid),
|
||||
"eventid": "",
|
||||
"interwork": "0",
|
||||
"openid": openid,
|
||||
"openkey": "openkey",
|
||||
"partition": "0",
|
||||
"pay_token": "",
|
||||
"plat": str(plat),
|
||||
"plat_pc": "0",
|
||||
"roleid": roleid,
|
||||
"_biz_code": "cjm",
|
||||
"_act_id": act_id,
|
||||
"_time": str(int(time.time())),
|
||||
"_sid": "6",
|
||||
}
|
||||
response = self._request(
|
||||
"get",
|
||||
self.XPD_BALANCE_API,
|
||||
source="查询小店点券余额",
|
||||
params=params,
|
||||
headers=self._xpd_daoju_headers(),
|
||||
)
|
||||
info = self._xpd_parse_var(response.text, "banlanceInfo")
|
||||
return {"balance": self._xpd_int(info.get("balance")), "raw": info}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
"""新增斗鱼和平小店配置、账号字段与商品快照
|
||||
|
||||
Revision ID: 20260806_0016
|
||||
Revises: 20260805_0015
|
||||
Create Date: 2026-08-06
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = "20260806_0016"
|
||||
down_revision: Union[str, None] = "20260805_0015"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def _columns(bind, table_name: str) -> set[str]:
|
||||
if not sa.inspect(bind).has_table(table_name):
|
||||
return set()
|
||||
return {column["name"] for column in sa.inspect(bind).get_columns(table_name)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
config_columns = _columns(bind, "douyu_config")
|
||||
for column in [
|
||||
sa.Column("xpd_act_alias", sa.String(length=64), nullable=True),
|
||||
sa.Column("xpd_act_id", sa.String(length=64), nullable=True),
|
||||
sa.Column("xpd_rid", sa.String(length=64), nullable=True),
|
||||
]:
|
||||
if column.name not in config_columns:
|
||||
op.add_column("douyu_config", column)
|
||||
|
||||
account_columns = _columns(bind, "accounts")
|
||||
for column in [
|
||||
sa.Column("xpd_game_name", sa.String(length=128), nullable=True),
|
||||
sa.Column("xpd_openid", sa.String(length=128), nullable=True),
|
||||
sa.Column("xpd_role_id", sa.String(length=64), nullable=True),
|
||||
sa.Column("xpd_plat_id", sa.Integer(), nullable=True),
|
||||
sa.Column("xpd_area_id", sa.Integer(), nullable=True),
|
||||
sa.Column("xpd_balance", sa.Integer(), nullable=True),
|
||||
sa.Column("xpd_bind_status", sa.String(length=32), nullable=True),
|
||||
]:
|
||||
if column.name not in account_columns:
|
||||
op.add_column("accounts", column)
|
||||
|
||||
if not sa.inspect(bind).has_table("douyu_xpd_goods_snapshot"):
|
||||
op.create_table(
|
||||
"douyu_xpd_goods_snapshot",
|
||||
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
|
||||
sa.Column("commodity_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("name", sa.String(length=256), nullable=True),
|
||||
sa.Column("price", sa.Integer(), nullable=True),
|
||||
sa.Column("org_price", sa.Integer(), nullable=True),
|
||||
sa.Column("category", sa.String(length=32), nullable=True),
|
||||
sa.Column("goods_left", sa.Integer(), nullable=True),
|
||||
sa.Column("raw", sa.JSON(), nullable=True),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=True),
|
||||
)
|
||||
op.create_index(
|
||||
"ix_douyu_xpd_goods_snapshot_commodity_id",
|
||||
"douyu_xpd_goods_snapshot",
|
||||
["commodity_id"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
if sa.inspect(bind).has_table("douyu_xpd_goods_snapshot"):
|
||||
op.drop_table("douyu_xpd_goods_snapshot")
|
||||
|
||||
account_columns = _columns(bind, "accounts")
|
||||
for column_name in [
|
||||
"xpd_bind_status",
|
||||
"xpd_balance",
|
||||
"xpd_area_id",
|
||||
"xpd_plat_id",
|
||||
"xpd_role_id",
|
||||
"xpd_openid",
|
||||
"xpd_game_name",
|
||||
]:
|
||||
if column_name in account_columns:
|
||||
op.drop_column("accounts", column_name)
|
||||
|
||||
config_columns = _columns(bind, "douyu_config")
|
||||
for column_name in ["xpd_rid", "xpd_act_id", "xpd_act_alias"]:
|
||||
if column_name in config_columns:
|
||||
op.drop_column("douyu_config", column_name)
|
||||
@@ -70,6 +70,13 @@ class Account(Base):
|
||||
esports_bind_status = Column(String(32), default="")
|
||||
esports_change_role_wait_time = Column(Integer, nullable=True)
|
||||
esports_can_change_time = Column(Integer, nullable=True)
|
||||
xpd_game_name = Column(String(128), default="")
|
||||
xpd_openid = Column(String(128), default="")
|
||||
xpd_role_id = Column(String(64), default="")
|
||||
xpd_plat_id = Column(Integer, nullable=True)
|
||||
xpd_area_id = Column(Integer, nullable=True)
|
||||
xpd_balance = Column(Integer, nullable=True)
|
||||
xpd_bind_status = Column(String(32), default="")
|
||||
created_at = Column(DateTime, default=_utcnow)
|
||||
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
|
||||
|
||||
@@ -132,6 +139,9 @@ class DouyuConfig(Base):
|
||||
esports_chicken_skin_id = Column(String(64), default="0")
|
||||
esports_firework_gift_id = Column(String(64), default="24767")
|
||||
esports_firework_skin_id = Column(String(64), default="3850")
|
||||
xpd_act_alias = Column(String(64), default="20260623KDQFH")
|
||||
xpd_act_id = Column(String(64), default="46195")
|
||||
xpd_rid = Column(String(64), default="9263298")
|
||||
gold_pay_type = Column(Integer, default=1)
|
||||
gift_id = Column(String(64), default="23643")
|
||||
skin_id = Column(String(64), default="2942")
|
||||
@@ -151,6 +161,21 @@ class DouyuGoodsSnapshot(Base):
|
||||
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
|
||||
|
||||
|
||||
class DouyuXpdGoodsSnapshot(Base):
|
||||
"""斗鱼和平小店商品快照"""
|
||||
__tablename__ = "douyu_xpd_goods_snapshot"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
commodity_id = Column(String(64), nullable=False, index=True)
|
||||
name = Column(String(256), default="")
|
||||
price = Column(Integer, nullable=True)
|
||||
org_price = Column(Integer, nullable=True)
|
||||
category = Column(String(32), default="")
|
||||
goods_left = Column(Integer, nullable=True)
|
||||
raw = Column(JSON, nullable=True)
|
||||
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
|
||||
|
||||
|
||||
class DouyuEsportsGoodsSnapshot(Base):
|
||||
"""斗鱼电竞手册皮肤商城商品快照"""
|
||||
__tablename__ = "douyu_esports_goods_snapshot"
|
||||
|
||||
@@ -12,7 +12,7 @@ from sqlalchemy.orm import Session, joinedload
|
||||
|
||||
from ..database import SessionLocal, get_db
|
||||
from ..deps import authenticate_websocket, get_current_user, require_permission
|
||||
from ..models import Account, DouyuConfig, DouyuEsportsGoodsSnapshot, DouyuGoodsSnapshot, DouyuTask, User
|
||||
from ..models import Account, DouyuConfig, DouyuEsportsGoodsSnapshot, DouyuGoodsSnapshot, DouyuTask, DouyuXpdGoodsSnapshot, User
|
||||
from ..permissions import user_has_permission
|
||||
from ..schemas import (
|
||||
DouyuConfigOut,
|
||||
@@ -21,6 +21,7 @@ from ..schemas import (
|
||||
DouyuTaskAccountOut,
|
||||
DouyuTaskBatchRequest,
|
||||
DouyuTaskOut,
|
||||
DouyuXpdGoodsOut,
|
||||
)
|
||||
from ..services.douyu_runner import DouyuBatchRunner, douyu_batch_registry
|
||||
from ..services.douyu_service import (
|
||||
@@ -113,6 +114,13 @@ def _account_out(account: Account) -> DouyuTaskAccountOut:
|
||||
esports_bind_status=account.esports_bind_status or "",
|
||||
esports_change_role_wait_time=account.esports_change_role_wait_time,
|
||||
esports_can_change_time=account.esports_can_change_time,
|
||||
xpd_game_name=account.xpd_game_name or "",
|
||||
xpd_openid=account.xpd_openid or "",
|
||||
xpd_role_id=account.xpd_role_id or "",
|
||||
xpd_plat_id=account.xpd_plat_id,
|
||||
xpd_area_id=account.xpd_area_id,
|
||||
xpd_balance=account.xpd_balance,
|
||||
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,
|
||||
)
|
||||
@@ -168,6 +176,11 @@ def _sanitize_task_result(result: dict | None, task_type: str, *, include_detail
|
||||
):
|
||||
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)
|
||||
@@ -223,6 +236,9 @@ def _config_out(config: DouyuConfig) -> DouyuConfigOut:
|
||||
esports_chicken_skin_id=douyu_config_value("esports_chicken_skin_id", config.esports_chicken_skin_id),
|
||||
esports_firework_gift_id=douyu_config_value("esports_firework_gift_id", config.esports_firework_gift_id),
|
||||
esports_firework_skin_id=douyu_config_value("esports_firework_skin_id", config.esports_firework_skin_id),
|
||||
xpd_act_alias=douyu_config_value("xpd_act_alias", config.xpd_act_alias),
|
||||
xpd_act_id=douyu_config_value("xpd_act_id", config.xpd_act_id),
|
||||
xpd_rid=douyu_config_value("xpd_rid", config.xpd_rid),
|
||||
gold_pay_type=douyu_config_value("gold_pay_type", config.gold_pay_type),
|
||||
gift_id=douyu_config_value("gift_id", config.gift_id),
|
||||
skin_id=douyu_config_value("skin_id", config.skin_id),
|
||||
@@ -267,6 +283,7 @@ def list_task_accounts(
|
||||
Account.tag.ilike(pattern),
|
||||
Account.game_name.ilike(pattern),
|
||||
Account.esports_game_name.ilike(pattern),
|
||||
Account.xpd_game_name.ilike(pattern),
|
||||
))
|
||||
total = None
|
||||
if page is not None:
|
||||
@@ -331,6 +348,16 @@ def list_esports_goods(
|
||||
return rows
|
||||
|
||||
|
||||
@router.get("/xpd-goods", response_model=list[DouyuXpdGoodsOut])
|
||||
def list_xpd_goods(
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(require_permission("douyu:task")),
|
||||
):
|
||||
"""查看已缓存的和平小店商品。"""
|
||||
rows = db.query(DouyuXpdGoodsSnapshot).order_by(DouyuXpdGoodsSnapshot.id.asc()).all()
|
||||
return rows
|
||||
|
||||
|
||||
@router.post("/tasks/batch")
|
||||
async def create_task_batch(
|
||||
req: DouyuTaskBatchRequest,
|
||||
|
||||
@@ -554,6 +554,9 @@ class DouyuConfigOut(BaseModel):
|
||||
esports_chicken_skin_id: str = "0"
|
||||
esports_firework_gift_id: str = "24767"
|
||||
esports_firework_skin_id: str = "3850"
|
||||
xpd_act_alias: str = "20260623KDQFH"
|
||||
xpd_act_id: str = "46195"
|
||||
xpd_rid: str = "9263298"
|
||||
gold_pay_type: int = 1
|
||||
gift_id: str = "23643"
|
||||
skin_id: str = "2942"
|
||||
@@ -576,6 +579,9 @@ class DouyuConfigOut(BaseModel):
|
||||
"esports_chicken_skin_id": self.esports_chicken_skin_id,
|
||||
"esports_firework_gift_id": self.esports_firework_gift_id,
|
||||
"esports_firework_skin_id": self.esports_firework_skin_id,
|
||||
"xpd_act_alias": self.xpd_act_alias,
|
||||
"xpd_act_id": self.xpd_act_id,
|
||||
"xpd_rid": self.xpd_rid,
|
||||
"gold_pay_type": self.gold_pay_type,
|
||||
"gift_id": self.gift_id,
|
||||
"skin_id": self.skin_id,
|
||||
@@ -598,6 +604,9 @@ class DouyuConfigUpdate(BaseModel):
|
||||
esports_chicken_skin_id: Optional[str] = None
|
||||
esports_firework_gift_id: Optional[str] = None
|
||||
esports_firework_skin_id: Optional[str] = None
|
||||
xpd_act_alias: Optional[str] = None
|
||||
xpd_act_id: Optional[str] = None
|
||||
xpd_rid: Optional[str] = None
|
||||
gold_pay_type: Optional[int] = Field(None, ge=1, le=9)
|
||||
gift_id: Optional[str] = None
|
||||
skin_id: Optional[str] = None
|
||||
@@ -689,10 +698,45 @@ class DouyuTaskAccountOut(BaseModel):
|
||||
esports_bind_status: str = ""
|
||||
esports_change_role_wait_time: Optional[int] = None
|
||||
esports_can_change_time: Optional[int] = None
|
||||
xpd_game_name: str = ""
|
||||
xpd_openid: str = ""
|
||||
xpd_role_id: str = ""
|
||||
xpd_plat_id: Optional[int] = None
|
||||
xpd_area_id: Optional[int] = None
|
||||
xpd_balance: Optional[int] = None
|
||||
xpd_bind_status: str = ""
|
||||
assigned_to: Optional[int] = None
|
||||
assigned_username: Optional[str] = None
|
||||
|
||||
|
||||
class DouyuXpdGoodsOut(BaseModel):
|
||||
id: int
|
||||
commodity_id: str
|
||||
name: str = ""
|
||||
price: Optional[int] = None
|
||||
org_price: Optional[int] = None
|
||||
category: str = ""
|
||||
goods_left: Optional[int] = None
|
||||
raw: Optional[dict[str, Any]] = None
|
||||
updated_at: Optional[datetime] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@model_serializer
|
||||
def _serialize(self) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"commodity_id": self.commodity_id,
|
||||
"name": self.name,
|
||||
"price": self.price,
|
||||
"org_price": self.org_price,
|
||||
"category": self.category,
|
||||
"goods_left": self.goods_left,
|
||||
"raw": self.raw,
|
||||
"updated_at": _ensure_tz(self.updated_at).isoformat() if self.updated_at else None,
|
||||
}
|
||||
|
||||
|
||||
# ---- 代理配置 ----
|
||||
class ProxyConfigOut(BaseModel):
|
||||
enabled: bool = False
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -75,6 +75,7 @@ function AppContent() {
|
||||
<Route path="douyu/tasks" element={<Navigate to="/douyu/elite" replace />} />
|
||||
<Route path="douyu/elite" element={lazyRoute(<DouyuTasksPage handbook="elite" />)} />
|
||||
<Route path="douyu/esports" element={lazyRoute(<DouyuTasksPage handbook="esports" />)} />
|
||||
<Route path="douyu/peace" element={lazyRoute(<DouyuTasksPage handbook="peace" />)} />
|
||||
<Route path="huya/accounts" element={lazyRoute(<HuyaAccountsPage />)} />
|
||||
<Route path="huya/register" element={lazyRoute(<HuyaRegisterPage />)} />
|
||||
<Route path="huya/assignments" element={lazyRoute(<HuyaAssignmentsPage />)} />
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
DouyuTaskBatchRequest,
|
||||
DouyuTaskBatchResult,
|
||||
DouyuTaskItem,
|
||||
DouyuXpdGoodsItem,
|
||||
MessageResponse,
|
||||
PageParams,
|
||||
PaginatedResponse,
|
||||
@@ -21,6 +22,7 @@ export const douyuApi = {
|
||||
updateConfig: (data: Partial<DouyuConfig>) => api.put<DouyuConfig, DouyuConfig>('/douyu/config', data),
|
||||
listGoods: () => api.get<DouyuGoodsItem[], DouyuGoodsItem[]>('/douyu/goods'),
|
||||
listEsportsGoods: () => api.get<DouyuGoodsItem[], DouyuGoodsItem[]>('/douyu/esports-goods'),
|
||||
listPeaceGoods: () => api.get<DouyuXpdGoodsItem[], DouyuXpdGoodsItem[]>('/douyu/xpd-goods'),
|
||||
createTasks: (data: DouyuTaskBatchRequest) =>
|
||||
api.post<DouyuTaskBatchResult, DouyuTaskBatchResult>('/douyu/tasks/batch', data),
|
||||
listTasks: (batchId?: string) =>
|
||||
|
||||
@@ -227,6 +227,13 @@ export interface DouyuTaskAccountItem {
|
||||
esports_bind_status: string;
|
||||
esports_change_role_wait_time: number | null;
|
||||
esports_can_change_time: number | null;
|
||||
xpd_game_name: string;
|
||||
xpd_openid: string;
|
||||
xpd_role_id: string;
|
||||
xpd_plat_id: number | null;
|
||||
xpd_area_id: number | null;
|
||||
xpd_balance: number | null;
|
||||
xpd_bind_status: string;
|
||||
assigned_to: number | null;
|
||||
assigned_username: string | null;
|
||||
}
|
||||
@@ -246,6 +253,9 @@ export interface DouyuConfig {
|
||||
esports_chicken_skin_id: string;
|
||||
esports_firework_gift_id: string;
|
||||
esports_firework_skin_id: string;
|
||||
xpd_act_alias: string;
|
||||
xpd_act_id: string;
|
||||
xpd_rid: string;
|
||||
gold_pay_type: number;
|
||||
gift_id: string;
|
||||
skin_id: string;
|
||||
@@ -291,6 +301,13 @@ export interface DouyuGoodsItem {
|
||||
updated_at: string | null;
|
||||
}
|
||||
|
||||
export interface DouyuXpdGoodsItem extends DouyuGoodsItem {
|
||||
price: number | null;
|
||||
org_price: number | null;
|
||||
category: string;
|
||||
goods_left: number | null;
|
||||
}
|
||||
|
||||
// ==================== Huya ====================
|
||||
|
||||
export interface HuyaAccountItem {
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
CloudServerOutlined, TeamOutlined, ApiOutlined, KeyOutlined,
|
||||
MenuFoldOutlined, MenuUnfoldOutlined, SwapOutlined,
|
||||
SunOutlined, MoonOutlined, DesktopOutlined, GiftOutlined, ShoppingCartOutlined, ApartmentOutlined, MobileOutlined,
|
||||
BookOutlined, TrophyOutlined,
|
||||
BookOutlined, TrophyOutlined, ShopOutlined,
|
||||
SafetyCertificateOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import { useNavigate, useLocation, Outlet } from 'react-router-dom';
|
||||
@@ -74,6 +74,7 @@ export default function MainLayout({ onLogout }: { onLogout?: () => void }) {
|
||||
if (can('douyu:task')) {
|
||||
douyuItems.push({ key: '/douyu/elite', label: '精英宝典', icon: <BookOutlined /> });
|
||||
douyuItems.push({ key: '/douyu/esports', label: '电竞手册', icon: <TrophyOutlined /> });
|
||||
douyuItems.push({ key: '/douyu/peace', label: '和平小店', icon: <ShopOutlined /> });
|
||||
}
|
||||
|
||||
// 虎牙
|
||||
|
||||
@@ -32,7 +32,7 @@ import { message } from '../utils/antdMessage';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
type HandbookKind = 'elite' | 'esports';
|
||||
type HandbookKind = 'elite' | 'esports' | 'peace';
|
||||
|
||||
const TASK_STATUS_COLORS: Record<string, string> = {
|
||||
planned: 'default',
|
||||
@@ -83,6 +83,12 @@ const ESPORTS_QUICK_ACTIONS = [
|
||||
{ key: 'donate_esports_firework_gift', icon: <GiftOutlined /> },
|
||||
];
|
||||
|
||||
const PEACE_QUICK_ACTIONS = [
|
||||
{ key: 'query_xpd_role', icon: <SearchOutlined /> },
|
||||
{ key: 'refresh_xpd_goods', icon: <ReloadOutlined /> },
|
||||
{ key: 'query_xpd_balance', icon: <SearchOutlined /> },
|
||||
];
|
||||
|
||||
const ELITE_TASK_TYPES = new Set([
|
||||
'get_bind_qr',
|
||||
'confirm_bind',
|
||||
@@ -115,6 +121,11 @@ const ESPORTS_TASK_TYPES = new Set([
|
||||
'donate_esports_chicken_gift',
|
||||
'donate_esports_firework_gift',
|
||||
]);
|
||||
const PEACE_TASK_TYPES = new Set([
|
||||
'query_xpd_role',
|
||||
'refresh_xpd_goods',
|
||||
'query_xpd_balance',
|
||||
]);
|
||||
const DOUYU_GOLD_AMOUNT_STORAGE_KEY = 'douyu_task_gold_amount';
|
||||
const DOUYU_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count';
|
||||
const DOUYU_LAYOUT_MODE_STORAGE_KEY = 'douyu_task_layout_mode';
|
||||
@@ -201,6 +212,13 @@ function goodsLabel(item: DouyuGoodsItem): string {
|
||||
return `${item.name || item.commodity_id}${item.score ? ` / ${item.score}积分` : ''}${stockText}`;
|
||||
}
|
||||
|
||||
function xpdGoodsLabel(item: DouyuGoodsItem): string {
|
||||
const xpd = item as DouyuGoodsItem & { price?: number | null; goods_left?: number | null };
|
||||
const price = xpd.price != null ? ` / ${xpd.price}点券` : '';
|
||||
const left = xpd.goods_left != null ? ` / 剩${xpd.goods_left}` : '';
|
||||
return `${item.name || item.commodity_id}${price}${left}`;
|
||||
}
|
||||
|
||||
function formatWaitSeconds(seconds: number | null | undefined): string {
|
||||
if (seconds == null || Number.isNaN(Number(seconds))) return '';
|
||||
const total = Math.max(0, Math.floor(Number(seconds)));
|
||||
@@ -265,13 +283,22 @@ function savedPositiveInteger(key: string, fallback = 1): number {
|
||||
}
|
||||
|
||||
export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) {
|
||||
const isPeaceHandbook = handbook === 'peace';
|
||||
const isEsportsHandbook = handbook === 'esports';
|
||||
const handbookTitle = isEsportsHandbook ? '电竞手册工作台' : '精英宝典工作台';
|
||||
const handbookDescription = isEsportsHandbook
|
||||
? '电竞手册角色绑定与开通任务'
|
||||
: '精英宝典绑定、开通、积分与兑换任务';
|
||||
const activeTaskTypes = isEsportsHandbook ? ESPORTS_TASK_TYPES : ELITE_TASK_TYPES;
|
||||
const quickActions = isEsportsHandbook ? ESPORTS_QUICK_ACTIONS : ELITE_QUICK_ACTIONS;
|
||||
const handbookTitle = isPeaceHandbook
|
||||
? '和平小店工作台'
|
||||
: (isEsportsHandbook ? '电竞手册工作台' : '精英宝典工作台');
|
||||
const handbookDescription = isPeaceHandbook
|
||||
? '和平小店绑定角色、商品与点券余额查询'
|
||||
: (isEsportsHandbook
|
||||
? '电竞手册角色绑定与开通任务'
|
||||
: '精英宝典绑定、开通、积分与兑换任务');
|
||||
const activeTaskTypes = isPeaceHandbook
|
||||
? PEACE_TASK_TYPES
|
||||
: (isEsportsHandbook ? ESPORTS_TASK_TYPES : ELITE_TASK_TYPES);
|
||||
const quickActions = isPeaceHandbook
|
||||
? PEACE_QUICK_ACTIONS
|
||||
: (isEsportsHandbook ? ESPORTS_QUICK_ACTIONS : ELITE_QUICK_ACTIONS);
|
||||
const { can } = usePermissions();
|
||||
const [accounts, setAccounts] = useState<DouyuTaskAccountItem[]>([]);
|
||||
// 工作台已导入账号 ID(localStorage 持久化,默认空白,用户手动导入/移除)
|
||||
@@ -462,7 +489,9 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
try {
|
||||
const [accResult, goodsResult, taskResult, cfgResult, typeResult] = await Promise.allSettled([
|
||||
workbenchIds.length > 0 ? douyuApi.listAccounts({ ids: workbenchIds.join(',') }) : Promise.resolve([]),
|
||||
isEsportsHandbook ? douyuApi.listEsportsGoods() : douyuApi.listGoods(),
|
||||
isPeaceHandbook
|
||||
? douyuApi.listPeaceGoods()
|
||||
: (isEsportsHandbook ? douyuApi.listEsportsGoods() : douyuApi.listGoods()),
|
||||
douyuApi.listTasks(),
|
||||
canConfig ? douyuApi.getConfig() : Promise.resolve(null),
|
||||
douyuApi.taskTypes(),
|
||||
@@ -508,7 +537,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [canConfig, isEsportsHandbook, workbenchIds]);
|
||||
}, [canConfig, isEsportsHandbook, isPeaceHandbook, workbenchIds]);
|
||||
|
||||
const loadTasks = useCallback(async () => {
|
||||
if (tasksLoadingRef.current) return;
|
||||
@@ -1104,6 +1133,11 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
{
|
||||
title: '游戏名', dataIndex: 'game_name', width: 170,
|
||||
render: (_, record) => {
|
||||
if (isPeaceHandbook) {
|
||||
return record.xpd_game_name
|
||||
? <Text ellipsis title={record.xpd_game_name}>{record.xpd_game_name}</Text>
|
||||
: <Text type="secondary">未查</Text>;
|
||||
}
|
||||
const queryTask = isEsportsHandbook
|
||||
? latestEsportsStateTaskByAccount.get(record.id) || null
|
||||
: latestQueryGameTaskByAccount.get(record.id) || null;
|
||||
@@ -1175,8 +1209,8 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
},
|
||||
},
|
||||
{
|
||||
title: isEsportsHandbook ? '电竞积分' : '积分',
|
||||
dataIndex: isEsportsHandbook ? 'esports_points' : 'points',
|
||||
title: isPeaceHandbook ? '点券' : (isEsportsHandbook ? '电竞积分' : '积分'),
|
||||
dataIndex: isPeaceHandbook ? 'xpd_balance' : (isEsportsHandbook ? 'esports_points' : 'points'),
|
||||
width: 80,
|
||||
render: (v) => v ?? <Text type="secondary">-</Text>,
|
||||
},
|
||||
@@ -1201,6 +1235,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
{
|
||||
title: '换绑时间', dataIndex: 'change_role_wait_time', width: 130,
|
||||
render: (_, record) => {
|
||||
if (isPeaceHandbook) return <Text type="secondary">-</Text>;
|
||||
const fromTask = latestChangeWaitByAccount.get(record.id);
|
||||
const canChangeTime = fromTask?.canChangeTime ?? (
|
||||
isEsportsHandbook ? record.esports_can_change_time : null
|
||||
@@ -1239,6 +1274,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
return <Text type="secondary">{text || '-'}</Text>;
|
||||
},
|
||||
sorter: (a, b) => {
|
||||
if (isPeaceHandbook) return 0;
|
||||
const aw = isEsportsHandbook
|
||||
? latestChangeWaitByAccount.get(a.id)?.canChangeTime ?? a.esports_can_change_time ?? -1
|
||||
: latestChangeWaitByAccount.get(a.id)?.wait
|
||||
@@ -1398,7 +1434,39 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
);
|
||||
const operationBody = (
|
||||
<>
|
||||
{isEsportsHandbook ? (
|
||||
{isPeaceHandbook ? (
|
||||
<div style={compactOperationGridStyle}>
|
||||
<div style={sectionStyle}>
|
||||
<div style={sectionTitleStyle}>
|
||||
<SearchOutlined />
|
||||
<span>小店查询</span>
|
||||
</div>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={6}>
|
||||
{renderActionButton('query_xpd_role', 'primary')}
|
||||
{renderActionButton('query_xpd_balance', 'primary')}
|
||||
{renderActionButton('refresh_xpd_goods')}
|
||||
</Space>
|
||||
</div>
|
||||
<div style={sectionStyle}>
|
||||
<div style={sectionTitleStyle}>
|
||||
<ShoppingOutlined />
|
||||
<span>小店商品</span>
|
||||
</div>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={6}>
|
||||
<Select
|
||||
size="small"
|
||||
value={selectedGoodsId || undefined}
|
||||
onChange={setSelectedGoodsId}
|
||||
options={goods.map((item) => ({ value: item.commodity_id, label: xpdGoodsLabel(item) }))}
|
||||
placeholder="选择小店商品"
|
||||
showSearch
|
||||
optionFilterProp="label"
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
) : isEsportsHandbook ? (
|
||||
<div style={compactOperationGridStyle}>
|
||||
<div style={sectionStyle}>
|
||||
<div style={sectionTitleStyle}>
|
||||
@@ -1690,7 +1758,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
</Card>
|
||||
<Card
|
||||
size="small"
|
||||
title={isEsportsHandbook ? '电竞手册操作' : '精英宝典操作'}
|
||||
title={isPeaceHandbook ? '和平小店操作' : (isEsportsHandbook ? '电竞手册操作' : '精英宝典操作')}
|
||||
extra={operationExtra}
|
||||
style={{ width: 340, flexShrink: 0, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
|
||||
styles={{ body: { padding: 8, flex: 1, minHeight: 0, overflowY: 'auto' } }}
|
||||
@@ -1702,7 +1770,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
<>
|
||||
<Card
|
||||
size="small"
|
||||
title={isEsportsHandbook ? '电竞手册操作' : '精英宝典操作'}
|
||||
title={isPeaceHandbook ? '和平小店操作' : (isEsportsHandbook ? '电竞手册操作' : '精英宝典操作')}
|
||||
extra={operationExtra}
|
||||
style={{ flexShrink: 0, marginBottom: 12 }}
|
||||
styles={{ body: { padding: 8 } }}
|
||||
@@ -1788,12 +1856,12 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
{ title: '标签', dataIndex: 'tag', width: 110, render: (v: string) => (v ? <Tag color="blue">{v}</Tag> : '-') },
|
||||
{
|
||||
title: '游戏名',
|
||||
dataIndex: isEsportsHandbook ? 'esports_game_name' : 'game_name',
|
||||
dataIndex: isPeaceHandbook ? 'xpd_game_name' : (isEsportsHandbook ? 'esports_game_name' : 'game_name'),
|
||||
render: (v) => v || '-',
|
||||
},
|
||||
{
|
||||
title: isEsportsHandbook ? '电竞积分' : '积分',
|
||||
dataIndex: isEsportsHandbook ? 'esports_points' : 'points',
|
||||
title: isPeaceHandbook ? '点券' : (isEsportsHandbook ? '电竞积分' : '积分'),
|
||||
dataIndex: isPeaceHandbook ? 'xpd_balance' : (isEsportsHandbook ? 'esports_points' : 'points'),
|
||||
render: (v) => v ?? '-',
|
||||
},
|
||||
]}
|
||||
@@ -1804,7 +1872,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
|
||||
{/* Config modal */}
|
||||
<Modal
|
||||
title={isEsportsHandbook ? '电竞手册配置' : '精英宝典配置'}
|
||||
title={isPeaceHandbook ? '和平小店配置' : (isEsportsHandbook ? '电竞手册配置' : '精英宝典配置')}
|
||||
open={configOpen}
|
||||
onCancel={() => setConfigOpen(false)}
|
||||
onOk={() => configFormValues && saveConfig(configFormValues)}
|
||||
@@ -1813,7 +1881,11 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
>
|
||||
{configFormValues && (
|
||||
<Space direction="vertical" style={{ width: '100%' }} size={8}>
|
||||
{(isEsportsHandbook ? [
|
||||
{(isPeaceHandbook ? [
|
||||
{ label: '小店活动代号', key: 'xpd_act_alias' },
|
||||
{ label: '道聚城活动 ID', key: 'xpd_act_id' },
|
||||
{ label: '房间 ID', key: 'xpd_rid' },
|
||||
] : isEsportsHandbook ? [
|
||||
{ label: '电竞手册 manualID', key: 'esports_manual_id' },
|
||||
{ label: '电竞手册活动', key: 'esports_act_alias' },
|
||||
{ label: '房间 ID', key: 'room_id' },
|
||||
@@ -1840,7 +1912,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
|
||||
/>
|
||||
</Space>
|
||||
))}
|
||||
{(isEsportsHandbook ? [
|
||||
{(isPeaceHandbook ? [] : isEsportsHandbook ? [
|
||||
{ label: '电竞手册金额(分)', key: 'esports_amount' },
|
||||
] : [
|
||||
{ label: '宝典金额(分)', key: 'elite_amount' },
|
||||
|
||||
Reference in New Issue
Block a user