['tmall', 'taobao', '1688'], FinanceBillBalanceRepository::SCOPE_JD_WALLET => ['jd', 'jingxi'], 'pinduoduo' => ['pinduoduo'], 'douyin' => ['douyin'], ]; private readonly ConnectionInterface $connection; public function __construct( DatabaseManager $database, private readonly JushuitanShopBalanceRepository $jushuitanBalances, private readonly FinanceBillBalanceRepository $financeBalances, ) { $this->connection = $database->connection( (string) config('upstream_mirrors.connection', 'business_mirror'), ); } /** * @return array{ * source_scope:string,supported:bool,reason:?string,missing:int,resolved:int, * unresolved:int,filled:int,dry_run:bool * } */ public function fillMissing(string $balanceDate, string $sourceScope, bool $dryRun = false): array { $date = CarbonImmutable::createFromFormat('!Y-m-d', $balanceDate); if (! $date instanceof CarbonImmutable || $date->format('Y-m-d') !== $balanceDate) { throw new InvalidArgumentException('余额日期必须使用 YYYY-MM-DD 格式。'); } $sourceScope = trim($sourceScope); $platformKeys = $this->platformKeysForScope($sourceScope); if ($platformKeys === []) { return [ 'source_scope' => $sourceScope, 'supported' => false, 'reason' => 'unknown_balance_scope', 'missing' => 0, 'resolved' => 0, 'unresolved' => 0, 'filled' => 0, 'dry_run' => $dryRun, ]; } $accounts = StoreReceivingAccount::withoutGlobalScopes() ->whereNull('deleted_at') ->get() ->filter(static function (StoreReceivingAccount $account) use ($platformKeys): bool { $platformKey = JushuitanShopBalanceRepository::platformKey( (string) $account->store_platform, ); return $platformKey !== null && in_array($platformKey, $platformKeys, true); }) ->values(); if ($accounts->isEmpty()) { return $this->result($sourceScope, true, null, 0, 0, $dryRun); } $matches = $this->jushuitanBalances->matchesForStoresOnDates( $accounts, [$balanceDate], JushuitanShopBalanceRepository::DATA_SOURCE_JUSHUITAN, ); $missingAccounts = $accounts->filter( static function (StoreReceivingAccount $account) use ($matches, $balanceDate): bool { $status = $matches[(int) $account->id][$balanceDate]['status'] ?? 'not_ready'; return in_array($status, ['unmatched', 'not_ready'], true); }, )->values(); $providerScopes = $this->providerScopes($sourceScope); if ($missingAccounts->isEmpty()) { if (! $dryRun && $providerScopes !== []) { $this->replaceFinanceRows($balanceDate, $sourceScope, []); } return $this->result( $sourceScope, $providerScopes !== [], $providerScopes === [] ? 'finance_bill_has_no_account_balance' : null, 0, 0, $dryRun, ); } if ($providerScopes === []) { return $this->result( $sourceScope, false, 'finance_bill_has_no_account_balance', $missingAccounts->count(), 0, $dryRun, ); } $storeNames = $missingAccounts ->flatMap(static fn (StoreReceivingAccount $account): array => [ (string) ($account->shop_name ?? ''), (string) ($account->erp_store_name ?? ''), ]) ->map(static fn (string $storeName): string => trim($storeName)) ->filter() ->unique() ->values() ->all(); $financeRows = []; foreach ($providerScopes as $providerScope) { $financeRows = [ ...$financeRows, ...$this->financeBalances->latestBalances($providerScope, $balanceDate, $storeNames), ]; } $financeByStore = []; foreach ($financeRows as $row) { $platformKey = JushuitanShopBalanceRepository::platformKey( (string) ($row['source_platform'] ?? ''), ); if ($platformKey === null) { continue; } $financeByStore[$platformKey . "\0" . $row['normalized_store_name']] = $row; } $now = now(); $rows = []; $resolved = 0; foreach ($missingAccounts as $account) { $platformKey = JushuitanShopBalanceRepository::platformKey( (string) $account->store_platform, ); if ($platformKey === null) { continue; } $names = array_values(array_unique(array_filter([ JushuitanShopBalanceRepository::normalizeStoreName((string) ($account->shop_name ?? '')), JushuitanShopBalanceRepository::normalizeStoreName((string) ($account->erp_store_name ?? '')), ]))); $candidates = []; foreach ($names as $normalizedStoreName) { $key = $platformKey . "\0" . $normalizedStoreName; if (isset($financeByStore[$key])) { $candidates[$key] = $financeByStore[$key]; } } if (count($candidates) !== 1) { continue; } $resolved++; $candidateKey = array_key_first($candidates); if (isset($rows[$candidateKey])) { continue; } $candidate = $candidates[$candidateKey]; $rows[$candidateKey] = [ 'balance_date' => $balanceDate, 'source_scope' => $sourceScope, 'data_source' => JushuitanShopBalanceRepository::DATA_SOURCE_FINANCE, 'source_platform' => $candidate['source_platform'], 'store_name' => $candidate['store_name'], 'normalized_store_name' => $candidate['normalized_store_name'], 'platform_account_id' => $candidate['platform_account_id'], 'balance' => round((float) $candidate['balance'], 6), 'source_row_key' => hash('sha256', 'finance' . "\0" . $sourceScope . "\0" . $candidateKey), 'source_file_name' => mb_substr((string) $candidate['source_file_name'], 0, 255), 'source_sha256' => hash( 'sha256', implode("\0", [$balanceDate, $sourceScope, $candidateKey, (string) $candidate['occurred_at']]), ), 'imported_at' => $now, 'created_at' => $now, 'updated_at' => $now, ]; } if (! $dryRun) { $rows = array_values($rows); $this->replaceFinanceRows($balanceDate, $sourceScope, $rows); } return $this->result( $sourceScope, true, null, $missingAccounts->count(), $resolved, $dryRun, count($rows), ); } /** @return list */ private function platformKeysForScope(string $sourceScope): array { if ($sourceScope === 'all') { return array_values(array_unique(array_merge(...array_values(self::SCOPE_PLATFORM_KEYS)))); } return self::SCOPE_PLATFORM_KEYS[$sourceScope] ?? []; } /** @return list */ private function providerScopes(string $sourceScope): array { return match ($sourceScope) { 'all' => [ FinanceBillBalanceRepository::SCOPE_TAOBAO, FinanceBillBalanceRepository::SCOPE_JD_WALLET, ], FinanceBillBalanceRepository::SCOPE_TAOBAO, FinanceBillBalanceRepository::SCOPE_JD_WALLET => [$sourceScope], default => [], }; } /** @param list> $rows */ private function replaceFinanceRows(string $balanceDate, string $sourceScope, array $rows): void { $this->connection->transaction(function () use ($balanceDate, $sourceScope, $rows): void { $this->connection->table(JushuitanShopBalanceRepository::TABLE) ->where('balance_date', $balanceDate) ->where('source_scope', $sourceScope) ->where('data_source', JushuitanShopBalanceRepository::DATA_SOURCE_FINANCE) ->delete(); foreach (array_chunk($rows, 500) as $chunk) { $this->connection->table(JushuitanShopBalanceRepository::TABLE)->insert($chunk); } }); } /** @return array{source_scope:string,supported:bool,reason:?string,missing:int,resolved:int,unresolved:int,filled:int,dry_run:bool} */ private function result( string $sourceScope, bool $supported, ?string $reason, int $missing, int $resolved, bool $dryRun, ?int $filled = null, ): array { return [ 'source_scope' => $sourceScope, 'supported' => $supported, 'reason' => $reason, 'missing' => $missing, 'resolved' => $resolved, 'unresolved' => max(0, $missing - $resolved), 'filled' => $filled ?? 0, 'dry_run' => $dryRun, ]; } }