Files
Logan 1acb0c49af feat: Agent CLI 与终端审计回放全面优化
- Agent CLI: 多轮对话、SSH 长连接、流式推理、Evolver 按用户隔离、历史侧栏
- 终端审计: 回放播放器、命令书签、筛选分页导出、命令输出统计、录制落盘修复
- 修复 session_commands 表迁移、Kill 竞态导致录制丢失、无录制会话回放提示
- 审计历史页只读(移除删除),移除回放弹框录制搜索
2026-07-01 12:13:53 +08:00

418 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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": "域名管理"},
{"value": "agent", "label": "Agent管理"},
}
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)
}