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 操作)""" def __init__(self, auth: AuthSchema) -> None: self.auth = auth @require_superadmin async def page( self, page_no: int, page_size: int, search: PluginQueryParam | None = None, order_by: list | None = None, ) -> dict: return await PluginCRUD(self.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, ) @require_superadmin async def detail(self, id: int) -> PluginOutSchema: return await PluginCRUD(self.auth).get_or_404(id=id, out_schema=PluginOutSchema) @require_superadmin async def create(self, data: PluginCreateSchema) -> PluginOutSchema: if await PluginCRUD(self.auth).get(code=data.code): raise CustomException(msg="创建失败,插件编码已存在") obj = await PluginCRUD(self.auth).create(data=data) return PluginOutSchema.model_validate(obj) @require_superadmin async def update(self, id: int, data: PluginUpdateSchema) -> PluginOutSchema: _ = await PluginCRUD(self.auth).get_or_404(id=id) updated = await PluginCRUD(self.auth).update(id=id, data=data) return PluginOutSchema.model_validate(updated) @require_superadmin async def delete(self, ids: list[int]) -> None: await PluginCRUD(self.auth).delete(ids=ids) async def marketplace(self, page_no: int, page_size: int, category: str | None = None) -> dict: search: dict = {"status": ("eq", "0")} if category: search["category"] = ("eq", category) result = await PluginCRUD(self.auth).page( offset=(page_no - 1) * page_size, limit=page_size, order_by=[{"sort": "asc"}], search=search, out_schema=PluginOutSchema, ) tenant_id = getattr(self.auth, "tenant_id", None) or self.auth.user.tenant_id if tenant_id and result.items: records = await self.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 async def install(self, plugin_id: int) -> None: tenant_id = getattr(self.auth, "tenant_id", None) or self.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(self.auth).get_tenant_available_plugin_ids(tenant_id) if allowed_plugin_ids and plugin_id not in allowed_plugin_ids: raise CustomException(msg="当前套餐不支持安装此插件") plugin = await PluginCRUD(self.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 self.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 self.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 self.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(), ) self.auth.db.add(tp) await self.auth.db.flush() logger.info(f"租户[{tenant_id}]安装插件[{plugin.name}]") async def uninstall(self, plugin_id: int) -> None: tenant_id = getattr(self.auth, "tenant_id", None) or self.auth.user.tenant_id if not tenant_id: raise CustomException(msg="无法获取租户信息") await self.auth.db.execute( sa.delete(TenantPluginModel).where( TenantPluginModel.tenant_id == tenant_id, TenantPluginModel.plugin_id == plugin_id, ) ) await self.auth.db.flush() logger.info(f"租户[{tenant_id}]卸载插件[{plugin_id}]") async def toggle(self, plugin_id: int) -> None: tenant_id = getattr(self.auth, "tenant_id", None) or self.auth.user.tenant_id tp = await self.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 self.auth.db.flush() logger.info(f"租户[{tenant_id}]插件[{plugin_id}]状态→{tp.enabled}") async def my_plugins(self) -> list[dict]: tenant_id = getattr(self.auth, "tenant_id", None) or self.auth.user.tenant_id if not tenant_id: return [] result = await self.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 @staticmethod def reload() -> 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} 条路由"