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