虎牙支持先导入账号密码再登录

This commit is contained in:
yml2213
2026-07-05 20:47:16 +08:00
parent 552eb9fa82
commit 2203420146
9 changed files with 493 additions and 73 deletions
+2
View File
@@ -148,6 +148,8 @@ _SENSITIVE_COLUMNS: tuple[tuple[str, str], ...] = (
("accounts", "email"),
("accounts", "email_password"),
("login_tasks", "cookie"),
("huya_accounts", "account_password"),
("huya_accounts", "cookie"),
("proxy_config", "api_url"),
("proxy_config", "http"),
("proxy_config", "https"),
@@ -0,0 +1,42 @@
"""虎牙账号增加密码导入字段
Revision ID: 20260705_0006
Revises: 20260704_0005
Create Date: 2026-07-05
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "20260705_0006"
down_revision: Union[str, None] = "20260704_0005"
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 _columns(bind, table_name: str) -> set[str]:
if not _has_table(bind, table_name):
return set()
return {column["name"] for column in sa.inspect(bind).get_columns(table_name)}
def upgrade() -> None:
bind = op.get_bind()
if "account_password" not in _columns(bind, "huya_accounts"):
op.add_column(
"huya_accounts",
sa.Column("account_password", sa.Text(), nullable=True),
)
def downgrade() -> None:
bind = op.get_bind()
if "account_password" in _columns(bind, "huya_accounts"):
op.drop_column("huya_accounts", "account_password")
+2 -1
View File
@@ -79,13 +79,14 @@ class LoginTask(Base):
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="")
account_password = Column(EncryptedText(), default="")
nickname = Column(String(128), default="")
cookie = Column(EncryptedText(), nullable=False)
tag = Column(String(64), default="")
+152
View File
@@ -27,7 +27,9 @@ from ..schemas import (
HuyaConfigUpdate,
HuyaCookieImport,
HuyaGoodsOut,
HuyaPasswordAccountImport,
HuyaPasswordLoginRequest,
HuyaPasswordLoginSelectedRequest,
HuyaRechargeGoodsOut,
HuyaTaskBatchRequest,
HuyaTaskOut,
@@ -40,6 +42,8 @@ from ..services.huya_service import (
ensure_huya_config,
huya_config_value,
import_huya_cookies,
import_huya_password_accounts,
save_huya_login_cookie_to_account,
upsert_huya_cookie,
)
from ..services.huya_runner import HuyaBatchRunner, huya_batch_registry
@@ -93,6 +97,7 @@ def _account_out(account: HuyaAccount, include_cookie: bool = True) -> HuyaAccou
uid=account.uid or "",
yyuid=account.yyuid or "",
username=account.username or "",
has_password=bool(account.account_password),
nickname=account.nickname or "",
cookie=cookie if include_cookie else "",
cookie_preview=_fmt_cookie_preview(cookie) if include_cookie else "***",
@@ -200,6 +205,23 @@ def import_cookies(
}
@router.post("/accounts/import-passwords")
def import_password_accounts(
req: HuyaPasswordAccountImport,
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""导入虎牙账号密码,只入库不立即登录。"""
_require_huya_perm(current, "huya:import")
count, skipped = import_huya_password_accounts(db, req.text, req.tag)
return {
"message": f"导入/更新 {count} 个虎牙账号,跳过 {skipped}",
"success": True,
"count": count,
"skipped": skipped,
}
@router.post("/accounts/password-login")
def password_login_account(
req: HuyaPasswordLoginRequest,
@@ -236,6 +258,136 @@ def password_login_account(
}
@router.post("/accounts/password-login/selected")
def password_login_selected_accounts(
req: HuyaPasswordLoginSelectedRequest,
db: Session = Depends(get_db),
current: User = Depends(get_current_user),
):
"""对已导入的虎牙账号执行密码登录。"""
_require_huya_perm(current, "huya:import")
if not req.account_ids:
raise HTTPException(status_code=400, detail="请选择虎牙账号")
accounts = (
_visible_huya_accounts_query(db, current)
.filter(HuyaAccount.id.in_(req.account_ids))
.all()
)
account_map = {account.id: account for account in accounts}
results = []
success_count = 0
failed_count = 0
include_cookie = _can_view_huya_cookie(current)
for account_id in req.account_ids:
account = account_map.get(account_id)
if account is None:
failed_count += 1
results.append({
"line": account_id,
"username": "",
"success": False,
"message": "账号不存在或无权登录",
})
continue
username = (account.username or "").strip()
password = (account.account_password or "").strip()
if not username or not password:
failed_count += 1
results.append({
"line": account.id,
"username": username,
"success": False,
"message": "该账号未导入密码",
"account": _account_out(account, include_cookie=include_cookie),
})
continue
try:
result = login_huya_password(
username=username,
password=password,
cookie=account.cookie or None,
)
if not result.success or not result.cookie:
account.status = "login_failed"
account.updated_at = datetime.now(timezone.utc)
db.commit()
failed_count += 1
results.append({
"line": account.id,
"username": username,
"success": False,
"message": result.message or "虎牙密码登录失败",
"account": _account_out(account, include_cookie=include_cookie),
"sdid": result.sdid,
})
continue
saved = save_huya_login_cookie_to_account(
db,
account,
result.cookie,
tag=account.tag or "",
username_hint=username,
)
success_count += 1
results.append({
"line": account.id,
"username": username,
"success": True,
"message": "登录成功,Cookie 已保存",
"account": _account_out(saved, include_cookie=include_cookie),
"sdid": result.sdid,
})
except HuyaCredentialError as exc:
account.status = "login_failed"
account.updated_at = datetime.now(timezone.utc)
db.commit()
failed_count += 1
results.append({
"line": account.id,
"username": username,
"success": False,
"message": str(exc),
"account": _account_out(account, include_cookie=include_cookie),
})
except (HuyaLoginError, ValueError) as exc:
account.status = "login_failed"
account.updated_at = datetime.now(timezone.utc)
db.commit()
failed_count += 1
results.append({
"line": account.id,
"username": username,
"success": False,
"message": str(exc),
"account": _account_out(account, include_cookie=include_cookie),
})
except Exception as exc:
account.status = "login_failed"
account.updated_at = datetime.now(timezone.utc)
db.commit()
failed_count += 1
results.append({
"line": account.id,
"username": username,
"success": False,
"message": f"虎牙密码登录失败: {exc}",
"account": _account_out(account, include_cookie=include_cookie),
})
return {
"message": f"登录完成:成功 {success_count} 条,失败 {failed_count}",
"success": True,
"count": success_count,
"failed": failed_count,
"results": results,
}
@router.delete("/accounts/batch")
def delete_accounts_batch(
account_ids: str = Query(..., description="逗号分隔的虎牙账号ID"),
+13
View File
@@ -183,11 +183,23 @@ class HuyaPasswordLoginRequest(BaseModel):
cookie: str = ""
class HuyaPasswordAccountImport(BaseModel):
"""导入虎牙账号密码,稍后再选择登录。"""
text: str = Field(..., min_length=1)
tag: str = ""
class HuyaPasswordLoginSelectedRequest(BaseModel):
"""选择已导入的虎牙账号执行密码登录。"""
account_ids: list[int]
class HuyaAccountOut(BaseModel):
id: int
uid: str = ""
yyuid: str = ""
username: str = ""
has_password: bool = False
nickname: str = ""
cookie: str = ""
cookie_preview: str = ""
@@ -212,6 +224,7 @@ class HuyaAccountOut(BaseModel):
"uid": self.uid,
"yyuid": self.yyuid,
"username": self.username,
"has_password": self.has_password,
"nickname": self.nickname,
"cookie": self.cookie,
"cookie_preview": self.cookie_preview,
+98
View File
@@ -1,5 +1,6 @@
"""虎牙基础业务服务。"""
import csv
import uuid
from datetime import datetime, timezone
@@ -93,6 +94,76 @@ def parse_huya_cookie_line(line: str) -> dict | None:
}
def split_huya_password_line(line: str) -> tuple[str, str, str] | None:
"""拆分虎牙账号密码行,返回账号、密码、预置 Cookie。"""
raw = (line or "").strip()
if not raw or raw.startswith("#"):
return None
if "----" in raw:
parts = [part.strip() for part in raw.split("----")]
elif "|" in raw:
parts = [part.strip() for part in raw.split("|")]
elif "\t" in raw:
parts = [part.strip() for part in raw.split("\t")]
elif "," in raw:
parts = [part.strip() for part in next(csv.reader([raw]))]
else:
parts = raw.split()
if len(parts) < 2:
return None
username = parts[0].strip()
password = parts[1].strip()
cookie = "----".join(part.strip() for part in parts[2:] if part.strip())
if not username or not password:
return None
return username, password, cookie
def import_huya_password_accounts(db: Session, text: str, tag: str = "") -> tuple[int, int]:
"""导入虎牙账号密码,返回 (导入/更新数, 跳过数)。"""
created_or_updated = 0
skipped = 0
tag = (tag or "").strip()
for line in (text or "").splitlines():
parsed = split_huya_password_line(line)
if not parsed:
if line.strip():
skipped += 1
continue
username, password, cookie = parsed
account = db.query(HuyaAccount).filter(HuyaAccount.username == username).first()
if account is None:
account = HuyaAccount(
uid="",
yyuid="",
username=username,
account_password=password,
cookie=normalize_huya_cookie(cookie) if cookie else "",
tag=tag,
status="password_imported",
)
db.add(account)
else:
account.account_password = password
if cookie:
account.cookie = normalize_huya_cookie(cookie)
if tag:
account.tag = tag
if not account.cookie:
account.status = "password_imported"
account.updated_at = datetime.now(timezone.utc)
created_or_updated += 1
if created_or_updated:
db.commit()
return created_or_updated, skipped
def _upsert_huya_account(db: Session, parsed: dict, tag: str = "", status: str | None = None) -> HuyaAccount:
"""按 uid/yyuid 新增或更新虎牙账号。"""
account = None
@@ -125,6 +196,33 @@ def _upsert_huya_account(db: Session, parsed: dict, tag: str = "", status: str |
return account
def save_huya_login_cookie_to_account(
db: Session,
account: HuyaAccount,
cookie: str,
tag: str = "",
username_hint: str = "",
) -> HuyaAccount:
"""把登录成功后的 Cookie 回填到指定虎牙账号。"""
line = f"{username_hint}----{cookie}" if username_hint else cookie
parsed = parse_huya_cookie_line(line)
if not parsed:
raise ValueError("登录成功但 Cookie 中没有识别到虎牙 uid")
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 = "login_success"
account.updated_at = datetime.now(timezone.utc)
db.commit()
db.refresh(account)
return account
def upsert_huya_cookie(db: Session, cookie: str, tag: str = "", username_hint: str = "") -> HuyaAccount:
"""保存单条登录得到的虎牙 Cookie。"""
line = f"{username_hint}----{cookie}" if username_hint else cookie