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
@@ -55,4 +55,59 @@ func seedData(db *gorm.DB) {
|
||||
zap.L().Info("seeded default LDAP config (8.217.205.92:389)")
|
||||
}
|
||||
}
|
||||
|
||||
// 3. 创建默认审批模板(资源申请)
|
||||
var tplCount int64
|
||||
db.Model(&models.ApprovalTemplate{}).Count(&tplCount)
|
||||
if tplCount == 0 {
|
||||
templates := []models.ApprovalTemplate{
|
||||
{
|
||||
Name: "云服务器申请",
|
||||
Code: "server_apply",
|
||||
Description: "申请云服务器资源(ECS/EC2),包含规格、数量、用途等信息",
|
||||
Type: "server_apply",
|
||||
FormSchema: `{
|
||||
"fields": [
|
||||
{"key": "provider", "label": "云厂商", "type": "select", "options": ["阿里云","腾讯云","华为云","AWS"], "required": true},
|
||||
{"key": "region", "label": "地域", "type": "text", "placeholder": "如 cn-hangzhou", "required": true},
|
||||
{"key": "spec", "label": "规格", "type": "select", "options": ["2C4G","4C8G","8C16G","16C32G","32C64G"], "required": true},
|
||||
{"key": "disk", "label": "系统盘(GB)", "type": "number", "default": 40, "required": true},
|
||||
{"key": "count", "label": "数量(台)", "type": "number", "default": 1, "required": true},
|
||||
{"key": "duration", "label": "时长", "type": "select", "options": ["1个月","3个月","6个月","1年"], "required": true},
|
||||
{"key": "purpose", "label": "用途说明", "type": "textarea", "required": true}
|
||||
]
|
||||
}`,
|
||||
FlowConfig: `[{"name":"直接主管","approver_role":"team_lead"},{"name":"运维负责人","approver_role":"ops_manager"}]`,
|
||||
Enabled: true,
|
||||
},
|
||||
{
|
||||
Name: "应用部署申请",
|
||||
Code: "app_deploy",
|
||||
Description: "申请新应用部署,包含 Git 地址、端口、Nacos 等配置信息",
|
||||
Type: "app_deploy",
|
||||
FormSchema: `{
|
||||
"fields": [
|
||||
{"key": "app_name", "label": "应用名称", "type": "text", "placeholder": "如 user-service", "required": true},
|
||||
{"key": "git_url", "label": "Git 地址", "type": "text", "placeholder": "如 https://github.com/org/repo.git", "required": true},
|
||||
{"key": "branch", "label": "分支", "type": "text", "default": "main", "required": true},
|
||||
{"key": "language", "label": "开发语言", "type": "select", "options": ["Java","Go","Python","Node.js","C++"], "required": true},
|
||||
{"key": "http_port", "label": "HTTP 端口", "type": "number", "placeholder": "如 8080", "required": false},
|
||||
{"key": "grpc_port", "label": "gRPC 端口", "type": "number", "placeholder": "如 9090", "required": false},
|
||||
{"key": "nacos_addr", "label": "Nacos 地址", "type": "text", "placeholder": "如 nacos.opsx.vip:8848", "required": true},
|
||||
{"key": "nacos_namespace", "label": "Nacos 命名空间", "type": "text", "placeholder": "如 production", "required": false},
|
||||
{"key": "env", "label": "部署环境", "type": "select", "options": ["dev","staging","prod"], "required": true},
|
||||
{"key": "replicas", "label": "副本数", "type": "number", "default": 2, "required": true},
|
||||
{"key": "remark", "label": "备注", "type": "textarea", "required": false}
|
||||
]
|
||||
}`,
|
||||
FlowConfig: `[{"name":"技术负责人","approver_role":"tech_lead"},{"name":"运维负责人","approver_role":"ops_manager"}]`,
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
if err := db.Create(&templates).Error; err != nil {
|
||||
zap.L().Warn("failed to seed approval templates", zap.Error(err))
|
||||
} else {
|
||||
zap.L().Info("seeded default approval templates (server_apply + app_deploy)")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -100,5 +100,7 @@ func AutoMigrate(db *gorm.DB) error {
|
||||
&models.AnsibleInventory{},
|
||||
&models.LogSource{},
|
||||
&models.LogEntry{},
|
||||
&models.SQLAuditRecord{},
|
||||
&models.SQLAuditRule{},
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user