mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-24 05:26:58 +00:00
本次提交包含多项优化和修复: 1. 修复CRUD初始化参数传递、搜索参数处理逻辑 2. 更新环境配置中的大模型相关参数 3. 重构部分服务方法命名,统一代码风格 4. 新增多个枚举类型,补充模型关联关系和加载选项 5. 优化查询参数类实现,完善字段校验逻辑 6. 调整Pydantic模型字段注释和类型定义 7. 简化并移除冗余的CRUD方法实现 8. 新增超级管理员权限装饰器 9. 修复邮件日志模型的租户关联和字段定义
327 lines
11 KiB
Python
327 lines
11 KiB
Python
from datetime import datetime
|
||
|
||
import sqlalchemy as sa
|
||
|
||
from app.core.base_schema import AuthSchema
|
||
from app.core.dependencies import require_superadmin
|
||
from app.core.exceptions import CustomException
|
||
from app.core.logger import logger
|
||
|
||
from .crud import PluginCRUD
|
||
from .model import PluginModel, TenantPluginModel
|
||
from .schema import PluginCreateSchema, PluginOutSchema, PluginQueryParam, PluginUpdateSchema
|
||
|
||
|
||
class PluginService:
|
||
"""
|
||
插件管理服务(仅超级管理员可操作 CRUD,租户通过 marketplace/install/uninstall/toggle/my 操作)
|
||
"""
|
||
|
||
@classmethod
|
||
@require_superadmin
|
||
async def page_service(
|
||
cls,
|
||
auth: AuthSchema,
|
||
page_no: int,
|
||
page_size: int,
|
||
search: PluginQueryParam | None = None,
|
||
order_by: list | None = None,
|
||
) -> dict:
|
||
"""
|
||
分页查询插件
|
||
|
||
参数:
|
||
- auth (AuthSchema): 认证信息模型
|
||
- page_no (int): 页码
|
||
- page_size (int): 每页数量
|
||
- search (PluginQueryParam | None): 查询参数
|
||
- order_by (list | None): 排序参数
|
||
|
||
返回:
|
||
- dict: 分页数据
|
||
"""
|
||
return await PluginCRUD(auth).page(
|
||
offset=(page_no - 1) * page_size,
|
||
limit=page_size,
|
||
order_by=order_by or [{"sort": "asc"}],
|
||
search=vars(search) if search else None,
|
||
out_schema=PluginOutSchema,
|
||
)
|
||
|
||
@classmethod
|
||
@require_superadmin
|
||
async def detail_service(cls, auth: AuthSchema, id: int) -> PluginOutSchema:
|
||
"""
|
||
插件详情
|
||
|
||
参数:
|
||
- auth (AuthSchema): 认证信息模型
|
||
- id (int): 插件ID
|
||
|
||
返回:
|
||
- PluginOutSchema: 插件详情
|
||
"""
|
||
return await PluginCRUD(auth).get_or_404(id=id, out_schema=PluginOutSchema)
|
||
|
||
@classmethod
|
||
@require_superadmin
|
||
async def create_service(cls, auth: AuthSchema, data: PluginCreateSchema) -> PluginOutSchema:
|
||
"""
|
||
创建插件
|
||
|
||
参数:
|
||
- auth (AuthSchema): 认证信息模型
|
||
- data (PluginCreateSchema): 插件创建模型
|
||
|
||
返回:
|
||
- PluginOutSchema: 插件详情
|
||
"""
|
||
if await PluginCRUD(auth).get(code=data.code):
|
||
raise CustomException(msg="创建失败,插件编码已存在")
|
||
obj = await PluginCRUD(auth).create(data=data)
|
||
return PluginOutSchema.model_validate(obj)
|
||
|
||
@classmethod
|
||
@require_superadmin
|
||
async def update_service(cls, auth: AuthSchema, id: int, data: PluginUpdateSchema) -> PluginOutSchema:
|
||
"""
|
||
更新插件
|
||
|
||
参数:
|
||
- auth (AuthSchema): 认证信息模型
|
||
- id (int): 插件ID
|
||
- data (PluginUpdateSchema): 插件更新模型
|
||
|
||
返回:
|
||
- PluginOutSchema: 插件详情
|
||
"""
|
||
_ = await PluginCRUD(auth).get_or_404(id=id)
|
||
updated = await PluginCRUD(auth).update(id=id, data=data)
|
||
return PluginOutSchema.model_validate(updated)
|
||
|
||
@classmethod
|
||
@require_superadmin
|
||
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> None:
|
||
"""
|
||
删除插件
|
||
|
||
参数:
|
||
- auth (AuthSchema): 认证信息模型
|
||
- ids (list[int]): 插件ID列表
|
||
|
||
返回:
|
||
- None
|
||
"""
|
||
await PluginCRUD(auth).delete(ids=ids)
|
||
|
||
# ───── 插件市场 API ─────
|
||
|
||
@classmethod
|
||
async def marketplace_service(cls, auth: AuthSchema, page_no: int, page_size: int, category: str | None = None) -> dict:
|
||
"""
|
||
插件市场列表(租户端)
|
||
|
||
参数:
|
||
- auth (AuthSchema): 认证信息模型
|
||
- page_no (int): 页码
|
||
- page_size (int): 每页数量
|
||
- category (str | None): 分类筛选
|
||
|
||
返回:
|
||
- dict: 分页数据(含租户的 installed/purchased 标记)
|
||
"""
|
||
search = {}
|
||
if category:
|
||
search["category"] = ("eq", category)
|
||
search["status"] = ("eq", "0")
|
||
result = await PluginCRUD(auth).page(
|
||
offset=(page_no - 1) * page_size,
|
||
limit=page_size,
|
||
order_by=[{"sort": "asc"}],
|
||
search=search,
|
||
out_schema=PluginOutSchema,
|
||
)
|
||
tenant_id = getattr(auth, "tenant_id", None) or auth.user.tenant_id
|
||
if tenant_id and result.items:
|
||
records = await auth.db.execute(
|
||
sa.select(TenantPluginModel.plugin_id, TenantPluginModel.purchased).where(
|
||
TenantPluginModel.tenant_id == tenant_id,
|
||
)
|
||
)
|
||
record_map = {r[0]: r[1] for r in records.all()}
|
||
for item in result.items:
|
||
pid = item["id"]
|
||
item["installed"] = pid in record_map
|
||
item["purchased"] = record_map.get(pid, False)
|
||
return result
|
||
|
||
@classmethod
|
||
async def install_service(cls, auth: AuthSchema, plugin_id: int) -> None:
|
||
"""
|
||
安装插件
|
||
|
||
参数:
|
||
- auth (AuthSchema): 认证信息模型
|
||
- plugin_id (int): 插件ID
|
||
|
||
返回:
|
||
- None
|
||
"""
|
||
tenant_id = getattr(auth, "tenant_id", None) or auth.user.tenant_id
|
||
if not tenant_id:
|
||
raise CustomException(msg="无法获取租户信息")
|
||
|
||
# 平台租户不限制插件安装
|
||
if tenant_id != 1:
|
||
from app.api.v1.module_platform.package.service import PackageService
|
||
|
||
allowed_plugin_ids = await PackageService.get_tenant_available_plugin_ids(auth, tenant_id)
|
||
# 套餐有插件配置时,仅允许安装套餐内的插件
|
||
if allowed_plugin_ids and plugin_id not in allowed_plugin_ids:
|
||
raise CustomException(msg="当前套餐不支持安装此插件")
|
||
|
||
plugin = await PluginCRUD(auth).get(id=plugin_id)
|
||
if not plugin or plugin.status == 1:
|
||
raise CustomException(msg="该数据不存在")
|
||
|
||
# 付费插件需要先购买
|
||
if tenant_id != 1 and getattr(plugin, "price", 0) > 0:
|
||
exist = await auth.db.execute(
|
||
sa.select(TenantPluginModel)
|
||
.where(
|
||
TenantPluginModel.tenant_id == tenant_id,
|
||
TenantPluginModel.plugin_id == plugin_id,
|
||
)
|
||
.limit(1)
|
||
)
|
||
tp_record = exist.scalar_one_or_none()
|
||
if not tp_record or not tp_record.purchased:
|
||
raise CustomException(msg="此插件为付费插件,请先购买后再安装")
|
||
|
||
exist = await auth.db.execute(
|
||
sa.select(TenantPluginModel)
|
||
.where(
|
||
TenantPluginModel.tenant_id == tenant_id,
|
||
TenantPluginModel.plugin_id == plugin_id,
|
||
)
|
||
.limit(1)
|
||
)
|
||
if exist.scalar_one_or_none():
|
||
await auth.db.execute(
|
||
sa.update(TenantPluginModel)
|
||
.where(
|
||
TenantPluginModel.tenant_id == tenant_id,
|
||
TenantPluginModel.plugin_id == plugin_id,
|
||
)
|
||
.values(enabled=False)
|
||
)
|
||
else:
|
||
tp = TenantPluginModel(
|
||
tenant_id=tenant_id,
|
||
plugin_id=plugin_id,
|
||
enabled=False,
|
||
purchased=True if getattr(plugin, "price", 0) == 0 else False,
|
||
installed_time=datetime.now(),
|
||
)
|
||
auth.db.add(tp)
|
||
await auth.db.flush()
|
||
logger.info(f"租户[{tenant_id}]安装插件[{plugin.name}]")
|
||
|
||
@classmethod
|
||
async def uninstall_service(cls, auth: AuthSchema, plugin_id: int) -> None:
|
||
"""
|
||
卸载插件
|
||
|
||
参数:
|
||
- auth (AuthSchema): 认证信息模型
|
||
- plugin_id (int): 插件ID
|
||
|
||
返回:
|
||
- None
|
||
"""
|
||
tenant_id = getattr(auth, "tenant_id", None) or auth.user.tenant_id
|
||
if not tenant_id:
|
||
raise CustomException(msg="无法获取租户信息")
|
||
await auth.db.execute(
|
||
sa.delete(TenantPluginModel).where(
|
||
TenantPluginModel.tenant_id == tenant_id,
|
||
TenantPluginModel.plugin_id == plugin_id,
|
||
)
|
||
)
|
||
await auth.db.flush()
|
||
logger.info(f"租户[{tenant_id}]卸载插件[{plugin_id}]")
|
||
|
||
@classmethod
|
||
async def toggle_service(cls, auth: AuthSchema, plugin_id: int) -> None:
|
||
"""
|
||
启用/禁用插件
|
||
|
||
参数:
|
||
- auth (AuthSchema): 认证信息模型
|
||
- plugin_id (int): 插件ID
|
||
|
||
返回:
|
||
- None
|
||
"""
|
||
tenant_id = getattr(auth, "tenant_id", None) or auth.user.tenant_id
|
||
tp = await auth.db.execute(
|
||
sa.select(TenantPluginModel)
|
||
.where(
|
||
TenantPluginModel.tenant_id == tenant_id,
|
||
TenantPluginModel.plugin_id == plugin_id,
|
||
)
|
||
.limit(1)
|
||
)
|
||
tp = tp.scalar_one_or_none()
|
||
if not tp:
|
||
raise CustomException(msg="未安装该插件")
|
||
tp.enabled = not tp.enabled
|
||
await auth.db.flush()
|
||
logger.info(f"租户[{tenant_id}]插件[{plugin_id}]状态→{tp.enabled}")
|
||
|
||
@classmethod
|
||
async def my_plugins_service(cls, auth: AuthSchema) -> list[dict]:
|
||
"""
|
||
查询我的插件列表(租户端)
|
||
|
||
参数:
|
||
- auth (AuthSchema): 认证信息模型
|
||
|
||
返回:
|
||
- list[dict]: 已安装插件列表
|
||
"""
|
||
tenant_id = getattr(auth, "tenant_id", None) or auth.user.tenant_id
|
||
if not tenant_id:
|
||
return []
|
||
result = await auth.db.execute(
|
||
sa.select(PluginModel, TenantPluginModel).join(TenantPluginModel, TenantPluginModel.plugin_id == PluginModel.id).where(TenantPluginModel.tenant_id == tenant_id).order_by(PluginModel.sort)
|
||
)
|
||
plugins = []
|
||
for p, tp in result.all():
|
||
d = PluginOutSchema.model_validate(p)
|
||
d["enabled"] = tp.enabled
|
||
d["installed"] = True
|
||
d["installed_time"] = tp.installed_time.strftime("%Y-%m-%d %H:%M") if tp.installed_time else ""
|
||
plugins.append(d)
|
||
return plugins
|
||
|
||
# ───── 插件热重载 ─────
|
||
|
||
@classmethod
|
||
def reload_service(cls) -> str:
|
||
"""
|
||
热重载插件路由(同步函数,直接操作模块缓存与 app.routes)。
|
||
|
||
要求 init_app.py 已在启动时调用 discover.set_app_ref(app),
|
||
否则仅重建 Router 对象,不会更新运行中的 app 路由。
|
||
|
||
返回:
|
||
- str: 重载结果描述
|
||
"""
|
||
from app.core.discover import reload_dynamic_router
|
||
|
||
router = reload_dynamic_router()
|
||
route_count = len(router.routes) if router and hasattr(router, "routes") else 0
|
||
logger.info(f"插件热重载完成,动态路由共 {route_count} 条")
|
||
return f"插件路由热重载完成,共注册 {route_count} 条路由"
|