feat: 新增刷新缓存功能并优化系统初始化

refactor: 重构模型加载选项和CRUD预加载逻辑

fix: 修复日期范围查询格式问题

style: 优化前端组件样式和布局

perf: 提升字典数据更新性能

docs: 更新注释和文档说明

chore: 清理无用文件和代码

test: 更新测试用例

build: 添加vue-json-pretty依赖

ci: 优化初始化脚本和事务处理
This commit is contained in:
zhangtao
2025-10-27 02:33:50 +08:00
parent ab90f86623
commit 780e02b9de
89 changed files with 1549 additions and 914 deletions
+11 -8
View File
@@ -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
@@ -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
+13 -9
View File
@@ -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]:
"""
@@ -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='字典标签')
@@ -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:
+7 -6
View File
@@ -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)
@@ -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="请求路径")
+11 -8
View File
@@ -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)
@@ -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")
@@ -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
@@ -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]:
"""
@@ -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公告)')
@@ -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)
@@ -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='参数名称')
@@ -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
@@ -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="显示排序")
@@ -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)
@@ -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")
+29 -12
View File
@@ -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()))
@@ -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')
@@ -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="备注")
@@ -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)