mirror of
https://github.com/lkddi/Xboard.git
synced 2026-04-03 18:40:52 +08:00
188 lines
5.0 KiB
PHP
188 lines
5.0 KiB
PHP
<?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
|
|
|
|
/**
|
|
* 移除 Redis key 的前缀
|
|
*/
|
|
private function removeRedisPrefix(string $key): string
|
|
{
|
|
$prefix = config('database.redis.options.prefix', '');
|
|
return $prefix ? substr($key, strlen($prefix)) : $key;
|
|
}
|
|
|
|
/**
|
|
* 批量设置设备
|
|
* 用于 HTTP /alive 和 WebSocket report.devices
|
|
*/
|
|
public function setDevices(int $userId, int $nodeId, array $ips): void
|
|
{
|
|
$key = self::PREFIX . $userId;
|
|
$timestamp = time();
|
|
|
|
$this->removeNodeDevices($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);
|
|
}
|
|
|
|
/**
|
|
* 获取某节点的所有设备数据
|
|
* 返回: {userId: [ip1, ip2, ...], ...}
|
|
*/
|
|
public function getNodeDevices(int $nodeId): array
|
|
{
|
|
$keys = Redis::keys(self::PREFIX . '*');
|
|
$prefix = "{$nodeId}:";
|
|
$result = [];
|
|
foreach ($keys as $key) {
|
|
$actualKey = $this->removeRedisPrefix($key);
|
|
$uid = (int) substr($actualKey, strlen(self::PREFIX));
|
|
$data = Redis::hgetall($actualKey);
|
|
foreach ($data as $field => $timestamp) {
|
|
if (str_starts_with($field, $prefix)) {
|
|
$ip = substr($field, strlen($prefix));
|
|
$result[$uid][] = $ip;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 删除某节点某用户的设备
|
|
*/
|
|
public function removeNodeDevices(int $nodeId, int $userId): void
|
|
{
|
|
$key = self::PREFIX . $userId;
|
|
$prefix = "{$nodeId}:";
|
|
|
|
foreach (Redis::hkeys($key) as $field) {
|
|
if (str_starts_with($field, $prefix)) {
|
|
Redis::hdel($key, $field);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 清除节点所有设备数据(用于节点断开连接)
|
|
*/
|
|
public function clearAllNodeDevices(int $nodeId): array
|
|
{
|
|
$oldDevices = $this->getNodeDevices($nodeId);
|
|
$prefix = "{$nodeId}:";
|
|
|
|
foreach ($oldDevices as $userId => $ips) {
|
|
$key = self::PREFIX . $userId;
|
|
foreach (Redis::hkeys($key) as $field) {
|
|
if (str_starts_with($field, $prefix)) {
|
|
Redis::hdel($key, $field);
|
|
}
|
|
}
|
|
}
|
|
|
|
return array_keys($oldDevices);
|
|
}
|
|
|
|
/**
|
|
* get user device count (deduplicated by IP, filter expired data)
|
|
*/
|
|
public function getDeviceCount(int $userId): int
|
|
{
|
|
$data = Redis::hgetall(self::PREFIX . $userId);
|
|
$now = time();
|
|
$ips = [];
|
|
|
|
foreach ($data as $field => $timestamp) {
|
|
if ($now - $timestamp <= self::TTL) {
|
|
$ips[] = substr($field, strpos($field, ':') + 1);
|
|
}
|
|
}
|
|
|
|
return count(array_unique($ips));
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
*/
|
|
public 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(),
|
|
]);
|
|
// }
|
|
}
|
|
}
|