QuestionsJsonImportPage.php 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. <?php
  2. namespace App\Filament\Pages;
  3. use App\Services\QuestionBulkImportService;
  4. use BackedEnum;
  5. use Filament\Forms;
  6. use Filament\Notifications\Notification;
  7. use Filament\Pages\Page;
  8. use Filament\Schemas\Components\Section;
  9. use Filament\Schemas\Schema;
  10. use Filament\Support\Enums\Width;
  11. use Illuminate\Support\Facades\File;
  12. use Illuminate\Support\Facades\Storage;
  13. use UnitEnum;
  14. class QuestionsJsonImportPage extends Page implements Forms\Contracts\HasForms
  15. {
  16. use Forms\Concerns\InteractsWithForms;
  17. /**
  18. * 主入口在「待入库质检」页右侧;本页仅保留直达 URL,不出现在侧栏。
  19. */
  20. protected static bool $shouldRegisterNavigation = false;
  21. protected static ?string $title = '题库 JSON 一键导入';
  22. protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-arrow-up-tray';
  23. protected static ?string $navigationLabel = 'JSON 一键导入';
  24. protected static string|UnitEnum|null $navigationGroup = '题库管理';
  25. protected static ?int $navigationSort = 6;
  26. /**
  27. * 固定路径,便于收藏与文档:/admin/questions-json-import
  28. */
  29. protected static ?string $slug = 'questions-json-import';
  30. protected Width|string|null $maxContentWidth = Width::Full;
  31. protected string $view = 'filament.pages.questions-json-import';
  32. /** @var array<string, mixed> */
  33. public array $data = [
  34. 'dry_run' => false,
  35. 'sql_only' => false,
  36. 'with_id' => false,
  37. ];
  38. public ?string $lastSqlPath = null;
  39. public ?string $lastMessage = null;
  40. public function mount(): void
  41. {
  42. $this->form->fill($this->data);
  43. }
  44. public function form(Schema $schema): Schema
  45. {
  46. return $schema
  47. ->schema([
  48. Section::make('从 JSON 导入 questions')
  49. ->description('支持整段 JSON 数组或 NDJSON(每行一题)。左侧选文件与选项,右侧点击「一键导入」执行。')
  50. ->schema([
  51. Forms\Components\FileUpload::make('json_file')
  52. ->label('JSON 文件')
  53. ->disk('local')
  54. ->directory('imports/questions')
  55. ->acceptedFileTypes(['application/json', 'text/plain', 'text/json'])
  56. ->maxSize(51200)
  57. ->helperText('最大约 50MB;字段含 question_code、kp_code、stem、options、answer、solution 等'),
  58. Forms\Components\Toggle::make('dry_run')
  59. ->label('仅校验(dry-run,不写库)')
  60. ->default(false),
  61. Forms\Components\Toggle::make('sql_only')
  62. ->label('仅生成 SQL(不写入本地 questions)')
  63. ->default(false),
  64. Forms\Components\Toggle::make('with_id')
  65. ->label('SQL 中含 id(仅当目标库必须对齐原主键时勾选;默认不含 id)')
  66. ->default(false),
  67. ])
  68. ->columns(1),
  69. ])
  70. ->statePath('data');
  71. }
  72. public function runImport(): void
  73. {
  74. $this->lastMessage = null;
  75. $this->lastSqlPath = null;
  76. $data = $this->data;
  77. $relative = $data['json_file'] ?? null;
  78. if (! is_string($relative) || $relative === '') {
  79. Notification::make()->title('请选择 JSON 文件')->warning()->send();
  80. return;
  81. }
  82. $fullPath = Storage::disk('local')->path($relative);
  83. if (! File::isFile($fullPath)) {
  84. Notification::make()->title('文件不存在')->body($fullPath)->danger()->send();
  85. return;
  86. }
  87. $dryRun = (bool) ($data['dry_run'] ?? false);
  88. $sqlOnly = (bool) ($data['sql_only'] ?? false);
  89. $withId = (bool) ($data['with_id'] ?? false);
  90. $service = app(QuestionBulkImportService::class);
  91. $pipeline = $service->runImportPipeline($fullPath, $dryRun, $sqlOnly, $withId);
  92. if (! $pipeline['ok']) {
  93. Notification::make()->title('导入失败')->body($pipeline['error'] ?? '')->danger()->send();
  94. return;
  95. }
  96. $this->lastSqlPath = $pipeline['sql_path'] ?? null;
  97. $this->lastMessage = $pipeline['message'] ?? null;
  98. if ($sqlOnly) {
  99. Notification::make()->title('SQL 已生成')->body($this->lastSqlPath ?? '')->success()->send();
  100. return;
  101. }
  102. $body = (string) $this->lastMessage;
  103. $stats = $pipeline['stats'] ?? null;
  104. if (is_array($stats) && $stats['errors'] !== []) {
  105. $body .= "\n".implode("\n", array_slice($stats['errors'], 0, 8));
  106. if (count($stats['errors']) > 8) {
  107. $body .= "\n… 另有 ".(count($stats['errors']) - 8).' 条错误';
  108. }
  109. }
  110. Notification::make()
  111. ->title($dryRun ? '校验完成(dry-run)' : '导入完成')
  112. ->body($body)
  113. ->success()
  114. ->send();
  115. }
  116. }