feat(audit): F1+F2 告警静默规则标记完成 + 审计日志CSV导出实现

F1 告警静默规则 [done]:
- 后端 alert_silence.go: CRUD + Toggle (onetime/daily/weekly 三模式)
- 告警引擎 isSilenced 集成 (alert_engine.go)
- 前端 SilenceRuleTab + SilenceDialog
- 路由 /alerts/silences 已注册

F2 审计日志导出 [done]:
- 重写 ExportAuditLogs: 真正的 CSV 导出 (encoding/csv + UTF-8 BOM)
- 支持 module/action/username/startTime/endTime 筛选
- 导出限制 10000 条防溢出,自动记录审计日志
- JSON 格式向后兼容 (format=json)
- 前端 exportLogs 传递查询参数

同时包含累积的前期工作:
- 目录重构 internal/ -> controller/model/router (v0.55-v0.57)
- 后端分层架构 dto/repository/service/utils
- 审计模块全功能 (终端审计/操作审计/登录日志)
- asciicast v2 录制格式
- SFTP 文件浏览器
- StatStrip 紧凑统计条
- 在线会话自动刷新
- LiveMonitor WebSocket 诊断修复

验证: go build ✅ | npm run build ✅ | Docker 部署 ✅ | 10 回归端点 ✅ | CSV 导出 ✅ | 静默 CRUD ✅
This commit is contained in:
attacker committed 2026-06-25 20:16:30 +08:00
1 parent e57718998a
commit 77c234b060
314 files changed
+10050 -4065

No files matched your search

