feat:新增规则列表及规则分类接口
parent
ddfaae5bd9
commit
8c5b39d03b
@ -0,0 +1,14 @@
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
.pytest_cache/
|
||||
.ruff_cache/
|
||||
.mypy_cache/
|
||||
|
||||
logs/
|
||||
*.log
|
||||
|
||||
env/.env.dev
|
||||
env/.env.prod
|
||||
@ -0,0 +1,9 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
from app.api.v1.module_rule.rule.controller import RuleRouter
|
||||
from app.api.v1.module_rule.rule_category.controller import RuleCategoryRouter
|
||||
|
||||
rule_router = APIRouter(prefix="/rule")
|
||||
|
||||
rule_router.include_router(RuleRouter)
|
||||
rule_router.include_router(RuleCategoryRouter)
|
||||
@ -0,0 +1,3 @@
|
||||
from .controller import RuleRouter
|
||||
|
||||
__all__ = ["RuleRouter"]
|
||||
@ -0,0 +1,95 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.common.response import ResponseSchema, SuccessResponse
|
||||
from app.core import cache_util
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.base_schema import AuthSchema, PageResultSchema
|
||||
from app.core.cache_util import cache
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.router_class import OperationLogRoute
|
||||
|
||||
from .schema import RuleCreateSchema, RuleOutSchema, RuleQueryParam, RuleUpdateSchema
|
||||
from .service import RuleService
|
||||
|
||||
RuleRouter = APIRouter(route_class=OperationLogRoute, prefix="", tags=["规则列表"])
|
||||
|
||||
_RULE_NS = "rule"
|
||||
|
||||
|
||||
@RuleRouter.get(
|
||||
"/list",
|
||||
summary="查询规则列表",
|
||||
response_model=ResponseSchema[PageResultSchema[RuleOutSchema]],
|
||||
)
|
||||
@cache(expire=300, namespace=_RULE_NS)
|
||||
async def get_rule_list_controller(
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[RuleQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_rule:rule:query"]))],
|
||||
) -> JSONResponse:
|
||||
result = await RuleService(auth).page(
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size,
|
||||
search=search,
|
||||
order_by=page.order_by or [{"id": "desc"}],
|
||||
)
|
||||
return SuccessResponse(data=result, msg="查询规则列表成功")
|
||||
|
||||
|
||||
@RuleRouter.get(
|
||||
"/detail/{id}",
|
||||
summary="查询规则详情",
|
||||
response_model=ResponseSchema[RuleOutSchema],
|
||||
)
|
||||
async def get_rule_detail_controller(
|
||||
id: Annotated[int, Path(description="规则ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_rule:rule:detail"]))],
|
||||
) -> JSONResponse:
|
||||
result = await RuleService(auth).detail(id=id)
|
||||
return SuccessResponse(data=result, msg="查询规则详情成功")
|
||||
|
||||
|
||||
@RuleRouter.post(
|
||||
"/create",
|
||||
summary="创建规则",
|
||||
response_model=ResponseSchema[RuleOutSchema],
|
||||
)
|
||||
async def create_rule_controller(
|
||||
data: RuleCreateSchema,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_rule:rule:create"]))],
|
||||
) -> JSONResponse:
|
||||
result = await RuleService(auth).create(data=data)
|
||||
await cache_util.clear(namespace=_RULE_NS)
|
||||
return SuccessResponse(data=result, msg="创建规则成功")
|
||||
|
||||
|
||||
@RuleRouter.put(
|
||||
"/update/{id}",
|
||||
summary="修改规则",
|
||||
response_model=ResponseSchema[RuleOutSchema],
|
||||
)
|
||||
async def update_rule_controller(
|
||||
data: RuleUpdateSchema,
|
||||
id: Annotated[int, Path(description="规则ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_rule:rule:update"]))],
|
||||
) -> JSONResponse:
|
||||
result = await RuleService(auth).update(id=id, data=data)
|
||||
await cache_util.clear(namespace=_RULE_NS)
|
||||
return SuccessResponse(data=result, msg="修改规则成功")
|
||||
|
||||
|
||||
@RuleRouter.delete(
|
||||
"/delete",
|
||||
summary="删除规则",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def delete_rule_controller(
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_rule:rule:delete"]))],
|
||||
) -> JSONResponse:
|
||||
await RuleService(auth).delete(ids=ids)
|
||||
await cache_util.clear(namespace=_RULE_NS)
|
||||
return SuccessResponse(msg="删除规则成功")
|
||||
@ -0,0 +1,12 @@
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.core.base_schema import AuthSchema
|
||||
|
||||
from .model import RuleModel
|
||||
from .schema import RuleCreateSchema, RuleUpdateSchema
|
||||
|
||||
|
||||
class RuleCRUD(CRUDBase[RuleModel, RuleCreateSchema, RuleUpdateSchema]):
|
||||
"""规则列表数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
super().__init__(model=RuleModel, auth=auth)
|
||||
@ -0,0 +1,34 @@
|
||||
from sqlalchemy import ForeignKey, Integer, String, Text, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.base_model import ModelMixin, TenantMixin, UserMixin
|
||||
|
||||
|
||||
class RuleModel(ModelMixin, TenantMixin, UserMixin):
|
||||
"""规则模型"""
|
||||
|
||||
__tablename__: str = "rule_list"
|
||||
__table_args__ = (UniqueConstraint("tenant_id", "name"), {"comment": "规则列表表"})
|
||||
__loader_options__: list[str] = [
|
||||
"category",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"deleted_by",
|
||||
"tenant_by",
|
||||
]
|
||||
|
||||
name: Mapped[str] = mapped_column(String(128), nullable=False, comment="规则名称", index=True)
|
||||
category_id: Mapped[int | None] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("rule_category.id", ondelete="SET NULL", onupdate="CASCADE"),
|
||||
default=None,
|
||||
nullable=True,
|
||||
index=True,
|
||||
comment="规则分类ID",
|
||||
)
|
||||
category_name_snapshot: Mapped[str | None] = mapped_column(String(64), default=None, nullable=True, comment="规则分类名称快照")
|
||||
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="规则描述")
|
||||
prompt_template: Mapped[str] = mapped_column(Text, nullable=False, comment="提示词模板")
|
||||
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态 0:启用 1:停用", index=True)
|
||||
|
||||
category = relationship("RuleCategoryModel", lazy="selectin", foreign_keys=[category_id], uselist=False)
|
||||
@ -0,0 +1,75 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastapi import Query
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
from app.common.enums import QueueEnum
|
||||
from app.core.base_params import BaseQueryParam, TenantByQueryParam, UserByQueryParam
|
||||
from app.core.base_schema import BaseSchema, TenantBySchema, UserBySchema
|
||||
|
||||
|
||||
class RuleCreateSchema(BaseModel):
|
||||
"""规则创建模型"""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=128, description="规则名称")
|
||||
category_id: int | None = Field(default=None, ge=0, description="规则分类ID")
|
||||
description: str | None = Field(default=None, max_length=2000, description="规则描述")
|
||||
prompt_template: str = Field(..., min_length=1, description="提示词模板")
|
||||
status: int = Field(default=0, ge=0, le=1, description="状态 0:启用 1:停用")
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, value: str) -> str:
|
||||
value = value.strip()
|
||||
if not value:
|
||||
raise ValueError("规则名称不能为空")
|
||||
return value
|
||||
|
||||
@field_validator("category_id")
|
||||
@classmethod
|
||||
def normalize_category_id(cls, value: int | None) -> int | None:
|
||||
return None if value == 0 else value
|
||||
|
||||
@field_validator("prompt_template")
|
||||
@classmethod
|
||||
def validate_prompt_template(cls, value: str) -> str:
|
||||
value = value.strip()
|
||||
if not value:
|
||||
raise ValueError("提示词模板不能为空")
|
||||
return value
|
||||
|
||||
@field_validator("status")
|
||||
@classmethod
|
||||
def validate_status(cls, value: int) -> int:
|
||||
if value not in {0, 1}:
|
||||
raise ValueError("状态仅支持 0(启用) 或 1(停用)")
|
||||
return value
|
||||
|
||||
|
||||
class RuleUpdateSchema(RuleCreateSchema):
|
||||
"""规则更新模型"""
|
||||
|
||||
|
||||
class RuleOutSchema(RuleCreateSchema, BaseSchema, UserBySchema, TenantBySchema):
|
||||
"""规则响应模型"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
category_name_snapshot: str | None = Field(default=None, max_length=64, description="规则分类名称快照")
|
||||
category_name: str | None = Field(default=None, max_length=64, description="规则分类名称")
|
||||
|
||||
|
||||
@dataclass
|
||||
class RuleQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam):
|
||||
"""规则查询参数"""
|
||||
|
||||
name: str | None = Query(None, description="规则名称")
|
||||
category_id: int | None = Query(None, ge=0, description="规则分类ID")
|
||||
status: int | None = Query(None, ge=0, le=1, description="状态 0:启用 1:停用")
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.name = (QueueEnum.like.value, self.name)
|
||||
if isinstance(self.category_id, int):
|
||||
self.category_id = (QueueEnum.eq.value, None if self.category_id == 0 else self.category_id)
|
||||
if isinstance(self.status, int):
|
||||
self.status = (QueueEnum.eq.value, self.status)
|
||||
@ -0,0 +1,85 @@
|
||||
from app.api.v1.module_rule.rule_category.crud import RuleCategoryCRUD
|
||||
from app.core.base_schema import AuthSchema
|
||||
from app.core.exceptions import CustomException
|
||||
|
||||
from .crud import RuleCRUD
|
||||
from .schema import RuleCreateSchema, RuleOutSchema, RuleQueryParam, RuleUpdateSchema
|
||||
|
||||
|
||||
class RuleService:
|
||||
"""规则列表服务"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
self.auth = auth
|
||||
|
||||
async def detail(self, id: int) -> RuleOutSchema:
|
||||
rule = await RuleCRUD(self.auth).get_or_404(id=id)
|
||||
return self._to_out_schema(rule)
|
||||
|
||||
async def page(
|
||||
self,
|
||||
page_no: int,
|
||||
page_size: int,
|
||||
search: RuleQueryParam | None = None,
|
||||
order_by: list[dict[str, str]] | None = None,
|
||||
) -> dict:
|
||||
offset = (page_no - 1) * page_size
|
||||
page_result = await RuleCRUD(self.auth).page(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by or [{"id": "desc"}],
|
||||
search=vars(search) if search else None,
|
||||
out_schema=RuleOutSchema,
|
||||
)
|
||||
for item in page_result.items:
|
||||
item["category_name"] = item.get("category_name_snapshot")
|
||||
return page_result
|
||||
|
||||
async def get_list(
|
||||
self,
|
||||
search: RuleQueryParam | None = None,
|
||||
order_by: list[dict] | None = None,
|
||||
) -> list[RuleOutSchema]:
|
||||
rule_list = await RuleCRUD(self.auth).get_list(search=vars(search) if search else None, order_by=order_by or [{"id": "desc"}])
|
||||
return [self._to_out_schema(rule) for rule in rule_list]
|
||||
|
||||
async def create(self, data: RuleCreateSchema) -> RuleOutSchema:
|
||||
await self._validate_unique_name(name=data.name)
|
||||
category_name_snapshot = await self._get_category_name_snapshot(category_id=data.category_id)
|
||||
rule = await RuleCRUD(self.auth).create(data={**data.model_dump(), "category_name_snapshot": category_name_snapshot})
|
||||
return self._to_out_schema(rule)
|
||||
|
||||
async def update(self, id: int, data: RuleUpdateSchema) -> RuleOutSchema:
|
||||
await RuleCRUD(self.auth).get_or_404(id=id, msg="更新失败,该规则不存在")
|
||||
await self._validate_unique_name(name=data.name, exclude_id=id)
|
||||
category_name_snapshot = await self._get_category_name_snapshot(category_id=data.category_id)
|
||||
rule = await RuleCRUD(self.auth).update(id=id, data={**data.model_dump(), "category_name_snapshot": category_name_snapshot})
|
||||
return self._to_out_schema(rule)
|
||||
|
||||
async def delete(self, ids: list[int]) -> None:
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||
rules = await RuleCRUD(self.auth).get_list(search={"id": ("in", ids)})
|
||||
rule_map = {rule.id: rule for rule in rules}
|
||||
for rule_id in ids:
|
||||
if rule_id not in rule_map:
|
||||
raise CustomException(msg="删除失败,该规则不存在")
|
||||
await RuleCRUD(self.auth).delete(ids=ids)
|
||||
|
||||
def _to_out_schema(self, rule) -> RuleOutSchema:
|
||||
rule_out = RuleOutSchema.model_validate(rule)
|
||||
rule_out.category_name = rule_out.category_name_snapshot
|
||||
return rule_out
|
||||
|
||||
async def _validate_unique_name(self, name: str, exclude_id: int | None = None) -> None:
|
||||
rule = await RuleCRUD(self.auth).get(name=name)
|
||||
if rule and rule.id != exclude_id:
|
||||
raise CustomException(msg="规则名称已存在")
|
||||
|
||||
async def _get_category_name_snapshot(self, category_id: int | None) -> str | None:
|
||||
if category_id is None:
|
||||
return None
|
||||
category = await RuleCategoryCRUD(self.auth).get(id=category_id)
|
||||
if not category:
|
||||
raise CustomException(msg="规则分类不存在")
|
||||
return category.name
|
||||
@ -0,0 +1,3 @@
|
||||
from .controller import RuleCategoryRouter
|
||||
|
||||
__all__ = ["RuleCategoryRouter"]
|
||||
@ -0,0 +1,115 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app.common.response import ResponseSchema, SuccessResponse
|
||||
from app.core import cache_util
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.base_schema import AuthSchema, PageResultSchema
|
||||
from app.core.cache_util import cache
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.router_class import OperationLogRoute
|
||||
|
||||
from .schema import (
|
||||
RuleCategoryCreateSchema,
|
||||
RuleCategoryOutSchema,
|
||||
RuleCategoryQueryParam,
|
||||
RuleCategoryTreeOutSchema,
|
||||
RuleCategoryUpdateSchema,
|
||||
)
|
||||
from .service import RuleCategoryService
|
||||
|
||||
RuleCategoryRouter = APIRouter(route_class=OperationLogRoute, prefix="/category", tags=["规则分类"])
|
||||
|
||||
_RULE_CATEGORY_NS = "rule_category"
|
||||
|
||||
|
||||
@RuleCategoryRouter.get(
|
||||
"/list",
|
||||
summary="查询规则分类列表",
|
||||
response_model=ResponseSchema[PageResultSchema[RuleCategoryOutSchema]],
|
||||
)
|
||||
@cache(expire=300, namespace=_RULE_CATEGORY_NS)
|
||||
async def get_rule_category_list_controller(
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[RuleCategoryQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_rule:rule_category:query"]))],
|
||||
) -> JSONResponse:
|
||||
result = await RuleCategoryService(auth).page(
|
||||
page_no=page.page_no,
|
||||
page_size=page.page_size,
|
||||
search=search,
|
||||
order_by=page.order_by or [{"order": "asc"}, {"id": "asc"}],
|
||||
)
|
||||
return SuccessResponse(data=result, msg="查询规则分类列表成功")
|
||||
|
||||
|
||||
@RuleCategoryRouter.get(
|
||||
"/tree",
|
||||
summary="查询规则分类树",
|
||||
response_model=ResponseSchema[list[RuleCategoryTreeOutSchema]],
|
||||
)
|
||||
@cache(expire=300, namespace=_RULE_CATEGORY_NS)
|
||||
async def get_rule_category_tree_controller(
|
||||
search: Annotated[RuleCategoryQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_rule:rule_category:query"]))],
|
||||
) -> JSONResponse:
|
||||
result = await RuleCategoryService(auth).tree(search=search)
|
||||
return SuccessResponse(data=result, msg="查询规则分类树成功")
|
||||
|
||||
|
||||
@RuleCategoryRouter.get(
|
||||
"/detail/{id}",
|
||||
summary="查询规则分类详情",
|
||||
response_model=ResponseSchema[RuleCategoryOutSchema],
|
||||
)
|
||||
async def get_rule_category_detail_controller(
|
||||
id: Annotated[int, Path(description="规则分类ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_rule:rule_category:detail"]))],
|
||||
) -> JSONResponse:
|
||||
result = await RuleCategoryService(auth).detail(id=id)
|
||||
return SuccessResponse(data=result, msg="查询规则分类详情成功")
|
||||
|
||||
|
||||
@RuleCategoryRouter.post(
|
||||
"/create",
|
||||
summary="创建规则分类",
|
||||
response_model=ResponseSchema[RuleCategoryOutSchema],
|
||||
)
|
||||
async def create_rule_category_controller(
|
||||
data: RuleCategoryCreateSchema,
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_rule:rule_category:create"]))],
|
||||
) -> JSONResponse:
|
||||
result = await RuleCategoryService(auth).create(data=data)
|
||||
await cache_util.clear(namespace=_RULE_CATEGORY_NS)
|
||||
return SuccessResponse(data=result, msg="创建规则分类成功")
|
||||
|
||||
|
||||
@RuleCategoryRouter.put(
|
||||
"/update/{id}",
|
||||
summary="修改规则分类",
|
||||
response_model=ResponseSchema[RuleCategoryOutSchema],
|
||||
)
|
||||
async def update_rule_category_controller(
|
||||
data: RuleCategoryUpdateSchema,
|
||||
id: Annotated[int, Path(description="规则分类ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_rule:rule_category:update"]))],
|
||||
) -> JSONResponse:
|
||||
result = await RuleCategoryService(auth).update(id=id, data=data)
|
||||
await cache_util.clear(namespace=_RULE_CATEGORY_NS)
|
||||
return SuccessResponse(data=result, msg="修改规则分类成功")
|
||||
|
||||
|
||||
@RuleCategoryRouter.delete(
|
||||
"/delete",
|
||||
summary="删除规则分类",
|
||||
response_model=ResponseSchema[None],
|
||||
)
|
||||
async def delete_rule_category_controller(
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_rule:rule_category:delete"]))],
|
||||
) -> JSONResponse:
|
||||
await RuleCategoryService(auth).delete(ids=ids)
|
||||
await cache_util.clear(namespace=_RULE_CATEGORY_NS)
|
||||
return SuccessResponse(msg="删除规则分类成功")
|
||||
@ -0,0 +1,12 @@
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.core.base_schema import AuthSchema
|
||||
|
||||
from .model import RuleCategoryModel
|
||||
from .schema import RuleCategoryCreateSchema, RuleCategoryUpdateSchema
|
||||
|
||||
|
||||
class RuleCategoryCRUD(CRUDBase[RuleCategoryModel, RuleCategoryCreateSchema, RuleCategoryUpdateSchema]):
|
||||
"""规则分类数据层"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
super().__init__(model=RuleCategoryModel, auth=auth)
|
||||
@ -0,0 +1,44 @@
|
||||
from sqlalchemy import ForeignKey, Integer, String, Text, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from app.core.base_model import ModelMixin, TenantMixin, UserMixin
|
||||
|
||||
|
||||
class RuleCategoryModel(ModelMixin, TenantMixin, UserMixin):
|
||||
"""规则分类模型"""
|
||||
|
||||
__tablename__: str = "rule_category"
|
||||
__table_args__ = (UniqueConstraint("tenant_id", "parent_id", "name"), {"comment": "规则分类表"})
|
||||
__tree_children_attr__: str = "children"
|
||||
__loader_options__: list[str] = [
|
||||
"children",
|
||||
"created_by",
|
||||
"updated_by",
|
||||
"deleted_by",
|
||||
"tenant_by",
|
||||
]
|
||||
|
||||
name: Mapped[str] = mapped_column(String(64), nullable=False, comment="分类名称", index=True)
|
||||
parent_id: Mapped[int | None] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("rule_category.id", ondelete="SET NULL", onupdate="CASCADE"),
|
||||
default=None,
|
||||
nullable=True,
|
||||
index=True,
|
||||
comment="父级分类ID",
|
||||
)
|
||||
order: Mapped[int] = mapped_column(Integer, nullable=False, default=999, comment="显示排序")
|
||||
status: Mapped[int] = mapped_column(Integer, default=0, nullable=False, comment="状态 0:启用 1:停用", index=True)
|
||||
description: Mapped[str | None] = mapped_column(Text, default=None, nullable=True, comment="描述")
|
||||
|
||||
parent: Mapped["RuleCategoryModel | None"] = relationship(
|
||||
back_populates="children",
|
||||
remote_side="RuleCategoryModel.id",
|
||||
foreign_keys=[parent_id],
|
||||
uselist=False,
|
||||
)
|
||||
children: Mapped[list["RuleCategoryModel"]] = relationship(
|
||||
back_populates="parent",
|
||||
foreign_keys=[parent_id],
|
||||
lazy="selectin",
|
||||
)
|
||||
@ -0,0 +1,72 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastapi import Query
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
from app.common.enums import QueueEnum
|
||||
from app.core.base_params import BaseQueryParam, TenantByQueryParam, UserByQueryParam
|
||||
from app.core.base_schema import BaseSchema, TenantBySchema, UserBySchema
|
||||
|
||||
|
||||
class RuleCategoryCreateSchema(BaseModel):
|
||||
"""规则分类创建模型"""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=64, description="分类名称")
|
||||
parent_id: int | None = Field(default=None, ge=0, description="父级分类ID")
|
||||
order: int = Field(default=999, ge=0, description="显示排序")
|
||||
status: int = Field(default=0, ge=0, le=1, description="状态 0:启用 1:停用")
|
||||
description: str | None = Field(default=None, max_length=500, description="描述")
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, value: str) -> str:
|
||||
value = value.strip()
|
||||
if not value:
|
||||
raise ValueError("分类名称不能为空")
|
||||
return value
|
||||
|
||||
@field_validator("parent_id")
|
||||
@classmethod
|
||||
def normalize_parent_id(cls, value: int | None) -> int | None:
|
||||
return None if value == 0 else value
|
||||
|
||||
@field_validator("status")
|
||||
@classmethod
|
||||
def validate_status(cls, value: int) -> int:
|
||||
if value not in {0, 1}:
|
||||
raise ValueError("状态仅支持 0(启用) 或 1(停用)")
|
||||
return value
|
||||
|
||||
|
||||
class RuleCategoryUpdateSchema(RuleCategoryCreateSchema):
|
||||
"""规则分类更新模型"""
|
||||
|
||||
|
||||
class RuleCategoryOutSchema(RuleCategoryCreateSchema, BaseSchema, UserBySchema, TenantBySchema):
|
||||
"""规则分类响应模型"""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
parent_name: str | None = Field(default=None, max_length=64, description="父级分类名称")
|
||||
|
||||
|
||||
class RuleCategoryTreeOutSchema(RuleCategoryOutSchema):
|
||||
"""规则分类树响应模型"""
|
||||
|
||||
children: list["RuleCategoryTreeOutSchema"] | None = Field(default=None, description="子分类列表")
|
||||
|
||||
|
||||
@dataclass
|
||||
class RuleCategoryQueryParam(BaseQueryParam, UserByQueryParam, TenantByQueryParam):
|
||||
"""规则分类查询参数"""
|
||||
|
||||
name: str | None = Query(None, description="分类名称")
|
||||
parent_id: int | None = Query(None, ge=0, description="父级分类ID")
|
||||
status: int | None = Query(None, ge=0, le=1, description="状态 0:启用 1:停用")
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
self.name = (QueueEnum.like.value, self.name)
|
||||
if isinstance(self.parent_id, int):
|
||||
self.parent_id = (QueueEnum.eq.value, None if self.parent_id == 0 else self.parent_id)
|
||||
if isinstance(self.status, int):
|
||||
self.status = (QueueEnum.eq.value, self.status)
|
||||
@ -0,0 +1,122 @@
|
||||
from app.core.base_schema import AuthSchema
|
||||
from app.core.exceptions import CustomException
|
||||
from app.utils.common_util import get_child_id_map, get_parent_id_map, get_parent_recursion
|
||||
from app.api.v1.module_rule.rule.crud import RuleCRUD
|
||||
|
||||
from .crud import RuleCategoryCRUD
|
||||
from .schema import (
|
||||
RuleCategoryCreateSchema,
|
||||
RuleCategoryOutSchema,
|
||||
RuleCategoryQueryParam,
|
||||
RuleCategoryTreeOutSchema,
|
||||
RuleCategoryUpdateSchema,
|
||||
)
|
||||
|
||||
|
||||
class RuleCategoryService:
|
||||
"""规则分类服务"""
|
||||
|
||||
def __init__(self, auth: AuthSchema) -> None:
|
||||
self.auth = auth
|
||||
|
||||
async def detail(self, id: int) -> RuleCategoryOutSchema:
|
||||
category = await RuleCategoryCRUD(self.auth).get_or_404(id=id)
|
||||
return await self._to_out_schema(category)
|
||||
|
||||
async def get_list(
|
||||
self,
|
||||
search: RuleCategoryQueryParam | None = None,
|
||||
order_by: list[dict] | None = None,
|
||||
) -> list[RuleCategoryOutSchema]:
|
||||
category_list = await RuleCategoryCRUD(self.auth).get_list(search=vars(search) if search else None, order_by=order_by or [{"order": "asc"}, {"id": "asc"}])
|
||||
return [await self._to_out_schema(category) for category in category_list]
|
||||
|
||||
async def page(
|
||||
self,
|
||||
page_no: int,
|
||||
page_size: int,
|
||||
search: RuleCategoryQueryParam | None = None,
|
||||
order_by: list[dict[str, str]] | None = None,
|
||||
) -> dict:
|
||||
offset = (page_no - 1) * page_size
|
||||
return await RuleCategoryCRUD(self.auth).page(
|
||||
offset=offset,
|
||||
limit=page_size,
|
||||
order_by=order_by or [{"order": "asc"}, {"id": "asc"}],
|
||||
search=vars(search) if search else None,
|
||||
out_schema=RuleCategoryOutSchema,
|
||||
)
|
||||
|
||||
async def tree(
|
||||
self,
|
||||
search: RuleCategoryQueryParam | None = None,
|
||||
order_by: list[dict] | None = None,
|
||||
) -> list[dict]:
|
||||
category_list = await RuleCategoryCRUD(self.auth).tree_list(search=vars(search) if search else None, order_by=order_by or [{"order": "asc"}, {"id": "asc"}])
|
||||
category_dict_list = [RuleCategoryTreeOutSchema.model_validate(category).model_dump() for category in category_list]
|
||||
return [category for category in category_dict_list if category.get("parent_id") is None]
|
||||
|
||||
async def create(self, data: RuleCategoryCreateSchema) -> RuleCategoryOutSchema:
|
||||
await self._validate_parent(parent_id=data.parent_id)
|
||||
await self._validate_unique_name(name=data.name, parent_id=data.parent_id)
|
||||
category = await RuleCategoryCRUD(self.auth).create(data=data)
|
||||
return await self._to_out_schema(category)
|
||||
|
||||
async def update(self, id: int, data: RuleCategoryUpdateSchema) -> RuleCategoryOutSchema:
|
||||
await RuleCategoryCRUD(self.auth).get_or_404(id=id, msg="更新失败,该规则分类不存在")
|
||||
await self._validate_parent(parent_id=data.parent_id, current_id=id)
|
||||
await self._validate_unique_name(name=data.name, parent_id=data.parent_id, exclude_id=id)
|
||||
category = await RuleCategoryCRUD(self.auth).update(id=id, data=data)
|
||||
return await self._to_out_schema(category)
|
||||
|
||||
async def delete(self, ids: list[int]) -> None:
|
||||
if len(ids) < 1:
|
||||
raise CustomException(msg="删除失败,删除对象不能为空")
|
||||
|
||||
categories = await RuleCategoryCRUD(self.auth).get_list(search={"id": ("in", ids)})
|
||||
category_map = {category.id: category for category in categories}
|
||||
for category_id in ids:
|
||||
if category_id not in category_map:
|
||||
raise CustomException(msg="删除失败,该规则分类不存在")
|
||||
|
||||
all_categories = await RuleCategoryCRUD(self.auth).get_list()
|
||||
child_id_map = get_child_id_map(model_list=all_categories)
|
||||
for category_id in ids:
|
||||
if child_id_map.get(category_id):
|
||||
raise CustomException(msg="存在子分类,不允许删除父级分类")
|
||||
|
||||
rules = await RuleCRUD(self.auth).get_list(search={"category_id": ("in", ids)})
|
||||
if rules:
|
||||
raise CustomException(msg="分类下存在规则,不允许删除")
|
||||
|
||||
await RuleCategoryCRUD(self.auth).delete(ids=ids)
|
||||
|
||||
async def _to_out_schema(self, category) -> RuleCategoryOutSchema:
|
||||
category_out = RuleCategoryOutSchema.model_validate(category)
|
||||
if category_out.parent_id:
|
||||
parent = await RuleCategoryCRUD(self.auth).get(id=category_out.parent_id)
|
||||
if parent:
|
||||
category_out.parent_name = parent.name
|
||||
return category_out
|
||||
|
||||
async def _validate_parent(self, parent_id: int | None, current_id: int | None = None) -> None:
|
||||
if parent_id is None:
|
||||
return
|
||||
if current_id is not None and parent_id == current_id:
|
||||
raise CustomException(msg="父级分类不能选择自身")
|
||||
|
||||
parent = await RuleCategoryCRUD(self.auth).get(id=parent_id)
|
||||
if not parent:
|
||||
raise CustomException(msg="父级分类不存在")
|
||||
|
||||
if current_id is not None:
|
||||
all_categories = await RuleCategoryCRUD(self.auth).get_list()
|
||||
parent_id_map = get_parent_id_map(model_list=all_categories)
|
||||
parent_ids = get_parent_recursion(id=parent_id, id_map=parent_id_map)
|
||||
if current_id in parent_ids:
|
||||
raise CustomException(msg="父级分类不能选择当前分类的子级")
|
||||
|
||||
async def _validate_unique_name(self, name: str, parent_id: int | None, exclude_id: int | None = None) -> None:
|
||||
category = await RuleCategoryCRUD(self.auth).get(name=name, parent_id=parent_id)
|
||||
if category and category.id != exclude_id:
|
||||
raise CustomException(msg="同一父级下分类名称已存在")
|
||||
Loading…
Reference in New Issue