refactor(common): 替换分页服务方法及代码生成模块重构

- 将分页服务中方法名由get_page_obj统一替换为paginate
- 注意相关controller均调整调用方式,保证统一接口调用
- 代码生成模块数据库模型统一替换为GenTableModel和GenTableColumnModel
- 更改数据库类型及分页相关配置为settings.DATABASE_TYPE统一管理
- 重构代码生成模块查询参数,新增GenTableQueryParam和GenTableColumnQueryParam类支持更灵活查询
- 数据模型中Pydantic Schema类型统一调整为Schema后缀
- 优化异常处理,增加请求参数验证错误的友好提示映射
- 调整中间件及依赖以支持更严格的类型检查及更健壮的用户权限认证逻辑
- 微调日志打印格式,改进请求日志信息输出风格
This commit is contained in:
zhangtao
2025-09-19 01:41:10 +08:00
parent 019bfdf57b
commit 81d131c0a4
35 changed files with 1127 additions and 369 deletions
@@ -37,7 +37,7 @@ async def get_obj_list_controller(
auth: AuthSchema = Depends(AuthPermission(permissions=["application:myapp:query"])) auth: AuthSchema = Depends(AuthPermission(permissions=["application:myapp:query"]))
) -> JSONResponse: ) -> JSONResponse:
result_dict_list = await ApplicationService.get_application_list_service(auth=auth, search=search, order_by=page.order_by) 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"查询应用列表成功") logger.info(f"查询应用列表成功")
return SuccessResponse(data=result_dict, msg="查询应用列表成功") return SuccessResponse(data=result_dict, msg="查询应用列表成功")
@@ -39,7 +39,7 @@ async def get_obj_list_controller(
auth: AuthSchema = Depends(AuthPermission(permissions=["demo:example:query"])) auth: AuthSchema = Depends(AuthPermission(permissions=["demo:example:query"]))
) -> JSONResponse: ) -> JSONResponse:
result_dict_list = await DemoService.get_demo_list_service(auth=auth, search=search, order_by=page.order_by) 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"查询示例列表成功") logger.info(f"查询示例列表成功")
return SuccessResponse(data=result_dict, msg="查询公告列表成功") return SuccessResponse(data=result_dict, msg="查询公告列表成功")
@@ -7,16 +7,16 @@ from sqlalchemy.orm import selectinload
from sqlglot.expressions import Expression from sqlglot.expressions import Expression
from typing import List from typing import List
from .model import GenTable, GenTableColumn from .model import GenTableModel, GenTableColumnModel
from config.env import DataBaseConfig from app.config.setting import settings
from utils.page_util import PageUtil from app.common.request import PaginationService
from .schema import ( from .schema import (
GenTableBaseModel, GenTableBaseSchema,
GenTableColumnBaseModel, GenTableColumnBaseSchema,
GenTableColumnModel, GenTableColumnSchema,
GenTableModel, GenTableSchema,
GenTablePageQueryModel,
) )
from .param import GenTableQueryParam, GenTableColumnBaseSchema
class GenTableDao: class GenTableDao:
@@ -36,7 +36,7 @@ class GenTableDao:
gen_table_info = ( gen_table_info = (
( (
await db.execute( 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() .scalars()
@@ -57,7 +57,7 @@ class GenTableDao:
gen_table_info = ( gen_table_info = (
( (
await db.execute( 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() .scalars()
@@ -74,7 +74,7 @@ class GenTableDao:
:param db: orm对象 :param db: orm对象
:return: 所有业务表信息 :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 return gen_table_all
@@ -88,11 +88,11 @@ class GenTableDao:
:return: :return:
""" """
for sql_statement in sql_statements: 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)) await db.execute(text(sql))
@classmethod @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: 代码生成业务表列表信息对象 :return: 代码生成业务表列表信息对象
""" """
query = ( query = (
select(GenTable) select(GenTableModel)
.options(selectinload(GenTable.columns)) .options(selectinload(GenTableModel.columns))
.where( .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 if query_object.table_name
else True, 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 if query_object.table_comment
else True, 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.begin_time, '%Y-%m-%d'), time(00, 00, 00)),
datetime.combine(datetime.strptime(query_object.end_time, '%Y-%m-%d'), time(23, 59, 59)), datetime.combine(datetime.strptime(query_object.end_time, '%Y-%m-%d'), time(23, 59, 59)),
) )
@@ -120,12 +120,12 @@ class GenTableDao:
) )
.distinct() .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 return gen_table_list
@classmethod @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: 是否开启分页 :param is_page: 是否开启分页
:return: 数据库列表信息对象 :return: 数据库列表信息对象
""" """
if DataBaseConfig.db_type == 'postgresql': if settings.DATABASE_TYPE == 'postgresql':
query_sql = """ query_sql = """
table_name as table_name, table_name as table_name,
table_comment as table_comment, table_comment as table_comment,
@@ -166,12 +166,12 @@ class GenTableDao:
if query_object.table_comment: if query_object.table_comment:
query_sql += """and lower(table_comment) like lower(concat('%', :table_comment, '%'))""" query_sql += """and lower(table_comment) like lower(concat('%', :table_comment, '%'))"""
if query_object.begin_time: 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')""" query_sql += """and create_time::date >= to_date(:begin_time, 'yyyy-MM-dd')"""
else: else:
query_sql += """and date_format(create_time, '%Y%m%d') >= date_format(:begin_time, '%Y%m%d')""" query_sql += """and date_format(create_time, '%Y%m%d') >= date_format(:begin_time, '%Y%m%d')"""
if query_object.end_time: 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')""" query_sql += """and create_time::date <= to_date(:end_time, 'yyyy-MM-dd')"""
else: else:
query_sql += """and date_format(create_time, '%Y%m%d') >= date_format(:end_time, '%Y%m%d')""" 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 return gen_db_table_list
@@ -197,7 +197,7 @@ class GenTableDao:
:param table_names: 业务表名称组 :param table_names: 业务表名称组
:return: 数据库列表信息对象 :return: 数据库列表信息对象
""" """
if DataBaseConfig.db_type == 'postgresql': if settings.DATABASE_TYPE == 'postgresql':
query_sql = """ query_sql = """
select select
table_name as table_name, table_name as table_name,
@@ -240,7 +240,7 @@ class GenTableDao:
:param gen_table: 业务表对象 :param gen_table: 业务表对象
:return: :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) db.add(db_gen_table)
await db.flush() await db.flush()
@@ -255,7 +255,7 @@ class GenTableDao:
:param gen_table: 需要更新的业务表字典 :param gen_table: 需要更新的业务表字典
:return: :return:
""" """
await db.execute(update(GenTable), [GenTableBaseModel(**gen_table).model_dump()]) await db.execute(update(GenTableModel), [GenTableBaseSchema(**gen_table).model_dump()])
@classmethod @classmethod
async def delete_gen_table_dao(cls, db: AsyncSession, gen_table: GenTableModel): async def delete_gen_table_dao(cls, db: AsyncSession, gen_table: GenTableModel):
@@ -266,7 +266,7 @@ class GenTableDao:
:param gen_table: 业务表对象 :param gen_table: 业务表对象
:return: :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: class GenTableColumnDao:
@@ -286,7 +286,7 @@ class GenTableColumnDao:
gen_table_column_list = ( gen_table_column_list = (
( (
await db.execute( 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() .scalars()
@@ -304,7 +304,7 @@ class GenTableColumnDao:
:param table_name: 业务表名称 :param table_name: 业务表名称
:return: 业务表字段列表信息对象 :return: 业务表字段列表信息对象
""" """
if DataBaseConfig.db_type == 'postgresql': if settings.DATABASE_TYPE == 'postgresql':
query_sql = """ query_sql = """
select select
column_name, is_required, is_pk, sort, column_comment, is_increment, column_type column_name, is_required, is_pk, sort, column_comment, is_increment, column_type
@@ -354,8 +354,8 @@ class GenTableColumnDao:
:param gen_table_column: 岗位对象 :param gen_table_column: 岗位对象
:return: :return:
""" """
db_gen_table_column = GenTableColumn( db_gen_table_column = GenTableColumnModel(
**GenTableColumnBaseModel(**gen_table_column.model_dump(by_alias=True)).model_dump() **GenTableColumnBaseSchema(**gen_table_column.model_dump(by_alias=True)).model_dump()
) )
db.add(db_gen_table_column) db.add(db_gen_table_column)
await db.flush() await db.flush()
@@ -371,7 +371,7 @@ class GenTableColumnDao:
:param gen_table_column: 需要更新的业务表字段字典 :param gen_table_column: 需要更新的业务表字段字典
:return: :return:
""" """
await db.execute(update(GenTableColumn), [GenTableColumnBaseModel(**gen_table_column).model_dump()]) await db.execute(update(GenTableColumnModel), [GenTableColumnBaseSchema(**gen_table_column).model_dump()])
@classmethod @classmethod
async def delete_gen_table_column_by_table_id_dao(cls, db: AsyncSession, gen_table_column: GenTableColumnModel): 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: 业务表字段对象 :param gen_table_column: 业务表字段对象
:return: :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 @classmethod
async def delete_gen_table_column_by_column_id_dao(cls, db: AsyncSession, gen_table_column: GenTableColumnModel): 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: 业务表字段对象 :param post: 业务表字段对象
:return: :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])))
@@ -45,7 +45,6 @@ class GenTableColumnModel(CreatorMixin):
""" """
代码生成业务表字段 代码生成业务表字段
""" """
__tablename__ = 'gen_table_column' __tablename__ = 'gen_table_column'
__table_args__ = ({'comment': '代码生成业务表字段'}) __table_args__ = ({'comment': '代码生成业务表字段'})
@@ -1,20 +1,64 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
@as_query from datetime import datetime
class GenTablePageQuerySchema(GenTableQuerySchema): from typing import Optional
""" from fastapi import Query
代码生成业务表分页查询模型
"""
page_num: int = Field(default=1, description='当前页码') from app.core.validator import DateTimeStr
page_size: int = Field(default=10, description='每页记录数') from app.common.request import PageResultSchema
from .schema import GenTableBaseSchema, GenTableColumnBaseSchema
@as_query class GenTableQueryParam(PageResultSchema, GenTableBaseSchema):
class GenTableColumnPageQuerySchema(GenTableColumnQuerySchema): """数据库表查询参数"""
"""
代码生成业务表字段分页查询模型 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='每页记录数')
@@ -6,7 +6,6 @@ from pydantic import BaseModel, ConfigDict, Field, model_validator
from pydantic.alias_generators import to_camel from pydantic.alias_generators import to_camel
from pydantic_validation_decorator import NotBlank from pydantic_validation_decorator import NotBlank
from module_admin.annotation.pydantic_annotation import as_query
from utils.string_util import StringUtil from utils.string_util import StringUtil
from app.common.constant import GenConstant from app.common.constant import GenConstant
@@ -89,9 +88,9 @@ class GenTableSchema(GenTableBaseSchema):
代码生成业务表模型 代码生成业务表模型
""" """
pk_column: Optional['GenTableColumnModel'] = Field(default=None, description='主键信息') pk_column: Optional['GenTableColumnSchema'] = Field(default=None, description='主键信息')
sub_table: Optional['GenTableModel'] = Field(default=None, description='子表信息') sub_table: Optional['GenTableSchema'] = Field(default=None, description='子表信息')
columns: Optional[List['GenTableColumnModel']] = Field(default=None, description='表列信息') columns: Optional[List['GenTableColumnSchema']] = Field(default=None, description='表列信息')
tree_code: Optional[str] = Field(default=None, description='树编码字段') tree_code: Optional[str] = Field(default=None, description='树编码字段')
tree_parent_code: Optional[str] = Field(default=None, description='树父编码字段') tree_parent_code: Optional[str] = Field(default=None, description='树父编码字段')
tree_name: 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='是否为单表') crud: Optional[bool] = Field(default=None, description='是否为单表')
@model_validator(mode='after') @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.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.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 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): class GenTableParamsSchema(BaseModel):
@@ -130,15 +129,6 @@ class GenTableParamsSchema(BaseModel):
parent_menu_id: Optional[int] = Field(default=None, description='上级菜单ID字段') 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): class DeleteGenTableSchema(BaseModel):
""" """
删除代码生成业务表模型 删除代码生成业务表模型
@@ -206,7 +196,7 @@ class GenTableColumnSchema(GenTableColumnBaseSchema):
usable_column: Optional[bool] = Field(default=None, description='是否为基类字段白名单') usable_column: Optional[bool] = Field(default=None, description='是否为基类字段白名单')
@model_validator(mode='after') @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.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.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 self.increment = True if self.is_increment and self.is_increment == '1' else False
@@ -227,15 +217,6 @@ class GenTableColumnSchema(GenTableColumnBaseSchema):
return self return self
class GenTableColumnQuerySchema(GenTableColumnBaseSchema):
"""
代码生成业务表字段不分页查询模型
"""
begin_time: Optional[str] = Field(default=None, description='开始时间')
end_time: Optional[str] = Field(default=None, description='结束时间')
class DeleteGenTableColumnSchema(BaseModel): class DeleteGenTableColumnSchema(BaseModel):
""" """
删除代码生成业务表字段模型 删除代码生成业务表字段模型
@@ -38,7 +38,7 @@ async def get_obj_list_controller(
auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:job:query"])) auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:job:query"]))
) -> JSONResponse: ) -> JSONResponse:
result_dict_list = await JobService.get_job_list_service(auth=auth, search=search, order_by=page.order_by) 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"查询定时任务列表成功") logger.info(f"查询定时任务列表成功")
return SuccessResponse(data=result_dict, msg="查询定时任务列表成功") 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"])) auth: AuthSchema = Depends(AuthPermission(permissions=["monitor:job:query"]))
) -> JSONResponse: ) -> JSONResponse:
result_dict_list = await JobLogService.get_job_log_list_service(auth=auth, search=search, order_by=page.order_by) 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"查询定时任务日志列表成功") logger.info(f"查询定时任务日志列表成功")
return SuccessResponse(data=result_dict, msg="查询定时任务日志列表成功") return SuccessResponse(data=result_dict, msg="查询定时任务日志列表成功")
@@ -30,7 +30,7 @@ async def get_online_list_controller(
)->JSONResponse: )->JSONResponse:
# 获取全量数据 # 获取全量数据
result_dict_list = await OnlineService.get_online_list_service(redis=redis, search=search) 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('获取成功') logger.info('获取成功')
return SuccessResponse(data=result_dict,msg='获取成功') return SuccessResponse(data=result_dict,msg='获取成功')
@@ -42,7 +42,7 @@ async def get_type_list_controller(
auth: AuthSchema = Depends(AuthPermission(permissions=["system:dict_type:query"])) auth: AuthSchema = Depends(AuthPermission(permissions=["system:dict_type:query"]))
) -> JSONResponse: ) -> JSONResponse:
result_dict_list = await DictTypeService.get_obj_list_service(auth=auth, search=search, order_by=page.order_by) 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"查询字典类型列表成功") logger.info(f"查询字典类型列表成功")
return SuccessResponse(data=result_dict, msg="查询字典类型列表成功") 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"])) auth: AuthSchema = Depends(AuthPermission(permissions=["system:dict_data:query"]))
) -> JSONResponse: ) -> JSONResponse:
result_dict_list = await DictDataService.get_obj_list_service(auth=auth, search=search, order_by=page.order_by) 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"查询字典数据列表成功") logger.info(f"查询字典数据列表成功")
return SuccessResponse(data=result_dict, msg="查询字典数据列表成功") return SuccessResponse(data=result_dict, msg="查询字典数据列表成功")
@@ -26,7 +26,7 @@ async def get_obj_list_controller(
) -> JSONResponse: ) -> JSONResponse:
""" 查询日志 """ """ 查询日志 """
result_dict_list = await OperationLogService.get_log_list_service(search=search, auth=auth, order_by=page.order_by) 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"查询日志成功") logger.info(f"查询日志成功")
return SuccessResponse(data=result_dict, msg="查询日志成功") return SuccessResponse(data=result_dict, msg="查询日志成功")
@@ -38,7 +38,7 @@ async def get_obj_list_controller(
auth: AuthSchema = Depends(AuthPermission(permissions=["system:notice:query"])) auth: AuthSchema = Depends(AuthPermission(permissions=["system:notice:query"]))
) -> JSONResponse: ) -> JSONResponse:
result_dict_list = await NoticeService.get_notice_list_service(auth=auth, search=search, order_by=page.order_by) 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"查询公告列表成功") logger.info(f"查询公告列表成功")
return SuccessResponse(data=result_dict, msg="查询公告列表成功") return SuccessResponse(data=result_dict, msg="查询公告列表成功")
@@ -103,6 +103,6 @@ async def get_obj_list_available_controller(
auth: AuthSchema = Depends(get_current_user) auth: AuthSchema = Depends(get_current_user)
) -> JSONResponse: ) -> JSONResponse:
result_dict_list = await NoticeService.get_notice_list_available_service(auth=auth) 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"查询已启用公告列表成功") logger.info(f"查询已启用公告列表成功")
return SuccessResponse(data=result_dict, msg="查询已启用公告列表成功") return SuccessResponse(data=result_dict, msg="查询已启用公告列表成功")
@@ -57,7 +57,7 @@ async def get_obj_list_controller(
search: ParamsQueryParam = Depends(), search: ParamsQueryParam = Depends(),
) -> JSONResponse: ) -> JSONResponse:
result_dict_list = await ParamsService.get_obj_list_service(auth=auth, search=search, order_by=page.order_by) 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"获取参数列表成功") logger.info(f"获取参数列表成功")
return SuccessResponse(data=result_dict, msg="查询参数列表成功") return SuccessResponse(data=result_dict, msg="查询参数列表成功")
@@ -30,7 +30,7 @@ async def get_obj_list_controller(
auth: AuthSchema = Depends(AuthPermission(permissions=["system:position:query"])), auth: AuthSchema = Depends(AuthPermission(permissions=["system:position:query"])),
) -> JSONResponse: ) -> JSONResponse:
result_dict_list = await PositionService.get_position_list_service(search=search, auth=auth, order_by=page.order_by) 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"查询岗位列表成功") logger.info(f"查询岗位列表成功")
return SuccessResponse(data=result_dict, msg="查询岗位列表成功") return SuccessResponse(data=result_dict, msg="查询岗位列表成功")
@@ -31,7 +31,7 @@ async def get_obj_list_controller(
auth: AuthSchema = Depends(AuthPermission(permissions=["system:role:query"])), auth: AuthSchema = Depends(AuthPermission(permissions=["system:role:query"])),
) -> JSONResponse: ) -> JSONResponse:
result_dict_list = await RoleService.get_role_list_service(search=search, auth=auth, order_by=page.order_by) 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"查询角色成功") logger.info(f"查询角色成功")
return SuccessResponse(data=result_dict, msg="查询角色成功") return SuccessResponse(data=result_dict, msg="查询角色成功")
@@ -35,7 +35,7 @@ async def get_ticket_list_controller(
auth: AuthSchema = Depends(AuthPermission(permissions=["system:ticket:query"])) auth: AuthSchema = Depends(AuthPermission(permissions=["system:ticket:query"]))
) -> JSONResponse: ) -> JSONResponse:
result_dict_list = await TicketService.get_ticket_list_service(auth=auth, search=search, order_by=page.order_by) 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("查询工单列表成功") logger.info("查询工单列表成功")
return SuccessResponse(data=result_dict, msg="查询工单列表成功") return SuccessResponse(data=result_dict, msg="查询工单列表成功")
@@ -106,7 +106,7 @@ async def get_obj_list_controller(
auth: AuthSchema = Depends(AuthPermission(permissions=["system:user:query"])), auth: AuthSchema = Depends(AuthPermission(permissions=["system:user:query"])),
) -> JSONResponse: ) -> JSONResponse:
result_dict_list = await UserService.get_user_list_service(search=search, auth=auth, order_by=page.order_by) 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"查询用户成功") logger.info(f"查询用户成功")
return SuccessResponse(data=result_dict, msg="查询用户成功") return SuccessResponse(data=result_dict, msg="查询用户成功")
@@ -32,7 +32,6 @@ from .schema import (
) )
class UserService: class UserService:
"""用户模块服务层""" """用户模块服务层"""
@@ -35,7 +35,7 @@ async def get_version_list_controller(
auth: AuthSchema = Depends(AuthPermission(permissions=["system:version:query"])) auth: AuthSchema = Depends(AuthPermission(permissions=["system:version:query"]))
) -> JSONResponse: ) -> JSONResponse:
result_dict_list = await VersionService.get_version_list_service(auth=auth, search=search, order_by=page.order_by) 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("查询版本列表成功") logger.info("查询版本列表成功")
return SuccessResponse(data=result_dict, msg="查询版本列表成功") return SuccessResponse(data=result_dict, msg="查询版本列表成功")
+1 -1
View File
@@ -23,7 +23,7 @@ class PaginationService:
"""分页服务类""" """分页服务类"""
@staticmethod @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和分页信息返回分页或非分页数据列表结果 输入数据列表data_list和分页信息返回分页或非分页数据列表结果
如果未传入page_no和page_size则返回全部数据 如果未传入page_no和page_size则返回全部数据
-1
View File
@@ -275,7 +275,6 @@ class Settings(BaseSettings):
"app.core.middlewares.CustomCORSMiddleware" if self.CORS_ORIGIN_ENABLE else None, "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.RequestLogMiddleware" if self.OPERATION_LOG_RECORD else None,
"app.core.middlewares.CustomGZipMiddleware" if self.GZIP_ENABLE else None, "app.core.middlewares.CustomGZipMiddleware" if self.GZIP_ENABLE else None,
"app.core.middlewares.DemoEnvMiddleware" if self.DEMO_ENABLE else None,
] ]
return MIDDLEWARES return MIDDLEWARES
+2 -2
View File
@@ -3,7 +3,7 @@
from pydantic import BaseModel from pydantic import BaseModel
from typing import TypeVar, Sequence, Generic, Dict, Any, List, Union, Optional from typing import TypeVar, Sequence, Generic, Dict, Any, List, Union, Optional
from sqlalchemy.sql.elements import ColumnElement 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.engine import Result
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import asc, func, select, delete, Select, desc, update, or_, and_ 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.model = model
self.auth = auth self.auth = auth
self.db: AsyncSession = auth.db self.db: AsyncSession | Session | None = auth.db
self.current_user = auth.user self.current_user = auth.user
async def get(self, **kwargs) -> Optional[ModelType]: async def get(self, **kwargs) -> Optional[ModelType]:
+3 -2
View File
@@ -8,6 +8,7 @@ from fastapi import Depends, Request
from motor.motor_asyncio import AsyncIOMotorDatabase from motor.motor_asyncio import AsyncIOMotorDatabase
from fastapi import Depends from fastapi import Depends
from app.api.v1.module_system.user.schema import UserOutSchema
from app.common.enums import RedisInitKeyConfig from app.common.enums import RedisInitKeyConfig
from app.core.exceptions import CustomException from app.core.exceptions import CustomException
from app.core.database import session_connect from app.core.database import session_connect
@@ -95,7 +96,7 @@ async def get_current_user(
if hasattr(user, 'positions'): if hasattr(user, 'positions'):
user.positions = [pos for pos in user.positions if pos.status] user.positions = [pos for pos in user.positions if pos.status]
auth.user = user auth.user = UserOutSchema.model_validate(user)
return auth return auth
@@ -133,7 +134,7 @@ class AuthPermission:
auth.check_data_scope = self.check_data_scope auth.check_data_scope = self.check_data_scope
# 超级管理员直接通过 # 超级管理员直接通过
if auth.user.is_superuser: if auth.user and auth.user.is_superuser:
return auth return auth
# 无需验证权限 # 无需验证权限
+12 -78
View File
@@ -1,14 +1,12 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Any, Optional, List, Tuple, Union from typing import Any, Optional
from fastapi import Request, status from fastapi import Request, status
from fastapi.exceptions import RequestValidationError, ResponseValidationError from fastapi.exceptions import RequestValidationError, ResponseValidationError
from pydantic_validation_decorator import FieldValidationError from pydantic_validation_decorator import FieldValidationError
from starlette.responses import JSONResponse from starlette.responses import JSONResponse
from starlette.exceptions import HTTPException from starlette.exceptions import HTTPException
from sqlalchemy.exc import SQLAlchemyError from sqlalchemy.exc import SQLAlchemyError
from pydantic_core import ErrorDetails
from pydantic import ValidationError
from app.common.constant import RET from app.common.constant import RET
from app.common.response import ErrorResponse from app.common.response import ErrorResponse
@@ -20,7 +18,7 @@ class CustomException(Exception):
def __init__( def __init__(
self, self,
msg: Optional[str] = RET.EXCEPTION.msg, msg: str = RET.EXCEPTION.msg,
code: int = RET.EXCEPTION.code, code: int = RET.EXCEPTION.code,
status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR, status_code: int = status.HTTP_500_INTERNAL_SERVER_ERROR,
data: Optional[Any] = None, data: Optional[Any] = None,
@@ -59,15 +57,23 @@ async def HttpExceptionHandler(request: Request, exc: HTTPException) -> JSONResp
async def ValidationExceptionHandler(request: Request, exc: RequestValidationError) -> JSONResponse: 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}") logger.error(f"请求地址: {request.url}, 错误信息: {msg}, 错误详情: {exc}")
return ErrorResponse(msg=str(msg), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, data=exc.body) return ErrorResponse(msg=str(msg), status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, data=exc.body)
async def ResponseValidationHandle(request: Request, exc: ResponseValidationError) -> JSONResponse: async def ResponseValidationHandle(request: Request, exc: ResponseValidationError) -> JSONResponse:
logger.error(f"请求地址: {request.url}, 错误详情: {exc}") logger.error(f"请求地址: {request.url}, 错误详情: {exc}")
return ErrorResponse(msg=str(exc), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, data=exc.body) return ErrorResponse(msg=str(exc), status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, data=exc.body)
async def SQLAlchemyExceptionHandler(request: Request, exc: SQLAlchemyError) -> JSONResponse: async def SQLAlchemyExceptionHandler(request: Request, exc: SQLAlchemyError) -> JSONResponse:
"""数据库异常处理器""" """数据库异常处理器"""
error_msg = f'数据库操作失败: {exc}' error_msg = f'数据库操作失败: {exc}'
@@ -91,75 +97,3 @@ async def AllExceptionHandler(request: Request, exc: Exception) -> JSONResponse:
"""全局异常处理器""" """全局异常处理器"""
logger.error(f"请求地址: {request.url}, 错误详情: {exc}") logger.error(f"请求地址: {request.url}, 错误详情: {exc}")
return ErrorResponse(msg='服务器内部错误', status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, data=str(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
+2 -1
View File
@@ -8,6 +8,7 @@ import logging
from logging.handlers import TimedRotatingFileHandler from logging.handlers import TimedRotatingFileHandler
from typing import Optional, Dict, Any from typing import Optional, Dict, Any
from pathlib import Path from pathlib import Path
import typing
from app.config.setting import settings from app.config.setting import settings
@@ -29,7 +30,7 @@ class CustomTimedRotatingFileHandler(TimedRotatingFileHandler):
# 使用流上下文管理确保资源正确释放 # 使用流上下文管理确保资源正确释放
if self.stream: if self.stream:
self.stream.close() self.stream.close()
self.stream = None self.stream = None # type: ignore
try: try:
# 计算轮换时间(使用缓存避免重复计算) # 计算轮换时间(使用缓存避免重复计算)
+38 -43
View File
@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import time import time
from typing import Dict, List, Union from typing import Any
from starlette.middleware.cors import CORSMiddleware from starlette.middleware.cors import CORSMiddleware
from starlette.types import ASGIApp from starlette.types import ASGIApp
from starlette.requests import Request from starlette.requests import Request
from starlette.middleware.gzip import GZipMiddleware 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.common.response import ErrorResponse
from app.config.setting import settings from app.config.setting import settings
@@ -17,7 +18,7 @@ from app.core.exceptions import CustomException
class CustomCORSMiddleware(CORSMiddleware): class CustomCORSMiddleware(CORSMiddleware):
"""CORS跨域中间件""" """CORS跨域中间件"""
def __init__(self, app: ASGIApp) -> None: def __init__(self, app: ASGIApp) -> None:
CORSMiddlewareConfig: Dict[str, Union[List[str], bool]] = { CORSMiddlewareConfig: dict[str, Any] = {
"allow_origins": settings.ALLOW_ORIGINS, "allow_origins": settings.ALLOW_ORIGINS,
"allow_methods": settings.ALLOW_METHODS, "allow_methods": settings.ALLOW_METHODS,
"allow_headers": settings.ALLOW_HEADERS, "allow_headers": settings.ALLOW_HEADERS,
@@ -38,42 +39,17 @@ class RequestLogMiddleware(BaseHTTPMiddleware):
) -> Response: ) -> Response:
start_time = time.time() start_time = time.time()
logger.info( # 构建请求日志信息
f"请求来源: {request.client.host}, " request_info = f"请求方法: {request.method}, 请求路径: {request.url.path}"
f"请求方法: {request.method}, " if request.client:
f"请求路径: {request.url.path}, " request_info = f"请求来源: {request.client.host}, {request_info}"
f"客户端IP: {request.client.host}" logger.info(request_info)
)
try: try:
response = await call_next(request)
process_time = round(time.time() - start_time, 5)
response.headers["X-Process-Time"] = str(process_time)
logger.info( if settings.DEMO_ENABLE:
f"会话ID: {request.scope.get('session_id')}, " # 在演示环境中,只有白名单内的IP或路径才能执行非GET请求
f"响应状态: {response.status_code}, " if request.method != "GET":
f"响应内容长度: {response.headers.get('content-length', '0')}, "
f"处理时间: {process_time}s"
)
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") path = request.scope.get("path")
request_ip = None request_ip = None
@@ -83,20 +59,39 @@ class DemoEnvMiddleware(BaseHTTPMiddleware):
request_ip = x_forwarded_for.split(',')[0].strip() request_ip = x_forwarded_for.split(',')[0].strip()
else: else:
# 若没有 X-Forwarded-For 头,则使用 request.client.host # 若没有 X-Forwarded-For 头,则使用 request.client.host
request_ip = request.client.host request_ip = request.client.host if request.client else None
user_username = request.scope.get("user_username")
logger.error(f"用户名称: {user_username}")
# 检查IP是否在白名单,或路径是否在白名单,或用户是否在白名单 # 检查IP是否在白名单,或路径是否在白名单,或用户是否在白名单
if (request_ip in settings.DEMO_IP_WHITE_LIST) or (path in settings.DEMO_WHITE_LIST_PATH): if (request_ip in settings.DEMO_IP_WHITE_LIST) or (path in settings.DEMO_WHITE_LIST_PATH):
return await call_next(request) response = await call_next(request)
else: else:
# 非白名单用户,禁止操作 # 非白名单用户,禁止操作
return ErrorResponse(msg="演示环境,禁止操作") 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)
return await call_next(request) # 构建响应日志信息
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"响应内容长度: {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"系统异常,请联系管理员", data=str(e))
class CustomGZipMiddleware(GZipMiddleware): class CustomGZipMiddleware(GZipMiddleware):
+81
View File
@@ -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
+2 -2
View File
@@ -148,7 +148,7 @@ class RedisCURD:
async def hash_set(self, name: str, key: str, value: Any) -> bool: async def hash_set(self, name: str, key: str, value: Any) -> bool:
"""设置哈希缓存""" """设置哈希缓存"""
try: try:
await self.redis.hset(name=name, key=key, value=value) self.redis.hset(name=name, key=key, value=value)
return True return True
except Exception as e: except Exception as e:
logger.error(f"设置哈希缓存失败: {str(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]]: async def hash_get(self, name: str, keys: list[str]) -> Optional[list[Any]]:
"""获取哈希缓存""" """获取哈希缓存"""
try: try:
return await self.redis.hmget(name=name, keys=keys) return self.redis.hmget(name=name, keys=keys)
except Exception as e: except Exception as e:
logger.error(f"获取哈希缓存失败: {str(e)}") logger.error(f"获取哈希缓存失败: {str(e)}")
return None return None
+8 -10
View File
@@ -35,7 +35,7 @@ class OperationLogRoute(APIRoute):
return response return response
if request.method not in settings.OPERATION_RECORD_METHOD: if request.method not in settings.OPERATION_RECORD_METHOD:
return response return response
route: APIRoute = request.scope.get("route") route: APIRoute = request.scope.get("route", None)
if route.name in settings.IGNORE_OPERATION_FUNCTION: if route.name in settings.IGNORE_OPERATION_FUNCTION:
return response return response
@@ -66,7 +66,6 @@ class OperationLogRoute(APIRoute):
oper_param['path_params'] = dict(path_params) oper_param['path_params'] = dict(path_params)
payload = json.dumps(oper_param, ensure_ascii=False) payload = json.dumps(oper_param, ensure_ascii=False)
# payload = str(oper_param)
# 日志表请求参数字段长度最大为2000,因此在此处判断长度 # 日志表请求参数字段长度最大为2000,因此在此处判断长度
if len(payload) > 2000: if len(payload) > 2000:
@@ -89,19 +88,18 @@ class OperationLogRoute(APIRoute):
request_ip = x_forwarded_for.split(',')[0].strip() request_ip = x_forwarded_for.split(',')[0].strip()
else: else:
# 若没有 X-Forwarded-For 头,则使用 request.client.host # 若没有 X-Forwarded-For 头,则使用 request.client.host
if request.client:
request_ip = request.client.host 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文档 # 判断请求是否来自api文档
request_from_swagger = ( referer = request.headers.get('referer')
request.headers.get('referer').endswith('docs') if request.headers.get('referer') else False request_from_swagger = referer and referer.endswith('docs')
) request_from_redoc = referer and referer.endswith('redoc')
request_from_redoc = (
request.headers.get('referer').endswith('redoc') if request.headers.get('referer') else False
)
if request_from_swagger or request_from_redoc: if request_from_swagger or request_from_redoc:
# 如果请求来自api文档,则不记录日志
pass pass
else: else:
async with session_connect() as session: async with session_connect() as session:
@@ -117,7 +115,7 @@ class OperationLogRoute(APIRoute):
request_os = user_agent.os.family, request_os = user_agent.os.family,
request_browser = user_agent.browser.family, request_browser = user_agent.browser.family,
response_code = response.status_code, 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, process_time = process_time,
description = route.summary, description = route.summary,
creator_id = current_user_id creator_id = current_user_id
+40
View File
@@ -2256,5 +2256,45 @@
"description": "前端构建" "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": "我的流程"
}
]
} }
] ]
@@ -430,5 +430,45 @@
{ {
"role_id": 1, "role_id": 1,
"menu_id": 108 "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
} }
] ]
-126
View File
@@ -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)
+196
View File
@@ -199,6 +199,7 @@ def bytes2human(n: int, format_str: str = '%(value).1f%(symbol)s') -> str:
return format_str % locals() return format_str % locals()
return format_str % dict(symbol=symbols[0], value=n) return format_str % dict(symbol=symbols[0], value=n)
def bytes2file_response(bytes_info: bytes): def bytes2file_response(bytes_info: bytes):
"""生成文件响应""" """生成文件响应"""
yield bytes_info 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) filepath = setting.settings.STATIC_ROOT.joinpath(task_path, task_id, file_name)
return filepath 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
+1 -1
View File
@@ -1,6 +1,6 @@
import re import re
from module_gen.constants.gen_constants import GenConstants from app.common.constant import GenConstants
def snake_to_pascal_case(value): def snake_to_pascal_case(value):
+1 -1
View File
@@ -1,7 +1,7 @@
from jinja2 import Environment, FileSystemLoader, select_autoescape from jinja2 import Environment, FileSystemLoader, select_autoescape
import os 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 get_sqlalchemy_type, get_column_options
@@ -0,0 +1,576 @@
<!-- 演示示例 -->
<template>
<div class="app-container">
<!-- 搜索区域 -->
<div class="search-container">
<el-form ref="queryFormRef" :model="queryFormData" :inline="true" label-suffix=":">
<el-form-item prop="name" label="名称">
<el-input v-model="queryFormData.name" placeholder="请输入名称" clearable />
</el-form-item>
<el-form-item prop="status" label="状态">
<el-select v-model="queryFormData.status" placeholder="请选择状态" style="width: 167.5px" clearable>
<el-option value="true" label="启用" />
<el-option value="false" label="停用" />
</el-select>
</el-form-item>
<!-- 时间范围收起状态下隐藏 -->
<el-form-item v-if="isExpand" prop="start_time" label="创建时间">
<DatePicker
v-model="dateRange"
@update:model-value="handleDateRangeChange"
/>
</el-form-item>
<!-- 查询重置展开/收起按钮 -->
<el-form-item class="search-buttons">
<el-button type="primary" icon="search" @click="handleQuery">
查询
</el-button>
<el-button icon="refresh" @click="handleResetQuery">
重置
</el-button>
<!-- 展开/收起 -->
<template v-if="isExpandable">
<el-link class="ml-3" type="primary" underline="never" @click="isExpand = !isExpand">
{{ isExpand ? "收起" : "展开" }}
<el-icon>
<template v-if="isExpand">
<ArrowUp />
</template>
<template v-else>
<ArrowDown />
</template>
</el-icon>
</el-link>
</template>
</el-form-item>
</el-form>
</div>
<!-- 内容区域 -->
<el-card shadow="hover" class="data-table">
<template #header>
<div class="card-header">
<span>
<el-tooltip content="流程列表">
<QuestionFilled class="w-4 h-4 mx-1" />
</el-tooltip>
演示示例列表
</span>
</div>
</template>
<!-- 功能区域 -->
<div class="data-table__toolbar">
<div class="data-table__toolbar--actions">
<el-button type="success" icon="plus" @click="handleOpenDialog('create')">新增</el-button>
<el-button type="danger" icon="delete" :disabled="selectIds.length === 0"
@click="handleDelete(selectIds)">批量删除</el-button>
<el-dropdown trigger="click">
<el-button type="default" :disabled="selectIds.length === 0" icon="ArrowDown">更多</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item icon="Check" @click="handleMoreClick(true)">批量启用</el-dropdown-item>
<el-dropdown-item icon="CircleClose"
@click="handleMoreClick(false)">批量停用</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
<div class="data-table__toolbar--tools">
<el-tooltip content="导入">
<el-button type="info" icon="upload" circle @click="handleOpenImportDialog" />
</el-tooltip>
<el-tooltip content="导出">
<el-button type="warning" icon="download" circle @click="handleExport" />
</el-tooltip>
<el-tooltip content="刷新">
<el-button type="primary" icon="refresh" circle @click="handleRefresh" />
</el-tooltip>
<el-tooltip content="列表筛选">
<el-dropdown trigger="click">
<el-button type="default" icon="operation" circle />
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item v-for="column in tableColumns" :key="column.prop"
:command="column">
<el-checkbox v-model="column.show">
{{ column.label }}
</el-checkbox>
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</el-tooltip>
</div>
</div>
<!-- 表格区域系统配置列表 -->
<el-table ref="dataTableRef" v-loading="loading" :data="pageTableData" highlight-current-row
class="data-table__content" height="450" border stripe @selection-change="handleSelectionChange">
<template #empty>
<el-empty :image-size="80" description="暂无数据" />
</template>
<el-table-column v-if="tableColumns.find(col => col.prop === 'selection')?.show" type="selection"
min-width="55" align="center" />
<el-table-column v-if="tableColumns.find(col => col.prop === 'index')?.show" fixed label="序号"
min-width="60">
<template #default="scope">
{{ (queryFormData.page_no - 1) * queryFormData.page_size + scope.$index + 1 }}
</template>
</el-table-column>
<el-table-column v-if="tableColumns.find(col => col.prop === 'name')?.show" label="名称"
prop="name" min-width="140" />
<el-table-column v-if="tableColumns.find(col => col.prop === 'status')?.show" label="状态" prop="status"
min-width="80">
<template #default="scope">
<el-tag :type="scope.row.status === true ? 'success' : 'danger'">
{{ scope.row.status === true ? "启用" : "停用" }}
</el-tag>
</template>
</el-table-column>
<el-table-column v-if="tableColumns.find(col => col.prop === 'description')?.show" label="描述"
prop="description" min-width="140" />
<el-table-column v-if="tableColumns.find(col => col.prop === 'created_at')?.show" label="创建时间"
prop="created_at" min-width="180" sortable />
<el-table-column v-if="tableColumns.find(col => col.prop === 'updated_at')?.show" label="更新时间"
prop="updated_at" min-width="180" sortable />
<el-table-column v-if="tableColumns.find(col => col.prop === 'creator')?.show" key="creator" label="创建人"
min-width="100">
<template #default="scope">
{{ scope.row.creator?.name }}
</template>
</el-table-column>
<el-table-column v-if="tableColumns.find(col => col.prop === 'operation')?.show" fixed="right"
label="操作" align="center" min-width="200">
<template #default="scope">
<el-button type="info" size="small" link icon="document"
@click="handleOpenDialog('detail', scope.row.id)">详情</el-button>
<el-button type="primary" size="small" link icon="edit"
@click="handleOpenDialog('update', scope.row.id)">编辑</el-button>
<el-button type="danger" size="small" link icon="delete"
@click="handleDelete([scope.row.id])">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页区域 -->
<template #footer>
<pagination v-model:total="total" v-model:page="queryFormData.page_no"
v-model:limit="queryFormData.page_size" @pagination="loadingData" />
</template>
</el-card>
<!-- 弹窗区域 -->
<el-dialog v-model="dialogVisible.visible" :title="dialogVisible.title" @close="handleCloseDialog">
<!-- 详情 -->
<template v-if="dialogVisible.type === 'detail'">
<el-descriptions :column="4" border>
<el-descriptions-item label="名称" :span="2">
{{ detailFormData.name }}
</el-descriptions-item>
<el-descriptions-item label="状态" :span="2">
<el-tag :type="detailFormData.status ? 'success' : 'danger'">
{{ detailFormData.status ? '启用' : '停用' }}
</el-tag>
</el-descriptions-item>
<el-descriptions-item label="描述" :span="2">
{{ detailFormData.description }}
</el-descriptions-item>
<el-descriptions-item label="创建人" :span="2">
{{ detailFormData.creator?.name }}
</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2">
{{ detailFormData.created_at }}
</el-descriptions-item>
<el-descriptions-item label="更新时间" :span="2">
{{ detailFormData.updated_at }}
</el-descriptions-item>
</el-descriptions>
</template>
<!-- 新增编辑表单 -->
<template v-else>
<el-form ref="dataFormRef" :model="formData" :rules="rules" label-suffix=":" label-width="auto"
label-position="right">
<el-form-item label="名称" prop="name">
<el-input v-model="formData.name" placeholder="请输入名称" :maxlength="50" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-radio-group v-model="formData.status">
<el-radio :value="true">
启用
</el-radio>
<el-radio :value="false">
停用
</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="描述" prop="description">
<el-input v-model="formData.description" :rows="4" :maxlength="100" show-word-limit
type="textarea" placeholder="请输入描述" />
</el-form-item>
</el-form>
</template>
<template #footer>
<div class="dialog-footer">
<!-- 详情弹窗不需要确定按钮的提交逻辑 -->
<el-button @click="handleCloseDialog">取消</el-button>
<el-button v-if="dialogVisible.type !== 'detail'" type="primary"
@click="handleSubmit">确定</el-button>
<el-button v-else type="primary" @click="handleCloseDialog">确定</el-button>
</div>
</template>
</el-dialog>
<!-- 用户导入 -->
<ImportModal v-model="importDialogVisible" title="导入数据" @import-success="handleQuery()" @download-template="handleDownloadTemplate" @upload="handleUpload" />
</div>
</template>
<script setup lang="ts">
defineOptions({
name: "Example",
inheritAttrs: false,
});
import { ref, reactive, onMounted } from "vue";
import { ElMessage } from "element-plus";
import { ResultEnum } from "@/enums/api/result.enum";
import ExampleAPI, { ExampleTable, ExampleForm, ExamplePageQuery } from "@/api/demo/example";
import ImportModal from "@/components/Upload/ImportModal.vue";
import DatePicker from "@/components/DatePicker/index.vue";
const emit = defineEmits(['import-success']);
const queryFormRef = ref();
const dataFormRef = ref();
const total = ref(0);
const selectIds = ref<number[]>([]);
const loading = ref(false);
const isExpand = ref(false);
const isExpandable = ref(true);
//
const pageTableData = ref<ExampleTable[]>([]);
//
const tableColumns = ref([
{ prop: 'selection', label: '选择框', show: true },
{ prop: 'index', label: '序号', show: true },
{ prop: 'name', label: '名称', show: true },
{ prop: 'status', label: '状态', show: true },
{ prop: 'description', label: '描述', show: true },
{ prop: 'created_at', label: '创建时间', show: true },
{ prop: 'updated_at', label: '更新时间', show: true },
{ prop: 'creator', label: '创建人', show: true },
{ prop: 'operation', label: '操作', show: true }
])
//
const detailFormData = ref<ExampleTable>({});
//
const dateRange = ref<[Date, Date] | []>([]);
//
function handleDateRangeChange(range: [Date, Date]) {
dateRange.value = range;
if (range && range.length === 2) {
queryFormData.start_time = range[0].toISOString();
queryFormData.end_time = range[1].toISOString();
} else {
queryFormData.start_time = undefined;
queryFormData.end_time = undefined;
}
}
//
const queryFormData = reactive<ExamplePageQuery>({
page_no: 1,
page_size: 10,
name: undefined,
status: undefined,
start_time: undefined,
end_time: undefined,
});
//
const formData = reactive<ExampleForm>({
id: undefined,
name: '',
status: true,
description: undefined,
})
//
const dialogVisible = reactive({
title: "",
visible: false,
type: 'create' as 'create' | 'update' | 'detail',
});
//
const rules = reactive({
name: [{ required: true, message: "请输入名称", trigger: "blur" }],
status: [{ required: true, message: "请选择状态", trigger: "blur" }],
});
//
const importDialogVisible = ref(false);
//
async function handleRefresh() {
await loadingData();
};
//
async function loadingData() {
loading.value = true;
try {
const response = await ExampleAPI.getExampleList(queryFormData);
pageTableData.value = response.data.data.items;
total.value = response.data.data.total;
}
catch (error: any) {
console.error(error);
}
finally {
loading.value = false;
}
}
//
async function handleQuery() {
queryFormData.page_no = 1;
loadingData();
}
//
async function handleResetQuery() {
queryFormRef.value.resetFields();
queryFormData.page_no = 1;
//
dateRange.value = [];
queryFormData.start_time = undefined;
queryFormData.end_time = undefined;
loadingData();
}
//
const initialFormData: ExampleForm = {
id: undefined,
name: '',
status: true,
description: '',
}
//
async function resetForm() {
if (dataFormRef.value) {
dataFormRef.value.resetFields();
dataFormRef.value.clearValidate();
}
// formData
Object.assign(formData, initialFormData);
}
//
async function handleSelectionChange(selection: any) {
selectIds.value = selection.map((item: any) => item.id);
}
//
async function handleCloseDialog() {
dialogVisible.visible = false;
resetForm();
}
//
async function handleOpenDialog(type: 'create' | 'update' | 'detail', id?: number) {
dialogVisible.type = type;
if (id) {
const response = await ExampleAPI.getExampleDetail(id);
if (type === 'detail') {
dialogVisible.title = "详情";
Object.assign(detailFormData.value, response.data.data);
} else if (type === 'update') {
dialogVisible.title = "修改";
Object.assign(formData, response.data.data);
}
} else {
dialogVisible.title = "新增公告通知";
formData.id = undefined;
}
dialogVisible.visible = true;
}
//
async function handleSubmit() {
//
dataFormRef.value.validate(async (valid: any) => {
if (valid) {
loading.value = true;
// (deatil\create\update)
const id = formData.id;
if (id) {
try {
await ExampleAPI.updateExample(id, { id, ...formData })
dialogVisible.visible = false;
resetForm();
handleCloseDialog();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
} else {
try {
await ExampleAPI.createExample(formData)
dialogVisible.visible = false;
resetForm();
handleCloseDialog();
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
}
}
});
}
//
async function handleDelete(ids: number[]) {
ElMessageBox.confirm("确认删除该项数据?", "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(async () => {
try {
loading.value = true;
await ExampleAPI.deleteExample(ids);
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
}).catch(() => {
ElMessageBox.close();
});
}
//
async function handleExport() {
ElMessageBox.confirm('是否确认导出当前系统配置?', '警告', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(async () => {
let downloadUrl = '';
try {
loading.value = true;
const response = await ExampleAPI.exportExample(queryFormData);
const fileData = response.data;
const fileName = decodeURI(response.headers["content-disposition"].split(";")[1].split("=")[1]);
const fileType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8";
const blob = new Blob([fileData], { type: fileType });
downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement("a");
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
} catch (error: any) {
//
console.error('导出失败:', error);
} finally {
if (downloadUrl) {
window.URL.revokeObjectURL(downloadUrl);
}
loading.value = false;
}
}).catch(() => {
ElMessageBox.close();
});
}
//
const handleUpload = async (formData: FormData, file: File) => {
try {
const response = await ExampleAPI.importExample(formData);
if (response.data.code === ResultEnum.SUCCESS) {
ElMessage.success(`${response.data.msg}${response.data.data}`);
emit('import-success');
}
} catch (error: any) {
console.error(error);
}
};
//
const handleDownloadTemplate = () => {
ExampleAPI.downloadTemplate().then((response: any) => {
const fileData = response.data;
const fileName = decodeURI(response.headers['content-disposition'].split('; ')[1].split('=')[1]);
const fileType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8';
const blob = new Blob([fileData], { type: fileType });
const downloadUrl = window.URL.createObjectURL(blob);
const downloadLink = document.createElement('a');
downloadLink.href = downloadUrl;
downloadLink.download = fileName;
document.body.appendChild(downloadLink);
downloadLink.click();
document.body.removeChild(downloadLink);
window.URL.revokeObjectURL(downloadUrl);
});
};
//
function handleOpenImportDialog() {
importDialogVisible.value = true;
}
// /
async function handleMoreClick(status: boolean) {
if (selectIds.value.length) {
ElMessageBox.confirm(`确认${status ? '启用' : '停用'}该项数据?`, "警告", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(async () => {
try {
loading.value = true;
await ExampleAPI.batchAvailableExample({ ids: selectIds.value, status });
handleResetQuery();
} catch (error: any) {
console.error(error);
} finally {
loading.value = false;
}
}).catch(() => {
ElMessageBox.close();
});
}
}
onMounted(() => {
loadingData();
});
</script>
<style lang="scss" scoped></style>