dongasai 6 сар өмнө
parent
commit
997660e0a0

+ 4 - 3
app/Module/Cleanup/CleanupServiceProvider.php

@@ -3,6 +3,7 @@
 namespace App\Module\Cleanup;
 
 use Illuminate\Support\ServiceProvider;
+use Illuminate\Support\Facades\Event;
 use App\Module\Cleanup\Commands\ScanTablesCommand;
 use App\Module\Cleanup\Commands\CleanupDataCommand;
 
@@ -58,19 +59,19 @@ class CleanupServiceProvider extends ServiceProvider
     protected function registerEventListeners(): void
     {
         // 任务状态变更事件
-        \Event::listen(
+        Event::listen(
             \App\Module\Cleanup\Events\TaskStatusChanged::class,
             \App\Module\Cleanup\Listeners\TaskStatusChangedListener::class
         );
 
         // 备份完成事件
-        \Event::listen(
+        Event::listen(
             \App\Module\Cleanup\Events\BackupCompleted::class,
             \App\Module\Cleanup\Listeners\BackupCompletedListener::class
         );
 
         // 清理完成事件
-        \Event::listen(
+        Event::listen(
             \App\Module\Cleanup\Events\CleanupCompleted::class,
             \App\Module\Cleanup\Listeners\CleanupCompletedListener::class
         );

+ 0 - 297
app/Module/Cleanup/Docs/灵活表选择更新总结.md

@@ -1,297 +0,0 @@
-# Cleanup 模块灵活表选择更新总结
-
-## 1. 更新概述
-
-根据用户需求"不要局限于模块,可以自由配置清理那个表",我们对 Cleanup 模块进行了全面的更新,实现了完全自由的表选择功能。
-
-## 2. 核心改进
-
-### 2.1 从模块限制到完全自由
-
-**原设计问题**:
-- 清理范围局限于模块边界
-- 无法跨模块选择特定表
-- 选择方式不够灵活
-
-**新设计优势**:
-- 完全自由选择任意表
-- 支持跨模块的精确控制
-- 提供5种灵活的选择方式
-
-### 2.2 数据库结构调整
-
-**原字段设计**:
-```sql
--- 原来的设计
-`target_modules` json DEFAULT NULL COMMENT '目标模块列表',
-`target_categories` json DEFAULT NULL COMMENT '目标数据分类列表',
-`target_tables` json DEFAULT NULL COMMENT '目标表列表',
-```
-
-**新字段设计**:
-```sql
--- 新的设计
-`target_selection` json DEFAULT NULL COMMENT '目标选择配置',
-```
-
-**新字段支持的配置**:
-```json
-{
-  "selection_type": "custom|module|category|all|mixed",
-  "modules": ["Farm", "GameItems"],           // 可选
-  "categories": [1, 2],                       // 可选
-  "tables": ["kku_farm_users", "..."],        // 可选
-  "exclude_modules": ["Config"],              // 可选
-  "exclude_categories": [5],                  // 可选
-  "exclude_tables": ["kku_farm_configs"]     // 可选
-}
-```
-
-## 3. 五种表选择方式
-
-### 3.1 自定义选择 (Custom)
-**用途**:直接指定要清理的表列表
-**示例**:
-```json
-{
-  "selection_type": "custom",
-  "tables": [
-    "kku_farm_users",
-    "kku_item_users", 
-    "kku_pet_users",
-    "kku_shop_orders"
-  ]
-}
-```
-
-### 3.2 模块选择 (Module)
-**用途**:选择一个或多个模块的所有表
-**示例**:
-```json
-{
-  "selection_type": "module",
-  "modules": ["Farm", "GameItems"],
-  "exclude_tables": ["kku_farm_configs"]
-}
-```
-
-### 3.3 分类选择 (Category)
-**用途**:按数据分类选择表
-**示例**:
-```json
-{
-  "selection_type": "category",
-  "categories": [1, 2],  // 用户数据和日志数据
-  "exclude_modules": ["Config"]
-}
-```
-
-### 3.4 全量选择 (All)
-**用途**:选择所有表,但可排除特定内容
-**示例**:
-```json
-{
-  "selection_type": "all",
-  "exclude_categories": [5],  // 排除配置数据
-  "exclude_tables": ["kku_user_profiles"]
-}
-```
-
-### 3.5 混合选择 (Mixed)
-**用途**:组合多种选择方式
-**示例**:
-```json
-{
-  "selection_type": "mixed",
-  "modules": ["Farm"],           // 包含Farm模块
-  "categories": [2],             // 包含所有日志数据
-  "tables": ["kku_pet_users"],   // 包含指定表
-  "exclude_tables": ["kku_farm_configs"]  // 排除特定表
-}
-```
-
-## 4. 技术实现
-
-### 4.1 表选择解析器
-```php
-class TableSelectionResolver
-{
-    /**
-     * 解析目标选择配置,返回最终的表列表
-     */
-    public function resolve(array $targetSelection): array
-    {
-        $selectionType = $targetSelection['selection_type'];
-        
-        switch ($selectionType) {
-            case 'custom':
-                return $this->resolveCustomSelection($targetSelection);
-            case 'module':
-                return $this->resolveModuleSelection($targetSelection);
-            case 'category':
-                return $this->resolveCategorySelection($targetSelection);
-            case 'all':
-                return $this->resolveAllSelection($targetSelection);
-            case 'mixed':
-                return $this->resolveMixedSelection($targetSelection);
-        }
-    }
-}
-```
-
-### 4.2 后台管理界面
-- 智能的表选择器组件
-- 支持搜索和过滤功能
-- 实时预览选择结果
-- 批量选择和排除功能
-
-### 4.3 命令行工具增强
-```bash
-# 支持更多参数
-php artisan cleanup:create-plan [plan_name] \
-    --type=4 \
-    --tables=kku_farm_users,kku_item_users \
-    --modules=Farm,GameItems \
-    --categories=1,2 \
-    --exclude-tables=kku_farm_configs \
-    --exclude-modules=Config
-```
-
-## 5. 使用场景示例
-
-### 5.1 跨模块用户数据清理
-```bash
-php artisan cleanup:create-plan "跨模块用户清理" \
-    --type=4 \
-    --tables=kku_farm_users,kku_item_users,kku_pet_users,kku_shop_users
-```
-
-### 5.2 特定功能数据清理
-```bash
-php artisan cleanup:create-plan "交易相关清理" \
-    --type=4 \
-    --tables=kku_shop_orders,kku_mex_orders,kku_fund_transfer,kku_point_transfer
-```
-
-### 5.3 日志数据清理(排除重要日志)
-```bash
-php artisan cleanup:create-plan "日志清理" \
-    --type=3 \
-    --categories=2 \
-    --exclude-tables=kku_admin_actionlogs,kku_sys_request_logs
-```
-
-### 5.4 测试环境全面清理
-```bash
-php artisan cleanup:create-plan "测试环境清理" \
-    --type=4 \
-    --modules=Farm,GameItems,Pet \
-    --categories=2 \
-    --exclude-categories=5 \
-    --exclude-tables=kku_user_profiles
-```
-
-## 6. API使用示例
-
-### 6.1 创建自定义清理计划
-```php
-$planData = [
-    'plan_name' => '跨模块数据清理',
-    'plan_type' => 4,
-    'target_selection' => [
-        'selection_type' => 'custom',
-        'tables' => [
-            'kku_farm_users',
-            'kku_item_users',
-            'kku_pet_users',
-            'kku_shop_orders'
-        ]
-    ],
-    'global_conditions' => [
-        'time_limit' => '30_days_ago'
-    ]
-];
-
-$result = CleanupService::createCleanupPlan($planData);
-```
-
-### 6.2 创建混合选择计划
-```php
-$planData = [
-    'plan_name' => '复杂清理计划',
-    'plan_type' => 4,
-    'target_selection' => [
-        'selection_type' => 'mixed',
-        'modules' => ['Farm'],
-        'categories' => [2],
-        'tables' => ['kku_pet_users'],
-        'exclude_tables' => ['kku_farm_configs']
-    ]
-];
-```
-
-## 7. 更新的文档
-
-### 7.1 已更新的文档列表
-1. **[概念层次设计.md](概念层次设计.md)** - 更新了示例和使用场景
-2. **[数据库设计.md](数据库设计.md)** - 调整了表结构和JSON配置
-3. **[接口设计.md](接口设计.md)** - 更新了接口参数和DTO
-4. **[灵活表选择设计.md](灵活表选择设计.md)** - 新增专门的设计文档
-5. **[README.md](../README.md)** - 更新了功能特性和使用示例
-
-### 7.2 文档更新要点
-- 所有示例都改为自定义选择或混合选择
-- 强调了跨模块选择的能力
-- 增加了排除机制的说明
-- 提供了丰富的使用场景
-
-## 8. 核心优势
-
-### 8.1 完全自由
-- **不受模块限制**:可以选择任意表,不受模块边界约束
-- **精确控制**:可以精确到表级别的控制
-- **跨模块选择**:可以同时选择多个模块的特定表
-
-### 8.2 灵活组合
-- **多种方式**:支持5种不同的选择方式
-- **组合使用**:混合选择支持多种方式的组合
-- **包含排除**:既可以包含也可以排除,灵活控制范围
-
-### 8.3 易于使用
-- **直观界面**:后台管理界面直观易用
-- **智能搜索**:支持表名搜索和过滤
-- **命令行友好**:丰富的命令行参数支持
-
-### 8.4 安全可控
-- **明确列表**:最终会生成明确的表列表
-- **预览功能**:执行前可以预览清理范围
-- **排除保护**:通过排除机制保护重要数据
-
-## 9. 实际应用场景
-
-### 9.1 开发测试
-- 清理特定功能相关的所有表
-- 跨模块清理用户测试数据
-- 保留配置但清理运行数据
-
-### 9.2 数据维护
-- 清理特定时间段的数据
-- 清理特定用户的所有数据
-- 清理无用的日志和缓存数据
-
-### 9.3 环境重置
-- 重置测试环境到初始状态
-- 清理演示环境的演示数据
-- 准备新的测试周期
-
-## 10. 总结
-
-通过这次更新,Cleanup 模块完全摆脱了模块边界的限制,实现了真正的"自由配置清理哪个表":
-
-- ✅ **完全自由**:可以选择任意表组合
-- ✅ **跨模块支持**:不受模块边界限制
-- ✅ **灵活配置**:5种选择方式满足各种需求
-- ✅ **易于使用**:直观的界面和命令行工具
-- ✅ **安全可控**:完善的排除和预览机制
-
-这个设计完全满足了用户的需求,提供了最大的灵活性和控制能力。

+ 703 - 0
app/Module/Cleanup/Logics/BackupLogic.php

@@ -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
+            ];
+        }
+    }
+}

