feat: enhance plugin management

- Add command support for plugin management
- Optimize plugin management page layout
- Add email copy functionality for users
- Convert payment methods and Telegram Bot to plugin system
This commit is contained in:
xboard
2025-07-26 18:49:58 +08:00
parent 02d853d46a
commit 58868268dd
56 changed files with 3677 additions and 1329 deletions
+64 -18
View File
@@ -2,36 +2,57 @@
namespace App\Services;
use App\Exceptions\ApiException;
use App\Models\Payment;
use App\Services\Plugin\PluginManager;
use App\Services\Plugin\HookManager;
class PaymentService
{
public $method;
protected $class;
protected $config;
protected $payment;
protected $pluginManager;
protected $class;
public function __construct($method, $id = NULL, $uuid = NULL)
{
$this->method = $method;
$this->class = '\\App\\Payments\\' . $this->method;
if (!class_exists($this->class))
throw new ApiException('gate is not found');
if ($id)
$this->pluginManager = app(PluginManager::class);
if ($method === 'temp') {
return;
}
if ($id) {
$payment = Payment::find($id)->toArray();
if ($uuid)
}
if ($uuid) {
$payment = Payment::where('uuid', $uuid)->first()->toArray();
}
$this->config = [];
if (isset($payment)) {
$this->config = $payment['config'];
$this->config = is_string($payment['config']) ? json_decode($payment['config'], true) : $payment['config'];
$this->config['enable'] = $payment['enable'];
$this->config['id'] = $payment['id'];
$this->config['uuid'] = $payment['uuid'];
$this->config['notify_domain'] = $payment['notify_domain'];
$this->config['notify_domain'] = $payment['notify_domain'] ?? '';
}
;
$paymentMethods = $this->getAvailablePaymentMethods();
if (isset($paymentMethods[$this->method])) {
$pluginCode = $paymentMethods[$this->method]['plugin_code'];
$paymentPlugins = $this->pluginManager->getEnabledPaymentPlugins();
foreach ($paymentPlugins as $plugin) {
if ($plugin->getPluginCode() === $pluginCode) {
$plugin->setConfig($this->config);
$this->payment = $plugin;
return;
}
}
}
$this->payment = new $this->class($this->config);
}
@@ -64,18 +85,43 @@ class PaymentService
public function form()
{
$form = $this->payment->form();
$keys = array_keys($form);
$result = [];
foreach ($form as $key => $field) {
$form[$key] = [
'label' => $field['label'],
'field_name' => $key,
'field_type' => $field['type'],
$result[$key] = [
'type' => $field['type'],
'label' => $field['label'] ?? '',
'placeholder' => $field['placeholder'] ?? '',
'value' => $this->config[$key] ?? '',
'select_options' => $field['select_options'] ?? [],
'description' => $field['description'] ?? '',
'value' => $this->config[$key] ?? $field['default'] ?? '',
'options' => $field['select_options'] ?? $field['options'] ?? []
];
}
return $form;
return $result;
}
/**
* 获取所有可用的支付方式
*/
public function getAvailablePaymentMethods(): array
{
$methods = [];
$methods = HookManager::filter('available_payment_methods', $methods);
return $methods;
}
/**
* 获取所有支付方式名称列表(用于管理后台)
*/
public static function getAllPaymentMethodNames(): array
{
$pluginManager = app(PluginManager::class);
$pluginManager->initializeEnabledPlugins();
$instance = new self('temp');
$methods = $instance->getAvailablePaymentMethods();
return array_keys($methods);
}
}
+29
View File
@@ -99,6 +99,35 @@ abstract class AbstractPlugin
HookManager::remove($hook);
}
/**
* 注册 Artisan 命令
*/
protected function registerCommand(string $commandClass): void
{
if (class_exists($commandClass)) {
app('Illuminate\Contracts\Console\Kernel')->registerCommand(new $commandClass());
}
}
/**
* 注册插件命令目录
*/
public function registerCommands(): void
{
$commandsPath = $this->basePath . '/Commands';
if (File::exists($commandsPath)) {
$files = File::glob($commandsPath . '/*.php');
foreach ($files as $file) {
$className = pathinfo($file, PATHINFO_FILENAME);
$commandClass = $this->namespace . '\\Commands\\' . $className;
if (class_exists($commandClass)) {
$this->registerCommand($commandClass);
}
}
}
}
/**
* 中断当前请求并返回新的响应
*
+62 -3
View File
@@ -11,7 +11,6 @@ use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use Symfony\Component\Finder\Finder;
class PluginManager
{
@@ -114,13 +113,25 @@ class PluginManager
*/
protected function loadViews(string $pluginCode): void
{
$viewsPath = $this->getPluginPath($pluginCode) . '/resources/views';
$viewsPath = $this->getPluginPath($pluginCode) . '/views';
if (File::exists($viewsPath)) {
View::addNamespace(Str::studly($pluginCode), $viewsPath);
}
}
/**
* 注册插件命令
*/
protected function registerPluginCommands(string $pluginCode, AbstractPlugin $pluginInstance): void
{
try {
// 调用插件的命令注册方法
$pluginInstance->registerCommands();
} catch (\Exception $e) {
Log::error("Failed to register commands for plugin '{$pluginCode}': " . $e->getMessage());
}
}
/**
* 安装插件
*/
@@ -163,6 +174,7 @@ class PluginManager
'code' => $pluginCode,
'name' => $config['name'],
'version' => $config['version'],
'type' => $config['type'] ?? Plugin::TYPE_FEATURE,
'is_enabled' => false,
'config' => json_encode($defaultValues),
'installed_at' => now(),
@@ -259,6 +271,14 @@ class PluginManager
return false;
}
// 验证插件类型
if (isset($config['type'])) {
$validTypes = ['feature', 'payment'];
if (!in_array($config['type'], $validTypes)) {
return false;
}
}
return true;
}
@@ -478,6 +498,7 @@ class PluginManager
$this->registerServiceProvider($pluginCode);
$this->loadRoutes($pluginCode);
$this->loadViews($pluginCode);
$this->registerPluginCommands($pluginCode, $pluginInstance);
$pluginInstance->boot();
@@ -535,4 +556,42 @@ class PluginManager
return array_intersect_key($this->loadedPlugins, array_flip($enabledPluginCodes));
}
/**
* Get enabled plugins by type
*/
public function getEnabledPluginsByType(string $type): array
{
$this->initializeEnabledPlugins();
$enabledPluginCodes = Plugin::where('is_enabled', true)
->byType($type)
->pluck('code')
->all();
return array_intersect_key($this->loadedPlugins, array_flip($enabledPluginCodes));
}
/**
* Get enabled payment plugins
*/
public function getEnabledPaymentPlugins(): array
{
return $this->getEnabledPluginsByType('payment');
}
/**
* install default plugins
*/
public static function installDefaultPlugins(): void
{
foreach (Plugin::PROTECTED_PLUGINS as $pluginCode) {
if (!Plugin::where('code', $pluginCode)->exists()) {
$pluginManager = app(self::class);
$pluginManager->install($pluginCode);
$pluginManager->enable($pluginCode);
Log::info("Installed and enabled default plugin: {$pluginCode}");
}
}
}
}
+115 -40
View File
@@ -1,85 +1,160 @@
<?php
namespace App\Services;
use App\Exceptions\ApiException;
use App\Jobs\SendTelegramJob;
use App\Models\User;
use \Curl\Curl;
use App\Services\Plugin\HookManager;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class TelegramService {
protected $api;
class TelegramService
{
protected PendingRequest $http;
protected string $apiUrl;
public function __construct($token = '')
public function __construct(?string $token = null)
{
$this->api = 'https://api.telegram.org/bot' . admin_setting('telegram_bot_token', $token) . '/';
$botToken = admin_setting('telegram_bot_token', $token);
$this->apiUrl = "https://api.telegram.org/bot{$botToken}/";
$this->http = Http::timeout(30)
->retry(3, 1000)
->withHeaders([
'Accept' => 'application/json',
]);
}
public function sendMessage(int $chatId, string $text, string $parseMode = '')
public function sendMessage(int $chatId, string $text, string $parseMode = ''): void
{
if ($parseMode === 'markdown') {
$text = str_replace('_', '\_', $text);
}
$text = $parseMode === 'markdown' ? str_replace('_', '\_', $text) : $text;
$this->request('sendMessage', [
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => $parseMode
'parse_mode' => $parseMode ?: null,
]);
}
public function approveChatJoinRequest(int $chatId, int $userId)
public function approveChatJoinRequest(int $chatId, int $userId): void
{
$this->request('approveChatJoinRequest', [
'chat_id' => $chatId,
'user_id' => $userId
'user_id' => $userId,
]);
}
public function declineChatJoinRequest(int $chatId, int $userId)
public function declineChatJoinRequest(int $chatId, int $userId): void
{
$this->request('declineChatJoinRequest', [
'chat_id' => $chatId,
'user_id' => $userId
'user_id' => $userId,
]);
}
public function getMe()
public function getMe(): object
{
return $this->request('getMe');
}
public function setWebhook(string $url)
public function setWebhook(string $url): object
{
return $this->request('setWebhook', [
'url' => $url
]);
$result = $this->request('setWebhook', ['url' => $url]);
return $result;
}
private function request(string $method, array $params = [])
/**
* 注册 Bot 命令列表
*/
public function registerBotCommands(): void
{
$curl = new Curl();
$curl->get($this->api . $method . '?' . http_build_query($params));
$response = $curl->response;
$curl->close();
if (!isset($response->ok)) throw new ApiException('请求失败');
if (!$response->ok) {
throw new ApiException('来自TG的错误:' . $response->description);
}
return $response;
}
try {
$commands = HookManager::filter('telegram.bot.commands', []);
public function sendMessageWithAdmin($message, $isStaff = false)
{
if (!admin_setting('telegram_bot_enable', 0)) return;
$users = User::where(function ($query) use ($isStaff) {
$query->where('is_admin', 1);
if ($isStaff) {
$query->orWhere('is_staff', 1);
if (empty($commands)) {
Log::warning('没有找到任何 Telegram Bot 命令');
return;
}
})
->where('telegram_id', '!=', NULL)
->get();
$this->request('setMyCommands', [
'commands' => json_encode($commands),
'scope' => json_encode(['type' => 'default'])
]);
Log::info('Telegram Bot 命令注册成功', [
'commands_count' => count($commands),
'commands' => $commands
]);
} catch (\Exception $e) {
Log::error('Telegram Bot 命令注册失败', [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
}
}
/**
* 获取当前注册的命令列表
*/
public function getMyCommands(): object
{
return $this->request('getMyCommands');
}
/**
* 删除所有命令
*/
public function deleteMyCommands(): object
{
return $this->request('deleteMyCommands');
}
public function sendMessageWithAdmin(string $message, bool $isStaff = false): void
{
$query = User::where('telegram_id', '!=', null);
$query->where(
fn($q) => $q->where('is_admin', 1)
->when($isStaff, fn($q) => $q->orWhere('is_staff', 1))
);
$users = $query->get();
foreach ($users as $user) {
SendTelegramJob::dispatch($user->telegram_id, $message);
}
}
protected function request(string $method, array $params = []): object
{
try {
$response = $this->http->get($this->apiUrl . $method, $params);
if (!$response->successful()) {
throw new ApiException("HTTP 请求失败: {$response->status()}");
}
$data = $response->object();
if (!isset($data->ok)) {
throw new ApiException('无效的 Telegram API 响应');
}
if (!$data->ok) {
$description = $data->description ?? '未知错误';
throw new ApiException("Telegram API 错误: {$description}");
}
return $data;
} catch (\Exception $e) {
Log::error('Telegram API 请求失败', [
'method' => $method,
'params' => $params,
'error' => $e->getMessage(),
]);
throw new ApiException("Telegram 服务错误: {$e->getMessage()}");
}
}
}
+40 -6
View File
@@ -11,10 +11,11 @@ use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use App\Services\Plugin\HookManager;
class TicketService {
class TicketService
{
public function reply($ticket, $message, $userId)
{
try{
try {
DB::beginTransaction();
$ticketMessage = TicketMessage::create([
'user_id' => $userId,
@@ -31,13 +32,13 @@ class TicketService {
}
DB::commit();
return $ticketMessage;
}catch(\Exception $e){
} catch (\Exception $e) {
DB::rollback();
return false;
}
}
public function replyByAdmin($ticketId, $message, $userId):void
public function replyByAdmin($ticketId, $message, $userId): void
{
$ticket = Ticket::where('id', $ticketId)
->first();
@@ -45,7 +46,7 @@ class TicketService {
throw new ApiException('工单不存在');
}
$ticket->status = Ticket::STATUS_OPENING;
try{
try {
DB::beginTransaction();
$ticketMessage = TicketMessage::create([
'user_id' => $userId,
@@ -62,13 +63,46 @@ class TicketService {
}
DB::commit();
HookManager::call('ticket.reply.admin.after', [$ticket, $ticketMessage]);
}catch(\Exception $e){
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
$this->sendEmailNotify($ticket, $ticketMessage);
}
public function createTicket($userId, $subject, $level, $message)
{
try {
DB::beginTransaction();
if (Ticket::where('status', 0)->where('user_id', $userId)->lockForUpdate()->first()) {
DB::rollBack();
throw new ApiException('存在未关闭的工单');
}
$ticket = Ticket::create([
'user_id' => $userId,
'subject' => $subject,
'level' => $level
]);
if (!$ticket) {
throw new ApiException('工单创建失败');
}
$ticketMessage = TicketMessage::create([
'user_id' => $userId,
'ticket_id' => $ticket->id,
'message' => $message
]);
if (!$ticketMessage) {
DB::rollBack();
throw new ApiException('工单消息创建失败');
}
DB::commit();
return $ticket;
} catch (\Exception $e) {
DB::rollBack();
throw $e;
}
}
// 半小时内不再重复通知
private function sendEmailNotify(Ticket $ticket, TicketMessage $ticketMessage)
{