CleanupExecutorLogic.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. <?php
  2. namespace App\Module\Cleanup\Logics;
  3. use App\Module\Cleanup\Models\CleanupTask;
  4. use App\Module\Cleanup\Models\CleanupPlan;
  5. use App\Module\Cleanup\Models\CleanupPlanContent;
  6. use App\Module\Cleanup\Models\CleanupLog;
  7. use App\Module\Cleanup\Enums\TASK_STATUS;
  8. use App\Module\Cleanup\Enums\CLEANUP_TYPE;
  9. use Illuminate\Support\Facades\DB;
  10. use Illuminate\Support\Facades\Log;
  11. use Illuminate\Support\Facades\Schema;
  12. /**
  13. * 清理执行逻辑类
  14. *
  15. * 负责实际的数据清理执行操作
  16. */
  17. class CleanupExecutorLogic
  18. {
  19. /**
  20. * 预览计划的清理结果
  21. *
  22. * @param int $planId 计划ID
  23. * @return array 预览结果
  24. */
  25. public static function previewPlanCleanup(int $planId): array
  26. {
  27. try {
  28. $plan = CleanupPlan::with('contents')->findOrFail($planId);
  29. $previewData = [];
  30. $totalRecords = 0;
  31. $totalTables = 0;
  32. foreach ($plan->contents->where('is_enabled', true) as $content) {
  33. $tablePreview = static::previewTableCleanup($content);
  34. $previewData[] = $tablePreview;
  35. $totalRecords += $tablePreview['affected_records'];
  36. $totalTables++;
  37. }
  38. return [
  39. 'success' => true,
  40. 'data' => [
  41. 'plan' => [
  42. 'id' => $plan->id,
  43. 'plan_name' => $plan->plan_name,
  44. 'description' => $plan->description,
  45. ],
  46. 'summary' => [
  47. 'total_tables' => $totalTables,
  48. 'total_records' => $totalRecords,
  49. 'estimated_time' => static::estimateExecutionTime($totalRecords),
  50. ],
  51. 'tables' => $previewData,
  52. ]
  53. ];
  54. } catch (\Exception $e) {
  55. Log::error('预览计划清理失败', [
  56. 'plan_id' => $planId,
  57. 'error' => $e->getMessage()
  58. ]);
  59. return [
  60. 'success' => false,
  61. 'message' => '预览计划清理失败: ' . $e->getMessage(),
  62. 'data' => null
  63. ];
  64. }
  65. }
  66. /**
  67. * 预览任务的清理结果
  68. *
  69. * @param int $taskId 任务ID
  70. * @return array 预览结果
  71. */
  72. public static function previewTaskCleanup(int $taskId): array
  73. {
  74. try {
  75. $task = CleanupTask::with('plan.contents')->findOrFail($taskId);
  76. if (!$task->plan) {
  77. throw new \Exception('任务关联的计划不存在');
  78. }
  79. return static::previewPlanCleanup($task->plan->id);
  80. } catch (\Exception $e) {
  81. Log::error('预览任务清理失败', [
  82. 'task_id' => $taskId,
  83. 'error' => $e->getMessage()
  84. ]);
  85. return [
  86. 'success' => false,
  87. 'message' => '预览任务清理失败: ' . $e->getMessage(),
  88. 'data' => null
  89. ];
  90. }
  91. }
  92. /**
  93. * 执行清理任务
  94. *
  95. * @param int $taskId 任务ID
  96. * @param bool $dryRun 是否为预演模式
  97. * @return array 执行结果
  98. */
  99. public static function executeTask(int $taskId, bool $dryRun = false): array
  100. {
  101. try {
  102. $task = CleanupTask::with('plan.contents')->findOrFail($taskId);
  103. if (!$task->plan) {
  104. throw new \Exception('任务关联的计划不存在');
  105. }
  106. // 检查任务状态
  107. $currentStatus = TASK_STATUS::from($task->status);
  108. if ($currentStatus !== TASK_STATUS::PENDING) {
  109. throw new \Exception('任务状态不正确,无法执行');
  110. }
  111. if ($dryRun) {
  112. return static::previewTaskCleanup($taskId);
  113. }
  114. // 开始执行任务
  115. return static::executeTaskInternal($task);
  116. } catch (\Exception $e) {
  117. Log::error('执行清理任务失败', [
  118. 'task_id' => $taskId,
  119. 'dry_run' => $dryRun,
  120. 'error' => $e->getMessage(),
  121. 'trace' => $e->getTraceAsString()
  122. ]);
  123. return [
  124. 'success' => false,
  125. 'message' => '执行清理任务失败: ' . $e->getMessage(),
  126. 'data' => null
  127. ];
  128. }
  129. }
  130. /**
  131. * 内部执行任务逻辑
  132. *
  133. * @param CleanupTask $task 任务对象
  134. * @return array 执行结果
  135. */
  136. private static function executeTaskInternal(CleanupTask $task): array
  137. {
  138. $startTime = microtime(true);
  139. $totalDeleted = 0;
  140. $processedTables = 0;
  141. $errors = [];
  142. try {
  143. // 更新任务状态为执行中
  144. CleanupTaskLogic::updateTaskStatus($task->id, TASK_STATUS::RUNNING, [
  145. 'current_step' => '开始执行清理',
  146. ]);
  147. // 获取启用的内容配置
  148. $enabledContents = $task->plan->contents->where('is_enabled', true)->sortBy('priority');
  149. foreach ($enabledContents as $content) {
  150. try {
  151. // 更新当前步骤
  152. CleanupTaskLogic::updateTaskProgress(
  153. $task->id,
  154. $processedTables,
  155. $totalDeleted,
  156. "正在清理表: {$content->table_name}"
  157. );
  158. // 执行表清理
  159. $result = static::executeTableCleanup($content, $task->id);
  160. if ($result['success']) {
  161. $totalDeleted += $result['deleted_records'];
  162. $processedTables++;
  163. } else {
  164. $errors[] = "表 {$content->table_name}: " . $result['message'];
  165. }
  166. } catch (\Exception $e) {
  167. $errors[] = "表 {$content->table_name}: " . $e->getMessage();
  168. Log::error("清理表失败", [
  169. 'task_id' => $task->id,
  170. 'table_name' => $content->table_name,
  171. 'error' => $e->getMessage()
  172. ]);
  173. }
  174. }
  175. $executionTime = round(microtime(true) - $startTime, 3);
  176. // 更新任务完成状态
  177. $finalStatus = empty($errors) ? TASK_STATUS::COMPLETED : TASK_STATUS::FAILED;
  178. CleanupTaskLogic::updateTaskStatus($task->id, $finalStatus, [
  179. 'total_records' => $totalDeleted,
  180. 'deleted_records' => $totalDeleted,
  181. 'execution_time' => $executionTime,
  182. 'error_message' => empty($errors) ? null : implode('; ', $errors),
  183. 'current_step' => $finalStatus === TASK_STATUS::COMPLETED ? '清理完成' : '清理失败',
  184. ]);
  185. return [
  186. 'success' => $finalStatus === TASK_STATUS::COMPLETED,
  187. 'message' => $finalStatus === TASK_STATUS::COMPLETED ? '清理任务执行成功' : '清理任务执行完成,但有错误',
  188. 'data' => [
  189. 'task_id' => $task->id,
  190. 'processed_tables' => $processedTables,
  191. 'total_tables' => $enabledContents->count(),
  192. 'deleted_records' => $totalDeleted,
  193. 'execution_time' => $executionTime,
  194. 'errors' => $errors,
  195. ]
  196. ];
  197. } catch (\Exception $e) {
  198. // 更新任务失败状态
  199. CleanupTaskLogic::updateTaskStatus($task->id, TASK_STATUS::FAILED, [
  200. 'error_message' => $e->getMessage(),
  201. 'execution_time' => round(microtime(true) - $startTime, 3),
  202. 'current_step' => '执行失败',
  203. ]);
  204. throw $e;
  205. }
  206. }
  207. /**
  208. * 预览表清理
  209. *
  210. * @param CleanupPlanContent $content 计划内容
  211. * @return array 预览结果
  212. */
  213. private static function previewTableCleanup(CleanupPlanContent $content): array
  214. {
  215. try {
  216. $tableName = $content->table_name;
  217. $cleanupType = CLEANUP_TYPE::from($content->cleanup_type);
  218. // 检查表是否存在
  219. if (!Schema::hasTable($tableName)) {
  220. return [
  221. 'table_name' => $tableName,
  222. 'cleanup_type' => $cleanupType->getDescription(),
  223. 'affected_records' => 0,
  224. 'current_records' => 0,
  225. 'error' => '表不存在',
  226. ];
  227. }
  228. $currentRecords = DB::table($tableName)->count();
  229. $affectedRecords = static::calculateAffectedRecords($tableName, $cleanupType, $content->conditions);
  230. return [
  231. 'table_name' => $tableName,
  232. 'cleanup_type' => $cleanupType->getDescription(),
  233. 'affected_records' => $affectedRecords,
  234. 'current_records' => $currentRecords,
  235. 'remaining_records' => $currentRecords - $affectedRecords,
  236. 'conditions' => $content->conditions,
  237. 'batch_size' => $content->batch_size,
  238. 'backup_enabled' => $content->backup_enabled,
  239. ];
  240. } catch (\Exception $e) {
  241. return [
  242. 'table_name' => $content->table_name,
  243. 'cleanup_type' => CLEANUP_TYPE::from($content->cleanup_type)->getDescription(),
  244. 'affected_records' => 0,
  245. 'current_records' => 0,
  246. 'error' => $e->getMessage(),
  247. ];
  248. }
  249. }
  250. /**
  251. * 计算受影响的记录数
  252. *
  253. * @param string $tableName 表名
  254. * @param CLEANUP_TYPE $cleanupType 清理类型
  255. * @param array $conditions 清理条件
  256. * @return int 受影响的记录数
  257. */
  258. private static function calculateAffectedRecords(string $tableName, CLEANUP_TYPE $cleanupType, array $conditions): int
  259. {
  260. switch ($cleanupType) {
  261. case CLEANUP_TYPE::TRUNCATE:
  262. case CLEANUP_TYPE::DELETE_ALL:
  263. return DB::table($tableName)->count();
  264. case CLEANUP_TYPE::DELETE_BY_TIME:
  265. return static::countByTimeCondition($tableName, $conditions);
  266. case CLEANUP_TYPE::DELETE_BY_USER:
  267. return static::countByUserCondition($tableName, $conditions);
  268. case CLEANUP_TYPE::DELETE_BY_CONDITION:
  269. return static::countByCustomCondition($tableName, $conditions);
  270. default:
  271. return 0;
  272. }
  273. }
  274. /**
  275. * 按时间条件统计记录数
  276. *
  277. * @param string $tableName 表名
  278. * @param array $conditions 条件
  279. * @return int 记录数
  280. */
  281. private static function countByTimeCondition(string $tableName, array $conditions): int
  282. {
  283. $query = DB::table($tableName);
  284. if (!empty($conditions['time_field']) && !empty($conditions['before'])) {
  285. $timeField = $conditions['time_field'];
  286. $beforeTime = static::parseTimeCondition($conditions['before']);
  287. $query->where($timeField, '<', $beforeTime);
  288. }
  289. return $query->count();
  290. }
  291. /**
  292. * 按用户条件统计记录数
  293. *
  294. * @param string $tableName 表名
  295. * @param array $conditions 条件
  296. * @return int 记录数
  297. */
  298. private static function countByUserCondition(string $tableName, array $conditions): int
  299. {
  300. $query = DB::table($tableName);
  301. if (!empty($conditions['user_field']) && !empty($conditions['user_ids'])) {
  302. $userField = $conditions['user_field'];
  303. $userIds = is_array($conditions['user_ids']) ? $conditions['user_ids'] : [$conditions['user_ids']];
  304. $query->whereIn($userField, $userIds);
  305. }
  306. return $query->count();
  307. }
  308. /**
  309. * 按自定义条件统计记录数
  310. *
  311. * @param string $tableName 表名
  312. * @param array $conditions 条件
  313. * @return int 记录数
  314. */
  315. private static function countByCustomCondition(string $tableName, array $conditions): int
  316. {
  317. $query = DB::table($tableName);
  318. if (!empty($conditions['where'])) {
  319. foreach ($conditions['where'] as $condition) {
  320. if (isset($condition['field'], $condition['operator'], $condition['value'])) {
  321. $query->where($condition['field'], $condition['operator'], $condition['value']);
  322. }
  323. }
  324. }
  325. return $query->count();
  326. }
  327. /**
  328. * 解析时间条件
  329. *
  330. * @param string $timeCondition 时间条件
  331. * @return string 解析后的时间
  332. */
  333. private static function parseTimeCondition(string $timeCondition): string
  334. {
  335. // 支持格式:30_days_ago, 1_month_ago, 2024-01-01, 等
  336. if (preg_match('/(\d+)_days?_ago/', $timeCondition, $matches)) {
  337. return now()->subDays((int)$matches[1])->toDateTimeString();
  338. }
  339. if (preg_match('/(\d+)_months?_ago/', $timeCondition, $matches)) {
  340. return now()->subMonths((int)$matches[1])->toDateTimeString();
  341. }
  342. if (preg_match('/(\d+)_years?_ago/', $timeCondition, $matches)) {
  343. return now()->subYears((int)$matches[1])->toDateTimeString();
  344. }
  345. // 直接返回时间字符串
  346. return $timeCondition;
  347. }
  348. /**
  349. * 估算执行时间
  350. *
  351. * @param int $totalRecords 总记录数
  352. * @return string 估算时间
  353. */
  354. private static function estimateExecutionTime(int $totalRecords): string
  355. {
  356. // 简单估算:每秒处理1000条记录
  357. $seconds = ceil($totalRecords / 1000);
  358. if ($seconds < 60) {
  359. return "{$seconds}秒";
  360. } elseif ($seconds < 3600) {
  361. $minutes = ceil($seconds / 60);
  362. return "{$minutes}分钟";
  363. } else {
  364. $hours = ceil($seconds / 3600);
  365. return "{$hours}小时";
  366. }
  367. }
  368. /**
  369. * 执行表清理
  370. *
  371. * @param CleanupPlanContent $content 计划内容
  372. * @param int $taskId 任务ID
  373. * @return array 执行结果
  374. */
  375. private static function executeTableCleanup(CleanupPlanContent $content, int $taskId): array
  376. {
  377. $startTime = microtime(true);
  378. $tableName = $content->table_name;
  379. $cleanupType = CLEANUP_TYPE::from($content->cleanup_type);
  380. try {
  381. // 检查表是否存在
  382. if (!Schema::hasTable($tableName)) {
  383. throw new \Exception("表 {$tableName} 不存在");
  384. }
  385. // 记录清理前的记录数
  386. $beforeCount = DB::table($tableName)->count();
  387. // 执行清理
  388. $deletedRecords = static::performCleanup($tableName, $cleanupType, $content->conditions, $content->batch_size);
  389. // 记录清理后的记录数
  390. $afterCount = DB::table($tableName)->count();
  391. $actualDeleted = $beforeCount - $afterCount;
  392. $executionTime = round(microtime(true) - $startTime, 3);
  393. // 记录清理日志
  394. static::logCleanupOperation($taskId, $tableName, $cleanupType, [
  395. 'before_count' => $beforeCount,
  396. 'after_count' => $afterCount,
  397. 'deleted_records' => $actualDeleted,
  398. 'execution_time' => $executionTime,
  399. 'conditions' => $content->conditions,
  400. 'batch_size' => $content->batch_size,
  401. ]);
  402. return [
  403. 'success' => true,
  404. 'message' => "表 {$tableName} 清理成功",
  405. 'deleted_records' => $actualDeleted,
  406. 'execution_time' => $executionTime,
  407. ];
  408. } catch (\Exception $e) {
  409. $executionTime = round(microtime(true) - $startTime, 3);
  410. // 记录错误日志
  411. static::logCleanupOperation($taskId, $tableName, $cleanupType, [
  412. 'error' => $e->getMessage(),
  413. 'execution_time' => $executionTime,
  414. 'conditions' => $content->conditions,
  415. ]);
  416. return [
  417. 'success' => false,
  418. 'message' => $e->getMessage(),
  419. 'deleted_records' => 0,
  420. 'execution_time' => $executionTime,
  421. ];
  422. }
  423. }
  424. /**
  425. * 执行实际的清理操作
  426. *
  427. * @param string $tableName 表名
  428. * @param CLEANUP_TYPE $cleanupType 清理类型
  429. * @param array $conditions 清理条件
  430. * @param int $batchSize 批处理大小
  431. * @return int 删除的记录数
  432. */
  433. private static function performCleanup(string $tableName, CLEANUP_TYPE $cleanupType, array $conditions, int $batchSize): int
  434. {
  435. switch ($cleanupType) {
  436. case CLEANUP_TYPE::TRUNCATE:
  437. return static::performTruncate($tableName);
  438. case CLEANUP_TYPE::DELETE_ALL:
  439. return static::performDeleteAll($tableName, $batchSize);
  440. case CLEANUP_TYPE::DELETE_BY_TIME:
  441. return static::performDeleteByTime($tableName, $conditions, $batchSize);
  442. case CLEANUP_TYPE::DELETE_BY_USER:
  443. return static::performDeleteByUser($tableName, $conditions, $batchSize);
  444. case CLEANUP_TYPE::DELETE_BY_CONDITION:
  445. return static::performDeleteByCondition($tableName, $conditions, $batchSize);
  446. default:
  447. throw new \Exception("不支持的清理类型: {$cleanupType->value}");
  448. }
  449. }
  450. /**
  451. * 执行TRUNCATE操作
  452. *
  453. * @param string $tableName 表名
  454. * @return int 删除的记录数
  455. */
  456. private static function performTruncate(string $tableName): int
  457. {
  458. $beforeCount = DB::table($tableName)->count();
  459. DB::statement("TRUNCATE TABLE `{$tableName}`");
  460. return $beforeCount;
  461. }
  462. /**
  463. * 执行DELETE ALL操作
  464. *
  465. * @param string $tableName 表名
  466. * @param int $batchSize 批处理大小
  467. * @return int 删除的记录数
  468. */
  469. private static function performDeleteAll(string $tableName, int $batchSize): int
  470. {
  471. $totalDeleted = 0;
  472. do {
  473. $deleted = DB::table($tableName)->limit($batchSize)->delete();
  474. $totalDeleted += $deleted;
  475. } while ($deleted > 0);
  476. return $totalDeleted;
  477. }
  478. /**
  479. * 执行按时间删除操作
  480. *
  481. * @param string $tableName 表名
  482. * @param array $conditions 条件
  483. * @param int $batchSize 批处理大小
  484. * @return int 删除的记录数
  485. */
  486. private static function performDeleteByTime(string $tableName, array $conditions, int $batchSize): int
  487. {
  488. if (empty($conditions['time_field']) || empty($conditions['before'])) {
  489. throw new \Exception('时间删除条件不完整');
  490. }
  491. $timeField = $conditions['time_field'];
  492. $beforeTime = static::parseTimeCondition($conditions['before']);
  493. $totalDeleted = 0;
  494. do {
  495. $deleted = DB::table($tableName)
  496. ->where($timeField, '<', $beforeTime)
  497. ->limit($batchSize)
  498. ->delete();
  499. $totalDeleted += $deleted;
  500. } while ($deleted > 0);
  501. return $totalDeleted;
  502. }
  503. /**
  504. * 执行按用户删除操作
  505. *
  506. * @param string $tableName 表名
  507. * @param array $conditions 条件
  508. * @param int $batchSize 批处理大小
  509. * @return int 删除的记录数
  510. */
  511. private static function performDeleteByUser(string $tableName, array $conditions, int $batchSize): int
  512. {
  513. if (empty($conditions['user_field']) || empty($conditions['user_ids'])) {
  514. throw new \Exception('用户删除条件不完整');
  515. }
  516. $userField = $conditions['user_field'];
  517. $userIds = is_array($conditions['user_ids']) ? $conditions['user_ids'] : [$conditions['user_ids']];
  518. $totalDeleted = 0;
  519. do {
  520. $deleted = DB::table($tableName)
  521. ->whereIn($userField, $userIds)
  522. ->limit($batchSize)
  523. ->delete();
  524. $totalDeleted += $deleted;
  525. } while ($deleted > 0);
  526. return $totalDeleted;
  527. }
  528. /**
  529. * 执行按条件删除操作
  530. *
  531. * @param string $tableName 表名
  532. * @param array $conditions 条件
  533. * @param int $batchSize 批处理大小
  534. * @return int 删除的记录数
  535. */
  536. private static function performDeleteByCondition(string $tableName, array $conditions, int $batchSize): int
  537. {
  538. if (empty($conditions['where'])) {
  539. throw new \Exception('自定义删除条件不完整');
  540. }
  541. $totalDeleted = 0;
  542. do {
  543. $query = DB::table($tableName);
  544. foreach ($conditions['where'] as $condition) {
  545. if (isset($condition['field'], $condition['operator'], $condition['value'])) {
  546. $query->where($condition['field'], $condition['operator'], $condition['value']);
  547. }
  548. }
  549. $deleted = $query->limit($batchSize)->delete();
  550. $totalDeleted += $deleted;
  551. } while ($deleted > 0);
  552. return $totalDeleted;
  553. }
  554. /**
  555. * 记录清理操作日志
  556. *
  557. * @param int $taskId 任务ID
  558. * @param string $tableName 表名
  559. * @param CLEANUP_TYPE $cleanupType 清理类型
  560. * @param array $details 详细信息
  561. */
  562. private static function logCleanupOperation(int $taskId, string $tableName, CLEANUP_TYPE $cleanupType, array $details): void
  563. {
  564. try {
  565. CleanupLog::create([
  566. 'task_id' => $taskId,
  567. 'table_name' => $tableName,
  568. 'cleanup_type' => $cleanupType->value,
  569. 'before_count' => $details['before_count'] ?? 0,
  570. 'after_count' => $details['after_count'] ?? 0,
  571. 'deleted_records' => $details['deleted_records'] ?? 0,
  572. 'execution_time' => $details['execution_time'] ?? 0,
  573. 'conditions' => $details['conditions'] ?? [],
  574. 'error_message' => $details['error'] ?? null,
  575. 'created_at' => now(),
  576. ]);
  577. } catch (\Exception $e) {
  578. Log::error('记录清理日志失败', [
  579. 'task_id' => $taskId,
  580. 'table_name' => $tableName,
  581. 'error' => $e->getMessage()
  582. ]);
  583. }
  584. }
  585. }