新增虎牙账号和任务基础功能

This commit is contained in:
yml2213
2026-07-04 16:31:12 +08:00
parent 3df247e4e5
commit e1d47a85be
26 changed files with 4513 additions and 3 deletions
+2 -1
View File
@@ -11,7 +11,7 @@ from fastapi.responses import FileResponse
from starlette.middleware.base import BaseHTTPMiddleware
from .database import init_db
from .routers import auth, users, accounts, login, proxy, cookies
from .routers import auth, users, accounts, login, proxy, cookies, huya
from .schemas import AppInfo
from .version import get_app_version
from utils import setup_logger
@@ -69,6 +69,7 @@ app.include_router(accounts.router)
app.include_router(login.router)
app.include_router(proxy.router)
app.include_router(cookies.router)
app.include_router(huya.router)
@app.get("/api/health")
@@ -0,0 +1,127 @@
"""新增虎牙基础数据表
Revision ID: 20260704_0004
Revises: 20260624_0003
Create Date: 2026-07-04
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "20260704_0004"
down_revision: Union[str, None] = "20260624_0003"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def _has_table(bind, table_name: str) -> bool:
return sa.inspect(bind).has_table(table_name)
def _indexes(bind, table_name: str) -> set[str]:
if not _has_table(bind, table_name):
return set()
return {index["name"] for index in sa.inspect(bind).get_indexes(table_name)}
def _create_index_if_missing(bind, name: str, table_name: str, columns: list[str], unique: bool = False) -> None:
if name not in _indexes(bind, table_name):
op.create_index(name, table_name, columns, unique=unique)
def upgrade() -> None:
bind = op.get_bind()
if not _has_table(bind, "huya_accounts"):
op.create_table(
"huya_accounts",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("uid", sa.String(length=32), nullable=True),
sa.Column("yyuid", sa.String(length=32), nullable=True),
sa.Column("username", sa.String(length=128), nullable=True),
sa.Column("nickname", sa.String(length=128), nullable=True),
sa.Column("cookie", sa.Text(), nullable=False),
sa.Column("tag", sa.String(length=64), nullable=True),
sa.Column("remark", sa.String(length=256), nullable=True),
sa.Column("status", sa.String(length=32), nullable=True),
sa.Column("points", sa.Integer(), nullable=True),
sa.Column("game_name", sa.String(length=128), nullable=True),
sa.Column("game_channel", sa.String(length=64), nullable=True),
sa.Column("game_phone", sa.String(length=64), nullable=True),
sa.Column("assigned_to", sa.Integer(), nullable=True),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("updated_at", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(["assigned_to"], ["users.id"]),
sa.PrimaryKeyConstraint("id"),
)
_create_index_if_missing(bind, "ix_huya_accounts_uid", "huya_accounts", ["uid"])
_create_index_if_missing(bind, "ix_huya_accounts_yyuid", "huya_accounts", ["yyuid"])
_create_index_if_missing(bind, "ix_huya_accounts_assigned_to", "huya_accounts", ["assigned_to"])
if not _has_table(bind, "huya_tasks"):
op.create_table(
"huya_tasks",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("batch_id", sa.String(length=64), nullable=False),
sa.Column("account_id", sa.Integer(), nullable=False),
sa.Column("task_type", sa.String(length=64), nullable=False),
sa.Column("status", sa.String(length=32), nullable=True),
sa.Column("message", sa.String(length=512), nullable=True),
sa.Column("result", sa.JSON(), nullable=True),
sa.Column("created_by", sa.Integer(), nullable=False),
sa.Column("created_at", sa.DateTime(), nullable=True),
sa.Column("finished_at", sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(["account_id"], ["huya_accounts.id"]),
sa.ForeignKeyConstraint(["created_by"], ["users.id"]),
sa.PrimaryKeyConstraint("id"),
)
_create_index_if_missing(bind, "ix_huya_tasks_batch_id", "huya_tasks", ["batch_id"])
_create_index_if_missing(bind, "ix_huya_tasks_task_type", "huya_tasks", ["task_type"])
if not _has_table(bind, "huya_config"):
op.create_table(
"huya_config",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("room_pid", sa.String(length=64), nullable=True),
sa.Column("sid", sa.String(length=32), nullable=True),
sa.Column("outer_act_id", sa.String(length=32), nullable=True),
sa.Column("bind_act_id", sa.String(length=32), nullable=True),
sa.Column("pay_channel", sa.String(length=16), nullable=True),
sa.Column("updated_at", sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
if not _has_table(bind, "huya_goods_snapshot"):
op.create_table(
"huya_goods_snapshot",
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
sa.Column("product_id", sa.String(length=64), nullable=False),
sa.Column("name", sa.String(length=256), nullable=True),
sa.Column("price", sa.Integer(), nullable=True),
sa.Column("remain_text", sa.String(length=64), nullable=True),
sa.Column("raw", sa.JSON(), nullable=True),
sa.Column("updated_at", sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint("id"),
)
_create_index_if_missing(bind, "ix_huya_goods_snapshot_product_id", "huya_goods_snapshot", ["product_id"])
def downgrade() -> None:
bind = op.get_bind()
if _has_table(bind, "huya_goods_snapshot"):
op.drop_index("ix_huya_goods_snapshot_product_id", table_name="huya_goods_snapshot")
op.drop_table("huya_goods_snapshot")
if _has_table(bind, "huya_config"):
op.drop_table("huya_config")
if _has_table(bind, "huya_tasks"):
op.drop_index("ix_huya_tasks_task_type", table_name="huya_tasks")
op.drop_index("ix_huya_tasks_batch_id", table_name="huya_tasks")
op.drop_table("huya_tasks")
if _has_table(bind, "huya_accounts"):
op.drop_index("ix_huya_accounts_assigned_to", table_name="huya_accounts")
op.drop_index("ix_huya_accounts_yyuid", table_name="huya_accounts")
op.drop_index("ix_huya_accounts_uid", table_name="huya_accounts")
op.drop_table("huya_accounts")
+70
View File
@@ -30,6 +30,7 @@ class User(Base):
# 客服被分配的账号
assigned_accounts = relationship("Account", back_populates="assigned_user", foreign_keys="Account.assigned_to")
huya_accounts = relationship("HuyaAccount", back_populates="assigned_user", foreign_keys="HuyaAccount.assigned_to")
class Account(Base):
@@ -70,6 +71,75 @@ class LoginTask(Base):
account = relationship("Account", back_populates="login_tasks")
class HuyaAccount(Base):
"""虎牙 Cookie 账号"""
__tablename__ = "huya_accounts"
id = Column(Integer, primary_key=True, autoincrement=True)
uid = Column(String(32), default="", index=True)
yyuid = Column(String(32), default="", index=True)
username = Column(String(128), default="")
nickname = Column(String(128), default="")
cookie = Column(EncryptedText(), nullable=False)
tag = Column(String(64), default="")
remark = Column(String(256), default="")
status = Column(String(32), default="imported")
points = Column(Integer, nullable=True)
game_name = Column(String(128), default="")
game_channel = Column(String(64), default="")
game_phone = Column(String(64), default="")
assigned_to = Column(Integer, ForeignKey("users.id"), nullable=True, index=True)
created_at = Column(DateTime, default=_utcnow)
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
assigned_user = relationship("User", back_populates="huya_accounts", foreign_keys=[assigned_to])
tasks = relationship("HuyaTask", back_populates="account")
class HuyaTask(Base):
"""虎牙业务任务"""
__tablename__ = "huya_tasks"
id = Column(Integer, primary_key=True, autoincrement=True)
batch_id = Column(String(64), nullable=False, index=True)
account_id = Column(Integer, ForeignKey("huya_accounts.id"), nullable=False)
task_type = Column(String(64), nullable=False, index=True)
status = Column(String(32), default="pending")
message = Column(String(512), default="")
result = Column(JSON, nullable=True)
created_by = Column(Integer, ForeignKey("users.id"), nullable=False)
created_at = Column(DateTime, default=_utcnow)
finished_at = Column(DateTime, nullable=True)
account = relationship("HuyaAccount", back_populates="tasks")
class HuyaConfig(Base):
"""虎牙业务配置"""
__tablename__ = "huya_config"
id = Column(Integer, primary_key=True, autoincrement=True)
room_pid = Column(String(64), default="")
sid = Column(String(32), default="")
outer_act_id = Column(String(32), default="9504")
bind_act_id = Column(String(32), default="17096")
pay_channel = Column(String(16), default="Zfb")
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
class HuyaGoodsSnapshot(Base):
"""虎牙兑换商品快照"""
__tablename__ = "huya_goods_snapshot"
id = Column(Integer, primary_key=True, autoincrement=True)
product_id = Column(String(64), nullable=False, index=True)
name = Column(String(256), default="")
price = Column(Integer, nullable=True)
remain_text = Column(String(64), default="")
raw = Column(JSON, nullable=True)
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
class ProxyConfig(Base):
"""代理配置(全局单条记录)"""
__tablename__ = "proxy_config"
+11
View File
@@ -22,6 +22,12 @@ PERMISSIONS = {
# Cookie
"cookie:view": "查看 Cookie",
"cookie:export": "导出 Cookie",
# 虎牙
"huya:account": "虎牙 CK 管理",
"huya:task": "虎牙任务管理",
"huya:bind": "虎牙绑定操作",
"huya:recharge": "虎牙充值操作",
"huya:config": "虎牙配置管理",
# 代理 & 白名单
"proxy:manage": "代理配置管理",
"whitelist:manage": "白名单配置管理",
@@ -42,6 +48,11 @@ ROLE_PERMISSIONS = {
"login:view_all",
"cookie:view",
"cookie:export",
"huya:account",
"huya:task",
"huya:bind",
"huya:recharge",
"huya:config",
"proxy:manage",
"whitelist:manage",
"whitelist:test",
+250
View File
@@ -0,0 +1,250 @@
"""虎牙基础管理路由"""
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket
from sqlalchemy.orm import Session, joinedload
from ..database import get_db
from ..deps import authenticate_websocket, require_permission
from ..models import HuyaAccount, HuyaConfig, HuyaGoodsSnapshot, HuyaTask, User
from ..schemas import (
HuyaAccountOut,
HuyaConfigOut,
HuyaConfigUpdate,
HuyaCookieImport,
HuyaGoodsOut,
HuyaTaskBatchRequest,
HuyaTaskOut,
)
from ..services.huya_service import (
SUPPORTED_TASK_TYPES,
create_planned_tasks,
ensure_huya_config,
import_huya_cookies,
)
router = APIRouter(prefix="/api/huya", tags=["虎牙"])
def _fmt_cookie_preview(cookie: str) -> str:
if not cookie:
return ""
return cookie[:50] + "..." if len(cookie) > 50 else cookie
def _account_out(account: HuyaAccount) -> HuyaAccountOut:
cookie = account.cookie or ""
return HuyaAccountOut(
id=account.id,
uid=account.uid or "",
yyuid=account.yyuid or "",
username=account.username or "",
nickname=account.nickname or "",
cookie=cookie,
cookie_preview=_fmt_cookie_preview(cookie),
tag=account.tag or "",
remark=account.remark or "",
status=account.status or "",
points=account.points,
game_name=account.game_name or "",
game_channel=account.game_channel or "",
game_phone=account.game_phone or "",
assigned_to=account.assigned_to,
assigned_username=account.assigned_user.username if account.assigned_user else None,
created_at=account.created_at,
updated_at=account.updated_at,
)
def _task_out(task: HuyaTask) -> HuyaTaskOut:
account = task.account
return HuyaTaskOut(
id=task.id,
batch_id=task.batch_id,
account_id=task.account_id,
account_uid=account.uid if account else "",
account_nickname=account.nickname if account else "",
task_type=task.task_type,
status=task.status or "",
message=task.message or "",
result=task.result,
created_by=task.created_by,
created_at=task.created_at,
finished_at=task.finished_at,
)
@router.get("/task-types")
def task_types(current: User = Depends(require_permission("huya:task"))):
"""返回当前规划的虎牙任务类型。"""
return SUPPORTED_TASK_TYPES
@router.get("/accounts", response_model=list[HuyaAccountOut])
def list_accounts(
tag: str | None = Query(None),
db: Session = Depends(get_db),
current: User = Depends(require_permission("huya:account")),
):
"""查看虎牙 CK 账号。"""
query = db.query(HuyaAccount).options(joinedload(HuyaAccount.assigned_user))
if tag:
query = query.filter(HuyaAccount.tag == tag)
accounts = query.order_by(HuyaAccount.id.desc()).all()
return [_account_out(account) for account in accounts]
@router.post("/accounts/import-cookies")
def import_cookies(
req: HuyaCookieImport,
db: Session = Depends(get_db),
current: User = Depends(require_permission("huya:account")),
):
"""粘贴并导入虎牙 Cookie。"""
count, skipped = import_huya_cookies(db, req.text, req.tag)
return {
"message": f"导入/更新 {count} 条,跳过 {skipped}",
"success": True,
"count": count,
"skipped": skipped,
}
@router.delete("/accounts/batch")
def delete_accounts_batch(
account_ids: str = Query(..., description="逗号分隔的虎牙账号ID"),
db: Session = Depends(get_db),
current: User = Depends(require_permission("huya:account")),
):
"""批量删除虎牙 CK 账号及任务记录。"""
ids = [int(x) for x in account_ids.split(",") if x.strip().isdigit()]
if not ids:
raise HTTPException(status_code=400, detail="无效的账号ID")
db.query(HuyaTask).filter(HuyaTask.account_id.in_(ids)).delete(synchronize_session=False)
deleted = db.query(HuyaAccount).filter(HuyaAccount.id.in_(ids)).delete(synchronize_session=False)
db.commit()
return {"message": f"已删除 {deleted} 个虎牙账号", "deleted": deleted, "success": True}
@router.delete("/accounts/{account_id}")
def delete_account(
account_id: int,
db: Session = Depends(get_db),
current: User = Depends(require_permission("huya:account")),
):
"""删除单个虎牙 CK 账号。"""
account = db.query(HuyaAccount).filter(HuyaAccount.id == account_id).first()
if not account:
raise HTTPException(status_code=404, detail="账号不存在")
db.query(HuyaTask).filter(HuyaTask.account_id == account_id).delete(synchronize_session=False)
db.delete(account)
db.commit()
return {"message": "已删除", "success": True}
@router.get("/config", response_model=HuyaConfigOut)
def get_config(
db: Session = Depends(get_db),
current: User = Depends(require_permission("huya:config")),
):
"""获取虎牙配置。"""
config = ensure_huya_config(db)
return HuyaConfigOut(
room_pid=config.room_pid or "",
sid=config.sid or "",
outer_act_id=config.outer_act_id or "9504",
bind_act_id=config.bind_act_id or "17096",
pay_channel=config.pay_channel or "Zfb",
updated_at=config.updated_at,
)
@router.put("/config", response_model=HuyaConfigOut)
def update_config(
req: HuyaConfigUpdate,
db: Session = Depends(get_db),
current: User = Depends(require_permission("huya:config")),
):
"""更新虎牙配置。"""
config = ensure_huya_config(db)
for field in ("room_pid", "sid", "outer_act_id", "bind_act_id", "pay_channel"):
value = getattr(req, field)
if value is not None:
setattr(config, field, value.strip())
config.updated_at = datetime.now(timezone.utc)
db.commit()
db.refresh(config)
return HuyaConfigOut(
room_pid=config.room_pid or "",
sid=config.sid or "",
outer_act_id=config.outer_act_id or "9504",
bind_act_id=config.bind_act_id or "17096",
pay_channel=config.pay_channel or "Zfb",
updated_at=config.updated_at,
)
@router.get("/goods", response_model=list[HuyaGoodsOut])
def list_goods(
db: Session = Depends(get_db),
current: User = Depends(require_permission("huya:task")),
):
"""查看已缓存的虎牙商品快照。"""
rows = db.query(HuyaGoodsSnapshot).order_by(HuyaGoodsSnapshot.updated_at.desc()).all()
return rows
@router.post("/tasks/batch")
def create_task_batch(
req: HuyaTaskBatchRequest,
db: Session = Depends(get_db),
current: User = Depends(require_permission("huya:task")),
):
"""创建虎牙任务记录,真实执行器后续接入。"""
if not req.account_ids:
raise HTTPException(status_code=400, detail="请选择虎牙账号")
try:
batch_id, count = create_planned_tasks(
db,
req.account_ids,
req.task_type,
current.id,
req.payload,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
if count == 0:
raise HTTPException(status_code=400, detail="没有有效的虎牙账号")
return {"batch_id": batch_id, "count": count, "success": True}
@router.get("/tasks", response_model=list[HuyaTaskOut])
def list_tasks(
batch_id: str | None = None,
db: Session = Depends(get_db),
current: User = Depends(require_permission("huya:task")),
):
"""查看虎牙任务记录。"""
query = db.query(HuyaTask).options(joinedload(HuyaTask.account))
if batch_id:
query = query.filter(HuyaTask.batch_id == batch_id)
tasks = query.order_by(HuyaTask.id.desc()).limit(300).all()
return [_task_out(task) for task in tasks]
@router.websocket("/ws/{batch_id}")
async def ws_huya_logs(websocket: WebSocket, batch_id: str):
"""虎牙实时日志占位通道。"""
user = authenticate_websocket(websocket)
if not user:
await websocket.close(code=1008, reason="未授权")
return
await websocket.accept()
await websocket.send_json({
"level": "warning",
"message": f"虎牙批次 {batch_id} 已创建,真实执行器尚未接入",
})
await websocket.send_json({"level": "result", "message": ""})
await websocket.close()
+146
View File
@@ -160,6 +160,152 @@ class LoginTaskOut(BaseModel):
}
# ---- 虎牙 ----
class HuyaCookieImport(BaseModel):
"""批量导入虎牙 Cookie。支持纯 CK 或 账号----密码----CK。"""
text: str
tag: str = ""
class HuyaAccountOut(BaseModel):
id: int
uid: str = ""
yyuid: str = ""
username: str = ""
nickname: str = ""
cookie: str = ""
cookie_preview: str = ""
tag: str = ""
remark: str = ""
status: str = ""
points: Optional[int] = None
game_name: str = ""
game_channel: str = ""
game_phone: str = ""
assigned_to: Optional[int] = None
assigned_username: Optional[str] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
@model_serializer
def _serialize(self) -> dict[str, Any]:
return {
"id": self.id,
"uid": self.uid,
"yyuid": self.yyuid,
"username": self.username,
"nickname": self.nickname,
"cookie": self.cookie,
"cookie_preview": self.cookie_preview,
"tag": self.tag,
"remark": self.remark,
"status": self.status,
"points": self.points,
"game_name": self.game_name,
"game_channel": self.game_channel,
"game_phone": self.game_phone,
"assigned_to": self.assigned_to,
"assigned_username": self.assigned_username,
"created_at": _ensure_tz(self.created_at).isoformat() if self.created_at else None,
"updated_at": _ensure_tz(self.updated_at).isoformat() if self.updated_at else None,
}
class HuyaConfigOut(BaseModel):
room_pid: str = ""
sid: str = ""
outer_act_id: str = "9504"
bind_act_id: str = "17096"
pay_channel: str = "Zfb"
updated_at: Optional[datetime] = None
@model_serializer
def _serialize(self) -> dict[str, Any]:
return {
"room_pid": self.room_pid,
"sid": self.sid,
"outer_act_id": self.outer_act_id,
"bind_act_id": self.bind_act_id,
"pay_channel": self.pay_channel,
"updated_at": _ensure_tz(self.updated_at).isoformat() if self.updated_at else None,
}
class HuyaConfigUpdate(BaseModel):
room_pid: Optional[str] = None
sid: Optional[str] = None
outer_act_id: Optional[str] = None
bind_act_id: Optional[str] = None
pay_channel: Optional[str] = None
class HuyaTaskBatchRequest(BaseModel):
account_ids: list[int]
task_type: str
concurrency: int = 3
payload: dict[str, Any] = Field(default_factory=dict)
class HuyaTaskOut(BaseModel):
id: int
batch_id: str
account_id: int
account_uid: str = ""
account_nickname: str = ""
task_type: str
status: str
message: str = ""
result: Optional[dict[str, Any]] = None
created_by: int
created_at: Optional[datetime] = None
finished_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
@model_serializer
def _serialize(self) -> dict[str, Any]:
return {
"id": self.id,
"batch_id": self.batch_id,
"account_id": self.account_id,
"account_uid": self.account_uid,
"account_nickname": self.account_nickname,
"task_type": self.task_type,
"status": self.status,
"message": self.message,
"result": self.result,
"created_by": self.created_by,
"created_at": _ensure_tz(self.created_at).isoformat() if self.created_at else None,
"finished_at": _ensure_tz(self.finished_at).isoformat() if self.finished_at else None,
}
class HuyaGoodsOut(BaseModel):
id: int
product_id: str
name: str = ""
price: Optional[int] = None
remain_text: str = ""
raw: Optional[dict[str, Any]] = None
updated_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
@model_serializer
def _serialize(self) -> dict[str, Any]:
return {
"id": self.id,
"product_id": self.product_id,
"name": self.name,
"price": self.price,
"remain_text": self.remain_text,
"raw": self.raw,
"updated_at": _ensure_tz(self.updated_at).isoformat() if self.updated_at else None,
}
# ---- 代理配置 ----
class ProxyConfigOut(BaseModel):
enabled: bool = False
+161
View File
@@ -0,0 +1,161 @@
"""虎牙基础业务服务。"""
import re
import uuid
from datetime import datetime, timezone
from sqlalchemy.orm import Session
from ..models import HuyaAccount, HuyaConfig, HuyaTask
SUPPORTED_TASK_TYPES = {
"get_bind_qr": "获取绑定二维码",
"query_points": "一键查询积分",
"open_elite_book": "开通精英宝典",
"recharge_points": "充值积分",
"query_game_name": "一键查询游戏名",
"query_exchange_records": "一键查询兑换记录",
"confirm_bind": "确认绑定",
"refresh_goods": "刷新商品列表",
}
def cookie_value(cookie: str, key: str) -> str:
"""从 Cookie 文本中提取指定 key。"""
match = re.search(rf"(?:^|;\s*){re.escape(key)}=([^;]+)", cookie or "")
return match.group(1).strip() if match else ""
def _looks_like_huya_cookie(value: str) -> bool:
"""判断文本是否像虎牙 Cookie。"""
return "udb_" in value or "yyuid=" in value
def parse_huya_cookie_line(line: str) -> dict | None:
"""解析单行虎牙 CK,兼容纯 CK、账号----密码----CK、CK----手机号。"""
raw = (line or "").strip()
if not raw:
return None
parts = [part.strip() for part in raw.split("----")]
username_hint = ""
game_phone = ""
if len(parts) == 1:
cookie = raw
elif _looks_like_huya_cookie(parts[0]):
cookie = parts[0]
game_phone = parts[1] if len(parts) >= 2 else ""
elif _looks_like_huya_cookie(parts[-1]):
cookie = parts[-1]
username_hint = parts[0]
else:
return None
if not _looks_like_huya_cookie(cookie):
return None
yyuid = cookie_value(cookie, "yyuid")
uid = cookie_value(cookie, "udb_uid") or yyuid
username = cookie_value(cookie, "udb_passport") or cookie_value(cookie, "username")
if not username and username_hint:
username = username_hint
if not uid and not yyuid:
return None
return {
"uid": uid,
"yyuid": yyuid or uid,
"username": username or uid or yyuid,
"cookie": cookie,
"game_phone": game_phone,
}
def import_huya_cookies(db: Session, text: str, tag: str = "") -> tuple[int, int]:
"""导入虎牙 Cookie,返回 (成功数, 跳过数)。"""
created_or_updated = 0
skipped = 0
tag = (tag or "").strip()
for line in (text or "").splitlines():
parsed = parse_huya_cookie_line(line)
if not parsed:
skipped += 1
continue
account = None
if parsed["uid"]:
account = db.query(HuyaAccount).filter(HuyaAccount.uid == parsed["uid"]).first()
if account is None and parsed["yyuid"]:
account = db.query(HuyaAccount).filter(HuyaAccount.yyuid == parsed["yyuid"]).first()
if account is None:
account = HuyaAccount(
uid=parsed["uid"],
yyuid=parsed["yyuid"],
username=parsed["username"],
cookie=parsed["cookie"],
game_phone=parsed["game_phone"],
tag=tag,
status="imported",
)
db.add(account)
else:
account.uid = parsed["uid"] or account.uid
account.yyuid = parsed["yyuid"] or account.yyuid
account.username = parsed["username"] or account.username
account.cookie = parsed["cookie"]
account.game_phone = parsed["game_phone"] or account.game_phone
if tag:
account.tag = tag
account.status = "updated"
account.updated_at = datetime.now(timezone.utc)
created_or_updated += 1
if created_or_updated:
db.commit()
return created_or_updated, skipped
def ensure_huya_config(db: Session) -> HuyaConfig:
"""获取单条虎牙配置,不存在则创建。"""
config = db.query(HuyaConfig).first()
if config:
return config
config = HuyaConfig()
db.add(config)
db.commit()
db.refresh(config)
return config
def create_planned_tasks(
db: Session,
account_ids: list[int],
task_type: str,
created_by: int,
payload: dict | None = None,
) -> tuple[str, int]:
"""创建虎牙任务记录,真实执行器后续接入。"""
if task_type not in SUPPORTED_TASK_TYPES:
raise ValueError("不支持的任务类型")
batch_id = uuid.uuid4().hex[:12]
payload = payload or {}
accounts = db.query(HuyaAccount).filter(HuyaAccount.id.in_(account_ids)).all()
for account in accounts:
db.add(HuyaTask(
batch_id=batch_id,
account_id=account.id,
task_type=task_type,
status="planned",
message="任务已创建,等待虎牙执行器接入",
result={"payload": payload} if payload else None,
created_by=created_by,
))
db.commit()
return batch_id, len(accounts)