feat: add plugin support

This commit is contained in:
xboard 2025-01-18 17:12:07 +08:00
parent a6b68bb2e5
commit 43faab4e9c
17 changed files with 1134 additions and 268 deletions

View File

@ -14,6 +14,7 @@ use App\Services\CouponService;
use App\Services\OrderService;
use App\Services\PaymentService;
use App\Services\PlanService;
use App\Services\Plugin\HookManager;
use App\Services\UserService;
use App\Utils\Helper;
use Illuminate\Http\Request;
@ -112,6 +113,7 @@ class OrderController extends Controller
if (!$order->save()) {
throw new ApiException(__('Failed to create order'));
}
HookManager::call('order.after_create', $order);
return $this->success($order->trade_no);
});

View File

@ -0,0 +1,195 @@
<?php
namespace App\Http\Controllers\V2\Admin;
use App\Http\Controllers\Controller;
use App\Models\Plugin;
use App\Services\Plugin\PluginManager;
use App\Services\Plugin\PluginConfigService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
class PluginController extends Controller
{
protected PluginManager $pluginManager;
protected PluginConfigService $configService;
public function __construct(
PluginManager $pluginManager,
PluginConfigService $configService
) {
$this->pluginManager = $pluginManager;
$this->configService = $configService;
}
/**
* 获取插件列表
*/
public function index()
{
$installedPlugins = Plugin::get()
->keyBy('code')
->toArray();
$pluginPath = base_path('plugins');
$plugins = [];
if (File::exists($pluginPath)) {
$directories = File::directories($pluginPath);
foreach ($directories as $directory) {
$pluginName = basename($directory);
$configFile = $directory . '/config.json';
if (File::exists($configFile)) {
$config = json_decode(File::get($configFile), true);
$installed = isset($installedPlugins[$pluginName]);
// 使用配置服务获取配置
$pluginConfig = $installed ? $this->configService->getConfig($pluginName) : ($config['config'] ?? []);
$plugins[] = [
'code' => $config['code'],
'name' => $config['name'],
'version' => $config['version'],
'description' => $config['description'],
'author' => $config['author'],
'is_installed' => $installed,
'is_enabled' => $installed ? $installedPlugins[$pluginName]['is_enabled'] : false,
'config' => $pluginConfig,
];
}
}
}
return response()->json([
'data' => $plugins
]);
}
/**
* 安装插件
*/
public function install(Request $request)
{
$request->validate([
'code' => 'required|string'
]);
try {
$this->pluginManager->install($request->input('code'));
return response()->json([
'message' => '插件安装成功'
]);
} catch (\Exception $e) {
return response()->json([
'message' => '插件安装失败:' . $e->getMessage()
], 400);
}
}
/**
* 卸载插件
*/
public function uninstall(Request $request)
{
$request->validate([
'code' => 'required|string'
]);
try {
$this->pluginManager->uninstall($request->input('code'));
return response()->json([
'message' => '插件卸载成功'
]);
} catch (\Exception $e) {
return response()->json([
'message' => '插件卸载失败:' . $e->getMessage()
], 400);
}
}
/**
* 启用插件
*/
public function enable(Request $request)
{
$request->validate([
'code' => 'required|string'
]);
try {
$this->pluginManager->enable($request->input('code'));
return response()->json([
'message' => '插件启用成功'
]);
} catch (\Exception $e) {
return response()->json([
'message' => '插件启用失败:' . $e->getMessage()
], 400);
}
}
/**
* 禁用插件
*/
public function disable(Request $request)
{
$request->validate([
'code' => 'required|string'
]);
try {
$this->pluginManager->disable($request->input('code'));
return response()->json([
'message' => '插件禁用成功'
]);
} catch (\Exception $e) {
return response()->json([
'message' => '插件禁用失败:' . $e->getMessage()
], 400);
}
}
/**
* 获取插件配置
*/
public function getConfig(Request $request)
{
$request->validate([
'code' => 'required|string'
]);
try {
$config = $this->configService->getConfig($request->input('code'));
return response()->json([
'data' => $config
]);
} catch (\Exception $e) {
return response()->json([
'message' => '获取配置失败:' . $e->getMessage()
], 400);
}
}
/**
* 更新插件配置
*/
public function updateConfig(Request $request)
{
$request->validate([
'code' => 'required|string',
'config' => 'required|array'
]);
try {
$this->configService->updateConfig(
$request->input('code'),
$request->input('config')
);
return response()->json([
'message' => '配置更新成功'
]);
} catch (\Exception $e) {
return response()->json([
'message' => '配置更新失败:' . $e->getMessage()
], 400);
}
}
}

