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

Firebase可调用函数无法返回值问题求助

解决Firebase可调用函数返回null及错误无法传递的问题

你的问题核心在于混用了回调式数据库操作与Promise链,且没有正确返回完整的异步操作链,导致客户端无法获取到最终的GUID,错误也无法传递。下面一步步帮你修正:

关键问题分析

  1. 你用了once("value", callback)这种回调写法,它不会返回Promise,导致后续的.then()无法正确衔接异步操作,函数提前返回了空值。
  2. 错误在回调内部抛出,没有进入Promise链,Firebase无法将这些错误传递给客户端。
  3. 部分异步操作没有返回Promise,导致Promise链断裂,函数无法等待异步操作完成就返回了。

修正后的完整代码

exports.findCreateGame = functions.https.onCall((data, context) => {
    console.log("findCurrentGame Called.")
    const uid = context.auth.uid;
    let section = "";
    let sUsername = "";
    let sProfilePic = "";
    let currentGames = null;
    let sGUID = "";

    // 第一步:获取用户信息,改用Promise式调用
    return admin.database().ref('users/' + uid).once("value")
        .then(userSnapshot => {
            const userData = userSnapshot.val();
            const wins = parseInt(userData.wins);
            const losses = parseInt(userData.losses);
            const lives = parseInt(userData.lives);

            if (lives < 1) {
                // 在这里抛出HttpsError,会被客户端catch捕获
                throw new functions.https.HttpsError('permission-denied', 'You do not have enough lives to start a new game.');
            }

            sUsername = userData.username;
            sProfilePic = userData.profilepicture;

            let ratio = 0;
            if (losses === 0) {
                ratio = 100;
            } else {
                ratio = (wins / losses) * 100;
            }

            // 新手 tier 判断
            if (wins + losses < 5) {
                ratio = 0;
            }

            if (ratio <= 33) {
                section = "noob";
            } else if (ratio > 33 && ratio <= 66) {
                section = "average";
            } else {
                section = "expert";
            }

            // 第二步:获取用户当前参与的游戏,返回Promise继续链
            return admin.database().ref('games').orderByChild(uid).once('value');
        })
        .then(gamesSnapshot => {
            currentGames = gamesSnapshot.val();

            // 生成GUID
            sGUID = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
                return v.toString(16);
            });

            // 第三步:处理游戏队列事务,返回Promise
            const queueref = admin.database().ref('gamequeue/' + section);
            return queueref.transaction(currentGUID => {
                if (currentGUID == null) {
                    return sGUID;
                } else {
                    sGUID = currentGUID;
                    return null;
                }
            });
        })
        .then(transactionResult => {
            const snapshot = transactionResult.snapshot;
            if (snapshot.val() != null) {
                // 创建新游戏
                let newAnswer = "";
                while (newAnswer.length < 4) {
                    const temp = Math.floor(Math.random() * 9) + 1;
                    const tempStr = temp.toString();
                    if (!newAnswer.includes(tempStr)) {
                        newAnswer += tempStr;
                    }
                }
                const userObj = {username: sUsername, score: 0, profilepicture: sProfilePic};
                // 返回创建游戏的Promise,最终返回GUID
                return admin.database().ref('games/' + sGUID).set({
                    id: sGUID,
                    requestor: uid,
                    [uid]: userObj,
                    answer: newAnswer,
                    turn: uid,
                    status: 'pending'
                }).then(() => {
                    return {guid: sGUID};
                });
            } else {
                // 加入已有游戏
                if (typeof(sGUID) !== 'string') {
                    sGUID = Object.keys(sGUID)[0];
                }

                // 检查是否找到自己的游戏请求
                if (currentGames && currentGames[sGUID] != null) {
                    // 将GUID重新放回队列,返回这个操作的Promise
                    return admin.database().ref('gamequeue/' + section).set(sGUID)
                        .then(() => {
                            // 重新执行整个流程?或者返回错误?这里根据你的业务逻辑调整
                            throw new functions.https.HttpsError('aborted', 'Found own game request, retrying.');
                        });
                } else {
                    // 加入游戏的逻辑,补充完整业务代码
                    const userObj = {username: sUsername, score: 0, profilepicture: sProfilePic};
                    return admin.database().ref('games/' + sGUID + '/' + uid).set(userObj)
                        .then(() => {
                            return admin.database().ref('users/' + uid + '/currentGames/' + sGUID).set(true);
                        })
                        .then(() => {
                            return {guid: sGUID};
                        });
                }
            }
        })
        .catch(error => {
            // 捕获所有错误,确保传递给客户端
            if (!(error instanceof functions.https.HttpsError)) {
                // 将普通错误转为HttpsError
                throw new functions.https.HttpsError('unknown', 'An unexpected error occurred', error);
            }
            throw error; // 抛出已定义的HttpsError
        });
});

核心修正点

  1. 统一使用Promise式调用:所有数据库操作(once、transaction、set)都返回Promise,不再用回调参数,确保异步操作能串联成完整的链。
  2. 每一步都返回Promise:每个.then()里的异步操作必须返回,这样Promise链才不会断裂,函数会等待所有异步操作完成后再返回结果。
  3. 正确抛出HttpsError:所有业务错误都要抛出functions.https.HttpsError,并且要在Promise链中抛出,这样客户端的.catch()才能捕获到。
  4. 错误统一处理:最后添加.catch()块,将非HttpsError转为标准的HttpsError,确保所有错误都能传递给客户端。

客户端调用注意事项

你的客户端代码基本没问题,但要确保正确处理返回的result.data.guid:

var newGame = firebase.functions().httpsCallable('findCreateGame');
newGame({}).then(function(result) {
    var sGameID = result.data.guid;
    console.log("Game ID:", sGameID);
}).catch(function(error) {
    console.error("Error:", error.message);
    // 可以根据error.code处理不同类型的错误
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.28 10:02:55