Files
usernode/internal/api/audit.go
T
cao.wangrenbo 7340a3dd48 feat(api): 审计查询支持按结果筛选
- AuditFilter 增加 Result 字段,Query 按 result 精确过滤
- GET /audit 接口解析 result 查询参数(success/failed)
2026-08-30 17:27:49 +08:00

104 lines
3.1 KiB
Go

package api
import (
"net/http"
"strconv"
"time"
"github.com/gin-gonic/gin"
"ws_usernode/internal/config"
"ws_usernode/internal/service"
)
// AuditHandler 审计接口(admin):查询 + 手动 CSV 导出(PLAN F6)。
type AuditHandler struct {
svc *service.AuditService
cfg *config.Config
}
// NewAuditHandler 创建审计 handler。
func NewAuditHandler(svc *service.AuditService, cfg *config.Config) *AuditHandler {
return &AuditHandler{svc: svc, cfg: cfg}
}
// parseTime 解析可选的时间参数(RFC3339)。
func parseTime(v string) (*time.Time, error) {
if v == "" {
return nil, nil
}
t, err := time.Parse(time.RFC3339, v)
if err != nil {
return nil, err
}
return &t, nil
}
// List GET /audit —— 审计查询(操作者/动作/资源/时间范围/分页)。
func (h *AuditHandler) List(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
since, err := parseTime(c.Query("since"))
if err != nil {
fail(c, http.StatusBadRequest, "since 需为 RFC3339 时间")
return
}
until, err := parseTime(c.Query("until"))
if err != nil {
fail(c, http.StatusBadRequest, "until 需为 RFC3339 时间")
return
}
rows, total, err := h.svc.Query(c.Request.Context(), service.AuditFilter{
ActorName: c.Query("actor"),
Action: c.Query("action"),
ResourceType: c.Query("resource_type"),
ResourceID: c.Query("resource_id"),
Result: c.Query("result"),
Since: since,
Until: until,
Page: page,
PageSize: pageSize,
})
if err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
}
ok(c, gin.H{"total": total, "items": rows})
}
// Export GET /audit/export —— 手动 CSV 导出(可选时间范围)。
func (h *AuditHandler) Export(c *gin.Context) {
since, err := parseTime(c.Query("since"))
if err != nil {
fail(c, http.StatusBadRequest, "since 需为 RFC3339 时间")
return
}
until, err := parseTime(c.Query("until"))
if err != nil {
fail(c, http.StatusBadRequest, "until 需为 RFC3339 时间")
return
}
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", `attachment; filename="audit-`+time.Now().Format("2006-01-02")+`.csv"`)
// 已开始写响应体后无法再返回 JSON 错误;查询失败在此吞掉(导出为管理操作,
// 失败由审计归档兜底),正常路径写入 CSV。
_ = h.svc.ExportCSV(c.Request.Context(), c.Writer, since, until)
}
// MyAudit GET /me/audit —— 外部用户查看与自己相关的审计痕迹(PLAN §8 个人中心)。
func (h *AuditHandler) MyAudit(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
sess := sessionFrom(c)
if sess == nil {
fail(c, http.StatusUnauthorized, "未登录")
return
}
rows, total, err := h.svc.QueryByUser(c.Request.Context(), sess.RefID, page, pageSize)
if err != nil {
fail(c, http.StatusInternalServerError, err.Error())
return
}
ok(c, gin.H{"total": total, "items": rows})
}