connection(); $openingDate = (string) config('finance_ledger.opening_date', '2026-10-17'); $now = now(); $opening = $connection->table('fin_account_opening') ->orderBy('opening_date') ->orderBy('id') ->get() ->groupBy('account_short') ->map(static fn ($rows) => $rows->last()); $accountCompanies = $connection->table('fin_accounts') ->select(['account_short', 'company_name']) ->whereNotNull('account_short') ->where('account_short', '<>', '') ->orderBy('id') ->get() ->groupBy('account_short') ->map(static fn ($rows) => (string) ($rows->first()->company_name ?? '')); $mapping = $connection->table('fin_account_map') ->whereNotNull('account_short') ->where('account_short', '<>', '') ->get(['source', 'source_value', 'account_short']) ->keyBy(static fn ($row): string => $row->source . "\0" . $row->source_value); $transactions = $connection->table('fin_transactions') ->where('txn_date', '>=', $openingDate) ->orderBy('txn_date') ->orderBy('id') ->get(['txn_date', 'account_short', 'source', 'source_value', 'income', 'expense']); $dailyByAccount = []; $balances = []; foreach ($transactions as $transaction) { $mapKey = $transaction->source . "\0" . $transaction->source_value; $account = (string) ($mapping->get($mapKey)->account_short ?? ''); if ($account === '') { $account = trim((string) $transaction->account_short); } if ($account === '' || ! $accountCompanies->has($account)) { continue; } $date = (string) $transaction->txn_date; $dailyByAccount[$account][$date] ??= [ 'flow_date' => $date, 'inflow' => 0.0, 'outflow' => 0.0, ]; $dailyByAccount[$account][$date]['inflow'] += (float) $transaction->income; $dailyByAccount[$account][$date]['outflow'] += (float) $transaction->expense; } $flows = []; $latest = []; foreach ($accountCompanies as $account => $company) { $account = (string) $account; $openingRow = $opening->get($account); $balance = $openingRow === null ? 0.0 : (float) $openingRow->opening_balance; $start = $openingRow === null ? $openingDate : max((string) $openingRow->opening_date, $openingDate); $rows = array_values($dailyByAccount[$account] ?? []); usort($rows, static fn (array $a, array $b): int => strcmp($a['flow_date'], $b['flow_date'])); $openingFlow = [ 'flow_date' => $start, 'company_name' => $company, 'account_short' => $account, 'opening_balance' => round($balance, 2), 'inflow' => 0.0, 'outflow' => 0.0, 'closing_balance' => round($balance, 2), 'remark' => '期初', 'created_at' => $now, 'updated_at' => $now, ]; $flows[] = $openingFlow; $openingIndex = array_key_last($flows); foreach ($rows as $row) { if ($row['flow_date'] < $start) { continue; } $inflow = round((float) $row['inflow'], 2); $outflow = round((float) $row['outflow'], 2); if (abs($inflow) < 0.005 && abs($outflow) < 0.005) { continue; } if ($row['flow_date'] === $start) { // 期初日发生动账时合并到同一行,避免唯一键冲突。 $balance = round($balance + $inflow - $outflow, 2); $flows[$openingIndex]['inflow'] = $inflow; $flows[$openingIndex]['outflow'] = $outflow; $flows[$openingIndex]['closing_balance'] = $balance; $flows[$openingIndex]['remark'] = null; continue; } $balance = round($balance + $inflow - $outflow, 2); $flows[] = [ 'flow_date' => $row['flow_date'], 'company_name' => $company, 'account_short' => $account, 'opening_balance' => round($balance - $inflow + $outflow, 2), 'inflow' => $inflow, 'outflow' => $outflow, 'closing_balance' => $balance, 'remark' => null, 'created_at' => $now, 'updated_at' => $now, ]; } $latest[$account] = $balance; } $connection->transaction(function () use ($connection, $flows, $latest, $now): void { $connection->table('fin_daily_flows')->delete(); foreach (array_chunk($flows, 500) as $chunk) { $connection->table('fin_daily_flows')->insert($chunk); } $this->updateBalances( $connection, 'fin_accounts', 'account_short', 'current_balance', $latest, $now, ); $this->refreshAccountAmounts(); $companies = $connection->table('fin_accounts') ->selectRaw('company_name, SUM(current_balance) AS balance') ->groupBy('company_name') ->pluck('balance', 'company_name') ->all(); $this->updateBalances( $connection, 'fin_shops', 'company_name', 'shop_balance', $companies, $now, ); }); return [ 'accounts' => count($latest), 'flows' => count($flows), 'opening_date' => $openingDate, 'balance_sum' => round(array_sum($latest), 2), ]; } /** * 按账户状态回填可用金额和不可用金额。 * * 正常账户:可用金额 = 当前余额,不可用金额 = 0; * 异常账户:可用金额 = 0,不可用金额 = 当前余额。 */ public function refreshAccountAmounts(?string $accountShort = null): int { $connection = $this->connection(); if ($accountShort !== null) { $status = $connection->table('fin_account_statuses') ->where('account_short', $accountShort) ->value('status'); $normal = $status === null || trim((string) $status) === '' || $status === FinanceLedgerAccountStatus::NORMAL; return $connection->table('fin_accounts') ->where('account_short', $accountShort) ->update([ 'daily_available' => $normal ? $connection->raw('current_balance') : 0, 'blocked_amount' => $normal ? 0 : $connection->raw('current_balance'), 'updated_at' => now(), ]); } $abnormalAccounts = $connection->table('fin_account_statuses') ->where('status', '<>', FinanceLedgerAccountStatus::NORMAL) ->pluck('account_short') ->filter() ->unique() ->values() ->all(); $normalCount = $connection->table('fin_accounts')->update([ 'daily_available' => $connection->raw('current_balance'), 'blocked_amount' => 0, 'updated_at' => now(), ]); if ($abnormalAccounts === []) { return $normalCount; } return $normalCount + $connection->table('fin_accounts') ->whereIn('account_short', $abnormalAccounts) ->update([ 'daily_available' => 0, 'blocked_amount' => $connection->raw('current_balance'), 'updated_at' => now(), ]); } private function connection() { return DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); } private function updateBalances( $connection, string $table, string $keyColumn, string $balanceColumn, array $balances, $now, ): void { foreach (array_chunk($balances, 500, true) as $chunk) { if ($chunk === []) { continue; } $caseClauses = []; foreach ($chunk as $key => $balance) { $caseClauses[] = 'WHEN ' . $connection->getPdo()->quote((string) $key) . ' THEN ' . number_format(round((float) $balance, 2), 2, '.', ''); } $connection->table($table) ->whereIn($keyColumn, array_keys($chunk)) ->update([ $balanceColumn => $connection->raw( "CASE `{$keyColumn}` " . implode(' ', $caseClauses) . " ELSE `{$balanceColumn}` END" ), 'updated_at' => $now, ]); } } }