feat: 服务器批量导入全栈实现 + SQL审计模型修复 + 审批流程增强

服务器批量导入:
- 后端 BatchCreateServers handler: 逐条校验/查重/GB→MB转换/错误明细
- 前端 CSV 解析器 + IP校验 + 模板下载 + 真实API导入
- 删除孤立占位组件 BatchImport.tsx

SQL审计:
- 修复 TableName 字段与 GORM 接口冲突 → TargetTable
- 补全路由注册 + AutoMigrate + 种子数据

审批流程增强 + 告警/菜单微调

// Logan.Li
This commit is contained in:
attacker committed 2026-06-15 02:12:05 +08:00
1 parent 6545597412
commit 77c33a6fee
16 files changed
+863 -573

No files matched your search

+76
View File
@@ -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 更新指定服务器信息
+5 -5
View File
@@ -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
+19
View File
@@ -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)
+1 -1
View File
@@ -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"`
}
+28
View File
@@ -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"`
+1 -1
View File
@@ -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