BackfillTermEditorUid.php 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. <?php
  2. namespace App\Console\Commands;
  3. use App\Models\UserInfo;
  4. use Illuminate\Console\Attributes\Description;
  5. use Illuminate\Console\Attributes\Signature;
  6. use Illuminate\Console\Command;
  7. use Illuminate\Support\Facades\DB;
  8. /**
  9. * editor_id 是人类用户的自增 sn,editor_uid 是 uuid。加 editor_uid 这一列之前
  10. * 写入的术语只有前者,这条命令按 user_infos.id → user_infos.userid 补上。
  11. *
  12. * 负数不碰:那是 AI 模型的哨兵值(-1),模型的身份只有 editor_uid,本来就
  13. * 没有 sn。editor_id = 0 是 admin 本人(user_infos 里就有 id=0 这一行),
  14. * 照常回填。
  15. */
  16. #[Signature('terms:backfill-editor-uid {--dry-run : 只统计与回显,不写库}')]
  17. #[Description('把 dhamma_terms 里为空的 editor_uid 按 editor_id 回填成用户 uuid')]
  18. class BackfillTermEditorUid extends Command
  19. {
  20. /**
  21. * Execute the console command.
  22. */
  23. public function handle(): int
  24. {
  25. $dryRun = (bool) $this->option('dry-run');
  26. $pending = DB::table('dhamma_terms')->whereNull('editor_uid');
  27. $total = (clone $pending)->count();
  28. if ($total === 0) {
  29. $this->info('没有 editor_uid 为空的术语,无需回填。');
  30. return self::SUCCESS;
  31. }
  32. $negative = (clone $pending)->where('editor_id', '<', 0)->count();
  33. $this->line("editor_uid 为空的术语:{$total} 条");
  34. if ($negative > 0) {
  35. $this->line(" · editor_id < 0(AI 模型哨兵):{$negative} 条 —— 跳过,它们没有 sn");
  36. }
  37. $target = (clone $pending)->where('editor_id', '>=', 0);
  38. $ids = (clone $target)->distinct()->pluck('editor_id');
  39. if ($ids->isEmpty()) {
  40. $this->warn('没有可回填的行。');
  41. return self::SUCCESS;
  42. }
  43. /** @var array<int, string> $uidById */
  44. $uidById = UserInfo::whereIn('id', $ids)->pluck('userid', 'id')->all();
  45. $missing = $ids->reject(fn ($id) => isset($uidById[$id]))->values();
  46. if ($missing->isNotEmpty()) {
  47. $this->warn("有 {$missing->count()} 个 editor_id 在 user_infos 里查不到,这些行会原样保留:");
  48. foreach ($missing as $id) {
  49. $count = (clone $target)->where('editor_id', $id)->count();
  50. $this->warn(" editor_id={$id} {$count} 条");
  51. }
  52. }
  53. $this->line(str_repeat('-', 60));
  54. $this->line(sprintf('%-12s %-38s %s', 'editor_id', 'userid', '条数'));
  55. $plan = [];
  56. foreach ($ids as $id) {
  57. if (! isset($uidById[$id])) {
  58. continue;
  59. }
  60. $count = (clone $target)->where('editor_id', $id)->count();
  61. $plan[$id] = ['uid' => $uidById[$id], 'count' => $count];
  62. $this->line(sprintf('%-12s %-38s %s', $id, $uidById[$id], $count));
  63. }
  64. $willWrite = array_sum(array_column($plan, 'count'));
  65. $this->line(str_repeat('-', 60));
  66. $this->line("将回填 {$willWrite} 条,涉及 ".count($plan).' 个用户。');
  67. if ($dryRun) {
  68. $this->info('--dry-run:未写库。');
  69. return self::SUCCESS;
  70. }
  71. if (! $this->confirm("确认写入这 {$willWrite} 条?", false)) {
  72. $this->warn('已取消,未改动任何数据。');
  73. return self::FAILURE;
  74. }
  75. // 一个 editor_id 一条 UPDATE(本例 77 条),比逐行快得多。
  76. // 走 DB::table 而不是 Eloquent:后者会顺手刷 updated_at,
  77. // 那是术语的最后修改时间,不该被一次数据补写污染。
  78. $written = 0;
  79. $bar = $this->output->createProgressBar(count($plan));
  80. $bar->start();
  81. foreach ($plan as $id => $row) {
  82. $written += DB::table('dhamma_terms')
  83. ->whereNull('editor_uid')
  84. ->where('editor_id', $id)
  85. ->update(['editor_uid' => $row['uid']]);
  86. $bar->advance();
  87. }
  88. $bar->finish();
  89. $this->newLine(2);
  90. $left = DB::table('dhamma_terms')->whereNull('editor_uid')->count();
  91. $this->info("已回填 {$written} 条。");
  92. $this->line("仍为空的还有 {$left} 条".($left > 0 ? '(AI 哨兵,或查不到的用户)' : '').'。');
  93. return self::SUCCESS;
  94. }
  95. }