Laravel invoice 和 Payment 页面如何计算 Balance?
支付老李
结论
累计余额要按时间正序计算,计算完成后再按 created_at DESC 倒序展示。
计算规则如下:
Invoice:balance += amount
Payment:balance -= amount
不能在当前的倒序循环中直接执行 $balance += $total。这种写法没有区分收款和账单的正负方向,还会从最新记录开始向历史记录反向累计。
推荐做法:在控制器中计算余额
先加载每笔 billing 的明细,按时间从旧到新计算累计余额:
$billings = $student->billings()
->with('billingRecords')
->get();
$balance = 0;
$billings = $billings
->sortBy('created_at')
->map(function ($billing) use (&$balance) {
$total = $billing->billingRecords->sum('amount');
if ($billing->type === 'Payment') {
$balance -= $total;
} elseif ($billing->type === 'Invoice') {
$balance += $total;
}
$billing->total = $total;
$billing->running_balance = $balance;
return $billing;
})
->sortByDesc('created_at')
->values();
return view('students.billings', compact('student', 'billings'));
整个过程分为两步:
- 使用
sortBy('created_at')按时间从旧到新计算余额。 - 计算完成后使用
sortByDesc('created_at'),按页面需要倒序展示。
Blade 只负责输出已经算好的结果:
<table>
<thead>
<tr>
<th>Date</th>
<th>Type</th>
<th>Amount</th>
<th>Balance</th>
</tr>
</thead>
<tbody>
@foreach ($billings as $billing)
<tr id="{{ $billing->id }}">
<td>
{{ optional($billing->created_at)->format('M d, Y') }}
{{ optional($billing->created_at)->format('h:i A') }}
</td>
<td>{{ $billing->type }}</td>
<td>
@if ($billing->type === 'Payment')
-${{ number_format($billing->total, 2) }}
@elseif ($billing->type === 'Invoice')
${{ number_format($billing->total, 2) }}
@endif
</td>
<td>
${{ number_format($billing->running_balance, 2) }}
</td>
</tr>
@endforeach
</tbody>
</table>
假设最早的一笔记录是金额为 $30 的 Payment,下一笔是金额为 $100 的 Invoice,计算过程如下:
0 - 30 = -30
-30 + 100 = 70
所以,即使页面按倒序显示,这两笔记录对应的余额仍然分别是 $-30 和 $70。
如果必须在 Blade 中计算
也可以先按时间正序遍历集合,将每笔余额按 billing ID 保存,再按原来的倒序输出:
@php
$balance = 0;
$balances = [];
$totals = [];
foreach ($student->billings->sortBy('created_at') as $billing) {
$total = $billing->billingRecords->sum('amount');
$totals[$billing->id] = $total;
if ($billing->type === 'Payment') {
$balance -= $total;
} elseif ($billing->type === 'Invoice') {
$balance += $total;
}
$balances[$billing->id] = $balance;
}
@endphp
@foreach ($student->billings->sortByDesc('created_at') as $billing)
@php
$total = $totals[$billing->id];
@endphp
<tr id="{{ $billing->id }}">
<td>
{{ optional($billing->created_at)->format('M d, Y') }}
{{ optional($billing->created_at)->format('h:i A') }}
</td>
<td>{{ $billing->type }}</td>
<td>
@if ($billing->type === 'Payment')
-${{ number_format($total, 2) }}
@else
${{ number_format($total, 2) }}
@endif
</td>
<td>
${{ number_format($balances[$billing->id], 2) }}
</td>
</tr>
@endforeach
这种写法可以正常工作,不过把业务计算放在控制器或服务类中,通常更便于测试和维护。
注意事项
预加载 billingRecords 可以避免在循环中产生 N+1 查询:
$student->load('billings.billingRecords');
如果多条记录的 created_at 完全相同,还需要增加一个稳定的第二排序条件,否则累计顺序可能不固定:
public function billings()
{
return $this->hasMany(Billing::class)
->orderByDesc('created_at')
->orderByDesc('id');
}
计算余额时也要使用对应的正序规则:
$student->billings()
->with('billingRecords')
->orderBy('created_at')
->orderBy('id')
->get();
金额字段不宜使用二进制浮点数进行财务计算。数据库字段可以使用 DECIMAL,也可以统一按最小货币单位(例如“分”)保存和计算,以免累计时出现精度误差。
备注:内容仅供参考。
