mirror of
https://github.com/lkddi/Xboard.git
synced 2026-04-24 03:57:27 +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,6 +2,7 @@
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\Plugin\PluginManager;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Encryption\Encrypter;
|
||||
use App\Models\User;
|
||||
@@ -15,6 +16,8 @@ use function Laravel\Prompts\confirm;
|
||||
use function Laravel\Prompts\text;
|
||||
use function Laravel\Prompts\note;
|
||||
use function Laravel\Prompts\select;
|
||||
use App\Models\Plugin;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class XboardInstall extends Command
|
||||
{
|
||||
@@ -157,6 +160,11 @@ class XboardInstall extends Command
|
||||
if (!self::registerAdmin($email, $password)) {
|
||||
abort(500, '管理员账号注册失败,请重试');
|
||||
}
|
||||
self::restoreProtectedPlugins($this);
|
||||
$this->info('正在安装默认插件...');
|
||||
PluginManager::installDefaultPlugins();
|
||||
$this->info('默认插件安装完成');
|
||||
|
||||
$this->info('🎉:一切就绪');
|
||||
$this->info("管理员邮箱:{$email}");
|
||||
$this->info("管理员密码:{$password}");
|
||||
@@ -356,4 +364,64 @@ class XboardInstall extends Command
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 还原内置受保护插件(可在安装和更新时调用)
|
||||
*/
|
||||
public static function restoreProtectedPlugins(Command $console = null)
|
||||
{
|
||||
exec("git config core.filemode false", $output, $returnVar);
|
||||
$cmd = "git status --porcelain plugins/ 2>/dev/null";
|
||||
exec($cmd, $output, $returnVar);
|
||||
if (!empty($output)) {
|
||||
$hasNonNewFiles = false;
|
||||
foreach ($output as $line) {
|
||||
$status = trim(substr($line, 0, 2));
|
||||
if ($status !== 'A') {
|
||||
$hasNonNewFiles = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($hasNonNewFiles) {
|
||||
if ($console)
|
||||
$console->info("检测到 plugins 目录有变更,正在还原...");
|
||||
|
||||
foreach ($output as $line) {
|
||||
$status = trim(substr($line, 0, 2));
|
||||
$filePath = trim(substr($line, 3));
|
||||
|
||||
if (strpos($filePath, 'plugins/') === 0 && $status !== 'A') {
|
||||
$relativePath = substr($filePath, 8);
|
||||
if ($console) {
|
||||
$action = match ($status) {
|
||||
'M' => '修改',
|
||||
'D' => '删除',
|
||||
'R' => '重命名',
|
||||
'C' => '复制',
|
||||
default => '变更'
|
||||
};
|
||||
$console->info("还原插件文件 [{$relativePath}] ({$action})");
|
||||
}
|
||||
|
||||
$cmd = "git checkout HEAD -- {$filePath}";
|
||||
exec($cmd, $gitOutput, $gitReturnVar);
|
||||
|
||||
if ($gitReturnVar === 0) {
|
||||
if ($console)
|
||||
$console->info("插件文件 [{$relativePath}] 已还原。");
|
||||
} else {
|
||||
if ($console)
|
||||
$console->error("插件文件 [{$relativePath}] 还原失败。");
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if ($console)
|
||||
$console->info("plugins 目录状态正常,无需还原。");
|
||||
}
|
||||
} else {
|
||||
if ($console)
|
||||
$console->info("plugins 目录状态正常,无需还原。");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,10 @@ use App\Services\ThemeService;
|
||||
use App\Services\UpdateService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Services\Plugin\PluginManager;
|
||||
use App\Models\Plugin;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Console\Commands\XboardInstall;
|
||||
|
||||
class XboardUpdate extends Command
|
||||
{
|
||||
@@ -44,12 +47,17 @@ class XboardUpdate extends Command
|
||||
$this->info('正在导入数据库请稍等...');
|
||||
Artisan::call("migrate");
|
||||
$this->info(Artisan::output());
|
||||
Artisan::call('horizon:terminate');
|
||||
$this->info('正在检查内置插件文件...');
|
||||
XboardInstall::restoreProtectedPlugins($this);
|
||||
$this->info('正在检查并安装默认插件...');
|
||||
PluginManager::installDefaultPlugins();
|
||||
$this->info('默认插件检查完成');
|
||||
Artisan::call('reset:traffic', ['--fix-null' => true]);
|
||||
$updateService = new UpdateService();
|
||||
$updateService->updateVersionCache();
|
||||
$themeService = app(ThemeService::class);
|
||||
$themeService->refreshCurrentTheme();
|
||||
Artisan::call('horizon:terminate');
|
||||
$this->info('更新完毕,队列服务已重启,你无需进行任何操作。');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,10 @@ class Kernel extends ConsoleKernel
|
||||
{
|
||||
$this->load(__DIR__ . '/Commands');
|
||||
|
||||
try {
|
||||
app(PluginManager::class)->initializeEnabledPlugins();
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
require base_path('routes/console.php');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,13 +11,16 @@ use App\Traits\HasPluginConfig;
|
||||
*/
|
||||
abstract class PluginController extends Controller
|
||||
{
|
||||
use HasPluginConfig;
|
||||
use HasPluginConfig;
|
||||
|
||||
/**
|
||||
* 执行插件操作前的检查
|
||||
*/
|
||||
protected function beforePluginAction(): ?array
|
||||
{
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* 执行插件操作前的检查
|
||||
*/
|
||||
protected function beforePluginAction(): ?array
|
||||
{
|
||||
if (!$this->isPluginEnabled()) {
|
||||
return [400, '插件未启用'];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -2,14 +2,12 @@
|
||||
|
||||
namespace App\Http\Controllers\V1\Guest;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Order;
|
||||
use App\Models\Payment;
|
||||
use App\Services\OrderService;
|
||||
use App\Services\PaymentService;
|
||||
use App\Services\TelegramService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use App\Services\Plugin\HookManager;
|
||||
|
||||
class PaymentController extends Controller
|
||||
@@ -30,7 +28,7 @@ class PaymentController extends Controller
|
||||
}
|
||||
return (isset($verify['custom_result']) ? $verify['custom_result'] : 'success');
|
||||
} catch (\Exception $e) {
|
||||
\Log::error($e);
|
||||
Log::error($e);
|
||||
return $this->fail([500, 'fail']);
|
||||
}
|
||||
}
|
||||
@@ -48,22 +46,7 @@ class PaymentController extends Controller
|
||||
return false;
|
||||
}
|
||||
|
||||
$payment = Payment::where('id', $order->payment_id)->first();
|
||||
$telegramService = new TelegramService();
|
||||
$message = sprintf(
|
||||
"💰成功收款%s元\n" .
|
||||
"———————————————\n" .
|
||||
"支付接口:%s\n" .
|
||||
"支付渠道:%s\n" .
|
||||
"本站订单:`%s`"
|
||||
,
|
||||
$order->total_amount / 100,
|
||||
$payment->payment,
|
||||
$payment->name,
|
||||
$order->trade_no
|
||||
);
|
||||
|
||||
$telegramService->sendMessageWithAdmin($message);
|
||||
HookManager::call('payment.notify.success', $order);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,121 +4,123 @@ namespace App\Http\Controllers\V1\Guest;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Services\Plugin\HookManager;
|
||||
use App\Services\TelegramService;
|
||||
use App\Services\UserService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TelegramController extends Controller
|
||||
{
|
||||
protected $msg;
|
||||
protected $commands = [];
|
||||
protected $telegramService;
|
||||
protected ?object $msg = null;
|
||||
protected TelegramService $telegramService;
|
||||
protected UserService $userService;
|
||||
|
||||
public function __construct(TelegramService $telegramService)
|
||||
public function __construct(TelegramService $telegramService, UserService $userService)
|
||||
{
|
||||
$this->telegramService = $telegramService;
|
||||
$this->userService = $userService;
|
||||
}
|
||||
|
||||
public function webhook(Request $request)
|
||||
public function webhook(Request $request): void
|
||||
{
|
||||
if ($request->input('access_token') !== md5(admin_setting('telegram_bot_token'))) {
|
||||
$expectedToken = md5(admin_setting('telegram_bot_token'));
|
||||
if ($request->input('access_token') !== $expectedToken) {
|
||||
throw new ApiException('access_token is error', 401);
|
||||
}
|
||||
$data = json_decode(request()->getContent(),true);
|
||||
|
||||
$data = $request->json()->all();
|
||||
|
||||
$this->formatMessage($data);
|
||||
$this->formatChatJoinRequest($data);
|
||||
$this->handle();
|
||||
}
|
||||
|
||||
private function handle()
|
||||
private function handle(): void
|
||||
{
|
||||
if (!$this->msg) return;
|
||||
if (!$this->msg)
|
||||
return;
|
||||
$msg = $this->msg;
|
||||
$commandName = explode('@', $msg->command);
|
||||
|
||||
// To reduce request, only commands contains @ will get the bot name
|
||||
if (count($commandName) == 2) {
|
||||
$botName = $this->getBotName();
|
||||
if ($commandName[1] === $botName){
|
||||
$msg->command = $commandName[0];
|
||||
}
|
||||
}
|
||||
|
||||
$this->processBotName($msg);
|
||||
try {
|
||||
foreach (glob(base_path('app//Plugins//Telegram//Commands') . '/*.php') as $file) {
|
||||
$command = basename($file, '.php');
|
||||
$class = '\\App\\Plugins\\Telegram\\Commands\\' . $command;
|
||||
if (!class_exists($class)) continue;
|
||||
$instance = new $class();
|
||||
if ($msg->message_type === 'message') {
|
||||
if (!isset($instance->command)) continue;
|
||||
if ($msg->command !== $instance->command) continue;
|
||||
$instance->handle($msg);
|
||||
return;
|
||||
}
|
||||
if ($msg->message_type === 'reply_message') {
|
||||
if (!isset($instance->regex)) continue;
|
||||
if (!preg_match($instance->regex, $msg->reply_text, $match)) continue;
|
||||
$instance->handle($msg, $match);
|
||||
return;
|
||||
}
|
||||
HookManager::call('telegram.message.before', [$msg]);
|
||||
$handled = HookManager::filter('telegram.message.handle', false, [$msg]);
|
||||
if (!$handled) {
|
||||
HookManager::call('telegram.message.unhandled', [$msg]);
|
||||
}
|
||||
HookManager::call('telegram.message.after', [$msg]);
|
||||
} catch (\Exception $e) {
|
||||
HookManager::call('telegram.message.error', [$msg, $e]);
|
||||
$this->telegramService->sendMessage($msg->chat_id, $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private function getBotName()
|
||||
private function processBotName(object $msg): void
|
||||
{
|
||||
$commandParts = explode('@', $msg->command);
|
||||
|
||||
if (count($commandParts) === 2) {
|
||||
$botName = $this->getBotName();
|
||||
if ($commandParts[1] === $botName) {
|
||||
$msg->command = $commandParts[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function getBotName(): string
|
||||
{
|
||||
$response = $this->telegramService->getMe();
|
||||
return $response->result->username;
|
||||
}
|
||||
|
||||
private function formatMessage(array $data)
|
||||
private function formatMessage(array $data): void
|
||||
{
|
||||
if (!isset($data['message'])) return;
|
||||
if (!isset($data['message']['text'])) return;
|
||||
$obj = new \StdClass();
|
||||
$text = explode(' ', $data['message']['text']);
|
||||
$obj->command = $text[0];
|
||||
$obj->args = array_slice($text, 1);
|
||||
$obj->chat_id = $data['message']['chat']['id'];
|
||||
$obj->message_id = $data['message']['message_id'];
|
||||
$obj->message_type = 'message';
|
||||
$obj->text = $data['message']['text'];
|
||||
$obj->is_private = $data['message']['chat']['type'] === 'private';
|
||||
if (isset($data['message']['reply_to_message']['text'])) {
|
||||
$obj->message_type = 'reply_message';
|
||||
$obj->reply_text = $data['message']['reply_to_message']['text'];
|
||||
if (!isset($data['message']['text']))
|
||||
return;
|
||||
|
||||
$message = $data['message'];
|
||||
$text = explode(' ', $message['text']);
|
||||
|
||||
$this->msg = (object) [
|
||||
'command' => $text[0],
|
||||
'args' => array_slice($text, 1),
|
||||
'chat_id' => $message['chat']['id'],
|
||||
'message_id' => $message['message_id'],
|
||||
'message_type' => 'message',
|
||||
'text' => $message['text'],
|
||||
'is_private' => $message['chat']['type'] === 'private',
|
||||
];
|
||||
|
||||
if (isset($message['reply_to_message']['text'])) {
|
||||
$this->msg->message_type = 'reply_message';
|
||||
$this->msg->reply_text = $message['reply_to_message']['text'];
|
||||
}
|
||||
$this->msg = $obj;
|
||||
}
|
||||
|
||||
private function formatChatJoinRequest(array $data)
|
||||
private function formatChatJoinRequest(array $data): void
|
||||
{
|
||||
if (!isset($data['chat_join_request'])) return;
|
||||
if (!isset($data['chat_join_request']['from']['id'])) return;
|
||||
if (!isset($data['chat_join_request']['chat']['id'])) return;
|
||||
$user = \App\Models\User::where('telegram_id', $data['chat_join_request']['from']['id'])
|
||||
->first();
|
||||
$joinRequest = $data['chat_join_request'] ?? null;
|
||||
if (!$joinRequest)
|
||||
return;
|
||||
|
||||
$chatId = $joinRequest['chat']['id'] ?? null;
|
||||
$userId = $joinRequest['from']['id'] ?? null;
|
||||
|
||||
if (!$chatId || !$userId)
|
||||
return;
|
||||
|
||||
$user = User::where('telegram_id', $userId)->first();
|
||||
|
||||
if (!$user) {
|
||||
$this->telegramService->declineChatJoinRequest(
|
||||
$data['chat_join_request']['chat']['id'],
|
||||
$data['chat_join_request']['from']['id']
|
||||
);
|
||||
$this->telegramService->declineChatJoinRequest($chatId, $userId);
|
||||
return;
|
||||
}
|
||||
$userService = new \App\Services\UserService();
|
||||
if (!$userService->isAvailable($user)) {
|
||||
$this->telegramService->declineChatJoinRequest(
|
||||
$data['chat_join_request']['chat']['id'],
|
||||
$data['chat_join_request']['from']['id']
|
||||
);
|
||||
|
||||
if (!$this->userService->isAvailable($user)) {
|
||||
$this->telegramService->declineChatJoinRequest($chatId, $userId);
|
||||
return;
|
||||
}
|
||||
$userService = new \App\Services\UserService();
|
||||
$this->telegramService->approveChatJoinRequest(
|
||||
$data['chat_join_request']['chat']['id'],
|
||||
$data['chat_join_request']['from']['id']
|
||||
);
|
||||
|
||||
$this->telegramService->approveChatJoinRequest($chatId, $userId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,12 +9,11 @@ use App\Http\Resources\TicketResource;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\TicketMessage;
|
||||
use App\Models\User;
|
||||
use App\Services\TelegramService;
|
||||
use App\Services\TicketService;
|
||||
use App\Utils\Dict;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Services\Plugin\HookManager;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class TicketController extends Controller
|
||||
{
|
||||
@@ -42,39 +41,20 @@ class TicketController extends Controller
|
||||
|
||||
public function save(TicketSave $request)
|
||||
{
|
||||
try{
|
||||
DB::beginTransaction();
|
||||
if (Ticket::where('status', 0)->where('user_id', $request->user()->id)->lockForUpdate()->first()) {
|
||||
DB::rollBack();
|
||||
return $this->fail([400, '存在未关闭的工单']);
|
||||
}
|
||||
$ticket = Ticket::create(array_merge($request->only([
|
||||
'subject',
|
||||
'level'
|
||||
]), [
|
||||
'user_id' => $request->user()->id
|
||||
]));
|
||||
if (!$ticket) {
|
||||
throw new \Exception(__('There are other unresolved tickets'));
|
||||
}
|
||||
$ticketMessage = TicketMessage::create([
|
||||
'user_id' => $request->user()->id,
|
||||
'ticket_id' => $ticket->id,
|
||||
'message' => $request->input('message')
|
||||
]);
|
||||
if (!$ticketMessage) {
|
||||
throw new \Exception(__('Failed to open ticket'));
|
||||
}
|
||||
DB::commit();
|
||||
try {
|
||||
$ticketService = new TicketService();
|
||||
$ticket = $ticketService->createTicket(
|
||||
$request->user()->id,
|
||||
$request->input('subject'),
|
||||
$request->input('level'),
|
||||
$request->input('message')
|
||||
);
|
||||
HookManager::call('ticket.create.after', $ticket);
|
||||
$this->sendNotify($ticket, $request->input('message'), $request->user()->id);
|
||||
return $this->success(true);
|
||||
}catch(\Exception $e){
|
||||
DB::rollBack();
|
||||
\Log::error($e);
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
return $this->fail([400, $e->getMessage()]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function reply(Request $request)
|
||||
@@ -95,18 +75,19 @@ class TicketController extends Controller
|
||||
return $this->fail([400, __('The ticket is closed and cannot be replied')]);
|
||||
}
|
||||
if ($request->user()->id == $this->getLastMessage($ticket->id)->user_id) {
|
||||
return $this->fail([400, __('Please wait for the technical enginneer to reply')]);
|
||||
return $this->fail(codeResponse: [400, __('Please wait for the technical enginneer to reply')]);
|
||||
}
|
||||
$ticketService = new TicketService();
|
||||
if (!$ticketService->reply(
|
||||
$ticket,
|
||||
$request->input('message'),
|
||||
$request->user()->id
|
||||
)) {
|
||||
if (
|
||||
!$ticketService->reply(
|
||||
$ticket,
|
||||
$request->input('message'),
|
||||
$request->user()->id
|
||||
)
|
||||
) {
|
||||
return $this->fail([400, __('Ticket reply failed')]);
|
||||
}
|
||||
HookManager::call('ticket.reply.user.after', [$ticket, $this->getLastMessage($ticket->id)]);
|
||||
$this->sendNotify($ticket, $request->input('message'), $request->user()->id);
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
@@ -138,13 +119,15 @@ class TicketController extends Controller
|
||||
|
||||
public function withdraw(TicketWithdraw $request)
|
||||
{
|
||||
if ((int)admin_setting('withdraw_close_enable', 0)) {
|
||||
if ((int) admin_setting('withdraw_close_enable', 0)) {
|
||||
return $this->fail([400, 'Unsupported withdraw']);
|
||||
}
|
||||
if (!in_array(
|
||||
$request->input('withdraw_method'),
|
||||
admin_setting('commission_withdraw_method',Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT)
|
||||
)) {
|
||||
if (
|
||||
!in_array(
|
||||
$request->input('withdraw_method'),
|
||||
admin_setting('commission_withdraw_method', Dict::WITHDRAW_METHOD_WHITELIST_DEFAULT)
|
||||
)
|
||||
) {
|
||||
return $this->fail([422, __('Unsupported withdrawal method')]);
|
||||
}
|
||||
$user = User::find($request->user()->id);
|
||||
@@ -152,77 +135,24 @@ class TicketController extends Controller
|
||||
if ($limit > ($user->commission_balance / 100)) {
|
||||
return $this->fail([422, __('The current required minimum withdrawal commission is :limit', ['limit' => $limit])]);
|
||||
}
|
||||
try{
|
||||
DB::beginTransaction();
|
||||
try {
|
||||
$ticketService = new TicketService();
|
||||
$subject = __('[Commission Withdrawal Request] This ticket is opened by the system');
|
||||
$ticket = Ticket::create([
|
||||
'subject' => $subject,
|
||||
'level' => 2,
|
||||
'user_id' => $request->user()->id
|
||||
]);
|
||||
if (!$ticket) {
|
||||
return $this->fail([400, __('Failed to open ticket')]);
|
||||
}
|
||||
$message = sprintf("%s\r\n%s",
|
||||
$message = sprintf(
|
||||
"%s\r\n%s",
|
||||
__('Withdrawal method') . ":" . $request->input('withdraw_method'),
|
||||
__('Withdrawal account') . ":" . $request->input('withdraw_account')
|
||||
);
|
||||
$ticketMessage = TicketMessage::create([
|
||||
'user_id' => $request->user()->id,
|
||||
'ticket_id' => $ticket->id,
|
||||
'message' => $message
|
||||
]);
|
||||
if (!$ticketMessage) {
|
||||
DB::rollBack();
|
||||
return $this->fail([400, __('Failed to open ticket')]);
|
||||
}
|
||||
DB::commit();
|
||||
}catch(\Exception $e){
|
||||
DB::rollBack();
|
||||
$ticket = $ticketService->createTicket(
|
||||
$request->user()->id,
|
||||
$subject,
|
||||
2,
|
||||
$message
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
throw $e;
|
||||
}
|
||||
$this->sendNotify($ticket, $message, $request->user()->id);
|
||||
HookManager::call('ticket.create.after', $ticket);
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
private function sendNotify(Ticket $ticket, string $message, $user_id)
|
||||
{
|
||||
$user = User::find($user_id)->load('plan');
|
||||
$transfer_enable = $this->getFlowData($user->transfer_enable); // 总流量
|
||||
$remaining_traffic = $this->getFlowData($user->transfer_enable - $user->u - $user->d); // 剩余流量
|
||||
$u = $this->getFlowData($user->u); // 上传
|
||||
$d = $this->getFlowData($user->d); // 下载
|
||||
$expired_at = date("Y-m-d h:m:s", $user->expired_at); // 到期时间
|
||||
$money = $user->balance / 100;
|
||||
$affmoney = $user->commission_balance / 100;
|
||||
$plan = $user->plan;
|
||||
$ip = request()->ip();
|
||||
$region = filter_var($ip,FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? (new \Ip2Region())->simple($ip) : "NULL";
|
||||
$TGmessage = "📮工单提醒 #{$ticket->id}\n———————————————\n";
|
||||
$TGmessage .= "邮箱: `{$user->email}`\n";
|
||||
$TGmessage .= "用户位置: \n`{$region}`\n";
|
||||
if($user->plan){
|
||||
$TGmessage .= "套餐与流量: \n`{$plan->name} {$transfer_enable}/{$remaining_traffic}`\n";
|
||||
$TGmessage .= "上传/下载: \n`{$u}/{$d}`\n";
|
||||
$TGmessage .= "到期时间: \n`{$expired_at}`\n";
|
||||
}else{
|
||||
$TGmessage .= "套餐与流量: \n`未订购任何套餐`\n";
|
||||
}
|
||||
$TGmessage .= "余额/佣金余额: \n`{$money}/{$affmoney}`\n";
|
||||
$TGmessage .= "主题:\n`{$ticket->subject}`\n内容:\n`{$message}`\n";
|
||||
$telegramService = new TelegramService();
|
||||
$telegramService->sendMessageWithAdmin($TGmessage, true);
|
||||
}
|
||||
|
||||
private function getFlowData($b)
|
||||
{
|
||||
$m = $b / (1024 * 1024);
|
||||
if ($m >= 1024) {
|
||||
$g = $m / 1024;
|
||||
$text = round($g, 2) . "GB";
|
||||
} else {
|
||||
$text = round($m, 2) . "MB";
|
||||
}
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,6 @@ class ConfigController extends Controller
|
||||
|
||||
public function setTelegramWebhook(Request $request)
|
||||
{
|
||||
// 判断站点网址
|
||||
$app_url = admin_setting('app_url');
|
||||
if (blank($app_url))
|
||||
return $this->fail([422, '请先设置站点网址']);
|
||||
@@ -81,17 +80,14 @@ class ConfigController extends Controller
|
||||
$telegramService = new TelegramService($request->input('telegram_bot_token'));
|
||||
$telegramService->getMe();
|
||||
$telegramService->setWebhook($hookUrl);
|
||||
$telegramService->registerBotCommands();
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
public function fetch(Request $request)
|
||||
{
|
||||
$key = $request->input('key');
|
||||
|
||||
// 构建配置数据映射
|
||||
$configMappings = $this->getConfigMappings();
|
||||
|
||||
// 如果请求特定分组,直接返回
|
||||
if ($key && isset($configMappings[$key])) {
|
||||
return $this->success([$key => $configMappings[$key]]);
|
||||
}
|
||||
|
||||
@@ -9,16 +9,18 @@ use App\Services\PaymentService;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class PaymentController extends Controller
|
||||
{
|
||||
public function getPaymentMethods()
|
||||
{
|
||||
$methods = [];
|
||||
foreach (glob(base_path('app//Payments') . '/*.php') as $file) {
|
||||
array_push($methods, pathinfo($file)['filename']);
|
||||
}
|
||||
return $this->success($methods);
|
||||
|
||||
$pluginMethods = PaymentService::getAllPaymentMethodNames();
|
||||
$methods = array_merge($methods, $pluginMethods);
|
||||
|
||||
return $this->success(array_unique($methods));
|
||||
}
|
||||
|
||||
public function fetch()
|
||||
@@ -37,23 +39,29 @@ class PaymentController extends Controller
|
||||
|
||||
public function getPaymentForm(Request $request)
|
||||
{
|
||||
$paymentService = new PaymentService($request->input('payment'), $request->input('id'));
|
||||
return $this->success(collect($paymentService->form())->values());
|
||||
try {
|
||||
$paymentService = new PaymentService($request->input('payment'), $request->input('id'));
|
||||
return $this->success(collect($paymentService->form()));
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail([400, '支付方式不存在或未启用']);
|
||||
}
|
||||
}
|
||||
|
||||
public function show(Request $request)
|
||||
{
|
||||
$payment = Payment::find($request->input('id'));
|
||||
if (!$payment) return $this->fail([400202 ,'支付方式不存在']);
|
||||
if (!$payment)
|
||||
return $this->fail([400202, '支付方式不存在']);
|
||||
$payment->enable = !$payment->enable;
|
||||
if (!$payment->save()) return $this->fail([500 ,'保存失败']);
|
||||
if (!$payment->save())
|
||||
return $this->fail([500, '保存失败']);
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
public function save(Request $request)
|
||||
{
|
||||
if (!admin_setting('app_url')) {
|
||||
return $this->fail([400 ,'请在站点配置中配置站点地址']);
|
||||
return $this->fail([400, '请在站点配置中配置站点地址']);
|
||||
}
|
||||
$params = $request->validate([
|
||||
'name' => 'required',
|
||||
@@ -73,18 +81,19 @@ class PaymentController extends Controller
|
||||
]);
|
||||
if ($request->input('id')) {
|
||||
$payment = Payment::find($request->input('id'));
|
||||
if (!$payment) return $this->fail([400202 ,'支付方式不存在']);
|
||||
if (!$payment)
|
||||
return $this->fail([400202, '支付方式不存在']);
|
||||
try {
|
||||
$payment->update($params);
|
||||
} catch (\Exception $e) {
|
||||
\Log::error($e);
|
||||
return $this->fail([500 ,'保存失败']);
|
||||
Log::error($e);
|
||||
return $this->fail([500, '保存失败']);
|
||||
}
|
||||
return $this->success(true);
|
||||
}
|
||||
$params['uuid'] = Helper::randomChar(8);
|
||||
if (!Payment::create($params)) {
|
||||
return $this->fail([500 ,'保存失败']);
|
||||
return $this->fail([500, '保存失败']);
|
||||
}
|
||||
return $this->success(true);
|
||||
}
|
||||
@@ -92,7 +101,8 @@ class PaymentController extends Controller
|
||||
public function drop(Request $request)
|
||||
{
|
||||
$payment = Payment::find($request->input('id'));
|
||||
if (!$payment) return $this->fail([400202 ,'支付方式不存在']);
|
||||
if (!$payment)
|
||||
return $this->fail([400202, '支付方式不存在']);
|
||||
return $this->success($payment->delete());
|
||||
}
|
||||
|
||||
@@ -105,7 +115,7 @@ class PaymentController extends Controller
|
||||
'ids.required' => '参数有误',
|
||||
'ids.array' => '参数有误'
|
||||
]);
|
||||
try{
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
foreach ($request->input('ids') as $k => $v) {
|
||||
if (!Payment::find($v)->update(['sort' => $k + 1])) {
|
||||
@@ -113,11 +123,11 @@ class PaymentController extends Controller
|
||||
}
|
||||
}
|
||||
DB::commit();
|
||||
}catch(\Exception $e){
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
return $this->fail([500 ,'保存失败']);
|
||||
return $this->fail([500, '保存失败']);
|
||||
}
|
||||
|
||||
|
||||
return $this->success(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,14 +23,43 @@ class PluginController extends Controller
|
||||
$this->configService = $configService;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有插件类型
|
||||
*/
|
||||
public function types()
|
||||
{
|
||||
return response()->json([
|
||||
'data' => [
|
||||
[
|
||||
'value' => Plugin::TYPE_FEATURE,
|
||||
'label' => '功能',
|
||||
'description' => '提供功能扩展的插件,如Telegram登录、邮件通知等',
|
||||
'icon' => '🔧'
|
||||
],
|
||||
[
|
||||
'value' => Plugin::TYPE_PAYMENT,
|
||||
'label' => '支付方式',
|
||||
'description' => '提供支付接口的插件,如支付宝、微信支付等',
|
||||
'icon' => '💳'
|
||||
]
|
||||
]
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取插件列表
|
||||
*/
|
||||
public function index()
|
||||
public function index(Request $request)
|
||||
{
|
||||
$installedPlugins = Plugin::get()
|
||||
$type = $request->query('type');
|
||||
|
||||
$installedPlugins = Plugin::when($type, function($query) use ($type) {
|
||||
return $query->byType($type);
|
||||
})
|
||||
->get()
|
||||
->keyBy('code')
|
||||
->toArray();
|
||||
|
||||
$pluginPath = base_path('plugins');
|
||||
$plugins = [];
|
||||
|
||||
@@ -42,8 +71,14 @@ class PluginController extends Controller
|
||||
if (File::exists($configFile)) {
|
||||
$config = json_decode(File::get($configFile), true);
|
||||
$code = $config['code'];
|
||||
$pluginType = $config['type'] ?? Plugin::TYPE_FEATURE;
|
||||
|
||||
// 如果指定了类型,过滤插件
|
||||
if ($type && $pluginType !== $type) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$installed = isset($installedPlugins[$code]);
|
||||
// 使用配置服务获取配置
|
||||
$pluginConfig = $installed ? $this->configService->getConfig($code) : ($config['config'] ?? []);
|
||||
$readmeFile = collect(['README.md', 'readme.md'])
|
||||
->map(fn($f) => $directory . '/' . $f)
|
||||
@@ -56,8 +91,11 @@ class PluginController extends Controller
|
||||
'version' => $config['version'],
|
||||
'description' => $config['description'],
|
||||
'author' => $config['author'],
|
||||
'type' => $pluginType,
|
||||
'is_installed' => $installed,
|
||||
'is_enabled' => $installed ? $installedPlugins[$code]['is_enabled'] : false,
|
||||
'is_protected' => in_array($code, Plugin::PROTECTED_PLUGINS),
|
||||
'can_be_deleted' => !in_array($code, Plugin::PROTECTED_PLUGINS),
|
||||
'config' => $pluginConfig,
|
||||
'readme' => $readmeContent,
|
||||
];
|
||||
@@ -236,8 +274,17 @@ class PluginController extends Controller
|
||||
'code' => 'required|string'
|
||||
]);
|
||||
|
||||
$code = $request->input('code');
|
||||
|
||||
// 检查是否为受保护的插件
|
||||
if (in_array($code, Plugin::PROTECTED_PLUGINS)) {
|
||||
return response()->json([
|
||||
'message' => '该插件为系统默认插件,不允许删除'
|
||||
], 403);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->pluginManager->delete($request->input('code'));
|
||||
$this->pluginManager->delete($code);
|
||||
return response()->json([
|
||||
'message' => '插件删除成功'
|
||||
]);
|
||||
|
||||
@@ -164,7 +164,6 @@ class UserController extends Controller
|
||||
$users = $userModel->orderBy('id', 'desc')
|
||||
->paginate($pageSize, ['*'], 'page', $current);
|
||||
|
||||
/** @phpstan-ignore-next-line */
|
||||
$users->getCollection()->transform(function ($user): array {
|
||||
return self::transformUserData($user);
|
||||
});
|
||||
|
||||
@@ -247,6 +247,7 @@ class AdminRoute
|
||||
$router->group([
|
||||
'prefix' => 'plugin'
|
||||
], function ($router) {
|
||||
$router->get('/types', [\App\Http\Controllers\V2\Admin\PluginController::class, 'types']);
|
||||
$router->get('/getPlugins', [\App\Http\Controllers\V2\Admin\PluginController::class, 'index']);
|
||||
$router->post('/upload', [\App\Http\Controllers\V2\Admin\PluginController::class, 'upload']);
|
||||
$router->post('/delete', [\App\Http\Controllers\V2\Admin\PluginController::class, 'delete']);
|
||||
|
||||
+45
-1
@@ -3,6 +3,8 @@
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
@@ -16,11 +18,27 @@ use Illuminate\Database\Eloquent\Model;
|
||||
* @property string $license
|
||||
* @property string $requires
|
||||
* @property string $config
|
||||
* @property string $type
|
||||
*/
|
||||
class Plugin extends Model
|
||||
{
|
||||
protected $table = 'v2_plugins';
|
||||
|
||||
const TYPE_FEATURE = 'feature';
|
||||
const TYPE_PAYMENT = 'payment';
|
||||
|
||||
// 默认不可删除的插件列表
|
||||
const PROTECTED_PLUGINS = [
|
||||
'epay', // EPay
|
||||
'alipay_f2f', // Alipay F2F
|
||||
'btcpay', // BTCPay
|
||||
'coinbase', // Coinbase
|
||||
'coin_payments', // CoinPayments
|
||||
'mgate', // MGate
|
||||
'smogate', // Smogate
|
||||
'telegram', // Telegram
|
||||
];
|
||||
|
||||
protected $guarded = [
|
||||
'id',
|
||||
'created_at',
|
||||
@@ -28,6 +46,32 @@ class Plugin extends Model
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'is_enabled' => 'boolean'
|
||||
'is_enabled' => 'boolean',
|
||||
];
|
||||
|
||||
public function scopeByType(Builder $query, string $type): Builder
|
||||
{
|
||||
return $query->where('type', $type);
|
||||
}
|
||||
|
||||
public function isFeaturePlugin(): bool
|
||||
{
|
||||
return $this->type === self::TYPE_FEATURE;
|
||||
}
|
||||
|
||||
public function isPaymentPlugin(): bool
|
||||
{
|
||||
return $this->type === self::TYPE_PAYMENT;
|
||||
}
|
||||
|
||||
public function isProtected(): bool
|
||||
{
|
||||
return in_array($this->code, self::PROTECTED_PLUGINS);
|
||||
}
|
||||
|
||||
public function canBeDeleted(): bool
|
||||
{
|
||||
return !$this->isProtected();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
* @property int|null $is_admin 是否管理员
|
||||
* @property int|null $next_reset_at 下次流量重置时间
|
||||
* @property int|null $last_reset_at 上次流量重置时间
|
||||
* @property int|null $telegram_id Telegram ID
|
||||
* @property int $reset_count 流量重置次数
|
||||
* @property int $created_at
|
||||
* @property int $updated_at
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
*
|
||||
!.gitignore
|
||||
!AlipayF2F.php
|
||||
!BTCPay.php
|
||||
!Coinbase.php
|
||||
!CoinPayments.php
|
||||
!EPay.php
|
||||
!MGate.php
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 自己写别抄,抄NMB抄
|
||||
*/
|
||||
namespace App\Payments;
|
||||
use App\Contracts\PaymentInterface;
|
||||
use App\Exceptions\ApiException;
|
||||
|
||||
class AlipayF2F implements PaymentInterface
|
||||
{
|
||||
protected $config;
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function form(): array
|
||||
{
|
||||
return [
|
||||
'app_id' => [
|
||||
'label' => '支付宝APPID',
|
||||
'description' => '',
|
||||
'type' => 'input',
|
||||
],
|
||||
'private_key' => [
|
||||
'label' => '支付宝私钥',
|
||||
'description' => '',
|
||||
'type' => 'input',
|
||||
],
|
||||
'public_key' => [
|
||||
'label' => '支付宝公钥',
|
||||
'description' => '',
|
||||
'type' => 'input',
|
||||
],
|
||||
'product_name' => [
|
||||
'label' => '自定义商品名称',
|
||||
'description' => '将会体现在支付宝账单中',
|
||||
'type' => 'input'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function pay($order): array
|
||||
{
|
||||
try {
|
||||
$gateway = new \Library\AlipayF2F();
|
||||
$gateway->setMethod('alipay.trade.precreate');
|
||||
$gateway->setAppId($this->config['app_id']);
|
||||
$gateway->setPrivateKey($this->config['private_key']); // 可以是路径,也可以是密钥内容
|
||||
$gateway->setAlipayPublicKey($this->config['public_key']); // 可以是路径,也可以是密钥内容
|
||||
$gateway->setNotifyUrl($order['notify_url']);
|
||||
$gateway->setBizContent([
|
||||
'subject' => $this->config['product_name'] ?? (admin_setting('app_name', 'XBoard') . ' - 订阅'),
|
||||
'out_trade_no' => $order['trade_no'],
|
||||
'total_amount' => $order['total_amount'] / 100
|
||||
]);
|
||||
$gateway->send();
|
||||
return [
|
||||
'type' => 0, // 0:qrcode 1:url
|
||||
'data' => $gateway->getQrCodeUrl()
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
\Log::error($e);
|
||||
throw new ApiException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function notify($params)
|
||||
{
|
||||
if ($params['trade_status'] !== 'TRADE_SUCCESS')
|
||||
return false;
|
||||
$gateway = new \Library\AlipayF2F();
|
||||
$gateway->setAppId($this->config['app_id']);
|
||||
$gateway->setPrivateKey($this->config['private_key']); // 可以是路径,也可以是密钥内容
|
||||
$gateway->setAlipayPublicKey($this->config['public_key']); // 可以是路径,也可以是密钥内容
|
||||
try {
|
||||
if ($gateway->verify($params)) {
|
||||
/**
|
||||
* Payment is successful
|
||||
*/
|
||||
return [
|
||||
'trade_no' => $params['out_trade_no'],
|
||||
'callback_no' => $params['trade_no']
|
||||
];
|
||||
} else {
|
||||
/**
|
||||
* Payment is not successful
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
/**
|
||||
* Payment is not successful
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Payments;
|
||||
|
||||
use App\Contracts\PaymentInterface;
|
||||
|
||||
class EPay implements PaymentInterface
|
||||
{
|
||||
protected $config;
|
||||
public function __construct($config)
|
||||
{
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function form(): array
|
||||
{
|
||||
return [
|
||||
'url' => [
|
||||
'label' => 'URL',
|
||||
'description' => '',
|
||||
'type' => 'input',
|
||||
],
|
||||
'pid' => [
|
||||
'label' => 'PID',
|
||||
'description' => '',
|
||||
'type' => 'input',
|
||||
],
|
||||
'key' => [
|
||||
'label' => 'KEY',
|
||||
'description' => '',
|
||||
'type' => 'input',
|
||||
],
|
||||
'type' => [
|
||||
'label' => 'TYPE',
|
||||
'description' => 'alipay / qqpay / wxpay',
|
||||
'type' => 'input',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function pay($order): array
|
||||
{
|
||||
$params = [
|
||||
'money' => $order['total_amount'] / 100,
|
||||
'name' => $order['trade_no'],
|
||||
'notify_url' => $order['notify_url'],
|
||||
'return_url' => $order['return_url'],
|
||||
'out_trade_no' => $order['trade_no'],
|
||||
'pid' => $this->config['pid']
|
||||
];
|
||||
if (optional($this->config)['type']) {
|
||||
$params['type'] = $this->config['type'];
|
||||
}
|
||||
ksort($params);
|
||||
reset($params);
|
||||
$str = stripslashes(urldecode(http_build_query($params))) . $this->config['key'];
|
||||
$params['sign'] = md5($str);
|
||||
$params['sign_type'] = 'MD5';
|
||||
return [
|
||||
'type' => 1, // 0:qrcode 1:url
|
||||
'data' => $this->config['url'] . '/submit.php?' . http_build_query($params)
|
||||
];
|
||||
}
|
||||
|
||||
public function notify($params): array|bool
|
||||
{
|
||||
$sign = $params['sign'];
|
||||
unset($params['sign']);
|
||||
unset($params['sign_type']);
|
||||
ksort($params);
|
||||
reset($params);
|
||||
$str = stripslashes(urldecode(http_build_query($params))) . $this->config['key'];
|
||||
if ($sign !== md5($str)) {
|
||||
return false;
|
||||
}
|
||||
return [
|
||||
'trade_no' => $params['out_trade_no'],
|
||||
'callback_no' => $params['trade_no']
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Plugins\Telegram\Commands;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Models\User;
|
||||
use App\Plugins\Telegram\Telegram;
|
||||
|
||||
class Bind extends Telegram {
|
||||
public $command = '/bind';
|
||||
public $description = '将Telegram账号绑定到网站';
|
||||
|
||||
public function handle($message, $match = []) {
|
||||
if (!$message->is_private) return;
|
||||
if (!isset($message->args[0])) {
|
||||
throw new ApiException('参数有误,请携带订阅地址发送', 422);
|
||||
}
|
||||
$subscribeUrl = $message->args[0];
|
||||
$subscribeUrl = parse_url($subscribeUrl);
|
||||
|
||||
// 首先尝试从查询参数获取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('订阅地址无效');
|
||||
}
|
||||
$user = User::where('token', $token)->first();
|
||||
if (!$user) {
|
||||
throw new ApiException('用户不存在');
|
||||
}
|
||||
if ($user->telegram_id) {
|
||||
throw new ApiException('该账号已经绑定了Telegram账号');
|
||||
}
|
||||
$user->telegram_id = $message->chat_id;
|
||||
if (!$user->save()) {
|
||||
throw new ApiException('设置失败');
|
||||
}
|
||||
$telegramService = $this->telegramService;
|
||||
$telegramService->sendMessage($message->chat_id, '绑定成功');
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Plugins\Telegram\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Plugins\Telegram\Telegram;
|
||||
|
||||
class GetLatestUrl extends Telegram {
|
||||
public $command = '/getlatesturl';
|
||||
public $description = '将Telegram账号绑定到网站';
|
||||
|
||||
public function handle($message, $match = []) {
|
||||
$telegramService = $this->telegramService;
|
||||
$text = sprintf(
|
||||
"%s的最新网址是:%s",
|
||||
admin_setting('app_name', 'XBoard'),
|
||||
admin_setting('app_url')
|
||||
);
|
||||
$telegramService->sendMessage($message->chat_id, $text, 'markdown');
|
||||
}
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Plugins\Telegram\Commands;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Models\User;
|
||||
use App\Plugins\Telegram\Telegram;
|
||||
use App\Services\TicketService;
|
||||
|
||||
class ReplyTicket extends Telegram {
|
||||
public $regex = '/[#](.*)/';
|
||||
public $description = '快速工单回复';
|
||||
|
||||
public function handle($message, $match = []) {
|
||||
if (!$message->is_private) return;
|
||||
$this->replayTicket($message, $match[1]);
|
||||
}
|
||||
|
||||
|
||||
private function replayTicket($msg, $ticketId)
|
||||
{
|
||||
$user = User::where('telegram_id', $msg->chat_id)->first();
|
||||
if (!$user) {
|
||||
throw new ApiException('用户不存在');
|
||||
}
|
||||
if (!$msg->text) return;
|
||||
if (!($user->is_admin || $user->is_staff)) return;
|
||||
$ticketService = new TicketService();
|
||||
$ticketService->replyByAdmin(
|
||||
$ticketId,
|
||||
$msg->text,
|
||||
$user->id
|
||||
);
|
||||
$telegramService = $this->telegramService;
|
||||
$telegramService->sendMessage($msg->chat_id, "#`{$ticketId}` 的工单已回复成功", 'markdown');
|
||||
$telegramService->sendMessageWithAdmin("#`{$ticketId}` 的工单已由 {$user->email} 进行回复", true);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Plugins\Telegram\Commands;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Plugins\Telegram\Telegram;
|
||||
use App\Utils\Helper;
|
||||
|
||||
class Traffic extends Telegram {
|
||||
public $command = '/traffic';
|
||||
public $description = '查询流量信息';
|
||||
|
||||
public function handle($message, $match = []) {
|
||||
$telegramService = $this->telegramService;
|
||||
if (!$message->is_private) return;
|
||||
$user = User::where('telegram_id', $message->chat_id)->first();
|
||||
if (!$user) {
|
||||
$telegramService->sendMessage($message->chat_id, '没有查询到您的用户信息,请先绑定账号', 'markdown');
|
||||
return;
|
||||
}
|
||||
$transferEnable = Helper::trafficConvert($user->transfer_enable);
|
||||
$up = Helper::trafficConvert($user->u);
|
||||
$down = Helper::trafficConvert($user->d);
|
||||
$remaining = Helper::trafficConvert($user->transfer_enable - ($user->u + $user->d));
|
||||
$text = "🚥流量查询\n———————————————\n计划流量:`{$transferEnable}`\n已用上行:`{$up}`\n已用下行:`{$down}`\n剩余流量:`{$remaining}`";
|
||||
$telegramService->sendMessage($message->chat_id, $text, 'markdown');
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Plugins\Telegram\Commands;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Models\User;
|
||||
use App\Plugins\Telegram\Telegram;
|
||||
|
||||
class UnBind extends Telegram {
|
||||
public $command = '/unbind';
|
||||
public $description = '将Telegram账号从网站解绑';
|
||||
|
||||
public function handle($message, $match = []) {
|
||||
if (!$message->is_private) return;
|
||||
$user = User::where('telegram_id', $message->chat_id)->first();
|
||||
$telegramService = $this->telegramService;
|
||||
if (!$user) {
|
||||
$telegramService->sendMessage($message->chat_id, '没有查询到您的用户信息,请先绑定账号', 'markdown');
|
||||
return;
|
||||
}
|
||||
$user->telegram_id = NULL;
|
||||
if (!$user->save()) {
|
||||
throw new ApiException('解绑失败');
|
||||
}
|
||||
$telegramService->sendMessage($message->chat_id, '解绑成功', 'markdown');
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Plugins\Telegram;
|
||||
|
||||
use App\Services\TelegramService;
|
||||
|
||||
abstract class Telegram {
|
||||
abstract protected function handle($message, $match);
|
||||
public $telegramService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->telegramService = new TelegramService();
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -134,9 +134,7 @@ class Setting
|
||||
*/
|
||||
private function flush(): void
|
||||
{
|
||||
// 清除共享的Redis缓存
|
||||
$this->cache->forget(self::CACHE_KEY);
|
||||
// 清除当前请求的实例内存缓存
|
||||
$this->loadedSettings = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ use Illuminate\Support\Arr;
|
||||
|
||||
class Helper
|
||||
{
|
||||
private static $subscribeUrlCache = null;
|
||||
|
||||
public static function uuidToBase64($uuid, $length)
|
||||
{
|
||||
return base64_encode(substr($uuid, 0, $length));
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('v2_plugins', function (Blueprint $table) {
|
||||
$table->string('type', 20)->default('feature')->after('code')->comment('插件类型:feature功能性,payment支付型');
|
||||
$table->index(['type', 'is_enabled']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('v2_plugins', function (Blueprint $table) {
|
||||
$table->dropIndex(['type', 'is_enabled']);
|
||||
$table->dropColumn('type');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -1,164 +0,0 @@
|
||||
<?php
|
||||
namespace Library;
|
||||
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class AlipayF2F {
|
||||
private $appId;
|
||||
private $privateKey;
|
||||
private $alipayPublicKey;
|
||||
private $signType = 'RSA2';
|
||||
public $bizContent;
|
||||
public $method;
|
||||
public $notifyUrl;
|
||||
public $response;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public function verify($data): bool
|
||||
{
|
||||
if (is_string($data)) {
|
||||
parse_str($data, $data);
|
||||
}
|
||||
$sign = $data['sign'];
|
||||
unset($data['sign']);
|
||||
unset($data['sign_type']);
|
||||
ksort($data);
|
||||
$data = $this->buildQuery($data);
|
||||
$res = "-----BEGIN PUBLIC KEY-----\n" .
|
||||
wordwrap($this->alipayPublicKey, 64, "\n", true) .
|
||||
"\n-----END PUBLIC KEY-----";
|
||||
if ("RSA2" == $this->signType) {
|
||||
$result = (openssl_verify($data, base64_decode($sign), $res, OPENSSL_ALGO_SHA256) === 1);
|
||||
} else {
|
||||
$result = (openssl_verify($data, base64_decode($sign), $res) === 1);
|
||||
}
|
||||
openssl_free_key(openssl_get_publickey($res));
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function setBizContent($bizContent = [])
|
||||
{
|
||||
$this->bizContent = json_encode($bizContent);
|
||||
}
|
||||
|
||||
public function setMethod($method)
|
||||
{
|
||||
$this->method = $method;
|
||||
}
|
||||
|
||||
public function setAppId($appId)
|
||||
{
|
||||
$this->appId = $appId;
|
||||
}
|
||||
|
||||
public function setPrivateKey($privateKey)
|
||||
{
|
||||
$this->privateKey = $privateKey;
|
||||
}
|
||||
|
||||
public function setAlipayPublicKey($alipayPublicKey)
|
||||
{
|
||||
$this->alipayPublicKey = $alipayPublicKey;
|
||||
}
|
||||
|
||||
public function setNotifyUrl($url)
|
||||
{
|
||||
$this->notifyUrl = $url;
|
||||
}
|
||||
|
||||
public function send()
|
||||
{
|
||||
$response = Http::get('https://openapi.alipay.com/gateway.do', $this->buildParam())->json();
|
||||
$resKey = str_replace('.', '_', $this->method) . '_response';
|
||||
if (!isset($response[$resKey])) throw new \Exception('从支付宝请求失败');
|
||||
$response = $response[$resKey];
|
||||
if ($response['msg'] !== 'Success') throw new \Exception($response['sub_msg']);
|
||||
$this->response = $response;
|
||||
}
|
||||
|
||||
public function getQrCodeUrl()
|
||||
{
|
||||
$response = $this->response;
|
||||
if (!isset($response['qr_code'])) throw new \Exception('获取付款二维码失败');
|
||||
return $response['qr_code'];
|
||||
}
|
||||
|
||||
public function getResponse()
|
||||
{
|
||||
return $this->response;
|
||||
}
|
||||
|
||||
public function buildParam(): array
|
||||
{
|
||||
$params = [
|
||||
'app_id' => $this->appId,
|
||||
'method' => $this->method,
|
||||
'charset' => 'UTF-8',
|
||||
'sign_type' => $this->signType,
|
||||
'timestamp' => date('Y-m-d H:m:s'),
|
||||
'biz_content' => $this->bizContent,
|
||||
'version' => '1.0',
|
||||
'_input_charset' => 'UTF-8'
|
||||
];
|
||||
if ($this->notifyUrl) $params['notify_url'] = $this->notifyUrl;
|
||||
ksort($params);
|
||||
$params['sign'] = $this->buildSign($this->buildQuery($params));
|
||||
return $params;
|
||||
}
|
||||
|
||||
public function buildQuery($query)
|
||||
{
|
||||
if (!$query) {
|
||||
throw new \Exception('参数构造错误');
|
||||
}
|
||||
//将要 参数 排序
|
||||
ksort($query);
|
||||
|
||||
//重新组装参数
|
||||
$params = array();
|
||||
foreach ($query as $key => $value) {
|
||||
$params[] = $key . '=' . $value;
|
||||
}
|
||||
$data = implode('&', $params);
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function buildSign(string $signData): string
|
||||
{
|
||||
$privateKey = $this->privateKey;
|
||||
$p_key = array();
|
||||
//如果私钥是 1行
|
||||
if (!stripos($privateKey, "\n")) {
|
||||
$i = 0;
|
||||
while ($key_str = substr($privateKey, $i * 64, 64)) {
|
||||
$p_key[] = $key_str;
|
||||
$i++;
|
||||
}
|
||||
}
|
||||
$privateKey = "-----BEGIN RSA PRIVATE KEY-----\n" . implode("\n", $p_key);
|
||||
$privateKey = $privateKey . "\n-----END RSA PRIVATE KEY-----";
|
||||
|
||||
//私钥
|
||||
$privateId = openssl_pkey_get_private($privateKey, '');
|
||||
|
||||
// 签名
|
||||
$signature = '';
|
||||
|
||||
if ("RSA2" == $this->signType) {
|
||||
|
||||
openssl_sign($signData, $signature, $privateId, OPENSSL_ALGO_SHA256);
|
||||
} else {
|
||||
|
||||
openssl_sign($signData, $signature, $privateId, OPENSSL_ALGO_SHA1);
|
||||
}
|
||||
|
||||
openssl_free_key($privateId);
|
||||
|
||||
//加密后的内容通常含有特殊字符,需要编码转换下
|
||||
$signature = base64_encode($signature);
|
||||
return $signature;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace Plugin\AlipayF2f;
|
||||
|
||||
use App\Services\Plugin\AbstractPlugin;
|
||||
use App\Contracts\PaymentInterface;
|
||||
use App\Exceptions\ApiException;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Plugin\AlipayF2f\library\AlipayF2F;
|
||||
|
||||
class Plugin extends AbstractPlugin implements PaymentInterface
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
$this->filter('available_payment_methods', function ($methods) {
|
||||
if ($this->getConfig('enabled', true)) {
|
||||
$methods['AlipayF2F'] = [
|
||||
'name' => $this->getConfig('display_name', '支付宝当面付'),
|
||||
'icon' => $this->getConfig('icon', '💙'),
|
||||
'plugin_code' => $this->getPluginCode(),
|
||||
'type' => 'plugin'
|
||||
];
|
||||
}
|
||||
return $methods;
|
||||
});
|
||||
}
|
||||
|
||||
public function form(): array
|
||||
{
|
||||
return [
|
||||
'app_id' => [
|
||||
'label' => '支付宝APPID',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => '支付宝开放平台应用的APPID'
|
||||
],
|
||||
'private_key' => [
|
||||
'label' => '支付宝私钥',
|
||||
'type' => 'textarea',
|
||||
'required' => true,
|
||||
'description' => '应用私钥,用于签名'
|
||||
],
|
||||
'public_key' => [
|
||||
'label' => '支付宝公钥',
|
||||
'type' => 'textarea',
|
||||
'required' => true,
|
||||
'description' => '支付宝公钥,用于验签'
|
||||
],
|
||||
'product_name' => [
|
||||
'label' => '自定义商品名称',
|
||||
'type' => 'string',
|
||||
'description' => '将会体现在支付宝账单中'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function pay($order): array
|
||||
{
|
||||
try {
|
||||
$gateway = new AlipayF2F();
|
||||
$gateway->setMethod('alipay.trade.precreate');
|
||||
$gateway->setAppId($this->getConfig('app_id'));
|
||||
$gateway->setPrivateKey($this->getConfig('private_key'));
|
||||
$gateway->setAlipayPublicKey($this->getConfig('public_key'));
|
||||
$gateway->setNotifyUrl($order['notify_url']);
|
||||
$gateway->setBizContent([
|
||||
'subject' => $this->getConfig('product_name') ?? (admin_setting('app_name', 'XBoard') . ' - 订阅'),
|
||||
'out_trade_no' => $order['trade_no'],
|
||||
'total_amount' => $order['total_amount'] / 100
|
||||
]);
|
||||
$gateway->send();
|
||||
return [
|
||||
'type' => 0,
|
||||
'data' => $gateway->getQrCodeUrl()
|
||||
];
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
throw new ApiException($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
public function notify($params): array|bool
|
||||
{
|
||||
if ($params['trade_status'] !== 'TRADE_SUCCESS')
|
||||
return false;
|
||||
|
||||
$gateway = new AlipayF2F();
|
||||
$gateway->setAppId($this->getConfig('app_id'));
|
||||
$gateway->setPrivateKey($this->getConfig('private_key'));
|
||||
$gateway->setAlipayPublicKey($this->getConfig('public_key'));
|
||||
|
||||
try {
|
||||
if ($gateway->verify($params)) {
|
||||
return [
|
||||
'trade_no' => $params['out_trade_no'],
|
||||
'callback_no' => $params['trade_no']
|
||||
];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "AlipayF2F",
|
||||
"code": "alipay_f2f",
|
||||
"type": "payment",
|
||||
"version": "1.0.0",
|
||||
"description": "AlipayF2F payment plugin",
|
||||
"author": "XBoard Team"
|
||||
}
|
||||
@@ -1,47 +1,60 @@
|
||||
<?php
|
||||
|
||||
namespace App\Payments;
|
||||
namespace Plugin\Btcpay;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Services\Plugin\AbstractPlugin;
|
||||
use App\Contracts\PaymentInterface;
|
||||
use App\Exceptions\ApiException;
|
||||
|
||||
class BTCPay implements PaymentInterface
|
||||
class Plugin extends AbstractPlugin implements PaymentInterface
|
||||
{
|
||||
protected $config;
|
||||
public function __construct($config)
|
||||
public function boot(): void
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->filter('available_payment_methods', function($methods) {
|
||||
if ($this->getConfig('enabled', true)) {
|
||||
$methods['BTCPay'] = [
|
||||
'name' => $this->getConfig('display_name', 'BTCPay'),
|
||||
'icon' => $this->getConfig('icon', '₿'),
|
||||
'plugin_code' => $this->getPluginCode(),
|
||||
'type' => 'plugin'
|
||||
];
|
||||
}
|
||||
return $methods;
|
||||
});
|
||||
}
|
||||
|
||||
public function form(): array
|
||||
{
|
||||
return [
|
||||
'btcpay_url' => [
|
||||
'label' => 'API接口所在网址(包含最后的斜杠)',
|
||||
'description' => '',
|
||||
'type' => 'input',
|
||||
'label' => 'API接口所在网址',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => '包含最后的斜杠,例如:https://your-btcpay.com/'
|
||||
],
|
||||
'btcpay_storeId' => [
|
||||
'label' => 'storeId',
|
||||
'description' => '',
|
||||
'type' => 'input',
|
||||
'label' => 'Store ID',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'BTCPay商店标识符'
|
||||
],
|
||||
'btcpay_api_key' => [
|
||||
'label' => 'API KEY',
|
||||
'description' => '个人设置中的API KEY(非商店设置中的)',
|
||||
'type' => 'input',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => '个人设置中的API KEY(非商店设置中的)'
|
||||
],
|
||||
'btcpay_webhook_key' => [
|
||||
'label' => 'WEBHOOK KEY',
|
||||
'description' => '',
|
||||
'type' => 'input',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'Webhook通知密钥'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function pay($order): array
|
||||
{
|
||||
|
||||
$params = [
|
||||
'jsonResponse' => true,
|
||||
'amount' => sprintf('%.2f', $order['total_amount'] / 100),
|
||||
@@ -52,16 +65,15 @@ class BTCPay implements PaymentInterface
|
||||
];
|
||||
|
||||
$params_string = @json_encode($params);
|
||||
|
||||
$ret_raw = self::_curlPost($this->config['btcpay_url'] . 'api/v1/stores/' . $this->config['btcpay_storeId'] . '/invoices', $params_string);
|
||||
|
||||
$ret_raw = $this->curlPost($this->getConfig('btcpay_url') . 'api/v1/stores/' . $this->getConfig('btcpay_storeId') . '/invoices', $params_string);
|
||||
$ret = @json_decode($ret_raw, true);
|
||||
|
||||
if (empty($ret['checkoutLink'])) {
|
||||
throw new ApiException("error!");
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 1, // Redirect to url
|
||||
'type' => 1,
|
||||
'data' => $ret['checkoutLink'],
|
||||
];
|
||||
}
|
||||
@@ -69,46 +81,38 @@ class BTCPay implements PaymentInterface
|
||||
public function notify($params): array|bool
|
||||
{
|
||||
$payload = trim(request()->getContent());
|
||||
|
||||
$headers = getallheaders();
|
||||
|
||||
//IS Btcpay-Sig
|
||||
//NOT BTCPay-Sig
|
||||
//API doc is WRONG!
|
||||
$headerName = 'Btcpay-Sig';
|
||||
$signraturHeader = isset($headers[$headerName]) ? $headers[$headerName] : '';
|
||||
$json_param = json_decode($payload, true);
|
||||
|
||||
$computedSignature = "sha256=" . \hash_hmac('sha256', $payload, $this->config['btcpay_webhook_key']);
|
||||
$computedSignature = "sha256=" . \hash_hmac('sha256', $payload, $this->getConfig('btcpay_webhook_key'));
|
||||
|
||||
if (!self::hashEqual($signraturHeader, $computedSignature)) {
|
||||
if (!$this->hashEqual($signraturHeader, $computedSignature)) {
|
||||
throw new ApiException('HMAC signature does not match', 400);
|
||||
}
|
||||
|
||||
//get order id store in metadata
|
||||
$context = stream_context_create(array(
|
||||
'http' => array(
|
||||
'method' => 'GET',
|
||||
'header' => "Authorization:" . "token " . $this->config['btcpay_api_key'] . "\r\n"
|
||||
'header' => "Authorization:" . "token " . $this->getConfig('btcpay_api_key') . "\r\n"
|
||||
)
|
||||
));
|
||||
|
||||
$invoiceDetail = file_get_contents($this->config['btcpay_url'] . 'api/v1/stores/' . $this->config['btcpay_storeId'] . '/invoices/' . $json_param['invoiceId'], false, $context);
|
||||
$invoiceDetail = file_get_contents($this->getConfig('btcpay_url') . 'api/v1/stores/' . $this->getConfig('btcpay_storeId') . '/invoices/' . $json_param['invoiceId'], false, $context);
|
||||
$invoiceDetail = json_decode($invoiceDetail, true);
|
||||
|
||||
|
||||
$out_trade_no = $invoiceDetail['metadata']["orderId"];
|
||||
$pay_trade_no = $json_param['invoiceId'];
|
||||
|
||||
return [
|
||||
'trade_no' => $out_trade_no,
|
||||
'callback_no' => $pay_trade_no
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
private function _curlPost($url, $params = false)
|
||||
private function curlPost($url, $params = false)
|
||||
{
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_HEADER, false);
|
||||
@@ -118,22 +122,15 @@ class BTCPay implements PaymentInterface
|
||||
curl_setopt(
|
||||
$ch,
|
||||
CURLOPT_HTTPHEADER,
|
||||
array('Authorization:' . 'token ' . $this->config['btcpay_api_key'], 'Content-Type: application/json')
|
||||
array('Authorization:' . 'token ' . $this->getConfig('btcpay_api_key'), 'Content-Type: application/json')
|
||||
);
|
||||
$result = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $str1
|
||||
* @param string $str2
|
||||
* @return bool
|
||||
*/
|
||||
private function hashEqual($str1, $str2)
|
||||
{
|
||||
|
||||
if (function_exists('hash_equals')) {
|
||||
return \hash_equals($str1, $str2);
|
||||
}
|
||||
@@ -150,4 +147,4 @@ class BTCPay implements PaymentInterface
|
||||
return !$ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "BTCPay",
|
||||
"code": "btcpay",
|
||||
"type": "payment",
|
||||
"version": "1.0.0",
|
||||
"description": "BTCPay payment plugin",
|
||||
"author": "XBoard Team"
|
||||
}
|
||||
@@ -1,16 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Payments;
|
||||
namespace Plugin\CoinPayments;
|
||||
|
||||
use App\Services\Plugin\AbstractPlugin;
|
||||
use App\Contracts\PaymentInterface;
|
||||
use App\Exceptions\ApiException;
|
||||
|
||||
class CoinPayments implements PaymentInterface
|
||||
class Plugin extends AbstractPlugin implements PaymentInterface
|
||||
{
|
||||
protected $config;
|
||||
public function __construct($config)
|
||||
public function boot(): void
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->filter('available_payment_methods', function($methods) {
|
||||
if ($this->getConfig('enabled', true)) {
|
||||
$methods['CoinPayments'] = [
|
||||
'name' => $this->getConfig('display_name', 'CoinPayments'),
|
||||
'icon' => $this->getConfig('icon', '💰'),
|
||||
'plugin_code' => $this->getPluginCode(),
|
||||
'type' => 'plugin'
|
||||
];
|
||||
}
|
||||
return $methods;
|
||||
});
|
||||
}
|
||||
|
||||
public function form(): array
|
||||
@@ -18,26 +28,27 @@ class CoinPayments implements PaymentInterface
|
||||
return [
|
||||
'coinpayments_merchant_id' => [
|
||||
'label' => 'Merchant ID',
|
||||
'description' => '商户 ID,填写您在 Account Settings 中得到的 ID',
|
||||
'type' => 'input',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => '商户 ID,填写您在 Account Settings 中得到的 ID'
|
||||
],
|
||||
'coinpayments_ipn_secret' => [
|
||||
'label' => 'IPN Secret',
|
||||
'description' => '通知密钥,填写您在 Merchant Settings 中自行设置的值',
|
||||
'type' => 'input',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => '通知密钥,填写您在 Merchant Settings 中自行设置的值'
|
||||
],
|
||||
'coinpayments_currency' => [
|
||||
'label' => '货币代码',
|
||||
'description' => '填写您的货币代码(大写),建议与 Merchant Settings 中的值相同',
|
||||
'type' => 'input',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => '填写您的货币代码(大写),建议与 Merchant Settings 中的值相同'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function pay($order): array
|
||||
{
|
||||
|
||||
// IPN notifications are slow, when the transaction is successful, we should return to the user center to avoid user confusion
|
||||
$parseUrl = parse_url($order['return_url']);
|
||||
$port = isset($parseUrl['port']) ? ":{$parseUrl['port']}" : '';
|
||||
$successUrl = "{$parseUrl['scheme']}://{$parseUrl['host']}{$port}";
|
||||
@@ -45,11 +56,11 @@ class CoinPayments implements PaymentInterface
|
||||
$params = [
|
||||
'cmd' => '_pay_simple',
|
||||
'reset' => 1,
|
||||
'merchant' => $this->config['coinpayments_merchant_id'],
|
||||
'merchant' => $this->getConfig('coinpayments_merchant_id'),
|
||||
'item_name' => $order['trade_no'],
|
||||
'item_number' => $order['trade_no'],
|
||||
'want_shipping' => 0,
|
||||
'currency' => $this->config['coinpayments_currency'],
|
||||
'currency' => $this->getConfig('coinpayments_currency'),
|
||||
'amountf' => sprintf('%.2f', $order['total_amount'] / 100),
|
||||
'success_url' => $successUrl,
|
||||
'cancel_url' => $order['return_url'],
|
||||
@@ -59,15 +70,14 @@ class CoinPayments implements PaymentInterface
|
||||
$params_string = http_build_query($params);
|
||||
|
||||
return [
|
||||
'type' => 1, // Redirect to url
|
||||
'type' => 1,
|
||||
'data' => 'https://www.coinpayments.net/index.php?' . $params_string
|
||||
];
|
||||
}
|
||||
|
||||
public function notify($params)
|
||||
public function notify($params): array|string
|
||||
{
|
||||
|
||||
if (!isset($params['merchant']) || $params['merchant'] != trim($this->config['coinpayments_merchant_id'])) {
|
||||
if (!isset($params['merchant']) || $params['merchant'] != trim($this->getConfig('coinpayments_merchant_id'))) {
|
||||
throw new ApiException('No or incorrect Merchant ID passed');
|
||||
}
|
||||
|
||||
@@ -80,31 +90,23 @@ class CoinPayments implements PaymentInterface
|
||||
$headerName = 'Hmac';
|
||||
$signHeader = isset($headers[$headerName]) ? $headers[$headerName] : '';
|
||||
|
||||
$hmac = hash_hmac("sha512", $request, trim($this->config['coinpayments_ipn_secret']));
|
||||
|
||||
// if ($hmac != $signHeader) { <-- Use this if you are running a version of PHP below 5.6.0 without the hash_equals function
|
||||
// throw new ApiException(400, 'HMAC signature does not match');
|
||||
// }
|
||||
$hmac = hash_hmac("sha512", $request, trim($this->getConfig('coinpayments_ipn_secret')));
|
||||
|
||||
if (!hash_equals($hmac, $signHeader)) {
|
||||
throw new ApiException('HMAC signature does not match', 400);
|
||||
}
|
||||
|
||||
// HMAC Signature verified at this point, load some variables.
|
||||
$status = $params['status'];
|
||||
if ($status >= 100 || $status == 2) {
|
||||
// payment is complete or queued for nightly payout, success
|
||||
return [
|
||||
'trade_no' => $params['item_number'],
|
||||
'callback_no' => $params['txn_id'],
|
||||
'custom_result' => 'IPN OK'
|
||||
];
|
||||
} else if ($status < 0) {
|
||||
//payment error, this is usually final but payments will sometimes be reopened if there was no exchange rate conversion or with seller consent
|
||||
throw new ApiException('Payment Timed Out or Error');
|
||||
} else {
|
||||
//payment is pending, you can optionally add a note to the order page
|
||||
return ('IPN OK: pending');
|
||||
return 'IPN OK: pending';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "CoinPayments",
|
||||
"code": "coin_payments",
|
||||
"type": "payment",
|
||||
"version": "1.0.0",
|
||||
"description": "CoinPayments payment plugin",
|
||||
"author": "XBoard Team"
|
||||
}
|
||||
@@ -1,16 +1,26 @@
|
||||
<?php
|
||||
|
||||
namespace App\Payments;
|
||||
namespace Plugin\Coinbase;
|
||||
|
||||
use App\Services\Plugin\AbstractPlugin;
|
||||
use App\Contracts\PaymentInterface;
|
||||
use App\Exceptions\ApiException;
|
||||
|
||||
class Coinbase implements PaymentInterface
|
||||
class Plugin extends AbstractPlugin implements PaymentInterface
|
||||
{
|
||||
protected $config;
|
||||
public function __construct($config)
|
||||
public function boot(): void
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->filter('available_payment_methods', function($methods) {
|
||||
if ($this->getConfig('enabled', true)) {
|
||||
$methods['Coinbase'] = [
|
||||
'name' => $this->getConfig('display_name', 'Coinbase'),
|
||||
'icon' => $this->getConfig('icon', '🪙'),
|
||||
'plugin_code' => $this->getPluginCode(),
|
||||
'type' => 'plugin'
|
||||
];
|
||||
}
|
||||
return $methods;
|
||||
});
|
||||
}
|
||||
|
||||
public function form(): array
|
||||
@@ -18,25 +28,27 @@ class Coinbase implements PaymentInterface
|
||||
return [
|
||||
'coinbase_url' => [
|
||||
'label' => '接口地址',
|
||||
'description' => '',
|
||||
'type' => 'input',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'Coinbase Commerce API地址'
|
||||
],
|
||||
'coinbase_api_key' => [
|
||||
'label' => 'API KEY',
|
||||
'description' => '',
|
||||
'type' => 'input',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'Coinbase Commerce API密钥'
|
||||
],
|
||||
'coinbase_webhook_key' => [
|
||||
'label' => 'WEBHOOK KEY',
|
||||
'description' => '',
|
||||
'type' => 'input',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'Webhook签名验证密钥'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function pay($order): array
|
||||
{
|
||||
|
||||
$params = [
|
||||
'name' => '订阅套餐',
|
||||
'description' => '订单号 ' . $order['trade_no'],
|
||||
@@ -51,14 +63,13 @@ class Coinbase implements PaymentInterface
|
||||
];
|
||||
|
||||
$params_string = http_build_query($params);
|
||||
|
||||
$ret_raw = self::_curlPost($this->config['coinbase_url'], $params_string);
|
||||
|
||||
$ret_raw = $this->curlPost($this->getConfig('coinbase_url'), $params_string);
|
||||
$ret = @json_decode($ret_raw, true);
|
||||
|
||||
if (empty($ret['data']['hosted_url'])) {
|
||||
throw new ApiException("error!");
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 1,
|
||||
'data' => $ret['data']['hosted_url'],
|
||||
@@ -67,32 +78,29 @@ class Coinbase implements PaymentInterface
|
||||
|
||||
public function notify($params): array
|
||||
{
|
||||
|
||||
$payload = trim(request()->getContent());
|
||||
$json_param = json_decode($payload, true);
|
||||
|
||||
|
||||
$headerName = 'X-Cc-Webhook-Signature';
|
||||
$headers = getallheaders();
|
||||
$signatureHeader = isset($headers[$headerName]) ? $headers[$headerName] : '';
|
||||
$computedSignature = \hash_hmac('sha256', $payload, $this->config['coinbase_webhook_key']);
|
||||
$computedSignature = \hash_hmac('sha256', $payload, $this->getConfig('coinbase_webhook_key'));
|
||||
|
||||
if (!self::hashEqual($signatureHeader, $computedSignature)) {
|
||||
if (!$this->hashEqual($signatureHeader, $computedSignature)) {
|
||||
throw new ApiException('HMAC signature does not match', 400);
|
||||
}
|
||||
|
||||
$out_trade_no = $json_param['event']['data']['metadata']['outTradeNo'];
|
||||
$pay_trade_no = $json_param['event']['id'];
|
||||
|
||||
return [
|
||||
'trade_no' => $out_trade_no,
|
||||
'callback_no' => $pay_trade_no
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
private function _curlPost($url, $params = false)
|
||||
private function curlPost($url, $params = false)
|
||||
{
|
||||
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_HEADER, false);
|
||||
@@ -102,20 +110,14 @@ class Coinbase implements PaymentInterface
|
||||
curl_setopt(
|
||||
$ch,
|
||||
CURLOPT_HTTPHEADER,
|
||||
array('X-CC-Api-Key:' . $this->config['coinbase_api_key'], 'X-CC-Version: 2018-03-22')
|
||||
array('X-CC-Api-Key:' . $this->getConfig('coinbase_api_key'), 'X-CC-Version: 2018-03-22')
|
||||
);
|
||||
$result = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string $str1
|
||||
* @param string $str2
|
||||
* @return bool
|
||||
*/
|
||||
public function hashEqual($str1, $str2)
|
||||
private function hashEqual($str1, $str2)
|
||||
{
|
||||
if (function_exists('hash_equals')) {
|
||||
return \hash_equals($str1, $str2);
|
||||
@@ -133,4 +135,4 @@ class Coinbase implements PaymentInterface
|
||||
return !$ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Coinbase",
|
||||
"code": "coinbase",
|
||||
"type": "payment",
|
||||
"version": "1.0.0",
|
||||
"description": "Coinbase payment plugin",
|
||||
"author": "XBoard Team"
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Plugin\Epay;
|
||||
|
||||
use App\Services\Plugin\AbstractPlugin;
|
||||
use App\Contracts\PaymentInterface;
|
||||
|
||||
class Plugin extends AbstractPlugin implements PaymentInterface
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
$this->filter('available_payment_methods', function ($methods) {
|
||||
if ($this->getConfig('enabled', true)) {
|
||||
$methods['EPay'] = [
|
||||
'name' => $this->getConfig('display_name', '易支付'),
|
||||
'icon' => $this->getConfig('icon', '💳'),
|
||||
'plugin_code' => $this->getPluginCode(),
|
||||
'type' => 'plugin'
|
||||
];
|
||||
}
|
||||
return $methods;
|
||||
});
|
||||
}
|
||||
|
||||
public function form(): array
|
||||
{
|
||||
return [
|
||||
'url' => [
|
||||
'label' => '支付网关地址',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => '请填写完整的支付网关地址,包括协议(http或https)'
|
||||
],
|
||||
'pid' => [
|
||||
'label' => '商户ID',
|
||||
'type' => 'string',
|
||||
'description' => '请填写商户ID',
|
||||
'required' => true
|
||||
],
|
||||
'key' => [
|
||||
'label' => '通信密钥',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => '请填写通信密钥'
|
||||
],
|
||||
'type' => [
|
||||
'label' => '支付类型',
|
||||
'type' => 'select',
|
||||
'options' => [
|
||||
['value' => 'alipay', 'label' => '支付宝'],
|
||||
['value' => 'wxpay', 'label' => '微信支付'],
|
||||
['value' => 'qqpay', 'label' => 'QQ钱包']
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function pay($order): array
|
||||
{
|
||||
$params = [
|
||||
'money' => $order['total_amount'] / 100,
|
||||
'name' => $order['trade_no'],
|
||||
'notify_url' => $order['notify_url'],
|
||||
'return_url' => $order['return_url'],
|
||||
'out_trade_no' => $order['trade_no'],
|
||||
'pid' => $this->getConfig('pid')
|
||||
];
|
||||
|
||||
if ($paymentType = $this->getConfig('type')) {
|
||||
$params['type'] = $paymentType;
|
||||
}
|
||||
|
||||
ksort($params);
|
||||
$str = stripslashes(urldecode(http_build_query($params))) . $this->getConfig('key');
|
||||
$params['sign'] = md5($str);
|
||||
$params['sign_type'] = 'MD5';
|
||||
|
||||
return [
|
||||
'type' => 1,
|
||||
'data' => $this->getConfig('url') . '/submit.php?' . http_build_query($params)
|
||||
];
|
||||
}
|
||||
|
||||
public function notify($params): array|bool
|
||||
{
|
||||
$sign = $params['sign'];
|
||||
unset($params['sign'], $params['sign_type']);
|
||||
ksort($params);
|
||||
$str = stripslashes(urldecode(http_build_query($params))) . $this->getConfig('key');
|
||||
|
||||
if ($sign !== md5($str)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'trade_no' => $params['out_trade_no'],
|
||||
'callback_no' => $params['trade_no']
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "EPay",
|
||||
"code": "epay",
|
||||
"type": "payment",
|
||||
"version": "1.0.0",
|
||||
"description": "EPay payment plugin",
|
||||
"author": "XBoard Team"
|
||||
}
|
||||
@@ -1,20 +1,27 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 自己写别抄,抄NMB抄
|
||||
*/
|
||||
namespace App\Payments;
|
||||
namespace Plugin\Mgate;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use \Curl\Curl;
|
||||
use App\Services\Plugin\AbstractPlugin;
|
||||
use App\Contracts\PaymentInterface;
|
||||
class MGate implements PaymentInterface
|
||||
{
|
||||
private $config;
|
||||
use App\Exceptions\ApiException;
|
||||
use Curl\Curl;
|
||||
|
||||
public function __construct($config)
|
||||
class Plugin extends AbstractPlugin implements PaymentInterface
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
$this->config = $config;
|
||||
$this->filter('available_payment_methods', function ($methods) {
|
||||
if ($this->getConfig('enabled', true)) {
|
||||
$methods['MGate'] = [
|
||||
'name' => $this->getConfig('display_name', 'MGate'),
|
||||
'icon' => $this->getConfig('icon', '🏛️'),
|
||||
'plugin_code' => $this->getPluginCode(),
|
||||
'type' => 'plugin'
|
||||
];
|
||||
}
|
||||
return $methods;
|
||||
});
|
||||
}
|
||||
|
||||
public function form(): array
|
||||
@@ -22,23 +29,26 @@ class MGate implements PaymentInterface
|
||||
return [
|
||||
'mgate_url' => [
|
||||
'label' => 'API地址',
|
||||
'description' => '',
|
||||
'type' => 'input',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'MGate支付网关API地址'
|
||||
],
|
||||
'mgate_app_id' => [
|
||||
'label' => 'APPID',
|
||||
'description' => '',
|
||||
'type' => 'input',
|
||||
'label' => 'APP ID',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'MGate应用标识符'
|
||||
],
|
||||
'mgate_app_secret' => [
|
||||
'label' => 'AppSecret',
|
||||
'description' => '',
|
||||
'type' => 'input',
|
||||
'label' => 'App Secret',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'MGate应用密钥'
|
||||
],
|
||||
'mgate_source_currency' => [
|
||||
'label' => '源货币',
|
||||
'description' => '默认CNY',
|
||||
'type' => 'input'
|
||||
'type' => 'string',
|
||||
'description' => '默认CNY,源货币类型'
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -51,21 +61,26 @@ class MGate implements PaymentInterface
|
||||
'notify_url' => $order['notify_url'],
|
||||
'return_url' => $order['return_url']
|
||||
];
|
||||
if (isset($this->config['mgate_source_currency'])) {
|
||||
$params['source_currency'] = $this->config['mgate_source_currency'];
|
||||
|
||||
if ($this->getConfig('mgate_source_currency')) {
|
||||
$params['source_currency'] = $this->getConfig('mgate_source_currency');
|
||||
}
|
||||
$params['app_id'] = $this->config['mgate_app_id'];
|
||||
|
||||
$params['app_id'] = $this->getConfig('mgate_app_id');
|
||||
ksort($params);
|
||||
$str = http_build_query($params) . $this->config['mgate_app_secret'];
|
||||
$str = http_build_query($params) . $this->getConfig('mgate_app_secret');
|
||||
$params['sign'] = md5($str);
|
||||
|
||||
$curl = new Curl();
|
||||
$curl->setUserAgent('MGate');
|
||||
$curl->setOpt(CURLOPT_SSL_VERIFYPEER, 0);
|
||||
$curl->post($this->config['mgate_url'] . '/v1/gateway/fetch', http_build_query($params));
|
||||
$curl->post($this->getConfig('mgate_url') . '/v1/gateway/fetch', http_build_query($params));
|
||||
$result = $curl->response;
|
||||
|
||||
if (!$result) {
|
||||
throw new ApiException('网络异常');
|
||||
}
|
||||
|
||||
if ($curl->error) {
|
||||
if (isset($result->errors)) {
|
||||
$errors = (array) $result->errors;
|
||||
@@ -76,12 +91,15 @@ class MGate implements PaymentInterface
|
||||
}
|
||||
throw new ApiException('未知错误');
|
||||
}
|
||||
|
||||
$curl->close();
|
||||
|
||||
if (!isset($result->data->trade_no)) {
|
||||
throw new ApiException('接口请求失败');
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 1, // 0:qrcode 1:url
|
||||
'type' => 1,
|
||||
'data' => $result->data->pay_url
|
||||
];
|
||||
}
|
||||
@@ -92,13 +110,15 @@ class MGate implements PaymentInterface
|
||||
unset($params['sign']);
|
||||
ksort($params);
|
||||
reset($params);
|
||||
$str = http_build_query($params) . $this->config['mgate_app_secret'];
|
||||
$str = http_build_query($params) . $this->getConfig('mgate_app_secret');
|
||||
|
||||
if ($sign !== md5($str)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'trade_no' => $params['out_trade_no'],
|
||||
'callback_no' => $params['trade_no']
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "MGate",
|
||||
"code": "mgate",
|
||||
"type": "payment",
|
||||
"version": "1.0.0",
|
||||
"description": "MGate payment plugin",
|
||||
"author": "XBoard Team"
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace Plugin\Smogate;
|
||||
|
||||
use App\Services\Plugin\AbstractPlugin;
|
||||
use App\Contracts\PaymentInterface;
|
||||
use Curl\Curl;
|
||||
|
||||
class Plugin extends AbstractPlugin implements PaymentInterface
|
||||
{
|
||||
public function boot(): void
|
||||
{
|
||||
$this->filter('available_payment_methods', function($methods) {
|
||||
if ($this->getConfig('enabled', true)) {
|
||||
$methods['Smogate'] = [
|
||||
'name' => $this->getConfig('display_name', 'Smogate'),
|
||||
'icon' => $this->getConfig('icon', '🔥'),
|
||||
'plugin_code' => $this->getPluginCode(),
|
||||
'type' => 'plugin'
|
||||
];
|
||||
}
|
||||
return $methods;
|
||||
});
|
||||
}
|
||||
|
||||
public function form(): array
|
||||
{
|
||||
return [
|
||||
'smogate_app_id' => [
|
||||
'label' => 'APP ID',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'Smogate -> 接入文档和密钥 -> 查看APPID和密钥'
|
||||
],
|
||||
'smogate_app_secret' => [
|
||||
'label' => 'APP Secret',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'Smogate -> 接入文档和密钥 -> 查看APPID和密钥'
|
||||
],
|
||||
'smogate_source_currency' => [
|
||||
'label' => '源货币',
|
||||
'type' => 'string',
|
||||
'description' => '默认CNY,源货币类型'
|
||||
],
|
||||
'smogate_method' => [
|
||||
'label' => '支付方式',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'description' => 'Smogate支付方式标识'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function pay($order): array
|
||||
{
|
||||
$params = [
|
||||
'out_trade_no' => $order['trade_no'],
|
||||
'total_amount' => $order['total_amount'],
|
||||
'notify_url' => $order['notify_url'],
|
||||
'method' => $this->getConfig('smogate_method')
|
||||
];
|
||||
|
||||
if ($this->getConfig('smogate_source_currency')) {
|
||||
$params['source_currency'] = strtolower($this->getConfig('smogate_source_currency'));
|
||||
}
|
||||
|
||||
$params['app_id'] = $this->getConfig('smogate_app_id');
|
||||
ksort($params);
|
||||
$str = http_build_query($params) . $this->getConfig('smogate_app_secret');
|
||||
$params['sign'] = md5($str);
|
||||
|
||||
$curl = new Curl();
|
||||
$curl->setUserAgent("Smogate {$this->getConfig('smogate_app_id')}");
|
||||
$curl->setOpt(CURLOPT_SSL_VERIFYPEER, 0);
|
||||
$curl->post("https://{$this->getConfig('smogate_app_id')}.vless.org/v1/gateway/pay", http_build_query($params));
|
||||
$result = $curl->response;
|
||||
|
||||
if (!$result) {
|
||||
abort(500, '网络异常');
|
||||
}
|
||||
|
||||
if ($curl->error) {
|
||||
if (isset($result->errors)) {
|
||||
$errors = (array)$result->errors;
|
||||
abort(500, $errors[array_keys($errors)[0]][0]);
|
||||
}
|
||||
if (isset($result->message)) {
|
||||
abort(500, $result->message);
|
||||
}
|
||||
abort(500, '未知错误');
|
||||
}
|
||||
|
||||
$curl->close();
|
||||
|
||||
if (!isset($result->data)) {
|
||||
abort(500, '请求失败');
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => $this->isMobile() ? 1 : 0,
|
||||
'data' => $result->data
|
||||
];
|
||||
}
|
||||
|
||||
public function notify($params): array|bool
|
||||
{
|
||||
$sign = $params['sign'];
|
||||
unset($params['sign']);
|
||||
ksort($params);
|
||||
reset($params);
|
||||
$str = http_build_query($params) . $this->getConfig('smogate_app_secret');
|
||||
|
||||
if ($sign !== md5($str)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return [
|
||||
'trade_no' => $params['out_trade_no'],
|
||||
'callback_no' => $params['trade_no']
|
||||
];
|
||||
}
|
||||
|
||||
private function isMobile(): bool
|
||||
{
|
||||
return strpos(strtolower($_SERVER['HTTP_USER_AGENT']), 'mobile') !== false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "Smogate",
|
||||
"code": "smogate",
|
||||
"type": "payment",
|
||||
"version": "1.0.0",
|
||||
"description": "Smogate payment plugin",
|
||||
"author": "XBoard Team"
|
||||
}
|
||||
@@ -0,0 +1,425 @@
|
||||
<?php
|
||||
|
||||
namespace Plugin\Telegram;
|
||||
|
||||
use App\Models\Order;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\User;
|
||||
use App\Services\Plugin\AbstractPlugin;
|
||||
use App\Services\Plugin\HookManager;
|
||||
use App\Services\TelegramService;
|
||||
use App\Services\TicketService;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class Plugin extends AbstractPlugin
|
||||
{
|
||||
protected array $commands = [];
|
||||
protected TelegramService $telegramService;
|
||||
|
||||
protected array $commandConfigs = [
|
||||
'/start' => ['description' => '开始使用', 'handler' => 'handleStartCommand'],
|
||||
'/bind' => ['description' => '绑定账号', 'handler' => 'handleBindCommand'],
|
||||
'/traffic' => ['description' => '查看流量', 'handler' => 'handleTrafficCommand'],
|
||||
'/getlatesturl' => ['description' => '获取订阅链接', 'handler' => 'handleGetLatestUrlCommand'],
|
||||
'/unbind' => ['description' => '解绑账号', 'handler' => 'handleUnbindCommand'],
|
||||
];
|
||||
|
||||
public function boot(): void
|
||||
{
|
||||
$this->telegramService = new TelegramService();
|
||||
$this->registerDefaultCommands();
|
||||
|
||||
$this->filter('telegram.message.handle', [$this, 'handleMessage'], 10);
|
||||
$this->listen('telegram.message.unhandled', [$this, 'handleUnknownCommand'], 10);
|
||||
$this->listen('telegram.message.error', [$this, 'handleError'], 10);
|
||||
$this->filter('telegram.bot.commands', [$this, 'addBotCommands'], 10);
|
||||
$this->listen('ticket.create.after', [$this, 'sendTicketNotify'], 10);
|
||||
$this->listen('ticket.reply.user.after', [$this, 'sendTicketNotify'], 10);
|
||||
$this->listen('payment.notify.success', [$this, 'sendPaymentNotify'], 10);
|
||||
}
|
||||
|
||||
public function sendPaymentNotify(Order $order): void
|
||||
{
|
||||
if (!$this->getConfig('enable_payment_notify', true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$payment = $order->payment;
|
||||
if (!$payment) {
|
||||
Log::warning('支付通知失败:订单关联的支付方式不存在', ['order_id' => $order->id]);
|
||||
return;
|
||||
}
|
||||
|
||||
$message = sprintf(
|
||||
"💰成功收款%s元\n" .
|
||||
"———————————————\n" .
|
||||
"支付接口:%s\n" .
|
||||
"支付渠道:%s\n" .
|
||||
"本站订单:`%s`",
|
||||
$order->total_amount / 100,
|
||||
$payment->payment,
|
||||
$payment->name,
|
||||
$order->trade_no
|
||||
);
|
||||
$this->telegramService->sendMessageWithAdmin($message, true);
|
||||
}
|
||||
|
||||
public function sendTicketNotify(array $data): void
|
||||
{
|
||||
if (!$this->getConfig('enable_ticket_notify', true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
[$ticket, $message] = $data;
|
||||
$user = User::find($ticket->user_id);
|
||||
if (!$user)
|
||||
return;
|
||||
$user->load('plan');
|
||||
$transfer_enable = Helper::transferToGB($user->transfer_enable);
|
||||
$remaining_traffic = Helper::transferToGB($user->transfer_enable - $user->u - $user->d);
|
||||
$u = Helper::transferToGB($user->u);
|
||||
$d = Helper::transferToGB($user->d);
|
||||
$expired_at = $user->expired_at ? date('Y-m-d H:i:s', $user->expired_at) : '';
|
||||
$money = $user->balance / 100;
|
||||
$affmoney = $user->commission_balance / 100;
|
||||
$plan = $user->plan;
|
||||
$ip = request()?->ip() ?? '';
|
||||
$region = $ip ? (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) ? (new \Ip2Region())->simple($ip) : 'NULL') : '';
|
||||
$TGmessage = "📮工单提醒 #{$ticket->id}\n———————————————\n";
|
||||
$TGmessage .= "邮箱: `{$user->email}`\n";
|
||||
$TGmessage .= "用户位置: \n`{$region}`\n";
|
||||
if ($plan) {
|
||||
$TGmessage .= "套餐与流量: \n`{$plan->name} {$transfer_enable}/{$remaining_traffic}`\n";
|
||||
$TGmessage .= "上传/下载: \n`{$u}/{$d}`\n";
|
||||
$TGmessage .= "到期时间: \n`{$expired_at}`\n";
|
||||
} else {
|
||||
$TGmessage .= "套餐与流量: \n`未订购任何套餐`\n";
|
||||
}
|
||||
$TGmessage .= "余额/佣金余额: \n`{$money}/{$affmoney}`\n";
|
||||
$TGmessage .= "主题:\n`{$ticket->subject}`\n内容:\n`{$message->message}`\n";
|
||||
$this->telegramService->sendMessageWithAdmin($TGmessage, true);
|
||||
}
|
||||
|
||||
protected function registerDefaultCommands(): void
|
||||
{
|
||||
foreach ($this->commandConfigs as $command => $config) {
|
||||
$this->registerTelegramCommand($command, [$this, $config['handler']]);
|
||||
}
|
||||
|
||||
$this->registerReplyHandler('/(工单提醒 #?|工单ID: ?)(\\d+)/', [$this, 'handleTicketReply']);
|
||||
}
|
||||
|
||||
public function registerTelegramCommand(string $command, callable $handler): void
|
||||
{
|
||||
$this->commands['commands'][$command] = $handler;
|
||||
}
|
||||
|
||||
public function registerReplyHandler(string $regex, callable $handler): void
|
||||
{
|
||||
$this->commands['replies'][$regex] = $handler;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息给用户
|
||||
*/
|
||||
protected function sendMessage(object $msg, string $message): void
|
||||
{
|
||||
$this->telegramService->sendMessage($msg->chat_id, $message, 'markdown');
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为私聊
|
||||
*/
|
||||
protected function checkPrivateChat(object $msg): bool
|
||||
{
|
||||
if (!$msg->is_private) {
|
||||
$this->sendMessage($msg, '请在私聊中使用此命令');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取绑定的用户
|
||||
*/
|
||||
protected function getBoundUser(object $msg): ?User
|
||||
{
|
||||
$user = User::where('telegram_id', $msg->chat_id)->first();
|
||||
if (!$user) {
|
||||
$this->sendMessage($msg, '请先绑定账号');
|
||||
return null;
|
||||
}
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function handleStartCommand(object $msg): void
|
||||
{
|
||||
$welcomeTitle = $this->getConfig('start_welcome_title', '🎉 欢迎使用 XBoard Telegram Bot!');
|
||||
$botDescription = $this->getConfig('start_bot_description', '🤖 我是您的专属助手,可以帮助您:\\n• 绑定您的 XBoard 账号\\n• 查看流量使用情况\\n• 获取最新订阅链接\\n• 管理账号绑定状态');
|
||||
$footer = $this->getConfig('start_footer', '💡 提示:所有命令都需要在私聊中使用');
|
||||
|
||||
$welcomeText = $welcomeTitle . "\n\n" . $botDescription . "\n\n";
|
||||
|
||||
$user = User::where('telegram_id', $msg->chat_id)->first();
|
||||
if ($user) {
|
||||
$welcomeText .= "✅ 您已绑定账号:{$user->email}\n\n";
|
||||
$welcomeText .= $this->getConfig('start_unbind_guide', '📋 可用命令:\\n/traffic - 查看流量使用情况\\n/getlatesturl - 获取订阅链接\\n/unbind - 解绑账号');
|
||||
} else {
|
||||
$welcomeText .= $this->getConfig('start_bind_guide', '🔗 请先绑定您的 XBoard 账号:\\n1. 登录您的 XBoard 账户\\n2. 复制您的订阅链接\\n3. 发送 /bind + 订阅链接') . "\n\n";
|
||||
$welcomeText .= $this->getConfig('start_bind_commands', '📋 可用命令:\\n/bind [订阅链接] - 绑定账号');
|
||||
}
|
||||
|
||||
$welcomeText .= "\n\n" . $footer;
|
||||
$welcomeText = str_replace('\\n', "\n", $welcomeText);
|
||||
|
||||
$this->sendMessage($msg, $welcomeText);
|
||||
}
|
||||
|
||||
public function handleMessage(bool $handled, array $data): bool
|
||||
{
|
||||
list($msg) = $data;
|
||||
if ($handled)
|
||||
return $handled;
|
||||
|
||||
try {
|
||||
return match ($msg->message_type) {
|
||||
'message' => $this->handleCommandMessage($msg),
|
||||
'reply_message' => $this->handleReplyMessage($msg),
|
||||
default => false
|
||||
};
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Telegram 命令处理意外错误', [
|
||||
'command' => $msg->command ?? 'unknown',
|
||||
'chat_id' => $msg->chat_id ?? 'unknown',
|
||||
'error' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine()
|
||||
]);
|
||||
|
||||
if (isset($msg->chat_id)) {
|
||||
$this->telegramService->sendMessage($msg->chat_id, '系统繁忙,请稍后重试');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
protected function handleCommandMessage(object $msg): bool
|
||||
{
|
||||
if (!isset($this->commands['commands'][$msg->command])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
call_user_func($this->commands['commands'][$msg->command], $msg);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function handleReplyMessage(object $msg): bool
|
||||
{
|
||||
if (!isset($this->commands['replies'])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($this->commands['replies'] as $regex => $handler) {
|
||||
if (preg_match($regex, $msg->reply_text, $matches)) {
|
||||
call_user_func($handler, $msg, $matches);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function handleUnknownCommand(array $data): void
|
||||
{
|
||||
list($msg) = $data;
|
||||
if (!$msg->is_private || $msg->message_type !== 'message')
|
||||
return;
|
||||
|
||||
$helpText = $this->getConfig('help_text', '未知命令,请查看帮助');
|
||||
$this->telegramService->sendMessage($msg->chat_id, $helpText);
|
||||
}
|
||||
|
||||
public function handleError(array $data): void
|
||||
{
|
||||
list($msg, $e) = $data;
|
||||
Log::error('Telegram 消息处理错误', [
|
||||
'chat_id' => $msg->chat_id ?? 'unknown',
|
||||
'command' => $msg->command ?? 'unknown',
|
||||
'message_type' => $msg->message_type ?? 'unknown',
|
||||
'error' => $e->getMessage(),
|
||||
'file' => $e->getFile(),
|
||||
'line' => $e->getLine()
|
||||
]);
|
||||
}
|
||||
|
||||
public function handleBindCommand(object $msg): void
|
||||
{
|
||||
if (!$this->checkPrivateChat($msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subscribeUrl = $msg->args[0] ?? null;
|
||||
if (!$subscribeUrl) {
|
||||
$this->sendMessage($msg, '参数有误,请携带订阅地址发送');
|
||||
return;
|
||||
}
|
||||
|
||||
$token = $this->extractTokenFromUrl($subscribeUrl);
|
||||
if (!$token) {
|
||||
$this->sendMessage($msg, '订阅地址无效');
|
||||
return;
|
||||
}
|
||||
|
||||
$user = User::where('token', $token)->first();
|
||||
if (!$user) {
|
||||
$this->sendMessage($msg, '用户不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
if ($user->telegram_id) {
|
||||
$this->sendMessage($msg, '该账号已经绑定了Telegram账号');
|
||||
return;
|
||||
}
|
||||
|
||||
$user->telegram_id = $msg->chat_id;
|
||||
if (!$user->save()) {
|
||||
$this->sendMessage($msg, '设置失败');
|
||||
return;
|
||||
}
|
||||
|
||||
HookManager::call('user.telegram.bind.after', [$user]);
|
||||
$this->sendMessage($msg, '绑定成功');
|
||||
}
|
||||
|
||||
protected function extractTokenFromUrl(string $url): ?string
|
||||
{
|
||||
$parsedUrl = parse_url($url);
|
||||
|
||||
if (isset($parsedUrl['query'])) {
|
||||
parse_str($parsedUrl['query'], $query);
|
||||
if (isset($query['token'])) {
|
||||
return $query['token'];
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($parsedUrl['path'])) {
|
||||
$pathParts = explode('/', trim($parsedUrl['path'], '/'));
|
||||
$lastPart = end($pathParts);
|
||||
return $lastPart ?: null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function handleTrafficCommand(object $msg): void
|
||||
{
|
||||
if (!$this->checkPrivateChat($msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $this->getBoundUser($msg);
|
||||
if (!$user) {
|
||||
return;
|
||||
}
|
||||
|
||||
$transferUsed = $user->u + $user->d;
|
||||
$transferTotal = $user->transfer_enable;
|
||||
$transferRemaining = $transferTotal - $transferUsed;
|
||||
$usagePercentage = $transferTotal > 0 ? ($transferUsed / $transferTotal) * 100 : 0;
|
||||
|
||||
$text = sprintf(
|
||||
"📊 流量使用情况\n\n已用流量:%s\n总流量:%s\n剩余流量:%s\n使用率:%.2f%%",
|
||||
Helper::transferToGB($transferUsed),
|
||||
Helper::transferToGB($transferTotal),
|
||||
Helper::transferToGB($transferRemaining),
|
||||
$usagePercentage
|
||||
);
|
||||
|
||||
$this->sendMessage($msg, $text);
|
||||
}
|
||||
|
||||
public function handleGetLatestUrlCommand(object $msg): void
|
||||
{
|
||||
if (!$this->checkPrivateChat($msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $this->getBoundUser($msg);
|
||||
if (!$user) {
|
||||
return;
|
||||
}
|
||||
|
||||
$subscribeUrl = Helper::getSubscribeUrl($user->token);
|
||||
$text = sprintf("🔗 您的订阅链接:\n\n%s", $subscribeUrl);
|
||||
|
||||
$this->sendMessage($msg, $text);
|
||||
}
|
||||
|
||||
public function handleUnbindCommand(object $msg): void
|
||||
{
|
||||
if (!$this->checkPrivateChat($msg)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user = $this->getBoundUser($msg);
|
||||
if (!$user) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user->telegram_id = null;
|
||||
if (!$user->save()) {
|
||||
$this->sendMessage($msg, '解绑失败');
|
||||
return;
|
||||
}
|
||||
|
||||
$this->sendMessage($msg, '解绑成功');
|
||||
}
|
||||
|
||||
public function handleTicketReply(object $msg, array $matches): void
|
||||
{
|
||||
$user = $this->getBoundUser($msg);
|
||||
if (!$user) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isset($matches[2]) || !is_numeric($matches[2])) {
|
||||
Log::warning('Telegram 工单回复正则未匹配到工单ID', ['matches' => $matches, 'msg' => $msg]);
|
||||
$this->sendMessage($msg, '未能识别工单ID,请直接回复工单提醒消息。');
|
||||
return;
|
||||
}
|
||||
|
||||
$ticketId = (int) $matches[2];
|
||||
$ticket = Ticket::where('id', $ticketId)->first();
|
||||
if (!$ticket) {
|
||||
$this->sendMessage($msg, '工单不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
$ticketService = new TicketService();
|
||||
$ticketService->replyByAdmin(
|
||||
$ticketId,
|
||||
$msg->text,
|
||||
$user->id
|
||||
);
|
||||
|
||||
$this->sendMessage($msg, "工单 #{$ticketId} 回复成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加 Bot 命令到命令列表
|
||||
*/
|
||||
public function addBotCommands(array $commands): array
|
||||
{
|
||||
foreach ($this->commandConfigs as $command => $config) {
|
||||
$commands[] = [
|
||||
'command' => $command,
|
||||
'description' => $config['description']
|
||||
];
|
||||
}
|
||||
|
||||
return $commands;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
# Telegram 插件
|
||||
|
||||
XBoard 的 Telegram Bot 插件,提供用户账号绑定、流量查询、订阅链接获取等功能。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 工单通知功能(可配置开关)
|
||||
- ✅ 支付通知功能(可配置开关)
|
||||
- ✅ 用户账号绑定/解绑
|
||||
- ✅ 流量使用情况查询
|
||||
- ✅ 订阅链接获取
|
||||
- ✅ 工单回复支持
|
||||
|
||||
## 可用命令
|
||||
|
||||
### `/start` - 开始使用
|
||||
|
||||
欢迎新用户并显示帮助信息,支持动态配置。
|
||||
|
||||
### `/bind` - 绑定账号
|
||||
|
||||
绑定用户的 XBoard 账号到 Telegram。
|
||||
|
||||
```
|
||||
/bind [订阅链接]
|
||||
```
|
||||
|
||||
### `/traffic` - 查看流量
|
||||
|
||||
查看当前绑定账号的流量使用情况。
|
||||
|
||||
### `/getlatesturl` - 获取订阅链接
|
||||
|
||||
获取最新的订阅链接。
|
||||
|
||||
### `/unbind` - 解绑账号
|
||||
|
||||
解绑当前 Telegram 账号与 XBoard 账号的关联。
|
||||
|
||||
## 配置选项
|
||||
|
||||
### 基础配置
|
||||
|
||||
| 配置项 | 类型 | 默认值 | 说明 |
|
||||
| ------------ | ------- | ------------------------------------------------------------------------------------------ | -------------------- |
|
||||
| `auto_reply` | boolean | true | 是否自动回复未知命令 |
|
||||
| `help_text` | text | '请使用以下命令:\\n/bind - 绑定账号\\n/traffic - 查看流量\\n/getlatesturl - 获取最新链接' | 未知命令的回复文本 |
|
||||
|
||||
### `/start` 命令动态配置
|
||||
|
||||
| 配置项 | 类型 | 说明 |
|
||||
| ----------------------- | ---- | ------------------------ |
|
||||
| `start_welcome_title` | text | 欢迎标题 |
|
||||
| `start_bot_description` | text | 机器人功能介绍 |
|
||||
| `start_bind_guide` | text | 未绑定用户的绑定指导 |
|
||||
| `start_unbind_guide` | text | 已绑定用户显示的命令列表 |
|
||||
| `start_bind_commands` | text | 未绑定用户显示的命令列表 |
|
||||
| `start_footer` | text | 底部提示信息 |
|
||||
|
||||
### 工单通知配置
|
||||
|
||||
| 配置项 | 类型 | 默认值 | 说明 |
|
||||
| ---------------------- | ------- | ------ | -------------------- |
|
||||
| `enable_ticket_notify` | boolean | true | 是否开启工单通知功能 |
|
||||
|
||||
### 支付通知配置
|
||||
|
||||
| 配置项 | 类型 | 默认值 | 说明 |
|
||||
| ----------------------- | ------- | ------ | -------------------- |
|
||||
| `enable_payment_notify` | boolean | true | 是否开启支付通知功能 |
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 新用户使用流程
|
||||
|
||||
1. 用户首次使用 Bot,发送 `/start`
|
||||
2. 根据提示绑定账号:`/bind [订阅链接]`
|
||||
3. 绑定成功后即可使用其他功能
|
||||
|
||||
### 日常使用流程
|
||||
|
||||
1. 查看流量:`/traffic`
|
||||
2. 获取订阅链接:`/getlatesturl`
|
||||
3. 管理绑定:`/unbind`
|
||||
@@ -0,0 +1,66 @@
|
||||
{
|
||||
"name": "Telegram Bot 集成",
|
||||
"code": "telegram",
|
||||
"version": "1.0.0",
|
||||
"description": "Telegram Bot 消息处理和命令系统",
|
||||
"author": "XBoard Team",
|
||||
"require": {
|
||||
"xboard": ">=1.0.0"
|
||||
},
|
||||
"config": {
|
||||
"enable_ticket_notify": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"label": "开启工单通知",
|
||||
"description": "是否开启工单创建和回复的 Telegram 通知功能"
|
||||
},
|
||||
"enable_payment_notify": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"label": "开启支付通知",
|
||||
"description": "是否开启支付成功的 Telegram 通知功能"
|
||||
},
|
||||
"start_welcome_title": {
|
||||
"type": "string",
|
||||
"default": "🎉 欢迎使用 XBoard Telegram Bot!",
|
||||
"label": "欢迎标题",
|
||||
"description": "/start 命令显示的欢迎标题"
|
||||
},
|
||||
"start_bot_description": {
|
||||
"type": "text",
|
||||
"default": "🤖 我是您的专属助手,可以帮助您:\\n• 绑定您的 XBoard 账号\\n• 查看流量使用情况\\n• 获取最新订阅链接\\n• 管理账号绑定状态",
|
||||
"label": "机器人描述",
|
||||
"description": "/start 命令显示的机器人功能介绍"
|
||||
},
|
||||
"start_bind_guide": {
|
||||
"type": "text",
|
||||
"default": "🔗 请先绑定您的 XBoard 账号:\\n1. 登录您的 XBoard 账户\\n2. 复制您的订阅链接\\n3. 发送 /bind + 订阅链接",
|
||||
"label": "绑定指导",
|
||||
"description": "未绑定用户显示的绑定指导文本"
|
||||
},
|
||||
"start_unbind_guide": {
|
||||
"type": "text",
|
||||
"default": "📋 可用命令:\\n/traffic - 查看流量使用情况\\n/getlatesturl - 获取订阅链接\\n/unbind - 解绑账号",
|
||||
"label": "已绑定用户命令列表",
|
||||
"description": "已绑定用户显示的命令列表"
|
||||
},
|
||||
"start_bind_commands": {
|
||||
"type": "text",
|
||||
"default": "📋 可用命令:\\n/bind [订阅链接] - 绑定账号",
|
||||
"label": "未绑定用户命令列表",
|
||||
"description": "未绑定用户显示的命令列表"
|
||||
},
|
||||
"start_footer": {
|
||||
"type": "text",
|
||||
"default": "💡 提示:所有命令都需要在私聊中使用",
|
||||
"label": "底部提示",
|
||||
"description": "/start 命令底部的提示信息"
|
||||
},
|
||||
"help_text": {
|
||||
"type": "text",
|
||||
"default": "请使用以下命令:\\n/bind - 绑定账号\\n/traffic - 查看流量\\n/getlatesturl - 获取最新链接",
|
||||
"label": "帮助文本",
|
||||
"description": "未知命令时显示的帮助文本"
|
||||
}
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+16
-16
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1829
-317
File diff suppressed because one or more lines are too long
Vendored
+20
-8
@@ -85,6 +85,9 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
"cancel": "Cancel",
|
||||
"submit": "Submit"
|
||||
},
|
||||
"sections": {
|
||||
"payment_config": "Payment Configuration"
|
||||
},
|
||||
"messages": {
|
||||
"success": "Saved successfully"
|
||||
}
|
||||
@@ -172,10 +175,9 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
"search": {
|
||||
"placeholder": "Search plugin name or description..."
|
||||
},
|
||||
"category": {
|
||||
"placeholder": "Select Category",
|
||||
"all": "All",
|
||||
"other": "Other"
|
||||
"type": {
|
||||
"placeholder": "Select Plugin Type",
|
||||
"all": "All Types"
|
||||
},
|
||||
"tabs": {
|
||||
"all": "All Plugins",
|
||||
@@ -184,14 +186,21 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
},
|
||||
"status": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled"
|
||||
"disabled": "Disabled",
|
||||
"not_installed": "Not Installed",
|
||||
"protected": "Protected",
|
||||
"filter_placeholder": "Install Status",
|
||||
"all": "All Status",
|
||||
"installed": "Installed",
|
||||
"available": "Available"
|
||||
},
|
||||
"button": {
|
||||
"install": "Install",
|
||||
"config": "Configure",
|
||||
"enable": "Enable",
|
||||
"disable": "Disable",
|
||||
"uninstall": "Uninstall"
|
||||
"uninstall": "Uninstall",
|
||||
"readme": "View Documentation"
|
||||
},
|
||||
"upload": {
|
||||
"button": "Upload Plugin",
|
||||
@@ -221,6 +230,9 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
"save": "Save",
|
||||
"cancel": "Cancel"
|
||||
},
|
||||
"readme": {
|
||||
"title": "Plugin Documentation"
|
||||
},
|
||||
"author": "Author",
|
||||
"messages": {
|
||||
"installSuccess": "Plugin installed successfully",
|
||||
@@ -767,8 +779,8 @@ window.XBOARD_TRANSLATIONS['en-US'] = {
|
||||
"success": "Webhook set successfully"
|
||||
},
|
||||
"bot_enable": {
|
||||
"title": "Enable Bot Notifications",
|
||||
"description": "When enabled, the bot will send basic notifications to administrators and users who have bound their Telegram accounts."
|
||||
"title": "Enable Telegram Binding Guide",
|
||||
"description": "When enabled, a Telegram binding guide will be displayed on the user side to help users bind their Telegram accounts for notifications."
|
||||
},
|
||||
"discuss_link": {
|
||||
"title": "Group Link",
|
||||
|
||||
Vendored
+20
-8
@@ -85,6 +85,9 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
"cancel": "取消",
|
||||
"submit": "提交"
|
||||
},
|
||||
"sections": {
|
||||
"payment_config": "支付配置"
|
||||
},
|
||||
"messages": {
|
||||
"success": "保存成功"
|
||||
}
|
||||
@@ -172,10 +175,9 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
"search": {
|
||||
"placeholder": "搜索插件名称或描述..."
|
||||
},
|
||||
"category": {
|
||||
"placeholder": "选择分类",
|
||||
"all": "全部",
|
||||
"other": "其他"
|
||||
"type": {
|
||||
"placeholder": "选择插件类型",
|
||||
"all": "全部类型"
|
||||
},
|
||||
"tabs": {
|
||||
"all": "所有插件",
|
||||
@@ -184,14 +186,21 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
},
|
||||
"status": {
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用"
|
||||
"disabled": "已禁用",
|
||||
"not_installed": "未安装",
|
||||
"protected": "受保护",
|
||||
"filter_placeholder": "安装状态",
|
||||
"all": "全部状态",
|
||||
"installed": "已安装",
|
||||
"available": "可安装"
|
||||
},
|
||||
"button": {
|
||||
"install": "安装",
|
||||
"config": "配置",
|
||||
"enable": "启用",
|
||||
"disable": "禁用",
|
||||
"uninstall": "卸载"
|
||||
"uninstall": "卸载",
|
||||
"readme": "查看文档"
|
||||
},
|
||||
"upload": {
|
||||
"button": "上传插件",
|
||||
@@ -221,6 +230,9 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
"save": "保存",
|
||||
"cancel": "取消"
|
||||
},
|
||||
"readme": {
|
||||
"title": "插件文档"
|
||||
},
|
||||
"author": "作者",
|
||||
"messages": {
|
||||
"installSuccess": "插件安装成功",
|
||||
@@ -687,8 +699,8 @@ window.XBOARD_TRANSLATIONS['zh-CN'] = {
|
||||
"success": "Webhook 设置成功"
|
||||
},
|
||||
"bot_enable": {
|
||||
"title": "启用机器人通知",
|
||||
"description": "开启后机器人将向管理员和已绑定Telegram的用户发送基础通知。"
|
||||
"title": "启用Telegram绑定引导",
|
||||
"description": "开启后将在用户端显示Telegram绑定引导,帮助用户绑定Telegram账户以接收通知。"
|
||||
},
|
||||
"discuss_link": {
|
||||
"title": "群组链接",
|
||||
|
||||
Reference in New Issue
Block a user