FixDoubleEncodedOptions.php 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. <?php
  2. namespace App\Console\Commands;
  3. use Illuminate\Console\Command;
  4. use Illuminate\Support\Facades\DB;
  5. class FixDoubleEncodedOptions extends Command
  6. {
  7. protected $signature = 'fix:options
  8. {--ids= : 指定题目ID,多个用逗号隔开,如: --ids=21757,21892}
  9. {--limit=10 : 限制处理数量,默认10条}
  10. {--execute : 真正执行修复,不加此参数则为 dry-run 模式}';
  11. protected $description = '修复 questions 表中双重编码的 options 字段';
  12. public function handle()
  13. {
  14. $ids = $this->option('ids');
  15. $limit = (int) $this->option('limit');
  16. $execute = $this->option('execute');
  17. $this->info('');
  18. $this->info($execute ? '=== 执行模式 ===' : '=== DRY-RUN 模式(不会修改数据)===');
  19. $this->info('');
  20. // 构建查询
  21. $query = DB::connection('remote_mysql')
  22. ->table('questions')
  23. ->where('options', 'LIKE', '"{%') // 双重编码特征:以 "{ 开头
  24. ->whereNotNull('options');
  25. // 如果指定了 IDs
  26. if ($ids) {
  27. $idArray = array_map('trim', explode(',', $ids));
  28. $query->whereIn('id', $idArray);
  29. $this->info("指定 ID: " . implode(', ', $idArray));
  30. } else {
  31. $query->limit($limit);
  32. $this->info("限制数量: {$limit} 条");
  33. }
  34. $questions = $query->get(['id', 'options']);
  35. if ($questions->isEmpty()) {
  36. $this->warn('没有找到需要修复的数据');
  37. return 0;
  38. }
  39. $this->info("找到 {$questions->count()} 条待处理记录");
  40. $this->info('');
  41. $this->table(['ID', '状态', '原始值 (前50字符)', '修复后 (前50字符)'], []);
  42. $successCount = 0;
  43. $failCount = 0;
  44. $results = [];
  45. foreach ($questions as $q) {
  46. $original = $q->options;
  47. $result = $this->tryFix($original);
  48. if ($result['success']) {
  49. $status = '<fg=green>可修复</>';
  50. $successCount++;
  51. if ($execute) {
  52. DB::connection('remote_mysql')
  53. ->table('questions')
  54. ->where('id', $q->id)
  55. ->update(['options' => $result['fixed']]);
  56. $status = '<fg=green>已修复</>';
  57. }
  58. } else {
  59. $status = '<fg=red>失败: ' . $result['error'] . '</>';
  60. $failCount++;
  61. }
  62. $results[] = [
  63. $q->id,
  64. $status,
  65. mb_substr($original, 0, 50) . '...',
  66. $result['success'] ? mb_substr($result['fixed'], 0, 50) . '...' : '-',
  67. ];
  68. // 详细日志
  69. $this->line("ID: {$q->id}");
  70. $this->line(" 原始: " . mb_substr($original, 0, 80) . '...');
  71. if ($result['success']) {
  72. $this->line(" 修复: " . mb_substr($result['fixed'], 0, 80) . '...');
  73. } else {
  74. $this->error(" 错误: " . $result['error']);
  75. }
  76. $this->line('');
  77. }
  78. // 汇总
  79. $this->info('=== 汇总 ===');
  80. $this->info("可修复/已修复: {$successCount} 条");
  81. if ($failCount > 0) {
  82. $this->warn("失败: {$failCount} 条");
  83. }
  84. if (!$execute && $successCount > 0) {
  85. $this->info('');
  86. $this->warn('这是 DRY-RUN 模式,数据未被修改。');
  87. $this->info('确认无误后,添加 --execute 参数真正执行:');
  88. if ($ids) {
  89. $this->info(" php artisan fix:options --ids={$ids} --execute");
  90. } else {
  91. $this->info(" php artisan fix:options --limit={$limit} --execute");
  92. }
  93. }
  94. return 0;
  95. }
  96. /**
  97. * 尝试修复双重编码的 JSON
  98. */
  99. private function tryFix(string $original): array
  100. {
  101. // 第一次解码
  102. $decoded = json_decode($original, true);
  103. if (json_last_error() !== JSON_ERROR_NONE) {
  104. return [
  105. 'success' => false,
  106. 'error' => 'JSON解码失败: ' . json_last_error_msg(),
  107. 'fixed' => null,
  108. ];
  109. }
  110. // 如果解码后是字符串,说明是双重编码
  111. if (is_string($decoded)) {
  112. // 先尝试直接解码
  113. $secondDecode = json_decode($decoded, true);
  114. if (json_last_error() !== JSON_ERROR_NONE) {
  115. // 解码失败,可能是 LaTeX 中的反斜杠问题
  116. // 问题:\times 中的 \t 是合法 JSON 转义(tab),\frac 中的 \f 也是(form feed)
  117. // 解决:第一次解码后,所有反斜杠都是 LaTeX 的,全部转义为双反斜杠
  118. $escapedDecoded = str_replace('\\', '\\\\', $decoded);
  119. $secondDecode = json_decode($escapedDecoded, true);
  120. if (json_last_error() !== JSON_ERROR_NONE) {
  121. return [
  122. 'success' => false,
  123. 'error' => '二次JSON解码失败: ' . json_last_error_msg(),
  124. 'fixed' => null,
  125. ];
  126. }
  127. }
  128. $decoded = $secondDecode;
  129. }
  130. // 验证结果是数组
  131. if (!is_array($decoded)) {
  132. return [
  133. 'success' => false,
  134. 'error' => '解码结果不是数组',
  135. 'fixed' => null,
  136. ];
  137. }
  138. // 重新编码为标准 JSON
  139. $fixed = json_encode($decoded, JSON_UNESCAPED_UNICODE);
  140. return [
  141. 'success' => true,
  142. 'error' => null,
  143. 'fixed' => $fixed,
  144. ];
  145. }
  146. }