diff --git a/.gosec.json b/.gosec.json deleted file mode 100644 index adfb1458..00000000 --- a/.gosec.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "global": { - "audit": true, - "nosec": true, - "confidence": "medium", - "severity": "medium", - "include": [], - "exclude": [ - "G101", "G102", "G103", "G104", "G106", "G107", - "G201", "G202", "G203", "G204", - "G301", "G302", "G303", "G304", "G305", "G306", "G307", - "G401", "G402", "G403", "G404", "G501", "G502", "G503", "G504", "G505", "G601" - ], - "exclude-dir": [ - "vendor", - "test", - "tests", - "mock", - "mocks", - "docs" - ], - "exclude-generated": true - }, - "rules": { - "G104": { - "audit": false - }, - "G204": { - "audit": false - }, - "G304": { - "audit": false - } - } -} diff --git a/internal/model/tree_account_region.go b/internal/model/tree_account_region.go deleted file mode 100644 index f99ef52c..00000000 --- a/internal/model/tree_account_region.go +++ /dev/null @@ -1,128 +0,0 @@ -/* - * MIT License - * - * Copyright (c) 2024 Bamboo - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - */ - -package model - -import "time" - -// CloudAccountRegionStatus 云账号区域状态 -type CloudAccountRegionStatus int8 - -const ( - CloudAccountRegionEnabled CloudAccountRegionStatus = iota + 1 // 启用 - CloudAccountRegionDisabled // 禁用 -) - -// CloudAccountRegion 云账号区域关联表 -type CloudAccountRegion struct { - Model - CloudAccountID int `json:"cloud_account_id" gorm:"not null;comment:云账户ID;index:idx_account_region,unique"` - CloudAccount *CloudAccount `json:"cloud_account,omitempty" gorm:"foreignKey:CloudAccountID"` - Region string `json:"region" gorm:"type:varchar(50);not null;comment:区域,如cn-hangzhou;index:idx_account_region,unique"` - RegionName string `json:"region_name" gorm:"type:varchar(100);comment:区域名称,如华东1(杭州)"` - Status CloudAccountRegionStatus `json:"status" gorm:"type:tinyint(1);not null;comment:区域状态,1:启用,2:禁用;default:1"` - IsDefault bool `json:"is_default" gorm:"comment:是否为默认区域;default:false"` - Description string `json:"description" gorm:"type:text;comment:区域描述"` - LastSyncTime *time.Time `json:"last_sync_time" gorm:"type:datetime;comment:最后同步时间"` - CreateUserID int `json:"create_user_id" gorm:"comment:创建者ID;default:0"` - CreateUserName string `json:"create_user_name" gorm:"type:varchar(100);comment:创建者姓名"` -} - -func (c *CloudAccountRegion) TableName() string { - return "cl_cloud_account_region" -} - -// GetCloudAccountRegionListReq 获取云账号区域列表请求 -type GetCloudAccountRegionListReq struct { - ListReq - CloudAccountID int `json:"cloud_account_id" form:"cloud_account_id" binding:"omitempty,gt=0"` - Region string `json:"region" form:"region"` - Status CloudAccountRegionStatus `json:"status" form:"status" binding:"omitempty,oneof=1 2"` -} - -// CreateCloudAccountRegionReq 创建云账号区域关联请求 -type CreateCloudAccountRegionReq struct { - CloudAccountID int `json:"cloud_account_id" binding:"required,gt=0"` - Region string `json:"region" binding:"required"` - RegionName string `json:"region_name"` - IsDefault bool `json:"is_default"` - Description string `json:"description"` - CreateUserID int `json:"create_user_id"` - CreateUserName string `json:"create_user_name"` -} - -// UpdateCloudAccountRegionReq 更新云账号区域关联请求 -type UpdateCloudAccountRegionReq struct { - ID int `json:"id" binding:"required,gt=0"` - RegionName string `json:"region_name"` - IsDefault bool `json:"is_default"` - Description string `json:"description"` -} - -// DeleteCloudAccountRegionReq 删除云账号区域关联请求 -type DeleteCloudAccountRegionReq struct { - ID int `json:"id" binding:"required,gt=0"` -} - -// UpdateCloudAccountRegionStatusReq 更新云账号区域状态请求 -type UpdateCloudAccountRegionStatusReq struct { - ID int `json:"id" binding:"required,gt=0"` - Status CloudAccountRegionStatus `json:"status" binding:"required,oneof=1 2"` -} - -// BatchCreateCloudAccountRegionReq 批量创建云账号区域关联请求 -type BatchCreateCloudAccountRegionReq struct { - CloudAccountID int `json:"cloud_account_id" binding:"required,gt=0"` - Regions []CreateCloudAccountRegionItem `json:"regions" binding:"required,min=1"` - CreateUserID int `json:"create_user_id"` - CreateUserName string `json:"create_user_name"` -} - -// CreateCloudAccountRegionItem 创建云账号区域项 -type CreateCloudAccountRegionItem struct { - Region string `json:"region" binding:"required"` - RegionName string `json:"region_name"` - IsDefault bool `json:"is_default"` - Description string `json:"description"` -} - -// GetAvailableRegionsReq 获取可用区域列表请求 -type GetAvailableRegionsReq struct { - Provider CloudProvider `json:"provider" form:"provider" binding:"required,oneof=1 2 3 4 5 6"` - AccessKey string `json:"access_key" form:"access_key"` // 可选,提供时会通过API动态获取 - SecretKey string `json:"secret_key" form:"secret_key"` // 可选,提供时会通过API动态获取 -} - -// AvailableRegion 可用区域信息 -type AvailableRegion struct { - Region string `json:"region"` // 区域代码,如cn-hangzhou - RegionName string `json:"region_name"` // 区域名称,如华东1(杭州) - Available bool `json:"available"` // 是否可用 -} - -// GetAvailableRegionsResp 获取可用区域列表响应 -type GetAvailableRegionsResp struct { - Regions []AvailableRegion `json:"regions"` -} diff --git a/internal/model/tree_cloud.go b/internal/model/tree_cloud.go index c59904aa..73d325c9 100644 --- a/internal/model/tree_cloud.go +++ b/internal/model/tree_cloud.go @@ -27,7 +27,7 @@ package model import "time" -// 变更类型常量 +// ChangeType 变更类型常量 const ( ChangeTypeCreated = "created" // 创建 ChangeTypeUpdated = "updated" // 更新 @@ -35,7 +35,7 @@ const ( ChangeTypeStatusChanged = "status_changed" // 状态变更 ) -// 变更来源常量 +// ChangeSource 变更来源常量 const ( ChangeSourceManual = "manual" // 手动操作 ChangeSourceSync = "sync" // 同步操作 @@ -53,26 +53,6 @@ const ( ProviderGCP // Google Cloud ) -// String 返回云厂商的字符串表示(用于日志和调试) -func (p CloudProvider) String() string { - switch p { - case ProviderAliyun: - return "阿里云" - case ProviderTencent: - return "腾讯云" - case ProviderAWS: - return "AWS" - case ProviderHuawei: - return "华为云" - case ProviderAzure: - return "Azure" - case ProviderGCP: - return "Google Cloud" - default: - return "未知" - } -} - // CloudResourceType 云资源类型 type CloudResourceType int8 @@ -85,26 +65,6 @@ const ( ResourceTypeOther // 其他资源 ) -// String 返回资源类型的字符串表示(用于日志和调试) -func (r CloudResourceType) String() string { - switch r { - case ResourceTypeECS: - return "云服务器" - case ResourceTypeRDS: - return "云数据库" - case ResourceTypeSLB: - return "负载均衡" - case ResourceTypeOSS: - return "对象存储" - case ResourceTypeVPC: - return "虚拟私有云" - case ResourceTypeOther: - return "其他" - default: - return "未知" - } -} - // CloudResourceStatus 云资源状态 type CloudResourceStatus int8 @@ -117,26 +77,6 @@ const ( CloudResourceUnknown // 未知状态 ) -// String 返回资源状态的字符串表示(用于日志和调试) -func (s CloudResourceStatus) String() string { - switch s { - case CloudResourceRunning: - return "运行中" - case CloudResourceStopped: - return "已停止" - case CloudResourceStarting: - return "启动中" - case CloudResourceStopping: - return "停止中" - case CloudResourceDeleted: - return "已删除" - case CloudResourceUnknown: - return "未知" - default: - return "未知" - } -} - // Currency 货币单位 type Currency string @@ -161,34 +101,42 @@ const ( SyncModeIncremental SyncMode = "incremental" // 增量同步 ) +// SyncStatus 同步状态 +type SyncStatus string + +const ( + SyncStatusSuccess SyncStatus = "success" // 成功 + SyncStatusFailed SyncStatus = "failed" // 失败 + SyncStatusPartial SyncStatus = "partial" // 部分成功 +) + // TreeCloudResource 云资源管理 type TreeCloudResource struct { Model - - Name string `json:"name" gorm:"type:varchar(100);not null;comment:资源名称"` - ResourceType CloudResourceType `json:"resource_type" gorm:"type:tinyint(1);not null;comment:资源类型;default:1"` - Status CloudResourceStatus `json:"status" gorm:"type:tinyint(1);not null;comment:资源状态;default:1"` - Environment string `json:"environment" gorm:"type:varchar(50);comment:环境标识(dev/test/prod)"` + Name string `json:"name" gorm:"type:varchar(100);not null;index;comment:资源名称"` + ResourceType CloudResourceType `json:"resource_type" gorm:"type:tinyint(1);not null;index;comment:资源类型;default:1"` + Status CloudResourceStatus `json:"status" gorm:"type:tinyint(1);not null;index;comment:资源状态;default:1"` + Environment string `json:"environment" gorm:"type:varchar(50);index;comment:环境标识(dev/test/prod)"` Description string `json:"description" gorm:"type:text;comment:资源描述"` Tags KeyValueList `json:"tags" gorm:"type:text;serializer:json;comment:资源标签集合"` - CreateUserID int `json:"create_user_id" gorm:"comment:创建者ID;default:0"` + CreateUserID int `json:"create_user_id" gorm:"index;comment:创建者ID;default:0"` CreateUserName string `json:"create_user_name" gorm:"type:varchar(100);comment:创建者姓名"` - CloudAccountID int `json:"cloud_account_id" gorm:"not null;comment:云账户ID"` + CloudAccountID int `json:"cloud_account_id" gorm:"not null;index;comment:云账户ID"` CloudAccount *CloudAccount `json:"cloud_account,omitempty" gorm:"foreignKey:CloudAccountID"` - CloudAccountRegionID int `json:"cloud_account_region_id" gorm:"not null;comment:云账户区域ID"` + CloudAccountRegionID int `json:"cloud_account_region_id" gorm:"not null;index;comment:云账户区域ID"` CloudAccountRegion *CloudAccountRegion `json:"cloud_account_region,omitempty" gorm:"foreignKey:CloudAccountRegionID"` - Region string `json:"region" gorm:"type:varchar(50);comment:区域,如cn-hangzhou(冗余字段,便于查询)"` - InstanceID string `json:"instance_id" gorm:"type:varchar(100);comment:云资源实例ID"` + Region string `json:"region" gorm:"type:varchar(50);index;comment:区域,如cn-hangzhou"` + InstanceID string `json:"instance_id" gorm:"type:varchar(100);uniqueIndex:idx_account_instance;comment:云资源实例ID"` InstanceType string `json:"instance_type" gorm:"type:varchar(100);comment:实例规格(如ecs.g6.large)"` Cpu int `json:"cpu" gorm:"comment:CPU核数;default:0"` Memory int `json:"memory" gorm:"comment:内存大小(GiB);default:0"` Disk int `json:"disk" gorm:"comment:磁盘大小(GiB);default:0"` - PublicIP string `json:"public_ip" gorm:"type:varchar(45);comment:公网IP"` - PrivateIP string `json:"private_ip" gorm:"type:varchar(45);comment:私网IP"` - VpcID string `json:"vpc_id" gorm:"type:varchar(100);comment:VPC ID"` - ZoneID string `json:"zone_id" gorm:"type:varchar(50);comment:可用区ID"` - ChargeType ChargeType `json:"charge_type" gorm:"type:varchar(50);comment:计费方式(PostPaid/PrePaid)"` - ExpireTime *time.Time `json:"expire_time" gorm:"type:datetime;comment:到期时间"` + PublicIP string `json:"public_ip" gorm:"type:varchar(45);index;comment:公网IP"` + PrivateIP string `json:"private_ip" gorm:"type:varchar(45);index;comment:私网IP"` + VpcID string `json:"vpc_id" gorm:"type:varchar(100);index;comment:VPC ID"` + ZoneID string `json:"zone_id" gorm:"type:varchar(50);index;comment:可用区ID"` + ChargeType ChargeType `json:"charge_type" gorm:"type:varchar(50);index;comment:计费方式(PostPaid/PrePaid)"` + ExpireTime *time.Time `json:"expire_time" gorm:"type:datetime;index;comment:到期时间"` MonthlyCost float64 `json:"monthly_cost" gorm:"type:decimal(10,2);comment:月度成本;default:0"` Currency Currency `json:"currency" gorm:"type:varchar(10);not null;comment:货币单位;default:'CNY'"` OSType string `json:"os_type" gorm:"type:varchar(50);comment:操作系统类型(linux/windows)"` @@ -200,7 +148,8 @@ type TreeCloudResource struct { Password string `json:"-" gorm:"type:varchar(500);comment:SSH密码(加密存储)"` Key string `json:"-" gorm:"type:text;comment:SSH密钥"` AuthMode AuthMode `json:"auth_mode" gorm:"type:tinyint(1);comment:SSH认证方式(1:密码,2:密钥);default:1"` - TreeNodes []*TreeNode `json:"tree_nodes" gorm:"many2many:cl_tree_node_cloud"` + LastSyncTime *time.Time `json:"last_sync_time" gorm:"type:datetime;comment:最后同步时间"` + TreeNodes []*TreeNode `json:"tree_nodes,omitempty" gorm:"many2many:cl_tree_node_cloud"` } func (t *TreeCloudResource) TableName() string { @@ -213,7 +162,10 @@ type GetTreeCloudResourceListReq struct { CloudAccountID int `json:"cloud_account_id" form:"cloud_account_id" binding:"omitempty,gt=0"` ResourceType CloudResourceType `json:"resource_type" form:"resource_type" binding:"omitempty,oneof=1 2 3 4 5 6"` Status CloudResourceStatus `json:"status" form:"status" binding:"omitempty,oneof=1 2 3 4 5 6"` - Environment string `json:"environment" form:"environment"` + Environment string `json:"environment" form:"environment" binding:"omitempty,max=50"` + Region string `json:"region" form:"region" binding:"omitempty,max=50"` + InstanceID string `json:"instance_id" form:"instance_id" binding:"omitempty,max=100"` + Keyword string `json:"keyword" form:"keyword" binding:"omitempty,max=100"` // 搜索关键词(名称、IP等) } // GetTreeCloudResourceDetailReq 获取云资源详情请求 @@ -224,56 +176,51 @@ type GetTreeCloudResourceDetailReq struct { // UpdateTreeCloudResourceReq 更新云资源本地元数据请求(不影响云上资源) type UpdateTreeCloudResourceReq struct { ID int `json:"id" binding:"required,gt=0"` - Environment string `json:"environment"` // 环境标识 - Description string `json:"description"` // 资源描述 + Environment string `json:"environment" binding:"omitempty,max=50"` // 环境标识 + Description string `json:"description" binding:"omitempty,max=500"` // 资源描述 Tags KeyValueList `json:"tags"` // 自定义标签 Port int `json:"port" binding:"omitempty,gte=1,lte=65535"` // SSH端口 - Username string `json:"username"` // SSH用户名 - Password string `json:"password"` // SSH密码 - Key string `json:"key"` // SSH密钥 + Username string `json:"username" binding:"omitempty,max=100"` // SSH用户名 + Password string `json:"password" binding:"omitempty,max=500"` // SSH密码 + Key string `json:"key" binding:"omitempty"` // SSH密钥 AuthMode AuthMode `json:"auth_mode" binding:"omitempty,oneof=1 2"` // SSH认证方式 - OperatorID int `json:"-"` // 操作人ID (不通过JSON传递) - OperatorName string `json:"-"` // 操作人姓名 (不通过JSON传递) + OperatorID int `json:"operator_id"` // 操作人ID + OperatorName string `json:"operator_name"` // 操作人姓名 } // DeleteTreeCloudResourceReq 删除云资源请求(仅从平台删除,不影响云上资源) type DeleteTreeCloudResourceReq struct { ID int `json:"id" binding:"required,gt=0"` - OperatorID int `json:"-"` // 操作人ID (不通过JSON传递) - OperatorName string `json:"-"` // 操作人姓名 (不通过JSON传递) + OperatorID int `json:"operator_id"` // 操作人ID + OperatorName string `json:"operator_name"` // 操作人姓名 +} + +// BatchDeleteTreeCloudResourceReq 批量删除云资源请求 +type BatchDeleteTreeCloudResourceReq struct { + IDs []int `json:"ids" binding:"required,min=1,max=100,dive,gt=0"` + OperatorID int `json:"operator_id"` // 操作人ID + OperatorName string `json:"operator_name"` // 操作人姓名 } // SyncTreeCloudResourceReq 从云厂商同步资源请求 type SyncTreeCloudResourceReq struct { CloudAccountID int `json:"cloud_account_id" binding:"required,gt=0"` - CloudAccountRegionIDs []int `json:"cloud_account_region_ids"` // 指定同步的账号区域ID列表,为空则同步账号的所有区域 - ResourceTypes []CloudResourceType `json:"resource_types" binding:"omitempty"` // 同步的资源类型列表,为空则同步所有 - InstanceIDs []string `json:"instance_ids"` // 指定同步的实例ID列表,为空则同步所有 + CloudAccountRegionIDs []int `json:"cloud_account_region_ids" binding:"omitempty,max=100,dive,gt=0"` // 指定同步的账号区域ID列表,为空则同步账号的所有区域 + ResourceTypes []CloudResourceType `json:"resource_types" binding:"omitempty,max=10,dive,oneof=1 2 3 4 5 6"` + InstanceIDs []string `json:"instance_ids" binding:"omitempty,max=100,dive,min=1"` // 指定同步的实例ID列表,为空则同步所有 SyncMode SyncMode `json:"sync_mode" binding:"omitempty,oneof=full incremental"` // 同步模式: full-全量, incremental-增量 AutoBind bool `json:"auto_bind"` // 是否自动绑定到服务树节点 BindNodeID int `json:"bind_node_id" binding:"omitempty,gt=0"` // 自动绑定的目标节点ID - OperatorID int `json:"-"` // 操作人ID (不通过JSON传递) - OperatorName string `json:"-"` // 操作人姓名 (不通过JSON传递) -} - -// SyncCloudResourceResp 同步云资源响应 -type SyncCloudResourceResp struct { - TotalCount int `json:"total_count"` // 总共同步的资源数量 - NewCount int `json:"new_count"` // 新增的资源数量 - UpdateCount int `json:"update_count"` // 更新的资源数量 - DeleteCount int `json:"delete_count"` // 删除的资源数量(全量同步时) - FailedCount int `json:"failed_count"` // 同步失败的数量 - FailedInstances []string `json:"failed_instances"` // 同步失败的实例ID列表 - SyncTime time.Time `json:"sync_time"` // 同步时间 + OperatorID int `json:"operator_id"` // 操作人ID + OperatorName string `json:"operator_name"` // 操作人姓名 } // VerifyCloudCredentialsReq 验证云厂商凭证请求 -// Deprecated: 使用 cloud_account.go 中的 VerifyCloudAccountReq type VerifyCloudCredentialsReq struct { Provider CloudProvider `json:"provider" binding:"required,oneof=1 2 3 4 5 6"` - Region string `json:"region" binding:"required"` - AccessKey string `json:"access_key" binding:"required"` - SecretKey string `json:"secret_key" binding:"required"` + Region string `json:"region" binding:"required,min=1,max=50"` + AccessKey string `json:"access_key" binding:"required,min=10,max=500"` + SecretKey string `json:"secret_key" binding:"required,min=10,max=500"` } // GetTreeNodeCloudResourcesReq 获取树节点下的云资源请求 @@ -282,24 +229,38 @@ type GetTreeNodeCloudResourcesReq struct { CloudAccountID int `json:"cloud_account_id" form:"cloud_account_id" binding:"omitempty,gt=0"` ResourceType CloudResourceType `json:"resource_type" form:"resource_type" binding:"omitempty,oneof=1 2 3 4 5 6"` Status CloudResourceStatus `json:"status" form:"status" binding:"omitempty,oneof=1 2 3 4 5 6"` + Page int `json:"page" form:"page" binding:"omitempty,gte=1"` + PageSize int `json:"page_size" form:"page_size" binding:"omitempty,gte=1,lte=100"` } // BindTreeCloudResourceReq 绑定云资源到树节点请求 type BindTreeCloudResourceReq struct { ID int `json:"id" binding:"required,gt=0"` - TreeNodeIDs []int `json:"tree_node_ids" binding:"required,min=1,dive,gt=0"` + TreeNodeIDs []int `json:"tree_node_ids" binding:"required,min=1,max=100,dive,gt=0"` } // UnBindTreeCloudResourceReq 解绑云资源与树节点请求 type UnBindTreeCloudResourceReq struct { ID int `json:"id" binding:"required,gt=0"` - TreeNodeIDs []int `json:"tree_node_ids" binding:"required,min=1,dive,gt=0"` + TreeNodeIDs []int `json:"tree_node_ids" binding:"required,min=1,max=100,dive,gt=0"` +} + +// BatchBindTreeCloudResourceReq 批量绑定云资源到树节点请求 +type BatchBindTreeCloudResourceReq struct { + IDs []int `json:"ids" binding:"required,min=1,max=100,dive,gt=0"` + TreeNodeID int `json:"tree_node_id" binding:"required,gt=0"` +} + +// BatchUnBindTreeCloudResourceReq 批量解绑云资源与树节点请求 +type BatchUnBindTreeCloudResourceReq struct { + IDs []int `json:"ids" binding:"required,min=1,max=100,dive,gt=0"` + TreeNodeID int `json:"tree_node_id" binding:"required,gt=0"` } // ConnectTreeCloudResourceTerminalReq 连接云资源终端请求(针对ECS) type ConnectTreeCloudResourceTerminalReq struct { ID int `json:"id" form:"id" binding:"required,gt=0"` - UserID int `json:"user_id"` + UserID int `json:"user_id" binding:"omitempty,gt=0"` } // UpdateCloudResourceStatusReq 更新云资源状态请求 @@ -308,57 +269,115 @@ type UpdateCloudResourceStatusReq struct { Status CloudResourceStatus `json:"status" binding:"required,oneof=1 2 3 4 5 6"` } +// BatchUpdateCloudResourceStatusReq 批量更新云资源状态请求 +type BatchUpdateCloudResourceStatusReq struct { + IDs []int `json:"ids" binding:"required,min=1,max=100,dive,gt=0"` + Status CloudResourceStatus `json:"status" binding:"required,oneof=1 2 3 4 5 6"` + OperatorID int `json:"operator_id"` // 操作人ID + OperatorName string `json:"operator_name"` // 操作人姓名 +} + // CloudResourceSyncHistory 云资源同步历史 type CloudResourceSyncHistory struct { Model - CloudAccountID int `json:"cloud_account_id" gorm:"not null;comment:云账户ID"` - SyncMode SyncMode `json:"sync_mode" gorm:"type:varchar(20);comment:同步模式"` - TotalCount int `json:"total_count" gorm:"comment:同步总数"` - NewCount int `json:"new_count" gorm:"comment:新增数量"` - UpdateCount int `json:"update_count" gorm:"comment:更新数量"` - DeleteCount int `json:"delete_count" gorm:"comment:删除数量"` - FailedCount int `json:"failed_count" gorm:"comment:失败数量"` - FailedInstances string `json:"failed_instances" gorm:"type:text;comment:失败的实例ID列表(JSON)"` - SyncStatus string `json:"sync_status" gorm:"type:varchar(20);comment:同步状态(success/failed/partial)"` - ErrorMessage string `json:"error_message" gorm:"type:text;comment:错误信息"` - StartTime time.Time `json:"start_time" gorm:"comment:开始时间"` - EndTime time.Time `json:"end_time" gorm:"comment:结束时间"` - Duration int `json:"duration" gorm:"comment:同步耗时(秒)"` + CloudAccountID int `json:"cloud_account_id" gorm:"not null;index;comment:云账户ID"` + CloudAccount *CloudAccount `json:"cloud_account,omitempty" gorm:"foreignKey:CloudAccountID"` + SyncMode SyncMode `json:"sync_mode" gorm:"type:varchar(20);index;comment:同步模式"` + TotalCount int `json:"total_count" gorm:"comment:同步总数;default:0"` + NewCount int `json:"new_count" gorm:"comment:新增数量;default:0"` + UpdateCount int `json:"update_count" gorm:"comment:更新数量;default:0"` + DeleteCount int `json:"delete_count" gorm:"comment:删除数量;default:0"` + FailedCount int `json:"failed_count" gorm:"comment:失败数量;default:0"` + FailedInstances string `json:"failed_instances" gorm:"type:text;comment:失败的实例ID列表(JSON)"` + SyncStatus SyncStatus `json:"sync_status" gorm:"type:varchar(20);index;comment:同步状态(success/failed/partial)"` + ErrorMessage string `json:"error_message" gorm:"type:text;comment:错误信息"` + StartTime time.Time `json:"start_time" gorm:"type:datetime;index;comment:开始时间"` + EndTime *time.Time `json:"end_time" gorm:"type:datetime;comment:结束时间"` + Duration int `json:"duration" gorm:"comment:同步耗时(秒);default:0"` + OperatorID int `json:"operator_id" gorm:"index;comment:操作人ID"` + OperatorName string `json:"operator_name" gorm:"type:varchar(100);comment:操作人姓名"` } func (c *CloudResourceSyncHistory) TableName() string { - return "cl_cloud_resource_sync_history" + return "cl_tree_cloud_resource_sync_history" } // GetCloudResourceSyncHistoryReq 获取同步历史请求 type GetCloudResourceSyncHistoryReq struct { ListReq - CloudAccountID int `json:"cloud_account_id" form:"cloud_account_id" binding:"omitempty,gt=0"` - SyncStatus string `json:"sync_status" form:"sync_status"` + CloudAccountID int `json:"cloud_account_id" form:"cloud_account_id" binding:"omitempty,gt=0"` + SyncStatus SyncStatus `json:"sync_status" form:"sync_status" binding:"omitempty,oneof=success failed partial"` + SyncMode SyncMode `json:"sync_mode" form:"sync_mode" binding:"omitempty,oneof=full incremental"` } // CloudResourceChangeLog 云资源变更日志 type CloudResourceChangeLog struct { Model - ResourceID int `json:"resource_id" gorm:"not null;comment:云资源ID"` - InstanceID string `json:"instance_id" gorm:"type:varchar(100);comment:实例ID"` - ChangeType string `json:"change_type" gorm:"type:varchar(20);comment:变更类型(created/updated/deleted/status_changed)"` - FieldName string `json:"field_name" gorm:"type:varchar(100);comment:变更字段名"` - OldValue string `json:"old_value" gorm:"type:text;comment:旧值"` - NewValue string `json:"new_value" gorm:"type:text;comment:新值"` - ChangeSource string `json:"change_source" gorm:"type:varchar(50);comment:变更来源(sync/manual)"` - OperatorID int `json:"operator_id" gorm:"comment:操作人ID"` - OperatorName string `json:"operator_name" gorm:"type:varchar(100);comment:操作人姓名"` - ChangeTime time.Time `json:"change_time" gorm:"comment:变更时间"` + ResourceID int `json:"resource_id" gorm:"not null;index;comment:云资源ID"` + CloudResource *TreeCloudResource `json:"cloud_resource,omitempty" gorm:"foreignKey:ResourceID"` + InstanceID string `json:"instance_id" gorm:"type:varchar(100);index;comment:实例ID"` + ChangeType string `json:"change_type" gorm:"type:varchar(20);index;comment:变更类型(created/updated/deleted/status_changed)"` + FieldName string `json:"field_name" gorm:"type:varchar(100);comment:变更字段名"` + OldValue string `json:"old_value" gorm:"type:text;comment:旧值"` + NewValue string `json:"new_value" gorm:"type:text;comment:新值"` + ChangeSource string `json:"change_source" gorm:"type:varchar(50);index;comment:变更来源(sync/manual)"` + OperatorID int `json:"operator_id" gorm:"index;comment:操作人ID"` + OperatorName string `json:"operator_name" gorm:"type:varchar(100);comment:操作人姓名"` + ChangeTime time.Time `json:"change_time" gorm:"type:datetime;index;comment:变更时间"` + CloudAccountID int `json:"cloud_account_id" gorm:"index;comment:云账户ID"` } func (c *CloudResourceChangeLog) TableName() string { - return "cl_cloud_resource_change_log" + return "cl_tree_cloud_resource_change_log" } // GetCloudResourceChangeLogReq 获取资源变更日志请求 type GetCloudResourceChangeLogReq struct { ListReq - ResourceID int `json:"resource_id" form:"resource_id" binding:"omitempty,gt=0"` - ChangeType string `json:"change_type" form:"change_type"` + ResourceID int `json:"resource_id" form:"resource_id" binding:"omitempty,gt=0"` + CloudAccountID int `json:"cloud_account_id" form:"cloud_account_id" binding:"omitempty,gt=0"` + ChangeType string `json:"change_type" form:"change_type" binding:"omitempty,oneof=created updated deleted status_changed"` + ChangeSource string `json:"change_source" form:"change_source" binding:"omitempty,oneof=sync manual"` +} + +// ExportCloudResourceReq 导出云资源请求 +type ExportCloudResourceReq struct { + CloudAccountID int `json:"cloud_account_id" form:"cloud_account_id" binding:"omitempty,gt=0"` + ResourceType CloudResourceType `json:"resource_type" form:"resource_type" binding:"omitempty,oneof=1 2 3 4 5 6"` + Status CloudResourceStatus `json:"status" form:"status" binding:"omitempty,oneof=1 2 3 4 5 6"` + Environment string `json:"environment" form:"environment" binding:"omitempty,max=50"` + Format string `json:"format" form:"format" binding:"omitempty,oneof=json csv excel"` + IDs []int `json:"ids" binding:"omitempty,max=1000,dive,gt=0"` // 指定导出的资源ID +} + +// SyncCloudResourceResp 云资源同步响应 +type SyncCloudResourceResp struct { + TotalCount int `json:"total_count"` // 总数 + NewCount int `json:"new_count"` // 新增数量 + UpdateCount int `json:"update_count"` // 更新数量 + DeleteCount int `json:"delete_count"` // 删除数量 + FailedCount int `json:"failed_count"` // 失败数量 + FailedInstances []string `json:"failed_instances"` // 失败的实例ID列表 + SyncTime time.Time `json:"sync_time"` // 同步时间 + Message string `json:"message"` // 同步消息 +} + +// String CloudProvider转字符串方法 +func (p CloudProvider) String() string { + switch p { + case ProviderAliyun: + return "aliyun" + case ProviderTencent: + return "tencent" + case ProviderAWS: + return "aws" + case ProviderHuawei: + return "huawei" + case ProviderAzure: + return "azure" + case ProviderGCP: + return "gcp" + default: + return "unknown" + } } diff --git a/internal/model/tree_cloud_account.go b/internal/model/tree_cloud_account.go index bf82b3ca..b088c59c 100644 --- a/internal/model/tree_cloud_account.go +++ b/internal/model/tree_cloud_account.go @@ -25,6 +25,8 @@ package model +import "time" + // CloudAccountStatus 云账户状态 type CloudAccountStatus int8 @@ -36,32 +38,60 @@ const ( // CloudAccount 云账户管理 type CloudAccount struct { Model - Name string `json:"name" gorm:"type:varchar(100);not null;comment:账户名称"` - Provider CloudProvider `json:"provider" gorm:"type:tinyint(1);not null;comment:云厂商类型;default:1"` - Region string `json:"region" gorm:"type:varchar(50);not null;comment:区域,如cn-hangzhou"` - AccessKey string `json:"-" gorm:"type:varchar(500);not null;comment:访问密钥ID,加密存储"` - SecretKey string `json:"-" gorm:"type:varchar(500);not null;comment:访问密钥Secret,加密存储"` - AccountID string `json:"account_id" gorm:"type:varchar(100);comment:云账号ID"` - AccountName string `json:"account_name" gorm:"type:varchar(100);comment:云账号名称"` - AccountAlias string `json:"account_alias" gorm:"type:varchar(100);comment:账号别名"` - Description string `json:"description" gorm:"type:text;comment:账户描述"` - Status CloudAccountStatus `json:"status" gorm:"type:tinyint(1);not null;comment:账户状态,1:启用,2:禁用;default:1"` - CreateUserID int `json:"create_user_id" gorm:"comment:创建者ID;default:0"` - CreateUserName string `json:"create_user_name" gorm:"type:varchar(100);comment:创建者姓名"` - CloudResources []*TreeCloudResource `json:"cloud_resources,omitempty" gorm:"foreignKey:CloudAccountID"` - Regions []*CloudAccountRegion `json:"regions,omitempty" gorm:"foreignKey:CloudAccountID"` + Name string `json:"name" gorm:"type:varchar(100);not null;uniqueIndex:idx_name_provider;comment:账户名称"` + Provider CloudProvider `json:"provider" gorm:"type:tinyint(1);not null;uniqueIndex:idx_name_provider;comment:云厂商类型;default:1"` + AccessKey string `json:"-" gorm:"type:varchar(500);not null;comment:访问密钥ID,加密存储"` + SecretKey string `json:"-" gorm:"type:varchar(500);not null;comment:访问密钥Secret,加密存储"` + AccountID string `json:"account_id" gorm:"type:varchar(100);index;comment:云账号ID"` + AccountName string `json:"account_name" gorm:"type:varchar(100);comment:云账号名称"` + AccountAlias string `json:"account_alias" gorm:"type:varchar(100);comment:账号别名"` + Description string `json:"description" gorm:"type:varchar(500);comment:账户描述"` + Status CloudAccountStatus `json:"status" gorm:"type:tinyint(1);not null;index;comment:账户状态,1:启用,2:禁用;default:1"` + CreateUserID int `json:"create_user_id" gorm:"not null;comment:创建者ID;default:0"` + CreateUserName string `json:"create_user_name" gorm:"type:varchar(100);not null;comment:创建者姓名"` + // 关联关系 + CloudResources []*TreeCloudResource `json:"cloud_resources,omitempty" gorm:"foreignKey:CloudAccountID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;comment:云账户资源"` + Regions []*CloudAccountRegion `json:"regions,omitempty" gorm:"foreignKey:CloudAccountID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;comment:云账户区域"` } func (c *CloudAccount) TableName() string { - return "cl_cloud_account" + return "cl_tree_cloud_account" +} + +// CloudAccountRegionStatus 云账号区域状态 +type CloudAccountRegionStatus int8 + +const ( + CloudAccountRegionEnabled CloudAccountRegionStatus = iota + 1 // 启用 + CloudAccountRegionDisabled // 禁用 +) + +// CloudAccountRegion 云账号区域关联表 +type CloudAccountRegion struct { + Model + CloudAccountID int `json:"cloud_account_id" gorm:"not null;comment:云账户ID;index:idx_account_region,unique"` + CloudAccount *CloudAccount `json:"cloud_account,omitempty" gorm:"foreignKey:CloudAccountID"` + Region string `json:"region" gorm:"type:varchar(50);not null;comment:区域,如cn-hangzhou;index:idx_account_region,unique"` + RegionName string `json:"region_name" gorm:"type:varchar(100);comment:区域名称,如华东1(杭州)"` + Status CloudAccountRegionStatus `json:"status" gorm:"type:tinyint(1);not null;comment:区域状态,1:启用,2:禁用;default:1"` + IsDefault bool `json:"is_default" gorm:"comment:是否为默认区域;default:false"` + Description string `json:"description" gorm:"type:text;comment:区域描述"` + LastSyncTime *time.Time `json:"last_sync_time" gorm:"type:datetime;comment:最后同步时间"` + CreateUserID int `json:"create_user_id" gorm:"comment:创建者ID;default:0"` + CreateUserName string `json:"create_user_name" gorm:"type:varchar(100);comment:创建者姓名"` +} + +func (c *CloudAccountRegion) TableName() string { + return "cl_tree_cloud_account_region" } // GetCloudAccountListReq 获取云账户列表请求 type GetCloudAccountListReq struct { ListReq - Provider CloudProvider `json:"provider" form:"provider" binding:"omitempty,oneof=1 2 3 4 5 6"` - Region string `json:"region" form:"region"` - Status CloudAccountStatus `json:"status" form:"status" binding:"omitempty,oneof=1 2"` + Provider CloudProvider `json:"provider" form:"provider" binding:"omitempty,oneof=1 2 3 4 5 6"` // 云厂商筛选 + Status CloudAccountStatus `json:"status" form:"status" binding:"omitempty,oneof=1 2"` // 状态筛选 + OrderBy string `json:"order_by" form:"order_by" binding:"omitempty,oneof=created_at updated_at name"` // 排序字段 + Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc"` // 排序方向 } // GetCloudAccountDetailReq 获取云账户详情请求 @@ -71,30 +101,28 @@ type GetCloudAccountDetailReq struct { // CreateCloudAccountReq 创建云账户请求 type CreateCloudAccountReq struct { - Name string `json:"name" binding:"required"` - Provider CloudProvider `json:"provider" binding:"required,oneof=1 2 3 4 5 6"` - Region string `json:"region" binding:"required"` - AccessKey string `json:"access_key" binding:"required"` - SecretKey string `json:"secret_key" binding:"required"` - AccountID string `json:"account_id"` - AccountName string `json:"account_name"` - AccountAlias string `json:"account_alias"` - Description string `json:"description"` - CreateUserID int `json:"create_user_id"` - CreateUserName string `json:"create_user_name"` - Regions []CreateCloudAccountRegionItem `json:"regions,omitempty"` + Name string `json:"name" binding:"required,min=2,max=100"` // 账户名称 + Provider CloudProvider `json:"provider" binding:"required,oneof=1 2 3 4 5 6"` // 云厂商类型 + AccessKey string `json:"access_key" binding:"required,min=10,max=500"` // 访问密钥ID + SecretKey string `json:"secret_key" binding:"required,min=10,max=500"` // 访问密钥Secret + AccountID string `json:"account_id" binding:"omitempty,max=100"` // 云账号ID + AccountName string `json:"account_name" binding:"omitempty,max=100"` // 云账号名称 + AccountAlias string `json:"account_alias" binding:"omitempty,max=100"` // 账号别名 + Description string `json:"description" binding:"omitempty,max=500"` // 账户描述 + Regions []CreateCloudAccountRegionItem `json:"regions" binding:"required,min=1,dive"` // 区域配置(至少一个) } // UpdateCloudAccountReq 更新云账户请求 type UpdateCloudAccountReq struct { - ID int `json:"id" binding:"required,gt=0"` - Name string `json:"name"` - AccessKey string `json:"access_key"` - SecretKey string `json:"secret_key"` - AccountID string `json:"account_id"` - AccountName string `json:"account_name"` - AccountAlias string `json:"account_alias"` - Description string `json:"description"` + ID int `json:"id" binding:"required,gt=0"` // 账户ID + Name string `json:"name" binding:"omitempty,min=2,max=100"` // 账户名称 + AccessKey string `json:"access_key" binding:"omitempty,min=10,max=500"` // 访问密钥ID + SecretKey string `json:"secret_key" binding:"omitempty,min=10,max=500"` // 访问密钥Secret + AccountID string `json:"account_id" binding:"omitempty,max=100"` // 云账号ID + AccountName string `json:"account_name" binding:"omitempty,max=100"` // 云账号名称 + AccountAlias string `json:"account_alias" binding:"omitempty,max=100"` // 账号别名 + Description string `json:"description" binding:"omitempty,max=500"` // 账户描述 + Regions []CreateCloudAccountRegionItem `json:"regions" binding:"omitempty,min=1,dive"` // 区域配置(可选,如果提供则至少一个) } // DeleteCloudAccountReq 删除云账户请求 @@ -110,5 +138,132 @@ type UpdateCloudAccountStatusReq struct { // VerifyCloudAccountReq 验证云账户凭证请求 type VerifyCloudAccountReq struct { + ID int `json:"id" binding:"required,gt=0"` // 账户ID +} + +// BatchDeleteCloudAccountReq 批量删除云账户请求 +type BatchDeleteCloudAccountReq struct { + IDs []int `json:"ids" binding:"required,min=1,max=100,dive,gt=0"` // 账户ID列表 +} + +// BatchUpdateCloudAccountStatusReq 批量更新云账户状态请求 +type BatchUpdateCloudAccountStatusReq struct { + IDs []int `json:"ids" binding:"required,min=1,max=100,dive,gt=0"` // 账户ID列表 + Status CloudAccountStatus `json:"status" binding:"required,oneof=1 2"` // 目标状态 +} + +// ImportCloudAccountReq 导入云账户请求 +type ImportCloudAccountReq struct { + Accounts []CreateCloudAccountReq `json:"accounts" binding:"required,min=1,max=50,dive"` // 账户列表 +} + +// ExportCloudAccountReq 导出云账户请求 +type ExportCloudAccountReq struct { + IDs []int `json:"ids" binding:"omitempty,max=100,dive,gt=0"` // 指定账户ID,为空则导出全部 + Provider CloudProvider `json:"provider" binding:"omitempty,oneof=1 2 3 4 5 6"` // 按云厂商过滤 + Format string `json:"format" binding:"omitempty,oneof=json csv"` // 导出格式:json或csv +} + +// ImportCloudAccountResp 导入云账户响应 +type ImportCloudAccountResp struct { + SuccessCount int `json:"success_count"` // 成功数量 + FailedCount int `json:"failed_count"` // 失败数量 + FailedItems []string `json:"failed_items"` // 失败的账户名称列表 + Message string `json:"message"` // 提示信息 +} + +// ExportRegion 导出区域信息 +type ExportRegion struct { + Region string `json:"region"` // 区域代码 + RegionName string `json:"region_name"` // 区域名称 + IsDefault bool `json:"is_default"` // 是否为默认区域 + Description string `json:"description"` // 区域描述 +} + +// ExportAccount 导出账户信息(不包含敏感信息) +type ExportAccount struct { + ID int `json:"id"` // 账户ID + Name string `json:"name"` // 账户名称 + Provider CloudProvider `json:"provider"` // 云厂商类型 + ProviderName string `json:"provider_name"` // 云厂商名称 + AccountID string `json:"account_id"` // 云账号ID + AccountName string `json:"account_name"` // 云账号名称 + AccountAlias string `json:"account_alias"` // 账号别名 + Description string `json:"description"` // 账户描述 + Status int8 `json:"status"` // 账户状态 + Regions []ExportRegion `json:"regions"` // 区域列表 + CreatedAt string `json:"created_at"` // 创建时间 +} + +// GetCloudAccountRegionListReq 获取云账号区域列表请求 +type GetCloudAccountRegionListReq struct { + ListReq + CloudAccountID int `json:"cloud_account_id" form:"cloud_account_id" binding:"omitempty,gt=0"` + Region string `json:"region" form:"region"` + Status CloudAccountRegionStatus `json:"status" form:"status" binding:"omitempty,oneof=1 2"` +} + +// CreateCloudAccountRegionReq 创建云账号区域关联请求 +type CreateCloudAccountRegionReq struct { + CloudAccountID int `json:"cloud_account_id" binding:"required,gt=0"` + Region string `json:"region" binding:"required"` + RegionName string `json:"region_name"` + IsDefault bool `json:"is_default"` + Description string `json:"description"` + CreateUserID int `json:"create_user_id"` + CreateUserName string `json:"create_user_name"` +} + +// UpdateCloudAccountRegionReq 更新云账号区域关联请求 +type UpdateCloudAccountRegionReq struct { + ID int `json:"id" binding:"required,gt=0"` + RegionName string `json:"region_name"` + IsDefault bool `json:"is_default"` + Description string `json:"description"` +} + +// DeleteCloudAccountRegionReq 删除云账号区域关联请求 +type DeleteCloudAccountRegionReq struct { ID int `json:"id" binding:"required,gt=0"` } + +// UpdateCloudAccountRegionStatusReq 更新云账号区域状态请求 +type UpdateCloudAccountRegionStatusReq struct { + ID int `json:"id" binding:"required,gt=0"` + Status CloudAccountRegionStatus `json:"status" binding:"required,oneof=1 2"` +} + +// BatchCreateCloudAccountRegionReq 批量创建云账号区域关联请求 +type BatchCreateCloudAccountRegionReq struct { + CloudAccountID int `json:"cloud_account_id" binding:"required,gt=0"` + Regions []CreateCloudAccountRegionItem `json:"regions" binding:"required,min=1"` + CreateUserID int `json:"create_user_id"` + CreateUserName string `json:"create_user_name"` +} + +// CreateCloudAccountRegionItem 创建云账号区域项 +type CreateCloudAccountRegionItem struct { + Region string `json:"region" binding:"required"` // 区域,如cn-hangzhou + RegionName string `json:"region_name"` // 区域名称,如华东1(杭州) + IsDefault bool `json:"is_default"` // 是否为默认区域 + Description string `json:"description"` // 区域描述 +} + +// GetAvailableRegionsReq 获取可用区域列表请求 +type GetAvailableRegionsReq struct { + Provider CloudProvider `json:"provider" form:"provider" binding:"required,oneof=1 2 3 4 5 6"` + AccessKey string `json:"access_key" form:"access_key"` // 可选,提供时会通过API动态获取 + SecretKey string `json:"secret_key" form:"secret_key"` // 可选,提供时会通过API动态获取 +} + +// AvailableRegion 可用区域信息 +type AvailableRegion struct { + Region string `json:"region"` // 区域代码,如cn-hangzhou + RegionName string `json:"region_name"` // 区域名称,如华东1(杭州) + Available bool `json:"available"` // 是否可用 +} + +// GetAvailableRegionsResp 获取可用区域列表响应 +type GetAvailableRegionsResp struct { + Regions []AvailableRegion `json:"regions"` +} diff --git a/internal/tree/api/cloud_account_handler.go b/internal/tree/api/cloud_account_handler.go index 6d59a202..c668a1d0 100644 --- a/internal/tree/api/cloud_account_handler.go +++ b/internal/tree/api/cloud_account_handler.go @@ -52,6 +52,10 @@ func (h *CloudAccountHandler) RegisterRouters(server *gin.Engine) { accountGroup.DELETE("/:id/delete", h.DeleteCloudAccount) accountGroup.PUT("/:id/status", h.UpdateCloudAccountStatus) accountGroup.POST("/:id/verify", h.VerifyCloudAccount) + accountGroup.POST("/batch/delete", h.BatchDeleteCloudAccount) + accountGroup.PUT("/batch/status", h.BatchUpdateCloudAccountStatus) + accountGroup.POST("/import", h.ImportCloudAccount) + accountGroup.POST("/export", h.ExportCloudAccount) } } @@ -87,11 +91,8 @@ func (h *CloudAccountHandler) CreateCloudAccount(ctx *gin.Context) { user := ctx.MustGet("user").(utils.UserClaims) - req.CreateUserID = user.Uid - req.CreateUserName = user.Username - utils.HandleRequest(ctx, &req, func() (interface{}, error) { - return nil, h.service.CreateCloudAccount(ctx, &req) + return nil, h.service.CreateCloudAccount(ctx, &req, user.Uid, user.Username) }) } @@ -162,3 +163,41 @@ func (h *CloudAccountHandler) VerifyCloudAccount(ctx *gin.Context) { return nil, h.service.VerifyCloudAccount(ctx, &req) }) } + +// BatchDeleteCloudAccount 批量删除云账户 +func (h *CloudAccountHandler) BatchDeleteCloudAccount(ctx *gin.Context) { + var req model.BatchDeleteCloudAccountReq + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.BatchDeleteCloudAccount(ctx, &req) + }) +} + +// BatchUpdateCloudAccountStatus 批量更新云账户状态 +func (h *CloudAccountHandler) BatchUpdateCloudAccountStatus(ctx *gin.Context) { + var req model.BatchUpdateCloudAccountStatusReq + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.BatchUpdateCloudAccountStatus(ctx, &req) + }) +} + +// ImportCloudAccount 导入云账户 +func (h *CloudAccountHandler) ImportCloudAccount(ctx *gin.Context) { + var req model.ImportCloudAccountReq + + user := ctx.MustGet("user").(utils.UserClaims) + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return h.service.ImportCloudAccount(ctx, &req, user.Uid, user.Username) + }) +} + +// ExportCloudAccount 导出云账户 +func (h *CloudAccountHandler) ExportCloudAccount(ctx *gin.Context) { + var req model.ExportCloudAccountReq + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return h.service.ExportCloudAccount(ctx, &req) + }) +} diff --git a/internal/tree/api/tree_cloud_handler.go b/internal/tree/api/tree_cloud_handler.go index e781fb74..892d44c5 100644 --- a/internal/tree/api/tree_cloud_handler.go +++ b/internal/tree/api/tree_cloud_handler.go @@ -60,6 +60,8 @@ func (h *TreeCloudHandler) RegisterRouters(server *gin.Engine) { cloudGroup.POST("/:id/bind", h.BindTreeCloudResource) cloudGroup.POST("/:id/unbind", h.UnBindTreeCloudResource) cloudGroup.GET("/changelog", h.GetChangeLog) + cloudGroup.POST("/batch/delete", h.BatchDeleteTreeCloudResource) + cloudGroup.PUT("/batch/status", h.BatchUpdateCloudResourceStatus) } } @@ -320,3 +322,31 @@ func (h *TreeCloudHandler) UpdateCloudResourceStatus(ctx *gin.Context) { return nil, h.service.UpdateCloudResourceStatus(ctx, &req) }) } + +// BatchDeleteTreeCloudResource 批量删除云资源 +func (h *TreeCloudHandler) BatchDeleteTreeCloudResource(ctx *gin.Context) { + var req model.BatchDeleteTreeCloudResourceReq + + // 获取当前用户信息 + uc := ctx.MustGet("user").(utils.UserClaims) + req.OperatorID = uc.Uid + req.OperatorName = uc.Username + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.BatchDeleteTreeCloudResource(ctx, &req) + }) +} + +// BatchUpdateCloudResourceStatus 批量更新云资源状态 +func (h *TreeCloudHandler) BatchUpdateCloudResourceStatus(ctx *gin.Context) { + var req model.BatchUpdateCloudResourceStatusReq + + // 获取当前用户信息 + uc := ctx.MustGet("user").(utils.UserClaims) + req.OperatorID = uc.Uid + req.OperatorName = uc.Username + + utils.HandleRequest(ctx, &req, func() (interface{}, error) { + return nil, h.service.BatchUpdateCloudResourceStatus(ctx, &req) + }) +} diff --git a/internal/tree/dao/cloud_account_dao.go b/internal/tree/dao/cloud_account_dao.go index 3b4da780..e0c109fd 100644 --- a/internal/tree/dao/cloud_account_dao.go +++ b/internal/tree/dao/cloud_account_dao.go @@ -39,12 +39,19 @@ type CloudAccountDAO interface { CreateWithTransaction(ctx context.Context, fn func(tx interface{}) error) error CreateInTransaction(ctx context.Context, account *model.CloudAccount, tx interface{}) error CreateRegionInTransaction(ctx context.Context, region *model.CloudAccountRegion, tx interface{}) error + DeleteRegionsByAccountIDInTransaction(ctx context.Context, accountID int, tx interface{}) error Update(ctx context.Context, account *model.CloudAccount) error + UpdateWithFields(ctx context.Context, account *model.CloudAccount, fields []string) error Delete(ctx context.Context, id int) error GetByID(ctx context.Context, id int) (*model.CloudAccount, error) GetList(ctx context.Context, req *model.GetCloudAccountListReq) ([]*model.CloudAccount, int64, error) UpdateStatus(ctx context.Context, id int, status model.CloudAccountStatus) error GetByProviderAndRegion(ctx context.Context, provider model.CloudProvider, region string) ([]*model.CloudAccount, error) + BatchDelete(ctx context.Context, ids []int) error + BatchUpdateStatus(ctx context.Context, ids []int, status model.CloudAccountStatus) error + CheckNameExists(ctx context.Context, name string, provider model.CloudProvider, excludeID int) (bool, error) + GetByIDs(ctx context.Context, ids []int) ([]*model.CloudAccount, error) + GetAll(ctx context.Context, provider model.CloudProvider) ([]*model.CloudAccount, error) } type cloudAccountDAO struct { @@ -122,26 +129,49 @@ func (d *cloudAccountDAO) GetList(ctx context.Context, req *model.GetCloudAccoun query = query.Where("status = ?", req.Status) } + // 支持搜索名称、账号名称、账号ID、账号别名 if req.Search != "" { - query = query.Where("name LIKE ? OR account_name LIKE ?", "%"+req.Search+"%", "%"+req.Search+"%") + searchPattern := "%" + req.Search + "%" + query = query.Where( + "name LIKE ? OR account_name LIKE ? OR account_id LIKE ? OR account_alias LIKE ?", + searchPattern, searchPattern, searchPattern, searchPattern, + ) } // 计算总数 err := query.Count(&total).Error if err != nil { - d.logger.Error("获取云账户总数失败", zap.Error(err)) + d.logger.Error("获取云账户总数失败", + zap.String("search", req.Search), + zap.Int8("provider", int8(req.Provider)), + zap.Error(err)) return nil, 0, err } - // 分页查询 + // 构建排序 + orderBy := "created_at" + order := "DESC" + + if req.OrderBy != "" { + orderBy = req.OrderBy + } + if req.Order != "" { + order = req.Order + } + + // 分页查询(预加载区域信息) offset := (req.Page - 1) * req.Size err = query. - Order("created_at DESC"). + Preload("Regions"). // 预加载区域信息 + Order(orderBy + " " + order). Limit(req.Size). Offset(offset). Find(&accounts).Error if err != nil { - d.logger.Error("获取云账户列表失败", zap.Error(err)) + d.logger.Error("获取云账户列表失败", + zap.Int("page", req.Page), + zap.Int("size", req.Size), + zap.Error(err)) return nil, 0, err } @@ -165,18 +195,26 @@ func (d *cloudAccountDAO) UpdateStatus(ctx context.Context, id int, status model func (d *cloudAccountDAO) GetByProviderAndRegion(ctx context.Context, provider model.CloudProvider, region string) ([]*model.CloudAccount, error) { var accounts []*model.CloudAccount - query := d.db.WithContext(ctx).Where("provider = ?", provider) - // 注:这里需要根据新的数据结构调整查询逻辑 - // 现在Region信息存储在 CloudAccountRegion 表中 + query := d.db.WithContext(ctx).Model(&model.CloudAccount{}). + Where("provider = ?", provider) + + // 如果指定了区域,通过JOIN查询特定区域的账户 if region != "" { query = query. - Joins("JOIN cl_cloud_account_region ON cl_cloud_account.id = cl_cloud_account_region.cloud_account_id"). - Where("cl_cloud_account_region.region = ?", region) + Joins("JOIN cl_tree_cloud_account_region ON cl_tree_cloud_account.id = cl_tree_cloud_account_region.cloud_account_id"). + Where("cl_tree_cloud_account_region.region = ?", region). + Distinct("cl_tree_cloud_account.*") // 避免重复 } + // 预加载区域信息 + query = query.Preload("Regions") + err := query.Find(&accounts).Error if err != nil { - d.logger.Error("根据云厂商和区域获取云账户列表失败", zap.Error(err)) + d.logger.Error("根据云厂商和区域获取云账户列表失败", + zap.Int8("provider", int8(provider)), + zap.String("region", region), + zap.Error(err)) return nil, err } @@ -219,3 +257,132 @@ func (d *cloudAccountDAO) CreateRegionInTransaction(ctx context.Context, region return nil } + +// DeleteRegionsByAccountIDInTransaction 在事务中删除指定账户的所有区域关联 +func (d *cloudAccountDAO) DeleteRegionsByAccountIDInTransaction(ctx context.Context, accountID int, tx interface{}) error { + gormTx, ok := tx.(*gorm.DB) + if !ok { + return errors.New("事务类型转换失败") + } + + if err := gormTx.WithContext(ctx). + Where("cloud_account_id = ?", accountID). + Delete(&model.CloudAccountRegion{}).Error; err != nil { + d.logger.Error("在事务中删除账户区域关联失败", + zap.Int("account_id", accountID), + zap.Error(err)) + return err + } + + return nil +} + +// UpdateWithFields 更新云账户(指定字段) +func (d *cloudAccountDAO) UpdateWithFields(ctx context.Context, account *model.CloudAccount, fields []string) error { + if len(fields) == 0 { + return errors.New("更新字段列表不能为空") + } + + if err := d.db.WithContext(ctx). + Model(account). + Select(fields). + Updates(account).Error; err != nil { + d.logger.Error("更新云账户失败", + zap.Int("id", account.ID), + zap.Strings("fields", fields), + zap.Error(err)) + return err + } + + return nil +} + +// BatchDelete 批量删除云账户 +func (d *cloudAccountDAO) BatchDelete(ctx context.Context, ids []int) error { + if len(ids) == 0 { + return errors.New("批量删除ID列表不能为空") + } + + if err := d.db.WithContext(ctx).Where("id IN ?", ids).Delete(&model.CloudAccount{}).Error; err != nil { + d.logger.Error("批量删除云账户失败", zap.Error(err), zap.Ints("ids", ids)) + return err + } + + return nil +} + +// BatchUpdateStatus 批量更新云账户状态 +func (d *cloudAccountDAO) BatchUpdateStatus(ctx context.Context, ids []int, status model.CloudAccountStatus) error { + if len(ids) == 0 { + return errors.New("批量更新ID列表不能为空") + } + + if err := d.db.WithContext(ctx). + Model(&model.CloudAccount{}). + Where("id IN ?", ids). + Update("status", status).Error; err != nil { + d.logger.Error("批量更新云账户状态失败", zap.Error(err), zap.Ints("ids", ids), zap.Int8("status", int8(status))) + return err + } + + return nil +} + +// CheckNameExists 检查云账户名称是否已存在(相同云厂商下) +func (d *cloudAccountDAO) CheckNameExists(ctx context.Context, name string, provider model.CloudProvider, excludeID int) (bool, error) { + var count int64 + + query := d.db.WithContext(ctx).Model(&model.CloudAccount{}). + Where("name = ? AND provider = ?", name, provider) + + // 如果提供了 excludeID(更新场景),排除当前记录 + if excludeID > 0 { + query = query.Where("id != ?", excludeID) + } + + if err := query.Count(&count).Error; err != nil { + d.logger.Error("检查云账户名称是否存在失败", zap.Error(err)) + return false, err + } + + return count > 0, nil +} + +// GetByIDs 根据ID列表获取云账户 +func (d *cloudAccountDAO) GetByIDs(ctx context.Context, ids []int) ([]*model.CloudAccount, error) { + if len(ids) == 0 { + return []*model.CloudAccount{}, nil + } + + var accounts []*model.CloudAccount + if err := d.db.WithContext(ctx). + Preload("Regions"). + Where("id IN ?", ids). + Find(&accounts).Error; err != nil { + d.logger.Error("根据ID列表获取云账户失败", zap.Error(err), zap.Ints("ids", ids)) + return nil, err + } + + return accounts, nil +} + +// GetAll 获取所有云账户(支持按云厂商筛选) +func (d *cloudAccountDAO) GetAll(ctx context.Context, provider model.CloudProvider) ([]*model.CloudAccount, error) { + var accounts []*model.CloudAccount + query := d.db.WithContext(ctx).Model(&model.CloudAccount{}) + + // 如果指定了云厂商,添加过滤条件 + if provider != 0 { + query = query.Where("provider = ?", provider) + } + + if err := query. + Preload("Regions"). + Order("created_at DESC"). + Find(&accounts).Error; err != nil { + d.logger.Error("获取所有云账户失败", zap.Error(err)) + return nil, err + } + + return accounts, nil +} diff --git a/internal/tree/dao/tree_cloud_dao.go b/internal/tree/dao/tree_cloud_dao.go index de26cb73..126335ef 100644 --- a/internal/tree/dao/tree_cloud_dao.go +++ b/internal/tree/dao/tree_cloud_dao.go @@ -48,7 +48,9 @@ type TreeCloudDAO interface { UnBindTreeNodes(ctx context.Context, cloudID int, treeNodeIds []int) error BatchGetByIDs(ctx context.Context, ids []int) ([]*model.TreeCloudResource, error) BatchCreate(ctx context.Context, clouds []*model.TreeCloudResource) error + BatchDelete(ctx context.Context, ids []int) error UpdateStatus(ctx context.Context, id int, status model.CloudResourceStatus) error + BatchUpdateStatus(ctx context.Context, ids []int, status model.CloudResourceStatus) error CreateSyncHistory(ctx context.Context, history *model.CloudResourceSyncHistory) error GetSyncHistoryList(ctx context.Context, req *model.GetCloudResourceSyncHistoryReq) ([]*model.CloudResourceSyncHistory, int64, error) CreateChangeLog(ctx context.Context, log *model.CloudResourceChangeLog) error @@ -340,7 +342,7 @@ func (d *treeCloudDAO) CreateSyncHistory(ctx context.Context, history *model.Clo d.logger.Info("创建同步历史成功", zap.Int("cloudAccountID", history.CloudAccountID), - zap.String("syncStatus", history.SyncStatus)) + zap.String("syncStatus", string(history.SyncStatus))) return nil } @@ -474,3 +476,36 @@ func (d *treeCloudDAO) GetByRegionAndInstanceID(ctx context.Context, regionID in return &resource, nil } + +// BatchDelete 批量删除云资源 +func (d *treeCloudDAO) BatchDelete(ctx context.Context, ids []int) error { + if len(ids) == 0 { + return errors.New("批量删除ID列表不能为空") + } + + if err := d.db.WithContext(ctx).Where("id IN ?", ids).Delete(&model.TreeCloudResource{}).Error; err != nil { + d.logger.Error("批量删除云资源失败", zap.Error(err), zap.Ints("ids", ids)) + return err + } + + d.logger.Info("批量删除云资源成功", zap.Int("count", len(ids))) + return nil +} + +// BatchUpdateStatus 批量更新云资源状态 +func (d *treeCloudDAO) BatchUpdateStatus(ctx context.Context, ids []int, status model.CloudResourceStatus) error { + if len(ids) == 0 { + return errors.New("批量更新ID列表不能为空") + } + + if err := d.db.WithContext(ctx). + Model(&model.TreeCloudResource{}). + Where("id IN ?", ids). + Update("status", status).Error; err != nil { + d.logger.Error("批量更新云资源状态失败", zap.Error(err), zap.Ints("ids", ids), zap.Int8("status", int8(status))) + return err + } + + d.logger.Info("批量更新云资源状态成功", zap.Int("count", len(ids)), zap.Int8("status", int8(status))) + return nil +} diff --git a/internal/tree/service/cloud_account_service.go b/internal/tree/service/cloud_account_service.go index db89e24e..26b1fca4 100644 --- a/internal/tree/service/cloud_account_service.go +++ b/internal/tree/service/cloud_account_service.go @@ -40,11 +40,15 @@ import ( type CloudAccountService interface { GetCloudAccountList(ctx context.Context, req *model.GetCloudAccountListReq) (model.ListResp[*model.CloudAccount], error) GetCloudAccountDetail(ctx context.Context, req *model.GetCloudAccountDetailReq) (*model.CloudAccount, error) - CreateCloudAccount(ctx context.Context, req *model.CreateCloudAccountReq) error + CreateCloudAccount(ctx context.Context, req *model.CreateCloudAccountReq, createUserID int, createUserName string) error UpdateCloudAccount(ctx context.Context, req *model.UpdateCloudAccountReq) error DeleteCloudAccount(ctx context.Context, req *model.DeleteCloudAccountReq) error UpdateCloudAccountStatus(ctx context.Context, req *model.UpdateCloudAccountStatusReq) error VerifyCloudAccount(ctx context.Context, req *model.VerifyCloudAccountReq) error + BatchDeleteCloudAccount(ctx context.Context, req *model.BatchDeleteCloudAccountReq) error + BatchUpdateCloudAccountStatus(ctx context.Context, req *model.BatchUpdateCloudAccountStatusReq) error + ImportCloudAccount(ctx context.Context, req *model.ImportCloudAccountReq, createUserID int, createUserName string) (*model.ImportCloudAccountResp, error) + ExportCloudAccount(ctx context.Context, req *model.ExportCloudAccountReq) (interface{}, error) } type cloudAccountService struct { @@ -64,12 +68,43 @@ func (s *cloudAccountService) GetCloudAccountList(ctx context.Context, req *mode // 兜底分页参数 treeUtils.ValidateAndSetPaginationDefaults(&req.Page, &req.Size) + // 设置默认排序 + if req.OrderBy == "" { + req.OrderBy = "created_at" + } + if req.Order == "" { + req.Order = "desc" + } + + // 记录查询参数 + s.logger.Debug("获取云账户列表", + zap.Int("page", req.Page), + zap.Int("size", req.Size), + zap.String("search", req.Search), + zap.Int8("provider", int8(req.Provider)), + zap.Int8("status", int8(req.Status)), + zap.String("order_by", req.OrderBy), + zap.String("order", req.Order)) + accounts, total, err := s.dao.GetList(ctx, req) if err != nil { - s.logger.Error("获取云账户列表失败", zap.Error(err)) - return model.ListResp[*model.CloudAccount]{}, err + s.logger.Error("获取云账户列表失败", + zap.Int("page", req.Page), + zap.Int("size", req.Size), + zap.Error(err)) + return model.ListResp[*model.CloudAccount]{}, fmt.Errorf("获取云账户列表失败: %w", err) } + // 清理敏感信息(双重保险,虽然json:"-"标签已经防止序列化) + treeUtils.SanitizeCloudAccounts(accounts) + + // 记录成功日志 + s.logger.Info("成功获取云账户列表", + zap.Int64("total", total), + zap.Int("returned", len(accounts)), + zap.Int("page", req.Page), + zap.Int("size", req.Size)) + return model.ListResp[*model.CloudAccount]{ Items: accounts, Total: total, @@ -82,44 +117,51 @@ func (s *cloudAccountService) GetCloudAccountDetail(ctx context.Context, req *mo return nil, fmt.Errorf("无效的云账户ID: %w", err) } + s.logger.Debug("获取云账户详情", zap.Int("id", req.ID)) + account, err := s.dao.GetByID(ctx, req.ID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { + s.logger.Warn("云账户不存在", zap.Int("id", req.ID)) return nil, errors.New("云账户不存在") } - s.logger.Error("获取云账户详情失败", zap.Int("id", req.ID), zap.Error(err)) - return nil, err + s.logger.Error("获取云账户详情失败", + zap.Int("id", req.ID), + zap.Error(err)) + return nil, fmt.Errorf("获取云账户详情失败: %w", err) } + // 清理敏感信息(双重保险,虽然json:"-"标签已经防止序列化) + treeUtils.SanitizeCloudAccount(account) + + // 记录成功日志(包含关键信息,但不包含敏感数据) + s.logger.Info("成功获取云账户详情", + zap.Int("id", account.ID), + zap.String("name", account.Name), + zap.Int8("provider", int8(account.Provider)), + zap.Int("region_count", len(account.Regions)), + zap.Int("resource_count", len(account.CloudResources))) + return account, nil } // CreateCloudAccount 创建云账户(支持多区域) -func (s *cloudAccountService) CreateCloudAccount(ctx context.Context, req *model.CreateCloudAccountReq) error { - // 验证区域列表 - if len(req.Regions) == 0 { - return errors.New("必须至少指定一个区域") +func (s *cloudAccountService) CreateCloudAccount(ctx context.Context, req *model.CreateCloudAccountReq, createUserID int, createUserName string) error { + // 验证和规范化区域列表 + normalizedRegions, err := treeUtils.ValidateAndNormalizeRegions(req.Regions) + if err != nil { + return fmt.Errorf("区域验证失败: %w", err) } - // 检查是否有重复的区域 - regionMap := make(map[string]bool) - var defaultCount int - for _, regionItem := range req.Regions { - if regionMap[regionItem.Region] { - return fmt.Errorf("区域 %s 重复", regionItem.Region) - } - regionMap[regionItem.Region] = true - - if regionItem.IsDefault { - defaultCount++ - } + // 检查账户名称是否已存在(同一云厂商下) + exists, err := s.dao.CheckNameExists(ctx, req.Name, req.Provider, 0) + if err != nil { + s.logger.Error("检查云账户名称是否存在失败", zap.Error(err)) + return fmt.Errorf("检查云账户名称失败: %w", err) } - // 确保只有一个默认区域,如果没有指定默认区域,则设置第一个为默认 - if defaultCount == 0 { - req.Regions[0].IsDefault = true - } else if defaultCount > 1 { - return errors.New("只能设置一个默认区域") + if exists { + return fmt.Errorf("云账户名称 %s 在 %s 下已存在", req.Name, treeUtils.GetProviderName(req.Provider)) } // 加密 AccessKey 和 SecretKey @@ -136,7 +178,7 @@ func (s *cloudAccountService) CreateCloudAccount(ctx context.Context, req *model } // 使用事务创建云账户和区域关联 - return s.dao.CreateWithTransaction(ctx, func(tx interface{}) error { + if err := s.dao.CreateWithTransaction(ctx, func(tx interface{}) error { // 创建云账户对象 account := &model.CloudAccount{ Name: req.Name, @@ -148,17 +190,19 @@ func (s *cloudAccountService) CreateCloudAccount(ctx context.Context, req *model AccountAlias: req.AccountAlias, Description: req.Description, Status: model.CloudAccountEnabled, // 默认启用 - CreateUserID: req.CreateUserID, - CreateUserName: req.CreateUserName, + CreateUserID: createUserID, + CreateUserName: createUserName, } if err := s.dao.CreateInTransaction(ctx, account, tx); err != nil { - s.logger.Error("创建云账户失败", zap.Error(err)) - return err + s.logger.Error("在事务中创建云账户失败", + zap.String("name", req.Name), + zap.Error(err)) + return fmt.Errorf("创建云账户失败: %w", err) } - // 创建区域关联 - for _, regionItem := range req.Regions { + // 创建区域关联(使用规范化后的区域列表) + for _, regionItem := range normalizedRegions { region := &model.CloudAccountRegion{ CloudAccountID: account.ID, Region: regionItem.Region, @@ -166,67 +210,178 @@ func (s *cloudAccountService) CreateCloudAccount(ctx context.Context, req *model IsDefault: regionItem.IsDefault, Description: regionItem.Description, Status: model.CloudAccountRegionEnabled, - CreateUserID: req.CreateUserID, - CreateUserName: req.CreateUserName, + CreateUserID: createUserID, + CreateUserName: createUserName, } if err := s.dao.CreateRegionInTransaction(ctx, region, tx); err != nil { - s.logger.Error("创建云账户区域关联失败", zap.Error(err)) - return err + s.logger.Error("在事务中创建云账户区域关联失败", + zap.Int("account_id", account.ID), + zap.String("region", regionItem.Region), + zap.Error(err)) + return fmt.Errorf("创建云账户区域关联失败: %w", err) } } + s.logger.Info("成功创建云账户", + zap.Int("account_id", account.ID), + zap.String("name", account.Name), + zap.Int8("provider", int8(account.Provider)), + zap.Int("region_count", len(normalizedRegions))) + return nil - }) + }); err != nil { + return err + } + + return nil } -// UpdateCloudAccount 更新云账户 +// UpdateCloudAccount 更新云账户(支持更新区域) func (s *cloudAccountService) UpdateCloudAccount(ctx context.Context, req *model.UpdateCloudAccountReq) error { if err := treeUtils.ValidateID(req.ID); err != nil { return fmt.Errorf("无效的云账户ID: %w", err) } // 检查云账户是否存在 - _, err := s.dao.GetByID(ctx, req.ID) + account, err := s.dao.GetByID(ctx, req.ID) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return errors.New("云账户不存在") } - return err + s.logger.Error("获取云账户失败", zap.Int("id", req.ID), zap.Error(err)) + return fmt.Errorf("获取云账户失败: %w", err) } - // 构建更新对象 - account := &model.CloudAccount{ - Model: model.Model{ID: req.ID}, - Name: req.Name, - AccountID: req.AccountID, - AccountName: req.AccountName, - AccountAlias: req.AccountAlias, - Description: req.Description, - } - - // 如果需要更新 AccessKey - if req.AccessKey != "" { - encryptedAccessKey, err := treeUtils.EncryptPassword(req.AccessKey) + // 如果修改了名称,检查新名称是否已存在(同一云厂商下) + if req.Name != "" && req.Name != account.Name { + exists, err := s.dao.CheckNameExists(ctx, req.Name, account.Provider, req.ID) if err != nil { - s.logger.Error("加密AccessKey失败", zap.Error(err)) - return fmt.Errorf("加密AccessKey失败: %w", err) + s.logger.Error("检查云账户名称是否存在失败", zap.Error(err)) + return fmt.Errorf("检查云账户名称失败: %w", err) + } + if exists { + return fmt.Errorf("云账户名称 %s 在 %s 下已存在", req.Name, treeUtils.GetProviderName(account.Provider)) } - account.AccessKey = encryptedAccessKey } - // 如果需要更新 SecretKey - if req.SecretKey != "" { - encryptedSecretKey, err := treeUtils.EncryptPassword(req.SecretKey) + // 如果需要更新区域,验证区域列表 + var normalizedRegions []model.CreateCloudAccountRegionItem + if len(req.Regions) > 0 { + normalizedRegions, err = treeUtils.ValidateAndNormalizeRegions(req.Regions) if err != nil { - s.logger.Error("加密SecretKey失败", zap.Error(err)) - return fmt.Errorf("加密SecretKey失败: %w", err) + return fmt.Errorf("区域验证失败: %w", err) } - account.SecretKey = encryptedSecretKey } - if err := s.dao.Update(ctx, account); err != nil { - s.logger.Error("更新云账户失败", zap.Error(err)) + // 使用事务更新云账户和区域 + if err := s.dao.CreateWithTransaction(ctx, func(tx interface{}) error { + // 构建更新对象和字段列表 + updateAccount := &model.CloudAccount{ + Model: model.Model{ID: req.ID}, + } + updateFields := make([]string, 0) + + // 基本信息字段 + if req.Name != "" { + updateAccount.Name = req.Name + updateFields = append(updateFields, "name") + } + if req.AccountID != "" { + updateAccount.AccountID = req.AccountID + updateFields = append(updateFields, "account_id") + } + if req.AccountName != "" { + updateAccount.AccountName = req.AccountName + updateFields = append(updateFields, "account_name") + } + if req.AccountAlias != "" { + updateAccount.AccountAlias = req.AccountAlias + updateFields = append(updateFields, "account_alias") + } + if req.Description != "" { + updateAccount.Description = req.Description + updateFields = append(updateFields, "description") + } + + // 加密并更新 AccessKey + if req.AccessKey != "" { + encryptedAccessKey, err := treeUtils.EncryptPassword(req.AccessKey) + if err != nil { + s.logger.Error("加密AccessKey失败", zap.Error(err)) + return fmt.Errorf("加密AccessKey失败: %w", err) + } + updateAccount.AccessKey = encryptedAccessKey + updateFields = append(updateFields, "access_key") + } + + // 加密并更新 SecretKey + if req.SecretKey != "" { + encryptedSecretKey, err := treeUtils.EncryptPassword(req.SecretKey) + if err != nil { + s.logger.Error("加密SecretKey失败", zap.Error(err)) + return fmt.Errorf("加密SecretKey失败: %w", err) + } + updateAccount.SecretKey = encryptedSecretKey + updateFields = append(updateFields, "secret_key") + } + + // 更新云账户基本信息(如果有字段需要更新) + if len(updateFields) > 0 { + if err := s.dao.UpdateWithFields(ctx, updateAccount, updateFields); err != nil { + s.logger.Error("更新云账户基本信息失败", + zap.Int("id", req.ID), + zap.Strings("fields", updateFields), + zap.Error(err)) + return fmt.Errorf("更新云账户基本信息失败: %w", err) + } + } + + // 如果需要更新区域,先删除旧的区域关联,再创建新的 + if len(normalizedRegions) > 0 { + // 删除旧的区域关联 + if err := s.dao.DeleteRegionsByAccountIDInTransaction(ctx, req.ID, tx); err != nil { + s.logger.Error("删除旧区域关联失败", + zap.Int("account_id", req.ID), + zap.Error(err)) + return fmt.Errorf("删除旧区域关联失败: %w", err) + } + + // 创建新的区域关联 + for _, regionItem := range normalizedRegions { + region := &model.CloudAccountRegion{ + CloudAccountID: req.ID, + Region: regionItem.Region, + RegionName: regionItem.RegionName, + IsDefault: regionItem.IsDefault, + Description: regionItem.Description, + Status: model.CloudAccountRegionEnabled, + CreateUserID: account.CreateUserID, // 保持原创建者 + CreateUserName: account.CreateUserName, + } + + if err := s.dao.CreateRegionInTransaction(ctx, region, tx); err != nil { + s.logger.Error("创建新区域关联失败", + zap.Int("account_id", req.ID), + zap.String("region", regionItem.Region), + zap.Error(err)) + return fmt.Errorf("创建新区域关联失败: %w", err) + } + } + + s.logger.Info("成功更新云账户区域", + zap.Int("account_id", req.ID), + zap.Int("region_count", len(normalizedRegions))) + } + + s.logger.Info("成功更新云账户", + zap.Int("account_id", req.ID), + zap.String("name", account.Name), + zap.Strings("updated_fields", updateFields), + zap.Bool("regions_updated", len(normalizedRegions) > 0)) + + return nil + }); err != nil { return err } @@ -303,18 +458,9 @@ func (s *cloudAccountService) VerifyCloudAccount(ctx context.Context, req *model } // 获取默认区域用于验证凭证 - defaultRegion := "cn-hangzhou" // 默认区域 - if len(account.Regions) > 0 { - for _, region := range account.Regions { - if region.IsDefault { - defaultRegion = region.Region - break - } - } - // 如果没有找到默认区域,使用第一个区域 - if defaultRegion == "cn-hangzhou" { - defaultRegion = account.Regions[0].Region - } + defaultRegion, err := treeUtils.GetDefaultRegion(account.Regions) + if err != nil { + return fmt.Errorf("获取默认区域失败: %w", err) } // 根据 Provider 调用相应的云厂商 SDK 验证凭证 @@ -367,3 +513,163 @@ func (s *cloudAccountService) VerifyCloudAccount(ctx context.Context, req *model return nil } + +// BatchDeleteCloudAccount 批量删除云账户 +func (s *cloudAccountService) BatchDeleteCloudAccount(ctx context.Context, req *model.BatchDeleteCloudAccountReq) error { + if len(req.IDs) == 0 { + return errors.New("批量删除ID列表不能为空") + } + + // 检查所有云账户是否存在及是否有关联的云资源 + accounts, err := s.dao.GetByIDs(ctx, req.IDs) + if err != nil { + return err + } + + if len(accounts) != len(req.IDs) { + return errors.New("部分云账户不存在") + } + + // 检查是否有关联的云资源 + for _, account := range accounts { + if len(account.CloudResources) > 0 { + return fmt.Errorf("云账户 %s 下还有 %d 个云资源,无法删除", account.Name, len(account.CloudResources)) + } + } + + // 执行批量删除 + if err := s.dao.BatchDelete(ctx, req.IDs); err != nil { + s.logger.Error("批量删除云账户失败", zap.Error(err)) + return err + } + + s.logger.Info("批量删除云账户成功", zap.Ints("ids", req.IDs)) + return nil +} + +// BatchUpdateCloudAccountStatus 批量更新云账户状态 +func (s *cloudAccountService) BatchUpdateCloudAccountStatus(ctx context.Context, req *model.BatchUpdateCloudAccountStatusReq) error { + if len(req.IDs) == 0 { + return errors.New("批量更新ID列表不能为空") + } + + // 检查所有云账户是否存在 + accounts, err := s.dao.GetByIDs(ctx, req.IDs) + if err != nil { + return err + } + + if len(accounts) != len(req.IDs) { + return errors.New("部分云账户不存在") + } + + // 执行批量更新状态 + if err := s.dao.BatchUpdateStatus(ctx, req.IDs, req.Status); err != nil { + s.logger.Error("批量更新云账户状态失败", zap.Error(err)) + return err + } + + s.logger.Info("批量更新云账户状态成功", + zap.Ints("ids", req.IDs), + zap.Int8("status", int8(req.Status))) + return nil +} + +// ImportCloudAccount 导入云账户 +func (s *cloudAccountService) ImportCloudAccount(ctx context.Context, req *model.ImportCloudAccountReq, createUserID int, createUserName string) (*model.ImportCloudAccountResp, error) { + if len(req.Accounts) == 0 { + return nil, errors.New("导入账户列表不能为空") + } + + resp := &model.ImportCloudAccountResp{ + SuccessCount: 0, + FailedCount: 0, + FailedItems: make([]string, 0), + } + + // 逐个导入云账户 + for _, accountReq := range req.Accounts { + // 检查账户名称是否已存在 + exists, err := s.dao.CheckNameExists(ctx, accountReq.Name, accountReq.Provider, 0) + if err != nil { + s.logger.Error("检查账户名称是否存在失败", zap.Error(err)) + resp.FailedCount++ + resp.FailedItems = append(resp.FailedItems, fmt.Sprintf("%s (检查失败)", accountReq.Name)) + continue + } + + if exists { + s.logger.Warn("云账户已存在", zap.String("name", accountReq.Name)) + resp.FailedCount++ + resp.FailedItems = append(resp.FailedItems, fmt.Sprintf("%s (已存在)", accountReq.Name)) + continue + } + + // 创建云账户 + if err := s.CreateCloudAccount(ctx, &accountReq, createUserID, createUserName); err != nil { + s.logger.Error("导入云账户失败", + zap.String("name", accountReq.Name), + zap.Error(err)) + resp.FailedCount++ + resp.FailedItems = append(resp.FailedItems, fmt.Sprintf("%s (%s)", accountReq.Name, err.Error())) + continue + } + + resp.SuccessCount++ + } + + // 生成提示信息 + if resp.FailedCount == 0 { + resp.Message = fmt.Sprintf("成功导入 %d 个云账户", resp.SuccessCount) + } else { + resp.Message = fmt.Sprintf("成功导入 %d 个云账户,失败 %d 个", resp.SuccessCount, resp.FailedCount) + } + + s.logger.Info("云账户导入完成", + zap.Int("success", resp.SuccessCount), + zap.Int("failed", resp.FailedCount)) + + return resp, nil +} + +// ExportCloudAccount 导出云账户 +func (s *cloudAccountService) ExportCloudAccount(ctx context.Context, req *model.ExportCloudAccountReq) (interface{}, error) { + var accounts []*model.CloudAccount + var err error + + // 根据条件获取云账户列表 + if len(req.IDs) > 0 { + // 导出指定的云账户 + accounts, err = s.dao.GetByIDs(ctx, req.IDs) + if err != nil { + s.logger.Error("获取指定云账户失败", zap.Error(err)) + return nil, err + } + } else { + // 导出所有云账户(支持按云厂商过滤) + accounts, err = s.dao.GetAll(ctx, req.Provider) + if err != nil { + s.logger.Error("获取所有云账户失败", zap.Error(err)) + return nil, err + } + } + + if len(accounts) == 0 { + return nil, errors.New("没有可导出的云账户") + } + + // 根据导出格式处理数据 + format := req.Format + if format == "" { + format = "json" + } + + switch format { + case "json": + return treeUtils.ExportAsJSON(accounts), nil + case "csv": + return treeUtils.ExportAsCSV(accounts), nil + default: + return nil, fmt.Errorf("不支持的导出格式: %s,仅支持json和csv", format) + } +} diff --git a/internal/tree/service/tree_cloud_service.go b/internal/tree/service/tree_cloud_service.go index f3e42057..6bc824de 100644 --- a/internal/tree/service/tree_cloud_service.go +++ b/internal/tree/service/tree_cloud_service.go @@ -48,7 +48,9 @@ type TreeCloudService interface { GetSyncHistory(ctx context.Context, req *model.GetCloudResourceSyncHistoryReq) (model.ListResp[*model.CloudResourceSyncHistory], error) UpdateTreeCloudResource(ctx context.Context, req *model.UpdateTreeCloudResourceReq) error DeleteTreeCloudResource(ctx context.Context, req *model.DeleteTreeCloudResourceReq) error + BatchDeleteTreeCloudResource(ctx context.Context, req *model.BatchDeleteTreeCloudResourceReq) error UpdateCloudResourceStatus(ctx context.Context, req *model.UpdateCloudResourceStatusReq) error + BatchUpdateCloudResourceStatus(ctx context.Context, req *model.BatchUpdateCloudResourceStatusReq) error BindTreeCloudResource(ctx context.Context, req *model.BindTreeCloudResourceReq) error UnBindTreeCloudResource(ctx context.Context, req *model.UnBindTreeCloudResourceReq) error GetChangeLog(ctx context.Context, req *model.GetCloudResourceChangeLogReq) (model.ListResp[*model.CloudResourceChangeLog], error) @@ -435,7 +437,7 @@ func (s *treeCloudService) SyncTreeCloudResource(ctx context.Context, req *model // 更新同步历史记录 endTime := time.Now() - syncHistory.EndTime = endTime + syncHistory.EndTime = &endTime syncHistory.Duration = int(endTime.Sub(startTime).Seconds()) syncHistory.TotalCount = resp.TotalCount syncHistory.NewCount = resp.NewCount @@ -884,3 +886,85 @@ func (s *treeCloudService) GetChangeLog(ctx context.Context, req *model.GetCloud Total: total, }, nil } + +// BatchDeleteTreeCloudResource 批量删除云资源 +func (s *treeCloudService) BatchDeleteTreeCloudResource(ctx context.Context, req *model.BatchDeleteTreeCloudResourceReq) error { + if len(req.IDs) == 0 { + return errors.New("批量删除ID列表不能为空") + } + + // 检查所有云资源是否存在 + resources, err := s.dao.BatchGetByIDs(ctx, req.IDs) + if err != nil { + return err + } + + if len(resources) != len(req.IDs) { + return errors.New("部分云资源不存在") + } + + // 执行批量删除 + if err := s.dao.BatchDelete(ctx, req.IDs); err != nil { + s.logger.Error("批量删除云资源失败", zap.Error(err)) + return err + } + + // 记录删除日志 + for _, resource := range resources { + s.recordChangeLog(ctx, resource, nil, model.ChangeSourceManual, req.OperatorID, req.OperatorName) + } + + s.logger.Info("批量删除云资源成功", zap.Ints("ids", req.IDs)) + return nil +} + +// BatchUpdateCloudResourceStatus 批量更新云资源状态 +func (s *treeCloudService) BatchUpdateCloudResourceStatus(ctx context.Context, req *model.BatchUpdateCloudResourceStatusReq) error { + if len(req.IDs) == 0 { + return errors.New("批量更新ID列表不能为空") + } + + // 检查所有云资源是否存在 + resources, err := s.dao.BatchGetByIDs(ctx, req.IDs) + if err != nil { + return err + } + + if len(resources) != len(req.IDs) { + return errors.New("部分云资源不存在") + } + + // 执行批量更新状态 + if err := s.dao.BatchUpdateStatus(ctx, req.IDs, req.Status); err != nil { + s.logger.Error("批量更新云资源状态失败", zap.Error(err)) + return err + } + + // 记录状态变更日志 + for _, resource := range resources { + if resource.Status != req.Status { + changeLog := &model.CloudResourceChangeLog{ + ResourceID: resource.ID, + InstanceID: resource.InstanceID, + ChangeType: model.ChangeTypeStatusChanged, + FieldName: "status", + OldValue: fmt.Sprintf("%d", resource.Status), + NewValue: fmt.Sprintf("%d", req.Status), + ChangeSource: model.ChangeSourceManual, + OperatorID: req.OperatorID, + OperatorName: req.OperatorName, + ChangeTime: time.Now(), + } + go func(log *model.CloudResourceChangeLog) { + if err := s.dao.CreateChangeLog(context.Background(), log); err != nil { + s.logger.Error("记录状态变更日志失败", zap.Error(err)) + } + }(changeLog) + } + } + + s.logger.Info("批量更新云资源状态成功", + zap.Ints("ids", req.IDs), + zap.Int8("status", int8(req.Status))) + return nil +} diff --git a/internal/tree/utils/cloud_account_util.go b/internal/tree/utils/cloud_account_util.go index bd03831a..f1f016c1 100644 --- a/internal/tree/utils/cloud_account_util.go +++ b/internal/tree/utils/cloud_account_util.go @@ -27,6 +27,7 @@ package utils import ( "context" + "errors" "fmt" "github.com/GoSimplicity/AI-CloudOps/internal/model" @@ -78,3 +79,151 @@ func VerifyGCPCredentials(ctx context.Context, req *model.VerifyCloudCredentials logger.Warn("GCP凭证验证功能暂未实现") return fmt.Errorf("GCP凭证验证功能暂未实现") } + +// GetDefaultRegion 从云账户的区域列表中获取默认区域 +// 返回默认区域的Region字符串,如果没有找到默认区域则返回第一个区域 +// 如果区域列表为空,返回空字符串和错误 +func GetDefaultRegion(regions []*model.CloudAccountRegion) (string, error) { + if len(regions) == 0 { + return "", errors.New("云账户没有配置区域信息") + } + + // 查找默认区域 + for _, region := range regions { + if region.IsDefault { + return region.Region, nil + } + } + + // 如果没有找到默认区域,返回第一个区域 + return regions[0].Region, nil +} + +// SanitizeCloudAccount 清理云账户敏感信息(双重保险) +// 虽然AccessKey和SecretKey已经设置了json:"-"标签,但这个方法提供额外的安全保障 +func SanitizeCloudAccount(account *model.CloudAccount) { + if account == nil { + return + } + // 清空敏感信息 + account.AccessKey = "" + account.SecretKey = "" +} + +// SanitizeCloudAccounts 批量清理云账户敏感信息 +func SanitizeCloudAccounts(accounts []*model.CloudAccount) { + for _, account := range accounts { + SanitizeCloudAccount(account) + } +} + +// ValidateAndNormalizeRegions 验证和规范化区域列表 +// 检查区域是否为空、是否重复、默认区域是否唯一 +// 如果没有指定默认区域,会将第一个区域设置为默认 +// 返回规范化后的区域列表(新切片),不修改传入的切片 +func ValidateAndNormalizeRegions(regions []model.CreateCloudAccountRegionItem) ([]model.CreateCloudAccountRegionItem, error) { + // 验证区域列表不为空 + if len(regions) == 0 { + return nil, errors.New("必须至少指定一个区域") + } + + // 创建规范化后的区域列表副本 + normalized := make([]model.CreateCloudAccountRegionItem, len(regions)) + copy(normalized, regions) + + // 检查是否有重复的区域 + regionMap := make(map[string]bool) + var defaultCount int + for i := range normalized { + // 检查重复 + if regionMap[normalized[i].Region] { + return nil, fmt.Errorf("区域 %s 重复", normalized[i].Region) + } + regionMap[normalized[i].Region] = true + + // 统计默认区域数量 + if normalized[i].IsDefault { + defaultCount++ + } + } + + // 确保只有一个默认区域 + if defaultCount == 0 { + // 如果没有指定默认区域,则设置第一个为默认 + normalized[0].IsDefault = true + } else if defaultCount > 1 { + return nil, errors.New("只能设置一个默认区域") + } + + return normalized, nil +} + +// ExportAsJSON 导出为 JSON 格式 +func ExportAsJSON(accounts []*model.CloudAccount) interface{} { + // 为了安全,不导出敏感信息(AccessKey、SecretKey) + exportAccounts := make([]model.ExportAccount, 0, len(accounts)) + for _, account := range accounts { + regions := make([]model.ExportRegion, 0, len(account.Regions)) + for _, region := range account.Regions { + regions = append(regions, model.ExportRegion{ + Region: region.Region, + RegionName: region.RegionName, + IsDefault: region.IsDefault, + Description: region.Description, + }) + } + + exportAccounts = append(exportAccounts, model.ExportAccount{ + ID: account.ID, + Name: account.Name, + Provider: account.Provider, + ProviderName: GetProviderName(account.Provider), + AccountID: account.AccountID, + AccountName: account.AccountName, + AccountAlias: account.AccountAlias, + Description: account.Description, + Status: int8(account.Status), + Regions: regions, + CreatedAt: account.CreatedAt.Format("2006-01-02 15:04:05"), + }) + } + + return exportAccounts +} + +// ExportAsCSV 导出为 CSV 格式 +func ExportAsCSV(accounts []*model.CloudAccount) [][]string { + csvData := [][]string{ + {"ID", "名称", "云厂商", "账号ID", "账号名称", "账号别名", "描述", "状态", "区域列表", "创建时间"}, + } + + for _, account := range accounts { + regions := "" + for i, region := range account.Regions { + if i > 0 { + regions += ";" + } + regions += fmt.Sprintf("%s(%s)", region.Region, region.RegionName) + } + + status := "禁用" + if account.Status == model.CloudAccountEnabled { + status = "启用" + } + + csvData = append(csvData, []string{ + fmt.Sprintf("%d", account.ID), + account.Name, + GetProviderName(account.Provider), + account.AccountID, + account.AccountName, + account.AccountAlias, + account.Description, + status, + regions, + account.CreatedAt.Format("2006-01-02 15:04:05"), + }) + } + + return csvData +} diff --git a/internal/tree/utils/common_util.go b/internal/tree/utils/common_util.go index 6a455243..32c1323f 100644 --- a/internal/tree/utils/common_util.go +++ b/internal/tree/utils/common_util.go @@ -230,3 +230,23 @@ func getGCPAvailableRegions(ctx context.Context, accessKey, secretKey string, lo // TODO: 实现GCP SDK调用 return GetAvailableRegionsByProviderWithoutCredentials(model.ProviderGCP), nil } + +// GetProviderName 获取云厂商名称 +func GetProviderName(provider model.CloudProvider) string { + switch provider { + case model.ProviderAliyun: + return "阿里云" + case model.ProviderTencent: + return "腾讯云" + case model.ProviderAWS: + return "AWS" + case model.ProviderHuawei: + return "华为云" + case model.ProviderAzure: + return "Azure" + case model.ProviderGCP: + return "Google Cloud" + default: + return "未知" + } +}