CaiWuXiTong/modules/EcommerceFinance/Services/ProductPriceWorkbookImporter.php

242 lines
8.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Modules\EcommerceFinance\Services;
use Illuminate\Http\UploadedFile;
use Illuminate\Validation\ValidationException;
use Modules\EcommerceFinance\Repositories\ProductPriceMirrorRepository;
use PhpOffice\PhpSpreadsheet\IOFactory;
use RuntimeException;
use Throwable;
class ProductPriceWorkbookImporter
{
/** @var array<string,string> */
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<string,int> */
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<string,array{label:string,type:string}> */
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, '+');
}
}