Files
live-hub-py/web/backend/services/huya_service.py
T
yml2213 45dd7e5a74 兼容虎牙四段账号格式
支持虎牙号、密码、手机号、验证码链接导入,并补充美国等国际手机号格式处理。
2026-07-06 11:45:42 +08:00

345 lines
11 KiB
Python

"""虎牙基础业务服务。"""
import csv
import uuid
from dataclasses import dataclass
from datetime import datetime, timezone
from urllib.parse import urlparse
from sqlalchemy.orm import Session
from core.huya.cookie_utils import cookie_value, normalize_huya_cookie
from ..huya_defaults import HUYA_CONFIG_DEFAULTS, HUYA_CONFIG_FIELDS
from ..models import HuyaAccount, HuyaConfig, HuyaTask
SUPPORTED_TASK_TYPES = {
"get_bind_qr": "获取绑定二维码",
"query_points": "一键查询积分",
"query_game_name": "一键查询游戏名",
"query_exchange_records": "一键查询兑换记录",
"confirm_bind": "确认绑定",
"refresh_goods": "刷新商品列表",
"refresh_recharge_goods": "刷新充值商品列表",
"exchange_goods": "兑换商品",
"create_recharge_order": "生成支付二维码",
}
def huya_config_value(field: str, value: str | None) -> str:
"""读取配置值;空值自动回退到当前活动默认配置。"""
text = str(value or "").strip()
return text or HUYA_CONFIG_DEFAULTS[field]
def apply_huya_config_defaults(config: HuyaConfig) -> bool:
"""补齐虎牙配置默认值,返回是否发生变更。"""
changed = False
for field in HUYA_CONFIG_FIELDS:
if field == "bind_act_id" and str(getattr(config, field, "") or "").strip() == "17096":
setattr(config, field, HUYA_CONFIG_DEFAULTS[field])
changed = True
continue
normalized = huya_config_value(field, getattr(config, field, None))
if getattr(config, field, None) != normalized:
setattr(config, field, normalized)
changed = True
return changed
def _looks_like_huya_cookie(value: str) -> bool:
"""判断文本是否像虎牙 Cookie。"""
return "udb_" in value or "yyuid=" in value
def parse_huya_cookie_line(line: str) -> dict | None:
"""解析单行虎牙 CK,兼容纯 CK、账号----密码----CK、CK----手机号。"""
raw = (line or "").strip()
if not raw:
return None
parts = [part.strip() for part in raw.split("----")]
username_hint = ""
game_phone = ""
if len(parts) == 1:
cookie = raw
elif _looks_like_huya_cookie(parts[0]):
cookie = parts[0]
game_phone = parts[1] if len(parts) >= 2 else ""
elif _looks_like_huya_cookie(parts[-1]):
cookie = parts[-1]
username_hint = parts[0]
else:
return None
if not _looks_like_huya_cookie(cookie):
return None
cookie = normalize_huya_cookie(cookie)
yyuid = cookie_value(cookie, "yyuid")
uid = cookie_value(cookie, "udb_uid") or yyuid
username = cookie_value(cookie, "udb_passport") or cookie_value(cookie, "username")
if not username and username_hint:
username = username_hint
if not uid and not yyuid:
return None
return {
"uid": uid,
"yyuid": yyuid or uid,
"username": username or uid or yyuid,
"cookie": cookie,
"game_phone": game_phone,
}
@dataclass
class HuyaPasswordLine:
"""虎牙账号密码导入行。"""
username: str
password: str
cookie: str = ""
phone: str = ""
sms_url: str = ""
def _looks_like_url(value: str) -> bool:
"""判断文本是否像 URL。"""
parsed = urlparse(str(value or "").strip())
return parsed.scheme in {"http", "https"} and bool(parsed.netloc)
def split_huya_password_line(line: str) -> HuyaPasswordLine | None:
"""拆分虎牙账号密码行,兼容账号----密码----手机号----验证码链接。"""
raw = (line or "").strip()
if not raw or raw.startswith("#"):
return None
if "----" in raw:
parts = [part.strip() for part in raw.split("----")]
elif "|" in raw:
parts = [part.strip() for part in raw.split("|")]
elif "\t" in raw:
parts = [part.strip() for part in raw.split("\t")]
elif "," in raw:
parts = [part.strip() for part in next(csv.reader([raw]))]
else:
parts = raw.split()
if len(parts) < 2:
return None
username = parts[0].strip()
password = parts[1].strip()
cookie = ""
phone = ""
sms_url = ""
extra = [part.strip() for part in parts[2:] if part.strip()]
if len(extra) >= 2 and _looks_like_url(extra[1]):
phone = extra[0]
sms_url = extra[1]
cookie = "----".join(extra[2:])
elif extra and _looks_like_url(extra[-1]) and len(extra) >= 2:
phone = extra[-2]
sms_url = extra[-1]
cookie = "----".join(extra[:-2])
elif extra:
cookie = "----".join(extra)
if not username or not password:
return None
return HuyaPasswordLine(
username=username,
password=password,
cookie=cookie,
phone=phone,
sms_url=sms_url,
)
def import_huya_password_accounts(db: Session, text: str, tag: str = "") -> tuple[int, int]:
"""导入虎牙账号密码,返回 (导入/更新数, 跳过数)。"""
created_or_updated = 0
skipped = 0
tag = (tag or "").strip()
for line in (text or "").splitlines():
parsed = split_huya_password_line(line)
if not parsed:
if line.strip():
skipped += 1
continue
username = parsed.username
password = parsed.password
cookie = parsed.cookie
account = db.query(HuyaAccount).filter(HuyaAccount.username == username).first()
if account is None:
account = HuyaAccount(
uid="",
yyuid="",
username=username,
account_password=password,
cookie=normalize_huya_cookie(cookie) if cookie else "",
game_phone=parsed.phone,
tag=tag,
status="password_imported",
)
db.add(account)
else:
account.account_password = password
if cookie:
account.cookie = normalize_huya_cookie(cookie)
if parsed.phone:
account.game_phone = parsed.phone
if tag:
account.tag = tag
if not account.cookie:
account.status = "password_imported"
account.updated_at = datetime.now(timezone.utc)
created_or_updated += 1
if created_or_updated:
db.commit()
return created_or_updated, skipped
def _upsert_huya_account(db: Session, parsed: dict, tag: str = "", status: str | None = None) -> HuyaAccount:
"""按 uid/yyuid 新增或更新虎牙账号。"""
account = None
if parsed["uid"]:
account = db.query(HuyaAccount).filter(HuyaAccount.uid == parsed["uid"]).first()
if account is None and parsed["yyuid"]:
account = db.query(HuyaAccount).filter(HuyaAccount.yyuid == parsed["yyuid"]).first()
if account is None:
account = HuyaAccount(
uid=parsed["uid"],
yyuid=parsed["yyuid"],
username=parsed["username"],
cookie=parsed["cookie"],
game_phone=parsed["game_phone"],
tag=tag,
status=status or "imported",
)
db.add(account)
else:
account.uid = parsed["uid"] or account.uid
account.yyuid = parsed["yyuid"] or account.yyuid
account.username = parsed["username"] or account.username
account.cookie = parsed["cookie"]
account.game_phone = parsed["game_phone"] or account.game_phone
if tag:
account.tag = tag
account.status = status or "updated"
account.updated_at = datetime.now(timezone.utc)
return account
def save_huya_login_cookie_to_account(
db: Session,
account: HuyaAccount,
cookie: str,
tag: str = "",
username_hint: str = "",
) -> HuyaAccount:
"""把登录成功后的 Cookie 回填到指定虎牙账号。"""
line = f"{username_hint}----{cookie}" if username_hint else cookie
parsed = parse_huya_cookie_line(line)
if not parsed:
raise ValueError("登录成功但 Cookie 中没有识别到虎牙 uid")
account.uid = parsed["uid"] or account.uid
account.yyuid = parsed["yyuid"] or account.yyuid
account.username = parsed["username"] or account.username
account.cookie = parsed["cookie"]
account.game_phone = parsed["game_phone"] or account.game_phone
if tag:
account.tag = tag
account.status = "login_success"
account.updated_at = datetime.now(timezone.utc)
db.commit()
db.refresh(account)
return account
def upsert_huya_cookie(db: Session, cookie: str, tag: str = "", username_hint: str = "") -> HuyaAccount:
"""保存单条登录得到的虎牙 Cookie。"""
line = f"{username_hint}----{cookie}" if username_hint else cookie
parsed = parse_huya_cookie_line(line)
if not parsed:
raise ValueError("登录成功但 Cookie 中没有识别到虎牙 uid")
account = _upsert_huya_account(db, parsed, tag=(tag or "").strip(), status="login_success")
db.commit()
db.refresh(account)
return account
def import_huya_cookies(db: Session, text: str, tag: str = "") -> tuple[int, int]:
"""导入虎牙 Cookie,返回 (成功数, 跳过数)。"""
created_or_updated = 0
skipped = 0
tag = (tag or "").strip()
for line in (text or "").splitlines():
parsed = parse_huya_cookie_line(line)
if not parsed:
skipped += 1
continue
_upsert_huya_account(db, parsed, tag=tag)
created_or_updated += 1
if created_or_updated:
db.commit()
return created_or_updated, skipped
def ensure_huya_config(db: Session) -> HuyaConfig:
"""获取单条虎牙配置,不存在则创建。"""
config = db.query(HuyaConfig).first()
if config:
if apply_huya_config_defaults(config):
db.commit()
db.refresh(config)
return config
config = HuyaConfig(**HUYA_CONFIG_DEFAULTS)
db.add(config)
db.commit()
db.refresh(config)
return config
def create_planned_tasks(
db: Session,
account_ids: list[int],
task_type: str,
created_by: int,
payload: dict | None = None,
) -> tuple[str, int]:
"""创建虎牙任务记录,等待后台执行器消费。"""
if task_type not in SUPPORTED_TASK_TYPES:
raise ValueError("不支持的任务类型")
batch_id = uuid.uuid4().hex[:12]
payload = payload or {}
accounts = db.query(HuyaAccount).filter(HuyaAccount.id.in_(account_ids)).all()
if task_type in {"refresh_goods", "refresh_recharge_goods", "exchange_goods", "create_recharge_order"} and accounts:
# 全局快照、单笔兑换和单笔支付二维码都使用一个选中的 CK 即可。
accounts = accounts[:1]
for account in accounts:
db.add(HuyaTask(
batch_id=batch_id,
account_id=account.id,
task_type=task_type,
status="planned",
message="任务已创建,等待执行",
result={"payload": payload} if payload else None,
created_by=created_by,
))
db.commit()
return batch_id, len(accounts)