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');
});
}
具体流程是:
- 第一笔 Payment 的
fk_training为null。 - 创建第一次 Training 后,把 Training 的 ID 写入这笔 Payment。
- 录入第二笔 Payment 时,新记录的
fk_training仍为null。 - 创建第二次 Training 时,程序会找到这笔新的 Payment。
- 每增加一笔尚未使用的 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 已正确配置,避免将请求中不应保存的字段写入数据库。
备注:内容仅供参考。
