,sheet:string,headers:list,summary:array} */ public function preview(UploadedFile $file): array { $spreadsheet = IOFactory::load($file->getRealPath()); $worksheet = $this->findWorksheet($spreadsheet); if ($worksheet === null) { throw ValidationException::withMessages([ 'import' => '未找到包含“仓储方、仓库、仓库简称”三列的工作表。Sheet2 仅是辅助仓库清单,不能直接导入。', ]); } $rows = $worksheet->toArray(null, true, false, false); $parsed = []; $seen = []; $created = 0; $same = 0; $conflicts = 0; $invalid = 0; foreach (array_slice($rows, 1) as $offset => $row) { $rowNumber = $offset + 2; $provider = trim((string) ($row[0] ?? '')); $warehouse = trim((string) ($row[1] ?? '')); $shortName = trim((string) ($row[2] ?? '')); if ($provider === '' && $warehouse === '' && $shortName === '') { continue; } $key = $this->key($provider, $warehouse); $item = [ 'row_number' => $rowNumber, 'warehouse_provider' => $provider, 'warehouse' => $warehouse, 'warehouse_short_name' => $shortName, 'status' => 'new', 'message' => '待新增', ]; if ($provider === '' || $warehouse === '' || $shortName === '') { $item['status'] = 'invalid'; $item['message'] = '仓储方、仓库、仓库简称均为必填字段'; $invalid++; } elseif (isset($seen[$key])) { $item['status'] = $seen[$key]['warehouse_short_name'] === $shortName ? 'same' : 'conflict'; $item['message'] = $item['status'] === 'same' ? '与文件内前一行重复,将跳过' : '与文件内前一行的仓库简称冲突'; $item['status'] === 'same' ? $same++ : $conflicts++; } else { $existing = InventoryWarehouseNameMapping::query() ->where('warehouse_provider', $provider) ->where('warehouse', $warehouse) ->first(); if ($existing) { $item['status'] = $existing->warehouse_short_name === $shortName ? 'same' : 'conflict'; $item['message'] = $item['status'] === 'same' ? '数据库已有相同映射,将跳过' : '数据库已有不同简称,禁止静默覆盖'; $item['status'] === 'same' ? $same++ : $conflicts++; } else { $created++; } } $parsed[] = $item; if ($provider !== '' && $warehouse !== '' && $shortName !== '' && ! isset($seen[$key])) { $seen[$key] = $item; } } return [ 'rows' => $parsed, 'sheet' => $worksheet->getTitle(), 'headers' => self::HEADERS, 'summary' => compact('created', 'same', 'conflicts', 'invalid'), ]; } /** @param array{rows:list} $preview */ public function commit(array $preview): array { $rows = $preview['rows'] ?? []; $invalid = collect($rows)->first(fn (array $row): bool => in_array($row['status'] ?? '', ['invalid', 'conflict'], true)); if ($invalid !== null) { throw ValidationException::withMessages([ 'import' => '导入预览存在必填字段错误或简称冲突,请先处理标记为失败的行。', ]); } $created = 0; $skipped = 0; DB::transaction(function () use ($rows, &$created, &$skipped): void { foreach ($rows as $row) { if (($row['status'] ?? '') === 'same') { $skipped++; continue; } $query = InventoryWarehouseNameMapping::query() ->where('warehouse_provider', $row['warehouse_provider']) ->where('warehouse', $row['warehouse']); if ($query->exists()) { throw ValidationException::withMessages([ 'import' => "第 {$row['row_number']} 行在确认时已被其他操作占用,请重新预览。", ]); } InventoryWarehouseNameMapping::create([ 'company_id' => company_id() ?: 1, 'warehouse_provider' => $row['warehouse_provider'], 'warehouse' => $row['warehouse'], 'warehouse_short_name' => $row['warehouse_short_name'], 'created_by' => auth()->id(), 'updated_by' => auth()->id(), ]); $created++; } }); return compact('created', 'skipped'); } public static function suggestion(string $provider, string $warehouse): string { $provider = trim($provider); $warehouse = trim($warehouse); $haystack = $provider . ' ' . $warehouse; if (str_starts_with($provider, '奇门-')) { return '不看'; } foreach ([ '配件单仓' => '配件单仓-不看', 'POP仓' => '京东入仓-不看', '自动发货仓' => '虚拟仓-不看', '京东云仓' => '京东云仓-不看', '京东京造' => '京东京造-不看', '苏州福利品仓' => '苏州福利品仓-不看', '信誉订单仓' => '信誉订单仓-不看', '代发仓' => '代发仓', '销退仓' => '销退仓-不看', '客退' => '客退仓-不看', '次品仓' => '退货报废-不看', '数量校验仓' => '数量校验仓-不看', ] as $needle => $result) { if (str_contains($haystack, $needle)) { return $result; } } foreach ([ '运营C崔桥工厂仓' => '崔桥工厂仓', '运营C横林二仓' => '横林二仓', '运营C睢宁' => '睢宁仓', '运营C新苏北' => '新苏北仓', '江西仓送货' => '南昌樵舍仓', ] as $needle => $result) { if (str_contains($haystack, $needle)) { return $result; } } $short = preg_replace('/(主仓|进货仓)$/u', '', $warehouse) ?: $warehouse; $short = preg_replace('/^(简尚-|京仓-|三方-|奇门-)/u', '', $short) ?: $short; $providerBase = preg_replace('/^(简尚-|京仓-|三方-|奇门-)/u', '', $provider) ?: $provider; if ($short === $providerBase || str_starts_with($short, $providerBase)) { $short = $providerBase; } return $short !== '' ? $short : $warehouse; } private function findWorksheet($spreadsheet): ?object { foreach ($spreadsheet->getWorksheetIterator() as $worksheet) { $row = $worksheet->toArray(null, true, false, false)[0] ?? []; $headers = array_map(fn ($value): string => $this->normalize((string) $value), $row); $expected = array_map(fn ($value): string => $this->normalize($value), self::HEADERS); if (count(array_intersect($expected, $headers)) === count($expected)) { return $worksheet; } } return null; } private function normalize(string $value): string { return mb_strtolower(preg_replace('/[\s\x{FEFF}_\-()()]+/u', '', trim($value))); } private function key(string $provider, string $warehouse): string { return $provider . "\0" . $warehouse; } }