PAYATHON 2026

Laravel 与 MySQL 按月显示用户支付状态

支付小周

结论

不要用 users.updated_at 判断支付月份。updated_at 记录的是用户资料最后一次更新的时间,不能说明付款发生在哪个月。

应按以下方式处理:

  1. 在 payments 表中保存支付日期,例如 paid_at 或 payment_date。
  2. 通过 users.id = payments.user_id 关联两张表。
  3. 根据管理员选择的年月,查询当月第一天至下月第一天之间的支付记录。
  4. 如果需要列出所有用户,包括当月未付款的用户,应使用 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 即可,也能避免为每名用户、每个月生成大量没有实际支付内容的数据。

备注:内容仅供参考。