
在 Laravel 控制器中批量操作多个模型时,需确保所有写入操作要么全部成功、要么全部回滚,避免数据不一致;通过 DB::beginTransaction()、commit() 和 rollback() 可实现强一致性事务控制。
在 laravel 控制器中批量操作多个模型时,需确保所有写入操作要么全部成功、要么全部回滚,避免数据不一致;通过 db::begintransaction()、commit() 和 rollback() 可实现强一致性事务控制。
当一个业务逻辑涉及多个模型(如 Loan、AssignLoan、TransactionReport 等)的创建或更新时,若中间某一步失败(例如验证异常、外键约束冲突、字段超长、SMS 接口调用前 DB 已出错等),而此前的模型已成功保存,就会导致数据状态不一致——这正是你当前代码面临的核心问题。
Laravel 提供了原生的数据库事务支持,可完美解决该场景。关键在于:将所有 Eloquent 操作包裹在事务块中,并统一捕获异常触发回滚。
以下是重构后的 store 方法示例(已整合事务、优化可读性并增强健壮性):
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
public function store(Request $request)
{
$this->authorize('loans.create');
$validated = $this->validate($request, [
'loan_id' => 'required|numeric',
'received_by' => 'required|string|max:100',
'received_amount' => 'required|numeric|min:0',
'deposit' => 'nullable|numeric|min:0',
'deposit_1' => 'nullable|numeric|min:0',
'installments' => 'required|numeric|min:1',
'date' => 'required|date',
'total_amount' => 'required|numeric|min:0',
]);
try {
DB::beginTransaction();
// 1. 创建 Loan 记录
$loan = Loan::create([
'loan_id' => $validated['loan_id'],
'received_by' => $validated['received_by'],
'received_amount' => $validated['received_amount'],
'deposit' => $validated['deposit'] ?? 0,
'deposit_1' => $validated['deposit_1'] ?? 0,
'installments' => $validated['installments'],
'date' => $validated['date'],
'Txn' => Str::random(10),
'total_amount' => $validated['total_amount'],
'somity_id' => auth()->user()->mst_somity_id,
'is_active' => 1,
]);
// 2. 更新 AssignLoan(需确保存在)
$assignLoan = AssignLoan::findOrFail($validated['loan_id']);
$assignLoan->deposit += ($validated['deposit'] ?? 0);
$assignLoan->deposit_1 += ($validated['deposit_1'] ?? 0);
$assignLoan->paid_amount += $validated['received_amount'];
$assignLoan->remaining_amount = $assignLoan->total_amount - $validated['received_amount'];
$assignLoan->save();
// 3. 创建 TransactionReport
$customer_id = $assignLoan->customer_id ?? null;
if (!$customer_id) {
throw new \Exception('关联客户信息缺失,无法生成交易报告');
}
TransactionReport::create([
'transaction_type' => 'loan_received',
'customer_id' => $customer_id,
'transaction_description' => 'loan received with deposit',
'transaction_method' => 'cash',
'transaction_amount' => $validated['total_amount'],
'transaction_date' => $validated['date'],
'transaction_time' => now()->format('H:i:s'),
'transaction_by' => $validated['received_by'],
'somity_id' => auth()->user()->mst_somity_id,
'created_user_id' => auth()->id(),
'is_loan_diposite' => $loan->id,
'Txn' => $loan->Txn,
]);
// ✅ 所有操作成功 → 提交事务
DB::commit();
// 4. 发送短信(注意:此操作应在事务外执行,避免阻塞或引入非 DB 依赖)
$this->sendLoanSms($assignLoan, $validated);
$notification = [
'message' => '贷款记录添加成功',
'alert-type' => 'success'
];
return redirect()->route('loan.index')->with($notification);
} catch (\Exception $e) {
// ❌ 任一环节失败 → 回滚所有已执行的 DB 操作
DB::rollBack();
\Log::error('Loan store transaction failed', [
'exception' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
$notification = [
'message' => '操作失败:' . $e->getMessage(),
'alert-type' => 'error'
];
return back()->with($notification)->withInput();
}
}
// 抽离短信逻辑(事务外执行,更安全)
protected function sendLoanSms($assignLoan, $data)
{
$adminSettings = AdminSeetings::where('somity_id', auth()->user()->mst_somity_id)->first();
if (!$adminSettings || $adminSettings->automatic_sms != 1) return;
$smsPackage = SmsPackageAssignToSomity::where('somity_id', auth()->user()->mst_somity_id)->first();
if (!$smsPackage || $smsPackage->remaining_sms <= 0) return;
$customerPhone = $assignLoan->customer->customer_phone ?? null;
if (!$customerPhone) return;
$message = "您的贷款编号:{$assignLoan->loan_number},已收款 {$data['received_amount']} Tk,剩余应还 {$assignLoan->remaining_amount} Tk。";
// 扣减短信余额(此处也建议加锁或使用乐观锁防止并发超发)
$smsPackage->decrement('remaining_sms');
$smsPackage->increment('use_sms');
$smsPackage->save();
// 实际调用短信网关(建议改用队列异步处理)
$url = "https://bulksms.aloitltd.com/services/send.php?key=" . env('API_KEY')
. "&number={$customerPhone}&message=" . urlencode($message)
. "&option=" . env('OPTION') . "&type=" . env('TYPE') . "&prioritize=" . env('PRIORITIZE');
// 注意:此处不应直接 return Redirect::to($url),会中断流程;应使用 HTTP 客户端发起请求
// 可考虑使用 Guzzle 或 Laravel 的 Http Facade 异步调用
}✅ 关键改进说明:
- 使用
DB::beginTransaction()显式开启事务,确保所有save()/create()操作处于同一原子上下文; - 所有数据库操作必须在
try块内完成,一旦抛出异常(包括模型事件、验证、关联查询失败等),立即DB::rollBack(); - 将非数据库操作(如短信发送、日志记录、外部 API 调用)移至事务提交之后,避免因外部服务延迟或失败影响事务完整性;
- 使用
findOrFail()替代find()+ 手动判空,提升错误定位效率; - 对敏感字段(如手机号、金额)做空值/合法性校验,防止静默失败;
- 日志记录异常详情,便于生产环境排查;
- 推荐将短信等耗时操作移交队列(如
dispatch(new SendLoanSmsJob($loan))),进一步解耦与保障主流程性能与可靠性。
⚠️ 注意事项:
- 事务仅对支持事务的数据库驱动生效(如 MySQL InnoDB、PostgreSQL),MyISAM 不支持;
- Eloquent 模型事件(如
creating、saved)在事务内触发,但若事件中执行了未受控的 DB 操作,可能绕过事务,请谨慎设计; - 避免在事务中执行
sleep()、文件 IO、HTTP 请求等阻塞操作; - 若使用读写分离,事务默认在主库执行,从库不会同步事务状态。
通过以上方式,即可确保多模型操作的 ACID 特性,真正实现「全成功 or 全回滚」的业务一致性。


















