mirror of
https://github.com/fastapiadmin/FastapiAdmin.git
synced 2026-09-22 21:06:32 +00:00
1. 重构后端API路由、CRUD与模块结构,整合日志管理,移除废弃demo代码 2. 优化前端组件类型定义、样式与路由配置,修复权限判断逻辑 3. 调整默认排序规则、滚动条样式与工具类函数,更新依赖与配置文件 4. 修复多处类型不匹配与默认值问题,完善表单与菜单验证逻辑
231 lines
8.6 KiB
Python
231 lines
8.6 KiB
Python
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} 条路由"
|