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 @@