虎牙支持先导入账号密码再登录
This commit is contained in:
@@ -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")
|
||||
@@ -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="")
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user