mirror of
https://github.com/lkddi/Xboard.git
synced 2026-04-29 07:27:26 +08:00
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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 中断当前请求并返回新的响应
|
||||
*
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user