214 lines
8.2 KiB
PHP
214 lines
8.2 KiB
PHP
<?php
|
||
|
||
namespace App\Business;
|
||
|
||
use App\Models\Business\InventoryWarehouseNameMapping;
|
||
use Illuminate\Http\UploadedFile;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Validation\ValidationException;
|
||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||
|
||
class InventoryWarehouseNameMappingWorkbookImporter
|
||
{
|
||
private const HEADERS = ['仓储方', '仓库', '仓库简称'];
|
||
|
||
/** @return array{rows:list<array>,sheet:string,headers:list<string>,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<array>} $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;
|
||
}
|
||
}
|