最新代码

This commit is contained in:
zhangtao
2025-12-04 01:08:12 +08:00
parent 0305b4b64b
commit b5a969c555
17 changed files with 217 additions and 180 deletions
@@ -86,9 +86,6 @@ class DeptService:
obj = await DeptCRUD(auth).get(code=data.code) obj = await DeptCRUD(auth).get(code=data.code)
if obj: if obj:
raise CustomException(msg='创建失败,编码已存在') raise CustomException(msg='创建失败,编码已存在')
# 检测循环引用
if data.parent_id:
await cls._check_circular_reference(auth, data.parent_id, id=None)
dept = await DeptCRUD(auth).create(data=data) dept = await DeptCRUD(auth).create(data=data)
return DeptOutSchema.model_validate(dept).model_dump() return DeptOutSchema.model_validate(dept).model_dump()
@@ -114,45 +111,9 @@ class DeptService:
exist_dept = await DeptCRUD(auth).get(name=data.name) exist_dept = await DeptCRUD(auth).get(name=data.name)
if exist_dept and exist_dept.id != id: if exist_dept and exist_dept.id != id:
raise CustomException(msg='更新失败,部门名称重复') raise CustomException(msg='更新失败,部门名称重复')
# 检测循环引用
if data.parent_id is not None:
await cls._check_circular_reference(auth, data.parent_id, id=id)
dept = await DeptCRUD(auth).update(id=id, data=data) dept = await DeptCRUD(auth).update(id=id, data=data)
return DeptOutSchema.model_validate(dept).model_dump() return DeptOutSchema.model_validate(dept).model_dump()
@classmethod
async def _check_circular_reference(cls, auth: AuthSchema, parent_id: int, id: int | None = None) -> None:
"""
检测部门层级循环引用
参数:
- auth (AuthSchema): 认证对象
- parent_id (int): 父部门ID
- id (int | None): 当前部门ID(用于更新时检测)
异常:
- CustomException: 当检测到循环引用时抛出
"""
if parent_id == id:
raise CustomException(msg='更新失败,不能将部门设置为自己的子部门')
# 获取所有部门信息用于检测循环引用
all_depts = await DeptCRUD(auth).get_list_crud()
parent_map = {dept.id: dept.parent_id for dept in all_depts}
# 检查循环引用
current_id = parent_id
visited = set()
while current_id and current_id not in visited:
visited.add(current_id)
if current_id == id:
raise CustomException(msg='更新失败,检测到部门层级循环引用')
current_id = parent_map.get(current_id)
if current_id == id:
raise CustomException(msg='更新失败,检测到部门层级循环引用')
@classmethod @classmethod
async def delete_dept_service(cls, auth: AuthSchema, ids: list[int]) -> None: async def delete_dept_service(cls, auth: AuthSchema, ids: list[int]) -> None:
""" """
@@ -59,13 +59,13 @@ class RoleDeptsModel(MappedBase):
) )
class RoleModel(ModelMixin, UserMixin): class RoleModel(ModelMixin):
""" """
角色模型 角色模型
""" """
__tablename__: str = "sys_role" __tablename__: str = "sys_role"
__table_args__: dict[str, str] = ({'comment': '角色表'}) __table_args__: dict[str, str] = ({'comment': '角色表'})
__loader_options__: list[str] = ["menus", "depts", "created_by", "updated_by"] __loader_options__: list[str] = ["menus", "depts"]
name: Mapped[str] = mapped_column(String(40), nullable=False, comment="角色名称") name: Mapped[str] = mapped_column(String(40), nullable=False, comment="角色名称")
code: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True, comment="角色编码") code: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True, comment="角色编码")
@@ -4,7 +4,7 @@ from fastapi import Query
from pydantic import BaseModel, ConfigDict, Field, model_validator, field_validator from pydantic import BaseModel, ConfigDict, Field, model_validator, field_validator
from app.core.validator import DateTimeStr from app.core.validator import DateTimeStr
from app.core.base_schema import BaseSchema, UserBySchema from app.core.base_schema import BaseSchema
from app.core.validator import role_permission_request_validator from app.core.validator import role_permission_request_validator
from ..dept.schema import DeptOutSchema from ..dept.schema import DeptOutSchema
@@ -50,7 +50,7 @@ class RoleUpdateSchema(RoleCreateSchema):
... ...
class RoleOutSchema(RoleCreateSchema, BaseSchema, UserBySchema): class RoleOutSchema(RoleCreateSchema, BaseSchema):
"""角色信息响应模型""" """角色信息响应模型"""
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
@@ -67,16 +67,12 @@ class RoleQueryParam:
status: str | None = Query(None, description="是否可用"), status: str | None = Query(None, description="是否可用"),
created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", example=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), created_time: list[DateTimeStr] | None = Query(None, description="创建时间范围", example=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", example=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]), updated_time: list[DateTimeStr] | None = Query(None, description="更新时间范围", example=["2025-01-01 00:00:00", "2025-12-31 23:59:59"]),
created_id: int | None = Query(None, description="创建人"),
updated_id: int | None = Query(None, description="更新人"),
) -> None: ) -> None:
# 模糊查询字段 # 模糊查询字段
self.name = ("like", name) self.name = ("like", name)
# 精确查询字段 # 精确查询字段
self.created_id = created_id
self.updated_id = updated_id
self.status = status self.status = status
# 时间范围查询 # 时间范围查询
@@ -104,3 +104,22 @@ class UserModel(ModelMixin, UserMixin):
back_populates="users", back_populates="users",
lazy="selectin" lazy="selectin"
) )
# 覆盖 UserMixin 的关系定义,显式指定 foreign_keys 避免自引用混淆
created_by: Mapped["UserModel | None"] = relationship(
"UserModel",
foreign_keys="UserModel.created_id",
remote_side="UserModel.id",
lazy="selectin",
uselist=False,
viewonly=True # 防止级联操作
)
updated_by: Mapped["UserModel | None"] = relationship(
"UserModel",
foreign_keys="UserModel.updated_id",
remote_side="UserModel.id",
lazy="selectin",
uselist=False,
viewonly=True # 防止级联操作
)
+61 -7
View File
@@ -5,7 +5,7 @@ from typing import TypeVar, Sequence, Generic, Dict, Any, List, Optional, Type,
from sqlalchemy.sql.elements import ColumnElement from sqlalchemy.sql.elements import ColumnElement
from sqlalchemy.orm import selectinload from sqlalchemy.orm import selectinload
from sqlalchemy.engine import Result from sqlalchemy.engine import Result
from sqlalchemy import asc, func, select, delete, Select, desc, update, or_, and_ from sqlalchemy import asc, func, select, delete, Select, desc, update
from sqlalchemy import inspect as sa_inspect from sqlalchemy import inspect as sa_inspect
from app.core.base_model import MappedBase from app.core.base_model import MappedBase
@@ -160,8 +160,16 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
sql = sql.options(opt) sql = sql.options(opt)
sql = await self.__filter_permissions(sql) sql = await self.__filter_permissions(sql)
# 获取总数 # 优化count查询:使用主键计数而非全表扫描
count_sql = select(func.count()).select_from(self.model) mapper = sa_inspect(self.model)
pk_cols = list(getattr(mapper, "primary_key", []))
if pk_cols:
# 使用主键的第一列进行计数(主键必定非NULL,性能更好)
count_sql = select(func.count(pk_cols[0])).select_from(self.model)
else:
# 降级方案:使用count(*)
count_sql = select(func.count()).select_from(self.model)
if conditions: if conditions:
count_sql = count_sql.where(*conditions) count_sql = count_sql.where(*conditions)
count_sql = await self.__filter_permissions(count_sql) count_sql = await self.__filter_permissions(count_sql)
@@ -244,6 +252,14 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
await self.auth.db.flush() await self.auth.db.flush()
await self.auth.db.refresh(obj) await self.auth.db.refresh(obj)
# 权限二次确认:flush后再次验证对象仍在权限范围内
# 防止并发修改导致的权限逃逸(如其他事务修改了created_id)
verify_obj = await self.get(id=id)
if not verify_obj:
# 对象已被删除或权限已失效
raise CustomException(msg="更新失败,对象不存在或无权限访问")
return obj return obj
except Exception as e: except Exception as e:
raise CustomException(msg=f"更新失败: {str(e)}") raise CustomException(msg=f"更新失败: {str(e)}")
@@ -259,13 +275,27 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
- CustomException: 删除失败时抛出异常 - CustomException: 删除失败时抛出异常
""" """
try: try:
# 先查询确认权限,避免删除无权限的数据
objs = await self.list(search={"id": ("in", ids)})
accessible_ids = [obj.id for obj in objs]
# 检查是否所有ID都有权限访问
inaccessible_count = len(ids) - len(accessible_ids)
if inaccessible_count > 0:
raise CustomException(msg=f"无权限删除{inaccessible_count}条数据")
if not accessible_ids:
return # 没有可删除的数据
mapper = sa_inspect(self.model) mapper = sa_inspect(self.model)
pk_cols = list(getattr(mapper, "primary_key", [])) pk_cols = list(getattr(mapper, "primary_key", []))
if not pk_cols: if not pk_cols:
raise CustomException(msg="模型缺少主键,无法删除") raise CustomException(msg="模型缺少主键,无法删除")
if len(pk_cols) > 1: if len(pk_cols) > 1:
raise CustomException(msg="暂不支持复合主键的批量删除") raise CustomException(msg="暂不支持复合主键的批量删除")
sql = delete(self.model).where(pk_cols[0].in_(ids))
# 只删除有权限的数据
sql = delete(self.model).where(pk_cols[0].in_(accessible_ids))
await self.auth.db.execute(sql) await self.auth.db.execute(sql)
await self.auth.db.flush() await self.auth.db.flush()
except Exception as e: except Exception as e:
@@ -297,15 +327,31 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
- CustomException: 更新失败时抛出异常 - CustomException: 更新失败时抛出异常
""" """
try: try:
# 先查询确认权限,避免更新无权限的数据
objs = await self.list(search={"id": ("in", ids)})
accessible_ids = [obj.id for obj in objs]
# 检查是否所有ID都有权限访问
inaccessible_count = len(ids) - len(accessible_ids)
if inaccessible_count > 0:
raise CustomException(msg=f"无权限更新{inaccessible_count}条数据")
if not accessible_ids:
return # 没有可更新的数据
mapper = sa_inspect(self.model) mapper = sa_inspect(self.model)
pk_cols = list(getattr(mapper, "primary_key", [])) pk_cols = list(getattr(mapper, "primary_key", []))
if not pk_cols: if not pk_cols:
raise CustomException(msg="模型缺少主键,无法更新") raise CustomException(msg="模型缺少主键,无法更新")
if len(pk_cols) > 1: if len(pk_cols) > 1:
raise CustomException(msg="暂不支持复合主键的批量更新") raise CustomException(msg="暂不支持复合主键的批量更新")
sql = update(self.model).where(pk_cols[0].in_(ids)).values(**kwargs)
# 只更新有权限的数据
sql = update(self.model).where(pk_cols[0].in_(accessible_ids)).values(**kwargs)
await self.auth.db.execute(sql) await self.auth.db.execute(sql)
await self.auth.db.flush() await self.auth.db.flush()
except CustomException:
raise
except Exception as e: except Exception as e:
raise CustomException(msg=f"批量更新失败: {str(e)}") raise CustomException(msg=f"批量更新失败: {str(e)}")
@@ -356,8 +402,16 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
conditions.append(attr.between(val[0], val[1])) conditions.append(attr.between(val[0], val[1]))
elif seq == "!=" and val: elif seq == "!=" and val:
conditions.append(attr != val) conditions.append(attr != val)
elif seq in [">", ">=", "<=", "=="] and val: elif seq == ">" and val:
conditions.append(getattr(attr, seq.replace("==", "__eq__"))(val)) conditions.append(attr > val)
elif seq == ">=" and val:
conditions.append(attr >= val)
elif seq == "<" and val:
conditions.append(attr < val)
elif seq == "<=" and val:
conditions.append(attr <= val)
elif seq == "==" and val:
conditions.append(attr == val)
else: else:
conditions.append(attr == value) conditions.append(attr == value)
return conditions return conditions
+5 -8
View File
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from typing import Optional
from datetime import datetime from datetime import datetime
from sqlalchemy import DateTime, String, Integer, Text, ForeignKey from sqlalchemy import DateTime, String, Integer, Text, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase, declared_attr, relationship from sqlalchemy.orm import Mapped, mapped_column, DeclarativeBase, declared_attr, relationship
@@ -93,29 +94,25 @@ class UserMixin(MappedBase):
) )
@declared_attr @declared_attr
def created_by(cls) -> Mapped["UserModel"]: def created_by(cls) -> Mapped[Optional["UserModel"]]:
""" """
创建人关联关系(延迟加载,避免循环依赖) 创建人关联关系(延迟加载,避免循环依赖)
""" """
return relationship( return relationship(
"UserModel", "UserModel",
primaryjoin=f"{cls.__name__}.created_id == UserModel.id",
lazy="selectin", lazy="selectin",
foreign_keys=lambda: [cls.created_id], foreign_keys=lambda: cls.created_id,
viewonly=True,
uselist=False uselist=False
) )
@declared_attr @declared_attr
def updated_by(cls) -> Mapped["UserModel"]: def updated_by(cls) -> Mapped[Optional["UserModel"]]:
""" """
更新人关联关系(延迟加载,避免循环依赖) 更新人关联关系(延迟加载,避免循环依赖)
""" """
return relationship( return relationship(
"UserModel", "UserModel",
primaryjoin=f"{cls.__name__}.updated_id == UserModel.id",
lazy="selectin", lazy="selectin",
foreign_keys=lambda: [cls.updated_id], foreign_keys=lambda: cls.updated_id,
viewonly=True,
uselist=False uselist=False
) )
+3 -3
View File
@@ -9,9 +9,9 @@ class UserInfoSchema(BaseModel):
"""用户信息模型""" """用户信息模型"""
model_config = ConfigDict(from_attributes=True) model_config = ConfigDict(from_attributes=True)
id: int = Field(description="用户ID") id: int | None = Field(default=None, description="用户ID")
name: str = Field(description="用户姓名") name: str | None = Field(default=None, description="用户姓名")
username: str = Field(description="用户名") username: str | None = Field(default=None, description="用户名")
class CommonSchema(BaseModel): class CommonSchema(BaseModel):
+1 -1
View File
@@ -93,7 +93,7 @@ async def get_current_user(
username=username, username=username,
preload=[ preload=[
"dept", "dept",
selectinload(UserModel.roles).selectinload(RoleModel.created_by), selectinload(UserModel.roles),
"positions", "positions",
"created_by" "created_by"
] ]
+39 -7
View File
@@ -2,12 +2,16 @@
import logging import logging
import sys import sys
import atexit
from typing_extensions import override from typing_extensions import override
from loguru import logger from loguru import logger
from app.config.path_conf import LOG_DIR from app.config.path_conf import LOG_DIR
from app.config.setting import settings from app.config.setting import settings
# 全局变量记录日志处理器ID
_logger_handlers = []
class InterceptHandler(logging.Handler): class InterceptHandler(logging.Handler):
""" """
@@ -39,6 +43,22 @@ class InterceptHandler(logging.Handler):
) )
def cleanup_logging():
"""
清理日志资源
在程序退出时调用,确保所有日志处理器被正确关闭
"""
global _logger_handlers
for handler_id in _logger_handlers:
try:
logger.remove(handler_id)
except Exception:
pass
_logger_handlers.clear()
def setup_logging(): def setup_logging():
""" """
配置日志系统 配置日志系统
@@ -47,8 +67,10 @@ def setup_logging():
1. 控制台彩色输出 1. 控制台彩色输出
2. 文件日志轮转 2. 文件日志轮转
3. 错误日志单独存储 3. 错误日志单独存储
4. 异步日志记录 4. 智能异步策略:开发环境同步(避免reload资源泄漏),生产环境异步(高性能)
""" """
global _logger_handlers
# 添加上下文信息 # 添加上下文信息
_ = logger.configure(extra={"app_name": "FastapiAdmin"}) _ = logger.configure(extra={"app_name": "FastapiAdmin"})
# 步骤1:移除默认处理器 # 步骤1:移除默认处理器
@@ -66,16 +88,20 @@ def setup_logging():
"<level>{message}</level>" "<level>{message}</level>"
) )
# 智能选择异步策略:开发环境禁用异步(避免reload时资源泄漏),生产环境启用异步(提升性能)
use_async = not settings.DEBUG
# 步骤3:配置控制台输出 # 步骤3:配置控制台输出
_ = logger.add( handler_id = logger.add(
sys.stdout, sys.stdout,
format=log_format, format=log_format,
level="DEBUG" if settings.DEBUG else "INFO", level="DEBUG" if settings.DEBUG else "INFO",
enqueue=True, # 启用异步写入 enqueue=use_async, # 开发同步,生产异步
backtrace=True, # 显示完整的异常回溯 backtrace=True, # 显示完整的异常回溯
diagnose=True, # 显示变量值等诊断信息 diagnose=True, # 显示变量值等诊断信息
colorize=True # 启用彩色输出 colorize=True # 启用彩色输出
) )
_logger_handlers.append(handler_id)
# 步骤4:创建日志目录 # 步骤4:创建日志目录
log_dir = LOG_DIR log_dir = LOG_DIR
@@ -83,7 +109,7 @@ def setup_logging():
log_dir.mkdir(parents=True, exist_ok=True) log_dir.mkdir(parents=True, exist_ok=True)
# 步骤5:配置常规日志文件 # 步骤5:配置常规日志文件
_ = logger.add( handler_id = logger.add(
str(log_dir / "info.log"), str(log_dir / "info.log"),
format=log_format, format=log_format,
level="INFO", level="INFO",
@@ -91,11 +117,12 @@ def setup_logging():
retention=30, # 日志保留天数,超过此天数的日志文件将被自动清理 retention=30, # 日志保留天数,超过此天数的日志文件将被自动清理
compression="gz", compression="gz",
encoding="utf-8", encoding="utf-8",
enqueue=True enqueue=use_async # 开发同步,生产异步
) )
_logger_handlers.append(handler_id)
# 步骤6:配置错误日志文件 # 步骤6:配置错误日志文件
_ = logger.add( handler_id = logger.add(
str(log_dir / "error.log"), str(log_dir / "error.log"),
format=log_format, format=log_format,
level="ERROR", level="ERROR",
@@ -103,18 +130,23 @@ def setup_logging():
retention=30, # 日志保留天数,超过此天数的日志文件将被自动清理 retention=30, # 日志保留天数,超过此天数的日志文件将被自动清理
compression="gz", compression="gz",
encoding="utf-8", encoding="utf-8",
enqueue=True, enqueue=use_async, # 开发同步,生产异步
backtrace=True, backtrace=True,
diagnose=True diagnose=True
) )
_logger_handlers.append(handler_id)
# 步骤7:配置标准库日志 # 步骤7:配置标准库日志
logging.basicConfig(handlers=[InterceptHandler()], level="DEBUG" if settings.DEBUG else "INFO", force=True) logging.basicConfig(handlers=[InterceptHandler()], level="DEBUG" if settings.DEBUG else "INFO", force=True)
logger_name_list = [name for name in logging.root.manager.loggerDict] logger_name_list = [name for name in logging.root.manager.loggerDict]
# 步骤8:配置第三方库日志 # 步骤8:配置第三方库日志
for logger_name in logger_name_list: for logger_name in logger_name_list:
_logger = logging.getLogger(logger_name) _logger = logging.getLogger(logger_name)
_logger.handlers = [InterceptHandler()] _logger.handlers = [InterceptHandler()]
_logger.propagate = False _logger.propagate = False
# 注册退出清理函数
atexit.register(cleanup_logging)
log = logger log = logger
+67 -65
View File
@@ -2,10 +2,9 @@
from typing import Any from typing import Any
from sqlalchemy.sql.elements import ColumnElement from sqlalchemy.sql.elements import ColumnElement
from sqlalchemy import select, and_ from sqlalchemy import select
from app.api.v1.module_system.user.model import UserModel from app.api.v1.module_system.user.model import UserModel
from app.api.v1.module_system.dept.model import DeptModel from app.api.v1.module_system.dept.model import DeptModel
from app.api.v1.module_system.role.model import RoleModel
from app.api.v1.module_system.auth.schema import AuthSchema from app.api.v1.module_system.auth.schema import AuthSchema
from app.utils.common_util import get_child_id_map, get_child_recursion from app.utils.common_util import get_child_id_map, get_child_recursion
@@ -59,7 +58,11 @@ class Permission:
3. 本部门及以下数据权限 - 可以查看本部门及所有子部门的数据 3. 本部门及以下数据权限 - 可以查看本部门及所有子部门的数据
4. 全部数据权限 - 可以查看所有数据 4. 全部数据权限 - 可以查看所有数据
5. 自定义数据权限 - 通过role_dept_relation表定义可访问的部门列表 5. 自定义数据权限 - 通过role_dept_relation表定义可访问的部门列表
构造权限过滤表达式,返回None表示不限制。
权限处理原则:
- 多个角色的权限取并集(最宽松原则)
- 优先级:全部数据 > 部门权限(2、3、5的并集)> 仅本人
- 构造权限过滤表达式,返回None表示不限制
""" """
# 如果不需要检查数据权限,则不限制 # 如果不需要检查数据权限,则不限制
if not self.auth.user: if not self.auth.user:
@@ -69,89 +72,88 @@ class Permission:
if not self.auth.check_data_scope: if not self.auth.check_data_scope:
return None return None
# 如果模型没有创建人creator_id字段,则不限制 # 如果模型没有创建人created_id字段,则不限制
if not hasattr(self.model, "creator_id"): if not hasattr(self.model, "created_id"):
return None return None
# 超级管理员可以查看所有数据 # 超级管理员可以查看所有数据
if self.auth.user.is_superuser: if self.auth.user.is_superuser:
return None return None
# 如果用户没有部门或角色,则只能查看自己的数据 # 如果用户没有角色,则只能查看自己的数据
if not getattr(self.auth.user, "dept_id", None) or not getattr(self.auth.user, "roles", None): roles = getattr(self.auth.user, "roles", []) or []
creator_id_attr = getattr(self.model, "creator_id", None) if not roles:
if creator_id_attr is not None: created_id_attr = getattr(self.model, "created_id", None)
return creator_id_attr == self.auth.user.id if created_id_attr is not None:
return created_id_attr == self.auth.user.id
return None return None
# 获取用户所有角色的权限范围 # 获取用户所有角色的权限范围
data_scopes = set() data_scopes = set()
dept_ids = set() custom_dept_ids = set() # 自定义权限(data_scope=5)关联的部门ID集合
roles = getattr(self.auth.user, "roles", []) or []
for role in roles: 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) data_scopes.add(role.data_scope)
# 收集自定义权限(data_scope=5)关联的部门ID
if role.data_scope == self.DATA_SCOPE_CUSTOM and hasattr(role, 'depts') and role.depts:
for dept in role.depts:
custom_dept_ids.add(dept.id)
# 如果有全部数据权限,直接返回 # 权限优先级处理:全部数据权限最高优先级
if self.DATA_SCOPE_ALL in data_scopes: if self.DATA_SCOPE_ALL in data_scopes:
# 全部数据权限
return None return None
# 如果有自定义数据权限且部门ID存在,优先处理 # 收集所有可访问的部门ID(2、3、5权限的并集)
if self.DATA_SCOPE_CUSTOM in data_scopes and dept_ids: accessible_dept_ids = set()
# 自定义数据权限 user_dept_id = getattr(self.auth.user, "dept_id", None)
creator_rel = getattr(self.model, "creator", None)
if hasattr(UserModel, 'dept_id') and creator_rel is not None: # 处理自定义数据权限(5)
return creator_rel.has(getattr(UserModel, 'dept_id').in_(list(dept_ids))) if self.DATA_SCOPE_CUSTOM in data_scopes:
accessible_dept_ids.update(custom_dept_ids)
# 处理本部门数据权限(2)
if self.DATA_SCOPE_DEPT in data_scopes:
if user_dept_id is not None:
accessible_dept_ids.add(user_dept_id)
# 处理本部门及以下数据权限(3)
if self.DATA_SCOPE_DEPT_AND_CHILD in data_scopes:
if user_dept_id is not None:
try:
# 查询所有部门并递归获取子部门
dept_sql = select(DeptModel)
dept_result = await self.auth.db.execute(dept_sql)
dept_objs = dept_result.scalars().all()
id_map = get_child_id_map(dept_objs)
# get_child_recursion返回的结果已包含自身ID和所有子部门ID
dept_with_children_ids = get_child_recursion(id=user_dept_id, id_map=id_map)
accessible_dept_ids.update(dept_with_children_ids)
except Exception:
# 查询失败时降级到本部门
accessible_dept_ids.add(user_dept_id)
# 如果有部门权限(2、3、5任一),使用部门过滤
if accessible_dept_ids:
creator_rel = getattr(self.model, "created_by", None)
# 优先使用关系过滤(性能更好)
if creator_rel is not None and hasattr(UserModel, 'dept_id'):
return creator_rel.has(getattr(UserModel, 'dept_id').in_(list(accessible_dept_ids)))
# 降级方案:如果模型没有created_by关系但有created_id,则只能查看自己的数据
else: else:
creator_id_attr = getattr(self.model, "creator_id", None) created_id_attr = getattr(self.model, "created_id", None)
if creator_id_attr is not None: if created_id_attr is not None:
return creator_id_attr == self.auth.user.id return created_id_attr == self.auth.user.id
return None return None
# 处理其他数据权限范围 # 处理仅本人数据权限(1)
dept_id_val = getattr(self.auth.user, "dept_id", None)
if self.DATA_SCOPE_SELF in data_scopes: if self.DATA_SCOPE_SELF in data_scopes:
# 仅本人数据 created_id_attr = getattr(self.model, "created_id", None)
creator_id_attr = getattr(self.model, "creator_id", None) if created_id_attr is not None:
if creator_id_attr is not None: return created_id_attr == self.auth.user.id
return creator_id_attr == self.auth.user.id
return None return None
if self.DATA_SCOPE_DEPT in data_scopes and dept_id_val is not None: # 默认情况:如果用户有角色但没有任何有效权限范围,只能查看自己的数据
# 本部门数据 created_id_attr = getattr(self.model, "created_id", None)
dept_ids.add(dept_id_val) if created_id_attr is not None:
return created_id_attr == self.auth.user.id
if self.DATA_SCOPE_DEPT_AND_CHILD in data_scopes and dept_id_val is not None:
# 本部门及以下数据(查询所有部门并递归)
dept_sql = select(DeptModel)
dept_result = await self.auth.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=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)
# 处理2、3汇总的数据权限
if (self.DATA_SCOPE_DEPT in data_scopes or self.DATA_SCOPE_DEPT_AND_CHILD 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.auth.user.id
return None
# 默认情况下,只能查看自己的数据
creator_id_attr = getattr(self.model, "creator_id", None)
if creator_id_attr is not None:
return creator_id_attr == self.auth.user.id
return None return None
+1 -13
View File
@@ -5,18 +5,6 @@
"status": "0", "status": "0",
"order": 1, "order": 1,
"data_scope": 4, "data_scope": 4,
"description": "初始化角色", "description": "初始化角色"
"created_id": 1,
"updated_id": 1
},
{
"name": "普通角色",
"code": "COMMON",
"status": "0",
"order": 1,
"data_scope": 3,
"description": "初始化角色",
"created_id": 1,
"updated_id": 1
} }
] ]
+1 -1
View File
@@ -32,10 +32,10 @@ class InitializeData:
MenuModel, MenuModel,
ParamsModel, ParamsModel,
DeptModel, DeptModel,
RoleModel,
DictTypeModel, DictTypeModel,
DictDataModel, DictDataModel,
UserModel, UserModel,
RoleModel,
UserRolesModel, UserRolesModel,
] ]
+1 -1
View File
@@ -22,7 +22,7 @@ DESCRIPTION = "该项目是一个基于python的web服务框架,基于fastapi
DEMO_ENABLE = False DEMO_ENABLE = False
# 数据库配置 # 数据库配置
DATABASE_TYPE = "postgres" # mysql、postgres DATABASE_TYPE = "postgres" # mysql、postgres
# 数据库配置 # 数据库配置
DATABASE_HOST = "localhost" DATABASE_HOST = "localhost"
+3
View File
@@ -84,6 +84,9 @@ def run(env: Annotated[EnvironmentEnum, typer.Option("--env", help="运行环境
display_shutdown_info() display_shutdown_info()
except Exception as e: except Exception as e:
raise raise
finally:
from app.core.logger import cleanup_logging
cleanup_logging()
@fastapiadmin_cli.command(name="revision", help="生成新的 Alembic 迁移脚本, 运行 python main.py revision --env=dev") @fastapiadmin_cli.command(name="revision", help="生成新的 Alembic 迁移脚本, 运行 python main.py revision --env=dev")
def revision(env: Annotated[EnvironmentEnum, typer.Option("--env", help="运行环境 (dev, prod)")] = EnvironmentEnum.DEV) -> None: def revision(env: Annotated[EnvironmentEnum, typer.Option("--env", help="运行环境 (dev, prod)")] = EnvironmentEnum.DEV) -> None:
-4
View File
@@ -73,8 +73,6 @@ export default RoleAPI;
export interface TablePageQuery extends PageQuery { export interface TablePageQuery extends PageQuery {
name?: string; name?: string;
status?: string; status?: string;
created_id?: number;
updated_id?: number;
created_time?: string[]; created_time?: string[];
updated_time?: string[]; updated_time?: string[];
} }
@@ -87,8 +85,6 @@ export interface RoleTable extends BaseType {
data_scope?: number; data_scope?: number;
menus?: permissionMenuType[]; menus?: permissionMenuType[];
depts?: permissionDeptType[]; depts?: permissionDeptType[];
created_by?: creatorType;
updated_by?: updatorType;
} }
export interface RoleForm extends BaseFormType { export interface RoleForm extends BaseFormType {
@@ -282,6 +282,16 @@
<el-tag>{{ scope.row.created_by?.name }}</el-tag> <el-tag>{{ scope.row.created_by?.name }}</el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column
v-if="tableColumns.find((col) => col.prop === 'updated_id')?.show"
label="更新人"
prop="updated_id"
min-width="120"
>
<template #default="scope">
<el-tag>{{ scope.row.updated_by?.name }}</el-tag>
</template>
</el-table-column>
<el-table-column <el-table-column
v-if="tableColumns.find((col) => col.prop === 'operation')?.show" v-if="tableColumns.find((col) => col.prop === 'operation')?.show"
fixed="right" fixed="right"
@@ -28,13 +28,6 @@
<el-form-item v-if="isExpand" prop="start_time" label="创建时间"> <el-form-item v-if="isExpand" prop="start_time" label="创建时间">
<DatePicker v-model="dateRange" @update:model-value="handleDateRangeChange" /> <DatePicker v-model="dateRange" @update:model-value="handleDateRangeChange" />
</el-form-item> </el-form-item>
<el-form-item v-if="isExpand" prop="created_id" label="创建人">
<UserTableSelect
v-model="queryFormData.created_id"
@confirm-click="handleConfirm"
@clear-click="handleQuery"
/>
</el-form-item>
<!-- 查询、重置、展开/收起按钮 --> <!-- 查询、重置、展开/收起按钮 -->
<el-form-item class="search-buttons"> <el-form-item class="search-buttons">
<el-button <el-button
@@ -403,12 +396,6 @@
{{ detailFormData.status ? "启用" : "停用" }} {{ detailFormData.status ? "启用" : "停用" }}
</el-tag> </el-tag>
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="创建人" :span="2">
{{ detailFormData.created_by?.name }}
</el-descriptions-item>
<el-descriptions-item label="更新人" :span="2">
{{ detailFormData.updated_by?.name }}
</el-descriptions-item>
<el-descriptions-item label="创建时间" :span="2"> <el-descriptions-item label="创建时间" :span="2">
{{ detailFormData.created_time }} {{ detailFormData.created_time }}
</el-descriptions-item> </el-descriptions-item>
@@ -503,7 +490,6 @@ defineOptions({
import { ElMessage, ElMessageBox } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import RoleAPI, { RoleTable, RoleForm, TablePageQuery } from "@/api/module_system/role"; import RoleAPI, { RoleTable, RoleForm, TablePageQuery } from "@/api/module_system/role";
import { useUserStore } from "@/store"; import { useUserStore } from "@/store";
import UserTableSelect from "@/views/module_system/user/components/UserTableSelect.vue";
import ExportModal from "@/components/CURD/ExportModal.vue"; import ExportModal from "@/components/CURD/ExportModal.vue";
import type { IContentConfig } from "@/components/CURD/types"; import type { IContentConfig } from "@/components/CURD/types";
import { QuestionFilled, ArrowUp, ArrowDown } from "@element-plus/icons-vue"; import { QuestionFilled, ArrowUp, ArrowDown } from "@element-plus/icons-vue";
@@ -551,8 +537,6 @@ const queryFormData = reactive<TablePageQuery>({
page_size: 10, page_size: 10,
name: "", name: "",
status: undefined, status: undefined,
created_id: undefined,
updated_id: undefined,
created_time: undefined, created_time: undefined,
updated_time: undefined, updated_time: undefined,
}); });
@@ -606,11 +590,6 @@ async function handleQuery() {
loadingData(); loadingData();
} }
// 选择创建人后触发查询
function handleConfirm() {
handleQuery();
}
// 行复选框选中项变化 // 行复选框选中项变化
async function handleSelectionChange(selection: any) { async function handleSelectionChange(selection: any) {
selectIds.value = selection.map((item: any) => item.id); selectIds.value = selection.map((item: any) => item.id);