feat(gateway): 网关健康状态——后台定时探活只保留最近一次结果并在列表展示

- 迁移 1043:gateway 表加 health_status/last_check_reason/last_checked_at
- Store.RecordGatewayCheck 覆盖写最近一次探活结果,Create/Update/List/Get 全量返回健康字段
- api.RecordGatewayHealthChecks 并发探 /healthz:连接失败或 5xx 判离线,其余判在线(401 等异常写入原因供排查)
- workers 每 30 秒一轮,app 启动接线
- 网关管理列表新增健康状态列:在线/离线/未检查 Tag,原因与检查时间收进 Tooltip
This commit is contained in:
2026-09-29 14:47:49 +08:00
parent 09a039eed2
commit a94bdf7921
12 changed files with 408 additions and 42 deletions
+23 -2
View File
@@ -1,15 +1,30 @@
// 网关管理:语义对齐 web.archived GatewaysPage.jsx(注册/编辑 Modal、令牌显隐复制、删除)。
import { useCallback, useEffect, useState } from 'react';
import { Alert, App, Button, Card, Flex, Form, Input, Modal, Popconfirm, Space, Table, Typography } from 'antd';
import { Alert, App, Button, Card, Flex, Form, Input, Modal, Popconfirm, Space, Table, Tag, Tooltip, Typography } from 'antd';
import { CopyOutlined, EyeInvisibleOutlined, EyeOutlined, PlusOutlined } from '@ant-design/icons';
import type { ColumnsType } from 'antd/es/table';
import { create, getList, remove, update } from '@/services/api';
import { conflictMessage } from '@/utils/helpers';
import { conflictMessage, dateTime } from '@/utils/helpers';
import { fixedLeft, fixedRight, tablePagination, tableScroll, useOverflowGrid } from '@/utils/table';
const namePattern = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
const tokenPattern = /^[A-Za-z0-9][A-Za-z0-9._-]{15,127}$/;
// 健康状态列:只展示后台定时探活的最近一次结果,不保留历史。
function HealthCell({ status, reason, checkedAt }: { status?: string; reason?: string; checkedAt?: string }) {
const label = status === 'healthy' ? '在线' : status === 'unhealthy' ? '离线' : '未检查';
const color = status === 'healthy' ? 'success' : status === 'unhealthy' ? 'error' : 'default';
const details = [
checkedAt ? `最近检查:${dateTime(checkedAt)}` : null,
reason || null,
].filter(Boolean).join(';');
return (
<Tooltip title={details || '尚未执行健康检查'}>
<Tag color={color}>{label}</Tag>
</Tooltip>
);
}
function GatewayFormModal({
open,
initial,
@@ -146,6 +161,12 @@ export default function Page() {
const columns: ColumnsType<any> = [
{ title: '名称', dataIndex: 'name', ...fixedLeft<any>({}), render: (value: string) => <Typography.Text strong>{value}</Typography.Text> },
{ title: 'Endpoint', dataIndex: 'endpoint', render: (value: string) => <Typography.Text code style={{ fontSize: 12 }} ellipsis>{value}</Typography.Text> },
{
title: '健康状态',
dataIndex: 'health_status',
width: 110,
render: (_, gateway) => <HealthCell status={gateway.health_status} reason={gateway.last_check_reason} checkedAt={gateway.last_checked_at} />,
},
{
title: '令牌',
dataIndex: 'token',