实现虎牙密码登录

This commit is contained in:
yml2213
2026-07-05 11:12:36 +08:00
parent 5534293ed1
commit 2824c5a54c
9 changed files with 657 additions and 39 deletions
+39
View File
@@ -7,6 +7,8 @@ from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, Query, WebSocket, WebSocketDisconnect
from sqlalchemy.orm import Session, joinedload
from core.huya import HuyaCredentialError, HuyaLoginError, login_huya_password
from ..database import SessionLocal, get_db
from ..deps import authenticate_websocket, require_permission
from ..models import HuyaAccount, HuyaConfig, HuyaGoodsSnapshot, HuyaRechargeGoodsSnapshot, HuyaTask, User
@@ -16,6 +18,7 @@ from ..schemas import (
HuyaConfigUpdate,
HuyaCookieImport,
HuyaGoodsOut,
HuyaPasswordLoginRequest,
HuyaRechargeGoodsOut,
HuyaTaskBatchRequest,
HuyaTaskOut,
@@ -28,6 +31,7 @@ from ..services.huya_service import (
ensure_huya_config,
huya_config_value,
import_huya_cookies,
upsert_huya_cookie,
)
from ..services.huya_runner import HuyaBatchRunner, huya_batch_registry
@@ -130,6 +134,41 @@ def import_cookies(
}
@router.post("/accounts/password-login")
def password_login_account(
req: HuyaPasswordLoginRequest,
db: Session = Depends(get_db),
current: User = Depends(require_permission("huya:account")),
):
"""使用账号密码登录虎牙,成功后保存 Cookie。"""
try:
result = login_huya_password(
username=req.username.strip(),
password=req.password,
cookie=req.cookie.strip() or None,
)
except HuyaCredentialError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except HuyaLoginError as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
except Exception as exc:
raise HTTPException(status_code=502, detail=f"虎牙密码登录失败: {exc}") from exc
if not result.success or not result.cookie:
raise HTTPException(status_code=502, detail=result.message or "虎牙密码登录失败")
try:
account = upsert_huya_cookie(db, result.cookie, tag=req.tag, username_hint=req.username)
except ValueError as exc:
raise HTTPException(status_code=502, detail=str(exc)) from exc
return {
"message": "登录成功,Cookie 已保存",
"success": True,
"account": _account_out(account),
"sdid": result.sdid,
}
@router.delete("/accounts/batch")
def delete_accounts_batch(
account_ids: str = Query(..., description="逗号分隔的虎牙账号ID"),
+8
View File
@@ -175,6 +175,14 @@ class HuyaCookieImport(BaseModel):
tag: str = ""
class HuyaPasswordLoginRequest(BaseModel):
"""虎牙账号密码登录并保存 Cookie。"""
username: str = Field(..., min_length=1, max_length=128)
password: str = Field(..., min_length=1, max_length=128)
tag: str = ""
cookie: str = ""
class HuyaAccountOut(BaseModel):
id: int
uid: str = ""
+45 -28
View File
@@ -97,6 +97,50 @@ def parse_huya_cookie_line(line: str) -> dict | None:
}
def _upsert_huya_account(db: Session, parsed: dict, tag: str = "", status: str | None = None) -> HuyaAccount:
"""按 uid/yyuid 新增或更新虎牙账号。"""
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=status or "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 = status or "updated"
account.updated_at = datetime.now(timezone.utc)
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
parsed = parse_huya_cookie_line(line)
if not parsed:
raise ValueError("登录成功但 Cookie 中没有识别到虎牙 uid")
account = _upsert_huya_account(db, parsed, tag=(tag or "").strip(), status="login_success")
db.commit()
db.refresh(account)
return account
def import_huya_cookies(db: Session, text: str, tag: str = "") -> tuple[int, int]:
"""导入虎牙 Cookie,返回 (成功数, 跳过数)。"""
created_or_updated = 0
@@ -109,34 +153,7 @@ def import_huya_cookies(db: Session, text: str, tag: str = "") -> tuple[int, int
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)
_upsert_huya_account(db, parsed, tag=tag)
created_or_updated += 1
if created_or_updated:
+4
View File
@@ -4,6 +4,8 @@ import type {
HuyaConfig,
HuyaCookieImportResult,
HuyaGoodsItem,
HuyaPasswordLoginRequest,
HuyaPasswordLoginResult,
HuyaRechargeGoodsItem,
HuyaTaskBatchRequest,
HuyaTaskBatchResult,
@@ -18,6 +20,8 @@ export const huyaApi = {
api.get<HuyaAccountItem[], HuyaAccountItem[]>('/huya/accounts', { params }),
importCookies: (text: string, tag: string = '') =>
api.post<HuyaCookieImportResult, HuyaCookieImportResult>('/huya/accounts/import-cookies', { text, tag }),
passwordLogin: (data: HuyaPasswordLoginRequest) =>
api.post<HuyaPasswordLoginResult, HuyaPasswordLoginResult>('/huya/accounts/password-login', data),
deleteAccount: (id: number) => api.delete<MessageResponse, MessageResponse>(`/huya/accounts/${id}`),
deleteAccounts: (accountIds: number[]) =>
api.delete<MessageDeletedResponse, MessageDeletedResponse>('/huya/accounts/batch', { params: { account_ids: accountIds.join(',') } }),
+12
View File
@@ -136,6 +136,18 @@ export interface HuyaCookieImportResult extends MessageCountResponse {
skipped: number;
}
export interface HuyaPasswordLoginRequest {
username: string;
password: string;
tag?: string;
cookie?: string;
}
export interface HuyaPasswordLoginResult extends MessageResponse {
account: HuyaAccountItem;
sdid: string;
}
export interface HuyaConfig {
room_pid: string;
sid: string;
+95 -2
View File
@@ -1,9 +1,9 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
Button, Card, Col, Input, message, Modal, Popconfirm, Row, Space, Statistic, Table, Tag, Typography,
Button, Card, Col, Form, Input, message, Modal, Popconfirm, Row, Space, Statistic, Table, Tag, Typography,
} from 'antd';
import type { TableProps } from 'antd';
import { DeleteOutlined, ImportOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
import { DeleteOutlined, ImportOutlined, LoginOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons';
import { huyaApi, type HuyaAccountItem } from '../api/modules';
import { usePermissions } from '../hooks/usePermissions';
import { formatTime } from '../utils/time';
@@ -15,6 +15,7 @@ const { TextArea } = Input;
const STATUS_LABELS: Record<string, string> = {
imported: '已导入',
updated: '已更新',
login_success: '登录成功',
active: '正常',
invalid: '失效',
};
@@ -22,10 +23,18 @@ const STATUS_LABELS: Record<string, string> = {
const STATUS_COLORS: Record<string, string> = {
imported: 'blue',
updated: 'cyan',
login_success: 'success',
active: 'success',
invalid: 'error',
};
interface PasswordLoginFormValues {
username: string;
password: string;
tag?: string;
cookie?: string;
}
export default function HuyaAccountsPage() {
const [accounts, setAccounts] = useState<HuyaAccountItem[]>([]);
const [loading, setLoading] = useState(false);
@@ -33,6 +42,8 @@ export default function HuyaAccountsPage() {
const [importText, setImportText] = useState('');
const [importTag, setImportTag] = useState('');
const [importing, setImporting] = useState(false);
const [passwordLoginOpen, setPasswordLoginOpen] = useState(false);
const [passwordLogging, setPasswordLogging] = useState(false);
const [searchText, setSearchText] = useState('');
const [selectedRowKeys, setSelectedRowKeys] = useState<React.Key[]>([]);
const [pageSize, setPageSize] = useState(() => {
@@ -40,6 +51,7 @@ export default function HuyaAccountsPage() {
return v ? Number(v) || 20 : 20;
});
const [currentPage, setCurrentPage] = useState(1);
const [passwordLoginForm] = Form.useForm<PasswordLoginFormValues>();
const { can } = usePermissions();
const canManage = can('huya:account');
@@ -98,6 +110,37 @@ export default function HuyaAccountsPage() {
}
};
const openPasswordLogin = () => {
passwordLoginForm.resetFields();
setPasswordLoginOpen(true);
};
const closePasswordLogin = () => {
if (passwordLogging) return;
setPasswordLoginOpen(false);
passwordLoginForm.resetFields();
};
const handlePasswordLogin = async (values: PasswordLoginFormValues) => {
setPasswordLogging(true);
try {
const result = await huyaApi.passwordLogin({
username: values.username.trim(),
password: values.password,
tag: values.tag?.trim() || '',
cookie: values.cookie?.trim() || '',
});
message.success(result.message || '登录成功,Cookie 已保存');
setPasswordLoginOpen(false);
passwordLoginForm.resetFields();
loadAccounts();
} catch (e: unknown) {
message.error(getErrorMessage(e));
} finally {
setPasswordLogging(false);
}
};
const handleDelete = async (id: number) => {
try {
await huyaApi.deleteAccount(id);
@@ -228,6 +271,11 @@ export default function HuyaAccountsPage() {
</Button>
</Popconfirm>
)}
{canManage && (
<Button icon={<LoginOutlined />} onClick={openPasswordLogin}>
</Button>
)}
{canManage && (
<Button type="primary" icon={<ImportOutlined />} onClick={() => setImportOpen(true)}>
CK
@@ -317,6 +365,51 @@ export default function HuyaAccountsPage() {
</Paragraph>
</Space>
</Modal>
<Modal
title="虎牙密码登录"
open={passwordLoginOpen}
onCancel={closePasswordLogin}
onOk={() => passwordLoginForm.submit()}
okText="登录并保存"
cancelButtonProps={{ disabled: passwordLogging }}
confirmLoading={passwordLogging}
maskClosable={!passwordLogging}
closable={!passwordLogging}
width={560}
>
<Form
form={passwordLoginForm}
layout="vertical"
requiredMark={false}
onFinish={handlePasswordLogin}
disabled={passwordLogging}
>
<Form.Item
name="username"
label="账号"
rules={[{ required: true, message: '请输入虎牙账号' }]}
>
<Input autoComplete="username" placeholder="手机号、邮箱或虎牙账号" />
</Form.Item>
<Form.Item
name="password"
label="密码"
rules={[{ required: true, message: '请输入密码' }]}
>
<Input.Password autoComplete="current-password" placeholder="虎牙登录密码" />
</Form.Item>
<Form.Item name="tag" label="标签">
<Input placeholder="可选,保存到账号标签" />
</Form.Item>
<Form.Item name="cookie" label="预置 Cookie">
<TextArea
rows={3}
placeholder="可选;若已有 hdid/sdid 等风控 Cookie,可粘贴在这里"
/>
</Form.Item>
</Form>
</Modal>
</div>
);
}