189 lines
6.2 KiB
PHP
189 lines
6.2 KiB
PHP
<?php
|
||
|
||
namespace App\Business;
|
||
|
||
use Illuminate\Http\UploadedFile;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Validation\ValidationException;
|
||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||
use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
|
||
|
||
class BusinessTableImporter
|
||
{
|
||
public function import(
|
||
UploadedFile $file,
|
||
TableDescriptor $descriptor,
|
||
string $section,
|
||
string $page,
|
||
?string $tab = null,
|
||
): array {
|
||
if ($section === 'fund-management' && $page === 'fund-forecast') {
|
||
return app(FundForecastWorkbookImporter::class)->import(
|
||
$file,
|
||
(int) (company_id() ?: 1),
|
||
);
|
||
}
|
||
|
||
$spreadsheet = IOFactory::load($file->getRealPath());
|
||
[$rows, $headerRow, $columnMap] = $this->bestMatchingSheet($spreadsheet, $descriptor);
|
||
|
||
if ($rows === null || $columnMap === []) {
|
||
throw ValidationException::withMessages([
|
||
'import' => '未找到可识别的表头,请使用当前页面显示的中文表头。',
|
||
]);
|
||
}
|
||
|
||
$created = 0;
|
||
$updated = 0;
|
||
$model = $descriptor->model;
|
||
$identityFields = TableRegistry::importIdentityFields($section, $page);
|
||
|
||
DB::transaction(function () use (
|
||
$rows,
|
||
$headerRow,
|
||
$columnMap,
|
||
$descriptor,
|
||
$section,
|
||
$page,
|
||
$tab,
|
||
$model,
|
||
$identityFields,
|
||
&$created,
|
||
&$updated,
|
||
) {
|
||
foreach (array_slice($rows, $headerRow + 1) as $offset => $row) {
|
||
$data = [];
|
||
|
||
foreach ($columnMap as $columnIndex => $field) {
|
||
$data[$field['name']] = $this->normalizeValue(
|
||
$row[$columnIndex] ?? null,
|
||
$field['type'],
|
||
$headerRow + $offset + 2,
|
||
$field['label'],
|
||
);
|
||
}
|
||
|
||
if (collect($data)->every(fn ($value) => $value === null || $value === '')) {
|
||
continue;
|
||
}
|
||
|
||
if ($descriptor->tabFilterColumn && $tab) {
|
||
$tabValue = TableRegistry::tabValue($section, $page, $tab);
|
||
|
||
if ($tabValue !== null) {
|
||
$data[$descriptor->tabFilterColumn] = $tabValue;
|
||
}
|
||
}
|
||
|
||
foreach (TableRegistry::fixedFilters($section, $page) as $column => $value) {
|
||
$data[$column] = $value;
|
||
}
|
||
|
||
$data['company_id'] = company_id() ?: 1;
|
||
$identity = collect($identityFields)
|
||
->filter(fn ($field) => array_key_exists($field, $data) && $data[$field] !== null && $data[$field] !== '')
|
||
->mapWithKeys(fn ($field) => [$field => $data[$field]])
|
||
->all();
|
||
|
||
$record = count($identity) === count($identityFields) && $identity !== []
|
||
? $model::query()->where($identity)->first()
|
||
: null;
|
||
|
||
if ($record) {
|
||
$record->update($data);
|
||
$updated++;
|
||
} else {
|
||
$model::create($data);
|
||
$created++;
|
||
}
|
||
}
|
||
});
|
||
|
||
if ($created + $updated === 0) {
|
||
throw ValidationException::withMessages([
|
||
'import' => '表格中没有可导入的数据行。',
|
||
]);
|
||
}
|
||
|
||
return compact('created', 'updated');
|
||
}
|
||
|
||
private function bestMatchingSheet($spreadsheet, TableDescriptor $descriptor): array
|
||
{
|
||
$fieldLookup = [];
|
||
|
||
foreach ($descriptor->fields as $field) {
|
||
$fieldLookup[$this->normalizeHeader($field['name'])] = $field;
|
||
$fieldLookup[$this->normalizeHeader($field['label'])] = $field;
|
||
}
|
||
|
||
$best = [null, null, []];
|
||
$bestScore = 0;
|
||
|
||
foreach ($spreadsheet->getWorksheetIterator() as $worksheet) {
|
||
$rows = $worksheet->toArray(null, true, false, false);
|
||
|
||
foreach (array_slice($rows, 0, 20, true) as $rowIndex => $row) {
|
||
$columnMap = [];
|
||
|
||
foreach ($row as $columnIndex => $heading) {
|
||
$key = $this->normalizeHeader($heading);
|
||
|
||
if ($key !== '' && isset($fieldLookup[$key])) {
|
||
$columnMap[$columnIndex] = $fieldLookup[$key];
|
||
}
|
||
}
|
||
|
||
if (count($columnMap) > $bestScore) {
|
||
$best = [$rows, $rowIndex, $columnMap];
|
||
$bestScore = count($columnMap);
|
||
}
|
||
}
|
||
}
|
||
|
||
return $best;
|
||
}
|
||
|
||
private function normalizeHeader($value): string
|
||
{
|
||
return mb_strtolower(preg_replace('/[\s\x{FEFF}_\-()()]+/u', '', trim((string) $value)));
|
||
}
|
||
|
||
private function normalizeValue($value, string $type, int $rowNumber, string $label)
|
||
{
|
||
if ($value === null || trim((string) $value) === '') {
|
||
return null;
|
||
}
|
||
|
||
if ($type === 'date') {
|
||
try {
|
||
return is_numeric($value)
|
||
? ExcelDate::excelToDateTimeObject($value)->format('Y-m-d')
|
||
: \Illuminate\Support\Carbon::parse($value)->format('Y-m-d');
|
||
} catch (\Throwable $e) {
|
||
throw ValidationException::withMessages([
|
||
'import' => "第 {$rowNumber} 行“{$label}”不是有效日期。",
|
||
]);
|
||
}
|
||
}
|
||
|
||
if (in_array($type, ['number', 'currency'], true)) {
|
||
$number = str_replace([',', ',', '¥', '¥'], '', (string) $value);
|
||
|
||
if (! is_numeric($number)) {
|
||
throw ValidationException::withMessages([
|
||
'import' => "第 {$rowNumber} 行“{$label}”不是有效数字。",
|
||
]);
|
||
}
|
||
|
||
return (float) $number;
|
||
}
|
||
|
||
if ($type === 'boolean') {
|
||
return in_array(mb_strtolower(trim((string) $value)), ['1', '是', 'true', 'yes', 'y'], true) ? 1 : 0;
|
||
}
|
||
|
||
return trim((string) $value);
|
||
}
|
||
}
|