161 lines
6.2 KiB
PHP
161 lines
6.2 KiB
PHP
<?php
|
||
|
||
namespace Modules\EcommerceFinance\Services;
|
||
|
||
use DateTimeImmutable;
|
||
use Illuminate\Database\ConnectionInterface;
|
||
use Illuminate\Database\DatabaseManager;
|
||
use Illuminate\Support\Facades\Schema;
|
||
use InvalidArgumentException;
|
||
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||
use RuntimeException;
|
||
|
||
class VipshopMpOpeningBalanceImporter
|
||
{
|
||
public const TABLE = 'vipshop_mp_opening_balance_snapshots';
|
||
|
||
private readonly ConnectionInterface $connection;
|
||
|
||
public function __construct(DatabaseManager $databases)
|
||
{
|
||
$this->connection = $databases->connection(
|
||
(string) config('upstream_mirrors.connection', 'business_mirror'),
|
||
);
|
||
}
|
||
|
||
/**
|
||
* @param list<string> $paths
|
||
* @param array<string,string> $storeMap
|
||
* @return array{status:string,period_end:string,store_count:int,balance_total:string,row_count:int}
|
||
*/
|
||
public function import(
|
||
array $paths,
|
||
string $periodEnd,
|
||
array $storeMap,
|
||
bool $dryRun = false,
|
||
): array {
|
||
$periodEnd = $this->periodEnd($periodEnd);
|
||
if (! Schema::connection($this->connection->getName())->hasTable(self::TABLE)) {
|
||
throw new RuntimeException('唯品会MP期初余额表尚未完成迁移。');
|
||
}
|
||
|
||
$balances = [];
|
||
$sourceFiles = [];
|
||
$sourceHashes = [];
|
||
$rowCount = 0;
|
||
foreach ($paths as $path) {
|
||
$realPath = realpath($path);
|
||
if ($realPath === false || ! is_file($realPath) || ! is_readable($realPath)) {
|
||
throw new InvalidArgumentException('唯品会MP余额文件不存在或不可读。');
|
||
}
|
||
if (strtolower((string) pathinfo($realPath, PATHINFO_EXTENSION)) !== 'xlsx') {
|
||
throw new InvalidArgumentException('唯品会MP余额只支持 XLSX 文件。');
|
||
}
|
||
|
||
$sourceName = basename($realPath);
|
||
$sourceFiles[] = $sourceName;
|
||
$sourceHashes[] = hash_file('sha256', $realPath);
|
||
$spreadsheet = IOFactory::load($realPath);
|
||
try {
|
||
$sheet = $spreadsheet->getActiveSheet();
|
||
$matrix = $sheet->toArray(null, true, true, false);
|
||
} finally {
|
||
$spreadsheet->disconnectWorksheets();
|
||
}
|
||
if ($matrix === []) {
|
||
continue;
|
||
}
|
||
|
||
$headers = array_map(
|
||
static fn (mixed $value): string => trim((string) $value),
|
||
array_shift($matrix),
|
||
);
|
||
$indexes = array_flip($headers);
|
||
foreach (['店铺名称', '待结算待付款(F2=F-F1)'] as $required) {
|
||
if (! array_key_exists($required, $indexes)) {
|
||
throw new InvalidArgumentException("{$sourceName} 缺少字段:{$required}");
|
||
}
|
||
}
|
||
|
||
foreach ($matrix as $offset => $values) {
|
||
if (! is_array($values) || ! array_filter($values, static fn (mixed $value): bool => $value !== null && $value !== '')) {
|
||
continue;
|
||
}
|
||
$rawStore = trim((string) ($values[$indexes['店铺名称']] ?? ''));
|
||
$store = trim((string) ($storeMap[$rawStore] ?? ''));
|
||
if ($store === '') {
|
||
throw new InvalidArgumentException(
|
||
"{$sourceName} 第 " . ($offset + 2) . " 行的店铺未配置财务系统名称:{$rawStore}",
|
||
);
|
||
}
|
||
$balance = $this->decimal(
|
||
$values[$indexes['待结算待付款(F2=F-F1)']] ?? null,
|
||
$offset + 2,
|
||
);
|
||
$balances[$store] = bcadd($balances[$store] ?? '0.000000', $balance, 6);
|
||
$rowCount++;
|
||
}
|
||
}
|
||
|
||
if ($balances === []) {
|
||
throw new InvalidArgumentException('唯品会MP余额文件中没有可导入的数据。');
|
||
}
|
||
|
||
if (! $dryRun) {
|
||
$now = now();
|
||
$sourceFilenames = mb_substr(implode(',', array_unique($sourceFiles)), 0, 255);
|
||
$sourceSha = hash('sha256', implode("\0", $sourceHashes));
|
||
$this->connection->transaction(function () use ($balances, $periodEnd, $sourceFilenames, $sourceSha, $now): void {
|
||
$this->connection->table(self::TABLE)->where('period_end', $periodEnd)->delete();
|
||
$this->connection->table(self::TABLE)->insert(array_map(
|
||
static fn (string $store, string $balance): array => [
|
||
'period_end' => $periodEnd,
|
||
'store_name' => mb_substr($store, 0, 255),
|
||
'balance' => $balance,
|
||
'source_filenames' => $sourceFilenames,
|
||
'source_sha256' => $sourceSha,
|
||
'imported_at' => $now,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
],
|
||
array_keys($balances),
|
||
array_values($balances),
|
||
));
|
||
});
|
||
}
|
||
|
||
$total = '0.000000';
|
||
foreach ($balances as $balance) {
|
||
$total = bcadd($total, $balance, 6);
|
||
}
|
||
|
||
return [
|
||
'status' => $dryRun ? 'validated' : 'succeeded',
|
||
'period_end' => $periodEnd,
|
||
'store_count' => count($balances),
|
||
'balance_total' => $total,
|
||
'row_count' => $rowCount,
|
||
];
|
||
}
|
||
|
||
private function periodEnd(string $value): string
|
||
{
|
||
$date = DateTimeImmutable::createFromFormat('!Y-m-d', trim($value));
|
||
if ($date === false || $date->format('Y-m-d') !== $value || $date->format('Y-m-t') !== $value) {
|
||
throw new InvalidArgumentException('唯品会MP余额截止日期必须是有效月末,格式 YYYY-MM-DD。');
|
||
}
|
||
|
||
return $value;
|
||
}
|
||
|
||
private function decimal(mixed $value, int $rowNumber): string
|
||
{
|
||
$raw = trim(str_replace([',', ',', '¥', '¥'], '', (string) $value));
|
||
if ($raw === '' || ! is_numeric($raw)) {
|
||
throw new InvalidArgumentException("第 {$rowNumber} 行的待结算待付款不是有效金额。");
|
||
}
|
||
|
||
return number_format((float) $raw, 6, '.', '');
|
||
}
|
||
}
|