mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-10-13 12:49:27 +00:00
* feat: 新增统一认证中心 * perf: 优化代码 * fix: 修复问题 * perf: 优化页面 * fix: 修复部分接口缺失Request参数的问题 * perf: 优化访问控制机制 * perf: 优化代码 * fix: 修复 Logout Token 缺少 exp * perf&fix: 优化和修复 * perf: 优化代码 * perf: 优化代码 * fix: 修复测试问题
269 lines
9.2 KiB
Python
269 lines
9.2 KiB
Python
import asyncio
|
||
from collections.abc import AsyncGenerator
|
||
from contextlib import asynccontextmanager
|
||
|
||
from fastapi import FastAPI
|
||
|
||
from common.constant import LockConstant
|
||
from common.router import auto_register_routers
|
||
from config.database import DataSourceRegistry
|
||
from config.env import AppConfig
|
||
from config.get_redis import RedisUtil
|
||
from config.lifecycle import init_create_table
|
||
from config.scheduler.manager import SchedulerManager
|
||
from exceptions.handle import handle_exception
|
||
from middlewares.handle import handle_middleware
|
||
from module_admin.service.log_service import LogAggregatorService
|
||
from module_identity.service.runtime_service import OidcRuntimeService
|
||
from plugins.core.runtime.application import get_plugin_application_runtime
|
||
from sub_applications.handle import handle_sub_applications
|
||
from utils.common_util import worship
|
||
from utils.log_util import logger
|
||
from utils.server_util import APIDocsUtil, IPUtil, StartupUtil
|
||
from utils.transport_crypto_util import TransportKeyProvider
|
||
|
||
|
||
async def _start_background_tasks(app: FastAPI) -> None:
|
||
"""
|
||
启动应用后台任务
|
||
|
||
:param app: FastAPI对象
|
||
:return: None
|
||
"""
|
||
await SchedulerManager.init_system_scheduler(app.state.redis)
|
||
app.state.log_aggregator_task = asyncio.create_task(LogAggregatorService.consume_stream(app.state.redis))
|
||
await OidcRuntimeService.start_background_tasks(app)
|
||
|
||
|
||
async def _stop_background_tasks(app: FastAPI) -> None:
|
||
"""
|
||
停止应用后台任务并释放资源
|
||
|
||
:param app: FastAPI对象
|
||
:return: None
|
||
"""
|
||
try:
|
||
log_task = getattr(app.state, 'log_aggregator_task', None)
|
||
if log_task:
|
||
log_task.cancel()
|
||
try:
|
||
await log_task
|
||
except asyncio.CancelledError:
|
||
pass
|
||
await OidcRuntimeService.stop_background_tasks(app)
|
||
finally:
|
||
try:
|
||
redis = getattr(app.state, 'redis', None)
|
||
if redis is not None:
|
||
try:
|
||
# Scheduler负责停止续期并释放Application租约,必须先于Redis连接池关闭。
|
||
await SchedulerManager.close_system_scheduler()
|
||
finally:
|
||
await RedisUtil.close_redis_pool(app)
|
||
finally:
|
||
await DataSourceRegistry.dispose_all()
|
||
|
||
|
||
async def _initialize_application_runtime(app: FastAPI, application_leader: bool) -> None:
|
||
"""
|
||
初始化应用运行时资源。
|
||
|
||
:param app: FastAPI对象
|
||
:param application_leader: 当前worker是否为Application leader
|
||
:return: None
|
||
"""
|
||
await DataSourceRegistry.initialize(log_enabled=application_leader)
|
||
|
||
plugin_runtime = get_plugin_application_runtime()
|
||
plugin_runtime.prepare_metadata(app)
|
||
|
||
await init_create_table(
|
||
stage='platform',
|
||
log_success_enabled=application_leader,
|
||
)
|
||
try:
|
||
await OidcRuntimeService.refresh_cors_snapshot(app)
|
||
except Exception:
|
||
app.state.oidc_registered_cors_origins = ()
|
||
logger.error('OIDC 已注册跨域来源快照加载失败')
|
||
await OidcRuntimeService.validate_runtime(app)
|
||
|
||
async def create_plugin_entity_tables() -> None:
|
||
"""
|
||
在插件 writer 导入实体后同步插件表。
|
||
"""
|
||
await init_create_table(
|
||
stage='plugin_entities',
|
||
log_success_enabled=True,
|
||
)
|
||
|
||
await plugin_runtime.startup(
|
||
app,
|
||
create_tables=create_plugin_entity_tables,
|
||
)
|
||
app.state.plugin_application_runtime_started = True
|
||
await RedisUtil.check_redis_connection(
|
||
app.state.redis,
|
||
log_enabled=application_leader,
|
||
log_error_enabled=True,
|
||
)
|
||
await RedisUtil.init_sys_dict(app.state.redis)
|
||
await RedisUtil.init_sys_config(app.state.redis)
|
||
app.state.application_leader = application_leader
|
||
await _start_background_tasks(app)
|
||
|
||
|
||
async def _shutdown_application_runtime(app: FastAPI) -> None:
|
||
"""
|
||
关闭插件运行时并保证基础设施资源始终释放。
|
||
|
||
:param app: FastAPI对象
|
||
:return: None
|
||
"""
|
||
try:
|
||
if getattr(app.state, 'plugin_application_runtime_started', False):
|
||
await get_plugin_application_runtime().shutdown(app)
|
||
finally:
|
||
try:
|
||
await _stop_background_tasks(app)
|
||
finally:
|
||
# 所有sink均使用enqueue=True,进程退出前必须等待插件Hook等尾部日志落盘。
|
||
await logger.complete()
|
||
|
||
|
||
def _log_address_group(
|
||
title: str,
|
||
local_ip: str,
|
||
network_ips: list[str],
|
||
*,
|
||
path: str = '',
|
||
) -> None:
|
||
"""
|
||
输出一组本地和网络访问地址。
|
||
|
||
:param title: 地址分组标题。
|
||
:param local_ip: 本机地址。
|
||
:param network_ips: 网络地址列表。
|
||
:param path: 地址路径。
|
||
"""
|
||
port = AppConfig.app_port
|
||
links = [f'🏠 Local: <cyan>http://{local_ip}:{port}{path}</cyan>']
|
||
links.extend(f'📡 Network: <cyan>http://{ip}:{port}{path}</cyan>' for ip in network_ips)
|
||
logger.opt(colors=True).info(f'{title}:\n' + '\n'.join(links))
|
||
|
||
|
||
def _show_startup_addresses() -> None:
|
||
"""
|
||
显示应用及接口文档访问地址
|
||
|
||
:return: None
|
||
"""
|
||
host = AppConfig.app_host
|
||
if host == '0.0.0.0':
|
||
local_ip = IPUtil.get_local_ip()
|
||
network_ips = IPUtil.get_network_ips()
|
||
else:
|
||
local_ip = host
|
||
network_ips = [host]
|
||
|
||
_log_address_group('💻 应用地址', local_ip, network_ips)
|
||
|
||
if not AppConfig.app_disable_swagger:
|
||
_log_address_group('📄 Swagger文档', local_ip, network_ips, path=APIDocsUtil.docs_url())
|
||
|
||
if not AppConfig.app_disable_redoc:
|
||
_log_address_group('📚 ReDoc文档', local_ip, network_ips, path=APIDocsUtil.redoc_url())
|
||
|
||
|
||
# 生命周期事件
|
||
@asynccontextmanager
|
||
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||
"""
|
||
应用生命周期管理
|
||
|
||
:param app: FastAPI对象
|
||
:return: None
|
||
"""
|
||
app.state.redis = None
|
||
app.state.plugin_application_runtime_started = False
|
||
try:
|
||
app.state.redis = await RedisUtil.create_redis_pool(log_enabled=False)
|
||
application_lock_owner_token = SchedulerManager.get_application_lock_owner_token()
|
||
application_leader = await StartupUtil.acquire_application_leader(
|
||
redis=app.state.redis,
|
||
lock_key=LockConstant.APP_STARTUP_LOCK_KEY,
|
||
owner_token=application_lock_owner_token,
|
||
lock_expire_seconds=LockConstant.LOCK_EXPIRE_SECONDS,
|
||
)
|
||
app.state.application_leader = application_leader
|
||
app.state.application_lock_owner_token = application_lock_owner_token
|
||
|
||
# 获取锁成功后立即启动锁续期任务,避免初始化时间过长导致锁过期
|
||
if application_leader:
|
||
SchedulerManager.start_application_lock_renewal(app.state.redis)
|
||
|
||
startup_logger = logger.bind(
|
||
startup_phase='application_startup',
|
||
startup_role='application_leader',
|
||
)
|
||
if application_leader:
|
||
startup_logger.info(f'⏰️ {AppConfig.app_name}开始启动')
|
||
startup_logger.bind(
|
||
storage_timezone='UTC',
|
||
app_timezone=AppConfig.app_timezone,
|
||
).info(f'🕒 时间系统已配置:storageTimeZone=UTC,appTimeZone={AppConfig.app_timezone}')
|
||
worship()
|
||
TransportKeyProvider.validate_runtime_configuration()
|
||
await _initialize_application_runtime(app, application_leader=application_leader)
|
||
|
||
# 初始化期间可能因续期失败失去租约;此时不得继续输出leader专属成功摘要。
|
||
application_leader = application_leader and SchedulerManager.is_application_leader()
|
||
app.state.application_leader = application_leader
|
||
if application_leader:
|
||
# 短暂等待确保下面的启动日志在最后打印
|
||
await asyncio.sleep(1)
|
||
startup_logger.info(f'🚀 {AppConfig.app_name}启动成功')
|
||
_show_startup_addresses()
|
||
# 确保启动阶段的插件摘要在ASGI lifespan启动完成前已写入stdout和日志文件。
|
||
await logger.complete()
|
||
yield
|
||
finally:
|
||
await _shutdown_application_runtime(app)
|
||
|
||
|
||
def create_app() -> FastAPI:
|
||
"""
|
||
创建FastAPI应用
|
||
|
||
:return: FastAPI对象
|
||
"""
|
||
# 配置API文档静态资源
|
||
APIDocsUtil.setup_docs_static_resources()
|
||
# 初始化FastAPI对象
|
||
app = FastAPI(
|
||
title=AppConfig.app_name,
|
||
description=f'{AppConfig.app_name}接口文档',
|
||
version=AppConfig.app_version,
|
||
lifespan=lifespan,
|
||
openapi_url=APIDocsUtil.proxy_openapi_url(),
|
||
docs_url=APIDocsUtil.proxy_docs_url(),
|
||
redoc_url=APIDocsUtil.proxy_redoc_url(),
|
||
swagger_ui_oauth2_redirect_url=APIDocsUtil.proxy_oauth2_redirect_url(),
|
||
)
|
||
|
||
# 自定义API文档路由,修复无法直接通过后端地址访问文档的问题
|
||
APIDocsUtil.custom_api_docs_router(app)
|
||
|
||
# 挂载子应用
|
||
handle_sub_applications(app)
|
||
# 加载中间件处理方法
|
||
handle_middleware(app)
|
||
# 加载全局异常处理方法
|
||
handle_exception(app)
|
||
# 自动注册内置路由
|
||
auto_register_routers(app)
|
||
# 初始化插件应用运行时
|
||
get_plugin_application_runtime().bind_app(app)
|
||
|
||
return app
|