mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-10-13 20:51:09 +00:00
最新代码
This commit is contained in:
@@ -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 # 防止级联操作
|
||||||
|
)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
@@ -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"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -32,10 +32,10 @@ class InitializeData:
|
|||||||
MenuModel,
|
MenuModel,
|
||||||
ParamsModel,
|
ParamsModel,
|
||||||
DeptModel,
|
DeptModel,
|
||||||
|
RoleModel,
|
||||||
DictTypeModel,
|
DictTypeModel,
|
||||||
DictDataModel,
|
DictDataModel,
|
||||||
UserModel,
|
UserModel,
|
||||||
RoleModel,
|
|
||||||
UserRolesModel,
|
UserRolesModel,
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
-1
@@ -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"
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user