*/ public function read(string $path): array { $reader = IOFactory::createReader('Xlsx'); $info = $reader->listWorksheetInfo($path); if ($info === [] || $info[0]['totalRows'] > 5001 || $info[0]['totalColumns'] > 20) { throw ValidationException::withMessages(['file' => '请使用第一个工作表存放配置,最多5000条。']); } $reader->setLoadSheetsOnly($info[0]['worksheetName']); $book = $reader->load($path); try { $sheet = $book->getSheet(0); $headers = []; for ($column = 1; $column <= $info[0]['totalColumns']; $column++) { $headers[$this->label($sheet->getCell([$column, 1])->getValue())] = $column; } $supplierColumn = $headers['供应商'] ?? $headers['供应商名称'] ?? null; $ruleColumn = $headers['厂家是否记账'] ?? null; $modeColumn = $headers['记账方式'] ?? null; if ($supplierColumn === null || ($ruleColumn === null && $modeColumn === null)) { throw ValidationException::withMessages(['file' => '缺少列:需要“供应商”和“厂家是否记账”(或“记账方式”)。']); } $rows = []; $seen = []; for ($number = 2; $number <= $sheet->getHighestDataRow(); $number++) { $supplier = $this->text($sheet, $supplierColumn, $number); $ruleText = $ruleColumn !== null ? $this->text($sheet, $ruleColumn, $number) : ''; $modeText = $modeColumn !== null ? $this->text($sheet, $modeColumn, $number) : ''; // 原表中间重复出现的表头行、空行跳过。 if (($supplier === '' && $ruleText === '' && $modeText === '') || $this->label($supplier) === '供应商') { continue; } if ($supplier === '' || mb_strlen($supplier) > 191) { throw ValidationException::withMessages(['file' => "第{$number}行供应商为空或超长。"]); } $key = mb_strtolower($supplier); if (isset($seen[$key])) { throw ValidationException::withMessages(['file' => "第{$number}行供应商“{$supplier}”与第{$seen[$key]}行重复。"]); } $seen[$key] = $number; try { $parsed = Rule::parseRuleText($ruleText !== '' ? $ruleText : $modeText); } catch (InvalidArgumentException $exception) { throw ValidationException::withMessages(['file' => "第{$number}行:" . $exception->getMessage()]); } foreach (['effective_from' => '生效开始日期', 'effective_to' => '生效结束日期'] as $field => $label) { if (isset($headers[$label])) { $date = $this->date($sheet, $headers[$label], $number); if ($date !== null) { $parsed[$field] = $date; } } } if ($parsed['effective_from'] !== null && $parsed['effective_to'] !== null && $parsed['effective_from'] > $parsed['effective_to']) { throw ValidationException::withMessages(['file' => "第{$number}行生效结束日期早于开始日期。"]); } $source = isset($headers['原始说明']) ? $this->text($sheet, $headers['原始说明'], $number) : ''; $rows[] = ['supplier_name' => $supplier] + $parsed + ['source_text' => $ruleText !== '' ? $ruleText : ($source !== '' ? $source : $modeText)]; } if ($rows === []) { throw ValidationException::withMessages(['file' => '配置表没有有效记录。']); } return $rows; } finally { $book->disconnectWorksheets(); } } private function label(mixed $value): string { return str_replace([' ', ' ', "\n"], '', trim((string) $value)); } private function text($sheet, int $column, int $row): string { $cell = $sheet->getCell([$column, $row]); if ($cell->getDataType() === 'f') { throw ValidationException::withMessages(['file' => "第{$row}行含公式,请使用实际配置值。"]); } return trim((string) $cell->getFormattedValue()); } private function date($sheet, int $column, int $row): ?string { $cell = $sheet->getCell([$column, $row]); $value = $cell->getValue(); if ($value === null || trim((string) $value) === '') { return null; } if (is_numeric($value) && ExcelDate::isDateTime($cell)) { return ExcelDate::excelToDateTimeObject($value)->format('Y-m-d'); } $text = str_replace(['/', '.'], '-', trim((string) $cell->getFormattedValue())); if (! preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $text, $m) || ! checkdate((int) $m[2], (int) $m[3], (int) $m[1])) { throw ValidationException::withMessages(['file' => "第{$row}行日期无效,请使用 2026-01-31 格式。"]); } return sprintf('%04d-%02d-%02d', $m[1], $m[2], $m[3]); } }