You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Laravel取消提现接口循环调用时increment方法重复执行问题

解决取消提现接口并发请求导致余额重复累加的问题

我开发了一个处理用户取消提现请求的功能,流程如下:

  • 检查用户是否存在该提现请求
  • 删除该提现请求
  • 将提现金额返还至用户余额

但通过AJAX循环调用该接口时,出现了余额被重复累加的问题。

原PHP代码

public function cancel_withdraw(Request $request){
    $validated = $request->validate([
        'withdraw_id' => 'required|numeric'
    ]);
    $withdraw=Withdraw::where('user_id',Auth::id())
    ->where('id',$request->withdraw_id)
    ->whereNull('status')
    ->first();

    $withdraw->delete();

    Wallet::where('user_id',Auth::id())->increment($withdraw->coin,$withdraw->amount);
    return response()->json(['message'=>'Successfully Deleted!','errors' => ['']]);                        
}

原JS代码

$(document).ready(function() {
        let i = 0;
        while (i < 100) {
        $.ajax({
            url: "{{ url('cancel_withdraw') }}",
            type:'POST',
            dataType: "json",
            data: "_token={{ csrf_token() }}&withdraw_id="+withdraw_id,
            success: function(data) {},
            error: function(data){}
        });
        e.preventDefault();
        i++;
        } 
});

问题原因

  1. 并发竞态冲突:循环发起的100次请求几乎同时到达服务器,第一个请求还没执行删除操作时,其他请求已经查询到了这条未处理的提现记录,导致多次触发余额累加。
  2. 操作非原子性:查询、删除、余额更新三步是独立执行的,没有加锁或事务控制,无法保证操作的唯一性。
  3. 接口无幂等性:同一个取消请求被多次执行时,会产生重复的副作用(余额重复增加)。

解决方案

方案一:用状态标记+事务控制原子操作

不直接删除提现记录,先标记为已取消,再执行余额累加,同时用数据库事务保证操作的原子性,避免并发冲突:

public function cancel_withdraw(Request $request){
    $validated = $request->validate([
        'withdraw_id' => 'required|numeric'
    ]);

    DB::beginTransaction();
    try {
        // 先更新状态锁定记录,避免并发查询
        $updated = Withdraw::where('user_id', Auth::id())
            ->where('id', $request->withdraw_id)
            ->whereNull('status')
            ->update(['status' => 'cancelled']);

        if (!$updated) {
            DB::rollBack();
            return response()->json(['message' => '提现请求不存在或已处理', 'errors' => ['']]);
        }

        // 获取更新后的提现记录
        $withdraw = Withdraw::find($request->withdraw_id);
        // 累加余额
        Wallet::where('user_id', Auth::id())->increment($withdraw->coin, $withdraw->amount);

        DB::commit();
        return response()->json(['message'=>'取消提现成功!','errors' => ['']]);
    } catch (\Exception $e) {
        DB::rollBack();
        return response()->json(['message'=>'操作失败','errors' => [$e->getMessage()]]);
    }
}

方案二:用悲观锁锁定提现记录

查询时给记录加行级锁,确保同一时间只有一个请求能处理这条记录:

public function cancel_withdraw(Request $request){
    $validated = $request->validate([
        'withdraw_id' => 'required|numeric'
    ]);

    DB::beginTransaction();
    try {
        // 加行级锁,防止并发请求同时读取记录
        $withdraw = Withdraw::where('user_id', Auth::id())
            ->where('id', $request->withdraw_id)
            ->whereNull('status')
            ->lockForUpdate()
            ->first();

        if (!$withdraw) {
            DB::rollBack();
            return response()->json(['message' => '提现请求不存在或已处理', 'errors' => ['']]);
        }

        $withdraw->status = 'cancelled';
        $withdraw->save();
        // 若一定要删除,可替换为 $withdraw->delete(); 但建议保留记录方便对账

        Wallet::where('user_id', Auth::id())->increment($withdraw->coin, $withdraw->amount);

        DB::commit();
        return response()->json(['message'=>'取消提现成功!','errors' => ['']]);
    } catch (\Exception $e) {
        DB::rollBack();
        return response()->json(['message'=>'操作失败','errors' => [$e->getMessage()]]);
    }
}

前端优化:避免重复请求

前端循环发起100次请求本身不符合业务逻辑,应该限制用户只能发起一次请求:

$(document).ready(function() {
    let isProcessing = false;
    // 假设是点击按钮触发取消操作
    $('#cancelWithdrawBtn').on('click', function(e) {
        e.preventDefault();
        // 正在处理时禁止再次发起请求
        if (isProcessing) return;
        isProcessing = true;

        $.ajax({
            url: "{{ url('cancel_withdraw') }}",
            type:'POST',
            dataType: "json",
            data: {
                _token: "{{ csrf_token() }}",
                withdraw_id: withdraw_id
            },
            success: function(data) {
                // 处理成功逻辑,比如提示用户或刷新页面
            },
            error: function(data){
                // 处理错误逻辑,比如提示用户操作失败
            },
            complete: function() {
                // 请求完成后解锁
                isProcessing = false;
            }
        });
    });
});

内容的提问来源于stack exchange,提问作者Arlong

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.17 18:30:41