diff --git a/app/Http/Controllers/V1/User/StatController.php b/app/Http/Controllers/V1/User/StatController.php index fb0985d..f3bc707 100644 --- a/app/Http/Controllers/V1/User/StatController.php +++ b/app/Http/Controllers/V1/User/StatController.php @@ -46,8 +46,22 @@ class StatController extends Controller $deviceList = data_get($devices, 'devices', []); return collect($deviceList) - ->filter(fn($item): bool => is_array($item) && !empty($item['ip']) && !empty($item['node_type'])) - ->groupBy(fn(array $item): string => strtolower((string) $item['node_type'])) + ->filter(fn($item): bool => is_array($item) && !empty($item['ip'])) + ->map(function (array $item): array { + $nodeKey = strtolower((string) ($item['node_key'] ?? '')); + if ($nodeKey === '') { + $nodeType = strtolower((string) ($item['node_type'] ?? '')); + $nodeId = (int) ($item['node_id'] ?? 0); + $nodeKey = ($nodeType !== '' && $nodeId > 0) ? "{$nodeType}{$nodeId}" : $nodeType; + } + + return [ + 'node_key' => $nodeKey, + 'ip' => (string) ($item['ip'] ?? ''), + ]; + }) + ->filter(fn(array $item): bool => $item['node_key'] !== '' && $item['ip'] !== '') + ->groupBy(fn(array $item): string => $item['node_key']) ->map(fn($items): array => collect($items) ->pluck('ip') ->filter() diff --git a/app/Http/Controllers/V2/Admin/StatController.php b/app/Http/Controllers/V2/Admin/StatController.php index d16362f..cfe25f6 100644 --- a/app/Http/Controllers/V2/Admin/StatController.php +++ b/app/Http/Controllers/V2/Admin/StatController.php @@ -273,8 +273,22 @@ class StatController extends Controller $deviceList = data_get($devices, 'devices', []); return collect($deviceList) - ->filter(fn($item): bool => is_array($item) && !empty($item['ip']) && !empty($item['node_type'])) - ->groupBy(fn(array $item): string => strtolower((string) $item['node_type'])) + ->filter(fn($item): bool => is_array($item) && !empty($item['ip'])) + ->map(function (array $item): array { + $nodeKey = strtolower((string) ($item['node_key'] ?? '')); + if ($nodeKey === '') { + $nodeType = strtolower((string) ($item['node_type'] ?? '')); + $nodeId = (int) ($item['node_id'] ?? 0); + $nodeKey = ($nodeType !== '' && $nodeId > 0) ? "{$nodeType}{$nodeId}" : $nodeType; + } + + return [ + 'node_key' => $nodeKey, + 'ip' => (string) ($item['ip'] ?? ''), + ]; + }) + ->filter(fn(array $item): bool => $item['node_key'] !== '' && $item['ip'] !== '') + ->groupBy(fn(array $item): string => $item['node_key']) ->map(fn($items): array => collect($items) ->pluck('ip') ->filter() diff --git a/app/Services/ServerService.php b/app/Services/ServerService.php index 30a883d..1ff023b 100644 --- a/app/Services/ServerService.php +++ b/app/Services/ServerService.php @@ -100,15 +100,23 @@ class ServerService */ public static function getServer($serverId, ?string $serverType) { - return Server::query() + $baseQuery = Server::query() ->when($serverType, function ($query) use ($serverType) { $query->where('type', Server::normalizeType($serverType)); - }) - ->where(function ($query) use ($serverId) { - $query->where('code', $serverId) - ->orWhere('id', $serverId); - }) - ->orderByRaw('CASE WHEN code = ? THEN 0 ELSE 1 END', [$serverId]) + }); + + $serverIdValue = is_scalar($serverId) ? (string) $serverId : ''; + $isCanonicalInt = preg_match('/^(0|[1-9][0-9]*)$/', $serverIdValue) === 1; + + if ($isCanonicalInt) { + $serverById = (clone $baseQuery)->whereKey((int) $serverIdValue)->first(); + if ($serverById) { + return $serverById; + } + } + + return (clone $baseQuery) + ->where('code', $serverIdValue) ->first(); } } diff --git a/app/Services/UserOnlineService.php b/app/Services/UserOnlineService.php index 42ec854..7aad786 100644 --- a/app/Services/UserOnlineService.php +++ b/app/Services/UserOnlineService.php @@ -3,6 +3,7 @@ namespace App\Services; +use App\Models\Server; use Illuminate\Support\Collection; use Illuminate\Support\Str; @@ -49,17 +50,26 @@ class UserOnlineService $devices = collect($data) ->filter(fn(mixed $item): bool => is_array($item) && isset($item['aliveips'])) ->flatMap(function (array $nodeData, string $nodeKey): array { + $parsedNode = self::parseNodeMeta((string) $nodeKey); + return collect($nodeData['aliveips']) - ->mapWithKeys(function (string $ipNodeId) use ($nodeData, $nodeKey): array { + ->map(function (string $ipNodeId) use ($nodeData, $parsedNode): array { $ip = Str::before($ipNodeId, '_'); + $payloadNodeId = (int) Str::after($ipNodeId, '_'); + $nodeType = $parsedNode['node_type'] ?? ''; + $nodeId = $payloadNodeId > 0 ? $payloadNodeId : ((int) ($parsedNode['node_id'] ?? 0)); + $nodeKeyValue = self::buildNodeKey($nodeType, $nodeId); + return [ - $ip => [ - 'ip' => $ip, - 'last_seen' => $nodeData['lastupdateAt'], - 'node_type' => Str::before($nodeKey, (string) $nodeData['lastupdateAt']) - ] + 'ip' => $ip, + 'last_seen' => (int) ($nodeData['lastupdateAt'] ?? 0), + 'node_type' => $nodeType, + 'node_id' => $nodeId > 0 ? $nodeId : null, + 'node_key' => $nodeKeyValue, ]; }) + ->filter(fn(array $item): bool => !empty($item['ip'])) + ->values() ->all(); }) ->values() @@ -71,6 +81,45 @@ class UserOnlineService ]; } + /** + * Parse node metadata from cache key such as "shadowsocks12". + * + * @return array{node_type: string, node_id: int} + */ + private static function parseNodeMeta(string $nodeKey): array + { + $normalizedKey = strtolower(trim($nodeKey)); + if ($normalizedKey === '') { + return ['node_type' => '', 'node_id' => 0]; + } + + $types = collect(Server::VALID_TYPES) + ->map(fn(string $type): string => strtolower($type)) + ->sortByDesc(fn(string $type): int => strlen($type)) + ->values(); + + foreach ($types as $type) { + if (!str_starts_with($normalizedKey, $type)) { + continue; + } + + $idPart = substr($normalizedKey, strlen($type)); + $nodeId = ctype_digit($idPart) ? (int) $idPart : 0; + return ['node_type' => $type, 'node_id' => $nodeId]; + } + + return ['node_type' => '', 'node_id' => 0]; + } + + private static function buildNodeKey(string $nodeType, int $nodeId): ?string + { + if ($nodeType === '' || $nodeId <= 0) { + return null; + } + + return "{$nodeType}{$nodeId}"; + } + /** * 批量获取用户在线设备数 @@ -120,4 +169,4 @@ class UserOnlineService default => throw new \InvalidArgumentException("Invalid device limit mode: $mode"), }; } -} \ No newline at end of file +}