PAYATHON 2026

Laravel 中如何在每次 Payment 后重置 Training 检查

支付老李

结论

不需要手动把某个”检查状态”重置为零。需要判断的是:

该学生是否还有一笔尚未用于 Training 的 Payment。

当前代码中的:

if (count($thisStudentsTrainings) >= 1)

统计了系统中的全部 Training。只要存在一条记录,后续请求就会被阻止。即使改为只统计当前学生,这个条件仍会让该学生只能参加一次培训。

更稳妥的做法是将每笔 Payment 与一次 Training 明确关联。创建 Training 前,查询尚未使用的 Payment;创建成功后,再将这笔 Payment 标记为已使用。

推荐的数据关系

假设 payments 表中已有 fk_training 字段,可以采用以下约定:

  • fk_training 为 null:这笔付款尚未兑换培训;
  • fk_training 有值:这笔付款已经对应某次培训。

如果还没有该字段,可以添加一个允许为空的外键:

Schema::table('payments', function (Blueprint $table) {
    $table->foreignId('fk_training')
        ->nullable()
        ->constrained('trainings')
        ->nullOnDelete();

    $table->unique('fk_training');
});

unique 约束可以避免同一笔 Training 被多笔 Payment 重复关联。是否需要这个约束,要根据现有业务模型和表结构决定。

Controller 实现

创建 Training 时,先查找该学生尚未使用,并且付款日期不晚于培训日期的 Payment:

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

public function store(Request $request)
{
    $validated = $request->validate([
        'date_seance'  => ['required', 'date'],
        'hour_start'   => ['required'],
        'hour_end'     => ['required', 'after:hour_start'],
        'fk_motorbike' => ['required'],
        'fk_former'    => ['required'],
        'fk_student'   => ['required'],
        'fk_typeseance'=> ['required'],
    ]);

    return DB::transaction(function () use ($validated) {
        $payment = Payment::where('fk_student', $validated['fk_student'])
            ->whereNull('fk_training')
            ->whereDate('date_payment', '<=', $validated['date_seance'])
            ->orderBy('date_payment')
            ->lockForUpdate()
            ->first();

        if (!$payment) {
            return redirect()
                ->route('trainings.index')
                ->with('error', 'No unused payment, no training!');
        }

        $training = Training::create($validated);

        $payment->fk_training = $training->id;
        $payment->save();

        return redirect()
            ->route('trainings.index')
            ->with('success', 'Add');
    });
}

具体流程是:

  1. 第一笔 Payment 的 fk_training 为 null。
  2. 创建第一次 Training 后,把 Training 的 ID 写入这笔 Payment。
  3. 录入第二笔 Payment 时,新记录的 fk_training 仍为 null。
  4. 创建第二次 Training 时,程序会找到这笔新的 Payment。
  5. 每增加一笔尚未使用的 Payment,就可以再创建一次 Training。

事务和 lockForUpdate() 可以避免两个并发请求同时使用同一笔 Payment。

当前查询存在的问题

原代码没有按学生过滤 Training:

$thisStudentsTrainings = Training::with([
    'payments' => function ($query) use ($fk_student) {
        $query->where('fk_student', $fk_student);
    }
])->get();

with() 只会限制预加载的 payments,不会限制外层的 Training 查询,所以 get() 取到的仍然是全部 Training。

如果只是要查询某个学生的 Training,应直接过滤外层查询:

$thisStudentsTrainings = Training::where('fk_student', $fk_student)->get();

但只修正查询还不够,因为下面的条件仍然表示:“只要学生已经有一次 Training,以后就不能再添加。”

if ($thisStudentsTrainings->count() >= 1)

因此,判断条件应从”是否已有 Training”改为”是否有尚未使用的 Payment”。

下面这个变量虽然已经计算,但目前没有参与任何判断:

$hasPaidForThisTraining = $thisStudentsTrainings
    ->contains('id', $request->get('fk_training'));

创建 Training 的请求中通常也不会预先存在 fk_training,因为 Training 的 ID 要等记录插入数据库后才会生成。

另一种简单实现

如果不需要记录 Payment 与具体 Training 的对应关系,也可以直接比较付款次数和培训次数:

$paymentCount = Payment::where('fk_student', $fk_student)
    ->whereDate('date_payment', '<=', $date_seance)
    ->count();

$trainingCount = Training::where('fk_student', $fk_student)
    ->count();

if ($trainingCount >= $paymentCount) {
    return redirect()
        ->route('trainings.index')
        ->with('error', 'No unused payment, no training!');
}

Training::create($validated);

这种写法也能实现”一笔 Payment 对应一次 Training”,但无法确定每次 Training 具体使用了哪笔 Payment。遇到删除、退款、补录或并发提交时,也更容易产生数据不一致。因此,实际项目中更适合建立明确的关联。

注意事项

原来的日期条件:

->whereDate('date_payment', '>', $date_seance)

查找的是培训日期之后发生的付款,不能用来证明当前培训已有有效付款。通常应该查询培训当天或之前尚未使用的付款:

->whereDate('date_payment', '<=', $date_seance)

如果业务允许先培训后付款,可以去掉日期限制,或者根据实际规则调整。

另外,不建议直接使用:

Training::create($request->all());

应改为使用验证后的数据:

Training::create($validated);

同时要确认 Training 模型的 $fillable 已正确配置,避免将请求中不应保存的字段写入数据库。

备注:内容仅供参考。