+260
View File
@@ -0,0 +1,260 @@
package controller
import (
"encoding/json"
"net/http"
"strconv"
"time"
"ezops/model"
"github.com/gin-gonic/gin"
)
// 告警静默规则 CRUD(AlertHandler 方法接收者) // Logan.Li
// CreateSilenceRequest 创建静默规则请求
type CreateSilenceRequest struct {
Name string `json:"name" binding:"required"`
Description string `json:"description"`
RuleIDs []uint `json:"rule_ids"`
TargetIDs []uint `json:"target_ids"`
Matchers []model.AlertSilenceMatcher `json:"matchers"`
SilenceType string `json:"silence_type"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time" binding:"required"`
Weekdays []int `json:"weekdays"`
Status *int `json:"status"`
}
// UpdateSilenceRequest 更新静默规则请求
type UpdateSilenceRequest struct {
Name string `json:"name"`
Description string `json:"description"`
RuleIDs []uint `json:"rule_ids"`
TargetIDs []uint `json:"target_ids"`
Matchers []model.AlertSilenceMatcher `json:"matchers"`
SilenceType string `json:"silence_type"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
Weekdays []int `json:"weekdays"`
Status *int `json:"status"`
}
// ListSilences GET /alerts/silences
func (h *AlertHandler) ListSilences(c *gin.Context) {
var silences []model.AlertSilence
query := h.db.Order("created_at DESC")
if status := c.Query("status"); status != "" {
if status == "active" {
now := time.Now()
query = query.Where("status = ? AND start_time <= ? AND end_time >= ?", 1, now, now)
} else if s, err := strconv.Atoi(status); err == nil {
query = query.Where("status = ?", s)
}
}
query.Find(&silences)
// 解析 JSON 字段为前端友好的格式
type SilenceWithParsed struct {
model.AlertSilence
RuleIDList []uint `json:"rule_id_list"`
TargetIDList []uint `json:"target_id_list"`
MatcherList []model.AlertSilenceMatcher `json:"matcher_list"`
WeekdayList []int `json:"weekday_list"`
IsActive bool `json:"is_active"`
}
var result []SilenceWithParsed
now := time.Now()
for _, s := range silences {
item := SilenceWithParsed{AlertSilence: s}
if s.RuleIDs != "" {
json.Unmarshal([]byte(s.RuleIDs), &item.RuleIDList)
}
if s.TargetIDs != "" {
json.Unmarshal([]byte(s.TargetIDs), &item.TargetIDList)
}
if s.Matchers != "" {
json.Unmarshal([]byte(s.Matchers), &item.MatcherList)
}
if s.Weekdays != "" {
json.Unmarshal([]byte(s.Weekdays), &item.WeekdayList)
}
item.IsActive = s.Status == 1 && now.After(s.StartTime) && now.Before(s.EndTime)
result = append(result, item)
}
c.JSON(http.StatusOK, model.Response{
Code: 200,
Message: "success",
Data: model.ListResponse{List: result, Total: int64(len(result))},
})
}
// CreateSilence POST /alerts/silences
func (h *AlertHandler) CreateSilence(c *gin.Context) {
var req CreateSilenceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误: " + err.Error()})
return
}
silenceType := model.AlertSilenceOneTime
if req.SilenceType != "" {
silenceType = model.AlertSilenceType(req.SilenceType)
}
// 解析时间
var startTime, endTime time.Time
var err error
if req.StartTime != "" {
startTime, err = time.Parse(time.RFC3339, req.StartTime)
if err != nil {
startTime, err = time.ParseInLocation("2006-01-02 15:04:05", req.StartTime, time.Local)
if err != nil {
startTime = time.Now()
}
}
} else {
startTime = time.Now()
}
endTime, err = time.Parse(time.RFC3339, req.EndTime)
if err != nil {
endTime, err = time.ParseInLocation("2006-01-02 15:04:05", req.EndTime, time.Local)
if err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "结束时间格式错误"})
return
}
}
ruleIDsJSON, _ := json.Marshal(req.RuleIDs)
targetIDsJSON, _ := json.Marshal(req.TargetIDs)
matchersJSON, _ := json.Marshal(req.Matchers)
weekdaysJSON, _ := json.Marshal(req.Weekdays)
silence := model.AlertSilence{
Name: req.Name,
Description: req.Description,
RuleIDs: string(ruleIDsJSON),
TargetIDs: string(targetIDsJSON),
Matchers: string(matchersJSON),
SilenceType: silenceType,
StartTime: startTime,
EndTime: endTime,
Weekdays: string(weekdaysJSON),
Status: 1,
}
// 获取创建人 ID
if userID, exists := c.Get("userID"); exists {
if uid, ok := userID.(uint); ok {
silence.CreatedBy = uid
}
}
if err := h.db.Create(&silence).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "创建失败: " + err.Error()})
return
}
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "创建成功", Data: silence})
}
// UpdateSilence PUT /alerts/silences/:id
func (h *AlertHandler) UpdateSilence(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var req UpdateSilenceRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "请求参数错误"})
return
}
var silence model.AlertSilence
if err := h.db.First(&silence, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "静默规则不存在"})
return
}
updates := map[string]interface{}{}
if req.Name != "" {
updates["name"] = req.Name
}
if req.Description != "" {
updates["description"] = req.Description
}
if req.SilenceType != "" {
updates["silence_type"] = req.SilenceType
}
if len(req.RuleIDs) > 0 || req.RuleIDs != nil {
ruleIDsJSON, _ := json.Marshal(req.RuleIDs)
updates["rule_ids"] = string(ruleIDsJSON)
}
if len(req.TargetIDs) > 0 || req.TargetIDs != nil {
targetIDsJSON, _ := json.Marshal(req.TargetIDs)
updates["target_ids"] = string(targetIDsJSON)
}
if len(req.Matchers) > 0 || req.Matchers != nil {
matchersJSON, _ := json.Marshal(req.Matchers)
updates["matchers"] = string(matchersJSON)
}
if len(req.Weekdays) > 0 || req.Weekdays != nil {
weekdaysJSON, _ := json.Marshal(req.Weekdays)
updates["weekdays"] = string(weekdaysJSON)
}
if req.StartTime != "" {
if st, err := time.Parse(time.RFC3339, req.StartTime); err == nil {
updates["start_time"] = st
} else if st, err := time.ParseInLocation("2006-01-02 15:04:05", req.StartTime, time.Local); err == nil {
updates["start_time"] = st
}
}
if req.EndTime != "" {
if et, err := time.Parse(time.RFC3339, req.EndTime); err == nil {
updates["end_time"] = et
} else if et, err := time.ParseInLocation("2006-01-02 15:04:05", req.EndTime, time.Local); err == nil {
updates["end_time"] = et
}
}
if req.Status != nil {
updates["status"] = *req.Status
}
h.db.Model(&silence).Updates(updates)
h.db.First(&silence, id)
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "更新成功", Data: silence})
}
// DeleteSilence DELETE /alerts/silences/:id
func (h *AlertHandler) DeleteSilence(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
if err := h.db.Delete(&model.AlertSilence{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "删除失败"})
return
}
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "删除成功"})
}
// ToggleSilence POST /alerts/silences/:id/toggle
func (h *AlertHandler) ToggleSilence(c *gin.Context) {
id, _ := strconv.Atoi(c.Param("id"))
var silence model.AlertSilence
if err := h.db.First(&silence, id).Error; err != nil {
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "静默规则不存在"})
return
}
newStatus := 0
if silence.Status == 0 {
newStatus = 1
}
h.db.Model(&silence).Update("status", newStatus)
h.db.First(&silence, id)
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "切换成功", Data: silence})
}