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
+196 -121
View File
@@ -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
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