CaiWuXiTong/modules/EcommerceFinance/Services/PayablesFinalImporter.php
renshen 813e956df5 应付款终版目录接入独立按表数据
两个「终版」目录(供应商应付(终版)、快递应付(终版))改为独立数据:
保留原有导入、导出、查询、重置、编辑按钮和导入模板,数据初始为空、按表独立存放,
不再借用基础目录的页面与数据。

- 终版目录拥有自己的同名子页面与横向页签
- 新增终版按表数据的独立导入/导出/模板/编辑接口(数据集键 final:<目录>:<页面>)
- 供应商终版 13 个页面、快递终版 4 个页面已接入
- 终版导入模板统一增加“月份”列,记录标识列改为选填
2026-09-29 11:06:41 +08:00

148 lines
5.6 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 PhpOffice\PhpSpreadsheet\IOFactory;
use Throwable;
/**
* 「终版」目录按表导入:数据独立存放,可以从空表开始导入,
* 不要求基础页面那种“先导出再改”的记录标识列。
*/
class PayablesFinalImporter
{
private const MAX_ROWS = 10000;
public function __construct(
private readonly PayablesFieldNormalizer $normalizer,
private readonly SupplierPayableOverrideStore $store,
) {
}
/**
* @param array<string,array{label:string,type:string}> $fields 该表的字段定义(决定表头)
* @param string $storeDataset 「终版」目录实际保存记录用的数据集键
* @return array{created:int,updated:int,rows:int}
*/
public function import(array $fields, string $storeDataset, UploadedFile $file, int $companyId, ?int $userId): array
{
$lookup = ['记录标识' => '_action_key', 'recordkey' => '_action_key', '_action_key' => '_action_key'];
foreach ($fields as $field => $definition) {
$lookup[$this->header($field)] = $field;
$lookup[$this->header($definition['label'])] = $field;
}
try {
$readerType = match (mb_strtolower($file->getClientOriginalExtension())) {
'xlsx' => 'Xlsx',
'xls' => 'Xls',
'csv' => 'Csv',
default => throw new \RuntimeException('Unsupported workbook format.'),
};
$spreadsheet = IOFactory::createReader($readerType)->load($file->getRealPath());
[$rows, $headerRow, $columns] = $this->bestSheet($spreadsheet, $lookup);
} catch (Throwable) {
throw ValidationException::withMessages([
'import' => '无法读取导入文件,请确认文件未损坏且格式为 xlsx、xls 或 csv。',
]);
} finally {
if (isset($spreadsheet)) {
$spreadsheet->disconnectWorksheets();
}
}
if ($rows === null || $columns === []) {
throw ValidationException::withMessages([
'import' => '没有识别到表头,请先下载本页面的导入模板,按模板列填写后重试。',
]);
}
$store = $this->store;
$records = [];
foreach (array_slice($rows, $headerRow + 1, null, true) as $rowIndex => $row) {
$line = $rowIndex + 1;
$key = '';
$rawValues = [];
foreach ($columns as $columnIndex => $field) {
if ($field === '_action_key') {
$key = trim((string) ($row[$columnIndex] ?? ''));
} else {
$rawValues[$field] = $row[$columnIndex] ?? null;
}
}
if (collect($rawValues)->every(static fn (mixed $value): bool => trim((string) $value) === '')) {
continue;
}
try {
$values = $this->normalizer->normalize($fields, $rawValues);
} catch (ValidationException $exception) {
throw ValidationException::withMessages([
'import' => "第 {$line} 行:" . collect($exception->errors())->flatten()->first(),
]);
}
if ($key === '') {
$key = $store->key($storeDataset, $values);
}
if (isset($records[$key])) {
throw ValidationException::withMessages(['import' => "第 {$line} 行与前一行内容重复。"]);
}
$records[$key] = $values;
if (count($records) > self::MAX_ROWS) {
throw ValidationException::withMessages(['import' => '单次最多导入 10,000 条记录,请拆分文件后重试。']);
}
}
if ($records === []) {
throw ValidationException::withMessages(['import' => '表格中没有可导入的数据行。']);
}
$counts = ['created' => 0, 'updated' => 0];
$store->transaction(function () use ($store, $records, $storeDataset, $companyId, $userId, &$counts): void {
foreach ($records as $key => $values) {
$result = $store->save($storeDataset, $key, $values, $companyId, $userId, 'import');
$counts['created'] += $result['created'];
$counts['updated'] += $result['updated'];
}
});
return $counts + ['rows' => count($records)];
}
private function bestSheet(mixed $spreadsheet, array $lookup): array
{
$best = [null, null, []];
$score = 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->header($heading);
if ($key !== '' && isset($lookup[$key])) {
$columns[$columnIndex] = $lookup[$key];
}
}
if (count($columns) > $score) {
$best = [$rows, $rowIndex, $columns];
$score = count($columns);
}
}
}
return $best;
}
private function header(mixed $value): string
{
return mb_strtolower(preg_replace('/[\s\x{FEFF}_\-()()]+/u', '', trim((string) $value)));
}
}