*/ private const HEADERS = [ '记录标识' => 'upstream_key', '商品编码' => 'barcode', 'SPU' => 'spu', '商品名称' => 'product_name', '类目' => 'category', '生产状态' => 'production_status', '最低售价' => 'minimum_price', '计算限价' => 'price_limit', '采购价' => 'purchase_price', '运费' => 'shipping_fee', '京喜限价' => 'jingxi_limit_price', ]; /** @var array */ private const TEXT_LIMITS = [ 'barcode' => 191, 'spu' => 191, 'product_name' => 5000, 'category' => 255, 'production_status' => 120, ]; public function __construct(private readonly ProductPriceMirrorRepository $repository) { } /** @return array */ public function templateFields(): array { $fields = []; foreach (self::HEADERS as $label => $field) { if ($field === 'upstream_key') { continue; } $fields[$field] = [ 'label' => $label, 'type' => isset(self::TEXT_LIMITS[$field]) ? 'text' : 'number', ]; } return $fields; } /** @return array{created:int,updated:int,rows:int} */ public function import(UploadedFile $file, int $companyId, ?int $userId): array { try { $spreadsheet = IOFactory::load($file->getRealPath()); [$rows, $headerRow, $columns] = $this->bestMatchingSheet($spreadsheet); } catch (Throwable) { throw ValidationException::withMessages([ 'import' => '无法读取导入文件,请确认文件未损坏且格式为 xlsx、xls 或 csv。', ]); } finally { if (isset($spreadsheet)) { $spreadsheet->disconnectWorksheets(); } } if ($rows === null || ! in_array('upstream_key', $columns, true)) { throw ValidationException::withMessages([ 'import' => '未找到“记录标识”列,请先导出当前数据,再使用导出的表格进行修改。', ]); } if (count(array_intersect($columns, ProductPriceMirrorRepository::EDITABLE_FIELDS)) === 0) { throw ValidationException::withMessages([ 'import' => '表格中没有可导入的商品价格字段。', ]); } $records = []; foreach (array_slice($rows, $headerRow + 1, null, true) as $rowIndex => $row) { $line = $rowIndex + 1; $upstreamKey = ''; $changes = []; foreach ($columns as $columnIndex => $field) { $value = $row[$columnIndex] ?? null; if ($field === 'upstream_key') { $upstreamKey = strtolower(trim((string) $value)); continue; } $changes[$field] = $this->normalizeValue($field, $value, $line); } if ($upstreamKey === '' && collect($changes)->every( static fn (mixed $value): bool => $value === null || $value === '' )) { continue; } if (preg_match('/^[a-f0-9]{64}$/', $upstreamKey) !== 1) { throw ValidationException::withMessages([ 'import' => "第 {$line} 行“记录标识”无效,请勿修改导出文件中的该列。", ]); } if (array_key_exists($upstreamKey, $records)) { throw ValidationException::withMessages([ 'import' => "第 {$line} 行“记录标识”重复。", ]); } if (array_key_exists('barcode', $changes) && trim((string) $changes['barcode']) === '') { throw ValidationException::withMessages([ 'import' => "第 {$line} 行“商品编码”不能为空。", ]); } if (collect($changes)->every(static fn (mixed $value): bool => $value === null || $value === '')) { continue; } $records[$upstreamKey] = $changes; if (count($records) > 10000) { throw ValidationException::withMessages([ 'import' => '单次最多导入 10,000 条商品价格记录,请拆分文件后重试。', ]); } } if ($records === []) { throw ValidationException::withMessages(['import' => '表格中没有可导入的数据行。']); } try { $result = $this->repository->applyOverrides($records, $companyId, $userId, 'import'); } catch (RuntimeException $exception) { if (str_starts_with($exception->getMessage(), 'product_price_record_not_found:')) { throw ValidationException::withMessages([ 'import' => '导入文件包含当前商品价格镜像中不存在的记录,请重新导出后再导入。', ]); } if ($exception->getMessage() === 'product_price_maintenance_schema_missing') { throw ValidationException::withMessages([ 'import' => '商品价格维护功能尚未完成初始化,请稍后重试。', ]); } throw $exception; } return $result + ['rows' => count($records)]; } private function bestMatchingSheet($spreadsheet): array { $lookup = []; foreach (self::HEADERS as $label => $field) { $lookup[$this->normalizeHeader($label)] = $field; $lookup[$this->normalizeHeader($field)] = $field; } $best = [null, null, []]; $bestScore = 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->normalizeHeader($heading); if ($key !== '' && isset($lookup[$key])) { $columns[$columnIndex] = $lookup[$key]; } } if (count($columns) > $bestScore) { $best = [$rows, $rowIndex, $columns]; $bestScore = count($columns); } } } return $best; } private function normalizeHeader(mixed $value): string { return mb_strtolower(preg_replace('/[\s\x{FEFF}_\-()()]+/u', '', trim((string) $value))); } private function normalizeValue(string $field, mixed $value, int $line): mixed { if ($value === null || trim((string) $value) === '') { return null; } if (isset(self::TEXT_LIMITS[$field])) { $text = trim((string) $value); if (mb_strlen($text) > self::TEXT_LIMITS[$field]) { $label = array_search($field, self::HEADERS, true) ?: $field; throw ValidationException::withMessages([ 'import' => "第 {$line} 行“{$label}”内容过长。", ]); } return $text; } $number = str_replace([',', ',', '¥', '¥'], '', trim((string) $value)); if (preg_match('/^(?:\d+(?:\.\d{1,4})?|\.\d{1,4})$/', $number) !== 1 || bccomp($number, '9999999999999999.9999', 4) === 1) { $label = array_search($field, self::HEADERS, true) ?: $field; throw ValidationException::withMessages([ 'import' => "第 {$line} 行“{$label}”必须是大于或等于 0、最多 4 位小数的有效金额。", ]); } return ltrim($number, '+'); } }