Files
FastapiAdmin/backend/app/api/v1/module_platform/plugin/service.py
T
zhangtao d34d4a4c50 chore: 完成多批次代码优化与重构
- 重构工作流模块目录结构,迁移代码文件
- 修复类型断言空值安全问题,添加 ! 操作符
- 优化样式类名,替换 flex-cc 为标准 flex 工具类
- 更新路由标签简化文案,移除冗余注释
- 调整 ruff 配置,放宽行长度限制
- 更新 README 与多语言文案,优化项目描述
- 修复表单、图表组件的类型与样式问题
- 简化搜索表单、数据卡片的布局代码
2026-06-20 05:31:46 +08:00

217 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from datetime import datetime
import sqlalchemy as sa
from app.core.base_schema import AuthSchema
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:
@classmethod
async def page_service(
cls,
auth: AuthSchema,
page_no: int,
page_size: int,
search: PluginQueryParam | None = None,
order_by: list | None = None,
) -> dict:
return await PluginCRUD(auth).page(
offset=(page_no - 1) * page_size,
limit=page_size,
order_by=order_by or [{"sort": "asc"}],
search=search.__dict__ if search else {},
out_schema=PluginOutSchema,
)
@classmethod
async def detail_service(cls, auth: AuthSchema, id: int) -> PluginOutSchema:
obj = await PluginCRUD(auth).get(id=id)
if not obj:
raise CustomException(msg="插件不存在")
return PluginOutSchema.model_validate(obj)
@classmethod
async def create_service(cls, 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
async def update_service(cls, auth: AuthSchema, id: int, data: PluginUpdateSchema) -> PluginOutSchema:
obj = await PluginCRUD(auth).get(id=id)
if not obj:
raise CustomException(msg="插件不存在")
updated = await PluginCRUD(auth).update(id=id, data=data)
return PluginOutSchema.model_validate(updated)
@classmethod
async def delete_service(cls, auth: AuthSchema, ids: list[int]) -> 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:
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, "0") == "1"
return result
@classmethod
async def install_service(cls, auth: AuthSchema, plugin_id: int) -> 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 tp_record.purchased != "1":
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="0")
)
else:
tp = TenantPluginModel(
tenant_id=tenant_id,
plugin_id=plugin_id,
enabled="0",
purchased="1" if getattr(plugin, "price", 0) == 0 else "0",
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:
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:
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 = "1" if tp.enabled == "0" else "0"
await auth.db.flush()
logger.info(f"租户[{tenant_id}]插件[{plugin_id}]状态→{tp.enabled}")
@classmethod
async def my_plugins_service(cls, 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} 条路由"