mirror of
https://github.com/cedar2025/Xboard.git
synced 2025-01-22 10:38:14 -05:00
Compare commits
7 Commits
a6b68bb2e5
...
d81974d8bc
Author | SHA1 | Date | |
---|---|---|---|
|
d81974d8bc | ||
|
1298a6fbbc | ||
|
e6c33776e5 | ||
|
9f95f015a0 | ||
|
9973db24d0 | ||
|
81ef1b8909 | ||
|
43faab4e9c |
@ -53,11 +53,12 @@ class MigrateFromV2b extends Command
|
||||
],
|
||||
'1.7.3' => [
|
||||
'ALTER TABLE `v2_stat_order` RENAME TO `v2_stat`;',
|
||||
"ALTER TABLE `v2_stat` CHANGE COLUMN order_amount order_total INT COMMENT '订单合计';",
|
||||
"ALTER TABLE `v2_stat` CHANGE COLUMN order_amount paid_total INT COMMENT '订单合计';",
|
||||
"ALTER TABLE `v2_stat` CHANGE COLUMN order_count paid_count INT COMMENT '邀请佣金';",
|
||||
"ALTER TABLE `v2_stat` CHANGE COLUMN commission_amount commission_total INT COMMENT '佣金合计';",
|
||||
"ALTER TABLE `v2_stat`
|
||||
ADD COLUMN paid_count INT NULL,
|
||||
ADD COLUMN paid_total INT NULL,
|
||||
ADD COLUMN order_count INT NULL,
|
||||
ADD COLUMN order_total INT NULL,
|
||||
ADD COLUMN register_count INT NULL,
|
||||
ADD COLUMN invite_count INT NULL,
|
||||
ADD COLUMN transfer_used_total VARCHAR(32) NULL;
|
||||
|
@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Server;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ServerShadowsocks;
|
||||
use App\Services\ServerService;
|
||||
use App\Services\UserService;
|
||||
use App\Utils\CacheKey;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/*
|
||||
* Tidal Lab Shadowsocks
|
||||
* Github: https://github.com/tokumeikoi/tidalab-ss
|
||||
*/
|
||||
class ShadowsocksTidalabController extends Controller
|
||||
{
|
||||
// 后端获取用户
|
||||
public function user(Request $request)
|
||||
{
|
||||
ini_set('memory_limit', -1);
|
||||
$server = $request->input('node_info');
|
||||
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_CHECK_AT', $server->id), time(), 3600);
|
||||
$users = ServerService::getAvailableUsers($server->group_ids);
|
||||
$result = [];
|
||||
foreach ($users as $user) {
|
||||
array_push($result, [
|
||||
'id' => $user->id,
|
||||
'port' => $server->server_port,
|
||||
'cipher' => $server->cipher,
|
||||
'secret' => $user->uuid
|
||||
]);
|
||||
}
|
||||
$eTag = sha1(json_encode($result));
|
||||
if (strpos($request->header('If-None-Match'), $eTag) !== false ) {
|
||||
return response(null,304);
|
||||
}
|
||||
return response([
|
||||
'data' => $result
|
||||
])->header('ETag', "\"{$eTag}\"");
|
||||
}
|
||||
|
||||
// 后端提交数据
|
||||
public function submit(Request $request)
|
||||
{
|
||||
$server = $request->input('node_info');
|
||||
$data = json_decode(request()->getContent(), true);
|
||||
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_ONLINE_USER', $server->id), count($data), 3600);
|
||||
Cache::put(CacheKey::get('SERVER_SHADOWSOCKS_LAST_PUSH_AT', $server->id), time(), 3600);
|
||||
$userService = new UserService();
|
||||
$formatData = [];
|
||||
|
||||
foreach ($data as $item) {
|
||||
$formatData[$item['user_id']] = [$item['u'], $item['d']];
|
||||
}
|
||||
$userService->trafficFetch($server->toArray(), 'shadowsocks', $formatData);
|
||||
|
||||
return response([
|
||||
'ret' => 1,
|
||||
'msg' => 'ok'
|
||||
]);
|
||||
}
|
||||
}
|
108
app/Http/Controllers/V1/Server/TrojanTidalabController.php
Normal file
108
app/Http/Controllers/V1/Server/TrojanTidalabController.php
Normal file
@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V1\Server;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\ServerTrojan;
|
||||
use App\Services\ServerService;
|
||||
use App\Services\UserService;
|
||||
use App\Utils\CacheKey;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
|
||||
/*
|
||||
* Tidal Lab Trojan
|
||||
* Github: https://github.com/tokumeikoi/tidalab-trojan
|
||||
*/
|
||||
class TrojanTidalabController extends Controller
|
||||
{
|
||||
const TROJAN_CONFIG = '{"run_type":"server","local_addr":"0.0.0.0","local_port":443,"remote_addr":"www.taobao.com","remote_port":80,"password":[],"ssl":{"cert":"server.crt","key":"server.key","sni":"domain.com"},"api":{"enabled":true,"api_addr":"127.0.0.1","api_port":10000}}';
|
||||
|
||||
// 后端获取用户
|
||||
public function user(Request $request)
|
||||
{
|
||||
ini_set('memory_limit', -1);
|
||||
$server = $request->input('node_info');
|
||||
if ($server->type !== 'trojan') {
|
||||
return $this->fail([400, '节点不存在']);
|
||||
}
|
||||
Cache::put(CacheKey::get('SERVER_TROJAN_LAST_CHECK_AT', $server->id), time(), 3600);
|
||||
$users = ServerService::getAvailableUsers($server->group_id);
|
||||
$result = [];
|
||||
foreach ($users as $user) {
|
||||
$user->trojan_user = [
|
||||
"password" => $user->uuid,
|
||||
];
|
||||
unset($user->uuid);
|
||||
array_push($result, $user);
|
||||
}
|
||||
$eTag = sha1(json_encode($result));
|
||||
if (strpos($request->header('If-None-Match'), $eTag) !== false) {
|
||||
return response(null, 304);
|
||||
}
|
||||
return response([
|
||||
'msg' => 'ok',
|
||||
'data' => $result,
|
||||
])->header('ETag', "\"{$eTag}\"");
|
||||
}
|
||||
|
||||
// 后端提交数据
|
||||
public function submit(Request $request)
|
||||
{
|
||||
$server = $request->input('node_info');
|
||||
if ($server->type !== 'trojan') {
|
||||
return $this->fail([400, '节点不存在']);
|
||||
}
|
||||
$data = json_decode(request()->getContent(), true);
|
||||
Cache::put(CacheKey::get('SERVER_TROJAN_ONLINE_USER', $server->id), count($data), 3600);
|
||||
Cache::put(CacheKey::get('SERVER_TROJAN_LAST_PUSH_AT', $server->id), time(), 3600);
|
||||
$userService = new UserService();
|
||||
$formatData = [];
|
||||
foreach ($data as $item) {
|
||||
$formatData[$item['user_id']] = [$item['u'], $item['d']];
|
||||
}
|
||||
$userService->trafficFetch($server->toArray(), 'trojan', $formatData);
|
||||
|
||||
return response([
|
||||
'ret' => 1,
|
||||
'msg' => 'ok'
|
||||
]);
|
||||
}
|
||||
|
||||
// 后端获取配置
|
||||
public function config(Request $request)
|
||||
{
|
||||
$server = $request->input('node_info');
|
||||
if ($server->type !== 'trojan') {
|
||||
return $this->fail([400, '节点不存在']);
|
||||
}
|
||||
$request->validate([
|
||||
'node_id' => 'required',
|
||||
'local_port' => 'required'
|
||||
], [
|
||||
'node_id.required' => '节点ID不能为空',
|
||||
'local_port.required' => '本地端口不能为空'
|
||||
]);
|
||||
try {
|
||||
$json = $this->getTrojanConfig($server, $request->input('local_port'));
|
||||
} catch (\Exception $e) {
|
||||
\Log::error($e);
|
||||
return $this->fail([500, '配置获取失败']);
|
||||
}
|
||||
|
||||
return (json_encode($json, JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
|
||||
private function getTrojanConfig($server, int $localPort)
|
||||
{
|
||||
$protocolSettings = $server->protocol_settings;
|
||||
$json = json_decode(self::TROJAN_CONFIG);
|
||||
$json->local_port = $server->server_port;
|
||||
$json->ssl->sni = data_get($protocolSettings, 'server_name', $server->host);
|
||||
$json->ssl->cert = "/root/.cert/server.crt";
|
||||
$json->ssl->key = "/root/.cert/server.key";
|
||||
$json->api->api_port = $localPort;
|
||||
return $json;
|
||||
}
|
||||
}
|
@ -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);
|
||||
});
|
||||
|
195
app/Http/Controllers/V2/Admin/PluginController.php
Normal file
195
app/Http/Controllers/V2/Admin/PluginController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -41,7 +41,6 @@ class Server
|
||||
],
|
||||
'node_id' => 'required',
|
||||
'node_type' => [
|
||||
'required',
|
||||
'nullable',
|
||||
function ($attribute, $value, $fail) use ($request) {
|
||||
if (!ServerModel::isValidType($value)) {
|
||||
|
@ -24,6 +24,21 @@ class ServerRoute
|
||||
$route->post('alive', [UniProxyController::class, 'alive']);
|
||||
$route->get('alivelist', [UniProxyController::class, 'alivelist']);
|
||||
});
|
||||
$router->group([
|
||||
'prefix' => 'ShadowsocksTidalab',
|
||||
'middleware' => 'server:shadowsocks'
|
||||
], function ($route) {
|
||||
$route->get('user', [ShadowsocksTidalabController::class, 'user']);
|
||||
$route->post('submit', [ShadowsocksTidalabController::class, 'submit']);
|
||||
});
|
||||
$router->group([
|
||||
'prefix' => 'TrojanTidalab',
|
||||
'middleware' => 'server:trojan'
|
||||
], function ($route) {
|
||||
$route->get('config', [TrojanTidalabController::class, 'config']);
|
||||
$route->get('user', [TrojanTidalabController::class, 'user']);
|
||||
$route->post('submit', [TrojanTidalabController::class, 'submit']);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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
16
app/Models/Plugin.php
Normal 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'
|
||||
];
|
||||
}
|
@ -17,8 +17,19 @@ class Bind extends Telegram {
|
||||
}
|
||||
$subscribeUrl = $message->args[0];
|
||||
$subscribeUrl = parse_url($subscribeUrl);
|
||||
parse_str($subscribeUrl['query'], $query);
|
||||
$token = $query['token'];
|
||||
|
||||
// 首先尝试从查询参数获取token
|
||||
$token = null;
|
||||
if (isset($subscribeUrl['query'])) {
|
||||
parse_str($subscribeUrl['query'], $query);
|
||||
$token = $query['token'] ?? null;
|
||||
}
|
||||
|
||||
if (!$token && isset($subscribeUrl['path'])) {
|
||||
$pathParts = explode('/', trim($subscribeUrl['path'], '/'));
|
||||
$token = end($pathParts);
|
||||
}
|
||||
|
||||
if (!$token) {
|
||||
throw new ApiException('订阅地址无效');
|
||||
}
|
||||
|
41
app/Providers/PluginServiceProvider.php
Normal file
41
app/Providers/PluginServiceProvider.php
Normal 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());
|
||||
}
|
||||
}
|
||||
}
|
56
app/Services/Plugin/AbstractPlugin.php
Normal file
56
app/Services/Plugin/AbstractPlugin.php
Normal 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);
|
||||
}
|
||||
}
|
43
app/Services/Plugin/HookManager.php
Normal file
43
app/Services/Plugin/HookManager.php
Normal 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);
|
||||
}
|
||||
}
|
103
app/Services/Plugin/PluginConfigService.php
Normal file
103
app/Services/Plugin/PluginConfigService.php
Normal 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);
|
||||
}
|
||||
}
|
180
app/Services/Plugin/PluginManager.php
Normal file
180
app/Services/Plugin/PluginManager.php
Normal 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;
|
||||
}
|
||||
}
|
@ -176,6 +176,7 @@ return [
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
App\Providers\SettingServiceProvider::class,
|
||||
App\Providers\OctaneSchedulerProvider::class,
|
||||
App\Providers\PluginServiceProvider::class,
|
||||
|
||||
],
|
||||
|
||||
|
@ -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');
|
||||
}
|
||||
};
|
@ -29,7 +29,7 @@ docker compose run -it --rm \
|
||||
-e ENABLE_SQLITE=true \
|
||||
-e ENABLE_REDIS=true \
|
||||
-e ADMIN_ACCOUNT=admin@demo.com \
|
||||
web php artisan xboard:install && \
|
||||
web php artisan xboard:install
|
||||
```
|
||||
- 自定义配置安装(高级用户)
|
||||
```bash
|
||||
|
2
plugins/.gitignore
vendored
Normal file
2
plugins/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
*
|
||||
!.gitignore
|
2
public/assets/admin/assets/index.css
vendored
2
public/assets/admin/assets/index.css
vendored
File diff suppressed because one or more lines are too long
14
public/assets/admin/assets/index.js
vendored
14
public/assets/admin/assets/index.js
vendored
File diff suppressed because one or more lines are too long
540
public/assets/admin/assets/vendor.js
vendored
540
public/assets/admin/assets/vendor.js
vendored
File diff suppressed because one or more lines are too long
53
public/assets/admin/locales/en-US.js
vendored
53
public/assets/admin/locales/en-US.js
vendored
@ -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",
|
||||
|
53
public/assets/admin/locales/ko-KR.js
vendored
53
public/assets/admin/locales/ko-KR.js
vendored
@ -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": "사이트, 보안, 구독, 초대 수수료, 노드, 이메일 및 알림을 포함한 핵심 시스템 구성을 관리합니다",
|
||||
|
54
public/assets/admin/locales/zh-CN.js
vendored
54
public/assets/admin/locales/zh-CN.js
vendored
@ -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": "支付配置",
|
||||
|
Loading…
Reference in New Issue
Block a user