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

如何在React+Firestore中防止并发选课并处理名额可用性

选课系统并发名额控制问题解决

问题背景

开发支持用户选修课程的Web应用,每门选修课设有固定名额限制,但遇到并发场景下的名额管控失效问题:

  • 仅剩1个名额时,多名用户可同时完成选课
  • 无剩余名额时,用户仍能成功选课

当前选课处理代码:

const handleApprove = async () => {
  if (isEnrolled || isConfirming) {
    return; 
  }

  setIsConfirming(true);

  const shouldEnroll = window.confirm("Are you sure you want to enroll?");

  setIsConfirming(false);

  if (shouldEnroll) {
    try {
      const firestore = firebase.firestore();
      const electiveRef = firestore.collection("electives").doc(project.id);
      const userRef = firestore.collection("users").doc(user.uid);

      await firestore.runTransaction(async (transaction) => {
        const electiveDoc = await transaction.get(electiveRef);
        if (!electiveDoc.exists) {
          throw new Error("Elective document does not exist!");
        }

        const electiveData = electiveDoc.data();
        const updatedSlots = electiveData.slots - 1;

        if (updatedSlots >= 0) {
         
          const updatedElectiveDoc = await transaction.get(electiveRef);
          const updatedElectiveData = updatedElectiveDoc.data();
          const remainingSlots = updatedElectiveData.slots;

          if (remainingSlots >= 0) {
            
            transaction.update(electiveRef, { slots: updatedSlots });

  
            await userRef.update({
              isEnroll: true,
              elective: project.name,
              subjectCode: project.details,
            });
            
            logout();
            setIsEnrolled(true);
          } else {
            alert("No available slots for enrollment in this elective.");
            setIsEnrolled(false); // Reset isEnrolled if enrollment fails
          }
        } else {
          alert("No available slots for enrollment.");
          setIsEnrolled(false); // Reset isEnrolled if there are no available slots
        }
      });
    } catch (error) {
      console.error("Error enrolling:", error);
      alert("An error occurred while enrolling. Please try again.");
      setIsEnrolled(false); // Reset isEnrolled if enrollment fails
    }
  }
};

核心需求

  • 并发操作场景下,仅剩1个名额时仅允许一名用户完成选课
  • 无剩余名额时彻底阻止用户选课操作

问题分析

原代码虽使用Firestore事务,但存在多个逻辑错误导致失效:

  1. 事务内重复获取课程文档,属于冗余操作且干扰判断逻辑
  2. 名额判断顺序错误:先扣减名额再检查是否合法,应先确认剩余名额>0再执行扣减
  3. 用户数据更新未通过事务管控,会出现事务回滚但用户状态已修改的不一致情况
  4. 事务内部执行alert、setState等UI操作,事务重试时会重复触发这些行为

解决方案

修正后的代码如下:

const handleApprove = async () => {
  if (isEnrolled || isConfirming) {
    return; 
  }

  setIsConfirming(true);
  const shouldEnroll = window.confirm("Are you sure you want to enroll?");
  setIsConfirming(false);

  if (!shouldEnroll) return;

  try {
    const firestore = firebase.firestore();
    const electiveRef = firestore.collection("electives").doc(project.id);
    const userRef = firestore.collection("users").doc(user.uid);

    await firestore.runTransaction(async (transaction) => {
      // 获取课程最新数据
      const electiveDoc = await transaction.get(electiveRef);
      if (!electiveDoc.exists) {
        throw new Error("Elective document does not exist!");
      }

      const electiveData = electiveDoc.data();
      const currentSlots = electiveData.slots;

      // 先检查是否还有可用名额
      if (currentSlots <= 0) {
        throw new Error("No available slots for enrollment.");
      }

      // 原子性更新课程名额
      transaction.update(electiveRef, { slots: currentSlots - 1 });
      // 通过事务更新用户选课状态,确保数据一致性
      transaction.update(userRef, {
        isEnroll: true,
        elective: project.name,
        subjectCode: project.details,
      });
    });

    // 事务成功后再执行UI更新和后续操作
    logout();
    setIsEnrolled(true);
    alert("Enrollment successful!");
  } catch (error) {
    console.error("Error enrolling:", error);
    // 根据错误类型给出精准提示
    const errorMsg = error.message.includes("No available slots") 
      ? "No available slots for enrollment." 
      : "An error occurred while enrolling. Please try again.";
    alert(errorMsg);
    setIsEnrolled(false);
  }
};

关键优化点

  • 简化事务逻辑:移除重复的文档获取操作,仅在事务初始化时获取一次课程数据
  • 修正名额判断逻辑:先验证当前剩余名额是否合法,再执行扣减操作,避免出现负数名额
  • 统一事务管控:用户数据更新通过transaction.update执行,确保课程名额与用户状态修改的原子性
  • 分离事务与UI操作:将弹窗、状态更新、退出登录等操作移至事务成功后执行,避免事务重试时重复触发
  • 精准错误处理:根据错误信息分类给出提示,提升用户体验

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 19:03:21