feat(api): 新增节点墙检测自动托管与显隐
新增定时墙检测命令与节点托管字段,自动为开启托管的父 节点创建检测任务,并在 blocked 时自动隐藏节点、normal 时仅恢复由墙检测自动隐藏的节点 更新自动上线服务以尊重 blocked 与自动隐藏状态,避免疑 似被墙节点被重新发布;同时补齐管理端墙检测托管开关、 刷新入口、批量设置与相关测试和知识库同步
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Services\ServerGfwCheckService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class SyncServerGfwChecks extends Command
|
||||
{
|
||||
protected $signature = 'sync:server-gfw-checks {--limit= : Maximum number of nodes to enqueue}';
|
||||
|
||||
protected $description = 'Create automated GFW check tasks for managed parent nodes';
|
||||
|
||||
public function handle(ServerGfwCheckService $service): int
|
||||
{
|
||||
$limit = $this->option('limit');
|
||||
$result = $service->startAutomaticChecks(
|
||||
is_numeric($limit) ? (int) $limit : null
|
||||
);
|
||||
|
||||
$this->info(sprintf(
|
||||
'Server GFW checks synced: total=%d started=%d skipped=%d active=%d',
|
||||
$result['total'],
|
||||
count($result['started']),
|
||||
count($result['skipped']),
|
||||
$result['active']
|
||||
));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,7 @@ class Kernel extends ConsoleKernel
|
||||
// cleanup stale online_count (GC for Redis TTL expiration)
|
||||
$schedule->command('cleanup:online-status')->everyFiveMinutes()->onOneServer();
|
||||
$schedule->command('sync:server-auto-online')->everyFiveMinutes()->onOneServer()->withoutOverlapping(5);
|
||||
$schedule->command('sync:server-gfw-checks')->everyThirtyMinutes()->onOneServer()->withoutOverlapping(30);
|
||||
// backup Timing
|
||||
// if (env('ENABLE_AUTO_BACKUP_AND_UPDATE', false)) {
|
||||
// $schedule->command('backup:database', ['true'])->daily()->onOneServer();
|
||||
|
||||
@@ -59,6 +59,10 @@ class ManageController extends Controller
|
||||
return $this->fail([400202, '服务器不存在']);
|
||||
}
|
||||
try {
|
||||
if (array_key_exists('show', $params)) {
|
||||
$params['gfw_auto_hidden'] = false;
|
||||
$params['gfw_auto_action_at'] = null;
|
||||
}
|
||||
$server->update($params);
|
||||
return $this->success(true);
|
||||
} catch (\Exception $e) {
|
||||
@@ -82,6 +86,7 @@ class ManageController extends Controller
|
||||
'id' => 'required|integer',
|
||||
'show' => 'nullable|integer',
|
||||
'auto_online' => 'nullable|boolean',
|
||||
'gfw_check_enabled' => 'nullable|boolean',
|
||||
'machine_id' => 'nullable|integer',
|
||||
'enabled' => 'nullable|boolean',
|
||||
]);
|
||||
@@ -93,10 +98,15 @@ class ManageController extends Controller
|
||||
|
||||
if (array_key_exists('show', $params)) {
|
||||
$server->show = (int) $params['show'];
|
||||
$server->gfw_auto_hidden = false;
|
||||
$server->gfw_auto_action_at = null;
|
||||
}
|
||||
if (array_key_exists('auto_online', $params)) {
|
||||
$server->auto_online = (bool) $params['auto_online'];
|
||||
}
|
||||
if (array_key_exists('gfw_check_enabled', $params)) {
|
||||
$server->gfw_check_enabled = (bool) $params['gfw_check_enabled'];
|
||||
}
|
||||
if (array_key_exists('machine_id', $params)) {
|
||||
$server->machine_id = $params['machine_id'] ?: null;
|
||||
}
|
||||
@@ -231,6 +241,7 @@ class ManageController extends Controller
|
||||
'ids.*' => 'integer',
|
||||
'show' => 'nullable|integer|in:0,1',
|
||||
'auto_online' => 'nullable|boolean',
|
||||
'gfw_check_enabled' => 'nullable|boolean',
|
||||
'enabled' => 'nullable|boolean',
|
||||
'machine_id' => 'nullable|integer',
|
||||
'host' => 'sometimes|required|string',
|
||||
@@ -247,10 +258,15 @@ class ManageController extends Controller
|
||||
$update = [];
|
||||
if (array_key_exists('show', $params) && $params['show'] !== null) {
|
||||
$update['show'] = (int) $params['show'];
|
||||
$update['gfw_auto_hidden'] = false;
|
||||
$update['gfw_auto_action_at'] = null;
|
||||
}
|
||||
if (array_key_exists('auto_online', $params) && $params['auto_online'] !== null) {
|
||||
$update['auto_online'] = (bool) $params['auto_online'];
|
||||
}
|
||||
if (array_key_exists('gfw_check_enabled', $params) && $params['gfw_check_enabled'] !== null) {
|
||||
$update['gfw_check_enabled'] = (bool) $params['gfw_check_enabled'];
|
||||
}
|
||||
if (array_key_exists('enabled', $params) && $params['enabled'] !== null) {
|
||||
$update['enabled'] = (bool) $params['enabled'];
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ class ServerSave extends FormRequest
|
||||
'code' => 'nullable|string',
|
||||
'show' => '',
|
||||
'auto_online' => 'nullable|boolean',
|
||||
'gfw_check_enabled' => 'nullable|boolean',
|
||||
'name' => 'required|string',
|
||||
'group_ids' => 'nullable|array',
|
||||
'route_ids' => 'nullable|array',
|
||||
|
||||
@@ -25,6 +25,9 @@ use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
* @property array|null $tags 标签
|
||||
* @property boolean $show 是否显示
|
||||
* @property boolean $auto_online 是否根据在线状态自动同步显示
|
||||
* @property boolean $gfw_check_enabled 是否自动检测墙状态并同步显示
|
||||
* @property boolean $gfw_auto_hidden 是否由墙状态自动隐藏
|
||||
* @property int|null $gfw_auto_action_at 最近墙状态自动显隐时间
|
||||
* @property string|null $allow_insecure 是否允许不安全
|
||||
* @property string|null $network 网络类型
|
||||
* @property int|null $parent_id 父节点ID
|
||||
@@ -127,6 +130,9 @@ class Server extends Model
|
||||
'last_push_at' => 'integer',
|
||||
'show' => 'boolean',
|
||||
'auto_online' => 'boolean',
|
||||
'gfw_check_enabled' => 'boolean',
|
||||
'gfw_auto_hidden' => 'boolean',
|
||||
'gfw_auto_action_at' => 'integer',
|
||||
'enabled' => 'boolean',
|
||||
'created_at' => 'timestamp',
|
||||
'updated_at' => 'timestamp',
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\Server;
|
||||
use App\Models\ServerGfwCheck;
|
||||
|
||||
class ServerAutoOnlineService
|
||||
{
|
||||
@@ -11,6 +12,7 @@ class ServerAutoOnlineService
|
||||
$servers = Server::query()
|
||||
->where('auto_online', true)
|
||||
->get();
|
||||
$gfwStatuses = app(ServerGfwCheckService::class)->getLatestStatusesForServers($servers);
|
||||
|
||||
$result = [
|
||||
'total' => $servers->count(),
|
||||
@@ -21,18 +23,37 @@ class ServerAutoOnlineService
|
||||
];
|
||||
|
||||
foreach ($servers as $server) {
|
||||
$shouldShow = (int) $server->available_status !== Server::STATUS_OFFLINE;
|
||||
$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 ((bool) $server->show === $shouldShow) {
|
||||
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']++;
|
||||
$shouldShow ? $result['shown']++ : $result['hidden']++;
|
||||
if ($wasShown !== $shouldShow) {
|
||||
$shouldShow ? $result['shown']++ : $result['hidden']++;
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
|
||||
@@ -13,7 +13,7 @@ class ServerGfwCheckService
|
||||
ServerGfwCheck::STATUS_CHECKING,
|
||||
];
|
||||
|
||||
public function startChecks(array $ids, ?int $adminUserId = null): array
|
||||
public function startChecks(array $ids, ?int $adminUserId = null, bool $respectAutoSwitch = false): array
|
||||
{
|
||||
$ids = array_values(array_unique(array_filter(array_map('intval', $ids))));
|
||||
$servers = Server::whereIn('id', $ids)->get()->keyBy('id');
|
||||
@@ -37,13 +37,16 @@ class ServerGfwCheckService
|
||||
continue;
|
||||
}
|
||||
|
||||
$check = ServerGfwCheck::create([
|
||||
'server_id' => $server->id,
|
||||
'status' => ServerGfwCheck::STATUS_PENDING,
|
||||
'triggered_by' => $adminUserId,
|
||||
]);
|
||||
if ($respectAutoSwitch && !$this->isGfwCheckEnabled($server)) {
|
||||
$skipped[] = [
|
||||
'id' => $id,
|
||||
'status' => ServerGfwCheck::STATUS_SKIPPED,
|
||||
'reason' => '节点已关闭自动墙检测',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
NodeSyncService::push($server->id, 'gfw.check', $this->formatTask($check));
|
||||
$check = $this->createCheck($server, $adminUserId);
|
||||
$started[] = [
|
||||
'id' => $server->id,
|
||||
'check_id' => $check->id,
|
||||
@@ -58,6 +61,54 @@ class ServerGfwCheckService
|
||||
];
|
||||
}
|
||||
|
||||
public function startAutomaticChecks(?int $limit = null): array
|
||||
{
|
||||
$query = Server::query()
|
||||
->whereNull('parent_id')
|
||||
->where('gfw_check_enabled', true)
|
||||
->orderBy('sort', 'ASC')
|
||||
->orderBy('id', 'ASC');
|
||||
|
||||
if ($limit !== null && $limit > 0) {
|
||||
$query->limit($limit);
|
||||
}
|
||||
|
||||
$servers = $query->get();
|
||||
$activeServerIds = ServerGfwCheck::whereIn('server_id', $servers->pluck('id'))
|
||||
->whereIn('status', self::TASK_STATUS)
|
||||
->pluck('server_id')
|
||||
->map(fn ($id) => (int) $id)
|
||||
->all();
|
||||
$activeLookup = array_flip($activeServerIds);
|
||||
$started = [];
|
||||
$skipped = [];
|
||||
|
||||
foreach ($servers as $server) {
|
||||
if (isset($activeLookup[(int) $server->id])) {
|
||||
$skipped[] = [
|
||||
'id' => (int) $server->id,
|
||||
'status' => ServerGfwCheck::STATUS_SKIPPED,
|
||||
'reason' => '已有检测任务等待上报',
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$check = $this->createCheck($server, null);
|
||||
$started[] = [
|
||||
'id' => (int) $server->id,
|
||||
'check_id' => (int) $check->id,
|
||||
'status' => $check->status,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'started' => $started,
|
||||
'skipped' => $skipped,
|
||||
'total' => $servers->count(),
|
||||
'active' => count($activeServerIds),
|
||||
];
|
||||
}
|
||||
|
||||
public function decorateServers(Collection $servers): Collection
|
||||
{
|
||||
$sourceIds = $servers
|
||||
@@ -65,11 +116,7 @@ class ServerGfwCheckService
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
$latestChecks = ServerGfwCheck::whereIn('server_id', $sourceIds)
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->groupBy('server_id')
|
||||
->map(fn (Collection $items) => $items->first());
|
||||
$latestChecks = $this->latestChecksByServerIds($sourceIds);
|
||||
|
||||
return $servers->map(function (Server $server) use ($latestChecks) {
|
||||
$sourceNodeId = (int) ($server->parent_id ?: $server->id);
|
||||
@@ -79,6 +126,43 @@ class ServerGfwCheckService
|
||||
});
|
||||
}
|
||||
|
||||
public function getBlockedSourceIdsForServers(Collection $servers): array
|
||||
{
|
||||
return collect($this->getLatestStatusesForServers($servers))
|
||||
->filter(fn (string $status) => $status === ServerGfwCheck::STATUS_BLOCKED)
|
||||
->keys()
|
||||
->map(fn ($id) => (int) $id)
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
public function getLatestStatusesForServers(Collection $servers): array
|
||||
{
|
||||
$sourceIds = $servers
|
||||
->map(fn (Server $server) => (int) ($server->parent_id ?: $server->id))
|
||||
->filter()
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
if ($sourceIds->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$enabledSourceIds = Server::whereIn('id', $sourceIds)
|
||||
->where('gfw_check_enabled', true)
|
||||
->pluck('id')
|
||||
->map(fn ($id) => (int) $id)
|
||||
->values();
|
||||
|
||||
if ($enabledSourceIds->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $this->latestChecksByServerIds($enabledSourceIds)
|
||||
->map(fn (ServerGfwCheck $check) => $check->status)
|
||||
->all();
|
||||
}
|
||||
|
||||
public function getPendingTaskForNode(Server $node): ?array
|
||||
{
|
||||
if ($node->parent_id) {
|
||||
@@ -134,9 +218,24 @@ class ServerGfwCheckService
|
||||
'checked_at' => time(),
|
||||
]);
|
||||
|
||||
$this->syncVisibilityFromStatus($node, $status);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function createCheck(Server $server, ?int $adminUserId): ServerGfwCheck
|
||||
{
|
||||
$check = ServerGfwCheck::create([
|
||||
'server_id' => $server->id,
|
||||
'status' => ServerGfwCheck::STATUS_PENDING,
|
||||
'triggered_by' => $adminUserId,
|
||||
]);
|
||||
|
||||
NodeSyncService::push($server->id, 'gfw.check', $this->formatTask($check));
|
||||
|
||||
return $check;
|
||||
}
|
||||
|
||||
private function formatTask(ServerGfwCheck $check): array
|
||||
{
|
||||
return [
|
||||
@@ -171,6 +270,85 @@ class ServerGfwCheckService
|
||||
];
|
||||
}
|
||||
|
||||
private function latestChecksByServerIds($sourceIds): Collection
|
||||
{
|
||||
$ids = collect($sourceIds)
|
||||
->map(fn ($id) => (int) $id)
|
||||
->filter()
|
||||
->unique()
|
||||
->values();
|
||||
|
||||
if ($ids->isEmpty()) {
|
||||
return collect();
|
||||
}
|
||||
|
||||
return ServerGfwCheck::whereIn('server_id', $ids)
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->groupBy('server_id')
|
||||
->map(fn (Collection $items) => $items->first());
|
||||
}
|
||||
|
||||
private function syncVisibilityFromStatus(Server $sourceNode, string $status): array
|
||||
{
|
||||
if (!in_array($status, [ServerGfwCheck::STATUS_BLOCKED, ServerGfwCheck::STATUS_NORMAL], true)) {
|
||||
return ['shown' => 0, 'hidden' => 0, 'unchanged' => 0];
|
||||
}
|
||||
|
||||
if (!$this->isGfwCheckEnabled($sourceNode)) {
|
||||
return ['shown' => 0, 'hidden' => 0, 'unchanged' => 1];
|
||||
}
|
||||
|
||||
$nodes = Server::query()
|
||||
->where('id', $sourceNode->id)
|
||||
->orWhere('parent_id', $sourceNode->id)
|
||||
->get();
|
||||
|
||||
$result = ['shown' => 0, 'hidden' => 0, 'unchanged' => 0];
|
||||
$now = time();
|
||||
|
||||
foreach ($nodes as $node) {
|
||||
if (!$this->isGfwCheckEnabled($node)) {
|
||||
$result['unchanged']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($status === ServerGfwCheck::STATUS_BLOCKED) {
|
||||
if (!(bool) $node->show) {
|
||||
$result['unchanged']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$node->update([
|
||||
'show' => false,
|
||||
'gfw_auto_hidden' => true,
|
||||
'gfw_auto_action_at' => $now,
|
||||
]);
|
||||
$result['hidden']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(bool) $node->gfw_auto_hidden) {
|
||||
$result['unchanged']++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$node->update([
|
||||
'show' => true,
|
||||
'gfw_auto_hidden' => false,
|
||||
'gfw_auto_action_at' => $now,
|
||||
]);
|
||||
$result['shown']++;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function isGfwCheckEnabled(Server $server): bool
|
||||
{
|
||||
return (bool) ($server->gfw_check_enabled ?? true);
|
||||
}
|
||||
|
||||
private function determineStatus(?array $operators, string $reportedStatus, string $errorMessage): string
|
||||
{
|
||||
if ($errorMessage !== '') {
|
||||
|
||||
Reference in New Issue
Block a user