mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-23 05:10:57 +00:00
refactor: 大规模代码整理与功能优化
1. 重构后端API路由、CRUD与模块结构,整合日志管理,移除废弃demo代码 2. 优化前端组件类型定义、样式与路由配置,修复权限判断逻辑 3. 调整默认排序规则、滚动条样式与工具类函数,更新依赖与配置文件 4. 修复多处类型不匹配与默认值问题,完善表单与菜单验证逻辑
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
from .controller import PackageRouter
|
||||
|
||||
__all__ = ["PackageRouter"]
|
||||
@@ -2,53 +2,73 @@ from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Body, Depends, Path
|
||||
from fastapi.responses import JSONResponse
|
||||
from fastapi_cache import FastAPICache
|
||||
from fastapi_cache.decorator import cache
|
||||
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.common.response import ResponseSchema, SuccessResponse
|
||||
from app.core.base_params import PaginationQueryParam
|
||||
from app.core.base_schema import BatchDelete, BatchSetAvailable
|
||||
from app.core.base_schema import AuthSchema, BatchSetAvailable, PageResultSchema
|
||||
from app.core.dependencies import AuthPermission
|
||||
from app.core.logger import log
|
||||
from app.core.router_class import OperationLogRoute
|
||||
|
||||
from .schema import (
|
||||
PackageCreateSchema,
|
||||
PackageMenuSetSchema,
|
||||
PackageOutSchema,
|
||||
PackagePluginSetSchema,
|
||||
PackageQueryParam,
|
||||
PackageUpdateSchema,
|
||||
)
|
||||
from .service import PackageService
|
||||
|
||||
PackageRouter = APIRouter(route_class=OperationLogRoute, prefix="/package", tags=["套餐管理"])
|
||||
PackageRouter = APIRouter(route_class=OperationLogRoute, prefix="/package", tags=["平台管理/套餐管理"])
|
||||
|
||||
_PKG_NS = "package"
|
||||
|
||||
|
||||
@PackageRouter.get(
|
||||
"/detail/{id}",
|
||||
summary="获取套餐详情",
|
||||
description="根据套餐ID获取套餐详情",
|
||||
response_model=ResponseSchema[PackageOutSchema],
|
||||
)
|
||||
@cache(expire=300, namespace=_PKG_NS)
|
||||
async def get_obj_detail_controller(
|
||||
id: Annotated[int, Path(description="套餐ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:query"]))],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_package:package:query']))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取套餐详情
|
||||
|
||||
参数:
|
||||
- id (int): 套餐 ID。
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含套餐详情的 JSON 响应。
|
||||
"""
|
||||
result_dict = await PackageService.detail_service(auth=auth, id=id)
|
||||
log.info(f"获取套餐详情成功 {id}")
|
||||
return SuccessResponse(data=result_dict, msg="获取套餐详情成功")
|
||||
|
||||
|
||||
@PackageRouter.get(
|
||||
"/list",
|
||||
summary="获取套餐列表",
|
||||
description="分页获取套餐列表",
|
||||
response_model=ResponseSchema[dict],
|
||||
response_model=ResponseSchema[PageResultSchema[PackageOutSchema]],
|
||||
)
|
||||
async def get_obj_list_controller(
|
||||
page: Annotated[PaginationQueryParam, Depends()],
|
||||
search: Annotated[PackageQueryParam, Depends()],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:query"]))],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_package:package:query']))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取套餐列表
|
||||
|
||||
参数:
|
||||
- page (PaginationQueryParam): 分页查询参数。
|
||||
- search (PackageQueryParam): 查询筛选参数。
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含分页套餐列表的 JSON 响应。
|
||||
"""
|
||||
result_dict = await PackageService.page_service(
|
||||
auth=auth,
|
||||
page_no=page.page_no,
|
||||
@@ -62,75 +82,116 @@ async def get_obj_list_controller(
|
||||
@PackageRouter.post(
|
||||
"/create",
|
||||
summary="创建套餐",
|
||||
description="创建新的租户套餐",
|
||||
response_model=ResponseSchema[PackageOutSchema],
|
||||
)
|
||||
async def create_obj_controller(
|
||||
data: Annotated[PackageCreateSchema, Body(description="套餐信息")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:create"]))],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_package:package:create']))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
创建套餐
|
||||
|
||||
参数:
|
||||
- data (PackageCreateSchema): 套餐创建参数。
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含创建后的套餐详情的 JSON 响应。
|
||||
"""
|
||||
result_dict = await PackageService.create_service(auth=auth, data=data)
|
||||
log.info(f"创建套餐成功 {result_dict.get('name')}")
|
||||
await FastAPICache.clear(namespace=_PKG_NS)
|
||||
return SuccessResponse(data=result_dict, msg="创建成功")
|
||||
|
||||
|
||||
@PackageRouter.put(
|
||||
"/update/{id}",
|
||||
summary="更新套餐",
|
||||
description="更新套餐信息",
|
||||
response_model=ResponseSchema[PackageOutSchema],
|
||||
)
|
||||
async def update_obj_controller(
|
||||
id: Annotated[int, Path(description="套餐ID")],
|
||||
data: Annotated[PackageUpdateSchema, Body(description="套餐信息")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:update"]))],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_package:package:update']))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
更新套餐
|
||||
|
||||
参数:
|
||||
- id (int): 套餐 ID。
|
||||
- data (PackageUpdateSchema): 套餐更新参数。
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含更新后的套餐详情的 JSON 响应。
|
||||
"""
|
||||
result_dict = await PackageService.update_service(auth=auth, id=id, data=data)
|
||||
log.info(f"更新套餐成功 {id}")
|
||||
await FastAPICache.clear(namespace=_PKG_NS)
|
||||
return SuccessResponse(data=result_dict, msg="更新成功")
|
||||
|
||||
|
||||
@PackageRouter.delete(
|
||||
"/delete",
|
||||
summary="删除套餐",
|
||||
description="批量删除套餐(已被租户使用的套餐无法删除)",
|
||||
response_model=ResponseSchema,
|
||||
)
|
||||
async def delete_obj_controller(
|
||||
data: Annotated[BatchDelete, Body(description="删除信息")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:delete"]))],
|
||||
ids: Annotated[list[int], Body(description="ID列表")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_package:package:delete']))],
|
||||
) -> JSONResponse:
|
||||
await PackageService.delete_service(auth=auth, ids=data.ids)
|
||||
log.info(f"删除套餐成功 {data.ids}")
|
||||
"""
|
||||
删除套餐
|
||||
|
||||
参数:
|
||||
- ids (list[int]): 套餐 ID 列表。
|
||||
|
||||
返回:
|
||||
- JSONResponse: 删除结果。
|
||||
"""
|
||||
await PackageService.delete_service(auth=auth, ids=ids)
|
||||
await FastAPICache.clear(namespace=_PKG_NS)
|
||||
return SuccessResponse(msg="删除成功")
|
||||
|
||||
|
||||
@PackageRouter.patch(
|
||||
"/status/batch",
|
||||
summary="批量修改状态",
|
||||
description="批量启用/禁用套餐",
|
||||
response_model=ResponseSchema,
|
||||
)
|
||||
async def set_available_controller(
|
||||
data: Annotated[BatchSetAvailable, Body(description="状态设置")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:update"]))],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_package:package:update']))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
批量修改套餐状态
|
||||
|
||||
参数:
|
||||
- data (BatchSetAvailable): 批量状态设置参数。
|
||||
|
||||
返回:
|
||||
- JSONResponse: 操作结果。
|
||||
"""
|
||||
for id in data.ids:
|
||||
await PackageService.update_service(auth=auth, id=id, data=PackageUpdateSchema(status=data.status))
|
||||
log.info(f"套餐状态设置成功 {data.ids}")
|
||||
await FastAPICache.clear(namespace=_PKG_NS)
|
||||
return SuccessResponse(msg="状态设置成功")
|
||||
|
||||
|
||||
@PackageRouter.get(
|
||||
"/menus/{package_id}",
|
||||
summary="获取套餐菜单",
|
||||
description="获取套餐包含的菜单ID列表",
|
||||
response_model=ResponseSchema[list[int]],
|
||||
)
|
||||
async def get_menus_controller(
|
||||
package_id: Annotated[int, Path(description="套餐ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:query"]))],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_package:package:query']))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取套餐关联菜单
|
||||
|
||||
参数:
|
||||
- package_id (int): 套餐 ID。
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含菜单 ID 列表的 JSON 响应。
|
||||
"""
|
||||
result = await PackageService.get_menus_service(auth=auth, package_id=package_id)
|
||||
return SuccessResponse(data=result, msg="获取成功")
|
||||
|
||||
@@ -138,14 +199,68 @@ async def get_menus_controller(
|
||||
@PackageRouter.post(
|
||||
"/menus/{package_id}/set",
|
||||
summary="设置套餐菜单",
|
||||
description="批量设置套餐包含的菜单(先清空再写入)",
|
||||
response_model=ResponseSchema,
|
||||
)
|
||||
async def set_menus_controller(
|
||||
package_id: Annotated[int, Path(description="套餐ID")],
|
||||
data: Annotated[PackageMenuSetSchema, Body(description="菜单列表")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(["module_package:package:update"]))],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_package:package:update']))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
设置套餐菜单权限
|
||||
|
||||
参数:
|
||||
- package_id (int): 套餐 ID。
|
||||
- data (PackageMenuSetSchema): 菜单 ID 列表。
|
||||
|
||||
返回:
|
||||
- JSONResponse: 操作结果。
|
||||
"""
|
||||
await PackageService.set_menus_service(auth=auth, package_id=package_id, data=data)
|
||||
log.info(f"套餐[{package_id}]菜单权限已设置, count={len(data.menu_ids)}")
|
||||
return SuccessResponse(msg="设置成功")
|
||||
|
||||
|
||||
@PackageRouter.get(
|
||||
"/plugins/{package_id}",
|
||||
summary="获取套餐插件",
|
||||
response_model=ResponseSchema[list[int]],
|
||||
)
|
||||
async def get_plugins_controller(
|
||||
package_id: Annotated[int, Path(description="套餐ID")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_package:package:query']))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
获取套餐关联插件
|
||||
|
||||
参数:
|
||||
- package_id (int): 套餐 ID。
|
||||
|
||||
返回:
|
||||
- JSONResponse: 包含插件 ID 列表的 JSON 响应。
|
||||
"""
|
||||
result = await PackageService.get_plugins_service(auth=auth, package_id=package_id)
|
||||
return SuccessResponse(data=result, msg="获取成功")
|
||||
|
||||
|
||||
@PackageRouter.post(
|
||||
"/plugins/{package_id}/set",
|
||||
summary="设置套餐插件",
|
||||
response_model=ResponseSchema,
|
||||
)
|
||||
async def set_plugins_controller(
|
||||
package_id: Annotated[int, Path(description="套餐ID")],
|
||||
data: Annotated[PackagePluginSetSchema, Body(description="插件列表")],
|
||||
auth: Annotated[AuthSchema, Depends(AuthPermission(['module_package:package:update']))],
|
||||
) -> JSONResponse:
|
||||
"""
|
||||
设置套餐插件
|
||||
|
||||
参数:
|
||||
- package_id (int): 套餐 ID。
|
||||
- data (PackagePluginSetSchema): 插件 ID 列表。
|
||||
|
||||
返回:
|
||||
- JSONResponse: 操作结果。
|
||||
"""
|
||||
await PackageService.set_plugins_service(auth=auth, package_id=package_id, data=data)
|
||||
return SuccessResponse(msg="设置成功")
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.base_crud import CRUDBase
|
||||
from app.core.base_schema import AuthSchema
|
||||
|
||||
from .model import PackageModel
|
||||
from .schema import PackageCreateSchema, PackageUpdateSchema
|
||||
|
||||
|
||||
class PackageCRUD(CRUDBase):
|
||||
class PackageCRUD(CRUDBase[PackageModel, PackageCreateSchema, PackageUpdateSchema]):
|
||||
"""套餐模块 CRUD"""
|
||||
|
||||
def __init__(self, auth: AuthSchema):
|
||||
super().__init__(PackageModel, auth)
|
||||
super().__init__(model=PackageModel, auth=auth)
|
||||
|
||||
@@ -1,34 +1,34 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import DateTime, ForeignKey, Integer, String, UniqueConstraint
|
||||
from sqlalchemy import ForeignKey, Integer, String, UniqueConstraint
|
||||
from sqlalchemy.orm import Mapped, mapped_column, validates
|
||||
|
||||
from app.core.base_model import MappedBase
|
||||
from app.core.base_model import MappedBase, ModelMixin
|
||||
|
||||
|
||||
class PackageModel(MappedBase):
|
||||
class PackageModel(ModelMixin):
|
||||
"""
|
||||
套餐模型 - 定义租户可用的功能套餐
|
||||
|
||||
status: 0=正常 1=禁用
|
||||
"""
|
||||
|
||||
__tablename__: str = "platform_package"
|
||||
__table_args__: dict[str, str] = {"comment": "租户套餐表"}
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment="主键ID")
|
||||
name: Mapped[str] = mapped_column(String(100), nullable=False, unique=True, comment="套餐名称")
|
||||
code: Mapped[str] = mapped_column(String(100), nullable=False, unique=True, comment="套餐编码")
|
||||
status: Mapped[str] = mapped_column(
|
||||
String(10), nullable=False, default="0", comment="状态(0:正常 1:禁用)"
|
||||
)
|
||||
sort: Mapped[int] = mapped_column(Integer, nullable=False, default=0, comment="排序")
|
||||
description: Mapped[str | None] = mapped_column(
|
||||
String(255), nullable=True, default=None, comment="描述"
|
||||
)
|
||||
create_time: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=datetime.now, nullable=False, comment="创建时间"
|
||||
)
|
||||
update_time: Mapped[datetime] = mapped_column(
|
||||
DateTime, default=datetime.now, onupdate=datetime.now, nullable=False, comment="更新时间"
|
||||
# ─── 定价与计费 ───
|
||||
price: Mapped[int] = mapped_column(Integer, nullable=False, default=0, comment="价格(分)")
|
||||
period: Mapped[str] = mapped_column(String(10), nullable=False, default="month", comment="计费周期(month/year)")
|
||||
trial_days: Mapped[int] = mapped_column(Integer, nullable=False, default=0, comment="免费试用天数")
|
||||
# ─── 配额限制 ───
|
||||
max_users: Mapped[int] = mapped_column(Integer, nullable=False, default=10, comment="最大用户数")
|
||||
max_roles: Mapped[int] = mapped_column(Integer, nullable=False, default=5, comment="最大角色数")
|
||||
max_depts: Mapped[int] = mapped_column(Integer, nullable=False, default=10, comment="最大部门数")
|
||||
max_storage_mb: Mapped[int] = mapped_column(Integer, nullable=False, default=1024, comment="最大存储(MB)")
|
||||
# ─── 速率限制 ───
|
||||
rate_limit: Mapped[int] = mapped_column(
|
||||
Integer, nullable=False, default=60, comment="API速率限制(请求/10秒)"
|
||||
)
|
||||
|
||||
@validates("name")
|
||||
@@ -67,8 +67,36 @@ class PackageMenuModel(MappedBase):
|
||||
)
|
||||
menu_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("sys_menu.id", ondelete="CASCADE", onupdate="CASCADE"),
|
||||
ForeignKey("platform_menu.id", ondelete="CASCADE", onupdate="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="菜单ID",
|
||||
)
|
||||
|
||||
|
||||
class PackagePluginModel(MappedBase):
|
||||
"""
|
||||
套餐-插件关联表 — 定义套餐包含的插件资源
|
||||
"""
|
||||
|
||||
__tablename__: str = "platform_package_plugin"
|
||||
__table_args__ = (
|
||||
UniqueConstraint("package_id", "plugin_id", name="uq_package_plugin"),
|
||||
{"comment": "套餐插件关联表"},
|
||||
)
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True, comment="主键ID")
|
||||
package_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("platform_package.id", ondelete="CASCADE", onupdate="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="套餐ID",
|
||||
)
|
||||
plugin_id: Mapped[int] = mapped_column(
|
||||
Integer,
|
||||
ForeignKey("platform_plugin.id", ondelete="CASCADE", onupdate="CASCADE"),
|
||||
nullable=False,
|
||||
index=True,
|
||||
comment="插件ID",
|
||||
)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from fastapi import Query
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
||||
|
||||
@@ -10,9 +12,17 @@ class PackageCreateSchema(BaseModel):
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=100, description="套餐名称")
|
||||
code: str = Field(..., min_length=2, max_length=100, description="套餐编码")
|
||||
status: str = Field(default="0", max_length=1, description="状态(0:正常 1:禁用)")
|
||||
status: int = Field(default=0, ge=0, le=1, description="状态(0:正常 1:禁用)")
|
||||
sort: int = Field(default=0, ge=0, description="排序")
|
||||
description: str | None = Field(default=None, max_length=255, description="描述")
|
||||
price: int = Field(default=0, ge=0, description="价格(分)")
|
||||
period: str = Field(default="month", pattern=r"^(month|year)$", description="计费周期")
|
||||
trial_days: int = Field(default=0, ge=0, description="免费试用天数")
|
||||
max_users: int = Field(default=10, ge=0, description="最大用户数")
|
||||
max_roles: int = Field(default=5, ge=0, description="最大角色数")
|
||||
max_depts: int = Field(default=10, ge=0, description="最大部门数")
|
||||
max_storage_mb: int = Field(default=1024, ge=0, description="最大存储(MB)")
|
||||
rate_limit: int = Field(default=60, ge=10, description="API速率限制(请求/10秒)")
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
@@ -34,20 +44,28 @@ class PackageCreateSchema(BaseModel):
|
||||
|
||||
@field_validator("status")
|
||||
@classmethod
|
||||
def _validate_status(cls, v: str) -> str:
|
||||
if v not in {"0", "1"}:
|
||||
def _validate_status(cls, v: int) -> int:
|
||||
if v not in {0, 1}:
|
||||
raise ValueError("状态仅支持 0(正常) 或 1(禁用)")
|
||||
return v
|
||||
|
||||
|
||||
class PackageUpdateSchema(BaseModel):
|
||||
class PackageUpdateSchema(PackageCreateSchema):
|
||||
"""更新套餐"""
|
||||
|
||||
name: str | None = Field(default=None, max_length=100, description="套餐名称")
|
||||
code: str | None = Field(default=None, max_length=100, description="套餐编码")
|
||||
status: str | None = Field(default=None, max_length=1, description="状态(0:正常 1:禁用)")
|
||||
name: str | None = Field(default=None, max_length=100, description="套餐名称") # type: ignore[assignment]
|
||||
code: str | None = Field(default=None, max_length=100, description="套餐编码") # type: ignore[assignment]
|
||||
status: int | None = Field(default=None, ge=0, le=1, description="状态(0:正常 1:禁用)")
|
||||
sort: int | None = Field(default=None, ge=0, description="排序")
|
||||
description: str | None = Field(default=None, max_length=255, description="描述")
|
||||
price: int | None = Field(default=None, ge=0, description="价格(分)")
|
||||
period: str | None = Field(default=None, pattern=r"^(month|year)$", description="计费周期")
|
||||
trial_days: int | None = Field(default=None, ge=0, description="免费试用天数")
|
||||
max_users: int | None = Field(default=None, ge=0, description="最大用户数")
|
||||
max_roles: int | None = Field(default=None, ge=0, description="最大角色数")
|
||||
max_depts: int | None = Field(default=None, ge=0, description="最大部门数")
|
||||
max_storage_mb: int | None = Field(default=None, ge=0, description="最大存储(MB)")
|
||||
rate_limit: int | None = Field(default=None, ge=10, description="API速率限制(请求/10秒)")
|
||||
|
||||
@field_validator("code")
|
||||
@classmethod
|
||||
@@ -61,10 +79,10 @@ class PackageUpdateSchema(BaseModel):
|
||||
|
||||
@field_validator("status")
|
||||
@classmethod
|
||||
def _validate_status(cls, v: str | None) -> str | None:
|
||||
def _validate_status(cls, v: int | None) -> int | None:
|
||||
if v is None:
|
||||
return v
|
||||
if v not in {"0", "1"}:
|
||||
if v not in {0, 1}:
|
||||
raise ValueError("状态仅支持 0(正常) 或 1(禁用)")
|
||||
return v
|
||||
|
||||
@@ -75,6 +93,7 @@ class PackageOutSchema(PackageCreateSchema, BaseSchema):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PackageQueryParam:
|
||||
"""套餐查询参数"""
|
||||
|
||||
@@ -96,3 +115,9 @@ class PackageMenuSetSchema(BaseModel):
|
||||
"""批量设置套餐菜单权限"""
|
||||
|
||||
menu_ids: list[int] = Field(..., description="菜单ID列表")
|
||||
|
||||
|
||||
class PackagePluginSetSchema(BaseModel):
|
||||
"""批量设置套餐插件"""
|
||||
|
||||
plugin_ids: list[int] = Field(..., description="插件ID列表")
|
||||
|
||||
@@ -2,16 +2,17 @@ import sqlalchemy as sa
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from app.api.v1.module_platform.tenant.model import TenantModel
|
||||
from app.api.v1.module_system.auth.schema import AuthSchema
|
||||
from app.core.base_schema import AuthSchema
|
||||
from app.core.exceptions import CustomException
|
||||
from app.core.logger import log
|
||||
from app.core.logger import logger
|
||||
|
||||
from .crud import PackageCRUD
|
||||
from .model import PackageMenuModel, PackageModel
|
||||
from .model import PackageMenuModel, PackageModel, PackagePluginModel
|
||||
from .schema import (
|
||||
PackageCreateSchema,
|
||||
PackageMenuSetSchema,
|
||||
PackageOutSchema,
|
||||
PackagePluginSetSchema,
|
||||
PackageQueryParam,
|
||||
PackageUpdateSchema,
|
||||
)
|
||||
@@ -21,11 +22,11 @@ class PackageService:
|
||||
"""套餐模块服务层"""
|
||||
|
||||
@classmethod
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> dict:
|
||||
obj = await PackageCRUD(auth).get_by_id_crud(id=id)
|
||||
async def detail_service(cls, auth: AuthSchema, id: int) -> PackageOutSchema:
|
||||
obj = await PackageCRUD(auth).get(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="套餐不存在")
|
||||
return PackageOutSchema.model_validate(obj).model_dump()
|
||||
return PackageOutSchema.model_validate(obj)
|
||||
|
||||
@classmethod
|
||||
async def page_service(
|
||||
@@ -36,7 +37,7 @@ class PackageService:
|
||||
search: PackageQueryParam | None = None,
|
||||
order_by: list[dict[str, str]] | None = None,
|
||||
) -> dict:
|
||||
return await PackageCRUD(auth).page_crud(
|
||||
return await PackageCRUD(auth).page(
|
||||
offset=(page_no - 1) * page_size,
|
||||
limit=page_size,
|
||||
order_by=order_by or [{"sort": "asc"}, {"id": "asc"}],
|
||||
@@ -45,24 +46,22 @@ class PackageService:
|
||||
)
|
||||
|
||||
@classmethod
|
||||
async def create_service(cls, auth: AuthSchema, data: PackageCreateSchema) -> dict:
|
||||
async def create_service(cls, auth: AuthSchema, data: PackageCreateSchema) -> PackageOutSchema:
|
||||
if await PackageCRUD(auth).get(name=data.name):
|
||||
raise CustomException(msg="创建失败,套餐名称已存在")
|
||||
if await PackageCRUD(auth).get(code=data.code):
|
||||
raise CustomException(msg="创建失败,套餐编码已存在")
|
||||
|
||||
obj = await PackageCRUD(auth).create_crud(data=data)
|
||||
if not obj:
|
||||
raise CustomException(msg="创建套餐失败")
|
||||
result = PackageOutSchema.model_validate(obj).model_dump()
|
||||
log.info(f"创建套餐成功: {result.get('name')}")
|
||||
obj = await PackageCRUD(auth).create(data=data)
|
||||
result = PackageOutSchema.model_validate(obj)
|
||||
logger.info(f"创建套餐成功: {result.name}")
|
||||
return result
|
||||
|
||||
@classmethod
|
||||
async def update_service(
|
||||
cls, auth: AuthSchema, id: int, data: PackageUpdateSchema
|
||||
) -> dict:
|
||||
obj = await PackageCRUD(auth).get_by_id_crud(id=id)
|
||||
) -> PackageOutSchema:
|
||||
obj = await PackageCRUD(auth).get(id=id)
|
||||
if not obj:
|
||||
raise CustomException(msg="套餐不存在")
|
||||
|
||||
@@ -75,10 +74,13 @@ class PackageService:
|
||||
if exist and exist.id != id:
|
||||
raise CustomException(msg="更新失败,编码重复")
|
||||
|
||||
updated = await PackageCRUD(auth).update_crud(id=id, data=data)
|
||||
if not updated:
|
||||
raise CustomException(msg="更新失败")
|
||||
return PackageOutSchema.model_validate(updated).model_dump()
|
||||
# 套餐禁用时记录受影响租户
|
||||
if data.status is not None and data.status == 1 and obj.status == 0:
|
||||
await cls.disable_cascade_service(auth=auth, package_id=id)
|
||||
|
||||
updated = await PackageCRUD(auth).update(id=id, data=data)
|
||||
|
||||
return PackageOutSchema.model_validate(updated)
|
||||
|
||||
@classmethod
|
||||
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||||
@@ -92,7 +94,20 @@ class PackageService:
|
||||
if count and count > 0:
|
||||
raise CustomException(msg=f"套餐 ID={pid} 已被 {count} 个租户使用,无法删除")
|
||||
|
||||
await PackageCRUD(auth).delete_crud(ids=ids)
|
||||
await PackageCRUD(auth).delete(ids=ids)
|
||||
|
||||
@classmethod
|
||||
async def disable_cascade_service(cls, auth: AuthSchema, package_id: int) -> None:
|
||||
"""套餐禁用时日志记录受影响租户"""
|
||||
stmt = select(TenantModel.id, TenantModel.name).where(
|
||||
TenantModel.package_id == package_id,
|
||||
TenantModel.status == 0,
|
||||
)
|
||||
result = await auth.db.execute(stmt)
|
||||
rows = result.all()
|
||||
if rows:
|
||||
tenant_ids = [row[0] for row in rows]
|
||||
logger.warning(f"套餐[{package_id}]已禁用,影响租户: {tenant_ids}")
|
||||
|
||||
@classmethod
|
||||
async def get_menus_service(cls, auth: AuthSchema, package_id: int) -> list[int]:
|
||||
@@ -105,12 +120,13 @@ class PackageService:
|
||||
async def set_menus_service(
|
||||
cls, auth: AuthSchema, package_id: int, data: PackageMenuSetSchema
|
||||
) -> None:
|
||||
|
||||
"""批量设置套餐菜单权限(先清空再写入)"""
|
||||
await auth.db.execute(sa.delete(PackageMenuModel).where(PackageMenuModel.package_id == package_id))
|
||||
for menu_id in data.menu_ids:
|
||||
auth.db.add(PackageMenuModel(package_id=package_id, menu_id=menu_id))
|
||||
await auth.db.flush()
|
||||
log.info(f"套餐[{package_id}]菜单权限已设置, count={len(data.menu_ids)}")
|
||||
logger.info(f"套餐[{package_id}]菜单权限已设置, count={len(data.menu_ids)}")
|
||||
|
||||
@staticmethod
|
||||
async def get_package_menu_ids(auth: AuthSchema, package_id: int) -> list[int]:
|
||||
@@ -121,8 +137,17 @@ class PackageService:
|
||||
|
||||
@staticmethod
|
||||
async def get_tenant_available_menu_ids(auth: AuthSchema, tenant_id: int) -> list[int]:
|
||||
"""获取租户的完整可用菜单ID列表(套餐菜单 + 自定义授权菜单)"""
|
||||
from app.api.v1.module_platform.tenant.model import TenantMenuModel, TenantModel
|
||||
"""获取租户的完整可用菜单ID列表(仅从套餐菜单获取)
|
||||
|
||||
平台租户 (id=1) 返回全部启用菜单,不受套餐限制。
|
||||
"""
|
||||
from app.api.v1.module_platform.menu.model import MenuModel
|
||||
from app.api.v1.module_platform.tenant.model import TenantModel
|
||||
|
||||
if tenant_id == 1:
|
||||
menu_stmt = select(MenuModel.id).where(MenuModel.status == 0)
|
||||
result = await auth.db.execute(menu_stmt)
|
||||
return [row[0] for row in result.all()]
|
||||
|
||||
stmt = select(TenantModel).where(TenantModel.id == tenant_id).limit(1)
|
||||
result = await auth.db.execute(stmt)
|
||||
@@ -130,21 +155,60 @@ class PackageService:
|
||||
if not tenant:
|
||||
return []
|
||||
|
||||
all_menu_ids: set[int] = set()
|
||||
if not tenant.package_id:
|
||||
return []
|
||||
|
||||
if tenant.package_id:
|
||||
pkg_stmt = select(PackageModel.status).where(PackageModel.id == tenant.package_id).limit(1)
|
||||
pkg_result = await auth.db.execute(pkg_stmt)
|
||||
pkg_status = pkg_result.scalar_one_or_none()
|
||||
if pkg_status == "0":
|
||||
stmt = select(PackageMenuModel.menu_id).where(PackageMenuModel.package_id == tenant.package_id)
|
||||
result = await auth.db.execute(stmt)
|
||||
for row in result.all():
|
||||
all_menu_ids.add(row[0])
|
||||
pkg_stmt = select(PackageModel.status).where(PackageModel.id == tenant.package_id).limit(1)
|
||||
pkg_result = await auth.db.execute(pkg_stmt)
|
||||
pkg_status = pkg_result.scalar_one_or_none()
|
||||
if pkg_status != 0:
|
||||
return []
|
||||
|
||||
stmt = select(TenantMenuModel.menu_id).where(TenantMenuModel.tenant_id == tenant_id)
|
||||
menu_stmt = select(PackageMenuModel.menu_id).where(PackageMenuModel.package_id == tenant.package_id)
|
||||
result = await auth.db.execute(menu_stmt)
|
||||
return [row[0] for row in result.all()]
|
||||
|
||||
@staticmethod
|
||||
async def get_tenant_available_plugin_ids(auth: AuthSchema, tenant_id: int) -> list[int]:
|
||||
"""获取租户套餐可用的插件ID列表"""
|
||||
from app.api.v1.module_platform.tenant.model import TenantModel
|
||||
|
||||
stmt = select(TenantModel).where(TenantModel.id == tenant_id).limit(1)
|
||||
result = await auth.db.execute(stmt)
|
||||
for row in result.all():
|
||||
all_menu_ids.add(row[0])
|
||||
tenant = result.scalar_one_or_none()
|
||||
if not tenant or not tenant.package_id:
|
||||
return []
|
||||
|
||||
return list(all_menu_ids)
|
||||
pkg_stmt = select(PackageModel.status).where(PackageModel.id == tenant.package_id).limit(1)
|
||||
pkg_result = await auth.db.execute(pkg_stmt)
|
||||
pkg_status = pkg_result.scalar_one_or_none()
|
||||
if pkg_status != 0:
|
||||
return []
|
||||
|
||||
plugin_stmt = select(PackagePluginModel.plugin_id).where(
|
||||
PackagePluginModel.package_id == tenant.package_id
|
||||
)
|
||||
result = await auth.db.execute(plugin_stmt)
|
||||
return [row[0] for row in result.all()]
|
||||
|
||||
@classmethod
|
||||
async def get_plugins_service(cls, auth: AuthSchema, package_id: int) -> list[int]:
|
||||
"""获取套餐插件权限(返回 plugin_id 列表)"""
|
||||
stmt = select(PackagePluginModel.plugin_id).where(
|
||||
PackagePluginModel.package_id == package_id
|
||||
)
|
||||
result = await auth.db.execute(stmt)
|
||||
return [row[0] for row in result.all()]
|
||||
|
||||
@classmethod
|
||||
async def set_plugins_service(
|
||||
cls, auth: AuthSchema, package_id: int, data: PackagePluginSetSchema
|
||||
) -> None:
|
||||
"""批量设置套餐插件(先清空再写入)"""
|
||||
await auth.db.execute(
|
||||
sa.delete(PackagePluginModel).where(PackagePluginModel.package_id == package_id)
|
||||
)
|
||||
for plugin_id in data.plugin_ids:
|
||||
auth.db.add(PackagePluginModel(package_id=package_id, plugin_id=plugin_id))
|
||||
await auth.db.flush()
|
||||
logger.info(f"套餐[{package_id}]插件已设置, count={len(data.plugin_ids)}")
|
||||
|
||||
Reference in New Issue
Block a user