|
|
@@ -0,0 +1,703 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace App\Module\Cleanup\Logics;
|
|
|
+
|
|
|
+use App\Module\Cleanup\Models\CleanupBackup;
|
|
|
+use App\Module\Cleanup\Models\CleanupBackupFile;
|
|
|
+use App\Module\Cleanup\Models\CleanupPlan;
|
|
|
+use App\Module\Cleanup\Models\CleanupTask;
|
|
|
+use App\Module\Cleanup\Enums\BACKUP_TYPE;
|
|
|
+use App\Module\Cleanup\Enums\BACKUP_STATUS;
|
|
|
+use App\Module\Cleanup\Enums\COMPRESSION_TYPE;
|
|
|
+use Illuminate\Support\Facades\DB;
|
|
|
+use Illuminate\Support\Facades\Log;
|
|
|
+use Illuminate\Support\Facades\Storage;
|
|
|
+use Illuminate\Support\Str;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 备份管理逻辑类
|
|
|
+ *
|
|
|
+ * 负责数据备份和恢复功能
|
|
|
+ */
|
|
|
+class BackupLogic
|
|
|
+{
|
|
|
+ /**
|
|
|
+ * 为计划创建数据备份
|
|
|
+ *
|
|
|
+ * @param int $planId 计划ID
|
|
|
+ * @param array $backupOptions 备份选项
|
|
|
+ * @return array 备份结果
|
|
|
+ */
|
|
|
+ public static function createPlanBackup(int $planId, array $backupOptions = []): array
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $plan = CleanupPlan::with('contents')->findOrFail($planId);
|
|
|
+
|
|
|
+ // 验证备份选项
|
|
|
+ $validatedOptions = static::validateBackupOptions($backupOptions);
|
|
|
+
|
|
|
+ // 创建备份记录
|
|
|
+ $backup = CleanupBackup::create([
|
|
|
+ 'backup_name' => $validatedOptions['backup_name'] ?? "计划备份 - {$plan->plan_name}",
|
|
|
+ 'plan_id' => $planId,
|
|
|
+ 'backup_type' => $validatedOptions['backup_type'],
|
|
|
+ 'compression_type' => $validatedOptions['compression_type'],
|
|
|
+ 'status' => BACKUP_STATUS::PENDING->value,
|
|
|
+ 'file_count' => 0,
|
|
|
+ 'backup_size' => 0,
|
|
|
+ 'created_by' => $validatedOptions['created_by'] ?? 0,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 执行备份
|
|
|
+ $result = static::performBackup($backup, $plan->contents->where('backup_enabled', true));
|
|
|
+
|
|
|
+ return $result;
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('创建计划备份失败', [
|
|
|
+ 'plan_id' => $planId,
|
|
|
+ 'backup_options' => $backupOptions,
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ 'trace' => $e->getTraceAsString()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'success' => false,
|
|
|
+ 'message' => '创建计划备份失败: ' . $e->getMessage(),
|
|
|
+ 'data' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 为任务创建数据备份
|
|
|
+ *
|
|
|
+ * @param int $taskId 任务ID
|
|
|
+ * @param array $backupOptions 备份选项
|
|
|
+ * @return array 备份结果
|
|
|
+ */
|
|
|
+ public static function createTaskBackup(int $taskId, array $backupOptions = []): array
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $task = CleanupTask::with('plan.contents')->findOrFail($taskId);
|
|
|
+
|
|
|
+ if (!$task->plan) {
|
|
|
+ throw new \Exception('任务关联的计划不存在');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证备份选项
|
|
|
+ $validatedOptions = static::validateBackupOptions($backupOptions);
|
|
|
+
|
|
|
+ // 创建备份记录
|
|
|
+ $backup = CleanupBackup::create([
|
|
|
+ 'backup_name' => $validatedOptions['backup_name'] ?? "任务备份 - {$task->task_name}",
|
|
|
+ 'task_id' => $taskId,
|
|
|
+ 'plan_id' => $task->plan_id,
|
|
|
+ 'backup_type' => $validatedOptions['backup_type'],
|
|
|
+ 'compression_type' => $validatedOptions['compression_type'],
|
|
|
+ 'status' => BACKUP_STATUS::PENDING->value,
|
|
|
+ 'file_count' => 0,
|
|
|
+ 'backup_size' => 0,
|
|
|
+ 'created_by' => $validatedOptions['created_by'] ?? 0,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // 更新任务的备份ID
|
|
|
+ $task->update(['backup_id' => $backup->id]);
|
|
|
+
|
|
|
+ // 执行备份
|
|
|
+ $result = static::performBackup($backup, $task->plan->contents->where('backup_enabled', true));
|
|
|
+
|
|
|
+ return $result;
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('创建任务备份失败', [
|
|
|
+ 'task_id' => $taskId,
|
|
|
+ 'backup_options' => $backupOptions,
|
|
|
+ 'error' => $e->getMessage(),
|
|
|
+ 'trace' => $e->getTraceAsString()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'success' => false,
|
|
|
+ 'message' => '创建任务备份失败: ' . $e->getMessage(),
|
|
|
+ 'data' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 执行备份操作
|
|
|
+ *
|
|
|
+ * @param CleanupBackup $backup 备份记录
|
|
|
+ * @param \Illuminate\Support\Collection $contents 内容集合
|
|
|
+ * @return array 备份结果
|
|
|
+ */
|
|
|
+ private static function performBackup(CleanupBackup $backup, $contents): array
|
|
|
+ {
|
|
|
+ $startTime = microtime(true);
|
|
|
+ $totalSize = 0;
|
|
|
+ $fileCount = 0;
|
|
|
+ $errors = [];
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 更新备份状态为进行中
|
|
|
+ $backup->update([
|
|
|
+ 'status' => BACKUP_STATUS::RUNNING->value,
|
|
|
+ 'started_at' => now(),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $backupType = BACKUP_TYPE::from($backup->backup_type);
|
|
|
+ $compressionType = COMPRESSION_TYPE::from($backup->compression_type);
|
|
|
+
|
|
|
+ foreach ($contents as $content) {
|
|
|
+ try {
|
|
|
+ $result = static::backupTable($backup, $content->table_name, $backupType, $compressionType);
|
|
|
+
|
|
|
+ if ($result['success']) {
|
|
|
+ $totalSize += $result['file_size'];
|
|
|
+ $fileCount++;
|
|
|
+ } else {
|
|
|
+ $errors[] = "表 {$content->table_name}: " . $result['message'];
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ $errors[] = "表 {$content->table_name}: " . $e->getMessage();
|
|
|
+ Log::error("备份表失败", [
|
|
|
+ 'backup_id' => $backup->id,
|
|
|
+ 'table_name' => $content->table_name,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $executionTime = round(microtime(true) - $startTime, 3);
|
|
|
+
|
|
|
+ // 更新备份完成状态
|
|
|
+ $finalStatus = empty($errors) ? BACKUP_STATUS::COMPLETED : BACKUP_STATUS::FAILED;
|
|
|
+ $backup->update([
|
|
|
+ 'status' => $finalStatus->value,
|
|
|
+ 'file_count' => $fileCount,
|
|
|
+ 'backup_size' => $totalSize,
|
|
|
+ 'execution_time' => $executionTime,
|
|
|
+ 'completed_at' => now(),
|
|
|
+ 'error_message' => empty($errors) ? null : implode('; ', $errors),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'success' => $finalStatus === BACKUP_STATUS::COMPLETED,
|
|
|
+ 'message' => $finalStatus === BACKUP_STATUS::COMPLETED ? '备份创建成功' : '备份创建完成,但有错误',
|
|
|
+ 'data' => [
|
|
|
+ 'backup_id' => $backup->id,
|
|
|
+ 'backup_name' => $backup->backup_name,
|
|
|
+ 'file_count' => $fileCount,
|
|
|
+ 'backup_size' => $totalSize,
|
|
|
+ 'execution_time' => $executionTime,
|
|
|
+ 'errors' => $errors,
|
|
|
+ ]
|
|
|
+ ];
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ // 更新备份失败状态
|
|
|
+ $backup->update([
|
|
|
+ 'status' => BACKUP_STATUS::FAILED->value,
|
|
|
+ 'execution_time' => round(microtime(true) - $startTime, 3),
|
|
|
+ 'completed_at' => now(),
|
|
|
+ 'error_message' => $e->getMessage(),
|
|
|
+ ]);
|
|
|
+
|
|
|
+ throw $e;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 备份单个表
|
|
|
+ *
|
|
|
+ * @param CleanupBackup $backup 备份记录
|
|
|
+ * @param string $tableName 表名
|
|
|
+ * @param BACKUP_TYPE $backupType 备份类型
|
|
|
+ * @param COMPRESSION_TYPE $compressionType 压缩类型
|
|
|
+ * @return array 备份结果
|
|
|
+ */
|
|
|
+ private static function backupTable(CleanupBackup $backup, string $tableName, BACKUP_TYPE $backupType, COMPRESSION_TYPE $compressionType): array
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ // 生成备份文件名
|
|
|
+ $fileName = static::generateBackupFileName($backup, $tableName, $backupType);
|
|
|
+ $filePath = "cleanup/backups/{$backup->id}/{$fileName}";
|
|
|
+
|
|
|
+ // 根据备份类型执行备份
|
|
|
+ $content = match ($backupType) {
|
|
|
+ BACKUP_TYPE::SQL => static::exportTableToSQL($tableName),
|
|
|
+ BACKUP_TYPE::JSON => static::exportTableToJSON($tableName),
|
|
|
+ BACKUP_TYPE::CSV => static::exportTableToCSV($tableName),
|
|
|
+ };
|
|
|
+
|
|
|
+ // 压缩内容(如果需要)
|
|
|
+ if ($compressionType !== COMPRESSION_TYPE::NONE) {
|
|
|
+ $content = static::compressContent($content, $compressionType);
|
|
|
+ $fileName .= static::getCompressionExtension($compressionType);
|
|
|
+ $filePath .= static::getCompressionExtension($compressionType);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 保存文件
|
|
|
+ Storage::disk('local')->put($filePath, $content);
|
|
|
+ $fileSize = Storage::disk('local')->size($filePath);
|
|
|
+
|
|
|
+ // 计算文件哈希
|
|
|
+ $fileHash = hash('sha256', $content);
|
|
|
+
|
|
|
+ // 记录备份文件
|
|
|
+ CleanupBackupFile::create([
|
|
|
+ 'backup_id' => $backup->id,
|
|
|
+ 'table_name' => $tableName,
|
|
|
+ 'file_name' => $fileName,
|
|
|
+ 'file_path' => $filePath,
|
|
|
+ 'file_size' => $fileSize,
|
|
|
+ 'file_hash' => $fileHash,
|
|
|
+ 'backup_type' => $backupType->value,
|
|
|
+ 'compression_type' => $compressionType->value,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'success' => true,
|
|
|
+ 'message' => "表 {$tableName} 备份成功",
|
|
|
+ 'file_size' => $fileSize,
|
|
|
+ 'file_path' => $filePath,
|
|
|
+ ];
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ return [
|
|
|
+ 'success' => false,
|
|
|
+ 'message' => $e->getMessage(),
|
|
|
+ 'file_size' => 0,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 导出表为SQL格式
|
|
|
+ *
|
|
|
+ * @param string $tableName 表名
|
|
|
+ * @return string SQL内容
|
|
|
+ */
|
|
|
+ private static function exportTableToSQL(string $tableName): string
|
|
|
+ {
|
|
|
+ $sql = "-- 表 {$tableName} 的数据备份\n";
|
|
|
+ $sql .= "-- 备份时间: " . now()->toDateTimeString() . "\n\n";
|
|
|
+
|
|
|
+ // 获取表结构
|
|
|
+ $createTable = DB::select("SHOW CREATE TABLE `{$tableName}`")[0];
|
|
|
+ $sql .= $createTable->{'Create Table'} . ";\n\n";
|
|
|
+
|
|
|
+ // 获取表数据
|
|
|
+ $records = DB::table($tableName)->get();
|
|
|
+
|
|
|
+ if ($records->isNotEmpty()) {
|
|
|
+ $sql .= "-- 数据插入\n";
|
|
|
+ $sql .= "INSERT INTO `{$tableName}` VALUES\n";
|
|
|
+
|
|
|
+ $values = [];
|
|
|
+ foreach ($records as $record) {
|
|
|
+ $recordArray = (array) $record;
|
|
|
+ $escapedValues = array_map(function ($value) {
|
|
|
+ return $value === null ? 'NULL' : "'" . addslashes($value) . "'";
|
|
|
+ }, $recordArray);
|
|
|
+ $values[] = '(' . implode(', ', $escapedValues) . ')';
|
|
|
+ }
|
|
|
+
|
|
|
+ $sql .= implode(",\n", $values) . ";\n";
|
|
|
+ }
|
|
|
+
|
|
|
+ return $sql;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 导出表为JSON格式
|
|
|
+ *
|
|
|
+ * @param string $tableName 表名
|
|
|
+ * @return string JSON内容
|
|
|
+ */
|
|
|
+ private static function exportTableToJSON(string $tableName): string
|
|
|
+ {
|
|
|
+ $data = [
|
|
|
+ 'table_name' => $tableName,
|
|
|
+ 'backup_time' => now()->toDateTimeString(),
|
|
|
+ 'records' => DB::table($tableName)->get()->toArray(),
|
|
|
+ ];
|
|
|
+
|
|
|
+ return json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 导出表为CSV格式
|
|
|
+ *
|
|
|
+ * @param string $tableName 表名
|
|
|
+ * @return string CSV内容
|
|
|
+ */
|
|
|
+ private static function exportTableToCSV(string $tableName): string
|
|
|
+ {
|
|
|
+ $records = DB::table($tableName)->get();
|
|
|
+
|
|
|
+ if ($records->isEmpty()) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+
|
|
|
+ $csv = '';
|
|
|
+ $headers = array_keys((array) $records->first());
|
|
|
+ $csv .= implode(',', $headers) . "\n";
|
|
|
+
|
|
|
+ foreach ($records as $record) {
|
|
|
+ $recordArray = (array) $record;
|
|
|
+ $escapedValues = array_map(function ($value) {
|
|
|
+ return '"' . str_replace('"', '""', $value ?? '') . '"';
|
|
|
+ }, $recordArray);
|
|
|
+ $csv .= implode(',', $escapedValues) . "\n";
|
|
|
+ }
|
|
|
+
|
|
|
+ return $csv;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 压缩内容
|
|
|
+ *
|
|
|
+ * @param string $content 原始内容
|
|
|
+ * @param COMPRESSION_TYPE $compressionType 压缩类型
|
|
|
+ * @return string 压缩后的内容
|
|
|
+ */
|
|
|
+ private static function compressContent(string $content, COMPRESSION_TYPE $compressionType): string
|
|
|
+ {
|
|
|
+ return match ($compressionType) {
|
|
|
+ COMPRESSION_TYPE::GZIP => gzencode($content),
|
|
|
+ COMPRESSION_TYPE::ZIP => static::createZipContent($content),
|
|
|
+ default => $content,
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建ZIP内容
|
|
|
+ *
|
|
|
+ * @param string $content 原始内容
|
|
|
+ * @return string ZIP内容
|
|
|
+ */
|
|
|
+ private static function createZipContent(string $content): string
|
|
|
+ {
|
|
|
+ $zip = new \ZipArchive();
|
|
|
+ $tempFile = tempnam(sys_get_temp_dir(), 'backup_');
|
|
|
+
|
|
|
+ if ($zip->open($tempFile, \ZipArchive::CREATE) === TRUE) {
|
|
|
+ $zip->addFromString('data.sql', $content);
|
|
|
+ $zip->close();
|
|
|
+
|
|
|
+ $zipContent = file_get_contents($tempFile);
|
|
|
+ unlink($tempFile);
|
|
|
+
|
|
|
+ return $zipContent;
|
|
|
+ }
|
|
|
+
|
|
|
+ throw new \Exception('创建ZIP文件失败');
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取压缩扩展名
|
|
|
+ *
|
|
|
+ * @param COMPRESSION_TYPE $compressionType 压缩类型
|
|
|
+ * @return string 扩展名
|
|
|
+ */
|
|
|
+ private static function getCompressionExtension(COMPRESSION_TYPE $compressionType): string
|
|
|
+ {
|
|
|
+ return match ($compressionType) {
|
|
|
+ COMPRESSION_TYPE::GZIP => '.gz',
|
|
|
+ COMPRESSION_TYPE::ZIP => '.zip',
|
|
|
+ default => '',
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成备份文件名
|
|
|
+ *
|
|
|
+ * @param CleanupBackup $backup 备份记录
|
|
|
+ * @param string $tableName 表名
|
|
|
+ * @param BACKUP_TYPE $backupType 备份类型
|
|
|
+ * @return string 文件名
|
|
|
+ */
|
|
|
+ private static function generateBackupFileName(CleanupBackup $backup, string $tableName, BACKUP_TYPE $backupType): string
|
|
|
+ {
|
|
|
+ $timestamp = now()->format('Y-m-d_H-i-s');
|
|
|
+ $extension = match ($backupType) {
|
|
|
+ BACKUP_TYPE::SQL => '.sql',
|
|
|
+ BACKUP_TYPE::JSON => '.json',
|
|
|
+ BACKUP_TYPE::CSV => '.csv',
|
|
|
+ };
|
|
|
+
|
|
|
+ return "{$tableName}_{$timestamp}{$extension}";
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 验证备份选项
|
|
|
+ *
|
|
|
+ * @param array $backupOptions 备份选项
|
|
|
+ * @return array 验证后的选项
|
|
|
+ */
|
|
|
+ private static function validateBackupOptions(array $backupOptions): array
|
|
|
+ {
|
|
|
+ return [
|
|
|
+ 'backup_name' => $backupOptions['backup_name'] ?? null,
|
|
|
+ 'backup_type' => $backupOptions['backup_type'] ?? BACKUP_TYPE::SQL->value,
|
|
|
+ 'compression_type' => $backupOptions['compression_type'] ?? COMPRESSION_TYPE::GZIP->value,
|
|
|
+ 'created_by' => $backupOptions['created_by'] ?? 0,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 清理过期备份
|
|
|
+ *
|
|
|
+ * @param int $retentionDays 保留天数
|
|
|
+ * @return array 清理结果
|
|
|
+ */
|
|
|
+ public static function cleanExpiredBackups(int $retentionDays = 30): array
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $expiredDate = now()->subDays($retentionDays);
|
|
|
+
|
|
|
+ // 获取过期的备份
|
|
|
+ $expiredBackups = CleanupBackup::where('created_at', '<', $expiredDate)
|
|
|
+ ->where('status', BACKUP_STATUS::COMPLETED->value)
|
|
|
+ ->get();
|
|
|
+
|
|
|
+ $deletedCount = 0;
|
|
|
+ $freedSpace = 0;
|
|
|
+ $errors = [];
|
|
|
+
|
|
|
+ foreach ($expiredBackups as $backup) {
|
|
|
+ try {
|
|
|
+ $result = static::deleteBackup($backup->id);
|
|
|
+ if ($result['success']) {
|
|
|
+ $deletedCount++;
|
|
|
+ $freedSpace += $backup->backup_size;
|
|
|
+ } else {
|
|
|
+ $errors[] = "备份 {$backup->id}: " . $result['message'];
|
|
|
+ }
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ $errors[] = "备份 {$backup->id}: " . $e->getMessage();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'success' => true,
|
|
|
+ 'message' => "清理完成,删除 {$deletedCount} 个过期备份",
|
|
|
+ 'data' => [
|
|
|
+ 'deleted_count' => $deletedCount,
|
|
|
+ 'total_expired' => $expiredBackups->count(),
|
|
|
+ 'freed_space' => $freedSpace,
|
|
|
+ 'errors' => $errors,
|
|
|
+ ]
|
|
|
+ ];
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('清理过期备份失败', [
|
|
|
+ 'retention_days' => $retentionDays,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'success' => false,
|
|
|
+ 'message' => '清理过期备份失败: ' . $e->getMessage(),
|
|
|
+ 'data' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 删除备份
|
|
|
+ *
|
|
|
+ * @param int $backupId 备份ID
|
|
|
+ * @return array 删除结果
|
|
|
+ */
|
|
|
+ public static function deleteBackup(int $backupId): array
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ DB::beginTransaction();
|
|
|
+
|
|
|
+ $backup = CleanupBackup::with('files')->findOrFail($backupId);
|
|
|
+
|
|
|
+ // 删除备份文件
|
|
|
+ foreach ($backup->files as $file) {
|
|
|
+ try {
|
|
|
+ if (Storage::disk('local')->exists($file->file_path)) {
|
|
|
+ Storage::disk('local')->delete($file->file_path);
|
|
|
+ }
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::warning('删除备份文件失败', [
|
|
|
+ 'file_path' => $file->file_path,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 删除备份文件记录
|
|
|
+ $backup->files()->delete();
|
|
|
+
|
|
|
+ // 删除备份记录
|
|
|
+ $backup->delete();
|
|
|
+
|
|
|
+ DB::commit();
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'success' => true,
|
|
|
+ 'message' => '备份删除成功',
|
|
|
+ 'data' => [
|
|
|
+ 'backup_id' => $backupId,
|
|
|
+ 'deleted_files' => $backup->files->count(),
|
|
|
+ ]
|
|
|
+ ];
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ DB::rollBack();
|
|
|
+ Log::error('删除备份失败', [
|
|
|
+ 'backup_id' => $backupId,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'success' => false,
|
|
|
+ 'message' => '删除备份失败: ' . $e->getMessage(),
|
|
|
+ 'data' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取备份详情
|
|
|
+ *
|
|
|
+ * @param int $backupId 备份ID
|
|
|
+ * @return array 备份详情
|
|
|
+ */
|
|
|
+ public static function getBackupDetails(int $backupId): array
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $backup = CleanupBackup::with(['files', 'plan', 'task'])->findOrFail($backupId);
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'success' => true,
|
|
|
+ 'data' => [
|
|
|
+ 'backup' => [
|
|
|
+ 'id' => $backup->id,
|
|
|
+ 'backup_name' => $backup->backup_name,
|
|
|
+ 'backup_type' => $backup->backup_type,
|
|
|
+ 'backup_type_name' => BACKUP_TYPE::from($backup->backup_type)->getDescription(),
|
|
|
+ 'compression_type' => $backup->compression_type,
|
|
|
+ 'compression_type_name' => COMPRESSION_TYPE::from($backup->compression_type)->getDescription(),
|
|
|
+ 'status' => $backup->status,
|
|
|
+ 'status_name' => BACKUP_STATUS::from($backup->status)->getDescription(),
|
|
|
+ 'file_count' => $backup->file_count,
|
|
|
+ 'backup_size' => $backup->backup_size,
|
|
|
+ 'execution_time' => $backup->execution_time,
|
|
|
+ 'started_at' => $backup->started_at,
|
|
|
+ 'completed_at' => $backup->completed_at,
|
|
|
+ 'error_message' => $backup->error_message,
|
|
|
+ 'created_at' => $backup->created_at,
|
|
|
+ ],
|
|
|
+ 'files' => $backup->files->map(function ($file) {
|
|
|
+ return [
|
|
|
+ 'id' => $file->id,
|
|
|
+ 'table_name' => $file->table_name,
|
|
|
+ 'file_name' => $file->file_name,
|
|
|
+ 'file_size' => $file->file_size,
|
|
|
+ 'file_hash' => $file->file_hash,
|
|
|
+ 'backup_type' => $file->backup_type,
|
|
|
+ 'compression_type' => $file->compression_type,
|
|
|
+ ];
|
|
|
+ }),
|
|
|
+ 'plan' => $backup->plan ? [
|
|
|
+ 'id' => $backup->plan->id,
|
|
|
+ 'plan_name' => $backup->plan->plan_name,
|
|
|
+ ] : null,
|
|
|
+ 'task' => $backup->task ? [
|
|
|
+ 'id' => $backup->task->id,
|
|
|
+ 'task_name' => $backup->task->task_name,
|
|
|
+ ] : null,
|
|
|
+ ]
|
|
|
+ ];
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('获取备份详情失败', [
|
|
|
+ 'backup_id' => $backupId,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'success' => false,
|
|
|
+ 'message' => '获取备份详情失败: ' . $e->getMessage(),
|
|
|
+ 'data' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 验证备份完整性
|
|
|
+ *
|
|
|
+ * @param int $backupId 备份ID
|
|
|
+ * @return array 验证结果
|
|
|
+ */
|
|
|
+ public static function verifyBackupIntegrity(int $backupId): array
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $backup = CleanupBackup::with('files')->findOrFail($backupId);
|
|
|
+
|
|
|
+ $verifiedFiles = 0;
|
|
|
+ $corruptedFiles = 0;
|
|
|
+ $missingFiles = 0;
|
|
|
+ $errors = [];
|
|
|
+
|
|
|
+ foreach ($backup->files as $file) {
|
|
|
+ try {
|
|
|
+ if (!Storage::disk('local')->exists($file->file_path)) {
|
|
|
+ $missingFiles++;
|
|
|
+ $errors[] = "文件缺失: {$file->file_name}";
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $content = Storage::disk('local')->get($file->file_path);
|
|
|
+ $currentHash = hash('sha256', $content);
|
|
|
+
|
|
|
+ if ($currentHash === $file->file_hash) {
|
|
|
+ $verifiedFiles++;
|
|
|
+ } else {
|
|
|
+ $corruptedFiles++;
|
|
|
+ $errors[] = "文件损坏: {$file->file_name}";
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ $errors[] = "验证失败: {$file->file_name} - " . $e->getMessage();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ $isIntact = $corruptedFiles === 0 && $missingFiles === 0;
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'success' => true,
|
|
|
+ 'data' => [
|
|
|
+ 'backup_id' => $backupId,
|
|
|
+ 'is_intact' => $isIntact,
|
|
|
+ 'total_files' => $backup->files->count(),
|
|
|
+ 'verified_files' => $verifiedFiles,
|
|
|
+ 'corrupted_files' => $corruptedFiles,
|
|
|
+ 'missing_files' => $missingFiles,
|
|
|
+ 'errors' => $errors,
|
|
|
+ ]
|
|
|
+ ];
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ Log::error('验证备份完整性失败', [
|
|
|
+ 'backup_id' => $backupId,
|
|
|
+ 'error' => $e->getMessage()
|
|
|
+ ]);
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'success' => false,
|
|
|
+ 'message' => '验证备份完整性失败: ' . $e->getMessage(),
|
|
|
+ 'data' => null
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|