虎牙自动注册持久化成功流水与批次,支持失败续跑与随时导出
将注册批次/条目/成功记录落库,成功一个写入一条流水;服务中断可恢复并续跑失败项,换电脑也能导出 txt。同时修复 pending 被误判为运行中导致页面卡住的问题。
This commit is contained in:
@@ -150,6 +150,10 @@ _SENSITIVE_COLUMNS: tuple[tuple[str, str], ...] = (
|
|||||||
("login_tasks", "cookie"),
|
("login_tasks", "cookie"),
|
||||||
("huya_accounts", "account_password"),
|
("huya_accounts", "account_password"),
|
||||||
("huya_accounts", "cookie"),
|
("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", "api_url"),
|
||||||
("proxy_config", "http"),
|
("proxy_config", "http"),
|
||||||
("proxy_config", "https"),
|
("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_name = Column(String(128), default="")
|
||||||
game_channel = Column(String(64), default="")
|
game_channel = Column(String(64), default="")
|
||||||
game_phone = 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)
|
assigned_to = Column(Integer, ForeignKey("users.id"), nullable=True, index=True)
|
||||||
created_at = Column(DateTime, default=_utcnow)
|
created_at = Column(DateTime, default=_utcnow)
|
||||||
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
|
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
|
||||||
@@ -104,6 +105,81 @@ class HuyaAccount(Base):
|
|||||||
tasks = relationship("HuyaTask", back_populates="account")
|
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):
|
class HuyaTask(Base):
|
||||||
"""虎牙业务任务"""
|
"""虎牙业务任务"""
|
||||||
__tablename__ = "huya_tasks"
|
__tablename__ = "huya_tasks"
|
||||||
|
|||||||
+121
-7
@@ -32,6 +32,7 @@ from ..schemas import (
|
|||||||
HuyaAccountOut,
|
HuyaAccountOut,
|
||||||
HuyaAutoRegisterBatchOut,
|
HuyaAutoRegisterBatchOut,
|
||||||
HuyaAutoRegisterRequest,
|
HuyaAutoRegisterRequest,
|
||||||
|
HuyaAutoRegisterRetryRequest,
|
||||||
HuyaConfigOut,
|
HuyaConfigOut,
|
||||||
HuyaConfigUpdate,
|
HuyaConfigUpdate,
|
||||||
HuyaCookieImport,
|
HuyaCookieImport,
|
||||||
@@ -40,6 +41,7 @@ from ..schemas import (
|
|||||||
HuyaPasswordLoginRequest,
|
HuyaPasswordLoginRequest,
|
||||||
HuyaPasswordLoginSelectedRequest,
|
HuyaPasswordLoginSelectedRequest,
|
||||||
HuyaRechargeGoodsOut,
|
HuyaRechargeGoodsOut,
|
||||||
|
HuyaRegisterSuccessLogOut,
|
||||||
HuyaSmsCodeRequest,
|
HuyaSmsCodeRequest,
|
||||||
HuyaSmsLoginRequest,
|
HuyaSmsLoginRequest,
|
||||||
HuyaTaskBatchRequest,
|
HuyaTaskBatchRequest,
|
||||||
@@ -58,7 +60,11 @@ from ..services.huya_service import (
|
|||||||
upsert_huya_cookie,
|
upsert_huya_cookie,
|
||||||
)
|
)
|
||||||
from ..services.huya_runner import HuyaBatchRunner, huya_batch_registry
|
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=["虎牙"])
|
router = APIRouter(prefix="/api/huya", tags=["虎牙"])
|
||||||
@@ -366,22 +372,33 @@ def create_auto_register_batch(
|
|||||||
use_proxy=req.use_proxy,
|
use_proxy=req.use_proxy,
|
||||||
proxy_config=proxy_config,
|
proxy_config=proxy_config,
|
||||||
)
|
)
|
||||||
|
runner.mark_running("批次运行中")
|
||||||
thread = threading.Thread(target=runner.run, daemon=True)
|
thread = threading.Thread(target=runner.run, daemon=True)
|
||||||
thread.start()
|
thread.start()
|
||||||
return runner.snapshot()
|
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)
|
@router.get("/register/batches/{batch_id}", response_model=HuyaAutoRegisterBatchOut)
|
||||||
def get_auto_register_batch(
|
def get_auto_register_batch(
|
||||||
batch_id: str,
|
batch_id: str,
|
||||||
current: User = Depends(get_current_user),
|
current: User = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""查询虎牙手机号自动注册批次状态。"""
|
"""查询虎牙手机号自动注册批次状态(优先内存,否则读库)。"""
|
||||||
_require_huya_perm(current, "huya:import")
|
_require_huya_perm(current, "huya:import")
|
||||||
runner = huya_register_registry.get(batch_id)
|
snapshot = huya_register_registry.get_snapshot(batch_id)
|
||||||
if not runner:
|
if not snapshot:
|
||||||
raise HTTPException(status_code=404, detail="批次不存在或服务已重启")
|
raise HTTPException(status_code=404, detail="批次不存在")
|
||||||
return runner.snapshot()
|
return snapshot
|
||||||
|
|
||||||
|
|
||||||
@router.post("/register/batches/{batch_id}/stop")
|
@router.post("/register/batches/{batch_id}/stop")
|
||||||
@@ -393,11 +410,108 @@ def stop_auto_register_batch(
|
|||||||
_require_huya_perm(current, "huya:import")
|
_require_huya_perm(current, "huya:import")
|
||||||
runner = huya_register_registry.get(batch_id)
|
runner = huya_register_registry.get(batch_id)
|
||||||
if not runner:
|
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()
|
runner.stop()
|
||||||
return {"message": "已发送停止信号", "success": True}
|
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")
|
@router.post("/accounts/password-login/selected")
|
||||||
def password_login_selected_accounts(
|
def password_login_selected_accounts(
|
||||||
req: HuyaPasswordLoginSelectedRequest,
|
req: HuyaPasswordLoginSelectedRequest,
|
||||||
|
|||||||
+28
-1
@@ -248,6 +248,16 @@ class HuyaAutoRegisterRequest(BaseModel):
|
|||||||
use_proxy: bool = False
|
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):
|
class HuyaAutoRegisterItemOut(BaseModel):
|
||||||
line: int
|
line: int
|
||||||
phone: str
|
phone: str
|
||||||
@@ -289,7 +299,24 @@ class HuyaAutoRegisterBatchOut(BaseModel):
|
|||||||
created_at: Optional[datetime] = None
|
created_at: Optional[datetime] = None
|
||||||
started_at: Optional[datetime] = None
|
started_at: Optional[datetime] = None
|
||||||
finished_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):
|
class HuyaPasswordAccountImport(BaseModel):
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""虎牙自动注册批次执行器。"""
|
"""虎牙自动注册批次执行器(支持持久化与失败续跑)。"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@@ -15,10 +15,22 @@ from core.huya.cookie_utils import normalize_huya_cookie
|
|||||||
from core.sms_provider import SmsLine
|
from core.sms_provider import SmsLine
|
||||||
|
|
||||||
from ..database import SessionLocal
|
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
|
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:
|
def _now() -> datetime:
|
||||||
return datetime.now(timezone.utc)
|
return datetime.now(timezone.utc)
|
||||||
|
|
||||||
@@ -32,7 +44,7 @@ def _cookie_preview(cookie: str) -> str:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class HuyaRegisterItemState:
|
class HuyaRegisterItemState:
|
||||||
"""单个手机号在批次中的状态。"""
|
"""单个手机号在批次中的状态(内存镜像)。"""
|
||||||
|
|
||||||
line: int
|
line: int
|
||||||
phone: str
|
phone: str
|
||||||
@@ -53,6 +65,7 @@ class HuyaRegisterItemState:
|
|||||||
cookie_preview: str = ""
|
cookie_preview: str = ""
|
||||||
started_at: datetime | None = None
|
started_at: datetime | None = None
|
||||||
finished_at: datetime | None = None
|
finished_at: datetime | None = None
|
||||||
|
db_id: int | None = None
|
||||||
|
|
||||||
def to_dict(self) -> dict:
|
def to_dict(self) -> dict:
|
||||||
return {
|
return {
|
||||||
@@ -97,6 +110,274 @@ class HuyaRegisterBatch:
|
|||||||
created_at: datetime = field(default_factory=_now)
|
created_at: datetime = field(default_factory=_now)
|
||||||
started_at: datetime | None = None
|
started_at: datetime | None = None
|
||||||
finished_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:
|
class HuyaRegisterRunner:
|
||||||
@@ -107,10 +388,13 @@ class HuyaRegisterRunner:
|
|||||||
batch: HuyaRegisterBatch,
|
batch: HuyaRegisterBatch,
|
||||||
sms_lines: list[SmsLine],
|
sms_lines: list[SmsLine],
|
||||||
proxy_config: Optional[ProxyConfigModel] = None,
|
proxy_config: Optional[ProxyConfigModel] = None,
|
||||||
|
*,
|
||||||
|
item_indices: list[int] | None = None,
|
||||||
):
|
):
|
||||||
self.batch = batch
|
self.batch = batch
|
||||||
self.sms_lines = sms_lines
|
self.sms_lines = sms_lines
|
||||||
self.proxy_config = proxy_config
|
self.proxy_config = proxy_config
|
||||||
|
self.item_indices = item_indices # None 表示跑全部
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
self._stop = threading.Event()
|
self._stop = threading.Event()
|
||||||
self._shared_proxy_fetcher = self._create_proxy_fetcher()
|
self._shared_proxy_fetcher = self._create_proxy_fetcher()
|
||||||
@@ -145,54 +429,123 @@ class HuyaRegisterRunner:
|
|||||||
with self._lock:
|
with self._lock:
|
||||||
if self.batch.status == "running":
|
if self.batch.status == "running":
|
||||||
self.batch.message = "正在停止"
|
self.batch.message = "正在停止"
|
||||||
|
self._persist_batch_meta()
|
||||||
|
|
||||||
def snapshot(self) -> dict:
|
def snapshot(self) -> dict:
|
||||||
with self._lock:
|
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")
|
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")
|
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")
|
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"})
|
row.success_count = success
|
||||||
return {
|
row.failed_count = failed
|
||||||
"batch_id": self.batch.batch_id,
|
row.stopped_count = stopped
|
||||||
"status": self.batch.status,
|
row.total = len(self.batch.items)
|
||||||
"message": self.batch.message,
|
db.commit()
|
||||||
"tag": self.batch.tag,
|
finally:
|
||||||
"created_by": self.batch.created_by,
|
db.close()
|
||||||
"concurrency": self.batch.concurrency,
|
|
||||||
"wait_seconds": self.batch.wait_seconds,
|
def _persist_item(self, index: int):
|
||||||
"poll_interval": self.batch.poll_interval,
|
"""把单条 item 状态写回数据库。"""
|
||||||
"password_prefix": self.batch.password_prefix,
|
item = self.batch.items[index]
|
||||||
"use_proxy": self.batch.use_proxy,
|
if not item.db_id:
|
||||||
"total": total,
|
return
|
||||||
"success_count": success,
|
db = SessionLocal()
|
||||||
"failed_count": failed,
|
try:
|
||||||
"stopped_count": stopped,
|
row = db.query(HuyaRegisterItemModel).filter(HuyaRegisterItemModel.id == item.db_id).first()
|
||||||
"running_count": running,
|
if not row:
|
||||||
"created_at": self.batch.created_at,
|
return
|
||||||
"started_at": self.batch.started_at,
|
row.status = item.status
|
||||||
"finished_at": self.batch.finished_at,
|
row.message = (item.message or "")[:512]
|
||||||
"items": [item.to_dict() for item in self.batch.items],
|
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):
|
def _set_item(self, index: int, **updates):
|
||||||
with self._lock:
|
with self._lock:
|
||||||
item = self.batch.items[index]
|
item = self.batch.items[index]
|
||||||
for key, value in updates.items():
|
for key, value in updates.items():
|
||||||
setattr(item, key, value)
|
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()
|
db = SessionLocal()
|
||||||
try:
|
try:
|
||||||
|
# upsert_huya_cookie 内部会 commit 一次
|
||||||
account = upsert_huya_cookie(db, result.cookie, tag=self.batch.tag, username_hint="")
|
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:
|
if result.username:
|
||||||
account.username = result.username
|
account.username = result.username
|
||||||
if result.password:
|
if result.password:
|
||||||
account.account_password = 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:
|
if result.password_changed:
|
||||||
account.status = "password_changed"
|
account.status = "password_changed"
|
||||||
account.updated_at = _now()
|
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.commit()
|
||||||
db.refresh(account)
|
db.refresh(account)
|
||||||
return account.id, account.username or "", account.uid or account.yyuid or ""
|
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())
|
self._set_item(index, status="error", message=proxy_error, finished_at=_now())
|
||||||
return
|
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(
|
result = register_huya_with_sms_line(
|
||||||
item,
|
item,
|
||||||
wait_seconds=self.batch.wait_seconds,
|
wait_seconds=self.batch.wait_seconds,
|
||||||
@@ -245,6 +615,7 @@ class HuyaRegisterRunner:
|
|||||||
message = result.message
|
message = result.message
|
||||||
status = result.status
|
status = result.status
|
||||||
cookie = result.cookie if result.success else ""
|
cookie = result.cookie if result.success else ""
|
||||||
|
full_cookie = cookie
|
||||||
if result.success:
|
if result.success:
|
||||||
self._set_item(
|
self._set_item(
|
||||||
index,
|
index,
|
||||||
@@ -260,38 +631,71 @@ class HuyaRegisterRunner:
|
|||||||
password_changed=result.password_changed,
|
password_changed=result.password_changed,
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
account_id, username, uid = self._save_cookie(result)
|
account_id, username, uid = self._save_success(index, result)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
status = "error"
|
status = "error"
|
||||||
cookie = ""
|
cookie = ""
|
||||||
|
full_cookie = ""
|
||||||
message = f"账号保存失败: {exc}"
|
message = f"账号保存失败: {exc}"
|
||||||
|
|
||||||
exposed_cookie = "" if result.password_changed else cookie
|
exposed_cookie = "" if result.password_changed else cookie
|
||||||
|
|
||||||
self._set_item(
|
# 先把完整 cookie 写内存供 persist,再把暴露值用于展示
|
||||||
index,
|
with self._lock:
|
||||||
status=status,
|
mem = self.batch.items[index]
|
||||||
message=message,
|
mem.status = status
|
||||||
code=result.code,
|
mem.message = message
|
||||||
change_code=result.change_code,
|
mem.code = result.code
|
||||||
attempts=result.attempts,
|
mem.change_code = result.change_code
|
||||||
change_attempts=result.change_attempts,
|
mem.attempts = result.attempts
|
||||||
account_id=account_id,
|
mem.change_attempts = result.change_attempts
|
||||||
username=username,
|
mem.account_id = account_id
|
||||||
uid=uid,
|
mem.username = username
|
||||||
password=result.password,
|
mem.uid = uid
|
||||||
password_changed=result.password_changed,
|
mem.password = result.password
|
||||||
cookie=normalize_huya_cookie(exposed_cookie),
|
mem.password_changed = result.password_changed
|
||||||
cookie_preview=_cookie_preview(exposed_cookie),
|
mem.cookie = normalize_huya_cookie(full_cookie)
|
||||||
finished_at=_now(),
|
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):
|
def run(self):
|
||||||
"""线程入口。"""
|
"""线程入口。"""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self.batch.status = "running"
|
if self.batch.status != "running":
|
||||||
self.batch.message = "批次运行中"
|
self.batch.status = "running"
|
||||||
self.batch.started_at = _now()
|
self.batch.message = "批次运行中"
|
||||||
|
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:
|
try:
|
||||||
if self._shared_proxy_fetcher:
|
if self._shared_proxy_fetcher:
|
||||||
@@ -299,13 +703,15 @@ class HuyaRegisterRunner:
|
|||||||
if not ok:
|
if not ok:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self.batch.message = f"代理白名单预热失败: {msg}"
|
self.batch.message = f"代理白名单预热失败: {msg}"
|
||||||
|
self._persist_batch_meta()
|
||||||
|
|
||||||
with ThreadPoolExecutor(max_workers=self.batch.concurrency) as executor:
|
with ThreadPoolExecutor(max_workers=self.batch.concurrency) as executor:
|
||||||
futures = []
|
futures = []
|
||||||
for index, item in enumerate(self.sms_lines):
|
for index in indices:
|
||||||
if self._stop.is_set():
|
if self._stop.is_set():
|
||||||
self._set_item(index, status="stopped", message="已停止", finished_at=_now())
|
self._set_item(index, status="stopped", message="已停止", finished_at=_now())
|
||||||
continue
|
continue
|
||||||
|
item = self.sms_lines[index]
|
||||||
futures.append(executor.submit(self._run_one, index, item))
|
futures.append(executor.submit(self._run_one, index, item))
|
||||||
|
|
||||||
for future in as_completed(futures):
|
for future in as_completed(futures):
|
||||||
@@ -315,6 +721,7 @@ class HuyaRegisterRunner:
|
|||||||
self.batch.status = "error"
|
self.batch.status = "error"
|
||||||
self.batch.message = f"批次执行异常: {exc}"
|
self.batch.message = f"批次执行异常: {exc}"
|
||||||
self.batch.finished_at = _now()
|
self.batch.finished_at = _now()
|
||||||
|
self._persist_batch_meta()
|
||||||
return
|
return
|
||||||
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
@@ -325,10 +732,11 @@ class HuyaRegisterRunner:
|
|||||||
self.batch.status = "finished"
|
self.batch.status = "finished"
|
||||||
self.batch.message = "批次已完成"
|
self.batch.message = "批次已完成"
|
||||||
self.batch.finished_at = _now()
|
self.batch.finished_at = _now()
|
||||||
|
self._persist_batch_meta()
|
||||||
|
|
||||||
|
|
||||||
class HuyaRegisterRegistry:
|
class HuyaRegisterRegistry:
|
||||||
"""管理自动注册批次。"""
|
"""管理自动注册批次(内存 runner + DB 持久化)。"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
@@ -348,29 +756,229 @@ class HuyaRegisterRegistry:
|
|||||||
proxy_config: Optional[ProxyConfigModel] = None,
|
proxy_config: Optional[ProxyConfigModel] = None,
|
||||||
) -> HuyaRegisterRunner:
|
) -> HuyaRegisterRunner:
|
||||||
batch_id = uuid.uuid4().hex[:12]
|
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 = HuyaRegisterBatch(
|
||||||
batch_id=batch_id,
|
batch_id=batch_id,
|
||||||
tag=tag,
|
tag=tag,
|
||||||
created_by=created_by,
|
created_by=created_by,
|
||||||
concurrency=max(1, min(int(concurrency or 1), 5)),
|
concurrency=concurrency,
|
||||||
wait_seconds=max(15.0, float(wait_seconds or 180)),
|
wait_seconds=wait_seconds,
|
||||||
poll_interval=max(1.0, float(poll_interval or 5)),
|
poll_interval=poll_interval,
|
||||||
password_prefix=(password_prefix or "hy").strip()[:8] or "hy",
|
password_prefix=password_prefix,
|
||||||
fixed_password=(fixed_password or "").strip(),
|
fixed_password=fixed_password,
|
||||||
use_proxy=bool(use_proxy),
|
use_proxy=bool(use_proxy),
|
||||||
items=[
|
items=items,
|
||||||
HuyaRegisterItemState(line=index + 1, phone=item.phone, provider=item.provider, sms_url=item.url)
|
db_id=db_id,
|
||||||
for index, item in enumerate(sms_lines)
|
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)
|
runner = HuyaRegisterRunner(batch=batch, sms_lines=sms_lines, proxy_config=proxy_config)
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self._runners[batch_id] = runner
|
self._runners[batch_id] = runner
|
||||||
return 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:
|
def get(self, batch_id: str) -> HuyaRegisterRunner | None:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
return self._runners.get(batch_id)
|
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()
|
huya_register_registry = HuyaRegisterRegistry()
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type {
|
|||||||
HuyaAccountItem,
|
HuyaAccountItem,
|
||||||
HuyaAutoRegisterBatch,
|
HuyaAutoRegisterBatch,
|
||||||
HuyaAutoRegisterRequest,
|
HuyaAutoRegisterRequest,
|
||||||
|
HuyaAutoRegisterRetryRequest,
|
||||||
HuyaConfig,
|
HuyaConfig,
|
||||||
HuyaCookieItem,
|
HuyaCookieItem,
|
||||||
HuyaCookieImportResult,
|
HuyaCookieImportResult,
|
||||||
@@ -13,6 +14,7 @@ import type {
|
|||||||
HuyaPasswordLoginResult,
|
HuyaPasswordLoginResult,
|
||||||
HuyaPasswordLoginSelectedRequest,
|
HuyaPasswordLoginSelectedRequest,
|
||||||
HuyaRechargeGoodsItem,
|
HuyaRechargeGoodsItem,
|
||||||
|
HuyaRegisterSuccessLog,
|
||||||
HuyaSmsCodeRequest,
|
HuyaSmsCodeRequest,
|
||||||
HuyaSmsCodeResult,
|
HuyaSmsCodeResult,
|
||||||
HuyaSmsLoginRequest,
|
HuyaSmsLoginRequest,
|
||||||
@@ -43,10 +45,20 @@ export const huyaApi = {
|
|||||||
api.post<HuyaSmsLoginResult, HuyaSmsLoginResult>('/huya/accounts/sms-login', data, { timeout: 120000 }),
|
api.post<HuyaSmsLoginResult, HuyaSmsLoginResult>('/huya/accounts/sms-login', data, { timeout: 120000 }),
|
||||||
startAutoRegister: (data: HuyaAutoRegisterRequest) =>
|
startAutoRegister: (data: HuyaAutoRegisterRequest) =>
|
||||||
api.post<HuyaAutoRegisterBatch, HuyaAutoRegisterBatch>('/huya/register/batches', data),
|
api.post<HuyaAutoRegisterBatch, HuyaAutoRegisterBatch>('/huya/register/batches', data),
|
||||||
|
listAutoRegisterBatches: (limit = 50) =>
|
||||||
|
api.get<HuyaAutoRegisterBatch[], HuyaAutoRegisterBatch[]>('/huya/register/batches', { params: { limit } }),
|
||||||
getAutoRegisterBatch: (batchId: string) =>
|
getAutoRegisterBatch: (batchId: string) =>
|
||||||
api.get<HuyaAutoRegisterBatch, HuyaAutoRegisterBatch>(`/huya/register/batches/${batchId}`),
|
api.get<HuyaAutoRegisterBatch, HuyaAutoRegisterBatch>(`/huya/register/batches/${batchId}`),
|
||||||
stopAutoRegisterBatch: (batchId: string) =>
|
stopAutoRegisterBatch: (batchId: string) =>
|
||||||
api.post<MessageResponse, MessageResponse>(`/huya/register/batches/${batchId}/stop`),
|
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) =>
|
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) =>
|
||||||
|
|||||||
@@ -223,6 +223,15 @@ export interface HuyaAutoRegisterRequest {
|
|||||||
use_proxy?: boolean;
|
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 {
|
export interface HuyaAutoRegisterItem {
|
||||||
line: number;
|
line: number;
|
||||||
phone: string;
|
phone: string;
|
||||||
@@ -267,6 +276,23 @@ export interface HuyaAutoRegisterBatch {
|
|||||||
items: HuyaAutoRegisterItem[];
|
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 {
|
export interface HuyaPasswordLoginBatchItem {
|
||||||
line: number;
|
line: number;
|
||||||
username: string;
|
username: string;
|
||||||
|
|||||||
@@ -3,8 +3,15 @@ import {
|
|||||||
Button, Card, Col, Input, InputNumber, message, Row, Segmented, Space, Statistic, Switch, Table, Tag, Typography,
|
Button, Card, Col, Input, InputNumber, message, Row, Segmented, Space, Statistic, Switch, Table, Tag, Typography,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import type { TableProps } from 'antd';
|
import type { TableProps } from 'antd';
|
||||||
import { DownloadOutlined, PlayCircleOutlined, ReloadOutlined, StopOutlined } from '@ant-design/icons';
|
import {
|
||||||
import { huyaApi, type HuyaAutoRegisterBatch, type HuyaAutoRegisterItem } from '../api/modules';
|
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 { formatTime } from '../utils/time';
|
||||||
import { getErrorMessage } from '../utils/error';
|
import { getErrorMessage } from '../utils/error';
|
||||||
|
|
||||||
@@ -48,6 +55,9 @@ const STATUS_LABELS: Record<string, string> = {
|
|||||||
success: '成功',
|
success: '成功',
|
||||||
error: '失败',
|
error: '失败',
|
||||||
stopped: '已停止',
|
stopped: '已停止',
|
||||||
|
interrupted: '已中断',
|
||||||
|
finished: '已完成',
|
||||||
|
running: '运行中',
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_COLORS: Record<string, string> = {
|
const STATUS_COLORS: Record<string, string> = {
|
||||||
@@ -59,9 +69,14 @@ const STATUS_COLORS: Record<string, string> = {
|
|||||||
success: 'success',
|
success: 'success',
|
||||||
error: 'error',
|
error: 'error',
|
||||||
stopped: 'warning',
|
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) {
|
function safeNumber(value: unknown, fallback: number) {
|
||||||
const n = Number(value);
|
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() {
|
export default function HuyaRegisterPage() {
|
||||||
const [initialForm] = useState(readStoredForm);
|
const [initialForm] = useState(readStoredForm);
|
||||||
const [text, setText] = useState(initialForm.text);
|
const [text, setText] = useState(initialForm.text);
|
||||||
@@ -108,13 +132,26 @@ export default function HuyaRegisterPage() {
|
|||||||
const [fixedPassword, setFixedPassword] = useState(initialForm.fixedPassword);
|
const [fixedPassword, setFixedPassword] = useState(initialForm.fixedPassword);
|
||||||
const [useProxy, setUseProxy] = useState(initialForm.useProxy);
|
const [useProxy, setUseProxy] = useState(initialForm.useProxy);
|
||||||
const [batch, setBatch] = useState<HuyaAutoRegisterBatch | null>(null);
|
const [batch, setBatch] = useState<HuyaAutoRegisterBatch | null>(null);
|
||||||
|
const [history, setHistory] = useState<HuyaAutoRegisterBatch[]>([]);
|
||||||
|
const [successLogs, setSuccessLogs] = useState<HuyaRegisterSuccessLog[]>([]);
|
||||||
const [starting, setStarting] = useState(false);
|
const [starting, setStarting] = useState(false);
|
||||||
|
const [retrying, setRetrying] = useState(false);
|
||||||
const [refreshing, setRefreshing] = useState(false);
|
const [refreshing, setRefreshing] = useState(false);
|
||||||
const [stopping, setStopping] = 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 restoredBatchRef = useRef(false);
|
||||||
|
|
||||||
const batchId = batch?.batch_id || '';
|
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) => {
|
const loadBatchById = useCallback(async (id: string) => {
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -125,10 +162,10 @@ export default function HuyaRegisterPage() {
|
|||||||
localStorage.setItem(BATCH_STORAGE_KEY, data.batch_id);
|
localStorage.setItem(BATCH_STORAGE_KEY, data.batch_id);
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
const err = getErrorMessage(e);
|
const err = getErrorMessage(e);
|
||||||
if (err.includes('批次不存在') || err.includes('服务已重启')) {
|
if (err.includes('批次不存在')) {
|
||||||
localStorage.removeItem(BATCH_STORAGE_KEY);
|
localStorage.removeItem(BATCH_STORAGE_KEY);
|
||||||
setBatch(null);
|
setBatch(null);
|
||||||
message.warning('上次自动注册批次已不存在,已清除恢复记录');
|
message.warning('批次不存在,已清除本地恢复记录');
|
||||||
} else {
|
} else {
|
||||||
message.error(err);
|
message.error(err);
|
||||||
}
|
}
|
||||||
@@ -142,6 +179,30 @@ export default function HuyaRegisterPage() {
|
|||||||
await loadBatchById(batchId);
|
await loadBatchById(batchId);
|
||||||
}, [batchId, loadBatchById]);
|
}, [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(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem(FORM_STORAGE_KEY, JSON.stringify({
|
localStorage.setItem(FORM_STORAGE_KEY, JSON.stringify({
|
||||||
text,
|
text,
|
||||||
@@ -163,7 +224,9 @@ export default function HuyaRegisterPage() {
|
|||||||
if (storedBatchId) {
|
if (storedBatchId) {
|
||||||
loadBatchById(storedBatchId);
|
loadBatchById(storedBatchId);
|
||||||
}
|
}
|
||||||
}, [loadBatchById]);
|
loadHistory();
|
||||||
|
loadSuccessLogs();
|
||||||
|
}, [loadBatchById, loadHistory, loadSuccessLogs]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (batch?.batch_id) {
|
if (batch?.batch_id) {
|
||||||
@@ -179,11 +242,28 @@ export default function HuyaRegisterPage() {
|
|||||||
return () => window.clearInterval(timer);
|
return () => window.clearInterval(timer);
|
||||||
}, [batchId, isRunning, refreshBatch]);
|
}, [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(
|
const successRows = useMemo(
|
||||||
() => (batch?.items || []).filter((item) => item.status === 'success' && item.password && (item.username || item.uid)),
|
() => (batch?.items || []).filter((item) => item.status === 'success' && item.password && (item.username || item.uid)),
|
||||||
[batch],
|
[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 () => {
|
const handleStart = async () => {
|
||||||
if (!text.trim()) {
|
if (!text.trim()) {
|
||||||
message.warning('请先粘贴手机号池');
|
message.warning('请先粘贴手机号池');
|
||||||
@@ -207,6 +287,7 @@ export default function HuyaRegisterPage() {
|
|||||||
});
|
});
|
||||||
setBatch(data);
|
setBatch(data);
|
||||||
message.success('自动注册批次已启动');
|
message.success('自动注册批次已启动');
|
||||||
|
loadHistory();
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
message.error(getErrorMessage(e));
|
message.error(getErrorMessage(e));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -228,21 +309,75 @@ export default function HuyaRegisterPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleExportSuccess = () => {
|
const handleRetry = async () => {
|
||||||
if (successRows.length === 0) {
|
if (!batchId) return;
|
||||||
message.warning('当前批次没有可导出的成功账号');
|
setRetrying(true);
|
||||||
return;
|
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;
|
||||||
|
}
|
||||||
|
const body = successRows
|
||||||
|
.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' });
|
||||||
|
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 body = successRows
|
|
||||||
.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);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const columns: TableProps<HuyaAutoRegisterItem>['columns'] = [
|
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 (
|
return (
|
||||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||||
<Title level={3} style={{ margin: 0 }}>虎牙自动注册</Title>
|
<Title level={3} style={{ margin: 0 }}>虎牙自动注册</Title>
|
||||||
@@ -315,7 +569,7 @@ export default function HuyaRegisterPage() {
|
|||||||
<Input
|
<Input
|
||||||
value={tag}
|
value={tag}
|
||||||
onChange={(event) => setTag(event.target.value)}
|
onChange={(event) => setTag(event.target.value)}
|
||||||
placeholder="注册批次"
|
placeholder="注册批次(导出历史可按标签过滤)"
|
||||||
disabled={isRunning}
|
disabled={isRunning}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -393,8 +647,8 @@ export default function HuyaRegisterPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} md={4}>
|
<Col xs={24} md={12}>
|
||||||
<Space style={{ width: '100%', paddingTop: 22 }}>
|
<Space wrap style={{ width: '100%', paddingTop: 22 }}>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlayCircleOutlined />}
|
icon={<PlayCircleOutlined />}
|
||||||
@@ -413,21 +667,44 @@ export default function HuyaRegisterPage() {
|
|||||||
>
|
>
|
||||||
停止
|
停止
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<RedoOutlined />}
|
||||||
|
loading={retrying}
|
||||||
|
disabled={!canRetry}
|
||||||
|
onClick={handleRetry}
|
||||||
|
>
|
||||||
|
续跑失败项{retryableCount > 0 ? ` (${retryableCount})` : ''}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
icon={<DownloadOutlined />}
|
||||||
|
loading={exporting}
|
||||||
|
onClick={handleExportAllSuccess}
|
||||||
|
>
|
||||||
|
导出全部成功历史
|
||||||
|
</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
<Text type="secondary">
|
||||||
|
成功账号会立即写入数据库成功流水;服务重启后可从历史批次查看并续跑失败项。换电脑登录同一服务即可导出 txt。
|
||||||
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<Card
|
<Card
|
||||||
title="批次结果"
|
title="当前批次结果"
|
||||||
extra={(
|
extra={(
|
||||||
<Space>
|
<Space>
|
||||||
<Button icon={<ReloadOutlined />} loading={refreshing} disabled={!batchId} onClick={refreshBatch}>
|
<Button icon={<ReloadOutlined />} loading={refreshing} disabled={!batchId} onClick={refreshBatch}>
|
||||||
刷新
|
刷新
|
||||||
</Button>
|
</Button>
|
||||||
<Button icon={<DownloadOutlined />} disabled={successRows.length === 0} onClick={handleExportSuccess}>
|
<Button
|
||||||
导出账号
|
icon={<DownloadOutlined />}
|
||||||
|
loading={exporting}
|
||||||
|
disabled={!batchId}
|
||||||
|
onClick={handleExportCurrentBatch}
|
||||||
|
>
|
||||||
|
导出本批成功
|
||||||
</Button>
|
</Button>
|
||||||
</Space>
|
</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?.stopped_count || 0} /></Col>
|
||||||
<Col xs={12} md={4}><Statistic title="批次" value={batch?.batch_id || '-'} /></Col>
|
<Col xs={12} md={4}><Statistic title="批次" value={batch?.batch_id || '-'} /></Col>
|
||||||
</Row>
|
</Row>
|
||||||
|
{batch?.message ? (
|
||||||
|
<Text type="secondary" style={{ display: 'block', marginBottom: 12 }}>
|
||||||
|
{STATUS_LABELS[batch.status] || batch.status} · {batch.message}
|
||||||
|
</Text>
|
||||||
|
) : null}
|
||||||
<Table
|
<Table
|
||||||
rowKey="line"
|
rowKey="line"
|
||||||
columns={columns}
|
columns={columns}
|
||||||
@@ -449,6 +731,50 @@ export default function HuyaRegisterPage() {
|
|||||||
scroll={{ x: 1320 }}
|
scroll={{ x: 1320 }}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</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>
|
</Space>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user