虎牙精英宝典

This commit is contained in:
yml2213
2026-08-31 19:34:59 +08:00
parent d391e2ce3e
commit 955ba45488
16 changed files with 743 additions and 5 deletions
@@ -0,0 +1,64 @@
"""增加虎牙精英宝典工作台范围与账号集合。"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
revision: str = "20260831_0032"
down_revision: str | None = "20260830_0031"
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
def _columns(bind, table: str) -> set[str]:
return {item["name"] for item in sa.inspect(bind).get_columns(table)}
def upgrade() -> None:
bind = op.get_bind()
if "handbook_scope" not in _columns(bind, "huya_tasks"):
op.add_column(
"huya_tasks",
sa.Column("handbook_scope", sa.String(length=16), nullable=False, server_default="legacy"),
)
op.create_index("ix_huya_tasks_handbook_scope", "huya_tasks", ["handbook_scope"])
inspector = sa.inspect(bind)
if not inspector.has_table("huya_workbench_accounts"):
op.create_table(
"huya_workbench_accounts",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("user_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=False),
sa.Column("handbook_scope", sa.String(length=16), nullable=False),
sa.Column("account_id", sa.Integer(), sa.ForeignKey("huya_accounts.id"), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.UniqueConstraint("user_id", "handbook_scope", "account_id", name="uq_huya_workbench_account"),
)
op.create_index("ix_huya_workbench_accounts_user_id", "huya_workbench_accounts", ["user_id"])
op.create_index("ix_huya_workbench_accounts_handbook_scope", "huya_workbench_accounts", ["handbook_scope"])
op.create_index("ix_huya_workbench_accounts_account_id", "huya_workbench_accounts", ["account_id"])
if not inspector.has_table("huya_workbenches"):
op.create_table(
"huya_workbenches",
sa.Column("id", sa.Integer(), primary_key=True, autoincrement=True),
sa.Column("user_id", sa.Integer(), sa.ForeignKey("users.id"), nullable=False),
sa.Column("handbook_scope", sa.String(length=16), nullable=False),
sa.Column("updated_at", sa.DateTime(), nullable=True),
sa.UniqueConstraint("user_id", "handbook_scope", name="uq_huya_workbench"),
)
op.create_index("ix_huya_workbenches_user_id", "huya_workbenches", ["user_id"])
op.create_index("ix_huya_workbenches_handbook_scope", "huya_workbenches", ["handbook_scope"])
def downgrade() -> None:
bind = op.get_bind()
inspector = sa.inspect(bind)
if inspector.has_table("huya_workbenches"):
op.drop_table("huya_workbenches")
if inspector.has_table("huya_workbench_accounts"):
op.drop_table("huya_workbench_accounts")
if "handbook_scope" in _columns(bind, "huya_tasks"):
op.drop_index("ix_huya_tasks_handbook_scope", table_name="huya_tasks")
op.drop_column("huya_tasks", "handbook_scope")
+40
View File
@@ -498,6 +498,10 @@ class HuyaTask(Base):
ForeignKey("huya_accounts.id"), nullable=False
)
task_type: Mapped[str] = mapped_column(String(64), nullable=False, index=True)
# 任务归属工作台;legacy 保留旧虎牙任务,elite 隔离精英宝典任务。
handbook_scope: Mapped[str] = mapped_column(
String(16), nullable=False, default="legacy", index=True
)
status: Mapped[str] = mapped_column(String(32), default="pending")
message: Mapped[str] = mapped_column(String(512), default="")
result: Mapped[dict | None] = mapped_column(JSON, nullable=True)
@@ -508,6 +512,42 @@ class HuyaTask(Base):
account: Mapped[HuyaAccount] = relationship("HuyaAccount", back_populates="tasks")
class HuyaWorkbenchAccount(Base):
"""用户在虎牙指定工作台中启用的账号。"""
__tablename__ = "huya_workbench_accounts"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False, index=True)
handbook_scope: Mapped[str] = mapped_column(String(16), nullable=False, index=True)
account_id: Mapped[int] = mapped_column(ForeignKey("huya_accounts.id"), nullable=False, index=True)
created_at: Mapped[datetime] = mapped_column(DateTime, default=_utcnow)
__table_args__ = (
UniqueConstraint(
"user_id", "handbook_scope", "account_id",
name="uq_huya_workbench_account",
),
)
class HuyaWorkbench(Base):
"""工作台配置哨兵,令空账号集合也能跨浏览器同步。"""
__tablename__ = "huya_workbenches"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False, index=True)
handbook_scope: Mapped[str] = mapped_column(String(16), nullable=False, index=True)
updated_at: Mapped[datetime] = mapped_column(
DateTime, default=_utcnow, onupdate=_utcnow
)
__table_args__ = (
UniqueConstraint("user_id", "handbook_scope", name="uq_huya_workbench"),
)
class HuyaConfig(Base):
"""虎牙业务配置"""
+78
View File
@@ -50,6 +50,8 @@ from ..models import (
HuyaRegisterItem,
HuyaRegisterSuccessLog,
HuyaTask,
HuyaWorkbench,
HuyaWorkbenchAccount,
ProxyConfig,
User,
)
@@ -79,6 +81,7 @@ from ..schemas import (
HuyaSmsLoginRequest,
HuyaTaskBatchRequest,
HuyaTaskOut,
HuyaWorkbenchAccountsUpdate,
)
from ..services.audit_service import record_audit
from ..services.huya_register_runner import (
@@ -281,6 +284,18 @@ def _require_huya_task_account_access(
raise HTTPException(status_code=403, detail="包含无权操作的虎牙账号")
def _elite_workbench_account_ids(db: Session, current: User) -> list[int]:
query = db.query(HuyaWorkbenchAccount.account_id).filter(
HuyaWorkbenchAccount.user_id == current.id,
HuyaWorkbenchAccount.handbook_scope == "elite",
)
if not _can_view_huya_all(current):
query = query.join(HuyaAccount, HuyaAccount.id == HuyaWorkbenchAccount.account_id).filter(
HuyaAccount.assigned_to == current.id
)
return [account_id for (account_id,) in query.order_by(HuyaWorkbenchAccount.id.asc()).all()]
def _require_huya_batch_owner(db: Session, current: User, batch_id: str) -> None:
"""客服只能停止或订阅自己创建的任务批次。"""
if _can_view_huya_all(current):
@@ -385,6 +400,7 @@ def _task_out(task: HuyaTask, *, include_images: bool = False) -> HuyaTaskOut:
account_uid=account.uid if account else "",
account_nickname=account.nickname if account else "",
task_type=task.task_type,
handbook_scope=getattr(task, "handbook_scope", "legacy") or "legacy",
status=task.status or "",
message=task.message or "",
result=_sanitize_task_result(
@@ -1282,6 +1298,9 @@ def delete_accounts_batch(
db.query(HuyaTask).filter(HuyaTask.account_id.in_(ids)).delete(
synchronize_session=False
)
db.query(HuyaWorkbenchAccount).filter(
HuyaWorkbenchAccount.account_id.in_(ids)
).delete(synchronize_session=False)
_clear_huya_account_references(db, ids)
deleted = (
db.query(HuyaAccount)
@@ -1310,6 +1329,9 @@ def delete_accounts_batch_selection(
db.query(HuyaTask).filter(HuyaTask.account_id.in_(ids)).delete(
synchronize_session=False
)
db.query(HuyaWorkbenchAccount).filter(
HuyaWorkbenchAccount.account_id.in_(ids)
).delete(synchronize_session=False)
_clear_huya_account_references(db, ids)
deleted = (
db.query(HuyaAccount)
@@ -1339,6 +1361,9 @@ def delete_account(
db.query(HuyaTask).filter(HuyaTask.account_id == account_id).delete(
synchronize_session=False
)
db.query(HuyaWorkbenchAccount).filter(
HuyaWorkbenchAccount.account_id == account_id
).delete(synchronize_session=False)
_clear_huya_account_references(db, [account_id])
db.delete(account)
db.commit()
@@ -1743,6 +1768,55 @@ def update_config(
return _config_out(config)
@router.get("/workbench-accounts")
def list_huya_workbench_accounts(
handbook_scope: str = Query(..., pattern="^elite$"),
db: Session = Depends(get_db),
current: User = Depends(require_permission("huya:task")),
):
"""读取当前用户的虎牙精英宝典账号集合。"""
ids = _elite_workbench_account_ids(db, current)
configured = db.query(HuyaWorkbench.id).filter(
HuyaWorkbench.user_id == current.id,
HuyaWorkbench.handbook_scope == handbook_scope,
).first() is not None
return {"account_ids": ids, "configured": configured}
@router.put("/workbench-accounts")
def update_huya_workbench_accounts(
req: HuyaWorkbenchAccountsUpdate,
db: Session = Depends(get_db),
current: User = Depends(require_permission("huya:task")),
):
"""覆盖虎牙精英宝典账号集合,支持跨浏览器同步。"""
ids = sorted(set(req.account_ids))
if any(account_id < 1 for account_id in ids):
raise HTTPException(status_code=400, detail="无效的账号 ID")
if ids:
_require_huya_task_account_access(db, current, ids)
workbench = db.query(HuyaWorkbench).filter(
HuyaWorkbench.user_id == current.id,
HuyaWorkbench.handbook_scope == req.handbook_scope,
).first()
if workbench is None:
db.add(HuyaWorkbench(user_id=current.id, handbook_scope=req.handbook_scope))
else:
workbench.updated_at = datetime.now(UTC)
db.query(HuyaWorkbenchAccount).filter(
HuyaWorkbenchAccount.user_id == current.id,
HuyaWorkbenchAccount.handbook_scope == req.handbook_scope,
).delete(synchronize_session=False)
db.add_all([
HuyaWorkbenchAccount(
user_id=current.id, handbook_scope=req.handbook_scope, account_id=account_id
)
for account_id in ids
])
db.commit()
return {"account_ids": ids, "success": True}
@router.get("/goods", response_model=list[HuyaGoodsOut])
def list_goods(
db: Session = Depends(get_db),
@@ -1794,6 +1868,7 @@ async def create_task_batch(
req.task_type,
current.id,
req.payload,
req.handbook_scope,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@@ -1838,6 +1913,7 @@ async def create_task_batch(
@router.get("/tasks", response_model=list[HuyaTaskOut])
def list_tasks(
batch_id: str | None = None,
handbook_scope: str | None = Query(None, pattern="^(legacy|elite)$"),
include_images: bool = Query(
False, description="是否返回 base64 小程序码(默认否,轮询请保持 false)"
),
@@ -1848,6 +1924,8 @@ def list_tasks(
query = _visible_huya_tasks_query(db, current)
if batch_id:
query = query.filter(HuyaTask.batch_id == batch_id)
if handbook_scope:
query = query.filter(HuyaTask.handbook_scope == handbook_scope)
tasks = query.order_by(HuyaTask.id.desc()).limit(300).all()
return [_task_out(task, include_images=include_images) for task in tasks]
+8
View File
@@ -503,10 +503,16 @@ class HuyaConfigUpdate(BaseModel):
class HuyaTaskBatchRequest(BaseModel):
account_ids: list[int]
task_type: str
handbook_scope: str = Field("legacy", pattern="^(legacy|elite)$")
concurrency: int = 3
payload: dict[str, Any] = Field(default_factory=dict)
class HuyaWorkbenchAccountsUpdate(BaseModel):
handbook_scope: str = Field(..., pattern="^elite$")
account_ids: list[int] = Field(default_factory=list)
class HuyaTaskOut(BaseModel):
id: int
batch_id: str
@@ -514,6 +520,7 @@ class HuyaTaskOut(BaseModel):
account_uid: str = ""
account_nickname: str = ""
task_type: str
handbook_scope: str = "legacy"
status: str
message: str = ""
result: dict[str, Any] | None = None
@@ -532,6 +539,7 @@ class HuyaTaskOut(BaseModel):
"account_uid": self.account_uid,
"account_nickname": self.account_nickname,
"task_type": self.task_type,
"handbook_scope": self.handbook_scope,
"status": self.status,
"message": self.message,
"result": self.result,
+6 -1
View File
@@ -60,6 +60,7 @@ class HuyaBatchRunner(
self._push_log("info", f"[{current}/{total}] 开始虎牙任务: {name}")
if self.task_type not in {
"query_act_tasks",
"query_points",
"get_bind_qr",
"confirm_bind",
@@ -77,7 +78,11 @@ class HuyaBatchRunner(
return
try:
if self.task_type == "query_points":
if self.task_type == "query_act_tasks":
self._execute_query_act_tasks(
worker_db, task, account, account_info, config_info
)
elif self.task_type == "query_points":
self._execute_query_points(
worker_db, task, account, account_info, config_info
)
+46
View File
@@ -31,6 +31,52 @@ class GoodsMixin:
def _wait_until(self, when: datetime, uid: int) -> bool: ...
def _parse_scheduled_time(self, value: Any) -> datetime | None: ...
def _execute_query_act_tasks(
self,
worker_db: Session,
task: HuyaTask,
account: HuyaAccount,
account_info: dict,
config_info: dict,
):
"""读取精英宝典任务详情,供专用工作台展示购买/观看任务。"""
uid = self._resolve_uid(account_info)
cookie = account_info.get("cookie") or ""
if not uid or not cookie:
self._mark_task(worker_db, task, "failed", "账号 UID 或 Cookie 为空")
return
act_id = self._to_int(self.payload.get("act_id") or 25135)
if not act_id:
self._mark_task(worker_db, task, "failed", "精英宝典活动 ID 无效")
return
client: Any = HuyaHttpClient(
logger=lambda msg: self._push_log("info", f"[{uid}] {msg}")
)
response = client.get_act_task_detail(uid=uid, cookie=cookie, act_id=act_id)
if response is None:
self._mark_task(worker_db, task, "error", "虎牙活动任务接口无响应")
return
result = response.to_dict()
result["act_id"] = act_id
if response.status != 200:
self._mark_task(
worker_db,
task,
"failed",
response.msg or f"虎牙活动任务查询失败: {response.status}",
result,
)
return
account.status = "tasks_queried"
account.updated_at = datetime.now(UTC)
self._mark_task(
worker_db,
task,
"success",
f"已读取 {len(result.get('tasks', []))} 项精英宝典任务",
result,
)
def _execute_query_points(
self,
worker_db: Session,
+20
View File
@@ -14,6 +14,7 @@ from ..huya_defaults import HUYA_CONFIG_DEFAULTS, HUYA_CONFIG_FIELDS
from ..models import HuyaAccount, HuyaConfig, HuyaTask
SUPPORTED_TASK_TYPES = {
"query_act_tasks": "查询精英宝典任务",
"get_bind_qr": "获取绑定二维码",
"query_points": "一键查询积分",
"query_game_name": "一键查询游戏名",
@@ -25,6 +26,19 @@ SUPPORTED_TASK_TYPES = {
"create_recharge_order": "生成支付二维码",
}
HUYA_HANDBOOK_TASK_TYPES = {
"query_act_tasks",
"get_bind_qr",
"confirm_bind",
"query_points",
"query_game_name",
"refresh_goods",
"exchange_goods",
"query_exchange_records",
"refresh_recharge_goods",
"create_recharge_order",
}
def huya_config_value(field: str, value: str | None) -> str:
"""读取配置值;空值自动回退到当前活动默认配置。"""
@@ -371,10 +385,15 @@ def create_planned_tasks(
task_type: str,
created_by: int,
payload: dict | None = None,
handbook_scope: str = "legacy",
) -> tuple[str, int]:
"""创建虎牙任务记录,等待后台执行器消费。"""
if task_type not in SUPPORTED_TASK_TYPES:
raise ValueError("不支持的任务类型")
if handbook_scope not in {"legacy", "elite"}:
raise ValueError("无效的工作台")
if handbook_scope == "elite" and task_type not in HUYA_HANDBOOK_TASK_TYPES:
raise ValueError("该任务不属于精英宝典工作台")
batch_id = uuid.uuid4().hex[:12]
payload = payload or {}
@@ -392,6 +411,7 @@ def create_planned_tasks(
batch_id=batch_id,
account_id=account.id,
task_type=task_type,
handbook_scope=handbook_scope,
status="planned",
message="任务已创建,等待执行",
result={"payload": payload} if payload else None,