1087 lines
39 KiB
Python
1087 lines
39 KiB
Python
"""虎牙自动注册批次执行器(支持持久化与从停止处继续)。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import threading
|
||
import uuid
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
from dataclasses import dataclass, field
|
||
from datetime import UTC, datetime
|
||
|
||
from core.douyu.proxy_fetcher import ProxyFetcher
|
||
from core.huya.auto_register import HuyaAutoRegisterResult, register_huya_with_sms_line
|
||
from core.huya.cookie_utils import normalize_huya_cookie
|
||
from core.sms_provider import SmsLine
|
||
|
||
from ..database import SessionLocal
|
||
from ..models import (
|
||
HuyaRegisterBatch as HuyaRegisterBatchModel,
|
||
)
|
||
from ..models import (
|
||
HuyaRegisterItem as HuyaRegisterItemModel,
|
||
)
|
||
from ..models import (
|
||
HuyaRegisterSuccessLog,
|
||
)
|
||
from ..models import (
|
||
ProxyConfig as ProxyConfigModel,
|
||
)
|
||
from .huya_service import upsert_huya_cookie
|
||
|
||
# 运行中状态:服务中断后视为未完成,可继续
|
||
RUNNING_ITEM_STATUSES = frozenset({"sending", "waiting", "changing", "logging"})
|
||
# 续跑(continue):只跑「还没跑完」的,不重试已明确失败(error)、不重跑成功(success)
|
||
# - pending: 队列里还没轮到
|
||
# - stopped: 点停止时尚未开始或被中断
|
||
# - running 中间态: 进程中断时卡在半路
|
||
RESUME_ITEM_STATUSES = frozenset({"pending", "stopped"} | RUNNING_ITEM_STATUSES)
|
||
# 仅重试明确失败(可选模式)
|
||
RETRY_FAILED_ITEM_STATUSES = frozenset({"error"})
|
||
TERMINAL_BATCH_STATUSES = frozenset({"finished", "stopped", "error", "interrupted"})
|
||
|
||
|
||
def _now() -> datetime:
|
||
return datetime.now(UTC)
|
||
|
||
|
||
def _cookie_preview(cookie: str) -> str:
|
||
normalized = normalize_huya_cookie(cookie or "")
|
||
if not normalized:
|
||
return ""
|
||
return normalized[:50] + "..." if len(normalized) > 50 else normalized
|
||
|
||
|
||
@dataclass
|
||
class HuyaRegisterItemState:
|
||
"""单个手机号在批次中的状态(内存镜像)。"""
|
||
|
||
line: int
|
||
phone: str
|
||
provider: str
|
||
sms_url: str = ""
|
||
status: str = "pending"
|
||
message: str = "等待开始"
|
||
code: str = ""
|
||
change_code: str = ""
|
||
attempts: int = 0
|
||
change_attempts: int = 0
|
||
account_id: int | None = None
|
||
username: str = ""
|
||
uid: str = ""
|
||
password: str = ""
|
||
password_changed: bool = False
|
||
cookie: str = ""
|
||
cookie_preview: str = ""
|
||
started_at: datetime | None = None
|
||
finished_at: datetime | None = None
|
||
db_id: int | None = None
|
||
|
||
def to_dict(self) -> dict:
|
||
return {
|
||
"line": self.line,
|
||
"phone": self.phone,
|
||
"provider": self.provider,
|
||
"sms_url": self.sms_url,
|
||
"status": self.status,
|
||
"message": self.message,
|
||
"code": self.code,
|
||
"change_code": self.change_code,
|
||
"attempts": self.attempts,
|
||
"change_attempts": self.change_attempts,
|
||
"account_id": self.account_id,
|
||
"username": self.username,
|
||
"uid": self.uid,
|
||
"password": self.password,
|
||
"password_changed": self.password_changed,
|
||
"cookie": self.cookie,
|
||
"cookie_preview": self.cookie_preview,
|
||
"started_at": self.started_at,
|
||
"finished_at": self.finished_at,
|
||
}
|
||
|
||
|
||
@dataclass
|
||
class HuyaRegisterBatch:
|
||
"""自动注册批次内存快照。"""
|
||
|
||
batch_id: str
|
||
tag: str
|
||
created_by: int
|
||
concurrency: int
|
||
wait_seconds: float
|
||
poll_interval: float
|
||
items: list[HuyaRegisterItemState]
|
||
password_prefix: str = "hy"
|
||
fixed_password: str = ""
|
||
use_proxy: bool = False
|
||
status: str = "pending"
|
||
message: str = "等待开始"
|
||
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:
|
||
# 记为 stopped 而非 error,以便「继续未完成」能接着跑
|
||
item.status = "stopped"
|
||
item.message = "服务中断,可继续"
|
||
item.finished_at = now
|
||
elif item.status == "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:
|
||
"""在后台线程中批量执行虎牙手机号自动注册。"""
|
||
|
||
def __init__(
|
||
self,
|
||
batch: HuyaRegisterBatch,
|
||
sms_lines: list[SmsLine],
|
||
proxy_config: ProxyConfigModel | None = 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()
|
||
|
||
def _create_proxy_fetcher(self) -> ProxyFetcher | None:
|
||
"""按需创建 API 代理获取器。"""
|
||
if (
|
||
not self.batch.use_proxy
|
||
or not self.proxy_config
|
||
or not self.proxy_config.enabled
|
||
):
|
||
return None
|
||
if not self.proxy_config.api_url:
|
||
return None
|
||
|
||
wl_platform = "xiequ"
|
||
wl_credentials = None
|
||
if self.proxy_config.whitelist_enabled:
|
||
wl_platform = (
|
||
getattr(self.proxy_config, "whitelist_platform", None) or "xiequ"
|
||
)
|
||
wl_credentials = getattr(self.proxy_config, "whitelist_credentials", None)
|
||
if (
|
||
not wl_credentials
|
||
and self.proxy_config.whitelist_uid
|
||
and self.proxy_config.whitelist_ukey
|
||
):
|
||
wl_credentials = {
|
||
"uid": self.proxy_config.whitelist_uid,
|
||
"ukey": self.proxy_config.whitelist_ukey,
|
||
}
|
||
|
||
return ProxyFetcher(
|
||
api_url=self.proxy_config.api_url,
|
||
whitelist_platform=wl_platform,
|
||
whitelist_credentials=wl_credentials,
|
||
stop_event=self._stop,
|
||
)
|
||
|
||
def stop(self):
|
||
self._stop.set()
|
||
with self._lock:
|
||
if self.batch.status == "running":
|
||
self.batch.message = "正在停止"
|
||
self._persist_batch_meta()
|
||
|
||
def snapshot(self) -> dict:
|
||
with self._lock:
|
||
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")
|
||
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_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 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 "",
|
||
)
|
||
finally:
|
||
db.close()
|
||
|
||
def _resolve_proxy(self) -> tuple[dict[str, str] | None, str]:
|
||
"""为单个手机号解析代理;返回代理字典和错误消息。"""
|
||
if not self.batch.use_proxy:
|
||
return None, ""
|
||
if not self.proxy_config or not self.proxy_config.enabled:
|
||
return None, "已开启代理,但代理配置未启用"
|
||
|
||
if self.proxy_config.http or self.proxy_config.https:
|
||
proxy_url = self.proxy_config.http or self.proxy_config.https
|
||
return {"http": proxy_url, "https": proxy_url}, ""
|
||
|
||
if self._shared_proxy_fetcher:
|
||
proxy_url = self._shared_proxy_fetcher.fetch_new_proxy(max_attempts=3)
|
||
if proxy_url:
|
||
return {"http": proxy_url, "https": proxy_url}, ""
|
||
return None, "获取代理失败"
|
||
|
||
return None, "已开启代理,但未配置静态代理或代理 API"
|
||
|
||
def _run_one(self, index: int, item: SmsLine):
|
||
if self._stop.is_set():
|
||
self._set_item(
|
||
index, status="stopped", message="已停止", finished_at=_now()
|
||
)
|
||
return
|
||
|
||
proxies, proxy_error = self._resolve_proxy()
|
||
if proxy_error:
|
||
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,
|
||
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,
|
||
poll_interval=self.batch.poll_interval,
|
||
password_prefix=self.batch.password_prefix,
|
||
fixed_password=self.batch.fixed_password,
|
||
proxies=proxies,
|
||
stop_event=self._stop,
|
||
)
|
||
|
||
account_id = None
|
||
username = result.username
|
||
uid = result.uid
|
||
message = result.message
|
||
status = result.status
|
||
cookie = result.cookie if result.success else ""
|
||
full_cookie = cookie
|
||
if result.success:
|
||
self._set_item(
|
||
index,
|
||
status="logging",
|
||
message="保存账号密码",
|
||
code=result.code,
|
||
change_code=result.change_code,
|
||
attempts=result.attempts,
|
||
change_attempts=result.change_attempts,
|
||
username=result.username,
|
||
uid=result.uid,
|
||
password=result.password,
|
||
password_changed=result.password_changed,
|
||
)
|
||
try:
|
||
account_id, username, uid = self._save_success(index, result)
|
||
except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
|
||
status = "error"
|
||
cookie = ""
|
||
full_cookie = ""
|
||
message = f"账号保存失败: {exc}"
|
||
|
||
exposed_cookie = "" if result.password_changed else cookie
|
||
|
||
# 先把完整 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 = 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:
|
||
ok, msg = self._shared_proxy_fetcher.warmup_whitelist()
|
||
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 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):
|
||
future.result()
|
||
except Exception as exc: # noqa: BLE001 外部接口与任务边界需要保留宽泛异常兜底
|
||
with self._lock:
|
||
self.batch.status = "error"
|
||
self.batch.message = f"批次执行异常: {exc}"
|
||
self.batch.finished_at = _now()
|
||
self._persist_batch_meta()
|
||
return
|
||
|
||
with self._lock:
|
||
if self._stop.is_set():
|
||
self.batch.status = "stopped"
|
||
self.batch.message = "批次已停止"
|
||
else:
|
||
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()
|
||
self._runners: dict[str, HuyaRegisterRunner] = {}
|
||
|
||
def create(
|
||
self,
|
||
sms_lines: list[SmsLine],
|
||
tag: str,
|
||
created_by: int,
|
||
concurrency: int,
|
||
wait_seconds: float,
|
||
poll_interval: float,
|
||
password_prefix: str = "hy",
|
||
fixed_password: str = "",
|
||
use_proxy: bool = False,
|
||
proxy_config: ProxyConfigModel | None = 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=concurrency,
|
||
wait_seconds=wait_seconds,
|
||
poll_interval=poll_interval,
|
||
password_prefix=password_prefix,
|
||
fixed_password=fixed_password,
|
||
use_proxy=bool(use_proxy),
|
||
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: ProxyConfigModel | None = None,
|
||
*,
|
||
mode: str = "continue",
|
||
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:
|
||
"""继续批次。
|
||
|
||
mode:
|
||
- continue(默认):从停止处往下跑,只处理 pending/stopped/中断中,
|
||
**不重试已失败(error)**,不重跑已成功。
|
||
- retry_failed:只重试 error。
|
||
- all_unfinished:error + pending + stopped + 中断中(旧行为)。
|
||
"""
|
||
with self._lock:
|
||
existing = self._runners.get(batch_id)
|
||
if existing and existing.batch.status == "running":
|
||
raise RuntimeError("批次正在运行中,请先停止或等待完成")
|
||
|
||
mode = (mode or "continue").strip().lower()
|
||
if mode not in {"continue", "retry_failed", "all_unfinished"}:
|
||
raise ValueError("mode 仅支持 continue / retry_failed / all_unfinished")
|
||
|
||
if mode == "continue":
|
||
target_statuses = RESUME_ITEM_STATUSES
|
||
empty_msg = "没有未完成条目可继续(已成功/已失败的不会自动重跑)"
|
||
run_label = "继续未完成"
|
||
elif mode == "retry_failed":
|
||
target_statuses = RETRY_FAILED_ITEM_STATUSES
|
||
empty_msg = "没有失败条目可重试"
|
||
run_label = "重试失败"
|
||
else:
|
||
target_statuses = RESUME_ITEM_STATUSES | RETRY_FAILED_ITEM_STATUSES
|
||
empty_msg = "没有可继续的未完成/失败条目"
|
||
run_label = "续跑未完成"
|
||
|
||
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):
|
||
status = row.status or "pending"
|
||
if status == "success":
|
||
continue
|
||
if status not in target_statuses:
|
||
continue
|
||
row.status = "pending"
|
||
row.message = "等待继续" if mode == "continue" else "等待重试"
|
||
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(empty_msg)
|
||
|
||
batch_row.status = "running"
|
||
batch_row.message = f"{run_label}({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:
|
||
pass
|
||
|
||
runner = HuyaRegisterRunner(
|
||
batch=batch,
|
||
sms_lines=sms_lines,
|
||
proxy_config=proxy_config if batch.use_proxy else None,
|
||
item_indices=retry_indices,
|
||
)
|
||
runner.batch.status = "running"
|
||
runner.batch.message = f"{run_label}({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()
|