虎牙自动注册持久化成功流水与批次,支持失败续跑与随时导出
将注册批次/条目/成功记录落库,成功一个写入一条流水;服务中断可恢复并续跑失败项,换电脑也能导出 txt。同时修复 pending 被误判为运行中导致页面卡住的问题。
This commit is contained in:
@@ -150,6 +150,10 @@ _SENSITIVE_COLUMNS: tuple[tuple[str, str], ...] = (
|
||||
("login_tasks", "cookie"),
|
||||
("huya_accounts", "account_password"),
|
||||
("huya_accounts", "cookie"),
|
||||
("huya_register_batches", "fixed_password"),
|
||||
("huya_register_items", "password"),
|
||||
("huya_register_items", "cookie"),
|
||||
("huya_register_success_logs", "password"),
|
||||
("proxy_config", "api_url"),
|
||||
("proxy_config", "http"),
|
||||
("proxy_config", "https"),
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
"""虎牙自动注册批次/条目/成功流水持久化
|
||||
|
||||
Revision ID: 20260712_0007
|
||||
Revises: 20260705_0006
|
||||
Create Date: 2026-07-12
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = "20260712_0007"
|
||||
down_revision: Union[str, None] = "20260705_0006"
|
||||
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 "sms_url" not in _columns(bind, "huya_accounts"):
|
||||
op.add_column("huya_accounts", sa.Column("sms_url", sa.Text(), nullable=True))
|
||||
|
||||
if not _has_table(bind, "huya_register_batches"):
|
||||
op.create_table(
|
||||
"huya_register_batches",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("batch_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("tag", sa.String(length=64), nullable=True),
|
||||
sa.Column("created_by", sa.Integer(), nullable=False),
|
||||
sa.Column("concurrency", sa.Integer(), nullable=True),
|
||||
sa.Column("wait_seconds", sa.Integer(), nullable=True),
|
||||
sa.Column("poll_interval", sa.Integer(), nullable=True),
|
||||
sa.Column("password_prefix", sa.String(length=16), nullable=True),
|
||||
sa.Column("fixed_password", sa.Text(), nullable=True),
|
||||
sa.Column("use_proxy", sa.Boolean(), nullable=True),
|
||||
sa.Column("status", sa.String(length=32), nullable=True),
|
||||
sa.Column("message", sa.String(length=512), nullable=True),
|
||||
sa.Column("total", sa.Integer(), nullable=True),
|
||||
sa.Column("success_count", sa.Integer(), nullable=True),
|
||||
sa.Column("failed_count", sa.Integer(), nullable=True),
|
||||
sa.Column("stopped_count", sa.Integer(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("started_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("finished_at", sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(["created_by"], ["users.id"]),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
sa.UniqueConstraint("batch_id"),
|
||||
)
|
||||
op.create_index("ix_huya_register_batches_batch_id", "huya_register_batches", ["batch_id"])
|
||||
op.create_index("ix_huya_register_batches_created_by", "huya_register_batches", ["created_by"])
|
||||
op.create_index("ix_huya_register_batches_status", "huya_register_batches", ["status"])
|
||||
|
||||
if not _has_table(bind, "huya_register_items"):
|
||||
op.create_table(
|
||||
"huya_register_items",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("batch_db_id", sa.Integer(), nullable=False),
|
||||
sa.Column("batch_id", sa.String(length=64), nullable=False),
|
||||
sa.Column("line", sa.Integer(), nullable=False),
|
||||
sa.Column("phone", sa.String(length=64), nullable=True),
|
||||
sa.Column("provider", sa.String(length=32), nullable=True),
|
||||
sa.Column("sms_url", sa.Text(), nullable=True),
|
||||
sa.Column("status", sa.String(length=32), nullable=True),
|
||||
sa.Column("message", sa.String(length=512), nullable=True),
|
||||
sa.Column("code", sa.String(length=32), nullable=True),
|
||||
sa.Column("change_code", sa.String(length=32), nullable=True),
|
||||
sa.Column("attempts", sa.Integer(), nullable=True),
|
||||
sa.Column("change_attempts", sa.Integer(), nullable=True),
|
||||
sa.Column("account_id", sa.Integer(), nullable=True),
|
||||
sa.Column("username", sa.String(length=128), nullable=True),
|
||||
sa.Column("uid", sa.String(length=32), nullable=True),
|
||||
sa.Column("password", sa.Text(), nullable=True),
|
||||
sa.Column("password_changed", sa.Boolean(), nullable=True),
|
||||
sa.Column("cookie", sa.Text(), nullable=True),
|
||||
sa.Column("started_at", sa.DateTime(), nullable=True),
|
||||
sa.Column("finished_at", sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(["account_id"], ["huya_accounts.id"]),
|
||||
sa.ForeignKeyConstraint(["batch_db_id"], ["huya_register_batches.id"]),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("ix_huya_register_items_batch_db_id", "huya_register_items", ["batch_db_id"])
|
||||
op.create_index("ix_huya_register_items_batch_id", "huya_register_items", ["batch_id"])
|
||||
op.create_index("ix_huya_register_items_phone", "huya_register_items", ["phone"])
|
||||
op.create_index("ix_huya_register_items_status", "huya_register_items", ["status"])
|
||||
|
||||
if not _has_table(bind, "huya_register_success_logs"):
|
||||
op.create_table(
|
||||
"huya_register_success_logs",
|
||||
sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column("batch_id", sa.String(length=64), nullable=True),
|
||||
sa.Column("item_id", sa.Integer(), nullable=True),
|
||||
sa.Column("account_id", sa.Integer(), nullable=True),
|
||||
sa.Column("phone", sa.String(length=64), nullable=True),
|
||||
sa.Column("username", sa.String(length=128), nullable=True),
|
||||
sa.Column("uid", sa.String(length=32), nullable=True),
|
||||
sa.Column("password", sa.Text(), nullable=True),
|
||||
sa.Column("sms_url", sa.Text(), nullable=True),
|
||||
sa.Column("tag", sa.String(length=64), nullable=True),
|
||||
sa.Column("provider", sa.String(length=32), nullable=True),
|
||||
sa.Column("created_by", sa.Integer(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=True),
|
||||
sa.ForeignKeyConstraint(["account_id"], ["huya_accounts.id"]),
|
||||
sa.ForeignKeyConstraint(["created_by"], ["users.id"]),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index("ix_huya_register_success_logs_batch_id", "huya_register_success_logs", ["batch_id"])
|
||||
op.create_index("ix_huya_register_success_logs_phone", "huya_register_success_logs", ["phone"])
|
||||
op.create_index("ix_huya_register_success_logs_tag", "huya_register_success_logs", ["tag"])
|
||||
op.create_index("ix_huya_register_success_logs_created_at", "huya_register_success_logs", ["created_at"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
if _has_table(bind, "huya_register_success_logs"):
|
||||
op.drop_table("huya_register_success_logs")
|
||||
if _has_table(bind, "huya_register_items"):
|
||||
op.drop_table("huya_register_items")
|
||||
if _has_table(bind, "huya_register_batches"):
|
||||
op.drop_table("huya_register_batches")
|
||||
if "sms_url" in _columns(bind, "huya_accounts"):
|
||||
op.drop_column("huya_accounts", "sms_url")
|
||||
@@ -96,6 +96,7 @@ class HuyaAccount(Base):
|
||||
game_name = Column(String(128), default="")
|
||||
game_channel = Column(String(64), default="")
|
||||
game_phone = Column(String(64), default="")
|
||||
sms_url = Column(Text, 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)
|
||||
@@ -104,6 +105,81 @@ class HuyaAccount(Base):
|
||||
tasks = relationship("HuyaTask", back_populates="account")
|
||||
|
||||
|
||||
class HuyaRegisterBatch(Base):
|
||||
"""虎牙自动注册批次(持久化元数据)"""
|
||||
__tablename__ = "huya_register_batches"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
batch_id = Column(String(64), unique=True, nullable=False, index=True)
|
||||
tag = Column(String(64), default="")
|
||||
created_by = Column(Integer, ForeignKey("users.id"), nullable=False, index=True)
|
||||
concurrency = Column(Integer, default=1)
|
||||
wait_seconds = Column(Integer, default=180)
|
||||
poll_interval = Column(Integer, default=5)
|
||||
password_prefix = Column(String(16), default="hy")
|
||||
fixed_password = Column(EncryptedText(), default="")
|
||||
use_proxy = Column(Boolean, default=False)
|
||||
status = Column(String(32), default="pending", index=True)
|
||||
message = Column(String(512), default="")
|
||||
total = Column(Integer, default=0)
|
||||
success_count = Column(Integer, default=0)
|
||||
failed_count = Column(Integer, default=0)
|
||||
stopped_count = Column(Integer, default=0)
|
||||
created_at = Column(DateTime, default=_utcnow)
|
||||
started_at = Column(DateTime, nullable=True)
|
||||
finished_at = Column(DateTime, nullable=True)
|
||||
|
||||
items = relationship("HuyaRegisterItem", back_populates="batch", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class HuyaRegisterItem(Base):
|
||||
"""虎牙自动注册批次内单条手机号状态"""
|
||||
__tablename__ = "huya_register_items"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
batch_db_id = Column(Integer, ForeignKey("huya_register_batches.id"), nullable=False, index=True)
|
||||
batch_id = Column(String(64), nullable=False, index=True)
|
||||
line = Column(Integer, nullable=False)
|
||||
phone = Column(String(64), default="", index=True)
|
||||
provider = Column(String(32), default="")
|
||||
sms_url = Column(Text, default="")
|
||||
status = Column(String(32), default="pending", index=True)
|
||||
message = Column(String(512), default="")
|
||||
code = Column(String(32), default="")
|
||||
change_code = Column(String(32), default="")
|
||||
attempts = Column(Integer, default=0)
|
||||
change_attempts = Column(Integer, default=0)
|
||||
account_id = Column(Integer, ForeignKey("huya_accounts.id"), nullable=True)
|
||||
username = Column(String(128), default="")
|
||||
uid = Column(String(32), default="")
|
||||
password = Column(EncryptedText(), default="")
|
||||
password_changed = Column(Boolean, default=False)
|
||||
cookie = Column(EncryptedText(), default="")
|
||||
started_at = Column(DateTime, nullable=True)
|
||||
finished_at = Column(DateTime, nullable=True)
|
||||
|
||||
batch = relationship("HuyaRegisterBatch", back_populates="items")
|
||||
|
||||
|
||||
class HuyaRegisterSuccessLog(Base):
|
||||
"""虎牙自动注册成功流水(只追加,供随时导出)"""
|
||||
__tablename__ = "huya_register_success_logs"
|
||||
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
batch_id = Column(String(64), default="", index=True)
|
||||
item_id = Column(Integer, nullable=True)
|
||||
account_id = Column(Integer, ForeignKey("huya_accounts.id"), nullable=True)
|
||||
phone = Column(String(64), default="", index=True)
|
||||
username = Column(String(128), default="")
|
||||
uid = Column(String(32), default="")
|
||||
password = Column(EncryptedText(), default="")
|
||||
sms_url = Column(Text, default="")
|
||||
tag = Column(String(64), default="", index=True)
|
||||
provider = Column(String(32), default="")
|
||||
created_by = Column(Integer, ForeignKey("users.id"), nullable=True)
|
||||
created_at = Column(DateTime, default=_utcnow, index=True)
|
||||
|
||||
|
||||
class HuyaTask(Base):
|
||||
"""虎牙业务任务"""
|
||||
__tablename__ = "huya_tasks"
|
||||
|
||||
+121
-7
@@ -32,6 +32,7 @@ from ..schemas import (
|
||||
HuyaAccountOut,
|
||||
HuyaAutoRegisterBatchOut,
|
||||
HuyaAutoRegisterRequest,
|
||||
HuyaAutoRegisterRetryRequest,
|
||||
HuyaConfigOut,
|
||||
HuyaConfigUpdate,
|
||||
HuyaCookieImport,
|
||||
@@ -40,6 +41,7 @@ from ..schemas import (
|
||||
HuyaPasswordLoginRequest,
|
||||
HuyaPasswordLoginSelectedRequest,
|
||||
HuyaRechargeGoodsOut,
|
||||
HuyaRegisterSuccessLogOut,
|
||||
HuyaSmsCodeRequest,
|
||||
HuyaSmsLoginRequest,
|
||||
HuyaTaskBatchRequest,
|
||||
@@ -58,7 +60,11 @@ from ..services.huya_service import (
|
||||
upsert_huya_cookie,
|
||||
)
|
||||
from ..services.huya_runner import HuyaBatchRunner, huya_batch_registry
|
||||
from ..services.huya_register_runner import huya_register_registry
|
||||
from ..services.huya_register_runner import (
|
||||
export_success_logs_text,
|
||||
huya_register_registry,
|
||||
list_success_logs,
|
||||
)
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/huya", tags=["虎牙"])
|
||||
@@ -366,22 +372,33 @@ def create_auto_register_batch(
|
||||
use_proxy=req.use_proxy,
|
||||
proxy_config=proxy_config,
|
||||
)
|
||||
runner.mark_running("批次运行中")
|
||||
thread = threading.Thread(target=runner.run, daemon=True)
|
||||
thread.start()
|
||||
return runner.snapshot()
|
||||
|
||||
|
||||
@router.get("/register/batches", response_model=list[HuyaAutoRegisterBatchOut])
|
||||
def list_auto_register_batches(
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""列出历史自动注册批次(摘要,不含明细)。"""
|
||||
_require_huya_perm(current, "huya:import")
|
||||
return huya_register_registry.list_summaries(limit=limit)
|
||||
|
||||
|
||||
@router.get("/register/batches/{batch_id}", response_model=HuyaAutoRegisterBatchOut)
|
||||
def get_auto_register_batch(
|
||||
batch_id: str,
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""查询虎牙手机号自动注册批次状态。"""
|
||||
"""查询虎牙手机号自动注册批次状态(优先内存,否则读库)。"""
|
||||
_require_huya_perm(current, "huya:import")
|
||||
runner = huya_register_registry.get(batch_id)
|
||||
if not runner:
|
||||
raise HTTPException(status_code=404, detail="批次不存在或服务已重启")
|
||||
return runner.snapshot()
|
||||
snapshot = huya_register_registry.get_snapshot(batch_id)
|
||||
if not snapshot:
|
||||
raise HTTPException(status_code=404, detail="批次不存在")
|
||||
return snapshot
|
||||
|
||||
|
||||
@router.post("/register/batches/{batch_id}/stop")
|
||||
@@ -393,11 +410,108 @@ def stop_auto_register_batch(
|
||||
_require_huya_perm(current, "huya:import")
|
||||
runner = huya_register_registry.get(batch_id)
|
||||
if not runner:
|
||||
raise HTTPException(status_code=404, detail="批次不存在或服务已重启")
|
||||
# 无内存 runner:若 DB 中存在且处于 running/interrupted,标记停止
|
||||
snapshot = huya_register_registry.get_snapshot(batch_id)
|
||||
if not snapshot:
|
||||
raise HTTPException(status_code=404, detail="批次不存在")
|
||||
return {"message": "批次未在本进程运行,无需停止", "success": True}
|
||||
runner.stop()
|
||||
return {"message": "已发送停止信号", "success": True}
|
||||
|
||||
|
||||
@router.post("/register/batches/{batch_id}/retry", response_model=HuyaAutoRegisterBatchOut)
|
||||
def retry_auto_register_batch(
|
||||
batch_id: str,
|
||||
req: HuyaAutoRegisterRetryRequest = HuyaAutoRegisterRetryRequest(),
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""从失败/停止/未完成条目续跑,成功项跳过。"""
|
||||
_require_huya_perm(current, "huya:import")
|
||||
use_proxy = req.use_proxy
|
||||
# 先看历史批次是否用过代理
|
||||
snapshot = huya_register_registry.get_snapshot(batch_id)
|
||||
if not snapshot:
|
||||
raise HTTPException(status_code=404, detail="批次不存在")
|
||||
effective_proxy = snapshot.get("use_proxy", False) if use_proxy is None else bool(use_proxy)
|
||||
proxy_config = db.query(ProxyConfig).first() if effective_proxy else None
|
||||
try:
|
||||
runner = huya_register_registry.retry(
|
||||
batch_id,
|
||||
proxy_config=proxy_config,
|
||||
concurrency=req.concurrency,
|
||||
wait_seconds=req.wait_seconds,
|
||||
poll_interval=req.poll_interval,
|
||||
password_prefix=req.password_prefix,
|
||||
fixed_password=req.fixed_password,
|
||||
use_proxy=use_proxy,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
except RuntimeError as exc:
|
||||
raise HTTPException(status_code=409, detail=str(exc)) from exc
|
||||
thread = threading.Thread(target=runner.run, daemon=True)
|
||||
thread.start()
|
||||
return runner.snapshot()
|
||||
|
||||
|
||||
@router.get("/register/batches/{batch_id}/export")
|
||||
def export_auto_register_batch_success(
|
||||
batch_id: str,
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""导出指定批次的成功账号 txt。"""
|
||||
_require_huya_perm(current, "huya:import")
|
||||
snapshot = huya_register_registry.get_snapshot(batch_id)
|
||||
if not snapshot:
|
||||
raise HTTPException(status_code=404, detail="批次不存在")
|
||||
content = export_success_logs_text(batch_id=batch_id)
|
||||
if not content.strip():
|
||||
raise HTTPException(status_code=404, detail="该批次没有可导出的成功记录")
|
||||
return StreamingResponse(
|
||||
iter([content]),
|
||||
media_type="text/plain; charset=utf-8",
|
||||
headers={"Content-Disposition": f"attachment; filename=huya-register-{batch_id}.txt"},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/register/success-logs", response_model=list[HuyaRegisterSuccessLogOut])
|
||||
def list_register_success_logs(
|
||||
batch_id: str | None = Query(None),
|
||||
tag: str | None = Query(None),
|
||||
limit: int = Query(200, ge=1, le=2000),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""列出注册成功流水(换电脑也可查看)。"""
|
||||
_require_huya_perm(current, "huya:import")
|
||||
return list_success_logs(batch_id=batch_id, tag=tag, limit=limit)
|
||||
|
||||
|
||||
@router.get("/register/success-logs/export")
|
||||
def export_register_success_logs(
|
||||
batch_id: str | None = Query(None),
|
||||
tag: str | None = Query(None),
|
||||
limit: int = Query(5000, ge=1, le=20000),
|
||||
current: User = Depends(get_current_user),
|
||||
):
|
||||
"""导出注册成功流水 txt:账号----密码----手机号----接码链接。"""
|
||||
_require_huya_perm(current, "huya:import")
|
||||
content = export_success_logs_text(batch_id=batch_id, tag=tag, limit=limit)
|
||||
if not content.strip():
|
||||
raise HTTPException(status_code=404, detail="没有可导出的成功记录")
|
||||
filename = "huya-register-success"
|
||||
if batch_id:
|
||||
filename += f"-{batch_id}"
|
||||
if tag:
|
||||
filename += f"-{tag}"
|
||||
filename += ".txt"
|
||||
return StreamingResponse(
|
||||
iter([content]),
|
||||
media_type="text/plain; charset=utf-8",
|
||||
headers={"Content-Disposition": f"attachment; filename={filename}"},
|
||||
)
|
||||
|
||||
|
||||
@router.post("/accounts/password-login/selected")
|
||||
def password_login_selected_accounts(
|
||||
req: HuyaPasswordLoginSelectedRequest,
|
||||
|
||||
+28
-1
@@ -248,6 +248,16 @@ class HuyaAutoRegisterRequest(BaseModel):
|
||||
use_proxy: bool = False
|
||||
|
||||
|
||||
class HuyaAutoRegisterRetryRequest(BaseModel):
|
||||
"""从失败/停止/未完成条目续跑。可覆盖运行参数。"""
|
||||
concurrency: Optional[int] = Field(None, ge=1, le=5)
|
||||
wait_seconds: Optional[float] = Field(None, ge=15, le=600)
|
||||
poll_interval: Optional[float] = Field(None, ge=1, le=30)
|
||||
password_prefix: Optional[str] = Field(None, max_length=8)
|
||||
fixed_password: Optional[str] = Field(None, max_length=64)
|
||||
use_proxy: Optional[bool] = None
|
||||
|
||||
|
||||
class HuyaAutoRegisterItemOut(BaseModel):
|
||||
line: int
|
||||
phone: str
|
||||
@@ -289,7 +299,24 @@ class HuyaAutoRegisterBatchOut(BaseModel):
|
||||
created_at: Optional[datetime] = None
|
||||
started_at: Optional[datetime] = None
|
||||
finished_at: Optional[datetime] = None
|
||||
items: list[HuyaAutoRegisterItemOut]
|
||||
items: list[HuyaAutoRegisterItemOut] = []
|
||||
|
||||
|
||||
class HuyaRegisterSuccessLogOut(BaseModel):
|
||||
id: int
|
||||
batch_id: str = ""
|
||||
item_id: Optional[int] = None
|
||||
account_id: Optional[int] = None
|
||||
phone: str = ""
|
||||
username: str = ""
|
||||
uid: str = ""
|
||||
password: str = ""
|
||||
sms_url: str = ""
|
||||
tag: str = ""
|
||||
provider: str = ""
|
||||
created_by: Optional[int] = None
|
||||
created_at: Optional[datetime] = None
|
||||
export_line: str = ""
|
||||
|
||||
|
||||
class HuyaPasswordAccountImport(BaseModel):
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""虎牙自动注册批次执行器。"""
|
||||
"""虎牙自动注册批次执行器(支持持久化与失败续跑)。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -15,10 +15,22 @@ from core.huya.cookie_utils import normalize_huya_cookie
|
||||
from core.sms_provider import SmsLine
|
||||
|
||||
from ..database import SessionLocal
|
||||
from ..models import ProxyConfig as ProxyConfigModel
|
||||
from ..models import (
|
||||
HuyaRegisterBatch as HuyaRegisterBatchModel,
|
||||
HuyaRegisterItem as HuyaRegisterItemModel,
|
||||
HuyaRegisterSuccessLog,
|
||||
ProxyConfig as ProxyConfigModel,
|
||||
)
|
||||
from .huya_service import upsert_huya_cookie
|
||||
|
||||
|
||||
# 运行中状态:服务中断后视为可续跑
|
||||
RUNNING_ITEM_STATUSES = frozenset({"sending", "waiting", "changing", "logging"})
|
||||
# 可重试状态:失败 / 停止 / 中断后的运行态 / 尚未开始
|
||||
RETRYABLE_ITEM_STATUSES = frozenset({"error", "stopped", "pending"} | RUNNING_ITEM_STATUSES)
|
||||
TERMINAL_BATCH_STATUSES = frozenset({"finished", "stopped", "error", "interrupted"})
|
||||
|
||||
|
||||
def _now() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
@@ -32,7 +44,7 @@ def _cookie_preview(cookie: str) -> str:
|
||||
|
||||
@dataclass
|
||||
class HuyaRegisterItemState:
|
||||
"""单个手机号在批次中的状态。"""
|
||||
"""单个手机号在批次中的状态(内存镜像)。"""
|
||||
|
||||
line: int
|
||||
phone: str
|
||||
@@ -53,6 +65,7 @@ class HuyaRegisterItemState:
|
||||
cookie_preview: str = ""
|
||||
started_at: datetime | None = None
|
||||
finished_at: datetime | None = None
|
||||
db_id: int | None = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
@@ -97,6 +110,274 @@ class HuyaRegisterBatch:
|
||||
created_at: datetime = field(default_factory=_now)
|
||||
started_at: datetime | None = None
|
||||
finished_at: datetime | None = None
|
||||
db_id: int | None = None
|
||||
|
||||
|
||||
def _item_from_db(row: HuyaRegisterItemModel) -> HuyaRegisterItemState:
|
||||
cookie = normalize_huya_cookie(row.cookie or "")
|
||||
exposed = "" if row.password_changed else cookie
|
||||
return HuyaRegisterItemState(
|
||||
line=row.line,
|
||||
phone=row.phone or "",
|
||||
provider=row.provider or "",
|
||||
sms_url=row.sms_url or "",
|
||||
status=row.status or "pending",
|
||||
message=row.message or "",
|
||||
code=row.code or "",
|
||||
change_code=row.change_code or "",
|
||||
attempts=int(row.attempts or 0),
|
||||
change_attempts=int(row.change_attempts or 0),
|
||||
account_id=row.account_id,
|
||||
username=row.username or "",
|
||||
uid=row.uid or "",
|
||||
password=row.password or "",
|
||||
password_changed=bool(row.password_changed),
|
||||
cookie=exposed,
|
||||
cookie_preview=_cookie_preview(exposed),
|
||||
started_at=row.started_at,
|
||||
finished_at=row.finished_at,
|
||||
db_id=row.id,
|
||||
)
|
||||
|
||||
|
||||
def _batch_from_db(
|
||||
batch_row: HuyaRegisterBatchModel,
|
||||
item_rows: list[HuyaRegisterItemModel],
|
||||
) -> HuyaRegisterBatch:
|
||||
items = [_item_from_db(row) for row in sorted(item_rows, key=lambda x: x.line)]
|
||||
return HuyaRegisterBatch(
|
||||
batch_id=batch_row.batch_id,
|
||||
tag=batch_row.tag or "",
|
||||
created_by=batch_row.created_by,
|
||||
concurrency=int(batch_row.concurrency or 1),
|
||||
wait_seconds=float(batch_row.wait_seconds or 180),
|
||||
poll_interval=float(batch_row.poll_interval or 5),
|
||||
items=items,
|
||||
password_prefix=batch_row.password_prefix or "hy",
|
||||
fixed_password=batch_row.fixed_password or "",
|
||||
use_proxy=bool(batch_row.use_proxy),
|
||||
status=batch_row.status or "pending",
|
||||
message=batch_row.message or "",
|
||||
created_at=batch_row.created_at or _now(),
|
||||
started_at=batch_row.started_at,
|
||||
finished_at=batch_row.finished_at,
|
||||
db_id=batch_row.id,
|
||||
)
|
||||
|
||||
|
||||
def snapshot_from_batch(batch: HuyaRegisterBatch) -> dict:
|
||||
"""从内存批次生成 API 快照。"""
|
||||
total = len(batch.items)
|
||||
success = sum(1 for item in batch.items if item.status == "success")
|
||||
failed = sum(1 for item in batch.items if item.status == "error")
|
||||
stopped = sum(1 for item in batch.items if item.status == "stopped")
|
||||
running = sum(1 for item in batch.items if item.status in RUNNING_ITEM_STATUSES)
|
||||
return {
|
||||
"batch_id": batch.batch_id,
|
||||
"status": batch.status,
|
||||
"message": batch.message,
|
||||
"tag": batch.tag,
|
||||
"created_by": batch.created_by,
|
||||
"concurrency": batch.concurrency,
|
||||
"wait_seconds": batch.wait_seconds,
|
||||
"poll_interval": batch.poll_interval,
|
||||
"password_prefix": batch.password_prefix,
|
||||
"use_proxy": batch.use_proxy,
|
||||
"total": total,
|
||||
"success_count": success,
|
||||
"failed_count": failed,
|
||||
"stopped_count": stopped,
|
||||
"running_count": running,
|
||||
"created_at": batch.created_at,
|
||||
"started_at": batch.started_at,
|
||||
"finished_at": batch.finished_at,
|
||||
"items": [item.to_dict() for item in batch.items],
|
||||
}
|
||||
|
||||
|
||||
def load_batch_snapshot(batch_id: str, *, recover_interrupted: bool = True) -> dict | None:
|
||||
"""从数据库加载批次详情;若服务中断则标记为 interrupted。"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
batch_row = (
|
||||
db.query(HuyaRegisterBatchModel)
|
||||
.filter(HuyaRegisterBatchModel.batch_id == batch_id)
|
||||
.first()
|
||||
)
|
||||
if not batch_row:
|
||||
return None
|
||||
item_rows = (
|
||||
db.query(HuyaRegisterItemModel)
|
||||
.filter(HuyaRegisterItemModel.batch_id == batch_id)
|
||||
.order_by(HuyaRegisterItemModel.line.asc())
|
||||
.all()
|
||||
)
|
||||
# 无内存 runner 时,running/pending 都视为中断(避免「准备续跑」永久卡住)
|
||||
if recover_interrupted and batch_row.status in {"running", "pending"}:
|
||||
now = _now()
|
||||
batch_row.status = "interrupted"
|
||||
batch_row.message = "服务中断或未真正启动,可从失败/未完成项续跑"
|
||||
batch_row.finished_at = now
|
||||
for item in item_rows:
|
||||
if item.status in RUNNING_ITEM_STATUSES:
|
||||
item.status = "error"
|
||||
item.message = "服务中断,可续跑"
|
||||
item.finished_at = now
|
||||
elif item.status == "pending":
|
||||
# 保留 pending 供续跑,仅更新提示
|
||||
item.message = item.message or "等待续跑"
|
||||
_refresh_batch_counts(batch_row, item_rows)
|
||||
db.commit()
|
||||
db.refresh(batch_row)
|
||||
item_rows = (
|
||||
db.query(HuyaRegisterItemModel)
|
||||
.filter(HuyaRegisterItemModel.batch_id == batch_id)
|
||||
.order_by(HuyaRegisterItemModel.line.asc())
|
||||
.all()
|
||||
)
|
||||
batch = _batch_from_db(batch_row, item_rows)
|
||||
return snapshot_from_batch(batch)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def list_batch_summaries(limit: int = 50, live_batch_ids: set[str] | None = None) -> list[dict]:
|
||||
"""列出最近的注册批次摘要。live_batch_ids 中的 running 保持运行中。"""
|
||||
live = live_batch_ids or set()
|
||||
db = SessionLocal()
|
||||
try:
|
||||
rows = (
|
||||
db.query(HuyaRegisterBatchModel)
|
||||
.order_by(HuyaRegisterBatchModel.id.desc())
|
||||
.limit(max(1, min(int(limit or 50), 200)))
|
||||
.all()
|
||||
)
|
||||
result = []
|
||||
for row in rows:
|
||||
status = row.status or "pending"
|
||||
message = row.message or ""
|
||||
# 无内存 runner 且状态仍是 running/pending,展示为 interrupted
|
||||
if status in {"running", "pending"} and row.batch_id not in live:
|
||||
status = "interrupted"
|
||||
message = message or "服务中断或未真正启动,可从失败/未完成项续跑"
|
||||
running_count = 0
|
||||
if status == "running":
|
||||
running_count = max(
|
||||
0,
|
||||
int(row.total or 0) - int(row.success_count or 0) - int(row.failed_count or 0) - int(row.stopped_count or 0),
|
||||
)
|
||||
result.append({
|
||||
"batch_id": row.batch_id,
|
||||
"status": status,
|
||||
"message": message,
|
||||
"tag": row.tag or "",
|
||||
"created_by": row.created_by,
|
||||
"concurrency": int(row.concurrency or 1),
|
||||
"wait_seconds": float(row.wait_seconds or 180),
|
||||
"poll_interval": float(row.poll_interval or 5),
|
||||
"password_prefix": row.password_prefix or "hy",
|
||||
"use_proxy": bool(row.use_proxy),
|
||||
"total": int(row.total or 0),
|
||||
"success_count": int(row.success_count or 0),
|
||||
"failed_count": int(row.failed_count or 0),
|
||||
"stopped_count": int(row.stopped_count or 0),
|
||||
"running_count": running_count,
|
||||
"created_at": row.created_at,
|
||||
"started_at": row.started_at,
|
||||
"finished_at": row.finished_at,
|
||||
"items": [],
|
||||
})
|
||||
return result
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def _refresh_batch_counts(batch_row: HuyaRegisterBatchModel, items: list[HuyaRegisterItemModel]):
|
||||
batch_row.total = len(items)
|
||||
batch_row.success_count = sum(1 for item in items if item.status == "success")
|
||||
batch_row.failed_count = sum(1 for item in items if item.status == "error")
|
||||
batch_row.stopped_count = sum(1 for item in items if item.status == "stopped")
|
||||
|
||||
|
||||
def format_success_export_line(username: str, uid: str, password: str, phone: str, sms_url: str) -> str:
|
||||
"""统一成功导出格式。"""
|
||||
account = (username or uid or "").strip()
|
||||
return f"{account}----{password or ''}----{phone or ''}----{sms_url or ''}"
|
||||
|
||||
|
||||
def export_success_logs_text(
|
||||
*,
|
||||
batch_id: str | None = None,
|
||||
tag: str | None = None,
|
||||
limit: int = 5000,
|
||||
) -> str:
|
||||
"""从成功流水表导出 txt。"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
query = db.query(HuyaRegisterSuccessLog).order_by(HuyaRegisterSuccessLog.id.asc())
|
||||
if batch_id:
|
||||
query = query.filter(HuyaRegisterSuccessLog.batch_id == batch_id)
|
||||
if tag:
|
||||
query = query.filter(HuyaRegisterSuccessLog.tag == tag)
|
||||
rows = query.limit(max(1, min(int(limit or 5000), 20000))).all()
|
||||
lines = [
|
||||
format_success_export_line(
|
||||
row.username or "",
|
||||
row.uid or "",
|
||||
row.password or "",
|
||||
row.phone or "",
|
||||
row.sms_url or "",
|
||||
)
|
||||
for row in rows
|
||||
if (row.username or row.uid) and row.password
|
||||
]
|
||||
return "\n".join(lines)
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
def list_success_logs(
|
||||
*,
|
||||
batch_id: str | None = None,
|
||||
tag: str | None = None,
|
||||
limit: int = 200,
|
||||
) -> list[dict]:
|
||||
"""列出成功流水(含密码,供管理端展示/导出)。"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
query = db.query(HuyaRegisterSuccessLog).order_by(HuyaRegisterSuccessLog.id.desc())
|
||||
if batch_id:
|
||||
query = query.filter(HuyaRegisterSuccessLog.batch_id == batch_id)
|
||||
if tag:
|
||||
query = query.filter(HuyaRegisterSuccessLog.tag == tag)
|
||||
rows = query.limit(max(1, min(int(limit or 200), 2000))).all()
|
||||
return [
|
||||
{
|
||||
"id": row.id,
|
||||
"batch_id": row.batch_id or "",
|
||||
"item_id": row.item_id,
|
||||
"account_id": row.account_id,
|
||||
"phone": row.phone or "",
|
||||
"username": row.username or "",
|
||||
"uid": row.uid or "",
|
||||
"password": row.password or "",
|
||||
"sms_url": row.sms_url or "",
|
||||
"tag": row.tag or "",
|
||||
"provider": row.provider or "",
|
||||
"created_by": row.created_by,
|
||||
"created_at": row.created_at,
|
||||
"export_line": format_success_export_line(
|
||||
row.username or "",
|
||||
row.uid or "",
|
||||
row.password or "",
|
||||
row.phone or "",
|
||||
row.sms_url or "",
|
||||
),
|
||||
}
|
||||
for row in rows
|
||||
]
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
class HuyaRegisterRunner:
|
||||
@@ -107,10 +388,13 @@ class HuyaRegisterRunner:
|
||||
batch: HuyaRegisterBatch,
|
||||
sms_lines: list[SmsLine],
|
||||
proxy_config: Optional[ProxyConfigModel] = None,
|
||||
*,
|
||||
item_indices: list[int] | None = None,
|
||||
):
|
||||
self.batch = batch
|
||||
self.sms_lines = sms_lines
|
||||
self.proxy_config = proxy_config
|
||||
self.item_indices = item_indices # None 表示跑全部
|
||||
self._lock = threading.Lock()
|
||||
self._stop = threading.Event()
|
||||
self._shared_proxy_fetcher = self._create_proxy_fetcher()
|
||||
@@ -145,54 +429,123 @@ class HuyaRegisterRunner:
|
||||
with self._lock:
|
||||
if self.batch.status == "running":
|
||||
self.batch.message = "正在停止"
|
||||
self._persist_batch_meta()
|
||||
|
||||
def snapshot(self) -> dict:
|
||||
with self._lock:
|
||||
total = len(self.batch.items)
|
||||
return snapshot_from_batch(self.batch)
|
||||
|
||||
def _persist_batch_meta(self):
|
||||
"""把批次汇总写回数据库。"""
|
||||
if not self.batch.db_id:
|
||||
return
|
||||
db = SessionLocal()
|
||||
try:
|
||||
row = db.query(HuyaRegisterBatchModel).filter(HuyaRegisterBatchModel.id == self.batch.db_id).first()
|
||||
if not row:
|
||||
return
|
||||
row.status = self.batch.status
|
||||
row.message = self.batch.message
|
||||
row.started_at = self.batch.started_at
|
||||
row.finished_at = self.batch.finished_at
|
||||
row.concurrency = self.batch.concurrency
|
||||
row.wait_seconds = int(self.batch.wait_seconds)
|
||||
row.poll_interval = int(self.batch.poll_interval)
|
||||
row.password_prefix = self.batch.password_prefix
|
||||
row.fixed_password = self.batch.fixed_password
|
||||
row.use_proxy = self.batch.use_proxy
|
||||
row.tag = self.batch.tag
|
||||
items = (
|
||||
db.query(HuyaRegisterItemModel)
|
||||
.filter(HuyaRegisterItemModel.batch_id == self.batch.batch_id)
|
||||
.all()
|
||||
)
|
||||
_refresh_batch_counts(row, items)
|
||||
# 同步内存计数到 batch 对象侧的 status 字段已足够;counts 以 DB items 为准
|
||||
success = sum(1 for item in self.batch.items if item.status == "success")
|
||||
failed = sum(1 for item in self.batch.items if item.status == "error")
|
||||
stopped = sum(1 for item in self.batch.items if item.status == "stopped")
|
||||
running = sum(1 for item in self.batch.items if item.status in {"sending", "waiting", "changing", "logging"})
|
||||
return {
|
||||
"batch_id": self.batch.batch_id,
|
||||
"status": self.batch.status,
|
||||
"message": self.batch.message,
|
||||
"tag": self.batch.tag,
|
||||
"created_by": self.batch.created_by,
|
||||
"concurrency": self.batch.concurrency,
|
||||
"wait_seconds": self.batch.wait_seconds,
|
||||
"poll_interval": self.batch.poll_interval,
|
||||
"password_prefix": self.batch.password_prefix,
|
||||
"use_proxy": self.batch.use_proxy,
|
||||
"total": total,
|
||||
"success_count": success,
|
||||
"failed_count": failed,
|
||||
"stopped_count": stopped,
|
||||
"running_count": running,
|
||||
"created_at": self.batch.created_at,
|
||||
"started_at": self.batch.started_at,
|
||||
"finished_at": self.batch.finished_at,
|
||||
"items": [item.to_dict() for item in self.batch.items],
|
||||
}
|
||||
row.success_count = success
|
||||
row.failed_count = failed
|
||||
row.stopped_count = stopped
|
||||
row.total = len(self.batch.items)
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def _persist_item(self, index: int):
|
||||
"""把单条 item 状态写回数据库。"""
|
||||
item = self.batch.items[index]
|
||||
if not item.db_id:
|
||||
return
|
||||
db = SessionLocal()
|
||||
try:
|
||||
row = db.query(HuyaRegisterItemModel).filter(HuyaRegisterItemModel.id == item.db_id).first()
|
||||
if not row:
|
||||
return
|
||||
row.status = item.status
|
||||
row.message = (item.message or "")[:512]
|
||||
row.code = item.code or ""
|
||||
row.change_code = item.change_code or ""
|
||||
row.attempts = item.attempts
|
||||
row.change_attempts = item.change_attempts
|
||||
row.account_id = item.account_id
|
||||
row.username = item.username or ""
|
||||
row.uid = item.uid or ""
|
||||
row.password = item.password or ""
|
||||
row.password_changed = bool(item.password_changed)
|
||||
# DB 存完整 cookie;内存暴露受改密控制
|
||||
if item.cookie:
|
||||
row.cookie = normalize_huya_cookie(item.cookie)
|
||||
elif item.password_changed and item.status == "success":
|
||||
# 改密成功时 cookie 可能被前端隐藏,保留 DB 已有值
|
||||
pass
|
||||
row.started_at = item.started_at
|
||||
row.finished_at = item.finished_at
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def _set_item(self, index: int, **updates):
|
||||
with self._lock:
|
||||
item = self.batch.items[index]
|
||||
for key, value in updates.items():
|
||||
setattr(item, key, value)
|
||||
self._persist_item(index)
|
||||
|
||||
def _save_cookie(self, result: HuyaAutoRegisterResult) -> tuple[int | None, str, str]:
|
||||
def _save_success(self, index: int, result: HuyaAutoRegisterResult) -> tuple[int | None, str, str]:
|
||||
"""成功时:写账号 + 成功流水(成功一个写一条,立即可导出)。"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
# upsert_huya_cookie 内部会 commit 一次
|
||||
account = upsert_huya_cookie(db, result.cookie, tag=self.batch.tag, username_hint="")
|
||||
account.game_phone = result.phone
|
||||
account.game_phone = result.phone or account.game_phone or ""
|
||||
if result.username:
|
||||
account.username = result.username
|
||||
if result.password:
|
||||
account.account_password = result.password
|
||||
sms_url = result.sms_url or self.batch.items[index].sms_url or ""
|
||||
if sms_url:
|
||||
account.sms_url = sms_url
|
||||
if result.password_changed:
|
||||
account.status = "password_changed"
|
||||
account.updated_at = _now()
|
||||
|
||||
item = self.batch.items[index]
|
||||
db.add(HuyaRegisterSuccessLog(
|
||||
batch_id=self.batch.batch_id,
|
||||
item_id=item.db_id,
|
||||
account_id=account.id,
|
||||
phone=result.phone or item.phone,
|
||||
username=result.username or account.username or "",
|
||||
uid=result.uid or account.uid or account.yyuid or "",
|
||||
password=result.password or "",
|
||||
sms_url=sms_url,
|
||||
tag=self.batch.tag,
|
||||
provider=result.provider or item.provider,
|
||||
created_by=self.batch.created_by,
|
||||
created_at=_now(),
|
||||
))
|
||||
db.commit()
|
||||
db.refresh(account)
|
||||
return account.id, account.username or "", account.uid or account.yyuid or ""
|
||||
@@ -228,7 +581,24 @@ class HuyaRegisterRunner:
|
||||
self._set_item(index, status="error", message=proxy_error, finished_at=_now())
|
||||
return
|
||||
|
||||
self._set_item(index, status="sending", message="注册并改密", started_at=_now(), finished_at=None)
|
||||
self._set_item(
|
||||
index,
|
||||
status="sending",
|
||||
message="注册并改密",
|
||||
started_at=_now(),
|
||||
finished_at=None,
|
||||
code="",
|
||||
change_code="",
|
||||
attempts=0,
|
||||
change_attempts=0,
|
||||
account_id=None,
|
||||
username="",
|
||||
uid="",
|
||||
password="",
|
||||
password_changed=False,
|
||||
cookie="",
|
||||
cookie_preview="",
|
||||
)
|
||||
result = register_huya_with_sms_line(
|
||||
item,
|
||||
wait_seconds=self.batch.wait_seconds,
|
||||
@@ -245,6 +615,7 @@ class HuyaRegisterRunner:
|
||||
message = result.message
|
||||
status = result.status
|
||||
cookie = result.cookie if result.success else ""
|
||||
full_cookie = cookie
|
||||
if result.success:
|
||||
self._set_item(
|
||||
index,
|
||||
@@ -260,38 +631,71 @@ class HuyaRegisterRunner:
|
||||
password_changed=result.password_changed,
|
||||
)
|
||||
try:
|
||||
account_id, username, uid = self._save_cookie(result)
|
||||
account_id, username, uid = self._save_success(index, result)
|
||||
except Exception as exc:
|
||||
status = "error"
|
||||
cookie = ""
|
||||
full_cookie = ""
|
||||
message = f"账号保存失败: {exc}"
|
||||
|
||||
exposed_cookie = "" if result.password_changed else cookie
|
||||
|
||||
self._set_item(
|
||||
index,
|
||||
status=status,
|
||||
message=message,
|
||||
code=result.code,
|
||||
change_code=result.change_code,
|
||||
attempts=result.attempts,
|
||||
change_attempts=result.change_attempts,
|
||||
account_id=account_id,
|
||||
username=username,
|
||||
uid=uid,
|
||||
password=result.password,
|
||||
password_changed=result.password_changed,
|
||||
cookie=normalize_huya_cookie(exposed_cookie),
|
||||
cookie_preview=_cookie_preview(exposed_cookie),
|
||||
finished_at=_now(),
|
||||
)
|
||||
# 先把完整 cookie 写内存供 persist,再把暴露值用于展示
|
||||
with self._lock:
|
||||
mem = self.batch.items[index]
|
||||
mem.status = status
|
||||
mem.message = message
|
||||
mem.code = result.code
|
||||
mem.change_code = result.change_code
|
||||
mem.attempts = result.attempts
|
||||
mem.change_attempts = result.change_attempts
|
||||
mem.account_id = account_id
|
||||
mem.username = username
|
||||
mem.uid = uid
|
||||
mem.password = result.password
|
||||
mem.password_changed = result.password_changed
|
||||
mem.cookie = normalize_huya_cookie(full_cookie)
|
||||
mem.cookie_preview = _cookie_preview(exposed_cookie)
|
||||
mem.finished_at = _now()
|
||||
# 展示用 cookie 在 to_dict 时再处理:成功且改密则隐藏
|
||||
if result.password_changed and status == "success":
|
||||
# to_dict 使用 mem.cookie;这里保持 DB 有完整 cookie,API 隐藏
|
||||
pass
|
||||
|
||||
self._persist_item(index)
|
||||
|
||||
# API 快照中改密成功不暴露 cookie
|
||||
with self._lock:
|
||||
mem = self.batch.items[index]
|
||||
if result.password_changed and status == "success":
|
||||
mem.cookie = ""
|
||||
mem.cookie_preview = ""
|
||||
else:
|
||||
mem.cookie = normalize_huya_cookie(exposed_cookie)
|
||||
mem.cookie_preview = _cookie_preview(exposed_cookie)
|
||||
|
||||
def mark_running(self, message: str = "批次运行中"):
|
||||
"""在启动线程前立刻标记 running,避免前端看到 pending 误判/卡住。"""
|
||||
with self._lock:
|
||||
self.batch.status = "running"
|
||||
self.batch.message = message
|
||||
self.batch.started_at = self.batch.started_at or _now()
|
||||
self.batch.finished_at = None
|
||||
self._persist_batch_meta()
|
||||
|
||||
def run(self):
|
||||
"""线程入口。"""
|
||||
with self._lock:
|
||||
if self.batch.status != "running":
|
||||
self.batch.status = "running"
|
||||
self.batch.message = "批次运行中"
|
||||
self.batch.started_at = _now()
|
||||
self.batch.started_at = self.batch.started_at or _now()
|
||||
self.batch.finished_at = None
|
||||
self._persist_batch_meta()
|
||||
|
||||
indices = self.item_indices
|
||||
if indices is None:
|
||||
indices = list(range(len(self.sms_lines)))
|
||||
|
||||
try:
|
||||
if self._shared_proxy_fetcher:
|
||||
@@ -299,13 +703,15 @@ class HuyaRegisterRunner:
|
||||
if not ok:
|
||||
with self._lock:
|
||||
self.batch.message = f"代理白名单预热失败: {msg}"
|
||||
self._persist_batch_meta()
|
||||
|
||||
with ThreadPoolExecutor(max_workers=self.batch.concurrency) as executor:
|
||||
futures = []
|
||||
for index, item in enumerate(self.sms_lines):
|
||||
for index in indices:
|
||||
if self._stop.is_set():
|
||||
self._set_item(index, status="stopped", message="已停止", finished_at=_now())
|
||||
continue
|
||||
item = self.sms_lines[index]
|
||||
futures.append(executor.submit(self._run_one, index, item))
|
||||
|
||||
for future in as_completed(futures):
|
||||
@@ -315,6 +721,7 @@ class HuyaRegisterRunner:
|
||||
self.batch.status = "error"
|
||||
self.batch.message = f"批次执行异常: {exc}"
|
||||
self.batch.finished_at = _now()
|
||||
self._persist_batch_meta()
|
||||
return
|
||||
|
||||
with self._lock:
|
||||
@@ -325,10 +732,11 @@ class HuyaRegisterRunner:
|
||||
self.batch.status = "finished"
|
||||
self.batch.message = "批次已完成"
|
||||
self.batch.finished_at = _now()
|
||||
self._persist_batch_meta()
|
||||
|
||||
|
||||
class HuyaRegisterRegistry:
|
||||
"""管理自动注册批次。"""
|
||||
"""管理自动注册批次(内存 runner + DB 持久化)。"""
|
||||
|
||||
def __init__(self):
|
||||
self._lock = threading.Lock()
|
||||
@@ -348,29 +756,229 @@ class HuyaRegisterRegistry:
|
||||
proxy_config: Optional[ProxyConfigModel] = None,
|
||||
) -> HuyaRegisterRunner:
|
||||
batch_id = uuid.uuid4().hex[:12]
|
||||
concurrency = max(1, min(int(concurrency or 1), 5))
|
||||
wait_seconds = max(15.0, float(wait_seconds or 180))
|
||||
poll_interval = max(1.0, float(poll_interval or 5))
|
||||
password_prefix = (password_prefix or "hy").strip()[:8] or "hy"
|
||||
fixed_password = (fixed_password or "").strip()
|
||||
tag = (tag or "").strip()
|
||||
|
||||
items = [
|
||||
HuyaRegisterItemState(
|
||||
line=index + 1,
|
||||
phone=item.phone,
|
||||
provider=item.provider,
|
||||
sms_url=item.url,
|
||||
message="等待开始",
|
||||
)
|
||||
for index, item in enumerate(sms_lines)
|
||||
]
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
batch_row = HuyaRegisterBatchModel(
|
||||
batch_id=batch_id,
|
||||
tag=tag,
|
||||
created_by=created_by,
|
||||
concurrency=concurrency,
|
||||
wait_seconds=int(wait_seconds),
|
||||
poll_interval=int(poll_interval),
|
||||
password_prefix=password_prefix,
|
||||
fixed_password=fixed_password,
|
||||
use_proxy=bool(use_proxy),
|
||||
status="pending",
|
||||
message="等待开始",
|
||||
total=len(items),
|
||||
success_count=0,
|
||||
failed_count=0,
|
||||
stopped_count=0,
|
||||
created_at=_now(),
|
||||
)
|
||||
db.add(batch_row)
|
||||
db.flush()
|
||||
for item in items:
|
||||
row = HuyaRegisterItemModel(
|
||||
batch_db_id=batch_row.id,
|
||||
batch_id=batch_id,
|
||||
line=item.line,
|
||||
phone=item.phone,
|
||||
provider=item.provider,
|
||||
sms_url=item.sms_url,
|
||||
status="pending",
|
||||
message="等待开始",
|
||||
password="",
|
||||
cookie="",
|
||||
)
|
||||
db.add(row)
|
||||
db.flush()
|
||||
item.db_id = row.id
|
||||
db.commit()
|
||||
db_id = batch_row.id
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
batch = HuyaRegisterBatch(
|
||||
batch_id=batch_id,
|
||||
tag=tag,
|
||||
created_by=created_by,
|
||||
concurrency=max(1, min(int(concurrency or 1), 5)),
|
||||
wait_seconds=max(15.0, float(wait_seconds or 180)),
|
||||
poll_interval=max(1.0, float(poll_interval or 5)),
|
||||
password_prefix=(password_prefix or "hy").strip()[:8] or "hy",
|
||||
fixed_password=(fixed_password or "").strip(),
|
||||
concurrency=concurrency,
|
||||
wait_seconds=wait_seconds,
|
||||
poll_interval=poll_interval,
|
||||
password_prefix=password_prefix,
|
||||
fixed_password=fixed_password,
|
||||
use_proxy=bool(use_proxy),
|
||||
items=[
|
||||
HuyaRegisterItemState(line=index + 1, phone=item.phone, provider=item.provider, sms_url=item.url)
|
||||
for index, item in enumerate(sms_lines)
|
||||
],
|
||||
items=items,
|
||||
db_id=db_id,
|
||||
status="running",
|
||||
message="批次运行中",
|
||||
started_at=_now(),
|
||||
)
|
||||
# DB 同步为 running,避免返回 pending 导致前端误判
|
||||
db = SessionLocal()
|
||||
try:
|
||||
row = db.query(HuyaRegisterBatchModel).filter(HuyaRegisterBatchModel.id == db_id).first()
|
||||
if row:
|
||||
row.status = "running"
|
||||
row.message = "批次运行中"
|
||||
row.started_at = batch.started_at
|
||||
db.commit()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
runner = HuyaRegisterRunner(batch=batch, sms_lines=sms_lines, proxy_config=proxy_config)
|
||||
with self._lock:
|
||||
self._runners[batch_id] = runner
|
||||
return runner
|
||||
|
||||
def retry(
|
||||
self,
|
||||
batch_id: str,
|
||||
proxy_config: Optional[ProxyConfigModel] = None,
|
||||
*,
|
||||
concurrency: int | None = None,
|
||||
wait_seconds: float | None = None,
|
||||
poll_interval: float | None = None,
|
||||
password_prefix: str | None = None,
|
||||
fixed_password: str | None = None,
|
||||
use_proxy: bool | None = None,
|
||||
) -> HuyaRegisterRunner:
|
||||
"""从失败/停止/未完成的条目续跑;成功项跳过。"""
|
||||
with self._lock:
|
||||
existing = self._runners.get(batch_id)
|
||||
if existing and existing.batch.status == "running":
|
||||
raise RuntimeError("批次正在运行中,请先停止或等待完成")
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
batch_row = (
|
||||
db.query(HuyaRegisterBatchModel)
|
||||
.filter(HuyaRegisterBatchModel.batch_id == batch_id)
|
||||
.first()
|
||||
)
|
||||
if not batch_row:
|
||||
raise ValueError("批次不存在")
|
||||
if batch_row.status == "running":
|
||||
# 无内存 runner 的 running 视为中断,允许续跑
|
||||
batch_row.status = "interrupted"
|
||||
batch_row.message = "服务中断,准备续跑"
|
||||
db.commit()
|
||||
|
||||
item_rows = (
|
||||
db.query(HuyaRegisterItemModel)
|
||||
.filter(HuyaRegisterItemModel.batch_id == batch_id)
|
||||
.order_by(HuyaRegisterItemModel.line.asc())
|
||||
.all()
|
||||
)
|
||||
if not item_rows:
|
||||
raise ValueError("批次没有可运行条目")
|
||||
|
||||
# 可选覆盖运行参数
|
||||
if concurrency is not None:
|
||||
batch_row.concurrency = max(1, min(int(concurrency), 5))
|
||||
if wait_seconds is not None:
|
||||
batch_row.wait_seconds = int(max(15.0, float(wait_seconds)))
|
||||
if poll_interval is not None:
|
||||
batch_row.poll_interval = int(max(1.0, float(poll_interval)))
|
||||
if password_prefix is not None:
|
||||
batch_row.password_prefix = (password_prefix or "hy").strip()[:8] or "hy"
|
||||
if fixed_password is not None:
|
||||
batch_row.fixed_password = (fixed_password or "").strip()
|
||||
if use_proxy is not None:
|
||||
batch_row.use_proxy = bool(use_proxy)
|
||||
|
||||
retry_indices: list[int] = []
|
||||
for idx, row in enumerate(item_rows):
|
||||
if (row.status or "pending") in RETRYABLE_ITEM_STATUSES and row.status != "success":
|
||||
row.status = "pending"
|
||||
row.message = "等待续跑"
|
||||
row.finished_at = None
|
||||
row.started_at = None
|
||||
row.code = ""
|
||||
row.change_code = ""
|
||||
row.attempts = 0
|
||||
row.change_attempts = 0
|
||||
# 保留历史密码等成功字段不覆盖;失败项本来也没有
|
||||
retry_indices.append(idx)
|
||||
|
||||
if not retry_indices:
|
||||
raise ValueError("没有可续跑的失败/未完成条目")
|
||||
|
||||
batch_row.status = "running"
|
||||
batch_row.message = f"续跑中({len(retry_indices)} 条)"
|
||||
batch_row.started_at = batch_row.started_at or _now()
|
||||
batch_row.finished_at = None
|
||||
_refresh_batch_counts(batch_row, item_rows)
|
||||
db.commit()
|
||||
|
||||
batch = _batch_from_db(batch_row, item_rows)
|
||||
sms_lines = [
|
||||
SmsLine(phone=item.phone, url=item.sms_url, provider=item.provider, raw=f"{item.phone}----{item.sms_url}")
|
||||
for item in batch.items
|
||||
]
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
if use_proxy is False:
|
||||
proxy_config = None
|
||||
elif batch.use_proxy and proxy_config is None:
|
||||
# 由调用方传入;若未传则 runner 内会报代理未配置
|
||||
pass
|
||||
|
||||
runner = HuyaRegisterRunner(
|
||||
batch=batch,
|
||||
sms_lines=sms_lines,
|
||||
proxy_config=proxy_config if batch.use_proxy else None,
|
||||
item_indices=retry_indices,
|
||||
)
|
||||
# 内存侧也保持 running,与 DB 一致
|
||||
runner.batch.status = "running"
|
||||
runner.batch.message = f"续跑中({len(retry_indices)} 条)"
|
||||
with self._lock:
|
||||
self._runners[batch_id] = runner
|
||||
return runner
|
||||
|
||||
def get(self, batch_id: str) -> HuyaRegisterRunner | None:
|
||||
with self._lock:
|
||||
return self._runners.get(batch_id)
|
||||
|
||||
def live_batch_ids(self) -> set[str]:
|
||||
with self._lock:
|
||||
return {
|
||||
batch_id
|
||||
for batch_id, runner in self._runners.items()
|
||||
if runner.batch.status == "running"
|
||||
}
|
||||
|
||||
def list_summaries(self, limit: int = 50) -> list[dict]:
|
||||
return list_batch_summaries(limit=limit, live_batch_ids=self.live_batch_ids())
|
||||
|
||||
def get_snapshot(self, batch_id: str) -> dict | None:
|
||||
"""优先内存 runner,否则读库(并处理中断恢复)。"""
|
||||
with self._lock:
|
||||
runner = self._runners.get(batch_id)
|
||||
if runner:
|
||||
return runner.snapshot()
|
||||
return load_batch_snapshot(batch_id, recover_interrupted=True)
|
||||
|
||||
|
||||
huya_register_registry = HuyaRegisterRegistry()
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
HuyaAccountItem,
|
||||
HuyaAutoRegisterBatch,
|
||||
HuyaAutoRegisterRequest,
|
||||
HuyaAutoRegisterRetryRequest,
|
||||
HuyaConfig,
|
||||
HuyaCookieItem,
|
||||
HuyaCookieImportResult,
|
||||
@@ -13,6 +14,7 @@ import type {
|
||||
HuyaPasswordLoginResult,
|
||||
HuyaPasswordLoginSelectedRequest,
|
||||
HuyaRechargeGoodsItem,
|
||||
HuyaRegisterSuccessLog,
|
||||
HuyaSmsCodeRequest,
|
||||
HuyaSmsCodeResult,
|
||||
HuyaSmsLoginRequest,
|
||||
@@ -43,10 +45,20 @@ export const huyaApi = {
|
||||
api.post<HuyaSmsLoginResult, HuyaSmsLoginResult>('/huya/accounts/sms-login', data, { timeout: 120000 }),
|
||||
startAutoRegister: (data: HuyaAutoRegisterRequest) =>
|
||||
api.post<HuyaAutoRegisterBatch, HuyaAutoRegisterBatch>('/huya/register/batches', data),
|
||||
listAutoRegisterBatches: (limit = 50) =>
|
||||
api.get<HuyaAutoRegisterBatch[], HuyaAutoRegisterBatch[]>('/huya/register/batches', { params: { limit } }),
|
||||
getAutoRegisterBatch: (batchId: string) =>
|
||||
api.get<HuyaAutoRegisterBatch, HuyaAutoRegisterBatch>(`/huya/register/batches/${batchId}`),
|
||||
stopAutoRegisterBatch: (batchId: string) =>
|
||||
api.post<MessageResponse, MessageResponse>(`/huya/register/batches/${batchId}/stop`),
|
||||
retryAutoRegisterBatch: (batchId: string, data?: HuyaAutoRegisterRetryRequest) =>
|
||||
api.post<HuyaAutoRegisterBatch, HuyaAutoRegisterBatch>(`/huya/register/batches/${batchId}/retry`, data || {}),
|
||||
exportAutoRegisterBatch: (batchId: string) =>
|
||||
api.get<Blob, Blob>(`/huya/register/batches/${batchId}/export`, { responseType: 'blob' }),
|
||||
listRegisterSuccessLogs: (params?: { batch_id?: string; tag?: string; limit?: number }) =>
|
||||
api.get<HuyaRegisterSuccessLog[], HuyaRegisterSuccessLog[]>('/huya/register/success-logs', { params }),
|
||||
exportRegisterSuccessLogs: (params?: { batch_id?: string; tag?: string; limit?: number }) =>
|
||||
api.get<Blob, Blob>('/huya/register/success-logs/export', { responseType: 'blob', params }),
|
||||
assign: (id: number, assigned_to: number | null) =>
|
||||
api.put<MessageResponse, MessageResponse>(`/huya/accounts/${id}/assign`, { assigned_to }),
|
||||
batchAssign: (account_ids: number[], assigned_to: number | null) =>
|
||||
|
||||
@@ -223,6 +223,15 @@ export interface HuyaAutoRegisterRequest {
|
||||
use_proxy?: boolean;
|
||||
}
|
||||
|
||||
export interface HuyaAutoRegisterRetryRequest {
|
||||
concurrency?: number;
|
||||
wait_seconds?: number;
|
||||
poll_interval?: number;
|
||||
password_prefix?: string;
|
||||
fixed_password?: string;
|
||||
use_proxy?: boolean;
|
||||
}
|
||||
|
||||
export interface HuyaAutoRegisterItem {
|
||||
line: number;
|
||||
phone: string;
|
||||
@@ -267,6 +276,23 @@ export interface HuyaAutoRegisterBatch {
|
||||
items: HuyaAutoRegisterItem[];
|
||||
}
|
||||
|
||||
export interface HuyaRegisterSuccessLog {
|
||||
id: number;
|
||||
batch_id: string;
|
||||
item_id: number | null;
|
||||
account_id: number | null;
|
||||
phone: string;
|
||||
username: string;
|
||||
uid: string;
|
||||
password: string;
|
||||
sms_url: string;
|
||||
tag: string;
|
||||
provider: string;
|
||||
created_by: number | null;
|
||||
created_at: string | null;
|
||||
export_line: string;
|
||||
}
|
||||
|
||||
export interface HuyaPasswordLoginBatchItem {
|
||||
line: number;
|
||||
username: string;
|
||||
|
||||
@@ -3,8 +3,15 @@ import {
|
||||
Button, Card, Col, Input, InputNumber, message, Row, Segmented, Space, Statistic, Switch, Table, Tag, Typography,
|
||||
} from 'antd';
|
||||
import type { TableProps } from 'antd';
|
||||
import { DownloadOutlined, PlayCircleOutlined, ReloadOutlined, StopOutlined } from '@ant-design/icons';
|
||||
import { huyaApi, type HuyaAutoRegisterBatch, type HuyaAutoRegisterItem } from '../api/modules';
|
||||
import {
|
||||
DownloadOutlined, PlayCircleOutlined, RedoOutlined, ReloadOutlined, StopOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import {
|
||||
huyaApi,
|
||||
type HuyaAutoRegisterBatch,
|
||||
type HuyaAutoRegisterItem,
|
||||
type HuyaRegisterSuccessLog,
|
||||
} from '../api/modules';
|
||||
import { formatTime } from '../utils/time';
|
||||
import { getErrorMessage } from '../utils/error';
|
||||
|
||||
@@ -48,6 +55,9 @@ const STATUS_LABELS: Record<string, string> = {
|
||||
success: '成功',
|
||||
error: '失败',
|
||||
stopped: '已停止',
|
||||
interrupted: '已中断',
|
||||
finished: '已完成',
|
||||
running: '运行中',
|
||||
};
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
@@ -59,9 +69,14 @@ const STATUS_COLORS: Record<string, string> = {
|
||||
success: 'success',
|
||||
error: 'error',
|
||||
stopped: 'warning',
|
||||
interrupted: 'warning',
|
||||
finished: 'success',
|
||||
running: 'processing',
|
||||
};
|
||||
|
||||
const RUNNING_STATUS = new Set(['pending', 'running']);
|
||||
// 只有真正 running 才锁 UI / 轮询;pending 可能是历史脏数据,不能当运行中
|
||||
const RUNNING_STATUS = new Set(['running']);
|
||||
const RETRYABLE_BATCH = new Set(['finished', 'stopped', 'error', 'interrupted', 'pending']);
|
||||
|
||||
function safeNumber(value: unknown, fallback: number) {
|
||||
const n = Number(value);
|
||||
@@ -96,6 +111,15 @@ function readStoredBatchId() {
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadBlob(blob: Blob, filename: string) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = filename;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
export default function HuyaRegisterPage() {
|
||||
const [initialForm] = useState(readStoredForm);
|
||||
const [text, setText] = useState(initialForm.text);
|
||||
@@ -108,13 +132,26 @@ export default function HuyaRegisterPage() {
|
||||
const [fixedPassword, setFixedPassword] = useState(initialForm.fixedPassword);
|
||||
const [useProxy, setUseProxy] = useState(initialForm.useProxy);
|
||||
const [batch, setBatch] = useState<HuyaAutoRegisterBatch | null>(null);
|
||||
const [history, setHistory] = useState<HuyaAutoRegisterBatch[]>([]);
|
||||
const [successLogs, setSuccessLogs] = useState<HuyaRegisterSuccessLog[]>([]);
|
||||
const [starting, setStarting] = useState(false);
|
||||
const [retrying, setRetrying] = useState(false);
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [stopping, setStopping] = useState(false);
|
||||
const [historyLoading, setHistoryLoading] = useState(false);
|
||||
const [logsLoading, setLogsLoading] = useState(false);
|
||||
const [exporting, setExporting] = useState(false);
|
||||
const restoredBatchRef = useRef(false);
|
||||
|
||||
const batchId = batch?.batch_id || '';
|
||||
const isRunning = !!batch && RUNNING_STATUS.has(batch.status);
|
||||
const isRunning = !!batch && batch.status === 'running';
|
||||
const hasUnfinished = !!batch && (
|
||||
(batch.failed_count || 0) + (batch.stopped_count || 0) > 0
|
||||
|| (batch.items || []).some((item) => item.status !== 'success')
|
||||
|| ((batch.success_count || 0) < (batch.total || 0))
|
||||
);
|
||||
const canRetry = !!batch && !isRunning && hasUnfinished
|
||||
&& (RETRYABLE_BATCH.has(batch.status) || batch.status === 'pending');
|
||||
|
||||
const loadBatchById = useCallback(async (id: string) => {
|
||||
if (!id) return;
|
||||
@@ -125,10 +162,10 @@ export default function HuyaRegisterPage() {
|
||||
localStorage.setItem(BATCH_STORAGE_KEY, data.batch_id);
|
||||
} catch (e: unknown) {
|
||||
const err = getErrorMessage(e);
|
||||
if (err.includes('批次不存在') || err.includes('服务已重启')) {
|
||||
if (err.includes('批次不存在')) {
|
||||
localStorage.removeItem(BATCH_STORAGE_KEY);
|
||||
setBatch(null);
|
||||
message.warning('上次自动注册批次已不存在,已清除恢复记录');
|
||||
message.warning('批次不存在,已清除本地恢复记录');
|
||||
} else {
|
||||
message.error(err);
|
||||
}
|
||||
@@ -142,6 +179,30 @@ export default function HuyaRegisterPage() {
|
||||
await loadBatchById(batchId);
|
||||
}, [batchId, loadBatchById]);
|
||||
|
||||
const loadHistory = useCallback(async () => {
|
||||
setHistoryLoading(true);
|
||||
try {
|
||||
const rows = await huyaApi.listAutoRegisterBatches(50);
|
||||
setHistory(rows);
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setHistoryLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const loadSuccessLogs = useCallback(async () => {
|
||||
setLogsLoading(true);
|
||||
try {
|
||||
const rows = await huyaApi.listRegisterSuccessLogs({ limit: 100 });
|
||||
setSuccessLogs(rows);
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setLogsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(FORM_STORAGE_KEY, JSON.stringify({
|
||||
text,
|
||||
@@ -163,7 +224,9 @@ export default function HuyaRegisterPage() {
|
||||
if (storedBatchId) {
|
||||
loadBatchById(storedBatchId);
|
||||
}
|
||||
}, [loadBatchById]);
|
||||
loadHistory();
|
||||
loadSuccessLogs();
|
||||
}, [loadBatchById, loadHistory, loadSuccessLogs]);
|
||||
|
||||
useEffect(() => {
|
||||
if (batch?.batch_id) {
|
||||
@@ -179,11 +242,28 @@ export default function HuyaRegisterPage() {
|
||||
return () => window.clearInterval(timer);
|
||||
}, [batchId, isRunning, refreshBatch]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isRunning) return undefined;
|
||||
// 批次结束时刷新历史与成功流水
|
||||
if (batch && (batch.status === 'finished' || batch.status === 'stopped' || batch.status === 'error')) {
|
||||
loadHistory();
|
||||
loadSuccessLogs();
|
||||
}
|
||||
return undefined;
|
||||
}, [batch?.status, isRunning, loadHistory, loadSuccessLogs, batch]);
|
||||
|
||||
const successRows = useMemo(
|
||||
() => (batch?.items || []).filter((item) => item.status === 'success' && item.password && (item.username || item.uid)),
|
||||
[batch],
|
||||
);
|
||||
|
||||
const retryableCount = useMemo(() => {
|
||||
if (!batch?.items?.length) {
|
||||
return (batch?.failed_count || 0) + (batch?.stopped_count || 0);
|
||||
}
|
||||
return batch.items.filter((item) => item.status !== 'success').length;
|
||||
}, [batch]);
|
||||
|
||||
const handleStart = async () => {
|
||||
if (!text.trim()) {
|
||||
message.warning('请先粘贴手机号池');
|
||||
@@ -207,6 +287,7 @@ export default function HuyaRegisterPage() {
|
||||
});
|
||||
setBatch(data);
|
||||
message.success('自动注册批次已启动');
|
||||
loadHistory();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
@@ -228,7 +309,41 @@ export default function HuyaRegisterPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportSuccess = () => {
|
||||
const handleRetry = async () => {
|
||||
if (!batchId) return;
|
||||
setRetrying(true);
|
||||
try {
|
||||
const data = await huyaApi.retryAutoRegisterBatch(batchId, {
|
||||
concurrency,
|
||||
wait_seconds: waitSeconds,
|
||||
poll_interval: pollInterval,
|
||||
password_prefix: passwordMode === 'random' ? (passwordPrefix.trim() || 'hy') : undefined,
|
||||
fixed_password: passwordMode === 'fixed' ? fixedPassword.trim() : undefined,
|
||||
use_proxy: useProxy,
|
||||
});
|
||||
setBatch(data);
|
||||
message.success(`已开始续跑 ${retryableCount} 条失败/未完成项`);
|
||||
loadHistory();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setRetrying(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportCurrentBatch = async () => {
|
||||
if (!batchId) return;
|
||||
setExporting(true);
|
||||
try {
|
||||
// 优先服务端历史流水(换电脑可用);若无记录再回退当前页内存
|
||||
try {
|
||||
const blob = await huyaApi.exportAutoRegisterBatch(batchId);
|
||||
await downloadBlob(blob, `huya-register-${batchId}.txt`);
|
||||
message.success('已导出当前批次成功账号');
|
||||
return;
|
||||
} catch {
|
||||
// fallthrough
|
||||
}
|
||||
if (successRows.length === 0) {
|
||||
message.warning('当前批次没有可导出的成功账号');
|
||||
return;
|
||||
@@ -237,12 +352,32 @@ export default function HuyaRegisterPage() {
|
||||
.map((item) => `${item.username || item.uid}----${item.password}----${item.phone}----${item.sms_url}`)
|
||||
.join('\n');
|
||||
const blob = new Blob([body], { type: 'text/plain;charset=utf-8' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = `huya-register-accounts-${batchId || 'success'}.txt`;
|
||||
link.click();
|
||||
URL.revokeObjectURL(url);
|
||||
await downloadBlob(blob, `huya-register-accounts-${batchId}.txt`);
|
||||
message.success('已导出当前批次成功账号');
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportAllSuccess = async () => {
|
||||
setExporting(true);
|
||||
try {
|
||||
const blob = await huyaApi.exportRegisterSuccessLogs({
|
||||
tag: tag.trim() || undefined,
|
||||
limit: 5000,
|
||||
});
|
||||
const name = tag.trim()
|
||||
? `huya-register-success-${tag.trim()}.txt`
|
||||
: 'huya-register-success-all.txt';
|
||||
await downloadBlob(blob, name);
|
||||
message.success(tag.trim() ? `已按标签「${tag.trim()}」导出成功历史` : '已导出全部成功历史');
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setExporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const columns: TableProps<HuyaAutoRegisterItem>['columns'] = [
|
||||
@@ -296,6 +431,125 @@ export default function HuyaRegisterPage() {
|
||||
},
|
||||
];
|
||||
|
||||
const historyColumns: TableProps<HuyaAutoRegisterBatch>['columns'] = [
|
||||
{
|
||||
title: '批次',
|
||||
dataIndex: 'batch_id',
|
||||
width: 130,
|
||||
render: (value: string) => <Text code>{value}</Text>,
|
||||
},
|
||||
{
|
||||
title: '标签',
|
||||
dataIndex: 'tag',
|
||||
width: 120,
|
||||
render: (value: string) => value || '-',
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 100,
|
||||
render: (status: string) => (
|
||||
<Tag color={STATUS_COLORS[status] || 'default'}>
|
||||
{STATUS_LABELS[status] || status}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '进度',
|
||||
width: 160,
|
||||
render: (_, row) => `${row.success_count}/${row.total} 成功 · 失败 ${row.failed_count}`,
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 170,
|
||||
render: (value: string | null) => formatTime(value),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 220,
|
||||
render: (_, row) => (
|
||||
<Space size={4}>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => loadBatchById(row.batch_id)}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<RedoOutlined />}
|
||||
disabled={row.status === 'running' || (row.success_count >= row.total && row.total > 0)}
|
||||
onClick={async () => {
|
||||
try {
|
||||
setRetrying(true);
|
||||
const data = await huyaApi.retryAutoRegisterBatch(row.batch_id, {
|
||||
concurrency,
|
||||
wait_seconds: waitSeconds,
|
||||
poll_interval: pollInterval,
|
||||
use_proxy: useProxy,
|
||||
});
|
||||
setBatch(data);
|
||||
message.success('已开始续跑失败/未完成项');
|
||||
loadHistory();
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
} finally {
|
||||
setRetrying(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
续跑
|
||||
</Button>
|
||||
<Button
|
||||
size="small"
|
||||
icon={<DownloadOutlined />}
|
||||
disabled={(row.success_count || 0) <= 0}
|
||||
onClick={async () => {
|
||||
try {
|
||||
const blob = await huyaApi.exportAutoRegisterBatch(row.batch_id);
|
||||
await downloadBlob(blob, `huya-register-${row.batch_id}.txt`);
|
||||
} catch (e: unknown) {
|
||||
message.error(getErrorMessage(e));
|
||||
}
|
||||
}}
|
||||
>
|
||||
导出
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const logColumns: TableProps<HuyaRegisterSuccessLog>['columns'] = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 70 },
|
||||
{
|
||||
title: '批次',
|
||||
dataIndex: 'batch_id',
|
||||
width: 120,
|
||||
render: (value: string) => value || '-',
|
||||
},
|
||||
{ title: '手机号', dataIndex: 'phone', width: 140 },
|
||||
{
|
||||
title: '虎牙号',
|
||||
width: 140,
|
||||
render: (_, row) => row.username || row.uid || '-',
|
||||
},
|
||||
{ title: '密码', dataIndex: 'password', width: 120 },
|
||||
{
|
||||
title: '标签',
|
||||
dataIndex: 'tag',
|
||||
width: 100,
|
||||
render: (value: string) => value || '-',
|
||||
},
|
||||
{
|
||||
title: '时间',
|
||||
dataIndex: 'created_at',
|
||||
width: 170,
|
||||
render: (value: string | null) => formatTime(value),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
<Title level={3} style={{ margin: 0 }}>虎牙自动注册</Title>
|
||||
@@ -315,7 +569,7 @@ export default function HuyaRegisterPage() {
|
||||
<Input
|
||||
value={tag}
|
||||
onChange={(event) => setTag(event.target.value)}
|
||||
placeholder="注册批次"
|
||||
placeholder="注册批次(导出历史可按标签过滤)"
|
||||
disabled={isRunning}
|
||||
/>
|
||||
</Col>
|
||||
@@ -393,8 +647,8 @@ export default function HuyaRegisterPage() {
|
||||
/>
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={24} md={4}>
|
||||
<Space style={{ width: '100%', paddingTop: 22 }}>
|
||||
<Col xs={24} md={12}>
|
||||
<Space wrap style={{ width: '100%', paddingTop: 22 }}>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
@@ -413,21 +667,44 @@ export default function HuyaRegisterPage() {
|
||||
>
|
||||
停止
|
||||
</Button>
|
||||
<Button
|
||||
icon={<RedoOutlined />}
|
||||
loading={retrying}
|
||||
disabled={!canRetry}
|
||||
onClick={handleRetry}
|
||||
>
|
||||
续跑失败项{retryableCount > 0 ? ` (${retryableCount})` : ''}
|
||||
</Button>
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
loading={exporting}
|
||||
onClick={handleExportAllSuccess}
|
||||
>
|
||||
导出全部成功历史
|
||||
</Button>
|
||||
</Space>
|
||||
</Col>
|
||||
</Row>
|
||||
<Text type="secondary">
|
||||
成功账号会立即写入数据库成功流水;服务重启后可从历史批次查看并续跑失败项。换电脑登录同一服务即可导出 txt。
|
||||
</Text>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="批次结果"
|
||||
title="当前批次结果"
|
||||
extra={(
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} loading={refreshing} disabled={!batchId} onClick={refreshBatch}>
|
||||
刷新
|
||||
</Button>
|
||||
<Button icon={<DownloadOutlined />} disabled={successRows.length === 0} onClick={handleExportSuccess}>
|
||||
导出账号
|
||||
<Button
|
||||
icon={<DownloadOutlined />}
|
||||
loading={exporting}
|
||||
disabled={!batchId}
|
||||
onClick={handleExportCurrentBatch}
|
||||
>
|
||||
导出本批成功
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
@@ -440,6 +717,11 @@ export default function HuyaRegisterPage() {
|
||||
<Col xs={12} md={4}><Statistic title="停止" value={batch?.stopped_count || 0} /></Col>
|
||||
<Col xs={12} md={4}><Statistic title="批次" value={batch?.batch_id || '-'} /></Col>
|
||||
</Row>
|
||||
{batch?.message ? (
|
||||
<Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
|
||||
{STATUS_LABELS[batch.status] || batch.status} · {batch.message}
|
||||
</Text>
|
||||
) : null}
|
||||
<Table
|
||||
rowKey="line"
|
||||
columns={columns}
|
||||
@@ -449,6 +731,50 @@ export default function HuyaRegisterPage() {
|
||||
scroll={{ x: 1320 }}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="历史批次"
|
||||
extra={(
|
||||
<Button icon={<ReloadOutlined />} loading={historyLoading} onClick={loadHistory}>
|
||||
刷新历史
|
||||
</Button>
|
||||
)}
|
||||
>
|
||||
<Table
|
||||
rowKey="batch_id"
|
||||
columns={historyColumns}
|
||||
dataSource={history}
|
||||
loading={historyLoading}
|
||||
pagination={{ pageSize: 10 }}
|
||||
scroll={{ x: 900 }}
|
||||
onRow={(row) => ({
|
||||
style: row.batch_id === batchId ? { background: 'rgba(22, 119, 255, 0.06)' } : undefined,
|
||||
})}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card
|
||||
title="成功流水(最近 100 条)"
|
||||
extra={(
|
||||
<Space>
|
||||
<Button icon={<ReloadOutlined />} loading={logsLoading} onClick={loadSuccessLogs}>
|
||||
刷新
|
||||
</Button>
|
||||
<Button icon={<DownloadOutlined />} loading={exporting} onClick={handleExportAllSuccess}>
|
||||
导出
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
>
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={logColumns}
|
||||
dataSource={successLogs}
|
||||
loading={logsLoading}
|
||||
pagination={{ pageSize: 10 }}
|
||||
scroll={{ x: 900 }}
|
||||
/>
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user