|
|
|
|
import os
|
|
|
|
|
import logging
|
|
|
|
|
from contextlib import asynccontextmanager
|
|
|
|
|
from fastapi import FastAPI
|
|
|
|
|
from tortoise import Tortoise
|
|
|
|
|
from redis.asyncio import Redis
|
|
|
|
|
from fastapi.staticfiles import StaticFiles
|
|
|
|
|
from aiomysql import create_pool, OperationalError as AiomysqlOperationalError
|
|
|
|
|
|
|
|
|
|
from app.core.exceptions import SettingNotFound
|
|
|
|
|
from app.core.init_app import (
|
|
|
|
|
init_data,
|
|
|
|
|
make_middlewares,
|
|
|
|
|
register_exceptions,
|
|
|
|
|
register_routers,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
from app.settings.config import settings
|
|
|
|
|
except ImportError:
|
|
|
|
|
raise SettingNotFound("Can not import settings")
|
|
|
|
|
|
|
|
|
|
# 配置日志
|
|
|
|
|
logging.basicConfig(level=logging.INFO)
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@asynccontextmanager
|
|
|
|
|
async def lifespan(app: FastAPI):
|
|
|
|
|
# 初始化资源
|
|
|
|
|
app.state.mysql_pool = None
|
|
|
|
|
app.state.redis_client = None
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# 初始化 MySQL 连接池
|
|
|
|
|
app.state.mysql_pool = await create_pool(
|
|
|
|
|
host=settings.FLOW_MYSQL_HOST,
|
|
|
|
|
port=settings.FLOW_MYSQL_PORT,
|
|
|
|
|
user=settings.FLOW_MYSQL_USER,
|
|
|
|
|
password=settings.FLOW_MYSQL_PASSWORD,
|
|
|
|
|
db=settings.FLOW_MYSQL_DB,
|
|
|
|
|
# 核心并发参数
|
|
|
|
|
minsize=15, # 初始保持20个连接
|
|
|
|
|
maxsize=50, # 最大连接数
|
|
|
|
|
# 连接管理优化
|
|
|
|
|
pool_recycle=100, # 180秒回收连接
|
|
|
|
|
connect_timeout=15, # 连接超时时间
|
|
|
|
|
# 其他优化
|
|
|
|
|
charset='utf8mb4',
|
|
|
|
|
echo=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# 验证连接有效性
|
|
|
|
|
async with app.state.mysql_pool.acquire() as conn:
|
|
|
|
|
await conn.ping()
|
|
|
|
|
logger.info("✅ MySQL 连接池初始化成功")
|
|
|
|
|
|
|
|
|
|
except AiomysqlOperationalError as e:
|
|
|
|
|
logger.error(f"❌ MySQL 连接池初始化失败: 数据库操作错误 - {str(e)}")
|
|
|
|
|
raise
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ MySQL 连接池初始化失败: 未知错误 - {str(e)}")
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
# 初始化 Redis 连接
|
|
|
|
|
app.state.redis_client = Redis(
|
|
|
|
|
host=settings.REDIS_HOST,
|
|
|
|
|
port=settings.REDIS_PORT,
|
|
|
|
|
db=settings.REDIS_DB,
|
|
|
|
|
decode_responses=True,
|
|
|
|
|
password=settings.REDIS_PASSWORD,
|
|
|
|
|
socket_connect_timeout=5,
|
|
|
|
|
socket_keepalive=True,
|
|
|
|
|
retry_on_timeout=True,
|
|
|
|
|
# 连接池参数
|
|
|
|
|
max_connections=1024, # Redis连接池最大连接数
|
|
|
|
|
health_check_interval=30,
|
|
|
|
|
retry_on_error=[ConnectionError, TimeoutError], # 重试错误类型
|
|
|
|
|
single_connection_client=False, # 不使用单连接客户端
|
|
|
|
|
auto_close_connection_pool=True # 自动关闭连接池
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await app.state.redis_client.ping()
|
|
|
|
|
logger.info("✅ Redis 连接成功")
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ Redis 连接失败: {str(e)}")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
await init_data()
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.error(f"❌ 初始化数据失败: {str(e)}")
|
|
|
|
|
raise
|
|
|
|
|
|
|
|
|
|
yield
|
|
|
|
|
|
|
|
|
|
# 资源清理
|
|
|
|
|
if app.state.redis_client:
|
|
|
|
|
try:
|
|
|
|
|
await app.state.redis_client.close()
|
|
|
|
|
logger.info("🛑 Redis 连接已关闭")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning(f"⚠️ Redis 关闭失败: {str(e)}")
|
|
|
|
|
|
|
|
|
|
if app.state.mysql_pool:
|
|
|
|
|
try:
|
|
|
|
|
app.state.mysql_pool.close()
|
|
|
|
|
await app.state.mysql_pool.wait_closed()
|
|
|
|
|
logger.info("🛑 MySQL 连接池已关闭")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning(f"⚠️ MySQL 连接池关闭失败: {str(e)}")
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
await Tortoise.close_connections()
|
|
|
|
|
logger.info("🛑 Tortoise 连接已关闭")
|
|
|
|
|
except Exception as e:
|
|
|
|
|
logger.warning(f"⚠️ Tortoise 连接关闭失败: {str(e)}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def create_app() -> FastAPI:
|
|
|
|
|
app = FastAPI(
|
|
|
|
|
title=settings.APP_TITLE,
|
|
|
|
|
description=settings.APP_DESCRIPTION,
|
|
|
|
|
version=settings.VERSION,
|
|
|
|
|
openapi_url="/openapi.json",
|
|
|
|
|
middleware=make_middlewares(),
|
|
|
|
|
lifespan=lifespan,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
static_dir = os.path.join(settings.BASE_DIR, 'web', 'public', 'resource')
|
|
|
|
|
if os.path.exists(static_dir):
|
|
|
|
|
app.mount("/resource", StaticFiles(directory=static_dir), name="resource")
|
|
|
|
|
else:
|
|
|
|
|
logger.warning(f"⚠️ 静态文件目录不存在: {static_dir}")
|
|
|
|
|
|
|
|
|
|
register_exceptions(app)
|
|
|
|
|
register_routers(app, prefix="/api")
|
|
|
|
|
return app
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app = create_app()
|