Files
RuoYi-Vue3-FastAPI/ruoyi-fastapi-backend/plugins/core/lifecycle/seed.py
T
insistence 2a055ba648 feat: 新增插件系统 (#112)
* feat: 初始化插件系统

* refactor: 收口插件系统运行时重构

* perf: 优化插件系统类型提示

* fix&perf: 修复和优化插件系统

* fix: 修复gitignore规则误忽略插件文件的问题

* fix: 修复运行时插件根路径算错的问题

* fix: 加强插件发现和路由注册的防护措施

* revert: 回滚定时任务白名单

* fix: 移除未使用的应用路由注册探测

* revert: 恢复部分代码

* perf: 优化插件系统

* docs: 新增插件开发文档

* perf: 优化插件管理模块

* perf: 提升插件系统核心能力

* refactor: 重构生命周期 step runner

* fix: 修复lint错误

* test: 清理测试用例

* test: 调整测试目录名称

* fix: 修复前后端目录硬编码的问题

* fix: 修复插件系统安全性缺口

* refactor: 重新设计插件生命周期 Migration 事务与回滚

* perf: 优化插件系统边界问题

* refactor: 重构当前插件系统的依赖体系设计

* perf: 优化代码

* perf: 优化代码

* fix: 修复代码合并问题

* fix: 修复bug

* perf: 优化代码

* perf&fix: 优化代码和修复bug

* docs: 优化文档格式

* feat: 适配Vue2版本

* docs: 更新README文档

* fix: 修复ruff lint错误

* chore: 更新后端依赖文件
2026-07-28 20:35:18 +08:00

166 lines
5.3 KiB
Python

import inspect
from dataclasses import dataclass
from pathlib import Path
from typing import Any
from sqlalchemy import text
from config.env import DataBaseConfig
from plugins.core.discovery.scanner import DiscoveredPlugin
from plugins.core.lifecycle.script import PluginLifecycleScriptHelper
SUPPORTED_SEED_SUFFIXES = {'.py', '.sql'}
@dataclass(frozen=True)
class PluginSeedResult:
"""
插件 seed 执行结果。
"""
seed_path: str
module_name: str
statement_count: int = 0
class PluginSeedRunner:
"""
插件 seed 运行器。
使用 Command Runner 模式按 manifest 声明顺序执行插件初始化脚本。
Python seed 模块需要暴露 `run(query_db)` 函数,SQL seed 会按分号拆分并逐条执行。
"""
def __init__(self, discovered_plugin: DiscoveredPlugin) -> None:
"""
初始化插件 seed 运行器。
:param discovered_plugin: 已发现插件对象
"""
self.discovered_plugin = discovered_plugin
async def run(self, query_db: Any) -> list[PluginSeedResult]:
"""
执行插件清单声明的 seed。
:param query_db: orm对象
:return: seed 执行结果列表
"""
return [
await self._run_seed(seed_path, query_db)
for seed_path in self._filter_current_database_seeds(self.discovered_plugin.manifest.backend.seeds)
]
async def _run_seed(self, seed_path: str, query_db: Any) -> PluginSeedResult:
"""
执行单个 seed。
:param seed_path: seed 相对插件根目录路径
:param query_db: orm对象
:return: seed 执行结果
"""
seed_file = self._resolve_seed_file(seed_path)
if seed_file.suffix == '.sql':
return await self._run_sql_seed(seed_path, seed_file, query_db)
return await self._run_python_seed(seed_path, seed_file, query_db)
async def _run_python_seed(self, seed_path: str, seed_file: Path, query_db: Any) -> PluginSeedResult:
"""
执行 Python seed。
:param seed_path: seed 相对插件根目录路径
:param seed_file: seed 文件绝对路径
:param query_db: orm对象
:return: seed 执行结果
"""
seed_module = self._load_seed_module(seed_file)
seed_runner = getattr(seed_module, 'run', None)
if not callable(seed_runner):
raise RuntimeError(f'插件 seed 必须暴露 run(query_db) 函数:{seed_path}')
result = seed_runner(query_db)
if inspect.isawaitable(result):
await result
return PluginSeedResult(seed_path=seed_path, module_name=seed_module.__name__)
async def _run_sql_seed(self, seed_path: str, seed_file: Path, query_db: Any) -> PluginSeedResult:
"""
执行 SQL seed。
:param seed_path: seed 相对插件根目录路径
:param seed_file: seed 文件绝对路径
:param query_db: orm对象
:return: seed 执行结果
"""
statements = self._load_sql_statements(seed_file)
for statement in statements:
await query_db.execute(text(statement))
return PluginSeedResult(
seed_path=seed_path,
module_name=self._build_seed_module_name(seed_file),
statement_count=len(statements),
)
def _load_sql_statements(self, seed_file: Path) -> list[str]:
"""
加载 SQL seed 语句列表。
:param seed_file: seed 文件绝对路径
:return: SQL 语句列表
"""
return PluginLifecycleScriptHelper.split_sql_statements(seed_file.read_text(encoding='utf-8'))
def _resolve_seed_file(self, seed_path: str) -> Path:
"""
解析 seed 文件绝对路径。
:param seed_path: seed 相对插件根目录路径
:return: seed 文件绝对路径
"""
return PluginLifecycleScriptHelper.resolve_file(
self.discovered_plugin.backend_path,
seed_path,
supported_suffixes=SUPPORTED_SEED_SUFFIXES,
label='seed',
)
@classmethod
def _filter_current_database_seeds(cls, seed_paths: list[str]) -> list[str]:
"""
过滤当前数据库方言不匹配的 seed。
:param seed_paths: seed 相对路径列表
:return: 当前数据库需要执行的 seed 列表
"""
return PluginLifecycleScriptHelper.filter_current_database_paths(
seed_paths,
root_dir='seeds',
database_type=DataBaseConfig.db_type,
)
def _load_seed_module(self, seed_file: Path) -> Any:
"""
加载 seed Python 模块。
:param seed_file: seed 文件绝对路径
:return: seed 模块
"""
module_name = self._build_seed_module_name(seed_file)
return PluginLifecycleScriptHelper.load_module(module_name, seed_file, label='seed')
def _build_seed_module_name(self, seed_file: Path) -> str:
"""
构建 seed 模块名。
:param seed_file: seed 文件绝对路径
:return: seed 模块名
"""
return PluginLifecycleScriptHelper.build_module_name(
self.discovered_plugin.manifest.id,
self.discovered_plugin.backend_path,
seed_file,
)