后端:使用 Redis 分布式锁

在处理表单提交的控制器方法中,可以使用 Cache::lock 方法来实现一个简单的锁机制。下面是一个示例代码:

use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;

public function submitOrder(Request $request)
{
    // 锁的键名,这里可以根据用户ID和场景自定义,确保唯一性
    $lockKey = 'order_submit_lock_' . auth()->id();

    // 尝试获取锁,$seconds是锁的有效期,这里设置为5秒
    $lock = Cache::lock($lockKey, 5);

    // 如果无法获取锁,说明有其他请求正在处理,直接返回错误
    if (!$lock->get()) {
        return response()->json(['message' => '请勿重复提交订单'], 429);
    }

    try {
        // 在这里执行你的业务逻辑,比如保存订单到数据库
        // ...

        // 业务逻辑处理成功后,可以手动释放锁,但在 Laravel 中通常不需要显式释放,
        // 因为锁会自动在给定的有效期后释放,或者在脚本结束时(如果设置了自动释放)
    } catch (\Exception $e) {
        // 记录异常,根据需要处理
        Log::error('提交订单时发生错误: ' . $e->getMessage());

        // 如果在 catch 块内,确保抛出异常前解锁,以防锁未被正确释放
        $lock->release();
        throw $e; // 再次抛出异常,让全局异常处理器处理
    }

    // 成功响应
    return response()->json(['message' => '订单提交成功'], 200);
}

注意事项

  • 上述示例中,Cache::lock 会尝试获取一个分布式锁。如果锁已经被持有,则该方法会立即返回 false,避免了重复处理。

  • 锁的有效期(上述示例中的5秒)应该根据你的业务需求调整,确保它足够长以覆盖正常的处理时间,但也不要过长,以免长时间阻塞其他请求。

  • Laravel 的 Redis 锁实现了自动释放机制,一般情况下不需要手动调用 release() 方法,除非你在特定的异常处理流程中需要提前释放锁。

  • 当使用锁时,要考虑死锁的可能性,确保锁的有效期合理,并且在逻辑处理完成后能正常结束,避免锁永远不被释放的情况。

前端:(vue)

在Vue组件中,当表单提交按钮被点击时,立即将按钮设置为disabled状态,防止再次点击。

创建一个自定义Vue指令来全局处理按钮的重复点击问题,如以下示例:

Vue.directive('prevent-repeated-submit', {
  // 当绑定元素插入到DOM中时...
  inserted(el) {
    // 添加点击事件监听器
    el.addEventListener('click', () => {
      // 检查按钮是否已经被禁用,避免重复触发
      if (el.disabled) return;

      // 设置loading和disabled属性为true
      el.loading = true;
      el.disabled = true;
    });
  }
});

然后在模板中使用该指令:

<button ref="submitBtn" v-prevent-repeated-submit @click="submitForm">提交订单</button>

提交完成后需要恢复loadingdisabled状态

methods: {
  submitForm() {
    this.$refs.myForm.validate((valid) => {
      if (valid) {
        axios.post('/api/order', this.formData)
          .then(() => {
            // 操作成功的处理...
          })
          .finally(() => {
            // 无论成功或失败,在异步操作完成后重置按钮状态
            this.$nextTick(() => {
              this.$refs.submitBtn.loading = false;
              this.$refs.submitBtn.disabled = false;
            });
          });
      }
    });
  }
}

使用this.$nextTick确保在DOM更新完成后再修改loadingdisabled状态。