Files
FastapiAdmin/backend/app/api/v1/module_platform/plugin/service.py
T
zhangtao 73f2823692 refactor: 大规模代码整理与功能优化
1. 重构后端API路由、CRUD与模块结构,整合日志管理,移除废弃demo代码
2. 优化前端组件类型定义、样式与路由配置,修复权限判断逻辑
3. 调整默认排序规则、滚动条样式与工具类函数,更新依赖与配置文件
4. 修复多处类型不匹配与默认值问题,完善表单与菜单验证逻辑
2026-06-17 01:56:31 +08:00

231 lines
8.6 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} 条路由"