CaiWuXiTong/modules/EcommerceFinance/Services/FinanceLedgerCalculator.php

183 lines
7.0 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Services;
use Illuminate\Support\Facades\DB;
/**
* 按账户滚动计算每日资金流水,并回写账户当前余额、每日可出金额和店铺余额。
*
* 计算口径固定在服务端,页面只读结果:
* 期末 = 期初 + 当日入账 - 当日出账
* 账户当前余额 = 最新一条流水期末
* 每日可出金额 = 当前余额 - 卡住金额
* 店铺余额 = 店铺所属公司名下账户当前余额合计(待财务确认)
*/
class FinanceLedgerCalculator
{
public function recalculate(): array
{
$connection = $this->connection();
$openingDate = (string) config('finance_ledger.opening_date', '2026-10-17');
$now = now();
$opening = $connection->table('fin_account_opening')
->where('opening_date', '<=', $openingDate)
->orderBy('opening_date')
->get()
->groupBy('account_short')
->map(static fn ($rows) => $rows->last());
$accountCompanies = $connection->table('fin_accounts')
->select(['account_short', 'company_name'])
->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']);
$daily = [];
$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 === '' || ! $opening->has($account)) {
continue;
}
$date = (string) $transaction->txn_date;
$key = $account . "\0" . $date;
$daily[$key] ??= [
'account_short' => $account,
'flow_date' => $date,
'inflow' => 0.0,
'outflow' => 0.0,
];
$daily[$key]['inflow'] += (float) $transaction->income;
$daily[$key]['outflow'] += (float) $transaction->expense;
}
$flows = [];
$latest = [];
foreach ($opening as $account => $openingRow) {
$account = (string) $account;
$balance = (float) $openingRow->opening_balance;
$company = $accountCompanies->get($account, '');
$start = max((string) $openingRow->opening_date, $openingDate);
$rows = array_values(array_filter(
$daily,
static fn (array $row): bool => $row['account_short'] === $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);
}
foreach ($latest as $account => $balance) {
$connection->table('fin_accounts')
->where('account_short', $account)
->update([
'current_balance' => round((float) $balance, 2),
'daily_available' => $connection->raw('ROUND(current_balance - blocked_amount, 2)'),
'updated_at' => $now,
]);
}
$companies = $connection->table('fin_accounts')
->selectRaw('company_name, SUM(current_balance) AS balance')
->groupBy('company_name')
->pluck('balance', 'company_name');
foreach ($companies as $company => $balance) {
$connection->table('fin_shops')
->where('company_name', (string) $company)
->update(['shop_balance' => round((float) $balance, 2), 'updated_at' => $now]);
}
});
return [
'accounts' => count($latest),
'flows' => count($flows),
'opening_date' => $openingDate,
'balance_sum' => round(array_sum($latest), 2),
];
}
private function connection()
{
return DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
}
}