mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-21 04:46:26 +00:00
chore: update .gitignore and enhance docstrings across multiple files
- Added *.pyc and *.pyo to .gitignore to prevent compiled Python files from being tracked. - Improved docstrings in various modules, providing clearer descriptions of functions, parameters, and return values to enhance code readability and maintainability.
This commit is contained in:
@@ -1,4 +1,6 @@
|
|||||||
__pycache__
|
__pycache__
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
.vscode
|
.vscode
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ def run_migrations_offline() -> None:
|
|||||||
Calls to context.execute() here emit the given string to the
|
Calls to context.execute() here emit the given string to the
|
||||||
script output.
|
script output.
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
url = alembic_config.get_main_option("sqlalchemy.url")
|
url = alembic_config.get_main_option("sqlalchemy.url")
|
||||||
# 确保URL不为None
|
# 确保URL不为None
|
||||||
@@ -84,6 +86,8 @@ def run_migrations_online() -> None:
|
|||||||
In this scenario we need to create an Engine
|
In this scenario we need to create an Engine
|
||||||
and associate a connection with the context.
|
and associate a connection with the context.
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
url = alembic_config.get_main_option("sqlalchemy.url")
|
url = alembic_config.get_main_option("sqlalchemy.url")
|
||||||
# 确保URL不为None
|
# 确保URL不为None
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ from pydantic.alias_generators import to_camel
|
|||||||
|
|
||||||
|
|
||||||
class ImportFieldModel(BaseModel):
|
class ImportFieldModel(BaseModel):
|
||||||
|
"""
|
||||||
|
Excel 导入时单字段映射配置(数据库列、Excel 列、默认值、是否必选等)。
|
||||||
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True)
|
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True)
|
||||||
|
|
||||||
base_column: str | None = Field(description="数据库字段名", default=None)
|
base_column: str | None = Field(description="数据库字段名", default=None)
|
||||||
@@ -42,6 +46,10 @@ class ImportFieldModel(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class ImportModel(BaseModel):
|
class ImportModel(BaseModel):
|
||||||
|
"""
|
||||||
|
Excel 导入请求体:目标表、Sheet、文件名及字段映射列表。
|
||||||
|
"""
|
||||||
|
|
||||||
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True)
|
model_config = ConfigDict(alias_generator=to_camel, from_attributes=True)
|
||||||
|
|
||||||
table_name: str | None = Field(description="表名", default=None)
|
table_name: str | None = Field(description="表名", default=None)
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ async def get_monitor_cache_info_controller(
|
|||||||
"""
|
"""
|
||||||
获取缓存监控统计信息
|
获取缓存监控统计信息
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- redis (Redis): Redis 客户端对象
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- JSONResponse: 包含缓存监控统计信息的JSON响应
|
- JSONResponse: 包含缓存监控统计信息的JSON响应
|
||||||
"""
|
"""
|
||||||
@@ -180,6 +183,9 @@ async def clear_monitor_cache_all_controller(
|
|||||||
"""
|
"""
|
||||||
清除所有缓存
|
清除所有缓存
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- redis (Redis): Redis 客户端对象
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- JSONResponse: 包含清除结果的JSON响应
|
- JSONResponse: 包含清除结果的JSON响应
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -93,6 +93,18 @@ class ResourceMoveSchema(BaseModel):
|
|||||||
@field_validator("source_path", "target_path")
|
@field_validator("source_path", "target_path")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_paths(cls, value: str):
|
def validate_paths(cls, value: str):
|
||||||
|
"""
|
||||||
|
校验移动/复制涉及的源路径与目标路径非空并去首尾空格。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str): 路径字段当前值。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 去空格后的路径。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 路径为空时抛出。
|
||||||
|
"""
|
||||||
if not value or len(value.strip()) == 0:
|
if not value or len(value.strip()) == 0:
|
||||||
raise ValueError("路径不能为空")
|
raise ValueError("路径不能为空")
|
||||||
return value.strip()
|
return value.strip()
|
||||||
@@ -113,6 +125,18 @@ class ResourceRenameSchema(BaseModel):
|
|||||||
@field_validator("old_path", "new_name")
|
@field_validator("old_path", "new_name")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_inputs(cls, value: str):
|
def validate_inputs(cls, value: str):
|
||||||
|
"""
|
||||||
|
校验重命名所需的原路径与新名称非空并去首尾空格。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str): 字段当前值。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 去空格后的值。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 值为空时抛出。
|
||||||
|
"""
|
||||||
if not value or len(value.strip()) == 0:
|
if not value or len(value.strip()) == 0:
|
||||||
raise ValueError("参数不能为空")
|
raise ValueError("参数不能为空")
|
||||||
return value.strip()
|
return value.strip()
|
||||||
@@ -137,6 +161,19 @@ class ResourceCreateDirSchema(BaseModel):
|
|||||||
@field_validator("parent_path", "dir_name")
|
@field_validator("parent_path", "dir_name")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_inputs(cls, value: str, info):
|
def validate_inputs(cls, value: str, info):
|
||||||
|
"""
|
||||||
|
校验创建目录的父路径与目录名,防止路径遍历等不安全输入。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str): 当前字段值。
|
||||||
|
- info: Pydantic 校验上下文(含 `field_name`)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 规范化后的字段值。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 含不安全字符或目录名为空时抛出。
|
||||||
|
"""
|
||||||
# 对于parent_path允许为空字符串(表示根目录)或 '/',其他情况必须非空
|
# 对于parent_path允许为空字符串(表示根目录)或 '/',其他情况必须非空
|
||||||
if info.field_name == "parent_path":
|
if info.field_name == "parent_path":
|
||||||
# 对于parent_path仍然严格检查路径遍历
|
# 对于parent_path仍然严格检查路径遍历
|
||||||
|
|||||||
@@ -77,6 +77,15 @@ class ResourceService:
|
|||||||
root_static_prefix = f"{root_prefix}{static_prefix}" if root_prefix else static_prefix
|
root_static_prefix = f"{root_prefix}{static_prefix}" if root_prefix else static_prefix
|
||||||
|
|
||||||
def strip_prefix(p: str) -> str:
|
def strip_prefix(p: str) -> str:
|
||||||
|
"""
|
||||||
|
去掉静态资源 URL 前缀,得到相对 upload 的路径片段。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- p (str): 原始路径或 URL 路径段。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 去掉已知前缀后的路径。
|
||||||
|
"""
|
||||||
if p.startswith(root_static_prefix):
|
if p.startswith(root_static_prefix):
|
||||||
return p[len(root_static_prefix) :].lstrip("/")
|
return p[len(root_static_prefix) :].lstrip("/")
|
||||||
if p.startswith(static_prefix):
|
if p.startswith(static_prefix):
|
||||||
@@ -560,6 +569,15 @@ class ResourceService:
|
|||||||
if isinstance(sort_conditions, list):
|
if isinstance(sort_conditions, list):
|
||||||
# 构建排序键函数
|
# 构建排序键函数
|
||||||
def sort_key(item):
|
def sort_key(item):
|
||||||
|
"""
|
||||||
|
按多条排序条件从资源项中抽取比较键(支持时间字段转 datetime)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- item (dict): 单条资源详情字典。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- list: 用于 `sorted` 的多字段键列表。
|
||||||
|
"""
|
||||||
keys = []
|
keys = []
|
||||||
for cond in sort_conditions:
|
for cond in sort_conditions:
|
||||||
field = cond.get("field", "name")
|
field = cond.get("field", "name")
|
||||||
@@ -797,10 +815,10 @@ class ResourceService:
|
|||||||
批量删除文件或目录
|
批量删除文件或目录
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- paths (List[str]): 文件或目录路径列表。
|
- paths (list[str]): 文件或目录路径列表。
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- Dict[str, List[str]]: 包含成功删除路径和失败删除路径的字典。
|
- dict[str, list[str]]: 键 `success` / `failed` 对应成功与失败路径列表。
|
||||||
"""
|
"""
|
||||||
if not paths:
|
if not paths:
|
||||||
raise CustomException(msg="删除失败,删除路径不能为空")
|
raise CustomException(msg="删除失败,删除路径不能为空")
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ async def login_for_access_token_controller(
|
|||||||
|
|
||||||
参数:
|
参数:
|
||||||
- request (Request): FastAPI请求对象
|
- request (Request): FastAPI请求对象
|
||||||
|
- redis (Redis): Redis 客户端对象
|
||||||
- login_form (CustomOAuth2PasswordRequestForm): 登录表单数据
|
- login_form (CustomOAuth2PasswordRequestForm): 登录表单数据
|
||||||
- db (AsyncSession): 数据库会话对象
|
- db (AsyncSession): 数据库会话对象
|
||||||
|
|
||||||
@@ -82,6 +83,8 @@ async def get_new_token_controller(
|
|||||||
参数:
|
参数:
|
||||||
- request (Request): FastAPI请求对象
|
- request (Request): FastAPI请求对象
|
||||||
- payload (RefreshTokenPayloadSchema): 刷新令牌负载模型
|
- payload (RefreshTokenPayloadSchema): 刷新令牌负载模型
|
||||||
|
- db (AsyncSession): 数据库会话对象
|
||||||
|
- redis (Redis): Redis 客户端对象
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- JWTOutSchema: 包含新的访问令牌和刷新令牌的响应模型
|
- JWTOutSchema: 包含新的访问令牌和刷新令牌的响应模型
|
||||||
|
|||||||
@@ -25,6 +25,15 @@ class JWTPayloadSchema(BaseModel):
|
|||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def validate_fields(self):
|
def validate_fields(self):
|
||||||
|
"""
|
||||||
|
校验 JWT 载荷字段的基本合法性。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JWTPayloadSchema: 校验后的载荷实例。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 必填字段为空或格式不正确时抛出。
|
||||||
|
"""
|
||||||
if not self.sub or len(self.sub.strip()) == 0:
|
if not self.sub or len(self.sub.strip()) == 0:
|
||||||
raise ValueError("会话编号不能为空")
|
raise ValueError("会话编号不能为空")
|
||||||
return self
|
return self
|
||||||
|
|||||||
@@ -11,7 +11,15 @@ class DeptCRUD(CRUDBase[DeptModel, DeptCreateSchema, DeptUpdateSchema]):
|
|||||||
"""部门模块数据层"""
|
"""部门模块数据层"""
|
||||||
|
|
||||||
def __init__(self, auth: AuthSchema) -> None:
|
def __init__(self, auth: AuthSchema) -> None:
|
||||||
"""初始化部门CRUD"""
|
"""
|
||||||
|
初始化部门数据层。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息模型(含 DB 会话等上下文)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
super().__init__(model=DeptModel, auth=auth)
|
super().__init__(model=DeptModel, auth=auth)
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,18 @@ class DeptCreateSchema(BaseModel):
|
|||||||
@field_validator("name")
|
@field_validator("name")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_name(cls, value: str):
|
def validate_name(cls, value: str):
|
||||||
|
"""
|
||||||
|
校验并规范化部门名称(去空格、非空)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str): 部门名称。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 规范化后的部门名称。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 部门名称为空时抛出。
|
||||||
|
"""
|
||||||
if not value or len(value.strip()) == 0:
|
if not value or len(value.strip()) == 0:
|
||||||
raise ValueError("部门名称不能为空")
|
raise ValueError("部门名称不能为空")
|
||||||
value = value.replace(" ", "")
|
value = value.replace(" ", "")
|
||||||
@@ -30,6 +42,18 @@ class DeptCreateSchema(BaseModel):
|
|||||||
@field_validator("code")
|
@field_validator("code")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_code(cls, value: str | None):
|
def validate_code(cls, value: str | None):
|
||||||
|
"""
|
||||||
|
校验部门编码:字母开头,仅包含字母/数字/下划线;空字符串视为 None。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str | None): 部门编码。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str | None: 规范化后的部门编码或 None。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 编码不满足格式要求时抛出。
|
||||||
|
"""
|
||||||
if value is None:
|
if value is None:
|
||||||
return value
|
return value
|
||||||
v = value.strip()
|
v = value.strip()
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ class DeptService:
|
|||||||
|
|
||||||
参数:
|
参数:
|
||||||
- auth (AuthSchema): 认证对象。
|
- auth (AuthSchema): 认证对象。
|
||||||
- ids (List[int]): 部门 ID 列表。
|
- ids (list[int]): 部门 ID 列表。
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- None
|
- None
|
||||||
|
|||||||
@@ -16,10 +16,13 @@ class DictTypeCRUD(CRUDBase[DictTypeModel, DictTypeCreateSchema, DictTypeUpdateS
|
|||||||
|
|
||||||
def __init__(self, auth: AuthSchema) -> None:
|
def __init__(self, auth: AuthSchema) -> None:
|
||||||
"""
|
"""
|
||||||
初始化数据字典类型CRUD
|
初始化数据字典类型数据层。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- auth (AuthSchema): 认证信息模型
|
- auth (AuthSchema): 认证信息模型(含 DB 会话等上下文)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
super().__init__(model=DictTypeModel, auth=auth)
|
super().__init__(model=DictTypeModel, auth=auth)
|
||||||
@@ -119,7 +122,7 @@ class DictTypeCRUD(CRUDBase[DictTypeModel, DictTypeCreateSchema, DictTypeUpdateS
|
|||||||
批量删除数据字典类型
|
批量删除数据字典类型
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- ids (List[int]): 数据字典类型ID列表
|
- ids (list[int]): 数据字典类型ID列表
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- int: 删除的记录数量
|
- int: 删除的记录数量
|
||||||
@@ -133,10 +136,13 @@ class DictDataCRUD(CRUDBase[DictDataModel, DictDataCreateSchema, DictDataUpdateS
|
|||||||
|
|
||||||
def __init__(self, auth: AuthSchema) -> None:
|
def __init__(self, auth: AuthSchema) -> None:
|
||||||
"""
|
"""
|
||||||
初始化数据字典数据CRUD
|
初始化数据字典项数据层。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- auth (AuthSchema): 认证信息模型
|
- auth (AuthSchema): 认证信息模型(含 DB 会话等上下文)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
super().__init__(model=DictDataModel, auth=auth)
|
super().__init__(model=DictDataModel, auth=auth)
|
||||||
@@ -236,7 +242,7 @@ class DictDataCRUD(CRUDBase[DictDataModel, DictDataCreateSchema, DictDataUpdateS
|
|||||||
批量删除数据字典数据
|
批量删除数据字典数据
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- ids (List[int]): 数据字典数据ID列表
|
- ids (list[int]): 数据字典数据ID列表
|
||||||
- exclude_system (bool): 是否排除系统默认数据,默认为True
|
- exclude_system (bool): 是否排除系统默认数据,默认为True
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
|
|||||||
@@ -26,12 +26,36 @@ class DictTypeCreateSchema(BaseModel):
|
|||||||
|
|
||||||
@field_validator("dict_name")
|
@field_validator("dict_name")
|
||||||
def validate_dict_name(cls, value: str):
|
def validate_dict_name(cls, value: str):
|
||||||
|
"""
|
||||||
|
校验字典名称为非空字符串。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str): 字典名称。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 去首尾空格后的字典名称。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 字典名称为空时抛出。
|
||||||
|
"""
|
||||||
if not value or value.strip() == "":
|
if not value or value.strip() == "":
|
||||||
raise ValueError("字典名称不能为空")
|
raise ValueError("字典名称不能为空")
|
||||||
return value.strip()
|
return value.strip()
|
||||||
|
|
||||||
@field_validator("dict_type")
|
@field_validator("dict_type")
|
||||||
def validate_dict_type(cls, value: str):
|
def validate_dict_type(cls, value: str):
|
||||||
|
"""
|
||||||
|
校验字典类型:小写字母开头,仅包含小写字母/数字/下划线。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str): 字典类型。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 去首尾空格后的字典类型。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 字典类型为空或不满足格式要求时抛出。
|
||||||
|
"""
|
||||||
if not value or value.strip() == "":
|
if not value or value.strip() == "":
|
||||||
raise ValueError("字典类型不能为空")
|
raise ValueError("字典类型不能为空")
|
||||||
regexp = r"^[a-z][a-z0-9_]*$"
|
regexp = r"^[a-z][a-z0-9_]*$"
|
||||||
@@ -105,6 +129,15 @@ class DictDataCreateSchema(BaseModel):
|
|||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def validate_after(self):
|
def validate_after(self):
|
||||||
|
"""
|
||||||
|
校验并规范化字典数据字段(标签/键值/类型/类型ID)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- DictDataCreateSchema: 校验与去空格后的同一实例。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 必填字段为空或类型ID非法时抛出。
|
||||||
|
"""
|
||||||
if not self.dict_label or not self.dict_label.strip():
|
if not self.dict_label or not self.dict_label.strip():
|
||||||
raise ValueError("字典标签不能为空")
|
raise ValueError("字典标签不能为空")
|
||||||
if not self.dict_value or not self.dict_value.strip():
|
if not self.dict_value or not self.dict_value.strip():
|
||||||
|
|||||||
@@ -76,7 +76,19 @@ class DictTypeService:
|
|||||||
search: DictTypeQueryParam | None = None,
|
search: DictTypeQueryParam | None = None,
|
||||||
order_by: list[dict] | None = None,
|
order_by: list[dict] | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""分页查询字典类型(数据库 OFFSET/LIMIT)。"""
|
"""
|
||||||
|
分页查询字典类型(数据库 OFFSET/LIMIT)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息模型
|
||||||
|
- page_no (int): 页码(从 1 开始)
|
||||||
|
- page_size (int): 每页条数
|
||||||
|
- search (DictTypeQueryParam | None): 查询条件
|
||||||
|
- order_by (list[dict] | None): 排序字段列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 分页结果(结构由 `CRUD.page` 返回约定)
|
||||||
|
"""
|
||||||
offset = (page_no - 1) * page_size
|
offset = (page_no - 1) * page_size
|
||||||
return await DictTypeCRUD(auth).page(
|
return await DictTypeCRUD(auth).page(
|
||||||
offset=offset,
|
offset=offset,
|
||||||
@@ -336,7 +348,19 @@ class DictDataService:
|
|||||||
search: DictDataQueryParam | None = None,
|
search: DictDataQueryParam | None = None,
|
||||||
order_by: list[dict] | None = None,
|
order_by: list[dict] | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""分页查询字典数据(数据库 OFFSET/LIMIT)。"""
|
"""
|
||||||
|
分页查询字典数据(数据库 OFFSET/LIMIT)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息模型
|
||||||
|
- page_no (int): 页码(从 1 开始)
|
||||||
|
- page_size (int): 每页条数
|
||||||
|
- search (DictDataQueryParam | None): 查询条件
|
||||||
|
- order_by (list[dict] | None): 排序字段列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 分页结果(结构由 `CRUD.page` 返回约定)
|
||||||
|
"""
|
||||||
offset = (page_no - 1) * page_size
|
offset = (page_no - 1) * page_size
|
||||||
return await DictDataCRUD(auth).page(
|
return await DictDataCRUD(auth).page(
|
||||||
offset=offset,
|
offset=offset,
|
||||||
|
|||||||
@@ -16,7 +16,13 @@ class OperationLogCRUD(
|
|||||||
|
|
||||||
def __init__(self, auth: AuthSchema) -> None:
|
def __init__(self, auth: AuthSchema) -> None:
|
||||||
"""
|
"""
|
||||||
初始化操作日志CRUD。
|
初始化操作日志数据层。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息模型(含 DB 会话等上下文)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
super().__init__(model=OperationLogModel, auth=auth)
|
super().__init__(model=OperationLogModel, auth=auth)
|
||||||
@@ -58,9 +64,9 @@ class OperationLogCRUD(
|
|||||||
获取操作日志列表。
|
获取操作日志列表。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- search (Dict | None): 搜索条件字典。
|
- search (dict | None): 搜索条件字典。
|
||||||
- order_by (List[Dict[str, str]] | None): 排序字段列表。
|
- order_by (list | None): 排序字段列表。
|
||||||
- preload (Optional[List[Union[str, Any]]]): 预加载关系,未提供时使用模型默认项
|
- preload (list | None): 预加载关系,未提供时使用模型默认项
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- Sequence[OperationLogModel]: 操作日志列表。
|
- Sequence[OperationLogModel]: 操作日志列表。
|
||||||
|
|||||||
@@ -6,12 +6,22 @@ from app.core.base_model import ModelMixin, UserMixin
|
|||||||
|
|
||||||
|
|
||||||
def get_log_text_column_type():
|
def get_log_text_column_type():
|
||||||
|
"""
|
||||||
|
根据数据库类型选择适合存储大文本的列类型。
|
||||||
|
|
||||||
|
MySQL 使用 LONGTEXT,PostgreSQL 使用 TEXT,其它数据库回退到 SQLAlchemy 的 Text。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- type: SQLAlchemy 列类型(可用于 mapped_column)。
|
||||||
|
"""
|
||||||
db_type = settings.DATABASE_TYPE
|
db_type = settings.DATABASE_TYPE
|
||||||
if db_type == "mysql":
|
if db_type == "mysql":
|
||||||
from sqlalchemy.dialects.mysql import LONGTEXT
|
from sqlalchemy.dialects.mysql import LONGTEXT
|
||||||
|
|
||||||
return LONGTEXT
|
return LONGTEXT
|
||||||
elif db_type == "postgres":
|
elif db_type == "postgres":
|
||||||
from sqlalchemy.dialects.postgresql import TEXT
|
from sqlalchemy.dialects.postgresql import TEXT
|
||||||
|
|
||||||
return TEXT
|
return TEXT
|
||||||
else:
|
else:
|
||||||
return Text
|
return Text
|
||||||
|
|||||||
@@ -65,7 +65,19 @@ class OperationLogService:
|
|||||||
search: OperationLogQueryParam | None = None,
|
search: OperationLogQueryParam | None = None,
|
||||||
order_by: list | None = None,
|
order_by: list | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""分页查询操作日志(数据库 OFFSET/LIMIT)。"""
|
"""
|
||||||
|
分页查询操作日志(数据库 OFFSET/LIMIT)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息模型
|
||||||
|
- page_no (int): 页码(从 1 开始)
|
||||||
|
- page_size (int): 每页条数
|
||||||
|
- search (OperationLogQueryParam | None): 查询条件
|
||||||
|
- order_by (list | None): 排序字段列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 分页结果(结构由 `CRUD.page` 返回约定)
|
||||||
|
"""
|
||||||
offset = (page_no - 1) * page_size
|
offset = (page_no - 1) * page_size
|
||||||
return await OperationLogCRUD(auth).page(
|
return await OperationLogCRUD(auth).page(
|
||||||
offset=offset,
|
offset=offset,
|
||||||
|
|||||||
@@ -11,7 +11,15 @@ class MenuCRUD(CRUDBase[MenuModel, MenuCreateSchema, MenuUpdateSchema]):
|
|||||||
"""菜单模块数据层"""
|
"""菜单模块数据层"""
|
||||||
|
|
||||||
def __init__(self, auth: AuthSchema) -> None:
|
def __init__(self, auth: AuthSchema) -> None:
|
||||||
"""初始化菜单CRUD"""
|
"""
|
||||||
|
初始化菜单数据层。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息模型(含 DB 会话等上下文)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
super().__init__(model=MenuModel, auth=auth)
|
super().__init__(model=MenuModel, auth=auth)
|
||||||
|
|
||||||
|
|||||||
@@ -72,6 +72,15 @@ class MenuCreateSchema(BaseModel):
|
|||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def validate_fields(self):
|
def validate_fields(self):
|
||||||
|
"""
|
||||||
|
统一校验菜单请求字段(委托到 `menu_request_validator`)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- MenuCreateSchema: 校验后的同一实例。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 字段不满足菜单类型约束时抛出。
|
||||||
|
"""
|
||||||
return menu_request_validator(self)
|
return menu_request_validator(self)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,31 @@ class MenuService:
|
|||||||
菜单模块服务层
|
菜单模块服务层
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def _validate_parent_child_type(
|
||||||
|
cls, auth: AuthSchema, parent_id: int | None, child_type: int
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
父子类型约束:目录下仅允许目录/菜单/外链;菜单下仅允许按钮;按钮与外链下不可挂子级。
|
||||||
|
无父级时仅允许目录、菜单、外链(与前端一致)。
|
||||||
|
"""
|
||||||
|
if parent_id is None:
|
||||||
|
if child_type not in (1, 2, 4):
|
||||||
|
raise CustomException(msg="顶级菜单仅允许目录、菜单或外链类型")
|
||||||
|
return
|
||||||
|
parent = await MenuCRUD(auth).get_by_id_crud(id=parent_id)
|
||||||
|
if not parent:
|
||||||
|
raise CustomException(msg="父级菜单不存在")
|
||||||
|
pt = parent.type
|
||||||
|
if pt == 1:
|
||||||
|
if child_type not in (1, 2, 4):
|
||||||
|
raise CustomException(msg="目录下仅允许新增目录、菜单或外链")
|
||||||
|
elif pt == 2:
|
||||||
|
if child_type != 3:
|
||||||
|
raise CustomException(msg="菜单下仅允许新增按钮")
|
||||||
|
else:
|
||||||
|
raise CustomException(msg="菜单或链接类型下不允许新增子菜单")
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_menu_detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
async def get_menu_detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||||
"""
|
"""
|
||||||
@@ -93,6 +118,8 @@ class MenuService:
|
|||||||
if menu:
|
if menu:
|
||||||
raise CustomException(msg="创建失败,该菜单已存在")
|
raise CustomException(msg="创建失败,该菜单已存在")
|
||||||
|
|
||||||
|
await cls._validate_parent_child_type(auth, data.parent_id, data.type)
|
||||||
|
|
||||||
new_menu = await MenuCRUD(auth).create(data=data)
|
new_menu = await MenuCRUD(auth).create(data=data)
|
||||||
new_menu_dict = MenuOutSchema.model_validate(new_menu).model_dump()
|
new_menu_dict = MenuOutSchema.model_validate(new_menu).model_dump()
|
||||||
return new_menu_dict
|
return new_menu_dict
|
||||||
@@ -113,6 +140,7 @@ class MenuService:
|
|||||||
menu = await MenuCRUD(auth).get_by_id_crud(id=id)
|
menu = await MenuCRUD(auth).get_by_id_crud(id=id)
|
||||||
if not menu:
|
if not menu:
|
||||||
raise CustomException(msg="更新失败,该菜单不存在")
|
raise CustomException(msg="更新失败,该菜单不存在")
|
||||||
|
await cls._validate_parent_child_type(auth, data.parent_id, data.type)
|
||||||
search: dict[str, Any] = {"title": data.title}
|
search: dict[str, Any] = {"title": data.title}
|
||||||
if data.parent_id is not None:
|
if data.parent_id is not None:
|
||||||
search["parent_id"] = data.parent_id
|
search["parent_id"] = data.parent_id
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ class NoticeCRUD(CRUDBase[NoticeModel, NoticeCreateSchema, NoticeUpdateSchema]):
|
|||||||
初始化公告数据层。
|
初始化公告数据层。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- auth (AuthSchema): 认证信息模型。
|
- auth (AuthSchema): 认证信息模型(含 DB 会话等上下文)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
super().__init__(model=NoticeModel, auth=auth)
|
super().__init__(model=NoticeModel, auth=auth)
|
||||||
|
|||||||
@@ -84,7 +84,19 @@ class NoticeService:
|
|||||||
search: NoticeQueryParam | None = None,
|
search: NoticeQueryParam | None = None,
|
||||||
order_by: list[dict] | None = None,
|
order_by: list[dict] | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""分页查询公告(数据库 OFFSET/LIMIT)。"""
|
"""
|
||||||
|
分页查询公告(数据库 OFFSET/LIMIT)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息模型
|
||||||
|
- page_no (int): 页码(从 1 开始)
|
||||||
|
- page_size (int): 每页条数
|
||||||
|
- search (NoticeQueryParam | None): 查询条件
|
||||||
|
- order_by (list[dict] | None): 排序字段列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 分页结果(结构由 `CRUD.page` 返回约定)
|
||||||
|
"""
|
||||||
offset = (page_no - 1) * page_size
|
offset = (page_no - 1) * page_size
|
||||||
return await NoticeCRUD(auth).page(
|
return await NoticeCRUD(auth).page(
|
||||||
offset=offset,
|
offset=offset,
|
||||||
@@ -96,7 +108,15 @@ class NoticeService:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_notice_available_page_service(cls, auth: AuthSchema) -> dict:
|
async def get_notice_available_page_service(cls, auth: AuthSchema) -> dict:
|
||||||
"""已启用公告分页(与历史行为一致:默认第 1 页、每页 10 条)。"""
|
"""
|
||||||
|
已启用公告分页(与历史行为一致:固定第 1 页、每页 10 条)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息模型
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 分页结果(结构由 `CRUD.page` 返回约定)
|
||||||
|
"""
|
||||||
return await NoticeCRUD(auth).page(
|
return await NoticeCRUD(auth).page(
|
||||||
offset=0,
|
offset=0,
|
||||||
limit=10,
|
limit=10,
|
||||||
@@ -164,6 +184,9 @@ class NoticeService:
|
|||||||
|
|
||||||
异常:
|
异常:
|
||||||
- CustomException: 删除失败,删除对象不能为空或该公告通知不存在。
|
- CustomException: 删除失败,删除对象不能为空或该公告通知不存在。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
if len(ids) < 1:
|
if len(ids) < 1:
|
||||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||||
@@ -184,6 +207,9 @@ class NoticeService:
|
|||||||
|
|
||||||
异常:
|
异常:
|
||||||
- CustomException: 批量设置失败,该公告通知不存在。
|
- CustomException: 批量设置失败,该公告通知不存在。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
await NoticeCRUD(auth).set_available_crud(ids=data.ids, status=data.status)
|
await NoticeCRUD(auth).set_available_crud(ids=data.ids, status=data.status)
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,13 @@ class ParamsCRUD(CRUDBase[ParamsModel, ParamsCreateSchema, ParamsUpdateSchema]):
|
|||||||
|
|
||||||
def __init__(self, auth: AuthSchema) -> None:
|
def __init__(self, auth: AuthSchema) -> None:
|
||||||
"""
|
"""
|
||||||
初始化配置CRUD
|
初始化系统参数配置数据层。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- auth (AuthSchema): 认证信息模型
|
- auth (AuthSchema): 认证信息模型(含 DB 会话等上下文)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
super().__init__(model=ParamsModel, auth=auth)
|
super().__init__(model=ParamsModel, auth=auth)
|
||||||
|
|||||||
@@ -112,7 +112,19 @@ class ParamsService:
|
|||||||
search: ParamsQueryParam | None = None,
|
search: ParamsQueryParam | None = None,
|
||||||
order_by: list[dict[str, str]] | None = None,
|
order_by: list[dict[str, str]] | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""分页查询参数(数据库 OFFSET/LIMIT)。"""
|
"""
|
||||||
|
分页查询系统参数(数据库 OFFSET/LIMIT)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息模型
|
||||||
|
- page_no (int): 页码(从 1 开始)
|
||||||
|
- page_size (int): 每页条数
|
||||||
|
- search (ParamsQueryParam | None): 查询条件
|
||||||
|
- order_by (list[dict[str, str]] | None): 排序字段列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 分页结果(结构由 `CRUD.page` 返回约定)
|
||||||
|
"""
|
||||||
offset = (page_no - 1) * page_size
|
offset = (page_no - 1) * page_size
|
||||||
return await ParamsCRUD(auth).page(
|
return await ParamsCRUD(auth).page(
|
||||||
offset=offset,
|
offset=offset,
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ class PositionCRUD(CRUDBase[PositionModel, PositionCreateSchema, PositionUpdateS
|
|||||||
|
|
||||||
def __init__(self, auth: AuthSchema) -> None:
|
def __init__(self, auth: AuthSchema) -> None:
|
||||||
"""
|
"""
|
||||||
初始化岗位CRUD
|
初始化岗位数据层。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- auth (AuthSchema): 认证信息模型
|
- auth (AuthSchema): 认证信息模型(含 DB 会话等上下文)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
super().__init__(model=PositionModel, auth=auth)
|
super().__init__(model=PositionModel, auth=auth)
|
||||||
|
|||||||
@@ -64,7 +64,19 @@ class PositionService:
|
|||||||
search: PositionQueryParam | None = None,
|
search: PositionQueryParam | None = None,
|
||||||
order_by: list[dict[str, str]] | None = None,
|
order_by: list[dict[str, str]] | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""分页查询岗位(数据库 OFFSET/LIMIT)。"""
|
"""
|
||||||
|
分页查询岗位(数据库 OFFSET/LIMIT)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息模型
|
||||||
|
- page_no (int): 页码(从 1 开始)
|
||||||
|
- page_size (int): 每页条数
|
||||||
|
- search (PositionQueryParam | None): 查询条件
|
||||||
|
- order_by (list[dict[str, str]] | None): 排序字段列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 分页结果(结构由 `CRUD.page` 返回约定)
|
||||||
|
"""
|
||||||
offset = (page_no - 1) * page_size
|
offset = (page_no - 1) * page_size
|
||||||
return await PositionCRUD(auth).page(
|
return await PositionCRUD(auth).page(
|
||||||
offset=offset,
|
offset=offset,
|
||||||
@@ -84,7 +96,7 @@ class PositionService:
|
|||||||
- data (PositionCreateSchema): 岗位创建模型
|
- data (PositionCreateSchema): 岗位创建模型
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- Dict: 创建的岗位对象
|
- dict: 创建的岗位详情字典
|
||||||
"""
|
"""
|
||||||
position = await PositionCRUD(auth).get(name=data.name)
|
position = await PositionCRUD(auth).get(name=data.name)
|
||||||
if position:
|
if position:
|
||||||
|
|||||||
@@ -14,10 +14,13 @@ class RoleCRUD(CRUDBase[RoleModel, RoleCreateSchema, RoleUpdateSchema]):
|
|||||||
|
|
||||||
def __init__(self, auth: AuthSchema) -> None:
|
def __init__(self, auth: AuthSchema) -> None:
|
||||||
"""
|
"""
|
||||||
初始化角色模块数据层
|
初始化角色数据层。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- auth (AuthSchema): 认证信息模型
|
- auth (AuthSchema): 认证信息模型(含 DB 会话等上下文)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
super().__init__(model=RoleModel, auth=auth)
|
super().__init__(model=RoleModel, auth=auth)
|
||||||
@@ -59,8 +62,8 @@ class RoleCRUD(CRUDBase[RoleModel, RoleCreateSchema, RoleUpdateSchema]):
|
|||||||
设置角色的菜单权限
|
设置角色的菜单权限
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- role_ids (List[int]): 角色ID列表
|
- role_ids (list[int]): 角色ID列表
|
||||||
- menu_ids (List[int]): 菜单ID列表
|
- menu_ids (list[int]): 菜单ID列表
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- None
|
- None
|
||||||
|
|||||||
@@ -34,6 +34,18 @@ class RoleCreateSchema(BaseModel):
|
|||||||
@field_validator("code")
|
@field_validator("code")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_code(cls, value: str | None):
|
def validate_code(cls, value: str | None):
|
||||||
|
"""
|
||||||
|
校验角色编码(委托到 `code_validator`)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str | None): 角色编码。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str | None: 校验后的角色编码。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 不满足编码规则时抛出。
|
||||||
|
"""
|
||||||
return code_validator(value)
|
return code_validator(value)
|
||||||
|
|
||||||
|
|
||||||
@@ -50,7 +62,18 @@ class RolePermissionSettingSchema(BaseModel):
|
|||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def validate_fields(self):
|
def validate_fields(self):
|
||||||
"""验证权限配置字段"""
|
"""
|
||||||
|
校验角色权限配置字段(数据范围与关联 ID 等)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- self: 当前模型实例(校验后状态)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- RolePermissionSettingSchema: 通过 `role_permission_request_validator` 校验后的同一实例。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 不满足权限配置约束时抛出。
|
||||||
|
"""
|
||||||
return role_permission_request_validator(self)
|
return role_permission_request_validator(self)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,19 @@ class RoleService:
|
|||||||
search: RoleQueryParam | None = None,
|
search: RoleQueryParam | None = None,
|
||||||
order_by: list[dict[str, str]] | None = None,
|
order_by: list[dict[str, str]] | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""分页查询角色(数据库 OFFSET/LIMIT)。"""
|
"""
|
||||||
|
分页查询角色(数据库 OFFSET/LIMIT)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息模型
|
||||||
|
- page_no (int): 页码(从 1 开始)
|
||||||
|
- page_size (int): 每页条数
|
||||||
|
- search (RoleQueryParam | None): 查询条件
|
||||||
|
- order_by (list[dict[str, str]] | None): 排序字段列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 分页结果(结构由 `CRUD.page` 返回约定)
|
||||||
|
"""
|
||||||
offset = (page_no - 1) * page_size
|
offset = (page_no - 1) * page_size
|
||||||
return await RoleCRUD(auth).page(
|
return await RoleCRUD(auth).page(
|
||||||
offset=offset,
|
offset=offset,
|
||||||
|
|||||||
@@ -20,10 +20,13 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]):
|
|||||||
|
|
||||||
def __init__(self, auth: AuthSchema) -> None:
|
def __init__(self, auth: AuthSchema) -> None:
|
||||||
"""
|
"""
|
||||||
初始化用户CRUD
|
初始化用户数据层。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- auth (AuthSchema): 认证信息模型
|
- auth (AuthSchema): 认证信息模型(含 DB 会话等上下文)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
super().__init__(model=UserModel, auth=auth)
|
super().__init__(model=UserModel, auth=auth)
|
||||||
@@ -123,10 +126,10 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]):
|
|||||||
|
|
||||||
参数:
|
参数:
|
||||||
- ids (list[int]): 用户ID列表
|
- ids (list[int]): 用户ID列表
|
||||||
- status (bool): 可用状态
|
- status (str): 可用状态(与表字段一致,如 "0"/"1")
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- None:
|
- None
|
||||||
"""
|
"""
|
||||||
await self.set(ids=ids, status=status)
|
await self.set(ids=ids, status=status)
|
||||||
|
|
||||||
@@ -139,7 +142,7 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]):
|
|||||||
- role_ids (list[int]): 角色ID列表
|
- role_ids (list[int]): 角色ID列表
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- None:
|
- None
|
||||||
"""
|
"""
|
||||||
user_objs = await self.list(search={"id": ("in", user_ids)})
|
user_objs = await self.list(search={"id": ("in", user_ids)})
|
||||||
if role_ids:
|
if role_ids:
|
||||||
@@ -162,7 +165,7 @@ class UserCRUD(CRUDBase[UserModel, UserCreateSchema, UserUpdateSchema]):
|
|||||||
- position_ids (list[int]): 岗位ID列表
|
- position_ids (list[int]): 岗位ID列表
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- None:
|
- None
|
||||||
"""
|
"""
|
||||||
user_objs = await self.list(search={"id": ("in", user_ids)})
|
user_objs = await self.list(search={"id": ("in", user_ids)})
|
||||||
if position_ids:
|
if position_ids:
|
||||||
|
|||||||
@@ -29,11 +29,35 @@ class CurrentUserUpdateSchema(BaseModel):
|
|||||||
@field_validator("mobile")
|
@field_validator("mobile")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_mobile(cls, value: str | None):
|
def validate_mobile(cls, value: str | None):
|
||||||
|
"""
|
||||||
|
校验手机号格式(委托到 `mobile_validator`)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str | None): 手机号。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str | None: 校验后的手机号。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 手机号格式非法时抛出。
|
||||||
|
"""
|
||||||
return mobile_validator(value)
|
return mobile_validator(value)
|
||||||
|
|
||||||
@field_validator("email")
|
@field_validator("email")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_email(cls, value: str | None):
|
def validate_email(cls, value: str | None):
|
||||||
|
"""
|
||||||
|
校验邮箱格式(为空则跳过;否则委托到 `email_validator`)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str | None): 邮箱。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str | None: 校验后的邮箱。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 邮箱格式非法时抛出。
|
||||||
|
"""
|
||||||
if not value:
|
if not value:
|
||||||
return value
|
return value
|
||||||
return email_validator(value)
|
return email_validator(value)
|
||||||
@@ -41,6 +65,18 @@ class CurrentUserUpdateSchema(BaseModel):
|
|||||||
@field_validator("avatar")
|
@field_validator("avatar")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_avatar(cls, value: str | None):
|
def validate_avatar(cls, value: str | None):
|
||||||
|
"""
|
||||||
|
校验头像地址为合法的 HTTP/HTTPS URL。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str | None): 头像 URL。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str | None: 校验后的头像 URL。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 头像 URL 非法时抛出。
|
||||||
|
"""
|
||||||
if not value:
|
if not value:
|
||||||
return value
|
return value
|
||||||
parsed = urlparse(value)
|
parsed = urlparse(value)
|
||||||
@@ -50,6 +86,15 @@ class CurrentUserUpdateSchema(BaseModel):
|
|||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def check_model(self):
|
def check_model(self):
|
||||||
|
"""
|
||||||
|
校验基础用户信息的长度约束。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- CurrentUserUpdateSchema: 校验后的同一实例。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 字段长度超限时抛出。
|
||||||
|
"""
|
||||||
if self.name and len(self.name) > 32:
|
if self.name and len(self.name) > 32:
|
||||||
raise ValueError("名称长度不能超过32个字符")
|
raise ValueError("名称长度不能超过32个字符")
|
||||||
return self
|
return self
|
||||||
@@ -69,11 +114,35 @@ class UserRegisterSchema(BaseModel):
|
|||||||
@field_validator("mobile")
|
@field_validator("mobile")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_mobile(cls, value: str | None):
|
def validate_mobile(cls, value: str | None):
|
||||||
|
"""
|
||||||
|
校验手机号格式(委托到 `mobile_validator`)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str | None): 手机号。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str | None: 校验后的手机号。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 手机号格式非法时抛出。
|
||||||
|
"""
|
||||||
return mobile_validator(value)
|
return mobile_validator(value)
|
||||||
|
|
||||||
@field_validator("username")
|
@field_validator("username")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_username(cls, value: str):
|
def validate_username(cls, value: str):
|
||||||
|
"""
|
||||||
|
校验并规范化账号:字母开头,长度 3-32,仅含字母/数字/_ . -。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str): 账号。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 规范化后的账号。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 账号为空或不满足格式约束时抛出。
|
||||||
|
"""
|
||||||
v = value.strip()
|
v = value.strip()
|
||||||
if not v:
|
if not v:
|
||||||
raise ValueError("账号不能为空")
|
raise ValueError("账号不能为空")
|
||||||
@@ -86,6 +155,15 @@ class UserRegisterSchema(BaseModel):
|
|||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
def check_model(self):
|
def check_model(self):
|
||||||
|
"""
|
||||||
|
校验注册信息的长度约束。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- UserRegisterSchema: 校验后的同一实例。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 任一字段长度超限时抛出。
|
||||||
|
"""
|
||||||
if self.name and len(self.name) > 32:
|
if self.name and len(self.name) > 32:
|
||||||
raise ValueError("名称长度不能超过32个字符")
|
raise ValueError("名称长度不能超过32个字符")
|
||||||
if self.username and len(self.username) > 32:
|
if self.username and len(self.username) > 32:
|
||||||
@@ -107,6 +185,18 @@ class UserForgetPasswordSchema(BaseModel):
|
|||||||
@field_validator("mobile")
|
@field_validator("mobile")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_mobile(cls, value: str | None):
|
def validate_mobile(cls, value: str | None):
|
||||||
|
"""
|
||||||
|
校验手机号格式(委托到 `mobile_validator`)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str | None): 手机号。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str | None: 校验后的手机号。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 手机号格式非法时抛出。
|
||||||
|
"""
|
||||||
return mobile_validator(value)
|
return mobile_validator(value)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,19 @@ class UserService:
|
|||||||
search: UserQueryParam | None = None,
|
search: UserQueryParam | None = None,
|
||||||
order_by: list[dict[str, str]] | None = None,
|
order_by: list[dict[str, str]] | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""分页查询用户(数据库 OFFSET/LIMIT)。"""
|
"""
|
||||||
|
分页查询用户(数据库 OFFSET/LIMIT)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息模型
|
||||||
|
- page_no (int): 页码(从 1 开始)
|
||||||
|
- page_size (int): 每页条数
|
||||||
|
- search (UserQueryParam | None): 查询条件
|
||||||
|
- order_by (list[dict[str, str]] | None): 排序字段列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 分页结果(结构由 `CRUD.page` 返回约定)
|
||||||
|
"""
|
||||||
offset = (page_no - 1) * page_size
|
offset = (page_no - 1) * page_size
|
||||||
return await UserCRUD(auth).page(
|
return await UserCRUD(auth).page(
|
||||||
offset=offset,
|
offset=offset,
|
||||||
@@ -677,7 +689,7 @@ class UserService:
|
|||||||
导出用户列表为Excel文件
|
导出用户列表为Excel文件
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- user_list (List[Dict[str, Any]]): 用户列表
|
- user_list (list[dict[str, Any]]): 用户列表
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- bytes: Excel文件字节流
|
- bytes: Excel文件字节流
|
||||||
|
|||||||
@@ -193,12 +193,22 @@ class RET(Enum):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def code(self) -> int:
|
def code(self) -> int:
|
||||||
"""获取错误码"""
|
"""
|
||||||
|
获取错误码。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- int: 错误码数值。
|
||||||
|
"""
|
||||||
return self._code
|
return self._code
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def msg(self) -> str:
|
def msg(self) -> str:
|
||||||
"""获取错误信息"""
|
"""
|
||||||
|
获取错误信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 错误信息文本。
|
||||||
|
"""
|
||||||
return self._msg
|
return self._msg
|
||||||
|
|
||||||
|
|
||||||
@@ -766,6 +776,10 @@ class GenConstant:
|
|||||||
|
|
||||||
|
|
||||||
class TypedContextProtocol(Protocol):
|
class TypedContextProtocol(Protocol):
|
||||||
|
"""
|
||||||
|
请求上下文中与日志/鉴权相关的结构化字段协议(供类型检查使用)。
|
||||||
|
"""
|
||||||
|
|
||||||
perf_time: float
|
perf_time: float
|
||||||
|
|
||||||
ip: str
|
ip: str
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from fastapi import Response
|
|||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class IpInfo:
|
class IpInfo:
|
||||||
|
"""IP 归属地解析结果。"""
|
||||||
|
|
||||||
ip: str
|
ip: str
|
||||||
country: str | None
|
country: str | None
|
||||||
region: str | None
|
region: str | None
|
||||||
@@ -14,6 +16,8 @@ class IpInfo:
|
|||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class UserAgentInfo:
|
class UserAgentInfo:
|
||||||
|
"""User-Agent 解析结果(操作系统、浏览器、设备)。"""
|
||||||
|
|
||||||
user_agent: str
|
user_agent: str
|
||||||
os: str | None
|
os: str | None
|
||||||
browser: str | None
|
browser: str | None
|
||||||
@@ -22,6 +26,8 @@ class UserAgentInfo:
|
|||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class RequestCallNext:
|
class RequestCallNext:
|
||||||
|
"""请求链路 call_next 封装结果(状态码、消息、异常、响应)。"""
|
||||||
|
|
||||||
code: str
|
code: str
|
||||||
msg: str
|
msg: str
|
||||||
err: Exception | None
|
err: Exception | None
|
||||||
@@ -30,6 +36,8 @@ class RequestCallNext:
|
|||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class AccessToken:
|
class AccessToken:
|
||||||
|
"""访问令牌及过期时间、会话 UUID。"""
|
||||||
|
|
||||||
access_token: str
|
access_token: str
|
||||||
access_token_expire_time: datetime
|
access_token_expire_time: datetime
|
||||||
session_uuid: str
|
session_uuid: str
|
||||||
@@ -37,12 +45,16 @@ class AccessToken:
|
|||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class RefreshToken:
|
class RefreshToken:
|
||||||
|
"""刷新令牌及过期时间。"""
|
||||||
|
|
||||||
refresh_token: str
|
refresh_token: str
|
||||||
refresh_token_expire_time: datetime
|
refresh_token_expire_time: datetime
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class NewToken:
|
class NewToken:
|
||||||
|
"""刷新后的一对访问/刷新令牌及会话 UUID。"""
|
||||||
|
|
||||||
new_access_token: str
|
new_access_token: str
|
||||||
new_access_token_expire_time: datetime
|
new_access_token_expire_time: datetime
|
||||||
new_refresh_token: str
|
new_refresh_token: str
|
||||||
@@ -52,6 +64,8 @@ class NewToken:
|
|||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class TokenPayload:
|
class TokenPayload:
|
||||||
|
"""JWT/会话载荷中的用户与会话标识。"""
|
||||||
|
|
||||||
id: int
|
id: int
|
||||||
session_uuid: str
|
session_uuid: str
|
||||||
expire_time: datetime
|
expire_time: datetime
|
||||||
@@ -59,11 +73,15 @@ class TokenPayload:
|
|||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class UploadUrl:
|
class UploadUrl:
|
||||||
|
"""上传完成后的访问 URL。"""
|
||||||
|
|
||||||
url: str
|
url: str
|
||||||
|
|
||||||
|
|
||||||
@dataclasses.dataclass
|
@dataclasses.dataclass
|
||||||
class SnowflakeInfo:
|
class SnowflakeInfo:
|
||||||
|
"""雪花 ID 拆解后的各段信息。"""
|
||||||
|
|
||||||
timestamp: int
|
timestamp: int
|
||||||
datetime: str
|
datetime: str
|
||||||
cluster_id: int
|
cluster_id: int
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ from enum import Enum, unique
|
|||||||
|
|
||||||
@unique
|
@unique
|
||||||
class EnvironmentEnum(str, Enum):
|
class EnvironmentEnum(str, Enum):
|
||||||
|
"""应用运行环境(开发 / 生产)。"""
|
||||||
|
|
||||||
DEV = "dev"
|
DEV = "dev"
|
||||||
PROD = "prod"
|
PROD = "prod"
|
||||||
|
|
||||||
@@ -52,12 +54,22 @@ class RedisInitKeyConfig(Enum):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def key(self) -> str:
|
def key(self) -> str:
|
||||||
"""获取Redis键名"""
|
"""
|
||||||
|
获取 Redis 键名。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 键名字符串。
|
||||||
|
"""
|
||||||
return self.value.get("key", "")
|
return self.value.get("key", "")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def remark(self) -> str:
|
def remark(self) -> str:
|
||||||
"""获取Redis键名说明"""
|
"""
|
||||||
|
获取 Redis 键说明。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 说明文案。
|
||||||
|
"""
|
||||||
return self.value.get("remark", "")
|
return self.value.get("remark", "")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -197,7 +197,12 @@ class Settings(BaseSettings):
|
|||||||
# ================================================= #
|
# ================================================= #
|
||||||
@property
|
@property
|
||||||
def MIDDLEWARE_LIST(self) -> list[str | None]:
|
def MIDDLEWARE_LIST(self) -> list[str | None]:
|
||||||
"""获取项目根目录"""
|
"""
|
||||||
|
根据开关组装的中间件类路径列表(未启用的项为 None)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- list[str | None]: 中间件 import 路径或 None。
|
||||||
|
"""
|
||||||
# 中间件列表
|
# 中间件列表
|
||||||
MIDDLEWARES: list[str | None] = [
|
MIDDLEWARES: list[str | None] = [
|
||||||
"app.core.middlewares.CustomCORSMiddleware" if self.CORS_ORIGIN_ENABLE else None,
|
"app.core.middlewares.CustomCORSMiddleware" if self.CORS_ORIGIN_ENABLE else None,
|
||||||
@@ -208,7 +213,12 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def EVENT_LIST(self) -> list[str | None]:
|
def EVENT_LIST(self) -> list[str | None]:
|
||||||
"""获取事件列表"""
|
"""
|
||||||
|
应用启动时加载的全局异步事件模块路径列表。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- list[str | None]: 事件模块路径或 None。
|
||||||
|
"""
|
||||||
EVENTS: list[str | None] = [
|
EVENTS: list[str | None] = [
|
||||||
"app.core.database.redis_connect" if self.REDIS_ENABLE else None,
|
"app.core.database.redis_connect" if self.REDIS_ENABLE else None,
|
||||||
]
|
]
|
||||||
@@ -216,7 +226,15 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def ASYNC_DB_URI(self) -> str:
|
def ASYNC_DB_URI(self) -> str:
|
||||||
"""获取异步数据库连接"""
|
"""
|
||||||
|
异步 SQLAlchemy 数据库 URL。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 异步驱动连接串。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 数据库类型不支持时抛出。
|
||||||
|
"""
|
||||||
if self.DATABASE_TYPE not in ("mysql", "postgres", "sqlite"):
|
if self.DATABASE_TYPE not in ("mysql", "postgres", "sqlite"):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"数据库驱动不支持: {self.DATABASE_TYPE}, 异步数据库请选择 mysql、postgres、sqlite"
|
f"数据库驱动不支持: {self.DATABASE_TYPE}, 异步数据库请选择 mysql、postgres、sqlite"
|
||||||
@@ -232,7 +250,15 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def DB_URI(self) -> str:
|
def DB_URI(self) -> str:
|
||||||
"""获取同步数据库连接"""
|
"""
|
||||||
|
同步 SQLAlchemy 数据库 URL。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 同步驱动连接串。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 数据库类型不支持时抛出。
|
||||||
|
"""
|
||||||
if self.DATABASE_TYPE not in ("mysql", "postgres", "sqlite"):
|
if self.DATABASE_TYPE not in ("mysql", "postgres", "sqlite"):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"数据库驱动不支持: {self.DATABASE_TYPE}, 同步数据库请选择 mysql、postgres、sqlite"
|
f"数据库驱动不支持: {self.DATABASE_TYPE}, 同步数据库请选择 mysql、postgres、sqlite"
|
||||||
@@ -248,12 +274,22 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def REDIS_URI(self) -> str:
|
def REDIS_URI(self) -> str:
|
||||||
"""获取Redis连接"""
|
"""
|
||||||
|
Redis 连接 URL。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: redis:// 连接串。
|
||||||
|
"""
|
||||||
return f"redis://{self.REDIS_USER}:{self.REDIS_PASSWORD}@{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB_NAME}"
|
return f"redis://{self.REDIS_USER}:{self.REDIS_PASSWORD}@{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB_NAME}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def FASTAPI_CONFIG(self) -> dict[str, Any]:
|
def FASTAPI_CONFIG(self) -> dict[str, Any]:
|
||||||
"""获取FastAPI应用属性"""
|
"""
|
||||||
|
创建 FastAPI 应用实例时使用的关键字参数子集。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict[str, Any]: debug、title、responses 等配置。
|
||||||
|
"""
|
||||||
return {
|
return {
|
||||||
"debug": self.DEBUG,
|
"debug": self.DEBUG,
|
||||||
"title": self.TITLE,
|
"title": self.TITLE,
|
||||||
@@ -277,7 +313,12 @@ class Settings(BaseSettings):
|
|||||||
|
|
||||||
@lru_cache(maxsize=1)
|
@lru_cache(maxsize=1)
|
||||||
def get_settings() -> Settings:
|
def get_settings() -> Settings:
|
||||||
"""获取配置实例"""
|
"""
|
||||||
|
获取全局 Settings 单例(lru_cache 缓存)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Settings: 配置实例。
|
||||||
|
"""
|
||||||
return Settings()
|
return Settings()
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -86,8 +86,13 @@ class SchedulerUtil:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def scheduler_event_listener(cls, event: JobEvent | JobExecutionEvent) -> None:
|
def scheduler_event_listener(cls, event: JobEvent | JobExecutionEvent) -> None:
|
||||||
"""
|
"""
|
||||||
监听任务执行事件,记录执行日志
|
监听任务执行事件,记录执行日志;每次执行新建日志行,保留历史。
|
||||||
每次执行都创建新记录,保留所有历史执行记录
|
|
||||||
|
参数:
|
||||||
|
- event (JobEvent | JobExecutionEvent): APScheduler 事件对象。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
# 事件处理器映射
|
# 事件处理器映射
|
||||||
@@ -628,7 +633,13 @@ class SchedulerUtil:
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def init_scheduler(cls, redis: Redis | None = None) -> None:
|
async def init_scheduler(cls, redis: Redis | None = None) -> None:
|
||||||
"""
|
"""
|
||||||
应用启动时初始化定时任务。
|
应用启动时初始化定时任务调度器。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- redis (Redis | None): 可选 Redis 实例,供任务侧使用。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
if redis:
|
if redis:
|
||||||
cls.redis_instance = redis
|
cls.redis_instance = redis
|
||||||
@@ -646,6 +657,12 @@ class SchedulerUtil:
|
|||||||
import types
|
import types
|
||||||
|
|
||||||
def run_sync_handler():
|
def run_sync_handler():
|
||||||
|
"""
|
||||||
|
在独立模块命名空间中执行代码块并调用 handler。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Any: handler 返回值;无代码块时为 None。
|
||||||
|
"""
|
||||||
if not code_block:
|
if not code_block:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -683,6 +700,15 @@ class SchedulerUtil:
|
|||||||
state = job.__getstate__()
|
state = job.__getstate__()
|
||||||
|
|
||||||
def serialize_value(obj):
|
def serialize_value(obj):
|
||||||
|
"""
|
||||||
|
将 job state 中的嵌套对象转为可 JSON 化的 Python 结构。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- obj (Any): 任意嵌套对象。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Any: 标量、dict、list 或简化后的描述。
|
||||||
|
"""
|
||||||
if obj is None:
|
if obj is None:
|
||||||
return None
|
return None
|
||||||
if isinstance(obj, (str, int, float, bool)):
|
if isinstance(obj, (str, int, float, bool)):
|
||||||
@@ -728,6 +754,15 @@ class SchedulerUtil:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
def serialize_value(obj: Any) -> Any:
|
def serialize_value(obj: Any) -> Any:
|
||||||
|
"""
|
||||||
|
递归反序列化 BLOB 中的嵌套结构为可 JSON 化数据。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- obj (Any): 节点对象。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Any: 标量、dict、list 或字符串化结果。
|
||||||
|
"""
|
||||||
if obj is None:
|
if obj is None:
|
||||||
return None
|
return None
|
||||||
if isinstance(obj, (str, int, float, bool)):
|
if isinstance(obj, (str, int, float, bool)):
|
||||||
@@ -954,7 +989,13 @@ class SchedulerUtil:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_job_status(cls, job_id: str | int) -> str:
|
def get_job_status(cls, job_id: str | int) -> str:
|
||||||
"""
|
"""
|
||||||
获取单个任务的当前状态。
|
获取单个任务的当前状态文案。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- job_id (str | int): 调度器任务 ID。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 运行中 / 暂停中 / 已停止 / 未知 等。
|
||||||
"""
|
"""
|
||||||
job = cls.get_job(job_id=str(job_id))
|
job = cls.get_job(job_id=str(job_id))
|
||||||
if not job:
|
if not job:
|
||||||
@@ -972,7 +1013,13 @@ class SchedulerUtil:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def add_and_run_job_now(cls, job_info: NodeModel) -> Job:
|
def add_and_run_job_now(cls, job_info: NodeModel) -> Job:
|
||||||
"""
|
"""
|
||||||
立即执行任务(添加到调度器并立即运行)
|
立即执行任务(加入调度器并尽快触发一次)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- job_info (NodeModel): 节点/任务配置。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Job: APScheduler Job 对象。
|
||||||
"""
|
"""
|
||||||
# 使用稍微延迟的时间,确保事件监听器能够捕获事件
|
# 使用稍微延迟的时间,确保事件监听器能够捕获事件
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@@ -990,13 +1037,16 @@ class SchedulerUtil:
|
|||||||
end_date: str | None = None,
|
end_date: str | None = None,
|
||||||
) -> Job:
|
) -> Job:
|
||||||
"""
|
"""
|
||||||
创建Cron定时任务
|
创建 Cron 定时任务。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- job_info: 任务信息
|
- job_info (NodeModel): 任务信息。
|
||||||
- trigger_args: Cron表达式
|
- trigger_args (str | None): Cron 表达式,默认取节点配置。
|
||||||
- start_date: 开始时间
|
- start_date (str | None): 开始时间。
|
||||||
- end_date: 结束时间
|
- end_date (str | None): 结束时间。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Job: 已注册的 APScheduler Job。
|
||||||
"""
|
"""
|
||||||
cron_expr = trigger_args or job_info.trigger_args
|
cron_expr = trigger_args or job_info.trigger_args
|
||||||
if not cron_expr:
|
if not cron_expr:
|
||||||
@@ -1040,13 +1090,16 @@ class SchedulerUtil:
|
|||||||
end_date: str | None = None,
|
end_date: str | None = None,
|
||||||
) -> Job:
|
) -> Job:
|
||||||
"""
|
"""
|
||||||
创建间隔执行任务
|
创建间隔执行任务。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- job_info: 任务信息
|
- job_info (NodeModel): 任务信息。
|
||||||
- trigger_args: 间隔参数 (秒 分 时 天 周)
|
- trigger_args (str | None): 间隔参数「秒 分 时 天 周」,默认取节点配置。
|
||||||
- start_date: 开始时间
|
- start_date (str | None): 开始时间。
|
||||||
- end_date: 结束时间
|
- end_date (str | None): 结束时间。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Job: 已注册的 APScheduler Job。
|
||||||
"""
|
"""
|
||||||
interval_args = trigger_args or job_info.trigger_args
|
interval_args = trigger_args or job_info.trigger_args
|
||||||
if not interval_args:
|
if not interval_args:
|
||||||
@@ -1074,11 +1127,14 @@ class SchedulerUtil:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def add_date_job(cls, job_info: NodeModel, run_date: str | None = None) -> Job:
|
def add_date_job(cls, job_info: NodeModel, run_date: str | None = None) -> Job:
|
||||||
"""
|
"""
|
||||||
创建指定时间执行任务
|
创建指定时刻执行一次的任务。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- job_info: 任务信息
|
- job_info (NodeModel): 任务信息。
|
||||||
- run_date: 执行时间
|
- run_date (str | None): 执行时间字符串,默认取节点 trigger 配置。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Job: 已注册的 APScheduler Job。
|
||||||
"""
|
"""
|
||||||
date_str = run_date or job_info.trigger_args
|
date_str = run_date or job_info.trigger_args
|
||||||
if not date_str:
|
if not date_str:
|
||||||
@@ -1151,30 +1207,83 @@ class SchedulerUtil:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def start(cls, paused: bool = False) -> None:
|
def start(cls, paused: bool = False) -> None:
|
||||||
|
"""
|
||||||
|
启动全局调度器。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- paused (bool): 是否以暂停状态启动。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
scheduler.start(paused=paused)
|
scheduler.start(paused=paused)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def shutdown(cls, wait: bool = False):
|
async def shutdown(cls, wait: bool = False):
|
||||||
|
"""
|
||||||
|
关闭调度器。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- wait (bool): 是否等待当前任务结束。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- 与 APScheduler shutdown 返回值一致。
|
||||||
|
"""
|
||||||
return scheduler.shutdown(wait=wait)
|
return scheduler.shutdown(wait=wait)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def configure(cls, gconfig: dict | None = None, prefix: str = "apscheduler.", **options) -> None:
|
def configure(cls, gconfig: dict | None = None, prefix: str = "apscheduler.", **options) -> None:
|
||||||
|
"""
|
||||||
|
透传配置底层 APScheduler。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- gconfig (dict | None): 全局配置字典。
|
||||||
|
- prefix (str): 配置键前缀。
|
||||||
|
- **options: 其它 configure 关键字参数。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
scheduler.configure(gconfig or {}, prefix, **options)
|
scheduler.configure(gconfig or {}, prefix, **options)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def pause(cls) -> None:
|
def pause(cls) -> None:
|
||||||
|
"""
|
||||||
|
暂停调度器。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
scheduler.pause()
|
scheduler.pause()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def resume(cls) -> None:
|
def resume(cls) -> None:
|
||||||
|
"""
|
||||||
|
恢复调度器。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
scheduler.resume()
|
scheduler.resume()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_running(cls) -> bool:
|
def is_running(cls) -> bool:
|
||||||
|
"""
|
||||||
|
调度器是否处于运行态。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- bool: 是否 running。
|
||||||
|
"""
|
||||||
return scheduler.running
|
return scheduler.running
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_scheduler_state(cls) -> str:
|
def get_scheduler_state(cls) -> str:
|
||||||
|
"""
|
||||||
|
将调度器内部 state 码映射为中文状态。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 停止 / 运行中 / 暂停 / 未知。
|
||||||
|
"""
|
||||||
if scheduler.state == 0:
|
if scheduler.state == 0:
|
||||||
return "停止"
|
return "停止"
|
||||||
if scheduler.state == 1:
|
if scheduler.state == 1:
|
||||||
@@ -1185,22 +1294,63 @@ class SchedulerUtil:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_job(cls, job_id: str | int, jobstore: str | None = None) -> Job | None:
|
def get_job(cls, job_id: str | int, jobstore: str | None = None) -> Job | None:
|
||||||
|
"""
|
||||||
|
按 ID 获取单个任务。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- job_id (str | int): 任务 ID。
|
||||||
|
- jobstore (str | None): 存储器别名。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Job | None: 任务对象或不存在。
|
||||||
|
"""
|
||||||
return scheduler.get_job(str(job_id), jobstore)
|
return scheduler.get_job(str(job_id), jobstore)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_jobs(cls, jobstore: str | None = None) -> list[Job]:
|
def get_jobs(cls, jobstore: str | None = None) -> list[Job]:
|
||||||
|
"""
|
||||||
|
列出指定存储器中的任务。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- jobstore (str | None): 存储器别名,None 表示默认存储。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- list[Job]: 任务列表。
|
||||||
|
"""
|
||||||
return scheduler.get_jobs(jobstore)
|
return scheduler.get_jobs(jobstore)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all_jobs(cls) -> list[Job]:
|
def get_all_jobs(cls) -> list[Job]:
|
||||||
|
"""
|
||||||
|
列出所有存储器中的任务。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- list[Job]: 任务列表。
|
||||||
|
"""
|
||||||
return scheduler.get_jobs()
|
return scheduler.get_jobs()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def remove_job(cls, job_id: str | int, jobstore: str | None = None) -> None:
|
def remove_job(cls, job_id: str | int, jobstore: str | None = None) -> None:
|
||||||
|
"""
|
||||||
|
从调度器移除指定任务。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- job_id (str | int): 任务 ID。
|
||||||
|
- jobstore (str | None): 存储器别名。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
scheduler.remove_job(str(job_id), jobstore)
|
scheduler.remove_job(str(job_id), jobstore)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clear_jobs(cls) -> None:
|
def clear_jobs(cls) -> None:
|
||||||
|
"""
|
||||||
|
移除所有存储器中的全部任务。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
scheduler.remove_all_jobs()
|
scheduler.remove_all_jobs()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -1259,23 +1409,61 @@ class SchedulerUtil:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def pause_job(cls, job_id: str | int, jobstore: str | None = None) -> Job | None:
|
def pause_job(cls, job_id: str | int, jobstore: str | None = None) -> Job | None:
|
||||||
|
"""
|
||||||
|
暂停单个任务。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- job_id (str | int): 任务 ID。
|
||||||
|
- jobstore (str | None): 存储器别名。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Job | None: 暂停后的 Job 或 None。
|
||||||
|
"""
|
||||||
return scheduler.pause_job(str(job_id), jobstore)
|
return scheduler.pause_job(str(job_id), jobstore)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def resume_job(cls, job_id: str | int, jobstore: str | None = None) -> Job | None:
|
def resume_job(cls, job_id: str | int, jobstore: str | None = None) -> Job | None:
|
||||||
|
"""
|
||||||
|
恢复单个任务。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- job_id (str | int): 任务 ID。
|
||||||
|
- jobstore (str | None): 存储器别名。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Job | None: 恢复后的 Job 或 None。
|
||||||
|
"""
|
||||||
return scheduler.resume_job(str(job_id), jobstore)
|
return scheduler.resume_job(str(job_id), jobstore)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def modify_job(cls, job_id: str | int, jobstore: str | None = None, **changes) -> Job | None:
|
def modify_job(cls, job_id: str | int, jobstore: str | None = None, **changes) -> Job | None:
|
||||||
|
"""
|
||||||
|
修改已存在任务的属性。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- job_id (str | int): 任务 ID。
|
||||||
|
- jobstore (str | None): 存储器别名。
|
||||||
|
- **changes: 传给 modify_job 的变更字段。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Job | None: 修改后的 Job 或 None。
|
||||||
|
"""
|
||||||
return scheduler.modify_job(str(job_id), jobstore, **changes)
|
return scheduler.modify_job(str(job_id), jobstore, **changes)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def run_job_now(cls, job_id: str | int, jobstore: str | None = None) -> Job | None:
|
def run_job_now(cls, job_id: str | int, jobstore: str | None = None) -> Job | None:
|
||||||
"""
|
"""
|
||||||
立即执行任务
|
立即执行任务(通过临时 Job,不修改原任务 trigger)。
|
||||||
|
|
||||||
注意:为了不改变原任务的触发器配置,我们创建一个新的临时任务来执行,
|
参数:
|
||||||
而不是修改原任务的 trigger。
|
- job_id (str | int): 原任务 ID。
|
||||||
|
- jobstore (str | None): 存储器别名。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Job | None: 临时任务对象;原任务不存在时为 None。
|
||||||
|
|
||||||
|
注意:
|
||||||
|
- 不改变原任务的触发器配置,仅追加一次性执行。
|
||||||
"""
|
"""
|
||||||
job = cls.get_job(job_id=job_id, jobstore=jobstore)
|
job = cls.get_job(job_id=job_id, jobstore=jobstore)
|
||||||
if not job:
|
if not job:
|
||||||
|
|||||||
@@ -299,6 +299,9 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
|||||||
参数:
|
参数:
|
||||||
- ids (List[int]): 对象ID列表
|
- ids (List[int]): 对象ID列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
|
||||||
异常:
|
异常:
|
||||||
- CustomException: 删除失败时抛出异常
|
- CustomException: 删除失败时抛出异常
|
||||||
"""
|
"""
|
||||||
@@ -321,6 +324,9 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
|||||||
"""
|
"""
|
||||||
清空对象表
|
清空对象表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
|
||||||
异常:
|
异常:
|
||||||
- CustomException: 清空失败时抛出异常
|
- CustomException: 清空失败时抛出异常
|
||||||
"""
|
"""
|
||||||
@@ -339,6 +345,9 @@ class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
|
|||||||
- ids (List[int]): 对象ID列表
|
- ids (List[int]): 对象ID列表
|
||||||
- **kwargs: 更新的属性及值
|
- **kwargs: 更新的属性及值
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
|
||||||
异常:
|
异常:
|
||||||
- CustomException: 更新失败时抛出异常
|
- CustomException: 更新失败时抛出异常
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -140,7 +140,10 @@ class UserMixin(MappedBase):
|
|||||||
@declared_attr
|
@declared_attr
|
||||||
def created_by(self) -> Mapped[Optional["UserModel"]]:
|
def created_by(self) -> Mapped[Optional["UserModel"]]:
|
||||||
"""
|
"""
|
||||||
创建人关联关系(延迟加载,避免循环依赖)
|
创建人关联关系(延迟加载,避免循环依赖)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Mapped[Optional[UserModel]]: 创建人 ORM 关系。
|
||||||
"""
|
"""
|
||||||
return relationship(
|
return relationship(
|
||||||
"UserModel",
|
"UserModel",
|
||||||
@@ -152,7 +155,10 @@ class UserMixin(MappedBase):
|
|||||||
@declared_attr
|
@declared_attr
|
||||||
def updated_by(self) -> Mapped[Optional["UserModel"]]:
|
def updated_by(self) -> Mapped[Optional["UserModel"]]:
|
||||||
"""
|
"""
|
||||||
更新人关联关系(延迟加载,避免循环依赖)
|
更新人关联关系(延迟加载,避免循环依赖)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Mapped[Optional[UserModel]]: 更新人 ORM 关系。
|
||||||
"""
|
"""
|
||||||
return relationship(
|
return relationship(
|
||||||
"UserModel",
|
"UserModel",
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ def create_async_engine_and_session(
|
|||||||
"""
|
"""
|
||||||
获取异步数据库会话连接。
|
获取异步数据库会话连接。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- db_url (str): 异步数据库 URL,默认取配置项 ASYNC_DB_URI。
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- tuple[AsyncEngine, async_sessionmaker[AsyncSession]]: 异步数据库引擎和会话工厂。
|
- tuple[AsyncEngine, async_sessionmaker[AsyncSession]]: 异步数据库引擎和会话工厂。
|
||||||
"""
|
"""
|
||||||
@@ -108,13 +111,23 @@ async_engine, async_db_session = create_async_engine_and_session(settings.ASYNC_
|
|||||||
|
|
||||||
|
|
||||||
async def create_tables() -> None:
|
async def create_tables() -> None:
|
||||||
"""创建数据库表"""
|
"""
|
||||||
|
创建数据库表(根据 ORM metadata)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
async with async_engine.begin() as coon:
|
async with async_engine.begin() as coon:
|
||||||
await coon.run_sync(MappedBase.metadata.create_all)
|
await coon.run_sync(MappedBase.metadata.create_all)
|
||||||
|
|
||||||
|
|
||||||
async def drop_tables() -> None:
|
async def drop_tables() -> None:
|
||||||
"""删除数据库表"""
|
"""
|
||||||
|
删除数据库表(根据 ORM metadata)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
async with async_engine.begin() as conn:
|
async with async_engine.begin() as conn:
|
||||||
await conn.run_sync(MappedBase.metadata.drop_all)
|
await conn.run_sync(MappedBase.metadata.drop_all)
|
||||||
|
|
||||||
|
|||||||
@@ -55,15 +55,17 @@ def get_custom_ui_html(
|
|||||||
] = "https://fastapi.tiangolo.com/img/favicon.png",
|
] = "https://fastapi.tiangolo.com/img/favicon.png",
|
||||||
) -> HTMLResponse:
|
) -> HTMLResponse:
|
||||||
"""
|
"""
|
||||||
Generate and return the HTML that loads Swagger UI for the interactive
|
生成加载 Swagger UI 的 HTML 页面(与 FastAPI 默认 `/docs` 行为一致,可自定义静态资源 URL)。
|
||||||
API docs (normally served at `/docs`).
|
|
||||||
|
|
||||||
You would only call this function yourself if you needed to override some parts,
|
参数:
|
||||||
for example the URLs to use to load Swagger UI's JavaScript and CSS.
|
- openapi_url (str): OpenAPI JSON 地址。
|
||||||
|
- title (str): 页面标题。
|
||||||
|
- swagger_js_url (str): Swagger UI JS 地址。
|
||||||
|
- swagger_css_url (str): Swagger UI CSS 地址。
|
||||||
|
- swagger_favicon_url (str): 站点图标地址。
|
||||||
|
|
||||||
Read more about it in the
|
返回:
|
||||||
[FastAPI docs for Configure Swagger UI](https://fastapi.tiangolo.com/how-to/configure-swagger-ui/)
|
- HTMLResponse: 可直接返回给浏览器的 HTML 响应。
|
||||||
and the [FastAPI docs for Custom Docs UI Static Assets (Self-Hosting)](https://fastapi.tiangolo.com/how-to/custom-docs-ui-assets/).
|
|
||||||
"""
|
"""
|
||||||
html = f"""
|
html = f"""
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
|
|||||||
@@ -9,12 +9,15 @@ from app.core.exceptions import CustomException
|
|||||||
|
|
||||||
def http_limit_callback(request: Request, response: Response, expire: int) -> NoReturn:
|
def http_limit_callback(request: Request, response: Response, expire: int) -> NoReturn:
|
||||||
"""
|
"""
|
||||||
请求限制时的默认回调函数
|
HTTP 触发限流时的默认回调:抛出 429。
|
||||||
|
|
||||||
:param request: FastAPI 请求对象
|
参数:
|
||||||
:param response: FastAPI 响应对象
|
- request (Request): 当前请求。
|
||||||
:param expire: 剩余毫秒数
|
- response (Response): 当前响应(未直接使用,保留与限流器签名一致)。
|
||||||
:return:
|
- expire (int): 剩余冷却毫秒数。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- 无(始终抛出 CustomException)。
|
||||||
"""
|
"""
|
||||||
expires = ceil(expire / 30)
|
expires = ceil(expire / 30)
|
||||||
raise CustomException(
|
raise CustomException(
|
||||||
@@ -26,11 +29,14 @@ def http_limit_callback(request: Request, response: Response, expire: int) -> No
|
|||||||
|
|
||||||
async def ws_limit_callback(ws: WebSocket, expire: int) -> None:
|
async def ws_limit_callback(ws: WebSocket, expire: int) -> None:
|
||||||
"""
|
"""
|
||||||
WebSocket请求限制时的默认回调函数
|
WebSocket 触发限流时的默认回调:关闭连接。
|
||||||
|
|
||||||
:param ws: WebSocket连接对象
|
参数:
|
||||||
:param expire: 剩余毫秒数
|
- ws (WebSocket): 当前 WebSocket。
|
||||||
:return:
|
- expire (int): 剩余冷却毫秒数。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
expires = ceil(expire / 30)
|
expires = ceil(expire / 30)
|
||||||
await ws.close(code=1008, reason=f"请求过于频繁,请稍后重试!{expires} 秒后重试")
|
await ws.close(code=1008, reason=f"请求过于频繁,请稍后重试!{expires} 秒后重试")
|
||||||
|
|||||||
@@ -24,6 +24,15 @@ class InterceptHandler(logging.Handler):
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
def emit(self, record: logging.LogRecord) -> None:
|
def emit(self, record: logging.LogRecord) -> None:
|
||||||
|
"""
|
||||||
|
将标准库 LogRecord 转发到 Loguru。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- record (logging.LogRecord): 标准库日志记录。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
# 尝试获取日志级别名称
|
# 尝试获取日志级别名称
|
||||||
try:
|
try:
|
||||||
level = logger.level(record.levelname).name
|
level = logger.level(record.levelname).name
|
||||||
@@ -42,8 +51,10 @@ class InterceptHandler(logging.Handler):
|
|||||||
|
|
||||||
def cleanup_logging() -> None:
|
def cleanup_logging() -> None:
|
||||||
"""
|
"""
|
||||||
清理日志资源
|
清理日志资源;在程序退出时调用,移除已注册的 Loguru 处理器。
|
||||||
在程序退出时调用,确保所有日志处理器被正确关闭
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
global _logger_handlers
|
global _logger_handlers
|
||||||
|
|
||||||
@@ -59,12 +70,10 @@ def cleanup_logging() -> None:
|
|||||||
|
|
||||||
def setup_logging() -> None:
|
def setup_logging() -> None:
|
||||||
"""
|
"""
|
||||||
配置日志系统
|
配置日志系统:控制台彩色输出、文件轮转、错误日志分文件。
|
||||||
|
|
||||||
功能:
|
返回:
|
||||||
1. 控制台彩色输出
|
- None
|
||||||
2. 文件日志轮转
|
|
||||||
3. 错误日志单独存储
|
|
||||||
"""
|
"""
|
||||||
global _logger_handlers
|
global _logger_handlers
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,16 @@ class RequestLogMiddleware(BaseHTTPMiddleware):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response:
|
||||||
|
"""
|
||||||
|
记录请求日志并透传响应。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- request (Request): 当前请求。
|
||||||
|
- call_next (RequestResponseEndpoint): 下一层 ASGI 可调用对象。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Response: 下游中间件/路由产生的响应。
|
||||||
|
"""
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
# 尝试提取session_id
|
# 尝试提取session_id
|
||||||
|
|||||||
@@ -40,13 +40,13 @@ class Permission:
|
|||||||
|
|
||||||
async def filter_query(self, query: Any) -> Any:
|
async def filter_query(self, query: Any) -> Any:
|
||||||
"""
|
"""
|
||||||
异步过滤查询对象
|
按数据权限为 SQLAlchemy 查询追加 WHERE 条件。
|
||||||
|
|
||||||
Args:
|
参数:
|
||||||
query: SQLAlchemy查询对象
|
- query (Any): SQLAlchemy 查询对象。
|
||||||
|
|
||||||
Returns:
|
返回:
|
||||||
过滤后的查询对象
|
- Any: 附加条件后的查询对象(无权限条件时原样返回)。
|
||||||
"""
|
"""
|
||||||
condition = await self.__permission_condition()
|
condition = await self.__permission_condition()
|
||||||
return query.where(condition) if condition is not None else query
|
return query.where(condition) if condition is not None else query
|
||||||
|
|||||||
@@ -47,7 +47,15 @@ class ChatSessionCRUD:
|
|||||||
self.db.close()
|
self.db.close()
|
||||||
|
|
||||||
async def get_by_id_crud(self, session_id: str) -> TeamSession | None:
|
async def get_by_id_crud(self, session_id: str) -> TeamSession | None:
|
||||||
"""获取会话详情"""
|
"""
|
||||||
|
获取会话详情。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- session_id (str): 会话 ID。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- TeamSession | None: 会话对象;失败或不存在时为 None。
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
return self.db.get_session(
|
return self.db.get_session(
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
@@ -63,7 +71,16 @@ class ChatSessionCRUD:
|
|||||||
search: dict[str, Any] | None = None,
|
search: dict[str, Any] | None = None,
|
||||||
order_by: list[dict[str, str]] | None = None,
|
order_by: list[dict[str, str]] | None = None,
|
||||||
) -> list[TeamSession]:
|
) -> list[TeamSession]:
|
||||||
"""列表查询 - 获取所有会话"""
|
"""
|
||||||
|
列表查询,获取当前用户的所有会话。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- search (dict[str, Any] | None): 预留查询条件(当前实现未使用)。
|
||||||
|
- order_by (list[dict[str, str]] | None): 预留排序(当前实现未使用)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- list[TeamSession]: 会话列表;失败时为空列表。
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
result = self.db.get_sessions(
|
result = self.db.get_sessions(
|
||||||
session_type=self.SESSION_TYPE,
|
session_type=self.SESSION_TYPE,
|
||||||
@@ -77,7 +94,15 @@ class ChatSessionCRUD:
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
async def create_crud(self, data: ChatSessionCreateSchema) -> TeamSession | None:
|
async def create_crud(self, data: ChatSessionCreateSchema) -> TeamSession | None:
|
||||||
"""创建会话 - Team 会在运行时自动创建和管理 session"""
|
"""
|
||||||
|
创建会话(Team 在运行时自动创建并管理 session)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- data (ChatSessionCreateSchema): 创建参数(如标题)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- TeamSession | None: 新建会话;失败时为 None。
|
||||||
|
"""
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
@@ -108,7 +133,16 @@ class ChatSessionCRUD:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
async def update_crud(self, session_id: str, data: ChatSessionUpdateSchema) -> bool:
|
async def update_crud(self, session_id: str, data: ChatSessionUpdateSchema) -> bool:
|
||||||
"""更新会话"""
|
"""
|
||||||
|
更新会话(如重命名)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- session_id (str): 会话 ID。
|
||||||
|
- data (ChatSessionUpdateSchema): 更新数据。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- bool: 是否成功。
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
self.db.rename_session(
|
self.db.rename_session(
|
||||||
session_id=session_id,
|
session_id=session_id,
|
||||||
@@ -122,7 +156,15 @@ class ChatSessionCRUD:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
async def delete_crud(self, session_ids: list[str]) -> bool:
|
async def delete_crud(self, session_ids: list[str]) -> bool:
|
||||||
"""批量删除会话"""
|
"""
|
||||||
|
批量删除会话。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- session_ids (list[str]): 会话 ID 列表。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- bool: 是否全部处理成功(任一出错则记日志并返回 False)。
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
for session_id in session_ids:
|
for session_id in session_ids:
|
||||||
self.db.delete_session(
|
self.db.delete_session(
|
||||||
|
|||||||
@@ -130,7 +130,16 @@ class ChatService:
|
|||||||
async def chat_query(
|
async def chat_query(
|
||||||
cls, query: ChatQuerySchema, auth: AuthSchema
|
cls, query: ChatQuerySchema, auth: AuthSchema
|
||||||
) -> AsyncGenerator[str, None]:
|
) -> AsyncGenerator[str, None]:
|
||||||
"""处理聊天查询并返回流式响应"""
|
"""
|
||||||
|
处理聊天查询并流式返回文本片段。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- query (ChatQuerySchema): 用户消息与会话等查询参数。
|
||||||
|
- auth (AuthSchema): 当前用户认证信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- AsyncGenerator[str, None]: 逐段输出的回复文本。
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
# 创建 CRUD 实例获取数据库连接
|
# 创建 CRUD 实例获取数据库连接
|
||||||
crud = ChatSessionCRUD(auth)
|
crud = ChatSessionCRUD(auth)
|
||||||
@@ -171,7 +180,17 @@ class ChatService:
|
|||||||
async def chat_non_stream(
|
async def chat_non_stream(
|
||||||
cls, message: str, session_id: str | None, auth: AuthSchema
|
cls, message: str, session_id: str | None, auth: AuthSchema
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""处理聊天查询并返回非流式响应"""
|
"""
|
||||||
|
处理聊天查询并返回非流式 JSON 结构(含 session_id、操作建议等)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- message (str): 用户输入文本。
|
||||||
|
- session_id (str | None): 已有会话 ID;为空则新建会话。
|
||||||
|
- auth (AuthSchema): 当前用户认证信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict[str, Any]: 包含 response、session_id、action 等字段的字典。
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
# 创建 CRUD 实例获取数据库连接
|
# 创建 CRUD 实例获取数据库连接
|
||||||
crud = ChatSessionCRUD(auth)
|
crud = ChatSessionCRUD(auth)
|
||||||
@@ -298,7 +317,16 @@ class ChatService:
|
|||||||
async def create_service(
|
async def create_service(
|
||||||
cls, auth: AuthSchema, data: ChatSessionCreateSchema
|
cls, auth: AuthSchema, data: ChatSessionCreateSchema
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
"""创建会话"""
|
"""
|
||||||
|
创建会话。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- data (ChatSessionCreateSchema): 创建参数。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict[str, Any] | None: 格式化后的会话字典;失败为 None。
|
||||||
|
"""
|
||||||
crud = ChatSessionCRUD(auth)
|
crud = ChatSessionCRUD(auth)
|
||||||
session = await crud.create_crud(data=data)
|
session = await crud.create_crud(data=data)
|
||||||
if session:
|
if session:
|
||||||
@@ -309,7 +337,16 @@ class ChatService:
|
|||||||
async def get_session_service(
|
async def get_session_service(
|
||||||
cls, auth: AuthSchema, session_id: str
|
cls, auth: AuthSchema, session_id: str
|
||||||
) -> dict[str, Any] | None:
|
) -> dict[str, Any] | None:
|
||||||
"""获取单个会话详情"""
|
"""
|
||||||
|
获取单个会话详情。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- session_id (str): 会话 ID。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict[str, Any] | None: 格式化后的会话字典;不存在为 None。
|
||||||
|
"""
|
||||||
crud = ChatSessionCRUD(auth)
|
crud = ChatSessionCRUD(auth)
|
||||||
session: TeamSession | None = await crud.get_by_id_crud(session_id=session_id)
|
session: TeamSession | None = await crud.get_by_id_crud(session_id=session_id)
|
||||||
if session:
|
if session:
|
||||||
@@ -325,7 +362,19 @@ class ChatService:
|
|||||||
search: ChatSessionQueryParam,
|
search: ChatSessionQueryParam,
|
||||||
order_by: list[dict[str, str]] | None = None,
|
order_by: list[dict[str, str]] | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""分页获取会话列表。会话由 Agno 存储,无统一 SQL 分页接口,仅能对内存列表切片。"""
|
"""
|
||||||
|
分页获取会话列表。会话由 Agno 存储,无统一 SQL 分页,仅对内存列表切片。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- page_no (int): 页码。
|
||||||
|
- page_size (int): 每页条数。
|
||||||
|
- search (ChatSessionQueryParam): 查询条件。
|
||||||
|
- order_by (list[dict[str, str]] | None): 排序,可选。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict[str, Any]: 分页结果(含 items、total 等)。
|
||||||
|
"""
|
||||||
crud = ChatSessionCRUD(auth)
|
crud = ChatSessionCRUD(auth)
|
||||||
# 获取所有会话
|
# 获取所有会话
|
||||||
sessions = await crud.list_crud()
|
sessions = await crud.list_crud()
|
||||||
@@ -346,12 +395,31 @@ class ChatService:
|
|||||||
async def update_service(
|
async def update_service(
|
||||||
cls, auth: AuthSchema, session_id: str, data: ChatSessionUpdateSchema
|
cls, auth: AuthSchema, session_id: str, data: ChatSessionUpdateSchema
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""更新会话"""
|
"""
|
||||||
|
更新会话。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- session_id (str): 会话 ID。
|
||||||
|
- data (ChatSessionUpdateSchema): 更新数据。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- bool: 是否成功。
|
||||||
|
"""
|
||||||
crud = ChatSessionCRUD(auth)
|
crud = ChatSessionCRUD(auth)
|
||||||
success = await crud.update_crud(session_id=session_id, data=data)
|
success = await crud.update_crud(session_id=session_id, data=data)
|
||||||
return success
|
return success
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def delete_service(cls, auth: AuthSchema, session_ids: list[str]) -> None:
|
async def delete_service(cls, auth: AuthSchema, session_ids: list[str]) -> None:
|
||||||
"""删除会话"""
|
"""
|
||||||
|
删除会话。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- session_ids (list[str]): 待删除会话 ID 列表。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
await ChatSessionCRUD(auth).delete_crud(session_ids=session_ids)
|
await ChatSessionCRUD(auth).delete_crud(session_ids=session_ids)
|
||||||
|
|||||||
@@ -23,7 +23,18 @@ class AgnoFactory:
|
|||||||
session_id: str,
|
session_id: str,
|
||||||
db: Any | None = None
|
db: Any | None = None
|
||||||
) -> Team:
|
) -> Team:
|
||||||
"""创建 Team 实例"""
|
"""
|
||||||
|
创建带 Agent 的 Team 实例。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- user_id (str): 用户标识。
|
||||||
|
- dept_id (str): 部门/团队标识。
|
||||||
|
- session_id (str): 会话 ID。
|
||||||
|
- db (Any | None): Agno 持久化数据库实例,可选。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Team: 配置好的 Team。
|
||||||
|
"""
|
||||||
|
|
||||||
# 创建 Agent
|
# 创建 Agent
|
||||||
fastapiadmin_agent = Agent(
|
fastapiadmin_agent = Agent(
|
||||||
|
|||||||
@@ -22,11 +22,17 @@ async def websocket_chat_controller(
|
|||||||
websocket: WebSocket,
|
websocket: WebSocket,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
WebSocket聊天接口
|
WebSocket 聊天接口。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- websocket (WebSocket): WebSocket 连接。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None: 长连接处理完毕或关闭后无返回值。
|
||||||
|
|
||||||
支持两种消息格式:
|
支持两种消息格式:
|
||||||
1. 纯文本:直接发送消息内容
|
1. 纯文本:直接发送消息内容
|
||||||
2. JSON格式:{"message": "消息内容", "session_id": "会话ID", "files": [...]}
|
2. JSON 格式:{"message": "消息内容", "session_id": "会话ID", "files": [...]}
|
||||||
|
|
||||||
ws://127.0.0.1:8001/api/v1/ai/chat/ws?token=xxx
|
ws://127.0.0.1:8001/api/v1/ai/chat/ws?token=xxx
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -86,6 +86,9 @@ class ApplicationCRUD(CRUDBase[ApplicationModel, ApplicationCreateSchema, Applic
|
|||||||
|
|
||||||
参数:
|
参数:
|
||||||
- ids (list[int]): 应用ID列表
|
- ids (list[int]): 应用ID列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
return await self.delete(ids=ids)
|
return await self.delete(ids=ids)
|
||||||
|
|
||||||
@@ -96,5 +99,8 @@ class ApplicationCRUD(CRUDBase[ApplicationModel, ApplicationCreateSchema, Applic
|
|||||||
参数:
|
参数:
|
||||||
- ids (list[int]): 应用ID列表
|
- ids (list[int]): 应用ID列表
|
||||||
- status (str): 可用状态,True为可用,False为不可用
|
- status (str): 可用状态,True为可用,False为不可用
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
return await self.set(ids=ids, status=status)
|
return await self.set(ids=ids, status=status)
|
||||||
|
|||||||
@@ -65,7 +65,19 @@ class ApplicationService:
|
|||||||
search: ApplicationQueryParam | None = None,
|
search: ApplicationQueryParam | None = None,
|
||||||
order_by: list[dict[str, str]] | None = None,
|
order_by: list[dict[str, str]] | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""分页查询应用(数据库 OFFSET/LIMIT)。"""
|
"""
|
||||||
|
分页查询应用(数据库 OFFSET/LIMIT)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- page_no (int): 页码。
|
||||||
|
- page_size (int): 每页条数。
|
||||||
|
- search (ApplicationQueryParam | None): 查询条件。
|
||||||
|
- order_by (list[dict[str, str]] | None): 排序。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 分页结果。
|
||||||
|
"""
|
||||||
offset = (page_no - 1) * page_size
|
offset = (page_no - 1) * page_size
|
||||||
search_dict = search.__dict__ if search else {}
|
search_dict = search.__dict__ if search else {}
|
||||||
return await ApplicationCRUD(auth).page(
|
return await ApplicationCRUD(auth).page(
|
||||||
|
|||||||
@@ -33,7 +33,18 @@ class DemoCreateSchema(BaseModel):
|
|||||||
@field_validator("name")
|
@field_validator("name")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_name(cls, v: str) -> str:
|
def validate_name(cls, v: str) -> str:
|
||||||
"""验证名称字段的格式和内容"""
|
"""
|
||||||
|
验证名称字段的格式和内容。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- v (str): 原始名称。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 去空白后的名称。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 名称为空时抛出。
|
||||||
|
"""
|
||||||
# 去除首尾空格
|
# 去除首尾空格
|
||||||
v = v.strip()
|
v = v.strip()
|
||||||
if not v:
|
if not v:
|
||||||
|
|||||||
@@ -25,7 +25,18 @@ class Demo01CreateSchema(BaseModel):
|
|||||||
@field_validator("name")
|
@field_validator("name")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_name(cls, v: str) -> str:
|
def validate_name(cls, v: str) -> str:
|
||||||
"""验证名称字段的格式和内容"""
|
"""
|
||||||
|
验证名称字段的格式和内容。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- v (str): 原始名称。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 去空白后的名称。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 名称为空时抛出。
|
||||||
|
"""
|
||||||
# 去除首尾空格
|
# 去除首尾空格
|
||||||
v = v.strip()
|
v = v.strip()
|
||||||
if not v:
|
if not v:
|
||||||
|
|||||||
@@ -348,5 +348,15 @@ async def sync_db_preview_controller(
|
|||||||
table_name: Annotated[str, Path(description="表名")],
|
table_name: Annotated[str, Path(description="表名")],
|
||||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_generator:db:sync"]))],
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_generator:db:sync"]))],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
|
"""
|
||||||
|
同步数据库前差异预览(主表 + 可选子表),不落库。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- table_name (str): 物理表名。
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功响应,data 为预览结构。
|
||||||
|
"""
|
||||||
result = await GenTableService.sync_db_preview_service(auth, table_name)
|
result = await GenTableService.sync_db_preview_service(auth, table_name)
|
||||||
return SuccessResponse(msg="获取同步差异预览成功", data=result)
|
return SuccessResponse(msg="获取同步差异预览成功", data=result)
|
||||||
|
|||||||
@@ -131,6 +131,9 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
|
|||||||
|
|
||||||
参数:
|
参数:
|
||||||
- ids (list[int]): 业务表ID列表。
|
- ids (list[int]): 业务表ID列表。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
await self.delete(ids=ids)
|
await self.delete(ids=ids)
|
||||||
|
|
||||||
@@ -198,6 +201,14 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
|
|||||||
- 旧实现使用 SQLAlchemy Inspector 全量遍历再内存分页,表多时非常慢。
|
- 旧实现使用 SQLAlchemy Inspector 全量遍历再内存分页,表多时非常慢。
|
||||||
- 这里按方言走系统表(MySQL information_schema / Postgres pg_catalog)进行分页与过滤。
|
- 这里按方言走系统表(MySQL information_schema / Postgres pg_catalog)进行分页与过滤。
|
||||||
- 若方言不支持,则回退到旧的全量遍历。
|
- 若方言不支持,则回退到旧的全量遍历。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- search (GenTableQueryParam | None): 表名/注释过滤条件。
|
||||||
|
- offset (int): 偏移量。
|
||||||
|
- limit (int): 每页条数。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- tuple[list[dict], int]: 当前页表信息列表与总条数。
|
||||||
"""
|
"""
|
||||||
database_name = settings.DATABASE_NAME
|
database_name = settings.DATABASE_NAME
|
||||||
db_type = (settings.DATABASE_TYPE or "").lower()
|
db_type = (settings.DATABASE_TYPE or "").lower()
|
||||||
@@ -342,6 +353,12 @@ class GenTableCRUD(CRUDBase[GenTableModel, GenTableSchema, GenTableSchema]):
|
|||||||
async def get_db_table_comment(self, table_name: str) -> str:
|
async def get_db_table_comment(self, table_name: str) -> str:
|
||||||
"""
|
"""
|
||||||
获取数据库中指定表的注释(用于主子表场景下从库中加载子表元信息)。
|
获取数据库中指定表的注释(用于主子表场景下从库中加载子表元信息)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- table_name (str): 物理表名。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 表注释;表不存在或失败时为空字符串。
|
||||||
"""
|
"""
|
||||||
from app.core.database import engine
|
from app.core.database import engine
|
||||||
|
|
||||||
|
|||||||
@@ -59,14 +59,38 @@ class GenTableModel(ModelMixin, UserMixin):
|
|||||||
|
|
||||||
@validates("table_name")
|
@validates("table_name")
|
||||||
def validate_table_name(self, key: str, table_name: str) -> str:
|
def validate_table_name(self, key: str, table_name: str) -> str:
|
||||||
"""验证表名不为空"""
|
"""
|
||||||
|
验证表名非空并去首尾空格。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- key (str): 字段名。
|
||||||
|
- table_name (str): 表名。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 规范化后的表名。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 表名为空时抛出。
|
||||||
|
"""
|
||||||
if not table_name or not table_name.strip():
|
if not table_name or not table_name.strip():
|
||||||
raise ValueError("表名称不能为空")
|
raise ValueError("表名称不能为空")
|
||||||
return table_name.strip()
|
return table_name.strip()
|
||||||
|
|
||||||
@validates("class_name")
|
@validates("class_name")
|
||||||
def validate_class_name(self, key: str, class_name: str) -> str:
|
def validate_class_name(self, key: str, class_name: str) -> str:
|
||||||
"""验证类名不为空"""
|
"""
|
||||||
|
验证实体类名非空并去首尾空格。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- key (str): 字段名。
|
||||||
|
- class_name (str): 类名。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 规范化后的类名。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 类名为空时抛出。
|
||||||
|
"""
|
||||||
if not class_name or not class_name.strip():
|
if not class_name or not class_name.strip():
|
||||||
raise ValueError("实体类名称不能为空")
|
raise ValueError("实体类名称不能为空")
|
||||||
return class_name.strip()
|
return class_name.strip()
|
||||||
@@ -190,14 +214,38 @@ class GenTableColumnModel(ModelMixin, UserMixin):
|
|||||||
|
|
||||||
@validates("column_name")
|
@validates("column_name")
|
||||||
def validate_column_name(self, key: str, column_name: str) -> str:
|
def validate_column_name(self, key: str, column_name: str) -> str:
|
||||||
"""验证列名不为空"""
|
"""
|
||||||
|
验证列名非空并去首尾空格。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- key (str): 字段名。
|
||||||
|
- column_name (str): 列名。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 规范化后的列名。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 列名为空时抛出。
|
||||||
|
"""
|
||||||
if not column_name or not column_name.strip():
|
if not column_name or not column_name.strip():
|
||||||
raise ValueError("列名称不能为空")
|
raise ValueError("列名称不能为空")
|
||||||
return column_name.strip()
|
return column_name.strip()
|
||||||
|
|
||||||
@validates("column_type")
|
@validates("column_type")
|
||||||
def validate_column_type(self, key: str, column_type: str) -> str:
|
def validate_column_type(self, key: str, column_type: str) -> str:
|
||||||
"""验证列类型不为空"""
|
"""
|
||||||
|
验证列类型非空并去首尾空格。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- key (str): 字段名。
|
||||||
|
- column_type (str): 列类型字符串。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 规范化后的列类型。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 列类型为空时抛出。
|
||||||
|
"""
|
||||||
if not column_type or not column_type.strip():
|
if not column_type or not column_type.strip():
|
||||||
raise ValueError("列类型不能为空")
|
raise ValueError("列类型不能为空")
|
||||||
return column_type.strip()
|
return column_type.strip()
|
||||||
|
|||||||
@@ -142,7 +142,18 @@ class GenTableSchema(BaseModel):
|
|||||||
@field_validator("table_name")
|
@field_validator("table_name")
|
||||||
@classmethod
|
@classmethod
|
||||||
def table_name_update(cls, v: str) -> str:
|
def table_name_update(cls, v: str) -> str:
|
||||||
"""更新表名称"""
|
"""
|
||||||
|
校验并规范化表名称。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- v (str): 原始表名。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 去空白后的表名。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 表名为空时抛出。
|
||||||
|
"""
|
||||||
if not v:
|
if not v:
|
||||||
raise ValueError("表名称不能为空")
|
raise ValueError("表名称不能为空")
|
||||||
return v.strip()
|
return v.strip()
|
||||||
@@ -156,7 +167,15 @@ class GenTableSchema(BaseModel):
|
|||||||
)
|
)
|
||||||
@classmethod
|
@classmethod
|
||||||
def strip_optional_text_fields(cls, v: str | None) -> str | None:
|
def strip_optional_text_fields(cls, v: str | None) -> str | None:
|
||||||
"""文本类字段统一去首尾空格;空串视为 None。"""
|
"""
|
||||||
|
文本类字段统一去首尾空格;空串视为 None。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- v (str | None): 原始值。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str | None: 非空字符串或 None。
|
||||||
|
"""
|
||||||
if v is None:
|
if v is None:
|
||||||
return None
|
return None
|
||||||
s = str(v).strip()
|
s = str(v).strip()
|
||||||
@@ -165,7 +184,15 @@ class GenTableSchema(BaseModel):
|
|||||||
@field_validator("package_name", mode="before")
|
@field_validator("package_name", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def normalize_package_name(cls, v: str | None) -> str | None:
|
def normalize_package_name(cls, v: str | None) -> str | None:
|
||||||
"""包名规范:必须是 module_xxx 形态(工程约定)。"""
|
"""
|
||||||
|
包名规范:必须是 module_xxx 形态(工程约定)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- v (str | None): 原始包名。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str | None: 规范化后的包名或 None。
|
||||||
|
"""
|
||||||
if v is None:
|
if v is None:
|
||||||
return None
|
return None
|
||||||
s = cls._normalize_slug_segment(str(v), allow_slash=False)
|
s = cls._normalize_slug_segment(str(v), allow_slash=False)
|
||||||
@@ -176,7 +203,15 @@ class GenTableSchema(BaseModel):
|
|||||||
@field_validator("module_name", mode="before")
|
@field_validator("module_name", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def normalize_module_name(cls, v: str | None) -> str | None:
|
def normalize_module_name(cls, v: str | None) -> str | None:
|
||||||
"""模块名规范:示例模式下要求不带 module_ 前缀;统一按 slug 规范。"""
|
"""
|
||||||
|
模块名规范:不带 module_ 前缀;统一按 slug 规范。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- v (str | None): 原始模块名。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str | None: 规范化后的模块名或 None。
|
||||||
|
"""
|
||||||
if v is None:
|
if v is None:
|
||||||
return None
|
return None
|
||||||
s = cls._normalize_slug_segment(str(v), allow_slash=False)
|
s = cls._normalize_slug_segment(str(v), allow_slash=False)
|
||||||
@@ -187,7 +222,15 @@ class GenTableSchema(BaseModel):
|
|||||||
@field_validator("business_name", mode="before")
|
@field_validator("business_name", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def normalize_business_name(cls, v: str | None) -> str | None:
|
def normalize_business_name(cls, v: str | None) -> str | None:
|
||||||
"""业务名允许多段:demo/demo01;统一按 slug 规范。"""
|
"""
|
||||||
|
业务名允许多段(如 demo/demo01);统一按 slug 规范。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- v (str | None): 原始业务名。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str | None: 规范化后的业务名或 None。
|
||||||
|
"""
|
||||||
if v is None:
|
if v is None:
|
||||||
return None
|
return None
|
||||||
s = cls._normalize_slug_segment(str(v), allow_slash=True)
|
s = cls._normalize_slug_segment(str(v), allow_slash=True)
|
||||||
@@ -196,7 +239,15 @@ class GenTableSchema(BaseModel):
|
|||||||
@field_validator("sub_table_name", "sub_table_fk_name", mode="before")
|
@field_validator("sub_table_name", "sub_table_fk_name", mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
def strip_optional_sub_fields(cls, v: str | None) -> str | None:
|
def strip_optional_sub_fields(cls, v: str | None) -> str | None:
|
||||||
"""主子表字段去首尾空格,空串视为未填。"""
|
"""
|
||||||
|
主子表字段去首尾空格,空串视为未填。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- v (str | None): 原始值。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str | None: 非空字符串或 None。
|
||||||
|
"""
|
||||||
if v is None:
|
if v is None:
|
||||||
return None
|
return None
|
||||||
s = str(v).strip()
|
s = str(v).strip()
|
||||||
|
|||||||
@@ -41,6 +41,16 @@ from .tools.jinja2_template_util import Jinja2TemplateUtil
|
|||||||
|
|
||||||
|
|
||||||
def handle_service_exception(func: Callable) -> Callable:
|
def handle_service_exception(func: Callable) -> Callable:
|
||||||
|
"""
|
||||||
|
服务层异步方法装饰器:透传 CustomException,其余异常包装为 CustomException。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- func (Callable): 被装饰的异步可调用对象。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Callable: 包装后的可调用对象(异步)。
|
||||||
|
"""
|
||||||
|
|
||||||
async def wrapper(*args, **kwargs):
|
async def wrapper(*args, **kwargs):
|
||||||
try:
|
try:
|
||||||
return await func(*args, **kwargs)
|
return await func(*args, **kwargs)
|
||||||
@@ -93,7 +103,6 @@ class GenTableService:
|
|||||||
raise CustomException(msg="包名不能为空")
|
raise CustomException(msg="包名不能为空")
|
||||||
return pn if pn.startswith("module_") else f"module_{pn}"
|
return pn if pn.startswith("module_") else f"module_{pn}"
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def _assert_parent_menu_is_catalog(cls, auth: AuthSchema, parent_menu_id: int | None) -> None:
|
async def _assert_parent_menu_is_catalog(cls, auth: AuthSchema, parent_menu_id: int | None) -> None:
|
||||||
"""上级菜单仅允许目录:与前端树只展示目录一致,避免挂到菜单/按钮下。"""
|
"""上级菜单仅允许目录:与前端树只展示目录一致,避免挂到菜单/按钮下。"""
|
||||||
@@ -208,7 +217,18 @@ class GenTableService:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def normalize_and_validate_master_sub(cls, data: GenTableSchema) -> None:
|
def normalize_and_validate_master_sub(cls, data: GenTableSchema) -> None:
|
||||||
"""主子表业务规则:两字段同填或同空;子表表名不得与主表相同。"""
|
"""
|
||||||
|
主子表业务规则:子表表名与外键列同填或同空;子表表名不得与主表相同。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- data (GenTableSchema): 主表配置。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 规则不满足时抛出。
|
||||||
|
"""
|
||||||
sn = data.sub_table_name
|
sn = data.sub_table_name
|
||||||
fk = data.sub_table_fk_name
|
fk = data.sub_table_fk_name
|
||||||
if bool(sn) ^ bool(fk):
|
if bool(sn) ^ bool(fk):
|
||||||
@@ -260,7 +280,19 @@ class GenTableService:
|
|||||||
search: GenTableQueryParam,
|
search: GenTableQueryParam,
|
||||||
order_by: list[dict[str, str]] | None = None,
|
order_by: list[dict[str, str]] | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""分页查询代码生成业务表(数据库 OFFSET/LIMIT)。"""
|
"""
|
||||||
|
分页查询代码生成业务表(数据库 OFFSET/LIMIT)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- page_no (int): 页码。
|
||||||
|
- page_size (int): 每页条数。
|
||||||
|
- search (GenTableQueryParam): 查询条件。
|
||||||
|
- order_by (list[dict[str, str]] | None): 排序。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 分页结果。
|
||||||
|
"""
|
||||||
offset = (page_no - 1) * page_size
|
offset = (page_no - 1) * page_size
|
||||||
order = order_by or [{"created_time": "desc"}]
|
order = order_by or [{"created_time": "desc"}]
|
||||||
return await GenTableCRUD(auth=auth).page(
|
return await GenTableCRUD(auth=auth).page(
|
||||||
@@ -297,7 +329,18 @@ class GenTableService:
|
|||||||
page_size: int,
|
page_size: int,
|
||||||
search: GenTableQueryParam,
|
search: GenTableQueryParam,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""数据库表列表分页(数据库侧 OFFSET/LIMIT)。"""
|
"""
|
||||||
|
数据库表列表分页(数据库侧 OFFSET/LIMIT)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- page_no (int): 页码。
|
||||||
|
- page_size (int): 每页条数。
|
||||||
|
- search (GenTableQueryParam): 查询条件。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict[str, Any]: 含 items、total、has_next 等字段。
|
||||||
|
"""
|
||||||
offset = (page_no - 1) * page_size
|
offset = (page_no - 1) * page_size
|
||||||
items, total = await GenTableCRUD(auth=auth).get_db_table_page(
|
items, total = await GenTableCRUD(auth=auth).get_db_table_page(
|
||||||
search=search, offset=offset, limit=page_size
|
search=search, offset=offset, limit=page_size
|
||||||
@@ -1041,9 +1084,17 @@ class GenTableService:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def hydrate_sub_table(cls, auth: AuthSchema, gen_table: GenTableOutSchema) -> None:
|
async def hydrate_sub_table(cls, auth: AuthSchema, gen_table: GenTableOutSchema) -> None:
|
||||||
"""主子表:优先使用“已导入的子表配置”,否则回退 DB 结构只读。
|
"""
|
||||||
|
主子表:优先使用已导入的子表配置,否则回退为只读 DB 结构。
|
||||||
|
|
||||||
对齐 RuoYi 的更佳体验:子表应当是一个可配置的 gen_table(可单独编辑字段),主表只引用它。
|
对齐 RuoYi:子表宜为独立 gen_table;主表仅引用。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- gen_table (GenTableOutSchema): 主表输出模型(原地填充 sub_table、master_sub_hint 等)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
gen_table.master_sub_hint = None
|
gen_table.master_sub_hint = None
|
||||||
sub_name_raw = (gen_table.sub_table_name or "").strip()
|
sub_name_raw = (gen_table.sub_table_name or "").strip()
|
||||||
@@ -1210,7 +1261,19 @@ class GenTableService:
|
|||||||
async def sync_db_preview_service(
|
async def sync_db_preview_service(
|
||||||
cls, auth: AuthSchema, table_name: str
|
cls, auth: AuthSchema, table_name: str
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""同步数据库前差异预览(主表 + 可选子表)。"""
|
"""
|
||||||
|
同步数据库前差异预览(主表 + 可选子表)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- table_name (str): 主表物理表名。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict[str, Any]: 预览差异结构(可序列化)。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 表名无效或业务表不存在等。
|
||||||
|
"""
|
||||||
if not table_name or not table_name.strip():
|
if not table_name or not table_name.strip():
|
||||||
raise CustomException(msg="表名不能为空")
|
raise CustomException(msg="表名不能为空")
|
||||||
gen_table = await GenTableCRUD(auth).get_gen_table_by_name(table_name, preload=["columns"])
|
gen_table = await GenTableCRUD(auth).get_gen_table_by_name(table_name, preload=["columns"])
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ class Jinja2TemplateUtil:
|
|||||||
def normalize_db_column_type_for_mapping(cls, column_type: str | None) -> str:
|
def normalize_db_column_type_for_mapping(cls, column_type: str | None) -> str:
|
||||||
"""
|
"""
|
||||||
与 ``GenUtils.get_db_type`` 一致地去掉 COLLATE / UNSIGNED,便于与 ``DB_TO_SQLALCHEMY`` 键匹配。
|
与 ``GenUtils.get_db_type`` 一致地去掉 COLLATE / UNSIGNED,便于与 ``DB_TO_SQLALCHEMY`` 键匹配。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- column_type (str | None): 原始列类型字符串。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 规范化后的类型片段;空输入返回空字符串。
|
||||||
"""
|
"""
|
||||||
ct = (column_type or "").strip()
|
ct = (column_type or "").strip()
|
||||||
if not ct:
|
if not ct:
|
||||||
@@ -91,12 +97,17 @@ class Jinja2TemplateUtil:
|
|||||||
"""
|
"""
|
||||||
return cls.get_env().get_template(template_path)
|
return cls.get_env().get_template(template_path)
|
||||||
|
|
||||||
@classmethod
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def business_name_to_slug(cls, business_name: str | None) -> str:
|
def business_name_to_slug(cls, business_name: str | None) -> str:
|
||||||
"""
|
"""
|
||||||
业务路径可含斜杠(如 ``demo/demo01``)用于目录与路由前缀;
|
业务路径可含斜杠(如 ``demo/demo01``)用于目录与路由前缀;
|
||||||
Python 函数/方法名仅使用最后一段并规范为合法 snake_case 片段。
|
Python 函数/方法名仅使用最后一段并规范为合法 snake_case 片段。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- business_name (str | None): 业务路径或名称。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 用于 Python 标识的 slug,默认 entity。
|
||||||
"""
|
"""
|
||||||
s = (business_name or "").strip().strip("/")
|
s = (business_name or "").strip().strip("/")
|
||||||
if not s:
|
if not s:
|
||||||
@@ -117,6 +128,12 @@ class Jinja2TemplateUtil:
|
|||||||
约定:`business_name` 允许 `a/b/c` 表示多级菜单目录。
|
约定:`business_name` 允许 `a/b/c` 表示多级菜单目录。
|
||||||
- 目录/路由:使用完整多段
|
- 目录/路由:使用完整多段
|
||||||
- 文件名/route_name:使用最后一段 slug(见 `business_name_to_slug`)
|
- 文件名/route_name:使用最后一段 slug(见 `business_name_to_slug`)
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- business_name (str | None): 业务路径或名称。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 多段路径字符串(小写 slug),默认 entity。
|
||||||
"""
|
"""
|
||||||
s = (business_name or "").strip().strip("/")
|
s = (business_name or "").strip().strip("/")
|
||||||
if not s:
|
if not s:
|
||||||
@@ -230,6 +247,12 @@ class Jinja2TemplateUtil:
|
|||||||
前端页面路由首段(与写入菜单 ``route_path`` 第一段一致):始终为 ``module_xxx``。
|
前端页面路由首段(与写入菜单 ``route_path`` 第一段一致):始终为 ``module_xxx``。
|
||||||
|
|
||||||
懒加载 ``GenTableService`` 避免与 ``service`` 模块循环依赖。
|
懒加载 ``GenTableService`` 避免与 ``service`` 模块循环依赖。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- gen_table (GenTableOutSchema): 生成表配置。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 路由首段(module_xxx)。
|
||||||
"""
|
"""
|
||||||
from app.plugin.module_generator.gencode.service import GenTableService
|
from app.plugin.module_generator.gencode.service import GenTableService
|
||||||
|
|
||||||
@@ -246,6 +269,13 @@ class Jinja2TemplateUtil:
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
子表业务代码渲染上下文(与主表同模块、独立业务目录)。
|
子表业务代码渲染上下文(与主表同模块、独立业务目录)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- parent (GenTableOutSchema): 主表配置。
|
||||||
|
- sub (GenTableOutSchema): 子表配置。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict[str, Any]: 子表模板上下文字典。
|
||||||
"""
|
"""
|
||||||
ctx = cls.prepare_context(sub)
|
ctx = cls.prepare_context(sub)
|
||||||
scn = (sub.class_name or GenUtils.convert_class_name(sub.table_name or "")).strip()
|
scn = (sub.class_name or GenUtils.convert_class_name(sub.table_name or "")).strip()
|
||||||
@@ -351,10 +381,13 @@ class Jinja2TemplateUtil:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def get_schema_import_list(cls, gen_table: GenTableOutSchema):
|
def get_schema_import_list(cls, gen_table: GenTableOutSchema):
|
||||||
"""
|
"""
|
||||||
获取schema模板导入包列表
|
获取 schema 模板所需的 Python 导入语句集合。
|
||||||
|
|
||||||
:param gen_table: 生成表的配置信息
|
参数:
|
||||||
:return: 导入包列表
|
- gen_table (GenTableOutSchema): 生成表配置(含主子表列)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- set[str]: 导入语句字符串集合。
|
||||||
"""
|
"""
|
||||||
columns = gen_table.columns or []
|
columns = gen_table.columns or []
|
||||||
import_list = set()
|
import_list = set()
|
||||||
@@ -418,11 +451,14 @@ class Jinja2TemplateUtil:
|
|||||||
cls, gen_table: GenTableOutSchema, *, is_sub_entity: bool = False
|
cls, gen_table: GenTableOutSchema, *, is_sub_entity: bool = False
|
||||||
) -> list[str]:
|
) -> list[str]:
|
||||||
"""
|
"""
|
||||||
获取do模板导入包列表
|
获取 model 模板所需的 Python 导入语句列表(含合并后的 sqlalchemy 导入)。
|
||||||
|
|
||||||
:param gen_table: 生成表的配置信息
|
参数:
|
||||||
:param is_sub_entity: 是否为子表独立生成(含外键与 relationship)
|
- gen_table (GenTableOutSchema): 生成表配置。
|
||||||
:return: 导入包列表
|
- is_sub_entity (bool): 是否为子表独立生成(含外键与 relationship)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- list[str]: 导入语句列表。
|
||||||
"""
|
"""
|
||||||
columns = gen_table.columns or []
|
columns = gen_table.columns or []
|
||||||
import_list = set()
|
import_list = set()
|
||||||
@@ -620,6 +656,12 @@ class Jinja2TemplateUtil:
|
|||||||
将列上的 Python 类型(`get_db_type` + `DB_TO_PYTHON` 映射结果)转为前端 TS 类型片段。
|
将列上的 Python 类型(`get_db_type` + `DB_TO_PYTHON` 映射结果)转为前端 TS 类型片段。
|
||||||
|
|
||||||
与 JSON 序列化习惯一致:Decimal、日期时间多为字符串;dict/list 用宽松类型。
|
与 JSON 序列化习惯一致:Decimal、日期时间多为字符串;dict/list 用宽松类型。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- python_type (str | None): Python 类型名。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 前端 TypeScript 类型片段。
|
||||||
"""
|
"""
|
||||||
if not python_type or not str(python_type).strip():
|
if not python_type or not str(python_type).strip():
|
||||||
return "string"
|
return "string"
|
||||||
|
|||||||
@@ -65,7 +65,10 @@ async def get_scheduler_jobs_controller() -> JSONResponse:
|
|||||||
)
|
)
|
||||||
async def start_scheduler_controller() -> JSONResponse:
|
async def start_scheduler_controller() -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
启动调度器
|
启动调度器。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功提示响应。
|
||||||
"""
|
"""
|
||||||
SchedulerUtil.start()
|
SchedulerUtil.start()
|
||||||
log.info("调度器已启动")
|
log.info("调度器已启动")
|
||||||
@@ -81,7 +84,10 @@ async def start_scheduler_controller() -> JSONResponse:
|
|||||||
)
|
)
|
||||||
async def pause_scheduler_controller() -> JSONResponse:
|
async def pause_scheduler_controller() -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
暂停调度器
|
暂停调度器。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功提示响应。
|
||||||
"""
|
"""
|
||||||
SchedulerUtil.pause()
|
SchedulerUtil.pause()
|
||||||
log.info("调度器已暂停")
|
log.info("调度器已暂停")
|
||||||
@@ -97,7 +103,10 @@ async def pause_scheduler_controller() -> JSONResponse:
|
|||||||
)
|
)
|
||||||
async def resume_scheduler_controller() -> JSONResponse:
|
async def resume_scheduler_controller() -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
恢复调度器
|
恢复调度器。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功提示响应。
|
||||||
"""
|
"""
|
||||||
SchedulerUtil.resume()
|
SchedulerUtil.resume()
|
||||||
log.info("调度器已恢复")
|
log.info("调度器已恢复")
|
||||||
@@ -113,7 +122,10 @@ async def resume_scheduler_controller() -> JSONResponse:
|
|||||||
)
|
)
|
||||||
async def shutdown_scheduler_controller() -> JSONResponse:
|
async def shutdown_scheduler_controller() -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
关闭调度器
|
关闭调度器。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功提示响应。
|
||||||
"""
|
"""
|
||||||
await SchedulerUtil.shutdown()
|
await SchedulerUtil.shutdown()
|
||||||
log.info("调度器已关闭")
|
log.info("调度器已关闭")
|
||||||
@@ -129,7 +141,10 @@ async def shutdown_scheduler_controller() -> JSONResponse:
|
|||||||
)
|
)
|
||||||
async def clear_jobs_controller() -> JSONResponse:
|
async def clear_jobs_controller() -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
清空调度器中的所有任务
|
清空调度器中的所有任务。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功提示响应。
|
||||||
"""
|
"""
|
||||||
SchedulerUtil.clear_jobs()
|
SchedulerUtil.clear_jobs()
|
||||||
log.info("已清空所有任务")
|
log.info("已清空所有任务")
|
||||||
@@ -191,6 +206,9 @@ async def pause_job_controller(
|
|||||||
|
|
||||||
参数:
|
参数:
|
||||||
- job_id (str): 调度器任务ID
|
- job_id (str): 调度器任务ID
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功提示响应。
|
||||||
"""
|
"""
|
||||||
SchedulerUtil.pause_job(job_id=job_id)
|
SchedulerUtil.pause_job(job_id=job_id)
|
||||||
log.info(f"暂停任务成功: {job_id}")
|
log.info(f"暂停任务成功: {job_id}")
|
||||||
@@ -212,6 +230,9 @@ async def resume_job_controller(
|
|||||||
|
|
||||||
参数:
|
参数:
|
||||||
- job_id (str): 调度器任务ID
|
- job_id (str): 调度器任务ID
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功提示响应。
|
||||||
"""
|
"""
|
||||||
SchedulerUtil.resume_job(job_id=job_id)
|
SchedulerUtil.resume_job(job_id=job_id)
|
||||||
log.info(f"恢复任务成功: {job_id}")
|
log.info(f"恢复任务成功: {job_id}")
|
||||||
@@ -233,6 +254,9 @@ async def run_job_controller(
|
|||||||
|
|
||||||
参数:
|
参数:
|
||||||
- job_id (str): 调度器任务ID
|
- job_id (str): 调度器任务ID
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功提示响应。
|
||||||
"""
|
"""
|
||||||
SchedulerUtil.run_job_now(job_id=job_id)
|
SchedulerUtil.run_job_now(job_id=job_id)
|
||||||
log.info(f"立即执行任务成功: {job_id}")
|
log.info(f"立即执行任务成功: {job_id}")
|
||||||
@@ -254,6 +278,9 @@ async def remove_job_controller(
|
|||||||
|
|
||||||
参数:
|
参数:
|
||||||
- job_id (str): 调度器任务ID
|
- job_id (str): 调度器任务ID
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功提示响应。
|
||||||
"""
|
"""
|
||||||
SchedulerUtil.remove_job(job_id=job_id)
|
SchedulerUtil.remove_job(job_id=job_id)
|
||||||
log.info(f"移除任务成功: {job_id}")
|
log.info(f"移除任务成功: {job_id}")
|
||||||
@@ -340,6 +367,9 @@ async def delete_job_log_controller(
|
|||||||
参数:
|
参数:
|
||||||
- ids (list[int]): ID列表
|
- ids (list[int]): ID列表
|
||||||
- auth (AuthSchema): 认证信息模型
|
- auth (AuthSchema): 认证信息模型
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功提示响应。
|
||||||
"""
|
"""
|
||||||
await JobService.delete_job_log_service(auth=auth, ids=ids)
|
await JobService.delete_job_log_service(auth=auth, ids=ids)
|
||||||
log.info(f"删除执行日志成功: {ids}")
|
log.info(f"删除执行日志成功: {ids}")
|
||||||
|
|||||||
@@ -86,11 +86,17 @@ class JobCRUD(CRUDBase[JobModel, JobCreateSchema, JobUpdateSchema]):
|
|||||||
|
|
||||||
参数:
|
参数:
|
||||||
- ids (list[int]): 日志ID列表
|
- ids (list[int]): 日志ID列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
return await self.delete(ids=ids)
|
return await self.delete(ids=ids)
|
||||||
|
|
||||||
async def clear_obj_crud(self) -> None:
|
async def clear_obj_crud(self) -> None:
|
||||||
"""
|
"""
|
||||||
清空所有执行日志
|
清空所有执行日志。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
return await self.clear()
|
return await self.clear()
|
||||||
|
|||||||
@@ -67,7 +67,19 @@ class JobService:
|
|||||||
search: JobQueryParam | None = None,
|
search: JobQueryParam | None = None,
|
||||||
order_by: list[dict[str, str]] | None = None,
|
order_by: list[dict[str, str]] | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""分页查询执行日志(数据库 OFFSET/LIMIT)。"""
|
"""
|
||||||
|
分页查询执行日志(数据库 OFFSET/LIMIT)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- page_no (int): 页码。
|
||||||
|
- page_size (int): 每页条数。
|
||||||
|
- search (JobQueryParam | None): 查询条件。
|
||||||
|
- order_by (list[dict[str, str]] | None): 排序。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 分页结果。
|
||||||
|
"""
|
||||||
offset = (page_no - 1) * page_size
|
offset = (page_no - 1) * page_size
|
||||||
ob = order_by or [{"created_time": "desc"}]
|
ob = order_by or [{"created_time": "desc"}]
|
||||||
return await JobCRUD(auth).page(
|
return await JobCRUD(auth).page(
|
||||||
@@ -149,6 +161,9 @@ class JobService:
|
|||||||
参数:
|
参数:
|
||||||
- auth (AuthSchema): 认证信息模型
|
- auth (AuthSchema): 认证信息模型
|
||||||
- ids (list[int]): 日志ID列表
|
- ids (list[int]): 日志ID列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
if len(ids) < 1:
|
if len(ids) < 1:
|
||||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||||
@@ -161,6 +176,9 @@ class JobService:
|
|||||||
|
|
||||||
参数:
|
参数:
|
||||||
- auth (AuthSchema): 认证信息模型
|
- auth (AuthSchema): 认证信息模型
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
await JobCRUD(auth).clear_obj_crud()
|
await JobCRUD(auth).clear_obj_crud()
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ async def get_node_options_controller(
|
|||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""
|
"""
|
||||||
获取数据库中的定时任务节点定义(task_node),与编排节点类型无关。
|
获取数据库中的定时任务节点定义(task_node),与编排节点类型无关。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功响应,data 为选项列表。
|
||||||
"""
|
"""
|
||||||
result = await NodeService.get_node_options_service(auth=auth)
|
result = await NodeService.get_node_options_service(auth=auth)
|
||||||
log.info("获取定时任务节点选项成功")
|
log.info("获取定时任务节点选项成功")
|
||||||
|
|||||||
@@ -89,6 +89,9 @@ class NodeCRUD(CRUDBase[NodeModel, NodeCreateSchema, NodeUpdateSchema]):
|
|||||||
|
|
||||||
参数:
|
参数:
|
||||||
- ids (list[int]): 节点ID列表
|
- ids (list[int]): 节点ID列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
return await self.delete(ids=ids)
|
return await self.delete(ids=ids)
|
||||||
|
|
||||||
@@ -99,6 +102,9 @@ class NodeCRUD(CRUDBase[NodeModel, NodeCreateSchema, NodeUpdateSchema]):
|
|||||||
参数:
|
参数:
|
||||||
- ids (list[int]): 节点ID列表
|
- ids (list[int]): 节点ID列表
|
||||||
- kwargs: 其他要设置的字段,例如 available=True 或 available=False
|
- kwargs: 其他要设置的字段,例如 available=True 或 available=False
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
return await self.set(ids=ids, **kwargs)
|
return await self.set(ids=ids, **kwargs)
|
||||||
|
|
||||||
@@ -108,5 +114,8 @@ class NodeCRUD(CRUDBase[NodeModel, NodeCreateSchema, NodeUpdateSchema]):
|
|||||||
|
|
||||||
注意:
|
注意:
|
||||||
- 此操作会删除所有节点日志,请谨慎操作
|
- 此操作会删除所有节点日志,请谨慎操作
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
return await self.clear()
|
return await self.clear()
|
||||||
|
|||||||
@@ -8,7 +8,12 @@ from datetime import datetime
|
|||||||
|
|
||||||
|
|
||||||
def demo_handler(*args, **kwargs) -> dict:
|
def demo_handler(*args, **kwargs) -> dict:
|
||||||
"""示例处理器"""
|
"""
|
||||||
|
示例处理器(演示节点调用形态)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 包含 message、入参快照与时间戳。
|
||||||
|
"""
|
||||||
return {
|
return {
|
||||||
"message": "Hello from demo_handler!",
|
"message": "Hello from demo_handler!",
|
||||||
"args": args,
|
"args": args,
|
||||||
@@ -19,9 +24,14 @@ def demo_handler(*args, **kwargs) -> dict:
|
|||||||
|
|
||||||
def process_data(data: list, operation: str = "sum") -> dict:
|
def process_data(data: list, operation: str = "sum") -> dict:
|
||||||
"""
|
"""
|
||||||
简单数据处理
|
简单数值列表聚合。
|
||||||
|
|
||||||
operation: sum, avg, max, min, count
|
参数:
|
||||||
|
- data (list): 数值列表。
|
||||||
|
- operation (str): sum、avg、max、min、count 之一。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 含 operation 与 result,或 error 说明。
|
||||||
"""
|
"""
|
||||||
if not data:
|
if not data:
|
||||||
return {"error": "数据为空"}
|
return {"error": "数据为空"}
|
||||||
|
|||||||
@@ -96,7 +96,19 @@ class NodeService:
|
|||||||
search: NodeQueryParam | None = None,
|
search: NodeQueryParam | None = None,
|
||||||
order_by: list[dict[str, str]] | None = None,
|
order_by: list[dict[str, str]] | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""分页查询定时任务节点(数据库 OFFSET/LIMIT)。"""
|
"""
|
||||||
|
分页查询定时任务节点(数据库 OFFSET/LIMIT)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- page_no (int): 页码。
|
||||||
|
- page_size (int): 每页条数。
|
||||||
|
- search (NodeQueryParam | None): 查询条件。
|
||||||
|
- order_by (list[dict[str, str]] | None): 排序。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 分页结果。
|
||||||
|
"""
|
||||||
offset = (page_no - 1) * page_size
|
offset = (page_no - 1) * page_size
|
||||||
return await NodeCRUD(auth).page(
|
return await NodeCRUD(auth).page(
|
||||||
offset=offset,
|
offset=offset,
|
||||||
@@ -157,6 +169,9 @@ class NodeService:
|
|||||||
参数:
|
参数:
|
||||||
- auth (AuthSchema): 认证信息模型
|
- auth (AuthSchema): 认证信息模型
|
||||||
- ids (list[int]): 节点ID列表
|
- ids (list[int]): 节点ID列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
if len(ids) < 1:
|
if len(ids) < 1:
|
||||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||||
@@ -178,6 +193,9 @@ class NodeService:
|
|||||||
|
|
||||||
参数:
|
参数:
|
||||||
- auth (AuthSchema): 认证信息模型
|
- auth (AuthSchema): 认证信息模型
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
SchedulerUtil.clear_jobs()
|
SchedulerUtil.clear_jobs()
|
||||||
await NodeCRUD(auth).clear_obj_crud()
|
await NodeCRUD(auth).clear_obj_crud()
|
||||||
|
|||||||
@@ -33,6 +33,16 @@ async def get_workflow_detail_controller(
|
|||||||
id: Annotated[int, Path(description="工作流ID")],
|
id: Annotated[int, Path(description="工作流ID")],
|
||||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:detail"]))],
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:detail"]))],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
|
"""
|
||||||
|
根据 ID 获取工作流详情(含画布 nodes/edges)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- id (int): 工作流 ID。
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功响应,data 为详情字典。
|
||||||
|
"""
|
||||||
result_dict = await WorkflowService.get_workflow_detail_service(auth=auth, id=id)
|
result_dict = await WorkflowService.get_workflow_detail_service(auth=auth, id=id)
|
||||||
log.info(f"获取工作流详情成功 {id}")
|
log.info(f"获取工作流详情成功 {id}")
|
||||||
return SuccessResponse(data=result_dict, msg="获取工作流详情成功")
|
return SuccessResponse(data=result_dict, msg="获取工作流详情成功")
|
||||||
@@ -49,6 +59,17 @@ async def get_workflow_list_controller(
|
|||||||
search: Annotated[WorkflowQueryParam, Depends()],
|
search: Annotated[WorkflowQueryParam, Depends()],
|
||||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:query"]))],
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:query"]))],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
|
"""
|
||||||
|
分页查询工作流列表。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- page (PaginationQueryParam): 分页与排序参数。
|
||||||
|
- search (WorkflowQueryParam): 查询条件。
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功响应,data 为分页结果。
|
||||||
|
"""
|
||||||
result_dict = await WorkflowService.get_workflow_page_service(
|
result_dict = await WorkflowService.get_workflow_page_service(
|
||||||
auth=auth,
|
auth=auth,
|
||||||
page_no=page.page_no,
|
page_no=page.page_no,
|
||||||
@@ -70,6 +91,16 @@ async def create_workflow_controller(
|
|||||||
data: WorkflowCreateSchema,
|
data: WorkflowCreateSchema,
|
||||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:create"]))],
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:create"]))],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
|
"""
|
||||||
|
创建草稿工作流。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- data (WorkflowCreateSchema): 创建体。
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功响应,data 为新建工作流。
|
||||||
|
"""
|
||||||
result_dict = await WorkflowService.create_workflow_service(auth=auth, data=data)
|
result_dict = await WorkflowService.create_workflow_service(auth=auth, data=data)
|
||||||
log.info("创建工作流成功")
|
log.info("创建工作流成功")
|
||||||
return SuccessResponse(data=result_dict, msg="创建工作流成功")
|
return SuccessResponse(data=result_dict, msg="创建工作流成功")
|
||||||
@@ -86,6 +117,17 @@ async def update_workflow_controller(
|
|||||||
data: WorkflowUpdateSchema,
|
data: WorkflowUpdateSchema,
|
||||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:update"]))],
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:update"]))],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
|
"""
|
||||||
|
更新工作流及画布。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- id (int): 工作流 ID。
|
||||||
|
- data (WorkflowUpdateSchema): 更新体。
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功响应,data 为更新后的工作流。
|
||||||
|
"""
|
||||||
result_dict = await WorkflowService.update_workflow_service(auth=auth, id=id, data=data)
|
result_dict = await WorkflowService.update_workflow_service(auth=auth, id=id, data=data)
|
||||||
log.info(f"更新工作流成功 {id}")
|
log.info(f"更新工作流成功 {id}")
|
||||||
return SuccessResponse(data=result_dict, msg="更新工作流成功")
|
return SuccessResponse(data=result_dict, msg="更新工作流成功")
|
||||||
@@ -101,6 +143,16 @@ async def delete_workflow_controller(
|
|||||||
ids: Annotated[list[int], Body(description="ID列表")],
|
ids: Annotated[list[int], Body(description="ID列表")],
|
||||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:delete"]))],
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:delete"]))],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
|
"""
|
||||||
|
批量删除工作流。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- ids (list[int]): 工作流 ID 列表。
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功提示响应。
|
||||||
|
"""
|
||||||
await WorkflowService.delete_workflow_service(auth=auth, ids=ids)
|
await WorkflowService.delete_workflow_service(auth=auth, ids=ids)
|
||||||
log.info(f"删除工作流成功 {ids}")
|
log.info(f"删除工作流成功 {ids}")
|
||||||
return SuccessResponse(msg="删除工作流成功")
|
return SuccessResponse(msg="删除工作流成功")
|
||||||
@@ -117,6 +169,17 @@ async def publish_workflow_controller(
|
|||||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:update"]))],
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:update"]))],
|
||||||
body: Annotated[WorkflowPublishSchema | None, Body()] = None,
|
body: Annotated[WorkflowPublishSchema | None, Body()] = None,
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
|
"""
|
||||||
|
校验 DAG 无环后发布工作流。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- id (int): 工作流 ID。
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- body (WorkflowPublishSchema | None): 可选发布附加参数。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功响应,data 为发布后工作流。
|
||||||
|
"""
|
||||||
result_dict = await WorkflowService.publish_workflow_service(auth=auth, id=id, body=body)
|
result_dict = await WorkflowService.publish_workflow_service(auth=auth, id=id, body=body)
|
||||||
log.info(f"发布工作流成功 {id}")
|
log.info(f"发布工作流成功 {id}")
|
||||||
return SuccessResponse(data=result_dict, msg="发布工作流成功")
|
return SuccessResponse(data=result_dict, msg="发布工作流成功")
|
||||||
@@ -132,6 +195,16 @@ async def execute_workflow_controller(
|
|||||||
body: WorkflowExecuteSchema,
|
body: WorkflowExecuteSchema,
|
||||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:execute"]))],
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:definition:execute"]))],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
|
"""
|
||||||
|
使用 Prefect 按拓扑顺序执行已发布工作流。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- body (WorkflowExecuteSchema): 工作流 ID 与变量等。
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功响应,data 为执行结果摘要。
|
||||||
|
"""
|
||||||
result_dict = await WorkflowService.execute_workflow_service(auth=auth, body=body)
|
result_dict = await WorkflowService.execute_workflow_service(auth=auth, body=body)
|
||||||
log.info(f"执行工作流完成 workflow_id={body.workflow_id}")
|
log.info(f"执行工作流完成 workflow_id={body.workflow_id}")
|
||||||
return SuccessResponse(data=result_dict, msg="执行工作流完成")
|
return SuccessResponse(data=result_dict, msg="执行工作流完成")
|
||||||
|
|||||||
@@ -12,12 +12,31 @@ class WorkflowCRUD(CRUDBase[WorkflowModel, WorkflowCreateSchema, WorkflowUpdateS
|
|||||||
"""工作流数据层"""
|
"""工作流数据层"""
|
||||||
|
|
||||||
def __init__(self, auth: AuthSchema) -> None:
|
def __init__(self, auth: AuthSchema) -> None:
|
||||||
|
"""
|
||||||
|
初始化工作流 CRUD。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
super().__init__(model=WorkflowModel, auth=auth)
|
super().__init__(model=WorkflowModel, auth=auth)
|
||||||
|
|
||||||
async def get_obj_by_id_crud(
|
async def get_obj_by_id_crud(
|
||||||
self, id: int, preload: list[str | Any] | None = None
|
self, id: int, preload: list[str | Any] | None = None
|
||||||
) -> WorkflowModel | None:
|
) -> WorkflowModel | None:
|
||||||
|
"""
|
||||||
|
按主键查询工作流。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- id (int): 工作流 ID。
|
||||||
|
- preload (list[str | Any] | None): 预加载关系。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- WorkflowModel | None: 实体或 None。
|
||||||
|
"""
|
||||||
return await self.get(id=id, preload=preload)
|
return await self.get(id=id, preload=preload)
|
||||||
|
|
||||||
async def get_obj_list_crud(
|
async def get_obj_list_crud(
|
||||||
@@ -26,13 +45,52 @@ class WorkflowCRUD(CRUDBase[WorkflowModel, WorkflowCreateSchema, WorkflowUpdateS
|
|||||||
order_by: list[dict[str, str]] | None = None,
|
order_by: list[dict[str, str]] | None = None,
|
||||||
preload: list[str | Any] | None = None,
|
preload: list[str | Any] | None = None,
|
||||||
) -> Sequence[WorkflowModel]:
|
) -> Sequence[WorkflowModel]:
|
||||||
|
"""
|
||||||
|
条件列表查询工作流。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- search (dict | None): 查询条件。
|
||||||
|
- order_by (list[dict[str, str]] | None): 排序。
|
||||||
|
- preload (list[str | Any] | None): 预加载关系。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Sequence[WorkflowModel]: 工作流列表。
|
||||||
|
"""
|
||||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||||
|
|
||||||
async def create_obj_crud(self, data: WorkflowCreateSchema) -> WorkflowModel | None:
|
async def create_obj_crud(self, data: WorkflowCreateSchema) -> WorkflowModel | None:
|
||||||
|
"""
|
||||||
|
创建工作流。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- data (WorkflowCreateSchema): 创建模型。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- WorkflowModel | None: 新建实体或 None。
|
||||||
|
"""
|
||||||
return await self.create(data=data)
|
return await self.create(data=data)
|
||||||
|
|
||||||
async def update_obj_crud(self, id: int, data: WorkflowUpdateSchema) -> WorkflowModel | None:
|
async def update_obj_crud(self, id: int, data: WorkflowUpdateSchema) -> WorkflowModel | None:
|
||||||
|
"""
|
||||||
|
更新工作流。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- id (int): 工作流 ID。
|
||||||
|
- data (WorkflowUpdateSchema): 更新模型。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- WorkflowModel | None: 更新后实体或 None。
|
||||||
|
"""
|
||||||
return await self.update(id=id, data=data)
|
return await self.update(id=id, data=data)
|
||||||
|
|
||||||
async def delete_obj_crud(self, ids: list[int]) -> None:
|
async def delete_obj_crud(self, ids: list[int]) -> None:
|
||||||
|
"""
|
||||||
|
批量删除工作流。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- ids (list[int]): ID 列表。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
await self.delete(ids=ids)
|
await self.delete(ids=ids)
|
||||||
|
|||||||
@@ -27,6 +27,19 @@ class WorkflowService:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_workflow_detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
async def get_workflow_detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||||
|
"""
|
||||||
|
获取工作流详情。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- id (int): 工作流 ID。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 序列化后的工作流详情。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 不存在时抛出。
|
||||||
|
"""
|
||||||
obj = await WorkflowCRUD(auth).get_obj_by_id_crud(id=id)
|
obj = await WorkflowCRUD(auth).get_obj_by_id_crud(id=id)
|
||||||
if not obj:
|
if not obj:
|
||||||
raise CustomException(msg="工作流不存在")
|
raise CustomException(msg="工作流不存在")
|
||||||
@@ -39,6 +52,17 @@ class WorkflowService:
|
|||||||
search: WorkflowQueryParam | None = None,
|
search: WorkflowQueryParam | None = None,
|
||||||
order_by: list[dict[str, str]] | None = None,
|
order_by: list[dict[str, str]] | None = None,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
|
"""
|
||||||
|
获取工作流列表(非分页)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- search (WorkflowQueryParam | None): 查询条件。
|
||||||
|
- order_by (list[dict[str, str]] | None): 排序。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- list[dict]: 工作流字典列表。
|
||||||
|
"""
|
||||||
if order_by is None:
|
if order_by is None:
|
||||||
order_by = [{"updated_time": "desc"}]
|
order_by = [{"updated_time": "desc"}]
|
||||||
obj_list = await WorkflowCRUD(auth).get_obj_list_crud(
|
obj_list = await WorkflowCRUD(auth).get_obj_list_crud(
|
||||||
@@ -56,7 +80,19 @@ class WorkflowService:
|
|||||||
search: WorkflowQueryParam | None = None,
|
search: WorkflowQueryParam | None = None,
|
||||||
order_by: list[dict[str, str]] | None = None,
|
order_by: list[dict[str, str]] | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""分页查询工作流(数据库 OFFSET/LIMIT)。"""
|
"""
|
||||||
|
分页查询工作流(数据库 OFFSET/LIMIT)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- page_no (int): 页码。
|
||||||
|
- page_size (int): 每页条数。
|
||||||
|
- search (WorkflowQueryParam | None): 查询条件。
|
||||||
|
- order_by (list[dict[str, str]] | None): 排序。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 分页结果(items 已 JSON 友好序列化)。
|
||||||
|
"""
|
||||||
offset = (page_no - 1) * page_size
|
offset = (page_no - 1) * page_size
|
||||||
order = order_by or [{"updated_time": "desc"}]
|
order = order_by or [{"updated_time": "desc"}]
|
||||||
result = await WorkflowCRUD(auth).page(
|
result = await WorkflowCRUD(auth).page(
|
||||||
@@ -73,6 +109,19 @@ class WorkflowService:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create_workflow_service(cls, auth: AuthSchema, data: WorkflowCreateSchema) -> dict:
|
async def create_workflow_service(cls, auth: AuthSchema, data: WorkflowCreateSchema) -> dict:
|
||||||
|
"""
|
||||||
|
创建工作流草稿。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- data (WorkflowCreateSchema): 创建体。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 新建工作流字典。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 编码重复或创建失败。
|
||||||
|
"""
|
||||||
exist = await WorkflowCRUD(auth).get(code=data.code)
|
exist = await WorkflowCRUD(auth).get(code=data.code)
|
||||||
if exist:
|
if exist:
|
||||||
raise CustomException(msg="流程编码已存在")
|
raise CustomException(msg="流程编码已存在")
|
||||||
@@ -85,6 +134,20 @@ class WorkflowService:
|
|||||||
async def update_workflow_service(
|
async def update_workflow_service(
|
||||||
cls, auth: AuthSchema, id: int, data: WorkflowUpdateSchema
|
cls, auth: AuthSchema, id: int, data: WorkflowUpdateSchema
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
"""
|
||||||
|
更新工作流。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- id (int): 工作流 ID。
|
||||||
|
- data (WorkflowUpdateSchema): 更新体。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 更新后工作流字典。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 不存在、编码冲突或更新失败。
|
||||||
|
"""
|
||||||
exist = await WorkflowCRUD(auth).get_obj_by_id_crud(id=id)
|
exist = await WorkflowCRUD(auth).get_obj_by_id_crud(id=id)
|
||||||
if not exist:
|
if not exist:
|
||||||
raise CustomException(msg="工作流不存在")
|
raise CustomException(msg="工作流不存在")
|
||||||
@@ -99,6 +162,19 @@ class WorkflowService:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def delete_workflow_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
async def delete_workflow_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||||
|
"""
|
||||||
|
批量删除工作流。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- ids (list[int]): ID 列表。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: ID 为空时抛出。
|
||||||
|
"""
|
||||||
if not ids:
|
if not ids:
|
||||||
raise CustomException(msg="删除ID不能为空")
|
raise CustomException(msg="删除ID不能为空")
|
||||||
await WorkflowCRUD(auth).delete_obj_crud(ids=ids)
|
await WorkflowCRUD(auth).delete_obj_crud(ids=ids)
|
||||||
@@ -107,6 +183,20 @@ class WorkflowService:
|
|||||||
async def publish_workflow_service(
|
async def publish_workflow_service(
|
||||||
cls, auth: AuthSchema, id: int, body: WorkflowPublishSchema | None = None
|
cls, auth: AuthSchema, id: int, body: WorkflowPublishSchema | None = None
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
"""
|
||||||
|
校验 DAG 后发布工作流。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- id (int): 工作流 ID。
|
||||||
|
- body (WorkflowPublishSchema | None): 可选附加参数。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 发布后工作流字典。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 不存在、图无效或发布失败。
|
||||||
|
"""
|
||||||
obj = await WorkflowCRUD(auth).get_obj_by_id_crud(id=id)
|
obj = await WorkflowCRUD(auth).get_obj_by_id_crud(id=id)
|
||||||
if not obj:
|
if not obj:
|
||||||
raise CustomException(msg="工作流不存在")
|
raise CustomException(msg="工作流不存在")
|
||||||
@@ -135,6 +225,19 @@ class WorkflowService:
|
|||||||
async def execute_workflow_service(
|
async def execute_workflow_service(
|
||||||
cls, auth: AuthSchema, body: WorkflowExecuteSchema
|
cls, auth: AuthSchema, body: WorkflowExecuteSchema
|
||||||
) -> dict:
|
) -> dict:
|
||||||
|
"""
|
||||||
|
执行已发布工作流(Prefect 同步入口在线程池中运行)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- body (WorkflowExecuteSchema): 工作流 ID 与变量。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 执行结果摘要(成功或失败结构)。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 未发布、缺节点、节点类型未注册等。
|
||||||
|
"""
|
||||||
obj = await WorkflowCRUD(auth).get_obj_by_id_crud(id=body.workflow_id)
|
obj = await WorkflowCRUD(auth).get_obj_by_id_crud(id=body.workflow_id)
|
||||||
if not obj:
|
if not obj:
|
||||||
raise CustomException(msg="工作流不存在")
|
raise CustomException(msg="工作流不存在")
|
||||||
|
|||||||
@@ -34,7 +34,19 @@ def _parse_kwargs(kwargs_str: str | None) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def validate_workflow_graph(nodes: list[dict], edges: list[dict]) -> None:
|
def validate_workflow_graph(nodes: list[dict], edges: list[dict]) -> None:
|
||||||
"""校验图有效且无环。"""
|
"""
|
||||||
|
校验画布图有效且无环。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- nodes (list[dict]): 节点列表(须含 id)。
|
||||||
|
- edges (list[dict]): 边列表(source/target)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ValueError: 图为空、边引用非法或存在环。
|
||||||
|
"""
|
||||||
if not nodes:
|
if not nodes:
|
||||||
raise ValueError("工作流至少需要一个节点")
|
raise ValueError("工作流至少需要一个节点")
|
||||||
ids = {n["id"] for n in nodes}
|
ids = {n["id"] for n in nodes}
|
||||||
@@ -60,6 +72,16 @@ def validate_workflow_graph(nodes: list[dict], edges: list[dict]) -> None:
|
|||||||
|
|
||||||
|
|
||||||
def topological_sort(nodes: list[dict], edges: list[dict]) -> list[dict]:
|
def topological_sort(nodes: list[dict], edges: list[dict]) -> list[dict]:
|
||||||
|
"""
|
||||||
|
按拓扑顺序返回节点(调用方须先保证无环)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- nodes (list[dict]): 节点列表。
|
||||||
|
- edges (list[dict]): 边列表。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- list[dict]: 拓扑有序节点。
|
||||||
|
"""
|
||||||
id_to_node = {n["id"]: n for n in nodes}
|
id_to_node = {n["id"]: n for n in nodes}
|
||||||
in_degree: dict[str, int] = {n["id"]: 0 for n in nodes}
|
in_degree: dict[str, int] = {n["id"]: 0 for n in nodes}
|
||||||
adj: dict[str, list[str]] = defaultdict(list)
|
adj: dict[str, list[str]] = defaultdict(list)
|
||||||
@@ -88,6 +110,21 @@ def prefect_node_task(
|
|||||||
upstream: dict[str, Any],
|
upstream: dict[str, Any],
|
||||||
flow_variables: dict[str, Any],
|
flow_variables: dict[str, Any],
|
||||||
) -> Any:
|
) -> Any:
|
||||||
|
"""
|
||||||
|
单个画布节点的 Prefect Task:通过 SchedulerUtil 执行用户代码块。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- vue_node_id (str): 画布节点 id。
|
||||||
|
- node_type_code (str): 节点类型编码。
|
||||||
|
- code_block (str): 可执行代码字符串。
|
||||||
|
- args_str (str | None): 逗号分隔位置参数说明。
|
||||||
|
- kwargs_str (str | None): JSON 关键字参数。
|
||||||
|
- upstream (dict[str, Any]): 上游节点输出。
|
||||||
|
- flow_variables (dict[str, Any]): 流程级变量。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Any: 任务执行结果。
|
||||||
|
"""
|
||||||
job_id = f"wfnode-{vue_node_id}"
|
job_id = f"wfnode-{vue_node_id}"
|
||||||
args = _parse_args(args_str)
|
args = _parse_args(args_str)
|
||||||
kw = _parse_kwargs(kwargs_str)
|
kw = _parse_kwargs(kwargs_str)
|
||||||
@@ -104,7 +141,16 @@ def run_workflow_prefect_flow(
|
|||||||
flow_variables: dict[str, Any],
|
flow_variables: dict[str, Any],
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
node_templates: code -> {func, args, kwargs} 来自 task_workflow_node_type
|
Prefect Flow:按拓扑顺序提交并收集各节点结果。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- ordered_nodes (list[dict]): 已排序节点列表。
|
||||||
|
- edges (list[dict]): 边列表。
|
||||||
|
- node_templates (dict[str, dict[str, Any]]): 类型编码到 {func, args, kwargs},来自 task_workflow_node_type。
|
||||||
|
- flow_variables (dict[str, Any]): 流程变量。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict[str, Any]: 含 node_results、status 等。
|
||||||
"""
|
"""
|
||||||
results: dict[str, Any] = {}
|
results: dict[str, Any] = {}
|
||||||
for node in ordered_nodes:
|
for node in ordered_nodes:
|
||||||
@@ -148,6 +194,15 @@ def run_prefect_workflow_sync(
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
同步入口:校验 DAG、拓扑排序后执行 Prefect Flow。
|
同步入口:校验 DAG、拓扑排序后执行 Prefect Flow。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- nodes (list[dict]): 画布节点。
|
||||||
|
- edges (list[dict]): 画布边。
|
||||||
|
- node_templates (dict[str, dict[str, Any]]): 节点类型模板。
|
||||||
|
- flow_variables (dict[str, Any]): 流程变量。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict[str, Any]: Flow 执行汇总结果。
|
||||||
"""
|
"""
|
||||||
validate_workflow_graph(nodes, edges)
|
validate_workflow_graph(nodes, edges)
|
||||||
ordered = topological_sort(nodes, edges)
|
ordered = topological_sort(nodes, edges)
|
||||||
@@ -160,4 +215,10 @@ def run_prefect_workflow_sync(
|
|||||||
|
|
||||||
|
|
||||||
def utc_now_iso() -> str:
|
def utc_now_iso() -> str:
|
||||||
|
"""
|
||||||
|
当前 UTC 时间的 ISO 8601 字符串。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: ISO 格式时间戳。
|
||||||
|
"""
|
||||||
return datetime.now(timezone.utc).isoformat()
|
return datetime.now(timezone.utc).isoformat()
|
||||||
|
|||||||
@@ -34,6 +34,15 @@ WorkflowNodeTypeRouter = APIRouter(
|
|||||||
async def get_workflow_node_type_options_controller(
|
async def get_workflow_node_type_options_controller(
|
||||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:query"]))],
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:query"]))],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
|
"""
|
||||||
|
获取画布用编排节点类型选项(仅启用项)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功响应,data 为选项列表。
|
||||||
|
"""
|
||||||
result = await WorkflowNodeTypeService.get_options_service(auth=auth)
|
result = await WorkflowNodeTypeService.get_options_service(auth=auth)
|
||||||
log.info("获取编排节点类型选项成功")
|
log.info("获取编排节点类型选项成功")
|
||||||
return SuccessResponse(data=result, msg="获取编排节点类型选项成功")
|
return SuccessResponse(data=result, msg="获取编排节点类型选项成功")
|
||||||
@@ -48,6 +57,16 @@ async def get_workflow_node_type_detail_controller(
|
|||||||
id: Annotated[int, Path(description="ID")],
|
id: Annotated[int, Path(description="ID")],
|
||||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:query"]))],
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:query"]))],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
|
"""
|
||||||
|
获取编排节点类型详情。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- id (int): 主键。
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功响应,data 为详情。
|
||||||
|
"""
|
||||||
result_dict = await WorkflowNodeTypeService.get_detail_service(auth=auth, id=id)
|
result_dict = await WorkflowNodeTypeService.get_detail_service(auth=auth, id=id)
|
||||||
log.info(f"获取编排节点类型详情成功 {id}")
|
log.info(f"获取编排节点类型详情成功 {id}")
|
||||||
return SuccessResponse(data=result_dict, msg="获取编排节点类型详情成功")
|
return SuccessResponse(data=result_dict, msg="获取编排节点类型详情成功")
|
||||||
@@ -63,6 +82,17 @@ async def get_workflow_node_type_list_controller(
|
|||||||
search: Annotated[WorkflowNodeTypeQueryParam, Depends()],
|
search: Annotated[WorkflowNodeTypeQueryParam, Depends()],
|
||||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:query"]))],
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:query"]))],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
|
"""
|
||||||
|
分页查询编排节点类型列表。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- page (PaginationQueryParam): 分页与排序。
|
||||||
|
- search (WorkflowNodeTypeQueryParam): 查询条件。
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功响应,data 为分页结果。
|
||||||
|
"""
|
||||||
order_by = [{"sort_order": "asc"}, {"id": "asc"}]
|
order_by = [{"sort_order": "asc"}, {"id": "asc"}]
|
||||||
if page.order_by:
|
if page.order_by:
|
||||||
order_by = page.order_by
|
order_by = page.order_by
|
||||||
@@ -86,6 +116,16 @@ async def create_workflow_node_type_controller(
|
|||||||
data: WorkflowNodeTypeCreateSchema,
|
data: WorkflowNodeTypeCreateSchema,
|
||||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:create"]))],
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:create"]))],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
|
"""
|
||||||
|
创建编排节点类型。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- data (WorkflowNodeTypeCreateSchema): 创建体。
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功响应,data 为新记录。
|
||||||
|
"""
|
||||||
result_dict = await WorkflowNodeTypeService.create_service(auth=auth, data=data)
|
result_dict = await WorkflowNodeTypeService.create_service(auth=auth, data=data)
|
||||||
log.info("创建编排节点类型成功")
|
log.info("创建编排节点类型成功")
|
||||||
return SuccessResponse(data=result_dict, msg="创建编排节点类型成功")
|
return SuccessResponse(data=result_dict, msg="创建编排节点类型成功")
|
||||||
@@ -101,6 +141,17 @@ async def update_workflow_node_type_controller(
|
|||||||
data: WorkflowNodeTypeUpdateSchema,
|
data: WorkflowNodeTypeUpdateSchema,
|
||||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:update"]))],
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:update"]))],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
|
"""
|
||||||
|
更新编排节点类型。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- id (int): 主键。
|
||||||
|
- data (WorkflowNodeTypeUpdateSchema): 更新体。
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功响应,data 为更新后记录。
|
||||||
|
"""
|
||||||
result_dict = await WorkflowNodeTypeService.update_service(auth=auth, id=id, data=data)
|
result_dict = await WorkflowNodeTypeService.update_service(auth=auth, id=id, data=data)
|
||||||
log.info(f"更新编排节点类型成功 {id}")
|
log.info(f"更新编排节点类型成功 {id}")
|
||||||
return SuccessResponse(data=result_dict, msg="更新编排节点类型成功")
|
return SuccessResponse(data=result_dict, msg="更新编排节点类型成功")
|
||||||
@@ -115,6 +166,16 @@ async def delete_workflow_node_type_controller(
|
|||||||
ids: Annotated[list[int], Body(description="ID列表")],
|
ids: Annotated[list[int], Body(description="ID列表")],
|
||||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:delete"]))],
|
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_task:workflow:node-type:delete"]))],
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
|
"""
|
||||||
|
批量删除编排节点类型。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- ids (list[int]): ID 列表。
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- JSONResponse: 成功提示响应。
|
||||||
|
"""
|
||||||
await WorkflowNodeTypeService.delete_service(auth=auth, ids=ids)
|
await WorkflowNodeTypeService.delete_service(auth=auth, ids=ids)
|
||||||
log.info(f"删除编排节点类型成功 {ids}")
|
log.info(f"删除编排节点类型成功 {ids}")
|
||||||
return SuccessResponse(msg="删除编排节点类型成功")
|
return SuccessResponse(msg="删除编排节点类型成功")
|
||||||
|
|||||||
@@ -13,12 +13,31 @@ class WorkflowNodeTypeCRUD(CRUDBase[WorkflowNodeTypeModel, WorkflowNodeTypeCreat
|
|||||||
"""编排节点类型 CRUD"""
|
"""编排节点类型 CRUD"""
|
||||||
|
|
||||||
def __init__(self, auth: AuthSchema) -> None:
|
def __init__(self, auth: AuthSchema) -> None:
|
||||||
|
"""
|
||||||
|
初始化编排节点类型 CRUD。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
self.auth = auth
|
self.auth = auth
|
||||||
super().__init__(model=WorkflowNodeTypeModel, auth=auth)
|
super().__init__(model=WorkflowNodeTypeModel, auth=auth)
|
||||||
|
|
||||||
async def get_obj_by_id_crud(
|
async def get_obj_by_id_crud(
|
||||||
self, id: int, preload: list[str | Any] | None = None
|
self, id: int, preload: list[str | Any] | None = None
|
||||||
) -> WorkflowNodeTypeModel | None:
|
) -> WorkflowNodeTypeModel | None:
|
||||||
|
"""
|
||||||
|
按主键查询节点类型。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- id (int): 主键。
|
||||||
|
- preload (list[str | Any] | None): 预加载关系。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- WorkflowNodeTypeModel | None: 实体或 None。
|
||||||
|
"""
|
||||||
return await self.get(id=id, preload=preload)
|
return await self.get(id=id, preload=preload)
|
||||||
|
|
||||||
async def get_obj_list_crud(
|
async def get_obj_list_crud(
|
||||||
@@ -27,19 +46,63 @@ class WorkflowNodeTypeCRUD(CRUDBase[WorkflowNodeTypeModel, WorkflowNodeTypeCreat
|
|||||||
order_by: list[dict[str, str]] | None = None,
|
order_by: list[dict[str, str]] | None = None,
|
||||||
preload: list[str | Any] | None = None,
|
preload: list[str | Any] | None = None,
|
||||||
) -> Sequence[WorkflowNodeTypeModel]:
|
) -> Sequence[WorkflowNodeTypeModel]:
|
||||||
|
"""
|
||||||
|
条件列表查询节点类型。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- search (dict | None): 查询条件。
|
||||||
|
- order_by (list[dict[str, str]] | None): 排序。
|
||||||
|
- preload (list[str | Any] | None): 预加载关系。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Sequence[WorkflowNodeTypeModel]: 列表。
|
||||||
|
"""
|
||||||
return await self.list(search=search, order_by=order_by, preload=preload)
|
return await self.list(search=search, order_by=order_by, preload=preload)
|
||||||
|
|
||||||
async def create_obj_crud(self, data: WorkflowNodeTypeCreateSchema) -> WorkflowNodeTypeModel | None:
|
async def create_obj_crud(self, data: WorkflowNodeTypeCreateSchema) -> WorkflowNodeTypeModel | None:
|
||||||
|
"""
|
||||||
|
创建节点类型。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- data (WorkflowNodeTypeCreateSchema): 创建模型。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- WorkflowNodeTypeModel | None: 新建实体或 None。
|
||||||
|
"""
|
||||||
return await self.create(data=data)
|
return await self.create(data=data)
|
||||||
|
|
||||||
async def update_obj_crud(self, id: int, data: WorkflowNodeTypeUpdateSchema) -> WorkflowNodeTypeModel | None:
|
async def update_obj_crud(self, id: int, data: WorkflowNodeTypeUpdateSchema) -> WorkflowNodeTypeModel | None:
|
||||||
|
"""
|
||||||
|
更新节点类型。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- id (int): 主键。
|
||||||
|
- data (WorkflowNodeTypeUpdateSchema): 更新模型。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- WorkflowNodeTypeModel | None: 更新后实体或 None。
|
||||||
|
"""
|
||||||
return await self.update(id=id, data=data)
|
return await self.update(id=id, data=data)
|
||||||
|
|
||||||
async def delete_obj_crud(self, ids: list[int]) -> None:
|
async def delete_obj_crud(self, ids: list[int]) -> None:
|
||||||
|
"""
|
||||||
|
批量删除节点类型。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- ids (list[int]): ID 列表。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
await self.delete(ids=ids)
|
await self.delete(ids=ids)
|
||||||
|
|
||||||
async def list_active_options_crud(self) -> Sequence[WorkflowNodeTypeModel]:
|
async def list_active_options_crud(self) -> Sequence[WorkflowNodeTypeModel]:
|
||||||
"""画布:仅启用的类型,按 sort_order、id 排序"""
|
"""
|
||||||
|
画布用:仅启用的类型,按 sort_order、id 排序。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Sequence[WorkflowNodeTypeModel]: 启用中的节点类型列表。
|
||||||
|
"""
|
||||||
return await self.get_obj_list_crud(
|
return await self.get_obj_list_crud(
|
||||||
search={"is_active": (QueueEnum.eq.value, True)},
|
search={"is_active": (QueueEnum.eq.value, True)},
|
||||||
order_by=[{"sort_order": "asc"}, {"id": "asc"}],
|
order_by=[{"sort_order": "asc"}, {"id": "asc"}],
|
||||||
|
|||||||
@@ -19,7 +19,15 @@ class WorkflowNodeTypeService:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_options_service(cls, auth: AuthSchema) -> list[dict]:
|
async def get_options_service(cls, auth: AuthSchema) -> list[dict]:
|
||||||
"""画布左侧 palette:仅返回启用项,结构与前端原 Node options 对齐"""
|
"""
|
||||||
|
画布左侧 palette:仅返回启用项,结构与前端 Node options 对齐。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- list[dict]: 选项字典列表。
|
||||||
|
"""
|
||||||
objs = await WorkflowNodeTypeCRUD(auth).list_active_options_crud()
|
objs = await WorkflowNodeTypeCRUD(auth).list_active_options_crud()
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
@@ -35,6 +43,19 @@ class WorkflowNodeTypeService:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def get_detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
async def get_detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||||
|
"""
|
||||||
|
获取编排节点类型详情。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- id (int): 主键。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 序列化后的详情。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 不存在时抛出。
|
||||||
|
"""
|
||||||
obj = await WorkflowNodeTypeCRUD(auth).get_obj_by_id_crud(id=id)
|
obj = await WorkflowNodeTypeCRUD(auth).get_obj_by_id_crud(id=id)
|
||||||
if not obj:
|
if not obj:
|
||||||
raise CustomException(msg="编排节点类型不存在")
|
raise CustomException(msg="编排节点类型不存在")
|
||||||
@@ -47,6 +68,17 @@ class WorkflowNodeTypeService:
|
|||||||
search: WorkflowNodeTypeQueryParam | None = None,
|
search: WorkflowNodeTypeQueryParam | None = None,
|
||||||
order_by: list[dict[str, str]] | None = None,
|
order_by: list[dict[str, str]] | None = None,
|
||||||
) -> list[dict]:
|
) -> list[dict]:
|
||||||
|
"""
|
||||||
|
获取编排节点类型列表(非分页)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- search (WorkflowNodeTypeQueryParam | None): 查询条件。
|
||||||
|
- order_by (list[dict[str, str]] | None): 排序。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- list[dict]: 字典列表。
|
||||||
|
"""
|
||||||
if order_by is None:
|
if order_by is None:
|
||||||
order_by = [{"sort_order": "asc"}, {"id": "asc"}]
|
order_by = [{"sort_order": "asc"}, {"id": "asc"}]
|
||||||
obj_list = await WorkflowNodeTypeCRUD(auth).get_obj_list_crud(
|
obj_list = await WorkflowNodeTypeCRUD(auth).get_obj_list_crud(
|
||||||
@@ -64,7 +96,19 @@ class WorkflowNodeTypeService:
|
|||||||
search: WorkflowNodeTypeQueryParam | None = None,
|
search: WorkflowNodeTypeQueryParam | None = None,
|
||||||
order_by: list[dict[str, str]] | None = None,
|
order_by: list[dict[str, str]] | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""分页查询编排节点类型(数据库 OFFSET/LIMIT)。"""
|
"""
|
||||||
|
分页查询编排节点类型(数据库 OFFSET/LIMIT)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- page_no (int): 页码。
|
||||||
|
- page_size (int): 每页条数。
|
||||||
|
- search (WorkflowNodeTypeQueryParam | None): 查询条件。
|
||||||
|
- order_by (list[dict[str, str]] | None): 排序。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 分页结果(items 已 JSON 友好序列化)。
|
||||||
|
"""
|
||||||
offset = (page_no - 1) * page_size
|
offset = (page_no - 1) * page_size
|
||||||
order = order_by or [{"sort_order": "asc"}, {"id": "asc"}]
|
order = order_by or [{"sort_order": "asc"}, {"id": "asc"}]
|
||||||
result = await WorkflowNodeTypeCRUD(auth).page(
|
result = await WorkflowNodeTypeCRUD(auth).page(
|
||||||
@@ -82,6 +126,19 @@ class WorkflowNodeTypeService:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create_service(cls, auth: AuthSchema, data: WorkflowNodeTypeCreateSchema) -> dict:
|
async def create_service(cls, auth: AuthSchema, data: WorkflowNodeTypeCreateSchema) -> dict:
|
||||||
|
"""
|
||||||
|
创建编排节点类型。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- data (WorkflowNodeTypeCreateSchema): 创建体。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 新建记录字典。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 编码重复或创建失败。
|
||||||
|
"""
|
||||||
exist = await WorkflowNodeTypeCRUD(auth).get(code=data.code)
|
exist = await WorkflowNodeTypeCRUD(auth).get(code=data.code)
|
||||||
if exist:
|
if exist:
|
||||||
raise CustomException(msg="节点编码已存在")
|
raise CustomException(msg="节点编码已存在")
|
||||||
@@ -92,6 +149,20 @@ class WorkflowNodeTypeService:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def update_service(cls, auth: AuthSchema, id: int, data: WorkflowNodeTypeUpdateSchema) -> dict:
|
async def update_service(cls, auth: AuthSchema, id: int, data: WorkflowNodeTypeUpdateSchema) -> dict:
|
||||||
|
"""
|
||||||
|
更新编排节点类型。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- id (int): 主键。
|
||||||
|
- data (WorkflowNodeTypeUpdateSchema): 更新体。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 更新后字典。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 不存在、编码冲突或更新失败。
|
||||||
|
"""
|
||||||
exist = await WorkflowNodeTypeCRUD(auth).get_obj_by_id_crud(id=id)
|
exist = await WorkflowNodeTypeCRUD(auth).get_obj_by_id_crud(id=id)
|
||||||
if not exist:
|
if not exist:
|
||||||
raise CustomException(msg="编排节点类型不存在")
|
raise CustomException(msg="编排节点类型不存在")
|
||||||
@@ -106,6 +177,19 @@ class WorkflowNodeTypeService:
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||||
|
"""
|
||||||
|
批量删除编排节点类型。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息。
|
||||||
|
- ids (list[int]): ID 列表。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: ID 为空时抛出。
|
||||||
|
"""
|
||||||
if not ids:
|
if not ids:
|
||||||
raise CustomException(msg="删除ID不能为空")
|
raise CustomException(msg="删除ID不能为空")
|
||||||
await WorkflowNodeTypeCRUD(auth).delete_obj_crud(ids=ids)
|
await WorkflowNodeTypeCRUD(auth).delete_obj_crud(ids=ids)
|
||||||
|
|||||||
@@ -131,6 +131,15 @@ class InitializeData:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def create_object(obj_data: dict) -> Any:
|
def create_object(obj_data: dict) -> Any:
|
||||||
|
"""
|
||||||
|
由单条 dict 递归构建模型实例(含 children)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- obj_data (dict): 行数据,可含嵌套 children。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Any: SQLAlchemy 模型实例。
|
||||||
|
"""
|
||||||
# 分离 children 数据
|
# 分离 children 数据
|
||||||
children_data = obj_data.pop("children", [])
|
children_data = obj_data.pop("children", [])
|
||||||
|
|
||||||
@@ -173,7 +182,10 @@ class InitializeData:
|
|||||||
|
|
||||||
async def init_db(self) -> None:
|
async def init_db(self) -> None:
|
||||||
"""
|
"""
|
||||||
执行完整初始化流程
|
执行完整初始化流程:建表并导入种子数据。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
# 先创建表结构
|
# 先创建表结构
|
||||||
await self.__init_create_table()
|
await self.__init_create_table()
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ from app.core.logger import log
|
|||||||
|
|
||||||
def worship(env: str) -> None:
|
def worship(env: str) -> None:
|
||||||
"""
|
"""
|
||||||
获取项目启动Banner(优先读取 banner.txt)
|
读取并打印启动 Banner(优先 `banner.txt`,并附带当前环境名)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- env (str): 当前运行环境标识。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
"""
|
"""
|
||||||
if BANNER_FILE.exists():
|
if BANNER_FILE.exists():
|
||||||
banner = BANNER_FILE.read_text(encoding="utf-8")
|
banner = BANNER_FILE.read_text(encoding="utf-8")
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class CaptchaUtil:
|
|||||||
生成带有噪声和干扰的验证码图片(4位随机字符)。
|
生成带有噪声和干扰的验证码图片(4位随机字符)。
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- Tuple[str, str]: [base64图片字符串, 验证码值]。
|
- tuple[str, str]: Base64 PNG 字符串与验证码明文。
|
||||||
"""
|
"""
|
||||||
# 生成4位随机验证码
|
# 生成4位随机验证码
|
||||||
chars = string.digits + string.ascii_letters
|
chars = string.digits + string.ascii_letters
|
||||||
@@ -82,40 +82,35 @@ class CaptchaUtil:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def captcha_arithmetic(cls, difficulty: str = "medium") -> tuple[str, int]:
|
def captcha_arithmetic(cls, difficulty: str = "medium") -> tuple[str, int]:
|
||||||
"""
|
"""
|
||||||
创建验证码图片(加减乘运算)。
|
创建算术验证码图片(加减乘运算);浅色底、居中算式,无旋转与干扰线/噪点。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- difficulty (str): 难度级别 (easy, medium, hard)
|
- difficulty (str): 难度级别(easy / medium / hard),控制数字范围与可用运算符。
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- Tuple[str, int]: [base64图片字符串, 计算结果]。
|
- tuple[str, int]: base64 编码的 PNG 图片字符串与正确答案(整数)。
|
||||||
"""
|
"""
|
||||||
# 根据难度级别调整参数
|
|
||||||
difficulty_config = {
|
difficulty_config = {
|
||||||
"easy": {"num_range": (1, 9), "operators": ["+", "-"], "noise_density": 0.01, "line_count": 3},
|
"easy": {"num_range": (1, 9), "operators": ["+", "-"]},
|
||||||
"medium": {"num_range": (1, 15), "operators": ["+", "-", "*"], "noise_density": 0.015, "line_count": 4},
|
"medium": {"num_range": (1, 15), "operators": ["+", "-", "*"]},
|
||||||
"hard": {"num_range": (1, 20), "operators": ["+", "-", "*"], "noise_density": 0.02, "line_count": 6}
|
"hard": {"num_range": (1, 20), "operators": ["+", "-", "*"]},
|
||||||
}
|
}
|
||||||
config = difficulty_config.get(difficulty, difficulty_config["medium"])
|
config = difficulty_config.get(difficulty, difficulty_config["medium"])
|
||||||
|
|
||||||
# 生成运算数字和运算符
|
|
||||||
operators = config["operators"]
|
operators = config["operators"]
|
||||||
operator = random.choice(operators)
|
operator = random.choice(operators)
|
||||||
num_range = config["num_range"]
|
num_range = config["num_range"]
|
||||||
|
|
||||||
# 对于减法,确保num1大于num2
|
|
||||||
if operator == "-":
|
if operator == "-":
|
||||||
num1 = random.randint(num_range[0] + 5, num_range[1])
|
num1 = random.randint(num_range[0] + 5, num_range[1])
|
||||||
num2 = random.randint(num_range[0], num1 - 1)
|
num2 = random.randint(num_range[0], num1 - 1)
|
||||||
elif operator == "*":
|
elif operator == "*":
|
||||||
# 对于乘法,限制数字大小避免结果过大
|
|
||||||
num1 = random.randint(num_range[0], min(10, num_range[1]))
|
num1 = random.randint(num_range[0], min(10, num_range[1]))
|
||||||
num2 = random.randint(num_range[0], min(10, num_range[1]))
|
num2 = random.randint(num_range[0], min(10, num_range[1]))
|
||||||
else: # 加法
|
else:
|
||||||
num1 = random.randint(num_range[0], num_range[1])
|
num1 = random.randint(num_range[0], num_range[1])
|
||||||
num2 = random.randint(num_range[0], num_range[1])
|
num2 = random.randint(num_range[0], num_range[1])
|
||||||
|
|
||||||
# 计算结果
|
|
||||||
result_map = {
|
result_map = {
|
||||||
"+": lambda x, y: x + y,
|
"+": lambda x, y: x + y,
|
||||||
"-": lambda x, y: x - y,
|
"-": lambda x, y: x - y,
|
||||||
@@ -123,73 +118,19 @@ class CaptchaUtil:
|
|||||||
}
|
}
|
||||||
captcha_value = result_map[operator](num1, num2)
|
captcha_value = result_map[operator](num1, num2)
|
||||||
|
|
||||||
# 创建空白图像,使用随机浅色背景
|
|
||||||
width, height = 160, 60
|
width, height = 160, 60
|
||||||
background_color = tuple(random.randint(230, 255) for _ in range(3))
|
image = Image.new("RGB", (width, height), color=(248, 249, 250))
|
||||||
image = Image.new("RGB", (width, height), color=background_color)
|
|
||||||
draw = ImageDraw.Draw(image)
|
draw = ImageDraw.Draw(image)
|
||||||
|
|
||||||
# 设置字体
|
|
||||||
font = ImageFont.truetype(font=settings.CAPTCHA_FONT_PATH, size=settings.CAPTCHA_FONT_SIZE)
|
font = ImageFont.truetype(font=settings.CAPTCHA_FONT_PATH, size=settings.CAPTCHA_FONT_SIZE)
|
||||||
|
|
||||||
# 绘制文本,使用深色增加对比度
|
|
||||||
text = f"{num1} {operator} {num2} = ?"
|
text = f"{num1} {operator} {num2} = ?"
|
||||||
text_bbox = draw.textbbox((0, 0), text, font=font)
|
tb = draw.textbbox((0, 0), text, font=font)
|
||||||
text_width = text_bbox[2] - text_bbox[0]
|
tw, th = tb[2] - tb[0], tb[3] - tb[1]
|
||||||
x = (width - text_width) // 2
|
x = (width - tw) // 2
|
||||||
y = 15
|
y = (height - th) // 2 - tb[1]
|
||||||
|
draw.text((x, y), text, fill=(55, 65, 81), font=font)
|
||||||
|
|
||||||
# 随机偏移和旋转文本
|
|
||||||
rotation = random.uniform(-5, 5)
|
|
||||||
text_image = Image.new("RGBA", (width, height), (255, 255, 255, 0))
|
|
||||||
text_draw = ImageDraw.Draw(text_image)
|
|
||||||
text_draw.text((x, y), text, fill=(0, 0, 139), font=font)
|
|
||||||
text_image = text_image.rotate(rotation, expand=1)
|
|
||||||
|
|
||||||
# 计算粘贴位置,使旋转后的文本居中
|
|
||||||
text_width, text_height = text_image.size
|
|
||||||
paste_x = (width - text_width) // 2
|
|
||||||
paste_y = (height - text_height) // 2
|
|
||||||
image.paste(text_image, (paste_x, paste_y), text_image)
|
|
||||||
|
|
||||||
# 添加干扰线
|
|
||||||
for _ in range(config["line_count"]):
|
|
||||||
line_color = tuple(random.randint(120, 180) for _ in range(3))
|
|
||||||
# 生成随机曲线
|
|
||||||
points = []
|
|
||||||
for i in range(0, width, 15):
|
|
||||||
points.append((i, int(random.uniform(0, height))))
|
|
||||||
draw.line(points, fill=line_color, width=2)
|
|
||||||
|
|
||||||
# 添加随机噪点
|
|
||||||
noise_count = int(width * height * config["noise_density"])
|
|
||||||
for _ in range(noise_count):
|
|
||||||
point_color = tuple(random.randint(0, 255) for _ in range(3))
|
|
||||||
draw.point(
|
|
||||||
(random.randint(0, width), random.randint(0, height)),
|
|
||||||
fill=point_color,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 添加背景纹理
|
|
||||||
for _ in range(15):
|
|
||||||
texture_color = tuple(random.randint(200, 240) for _ in range(3))
|
|
||||||
radius = random.randint(3, 10)
|
|
||||||
x0 = random.randint(0, width - radius)
|
|
||||||
y0 = random.randint(0, height - radius)
|
|
||||||
x1 = x0 + radius
|
|
||||||
y1 = y0 + radius
|
|
||||||
draw.ellipse(
|
|
||||||
(
|
|
||||||
x0,
|
|
||||||
y0,
|
|
||||||
x1,
|
|
||||||
y1
|
|
||||||
),
|
|
||||||
fill=texture_color,
|
|
||||||
outline=None
|
|
||||||
)
|
|
||||||
|
|
||||||
# 将图像数据保存到内存中并转换为base64
|
|
||||||
buffer = BytesIO()
|
buffer = BytesIO()
|
||||||
image.save(buffer, format="PNG", optimize=True)
|
image.save(buffer, format="PNG", optimize=True)
|
||||||
base64_string = base64.b64encode(buffer.getvalue()).decode()
|
base64_string = base64.b64encode(buffer.getvalue()).decode()
|
||||||
|
|||||||
@@ -78,7 +78,12 @@ def get_random_character() -> str:
|
|||||||
|
|
||||||
|
|
||||||
def uuid4_str() -> str:
|
def uuid4_str() -> str:
|
||||||
"""数据库引擎 UUID 类型兼容性解决方案"""
|
"""
|
||||||
|
数据库引擎 UUID 类型兼容:返回无连字符的 UUID 字符串。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: UUID 字符串。
|
||||||
|
"""
|
||||||
return str(uuid.uuid4())
|
return str(uuid.uuid4())
|
||||||
|
|
||||||
|
|
||||||
@@ -251,7 +256,15 @@ def bytes2human(n: int, format_str: str = "%(value).1f%(symbol)s") -> str:
|
|||||||
|
|
||||||
|
|
||||||
def bytes2file_response(bytes_info: bytes) -> Generator[bytes, Any, None]:
|
def bytes2file_response(bytes_info: bytes) -> Generator[bytes, Any, None]:
|
||||||
"""生成文件响应"""
|
"""
|
||||||
|
将字节内容封装为单块流式生成器,供文件下载响应使用。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- bytes_info (bytes): 文件二进制内容。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Generator[bytes, Any, None]: 仅 yield 一次的字节生成器。
|
||||||
|
"""
|
||||||
yield bytes_info
|
yield bytes_info
|
||||||
|
|
||||||
|
|
||||||
@@ -286,11 +299,14 @@ class SqlalchemyUtil:
|
|||||||
transform_case: Literal["no_case", "snake_to_camel", "camel_to_snake"] = "no_case",
|
transform_case: Literal["no_case", "snake_to_camel", "camel_to_snake"] = "no_case",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
将sqlalchemy模型对象转换为字典
|
将 SQLAlchemy 模型或字典转为普通 dict,并可做键名大小写转换。
|
||||||
|
|
||||||
:param obj: sqlalchemy模型对象或普通字典
|
参数:
|
||||||
:param transform_case: 转换得到的结果形式,可选的有'no_case'(不转换)、'snake_to_camel'(下划线转小驼峰)、'camel_to_snake'(小驼峰转下划线),默认为'no_case'
|
- obj (DeclarativeBase | dict[str, Any]): 模型实例或字典。
|
||||||
:return: 字典结果
|
- transform_case (Literal[...]): no_case / snake_to_camel / camel_to_snake。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict[str, Any]: 扁平字典结果。
|
||||||
"""
|
"""
|
||||||
if isinstance(obj, DeclarativeBase):
|
if isinstance(obj, DeclarativeBase):
|
||||||
base_dict = obj.__dict__.copy()
|
base_dict = obj.__dict__.copy()
|
||||||
@@ -314,11 +330,14 @@ class SqlalchemyUtil:
|
|||||||
transform_case: Literal["no_case", "snake_to_camel", "camel_to_snake"] = "no_case",
|
transform_case: Literal["no_case", "snake_to_camel", "camel_to_snake"] = "no_case",
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
将sqlalchemy查询结果序列化
|
将 SQLAlchemy 查询结果(模型、列表、Row 等)递归序列化为可 JSON 化结构。
|
||||||
|
|
||||||
:param result: sqlalchemy查询结果
|
参数:
|
||||||
:param transform_case: 'no_case'(不转换)、'snake_to_camel'(下划线转小驼峰)、'camel_to_snake'(小驼峰转下划线),默认为'no_case'
|
- result (Any): ORM 对象、列表、Row 等。
|
||||||
:return: 序列化结果
|
- transform_case (Literal[...]): 键名转换策略。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Any: 序列化后的 Python 内置类型或嵌套结构。
|
||||||
"""
|
"""
|
||||||
if isinstance(result, (DeclarativeBase, dict)):
|
if isinstance(result, (DeclarativeBase, dict)):
|
||||||
return cls.base_to_dict(result, transform_case)
|
return cls.base_to_dict(result, transform_case)
|
||||||
@@ -342,11 +361,14 @@ class SqlalchemyUtil:
|
|||||||
cls, dialect_name: str, need_explicit_null: bool = True
|
cls, dialect_name: str, need_explicit_null: bool = True
|
||||||
) -> Null | None:
|
) -> Null | None:
|
||||||
"""
|
"""
|
||||||
根据数据库方言动态返回值为null的server_default
|
按方言返回列默认值中的 NULL 表达(PostgreSQL 可显式 DEFAULT NULL)。
|
||||||
|
|
||||||
:param dialect_name: 数据库方言名称
|
参数:
|
||||||
:param need_explicit_null: 是否需要显式DEFAULT NULL
|
- dialect_name (str): 数据库方言名。
|
||||||
:return: 不同数据库方言对应的null_server_default
|
- need_explicit_null (bool): 是否生成显式 NULL 默认值。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Null | None: SQLAlchemy null() 或 None。
|
||||||
"""
|
"""
|
||||||
if need_explicit_null and dialect_name == "postgres":
|
if need_explicit_null and dialect_name == "postgres":
|
||||||
return null()
|
return null()
|
||||||
@@ -361,10 +383,13 @@ class CamelCaseUtil:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def snake_to_camel(cls, snake_str: str):
|
def snake_to_camel(cls, snake_str: str):
|
||||||
"""
|
"""
|
||||||
下划线形式字符串(snake_case)转换为小驼峰形式字符串(camelCase)
|
下划线形式 (snake_case) 转为小驼峰形式 (camelCase)。
|
||||||
|
|
||||||
:param snake_str: 下划线形式字符串
|
参数:
|
||||||
:return: 小驼峰形式字符串
|
- snake_str (str): 下划线分隔字符串。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 合并首字母大写后的驼峰字符串。
|
||||||
"""
|
"""
|
||||||
# 分割字符串
|
# 分割字符串
|
||||||
words = snake_str.split("_")
|
words = snake_str.split("_")
|
||||||
@@ -376,10 +401,13 @@ class CamelCaseUtil:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def transform_result(cls, result: Any):
|
def transform_result(cls, result: Any):
|
||||||
"""
|
"""
|
||||||
针对不同类型将下划线形式(snake_case)批量转换为小驼峰形式(camelCase)方法
|
将查询结果递归序列化并将键名转为小驼峰。
|
||||||
|
|
||||||
:param result: 输入数据
|
参数:
|
||||||
:return: 小驼峰形式结果
|
- result (Any): ORM 查询结果或嵌套结构。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Any: 小驼峰键名的序列化结果。
|
||||||
"""
|
"""
|
||||||
return SqlalchemyUtil.serialize_result(result=result, transform_case="snake_to_camel")
|
return SqlalchemyUtil.serialize_result(result=result, transform_case="snake_to_camel")
|
||||||
|
|
||||||
@@ -392,10 +420,13 @@ class SnakeCaseUtil:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def camel_to_snake(cls, camel_str: str):
|
def camel_to_snake(cls, camel_str: str):
|
||||||
"""
|
"""
|
||||||
小驼峰形式字符串(camelCase)转换为下划线形式字符串(snake_case)
|
小驼峰形式 (camelCase) 转为下划线形式 (snake_case)。
|
||||||
|
|
||||||
:param camel_str: 小驼峰形式字符串
|
参数:
|
||||||
:return: 下划线形式字符串
|
- camel_str (str): 驼峰字符串。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 下划线分隔且全小写。
|
||||||
"""
|
"""
|
||||||
# 在大写字母前添加一个下划线,然后将整个字符串转为小写
|
# 在大写字母前添加一个下划线,然后将整个字符串转为小写
|
||||||
words = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", camel_str)
|
words = re.sub(r"(.)([A-Z][a-z]+)", r"\1_\2", camel_str)
|
||||||
@@ -404,9 +435,12 @@ class SnakeCaseUtil:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def transform_result(cls, result: Any):
|
def transform_result(cls, result: Any):
|
||||||
"""
|
"""
|
||||||
针对不同类型将下划线形式(snake_case)批量转换为小驼峰形式(camelCase)方法
|
将查询结果递归序列化并将键名转为下划线形式。
|
||||||
|
|
||||||
:param result: 输入数据
|
参数:
|
||||||
:return: 小驼峰形式结果
|
- result (Any): ORM 查询结果或嵌套结构。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Any: 下划线键名的序列化结果。
|
||||||
"""
|
"""
|
||||||
return SqlalchemyUtil.serialize_result(result=result, transform_case="camel_to_snake")
|
return SqlalchemyUtil.serialize_result(result=result, transform_case="camel_to_snake")
|
||||||
|
|||||||
@@ -20,7 +20,21 @@ def console_run(
|
|||||||
scheduler_ready: bool | None = None,
|
scheduler_ready: bool | None = None,
|
||||||
limiter_ready: bool | None = None,
|
limiter_ready: bool | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""显示启动信息面板"""
|
"""
|
||||||
|
在终端输出 Rich 面板:服务信息、组件就绪状态与文档链接。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- host (str): 监听主机。
|
||||||
|
- port (int): 监听端口。
|
||||||
|
- reload (bool): 是否开启热重载。
|
||||||
|
- database_ready (bool | None): 数据库是否就绪。
|
||||||
|
- redis_ready (bool | None): Redis 是否就绪。
|
||||||
|
- scheduler_ready (bool | None): 调度器是否就绪。
|
||||||
|
- limiter_ready (bool | None): 限流器是否就绪。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
|
|
||||||
url = f"http://{host}:{port}"
|
url = f"http://{host}:{port}"
|
||||||
base_url = f"{url}{settings.ROOT_PATH}"
|
base_url = f"{url}{settings.ROOT_PATH}"
|
||||||
@@ -85,7 +99,12 @@ def console_run(
|
|||||||
|
|
||||||
|
|
||||||
def console_close() -> None:
|
def console_close() -> None:
|
||||||
"""显示关闭信息"""
|
"""
|
||||||
|
在终端输出服务关闭提示面板。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
shutdown_content = Text()
|
shutdown_content = Text()
|
||||||
shutdown_content.append("🛑 ", style="bold red")
|
shutdown_content.append("🛑 ", style="bold red")
|
||||||
shutdown_content.append("FastapiAdmin 服务关闭")
|
shutdown_content.append("FastapiAdmin 服务关闭")
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ from openpyxl.worksheet.datavalidation import DataValidation
|
|||||||
|
|
||||||
|
|
||||||
class ExcelUtil:
|
class ExcelUtil:
|
||||||
"""Excel文件处理工具类"""
|
"""
|
||||||
|
Excel 模板生成与列表导出(openpyxl / pandas)。
|
||||||
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def __mapping_list(cls, list_data: list[dict[str, Any]], mapping_dict: dict) -> list:
|
def __mapping_list(cls, list_data: list[dict[str, Any]], mapping_dict: dict) -> list:
|
||||||
|
|||||||
@@ -12,23 +12,31 @@ from app.core.exceptions import CustomException
|
|||||||
|
|
||||||
|
|
||||||
class ImportUtil:
|
class ImportUtil:
|
||||||
|
"""
|
||||||
|
扫描工程中的 ORM 模型文件并做有效性校验的辅助类。
|
||||||
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def find_project_root(cls) -> Path:
|
def find_project_root(cls) -> Path:
|
||||||
"""
|
"""
|
||||||
查找项目根目录
|
返回项目根目录(与配置中的 `BASE_DIR` 一致)。
|
||||||
|
|
||||||
:return: 项目根目录路径
|
返回:
|
||||||
|
- Path: 项目根路径。
|
||||||
"""
|
"""
|
||||||
return BASE_DIR
|
return BASE_DIR
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_valid_model(cls, obj: Any, base_class: type) -> bool:
|
def is_valid_model(cls, obj: Any, base_class: type) -> bool:
|
||||||
"""
|
"""
|
||||||
验证是否为有效的SQLAlchemy模型类
|
判断是否为可映射的 SQLAlchemy 模型类(含表名与非空列)。
|
||||||
|
|
||||||
:param obj: 待验证的对象
|
参数:
|
||||||
:param base_class: SQLAlchemy的基类
|
- obj (Any): 待验证对象(一般为类)。
|
||||||
:return: 验证结果
|
- base_class (type): ORM 声明基类。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- bool: 是否为有效模型类。
|
||||||
"""
|
"""
|
||||||
# 必须继承自base_class且不是base_class本身
|
# 必须继承自base_class且不是base_class本身
|
||||||
if not (inspect.isclass(obj) and issubclass(obj, base_class) and obj is not base_class):
|
if not (inspect.isclass(obj) and issubclass(obj, base_class) and obj is not base_class):
|
||||||
@@ -48,10 +56,17 @@ class ImportUtil:
|
|||||||
@lru_cache(maxsize=256)
|
@lru_cache(maxsize=256)
|
||||||
def find_models(cls, base_class: type) -> list[Any]:
|
def find_models(cls, base_class: type) -> list[Any]:
|
||||||
"""
|
"""
|
||||||
查找并过滤有效的模型类,避免重复和无效定义
|
遍历工程内 `model.py` / `models.py`,收集去重后的有效模型类。
|
||||||
|
|
||||||
:param base_class: SQLAlchemy的Base类,用于验证模型类
|
参数:
|
||||||
:return: 有效模型类列表
|
- base_class (type): SQLAlchemy 声明基类。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- list[Any]: 模型类列表。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- ImportError: 模块导入失败(非「无法从某名导入」类警告)。
|
||||||
|
- CustomException: 处理模块时发生未预期错误。
|
||||||
"""
|
"""
|
||||||
models = []
|
models = []
|
||||||
# 按类对象去重
|
# 按类对象去重
|
||||||
@@ -156,12 +171,19 @@ class ImportUtil:
|
|||||||
seen_tables: set[str],
|
seen_tables: set[str],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
专门查找APScheduler相关的模型
|
尝试从调度相关模块补充 `apscheduler_jobs` 表对应模型。
|
||||||
|
|
||||||
:param base_class: SQLAlchemy的Base类
|
参数:
|
||||||
:param models: 模型列表
|
- base_class (type): ORM 声明基类。
|
||||||
:param seen_models: 已处理的模型集合
|
- models (list[Any]): 已收集模型列表(就地追加)。
|
||||||
:param seen_tables: 已处理的表名集合
|
- seen_models (set[Any]): 已见模型对象集合。
|
||||||
|
- seen_tables (set[str]): 已见表名集合。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 扫描过程出现未预期错误。
|
||||||
"""
|
"""
|
||||||
# 尝试从apscheduler相关模块导入
|
# 尝试从apscheduler相关模块导入
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -44,10 +44,16 @@ class IpLocalUtil:
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def resolve_location_for_log(cls, ip: str | None) -> str | None:
|
async def resolve_location_for_log(cls, ip: str | None) -> str | None:
|
||||||
"""
|
"""
|
||||||
登录与操作日志写入 ``login_location`` 时统一入口。
|
登录与操作日志写入 ``login_location`` 时的统一解析入口。
|
||||||
|
|
||||||
与 ``settings.LOGIN_RESOLVE_IP_LOCATION`` 一致:为 ``False`` 时不请求外网,
|
与 ``settings.LOGIN_RESOLVE_IP_LOCATION`` 联动:关闭时不请求外网,仅返回占位描述,
|
||||||
避免登录 POST 在 ``OperationLogRoute`` 收尾阶段仍触发 IP 归属地查询导致变慢。
|
避免登录 POST 在 ``OperationLogRoute`` 收尾阶段因外网查询变慢。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- ip (str | None): 客户端 IP,可为空。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str | None: 展示用归属地文案;无需解析时可能为 ``None``。
|
||||||
"""
|
"""
|
||||||
if not ip:
|
if not ip:
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -119,13 +119,16 @@ def sanitize_html(content: str) -> str:
|
|||||||
|
|
||||||
def sanitize_html_with_styles(content: str) -> str:
|
def sanitize_html_with_styles(content: str) -> str:
|
||||||
"""
|
"""
|
||||||
清理 HTML 内容(包含样式),移除潜在的 XSS 攻击代码。
|
清理 HTML 内容;标签与属性白名单与 `sanitize_html` 一致。
|
||||||
|
|
||||||
参数:
|
参数:
|
||||||
- content (str): 需要清理的 HTML 内容
|
- content (str): 需要清理的 HTML 内容。
|
||||||
|
|
||||||
返回:
|
返回:
|
||||||
- str: 清理后的安全 HTML 内容
|
- str: 清理后的 HTML 字符串。
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- `ALLOWED_STYLES` 供后续接入 bleach 样式清洗时使用,当前实现与 `sanitize_html` 相同。
|
||||||
"""
|
"""
|
||||||
if not content:
|
if not content:
|
||||||
return content
|
return content
|
||||||
|
|||||||
+33
-4
@@ -14,7 +14,12 @@ alembic_cfg = Config("alembic.ini")
|
|||||||
|
|
||||||
|
|
||||||
def create_app() -> FastAPI:
|
def create_app() -> FastAPI:
|
||||||
"""创建 FastAPI 应用实例"""
|
"""
|
||||||
|
创建 FastAPI 应用实例并完成日志、中间件、路由与静态资源注册。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- FastAPI: 已配置生命周期的应用对象。
|
||||||
|
"""
|
||||||
from app.config.setting import settings
|
from app.config.setting import settings
|
||||||
from app.plugin.init_app import (
|
from app.plugin.init_app import (
|
||||||
lifespan,
|
lifespan,
|
||||||
@@ -56,7 +61,15 @@ def run(
|
|||||||
EnvironmentEnum, typer.Option("--env", help="运行环境 (dev, prod)")
|
EnvironmentEnum, typer.Option("--env", help="运行环境 (dev, prod)")
|
||||||
] = EnvironmentEnum.DEV,
|
] = EnvironmentEnum.DEV,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""启动FastAPI服务"""
|
"""
|
||||||
|
按指定环境加载配置并启动 Uvicorn(开发环境开启 reload)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- env (EnvironmentEnum): 运行环境,对应 `--env`。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# 设置环境变量
|
# 设置环境变量
|
||||||
@@ -104,7 +117,15 @@ def revision(
|
|||||||
EnvironmentEnum, typer.Option("--env", help="运行环境 (dev, prod)")
|
EnvironmentEnum, typer.Option("--env", help="运行环境 (dev, prod)")
|
||||||
] = EnvironmentEnum.DEV,
|
] = EnvironmentEnum.DEV,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""生成新的 Alembic 迁移脚本"""
|
"""
|
||||||
|
使用 Alembic 自动生成迁移脚本(autogenerate)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- env (EnvironmentEnum): 运行环境,用于加载对应数据库模型元数据。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
os.environ["ENVIRONMENT"] = env.value
|
os.environ["ENVIRONMENT"] = env.value
|
||||||
command.revision(alembic_cfg, autogenerate=True, message="迁移脚本")
|
command.revision(alembic_cfg, autogenerate=True, message="迁移脚本")
|
||||||
typer.echo("迁移脚本已生成")
|
typer.echo("迁移脚本已生成")
|
||||||
@@ -119,7 +140,15 @@ def upgrade(
|
|||||||
EnvironmentEnum, typer.Option("--env", help="运行环境 (dev, prod)")
|
EnvironmentEnum, typer.Option("--env", help="运行环境 (dev, prod)")
|
||||||
] = EnvironmentEnum.DEV,
|
] = EnvironmentEnum.DEV,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""应用最新的 Alembic 迁移"""
|
"""
|
||||||
|
将数据库升级到 Alembic 最新版本(head)。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- env (EnvironmentEnum): 运行环境。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
os.environ["ENVIRONMENT"] = env.value
|
os.environ["ENVIRONMENT"] = env.value
|
||||||
command.upgrade(alembic_cfg, "head")
|
command.upgrade(alembic_cfg, "head")
|
||||||
typer.echo("所有迁移已应用。")
|
typer.echo("所有迁移已应用。")
|
||||||
|
|||||||
@@ -15,5 +15,11 @@ app = create_app()
|
|||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def test_client():
|
def test_client():
|
||||||
|
"""
|
||||||
|
模块级 HTTP 测试客户端(复用同一应用实例与生命周期)。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- TestClient: 供用例发起的同步测试客户端(yield 注入)。
|
||||||
|
"""
|
||||||
with TestClient(app) as client:
|
with TestClient(app) as client:
|
||||||
yield client
|
yield client
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"""
|
"""
|
||||||
测试文件
|
后端接口测试入口。
|
||||||
|
|
||||||
注意:使用普通的 def 定义测试函数,不要使用 async def
|
注意:测试函数使用同步 `def`,由 TestClient 驱动;勿对用例本身使用 `async def`。
|
||||||
执行命令: pytest tests/test.py
|
执行示例: `pytest tests/test_main.py` 或 `pytest tests/`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -10,10 +10,23 @@ from fastapi.testclient import TestClient
|
|||||||
|
|
||||||
|
|
||||||
def test_check_health(test_client: TestClient) -> None:
|
def test_check_health(test_client: TestClient) -> None:
|
||||||
"""测试健康检查接口"""
|
"""
|
||||||
response = test_client.get("/common/health")
|
校验 `/common/health/` 返回统一成功响应结构。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- test_client (TestClient): pytest 注入的客户端。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
|
response = test_client.get("/common/health/")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
assert response.json() == {"msg": "Healthy"}
|
body = response.json()
|
||||||
|
assert body["success"] is True
|
||||||
|
assert body["code"] == 0
|
||||||
|
assert body["msg"] == "系统健康"
|
||||||
|
assert body["data"] is True
|
||||||
|
assert body["status_code"] == 200
|
||||||
|
|
||||||
|
|
||||||
# 运行所有测试
|
# 运行所有测试
|
||||||
|
|||||||
@@ -732,17 +732,18 @@ const fullMenuTree = ref<MenuTable[]>([]);
|
|||||||
/** 从表格「在菜单下新增」进入时锁定父级(仅允许按钮) */
|
/** 从表格「在菜单下新增」进入时锁定父级(仅允许按钮) */
|
||||||
const createParentLocked = ref(false);
|
const createParentLocked = ref(false);
|
||||||
|
|
||||||
|
/** 目录下:目录、菜单、外链;菜单下:仅按钮 */
|
||||||
function typesAllowedUnderParent(parentType: MenuTypeEnum): MenuTypeEnum[] {
|
function typesAllowedUnderParent(parentType: MenuTypeEnum): MenuTypeEnum[] {
|
||||||
switch (parentType) {
|
switch (parentType) {
|
||||||
case MenuTypeEnum.CATALOG:
|
case MenuTypeEnum.CATALOG:
|
||||||
return [MenuTypeEnum.CATALOG, MenuTypeEnum.MENU, MenuTypeEnum.BUTTON, MenuTypeEnum.EXTLINK];
|
return [MenuTypeEnum.CATALOG, MenuTypeEnum.MENU, MenuTypeEnum.EXTLINK];
|
||||||
case MenuTypeEnum.MENU:
|
case MenuTypeEnum.MENU:
|
||||||
return [MenuTypeEnum.BUTTON];
|
return [MenuTypeEnum.BUTTON];
|
||||||
case MenuTypeEnum.BUTTON:
|
case MenuTypeEnum.BUTTON:
|
||||||
case MenuTypeEnum.EXTLINK:
|
case MenuTypeEnum.EXTLINK:
|
||||||
return [];
|
return [];
|
||||||
default:
|
default:
|
||||||
return [MenuTypeEnum.CATALOG, MenuTypeEnum.MENU, MenuTypeEnum.BUTTON, MenuTypeEnum.EXTLINK];
|
return [MenuTypeEnum.CATALOG, MenuTypeEnum.MENU, MenuTypeEnum.EXTLINK];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -768,11 +769,11 @@ const allowedMenuTypeValues = computed((): MenuTypeEnum[] => {
|
|||||||
}
|
}
|
||||||
const pid = formData.parent_id;
|
const pid = formData.parent_id;
|
||||||
if (pid == null || pid === undefined) {
|
if (pid == null || pid === undefined) {
|
||||||
return [MenuTypeEnum.CATALOG, MenuTypeEnum.MENU, MenuTypeEnum.BUTTON, MenuTypeEnum.EXTLINK];
|
return [MenuTypeEnum.CATALOG, MenuTypeEnum.MENU, MenuTypeEnum.EXTLINK];
|
||||||
}
|
}
|
||||||
const parentNode = findMenuNodeById(pid);
|
const parentNode = findMenuNodeById(pid);
|
||||||
if (!parentNode?.type) {
|
if (!parentNode?.type) {
|
||||||
return [MenuTypeEnum.CATALOG, MenuTypeEnum.MENU, MenuTypeEnum.BUTTON, MenuTypeEnum.EXTLINK];
|
return [MenuTypeEnum.CATALOG, MenuTypeEnum.MENU, MenuTypeEnum.EXTLINK];
|
||||||
}
|
}
|
||||||
return typesAllowedUnderParent(parentNode.type as MenuTypeEnum);
|
return typesAllowedUnderParent(parentNode.type as MenuTypeEnum);
|
||||||
});
|
});
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.api.v1.module_system.user.model import UserModel
|
||||||
|
|
||||||
|
|
||||||
|
class AuthSchema(BaseModel):
|
||||||
|
"""权限认证模型"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
||||||
|
|
||||||
|
user: UserModel | None = Field(default=None, description="用户信息")
|
||||||
|
check_data_scope: bool = Field(default=True, description="是否检查数据权限")
|
||||||
|
db: AsyncSession = Field(description="数据库会话")
|
||||||
|
current_permission: str | None = Field(default=None, description="当前请求的权限标识")
|
||||||
|
|
||||||
|
|
||||||
|
class JWTPayloadSchema(BaseModel):
|
||||||
|
"""JWT载荷模型"""
|
||||||
|
|
||||||
|
sub: str = Field(..., description="用户登录信息")
|
||||||
|
is_refresh: bool = Field(default=False, description="是否刷新token")
|
||||||
|
exp: datetime | int = Field(..., description="过期时间")
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def validate_fields(self):
|
||||||
|
if not self.sub or len(self.sub.strip()) == 0:
|
||||||
|
raise ValueError("会话编号不能为空")
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
class JWTOutSchema(BaseModel):
|
||||||
|
"""JWT响应模型"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
access_token: str = Field(..., min_length=1, description="访问token")
|
||||||
|
refresh_token: str = Field(..., min_length=1, description="刷新token")
|
||||||
|
token_type: str = Field(default="Bearer", description="token类型")
|
||||||
|
expires_in: int = Field(..., gt=0, description="过期时间(秒)")
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshTokenPayloadSchema(BaseModel):
|
||||||
|
"""刷新Token载荷模型"""
|
||||||
|
|
||||||
|
refresh_token: str = Field(..., min_length=1, description="刷新token")
|
||||||
|
|
||||||
|
|
||||||
|
class LogoutPayloadSchema(BaseModel):
|
||||||
|
"""退出登录载荷模型"""
|
||||||
|
|
||||||
|
token: str = Field(..., min_length=1, description="token")
|
||||||
|
|
||||||
|
|
||||||
|
class CaptchaOutSchema(BaseModel):
|
||||||
|
"""验证码响应模型"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
enable: bool = Field(default=True, description="是否启用验证码")
|
||||||
|
key: str = Field(..., min_length=1, description="验证码唯一标识")
|
||||||
|
img_base: str = Field(..., min_length=1, description="Base64编码的验证码图片")
|
||||||
|
|
||||||
|
|
||||||
|
class AutoLoginUserSchema(BaseModel):
|
||||||
|
"""免登录用户信息模型"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: int = Field(..., description="用户ID")
|
||||||
|
username: str = Field(..., description="用户名")
|
||||||
|
name: str = Field(..., description="用户姓名")
|
||||||
|
avatar: str | None = Field(default=None, description="头像")
|
||||||
|
|
||||||
|
|
||||||
|
class AutoLoginTokenSchema(BaseModel):
|
||||||
|
"""免登录Token响应模型"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
token: str = Field(..., description="免登录Token")
|
||||||
|
user: AutoLoginUserSchema = Field(..., description="用户信息")
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
from collections.abc import Sequence
|
||||||
|
|
||||||
|
from sqlalchemy import delete, select, update
|
||||||
|
|
||||||
|
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||||
|
from app.api.v1.module_system.dept.crud import DeptCRUD
|
||||||
|
from app.api.v1.module_system.menu.crud import MenuCRUD
|
||||||
|
from app.core.base_crud import CRUDBase
|
||||||
|
|
||||||
|
from .model import RoleMenuDeptsModel, RoleMenusModel, RoleModel
|
||||||
|
from .schema import MenuDataScopeItem, RoleCreateSchema, RoleUpdateSchema
|
||||||
|
|
||||||
|
|
||||||
|
class RoleCRUD(CRUDBase[RoleModel, RoleCreateSchema, RoleUpdateSchema]):
|
||||||
|
"""角色模块数据层"""
|
||||||
|
|
||||||
|
def __init__(self, auth: AuthSchema) -> None:
|
||||||
|
"""
|
||||||
|
初始化角色模块数据层
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息模型
|
||||||
|
"""
|
||||||
|
self.auth = auth
|
||||||
|
super().__init__(model=RoleModel, auth=auth)
|
||||||
|
|
||||||
|
async def get_by_id_crud(self, id: int, preload: list | None = None) -> RoleModel | None:
|
||||||
|
"""
|
||||||
|
根据id获取角色信息
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- id (int): 角色ID
|
||||||
|
- preload (list | None): 预加载选项
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- RoleModel | None: 角色模型对象
|
||||||
|
"""
|
||||||
|
return await self.get(id=id, preload=preload)
|
||||||
|
|
||||||
|
async def get_list_crud(
|
||||||
|
self,
|
||||||
|
search: dict | None = None,
|
||||||
|
order_by: list | None = None,
|
||||||
|
preload: list | None = None,
|
||||||
|
) -> Sequence[RoleModel]:
|
||||||
|
"""
|
||||||
|
获取角色列表
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- search (dict | None): 查询参数
|
||||||
|
- order_by (list | None): 排序参数
|
||||||
|
- preload (list | None): 预加载选项
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Sequence[RoleModel]: 角色模型对象列表
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
"""
|
||||||
|
设置角色的菜单权限
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- role_ids (List[int]): 角色ID列表
|
||||||
|
- menu_ids (List[int]): 菜单ID列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
|
roles = await self.list(search={"id": ("in", role_ids)})
|
||||||
|
menus = await MenuCRUD(self.auth).get_list_crud(search={"id": ("in", menu_ids)})
|
||||||
|
|
||||||
|
for obj in roles:
|
||||||
|
relationship = obj.menus
|
||||||
|
relationship.clear()
|
||||||
|
relationship.extend(menus)
|
||||||
|
await self.auth.db.flush()
|
||||||
|
|
||||||
|
async def set_role_data_scope_crud(self, role_ids: list[int], data_scope: int) -> None:
|
||||||
|
"""
|
||||||
|
设置角色的数据范围
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- role_ids (list[int]): 角色ID列表
|
||||||
|
- data_scope (int): 数据范围
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
|
await self.set(ids=role_ids, data_scope=data_scope)
|
||||||
|
|
||||||
|
async def set_role_depts_crud(self, role_ids: list[int], dept_ids: list[int]) -> None:
|
||||||
|
"""
|
||||||
|
设置角色的部门权限
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- role_ids (list[int]): 角色ID列表
|
||||||
|
- dept_ids (list[int]): 部门ID列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
|
roles = await self.list(search={"id": ("in", role_ids)})
|
||||||
|
depts = await DeptCRUD(self.auth).get_list_crud(search={"id": ("in", dept_ids)})
|
||||||
|
|
||||||
|
for obj in roles:
|
||||||
|
relationship = obj.depts
|
||||||
|
relationship.clear()
|
||||||
|
relationship.extend(depts)
|
||||||
|
await self.auth.db.flush()
|
||||||
|
|
||||||
|
async def set_available_crud(self, ids: list[int], status: str) -> None:
|
||||||
|
"""
|
||||||
|
设置角色的可用状态
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- ids (list[int]): 角色ID列表
|
||||||
|
- status (str): 可用状态
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
|
await self.set(ids=ids, status=status)
|
||||||
|
|
||||||
|
async def set_role_menu_data_scopes_crud(
|
||||||
|
self, role_id: int, menu_data_scopes: list[MenuDataScopeItem]
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
设置角色的菜单级数据权限
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- role_id (int): 角色ID
|
||||||
|
- menu_data_scopes (list[MenuDataScopeItem]): 菜单级数据权限配置列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
|
for item in menu_data_scopes:
|
||||||
|
# 更新 sys_role_menus 中的 data_scope
|
||||||
|
await self.auth.db.execute(
|
||||||
|
update(RoleMenusModel)
|
||||||
|
.where(RoleMenusModel.role_id == role_id, RoleMenusModel.menu_id == item.menu_id)
|
||||||
|
.values(data_scope=item.data_scope)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 先清除该(角色, 菜单)对的旧自定义部门记录
|
||||||
|
await self.auth.db.execute(
|
||||||
|
delete(RoleMenuDeptsModel).where(
|
||||||
|
RoleMenuDeptsModel.role_id == role_id,
|
||||||
|
RoleMenuDeptsModel.menu_id == item.menu_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# 如果 data_scope=5,写入新的自定义部门记录
|
||||||
|
if item.data_scope == 5 and item.dept_ids:
|
||||||
|
for dept_id in item.dept_ids:
|
||||||
|
self.auth.db.add(
|
||||||
|
RoleMenuDeptsModel(role_id=role_id, menu_id=item.menu_id, dept_id=dept_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
await self.auth.db.flush()
|
||||||
|
|
||||||
|
async def get_role_menu_data_scopes_crud(self, role_id: int) -> list[dict]:
|
||||||
|
"""
|
||||||
|
获取角色的菜单级数据权限配置
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- role_id (int): 角色ID
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- list[dict]: 菜单级数据权限配置列表
|
||||||
|
"""
|
||||||
|
# 查询所有设置了菜单级 data_scope 的记录
|
||||||
|
result = await self.auth.db.execute(
|
||||||
|
select(RoleMenusModel.menu_id, RoleMenusModel.data_scope).where(
|
||||||
|
RoleMenusModel.role_id == role_id,
|
||||||
|
RoleMenusModel.data_scope.isnot(None),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
rows = result.all()
|
||||||
|
|
||||||
|
items = []
|
||||||
|
for row in rows:
|
||||||
|
dept_ids: list[int] = []
|
||||||
|
if row.data_scope == 5:
|
||||||
|
# 查询自定义部门
|
||||||
|
dept_result = await self.auth.db.execute(
|
||||||
|
select(RoleMenuDeptsModel.dept_id).where(
|
||||||
|
RoleMenuDeptsModel.role_id == role_id,
|
||||||
|
RoleMenuDeptsModel.menu_id == row.menu_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
dept_ids = [r.dept_id for r in dept_result.all()]
|
||||||
|
|
||||||
|
items.append(
|
||||||
|
{"menu_id": row.menu_id, "data_scope": row.data_scope, "dept_ids": dept_ids}
|
||||||
|
)
|
||||||
|
|
||||||
|
return items
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
import json
|
||||||
|
from collections.abc import AsyncGenerator
|
||||||
|
|
||||||
|
from fastapi import Depends, Query, Request
|
||||||
|
from redis.asyncio.client import Redis
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||||
|
from app.api.v1.module_system.user.crud import UserCRUD
|
||||||
|
from app.api.v1.module_system.user.model import UserModel
|
||||||
|
from app.common.enums import RedisInitKeyConfig
|
||||||
|
from app.config.setting import settings
|
||||||
|
from app.core.database import async_db_session
|
||||||
|
from app.core.exceptions import CustomException
|
||||||
|
from app.core.logger import log
|
||||||
|
from app.core.redis_crud import RedisCURD
|
||||||
|
from app.core.security import OAuth2Schema, decode_access_token
|
||||||
|
|
||||||
|
|
||||||
|
async def db_getter() -> AsyncGenerator[AsyncSession, None]:
|
||||||
|
"""获取数据库会话连接
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- AsyncSession: 数据库会话连接
|
||||||
|
"""
|
||||||
|
async with async_db_session() as session:
|
||||||
|
async with session.begin():
|
||||||
|
yield session
|
||||||
|
|
||||||
|
|
||||||
|
async def redis_getter(request: Request) -> Redis:
|
||||||
|
"""获取Redis连接
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- request (Request): 请求对象
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Redis: Redis连接
|
||||||
|
"""
|
||||||
|
return request.app.state.redis
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_user(
|
||||||
|
request: Request,
|
||||||
|
db: AsyncSession = Depends(db_getter),
|
||||||
|
redis: Redis = Depends(redis_getter),
|
||||||
|
token: str = Depends(OAuth2Schema),
|
||||||
|
) -> AuthSchema:
|
||||||
|
"""获取当前用户
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- request (Request): 请求对象
|
||||||
|
- db (AsyncSession): 数据库会话
|
||||||
|
- redis (Redis): Redis连接
|
||||||
|
- token (str): 访问令牌
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- AuthSchema: 认证信息模型
|
||||||
|
"""
|
||||||
|
if not token:
|
||||||
|
raise CustomException(msg="认证已失效", code=10401, status_code=401)
|
||||||
|
|
||||||
|
# 处理Bearer token
|
||||||
|
if token.startswith("Bearer"):
|
||||||
|
token = token.split(" ")[1]
|
||||||
|
|
||||||
|
payload = decode_access_token(token)
|
||||||
|
if not payload or not hasattr(payload, "is_refresh") or payload.is_refresh:
|
||||||
|
raise CustomException(msg="非法凭证", code=10401, status_code=401)
|
||||||
|
|
||||||
|
online_user_info = payload.sub
|
||||||
|
# 从Redis中获取用户信息
|
||||||
|
user_info = json.loads(online_user_info) # 确保是字典类型
|
||||||
|
|
||||||
|
session_id = user_info.get("session_id")
|
||||||
|
if not session_id:
|
||||||
|
raise CustomException(msg="认证已失效", code=10401, status_code=401)
|
||||||
|
|
||||||
|
# 检查用户是否在线
|
||||||
|
online_ok = await RedisCURD(redis).exists(
|
||||||
|
key=f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}"
|
||||||
|
)
|
||||||
|
if not online_ok:
|
||||||
|
raise CustomException(msg="认证已失效", code=10401, status_code=401)
|
||||||
|
|
||||||
|
# 如果启用了滑动过期,自动续期token
|
||||||
|
if settings.TOKEN_SLIDING_EXPIRE:
|
||||||
|
await RedisCURD(redis).expire(
|
||||||
|
key=f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}",
|
||||||
|
expire=settings.ACCESS_TOKEN_EXPIRE_MINUTES,
|
||||||
|
)
|
||||||
|
await RedisCURD(redis).expire(
|
||||||
|
key=f"{RedisInitKeyConfig.REFRESH_TOKEN.key}:{session_id}",
|
||||||
|
expire=settings.REFRESH_TOKEN_EXPIRE_MINUTES,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 关闭数据权限过滤,避免当前用户查询被拦截
|
||||||
|
auth = AuthSchema(db=db, check_data_scope=False)
|
||||||
|
username = user_info.get("user_name")
|
||||||
|
if not username:
|
||||||
|
raise CustomException(msg="认证已失效", code=10401, status_code=401)
|
||||||
|
# 获取用户信息,使用深层预加载确保RoleModel.creator被正确加载
|
||||||
|
user = await UserCRUD(auth).get_by_username_crud(
|
||||||
|
username=username,
|
||||||
|
preload=[
|
||||||
|
"dept",
|
||||||
|
selectinload(UserModel.roles),
|
||||||
|
"positions",
|
||||||
|
"created_by",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if not user:
|
||||||
|
raise CustomException(msg="用户不存在", code=10401, status_code=401)
|
||||||
|
if user.status == "1":
|
||||||
|
raise CustomException(msg="用户已被停用", code=10401, status_code=401)
|
||||||
|
|
||||||
|
# 设置请求上下文
|
||||||
|
request.scope["user_id"] = user.id
|
||||||
|
request.scope["user_username"] = user.username
|
||||||
|
|
||||||
|
# 过滤可用的角色和职位
|
||||||
|
if hasattr(user, "roles"):
|
||||||
|
user.roles = [role for role in user.roles if role and role.status]
|
||||||
|
if hasattr(user, "positions"):
|
||||||
|
user.positions = [pos for pos in user.positions if pos and pos.status]
|
||||||
|
|
||||||
|
auth.user = user
|
||||||
|
return auth
|
||||||
|
|
||||||
|
|
||||||
|
async def get_current_user_ws(
|
||||||
|
token: str = Query(..., description="认证token"),
|
||||||
|
db: AsyncSession = Depends(db_getter),
|
||||||
|
redis: Redis = Depends(redis_getter),
|
||||||
|
) -> AuthSchema:
|
||||||
|
"""获取当前用户(WebSocket专用,从查询参数获取token)
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- token (str): 认证token
|
||||||
|
- db (AsyncSession): 数据库会话
|
||||||
|
- redis (Redis): Redis连接
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- AuthSchema: 认证信息模型
|
||||||
|
"""
|
||||||
|
return await _verify_token(token, db, redis)
|
||||||
|
|
||||||
|
|
||||||
|
async def _verify_token(
|
||||||
|
token: str,
|
||||||
|
db: AsyncSession,
|
||||||
|
redis: Redis,
|
||||||
|
) -> AuthSchema:
|
||||||
|
"""验证token并返回用户信息
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- token (str): 认证token
|
||||||
|
- db (AsyncSession): 数据库会话
|
||||||
|
- redis (Redis): Redis连接
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- AuthSchema: 认证信息模型
|
||||||
|
"""
|
||||||
|
if not token:
|
||||||
|
raise CustomException(msg="认证已失效", code=10401, status_code=401)
|
||||||
|
|
||||||
|
# 处理Bearer token(如果通过查询参数传递时包含Bearer前缀)
|
||||||
|
if token.startswith("Bearer"):
|
||||||
|
token = token.split(" ")[1]
|
||||||
|
|
||||||
|
payload = decode_access_token(token)
|
||||||
|
if not payload or not hasattr(payload, "is_refresh") or payload.is_refresh:
|
||||||
|
raise CustomException(msg="非法凭证", code=10401, status_code=401)
|
||||||
|
|
||||||
|
online_user_info = payload.sub
|
||||||
|
# 从Redis中获取用户信息
|
||||||
|
user_info = json.loads(online_user_info) # 确保是字典类型
|
||||||
|
|
||||||
|
session_id = user_info.get("session_id")
|
||||||
|
if not session_id:
|
||||||
|
raise CustomException(msg="认证已失效", code=10401, status_code=401)
|
||||||
|
|
||||||
|
# 检查用户是否在线
|
||||||
|
online_ok = await RedisCURD(redis).exists(
|
||||||
|
key=f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}"
|
||||||
|
)
|
||||||
|
if not online_ok:
|
||||||
|
raise CustomException(msg="认证已失效", code=10401, status_code=401)
|
||||||
|
|
||||||
|
# 如果启用了滑动过期,自动续期token
|
||||||
|
if settings.TOKEN_SLIDING_EXPIRE:
|
||||||
|
await RedisCURD(redis).expire(
|
||||||
|
key=f"{RedisInitKeyConfig.ACCESS_TOKEN.key}:{session_id}",
|
||||||
|
expire=settings.ACCESS_TOKEN_EXPIRE_MINUTES,
|
||||||
|
)
|
||||||
|
await RedisCURD(redis).expire(
|
||||||
|
key=f"{RedisInitKeyConfig.REFRESH_TOKEN.key}:{session_id}",
|
||||||
|
expire=settings.REFRESH_TOKEN_EXPIRE_MINUTES,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 关闭数据权限过滤,避免当前用户查询被拦截
|
||||||
|
auth = AuthSchema(db=db, check_data_scope=False)
|
||||||
|
username = user_info.get("user_name")
|
||||||
|
if not username:
|
||||||
|
raise CustomException(msg="认证已失效", code=10401, status_code=401)
|
||||||
|
# 获取用户信息,使用深层预加载确保RoleModel.creator被正确加载
|
||||||
|
user = await UserCRUD(auth).get_by_username_crud(
|
||||||
|
username=username,
|
||||||
|
preload=[
|
||||||
|
"dept",
|
||||||
|
selectinload(UserModel.roles),
|
||||||
|
"positions",
|
||||||
|
"created_by",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if not user:
|
||||||
|
raise CustomException(msg="用户不存在", code=10401, status_code=401)
|
||||||
|
if user.status == "1":
|
||||||
|
raise CustomException(msg="用户已被停用", code=10401, status_code=401)
|
||||||
|
|
||||||
|
# 设置请求上下文
|
||||||
|
# request.scope["user_id"] = user.id
|
||||||
|
# request.scope["user_username"] = user.username
|
||||||
|
|
||||||
|
# 过滤可用的角色和职位
|
||||||
|
if hasattr(user, "roles"):
|
||||||
|
user.roles = [role for role in user.roles if role and role.status]
|
||||||
|
if hasattr(user, "positions"):
|
||||||
|
user.positions = [pos for pos in user.positions if pos and pos.status]
|
||||||
|
|
||||||
|
auth.user = user
|
||||||
|
return auth
|
||||||
|
|
||||||
|
|
||||||
|
class AuthPermission:
|
||||||
|
"""权限验证类"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
permissions: list[str] | None = None,
|
||||||
|
check_data_scope: bool = True,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
初始化权限验证
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- permissions (list[str] | None): 权限标识列表。
|
||||||
|
- check_data_scope (bool): 是否启用严格模式校验。
|
||||||
|
"""
|
||||||
|
self.permissions = permissions or []
|
||||||
|
self.check_data_scope = check_data_scope
|
||||||
|
|
||||||
|
async def __call__(self, auth: AuthSchema = Depends(get_current_user)) -> AuthSchema:
|
||||||
|
"""
|
||||||
|
调用权限验证
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息对象。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- AuthSchema: 认证信息对象。
|
||||||
|
"""
|
||||||
|
auth.check_data_scope = self.check_data_scope
|
||||||
|
|
||||||
|
# 超级管理员直接通过
|
||||||
|
if auth.user and auth.user.is_superuser:
|
||||||
|
return auth
|
||||||
|
|
||||||
|
# 无需验证权限
|
||||||
|
if not self.permissions:
|
||||||
|
return auth
|
||||||
|
|
||||||
|
# 超级管理员权限标识
|
||||||
|
if "*" in self.permissions or "*:*:*" in self.permissions:
|
||||||
|
return auth
|
||||||
|
|
||||||
|
# 检查用户是否有角色
|
||||||
|
if not auth.user or not auth.user.roles:
|
||||||
|
raise CustomException(msg="无权限操作", code=10403, status_code=403)
|
||||||
|
|
||||||
|
# 获取用户权限集合
|
||||||
|
user_permissions = {
|
||||||
|
menu.permission
|
||||||
|
for role in auth.user.roles
|
||||||
|
for menu in role.menus
|
||||||
|
if role.status == "0" and menu.permission and menu.status == "0"
|
||||||
|
}
|
||||||
|
|
||||||
|
# 权限验证 - 满足任一权限即可
|
||||||
|
if not any(perm in user_permissions for perm in self.permissions):
|
||||||
|
log.error(f"用户缺少任何所需的权限: {self.permissions}")
|
||||||
|
raise CustomException(msg="无权限操作", code=10403, status_code=403)
|
||||||
|
|
||||||
|
# 记录匹配到的权限标识,供数据权限引擎使用
|
||||||
|
for perm in self.permissions:
|
||||||
|
if perm in user_permissions:
|
||||||
|
auth.current_permission = perm
|
||||||
|
break
|
||||||
|
|
||||||
|
return auth
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from sqlalchemy import ForeignKey, Integer, String
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||||
|
|
||||||
|
from app.common.enums import PermissionFilterStrategy
|
||||||
|
from app.core.base_model import MappedBase, ModelMixin
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from app.api.v1.module_system.dept.model import DeptModel
|
||||||
|
from app.api.v1.module_system.menu.model import MenuModel
|
||||||
|
from app.api.v1.module_system.user.model import UserModel
|
||||||
|
|
||||||
|
|
||||||
|
class RoleMenusModel(MappedBase):
|
||||||
|
"""
|
||||||
|
角色菜单关联表
|
||||||
|
|
||||||
|
定义角色与菜单的多对多关系,用于权限控制
|
||||||
|
支持菜单级数据权限覆盖:data_scope 为 NULL 时继承角色级 data_scope
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__: str = "sys_role_menus"
|
||||||
|
__table_args__: dict[str, str] = {"comment": "角色菜单关联表"}
|
||||||
|
|
||||||
|
role_id: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("sys_role.id", ondelete="CASCADE", onupdate="CASCADE"),
|
||||||
|
primary_key=True,
|
||||||
|
comment="角色ID",
|
||||||
|
)
|
||||||
|
menu_id: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("sys_menu.id", ondelete="CASCADE", onupdate="CASCADE"),
|
||||||
|
primary_key=True,
|
||||||
|
comment="菜单ID",
|
||||||
|
)
|
||||||
|
data_scope: Mapped[int | None] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
nullable=True,
|
||||||
|
default=None,
|
||||||
|
comment="菜单级数据权限范围(NULL则继承角色级, 1:仅本人 2:本部门 3:本部门及以下 4:全部 5:自定义)",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleDeptsModel(MappedBase):
|
||||||
|
"""
|
||||||
|
角色部门关联表
|
||||||
|
|
||||||
|
定义角色与部门的多对多关系,用于数据权限控制
|
||||||
|
仅当角色的data_scope=5(自定义数据权限)时使用此表
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__: str = "sys_role_depts"
|
||||||
|
__table_args__: dict[str, str] = {"comment": "角色部门关联表"}
|
||||||
|
|
||||||
|
role_id: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("sys_role.id", ondelete="CASCADE", onupdate="CASCADE"),
|
||||||
|
primary_key=True,
|
||||||
|
comment="角色ID",
|
||||||
|
)
|
||||||
|
dept_id: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("sys_dept.id", ondelete="CASCADE", onupdate="CASCADE"),
|
||||||
|
primary_key=True,
|
||||||
|
comment="部门ID",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleMenuDeptsModel(MappedBase):
|
||||||
|
"""
|
||||||
|
角色菜单部门关联表
|
||||||
|
|
||||||
|
定义角色菜单级别的自定义部门数据权限
|
||||||
|
仅当 sys_role_menus.data_scope=5(自定义数据权限)时使用此表
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__: str = "sys_role_menu_depts"
|
||||||
|
__table_args__: dict[str, str] = {"comment": "角色菜单部门关联表(菜单级自定义数据权限)"}
|
||||||
|
|
||||||
|
role_id: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("sys_role.id", ondelete="CASCADE", onupdate="CASCADE"),
|
||||||
|
primary_key=True,
|
||||||
|
comment="角色ID",
|
||||||
|
)
|
||||||
|
menu_id: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("sys_menu.id", ondelete="CASCADE", onupdate="CASCADE"),
|
||||||
|
primary_key=True,
|
||||||
|
comment="菜单ID",
|
||||||
|
)
|
||||||
|
dept_id: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
ForeignKey("sys_dept.id", ondelete="CASCADE", onupdate="CASCADE"),
|
||||||
|
primary_key=True,
|
||||||
|
comment="部门ID",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleModel(ModelMixin):
|
||||||
|
"""
|
||||||
|
角色模型
|
||||||
|
|
||||||
|
角色列表只显示当前用户绑定的角色
|
||||||
|
"""
|
||||||
|
|
||||||
|
__tablename__: str = "sys_role"
|
||||||
|
__table_args__: dict[str, str] = {"comment": "角色表"}
|
||||||
|
__loader_options__: list[str] = ["menus", "depts"]
|
||||||
|
__permission_strategy__: PermissionFilterStrategy = PermissionFilterStrategy.USER_ROLE
|
||||||
|
|
||||||
|
name: Mapped[str] = mapped_column(String(64), nullable=False, comment="角色名称")
|
||||||
|
code: Mapped[str | None] = mapped_column(
|
||||||
|
String(16), nullable=True, index=True, comment="角色编码"
|
||||||
|
)
|
||||||
|
order: Mapped[int] = mapped_column(Integer, nullable=False, default=999, comment="显示排序")
|
||||||
|
data_scope: Mapped[int] = mapped_column(
|
||||||
|
Integer,
|
||||||
|
default=1,
|
||||||
|
nullable=False,
|
||||||
|
comment="数据权限范围(1:仅本人 2:本部门 3:本部门及以下 4:全部 5:自定义)",
|
||||||
|
)
|
||||||
|
|
||||||
|
menus: Mapped[list["MenuModel"]] = relationship(
|
||||||
|
secondary="sys_role_menus",
|
||||||
|
back_populates="roles",
|
||||||
|
lazy="selectin",
|
||||||
|
order_by="MenuModel.order",
|
||||||
|
)
|
||||||
|
depts: Mapped[list["DeptModel"]] = relationship(
|
||||||
|
secondary="sys_role_depts", back_populates="roles", lazy="selectin"
|
||||||
|
)
|
||||||
|
users: Mapped[list["UserModel"]] = relationship(
|
||||||
|
secondary="sys_user_roles", back_populates="roles", lazy="selectin"
|
||||||
|
)
|
||||||
@@ -0,0 +1,428 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from sqlalchemy import and_, or_, select
|
||||||
|
from sqlalchemy.sql.elements import ColumnElement
|
||||||
|
|
||||||
|
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||||
|
from app.api.v1.module_system.dept.model import DeptModel
|
||||||
|
from app.api.v1.module_system.user.model import UserModel
|
||||||
|
from app.common.enums import PermissionFilterStrategy
|
||||||
|
from app.utils.common_util import get_child_id_map, get_child_recursion
|
||||||
|
|
||||||
|
|
||||||
|
class Permission:
|
||||||
|
"""
|
||||||
|
为业务模型提供数据权限过滤功能
|
||||||
|
|
||||||
|
使用策略模式,根据模型的 __permission_strategy__ 属性选择合适的过滤策略
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 数据权限常量定义,提高代码可读性
|
||||||
|
DATA_SCOPE_SELF = 1 # 仅本人数据
|
||||||
|
DATA_SCOPE_DEPT = 2 # 本部门数据
|
||||||
|
DATA_SCOPE_DEPT_AND_CHILD = 3 # 本部门及以下数据
|
||||||
|
DATA_SCOPE_ALL = 4 # 全部数据
|
||||||
|
DATA_SCOPE_CUSTOM = 5 # 自定义数据
|
||||||
|
|
||||||
|
def __init__(self, model: Any, auth: AuthSchema) -> None:
|
||||||
|
"""
|
||||||
|
初始化权限过滤器实例
|
||||||
|
|
||||||
|
Args:
|
||||||
|
db: 数据库会话
|
||||||
|
model: 数据模型类
|
||||||
|
current_user: 当前用户对象
|
||||||
|
auth: 认证信息对象
|
||||||
|
"""
|
||||||
|
self.model = model
|
||||||
|
self.auth = auth
|
||||||
|
self.conditions: list[ColumnElement] = [] # 权限条件列表
|
||||||
|
|
||||||
|
async def filter_query(self, query: Any) -> Any:
|
||||||
|
"""
|
||||||
|
异步过滤查询对象
|
||||||
|
|
||||||
|
Args:
|
||||||
|
query: SQLAlchemy查询对象
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
过滤后的查询对象
|
||||||
|
"""
|
||||||
|
condition = await self.__permission_condition()
|
||||||
|
return query.where(condition) if condition is not None else query
|
||||||
|
|
||||||
|
async def __permission_condition(self) -> ColumnElement | None:
|
||||||
|
"""
|
||||||
|
应用数据范围权限隔离
|
||||||
|
|
||||||
|
根据模型的权限过滤策略,选择合适的过滤方法
|
||||||
|
"""
|
||||||
|
# 如果不需要检查数据权限,则不限制
|
||||||
|
if not self.auth.user:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 如果检查数据权限为False,则不限制
|
||||||
|
if not self.auth.check_data_scope:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 超级管理员可以查看所有数据
|
||||||
|
if self.auth.user.is_superuser:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 获取模型的权限过滤策略
|
||||||
|
strategy = getattr(self.model, "__permission_strategy__", PermissionFilterStrategy.DATA_SCOPE)
|
||||||
|
|
||||||
|
# 根据策略选择过滤方法
|
||||||
|
if strategy == PermissionFilterStrategy.ROLE_BASED:
|
||||||
|
return await self.__filter_by_role_based()
|
||||||
|
elif strategy == PermissionFilterStrategy.DEPT_BASED:
|
||||||
|
return await self.__filter_by_dept_based()
|
||||||
|
elif strategy == PermissionFilterStrategy.SELF_ONLY:
|
||||||
|
return await self.__filter_by_self_only()
|
||||||
|
elif strategy == PermissionFilterStrategy.USER_ROLE:
|
||||||
|
return await self.__filter_by_user_role()
|
||||||
|
else:
|
||||||
|
return await self.__filter_by_data_scope()
|
||||||
|
|
||||||
|
async def __filter_by_role_based(self) -> ColumnElement | None:
|
||||||
|
"""
|
||||||
|
基于角色授权的权限过滤(适用于菜单等)
|
||||||
|
|
||||||
|
只显示用户角色授权的菜单
|
||||||
|
"""
|
||||||
|
roles = getattr(self.auth.user, "roles", []) or []
|
||||||
|
if not roles:
|
||||||
|
id_attr = getattr(self.model, "id", None)
|
||||||
|
if id_attr is not None:
|
||||||
|
return id_attr == -1
|
||||||
|
return None
|
||||||
|
|
||||||
|
menu_ids = set()
|
||||||
|
for role in roles:
|
||||||
|
if hasattr(role, "menus") and role.menus:
|
||||||
|
menu_ids.update(menu.id for menu in role.menus if menu.status == "0")
|
||||||
|
|
||||||
|
if menu_ids:
|
||||||
|
id_attr = getattr(self.model, "id", None)
|
||||||
|
if id_attr is not None:
|
||||||
|
return id_attr.in_(list(menu_ids))
|
||||||
|
|
||||||
|
id_attr = getattr(self.model, "id", None)
|
||||||
|
if id_attr is not None:
|
||||||
|
return id_attr == -1
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def __filter_by_user_role(self) -> ColumnElement | None:
|
||||||
|
"""
|
||||||
|
基于当前用户绑定角色的权限过滤(适用于角色列表)
|
||||||
|
|
||||||
|
只显示当前用户绑定的角色
|
||||||
|
"""
|
||||||
|
roles = getattr(self.auth.user, "roles", []) or []
|
||||||
|
if not roles:
|
||||||
|
id_attr = getattr(self.model, "id", None)
|
||||||
|
if id_attr is not None:
|
||||||
|
return id_attr == -1
|
||||||
|
return None
|
||||||
|
|
||||||
|
role_ids = [role.id for role in roles]
|
||||||
|
id_attr = getattr(self.model, "id", None)
|
||||||
|
if id_attr is not None:
|
||||||
|
return id_attr.in_(role_ids)
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def __filter_by_dept_based(self) -> ColumnElement | None:
|
||||||
|
"""
|
||||||
|
基于部门关联的权限过滤(适用于部门、角色等)
|
||||||
|
|
||||||
|
根据用户的部门权限范围过滤数据
|
||||||
|
"""
|
||||||
|
# 如果用户没有角色,则只能查看自己部门的数据
|
||||||
|
roles = getattr(self.auth.user, "roles", []) or []
|
||||||
|
if not roles:
|
||||||
|
user_dept_id = getattr(self.auth.user, "dept_id", None)
|
||||||
|
if user_dept_id is not None and hasattr(self.model, "id"):
|
||||||
|
id_attr = getattr(self.model, "id", None)
|
||||||
|
if id_attr is not None:
|
||||||
|
return id_attr == user_dept_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 获取用户所有角色的权限范围(支持菜单/按钮级覆盖)
|
||||||
|
data_scopes, custom_dept_ids = await self.__resolve_menu_data_scope(
|
||||||
|
roles, self.auth.current_permission
|
||||||
|
)
|
||||||
|
|
||||||
|
# 全部数据权限最高优先级
|
||||||
|
if self.DATA_SCOPE_ALL in data_scopes:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 收集所有可访问的部门ID
|
||||||
|
accessible_dept_ids = await self.__get_accessible_dept_ids(data_scopes, custom_dept_ids)
|
||||||
|
|
||||||
|
# 根据模型类型过滤
|
||||||
|
if self.model.__name__ == "DeptModel":
|
||||||
|
return self.__filter_dept_model(accessible_dept_ids)
|
||||||
|
elif self.model.__name__ == "UserModel":
|
||||||
|
return self.__filter_user_model(accessible_dept_ids)
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def __filter_by_self_only(self) -> ColumnElement | None:
|
||||||
|
"""
|
||||||
|
仅本人数据权限过滤
|
||||||
|
"""
|
||||||
|
created_id_attr = getattr(self.model, "created_id", None)
|
||||||
|
if created_id_attr is not None and self.auth.user:
|
||||||
|
return created_id_attr == self.auth.user.id
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def __filter_by_data_scope(self) -> ColumnElement | None:
|
||||||
|
"""
|
||||||
|
基于数据范围权限的通用过滤(默认策略)
|
||||||
|
|
||||||
|
适用于大多数业务模型
|
||||||
|
"""
|
||||||
|
# 如果模型没有创建人created_id字段,则不限制
|
||||||
|
if not hasattr(self.model, "created_id"):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 如果用户没有角色,则只能查看自己的数据
|
||||||
|
roles = getattr(self.auth.user, "roles", []) or []
|
||||||
|
if not roles:
|
||||||
|
created_id_attr = getattr(self.model, "created_id", None)
|
||||||
|
if created_id_attr is not None and self.auth.user:
|
||||||
|
return created_id_attr == self.auth.user.id
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 获取用户所有角色的权限范围(支持菜单/按钮级覆盖)
|
||||||
|
data_scopes, custom_dept_ids = await self.__resolve_menu_data_scope(
|
||||||
|
roles, self.auth.current_permission
|
||||||
|
)
|
||||||
|
|
||||||
|
# 全部数据权限最高优先级
|
||||||
|
if self.DATA_SCOPE_ALL in data_scopes:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 收集所有可访问的部门ID
|
||||||
|
accessible_dept_ids = await self.__get_accessible_dept_ids(data_scopes, custom_dept_ids)
|
||||||
|
|
||||||
|
# 如果有部门权限,使用部门过滤
|
||||||
|
if accessible_dept_ids:
|
||||||
|
# 特殊处理:如果模型本身就是UserModel,直接过滤用户的dept_id
|
||||||
|
if self.model.__name__ == "UserModel" and hasattr(self.model, "dept_id"):
|
||||||
|
dept_id_attr = getattr(self.model, "dept_id", None)
|
||||||
|
if dept_id_attr is not None:
|
||||||
|
return dept_id_attr.in_(list(accessible_dept_ids))
|
||||||
|
|
||||||
|
# 其他模型:通过created_by关系过滤创建人的部门
|
||||||
|
creator_rel = getattr(self.model, "created_by", None)
|
||||||
|
if creator_rel is not None and hasattr(UserModel, "dept_id"):
|
||||||
|
return creator_rel.has(UserModel.dept_id.in_(list(accessible_dept_ids)))
|
||||||
|
|
||||||
|
# 降级方案:只能查看自己的数据
|
||||||
|
created_id_attr = getattr(self.model, "created_id", None)
|
||||||
|
if created_id_attr is not None and self.auth.user:
|
||||||
|
return created_id_attr == self.auth.user.id
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 处理仅本人数据权限
|
||||||
|
if self.DATA_SCOPE_SELF in data_scopes:
|
||||||
|
created_id_attr = getattr(self.model, "created_id", None)
|
||||||
|
if created_id_attr is not None and self.auth.user:
|
||||||
|
return created_id_attr == self.auth.user.id
|
||||||
|
return None
|
||||||
|
|
||||||
|
# 默认情况:只能查看自己的数据
|
||||||
|
created_id_attr = getattr(self.model, "created_id", None)
|
||||||
|
if created_id_attr is not None and self.auth.user:
|
||||||
|
return created_id_attr == self.auth.user.id
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def __resolve_menu_data_scope(
|
||||||
|
self, roles: list, current_permission: str | None
|
||||||
|
) -> tuple[set[int], set[int]]:
|
||||||
|
"""
|
||||||
|
解析数据权限范围,支持菜单/按钮级覆盖
|
||||||
|
|
||||||
|
三级继承链路:按钮自身 data_scope → 父菜单 data_scope → 角色 data_scope
|
||||||
|
多角色场景采用并集策略(权限最大化)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
roles: 用户的角色列表
|
||||||
|
current_permission: 当前请求的权限标识(如 "module_system:user:query")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(data_scopes, custom_dept_ids) 元组
|
||||||
|
"""
|
||||||
|
data_scopes: set[int] = set()
|
||||||
|
custom_dept_ids: set[int] = set()
|
||||||
|
|
||||||
|
if not current_permission:
|
||||||
|
# 回退:使用角色级 data_scope(现有行为)
|
||||||
|
for role in roles:
|
||||||
|
data_scopes.add(role.data_scope)
|
||||||
|
if role.data_scope == self.DATA_SCOPE_CUSTOM and hasattr(role, "depts") and role.depts:
|
||||||
|
custom_dept_ids.update(dept.id for dept in role.depts)
|
||||||
|
return data_scopes, custom_dept_ids
|
||||||
|
|
||||||
|
# 在所有角色中查找匹配 current_permission 的菜单,同时记录菜单对象以获取 type 和 parent_id
|
||||||
|
role_menu_pairs: list[tuple[int, int, Any, Any]] = [] # (role_id, menu_id, role, menu)
|
||||||
|
for role in roles:
|
||||||
|
for menu in (getattr(role, "menus", None) or []):
|
||||||
|
if (
|
||||||
|
getattr(menu, "permission", None) == current_permission
|
||||||
|
and getattr(menu, "status", None) == "0"
|
||||||
|
):
|
||||||
|
role_menu_pairs.append((role.id, menu.id, role, menu))
|
||||||
|
break # 每个角色只取一个匹配
|
||||||
|
|
||||||
|
if not role_menu_pairs:
|
||||||
|
# 没有角色包含此权限——回退到角色级
|
||||||
|
for role in roles:
|
||||||
|
data_scopes.add(role.data_scope)
|
||||||
|
if role.data_scope == self.DATA_SCOPE_CUSTOM and hasattr(role, "depts") and role.depts:
|
||||||
|
custom_dept_ids.update(dept.id for dept in role.depts)
|
||||||
|
return data_scopes, custom_dept_ids
|
||||||
|
|
||||||
|
# 批量查询 RoleMenusModel 中的菜单级 data_scope
|
||||||
|
from app.api.v1.module_system.role.model import RoleMenuDeptsModel, RoleMenusModel
|
||||||
|
|
||||||
|
pairs_filter = or_(
|
||||||
|
*[
|
||||||
|
and_(RoleMenusModel.role_id == rid, RoleMenusModel.menu_id == mid)
|
||||||
|
for rid, mid, _, _ in role_menu_pairs
|
||||||
|
]
|
||||||
|
)
|
||||||
|
result = await self.auth.db.execute(
|
||||||
|
select(RoleMenusModel.role_id, RoleMenusModel.menu_id, RoleMenusModel.data_scope).where(
|
||||||
|
pairs_filter
|
||||||
|
)
|
||||||
|
)
|
||||||
|
per_menu_scopes = {(row.role_id, row.menu_id): row.data_scope for row in result}
|
||||||
|
|
||||||
|
# 对 type=3 且自身 data_scope 为 NULL 的按钮,收集需要查询父菜单 data_scope 的对
|
||||||
|
parent_lookups: list[tuple[int, int]] = [] # (role_id, parent_menu_id)
|
||||||
|
for role_id, menu_id, _, menu in role_menu_pairs:
|
||||||
|
scope = per_menu_scopes.get((role_id, menu_id))
|
||||||
|
parent_id = getattr(menu, "parent_id", None)
|
||||||
|
if scope is None and getattr(menu, "type", None) == 3 and parent_id:
|
||||||
|
parent_lookups.append((role_id, parent_id))
|
||||||
|
|
||||||
|
# 批量查询父菜单 data_scope
|
||||||
|
parent_scopes: dict[tuple[int, int], int | None] = {}
|
||||||
|
if parent_lookups:
|
||||||
|
parent_filter = or_(
|
||||||
|
*[
|
||||||
|
and_(RoleMenusModel.role_id == rid, RoleMenusModel.menu_id == pid)
|
||||||
|
for rid, pid in parent_lookups
|
||||||
|
]
|
||||||
|
)
|
||||||
|
parent_result = await self.auth.db.execute(
|
||||||
|
select(
|
||||||
|
RoleMenusModel.role_id, RoleMenusModel.menu_id, RoleMenusModel.data_scope
|
||||||
|
).where(parent_filter)
|
||||||
|
)
|
||||||
|
parent_scopes = {(row.role_id, row.menu_id): row.data_scope for row in parent_result}
|
||||||
|
|
||||||
|
# 解析每个角色在该权限下的实际 data_scope(三级继承)
|
||||||
|
for role_id, menu_id, role, menu in role_menu_pairs:
|
||||||
|
scope = per_menu_scopes.get((role_id, menu_id))
|
||||||
|
# 记录 scope 实际来源的 menu_id(用于查找自定义部门)
|
||||||
|
scope_menu_id: int | None = menu_id
|
||||||
|
|
||||||
|
# 三级继承:按钮自身 → 父菜单 → 角色
|
||||||
|
if scope is None and getattr(menu, "type", None) == 3 and getattr(menu, "parent_id", None):
|
||||||
|
parent_scope = parent_scopes.get((role_id, menu.parent_id))
|
||||||
|
if parent_scope is not None:
|
||||||
|
scope = parent_scope
|
||||||
|
scope_menu_id = menu.parent_id # scope 来源于父菜单
|
||||||
|
|
||||||
|
if scope is None:
|
||||||
|
scope = role.data_scope # 最终回退到角色级
|
||||||
|
scope_menu_id = None # scope 来源于角色级
|
||||||
|
|
||||||
|
data_scopes.add(scope)
|
||||||
|
|
||||||
|
if scope == self.DATA_SCOPE_CUSTOM:
|
||||||
|
if scope_menu_id is not None:
|
||||||
|
# 从 scope 来源的菜单级查找自定义部门
|
||||||
|
dept_result = await self.auth.db.execute(
|
||||||
|
select(RoleMenuDeptsModel.dept_id).where(
|
||||||
|
RoleMenuDeptsModel.role_id == role_id,
|
||||||
|
RoleMenuDeptsModel.menu_id == scope_menu_id,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
menu_dept_ids = {row.dept_id for row in dept_result}
|
||||||
|
if menu_dept_ids:
|
||||||
|
custom_dept_ids.update(menu_dept_ids)
|
||||||
|
elif hasattr(role, "depts") and role.depts:
|
||||||
|
# 菜单级没有配置部门,回退到角色级自定义部门
|
||||||
|
custom_dept_ids.update(dept.id for dept in role.depts)
|
||||||
|
elif hasattr(role, "depts") and role.depts:
|
||||||
|
# scope 来源于角色级,直接使用角色级自定义部门
|
||||||
|
custom_dept_ids.update(dept.id for dept in role.depts)
|
||||||
|
|
||||||
|
return data_scopes, custom_dept_ids
|
||||||
|
|
||||||
|
async def __get_accessible_dept_ids(
|
||||||
|
self, data_scopes: set, custom_dept_ids: set
|
||||||
|
) -> set[int]:
|
||||||
|
"""
|
||||||
|
获取用户可访问的所有部门ID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data_scopes: 用户角色的数据权限范围集合
|
||||||
|
custom_dept_ids: 自定义权限关联的部门ID集合
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
可访问的部门ID集合
|
||||||
|
"""
|
||||||
|
accessible_dept_ids = set()
|
||||||
|
user_dept_id = getattr(self.auth.user, "dept_id", None)
|
||||||
|
|
||||||
|
# 处理自定义数据权限(5)
|
||||||
|
if self.DATA_SCOPE_CUSTOM in data_scopes:
|
||||||
|
accessible_dept_ids.update(custom_dept_ids)
|
||||||
|
|
||||||
|
# 处理本部门数据权限(2)
|
||||||
|
if self.DATA_SCOPE_DEPT in data_scopes and user_dept_id is not None:
|
||||||
|
accessible_dept_ids.add(user_dept_id)
|
||||||
|
|
||||||
|
# 处理本部门及以下数据权限(3)
|
||||||
|
if self.DATA_SCOPE_DEPT_AND_CHILD in data_scopes and 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)
|
||||||
|
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)
|
||||||
|
|
||||||
|
return accessible_dept_ids
|
||||||
|
|
||||||
|
def __filter_dept_model(self, accessible_dept_ids: set[int]) -> ColumnElement | None:
|
||||||
|
"""
|
||||||
|
过滤部门模型
|
||||||
|
"""
|
||||||
|
if accessible_dept_ids:
|
||||||
|
id_attr = getattr(self.model, "id", None)
|
||||||
|
if id_attr is not None:
|
||||||
|
return id_attr.in_(list(accessible_dept_ids))
|
||||||
|
user_dept_id = getattr(self.auth.user, "dept_id", None)
|
||||||
|
if user_dept_id is not None:
|
||||||
|
id_attr = getattr(self.model, "id", None)
|
||||||
|
if id_attr is not None:
|
||||||
|
return id_attr == user_dept_id
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __filter_user_model(self, accessible_dept_ids: set[int]) -> ColumnElement | None:
|
||||||
|
"""
|
||||||
|
过滤用户模型
|
||||||
|
"""
|
||||||
|
if accessible_dept_ids:
|
||||||
|
dept_id_attr = getattr(self.model, "dept_id", None)
|
||||||
|
if dept_id_attr is not None:
|
||||||
|
return dept_id_attr.in_(list(accessible_dept_ids))
|
||||||
|
return None
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import request from "@/utils/request";
|
||||||
|
|
||||||
|
const API_PATH = "/system/role";
|
||||||
|
|
||||||
|
const RoleAPI = {
|
||||||
|
listRole(query?: TablePageQuery) {
|
||||||
|
return request<ApiResponse<PageResult<RoleTable[]>>>({
|
||||||
|
url: `${API_PATH}/list`,
|
||||||
|
method: "get",
|
||||||
|
params: query,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
detailRole(query: number) {
|
||||||
|
return request<ApiResponse<RoleTable>>({
|
||||||
|
url: `${API_PATH}/detail/${query}`,
|
||||||
|
method: "get",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
createRole(body: RoleForm) {
|
||||||
|
return request<ApiResponse>({
|
||||||
|
url: `${API_PATH}/create`,
|
||||||
|
method: "post",
|
||||||
|
data: body,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
updateRole(id: number, body: RoleForm) {
|
||||||
|
return request<ApiResponse>({
|
||||||
|
url: `${API_PATH}/update/${id}`,
|
||||||
|
method: "put",
|
||||||
|
data: body,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
deleteRole(body: number[]) {
|
||||||
|
return request<ApiResponse>({
|
||||||
|
url: `${API_PATH}/delete`,
|
||||||
|
method: "delete",
|
||||||
|
data: body,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
batchRole(body: BatchType) {
|
||||||
|
return request<ApiResponse>({
|
||||||
|
url: `${API_PATH}/available/setting`,
|
||||||
|
method: "patch",
|
||||||
|
data: body,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setPermission(body: permissionDataType) {
|
||||||
|
return request<ApiResponse>({
|
||||||
|
url: `${API_PATH}/permission/setting`,
|
||||||
|
method: "patch",
|
||||||
|
data: body,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
exportRole(body: TablePageQuery) {
|
||||||
|
return request<Blob>({
|
||||||
|
url: `${API_PATH}/export`,
|
||||||
|
method: "post",
|
||||||
|
data: body,
|
||||||
|
responseType: "blob",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default RoleAPI;
|
||||||
|
|
||||||
|
export interface TablePageQuery extends PageQuery {
|
||||||
|
name?: string;
|
||||||
|
status?: string;
|
||||||
|
created_time?: string[];
|
||||||
|
updated_time?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoleTable extends BaseType {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
order?: number;
|
||||||
|
code?: string;
|
||||||
|
data_scope?: number;
|
||||||
|
menus?: permissionMenuType[];
|
||||||
|
depts?: permissionDeptType[];
|
||||||
|
menu_data_scopes?: MenuDataScopeItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RoleForm extends BaseFormType {
|
||||||
|
name?: string;
|
||||||
|
order?: number;
|
||||||
|
code?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MenuDataScopeItem {
|
||||||
|
menu_id: number;
|
||||||
|
data_scope: number | null; // null = 继承角色默认
|
||||||
|
dept_ids: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface permissionDataType {
|
||||||
|
data_scope: number;
|
||||||
|
role_ids: RoleTable["id"][];
|
||||||
|
menu_ids: permissionMenuType["id"][];
|
||||||
|
dept_ids: permissionDeptType["id"][];
|
||||||
|
menu_data_scopes: MenuDataScopeItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface permissionDeptType {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
parent_id: number;
|
||||||
|
children: permissionDeptType[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface permissionMenuType {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
type: number;
|
||||||
|
permission: string;
|
||||||
|
parent_id?: number;
|
||||||
|
status: string;
|
||||||
|
description?: string;
|
||||||
|
children?: permissionMenuType[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
from fastapi import Query
|
||||||
|
from pydantic import (
|
||||||
|
BaseModel,
|
||||||
|
ConfigDict,
|
||||||
|
Field,
|
||||||
|
field_validator,
|
||||||
|
model_validator,
|
||||||
|
)
|
||||||
|
|
||||||
|
from app.api.v1.module_system.dept.schema import DeptOutSchema
|
||||||
|
from app.api.v1.module_system.menu.schema import MenuOutSchema
|
||||||
|
from app.common.enums import QueueEnum
|
||||||
|
from app.core.base_schema import BaseSchema
|
||||||
|
from app.core.validator import (
|
||||||
|
DateTimeStr,
|
||||||
|
code_validator,
|
||||||
|
role_permission_request_validator,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleCreateSchema(BaseModel):
|
||||||
|
"""角色创建模型"""
|
||||||
|
|
||||||
|
name: str = Field(..., max_length=64, description="角色名称")
|
||||||
|
code: str | None = Field(default=None, max_length=16, description="角色编码")
|
||||||
|
order: int | None = Field(default=1, ge=1, description="显示排序")
|
||||||
|
data_scope: int | None = Field(
|
||||||
|
default=1,
|
||||||
|
description="数据权限范围(1:仅本人 2:本部门 3:本部门及以下 4:全部 5:自定义)",
|
||||||
|
)
|
||||||
|
status: str = Field(default="0", description="是否启用")
|
||||||
|
description: str | None = Field(default=None, max_length=255, description="描述")
|
||||||
|
|
||||||
|
@field_validator("code")
|
||||||
|
@classmethod
|
||||||
|
def validate_code(cls, value: str | None):
|
||||||
|
return code_validator(value)
|
||||||
|
|
||||||
|
|
||||||
|
class MenuDataScopeItem(BaseModel):
|
||||||
|
"""菜单级数据权限配置项"""
|
||||||
|
|
||||||
|
menu_id: int = Field(..., description="菜单ID")
|
||||||
|
data_scope: int | None = Field(default=None, description="数据权限范围(NULL继承角色级)")
|
||||||
|
dept_ids: list[int] = Field(default_factory=list, description="自定义部门ID列表(data_scope=5时)")
|
||||||
|
|
||||||
|
|
||||||
|
class RolePermissionSettingSchema(BaseModel):
|
||||||
|
"""角色权限配置模型"""
|
||||||
|
|
||||||
|
data_scope: int = Field(
|
||||||
|
default=1,
|
||||||
|
description="数据权限范围(1:仅本人 2:本部门 3:本部门及以下 4:全部 5:自定义)",
|
||||||
|
)
|
||||||
|
role_ids: list[int] = Field(default_factory=list, description="角色ID列表")
|
||||||
|
menu_ids: list[int] = Field(default_factory=list, description="菜单ID列表")
|
||||||
|
dept_ids: list[int] = Field(default_factory=list, description="部门ID列表")
|
||||||
|
menu_data_scopes: list[MenuDataScopeItem] = Field(
|
||||||
|
default_factory=list, description="菜单级数据权限配置列表"
|
||||||
|
)
|
||||||
|
|
||||||
|
@model_validator(mode="after")
|
||||||
|
def validate_fields(self):
|
||||||
|
"""验证权限配置字段"""
|
||||||
|
return role_permission_request_validator(self)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleUpdateSchema(RoleCreateSchema):
|
||||||
|
"""角色更新模型"""
|
||||||
|
|
||||||
|
|
||||||
|
class RoleOutSchema(RoleCreateSchema, BaseSchema):
|
||||||
|
"""角色信息响应模型"""
|
||||||
|
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
menus: list[MenuOutSchema] = Field(default_factory=list, description="角色菜单列表")
|
||||||
|
depts: list[DeptOutSchema] = Field(default_factory=list, description="角色部门列表")
|
||||||
|
menu_data_scopes: list[MenuDataScopeItem] = Field(
|
||||||
|
default_factory=list, description="菜单级数据权限配置列表"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleQueryParam:
|
||||||
|
"""角色管理查询参数"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
name: str | None = Query(None, description="角色名称"),
|
||||||
|
description: str | None = Query(None, description="描述"),
|
||||||
|
status: str | None = Query(None, description="是否启用"),
|
||||||
|
created_time: list[DateTimeStr] | None = Query(
|
||||||
|
None,
|
||||||
|
description="创建时间范围",
|
||||||
|
examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"],
|
||||||
|
),
|
||||||
|
updated_time: list[DateTimeStr] | None = Query(
|
||||||
|
None,
|
||||||
|
description="更新时间范围",
|
||||||
|
examples=["2025-01-01 00:00:00", "2025-12-31 23:59:59"],
|
||||||
|
),
|
||||||
|
) -> None:
|
||||||
|
# 模糊查询字段
|
||||||
|
self.name = (QueueEnum.like.value, name)
|
||||||
|
if description:
|
||||||
|
self.description = (QueueEnum.like.value, description)
|
||||||
|
|
||||||
|
# 精确查询字段
|
||||||
|
if status:
|
||||||
|
self.status = (QueueEnum.eq.value, status)
|
||||||
|
|
||||||
|
# 时间范围查询
|
||||||
|
if created_time and len(created_time) == 2:
|
||||||
|
self.created_time = (QueueEnum.between.value, (created_time[0], created_time[1]))
|
||||||
|
if updated_time and len(updated_time) == 2:
|
||||||
|
self.updated_time = (QueueEnum.between.value, (updated_time[0], updated_time[1]))
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||||
|
from app.core.base_schema import BatchSetAvailable
|
||||||
|
from app.core.exceptions import CustomException
|
||||||
|
from app.utils.excel_util import ExcelUtil
|
||||||
|
|
||||||
|
from .crud import RoleCRUD
|
||||||
|
from .schema import (
|
||||||
|
RoleCreateSchema,
|
||||||
|
RoleOutSchema,
|
||||||
|
RolePermissionSettingSchema,
|
||||||
|
RoleQueryParam,
|
||||||
|
RoleUpdateSchema,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class RoleService:
|
||||||
|
"""角色模块服务层"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_role_detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||||
|
"""
|
||||||
|
获取角色详情
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息模型
|
||||||
|
- id (int): 角色ID
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 角色详情字典
|
||||||
|
"""
|
||||||
|
role = await RoleCRUD(auth).get_by_id_crud(id=id)
|
||||||
|
role_dict = RoleOutSchema.model_validate(role).model_dump()
|
||||||
|
# 加载菜单级数据权限配置
|
||||||
|
role_dict["menu_data_scopes"] = await RoleCRUD(auth).get_role_menu_data_scopes_crud(
|
||||||
|
role_id=id
|
||||||
|
)
|
||||||
|
return role_dict
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def get_role_list_service(
|
||||||
|
cls,
|
||||||
|
auth: AuthSchema,
|
||||||
|
search: RoleQueryParam | None = None,
|
||||||
|
order_by: list[dict[str, str]] | None = None,
|
||||||
|
) -> list[dict]:
|
||||||
|
"""
|
||||||
|
获取角色列表
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息模型
|
||||||
|
- search (RoleQueryParam | None): 查询参数模型
|
||||||
|
- order_by (list[dict[str, str]] | None): 排序参数列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- list[dict]: 角色详情字典列表
|
||||||
|
"""
|
||||||
|
role_list = await RoleCRUD(auth).get_list_crud(search=search.__dict__, order_by=order_by)
|
||||||
|
return [RoleOutSchema.model_validate(role).model_dump() for role in role_list]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def create_role_service(cls, auth: AuthSchema, data: RoleCreateSchema) -> dict:
|
||||||
|
"""
|
||||||
|
创建角色
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息模型
|
||||||
|
- data (RoleCreateSchema): 创建角色模型
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 新创建的角色详情字典
|
||||||
|
"""
|
||||||
|
role = await RoleCRUD(auth).get(name=data.name)
|
||||||
|
if role:
|
||||||
|
raise CustomException(msg="创建失败,该角色已存在")
|
||||||
|
obj = await RoleCRUD(auth).get(code=data.code)
|
||||||
|
if obj:
|
||||||
|
raise CustomException(msg="创建失败,编码已存在")
|
||||||
|
new_role = await RoleCRUD(auth).create(data=data)
|
||||||
|
return RoleOutSchema.model_validate(new_role).model_dump()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def update_role_service(cls, auth: AuthSchema, id: int, data: RoleUpdateSchema) -> dict:
|
||||||
|
"""
|
||||||
|
更新角色
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息模型
|
||||||
|
- id (int): 角色ID
|
||||||
|
- data (RoleUpdateSchema): 更新角色模型
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- dict: 更新后的角色详情字典
|
||||||
|
"""
|
||||||
|
role = await RoleCRUD(auth).get_by_id_crud(id=id)
|
||||||
|
if not role:
|
||||||
|
raise CustomException(msg="更新失败,该角色不存在")
|
||||||
|
exist_role = await RoleCRUD(auth).get(name=data.name)
|
||||||
|
if exist_role and exist_role.id != id:
|
||||||
|
raise CustomException(msg="更新失败,角色名称重复")
|
||||||
|
updated_role = await RoleCRUD(auth).update(id=id, data=data)
|
||||||
|
return RoleOutSchema.model_validate(updated_role).model_dump()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def delete_role_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||||
|
"""
|
||||||
|
删除角色
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息模型
|
||||||
|
- ids (list[int]): 角色ID列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
|
if len(ids) < 1:
|
||||||
|
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||||
|
for id in ids:
|
||||||
|
role = await RoleCRUD(auth).get_by_id_crud(id=id)
|
||||||
|
if not role:
|
||||||
|
raise CustomException(msg="删除失败,该角色不存在")
|
||||||
|
await RoleCRUD(auth).delete(ids=ids)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def set_role_permission_service(
|
||||||
|
cls, auth: AuthSchema, data: RolePermissionSettingSchema
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
设置角色权限
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息模型
|
||||||
|
- data (RolePermissionSettingSchema): 角色权限设置模型
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
|
# 设置角色菜单权限
|
||||||
|
await RoleCRUD(auth).set_role_menus_crud(role_ids=data.role_ids, menu_ids=data.menu_ids)
|
||||||
|
|
||||||
|
# 设置数据权限范围
|
||||||
|
await RoleCRUD(auth).set_role_data_scope_crud(
|
||||||
|
role_ids=data.role_ids, data_scope=data.data_scope
|
||||||
|
)
|
||||||
|
|
||||||
|
# 设置自定义数据权限部门
|
||||||
|
if data.data_scope == 5 and data.dept_ids:
|
||||||
|
await RoleCRUD(auth).set_role_depts_crud(role_ids=data.role_ids, dept_ids=data.dept_ids)
|
||||||
|
else:
|
||||||
|
await RoleCRUD(auth).set_role_depts_crud(role_ids=data.role_ids, dept_ids=[])
|
||||||
|
|
||||||
|
# 设置菜单级数据权限
|
||||||
|
if data.menu_data_scopes:
|
||||||
|
for role_id in data.role_ids:
|
||||||
|
await RoleCRUD(auth).set_role_menu_data_scopes_crud(
|
||||||
|
role_id=role_id, menu_data_scopes=data.menu_data_scopes
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def set_role_available_service(cls, auth: AuthSchema, data: BatchSetAvailable) -> None:
|
||||||
|
"""
|
||||||
|
设置角色可用状态
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- auth (AuthSchema): 认证信息模型
|
||||||
|
- data (BatchSetAvailable): 批量设置可用状态模型
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- None
|
||||||
|
"""
|
||||||
|
await RoleCRUD(auth).set_available_crud(ids=data.ids, status=data.status)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
async def export_role_list_service(cls, role_list: list[dict[str, Any]]) -> bytes:
|
||||||
|
"""
|
||||||
|
导出角色列表
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- role_list (list[dict[str, Any]]): 角色详情字典列表
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- bytes: Excel文件字节流
|
||||||
|
"""
|
||||||
|
# 字段映射配置
|
||||||
|
mapping_dict = {
|
||||||
|
"id": "角色编号",
|
||||||
|
"name": "角色名称",
|
||||||
|
"order": "显示顺序",
|
||||||
|
"data_scope": "数据权限",
|
||||||
|
"status": "状态",
|
||||||
|
"description": "备注",
|
||||||
|
"created_time": "创建时间",
|
||||||
|
"updated_time": "更新时间",
|
||||||
|
"created_id": "创建者ID",
|
||||||
|
"updated_id": "更新者ID",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 数据权限映射
|
||||||
|
data_scope_map = {
|
||||||
|
1: "仅本人数据权限",
|
||||||
|
2: "本部门数据权限",
|
||||||
|
3: "本部门及以下数据权限",
|
||||||
|
4: "全部数据权限",
|
||||||
|
5: "自定义数据权限",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 处理数据
|
||||||
|
data = role_list.copy()
|
||||||
|
for item in data:
|
||||||
|
item["status"] = "启用" if item.get("status") == "0" else "停用"
|
||||||
|
item["data_scope"] = data_scope_map.get(item.get("data_scope", 1), "")
|
||||||
|
item["creator"] = (
|
||||||
|
item.get("creator", {}).get("name", "未知")
|
||||||
|
if isinstance(item.get("creator"), dict)
|
||||||
|
else "未知"
|
||||||
|
)
|
||||||
|
|
||||||
|
return ExcelUtil.export_list2excel(list_data=data, mapping_dict=mapping_dict)
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
import re
|
||||||
|
from datetime import date, datetime, time
|
||||||
|
from typing import Annotated, Any
|
||||||
|
|
||||||
|
from pydantic import AfterValidator, PlainSerializer, WithJsonSchema
|
||||||
|
|
||||||
|
from app.common.constant import RET
|
||||||
|
from app.core.exceptions import CustomException
|
||||||
|
|
||||||
|
# 自定义日期时间字符串类型
|
||||||
|
DateTimeStr = Annotated[
|
||||||
|
datetime,
|
||||||
|
AfterValidator(lambda x: datetime_validator(x)),
|
||||||
|
PlainSerializer(
|
||||||
|
lambda x: x.strftime("%Y-%m-%d %H:%M:%S") if isinstance(x, datetime) else str(x),
|
||||||
|
return_type=str,
|
||||||
|
),
|
||||||
|
WithJsonSchema({"type": "string"}, mode="serialization"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 自定义日期字符串类型
|
||||||
|
DateStr = Annotated[
|
||||||
|
date,
|
||||||
|
AfterValidator(lambda x: date_validator(x)),
|
||||||
|
PlainSerializer(
|
||||||
|
lambda x: x.strftime("%Y-%m-%d") if isinstance(x, date) else str(x),
|
||||||
|
return_type=str,
|
||||||
|
),
|
||||||
|
WithJsonSchema({"type": "string"}, mode="serialization"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 自定义时间字符串类型
|
||||||
|
TimeStr = Annotated[
|
||||||
|
time,
|
||||||
|
AfterValidator(lambda x: time_validator(x)),
|
||||||
|
PlainSerializer(
|
||||||
|
lambda x: x.strftime("%H:%M:%S") if isinstance(x, time) else str(x),
|
||||||
|
return_type=str,
|
||||||
|
),
|
||||||
|
WithJsonSchema({"type": "string"}, mode="serialization"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 自定义手机号类型
|
||||||
|
Telephone = Annotated[
|
||||||
|
str,
|
||||||
|
AfterValidator(lambda x: mobile_validator(x)),
|
||||||
|
PlainSerializer(lambda x: x, return_type=str),
|
||||||
|
WithJsonSchema({"type": "string"}, mode="serialization"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 自定义邮箱类型
|
||||||
|
Email = Annotated[
|
||||||
|
str,
|
||||||
|
AfterValidator(lambda x: email_validator(x)),
|
||||||
|
PlainSerializer(lambda x: x, return_type=str),
|
||||||
|
WithJsonSchema({"type": "string"}, mode="serialization"),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def datetime_validator(value: str | datetime) -> datetime:
|
||||||
|
"""
|
||||||
|
日期格式验证器。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str | datetime): 日期值。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- datetime: 格式化后的日期。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 日期格式无效时抛出。
|
||||||
|
"""
|
||||||
|
pattern = "%Y-%m-%d %H:%M:%S"
|
||||||
|
try:
|
||||||
|
if isinstance(value, str):
|
||||||
|
return datetime.strptime(value, pattern)
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
return value
|
||||||
|
except Exception:
|
||||||
|
raise CustomException(code=RET.ERROR.code, msg="无效的日期格式")
|
||||||
|
|
||||||
|
|
||||||
|
def date_validator(value: str | date) -> date:
|
||||||
|
"""
|
||||||
|
日期格式验证器。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str | date): 日期值。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- date: 格式化后的日期。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 日期格式无效时抛出。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if isinstance(value, str):
|
||||||
|
return datetime.strptime(value, "%Y-%m-%d").date()
|
||||||
|
if isinstance(value, date):
|
||||||
|
return value
|
||||||
|
except Exception:
|
||||||
|
raise CustomException(code=RET.ERROR.code, msg="无效的日期格式")
|
||||||
|
|
||||||
|
|
||||||
|
def time_validator(value: str | time) -> time:
|
||||||
|
"""
|
||||||
|
时间格式验证器。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str | time): 时间值。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- time: 格式化后的时间。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 时间格式无效时抛出。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if isinstance(value, str):
|
||||||
|
return datetime.strptime(value, "%H:%M:%S").time()
|
||||||
|
if isinstance(value, time):
|
||||||
|
return value
|
||||||
|
except Exception:
|
||||||
|
raise CustomException(code=RET.ERROR.code, msg="无效的时间格式")
|
||||||
|
|
||||||
|
|
||||||
|
def email_validator(value: str) -> str:
|
||||||
|
"""
|
||||||
|
邮箱地址验证器。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str): 邮箱地址。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str: 验证后的邮箱地址。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 邮箱格式无效时抛出。
|
||||||
|
"""
|
||||||
|
if not value:
|
||||||
|
raise CustomException(code=RET.ERROR.code, msg="邮箱地址不能为空")
|
||||||
|
|
||||||
|
regex = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
|
||||||
|
|
||||||
|
if not re.match(regex, value):
|
||||||
|
raise CustomException(code=RET.ERROR.code, msg="邮箱地址格式不正确")
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def mobile_validator(value: str | None) -> str | None:
|
||||||
|
"""
|
||||||
|
手机号验证器。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str | None): 手机号。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str | None: 验证后的手机号。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 手机号格式无效时抛出。
|
||||||
|
"""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
|
||||||
|
if len(value) != 11 or not value.isdigit():
|
||||||
|
raise CustomException(code=RET.ERROR.code, msg="手机号格式不正确")
|
||||||
|
|
||||||
|
regex = r"^1(3\d|4[4-9]|5[0-35-9]|6[67]|7[013-8]|8[0-9]|9[0-9])\d{8}$"
|
||||||
|
|
||||||
|
if not re.match(regex, value):
|
||||||
|
raise CustomException(code=RET.ERROR.code, msg="手机号格式不正确")
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def code_validator(value: str | None) -> str | None:
|
||||||
|
"""
|
||||||
|
编码验证器。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- value (str | None): 编码。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- str | None: 验证后的编码。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 编码格式无效时抛出。
|
||||||
|
"""
|
||||||
|
if not value:
|
||||||
|
return value
|
||||||
|
v = value.strip()
|
||||||
|
if not re.match(r"^[A-Za-z][A-Za-z0-9_]{1,15}$", v):
|
||||||
|
raise CustomException(
|
||||||
|
code=RET.ERROR.code,
|
||||||
|
msg="编码需字母开头,允许字母/数字/下划线,长度2-16",
|
||||||
|
)
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
|
def menu_request_validator(data: Any) -> Any:
|
||||||
|
"""
|
||||||
|
菜单请求数据验证器。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- data (Any): 请求数据。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Any: 验证后的请求数据。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 请求数据无效时抛出。
|
||||||
|
"""
|
||||||
|
menu_types = {1: "目录", 2: "功能", 3: "权限", 4: "外链"}
|
||||||
|
|
||||||
|
if data.type not in menu_types:
|
||||||
|
raise CustomException(
|
||||||
|
code=RET.ERROR.code,
|
||||||
|
msg=f"菜单类型必须为: {','.join(map(str, menu_types.keys()))}",
|
||||||
|
)
|
||||||
|
|
||||||
|
if data.type in [1, 2]:
|
||||||
|
if not data.route_name:
|
||||||
|
raise CustomException(code=RET.ERROR.code, msg="路由名称不能为空")
|
||||||
|
if not data.route_path:
|
||||||
|
raise CustomException(code=RET.ERROR.code, msg="路由路径不能为空")
|
||||||
|
|
||||||
|
if data.type == 2 and not data.component_path:
|
||||||
|
raise CustomException(code=RET.ERROR.code, msg="组件路径不能为空")
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def role_permission_request_validator(data: Any) -> Any:
|
||||||
|
"""
|
||||||
|
角色权限设置数据验证器。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
- data (Any): 请求数据。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
- Any: 验证后的请求数据。
|
||||||
|
|
||||||
|
异常:
|
||||||
|
- CustomException: 请求数据无效时抛出。
|
||||||
|
"""
|
||||||
|
data_scopes = {
|
||||||
|
1: "仅本人数据权限",
|
||||||
|
2: "本部门数据权限",
|
||||||
|
3: "本部门及以下数据权限",
|
||||||
|
4: "全部数据权限",
|
||||||
|
5: "自定义数据权限",
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.data_scope not in data_scopes:
|
||||||
|
raise CustomException(
|
||||||
|
code=RET.ERROR.code,
|
||||||
|
msg=f"数据权限范围必须为: {','.join(map(str, data_scopes.keys()))}",
|
||||||
|
)
|
||||||
|
|
||||||
|
if not data.role_ids:
|
||||||
|
raise CustomException(code=RET.ERROR.code, msg="角色不能为空")
|
||||||
|
|
||||||
|
# 验证菜单级数据权限配置
|
||||||
|
if hasattr(data, "menu_data_scopes") and data.menu_data_scopes:
|
||||||
|
menu_id_set = set(data.menu_ids) if data.menu_ids else set()
|
||||||
|
for item in data.menu_data_scopes:
|
||||||
|
if item.data_scope is not None and item.data_scope not in data_scopes:
|
||||||
|
raise CustomException(
|
||||||
|
code=RET.ERROR.code,
|
||||||
|
msg=f"菜单级数据权限范围必须为: {','.join(map(str, data_scopes.keys()))} 或 NULL",
|
||||||
|
)
|
||||||
|
if item.data_scope != 5 and item.dept_ids:
|
||||||
|
raise CustomException(
|
||||||
|
code=RET.ERROR.code,
|
||||||
|
msg="仅当数据权限范围为自定义(5)时才能配置部门列表",
|
||||||
|
)
|
||||||
|
if menu_id_set and item.menu_id not in menu_id_set:
|
||||||
|
raise CustomException(
|
||||||
|
code=RET.ERROR.code,
|
||||||
|
msg=f"菜单级数据权限中的菜单ID {item.menu_id} 不在已分配的菜单列表中",
|
||||||
|
)
|
||||||
|
|
||||||
|
return data
|
||||||
Reference in New Issue
Block a user