CaiWuXiTong/modules/EcommerceFinance/Services/FinanceLedgerBaseImporter.php

290 lines
10 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\Support\Facades\DB;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Worksheet\Worksheet;
use RuntimeException;
/**
* 导入财务台账的基础数据:账户、店铺、公司和期初余额。
*
* 源文件是财务维护的《银行当前余额-*.xlsx》。这里只做结构校验和全量
* 覆盖,不推断业务口径;导入完成后由 FinanceLedgerCalculator 重算流水。
*/
class FinanceLedgerBaseImporter
{
public function import(string $path, ?string $openingDate = null): array
{
if (! is_file($path)) {
throw new RuntimeException('基础数据文件不存在:' . $path);
}
$reader = IOFactory::createReaderForFile($path);
$reader->setReadDataOnly(true);
$spreadsheet = $reader->load($path);
try {
$accounts = $this->accounts($spreadsheet->getSheetByName('账户总表'));
$shops = $this->shops($spreadsheet->getSheetByName('店铺台账'));
$companies = $this->companies($spreadsheet->getSheetByName('公司台账'));
$openingDate = $openingDate ?: (string) config('finance_ledger.opening_date', '2026-10-17');
$opening = $this->opening($spreadsheet->getSheetByName('每日资金流水'), $openingDate);
} finally {
$spreadsheet->disconnectWorksheets();
}
if ($accounts === []) {
throw new RuntimeException('账户总表没有可导入的数据。');
}
$accountKeys = array_fill_keys(array_map(
static fn (array $row): string => (string) $row['account_short'],
$accounts,
), true);
$missing = [];
foreach ($opening as $row) {
if (! isset($accountKeys[$row['account_short']])) {
$missing[] = $row['account_short'];
}
}
if ($missing !== []) {
throw new RuntimeException('期初账户在账户总表中不存在:' . implode('、', array_slice(array_unique($missing), 0, 10)));
}
$accountBalance = round(array_sum(array_column($accounts, 'current_balance')), 2);
$openingBalance = round(array_sum(array_column($opening, 'opening_balance')), 2);
if (abs($accountBalance - $openingBalance) > 0.01) {
throw new RuntimeException(sprintf(
'期初合计 %.2f 与账户余额合计 %.2f 不一致。',
$openingBalance,
$accountBalance,
));
}
$now = now();
$connection = $this->connection();
$connection->transaction(function () use ($connection, $accounts, $shops, $companies, $opening, $now): void {
$connection->table('fin_account_opening')->delete();
$connection->table('fin_shops')->delete();
$connection->table('fin_companies')->delete();
$connection->table('fin_accounts')->delete();
$this->insert($connection, 'fin_accounts', $accounts, $now);
$this->insert($connection, 'fin_shops', $shops, $now);
$this->insert($connection, 'fin_companies', $companies, $now);
$this->insert($connection, 'fin_account_opening', $opening, $now);
});
return [
'accounts' => count($accounts),
'shops' => count($shops),
'companies' => count($companies),
'opening_rows' => count($opening),
'opening_sum' => $openingBalance,
'account_balance_sum' => $accountBalance,
'opening_date' => $openingDate,
];
}
private function accounts(?Worksheet $sheet): array
{
if (! $sheet) {
throw new RuntimeException('缺少「账户总表」工作表。');
}
$rows = [];
foreach ($this->rows($sheet, 2, 23) as $row) {
$company = $this->text($row[0] ?? null);
$short = $this->text($row[1] ?? null);
if ($company === '' || $short === '') {
continue;
}
$rows[] = [
'company_name' => $company,
'account_short' => $short,
'region' => null,
'account_no' => $this->text($row[2] ?? null),
'bank_name' => $this->text($row[3] ?? null),
'bank_city_branch' => $this->text($row[4] ?? null),
'account_type' => $this->text($row[5] ?? null),
'company_plan' => $this->text($row[6] ?? null),
'account_last4' => $this->text($row[7] ?? null),
'single_limit' => $this->text($row[8] ?? null),
'daily_limit' => $this->text($row[9] ?? null),
'monthly_limit' => $this->text($row[10] ?? null),
'yearly_limit' => $this->text($row[11] ?? null),
'current_balance' => $this->number($row[12] ?? null),
'blocked_amount' => $this->number($row[13] ?? null),
'daily_available' => $this->number($row[14] ?? null),
'clearing_days' => $this->nullableNumber($row[15] ?? null),
'raise_target' => $this->text($row[16] ?? null),
'raise_status' => $this->text($row[17] ?? null),
'raise_actions' => $this->text($row[18] ?? null),
'done_actions' => $this->text($row[19] ?? null),
'next_action' => $this->text($row[20] ?? null),
'remark' => $this->text($row[21] ?? null),
'owner' => $this->text($row[22] ?? null),
];
}
return $rows;
}
private function shops(?Worksheet $sheet): array
{
if (! $sheet) {
throw new RuntimeException('缺少「店铺台账」工作表。');
}
$statusMap = ['正常' => '在营', '关闭中' => '暂停', '已关闭' => '已注销'];
$rows = [];
foreach ($this->rows($sheet, 6, 8) as $row) {
$name = $this->text($row[0] ?? null);
if ($name === '') {
continue;
}
$status = $this->text($row[4] ?? null);
$rows[] = [
'shop_name' => $name,
'platform' => $this->text($row[1] ?? null),
'company_name' => $this->text($row[2] ?? null),
'account_short' => $this->text($row[3] ?? null),
'status' => $statusMap[$status] ?? $status,
'payout_account' => $this->text($row[5] ?? null),
'shop_balance' => $this->number($row[6] ?? null),
'remark' => $this->text($row[7] ?? null),
];
}
return $rows;
}
private function companies(?Worksheet $sheet): array
{
if (! $sheet) {
throw new RuntimeException('缺少「公司台账」工作表。');
}
$rows = [];
foreach ($this->rows($sheet, 6, 10) as $row) {
$name = $this->text($row[0] ?? null);
if ($name === '') {
continue;
}
$rows[] = [
'name' => $name,
'register_place' => $this->text($row[1] ?? null),
'business_type' => $this->text($row[2] ?? null),
'tax_type' => $this->text($row[3] ?? null),
'status' => $this->text($row[4] ?? null),
'basic_bank' => $this->text($row[5] ?? null),
'basic_account_last4' => $this->text($row[6] ?? null),
'remark' => $this->text($row[9] ?? null),
];
}
return $rows;
}
private function opening(?Worksheet $sheet, string $openingDate): array
{
if (! $sheet) {
throw new RuntimeException('缺少「每日资金流水」工作表。');
}
$rows = [];
foreach ($this->rows($sheet, 6, 8) as $row) {
$short = $this->text($row[2] ?? null);
if ($short === '') {
continue;
}
$rows[] = [
'account_short' => $short,
'opening_date' => $openingDate,
'opening_balance' => $this->number($row[3] ?? null),
'remark' => $this->text($row[7] ?? null),
];
}
return $rows;
}
/** @return array<int,array<int,mixed>> */
private function rows(Worksheet $sheet, int $startRow, int $columnCount): array
{
$lastRow = $sheet->getHighestDataRow();
if ($lastRow < $startRow) {
return [];
}
return $sheet->rangeToArray(
'A' . $startRow . ':' . \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($columnCount) . $lastRow,
null,
false,
false,
false,
);
}
private function insert($connection, string $table, array $rows, $now): void
{
foreach ($rows as &$row) {
$row['created_at'] = $now;
$row['updated_at'] = $now;
}
unset($row);
foreach (array_chunk($rows, 500) as $chunk) {
$connection->table($table)->insert($chunk);
}
}
private function connection()
{
return DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
}
private function text(mixed $value): string
{
if ($value === null) {
return '';
}
$text = trim((string) $value);
if (in_array($text, ['#N/A', '#DIV/0!', '#VALUE!', '#REF!', 'None'], true)) {
return '';
}
return preg_replace('/\s+/u', ' ', $text) ?? $text;
}
private function number(mixed $value): float
{
if ($value === null || $value === '') {
return 0.0;
}
if (is_int($value) || is_float($value)) {
return round((float) $value, 2);
}
$text = str_replace([',', ',', '¥', '¥', ' '], '', trim((string) $value));
if ($text === '' || $text === '-' || $text === '/') {
return 0.0;
}
return is_numeric($text) ? round((float) $text, 2) : 0.0;
}
private function nullableNumber(mixed $value): ?float
{
if ($value === null || trim((string) $value) === '' || trim((string) $value) === '-') {
return null;
}
return $this->number($value);
}
}