TextbookExcelImportPage.php 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. <?php
  2. namespace App\Filament\Pages\TextbookImport;
  3. use App\Services\Import\TextbookExcelImporter;
  4. use App\Services\TextbookApiService;
  5. use PhpOffice\PhpSpreadsheet\IOFactory;
  6. use Filament\Pages\Page;
  7. use UnitEnum;
  8. use BackedEnum;
  9. use Filament\Actions\Action;
  10. use Filament\Forms;
  11. use Filament\Forms\Components\FileUpload;
  12. use Filament\Notifications\Notification;
  13. use Illuminate\Support\Facades\Storage;
  14. use Illuminate\Support\Facades\Log;
  15. use Livewire\WithFileUploads;
  16. use Livewire\Component;
  17. use Livewire\Attributes\Validate;
  18. class TextbookExcelImportPage extends Page
  19. {
  20. use WithFileUploads;
  21. protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-document-arrow-up';
  22. protected static ?string $navigationLabel = 'Excel导入';
  23. protected static UnitEnum|string|null $navigationGroup = '教材管理';
  24. protected static ?int $navigationSort = 4;
  25. public string $view = 'filament.pages.textbook-import';
  26. #[Validate('required|string')]
  27. public $selectedType = 'textbook_series';
  28. #[Validate('required|file|mimes:xlsx,xls|max:10240')]
  29. public $file;
  30. public $importResult = null;
  31. protected $importer;
  32. protected $apiService;
  33. public function boot()
  34. {
  35. $this->importer = app(TextbookExcelImporter::class);
  36. $this->apiService = app(TextbookApiService::class);
  37. }
  38. public function mount()
  39. {
  40. $this->importer = app(TextbookExcelImporter::class);
  41. $this->apiService = app(TextbookApiService::class);
  42. // 检查URL参数,设置默认导入类型
  43. $type = request()->get('type');
  44. if (in_array($type, ['textbook_series', 'textbook', 'textbook_catalog'])) {
  45. $this->selectedType = $type;
  46. }
  47. }
  48. public function getHeaderActions(): array
  49. {
  50. return [
  51. Action::make('downloadTemplate')
  52. ->label('下载模板')
  53. ->icon('heroicon-o-arrow-down-tray')
  54. ->color('primary')
  55. ->action('downloadTemplate'),
  56. Action::make('import')
  57. ->label('导入数据')
  58. ->icon('heroicon-o-cloud-arrow-up')
  59. ->color('success')
  60. ->action('importData')
  61. ->requiresConfirmation()
  62. ->modalHeading('确认导入')
  63. ->modalDescription('确定要导入Excel文件中的数据吗?此操作将同步到题库服务。')
  64. ->modalSubmitActionLabel('确认导入'),
  65. ];
  66. }
  67. public function downloadTemplate()
  68. {
  69. try {
  70. $importer = app(TextbookExcelImporter::class);
  71. if ($this->selectedType === 'textbook_series') {
  72. $filePath = $importer->generateTextbookSeriesTemplate();
  73. $fileName = '教材系列导入模板.xlsx';
  74. } elseif ($this->selectedType === 'textbook') {
  75. $filePath = $importer->generateTextbookTemplate();
  76. $fileName = '教材导入模板.xlsx';
  77. } else {
  78. $filePath = $importer->generateTextbookCatalogTemplate();
  79. $fileName = '教材目录导入模板.xlsx';
  80. }
  81. return response()->download($filePath, $fileName)->deleteFileAfterSend();
  82. } catch (\Exception $e) {
  83. Notification::make()
  84. ->title('模板生成失败')
  85. ->body($e->getMessage())
  86. ->danger()
  87. ->send();
  88. }
  89. }
  90. public function importData()
  91. {
  92. $this->validate();
  93. $temporaryPath = null;
  94. $finalPath = null;
  95. try {
  96. $importer = app(TextbookExcelImporter::class);
  97. // 获取文件的临时路径(Livewire 上传的文件会有临时路径)
  98. $temporaryPath = $this->file->getRealPath();
  99. if (!$temporaryPath || !file_exists($temporaryPath)) {
  100. throw new \Exception('文件上传失败,请重新上传');
  101. }
  102. // 检查文件是否可读
  103. if (!is_readable($temporaryPath)) {
  104. throw new \Exception('文件不可读,请检查文件权限');
  105. }
  106. // 调试信息
  107. Log::info('文件上传信息', [
  108. 'original_name' => $this->file->getClientOriginalName(),
  109. 'temporary_path' => $temporaryPath,
  110. 'exists' => file_exists($temporaryPath),
  111. 'is_file' => is_file($temporaryPath),
  112. 'readable' => is_readable($temporaryPath),
  113. 'file_size' => filesize($temporaryPath),
  114. 'mime_type' => $this->file->getMimeType(),
  115. ]);
  116. // 执行导入 - 直接使用临时路径
  117. if ($this->selectedType === 'textbook_series') {
  118. $result = $importer->importTextbookSeries($temporaryPath);
  119. } elseif ($this->selectedType === 'textbook') {
  120. $result = $importer->importTextbook($temporaryPath);
  121. } else {
  122. // 教材目录导入 - 需要从Excel中获取textbook_id
  123. $spreadsheet = IOFactory::load($temporaryPath);
  124. $sheet = $spreadsheet->getActiveSheet();
  125. $data = $sheet->toArray();
  126. $header = $data[0] ?? [];
  127. $textbookIdIndex = 0;
  128. $seriesIdIndex = null;
  129. foreach ($header as $index => $label) {
  130. $label = trim((string) $label);
  131. if ($label !== '' && str_contains($label, '教材ID')) {
  132. $textbookIdIndex = $index;
  133. }
  134. if ($label !== '' && str_contains($label, '系列ID')) {
  135. $seriesIdIndex = $index;
  136. }
  137. }
  138. $textbookId = 0;
  139. $seriesId = null;
  140. foreach (array_slice($data, 1) as $row) {
  141. $candidate = (int) ($row[$textbookIdIndex] ?? 0);
  142. if ($candidate > 0) {
  143. $textbookId = $candidate;
  144. break;
  145. }
  146. }
  147. if ($seriesIdIndex !== null) {
  148. foreach (array_slice($data, 1) as $row) {
  149. $candidate = (int) ($row[$seriesIdIndex] ?? 0);
  150. if ($candidate > 0) {
  151. $seriesId = $candidate;
  152. break;
  153. }
  154. }
  155. }
  156. if ($textbookId <= 0) {
  157. throw new \Exception('Excel文件中未找到有效的教材ID,请确保第一列包含教材ID');
  158. }
  159. $result = $importer->importTextbookCatalog($temporaryPath, $textbookId, $seriesId);
  160. }
  161. $this->importResult = $result;
  162. if ($result['success']) {
  163. $message = sprintf(
  164. '导入完成!成功: %d 条,失败: %d 条',
  165. $result['success_count'],
  166. $result['error_count']
  167. );
  168. Notification::make()
  169. ->title('导入成功')
  170. ->body($message)
  171. ->success()
  172. ->send();
  173. } else {
  174. Notification::make()
  175. ->title('导入失败')
  176. ->body($result['message'])
  177. ->danger()
  178. ->send();
  179. }
  180. } catch (\Exception $e) {
  181. Notification::make()
  182. ->title('导入失败')
  183. ->body($e->getMessage())
  184. ->danger()
  185. ->send();
  186. Log::error('Excel导入失败', [
  187. 'error' => $e->getMessage(),
  188. 'temporary_path' => $temporaryPath,
  189. 'final_path' => $finalPath,
  190. 'trace' => $e->getTraceAsString(),
  191. ]);
  192. }
  193. // 不需要手动删除文件,临时文件会在请求结束后自动清理
  194. }
  195. }