CaiWuXiTong/modules/EcommerceFinance/Services/FinanceLedgerCalculator.php

275 lines
10 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Modules\EcommerceFinance\Services;
use App\Utilities\FinanceLedgerAccountStatus;
use Illuminate\Support\Facades\DB;
/**
* 按账户滚动计算每日资金流水,并回写账户当前余额、每日可出金额和店铺余额。
*
* 计算口径固定在服务端,页面只读结果:
* 期末 = 期初 + 当日入账 - 当日出账
* 账户当前余额 = 最新一条流水期末
* 正常账户:可用金额 = 当前余额,不可用金额 = 0
* 异常账户:可用金额 = 0,不可用金额 = 当前余额
* 店铺余额 = 店铺所属公司名下账户当前余额合计(待财务确认)
*/
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')
->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,
]);
}
}
}