merge: sync upstream/master from cedar2025/Xboard
合并上游 cedar2025/Xboard 的 master,并按交互决策保留本地改动。
This commit is contained in:
@@ -3,7 +3,8 @@
|
||||
namespace App\Http\Controllers\V1\Server;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\UpdateAliveDataJob;
|
||||
use App\Jobs\UserAliveSyncJob;
|
||||
use App\Services\NodeSyncService;
|
||||
use App\Services\ServerService;
|
||||
use App\Services\UserService;
|
||||
use App\Utils\CacheKey;
|
||||
@@ -88,117 +89,13 @@ class UniProxyController extends Controller
|
||||
public function config(Request $request)
|
||||
{
|
||||
$node = $this->getNodeInfo($request);
|
||||
$nodeType = $node->type;
|
||||
$protocolSettings = $node->protocol_settings;
|
||||
|
||||
$serverPort = $node->server_port;
|
||||
$host = $node->host;
|
||||
|
||||
$baseConfig = [
|
||||
'protocol' => $nodeType,
|
||||
'listen_ip' => '0.0.0.0',
|
||||
'server_port' => (int) $serverPort,
|
||||
'network' => data_get($protocolSettings, 'network'),
|
||||
'networkSettings' => data_get($protocolSettings, 'network_settings') ?: null,
|
||||
];
|
||||
|
||||
$response = match ($nodeType) {
|
||||
'shadowsocks' => [
|
||||
...$baseConfig,
|
||||
'cipher' => $protocolSettings['cipher'],
|
||||
'plugin' => $protocolSettings['plugin'],
|
||||
'plugin_opts' => $protocolSettings['plugin_opts'],
|
||||
'server_key' => match ($protocolSettings['cipher']) {
|
||||
'2022-blake3-aes-128-gcm' => Helper::getServerKey($node->created_at, 16),
|
||||
'2022-blake3-aes-256-gcm' => Helper::getServerKey($node->created_at, 32),
|
||||
default => null
|
||||
}
|
||||
],
|
||||
'vmess' => [
|
||||
...$baseConfig,
|
||||
'tls' => (int) $protocolSettings['tls']
|
||||
],
|
||||
'trojan' => [
|
||||
...$baseConfig,
|
||||
'host' => $host,
|
||||
'server_name' => $protocolSettings['server_name'],
|
||||
],
|
||||
'vless' => [
|
||||
...$baseConfig,
|
||||
'tls' => (int) $protocolSettings['tls'],
|
||||
'flow' => $protocolSettings['flow'],
|
||||
'tls_settings' =>
|
||||
match ((int) $protocolSettings['tls']) {
|
||||
2 => $protocolSettings['reality_settings'],
|
||||
default => $protocolSettings['tls_settings']
|
||||
}
|
||||
],
|
||||
'hysteria' => [
|
||||
...$baseConfig,
|
||||
'server_port' => (int) $serverPort,
|
||||
'version' => (int) $protocolSettings['version'],
|
||||
'host' => $host,
|
||||
'server_name' => $protocolSettings['tls']['server_name'],
|
||||
'up_mbps' => (int) $protocolSettings['bandwidth']['up'],
|
||||
'down_mbps' => (int) $protocolSettings['bandwidth']['down'],
|
||||
...match ((int) $protocolSettings['version']) {
|
||||
1 => ['obfs' => $protocolSettings['obfs']['password'] ?? null],
|
||||
2 => [
|
||||
'obfs' => $protocolSettings['obfs']['open'] ? $protocolSettings['obfs']['type'] : null,
|
||||
'obfs-password' => $protocolSettings['obfs']['password'] ?? null
|
||||
],
|
||||
default => []
|
||||
}
|
||||
],
|
||||
'tuic' => [
|
||||
...$baseConfig,
|
||||
'version' => (int) $protocolSettings['version'],
|
||||
'server_port' => (int) $serverPort,
|
||||
'server_name' => $protocolSettings['tls']['server_name'],
|
||||
'congestion_control' => $protocolSettings['congestion_control'],
|
||||
'auth_timeout' => '3s',
|
||||
'zero_rtt_handshake' => false,
|
||||
'heartbeat' => "3s",
|
||||
],
|
||||
'anytls' => [
|
||||
...$baseConfig,
|
||||
'server_port' => (int) $serverPort,
|
||||
'server_name' => $protocolSettings['tls']['server_name'],
|
||||
'padding_scheme' => $protocolSettings['padding_scheme'],
|
||||
],
|
||||
'socks' => [
|
||||
...$baseConfig,
|
||||
'server_port' => (int) $serverPort,
|
||||
],
|
||||
'naive' => [
|
||||
...$baseConfig,
|
||||
'server_port' => (int) $serverPort,
|
||||
'tls' => (int) $protocolSettings['tls'],
|
||||
'tls_settings' => $protocolSettings['tls_settings']
|
||||
],
|
||||
'http' => [
|
||||
...$baseConfig,
|
||||
'server_port' => (int) $serverPort,
|
||||
'tls' => (int) $protocolSettings['tls'],
|
||||
'tls_settings' => $protocolSettings['tls_settings']
|
||||
],
|
||||
'mieru' => [
|
||||
...$baseConfig,
|
||||
'server_port' => (string) $serverPort,
|
||||
'protocol' => (int) $protocolSettings['protocol'],
|
||||
],
|
||||
default => []
|
||||
};
|
||||
$response = ServerService::buildNodeConfig($node);
|
||||
|
||||
$response['base_config'] = [
|
||||
'push_interval' => (int) admin_setting('server_push_interval', 60),
|
||||
'pull_interval' => (int) admin_setting('server_pull_interval', 60)
|
||||
];
|
||||
|
||||
if (!empty($node['route_ids'])) {
|
||||
$response['routes'] = ServerService::getRoutes($node['route_ids']);
|
||||
}
|
||||
|
||||
$eTag = sha1(json_encode($response));
|
||||
if (strpos($request->header('If-None-Match', ''), $eTag) !== false) {
|
||||
return response(null, 304);
|
||||
@@ -226,7 +123,7 @@ class UniProxyController extends Controller
|
||||
'error' => 'Invalid online data'
|
||||
], 400);
|
||||
}
|
||||
UpdateAliveDataJob::dispatch($data, $node->type, $node->id);
|
||||
UserAliveSyncJob::dispatch($data, $node->type, $node->id);
|
||||
return response()->json(['data' => true]);
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ use App\Services\PaymentService;
|
||||
use App\Services\PlanService;
|
||||
use App\Services\UserService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class OrderController extends Controller
|
||||
{
|
||||
|
||||
@@ -70,7 +70,7 @@ class TicketController extends Controller
|
||||
if ($ticket->status) {
|
||||
return $this->fail([400, __('The ticket is closed and cannot be replied')]);
|
||||
}
|
||||
if ($request->user()->id == $this->getLastMessage($ticket->id)->user_id) {
|
||||
if ((int) admin_setting('ticket_must_wait_reply', 0) && $request->user()->id == $this->getLastMessage($ticket->id)->user_id) {
|
||||
return $this->fail(codeResponse: [400, __('Please wait for the technical enginneer to reply')]);
|
||||
}
|
||||
$ticketService = new TicketService();
|
||||
|
||||
@@ -18,6 +18,7 @@ use App\Utils\CacheKey;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
@@ -31,20 +32,14 @@ class UserController extends Controller
|
||||
|
||||
public function getActiveSession(Request $request)
|
||||
{
|
||||
$user = User::find($request->user()->id);
|
||||
if (!$user) {
|
||||
return $this->fail([400, __('The user does not exist')]);
|
||||
}
|
||||
$user = $request->user();
|
||||
$authService = new AuthService($user);
|
||||
return $this->success($authService->getSessions());
|
||||
}
|
||||
|
||||
public function removeActiveSession(Request $request)
|
||||
{
|
||||
$user = User::find($request->user()->id);
|
||||
if (!$user) {
|
||||
return $this->fail([400, __('The user does not exist')]);
|
||||
}
|
||||
$user = $request->user();
|
||||
$authService = new AuthService($user);
|
||||
return $this->success($authService->removeSession($request->input('session_id')));
|
||||
}
|
||||
@@ -62,10 +57,7 @@ class UserController extends Controller
|
||||
|
||||
public function changePassword(UserChangePassword $request)
|
||||
{
|
||||
$user = User::find($request->user()->id);
|
||||
if (!$user) {
|
||||
return $this->fail([400, __('The user does not exist')]);
|
||||
}
|
||||
$user = $request->user();
|
||||
if (
|
||||
!Helper::multiPasswordVerify(
|
||||
$user->password_algo,
|
||||
@@ -163,10 +155,7 @@ class UserController extends Controller
|
||||
|
||||
public function resetSecurity(Request $request)
|
||||
{
|
||||
$user = User::find($request->user()->id);
|
||||
if (!$user) {
|
||||
return $this->fail([400, __('The user does not exist')]);
|
||||
}
|
||||
$user = $request->user();
|
||||
$user->uuid = Helper::guid(true);
|
||||
$user->token = Helper::guid();
|
||||
if (!$user->save()) {
|
||||
@@ -182,10 +171,7 @@ class UserController extends Controller
|
||||
'remind_traffic'
|
||||
]);
|
||||
|
||||
$user = User::find($request->user()->id);
|
||||
if (!$user) {
|
||||
return $this->fail([400, __('The user does not exist')]);
|
||||
}
|
||||
$user = $request->user();
|
||||
try {
|
||||
$user->update($updateData);
|
||||
} catch (\Exception $e) {
|
||||
@@ -197,27 +183,31 @@ class UserController extends Controller
|
||||
|
||||
public function transfer(UserTransfer $request)
|
||||
{
|
||||
$user = User::find($request->user()->id);
|
||||
if (!$user) {
|
||||
return $this->fail([400, __('The user does not exist')]);
|
||||
}
|
||||
if ($request->input('transfer_amount') > $user->commission_balance) {
|
||||
return $this->fail([400, __('Insufficient commission balance')]);
|
||||
}
|
||||
$user->commission_balance = $user->commission_balance - $request->input('transfer_amount');
|
||||
$user->balance = $user->balance + $request->input('transfer_amount');
|
||||
if (!$user->save()) {
|
||||
return $this->fail([400, __('Transfer failed')]);
|
||||
$amount = $request->input('transfer_amount');
|
||||
try {
|
||||
DB::transaction(function () use ($request, $amount) {
|
||||
$user = User::lockForUpdate()->find($request->user()->id);
|
||||
if (!$user) {
|
||||
throw new \Exception(__('The user does not exist'));
|
||||
}
|
||||
if ($amount > $user->commission_balance) {
|
||||
throw new \Exception(__('Insufficient commission balance'));
|
||||
}
|
||||
$user->commission_balance -= $amount;
|
||||
$user->balance += $amount;
|
||||
if (!$user->save()) {
|
||||
throw new \Exception(__('Transfer failed'));
|
||||
}
|
||||
});
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail([400, $e->getMessage()]);
|
||||
}
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
public function getQuickLoginUrl(Request $request)
|
||||
{
|
||||
$user = User::find($request->user()->id);
|
||||
if (!$user) {
|
||||
return $this->fail([400, __('The user does not exist')]);
|
||||
}
|
||||
$user = $request->user();
|
||||
|
||||
$url = $this->loginService->generateQuickLoginUrl($user, $request->input('redirect'));
|
||||
return $this->success($url);
|
||||
|
||||
@@ -4,20 +4,12 @@ namespace App\Http\Controllers\V2\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Http\Requests\Admin\ConfigSave;
|
||||
use App\Protocols\Clash;
|
||||
use App\Protocols\ClashMeta;
|
||||
use App\Protocols\SingBox;
|
||||
use App\Protocols\Stash;
|
||||
use App\Protocols\Surfboard;
|
||||
use App\Protocols\Surge;
|
||||
use App\Models\SubscribeTemplate;
|
||||
use App\Services\MailService;
|
||||
use App\Services\TelegramService;
|
||||
use App\Services\ThemeService;
|
||||
use App\Utils\Dict;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
||||
class ConfigController extends Controller
|
||||
{
|
||||
@@ -57,31 +49,24 @@ class ConfigController extends Controller
|
||||
'data' => $mailLog,
|
||||
]);
|
||||
}
|
||||
/**
|
||||
* 获取规则模板内容
|
||||
*
|
||||
* @param string $file 文件路径
|
||||
* @return string 文件内容
|
||||
*/
|
||||
private function getTemplateContent(string $file): string
|
||||
{
|
||||
$path = base_path($file);
|
||||
return File::exists($path) ? File::get($path) : '';
|
||||
}
|
||||
|
||||
public function setTelegramWebhook(Request $request)
|
||||
{
|
||||
$app_url = admin_setting('app_url');
|
||||
if (blank($app_url))
|
||||
return $this->fail([422, '请先设置站点网址']);
|
||||
$hookUrl = $app_url . '/api/v1/guest/telegram/webhook?' . http_build_query([
|
||||
$hookUrl = $this->resolveTelegramWebhookUrl();
|
||||
if (blank($hookUrl)) {
|
||||
return $this->fail([422, 'Telegram Webhook地址未配置']);
|
||||
}
|
||||
$hookUrl .= '?' . http_build_query([
|
||||
'access_token' => md5(admin_setting('telegram_bot_token', $request->input('telegram_bot_token')))
|
||||
]);
|
||||
$telegramService = new TelegramService($request->input('telegram_bot_token'));
|
||||
$telegramService->getMe();
|
||||
$telegramService->setWebhook($hookUrl);
|
||||
$telegramService->setWebhook(url: $hookUrl);
|
||||
$telegramService->registerBotCommands();
|
||||
return $this->success(true);
|
||||
return $this->success([
|
||||
'success' => true,
|
||||
'webhook_url' => $hookUrl,
|
||||
'webhook_base_url' => $this->getTelegramWebhookBaseUrl(),
|
||||
]);
|
||||
}
|
||||
|
||||
public function fetch(Request $request)
|
||||
@@ -131,6 +116,7 @@ class ConfigController extends Controller
|
||||
'tos_url' => admin_setting('tos_url'),
|
||||
'currency' => admin_setting('currency', 'CNY'),
|
||||
'currency_symbol' => admin_setting('currency_symbol', '¥'),
|
||||
'ticket_must_wait_reply' => (bool) admin_setting('ticket_must_wait_reply', 0),
|
||||
],
|
||||
'subscribe' => [
|
||||
'plan_change_enable' => (bool) admin_setting('plan_change_enable', 1),
|
||||
@@ -157,6 +143,8 @@ class ConfigController extends Controller
|
||||
'server_pull_interval' => admin_setting('server_pull_interval', 60),
|
||||
'server_push_interval' => admin_setting('server_push_interval', 60),
|
||||
'device_limit_mode' => (int) admin_setting('device_limit_mode', 0),
|
||||
'server_ws_enable' => (bool) admin_setting('server_ws_enable', 1),
|
||||
'server_ws_url' => admin_setting('server_ws_url', ''),
|
||||
],
|
||||
'email' => [
|
||||
'email_template' => admin_setting('email_template', 'default'),
|
||||
@@ -171,6 +159,7 @@ class ConfigController extends Controller
|
||||
'telegram' => [
|
||||
'telegram_bot_enable' => (bool) admin_setting('telegram_bot_enable', 0),
|
||||
'telegram_bot_token' => admin_setting('telegram_bot_token'),
|
||||
'telegram_webhook_url' => admin_setting('telegram_webhook_url'),
|
||||
'telegram_discuss_link' => admin_setting('telegram_discuss_link')
|
||||
],
|
||||
'app' => [
|
||||
@@ -208,14 +197,14 @@ class ConfigController extends Controller
|
||||
],
|
||||
'subscribe_template' => [
|
||||
'subscribe_template_singbox' => $this->formatTemplateContent(
|
||||
admin_setting('subscribe_template_singbox', $this->getDefaultTemplate('singbox')),
|
||||
subscribe_template('singbox') ?? '',
|
||||
'json'
|
||||
),
|
||||
'subscribe_template_clash' => admin_setting('subscribe_template_clash', $this->getDefaultTemplate('clash')),
|
||||
'subscribe_template_clashmeta' => admin_setting('subscribe_template_clashmeta', $this->getDefaultTemplate('clashmeta')),
|
||||
'subscribe_template_stash' => admin_setting('subscribe_template_stash', $this->getDefaultTemplate('stash')),
|
||||
'subscribe_template_surge' => admin_setting('subscribe_template_surge', $this->getDefaultTemplate('surge')),
|
||||
'subscribe_template_surfboard' => admin_setting('subscribe_template_surfboard', $this->getDefaultTemplate('surfboard'))
|
||||
'subscribe_template_clash' => subscribe_template('clash') ?? '',
|
||||
'subscribe_template_clashmeta' => subscribe_template('clashmeta') ?? '',
|
||||
'subscribe_template_stash' => subscribe_template('stash') ?? '',
|
||||
'subscribe_template_surge' => subscribe_template('surge') ?? '',
|
||||
'subscribe_template_surfboard' => subscribe_template('surfboard') ?? ''
|
||||
]
|
||||
];
|
||||
}
|
||||
@@ -224,7 +213,20 @@ class ConfigController extends Controller
|
||||
{
|
||||
$data = $request->validated();
|
||||
|
||||
$templateKeys = [
|
||||
'subscribe_template_singbox' => 'singbox',
|
||||
'subscribe_template_clash' => 'clash',
|
||||
'subscribe_template_clashmeta' => 'clashmeta',
|
||||
'subscribe_template_stash' => 'stash',
|
||||
'subscribe_template_surge' => 'surge',
|
||||
'subscribe_template_surfboard' => 'surfboard',
|
||||
];
|
||||
|
||||
foreach ($data as $k => $v) {
|
||||
if (isset($templateKeys[$k])) {
|
||||
SubscribeTemplate::setContent($templateKeys[$k], $v);
|
||||
continue;
|
||||
}
|
||||
if ($k == 'frontend_theme') {
|
||||
$themeService = app(ThemeService::class);
|
||||
$themeService->switch($v);
|
||||
@@ -267,50 +269,32 @@ class ConfigController extends Controller
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认模板内容
|
||||
*
|
||||
* @param string $type 模板类型
|
||||
* @return string 默认模板内容
|
||||
*/
|
||||
private function getDefaultTemplate(string $type): string
|
||||
private function getTelegramWebhookBaseUrl(): ?string
|
||||
{
|
||||
$fileMap = [
|
||||
'singbox' => [SingBox::CUSTOM_TEMPLATE_FILE, SingBox::DEFAULT_TEMPLATE_FILE],
|
||||
'clash' => [Clash::CUSTOM_TEMPLATE_FILE, Clash::DEFAULT_TEMPLATE_FILE],
|
||||
'clashmeta' => [
|
||||
ClashMeta::CUSTOM_TEMPLATE_FILE,
|
||||
ClashMeta::CUSTOM_CLASH_TEMPLATE_FILE,
|
||||
ClashMeta::DEFAULT_TEMPLATE_FILE
|
||||
],
|
||||
'stash' => [
|
||||
Stash::CUSTOM_TEMPLATE_FILE,
|
||||
Stash::CUSTOM_CLASH_TEMPLATE_FILE,
|
||||
Stash::DEFAULT_TEMPLATE_FILE
|
||||
],
|
||||
'surge' => [Surge::CUSTOM_TEMPLATE_FILE, Surge::DEFAULT_TEMPLATE_FILE],
|
||||
'surfboard' => [Surfboard::CUSTOM_TEMPLATE_FILE, Surfboard::DEFAULT_TEMPLATE_FILE],
|
||||
];
|
||||
|
||||
if (!isset($fileMap[$type])) {
|
||||
return '';
|
||||
$customUrl = trim((string) admin_setting('telegram_webhook_url', ''));
|
||||
if ($customUrl !== '') {
|
||||
return rtrim($customUrl, '/');
|
||||
}
|
||||
|
||||
// 按优先级查找可用的模板文件
|
||||
foreach ($fileMap[$type] as $file) {
|
||||
$content = $this->getTemplateContent($file);
|
||||
if (!empty($content)) {
|
||||
// 对于 SingBox,需要格式化 JSON
|
||||
if ($type === 'singbox') {
|
||||
$decoded = json_decode($content, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
return json_encode($decoded, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
}
|
||||
return $content;
|
||||
}
|
||||
$appUrl = trim((string) admin_setting('app_url', ''));
|
||||
if ($appUrl !== '') {
|
||||
return rtrim($appUrl, '/');
|
||||
}
|
||||
|
||||
return '';
|
||||
return null;
|
||||
}
|
||||
|
||||
private function resolveTelegramWebhookUrl(): ?string
|
||||
{
|
||||
$baseUrl = $this->getTelegramWebhookBaseUrl();
|
||||
if (!$baseUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (str_contains($baseUrl, '/api/v1/guest/telegram/webhook')) {
|
||||
return $baseUrl;
|
||||
}
|
||||
|
||||
return $baseUrl . '/api/v1/guest/telegram/webhook';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,12 @@ class ManageController extends Controller
|
||||
'show' => 'integer',
|
||||
]);
|
||||
|
||||
if (!Server::where('id', $request->id)->update(['show' => $request->show])) {
|
||||
$server = Server::find($request->id);
|
||||
if (!$server) {
|
||||
return $this->fail([400202, '服务器不存在']);
|
||||
}
|
||||
$server->show = (int) $request->show;
|
||||
if (!$server->save()) {
|
||||
return $this->fail([500, '保存失败']);
|
||||
}
|
||||
return $this->success(true);
|
||||
|
||||
@@ -23,7 +23,7 @@ class RouteController extends Controller
|
||||
$params = $request->validate([
|
||||
'remarks' => 'required',
|
||||
'match' => 'required|array',
|
||||
'action' => 'required|in:block,dns',
|
||||
'action' => 'required|in:block,direct,dns,proxy',
|
||||
'action_value' => 'nullable'
|
||||
], [
|
||||
'remarks.required' => '备注不能为空',
|
||||
|
||||
@@ -536,19 +536,20 @@ class StatController extends Controller
|
||||
}
|
||||
|
||||
$result = [];
|
||||
$ids = $currentData->pluck('id');
|
||||
$names = $type === 'node'
|
||||
? Server::whereIn('id', $ids)->pluck('name', 'id')
|
||||
: User::whereIn('id', $ids)->pluck('email', 'id');
|
||||
|
||||
foreach ($currentData as $data) {
|
||||
$previousValue = isset($previousData[$data->id]) ? $previousData[$data->id]->value : 0;
|
||||
$change = $previousValue > 0 ? round(($data->value - $previousValue) / $previousValue * 100, 1) : 0;
|
||||
|
||||
$name = $type === 'node'
|
||||
? optional(Server::find($data->id))->name ?? "Node {$data->id}"
|
||||
: optional(User::find($data->id))->email ?? "User {$data->id}";
|
||||
|
||||
$result[] = [
|
||||
'id' => (string) $data->id,
|
||||
'name' => $name,
|
||||
'value' => $data->value, // Convert to GB
|
||||
'previousValue' => $previousValue, // Convert to GB
|
||||
'name' => $names[$data->id] ?? ($type === 'node' ? "Node {$data->id}" : "User {$data->id}"),
|
||||
'value' => $data->value,
|
||||
'previousValue' => $previousValue,
|
||||
'change' => $change,
|
||||
'timestamp' => date('c', $endDate)
|
||||
];
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Http\Controllers\V2\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Log as LogModel;
|
||||
use App\Models\AdminAuditLog;
|
||||
use App\Utils\CacheKey;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
@@ -23,37 +23,10 @@ class SystemController extends Controller
|
||||
'schedule' => $this->getScheduleStatus(),
|
||||
'horizon' => $this->getHorizonStatus(),
|
||||
'schedule_last_runtime' => Cache::get(CacheKey::get('SCHEDULE_LAST_CHECK_AT', null)),
|
||||
'logs' => $this->getLogStatistics()
|
||||
];
|
||||
return $this->success($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志统计信息
|
||||
*
|
||||
* @return array 各级别日志的数量统计
|
||||
*/
|
||||
protected function getLogStatistics(): array
|
||||
{
|
||||
// 初始化日志统计数组
|
||||
$statistics = [
|
||||
'info' => 0,
|
||||
'warning' => 0,
|
||||
'error' => 0,
|
||||
'total' => 0
|
||||
];
|
||||
|
||||
if (class_exists(LogModel::class) && LogModel::count() > 0) {
|
||||
$statistics['info'] = LogModel::where('level', 'INFO')->count();
|
||||
$statistics['warning'] = LogModel::where('level', 'WARNING')->count();
|
||||
$statistics['error'] = LogModel::where('level', 'ERROR')->count();
|
||||
$statistics['total'] = LogModel::count();
|
||||
|
||||
return $statistics;
|
||||
}
|
||||
return $statistics;
|
||||
}
|
||||
|
||||
public function getQueueWorkload(WorkloadRepository $workload)
|
||||
{
|
||||
return $this->success(collect($workload->get())->sortBy('name')->values()->toArray());
|
||||
@@ -125,34 +98,26 @@ class SystemController extends Controller
|
||||
})->count();
|
||||
}
|
||||
|
||||
public function getSystemLog(Request $request)
|
||||
public function getAuditLog(Request $request)
|
||||
{
|
||||
$current = $request->input('current') ? $request->input('current') : 1;
|
||||
$pageSize = $request->input('page_size') >= 10 ? $request->input('page_size') : 10;
|
||||
$level = $request->input('level');
|
||||
$keyword = $request->input('keyword');
|
||||
$current = max(1, (int) $request->input('current', 1));
|
||||
$pageSize = max(10, (int) $request->input('page_size', 10));
|
||||
|
||||
$builder = LogModel::orderBy('created_at', 'DESC')
|
||||
->when($level, function ($query) use ($level) {
|
||||
return $query->where('level', strtoupper($level));
|
||||
})
|
||||
->when($keyword, function ($query) use ($keyword) {
|
||||
return $query->where(function ($q) use ($keyword) {
|
||||
$q->where('data', 'like', '%' . $keyword . '%')
|
||||
->orWhere('context', 'like', '%' . $keyword . '%')
|
||||
->orWhere('title', 'like', '%' . $keyword . '%')
|
||||
->orWhere('uri', 'like', '%' . $keyword . '%');
|
||||
$builder = AdminAuditLog::with('admin:id,email')
|
||||
->orderBy('id', 'DESC')
|
||||
->when($request->input('action'), fn($q, $v) => $q->where('action', $v))
|
||||
->when($request->input('admin_id'), fn($q, $v) => $q->where('admin_id', $v))
|
||||
->when($request->input('keyword'), function ($q, $keyword) {
|
||||
$q->where(function ($q) use ($keyword) {
|
||||
$q->where('uri', 'like', '%' . $keyword . '%')
|
||||
->orWhere('request_data', 'like', '%' . $keyword . '%');
|
||||
});
|
||||
});
|
||||
|
||||
$total = $builder->count();
|
||||
$res = $builder->forPage($current, $pageSize)
|
||||
->get();
|
||||
$res = $builder->forPage($current, $pageSize)->get();
|
||||
|
||||
return response([
|
||||
'data' => $res,
|
||||
'total' => $total
|
||||
]);
|
||||
return response(['data' => $res, 'total' => $total]);
|
||||
}
|
||||
|
||||
public function getHorizonFailedJobs(Request $request, JobRepository $jobRepository)
|
||||
@@ -176,125 +141,4 @@ class SystemController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除系统日志
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function clearSystemLog(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'days' => 'integer|min:0|max:365',
|
||||
'level' => 'string|in:info,warning,error,all',
|
||||
'limit' => 'integer|min:100|max:10000'
|
||||
], [
|
||||
'days.required' => '请指定要清除多少天前的日志',
|
||||
'days.integer' => '天数必须为整数',
|
||||
'days.min' => '天数不能少于1天',
|
||||
'days.max' => '天数不能超过365天',
|
||||
'level.in' => '日志级别只能是:info、warning、error、all',
|
||||
'limit.min' => '单次清除数量不能少于100条',
|
||||
'limit.max' => '单次清除数量不能超过10000条'
|
||||
]);
|
||||
|
||||
$days = $request->input('days', 30); // 默认清除30天前的日志
|
||||
$level = $request->input('level', 'all'); // 默认清除所有级别
|
||||
$limit = $request->input('limit', 1000); // 默认单次清除1000条
|
||||
|
||||
try {
|
||||
$cutoffDate = now()->subDays($days);
|
||||
|
||||
// 构建查询条件
|
||||
$query = LogModel::where('created_at', '<', $cutoffDate->timestamp);
|
||||
|
||||
if ($level !== 'all') {
|
||||
$query->where('level', strtoupper($level));
|
||||
}
|
||||
|
||||
// 获取要删除的记录数量
|
||||
$totalCount = $query->count();
|
||||
|
||||
if ($totalCount === 0) {
|
||||
return $this->success([
|
||||
'message' => '没有找到符合条件的日志记录',
|
||||
'deleted_count' => 0,
|
||||
'total_count' => $totalCount
|
||||
]);
|
||||
}
|
||||
|
||||
// 分批删除,避免单次删除过多数据
|
||||
$deletedCount = 0;
|
||||
$batchSize = min($limit, 1000); // 每批最多1000条
|
||||
|
||||
while ($deletedCount < $limit && $deletedCount < $totalCount) {
|
||||
$remainingLimit = min($batchSize, $limit - $deletedCount);
|
||||
|
||||
$batchQuery = LogModel::where('created_at', '<', $cutoffDate->timestamp);
|
||||
if ($level !== 'all') {
|
||||
$batchQuery->where('level', strtoupper($level));
|
||||
}
|
||||
|
||||
$idsToDelete = $batchQuery->limit($remainingLimit)->pluck('id');
|
||||
|
||||
if ($idsToDelete->isEmpty()) {
|
||||
break;
|
||||
}
|
||||
|
||||
$batchDeleted = LogModel::whereIn('id', $idsToDelete)->delete();
|
||||
$deletedCount += $batchDeleted;
|
||||
|
||||
// 避免长时间占用数据库连接
|
||||
if ($deletedCount < $limit && $deletedCount < $totalCount) {
|
||||
usleep(100000); // 暂停0.1秒
|
||||
}
|
||||
}
|
||||
|
||||
return $this->success([
|
||||
'message' => '日志清除完成',
|
||||
'deleted_count' => $deletedCount,
|
||||
'total_count' => $totalCount,
|
||||
'remaining_count' => max(0, $totalCount - $deletedCount)
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail(ResponseEnum::HTTP_ERROR, null, '清除日志失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志清除统计信息
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
public function getLogClearStats(Request $request)
|
||||
{
|
||||
$days = $request->input('days', 30);
|
||||
$level = $request->input('level', 'all');
|
||||
|
||||
try {
|
||||
$cutoffDate = now()->subDays($days);
|
||||
|
||||
$query = LogModel::where('created_at', '<', $cutoffDate->timestamp);
|
||||
if ($level !== 'all') {
|
||||
$query->where('level', strtoupper($level));
|
||||
}
|
||||
|
||||
$stats = [
|
||||
'days' => $days,
|
||||
'level' => $level,
|
||||
'cutoff_date' => $cutoffDate->format(format: 'Y-m-d H:i:s'),
|
||||
'total_logs' => LogModel::count(),
|
||||
'logs_to_clear' => $query->count(),
|
||||
'oldest_log' => LogModel::orderBy('created_at', 'asc')->first(),
|
||||
'newest_log' => LogModel::orderBy('created_at', 'desc')->first(),
|
||||
];
|
||||
|
||||
return $this->success($stats);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return $this->fail(ResponseEnum::HTTP_ERROR, null, '获取统计信息失败:' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,12 @@ use App\Jobs\SendEmailJob;
|
||||
use App\Models\Plan;
|
||||
use App\Models\User;
|
||||
use App\Services\AuthService;
|
||||
use App\Services\NodeSyncService;
|
||||
use App\Services\UserService;
|
||||
use App\Traits\QueryOperators;
|
||||
use App\Utils\Helper;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
@@ -34,27 +36,15 @@ class UserController extends Controller
|
||||
return $this->success($user->save());
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filters and sorts to the query builder
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Builder $builder
|
||||
* @return void
|
||||
*/
|
||||
private function applyFiltersAndSorts(Request $request, Builder $builder): void
|
||||
// Apply filters and sorts to the query builder.
|
||||
private function applyFiltersAndSorts(Request $request, Builder|QueryBuilder $builder): void
|
||||
{
|
||||
$this->applyFilters($request, $builder);
|
||||
$this->applySorting($request, $builder);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply filters to the query builder
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Builder $builder
|
||||
* @return void
|
||||
*/
|
||||
private function applyFilters(Request $request, Builder $builder): void
|
||||
// Apply filters to the query builder.
|
||||
private function applyFilters(Request $request, Builder|QueryBuilder $builder): void
|
||||
{
|
||||
if (!$request->has('filter')) {
|
||||
return;
|
||||
@@ -70,18 +60,14 @@ class UserController extends Controller
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the filter query based on field and value
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param string $field
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
private function buildFilterQuery(Builder $query, string $field, mixed $value): void
|
||||
// Build one filter query condition.
|
||||
private function buildFilterQuery(Builder|QueryBuilder $query, string $field, mixed $value): void
|
||||
{
|
||||
// 处理关联查询
|
||||
if (str_contains($field, '.')) {
|
||||
if (!method_exists($query, 'whereHas')) {
|
||||
return;
|
||||
}
|
||||
[$relation, $relationField] = explode('.', $field);
|
||||
$query->whereHas($relation, function ($q) use ($relationField, $value) {
|
||||
if (is_array($value)) {
|
||||
@@ -126,14 +112,8 @@ class UserController extends Controller
|
||||
$this->applyQueryCondition($query, $queryField, $operator, $filterValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply sorting to the query builder
|
||||
*
|
||||
* @param Request $request
|
||||
* @param Builder $builder
|
||||
* @return void
|
||||
*/
|
||||
private function applySorting(Request $request, Builder $builder): void
|
||||
// Apply sorting rules to the query builder.
|
||||
private function applySorting(Request $request, Builder|QueryBuilder $builder): void
|
||||
{
|
||||
if (!$request->has('sort')) {
|
||||
return;
|
||||
@@ -146,19 +126,50 @@ class UserController extends Controller
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch paginated user list with filters and sorting
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
// Resolve bulk operation scope and normalize user_ids.
|
||||
private function resolveScope(Request $request): array
|
||||
{
|
||||
$scope = $request->input('scope');
|
||||
$userIds = $request->input('user_ids');
|
||||
|
||||
$hasSelection = is_array($userIds) && count(array_filter($userIds, static fn($v) => is_numeric($v))) > 0;
|
||||
$hasFilter = $request->has('filter') && !empty($request->input('filter'));
|
||||
|
||||
if (!in_array($scope, ['selected', 'filtered', 'all'], true)) {
|
||||
if ($hasSelection) {
|
||||
$scope = 'selected';
|
||||
} elseif ($hasFilter) {
|
||||
$scope = 'filtered';
|
||||
} else {
|
||||
$scope = 'all';
|
||||
}
|
||||
}
|
||||
|
||||
$normalizedIds = [];
|
||||
if ($scope === 'selected') {
|
||||
$normalizedIds = is_array($userIds) ? $userIds : [];
|
||||
$normalizedIds = array_values(array_unique(array_map(static function ($v) {
|
||||
return is_numeric($v) ? (int) $v : null;
|
||||
}, $normalizedIds)));
|
||||
$normalizedIds = array_values(array_filter($normalizedIds, static fn($v) => is_int($v)));
|
||||
}
|
||||
|
||||
return [
|
||||
'scope' => $scope,
|
||||
'user_ids' => $normalizedIds,
|
||||
];
|
||||
}
|
||||
|
||||
// Fetch paginated user list (filters + sorting).
|
||||
public function fetch(Request $request)
|
||||
{
|
||||
$current = $request->input('current', 1);
|
||||
$pageSize = $request->input('pageSize', 10);
|
||||
|
||||
$userModel = User::with(['plan:id,name', 'invite_user:id,email', 'group:id,name'])
|
||||
->select(DB::raw('*, (u+d) as total_used'));
|
||||
$userModel = User::query()
|
||||
->with(['plan:id,name', 'invite_user:id,email', 'group:id,name'])
|
||||
->select((new User())->getTable() . '.*')
|
||||
->selectRaw('(u + d) as total_used');
|
||||
|
||||
$this->applyFiltersAndSorts($request, $userModel);
|
||||
|
||||
@@ -172,12 +183,7 @@ class UserController extends Controller
|
||||
return $this->paginate($users);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform user data for response
|
||||
*
|
||||
* @param User $user
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
// Transform user fields for API response.
|
||||
public static function transformUserData(User $user): array
|
||||
{
|
||||
$user = $user->toArray();
|
||||
@@ -253,19 +259,25 @@ class UserController extends Controller
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出用户数据为CSV格式
|
||||
*
|
||||
* @param Request $request
|
||||
* @return \Symfony\Component\HttpFoundation\StreamedResponse
|
||||
*/
|
||||
// Export users to CSV.
|
||||
public function dumpCSV(Request $request)
|
||||
{
|
||||
ini_set('memory_limit', '-1');
|
||||
gc_enable(); // 启用垃圾回收
|
||||
|
||||
$scopeInfo = $this->resolveScope($request);
|
||||
$scope = $scopeInfo['scope'];
|
||||
$userIds = $scopeInfo['user_ids'];
|
||||
|
||||
if ($scope === 'selected') {
|
||||
if (empty($userIds)) {
|
||||
return $this->fail([422, 'user_ids不能为空']);
|
||||
}
|
||||
}
|
||||
|
||||
// 优化查询:使用with预加载plan关系,避免N+1问题
|
||||
$query = User::with('plan:id,name')
|
||||
$query = User::query()
|
||||
->with('plan:id,name')
|
||||
->orderBy('id', 'asc')
|
||||
->select([
|
||||
'email',
|
||||
@@ -279,7 +291,11 @@ class UserController extends Controller
|
||||
'plan_id'
|
||||
]);
|
||||
|
||||
$this->applyFiltersAndSorts($request, $query);
|
||||
if ($scope === 'selected') {
|
||||
$query->whereIn('id', $userIds);
|
||||
} elseif ($scope === 'filtered') {
|
||||
$this->applyFiltersAndSorts($request, $query);
|
||||
} // all: ignore filter/sort
|
||||
|
||||
$filename = 'users_' . date('Y-m-d_His') . '.csv';
|
||||
|
||||
@@ -439,26 +455,65 @@ class UserController extends Controller
|
||||
public function sendMail(UserSendMail $request)
|
||||
{
|
||||
ini_set('memory_limit', '-1');
|
||||
$scopeInfo = $this->resolveScope($request);
|
||||
$scope = $scopeInfo['scope'];
|
||||
$userIds = $scopeInfo['user_ids'];
|
||||
|
||||
if ($scope === 'selected') {
|
||||
if (empty($userIds)) {
|
||||
return $this->fail([422, 'user_ids不能为空']);
|
||||
}
|
||||
}
|
||||
|
||||
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
|
||||
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
|
||||
$hourlyLimit = (int) env('MASS_EMAIL_HOURLY_LIMIT', 500);
|
||||
$hourlyLimit = $hourlyLimit > 0 ? $hourlyLimit : 500;
|
||||
$builder = User::orderBy($sort, $sortType);
|
||||
$this->applyFiltersAndSorts($request, $builder);
|
||||
|
||||
$builder = User::query()
|
||||
->with('plan:id,name')
|
||||
->orderBy('id', 'desc');
|
||||
|
||||
if ($scope === 'filtered') {
|
||||
// filtered: apply filters/sort
|
||||
$builder->orderBy($sort, $sortType);
|
||||
$this->applyFiltersAndSorts($request, $builder);
|
||||
} elseif ($scope === 'selected') {
|
||||
$builder->whereIn('id', $userIds);
|
||||
} // all: ignore filter/sort
|
||||
|
||||
$subject = $request->input('subject');
|
||||
$content = $request->input('content');
|
||||
$templateValue = [
|
||||
'name' => admin_setting('app_name', 'Notification Service'),
|
||||
'url' => admin_setting('app_url'),
|
||||
'content' => $content
|
||||
];
|
||||
$appName = admin_setting('app_name', 'Notification Service');
|
||||
$appUrl = admin_setting('app_url');
|
||||
|
||||
$chunkSize = 1000;
|
||||
$processed = 0;
|
||||
|
||||
$builder->chunk($chunkSize, function ($users) use ($subject, $templateValue, $hourlyLimit, &$processed) {
|
||||
$builder->chunk($chunkSize, function ($users) use ($subject, $content, $appName, $appUrl, $hourlyLimit, &$processed) {
|
||||
foreach ($users as $user) {
|
||||
$vars = [
|
||||
'app.name' => $appName,
|
||||
'app.url' => $appUrl,
|
||||
'now' => now()->format('Y-m-d H:i:s'),
|
||||
'user.id' => $user->id,
|
||||
'user.email' => $user->email,
|
||||
'user.uuid' => $user->uuid,
|
||||
'user.plan_name' => $user->plan?->name ?? '',
|
||||
'user.expired_at' => $user->expired_at ? date('Y-m-d H:i:s', $user->expired_at) : '',
|
||||
'user.transfer_enable' => (int) ($user->transfer_enable ?? 0),
|
||||
'user.transfer_used' => (int) (($user->u ?? 0) + ($user->d ?? 0)),
|
||||
'user.transfer_left' => (int) (($user->transfer_enable ?? 0) - (($user->u ?? 0) + ($user->d ?? 0))),
|
||||
];
|
||||
|
||||
$templateValue = [
|
||||
'name' => $appName,
|
||||
'url' => $appUrl,
|
||||
'content' => $content,
|
||||
'vars' => $vars,
|
||||
'content_mode' => 'text',
|
||||
];
|
||||
|
||||
$delaySeconds = intdiv($processed, $hourlyLimit) * 3600;
|
||||
dispatch(new SendEmailJob([
|
||||
'email' => $user->email,
|
||||
@@ -475,10 +530,29 @@ class UserController extends Controller
|
||||
|
||||
public function ban(Request $request)
|
||||
{
|
||||
$scopeInfo = $this->resolveScope($request);
|
||||
$scope = $scopeInfo['scope'];
|
||||
$userIds = $scopeInfo['user_ids'];
|
||||
|
||||
if ($scope === 'selected') {
|
||||
if (empty($userIds)) {
|
||||
return $this->fail([422, 'user_ids不能为空']);
|
||||
}
|
||||
}
|
||||
|
||||
$sortType = in_array($request->input('sort_type'), ['ASC', 'DESC']) ? $request->input('sort_type') : 'DESC';
|
||||
$sort = $request->input('sort') ? $request->input('sort') : 'created_at';
|
||||
$builder = User::orderBy($sort, $sortType);
|
||||
$this->applyFilters($request, $builder);
|
||||
|
||||
$builder = User::query()->orderBy('id', 'desc');
|
||||
|
||||
if ($scope === 'filtered') {
|
||||
// filtered: keep current semantics
|
||||
$builder->orderBy($sort, $sortType);
|
||||
$this->applyFiltersAndSorts($request, $builder);
|
||||
} elseif ($scope === 'selected') {
|
||||
$builder->whereIn('id', $userIds);
|
||||
} // all: ignore filter/sort
|
||||
|
||||
try {
|
||||
$builder->update([
|
||||
'banned' => 1
|
||||
@@ -487,16 +561,11 @@ class UserController extends Controller
|
||||
Log::error($e);
|
||||
return $this->fail([500, '处理失败']);
|
||||
}
|
||||
|
||||
// Full refresh not implemented.
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户及其关联数据
|
||||
*
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
// Delete user and related data.
|
||||
public function destroy(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\V2\Server;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Jobs\UserAliveSyncJob;
|
||||
use App\Services\ServerService;
|
||||
use App\Services\UserService;
|
||||
use App\Utils\CacheKey;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Log;
|
||||
|
||||
class ServerController extends Controller
|
||||
{
|
||||
/**
|
||||
* server handshake api
|
||||
*/
|
||||
public function handshake(Request $request): JsonResponse
|
||||
{
|
||||
$websocket = ['enabled' => false];
|
||||
|
||||
if ((bool) admin_setting('server_ws_enable', 1)) {
|
||||
$customUrl = trim((string) admin_setting('server_ws_url', ''));
|
||||
|
||||
if ($customUrl !== '') {
|
||||
$wsUrl = rtrim($customUrl, '/');
|
||||
} else {
|
||||
$wsScheme = $request->isSecure() ? 'wss' : 'ws';
|
||||
$wsUrl = "{$wsScheme}://{$request->getHost()}:8076";
|
||||
}
|
||||
|
||||
$websocket = [
|
||||
'enabled' => true,
|
||||
'ws_url' => $wsUrl,
|
||||
];
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'websocket' => $websocket
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* node report api - merge traffic + alive + status
|
||||
* POST /api/v2/server/node/report
|
||||
*/
|
||||
public function report(Request $request): JsonResponse
|
||||
{
|
||||
$node = $request->attributes->get('node_info');
|
||||
$nodeType = $node->type;
|
||||
$nodeId = $node->id;
|
||||
|
||||
Cache::put(CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_CHECK_AT', $nodeId), time(), 3600);
|
||||
|
||||
// hanle traffic data
|
||||
$traffic = $request->input('traffic');
|
||||
if (is_array($traffic) && !empty($traffic)) {
|
||||
$data = array_filter($traffic, function ($item) {
|
||||
return is_array($item)
|
||||
&& count($item) === 2
|
||||
&& is_numeric($item[0])
|
||||
&& is_numeric($item[1]);
|
||||
});
|
||||
|
||||
if (!empty($data)) {
|
||||
Cache::put(
|
||||
CacheKey::get('SERVER_' . strtoupper($nodeType) . '_ONLINE_USER', $nodeId),
|
||||
count($data),
|
||||
3600
|
||||
);
|
||||
Cache::put(
|
||||
CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_PUSH_AT', $nodeId),
|
||||
time(),
|
||||
3600
|
||||
);
|
||||
$userService = new UserService();
|
||||
$userService->trafficFetch($node, $nodeType, $data);
|
||||
}
|
||||
}
|
||||
|
||||
// handle alive data
|
||||
$alive = $request->input('alive');
|
||||
if (is_array($alive) && !empty($alive)) {
|
||||
UserAliveSyncJob::dispatch($alive, $nodeType, $nodeId);
|
||||
}
|
||||
|
||||
// handle active connections
|
||||
$online = $request->input('online');
|
||||
if (is_array($online) && !empty($online)) {
|
||||
$cacheTime = max(300, (int) admin_setting('server_push_interval', 60) * 3);
|
||||
foreach ($online as $uid => $conn) {
|
||||
$cacheKey = CacheKey::get("USER_ONLINE_CONN_{$nodeType}_{$nodeId}", $uid);
|
||||
Cache::put($cacheKey, (int) $conn, $cacheTime);
|
||||
}
|
||||
}
|
||||
|
||||
// handle node status
|
||||
$status = $request->input('status');
|
||||
if (is_array($status) && !empty($status)) {
|
||||
$statusData = [
|
||||
'cpu' => (float) ($status['cpu'] ?? 0),
|
||||
'mem' => [
|
||||
'total' => (int) ($status['mem']['total'] ?? 0),
|
||||
'used' => (int) ($status['mem']['used'] ?? 0),
|
||||
],
|
||||
'swap' => [
|
||||
'total' => (int) ($status['swap']['total'] ?? 0),
|
||||
'used' => (int) ($status['swap']['used'] ?? 0),
|
||||
],
|
||||
'disk' => [
|
||||
'total' => (int) ($status['disk']['total'] ?? 0),
|
||||
'used' => (int) ($status['disk']['used'] ?? 0),
|
||||
],
|
||||
'updated_at' => now()->timestamp,
|
||||
'kernel_status' => $status['kernel_status'] ?? null,
|
||||
];
|
||||
|
||||
$cacheTime = max(300, (int) admin_setting('server_push_interval', 60) * 3);
|
||||
cache([
|
||||
CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LOAD_STATUS', $nodeId) => $statusData,
|
||||
CacheKey::get('SERVER_' . strtoupper($nodeType) . '_LAST_LOAD_AT', $nodeId) => now()->timestamp,
|
||||
], $cacheTime);
|
||||
}
|
||||
|
||||
// handle node metrics (Metrics)
|
||||
$metrics = $request->input('metrics');
|
||||
if (is_array($metrics) && !empty($metrics)) {
|
||||
ServerService::updateMetrics($node, $metrics);
|
||||
}
|
||||
|
||||
return response()->json(['data' => true]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user