CaiWuXiTong/modules/EcommerceFinance/Services/AlibabaAccountBalanceWorkbookImporter.php

208 lines
8.0 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 Carbon\CarbonImmutable;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\DatabaseManager;
use Illuminate\Support\Facades\Schema;
use InvalidArgumentException;
use Modules\EcommerceFinance\Repositories\AlibabaAccountBalanceRepository;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Shared\Date as ExcelDate;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use RuntimeException;
class AlibabaAccountBalanceWorkbookImporter
{
private const HEADERS = [
'日期', '店铺名称', '期初余额', '本期收入', '本期支出', '期末余额',
'系统期末余额', '余额差异', '差异原因', '核对状态', '备注',
];
private readonly ConnectionInterface $connection;
public function __construct(DatabaseManager $database)
{
$this->connection = $database->connection(
(string) config('upstream_mirrors.connection', 'business_mirror')
);
}
/** @return array{created:int,updated:int,skipped:int,total:int} */
public function import(string $path, ?string $sourceFileName = null, bool $dryRun = false): array
{
if (! is_file($path) || ! is_readable($path)) {
throw new InvalidArgumentException('余额文件不存在或不可读取。');
}
$rows = $this->readRows($path);
if ($rows === []) {
throw new InvalidArgumentException('余额文件中没有可导入的数据行。');
}
if (! Schema::connection($this->connection->getName())->hasTable(AlibabaAccountBalanceRepository::TABLE)) {
throw new RuntimeException('阿里巴巴余额快照表尚未创建,请先执行数据库迁移。');
}
$created = 0;
$updated = 0;
$sourceFileName = mb_substr(basename($sourceFileName ?: $path), 0, 255);
$sourceSha256 = hash_file('sha256', $path);
$now = now();
$this->connection->transaction(function () use (
$rows, $sourceFileName, $sourceSha256, $now, $dryRun, &$created, &$updated
): void {
foreach ($rows as $row) {
$identity = [
'balance_date' => $row['balance_date'],
'store_name' => $row['store_name'],
];
$exists = $this->connection->table(AlibabaAccountBalanceRepository::TABLE)
->where($identity)
->exists();
$exists ? $updated++ : $created++;
if ($dryRun) {
continue;
}
$values = $row + [
'source_file_name' => $sourceFileName,
'source_sha256' => $sourceSha256,
'imported_at' => $now,
'updated_at' => $now,
];
if ($exists) {
$this->connection->table(AlibabaAccountBalanceRepository::TABLE)
->where($identity)
->update($values);
} else {
$this->connection->table(AlibabaAccountBalanceRepository::TABLE)
->insert($values + ['created_at' => $now]);
}
}
});
return ['created' => $created, 'updated' => $updated, 'skipped' => 0, 'total' => count($rows)];
}
/** @return array<int,array<string,mixed>> */
private function readRows(string $path): array
{
$spreadsheet = IOFactory::load($path);
$worksheet = $spreadsheet->getSheetByName('阿里巴巴账户') ?: $spreadsheet->getActiveSheet();
$headerRow = $this->findHeaderRow($worksheet);
$rows = [];
$seen = [];
for ($rowNumber = $headerRow + 1; $rowNumber <= $worksheet->getHighestDataRow(); $rowNumber++) {
$values = [];
foreach (range(1, count(self::HEADERS)) as $column) {
$values[] = $worksheet->getCell([$column, $rowNumber])->getCalculatedValue();
}
if (collect($values)->every(static fn (mixed $value): bool => $value === null || trim((string) $value) === '')) {
continue;
}
$date = $this->date($values[0] ?? null, $rowNumber);
$storeName = trim((string) ($values[1] ?? ''));
if ($storeName === '') {
throw new InvalidArgumentException("第 {$rowNumber} 行“店铺名称”不能为空。");
}
if (mb_strlen($storeName) > 255) {
throw new InvalidArgumentException("第 {$rowNumber} 行“店铺名称”超过 255 个字符。");
}
$key = $date . "\0" . $storeName;
if (isset($seen[$key])) {
throw new InvalidArgumentException("第 {$rowNumber} 行与第 {$seen[$key]} 行的日期和店铺名称重复。");
}
$seen[$key] = $rowNumber;
$rows[] = [
'balance_date' => $date,
'store_name' => $storeName,
'opening_balance' => $this->number($values[2] ?? null, $rowNumber, '期初余额'),
'income_total' => $this->number($values[3] ?? null, $rowNumber, '本期收入'),
'expense_total' => $this->number($values[4] ?? null, $rowNumber, '本期支出'),
'closing_balance' => $this->number($values[5] ?? null, $rowNumber, '期末余额'),
'system_closing_balance' => $this->number($values[6] ?? null, $rowNumber, '系统期末余额'),
'balance_difference' => $this->number($values[7] ?? null, $rowNumber, '余额差异'),
'difference_reason' => $this->text($values[8] ?? null),
'verification_status' => $this->text($values[9] ?? null, 64, $rowNumber, '核对状态'),
'notes' => $this->text($values[10] ?? null),
];
}
$spreadsheet->disconnectWorksheets();
return $rows;
}
private function findHeaderRow(Worksheet $worksheet): int
{
$maximum = min($worksheet->getHighestDataRow(), 20);
for ($row = 1; $row <= $maximum; $row++) {
$headers = [];
foreach (range(1, count(self::HEADERS)) as $column) {
$headers[] = trim((string) $worksheet->getCell([$column, $row])->getValue());
}
if ($headers === self::HEADERS) {
return $row;
}
}
throw new InvalidArgumentException('未找到阿里巴巴账户余额的 11 列标准表头。');
}
private function date(mixed $value, int $rowNumber): string
{
try {
$date = is_numeric($value)
? CarbonImmutable::instance(ExcelDate::excelToDateTimeObject((float) $value))
: CarbonImmutable::parse((string) $value);
} catch (\Throwable) {
throw new InvalidArgumentException("第 {$rowNumber} 行“日期”不是有效日期。");
}
if (! $date->isLastOfMonth()) {
throw new InvalidArgumentException("第 {$rowNumber} 行“日期”必须是月末日期。");
}
return $date->format('Y-m-d');
}
private function number(mixed $value, int $rowNumber, string $label): ?float
{
if ($value === null || trim((string) $value) === '') {
return null;
}
$normalized = str_replace([',', ',', '¥', '¥'], '', trim((string) $value));
if (! is_numeric($normalized)) {
throw new InvalidArgumentException("第 {$rowNumber} 行“{$label}”不是有效金额。");
}
return round((float) $normalized, 6);
}
private function text(mixed $value, ?int $limit = null, ?int $rowNumber = null, ?string $label = null): ?string
{
if ($value === null || trim((string) $value) === '') {
return null;
}
$text = trim((string) $value);
if ($limit !== null && mb_strlen($text) > $limit) {
throw new InvalidArgumentException("第 {$rowNumber} 行“{$label}”超过 {$limit} 个字符。");
}
return $text;
}
}