diff --git a/core/douyu/activity_client.py b/core/douyu/activity_client.py index 4d0ec59..1eccc7a 100644 --- a/core/douyu/activity_client.py +++ b/core/douyu/activity_client.py @@ -50,6 +50,8 @@ class DouyuActivityClient: 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_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_ACCOUNT_API = "https://cz.douyu.com/item/gold/account" EXCHANGE_BALANCE_API = "https://www.douyu.com/wjapi/nc/exchange/fim" @@ -507,6 +509,71 @@ class DouyuActivityClient: "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]: """生成鱼翅充值支付二维码。""" payload = self._request_json( @@ -533,19 +600,20 @@ class DouyuActivityClient: raise DouyuActivityError(payload.get("msg") or "生成鱼翅充值码失败") return {"pay_url": pay_url, "token": token, "raw": payload} - def donate_elite_gift( + def _donate_gift( self, *, gift_count: int, room_id: str, gift_id: str, skin_id: str, + source: str, ) -> dict[str, Any]: - """赠送精英令礼物。""" + """赠送直播间礼物。""" payload = self._request_json( "post", self.DONATE_API, - "赠送精英令", + source, data={ "giftId": gift_id, "giftCount": str(gift_count), @@ -559,9 +627,44 @@ class DouyuActivityClient: }, ) 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} + 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]: """查询手册积分余额。""" payload = self._request_json( diff --git a/web/backend/migrations/versions/20260728_0013_douyu_esports_gift_config.py b/web/backend/migrations/versions/20260728_0013_douyu_esports_gift_config.py new file mode 100644 index 0000000..6ae7224 --- /dev/null +++ b/web/backend/migrations/versions/20260728_0013_douyu_esports_gift_config.py @@ -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) diff --git a/web/backend/migrations/versions/20260728_0014_douyu_esports_goods_snapshot.py b/web/backend/migrations/versions/20260728_0014_douyu_esports_goods_snapshot.py new file mode 100644 index 0000000..ed67501 --- /dev/null +++ b/web/backend/migrations/versions/20260728_0014_douyu_esports_goods_snapshot.py @@ -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") diff --git a/web/backend/models.py b/web/backend/models.py index d77d9fd..9fa0fab 100644 --- a/web/backend/models.py +++ b/web/backend/models.py @@ -128,6 +128,10 @@ class DouyuConfig(Base): esports_manual_id = Column(String(64), default="1W0yGkOdDbAXb") esports_act_alias = Column(String(64), default="20260226ZMGDG") 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) gift_id = Column(String(64), default="23643") skin_id = Column(String(64), default="2942") @@ -147,6 +151,19 @@ class DouyuGoodsSnapshot(Base): 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): """虎牙账号""" __tablename__ = "huya_accounts" diff --git a/web/backend/routers/douyu.py b/web/backend/routers/douyu.py index ab9f90a..2e54504 100644 --- a/web/backend/routers/douyu.py +++ b/web/backend/routers/douyu.py @@ -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, DouyuGoodsSnapshot, DouyuTask, User +from ..models import Account, DouyuConfig, DouyuEsportsGoodsSnapshot, DouyuGoodsSnapshot, DouyuTask, User from ..permissions import user_has_permission from ..schemas import ( 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_act_alias=douyu_config_value("esports_act_alias", config.esports_act_alias), 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), gift_id=douyu_config_value("gift_id", config.gift_id), skin_id=douyu_config_value("skin_id", config.skin_id), @@ -224,6 +228,16 @@ def list_goods( 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") async def create_task_batch( req: DouyuTaskBatchRequest, diff --git a/web/backend/schemas.py b/web/backend/schemas.py index 3537746..0dd6710 100644 --- a/web/backend/schemas.py +++ b/web/backend/schemas.py @@ -545,6 +545,10 @@ class DouyuConfigOut(BaseModel): esports_manual_id: str = "1W0yGkOdDbAXb" esports_act_alias: str = "20260226ZMGDG" 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 gift_id: str = "23643" skin_id: str = "2942" @@ -563,6 +567,10 @@ class DouyuConfigOut(BaseModel): "esports_manual_id": self.esports_manual_id, "esports_act_alias": self.esports_act_alias, "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, "gift_id": self.gift_id, "skin_id": self.skin_id, @@ -581,6 +589,10 @@ class DouyuConfigUpdate(BaseModel): esports_manual_id: Optional[str] = None esports_act_alias: Optional[str] = None 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) gift_id: Optional[str] = None skin_id: Optional[str] = None diff --git a/web/backend/services/douyu_runner.py b/web/backend/services/douyu_runner.py index 070b297..b63d8fa 100644 --- a/web/backend/services/douyu_runner.py +++ b/web/backend/services/douyu_runner.py @@ -15,7 +15,7 @@ from sqlalchemy.orm import Session, joinedload from core.douyu import DouyuActivityClient, DouyuActivityError from ..database import SessionLocal -from ..models import Account, DouyuGoodsSnapshot, DouyuTask +from ..models import Account, DouyuEsportsGoodsSnapshot, DouyuGoodsSnapshot, DouyuTask from .douyu_service import ( DOUYU_CONFIG_FIELDS, account_uid, @@ -382,6 +382,27 @@ class DouyuBatchRunner: row.updated_at = now 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: config = ensure_douyu_config(db) 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) 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): client = self._client(cookie) qr_act_alias = self._bind_qr_act_alias(config) @@ -1492,6 +1539,147 @@ class DouyuBatchRunner: 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): payload = self._task_payload(task) 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}, ) + 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): client = self._client(cookie) query_aliases = self._query_bind_act_aliases(config) @@ -1870,6 +2123,7 @@ class DouyuBatchRunner: handler = { "refresh_goods": self._execute_refresh_goods, + "refresh_esports_goods": self._execute_refresh_esports_goods, "get_bind_qr": self._execute_get_bind_qr, "confirm_bind": self._execute_confirm_bind, "create_elite_qr": self._execute_create_elite_qr, @@ -1878,10 +2132,14 @@ class DouyuBatchRunner: "query_esports_game_name": self._execute_query_esports_game_name, "confirm_esports_bind": self._execute_confirm_esports_bind, "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, "donate_elite_gift": self._execute_donate_elite_gift, "query_points": self._execute_query_points, "exchange_goods": self._execute_exchange_goods, + "exchange_esports_goods": self._execute_exchange_esports_goods, "query_game_name": self._execute_query_game_name, "query_change_bind_time": self._execute_query_change_bind_time, "query_limited_goods": self._execute_query_limited_goods, diff --git a/web/backend/services/douyu_service.py b/web/backend/services/douyu_service.py index 6971894..372c278 100644 --- a/web/backend/services/douyu_service.py +++ b/web/backend/services/douyu_service.py @@ -22,6 +22,11 @@ SUPPORTED_DOUYU_TASK_TYPES = { "query_esports_game_name": "查询最新游戏角色", "confirm_esports_bind": "确认电竞手册绑定", "create_esports_qr": "开通电竞手册30", + "query_esports_points": "查询电竞积分", + "donate_esports_chicken_gift": "赠送冠军鸡腿", + "donate_esports_firework_gift": "赠送冠军烟花", + "refresh_esports_goods": "刷新电竞皮肤商城", + "exchange_esports_goods": "兑换电竞皮肤", "create_gold_qr": "充值鱼翅", "donate_elite_gift": "赠送精英令", "query_points": "一键查询积分", @@ -47,6 +52,10 @@ DOUYU_CONFIG_DEFAULTS = { "esports_manual_id": "1W0yGkOdDbAXb", "esports_act_alias": "20260226ZMGDG", "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, "gift_id": "23643", "skin_id": "2942", @@ -160,7 +169,7 @@ def create_douyu_planned_tasks( raise ValueError("不支持的任务类型") 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 时前端无法选账号创建任务。 accounts = accounts[:1] diff --git a/web/frontend/src/api/douyu.ts b/web/frontend/src/api/douyu.ts index 52653a0..c075b53 100644 --- a/web/frontend/src/api/douyu.ts +++ b/web/frontend/src/api/douyu.ts @@ -16,6 +16,7 @@ export const douyuApi = { getConfig: () => api.get('/douyu/config'), updateConfig: (data: Partial) => api.put('/douyu/config', data), listGoods: () => api.get('/douyu/goods'), + listEsportsGoods: () => api.get('/douyu/esports-goods'), createTasks: (data: DouyuTaskBatchRequest) => api.post('/douyu/tasks/batch', data), listTasks: (batchId?: string) => diff --git a/web/frontend/src/api/types.ts b/web/frontend/src/api/types.ts index 7aaea24..2dd060d 100644 --- a/web/frontend/src/api/types.ts +++ b/web/frontend/src/api/types.ts @@ -217,6 +217,10 @@ export interface DouyuConfig { esports_manual_id: string; esports_act_alias: string; 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; gift_id: string; skin_id: string; diff --git a/web/frontend/src/pages/DouyuTasksPage.tsx b/web/frontend/src/pages/DouyuTasksPage.tsx index 6f505ca..8d5981e 100644 --- a/web/frontend/src/pages/DouyuTasksPage.tsx +++ b/web/frontend/src/pages/DouyuTasksPage.tsx @@ -44,6 +44,13 @@ const ESPORTS_QUICK_ACTIONS = [ { key: 'prepare_esports_bind', icon: }, { key: 'query_esports_game_name', icon: }, { key: 'create_esports_qr', icon: }, + { key: 'query_esports_points', icon: }, + { key: 'query_gold_balance', icon: }, + { key: 'refresh_esports_goods', icon: }, + { key: 'exchange_esports_goods', icon: }, + { key: 'create_gold_qr', icon: }, + { key: 'donate_esports_chicken_gift', icon: }, + { key: 'donate_esports_firework_gift', icon: }, ]; 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', 'confirm_esports_bind', '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_GIFT_COUNT_STORAGE_KEY = 'douyu_task_gift_count'; @@ -97,6 +111,10 @@ const BIND_STATUS_LABELS: Record = { esports_bound: '电竞已绑定', esports_qr_created: '电竞待开通', esports_opened: '电竞已开通', + esports_points_queried: '已查电竞积分', + esports_gift_donated: '已赠电竞礼物', + esports_goods_refreshed: '已刷电竞皮肤', + esports_goods_exchanged: '已兑电竞皮肤', change_time_queried: '已查换绑', }; @@ -309,7 +327,7 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) try { const [accResult, goodsResult, taskResult, cfgResult, typeResult] = await Promise.allSettled([ douyuApi.listAccounts(), - douyuApi.listGoods(), + isEsportsHandbook ? douyuApi.listEsportsGoods() : douyuApi.listGoods(), douyuApi.listTasks(), canConfig ? douyuApi.getConfig() : Promise.resolve(null), douyuApi.taskTypes(), @@ -475,14 +493,16 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) message.warning('当前已有批次在运行,请先停止或等待结束'); return; } - if (taskType === 'exchange_goods' && !selectedGoodsId) { + if (['exchange_goods', 'exchange_esports_goods'].includes(taskType) && !selectedGoodsId) { message.warning('请先选择兑换商品'); return; } const payload: Record = { ...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 === '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 { const result = await douyuApi.createTasks({ account_ids: ids, @@ -798,10 +818,10 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) width: 80, render: (v) => v ?? -, }, - ...(isEsportsHandbook ? [] : [{ + { title: '鱼翅', dataIndex: 'gold_balance', width: 70, render: (v: number | null) => v ?? -, - }]), + }, { title: '换绑时间', dataIndex: 'change_role_wait_time', width: 130, 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 actionDisabled = (taskType: string) => { 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; }; const renderActionButton = (taskType: string, type: 'default' | 'primary' = 'default') => { @@ -1166,6 +1186,75 @@ export default function DouyuTasksPage({ handbook }: { handbook: HandbookKind }) {renderActionButton('create_esports_qr', 'primary')} +
+
+ + 查询 +
+
+ {renderActionButton('query_esports_points')} + {renderActionButton('query_gold_balance')} +
+
+
+
+ + 皮肤兑换 +
+ +