Merge remote-tracking branch 'upstream/master'

# Conflicts:
#	app/Services/UserOnlineService.php
#	public/assets/admin
This commit is contained in:
yinjianm
2026-04-16 16:59:21 +08:00
103 changed files with 2153 additions and 16043 deletions
+2 -2
View File
@@ -36,7 +36,7 @@ class LoginService
}
// 查找用户
$user = User::where('email', $email)->first();
$user = User::byEmail($email)->first();
if (!$user) {
return [false, [400, __('Incorrect email or password')]];
}
@@ -99,7 +99,7 @@ class LoginService
}
// 查找用户
$user = User::where('email', $email)->first();
$user = User::byEmail($email)->first();
if (!$user) {
return [false, [400, __('This email is not registered in the system')]];
}
+2 -2
View File
@@ -27,7 +27,7 @@ class MailLinkService
return [false, [429, __('Sending frequently, please try again later')]];
}
$user = User::where('email', $email)->first();
$user = User::byEmail($email)->first();
if (!$user) {
return [true, true]; // 成功但用户不存在,保护用户隐私
}
@@ -46,7 +46,7 @@ class MailLinkService
$this->sendMailLinkEmail($user, $link);
return [true, $link];
return [true, true];
}
/**
+1 -2
View File
@@ -91,8 +91,7 @@ class RegisterService
}
// 检查邮箱是否存在
$email = $request->input('email');
$exist = User::where('email', $email)->first();
$exist = User::byEmail($request->input('email'))->first();
if ($exist) {
return [false, [400201, __('Email already exists')]];
}
+187
View File
@@ -0,0 +1,187 @@
<?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(),
]);
// }
}
}
+2 -2
View File
@@ -13,7 +13,7 @@ class NodeSyncService
/**
* Check if node has active WS connection
*/
private static function isNodeOnline(int $nodeId): bool
public static function isNodeOnline(int $nodeId): bool
{
return (bool) Cache::get("node_ws_alive:{$nodeId}");
}
@@ -125,7 +125,7 @@ class NodeSyncService
/**
* Publish a push command to Redis — picked up by the Workerman WS server
*/
private static function push(int $nodeId, string $event, array $data): void
public static function push(int $nodeId, string $event, array $data): void
{
try {
Redis::publish('node:push', json_encode([
+20 -6
View File
@@ -42,6 +42,11 @@ class ServerService
{
$servers = Server::whereJsonContains('group_ids', (string) $user->group_id)
->where('show', true)
->where(function ($query) {
$query->whereNull('transfer_enable')
->orWhere('transfer_enable', 0)
->orWhereRaw('u + d < transfer_enable');
})
->orderBy('sort', 'ASC')
->get()
->append(['last_check_at', 'last_push_at', 'online', 'is_online', 'available_status', 'cache_key', 'server_key']);
@@ -168,11 +173,20 @@ class ServerService
'host' => $host,
'server_name' => $protocolSettings['server_name'],
'multiplex' => data_get($protocolSettings, 'multiplex'),
'tls' => (int) $protocolSettings['tls'],
'tls_settings' => match ((int) $protocolSettings['tls']) {
2 => $protocolSettings['reality_settings'],
default => null,
},
],
'vless' => [
...$baseConfig,
'tls' => (int) $protocolSettings['tls'],
'flow' => $protocolSettings['flow'],
'decryption' => match (data_get($protocolSettings, 'encryption.enabled')) {
true => data_get($protocolSettings, 'encryption.decryption'),
default => null,
},
'tls_settings' => match ((int) $protocolSettings['tls']) {
2 => $protocolSettings['reality_settings'],
default => $protocolSettings['tls_settings'],
@@ -239,10 +253,10 @@ class ServerService
default => [],
};
$response = array_filter(
$response,
static fn ($value) => $value !== null
);
// $response = array_filter(
// $response,
// static fn ($value) => $value !== null
// );
if (!empty($node['route_ids'])) {
$response['routes'] = self::getRoutes($node['route_ids']);
@@ -256,7 +270,7 @@ class ServerService
$response['custom_routes'] = $node['custom_routes'];
}
if (!empty($node['cert_config'])) {
if (!empty($node['cert_config']) && data_get($node['cert_config'],'cert_mode') !== 'none' ) {
$response['cert_config'] = $node['cert_config'];
}
@@ -269,7 +283,7 @@ class ServerService
* @param string $serverType
* @return Server|null
*/
public static function getServer($serverId, ?string $serverType)
public static function getServer($serverId, ?string $serverType = null): Server | null
{
$baseQuery = Server::query()
->when($serverType, function ($query) use ($serverType) {