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> */ 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); } }