+ 678 - 0
app/Module/Cleanup/Logics/CleanupExecutorLogic.php

@@ -0,0 +1,678 @@
+<?php
+
+namespace App\Module\Cleanup\Logics;
+
+use App\Module\Cleanup\Models\CleanupTask;
+use App\Module\Cleanup\Models\CleanupPlan;
+use App\Module\Cleanup\Models\CleanupPlanContent;
+use App\Module\Cleanup\Models\CleanupLog;
+use App\Module\Cleanup\Enums\TASK_STATUS;
+use App\Module\Cleanup\Enums\CLEANUP_TYPE;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Schema;
+
+/**
+ * 清理执行逻辑类
+ * 
+ * 负责实际的数据清理执行操作
+ */
+class CleanupExecutorLogic
+{
+    /**
+     * 预览计划的清理结果
+     *
+     * @param int $planId 计划ID
+     * @return array 预览结果
+     */
+    public static function previewPlanCleanup(int $planId): array
+    {
+        try {
+            $plan = CleanupPlan::with('contents')->findOrFail($planId);
+            
+            $previewData = [];
+            $totalRecords = 0;
+            $totalTables = 0;
+            
+            foreach ($plan->contents->where('is_enabled', true) as $content) {
+                $tablePreview = static::previewTableCleanup($content);
+                $previewData[] = $tablePreview;
+                $totalRecords += $tablePreview['affected_records'];
+                $totalTables++;
+            }
+
+            return [
+                'success' => true,
+                'data' => [
+                    'plan' => [
+                        'id' => $plan->id,
+                        'plan_name' => $plan->plan_name,
+                        'description' => $plan->description,
+                    ],
+                    'summary' => [
+                        'total_tables' => $totalTables,
+                        'total_records' => $totalRecords,
+                        'estimated_time' => static::estimateExecutionTime($totalRecords),
+                    ],
+                    'tables' => $previewData,
+                ]
+            ];
+
+        } catch (\Exception $e) {
+            Log::error('预览计划清理失败', [
+                'plan_id' => $planId,
+                'error' => $e->getMessage()
+            ]);
+
+            return [
+                'success' => false,
+                'message' => '预览计划清理失败: ' . $e->getMessage(),
+                'data' => null
+            ];
+        }
+    }
+
+    /**
+     * 预览任务的清理结果
+     *
+     * @param int $taskId 任务ID
+     * @return array 预览结果
+     */
+    public static function previewTaskCleanup(int $taskId): array
+    {
+        try {
+            $task = CleanupTask::with('plan.contents')->findOrFail($taskId);
+            
+            if (!$task->plan) {
+                throw new \Exception('任务关联的计划不存在');
+            }
+
+            return static::previewPlanCleanup($task->plan->id);
+
+        } catch (\Exception $e) {
+            Log::error('预览任务清理失败', [
+                'task_id' => $taskId,
+                'error' => $e->getMessage()
+            ]);
+
+            return [
+                'success' => false,
+                'message' => '预览任务清理失败: ' . $e->getMessage(),
+                'data' => null
+            ];
+        }
+    }
+
+    /**
+     * 执行清理任务
+     *
+     * @param int $taskId 任务ID
+     * @param bool $dryRun 是否为预演模式
+     * @return array 执行结果
+     */
+    public static function executeTask(int $taskId, bool $dryRun = false): array
+    {
+        try {
+            $task = CleanupTask::with('plan.contents')->findOrFail($taskId);
+            
+            if (!$task->plan) {
+                throw new \Exception('任务关联的计划不存在');
+            }
+
+            // 检查任务状态
+            $currentStatus = TASK_STATUS::from($task->status);
+            if ($currentStatus !== TASK_STATUS::PENDING) {
+                throw new \Exception('任务状态不正确,无法执行');
+            }
+
+            if ($dryRun) {
+                return static::previewTaskCleanup($taskId);
+            }
+
+            // 开始执行任务
+            return static::executeTaskInternal($task);
+
+        } catch (\Exception $e) {
+            Log::error('执行清理任务失败', [
+                'task_id' => $taskId,
+                'dry_run' => $dryRun,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+
+            return [
+                'success' => false,
+                'message' => '执行清理任务失败: ' . $e->getMessage(),
+                'data' => null
+            ];
+        }
+    }
+
+    /**
+     * 内部执行任务逻辑
+     *
+     * @param CleanupTask $task 任务对象
+     * @return array 执行结果
+     */
+    private static function executeTaskInternal(CleanupTask $task): array
+    {
+        $startTime = microtime(true);
+        $totalDeleted = 0;
+        $processedTables = 0;
+        $errors = [];
+
+        try {
+            // 更新任务状态为执行中
+            CleanupTaskLogic::updateTaskStatus($task->id, TASK_STATUS::RUNNING, [
+                'current_step' => '开始执行清理',
+            ]);
+
+            // 获取启用的内容配置
+            $enabledContents = $task->plan->contents->where('is_enabled', true)->sortBy('priority');
+
+            foreach ($enabledContents as $content) {
+                try {
+                    // 更新当前步骤
+                    CleanupTaskLogic::updateTaskProgress(
+                        $task->id,
+                        $processedTables,
+                        $totalDeleted,
+                        "正在清理表: {$content->table_name}"
+                    );
+
+                    // 执行表清理
+                    $result = static::executeTableCleanup($content, $task->id);
+                    
+                    if ($result['success']) {
+                        $totalDeleted += $result['deleted_records'];
+                        $processedTables++;
+                    } else {
+                        $errors[] = "表 {$content->table_name}: " . $result['message'];
+                    }
+
+                } catch (\Exception $e) {
+                    $errors[] = "表 {$content->table_name}: " . $e->getMessage();
+                    Log::error("清理表失败", [
+                        'task_id' => $task->id,
+                        'table_name' => $content->table_name,
+                        'error' => $e->getMessage()
+                    ]);
+                }
+            }
+
+            $executionTime = round(microtime(true) - $startTime, 3);
+
+            // 更新任务完成状态
+            $finalStatus = empty($errors) ? TASK_STATUS::COMPLETED : TASK_STATUS::FAILED;
+            CleanupTaskLogic::updateTaskStatus($task->id, $finalStatus, [
+                'total_records' => $totalDeleted,
+                'deleted_records' => $totalDeleted,
+                'execution_time' => $executionTime,
+                'error_message' => empty($errors) ? null : implode('; ', $errors),
+                'current_step' => $finalStatus === TASK_STATUS::COMPLETED ? '清理完成' : '清理失败',
+            ]);
+
+            return [
+                'success' => $finalStatus === TASK_STATUS::COMPLETED,
+                'message' => $finalStatus === TASK_STATUS::COMPLETED ? '清理任务执行成功' : '清理任务执行完成,但有错误',
+                'data' => [
+                    'task_id' => $task->id,
+                    'processed_tables' => $processedTables,
+                    'total_tables' => $enabledContents->count(),
+                    'deleted_records' => $totalDeleted,
+                    'execution_time' => $executionTime,
+                    'errors' => $errors,
+                ]
+            ];
+
+        } catch (\Exception $e) {
+            // 更新任务失败状态
+            CleanupTaskLogic::updateTaskStatus($task->id, TASK_STATUS::FAILED, [
+                'error_message' => $e->getMessage(),
+                'execution_time' => round(microtime(true) - $startTime, 3),
+                'current_step' => '执行失败',
+            ]);
+
+            throw $e;
+        }
+    }
+
+    /**
+     * 预览表清理
+     *
+     * @param CleanupPlanContent $content 计划内容
+     * @return array 预览结果
+     */
+    private static function previewTableCleanup(CleanupPlanContent $content): array
+    {
+        try {
+            $tableName = $content->table_name;
+            $cleanupType = CLEANUP_TYPE::from($content->cleanup_type);
+            
+            // 检查表是否存在
+            if (!Schema::hasTable($tableName)) {
+                return [
+                    'table_name' => $tableName,
+                    'cleanup_type' => $cleanupType->getDescription(),
+                    'affected_records' => 0,
+                    'current_records' => 0,
+                    'error' => '表不存在',
+                ];
+            }
+
+            $currentRecords = DB::table($tableName)->count();
+            $affectedRecords = static::calculateAffectedRecords($tableName, $cleanupType, $content->conditions);
+
+            return [
+                'table_name' => $tableName,
+                'cleanup_type' => $cleanupType->getDescription(),
+                'affected_records' => $affectedRecords,
+                'current_records' => $currentRecords,
+                'remaining_records' => $currentRecords - $affectedRecords,
+                'conditions' => $content->conditions,
+                'batch_size' => $content->batch_size,
+                'backup_enabled' => $content->backup_enabled,
+            ];
+
+        } catch (\Exception $e) {
+            return [
+                'table_name' => $content->table_name,
+                'cleanup_type' => CLEANUP_TYPE::from($content->cleanup_type)->getDescription(),
+                'affected_records' => 0,
+                'current_records' => 0,
+                'error' => $e->getMessage(),
+            ];
+        }
+    }
+
+    /**
+     * 计算受影响的记录数
+     *
+     * @param string $tableName 表名
+     * @param CLEANUP_TYPE $cleanupType 清理类型
+     * @param array $conditions 清理条件
+     * @return int 受影响的记录数
+     */
+    private static function calculateAffectedRecords(string $tableName, CLEANUP_TYPE $cleanupType, array $conditions): int
+    {
+        switch ($cleanupType) {
+            case CLEANUP_TYPE::TRUNCATE:
+            case CLEANUP_TYPE::DELETE_ALL:
+                return DB::table($tableName)->count();
+                
+            case CLEANUP_TYPE::DELETE_BY_TIME:
+                return static::countByTimeCondition($tableName, $conditions);
+                
+            case CLEANUP_TYPE::DELETE_BY_USER:
+                return static::countByUserCondition($tableName, $conditions);
+                
+            case CLEANUP_TYPE::DELETE_BY_CONDITION:
+                return static::countByCustomCondition($tableName, $conditions);
+                
+            default:
+                return 0;
+        }
+    }
+
+    /**
+     * 按时间条件统计记录数
+     *
+     * @param string $tableName 表名
+     * @param array $conditions 条件
+     * @return int 记录数
+     */
+    private static function countByTimeCondition(string $tableName, array $conditions): int
+    {
+        $query = DB::table($tableName);
+        
+        if (!empty($conditions['time_field']) && !empty($conditions['before'])) {
+            $timeField = $conditions['time_field'];
+            $beforeTime = static::parseTimeCondition($conditions['before']);
+            $query->where($timeField, '<', $beforeTime);
+        }
+        
+        return $query->count();
+    }
+
+    /**
+     * 按用户条件统计记录数
+     *
+     * @param string $tableName 表名
+     * @param array $conditions 条件
+     * @return int 记录数
+     */
+    private static function countByUserCondition(string $tableName, array $conditions): int
+    {
+        $query = DB::table($tableName);
+        
+        if (!empty($conditions['user_field']) && !empty($conditions['user_ids'])) {
+            $userField = $conditions['user_field'];
+            $userIds = is_array($conditions['user_ids']) ? $conditions['user_ids'] : [$conditions['user_ids']];
+            $query->whereIn($userField, $userIds);
+        }
+        
+        return $query->count();
+    }
+
+    /**
+     * 按自定义条件统计记录数
+     *
+     * @param string $tableName 表名
+     * @param array $conditions 条件
+     * @return int 记录数
+     */
+    private static function countByCustomCondition(string $tableName, array $conditions): int
+    {
+        $query = DB::table($tableName);
+        
+        if (!empty($conditions['where'])) {
+            foreach ($conditions['where'] as $condition) {
+                if (isset($condition['field'], $condition['operator'], $condition['value'])) {
+                    $query->where($condition['field'], $condition['operator'], $condition['value']);
+                }
+            }
+        }
+        
+        return $query->count();
+    }
+
+    /**
+     * 解析时间条件
+     *
+     * @param string $timeCondition 时间条件
+     * @return string 解析后的时间
+     */
+    private static function parseTimeCondition(string $timeCondition): string
+    {
+        // 支持格式:30_days_ago, 1_month_ago, 2024-01-01, 等
+        if (preg_match('/(\d+)_days?_ago/', $timeCondition, $matches)) {
+            return now()->subDays((int)$matches[1])->toDateTimeString();
+        }
+        
+        if (preg_match('/(\d+)_months?_ago/', $timeCondition, $matches)) {
+            return now()->subMonths((int)$matches[1])->toDateTimeString();
+        }
+        
+        if (preg_match('/(\d+)_years?_ago/', $timeCondition, $matches)) {
+            return now()->subYears((int)$matches[1])->toDateTimeString();
+        }
+        
+        // 直接返回时间字符串
+        return $timeCondition;
+    }
+
+    /**
+     * 估算执行时间
+     *
+     * @param int $totalRecords 总记录数
+     * @return string 估算时间
+     */
+    private static function estimateExecutionTime(int $totalRecords): string
+    {
+        // 简单估算:每秒处理1000条记录
+        $seconds = ceil($totalRecords / 1000);
+        
+        if ($seconds < 60) {
+            return "{$seconds}秒";
+        } elseif ($seconds < 3600) {
+            $minutes = ceil($seconds / 60);
+            return "{$minutes}分钟";
+        } else {
+            $hours = ceil($seconds / 3600);
+            return "{$hours}小时";
+        }
+    }
+
+    /**
+     * 执行表清理
+     *
+     * @param CleanupPlanContent $content 计划内容
+     * @param int $taskId 任务ID
+     * @return array 执行结果
+     */
+    private static function executeTableCleanup(CleanupPlanContent $content, int $taskId): array
+    {
+        $startTime = microtime(true);
+        $tableName = $content->table_name;
+        $cleanupType = CLEANUP_TYPE::from($content->cleanup_type);
+
+        try {
+            // 检查表是否存在
+            if (!Schema::hasTable($tableName)) {
+                throw new \Exception("表 {$tableName} 不存在");
+            }
+
+            // 记录清理前的记录数
+            $beforeCount = DB::table($tableName)->count();
+
+            // 执行清理
+            $deletedRecords = static::performCleanup($tableName, $cleanupType, $content->conditions, $content->batch_size);
+
+            // 记录清理后的记录数
+            $afterCount = DB::table($tableName)->count();
+            $actualDeleted = $beforeCount - $afterCount;
+
+            $executionTime = round(microtime(true) - $startTime, 3);
+
+            // 记录清理日志
+            static::logCleanupOperation($taskId, $tableName, $cleanupType, [
+                'before_count' => $beforeCount,
+                'after_count' => $afterCount,
+                'deleted_records' => $actualDeleted,
+                'execution_time' => $executionTime,
+                'conditions' => $content->conditions,
+                'batch_size' => $content->batch_size,
+            ]);
+
+            return [
+                'success' => true,
+                'message' => "表 {$tableName} 清理成功",
+                'deleted_records' => $actualDeleted,
+                'execution_time' => $executionTime,
+            ];
+
+        } catch (\Exception $e) {
+            $executionTime = round(microtime(true) - $startTime, 3);
+
+            // 记录错误日志
+            static::logCleanupOperation($taskId, $tableName, $cleanupType, [
+                'error' => $e->getMessage(),
+                'execution_time' => $executionTime,
+                'conditions' => $content->conditions,
+            ]);
+
+            return [
+                'success' => false,
+                'message' => $e->getMessage(),
+                'deleted_records' => 0,
+                'execution_time' => $executionTime,
+            ];
+        }
+    }
+
+    /**
+     * 执行实际的清理操作
+     *
+     * @param string $tableName 表名
+     * @param CLEANUP_TYPE $cleanupType 清理类型
+     * @param array $conditions 清理条件
+     * @param int $batchSize 批处理大小
+     * @return int 删除的记录数
+     */
+    private static function performCleanup(string $tableName, CLEANUP_TYPE $cleanupType, array $conditions, int $batchSize): int
+    {
+        switch ($cleanupType) {
+            case CLEANUP_TYPE::TRUNCATE:
+                return static::performTruncate($tableName);
+
+            case CLEANUP_TYPE::DELETE_ALL:
+                return static::performDeleteAll($tableName, $batchSize);
+
+            case CLEANUP_TYPE::DELETE_BY_TIME:
+                return static::performDeleteByTime($tableName, $conditions, $batchSize);
+
+            case CLEANUP_TYPE::DELETE_BY_USER:
+                return static::performDeleteByUser($tableName, $conditions, $batchSize);
+
+            case CLEANUP_TYPE::DELETE_BY_CONDITION:
+                return static::performDeleteByCondition($tableName, $conditions, $batchSize);
+
+            default:
+                throw new \Exception("不支持的清理类型: {$cleanupType->value}");
+        }
+    }
+
+    /**
+     * 执行TRUNCATE操作
+     *
+     * @param string $tableName 表名
+     * @return int 删除的记录数
+     */
+    private static function performTruncate(string $tableName): int
+    {
+        $beforeCount = DB::table($tableName)->count();
+        DB::statement("TRUNCATE TABLE `{$tableName}`");
+        return $beforeCount;
+    }
+
+    /**
+     * 执行DELETE ALL操作
+     *
+     * @param string $tableName 表名
+     * @param int $batchSize 批处理大小
+     * @return int 删除的记录数
+     */
+    private static function performDeleteAll(string $tableName, int $batchSize): int
+    {
+        $totalDeleted = 0;
+
+        do {
+            $deleted = DB::table($tableName)->limit($batchSize)->delete();
+            $totalDeleted += $deleted;
+        } while ($deleted > 0);
+
+        return $totalDeleted;
+    }
+
+    /**
+     * 执行按时间删除操作
+     *
+     * @param string $tableName 表名
+     * @param array $conditions 条件
+     * @param int $batchSize 批处理大小
+     * @return int 删除的记录数
+     */
+    private static function performDeleteByTime(string $tableName, array $conditions, int $batchSize): int
+    {
+        if (empty($conditions['time_field']) || empty($conditions['before'])) {
+            throw new \Exception('时间删除条件不完整');
+        }
+
+        $timeField = $conditions['time_field'];
+        $beforeTime = static::parseTimeCondition($conditions['before']);
+        $totalDeleted = 0;
+
+        do {
+            $deleted = DB::table($tableName)
+                ->where($timeField, '<', $beforeTime)
+                ->limit($batchSize)
+                ->delete();
+            $totalDeleted += $deleted;
+        } while ($deleted > 0);
+
+        return $totalDeleted;
+    }
+
+    /**
+     * 执行按用户删除操作
+     *
+     * @param string $tableName 表名
+     * @param array $conditions 条件
+     * @param int $batchSize 批处理大小
+     * @return int 删除的记录数
+     */
+    private static function performDeleteByUser(string $tableName, array $conditions, int $batchSize): int
+    {
+        if (empty($conditions['user_field']) || empty($conditions['user_ids'])) {
+            throw new \Exception('用户删除条件不完整');
+        }
+
+        $userField = $conditions['user_field'];
+        $userIds = is_array($conditions['user_ids']) ? $conditions['user_ids'] : [$conditions['user_ids']];
+        $totalDeleted = 0;
+
+        do {
+            $deleted = DB::table($tableName)
+                ->whereIn($userField, $userIds)
+                ->limit($batchSize)
+                ->delete();
+            $totalDeleted += $deleted;
+        } while ($deleted > 0);
+
+        return $totalDeleted;
+    }
+
+    /**
+     * 执行按条件删除操作
+     *
+     * @param string $tableName 表名
+     * @param array $conditions 条件
+     * @param int $batchSize 批处理大小
+     * @return int 删除的记录数
+     */
+    private static function performDeleteByCondition(string $tableName, array $conditions, int $batchSize): int
+    {
+        if (empty($conditions['where'])) {
+            throw new \Exception('自定义删除条件不完整');
+        }
+
+        $totalDeleted = 0;
+
+        do {
+            $query = DB::table($tableName);
+
+            foreach ($conditions['where'] as $condition) {
+                if (isset($condition['field'], $condition['operator'], $condition['value'])) {
+                    $query->where($condition['field'], $condition['operator'], $condition['value']);
+                }
+            }
+
+            $deleted = $query->limit($batchSize)->delete();
+            $totalDeleted += $deleted;
+        } while ($deleted > 0);
+
+        return $totalDeleted;
+    }
+
+    /**
+     * 记录清理操作日志
+     *
+     * @param int $taskId 任务ID
+     * @param string $tableName 表名
+     * @param CLEANUP_TYPE $cleanupType 清理类型
+     * @param array $details 详细信息
+     */
+    private static function logCleanupOperation(int $taskId, string $tableName, CLEANUP_TYPE $cleanupType, array $details): void
+    {
+        try {
+            CleanupLog::create([
+                'task_id' => $taskId,
+                'table_name' => $tableName,
+                'cleanup_type' => $cleanupType->value,
+                'before_count' => $details['before_count'] ?? 0,
+                'after_count' => $details['after_count'] ?? 0,
+                'deleted_records' => $details['deleted_records'] ?? 0,
+                'execution_time' => $details['execution_time'] ?? 0,
+                'conditions' => $details['conditions'] ?? [],
+                'error_message' => $details['error'] ?? null,
+                'created_at' => now(),
+            ]);
+        } catch (\Exception $e) {
+            Log::error('记录清理日志失败', [
+                'task_id' => $taskId,
+                'table_name' => $tableName,
+                'error' => $e->getMessage()
+            ]);
+        }
+    }
+}

+ 518 - 0
app/Module/Cleanup/Logics/CleanupPlanLogic.php

@@ -0,0 +1,518 @@
+<?php
+
+namespace App\Module\Cleanup\Logics;
+
+use App\Module\Cleanup\Models\CleanupPlan;
+use App\Module\Cleanup\Models\CleanupPlanContent;
+use App\Module\Cleanup\Models\CleanupConfig;
+use App\Module\Cleanup\Enums\PLAN_TYPE;
+use App\Module\Cleanup\Enums\DATA_CATEGORY;
+use App\Module\Cleanup\Enums\CLEANUP_TYPE;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+
+/**
+ * 清理计划管理逻辑类
+ * 
+ * 负责清理计划的创建、管理和内容生成
+ */
+class CleanupPlanLogic
+{
+    /**
+     * 创建清理计划
+     *
+     * @param array $planData 计划数据
+     * @return array 创建结果
+     */
+    public static function createPlan(array $planData): array
+    {
+        try {
+            DB::beginTransaction();
+
+            // 验证计划数据
+            $validatedData = static::validatePlanData($planData);
+            
+            // 创建计划
+            $plan = CleanupPlan::create($validatedData);
+            
+            // 如果需要自动生成内容,则生成计划内容
+            if ($planData['auto_generate_contents'] ?? true) {
+                $contentResult = static::generateContents($plan->id, true);
+                if (!$contentResult['success']) {
+                    throw new \Exception('生成计划内容失败: ' . $contentResult['message']);
+                }
+            }
+
+            DB::commit();
+
+            return [
+                'success' => true,
+                'message' => '清理计划创建成功',
+                'data' => [
+                    'plan_id' => $plan->id,
+                    'plan_name' => $plan->plan_name,
+                    'plan_type' => $plan->plan_type,
+                    'contents_count' => $plan->contents()->count(),
+                ]
+            ];
+
+        } catch (\Exception $e) {
+            DB::rollBack();
+            Log::error('创建清理计划失败', [
+                'plan_data' => $planData,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+
+            return [
+                'success' => false,
+                'message' => '创建清理计划失败: ' . $e->getMessage(),
+                'data' => null
+            ];
+        }
+    }
+
+    /**
+     * 为计划生成内容配置
+     *
+     * @param int $planId 计划ID
+     * @param bool $autoGenerate 是否自动生成
+     * @return array 生成结果
+     */
+    public static function generateContents(int $planId, bool $autoGenerate = true): array
+    {
+        try {
+            $plan = CleanupPlan::findOrFail($planId);
+            
+            // 根据计划类型获取目标表
+            $targetTables = static::getTargetTables($plan);
+            
+            if (empty($targetTables)) {
+                return [
+                    'success' => false,
+                    'message' => '未找到符合条件的目标表',
+                    'data' => null
+                ];
+            }
+
+            $generatedCount = 0;
+            $skippedCount = 0;
+            $errors = [];
+
+            foreach ($targetTables as $tableName) {
+                try {
+                    // 检查是否已存在内容配置
+                    $existingContent = CleanupPlanContent::where('plan_id', $planId)
+                        ->where('table_name', $tableName)
+                        ->first();
+
+                    if ($existingContent && !$autoGenerate) {
+                        $skippedCount++;
+                        continue;
+                    }
+
+                    // 获取表的配置信息
+                    $tableConfig = CleanupConfig::where('table_name', $tableName)->first();
+                    
+                    // 生成内容配置
+                    $contentData = static::generateTableContent($plan, $tableName, $tableConfig);
+                    
+                    if ($existingContent) {
+                        $existingContent->update($contentData);
+                    } else {
+                        $contentData['plan_id'] = $planId;
+                        $contentData['table_name'] = $tableName;
+                        CleanupPlanContent::create($contentData);
+                    }
+                    
+                    $generatedCount++;
+
+                } catch (\Exception $e) {
+                    $errors[] = "表 {$tableName}: " . $e->getMessage();
+                    Log::error("生成表内容配置失败", [
+                        'plan_id' => $planId,
+                        'table_name' => $tableName,
+                        'error' => $e->getMessage()
+                    ]);
+                }
+            }
+
+            return [
+                'success' => true,
+                'message' => "内容生成完成,生成 {$generatedCount} 个,跳过 {$skippedCount} 个",
+                'data' => [
+                    'generated_count' => $generatedCount,
+                    'skipped_count' => $skippedCount,
+                    'total_tables' => count($targetTables),
+                    'errors' => $errors
+                ]
+            ];
+
+        } catch (\Exception $e) {
+            Log::error('生成计划内容失败', [
+                'plan_id' => $planId,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+
+            return [
+                'success' => false,
+                'message' => '生成计划内容失败: ' . $e->getMessage(),
+                'data' => null
+            ];
+        }
+    }
+
+    /**
+     * 验证计划数据
+     *
+     * @param array $planData 计划数据
+     * @return array 验证后的数据
+     * @throws \Exception
+     */
+    private static function validatePlanData(array $planData): array
+    {
+        // 必填字段验证
+        $required = ['plan_name', 'plan_type'];
+        foreach ($required as $field) {
+            if (empty($planData[$field])) {
+                throw new \Exception("字段 {$field} 不能为空");
+            }
+        }
+
+        // 验证计划类型
+        $planType = PLAN_TYPE::tryFrom($planData['plan_type']);
+        if (!$planType) {
+            throw new \Exception('无效的计划类型');
+        }
+
+        // 根据计划类型验证目标选择
+        if ($planType !== PLAN_TYPE::CUSTOM && empty($planData['target_selection'])) {
+            throw new \Exception('目标选择不能为空');
+        }
+
+        return [
+            'plan_name' => $planData['plan_name'],
+            'plan_type' => $planData['plan_type'],
+            'target_selection' => $planData['target_selection'] ?? [],
+            'global_conditions' => $planData['global_conditions'] ?? [],
+            'backup_config' => $planData['backup_config'] ?? [],
+            'is_template' => $planData['is_template'] ?? false,
+            'is_enabled' => $planData['is_enabled'] ?? true,
+            'description' => $planData['description'] ?? '',
+            'created_by' => $planData['created_by'] ?? 0,
+        ];
+    }
+
+    /**
+     * 根据计划获取目标表列表
+     *
+     * @param CleanupPlan $plan 清理计划
+     * @return array 目标表列表
+     */
+    private static function getTargetTables(CleanupPlan $plan): array
+    {
+        $planType = PLAN_TYPE::from($plan->plan_type);
+        $targetSelection = $plan->target_selection;
+
+        switch ($planType) {
+            case PLAN_TYPE::MODULE:
+                return static::getModuleTables($targetSelection['modules'] ?? []);
+                
+            case PLAN_TYPE::CATEGORY:
+                return static::getCategoryTables($targetSelection['categories'] ?? []);
+                
+            case PLAN_TYPE::FULL:
+                return static::getAllTables($targetSelection['exclude_tables'] ?? []);
+                
+            case PLAN_TYPE::MIXED:
+                return static::getMixedTables($targetSelection);
+                
+            case PLAN_TYPE::CUSTOM:
+                return $targetSelection['tables'] ?? [];
+                
+            default:
+                return [];
+        }
+    }
+
+    /**
+     * 获取指定模块的表
+     *
+     * @param array $modules 模块列表
+     * @return array 表列表
+     */
+    private static function getModuleTables(array $modules): array
+    {
+        if (empty($modules)) {
+            return [];
+        }
+
+        return CleanupConfig::whereIn('module_name', $modules)
+            ->where('is_enabled', true)
+            ->pluck('table_name')
+            ->toArray();
+    }
+
+    /**
+     * 获取指定分类的表
+     *
+     * @param array $categories 分类列表
+     * @return array 表列表
+     */
+    private static function getCategoryTables(array $categories): array
+    {
+        if (empty($categories)) {
+            return [];
+        }
+
+        return CleanupConfig::whereIn('data_category', $categories)
+            ->where('is_enabled', true)
+            ->pluck('table_name')
+            ->toArray();
+    }
+
+    /**
+     * 获取所有表(排除指定表)
+     *
+     * @param array $excludeTables 排除的表
+     * @return array 表列表
+     */
+    private static function getAllTables(array $excludeTables = []): array
+    {
+        $query = CleanupConfig::where('is_enabled', true);
+        
+        if (!empty($excludeTables)) {
+            $query->whereNotIn('table_name', $excludeTables);
+        }
+
+        return $query->pluck('table_name')->toArray();
+    }
+
+    /**
+     * 获取混合选择的表
+     *
+     * @param array $selection 混合选择配置
+     * @return array 表列表
+     */
+    private static function getMixedTables(array $selection): array
+    {
+        $tables = [];
+
+        // 添加指定模块的表
+        if (!empty($selection['modules'])) {
+            $tables = array_merge($tables, static::getModuleTables($selection['modules']));
+        }
+
+        // 添加指定分类的表
+        if (!empty($selection['categories'])) {
+            $tables = array_merge($tables, static::getCategoryTables($selection['categories']));
+        }
+
+        // 添加自定义表
+        if (!empty($selection['tables'])) {
+            $tables = array_merge($tables, $selection['tables']);
+        }
+
+        // 排除指定表
+        if (!empty($selection['exclude_tables'])) {
+            $tables = array_diff($tables, $selection['exclude_tables']);
+        }
+
+        return array_unique($tables);
+    }
+
+    /**
+     * 为表生成内容配置
+     *
+     * @param CleanupPlan $plan 清理计划
+     * @param string $tableName 表名
+     * @param CleanupConfig|null $tableConfig 表配置
+     * @return array 内容配置数据
+     */
+    private static function generateTableContent(CleanupPlan $plan, string $tableName, ?CleanupConfig $tableConfig): array
+    {
+        // 基础配置
+        $contentData = [
+            'cleanup_type' => $tableConfig?->default_cleanup_type ?? CLEANUP_TYPE::DELETE_ALL->value,
+            'conditions' => $tableConfig?->default_conditions ?? [],
+            'priority' => $tableConfig?->priority ?? 100,
+            'batch_size' => $tableConfig?->batch_size ?? 1000,
+            'backup_enabled' => true,
+            'is_enabled' => true,
+            'notes' => $tableConfig?->description ?? "自动生成的 {$tableName} 表清理配置",
+        ];
+
+        // 合并计划的全局条件
+        if (!empty($plan->global_conditions)) {
+            $contentData['conditions'] = array_merge(
+                $contentData['conditions'],
+                $plan->global_conditions
+            );
+        }
+
+        // 合并计划的备份配置
+        if (!empty($plan->backup_config)) {
+            $contentData['backup_config'] = $plan->backup_config;
+        }
+
+        return $contentData;
+    }
+
+    /**
+     * 获取计划详情
+     *
+     * @param int $planId 计划ID
+     * @return array 计划详情
+     */
+    public static function getPlanDetails(int $planId): array
+    {
+        try {
+            $plan = CleanupPlan::with(['contents.config'])->findOrFail($planId);
+
+            $contents = $plan->contents->map(function ($content) {
+                return [
+                    'id' => $content->id,
+                    'table_name' => $content->table_name,
+                    'cleanup_type' => $content->cleanup_type,
+                    'cleanup_type_name' => CLEANUP_TYPE::from($content->cleanup_type)->getDescription(),
+                    'conditions' => $content->conditions,
+                    'priority' => $content->priority,
+                    'batch_size' => $content->batch_size,
+                    'backup_enabled' => $content->backup_enabled,
+                    'is_enabled' => $content->is_enabled,
+                    'notes' => $content->notes,
+                    'module_name' => $content->config?->module_name,
+                    'data_category' => $content->config?->data_category,
+                ];
+            });
+
+            return [
+                'success' => true,
+                'data' => [
+                    'plan' => [
+                        'id' => $plan->id,
+                        'plan_name' => $plan->plan_name,
+                        'plan_type' => $plan->plan_type,
+                        'plan_type_name' => PLAN_TYPE::from($plan->plan_type)->getDescription(),
+                        'target_selection' => $plan->target_selection,
+                        'global_conditions' => $plan->global_conditions,
+                        'backup_config' => $plan->backup_config,
+                        'is_template' => $plan->is_template,
+                        'is_enabled' => $plan->is_enabled,
+                        'description' => $plan->description,
+                        'created_at' => $plan->created_at,
+                        'updated_at' => $plan->updated_at,
+                    ],
+                    'contents' => $contents,
+                    'statistics' => [
+                        'total_tables' => $contents->count(),
+                        'enabled_tables' => $contents->where('is_enabled', true)->count(),
+                        'backup_enabled_tables' => $contents->where('backup_enabled', true)->count(),
+                    ]
+                ]
+            ];
+
+        } catch (\Exception $e) {
+            Log::error('获取计划详情失败', [
+                'plan_id' => $planId,
+                'error' => $e->getMessage()
+            ]);
+
+            return [
+                'success' => false,
+                'message' => '获取计划详情失败: ' . $e->getMessage(),
+                'data' => null
+            ];
+        }
+    }
+
+    /**
+     * 更新计划
+     *
+     * @param int $planId 计划ID
+     * @param array $planData 计划数据
+     * @return array 更新结果
+     */
+    public static function updatePlan(int $planId, array $planData): array
+    {
+        try {
+            $plan = CleanupPlan::findOrFail($planId);
+
+            // 验证数据
+            $validatedData = static::validatePlanData($planData);
+
+            // 更新计划
+            $plan->update($validatedData);
+
+            return [
+                'success' => true,
+                'message' => '计划更新成功',
+                'data' => [
+                    'plan_id' => $plan->id,
+                    'plan_name' => $plan->plan_name,
+                ]
+            ];
+
+        } catch (\Exception $e) {
+            Log::error('更新计划失败', [
+                'plan_id' => $planId,
+                'plan_data' => $planData,
+                'error' => $e->getMessage()
+            ]);
+
+            return [
+                'success' => false,
+                'message' => '更新计划失败: ' . $e->getMessage(),
+                'data' => null
+            ];
+        }
+    }
+
+    /**
+     * 删除计划
+     *
+     * @param int $planId 计划ID
+     * @return array 删除结果
+     */
+    public static function deletePlan(int $planId): array
+    {
+        try {
+            DB::beginTransaction();
+
+            $plan = CleanupPlan::findOrFail($planId);
+
+            // 检查是否有关联的任务
+            if ($plan->tasks()->exists()) {
+                throw new \Exception('该计划存在关联的任务,无法删除');
+            }
+
+            // 删除计划内容
+            $plan->contents()->delete();
+
+            // 删除计划
+            $plan->delete();
+
+            DB::commit();
+
+            return [
+                'success' => true,
+                'message' => '计划删除成功',
+                'data' => null
+            ];
+
+        } catch (\Exception $e) {
+            DB::rollBack();
+            Log::error('删除计划失败', [
+                'plan_id' => $planId,
+                'error' => $e->getMessage()
+            ]);
+
+            return [
+                'success' => false,
+                'message' => '删除计划失败: ' . $e->getMessage(),
+                'data' => null
+            ];
+        }
+    }
+}

+ 340 - 0
app/Module/Cleanup/Logics/CleanupTaskLogic.php

@@ -0,0 +1,340 @@
+<?php
+
+namespace App\Module\Cleanup\Logics;
+
+use App\Module\Cleanup\Models\CleanupTask;
+use App\Module\Cleanup\Models\CleanupPlan;
+use App\Module\Cleanup\Models\CleanupBackup;
+use App\Module\Cleanup\Enums\TASK_STATUS;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+
+/**
+ * 清理任务管理逻辑类
+ * 
+ * 负责清理任务的创建、管理和状态控制
+ */
+class CleanupTaskLogic
+{
+    /**
+     * 基于计划创建清理任务
+     *
+     * @param int $planId 计划ID
+     * @param array $taskOptions 任务选项
+     * @return array 创建结果
+     */
+    public static function createTask(int $planId, array $taskOptions = []): array
+    {
+        try {
+            DB::beginTransaction();
+
+            $plan = CleanupPlan::with('contents')->findOrFail($planId);
+            
+            if (!$plan->is_enabled) {
+                throw new \Exception('计划已禁用,无法创建任务');
+            }
+
+            if ($plan->contents->isEmpty()) {
+                throw new \Exception('计划没有配置内容,无法创建任务');
+            }
+
+            // 验证任务选项
+            $validatedOptions = static::validateTaskOptions($taskOptions);
+            
+            // 统计任务信息
+            $enabledContents = $plan->contents->where('is_enabled', true);
+            $totalTables = $enabledContents->count();
+            
+            if ($totalTables === 0) {
+                throw new \Exception('计划中没有启用的表配置,无法创建任务');
+            }
+
+            // 创建任务
+            $task = CleanupTask::create([
+                'task_name' => $validatedOptions['task_name'] ?? "清理任务 - {$plan->plan_name}",
+                'plan_id' => $planId,
+                'status' => TASK_STATUS::PENDING->value,
+                'progress' => 0,
+                'current_step' => '准备中',
+                'total_tables' => $totalTables,
+                'processed_tables' => 0,
+                'total_records' => 0,
+                'deleted_records' => 0,
+                'backup_size' => 0,
+                'execution_time' => 0,
+                'backup_time' => 0,
+                'created_by' => $validatedOptions['created_by'] ?? 0,
+            ]);
+
+            DB::commit();
+
+            return [
+                'success' => true,
+                'message' => '清理任务创建成功',
+                'data' => [
+                    'task_id' => $task->id,
+                    'task_name' => $task->task_name,
+                    'plan_name' => $plan->plan_name,
+                    'total_tables' => $totalTables,
+                    'status' => TASK_STATUS::from($task->status)->getDescription(),
+                ]
+            ];
+
+        } catch (\Exception $e) {
+            DB::rollBack();
+            Log::error('创建清理任务失败', [
+                'plan_id' => $planId,
+                'task_options' => $taskOptions,
+                'error' => $e->getMessage(),
+                'trace' => $e->getTraceAsString()
+            ]);
+
+            return [
+                'success' => false,
+                'message' => '创建清理任务失败: ' . $e->getMessage(),
+                'data' => null
+            ];
+        }
+    }
+
+    /**
+     * 更新任务状态
+     *
+     * @param int $taskId 任务ID
+     * @param TASK_STATUS $status 新状态
+     * @param array $updateData 更新数据
+     * @return array 更新结果
+     */
+    public static function updateTaskStatus(int $taskId, TASK_STATUS $status, array $updateData = []): array
+    {
+        try {
+            $task = CleanupTask::findOrFail($taskId);
+            
+            $data = array_merge($updateData, [
+                'status' => $status->value,
+            ]);
+
+            // 根据状态设置时间戳
+            switch ($status) {
+                case TASK_STATUS::RUNNING:
+                    $data['started_at'] = now();
+                    break;
+                case TASK_STATUS::BACKING_UP:
+                    $data['started_at'] = $data['started_at'] ?? now();
+                    break;
+                case TASK_STATUS::COMPLETED:
+                    $data['completed_at'] = now();
+                    $data['progress'] = 100;
+                    break;
+                case TASK_STATUS::FAILED:
+                case TASK_STATUS::CANCELLED:
+                    $data['completed_at'] = now();
+                    break;
+            }
+
+            $task->update($data);
+
+            return [
+                'success' => true,
+                'message' => '任务状态更新成功',
+                'data' => [
+                    'task_id' => $task->id,
+                    'status' => $status->getDescription(),
+                    'progress' => $task->progress,
+                ]
+            ];
+
+        } catch (\Exception $e) {
+            Log::error('更新任务状态失败', [
+                'task_id' => $taskId,
+                'status' => $status->value,
+                'update_data' => $updateData,
+                'error' => $e->getMessage()
+            ]);
+
+            return [
+                'success' => false,
+                'message' => '更新任务状态失败: ' . $e->getMessage(),
+                'data' => null
+            ];
+        }
+    }
+
+    /**
+     * 更新任务进度
+     *
+     * @param int $taskId 任务ID
+     * @param int $processedTables 已处理表数
+     * @param int $deletedRecords 已删除记录数
+     * @param string $currentStep 当前步骤
+     * @return array 更新结果
+     */
+    public static function updateTaskProgress(int $taskId, int $processedTables, int $deletedRecords, string $currentStep): array
+    {
+        try {
+            $task = CleanupTask::findOrFail($taskId);
+            
+            $progress = $task->total_tables > 0 ? round(($processedTables / $task->total_tables) * 100, 2) : 0;
+            
+            $task->update([
+                'progress' => $progress,
+                'processed_tables' => $processedTables,
+                'deleted_records' => $deletedRecords,
+                'current_step' => $currentStep,
+            ]);
+
+            return [
+                'success' => true,
+                'data' => [
+                    'task_id' => $task->id,
+                    'progress' => $progress,
+                    'processed_tables' => $processedTables,
+                    'total_tables' => $task->total_tables,
+                    'deleted_records' => $deletedRecords,
+                    'current_step' => $currentStep,
+                ]
+            ];
+
+        } catch (\Exception $e) {
+            Log::error('更新任务进度失败', [
+                'task_id' => $taskId,
+                'processed_tables' => $processedTables,
+                'deleted_records' => $deletedRecords,
+                'current_step' => $currentStep,
+                'error' => $e->getMessage()
+            ]);
+
+            return [
+                'success' => false,
+                'message' => '更新任务进度失败: ' . $e->getMessage(),
+                'data' => null
+            ];
+        }
+    }
+
+    /**
+     * 取消任务
+     *
+     * @param int $taskId 任务ID
+     * @param string $reason 取消原因
+     * @return array 取消结果
+     */
+    public static function cancelTask(int $taskId, string $reason = ''): array
+    {
+        try {
+            $task = CleanupTask::findOrFail($taskId);
+            
+            // 检查任务状态
+            $currentStatus = TASK_STATUS::from($task->status);
+            if (in_array($currentStatus, [TASK_STATUS::COMPLETED, TASK_STATUS::FAILED, TASK_STATUS::CANCELLED])) {
+                throw new \Exception('任务已完成或已取消,无法再次取消');
+            }
+
+            $task->update([
+                'status' => TASK_STATUS::CANCELLED->value,
+                'completed_at' => now(),
+                'error_message' => $reason ?: '用户取消',
+            ]);
+
+            return [
+                'success' => true,
+                'message' => '任务已取消',
+                'data' => [
+                    'task_id' => $task->id,
+                    'status' => TASK_STATUS::CANCELLED->getDescription(),
+                ]
+            ];
+
+        } catch (\Exception $e) {
+            Log::error('取消任务失败', [
+                'task_id' => $taskId,
+                'reason' => $reason,
+                'error' => $e->getMessage()
+            ]);
+
+            return [
+                'success' => false,
+                'message' => '取消任务失败: ' . $e->getMessage(),
+                'data' => null
+            ];
+        }
+    }
+
+    /**
+     * 获取任务详情
+     *
+     * @param int $taskId 任务ID
+     * @return array 任务详情
+     */
+    public static function getTaskDetails(int $taskId): array
+    {
+        try {
+            $task = CleanupTask::with(['plan', 'backup'])->findOrFail($taskId);
+            
+            return [
+                'success' => true,
+                'data' => [
+                    'task' => [
+                        'id' => $task->id,
+                        'task_name' => $task->task_name,
+                        'status' => $task->status,
+                        'status_name' => TASK_STATUS::from($task->status)->getDescription(),
+                        'progress' => $task->progress,
+                        'current_step' => $task->current_step,
+                        'total_tables' => $task->total_tables,
+                        'processed_tables' => $task->processed_tables,
+                        'total_records' => $task->total_records,
+                        'deleted_records' => $task->deleted_records,
+                        'backup_size' => $task->backup_size,
+                        'execution_time' => $task->execution_time,
+                        'backup_time' => $task->backup_time,
+                        'started_at' => $task->started_at,
+                        'backup_completed_at' => $task->backup_completed_at,
+                        'completed_at' => $task->completed_at,
+                        'error_message' => $task->error_message,
+                        'created_at' => $task->created_at,
+                    ],
+                    'plan' => $task->plan ? [
+                        'id' => $task->plan->id,
+                        'plan_name' => $task->plan->plan_name,
+                        'plan_type' => $task->plan->plan_type,
+                        'description' => $task->plan->description,
+                    ] : null,
+                    'backup' => $task->backup ? [
+                        'id' => $task->backup->id,
+                        'backup_name' => $task->backup->backup_name,
+                        'backup_size' => $task->backup->backup_size,
+                        'file_count' => $task->backup->file_count,
+                        'status' => $task->backup->status,
+                    ] : null,
+                ]
+            ];
+
+        } catch (\Exception $e) {
+            Log::error('获取任务详情失败', [
+                'task_id' => $taskId,
+                'error' => $e->getMessage()
+            ]);
+
+            return [
+                'success' => false,
+                'message' => '获取任务详情失败: ' . $e->getMessage(),
+                'data' => null
+            ];
+        }
+    }
+
+    /**
+     * 验证任务选项
+     *
+     * @param array $taskOptions 任务选项
+     * @return array 验证后的选项
+     */
+    private static function validateTaskOptions(array $taskOptions): array
+    {
+        return [
+            'task_name' => $taskOptions['task_name'] ?? null,
+            'created_by' => $taskOptions['created_by'] ?? 0,
+        ];
+    }
+}

+ 112 - 0
app/Module/Cleanup/Models/CleanupBackupFile.php

@@ -0,0 +1,112 @@
+<?php
+
+namespace App\Module\Cleanup\Models;
+
+use App\Module\Cleanup\Enums\BACKUP_TYPE;
+use App\Module\Cleanup\Enums\COMPRESSION_TYPE;
+use UCore\ModelCore;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+/**
+ * 清理备份文件模型
+ * 
+ * 记录备份文件的详细信息
+ */
+class CleanupBackupFile extends ModelCore
+{
+    /**
+     * 数据表名
+     */
+    protected $table = 'cleanup_backup_files';
+
+    // field start
+    /**
+     * 可批量赋值的字段
+     */
+    protected $fillable = [
+        'backup_id',
+        'table_name',
+        'file_name',
+        'file_path',
+        'file_size',
+        'file_hash',
+        'backup_type',
+        'compression_type',
+    ];
+    // field end
+
+    /**
+     * 字段类型转换
+     */
+    protected $casts = [
+        'backup_id' => 'integer',
+        'file_size' => 'integer',
+        'backup_type' => 'integer',
+        'compression_type' => 'integer',
+        'created_at' => 'datetime',
+        'updated_at' => 'datetime',
+    ];
+
+    /**
+     * 获取备份类型枚举
+     */
+    public function getBackupTypeEnumAttribute(): BACKUP_TYPE
+    {
+        return BACKUP_TYPE::from($this->backup_type);
+    }
+
+    /**
+     * 获取压缩类型枚举
+     */
+    public function getCompressionTypeEnumAttribute(): COMPRESSION_TYPE
+    {
+        return COMPRESSION_TYPE::from($this->compression_type);
+    }
+
+    /**
+     * 关联备份记录
+     */
+    public function backup(): BelongsTo
+    {
+        return $this->belongsTo(CleanupBackup::class, 'backup_id');
+    }
+
+    /**
+     * 按表名筛选
+     */
+    public function scopeByTable($query, string $tableName)
+    {
+        return $query->where('table_name', $tableName);
+    }
+
+    /**
+     * 按备份类型筛选
+     */
+    public function scopeByBackupType($query, BACKUP_TYPE $backupType)
+    {
+        return $query->where('backup_type', $backupType->value);
+    }
+
+    /**
+     * 按压缩类型筛选
+     */
+    public function scopeByCompressionType($query, COMPRESSION_TYPE $compressionType)
+    {
+        return $query->where('compression_type', $compressionType->value);
+    }
+
+    /**
+     * 获取文件大小的可读格式
+     */
+    public function getFileSizeHumanAttribute(): string
+    {
+        $bytes = $this->file_size;
+        $units = ['B', 'KB', 'MB', 'GB', 'TB'];
+        
+        for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
+            $bytes /= 1024;
+        }
+        
+        return round($bytes, 2) . ' ' . $units[$i];
+    }
+}

+ 100 - 0
app/Module/Cleanup/Models/CleanupLog.php

@@ -0,0 +1,100 @@
+<?php
+
+namespace App\Module\Cleanup\Models;
+
+use App\Module\Cleanup\Enums\CLEANUP_TYPE;
+use UCore\ModelCore;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
+
+/**
+ * 清理日志模型
+ * 
+ * 记录每个表的清理操作详情
+ */
+class CleanupLog extends ModelCore
+{
+    /**
+     * 数据表名
+     */
+    protected $table = 'cleanup_logs';
+
+    // field start
+    /**
+     * 可批量赋值的字段
+     */
+    protected $fillable = [
+        'task_id',
+        'table_name',
+        'cleanup_type',
+        'before_count',
+        'after_count',
+        'deleted_records',
+        'execution_time',
+        'conditions',
+        'error_message',
+    ];
+    // field end
+
+    /**
+     * 字段类型转换
+     */
+    protected $casts = [
+        'task_id' => 'integer',
+        'cleanup_type' => 'integer',
+        'before_count' => 'integer',
+        'after_count' => 'integer',
+        'deleted_records' => 'integer',
+        'execution_time' => 'float',
+        'conditions' => 'array',
+        'created_at' => 'datetime',
+        'updated_at' => 'datetime',
+    ];
+
+    /**
+     * 获取清理类型枚举
+     */
+    public function getCleanupTypeEnumAttribute(): CLEANUP_TYPE
+    {
+        return CLEANUP_TYPE::from($this->cleanup_type);
+    }
+
+    /**
+     * 关联任务
+     */
+    public function task(): BelongsTo
+    {
+        return $this->belongsTo(CleanupTask::class, 'task_id');
+    }
+
+    /**
+     * 获取成功的日志
+     */
+    public function scopeSuccessful($query)
+    {
+        return $query->whereNull('error_message');
+    }
+
+    /**
+     * 获取失败的日志
+     */
+    public function scopeFailed($query)
+    {
+        return $query->whereNotNull('error_message');
+    }
+
+    /**
+     * 按表名筛选
+     */
+    public function scopeByTable($query, string $tableName)
+    {
+        return $query->where('table_name', $tableName);
+    }
+
+    /**
+     * 按清理类型筛选
+     */
+    public function scopeByCleanupType($query, CLEANUP_TYPE $cleanupType)
+    {
+        return $query->where('cleanup_type', $cleanupType->value);
+    }
+}

+ 143 - 0
app/Module/Cleanup/Models/CleanupTableStats.php

@@ -0,0 +1,143 @@
+<?php
+
+namespace App\Module\Cleanup\Models;
+
+use UCore\ModelCore;
+
+/**
+ * 清理表统计模型
+ * 
+ * 记录表的统计信息和扫描历史
+ */
+class CleanupTableStats extends ModelCore
+{
+    /**
+     * 数据表名
+     */
+    protected $table = 'cleanup_table_stats';
+
+    // field start
+    /**
+     * 可批量赋值的字段
+     */
+    protected $fillable = [
+        'table_name',
+        'record_count',
+        'table_size_mb',
+        'index_size_mb',
+        'data_free_mb',
+        'avg_row_length',
+        'auto_increment',
+        'engine',
+        'collation',
+        'has_time_field',
+        'time_fields',
+        'has_user_field',
+        'user_fields',
+        'has_status_field',
+        'status_fields',
+        'last_scanned_at',
+    ];
+    // field end
+
+    /**
+     * 字段类型转换
+     */
+    protected $casts = [
+        'record_count' => 'integer',
+        'table_size_mb' => 'float',
+        'index_size_mb' => 'float',
+        'data_free_mb' => 'float',
+        'avg_row_length' => 'integer',
+        'auto_increment' => 'integer',
+        'has_time_field' => 'boolean',
+        'time_fields' => 'array',
+        'has_user_field' => 'boolean',
+        'user_fields' => 'array',
+        'has_status_field' => 'boolean',
+        'status_fields' => 'array',
+        'last_scanned_at' => 'datetime',
+        'created_at' => 'datetime',
+        'updated_at' => 'datetime',
+    ];
+
+    /**
+     * 按表名筛选
+     */
+    public function scopeByTable($query, string $tableName)
+    {
+        return $query->where('table_name', $tableName);
+    }
+
+    /**
+     * 获取大表(记录数超过指定数量)
+     */
+    public function scopeLargeTables($query, int $minRecords = 10000)
+    {
+        return $query->where('record_count', '>', $minRecords);
+    }
+
+    /**
+     * 获取有时间字段的表
+     */
+    public function scopeWithTimeFields($query)
+    {
+        return $query->where('has_time_field', true);
+    }
+
+    /**
+     * 获取有用户字段的表
+     */
+    public function scopeWithUserFields($query)
+    {
+        return $query->where('has_user_field', true);
+    }
+
+    /**
+     * 获取有状态字段的表
+     */
+    public function scopeWithStatusFields($query)
+    {
+        return $query->where('has_status_field', true);
+    }
+
+    /**
+     * 获取表大小的可读格式
+     */
+    public function getTableSizeHumanAttribute(): string
+    {
+        $mb = $this->table_size_mb;
+        
+        if ($mb < 1) {
+            return round($mb * 1024, 2) . ' KB';
+        } elseif ($mb < 1024) {
+            return round($mb, 2) . ' MB';
+        } else {
+            return round($mb / 1024, 2) . ' GB';
+        }
+    }
+
+    /**
+     * 获取总大小(表+索引)
+     */
+    public function getTotalSizeMbAttribute(): float
+    {
+        return $this->table_size_mb + $this->index_size_mb;
+    }
+
+    /**
+     * 获取总大小的可读格式
+     */
+    public function getTotalSizeHumanAttribute(): string
+    {
+        $mb = $this->total_size_mb;
+        
+        if ($mb < 1) {
+            return round($mb * 1024, 2) . ' KB';
+        } elseif ($mb < 1024) {
+            return round($mb, 2) . ' MB';
+        } else {
+            return round($mb / 1024, 2) . ' GB';
+        }
+    }
+}

+ 0 - 1
app/Module/Farm/Providers/FarmServiceProvider.php

@@ -65,7 +65,6 @@ class FarmServiceProvider extends ServiceProvider
             Commands\MigrateLandUpgradeMaterialsToConsumeGroupsCommand::class,
             Commands\MigrateLandUpgradeConditionsToConditionGroupsCommand::class,
             Commands\InitializeUserLandsCommand::class,
-            Commands\TestFarmConfigCommand::class
         ]);
 
 

+ 3 - 0
config/app.php

@@ -226,6 +226,9 @@ return [
         // UrsPromotion 模块
         \App\Module\UrsPromotion\Providers\UrsPromotionServiceProvider::class,
 
+        // Cleanup 模块
+        \App\Module\Cleanup\CleanupServiceProvider::class,
+
         // ThirdParty 包
         \ThirdParty\Urs\UrsServiceProvider::class,