feat(api): 新增节点墙检测自动托管与显隐

新增定时墙检测命令与节点托管字段,自动为开启托管的父
节点创建检测任务,并在 blocked 时自动隐藏节点、normal
时仅恢复由墙检测自动隐藏的节点

更新自动上线服务以尊重 blocked 与自动隐藏状态,避免疑
似被墙节点被重新发布;同时补齐管理端墙检测托管开关、
刷新入口、批量设置与相关测试和知识库同步
This commit is contained in:
yinjianm
2026-04-28 00:51:49 +08:00
parent 73b1696b0a
commit ff50030364
27 changed files with 998 additions and 24 deletions
+190 -12
View File
@@ -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 !== '') {