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

+416
View File
@@ -0,0 +1,416 @@
package controller
import (
"bytes"
"encoding/csv"
"fmt"
"net/http"
"strconv"
"time"
"ezops/model"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
// AuditHandler 审计日志处理器
type AuditHandler struct {
db *gorm.DB
}
// NewAuditHandler 创建审计日志处理器
func NewAuditHandler(db *gorm.DB) *AuditHandler {
return &AuditHandler{db: db}
}
// ListAuditLogs 获取审计日志列表
// @Summary 获取审计日志列表
// @Description 获取审计日志列表,支持分页和筛选
// @Tags 审计日志
// @Accept json
// @Produce json
// @Param page query int false "页码" default(1)
// @Param pageSize query int false "每页数量" default(20)
// @Param module query string false "模块"
// @Param action query string false "操作类型"
// @Param username query string false "用户名"
// @Param startTime query string false "开始时间"
// @Param endTime query string false "结束时间"
// @Success 200 {object} model.AuditLogListResponse
// @Router /api/v1/audit/logs [get]
func (h *AuditHandler) ListAuditLogs(c *gin.Context) {
var req model.AuditLogQuery
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: err.Error()})
return
}
// 设置默认值
if req.Page <= 0 {
req.Page = 1
}
if req.PageSize <= 0 {
req.PageSize = 20
}
// 构建查询
query := h.db.Model(&model.AuditLog{})
if req.Module != "" {
query = query.Where("module = ?", req.Module)
}
if req.Action != "" {
query = query.Where("action = ?", req.Action)
}
if req.Username != "" {
query = query.Where("username LIKE ?", "%"+req.Username+"%")
}
if req.StartTime != "" {
query = query.Where("created_at >= ?", req.StartTime)
}
if req.EndTime != "" {
query = query.Where("created_at <= ?", req.EndTime)
}
// 获取总数
var total int64
if err := query.Count(&total).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"})
return
}
// 获取列表
var logs []model.AuditLog
if err := query.Order("created_at DESC").
Offset((req.Page - 1) * req.PageSize).
Limit(req.PageSize).
Find(&logs).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"})
return
}
c.JSON(http.StatusOK, model.AuditLogListResponse{
List: logs,
Total: total,
})
}
// GetAuditLogDetail 获取审计日志详情
// @Summary 获取审计日志详情
// @Description 获取指定ID的审计日志详情
// @Tags 审计日志
// @Accept json
// @Produce json
// @Param id path int true "日志ID"
// @Success 200 {object} model.AuditLog
// @Router /api/v1/audit/logs/{id} [get]
func (h *AuditHandler) GetAuditLogDetail(c *gin.Context) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的日志ID"})
return
}
var log model.AuditLog
if err := h.db.First(&log, id).Error; err != nil {
if err == gorm.ErrRecordNotFound {
c.JSON(http.StatusNotFound, model.Response{Code: 404, Message: "日志不存在"})
return
}
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "查询失败"})
return
}
c.JSON(http.StatusOK, log)
}
// GetAuditModules 获取审计模块列表
// @Summary 获取审计模块列表
// @Description 获取所有审计模块类型
// @Tags 审计日志
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/audit/modules [get]
func (h *AuditHandler) GetAuditModules(c *gin.Context) {
modules := []map[string]string{
{"value": "user", "label": "用户管理"},
{"value": "server", "label": "服务器管理"},
{"value": "kubernetes", "label": "Kubernetes"},
{"value": "alert", "label": "监控告警"},
{"value": "pipeline", "label": "应用发布"},
{"value": "automation", "label": "自动化运维"},
{"value": "approval", "label": "审批流程"},
{"value": "navigation", "label": "站内导航"},
{"value": "cloud_account", "label": "云账号管理"},
{"value": "domain", "label": "域名管理"},
}
c.JSON(http.StatusOK, model.Response{Code: 200, Data: modules}) // Logan.Li
}
// GetAuditActions 获取审计操作类型列表
// @Summary 获取审计操作类型列表
// @Description 获取所有审计操作类型
// @Tags 审计日志
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/audit/actions [get]
func (h *AuditHandler) GetAuditActions(c *gin.Context) {
actions := []map[string]string{
{"value": "create", "label": "创建"},
{"value": "update", "label": "更新"},
{"value": "delete", "label": "删除"},
{"value": "view", "label": "查看"},
{"value": "login", "label": "登录"},
{"value": "logout", "label": "登出"},
{"value": "execute", "label": "执行"},
{"value": "approve", "label": "审批"},
{"value": "reject", "label": "驳回"},
{"value": "import", "label": "导入"},
{"value": "export", "label": "导出"},
}
c.JSON(http.StatusOK, model.Response{Code: 200, Data: actions}) // Logan.Li
}
// buildAuditQuery 构建审计日志筛选查询(复用于列表和导出)
func (h *AuditHandler) buildAuditQuery(req model.AuditLogQuery) *gorm.DB {
query := h.db.Model(&model.AuditLog{})
if req.Module != "" {
query = query.Where("module = ?", req.Module)
}
if req.Action != "" {
query = query.Where("action = ?", req.Action)
}
if req.Username != "" {
query = query.Where("username LIKE ?", "%"+req.Username+"%")
}
if req.StartTime != "" {
query = query.Where("created_at >= ?", req.StartTime)
}
if req.EndTime != "" {
query = query.Where("created_at <= ?", req.EndTime)
}
return query
}
// ExportAuditLogs 导出审计日志
// @Summary 导出审计日志
// @Description 导出审计日志为CSV格式(带UTF-8 BOM,Excel可直接打开)
// @Tags 审计日志
// @Accept json
// @Produce text/csv
// @Param module query string false "模块"
// @Param action query string false "操作类型"
// @Param username query string false "用户名"
// @Param startTime query string false "开始时间"
// @Param endTime query string false "结束时间"
// @Param format query string false "导出格式" default("csv") Enums(csv, json)
// @Success 200 {file} binary
// @Router /api/v1/audit/logs/export [get]
func (h *AuditHandler) ExportAuditLogs(c *gin.Context) {
var req model.AuditLogQuery
_ = c.ShouldBindQuery(&req)
// 导出最多 10000 条,防止内存溢出
query := h.buildAuditQuery(req)
var logs []model.AuditLog
if err := query.Order("created_at DESC").Limit(10000).Find(&logs).Error; err != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "导出失败: " + err.Error()})
return
}
format := c.DefaultQuery("format", "csv")
switch format {
case "json":
// JSON 格式(向后兼容)
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "导出成功", Data: gin.H{
"count": len(logs),
"logs": logs,
}}) // Logan.Li
return
case "csv":
fallthrough
default:
// CSV 格式 — 带 UTF-8 BOM 以便 Excel 正确显示中文
var buf bytes.Buffer
buf.WriteRune('\uFEFF') // UTF-8 BOM
writer := csv.NewWriter(&buf)
_ = writer.Write([]string{"ID", "时间", "用户", "IP地址", "模块", "操作类型", "资源", "资源ID", "状态", "User-Agent", "耗时(ms)", "错误信息"})
for _, l := range logs {
statusStr := "成功"
if l.Status != 0 {
statusStr = "失败"
}
_ = writer.Write([]string{
fmt.Sprintf("%d", l.ID),
l.CreatedAt.Format("2006-01-02 15:04:05"),
l.Username,
l.IP,
l.Module,
l.Action,
l.Resource,
l.ResourceID,
statusStr,
l.UserAgent,
fmt.Sprintf("%d", l.Duration),
l.Error,
})
}
writer.Flush()
// 记录审计日志
if uid, exists := c.Get("userID"); exists {
if u, ok := uid.(uint); ok {
username, _ := c.Get("username")
un, _ := username.(string)
h.RecordAuditLog(u, un, "audit", "export", fmt.Sprintf("导出审计日志(%d条)", len(logs)), nil)
}
}
filename := fmt.Sprintf("audit-logs-%s.csv", time.Now().Format("20060102-150405"))
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
c.Data(http.StatusOK, "text/csv; charset=utf-8", buf.Bytes())
}
}
// GetAuditStatistics 获取审计统计
// @Summary 获取审计统计
// @Description 获取审计日志统计信息
// @Tags 审计日志
// @Accept json
// @Produce json
// @Param days query int false "统计天数" default(7)
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/audit/statistics [get]
func (h *AuditHandler) GetAuditStatistics(c *gin.Context) {
days, _ := strconv.Atoi(c.DefaultQuery("days", "7"))
if days <= 0 {
days = 7
}
startTime := time.Now().AddDate(0, 0, -days)
// 统计各模块的操作数量
var moduleStats []struct {
Module string `json:"module"`
Count int64 `json:"count"`
}
h.db.Model(&model.AuditLog{}).
Select("module, COUNT(*) as count").
Where("created_at >= ?", startTime).
Group("module").
Scan(&moduleStats)
// 统计各操作类型的数量
var actionStats []struct {
Action string `json:"action"`
Count int64 `json:"count"`
}
h.db.Model(&model.AuditLog{}).
Select("action, COUNT(*) as count").
Where("created_at >= ?", startTime).
Group("action").
Scan(&actionStats)
// 统计每日操作数量
var dailyStats []struct {
Date string `json:"date"`
Count int64 `json:"count"`
}
h.db.Model(&model.AuditLog{}).
Select("DATE(created_at) as date, COUNT(*) as count").
Where("created_at >= ?", startTime).
Group("DATE(created_at)").
Order("date").
Scan(&dailyStats)
// 总记录数
var totalCount int64
h.db.Model(&model.AuditLog{}).Count(&totalCount)
// 今日记录数
today := time.Now().Format("2006-01-02")
var todayCount int64
h.db.Model(&model.AuditLog{}).
Where("DATE(created_at) = ?", today).
Count(&todayCount)
c.JSON(http.StatusOK, model.Response{Code: 200, Data: gin.H{
"total_count": totalCount,
"today_count": todayCount,
"module_stats": moduleStats,
"action_stats": actionStats,
"daily_stats": dailyStats,
}}) // Logan.Li
}
// ClearAuditLogs 清理审计日志
// @Summary 清理审计日志
// @Description 清理指定日期之前的审计日志
// @Tags 审计日志
// @Accept json
// @Produce json
// @Param request body map[string]string true "清理请求"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/audit/logs/clear [post]
func (h *AuditHandler) ClearAuditLogs(c *gin.Context) {
var req struct {
BeforeDate string `json:"before_date" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的请求参数"})
return
}
// 解析日期
beforeDate, err := time.Parse("2006-01-02", req.BeforeDate)
if err != nil {
c.JSON(http.StatusBadRequest, model.Response{Code: 400, Message: "无效的日期格式"})
return
}
// 删除指定日期之前的日志
result := h.db.Where("created_at < ?", beforeDate).Delete(&model.AuditLog{})
if result.Error != nil {
c.JSON(http.StatusInternalServerError, model.Response{Code: 500, Message: "清理失败"})
return
}
// 记录审计日志
userID, _ := c.Get("userID")
username, _ := c.Get("username")
h.RecordAuditLog(userID.(uint), username.(string), "audit", "clear", "清理审计日志", gin.H{
"before_date": req.BeforeDate,
"deleted": result.RowsAffected,
})
c.JSON(http.StatusOK, model.Response{Code: 200, Message: "清理成功", Data: gin.H{
"deleted": result.RowsAffected,
}}) // Logan.Li
}
// RecordAuditLog 记录审计日志
func (h *AuditHandler) RecordAuditLog(userID uint, username, module, action, description string, details interface{}) {
uid := userID
log := model.AuditLog{
UserID: &uid,
Username: username,
Module: module,
Action: action,
Resource: description,
}
h.db.Create(&log)
}