CaiWuXiTong/app/Business/InventoryWarehouseNameMappingWorkbookImporter.php
chenxi 26556a9169 Add warehouse short names to inventory report
Restore warehouse name mapping ledger and inventory short-name column.
2026-09-28 16:33:38 +08:00

214 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 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;
}
}