Laravel 与 MySQL 按月显示用户支付状态
结论
不要用 users.updated_at 判断支付月份。updated_at 记录的是用户资料最后一次更新的时间,不能说明付款发生在哪个月。
应按以下方式处理:
- 在
payments表中保存支付日期,例如paid_at或payment_date。 - 通过
users.id = payments.user_id关联两张表。 - 根据管理员选择的年月,查询当月第一天至下月第一天之间的支付记录。
- 如果需要列出所有用户,包括当月未付款的用户,应使用
LEFT JOIN,并把日期条件放在关联条件中。
推荐的数据结构
假设 payments 表包含以下字段:
id
user_id
payment_status
payment
paid_at
created_at
updated_at
各字段的用途如下:
payment:支付金额。payment_status:支付状态,例如Done、Pending。paid_at:实际支付时间。
如果暂时没有 paid_at 字段,也可以用 payments.created_at 筛选,但需要确认记录创建时间就是业务上的支付时间。
Laravel Query Builder 写法
管理员提交的月份可以采用 YYYY-MM 格式,例如:
2019-08
查询代码如下:
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
$month = $request->input('month'); // 例如 2019-08
$start = Carbon::createFromFormat('Y-m', $month)->startOfMonth();
$end = $start->copy()->addMonth();
$users = DB::table('users')
->leftJoin('payments', function ($join) use ($start, $end) {
$join->on('users.id', '=', 'payments.user_id')
->where('payments.paid_at', '>=', $start)
->where('payments.paid_at', '<', $end);
})
->select([
'users.id',
'users.name',
'payments.payment_status',
'payments.payment',
'payments.paid_at',
])
->orderBy('users.name')
->get();
这里采用左闭右开的时间范围:
paid_at >= 当月第一天
paid_at < 下月第一天
这种写法适用于日期时间字段,也不会漏掉当月最后一天带有时分秒的记录。
在视图中,没有支付记录的用户可以显示默认状态:
@foreach ($users as $user)
<tr>
<td>{{ $user->name }}</td>
<td>{{ $user->payment_status ?? 'Not Done' }}</td>
<td>
{{ $user->paid_at
? \Carbon\Carbon::parse($user->paid_at)->format('d-m-Y')
: '-' }}
</td>
</tr>
@endforeach
对应的 MySQL 查询
以 2019 年 8 月为例:
SELECT
users.id,
users.name,
payments.payment_status,
payments.payment,
payments.paid_at
FROM users
LEFT JOIN payments
ON payments.user_id = users.id
AND payments.paid_at >= '2019-08-01 00:00:00'
AND payments.paid_at < '2019-09-01 00:00:00'
ORDER BY users.name;
日期条件必须放在 LEFT JOIN ... ON 中。如果写成:
WHERE payments.paid_at >= '2019-08-01'
当月没有支付记录的用户会被过滤,查询效果接近 INNER JOIN,因此无法列出状态为 Not Done 的用户。
使用 Eloquent 关联
先在模型中定义关联:
// User.php
public function payments()
{
return $this->hasMany(Payment::class);
}
// Payment.php
public function user()
{
return $this->belongsTo(User::class);
}
然后按月份加载支付记录:
use App\Models\User;
use Carbon\Carbon;
$month = $request->input('month');
$start = Carbon::createFromFormat('Y-m', $month)->startOfMonth();
$end = $start->copy()->addMonth();
$users = User::with([
'payments' => function ($query) use ($start, $end) {
$query->where('paid_at', '>=', $start)
->where('paid_at', '<', $end);
}
])->get();
在 Blade 中读取当月支付记录:
@foreach ($users as $user)
@php
$payment = $user->payments->first();
@endphp
<tr>
<td>{{ $user->name }}</td>
<td>{{ $payment?->payment_status ?? 'Not Done' }}</td>
<td>{{ $payment?->paid_at?->format('d-m-Y') ?? '-' }}</td>
</tr>
@endforeach
如果需要格式化日期,建议在 Payment 模型中添加类型转换:
protected $casts = [
'paid_at' => 'datetime',
];
月份输入校验
不要直接使用 dropdown 提交的值,应先校验格式:
$request->validate([
'month' => ['required', 'date_format:Y-m'],
]);
HTML 可以使用月份输入控件:
<input type="month" name="month" required>
需要注意的情况
如果同一用户在一个月内可能有多条支付记录,上面的查询会为该用户返回多行。需要先确定具体的业务规则:
- 显示当月所有支付记录;
- 只显示最新一笔;
- 汇总当月支付金额;
- 根据多笔记录计算最终状态。
如果业务规定每名用户每月只能有一条支付记录,建议在 payments 表中增加账期字段,例如 billing_month,并建立唯一约束:
ALTER TABLE payments
ADD COLUMN billing_month DATE NOT NULL,
ADD UNIQUE KEY payments_user_month_unique (user_id, billing_month);
还可以为常用查询字段建立索引:
CREATE INDEX payments_user_paid_at_index
ON payments (user_id, paid_at);
通常不需要提前为每名用户插入一条状态为 Not Done 的静态记录。使用 LEFT JOIN 后,如果查不到该用户当月的付款记录,直接在展示层显示 Not Done 即可,也能避免为每名用户、每个月生成大量没有实际支付内容的数据。
备注:内容仅供参考。
