虎牙支持先导入账号密码再登录
This commit is contained in:
@@ -148,6 +148,8 @@ _SENSITIVE_COLUMNS: tuple[tuple[str, str], ...] = (
|
|||||||
("accounts", "email"),
|
("accounts", "email"),
|
||||||
("accounts", "email_password"),
|
("accounts", "email_password"),
|
||||||
("login_tasks", "cookie"),
|
("login_tasks", "cookie"),
|
||||||
|
("huya_accounts", "account_password"),
|
||||||
|
("huya_accounts", "cookie"),
|
||||||
("proxy_config", "api_url"),
|
("proxy_config", "api_url"),
|
||||||
("proxy_config", "http"),
|
("proxy_config", "http"),
|
||||||
("proxy_config", "https"),
|
("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):
|
class HuyaAccount(Base):
|
||||||
"""虎牙 Cookie 账号"""
|
"""虎牙账号"""
|
||||||
__tablename__ = "huya_accounts"
|
__tablename__ = "huya_accounts"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
uid = Column(String(32), default="", index=True)
|
uid = Column(String(32), default="", index=True)
|
||||||
yyuid = Column(String(32), default="", index=True)
|
yyuid = Column(String(32), default="", index=True)
|
||||||
username = Column(String(128), default="")
|
username = Column(String(128), default="")
|
||||||
|
account_password = Column(EncryptedText(), default="")
|
||||||
nickname = Column(String(128), default="")
|
nickname = Column(String(128), default="")
|
||||||
cookie = Column(EncryptedText(), nullable=False)
|
cookie = Column(EncryptedText(), nullable=False)
|
||||||
tag = Column(String(64), default="")
|
tag = Column(String(64), default="")
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ from ..schemas import (
|
|||||||
HuyaConfigUpdate,
|
HuyaConfigUpdate,
|
||||||
HuyaCookieImport,
|
HuyaCookieImport,
|
||||||
HuyaGoodsOut,
|
HuyaGoodsOut,
|
||||||
|
HuyaPasswordAccountImport,
|
||||||
HuyaPasswordLoginRequest,
|
HuyaPasswordLoginRequest,
|
||||||
|
HuyaPasswordLoginSelectedRequest,
|
||||||
HuyaRechargeGoodsOut,
|
HuyaRechargeGoodsOut,
|
||||||
HuyaTaskBatchRequest,
|
HuyaTaskBatchRequest,
|
||||||
HuyaTaskOut,
|
HuyaTaskOut,
|
||||||
@@ -40,6 +42,8 @@ from ..services.huya_service import (
|
|||||||
ensure_huya_config,
|
ensure_huya_config,
|
||||||
huya_config_value,
|
huya_config_value,
|
||||||
import_huya_cookies,
|
import_huya_cookies,
|
||||||
|
import_huya_password_accounts,
|
||||||
|
save_huya_login_cookie_to_account,
|
||||||
upsert_huya_cookie,
|
upsert_huya_cookie,
|
||||||
)
|
)
|
||||||
from ..services.huya_runner import HuyaBatchRunner, huya_batch_registry
|
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 "",
|
uid=account.uid or "",
|
||||||
yyuid=account.yyuid or "",
|
yyuid=account.yyuid or "",
|
||||||
username=account.username or "",
|
username=account.username or "",
|
||||||
|
has_password=bool(account.account_password),
|
||||||
nickname=account.nickname or "",
|
nickname=account.nickname or "",
|
||||||
cookie=cookie if include_cookie else "",
|
cookie=cookie if include_cookie else "",
|
||||||
cookie_preview=_fmt_cookie_preview(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")
|
@router.post("/accounts/password-login")
|
||||||
def password_login_account(
|
def password_login_account(
|
||||||
req: HuyaPasswordLoginRequest,
|
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")
|
@router.delete("/accounts/batch")
|
||||||
def delete_accounts_batch(
|
def delete_accounts_batch(
|
||||||
account_ids: str = Query(..., description="逗号分隔的虎牙账号ID"),
|
account_ids: str = Query(..., description="逗号分隔的虎牙账号ID"),
|
||||||
|
|||||||
@@ -183,11 +183,23 @@ class HuyaPasswordLoginRequest(BaseModel):
|
|||||||
cookie: str = ""
|
cookie: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class HuyaPasswordAccountImport(BaseModel):
|
||||||
|
"""导入虎牙账号密码,稍后再选择登录。"""
|
||||||
|
text: str = Field(..., min_length=1)
|
||||||
|
tag: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class HuyaPasswordLoginSelectedRequest(BaseModel):
|
||||||
|
"""选择已导入的虎牙账号执行密码登录。"""
|
||||||
|
account_ids: list[int]
|
||||||
|
|
||||||
|
|
||||||
class HuyaAccountOut(BaseModel):
|
class HuyaAccountOut(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
uid: str = ""
|
uid: str = ""
|
||||||
yyuid: str = ""
|
yyuid: str = ""
|
||||||
username: str = ""
|
username: str = ""
|
||||||
|
has_password: bool = False
|
||||||
nickname: str = ""
|
nickname: str = ""
|
||||||
cookie: str = ""
|
cookie: str = ""
|
||||||
cookie_preview: str = ""
|
cookie_preview: str = ""
|
||||||
@@ -212,6 +224,7 @@ class HuyaAccountOut(BaseModel):
|
|||||||
"uid": self.uid,
|
"uid": self.uid,
|
||||||
"yyuid": self.yyuid,
|
"yyuid": self.yyuid,
|
||||||
"username": self.username,
|
"username": self.username,
|
||||||
|
"has_password": self.has_password,
|
||||||
"nickname": self.nickname,
|
"nickname": self.nickname,
|
||||||
"cookie": self.cookie,
|
"cookie": self.cookie,
|
||||||
"cookie_preview": self.cookie_preview,
|
"cookie_preview": self.cookie_preview,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"""虎牙基础业务服务。"""
|
"""虎牙基础业务服务。"""
|
||||||
|
|
||||||
|
import csv
|
||||||
import uuid
|
import uuid
|
||||||
from datetime import datetime, timezone
|
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:
|
def _upsert_huya_account(db: Session, parsed: dict, tag: str = "", status: str | None = None) -> HuyaAccount:
|
||||||
"""按 uid/yyuid 新增或更新虎牙账号。"""
|
"""按 uid/yyuid 新增或更新虎牙账号。"""
|
||||||
account = None
|
account = None
|
||||||
@@ -125,6 +196,33 @@ def _upsert_huya_account(db: Session, parsed: dict, tag: str = "", status: str |
|
|||||||
return account
|
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:
|
def upsert_huya_cookie(db: Session, cookie: str, tag: str = "", username_hint: str = "") -> HuyaAccount:
|
||||||
"""保存单条登录得到的虎牙 Cookie。"""
|
"""保存单条登录得到的虎牙 Cookie。"""
|
||||||
line = f"{username_hint}----{cookie}" if username_hint else cookie
|
line = f"{username_hint}----{cookie}" if username_hint else cookie
|
||||||
|
|||||||
@@ -5,8 +5,11 @@ import type {
|
|||||||
HuyaCookieItem,
|
HuyaCookieItem,
|
||||||
HuyaCookieImportResult,
|
HuyaCookieImportResult,
|
||||||
HuyaGoodsItem,
|
HuyaGoodsItem,
|
||||||
|
HuyaPasswordAccountImportResult,
|
||||||
|
HuyaPasswordLoginBatchResult,
|
||||||
HuyaPasswordLoginRequest,
|
HuyaPasswordLoginRequest,
|
||||||
HuyaPasswordLoginResult,
|
HuyaPasswordLoginResult,
|
||||||
|
HuyaPasswordLoginSelectedRequest,
|
||||||
HuyaRechargeGoodsItem,
|
HuyaRechargeGoodsItem,
|
||||||
HuyaTaskBatchRequest,
|
HuyaTaskBatchRequest,
|
||||||
HuyaTaskBatchResult,
|
HuyaTaskBatchResult,
|
||||||
@@ -22,8 +25,12 @@ export const huyaApi = {
|
|||||||
api.get<HuyaAccountItem[], HuyaAccountItem[]>('/huya/accounts', { params }),
|
api.get<HuyaAccountItem[], HuyaAccountItem[]>('/huya/accounts', { params }),
|
||||||
importCookies: (text: string, tag: string = '') =>
|
importCookies: (text: string, tag: string = '') =>
|
||||||
api.post<HuyaCookieImportResult, HuyaCookieImportResult>('/huya/accounts/import-cookies', { text, tag }),
|
api.post<HuyaCookieImportResult, HuyaCookieImportResult>('/huya/accounts/import-cookies', { text, tag }),
|
||||||
|
importPasswordAccounts: (text: string, tag: string = '') =>
|
||||||
|
api.post<HuyaPasswordAccountImportResult, HuyaPasswordAccountImportResult>('/huya/accounts/import-passwords', { text, tag }),
|
||||||
passwordLogin: (data: HuyaPasswordLoginRequest) =>
|
passwordLogin: (data: HuyaPasswordLoginRequest) =>
|
||||||
api.post<HuyaPasswordLoginResult, HuyaPasswordLoginResult>('/huya/accounts/password-login', data),
|
api.post<HuyaPasswordLoginResult, HuyaPasswordLoginResult>('/huya/accounts/password-login', data),
|
||||||
|
passwordLoginSelected: (data: HuyaPasswordLoginSelectedRequest) =>
|
||||||
|
api.post<HuyaPasswordLoginBatchResult, HuyaPasswordLoginBatchResult>('/huya/accounts/password-login/selected', data),
|
||||||
assign: (id: number, assigned_to: number | null) =>
|
assign: (id: number, assigned_to: number | null) =>
|
||||||
api.put<MessageResponse, MessageResponse>(`/huya/accounts/${id}/assign`, { assigned_to }),
|
api.put<MessageResponse, MessageResponse>(`/huya/accounts/${id}/assign`, { assigned_to }),
|
||||||
batchAssign: (account_ids: number[], assigned_to: number | null) =>
|
batchAssign: (account_ids: number[], assigned_to: number | null) =>
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export interface HuyaAccountItem {
|
|||||||
uid: string;
|
uid: string;
|
||||||
yyuid: string;
|
yyuid: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
has_password: boolean;
|
||||||
nickname: string;
|
nickname: string;
|
||||||
cookie: string;
|
cookie: string;
|
||||||
cookie_preview: string;
|
cookie_preview: string;
|
||||||
@@ -148,6 +149,28 @@ export interface HuyaPasswordLoginResult extends MessageResponse {
|
|||||||
sdid: string;
|
sdid: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface HuyaPasswordLoginBatchItem {
|
||||||
|
line: number;
|
||||||
|
username: string;
|
||||||
|
success: boolean;
|
||||||
|
message: string;
|
||||||
|
account?: HuyaAccountItem;
|
||||||
|
sdid?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HuyaPasswordLoginBatchResult extends MessageCountResponse {
|
||||||
|
failed: number;
|
||||||
|
results: HuyaPasswordLoginBatchItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HuyaPasswordAccountImportResult extends MessageCountResponse {
|
||||||
|
skipped: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HuyaPasswordLoginSelectedRequest {
|
||||||
|
account_ids: number[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface HuyaCookieItem {
|
export interface HuyaCookieItem {
|
||||||
id: number;
|
id: number;
|
||||||
account_id: number;
|
account_id: number;
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import {
|
import {
|
||||||
Button, Card, Col, Form, Input, message, Modal, Popconfirm, Row, Select, Space, Statistic, Table, Tag, Typography,
|
Button, Card, Col, Input, message, Modal, Popconfirm, Row, Select, Space, Statistic, Table, Tag, Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { TableProps } from 'antd';
|
import type { TableProps } from 'antd';
|
||||||
import { DeleteOutlined, FilterOutlined, ImportOutlined, LoginOutlined, ReloadOutlined, SearchOutlined, TagOutlined } from '@ant-design/icons';
|
import { DeleteOutlined, FilterOutlined, ImportOutlined, LoginOutlined, ReloadOutlined, SearchOutlined, TagOutlined } from '@ant-design/icons';
|
||||||
import { huyaApi, type HuyaAccountItem, type SupportUserItem } from '../api/modules';
|
import {
|
||||||
|
huyaApi,
|
||||||
|
type HuyaAccountItem,
|
||||||
|
type HuyaPasswordLoginBatchItem,
|
||||||
|
type SupportUserItem,
|
||||||
|
} from '../api/modules';
|
||||||
import { usePermissions } from '../hooks/usePermissions';
|
import { usePermissions } from '../hooks/usePermissions';
|
||||||
import { formatTime } from '../utils/time';
|
import { formatTime } from '../utils/time';
|
||||||
import { getErrorMessage } from '../utils/error';
|
import { getErrorMessage } from '../utils/error';
|
||||||
@@ -15,7 +20,9 @@ const { TextArea } = Input;
|
|||||||
const STATUS_LABELS: Record<string, string> = {
|
const STATUS_LABELS: Record<string, string> = {
|
||||||
imported: '已导入',
|
imported: '已导入',
|
||||||
updated: '已更新',
|
updated: '已更新',
|
||||||
|
password_imported: '待登录',
|
||||||
login_success: '登录成功',
|
login_success: '登录成功',
|
||||||
|
login_failed: '登录失败',
|
||||||
active: '正常',
|
active: '正常',
|
||||||
invalid: '失效',
|
invalid: '失效',
|
||||||
};
|
};
|
||||||
@@ -23,18 +30,13 @@ const STATUS_LABELS: Record<string, string> = {
|
|||||||
const STATUS_COLORS: Record<string, string> = {
|
const STATUS_COLORS: Record<string, string> = {
|
||||||
imported: 'blue',
|
imported: 'blue',
|
||||||
updated: 'cyan',
|
updated: 'cyan',
|
||||||
|
password_imported: 'warning',
|
||||||
login_success: 'success',
|
login_success: 'success',
|
||||||
|
login_failed: 'error',
|
||||||
active: 'success',
|
active: 'success',
|
||||||
invalid: 'error',
|
invalid: 'error',
|
||||||
};
|
};
|
||||||
|
|
||||||
interface PasswordLoginFormValues {
|
|
||||||
username: string;
|
|
||||||
password: string;
|
|
||||||
tag?: string;
|
|
||||||
cookie?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HuyaAccountsPage() {
|
export default function HuyaAccountsPage() {
|
||||||
const [accounts, setAccounts] = useState<HuyaAccountItem[]>([]);
|
const [accounts, setAccounts] = useState<HuyaAccountItem[]>([]);
|
||||||
const [users, setUsers] = useState<SupportUserItem[]>([]);
|
const [users, setUsers] = useState<SupportUserItem[]>([]);
|
||||||
@@ -44,8 +46,13 @@ export default function HuyaAccountsPage() {
|
|||||||
const [importText, setImportText] = useState('');
|
const [importText, setImportText] = useState('');
|
||||||
const [importTag, setImportTag] = useState('');
|
const [importTag, setImportTag] = useState('');
|
||||||
const [importing, setImporting] = useState(false);
|
const [importing, setImporting] = useState(false);
|
||||||
const [passwordLoginOpen, setPasswordLoginOpen] = useState(false);
|
const [passwordImportOpen, setPasswordImportOpen] = useState(false);
|
||||||
|
const [passwordImportText, setPasswordImportText] = useState('');
|
||||||
|
const [passwordImportTag, setPasswordImportTag] = useState<string[]>([]);
|
||||||
|
const [passwordImporting, setPasswordImporting] = useState(false);
|
||||||
|
const [passwordLoginResultOpen, setPasswordLoginResultOpen] = useState(false);
|
||||||
const [passwordLogging, setPasswordLogging] = useState(false);
|
const [passwordLogging, setPasswordLogging] = useState(false);
|
||||||
|
const [passwordLoginResults, setPasswordLoginResults] = useState<HuyaPasswordLoginBatchItem[]>([]);
|
||||||
const [searchText, setSearchText] = useState('');
|
const [searchText, setSearchText] = useState('');
|
||||||
const [tagFilter, setTagFilter] = useState('');
|
const [tagFilter, setTagFilter] = useState('');
|
||||||
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
|
||||||
@@ -56,7 +63,6 @@ export default function HuyaAccountsPage() {
|
|||||||
return v ? Number(v) || 20 : 20;
|
return v ? Number(v) || 20 : 20;
|
||||||
});
|
});
|
||||||
const [currentPage, setCurrentPage] = useState(1);
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
const [passwordLoginForm] = Form.useForm<PasswordLoginFormValues>();
|
|
||||||
const { can } = usePermissions();
|
const { can } = usePermissions();
|
||||||
|
|
||||||
const canManage = can('huya:account');
|
const canManage = can('huya:account');
|
||||||
@@ -146,29 +152,52 @@ export default function HuyaAccountsPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openPasswordLogin = () => {
|
const openPasswordImport = () => {
|
||||||
passwordLoginForm.resetFields();
|
setPasswordImportText('');
|
||||||
setPasswordLoginOpen(true);
|
setPasswordImportTag([]);
|
||||||
|
setPasswordLoginResults([]);
|
||||||
|
setPasswordImportOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const closePasswordLogin = () => {
|
const handleImportPasswordAccounts = async () => {
|
||||||
if (passwordLogging) return;
|
if (!passwordImportText.trim()) {
|
||||||
setPasswordLoginOpen(false);
|
message.warning('请先粘贴虎牙账号密码');
|
||||||
passwordLoginForm.resetFields();
|
return;
|
||||||
|
}
|
||||||
|
setPasswordImporting(true);
|
||||||
|
try {
|
||||||
|
const tag = passwordImportTag.length > 0 ? passwordImportTag[passwordImportTag.length - 1].trim() : '';
|
||||||
|
const result = await huyaApi.importPasswordAccounts(passwordImportText, tag);
|
||||||
|
message.success(result.message);
|
||||||
|
setPasswordImportOpen(false);
|
||||||
|
setPasswordImportText('');
|
||||||
|
setPasswordImportTag([]);
|
||||||
|
loadAccounts();
|
||||||
|
loadTags();
|
||||||
|
} catch (e: unknown) {
|
||||||
|
message.error(getErrorMessage(e));
|
||||||
|
} finally {
|
||||||
|
setPasswordImporting(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePasswordLogin = async (values: PasswordLoginFormValues) => {
|
const handleLoginSelected = async () => {
|
||||||
|
if (selectedRowKeys.length === 0) {
|
||||||
|
message.warning('请先选择虎牙账号');
|
||||||
|
return;
|
||||||
|
}
|
||||||
setPasswordLogging(true);
|
setPasswordLogging(true);
|
||||||
try {
|
try {
|
||||||
const result = await huyaApi.passwordLogin({
|
const result = await huyaApi.passwordLoginSelected({
|
||||||
username: values.username.trim(),
|
account_ids: selectedRowKeys.map((key) => Number(key)),
|
||||||
password: values.password,
|
|
||||||
tag: values.tag?.trim() || '',
|
|
||||||
cookie: values.cookie?.trim() || '',
|
|
||||||
});
|
});
|
||||||
message.success(result.message || '登录成功,Cookie 已保存');
|
setPasswordLoginResults(result.results || []);
|
||||||
setPasswordLoginOpen(false);
|
setPasswordLoginResultOpen(true);
|
||||||
passwordLoginForm.resetFields();
|
if (result.failed > 0) {
|
||||||
|
message.warning(result.message);
|
||||||
|
} else {
|
||||||
|
message.success(result.message || '登录成功,Cookie 已保存');
|
||||||
|
}
|
||||||
loadAccounts();
|
loadAccounts();
|
||||||
loadTags();
|
loadTags();
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
@@ -192,7 +221,7 @@ export default function HuyaAccountsPage() {
|
|||||||
|
|
||||||
const handleDeleteSelected = async () => {
|
const handleDeleteSelected = async () => {
|
||||||
if (selectedRowKeys.length === 0) {
|
if (selectedRowKeys.length === 0) {
|
||||||
message.warning('请先选择虎牙 CK');
|
message.warning('请先选择虎牙账号');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@@ -248,6 +277,28 @@ export default function HuyaAccountsPage() {
|
|||||||
const boundCount = accounts.filter((item) => item.game_name || item.game_channel || item.game_phone).length;
|
const boundCount = accounts.filter((item) => item.game_name || item.game_channel || item.game_phone).length;
|
||||||
const pointCount = accounts.filter((item) => item.points !== null && item.points !== undefined).length;
|
const pointCount = accounts.filter((item) => item.points !== null && item.points !== undefined).length;
|
||||||
const assignedCount = accounts.filter((item) => item.assigned_to).length;
|
const assignedCount = accounts.filter((item) => item.assigned_to).length;
|
||||||
|
const passwordReadyCount = accounts.filter((item) => item.has_password).length;
|
||||||
|
|
||||||
|
const passwordLoginResultColumns: TableProps<HuyaPasswordLoginBatchItem>['columns'] = [
|
||||||
|
{ title: '账号ID', dataIndex: 'line', width: 80, align: 'center' },
|
||||||
|
{ title: '账号', dataIndex: 'username', width: 160, ellipsis: true },
|
||||||
|
{
|
||||||
|
title: '状态',
|
||||||
|
dataIndex: 'success',
|
||||||
|
width: 90,
|
||||||
|
align: 'center',
|
||||||
|
render: (success: boolean) => (
|
||||||
|
<Tag color={success ? 'success' : 'error'}>{success ? '成功' : '失败'}</Tag>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '保存账号',
|
||||||
|
width: 160,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (_: unknown, record) => record.account?.nickname || record.account?.username || record.account?.uid || '-',
|
||||||
|
},
|
||||||
|
{ title: '提示', dataIndex: 'message', ellipsis: true },
|
||||||
|
];
|
||||||
|
|
||||||
const columns: TableProps<HuyaAccountItem>['columns'] = [
|
const columns: TableProps<HuyaAccountItem>['columns'] = [
|
||||||
{ title: 'ID', dataIndex: 'id', width: 70, align: 'center' },
|
{ title: 'ID', dataIndex: 'id', width: 70, align: 'center' },
|
||||||
@@ -260,6 +311,7 @@ export default function HuyaAccountsPage() {
|
|||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
UID {record.uid || record.yyuid || '-'}
|
UID {record.uid || record.yyuid || '-'}
|
||||||
</Text>
|
</Text>
|
||||||
|
{record.has_password ? <Tag color="gold">已导入密码</Tag> : null}
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
@@ -422,16 +474,26 @@ export default function HuyaAccountsPage() {
|
|||||||
批量打标签
|
批量打标签
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{canImport && (
|
||||||
|
<Button
|
||||||
|
disabled={selectedRowKeys.length === 0}
|
||||||
|
icon={<LoginOutlined />}
|
||||||
|
loading={passwordLogging}
|
||||||
|
onClick={handleLoginSelected}
|
||||||
|
>
|
||||||
|
登录选中
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
{canDelete && selectedRowKeys.length > 0 && (
|
{canDelete && selectedRowKeys.length > 0 && (
|
||||||
<Popconfirm title={`确认删除选中的 ${selectedRowKeys.length} 条虎牙 CK?`} onConfirm={handleDeleteSelected}>
|
<Popconfirm title={`确认删除选中的 ${selectedRowKeys.length} 条虎牙账号?`} onConfirm={handleDeleteSelected}>
|
||||||
<Button danger icon={<DeleteOutlined />}>
|
<Button danger icon={<DeleteOutlined />}>
|
||||||
删除选中 ({selectedRowKeys.length})
|
删除选中 ({selectedRowKeys.length})
|
||||||
</Button>
|
</Button>
|
||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
)}
|
)}
|
||||||
{canImport && (
|
{canImport && (
|
||||||
<Button icon={<LoginOutlined />} onClick={openPasswordLogin}>
|
<Button icon={<ImportOutlined />} onClick={openPasswordImport}>
|
||||||
密码登录
|
导入账号密码
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
{canImport && (
|
{canImport && (
|
||||||
@@ -449,6 +511,9 @@ export default function HuyaAccountsPage() {
|
|||||||
<Col xs={24} sm={4}>
|
<Col xs={24} sm={4}>
|
||||||
<Card size="small"><Statistic title="标签数" value={tags.length} /></Card>
|
<Card size="small"><Statistic title="标签数" value={tags.length} /></Card>
|
||||||
</Col>
|
</Col>
|
||||||
|
<Col xs={24} sm={4}>
|
||||||
|
<Card size="small"><Statistic title="已导入密码" value={passwordReadyCount} /></Card>
|
||||||
|
</Col>
|
||||||
<Col xs={24} sm={4}>
|
<Col xs={24} sm={4}>
|
||||||
<Card size="small"><Statistic title="已查积分" value={pointCount} /></Card>
|
<Card size="small"><Statistic title="已查积分" value={pointCount} /></Card>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -553,48 +618,65 @@ export default function HuyaAccountsPage() {
|
|||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title="虎牙密码登录"
|
title="导入虎牙账号密码"
|
||||||
open={passwordLoginOpen}
|
open={passwordImportOpen}
|
||||||
onCancel={closePasswordLogin}
|
onCancel={() => {
|
||||||
onOk={() => passwordLoginForm.submit()}
|
if (passwordImporting) return;
|
||||||
okText="登录并保存"
|
setPasswordImportOpen(false);
|
||||||
cancelButtonProps={{ disabled: passwordLogging }}
|
}}
|
||||||
confirmLoading={passwordLogging}
|
onOk={handleImportPasswordAccounts}
|
||||||
maskClosable={!passwordLogging}
|
okText="导入"
|
||||||
closable={!passwordLogging}
|
cancelButtonProps={{ disabled: passwordImporting }}
|
||||||
width={560}
|
confirmLoading={passwordImporting}
|
||||||
|
maskClosable={!passwordImporting}
|
||||||
|
closable={!passwordImporting}
|
||||||
|
width={720}
|
||||||
>
|
>
|
||||||
<Form
|
<Space direction="vertical" style={{ width: '100%' }} size={12}>
|
||||||
form={passwordLoginForm}
|
<TextArea
|
||||||
layout="vertical"
|
rows={10}
|
||||||
requiredMark={false}
|
value={passwordImportText}
|
||||||
onFinish={handlePasswordLogin}
|
onChange={(e) => setPasswordImportText(e.target.value)}
|
||||||
disabled={passwordLogging}
|
placeholder="每行一条:账号----密码;如需预置 Cookie:账号----密码----Cookie"
|
||||||
>
|
disabled={passwordImporting}
|
||||||
<Form.Item
|
/>
|
||||||
name="username"
|
<Select
|
||||||
label="账号"
|
mode="tags"
|
||||||
rules={[{ required: true, message: '请输入虎牙账号' }]}
|
style={{ width: '100%' }}
|
||||||
>
|
placeholder="可选,保存到账号标签"
|
||||||
<Input autoComplete="username" placeholder="手机号、邮箱或虎牙账号" />
|
maxCount={1}
|
||||||
</Form.Item>
|
value={passwordImportTag}
|
||||||
<Form.Item
|
onChange={setPasswordImportTag}
|
||||||
name="password"
|
options={tags.map((tag) => ({ value: tag, label: tag }))}
|
||||||
label="密码"
|
disabled={passwordImporting}
|
||||||
rules={[{ required: true, message: '请输入密码' }]}
|
/>
|
||||||
>
|
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
|
||||||
<Input.Password autoComplete="current-password" placeholder="虎牙登录密码" />
|
导入后账号会出现在列表里,勾选需要登录的账号再点“登录选中”。
|
||||||
</Form.Item>
|
</Paragraph>
|
||||||
<Form.Item name="tag" label="标签">
|
</Space>
|
||||||
<Input placeholder="可选,保存到账号标签" />
|
</Modal>
|
||||||
</Form.Item>
|
|
||||||
<Form.Item name="cookie" label="预置 Cookie">
|
<Modal
|
||||||
<TextArea
|
title="虎牙登录结果"
|
||||||
rows={3}
|
open={passwordLoginResultOpen}
|
||||||
placeholder="可选;若已有 hdid/sdid 等风控 Cookie,可粘贴在这里"
|
onCancel={() => setPasswordLoginResultOpen(false)}
|
||||||
/>
|
footer={[
|
||||||
</Form.Item>
|
<Button key="ok" type="primary" onClick={() => setPasswordLoginResultOpen(false)}>
|
||||||
</Form>
|
确定
|
||||||
|
</Button>,
|
||||||
|
]}
|
||||||
|
width={860}
|
||||||
|
>
|
||||||
|
{passwordLoginResults.length > 0 && (
|
||||||
|
<Table
|
||||||
|
columns={passwordLoginResultColumns}
|
||||||
|
dataSource={passwordLoginResults}
|
||||||
|
rowKey={(record) => `${record.line}-${record.username || 'empty'}`}
|
||||||
|
size="small"
|
||||||
|
pagination={false}
|
||||||
|
scroll={{ x: 720, y: 260 }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user