FixDoubleEncodedOptions.php 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. <?php
  2. namespace App\Console\Commands;
  3. use Illuminate\Console\Command;
  4. use Illuminate\Support\Facades\DB;
  5. use Illuminate\Support\Facades\Log;
  6. class FixDoubleEncodedOptions extends Command
  7. {
  8. protected $signature = 'fix:options
  9. {--ids= : 指定题目ID,多个用逗号隔开,如: --ids=21757,21892}
  10. {--limit=10 : 限制处理数量,默认10条}
  11. {--execute : 真正执行修复,不加此参数则为 dry-run 模式}';
  12. protected $description = '修复 questions 表中双重编码的 options 字段';
  13. public function handle()
  14. {
  15. $ids = $this->option('ids');
  16. $limit = (int) $this->option('limit');
  17. $execute = $this->option('execute');
  18. $this->info('');
  19. $this->info($execute ? '=== 执行模式 ===' : '=== DRY-RUN 模式(不会修改数据)===');
  20. $this->info('');
  21. // 构建查询
  22. $query = DB::connection('remote_mysql')
  23. ->table('questions')
  24. ->where('options', 'LIKE', '"{%') // 双重编码特征:以 "{ 开头
  25. ->whereNotNull('options');
  26. // 如果指定了 IDs
  27. if ($ids) {
  28. $idArray = array_map('trim', explode(',', $ids));
  29. $query->whereIn('id', $idArray);
  30. $this->info("指定 ID: " . implode(', ', $idArray));
  31. } else {
  32. $query->limit($limit);
  33. $this->info("限制数量: {$limit} 条");
  34. }
  35. $questions = $query->get(['id', 'options']);
  36. if ($questions->isEmpty()) {
  37. $this->warn('没有找到需要修复的数据');
  38. return 0;
  39. }
  40. $this->info("找到 {$questions->count()} 条待处理记录");
  41. $this->info('');
  42. $this->table(['ID', '状态', '原始值 (前50字符)', '修复后 (前50字符)'], []);
  43. $successCount = 0;
  44. $failCount = 0;
  45. $results = [];
  46. foreach ($questions as $q) {
  47. $original = $q->options;
  48. $result = $this->tryFix($original);
  49. if ($result['success']) {
  50. $status = '<fg=green>可修复</>';
  51. $successCount++;
  52. if ($execute) {
  53. DB::connection('remote_mysql')
  54. ->table('questions')
  55. ->where('id', $q->id)
  56. ->update(['options' => $result['fixed']]);
  57. $status = '<fg=green>已修复</>';
  58. }
  59. } else {
  60. $status = '<fg=red>失败: ' . $result['error'] . '</>';
  61. $failCount++;
  62. }
  63. $results[] = [
  64. $q->id,
  65. $status,
  66. mb_substr($original, 0, 50) . '...',
  67. $result['success'] ? mb_substr($result['fixed'], 0, 50) . '...' : '-',
  68. ];
  69. // 详细日志
  70. $this->line("ID: {$q->id}");
  71. $this->line(" 原始: " . mb_substr($original, 0, 80) . '...');
  72. if ($result['success']) {
  73. $this->line(" 修复: " . mb_substr($result['fixed'], 0, 80) . '...');
  74. } else {
  75. $this->error(" 错误: " . $result['error']);
  76. }
  77. $this->line('');
  78. }
  79. // 汇总
  80. $this->info('=== 汇总 ===');
  81. $this->info("可修复/已修复: {$successCount} 条");
  82. if ($failCount > 0) {
  83. $this->warn("失败: {$failCount} 条");
  84. }
  85. if (!$execute && $successCount > 0) {
  86. $this->info('');
  87. $this->warn('这是 DRY-RUN 模式,数据未被修改。');
  88. $this->info('确认无误后,添加 --execute 参数真正执行:');
  89. if ($ids) {
  90. $this->info(" php artisan fix:options --ids={$ids} --execute");
  91. } else {
  92. $this->info(" php artisan fix:options --limit={$limit} --execute");
  93. }
  94. }
  95. return 0;
  96. }
  97. /**
  98. * 尝试修复双重编码的 JSON
  99. */
  100. private function tryFix(string $original): array
  101. {
  102. // 第一次解码
  103. $decoded = json_decode($original, true);
  104. if (json_last_error() !== JSON_ERROR_NONE) {
  105. return [
  106. 'success' => false,
  107. 'error' => 'JSON解码失败: ' . json_last_error_msg(),
  108. 'fixed' => null,
  109. ];
  110. }
  111. // 如果解码后是字符串,说明是双重编码
  112. if (is_string($decoded)) {
  113. // 先尝试直接解码
  114. $secondDecode = json_decode($decoded, true);
  115. if (json_last_error() !== JSON_ERROR_NONE) {
  116. // 解码失败,可能是 LaTeX 中的反斜杠问题(如 \times, \frac 等)
  117. // 尝试将单个反斜杠转义为双反斜杠后再解码
  118. $escapedDecoded = $this->escapeLatexBackslashes($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. /**
  147. * 转义 LaTeX 公式中的反斜杠
  148. * 将非 JSON 转义序列的反斜杠转义为双反斜杠
  149. */
  150. private function escapeLatexBackslashes(string $str): string
  151. {
  152. // JSON 合法的转义序列: \", \\, \/, \b, \f, \n, \r, \t, \uXXXX
  153. // 其他的反斜杠(如 LaTeX 的 \times, \frac, \sqrt 等)需要转义
  154. // 使用回调函数处理每个反斜杠
  155. return preg_replace_callback('/\\\\(?!["\\\\/bfnrt]|u[0-9a-fA-F]{4})/', function ($match) {
  156. // 将单个反斜杠替换为双反斜杠
  157. return '\\' . $match[0];
  158. }, $str);
  159. }
  160. }