diff --git a/backend/internal/api/handlers/server.go b/backend/internal/api/handlers/server.go index e704777..ca699d4 100644 --- a/backend/internal/api/handlers/server.go +++ b/backend/internal/api/handlers/server.go @@ -3,6 +3,7 @@ package handlers import ( "net/http" "strconv" + "strings" "ezops/internal/models" @@ -401,6 +402,16 @@ func (h *CloudAccountHandler) ListCloudAccounts(c *gin.Context) { }) } +// createCloudAccountRequest 创建云账号请求(独立 request struct,避免 CloudAccount.AccessKey/SecretKey 的 json:"-" 导致无法反序列化) +type createCloudAccountRequest struct { + Name string `json:"name"` + Provider string `json:"provider"` + AccessKey string `json:"access_key"` + SecretKey string `json:"secret_key"` + MonthlyCost float64 `json:"monthly_cost"` + Status string `json:"status"` +} + // CreateCloudAccount 创建云账号 // @Summary 创建云账号 // @Description 创建新云账号 @@ -408,12 +419,12 @@ func (h *CloudAccountHandler) ListCloudAccounts(c *gin.Context) { // @Accept json // @Produce json // @Security Bearer -// @Param request body models.CloudAccount true "创建请求" +// @Param request body createCloudAccountRequest true "创建请求" // @Success 200 {object} models.Response{data=models.CloudAccount} // @Router /cloud-accounts [post] func (h *CloudAccountHandler) CreateCloudAccount(c *gin.Context) { - var account models.CloudAccount - if err := c.ShouldBindJSON(&account); err != nil { + var req createCloudAccountRequest + if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, models.Response{ Code: 400, Message: "请求参数错误: " + err.Error(), @@ -421,6 +432,28 @@ func (h *CloudAccountHandler) CreateCloudAccount(c *gin.Context) { return } + if strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.Provider) == "" { + c.JSON(http.StatusBadRequest, models.Response{ + Code: 400, + Message: "账号名称和云平台不能为空", + }) + return + } + + status := strings.TrimSpace(req.Status) + if status == "" { + status = "active" + } + + account := models.CloudAccount{ + Name: strings.TrimSpace(req.Name), + Provider: strings.TrimSpace(req.Provider), + AccessKey: req.AccessKey, + SecretKey: req.SecretKey, + MonthlyCost: req.MonthlyCost, + Status: status, + } + if err := h.db.Create(&account).Error; err != nil { c.JSON(http.StatusInternalServerError, models.Response{ Code: 500, diff --git a/docs/dev-log.md b/docs/dev-log.md index 23de7fa..a2d6ed6 100644 --- a/docs/dev-log.md +++ b/docs/dev-log.md @@ -171,7 +171,23 @@ cd ~/devops/aiops - 浏览器验证: 仪表盘「本月云成本」显示 ¥95,200(真实计算值)✅ - 健康检查 /health 正常 ✅ - **下一步建议**: - - 云账号「添加账号」按钮弹窗表单实现(当前点击无响应,无法录入月费用) + - ~~云账号「添加账号」按钮弹窗表单实现(当前点击无响应,无法录入月费用)~~ ✅ 已完成 - K8s 集群拓扑图(TopologyView)前后端数据联调 - 告警「通知配置」按钮功能实现(通知渠道管理弹窗) - 仪表盘「+12.5%」等增长率标签从硬编码改为基于 month_trend 真实计算 + - 云账号「编辑」功能(需后端新增 PUT /cloud-accounts/:id 端点) + +## 2026-06-14 07:10 自动推进 + +- **任务**: 云账号创建表单实现 — 修复后端创建接口无法接收凭证的 bug + 前端添加账号弹窗表单 +- **修改**: + - `backend/internal/api/handlers/server.go` — 重构 `CreateCloudAccount`:新增独立的 `createCloudAccountRequest` struct(遵循 Go Pitfall #8)。原代码直接绑定 `models.CloudAccount`,但 AccessKey/SecretKey 字段标记 `json:"-"` 会导致反序列化时被跳过,凭证永远为空。现用独立 request struct 接收后映射到 model;新增 name/provider 非空校验,status 默认 active;补充 `strings` 导入 + - `frontend/src/components/cloud/CloudAccountManager.tsx` — 「添加账号」按钮接入创建表单 Dialog(账号名称/云平台/AccessKey/SecretKey/月费用/状态),提交调用 cloudApi.createAccount 后刷新列表;移除无 handler 的刷新/编辑按钮(编辑需后端新增 PUT 端点,留待后续);按钮 disabled 校验(名称必填) +- **结果**: + - `go build ./cmd/` 编译通过 ✅ + - `npx tsc --noEmit` 类型检查通过 ✅ + - `npm run build` 构建通过 ✅ + - API 验证: POST /cloud-accounts → Code 200,DB 验证 access_key/secret_key 正确持久化(原 bug 下为空字符串)✅ + - 联动验证: 新增账号后 dashboard month_cost 实时更新(95200→100200)✅ + - 浏览器 UI 验证: 添加账号弹窗正常打开、字段填写、提交后列表刷新显示新账号 ✅ + - 服务运行中: 后端:8080 + 前端:5173 ✅ diff --git a/frontend/src/components/cloud/CloudAccountManager.tsx b/frontend/src/components/cloud/CloudAccountManager.tsx index 16d3c12..dbdefc2 100644 --- a/frontend/src/components/cloud/CloudAccountManager.tsx +++ b/frontend/src/components/cloud/CloudAccountManager.tsx @@ -1,18 +1,27 @@ import { useState, useEffect } from 'react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Label } from '@/components/ui/label'; +import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; import { Plus, Search, - Edit2, Trash2, Key, Cloud, CheckCircle2, XCircle, - RefreshCw + RefreshCw, + Loader2, } from 'lucide-react'; import { cloudApi } from '@/services/api'; @@ -38,6 +47,55 @@ export function CloudAccountManager() { const [searchQuery, setSearchQuery] = useState(''); const [selectedProvider, setSelectedProvider] = useState('all'); + // 创建账号表单状态 + const [formOpen, setFormOpen] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [formData, setFormData] = useState({ + name: '', + provider: 'aliyun', + access_key: '', + secret_key: '', + monthly_cost: '', + status: 'active', + }); + + const resetForm = () => { + setFormData({ + name: '', + provider: 'aliyun', + access_key: '', + secret_key: '', + monthly_cost: '', + status: 'active', + }); + }; + + const handleOpenCreate = () => { + resetForm(); + setFormOpen(true); + }; + + const handleCreate = async () => { + if (!formData.name.trim() || !formData.provider.trim()) return; + try { + setSubmitting(true); + await cloudApi.createAccount({ + name: formData.name.trim(), + provider: formData.provider, + access_key: formData.access_key.trim(), + secret_key: formData.secret_key.trim(), + monthly_cost: Number(formData.monthly_cost) || 0, + status: formData.status, + }); + setFormOpen(false); + loadData(); + } catch (e) { + console.error('创建云账号失败:', e); + } finally { + setSubmitting(false); + } + }; + const loadData = async () => { try { setLoading(true); @@ -92,7 +150,7 @@ export function CloudAccountManager() { 刷新 - @@ -239,12 +297,6 @@ export function CloudAccountManager() {
- - + + + +
); }