fix(api): 修复节点流量限额共享统计与父子显隐联动
统一节点流量统计与限额展示口径,节点详情新增昨日流量, 并让今日、昨日和本月使用清晰的半开时间窗口聚合 同 machine_id 或同 host 的节点现在共享当前账期已用流量, 管理端优先使用后端 traffic_limit_snapshot 展示月额度状态, mi-node 下发的 current_used 也改为共享账期统计 新增 parent_auto_hidden 标记与父节点显隐联动服务,父节点 因自动上线或流量限额变为不可展示时会隐藏当前显示的子节点, 恢复时只恢复这批自动隐藏的子节点,避免覆盖手动操作
This commit is contained in:
@@ -53,6 +53,7 @@ class ServerAutoOnlineService
|
||||
$wasShown = (bool) $server->show;
|
||||
|
||||
if ($wasShown === $shouldShow && !$shouldClearGfwAutoHidden) {
|
||||
$this->syncChildrenForFinalState($server, $shouldShow, $result);
|
||||
$result['unchanged']++;
|
||||
return;
|
||||
}
|
||||
@@ -71,6 +72,24 @@ class ServerAutoOnlineService
|
||||
if ($wasShown !== $shouldShow) {
|
||||
$shouldShow ? $result['shown']++ : $result['hidden']++;
|
||||
}
|
||||
$this->syncChildrenForFinalState($server, $shouldShow, $result);
|
||||
}
|
||||
|
||||
private function syncChildrenForFinalState(Server $server, bool $shouldShow, array &$result): void
|
||||
{
|
||||
$childResult = app(ServerParentVisibilityService::class)
|
||||
->syncChildrenForParent($server, $shouldShow);
|
||||
$hidden = (int) ($childResult['hidden'] ?? 0);
|
||||
$restored = (int) ($childResult['restored'] ?? 0);
|
||||
$childUpdates = $hidden + $restored;
|
||||
|
||||
if ($childUpdates <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$result['updated'] += $childUpdates;
|
||||
$result['hidden'] += $hidden;
|
||||
$result['shown'] += $restored;
|
||||
}
|
||||
|
||||
private function emptyResult(int $total = 0): array
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Server;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class ServerParentVisibilityService
|
||||
{
|
||||
public function syncChildrenForParent(Server $parent, bool $parentVisible): array
|
||||
{
|
||||
return $parentVisible
|
||||
? $this->restoreChildrenForParent($parent)
|
||||
: $this->hideChildrenForParent($parent);
|
||||
}
|
||||
|
||||
public function hideChildrenForParent(Server $parent): array
|
||||
{
|
||||
if (!$this->isParentNode($parent)) {
|
||||
return $this->emptyResult();
|
||||
}
|
||||
|
||||
$result = $this->emptyResult();
|
||||
$now = time();
|
||||
|
||||
$children = $this->childrenQuery($parent)
|
||||
->where('show', true)
|
||||
->get();
|
||||
|
||||
foreach ($children as $child) {
|
||||
if (!$child instanceof Server) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$child->forceFill([
|
||||
'show' => false,
|
||||
'parent_auto_hidden' => true,
|
||||
'parent_auto_action_at' => $now,
|
||||
])->save();
|
||||
$result['hidden']++;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function restoreChildrenForParent(Server $parent): array
|
||||
{
|
||||
if (!$this->isParentNode($parent)) {
|
||||
return $this->emptyResult();
|
||||
}
|
||||
|
||||
$result = $this->emptyResult();
|
||||
$now = time();
|
||||
|
||||
$children = $this->childrenQuery($parent)
|
||||
->where('parent_auto_hidden', true)
|
||||
->get();
|
||||
|
||||
foreach ($children as $child) {
|
||||
if (!$child instanceof Server) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->hasBlockingAutoHide($child)) {
|
||||
$result['unchanged']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$child->forceFill([
|
||||
'show' => true,
|
||||
'parent_auto_hidden' => false,
|
||||
'parent_auto_action_at' => $now,
|
||||
])->save();
|
||||
$result['restored']++;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
public function clearParentAutoHidden(Server $server): void
|
||||
{
|
||||
if (!(bool) $server->parent_auto_hidden && $server->parent_auto_action_at === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$server->parent_auto_hidden = false;
|
||||
$server->parent_auto_action_at = null;
|
||||
}
|
||||
|
||||
private function isParentNode(Server $server): bool
|
||||
{
|
||||
return (int) ($server->parent_id ?? 0) <= 0 && (int) ($server->id ?? 0) > 0;
|
||||
}
|
||||
|
||||
private function childrenQuery(Server $parent): Builder
|
||||
{
|
||||
return Server::query()->where('parent_id', (int) $parent->id);
|
||||
}
|
||||
|
||||
private function hasBlockingAutoHide(Server $server): bool
|
||||
{
|
||||
return (bool) $server->gfw_auto_hidden;
|
||||
}
|
||||
|
||||
private function emptyResult(): array
|
||||
{
|
||||
return [
|
||||
'hidden' => 0,
|
||||
'restored' => 0,
|
||||
'unchanged' => 0,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -247,7 +247,8 @@ class ServerService
|
||||
];
|
||||
|
||||
if (isset($metrics['traffic_limit']) && is_array($metrics['traffic_limit'])) {
|
||||
app(ServerTrafficLimitService::class)->applyRuntimeMetrics($node, $metrics['traffic_limit']);
|
||||
$metricsData['traffic_limit'] = app(ServerTrafficLimitService::class)
|
||||
->applyRuntimeMetrics($node, $metrics['traffic_limit']);
|
||||
}
|
||||
|
||||
Cache::put(
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\StatServer;
|
||||
use App\Utils\CacheKey;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class ServerTrafficLimitService
|
||||
@@ -13,22 +17,95 @@ class ServerTrafficLimitService
|
||||
*/
|
||||
public function buildNodeConfig(Server $server): array
|
||||
{
|
||||
$enabled = $this->isEnabled($server);
|
||||
$nextResetAt = $enabled
|
||||
? ($server->traffic_limit_next_reset_at ?: $this->calculateNextResetAt($server)?->timestamp)
|
||||
: null;
|
||||
$snapshot = $this->buildTrafficLimitSnapshot($server);
|
||||
$enabled = (bool) $snapshot['enabled'];
|
||||
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
'limit' => $enabled ? (int) $server->transfer_enable : 0,
|
||||
'limit' => (int) $snapshot['limit'],
|
||||
'reset_day' => $enabled ? $this->normalizeResetDay($server->traffic_limit_reset_day) : 0,
|
||||
'reset_time' => $enabled ? $this->normalizeResetTime($server->traffic_limit_reset_time) : null,
|
||||
'timezone' => $enabled ? $this->normalizeTimezone($server->traffic_limit_timezone) : null,
|
||||
'current_used' => max(0, (int) $server->u + (int) $server->d),
|
||||
'last_reset_at' => (int) ($server->traffic_limit_last_reset_at ?? 0),
|
||||
'current_used' => (int) $snapshot['used'],
|
||||
'last_reset_at' => (int) $snapshot['last_reset_at'],
|
||||
'next_reset_at' => (int) $snapshot['next_reset_at'],
|
||||
'suspended_at' => (int) $snapshot['suspended_at'],
|
||||
'status' => (string) $snapshot['status'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build traffic limit snapshots for an already loaded server list.
|
||||
*/
|
||||
public function buildSnapshotsForServers(Collection $servers, ?int $referenceTimestamp = null): array
|
||||
{
|
||||
$serversByScope = $servers->groupBy(fn (Server $server) => $this->trafficLimitScopeKey($server));
|
||||
$snapshots = [];
|
||||
|
||||
foreach ($servers as $server) {
|
||||
$scopeServers = $serversByScope->get($this->trafficLimitScopeKey($server), collect([$server]));
|
||||
$snapshots[(int) $server->id] = $this->buildTrafficLimitSnapshot(
|
||||
$server,
|
||||
$scopeServers,
|
||||
$referenceTimestamp
|
||||
);
|
||||
}
|
||||
|
||||
return $snapshots;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a panel-side snapshot using the shared traffic limit scope.
|
||||
*/
|
||||
public function buildTrafficLimitSnapshot(
|
||||
Server $server,
|
||||
?Collection $scopeServers = null,
|
||||
?int $referenceTimestamp = null
|
||||
): array {
|
||||
$enabled = $this->isEnabled($server);
|
||||
$scopeServers = $this->resolveTrafficLimitScopeServers($server, $scopeServers);
|
||||
$limit = $enabled ? (int) $server->transfer_enable : 0;
|
||||
$trafficLimit = $server->getKey() ? $this->cachedTrafficLimitMetrics($server) : null;
|
||||
$used = $enabled ? $this->currentUsed($server, $trafficLimit, $scopeServers, $referenceTimestamp) : 0;
|
||||
$reportedSuspension = $this->scopeReportedSuspension($server, $trafficLimit, $scopeServers, $limit);
|
||||
$suspended = $enabled && $limit > 0 && (
|
||||
$used >= $limit
|
||||
|| $reportedSuspension['suspended']
|
||||
);
|
||||
$reference = $this->referenceTime($server, $referenceTimestamp);
|
||||
$cycleStartAt = $enabled ? $this->calculateCurrentCycleStartAt($server, $reference) : null;
|
||||
$cycleStartTimestamp = $cycleStartAt ? $cycleStartAt->timestamp : 0;
|
||||
$nextResetAt = $enabled
|
||||
? ($server->traffic_limit_next_reset_at ?: $this->calculateNextResetAt($server, $reference)?->timestamp)
|
||||
: 0;
|
||||
$lastResetAt = max(
|
||||
(int) ($server->traffic_limit_last_reset_at ?? 0),
|
||||
$cycleStartTimestamp
|
||||
);
|
||||
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
'limit' => $limit,
|
||||
'used' => $used,
|
||||
'percent' => $limit > 0 ? min(100, (int) round(($used / $limit) * 100)) : 0,
|
||||
'suspended' => $suspended,
|
||||
'last_reset_at' => $enabled ? $lastResetAt : 0,
|
||||
'cycle_start_at' => $enabled ? $cycleStartTimestamp : 0,
|
||||
'next_reset_at' => (int) ($nextResetAt ?? 0),
|
||||
'suspended_at' => (int) ($server->traffic_limit_suspended_at ?? 0),
|
||||
'status' => $server->traffic_limit_status ?: Server::TRAFFIC_LIMIT_STATUS_NORMAL,
|
||||
'suspended_at' => $suspended
|
||||
? (int) ($reportedSuspension['suspended_at'] ?: ($server->traffic_limit_suspended_at ?? time()))
|
||||
: 0,
|
||||
'status' => $suspended
|
||||
? Server::TRAFFIC_LIMIT_STATUS_SUSPENDED
|
||||
: Server::TRAFFIC_LIMIT_STATUS_NORMAL,
|
||||
'scope_key' => $this->trafficLimitScopeKey($server),
|
||||
'scope_node_ids' => $scopeServers
|
||||
->pluck('id')
|
||||
->filter(fn ($id) => (int) $id > 0)
|
||||
->map(fn ($id) => (int) $id)
|
||||
->unique()
|
||||
->values()
|
||||
->all(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -39,6 +116,9 @@ class ServerTrafficLimitService
|
||||
{
|
||||
$values = $this->scheduleValues($server);
|
||||
$server->forceFill($values)->saveQuietly();
|
||||
$freshServer = $server->refresh();
|
||||
$this->syncCachedRuntimeMetrics($freshServer);
|
||||
$this->syncParentChildrenFromTrafficLimit($freshServer);
|
||||
|
||||
if ($notifyNode) {
|
||||
NodeSyncService::notifyConfigUpdated((int) $server->id);
|
||||
@@ -61,6 +141,9 @@ class ServerTrafficLimitService
|
||||
: null,
|
||||
'traffic_limit_suspended_at' => null,
|
||||
])->saveQuietly();
|
||||
$freshServer = $server->refresh();
|
||||
$this->syncCachedRuntimeMetrics($freshServer);
|
||||
$this->syncParentChildrenFromTrafficLimit($freshServer);
|
||||
|
||||
if ($notifyNode) {
|
||||
NodeSyncService::notifyFullSync((int) $server->id);
|
||||
@@ -147,29 +230,69 @@ class ServerTrafficLimitService
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply limiter metrics from mi-node to panel-side runtime fields.
|
||||
* Calculate the reset boundary that starts the current billing cycle.
|
||||
*/
|
||||
public function applyRuntimeMetrics(Server $server, array $trafficLimit): void
|
||||
public function calculateCurrentCycleStartAt(Server $server, ?Carbon $from = null): ?Carbon
|
||||
{
|
||||
if (empty($trafficLimit)) {
|
||||
return;
|
||||
if (!$this->isEnabled($server)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$suspended = (bool) ($trafficLimit['suspended'] ?? false);
|
||||
$timezone = $this->normalizeTimezone($server->traffic_limit_timezone);
|
||||
$from = ($from ?: Carbon::now($timezone))->copy()->timezone($timezone);
|
||||
[$hour, $minute] = $this->parseResetTime($server->traffic_limit_reset_time);
|
||||
|
||||
$currentMonthTarget = $this->targetForMonth(
|
||||
$from->year,
|
||||
$from->month,
|
||||
$this->normalizeResetDay($server->traffic_limit_reset_day),
|
||||
$hour,
|
||||
$minute,
|
||||
$timezone
|
||||
);
|
||||
|
||||
if ($currentMonthTarget->timestamp <= $from->timestamp) {
|
||||
return $currentMonthTarget;
|
||||
}
|
||||
|
||||
$previousMonth = $from->copy()->startOfMonth()->subMonthNoOverflow();
|
||||
return $this->targetForMonth(
|
||||
$previousMonth->year,
|
||||
$previousMonth->month,
|
||||
$this->normalizeResetDay($server->traffic_limit_reset_day),
|
||||
$hour,
|
||||
$minute,
|
||||
$timezone
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply limiter metrics from mi-node to panel-side runtime fields.
|
||||
*/
|
||||
public function applyRuntimeMetrics(Server $server, array $trafficLimit): array
|
||||
{
|
||||
if (empty($trafficLimit)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$snapshot = $this->runtimeSnapshot($server, $trafficLimit);
|
||||
$suspended = (bool) $snapshot['suspended'];
|
||||
$server->forceFill([
|
||||
'traffic_limit_status' => $suspended
|
||||
? Server::TRAFFIC_LIMIT_STATUS_SUSPENDED
|
||||
: Server::TRAFFIC_LIMIT_STATUS_NORMAL,
|
||||
'traffic_limit_last_reset_at' => $this->nullableTimestamp($trafficLimit['last_reset_at'] ?? null),
|
||||
'traffic_limit_next_reset_at' => $this->nullableTimestamp($trafficLimit['next_reset_at'] ?? null),
|
||||
'traffic_limit_suspended_at' => $suspended
|
||||
? $this->nullableTimestamp($trafficLimit['suspended_at'] ?? null)
|
||||
: null,
|
||||
'traffic_limit_last_reset_at' => $this->nullableTimestamp($snapshot['last_reset_at']),
|
||||
'traffic_limit_next_reset_at' => $this->nullableTimestamp($snapshot['next_reset_at']),
|
||||
'traffic_limit_suspended_at' => $this->nullableTimestamp($snapshot['suspended_at']),
|
||||
]);
|
||||
|
||||
if ($server->isDirty()) {
|
||||
$server->saveQuietly();
|
||||
}
|
||||
|
||||
$this->syncParentChildrenFromTrafficLimit($server->refresh());
|
||||
|
||||
return $snapshot;
|
||||
}
|
||||
|
||||
private function scheduleValues(Server $server): array
|
||||
@@ -183,13 +306,21 @@ class ServerTrafficLimitService
|
||||
];
|
||||
}
|
||||
|
||||
$currentUsed = $this->currentUsed($server);
|
||||
$suspended = $this->isSuspendedByUsage($server, $currentUsed);
|
||||
|
||||
return [
|
||||
'traffic_limit_enabled' => true,
|
||||
'traffic_limit_reset_day' => $this->normalizeResetDay($server->traffic_limit_reset_day),
|
||||
'traffic_limit_reset_time' => $this->normalizeResetTime($server->traffic_limit_reset_time),
|
||||
'traffic_limit_timezone' => $this->normalizeTimezone($server->traffic_limit_timezone),
|
||||
'traffic_limit_status' => $server->traffic_limit_status ?: Server::TRAFFIC_LIMIT_STATUS_NORMAL,
|
||||
'traffic_limit_status' => $suspended
|
||||
? Server::TRAFFIC_LIMIT_STATUS_SUSPENDED
|
||||
: Server::TRAFFIC_LIMIT_STATUS_NORMAL,
|
||||
'traffic_limit_next_reset_at' => $this->calculateNextResetAt($server)?->timestamp,
|
||||
'traffic_limit_suspended_at' => $suspended
|
||||
? ($server->traffic_limit_suspended_at ?: time())
|
||||
: null,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -198,6 +329,285 @@ class ServerTrafficLimitService
|
||||
return (bool) $server->traffic_limit_enabled && (int) $server->transfer_enable > 0;
|
||||
}
|
||||
|
||||
private function isSuspendedByUsage(Server $server, ?int $used = null): bool
|
||||
{
|
||||
return $this->isEnabled($server)
|
||||
&& ($used ?? $this->currentUsed($server)) >= (int) $server->transfer_enable;
|
||||
}
|
||||
|
||||
private function syncParentChildrenFromTrafficLimit(Server $server): void
|
||||
{
|
||||
if ((int) ($server->parent_id ?? 0) > 0 || (int) ($server->id ?? 0) <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$suspended = $server->traffic_limit_status === Server::TRAFFIC_LIMIT_STATUS_SUSPENDED
|
||||
|| $this->isSuspendedByUsage($server);
|
||||
|
||||
app(ServerParentVisibilityService::class)->syncChildrenForParent(
|
||||
$server,
|
||||
!$suspended && (bool) $server->show
|
||||
);
|
||||
}
|
||||
|
||||
private function currentUsed(
|
||||
Server $server,
|
||||
?array $trafficLimit = null,
|
||||
?Collection $scopeServers = null,
|
||||
?int $referenceTimestamp = null
|
||||
): int
|
||||
{
|
||||
$scopeServers = $this->resolveTrafficLimitScopeServers($server, $scopeServers);
|
||||
$cycleUsed = $this->currentCycleUsed($server, $scopeServers, $referenceTimestamp);
|
||||
$panelUsed = $this->panelUsed($scopeServers);
|
||||
$reportedUsed = $this->scopeReportedUsed($server, $trafficLimit, $scopeServers);
|
||||
|
||||
return max($cycleUsed ?? $panelUsed, $reportedUsed);
|
||||
}
|
||||
|
||||
private function runtimeSnapshot(Server $server, array $trafficLimit): array
|
||||
{
|
||||
$enabled = $this->isEnabled($server);
|
||||
$limit = $enabled ? (int) $server->transfer_enable : 0;
|
||||
$used = $this->currentUsed($server, $trafficLimit);
|
||||
$reportedLimit = (int) ($trafficLimit['limit'] ?? 0);
|
||||
$snapshotCurrent = $this->isRuntimeSnapshotCurrent($server, $trafficLimit);
|
||||
$reportedSuspended = $snapshotCurrent && (bool) ($trafficLimit['suspended'] ?? false);
|
||||
$sameLimitSnapshot = $reportedLimit === 0 || $reportedLimit === $limit;
|
||||
$metricNextResetAt = (int) ($trafficLimit['next_reset_at'] ?? 0);
|
||||
$serverNextResetAt = (int) ($server->traffic_limit_next_reset_at ?? 0);
|
||||
$suspended = $enabled && (
|
||||
$used >= $limit
|
||||
|| ($reportedSuspended && $sameLimitSnapshot)
|
||||
);
|
||||
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
'limit' => $limit,
|
||||
'used' => $used,
|
||||
'suspended' => $suspended,
|
||||
'last_reset_at' => max(
|
||||
(int) ($server->traffic_limit_last_reset_at ?? 0),
|
||||
(int) ($trafficLimit['last_reset_at'] ?? 0)
|
||||
),
|
||||
'next_reset_at' => $snapshotCurrent && $sameLimitSnapshot && $metricNextResetAt > 0
|
||||
? $metricNextResetAt
|
||||
: $serverNextResetAt,
|
||||
'suspended_at' => $suspended
|
||||
? (int) ($trafficLimit['suspended_at'] ?? $server->traffic_limit_suspended_at ?? time())
|
||||
: 0,
|
||||
'status' => $suspended
|
||||
? Server::TRAFFIC_LIMIT_STATUS_SUSPENDED
|
||||
: Server::TRAFFIC_LIMIT_STATUS_NORMAL,
|
||||
];
|
||||
}
|
||||
|
||||
private function syncCachedRuntimeMetrics(Server $server): void
|
||||
{
|
||||
$metrics = Cache::get($this->metricsCacheKey($server));
|
||||
if (!is_array($metrics)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$trafficLimit = $this->runtimeSnapshot(
|
||||
$server,
|
||||
is_array($metrics['traffic_limit'] ?? null) ? $metrics['traffic_limit'] : []
|
||||
);
|
||||
|
||||
$metrics['traffic_limit'] = $trafficLimit;
|
||||
Cache::put(
|
||||
$this->metricsCacheKey($server),
|
||||
$metrics,
|
||||
max(300, (int) admin_setting('server_push_interval', 60) * 3)
|
||||
);
|
||||
}
|
||||
|
||||
private function cachedTrafficLimitMetrics(Server $server): ?array
|
||||
{
|
||||
$metrics = Cache::get($this->metricsCacheKey($server));
|
||||
return is_array($metrics) && is_array($metrics['traffic_limit'] ?? null)
|
||||
? $metrics['traffic_limit']
|
||||
: null;
|
||||
}
|
||||
|
||||
private function scopeReportedUsed(Server $server, ?array $trafficLimit, Collection $scopeServers): int
|
||||
{
|
||||
$used = 0;
|
||||
|
||||
foreach ($scopeServers as $scopeServer) {
|
||||
$scopeTrafficLimit = $this->scopeTrafficLimitMetrics($server, $trafficLimit, $scopeServer);
|
||||
if (!is_array($scopeTrafficLimit) || !$this->isRuntimeSnapshotCurrent($scopeServer, $scopeTrafficLimit)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$used = max($used, max(0, (int) ($scopeTrafficLimit['used'] ?? 0)));
|
||||
}
|
||||
|
||||
return $used;
|
||||
}
|
||||
|
||||
private function scopeReportedSuspension(
|
||||
Server $server,
|
||||
?array $trafficLimit,
|
||||
Collection $scopeServers,
|
||||
int $limit
|
||||
): array {
|
||||
foreach ($scopeServers as $scopeServer) {
|
||||
$scopeTrafficLimit = $this->scopeTrafficLimitMetrics($server, $trafficLimit, $scopeServer);
|
||||
if (!is_array($scopeTrafficLimit) || !$this->isRuntimeSnapshotCurrent($scopeServer, $scopeTrafficLimit)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$reportedLimit = (int) ($scopeTrafficLimit['limit'] ?? 0);
|
||||
$sameLimitSnapshot = $reportedLimit === 0 || $reportedLimit === $limit;
|
||||
if ($sameLimitSnapshot && (bool) ($scopeTrafficLimit['suspended'] ?? false)) {
|
||||
return [
|
||||
'suspended' => true,
|
||||
'suspended_at' => (int) ($scopeTrafficLimit['suspended_at'] ?? 0),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'suspended' => false,
|
||||
'suspended_at' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
private function scopeTrafficLimitMetrics(
|
||||
Server $server,
|
||||
?array $trafficLimit,
|
||||
Server $scopeServer
|
||||
): ?array {
|
||||
if (!$scopeServer->getKey()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ((int) $scopeServer->getKey() === (int) $server->getKey()) {
|
||||
return $trafficLimit ?? $this->cachedTrafficLimitMetrics($scopeServer);
|
||||
}
|
||||
|
||||
return $this->cachedTrafficLimitMetrics($scopeServer);
|
||||
}
|
||||
|
||||
private function isRuntimeSnapshotCurrent(Server $server, array $trafficLimit): bool
|
||||
{
|
||||
$serverLastResetAt = (int) ($server->traffic_limit_last_reset_at ?? 0);
|
||||
$snapshotLastResetAt = (int) ($trafficLimit['last_reset_at'] ?? 0);
|
||||
|
||||
return $serverLastResetAt <= 0
|
||||
|| ($snapshotLastResetAt > 0 && $snapshotLastResetAt >= $serverLastResetAt);
|
||||
}
|
||||
|
||||
private function metricsCacheKey(Server $server): string
|
||||
{
|
||||
$serverId = $server->parent_id ?: $server->id;
|
||||
return CacheKey::get('SERVER_' . strtoupper((string) $server->type) . '_METRICS', $serverId);
|
||||
}
|
||||
|
||||
private function resolveTrafficLimitScopeServers(Server $server, ?Collection $scopeServers = null): Collection
|
||||
{
|
||||
if ($scopeServers instanceof Collection && $scopeServers->isNotEmpty()) {
|
||||
return $scopeServers->values();
|
||||
}
|
||||
|
||||
if (!$server->exists || !$server->getKey()) {
|
||||
return collect([$server]);
|
||||
}
|
||||
|
||||
$machineId = (int) ($server->machine_id ?? 0);
|
||||
if ($machineId > 0) {
|
||||
return Server::query()
|
||||
->where('machine_id', $machineId)
|
||||
->get();
|
||||
}
|
||||
|
||||
$host = $this->normalizeHost($server->host);
|
||||
if ($host === '') {
|
||||
return collect([$server]);
|
||||
}
|
||||
|
||||
return Server::query()
|
||||
->whereRaw('LOWER(TRIM(host)) = ?', [$host])
|
||||
->get();
|
||||
}
|
||||
|
||||
private function currentCycleUsed(
|
||||
Server $server,
|
||||
Collection $scopeServers,
|
||||
?int $referenceTimestamp = null
|
||||
): ?int {
|
||||
$serverIds = $scopeServers
|
||||
->pluck('id')
|
||||
->filter(fn ($id) => (int) $id > 0)
|
||||
->map(fn ($id) => (int) $id)
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
if ($serverIds->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$reference = $this->referenceTime($server, $referenceTimestamp);
|
||||
$cycleStartAt = $this->calculateCurrentCycleStartAt($server, $reference);
|
||||
if (!$cycleStartAt) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// v2_stat_server is stored at day granularity, so include the reset day.
|
||||
$startAt = $cycleStartAt->copy()->startOfDay()->timestamp;
|
||||
$endAt = $reference->copy()->addDay()->startOfDay()->timestamp;
|
||||
$row = StatServer::query()
|
||||
->selectRaw('COUNT(*) as records, COALESCE(SUM(u), 0) as upload, COALESCE(SUM(d), 0) as download')
|
||||
->whereIn('server_id', $serverIds->all())
|
||||
->where('record_type', 'd')
|
||||
->where('record_at', '>=', $startAt)
|
||||
->where('record_at', '<', $endAt)
|
||||
->first();
|
||||
|
||||
if ((int) ($row->records ?? 0) <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return max(0, (int) ($row->upload ?? 0) + (int) ($row->download ?? 0));
|
||||
}
|
||||
|
||||
private function panelUsed(Collection $scopeServers): int
|
||||
{
|
||||
return max(0, (int) $scopeServers->sum(
|
||||
fn (Server $server) => max(0, (int) $server->u + (int) $server->d)
|
||||
));
|
||||
}
|
||||
|
||||
private function trafficLimitScopeKey(Server $server): string
|
||||
{
|
||||
$machineId = (int) ($server->machine_id ?? 0);
|
||||
if ($machineId > 0) {
|
||||
return 'machine:' . $machineId;
|
||||
}
|
||||
|
||||
$host = $this->normalizeHost($server->host);
|
||||
if ($host !== '') {
|
||||
return 'host:' . $host;
|
||||
}
|
||||
|
||||
return 'server:' . (int) ($server->id ?? 0);
|
||||
}
|
||||
|
||||
private function normalizeHost(?string $host): string
|
||||
{
|
||||
return strtolower(trim((string) $host));
|
||||
}
|
||||
|
||||
private function referenceTime(Server $server, ?int $referenceTimestamp = null): Carbon
|
||||
{
|
||||
$timezone = $this->normalizeTimezone($server->traffic_limit_timezone);
|
||||
|
||||
return $referenceTimestamp !== null
|
||||
? Carbon::createFromTimestamp($referenceTimestamp, $timezone)
|
||||
: Carbon::now($timezone);
|
||||
}
|
||||
|
||||
private function normalizeResetDay($day): int
|
||||
{
|
||||
$normalized = (int) ($day ?: 1);
|
||||
|
||||
Reference in New Issue
Block a user