From b58c6b43577a661d4a0ddda6f16225c495de6d59 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 31 Aug 2026 10:28:14 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=A7=E8=A1=8C=20Ruff=20=E5=AE=89=E5=85=A8?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/douyu/__init__.py | 20 +- core/douyu/activity_client.py | 4 +- core/douyu/crypto.py | 6 +- core/douyu/email_verifier.py | 25 +- core/douyu/login.py | 50 ++-- core/douyu/login_api.py | 3 +- core/douyu/proxy_fetcher.py | 1 + core/douyu/proxy_parser.py | 3 +- core/douyu/proxy_platforms/__init__.py | 4 +- core/douyu/proxy_platforms/base.py | 7 +- core/douyu/proxy_resolver.py | 19 +- core/douyu/proxy_verifier.py | 3 +- core/douyu/proxy_whitelist.py | 7 +- core/douyu/recharge_api.py | 13 +- core/geetest/common/crypto.py | 5 +- core/geetest/common/imaging.py | 4 +- core/geetest/common/network.py | 25 +- core/geetest/common/performance.py | 5 +- core/geetest/common/trajectory.py | 6 +- core/geetest/v3_slide/solver.py | 30 ++- core/huya/__init__.py | 14 +- core/huya/account_env.py | 3 +- core/huya/app_login.py | 4 +- core/huya/cert_forge.py | 1 - core/huya/change_password.py | 1 - core/huya/cookie_utils.py | 1 - core/huya/envelope_forge.py | 10 +- core/huya/frame_decoder.py | 11 +- core/huya/http_client.py | 14 +- core/huya/login.py | 5 +- core/huya/shop_structs.py | 28 +- core/huya/sms_login.py | 3 +- core/huya/taf_protocol.py | 20 +- core/huya/verification/ocr.py | 1 - core/huya/verification/solver.py | 3 +- core/huya/wss_client.py | 24 +- core/huya/wup_encoder.py | 8 +- core/huya/wup_protocol.py | 19 +- core/sms_provider.py | 1 - scripts/migrate_sqlite_to_mysql.py | 3 +- services/yyb-worker/runtime/main.py | 15 +- services/yyb-worker/runtime/pyvm/__init__.py | 18 +- services/yyb-worker/runtime/pyvm/algorithm.py | 7 +- services/yyb-worker/runtime/pyvm/goods.py | 5 +- services/yyb-worker/runtime/pyvm/mall.py | 5 +- .../yyb-worker/runtime/pyvm/pagedoo_vm.py | 73 +++-- services/yyb-worker/runtime/pyvm/protocol.py | 1 - services/yyb-worker/runtime/pyvm/session.py | 2 +- .../yyb-worker/runtime/scripts/jsdom-pay.py | 11 +- .../yyb-worker/runtime/scripts/login-qq.py | 6 +- .../runtime/scripts/login-wechat.py | 6 +- .../runtime/scripts/select-peace-elite.py | 2 +- .../yyb-worker/runtime/scripts/yyb-worker.py | 10 +- .../runtime/tests/test_jsdom_pay.py | 3 +- tests/test_account_sensitive_fields.py | 3 +- tests/test_audit_logs.py | 3 +- tests/test_cookie_custom_order.py | 4 +- tests/test_cookie_operations.py | 14 +- tests/test_douyu_elite_exchange_flow.py | 4 +- tests/test_douyu_elite_lock_exchange.py | 2 +- tests/test_douyu_gold_recharge_channel.py | 4 +- tests/test_douyu_mobile_bind_skip.py | 2 +- tests/test_douyu_runner_proxy.py | 4 +- tests/test_douyu_workbench_scopes.py | 1 - tests/test_douyu_xpd_exchange.py | 3 +- tests/test_fish_fin_recharge_api.py | 4 +- tests/test_huya_app_login.py | 26 +- tests/test_logger.py | 2 +- tests/test_migrations.py | 2 +- utils/logger.py | 7 +- web/backend/crypto_storage.py | 3 +- web/backend/database.py | 3 +- web/backend/deps.py | 18 +- web/backend/main.py | 28 +- web/backend/migrations/env.py | 8 +- .../20260623_0001_initial_web_schema.py | 10 +- .../20260624_0002_sensitive_columns_text.py | 11 +- .../versions/20260624_0003_proxy_platform.py | 11 +- .../versions/20260704_0004_huya_foundation.py | 11 +- .../20260704_0005_huya_recharge_goods.py | 11 +- .../20260705_0006_huya_password_accounts.py | 11 +- .../20260712_0007_huya_register_history.py | 11 +- .../20260724_0008_douyu_activity_tasks.py | 11 +- ...60725_0009_database_performance_indexes.py | 11 +- .../20260728_0010_douyu_esports_handbook.py | 11 +- ...60728_0011_douyu_esports_account_fields.py | 11 +- ...0728_0012_douyu_esports_can_change_time.py | 11 +- ...20260728_0013_douyu_esports_gift_config.py | 11 +- ...60728_0014_douyu_esports_goods_snapshot.py | 11 +- .../20260805_0015_task_status_indexes.py | 11 +- .../20260806_0016_douyu_peace_shop.py | 11 +- .../20260807_0017_douyu_xpd_fragments.py | 11 +- .../20260807_0018_cookie_check_fields.py | 11 +- ...60808_0019_dedupe_douyu_success_cookies.py | 11 +- .../versions/20260812_0020_yyb_recharge.py | 11 +- .../20260812_0021_yyb_qr_mediumtext.py | 10 +- .../20260812_0022_yyb_payment_meta.py | 11 +- .../20260813_0023_soft_delete_users.py | 11 +- .../20260813_0024_douyu_workbench_scopes.py | 11 +- ...260813_0025_douyu_workbench_empty_state.py | 11 +- ...260813_0026_douyu_gold_recharge_channel.py | 11 +- .../20260813_0027_douyu_supplier_order_id.py | 11 +- .../20260814_0028_douyu_nickname_recharge.py | 11 +- .../20260814_0029_audit_log_result.py | 11 +- .../20260829_0030_huya_login_channel.py | 11 +- .../20260830_0031_mysql_task_query_indexes.py | 11 +- web/backend/models.py | 17 +- web/backend/routers/account_check.py | 4 +- web/backend/routers/accounts.py | 12 +- web/backend/routers/audit.py | 1 - web/backend/routers/auth.py | 10 +- web/backend/routers/cookies.py | 24 +- web/backend/routers/dashboard.py | 1 - web/backend/routers/douyu.py | 31 ++- web/backend/routers/huya.py | 48 ++-- web/backend/routers/login.py | 13 +- web/backend/routers/proxy.py | 18 +- web/backend/routers/users.py | 12 +- web/backend/routers/yyb.py | 3 +- web/backend/schemas.py | 253 +++++++++--------- web/backend/security.py | 10 +- web/backend/services/account_check_service.py | 10 +- web/backend/services/account_service.py | 1 - web/backend/services/audit_service.py | 1 - web/backend/services/cookie_check_service.py | 5 +- web/backend/services/douyu_runner.py | 12 +- web/backend/services/douyu_runner_bind.py | 34 +-- web/backend/services/douyu_runner_core.py | 23 +- web/backend/services/douyu_runner_donate.py | 14 +- web/backend/services/douyu_runner_gold.py | 26 +- web/backend/services/douyu_runner_goods.py | 32 +-- web/backend/services/douyu_runner_manual.py | 32 +-- web/backend/services/douyu_runner_xpd.py | 28 +- web/backend/services/douyu_service.py | 7 +- web/backend/services/huya_register_runner.py | 18 +- web/backend/services/huya_runner.py | 17 +- web/backend/services/huya_runner_bind.py | 19 +- web/backend/services/huya_runner_core.py | 16 +- web/backend/services/huya_runner_goods.py | 19 +- web/backend/services/huya_runner_recharge.py | 17 +- web/backend/services/huya_service.py | 11 +- web/backend/services/login_service.py | 43 +-- web/backend/services/proxy_service.py | 12 +- web/backend/services/yyb_service.py | 10 +- web/backend/version.py | 7 +- 145 files changed, 940 insertions(+), 993 deletions(-) diff --git a/core/douyu/__init__.py b/core/douyu/__init__.py index c9180aa..b1a85d1 100644 --- a/core/douyu/__init__.py +++ b/core/douyu/__init__.py @@ -1,11 +1,11 @@ """斗鱼登录模块""" -from .login import DouyuLogin, CredentialError -from .login_api import LoginAPIStrategy -from .login_api_wgapi import WgapiLoginAPI -from .login_api_iframe import IframeLoginAPI -from .email_verifier import EmailVerifier from .activity_client import DouyuActivityClient, DouyuActivityError +from .email_verifier import EmailVerifier +from .login import CredentialError, DouyuLogin +from .login_api import LoginAPIStrategy +from .login_api_iframe import IframeLoginAPI +from .login_api_wgapi import WgapiLoginAPI from .recharge_api import ( FishFinRechargeClient, FishFinRechargeConfig, @@ -14,16 +14,16 @@ from .recharge_api import ( ) __all__ = [ - "DouyuLogin", "CredentialError", - "LoginAPIStrategy", - "WgapiLoginAPI", - "IframeLoginAPI", - "EmailVerifier", "DouyuActivityClient", "DouyuActivityError", + "DouyuLogin", + "EmailVerifier", "FishFinRechargeClient", "FishFinRechargeConfig", "FishFinRechargeConfigError", "FishFinRechargeError", + "IframeLoginAPI", + "LoginAPIStrategy", + "WgapiLoginAPI", ] diff --git a/core/douyu/activity_client.py b/core/douyu/activity_client.py index 92b89c2..816dcd3 100644 --- a/core/douyu/activity_client.py +++ b/core/douyu/activity_client.py @@ -18,7 +18,6 @@ from .cookie_utils import ( normalize_douyu_cookie, ) - PC_UA = ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " @@ -1446,8 +1445,7 @@ class DouyuActivityClient: if not match: raise DouyuActivityError(f"响应中找不到 var {varname}=: {text[:200]}") chunk = text[match.end() :].strip() - if chunk.endswith(";"): - chunk = chunk[:-1] + chunk = chunk.removesuffix(";") try: return json.loads(chunk) except json.JSONDecodeError as exc: diff --git a/core/douyu/crypto.py b/core/douyu/crypto.py index c4c0e92..847a9fd 100644 --- a/core/douyu/crypto.py +++ b/core/douyu/crypto.py @@ -2,11 +2,9 @@ import base64 import hashlib -from Crypto.PublicKey import RSA -from Crypto.Cipher import PKCS1_v1_5 -from Crypto.Cipher import AES -from Crypto.Cipher import ARC4 +from Crypto.Cipher import AES, ARC4, PKCS1_v1_5 +from Crypto.PublicKey import RSA # 斗鱼RSA公钥(从JS中提取) DOUYU_RSA_PUBLIC_KEY = """-----BEGIN PUBLIC KEY----- diff --git a/core/douyu/email_verifier.py b/core/douyu/email_verifier.py index 23825bd..b28882a 100644 --- a/core/douyu/email_verifier.py +++ b/core/douyu/email_verifier.py @@ -6,11 +6,9 @@ import re import threading import time from datetime import datetime, timedelta -from typing import Optional -from loguru import logger import requests - +from loguru import logger # Roundcube Webmail 地址。可通过环境变量覆盖。空值时回退到默认。 ROUNDCUBE_URL = os.getenv("MAIL_ROUNDCUBE_URL") or "http://111.229.206.54:8000/" @@ -40,7 +38,7 @@ class EmailVerifier: max_messages: int = 20, use_ssl: bool = False, roundcube_url: str = "", - backup_passwords: Optional[list[str]] = None, + backup_passwords: list[str] | None = None, ): # 兼容旧参数名 self.imap_server = imap_server @@ -54,14 +52,13 @@ class EmailVerifier: ) # Roundcube 会话(懒初始化) - self._rc_session: Optional[requests.Session] = None - self._rc_token: Optional[str] = None + self._rc_session: requests.Session | None = None + self._rc_token: str | None = None self._rc_logged_in = False self._rc_login_error = "" def connect(self) -> None: """兼容接口""" - pass def disconnect(self) -> None: """关闭 Roundcube 会话""" @@ -239,7 +236,7 @@ class EmailVerifier: # ── Roundcube 读取邮件内容 ───────────────────────────── - def _rc_fetch_email_body(self, uid: int) -> Optional[str]: + def _rc_fetch_email_body(self, uid: int) -> str | None: """通过 Roundcube 读取指定 UID 邮件的正文""" if not self._ensure_roundcube_session(): if self._rc_login_error: @@ -262,7 +259,7 @@ class EmailVerifier: # ── Roundcube 日期解析 ───────────────────────────────── @staticmethod - def _parse_rc_date(date_str: str) -> Optional[datetime]: + def _parse_rc_date(date_str: str) -> datetime | None: """ 解析 Roundcube 返回的日期字符串为 datetime。 @@ -363,9 +360,9 @@ class EmailVerifier: self, max_wait: int = 60, interval: int = 3, - after_timestamp: Optional[float] = None, + after_timestamp: float | None = None, allow_old_seconds: int = 15, - stop_event: Optional[threading.Event] = None, + stop_event: threading.Event | None = None, ) -> str: """ 轮询获取斗鱼验证码。 @@ -417,9 +414,9 @@ class EmailVerifier: def _fetch_code_via_roundcube( self, - after_timestamp: Optional[float] = None, + after_timestamp: float | None = None, allow_old_seconds: int = 15, - ) -> Optional[str]: + ) -> str | None: """通过 Roundcube API 获取最新斗鱼验证码""" messages = self._rc_fetch_mail_list() @@ -465,7 +462,7 @@ class EmailVerifier: # ── 通用工具方法 ─────────────────────────────────────── - def _extract_verification_code(self, text: str) -> Optional[str]: + def _extract_verification_code(self, text: str) -> str | None: """从文本中提取6位验证码""" # 清理HTML标签和实体 text = html.unescape(text) diff --git a/core/douyu/login.py b/core/douyu/login.py index 9539e49..605f993 100644 --- a/core/douyu/login.py +++ b/core/douyu/login.py @@ -4,28 +4,30 @@ import json import re import threading import time -import requests -from typing import Mapping, Optional, Protocol, Tuple +from collections.abc import Mapping +from typing import Protocol from urllib.parse import urlsplit, urlunsplit + +import requests from loguru import logger +from core.geetest.common.network import ( + get_c_s, + get_js_address, + req_fullpage_validate, +) +from core.geetest.v3_slide.solver import ( + _generate_seed, + get_w1, + get_w2, +) + from .cookie_enricher import CookieEnricher from .email_verifier import EmailLoginError, EmailVerifier from .login_api import LoginAPIStrategy from .login_api_wgapi import WgapiLoginAPI from .proxy_fetcher import ProxyFetcher -from core.geetest.v3_slide.solver import ( - _generate_seed, - get_w1, - get_w2, -) -from core.geetest.common.network import ( - get_js_address, - get_c_s, - req_fullpage_validate, -) - # ── 全局极验并发限制:同一时刻最多2个线程做极验验证 ── _geetest_semaphore = threading.Semaphore(2) @@ -95,14 +97,14 @@ class DouyuLogin: def __init__( self, account: AccountLike, - proxy: Optional[str | Mapping[str, str]] = None, - proxy_api_url: Optional[str] = None, + proxy: str | Mapping[str, str] | None = None, + proxy_api_url: str | None = None, timeout: tuple[float, float] = REQUEST_TIMEOUT, max_login_retries: int = 0, max_total_time: float = 0, - proxy_fetcher: Optional[ProxyFetcher] = None, - stop_event: Optional[threading.Event] = None, - api_strategy: Optional[LoginAPIStrategy] = None, + proxy_fetcher: ProxyFetcher | None = None, + stop_event: threading.Event | None = None, + api_strategy: LoginAPIStrategy | None = None, ): self.account = account self.proxy = proxy @@ -117,7 +119,7 @@ class DouyuLogin: ProxyFetcher(api_url=proxy_api_url) if proxy_api_url else None ) - self._current_proxy_url: Optional[str] = None + self._current_proxy_url: str | None = None self._cookie_enrich_error = "" self._static_retry_count = 0 self._setup_session() @@ -565,7 +567,7 @@ class DouyuLogin: ) return True - def _first_login(self) -> Tuple[str, str, str, dict]: + def _first_login(self) -> tuple[str, str, str, dict]: """ 第一次登录,获取极验参数 @@ -608,7 +610,7 @@ class DouyuLogin: def _solve_geetest( self, gt: str, challenge: str, deadline: float = 0 - ) -> Tuple[str, str]: + ) -> tuple[str, str]: """ 解决极验 fullpage 验证(最多3次尝试,失败直接抛异常回到login换新代理) @@ -635,7 +637,7 @@ class DouyuLogin: def _solve_geetest_inner( self, gt: str, challenge: str, deadline: float = 0 - ) -> Tuple[str, str]: + ) -> tuple[str, str]: """极验验证内部实现:最多3次尝试,失败直接抛异常。""" _MAX_ATTEMPTS = 3 @@ -652,7 +654,7 @@ class DouyuLogin: # 超时兜底 if deadline and time.monotonic() > deadline: - raise ValueError(f"极验验证超时(登录整体时间耗尽)") + raise ValueError("极验验证超时(登录整体时间耗尽)") try: logger.info(f"极验验证尝试 {attempt}/{_MAX_ATTEMPTS}") @@ -825,7 +827,7 @@ class DouyuLogin: logger.info("验证邮件已发送") - def _get_email_code(self, after_timestamp: Optional[float] = None) -> str: + def _get_email_code(self, after_timestamp: float | None = None) -> str: """获取邮箱验证码""" verifier = EmailVerifier( imap_server=self.account.email_imap_server, diff --git a/core/douyu/login_api.py b/core/douyu/login_api.py index b1ef525..a4c8997 100644 --- a/core/douyu/login_api.py +++ b/core/douyu/login_api.py @@ -1,7 +1,6 @@ """登录接口策略基类""" from abc import ABC, abstractmethod -from typing import Tuple class LoginAPIStrategy(ABC): @@ -78,7 +77,7 @@ class LoginAPIStrategy(ABC): """构建提交验证码参数""" ... - def extract_geetest_params(self, payload: dict) -> Tuple[str, str, str]: + def extract_geetest_params(self, payload: dict) -> tuple[str, str, str]: """从第一次登录响应提取极验参数 Args: diff --git a/core/douyu/proxy_fetcher.py b/core/douyu/proxy_fetcher.py index 42d1a1e..f4439c3 100644 --- a/core/douyu/proxy_fetcher.py +++ b/core/douyu/proxy_fetcher.py @@ -10,6 +10,7 @@ import threading import time + from loguru import logger from .proxy_resolver import ProxyResolver diff --git a/core/douyu/proxy_parser.py b/core/douyu/proxy_parser.py index d490c95..5aa5709 100644 --- a/core/douyu/proxy_parser.py +++ b/core/douyu/proxy_parser.py @@ -2,10 +2,9 @@ import json import re -from typing import Optional -def parse_proxy_response(text: str) -> tuple[list[str], Optional[str]]: +def parse_proxy_response(text: str) -> tuple[list[str], str | None]: """ 解析代理 API 响应,支持 JSON 格式与旧版纯文本格式。 diff --git a/core/douyu/proxy_platforms/__init__.py b/core/douyu/proxy_platforms/__init__.py index bdd3ae8..13f64b5 100644 --- a/core/douyu/proxy_platforms/__init__.py +++ b/core/douyu/proxy_platforms/__init__.py @@ -13,7 +13,7 @@ from .xiequ import XiequAdapter from .xkdaili import XkdailiAdapter # ── 平台注册表 ── -_PLATFORM_REGISTRY: dict[str, Type[BaseWhitelistAdapter]] = { +_PLATFORM_REGISTRY: dict[str, type[BaseWhitelistAdapter]] = { "xiequ": XiequAdapter, "xkdaili": XkdailiAdapter, } @@ -49,7 +49,7 @@ _PLATFORM_LABELS: dict[str, str] = { } -def create_adapter(platform: str, credentials: dict) -> Optional[BaseWhitelistAdapter]: +def create_adapter(platform: str, credentials: dict) -> BaseWhitelistAdapter | None: """根据平台标识符和凭据创建适配器实例。""" cls = _PLATFORM_REGISTRY.get(platform) if not cls: diff --git a/core/douyu/proxy_platforms/base.py b/core/douyu/proxy_platforms/base.py index 7f74e60..30d3f96 100644 --- a/core/douyu/proxy_platforms/base.py +++ b/core/douyu/proxy_platforms/base.py @@ -8,7 +8,6 @@ import re import threading import time from abc import ABC, abstractmethod -from typing import Optional import requests from loguru import logger @@ -144,12 +143,12 @@ class BaseWhitelistAdapter(ABC): return False, msg @staticmethod - def get_local_exit_ip() -> Optional[str]: + def get_local_exit_ip() -> str | None: """获取本机当前公网出口IP(不走代理)。""" return _get_local_exit_ip() -def _get_local_exit_ip() -> Optional[str]: +def _get_local_exit_ip() -> str | None: """获取本机当前公网出口IP(不走代理)。""" targets = [ "https://myip.ipip.net", @@ -171,7 +170,7 @@ def _get_local_exit_ip() -> Optional[str]: return None -def get_exit_ip_via_proxy(proxy: str) -> Optional[str]: +def get_exit_ip_via_proxy(proxy: str) -> str | None: """通过代理获取出口IP。""" targets = [ "https://qifu-api.baidubce.com/ip/local/geo/v1/district", diff --git a/core/douyu/proxy_resolver.py b/core/douyu/proxy_resolver.py index 131c9fc..ade9eff 100644 --- a/core/douyu/proxy_resolver.py +++ b/core/douyu/proxy_resolver.py @@ -2,7 +2,8 @@ import threading import time -from typing import Callable, Optional, Protocol +from collections.abc import Callable +from typing import Protocol import requests from loguru import logger @@ -19,7 +20,7 @@ class WhitelistSyncer(Protocol): def sync_ip(self, ip: str) -> tuple[bool, str]: ... - def get_local_exit_ip(self) -> Optional[str]: ... + def get_local_exit_ip(self) -> str | None: ... class ProxyResolver: @@ -28,11 +29,11 @@ class ProxyResolver: def __init__( self, api_url: str, - whitelist_syncer: Optional[WhitelistSyncer] = None, - log_func: Optional[LogFunc] = None, + whitelist_syncer: WhitelistSyncer | None = None, + log_func: LogFunc | None = None, sync_local_exit_ip: bool = False, sync_whitelist_once: bool = True, - stop_event: Optional[threading.Event] = None, + stop_event: threading.Event | None = None, ): self.api_url = api_url self.whitelist_syncer = whitelist_syncer @@ -40,7 +41,7 @@ class ProxyResolver: self.sync_local_exit_ip = sync_local_exit_ip self.sync_whitelist_once = sync_whitelist_once self.stop_event = stop_event - self._last_synced_ip: Optional[str] = None + self._last_synced_ip: str | None = None self._has_synced_whitelist = False def _is_stopped(self) -> bool: @@ -95,7 +96,7 @@ class ProxyResolver: self, max_attempts: int = 4, return_all: bool = False, - ) -> tuple[Optional[str | list[str]], str]: + ) -> tuple[str | list[str] | None, str]: """ 获取并验证代理。 @@ -178,8 +179,8 @@ def resolve_working_proxy( whitelist_platform: str = "xiequ", whitelist_credentials: dict | None = None, max_attempts: int = 4, - log_func: Optional[LogFunc] = None, -) -> tuple[Optional[str], str]: + log_func: LogFunc | None = None, +) -> tuple[str | None, str]: """从代理 API 获取可用代理,自动处理白名单同步。""" syncer = ( DouyuWhitelistSyncer( diff --git a/core/douyu/proxy_verifier.py b/core/douyu/proxy_verifier.py index d9d5f64..8f50ffe 100644 --- a/core/douyu/proxy_verifier.py +++ b/core/douyu/proxy_verifier.py @@ -1,7 +1,6 @@ """代理可用性验证。""" from concurrent.futures import ThreadPoolExecutor, as_completed -from typing import Optional import requests from loguru import logger @@ -45,7 +44,7 @@ def verify_proxies_concurrent( timeout: tuple = (5, 8), max_workers: int = 5, return_all: bool = False, -) -> tuple[Optional[str | list[str]], str]: +) -> tuple[str | list[str] | None, str]: """ 并发验证多个代理 URL。 diff --git a/core/douyu/proxy_whitelist.py b/core/douyu/proxy_whitelist.py index 1ff42de..3a2d5f8 100644 --- a/core/douyu/proxy_whitelist.py +++ b/core/douyu/proxy_whitelist.py @@ -1,6 +1,5 @@ """代理模块使用的白名单适配器。""" -from typing import Optional from .proxy_platforms import create_adapter from .proxy_platforms.base import BaseWhitelistAdapter, _get_local_exit_ip @@ -17,7 +16,7 @@ class DouyuWhitelistSyncer: def __init__( self, platform: str = "xiequ", - credentials: Optional[dict] = None, + credentials: dict | None = None, uid: str = "", ukey: str = "", ): @@ -28,7 +27,7 @@ class DouyuWhitelistSyncer: self.platform = platform self.credentials = credentials or {} - self._adapter: Optional[BaseWhitelistAdapter] = None + self._adapter: BaseWhitelistAdapter | None = None if self.credentials: self._adapter = create_adapter(platform, self.credentials) @@ -38,5 +37,5 @@ class DouyuWhitelistSyncer: return False, "未配置白名单凭据" return self._adapter.sync_ip(ip) - def get_local_exit_ip(self) -> Optional[str]: + def get_local_exit_ip(self) -> str | None: return _get_local_exit_ip() diff --git a/core/douyu/recharge_api.py b/core/douyu/recharge_api.py index bafce9a..0df75d6 100644 --- a/core/douyu/recharge_api.py +++ b/core/douyu/recharge_api.py @@ -6,9 +6,10 @@ import hashlib import json import os import time +from collections.abc import Callable, Mapping from dataclasses import dataclass from decimal import Decimal, InvalidOperation -from typing import Any, Callable, Mapping +from typing import Any import requests @@ -34,7 +35,7 @@ class FishFinRechargeConfig: debug: bool = False @classmethod - def from_env(cls) -> "FishFinRechargeConfig": + def from_env(cls) -> FishFinRechargeConfig: """从环境变量读取配置,不在代码或数据库中保存商户密钥。""" timeout = float(os.getenv("FISH_FIN_RECHARGE_TIMEOUT", "20")) return cls( @@ -189,7 +190,7 @@ class FishFinRechargeClient: "sign_params": sign_params, # 不把可重放签名原文或 AppSecret 写入日志,只记录待签名串摘要。 "sign_source_digest": hashlib.sha256( - f"{sign_query}{method.upper()}".encode("utf-8") + f"{sign_query}{method.upper()}".encode() ).hexdigest()[:12], } ) @@ -240,7 +241,7 @@ class FishFinRechargeClient: return payload @staticmethod - def _amount(value: Decimal | int | float | str) -> str: + def _amount(value: Decimal | float | str) -> str: """规范化金额,避免浮点数表达式进入签名或订单请求。""" try: price = Decimal(str(value)) @@ -251,7 +252,7 @@ class FishFinRechargeClient: return format(price.normalize(), "f") @classmethod - def _json_amount(cls, value: Decimal | int | float | str) -> int | float: + def _json_amount(cls, value: Decimal | float | str) -> int | float: """按文档以 JSON 数字发送金额,整数不附带无意义的小数位。""" amount_text = cls._amount(value) return int(amount_text) if "." not in amount_text else float(amount_text) @@ -267,7 +268,7 @@ class FishFinRechargeClient: self, *, buy_num: int, - pay_amount: Decimal | int | float | str, + pay_amount: Decimal | float | str, out_order_id: str, product_id: str, recharge_arg: list[dict[str, Any]], diff --git a/core/geetest/common/crypto.py b/core/geetest/common/crypto.py index 99bd409..cd1a347 100644 --- a/core/geetest/common/crypto.py +++ b/core/geetest/common/crypto.py @@ -1,8 +1,9 @@ -import random import hashlib +import random from typing import Any + +from Crypto.Cipher import AES, PKCS1_v1_5 from Crypto.PublicKey import RSA -from Crypto.Cipher import PKCS1_v1_5, AES # 随机产生4个字符组成的字符串 diff --git a/core/geetest/common/imaging.py b/core/geetest/common/imaging.py index 2e67e01..75d97c3 100644 --- a/core/geetest/common/imaging.py +++ b/core/geetest/common/imaging.py @@ -1,8 +1,8 @@ -import requests import cv2 import numpy as np -from PIL import Image +import requests from loguru import logger +from PIL import Image REQUEST_TIMEOUT = (3.05, 12) diff --git a/core/geetest/common/network.py b/core/geetest/common/network.py index ce2d41a..a1dc848 100644 --- a/core/geetest/common/network.py +++ b/core/geetest/common/network.py @@ -1,8 +1,9 @@ -import time -import requests import json import re -from typing import Mapping, Optional, Tuple +import time +from collections.abc import Mapping + +import requests from loguru import logger REQUEST_TIMEOUT = (10, 30) @@ -12,9 +13,9 @@ PASSPORT_REFERER = "https://passport.douyu.com/" def _get( url: str, *, - params: Optional[dict] = None, - headers: Optional[dict] = None, - proxies: Optional[Mapping[str, str]] = None, + params: dict | None = None, + headers: dict | None = None, + proxies: Mapping[str, str] | None = None, ) -> requests.Response: """发送极验 GET 请求,确保使用同一个代理出口。""" return requests.get( @@ -55,7 +56,7 @@ def _parse_json_response(response: requests.Response, source: str) -> dict: raise ValueError(f"{source} 返回的不是有效 JSON: {preview}") from exc -def get_challenge_gt_bak() -> Tuple[str, str]: +def get_challenge_gt_bak() -> tuple[str, str]: headers = { "accept": "application/json, text/javascript, */*; q=0.01", "accept-language": "zh-CN,zh;q=0.9", @@ -85,7 +86,7 @@ def get_challenge_gt_bak() -> Tuple[str, str]: return data["gt"], data["challenge"] -def get_challenge_gt() -> Tuple[str, str]: +def get_challenge_gt() -> tuple[str, str]: headers = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36", "Accept": "application/json, text/javascript, */*; q=0.01", @@ -116,7 +117,7 @@ def get_challenge_gt() -> Tuple[str, str]: raise ValueError(f"斗鱼登录接口返回中缺少极验参数: {preview}") from exc -def get_js_address(gt: str, proxies: Optional[Mapping[str, str]] = None) -> dict: +def get_js_address(gt: str, proxies: Mapping[str, str] | None = None) -> dict: headers = { "accept": "*/*", "accept-language": "zh-CN,zh;q=0.9", @@ -149,8 +150,8 @@ def get_c_s( gt: str, challenge: str, w: str, - proxies: Optional[Mapping[str, str]] = None, -) -> Tuple[list[int], str]: + proxies: Mapping[str, str] | None = None, +) -> tuple[list[int], str]: headers = { "accept": "*/*", "accept-language": "zh-CN,zh;q=0.9", @@ -184,7 +185,7 @@ def req_fullpage_validate( gt: str, challenge: str, w: str, - proxies: Optional[Mapping[str, str]] = None, + proxies: Mapping[str, str] | None = None, ) -> dict: """HAR 中的 fullpage 最终校验,成功后直接返回 validate。""" headers = { diff --git a/core/geetest/common/performance.py b/core/geetest/common/performance.py index 59154ac..7eea0e1 100644 --- a/core/geetest/common/performance.py +++ b/core/geetest/common/performance.py @@ -1,9 +1,8 @@ -import time import random -from typing import Optional +import time -def generate_fake_performance_timing(base_time: Optional[int] = None) -> dict[str, int]: +def generate_fake_performance_timing(base_time: int | None = None) -> dict[str, int]: """ 生成伪造的浏览器性能时间戳数据 diff --git a/core/geetest/common/trajectory.py b/core/geetest/common/trajectory.py index bc71185..6fde972 100644 --- a/core/geetest/common/trajectory.py +++ b/core/geetest/common/trajectory.py @@ -1,6 +1,6 @@ -import random import math -from typing import Any, Optional +import random +from typing import Any # 生成类人的鼠标轨迹 @@ -91,7 +91,7 @@ def generate_realistic_trajectory( # 处理原始轨迹数组 def process_mouse_trajectory( - events: list[Any], max_records: Optional[int] = None + events: list[Any], max_records: int | None = None ) -> dict[str, Any]: """ 处理鼠标/触摸轨迹数据,将绝对坐标转换为相对坐标和时间差 diff --git a/core/geetest/v3_slide/solver.py b/core/geetest/v3_slide/solver.py index 8cc9694..79f0ab6 100644 --- a/core/geetest/v3_slide/solver.py +++ b/core/geetest/v3_slide/solver.py @@ -1,35 +1,37 @@ -import time -import random import json +import random +import time + from loguru import logger -from core.geetest.common.trajectory import ( - generate_realistic_trajectory, - process_mouse_trajectory, - compress_trajectory, - TrajectoryEncoder, - H, -) + from core.geetest.common.crypto import ( - four_random_chart, - RSA_jiami_r, AES_O, - geetest_base64_encode, + RSA_jiami_r, encrypt_string, + four_random_chart, + geetest_base64_encode, simple_md5, ) from core.geetest.common.imaging import download_picture from core.geetest.common.network import ( + get_c_s, get_challenge_gt, get_js_address, - get_c_s, - req_slide, get_picture, req_end, + req_slide, ) from core.geetest.common.performance import ( generate_fake_performance_timing, get_slide_track, ) +from core.geetest.common.trajectory import ( + H, + TrajectoryEncoder, + compress_trajectory, + generate_realistic_trajectory, + process_mouse_trajectory, +) def _generate_seed() -> str: diff --git a/core/huya/__init__.py b/core/huya/__init__.py index f131a50..4742139 100644 --- a/core/huya/__init__.py +++ b/core/huya/__init__.py @@ -33,23 +33,23 @@ if TYPE_CHECKING: ) __all__ = [ - "HuyaHttpClient", - "HuyaWssClient", "GetUserScoreReq", "GetUserScoreResp", + "HuyaAppLoginError", + "HuyaAppPasswordLogin", + "HuyaAppQrAuthRequiredError", "HuyaCredentialError", + "HuyaHttpClient", "HuyaLoginError", "HuyaLoginResult", "HuyaPasswordLogin", - "HuyaAppLoginError", - "HuyaAppQrAuthRequiredError", - "HuyaAppPasswordLogin", "HuyaSmsCodeResult", "HuyaSmsLogin", "HuyaVerificationError", "HuyaVerificationSolver", - "login_huya_password", + "HuyaWssClient", "login_huya_app_password", + "login_huya_password", "login_huya_sms", "send_huya_sms_code", "solve_huya_verification", @@ -98,8 +98,8 @@ def __getattr__(name: str): }: from .app_login import ( HuyaAppLoginError, - HuyaAppQrAuthRequiredError, HuyaAppPasswordLogin, + HuyaAppQrAuthRequiredError, login_huya_app_password, ) diff --git a/core/huya/account_env.py b/core/huya/account_env.py index fcf5877..854d061 100644 --- a/core/huya/account_env.py +++ b/core/huya/account_env.py @@ -35,13 +35,12 @@ import os import sys import time +from .app_login import HuyaAppPasswordLogin from .device_profile import ( - HDID32, # 登录帧 t1.t0 32hex 常量 (R15 公式结果, k1 来源见 R39; device_profile.py) _load_db, _save_db, get_profile, # 幂等画像: 同账号永远复用同一套 (data/huya_device_profiles.json) ) -from .app_login import HuyaAppPasswordLogin # --------------------------------------------------------------------------- diff --git a/core/huya/app_login.py b/core/huya/app_login.py index 2dbc562..967a3d3 100644 --- a/core/huya/app_login.py +++ b/core/huya/app_login.py @@ -130,7 +130,7 @@ def wup_password_login_raw( 固定 action/device_id。风控重试调用方应显式复用同一注册结果。 注册链失败抛 ``HuyaAppLoginError``,绝不静默回退旧固定值。 """ - uid_str = account[3:] if account.startswith("hy_") else account + uid_str = account.removeprefix("hy_") dev = dict(device_info) if device_info is not None else get_profile(account) mj, ua, _old_sd = _golden_session_assets() if not safedeviceid: @@ -304,7 +304,7 @@ def login_cred_with_flow( logger.info(f"[huya-app] 第 {rnd + 1} 轮触发安全验证: {kind}") if "qr_auth" in risk_url: raise HuyaAppQrAuthRequiredError( - f"该账号 App 渠道要求扫码验证(qr_auth),请先在手机虎牙 App 上正常登录一次建立设备信任。" + "该账号 App 渠道要求扫码验证(qr_auth),请先在手机虎牙 App 上正常登录一次建立设备信任。" ) solve_safe_auth(risk_url, proxies=proxies, device_info=dev) logger.info("[huya-app] safe_auth 滑块过验成功,重发 WUP 登录...") diff --git a/core/huya/cert_forge.py b/core/huya/cert_forge.py index 407c465..bfc4ea9 100644 --- a/core/huya/cert_forge.py +++ b/core/huya/cert_forge.py @@ -5,7 +5,6 @@ from __future__ import annotations -import base64 import os import struct diff --git a/core/huya/change_password.py b/core/huya/change_password.py index b495390..54ed701 100644 --- a/core/huya/change_password.py +++ b/core/huya/change_password.py @@ -30,7 +30,6 @@ from .login import ( generate_request_id, ) - CHANGE_PASSWORD_VERSION = "2.5" CHANGE_PASSWORD_CHECK_URI = "60011" CHANGE_PASSWORD_SEND_SMS_URI = "60003" diff --git a/core/huya/cookie_utils.py b/core/huya/cookie_utils.py index 4f2c917..af1cb92 100644 --- a/core/huya/cookie_utils.py +++ b/core/huya/cookie_utils.py @@ -4,7 +4,6 @@ from __future__ import annotations from collections.abc import Iterable, Mapping -import requests from requests.cookies import RequestsCookieJar diff --git a/core/huya/envelope_forge.py b/core/huya/envelope_forge.py index e162160..8fcdc75 100644 --- a/core/huya/envelope_forge.py +++ b/core/huya/envelope_forge.py @@ -116,7 +116,7 @@ class Envelope: self._parse() @classmethod - def load(cls, path: str | Path | None = None) -> "Envelope": + def load(cls, path: str | Path | None = None) -> Envelope: """加载信封模板,支持从文件加载或使用内嵌金样本。""" if path: p = Path(path) @@ -136,7 +136,7 @@ class Envelope: return cls(base64.b64decode(DEFAULT_QURL_B64)) @classmethod - def _load_from_path(cls, p: Path) -> "Envelope": + def _load_from_path(cls, p: Path) -> Envelope: if p.suffix == ".json": j = json.loads(p.read_text("utf-8")) q = next( @@ -236,7 +236,7 @@ class Envelope: assert self.uid_off is not None return struct.unpack_from(">Q", self.raw, self.uid_off)[0] - def patch_uid(self, uid: int) -> "Envelope": + def patch_uid(self, uid: int) -> Envelope: assert self.uid_off is not None struct.pack_into(">Q", self.raw, self.uid_off, uid) return self @@ -246,7 +246,7 @@ class Envelope: assert self.cert_off is not None return bytes(self.raw[self.cert_off : self.cert_off + self.cert_len]) - def patch_cert(self, cert: bytes) -> "Envelope": + def patch_cert(self, cert: bytes) -> Envelope: assert self.cert_off is not None b64 = base64.b64encode(cert) if len(b64) != self.cert_len: @@ -256,7 +256,7 @@ class Envelope: self.raw[self.cert_off : self.cert_off + self.cert_len] = b64 return self - def patch_session(self, session: int) -> "Envelope": + def patch_session(self, session: int) -> Envelope: if self.tag4_span: struct.pack_into(">I", self.raw, self.tag4_span[0], session & 0xFFFFFFFF) d = self.raw diff --git a/core/huya/frame_decoder.py b/core/huya/frame_decoder.py index c62fd6a..3b7e5f6 100644 --- a/core/huya/frame_decoder.py +++ b/core/huya/frame_decoder.py @@ -3,6 +3,7 @@ TAF/WUP 帧解码器 — 将二进制帧转为可读摘要,用于日志输出 """ from typing import Any, cast + from .taf_protocol import TafInputStream, TafType from .wup_protocol import normalize_wup_payload @@ -103,7 +104,7 @@ def _decode_taf_struct(ins: TafInputStream, depth: int = 0) -> dict: except Exception: val = f"" else: - val = f"<...>" + val = "<...>" try: ins.skip_field(dtype) except Exception: @@ -152,13 +153,7 @@ def _decode_wup_body(body: bytes) -> dict: if tag > 10: break ins.read_head() - if tag == 1: - if dtype != TafType.ZERO: - ins._read_int_value(dtype) - elif tag in (2, 3): - if dtype != TafType.ZERO: - ins._read_int_value(dtype) - elif tag == 4: + if tag == 1 or tag in (2, 3) or tag == 4: if dtype != TafType.ZERO: ins._read_int_value(dtype) elif tag == 5: diff --git a/core/huya/http_client.py b/core/huya/http_client.py index 66271b6..daae9d4 100644 --- a/core/huya/http_client.py +++ b/core/huya/http_client.py @@ -11,13 +11,13 @@ import base64 import hashlib import json import random -import struct import urllib.parse import urllib.request -from typing import Any, Optional, Callable +from collections.abc import Callable +from typing import Any from .cookie_utils import cookie_pairs, normalize_cookie_pairs, normalize_huya_cookie -from .taf_protocol import TafOutputStream, TafInputStream, TafType, TafStruct +from .taf_protocol import TafInputStream, TafOutputStream, TafStruct from .wup_protocol import WupRequest, WupResponse CDNWS_HOST = "cdnws.api.huya.com" @@ -849,11 +849,11 @@ class HuyaHttpClient: ): """order_type=None 时自动尝试从 1 到 10 找到有效值""" from .shop_structs import ( - CreateOrderReqV5, - CreateOrderRsp, + CreateOrderAccountParam, CreateOrderExtraParam, CreateOrderPromotionParam, - CreateOrderAccountParam, + CreateOrderReqV5, + CreateOrderRsp, ) # 如果指定了具体值,直接试 @@ -951,7 +951,7 @@ class HuyaHttpClient: baseinfo = self._generate_rpc_baseinfo(uid, guid, cookie) url = f"https://{CDNWS_HOST}/?baseinfo={baseinfo}" - self.logger(f"[HTTP] POST shopMiddleUI.payOrderSubmitV5") + self.logger("[HTTP] POST shopMiddleUI.payOrderSubmitV5") self.logger(f"[HTTP] 发送 hex前60: {wup_data[:60].hex()}") req = urllib.request.Request( diff --git a/core/huya/login.py b/core/huya/login.py index 63d47c7..8449acb 100644 --- a/core/huya/login.py +++ b/core/huya/login.py @@ -14,12 +14,11 @@ from http.cookies import SimpleCookie from urllib.parse import quote, urlsplit, urlunsplit import requests -from requests.cookies import RequestsCookieJar from loguru import logger +from requests.cookies import RequestsCookieJar from .cookie_utils import normalize_huya_cookie - APP_ID = "5002" APP_VERSION = "2.6" APP_SIGN = "1ce3bf682483d03f146f58232ec10635" @@ -101,7 +100,7 @@ def generate_context(device_id: str | None = None, mid: str | None = None) -> st def generate_request_id() -> str: """生成 requestId,形态参考旧实现的日内毫秒数。""" - now = dt.datetime.now(dt.timezone.utc) + now = dt.datetime.now(dt.UTC) midnight = now.replace(hour=0, minute=0, second=0, microsecond=0) return str(int((now - midnight).total_seconds() * 1000)) diff --git a/core/huya/shop_structs.py b/core/huya/shop_structs.py index ca0800e..2f3405e 100644 --- a/core/huya/shop_structs.py +++ b/core/huya/shop_structs.py @@ -5,8 +5,8 @@ 来源:m-shop.yaoguo.com 的 jce/ShopFacade.js、api/orderui.ts、api/mall/PlayMallNewHome.ts """ -from typing import List, Dict, Optional -from .taf_protocol import TafOutputStream, TafInputStream, TafStruct, TafType + +from .taf_protocol import TafInputStream, TafOutputStream, TafStruct, TafType class OrderType: @@ -387,7 +387,7 @@ class GoodsPriceInfo(TafStruct): self.spuId: str = "" self.minPrice: int = 0 self.maxPrice: int = 0 - self.skuMap: Dict[int, GoodsSkuItem] = {} + self.skuMap: dict[int, GoodsSkuItem] = {} self.stock: int = 0 self.buyLimit: int = 0 self.price: int = 0 @@ -500,7 +500,7 @@ class OrderListGoodsDetail(TafStruct): self.buyerUid: int = 0 # tag 18 self.virtualType: int = 0 # tag 19 self.quantity: int = 0 # tag 20 - self.shopInfo: Optional[OrderListShopInfo] = None # tag 21 + self.shopInfo: OrderListShopInfo | None = None # tag 21 self.points: int = 0 # tag 23 def read_from(self, ins: TafInputStream): @@ -544,7 +544,7 @@ class OrderListItem(TafStruct): self.totalPrice: int = 0 # tag 12 分 self.createTime: int = 0 # tag 14 毫秒时间戳 self.payTime: int = 0 # tag 15 毫秒时间戳 - self.goodsDetail: Optional[OrderListGoodsDetail] = None # tag 16 + self.goodsDetail: OrderListGoodsDetail | None = None # tag 16 def read_from(self, ins: TafInputStream): self.bizOrderId = ins.read_string(0, default=self.bizOrderId) @@ -611,7 +611,7 @@ class QueryUserOrderListRsp(TafStruct): self.code: int = 0 self.message: str = "" self.totalCount: int = 0 - self.orders: List[OrderListItem] = [] + self.orders: list[OrderListItem] = [] @staticmethod def _read_order_item(ins: TafInputStream, _tag: int): @@ -692,9 +692,9 @@ class CreateOrderPromotionParam(TafStruct): class CreateOrderAccountParam(TafStruct): def __init__(self): - self.payoutTypeList: List[int] = [] # tag 0 Vector + self.payoutTypeList: list[int] = [] # tag 0 Vector self.payoutChargeAmount: int = 0 # tag 1 - self.cancelPayoutTypeList: List[int] = [] # tag 2 + self.cancelPayoutTypeList: list[int] = [] # tag 2 self.recycleSupplierId: int = 0 # tag 3 self.claimPrice: int = 0 # tag 4 @@ -739,20 +739,20 @@ class CreateOrderReqV5(TafStruct): self.gameId: str = "" # tag 8 self.orderId: int = 0 # tag 9 self.src: int = 0 # tag 10 - self.couponUserIds: List[int] = [] # tag 11 Vector + self.couponUserIds: list[int] = [] # tag 11 Vector self.orderType: int = 0 # tag 12 - self.extraParam: Optional[CreateOrderExtraParam] = None # tag 13 + self.extraParam: CreateOrderExtraParam | None = None # tag 13 self.scene: int = 0 # tag 14 - self.promotionItems: List = [] # tag 15 Vector + self.promotionItems: list = [] # tag 15 Vector self.sourceId: str = "" # tag 16 - self.env: Dict[str, str] = {} # tag 17 Map + self.env: dict[str, str] = {} # tag 17 Map self.orderScene: int = 0 # tag 18 self.watchWord: str = "" # tag 19 self.marketingChannel: str = "" # tag 20 - self.promotionParam: Optional[CreateOrderPromotionParam] = None # tag 21 + self.promotionParam: CreateOrderPromotionParam | None = None # tag 21 self.externalTraceKey: str = "" # tag 22 self.kefuUid: int = 0 # tag 23 - self.accountParam: Optional[CreateOrderAccountParam] = None # tag 24 + self.accountParam: CreateOrderAccountParam | None = None # tag 24 self.parentOrderId: int = 0 # tag 25 self.vendorAccountType: str = "" # tag 26 self.vendorAccountVal: str = "" # tag 27 diff --git a/core/huya/sms_login.py b/core/huya/sms_login.py index c9e5648..947f68e 100644 --- a/core/huya/sms_login.py +++ b/core/huya/sms_login.py @@ -29,7 +29,6 @@ from .login import ( generate_request_id, ) - SMS_CODE_URI = "60027" SMS_LOGIN_URI = "60025" SMS_CODE_URL = "https://udblgn.huya.com/web/v2/smsCode" @@ -604,7 +603,7 @@ class HuyaSmsLogin: phone: str = "", proxies: Mapping[str, str] | None = None, timeout: tuple[float, float] | None = None, - ) -> "HuyaSmsLogin": + ) -> HuyaSmsLogin: """从发码阶段返回的 state 恢复短信登录会话。""" try: raw = base64.urlsafe_b64decode(state.encode("ascii")) diff --git a/core/huya/taf_protocol.py b/core/huya/taf_protocol.py index d9dadb9..410d3e7 100644 --- a/core/huya/taf_protocol.py +++ b/core/huya/taf_protocol.py @@ -9,9 +9,9 @@ 0x0c ZERO 0x0d SIMPLE_LIST """ -import struct import io -from typing import Any, Dict, List, Optional, Tuple +import struct +from typing import Any class TafType: @@ -136,7 +136,7 @@ class TafOutputStream: # ---- Map ---- def write_map( - self, tag: int, value: Dict[Any, Any], key_writer=None, val_writer=None + self, tag: int, value: dict[Any, Any], key_writer=None, val_writer=None ): self.write_head(tag, TafType.MAP) self.write_int32(0, len(value)) @@ -151,7 +151,7 @@ class TafOutputStream: self._write_any(1, v) # ---- List ---- - def write_list(self, tag: int, value: List[Any], item_writer=None): + def write_list(self, tag: int, value: list[Any], item_writer=None): self.write_head(tag, TafType.LIST) self.write_int32(0, len(value)) for item in value: @@ -192,7 +192,7 @@ class TafInputStream: def __init__(self, data: bytes): self.buf = io.BytesIO(data) - def peek_head(self) -> Tuple[int, int]: + def peek_head(self) -> tuple[int, int]: """读取 head 但不消费(用于探测)""" pos = self.buf.tell() try: @@ -200,7 +200,7 @@ class TafInputStream: finally: self.buf.seek(pos) - def read_head(self) -> Tuple[int, int]: + def read_head(self) -> tuple[int, int]: """返回 (tag, type)""" data = self.buf.read(1) if not data: @@ -293,7 +293,7 @@ class TafInputStream: self.skip_field(it) # ---- 带跳过策略的字段读取:找到 tag,否则返回默认 ---- - def _find_tag(self, target_tag: int, required: bool) -> Optional[Tuple[int, int]]: + def _find_tag(self, target_tag: int, required: bool) -> tuple[int, int] | None: """逐个读 head,tag 相等则返回,tag 超过则回退并返回 None""" while True: pos = self.buf.tell() @@ -402,7 +402,7 @@ class TafInputStream: # ---- 复合类型 ---- def read_map( self, tag: int, required: bool = False, key_reader=None, val_reader=None - ) -> Dict: + ) -> dict: found = self._find_tag(tag, required) if not found: return {} @@ -418,7 +418,7 @@ class TafInputStream: result[k] = v return result - def read_list(self, tag: int, required: bool = False, item_reader=None) -> List: + def read_list(self, tag: int, required: bool = False, item_reader=None) -> list: found = self._find_tag(tag, required) if not found: return [] @@ -486,7 +486,7 @@ class TafStruct: def read_from(self, ins: TafInputStream): raise NotImplementedError - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: """调试用:转字典""" return {k: v for k, v in self.__dict__.items() if not k.startswith("_")} diff --git a/core/huya/verification/ocr.py b/core/huya/verification/ocr.py index 088e8f3..d909136 100644 --- a/core/huya/verification/ocr.py +++ b/core/huya/verification/ocr.py @@ -13,7 +13,6 @@ import onnxruntime as ort from PIL import Image from scipy.optimize import linear_sum_assignment - MODEL_DIR = Path(__file__).resolve().parent / "models" diff --git a/core/huya/verification/solver.py b/core/huya/verification/solver.py index 5131a95..cd74bf7 100644 --- a/core/huya/verification/solver.py +++ b/core/huya/verification/solver.py @@ -17,13 +17,12 @@ import cv2 import execjs import numpy as np import requests -from PIL import Image from loguru import logger +from PIL import Image from .ocr import HuyaCaptchaOcr, default_ocr from .track import format_track, generate_slide_track - DEFAULT_UA = ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " diff --git a/core/huya/wss_client.py b/core/huya/wss_client.py index 77c3bec..ec2f443 100644 --- a/core/huya/wss_client.py +++ b/core/huya/wss_client.py @@ -21,14 +21,14 @@ import re import struct import time from collections import deque -from typing import Any, Optional, Callable, cast +from collections.abc import Callable +from typing import Any, cast import websockets +from .frame_decoder import _decode_taf_struct, _truncate, format_wss_log +from .taf_protocol import TafInputStream, TafOutputStream, TafStruct, TafType from .wup_protocol import WupRequest, WupResponse -from .taf_protocol import TafOutputStream, TafInputStream, TafType, TafStruct -from .frame_decoder import format_wss_log, _decode_taf_struct, _truncate - SHOP_WS_HOST = "77bc035c-ws.va.huya.com" # 商城端点 baseinfo (conn4, h5_/index.html) — 商城业务 shopMiddleUI 走此通道 @@ -225,7 +225,7 @@ class HuyaWssClient: ), timeout=timeout, ) - except asyncio.TimeoutError: + except TimeoutError: self.logger(f"[WSS] 连接超时({timeout}s)") raise except Exception as e: @@ -376,7 +376,7 @@ class HuyaWssClient: try: body = await asyncio.wait_for(future, timeout) - except asyncio.TimeoutError: + except TimeoutError: if future in self.rpc_queue: self.rpc_queue.remove(future) self.logger("[✗] wsLaunch 超时") @@ -526,7 +526,7 @@ class HuyaWssClient: try: body = await asyncio.wait_for(future, timeout) - except asyncio.TimeoutError: + except TimeoutError: if future in self.rpc_queue: self.rpc_queue.remove(future) self.logger("[✗] getConfig 超时") @@ -596,7 +596,7 @@ class HuyaWssClient: try: body = await asyncio.wait_for(future, timeout) - except asyncio.TimeoutError: + except TimeoutError: if future in self.rpc_queue: self.rpc_queue.remove(future) self.logger(f"[✗] {service}.{method} 超时") @@ -696,11 +696,11 @@ class HuyaWssClient: order_type: int = 6, ): from .shop_structs import ( - CreateOrderReqV5, - CreateOrderRsp, + CreateOrderAccountParam, CreateOrderExtraParam, CreateOrderPromotionParam, - CreateOrderAccountParam, + CreateOrderReqV5, + CreateOrderRsp, ) req = CreateOrderReqV5() @@ -781,7 +781,7 @@ class HuyaWssClient: try: body = await asyncio.wait_for(future, 15.0) - except asyncio.TimeoutError: + except TimeoutError: if future in self.rpc_queue: self.rpc_queue.remove(future) self.logger("[✗] payOrderSubmitV5 超时") diff --git a/core/huya/wup_encoder.py b/core/huya/wup_encoder.py index e2d1b27..26f00ba 100644 --- a/core/huya/wup_encoder.py +++ b/core/huya/wup_encoder.py @@ -9,7 +9,7 @@ import json import random import struct import time as _time -from typing import Any, Dict +from typing import Any # TAF 类型标签 INT8, INT16, INT32, INT64 = 0x00, 0x01, 0x02, 0x03 @@ -97,7 +97,7 @@ class _Writer: def _build_meta_json(session: int, trace_id: str) -> str: """构造 _wup_data.t0.t2 元数据 JSON。""" - meta: Dict[str, Any] = { + meta: dict[str, Any] = { "associationId": 8193, "funcName": "hypasswordLogin", "group": 1, @@ -167,7 +167,7 @@ def _build_wup_data( session: int, trace_id: str, user_action_json: str, - device_info: Dict[str, str], + device_info: dict[str, str], ) -> None: """编码 _wup_data struct。""" meta_json = _build_meta_json(session, trace_id) @@ -235,7 +235,7 @@ def build_password_login_wup( session: int, trace_id: str, user_action_json: str, - device_info: Dict[str, str], + device_info: dict[str, str], ) -> bytes: """构造密码登录的 WUP TAF 请求体。""" wd = _Writer() diff --git a/core/huya/wup_protocol.py b/core/huya/wup_protocol.py index 1197868..002b481 100644 --- a/core/huya/wup_protocol.py +++ b/core/huya/wup_protocol.py @@ -11,8 +11,9 @@ Wup 包结构: """ import struct -from typing import Any, Dict, Optional -from .taf_protocol import TafOutputStream, TafInputStream, TafType, TafStruct +from typing import Any + +from .taf_protocol import TafInputStream, TafOutputStream, TafStruct, TafType class WupRequest: @@ -27,9 +28,9 @@ class WupRequest: self.sFuncName: str = "" # tag 6 self.sBuffer: bytes = b"" # tag 7 self.iTimeout: int = 3000 # tag 8 - self.context: Dict[str, str] = {} # tag 9 - self.status: Dict[str, str] = {} # tag 10 - self.newdata: Dict[str, bytes] = {} + self.context: dict[str, str] = {} # tag 9 + self.status: dict[str, str] = {} # tag 10 + self.newdata: dict[str, bytes] = {} def setServant(self, name: str): self.sServantName = name @@ -141,9 +142,9 @@ class WupResponse: self.sFuncName: str = "" self.sBuffer: bytes = b"" self.iTimeout: int = 0 - self.context: Dict[str, str] = {} - self.status: Dict[str, str] = {} - self.newdata: Dict[str, bytes] = {} + self.context: dict[str, str] = {} + self.status: dict[str, str] = {} + self.newdata: dict[str, bytes] = {} def decode(self, data: bytes): """解码响应(不包含长度前缀;若含前缀会自动跳过)""" @@ -269,7 +270,7 @@ def _read_bytes_value(ins: TafInputStream, dtype: int) -> bytes: return ins.buf.read(length) -def _read_map_value(ins: TafInputStream, dtype: int) -> Dict: +def _read_map_value(ins: TafInputStream, dtype: int) -> dict: if dtype != TafType.MAP: return {} count = ins._read_int_len() diff --git a/core/sms_provider.py b/core/sms_provider.py index 834d45f..8d8d583 100644 --- a/core/sms_provider.py +++ b/core/sms_provider.py @@ -10,7 +10,6 @@ from urllib.parse import urlparse import requests - CODE_PATTERN = re.compile( r"(?:verification\s+code|验证码|校验码|动态码|安全码)\D{0,20}(\d{4,8})", re.IGNORECASE, diff --git a/scripts/migrate_sqlite_to_mysql.py b/scripts/migrate_sqlite_to_mysql.py index 1883cba..44fb8b5 100644 --- a/scripts/migrate_sqlite_to_mysql.py +++ b/scripts/migrate_sqlite_to_mysql.py @@ -14,7 +14,6 @@ from sqlalchemy import JSON, MetaData, create_engine, func, inspect, select from sqlalchemy.engine import Connection, Engine from sqlalchemy.schema import Table - PROJECT_ROOT = Path(__file__).resolve().parents[1] DEFAULT_SOURCE = PROJECT_ROOT / "data" / "web.db" IGNORED_SOURCE_TABLES = {"alembic_version"} @@ -147,7 +146,7 @@ def main() -> int: if target_url: os.environ["DATABASE_URL"] = target_url from web.backend import models # noqa: F401 - from web.backend.database import Base, DATABASE_URL, run_migrations + from web.backend.database import DATABASE_URL, Base, run_migrations target_url = target_url or DATABASE_URL if not target_url.startswith("mysql+"): diff --git a/services/yyb-worker/runtime/main.py b/services/yyb-worker/runtime/main.py index 90f9d9f..891f43b 100644 --- a/services/yyb-worker/runtime/main.py +++ b/services/yyb-worker/runtime/main.py @@ -34,19 +34,20 @@ from pathlib import Path ROOT = Path(__file__).resolve().parent sys.path.insert(0, str(ROOT)) -from pyvm.algorithm import build_plaintext, generate_encrypt_msg_offline # noqa: E402 -from pyvm.mall import MallSession, generate_encrypt_msg as mall_generate # noqa: E402 -from pyvm.session import SessionState, load_session # noqa: E402 -from pyvm.login_profile import midas_login_params # noqa: E402 -from pyvm.payment_errors import describe_payment_failure # noqa: E402 -from pyvm.protocol import ( # noqa: E402 +from pyvm.algorithm import generate_encrypt_msg_offline +from pyvm.login_profile import midas_login_params +from pyvm.mall import MallSession +from pyvm.mall import generate_encrypt_msg as mall_generate +from pyvm.payment_errors import describe_payment_failure +from pyvm.protocol import ( + GOODS_USER_AGENT, MALL_API_URL, MALL_USER_AGENT, - GOODS_USER_AGENT, PAY_APPID, validate_goods_materials, validate_mall_materials, ) +from pyvm.session import SessionState, load_session REPLAY = ROOT / "replay" DEFAULT_APPID = PAY_APPID diff --git a/services/yyb-worker/runtime/pyvm/__init__.py b/services/yyb-worker/runtime/pyvm/__init__.py index 821fb41..ee6eb7a 100644 --- a/services/yyb-worker/runtime/pyvm/__init__.py +++ b/services/yyb-worker/runtime/pyvm/__init__.py @@ -13,22 +13,24 @@ from .algorithm import ( generate_encrypt_msg, generate_encrypt_msg_offline, ) -from .goods import GoodsSession, generate_encrypt_msg as goods_generate -from .mall import MallSession, generate_encrypt_msg as mall_generate +from .goods import GoodsSession +from .goods import generate_encrypt_msg as goods_generate +from .mall import MallSession +from .mall import generate_encrypt_msg as mall_generate from .pagedoo_vm import PagedooVM, run_frame from .session import SessionState, load_session __all__ = [ + "GoodsSession", + "MallSession", + "PagedooVM", + "SessionState", "build_plaintext", "decode_d", "generate_encrypt_msg", "generate_encrypt_msg_offline", - "GoodsSession", "goods_generate", - "MallSession", - "mall_generate", - "PagedooVM", - "run_frame", - "SessionState", "load_session", + "mall_generate", + "run_frame", ] diff --git a/services/yyb-worker/runtime/pyvm/algorithm.py b/services/yyb-worker/runtime/pyvm/algorithm.py index 3828de9..b6c6163 100644 --- a/services/yyb-worker/runtime/pyvm/algorithm.py +++ b/services/yyb-worker/runtime/pyvm/algorithm.py @@ -13,6 +13,7 @@ """ from __future__ import annotations + import itertools import json import math @@ -355,7 +356,7 @@ def js_set(obj, key, val): if isinstance(obj, str): raise TypeError("Cannot assign to read only property") cur = getattr( - getattr(__import__("pyvm.algorithm", fromlist=["x"]), "VM"), "_last_u", None + __import__("pyvm.algorithm", fromlist=["x"]).VM, "_last_u", None ) raise TypeError( f"invalid set target obj={type(obj).__name__} key={key!r} val={type(val).__name__} u={cur}" @@ -418,7 +419,7 @@ class Window: class JSFunction: """VM 函数(解释器实例工厂返回的 h)。""" - __slots__ = ("entry", "args", "s", "n", "t", "vm", "name") + __slots__ = ("args", "entry", "n", "name", "s", "t", "vm") def __init__(self, vm, entry, args, s, n, t): self.vm = vm @@ -455,7 +456,7 @@ def js_call(fn, this, args): return fn.fn(this, *args) if callable(fn): return fn(this, *args) - raise TypeError(f"{str(fn)} is not a function u={getattr(VM, '_last_u', None)}") + raise TypeError(f"{fn!s} is not a function u={getattr(VM, '_last_u', None)}") def js_apply(fn, this, args): diff --git a/services/yyb-worker/runtime/pyvm/goods.py b/services/yyb-worker/runtime/pyvm/goods.py index e2bad87..d453ff1 100644 --- a/services/yyb-worker/runtime/pyvm/goods.py +++ b/services/yyb-worker/runtime/pyvm/goods.py @@ -12,7 +12,6 @@ from __future__ import annotations import json from pathlib import Path -from typing import Any from .algorithm import generate_encrypt_msg_offline @@ -64,7 +63,7 @@ class GoodsSession: raise ValueError("key16/key1 应为 16 字节") @classmethod - def from_session_state(cls, path: str | Path) -> "GoodsSession": + def from_session_state(cls, path: str | Path) -> GoodsSession: """从 scripts/capture-session.mjs 生成的 session-state.json 加载。""" d = json.loads(Path(path).read_text(encoding="utf-8")) return cls( @@ -85,7 +84,7 @@ class GoodsSession: } @classmethod - def from_json(cls, d: dict) -> "GoodsSession": + def from_json(cls, d: dict) -> GoodsSession: return cls( d["xmidas_ops"], d["key16"], diff --git a/services/yyb-worker/runtime/pyvm/mall.py b/services/yyb-worker/runtime/pyvm/mall.py index 81c29eb..2009f6b 100644 --- a/services/yyb-worker/runtime/pyvm/mall.py +++ b/services/yyb-worker/runtime/pyvm/mall.py @@ -19,7 +19,6 @@ import copy import json import re from pathlib import Path -from typing import Any from .algorithm import UNDEF, JSObject, Window from .pagedoo_vm import PagedooVM @@ -78,7 +77,7 @@ class MallSession: raise ValueError(f"624B 中间态长度 != 624: {len(mid[0])}") @classmethod - def from_capture_file(cls, frames_jsonl: str | Path) -> "MallSession": + def from_capture_file(cls, frames_jsonl: str | Path) -> MallSession: """从捕获的 frames.jsonl 提取同会话 transform_input + xMidasOps。 frames.jsonl 由 scripts/capture-mall-session.mjs 实时落盘: @@ -127,7 +126,7 @@ class MallSession: } @classmethod - def from_json(cls, d: dict) -> "MallSession": + def from_json(cls, d: dict) -> MallSession: return cls(d["transform_input"], d["xmidas_ops"]) diff --git a/services/yyb-worker/runtime/pyvm/pagedoo_vm.py b/services/yyb-worker/runtime/pyvm/pagedoo_vm.py index 9ca3ce4..a832c0e 100644 --- a/services/yyb-worker/runtime/pyvm/pagedoo_vm.py +++ b/services/yyb-worker/runtime/pyvm/pagedoo_vm.py @@ -13,56 +13,51 @@ JS 语义辅助复用 algorithm.py(_ic/i32/js_add/js_index/JSObject/JSFunction from __future__ import annotations import json -import math -import urllib.parse -from pathlib import Path from .algorithm import ( - JSDate, UNDEF, + HostFunction, + JSDate, JSFunction, JSObject, - HostFunction, _ic, - i32, - u32, - ushr, - shl, - shr, - js_typeof, - js_truthy, - js_str, - js_add, - js_num, - js_eq, - js_streq, - js_index, - js_set, - js_del, - js_keys, - js_call, - js_apply, - js_new, - h_math_floor, - h_math_round, - h_math_ceil, - h_math_min, - h_math_max, - h_math_abs, - h_math_pow, - h_math_sqrt, - h_parseint, - h_parsefloat, - h_isnan, - h_encodeuri, - h_encodeuricomponent, h_decodeuri, h_decodeuricomponent, - h_string_fromcharcode, + h_encodeuri, + h_encodeuricomponent, + h_isnan, + h_math_abs, + h_math_ceil, + h_math_floor, + h_math_max, + h_math_min, + h_math_pow, + h_math_round, + h_math_sqrt, h_new_date, + h_parsefloat, + h_parseint, + h_string_fromcharcode, + i32, + js_add, + js_apply, + js_call, + js_del, + js_eq, + js_index, + js_keys, + js_new, + js_num, + js_set, + js_str, + js_truthy, + js_typeof, + shl, + shr, + ushr, ) -__all__ = ["PagedooVM", "run_frame", "REPLAY"] +__all__ = ["REPLAY", "PagedooVM", "run_frame"] # ---------------------------------------------------------------- 辅助 diff --git a/services/yyb-worker/runtime/pyvm/protocol.py b/services/yyb-worker/runtime/pyvm/protocol.py index 87bb095..814c888 100644 --- a/services/yyb-worker/runtime/pyvm/protocol.py +++ b/services/yyb-worker/runtime/pyvm/protocol.py @@ -10,7 +10,6 @@ import json from pathlib import Path from typing import Any - PAY_APPID = "1450243039" PAY_GOODS_URL = "https://pay.qq.com/midas/minipay_v2/views/cpay/goods.shtml" PAY_SAVE_URL = f"https://api.unipay.qq.com/v1/r/{PAY_APPID}/web_save" diff --git a/services/yyb-worker/runtime/pyvm/session.py b/services/yyb-worker/runtime/pyvm/session.py index 0fbedb6..0e29086 100644 --- a/services/yyb-worker/runtime/pyvm/session.py +++ b/services/yyb-worker/runtime/pyvm/session.py @@ -68,7 +68,7 @@ class SessionState: } @classmethod - def from_json(cls, d: dict) -> "SessionState": + def from_json(cls, d: dict) -> SessionState: return cls( xmidas_ops=list(d.get("xmidas_ops", [])), key16=list(d.get("key16", [])), diff --git a/services/yyb-worker/runtime/scripts/jsdom-pay.py b/services/yyb-worker/runtime/scripts/jsdom-pay.py index f6ccbf8..1a68392 100644 --- a/services/yyb-worker/runtime/scripts/jsdom-pay.py +++ b/services/yyb-worker/runtime/scripts/jsdom-pay.py @@ -31,10 +31,10 @@ from pyvm.algorithm import ( build_plaintext, derive_key1_from_key16, generate_encrypt_msg_offline, -) # noqa: E402 -from pyvm.login_profile import midas_login_params # noqa: E402 -from pyvm.payment_errors import describe_payment_failure # noqa: E402 -from pyvm.protocol import ( # noqa: E402 +) +from pyvm.login_profile import midas_login_params +from pyvm.payment_errors import describe_payment_failure +from pyvm.protocol import ( DEFAULT_ORDER_PF, GOODS_USER_AGENT, PAY_APPID, @@ -450,7 +450,6 @@ def cmd_check_only(session_path: Path, out_dir: Path) -> int: from pyvm.order_status import ( completion_summary, get_official_orders, - is_finished, ) session = load_json(session_path) @@ -697,7 +696,7 @@ def main() -> int: out_dir, fields, int(fp.get("device_fp_length", 0)), plaintext_length ) body = urllib.parse.urlencode(fields) - from pyvm.order_status import order_completion_states, get_official_orders + from pyvm.order_status import get_official_orders, order_completion_states # 尽量在发起支付前建立基线,缩小极快支付造成的检测窗口;失败不阻塞支付,付款码生成后重试。 baseline_document = None diff --git a/services/yyb-worker/runtime/scripts/login-qq.py b/services/yyb-worker/runtime/scripts/login-qq.py index 1fe49b7..4b95c96 100644 --- a/services/yyb-worker/runtime/scripts/login-qq.py +++ b/services/yyb-worker/runtime/scripts/login-qq.py @@ -17,8 +17,8 @@ from pathlib import Path from urllib.error import HTTPError from urllib.parse import urlencode from urllib.request import ( - HTTPRedirectHandler, HTTPCookieProcessor, + HTTPRedirectHandler, Request, build_opener, ) @@ -43,7 +43,7 @@ USER_AGENT = ( class NoRedirect(HTTPRedirectHandler): """Keep OAuth redirects visible while CookieJar receives Set-Cookie headers.""" - def redirect_request(self, req, fp, code, msg, headers, newurl): # noqa: D401 + def redirect_request(self, req, fp, code, msg, headers, newurl): return None @@ -118,7 +118,7 @@ def g_tk(p_skey: str) -> int: def parse_poll(body: str) -> tuple[int, str]: - match = re.search(r"ptuiCB\((.*)\)", body, re.S) + match = re.search(r"ptuiCB\((.*)\)", body, re.DOTALL) if not match: raise ValueError("QQ 二维码轮询响应缺少 ptuiCB") values = re.findall(r"'([^']*)'", match.group(1)) diff --git a/services/yyb-worker/runtime/scripts/login-wechat.py b/services/yyb-worker/runtime/scripts/login-wechat.py index 113cf9a..8ec55d2 100644 --- a/services/yyb-worker/runtime/scripts/login-wechat.py +++ b/services/yyb-worker/runtime/scripts/login-wechat.py @@ -20,8 +20,8 @@ from pathlib import Path from urllib.error import HTTPError from urllib.parse import urlencode from urllib.request import ( - HTTPRedirectHandler, HTTPCookieProcessor, + HTTPRedirectHandler, Request, build_opener, ) @@ -42,7 +42,7 @@ USER_AGENT = ( class NoRedirect(HTTPRedirectHandler): """保留 OAuth 回调的 302 和 Set-Cookie。""" - def redirect_request(self, req, fp, code, msg, headers, newurl): # noqa: D401 + def redirect_request(self, req, fp, code, msg, headers, newurl): return None @@ -88,7 +88,7 @@ def extract_uuid(page: str) -> str: r"connect/qrcode/([A-Za-z0-9_-]{8,64})", r"uuid=([A-Za-z0-9_-]{8,64})", ): - match = re.search(pattern, page, re.I) + match = re.search(pattern, page, re.IGNORECASE) if match: return match.group(1) raise ValueError("微信授权页未找到二维码 UUID") diff --git a/services/yyb-worker/runtime/scripts/select-peace-elite.py b/services/yyb-worker/runtime/scripts/select-peace-elite.py index 29feb60..6fa8984 100644 --- a/services/yyb-worker/runtime/scripts/select-peace-elite.py +++ b/services/yyb-worker/runtime/scripts/select-peace-elite.py @@ -18,7 +18,7 @@ from curl_cffi import requests ROOT = Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT)) -from pyvm.login_profile import midas_login_params # noqa: E402 +from pyvm.login_profile import midas_login_params YYB_APP_ID = 52575843 SOURCE_ID = "24292013" diff --git a/services/yyb-worker/runtime/scripts/yyb-worker.py b/services/yyb-worker/runtime/scripts/yyb-worker.py index ac84bf3..08c6e57 100644 --- a/services/yyb-worker/runtime/scripts/yyb-worker.py +++ b/services/yyb-worker/runtime/scripts/yyb-worker.py @@ -32,7 +32,7 @@ WORKER_KEY = os.environ.get("YYB_WORKER_KEY", "") _jobs: dict[str, dict] = {} _lock = threading.Lock() -from pyvm.payment_errors import describe_payment_failure # noqa: E402 +from pyvm.payment_errors import describe_payment_failure def _payment_failure_message(job: dict, phase: str, fallback: str) -> str: @@ -81,13 +81,13 @@ def _safe_log(job: dict, line: str) -> None: clean = re.sub(r"HOLD_[A-Za-z0-9_-]+", "[订单已隐藏]", clean) clean = re.sub(r"(订单\s*[::]\s*)\S+", r"\1[订单已隐藏]", clean) clean = re.sub( - r"(?:token|openid|openkey|cookie)=\S+", "[敏感字段已隐藏]", clean, flags=re.I + r"(?:token|openid|openkey|cookie)=\S+", "[敏感字段已隐藏]", clean, flags=re.IGNORECASE ) clean = re.sub( r"(?:pay_token|web_token|anti_token|session_id|sessionid)\s*[:= ]\s*\S+", "[敏感字段已隐藏]", clean, - flags=re.I, + flags=re.IGNORECASE, ) timestamp = time.strftime("%H:%M:%S") with _lock: @@ -523,7 +523,7 @@ class Handler(BaseHTTPRequestHandler): value = self.headers.get("Authorization", "") return value == f"Bearer {WORKER_KEY}" - def do_POST(self) -> None: # noqa: N802 + def do_POST(self) -> None: if not self._authorized(): return self._json(401, {"detail": "未授权"}) path = urlparse(self.path).path.strip("/").split("/") @@ -687,7 +687,7 @@ class Handler(BaseHTTPRequestHandler): except Exception as exc: # noqa: BLE001 return self._json(500, {"detail": str(exc)}) - def do_GET(self) -> None: # noqa: N802 + def do_GET(self) -> None: if urlparse(self.path).path == "/health": return self._json(200, {"status": "ok"}) if not self._authorized(): diff --git a/services/yyb-worker/runtime/tests/test_jsdom_pay.py b/services/yyb-worker/runtime/tests/test_jsdom_pay.py index b46e093..9b21cc8 100644 --- a/services/yyb-worker/runtime/tests/test_jsdom_pay.py +++ b/services/yyb-worker/runtime/tests/test_jsdom_pay.py @@ -8,7 +8,6 @@ import unittest from pathlib import Path from urllib.parse import parse_qs - _SCRIPT = Path(__file__).resolve().parents[1] / "scripts" / "jsdom-pay.py" _SPEC = importlib.util.spec_from_file_location("yyb_jsdom_pay", _SCRIPT) assert _SPEC and _SPEC.loader @@ -16,8 +15,8 @@ _MODULE = importlib.util.module_from_spec(_SPEC) sys.modules[_SPEC.name] = _MODULE _SPEC.loader.exec_module(_MODULE) -from pyvm.payment_errors import describe_payment_failure from pyvm.algorithm import derive_key1_from_key16, generate_encrypt_msg_offline +from pyvm.payment_errors import describe_payment_failure from pyvm.protocol import goods_material_diagnostics, validate_goods_materials diff --git a/tests/test_account_sensitive_fields.py b/tests/test_account_sensitive_fields.py index 957bc16..4907c6d 100644 --- a/tests/test_account_sensitive_fields.py +++ b/tests/test_account_sensitive_fields.py @@ -1,8 +1,7 @@ import pytest - +from fastapi import HTTPException from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker -from fastapi import HTTPException from web.backend.database import Base from web.backend.models import Account, User diff --git a/tests/test_audit_logs.py b/tests/test_audit_logs.py index a1abefc..4a180fd 100644 --- a/tests/test_audit_logs.py +++ b/tests/test_audit_logs.py @@ -1,9 +1,8 @@ """充值审计日志的权限、查询和脱敏测试。""" -import pytest - import json +import pytest from fastapi import HTTPException from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker diff --git a/tests/test_cookie_custom_order.py b/tests/test_cookie_custom_order.py index abbb9e3..a531ffe 100644 --- a/tests/test_cookie_custom_order.py +++ b/tests/test_cookie_custom_order.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta from sqlalchemy import create_engine from sqlalchemy.orm import Session @@ -17,7 +17,7 @@ class TestCustomCookieOrder: self.db.add(self.user) self.db.flush() - now = datetime.now(timezone.utc) + now = datetime.now(UTC) accounts = { name: Account(username=name, password="p", email="e", email_password="ep") for name in ("account-a", "account-b", "account-c") diff --git a/tests/test_cookie_operations.py b/tests/test_cookie_operations.py index 78c1579..5eb38d8 100644 --- a/tests/test_cookie_operations.py +++ b/tests/test_cookie_operations.py @@ -1,18 +1,13 @@ -import pytest from types import SimpleNamespace -from unittest.mock import patch from typing import cast +from unittest.mock import patch +import pytest from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from web.backend.database import Base from web.backend.models import Account, LoginTask, ProxyConfig, User -from web.backend.services.login_service import ( - LoginBatchRunner, - cleanup_orphan_relogin_tasks, -) -from web.backend.services.cookie_check_service import check_douyu_cookie from web.backend.routers.cookies import ( check_cookie_operations, get_cookie, @@ -21,6 +16,11 @@ from web.backend.routers.cookies import ( list_cookies, relogin_invalid_cookie_operations, ) +from web.backend.services.cookie_check_service import check_douyu_cookie +from web.backend.services.login_service import ( + LoginBatchRunner, + cleanup_orphan_relogin_tasks, +) class TestCookieOperation: diff --git a/tests/test_douyu_elite_exchange_flow.py b/tests/test_douyu_elite_exchange_flow.py index c486aa2..cd03242 100644 --- a/tests/test_douyu_elite_exchange_flow.py +++ b/tests/test_douyu_elite_exchange_flow.py @@ -1,9 +1,9 @@ """精英手册兑换:csrf 复用、新链路接口与浏览器状态机路由的测试。""" -import pytest - from unittest.mock import Mock, call +import pytest + from core.douyu.activity_client import DouyuActivityClient, DouyuActivityError diff --git a/tests/test_douyu_elite_lock_exchange.py b/tests/test_douyu_elite_lock_exchange.py index 7bf31b7..3ac2d41 100644 --- a/tests/test_douyu_elite_lock_exchange.py +++ b/tests/test_douyu_elite_lock_exchange.py @@ -1,6 +1,6 @@ -import pytest from unittest.mock import Mock +import pytest import requests from core.douyu.activity_client import DouyuActivityClient, DouyuActivityError diff --git a/tests/test_douyu_gold_recharge_channel.py b/tests/test_douyu_gold_recharge_channel.py index 3337672..1bb49e2 100644 --- a/tests/test_douyu_gold_recharge_channel.py +++ b/tests/test_douyu_gold_recharge_channel.py @@ -7,13 +7,13 @@ from unittest.mock import ANY, AsyncMock, Mock, patch from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker +from core.douyu import FishFinRechargeClient, FishFinRechargeConfig from web.backend.database import Base from web.backend.models import Account, AuditLog, DouyuTask, LoginTask, User from web.backend.routers.douyu import get_recharge_channel, supplier_recharge_callback +from web.backend.services.audit_service import record_audit from web.backend.services.douyu_runner import DouyuBatchRunner from web.backend.services.douyu_service import ensure_douyu_config -from web.backend.services.audit_service import record_audit -from core.douyu import FishFinRechargeClient, FishFinRechargeConfig class TestDouyuGoldRechargeChannel: diff --git a/tests/test_douyu_mobile_bind_skip.py b/tests/test_douyu_mobile_bind_skip.py index cb62697..bf3d2e0 100644 --- a/tests/test_douyu_mobile_bind_skip.py +++ b/tests/test_douyu_mobile_bind_skip.py @@ -1,6 +1,6 @@ from types import SimpleNamespace -from unittest.mock import Mock from typing import Any, cast +from unittest.mock import Mock from core.douyu.login import DouyuLogin from core.douyu.login_api_wgapi import WgapiLoginAPI diff --git a/tests/test_douyu_runner_proxy.py b/tests/test_douyu_runner_proxy.py index b1dc6f8..8c4afa2 100644 --- a/tests/test_douyu_runner_proxy.py +++ b/tests/test_douyu_runner_proxy.py @@ -3,12 +3,12 @@ from unittest.mock import Mock from core.douyu.activity_client import DouyuActivityClient +from web.backend.models import ProxyConfig from web.backend.services.douyu_runner import DouyuBatchRunner from web.backend.services.douyu_runner_core import ( - DouyuBatchRunnerCore, DOUYU_PROXY_TASK_TYPES, + DouyuBatchRunnerCore, ) -from web.backend.models import ProxyConfig def _cfg(**overrides) -> ProxyConfig: diff --git a/tests/test_douyu_workbench_scopes.py b/tests/test_douyu_workbench_scopes.py index 24e992a..118d966 100644 --- a/tests/test_douyu_workbench_scopes.py +++ b/tests/test_douyu_workbench_scopes.py @@ -1,5 +1,4 @@ import pytest - from sqlalchemy import create_engine, event from sqlalchemy.orm import sessionmaker diff --git a/tests/test_douyu_xpd_exchange.py b/tests/test_douyu_xpd_exchange.py index 943284c..795b350 100644 --- a/tests/test_douyu_xpd_exchange.py +++ b/tests/test_douyu_xpd_exchange.py @@ -1,9 +1,10 @@ -import pytest import json import re from types import SimpleNamespace from unittest.mock import Mock +import pytest + from core.douyu.activity_client import DouyuActivityClient, DouyuActivityError diff --git a/tests/test_fish_fin_recharge_api.py b/tests/test_fish_fin_recharge_api.py index 659338d..dc33b52 100644 --- a/tests/test_fish_fin_recharge_api.py +++ b/tests/test_fish_fin_recharge_api.py @@ -1,9 +1,9 @@ """鱼翅直充供应商 API 客户端测试。""" -import pytest - from unittest.mock import Mock +import pytest + from core.douyu.recharge_api import ( FishFinRechargeClient, FishFinRechargeConfig, diff --git a/tests/test_huya_app_login.py b/tests/test_huya_app_login.py index 403964a..e2ac958 100644 --- a/tests/test_huya_app_login.py +++ b/tests/test_huya_app_login.py @@ -1,21 +1,15 @@ """虎牙 App 密码登录及相关组件测试。""" -import pytest - -import base64 import os import struct -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch +import pytest from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from core.huya import ( HuyaAppLoginError, - HuyaAppPasswordLogin, - HuyaAppQrAuthRequiredError, - login_huya_app_password, - login_huya_password, ) from core.huya.app_login import ( DEFAULT_GOLDEN_DEV, @@ -30,17 +24,16 @@ from core.huya.login import HuyaLoginResult from core.huya.nonce_forge import K1_DEFAULT, gen_nonce from core.huya.udb_aes import udb_decrypt, udb_encrypt from core.huya.wup_encoder import build_password_login_wup - from web.backend.database import Base -from web.backend.models import User, HuyaAccount -from web.backend.schemas import ( - HuyaAppPasswordLoginRequest, - HuyaPasswordLoginSelectedRequest, -) +from web.backend.models import HuyaAccount, User from web.backend.routers.huya import ( app_password_login_account, app_password_login_selected_accounts, ) +from web.backend.schemas import ( + HuyaAppPasswordLoginRequest, + HuyaPasswordLoginSelectedRequest, +) class TestHuyaAppLogin: @@ -193,9 +186,8 @@ class TestHuyaAppLogin: with patch( "core.huya.app_login.register_device", side_effect=DfpRegistrationError("注册链 HTTP 500"), - ): - with pytest.raises(HuyaAppLoginError, match="注册失败"): - login_cred_with_flow("300023887", "pw") + ), pytest.raises(HuyaAppLoginError, match="注册失败"): + login_cred_with_flow("300023887", "pw") def test_router_functions(self): mock_res = HuyaLoginResult( diff --git a/tests/test_logger.py b/tests/test_logger.py index 9d41ed3..2aea49f 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -7,9 +7,9 @@ from datetime import datetime, timedelta from pathlib import Path from utils.logger import ( + _parse_size, _SensitiveDataFilter, _SizeAndDayRotatingFileHandler, - _parse_size, ) diff --git a/tests/test_migrations.py b/tests/test_migrations.py index 589fe4b..e602f5a 100644 --- a/tests/test_migrations.py +++ b/tests/test_migrations.py @@ -68,8 +68,8 @@ class TestMigrationSmoke: def test_full_chain_applies_on_fresh_sqlite_and_matches_models(self): """空库执行整条迁移链,校验表/列/索引与模型元数据对齐。""" - from web.backend.database import Base import web.backend.database as database_module + from web.backend.database import Base original_url = database_module.DATABASE_URL with tempfile.TemporaryDirectory() as tmp: diff --git a/utils/logger.py b/utils/logger.py index d4a1817..c863e9a 100644 --- a/utils/logger.py +++ b/utils/logger.py @@ -14,7 +14,6 @@ from pathlib import Path from loguru import logger - _LOG_LEVELS = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"} _DEFAULT_RETENTION_DAYS = 14 _DEFAULT_ROTATION_SIZE = 50 * 1024 * 1024 @@ -36,7 +35,7 @@ def _parse_positive_int(value: str | None, default: int) -> int: def _parse_size(value: str | None, default: int = _DEFAULT_ROTATION_SIZE) -> int: """解析 50M、1GiB 等易读大小;非法值保持安全默认值。""" - matched = re.fullmatch(r"\s*(\d+)\s*([kmgt]?i?b?)?\s*", str(value or ""), re.I) + matched = re.fullmatch(r"\s*(\d+)\s*([kmgt]?i?b?)?\s*", str(value or ""), re.IGNORECASE) if not matched: return default amount = int(matched.group(1)) @@ -113,7 +112,7 @@ class _SizeAndDayRotatingFileHandler(BaseRotatingHandler): self._log_dir / f"{self._filename_prefix}-{day.isoformat()}{self._suffix}" ) - def shouldRollover(self, record: logging.LogRecord) -> bool: # noqa: N802 + def shouldRollover(self, record: logging.LogRecord) -> bool: if datetime.now().date() != self._active_day: return True if self.stream is None: @@ -122,7 +121,7 @@ class _SizeAndDayRotatingFileHandler(BaseRotatingHandler): message = f"{self.format(record)}\n" return self.stream.tell() + len(message.encode("utf-8")) >= self.max_bytes - def doRollover(self) -> None: # noqa: N802 + def doRollover(self) -> None: if self.stream is not None: self.stream.close() self.stream = None diff --git a/web/backend/crypto_storage.py b/web/backend/crypto_storage.py index 7a3426e..0711883 100644 --- a/web/backend/crypto_storage.py +++ b/web/backend/crypto_storage.py @@ -10,11 +10,10 @@ from dataclasses import dataclass from functools import lru_cache from Crypto.Cipher import AES +from loguru import logger from sqlalchemy import text from sqlalchemy.engine import Engine from sqlalchemy.types import Text, TypeDecorator -from loguru import logger - _PREFIX = "enc:v1:" _FALLBACK_SECRET = "douyu-login-py-dev-storage-key-change-me" diff --git a/web/backend/database.py b/web/backend/database.py index 8d33b9f..19fccab 100644 --- a/web/backend/database.py +++ b/web/backend/database.py @@ -5,7 +5,7 @@ from pathlib import Path from urllib.parse import quote_plus from sqlalchemy import create_engine, event -from sqlalchemy.orm import sessionmaker, declarative_base +from sqlalchemy.orm import declarative_base, sessionmaker PROJECT_ROOT = Path(__file__).resolve().parents[2] @@ -147,6 +147,7 @@ def _seed(): def _encrypt_existing_sensitive_data(): """启动时把历史明文敏感数据迁移为密文。""" from loguru import logger + from .crypto_storage import encrypt_existing_sensitive_data changed = encrypt_existing_sensitive_data(engine) diff --git a/web/backend/deps.py b/web/backend/deps.py index 1a1b79f..20d5faa 100644 --- a/web/backend/deps.py +++ b/web/backend/deps.py @@ -1,15 +1,15 @@ """FastAPI 依赖注入""" -from typing import Optional -from fastapi import Depends, HTTPException, Request, status, WebSocket -from fastapi.security import OAuth2PasswordBearer -from sqlalchemy.orm import Session -from jose import JWTError -from .database import get_db, SessionLocal -from .security import decode_access_token +from fastapi import Depends, HTTPException, Request, WebSocket, status +from fastapi.security import OAuth2PasswordBearer +from jose import JWTError +from sqlalchemy.orm import Session + +from .database import SessionLocal, get_db from .models import User from .permissions import get_user_permissions +from .security import decode_access_token # auto_error=False: 允许 token 为空(后续从 cookie 读取) oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login", auto_error=False) @@ -17,7 +17,7 @@ oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/auth/login", auto_error=Fals def get_current_user( request: Request, - token: Optional[str] = Depends(oauth2_scheme), + token: str | None = Depends(oauth2_scheme), db: Session = Depends(get_db), ) -> User: credentials_exc = HTTPException( @@ -61,7 +61,7 @@ def require_permission(permission: str): return checker -def authenticate_websocket(websocket: WebSocket) -> Optional[User]: +def authenticate_websocket(websocket: WebSocket) -> User | None: """WebSocket 认证:从 cookie 或 query param token 中验证用户身份。 Returns: diff --git a/web/backend/main.py b/web/backend/main.py index f57d80f..53ef383 100644 --- a/web/backend/main.py +++ b/web/backend/main.py @@ -1,34 +1,36 @@ """FastAPI 入口""" import os -from pathlib import Path -import uvicorn from contextlib import asynccontextmanager +from pathlib import Path + +import uvicorn from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.gzip import GZipMiddleware -from fastapi.staticfiles import StaticFiles from fastapi.responses import FileResponse +from fastapi.staticfiles import StaticFiles from starlette.middleware.base import BaseHTTPMiddleware +from utils import setup_logger + from .database import init_db from .routers import ( - auth, - users, - accounts, account_check, + accounts, + audit, + auth, + cookies, dashboard, + douyu, + huya, login, proxy, - cookies, - huya, - douyu, + users, yyb, - audit, ) from .schemas import AppInfo from .version import get_app_version -from utils import setup_logger @asynccontextmanager @@ -45,9 +47,9 @@ async def lifespan(app: FastAPI): from loguru import logger from .database import SessionLocal - from .services.login_service import cleanup_orphan_relogin_tasks - from .services.huya_service import cleanup_orphan_huya_tasks from .services.douyu_service import cleanup_orphan_douyu_tasks + from .services.huya_service import cleanup_orphan_huya_tasks + from .services.login_service import cleanup_orphan_relogin_tasks db = SessionLocal() try: diff --git a/web/backend/migrations/env.py b/web/backend/migrations/env.py index 6116008..27044c5 100644 --- a/web/backend/migrations/env.py +++ b/web/backend/migrations/env.py @@ -1,9 +1,9 @@ """Alembic 迁移环境。""" -from logging.config import fileConfig -from pathlib import Path import os import sys +from logging.config import fileConfig +from pathlib import Path from alembic import context from sqlalchemy import engine_from_config, pool @@ -12,8 +12,8 @@ ROOT_DIR = Path(__file__).resolve().parents[3] if str(ROOT_DIR) not in sys.path: sys.path.insert(0, str(ROOT_DIR)) -from web.backend.database import Base, DATABASE_URL # noqa: E402 -from web.backend import models # noqa: F401,E402 +from web.backend import models # noqa: F401 +from web.backend.database import DATABASE_URL, Base config = context.config config.set_main_option("sqlalchemy.url", DATABASE_URL) diff --git a/web/backend/migrations/versions/20260623_0001_initial_web_schema.py b/web/backend/migrations/versions/20260623_0001_initial_web_schema.py index 2e0486d..79dda9a 100644 --- a/web/backend/migrations/versions/20260623_0001_initial_web_schema.py +++ b/web/backend/migrations/versions/20260623_0001_initial_web_schema.py @@ -5,15 +5,15 @@ Revises: Create Date: 2026-06-23 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa +from alembic import op revision: str = "20260623_0001" -down_revision: Union[str, None] = None -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = None +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def _has_table(bind, table_name: str) -> bool: diff --git a/web/backend/migrations/versions/20260624_0002_sensitive_columns_text.py b/web/backend/migrations/versions/20260624_0002_sensitive_columns_text.py index 493798f..c9970d9 100644 --- a/web/backend/migrations/versions/20260624_0002_sensitive_columns_text.py +++ b/web/backend/migrations/versions/20260624_0002_sensitive_columns_text.py @@ -5,16 +5,15 @@ Revises: 20260623_0001 Create Date: 2026-06-24 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260624_0002" -down_revision: Union[str, None] = "20260623_0001" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260623_0001" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/web/backend/migrations/versions/20260624_0003_proxy_platform.py b/web/backend/migrations/versions/20260624_0003_proxy_platform.py index 00b6a1d..11e0372 100644 --- a/web/backend/migrations/versions/20260624_0003_proxy_platform.py +++ b/web/backend/migrations/versions/20260624_0003_proxy_platform.py @@ -5,16 +5,15 @@ Revises: 20260624_0002 Create Date: 2026-06-24 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260624_0003" -down_revision: Union[str, None] = "20260624_0002" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260624_0002" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/web/backend/migrations/versions/20260704_0004_huya_foundation.py b/web/backend/migrations/versions/20260704_0004_huya_foundation.py index 43235dc..ea43e29 100644 --- a/web/backend/migrations/versions/20260704_0004_huya_foundation.py +++ b/web/backend/migrations/versions/20260704_0004_huya_foundation.py @@ -5,16 +5,15 @@ Revises: 20260624_0003 Create Date: 2026-07-04 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260704_0004" -down_revision: Union[str, None] = "20260624_0003" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260624_0003" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def _has_table(bind, table_name: str) -> bool: diff --git a/web/backend/migrations/versions/20260704_0005_huya_recharge_goods.py b/web/backend/migrations/versions/20260704_0005_huya_recharge_goods.py index 4c0452c..87317d3 100644 --- a/web/backend/migrations/versions/20260704_0005_huya_recharge_goods.py +++ b/web/backend/migrations/versions/20260704_0005_huya_recharge_goods.py @@ -5,16 +5,15 @@ Revises: 20260704_0004 Create Date: 2026-07-04 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260704_0005" -down_revision: Union[str, None] = "20260704_0004" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260704_0004" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def _has_table(bind, table_name: str) -> bool: diff --git a/web/backend/migrations/versions/20260705_0006_huya_password_accounts.py b/web/backend/migrations/versions/20260705_0006_huya_password_accounts.py index bc56add..15a08df 100644 --- a/web/backend/migrations/versions/20260705_0006_huya_password_accounts.py +++ b/web/backend/migrations/versions/20260705_0006_huya_password_accounts.py @@ -5,16 +5,15 @@ Revises: 20260704_0005 Create Date: 2026-07-05 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260705_0006" -down_revision: Union[str, None] = "20260704_0005" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260704_0005" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def _has_table(bind, table_name: str) -> bool: diff --git a/web/backend/migrations/versions/20260712_0007_huya_register_history.py b/web/backend/migrations/versions/20260712_0007_huya_register_history.py index 74bf9c6..f52d7c9 100644 --- a/web/backend/migrations/versions/20260712_0007_huya_register_history.py +++ b/web/backend/migrations/versions/20260712_0007_huya_register_history.py @@ -5,16 +5,15 @@ Revises: 20260705_0006 Create Date: 2026-07-12 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op 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 +down_revision: str | None = "20260705_0006" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def _has_table(bind, table_name: str) -> bool: diff --git a/web/backend/migrations/versions/20260724_0008_douyu_activity_tasks.py b/web/backend/migrations/versions/20260724_0008_douyu_activity_tasks.py index 10169ed..7fbd95d 100644 --- a/web/backend/migrations/versions/20260724_0008_douyu_activity_tasks.py +++ b/web/backend/migrations/versions/20260724_0008_douyu_activity_tasks.py @@ -5,16 +5,15 @@ Revises: 20260712_0007 Create Date: 2026-07-24 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260724_0008" -down_revision: Union[str, None] = "20260712_0007" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260712_0007" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def _has_table(bind, table_name: str) -> bool: diff --git a/web/backend/migrations/versions/20260725_0009_database_performance_indexes.py b/web/backend/migrations/versions/20260725_0009_database_performance_indexes.py index 96ea141..60d077c 100644 --- a/web/backend/migrations/versions/20260725_0009_database_performance_indexes.py +++ b/web/backend/migrations/versions/20260725_0009_database_performance_indexes.py @@ -5,16 +5,15 @@ Revises: 20260724_0008 Create Date: 2026-07-25 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260725_0009" -down_revision: Union[str, None] = "20260724_0008" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260724_0008" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def _has_table(bind, table_name: str) -> bool: diff --git a/web/backend/migrations/versions/20260728_0010_douyu_esports_handbook.py b/web/backend/migrations/versions/20260728_0010_douyu_esports_handbook.py index 37aa97b..0c4c475 100644 --- a/web/backend/migrations/versions/20260728_0010_douyu_esports_handbook.py +++ b/web/backend/migrations/versions/20260728_0010_douyu_esports_handbook.py @@ -5,16 +5,15 @@ Revises: 20260725_0009 Create Date: 2026-07-28 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260728_0010" -down_revision: Union[str, None] = "20260725_0009" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260725_0009" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def _columns(bind, table_name: str) -> set[str]: diff --git a/web/backend/migrations/versions/20260728_0011_douyu_esports_account_fields.py b/web/backend/migrations/versions/20260728_0011_douyu_esports_account_fields.py index dcf44a0..4aac1f0 100644 --- a/web/backend/migrations/versions/20260728_0011_douyu_esports_account_fields.py +++ b/web/backend/migrations/versions/20260728_0011_douyu_esports_account_fields.py @@ -5,16 +5,15 @@ Revises: 20260728_0010 Create Date: 2026-07-28 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260728_0011" -down_revision: Union[str, None] = "20260728_0010" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260728_0010" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def _columns(bind, table_name: str) -> set[str]: diff --git a/web/backend/migrations/versions/20260728_0012_douyu_esports_can_change_time.py b/web/backend/migrations/versions/20260728_0012_douyu_esports_can_change_time.py index dbda1cb..bd87e3b 100644 --- a/web/backend/migrations/versions/20260728_0012_douyu_esports_can_change_time.py +++ b/web/backend/migrations/versions/20260728_0012_douyu_esports_can_change_time.py @@ -5,16 +5,15 @@ Revises: 20260728_0011 Create Date: 2026-07-28 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260728_0012" -down_revision: Union[str, None] = "20260728_0011" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260728_0011" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/web/backend/migrations/versions/20260728_0013_douyu_esports_gift_config.py b/web/backend/migrations/versions/20260728_0013_douyu_esports_gift_config.py index 6ae7224..18c881a 100644 --- a/web/backend/migrations/versions/20260728_0013_douyu_esports_gift_config.py +++ b/web/backend/migrations/versions/20260728_0013_douyu_esports_gift_config.py @@ -5,16 +5,15 @@ Revises: 20260728_0012 Create Date: 2026-07-28 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260728_0013" -down_revision: Union[str, None] = "20260728_0012" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260728_0012" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def _columns(bind, table_name: str) -> set[str]: diff --git a/web/backend/migrations/versions/20260728_0014_douyu_esports_goods_snapshot.py b/web/backend/migrations/versions/20260728_0014_douyu_esports_goods_snapshot.py index ed67501..d472675 100644 --- a/web/backend/migrations/versions/20260728_0014_douyu_esports_goods_snapshot.py +++ b/web/backend/migrations/versions/20260728_0014_douyu_esports_goods_snapshot.py @@ -5,16 +5,15 @@ Revises: 20260728_0013 Create Date: 2026-07-28 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260728_0014" -down_revision: Union[str, None] = "20260728_0013" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260728_0013" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/web/backend/migrations/versions/20260805_0015_task_status_indexes.py b/web/backend/migrations/versions/20260805_0015_task_status_indexes.py index 4e81c52..cf8cfca 100644 --- a/web/backend/migrations/versions/20260805_0015_task_status_indexes.py +++ b/web/backend/migrations/versions/20260805_0015_task_status_indexes.py @@ -5,16 +5,15 @@ Revises: 20260728_0014 Create Date: 2026-08-05 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260805_0015" -down_revision: Union[str, None] = "20260728_0014" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260728_0014" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None INDEXES = [ diff --git a/web/backend/migrations/versions/20260806_0016_douyu_peace_shop.py b/web/backend/migrations/versions/20260806_0016_douyu_peace_shop.py index f90b377..9fb20d3 100644 --- a/web/backend/migrations/versions/20260806_0016_douyu_peace_shop.py +++ b/web/backend/migrations/versions/20260806_0016_douyu_peace_shop.py @@ -5,16 +5,15 @@ Revises: 20260805_0015 Create Date: 2026-08-06 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260806_0016" -down_revision: Union[str, None] = "20260805_0015" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260805_0015" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def _columns(bind, table_name: str) -> set[str]: diff --git a/web/backend/migrations/versions/20260807_0017_douyu_xpd_fragments.py b/web/backend/migrations/versions/20260807_0017_douyu_xpd_fragments.py index 3367604..c5c4f80 100644 --- a/web/backend/migrations/versions/20260807_0017_douyu_xpd_fragments.py +++ b/web/backend/migrations/versions/20260807_0017_douyu_xpd_fragments.py @@ -5,16 +5,15 @@ Revises: 20260806_0016 Create Date: 2026-08-07 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260807_0017" -down_revision: Union[str, None] = "20260806_0016" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260806_0016" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/web/backend/migrations/versions/20260807_0018_cookie_check_fields.py b/web/backend/migrations/versions/20260807_0018_cookie_check_fields.py index b52064f..8bd9379 100644 --- a/web/backend/migrations/versions/20260807_0018_cookie_check_fields.py +++ b/web/backend/migrations/versions/20260807_0018_cookie_check_fields.py @@ -5,16 +5,15 @@ Revises: 20260807_0017 Create Date: 2026-08-07 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260807_0018" -down_revision: Union[str, None] = "20260807_0017" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260807_0017" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/web/backend/migrations/versions/20260808_0019_dedupe_douyu_success_cookies.py b/web/backend/migrations/versions/20260808_0019_dedupe_douyu_success_cookies.py index 1a55f69..7bf7447 100644 --- a/web/backend/migrations/versions/20260808_0019_dedupe_douyu_success_cookies.py +++ b/web/backend/migrations/versions/20260808_0019_dedupe_douyu_success_cookies.py @@ -5,16 +5,15 @@ Revises: 20260807_0018 Create Date: 2026-08-08 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260808_0019" -down_revision: Union[str, None] = "20260807_0018" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260807_0018" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/web/backend/migrations/versions/20260812_0020_yyb_recharge.py b/web/backend/migrations/versions/20260812_0020_yyb_recharge.py index 3fe1219..11e1c18 100644 --- a/web/backend/migrations/versions/20260812_0020_yyb_recharge.py +++ b/web/backend/migrations/versions/20260812_0020_yyb_recharge.py @@ -1,13 +1,14 @@ """增加应用宝和平精英充值任务""" -from typing import Sequence, Union -from alembic import op +from collections.abc import Sequence + import sqlalchemy as sa +from alembic import op revision: str = "20260812_0020" -down_revision: Union[str, None] = "20260808_0019" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260808_0019" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/web/backend/migrations/versions/20260812_0021_yyb_qr_mediumtext.py b/web/backend/migrations/versions/20260812_0021_yyb_qr_mediumtext.py index 7c55e10..b33f3dd 100644 --- a/web/backend/migrations/versions/20260812_0021_yyb_qr_mediumtext.py +++ b/web/backend/migrations/versions/20260812_0021_yyb_qr_mediumtext.py @@ -1,15 +1,13 @@ """扩展应用宝二维码密文字段容量""" -from typing import Sequence, Union +from collections.abc import Sequence from alembic import op -import sqlalchemy as sa - revision: str = "20260812_0021" -down_revision: Union[str, None] = "20260812_0020" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260812_0020" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def _is_mysql() -> bool: diff --git a/web/backend/migrations/versions/20260812_0022_yyb_payment_meta.py b/web/backend/migrations/versions/20260812_0022_yyb_payment_meta.py index 312c9c1..e4c8f1d 100644 --- a/web/backend/migrations/versions/20260812_0022_yyb_payment_meta.py +++ b/web/backend/migrations/versions/20260812_0022_yyb_payment_meta.py @@ -1,15 +1,14 @@ """应用宝支付元数据字段:金额与支付时间线""" -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260812_0022" -down_revision: Union[str, None] = "20260812_0021" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260812_0021" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/web/backend/migrations/versions/20260813_0023_soft_delete_users.py b/web/backend/migrations/versions/20260813_0023_soft_delete_users.py index 1fead23..625415b 100644 --- a/web/backend/migrations/versions/20260813_0023_soft_delete_users.py +++ b/web/backend/migrations/versions/20260813_0023_soft_delete_users.py @@ -1,15 +1,14 @@ """支持用户软删除并保留历史任务归属""" -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260813_0023" -down_revision: Union[str, None] = "20260812_0022" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260812_0022" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def _columns(bind, table_name: str) -> set[str]: diff --git a/web/backend/migrations/versions/20260813_0024_douyu_workbench_scopes.py b/web/backend/migrations/versions/20260813_0024_douyu_workbench_scopes.py index 86c629d..6c7c31b 100644 --- a/web/backend/migrations/versions/20260813_0024_douyu_workbench_scopes.py +++ b/web/backend/migrations/versions/20260813_0024_douyu_workbench_scopes.py @@ -5,16 +5,15 @@ Revises: 20260813_0023 Create Date: 2026-08-13 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260813_0024" -down_revision: Union[str, None] = "20260813_0023" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260813_0023" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def _columns(bind, table_name: str) -> set[str]: diff --git a/web/backend/migrations/versions/20260813_0025_douyu_workbench_empty_state.py b/web/backend/migrations/versions/20260813_0025_douyu_workbench_empty_state.py index d3db005..da7d930 100644 --- a/web/backend/migrations/versions/20260813_0025_douyu_workbench_empty_state.py +++ b/web/backend/migrations/versions/20260813_0025_douyu_workbench_empty_state.py @@ -5,16 +5,15 @@ Revises: 20260813_0024 Create Date: 2026-08-13 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260813_0025" -down_revision: Union[str, None] = "20260813_0024" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260813_0024" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/web/backend/migrations/versions/20260813_0026_douyu_gold_recharge_channel.py b/web/backend/migrations/versions/20260813_0026_douyu_gold_recharge_channel.py index 98227a4..7cf0b23 100644 --- a/web/backend/migrations/versions/20260813_0026_douyu_gold_recharge_channel.py +++ b/web/backend/migrations/versions/20260813_0026_douyu_gold_recharge_channel.py @@ -5,16 +5,15 @@ Revises: 20260813_0025 Create Date: 2026-08-13 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260813_0026" -down_revision: Union[str, None] = "20260813_0025" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260813_0025" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def _columns(bind) -> set[str]: diff --git a/web/backend/migrations/versions/20260813_0027_douyu_supplier_order_id.py b/web/backend/migrations/versions/20260813_0027_douyu_supplier_order_id.py index 6055c36..20e8e60 100644 --- a/web/backend/migrations/versions/20260813_0027_douyu_supplier_order_id.py +++ b/web/backend/migrations/versions/20260813_0027_douyu_supplier_order_id.py @@ -5,16 +5,15 @@ Revises: 20260813_0026 Create Date: 2026-08-13 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260813_0027" -down_revision: Union[str, None] = "20260813_0026" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260813_0026" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/web/backend/migrations/versions/20260814_0028_douyu_nickname_recharge.py b/web/backend/migrations/versions/20260814_0028_douyu_nickname_recharge.py index 3ac303b..efdd30d 100644 --- a/web/backend/migrations/versions/20260814_0028_douyu_nickname_recharge.py +++ b/web/backend/migrations/versions/20260814_0028_douyu_nickname_recharge.py @@ -5,16 +5,15 @@ Revises: 20260813_0027 Create Date: 2026-08-14 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260814_0028" -down_revision: Union[str, None] = "20260813_0027" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260813_0027" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/web/backend/migrations/versions/20260814_0029_audit_log_result.py b/web/backend/migrations/versions/20260814_0029_audit_log_result.py index 4342e4a..2512609 100644 --- a/web/backend/migrations/versions/20260814_0029_audit_log_result.py +++ b/web/backend/migrations/versions/20260814_0029_audit_log_result.py @@ -5,16 +5,15 @@ Revises: 20260814_0028 Create Date: 2026-08-14 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260814_0029" -down_revision: Union[str, None] = "20260814_0028" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260814_0028" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/web/backend/migrations/versions/20260829_0030_huya_login_channel.py b/web/backend/migrations/versions/20260829_0030_huya_login_channel.py index f2fd854..81a99ad 100644 --- a/web/backend/migrations/versions/20260829_0030_huya_login_channel.py +++ b/web/backend/migrations/versions/20260829_0030_huya_login_channel.py @@ -5,16 +5,15 @@ Revises: 20260814_0029 Create Date: 2026-08-29 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260829_0030" -down_revision: Union[str, None] = "20260814_0029" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260814_0029" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None def upgrade() -> None: diff --git a/web/backend/migrations/versions/20260830_0031_mysql_task_query_indexes.py b/web/backend/migrations/versions/20260830_0031_mysql_task_query_indexes.py index ce7df3c..13e4132 100644 --- a/web/backend/migrations/versions/20260830_0031_mysql_task_query_indexes.py +++ b/web/backend/migrations/versions/20260830_0031_mysql_task_query_indexes.py @@ -5,16 +5,15 @@ Revises: 20260829_0030 Create Date: 2026-08-30 """ -from typing import Sequence, Union +from collections.abc import Sequence -from alembic import op import sqlalchemy as sa - +from alembic import op revision: str = "20260830_0031" -down_revision: Union[str, None] = "20260829_0030" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None +down_revision: str | None = "20260829_0030" +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None INDEXES = ( diff --git a/web/backend/models.py b/web/backend/models.py index 05fcda0..0159c47 100644 --- a/web/backend/models.py +++ b/web/backend/models.py @@ -2,21 +2,22 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime + from sqlalchemy import ( - Column, - Integer, - String, + JSON, Boolean, - Text, DateTime, ForeignKey, - JSON, + Integer, + String, + Text, UniqueConstraint, ) from sqlalchemy.orm import Mapped, mapped_column, relationship -from .database import Base + from .crypto_storage import EncryptedText +from .database import Base from .huya_defaults import ( HUYA_DEFAULT_BIND_ACT_ID, HUYA_DEFAULT_OUTER_ACT_ID, @@ -28,7 +29,7 @@ from .huya_defaults import ( def _utcnow(): """返回时区感知的 UTC 当前时间,替代 _utcnow()。""" - return datetime.now(timezone.utc) + return datetime.now(UTC) class User(Base): diff --git a/web/backend/routers/account_check.py b/web/backend/routers/account_check.py index 7900e68..cd44842 100644 --- a/web/backend/routers/account_check.py +++ b/web/backend/routers/account_check.py @@ -10,7 +10,8 @@ from sqlalchemy.orm import Session from ..database import get_db from ..deps import get_current_user -from ..models import ProxyConfig as ProxyConfigModel, User +from ..models import ProxyConfig as ProxyConfigModel +from ..models import User from ..permissions import user_has_permission from ..schemas import AccountCheckBatchOut, AccountCheckBatchRequest from ..services.account_check_service import ( @@ -18,7 +19,6 @@ from ..services.account_check_service import ( parse_account_check_lines, ) - router = APIRouter(prefix="/api/account-check", tags=["账号检测"]) diff --git a/web/backend/routers/accounts.py b/web/backend/routers/accounts.py index ef2a83c..efa5691 100644 --- a/web/backend/routers/accounts.py +++ b/web/backend/routers/accounts.py @@ -5,25 +5,25 @@ from sqlalchemy import func, or_, select from sqlalchemy.orm import Session, defer, joinedload from ..database import get_db +from ..deps import get_current_user, require_permission from ..models import ( - User, Account, AuditLog, - LoginTask, DouyuTask, DouyuWorkbenchAccount, + LoginTask, + User, ) +from ..permissions import user_has_permission from ..schemas import ( + AccountAssign, AccountBulkSelection, AccountBulkTag, AccountImport, - AccountAssign, - AccountTag, AccountOut, + AccountTag, BatchAssign, ) -from ..deps import get_current_user, require_permission -from ..permissions import user_has_permission from ..services.account_service import ( cookie_account_ids_query, parse_and_build_accounts, diff --git a/web/backend/routers/audit.py b/web/backend/routers/audit.py index c7d54e1..6fd8de3 100644 --- a/web/backend/routers/audit.py +++ b/web/backend/routers/audit.py @@ -12,7 +12,6 @@ from ..database import get_db from ..deps import get_current_user from ..models import Account, AuditLog, DouyuTask, User - router = APIRouter(prefix="/api/audit-logs", tags=["审计日志"]) diff --git a/web/backend/routers/auth.py b/web/backend/routers/auth.py index 404d8df..091c346 100644 --- a/web/backend/routers/auth.py +++ b/web/backend/routers/auth.py @@ -1,16 +1,16 @@ """认证路由""" import os -from datetime import datetime + from fastapi import APIRouter, Depends, HTTPException, Response from sqlalchemy.orm import Session from ..database import get_db -from ..models import User, AuditLog -from ..security import verify_password, create_access_token, ACCESS_TOKEN_EXPIRE_HOURS -from ..permissions import get_user_permissions, ROLE_LABELS -from ..schemas import LoginRequest, TokenResponse from ..deps import get_current_user +from ..models import AuditLog, User +from ..permissions import ROLE_LABELS, get_user_permissions +from ..schemas import LoginRequest, TokenResponse +from ..security import ACCESS_TOKEN_EXPIRE_HOURS, create_access_token, verify_password router = APIRouter(prefix="/api/auth", tags=["认证"]) diff --git a/web/backend/routers/cookies.py b/web/backend/routers/cookies.py index 429e636..9eccc7f 100644 --- a/web/backend/routers/cookies.py +++ b/web/backend/routers/cookies.py @@ -1,22 +1,24 @@ """Cookie 管理路由""" +import csv +import io import threading from concurrent.futures import ThreadPoolExecutor, as_completed -from datetime import datetime, timezone +from datetime import UTC, datetime + from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import StreamingResponse from sqlalchemy import case, func, or_ from sqlalchemy.orm import Session, defer, joinedload -import io -import csv -from ..database import get_db, SessionLocal -from ..models import User, LoginTask, Account, AuditLog, ProxyConfig as ProxyConfigModel +from ..database import SessionLocal, get_db from ..deps import get_current_user, require_permission -from ..permissions import user_has_permission, get_user_permissions +from ..models import Account, AuditLog, LoginTask, User +from ..models import ProxyConfig as ProxyConfigModel +from ..permissions import get_user_permissions, user_has_permission from ..schemas import CookieReloginRequest -from ..services.login_service import BatchRegistry, LoginBatchRunner, get_relogin_limits from ..services.cookie_check_service import check_douyu_cookie +from ..services.login_service import BatchRegistry, LoginBatchRunner, get_relogin_limits def _fmt_dt(dt) -> str | None: @@ -24,7 +26,7 @@ def _fmt_dt(dt) -> str | None: if dt is None: return None if dt.tzinfo is None: - dt = dt.replace(tzinfo=timezone.utc) + dt = dt.replace(tzinfo=UTC) return dt.isoformat() @@ -491,7 +493,7 @@ def _check_cookies(ids: str, db: Session, current: User, *, detailed: bool) -> d "fish_ball": None, "nickname": None, "level": None, - "checked_at": datetime.now(timezone.utc).isoformat(), + "checked_at": datetime.now(UTC).isoformat(), } ) results.sort(key=lambda item: item["id"]) @@ -509,7 +511,7 @@ def _check_cookies(ids: str, db: Session, current: User, *, detailed: bool) -> d "level": item.get("level"), "message": item.get("message", ""), } - task.ck_checked_at = datetime.now(timezone.utc) + task.ck_checked_at = datetime.now(UTC) db.add( AuditLog( user_id=current.id, @@ -571,7 +573,7 @@ def _start_relogin_tasks( if not acc.password or not acc.email: task.status = "relogin_failed" task.message = "重新登录失败: 账号缺少密码或邮箱,旧 Cookie 已保留" - task.finished_at = datetime.now(timezone.utc) + task.finished_at = datetime.now(UTC) skipped.append(task.id) continue if task.status in {"relogin_pending", "relogin_running"}: diff --git a/web/backend/routers/dashboard.py b/web/backend/routers/dashboard.py index e340d67..96ce8c5 100644 --- a/web/backend/routers/dashboard.py +++ b/web/backend/routers/dashboard.py @@ -17,7 +17,6 @@ from ..models import ( ) from ..permissions import user_has_permission - router = APIRouter(prefix="/api/dashboard", tags=["首页概览"]) diff --git a/web/backend/routers/douyu.py b/web/backend/routers/douyu.py index b91e664..8232930 100644 --- a/web/backend/routers/douyu.py +++ b/web/backend/routers/douyu.py @@ -4,7 +4,7 @@ from __future__ import annotations import asyncio import threading -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any, cast from fastapi import ( @@ -18,7 +18,13 @@ from fastapi import ( ) from loguru import logger from sqlalchemy import func, or_, select -from sqlalchemy.orm import Session, joinedload, load_only +from sqlalchemy.orm import Session, joinedload + +from core.douyu import ( + FishFinRechargeClient, + FishFinRechargeConfig, + FishFinRechargeConfigError, +) from ..database import SessionLocal, get_db from ..deps import authenticate_websocket, get_current_user, require_permission @@ -45,26 +51,19 @@ from ..schemas import ( DouyuWorkbenchAccountsUpdate, DouyuXpdGoodsOut, ) -from ..services.douyu_runner import DouyuBatchRunner, douyu_batch_registry from ..services.audit_service import record_audit +from ..services.douyu_runner import DouyuBatchRunner, douyu_batch_registry from ..services.douyu_service import ( DOUYU_CONFIG_FIELDS, DOUYU_HANDBOOK_TASK_TYPES, SUPPORTED_DOUYU_TASK_TYPES, apply_douyu_config_defaults, cleanup_orphan_douyu_tasks, - cookie_account_ids_query, create_douyu_planned_tasks, douyu_config_value, douyu_task_payload, ensure_douyu_config, ) -from core.douyu import ( - FishFinRechargeClient, - FishFinRechargeConfig, - FishFinRechargeConfigError, -) - router = APIRouter(prefix="/api/douyu", tags=["斗鱼活动"]) @@ -132,10 +131,10 @@ async def supplier_recharge_callback(request: Request, db: Session = Depends(get if status == 2: task.status = "success" task.message = "供应商直充成功(异步通知)" - task.finished_at = datetime.now(timezone.utc) + task.finished_at = datetime.now(UTC) if task.account: task.account.bind_status = "gold_recharged" - task.account.updated_at = datetime.now(timezone.utc) + task.account.updated_at = datetime.now(UTC) elif status in {3, 4}: reason = str( _supplier_response_value(payload, "fail_reason", "message", "msg") @@ -143,7 +142,7 @@ async def supplier_recharge_callback(request: Request, db: Session = Depends(get ) task.status = "failed" task.message = reason[:512] - task.finished_at = datetime.now(timezone.utc) + task.finished_at = datetime.now(UTC) else: task.status = "running" task.message = ( @@ -466,7 +465,7 @@ def update_workbench_accounts( if workbench is None: db.add(DouyuWorkbench(user_id=current.id, handbook_scope=req.handbook_scope)) else: - workbench.updated_at = datetime.now(timezone.utc) + workbench.updated_at = datetime.now(UTC) db.query(DouyuWorkbenchAccount).filter( DouyuWorkbenchAccount.user_id == current.id, DouyuWorkbenchAccount.handbook_scope == req.handbook_scope, @@ -525,7 +524,7 @@ def update_config( setattr(config, field, value.strip() if isinstance(value, str) else value) changed_fields.append(field) apply_douyu_config_defaults(config) - config.updated_at = datetime.now(timezone.utc) + config.updated_at = datetime.now(UTC) recharge_fields = [field for field in changed_fields if field.startswith("gold_")] if recharge_fields: record_audit( @@ -853,7 +852,7 @@ async def ws_douyu_logs(websocket: WebSocket, batch_id: str): if msg.get("level") == "result": await asyncio.sleep(0.1) break - except asyncio.TimeoutError: + except TimeoutError: await websocket.send_json({"level": "heartbeat", "message": ""}) except WebSocketDisconnect: pass diff --git a/web/backend/routers/huya.py b/web/backend/routers/huya.py index dc2613c..3bfb7e5 100644 --- a/web/backend/routers/huya.py +++ b/web/backend/routers/huya.py @@ -3,14 +3,11 @@ import asyncio import csv import io -import json import shutil import threading -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import cast -from pathlib import Path - from fastapi import ( APIRouter, Depends, @@ -37,6 +34,8 @@ from core.huya.cookie_utils import normalize_huya_cookie from core.huya.device_fingerprint import account_state_dir as _fp_state_dir from core.huya.device_profile import ( _load_db as _load_profile_db, +) +from core.huya.device_profile import ( _save_db as _save_profile_db, ) from core.sms_provider import parse_sms_lines @@ -62,6 +61,7 @@ from ..schemas import ( AccountTag, BatchAssign, HuyaAccountOut, + HuyaAppPasswordLoginRequest, HuyaAutoRegisterBatchOut, HuyaAutoRegisterRequest, HuyaAutoRegisterRetryRequest, @@ -72,7 +72,6 @@ from ..schemas import ( HuyaGoodsOut, HuyaPasswordAccountImport, HuyaPasswordLoginRequest, - HuyaAppPasswordLoginRequest, HuyaPasswordLoginSelectedRequest, HuyaRechargeGoodsOut, HuyaRegisterSuccessLogOut, @@ -81,6 +80,13 @@ from ..schemas import ( HuyaTaskBatchRequest, HuyaTaskOut, ) +from ..services.audit_service import record_audit +from ..services.huya_register_runner import ( + export_success_logs_text, + huya_register_registry, + list_success_logs, +) +from ..services.huya_runner import HuyaBatchRunner, huya_batch_registry from ..services.huya_service import ( HUYA_CONFIG_FIELDS, SUPPORTED_TASK_TYPES, @@ -94,14 +100,6 @@ from ..services.huya_service import ( save_huya_login_cookie_to_account, upsert_huya_cookie, ) -from ..services.huya_runner import HuyaBatchRunner, huya_batch_registry -from ..services.audit_service import record_audit -from ..services.huya_register_runner import ( - export_success_logs_text, - huya_register_registry, - list_success_logs, -) - router = APIRouter(prefix="/api/huya", tags=["虎牙"]) @@ -760,7 +758,7 @@ def sms_login_account( phone = (req.phone or "").strip() if phone: account.game_phone = phone - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) db.commit() db.refresh(account) except ValueError as exc: @@ -1011,7 +1009,7 @@ def password_login_selected_accounts( ) if not result.success or not result.cookie: account.status = "login_failed" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) db.commit() failed_count += 1 results.append( @@ -1051,7 +1049,7 @@ def password_login_selected_accounts( ) except HuyaCredentialError as exc: account.status = "login_failed" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) db.commit() failed_count += 1 results.append( @@ -1065,7 +1063,7 @@ def password_login_selected_accounts( ) except (HuyaLoginError, ValueError) as exc: account.status = "login_failed" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) db.commit() failed_count += 1 results.append( @@ -1079,7 +1077,7 @@ def password_login_selected_accounts( ) except Exception as exc: account.status = "login_failed" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) db.commit() failed_count += 1 results.append( @@ -1163,7 +1161,7 @@ def app_password_login_selected_accounts( ) if not result.success or not result.cookie: account.status = "login_failed" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) db.commit() failed_count += 1 results.append( @@ -1201,7 +1199,7 @@ def app_password_login_selected_accounts( ) except HuyaAppQrAuthRequiredError as exc: account.status = "login_failed" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) db.commit() failed_count += 1 results.append( @@ -1215,7 +1213,7 @@ def app_password_login_selected_accounts( ) except HuyaCredentialError as exc: account.status = "login_failed" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) db.commit() failed_count += 1 results.append( @@ -1229,7 +1227,7 @@ def app_password_login_selected_accounts( ) except (HuyaAppLoginError, HuyaLoginError, ValueError) as exc: account.status = "login_failed" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) db.commit() failed_count += 1 results.append( @@ -1243,7 +1241,7 @@ def app_password_login_selected_accounts( ) except Exception as exc: account.status = "login_failed" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) db.commit() failed_count += 1 results.append( @@ -1729,7 +1727,7 @@ def update_config( setattr(config, field, value.strip()) changed_fields.append(field) apply_huya_config_defaults(config) - config.updated_at = datetime.now(timezone.utc) + config.updated_at = datetime.now(UTC) if changed_fields: record_audit( db, @@ -1995,7 +1993,7 @@ async def ws_huya_logs(websocket: WebSocket, batch_id: str): if msg.get("level") == "result": await asyncio.sleep(0.1) break - except asyncio.TimeoutError: + except TimeoutError: await websocket.send_json({"level": "heartbeat", "message": ""}) except WebSocketDisconnect: pass diff --git a/web/backend/routers/login.py b/web/backend/routers/login.py index 3718e0b..b45ff1e 100644 --- a/web/backend/routers/login.py +++ b/web/backend/routers/login.py @@ -2,16 +2,17 @@ import asyncio import threading -from datetime import datetime + from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect from sqlalchemy import func from sqlalchemy.orm import Session, defer, load_only -from ..database import get_db, SessionLocal -from ..models import User, Account, LoginTask, ProxyConfig as ProxyConfigModel +from ..database import SessionLocal, get_db +from ..deps import authenticate_websocket, get_current_user, require_permission +from ..models import Account, LoginTask, User +from ..models import ProxyConfig as ProxyConfigModel +from ..permissions import get_user_permissions, user_has_permission from ..schemas import LoginBatchRequest, LoginTaskOut -from ..deps import get_current_user, require_permission, authenticate_websocket -from ..permissions import user_has_permission, get_user_permissions from ..services.login_service import LoginBatchRunner, batch_registry router = APIRouter(prefix="/api/login", tags=["登录任务"]) @@ -259,7 +260,7 @@ async def ws_login_logs(websocket: WebSocket, batch_id: str): if msg.get("level") == "result": await asyncio.sleep(0.1) break - except asyncio.TimeoutError: + except TimeoutError: await websocket.send_json({"level": "heartbeat", "message": ""}) except WebSocketDisconnect: pass diff --git a/web/backend/routers/proxy.py b/web/backend/routers/proxy.py index fddf8ff..4aad1e3 100644 --- a/web/backend/routers/proxy.py +++ b/web/backend/routers/proxy.py @@ -2,20 +2,22 @@ import asyncio import threading + from fastapi import APIRouter, Depends, WebSocket, WebSocketDisconnect from sqlalchemy.orm import Session -from ..database import get_db -from ..models import User -from ..schemas import ProxyConfigOut, ProxyConfigUpdate, PlatformInfo, PlatformFieldDef -from ..deps import require_permission, authenticate_websocket -from ..services.proxy_service import proxy_service from core.douyu.proxy_platforms import ( - get_platform_names, - get_platform_labels, get_credential_fields, + get_platform_labels, + get_platform_names, ) +from ..database import get_db +from ..deps import authenticate_websocket, require_permission +from ..models import User +from ..schemas import PlatformFieldDef, PlatformInfo, ProxyConfigOut, ProxyConfigUpdate +from ..services.proxy_service import proxy_service + router = APIRouter(prefix="/api/proxy", tags=["代理与白名单"]) @@ -97,7 +99,7 @@ async def ws_test_logs(websocket: WebSocket, test_id: str): if msg.get("level") == "result": await asyncio.sleep(0.1) break - except asyncio.TimeoutError: + except TimeoutError: await websocket.send_json({"level": "heartbeat", "message": ""}) except WebSocketDisconnect: pass diff --git a/web/backend/routers/users.py b/web/backend/routers/users.py index dc37f36..0f2c3ed 100644 --- a/web/backend/routers/users.py +++ b/web/backend/routers/users.py @@ -1,6 +1,6 @@ """用户管理路由(超管)""" -from datetime import datetime, timezone +from datetime import UTC, datetime from uuid import uuid4 from fastapi import APIRouter, Depends, HTTPException @@ -8,11 +8,11 @@ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from ..database import get_db -from ..models import Account, AuditLog, HuyaAccount, User -from ..security import hash_password -from ..permissions import PERMISSIONS, get_user_permissions -from ..schemas import UserCreate, UserRename, UserUpdate, UserInfo from ..deps import require_permission +from ..models import Account, AuditLog, HuyaAccount, User +from ..permissions import PERMISSIONS, get_user_permissions +from ..schemas import UserCreate, UserInfo, UserRename, UserUpdate +from ..security import hash_password router = APIRouter(prefix="/api/users", tags=["用户管理"]) @@ -221,7 +221,7 @@ def delete_user( ) ) user.is_active = False - user.deleted_at = datetime.now(timezone.utc) + user.deleted_at = datetime.now(UTC) user.deleted_username = deleted_username # 释放用户名供新用户使用,同时不改变历史任务通过 user.id 保存的归属。 user.username = f"deleted-{user.id}-{uuid4().hex[:12]}" diff --git a/web/backend/routers/yyb.py b/web/backend/routers/yyb.py index 6b489ad..ff58bce 100644 --- a/web/backend/routers/yyb.py +++ b/web/backend/routers/yyb.py @@ -12,10 +12,9 @@ from ..deps import get_current_user, require_permission from ..models import User, YybRechargeTask from ..permissions import user_has_permission from ..schemas import YybLoginRequest, YybSelectionRequest, YybTaskCreateRequest +from ..services.audit_service import record_audit from ..services.yyb_service import _utcnow, public_task, sync_task from ..services.yyb_worker_client import YybWorkerClient, YybWorkerError -from ..services.audit_service import record_audit - router = APIRouter(prefix="/api/yyb", tags=["应用宝充值"]) diff --git a/web/backend/schemas.py b/web/backend/schemas.py index 388b488..692f383 100644 --- a/web/backend/schemas.py +++ b/web/backend/schemas.py @@ -1,8 +1,9 @@ """Pydantic 请求/响应模型""" -from datetime import datetime, timezone -from typing import Optional, Any -from pydantic import BaseModel, Field, ConfigDict, model_serializer +from datetime import UTC, datetime +from typing import Any + +from pydantic import BaseModel, ConfigDict, Field, model_serializer from .huya_defaults import ( HUYA_DEFAULT_BIND_ACT_ID, @@ -32,16 +33,16 @@ class YybSelectionRequest(BaseModel): role_name: str = Field(..., min_length=1, max_length=128) -def _ensure_tz(dt: Optional[datetime]) -> Optional[datetime]: +def _ensure_tz(dt: datetime | None) -> datetime | None: """确保 datetime 带有 UTC 时区信息,无时区的视为 UTC。""" if dt is None: return None if dt.tzinfo is None: - return dt.replace(tzinfo=timezone.utc) + return dt.replace(tzinfo=UTC) return dt -def _isoformat(dt: Optional[datetime]) -> str | None: +def _isoformat(dt: datetime | None) -> str | None: normalized = _ensure_tz(dt) return normalized.isoformat() if normalized is not None else None @@ -66,9 +67,9 @@ class UserInfo(BaseModel): role: str is_active: bool remark: str = "" - created_at: Optional[datetime] = None + created_at: datetime | None = None permissions: list[str] = [] - custom_permissions: Optional[list[str]] = None + custom_permissions: list[str] | None = None model_config = ConfigDict(from_attributes=True) @@ -95,11 +96,11 @@ class UserCreate(BaseModel): class UserUpdate(BaseModel): - password: Optional[str] = None - role: Optional[str] = None - is_active: Optional[bool] = None - remark: Optional[str] = None - custom_permissions: Optional[list[str]] = None + password: str | None = None + role: str | None = None + is_active: bool | None = None + remark: str | None = None + custom_permissions: list[str] | None = None class UserRename(BaseModel): @@ -119,12 +120,12 @@ class AccountImport(BaseModel): class AccountAssign(BaseModel): - assigned_to: Optional[int] = None + assigned_to: int | None = None class BatchAssign(BaseModel): account_ids: list[int] - assigned_to: Optional[int] = None # None=取消分配 + assigned_to: int | None = None # None=取消分配 class AccountBulkSelection(BaseModel): @@ -143,22 +144,22 @@ class AccountBulkTag(AccountBulkSelection): class AccountTag(BaseModel): - tag: Optional[str] = None - account_ids: Optional[list[int]] = None + tag: str | None = None + account_ids: list[int] | None = None class AccountOut(BaseModel): id: int username: str # 敏感字段根据角色决定是否返回 - password: Optional[str] = None - email: Optional[str] = None - email_password: Optional[str] = None + password: str | None = None + email: str | None = None + email_password: str | None = None tag: str = "" - assigned_to: Optional[int] = None - assigned_username: Optional[str] = None + assigned_to: int | None = None + assigned_username: str | None = None remark: str = "" - created_at: Optional[datetime] = None + created_at: datetime | None = None model_config = ConfigDict(from_attributes=True) @@ -203,8 +204,8 @@ class LoginTaskOut(BaseModel): cookie: str = "" message: str = "" created_by: int - created_at: Optional[datetime] = None - finished_at: Optional[datetime] = None + created_at: datetime | None = None + finished_at: datetime | None = None model_config = ConfigDict(from_attributes=True) @@ -239,8 +240,8 @@ class AccountCheckItemOut(BaseModel): email: str status: str message: str - started_at: Optional[datetime] = None - finished_at: Optional[datetime] = None + started_at: datetime | None = None + finished_at: datetime | None = None class AccountCheckBatchOut(BaseModel): @@ -256,9 +257,9 @@ class AccountCheckBatchOut(BaseModel): finished_count: int running_count: int status_counts: dict[str, int] - created_at: Optional[datetime] = None - started_at: Optional[datetime] = None - finished_at: Optional[datetime] = None + created_at: datetime | None = None + started_at: datetime | None = None + finished_at: datetime | None = None items: list[AccountCheckItemOut] @@ -327,12 +328,12 @@ class HuyaAutoRegisterRetryRequest(BaseModel): """ mode: str = Field("continue", max_length=32) - 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 + concurrency: int | None = Field(None, ge=1, le=5) + wait_seconds: float | None = Field(None, ge=15, le=600) + poll_interval: float | None = Field(None, ge=1, le=30) + password_prefix: str | None = Field(None, max_length=8) + fixed_password: str | None = Field(None, max_length=64) + use_proxy: bool | None = None class HuyaAutoRegisterItemOut(BaseModel): @@ -346,15 +347,15 @@ class HuyaAutoRegisterItemOut(BaseModel): change_code: str = "" attempts: int = 0 change_attempts: int = 0 - account_id: Optional[int] = None + account_id: int | None = None username: str = "" uid: str = "" password: str = "" password_changed: bool = False cookie: str = "" cookie_preview: str = "" - started_at: Optional[datetime] = None - finished_at: Optional[datetime] = None + started_at: datetime | None = None + finished_at: datetime | None = None class HuyaAutoRegisterBatchOut(BaseModel): @@ -373,17 +374,17 @@ class HuyaAutoRegisterBatchOut(BaseModel): failed_count: int stopped_count: int running_count: int - created_at: Optional[datetime] = None - started_at: Optional[datetime] = None - finished_at: Optional[datetime] = None + created_at: datetime | None = None + started_at: datetime | None = None + finished_at: datetime | None = None items: list[HuyaAutoRegisterItemOut] = [] class HuyaRegisterSuccessLogOut(BaseModel): id: int batch_id: str = "" - item_id: Optional[int] = None - account_id: Optional[int] = None + item_id: int | None = None + account_id: int | None = None phone: str = "" username: str = "" uid: str = "" @@ -391,8 +392,8 @@ class HuyaRegisterSuccessLogOut(BaseModel): sms_url: str = "" tag: str = "" provider: str = "" - created_by: Optional[int] = None - created_at: Optional[datetime] = None + created_by: int | None = None + created_at: datetime | None = None export_line: str = "" @@ -435,14 +436,14 @@ class HuyaAccountOut(BaseModel): status: str = "" # 上次成功登录渠道: web / app / sms (空 = 仅导入 Cookie, 从未协议登录) login_channel: str = "" - points: Optional[int] = None + points: int | None = None game_name: str = "" game_channel: str = "" game_phone: str = "" - assigned_to: Optional[int] = None - assigned_username: Optional[str] = None - created_at: Optional[datetime] = None - updated_at: Optional[datetime] = None + assigned_to: int | None = None + assigned_username: str | None = None + created_at: datetime | None = None + updated_at: datetime | None = None model_config = ConfigDict(from_attributes=True) @@ -477,7 +478,7 @@ class HuyaConfigOut(BaseModel): outer_act_id: str = HUYA_DEFAULT_OUTER_ACT_ID bind_act_id: str = HUYA_DEFAULT_BIND_ACT_ID pay_channel: str = HUYA_DEFAULT_PAY_CHANNEL - updated_at: Optional[datetime] = None + updated_at: datetime | None = None @model_serializer def _serialize(self) -> dict[str, Any]: @@ -492,11 +493,11 @@ class HuyaConfigOut(BaseModel): class HuyaConfigUpdate(BaseModel): - room_pid: Optional[str] = None - sid: Optional[str] = None - outer_act_id: Optional[str] = None - bind_act_id: Optional[str] = None - pay_channel: Optional[str] = None + room_pid: str | None = None + sid: str | None = None + outer_act_id: str | None = None + bind_act_id: str | None = None + pay_channel: str | None = None class HuyaTaskBatchRequest(BaseModel): @@ -515,10 +516,10 @@ class HuyaTaskOut(BaseModel): task_type: str status: str message: str = "" - result: Optional[dict[str, Any]] = None + result: dict[str, Any] | None = None created_by: int - created_at: Optional[datetime] = None - finished_at: Optional[datetime] = None + created_at: datetime | None = None + finished_at: datetime | None = None model_config = ConfigDict(from_attributes=True) @@ -544,10 +545,10 @@ class HuyaGoodsOut(BaseModel): id: int product_id: str name: str = "" - price: Optional[int] = None + price: int | None = None remain_text: str = "" - raw: Optional[dict[str, Any]] = None - updated_at: Optional[datetime] = None + raw: dict[str, Any] | None = None + updated_at: datetime | None = None model_config = ConfigDict(from_attributes=True) @@ -569,15 +570,15 @@ class HuyaRechargeGoodsOut(BaseModel): spu_id: str sku_id: str = "" name: str = "" - price: Optional[int] = None - stock: Optional[int] = None - buy_limit: Optional[int] = None + price: int | None = None + stock: int | None = None + buy_limit: int | None = None icon: str = "" description: str = "" task_id: str = "" task_name: str = "" - raw: Optional[dict[str, Any]] = None - updated_at: Optional[datetime] = None + raw: dict[str, Any] | None = None + updated_at: datetime | None = None model_config = ConfigDict(from_attributes=True) @@ -625,7 +626,7 @@ class DouyuConfigOut(BaseModel): gold_api_account_template_name: str = "斗鱼昵称" gift_id: str = "23643" skin_id: str = "2942" - updated_at: Optional[datetime] = None + updated_at: datetime | None = None @model_serializer def _serialize(self) -> dict[str, Any]: @@ -658,31 +659,31 @@ class DouyuConfigOut(BaseModel): class DouyuConfigUpdate(BaseModel): - manual_id: Optional[str] = None - rid: Optional[str] = None - bind_act_alias: Optional[str] = None - confirm_act_alias: Optional[str] = None - legacy_act_alias: Optional[str] = None - room_id: Optional[str] = None - elite_amount: Optional[int] = Field(None, ge=1) - esports_manual_id: Optional[str] = None - esports_act_alias: Optional[str] = None - esports_amount: Optional[int] = Field(None, ge=1) - esports_chicken_gift_id: Optional[str] = None - esports_chicken_skin_id: Optional[str] = None - esports_firework_gift_id: Optional[str] = None - esports_firework_skin_id: Optional[str] = None - xpd_act_alias: Optional[str] = None - xpd_act_id: Optional[str] = None - xpd_rid: Optional[str] = None - gold_pay_type: Optional[int] = Field(None, ge=1, le=9) - gold_recharge_channel: Optional[str] = Field( + manual_id: str | None = None + rid: str | None = None + bind_act_alias: str | None = None + confirm_act_alias: str | None = None + legacy_act_alias: str | None = None + room_id: str | None = None + elite_amount: int | None = Field(None, ge=1) + esports_manual_id: str | None = None + esports_act_alias: str | None = None + esports_amount: int | None = Field(None, ge=1) + esports_chicken_gift_id: str | None = None + esports_chicken_skin_id: str | None = None + esports_firework_gift_id: str | None = None + esports_firework_skin_id: str | None = None + xpd_act_alias: str | None = None + xpd_act_id: str | None = None + xpd_rid: str | None = None + gold_pay_type: int | None = Field(None, ge=1, le=9) + gold_recharge_channel: str | None = Field( None, pattern="^(wechat_qr|supplier_api)$" ) - gold_api_product_id: Optional[str] = Field(None, max_length=128) - gold_api_account_template_name: Optional[str] = Field(None, max_length=64) - gift_id: Optional[str] = None - skin_id: Optional[str] = None + gold_api_product_id: str | None = Field(None, max_length=128) + gold_api_account_template_name: str | None = Field(None, max_length=64) + gift_id: str | None = None + skin_id: str | None = None class DouyuTaskBatchRequest(BaseModel): @@ -709,10 +710,10 @@ class DouyuTaskOut(BaseModel): handbook_scope: str = "legacy" status: str message: str = "" - result: Optional[dict[str, Any]] = None + result: dict[str, Any] | None = None created_by: int - created_at: Optional[datetime] = None - finished_at: Optional[datetime] = None + created_at: datetime | None = None + finished_at: datetime | None = None model_config = ConfigDict(from_attributes=True) @@ -740,10 +741,10 @@ class DouyuGoodsOut(BaseModel): id: int commodity_id: str name: str = "" - score: Optional[int] = None + score: int | None = None status: str = "" - raw: Optional[dict[str, Any]] = None - updated_at: Optional[datetime] = None + raw: dict[str, Any] | None = None + updated_at: datetime | None = None model_config = ConfigDict(from_attributes=True) @@ -766,41 +767,41 @@ class DouyuTaskAccountOut(BaseModel): uid: str = "" nickname: str = "" tag: str = "" - points: Optional[int] = None + points: int | None = None game_name: str = "" game_channel: str = "" - gold_balance: Optional[int] = None - exchange_balance: Optional[int] = None + gold_balance: int | None = None + exchange_balance: int | None = None bind_status: str = "" - change_role_wait_time: Optional[int] = None - esports_points: Optional[int] = None + change_role_wait_time: int | None = None + esports_points: int | None = None esports_game_name: str = "" esports_game_channel: str = "" esports_bind_status: str = "" - esports_change_role_wait_time: Optional[int] = None - esports_can_change_time: Optional[int] = None + esports_change_role_wait_time: int | None = None + esports_can_change_time: int | None = None xpd_game_name: str = "" xpd_openid: str = "" xpd_role_id: str = "" - xpd_plat_id: Optional[int] = None - xpd_area_id: Optional[int] = None - xpd_balance: Optional[int] = None - xpd_fragments: Optional[int] = None + xpd_plat_id: int | None = None + xpd_area_id: int | None = None + xpd_balance: int | None = None + xpd_fragments: int | None = None xpd_bind_status: str = "" - assigned_to: Optional[int] = None - assigned_username: Optional[str] = None + assigned_to: int | None = None + assigned_username: str | None = None class DouyuXpdGoodsOut(BaseModel): id: int commodity_id: str name: str = "" - price: Optional[int] = None - org_price: Optional[int] = None + price: int | None = None + org_price: int | None = None category: str = "" - goods_left: Optional[int] = None - raw: Optional[dict[str, Any]] = None - updated_at: Optional[datetime] = None + goods_left: int | None = None + raw: dict[str, Any] | None = None + updated_at: datetime | None = None model_config = ConfigDict(from_attributes=True) @@ -827,23 +828,23 @@ class ProxyConfigOut(BaseModel): https: str = "" whitelist_enabled: bool = False whitelist_platform: str = "xiequ" - whitelist_credentials: Optional[dict] = None + whitelist_credentials: dict | None = None # 旧字段保留(向后兼容) whitelist_uid: str = "" whitelist_ukey: str = "" class ProxyConfigUpdate(BaseModel): - enabled: Optional[bool] = None - api_url: Optional[str] = None - http: Optional[str] = None - https: Optional[str] = None - whitelist_enabled: Optional[bool] = None - whitelist_platform: Optional[str] = "xiequ" - whitelist_credentials: Optional[dict] = None + enabled: bool | None = None + api_url: str | None = None + http: str | None = None + https: str | None = None + whitelist_enabled: bool | None = None + whitelist_platform: str | None = "xiequ" + whitelist_credentials: dict | None = None # 旧字段保留(向后兼容) - whitelist_uid: Optional[str] = None - whitelist_ukey: Optional[str] = None + whitelist_uid: str | None = None + whitelist_ukey: str | None = None # ---- 代理平台元信息 ---- diff --git a/web/backend/security.py b/web/backend/security.py index 171b55b..05927a3 100644 --- a/web/backend/security.py +++ b/web/backend/security.py @@ -3,10 +3,10 @@ import os import secrets import warnings -from datetime import datetime, timedelta, timezone -from typing import Optional +from datetime import UTC, datetime, timedelta + import bcrypt -from jose import jwt, JWTError +from jose import JWTError, jwt # JWT 密钥:生产环境必须通过环境变量 JWT_SECRET_KEY 设置 SECRET_KEY = os.getenv("JWT_SECRET_KEY") @@ -41,12 +41,12 @@ def create_access_token( data: dict, expires_hours: int = ACCESS_TOKEN_EXPIRE_HOURS ) -> str: to_encode = data.copy() - expire = datetime.now(timezone.utc) + timedelta(hours=expires_hours) + expire = datetime.now(UTC) + timedelta(hours=expires_hours) to_encode.update({"exp": expire}) return jwt.encode(to_encode, SECRET_KEY_TYPED, algorithm=ALGORITHM) -def decode_access_token(token: str) -> Optional[dict]: +def decode_access_token(token: str) -> dict | None: try: return jwt.decode(token, SECRET_KEY_TYPED, algorithms=[ALGORITHM]) except JWTError: diff --git a/web/backend/services/account_check_service.py b/web/backend/services/account_check_service.py index 4f13b5f..1a8e78b 100644 --- a/web/backend/services/account_check_service.py +++ b/web/backend/services/account_check_service.py @@ -8,9 +8,9 @@ import uuid import zipfile from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import dataclass, field -from datetime import datetime, timezone +from datetime import UTC, datetime from types import SimpleNamespace -from typing import Optional, cast +from typing import cast from core.douyu import DouyuLogin, WgapiLoginAPI from core.douyu.login import AccountLike @@ -21,7 +21,7 @@ from ..models import ProxyConfig as ProxyConfigModel def _now() -> datetime: """返回时区感知 UTC 时间。""" - return datetime.now(timezone.utc) + return datetime.now(UTC) STATUS_LABELS = { @@ -139,7 +139,7 @@ class AccountCheckRunner: self, batch: AccountCheckBatch, accounts: list[AccountCheckInput], - proxy_config: Optional[ProxyConfigModel] = None, + proxy_config: ProxyConfigModel | None = None, ): self.batch = batch self.accounts = accounts @@ -394,7 +394,7 @@ class AccountCheckRegistry: max_login_retries: int, max_total_time: float, use_proxy: bool = False, - proxy_config: Optional[ProxyConfigModel] = None, + proxy_config: ProxyConfigModel | None = None, ) -> AccountCheckRunner: batch_id = uuid.uuid4().hex[:12] normalized_retries = max_login_retries if max_login_retries > 0 else 20 diff --git a/web/backend/services/account_service.py b/web/backend/services/account_service.py index ca1d3b4..7a4781e 100644 --- a/web/backend/services/account_service.py +++ b/web/backend/services/account_service.py @@ -7,7 +7,6 @@ from sqlalchemy.orm import Session from ..models import Account, LoginTask - EMAIL_PATTERN = re.compile(r"^[^\s@|]+@[^\s@|]+\.[^\s@|]+$") diff --git a/web/backend/services/audit_service.py b/web/backend/services/audit_service.py index c2dc8eb..6eee960 100644 --- a/web/backend/services/audit_service.py +++ b/web/backend/services/audit_service.py @@ -10,7 +10,6 @@ from sqlalchemy.orm import Session from ..models import AuditLog, User - _SENSITIVE_KEY_PARTS = ( "cookie", "token", diff --git a/web/backend/services/cookie_check_service.py b/web/backend/services/cookie_check_service.py index 12457c8..d2be2e5 100644 --- a/web/backend/services/cookie_check_service.py +++ b/web/backend/services/cookie_check_service.py @@ -1,10 +1,9 @@ """斗鱼 Cookie 有效性检测服务。""" -from datetime import datetime, timezone +from datetime import UTC, datetime import requests - CHECK_UA = ( "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 " "(KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36" @@ -15,7 +14,7 @@ USER_LEVEL_API = "https://www.douyu.com/japi/interactnc/web/userLevel/userLevelD def check_douyu_cookie(cookie: str) -> dict: """检测斗鱼 Cookie 有效性,鱼丸与等级接口均通过才算有效。""" - checked_at = datetime.now(timezone.utc) + checked_at = datetime.now(UTC) base = { "checked_at": checked_at, "fish_ball": None, diff --git a/web/backend/services/douyu_runner.py b/web/backend/services/douyu_runner.py index 4d58c9f..c811e81 100644 --- a/web/backend/services/douyu_runner.py +++ b/web/backend/services/douyu_runner.py @@ -7,17 +7,21 @@ from concurrent.futures import ThreadPoolExecutor, as_completed from sqlalchemy.orm import joinedload from core.douyu import DouyuActivityError + from ..database import SessionLocal from ..models import DouyuTask from .cookie_check_service import check_douyu_cookie -from .douyu_service import latest_success_login_task, update_account_profile_from_cookie -from .douyu_runner_core import DouyuBatchRunnerCore, douyu_batch_registry # noqa: F401 (douyu_batch_registry 供 routers 重导出) from .douyu_runner_bind import BindMixin -from .douyu_runner_manual import ManualMixin -from .douyu_runner_gold import GoldMixin +from .douyu_runner_core import ( # noqa: F401 (douyu_batch_registry 供 routers 重导出) + DouyuBatchRunnerCore, + douyu_batch_registry, +) from .douyu_runner_donate import DonateMixin +from .douyu_runner_gold import GoldMixin from .douyu_runner_goods import GoodsMixin +from .douyu_runner_manual import ManualMixin from .douyu_runner_xpd import XpdMixin +from .douyu_service import latest_success_login_task, update_account_profile_from_cookie class DouyuBatchRunner( diff --git a/web/backend/services/douyu_runner_bind.py b/web/backend/services/douyu_runner_bind.py index 74a8b9a..6fe7a5f 100644 --- a/web/backend/services/douyu_runner_bind.py +++ b/web/backend/services/douyu_runner_bind.py @@ -1,16 +1,16 @@ """斗鱼任务执行器:角色绑定(由 douyu_runner.py 按功能域拆分)。""" from __future__ import annotations + import time -from datetime import datetime, timezone -from typing import Any, TYPE_CHECKING +from datetime import UTC, datetime +from typing import TYPE_CHECKING, Any + from sqlalchemy.orm import Session from core.douyu import DouyuActivityClient, DouyuActivityError -from ..models import Account, DouyuTask -if TYPE_CHECKING: - from .douyu_runner import DouyuBatchRunner +from ..models import Account, DouyuTask DOUYU_LEGACY_BIND_ACT_ALIAS = "20250213NQCYX" DOUYU_BIND_ROLE_POLL_SECONDS = 65 @@ -126,7 +126,7 @@ class BindMixin: if str(bind_info.get("api_version") or "") == "esports": can_change_time = cls._to_int(bind_info.get("can_change_time")) if can_change_time is not None: - return can_change_time <= int(datetime.now(timezone.utc).timestamp()) + return can_change_time <= int(datetime.now(UTC).timestamp()) wait_time = cls._to_int(bind_info.get("change_role_wait_time")) if wait_time is not None and wait_time > 0: return False @@ -236,7 +236,7 @@ class BindMixin: bind_info.get("change_role_wait_time") ) account.bind_status = status - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) def _apply_esports_bind_info_to_account( self, account: Account, bind_info: dict, status: str @@ -252,7 +252,7 @@ class BindMixin: ) account.esports_can_change_time = self._to_int(bind_info.get("can_change_time")) account.esports_bind_status = status - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) def _esports_bind_state( self, @@ -503,7 +503,7 @@ class BindMixin: role_name = snapshot["role_name"] # 待确认角色只回传前端展示,不写入账号表,避免“未换绑成功但角色信息已变新” account.bind_status = "game_queried" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._push_log( "success", f"识别到待确认角色: {role_name} (act={query_alias})" ) @@ -688,7 +688,7 @@ class BindMixin: "bind_polling": True, } account.bind_status = "bind_qr_generated" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) # 关键:先把二维码 progress 出去,前端 running 期间即可弹窗扫码。 self._update_task_progress( db, task, "running", "已生成绑定二维码,等待扫码绑定", result @@ -822,7 +822,7 @@ class BindMixin: self._apply_bind_info_to_account(account, before_bound, "game_queried") else: account.bind_status = "game_queried" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._mark_task( db, task, @@ -882,7 +882,7 @@ class BindMixin: ) else: account.bind_status = "game_queried" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._mark_task( db, task, @@ -917,7 +917,7 @@ class BindMixin: ) else: account.bind_status = "bind_confirmed" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._mark_task( db, task, @@ -1027,7 +1027,7 @@ class BindMixin: "bind_phase": "switching_role", } account.esports_bind_status = "esports_role_switching" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._mark_task( db, task, @@ -1052,7 +1052,7 @@ class BindMixin: return account.esports_bind_status = "esports_bind_confirming" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._update_task_progress( db, task, @@ -1191,7 +1191,7 @@ class BindMixin: self._apply_bind_info_to_account(account, bind_info, "game_queried") else: account.bind_status = "game_queried" if has_pending else "game_not_bound" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) result = { "act_alias": source_alias, "query_act_alias": source_alias, @@ -1281,7 +1281,7 @@ class BindMixin: wait_time = snapshot["change_role_wait_time"] account.change_role_wait_time = wait_time account.bind_status = "change_time_queried" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) source_alias = str(bind_info.get("act_alias") or "") wait_text = self._format_wait_time(wait_time) can_change = snapshot["can_change_role"] diff --git a/web/backend/services/douyu_runner_core.py b/web/backend/services/douyu_runner_core.py index 5360c0f..da9714e 100644 --- a/web/backend/services/douyu_runner_core.py +++ b/web/backend/services/douyu_runner_core.py @@ -5,27 +5,30 @@ from __future__ import annotations import asyncio import threading import time -from datetime import datetime, timezone -from typing import Optional, TYPE_CHECKING +from datetime import UTC, datetime +from typing import TYPE_CHECKING from loguru import logger from sqlalchemy.orm import Session from core.douyu import DouyuActivityClient from core.douyu.proxy_fetcher import ProxyFetcher + from ..models import ( Account, DouyuEsportsGoodsSnapshot, DouyuGoodsSnapshot, DouyuTask, DouyuXpdGoodsSnapshot, +) +from ..models import ( ProxyConfig as ProxyConfigModel, ) from .douyu_service import ( DOUYU_CONFIG_FIELDS, douyu_config_value, - ensure_douyu_config, douyu_task_payload, + ensure_douyu_config, ) if TYPE_CHECKING: @@ -62,9 +65,9 @@ class DouyuBatchRunnerCore: db: Session, batch_id: str, task_type: str, - payload: Optional[dict] = None, - log_queue: Optional[asyncio.Queue] = None, - loop: Optional[asyncio.AbstractEventLoop] = None, + payload: dict | None = None, + log_queue: asyncio.Queue | None = None, + loop: asyncio.AbstractEventLoop | None = None, concurrency: int = 3, ): self.db = db @@ -212,7 +215,7 @@ class DouyuBatchRunnerCore: task.message = message[:512] if result is not None: task.result = result - task.finished_at = datetime.now(timezone.utc) + task.finished_at = datetime.now(UTC) db.commit() self._push_task_event(task) @@ -232,7 +235,7 @@ class DouyuBatchRunnerCore: self._push_task_event(task) def _upsert_goods(self, db: Session, goods: list[dict]) -> None: - now = datetime.now(timezone.utc) + now = datetime.now(UTC) for raw in goods: commodity_id = str(raw.get("commodityId") or raw.get("commodity_id") or "") if not commodity_id: @@ -254,7 +257,7 @@ class DouyuBatchRunnerCore: db.commit() def _upsert_esports_goods(self, db: Session, goods: list[dict]) -> None: - now = datetime.now(timezone.utc) + now = datetime.now(UTC) for raw in goods: commodity_id = str(raw.get("commodityId") or raw.get("commodity_id") or "") if not commodity_id: @@ -276,7 +279,7 @@ class DouyuBatchRunnerCore: def _upsert_xpd_goods(self, db: Session, goods: list[dict]) -> None: """同步和平小店商品快照,移除上一次热门抢购等遗留商品。""" - now = datetime.now(timezone.utc) + now = datetime.now(UTC) commodity_ids = { str(raw.get("commodity_id") or raw.get("iGoodsId") or "") for raw in goods } diff --git a/web/backend/services/douyu_runner_donate.py b/web/backend/services/douyu_runner_donate.py index 69cf9bb..10a9b2d 100644 --- a/web/backend/services/douyu_runner_donate.py +++ b/web/backend/services/douyu_runner_donate.py @@ -1,15 +1,15 @@ """斗鱼任务执行器:送礼(由 douyu_runner.py 按功能域拆分)。""" from __future__ import annotations -from datetime import datetime, timezone -from typing import Any, TYPE_CHECKING + +from datetime import UTC, datetime +from typing import TYPE_CHECKING, Any + from sqlalchemy.orm import Session from core.douyu import DouyuActivityClient -from ..models import Account, DouyuTask -if TYPE_CHECKING: - from .douyu_runner import DouyuBatchRunner +from ..models import Account, DouyuTask DOUYU_GIFT_POINTS_REFRESH_TIMES = 3 DOUYU_GIFT_POINTS_REFRESH_INTERVAL = 2 @@ -177,7 +177,7 @@ class DonateMixin: result["refresh_errors"] = refresh_errors account.esports_bind_status = "esports_gift_donated" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) message = f"赠送{gift_name}成功: {gift_count}" if account.gold_balance is not None: message += f",鱼翅余额: {account.gold_balance}" @@ -273,7 +273,7 @@ class DonateMixin: if refresh_errors: result["refresh_errors"] = refresh_errors account.bind_status = "gift_donated" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) message = f"赠送精英令成功: {gift_count}" if account.gold_balance is not None: message += f",鱼翅余额: {account.gold_balance}" diff --git a/web/backend/services/douyu_runner_gold.py b/web/backend/services/douyu_runner_gold.py index c421f3e..9434ca7 100644 --- a/web/backend/services/douyu_runner_gold.py +++ b/web/backend/services/douyu_runner_gold.py @@ -1,11 +1,13 @@ """斗鱼任务执行器:鱼翅充值(由 douyu_runner.py 按功能域拆分)。""" from __future__ import annotations + import re import time +from datetime import UTC, datetime from decimal import Decimal -from datetime import datetime, timezone -from typing import Any, TYPE_CHECKING, cast +from typing import TYPE_CHECKING, Any, cast + from sqlalchemy.orm import Session from core.douyu import ( @@ -14,12 +16,10 @@ from core.douyu import ( FishFinRechargeConfig, FishFinRechargeError, ) -from ..models import Account, DouyuTask -from .douyu_service import update_account_profile_from_cookie -from .douyu_runner_core import DOUYU_PAYMENT_POLL_INTERVAL, DOUYU_PAYMENT_POLL_SECONDS -if TYPE_CHECKING: - from .douyu_runner import DouyuBatchRunner +from ..models import Account, DouyuTask +from .douyu_runner_core import DOUYU_PAYMENT_POLL_INTERVAL, DOUYU_PAYMENT_POLL_SECONDS +from .douyu_service import update_account_profile_from_cookie class GoldMixin: @@ -45,7 +45,7 @@ class GoldMixin: exchange = client.exchange_balance() account.gold_balance = self._to_int(gold.get("gold")) account.exchange_balance = self._to_int(exchange.get("count")) - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) return { "gold_balance": account.gold_balance, "exchange_balance": account.exchange_balance, @@ -266,7 +266,7 @@ class GoldMixin: amount=amount, pay_type=int(config["gold_pay_type"]) ) account.bind_status = "gold_qr_created" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._update_task_progress( db, task, "running", f"鱼翅 {amount} 元支付码已生成,等待到账", result ) @@ -278,7 +278,7 @@ class GoldMixin: return if recharged: account.bind_status = "gold_recharged" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._mark_task( db, task, @@ -414,7 +414,7 @@ class GoldMixin: ) return account.bind_status = "gold_api_order_created" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._update_task_progress( db, task, "running", "供应商直充订单已创建,等待到账", result ) @@ -425,7 +425,7 @@ class GoldMixin: return if status == 2: account.bind_status = "gold_recharged" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._mark_task(db, task, "success", "供应商直充成功", result) return if status in {3, 4}: @@ -439,7 +439,7 @@ class GoldMixin: client = self._client(cookie) result = self._refresh_account_gold_balance(client, account) account.bind_status = "gold_balance_queried" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._mark_task( db, task, diff --git a/web/backend/services/douyu_runner_goods.py b/web/backend/services/douyu_runner_goods.py index 359a448..cac37c7 100644 --- a/web/backend/services/douyu_runner_goods.py +++ b/web/backend/services/douyu_runner_goods.py @@ -1,17 +1,17 @@ """斗鱼任务执行器:商城兑换(由 douyu_runner.py 按功能域拆分)。""" from __future__ import annotations + import random import time -from datetime import datetime, timezone -from typing import Any, TYPE_CHECKING +from datetime import UTC, datetime +from typing import TYPE_CHECKING, Any + from sqlalchemy.orm import Session from core.douyu import DouyuActivityClient, DouyuActivityError -from ..models import Account, DouyuEsportsGoodsSnapshot, DouyuGoodsSnapshot, DouyuTask -if TYPE_CHECKING: - from .douyu_runner import DouyuBatchRunner +from ..models import Account, DouyuEsportsGoodsSnapshot, DouyuGoodsSnapshot, DouyuTask # 兑换节奏与重试(对齐 8.30 浏览器抓包:锁单->支付间隔约 2.2~4.2s;火爆类错误要长退避而不是秒级连打) DOUYU_EXCHANGE_PRE_CREATE_JITTER = (0.3, 1.2) @@ -65,7 +65,7 @@ class GoodsMixin: goods = result["goods"] self._upsert_goods(db, goods) account.bind_status = account.bind_status or "active" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._mark_task( db, task, @@ -91,7 +91,7 @@ class GoodsMixin: goods = result["goods"] self._upsert_esports_goods(db, goods) account.esports_bind_status = "esports_goods_refreshed" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._mark_task( db, task, @@ -131,7 +131,7 @@ class GoodsMixin: .first() ) account.bind_status = "goods_locked" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) expire_seconds = result.get("expire_seconds") expire_text = f",{expire_seconds} 秒内有效" if expire_seconds else "" self._mark_task( @@ -181,7 +181,7 @@ class GoodsMixin: .first() ) account.bind_status = "goods_exchanged" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) result = { "commodity_id": commodity_id, "order_id": order_id, @@ -346,7 +346,7 @@ class GoodsMixin: finish_failed(f"预约到货失败: {exc}") return account.bind_status = "goods_subscribed" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._mark_task( db, task, @@ -383,7 +383,7 @@ class GoodsMixin: finish_failed(f"预兑失败: {exc}") return account.bind_status = "goods_pre_exchanged" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._mark_task( db, task, @@ -485,7 +485,7 @@ class GoodsMixin: } ) account.bind_status = "goods_exchanged" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) # 兑换成功后自动刷新积分,更新账号最新积分信息(失败不阻断兑换成功) points_refresh = None try: @@ -575,7 +575,7 @@ class GoodsMixin: result["esports_points_refresh_error"] = str(exc) account.esports_bind_status = "esports_goods_exchanged" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) name = (goods.name if goods else "") or commodity_id message = f"兑换电竞皮肤成功: {name}" if quantity > 1: @@ -608,7 +608,7 @@ class GoodsMixin: else f"限兑 {len(names)} 个: {', '.join(names[:5])}" ) account.bind_status = "limited_goods_queried" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._mark_task( db, task, @@ -624,7 +624,7 @@ class GoodsMixin: result = client.exchange_records(manual_id=str(config["manual_id"])) records = result["records"] account.bind_status = "exchange_records_queried" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._mark_task( db, task, @@ -639,7 +639,7 @@ class GoodsMixin: client = self._client(cookie) token = client.csrf_token() account.bind_status = "csrf_token_ready" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._mark_task( db, task, diff --git a/web/backend/services/douyu_runner_manual.py b/web/backend/services/douyu_runner_manual.py index 665765a..95205db 100644 --- a/web/backend/services/douyu_runner_manual.py +++ b/web/backend/services/douyu_runner_manual.py @@ -1,18 +1,18 @@ """斗鱼任务执行器:手册开通与积分(由 douyu_runner.py 按功能域拆分)。""" from __future__ import annotations + import time -from datetime import datetime, timezone -from typing import Any, TYPE_CHECKING +from datetime import UTC, datetime +from typing import TYPE_CHECKING, Any + from sqlalchemy.orm import Session from core.douyu import DouyuActivityClient, DouyuActivityError -from ..models import Account, DouyuTask -from .douyu_service import account_uid, update_account_profile_from_cookie -from .douyu_runner_core import DOUYU_PAYMENT_POLL_INTERVAL, DOUYU_PAYMENT_POLL_SECONDS -if TYPE_CHECKING: - from .douyu_runner import DouyuBatchRunner +from ..models import Account, DouyuTask +from .douyu_runner_core import DOUYU_PAYMENT_POLL_INTERVAL, DOUYU_PAYMENT_POLL_SECONDS +from .douyu_service import account_uid, update_account_profile_from_cookie class ManualMixin: @@ -54,7 +54,7 @@ class ManualMixin: account.uid = uid account.points = points update_account_profile_from_cookie(account, cookie) - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) return {"points": points, "points_query": result} def _wait_points_after_payment( @@ -121,7 +121,7 @@ class ManualMixin: manual_type = self._to_int(result.get("manual_type")) manual_score = self._to_int(result.get("manual_score")) account.esports_points = manual_score - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) return { "esports_manual_type": manual_type, "esports_manual_score": manual_score, @@ -220,7 +220,7 @@ class ManualMixin: room_id=str(config["room_id"]), ) account.bind_status = "elite_qr_created" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._update_task_progress( db, task, "running", "精英宝典支付码已生成,等待开通到账", result ) @@ -232,7 +232,7 @@ class ManualMixin: return if opened: account.bind_status = "elite_opened" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._mark_task( db, task, "success", f"精英宝典已开通,积分: {account.points}", result ) @@ -281,7 +281,7 @@ class ManualMixin: if baseline_manual_type is not None and baseline_manual_type >= 1: account.esports_bind_status = "esports_opened" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._mark_task( db, task, @@ -299,7 +299,7 @@ class ManualMixin: ) result.update(baseline_result) account.esports_bind_status = "esports_qr_created" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._update_task_progress( db, task, "running", "电竞手册支付码已生成,等待开通到账", result ) @@ -318,7 +318,7 @@ class ManualMixin: return if opened: account.esports_bind_status = "esports_opened" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._mark_task( db, task, @@ -353,7 +353,7 @@ class ManualMixin: client = self._client(cookie) result = self._refresh_esports_handbook(client, account, manual_id=manual_id) account.esports_bind_status = "esports_points_queried" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) points = result["esports_points"] self._mark_task( db, @@ -370,7 +370,7 @@ class ManualMixin: ctn = client.acf_ccn(refresh_subscribe=False) result = self._refresh_account_points(client, account, cookie, ctn=ctn) account.bind_status = "points_queried" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) points = result["points"] self._mark_task( db, diff --git a/web/backend/services/douyu_runner_xpd.py b/web/backend/services/douyu_runner_xpd.py index 5e8852b..ea853cd 100644 --- a/web/backend/services/douyu_runner_xpd.py +++ b/web/backend/services/douyu_runner_xpd.py @@ -1,16 +1,16 @@ """斗鱼任务执行器:和平小店(由 douyu_runner.py 按功能域拆分)。""" from __future__ import annotations + import time -from datetime import datetime, timezone -from typing import Any, TYPE_CHECKING, cast +from datetime import UTC, datetime +from typing import TYPE_CHECKING, Any, cast + from sqlalchemy.orm import Session from core.douyu import DouyuActivityClient, DouyuActivityError -from ..models import Account, DouyuTask, DouyuXpdGoodsSnapshot -if TYPE_CHECKING: - from .douyu_runner import DouyuBatchRunner +from ..models import Account, DouyuTask, DouyuXpdGoodsSnapshot DOUYU_XPD_BIND_POLL_SECONDS = 300 DOUYU_XPD_BIND_POLL_INTERVAL = 5 @@ -72,7 +72,7 @@ class XpdMixin: account.xpd_role_id = str(role.get("role_id") or "") or account.xpd_role_id account.xpd_plat_id = self._to_int(role.get("plat_id")) account.xpd_area_id = area_id - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) def _execute_query_xpd_role( self, @@ -126,7 +126,7 @@ class XpdMixin: return result = client.xpd_bind_qr(act_alias=act_alias) account.xpd_bind_status = "xpd_bind_qr_ready" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) db.commit() # 记录绑定前状态:已绑定账号生成二维码后必须等扫码换绑,不能立即成功 try: @@ -242,7 +242,7 @@ class XpdMixin: role_name = str(result.get("role_name") or "") if not result.get("bind_role") or not role_name: account.xpd_bind_status = "xpd_not_bound" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) db.commit() self._mark_task( db, task, "failed", "尚未检测到小店绑定角色,请先扫码绑定", result @@ -258,12 +258,12 @@ class XpdMixin: ) else: account.xpd_game_name = role_name - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) except Exception: account.xpd_game_name = role_name - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) account.xpd_bind_status = "xpd_bound" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) db.commit() result["xpd_pending_confirm"] = False result["xpd_bound"] = True @@ -427,7 +427,7 @@ class XpdMixin: fragments = result.get("fragments") account.xpd_fragments = fragments account.xpd_bind_status = "xpd_fragments_queried" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) db.commit() if fragments is None: self._mark_task(db, task, "failed", "未获取到小店扭蛋碎片数量") @@ -459,7 +459,7 @@ class XpdMixin: act_id=str(config["xpd_act_id"]), ) account.xpd_bind_status = "xpd_purchase_records_queried" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) total = result.get("total") or len(result.get("records") or []) self._mark_task( db, @@ -561,7 +561,7 @@ class XpdMixin: if pay_type == 5 and result.get("new_balance") is not None: account.xpd_fragments = result["new_balance"] account.xpd_bind_status = "xpd_goods_exchanged" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) db.commit() currency = "点券" if pay_type == 1 else "扭蛋碎片" diff --git a/web/backend/services/douyu_service.py b/web/backend/services/douyu_service.py index 5e8711b..4a5b209 100644 --- a/web/backend/services/douyu_service.py +++ b/web/backend/services/douyu_service.py @@ -3,7 +3,7 @@ from __future__ import annotations import uuid -from datetime import datetime, timezone +from datetime import UTC, datetime from sqlalchemy import select from sqlalchemy.orm import Session @@ -13,7 +13,6 @@ from core.douyu.cookie_utils import cookie_value from ..models import Account, DouyuConfig, DouyuTask, LoginTask - SUPPORTED_DOUYU_TASK_TYPES = { "get_bind_qr": "获取绑定二维码", "confirm_bind": "确认绑定", @@ -302,7 +301,7 @@ def cleanup_orphan_douyu_tasks( tasks = query.all() if not tasks: return 0 - now = datetime.now(timezone.utc) + now = datetime.now(UTC) for task in tasks: task.status = "stopped" task.message = message @@ -426,7 +425,7 @@ def douyu_task_payload(task: DouyuTask, *, include_detail: bool = False) -> dict if value is None: return None if value.tzinfo is None: - value = value.replace(tzinfo=timezone.utc) + value = value.replace(tzinfo=UTC) return value.isoformat() return { diff --git a/web/backend/services/huya_register_runner.py b/web/backend/services/huya_register_runner.py index 3e1c9d2..a43ab5c 100644 --- a/web/backend/services/huya_register_runner.py +++ b/web/backend/services/huya_register_runner.py @@ -6,8 +6,7 @@ import threading import uuid from concurrent.futures import ThreadPoolExecutor, as_completed from dataclasses import dataclass, field -from datetime import datetime, timezone -from typing import Optional +from datetime import UTC, datetime from core.douyu.proxy_fetcher import ProxyFetcher from core.huya.auto_register import HuyaAutoRegisterResult, register_huya_with_sms_line @@ -17,13 +16,18 @@ 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) @@ -37,7 +41,7 @@ TERMINAL_BATCH_STATUSES = frozenset({"finished", "stopped", "error", "interrupte def _now() -> datetime: - return datetime.now(timezone.utc) + return datetime.now(UTC) def _cookie_preview(cookie: str) -> str: @@ -409,7 +413,7 @@ class HuyaRegisterRunner: self, batch: HuyaRegisterBatch, sms_lines: list[SmsLine], - proxy_config: Optional[ProxyConfigModel] = None, + proxy_config: ProxyConfigModel | None = None, *, item_indices: list[int] | None = None, ): @@ -812,7 +816,7 @@ class HuyaRegisterRegistry: password_prefix: str = "hy", fixed_password: str = "", use_proxy: bool = False, - proxy_config: Optional[ProxyConfigModel] = None, + proxy_config: ProxyConfigModel | None = None, ) -> HuyaRegisterRunner: batch_id = uuid.uuid4().hex[:12] concurrency = max(1, min(int(concurrency or 1), 5)) @@ -918,7 +922,7 @@ class HuyaRegisterRegistry: def retry( self, batch_id: str, - proxy_config: Optional[ProxyConfigModel] = None, + proxy_config: ProxyConfigModel | None = None, *, mode: str = "continue", concurrency: int | None = None, diff --git a/web/backend/services/huya_runner.py b/web/backend/services/huya_runner.py index 7dbb426..46ffec6 100644 --- a/web/backend/services/huya_runner.py +++ b/web/backend/services/huya_runner.py @@ -3,19 +3,22 @@ from __future__ import annotations from concurrent.futures import ThreadPoolExecutor, as_completed +from datetime import UTC, datetime from sqlalchemy.orm import joinedload -from ..database import SessionLocal -from datetime import datetime, timezone - -from ..models import HuyaAccount, HuyaTask from core.huya.cookie_utils import normalize_huya_cookie -from .huya_service import HUYA_CONFIG_FIELDS, ensure_huya_config, huya_config_value -from .huya_runner_core import HuyaBatchRunnerCore, huya_batch_registry # noqa: F401 (huya_batch_registry 供 routers 重导出) + +from ..database import SessionLocal +from ..models import HuyaAccount, HuyaTask from .huya_runner_bind import BindMixin +from .huya_runner_core import ( # noqa: F401 (huya_batch_registry 供 routers 重导出) + HuyaBatchRunnerCore, + huya_batch_registry, +) from .huya_runner_goods import GoodsMixin from .huya_runner_recharge import RechargeMixin +from .huya_service import HUYA_CONFIG_FIELDS, ensure_huya_config, huya_config_value class HuyaBatchRunner( @@ -148,7 +151,7 @@ class HuyaBatchRunner( if not account: task.status = "error" task.message = "账号不存在" - task.finished_at = datetime.now(timezone.utc) + task.finished_at = datetime.now(UTC) continue task.status = "pending" task.message = "等待执行" diff --git a/web/backend/services/huya_runner_bind.py b/web/backend/services/huya_runner_bind.py index 20a7fec..a9d90d9 100644 --- a/web/backend/services/huya_runner_bind.py +++ b/web/backend/services/huya_runner_bind.py @@ -1,20 +1,17 @@ """虎牙任务执行器:角色绑定(由 huya_runner.py 按功能域拆分)。""" from __future__ import annotations + import time -from datetime import datetime, timezone -from typing import Any +from datetime import UTC, datetime +from typing import TYPE_CHECKING, Any from sqlalchemy.orm import Session from core.huya import HuyaHttpClient + from ..models import HuyaAccount, HuyaTask -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from .huya_runner import HuyaBatchRunner - HUYA_BIND_ROLE_POLL_SECONDS = 180 HUYA_BIND_ROLE_POLL_INTERVAL = 3 HUYA_BIND_ZT_UUID = "b02faae1" @@ -77,7 +74,7 @@ class BindMixin: account_data = bind_status.accountData is_bound = bool(account_data.isBindAcount and account_data.isBindRole) change_time = int(account_data.changBindTime or 0) - now = int(datetime.now(timezone.utc).timestamp()) + now = int(datetime.now(UTC).timestamp()) can_change = not is_bound or not change_time or change_time <= now return { "is_bound": is_bound, @@ -150,7 +147,7 @@ class BindMixin: account.status = status account.game_name = role_name or account.game_name account.game_channel = self._role_channel(bind_status) or account.game_channel - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) @staticmethod def _bind_redirect_url(config_info: dict) -> str: @@ -439,7 +436,7 @@ class BindMixin: account.game_name = role_info["role_name"] or account.game_name account.game_channel = self._role_channel(bind_status) or account.game_channel account.nickname = profile_nick or account.nickname - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) # 生成二维码后自动轮询扫码/角色,避免用户必须手动点「查询角色」。 role_name, result = self._wait_bind_role_result( @@ -545,7 +542,7 @@ class BindMixin: return account.status = "game_not_bound" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._mark_task(worker_db, task, "success", "未绑定游戏角色", result) def _execute_confirm_bind( diff --git a/web/backend/services/huya_runner_core.py b/web/backend/services/huya_runner_core.py index ce81cb7..138a373 100644 --- a/web/backend/services/huya_runner_core.py +++ b/web/backend/services/huya_runner_core.py @@ -5,11 +5,11 @@ from __future__ import annotations import asyncio import threading import time -from datetime import datetime, timezone -from typing import Optional +from datetime import UTC, datetime from loguru import logger from sqlalchemy.orm import Session + from ..models import HuyaTask from .huya_service import cookie_value @@ -29,9 +29,9 @@ class HuyaBatchRunnerCore: db: Session, batch_id: str, task_type: str, - payload: Optional[dict] = None, - log_queue: Optional[asyncio.Queue] = None, - loop: Optional[asyncio.AbstractEventLoop] = None, + payload: dict | None = None, + log_queue: asyncio.Queue | None = None, + loop: asyncio.AbstractEventLoop | None = None, concurrency: int = 3, ): self.db = db @@ -123,12 +123,12 @@ class HuyaBatchRunnerCore: task: HuyaTask, status: str, message: str, - result: Optional[dict] = None, + result: dict | None = None, ): task.status = status task.message = message task.result = result - task.finished_at = datetime.now(timezone.utc) + task.finished_at = datetime.now(UTC) worker_db.commit() def _update_task_progress( @@ -137,7 +137,7 @@ class HuyaBatchRunnerCore: task: HuyaTask, status: str, message: str, - result: Optional[dict] = None, + result: dict | None = None, ): task.status = status task.message = message diff --git a/web/backend/services/huya_runner_goods.py b/web/backend/services/huya_runner_goods.py index 44abb17..9a3abb3 100644 --- a/web/backend/services/huya_runner_goods.py +++ b/web/backend/services/huya_runner_goods.py @@ -1,17 +1,16 @@ """虎牙任务执行器:积分与商城(由 huya_runner.py 按功能域拆分)。""" from __future__ import annotations + import time -from datetime import datetime, timezone -from typing import Any, TYPE_CHECKING +from datetime import UTC, datetime +from typing import TYPE_CHECKING, Any from sqlalchemy.orm import Session from core.huya import HuyaHttpClient -from ..models import HuyaAccount, HuyaGoodsSnapshot, HuyaTask -if TYPE_CHECKING: - from .huya_runner import HuyaBatchRunner +from ..models import HuyaAccount, HuyaGoodsSnapshot, HuyaTask class GoodsMixin: @@ -83,7 +82,7 @@ class GoodsMixin: points = response.available_score account.points = points account.status = "points_queried" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) self._mark_task(worker_db, task, "success", f"积分: {points}", result) def _execute_query_exchange_records( @@ -144,7 +143,7 @@ class GoodsMixin: item["score_text"] = f"{int(item.get('score') or 0)}积分" account.status = "exchange_records_queried" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) count = len(records) message = f"兑换记录 {count} 条" if count else "暂无兑换记录" self._mark_task(worker_db, task, "success", message, result) @@ -202,7 +201,7 @@ class GoodsMixin: for item in result.get("goods", []) if item.get("product_id") and item.get("name") ] - now = datetime.now(timezone.utc) + now = datetime.now(UTC) worker_db.query(HuyaGoodsSnapshot).delete(synchronize_session=False) for item in goods: worker_db.add( @@ -290,7 +289,7 @@ class GoodsMixin: "product_id": str(product_id), "product_name": product_name, "scheduled_at": scheduled_at.isoformat() if scheduled_at else "", - "executed_at": datetime.now(timezone.utc).isoformat(), + "executed_at": datetime.now(UTC).isoformat(), "goods": snapshot.raw if snapshot else None, } ) @@ -305,6 +304,6 @@ class GoodsMixin: return account.status = "goods_exchanged" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) message = response.msg or f"兑换成功: {product_name}" self._mark_task(worker_db, task, "success", message, result) diff --git a/web/backend/services/huya_runner_recharge.py b/web/backend/services/huya_runner_recharge.py index d4a67cb..ebbde25 100644 --- a/web/backend/services/huya_runner_recharge.py +++ b/web/backend/services/huya_runner_recharge.py @@ -1,13 +1,15 @@ """虎牙任务执行器:充值(由 huya_runner.py 按功能域拆分)。""" from __future__ import annotations + import time -from datetime import datetime, timezone -from typing import Any, TYPE_CHECKING +from datetime import UTC, datetime +from typing import TYPE_CHECKING, Any from sqlalchemy.orm import Session from core.huya import HuyaHttpClient + from ..models import HuyaAccount, HuyaRechargeGoodsSnapshot, HuyaTask HUYA_RECHARGE_ACT_ID = 25135 @@ -25,9 +27,6 @@ HUYA_RECHARGE_EXTRA_PRODUCTS = [ ] from .huya_runner_core import HUYA_RECHARGE_SOURCE_ID -if TYPE_CHECKING: - from .huya_runner import HuyaBatchRunner - class RechargeMixin: """充值域:充值商品、下单与到账轮询。""" @@ -103,7 +102,7 @@ class RechargeMixin: status=0, timeout=10.0, ) - checked_at = datetime.now(timezone.utc).isoformat() + checked_at = datetime.now(UTC).isoformat() if resp is not None and getattr(resp, "orders", None): for order in resp.orders: if str(getattr(order, "orderId", "")) != order_id_text: @@ -248,7 +247,7 @@ class RechargeMixin: ) return - now = datetime.now(timezone.utc) + now = datetime.now(UTC) goods: list[dict] = [] failed: list[dict] = [] @@ -501,7 +500,7 @@ class RechargeMixin: "order": order_result, } account.status = "recharge_order_created" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) message = f"{product_name} x{count} {self._pay_channel_label(pay_channel)} {result['amount_text']}" self._update_task_progress( worker_db, task, "running", f"{message},等待扫码支付", result @@ -518,7 +517,7 @@ class RechargeMixin: order_id=order_resp.orderId, result=result, ) - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) if payment_status == "paid": account.status = "recharge_paid" paid_message = f"支付成功: {product_name} x{count} {result['amount_text']}" diff --git a/web/backend/services/huya_service.py b/web/backend/services/huya_service.py index 4cbce3d..66fc163 100644 --- a/web/backend/services/huya_service.py +++ b/web/backend/services/huya_service.py @@ -3,7 +3,7 @@ import csv import uuid from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import UTC, datetime from urllib.parse import urlparse from sqlalchemy.orm import Session @@ -13,7 +13,6 @@ 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": "一键查询积分", @@ -204,7 +203,7 @@ def import_huya_password_accounts( account.tag = tag if not account.cookie: account.status = "password_imported" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) created_or_updated += 1 if created_or_updated: @@ -247,7 +246,7 @@ def _upsert_huya_account( if tag: account.tag = tag account.status = status or "updated" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) return account @@ -276,7 +275,7 @@ def save_huya_login_cookie_to_account( if tag: account.tag = tag account.status = "login_success" - account.updated_at = datetime.now(timezone.utc) + account.updated_at = datetime.now(UTC) db.commit() db.refresh(account) return account @@ -358,7 +357,7 @@ def cleanup_orphan_huya_tasks( if not tasks: return 0 - now = datetime.now(timezone.utc) + now = datetime.now(UTC) for task in tasks: task.status = "stopped" task.message = message diff --git a/web/backend/services/login_service.py b/web/backend/services/login_service.py index f7f5b57..207eb7a 100644 --- a/web/backend/services/login_service.py +++ b/web/backend/services/login_service.py @@ -5,18 +5,21 @@ import os import threading import time import uuid -from types import SimpleNamespace from concurrent.futures import ThreadPoolExecutor, as_completed -from datetime import datetime, timezone -from typing import Optional, cast +from datetime import UTC, datetime +from types import SimpleNamespace +from typing import cast -from sqlalchemy.orm import Session from loguru import logger +from sqlalchemy.orm import Session -from core.douyu import DouyuLogin, WgapiLoginAPI, IframeLoginAPI +from core.douyu import DouyuLogin, IframeLoginAPI, WgapiLoginAPI from core.douyu.login import AccountLike from core.douyu.proxy_fetcher import ProxyFetcher -from ..models import Account as AccountModel, LoginTask, ProxyConfig as ProxyConfigModel + +from ..models import Account as AccountModel +from ..models import LoginTask +from ..models import ProxyConfig as ProxyConfigModel from .cookie_check_service import check_douyu_cookie @@ -62,8 +65,8 @@ def get_relogin_limits() -> tuple[int, int]: def _snapshot_proxy_config( - proxy_config: Optional[ProxyConfigModel], -) -> Optional[ProxyConfigModel]: + proxy_config: ProxyConfigModel | None, +) -> ProxyConfigModel | None: """复制代理配置,避免后台线程访问已关闭会话中的 ORM 对象。""" if proxy_config is None: return None @@ -97,13 +100,13 @@ class LoginBatchRunner: creator_permissions: list[str], max_login_retries: int = 0, max_total_time: float = 0, - proxy_config: Optional[ProxyConfigModel] = None, - log_queue: Optional[asyncio.Queue] = None, - loop: Optional[asyncio.AbstractEventLoop] = None, + proxy_config: ProxyConfigModel | None = None, + log_queue: asyncio.Queue | None = None, + loop: asyncio.AbstractEventLoop | None = None, concurrency: int = 3, api_strategy: str = "wgapi", mode: str = "login", - relogin_task_ids: Optional[list[int]] = None, + relogin_task_ids: list[int] | None = None, ): self.db = db self.account_ids = account_ids @@ -179,7 +182,7 @@ class LoginBatchRunner: if task and task.status in ("relogin_pending", "relogin_running"): task.status = "relogin_failed" task.message = message - task.finished_at = datetime.now(timezone.utc) + task.finished_at = datetime.now(UTC) worker_db.commit() finally: worker_db.close() @@ -199,7 +202,7 @@ class LoginBatchRunner: self.loop, ) - def _resolve_static_proxy(self) -> tuple[Optional[dict], str]: + def _resolve_static_proxy(self) -> tuple[dict | None, str]: """解析静态代理配置。""" if not self.proxy_config or not self.proxy_config.enabled: return None, "" @@ -270,7 +273,7 @@ class LoginBatchRunner: else: task.status = "error" task.message = "代理不可用: 未配置代理" - task.finished_at = datetime.now(timezone.utc) + task.finished_at = datetime.now(UTC) worker_db.commit() self._push_log( "error", f"[{current}] {acc_info['username']} 代理不可用" @@ -384,7 +387,7 @@ class LoginBatchRunner: f"[{current}] {acc_info['username']} {action_name}异常: {e}", ) - task.finished_at = datetime.now(timezone.utc) + task.finished_at = datetime.now(UTC) worker_db.commit() finally: @@ -581,10 +584,10 @@ class BatchRegistry: def register( self, batch_id: str, - log_queue: Optional[asyncio.Queue], - loop: Optional[asyncio.AbstractEventLoop], + log_queue: asyncio.Queue | None, + loop: asyncio.AbstractEventLoop | None, runner: LoginBatchRunner, - owner_id: Optional[int] = None, + owner_id: int | None = None, ): self._batches[batch_id] = { "log_queue": log_queue, @@ -630,7 +633,7 @@ def cleanup_orphan_relogin_tasks( if not tasks: return 0 - finished_at = datetime.now(timezone.utc) + finished_at = datetime.now(UTC) for task in tasks: task.status = "relogin_failed" task.message = message diff --git a/web/backend/services/proxy_service.py b/web/backend/services/proxy_service.py index e45e43e..5a576fe 100644 --- a/web/backend/services/proxy_service.py +++ b/web/backend/services/proxy_service.py @@ -1,21 +1,21 @@ """代理 & 白名单服务层:封装 core/ 代理/白名单逻辑,供路由调用。""" import asyncio -import re import threading import uuid -from typing import Optional from sqlalchemy.orm import Session from core.douyu.proxy import ( + parse_proxy_response, resolve_working_proxy, verify_proxy_url, - parse_proxy_response, ) -from core.douyu.proxy_platforms import create_adapter, get_platform_labels +from core.douyu.proxy_platforms import create_adapter from core.douyu.proxy_platforms.base import _get_local_exit_ip -from ..models import ProxyConfig as ProxyConfigModel, AuditLog + +from ..models import AuditLog +from ..models import ProxyConfig as ProxyConfigModel def _build_whitelist_params(cfg: ProxyConfigModel) -> dict: @@ -128,7 +128,7 @@ class ProxyService: self._active_tests[test_id] = {"log_queue": log_queue, "loop": loop} return test_id, log_queue, loop - def get_test(self, test_id: str) -> Optional[dict]: + def get_test(self, test_id: str) -> dict | None: """获取测试任务信息。""" with self._active_tests_lock: return self._active_tests.get(test_id) diff --git a/web/backend/services/yyb_service.py b/web/backend/services/yyb_service.py index f71bb18..efe15ac 100644 --- a/web/backend/services/yyb_service.py +++ b/web/backend/services/yyb_service.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime from typing import Any from sqlalchemy.orm import Session @@ -12,12 +12,12 @@ from .yyb_worker_client import YybWorkerClient def _utcnow(): - return datetime.now(timezone.utc) + return datetime.now(UTC) def _from_ts(value) -> datetime | None: try: - return datetime.fromtimestamp(int(value), tz=timezone.utc) if value else None + return datetime.fromtimestamp(int(value), tz=UTC) if value else None except (TypeError, ValueError, OSError): return None @@ -27,8 +27,8 @@ def _as_utc(value: datetime | None) -> datetime | None: if value is None: return None if value.tzinfo is None: - return value.replace(tzinfo=timezone.utc) - return value.astimezone(timezone.utc) + return value.replace(tzinfo=UTC) + return value.astimezone(UTC) def cleanup_orphan_yyb_tasks(db: Session, message: str) -> int: diff --git a/web/backend/version.py b/web/backend/version.py index 97eb68a..d8adb6b 100644 --- a/web/backend/version.py +++ b/web/backend/version.py @@ -1,9 +1,10 @@ """应用版本信息。""" -from functools import lru_cache -from importlib.metadata import PackageNotFoundError, version as package_version -from pathlib import Path import tomllib +from functools import lru_cache +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as package_version +from pathlib import Path @lru_cache(maxsize=1)