refactor: restructure device limit system

This commit is contained in:
xboard
2026-03-25 17:50:16 +08:00
parent 73a37a07dd
commit 420521d90a
10 changed files with 429 additions and 299 deletions
@@ -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;
}
}
}
+109 -3
View File
@@ -3,6 +3,7 @@
namespace App\Console\Commands;
use App\Models\Server;
use App\Services\DeviceStateService;
use App\Services\NodeSyncService;
use App\Services\NodeRegistry;
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) {
@@ -126,6 +144,10 @@ class NodeWebSocketServer extends Command
NodeRegistry::add($nodeId, $conn);
Cache::put("node_ws_alive:{$nodeId}", true, 86400);
// 清理该节点的旧设备数据(节点重连后需重新上报全量)
$deviceStateService = app(DeviceStateService::class);
$deviceStateService->clearNodeDevices($nodeId);
Log::debug("[WS] Node#{$nodeId} connected", [
'remote' => $conn->getRemoteIp(),
'total' => NodeRegistry::count(),
@@ -162,8 +184,17 @@ class NodeWebSocketServer extends Command
$this->handleNodeStatus($nodeId, $msg['data']);
}
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:
// Future: handle other node-initiated messages if needed
break;
}
};
@@ -173,6 +204,9 @@ class NodeWebSocketServer extends Command
$nodeId = $conn->nodeId;
NodeRegistry::remove($nodeId);
Cache::forget("node_ws_alive:{$nodeId}");
app(DeviceStateService::class)->clearNodeDevices($nodeId);
Log::debug("[WS] Node#{$nodeId} disconnected", [
'total' => NodeRegistry::count(),
]);
@@ -194,13 +228,85 @@ class NodeWebSocketServer extends Command
// Update last check-in cache
Cache::put(\App\Utils\CacheKey::get('SERVER_' . $nodeType . '_LAST_CHECK_AT', $nodeId), time(), 3600);
// Update metrics cache via Service
ServerService::updateMetrics($node, $data);
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.
* 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);
if ($sent) {
Log::debug("[WS] Pushed {$event} to node#{$nodeId}");
Log::debug("[WS] Pushed {$event} to node#{$nodeId}, data: " . json_encode($data));
}
});
+1 -4
View File
@@ -7,7 +7,6 @@ use App\Utils\CacheKey;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
use Illuminate\Support\Facades\Cache;
use App\Services\UserOnlineService;
class Kernel extends ConsoleKernel
{
@@ -35,7 +34,7 @@ class Kernel extends ConsoleKernel
$schedule->command('check:order')->everyMinute()->onOneServer()->withoutOverlapping(5);
$schedule->command('check:commission')->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
$schedule->command('reset:traffic')->everyMinute()->onOneServer()->withoutOverlapping(10);
$schedule->command('reset:log')->daily()->onOneServer();
@@ -47,8 +46,6 @@ class Kernel extends ConsoleKernel
// if (env('ENABLE_AUTO_BACKUP_AND_UPDATE', false)) {
// $schedule->command('backup:database', ['true'])->daily()->onOneServer();
// }
$schedule->command('cleanup:expired-online-status')->everyMinute()->onOneServer()->withoutOverlapping(4);
app(PluginManager::class)->registerPluginSchedules($schedule);
}