mirror of
https://github.com/Awuqing/BackupX.git
synced 2026-06-13 05:39:35 +08:00
新增合规报表:ReportService 逐任务聚合备份合规证据(成功率/最近成功/SLA 判定/加密/受保护量)+ JSON/CSV API;前端新增 /reports 页面(汇总卡片+明细表+CSV 导出)。后端 go test、前端 tsc+vite、端到端路由验证均通过。
96 lines
2.5 KiB
Go
96 lines
2.5 KiB
Go
package http
|
||
|
||
import (
|
||
"encoding/csv"
|
||
"fmt"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"backupx/server/internal/service"
|
||
"backupx/server/pkg/response"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
type ReportHandler struct {
|
||
service *service.ReportService
|
||
}
|
||
|
||
func NewReportHandler(reportService *service.ReportService) *ReportHandler {
|
||
return &ReportHandler{service: reportService}
|
||
}
|
||
|
||
func reportDays(c *gin.Context) int {
|
||
days := 30
|
||
if v := strings.TrimSpace(c.Query("days")); v != "" {
|
||
if parsed, err := strconv.Atoi(v); err == nil && parsed > 0 {
|
||
days = parsed
|
||
}
|
||
}
|
||
return days
|
||
}
|
||
|
||
// Compliance 返回 JSON 合规报表(按任务的备份合规证据 + 汇总)。
|
||
func (h *ReportHandler) Compliance(c *gin.Context) {
|
||
payload, err := h.service.ComplianceReport(c.Request.Context(), reportDays(c))
|
||
if err != nil {
|
||
response.Error(c, err)
|
||
return
|
||
}
|
||
response.Success(c, payload)
|
||
}
|
||
|
||
// ComplianceCSV 把合规报表导出为 CSV(供审计归档)。带 UTF-8 BOM 以便 Excel 正确识别中文。
|
||
func (h *ReportHandler) ComplianceCSV(c *gin.Context) {
|
||
report, err := h.service.ComplianceReport(c.Request.Context(), reportDays(c))
|
||
if err != nil {
|
||
response.Error(c, err)
|
||
return
|
||
}
|
||
filename := fmt.Sprintf("backupx-compliance-%s.csv", report.GeneratedAt.Format("20060102-150405"))
|
||
c.Header("Content-Type", "text/csv; charset=utf-8")
|
||
c.Header("Content-Disposition", "attachment; filename="+filename)
|
||
_, _ = c.Writer.WriteString("\ufeff") // UTF-8 BOM
|
||
w := csv.NewWriter(c.Writer)
|
||
_ = w.Write([]string{
|
||
"任务ID", "任务名", "类型", "启用", "节点", "加密", "保留天数", "SLA(RPO小时)",
|
||
"周期内运行", "成功", "失败", "成功率", "最近状态", "最近运行(UTC)", "最近成功(UTC)", "保护字节数", "合规判定",
|
||
})
|
||
for _, row := range report.Tasks {
|
||
_ = w.Write([]string{
|
||
strconv.FormatUint(uint64(row.TaskID), 10),
|
||
row.TaskName,
|
||
row.Type,
|
||
boolCN(row.Enabled),
|
||
row.NodeName,
|
||
boolCN(row.Encrypted),
|
||
strconv.Itoa(row.RetentionDays),
|
||
strconv.Itoa(row.SLAHoursRPO),
|
||
strconv.Itoa(row.TotalRuns),
|
||
strconv.Itoa(row.Successes),
|
||
strconv.Itoa(row.Failures),
|
||
fmt.Sprintf("%.2f%%", row.SuccessRate*100),
|
||
row.LastStatus,
|
||
fmtTimePtr(row.LastRunAt),
|
||
fmtTimePtr(row.LastSuccessAt),
|
||
strconv.FormatInt(row.ProtectedBytes, 10),
|
||
row.Risk,
|
||
})
|
||
}
|
||
w.Flush()
|
||
}
|
||
|
||
func boolCN(b bool) string {
|
||
if b {
|
||
return "是"
|
||
}
|
||
return "否"
|
||
}
|
||
|
||
func fmtTimePtr(t *time.Time) string {
|
||
if t == nil {
|
||
return ""
|
||
}
|
||
return t.UTC().Format("2006-01-02 15:04:05")
|
||
}
|