View File

@ -17,6 +17,7 @@ use App\Http\Controllers\V2\Admin\PaymentController;
use App\Http\Controllers\V2\Admin\SystemController;
use App\Http\Controllers\V2\Admin\ThemeController;
use Illuminate\Contracts\Routing\Registrar;
use Illuminate\Support\Facades\Route;
class AdminRoute
{
@ -202,6 +203,20 @@ class AdminRoute
$router->post('/saveThemeConfig', [ThemeController::class, 'saveThemeConfig']);
$router->post('/getThemeConfig', [ThemeController::class, 'getThemeConfig']);
});
// Plugin
$router->group([
'prefix' => 'plugin'
], function ($router) {
$router->get('/getPlugins', [\App\Http\Controllers\V2\Admin\PluginController::class, 'index']);
$router->post('install', [\App\Http\Controllers\V2\Admin\PluginController::class, 'install']);
$router->post('uninstall', [\App\Http\Controllers\V2\Admin\PluginController::class, 'uninstall']);
$router->post('enable', [\App\Http\Controllers\V2\Admin\PluginController::class, 'enable']);
$router->post('disable', [\App\Http\Controllers\V2\Admin\PluginController::class, 'disable']);
$router->get('config', [\App\Http\Controllers\V2\Admin\PluginController::class, 'getConfig']);
$router->post('config', [\App\Http\Controllers\V2\Admin\PluginController::class, 'updateConfig']);
});
});
}
}

16
app/Models/Plugin.php Normal file
View File

@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Plugin extends Model
{
protected $table = 'v2_plugins';
protected $guarded = [
'id',
'created_at',
'updated_at'
];
}

View File

