backup.php 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. <?php
  2. /**
  3. * 备份生产 related_paragraphs:库内快照表 + 库外 csv 存档 + 清单。
  4. *
  5. * 用法:
  6. * php scripts/related/backup.php # 表名自动带当天日期
  7. * php scripts/related/backup.php --tag=20260909 # 指定后缀
  8. * php scripts/related/backup.php --force # 快照表已存在时覆盖
  9. *
  10. * 这是整个重建工程唯一允许写生产库的脚本,而且只做 CREATE TABLE AS,不改原表。
  11. */
  12. require __DIR__.'/lib/boot.php';
  13. use Illuminate\Support\Facades\DB;
  14. [$options] = rp_args($argv);
  15. $source = 'related_paragraphs';
  16. $tag = $options['tag'] ?? date('Ymd');
  17. $snapshot = rp_assert_table("related_paragraphs_bak_$tag");
  18. $log = new RpLogger('backup', rp_report_dir());
  19. $log->info("源表:$source");
  20. $log->info("快照表:$snapshot");
  21. $sourceCount = DB::table($source)->count();
  22. $log->info("源表行数:$sourceCount");
  23. if ($sourceCount === 0) {
  24. $log->error('源表为空,拒绝备份——这几乎一定是连错库了。');
  25. $log->finish();
  26. exit(1);
  27. }
  28. $exists = DB::selectOne('select to_regclass(?) as reg', [$snapshot])->reg !== null;
  29. if ($exists) {
  30. if (! isset($options['force'])) {
  31. $existing = DB::table($snapshot)->count();
  32. $log->error("快照表已存在($existing 行)。要覆盖请加 --force。");
  33. $log->finish();
  34. exit(1);
  35. }
  36. $log->warn('快照表已存在,--force 生效,先 DROP。');
  37. DB::statement("drop table $snapshot");
  38. }
  39. $log->info('建快照表中…');
  40. DB::statement("create table $snapshot as select * from $source");
  41. $snapshotCount = DB::table($snapshot)->count();
  42. $log->info("快照表行数:$snapshotCount");
  43. if ($snapshotCount !== $sourceCount) {
  44. $log->error("行数不一致:源 $sourceCount vs 快照 $snapshotCount");
  45. $log->finish();
  46. exit(1);
  47. }
  48. // 快照表只用于比对,建最小索引即可(按 book/para 定位、按 book_name+cs_para 配对)。
  49. $log->info('建索引中…');
  50. DB::statement("create index on $snapshot (book, para)");
  51. DB::statement("create index on $snapshot (book_name, cs_para)");
  52. // 库外存档:库没了也能拿回数据。
  53. $archiveDir = rp_data_dir().'/backup';
  54. if (! is_dir($archiveDir)) {
  55. mkdir($archiveDir, 0o755, true);
  56. }
  57. $csvPath = "$archiveDir/$snapshot.csv";
  58. $log->info("导出 csv:$csvPath");
  59. $handle = fopen($csvPath, 'w');
  60. fputcsv($handle, ['book', 'para', 'book_id', 'cs_para', 'book_name']);
  61. $rows = 0;
  62. DB::table($source)->orderBy('book')->orderBy('para')->orderBy('cs_para')->orderBy('id')
  63. ->select('book', 'para', 'book_id', 'cs_para', 'book_name')
  64. ->chunk(20000, function ($chunk) use ($handle, &$rows, $log) {
  65. foreach ($chunk as $row) {
  66. fputcsv($handle, [$row->book, $row->para, $row->book_id, $row->cs_para, $row->book_name]);
  67. $rows++;
  68. }
  69. if ($rows % 200000 === 0) {
  70. $log->info(" 已导出 $rows 行");
  71. }
  72. });
  73. fclose($handle);
  74. $log->info("csv 行数:$rows");
  75. $manifest = [
  76. 'created_at' => date('c'),
  77. 'database' => DB::connection()->getDatabaseName(),
  78. 'source_table' => $source,
  79. 'snapshot_table' => $snapshot,
  80. 'rows' => $sourceCount,
  81. 'csv_path' => $csvPath,
  82. 'csv_sha256' => hash_file('sha256', $csvPath),
  83. 'csv_bytes' => filesize($csvPath),
  84. ];
  85. rp_json_write("$archiveDir/$snapshot.manifest.json", $manifest);
  86. $log->info("清单:$archiveDir/$snapshot.manifest.json");
  87. $log->info('sha256: '.$manifest['csv_sha256']);
  88. $log->finish();