feat(M4): 生命周期 + 审计 — 到期锁定/回收 cron、审计查询/CSV 导出/每日归档、settings 动态策略

This commit is contained in:
2026-08-30 10:17:22 +08:00
parent 1ea18490e0
commit c0e2ff975a
18 changed files with 1088 additions and 75 deletions
+85
View File
@@ -0,0 +1,85 @@
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"),
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)
}