@ -0,0 +1,41 @@
<?php
namespace App\Providers;
use App\Models\Plugin;
use App\Services\Plugin\PluginManager;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\DB;
class PluginServiceProvider extends ServiceProvider
{
public function register(): void
{
$this->app->scoped(PluginManager::class, function ($app) {
return new PluginManager();
});
}
public function boot(): void
{
if (!file_exists(base_path('plugins'))) {
mkdir(base_path('plugins'), 0755, true);
}
try {
$plugins = Plugin::query()
->where('is_enabled', true)
->get();
foreach ($plugins as $plugin) {
$manager = $this->app->make(PluginManager::class);
$manager->enable($plugin->code);
}
} catch (\Exception $e) {
\Log::error('Failed to load plugins: ' . $e->getMessage());
}
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Services\Plugin;
abstract class AbstractPlugin
{
protected array $config = [];
/**
* 插件启动时调用
*/
public function boot(): void
{
// 子类实现具体逻辑
}
/**
* 插件禁用时调用
*/
public function cleanup(): void
{
// 子类实现具体逻辑
}
/**
* 设置配置
*/
public function setConfig(array $config): void
{
$this->config = $config;
}
/**
* 获取配置
*/
public function getConfig(): array
{
return $this->config;
}
/**
* 注册事件监听器
*/
protected function listen(string $hook, callable $callback): void
{
HookManager::register($hook, $callback);
}
/**
* 移除事件监听器
*/
protected function removeListener(string $hook): void
{
HookManager::remove($hook);
}
}

View File

@ -0,0 +1,43 @@
<?php
namespace App\Services\Plugin;
use Illuminate\Support\Facades\Event;
class HookManager
{
/**
* 触发钩子
*
* @param string $hook 钩子名称
* @param mixed $payload 传递给钩子的数据
* @return mixed
*/
public static function call(string $hook, mixed $payload = null): mixed
{
return Event::dispatch($hook, [$payload]);
}
/**
* 注册钩子监听器
*
* @param string $hook 钩子名称
* @param callable $callback 回调函数
* @return void
*/
public static function register(string $hook, callable $callback): void
{
Event::listen($hook, $callback);
}
/**
* 移除钩子监听器
*
* @param string $hook 钩子名称
* @return void
*/
public static function remove(string $hook): void
{
Event::forget($hook);
}
}

View File

@ -0,0 +1,103 @@
<?php
namespace App\Services\Plugin;
use App\Models\Plugin;
use Illuminate\Support\Facades\File;
class PluginConfigService
{
/**
* 获取插件配置
*
* @param string $pluginCode
* @return array
*/
public function getConfig(string $pluginCode): array
{
$defaultConfig = $this->getDefaultConfig($pluginCode);
if (empty($defaultConfig)) {
return [];
}
$dbConfig = $this->getDbConfig($pluginCode);
$result = [];
foreach ($defaultConfig as $key => $item) {
$result[$key] = [
'type' => $item['type'],
'label' => $item['label'] ?? '',
'placeholder' => $item['placeholder'] ?? '',
'description' => $item['description'] ?? '',
'value' => $dbConfig[$key] ?? $item['default']
];
}
return $result;
}
/**
* 更新插件配置
*
* @param string $pluginCode
* @param array $config
* @return bool
*/
public function updateConfig(string $pluginCode, array $config): bool
{
$defaultConfig = $this->getDefaultConfig($pluginCode);
if (empty($defaultConfig)) {
throw new \Exception('插件配置结构不存在');
}
$values = [];
foreach ($config as $key => $value) {
if (!isset($defaultConfig[$key])) {
continue;
}
$values[$key] = $value;
}
Plugin::query()
->where('code', $pluginCode)
->update([
'config' => json_encode($values),
'updated_at' => now()
]);
return true;
}
/**
* 获取插件默认配置
*
* @param string $pluginCode
* @return array
*/
protected function getDefaultConfig(string $pluginCode): array
{
$configFile = base_path("plugins/{$pluginCode}/config.json");
if (!File::exists($configFile)) {
return [];
}
$config = json_decode(File::get($configFile), true);
return $config['config'] ?? [];
}
/**
* 获取数据库中的配置
*
* @param string $pluginCode
* @return array
*/
protected function getDbConfig(string $pluginCode): array
{
$plugin = Plugin::query()
->where('code', $pluginCode)
->first();
if (!$plugin || empty($plugin->config)) {
return [];
}
return json_decode($plugin->config, true);
}
}

View File

@ -0,0 +1,180 @@
<?php
namespace App\Services\Plugin;
use App\Models\Plugin;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\File;
class PluginManager
{
protected string $pluginPath;
public function __construct()
{
$this->pluginPath = base_path('plugins');
}
/**
* 安装插件
*/
public function install(string $pluginCode): bool
{
$configFile = $this->pluginPath . '/' . $pluginCode . '/config.json';
if (!File::exists($configFile)) {
throw new \Exception('Plugin config file not found');
}
$config = json_decode(File::get($configFile), true);
if (!$this->validateConfig($config)) {
throw new \Exception('Invalid plugin config');
}
// 检查依赖
if (!$this->checkDependencies($config['require'] ?? [])) {
throw new \Exception('Dependencies not satisfied');
}
// 提取配置默认值
$defaultValues = [];
if (isset($config['config']) && is_array($config['config'])) {
foreach ($config['config'] as $key => $item) {
$defaultValues[$key] = $item['default'] ?? null;
}
}
// 注册到数据库
Plugin::create([
'code' => $pluginCode,
'name' => $config['name'],
'version' => $config['version'],
'is_enabled' => false,
'config' => json_encode($defaultValues),
'installed_at' => now(),
]);
return true;
}
/**
* 启用插件
*/
public function enable(string $pluginCode): bool
{
$plugin = $this->loadPlugin($pluginCode);
if (!$plugin) {
throw new \Exception('Plugin not found');
}
// 获取插件配置
$dbPlugin = Plugin::query()
->where('code', $pluginCode)
->first();
if ($dbPlugin && !empty($dbPlugin->config)) {
$plugin->setConfig(json_decode($dbPlugin->config, true));
}
// 更新数据库状态
Plugin::query()
->where('code', $pluginCode)
->update([
'is_enabled' => true,
'updated_at' => now(),
]);
// 加载路由
$routesFile = $this->pluginPath . '/' . $pluginCode . '/routes/web.php';
if (File::exists($routesFile)) {
require $routesFile;
}
// 初始化插件
if (method_exists($plugin, 'boot')) {
$plugin->boot();
}
return true;
}
/**
* 禁用插件
*/
public function disable(string $pluginCode): bool
{
$plugin = $this->loadPlugin($pluginCode);
if (!$plugin) {
throw new \Exception('Plugin not found');
}
// 更新数据库状态
Plugin::query()
->where('code', $pluginCode)
->update([
'is_enabled' => false,
'updated_at' => now(),
]);
// 清理插件
if (method_exists($plugin, 'cleanup')) {
$plugin->cleanup();
}
return true;
}
/**
* 卸载插件
*/
public function uninstall(string $pluginCode): bool
{
// 先禁用插件
$this->disable($pluginCode);
// 删除数据库记录
Plugin::query()->where('code', $pluginCode)->delete();
return true;
}
/**
* 加载插件实例
*/
protected function loadPlugin(string $pluginCode)
{
$pluginFile = $this->pluginPath . '/' . $pluginCode . '/Plugin.php';
if (!File::exists($pluginFile)) {
return null;
}
require_once $pluginFile;
$className = "Plugin\\{$pluginCode}\\Plugin";
return new $className();
}
/**
* 验证配置文件
*/
protected function validateConfig(array $config): bool
{
return isset($config['code'])
&& isset($config['version'])
&& isset($config['description'])
&& isset($config['author']);
}
/**
* 检查依赖关系
*/
protected function checkDependencies(array $requires): bool
{
foreach ($requires as $package => $version) {
if ($package === 'xboard') {
// 检查xboard版本
// 实现版本比较逻辑
}
}
return true;
}
}

View File

@ -0,0 +1,33 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('v2_plugins', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('code')->unique();
$table->string('version', 50);
$table->boolean('is_enabled')->default(false);
$table->json('config')->nullable();
$table->timestamp('installed_at')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('v2_plugins');
}
};

2
plugins/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*
!.gitignore

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -151,6 +151,7 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"systemConfig": "System Configuration",
"themeConfig": "Theme Configuration",
"noticeManagement": "Notice Management",
"pluginManagement": "Plugin Management",
"paymentConfig": "Payment Configuration",
"knowledgeManagement": "Knowledge Management",
"nodeManagement": "Node Management",
@ -163,6 +164,58 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
"userManagement": "User Management",
"ticketManagement": "Ticket Management"
},
"plugin": {
"title": "Plugin Management",
"description": "Manage and configure system plugins",
"search": {
"placeholder": "Search plugin name or description..."
},
"category": {
"placeholder": "Select Category",
"all": "All",
"other": "Other"
},
"tabs": {
"all": "All Plugins",
"installed": "Installed",
"available": "Available"
},
"status": {
"enabled": "Enabled",
"disabled": "Disabled"
},
"button": {
"install": "Install",
"config": "Configure",
"enable": "Enable",
"disable": "Disable"
},
"uninstall": {
"title": "Uninstall Plugin",
"description": "Are you sure you want to uninstall this plugin? Plugin data will be cleared after uninstallation.",
"button": "Uninstall"
},
"config": {
"title": "Configuration",
"description": "Modify plugin configuration",
"save": "Save",
"cancel": "Cancel"
},
"author": "Author",
"messages": {
"installSuccess": "Plugin installed successfully",
"installError": "Failed to install plugin",
"uninstallSuccess": "Plugin uninstalled successfully",
"uninstallError": "Failed to uninstall plugin",
"enableSuccess": "Plugin enabled successfully",
"enableError": "Failed to enable plugin",
"disableSuccess": "Plugin disabled successfully",
"disableError": "Failed to disable plugin",
"configLoadError": "Failed to load plugin configuration",
"configSaveSuccess": "Configuration saved successfully",
"configSaveError": "Failed to save configuration"
}
},
"settings": {
"title": "System Settings",
"description": "Manage core system configurations, including site, security, subscription, invite commission, nodes, email, and notifications",

View File

@ -151,6 +151,7 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
"systemConfig": "시스템 설정",
"themeConfig": "테마 설정",
"noticeManagement": "공지사항 관리",
"pluginManagement": "플러그인 관리",
"paymentConfig": "결제 설정",
"knowledgeManagement": "지식 관리",
"nodeManagement": "노드 관리",
@ -163,6 +164,58 @@ window.XBOARD_TRANSLATIONS['ko-KR'] = {
"userManagement": "사용자 관리",
"ticketManagement": "티켓 관리"
},
"plugin": {
"title": "플러그인 관리",
"description": "시스템 플러그인 관리 및 설정",
"search": {
"placeholder": "플러그인 이름 또는 설명 검색..."
},
"category": {
"placeholder": "카테고리 선택",
"all": "전체",
"other": "기타"
},
"tabs": {
"all": "전체 플러그인",
"installed": "설치됨",
"available": "사용 가능"
},
"status": {
"enabled": "활성화됨",
"disabled": "비활성화됨"
},
"button": {
"install": "설치",
"config": "설정",
"enable": "활성화",
"disable": "비활성화"
},
"uninstall": {
"title": "플러그인 제거",
"description": "이 플러그인을 제거하시겠습니까? 제거 후 플러그인 데이터가 삭제됩니다.",
"button": "제거"
},
"config": {
"title": "설정",
"description": "플러그인 설정 수정",
"save": "저장",
"cancel": "취소"
},
"author": "작성자",
"messages": {
"installSuccess": "플러그인이 성공적으로 설치되었습니다",
"installError": "플러그인 설치에 실패했습니다",
"uninstallSuccess": "플러그인이 성공적으로 제거되었습니다",
"uninstallError": "플러그인 제거에 실패했습니다",
"enableSuccess": "플러그인이 성공적으로 활성화되었습니다",
"enableError": "플러그인 활성화에 실패했습니다",
"disableSuccess": "플러그인이 성공적으로 비활성화되었습니다",
"disableError": "플러그인 비활성화에 실패했습니다",
"configLoadError": "플러그인 설정을 불러오는데 실패했습니다",
"configSaveSuccess": "설정이 성공적으로 저장되었습니다",
"configSaveError": "설정 저장에 실패했습니다"
}
},
"settings": {
"title": "시스템 설정",
"description": "사이트, 보안, 구독, 초대 수수료, 노드, 이메일 및 알림을 포함한 핵심 시스템 구성을 관리합니다",

View File

@ -150,6 +150,7 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"systemManagement": "系统管理",
"systemConfig": "系统配置",
"themeConfig": "主题配置",
"pluginManagement": "插件管理",
"noticeManagement": "公告管理",
"paymentConfig": "支付配置",
"knowledgeManagement": "知识库管理",
@ -163,6 +164,58 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"userManagement": "用户管理",
"ticketManagement": "工单管理"
},
"plugin": {
"title": "插件管理",
"description": "管理和配置系统插件",
"search": {
"placeholder": "搜索插件名称或描述..."
},
"category": {
"placeholder": "选择分类",
"all": "全部",
"other": "其他"
},
"tabs": {
"all": "全部插件",
"installed": "已安装",
"available": "可用插件"
},
"status": {
"enabled": "已启用",
"disabled": "已禁用"
},
"button": {
"install": "安装",
"config": "配置",
"enable": "启用",
"disable": "禁用"
},
"uninstall": {
"title": "卸载插件",
"description": "确定要卸载该插件吗?卸载后插件数据将被清除。",
"button": "卸载"
},
"config": {
"title": "配置",
"description": "修改插件配置",
"save": "保存",
"cancel": "取消"
},
"author": "作者",
"messages": {
"installSuccess": "插件安装成功",
"installError": "插件安装失败",
"uninstallSuccess": "插件卸载成功",
"uninstallError": "插件卸载失败",
"enableSuccess": "插件启用成功",
"enableError": "插件启用失败",
"disableSuccess": "插件禁用成功",
"disableError": "插件禁用失败",
"configLoadError": "加载插件配置失败",
"configSaveSuccess": "配置保存成功",
"configSaveError": "配置保存失败"
}
},
"settings": {
"title": "系统设置",
"description": "管理系统核心配置,包括站点、安全、订阅、邀请佣金、节点、邮件和通知等设置",
@ -1941,6 +1994,7 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
"dashboard": "仪表盘",
"systemManagement": "系统管理",
"systemConfig": "系统配置",
"pluginManagement": "插件管理",
"themeConfig": "主题配置",
"noticeManagement": "公告管理",
"paymentConfig": "支付配置",