CaiWuXiTong/app/Business/InventoryInboundDetailImporter.php

238 lines
8.8 KiB
PHP

<?php
namespace App\Business;
use App\Models\Business\InventoryWarehouseNameMapping;
use DateTimeImmutable;
use Illuminate\Support\Facades\DB;
use PhpOffice\PhpSpreadsheet\IOFactory;
use RuntimeException;
class InventoryInboundDetailImporter
{
private const TABLE = 'inventory_inbound_detail_rows';
/**
* Replace the imported inbound detail snapshot only after the workbook has
* been opened successfully. The delete and inserts share one transaction,
* so a malformed file cannot leave the page with a partially imported table.
*
* @return array{imported:int,mapped:int,skipped:int,total:int}
*/
public function import(string $file, string $forcedMonth = ''): array
{
$path = realpath($file);
if ($path === false || ! is_file($path) || ! is_readable($path)) {
throw new RuntimeException('入库明细文件不存在或不可读取。');
}
$forcedMonth = $this->normalizeForcedMonth($forcedMonth);
$connection = (string) config('upstream_mirrors.connection', 'business_mirror');
$now = now();
$maps = $this->shortNameMap();
$reader = IOFactory::createReaderForFile($path);
$reader->setReadDataOnly(true);
$spreadsheet = $reader->load($path);
$sheet = $spreadsheet->getSheet(0);
try {
return DB::connection($connection)->transaction(function () use (
$connection,
$sheet,
$maps,
$forcedMonth,
$now,
): array {
DB::connection($connection)->table(self::TABLE)->delete();
$batch = [];
$imported = 0;
$mapped = 0;
$skipped = 0;
foreach ($sheet->getRowIterator(2) as $row) {
$rowNumber = $row->getRowIndex();
$cells = [];
foreach ($row->getCellIterator('A', 'U') as $cell) {
$cells[] = $cell->getFormattedValue();
}
$warehouse = trim((string) ($cells[14] ?? ''));
$createdRaw = trim((string) ($cells[16] ?? ''));
if ($warehouse === '' && $createdRaw === '') {
$skipped++;
continue;
}
$month = $forcedMonth !== '' ? $forcedMonth : $this->monthOf($createdRaw);
if ($month === '') {
$skipped++;
continue;
}
// 代发仓按仓储方匹配;其它仓库按仓库名称做唯一匹配。
$shortName = '';
if ($warehouse !== '') {
$shortName = str_contains($warehouse, '代发仓')
? ($maps['provider'][$warehouse] ?? $maps['warehouse'][$warehouse] ?? '')
: ($maps['warehouse'][$warehouse] ?? '');
}
if ($shortName !== '') {
$mapped++;
}
$batch[] = [
'report_month' => $month,
'row_key' => sha1(substr($month, 0, 7) . '|' . $rowNumber),
'source_no' => $this->text($cells[0] ?? ''),
'document_no' => $this->text($cells[1] ?? ''),
'created_serial' => $this->text($cells[2] ?? ''),
'supplier' => $this->text($cells[3] ?? ''),
'item_no' => $this->text($cells[4] ?? ''),
'barcode' => $this->text($cells[5] ?? ''),
'spec_name' => $this->text($cells[6] ?? ''),
'quantity' => $this->number($cells[7] ?? ''),
'unit_price' => $this->number($cells[8] ?? ''),
'amount' => $this->number($cells[9] ?? ''),
'estimated_unit_price' => $this->number($cells[10] ?? ''),
'estimated_amount' => $this->number($cells[11] ?? ''),
'supplier_barcode' => $this->text($cells[12] ?? ''),
'remark' => $this->text($cells[13] ?? ''),
'warehouse' => $warehouse !== '' ? $warehouse : null,
'warehouse_short_name' => $shortName !== '' ? $shortName : null,
'created_at_raw' => $this->moment($createdRaw),
'express_name' => $this->text($cells[17] ?? ''),
'express_no' => $this->text($cells[18] ?? ''),
'not_sent' => $this->text($cells[19] ?? ''),
'owner_name' => $this->text($cells[20] ?? ''),
'imported_at' => $now,
];
$imported++;
if (count($batch) >= 1000) {
DB::connection($connection)->table(self::TABLE)->insertOrIgnore($batch);
$batch = [];
}
}
if ($batch !== []) {
DB::connection($connection)->table(self::TABLE)->insertOrIgnore($batch);
}
if ($imported === 0) {
throw new RuntimeException('入库明细文件中没有可导入的数据。');
}
return [
'imported' => $imported,
'mapped' => $mapped,
'skipped' => $skipped,
'total' => DB::connection($connection)->table(self::TABLE)->count(),
];
});
} finally {
$spreadsheet->disconnectWorksheets();
}
}
private function normalizeForcedMonth(string $month): string
{
$month = trim($month);
if ($month === '') {
return '';
}
if (preg_match('/^\d{4}-\d{2}$/', $month) === 1) {
return $month . '-01';
}
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $month) === 1) {
return $month;
}
throw new RuntimeException('指定月份格式应为 YYYY-MM。');
}
/** @return array{warehouse:array<string,string>,provider:array<string,string>} */
private function shortNameMap(): array
{
$rows = InventoryWarehouseNameMapping::withoutGlobalScopes()
->get(['warehouse_provider', 'warehouse', 'warehouse_short_name']);
$grouped = [];
foreach ($rows as $row) {
$warehouse = trim((string) $row->warehouse);
if ($warehouse === '') {
continue;
}
$grouped[$warehouse][] = trim((string) $row->warehouse_short_name);
}
$byWarehouse = [];
foreach ($grouped as $warehouse => $names) {
$unique = array_values(array_unique($names));
if (count($unique) === 1 && $unique[0] !== '') {
$byWarehouse[$warehouse] = $unique[0];
}
}
$byProvider = [];
foreach ($rows as $row) {
$provider = trim((string) $row->warehouse_provider);
if ($provider !== '') {
$byProvider[$provider][] = trim((string) $row->warehouse_short_name);
}
}
foreach ($byProvider as $provider => $names) {
$unique = array_values(array_unique($names));
if (count($unique) === 1 && $unique[0] !== '') {
$byProvider[$provider] = $unique[0];
}
}
return ['warehouse' => $byWarehouse, 'provider' => $byProvider];
}
private function monthOf(string $value): string
{
$value = trim($value);
if ($value === '') {
return '';
}
foreach (['Y/m/d H:i:s', 'Y-m-d H:i:s', 'Y/m/d', 'Y-m-d'] as $format) {
$parsed = DateTimeImmutable::createFromFormat($format, $value);
if ($parsed instanceof DateTimeImmutable) {
return $parsed->format('Y-m') . '-01';
}
}
$timestamp = strtotime($value);
return $timestamp === false ? '' : date('Y-m-01', $timestamp);
}
private function moment(string $value): ?string
{
$value = trim($value);
if ($value === '') {
return null;
}
$timestamp = strtotime($value);
return $timestamp === false ? null : date('Y-m-d H:i:s', $timestamp);
}
private function text(mixed $value): ?string
{
$text = trim((string) $value);
return $text === '' ? null : $text;
}
private function number(mixed $value): ?float
{
$text = str_replace(',', '', trim((string) $value));
if ($text === '' || ! is_numeric($text)) {
return null;
}
return (float) $text;
}
}