From a1237587f63c80bf295723151cbff9be0ade2d4b Mon Sep 17 00:00:00 2001 From: attacker Date: Sun, 14 Jun 2026 05:11:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9C=8D=E5=8A=A1=E5=99=A8=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=89=8D=E5=90=8E=E7=AB=AF=E5=AD=97=E6=AE=B5=E5=AF=B9?= =?UTF-8?q?=E9=BD=90=20=E2=80=94=20=E7=A7=BB=E9=99=A4CMDB=E5=B9=BD?= =?UTF-8?q?=E7=81=B5=E5=AD=97=E6=AE=B5=20+=20SSH=E5=AF=BC=E5=90=91?= =?UTF-8?q?=E8=A1=A8=E5=8D=95=20+=20GB=E2=86=92MB=E5=86=85=E5=AD=98?= =?UTF-8?q?=E8=BD=AC=E6=8D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/api/handlers/server.go | 4 + backend/internal/models/server.go | 1 + docs/dev-log.md | 24 ++ .../src/components/server/ServerManager.tsx | 304 +++++++----------- 4 files changed, 146 insertions(+), 187 deletions(-) diff --git a/backend/internal/api/handlers/server.go b/backend/internal/api/handlers/server.go index a1e8324..e704777 100644 --- a/backend/internal/api/handlers/server.go +++ b/backend/internal/api/handlers/server.go @@ -148,6 +148,10 @@ func (h *ServerHandler) CreateServer(c *gin.Context) { Status: "stopped", } + if req.Status != "" { + server.Status = req.Status + } + if server.Port == 0 { server.Port = 22 } diff --git a/backend/internal/models/server.go b/backend/internal/models/server.go index 005bcc5..66b6cca 100644 --- a/backend/internal/models/server.go +++ b/backend/internal/models/server.go @@ -144,6 +144,7 @@ type CreateServerRequest struct { CPU int `json:"cpu"` Memory int64 `json:"memory"` Disk int64 `json:"disk"` + Status string `json:"status" binding:"omitempty,oneof=running stopped maintenance error"` RegionID *uint `json:"region_id"` CloudID *uint `json:"cloud_id"` Tags string `json:"tags"` diff --git a/docs/dev-log.md b/docs/dev-log.md index 11837fc..c4605be 100644 --- a/docs/dev-log.md +++ b/docs/dev-log.md @@ -129,3 +129,27 @@ cd ~/devops/aiops - 成本统计从硬编码改为基于实际云账号数据计算 - K8s 集群拓扑图(TopologyView)前后端数据联调 - 告警"通知配置"按钮功能实现(通知渠道管理弹窗) + +## 2026-06-14 05:15 自动推进 + +- **任务**: 服务器管理模块前后端字段对齐 — 修复前端表单使用后端不支持的CMDB幽灵字段导致数据丢失 + 内存单位 GB→MB 转换 + 表格列重构 +- **修改**: + - `backend/internal/models/server.go` — CreateServerRequest 添加 Status 字段(原硬编码 "stopped") + - `backend/internal/api/handlers/server.go` — CreateServer handler 改用 req.Status 替代硬编码值 + - `frontend/src/components/server/ServerManager.tsx` — 完全重写表单/表格/过滤器: + - 移除后端不支持的幽灵字段:asset_type/connect_type/floatingip/instance_id/expire_time/remark/region/region_id + - 表单改为SSH连接导向:主机名/IP/公网IP/SSH端口/登录用户/认证方式(密码/密钥)/密码 + 可折叠详情区(OS/状态/CPU/内存/磁盘/标签/描述) + - 内存字段前端输入GB,提交时 ×1024 转换为MB(后端存储MB) + - 表格列从11列精简到8列,新增公网IP列,移除资产类型/连接方式/区域列 + - 移除不存在的区域筛选器(servers 无 region_id 数据) + - 移除未实现的"查看详情"下拉项(无详情页) +- **结果**: + - `go build -o aiops-server cmd/main.go` 编译通过 ✅ + - `npx tsc --noEmit` 类型检查通过 ✅ + - `npm run build` 构建通过 ✅ + - API端到端验证: POST /servers → Code 200,hostname/ip/public_ip/port/username/auth_type/memory(16384MB=16GB)/status/tags 全部正确保存 ✅ + - API端到端验证: PUT /servers/:id → Code 200,更新成功 ✅ + - API端到端验证: DELETE /servers/:id → Code 200 "删除成功" ✅ + - 浏览器UI验证: 服务器列表页正常渲染,内存显示为GB(128GB而非131072MB),公网IP列正确显示 ✅ + - 表单UI验证: 添加服务器弹窗字段正确,验证逻辑正常(hostname+ip必填时按钮启用)✅ + - 服务运行中: 后端:8080 + 前端:5173 ✅ diff --git a/frontend/src/components/server/ServerManager.tsx b/frontend/src/components/server/ServerManager.tsx index 97bae58..2dcd561 100644 --- a/frontend/src/components/server/ServerManager.tsx +++ b/frontend/src/components/server/ServerManager.tsx @@ -12,7 +12,6 @@ import { CollapsibleContent, CollapsibleTrigger, } from '@/components/ui/collapsible'; -import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { DropdownMenu, DropdownMenuContent, @@ -53,7 +52,6 @@ import { RotateCw, Trash2, Edit, - Eye, Download, Upload, Server, @@ -70,59 +68,39 @@ import type { Server as ServerType } from '@/types'; import { CloudServerImport } from './CloudServerImport'; import { ExcelServerImport } from './ExcelServerImport'; -// 资产类型字典(参考 CMDB 运维平台) -const ASSET_TYPE_OPTIONS = [ - { value: 'physical', label: '物理机', color: 'var(--dg-orange)' }, - { value: 'virtual', label: '虚拟机', color: 'var(--dg-blue)' }, - { value: 'cloud', label: '云主机', color: 'var(--dg-cyan)' }, - { value: 'container', label: '容器', color: 'var(--dg-purple)' }, -]; - -const getAssetTypeLabel = (value: string): string => - ASSET_TYPE_OPTIONS.find((o) => o.value === value)?.label || value || '-'; - -const getAssetTypeColor = (value: string): string => - ASSET_TYPE_OPTIONS.find((o) => o.value === value)?.color || 'var(--dg-text-secondary)'; - -const getConnectTypeLabel = (value: string): string => { - if (value === 'public') return '公网'; - if (value === 'private') return '内网'; - return value || '-'; -}; - -// 表单数据结构 +// 表单数据结构 — 与后端 CreateServerRequest/UpdateServerRequest 字段对齐 interface ServerFormData { hostname: string; ip: string; + public_ip: string; + port: string; + username: string; + auth_type: string; // password | key + password: string; os: string; cpu: string; - memory: string; + memory: string; // GB (表单) → MB (后端) disk: string; status: string; - region: string; - asset_type: string; - connect_type: string; - floatingip: string; - instance_id: string; - expire_time: string; - remark: string; + tags: string; + description: string; } const EMPTY_FORM: ServerFormData = { hostname: '', ip: '', + public_ip: '', + port: '22', + username: 'root', + auth_type: 'password', + password: '', os: '', cpu: '', memory: '', disk: '', status: 'running', - region: '', - asset_type: 'physical', - connect_type: 'private', - floatingip: '', - instance_id: '', - expire_time: '', - remark: '', + tags: '', + description: '', }; export function ServerManager() { @@ -131,7 +109,6 @@ export function ServerManager() { const [selectedServers, setSelectedServers] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); - const [regionFilter, setRegionFilter] = useState('all'); const [cloudImportOpen, setCloudImportOpen] = useState(false); const [excelImportOpen, setExcelImportOpen] = useState(false); @@ -175,21 +152,13 @@ export function ServerManager() { } }; - // 获取区域名称:后端 region 可能是对象 {name} 或字符串 - const getRegionName = (server: any): string => { - if (!server.region) return ''; - if (typeof server.region === 'string') return server.region; - return server.region.name || ''; - }; - const filteredServers = servers.filter((server) => { - const region = getRegionName(server); const matchesSearch = (server.hostname || '').toLowerCase().includes(searchQuery.toLowerCase()) || - (server.ip || '').includes(searchQuery); + (server.ip || '').includes(searchQuery) || + (server.public_ip || '').includes(searchQuery); const matchesStatus = statusFilter === 'all' || server.status === statusFilter; - const matchesRegion = regionFilter === 'all' || region === regionFilter; - return matchesSearch && matchesStatus && matchesRegion; + return matchesSearch && matchesStatus; }); const toggleSelectAll = () => { @@ -243,18 +212,18 @@ export function ServerManager() { setFormData({ hostname: server.hostname || '', ip: server.ip || '', + public_ip: server.public_ip || '', + port: String(server.port || 22), + username: server.username || 'root', + auth_type: server.auth_type || 'password', + password: '', os: server.os || '', cpu: String(server.cpu || ''), - memory: String(server.memory || ''), + memory: server.memory ? String(Math.round(server.memory / 1024)) : '', // MB → GB disk: String(server.disk || ''), status: server.status || 'running', - region: getRegionName(server), - asset_type: server.asset_type || 'physical', - connect_type: server.connect_type || 'private', - floatingip: server.floatingip || '', - instance_id: server.instance_id || '', - expire_time: server.expire_time || '', - remark: server.remark || '', + tags: server.tags || '', + description: server.description || '', }); setDetailsOpen(false); setFormOpen(true); @@ -265,12 +234,25 @@ export function ServerManager() { if (!formData.hostname || !formData.ip) return; try { setFormSaving(true); - const payload = { - ...formData, + const payload: Record = { + hostname: formData.hostname, + ip: formData.ip, + public_ip: formData.public_ip || undefined, + port: Number(formData.port) || 22, + username: formData.username || 'root', + auth_type: formData.auth_type || 'password', + os: formData.os, cpu: Number(formData.cpu) || 0, - memory: Number(formData.memory) || 0, + memory: (Number(formData.memory) || 0) * 1024, // GB → MB disk: Number(formData.disk) || 0, + status: formData.status, + tags: formData.tags || undefined, + description: formData.description || undefined, }; + // 密码仅在填写时提交 + if (formData.password) { + payload.password = formData.password; + } if (editingId) { await serverApi.updateServer(editingId, payload); } else { @@ -318,9 +300,7 @@ export function ServerManager() { } }; - const regions = Array.from(new Set(servers.map((s) => getRegionName(s)).filter(Boolean))); - - const colSpanCount = 11; + const colSpanCount = 8; return (
@@ -470,19 +450,6 @@ export function ServerManager() { 离线 -
{selectedServers.length > 0 && (
@@ -523,12 +490,10 @@ export function ServerManager() { 主机名 IP地址 - 资产类型 - 连接方式 + 公网IP 状态 配置 使用率 - 区域 标签 操作 @@ -552,7 +517,7 @@ export function ServerManager() { const cpuUsage = Number(server.cpu_usage || 0); const memoryUsage = Number(server.memory_usage || 0); const tags = getTags(server); - const region = getRegionName(server); + const memGB = server.memory ? Math.round(server.memory / 1024) : 0; return ( @@ -576,35 +541,21 @@ export function ServerManager() { {server.ip} - {/* 资产类型 */} - - {getAssetTypeLabel(server.asset_type)} - - - {/* 连接方式 */} - - - {getConnectTypeLabel(server.connect_type)} - + {server.public_ip ? ( + + {server.public_ip} + + ) : ( + - + )} {getStatusBadge(server.status)}
{server.cpu}核 / - {server.memory}GB + {memGB}GB / {server.disk}GB
@@ -639,9 +590,6 @@ export function ServerManager() {
- - {region || '-'} -
{tags.slice(0, 3).map((tag) => ( @@ -674,10 +622,6 @@ export function ServerManager() { 操作 - - - 查看详情 - openEditForm(server)} @@ -755,91 +699,68 @@ export function ServerManager() {
- {/* CMDB 资产字段 */} + {/* SSH 连接配置 */}
- + + setFormData({ ...formData, public_ip: e.target.value })} + className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]" + /> +
+
+ + setFormData({ ...formData, port: e.target.value })} + className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]" + /> +
+
+ +
+
+ + setFormData({ ...formData, username: e.target.value })} + className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]" + /> +
+
+
-
- - setFormData({ ...formData, connect_type: v })} - className="flex items-center gap-6 h-9" - > -
- - -
-
- - -
-
-
- {/* 云主机扩展字段 */} -
-
- - setFormData({ ...formData, floatingip: e.target.value })} - className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]" - /> -
-
- - setFormData({ ...formData, instance_id: e.target.value })} - className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]" - /> -
-
- -
-
- - setFormData({ ...formData, expire_time: e.target.value })} - className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)]" - /> -
-
- - setFormData({ ...formData, region: e.target.value })} - className="bg-[var(--dg-bg-tertiary)] border-[var(--dg-border)] text-[var(--dg-text-primary)] placeholder:text-[var(--dg-text-muted)]" - /> -
+
+ +