diff --git a/app/Http/Controllers/V1/User/StatController.php b/app/Http/Controllers/V1/User/StatController.php index 11bb9c0..fb0985d 100644 --- a/app/Http/Controllers/V1/User/StatController.php +++ b/app/Http/Controllers/V1/User/StatController.php @@ -5,22 +5,55 @@ namespace App\Http\Controllers\V1\User; use App\Http\Controllers\Controller; use App\Http\Resources\TrafficLogResource; use App\Models\StatUser; -use App\Services\StatisticalService; +use App\Services\UserOnlineService; use Illuminate\Http\Request; -use Illuminate\Support\Facades\DB; class StatController extends Controller { public function getTrafficLog(Request $request) { $startDate = now()->startOfMonth()->timestamp; + $userId = (int) $request->user()->id; + $records = StatUser::query() - ->where('user_id', $request->user()->id) + ->with(['server:id,name']) + ->where('user_id', $userId) ->where('record_at', '>=', $startDate) ->orderBy('record_at', 'DESC') ->get(); + $deviceMap = $this->buildNodeDeviceMap($userId); + $records->each(function (StatUser $record) use ($deviceMap): void { + $serverType = strtolower((string) $record->server_type); + $serverId = (int) $record->server_id; + $nodeKey = $serverType !== '' && $serverId > 0 ? "{$serverType}{$serverId}" : null; + $deviceIps = $nodeKey ? ($deviceMap[$nodeKey] ?? []) : []; + + $record->setAttribute('server_name', $record->server?->name); + $record->setAttribute('node_name', $record->server?->name); + $record->setAttribute('device_ips', $deviceIps); + $record->setAttribute('device_count', count($deviceIps)); + $record->setAttribute('device_name', $deviceIps[0] ?? 'Unknown'); + }); + $data = TrafficLogResource::collection(collect($records)); return $this->success($data); } + + private function buildNodeDeviceMap(int $userId): array + { + $devices = UserOnlineService::getUserDevices($userId); + $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'])) + ->map(fn($items): array => collect($items) + ->pluck('ip') + ->filter() + ->unique() + ->values() + ->all()) + ->all(); + } } diff --git a/app/Http/Controllers/V2/Admin/StatController.php b/app/Http/Controllers/V2/Admin/StatController.php index e7d615c..d16362f 100644 --- a/app/Http/Controllers/V2/Admin/StatController.php +++ b/app/Http/Controllers/V2/Admin/StatController.php @@ -3,6 +3,7 @@ namespace App\Http\Controllers\V2\Admin; use App\Http\Controllers\Controller; +use App\Http\Resources\TrafficLogResource; use App\Models\CommissionLog; use App\Models\Order; use App\Models\Server; @@ -12,6 +13,7 @@ use App\Models\StatUser; use App\Models\Ticket; use App\Models\User; use App\Services\StatisticalService; +use App\Services\UserOnlineService; use Illuminate\Http\Request; class StatController extends Controller @@ -234,17 +236,54 @@ class StatController extends Controller ]); $pageSize = $request->input('pageSize', 10); - $records = StatUser::orderBy('record_at', 'DESC') - ->where('user_id', $request->input('user_id')) + $userId = (int) $request->input('user_id'); + $records = StatUser::query() + ->with(['server:id,name']) + ->orderBy('record_at', 'DESC') + ->where('user_id', $userId) ->paginate($pageSize); - $data = $records->items(); + $deviceMap = $this->buildNodeDeviceMap($userId); + $data = collect($records->items()) + ->map(function (StatUser $record) use ($deviceMap, $request): array { + $serverType = strtolower((string) $record->server_type); + $serverId = (int) $record->server_id; + $nodeKey = $serverType !== '' && $serverId > 0 ? "{$serverType}{$serverId}" : null; + $deviceIps = $nodeKey ? ($deviceMap[$nodeKey] ?? []) : []; + + $record->setAttribute('server_name', $record->server?->name); + $record->setAttribute('node_name', $record->server?->name); + $record->setAttribute('device_ips', $deviceIps); + $record->setAttribute('device_count', count($deviceIps)); + $record->setAttribute('device_name', $deviceIps[0] ?? 'Unknown'); + + return (new TrafficLogResource($record))->toArray($request); + }) + ->all(); + return [ 'data' => $data, 'total' => $records->total(), ]; } + private function buildNodeDeviceMap(int $userId): array + { + $devices = UserOnlineService::getUserDevices($userId); + $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'])) + ->map(fn($items): array => collect($items) + ->pluck('ip') + ->filter() + ->unique() + ->values() + ->all()) + ->all(); + } + public function getStatRecord(Request $request) { return [ diff --git a/app/Http/Resources/TrafficLogResource.php b/app/Http/Resources/TrafficLogResource.php index 798ea7e..d5333fb 100644 --- a/app/Http/Resources/TrafficLogResource.php +++ b/app/Http/Resources/TrafficLogResource.php @@ -14,13 +14,49 @@ class TrafficLogResource extends JsonResource */ public function toArray(Request $request): array { + $serverId = (int) data_get($this->resource, 'server_id', 0); + $serverType = strtolower((string) data_get($this->resource, 'server_type', '')); + $serverName = data_get($this->resource, 'server_name') + ?: data_get($this->resource, 'node_name') + ?: data_get($this->resource, 'server.name'); + + if (!$serverName && $serverId > 0) { + $serverName = "Node #{$serverId}"; + } + + $deviceIps = data_get($this->resource, 'device_ips', []); + if (!is_array($deviceIps)) { + $deviceIps = []; + } + + $deviceName = data_get($this->resource, 'device_name'); + if (!$deviceName) { + $deviceName = $deviceIps[0] ?? 'Unknown'; + } + $data = [ - "d" => $this['d'], - "u" => $this['u'], - "record_at" => $this['record_at'], - "server_rate" => $this['server_rate'], + 'id' => data_get($this->resource, 'id'), + 'd' => (int) data_get($this->resource, 'd', 0), + 'u' => (int) data_get($this->resource, 'u', 0), + 'record_at' => (int) data_get($this->resource, 'record_at', 0), + 'record_type' => data_get($this->resource, 'record_type'), + 'server_rate' => (float) data_get($this->resource, 'server_rate', 1), + 'server_id' => $serverId > 0 ? $serverId : null, + 'server_type' => $serverType !== '' ? $serverType : null, + 'server_name' => $serverName, + 'node_name' => $serverName, + 'node_key' => $serverId > 0 && $serverType !== '' ? "{$serverType}{$serverId}" : null, + 'device_name' => $deviceName, + 'device_ips' => $deviceIps, + 'device_count' => (int) data_get($this->resource, 'device_count', count($deviceIps)), + 'created_at' => data_get($this->resource, 'created_at'), + 'updated_at' => data_get($this->resource, 'updated_at'), ]; - if(!config('hidden_features.enable_exposed_user_count_fix')) $data['user_id']= $this['user_id']; + + if (!config('hidden_features.enable_exposed_user_count_fix')) { + $data['user_id'] = (int) data_get($this->resource, 'user_id', 0); + } + return $data; } } diff --git a/app/Jobs/StatUserJob.php b/app/Jobs/StatUserJob.php index 4e3b81c..da03a3f 100644 --- a/app/Jobs/StatUserJob.php +++ b/app/Jobs/StatUserJob.php @@ -75,10 +75,15 @@ class StatUserJob implements ShouldQueue protected function processUserStatForSqlite(int $uid, array $v, int $recordAt): void { - DB::transaction(function () use ($uid, $v, $recordAt) { + $serverId = $this->getServerId(); + $serverType = $this->getServerType(); + + DB::transaction(function () use ($uid, $v, $recordAt, $serverId, $serverType) { $existingRecord = StatUser::where([ 'user_id' => $uid, 'server_rate' => $this->server['rate'], + 'server_id' => $serverId, + 'server_type' => $serverType, 'record_at' => $recordAt, 'record_type' => $this->recordType, ])->first(); @@ -93,6 +98,8 @@ class StatUserJob implements ShouldQueue StatUser::create([ 'user_id' => $uid, 'server_rate' => $this->server['rate'], + 'server_id' => $serverId, + 'server_type' => $serverType, 'record_at' => $recordAt, 'record_type' => $this->recordType, 'u' => ($v[0] * $this->server['rate']), @@ -106,10 +113,15 @@ class StatUserJob implements ShouldQueue protected function processUserStatForOtherDatabases(int $uid, array $v, int $recordAt): void { + $serverId = $this->getServerId(); + $serverType = $this->getServerType(); + StatUser::upsert( [ 'user_id' => $uid, 'server_rate' => $this->server['rate'], + 'server_id' => $serverId, + 'server_type' => $serverType, 'record_at' => $recordAt, 'record_type' => $this->recordType, 'u' => ($v[0] * $this->server['rate']), @@ -117,7 +129,7 @@ class StatUserJob implements ShouldQueue 'created_at' => time(), 'updated_at' => time(), ], - ['user_id', 'server_rate', 'record_at', 'record_type'], + ['user_id', 'server_rate', 'server_id', 'server_type', 'record_at'], [ 'u' => DB::raw("u + VALUES(u)"), 'd' => DB::raw("d + VALUES(d)"), @@ -135,10 +147,12 @@ class StatUserJob implements ShouldQueue $now = time(); $u = ($v[0] * $this->server['rate']); $d = ($v[1] * $this->server['rate']); + $serverId = $this->getServerId(); + $serverType = $this->getServerType(); - $sql = "INSERT INTO {$table} (user_id, server_rate, record_at, record_type, u, d, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?) - ON CONFLICT (user_id, server_rate, record_at) + $sql = "INSERT INTO {$table} (user_id, server_rate, server_id, server_type, record_at, record_type, u, d, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT (user_id, server_rate, server_id, server_type, record_at) DO UPDATE SET u = {$table}.u + EXCLUDED.u, d = {$table}.d + EXCLUDED.d, @@ -147,6 +161,8 @@ class StatUserJob implements ShouldQueue DB::statement($sql, [ $uid, $this->server['rate'], + $serverId, + $serverType, $recordAt, $this->recordType, $u, @@ -155,4 +171,15 @@ class StatUserJob implements ShouldQueue $now, ]); } -} \ No newline at end of file + + private function getServerId(): int + { + return (int) ($this->server['id'] ?? 0); + } + + private function getServerType(): string + { + $serverType = $this->server['type'] ?? $this->protocol ?? ''; + return strtolower((string) $serverType); + } +} diff --git a/app/Models/StatUser.php b/app/Models/StatUser.php index a956bd7..1fb62e2 100644 --- a/app/Models/StatUser.php +++ b/app/Models/StatUser.php @@ -3,6 +3,7 @@ namespace App\Models; use Illuminate\Database\Eloquent\Model; +use Illuminate\Database\Eloquent\Relations\BelongsTo; /** * App\Models\StatUser @@ -22,7 +23,25 @@ class StatUser extends Model protected $dateFormat = 'U'; protected $guarded = ['id']; protected $casts = [ + 'server_id' => 'integer', 'created_at' => 'timestamp', 'updated_at' => 'timestamp' ]; + + public function server(): BelongsTo + { + return $this->belongsTo(Server::class, 'server_id', 'id'); + } + + public function getNodeKeyAttribute(): ?string + { + $serverType = strtolower((string) $this->server_type); + $serverId = (int) $this->server_id; + + if ($serverType === '' || $serverId <= 0) { + return null; + } + + return $serverType . $serverId; + } } diff --git a/database/migrations/2026_02_25_000001_add_node_fields_to_v2_stat_user_table.php b/database/migrations/2026_02_25_000001_add_node_fields_to_v2_stat_user_table.php new file mode 100644 index 0000000..9051523 --- /dev/null +++ b/database/migrations/2026_02_25_000001_add_node_fields_to_v2_stat_user_table.php @@ -0,0 +1,115 @@ +unsignedInteger('server_id')->default(0)->comment('Node ID'); + } + + if (!Schema::hasColumn(self::TABLE, 'server_type')) { + $table->string('server_type', 32)->default('')->comment('Node type'); + } + }); + + Schema::table(self::TABLE, function (Blueprint $table) { + try { + $table->dropUnique(self::OLD_UNIQUE_INDEX); + } catch (\Throwable) { + } + + try { + $table->dropIndex(self::OLD_COMPOSITE_INDEX); + } catch (\Throwable) { + } + + try { + $table->dropIndex(['user_id', 'server_rate', 'record_at']); + } catch (\Throwable) { + } + }); + + Schema::table(self::TABLE, function (Blueprint $table) { + try { + $table->unique( + ['user_id', 'server_rate', 'server_id', 'server_type', 'record_at'], + self::NEW_UNIQUE_INDEX + ); + } catch (\Throwable) { + } + + try { + $table->index( + ['user_id', 'server_id', 'server_type', 'record_at'], + self::NEW_COMPOSITE_INDEX + ); + } catch (\Throwable) { + } + + try { + $table->index('server_id', self::NEW_SERVER_ID_INDEX); + } catch (\Throwable) { + } + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table(self::TABLE, function (Blueprint $table) { + try { + $table->dropUnique(self::NEW_UNIQUE_INDEX); + } catch (\Throwable) { + } + + try { + $table->dropIndex(self::NEW_COMPOSITE_INDEX); + } catch (\Throwable) { + } + + try { + $table->dropIndex(self::NEW_SERVER_ID_INDEX); + } catch (\Throwable) { + } + }); + + Schema::table(self::TABLE, function (Blueprint $table) { + try { + $table->unique(['server_rate', 'user_id', 'record_at'], self::OLD_UNIQUE_INDEX); + } catch (\Throwable) { + } + + try { + $table->index(['user_id', 'server_rate', 'record_at']); + } catch (\Throwable) { + } + }); + + Schema::table(self::TABLE, function (Blueprint $table) { + if (Schema::hasColumn(self::TABLE, 'server_type')) { + $table->dropColumn('server_type'); + } + + if (Schema::hasColumn(self::TABLE, 'server_id')) { + $table->dropColumn('server_id'); + } + }); + } +};