275 lines
10 KiB
PHP
275 lines
10 KiB
PHP
<?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,
|
||
]);
|
||
}
|
||
}
|
||
}
|