BackfillPointsScriptCommand.php 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. <?php
  2. namespace App\Console\Commands;
  3. use App\Services\PointsService;
  4. use Illuminate\Console\Command;
  5. use Illuminate\Support\Facades\DB;
  6. use Illuminate\Support\Facades\Schema;
  7. class BackfillPointsScriptCommand extends Command
  8. {
  9. /**
  10. * The name and signature of the console command.
  11. *
  12. * @var string
  13. */
  14. protected $signature = 'points:backfill-script
  15. {--type= : 只回填指定类型(video/image/chat),默认三种类型全部回填}
  16. {--dry-run : 只统计将回填的记录数,不实际更新}
  17. {--force : 重算已有锚点的记录(默认只处理缺失动漫/剧本的记录)}
  18. {--limit= : 最多处理记录数(默认全部)}';
  19. /**
  20. * The console command description.
  21. *
  22. * @var string
  23. */
  24. protected $description = '回填积分明细的动漫ID/动漫名与剧本ID/剧本名(历史数据无动漫/剧本维度时为空)';
  25. /**
  26. * 每批处理条数
  27. */
  28. const CHUNK_SIZE = 500;
  29. /**
  30. * Execute the console command.
  31. *
  32. * @param PointsService $pointsService
  33. * @return int
  34. */
  35. public function handle(PointsService $pointsService)
  36. {
  37. if (!Schema::hasColumn('mp_user_points_details', 'script_id')) {
  38. $this->error('mp_user_points_details 缺少 script_id/script_name 字段,请先执行 php artisan migrate');
  39. return 1;
  40. }
  41. $dryRun = (bool)$this->option('dry-run');
  42. $force = (bool)$this->option('force');
  43. $limit = (int)$this->option('limit');
  44. if ($limit < 0) {
  45. $limit = 0;
  46. }
  47. $type = trim((string)$this->option('type'));
  48. $types = in_array($type, ['video', 'image', 'chat'], true) ? [$type] : ['video', 'image', 'chat'];
  49. $query = DB::table('mp_user_points_details')
  50. ->select('id', 'type', 'task_id', 'charge_info')
  51. ->whereIn('type', $types)
  52. ->orderBy('id');
  53. if (!$force) {
  54. $query->where(function ($q) {
  55. $q->whereNull('anime_id')
  56. ->orWhereNull('anime_name')
  57. ->orWhereNull('script_id')
  58. ->orWhere('script_id', 0);
  59. });
  60. }
  61. $total = (clone $query)->count();
  62. $this->info('待回填明细总数: ' . $total . ($dryRun ? '(dry-run 模式,不更新)' : ''));
  63. if ($total <= 0) {
  64. return 0;
  65. }
  66. if ($limit > 0) {
  67. $this->info('本次最多处理: ' . $limit . ' 条');
  68. }
  69. $scanned = 0;
  70. $filled = 0;
  71. $skipped = 0;
  72. $query->chunkById(self::CHUNK_SIZE, function ($rows) use ($pointsService, $dryRun, $limit, &$scanned, &$filled, &$skipped) {
  73. /** @var array 剧本 => 待更新明细ID */
  74. $pending = [];
  75. foreach ($rows as $row) {
  76. if ($limit > 0 && $scanned >= $limit) {
  77. return false;
  78. }
  79. $scanned++;
  80. $chargeInfo = $row->charge_info;
  81. if (is_string($chargeInfo)) {
  82. $chargeInfo = json_decode($chargeInfo, true);
  83. }
  84. $chargeInfo = is_array($chargeInfo) ? $chargeInfo : [];
  85. $context = $pointsService->resolveScriptContext(
  86. $chargeInfo,
  87. $row->task_id === null ? null : (int)$row->task_id,
  88. (string)$row->type
  89. );
  90. if (empty($context['anime_id']) && empty($context['script_id'])) {
  91. $skipped++;
  92. continue;
  93. }
  94. $key = (int)$context['anime_id'] . '|' . (string)$context['anime_name'] . '|' . (int)$context['script_id'] . '|' . (string)$context['script_name'];
  95. $pending[$key][] = (int)$row->id;
  96. }
  97. foreach ($pending as $key => $ids) {
  98. list($animeId, $animeName, $scriptId, $scriptName) = explode('|', $key, 4);
  99. $filled += count($ids);
  100. if ($dryRun) {
  101. continue;
  102. }
  103. DB::table('mp_user_points_details')->whereIn('id', $ids)->update([
  104. 'anime_id' => (int)$animeId ?: null,
  105. 'anime_name' => $animeName !== '' ? $animeName : null,
  106. 'script_id' => (int)$scriptId ?: null,
  107. 'script_name' => $scriptName !== '' ? $scriptName : null,
  108. ]);
  109. }
  110. return true;
  111. });
  112. $this->info('扫描明细: ' . $scanned . ' 条,可回填: ' . $filled . ' 条,与剧本无关: ' . $skipped . ' 条');
  113. $this->info($dryRun ? 'dry-run 结束,未更新任何数据' : '回填完成');
  114. return 0;
  115. }
  116. }