增加兑换

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
@@ -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_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"
+15 -1
View File
@@ -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,
+12
View File
@@ -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
+259 -1
View File
@@ -15,7 +15,7 @@ from sqlalchemy.orm import Session, joinedload
from core.douyu import DouyuActivityClient, DouyuActivityError
from ..database import SessionLocal
from ..models import Account, 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,
+10 -1
View File
@@ -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]