diff --git a/backend/app/api/v1/module_application/myapp/controller.py b/backend/app/api/v1/module_application/myapp/controller.py index 15f24d3b..14706ed7 100644 --- a/backend/app/api/v1/module_application/myapp/controller.py +++ b/backend/app/api/v1/module_application/myapp/controller.py @@ -37,7 +37,7 @@ async def get_obj_list_controller( 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) + result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size) logger.info(f"查询应用列表成功") return SuccessResponse(data=result_dict, msg="查询应用列表成功") diff --git a/backend/app/api/v1/module_example/demo/controller.py b/backend/app/api/v1/module_example/demo/controller.py index ca088874..6953e8a0 100644 --- a/backend/app/api/v1/module_example/demo/controller.py +++ b/backend/app/api/v1/module_example/demo/controller.py @@ -39,7 +39,7 @@ async def get_obj_list_controller( auth: AuthSchema = Depends(AuthPermission(permissions=["demo:example:query"])) ) -> JSONResponse: result_dict_list = await DemoService.get_demo_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) + result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size) logger.info(f"查询示例列表成功") return SuccessResponse(data=result_dict, msg="查询公告列表成功") diff --git a/backend/app/api/v1/module_generator/gencode/crud.py b/backend/app/api/v1/module_generator/gencode/crud.py index e476a5fe..2b6990af 100644 --- a/backend/app/api/v1/module_generator/gencode/crud.py +++ b/backend/app/api/v1/module_generator/gencode/crud.py @@ -7,16 +7,16 @@ from sqlalchemy.orm import selectinload from sqlglot.expressions import Expression from typing import List -from .model import GenTable, GenTableColumn -from config.env import DataBaseConfig -from utils.page_util import PageUtil +from .model import GenTableModel, GenTableColumnModel +from app.config.setting import settings +from app.common.request import PaginationService from .schema import ( - GenTableBaseModel, - GenTableColumnBaseModel, - GenTableColumnModel, - GenTableModel, - GenTablePageQueryModel, + GenTableBaseSchema, + GenTableColumnBaseSchema, + GenTableColumnSchema, + GenTableSchema, ) +from .param import GenTableQueryParam, GenTableColumnBaseSchema class GenTableDao: @@ -36,7 +36,7 @@ class GenTableDao: gen_table_info = ( ( await db.execute( - select(GenTable).options(selectinload(GenTable.columns)).where(GenTable.table_id == table_id) + select(GenTableModel).options(selectinload(GenTableModel.columns)).where(GenTableModel.table_id == table_id) ) ) .scalars() @@ -57,7 +57,7 @@ class GenTableDao: gen_table_info = ( ( await db.execute( - select(GenTable).options(selectinload(GenTable.columns)).where(GenTable.table_name == table_name) + select(GenTableModel).options(selectinload(GenTableModel.columns)).where(GenTableModel.table_name == table_name) ) ) .scalars() @@ -74,7 +74,7 @@ class GenTableDao: :param db: orm对象 :return: 所有业务表信息 """ - gen_table_all = (await db.execute(select(GenTable).options(selectinload(GenTable.columns)))).scalars().all() + gen_table_all = (await db.execute(select(GenTableModel).options(selectinload(GenTableModel.columns)))).scalars().all() return gen_table_all @@ -88,11 +88,11 @@ class GenTableDao: :return: """ for sql_statement in sql_statements: - sql = sql_statement.sql(dialect=DataBaseConfig.sqlglot_parse_dialect) + sql = sql_statement.sql(dialect=settings.DATABASE_TYPE) await db.execute(text(sql)) @classmethod - async def get_gen_table_list(cls, db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False): + async def get_gen_table_list(cls, db: AsyncSession, query_object: GenTableQueryParam, is_page: bool = False): """ 根据查询参数获取代码生成业务表列表信息 @@ -102,16 +102,16 @@ class GenTableDao: :return: 代码生成业务表列表信息对象 """ query = ( - select(GenTable) - .options(selectinload(GenTable.columns)) + select(GenTableModel) + .options(selectinload(GenTableModel.columns)) .where( - func.lower(GenTable.table_name).like(f'%{query_object.table_name.lower()}%') + func.lower(GenTableModel.table_name).like(f'%{query_object.table_name.lower()}%') if query_object.table_name else True, - func.lower(GenTable.table_comment).like(f'%{query_object.table_comment.lower()}%') + func.lower(GenTableModel.table_comment).like(f'%{query_object.table_comment.lower()}%') if query_object.table_comment else True, - GenTable.create_time.between( + GenTableModel.create_time.between( datetime.combine(datetime.strptime(query_object.begin_time, '%Y-%m-%d'), time(00, 00, 00)), datetime.combine(datetime.strptime(query_object.end_time, '%Y-%m-%d'), time(23, 59, 59)), ) @@ -120,12 +120,12 @@ class GenTableDao: ) .distinct() ) - gen_table_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + gen_table_list = await PaginationService.paginate(db, query, query_object.page_no, query_object.page_size, is_page) return gen_table_list @classmethod - async def get_gen_db_table_list(cls, db: AsyncSession, query_object: GenTablePageQueryModel, is_page: bool = False): + async def get_gen_db_table_list(cls, db: AsyncSession, query_object: GenTableQueryParam, is_page: bool = False): """ 根据查询参数获取数据库列表信息 @@ -134,7 +134,7 @@ class GenTableDao: :param is_page: 是否开启分页 :return: 数据库列表信息对象 """ - if DataBaseConfig.db_type == 'postgresql': + if settings.DATABASE_TYPE == 'postgresql': query_sql = """ table_name as table_name, table_comment as table_comment, @@ -166,12 +166,12 @@ class GenTableDao: if query_object.table_comment: query_sql += """and lower(table_comment) like lower(concat('%', :table_comment, '%'))""" if query_object.begin_time: - if DataBaseConfig.db_type == 'postgresql': + if settings.DATABASE_TYPE == 'postgresql': query_sql += """and create_time::date >= to_date(:begin_time, 'yyyy-MM-dd')""" else: query_sql += """and date_format(create_time, '%Y%m%d') >= date_format(:begin_time, '%Y%m%d')""" if query_object.end_time: - if DataBaseConfig.db_type == 'postgresql': + if settings.DATABASE_TYPE == 'postgresql': query_sql += """and create_time::date <= to_date(:end_time, 'yyyy-MM-dd')""" else: query_sql += """and date_format(create_time, '%Y%m%d') >= date_format(:end_time, '%Y%m%d')""" @@ -184,7 +184,7 @@ class GenTableDao: } ) ) - gen_db_table_list = await PageUtil.paginate(db, query, query_object.page_num, query_object.page_size, is_page) + gen_db_table_list = await PaginationService.paginate(db, query, query_object.page_no, query_object.page_size, is_page) return gen_db_table_list @@ -197,7 +197,7 @@ class GenTableDao: :param table_names: 业务表名称组 :return: 数据库列表信息对象 """ - if DataBaseConfig.db_type == 'postgresql': + if settings.DATABASE_TYPE == 'postgresql': query_sql = """ select table_name as table_name, @@ -240,7 +240,7 @@ class GenTableDao: :param gen_table: 业务表对象 :return: """ - db_gen_table = GenTable(**GenTableBaseModel(**gen_table.model_dump(by_alias=True)).model_dump()) + db_gen_table = GenTableModel(**GenTableBaseSchema(**gen_table.model_dump(by_alias=True)).model_dump()) db.add(db_gen_table) await db.flush() @@ -255,7 +255,7 @@ class GenTableDao: :param gen_table: 需要更新的业务表字典 :return: """ - await db.execute(update(GenTable), [GenTableBaseModel(**gen_table).model_dump()]) + await db.execute(update(GenTableModel), [GenTableBaseSchema(**gen_table).model_dump()]) @classmethod async def delete_gen_table_dao(cls, db: AsyncSession, gen_table: GenTableModel): @@ -266,7 +266,7 @@ class GenTableDao: :param gen_table: 业务表对象 :return: """ - await db.execute(delete(GenTable).where(GenTable.table_id.in_([gen_table.table_id]))) + await db.execute(delete(GenTableModel).where(GenTableModel.table_id.in_([gen_table.table_id]))) class GenTableColumnDao: @@ -286,7 +286,7 @@ class GenTableColumnDao: gen_table_column_list = ( ( await db.execute( - select(GenTableColumn).where(GenTableColumn.table_id == table_id).order_by(GenTableColumn.sort) + select(GenTableColumnModel).where(GenTableColumnModel.table_id == table_id).order_by(GenTableColumnModel.sort) ) ) .scalars() @@ -304,7 +304,7 @@ class GenTableColumnDao: :param table_name: 业务表名称 :return: 业务表字段列表信息对象 """ - if DataBaseConfig.db_type == 'postgresql': + if settings.DATABASE_TYPE == 'postgresql': query_sql = """ select column_name, is_required, is_pk, sort, column_comment, is_increment, column_type @@ -354,8 +354,8 @@ class GenTableColumnDao: :param gen_table_column: 岗位对象 :return: """ - db_gen_table_column = GenTableColumn( - **GenTableColumnBaseModel(**gen_table_column.model_dump(by_alias=True)).model_dump() + db_gen_table_column = GenTableColumnModel( + **GenTableColumnBaseSchema(**gen_table_column.model_dump(by_alias=True)).model_dump() ) db.add(db_gen_table_column) await db.flush() @@ -371,7 +371,7 @@ class GenTableColumnDao: :param gen_table_column: 需要更新的业务表字段字典 :return: """ - await db.execute(update(GenTableColumn), [GenTableColumnBaseModel(**gen_table_column).model_dump()]) + await db.execute(update(GenTableColumnModel), [GenTableColumnBaseSchema(**gen_table_column).model_dump()]) @classmethod async def delete_gen_table_column_by_table_id_dao(cls, db: AsyncSession, gen_table_column: GenTableColumnModel): @@ -382,7 +382,7 @@ class GenTableColumnDao: :param gen_table_column: 业务表字段对象 :return: """ - await db.execute(delete(GenTableColumn).where(GenTableColumn.table_id.in_([gen_table_column.table_id]))) + await db.execute(delete(GenTableColumnModel).where(GenTableColumnModel.table_id.in_([gen_table_column.table_id]))) @classmethod async def delete_gen_table_column_by_column_id_dao(cls, db: AsyncSession, gen_table_column: GenTableColumnModel): @@ -393,4 +393,4 @@ class GenTableColumnDao: :param post: 业务表字段对象 :return: """ - await db.execute(delete(GenTableColumn).where(GenTableColumn.column_id.in_([gen_table_column.column_id]))) + await db.execute(delete(GenTableColumnModel).where(GenTableColumnModel.column_id.in_([gen_table_column.column_id]))) diff --git a/backend/app/api/v1/module_generator/gencode/model.py b/backend/app/api/v1/module_generator/gencode/model.py index cc69a4dd..5a544a7e 100644 --- a/backend/app/api/v1/module_generator/gencode/model.py +++ b/backend/app/api/v1/module_generator/gencode/model.py @@ -45,7 +45,6 @@ class GenTableColumnModel(CreatorMixin): """ 代码生成业务表字段 """ - __tablename__ = 'gen_table_column' __table_args__ = ({'comment': '代码生成业务表字段'}) diff --git a/backend/app/api/v1/module_generator/gencode/param.py b/backend/app/api/v1/module_generator/gencode/param.py index 7f30a0bf..a6bcfa1a 100644 --- a/backend/app/api/v1/module_generator/gencode/param.py +++ b/backend/app/api/v1/module_generator/gencode/param.py @@ -1,20 +1,64 @@ -# -*- coding:utf-8 -*- +# -*- coding: utf-8 -*- -@as_query -class GenTablePageQuerySchema(GenTableQuerySchema): - """ - 代码生成业务表分页查询模型 - """ +from datetime import datetime +from typing import Optional +from fastapi import Query - page_num: int = Field(default=1, description='当前页码') - page_size: int = Field(default=10, description='每页记录数') +from app.core.validator import DateTimeStr +from app.common.request import PageResultSchema +from .schema import GenTableBaseSchema, GenTableColumnBaseSchema -@as_query -class GenTableColumnPageQuerySchema(GenTableColumnQuerySchema): - """ - 代码生成业务表字段分页查询模型 - """ +class GenTableQueryParam(PageResultSchema, GenTableBaseSchema): + """数据库表查询参数""" + + def __init__( + self, + name: Optional[str] = Query(None, description="名称"), + status: Optional[bool] = Query(None, description="是否启用"), + creator: Optional[int] = Query(None, description="创建人"), + start_time: Optional[DateTimeStr] = Query(None, description="开始时间", example="2023-01-01 00:00:00"), + end_time: Optional[DateTimeStr] = Query(None, description="结束时间", example="2023-12-31 23:59:59"), + ) -> None: + super().__init__() + + # 模糊查询字段 + self.name = ("like", name) + + # 精确查询字段 + self.creator_id = creator + self.status = status + + # 时间范围查询 + if start_time and end_time: + start_datetime = datetime.strptime(str(start_time), '%Y-%m-%d %H:%M:%S') + end_datetime = datetime.strptime(str(end_time), '%Y-%m-%d %H:%M:%S') + self.created_at = ("between", (start_datetime, end_datetime)) + + +class GenTableColumnQueryParam(PageResultSchema, GenTableColumnBaseSchema): + """数据库表字段查询参数""" + + def __init__( + self, + name: Optional[str] = Query(None, description="名称"), + status: Optional[bool] = Query(None, description="是否启用"), + creator: Optional[int] = Query(None, description="创建人"), + start_time: Optional[DateTimeStr] = Query(None, description="开始时间", example="2023-01-01 00:00:00"), + end_time: Optional[DateTimeStr] = Query(None, description="结束时间", example="2023-12-31 23:59:59"), + ) -> None: + super().__init__() + + # 模糊查询字段 + self.name = ("like", name) + + # 精确查询字段 + self.creator_id = creator + self.status = status + + # 时间范围查询 + if start_time and end_time: + start_datetime = datetime.strptime(str(start_time), '%Y-%m-%d %H:%M:%S') + end_datetime = datetime.strptime(str(end_time), '%Y-%m-%d %H:%M:%S') + self.created_at = ("between", (start_datetime, end_datetime)) - page_num: int = Field(default=1, description='当前页码') - page_size: int = Field(default=10, description='每页记录数') diff --git a/backend/app/api/v1/module_generator/gencode/schema.py b/backend/app/api/v1/module_generator/gencode/schema.py index 0f0f1d7d..436c1e72 100644 --- a/backend/app/api/v1/module_generator/gencode/schema.py +++ b/backend/app/api/v1/module_generator/gencode/schema.py @@ -6,7 +6,6 @@ from pydantic import BaseModel, ConfigDict, Field, model_validator from pydantic.alias_generators import to_camel from pydantic_validation_decorator import NotBlank -from module_admin.annotation.pydantic_annotation import as_query from utils.string_util import StringUtil from app.common.constant import GenConstant @@ -89,9 +88,9 @@ class GenTableSchema(GenTableBaseSchema): 代码生成业务表模型 """ - pk_column: Optional['GenTableColumnModel'] = Field(default=None, description='主键信息') - sub_table: Optional['GenTableModel'] = Field(default=None, description='子表信息') - columns: Optional[List['GenTableColumnModel']] = Field(default=None, description='表列信息') + pk_column: Optional['GenTableColumnSchema'] = Field(default=None, description='主键信息') + sub_table: Optional['GenTableSchema'] = Field(default=None, description='子表信息') + columns: Optional[List['GenTableColumnSchema']] = Field(default=None, description='表列信息') tree_code: Optional[str] = Field(default=None, description='树编码字段') tree_parent_code: Optional[str] = Field(default=None, description='树父编码字段') tree_name: Optional[str] = Field(default=None, description='树名称字段') @@ -102,7 +101,7 @@ class GenTableSchema(GenTableBaseSchema): crud: Optional[bool] = Field(default=None, description='是否为单表') @model_validator(mode='after') - def check_some_is(self) -> 'GenTableModel': + def check_some_is(self) -> 'GenTableSchema': self.sub = True if self.tpl_category and self.tpl_category == GenConstant.TPL_SUB else False self.tree = True if self.tpl_category and self.tpl_category == GenConstant.TPL_TREE else False self.crud = True if self.tpl_category and self.tpl_category == GenConstant.TPL_CRUD else False @@ -114,7 +113,7 @@ class EditGenTableSchema(GenTableSchema): 修改代码生成业务表模型 """ - params: Optional['GenTableParamsModel'] = Field(default=None, description='业务表参数') + params: Optional['GenTableParamsSchema'] = Field(default=None, description='业务表参数') class GenTableParamsSchema(BaseModel): @@ -130,15 +129,6 @@ class GenTableParamsSchema(BaseModel): parent_menu_id: Optional[int] = Field(default=None, description='上级菜单ID字段') -class GenTableQuerySchema(GenTableBaseSchema): - """ - 代码生成业务表不分页查询模型 - """ - - begin_time: Optional[str] = Field(default=None, description='开始时间') - end_time: Optional[str] = Field(default=None, description='结束时间') - - class DeleteGenTableSchema(BaseModel): """ 删除代码生成业务表模型 @@ -206,7 +196,7 @@ class GenTableColumnSchema(GenTableColumnBaseSchema): usable_column: Optional[bool] = Field(default=None, description='是否为基类字段白名单') @model_validator(mode='after') - def check_some_is(self) -> 'GenTableModel': + def check_some_is(self) -> 'GenTableSchema': self.cap_python_field = self.python_field[0].upper() + self.python_field[1:] if self.python_field else None self.pk = True if self.is_pk and self.is_pk == '1' else False self.increment = True if self.is_increment and self.is_increment == '1' else False @@ -227,15 +217,6 @@ class GenTableColumnSchema(GenTableColumnBaseSchema): return self -class GenTableColumnQuerySchema(GenTableColumnBaseSchema): - """ - 代码生成业务表字段不分页查询模型 - """ - - begin_time: Optional[str] = Field(default=None, description='开始时间') - end_time: Optional[str] = Field(default=None, description='结束时间') - - class DeleteGenTableColumnSchema(BaseModel): """ 删除代码生成业务表字段模型 diff --git a/backend/app/api/v1/module_monitor/job/controller.py b/backend/app/api/v1/module_monitor/job/controller.py index c2faf5fa..06e4ea72 100644 --- a/backend/app/api/v1/module_monitor/job/controller.py +++ b/backend/app/api/v1/module_monitor/job/controller.py @@ -38,7 +38,7 @@ async def get_obj_list_controller( auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:job:query"])) ) -> JSONResponse: result_dict_list = await JobService.get_job_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) + result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size) logger.info(f"查询定时任务列表成功") return SuccessResponse(data=result_dict, msg="查询定时任务列表成功") @@ -148,7 +148,7 @@ async def get_job_log_list_controller( auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:job:query"])) ) -> JSONResponse: result_dict_list = await JobLogService.get_job_log_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) + result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size) logger.info(f"查询定时任务日志列表成功") return SuccessResponse(data=result_dict, msg="查询定时任务日志列表成功") diff --git a/backend/app/api/v1/module_monitor/online/controller.py b/backend/app/api/v1/module_monitor/online/controller.py index ac0e4b7b..7acbb949 100644 --- a/backend/app/api/v1/module_monitor/online/controller.py +++ b/backend/app/api/v1/module_monitor/online/controller.py @@ -30,7 +30,7 @@ async def get_online_list_controller( )->JSONResponse: # 获取全量数据 result_dict_list = await OnlineService.get_online_list_service(redis=redis, search=search) - result_dict = await PaginationService.get_page_obj(data_list= result_dict_list, page_no= paging_query.page_no, page_size = paging_query.page_size) + result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= paging_query.page_no, page_size = paging_query.page_size) logger.info('获取成功') return SuccessResponse(data=result_dict,msg='获取成功') diff --git a/backend/app/api/v1/module_system/dict/controller.py b/backend/app/api/v1/module_system/dict/controller.py index 50459c42..348174c4 100644 --- a/backend/app/api/v1/module_system/dict/controller.py +++ b/backend/app/api/v1/module_system/dict/controller.py @@ -42,7 +42,7 @@ async def get_type_list_controller( auth: AuthSchema = Depends(AuthPermission(permissions=["system:dict_type:query"])) ) -> JSONResponse: result_dict_list = await DictTypeService.get_obj_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) + result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size) logger.info(f"查询字典类型列表成功") return SuccessResponse(data=result_dict, msg="查询字典类型列表成功") @@ -128,7 +128,7 @@ async def get_data_list_controller( auth: AuthSchema = Depends(AuthPermission(permissions=["system:dict_data:query"])) ) -> JSONResponse: result_dict_list = await DictDataService.get_obj_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) + result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size) logger.info(f"查询字典数据列表成功") return SuccessResponse(data=result_dict, msg="查询字典数据列表成功") diff --git a/backend/app/api/v1/module_system/log/controller.py b/backend/app/api/v1/module_system/log/controller.py index 6993e107..ec3cc857 100644 --- a/backend/app/api/v1/module_system/log/controller.py +++ b/backend/app/api/v1/module_system/log/controller.py @@ -26,7 +26,7 @@ async def get_obj_list_controller( ) -> JSONResponse: """ 查询日志 """ result_dict_list = await OperationLogService.get_log_list_service(search=search, auth=auth, 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) + result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size) logger.info(f"查询日志成功") return SuccessResponse(data=result_dict, msg="查询日志成功") diff --git a/backend/app/api/v1/module_system/notice/controller.py b/backend/app/api/v1/module_system/notice/controller.py index 1521d3e4..604364f7 100644 --- a/backend/app/api/v1/module_system/notice/controller.py +++ b/backend/app/api/v1/module_system/notice/controller.py @@ -38,7 +38,7 @@ async def get_obj_list_controller( auth: AuthSchema = Depends(AuthPermission(permissions=["system:notice:query"])) ) -> JSONResponse: result_dict_list = await NoticeService.get_notice_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) + result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size) logger.info(f"查询公告列表成功") return SuccessResponse(data=result_dict, msg="查询公告列表成功") @@ -103,6 +103,6 @@ async def get_obj_list_available_controller( auth: AuthSchema = Depends(get_current_user) ) -> JSONResponse: result_dict_list = await NoticeService.get_notice_list_available_service(auth=auth) - result_dict = await PaginationService.get_page_obj(data_list= result_dict_list) + result_dict = await PaginationService.paginate(data_list= result_dict_list) logger.info(f"查询已启用公告列表成功") return SuccessResponse(data=result_dict, msg="查询已启用公告列表成功") diff --git a/backend/app/api/v1/module_system/params/controller.py b/backend/app/api/v1/module_system/params/controller.py index 76f6d489..31e83f36 100644 --- a/backend/app/api/v1/module_system/params/controller.py +++ b/backend/app/api/v1/module_system/params/controller.py @@ -57,7 +57,7 @@ async def get_obj_list_controller( search: ParamsQueryParam = Depends(), ) -> JSONResponse: result_dict_list = await ParamsService.get_obj_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) + result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size) logger.info(f"获取参数列表成功") return SuccessResponse(data=result_dict, msg="查询参数列表成功") diff --git a/backend/app/api/v1/module_system/position/controller.py b/backend/app/api/v1/module_system/position/controller.py index 02b6f88e..52e25eea 100644 --- a/backend/app/api/v1/module_system/position/controller.py +++ b/backend/app/api/v1/module_system/position/controller.py @@ -30,7 +30,7 @@ async def get_obj_list_controller( auth: AuthSchema = Depends(AuthPermission(permissions=["system:position:query"])), ) -> JSONResponse: result_dict_list = await PositionService.get_position_list_service(search=search, auth=auth, 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) + result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size) logger.info(f"查询岗位列表成功") return SuccessResponse(data=result_dict, msg="查询岗位列表成功") diff --git a/backend/app/api/v1/module_system/role/controller.py b/backend/app/api/v1/module_system/role/controller.py index 2326b55b..2e0a0f7b 100644 --- a/backend/app/api/v1/module_system/role/controller.py +++ b/backend/app/api/v1/module_system/role/controller.py @@ -31,7 +31,7 @@ async def get_obj_list_controller( auth: AuthSchema = Depends(AuthPermission(permissions=["system:role:query"])), ) -> JSONResponse: result_dict_list = await RoleService.get_role_list_service(search=search, auth=auth, 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) + result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size) logger.info(f"查询角色成功") return SuccessResponse(data=result_dict, msg="查询角色成功") diff --git a/backend/app/api/v1/module_system/ticket/controller.py b/backend/app/api/v1/module_system/ticket/controller.py index d0117156..618ed208 100644 --- a/backend/app/api/v1/module_system/ticket/controller.py +++ b/backend/app/api/v1/module_system/ticket/controller.py @@ -35,7 +35,7 @@ async def get_ticket_list_controller( auth: AuthSchema = Depends(AuthPermission(permissions=["system:ticket:query"])) ) -> JSONResponse: result_dict_list = await TicketService.get_ticket_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) + result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size) logger.info("查询工单列表成功") return SuccessResponse(data=result_dict, msg="查询工单列表成功") diff --git a/backend/app/api/v1/module_system/user/controller.py b/backend/app/api/v1/module_system/user/controller.py index 81dd423c..5337cd83 100644 --- a/backend/app/api/v1/module_system/user/controller.py +++ b/backend/app/api/v1/module_system/user/controller.py @@ -106,7 +106,7 @@ async def get_obj_list_controller( auth: AuthSchema = Depends(AuthPermission(permissions=["system:user:query"])), ) -> JSONResponse: result_dict_list = await UserService.get_user_list_service(search=search, auth=auth, 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) + result_dict = await PaginationService.paginate(data_list= result_dict_list, page_no= page.page_no, page_size = page.page_size) logger.info(f"查询用户成功") return SuccessResponse(data=result_dict, msg="查询用户成功") diff --git a/backend/app/api/v1/module_system/user/service.py b/backend/app/api/v1/module_system/user/service.py index 849ced7c..efa2a766 100644 --- a/backend/app/api/v1/module_system/user/service.py +++ b/backend/app/api/v1/module_system/user/service.py @@ -32,7 +32,6 @@ from .schema import ( ) - class UserService: """用户模块服务层""" diff --git a/backend/app/api/v1/module_system/version/controller.py b/backend/app/api/v1/module_system/version/controller.py index 9ef9e3a4..021554ab 100644 --- a/backend/app/api/v1/module_system/version/controller.py +++ b/backend/app/api/v1/module_system/version/controller.py @@ -35,7 +35,7 @@ async def get_version_list_controller( auth: AuthSchema = Depends(AuthPermission(permissions=["system:version:query"])) ) -> JSONResponse: result_dict_list = await VersionService.get_version_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) + result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size) logger.info("查询版本列表成功") return SuccessResponse(data=result_dict, msg="查询版本列表成功") diff --git a/backend/app/common/request.py b/backend/app/common/request.py index 2e94e972..0d45add1 100644 --- a/backend/app/common/request.py +++ b/backend/app/common/request.py @@ -23,7 +23,7 @@ class PaginationService: """分页服务类""" @staticmethod - async def get_page_obj(data_list: List[Any], page_no: Optional[int] = None, page_size: Optional[int] = None) -> Dict[str, Any]: + async def paginate(data_list: List[Any], page_no: Optional[int] = None, page_size: Optional[int] = None) -> Dict[str, Any]: """ 输入数据列表data_list和分页信息,返回分页或非分页数据列表结果。 如果未传入page_no和page_size,则返回全部数据。 diff --git a/backend/app/config/setting.py b/backend/app/config/setting.py index 11fb654d..34c6d40c 100755 --- a/backend/app/config/setting.py +++ b/backend/app/config/setting.py @@ -275,7 +275,6 @@ class Settings(BaseSettings): "app.core.middlewares.CustomCORSMiddleware" if self.CORS_ORIGIN_ENABLE else None, "app.core.middlewares.RequestLogMiddleware" if self.OPERATION_LOG_RECORD else None, "app.core.middlewares.CustomGZipMiddleware" if self.GZIP_ENABLE else None, - "app.core.middlewares.DemoEnvMiddleware" if self.DEMO_ENABLE else None, ] return MIDDLEWARES diff --git a/backend/app/core/base_crud.py b/backend/app/core/base_crud.py index 5ec679c3..4b65f6ab 100644 --- a/backend/app/core/base_crud.py +++ b/backend/app/core/base_crud.py @@ -3,7 +3,7 @@ from pydantic import BaseModel from typing import TypeVar, Sequence, Generic, Dict, Any, List, Union, Optional from sqlalchemy.sql.elements import ColumnElement -from sqlalchemy.orm import selectinload, DeclarativeBase +from sqlalchemy.orm import Session, selectinload, DeclarativeBase from sqlalchemy.engine import Result from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import asc, func, select, delete, Select, desc, update, or_, and_ @@ -32,7 +32,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): """ self.model = model self.auth = auth - self.db: AsyncSession = auth.db + self.db: AsyncSession | Session | None = auth.db self.current_user = auth.user async def get(self, **kwargs) -> Optional[ModelType]: diff --git a/backend/app/core/dependencies.py b/backend/app/core/dependencies.py index 5ab4eaa9..20aa6b05 100644 --- a/backend/app/core/dependencies.py +++ b/backend/app/core/dependencies.py @@ -8,6 +8,7 @@ from fastapi import Depends, Request from motor.motor_asyncio import AsyncIOMotorDatabase from fastapi import Depends +from app.api.v1.module_system.user.schema import UserOutSchema from app.common.enums import RedisInitKeyConfig from app.core.exceptions import CustomException from app.core.database import session_connect @@ -95,7 +96,7 @@ async def get_current_user( if hasattr(user, 'positions'): user.positions = [pos for pos in user.positions if pos.status] - auth.user = user + auth.user = UserOutSchema.model_validate(user) return auth @@ -133,7 +134,7 @@ class AuthPermission: auth.check_data_scope = self.check_data_scope # 超级管理员直接通过 - if auth.user.is_superuser: + if auth.user and auth.user.is_superuser: return auth # 无需验证权限 diff --git a/backend/app/core/exceptions.py b/backend/app/core/exceptions.py index 46b18a86..4c3fb660 100644 --- a/backend/app/core/exceptions.py +++ b/backend/app/core/exceptions.py @@ -1,14 +1,12 @@ # -*- coding: utf-8 -*- -from typing import Any, Optional, List, Tuple, Union +from typing import Any, Optional from fastapi import Request, status from fastapi.exceptions import RequestValidationError, ResponseValidationError from pydantic_validation_decorator import FieldValidationError from starlette.responses import JSONResponse from starlette.exceptions import HTTPException from sqlalchemy.exc import SQLAlchemyError -from pydantic_core import ErrorDetails -from pydantic import ValidationError from app.common.constant import RET from app.common.response import ErrorResponse @@ -20,7 +18,7 @@ class CustomException(Exception): def __init__( self, - msg: Optional[str] = RET.EXCEPTION.msg, + msg: str = RET.EXCEPTION.msg, code: int = RET.EXCEPTION.code, status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR, data: Optional[Any] = None, @@ -59,15 +57,23 @@ async def HttpExceptionHandler(request: Request, exc: HTTPException) -> JSONResp async def ValidationExceptionHandler(request: Request, exc: RequestValidationError) -> JSONResponse: """请求参数验证异常处理器""" - msg:List[ErrorDetails] = custom_convert_errors(exc) - + error_mapping = { + "Field required": "请求失败,缺少必填项!", + "value is not a valid list": "类型错误,提交参数应该为列表!", + "value is not a valid int": "类型错误,提交参数应该为整数!", + "value could not be parsed to a boolean": "类型错误,提交参数应该为布尔值!", + "Input should be a valid list": "类型错误,输入应该是一个有效的列表!" + } + msg = error_mapping.get(exc.errors()[0].get('msg'), exc.errors()[0].get('msg')) logger.error(f"请求地址: {request.url}, 错误信息: {msg}, 错误详情: {exc}") return ErrorResponse(msg=str(msg), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, data=exc.body) + async def ResponseValidationHandle(request: Request, exc: ResponseValidationError) -> JSONResponse: logger.error(f"请求地址: {request.url}, 错误详情: {exc}") return ErrorResponse(msg=str(exc), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, data=exc.body) + async def SQLAlchemyExceptionHandler(request: Request, exc: SQLAlchemyError) -> JSONResponse: """数据库异常处理器""" error_msg = f'数据库操作失败: {exc}' @@ -91,75 +97,3 @@ async def AllExceptionHandler(request: Request, exc: Exception) -> JSONResponse: """全局异常处理器""" logger.error(f"请求地址: {request.url}, 错误详情: {exc}") return ErrorResponse(msg='服务器内部错误', status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, data=str(exc)) - -ERROR_MAPPING = { - "missing": "请求失败,缺少必填项!", - # 字符串 - "string_pattern_mismatch": "值错误,提交参数不满足正则表达式{pattern}!", - "string_too_long": "值错误,提交参数长度必须小于等于{max_length}!", - "string_too_short": "值错误,提交参数长度必须大于等于{min_length}!", - "string_type": "类型错误,提交参数应该为字符串!", - # 列表 - "list_type": "类型错误,提交参数应该为列表!", - # 字典 - "dict_type": "类型错误,提交参数应该为字典!", - # 集合 - "set_type": "类型错误,提交参数应该为集合!", - # 元组 - "tuple_type": "类型错误,提交参数应该为元组!", - # 元素数量 - "too_long": "数量错误,提交参数的元素数量必须小于等于{max_length}!", - "too_short": "数量错误,提交参数的元素数量必须大于等于{min_length}!", - # 大小比值 - "less_than_equal": "值错误,提交参数必须小于等于{le}!", - "greater_than_equal": "值错误,提交参数必须大于等于{ge}!", - "less_than": "值错误,提交参数必须小于{lt}!", - "greater_than": "值错误,提交参数必须大于{gt}!", - # 布尔值 - "bool_type": "类型错误,提交参数应该为布尔值!", - "bool_parsing": "类型错误,提交参数应该为布尔值!", - # 字节 - "bytes_type": "类型错误,提交参数应该为字节!", - "bytes_too_long": "值错误,提交参数长度必须小于等于{max_length}!", - "bytes_too_short": "值错误,提交参数长度必须大于等于{min_length}!", - # 整数 - "int_parsing": "类型错误,提交参数应该为整数!", - "int_type": "类型错误,提交参数应该为整数!", - # 浮点数 - "float_parsing": "类型错误,提交参数应该为浮点数!", - "float_type": "类型错误,提交参数应该为浮点数!", - # 日期时间 - "date_parsing": "类型错误,提交参数应该为日期!", - "date_type": "类型错误,提交参数应该为日期!", - "time_parsing": "类型错误,提交参数应该为时间!", - "time_type": "类型错误,提交参数应该为时间!", - # 其他 - "literal_error": "值错误,提交参数值在为{expected}中一个!", - "extra_forbidden": "值错误,提交参数值不在允许范围内!", -} - -def custom_convert_errors(e: ValidationError | RequestValidationError) -> List[ErrorDetails]: - new_errors: List[ErrorDetails] = [] - for error in e.errors(): - error['loc'] = loc_to_dot_sep(error['loc']) - custom_message = ERROR_MAPPING.get(error['type']) - if custom_message: - ctx = error.get('ctx') - error['msg'] = ( - custom_message.format(**ctx) if ctx else custom_message - ) - new_errors.append(error) - return new_errors - -def loc_to_dot_sep(loc: Tuple[Union[str, int], ...]) -> str: - path = '' - for i, x in enumerate(loc): - if isinstance(x, str): - if i > 0: - path += '.' - path += x - elif isinstance(x, int): - path += f'[{x}]' - else: - raise TypeError('Unexpected type') - return path diff --git a/backend/app/core/logger.py b/backend/app/core/logger.py index a990ea13..693ecca5 100644 --- a/backend/app/core/logger.py +++ b/backend/app/core/logger.py @@ -8,6 +8,7 @@ import logging from logging.handlers import TimedRotatingFileHandler from typing import Optional, Dict, Any from pathlib import Path +import typing from app.config.setting import settings @@ -29,7 +30,7 @@ class CustomTimedRotatingFileHandler(TimedRotatingFileHandler): # 使用流上下文管理确保资源正确释放 if self.stream: self.stream.close() - self.stream = None + self.stream = None # type: ignore try: # 计算轮换时间(使用缓存避免重复计算) diff --git a/backend/app/core/middlewares.py b/backend/app/core/middlewares.py index 9dd0ce9a..440103cb 100644 --- a/backend/app/core/middlewares.py +++ b/backend/app/core/middlewares.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- import time -from typing import Dict, List, Union +from typing import Any from starlette.middleware.cors import CORSMiddleware from starlette.types import ASGIApp from starlette.requests import Request from starlette.middleware.gzip import GZipMiddleware -from starlette.middleware.base import Response, BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.responses import Response +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint from app.common.response import ErrorResponse from app.config.setting import settings @@ -17,7 +18,7 @@ from app.core.exceptions import CustomException class CustomCORSMiddleware(CORSMiddleware): """CORS跨域中间件""" def __init__(self, app: ASGIApp) -> None: - CORSMiddlewareConfig: Dict[str, Union[List[str], bool]] = { + CORSMiddlewareConfig: dict[str, Any] = { "allow_origins": settings.ALLOW_ORIGINS, "allow_methods": settings.ALLOW_METHODS, "allow_headers": settings.ALLOW_HEADERS, @@ -38,65 +39,59 @@ class RequestLogMiddleware(BaseHTTPMiddleware): ) -> Response: start_time = time.time() - logger.info( - f"请求来源: {request.client.host}, " - f"请求方法: {request.method}, " - f"请求路径: {request.url.path}, " - f"客户端IP: {request.client.host}" - ) - + # 构建请求日志信息 + request_info = f"请求方法: {request.method}, 请求路径: {request.url.path}" + if request.client: + request_info = f"请求来源: {request.client.host}, {request_info}" + logger.info(request_info) + try: - response = await call_next(request) + + if settings.DEMO_ENABLE: + # 在演示环境中,只有白名单内的IP或路径才能执行非GET请求 + if request.method != "GET": + path = request.scope.get("path") + + request_ip = None + x_forwarded_for = request.headers.get('X-Forwarded-For') + if x_forwarded_for: + # 取第一个 IP 地址,通常为客户端真实 IP + request_ip = x_forwarded_for.split(',')[0].strip() + else: + # 若没有 X-Forwarded-For 头,则使用 request.client.host + request_ip = request.client.host if request.client else None + + # 检查IP是否在白名单,或路径是否在白名单,或用户是否在白名单 + if (request_ip in settings.DEMO_IP_WHITE_LIST) or (path in settings.DEMO_WHITE_LIST_PATH): + response = await call_next(request) + else: + # 非白名单用户,禁止操作 + return ErrorResponse(msg="演示环境,禁止操作") + else: + # GET请求在演示环境中总是允许的 + response = await call_next(request) + else: + # 非演示环境,正常处理请求 + response = await call_next(request) process_time = round(time.time() - start_time, 5) response.headers["X-Process-Time"] = str(process_time) - logger.info( - f"会话ID: {request.scope.get('session_id')}, " + # 构建响应日志信息 + session_id = request.scope.get('session_id') + content_length = response.headers.get('content-length', '0') + response_info = ( + f"会话ID: {session_id}, " f"响应状态: {response.status_code}, " - f"响应内容长度: {response.headers.get('content-length', '0')}, " + f"响应内容长度: {content_length}, " f"处理时间: {process_time}s" ) + logger.info(response_info) return response except CustomException as e: logger.error(f"系统异常: {str(e)}") - return ErrorResponse(msg=f"系统异常,请联系管理员: {str(e)}") - - -class DemoEnvMiddleware(BaseHTTPMiddleware): - """演示环境中间件""" - def __init__(self, app: ASGIApp) -> None: - super().__init__(app) - - async def dispatch( - self, request: Request, call_next: RequestResponseEndpoint - ) -> Response: - - if settings.DEMO_ENABLE and request.method != "GET": - path = request.scope.get("path") - - request_ip = None - x_forwarded_for = request.headers.get('X-Forwarded-For') - if x_forwarded_for: - # 取第一个 IP 地址,通常为客户端真实 IP - request_ip = x_forwarded_for.split(',')[0].strip() - else: - # 若没有 X-Forwarded-For 头,则使用 request.client.host - request_ip = request.client.host - - user_username = request.scope.get("user_username") - logger.error(f"用户名称: {user_username}") - - # 检查IP是否在白名单,或路径是否在白名单,或用户是否在白名单 - if (request_ip in settings.DEMO_IP_WHITE_LIST) or (path in settings.DEMO_WHITE_LIST_PATH): - return await call_next(request) - - else: - # 非白名单用户,禁止操作 - return ErrorResponse(msg="演示环境,禁止操作") - - return await call_next(request) + return ErrorResponse(msg=f"系统异常,请联系管理员", data=str(e)) class CustomGZipMiddleware(GZipMiddleware): diff --git a/backend/app/core/pydantic_annotation.py b/backend/app/core/pydantic_annotation.py new file mode 100644 index 00000000..0799f66a --- /dev/null +++ b/backend/app/core/pydantic_annotation.py @@ -0,0 +1,81 @@ +import inspect +from fastapi import Form, Query +from pydantic import BaseModel +from pydantic.fields import FieldInfo +from typing import Type + + +def as_query(cls: Type[BaseModel]): + """ + pydantic模型查询参数装饰器,将pydantic模型用于接收查询参数 + """ + new_parameters = [] + + for field_name, model_field in cls.model_fields.items(): + model_field: FieldInfo # type: ignore + + if not model_field.is_required(): + new_parameters.append( + inspect.Parameter( + model_field.alias, + inspect.Parameter.POSITIONAL_ONLY, + default=Query(default=model_field.default, description=model_field.description), + annotation=model_field.annotation, + ) + ) + else: + new_parameters.append( + inspect.Parameter( + model_field.alias, + inspect.Parameter.POSITIONAL_ONLY, + default=Query(..., description=model_field.description), + annotation=model_field.annotation, + ) + ) + + async def as_query_func(**data): + return cls(**data) + + sig = inspect.signature(as_query_func) + sig = sig.replace(parameters=new_parameters) + as_query_func.__signature__ = sig # type: ignore + setattr(cls, 'as_query', as_query_func) + return cls + + +def as_form(cls: Type[BaseModel]): + """ + pydantic模型表单参数装饰器,将pydantic模型用于接收表单参数 + """ + new_parameters = [] + + for field_name, model_field in cls.model_fields.items(): + model_field: FieldInfo # type: ignore + + if not model_field.is_required(): + new_parameters.append( + inspect.Parameter( + model_field.alias, + inspect.Parameter.POSITIONAL_ONLY, + default=Form(default=model_field.default, description=model_field.description), + annotation=model_field.annotation, + ) + ) + else: + new_parameters.append( + inspect.Parameter( + model_field.alias, + inspect.Parameter.POSITIONAL_ONLY, + default=Form(..., description=model_field.description), + annotation=model_field.annotation, + ) + ) + + async def as_form_func(**data): + return cls(**data) + + sig = inspect.signature(as_form_func) + sig = sig.replace(parameters=new_parameters) + as_form_func.__signature__ = sig # type: ignore + setattr(cls, 'as_form', as_form_func) + return cls diff --git a/backend/app/core/redis_crud.py b/backend/app/core/redis_crud.py index 03589b81..10b346df 100644 --- a/backend/app/core/redis_crud.py +++ b/backend/app/core/redis_crud.py @@ -148,7 +148,7 @@ class RedisCURD: async def hash_set(self, name: str, key: str, value: Any) -> bool: """设置哈希缓存""" try: - await self.redis.hset(name=name, key=key, value=value) + self.redis.hset(name=name, key=key, value=value) return True except Exception as e: logger.error(f"设置哈希缓存失败: {str(e)}") @@ -157,7 +157,7 @@ class RedisCURD: async def hash_get(self, name: str, keys: list[str]) -> Optional[list[Any]]: """获取哈希缓存""" try: - return await self.redis.hmget(name=name, keys=keys) + return self.redis.hmget(name=name, keys=keys) except Exception as e: logger.error(f"获取哈希缓存失败: {str(e)}") return None \ No newline at end of file diff --git a/backend/app/core/router_class.py b/backend/app/core/router_class.py index 6b8d69b0..efe2cb6a 100644 --- a/backend/app/core/router_class.py +++ b/backend/app/core/router_class.py @@ -35,7 +35,7 @@ class OperationLogRoute(APIRoute): return response if request.method not in settings.OPERATION_RECORD_METHOD: return response - route: APIRoute = request.scope.get("route") + route: APIRoute = request.scope.get("route", None) if route.name in settings.IGNORE_OPERATION_FUNCTION: return response @@ -66,7 +66,6 @@ class OperationLogRoute(APIRoute): oper_param['path_params'] = dict(path_params) payload = json.dumps(oper_param, ensure_ascii=False) - # payload = str(oper_param) # 日志表请求参数字段长度最大为2000,因此在此处判断长度 if len(payload) > 2000: @@ -89,19 +88,18 @@ class OperationLogRoute(APIRoute): request_ip = x_forwarded_for.split(',')[0].strip() else: # 若没有 X-Forwarded-For 头,则使用 request.client.host - request_ip = request.client.host + if request.client: + request_ip = request.client.host - login_location = await IpLocalUtil.get_ip_location(request_ip) + login_location = await IpLocalUtil.get_ip_location(request_ip) if request_ip else None # 判断请求是否来自api文档 - request_from_swagger = ( - request.headers.get('referer').endswith('docs') if request.headers.get('referer') else False - ) - request_from_redoc = ( - request.headers.get('referer').endswith('redoc') if request.headers.get('referer') else False - ) + referer = request.headers.get('referer') + request_from_swagger = referer and referer.endswith('docs') + request_from_redoc = referer and referer.endswith('redoc') if request_from_swagger or request_from_redoc: + # 如果请求来自api文档,则不记录日志 pass else: async with session_connect() as session: @@ -117,7 +115,7 @@ class OperationLogRoute(APIRoute): request_os = user_agent.os.family, request_browser = user_agent.browser.family, response_code = response.status_code, - response_json = response_data.decode(), + response_json = response_data.decode() if isinstance(response_data, (bytes, bytearray)) else str(response_data), process_time = process_time, description = route.summary, creator_id = current_user_id diff --git a/backend/app/scripts/data/system_menu.json b/backend/app/scripts/data/system_menu.json index 26e3d6fc..872892bd 100644 --- a/backend/app/scripts/data/system_menu.json +++ b/backend/app/scripts/data/system_menu.json @@ -2256,5 +2256,45 @@ "description": "前端构建" } ] + }, + { + "name": "流程管理", + "type": 1, + "icon": "el-icon-ShoppingBag", + "order": 10, + "permission": null, + "route_name": "Workflow", + "route_path": "/workflow", + "component_path": null, + "status": true, + "keep_alive": false, + "hidden": false, + "always_show": false, + "title": "流程管理", + "params": null, + "affix": false, + "redirect": "/workflow/operator", + "description": "流程管理", + "children": [ + { + "name": "我的流程", + "type": 2, + "icon": "el-icon-ShoppingBag", + "order": 1, + "permission": "workflow:operator:query", + "route_name": "Operator", + "route_path": "/workflow/operator", + "component_path": "workflow/operator/index", + "status": true, + "keep_alive": true, + "hidden": false, + "always_show": false, + "title": "我的流程", + "params": null, + "affix": false, + "redirect": null, + "description": "我的流程" + } + ] } ] \ No newline at end of file diff --git a/backend/app/scripts/data/system_role_menus.json b/backend/app/scripts/data/system_role_menus.json index 0e36a81a..82be4a96 100644 --- a/backend/app/scripts/data/system_role_menus.json +++ b/backend/app/scripts/data/system_role_menus.json @@ -430,5 +430,45 @@ { "role_id": 1, "menu_id": 108 + }, + { + "role_id": 1, + "menu_id": 109 + }, + { + "role_id": 1, + "menu_id": 110 + }, + { + "role_id": 1, + "menu_id": 111 + }, + { + "role_id": 1, + "menu_id": 112 + }, + { + "role_id": 1, + "menu_id": 113 + }, + { + "role_id": 1, + "menu_id": 114 + }, + { + "role_id": 1, + "menu_id": 115 + }, + { + "role_id": 1, + "menu_id": 116 + }, + { + "role_id": 1, + "menu_id": 117 + }, + { + "role_id": 1, + "menu_id": 118 } ] \ No newline at end of file diff --git a/backend/app/utils/build_tree.py b/backend/app/utils/build_tree.py deleted file mode 100644 index 37377af4..00000000 --- a/backend/app/utils/build_tree.py +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -from typing import Any, Sequence - -from backend.common.enums import BuildTreeType -from backend.utils.serializers import RowData, select_list_serialize - - -def get_tree_nodes(row: Sequence[RowData], is_sort: bool, sort_key: str) -> list[dict[str, Any]]: - """ - 获取所有树形结构节点 - - :param row: 原始数据行序列 - :param is_sort: 是否启用结果排序 - :param sort_key: 基于此键对结果进行进行排序 - :return: - """ - tree_nodes = select_list_serialize(row) - if is_sort: - tree_nodes.sort(key=lambda x: x[sort_key]) - return tree_nodes - - -def traversal_to_tree(nodes: list[dict[str, Any]]) -> list[dict[str, Any]]: - """ - 通过遍历算法构造树形结构 - - :param nodes: 树节点列表 - :return: - """ - tree: list[dict[str, Any]] = [] - node_dict = {node['id']: node for node in nodes} - - for node in nodes: - parent_id = node['parent_id'] - if parent_id is None: - tree.append(node) - else: - parent_node = node_dict.get(parent_id) - if parent_node is not None: - if 'children' not in parent_node: - parent_node['children'] = [] - if node not in parent_node['children']: - parent_node['children'].append(node) - else: - if node not in tree: - tree.append(node) - - return tree - - -def recursive_to_tree(nodes: list[dict[str, Any]], *, parent_id: int | None = None) -> list[dict[str, Any]]: - """ - 通过递归算法构造树形结构(性能影响较大) - - :param nodes: 树节点列表 - :param parent_id: 父节点 ID,默认为 None 表示根节点 - :return: - """ - tree: list[dict[str, Any]] = [] - for node in nodes: - if node['parent_id'] == parent_id: - child_nodes = recursive_to_tree(nodes, parent_id=node['id']) - if child_nodes: - node['children'] = child_nodes - tree.append(node) - return tree - - -def get_tree_data( - row: Sequence[RowData], - build_type: BuildTreeType = BuildTreeType.traversal, - *, - parent_id: int | None = None, - is_sort: bool = True, - sort_key: str = 'sort', -) -> list[dict[str, Any]]: - """ - 获取树形结构数据 - - :param row: 原始数据行序列 - :param build_type: 构建树形结构的算法类型,默认为遍历算法 - :param parent_id: 父节点 ID,仅在递归算法中使用 - :param is_sort: 是否启用结果排序 - :param sort_key: 基于此键对结果进行进行排序 - :return: - """ - nodes = get_tree_nodes(row, is_sort, sort_key) - match build_type: - case BuildTreeType.traversal: - tree = traversal_to_tree(nodes) - case BuildTreeType.recursive: - tree = recursive_to_tree(nodes, parent_id=parent_id) - case _: - raise ValueError(f'无效的算法类型:{build_type}') - return tree - - -def get_vben5_tree_data(row: Sequence[RowData], is_sort: bool = True, sort_key: str = 'sort') -> list[dict[str, Any]]: - """ - 获取 vben5 菜单树形结构数据 - - :param row: 原始数据行序列 - :param is_sort: 是否启用结果排序 - :param sort_key: 基于此键对结果进行进行排序 - :return: - """ - meta_keys = {'title', 'icon', 'link', 'cache', 'display', 'status'} - - vben5_nodes = [ - { - **{k: v for k, v in node.items() if k not in meta_keys}, - 'meta': { - 'title': node['title'], - 'icon': node['icon'], - 'iframeSrc': node['link'] if node['type'] == 3 else '', - 'link': node['link'] if node['type'] == 4 else '', - 'keepAlive': node['cache'], - 'hideInMenu': not bool(node['display']), - 'menuVisibleWithForbidden': not bool(node['status']), - }, - } - for node in get_tree_nodes(row, is_sort, sort_key) - ] - - return traversal_to_tree(vben5_nodes) diff --git a/backend/app/utils/common_util.py b/backend/app/utils/common_util.py index 3f04b9c4..cc7b2ac3 100644 --- a/backend/app/utils/common_util.py +++ b/backend/app/utils/common_util.py @@ -199,6 +199,7 @@ def bytes2human(n: int, format_str: str = '%(value).1f%(symbol)s') -> str: return format_str % locals() return format_str % dict(symbol=symbols[0], value=n) + def bytes2file_response(bytes_info: bytes): """生成文件响应""" yield bytes_info @@ -218,3 +219,198 @@ def get_filepath_from_url(url: str): filepath = setting.settings.STATIC_ROOT.joinpath(task_path, task_id, file_name) return filepath + + + +class SqlalchemyUtil: + """ + sqlalchemy工具类 + """ + + @classmethod + def base_to_dict( + cls, obj: Union[Base, Dict], transform_case: Literal['no_case', 'snake_to_camel', 'camel_to_snake'] = 'no_case' + ): + """ + 将sqlalchemy模型对象转换为字典 + + :param obj: sqlalchemy模型对象或普通字典 + :param transform_case: 转换得到的结果形式,可选的有'no_case'(不转换)、'snake_to_camel'(下划线转小驼峰)、'camel_to_snake'(小驼峰转下划线),默认为'no_case' + :return: 字典结果 + """ + if isinstance(obj, Base): + base_dict = obj.__dict__.copy() + base_dict.pop('_sa_instance_state', None) + for name, value in base_dict.items(): + if isinstance(value, InstrumentedList): + base_dict[name] = cls.serialize_result(value, 'snake_to_camel') + elif isinstance(obj, dict): + base_dict = obj.copy() + if transform_case == 'snake_to_camel': + return {CamelCaseUtil.snake_to_camel(k): v for k, v in base_dict.items()} + elif transform_case == 'camel_to_snake': + return {SnakeCaseUtil.camel_to_snake(k): v for k, v in base_dict.items()} + + return base_dict + + @classmethod + def serialize_result( + cls, result: Any, transform_case: Literal['no_case', 'snake_to_camel', 'camel_to_snake'] = 'no_case' + ): + """ + 将sqlalchemy查询结果序列化 + + :param result: sqlalchemy查询结果 + :param transform_case: 转换得到的结果形式,可选的有'no_case'(不转换)、'snake_to_camel'(下划线转小驼峰)、'camel_to_snake'(小驼峰转下划线),默认为'no_case' + :return: 序列化结果 + """ + if isinstance(result, (Base, dict)): + return cls.base_to_dict(result, transform_case) + elif isinstance(result, list): + return [cls.serialize_result(row, transform_case) for row in result] + elif isinstance(result, Row): + if all([isinstance(row, Base) for row in result]): + return [cls.base_to_dict(row, transform_case) for row in result] + elif any([isinstance(row, Base) for row in result]): + return [cls.serialize_result(row, transform_case) for row in result] + else: + result_dict = result._asdict() + if transform_case == 'snake_to_camel': + return {CamelCaseUtil.snake_to_camel(k): v for k, v in result_dict.items()} + elif transform_case == 'camel_to_snake': + return {SnakeCaseUtil.camel_to_snake(k): v for k, v in result_dict.items()} + return result_dict + return result + + +class CamelCaseUtil: + """ + 下划线形式(snake_case)转小驼峰形式(camelCase)工具方法 + """ + + @classmethod + def snake_to_camel(cls, snake_str: str): + """ + 下划线形式字符串(snake_case)转换为小驼峰形式字符串(camelCase) + + :param snake_str: 下划线形式字符串 + :return: 小驼峰形式字符串 + """ + # 分割字符串 + words = snake_str.split('_') + # 小驼峰命名,第一个词首字母小写,其余词首字母大写 + return words[0] + ''.join(word.capitalize() for word in words[1:]) + + @classmethod + def transform_result(cls, result: Any): + """ + 针对不同类型将下划线形式(snake_case)批量转换为小驼峰形式(camelCase)方法 + + :param result: 输入数据 + :return: 小驼峰形式结果 + """ + return SqlalchemyUtil.serialize_result(result=result, transform_case='snake_to_camel') + + +class SnakeCaseUtil: + """ + 小驼峰形式(camelCase)转下划线形式(snake_case)工具方法 + """ + + @classmethod + def camel_to_snake(cls, camel_str: str): + """ + 小驼峰形式字符串(camelCase)转换为下划线形式字符串(snake_case) + + :param camel_str: 小驼峰形式字符串 + :return: 下划线形式字符串 + """ + # 在大写字母前添加一个下划线,然后将整个字符串转为小写 + words = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', camel_str) + return re.sub('([a-z0-9])([A-Z])', r'\1_\2', words).lower() + + @classmethod + def transform_result(cls, result: Any): + """ + 针对不同类型将下划线形式(snake_case)批量转换为小驼峰形式(camelCase)方法 + + :param result: 输入数据 + :return: 小驼峰形式结果 + """ + return SqlalchemyUtil.serialize_result(result=result, transform_case='camel_to_snake') + + +def export_list2excel(list_data: List): + """ + 工具方法:将需要导出的list数据转化为对应excel的二进制数据 + + :param list_data: 数据列表 + :return: 字典信息对应excel的二进制数据 + """ + df = pd.DataFrame(list_data) + binary_data = io.BytesIO() + df.to_excel(binary_data, index=False, engine='openpyxl') + binary_data = binary_data.getvalue() + + return binary_data + + +def get_excel_template(header_list: List, selector_header_list: List, option_list: List[dict]): + """ + 工具方法:将需要导出的list数据转化为对应excel的二进制数据 + + :param header_list: 表头数据列表 + :param selector_header_list: 需要设置为选择器格式的表头数据列表 + :param option_list: 选择器格式的表头预设的选项列表 + :return: 模板excel的二进制数据 + """ + # 创建Excel工作簿 + wb = Workbook() + # 选择默认的活动工作表 + ws = wb.active + + # 设置表头文字 + headers = header_list + + # 设置表头背景样式为灰色,前景色为白色 + header_fill = PatternFill(start_color='ababab', end_color='ababab', fill_type='solid') + + # 将表头写入第一行 + for col_num, header in enumerate(headers, 1): + cell = ws.cell(row=1, column=col_num) + cell.value = header + cell.fill = header_fill + # 设置列宽度为16 + ws.column_dimensions[chr(64 + col_num)].width = 12 + # 设置水平居中对齐 + cell.alignment = Alignment(horizontal='center') + + # 设置选择器的预设选项 + options = option_list + + # 获取selector_header的字母索引 + for selector_header in selector_header_list: + column_selector_header_index = headers.index(selector_header) + 1 + + # 创建数据有效性规则 + header_option = [] + for option in options: + if option.get(selector_header): + header_option = option.get(selector_header) + dv = DataValidation(type='list', formula1=f'"{",".join(header_option)}"') + # 设置数据有效性规则的起始单元格和结束单元格 + dv.add( + f'{get_column_letter(column_selector_header_index)}2:{get_column_letter(column_selector_header_index)}1048576' + ) + # 添加数据有效性规则到工作表 + ws.add_data_validation(dv) + + # 保存Excel文件为字节类型的数据 + file = io.BytesIO() + wb.save(file) + file.seek(0) + + # 读取字节数据 + excel_data = file.getvalue() + + return excel_data diff --git a/backend/app/utils/jinja2_tools.py b/backend/app/utils/jinja2_tools.py index c2215a7f..32f98e36 100644 --- a/backend/app/utils/jinja2_tools.py +++ b/backend/app/utils/jinja2_tools.py @@ -1,6 +1,6 @@ import re -from module_gen.constants.gen_constants import GenConstants +from app.common.constant import GenConstants def snake_to_pascal_case(value): diff --git a/backend/app/utils/velocity_initializer.py b/backend/app/utils/velocity_initializer.py index c5374d8f..1b94f5da 100644 --- a/backend/app/utils/velocity_initializer.py +++ b/backend/app/utils/velocity_initializer.py @@ -1,7 +1,7 @@ from jinja2 import Environment, FileSystemLoader, select_autoescape import os -from module_gen.utils.jinja2_tools import snake_to_pascal_case, snake_to_camel, snake_2_colon, is_base_column, \ +from .jinja2_tools import snake_to_pascal_case, snake_to_camel, snake_2_colon, is_base_column, \ get_sqlalchemy_type, get_column_options diff --git a/frontend/src/views/workflow/operator/index.vue b/frontend/src/views/workflow/operator/index.vue new file mode 100644 index 00000000..655b6a61 --- /dev/null +++ b/frontend/src/views/workflow/operator/index.vue @@ -0,0 +1,576 @@ + + + + + +