diff --git a/backend/app/api/v1/module_application/ai/crud.py b/backend/app/api/v1/module_application/ai/crud.py index 3f9cf281..d6f9f6eb 100644 --- a/backend/app/api/v1/module_application/ai/crud.py +++ b/backend/app/api/v1/module_application/ai/crud.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from typing import Dict, List, Optional, Sequence +from typing import Dict, List, Optional, Sequence, Union, Any from app.core.base_crud import CRUDBase from app.api.v1.module_system.auth.schema import AuthSchema @@ -21,42 +21,45 @@ class McpCRUD(CRUDBase[McpModel, McpCreateSchema, McpUpdateSchema]): self.auth = auth super().__init__(model=McpModel, auth=auth) - async def get_by_id_crud(self, id: int) -> Optional[McpModel]: + async def get_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[McpModel]: """ 获取MCP服务器详情 参数: - id (int): MCP服务器ID + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Optional[McpModel]: MCP服务器模型实例(如果存在) """ - return await self.get(id=id) + return await self.get(id=id, preload=preload) - async def get_by_name_crud(self, name: str) -> Optional[McpModel]: + async def get_by_name_crud(self, name: str, preload: Optional[List[Union[str, Any]]] = None) -> Optional[McpModel]: """ 通过名称获取MCP服务器 参数: - name (str): MCP服务器名称 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Optional[McpModel]: MCP服务器模型实例(如果存在) """ - return await self.get(name=name) + return await self.get(name=name, preload=preload) - async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[McpModel]: + async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[McpModel]: """ 列表查询MCP服务器 参数: - search (Optional[Dict]): 查询参数字典 - order_by (Optional[List[Dict[str, str]]]): 排序参数列表 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Sequence[McpModel]: MCP服务器模型实例序列 """ - return await self.list(search=search or {}, order_by=order_by or [{'id': 'asc'}]) + return await self.list(search=search or {}, order_by=order_by or [{'id': 'asc'}], preload=preload) async def create_crud(self, data: McpCreateSchema) -> Optional[McpModel]: """ @@ -93,4 +96,4 @@ class McpCRUD(CRUDBase[McpModel, McpCreateSchema, McpUpdateSchema]): 返回: - None """ - return await self.delete(ids=ids) + return await self.delete(ids=ids) \ No newline at end of file diff --git a/backend/app/api/v1/module_application/ai/model.py b/backend/app/api/v1/module_application/ai/model.py index 28f1a35c..f34434e5 100644 --- a/backend/app/api/v1/module_application/ai/model.py +++ b/backend/app/api/v1/module_application/ai/model.py @@ -14,6 +14,7 @@ class McpModel(CreatorMixin): __tablename__ = 'app_ai_mcp' __table_args__ = ({'comment': 'MCP 服务器表'}) + __loader_options__ = ["creator"] name: Mapped[str] = mapped_column(String(50), unique=True, comment='MCP 名称') type: Mapped[int] = mapped_column(Integer, default=0, comment='MCP 类型(0:stdio 1:sse)') diff --git a/backend/app/api/v1/module_application/job/crud.py b/backend/app/api/v1/module_application/job/crud.py index aa0aeda2..9c310cf5 100644 --- a/backend/app/api/v1/module_application/job/crud.py +++ b/backend/app/api/v1/module_application/job/crud.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from typing import Dict, List, Optional, Sequence +from typing import Dict, List, Optional, Sequence, Union, Any from app.core.base_crud import CRUDBase from app.api.v1.module_system.auth.schema import AuthSchema @@ -21,30 +21,32 @@ class JobCRUD(CRUDBase[JobModel, JobCreateSchema, JobUpdateSchema]): self.auth = auth super().__init__(model=JobModel, auth=auth) - async def get_obj_by_id_crud(self, id: int) -> Optional[JobModel]: + async def get_obj_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[JobModel]: """ 获取定时任务详情 参数: - id (int): 定时任务ID + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Optional[JobModel]: 定时任务模型,如果不存在则为None """ - return await self.get(id=id) + return await self.get(id=id, preload=preload) - async def get_obj_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[JobModel]: + async def get_obj_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[JobModel]: """ 获取定时任务列表 参数: - search (Optional[Dict]): 查询参数字典 - order_by (Optional[List[Dict[str, str]]]): 排序参数列表 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Sequence[JobModel]: 定时任务模型序列 """ - return await self.list(search=search, order_by=order_by) + return await self.list(search=search, order_by=order_by, preload=preload) async def create_obj_crud(self, data: JobCreateSchema) -> Optional[JobModel]: """ @@ -113,30 +115,32 @@ class JobLogCRUD(CRUDBase[JobLogModel, JobLogCreateSchema, JobLogUpdateSchema]): self.auth = auth super().__init__(model=JobLogModel, auth=auth) - async def get_obj_log_by_id_crud(self, id: int) -> Optional[JobLogModel]: + async def get_obj_log_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[JobLogModel]: """ 获取定时任务日志详情 参数: - id (int): 定时任务日志ID + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Optional[JobLogModel]: 定时任务日志模型,如果不存在则为None """ - return await self.get(id=id) + return await self.get(id=id, preload=preload) - async def get_obj_log_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[JobLogModel]: + async def get_obj_log_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[JobLogModel]: """ 获取定时任务日志列表 参数: - search (Optional[Dict]): 查询参数字典 - order_by (Optional[List[Dict[str, str]]]): 排序参数列表 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Sequence[JobLogModel]: 定时任务日志模型序列 """ - return await self.list(search=search, order_by=order_by) + return await self.list(search=search, order_by=order_by, preload=preload) async def delete_obj_log_crud(self, ids: List[int]) -> None: """ diff --git a/backend/app/api/v1/module_application/job/model.py b/backend/app/api/v1/module_application/job/model.py index 2a5d5fd6..982a48ce 100644 --- a/backend/app/api/v1/module_application/job/model.py +++ b/backend/app/api/v1/module_application/job/model.py @@ -14,6 +14,7 @@ class JobModel(CreatorMixin): """ __tablename__ = 'app_job' __table_args__ = ({'comment': '定时任务调度表'}) + __loader_options__ = ["job_logs", "creator"] name: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, default='', comment='任务名称') jobstore: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, default='default', comment='存储器') @@ -37,6 +38,7 @@ class JobLogModel(MappedBase): """ __tablename__ = 'app_job_log' __table_args__ = ({'comment': '定时任务调度日志表'}) + __loader_options__ = ["job"] id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment='主键ID') job_name: Mapped[str] = mapped_column(String(64),nullable=False,comment='任务名称') @@ -52,4 +54,4 @@ class JobLogModel(MappedBase): status: Mapped[bool] = mapped_column(Boolean(), default=True, nullable=False, comment="是否启用(True:启用 False:禁用)") create_time: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True, default=datetime.now, comment='创建时间') # 任务关联关系 - job: Mapped[Optional["JobModel"]] = relationship(back_populates="job_logs", lazy="selectin") + job: Mapped[Optional["JobModel"]] = relationship(back_populates="job_logs", lazy="selectin") \ No newline at end of file diff --git a/backend/app/api/v1/module_application/job/schema.py b/backend/app/api/v1/module_application/job/schema.py index 28c09866..7d292618 100644 --- a/backend/app/api/v1/module_application/job/schema.py +++ b/backend/app/api/v1/module_application/job/schema.py @@ -60,10 +60,10 @@ class JobLogCreateSchema(BaseModel): class JobLogUpdateSchema(JobLogCreateSchema): """定时任务调度日志表更新模型""" ... - job_log_id: Optional[int] = Field(default=None, description='任务日志ID') + id: Optional[int] = Field(default=None, description='任务日志ID') -class JobLogOutSchema(JobLogCreateSchema): +class JobLogOutSchema(JobLogUpdateSchema): """定时任务调度日志表响应模型""" model_config = ConfigDict(from_attributes=True) ... diff --git a/backend/app/api/v1/module_application/myapp/crud.py b/backend/app/api/v1/module_application/myapp/crud.py index 2e070c56..3a97e214 100644 --- a/backend/app/api/v1/module_application/myapp/crud.py +++ b/backend/app/api/v1/module_application/myapp/crud.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from typing import Dict, List, Optional, Sequence +from typing import Dict, List, Optional, Sequence, Union, Any from app.core.base_crud import CRUDBase from app.api.v1.module_system.auth.schema import AuthSchema @@ -21,30 +21,32 @@ class ApplicationCRUD(CRUDBase[ApplicationModel, ApplicationCreateSchema, Applic self.auth = auth super().__init__(model=ApplicationModel, auth=auth) - async def get_by_id_crud(self, id: int) -> Optional[ApplicationModel]: + async def get_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[ApplicationModel]: """ 根据id获取应用详情 参数: - id (int): 应用ID + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Optional[ApplicationModel]: 应用详情,如果不存在则为None """ - return await self.get(id=id) + return await self.get(id=id, preload=preload) - async def list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[ApplicationModel]: + async def list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[ApplicationModel]: """ 列表查询应用 参数: - search (Optional[Dict]): 查询参数,默认None - order_by (Optional[List[Dict[str, str]]]): 排序参数,默认None + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Sequence[ApplicationModel]: 应用列表 """ - return await self.list(search=search, order_by=order_by) + return await self.list(search=search, order_by=order_by, preload=preload) async def create_crud(self, data: ApplicationCreateSchema) -> Optional[ApplicationModel]: """ diff --git a/backend/app/api/v1/module_application/myapp/model.py b/backend/app/api/v1/module_application/myapp/model.py index 8d2e4a1f..3bc6815c 100644 --- a/backend/app/api/v1/module_application/myapp/model.py +++ b/backend/app/api/v1/module_application/myapp/model.py @@ -13,6 +13,7 @@ class ApplicationModel(CreatorMixin): __tablename__ = 'app_myapp' __table_args__ = ({'comment': '应用系统表'}) + __loader_options__ = ["creator"] # 基本信息(必备字段) name: Mapped[str] = mapped_column(String(64), nullable=False, comment='应用名称', unique=True) @@ -24,5 +25,4 @@ class ApplicationModel(CreatorMixin): access_url: Mapped[str] = mapped_column(String(500), nullable=False, comment='访问地址') # 外观展示 - icon_url: Mapped[str] = mapped_column(String(300), nullable=True, comment='应用图标URL') - \ No newline at end of file + icon_url: Mapped[str] = mapped_column(String(300), nullable=True, comment='应用图标URL') \ No newline at end of file diff --git a/backend/app/api/v1/module_generator/demo/controller.py b/backend/app/api/v1/module_generator/demo/controller.py index e283d75a..181df57a 100644 --- a/backend/app/api/v1/module_generator/demo/controller.py +++ b/backend/app/api/v1/module_generator/demo/controller.py @@ -5,7 +5,6 @@ from fastapi.responses import JSONResponse, StreamingResponse import urllib.parse from app.common.response import StreamResponse, SuccessResponse -from app.common.request import PaginationService from app.utils.common_util import bytes2file_response from app.core.base_params import PaginationQueryParam from app.core.dependencies import AuthPermission @@ -59,8 +58,14 @@ async def get_obj_list_controller( 返回: - JSONResponse: 包含示例列表分页信息的JSON响应 """ - result_dict_list = await DemoService.list_service(auth=auth, search=search, order_by=page.order_by) - result_dict = await PaginationService.paginate(data_list=result_dict_list, page_no=page.page_no, page_size=page.page_size) + # 使用数据库分页而不是应用层分页 + result_dict = await DemoService.page_service( + auth=auth, + page_no=page.page_no if page.page_no is not None else 1, + page_size=page.page_size if page.page_size is not None else 10, + search=search, + order_by=page.order_by + ) logger.info("查询示例列表成功") return SuccessResponse(data=result_dict, msg="查询示例列表成功") diff --git a/backend/app/api/v1/module_generator/demo/crud.py b/backend/app/api/v1/module_generator/demo/crud.py index acb90812..c4c08b92 100644 --- a/backend/app/api/v1/module_generator/demo/crud.py +++ b/backend/app/api/v1/module_generator/demo/crud.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- -from typing import Dict, List, Optional, Sequence +from typing import Dict, List, Optional, Sequence, Union, Any from app.core.base_crud import CRUDBase from app.api.v1.module_system.auth.schema import AuthSchema from .model import DemoModel -from .schema import DemoCreateSchema, DemoUpdateSchema +from .schema import DemoCreateSchema, DemoUpdateSchema, DemoOutSchema class DemoCRUD(CRUDBase[DemoModel, DemoCreateSchema, DemoUpdateSchema]): @@ -20,30 +20,32 @@ class DemoCRUD(CRUDBase[DemoModel, DemoCreateSchema, DemoUpdateSchema]): """ super().__init__(model=DemoModel, auth=auth) - async def get_by_id_crud(self, id: int) -> Optional[DemoModel]: + async def get_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[DemoModel]: """ 详情 参数: - id (int): 示例ID + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Optional[DemoModel]: 示例模型实例或None """ - return await self.get(id=id) + return await self.get(id=id, preload=preload) - async def list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[DemoModel]: + async def list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[DemoModel]: """ 列表查询 参数: - search (Optional[Dict]): 查询参数 - order_by (Optional[List[Dict[str, str]]]): 排序参数 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Sequence[DemoModel]: 示例模型实例序列 """ - return await self.list(search=search, order_by=order_by) + return await self.list(search=search, order_by=order_by, preload=preload) async def create_crud(self, data: DemoCreateSchema) -> Optional[DemoModel]: """ @@ -93,4 +95,30 @@ class DemoCRUD(CRUDBase[DemoModel, DemoCreateSchema, DemoUpdateSchema]): 返回: - None """ - return await self.set(ids=ids, status=status) \ No newline at end of file + return await self.set(ids=ids, status=status) + + async def page_crud(self, offset: int, limit: int, order_by: Optional[List[Dict[str, str]]] = None, search: Optional[Dict] = None, preload: Optional[List[Union[str, Any]]] = None) -> Dict: + """ + 分页查询 + + 参数: + - offset (int): 偏移量 + - limit (int): 每页数量 + - order_by (Optional[List[Dict[str, str]]]): 排序参数 + - search (Optional[Dict]): 查询参数 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 + + 返回: + - Dict: 分页数据 + """ + order_by_list = order_by or [{'id': 'asc'}] + search_dict = search or {} + + return await self.page( + offset=offset, + limit=limit, + order_by=order_by_list, + search=search_dict, + out_schema=DemoOutSchema, + preload=preload + ) diff --git a/backend/app/api/v1/module_generator/demo/model.py b/backend/app/api/v1/module_generator/demo/model.py index 8d966942..30553162 100644 --- a/backend/app/api/v1/module_generator/demo/model.py +++ b/backend/app/api/v1/module_generator/demo/model.py @@ -13,6 +13,7 @@ class DemoModel(CreatorMixin): """ __tablename__ = 'gen_demo' __table_args__ = ({'comment': '示例表'}) + __loader_options__ = ["creator"] name: Mapped[Optional[str]] = mapped_column(String(64), nullable=True, default='', comment='名称') - status: Mapped[bool] = mapped_column(Boolean(), default=True, nullable=False, comment="是否启用(True:启用 False:禁用)") + status: Mapped[bool] = mapped_column(Boolean(), default=True, nullable=False, comment="是否启用(True:启用 False:禁用)") \ No newline at end of file diff --git a/backend/app/api/v1/module_generator/demo/service.py b/backend/app/api/v1/module_generator/demo/service.py index e7f9f234..6a0b912f 100644 --- a/backend/app/api/v1/module_generator/demo/service.py +++ b/backend/app/api/v1/module_generator/demo/service.py @@ -54,6 +54,33 @@ class DemoService: obj_list = await DemoCRUD(auth).list_crud(search=search_dict, order_by=order_by) return [DemoOutSchema.model_validate(obj).model_dump() for obj in obj_list] + @classmethod + async def page_service(cls, auth: AuthSchema, page_no: int, page_size: int, search: Optional[DemoQueryParam] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Dict: + """ + 分页查询 + + 参数: + - auth (AuthSchema): 认证信息模型 + - page_no (int): 页码 + - page_size (int): 每页数量 + - search (Optional[DemoQueryParam]): 查询参数 + - order_by (Optional[List[Dict[str, str]]]): 排序参数 + + 返回: + - Dict: 分页数据 + """ + search_dict = search.__dict__ if search else {} + order_by_list = order_by or [{'id': 'asc'}] + offset = (page_no - 1) * page_size + + result = await DemoCRUD(auth).page_crud( + offset=offset, + limit=page_size, + order_by=order_by_list, + search=search_dict + ) + return result + @classmethod async def create_service(cls, auth: AuthSchema, data: DemoCreateSchema) -> Dict: """ diff --git a/backend/app/api/v1/module_generator/gencode/crud.py b/backend/app/api/v1/module_generator/gencode/crud.py index 1f047df7..ab1e62da 100644 --- a/backend/app/api/v1/module_generator/gencode/crud.py +++ b/backend/app/api/v1/module_generator/gencode/crud.py @@ -4,7 +4,7 @@ from sqlalchemy.engine.row import Row from sqlalchemy import and_, delete, select, text, update from sqlalchemy.orm import selectinload from sqlglot.expressions import Expression -from typing import List, Optional, Sequence, Dict +from typing import List, Optional, Sequence, Dict, Union, Any from app.core.logger import logger from app.config.setting import settings @@ -34,12 +34,13 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]): """ super().__init__(model=GenTableModel, auth=auth) - async def get_gen_table_by_id(self, table_id: int) -> Optional[GenTableModel]: + async def get_gen_table_by_id(self, table_id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[GenTableModel]: """ 根据业务表ID获取需要生成的业务表信息。 参数: - table_id (int): 业务表ID。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - GenTableModel | None: 业务表信息对象。 @@ -58,12 +59,13 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]): return gen_table - async def get_gen_table_by_name(self, table_name: str) -> Optional[GenTableModel]: + async def get_gen_table_by_name(self, table_name: str, preload: Optional[List[Union[str, Any]]] = None) -> Optional[GenTableModel]: """ 根据业务表名称获取需要生成的业务表信息。 参数: - table_name (str): 业务表名称。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - GenTableModel | None: 业务表信息对象。 @@ -82,10 +84,13 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]): return gen_table - async def get_gen_table_all(self) -> Sequence[GenTableModel]: + async def get_gen_table_all(self, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[GenTableModel]: """ 获取所有业务表信息。 + 参数: + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 + 返回: - Sequence[GenTableModel]: 所有业务表信息列表。 """ @@ -98,12 +103,13 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]): return gen_table_all - async def get_gen_table_list(self, search: Optional[GenTableQueryParam] = None) -> Sequence[GenTableModel]: + async def get_gen_table_list(self, search: Optional[GenTableQueryParam] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[GenTableModel]: """ 根据查询参数获取代码生成业务表列表信息。 参数: - search (GenTableQueryParam | None): 查询参数对象。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Sequence[GenTableModel]: 业务表列表信息。 @@ -400,39 +406,42 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen """ super().__init__(model=GenTableColumnModel, auth=auth) - async def get_gen_table_column_by_id(self, id: int) -> Optional[GenTableColumnModel]: + async def get_gen_table_column_by_id(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[GenTableColumnModel]: """根据业务表字段ID获取业务表字段信息。 参数: - id (int): 业务表字段ID。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Optional[GenTableColumnModel]: 业务表字段信息对象。 """ - return await self.get(id=id) + return await self.get(id=id, preload=preload) - async def get_gen_table_column_list_by_table_id(self, table_id: int) -> Optional[GenTableColumnModel]: + async def get_gen_table_column_list_by_table_id(self, table_id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[GenTableColumnModel]: """根据业务表ID获取业务表字段列表信息。 参数: - table_id (int): 业务表ID。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Optional[GenTableColumnModel]: 业务表字段列表信息对象。 """ - return await self.get(table_id=table_id) + return await self.get(table_id=table_id, preload=preload) - async def list_gen_table_column_crud_by_table_id(self, table_id: int, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[GenTableColumnModel]: + async def list_gen_table_column_crud_by_table_id(self, table_id: int, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[GenTableColumnModel]: """根据业务表ID查询业务表字段列表。 参数: - table_id (int): 业务表ID。 - order_by (Optional[List[Dict[str, str]]]): 排序字段列表,每个元素为{"field": "字段名", "order": "asc" | "desc"}。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Sequence[GenTableColumnModel]: 业务表字段列表信息对象序列。 """ - return await self.list(search={"table_id": table_id}, order_by=order_by) + return await self.list(search={"table_id": table_id}, order_by=order_by, preload=preload) async def get_gen_db_table_columns_by_name(self, table_name: str | None) -> List[GenTableColumnOutSchema]: """ @@ -516,17 +525,18 @@ class GenTableColumnCRUD(CRUDBase[GenTableColumnModel, GenTableColumnSchema, Gen return result - async def list_gen_table_column_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[GenTableColumnModel]: + async def list_gen_table_column_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[GenTableColumnModel]: """根据业务表字段查询业务表字段列表。 参数: - search (Optional[Dict]): 查询参数,例如{"table_id": 1}。 - order_by (Optional[List[Dict[str, str]]]): 排序字段列表,每个元素为{"field": "字段名", "order": "asc" | "desc"}。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Sequence[GenTableColumnModel]: 业务表字段列表信息对象序列。 """ - return await self.list(search=search, order_by=order_by) + return await self.list(search=search, order_by=order_by, preload=preload) async def create_gen_table_column_crud(self, data: GenTableColumnSchema) -> Optional[GenTableColumnModel]: """创建业务表字段。 diff --git a/backend/app/api/v1/module_generator/gencode/model.py b/backend/app/api/v1/module_generator/gencode/model.py index 48f2885f..ad4a4d8b 100644 --- a/backend/app/api/v1/module_generator/gencode/model.py +++ b/backend/app/api/v1/module_generator/gencode/model.py @@ -13,6 +13,7 @@ class GenTableModel(CreatorMixin): """ __tablename__ = 'gen_table' __table_args__ = ({'comment': '代码生成表'}) + __loader_options__ = ["columns", "creator"] table_name: Mapped[Optional[str]] = mapped_column(String(200), nullable=True, default='', comment='表名称') table_comment: Mapped[Optional[str]] = mapped_column(String(500), nullable=True, default='', comment='表描述') @@ -45,6 +46,7 @@ class GenTableColumnModel(CreatorMixin): """ __tablename__ = 'gen_table_column' __table_args__ = ({'comment': '代码生成表字段'}) + __loader_options__ = ["tables", "creator"] column_name: Mapped[Optional[str]] = mapped_column(String(200), nullable=True, comment='列名称') column_comment: Mapped[Optional[str]] = mapped_column(String(500), nullable=True, comment='列描述') diff --git a/backend/app/api/v1/module_system/dept/crud.py b/backend/app/api/v1/module_system/dept/crud.py index cecfb3a7..84c70191 100644 --- a/backend/app/api/v1/module_system/dept/crud.py +++ b/backend/app/api/v1/module_system/dept/crud.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from typing import Dict, List, Optional, Sequence +from typing import Dict, List, Optional, Sequence, Union, Any from app.core.base_crud import CRUDBase from ..auth.schema import AuthSchema @@ -16,46 +16,49 @@ class DeptCRUD(CRUDBase[DeptModel, DeptCreateSchema, DeptUpdateSchema]): self.auth = auth super().__init__(model=DeptModel, auth=auth) - async def get_by_id_crud(self, id: int) -> Optional[DeptModel]: + async def get_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[DeptModel]: """ 根据 id 获取部门信息。 参数: - id (int): 部门 ID。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - DeptModel | None: 部门信息,未找到返回 None。 """ - obj = await self.get(id=id) + obj = await self.get(id=id, preload=preload) if not obj: return None return obj - async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[DeptModel]: + async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[DeptModel]: """ 获取部门列表。 参数: - search (Dict | None): 搜索条件。 - order_by (List[Dict[str, str]] | None): 排序字段列表。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Sequence[DeptModel]: 部门列表。 """ - return await self.list(search=search, order_by=order_by) + return await self.list(search=search, order_by=order_by, preload=preload) - async def get_tree_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[DeptModel]: + async def get_tree_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[DeptModel]: """ 获取部门树形列表。 参数: - search (Dict | None): 搜索条件。 - order_by (List[Dict[str, str]] | None): 排序字段列表。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Sequence[DeptModel]: 部门树形列表。 """ - return await self.tree_list(search=search, order_by=order_by, children_attr='children') + return await self.tree_list(search=search, order_by=order_by, children_attr='children', preload=preload) async def set_available_crud(self, ids: List[int], status: bool) -> None: """ @@ -81,4 +84,4 @@ class DeptCRUD(CRUDBase[DeptModel, DeptCreateSchema, DeptUpdateSchema]): - str | None: 部门名称,未找到返回 None。 """ obj = await self.get(id=id) - return obj.name if obj else None + return obj.name if obj else None \ No newline at end of file diff --git a/backend/app/api/v1/module_system/dept/service.py b/backend/app/api/v1/module_system/dept/service.py index 5c72e315..19b67b34 100644 --- a/backend/app/api/v1/module_system/dept/service.py +++ b/backend/app/api/v1/module_system/dept/service.py @@ -136,6 +136,13 @@ class DeptService: dept = await DeptCRUD(auth).get_by_id_crud(id=id) if not dept: raise CustomException(msg='删除失败,该部门不存在') + # 校验是否存在子级部门,存在则禁止删除 + dept_list = await DeptCRUD(auth).get_list_crud() + id_map = get_child_id_map(model_list=dept_list) + for id in ids: + descendants = get_child_recursion(id=id, id_map=id_map) + if len(descendants) > 1: + raise CustomException(msg='删除失败,存在子级部门,请先删除子级部门') await DeptCRUD(auth).delete(ids=ids) @classmethod diff --git a/backend/app/api/v1/module_system/dict/crud.py b/backend/app/api/v1/module_system/dict/crud.py index 0696a145..b2d4e31b 100644 --- a/backend/app/api/v1/module_system/dict/crud.py +++ b/backend/app/api/v1/module_system/dict/crud.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from typing import Dict, List, Optional, Sequence +from typing import Dict, List, Optional, Sequence, Union, Any from app.core.base_crud import CRUDBase from app.api.v1.module_system.dict.model import DictDataModel, DictTypeModel @@ -21,30 +21,32 @@ class DictTypeCRUD(CRUDBase[DictTypeModel, DictTypeCreateSchema, DictTypeUpdateS self.auth = auth super().__init__(model=DictTypeModel, auth=auth) - async def get_obj_by_id_crud(self, id: int) -> Optional[DictTypeModel]: + async def get_obj_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[DictTypeModel]: """ 获取数据字典类型详情 参数: - id (int): 数据字典类型ID + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Optional[DictTypeModel]: 数据字典类型模型,如果不存在则为None """ - return await self.get(id=id) + return await self.get(id=id, preload=preload) - async def get_obj_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[DictTypeModel]: + async def get_obj_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[DictTypeModel]: """ 获取数据字典类型列表 参数: - search (Optional[Dict]): 查询参数,默认值为None - order_by (Optional[List[Dict[str, str]]]): 排序参数,默认值为None + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Sequence[DictTypeModel]: 数据字典类型模型序列 """ - return await self.list(search=search, order_by=order_by) + return await self.list(search=search, order_by=order_by, preload=preload) async def create_obj_crud(self, data: DictTypeCreateSchema) -> Optional[DictTypeModel]: """ @@ -110,30 +112,32 @@ class DictDataCRUD(CRUDBase[DictDataModel, DictDataCreateSchema, DictDataUpdateS self.auth = auth super().__init__(model=DictDataModel, auth=auth) - async def get_obj_by_id_crud(self, id: int) -> Optional[DictDataModel]: + async def get_obj_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[DictDataModel]: """ 获取数据字典数据详情 参数: - id (int): 数据字典数据ID + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Optional[DictDataModel]: 数据字典数据模型,如果不存在则为None """ - return await self.get(id=id) + return await self.get(id=id, preload=preload) - async def get_obj_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[DictDataModel]: + async def get_obj_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[DictDataModel]: """ 获取数据字典数据列表 参数: - search (Optional[Dict]): 查询参数,默认值为None - order_by (Optional[List[Dict[str, str]]]): 排序参数,默认值为None + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Sequence[DictDataModel]: 数据字典数据模型序列 """ - return await self.list(search=search, order_by=order_by) + return await self.list(search=search, order_by=order_by, preload=preload) async def create_obj_crud(self, data: DictDataCreateSchema) -> Optional[DictDataModel]: """ diff --git a/backend/app/api/v1/module_system/dict/model.py b/backend/app/api/v1/module_system/dict/model.py index 11639ea3..27755f58 100644 --- a/backend/app/api/v1/module_system/dict/model.py +++ b/backend/app/api/v1/module_system/dict/model.py @@ -14,6 +14,7 @@ class DictTypeModel(CreatorMixin): __tablename__ = "system_dict_type" __table_args__ = ({'comment': '字典类型表'}) + __loader_options__ = ["creator"] dict_name: Mapped[str] = mapped_column(String(100), nullable=False, unique=True, comment='字典名称') dict_type: Mapped[str] = mapped_column(String(100), nullable=False, unique=True, comment='字典类型') @@ -28,6 +29,7 @@ class DictDataModel(CreatorMixin): __tablename__ = 'system_dict_data' __table_args__ = ({'comment': '字典数据表'}) + __loader_options__ = ["creator"] dict_sort: Mapped[int] = mapped_column(Integer, nullable=False, default=0, comment='字典排序') dict_label: Mapped[str] = mapped_column(String(100), nullable=False, comment='字典标签') diff --git a/backend/app/api/v1/module_system/dict/service.py b/backend/app/api/v1/module_system/dict/service.py index 0f4e7406..efa48258 100644 --- a/backend/app/api/v1/module_system/dict/service.py +++ b/backend/app/api/v1/module_system/dict/service.py @@ -378,27 +378,16 @@ class DictDataService: - Dict: 数据字典数据详情字典 """ exist_obj = await DictDataCRUD(auth).get_obj_by_id_crud(id=id) - if not exist_obj: - raise CustomException(msg='更新失败,该字典数据不存在') - exist_obj = await DictDataCRUD(auth).get(dict_label=data.dict_label) if not exist_obj: raise CustomException(msg='更新失败,该字典数据不存在') if exist_obj.id != id: raise CustomException(msg='更新失败,数据字典数据重复') - # 如果状态变更,需要同步更新字典类型状态并刷新缓存 - if exist_obj.status != data.status or exist_obj.dict_type != data.dict_type: + # 如果字典类型变更,仅刷新旧类型缓存,不联动字典类型状态 + if exist_obj.dict_type != data.dict_type: dict_type = await DictTypeCRUD(auth).get(dict_type=exist_obj.dict_type) if dict_type: - update_data = DictTypeUpdateSchema( - dict_name=dict_type.dict_name, - dict_type=dict_type.dict_type, - status=data.status, - description=dict_type.description - ) - await DictTypeCRUD(auth).update_obj_crud(id=dict_type.id, data=update_data) - # 刷新Redis缓存 redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{dict_type.dict_type}" try: dict_data_list = await DictDataCRUD(auth).get_obj_list_crud(search={'dict_type': dict_type.dict_type}) @@ -409,7 +398,7 @@ class DictDataService: value=value, ) except Exception as e: - logger.error(f"更新字典数据状态时刷新缓存失败: {e}") + logger.error(f"更新字典数据类型变更时刷新旧缓存失败: {e}") obj = await DictDataCRUD(auth).update_obj_crud(id=id, data=data) redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{data.dict_type}" @@ -451,6 +440,9 @@ class DictDataService: exist_obj = await DictDataCRUD(auth).get_obj_by_id_crud(id=id) if not exist_obj: raise CustomException(msg=f'{id} 删除失败,该字典数据不存在') + # 新增:系统默认字典数据不允许删除(通过 is_default 判断) + if exist_obj.is_default: + raise CustomException(msg='删除失败,系统默认字典数据不允许删除') # 删除Redis缓存 redis_key = f"{RedisInitKeyConfig.SYSTEM_DICT.key}:{exist_obj.dict_type}" try: diff --git a/backend/app/api/v1/module_system/log/crud.py b/backend/app/api/v1/module_system/log/crud.py index b35b0800..f442dba3 100644 --- a/backend/app/api/v1/module_system/log/crud.py +++ b/backend/app/api/v1/module_system/log/crud.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from typing import Dict, List, Optional, Sequence +from typing import Dict, List, Optional, Sequence, Union, Any from app.core.base_crud import CRUDBase from ..auth.schema import AuthSchema @@ -32,28 +32,29 @@ class OperationLogCRUD(CRUDBase[OperationLogModel, OperationLogCreateSchema, Ope """ return await self.create(data=data) - async def get_by_id_crud(self, id: int) -> Optional[OperationLogModel]: + async def get_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[OperationLogModel]: """ 根据ID获取操作日志详情。 参数: - id (int): 操作日志ID。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - OperationLogModel | None: 操作日志记录。 """ - return await self.get(id=id) + return await self.get(id=id, preload=preload) - async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[OperationLogModel]: + async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[OperationLogModel]: """ 获取操作日志列表。 参数: - search (Dict | None): 搜索条件字典。 - order_by (List[Dict[str, str]] | None): 排序字段列表。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Sequence[OperationLogModel]: 操作日志列表。 """ - return await self.list(search=search, order_by=order_by) - + return await self.list(search=search, order_by=order_by, preload=preload) \ No newline at end of file diff --git a/backend/app/api/v1/module_system/log/model.py b/backend/app/api/v1/module_system/log/model.py index ef99ad4d..e5a3d976 100644 --- a/backend/app/api/v1/module_system/log/model.py +++ b/backend/app/api/v1/module_system/log/model.py @@ -13,6 +13,7 @@ class OperationLogModel(CreatorMixin): """ __tablename__ = "system_log" __table_args__ = ({'comment': '系统日志表'}) + __loader_options__ = ["creator"] type: Mapped[int] = mapped_column(Integer, comment="日志类型(1登录日志 2操作日志)") request_path: Mapped[str] = mapped_column(String(255), comment="请求路径") diff --git a/backend/app/api/v1/module_system/menu/crud.py b/backend/app/api/v1/module_system/menu/crud.py index d647d3d9..d7bd1489 100644 --- a/backend/app/api/v1/module_system/menu/crud.py +++ b/backend/app/api/v1/module_system/menu/crud.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from typing import Dict, List, Optional, Sequence +from typing import Dict, List, Optional, Sequence, Union, Any from app.core.base_crud import CRUDBase from ..auth.schema import AuthSchema @@ -16,46 +16,49 @@ class MenuCRUD(CRUDBase[MenuModel, MenuCreateSchema, MenuUpdateSchema]): self.auth = auth super().__init__(model=MenuModel, auth=auth) - async def get_by_id_crud(self, id: int) -> Optional[MenuModel]: + async def get_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[MenuModel]: """ 根据 id 获取菜单信息。 参数: - id (int): 菜单 ID。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - MenuModel | None: 菜单信息,未找到返回 None。 """ - obj = await self.get(id=id) + obj = await self.get(id=id, preload=preload) if not obj: return None return obj - async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[MenuModel]: + async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[MenuModel]: """ 获取菜单列表。 参数: - search (Dict | None): 搜索条件。 - order_by (List[Dict[str, str]] | None): 排序字段列表。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Sequence[MenuModel]: 菜单列表。 """ - return await self.list(search=search, order_by=order_by) + return await self.list(search=search, order_by=order_by, preload=preload) - async def get_tree_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[MenuModel]: + async def get_tree_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[MenuModel]: """ 获取菜单树形列表。 参数: - search (Dict | None): 搜索条件。 - order_by (List[Dict[str, str]] | None): 排序字段列表。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Sequence[MenuModel]: 菜单树形列表。 """ - return await self.tree_list(search=search, order_by=order_by, children_attr='children') + return await self.tree_list(search=search, order_by=order_by, children_attr='children', preload=preload) async def set_available_crud(self, ids: List[int], status: bool) -> None: """ @@ -68,4 +71,4 @@ class MenuCRUD(CRUDBase[MenuModel, MenuCreateSchema, MenuUpdateSchema]): 返回: - None """ - await self.set(ids=ids, status=status) + await self.set(ids=ids, status=status) \ No newline at end of file diff --git a/backend/app/api/v1/module_system/menu/model.py b/backend/app/api/v1/module_system/menu/model.py index 4b495459..a396a7dc 100644 --- a/backend/app/api/v1/module_system/menu/model.py +++ b/backend/app/api/v1/module_system/menu/model.py @@ -4,13 +4,16 @@ 定义系统菜单相关数据模型 """ -from typing import Optional, List +from typing import Optional, List, TYPE_CHECKING from sqlalchemy import Boolean, String, Integer, JSON, ForeignKey from sqlalchemy.orm import relationship, Mapped, mapped_column from app.core.base_model import ModelMixin +if TYPE_CHECKING: + from app.api.v1.module_system.role.model import RoleModel + class MenuModel(ModelMixin): """ @@ -24,6 +27,8 @@ class MenuModel(ModelMixin): """ __tablename__ = "system_menu" __table_args__ = ({'comment': '菜单表'}) + __loader_options__ = ["roles"] + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment='主键ID') name: Mapped[str] = mapped_column(String(50), nullable=False, comment='菜单名称', unique=True) type: Mapped[int] = mapped_column(Integer, nullable=False, default=2, comment='菜单类型(1:目录 2:菜单 3:按钮/权限 4:链接)') @@ -44,7 +49,7 @@ class MenuModel(ModelMixin): parent_id: Mapped[Optional[int]] = mapped_column(Integer, ForeignKey('system_menu.id', ondelete='SET NULL'), default=None, index=True, comment='父菜单ID') parent: Mapped[Optional['MenuModel']] = relationship(back_populates='children', remote_side=[id], uselist=False) - children: Mapped[Optional[List['MenuModel']]] = relationship(back_populates='parent') + children: Mapped[Optional[List['MenuModel']]] = relationship(back_populates='parent', order_by="MenuModel.order") # 角色关联关系 roles: Mapped[List["RoleModel"]] = relationship(secondary="system_role_menus", back_populates="menus", lazy="selectin") diff --git a/backend/app/api/v1/module_system/menu/service.py b/backend/app/api/v1/module_system/menu/service.py index cb29d01d..e33bdde7 100644 --- a/backend/app/api/v1/module_system/menu/service.py +++ b/backend/app/api/v1/module_system/menu/service.py @@ -137,6 +137,13 @@ class MenuService: menu = await MenuCRUD(auth).get_by_id_crud(id=id) if not menu: raise CustomException(msg='删除失败,该菜单不存在') + # 校验是否存在子级菜单,存在则禁止删除 + menu_list = await MenuCRUD(auth).get_list_crud() + id_map = get_child_id_map(model_list=menu_list) + for id in ids: + descendants = get_child_recursion(id=id, id_map=id_map) + if len(descendants) > 1: + raise CustomException(msg='删除失败,存在子级菜单,请先删除子级菜单') await MenuCRUD(auth).delete(ids=ids) @classmethod diff --git a/backend/app/api/v1/module_system/notice/crud.py b/backend/app/api/v1/module_system/notice/crud.py index a88efda8..5db44a80 100644 --- a/backend/app/api/v1/module_system/notice/crud.py +++ b/backend/app/api/v1/module_system/notice/crud.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from typing import Dict, List, Optional, Sequence +from typing import Dict, List, Optional, Sequence, Union, Any from app.core.base_crud import CRUDBase from ..auth.schema import AuthSchema @@ -21,30 +21,32 @@ class NoticeCRUD(CRUDBase[NoticeModel, NoticeCreateSchema, NoticeUpdateSchema]): self.auth = auth super().__init__(model=NoticeModel, auth=auth) - async def get_by_id_crud(self, id: int) -> Optional[NoticeModel]: + async def get_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[NoticeModel]: """ 根据ID获取公告详情。 参数: - id (int): 公告ID。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Optional[NoticeModel]: 公告模型实例。 """ - return await self.get(id=id) + return await self.get(id=id, preload=preload) - async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[NoticeModel]: + async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[NoticeModel]: """ 获取公告列表。 参数: - search (Optional[Dict]): 查询参数。 - order_by (Optional[List[Dict[str, str]]]): 排序参数。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Sequence[NoticeModel]: 公告模型实例列表。 """ - return await self.list(search=search, order_by=order_by) + return await self.list(search=search, order_by=order_by, preload=preload) async def create_crud(self, data: NoticeCreateSchema) -> Optional[NoticeModel]: """ diff --git a/backend/app/api/v1/module_system/notice/model.py b/backend/app/api/v1/module_system/notice/model.py index 3f2bba0d..a99bf6b8 100644 --- a/backend/app/api/v1/module_system/notice/model.py +++ b/backend/app/api/v1/module_system/notice/model.py @@ -22,6 +22,7 @@ class NoticeModel(CreatorMixin): """ __tablename__ = "system_notice" __table_args__ = ({'comment': '通知公告表'}) + __loader_options__ = ["creator"] notice_title: Mapped[str] = mapped_column(String(50), nullable=False, comment='公告标题') notice_type: Mapped[str] = mapped_column(String(50), nullable=False, comment='公告类型(1通知 2公告)') diff --git a/backend/app/api/v1/module_system/params/crud.py b/backend/app/api/v1/module_system/params/crud.py index 1bb70ca0..eaa78e7e 100644 --- a/backend/app/api/v1/module_system/params/crud.py +++ b/backend/app/api/v1/module_system/params/crud.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from typing import Dict, List, Optional, Sequence +from typing import Dict, List, Optional, Sequence, Union, Any from app.core.base_crud import CRUDBase from ..auth.schema import AuthSchema @@ -21,42 +21,45 @@ class ParamsCRUD(CRUDBase[ParamsModel, ParamsCreateSchema, ParamsUpdateSchema]): self.auth = auth super().__init__(model=ParamsModel, auth=auth) - async def get_obj_by_id_crud(self, id: int) -> Optional[ParamsModel]: + async def get_obj_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[ParamsModel]: """ 获取配置管理型详情 参数: - id (int): 配置管理型ID + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Optional[ParamsModel]: 配置管理型模型实例 """ - return await self.get(id=id) + return await self.get(id=id, preload=preload) - async def get_obj_by_key_crud(self, key: str) -> Optional[ParamsModel]: + async def get_obj_by_key_crud(self, key: str, preload: Optional[List[Union[str, Any]]] = None) -> Optional[ParamsModel]: """ 根据key获取配置管理型详情 参数: - key (str): 配置管理型key + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Optional[ParamsModel]: 配置管理型模型实例 """ - return await self.get(config_key=key) + return await self.get(config_key=key, preload=preload) - async def get_obj_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[ParamsModel]: + async def get_obj_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[ParamsModel]: """ 获取配置管理型列表 参数: - search (Dict | None): 查询参数对象。 - order_by (List[Dict[str, str]] | None): 排序参数列表。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Sequence[ParamsModel]: 配置管理型模型实例列表 """ - return await self.list(search=search, order_by=order_by) + return await self.list(search=search, order_by=order_by, preload=preload) async def create_obj_crud(self, data: ParamsCreateSchema) -> Optional[ParamsModel]: """ @@ -93,4 +96,4 @@ class ParamsCRUD(CRUDBase[ParamsModel, ParamsCreateSchema, ParamsUpdateSchema]): 返回: - None """ - return await self.delete(ids=ids) + return await self.delete(ids=ids) \ No newline at end of file diff --git a/backend/app/api/v1/module_system/params/model.py b/backend/app/api/v1/module_system/params/model.py index 2007768d..09ee0209 100644 --- a/backend/app/api/v1/module_system/params/model.py +++ b/backend/app/api/v1/module_system/params/model.py @@ -13,6 +13,7 @@ class ParamsModel(CreatorMixin): """ __tablename__ = "system_param" __table_args__ = ({'comment': '系统参数表'}) + __loader_options__ = ["creator"] # 基础字段 config_name: Mapped[str] = mapped_column(String(500), nullable=False, unique=True, comment='参数名称') diff --git a/backend/app/api/v1/module_system/position/crud.py b/backend/app/api/v1/module_system/position/crud.py index 996cafbe..5420c006 100644 --- a/backend/app/api/v1/module_system/position/crud.py +++ b/backend/app/api/v1/module_system/position/crud.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from typing import Dict, List, Optional, Sequence +from typing import Dict, List, Optional, Sequence, Union, Any from app.core.base_crud import CRUDBase from ..auth.schema import AuthSchema @@ -21,30 +21,32 @@ class PositionCRUD(CRUDBase[PositionModel, PositionCreateSchema, PositionUpdateS self.auth = auth super().__init__(model=PositionModel, auth=auth) - async def get_by_id_crud(self, id: int) -> Optional[PositionModel]: + async def get_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[PositionModel]: """ 根据 id 获取岗位信息。 参数: - id (int): 岗位 ID。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - PositionModel | None: 岗位信息,未找到返回 None。 """ - return await self.get(id=id) + return await self.get(id=id, preload=preload) - async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[PositionModel]: + async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[PositionModel]: """ 获取岗位列表。 参数: - search (Dict | None): 搜索条件。 - order_by (List[Dict[str, str]] | None): 排序字段列表。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Sequence[PositionModel]: 岗位列表。 """ - return await self.list(search=search, order_by=order_by) + return await self.list(search=search, order_by=order_by, preload=preload) async def set_available_crud(self, ids: List[int], status: bool) -> None: """ @@ -74,4 +76,4 @@ class PositionCRUD(CRUDBase[PositionModel, PositionCreateSchema, PositionUpdateS obj = await self.get(id=id) if obj: position_names.append(obj.name) - return position_names + return position_names \ No newline at end of file diff --git a/backend/app/api/v1/module_system/position/model.py b/backend/app/api/v1/module_system/position/model.py index 4e26a3d1..2c195aac 100644 --- a/backend/app/api/v1/module_system/position/model.py +++ b/backend/app/api/v1/module_system/position/model.py @@ -18,6 +18,7 @@ class PositionModel(CreatorMixin): """ __tablename__ = "system_position" __table_args__ = ({'comment': '岗位表'}) + __loader_options__ = ["creator"] name: Mapped[str] = mapped_column(String(40), nullable=False, unique=True, comment="岗位名称") order: Mapped[int] = mapped_column(Integer, nullable=False, default=1, comment="显示排序") diff --git a/backend/app/api/v1/module_system/role/crud.py b/backend/app/api/v1/module_system/role/crud.py index 7ed42a1c..b8938e97 100644 --- a/backend/app/api/v1/module_system/role/crud.py +++ b/backend/app/api/v1/module_system/role/crud.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -from typing import Dict, List, Sequence, Optional +from typing import Dict, List, Sequence, Optional, Union, Any from app.core.base_crud import CRUDBase from .model import RoleModel @@ -23,30 +23,32 @@ class RoleCRUD(CRUDBase[RoleModel, RoleCreateSchema, RoleUpdateSchema]): self.auth = auth super().__init__(model=RoleModel, auth=auth) - async def get_by_id_crud(self, id: int) -> Optional[RoleModel]: + async def get_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[RoleModel]: """ 根据id获取角色信息 参数: - id (int): 角色ID + - preload (Optional[List[Union[str, Any]]]): 预加载选项 返回: - Optional[RoleModel]: 角色模型对象 """ - return await self.get(id=id) + return await self.get(id=id, preload=preload) - async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[RoleModel]: + async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[RoleModel]: """ 获取角色列表 参数: - search (Optional[Dict]): 查询参数 - order_by (Optional[List[Dict[str, str]]]): 排序参数 + - preload (Optional[List[Union[str, Any]]]): 预加载选项 返回: - Sequence[RoleModel]: 角色模型对象列表 """ - return await self.list(search=search, order_by=order_by) + return await self.list(search=search, order_by=order_by, preload=preload) async def set_role_menus_crud(self, role_ids: List[int], menu_ids: List[int]) -> None: """ @@ -112,4 +114,4 @@ class RoleCRUD(CRUDBase[RoleModel, RoleCreateSchema, RoleUpdateSchema]): 返回: - None """ - await self.set(ids=ids, status=status) + await self.set(ids=ids, status=status) \ No newline at end of file diff --git a/backend/app/api/v1/module_system/role/model.py b/backend/app/api/v1/module_system/role/model.py index fd847468..32c2c638 100644 --- a/backend/app/api/v1/module_system/role/model.py +++ b/backend/app/api/v1/module_system/role/model.py @@ -4,13 +4,18 @@ 定义角色相关数据模型和关联表 """ -from typing import Optional, List +from typing import Optional, List, TYPE_CHECKING from sqlalchemy import Boolean, String, Integer, ForeignKey from sqlalchemy.orm import relationship, Mapped, mapped_column from app.core.base_model import MappedBase, CreatorMixin +if TYPE_CHECKING: + from app.api.v1.module_system.menu.model import MenuModel + from app.api.v1.module_system.dept.model import DeptModel + from app.api.v1.module_system.user.model import UserModel + class RoleMenusModel(MappedBase): """ @@ -71,6 +76,7 @@ class RoleModel(CreatorMixin): """ __tablename__ = "system_role" __table_args__ = ({'comment': '角色表'}) + __loader_options__ = ["menus", "depts", "creator"] name: Mapped[str] = mapped_column(String(40), nullable=False, unique=True, comment="角色名称") code: Mapped[Optional[str]] = mapped_column(String(20), nullable=True, unique=True, comment="角色编码") @@ -78,7 +84,7 @@ class RoleModel(CreatorMixin): status: Mapped[bool] = mapped_column(Boolean(), default=True, nullable=False, comment="是否启用(True:启用 False:禁用)") data_scope: Mapped[int] = mapped_column(Integer, nullable=False, default=1, comment="数据权限范围") - menus: Mapped[List["MenuModel"]] = relationship(secondary="system_role_menus", back_populates="roles", lazy="selectin") + menus: Mapped[List["MenuModel"]] = relationship(secondary="system_role_menus", back_populates="roles", lazy="selectin", order_by="MenuModel.order") depts: Mapped[List["DeptModel"]] = relationship(secondary="system_role_depts", back_populates="roles", lazy="selectin") users: Mapped[List["UserModel"]] = relationship(secondary="system_user_roles", back_populates="roles", lazy="selectin") diff --git a/backend/app/api/v1/module_system/user/crud.py b/backend/app/api/v1/module_system/user/crud.py index 4bfd5cca..02bdedfd 100644 --- a/backend/app/api/v1/module_system/user/crud.py +++ b/backend/app/api/v1/module_system/user/crud.py @@ -1,9 +1,8 @@ # -*- coding: utf-8 -*- -from typing import Dict, List, Sequence, Optional +from typing import Dict, List, Optional, Sequence, Union, Any from datetime import datetime - from app.core.base_crud import CRUDBase from .model import UserModel from .schema import UserCreateSchema, UserForgetPasswordSchema, UserUpdateSchema @@ -26,54 +25,73 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]): self.auth = auth super().__init__(model=UserModel, auth=auth) - async def get_by_id_crud(self, id: int) -> Optional[UserModel]: + async def get_by_id_crud(self, id: int, preload: Optional[List[Union[str, Any]]] = None) -> Optional[UserModel]: """ 根据id获取用户信息 参数: - id (int): 用户ID + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Optional[UserModel]: 用户信息,如果不存在则为None """ - return await self.get(id=id) + return await self.get( + preload=preload, + id=id, + ) - async def get_by_username_crud(self, username: str) -> Optional[UserModel]: + async def get_by_username_crud(self, username: str, preload: Optional[List[Union[str, Any]]] = None) -> Optional[UserModel]: """ 根据用户名获取用户信息 参数: - username (str): 用户名 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Optional[UserModel]: 用户信息,如果不存在则为None """ - return await self.get(username=username) + return await self.get( + preload=preload, + username=username, + ) - async def get_by_mobile_crud(self, mobile: str) -> Optional[UserModel]: + + + async def get_by_mobile_crud(self, mobile: str, preload: Optional[List[Union[str, Any]]] = None) -> Optional[UserModel]: """ 根据手机号获取用户信息 参数: - mobile (str): 手机号 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: - Optional[UserModel]: 用户信息,如果不存在则为None """ - return await self.get(mobile=mobile) + return await self.get( + preload=preload, + mobile=mobile, + ) - async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[UserModel]: + async def get_list_crud(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[UserModel]: """ 获取用户列表 参数: - search (Dict | None): 查询参数对象。 - order_by (List[Dict[str, str]] | None): 排序参数列表。 + - preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项 返回: Sequence[UserModel]: 用户列表 """ - return await self.list(search=search, order_by=order_by) + return await self.list( + search=search, + order_by=order_by, + preload=preload, + ) async def update_last_login_crud(self, id: int) -> Optional[UserModel]: """ @@ -187,5 +205,4 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]): """ if await self.get_by_username_crud(username=data.username): return None - - return await self.create(data=UserCreateSchema(**data.model_dump())) + return await self.create(data=UserCreateSchema(**data.model_dump())) \ No newline at end of file diff --git a/backend/app/api/v1/module_system/user/model.py b/backend/app/api/v1/module_system/user/model.py index 0e59642d..90d53643 100644 --- a/backend/app/api/v1/module_system/user/model.py +++ b/backend/app/api/v1/module_system/user/model.py @@ -13,7 +13,7 @@ from sqlalchemy.orm import relationship, Mapped, mapped_column from app.api.v1.module_system.dept.model import DeptModel from app.api.v1.module_system.position.model import PositionModel from app.api.v1.module_system.role.model import RoleModel -from app.core.base_model import MappedBase, CreatorMixin +from app.core.base_model import MappedBase class UserRolesModel(MappedBase): @@ -68,6 +68,7 @@ class UserModel(MappedBase): """ __tablename__ = "system_users" __table_args__ = ({'comment': '用户表'}) + __loader_options__ = ["dept", "roles", "positions", "creator"] id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment='主键ID') diff --git a/backend/app/api/v1/module_system/user/schema.py b/backend/app/api/v1/module_system/user/schema.py index 8eee6179..cebdd2ca 100644 --- a/backend/app/api/v1/module_system/user/schema.py +++ b/backend/app/api/v1/module_system/user/schema.py @@ -27,7 +27,7 @@ class UserRegisterSchema(BaseModel): mobile: Optional[str] = Field(default=None, description="手机号") username: str = Field(..., max_length=32, description="账号") password: str = Field(..., max_length=128, description="密码哈希值") - role_ids: Optional[List[int]] = Field(default=[2], description='角色ID') + role_ids: Optional[List[int]] = Field(default=[1], description='角色ID') creator_id: Optional[int] = Field(default=1, description='创建人ID') description: Optional[str] = Field(default=None, max_length=255, description="备注") diff --git a/backend/app/api/v1/module_system/user/service.py b/backend/app/api/v1/module_system/user/service.py index 82a00035..6c885316 100644 --- a/backend/app/api/v1/module_system/user/service.py +++ b/backend/app/api/v1/module_system/user/service.py @@ -77,11 +77,6 @@ class UserService: user_list = await UserCRUD(auth).get_list_crud(search=search.__dict__, order_by=order_by) user_dict_list = [] for user in user_list: - if user.dept_id: - dept = await DeptCRUD(auth).get_by_id_crud(id=user.dept_id) - UserOutSchema.dept_name = dept.name if dept else None - else: - UserOutSchema.dept_name = None user_dict = UserOutSchema.model_validate(user).model_dump() user_dict_list.append(user_dict) @@ -101,6 +96,9 @@ class UserService: """ if not data.username: raise CustomException(msg="用户名不能为空") + # 检查是否试图创建超级管理员 + if data.is_superuser: + raise CustomException(msg='不允许创建超级管理员') # 检查用户名是否存在 user = await UserCRUD(auth).get_by_username_crud(username=data.username) if user: @@ -142,17 +140,34 @@ class UserService: """ if not data.username: raise CustomException(msg="用户名不能为空") + # 检查是否是超级管理员 + if data.is_superuser: + raise CustomException(msg='超级管理员系统唯一') # 检查用户是否存在 user = await UserCRUD(auth).get_by_id_crud(id=id) if not user: raise CustomException(msg='用户不存在') + # 检查是否尝试修改超级管理员 + if user.is_superuser: + raise CustomException(msg='超级管理员不允许修改') + # 检查用户名是否重复 exist_user = await UserCRUD(auth).get_by_username_crud(username=data.username) if exist_user and exist_user.id != id: raise CustomException(msg='已存在相同的用户名') - + # 新增:检查手机号是否重复 + if data.mobile: + exist_mobile_user = await UserCRUD(auth).get_by_mobile_crud(mobile=data.mobile) + if exist_mobile_user and exist_mobile_user.id != id: + raise CustomException(msg='更新失败,手机号已存在') + # 新增:检查邮箱是否重复 + if data.email: + exist_email_user = await UserCRUD(auth).get(email=data.email) + if exist_email_user and exist_email_user.id != id: + raise CustomException(msg='更新失败,邮箱已存在') # 检查部门是否存在且可用 + if data.dept_id: dept = await DeptCRUD(auth).get_by_id_crud(id=data.dept_id) if not dept: @@ -167,7 +182,8 @@ class UserService: # 更新用户 # user_dict = data.model_dump(exclude_unset=True, exclude={"role_ids", "position_ids"}) # new_user = await UserCRUD(auth).update(id=id, data=user_dict) - new_user = await UserCRUD(auth).update(id=id, data=data) + user_dict = data.model_dump(exclude_unset=True, exclude={"role_ids", "position_ids", "last_login", "password"}) + new_user = await UserCRUD(auth).update(id=id, data=user_dict) # 更新角色和岗位 if data.role_ids and len(data.role_ids) > 0: @@ -248,7 +264,7 @@ class UserService: # 获取菜单权限 if auth.user and auth.user.is_superuser: # 使用树形结构查询,预加载children关系 - menu_all = await MenuCRUD(auth).get_tree_list_crud(search={'type': ('in', [1, 2, 4]), 'status': True}) + menu_all = await MenuCRUD(auth).get_tree_list_crud(search={'type': ('in', [1, 2, 4]), 'status': True}, order_by=[{"order": "asc"}]) menus = [MenuOutSchema.model_validate(menu).model_dump() for menu in menu_all] else: @@ -263,7 +279,7 @@ class UserService: # 使用树形结构查询,预加载children关系 menus = [ MenuOutSchema.model_validate(menu).model_dump() - for menu in await MenuCRUD(auth).get_tree_list_crud(search={'id': ('in', list(menu_ids))}) + for menu in await MenuCRUD(auth).get_tree_list_crud(search={'id': ('in', list(menu_ids))}, order_by=[{"order": "asc"}]) ] if menu_ids else [] user_dict["menus"] = traversal_to_tree(menus) return user_dict @@ -285,6 +301,18 @@ class UserService: user = await UserCRUD(auth).get_by_id_crud(id=auth.user.id) if not user: raise CustomException(msg="用户不存在") + if user.is_superuser: + raise CustomException(msg="超级管理员不能修改个人信息") + # 新增:检查手机号是否重复 + if data.mobile: + exist_mobile_user = await UserCRUD(auth).get_by_mobile_crud(mobile=data.mobile) + if exist_mobile_user and exist_mobile_user.id != auth.user.id: + raise CustomException(msg='更新失败,手机号已存在') + # 新增:检查邮箱是否重复 + if data.email: + exist_email_user = await UserCRUD(auth).get(email=data.email) + if exist_email_user and exist_email_user.id != auth.user.id: + raise CustomException(msg='更新失败,邮箱已存在') user_update_data = UserUpdateSchema(**data.model_dump()) new_user = await UserCRUD(auth).update(id=auth.user.id, data=user_update_data) return UserOutSchema.model_validate(new_user).model_dump() @@ -380,6 +408,10 @@ class UserService: user = await UserCRUD(auth).get_by_id_crud(id=data.id) if not user: raise CustomException(msg="用户不存在") + + # 检查是否是超级管理员 + if user.is_superuser: + raise CustomException(msg="超级管理员密码不能重置") # 更新密码 new_password_hash = PwdUtil.set_password_hash(password=data.password) @@ -405,14 +437,10 @@ class UserService: data.password = PwdUtil.set_password_hash(password=data.password) data.name = data.username - data.creator_id = 1 - # dict_data = data.model_dump(exclude_unset=True) - # result = await UserCRUD(auth).create(data=dict_data) - user_create_data = UserCreateSchema(**data.model_dump()) - result = await UserCRUD(auth).create(data=user_create_data) + create_dict = data.model_dump(exclude_unset=True, exclude={"role_ids", "position_ids"}) + result = await UserCRUD(auth).create(data=create_dict) if data.role_ids: await UserCRUD(auth).set_user_roles_crud(user_ids=[result.id], role_ids=data.role_ids) - # await UserCRUD(auth).set_user_positions_crud(user_ids=[result.id], position_ids=data.position_ids) return UserOutSchema.model_validate(result).model_dump() @classmethod @@ -432,6 +460,11 @@ class UserService: raise CustomException(msg="用户不存在") if not user.status: raise CustomException(msg="用户已停用") + + # 检查是否是超级管理员 + if user.is_superuser: + raise CustomException(msg="超级管理员密码不能重置") + new_password_hash = PwdUtil.set_password_hash(password=data.new_password) new_user = await UserCRUD(auth).forget_password_crud(id=user.id, password_hash=new_password_hash) return UserOutSchema.model_validate(new_user).model_dump() @@ -510,6 +543,10 @@ class UserService: # 处理用户导入 exists_user = await UserCRUD(auth).get_by_username_crud(username=user_data["username"]) if exists_user: + # 检查是否是超级管理员 + if exists_user.is_superuser: + error_msgs.append(f"第{count}行: 超级管理员不允许修改") + continue if update_support: user_update_data = UserUpdateSchema(**user_data) await UserCRUD(auth).update(id=exists_user.id, data=user_update_data) diff --git a/backend/app/core/ap_scheduler.py b/backend/app/core/ap_scheduler.py index aee30735..23357576 100644 --- a/backend/app/core/ap_scheduler.py +++ b/backend/app/core/ap_scheduler.py @@ -15,12 +15,9 @@ from apscheduler.jobstores.redis import RedisJobStore from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.date import DateTrigger from apscheduler.triggers.interval import IntervalTrigger -# from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore -# from apscheduler.jobstores.mongodb import MongoDBJobStore -# from apscheduler.jobstores.base import JobLookupError, ConflictingIdError -# from apscheduler.schedulers.background import BackgroundScheduler -# from apscheduler.triggers.combining import OrTrigger -# from pymongo import MongoClient +import asyncio +import threading +from concurrent.futures import ThreadPoolExecutor from app.config.setting import settings from app.core.database import SessionLocal, AsyncSessionLocal @@ -31,13 +28,13 @@ from app.core.logger import logger job_stores = { 'default': MemoryJobStore(), # 'sqlalchemy': SQLAlchemyJobStore(url=settings.DB_URI, engine=engine), 如果用同一个数据库会有lock冲突 - 'redis': RedisJobStore(**dict( + 'redis': RedisJobStore( host=settings.REDIS_HOST, - port=settings.REDIS_PORT, + port=int(settings.REDIS_PORT), username=settings.REDIS_USER, password=settings.REDIS_PASSWORD, - db=settings.REDIS_DB_NAME, - )), + db=int(settings.REDIS_DB_NAME), + ), } # 配置执行器 executors = { @@ -75,7 +72,7 @@ class SchedulerUtil: - None """ # 延迟导入避免循环导入 - from app.api.v1.module_application.job.schema import JobLogCreateSchema + from app.api.v1.module_application.job.model import JobLogModel # 获取事件类型和任务ID event_type = event.__class__.__name__ @@ -106,7 +103,9 @@ class SchedulerUtil: job_trigger = str(query_job_info.get('trigger')) # 构造日志消息 job_message = f"事件类型: {event_type}, 任务ID: {job_id}, 任务名称: {job_name}, 状态: {status}, 任务组: {job_group}, 错误详情: {exception_info}, 执行于{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}" - job_log = JobLogCreateSchema( + + # 创建ORM对象 + job_log = JobLogModel( job_name=job_name, job_group=job_group, job_executor=job_executor, @@ -119,10 +118,44 @@ class SchedulerUtil: exception_info=exception_info, create_time=datetime.now(), ) - session = SessionLocal() - session.add(**job_log.model_dump()) - session.commit() - session.close() + + # 使用线程池执行异步操作以避免阻塞调度器和数据库锁定问题 + executor = ThreadPoolExecutor(max_workers=1) + executor.submit(cls._save_job_log_async_wrapper, job_log) + executor.shutdown(wait=False) + + @classmethod + def _save_job_log_async_wrapper(cls, job_log): + """ + 异步保存任务日志的包装器函数,在独立线程中运行 + + 参数: + - job_log (JobLogModel): 任务日志对象 + + 返回: + - None + """ + import asyncio + from app.core.database import AsyncSessionLocal + + async def _save_job_log_async(): + async with AsyncSessionLocal() as session: + try: + session.add(job_log) + await session.commit() + except Exception as e: + await session.rollback() + logger.error(f"保存任务日志失败: {str(e)}") + finally: + await session.close() + + # 创建新的事件循环 + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + loop.run_until_complete(_save_job_log_async()) + finally: + loop.close() @classmethod async def init_system_scheduler(cls): @@ -154,8 +187,14 @@ class SchedulerUtil: 返回: - None """ - scheduler.shutdown(wait=False) - logger.info('关闭定时任务成功') + try: + # 移除所有任务 + scheduler.remove_all_jobs() + # 等待所有任务完成后再关闭 + scheduler.shutdown(wait=True) + logger.info('关闭定时任务成功') + except Exception as e: + logger.error(f'关闭定时任务失败: {str(e)}') @classmethod def get_job(cls, job_id: Union[str, int]) -> Optional[Job]: diff --git a/backend/app/core/base_crud.py b/backend/app/core/base_crud.py index 7cfd8f0d..feeade26 100644 --- a/backend/app/core/base_crud.py +++ b/backend/app/core/base_crud.py @@ -6,6 +6,7 @@ from sqlalchemy.sql.elements import ColumnElement from sqlalchemy.orm import selectinload from sqlalchemy.engine import Result from sqlalchemy import asc, func, select, delete, Select, desc, update, or_, and_ +from sqlalchemy import inspect as sa_inspect from app.core.base_model import MappedBase from app.api.v1.module_system.auth.schema import AuthSchema @@ -41,50 +42,46 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): self.db = auth.db self.current_user = auth.user - async def get(self, **kwargs) -> Optional[ModelType]: + async def get(self, preload: Optional[List[Union[str, Any]]] = None, **kwargs) -> Optional[ModelType]: """ 根据条件获取单个对象 参数: + - preload (Optional[List[Union[str, Any]]]): 预加载关系,支持关系名字符串或SQLAlchemy loader option - **kwargs: 查询条件 返回: - Optional[ModelType]: 对象实例 - 返回: - - Optional[ModelType]: 对象实例 - 异常: - CustomException: 查询失败时抛出异常 """ try: conditions = await self.__build_conditions(**kwargs) sql = select(self.model).where(*conditions) - # 只有继承自CreatorMixin的模型才有creator关系 - if hasattr(self.model, "creator_id"): - sql = sql.options(selectinload(self.model.creator)) + # 应用可配置的预加载选项 + for opt in self.__loader_options(preload): + sql = sql.options(opt) sql = await self.__filter_permissions(sql) result: Result = await self.db.execute(sql) obj = result.scalars().first() - # if not obj: - # raise CustomException(msg="该信息不存在") - return obj except Exception as e: raise CustomException(msg=f"获取查询失败: {str(e)}") - async def list(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None) -> Sequence[ModelType]: + async def list(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, preload: Optional[List[Union[str, Any]]] = None) -> Sequence[ModelType]: """ - 根据条件获取对象列表和总数 + 根据条件获取对象列表 参数: - search (Optional[Dict]): 查询条件,格式为 {'id': value, 'name': value} - order_by (Optional[List[Dict[str, str]]]): 排序字段,格式为 [{'id': 'asc'}, {'name': 'desc'}] + - preload (Optional[List[Union[str, Any]]]): 预加载关系,支持关系名字符串或SQLAlchemy loader option 返回: - - Sequence[ModelType]: 对象列表和总数 + - Sequence[ModelType]: 对象列表 异常: - CustomException: 查询失败时抛出异常 @@ -93,16 +90,16 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): conditions = await self.__build_conditions(**search) if search else [] order = order_by or [{'id': 'asc'}] sql = select(self.model).where(*conditions).order_by(*self.__order_by(order)) - # 只有继承自CreatorMixin的模型才有creator关系 - if hasattr(self.model, "creator_id"): - sql = sql.options(selectinload(self.model.creator)) + # 应用可配置的预加载选项 + for opt in self.__loader_options(preload): + sql = sql.options(opt) sql = await self.__filter_permissions(sql) result: Result = await self.db.execute(sql) return result.scalars().all() except Exception as e: raise CustomException(msg=f"列表查询失败: {str(e)}") - async def tree_list(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, children_attr: str = 'children') -> Sequence[ModelType]: + async def tree_list(self, search: Optional[Dict] = None, order_by: Optional[List[Dict[str, str]]] = None, children_attr: str = 'children', preload: Optional[List[Union[str, Any]]] = None) -> Sequence[ModelType]: """ 获取树形结构数据列表 @@ -110,6 +107,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): - search (Optional[Dict]): 查询条件 - order_by (Optional[List[Dict[str, str]]]): 排序字段 - children_attr (str): 子节点属性名 + - preload (Optional[List[Union[str, Any]]]): 额外预加载关系,若为None则默认包含children_attr 返回: - Sequence[ModelType]: 树形结构数据列表 @@ -118,27 +116,30 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): - CustomException: 查询失败时抛出异常 """ try: - from sqlalchemy.orm import selectinload - conditions = await self.__build_conditions(**search) if search else [] order = order_by or [{'id': 'asc'}] sql = select(self.model).where(*conditions).order_by(*self.__order_by(order)) - # 如果模型有children属性,则预加载该关系 - if hasattr(self.model, children_attr): - sql = sql.options(selectinload(getattr(self.model, children_attr))) + # 处理预加载选项 + final_preload = preload + # 如果没有提供preload且children_attr存在,则添加到预加载选项中 + if preload is None and children_attr and hasattr(self.model, children_attr): + # 获取模型默认预加载选项 + model_defaults = getattr(self.model, "__loader_options__", []) + # 将children_attr添加到默认预加载选项中 + final_preload = list(model_defaults) + [children_attr] + + # 应用预加载选项 + for opt in self.__loader_options(final_preload): + sql = sql.options(opt) - # 只有继承自CreatorMixin的模型才有creator关系 - if hasattr(self.model, "creator_id"): - sql = sql.options(selectinload(self.model.creator)) - sql = await self.__filter_permissions(sql) result: Result = await self.db.execute(sql) return result.scalars().all() except Exception as e: raise CustomException(msg=f"树形列表查询失败: {str(e)}") - async def page(self, offset: int, limit: int, order_by: List[Dict[str, str]], search: Dict, out_schema: Type[OutSchemaType]) -> Dict: + async def page(self, offset: int, limit: int, order_by: List[Dict[str, str]], search: Dict, out_schema: Type[OutSchemaType], preload: Optional[List[Union[str, Any]]] = None) -> Dict: """ 获取分页数据 @@ -148,6 +149,7 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): - order_by (List[Dict[str, str]]): 排序字段 - search (Dict): 查询条件 - out_schema (Type[OutSchemaType]): 输出数据模型 + - preload (Optional[List[Union[str, Any]]]): 预加载关系 返回: - Dict: 分页数据 @@ -156,35 +158,28 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): - CustomException: 查询失败时抛出异常 """ try: - from sqlalchemy.orm import selectinload - conditions = await self.__build_conditions(**search) if search else [] order = order_by or [{'id': 'asc'}] sql = select(self.model).where(*conditions).order_by(*self.__order_by(order)) - # 只有继承自CreatorMixin的模型才有creator关系 - if hasattr(self.model, "creator_id"): - sql = sql.options(selectinload(self.model.creator)) + # 应用预加载选项 + for opt in self.__loader_options(preload): + sql = sql.options(opt) sql = await self.__filter_permissions(sql) # 获取总数 count_sql = select(func.count()).select_from(self.model) - # 应用相同的过滤条件到计数查询 if conditions: count_sql = count_sql.where(*conditions) count_sql = await self.__filter_permissions(count_sql) total_result = await self.db.execute(count_sql) - total = total_result.scalar() - - if total is None: - total = 0 + total = total_result.scalar() or 0 result: Result = await self.db.execute(sql.offset(offset).limit(limit)) - objs = result.scalars().all() - data=PageResultSchema( - items=[out_schema.model_validate(obj).model_dump() for obj in objs], + data = PageResultSchema( + items=[out_schema.model_validate(obj).model_dump() for obj in objs], total=total, page_no=offset // limit + 1 if limit else 1, page_size=limit, @@ -212,11 +207,10 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): obj_dict = data if isinstance(data, dict) else data.model_dump() obj = self.model(**obj_dict) - # 只有继承自CreatorMixin的模型才有creator关系 - if hasattr(self.model, "creator_id") and self.current_user: - # 设置创建人ID - obj.creator_id = self.current_user.id - + # 设置创建人ID(存在该字段时) + if hasattr(obj, "creator_id") and self.current_user: + setattr(obj, "creator_id", self.current_user.id) + self.db.add(obj) await self.db.flush() await self.db.refresh(obj) @@ -265,8 +259,17 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): - CustomException: 删除失败时抛出异常 """ try: - sql = delete(self.model).where(self.model.id.in_(ids)) - sql = await self.__filter_permissions(sql) + mapper = sa_inspect(self.model) + pk_cols = list(getattr(mapper, "primary_key", [])) + if not pk_cols: + raise CustomException(msg="模型缺少主键,无法删除") + if len(pk_cols) > 1: + raise CustomException(msg="暂不支持复合主键的批量删除") + sql = delete(self.model).where(pk_cols[0].in_(ids)) + # 权限条件 + perm = await self.__permission_condition() + if perm is not None: + sql = sql.where(perm) await self.db.execute(sql) await self.db.flush() except Exception as e: @@ -298,7 +301,17 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): - CustomException: 更新失败时抛出异常 """ try: - sql = update(self.model).where(self.model.id.in_(ids)).values(**kwargs) + mapper = sa_inspect(self.model) + pk_cols = list(getattr(mapper, "primary_key", [])) + if not pk_cols: + raise CustomException(msg="模型缺少主键,无法更新") + if len(pk_cols) > 1: + raise CustomException(msg="暂不支持复合主键的批量更新") + sql = update(self.model).where(pk_cols[0].in_(ids)).values(**kwargs) + # 权限条件 + perm = await self.__permission_condition() + if perm is not None: + sql = sql.where(perm) await self.db.execute(sql) await self.db.flush() except Exception as e: @@ -306,105 +319,107 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): async def __filter_permissions(self, sql: Select) -> Select: """ - 过滤数据权限 - - 参数: - - sql (Select): SQL查询对象 - - 返回: - - Select: 过滤后的数据查询对象 - - 异常: - - CustomException: 权限过滤失败时抛出异常 + 过滤数据权限(仅用于Select)。 """ - # 如果不需要检查数据权限,则直接返回 - if not self.current_user or not self.auth.check_data_scope: + perm = await self.__permission_condition() + if perm is None: return sql + return sql.where(perm) - # 1. 如果模型没有创建人creator字段,则不需要权限判断 + async def __permission_condition(self) -> Optional[ColumnElement]: + """ + 构造权限过滤表达式,返回None表示不限制。 + """ + # 如果不需要检查数据权限,则不限制 + if not self.current_user or not self.auth.check_data_scope: + return None + + # 如果模型没有创建人creator_id字段,则不限制 if not hasattr(self.model, "creator_id"): - return sql + return None - sql = sql.options(selectinload(self.model.creator)) - - # 2. 超级管理员可以查看所有数据 - if self.current_user.is_superuser: - return sql + # 超级管理员可以查看所有数据 + if getattr(self.current_user, "is_superuser", False): + return None - # 3. 如果用户没有部门或角色,则只能查看自己的数据 - if not self.current_user.dept_id or not self.current_user.roles: - return sql.where(self.model.creator_id == self.current_user.id) + # 如果用户没有部门或角色,则只能查看自己的数据 + if not getattr(self.current_user, "dept_id", None) or not getattr(self.current_user, "roles", None): + creator_id_attr = getattr(self.model, "creator_id", None) + if creator_id_attr is not None: + return creator_id_attr == self.current_user.id + return None - # 4. 获取用户所有角色的权限范围 + # 获取用户所有角色的权限范围 data_scopes = set() dept_ids = set() + roles = getattr(self.current_user, "roles", []) or [] - # data_scope 数据权限范围说明: - # 1: 仅本人数据权限 - # 2: 本部门数据权限 - # 3: 本部门及以下数据权限 - # 4: 全部数据权限 - # 5: 自定义数据权限 - - # 获取当前用户所绑定角色的数据权限范围 - for role in self.current_user.roles: - # 检查role是否有depts属性 - if hasattr(role, 'depts'): + for role in roles: + # 角色的部门集合 + if hasattr(role, 'depts') and role.depts: for dept in role.depts: dept_ids.add(dept.id) - data_scopes.add(role.data_scope) + # 如果有全部数据权限,直接返回 if 4 in data_scopes: - # 4、全部数据权限 - return sql + # 全部数据权限 + return None + # 如果有自定义数据权限且部门ID存在,优先处理 + if 5 in data_scopes and dept_ids: + # 自定义数据权限 + creator_rel = getattr(self.model, "creator", None) + if hasattr(UserModel, 'dept_id') and creator_rel is not None: + return creator_rel.has(getattr(UserModel, 'dept_id').in_(list(dept_ids))) + else: + creator_id_attr = getattr(self.model, "creator_id", None) + if creator_id_attr is not None: + return creator_id_attr == self.current_user.id + return None + + # 处理其他数据权限范围 + dept_id_val = getattr(self.current_user, "dept_id", None) + if 1 in data_scopes: - # 1、仅本人数据 - return sql.where(self.model.creator_id == self.current_user.id) + # 仅本人数据 + creator_id_attr = getattr(self.model, "creator_id", None) + if creator_id_attr is not None: + return creator_id_attr == self.current_user.id + return None - if 2 in data_scopes: - # 2、本部门数据 - dept_ids.add(self.current_user.dept_id) + if 2 in data_scopes and dept_id_val is not None: + # 本部门数据 + dept_ids.add(dept_id_val) - if 3 in data_scopes: - # 3、本部门及以下数据 - # 直接查询部门表,避免递归调用CRUD + if 3 in data_scopes and dept_id_val is not None: + # 本部门及以下数据(查询所有部门并递归) dept_sql = select(DeptModel) dept_result = await self.db.execute(dept_sql) dept_objs = dept_result.scalars().all() id_map = get_child_id_map(dept_objs) - dept_child_ids = get_child_recursion(id=self.current_user.dept_id, id_map=id_map) + dept_child_ids = get_child_recursion(id=dept_id_val, id_map=id_map) + dept_ids.add(dept_id_val) # 包含本部门 for child_id in dept_child_ids: dept_ids.add(child_id) - # 5、自定义权限 - # 检查UserModel是否有dept_id属性 - if hasattr(UserModel, 'dept_id'): - return sql.where(self.model.creator.has(UserModel.dept_id.in_(list(dept_ids)))) - else: - # 如果没有dept_id属性,回退到只显示自己的数据 - return sql.where(self.model.creator_id == self.current_user.id) + # 处理2、3汇总的数据权限 + if (2 in data_scopes or 3 in data_scopes) and dept_ids: + # 使用关系creator进行筛选(若存在),否则回退到仅本人数据 + creator_rel = getattr(self.model, "creator", None) + if hasattr(UserModel, 'dept_id') and creator_rel is not None and dept_ids: + return creator_rel.has(getattr(UserModel, 'dept_id').in_(list(dept_ids))) + else: + creator_id_attr = getattr(self.model, "creator_id", None) + if creator_id_attr is not None: + return creator_id_attr == self.current_user.id + return None - def __order_by(self, order_by: List[Dict[str, str]]) -> List[ColumnElement]: - """ - 获取排序字段 - - 参数: - - order_by (List[Dict[str, str]]): 排序字段列表,格式为 [{'id': 'asc'}, {'name': 'desc'}] - - 返回: - - List[ColumnElement]: 排序字段列表 - - 异常: - - CustomException: 排序字段不存在时抛出异常 - """ - columns = [] - for order in order_by: - for field, direction in order.items(): - column = getattr(self.model, field) - columns.append(desc(column) if direction.lower() == 'desc' else asc(column)) - return columns + # 默认情况下,只能查看自己的数据 + creator_id_attr = getattr(self.model, "creator_id", None) + if creator_id_attr is not None: + return creator_id_attr == self.current_user.id + return None async def __build_conditions(self, **kwargs) -> List[ColumnElement]: """ @@ -447,4 +462,64 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]): conditions.append(getattr(attr, seq.replace("==", "__eq__"))(val)) else: conditions.append(attr == value) - return conditions \ No newline at end of file + return conditions + + def __order_by(self, order_by: List[Dict[str, str]]) -> List[ColumnElement]: + """ + 获取排序字段 + + 参数: + - order_by (List[Dict[str, str]]): 排序字段列表,格式为 [{'id': 'asc'}, {'name': 'desc'}] + + 返回: + - List[ColumnElement]: 排序字段列表 + + 异常: + - CustomException: 排序字段不存在时抛出异常 + """ + columns = [] + for order in order_by: + for field, direction in order.items(): + column = getattr(self.model, field) + columns.append(desc(column) if direction.lower() == 'desc' else asc(column)) + return columns + + def __loader_options(self, preload: Optional[List[Union[str, Any]]] = None) -> List[Any]: + """ + 将预加载参数标准化为SQLAlchemy loader options。 + 字符串会转换为selectinload(getattr(self.model, name));loader option对象将原样返回。 + 若模型定义了 __loader_options__,会作为默认预加载。 + """ + # 获取模型默认预加载选项 + model_defaults = getattr(self.model, "__loader_options__", []) + + # 确定最终使用的预加载选项 + final_preload = [] + if preload is None: + # 如果未指定preload,使用模型默认选项 + final_preload = model_defaults + elif preload == []: + # 如果preload为空列表,表示不使用任何预加载 + final_preload = [] + else: + # 如果指定了preload,使用指定的选项(完全替换默认选项) + final_preload = preload + + # 转换为SQLAlchemy loader options并去重 + opts = [] + added_options = set() + + for item in final_preload: + if isinstance(item, str): + # 字符串类型的预加载选项 + if item not in added_options and hasattr(self.model, item): + opts.append(selectinload(getattr(self.model, item))) + added_options.add(item) + else: + # loader option对象 + item_str = str(item) + if item_str not in added_options and item is not None: + opts.append(item) + added_options.add(item_str) + + return opts diff --git a/backend/app/core/dependencies.py b/backend/app/core/dependencies.py index 51431f7d..3ab42bf5 100644 --- a/backend/app/core/dependencies.py +++ b/backend/app/core/dependencies.py @@ -3,12 +3,15 @@ import json from redis.asyncio.client import Redis from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload from typing import AsyncGenerator, Optional 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.api.v1.module_system.user.model import UserModel +from app.api.v1.module_system.role.model import RoleModel from app.common.enums import RedisInitKeyConfig from app.core.exceptions import CustomException from app.core.database import session_connect @@ -53,29 +56,28 @@ async def mongo_getter(request: Request) -> AsyncIOMotorDatabase: async def get_current_user( request: Request, + db: AsyncSession = Depends(db_getter), + redis: Redis = Depends(redis_getter), token: str = Depends(OAuth2Schema), - redis: Redis = Depends(redis_getter), - db: AsyncSession = Depends(db_getter) ) -> AuthSchema: - """获取并验证当前用户信息 + """获取当前用户 参数: - - request (Request): 请求对象。 - - token (str): 认证token。 - - redis (Redis): Redis连接。 - - db (AsyncSession): 数据库会话连接。 + - request (Request): 请求对象 + - db (AsyncSession): 数据库会话 + - redis (Redis): Redis连接 + - token (str): 访问令牌 返回: - - AuthSchema: 包含用户信息的认证对象。 - - 异常: - - CustomException: 认证失败时抛出异常。 + - AuthSchema: 认证信息模型 """ + if not token: + raise CustomException(msg="认证已失效", code=10401, status_code=401) + # 处理Bearer token if token.startswith('Bearer'): token = token.split(' ')[1] - - # 解析token + payload = decode_access_token(token) if not payload or not hasattr(payload, 'is_refresh') or payload.is_refresh: raise CustomException(msg="非法凭证", code=10401, status_code=401) @@ -93,12 +95,21 @@ async def get_current_user( if not online_ok: raise CustomException(msg="认证已失效", code=10401, status_code=401) - auth = AuthSchema(db=db) + # 关闭数据权限过滤,避免当前用户查询被拦截 + auth = AuthSchema(db=db, check_data_scope=False) username = user_info.get("user_name") if not username: raise CustomException(msg="认证已失效", code=10401, status_code=401) - # 获取用户信息 - user = await UserCRUD(auth).get_by_username_crud(username=username) + # 获取用户信息,使用深层预加载确保RoleModel.creator被正确加载 + user = await UserCRUD(auth).get_by_username_crud( + username=username, + preload=[ + "dept", + selectinload(UserModel.roles).selectinload(RoleModel.creator), + "positions", + "creator" + ] + ) if not user: raise CustomException(msg="用户不存在", code=10401, status_code=401) if not user.status: @@ -110,9 +121,9 @@ async def get_current_user( # 过滤可用的角色和职位 if hasattr(user, 'roles'): - user.roles = [role for role in user.roles if role.status] + user.roles = [role for role in user.roles if role and role.status] 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 and pos.status] auth.user = UserOutSchema.model_validate(user) return auth @@ -129,22 +140,18 @@ class AuthPermission: - permissions (Optional[list[str]]): 权限标识列表。 - check_data_scope (bool): 是否启用严格模式校验。 """ - self.permissions = set(permissions) if permissions else None + self.permissions = permissions or [] self.check_data_scope = check_data_scope async def __call__(self, auth: AuthSchema = Depends(get_current_user)) -> AuthSchema: """ - 执行权限验证 + 调用权限验证 参数: - - request (Request): 请求对象。 - - auth (AuthSchema): 认证信息。 - + - auth (AuthSchema): 认证信息对象。 + 返回: - - AuthSchema: 认证对象 - - 异常: - - CustomException: 权限验证失败时抛出异常。 + - AuthSchema: 认证信息对象。 """ auth.check_data_scope = self.check_data_scope @@ -157,7 +164,7 @@ class AuthPermission: return auth # 超级管理员权限标识 - if {"*:*:*"} <= self.permissions: + if "*" in self.permissions or "*:*:*" in self.permissions: return auth # 检查用户是否有角色 @@ -169,19 +176,12 @@ class AuthPermission: menu.permission for role in auth.user.roles for menu in role.menus - if menu.permission and menu.status + if role.status and menu.permission and menu.status } - # 权限验证 - if self.check_data_scope: - # 严格模式:要求所有权限都满足 - if not all(perm in user_permissions for perm in self.permissions): - logger.error(f"用户缺少所需的权限: {self.permissions}") - raise CustomException(msg="无权限操作", code=10403, status_code=403) - else: - # 非严格模式:满足任一权限即可 - if not any(perm in user_permissions for perm in self.permissions): - logger.error(f"用户缺少任何所需的权限: {self.permissions}") - raise CustomException(msg="无权限操作", code=10403, status_code=403) + # 权限验证 - 满足任一权限即可 + if not any(perm in user_permissions for perm in self.permissions): + logger.error(f"用户缺少任何所需的权限: {self.permissions}") + raise CustomException(msg="无权限操作", code=10403, status_code=403) return auth diff --git a/backend/app/scripts/data/system_dept.json b/backend/app/scripts/data/system_dept.json index b458541a..b73bc30c 100644 --- a/backend/app/scripts/data/system_dept.json +++ b/backend/app/scripts/data/system_dept.json @@ -1,7 +1,7 @@ [ { "name": "集团总公司", - "code": "GROUP001", + "code": "GROUP", "order": 1, "status": true, "parent_id": null, @@ -9,47 +9,26 @@ "children": [ { "name": "北京分公司", - "code": "BJ001", + "code": "BJ", "order": 1, "status": true, "description": "北京分公司", "children": [ { - "name": "技术部", - "code": "TECH001", + "name": "软件部", + "code": "TECH", "order": 1, "status": true, - "description": "技术部" - }, - { - "name": "销售部", - "code": "SALES001", - "order": 2, - "status": true, - "description": "销售部" - } - ] - }, - { - "name": "上海分公司", - "code": "SH001", - "order": 2, - "status": true, - "description": "上海分公司", - "children": [ - { - "name": "市场部", - "code": "MARKET001", - "order": 1, - "status": true, - "description": "市场部" - }, - { - "name": "人事部", - "code": "HR001", - "order": 2, - "status": true, - "description": "人事部" + "description": "软件部", + "children": [ + { + "name": "开发组", + "code": "DEV", + "order": 1, + "status": true, + "description": "开发部" + } + ] } ] } diff --git a/backend/app/scripts/data/system_menu.json b/backend/app/scripts/data/system_menu.json index 10192ef8..a439ed90 100644 --- a/backend/app/scripts/data/system_menu.json +++ b/backend/app/scripts/data/system_menu.json @@ -21,7 +21,7 @@ { "name": "工作台", "type": 2, - "icon": "homepage", + "icon": "el-icon-PieChart", "order": 1, "permission": "dashboard:workplace:query", "route_name": "Workplace", @@ -33,25 +33,6 @@ "always_show": false, "title": "工作台", "params": null, - "affix": true, - "redirect": null, - "description": "初始化数据" - }, - { - "name": "分析页", - "type": 2, - "icon": "el-icon-PieChart", - "order": 2, - "permission": "dashboard:analysis:query", - "route_name": "Analysis", - "route_path": "/dashboard/analysis", - "component_path": "dashboard/analysis", - "status": true, - "keep_alive": true, - "hidden": false, - "always_show": false, - "title": "分析页", - "params": null, "affix": false, "redirect": null, "description": "初始化数据" diff --git a/backend/app/scripts/data/system_notice.json b/backend/app/scripts/data/system_notice.json deleted file mode 100644 index 843e1c97..00000000 --- a/backend/app/scripts/data/system_notice.json +++ /dev/null @@ -1,18 +0,0 @@ -[ - { - "notice_title": "系统通知", - "notice_type": "1", - "notice_content": "维护通知:2025-10-01 fastapi_vue3_admin系统凌晨维护", - "status": true, - "description": "通知", - "creator_id": 1 - }, - { - "notice_title": "系统公告", - "notice_type": "2", - "notice_content": "温馨提醒:2025-10-01 fastapi_vue3_admin新版本发布啦", - "status": true, - "description": "公告", - "creator_id": 1 - } -] \ No newline at end of file diff --git a/backend/app/scripts/data/system_position.json b/backend/app/scripts/data/system_position.json deleted file mode 100644 index 296104c6..00000000 --- a/backend/app/scripts/data/system_position.json +++ /dev/null @@ -1,9 +0,0 @@ -[ - { - "name": "董事长岗", - "status": true, - "order": 1, - "description": "董事长岗位", - "creator_id": 1 - } -] \ No newline at end of file diff --git a/backend/app/scripts/data/system_role.json b/backend/app/scripts/data/system_role.json index 1e3c96ae..93018a44 100644 --- a/backend/app/scripts/data/system_role.json +++ b/backend/app/scripts/data/system_role.json @@ -5,7 +5,7 @@ "code": "ADMIN", "status": true, "order": 1, - "description": "管理员", + "description": "初始化角色", "creator_id": 1 } ] \ No newline at end of file diff --git a/backend/app/scripts/data/system_role_depts.json b/backend/app/scripts/data/system_role_depts.json deleted file mode 100644 index 923fa53b..00000000 --- a/backend/app/scripts/data/system_role_depts.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "role_id": 1, - "dept_id": 1 - } -] \ 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 deleted file mode 100644 index 94961c81..00000000 --- a/backend/app/scripts/data/system_role_menus.json +++ /dev/null @@ -1,115 +0,0 @@ -[ - {"role_id": 1, "menu_id": 1}, - {"role_id": 1, "menu_id": 2}, - {"role_id": 1, "menu_id": 3}, - {"role_id": 1, "menu_id": 4}, - {"role_id": 1, "menu_id": 5}, - {"role_id": 1, "menu_id": 6}, - {"role_id": 1, "menu_id": 7}, - {"role_id": 1, "menu_id": 8}, - {"role_id": 1, "menu_id": 9}, - {"role_id": 1, "menu_id": 10}, - {"role_id": 1, "menu_id": 11}, - {"role_id": 1, "menu_id": 12}, - {"role_id": 1, "menu_id": 13}, - {"role_id": 1, "menu_id": 14}, - {"role_id": 1, "menu_id": 15}, - {"role_id": 1, "menu_id": 16}, - {"role_id": 1, "menu_id": 17}, - {"role_id": 1, "menu_id": 18}, - {"role_id": 1, "menu_id": 19}, - {"role_id": 1, "menu_id": 20}, - {"role_id": 1, "menu_id": 21}, - {"role_id": 1, "menu_id": 22}, - {"role_id": 1, "menu_id": 23}, - {"role_id": 1, "menu_id": 24}, - {"role_id": 1, "menu_id": 25}, - {"role_id": 1, "menu_id": 26}, - {"role_id": 1, "menu_id": 27}, - {"role_id": 1, "menu_id": 28}, - {"role_id": 1, "menu_id": 29}, - {"role_id": 1, "menu_id": 30}, - {"role_id": 1, "menu_id": 31}, - {"role_id": 1, "menu_id": 32}, - {"role_id": 1, "menu_id": 33}, - {"role_id": 1, "menu_id": 34}, - {"role_id": 1, "menu_id": 35}, - {"role_id": 1, "menu_id": 36}, - {"role_id": 1, "menu_id": 37}, - {"role_id": 1, "menu_id": 38}, - {"role_id": 1, "menu_id": 39}, - {"role_id": 1, "menu_id": 40}, - {"role_id": 1, "menu_id": 41}, - {"role_id": 1, "menu_id": 42}, - {"role_id": 1, "menu_id": 43}, - {"role_id": 1, "menu_id": 44}, - {"role_id": 1, "menu_id": 45}, - {"role_id": 1, "menu_id": 46}, - {"role_id": 1, "menu_id": 47}, - {"role_id": 1, "menu_id": 48}, - {"role_id": 1, "menu_id": 49}, - {"role_id": 1, "menu_id": 50}, - {"role_id": 1, "menu_id": 51}, - {"role_id": 1, "menu_id": 52}, - {"role_id": 1, "menu_id": 53}, - {"role_id": 1, "menu_id": 54}, - {"role_id": 1, "menu_id": 55}, - {"role_id": 1, "menu_id": 56}, - {"role_id": 1, "menu_id": 57}, - {"role_id": 1, "menu_id": 58}, - {"role_id": 1, "menu_id": 59}, - {"role_id": 1, "menu_id": 60}, - {"role_id": 1, "menu_id": 61}, - {"role_id": 1, "menu_id": 62}, - {"role_id": 1, "menu_id": 63}, - {"role_id": 1, "menu_id": 64}, - {"role_id": 1, "menu_id": 65}, - {"role_id": 1, "menu_id": 66}, - {"role_id": 1, "menu_id": 67}, - {"role_id": 1, "menu_id": 68}, - {"role_id": 1, "menu_id": 69}, - {"role_id": 1, "menu_id": 70}, - {"role_id": 1, "menu_id": 71}, - {"role_id": 1, "menu_id": 72}, - {"role_id": 1, "menu_id": 73}, - {"role_id": 1, "menu_id": 74}, - {"role_id": 1, "menu_id": 75}, - {"role_id": 1, "menu_id": 76}, - {"role_id": 1, "menu_id": 77}, - {"role_id": 1, "menu_id": 78}, - {"role_id": 1, "menu_id": 79}, - {"role_id": 1, "menu_id": 80}, - {"role_id": 1, "menu_id": 81}, - {"role_id": 1, "menu_id": 82}, - {"role_id": 1, "menu_id": 83}, - {"role_id": 1, "menu_id": 84}, - {"role_id": 1, "menu_id": 85}, - {"role_id": 1, "menu_id": 86}, - {"role_id": 1, "menu_id": 87}, - {"role_id": 1, "menu_id": 88}, - {"role_id": 1, "menu_id": 89}, - {"role_id": 1, "menu_id": 90}, - {"role_id": 1, "menu_id": 91}, - {"role_id": 1, "menu_id": 92}, - {"role_id": 1, "menu_id": 93}, - {"role_id": 1, "menu_id": 94}, - {"role_id": 1, "menu_id": 95}, - {"role_id": 1, "menu_id": 96}, - {"role_id": 1, "menu_id": 97}, - {"role_id": 1, "menu_id": 98}, - {"role_id": 1, "menu_id": 99}, - {"role_id": 1, "menu_id": 100}, - {"role_id": 1, "menu_id": 101}, - {"role_id": 1, "menu_id": 102}, - {"role_id": 1, "menu_id": 103}, - {"role_id": 1, "menu_id": 104}, - {"role_id": 1, "menu_id": 105}, - {"role_id": 1, "menu_id": 106}, - {"role_id": 1, "menu_id": 107}, - {"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} -] \ No newline at end of file diff --git a/backend/app/scripts/data/system_user_positions.json b/backend/app/scripts/data/system_user_positions.json deleted file mode 100644 index 877c2316..00000000 --- a/backend/app/scripts/data/system_user_positions.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "user_id": 1, - "position_id": 1 - }, - { - "user_id": 2, - "position_id": 1 - } -] \ No newline at end of file diff --git a/backend/app/scripts/data/system_users.json b/backend/app/scripts/data/system_users.json index 010be836..e14295ea 100644 --- a/backend/app/scripts/data/system_users.json +++ b/backend/app/scripts/data/system_users.json @@ -2,31 +2,31 @@ { "username": "admin", "password": "$2b$12$e2IJgS/cvHgJ0H3G7Xa08OXoXnk6N/NX3IZRtubBDElA0VLZhkNOa", - "name": "管理员", - "mobile": "15382112222", - "email": "admin@qq.com", + "name": "超级管理员", + "mobile": null, + "email": null, "gender": "0", "avatar": "https://service.fastapiadmin.com/api/v1/static/image/avatar.png", "status": true, "is_superuser": true, "last_login": null, "dept_id": 1, - "description": "管理员", + "description": "初始化管理员", "creator_id": null }, { "username": "demo", "password": "$2b$12$e2IJgS/cvHgJ0H3G7Xa08OXoXnk6N/NX3IZRtubBDElA0VLZhkNOa", "name": "演示用户", - "mobile": "15382112121", - "email": "demo@qq.com", - "gender": "0", + "mobile": null, + "email": null, + "gender": "1", "avatar": "https://service.fastapiadmin.com/api/v1/static/image/avatar.png", "status": true, "is_superuser": false, "last_login": null, "dept_id": 1, - "description": "演示用户", + "description": "初始化演示用户", "creator_id": 1 } ] \ No newline at end of file diff --git a/backend/app/scripts/initialize.py b/backend/app/scripts/initialize.py index 84d0ea37..3c17f4a8 100644 --- a/backend/app/scripts/initialize.py +++ b/backend/app/scripts/initialize.py @@ -32,27 +32,15 @@ class InitializeData: """ # 按照依赖关系排序:先创建基础表,再创建关联表 self.prepare_init_models = [ - # 部门表(自引用,需要先创建) + # 基础表(项目启动初始化数据表,部门和菜单必须先创建) DeptModel, - # 菜单表(自引用,需要先创建) MenuModel, - # 用户表(依赖部门和角色) UserModel, - # 角色表(依赖菜单和部门) RoleModel, - # 岗位表(无外键依赖) - PositionModel, - # 基础表(无外键依赖) + UserRolesModel, ParamsModel, DictTypeModel, - DictDataModel, - # 关联表(依赖基础表) - UserPositionsModel, - UserRolesModel, - RoleDeptsModel, - RoleMenusModel, - # 通知表(无外键依赖) - NoticeModel, + DictDataModel ] async def __init_create_table(self) -> None: @@ -173,4 +161,6 @@ class InitializeData: async with AsyncSessionLocal() as session: async with session.begin(): await self.__init_data(session) + # 确保提交事务 + await session.commit() \ No newline at end of file diff --git a/frontend/package.json b/frontend/package.json index 8d6b5f5c..7aeff136 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -89,6 +89,7 @@ "vue": "^3.5.17", "vue-draggable-plus": "^0.6.0", "vue-i18n": "^11.1.10", + "vue-json-pretty": "^2.5.0", "vue-router": "^4.5.1", "vue3-cron-plus": "^0.1.9", "vuedraggable": "^4.1.0" diff --git a/frontend/src/api/application/job.ts b/frontend/src/api/application/job.ts index 6b929f5e..e2f4900f 100644 --- a/frontend/src/api/application/job.ts +++ b/frontend/src/api/application/job.ts @@ -129,6 +129,8 @@ export interface JobPageQuery extends PageQuery { start_time?: string; /** 结束时间 */ end_time?: string; + // 创建人ID + creator?: number; } export interface JobLogPageQuery extends PageQuery { @@ -137,6 +139,8 @@ export interface JobLogPageQuery extends PageQuery { start_time?: string; /** 结束时间 */ end_time?: string; + /** 任务ID */ + job_id?: number; } export interface JobOptionData { diff --git a/frontend/src/api/generator/demo.ts b/frontend/src/api/generator/demo.ts index ba5a6792..55258151 100644 --- a/frontend/src/api/generator/demo.ts +++ b/frontend/src/api/generator/demo.ts @@ -90,6 +90,8 @@ export interface ExamplePageQuery extends PageQuery { start_time?: string; /** 结束时间 */ end_time?: string; + /** 创建人 */ + creator?: number; } export interface ExampleTable { diff --git a/frontend/src/components/DatePicker/index.vue b/frontend/src/components/DatePicker/index.vue index 6fad0329..805b0860 100644 --- a/frontend/src/components/DatePicker/index.vue +++ b/frontend/src/components/DatePicker/index.vue @@ -34,6 +34,8 @@ const shortcuts = [ value: () => { const end = new Date() const start = new Date() + // 修正:起始时间为当前时间往前 24 小时 + start.setTime(start.getTime() - 3600 * 1000 * 24) return [start, end] } }, diff --git a/frontend/src/components/JsonPretty/index.vue b/frontend/src/components/JsonPretty/index.vue new file mode 100644 index 00000000..967f9606 --- /dev/null +++ b/frontend/src/components/JsonPretty/index.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/frontend/src/components/Notification/index.vue b/frontend/src/components/Notification/index.vue index 64a6e6b6..e24fcb22 100644 --- a/frontend/src/components/Notification/index.vue +++ b/frontend/src/components/Notification/index.vue @@ -98,13 +98,12 @@ function handleViewMoreNotice() { router.push({ name: "Notice" }); } -// 全部已读 +// 全部已读:将这些公告禁用(status=false),刷新后不再出现 function handleMarkAllAsRead() { - NoticeAPI.batchAvailableNotice({ - ids: noticeList.value.map((item) => item.id).filter((id): id is number => id !== undefined), - status: true - }).then(() => { - noticeList.value = []; + const ids = noticeList.value.map((item) => item.id).filter((id): id is number => id !== undefined); + NoticeAPI.batchAvailableNotice({ ids, status: false }).then(async () => { + await noticeStore.getNotice(); + noticeList.value = noticeStore.noticeList; }); } diff --git a/frontend/src/components/Pagination/index.vue b/frontend/src/components/Pagination/index.vue index ae1e7036..235fef35 100644 --- a/frontend/src/components/Pagination/index.vue +++ b/frontend/src/components/Pagination/index.vue @@ -3,7 +3,6 @@ @@ -78,7 +78,7 @@ - + @@ -89,7 +89,7 @@ - + @@ -174,6 +174,18 @@ function handleTourClick() { } } +// 引导结束(点击跳过或最后一步完成关闭)后,自动关闭下次登录的自动展示 +function handleGuideExit() { + // 关闭自动展示开关,确保下次登录不再自动开启 + settingStore.updateSetting('showGuide', false); +} + +// 监听引导关闭(从 true -> false),也同步关闭自动展示开关 +watch(() => guideVisible.value, (val, oldVal) => { + if (oldVal && !val) { + settingStore.updateSetting('showGuide', false); + } +}); /** * 锁屏 diff --git a/frontend/src/layouts/components/TagsView/index.vue b/frontend/src/layouts/components/TagsView/index.vue index 2b2ce6b9..8a92d79b 100644 --- a/frontend/src/layouts/components/TagsView/index.vue +++ b/frontend/src/layouts/components/TagsView/index.vue @@ -7,7 +7,6 @@