执行 Ruff 安全自动修复
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
|
||||
+9
-9
@@ -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:
|
||||
|
||||
+15
-13
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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]:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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=["账号检测"])
|
||||
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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=["审计日志"])
|
||||
|
||||
|
||||
|
||||
@@ -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=["认证"])
|
||||
|
||||
|
||||
@@ -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"}:
|
||||
|
||||
@@ -17,7 +17,6 @@ from ..models import (
|
||||
)
|
||||
from ..permissions import user_has_permission
|
||||
|
||||
|
||||
router = APIRouter(prefix="/api/dashboard", tags=["首页概览"])
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+23
-25
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]}"
|
||||
|
||||
@@ -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=["应用宝充值"])
|
||||
|
||||
|
||||
+127
-126
@@ -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
|
||||
|
||||
|
||||
# ---- 代理平台元信息 ----
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,7 +7,6 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from ..models import Account, LoginTask
|
||||
|
||||
|
||||
EMAIL_PATTERN = re.compile(r"^[^\s@|]+@[^\s@|]+\.[^\s@|]+$")
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ from sqlalchemy.orm import Session
|
||||
|
||||
from ..models import AuditLog, User
|
||||
|
||||
|
||||
_SENSITIVE_KEY_PARTS = (
|
||||
"cookie",
|
||||
"token",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 "扭蛋碎片"
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = "等待执行"
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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']}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user