201 lines
8.6 KiB
PHP
201 lines
8.6 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Services;
|
||
|
||
use Illuminate\Http\UploadedFile;
|
||
use Illuminate\Validation\ValidationException;
|
||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||
use PhpOffice\PhpSpreadsheet\Style\Alignment;
|
||
use PhpOffice\PhpSpreadsheet\Style\Border;
|
||
use PhpOffice\PhpSpreadsheet\Style\Fill;
|
||
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
|
||
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||
use Throwable;
|
||
|
||
/** 读取和生成「是否包邮厂家」导入文件。 */
|
||
class SupplierFreeShippingWorkbook
|
||
{
|
||
public const SUPPLIER_CODE_HEADER = '供应商代码';
|
||
|
||
public const FIELD_HEADER = '是否包邮厂家';
|
||
|
||
/**
|
||
* 返回供应商代码 => 是否包邮厂家。空白或“-”表示否。
|
||
*
|
||
* @return array<string,bool>
|
||
*/
|
||
public function read(UploadedFile $file): array
|
||
{
|
||
try {
|
||
$reader = IOFactory::createReader(match (mb_strtolower($file->getClientOriginalExtension())) {
|
||
'xlsx' => 'Xlsx',
|
||
'xls' => 'Xls',
|
||
'csv' => 'Csv',
|
||
default => throw new \RuntimeException('Unsupported workbook format.'),
|
||
});
|
||
$book = $reader->load($file->getRealPath());
|
||
} catch (Throwable) {
|
||
throw ValidationException::withMessages([
|
||
'import' => '无法读取导入文件,请确认文件未损坏且格式为 xlsx、xls 或 csv。',
|
||
]);
|
||
}
|
||
|
||
try {
|
||
[$sheet, $headerRow, $codeColumn, $fieldColumn] = $this->locateColumns($book);
|
||
$highestRow = $sheet->getHighestDataRow();
|
||
if ($highestRow > 5001) {
|
||
throw ValidationException::withMessages(['import' => '单次最多导入 5,000 条供应商记录,请拆分文件后重试。']);
|
||
}
|
||
|
||
$records = [];
|
||
$lineByCode = [];
|
||
for ($row = $headerRow + 1; $row <= $highestRow; $row++) {
|
||
$code = $this->text($sheet, $codeColumn, $row);
|
||
if (str_starts_with($code, "'")) {
|
||
$code = substr($code, 1);
|
||
}
|
||
$field = $this->text($sheet, $fieldColumn, $row);
|
||
if ($code === '' && $field === '') {
|
||
continue;
|
||
}
|
||
if ($code === '') {
|
||
throw ValidationException::withMessages(['import' => "第 {$row} 行供应商代码为空。"]);
|
||
}
|
||
if (mb_strlen($code) > 191) {
|
||
throw ValidationException::withMessages(['import' => "第 {$row} 行供应商代码超过 191 个字符。"]);
|
||
}
|
||
$codeKey = mb_strtolower($code);
|
||
if (isset($lineByCode[$codeKey])) {
|
||
throw ValidationException::withMessages([
|
||
'import' => "第 {$row} 行供应商代码“{$code}”与第 {$lineByCode[$codeKey]} 行重复。",
|
||
]);
|
||
}
|
||
|
||
$records[$code] = $this->parseFlag($field, $row);
|
||
$lineByCode[$codeKey] = $row;
|
||
}
|
||
|
||
if ($records === []) {
|
||
throw ValidationException::withMessages(['import' => '表格中没有可导入的供应商记录。']);
|
||
}
|
||
|
||
return $records;
|
||
} finally {
|
||
$book->disconnectWorksheets();
|
||
}
|
||
}
|
||
|
||
public function template(): StreamedResponse
|
||
{
|
||
return response()->streamDownload(function (): void {
|
||
$book = new Spreadsheet();
|
||
$sheet = $book->getActiveSheet();
|
||
$sheet->setTitle('是否包邮厂家');
|
||
$sheet->fromArray([self::SUPPLIER_CODE_HEADER, '供应商名称', self::FIELD_HEADER], null, 'A1');
|
||
$sheet->getStyle('A1:C1')->applyFromArray([
|
||
'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']],
|
||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '856632']],
|
||
'alignment' => ['horizontal' => Alignment::HORIZONTAL_CENTER],
|
||
'borders' => ['allBorders' => ['borderStyle' => Border::BORDER_THIN, 'color' => ['rgb' => 'D8D2C7']]],
|
||
]);
|
||
$sheet->getRowDimension(1)->setRowHeight(24);
|
||
$sheet->freezePane('A2');
|
||
$sheet->setAutoFilter('A1:C1');
|
||
foreach (['A' => 22, 'B' => 34, 'C' => 20] as $column => $width) {
|
||
$sheet->getColumnDimension($column)->setWidth($width);
|
||
}
|
||
$sheet->getStyle('A2:A5001')->getNumberFormat()->setFormatCode('@');
|
||
|
||
$instructions = $book->createSheet();
|
||
$instructions->setTitle('填写说明');
|
||
$instructions->fromArray([
|
||
['项目', '说明'],
|
||
[self::SUPPLIER_CODE_HEADER, '必填。请填写供应商管理页面中的供应商代码,按代码精确关联。'],
|
||
[self::FIELD_HEADER, '填写“包邮”表示是包邮厂家;空白或“-”表示不是包邮厂家。'],
|
||
['兼容导出表', '也可以使用本页面导出的文件,在此列补充后直接导入。'],
|
||
['覆盖规则', '同一供应商再次导入会更新该字段;其他列不会修改。'],
|
||
], null, 'A1');
|
||
$instructions->getStyle('A1:B1')->applyFromArray([
|
||
'font' => ['bold' => true, 'color' => ['rgb' => 'FFFFFF']],
|
||
'fill' => ['fillType' => Fill::FILL_SOLID, 'startColor' => ['rgb' => '856632']],
|
||
]);
|
||
$instructions->getColumnDimension('A')->setWidth(22);
|
||
$instructions->getColumnDimension('B')->setWidth(76);
|
||
$instructions->getStyle('A1:B5')->getAlignment()->setVertical(Alignment::VERTICAL_CENTER)->setWrapText(true);
|
||
foreach (range(1, 5) as $row) {
|
||
$instructions->getRowDimension($row)->setRowHeight($row === 1 ? 24 : 34);
|
||
}
|
||
|
||
$book->setActiveSheetIndex(0);
|
||
(new Xlsx($book))->save('php://output');
|
||
$book->disconnectWorksheets();
|
||
}, '供应商是否包邮厂家_导入模板_' . now()->format('Ymd') . '.xlsx', [
|
||
'Content-Type' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
||
'Cache-Control' => 'private, no-store',
|
||
]);
|
||
}
|
||
|
||
/** @return array{0:Worksheet,1:int,2:int,3:int} */
|
||
private function locateColumns(Spreadsheet $book): array
|
||
{
|
||
$best = null;
|
||
foreach ($book->getWorksheetIterator() as $sheet) {
|
||
$rows = $sheet->toArray(null, false, false, false);
|
||
foreach (array_slice($rows, 0, 20, true) as $rowIndex => $row) {
|
||
$headers = [];
|
||
foreach ($row as $columnIndex => $value) {
|
||
$header = $this->header($value);
|
||
if ($header !== '') {
|
||
$headers[$header] = $columnIndex + 1;
|
||
}
|
||
}
|
||
if (isset($headers[self::SUPPLIER_CODE_HEADER], $headers[self::FIELD_HEADER])) {
|
||
$candidate = [$sheet, $rowIndex + 1, $headers[self::SUPPLIER_CODE_HEADER], $headers[self::FIELD_HEADER]];
|
||
$best = $candidate;
|
||
break 2;
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($best === null) {
|
||
throw ValidationException::withMessages([
|
||
'import' => '缺少表头:需要“供应商代码”和“是否包邮厂家”两列。',
|
||
]);
|
||
}
|
||
|
||
return $best;
|
||
}
|
||
|
||
private function parseFlag(string $value, int $row): bool
|
||
{
|
||
$normalized = mb_strtolower(preg_replace('/[\s\x{FEFF}]+/u', '', $value));
|
||
if (in_array($normalized, ['', '-', '否', '不是', '0', 'false', 'no'], true)) {
|
||
return false;
|
||
}
|
||
if (in_array($normalized, ['包邮', '是', '是包邮厂家', '1', 'true', 'yes'], true)) {
|
||
return true;
|
||
}
|
||
|
||
throw ValidationException::withMessages([
|
||
'import' => "第 {$row} 行“是否包邮厂家”只能填写“包邮”,或留空表示否。",
|
||
]);
|
||
}
|
||
|
||
private function text(Worksheet $sheet, int $column, int $row): string
|
||
{
|
||
$cell = $sheet->getCell([$column, $row]);
|
||
if ($cell->getDataType() === 'f') {
|
||
throw ValidationException::withMessages(['import' => "第 {$row} 行含公式,请填写实际值。"]);
|
||
}
|
||
|
||
return trim((string) $cell->getFormattedValue());
|
||
}
|
||
|
||
private function header(mixed $value): string
|
||
{
|
||
return str_replace([' ', ' ', "\n", "\r", "\t", "\x{FEFF}"], '', trim((string) $value));
|
||
}
|
||
}
|