diff --git a/server/internal/app/app.go b/server/internal/app/app.go index bb3174d..5a361a4 100644 --- a/server/internal/app/app.go +++ b/server/internal/app/app.go @@ -94,6 +94,7 @@ func New(ctx context.Context, cfg config.Config, version string) (*Application, backupExecutionService := service.NewBackupExecutionService(backupTaskRepo, backupRecordRepo, storageTargetRepo, storageRegistry, backupRunnerRegistry, logHub, retentionService, configCipher, notificationService, cfg.Backup.TempDir, cfg.Backup.MaxConcurrent, cfg.Backup.Retries, cfg.Backup.BandwidthLimit) schedulerService := scheduler.NewService(backupTaskRepo, backupExecutionService, appLogger) backupTaskService.SetScheduler(schedulerService) + // 审计日志注入延迟到 auditService 创建后(见下方) backupRecordService := service.NewBackupRecordService(backupRecordRepo, backupExecutionService, logHub) dashboardService := service.NewDashboardService(backupTaskRepo, backupRecordRepo, storageTargetRepo) settingsService := service.NewSettingsService(systemConfigRepo) @@ -102,6 +103,7 @@ func New(ctx context.Context, cfg config.Config, version string) (*Application, auditLogRepo := repository.NewAuditLogRepository(db) auditService := service.NewAuditService(auditLogRepo) authService.SetAuditService(auditService) + schedulerService.SetAuditRecorder(auditService) // Database discovery databaseDiscoveryService := service.NewDatabaseDiscoveryService(backup.NewOSCommandExecutor()) diff --git a/server/internal/http/backup_record_handler.go b/server/internal/http/backup_record_handler.go index 18f3c24..d3eca44 100644 --- a/server/internal/http/backup_record_handler.go +++ b/server/internal/http/backup_record_handler.go @@ -147,6 +147,24 @@ func (h *BackupRecordHandler) Delete(c *gin.Context) { response.Success(c, gin.H{"deleted": true}) } +func (h *BackupRecordHandler) BatchDelete(c *gin.Context) { + var input struct { + IDs []uint `json:"ids" binding:"required,min=1"` + } + if err := c.ShouldBindJSON(&input); err != nil { + response.Error(c, apperror.BadRequest("BACKUP_RECORD_BATCH_INVALID", "批量删除参数不合法", err)) + return + } + deleted := 0 + for _, id := range input.IDs { + if err := h.service.Delete(c.Request.Context(), id); err == nil { + deleted++ + } + } + recordAudit(c, h.auditService, "backup_record", "batch_delete", "backup_record", "", "", fmt.Sprintf("批量删除 %d 条备份记录", deleted)) + response.Success(c, gin.H{"deleted": deleted}) +} + func buildRecordFilter(c *gin.Context) (service.BackupRecordListInput, error) { var filter service.BackupRecordListInput if taskIDValue := strings.TrimSpace(c.Query("taskId")); taskIDValue != "" { diff --git a/server/internal/http/router.go b/server/internal/http/router.go index 90c404f..33edfc7 100644 --- a/server/internal/http/router.go +++ b/server/internal/http/router.go @@ -106,6 +106,7 @@ func NewRouter(deps RouterDependencies) *gin.Engine { backupRecords.GET("/:id/logs/stream", backupRecordHandler.StreamLogs) backupRecords.GET("/:id/download", backupRecordHandler.Download) backupRecords.POST("/:id/restore", backupRecordHandler.Restore) + backupRecords.POST("/batch-delete", backupRecordHandler.BatchDelete) backupRecords.DELETE("/:id", backupRecordHandler.Delete) dashboard := api.Group("/dashboard") dashboard.Use(AuthMiddleware(deps.JWTManager)) diff --git a/server/internal/scheduler/service.go b/server/internal/scheduler/service.go index 22b44f1..65ff9e9 100644 --- a/server/internal/scheduler/service.go +++ b/server/internal/scheduler/service.go @@ -17,12 +17,18 @@ type TaskRunner interface { RunTaskByID(context.Context, uint) (*servicepkg.BackupRecordDetail, error) } +// AuditRecorder 记录审计日志(可选依赖) +type AuditRecorder interface { + Record(servicepkg.AuditEntry) +} + type Service struct { mu sync.Mutex cron *cron.Cron tasks repository.BackupTaskRepository runner TaskRunner logger *zap.Logger + audit AuditRecorder entries map[uint]cron.EntryID } @@ -31,6 +37,8 @@ func NewService(tasks repository.BackupTaskRepository, runner TaskRunner, logger return &Service{cron: cron.New(cron.WithParser(parser), cron.WithLocation(time.UTC)), tasks: tasks, runner: runner, logger: logger, entries: make(map[uint]cron.EntryID)} } +func (s *Service) SetAuditRecorder(audit AuditRecorder) { s.audit = audit } + func (s *Service) Start(ctx context.Context) error { if err := s.Reload(ctx); err != nil { return err @@ -96,9 +104,19 @@ func (s *Service) syncTaskLocked(task *model.BackupTask) error { if !task.Enabled || task.CronExpr == "" { return nil } + taskID := task.ID + taskName := task.Name entryID, err := s.cron.AddFunc(task.CronExpr, func() { - if _, runErr := s.runner.RunTaskByID(context.Background(), task.ID); runErr != nil && s.logger != nil { - s.logger.Warn("scheduled backup run failed", zap.Uint("task_id", task.ID), zap.Error(runErr)) + // 自动调度任务记录审计日志 + if s.audit != nil { + s.audit.Record(servicepkg.AuditEntry{ + Username: "system", Category: "backup_task", Action: "scheduled_run", + TargetType: "backup_task", TargetID: fmt.Sprintf("%d", taskID), + TargetName: taskName, Detail: fmt.Sprintf("定时调度触发备份任务: %s (cron: %s)", taskName, task.CronExpr), + }) + } + if _, runErr := s.runner.RunTaskByID(context.Background(), taskID); runErr != nil && s.logger != nil { + s.logger.Warn("scheduled backup run failed", zap.Uint("task_id", taskID), zap.Error(runErr)) } }) if err != nil { diff --git a/web/src/services/rclone.ts b/web/src/services/rclone.ts index 75a1ba3..cc5f7a6 100644 --- a/web/src/services/rclone.ts +++ b/web/src/services/rclone.ts @@ -14,6 +14,6 @@ export interface RcloneBackendInfo { } export async function listRcloneBackends(): Promise { - const { data } = await http.get<{ data: RcloneBackendInfo[] }>('/api/storage-targets/rclone/backends') + const { data } = await http.get<{ data: RcloneBackendInfo[] }>('/storage-targets/rclone/backends') return data.data }