ZHIWEI666 commited on
Commit
a0ab3de
·
verified ·
1 Parent(s): ca36b9a

提升安全与流畅度

Browse files
Dockerfile CHANGED
@@ -1,10 +1,20 @@
1
- FROM python:3.9
 
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
- # 初始化 SQL 数据库
55
- init_sql_db()
56
- print("关系型数据库加载完毕,金融表同步完成。")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- from sqlalchemy import create_engine
 
 
 
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
- # 兼容处理只有在使用降级的 SQLite 时,才需要 check_same_thread=False
11
- connect_args = {"check_same_thread": False} if "sqlite" in SQLALCHEMY_DATABASE_URL else {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- Base.metadata.create_all(bind=engine)
 
 
 
 
 
 
 
 
 
 
 
 
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
- fastapi
2
- uvicorn
3
- pydantic
4
- huggingface_hub
5
- datasets
6
- python-multipart
 
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
- u_items = [i for i in items_db if i.get("author") == account]
 
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, author: str):
 
 
 
 
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
- if item.get("author") != author: raise HTTPException(status_code=403, detail="无权修改他人发布内容")
 
 
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
- # 安全检查:使用 JWT Token 解析出真实用户账号,而不是请求体中的字段
28
- if current_user != "123456":
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": "123456",
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
- 安全改造使用 JWT Token 验证真实登录用户
60
  """
61
- # 安全检查:使用 JWT Token 解析出真实用户账号
62
- if current_user != "123456":
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
- # 安全检查仅允许执行 .py 文件
70
- if not script_name.endswith(".py"):
71
- raise HTTPException(status_code=400, detail="仅支持执行 .py 文件")
 
 
 
 
 
 
 
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当前目录: {current_dir}\n\n可用脚本: {[f for f in os.listdir(current_dir) if f.endswith('.py')]}"
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
- async def send_verify_code(req: SendCodeRequest, bg_tasks: BackgroundTasks):
 
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
- async def login_user(user: UserLogin):
 
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
- if not code_data or code_data["code"] != req.code or time.time() > code_data["expires"]:
 
 
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
- """请求 GitHub API 获取最新版的 Commit Hash"""
 
 
 
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
- async with httpx.AsyncClient(follow_redirects=True) as client:
20
- resp = await client.get(api_url, headers=headers, timeout=15.0)
21
- if resp.status_code == 200:
22
- data = resp.json()
23
- if isinstance(data, list) and len(data) > 0:
24
- return data[0]["sha"] # 返回最新的 Commit Hash
 
 
 
 
 
 
 
 
 
 
 
 
25
  except Exception as e:
26
- print(f"检测版本失败 {repo_url}: {e}")
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():