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 { // 数据库备份类型直接存储到数据库 if ($backupType === BACKUP_TYPE::DATABASE) { return static::backupTableToDatabase($backup, $tableName); } // 生成备份文件名 $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 CleanupBackup $backup 备份记录 * @param string $tableName 表名 * @return array 备份结果 */ private static function backupTableToDatabase(CleanupBackup $backup, string $tableName): array { try { // 获取表数据 $records = DB::table($tableName)->get(); if ($records->isEmpty()) { // 即使没有数据也创建记录,记录表结构 $sqlContent = "-- 表 {$tableName} 的数据备份\n"; $sqlContent .= "-- 备份时间: " . now()->toDateTimeString() . "\n"; $sqlContent .= "-- 该表无数据记录\n\n"; // 获取表结构 $createTable = DB::select("SHOW CREATE TABLE `{$tableName}`")[0]; $sqlContent .= $createTable->{'Create Table'} . ";\n"; $recordsCount = 0; } else { // 生成INSERT语句 $sqlContent = static::generateInsertStatements($tableName, $records); $recordsCount = $records->count(); } $contentSize = strlen($sqlContent); $contentHash = hash('sha256', $sqlContent); // 保存到数据库 CleanupSqlBackup::create([ 'backup_id' => $backup->id, 'table_name' => $tableName, 'sql_content' => $sqlContent, 'records_count' => $recordsCount, 'content_size' => $contentSize, 'content_hash' => $contentHash, 'backup_conditions' => null, // 暂时不支持条件备份 ]); return [ 'success' => true, 'message' => "表 {$tableName} 数据库备份成功", 'file_size' => $contentSize, // 用内容大小代替文件大小 'records_count' => $recordsCount, ]; } catch (\Exception $e) { return [ 'success' => false, 'message' => $e->getMessage(), 'file_size' => 0, 'records_count' => 0, ]; } } /** * 生成INSERT语句 * * @param string $tableName 表名 * @param \Illuminate\Support\Collection $records 记录集合 * @return string SQL内容 */ private static function generateInsertStatements(string $tableName, $records): string { $sql = "-- 表 {$tableName} 的数据备份\n"; $sql .= "-- 备份时间: " . now()->toDateTimeString() . "\n"; $sql .= "-- 记录数量: " . $records->count() . "\n\n"; // 获取表结构 $createTable = DB::select("SHOW CREATE TABLE `{$tableName}`")[0]; $sql .= $createTable->{'Create Table'} . ";\n\n"; // 生成INSERT语句 if ($records->isNotEmpty()) { $sql .= "-- 数据插入\n"; // 获取字段名 $firstRecord = (array) $records->first(); $columns = array_keys($firstRecord); $columnList = '`' . implode('`, `', $columns) . '`'; $sql .= "INSERT INTO `{$tableName}` ({$columnList}) VALUES\n"; $values = []; foreach ($records as $record) { $recordArray = (array) $record; $escapedValues = array_map(function ($value) { if ($value === null) { return 'NULL'; } elseif (is_numeric($value)) { return $value; } else { return "'" . addslashes($value) . "'"; } }, $recordArray); $values[] = '(' . implode(', ', $escapedValues) . ')'; } $sql .= implode(",\n", $values) . ";\n"; } return $sql; } /** * 压缩内容 * * @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', 'sqlBackups'])->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(); // 删除SQL备份记录 $backup->sqlBackups()->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 ]; } } }