feat(api): 新增节点流量悬浮详情与即时自动上线同步

为 server/manage/getNodes 返回节点级今日、本月与累计流量统计,
并在节点管理页名称悬浮层展示上行、下行和合计流量。

同时为自动上线补齐单节点同步入口,在管理端保存、
批量更新以及 REST/WS 心跳后立即同步 show 状态,
避免复制节点后开启自动上线仍需等待定时任务。

另优化管理端前端 Docker 发布流程,默认仅构建 amd64,
并收敛 BuildKit 缓存导出以缩短发布时间
This commit is contained in:
yinjianm
2026-04-28 16:51:35 +08:00
parent a62a124710
commit 1739f7a2f9
21 changed files with 966 additions and 65 deletions
@@ -7,6 +7,8 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\ServerSave;
use App\Models\Server;
use App\Models\ServerGroup;
use App\Models\StatServer;
use App\Services\ServerAutoOnlineService;
use App\Services\ServerGfwCheckService;
use App\Services\ServerService;
use Illuminate\Http\Request;
@@ -17,14 +19,80 @@ class ManageController extends Controller
{
public function getNodes(Request $request)
{
$servers = app(ServerGfwCheckService::class)->decorateServers(ServerService::getAllServers())->map(function ($item) {
$servers = ServerService::getAllServers();
$trafficStats = $this->buildNodeTrafficStats($servers);
$servers = app(ServerGfwCheckService::class)->decorateServers($servers)->map(function ($item) use ($trafficStats) {
$item['groups'] = ServerGroup::whereIn('id', $item['group_ids'] ?? [])->get(['name', 'id']);
$item['parent'] = $item->parent;
$item['traffic_stats'] = $trafficStats[(int) $item['id']] ?? $this->emptyNodeTrafficStats();
return $item;
});
return $this->success($servers);
}
private function buildNodeTrafficStats($servers): array
{
$stats = [];
foreach ($servers as $server) {
$serverId = (int) $server->id;
$stats[$serverId] = $this->emptyNodeTrafficStats();
$stats[$serverId]['total'] = $this->buildTrafficAmount($server->u ?? 0, $server->d ?? 0);
}
if (empty($stats)) {
return [];
}
$this->fillTrafficWindow($stats, 'today', strtotime('today'));
$this->fillTrafficWindow($stats, 'month', strtotime(date('Y-m-01')));
return $stats;
}
private function fillTrafficWindow(array &$stats, string $key, int $startAt): void
{
$rows = StatServer::query()
->selectRaw('server_id, COALESCE(SUM(u), 0) as upload, COALESCE(SUM(d), 0) as download')
->whereIn('server_id', array_keys($stats))
->where('record_type', 'd')
->where('record_at', '>=', $startAt)
->groupBy('server_id')
->get();
foreach ($rows as $row) {
$stats[(int) $row->server_id][$key] = $this->buildTrafficAmount($row->upload, $row->download);
}
}
private function emptyNodeTrafficStats(): array
{
return [
'today' => $this->buildTrafficAmount(0, 0),
'month' => $this->buildTrafficAmount(0, 0),
'total' => $this->buildTrafficAmount(0, 0),
];
}
private function buildTrafficAmount($upload, $download): array
{
$upload = max(0, (int) $upload);
$download = max(0, (int) $download);
return [
'upload' => $upload,
'download' => $download,
'total' => $upload + $download,
];
}
private function syncAutoOnlineIfEnabled(Server $server): void
{
if ((bool) $server->auto_online) {
app(ServerAutoOnlineService::class)->syncServer($server);
}
}
public function sort(Request $request)
{
ini_set('post_max_size', '1m');
@@ -64,6 +132,7 @@ class ManageController extends Controller
$params['gfw_auto_action_at'] = null;
}
$server->update($params);
$this->syncAutoOnlineIfEnabled($server);
return $this->success(true);
} catch (\Exception $e) {
Log::error($e);
@@ -72,7 +141,8 @@ class ManageController extends Controller
}
try {
Server::create($params);
$server = Server::create($params);
$this->syncAutoOnlineIfEnabled($server);
return $this->success(true);
} catch (\Exception $e) {
Log::error($e);
@@ -118,6 +188,8 @@ class ManageController extends Controller
return $this->fail([500, '保存失败']);
}
$this->syncAutoOnlineIfEnabled($server);
return $this->success(true);
}
@@ -295,6 +367,12 @@ class ManageController extends Controller
$server->update($update);
}
});
$servers->each(function (Server $server) {
$freshServer = $server->fresh();
if ($freshServer) {
$this->syncAutoOnlineIfEnabled($freshServer);
}
});
return $this->success(true);
} catch (\Exception $e) {
Log::error($e);
+64 -39
View File
@@ -4,6 +4,7 @@ namespace App\Services;
use App\Models\Server;
use App\Models\ServerGfwCheck;
use Illuminate\Support\Collection;
class ServerAutoOnlineService
{
@@ -12,50 +13,74 @@ class ServerAutoOnlineService
$servers = Server::query()
->where('auto_online', true)
->get();
$gfwStatuses = app(ServerGfwCheckService::class)->getLatestStatusesForServers($servers);
$result = [
'total' => $servers->count(),
return $this->syncServers($servers);
}
public function syncServer(Server $server): array
{
if (!(bool) $server->auto_online) {
return $this->emptyResult();
}
return $this->syncServers(collect([$server]));
}
private function syncServers(Collection $servers): array
{
$gfwStatuses = app(ServerGfwCheckService::class)->getLatestStatusesForServers($servers);
$result = $this->emptyResult($servers->count());
foreach ($servers as $server) {
$this->syncServerWithStatuses($server, $gfwStatuses, $result);
}
return $result;
}
private function syncServerWithStatuses(Server $server, array $gfwStatuses, array &$result): void
{
$sourceNodeId = (int) ($server->parent_id ?: $server->id);
$gfwStatus = $gfwStatuses[$sourceNodeId] ?? null;
$isGfwManaged = (bool) ($server->gfw_check_enabled ?? true) && $gfwStatus !== null;
$isGfwBlocked = $isGfwManaged && $gfwStatus === ServerGfwCheck::STATUS_BLOCKED;
$isGfwHeld = $isGfwManaged
&& (bool) $server->gfw_auto_hidden
&& $gfwStatus !== ServerGfwCheck::STATUS_NORMAL;
$shouldShow = !$isGfwBlocked && !$isGfwHeld && (int) $server->available_status !== Server::STATUS_OFFLINE;
$shouldClearGfwAutoHidden = $gfwStatus === ServerGfwCheck::STATUS_NORMAL
&& (bool) $server->gfw_auto_hidden;
$wasShown = (bool) $server->show;
if ($wasShown === $shouldShow && !$shouldClearGfwAutoHidden) {
$result['unchanged']++;
return;
}
$server->show = $shouldShow;
if ($isGfwBlocked) {
$server->gfw_auto_hidden = true;
$server->gfw_auto_action_at = time();
} elseif ($shouldClearGfwAutoHidden) {
$server->gfw_auto_hidden = false;
$server->gfw_auto_action_at = time();
}
$server->save();
$result['updated']++;
if ($wasShown !== $shouldShow) {
$shouldShow ? $result['shown']++ : $result['hidden']++;
}
}
private function emptyResult(int $total = 0): array
{
return [
'total' => $total,
'updated' => 0,
'shown' => 0,
'hidden' => 0,
'unchanged' => 0,
];
foreach ($servers as $server) {
$sourceNodeId = (int) ($server->parent_id ?: $server->id);
$gfwStatus = $gfwStatuses[$sourceNodeId] ?? null;
$isGfwManaged = (bool) ($server->gfw_check_enabled ?? true) && $gfwStatus !== null;
$isGfwBlocked = $isGfwManaged && $gfwStatus === ServerGfwCheck::STATUS_BLOCKED;
$isGfwHeld = $isGfwManaged
&& (bool) $server->gfw_auto_hidden
&& $gfwStatus !== ServerGfwCheck::STATUS_NORMAL;
$shouldShow = !$isGfwBlocked && !$isGfwHeld && (int) $server->available_status !== Server::STATUS_OFFLINE;
$shouldClearGfwAutoHidden = $gfwStatus === ServerGfwCheck::STATUS_NORMAL
&& (bool) $server->gfw_auto_hidden;
$wasShown = (bool) $server->show;
if ($wasShown === $shouldShow && !$shouldClearGfwAutoHidden) {
$result['unchanged']++;
continue;
}
$server->show = $shouldShow;
if ($isGfwBlocked) {
$server->gfw_auto_hidden = true;
$server->gfw_auto_action_at = time();
} elseif ($shouldClearGfwAutoHidden) {
$server->gfw_auto_hidden = false;
$server->gfw_auto_action_at = time();
}
$server->save();
$result['updated']++;
if ($wasShown !== $shouldShow) {
$shouldShow ? $result['shown']++ : $result['hidden']++;
}
}
return $result;
}
}
+4
View File
@@ -210,6 +210,10 @@ class ServerService
time(),
3600
);
if ((bool) $node->auto_online) {
app(ServerAutoOnlineService::class)->syncServer($node);
}
}
/**
+1 -2
View File
@@ -29,8 +29,7 @@ class NodeEventHandlers
$node = Server::find($nodeId);
if (!$node) return;
$nodeType = strtoupper($node->type);
Cache::put(\App\Utils\CacheKey::get('SERVER_' . $nodeType . '_LAST_CHECK_AT', $nodeId), time(), 3600);
ServerService::touchNode($node);
ServerService::updateMetrics($node, $data);
Log::debug("[WS] Node#{$nodeId} status updated");