Spaces:
Running
Running
提升安全与流畅度
Browse files- Dockerfile +11 -1
- app.py +129 -6
- database_sql.py +99 -7
- requirements.txt +14 -11
- router_items.py +25 -4
- router_messages.py +38 -10
- router_users_auth.py +13 -3
- router_wallet.py +16 -1
- verify_code_engine.py +32 -0
- 云端_定时版本检测引擎.py +27 -9
Dockerfile
CHANGED
|
@@ -1,10 +1,20 @@
|
|
| 1 |
-
|
|
|
|
| 2 |
|
|
|
|
| 3 |
WORKDIR /code
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
COPY ./requirements.txt /code/requirements.txt
|
| 6 |
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
| 7 |
|
|
|
|
| 8 |
COPY . .
|
| 9 |
|
|
|
|
| 10 |
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
|
|
|
| 1 |
+
# 🚀 P2优化:使用轻量级基镜像,从 900MB 降到 ~150MB
|
| 2 |
+
FROM python:3.9-slim
|
| 3 |
|
| 4 |
+
# 设置工作目录
|
| 5 |
WORKDIR /code
|
| 6 |
|
| 7 |
+
# 安装系统依赖 (psycopg2 需要)
|
| 8 |
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
| 9 |
+
libpq-dev \
|
| 10 |
+
&& rm -rf /var/lib/apt/lists/*
|
| 11 |
+
|
| 12 |
+
# 先复制依赖文件,利用 Docker 缓存层
|
| 13 |
COPY ./requirements.txt /code/requirements.txt
|
| 14 |
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
|
| 15 |
|
| 16 |
+
# 复制应用代码
|
| 17 |
COPY . .
|
| 18 |
|
| 19 |
+
# 启动命令
|
| 20 |
CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
|
app.py
CHANGED
|
@@ -10,11 +10,24 @@ import urllib.parse
|
|
| 10 |
import urllib.request
|
| 11 |
import urllib.error
|
| 12 |
import os
|
| 13 |
-
import mimetypes
|
|
|
|
|
|
|
| 14 |
import 数据库连接 as db
|
| 15 |
import asyncio
|
| 16 |
|
| 17 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
from 云端_定时版本检测引擎 import daily_version_check_task
|
| 19 |
|
| 20 |
# ==========================================
|
|
@@ -39,24 +52,134 @@ from router_proxy import router as proxy_router # 🔗 代理下载
|
|
| 39 |
from database_sql import init_sql_db, get_db
|
| 40 |
from models_sql import Ownership
|
| 41 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 42 |
app = FastAPI(title="ComfyUI Ranking Community API")
|
|
|
|
|
|
|
|
|
|
| 43 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
@app.get("/")
|
| 45 |
def health_check():
|
| 46 |
return {"status": "ok", "message": "ComfyUI Ranking API is running perfectly!"}
|
| 47 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
# ==========================================
|
| 49 |
# 🚀 应用启动事件
|
| 50 |
# ==========================================
|
| 51 |
-
# 作用:初始化数据库 + 启动定时版本检测后台任务
|
| 52 |
@app.on_event("startup")
|
| 53 |
async def on_startup():
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
|
| 58 |
-
#
|
| 59 |
asyncio.create_task(daily_version_check_task())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
|
| 61 |
app.add_middleware(
|
| 62 |
CORSMiddleware,
|
|
|
|
| 10 |
import urllib.request
|
| 11 |
import urllib.error
|
| 12 |
import os
|
| 13 |
+
import mimetypes
|
| 14 |
+
import logging
|
| 15 |
+
import time
|
| 16 |
import 数据库连接 as db
|
| 17 |
import asyncio
|
| 18 |
|
| 19 |
|
| 20 |
+
# ==========================================
|
| 21 |
+
# 📝 P2优化:日志配置
|
| 22 |
+
# ==========================================
|
| 23 |
+
logging.basicConfig(
|
| 24 |
+
level=logging.INFO,
|
| 25 |
+
format='%(asctime)s | %(levelname)s | %(name)s | %(message)s',
|
| 26 |
+
datefmt='%Y-%m-%d %H:%M:%S'
|
| 27 |
+
)
|
| 28 |
+
logger = logging.getLogger("ComfyUI-Ranking")
|
| 29 |
+
|
| 30 |
+
|
| 31 |
from 云端_定时版本检测引擎 import daily_version_check_task
|
| 32 |
|
| 33 |
# ==========================================
|
|
|
|
| 52 |
from database_sql import init_sql_db, get_db
|
| 53 |
from models_sql import Ownership
|
| 54 |
|
| 55 |
+
# 🚀 P2优化:速率限制 (防止暴力攻击)
|
| 56 |
+
from slowapi import Limiter, _rate_limit_exceeded_handler
|
| 57 |
+
from slowapi.util import get_remote_address
|
| 58 |
+
from slowapi.errors import RateLimitExceeded
|
| 59 |
+
from fastapi.exceptions import RequestValidationError
|
| 60 |
+
from starlette.exceptions import HTTPException as StarletteHTTPException
|
| 61 |
+
import traceback
|
| 62 |
+
|
| 63 |
+
limiter = Limiter(key_func=get_remote_address)
|
| 64 |
app = FastAPI(title="ComfyUI Ranking Community API")
|
| 65 |
+
app.state.limiter = limiter
|
| 66 |
+
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
| 67 |
+
|
| 68 |
|
| 69 |
+
# ==========================================
|
| 70 |
+
# 🛡️ 稳定性优化:全局异常处理器
|
| 71 |
+
# ==========================================
|
| 72 |
+
# 作用:捕获所有未处理异常,防止单个请求崩溃整个服务
|
| 73 |
+
|
| 74 |
+
@app.exception_handler(StarletteHTTPException)
|
| 75 |
+
async def http_exception_handler(request, exc):
|
| 76 |
+
"""HTTP 异常处理,返回友好错误信息"""
|
| 77 |
+
logger.warning(f"HTTP {exc.status_code} | {request.url.path} | {exc.detail}")
|
| 78 |
+
return JSONResponse(
|
| 79 |
+
status_code=exc.status_code,
|
| 80 |
+
content={"status": "error", "detail": str(exc.detail)}
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
@app.exception_handler(RequestValidationError)
|
| 84 |
+
async def validation_exception_handler(request, exc):
|
| 85 |
+
"""请求参数校验失败处理"""
|
| 86 |
+
logger.warning(f"Validation Error | {request.url.path} | {exc.errors()}")
|
| 87 |
+
return JSONResponse(
|
| 88 |
+
status_code=422,
|
| 89 |
+
content={"status": "error", "detail": "请求参数格式错误", "errors": exc.errors()}
|
| 90 |
+
)
|
| 91 |
+
|
| 92 |
+
@app.exception_handler(Exception)
|
| 93 |
+
async def global_exception_handler(request, exc):
|
| 94 |
+
"""全局异常处理,捕获所有未预期异常"""
|
| 95 |
+
error_id = f"ERR_{int(time.time())}_{id(exc) % 10000}"
|
| 96 |
+
logger.error(f"Unhandled Exception [{error_id}] | {request.url.path} | {type(exc).__name__}: {exc}")
|
| 97 |
+
logger.error(traceback.format_exc())
|
| 98 |
+
return JSONResponse(
|
| 99 |
+
status_code=500,
|
| 100 |
+
content={
|
| 101 |
+
"status": "error",
|
| 102 |
+
"detail": "服务器内部错误,请稍后重试",
|
| 103 |
+
"error_id": error_id # 方便排查
|
| 104 |
+
}
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
# ==========================================
|
| 109 |
+
# ❤️ 健康检查接口 (增强版)
|
| 110 |
+
# ==========================================
|
| 111 |
@app.get("/")
|
| 112 |
def health_check():
|
| 113 |
return {"status": "ok", "message": "ComfyUI Ranking API is running perfectly!"}
|
| 114 |
|
| 115 |
+
@app.get("/health")
|
| 116 |
+
def detailed_health_check():
|
| 117 |
+
"""详细健康检查,含依赖状态"""
|
| 118 |
+
health_status = {
|
| 119 |
+
"status": "ok",
|
| 120 |
+
"components": {}
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
# 检查 SQL 数据库
|
| 124 |
+
try:
|
| 125 |
+
from database_sql import check_db_connection
|
| 126 |
+
if check_db_connection():
|
| 127 |
+
health_status["components"]["sql_database"] = "ok"
|
| 128 |
+
else:
|
| 129 |
+
health_status["components"]["sql_database"] = "error: connection failed"
|
| 130 |
+
health_status["status"] = "degraded"
|
| 131 |
+
except Exception as e:
|
| 132 |
+
health_status["components"]["sql_database"] = f"error: {str(e)}"
|
| 133 |
+
health_status["status"] = "degraded"
|
| 134 |
+
|
| 135 |
+
# 检查 JSON 文件访问
|
| 136 |
+
try:
|
| 137 |
+
import 数据库连接 as json_db
|
| 138 |
+
json_db.load_data("users.json", default_data={})
|
| 139 |
+
health_status["components"]["json_storage"] = "ok"
|
| 140 |
+
except Exception as e:
|
| 141 |
+
health_status["components"]["json_storage"] = f"error: {str(e)}"
|
| 142 |
+
health_status["status"] = "degraded"
|
| 143 |
+
|
| 144 |
+
return health_status
|
| 145 |
+
|
| 146 |
# ==========================================
|
| 147 |
# 🚀 应用启动事件
|
| 148 |
# ==========================================
|
| 149 |
+
# 作用:初始化数据库 + 启动定时版本检测后台任务 + 预热检查
|
| 150 |
@app.on_event("startup")
|
| 151 |
async def on_startup():
|
| 152 |
+
logger.info("🚀 ComfyUI-Ranking API 启动中...")
|
| 153 |
+
|
| 154 |
+
# ========== 预热检查:SQL 数据库 ==========
|
| 155 |
+
try:
|
| 156 |
+
init_sql_db()
|
| 157 |
+
logger.info("✅ SQL 数据库初始化完成")
|
| 158 |
+
except Exception as e:
|
| 159 |
+
logger.error(f"❌ SQL 数据库初始化失败: {e}")
|
| 160 |
+
# 不抛出异常,允许降级运行
|
| 161 |
+
|
| 162 |
+
# ========== 预热检查:JSON 数据库 ==========
|
| 163 |
+
try:
|
| 164 |
+
db.load_data("users.json", default_data={})
|
| 165 |
+
db.load_data("items.json", default_data=[])
|
| 166 |
+
logger.info("✅ JSON 数据库访问正常")
|
| 167 |
+
except Exception as e:
|
| 168 |
+
logger.warning(f"⚠️ JSON 数据库访问异常: {e}")
|
| 169 |
|
| 170 |
+
# ========== 启动后台任务 ==========
|
| 171 |
asyncio.create_task(daily_version_check_task())
|
| 172 |
+
logger.info("✅ 定时版本检测任务已挂载")
|
| 173 |
+
|
| 174 |
+
logger.info("🎉 ComfyUI-Ranking API 启动完成!")
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
@app.on_event("shutdown")
|
| 178 |
+
async def on_shutdown():
|
| 179 |
+
"""优雅关闭,清理资源"""
|
| 180 |
+
logger.info("🛑 ComfyUI-Ranking API 正在关闭...")
|
| 181 |
+
# 这里可以添加其他清理逻辑(如关闭连接池等)
|
| 182 |
+
logger.info("✅ 关闭完成")
|
| 183 |
|
| 184 |
app.add_middleware(
|
| 185 |
CORSMiddleware,
|
database_sql.py
CHANGED
|
@@ -1,25 +1,117 @@
|
|
| 1 |
# database_sql.py
|
|
|
|
|
|
|
|
|
|
| 2 |
import os
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
| 4 |
from sqlalchemy.orm import sessionmaker
|
|
|
|
|
|
|
| 5 |
from models_sql import Base
|
| 6 |
|
|
|
|
|
|
|
| 7 |
# 核心:优先读取环境变量中的 PostgreSQL 数据库连接
|
| 8 |
SQLALCHEMY_DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:////tmp/comfy_financial.db")
|
| 9 |
|
| 10 |
-
#
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args=connect_args)
|
| 14 |
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
def init_sql_db():
|
| 17 |
-
|
| 18 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
def get_db():
|
|
|
|
| 21 |
db = SessionLocal()
|
| 22 |
try:
|
| 23 |
yield db
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
finally:
|
| 25 |
-
db.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
# database_sql.py
|
| 2 |
+
# ==========================================
|
| 3 |
+
# 🛡️ 稳定性优化:SQL 数据库连接模块
|
| 4 |
+
# ==========================================
|
| 5 |
import os
|
| 6 |
+
import time
|
| 7 |
+
import logging
|
| 8 |
+
from functools import wraps
|
| 9 |
+
from sqlalchemy import create_engine, text
|
| 10 |
from sqlalchemy.orm import sessionmaker
|
| 11 |
+
from sqlalchemy.pool import QueuePool, NullPool
|
| 12 |
+
from sqlalchemy.exc import OperationalError, InterfaceError, DBAPIError
|
| 13 |
from models_sql import Base
|
| 14 |
|
| 15 |
+
logger = logging.getLogger("ComfyUI-Ranking.Database")
|
| 16 |
+
|
| 17 |
# 核心:优先读取环境变量中的 PostgreSQL 数据库连接
|
| 18 |
SQLALCHEMY_DATABASE_URL = os.environ.get("DATABASE_URL", "sqlite:////tmp/comfy_financial.db")
|
| 19 |
|
| 20 |
+
# 🚀 P1性能优化:根据数据库类型配置连接池
|
| 21 |
+
if "sqlite" in SQLALCHEMY_DATABASE_URL:
|
| 22 |
+
# SQLite:使用空连接池,单线程模式
|
| 23 |
+
connect_args = {"check_same_thread": False}
|
| 24 |
+
engine = create_engine(
|
| 25 |
+
SQLALCHEMY_DATABASE_URL,
|
| 26 |
+
connect_args=connect_args,
|
| 27 |
+
poolclass=NullPool # SQLite 不需要连接池
|
| 28 |
+
)
|
| 29 |
+
else:
|
| 30 |
+
# PostgreSQL/MySQL:配置连接池参数
|
| 31 |
+
engine = create_engine(
|
| 32 |
+
SQLALCHEMY_DATABASE_URL,
|
| 33 |
+
poolclass=QueuePool,
|
| 34 |
+
pool_size=5, # 核心连接数
|
| 35 |
+
max_overflow=10, # 超出 pool_size 后可创建的最大连接数
|
| 36 |
+
pool_timeout=30, # 获取连接超时(秒)
|
| 37 |
+
pool_recycle=1800, # 连接回收时间(30分钟),防止数据库断开
|
| 38 |
+
pool_pre_ping=True # 使用前检测连接有效性
|
| 39 |
+
)
|
| 40 |
|
|
|
|
| 41 |
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 42 |
|
| 43 |
+
|
| 44 |
+
# ==========================================
|
| 45 |
+
# 🛡️ 稳定性优化:数据库操作重试装饰器
|
| 46 |
+
# ==========================================
|
| 47 |
+
def db_retry(max_retries: int = 3, delay: float = 0.5):
|
| 48 |
+
"""
|
| 49 |
+
数据库操作重试装饰器
|
| 50 |
+
|
| 51 |
+
用法:
|
| 52 |
+
@db_retry(max_retries=3)
|
| 53 |
+
def my_db_function(db):
|
| 54 |
+
...
|
| 55 |
+
"""
|
| 56 |
+
def decorator(func):
|
| 57 |
+
@wraps(func)
|
| 58 |
+
def wrapper(*args, **kwargs):
|
| 59 |
+
last_error = None
|
| 60 |
+
for attempt in range(max_retries):
|
| 61 |
+
try:
|
| 62 |
+
return func(*args, **kwargs)
|
| 63 |
+
except (OperationalError, InterfaceError, DBAPIError) as e:
|
| 64 |
+
last_error = e
|
| 65 |
+
if attempt < max_retries - 1:
|
| 66 |
+
wait_time = delay * (2 ** attempt) # 指数退避
|
| 67 |
+
logger.warning(f"DB 操作失败 (第{attempt+1}次):{e}, {wait_time}秒后重试")
|
| 68 |
+
time.sleep(wait_time)
|
| 69 |
+
else:
|
| 70 |
+
logger.error(f"DB 操作失败 (重试{max_retries}次后放弃): {e}")
|
| 71 |
+
raise last_error
|
| 72 |
+
return wrapper
|
| 73 |
+
return decorator
|
| 74 |
+
|
| 75 |
+
|
| 76 |
def init_sql_db():
|
| 77 |
+
"""初始化数据库,包含重试机制"""
|
| 78 |
+
for attempt in range(3):
|
| 79 |
+
try:
|
| 80 |
+
Base.metadata.create_all(bind=engine)
|
| 81 |
+
logger.info("数据库初始化成功")
|
| 82 |
+
return
|
| 83 |
+
except Exception as e:
|
| 84 |
+
if attempt < 2:
|
| 85 |
+
logger.warning(f"数据库初始化失败 (第{attempt+1}次): {e}")
|
| 86 |
+
time.sleep(2 ** attempt)
|
| 87 |
+
else:
|
| 88 |
+
logger.error(f"数据库初始化失败 (已重试3次): {e}")
|
| 89 |
+
raise
|
| 90 |
+
|
| 91 |
|
| 92 |
def get_db():
|
| 93 |
+
"""获取数据库会话,带连接有效性检测"""
|
| 94 |
db = SessionLocal()
|
| 95 |
try:
|
| 96 |
yield db
|
| 97 |
+
except (OperationalError, InterfaceError) as e:
|
| 98 |
+
logger.error(f"数据库连接错误: {e}")
|
| 99 |
+
db.rollback()
|
| 100 |
+
raise
|
| 101 |
finally:
|
| 102 |
+
db.close()
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
def check_db_connection() -> bool:
|
| 106 |
+
"""
|
| 107 |
+
检查数据库连接是否正常
|
| 108 |
+
用于健康检查接口
|
| 109 |
+
"""
|
| 110 |
+
try:
|
| 111 |
+
db = SessionLocal()
|
| 112 |
+
db.execute(text("SELECT 1"))
|
| 113 |
+
db.close()
|
| 114 |
+
return True
|
| 115 |
+
except Exception as e:
|
| 116 |
+
logger.error(f"数据库连接检查失败: {e}")
|
| 117 |
+
return False
|
requirements.txt
CHANGED
|
@@ -1,12 +1,15 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
|
|
|
| 7 |
alibabacloud_dysmsapi20170525==2.0.24
|
| 8 |
-
sqlalchemy
|
| 9 |
-
psycopg2-binary
|
| 10 |
-
httpx
|
| 11 |
-
python-alipay-sdk
|
| 12 |
-
aiofiles
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🔒 P0安全修复:依赖版本锁定,防止供应链攻击和不兼容更新
|
| 2 |
+
fastapi==0.104.1
|
| 3 |
+
uvicorn==0.24.0
|
| 4 |
+
pydantic==2.5.2
|
| 5 |
+
huggingface_hub==0.19.4
|
| 6 |
+
datasets==2.15.0
|
| 7 |
+
python-multipart==0.0.6
|
| 8 |
alibabacloud_dysmsapi20170525==2.0.24
|
| 9 |
+
sqlalchemy==2.0.23
|
| 10 |
+
psycopg2-binary==2.9.9
|
| 11 |
+
httpx==0.25.2
|
| 12 |
+
python-alipay-sdk==3.1.0
|
| 13 |
+
aiofiles==23.2.1
|
| 14 |
+
# 🚀 P2优化:速率限制
|
| 15 |
+
slowapi==0.1.9
|
router_items.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
# router_items.py
|
| 2 |
-
from fastapi import APIRouter, HTTPException
|
| 3 |
import time
|
| 4 |
import uuid
|
| 5 |
import datetime
|
|
@@ -9,6 +9,7 @@ import urllib.error
|
|
| 9 |
import json
|
| 10 |
import 数据库连接 as db
|
| 11 |
from models import ItemCreate, ItemUpdate
|
|
|
|
| 12 |
|
| 13 |
router = APIRouter()
|
| 14 |
|
|
@@ -56,15 +57,29 @@ async def get_items(type: str = "tool", sort: str = "time", limit: int = 50): #
|
|
| 56 |
|
| 57 |
@router.get("/api/creators")
|
| 58 |
async def get_creators(sort: str = "downloads", limit: int = 20):
|
|
|
|
|
|
|
|
|
|
|
|
|
| 59 |
users_db = db.load_data("users.json", default_data={})
|
| 60 |
items_db = db.load_data("items.json", default_data=[])
|
| 61 |
comments_db = db.load_data("comments.json", default_data={})
|
| 62 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
creators = []
|
| 64 |
months = get_last_6_months()
|
| 65 |
|
| 66 |
for account, u in users_db.items():
|
| 67 |
-
|
|
|
|
| 68 |
|
| 69 |
trend_tools = {m: 0 for m in months}
|
| 70 |
trend_apps = {m: 0 for m in months}
|
|
@@ -126,7 +141,11 @@ async def create_item(item: ItemCreate):
|
|
| 126 |
return {"status": "success", "data": new_item}
|
| 127 |
|
| 128 |
@router.put("/api/items/{item_id}")
|
| 129 |
-
async def update_item(item_id: str, update_data: ItemUpdate,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
if update_data.price is not None:
|
| 131 |
update_data.price = int(update_data.price)
|
| 132 |
if update_data.price < 0:
|
|
@@ -135,7 +154,9 @@ async def update_item(item_id: str, update_data: ItemUpdate, author: str):
|
|
| 135 |
items_db = db.load_data("items.json", default_data=[])
|
| 136 |
for item in items_db:
|
| 137 |
if item["id"] == item_id:
|
| 138 |
-
|
|
|
|
|
|
|
| 139 |
|
| 140 |
if update_data.title is not None: item["title"] = update_data.title
|
| 141 |
if update_data.shortDesc is not None: item["shortDesc"] = update_data.shortDesc
|
|
|
|
| 1 |
# router_items.py
|
| 2 |
+
from fastapi import APIRouter, HTTPException, Depends
|
| 3 |
import time
|
| 4 |
import uuid
|
| 5 |
import datetime
|
|
|
|
| 9 |
import json
|
| 10 |
import 数据库连接 as db
|
| 11 |
from models import ItemCreate, ItemUpdate
|
| 12 |
+
from 安全认证 import require_auth
|
| 13 |
|
| 14 |
router = APIRouter()
|
| 15 |
|
|
|
|
| 57 |
|
| 58 |
@router.get("/api/creators")
|
| 59 |
async def get_creators(sort: str = "downloads", limit: int = 20):
|
| 60 |
+
"""
|
| 61 |
+
获取创作者列表
|
| 62 |
+
🚀 P1性能优化:预构建 author->items 索引,避免 N+1 查询
|
| 63 |
+
"""
|
| 64 |
users_db = db.load_data("users.json", default_data={})
|
| 65 |
items_db = db.load_data("items.json", default_data=[])
|
| 66 |
comments_db = db.load_data("comments.json", default_data={})
|
| 67 |
|
| 68 |
+
# 🚀 P1性能优化:预构建 author->items 索引,复杂度从 O(n*m) 降到 O(n+m)
|
| 69 |
+
author_items_index = {}
|
| 70 |
+
for item in items_db:
|
| 71 |
+
author = item.get("author")
|
| 72 |
+
if author:
|
| 73 |
+
if author not in author_items_index:
|
| 74 |
+
author_items_index[author] = []
|
| 75 |
+
author_items_index[author].append(item)
|
| 76 |
+
|
| 77 |
creators = []
|
| 78 |
months = get_last_6_months()
|
| 79 |
|
| 80 |
for account, u in users_db.items():
|
| 81 |
+
# 🚀 P1性能优化:直接从索引获取,而非遍历全表
|
| 82 |
+
u_items = author_items_index.get(account, [])
|
| 83 |
|
| 84 |
trend_tools = {m: 0 for m in months}
|
| 85 |
trend_apps = {m: 0 for m in months}
|
|
|
|
| 141 |
return {"status": "success", "data": new_item}
|
| 142 |
|
| 143 |
@router.put("/api/items/{item_id}")
|
| 144 |
+
async def update_item(item_id: str, update_data: ItemUpdate, current_user: str = Depends(require_auth)):
|
| 145 |
+
"""
|
| 146 |
+
更新内容接口
|
| 147 |
+
🔒 P0安全修复:使用 JWT Token 验证用户身份,而非前端传入的 author 参数
|
| 148 |
+
"""
|
| 149 |
if update_data.price is not None:
|
| 150 |
update_data.price = int(update_data.price)
|
| 151 |
if update_data.price < 0:
|
|
|
|
| 154 |
items_db = db.load_data("items.json", default_data=[])
|
| 155 |
for item in items_db:
|
| 156 |
if item["id"] == item_id:
|
| 157 |
+
# 🔒 P0安全修复:使用 JWT 解析出的真实用户账号进行校验
|
| 158 |
+
if item.get("author") != current_user:
|
| 159 |
+
raise HTTPException(status_code=403, detail="无权修改他人发布的内容")
|
| 160 |
|
| 161 |
if update_data.title is not None: item["title"] = update_data.title
|
| 162 |
if update_data.shortDesc is not None: item["shortDesc"] = update_data.shortDesc
|
router_messages.py
CHANGED
|
@@ -12,6 +12,20 @@ from 安全认证 import require_auth
|
|
| 12 |
|
| 13 |
router = APIRouter()
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
# ==========================================
|
| 16 |
# 新增:系统公告请求体模型
|
| 17 |
# ==========================================
|
|
@@ -24,8 +38,8 @@ class SystemAnnouncement(BaseModel):
|
|
| 24 |
# ==========================================
|
| 25 |
@router.post("/api/system/announcement")
|
| 26 |
async def publish_announcement(ann: SystemAnnouncement, current_user: str = Depends(require_auth)):
|
| 27 |
-
#
|
| 28 |
-
if
|
| 29 |
raise HTTPException(status_code=403, detail="无权发布系统公告,仅管理员可操作")
|
| 30 |
|
| 31 |
announcements_db = db.load_data("announcements.json", default_data=[])
|
|
@@ -33,7 +47,7 @@ async def publish_announcement(ann: SystemAnnouncement, current_user: str = Depe
|
|
| 33 |
new_ann = {
|
| 34 |
"id": f"sys_{int(time.time())}_{uuid.uuid4().hex[:6]}",
|
| 35 |
"type": "system",
|
| 36 |
-
"from_user":
|
| 37 |
"from_name": "官方团队",
|
| 38 |
"from_avatar": "https://via.placeholder.com/150/FF9800/FFFFFF?text=Sys",
|
| 39 |
"content": ann.content,
|
|
@@ -52,23 +66,37 @@ class AdminScriptRequest(BaseModel):
|
|
| 52 |
admin_account: str
|
| 53 |
script_name: str
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
@router.post("/api/admin/run-script")
|
| 56 |
async def run_admin_script(req: AdminScriptRequest, current_user: str = Depends(require_auth)):
|
| 57 |
"""
|
| 58 |
管理员专属:执行指定的 Python 脚本
|
| 59 |
-
|
| 60 |
"""
|
| 61 |
-
#
|
| 62 |
-
if
|
| 63 |
raise HTTPException(status_code=403, detail="无权执行此操作,仅管理员可操作")
|
| 64 |
|
| 65 |
script_name = req.script_name.strip()
|
| 66 |
if not script_name:
|
| 67 |
raise HTTPException(status_code=400, detail="脚本名称不能为空")
|
| 68 |
|
| 69 |
-
# 安全
|
| 70 |
-
if
|
| 71 |
-
raise HTTPException(status_code=400, detail="
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
# 获取当前工作目录
|
| 74 |
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
@@ -78,7 +106,7 @@ async def run_admin_script(req: AdminScriptRequest, current_user: str = Depends(
|
|
| 78 |
if not os.path.exists(script_path):
|
| 79 |
return {
|
| 80 |
"status": "error",
|
| 81 |
-
"output": f"❌ 脚本文件不存在: {script_name}\n\n
|
| 82 |
}
|
| 83 |
|
| 84 |
try:
|
|
|
|
| 12 |
|
| 13 |
router = APIRouter()
|
| 14 |
|
| 15 |
+
# ==========================================
|
| 16 |
+
# 🔒 P0安全修复:管理员账号从环境变量读取
|
| 17 |
+
# ==========================================
|
| 18 |
+
# 支持多管理员,逗号分隔,如:ADMIN_ACCOUNTS=admin1,admin2,admin3
|
| 19 |
+
ADMIN_ACCOUNTS = set(
|
| 20 |
+
acc.strip()
|
| 21 |
+
for acc in os.environ.get("ADMIN_ACCOUNTS", "").split(",")
|
| 22 |
+
if acc.strip()
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
def is_admin(account: str) -> bool:
|
| 26 |
+
"""检查账号是否为管理员"""
|
| 27 |
+
return account in ADMIN_ACCOUNTS
|
| 28 |
+
|
| 29 |
# ==========================================
|
| 30 |
# 新增:系统公告请求体模型
|
| 31 |
# ==========================================
|
|
|
|
| 38 |
# ==========================================
|
| 39 |
@router.post("/api/system/announcement")
|
| 40 |
async def publish_announcement(ann: SystemAnnouncement, current_user: str = Depends(require_auth)):
|
| 41 |
+
# 🔒 P0安全修复:使用环境变量配置的管理员列表
|
| 42 |
+
if not is_admin(current_user):
|
| 43 |
raise HTTPException(status_code=403, detail="无权发布系统公告,仅管理员可操作")
|
| 44 |
|
| 45 |
announcements_db = db.load_data("announcements.json", default_data=[])
|
|
|
|
| 47 |
new_ann = {
|
| 48 |
"id": f"sys_{int(time.time())}_{uuid.uuid4().hex[:6]}",
|
| 49 |
"type": "system",
|
| 50 |
+
"from_user": current_user, # 使用真实的管理员账号
|
| 51 |
"from_name": "官方团队",
|
| 52 |
"from_avatar": "https://via.placeholder.com/150/FF9800/FFFFFF?text=Sys",
|
| 53 |
"content": ann.content,
|
|
|
|
| 66 |
admin_account: str
|
| 67 |
script_name: str
|
| 68 |
|
| 69 |
+
# 🔒 P0安全修复:脚本白名单(仅允许执行指定的脚本)
|
| 70 |
+
# 警告:添加新脚本前请确保其安全性
|
| 71 |
+
ALLOWED_SCRIPTS = {
|
| 72 |
+
"密码迁移.py", # 用户密码哈希化迁移
|
| 73 |
+
"测试脚本.py", # 接口测试工具
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
@router.post("/api/admin/run-script")
|
| 77 |
async def run_admin_script(req: AdminScriptRequest, current_user: str = Depends(require_auth)):
|
| 78 |
"""
|
| 79 |
管理员专属:执行指定的 Python 脚本
|
| 80 |
+
🔒 P0安全修复:白名单 + 路径穿越防护
|
| 81 |
"""
|
| 82 |
+
# 🔒 P0安全修复:使用环境变量配置的管理员列表
|
| 83 |
+
if not is_admin(current_user):
|
| 84 |
raise HTTPException(status_code=403, detail="无权执行此操作,仅管理员可操作")
|
| 85 |
|
| 86 |
script_name = req.script_name.strip()
|
| 87 |
if not script_name:
|
| 88 |
raise HTTPException(status_code=400, detail="脚本名称不能为空")
|
| 89 |
|
| 90 |
+
# 🔒 P0安全修复:路径穿越攻击防护
|
| 91 |
+
if ".." in script_name or "/" in script_name or "\\" in script_name:
|
| 92 |
+
raise HTTPException(status_code=400, detail="🚨 安全拦截:脚本名称包含非法字符")
|
| 93 |
+
|
| 94 |
+
# 🔒 P0安全修复:白名单检查
|
| 95 |
+
if script_name not in ALLOWED_SCRIPTS:
|
| 96 |
+
raise HTTPException(
|
| 97 |
+
status_code=403,
|
| 98 |
+
detail=f"🚨 安全拦截:脚本 [{script_name}] 不在白名单中。允许的脚本: {list(ALLOWED_SCRIPTS)}"
|
| 99 |
+
)
|
| 100 |
|
| 101 |
# 获取当前工作目录
|
| 102 |
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
| 106 |
if not os.path.exists(script_path):
|
| 107 |
return {
|
| 108 |
"status": "error",
|
| 109 |
+
"output": f"❌ 脚本文件不存在: {script_name}\n\n白名单脚本: {list(ALLOWED_SCRIPTS)}"
|
| 110 |
}
|
| 111 |
|
| 112 |
try:
|
router_users_auth.py
CHANGED
|
@@ -17,11 +17,16 @@ import random
|
|
| 17 |
import json
|
| 18 |
import 数据库连接 as db
|
| 19 |
from models import UserRegister, UserLogin, SendCodeRequest
|
| 20 |
-
from verify_code_engine import VERIFY_CODES, send_email_code, send_sms_code
|
| 21 |
|
| 22 |
# 🔒 P0安全增强:导入密码哈希和 JWT 工具
|
| 23 |
from 安全认证 import hash_password, verify_password, create_token
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
# 创建子路由实例
|
| 26 |
router = APIRouter()
|
| 27 |
|
|
@@ -33,7 +38,8 @@ router = APIRouter()
|
|
| 33 |
# 关联:verify_code_engine.py 的 send_email_code / send_sms_code
|
| 34 |
# 前端调用:注册表单组件.js、重置密码表单组件.js
|
| 35 |
@router.post("/api/users/send-code")
|
| 36 |
-
|
|
|
|
| 37 |
"""
|
| 38 |
发送验证码接口(异步版本)
|
| 39 |
|
|
@@ -62,6 +68,9 @@ async def send_verify_code(req: SendCodeRequest, bg_tasks: BackgroundTasks):
|
|
| 62 |
# 生成6位随机验证码
|
| 63 |
code = str(random.randint(100000, 999999))
|
| 64 |
|
|
|
|
|
|
|
|
|
|
| 65 |
# 构建缓存键(联系方式_动作类型)
|
| 66 |
cache_key = f"{req.contact}_{req.action_type}"
|
| 67 |
|
|
@@ -209,7 +218,8 @@ async def register_user(user: UserRegister):
|
|
| 209 |
# - 数据库连接.py (读取 users.json 校验密码)
|
| 210 |
# - 前端 登录表单组件.js
|
| 211 |
@router.post("/api/users/login")
|
| 212 |
-
|
|
|
|
| 213 |
"""
|
| 214 |
用户登录接口
|
| 215 |
|
|
|
|
| 17 |
import json
|
| 18 |
import 数据库连接 as db
|
| 19 |
from models import UserRegister, UserLogin, SendCodeRequest
|
| 20 |
+
from verify_code_engine import VERIFY_CODES, send_email_code, send_sms_code, cleanup_expired_codes
|
| 21 |
|
| 22 |
# 🔒 P0安全增强:导入密码哈希和 JWT 工具
|
| 23 |
from 安全认证 import hash_password, verify_password, create_token
|
| 24 |
|
| 25 |
+
# 🚀 P2优化:速率限制
|
| 26 |
+
from slowapi import Limiter
|
| 27 |
+
from slowapi.util import get_remote_address
|
| 28 |
+
limiter = Limiter(key_func=get_remote_address)
|
| 29 |
+
|
| 30 |
# 创建子路由实例
|
| 31 |
router = APIRouter()
|
| 32 |
|
|
|
|
| 38 |
# 关联:verify_code_engine.py 的 send_email_code / send_sms_code
|
| 39 |
# 前端调用:注册表单组件.js、重置密码表单组件.js
|
| 40 |
@router.post("/api/users/send-code")
|
| 41 |
+
@limiter.limit("5/minute") # 🚀 P2优化:每分钟最多5次
|
| 42 |
+
async def send_verify_code(request: Request, req: SendCodeRequest, bg_tasks: BackgroundTasks):
|
| 43 |
"""
|
| 44 |
发送验证码接口(异步版本)
|
| 45 |
|
|
|
|
| 68 |
# 生成6位随机验证码
|
| 69 |
code = str(random.randint(100000, 999999))
|
| 70 |
|
| 71 |
+
# 🚀 P1性能优化:每次写入前触发清理过期验证码
|
| 72 |
+
cleanup_expired_codes()
|
| 73 |
+
|
| 74 |
# 构建缓存键(联系方式_动作类型)
|
| 75 |
cache_key = f"{req.contact}_{req.action_type}"
|
| 76 |
|
|
|
|
| 218 |
# - 数据库连接.py (读取 users.json 校验密码)
|
| 219 |
# - 前端 登录表单组件.js
|
| 220 |
@router.post("/api/users/login")
|
| 221 |
+
@limiter.limit("10/minute") # 🚀 P2优化:每分钟最多10次登录尝试
|
| 222 |
+
async def login_user(request: Request, user: UserLogin):
|
| 223 |
"""
|
| 224 |
用户登录接口
|
| 225 |
|
router_wallet.py
CHANGED
|
@@ -17,11 +17,15 @@ import uuid
|
|
| 17 |
import hashlib
|
| 18 |
import os
|
| 19 |
import datetime
|
|
|
|
| 20 |
from database_sql import get_db
|
| 21 |
from models_sql import Wallet, Transaction, Ownership
|
| 22 |
from models import RechargeRequest, WithdrawRequest, PurchaseRequest, TipRequest
|
| 23 |
import 数据库连接 as json_db
|
| 24 |
|
|
|
|
|
|
|
|
|
|
| 25 |
# 🔐 导入验证码缓存 (提现时需要验证)
|
| 26 |
from verify_code_engine import VERIFY_CODES
|
| 27 |
|
|
@@ -180,6 +184,9 @@ async def purchase_item(req: PurchaseRequest, db: Session = Depends(get_db)):
|
|
| 180 |
db.add(new_tx)
|
| 181 |
db.commit()
|
| 182 |
|
|
|
|
|
|
|
|
|
|
| 183 |
return {"status": "success", "already_owned": False}
|
| 184 |
|
| 185 |
@router.post("/api/wallet/tip")
|
|
@@ -224,6 +231,9 @@ async def tip_user(req: TipRequest, db: Session = Depends(get_db)):
|
|
| 224 |
db.add(tx_target)
|
| 225 |
db.commit()
|
| 226 |
|
|
|
|
|
|
|
|
|
|
| 227 |
# 🚀 核心新增:记录打赏榜单和月度收益趋势 (写入 JSON 以供高频读取)
|
| 228 |
users_db = json_db.load_data("users.json", default_data={})
|
| 229 |
items_db = json_db.load_data("items.json", default_data=[])
|
|
@@ -267,7 +277,9 @@ async def tip_user(req: TipRequest, db: Session = Depends(get_db)):
|
|
| 267 |
async def withdraw(req: WithdrawRequest, db: Session = Depends(get_db)):
|
| 268 |
key = f"{req.account}_withdraw"
|
| 269 |
code_data = VERIFY_CODES.get(key)
|
| 270 |
-
|
|
|
|
|
|
|
| 271 |
raise HTTPException(status_code=400, detail="验证码无效或已过期")
|
| 272 |
|
| 273 |
wallet = db.query(Wallet).filter(Wallet.account == req.account).with_for_update().first()
|
|
@@ -327,6 +339,9 @@ async def withdraw(req: WithdrawRequest, db: Session = Depends(get_db)):
|
|
| 327 |
|
| 328 |
db.commit()
|
| 329 |
|
|
|
|
|
|
|
|
|
|
| 330 |
del VERIFY_CODES[key]
|
| 331 |
return {
|
| 332 |
"status": "success",
|
|
|
|
| 17 |
import hashlib
|
| 18 |
import os
|
| 19 |
import datetime
|
| 20 |
+
import logging
|
| 21 |
from database_sql import get_db
|
| 22 |
from models_sql import Wallet, Transaction, Ownership
|
| 23 |
from models import RechargeRequest, WithdrawRequest, PurchaseRequest, TipRequest
|
| 24 |
import 数据库连接 as json_db
|
| 25 |
|
| 26 |
+
# 📝 P2优化:审计日志
|
| 27 |
+
logger = logging.getLogger("ComfyUI-Ranking.Wallet")
|
| 28 |
+
|
| 29 |
# 🔐 导入验证码缓存 (提现时需要验证)
|
| 30 |
from verify_code_engine import VERIFY_CODES
|
| 31 |
|
|
|
|
| 184 |
db.add(new_tx)
|
| 185 |
db.commit()
|
| 186 |
|
| 187 |
+
# 📝 P2优化:购买审计日志
|
| 188 |
+
logger.info(f"PURCHASE | buyer={req.account} | seller={seller_account} | item={req.item_id} | amount={price} | tx={tx_id}")
|
| 189 |
+
|
| 190 |
return {"status": "success", "already_owned": False}
|
| 191 |
|
| 192 |
@router.post("/api/wallet/tip")
|
|
|
|
| 231 |
db.add(tx_target)
|
| 232 |
db.commit()
|
| 233 |
|
| 234 |
+
# 📝 P2优化:打赏审计日志
|
| 235 |
+
logger.info(f"TIP | from={req.sender_account} | to={req.target_account} | amount={req.amount} | item={req.item_id or 'N/A'} | anon={req.is_anonymous}")
|
| 236 |
+
|
| 237 |
# 🚀 核心新增:记录打赏榜单和月度收益趋势 (写入 JSON 以供高频读取)
|
| 238 |
users_db = json_db.load_data("users.json", default_data={})
|
| 239 |
items_db = json_db.load_data("items.json", default_data=[])
|
|
|
|
| 277 |
async def withdraw(req: WithdrawRequest, db: Session = Depends(get_db)):
|
| 278 |
key = f"{req.account}_withdraw"
|
| 279 |
code_data = VERIFY_CODES.get(key)
|
| 280 |
+
# 🔒 P0安全修复:统一使用 expires_at 字段,兼容旧版 expires
|
| 281 |
+
expire_time = code_data.get("expires_at", code_data.get("expires", 0)) if code_data else 0
|
| 282 |
+
if not code_data or code_data["code"] != req.code or time.time() > expire_time:
|
| 283 |
raise HTTPException(status_code=400, detail="验证码无效或已过期")
|
| 284 |
|
| 285 |
wallet = db.query(Wallet).filter(Wallet.account == req.account).with_for_update().first()
|
|
|
|
| 339 |
|
| 340 |
db.commit()
|
| 341 |
|
| 342 |
+
# 📝 P2优化:提现审计日志
|
| 343 |
+
logger.info(f"WITHDRAW | account={req.account} | amount={actual_withdraw} | fee={fee_amount} | net={net_amount} | tx={tx_id}")
|
| 344 |
+
|
| 345 |
del VERIFY_CODES[key]
|
| 346 |
return {
|
| 347 |
"status": "success",
|
verify_code_engine.py
CHANGED
|
@@ -10,6 +10,7 @@
|
|
| 10 |
|
| 11 |
import os
|
| 12 |
import json
|
|
|
|
| 13 |
import urllib.request
|
| 14 |
import urllib.parse
|
| 15 |
import base64
|
|
@@ -22,6 +23,37 @@ import base64
|
|
| 22 |
# 注意:服务重启后缓存会清空,生产环境建议改用 Redis
|
| 23 |
VERIFY_CODES = {}
|
| 24 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
def send_email_code(to_email: str, code: str, action: str):
|
| 27 |
"""
|
|
|
|
| 10 |
|
| 11 |
import os
|
| 12 |
import json
|
| 13 |
+
import time
|
| 14 |
import urllib.request
|
| 15 |
import urllib.parse
|
| 16 |
import base64
|
|
|
|
| 23 |
# 注意:服务重启后缓存会清空,生产环境建议改用 Redis
|
| 24 |
VERIFY_CODES = {}
|
| 25 |
|
| 26 |
+
# 🚀 P1性能优化:记录上次清理时间,避免频繁清理
|
| 27 |
+
_last_cleanup_time = 0
|
| 28 |
+
_CLEANUP_INTERVAL = 300 # 5分钟清理一次
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def cleanup_expired_codes():
|
| 32 |
+
"""
|
| 33 |
+
🚀 P1性能优化:清理过期验证码,防止内存泄漏
|
| 34 |
+
"""
|
| 35 |
+
global _last_cleanup_time
|
| 36 |
+
now = time.time()
|
| 37 |
+
|
| 38 |
+
# 限制清理频率,避免每次请求都清理
|
| 39 |
+
if now - _last_cleanup_time < _CLEANUP_INTERVAL:
|
| 40 |
+
return
|
| 41 |
+
|
| 42 |
+
_last_cleanup_time = now
|
| 43 |
+
|
| 44 |
+
# 收集过期的键
|
| 45 |
+
expired_keys = [
|
| 46 |
+
key for key, data in VERIFY_CODES.items()
|
| 47 |
+
if now > data.get("expires_at", data.get("expires", 0))
|
| 48 |
+
]
|
| 49 |
+
|
| 50 |
+
# 删除过期验证码
|
| 51 |
+
for key in expired_keys:
|
| 52 |
+
del VERIFY_CODES[key]
|
| 53 |
+
|
| 54 |
+
if expired_keys:
|
| 55 |
+
print(f"🧹 已清理 {len(expired_keys)} 个过期验证码,当前缓存: {len(VERIFY_CODES)} 条")
|
| 56 |
+
|
| 57 |
|
| 58 |
def send_email_code(to_email: str, code: str, action: str):
|
| 59 |
"""
|
云端_定时版本检测引擎.py
CHANGED
|
@@ -2,10 +2,16 @@
|
|
| 2 |
import asyncio
|
| 3 |
import datetime
|
| 4 |
import httpx
|
|
|
|
| 5 |
import 数据库连接 as db
|
| 6 |
|
|
|
|
|
|
|
| 7 |
async def fetch_latest_github_hash(repo_url, token):
|
| 8 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 9 |
try:
|
| 10 |
parts = repo_url.rstrip("/").split("/")
|
| 11 |
if len(parts) < 2: return None
|
|
@@ -15,15 +21,27 @@ async def fetch_latest_github_hash(repo_url, token):
|
|
| 15 |
headers = {"Accept": "application/vnd.github.v3+json", "User-Agent": "ComfyUI-Hub"}
|
| 16 |
if token:
|
| 17 |
headers["Authorization"] = f"token {token}"
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
except Exception as e:
|
| 26 |
-
|
| 27 |
return None
|
| 28 |
|
| 29 |
async def daily_version_check_task():
|
|
|
|
| 2 |
import asyncio
|
| 3 |
import datetime
|
| 4 |
import httpx
|
| 5 |
+
import logging
|
| 6 |
import 数据库连接 as db
|
| 7 |
|
| 8 |
+
logger = logging.getLogger("ComfyUI-Ranking.VersionCheck")
|
| 9 |
+
|
| 10 |
async def fetch_latest_github_hash(repo_url, token):
|
| 11 |
+
"""
|
| 12 |
+
请求 GitHub API 获取最新版的 Commit Hash
|
| 13 |
+
🛡️ 稳定性优化:超时保护 + 异常捕获 + 重试机制
|
| 14 |
+
"""
|
| 15 |
try:
|
| 16 |
parts = repo_url.rstrip("/").split("/")
|
| 17 |
if len(parts) < 2: return None
|
|
|
|
| 21 |
headers = {"Accept": "application/vnd.github.v3+json", "User-Agent": "ComfyUI-Hub"}
|
| 22 |
if token:
|
| 23 |
headers["Authorization"] = f"token {token}"
|
| 24 |
+
|
| 25 |
+
# 🛡️ 稳定性优化:超时 + 重试
|
| 26 |
+
async with httpx.AsyncClient(follow_redirects=True, timeout=httpx.Timeout(15.0, connect=5.0)) as client:
|
| 27 |
+
for attempt in range(3): # 最多重试 3 次
|
| 28 |
+
try:
|
| 29 |
+
resp = await client.get(api_url, headers=headers)
|
| 30 |
+
if resp.status_code == 200:
|
| 31 |
+
data = resp.json()
|
| 32 |
+
if isinstance(data, list) and len(data) > 0:
|
| 33 |
+
return data[0]["sha"] # 返回最新的 Commit Hash
|
| 34 |
+
elif resp.status_code == 403: # Rate limit
|
| 35 |
+
logger.warning(f"GitHub API 速率限制: {repo_url}")
|
| 36 |
+
return None
|
| 37 |
+
break
|
| 38 |
+
except (httpx.TimeoutException, httpx.ConnectError) as e:
|
| 39 |
+
if attempt < 2:
|
| 40 |
+
await asyncio.sleep(2 ** attempt) # 指数退避
|
| 41 |
+
continue
|
| 42 |
+
logger.warning(f"版本检测超时 {repo_url}: {e}")
|
| 43 |
except Exception as e:
|
| 44 |
+
logger.error(f"检测版本失败 {repo_url}: {e}")
|
| 45 |
return None
|
| 46 |
|
| 47 |
async def daily_version_check_task():
|