feat: 服务器批量导入全栈实现 + SQL审计模型修复 + 审批流程增强
服务器批量导入: - 后端 BatchCreateServers handler: 逐条校验/查重/GB→MB转换/错误明细 - 前端 CSV 解析器 + IP校验 + 模板下载 + 真实API导入 - 删除孤立占位组件 BatchImport.tsx SQL审计: - 修复 TableName 字段与 GORM 接口冲突 → TargetTable - 补全路由注册 + AutoMigrate + 种子数据 审批流程增强 + 告警/菜单微调 // Logan.Li
This commit is contained in:
1 parent
6545597412
commit
77c33a6fee
16 files changed
+863
-573
No files matched your search
@@ -1,8 +1,10 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"ezops/internal/models"
|
||||
|
||||
@@ -177,6 +179,80 @@ func (h *ServerHandler) CreateServer(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// BatchCreateServers 批量导入服务器 // Logan.Li
|
||||
// 接收 {servers:[...], region_id, auth_type},逐条校验并创建,跳过重复(主机名或IP已存在)
|
||||
func (h *ServerHandler) BatchCreateServers(c *gin.Context) {
|
||||
var req models.BatchCreateServerRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, models.Response{
|
||||
Code: 400,
|
||||
Message: "请求参数错误: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
authType := strings.TrimSpace(req.AuthType)
|
||||
if authType == "" {
|
||||
authType = "password"
|
||||
}
|
||||
|
||||
result := models.BatchCreateResult{Errors: []string{}}
|
||||
for i, item := range req.Servers {
|
||||
hostname := strings.TrimSpace(item.Hostname)
|
||||
ip := strings.TrimSpace(item.IP)
|
||||
if hostname == "" || ip == "" {
|
||||
result.Failed++
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("第%d行: 主机名和IP不能为空", i+2))
|
||||
continue
|
||||
}
|
||||
|
||||
// 重复检查(主机名或 IP 已存在则跳过)
|
||||
var count int64
|
||||
h.db.Model(&models.Server{}).Where("hostname = ? OR ip = ?", hostname, ip).Count(&count)
|
||||
if count > 0 {
|
||||
result.Skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
port := item.Port
|
||||
if port == 0 {
|
||||
port = 22
|
||||
}
|
||||
username := strings.TrimSpace(item.Username)
|
||||
if username == "" {
|
||||
username = "root"
|
||||
}
|
||||
|
||||
server := models.Server{
|
||||
Hostname: hostname,
|
||||
IP: ip,
|
||||
Port: port,
|
||||
Username: username,
|
||||
AuthType: authType,
|
||||
OS: item.OS,
|
||||
CPU: item.CPU,
|
||||
Memory: item.Memory * 1024, // GB → MB
|
||||
Disk: item.Disk,
|
||||
Tags: item.Tags,
|
||||
RegionID: req.RegionID,
|
||||
Status: "stopped",
|
||||
}
|
||||
if err := h.db.Create(&server).Error; err != nil {
|
||||
result.Failed++
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("%s(%s): %s", hostname, ip, err.Error()))
|
||||
continue
|
||||
}
|
||||
result.Success++
|
||||
}
|
||||
|
||||
msg := fmt.Sprintf("导入完成: 成功 %d, 跳过 %d, 失败 %d", result.Success, result.Skipped, result.Failed)
|
||||
c.JSON(http.StatusOK, models.Response{
|
||||
Code: 200,
|
||||
Message: msg,
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateServer 更新服务器
|
||||
// @Summary 更新服务器
|
||||
// @Description 更新指定服务器信息
|
||||
|
||||
@@ -119,7 +119,7 @@ func (h *SQLAuditHandler) AnalyzeSQL(c *gin.Context) {
|
||||
SQLContent: req.SQL,
|
||||
SQLType: sqlType,
|
||||
DatabaseName: req.DatabaseName,
|
||||
TableName: tableName,
|
||||
TargetTable: tableName,
|
||||
RiskLevel: riskLevel,
|
||||
RiskRules: rulesJSON,
|
||||
Status: "pending",
|
||||
@@ -389,22 +389,22 @@ func (h *SQLAuditHandler) analyzeRisk(sqlUpper string) (string, []string) {
|
||||
}
|
||||
|
||||
// matchRule 检查SQL是否匹配规则 // Logan.Li
|
||||
func matchRule(sqlUpper string, rule models.SQLAuditRule) string {
|
||||
func matchRule(sqlUpper string, rule models.SQLAuditRule) bool {
|
||||
// 按 category 快速匹配
|
||||
if rule.Category != "" {
|
||||
if strings.Contains(sqlUpper, strings.ToUpper(rule.Category)) {
|
||||
return rule.Name
|
||||
return true
|
||||
}
|
||||
}
|
||||
// 按 pattern 正则匹配
|
||||
if rule.Pattern != "" {
|
||||
if re, err := regexp.Compile("(?i)" + rule.Pattern); err == nil {
|
||||
if re.MatchString(sqlUpper) {
|
||||
return rule.Name
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
return false
|
||||
}
|
||||
|
||||
// severityRank 风险等级排序 // Logan.Li
|
||||
|
||||
@@ -110,6 +110,7 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
|
||||
servers.PUT("/:id", serverHandler.UpdateServer)
|
||||
servers.DELETE("/:id", serverHandler.DeleteServer)
|
||||
servers.POST("/batch-delete", serverHandler.BatchDeleteServers)
|
||||
servers.POST("/batch", serverHandler.BatchCreateServers)
|
||||
}
|
||||
|
||||
// 云账号路由
|
||||
@@ -351,6 +352,24 @@ func Register(r *gin.Engine, db *gorm.DB, log *zap.Logger, cfg *config.Config) {
|
||||
audit.POST("/logs/clear", auditHandler.ClearAuditLogs)
|
||||
}
|
||||
|
||||
// SQL审计路由 // Logan.Li
|
||||
sqlAuditHandler := handlers.NewSQLAuditHandler(db)
|
||||
sqlAudit := authorized.Group("/sql-audit")
|
||||
{
|
||||
sqlAudit.GET("/records", sqlAuditHandler.ListRecords)
|
||||
sqlAudit.GET("/records/:id", sqlAuditHandler.GetRecord)
|
||||
sqlAudit.POST("/analyze", sqlAuditHandler.AnalyzeSQL)
|
||||
sqlAudit.POST("/records/:id/approve", sqlAuditHandler.ApproveRecord)
|
||||
sqlAudit.POST("/records/:id/reject", sqlAuditHandler.RejectRecord)
|
||||
sqlAudit.POST("/records/:id/execute", sqlAuditHandler.ExecuteRecord)
|
||||
sqlAudit.DELETE("/records/:id", sqlAuditHandler.DeleteRecord)
|
||||
sqlAudit.GET("/rules", sqlAuditHandler.ListRules)
|
||||
sqlAudit.POST("/rules", sqlAuditHandler.CreateRule)
|
||||
sqlAudit.PUT("/rules/:id", sqlAuditHandler.UpdateRule)
|
||||
sqlAudit.DELETE("/rules/:id", sqlAuditHandler.DeleteRule)
|
||||
sqlAudit.POST("/rules/:id/toggle", sqlAuditHandler.ToggleRule)
|
||||
}
|
||||
|
||||
// Agent 上报路由(无需 JWT)
|
||||
agentHandler := handlers.NewAgentHandler(db)
|
||||
taskHandler := handlers.NewAgentTaskHandler(db)
|
||||
|
||||
@@ -80,7 +80,7 @@ type CreateApprovalTemplateRequest struct {
|
||||
Name string `json:"name" binding:"required"`
|
||||
Code string `json:"code" binding:"required"`
|
||||
Description string `json:"description"`
|
||||
Type string `json:"type" binding:"required,oneof=release server permission"`
|
||||
Type string `json:"type" binding:"required,oneof=server_apply app_deploy"`
|
||||
FormSchema string `json:"form_schema"`
|
||||
FlowConfig string `json:"flow_config"`
|
||||
}
|
||||
|
||||
@@ -172,6 +172,34 @@ type UpdateServerRequest struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// BatchServerItem 批量导入单条服务器 // Logan.Li
|
||||
type BatchServerItem struct {
|
||||
Hostname string `json:"hostname"`
|
||||
IP string `json:"ip"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username"`
|
||||
OS string `json:"os"`
|
||||
CPU int `json:"cpu"`
|
||||
Memory int64 `json:"memory"` // GB(后端转 MB 存储)
|
||||
Disk int64 `json:"disk"` // GB
|
||||
Tags string `json:"tags"`
|
||||
}
|
||||
|
||||
// BatchCreateServerRequest 批量导入服务器请求 // Logan.Li
|
||||
type BatchCreateServerRequest struct {
|
||||
Servers []BatchServerItem `json:"servers"`
|
||||
RegionID *uint `json:"region_id"`
|
||||
AuthType string `json:"auth_type"`
|
||||
}
|
||||
|
||||
// BatchCreateResult 批量导入结果 // Logan.Li
|
||||
type BatchCreateResult struct {
|
||||
Success int `json:"success"`
|
||||
Skipped int `json:"skipped"`
|
||||
Failed int `json:"failed"`
|
||||
Errors []string `json:"errors"`
|
||||
}
|
||||
|
||||
// ServerListResponse 服务器列表响应
|
||||
type ServerListResponse struct {
|
||||
List []Server `json:"list"`
|
||||
|
||||
@@ -12,7 +12,7 @@ type SQLAuditRecord struct {
|
||||
SQLContent string `gorm:"type:text;not null" json:"sql_content"`
|
||||
SQLType string `gorm:"size:50" json:"sql_type"` // DDL/DML/DQL/DCL
|
||||
DatabaseName string `gorm:"size:255" json:"database_name"`
|
||||
TableName string `gorm:"size:255" json:"table_name"`
|
||||
TargetTable string `gorm:"column:table_name;size:255" json:"table_name"`
|
||||
RiskLevel string `gorm:"size:20;default:safe" json:"risk_level"` // high/medium/low/safe
|
||||
RiskRules string `gorm:"type:text" json:"risk_rules"` // JSON 数组
|
||||
Status string `gorm:"size:20;default:pending" json:"status"` // pending/approved/rejected/executed
|
||||
|
||||
Reference in new issue
Block a user