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

Firebase Cloud Functions更新分数后用户节点被删除问题求助

问题分析与解决方案

你遇到的问题根源在于错误使用了Firebase的Transaction API:你在单个用户的节点引用userRef上调用了transaction,但这个事务的回调返回了一个Promise——而Firebase Transaction期望回调返回的是要写入当前引用的具体值(或者undefined来中止事务)。这个返回的Promise会被当作无效值处理,最终导致你的用户节点被覆盖为null,也就是被删除了。

而且你的逻辑里根本不需要对单个用户节点做事务,我们需要保护的是父节点上averageScore和userCount的原子更新,避免并发修改时出现计算误差。

修正后的代码

exports.updateAverageScore = functions.database.ref('/users/{userId}/score')
  .onWrite((change, context) => {
    // 处理节点创建或删除的边界情况
    if (!change.before.exists() || !change.after.exists()) {
      // 新增用户的场景:同时更新userCount和averageScore
      if (!change.before.exists() && change.after.exists()) {
        const parentRef = change.after.ref.parent.parent;
        return parentRef.transaction((parentData) => {
          if (parentData) {
            const newCount = (parentData.userCount || 0) + 1;
            const newAverage = ((parentData.averageScore || 0) * (newCount - 1) + parseFloat(change.after.val().score)) / newCount;
            return {
              ...parentData,
              userCount: newCount,
              averageScore: parseFloat(newAverage.toFixed(2))
            };
          }
          // 父节点不存在时初始化数据
          return { userCount: 1, averageScore: parseFloat(change.after.val().score) };
        });
      }
      // 如果需要处理用户删除的场景,可以在这里补充对应的逻辑
      return null;
    }

    const beforeScore = parseFloat(change.before.val().score);
    const afterScore = parseFloat(change.after.val().score);
    const scoreDiff = afterScore - beforeScore;

    // 获取包含averageScore和userCount的父节点引用
    const parentRef = change.after.ref.parent.parent;

    // 对父节点做事务,确保并发修改时的原子性
    return parentRef.transaction((parentData) => {
      if (!parentData || !parentData.userCount) {
        // 异常情况:父节点不存在或userCount丢失,直接中止事务
        return;
      }
      const currentCount = parentData.userCount;
      const currentAverage = parentData.averageScore || 0;
      // 计算新平均分:(原总分 + 分数变化) / 用户数
      const newTotal = currentAverage * currentCount + scoreDiff;
      const newAverage = parseFloat((newTotal / currentCount).toFixed(2));
      
      // 返回更新后的父节点数据,事务会自动写入
      return {
        ...parentData,
        averageScore: newAverage
      };
    });
  });

关键修改点说明

  • 移除了对userRef的事务调用:这是导致用户节点被删除的直接原因,我们不需要修改单个用户的节点,完全没必要在这里调用事务。
  • 转移事务到父节点:所有涉及averageScore和userCount的修改都在父节点的事务中完成,保证并发更新时的原子性,避免多个用户同时修改分数导致的计算错误。
  • 补充边界场景处理:原代码只处理了分数更新的情况,现在新增了用户创建时同步更新用户数和平均分的逻辑(如果需要处理用户删除,也可以在对应分支补充)。
  • 简化计算逻辑:通过计算分数变化值scoreDiff,避免重复计算总分,逻辑更清晰易维护。

额外注意事项

  • 确保userCount字段始终准确,它是平均分计算的核心基础,用户新增/删除时一定要同步更新这个字段。
  • 用parseFloat处理toFixed的结果,因为toFixed返回的是字符串,直接写入数据库会导致类型异常。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.29 06:39:58