mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-09-26 06:19:05 +00:00
* 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: 更新后端依赖文件
537 lines
15 KiB
Python
537 lines
15 KiB
Python
from .naming import PluginScaffoldNaming
|
|
from .options import PluginScaffoldOptions
|
|
|
|
|
|
class PluginBackendScaffoldTemplateBuilder:
|
|
"""
|
|
后端插件模板内容构建器。
|
|
"""
|
|
|
|
@staticmethod
|
|
def build_manifest(plugin_id: str, options: PluginScaffoldOptions) -> str:
|
|
"""
|
|
构建后端插件清单内容。
|
|
|
|
:param plugin_id: 插件ID
|
|
:param options: 插件模板生成选项
|
|
:return: 后端插件清单内容
|
|
"""
|
|
migrations = ' []' if not options.migration else '\n - migrations/001_init.sql'
|
|
seeds = ' []' if not options.seed else '\n - seeds/001_seed.sql'
|
|
jobs = (
|
|
' []'
|
|
if not options.job
|
|
else f"""
|
|
- id: heartbeat
|
|
name: {plugin_id} 心跳
|
|
callable: plugins.{plugin_id}.jobs.heartbeat
|
|
trigger: cron
|
|
cronExpression: '0 0/30 * * * ?'
|
|
enabled: false
|
|
description: {plugin_id} 插件定时任务声明示例"""
|
|
)
|
|
config = (
|
|
''
|
|
if not options.config
|
|
else """
|
|
|
|
config:
|
|
items:
|
|
- key: enabled_feature
|
|
label: 示例开关
|
|
type: boolean
|
|
default: true
|
|
required: false
|
|
description: 示例插件开关
|
|
- key: api_key
|
|
label: 示例密钥
|
|
type: password
|
|
default: ''
|
|
required: false
|
|
secret: true
|
|
description: 敏感配置示例,请在安装后填写"""
|
|
)
|
|
frontend_menus = (
|
|
f"""
|
|
menus:
|
|
- name: {plugin_id}
|
|
path: {plugin_id}
|
|
component: plugin/{plugin_id}/index
|
|
perms: {plugin_id}:list
|
|
type: C
|
|
icon: '#'"""
|
|
if options.frontend
|
|
else """
|
|
menus: []"""
|
|
)
|
|
return f"""id: {plugin_id}
|
|
name: {plugin_id}
|
|
version: 0.1.0
|
|
description: {plugin_id} 插件
|
|
|
|
backend:
|
|
module: plugins.{plugin_id}
|
|
routers:
|
|
autoScan: true
|
|
migrations:{migrations}
|
|
seeds:{seeds}
|
|
hooks:
|
|
onInstall: hooks:on_install
|
|
onUpgrade: hooks:on_upgrade
|
|
onStartup: hooks:on_startup
|
|
onShutdown: hooks:on_shutdown
|
|
onPurge: hooks:on_purge
|
|
jobs:{jobs}
|
|
|
|
frontend:
|
|
basePath: {plugin_id}
|
|
pluginId: {plugin_id}
|
|
viewsPath: views
|
|
apiPath: api
|
|
{frontend_menus}
|
|
|
|
permissions:
|
|
- {plugin_id}:list
|
|
|
|
dependencies:
|
|
python: []
|
|
npm: []
|
|
npmDev: []
|
|
plugins: []
|
|
{config}
|
|
"""
|
|
|
|
@staticmethod
|
|
def build_controller(plugin_id: str) -> str:
|
|
"""
|
|
构建后端控制器模板内容。
|
|
|
|
:param plugin_id: 插件ID
|
|
:return: 后端控制器模板内容
|
|
"""
|
|
service_class_name = PluginScaffoldNaming.to_class_name(plugin_id)
|
|
return f"""from common.router import APIRouterPro
|
|
|
|
from plugins.{plugin_id}.service.{plugin_id}_service import {service_class_name}Service
|
|
|
|
router = APIRouterPro(prefix='/{plugin_id}', tags=['{plugin_id}'])
|
|
|
|
|
|
@router.get('/ping')
|
|
async def ping() -> dict[str, str]:
|
|
\"\"\"
|
|
插件探活接口。
|
|
|
|
:return: 插件探活结果
|
|
\"\"\"
|
|
return {service_class_name}Service.ping()
|
|
"""
|
|
|
|
@staticmethod
|
|
def build_crud_controller(plugin_id: str) -> str:
|
|
"""
|
|
构建后端 CRUD 控制器模板内容。
|
|
|
|
:param plugin_id: 插件ID
|
|
:return: 后端 CRUD 控制器模板内容
|
|
"""
|
|
service_class_name = PluginScaffoldNaming.to_class_name(plugin_id)
|
|
return f"""from common.router import APIRouterPro
|
|
|
|
from plugins.{plugin_id}.service.{plugin_id}_service import {service_class_name}Service
|
|
|
|
router = APIRouterPro(prefix='/{plugin_id}', tags=['{plugin_id}'])
|
|
|
|
|
|
@router.get('/ping')
|
|
async def ping() -> dict[str, str]:
|
|
\"\"\"
|
|
插件探活接口。
|
|
|
|
:return: 插件探活结果
|
|
\"\"\"
|
|
return {service_class_name}Service.ping()
|
|
|
|
|
|
@router.get('/items')
|
|
async def list_items(keyword: str = '') -> dict[str, object]:
|
|
\"\"\"
|
|
查询示例数据列表。
|
|
|
|
:param keyword: 名称关键字
|
|
:return: 示例数据分页结果
|
|
\"\"\"
|
|
return {service_class_name}Service.list_items(keyword)
|
|
|
|
|
|
@router.post('/items')
|
|
async def create_item(payload: dict[str, object]) -> dict[str, object]:
|
|
\"\"\"
|
|
创建示例数据。
|
|
|
|
:param payload: 示例数据负载
|
|
:return: 创建后的示例数据
|
|
\"\"\"
|
|
return {service_class_name}Service.create_item(payload)
|
|
|
|
|
|
@router.put('/items/{{item_id}}')
|
|
async def update_item(item_id: int, payload: dict[str, object]) -> dict[str, object]:
|
|
\"\"\"
|
|
更新示例数据。
|
|
|
|
:param item_id: 示例数据ID
|
|
:param payload: 示例数据负载
|
|
:return: 更新后的示例数据
|
|
\"\"\"
|
|
return {service_class_name}Service.update_item(item_id, payload)
|
|
|
|
|
|
@router.delete('/items/{{item_id}}')
|
|
async def delete_item(item_id: int) -> dict[str, object]:
|
|
\"\"\"
|
|
删除示例数据。
|
|
|
|
:param item_id: 示例数据ID
|
|
:return: 删除结果
|
|
\"\"\"
|
|
return {service_class_name}Service.delete_item(item_id)
|
|
"""
|
|
|
|
@staticmethod
|
|
def build_service(plugin_id: str) -> str:
|
|
"""
|
|
构建后端服务模板内容。
|
|
|
|
:param plugin_id: 插件ID
|
|
:return: 后端服务模板内容
|
|
"""
|
|
service_class_name = PluginScaffoldNaming.to_class_name(plugin_id)
|
|
return f"""class {service_class_name}Service:
|
|
\"\"\"
|
|
{plugin_id} 插件服务。
|
|
\"\"\"
|
|
|
|
@classmethod
|
|
def ping(cls) -> dict[str, str]:
|
|
\"\"\"
|
|
返回插件探活结果。
|
|
|
|
:return: 插件探活结果
|
|
\"\"\"
|
|
return {{'message': '{plugin_id} plugin ok'}}
|
|
"""
|
|
|
|
@staticmethod
|
|
def build_crud_service(plugin_id: str) -> str:
|
|
"""
|
|
构建后端 CRUD 服务模板内容。
|
|
|
|
:param plugin_id: 插件ID
|
|
:return: 后端 CRUD 服务模板内容
|
|
"""
|
|
service_class_name = PluginScaffoldNaming.to_class_name(plugin_id)
|
|
return f"""class {service_class_name}Service:
|
|
\"\"\"
|
|
{plugin_id} 插件 CRUD 示例服务。
|
|
|
|
第一版模板使用内存数据演示 controller/service 分层,实际业务可替换为 dao/entity 实现。
|
|
\"\"\"
|
|
|
|
_items = [
|
|
{{'itemId': 1, 'itemName': '{plugin_id} 示例', 'status': '0', 'remark': '插件 CRUD 模板数据'}},
|
|
]
|
|
|
|
@classmethod
|
|
def ping(cls) -> dict[str, str]:
|
|
\"\"\"
|
|
返回插件探活结果。
|
|
|
|
:return: 插件探活结果
|
|
\"\"\"
|
|
return {{'message': '{plugin_id} plugin ok'}}
|
|
|
|
@classmethod
|
|
def list_items(cls, keyword: str = '') -> dict[str, object]:
|
|
\"\"\"
|
|
查询示例数据列表。
|
|
|
|
:param keyword: 名称关键字
|
|
:return: 示例数据分页结果
|
|
\"\"\"
|
|
rows = [
|
|
item
|
|
for item in cls._items
|
|
if not keyword or keyword.lower() in str(item.get('itemName', '')).lower()
|
|
]
|
|
return {{'rows': rows, 'total': len(rows)}}
|
|
|
|
@classmethod
|
|
def create_item(cls, payload: dict[str, object]) -> dict[str, object]:
|
|
\"\"\"
|
|
创建示例数据。
|
|
|
|
:param payload: 示例数据负载
|
|
:return: 创建后的示例数据
|
|
\"\"\"
|
|
next_id = max([int(item['itemId']) for item in cls._items], default=0) + 1
|
|
item = {{
|
|
'itemId': next_id,
|
|
'itemName': str(payload.get('itemName') or '未命名'),
|
|
'status': str(payload.get('status') or '0'),
|
|
'remark': str(payload.get('remark') or ''),
|
|
}}
|
|
cls._items.append(item)
|
|
return item
|
|
|
|
@classmethod
|
|
def update_item(cls, item_id: int, payload: dict[str, object]) -> dict[str, object]:
|
|
\"\"\"
|
|
更新示例数据。
|
|
|
|
:param item_id: 示例数据ID
|
|
:param payload: 示例数据负载
|
|
:return: 更新后的示例数据
|
|
\"\"\"
|
|
for item in cls._items:
|
|
if item['itemId'] != item_id:
|
|
continue
|
|
item.update(
|
|
{{
|
|
'itemName': str(payload.get('itemName') or item.get('itemName')),
|
|
'status': str(payload.get('status') or item.get('status')),
|
|
'remark': str(payload.get('remark') or ''),
|
|
}}
|
|
)
|
|
return item
|
|
return {{'itemId': item_id, 'itemName': '', 'status': '1', 'remark': 'not found'}}
|
|
|
|
@classmethod
|
|
def delete_item(cls, item_id: int) -> dict[str, object]:
|
|
\"\"\"
|
|
删除示例数据。
|
|
|
|
:param item_id: 示例数据ID
|
|
:return: 删除结果
|
|
\"\"\"
|
|
before_count = len(cls._items)
|
|
cls._items = [item for item in cls._items if item['itemId'] != item_id]
|
|
return {{'deleted': len(cls._items) < before_count, 'itemId': item_id}}
|
|
"""
|
|
|
|
@staticmethod
|
|
def build_hooks(plugin_id: str) -> str:
|
|
"""
|
|
构建后端生命周期钩子模板内容。
|
|
|
|
:param plugin_id: 插件ID
|
|
:return: 后端生命周期钩子模板内容
|
|
"""
|
|
return f"""from plugins.core.runtime.hooks import PluginHookContext
|
|
from utils.log_util import logger
|
|
|
|
|
|
async def on_install(context: PluginHookContext) -> None:
|
|
\"\"\"
|
|
插件安装生命周期钩子。
|
|
|
|
:param context: 插件生命周期钩子上下文
|
|
:return: None
|
|
\"\"\"
|
|
logger.info('{plugin_id} plugin install hook executed')
|
|
|
|
|
|
async def on_upgrade(context: PluginHookContext) -> None:
|
|
\"\"\"
|
|
插件升级生命周期钩子。
|
|
|
|
:param context: 插件生命周期钩子上下文
|
|
:return: None
|
|
\"\"\"
|
|
logger.info('{plugin_id} plugin upgrade hook executed')
|
|
|
|
|
|
async def on_startup(context: PluginHookContext) -> None:
|
|
\"\"\"
|
|
插件启动生命周期钩子。
|
|
|
|
:param context: 插件生命周期钩子上下文
|
|
:return: None
|
|
\"\"\"
|
|
logger.info('{plugin_id} plugin startup hook executed')
|
|
|
|
|
|
async def on_shutdown(context: PluginHookContext) -> None:
|
|
\"\"\"
|
|
插件关闭生命周期钩子。
|
|
|
|
:param context: 插件生命周期钩子上下文
|
|
:return: None
|
|
\"\"\"
|
|
logger.info('{plugin_id} plugin shutdown hook executed')
|
|
|
|
|
|
async def on_purge(context: PluginHookContext) -> None:
|
|
\"\"\"
|
|
插件物理清理生命周期钩子。
|
|
|
|
:param context: 插件生命周期钩子上下文
|
|
:return: None
|
|
\"\"\"
|
|
logger.info('{plugin_id} plugin purge hook executed')
|
|
"""
|
|
|
|
@staticmethod
|
|
def build_jobs(plugin_id: str) -> str:
|
|
"""
|
|
构建后端定时任务模板内容。
|
|
|
|
:param plugin_id: 插件ID
|
|
:return: 后端定时任务模板内容
|
|
"""
|
|
return f"""from utils.log_util import logger
|
|
|
|
|
|
def heartbeat() -> None:
|
|
\"\"\"
|
|
插件心跳定时任务。
|
|
|
|
:return: None
|
|
\"\"\"
|
|
logger.info('{plugin_id} plugin heartbeat job executed')
|
|
"""
|
|
|
|
@staticmethod
|
|
def build_migration(plugin_id: str) -> str:
|
|
"""
|
|
构建后端 migration 模板内容。
|
|
|
|
:param plugin_id: 插件ID
|
|
:return: 后端 migration 模板内容
|
|
"""
|
|
return f"""-- {plugin_id} plugin initial migration.
|
|
-- Add plugin tables or schema changes here.
|
|
"""
|
|
|
|
@staticmethod
|
|
def build_seed(plugin_id: str) -> str:
|
|
"""
|
|
构建后端 seed 模板内容。
|
|
|
|
:param plugin_id: 插件ID
|
|
:return: 后端 seed 模板内容
|
|
"""
|
|
return f"""-- {plugin_id} plugin initial seed.
|
|
-- Add idempotent initialization data here.
|
|
"""
|
|
|
|
@staticmethod
|
|
def build_test(plugin_id: str) -> str:
|
|
"""
|
|
构建后端插件 pytest 样例。
|
|
|
|
:param plugin_id: 插件ID
|
|
:return: 后端插件测试样例内容
|
|
"""
|
|
service_class_name = PluginScaffoldNaming.to_class_name(plugin_id)
|
|
return f"""from plugins.{plugin_id}.service.{plugin_id}_service import {service_class_name}Service
|
|
|
|
|
|
def test_{plugin_id}_service_ping() -> None:
|
|
\"\"\"
|
|
校验插件服务探活返回稳定负载。
|
|
|
|
:return: None
|
|
\"\"\"
|
|
assert {service_class_name}Service.ping() == {{'message': '{plugin_id} plugin ok'}}
|
|
"""
|
|
|
|
@staticmethod
|
|
def build_crud_test(plugin_id: str) -> str:
|
|
"""
|
|
构建后端插件 CRUD pytest 样例。
|
|
|
|
:param plugin_id: 插件ID
|
|
:return: 后端插件 CRUD 测试样例内容
|
|
"""
|
|
service_class_name = PluginScaffoldNaming.to_class_name(plugin_id)
|
|
return f"""from plugins.{plugin_id}.service.{plugin_id}_service import {service_class_name}Service
|
|
|
|
|
|
def test_{plugin_id}_service_ping() -> None:
|
|
\"\"\"
|
|
校验插件服务探活返回稳定负载。
|
|
|
|
:return: None
|
|
\"\"\"
|
|
assert {service_class_name}Service.ping() == {{'message': '{plugin_id} plugin ok'}}
|
|
|
|
|
|
def test_{plugin_id}_service_crud_flow() -> None:
|
|
\"\"\"
|
|
校验插件 CRUD 示例服务返回稳定负载。
|
|
|
|
:return: None
|
|
\"\"\"
|
|
created = {service_class_name}Service.create_item({{'itemName': '测试数据', 'status': '0'}})
|
|
listed = {service_class_name}Service.list_items('测试')
|
|
updated = {service_class_name}Service.update_item(created['itemId'], {{'itemName': '测试数据2'}})
|
|
deleted = {service_class_name}Service.delete_item(created['itemId'])
|
|
|
|
assert listed['total'] >= 1
|
|
assert updated['itemName'] == '测试数据2'
|
|
assert deleted['deleted'] is True
|
|
"""
|
|
|
|
@staticmethod
|
|
def build_readme(plugin_id: str) -> str:
|
|
"""
|
|
构建后端 README 内容。
|
|
|
|
:param plugin_id: 插件ID
|
|
:return: 后端 README 内容
|
|
"""
|
|
return f"""# {plugin_id} backend plugin
|
|
|
|
Backend plugin scaffold generated by `ruoyi plugin create`.
|
|
|
|
## Structure
|
|
|
|
- `plugin.yaml`: backend manifest, menus, permissions and dependencies.
|
|
- `controller/`: FastAPI routers discovered when the plugin is enabled.
|
|
- `service/`: plugin service classes.
|
|
- `dao/`: plugin data access classes.
|
|
- `entity/do/`: SQLAlchemy models imported before table creation.
|
|
- `entity/vo/`: Pydantic request and response models.
|
|
- `hooks.py`: lifecycle hook examples declared in `plugin.yaml`.
|
|
- `jobs.py`: scheduled job example declared in `plugin.yaml`.
|
|
- `migrations/`: database migration scripts declared in `plugin.yaml`.
|
|
- `seeds/`: initialization scripts declared in `plugin.yaml`.
|
|
- `tests/plugins/{plugin_id}/`: pytest examples for this plugin.
|
|
- frontend project `tests/plugins/{plugin_id}/`: frontend node tests for this plugin.
|
|
|
|
## Commands
|
|
|
|
```bash
|
|
ruoyi plugin check {plugin_id}
|
|
pytest tests/plugins/{plugin_id}
|
|
cd <frontend-project> && node tests/plugins/{plugin_id}/pluginView.test.js
|
|
ruoyi plugin install {plugin_id} --dry-run
|
|
ruoyi plugin install {plugin_id} --yes
|
|
ruoyi plugin enable {plugin_id} --yes
|
|
ruoyi plugin disable {plugin_id} --yes
|
|
ruoyi plugin upgrade {plugin_id} --dry-run
|
|
ruoyi plugin uninstall {plugin_id} --yes
|
|
ruoyi plugin purge {plugin_id} --dry-run
|
|
ruoyi plugin config get {plugin_id}
|
|
```
|
|
|
|
## Frontend View
|
|
|
|
The menu component `plugin/{plugin_id}/index` maps to:
|
|
|
|
```text
|
|
<frontend-project>/plugins/{plugin_id}/views/index.vue
|
|
```
|
|
"""
|