增加兑换

This commit is contained in:
yml2213
2026-07-28 18:12:11 +08:00
parent 624198bca4
commit 8de269f958
11 changed files with 618 additions and 14 deletions
+107 -4
View File
@@ -50,6 +50,8 @@ class DouyuActivityClient:
ESPORTS_AGREEMENT_API = "https://www.douyu.com/wgapi/ordnc/activity/peacematch/agreement" ESPORTS_AGREEMENT_API = "https://www.douyu.com/wgapi/ordnc/activity/peacematch/agreement"
ESPORTS_ACT_BIND_API = "https://www.douyu.com/wgapi/ordnc/live/memberthird/tencent/actBind" ESPORTS_ACT_BIND_API = "https://www.douyu.com/wgapi/ordnc/live/memberthird/tencent/actBind"
ESPORTS_USER_INFO_API = "https://www.douyu.com/wgapi/ordnc/activity/peacematch/getUserInfo" ESPORTS_USER_INFO_API = "https://www.douyu.com/wgapi/ordnc/activity/peacematch/getUserInfo"
ESPORTS_GOODS_API = "https://www.douyu.com/wgapi/ordnc/activity/peacematch/storehome"
ESPORTS_EXCHANGE_API = "https://www.douyu.com/wgapi/ordnc/activity/peacematch/exchange"
GOLD_QR_API = "https://cz.douyu.com/m/gold/getQrCode" GOLD_QR_API = "https://cz.douyu.com/m/gold/getQrCode"
GOLD_ACCOUNT_API = "https://cz.douyu.com/item/gold/account" GOLD_ACCOUNT_API = "https://cz.douyu.com/item/gold/account"
EXCHANGE_BALANCE_API = "https://www.douyu.com/wjapi/nc/exchange/fim" EXCHANGE_BALANCE_API = "https://www.douyu.com/wjapi/nc/exchange/fim"
@@ -507,6 +509,71 @@ class DouyuActivityClient:
"raw": payload, "raw": payload,
} }
@staticmethod
def _esports_shop_referer(rid: str) -> str:
return (
"https://www.douyu.com/pages/live-peace-pel/web/shop"
f"?roomId={rid or '0'}&isAnchorSide=0"
)
def list_esports_goods(self, *, manual_id: str, rid: str) -> dict[str, Any]:
"""查询电竞手册皮肤商城。"""
payload = self._request_json(
"get",
self.ESPORTS_GOODS_API,
"刷新电竞皮肤商城",
params={"manualID": manual_id, "rid": rid},
headers={
"Origin": "",
"Referer": self._esports_shop_referer(rid),
},
)
goods = (payload.get("data") or {}).get("list") or []
return {
"goods": goods,
"score": (payload.get("data") or {}).get("score"),
"raw": payload,
}
def exchange_esports_goods(
self,
*,
manual_id: str,
rid: str,
commodity_id: str,
quantity: int = 1,
ctn: str | None = None,
) -> dict[str, Any]:
"""兑换电竞手册皮肤商城商品。"""
referer = self._esports_shop_referer(rid)
ctn_value = ctn or self.esports_ctn(referer=referer)
self._set_cookie_value("acf_ccn", ctn_value)
payload = self._request_json(
"post",
self.ESPORTS_EXCHANGE_API,
"兑换电竞皮肤",
data={
"manualID": manual_id,
"commodityID": commodity_id,
"rid": rid or "0",
"num": str(max(1, quantity)),
"ctn": ctn_value,
},
headers={
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Origin": "",
"Referer": referer,
},
)
if payload.get("error") not in (0, "0"):
raise DouyuActivityError(payload.get("msg") or "兑换电竞皮肤失败")
return {
"commodity_id": commodity_id,
"quantity": max(1, quantity),
"ctn": ctn_value,
"raw": payload,
}
def create_gold_qr(self, *, amount: int, pay_type: int = 1, product_id: str = "DYTV_Product_Gold_1") -> dict[str, Any]: def create_gold_qr(self, *, amount: int, pay_type: int = 1, product_id: str = "DYTV_Product_Gold_1") -> dict[str, Any]:
"""生成鱼翅充值支付二维码。""" """生成鱼翅充值支付二维码。"""
payload = self._request_json( payload = self._request_json(
@@ -533,19 +600,20 @@ class DouyuActivityClient:
raise DouyuActivityError(payload.get("msg") or "生成鱼翅充值码失败") raise DouyuActivityError(payload.get("msg") or "生成鱼翅充值码失败")
return {"pay_url": pay_url, "token": token, "raw": payload} return {"pay_url": pay_url, "token": token, "raw": payload}
def donate_elite_gift( def _donate_gift(
self, self,
*, *,
gift_count: int, gift_count: int,
room_id: str, room_id: str,
gift_id: str, gift_id: str,
skin_id: str, skin_id: str,
source: str,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""赠送精英令礼物。""" """赠送直播间礼物。"""
payload = self._request_json( payload = self._request_json(
"post", "post",
self.DONATE_API, self.DONATE_API,
"赠送精英令", source,
data={ data={
"giftId": gift_id, "giftId": gift_id,
"giftCount": str(gift_count), "giftCount": str(gift_count),
@@ -559,9 +627,44 @@ class DouyuActivityClient:
}, },
) )
if payload.get("error") not in (0, "0") and payload.get("msg") != "success": if payload.get("error") not in (0, "0") and payload.get("msg") != "success":
raise DouyuActivityError(payload.get("msg") or "赠送精英令失败") raise DouyuActivityError(payload.get("msg") or f"{source}失败")
return {"raw": payload} return {"raw": payload}
def donate_elite_gift(
self,
*,
gift_count: int,
room_id: str,
gift_id: str,
skin_id: str,
) -> dict[str, Any]:
"""赠送精英令礼物。"""
return self._donate_gift(
gift_count=gift_count,
room_id=room_id,
gift_id=gift_id,
skin_id=skin_id,
source="赠送精英令",
)
def donate_esports_gift(
self,
*,
gift_name: str,
gift_count: int,
room_id: str,
gift_id: str,
skin_id: str,
) -> dict[str, Any]:
"""赠送电竞手册任务指定礼物。"""
return self._donate_gift(
gift_count=gift_count,
room_id=room_id,
gift_id=gift_id,
skin_id=skin_id,
source=f"赠送{gift_name}",
)
def query_points(self, *, uid: str, ctn: str) -> dict[str, Any]: def query_points(self, *, uid: str, ctn: str) -> dict[str, Any]:
"""查询手册积分余额。""" """查询手册积分余额。"""
payload = self._request_json( payload = self._request_json(
@@ -0,0 +1,49 @@
"""新增电竞手册送礼配置
Revision ID: 20260728_0013
Revises: 20260728_0012
Create Date: 2026-07-28
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "20260728_0013"
down_revision: Union[str, None] = "20260728_0012"
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()
columns = _columns(bind, "douyu_config")
for column in [
sa.Column("esports_chicken_gift_id", sa.String(length=64), nullable=True),
sa.Column("esports_chicken_skin_id", sa.String(length=64), nullable=True),
sa.Column("esports_firework_gift_id", sa.String(length=64), nullable=True),
sa.Column("esports_firework_skin_id", sa.String(length=64), nullable=True),
]:
if column.name not in columns:
op.add_column("douyu_config", column)
def downgrade() -> None:
bind = op.get_bind()
columns = _columns(bind, "douyu_config")
for column_name in [
"esports_firework_skin_id",
"esports_firework_gift_id",
"esports_chicken_skin_id",
"esports_chicken_gift_id",
]:
if column_name in columns:
op.drop_column("douyu_config", column_name)
@@ -0,0 +1,44 @@
"""新增电竞手册皮肤商城快照
Revision ID: 20260728_0014
Revises: 20260728_0013
Create Date: 2026-07-28
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "20260728_0014"
down_revision: Union[str, None] = "20260728_0013"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
bind = op.get_bind()
if sa.inspect(bind).has_table("douyu_esports_goods_snapshot"):
return
op.create_table(
"douyu_esports_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("score", sa.Integer(), nullable=True),
sa.Column("status", sa.String(length=32), nullable=True),
sa.Column("raw", sa.JSON(), nullable=True),
sa.Column("updated_at", sa.DateTime(), nullable=True),
)
op.create_index(
"ix_douyu_esports_goods_snapshot_commodity_id",
"douyu_esports_goods_snapshot",
["commodity_id"],
)
def downgrade() -> None:
bind = op.get_bind()
if sa.inspect(bind).has_table("douyu_esports_goods_snapshot"):
op.drop_table("douyu_esports_goods_snapshot")
+17
View File
@@ -128,6 +128,10 @@ class DouyuConfig(Base):
esports_manual_id = Column(String(64), default="1W0yGkOdDbAXb") esports_manual_id = Column(String(64), default="1W0yGkOdDbAXb")
esports_act_alias = Column(String(64), default="20260226ZMGDG") esports_act_alias = Column(String(64), default="20260226ZMGDG")
esports_amount = Column(Integer, default=3000) esports_amount = Column(Integer, default=3000)
esports_chicken_gift_id = Column(String(64), default="24766")
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")
gold_pay_type = Column(Integer, default=1) gold_pay_type = Column(Integer, default=1)
gift_id = Column(String(64), default="23643") gift_id = Column(String(64), default="23643")
skin_id = Column(String(64), default="2942") skin_id = Column(String(64), default="2942")
@@ -147,6 +151,19 @@ class DouyuGoodsSnapshot(Base):
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow) updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
class DouyuEsportsGoodsSnapshot(Base):
"""斗鱼电竞手册皮肤商城商品快照"""
__tablename__ = "douyu_esports_goods_snapshot"
id = Column(Integer, primary_key=True, autoincrement=True)
commodity_id = Column(String(64), nullable=False, index=True)
name = Column(String(256), default="")
score = Column(Integer, nullable=True)
status = Column(String(32), default="")
raw = Column(JSON, nullable=True)
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
class HuyaAccount(Base): class HuyaAccount(Base):
"""虎牙账号""" """虎牙账号"""
__tablename__ = "huya_accounts" __tablename__ = "huya_accounts"
+15 -1
View File
@@ -12,7 +12,7 @@ from sqlalchemy.orm import Session, joinedload
from ..database import SessionLocal, get_db from ..database import SessionLocal, get_db
from ..deps import authenticate_websocket, get_current_user, require_permission from ..deps import authenticate_websocket, get_current_user, require_permission
from ..models import Account, DouyuConfig, DouyuGoodsSnapshot, DouyuTask, User from ..models import Account, DouyuConfig, DouyuEsportsGoodsSnapshot, DouyuGoodsSnapshot, DouyuTask, User
from ..permissions import user_has_permission from ..permissions import user_has_permission
from ..schemas import ( from ..schemas import (
DouyuConfigOut, DouyuConfigOut,
@@ -149,6 +149,10 @@ def _config_out(config: DouyuConfig) -> DouyuConfigOut:
esports_manual_id=douyu_config_value("esports_manual_id", config.esports_manual_id), esports_manual_id=douyu_config_value("esports_manual_id", config.esports_manual_id),
esports_act_alias=douyu_config_value("esports_act_alias", config.esports_act_alias), esports_act_alias=douyu_config_value("esports_act_alias", config.esports_act_alias),
esports_amount=douyu_config_value("esports_amount", config.esports_amount), esports_amount=douyu_config_value("esports_amount", config.esports_amount),
esports_chicken_gift_id=douyu_config_value("esports_chicken_gift_id", config.esports_chicken_gift_id),
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),
gold_pay_type=douyu_config_value("gold_pay_type", config.gold_pay_type), gold_pay_type=douyu_config_value("gold_pay_type", config.gold_pay_type),
gift_id=douyu_config_value("gift_id", config.gift_id), gift_id=douyu_config_value("gift_id", config.gift_id),
skin_id=douyu_config_value("skin_id", config.skin_id), skin_id=douyu_config_value("skin_id", config.skin_id),
@@ -224,6 +228,16 @@ def list_goods(
return rows return rows
@router.get("/esports-goods", response_model=list[DouyuGoodsOut])
def list_esports_goods(
db: Session = Depends(get_db),
current: User = Depends(require_permission("douyu:task")),
):
"""查看已缓存的电竞手册皮肤商城商品。"""
rows = db.query(DouyuEsportsGoodsSnapshot).order_by(DouyuEsportsGoodsSnapshot.id.asc()).all()
return rows
@router.post("/tasks/batch") @router.post("/tasks/batch")
async def create_task_batch( async def create_task_batch(
req: DouyuTaskBatchRequest, req: DouyuTaskBatchRequest,
+12
View File
@@ -545,6 +545,10 @@ class DouyuConfigOut(BaseModel):
esports_manual_id: str = "1W0yGkOdDbAXb" esports_manual_id: str = "1W0yGkOdDbAXb"
esports_act_alias: str = "20260226ZMGDG" esports_act_alias: str = "20260226ZMGDG"
esports_amount: int = 3000 esports_amount: int = 3000
esports_chicken_gift_id: str = "24766"
esports_chicken_skin_id: str = "0"
esports_firework_gift_id: str = "24767"
esports_firework_skin_id: str = "3850"
gold_pay_type: int = 1 gold_pay_type: int = 1
gift_id: str = "23643" gift_id: str = "23643"
skin_id: str = "2942" skin_id: str = "2942"
@@ -563,6 +567,10 @@ class DouyuConfigOut(BaseModel):
"esports_manual_id": self.esports_manual_id, "esports_manual_id": self.esports_manual_id,
"esports_act_alias": self.esports_act_alias, "esports_act_alias": self.esports_act_alias,
"esports_amount": self.esports_amount, "esports_amount": self.esports_amount,
"esports_chicken_gift_id": self.esports_chicken_gift_id,
"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,
"gold_pay_type": self.gold_pay_type, "gold_pay_type": self.gold_pay_type,
"gift_id": self.gift_id, "gift_id": self.gift_id,
"skin_id": self.skin_id, "skin_id": self.skin_id,
@@ -581,6 +589,10 @@ class DouyuConfigUpdate(BaseModel):
esports_manual_id: Optional[str] = None esports_manual_id: Optional[str] = None
esports_act_alias: Optional[str] = None esports_act_alias: Optional[str] = None
esports_amount: Optional[int] = Field(None, ge=1) esports_amount: Optional[int] = Field(None, ge=1)
esports_chicken_gift_id: Optional[str] = None
esports_chicken_skin_id: Optional[str] = None
esports_firework_gift_id: Optional[str] = None
esports_firework_skin_id: Optional[str] = None
gold_pay_type: Optional[int] = Field(None, ge=1, le=9) gold_pay_type: Optional[int] = Field(None, ge=1, le=9)
gift_id: Optional[str] = None gift_id: Optional[str] = None
skin_id: Optional[str] = None skin_id: Optional[str] = None
+259 -1
View File
@@ -15,7 +15,7 @@ from sqlalchemy.orm import Session, joinedload
from core.douyu import DouyuActivityClient, DouyuActivityError from core.douyu import DouyuActivityClient, DouyuActivityError
from ..database import SessionLocal from ..database import SessionLocal
from ..models import Account, DouyuGoodsSnapshot, DouyuTask from ..models import Account, DouyuEsportsGoodsSnapshot, DouyuGoodsSnapshot, DouyuTask
from .douyu_service import ( from .douyu_service import (
DOUYU_CONFIG_FIELDS, DOUYU_CONFIG_FIELDS,
account_uid, account_uid,
@@ -382,6 +382,27 @@ class DouyuBatchRunner:
row.updated_at = now row.updated_at = now
db.commit() db.commit()
def _upsert_esports_goods(self, db: Session, goods: list[dict]) -> None:
now = datetime.now(timezone.utc)
for raw in goods:
commodity_id = str(raw.get("commodityId") or raw.get("commodity_id") or "")
if not commodity_id:
continue
row = (
db.query(DouyuEsportsGoodsSnapshot)
.filter(DouyuEsportsGoodsSnapshot.commodity_id == commodity_id)
.first()
)
if row is None:
row = DouyuEsportsGoodsSnapshot(commodity_id=commodity_id)
db.add(row)
row.name = str(raw.get("commodityName") or raw.get("name") or "")
row.score = self._to_int(raw.get("score"))
row.status = str(raw.get("status") or "")
row.raw = raw
row.updated_at = now
db.commit()
def _config_info(self, db: Session) -> dict: def _config_info(self, db: Session) -> dict:
config = ensure_douyu_config(db) config = ensure_douyu_config(db)
return {field: douyu_config_value(field, getattr(config, field, None)) for field in DOUYU_CONFIG_FIELDS} return {field: douyu_config_value(field, getattr(config, field, None)) for field in DOUYU_CONFIG_FIELDS}
@@ -921,6 +942,32 @@ class DouyuBatchRunner:
account.updated_at = datetime.now(timezone.utc) account.updated_at = datetime.now(timezone.utc)
self._mark_task(db, task, "success", f"已刷新商品 {len(goods)}", {"goods_count": len(goods), "goods": goods}) self._mark_task(db, task, "success", f"已刷新商品 {len(goods)}", {"goods_count": len(goods), "goods": goods})
def _execute_refresh_esports_goods(
self,
db: Session,
task: DouyuTask,
account: Account,
cookie: str,
config: dict,
):
"""刷新电竞手册皮肤商城快照。"""
client = self._client(cookie)
result = client.list_esports_goods(
manual_id=str(config["esports_manual_id"]),
rid=str(config["room_id"]),
)
goods = result["goods"]
self._upsert_esports_goods(db, goods)
account.esports_bind_status = "esports_goods_refreshed"
account.updated_at = datetime.now(timezone.utc)
self._mark_task(
db,
task,
"success",
f"已刷新电竞皮肤 {len(goods)}",
{"goods_count": len(goods), "esports_store_score": result["score"], "goods": goods},
)
def _execute_get_bind_qr(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict): def _execute_get_bind_qr(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
client = self._client(cookie) client = self._client(cookie)
qr_act_alias = self._bind_qr_act_alias(config) qr_act_alias = self._bind_qr_act_alias(config)
@@ -1492,6 +1539,147 @@ class DouyuBatchRunner:
result, result,
) )
def _execute_query_esports_points(
self,
db: Session,
task: DouyuTask,
account: Account,
cookie: str,
config: dict,
):
"""查询电竞手册积分。"""
manual_id = str(config.get("esports_manual_id") or "").strip()
if not manual_id:
self._mark_task(db, task, "failed", "请先配置电竞手册 manualID")
return
client = self._client(cookie)
result = self._refresh_esports_handbook(client, account, manual_id=manual_id)
account.esports_bind_status = "esports_points_queried"
account.updated_at = datetime.now(timezone.utc)
points = result["esports_points"]
self._mark_task(db, task, "success", f"电竞积分: {points if points is not None else '-'}", result)
def _execute_donate_esports_gift(
self,
db: Session,
task: DouyuTask,
account: Account,
cookie: str,
config: dict,
*,
gift_name: str,
config_gift_id_key: str,
config_skin_id_key: str,
):
"""赠送电竞手册任务礼物并刷新独立积分。"""
payload = self._task_payload(task)
try:
gift_count = max(1, int(payload.get("gift_count") or payload.get("count") or 1))
except (TypeError, ValueError):
self._mark_task(db, task, "failed", "赠送数量必须是正整数")
return
manual_id = str(config.get("esports_manual_id") or "").strip()
gift_id = str(payload.get("gift_id") or config.get(config_gift_id_key) or "").strip()
skin_id = str(payload.get("skin_id") or config.get(config_skin_id_key) or "").strip()
room_id = str(payload.get("room_id") or config.get("room_id") or "").strip()
if not manual_id or not gift_id or not skin_id or not room_id:
self._mark_task(db, task, "failed", "请先完整配置电竞手册、房间和礼物参数")
return
client = self._client(cookie)
baseline_points = account.esports_points
try:
baseline = self._refresh_esports_handbook(client, account, manual_id=manual_id)
baseline_points = baseline["esports_points"]
db.commit()
except Exception as exc:
self._push_log("warning", f"赠送{gift_name}前刷新电竞积分失败: {exc}")
result = client.donate_esports_gift(
gift_name=gift_name,
gift_count=gift_count,
room_id=room_id,
gift_id=gift_id,
skin_id=skin_id,
)
result.update(
{
"gift_name": gift_name,
"gift_id": gift_id,
"skin_id": skin_id,
"gift_count": gift_count,
"esports_points_baseline": baseline_points,
}
)
refresh_errors = []
try:
result.update(self._refresh_account_gold_balance(client, account))
except Exception as exc:
refresh_errors.append(f"鱼翅余额: {exc}")
try:
points_result = self._refresh_esports_handbook(client, account, manual_id=manual_id)
result.update(points_result)
result["esports_points_after_gift"] = points_result["esports_points"]
result["esports_points_changed"] = (
baseline_points is not None
and points_result["esports_points"] is not None
and points_result["esports_points"] != baseline_points
)
except Exception as exc:
refresh_errors.append(f"电竞积分: {exc}")
if refresh_errors:
result["refresh_errors"] = refresh_errors
account.esports_bind_status = "esports_gift_donated"
account.updated_at = datetime.now(timezone.utc)
message = f"赠送{gift_name}成功: {gift_count}"
if account.gold_balance is not None:
message += f",鱼翅余额: {account.gold_balance}"
if account.esports_points is not None:
message += f",电竞积分: {account.esports_points}"
self._mark_task(db, task, "success", message, result)
def _execute_donate_esports_chicken_gift(
self,
db: Session,
task: DouyuTask,
account: Account,
cookie: str,
config: dict,
):
"""赠送冠军鸡腿。"""
self._execute_donate_esports_gift(
db,
task,
account,
cookie,
config,
gift_name="冠军鸡腿",
config_gift_id_key="esports_chicken_gift_id",
config_skin_id_key="esports_chicken_skin_id",
)
def _execute_donate_esports_firework_gift(
self,
db: Session,
task: DouyuTask,
account: Account,
cookie: str,
config: dict,
):
"""赠送冠军烟花。"""
self._execute_donate_esports_gift(
db,
task,
account,
cookie,
config,
gift_name="冠军烟花",
config_gift_id_key="esports_firework_gift_id",
config_skin_id_key="esports_firework_skin_id",
)
def _execute_create_gold_qr(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict): def _execute_create_gold_qr(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
payload = self._task_payload(task) payload = self._task_payload(task)
amount = int(payload.get("amount") or payload.get("gold_amount") or 1) amount = int(payload.get("amount") or payload.get("gold_amount") or 1)
@@ -1650,6 +1838,71 @@ class DouyuBatchRunner:
{"goods": goods.raw if goods else None, **result}, {"goods": goods.raw if goods else None, **result},
) )
def _execute_exchange_esports_goods(
self,
db: Session,
task: DouyuTask,
account: Account,
cookie: str,
config: dict,
):
"""兑换电竞手册皮肤,并同步电竞积分。"""
payload = self._task_payload(task)
commodity_id = str(payload.get("commodity_id") or payload.get("commodityId") or "").strip()
if not commodity_id:
self._mark_task(db, task, "failed", "请选择电竞皮肤")
return
try:
quantity = max(1, int(payload.get("quantity") or payload.get("num") or 1))
except (TypeError, ValueError):
self._mark_task(db, task, "failed", "兑换数量必须是正整数")
return
manual_id = str(config.get("esports_manual_id") or "").strip()
room_id = str(config.get("room_id") or "").strip()
if not manual_id or not room_id:
self._mark_task(db, task, "failed", "请先配置电竞手册 manualID 和房间 ID")
return
client = self._client(cookie)
baseline_points = account.esports_points
try:
baseline = self._refresh_esports_handbook(client, account, manual_id=manual_id)
baseline_points = baseline["esports_points"]
db.commit()
except Exception as exc:
self._push_log("warning", f"兑换电竞皮肤前刷新积分失败: {exc}")
result = client.exchange_esports_goods(
manual_id=manual_id,
rid=room_id,
commodity_id=commodity_id,
quantity=quantity,
)
goods = (
db.query(DouyuEsportsGoodsSnapshot)
.filter(DouyuEsportsGoodsSnapshot.commodity_id == commodity_id)
.first()
)
result["goods"] = goods.raw if goods else None
result["esports_points_baseline"] = baseline_points
try:
points_result = self._refresh_esports_handbook(client, account, manual_id=manual_id)
result.update(points_result)
result["esports_points_after_exchange"] = points_result["esports_points"]
except Exception as exc:
result["esports_points_refresh_error"] = str(exc)
account.esports_bind_status = "esports_goods_exchanged"
account.updated_at = datetime.now(timezone.utc)
name = (goods.name if goods else "") or commodity_id
message = f"兑换电竞皮肤成功: {name}"
if quantity > 1:
message += f" x{quantity}"
if account.esports_points is not None:
message += f",电竞积分: {account.esports_points}"
self._mark_task(db, task, "success", message, result)
def _execute_query_game_name(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict): def _execute_query_game_name(self, db: Session, task: DouyuTask, account: Account, cookie: str, config: dict):
client = self._client(cookie) client = self._client(cookie)
query_aliases = self._query_bind_act_aliases(config) query_aliases = self._query_bind_act_aliases(config)
@@ -1870,6 +2123,7 @@ class DouyuBatchRunner:
handler = { handler = {
"refresh_goods": self._execute_refresh_goods, "refresh_goods": self._execute_refresh_goods,
"refresh_esports_goods": self._execute_refresh_esports_goods,
"get_bind_qr": self._execute_get_bind_qr, "get_bind_qr": self._execute_get_bind_qr,
"confirm_bind": self._execute_confirm_bind, "confirm_bind": self._execute_confirm_bind,
"create_elite_qr": self._execute_create_elite_qr, "create_elite_qr": self._execute_create_elite_qr,
@@ -1878,10 +2132,14 @@ class DouyuBatchRunner:
"query_esports_game_name": self._execute_query_esports_game_name, "query_esports_game_name": self._execute_query_esports_game_name,
"confirm_esports_bind": self._execute_confirm_esports_bind, "confirm_esports_bind": self._execute_confirm_esports_bind,
"create_esports_qr": self._execute_create_esports_qr, "create_esports_qr": self._execute_create_esports_qr,
"query_esports_points": self._execute_query_esports_points,
"donate_esports_chicken_gift": self._execute_donate_esports_chicken_gift,
"donate_esports_firework_gift": self._execute_donate_esports_firework_gift,
"create_gold_qr": self._execute_create_gold_qr, "create_gold_qr": self._execute_create_gold_qr,
"donate_elite_gift": self._execute_donate_elite_gift, "donate_elite_gift": self._execute_donate_elite_gift,
"query_points": self._execute_query_points, "query_points": self._execute_query_points,
"exchange_goods": self._execute_exchange_goods, "exchange_goods": self._execute_exchange_goods,
"exchange_esports_goods": self._execute_exchange_esports_goods,
"query_game_name": self._execute_query_game_name, "query_game_name": self._execute_query_game_name,
"query_change_bind_time": self._execute_query_change_bind_time, "query_change_bind_time": self._execute_query_change_bind_time,
"query_limited_goods": self._execute_query_limited_goods, "query_limited_goods": self._execute_query_limited_goods,
+10 -1
View File
@@ -22,6 +22,11 @@ SUPPORTED_DOUYU_TASK_TYPES = {
"query_esports_game_name": "查询最新游戏角色", "query_esports_game_name": "查询最新游戏角色",
"confirm_esports_bind": "确认电竞手册绑定", "confirm_esports_bind": "确认电竞手册绑定",
"create_esports_qr": "开通电竞手册30", "create_esports_qr": "开通电竞手册30",
"query_esports_points": "查询电竞积分",
"donate_esports_chicken_gift": "赠送冠军鸡腿",
"donate_esports_firework_gift": "赠送冠军烟花",
"refresh_esports_goods": "刷新电竞皮肤商城",
"exchange_esports_goods": "兑换电竞皮肤",
"create_gold_qr": "充值鱼翅", "create_gold_qr": "充值鱼翅",
"donate_elite_gift": "赠送精英令", "donate_elite_gift": "赠送精英令",
"query_points": "一键查询积分", "query_points": "一键查询积分",
@@ -47,6 +52,10 @@ DOUYU_CONFIG_DEFAULTS = {
"esports_manual_id": "1W0yGkOdDbAXb", "esports_manual_id": "1W0yGkOdDbAXb",
"esports_act_alias": "20260226ZMGDG", "esports_act_alias": "20260226ZMGDG",
"esports_amount": 3000, "esports_amount": 3000,
"esports_chicken_gift_id": "24766",
"esports_chicken_skin_id": "0",
"esports_firework_gift_id": "24767",
"esports_firework_skin_id": "3850",
"gold_pay_type": 1, "gold_pay_type": 1,
"gift_id": "23643", "gift_id": "23643",
"skin_id": "2942", "skin_id": "2942",
@@ -160,7 +169,7 @@ def create_douyu_planned_tasks(
raise ValueError("不支持的任务类型") raise ValueError("不支持的任务类型")
accounts = visible_douyu_task_accounts(db, account_ids) accounts = visible_douyu_task_accounts(db, account_ids)
if task_type == "refresh_goods" and accounts: if task_type in {"refresh_goods", "refresh_esports_goods"} and accounts:
# 商品快照是全局数据,一个可用 CK 足够;没有 CK 时前端无法选账号创建任务。 # 商品快照是全局数据,一个可用 CK 足够;没有 CK 时前端无法选账号创建任务。
accounts = accounts[:1] accounts = accounts[:1]
+1
View File
@@ -16,6 +16,7 @@ export const douyuApi = {
getConfig: () => api.get<DouyuConfig, DouyuConfig>('/douyu/config'), getConfig: () => api.get<DouyuConfig, DouyuConfig>('/douyu/config'),
updateConfig: (data: Partial<DouyuConfig>) => api.put<DouyuConfig, DouyuConfig>('/douyu/config', data), updateConfig: (data: Partial<DouyuConfig>) => api.put<DouyuConfig, DouyuConfig>('/douyu/config', data),
listGoods: () => api.get<DouyuGoodsItem[], DouyuGoodsItem[]>('/douyu/goods'), listGoods: () => api.get<DouyuGoodsItem[], DouyuGoodsItem[]>('/douyu/goods'),
listEsportsGoods: () => api.get<DouyuGoodsItem[], DouyuGoodsItem[]>('/douyu/esports-goods'),
createTasks: (data: DouyuTaskBatchRequest) => createTasks: (data: DouyuTaskBatchRequest) =>
api.post<DouyuTaskBatchResult, DouyuTaskBatchResult>('/douyu/tasks/batch', data), api.post<DouyuTaskBatchResult, DouyuTaskBatchResult>('/douyu/tasks/batch', data),
listTasks: (batchId?: string) => listTasks: (batchId?: string) =>
+4
View File
@@ -217,6 +217,10 @@ export interface DouyuConfig {
esports_manual_id: string; esports_manual_id: string;
esports_act_alias: string; esports_act_alias: string;
esports_amount: number; esports_amount: number;
esports_chicken_gift_id: string;
esports_chicken_skin_id: string;
esports_firework_gift_id: string;
esports_firework_skin_id: string;
gold_pay_type: number; gold_pay_type: number;
gift_id: string; gift_id: string;
skin_id: string; skin_id: string;
+100 -7
View File
@@ -44,6 +44,13 @@ const ESPORTS_QUICK_ACTIONS = [
{ key: 'prepare_esports_bind', icon: <QrcodeOutlined /> }, { key: 'prepare_esports_bind', icon: <QrcodeOutlined /> },
{ key: 'query_esports_game_name', icon: <SearchOutlined /> }, { key: 'query_esports_game_name', icon: <SearchOutlined /> },
{ key: 'create_esports_qr', icon: <CreditCardOutlined /> }, { key: 'create_esports_qr', icon: <CreditCardOutlined /> },
{ key: 'query_esports_points', icon: <SearchOutlined /> },
{ key: 'query_gold_balance', icon: <SearchOutlined /> },
{ key: 'refresh_esports_goods', icon: <ReloadOutlined /> },
{ key: 'exchange_esports_goods', icon: <ShoppingOutlined /> },
{ key: 'create_gold_qr', icon: <CreditCardOutlined /> },
{ key: 'donate_esports_chicken_gift', icon: <GiftOutlined /> },
{ key: 'donate_esports_firework_gift', icon: <GiftOutlined /> },
]; ];
const ELITE_BIND_ACTION_KEYS = ['get_bind_qr', 'confirm_bind', 'query_game_name', 'query_change_bind_time']; const ELITE_BIND_ACTION_KEYS = ['get_bind_qr', 'confirm_bind', 'query_game_name', 'query_change_bind_time'];
@@ -70,6 +77,13 @@ const ESPORTS_TASK_TYPES = new Set([
'query_esports_game_name', 'query_esports_game_name',
'confirm_esports_bind', 'confirm_esports_bind',
'create_esports_qr', 'create_esports_qr',
'query_esports_points',
'query_gold_balance',
'refresh_esports_goods',
'exchange_esports_goods',
'create_gold_qr',
'donate_esports_chicken_gift',
'donate_esports_firework_gift',
]); ]);
const DOUYU_GOLD_AMOUNT_STORAGE_KEY = 'douyu_task_gold_amount'; const DOUYU_GOLD_AMOUNT_STORAGE_KEY = 'douyu_task_gold_amount';
const DOUYU_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count'; const DOUYU_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count';
@@ -97,6 +111,10 @@ const BIND_STATUS_LABELS: Record<string, string> = {
esports_bound: '电竞已绑定', esports_bound: '电竞已绑定',
esports_qr_created: '电竞待开通', esports_qr_created: '电竞待开通',
esports_opened: '电竞已开通', esports_opened: '电竞已开通',
esports_points_queried: '已查电竞积分',
esports_gift_donated: '已赠电竞礼物',
esports_goods_refreshed: '已刷电竞皮肤',
esports_goods_exchanged: '已兑电竞皮肤',
change_time_queried: '已查换绑', change_time_queried: '已查换绑',
}; };
@@ -309,7 +327,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
try { try {
const [accResult, goodsResult, taskResult, cfgResult, typeResult] = await Promise.allSettled([ const [accResult, goodsResult, taskResult, cfgResult, typeResult] = await Promise.allSettled([
douyuApi.listAccounts(), douyuApi.listAccounts(),
douyuApi.listGoods(), isEsportsHandbook ? douyuApi.listEsportsGoods() : douyuApi.listGoods(),
douyuApi.listTasks(), douyuApi.listTasks(),
canConfig ? douyuApi.getConfig() : Promise.resolve(null), canConfig ? douyuApi.getConfig() : Promise.resolve(null),
douyuApi.taskTypes(), douyuApi.taskTypes(),
@@ -475,14 +493,16 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
message.warning('当前已有批次在运行,请先停止或等待结束'); message.warning('当前已有批次在运行,请先停止或等待结束');
return; return;
} }
if (taskType === 'exchange_goods' && !selectedGoodsId) { if (['exchange_goods', 'exchange_esports_goods'].includes(taskType) && !selectedGoodsId) {
message.warning('请先选择兑换商品'); message.warning('请先选择兑换商品');
return; return;
} }
const payload: Record<string, unknown> = { ...extraPayload }; const payload: Record<string, unknown> = { ...extraPayload };
if (taskType === 'exchange_goods') payload.commodity_id = selectedGoodsId; if (['exchange_goods', 'exchange_esports_goods'].includes(taskType)) payload.commodity_id = selectedGoodsId;
if (taskType === 'create_gold_qr') payload.amount = goldAmount; if (taskType === 'create_gold_qr') payload.amount = goldAmount;
if (taskType === 'donate_elite_gift') payload.gift_count = giftCount; if (['donate_elite_gift', 'donate_esports_chicken_gift', 'donate_esports_firework_gift'].includes(taskType)) {
payload.gift_count = giftCount;
}
try { try {
const result = await douyuApi.createTasks({ const result = await douyuApi.createTasks({
account_ids: ids, account_ids: ids,
@@ -798,10 +818,10 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
width: 80, width: 80,
render: (v) => v ?? <Text type="secondary">-</Text>, render: (v) => v ?? <Text type="secondary">-</Text>,
}, },
...(isEsportsHandbook ? [] : [{ {
title: '鱼翅', dataIndex: 'gold_balance', width: 70, title: '鱼翅', dataIndex: 'gold_balance', width: 70,
render: (v: number | null) => v ?? <Text type="secondary">-</Text>, render: (v: number | null) => v ?? <Text type="secondary">-</Text>,
}]), },
{ {
title: '换绑时间', dataIndex: 'change_role_wait_time', width: 130, title: '换绑时间', dataIndex: 'change_role_wait_time', width: 130,
render: (_, record) => { render: (_, record) => {
@@ -1001,7 +1021,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
const actionByKey = useMemo(() => new Map(quickActions.map((item) => [item.key, item])), [quickActions]); const actionByKey = useMemo(() => new Map(quickActions.map((item) => [item.key, item])), [quickActions]);
const actionDisabled = (taskType: string) => { const actionDisabled = (taskType: string) => {
if (batchBusy || selectedIds.length === 0) return true; if (batchBusy || selectedIds.length === 0) return true;
if (taskType === 'exchange_goods') return !selectedGoodsId; if (['exchange_goods', 'exchange_esports_goods'].includes(taskType)) return !selectedGoodsId;
return false; return false;
}; };
const renderActionButton = (taskType: string, type: 'default' | 'primary' = 'default') => { const renderActionButton = (taskType: string, type: 'default' | 'primary' = 'default') => {
@@ -1166,6 +1186,75 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
</div> </div>
{renderActionButton('create_esports_qr', 'primary')} {renderActionButton('create_esports_qr', 'primary')}
</div> </div>
<div style={sectionStyle}>
<div style={sectionTitleStyle}>
<SearchOutlined />
<span></span>
</div>
<div style={actionGridStyle}>
{renderActionButton('query_esports_points')}
{renderActionButton('query_gold_balance')}
</div>
</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: goodsLabel(item) }))}
placeholder="选择电竞皮肤"
showSearch
optionFilterProp="label"
style={{ width: '100%' }}
/>
<div style={actionGridStyle}>
{renderActionButton('refresh_esports_goods')}
{renderActionButton('exchange_esports_goods', 'primary')}
</div>
</Space>
</div>
<div style={sectionStyle}>
<div style={sectionTitleStyle}>
<CreditCardOutlined />
<span></span>
</div>
<Space direction="vertical" style={{ width: '100%' }} size={6}>
<Space.Compact style={{ width: '100%' }}>
<InputNumber
size="small"
min={1}
value={goldAmount}
onChange={(v) => setGoldAmount(v || 1)}
style={{ width: '100%' }}
/>
<Button
size="small"
icon={<CreditCardOutlined />}
onClick={() => startTask('create_gold_qr')}
disabled={actionDisabled('create_gold_qr')}
>
</Button>
</Space.Compact>
<InputNumber
size="small"
min={1}
value={giftCount}
onChange={(v) => setGiftCount(v || 1)}
addonAfter="赠送数量"
style={{ width: '100%' }}
/>
<div style={actionGridStyle}>
{renderActionButton('donate_esports_chicken_gift', 'primary')}
{renderActionButton('donate_esports_firework_gift', 'primary')}
</div>
</Space>
</div>
</> </>
) : ( ) : (
<> <>
@@ -1354,6 +1443,10 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind })
{ label: '电竞手册 manualID', key: 'esports_manual_id' }, { label: '电竞手册 manualID', key: 'esports_manual_id' },
{ label: '电竞手册活动', key: 'esports_act_alias' }, { label: '电竞手册活动', key: 'esports_act_alias' },
{ label: '房间 ID', key: 'room_id' }, { label: '房间 ID', key: 'room_id' },
{ label: '冠军鸡腿礼物 ID', key: 'esports_chicken_gift_id' },
{ label: '冠军鸡腿皮肤 ID', key: 'esports_chicken_skin_id' },
{ label: '冠军烟花礼物 ID', key: 'esports_firework_gift_id' },
{ label: '冠军烟花皮肤 ID', key: 'esports_firework_skin_id' },
] : [ ] : [
{ label: 'manualID', key: 'manual_id' }, { label: 'manualID', key: 'manual_id' },
{ label: 'RID', key: 'rid' }, { label: 'RID', key: 'rid' },