虎牙精英宝典
This commit is contained in:
@@ -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")
|
||||
@@ -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):
|
||||
"""虎牙业务配置"""
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user