mirror of
https://github.com/lkddi/Xboard.git
synced 2026-04-03 10:30:51 +08:00
refactor: restructure device limit system
This commit is contained in:
@@ -1,52 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Console\Commands;
|
|
||||||
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class CleanupExpiredOnlineStatus extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'cleanup:expired-online-status';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Reset online_count to 0 for users stale for 5+ minutes';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the console command.
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$affected = 0;
|
|
||||||
User::query()
|
|
||||||
->where('online_count', '>', 0)
|
|
||||||
->where('last_online_at', '<', now()->subMinutes(5))
|
|
||||||
->chunkById(1000, function ($users) use (&$affected) {
|
|
||||||
if ($users->isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
$count = User::whereIn('id', $users->pluck('id'))
|
|
||||||
->update(['online_count' => 0]);
|
|
||||||
$affected += $count;
|
|
||||||
}, 'id');
|
|
||||||
|
|
||||||
$this->info("Expired online status cleaned. Affected: {$affected}");
|
|
||||||
return self::SUCCESS;
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
Log::error('CleanupExpiredOnlineStatus failed', ['error' => $e->getMessage()]);
|
|
||||||
$this->error('Cleanup failed: ' . $e->getMessage());
|
|
||||||
return self::FAILURE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Console\Commands;
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
use App\Models\Server;
|
use App\Models\Server;
|
||||||
|
use App\Services\DeviceStateService;
|
||||||
use App\Services\NodeSyncService;
|
use App\Services\NodeSyncService;
|
||||||
use App\Services\NodeRegistry;
|
use App\Services\NodeRegistry;
|
||||||
use App\Services\ServerService;
|
use App\Services\ServerService;
|
||||||
@@ -69,6 +70,23 @@ class NodeWebSocketServer extends Command
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 定时推送设备状态给节点(每10秒)
|
||||||
|
Timer::add(10, function () {
|
||||||
|
$pendingNodeIds = Redis::spop('device:push_pending_nodes', 100);
|
||||||
|
if (empty($pendingNodeIds)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$deviceStateService = app(DeviceStateService::class);
|
||||||
|
|
||||||
|
foreach ($pendingNodeIds as $nodeId) {
|
||||||
|
$nodeId = (int) $nodeId;
|
||||||
|
if (NodeRegistry::get($nodeId) !== null) {
|
||||||
|
$this->pushDeviceStateToNode($nodeId, $deviceStateService);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
$worker->onConnect = function (TcpConnection $conn) {
|
$worker->onConnect = function (TcpConnection $conn) {
|
||||||
@@ -126,6 +144,10 @@ class NodeWebSocketServer extends Command
|
|||||||
NodeRegistry::add($nodeId, $conn);
|
NodeRegistry::add($nodeId, $conn);
|
||||||
Cache::put("node_ws_alive:{$nodeId}", true, 86400);
|
Cache::put("node_ws_alive:{$nodeId}", true, 86400);
|
||||||
|
|
||||||
|
// 清理该节点的旧设备数据(节点重连后需重新上报全量)
|
||||||
|
$deviceStateService = app(DeviceStateService::class);
|
||||||
|
$deviceStateService->clearNodeDevices($nodeId);
|
||||||
|
|
||||||
Log::debug("[WS] Node#{$nodeId} connected", [
|
Log::debug("[WS] Node#{$nodeId} connected", [
|
||||||
'remote' => $conn->getRemoteIp(),
|
'remote' => $conn->getRemoteIp(),
|
||||||
'total' => NodeRegistry::count(),
|
'total' => NodeRegistry::count(),
|
||||||
@@ -162,8 +184,17 @@ class NodeWebSocketServer extends Command
|
|||||||
$this->handleNodeStatus($nodeId, $msg['data']);
|
$this->handleNodeStatus($nodeId, $msg['data']);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case 'report.devices':
|
||||||
|
if ($nodeId && isset($msg['data'])) {
|
||||||
|
$this->handleDeviceReport($nodeId, $msg['data']);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'request.devices':
|
||||||
|
if ($nodeId) {
|
||||||
|
$this->handleDeviceRequest($conn, $nodeId);
|
||||||
|
}
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// Future: handle other node-initiated messages if needed
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -173,6 +204,9 @@ class NodeWebSocketServer extends Command
|
|||||||
$nodeId = $conn->nodeId;
|
$nodeId = $conn->nodeId;
|
||||||
NodeRegistry::remove($nodeId);
|
NodeRegistry::remove($nodeId);
|
||||||
Cache::forget("node_ws_alive:{$nodeId}");
|
Cache::forget("node_ws_alive:{$nodeId}");
|
||||||
|
|
||||||
|
app(DeviceStateService::class)->clearNodeDevices($nodeId);
|
||||||
|
|
||||||
Log::debug("[WS] Node#{$nodeId} disconnected", [
|
Log::debug("[WS] Node#{$nodeId} disconnected", [
|
||||||
'total' => NodeRegistry::count(),
|
'total' => NodeRegistry::count(),
|
||||||
]);
|
]);
|
||||||
@@ -194,13 +228,85 @@ class NodeWebSocketServer extends Command
|
|||||||
|
|
||||||
// Update last check-in cache
|
// Update last check-in cache
|
||||||
Cache::put(\App\Utils\CacheKey::get('SERVER_' . $nodeType . '_LAST_CHECK_AT', $nodeId), time(), 3600);
|
Cache::put(\App\Utils\CacheKey::get('SERVER_' . $nodeType . '_LAST_CHECK_AT', $nodeId), time(), 3600);
|
||||||
|
|
||||||
// Update metrics cache via Service
|
// Update metrics cache via Service
|
||||||
ServerService::updateMetrics($node, $data);
|
ServerService::updateMetrics($node, $data);
|
||||||
|
|
||||||
Log::debug("[WS] Node#{$nodeId} status updated via WebSocket");
|
Log::debug("[WS] Node#{$nodeId} status updated via WebSocket");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle device report from node via WebSocket
|
||||||
|
*
|
||||||
|
* 节点发送全量设备列表,面板负责差异计算
|
||||||
|
* 数据格式: {"event": "report.devices", "data": {userId: [ip1, ip2, ...], ...}}
|
||||||
|
*
|
||||||
|
* 示例: {"event": "report.devices", "data": {"123": ["1.1.1.1", "2.2.2.2"], "456": ["3.3.3.3"]}}
|
||||||
|
*/
|
||||||
|
private function handleDeviceReport(int $nodeId, array $data): void
|
||||||
|
{
|
||||||
|
$deviceStateService = app(DeviceStateService::class);
|
||||||
|
|
||||||
|
// 清理该节点的旧数据
|
||||||
|
$deviceStateService->clearNodeDevices($nodeId);
|
||||||
|
|
||||||
|
// 全量写入新数据
|
||||||
|
foreach ($data as $userId => $ips) {
|
||||||
|
if (is_numeric($userId) && is_array($ips)) {
|
||||||
|
$deviceStateService->setDevices((int) $userId, $nodeId, $ips);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 标记该节点待推送(由定时器批量处理)
|
||||||
|
Redis::sadd('device:push_pending_nodes', $nodeId);
|
||||||
|
|
||||||
|
Log::debug("[WS] Node#{$nodeId} synced " . count($data) . " users");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 推送全量设备状态给指定节点
|
||||||
|
*/
|
||||||
|
private function pushDeviceStateToNode(int $nodeId, DeviceStateService $service): void
|
||||||
|
{
|
||||||
|
$node = Server::find($nodeId);
|
||||||
|
if (!$node) return;
|
||||||
|
|
||||||
|
// 获取该节点关联的所有用户
|
||||||
|
$users = ServerService::getAvailableUsers($node);
|
||||||
|
$userIds = $users->pluck('id')->toArray();
|
||||||
|
|
||||||
|
// 获取这些用户的设备列表
|
||||||
|
$devices = $service->getUsersDevices($userIds);
|
||||||
|
|
||||||
|
NodeRegistry::send($nodeId, 'sync.devices', [
|
||||||
|
'users' => $devices
|
||||||
|
]);
|
||||||
|
|
||||||
|
Log::debug("[WS] Pushed device state to node#{$nodeId}: " . count($devices) . " users");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle device state request from node via WebSocket
|
||||||
|
*/
|
||||||
|
private function handleDeviceRequest(TcpConnection $conn, int $nodeId): void
|
||||||
|
{
|
||||||
|
$node = Server::find($nodeId);
|
||||||
|
if (!$node) return;
|
||||||
|
|
||||||
|
$users = ServerService::getAvailableUsers($node);
|
||||||
|
$userIds = $users->pluck('id')->toArray();
|
||||||
|
|
||||||
|
$deviceStateService = app(DeviceStateService::class);
|
||||||
|
$devices = $deviceStateService->getUsersDevices($userIds);
|
||||||
|
|
||||||
|
$conn->send(json_encode([
|
||||||
|
'event' => 'sync.devices',
|
||||||
|
'data' => ['users' => $devices],
|
||||||
|
]));
|
||||||
|
|
||||||
|
Log::debug("[WS] Node#{$nodeId} requested devices, sent " . count($devices) . " users");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe to Redis pub/sub channel for receiving push commands from Laravel.
|
* Subscribe to Redis pub/sub channel for receiving push commands from Laravel.
|
||||||
* Laravel app publishes to "node:push" channel, Workerman picks it up and forwards to the right node.
|
* Laravel app publishes to "node:push" channel, Workerman picks it up and forwards to the right node.
|
||||||
@@ -244,7 +350,7 @@ class NodeWebSocketServer extends Command
|
|||||||
|
|
||||||
$sent = NodeRegistry::send((int) $nodeId, $event, $data);
|
$sent = NodeRegistry::send((int) $nodeId, $event, $data);
|
||||||
if ($sent) {
|
if ($sent) {
|
||||||
Log::debug("[WS] Pushed {$event} to node#{$nodeId}");
|
Log::debug("[WS] Pushed {$event} to node#{$nodeId}, data: " . json_encode($data));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ use App\Utils\CacheKey;
|
|||||||
use Illuminate\Console\Scheduling\Schedule;
|
use Illuminate\Console\Scheduling\Schedule;
|
||||||
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use App\Services\UserOnlineService;
|
|
||||||
|
|
||||||
class Kernel extends ConsoleKernel
|
class Kernel extends ConsoleKernel
|
||||||
{
|
{
|
||||||
@@ -35,7 +34,7 @@ class Kernel extends ConsoleKernel
|
|||||||
$schedule->command('check:order')->everyMinute()->onOneServer()->withoutOverlapping(5);
|
$schedule->command('check:order')->everyMinute()->onOneServer()->withoutOverlapping(5);
|
||||||
$schedule->command('check:commission')->everyMinute()->onOneServer()->withoutOverlapping(5);
|
$schedule->command('check:commission')->everyMinute()->onOneServer()->withoutOverlapping(5);
|
||||||
$schedule->command('check:ticket')->everyMinute()->onOneServer()->withoutOverlapping(5);
|
$schedule->command('check:ticket')->everyMinute()->onOneServer()->withoutOverlapping(5);
|
||||||
$schedule->command('check:traffic-exceeded')->everyMinute()->onOneServer()->withoutOverlapping(10);
|
$schedule->command('check:traffic-exceeded')->everyMinute()->onOneServer()->withoutOverlapping(10)->runInBackground();
|
||||||
// reset
|
// reset
|
||||||
$schedule->command('reset:traffic')->everyMinute()->onOneServer()->withoutOverlapping(10);
|
$schedule->command('reset:traffic')->everyMinute()->onOneServer()->withoutOverlapping(10);
|
||||||
$schedule->command('reset:log')->daily()->onOneServer();
|
$schedule->command('reset:log')->daily()->onOneServer();
|
||||||
@@ -47,8 +46,6 @@ class Kernel extends ConsoleKernel
|
|||||||
// if (env('ENABLE_AUTO_BACKUP_AND_UPDATE', false)) {
|
// if (env('ENABLE_AUTO_BACKUP_AND_UPDATE', false)) {
|
||||||
// $schedule->command('backup:database', ['true'])->daily()->onOneServer();
|
// $schedule->command('backup:database', ['true'])->daily()->onOneServer();
|
||||||
// }
|
// }
|
||||||
$schedule->command('cleanup:expired-online-status')->everyMinute()->onOneServer()->withoutOverlapping(4);
|
|
||||||
|
|
||||||
app(PluginManager::class)->registerPluginSchedules($schedule);
|
app(PluginManager::class)->registerPluginSchedules($schedule);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
namespace App\Http\Controllers\V1\Server;
|
namespace App\Http\Controllers\V1\Server;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Jobs\UserAliveSyncJob;
|
use App\Services\DeviceStateService;
|
||||||
use App\Services\NodeSyncService;
|
use App\Services\NodeSyncService;
|
||||||
use App\Services\ServerService;
|
use App\Services\ServerService;
|
||||||
use App\Services\UserService;
|
use App\Services\UserService;
|
||||||
@@ -11,13 +11,12 @@ use App\Utils\CacheKey;
|
|||||||
use App\Utils\Helper;
|
use App\Utils\Helper;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use App\Services\UserOnlineService;
|
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
class UniProxyController extends Controller
|
class UniProxyController extends Controller
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly UserOnlineService $userOnlineService
|
private readonly DeviceStateService $deviceStateService
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,13 +102,15 @@ class UniProxyController extends Controller
|
|||||||
return response($response)->header('ETag', "\"{$eTag}\"");
|
return response($response)->header('ETag', "\"{$eTag}\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取在线用户数据(wyx2685
|
// 获取在线用户数据
|
||||||
public function alivelist(Request $request): JsonResponse
|
public function alivelist(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
$node = $this->getNodeInfo($request);
|
$node = $this->getNodeInfo($request);
|
||||||
$deviceLimitUsers = ServerService::getAvailableUsers($node)
|
$deviceLimitUsers = ServerService::getAvailableUsers($node)
|
||||||
->where('device_limit', '>', 0);
|
->where('device_limit', '>', 0);
|
||||||
$alive = $this->userOnlineService->getAliveList($deviceLimitUsers);
|
|
||||||
|
$alive = $this->deviceStateService->getAliveList(collect($deviceLimitUsers));
|
||||||
|
|
||||||
return response()->json(['alive' => (object) $alive]);
|
return response()->json(['alive' => (object) $alive]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +124,11 @@ class UniProxyController extends Controller
|
|||||||
'error' => 'Invalid online data'
|
'error' => 'Invalid online data'
|
||||||
], 400);
|
], 400);
|
||||||
}
|
}
|
||||||
UserAliveSyncJob::dispatch($data, $node->type, $node->id);
|
|
||||||
|
foreach ($data as $uid => $ips) {
|
||||||
|
$this->deviceStateService->setDevices((int) $uid, $node->id, $ips);
|
||||||
|
}
|
||||||
|
|
||||||
return response()->json(['data' => true]);
|
return response()->json(['data' => true]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
153
app/Http/Controllers/V2/Client/AppController.php
Normal file
153
app/Http/Controllers/V2/Client/AppController.php
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\V2\Client;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\ServerService;
|
||||||
|
use App\Services\UserService;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\File;
|
||||||
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
|
||||||
|
class AppController extends Controller
|
||||||
|
{
|
||||||
|
public function getConfig(Request $request)
|
||||||
|
{
|
||||||
|
$config = [
|
||||||
|
'app_info' => [
|
||||||
|
'app_name' => admin_setting('app_name', 'XB加速器'), // 应用名称
|
||||||
|
'app_description' => admin_setting('app_description', '专业的网络加速服务'), // 应用描述
|
||||||
|
'app_url' => admin_setting('app_url', 'https://app.example.com'), // 应用官网 URL
|
||||||
|
'logo' => admin_setting('logo', 'https://example.com/logo.png'), // 应用 Logo URL
|
||||||
|
'version' => admin_setting('app_version', '1.0.0'), // 应用版本号
|
||||||
|
],
|
||||||
|
'features' => [
|
||||||
|
'enable_register' => (bool) admin_setting('app_enable_register', true), // 是否开启注册功能
|
||||||
|
'enable_invite_system' => (bool) admin_setting('app_enable_invite_system', true), // 是否开启邀请系统
|
||||||
|
'enable_telegram_bot' => (bool) admin_setting('telegram_bot_enable', false), // 是否开启 Telegram 机器人
|
||||||
|
'enable_ticket_system' => (bool) admin_setting('app_enable_ticket_system', true), // 是否开启工单系统
|
||||||
|
'ticket_must_wait_reply' => (bool) admin_setting('ticket_must_wait_reply', 0), // 工单是否需要等待管理员回复后才可继续发消息
|
||||||
|
'enable_commission_system' => (bool) admin_setting('app_enable_commission_system', true), // 是否开启佣金系统
|
||||||
|
'enable_traffic_log' => (bool) admin_setting('app_enable_traffic_log', true), // 是否开启流量日志
|
||||||
|
'enable_knowledge_base' => (bool) admin_setting('app_enable_knowledge_base', true), // 是否开启知识库
|
||||||
|
'enable_announcements' => (bool) admin_setting('app_enable_announcements', true), // 是否开启公告系统
|
||||||
|
'enable_auto_renewal' => (bool) admin_setting('app_enable_auto_renewal', false), // 是否开启自动续费
|
||||||
|
'enable_coupon_system' => (bool) admin_setting('app_enable_coupon_system', true), // 是否开启优惠券系统
|
||||||
|
'enable_speed_test' => (bool) admin_setting('app_enable_speed_test', true), // 是否开启测速功能
|
||||||
|
'enable_server_ping' => (bool) admin_setting('app_enable_server_ping', true), // 是否开启服务器延迟检测
|
||||||
|
],
|
||||||
|
'ui_config' => [
|
||||||
|
'theme' => [
|
||||||
|
'primary_color' => admin_setting('app_primary_color', '#00C851'), // 主色调 (十六进制)
|
||||||
|
'secondary_color' => admin_setting('app_secondary_color', '#007E33'), // 辅助色 (十六进制)
|
||||||
|
'accent_color' => admin_setting('app_accent_color', '#FF6B35'), // 强调色 (十六进制)
|
||||||
|
'background_color' => admin_setting('app_background_color', '#F5F5F5'), // 背景色 (十六进制)
|
||||||
|
'text_color' => admin_setting('app_text_color', '#333333'), // 文字色 (十六进制)
|
||||||
|
],
|
||||||
|
'home_screen' => [
|
||||||
|
'show_speed_test' => (bool) admin_setting('app_show_speed_test', true), // 是否显示测速
|
||||||
|
'show_traffic_chart' => (bool) admin_setting('app_show_traffic_chart', true), // 是否显示流量图表
|
||||||
|
'show_server_ping' => (bool) admin_setting('app_show_server_ping', true), // 是否显示服务器延迟
|
||||||
|
'default_server_sort' => admin_setting('app_default_server_sort', 'ping'), // 默认服务器排序方式
|
||||||
|
'show_connection_status' => (bool) admin_setting('app_show_connection_status', true), // 是否显示连接状态
|
||||||
|
],
|
||||||
|
'server_list' => [
|
||||||
|
'show_country_flags' => (bool) admin_setting('app_show_country_flags', true), // 是否显示国家旗帜
|
||||||
|
'show_ping_values' => (bool) admin_setting('app_show_ping_values', true), // 是否显示延迟值
|
||||||
|
'show_traffic_usage' => (bool) admin_setting('app_show_traffic_usage', true), // 是否显示流量使用
|
||||||
|
'group_by_country' => (bool) admin_setting('app_group_by_country', false), // 是否按国家分组
|
||||||
|
'show_server_status' => (bool) admin_setting('app_show_server_status', true), // 是否显示服务器状态
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'business_rules' => [
|
||||||
|
'min_password_length' => (int) admin_setting('app_min_password_length', 8), // 最小密码长度
|
||||||
|
'max_login_attempts' => (int) admin_setting('app_max_login_attempts', 5), // 最大登录尝试次数
|
||||||
|
'session_timeout_minutes' => (int) admin_setting('app_session_timeout_minutes', 30), // 会话超时时间(分钟)
|
||||||
|
'auto_disconnect_after_minutes' => (int) admin_setting('app_auto_disconnect_after_minutes', 60), // 自动断开连接时间(分钟)
|
||||||
|
'max_concurrent_connections' => (int) admin_setting('app_max_concurrent_connections', 3), // 最大并发连接数
|
||||||
|
'traffic_warning_threshold' => (float) admin_setting('app_traffic_warning_threshold', 0.8), // 流量警告阈值(0-1)
|
||||||
|
'subscription_reminder_days' => admin_setting('app_subscription_reminder_days', [7, 3, 1]), // 订阅到期提醒天数
|
||||||
|
'connection_timeout_seconds' => (int) admin_setting('app_connection_timeout_seconds', 10), // 连接超时时间(秒)
|
||||||
|
'health_check_interval_seconds' => (int) admin_setting('app_health_check_interval_seconds', 30), // 健康检查间隔(秒)
|
||||||
|
],
|
||||||
|
'server_config' => [
|
||||||
|
'default_kernel' => admin_setting('app_default_kernel', 'clash'), // 默认内核 (clash/singbox)
|
||||||
|
'auto_select_fastest' => (bool) admin_setting('app_auto_select_fastest', true), // 是否自动选择最快服务器
|
||||||
|
'fallback_servers' => admin_setting('app_fallback_servers', ['server1', 'server2']), // 备用服务器列表
|
||||||
|
'enable_auto_switch' => (bool) admin_setting('app_enable_auto_switch', true), // 是否开启自动切换
|
||||||
|
'switch_threshold_ms' => (int) admin_setting('app_switch_threshold_ms', 1000), // 切换阈值(毫秒)
|
||||||
|
],
|
||||||
|
'security_config' => [
|
||||||
|
'tos_url' => admin_setting('tos_url', 'https://example.com/tos'), // 服务条款 URL
|
||||||
|
'privacy_policy_url' => admin_setting('app_privacy_policy_url', 'https://example.com/privacy'), // 隐私政策 URL
|
||||||
|
'is_email_verify' => (int) admin_setting('email_verify', 1), // 是否开启邮箱验证 (0/1)
|
||||||
|
'is_invite_force' => (int) admin_setting('invite_force', 0), // 是否强制邀请码 (0/1)
|
||||||
|
'email_whitelist_suffix' => (int) admin_setting('email_whitelist_suffix', 0), // 邮箱白名单后缀 (0/1)
|
||||||
|
'is_captcha' => (int) admin_setting('captcha_enable', 1), // 是否开启验证码 (0/1)
|
||||||
|
'captcha_type' => admin_setting('captcha_type', 'recaptcha'), // 验证码类型 (recaptcha/turnstile)
|
||||||
|
'recaptcha_site_key' => admin_setting('recaptcha_site_key', '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI'), // reCAPTCHA 站点密钥
|
||||||
|
'recaptcha_v3_site_key' => admin_setting('recaptcha_v3_site_key', '6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI'), // reCAPTCHA v3 站点密钥
|
||||||
|
'recaptcha_v3_score_threshold' => (float) admin_setting('recaptcha_v3_score_threshold', 0.5), // reCAPTCHA v3 分数阈值
|
||||||
|
'turnstile_site_key' => admin_setting('turnstile_site_key', '0x4AAAAAAAABkMYinukE8nzUg'), // Turnstile 站点密钥
|
||||||
|
],
|
||||||
|
'payment_config' => [
|
||||||
|
'currency' => admin_setting('currency', 'CNY'), // 货币类型
|
||||||
|
'currency_symbol' => admin_setting('currency_symbol', '¥'), // 货币符号
|
||||||
|
'withdraw_methods' => admin_setting('app_withdraw_methods', ['alipay', 'wechat', 'bank']), // 提现方式列表
|
||||||
|
'min_withdraw_amount' => (int) admin_setting('app_min_withdraw_amount', 100), // 最小提现金额(分)
|
||||||
|
'withdraw_fee_rate' => (float) admin_setting('app_withdraw_fee_rate', 0.01), // 提现手续费率
|
||||||
|
],
|
||||||
|
'notification_config' => [
|
||||||
|
'enable_push_notifications' => (bool) admin_setting('app_enable_push_notifications', true), // 是否开启推送通知
|
||||||
|
'enable_email_notifications' => (bool) admin_setting('app_enable_email_notifications', true), // 是否开启邮件通知
|
||||||
|
'enable_sms_notifications' => (bool) admin_setting('app_enable_sms_notifications', false), // 是否开启短信通知
|
||||||
|
'notification_schedule' => [
|
||||||
|
'traffic_warning' => (bool) admin_setting('app_notification_traffic_warning', true), // 流量警告通知
|
||||||
|
'subscription_expiry' => (bool) admin_setting('app_notification_subscription_expiry', true), // 订阅到期通知
|
||||||
|
'server_maintenance' => (bool) admin_setting('app_notification_server_maintenance', true), // 服务器维护通知
|
||||||
|
'promotional_offers' => (bool) admin_setting('app_notification_promotional_offers', false), // 促销优惠通知
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'cache_config' => [
|
||||||
|
'config_cache_duration' => (int) admin_setting('app_config_cache_duration', 3600), // 配置缓存时长(秒)
|
||||||
|
'server_list_cache_duration' => (int) admin_setting('app_server_list_cache_duration', 1800), // 服务器列表缓存时长(秒)
|
||||||
|
'user_info_cache_duration' => (int) admin_setting('app_user_info_cache_duration', 900), // 用户信息缓存时长(秒)
|
||||||
|
],
|
||||||
|
'last_updated' => time(), // 最后更新时间戳
|
||||||
|
];
|
||||||
|
$config['config_hash'] = md5(json_encode($config)); // 配置哈希值(用于校验)
|
||||||
|
|
||||||
|
$config = $config ?? [];
|
||||||
|
return response()->json(['data' => $config]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getVersion(Request $request)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
strpos($request->header('user-agent'), 'tidalab/4.0.0') !== false
|
||||||
|
|| strpos($request->header('user-agent'), 'tunnelab/4.0.0') !== false
|
||||||
|
) {
|
||||||
|
if (strpos($request->header('user-agent'), 'Win64') !== false) {
|
||||||
|
$data = [
|
||||||
|
'version' => admin_setting('windows_version'),
|
||||||
|
'download_url' => admin_setting('windows_download_url')
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$data = [
|
||||||
|
'version' => admin_setting('macos_version'),
|
||||||
|
'download_url' => admin_setting('macos_download_url')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$data = [
|
||||||
|
'windows_version' => admin_setting('windows_version'),
|
||||||
|
'windows_download_url' => admin_setting('windows_download_url'),
|
||||||
|
'macos_version' => admin_setting('macos_version'),
|
||||||
|
'macos_download_url' => admin_setting('macos_download_url'),
|
||||||
|
'android_version' => admin_setting('android_version'),
|
||||||
|
'android_download_url' => admin_setting('android_download_url')
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $this->success($data);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\V2\Server;
|
|||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Jobs\UserAliveSyncJob;
|
use App\Jobs\UserAliveSyncJob;
|
||||||
|
use App\Services\DeviceStateService;
|
||||||
use App\Services\ServerService;
|
use App\Services\ServerService;
|
||||||
use App\Services\UserService;
|
use App\Services\UserService;
|
||||||
use App\Utils\CacheKey;
|
use App\Utils\CacheKey;
|
||||||
@@ -83,7 +84,10 @@ class ServerController extends Controller
|
|||||||
// handle alive data
|
// handle alive data
|
||||||
$alive = $request->input('alive');
|
$alive = $request->input('alive');
|
||||||
if (is_array($alive) && !empty($alive)) {
|
if (is_array($alive) && !empty($alive)) {
|
||||||
UserAliveSyncJob::dispatch($alive, $nodeType, $nodeId);
|
$deviceStateService = app(DeviceStateService::class);
|
||||||
|
foreach ($alive as $uid => $ips) {
|
||||||
|
$deviceStateService->setDevices((int) $uid, $nodeId, (array) $ips);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle active connections
|
// handle active connections
|
||||||
@@ -127,7 +131,7 @@ class ServerController extends Controller
|
|||||||
// handle node metrics (Metrics)
|
// handle node metrics (Metrics)
|
||||||
$metrics = $request->input('metrics');
|
$metrics = $request->input('metrics');
|
||||||
if (is_array($metrics) && !empty($metrics)) {
|
if (is_array($metrics) && !empty($metrics)) {
|
||||||
ServerService::updateMetrics($node, $metrics);
|
ServerService::updateMetrics($node, $metrics);
|
||||||
}
|
}
|
||||||
|
|
||||||
return response()->json(['data' => true]);
|
return response()->json(['data' => true]);
|
||||||
|
|||||||
@@ -1,108 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Jobs;
|
|
||||||
|
|
||||||
use App\Models\User;
|
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
use Illuminate\Support\Facades\Cache;
|
|
||||||
use App\Services\UserOnlineService;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class UserAliveSyncJob implements ShouldQueue
|
|
||||||
{
|
|
||||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
|
||||||
|
|
||||||
private const CACHE_PREFIX = 'ALIVE_IP_USER_';
|
|
||||||
private const CACHE_TTL = 120;
|
|
||||||
private const NODE_DATA_EXPIRY = 100;
|
|
||||||
|
|
||||||
public function __construct(
|
|
||||||
private readonly array $data,
|
|
||||||
private readonly string $nodeType,
|
|
||||||
private readonly int $nodeId
|
|
||||||
) {
|
|
||||||
$this->onQueue('user_alive_sync');
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handle(): void
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$updateAt = time();
|
|
||||||
$nowTs = time();
|
|
||||||
$now = now();
|
|
||||||
$nodeKey = $this->nodeType . $this->nodeId;
|
|
||||||
$userUpdates = [];
|
|
||||||
|
|
||||||
foreach ($this->data as $uid => $ips) {
|
|
||||||
$cacheKey = self::CACHE_PREFIX . $uid;
|
|
||||||
$ipsArray = Cache::get($cacheKey, []);
|
|
||||||
$ipsArray = [
|
|
||||||
...collect($ipsArray)
|
|
||||||
->filter(fn(mixed $value): bool => is_array($value) && ($updateAt - ($value['lastupdateAt'] ?? 0) <= self::NODE_DATA_EXPIRY)),
|
|
||||||
$nodeKey => [
|
|
||||||
'aliveips' => $ips,
|
|
||||||
'lastupdateAt' => $updateAt,
|
|
||||||
],
|
|
||||||
];
|
|
||||||
|
|
||||||
$count = UserOnlineService::calculateDeviceCount($ipsArray);
|
|
||||||
$ipsArray['alive_ip'] = $count;
|
|
||||||
Cache::put($cacheKey, $ipsArray, now()->addSeconds(self::CACHE_TTL));
|
|
||||||
|
|
||||||
$userUpdates[] = [
|
|
||||||
'id' => (int) $uid,
|
|
||||||
'count' => (int) $count,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!empty($userUpdates)) {
|
|
||||||
$allIds = collect($userUpdates)
|
|
||||||
->pluck('id')
|
|
||||||
->filter()
|
|
||||||
->map(fn($v) => (int) $v)
|
|
||||||
->unique()
|
|
||||||
->values()
|
|
||||||
->all();
|
|
||||||
|
|
||||||
if (!empty($allIds)) {
|
|
||||||
$existingIds = User::query()
|
|
||||||
->whereIn('id', $allIds)
|
|
||||||
->pluck('id')
|
|
||||||
->map(fn($v) => (int) $v)
|
|
||||||
->all();
|
|
||||||
|
|
||||||
if (!empty($existingIds)) {
|
|
||||||
collect($userUpdates)
|
|
||||||
->filter(fn($row) => in_array((int) ($row['id'] ?? 0), $existingIds, true))
|
|
||||||
->chunk(1000)
|
|
||||||
->each(function ($chunk) use ($now) {
|
|
||||||
collect($chunk)->each(function ($update) use ($now) {
|
|
||||||
$id = (int) ($update['id'] ?? 0);
|
|
||||||
$count = (int) ($update['count'] ?? 0);
|
|
||||||
if ($id > 0) {
|
|
||||||
User::query()
|
|
||||||
->whereKey($id)
|
|
||||||
->update([
|
|
||||||
'online_count' => $count,
|
|
||||||
'last_online_at' => $now,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (\Throwable $e) {
|
|
||||||
Log::error('UserAliveSyncJob failed', [
|
|
||||||
'error' => $e->getMessage(),
|
|
||||||
]);
|
|
||||||
$this->fail($e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
148
app/Services/DeviceStateService.php
Normal file
148
app/Services/DeviceStateService.php
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Illuminate\Support\Facades\Redis;
|
||||||
|
|
||||||
|
class DeviceStateService
|
||||||
|
{
|
||||||
|
private const PREFIX = 'user_devices:';
|
||||||
|
private const TTL = 300; // device state ttl
|
||||||
|
private const DB_THROTTLE = 10; // update db throttle
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量设置设备
|
||||||
|
* 用于 HTTP /alive 和 WebSocket report.devices
|
||||||
|
*/
|
||||||
|
public function setDevices(int $userId, int $nodeId, array $ips): void
|
||||||
|
{
|
||||||
|
$key = self::PREFIX . $userId;
|
||||||
|
$timestamp = time();
|
||||||
|
|
||||||
|
$this->clearNodeDevices($nodeId, $userId);
|
||||||
|
|
||||||
|
if (!empty($ips)) {
|
||||||
|
$fields = [];
|
||||||
|
foreach ($ips as $ip) {
|
||||||
|
$fields["{$nodeId}:{$ip}"] = $timestamp;
|
||||||
|
}
|
||||||
|
Redis::hMset($key, $fields);
|
||||||
|
Redis::expire($key, self::TTL);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->notifyUpdate($userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* clear node devices
|
||||||
|
* - only nodeId: clear all devices of the node
|
||||||
|
* - userId and nodeId: clear specific user's specific node device
|
||||||
|
*/
|
||||||
|
public function clearNodeDevices(int $nodeId, ?int $userId = null): void
|
||||||
|
{
|
||||||
|
if ($userId !== null) {
|
||||||
|
$key = self::PREFIX . $userId;
|
||||||
|
$prefix = "{$nodeId}:";
|
||||||
|
foreach (Redis::hkeys($key) as $field) {
|
||||||
|
if (str_starts_with($field, $prefix)) {
|
||||||
|
Redis::hdel($key, $field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$keys = Redis::keys(self::PREFIX . '*');
|
||||||
|
$prefix = "{$nodeId}:";
|
||||||
|
|
||||||
|
foreach ($keys as $key) {
|
||||||
|
foreach (Redis::hkeys($key) as $field) {
|
||||||
|
if (str_starts_with($field, $prefix)) {
|
||||||
|
Redis::hdel($key, $field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get user device count (filter expired data)
|
||||||
|
*/
|
||||||
|
public function getDeviceCount(int $userId): int
|
||||||
|
{
|
||||||
|
$data = Redis::hgetall(self::PREFIX . $userId);
|
||||||
|
$now = time();
|
||||||
|
$count = 0;
|
||||||
|
foreach ($data as $field => $timestamp) {
|
||||||
|
// if ($now - $timestamp <= self::TTL) {
|
||||||
|
$count++;
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get user device count (for alivelist interface)
|
||||||
|
*/
|
||||||
|
public function getAliveList(Collection $users): array
|
||||||
|
{
|
||||||
|
if ($users->isEmpty()) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = [];
|
||||||
|
foreach ($users as $user) {
|
||||||
|
$count = $this->getDeviceCount($user->id);
|
||||||
|
if ($count > 0) {
|
||||||
|
$result[$user->id] = $count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* get devices of multiple users (for sync.devices, filter expired data)
|
||||||
|
*/
|
||||||
|
public function getUsersDevices(array $userIds): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
$now = time();
|
||||||
|
|
||||||
|
foreach ($userIds as $userId) {
|
||||||
|
$data = Redis::hgetall(self::PREFIX . $userId);
|
||||||
|
if (!empty($data)) {
|
||||||
|
$ips = [];
|
||||||
|
foreach ($data as $field => $timestamp) {
|
||||||
|
// if ($now - $timestamp <= self::TTL) {
|
||||||
|
$ips[] = substr($field, strpos($field, ':') + 1);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
if (!empty($ips)) {
|
||||||
|
$result[$userId] = array_unique($ips);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* notify update (throttle control)
|
||||||
|
*/
|
||||||
|
private function notifyUpdate(int $userId): void
|
||||||
|
{
|
||||||
|
$dbThrottleKey = "device:db_throttle:{$userId}";
|
||||||
|
|
||||||
|
if (Redis::setnx($dbThrottleKey, 1)) {
|
||||||
|
Redis::expire($dbThrottleKey, self::DB_THROTTLE);
|
||||||
|
|
||||||
|
User::query()
|
||||||
|
->whereKey($userId)
|
||||||
|
->update([
|
||||||
|
'online_count' => $this->getDeviceCount($userId),
|
||||||
|
'last_online_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
|
|
||||||
namespace App\Services;
|
|
||||||
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Illuminate\Support\Str;
|
|
||||||
|
|
||||||
class UserOnlineService
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* 缓存相关常量
|
|
||||||
*/
|
|
||||||
private const CACHE_PREFIX = 'ALIVE_IP_USER_';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取所有限制设备用户的在线数量
|
|
||||||
*/
|
|
||||||
public function getAliveList(Collection $deviceLimitUsers): array
|
|
||||||
{
|
|
||||||
if ($deviceLimitUsers->isEmpty()) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$cacheKeys = $deviceLimitUsers->pluck('id')
|
|
||||||
->map(fn(int $id): string => self::CACHE_PREFIX . $id)
|
|
||||||
->all();
|
|
||||||
|
|
||||||
return collect(cache()->many($cacheKeys))
|
|
||||||
->filter()
|
|
||||||
->map(fn(array $data): ?int => $data['alive_ip'] ?? null)
|
|
||||||
->filter()
|
|
||||||
->mapWithKeys(fn(int $count, string $key): array => [
|
|
||||||
(int) Str::after($key, self::CACHE_PREFIX) => $count
|
|
||||||
])
|
|
||||||
->all();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取指定用户的在线设备信息
|
|
||||||
*/
|
|
||||||
public static function getUserDevices(int $userId): array
|
|
||||||
{
|
|
||||||
$data = cache()->get(self::CACHE_PREFIX . $userId, []);
|
|
||||||
if (empty($data)) {
|
|
||||||
return ['total_count' => 0, 'devices' => []];
|
|
||||||
}
|
|
||||||
|
|
||||||
$devices = collect($data)
|
|
||||||
->filter(fn(mixed $item): bool => is_array($item) && isset($item['aliveips']))
|
|
||||||
->flatMap(function (array $nodeData, string $nodeKey): array {
|
|
||||||
return collect($nodeData['aliveips'])
|
|
||||||
->mapWithKeys(function (string $ipNodeId) use ($nodeData, $nodeKey): array {
|
|
||||||
$ip = Str::before($ipNodeId, '_');
|
|
||||||
return [
|
|
||||||
$ip => [
|
|
||||||
'ip' => $ip,
|
|
||||||
'last_seen' => $nodeData['lastupdateAt'],
|
|
||||||
'node_type' => Str::before($nodeKey, (string) $nodeData['lastupdateAt'])
|
|
||||||
]
|
|
||||||
];
|
|
||||||
})
|
|
||||||
->all();
|
|
||||||
})
|
|
||||||
->values()
|
|
||||||
->all();
|
|
||||||
|
|
||||||
return [
|
|
||||||
'total_count' => $data['alive_ip'] ?? 0,
|
|
||||||
'devices' => $devices
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量获取用户在线设备数
|
|
||||||
*/
|
|
||||||
public function getOnlineCounts(array $userIds): array
|
|
||||||
{
|
|
||||||
$cacheKeys = collect($userIds)
|
|
||||||
->map(fn(int $id): string => self::CACHE_PREFIX . $id)
|
|
||||||
->all();
|
|
||||||
|
|
||||||
return collect(cache()->many($cacheKeys))
|
|
||||||
->filter()
|
|
||||||
->map(fn(array $data): int => $data['alive_ip'] ?? 0)
|
|
||||||
->all();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 获取用户在线设备数
|
|
||||||
*/
|
|
||||||
public function getOnlineCount(int $userId): int
|
|
||||||
{
|
|
||||||
$data = cache()->get(self::CACHE_PREFIX . $userId, []);
|
|
||||||
return $data['alive_ip'] ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 计算在线设备数量
|
|
||||||
*/
|
|
||||||
public static function calculateDeviceCount(array $ipsArray): int
|
|
||||||
{
|
|
||||||
$mode = (int) admin_setting('device_limit_mode', 0);
|
|
||||||
|
|
||||||
return match ($mode) {
|
|
||||||
1 => collect($ipsArray)
|
|
||||||
->filter(fn(mixed $data): bool => is_array($data) && isset($data['aliveips']))
|
|
||||||
->flatMap(
|
|
||||||
fn(array $data): array => collect($data['aliveips'])
|
|
||||||
->map(fn(string $ipNodeId): string => Str::before($ipNodeId, '_'))
|
|
||||||
->unique()
|
|
||||||
->all()
|
|
||||||
)
|
|
||||||
->unique()
|
|
||||||
->count(),
|
|
||||||
0 => collect($ipsArray)
|
|
||||||
->filter(fn(mixed $data): bool => is_array($data) && isset($data['aliveips']))
|
|
||||||
->sum(fn(array $data): int => count($data['aliveips'])),
|
|
||||||
default => throw new \InvalidArgumentException("Invalid device limit mode: $mode"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Submodule public/assets/admin updated: 878489d9a5...89c0577191
Reference in New Issue
Block a user