diff --git a/backend/app/api/v1/module_application/myapp/controller.py b/backend/app/api/v1/module_application/myapp/controller.py index 1bce9ceb..52a46732 100644 --- a/backend/app/api/v1/module_application/myapp/controller.py +++ b/backend/app/api/v1/module_application/myapp/controller.py @@ -24,7 +24,7 @@ MyAppRouter = APIRouter(route_class=OperationLogRoute, prefix="/application", ta @MyAppRouter.get("/detail/{id}", summary="获取应用详情", description="获取应用详情") async def get_obj_detail_controller( id: int = Path(..., description="应用ID"), - auth: AuthSchema = Depends(AuthPermission(permissions=["application:system:query"])) + auth: AuthSchema = Depends(AuthPermission(permissions=["application:myapp:query"])) ) -> JSONResponse: result_dict = await ApplicationService.get_application_detail_service(id=id, auth=auth) logger.info(f"获取应用详情成功 {id}") @@ -34,7 +34,7 @@ async def get_obj_detail_controller( async def get_obj_list_controller( page: PaginationQueryParams = Depends(), search: ApplicationQueryParams = Depends(), - auth: AuthSchema = Depends(AuthPermission(permissions=["application:system:query"])) + auth: AuthSchema = Depends(AuthPermission(permissions=["application:myapp:query"])) ) -> JSONResponse: result_dict_list = await ApplicationService.get_application_list_service(auth=auth, search=search, order_by=page.order_by) result_dict = await PaginationService.get_page_obj(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size) @@ -44,7 +44,7 @@ async def get_obj_list_controller( @MyAppRouter.post("/create", summary="创建应用", description="创建应用") async def create_obj_controller( data: ApplicationCreateSchema, - auth: AuthSchema = Depends(AuthPermission(permissions=["application:system:create"])) + auth: AuthSchema = Depends(AuthPermission(permissions=["application:myapp:create"])) ) -> JSONResponse: result_dict = await ApplicationService.create_application_service(auth=auth, data=data) logger.info(f"创建应用成功: {result_dict}") @@ -54,7 +54,7 @@ async def create_obj_controller( async def update_obj_controller( data: ApplicationUpdateSchema, id: int = Path(..., description="应用ID"), - auth: AuthSchema = Depends(AuthPermission(permissions=["application:system:update"])) + auth: AuthSchema = Depends(AuthPermission(permissions=["application:myapp:update"])) ) -> JSONResponse: result_dict = await ApplicationService.update_application_service(auth=auth, id=id, data=data) logger.info(f"修改应用成功: {result_dict}") @@ -63,7 +63,7 @@ async def update_obj_controller( @MyAppRouter.delete("/delete", summary="删除应用", description="删除应用") async def delete_obj_controller( ids: list[int] = Body(..., description="ID列表"), - auth: AuthSchema = Depends(AuthPermission(permissions=["application:system:delete"])) + auth: AuthSchema = Depends(AuthPermission(permissions=["application:myapp:delete"])) ) -> JSONResponse: await ApplicationService.delete_application_service(auth=auth, ids=ids) logger.info(f"删除应用成功: {ids}") @@ -72,7 +72,7 @@ async def delete_obj_controller( @MyAppRouter.patch("/available/setting", summary="批量修改应用状态", description="批量修改应用状态") async def batch_set_available_obj_controller( data: BatchSetAvailable, - auth: AuthSchema = Depends(AuthPermission(permissions=["application:system:patch"])) + auth: AuthSchema = Depends(AuthPermission(permissions=["application:myapp:patch"])) ) -> JSONResponse: await ApplicationService.set_application_available_service(auth=auth, data=data) logger.info(f"批量修改应用状态成功: {data.ids}") diff --git a/backend/app/api/v1/module_resource/resource/service.py b/backend/app/api/v1/module_resource/resource/service.py index dd882e81..f8a9e5fe 100644 --- a/backend/app/api/v1/module_resource/resource/service.py +++ b/backend/app/api/v1/module_resource/resource/service.py @@ -146,7 +146,13 @@ class ResourceService: # 生成HTTP URL路径而不是文件系统路径 if base_url: from urllib.parse import urljoin - http_url = urljoin(base_url.rstrip('/') + '/', f"{settings.STATIC_URL.lstrip('/')}/{relative_path}".lstrip('/')).replace('\\', '/').replace('//', '/') + base_part = base_url.rstrip('/') + static_part = settings.STATIC_URL.lstrip('/') + relative_part = relative_path.lstrip('/') + # 手动构建URL而不是使用urljoin,避免双斜杠问题 + if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']): + base_part += '/' + http_url = f"{base_part}{static_part}/{relative_part}".replace('\\', '/').replace('//', '/').replace(':/', '://') else: http_url = f"{settings.STATIC_URL}/{relative_path}".replace('\\', '/').replace('//', '/') @@ -186,7 +192,12 @@ class ResourceService: # 对于根目录,返回静态URL路径 if base_url: from urllib.parse import urljoin - display_path = urljoin(base_url.rstrip('/') + '/', settings.STATIC_URL.lstrip('/')) + # 修复URL生成逻辑 + base_part = base_url.rstrip('/') + static_part = settings.STATIC_URL.lstrip('/') + if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']): + base_part += '/' + display_path = f"{base_part}{static_part}".replace('//', '/').replace(':/', '://') else: display_path = settings.STATIC_URL else: @@ -197,13 +208,24 @@ class ResourceService: relative_path = os.path.relpath(safe_path, resource_root) if base_url: from urllib.parse import urljoin - display_path = urljoin(base_url.rstrip('/') + '/', f"{settings.STATIC_URL.lstrip('/')}/{relative_path}".lstrip('/')).replace('\\', '/').replace('//', '/') + # 修复URL生成逻辑 + base_part = base_url.rstrip('/') + static_part = settings.STATIC_URL.lstrip('/') + relative_part = relative_path.lstrip('/') + if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']): + base_part += '/' + display_path = f"{base_part}{static_part}/{relative_part}".replace('\\', '/').replace('//', '/').replace(':/', '://') else: display_path = f"{settings.STATIC_URL}/{relative_path}".replace('\\', '/').replace('//', '/') except ValueError: if base_url: from urllib.parse import urljoin - display_path = urljoin(base_url.rstrip('/') + '/', settings.STATIC_URL.lstrip('/')) + # 修复URL生成逻辑 + base_part = base_url.rstrip('/') + static_part = settings.STATIC_URL.lstrip('/') + if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']): + base_part += '/' + display_path = f"{base_part}{static_part}".replace('//', '/').replace(':/', '://') else: display_path = settings.STATIC_URL @@ -466,14 +488,27 @@ class ResourceService: # 如果提供了base_url,使用它生成完整URL,否则使用settings.STATIC_URL if base_url: from urllib.parse import urljoin - file_url = urljoin(base_url.rstrip('/') + '/', f"{settings.STATIC_URL.lstrip('/')}/{file_url_path}".lstrip('/')) + # 修复URL生成逻辑 + base_part = base_url.rstrip('/') + static_part = settings.STATIC_URL.lstrip('/') + file_url_part = file_url_path.lstrip('/') + if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']): + base_part += '/' + file_url = f"{base_part}{static_part}/{file_url_part}".replace('//', '/').replace(':/', '://') else: file_url = f"{settings.STATIC_URL}/{file_url_path}".replace('//', '/') except ValueError: # 如果无法计算相对路径,使用文件名 + filename = os.path.basename(file_path) if base_url: from urllib.parse import urljoin - file_url = urljoin(base_url.rstrip('/') + '/', f"{settings.STATIC_URL.lstrip('/')}/{filename}".lstrip('/')) + # 修复URL生成逻辑 + base_part = base_url.rstrip('/') + static_part = settings.STATIC_URL.lstrip('/') + filename_part = filename.lstrip('/') + if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']): + base_part += '/' + file_url = f"{base_part}{static_part}/{filename_part}".replace('//', '/').replace(':/', '://') else: file_url = f"{settings.STATIC_URL}/{filename}" @@ -511,7 +546,13 @@ class ResourceService: # 生成HTTP URL if base_url: from urllib.parse import urljoin - http_url = urljoin(base_url.rstrip('/') + '/', f"{settings.STATIC_URL.lstrip('/')}/{relative_path}".lstrip('/')).replace('\\', '/').replace('//', '/') + # 修复URL生成逻辑 + base_part = base_url.rstrip('/') + static_part = settings.STATIC_URL.lstrip('/') + relative_part = relative_path.lstrip('/') + if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']): + base_part += '/' + http_url = f"{base_part}{static_part}/{relative_part}".replace('\\', '/').replace('//', '/').replace(':/', '://') else: http_url = f"{settings.STATIC_URL}/{relative_path}".replace('\\', '/').replace('//', '/') logger.info(f"生成文件访问URL: {http_url}") @@ -521,7 +562,13 @@ class ResourceService: filename = os.path.basename(safe_path) if base_url: from urllib.parse import urljoin - http_url = urljoin(base_url.rstrip('/') + '/', f"{settings.STATIC_URL.lstrip('/')}/{filename}".lstrip('/')) + # 修复URL生成逻辑 + base_part = base_url.rstrip('/') + static_part = settings.STATIC_URL.lstrip('/') + filename_part = filename.lstrip('/') + if base_part.endswith(':') or (len(base_part) > 0 and base_part[-1] not in ['/', ':']): + base_part += '/' + http_url = f"{base_part}{static_part}/{filename_part}".replace('//', '/').replace(':/', '://') else: http_url = f"{settings.STATIC_URL}/{filename}" logger.info(f"生成文件访问URL: {http_url}") diff --git a/backend/app/core/database.py b/backend/app/core/database.py index e178bdef..6f3ca92b 100644 --- a/backend/app/core/database.py +++ b/backend/app/core/database.py @@ -3,7 +3,7 @@ from redis import asyncio as aioredis from motor.motor_asyncio import AsyncIOMotorClient from fastapi import FastAPI -from sqlalchemy import create_engine, text +from sqlalchemy import create_engine, text, Engine from sqlalchemy.orm import sessionmaker from sqlalchemy.ext.asyncio import ( create_async_engine, @@ -27,11 +27,14 @@ from sqlalchemy.exc import ( from app.core.logger import logger from app.config.setting import settings from app.core.exceptions import CustomException +from app.core.base_model import MappedBase # 同步数据库引擎 -engine = create_engine( +engine: Engine = create_engine( url=settings.DB_URI, echo=settings.DATABASE_ECHO, + pool_pre_ping=settings.POOL_PRE_PING, + pool_recycle=settings.POOL_RECYCLE, ) # 同步数据库会话工厂 SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) @@ -67,33 +70,18 @@ def session_connect() -> AsyncSession: except Exception as e: raise CustomException(msg=f"数据库连接失败: {e}") -async def test_db_connection(session: AsyncSession) -> bool: +async def init_create_table(): + """ + 应用启动时初始化数据库连接 + + :return: + """ try: - # 执行简单查询测试连接是否真实可用 - await session.execute(text("SELECT 1")) - return True - except OperationalError as e: - raise CustomException(msg=f"数据库操作失败: {e},请检查数据库服务是否正常运行") - except TimeoutError as e: - raise CustomException(msg=f"数据库连接超时: {e},请检查网络连接或增加连接超时时间") - except DisconnectionError as e: - raise CustomException(msg=f"数据库连接中断: {e},请检查数据库服务状态") - except InterfaceError as e: - raise CustomException(msg=f"数据库接口错误: {e},请检查数据库驱动配置") - except ProgrammingError as e: - raise CustomException(msg=f"SQL语句错误: {e},请检查SQL语法") - except IntegrityError as e: - raise CustomException(msg=f"数据完整性错误: {e},请检查数据约束条件") - except DataError as e: - raise CustomException(msg=f"数据类型错误: {e},请检查数据格式") - except InternalError as e: - raise CustomException(msg=f"数据库内部错误: {e},请联系数据库管理员") - except NotSupportedError as e: - raise CustomException(msg=f"数据库不支持该操作: {e},请检查数据库版本") - except InvalidRequestError as e: - raise CustomException(msg=f"无效的数据库请求: {e},请检查请求参数") + async with async_engine.begin() as conn: + await conn.run_sync(MappedBase.metadata.create_all) + logger.info('数据库连接成功...') except Exception as e: - raise CustomException(msg=f"数据库操作异常: {e},请联系管理员") + raise CustomException(msg=f"数据库连接失败: {e}") async def redis_connect(app: FastAPI, status: bool) -> aioredis.Redis: """创建或关闭Redis连接""" diff --git a/backend/app/core/dependencies.py b/backend/app/core/dependencies.py index 5bb05ce4..5ab4eaa9 100644 --- a/backend/app/core/dependencies.py +++ b/backend/app/core/dependencies.py @@ -21,7 +21,8 @@ from app.api.v1.module_system.auth.schema import AuthSchema async def db_getter() -> AsyncGenerator[AsyncSession, None]: """获取数据库会话连接""" async with session_connect() as session: - yield session + async with session.begin(): + yield session async def redis_getter(request: Request) -> Redis: """获取Redis连接""" diff --git a/backend/app/core/router_class.py b/backend/app/core/router_class.py index 19cf0e3b..6b8d69b0 100644 --- a/backend/app/core/router_class.py +++ b/backend/app/core/router_class.py @@ -105,22 +105,23 @@ class OperationLogRoute(APIRoute): pass else: async with session_connect() as session: - auth = AuthSchema(db=session) - await OperationLogService.create_log_service(data=OperationLogCreateSchema( - type = log_type, - request_path = request.url.path, - request_method = request.method, - request_payload = payload, - request_ip = request_ip, - login_location=login_location, - request_os = user_agent.os.family, - request_browser = user_agent.browser.family, - response_code = response.status_code, - response_json = response_data.decode(), - process_time = process_time, - description = route.summary, - creator_id = current_user_id - ), auth = auth) + async with session.begin(): + auth = AuthSchema(db=session) + await OperationLogService.create_log_service(data=OperationLogCreateSchema( + type = log_type, + request_path = request.url.path, + request_method = request.method, + request_payload = payload, + request_ip = request_ip, + login_location=login_location, + request_os = user_agent.os.family, + request_browser = user_agent.browser.family, + response_code = response.status_code, + response_json = response_data.decode(), + process_time = process_time, + description = route.summary, + creator_id = current_user_id + ), auth = auth) return response diff --git a/backend/app/plugin/init_app.py b/backend/app/plugin/init_app.py index f50d4761..bb61eaf2 100644 --- a/backend/app/plugin/init_app.py +++ b/backend/app/plugin/init_app.py @@ -31,7 +31,7 @@ from app.core.exceptions import ( ResponseValidationHandle, ResponseValidationError ) -from app.core.database import session_connect, test_db_connection +from app.core.database import session_connect, init_create_table from app.scripts.initialize import InitializeData from app.api.v1.module_system.config.service import ConfigService from app.api.v1.module_system.dict.service import DictDataService @@ -46,30 +46,31 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[Any, Any]: logger.info(settings.BANNER + '\n' + f'{settings.TITLE} 服务开始启动...') try: + # 初始化数据库表 + await init_create_table() + # 使用单个会话完成所有初始化操作 async with session_connect() as session: - # 测试数据库连接 - await test_db_connection(session) - logger.info("数据库连接成功...") + async with session.begin(): - # 初始化数据库 - await InitializeData().init_db(db=session) - logger.info("初始化数据完成...") + # 初始化数据库数据 + await InitializeData().init_db(db=session) + logger.info("初始化数据完成...") - # 初始化全局事件 - await import_modules_async(modules=settings.EVENT_LIST, desc="全局事件", app=app, status=True) - - # 初始化系统配置 - await ConfigService().init_config_service(redis=app.state.redis, db=session) - logger.info("初始化系统配置完成...") - - # 初始化数据字典 - await DictDataService().init_dict_service(redis=app.state.redis, db=session) - logger.info('初始化数据字典完成...') - - # 初始化定时任务 - await SchedulerUtil.init_system_scheduler(db=session) - logger.info('初始化定时任务完成...') + # 初始化全局事件 + await import_modules_async(modules=settings.EVENT_LIST, desc="全局事件", app=app, status=True) + + # 初始化系统配置 + await ConfigService().init_config_service(redis=app.state.redis, db=session) + logger.info("初始化系统配置完成...") + + # 初始化数据字典 + await DictDataService().init_dict_service(redis=app.state.redis, db=session) + logger.info('初始化数据字典完成...') + + # 初始化定时任务 + await SchedulerUtil.init_system_scheduler(db=session) + logger.info('初始化定时任务完成...') logger.info(f'{settings.TITLE} 服务成功启动...') except Exception as e: diff --git a/backend/app/scripts/initialize.py b/backend/app/scripts/initialize.py index 2c868704..edcd7ce5 100644 --- a/backend/app/scripts/initialize.py +++ b/backend/app/scripts/initialize.py @@ -4,7 +4,7 @@ import uuid import json from pathlib import Path from typing import Dict, List -from sqlalchemy import inspect, select, func, text +from sqlalchemy import inspect, select, func from sqlalchemy.ext.asyncio import AsyncSession from app.core.base_model import MappedBase @@ -59,134 +59,46 @@ class InitializeData: RoleDeptsModel, RoleMenusModel, ] - # 需要更新序列的模型(排除关联表模型,因为它们没有id字段) - self.models_with_id = [ - DeptModel, - MenuModel, - UserModel, - RoleModel, - PositionModel, - ConfigModel, - DictTypeModel, - NoticeModel, - OperationLogModel, - DictDataModel, - JobModel, - JobLogModel, - DemoModel, - ApplicationModel, - ] - self.created_tables = set() - - async def __get_existing_tables(self, db: AsyncSession) -> List[str]: - return await db.run_sync( - lambda sync_db: inspect(sync_db.get_bind()).get_table_names() - ) - async def __init_model(self, db: AsyncSession) -> None: - """初始化数据库表结构""" - try: - # 获取所有模型元数据 - metadata = MappedBase.metadata - - # 只创建不存在的表 - for table in metadata.sorted_tables: - if table.name not in await self.__get_existing_tables(db): - await db.run_sync(lambda sync_db: table.create(sync_db.bind)) - self.created_tables.add(table.name) - logger.info(f"已创建表: {table.name}") - - await self.__init_data(db) - except Exception as e: - logger.error(f"初始化数据库结构失败: {str(e)}") - raise async def __init_data(self, db: AsyncSession) -> None: """初始化基础数据""" - try: - inserted_data = False # 标记是否有数据插入 + for model in self.prepare_init_models: + table_name = model.__tablename__ - for model in self.prepare_init_models: - table_name = model.__tablename__ - - # 检查表中是否已经有数据 - count_result = await db.execute(select(func.count()).select_from(model)) - existing_count = count_result.scalar() - - logger.info(f"检查表 {table_name} 数据: 已存在 {existing_count} 条记录") - - if existing_count > 0: - logger.warning(f"跳过 {table_name} 表数据初始化(表已存在 {existing_count} 条记录)") - continue - - data = await self.__get_data(table_name) - if not data: - logger.warning(f"跳过 {table_name} 表,无初始化数据") - continue - - try: - # 表为空,直接插入全部数据 - logger.info(f"准备向 {table_name} 表插入 {len(data)} 条记录") - objs = [model(**item) for item in data] - db.add_all(objs) - inserted_data = True - - # 对于 PostgreSQL,更新序列值 - if settings.DATABASE_TYPE == "postgresql" and model in self.models_with_id and len(objs) > 0: - await self.__update_postgresql_sequence_for_model(db, model, table_name) - - logger.info(f"已向 {table_name} 表写入 {len(objs)} 条记录") - - except Exception as e: - logger.error(f"初始化 {table_name} 表数据失败: {str(e)}") - raise + # 检查表中是否已经有数据 + count_result = await db.execute(select(func.count()).select_from(model)) + existing_count = count_result.scalar() - # 只有在有数据插入时才提交事务 - if inserted_data: - await db.commit() - logger.info("数据初始化事务已提交") - else: - logger.info("没有新数据需要插入,跳过事务提交") - - except Exception as e: - logger.error(f"初始化数据过程中出现错误: {str(e)}") - # 如果出现错误,回滚事务 - await db.rollback() - raise + if existing_count > 0: + logger.warning(f"跳过 {table_name} 表数据初始化(表已存在 {existing_count} 条记录)") + continue - async def __update_postgresql_sequence_for_model(self, db: AsyncSession, model, table_name: str) -> None: - """为特定模型更新 PostgreSQL 序列值""" - try: - # 检查模型是否有id属性 - if not hasattr(model, 'id'): - return - - # 获取表中最大的 ID 值 - max_id_result = await db.execute(select(func.max(model.id)).select_from(model)) - max_id = max_id_result.scalar() + data = await self.__get_data(table_name) + if not data: + logger.warning(f"跳过 {table_name} 表,无初始化数据") + continue - if max_id is not None and max_id > 0: - # 更新序列值 - sequence_name = f"{table_name}_id_seq" - await db.execute(text(f"SELECT setval('{sequence_name}', {max_id}, true)")) - logger.info(f"已更新 {table_name} 表的序列 {sequence_name} 值为 {max_id}") - except Exception as e: - logger.error(f"更新 {table_name} 表的 PostgreSQL 序列值失败: {str(e)}") - # 不抛出异常,因为序列更新失败不应该导致整个初始化失败 + try: + # 表为空,直接插入全部数据 + objs = [model(**item) for item in data] + db.add_all(objs) + await db.flush() + logger.info(f"已向 {table_name} 表写入 {len(objs)} 条记录") - async def __get_data(self, table_name: str) -> List[Dict]: + except Exception as e: + logger.error(f"初始化 {table_name} 表数据失败: {str(e)}") + raise + + async def __get_data(self, filename: str) -> List[Dict]: """读取初始化数据文件""" - json_path = Path.joinpath(settings.SCRIPT_DIR, f'{table_name}.json') - logger.info(f"尝试读取初始化数据文件: {json_path}") + json_path = Path.joinpath(settings.SCRIPT_DIR, f'{filename}.json') if not json_path.exists(): - logger.warning(f"初始化数据文件不存在: {json_path}") return [] try: with open(json_path, 'r', encoding='utf-8') as f: - data = json.loads(f.read()) - logger.info(f"成功读取 {table_name} 数据文件,包含 {len(data)} 条记录") - return data + return json.loads(f.read()) except json.JSONDecodeError as e: logger.error(f"解析 {json_path} 失败: {str(e)}") raise @@ -198,8 +110,5 @@ class InitializeData: """ 执行完整初始化流程 """ - logger.info("开始执行数据库初始化流程") - await self.__init_model(db) - # 刷新session以确保数据可见性 - await db.flush() - logger.info("数据库初始化流程完成") \ No newline at end of file + await self.__init_data(db) + diff --git a/frontend/.env.development b/frontend/.env.development index 4b3269b5..e9b46cf7 100644 --- a/frontend/.env.development +++ b/frontend/.env.development @@ -6,7 +6,7 @@ VITE_APP_TITLE=fastapiadmin # 网络请求公用地址 -VITE_API_BASE_URL=http://localhost:8001 +VITE_API_BASE_URL=http://127.0.0.1:8001 # VITE_API_BASE_URL=https://service.fastapiadmin.com # 代理前缀 diff --git a/frontend/.env.production b/frontend/.env.production index da61f5ae..efc6a83e 100644 --- a/frontend/.env.production +++ b/frontend/.env.production @@ -5,7 +5,7 @@ VITE_APP_ENV=production VITE_APP_TITLE=fastapiadmin # 网络请求公用地址 -VITE_API_BASE_URL=http://localhost:8001 +VITE_API_BASE_URL=https://service.fastapiadmin.com # 代理前缀 VITE_APP_BASE_API=/api/v1