2026-04-26 10:20:32 +08:00
|
|
|
import time
|
2026-04-20 17:11:31 +08:00
|
|
|
from pathlib import Path
|
2026-03-06 17:57:44 +08:00
|
|
|
from contextlib import asynccontextmanager
|
|
|
|
|
|
2026-04-15 16:05:31 +08:00
|
|
|
from fastapi import FastAPI, Request
|
2026-04-13 15:55:12 +08:00
|
|
|
from fastapi.responses import JSONResponse
|
2026-03-06 17:57:44 +08:00
|
|
|
from fastapi.middleware.cors import CORSMiddleware
|
2026-04-20 17:11:31 +08:00
|
|
|
from fastapi.staticfiles import StaticFiles
|
2026-03-06 17:57:44 +08:00
|
|
|
from sqlmodel import SQLModel, text
|
|
|
|
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
|
|
|
|
from sqlalchemy.exc import OperationalError
|
|
|
|
|
|
|
|
|
|
from .core.config import get_config
|
2026-04-13 15:55:12 +08:00
|
|
|
from .core.cache import get_cache
|
2026-03-06 17:57:44 +08:00
|
|
|
from .core.logger import get_logger
|
2026-04-15 16:05:31 +08:00
|
|
|
from .core.static_files import SVFSStaticFiles
|
2026-03-06 17:57:44 +08:00
|
|
|
from .api import api_router
|
2026-04-13 15:55:12 +08:00
|
|
|
from .task import init_tasks_callback
|
|
|
|
|
|
2026-04-26 10:20:32 +08:00
|
|
|
from plugins.scheduler.scheduler import get_task_service_instance
|
2026-04-13 15:55:12 +08:00
|
|
|
|
2026-03-06 17:57:44 +08:00
|
|
|
|
|
|
|
|
config = get_config()
|
2026-04-26 10:20:32 +08:00
|
|
|
cache = get_cache()
|
2026-03-06 17:57:44 +08:00
|
|
|
logger = get_logger("app")
|
|
|
|
|
|
2026-04-20 17:11:31 +08:00
|
|
|
|
2026-03-06 17:57:44 +08:00
|
|
|
try:
|
|
|
|
|
env = config["default"]["env"]
|
|
|
|
|
except KeyError: # pragma: no cover
|
|
|
|
|
raise ValueError("配置不存在")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager
|
|
|
|
|
async def lifespan(app: FastAPI):
|
2026-04-26 21:57:17 +08:00
|
|
|
"""
|
|
|
|
|
应用生命周期管理函数,负责应用的启动和关闭时的资源初始化与清理。
|
|
|
|
|
|
|
|
|
|
在应用启动时:
|
|
|
|
|
- 初始化JWT缓存
|
|
|
|
|
- 创建异步数据库引擎和会话工厂
|
|
|
|
|
- 在开发环境下创建数据库表
|
|
|
|
|
- 初始化定时任务服务
|
|
|
|
|
|
|
|
|
|
在应用关闭时:
|
|
|
|
|
- 关闭定时任务服务
|
|
|
|
|
- 释放数据库引擎资源
|
|
|
|
|
- 关闭缓存连接
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
app: FastAPI应用实例
|
|
|
|
|
|
|
|
|
|
Yields:
|
|
|
|
|
None: 应用运行期间保持上下文激活
|
|
|
|
|
"""
|
2026-03-06 17:57:44 +08:00
|
|
|
from plugins import hello
|
2026-04-26 10:20:32 +08:00
|
|
|
from plugins.jwt.jwt_cache import get_jwt_cache
|
|
|
|
|
|
|
|
|
|
get_jwt_cache(config, cache)
|
2026-03-06 17:57:44 +08:00
|
|
|
|
|
|
|
|
hello()
|
|
|
|
|
database = config["database"]
|
|
|
|
|
database_type = database["type"]
|
|
|
|
|
database_config = database.get(database_type, None)
|
|
|
|
|
if database_config is None:
|
|
|
|
|
raise ValueError("配置不存在")
|
|
|
|
|
|
|
|
|
|
engine = create_async_engine(database["url"], **database_config)
|
|
|
|
|
async_session_maker = async_sessionmaker(
|
|
|
|
|
engine, class_=AsyncSession, expire_on_commit=False
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
app.state.engine = engine
|
|
|
|
|
app.state.async_session_maker = async_session_maker
|
|
|
|
|
|
|
|
|
|
if env == "development":
|
|
|
|
|
async with engine.begin() as conn:
|
|
|
|
|
if database_type == "sqlite":
|
|
|
|
|
await conn.execute(text("PRAGMA journal_mode=WAL"))
|
|
|
|
|
await conn.execute(text("PRAGMA synchronous=NORMAL"))
|
|
|
|
|
try:
|
|
|
|
|
await conn.run_sync(
|
|
|
|
|
lambda conn: SQLModel.metadata.create_all(
|
|
|
|
|
bind=conn, checkfirst=True
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
except OperationalError as e:
|
|
|
|
|
if "already exists" in str(e):
|
|
|
|
|
raise
|
|
|
|
|
|
2026-04-26 10:20:32 +08:00
|
|
|
task_service = get_task_service_instance(
|
|
|
|
|
config, get_logger("scheduler"), async_session_maker
|
|
|
|
|
)
|
2026-04-15 16:05:31 +08:00
|
|
|
app.state.task_service = task_service
|
|
|
|
|
await task_service.start(init_tasks_callback)
|
|
|
|
|
|
2026-04-26 21:57:17 +08:00
|
|
|
yield
|
2026-03-06 17:57:44 +08:00
|
|
|
|
2026-04-13 15:55:12 +08:00
|
|
|
await task_service.shutdown(wait=True)
|
2026-03-06 17:57:44 +08:00
|
|
|
await engine.dispose()
|
2026-04-26 10:20:32 +08:00
|
|
|
cache.close()
|
2026-03-06 17:57:44 +08:00
|
|
|
|
|
|
|
|
|
2026-04-20 17:11:31 +08:00
|
|
|
cors = config[env].get("cors", {})
|
2026-03-06 17:57:44 +08:00
|
|
|
app = FastAPI(
|
|
|
|
|
title=config["default"]["app"]["name"],
|
|
|
|
|
version=config["default"]["app"]["version"],
|
|
|
|
|
description=config["default"]["app"]["description"],
|
|
|
|
|
docs_url="/docs" if config[env]["debug"] else None,
|
|
|
|
|
redoc_url="/redoc" if config[env]["debug"] else None,
|
|
|
|
|
openapi_url="/openapi.json" if config[env]["debug"] else None,
|
|
|
|
|
lifespan=lifespan,
|
|
|
|
|
)
|
2026-04-20 17:11:31 +08:00
|
|
|
|
2026-03-06 17:57:44 +08:00
|
|
|
app.add_middleware(CORSMiddleware, **cors)
|
2026-04-20 17:11:31 +08:00
|
|
|
|
|
|
|
|
app.include_router(api_router, prefix=config["default"]["prefix"])
|
2026-03-06 17:57:44 +08:00
|
|
|
|
|
|
|
|
|
2026-04-26 10:20:32 +08:00
|
|
|
@app.middleware("http")
|
|
|
|
|
async def access_log_middleware(request: Request, call_next):
|
2026-04-26 21:57:17 +08:00
|
|
|
"""
|
|
|
|
|
HTTP访问日志中间件,记录每个请求的详细信息。
|
|
|
|
|
|
|
|
|
|
记录的信息包括:
|
|
|
|
|
- 请求方法和路径
|
|
|
|
|
- 响应状态码
|
|
|
|
|
- 请求处理时长(毫秒)
|
|
|
|
|
- 客户端IP地址
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
request: FastAPI请求对象
|
|
|
|
|
call_next: 下一个中间件或路由处理函数
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
Response: 处理后的响应对象
|
|
|
|
|
"""
|
2026-04-26 10:20:32 +08:00
|
|
|
start = time.perf_counter()
|
|
|
|
|
response = await call_next(request)
|
|
|
|
|
duration = (time.perf_counter() - start) * 1000
|
|
|
|
|
client = request.client.host if request.client else "-"
|
|
|
|
|
|
|
|
|
|
logger.info(
|
|
|
|
|
f"{request.method} {request.url.path} status={response.status_code} "
|
|
|
|
|
f"duration={duration:.2f}ms client={client}",
|
2026-04-26 21:57:17 +08:00
|
|
|
extra={"status_code": response.status_code},
|
2026-04-26 10:20:32 +08:00
|
|
|
)
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
|
2026-03-06 17:57:44 +08:00
|
|
|
@app.get("/", summary="根路径")
|
|
|
|
|
async def root():
|
2026-04-26 21:57:17 +08:00
|
|
|
"""
|
|
|
|
|
根路径端点,返回应用的基本信息。
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
dict: 包含应用名称、版本、文档地址和健康检查地址的字典
|
|
|
|
|
"""
|
2026-03-06 17:57:44 +08:00
|
|
|
return {
|
2026-04-13 15:55:12 +08:00
|
|
|
"message": f"欢迎使用 {config['default']['app']['name']}",
|
2026-03-06 17:57:44 +08:00
|
|
|
"version": config["default"]["app"]["version"],
|
|
|
|
|
"docs": "/docs" if config[env]["debug"] else None,
|
2026-04-20 17:11:31 +08:00
|
|
|
"health": f"{config['default']['prefix']}/health",
|
2026-03-06 17:57:44 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2026-04-26 10:20:32 +08:00
|
|
|
# from pydantic import BaseModel
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# class TaskRequest(BaseModel):
|
|
|
|
|
# task_name: str
|
|
|
|
|
# interval: int
|
|
|
|
|
|
|
|
|
|
|
2026-04-15 16:05:31 +08:00
|
|
|
# @app.post("/schedule")
|
|
|
|
|
# async def add_task(
|
|
|
|
|
# request: TaskRequest, task_service: TaskService = Depends(get_task_service)
|
|
|
|
|
# ):
|
|
|
|
|
# """添加定时任务接口"""
|
|
|
|
|
# job = await task_service.add_interval_job(
|
|
|
|
|
# execute_background_task,
|
|
|
|
|
# seconds=request.interval,
|
|
|
|
|
# args=[request.task_name],
|
|
|
|
|
# job_id=request.task_name,
|
|
|
|
|
# )
|
|
|
|
|
# return {"status": "success", "job_id": job.id}
|
2026-04-13 15:55:12 +08:00
|
|
|
|
2026-04-20 17:11:31 +08:00
|
|
|
config = get_config()
|
|
|
|
|
IMAGE_DIR = Path(f"./{config['default']['upload_path']}/images")
|
|
|
|
|
app.mount(
|
|
|
|
|
f"{config['default']['prefix']}/images",
|
|
|
|
|
StaticFiles(directory=f"./{config['default']['upload_path']}/images"),
|
|
|
|
|
name="images",
|
|
|
|
|
)
|
2026-04-13 15:55:12 +08:00
|
|
|
|
2026-04-20 17:11:31 +08:00
|
|
|
if vfs_db_path := config[env].get("file_system", {}).get("path", None):
|
2026-04-15 16:05:31 +08:00
|
|
|
logger.info("静态文件已存在,将使用静态文件服务")
|
2026-04-20 17:11:31 +08:00
|
|
|
static_files_handler = SVFSStaticFiles(directory="/", vfs_db_path=vfs_db_path)
|
2026-04-14 19:44:25 +08:00
|
|
|
app.mount("/static", static_files_handler, name="static")
|
|
|
|
|
|
|
|
|
|
|
2026-03-06 17:57:44 +08:00
|
|
|
@app.exception_handler(Exception)
|
2026-04-13 15:55:12 +08:00
|
|
|
async def global_exception_handler(_: Request, exc: Exception):
|
2026-04-26 21:57:17 +08:00
|
|
|
"""
|
|
|
|
|
全局异常处理器,捕获并处理应用中未捕获的异常。
|
|
|
|
|
|
|
|
|
|
在调试模式下返回详细的异常信息,在生产模式下返回通用错误提示。
|
|
|
|
|
|
|
|
|
|
参数:
|
|
|
|
|
_: FastAPI请求对象(未使用)
|
|
|
|
|
exc: 捕获到的异常对象
|
|
|
|
|
|
|
|
|
|
返回:
|
|
|
|
|
JSONResponse: 包含错误信息的JSON响应,状态码为500
|
|
|
|
|
"""
|
2026-03-06 17:57:44 +08:00
|
|
|
logger.error(f"全局异常: {exc}")
|
2026-04-13 15:55:12 +08:00
|
|
|
return JSONResponse(
|
|
|
|
|
status_code=500,
|
|
|
|
|
content={
|
|
|
|
|
"error": "内部服务器错误",
|
|
|
|
|
"detail": str(exc) if config[env]["debug"] else "请查看服务器日志",
|
|
|
|
|
},
|
|
|
|
|
)
|