$fields 该表的字段定义(决定表头) * @param string $storeDataset 「终版」目录实际保存记录用的数据集键 * @return array{created:int,updated:int,rows:int} */ public function import(array $fields, string $storeDataset, UploadedFile $file, int $companyId, ?int $userId): array { $lookup = ['记录标识' => '_action_key', 'recordkey' => '_action_key', '_action_key' => '_action_key']; foreach ($fields as $field => $definition) { $lookup[$this->header($field)] = $field; $lookup[$this->header($definition['label'])] = $field; } try { $readerType = match (mb_strtolower($file->getClientOriginalExtension())) { 'xlsx' => 'Xlsx', 'xls' => 'Xls', 'csv' => 'Csv', default => throw new \RuntimeException('Unsupported workbook format.'), }; $spreadsheet = IOFactory::createReader($readerType)->load($file->getRealPath()); [$rows, $headerRow, $columns] = $this->bestSheet($spreadsheet, $lookup); } catch (Throwable) { throw ValidationException::withMessages([ 'import' => '无法读取导入文件,请确认文件未损坏且格式为 xlsx、xls 或 csv。', ]); } finally { if (isset($spreadsheet)) { $spreadsheet->disconnectWorksheets(); } } if ($rows === null || $columns === []) { throw ValidationException::withMessages([ 'import' => '没有识别到表头,请先下载本页面的导入模板,按模板列填写后重试。', ]); } $store = $this->store; $records = []; foreach (array_slice($rows, $headerRow + 1, null, true) as $rowIndex => $row) { $line = $rowIndex + 1; $key = ''; $rawValues = []; foreach ($columns as $columnIndex => $field) { if ($field === '_action_key') { $key = trim((string) ($row[$columnIndex] ?? '')); } else { $rawValues[$field] = $row[$columnIndex] ?? null; } } if (collect($rawValues)->every(static fn (mixed $value): bool => trim((string) $value) === '')) { continue; } try { $values = $this->normalizer->normalize($fields, $rawValues); } catch (ValidationException $exception) { throw ValidationException::withMessages([ 'import' => "第 {$line} 行:" . collect($exception->errors())->flatten()->first(), ]); } if ($key === '') { $key = $store->key($storeDataset, $values); } if (isset($records[$key])) { throw ValidationException::withMessages(['import' => "第 {$line} 行与前一行内容重复。"]); } $records[$key] = $values; if (count($records) > self::MAX_ROWS) { throw ValidationException::withMessages(['import' => '单次最多导入 10,000 条记录,请拆分文件后重试。']); } } if ($records === []) { throw ValidationException::withMessages(['import' => '表格中没有可导入的数据行。']); } $counts = ['created' => 0, 'updated' => 0]; $store->transaction(function () use ($store, $records, $storeDataset, $companyId, $userId, &$counts): void { foreach ($records as $key => $values) { $result = $store->save($storeDataset, $key, $values, $companyId, $userId, 'import'); $counts['created'] += $result['created']; $counts['updated'] += $result['updated']; } }); return $counts + ['rows' => count($records)]; } private function bestSheet(mixed $spreadsheet, array $lookup): array { $best = [null, null, []]; $score = 0; foreach ($spreadsheet->getWorksheetIterator() as $worksheet) { $rows = $worksheet->toArray(null, false, false, false); foreach (array_slice($rows, 0, 20, true) as $rowIndex => $row) { $columns = []; foreach ($row as $columnIndex => $heading) { $key = $this->header($heading); if ($key !== '' && isset($lookup[$key])) { $columns[$columnIndex] = $lookup[$key]; } } if (count($columns) > $score) { $best = [$rows, $rowIndex, $columns]; $score = count($columns); } } } return $best; } private function header(mixed $value): string { return mb_strtolower(preg_replace('/[\s\x{FEFF}_\-()()]+/u', '', trim((string) $value))); } }