新增备份监控与定时配置

This commit is contained in:
yml2213
2026-08-16 23:53:50 +08:00
parent 66cb03ebce
commit f67ee12d72
19 changed files with 612 additions and 3 deletions
@@ -0,0 +1,59 @@
package backupmonitor
import (
"encoding/json"
"errors"
"net/http"
"os"
"hfb_sys/backend/pkg/response"
"github.com/gin-gonic/gin"
)
type Status struct {
GeneratedAt string `json:"generated_at"`
Health string `json:"health"`
Schedule struct {
Enabled string `json:"enabled"`
FullHour string `json:"full_hour"`
FullMinute string `json:"full_minute"`
BinlogIntervalMinutes string `json:"binlog_interval_minutes"`
} `json:"schedule"`
LastFull struct {
LocalComplete string `json:"local_complete"`
RemoteComplete string `json:"remote_complete"`
} `json:"last_full"`
LastBinlog struct {
File string `json:"file"`
RemoteDir string `json:"remote_dir"`
} `json:"last_binlog"`
LastJob struct {
Type string `json:"type"`
Status string `json:"status"`
At string `json:"at"`
Message string `json:"message"`
} `json:"last_job"`
}
type Handler struct{ path string }
func NewHandler(path string) *Handler { return &Handler{path: path} }
func (h *Handler) Status(c *gin.Context) {
content, err := os.ReadFile(h.path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
response.Error(c, http.StatusServiceUnavailable, "backup_status_unavailable", "备份状态尚未生成")
return
}
response.Error(c, http.StatusInternalServerError, "backup_status_unavailable", "无法读取备份状态")
return
}
var status Status
if err := json.Unmarshal(content, &status); err != nil {
response.Error(c, http.StatusServiceUnavailable, "backup_status_invalid", "备份状态文件格式无效")
return
}
response.OK(c, status)
}
@@ -17,6 +17,14 @@ type UpdateRequest struct {
Description string `json:"description"`
}
// BackupScheduleDTO 仅描述调度时间,不包含备份目录、OSS 地址或任何密钥。
type BackupScheduleDTO struct {
Enabled bool `json:"enabled"`
FullHour int `json:"full_hour"`
FullMinute int `json:"full_minute"`
BinlogIntervalMinutes int `json:"binlog_interval_minutes"`
}
type HomeAnnouncementsDTO struct {
Items []string `json:"items"`
}
@@ -110,6 +110,34 @@ func (h *Handler) Update(c *gin.Context) {
response.OK(c, item)
}
func (h *Handler) BackupSchedule(c *gin.Context) {
schedule, err := h.service.BackupSchedule(c.Request.Context())
if err != nil {
writeConfigError(c, err)
return
}
response.OK(c, schedule)
}
func (h *Handler) UpdateBackupSchedule(c *gin.Context) {
adminID, ok := currentAdminID(c)
if !ok {
response.Unauthorized(c, "缺少管理员上下文")
return
}
var req BackupScheduleDTO
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "备份定时配置格式无效")
return
}
schedule, err := h.service.UpdateBackupSchedule(c.Request.Context(), adminID, req, auditMeta(c))
if err != nil {
writeConfigError(c, err)
return
}
response.OK(c, schedule)
}
func auditMeta(c *gin.Context) AuditMeta {
return AuditMeta{
IP: c.ClientIP(),
@@ -51,6 +51,10 @@ var defaultConfigs = []defaultConfig{
{Key: cooperationFeedbackConfigKey, Value: defaultCooperationFeedbackConfigValue(), Description: "首页 合作与反馈入口配置 JSON(标题/副标题/展示图片)"},
{Key: publishOptionsConfigKey, Value: defaultPublishOptionsConfigValue(), Description: "发布页选项配置 JSON"},
{Key: salePriceConfigKey, Value: defaultSalePriceConfigValue(), Description: "内部出售定价规则 JSON"},
{Key: "backup.schedule_enabled", Value: "true", Description: "在线备份定时任务是否启用"},
{Key: "backup.full_hour", Value: "4", Description: "每日全量备份小时(Asia/Shanghai"},
{Key: "backup.full_minute", Value: "30", Description: "每日全量备份分钟(Asia/Shanghai"},
{Key: "backup.binlog_interval_minutes", Value: "1", Description: "binlog 归档间隔分钟(1-5"},
}
var adminVisibleConfigKeys = []string{
@@ -3,6 +3,7 @@ package systemconfig
import (
"context"
"errors"
"strconv"
)
var (
@@ -24,3 +25,31 @@ func (s *Service) List(ctx context.Context) ([]ConfigDTO, error) {
}
return s.repo.List(ctx)
}
func (s *Service) BackupSchedule(ctx context.Context) (*BackupScheduleDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
values := map[string]string{}
for _, item := range defaultConfigs {
if item.Key == "backup.schedule_enabled" || item.Key == "backup.full_hour" || item.Key == "backup.full_minute" || item.Key == "backup.binlog_interval_minutes" {
values[item.Key] = item.Value
}
}
for key := range values {
value, err := s.repo.FindValue(ctx, key)
if err != nil {
return nil, err
}
values[key] = value
}
hour, _ := strconv.Atoi(values["backup.full_hour"])
minute, _ := strconv.Atoi(values["backup.full_minute"])
interval, _ := strconv.Atoi(values["backup.binlog_interval_minutes"])
return &BackupScheduleDTO{
Enabled: values["backup.schedule_enabled"] == "true",
FullHour: hour,
FullMinute: minute,
BinlogIntervalMinutes: interval,
}, nil
}
@@ -12,6 +12,10 @@ func (s *Service) Update(ctx context.Context, actorID uint64, key string, req Up
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
// 备份定时只能通过独立接口修改,避免拥有普通系统配置权限的管理员改变备份策略。
if strings.HasPrefix(key, "backup.") {
return nil, ErrInvalidConfig
}
if key == "" || (req.Value == "" && key != "integration.paddle_ocr_token") {
return nil, ErrInvalidConfig
}
@@ -34,3 +38,25 @@ func (s *Service) Update(ctx context.Context, actorID uint64, key string, req Up
}
return s.repo.Update(ctx, actorID, key, req, meta)
}
func (s *Service) UpdateBackupSchedule(ctx context.Context, actorID uint64, req BackupScheduleDTO, meta AuditMeta) (*BackupScheduleDTO, error) {
if s.repo == nil || req.FullHour < 0 || req.FullHour > 23 || req.FullMinute < 0 || req.FullMinute > 59 || req.BinlogIntervalMinutes < 1 || req.BinlogIntervalMinutes > 5 {
return nil, ErrInvalidConfig
}
updates := []struct {
key string
value string
description string
}{
{"backup.schedule_enabled", strconv.FormatBool(req.Enabled), "在线备份定时任务是否启用"},
{"backup.full_hour", strconv.Itoa(req.FullHour), "每日全量备份小时(Asia/Shanghai"},
{"backup.full_minute", strconv.Itoa(req.FullMinute), "每日全量备份分钟(Asia/Shanghai"},
{"backup.binlog_interval_minutes", strconv.Itoa(req.BinlogIntervalMinutes), "binlog 归档间隔分钟(1-5"},
}
for _, item := range updates {
if _, err := s.repo.Update(ctx, actorID, item.key, UpdateRequest{Value: item.value, Description: item.description}, meta); err != nil {
return nil, err
}
}
return &req, nil
}