feat: enhance plugin management

- Add command support for plugin management
- Optimize plugin management page layout
- Add email copy functionality for users
- Convert payment methods and Telegram Bot to plugin system
This commit is contained in:
xboard
2025-07-26 18:49:58 +08:00
parent 02d853d46a
commit 58868268dd
56 changed files with 3677 additions and 1329 deletions
@@ -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);
}
}
+38 -108
View File
@@ -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;
}
}