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);