React订阅应用Firebase免费试用计数器实现方案咨询
技术方案建议:免费保存计数器的实现选择
两种方案的优缺点分析
客户端处理
- 优势:开发成本低,无需额外部署资源,用户操作能得到即时反馈,无网络延迟感。
- 致命问题:完全不可靠。用户可通过修改前端代码、篡改本地存储或抓包等方式绕过计数限制,直接破坏免费额度规则,影响付费转化。
事件型Cloud Function(服务端触发)
这是保障规则有效性的核心方案,所有逻辑在服务端执行:
- 在用户文档中新增
freeSavesRemaining字段,默认值设为5;同时可添加isSubscribed字段标记付费状态。 - 当用户创建新的prompt文档时,触发Cloud Function:
- 先校验用户是否存在,若为订阅用户则直接放行
- 未订阅用户读取当前剩余免费次数,若次数>0则递减计数并更新用户文档,允许保存;若次数≤0则删除刚创建的prompt并返回权限错误。
- 优势:规则完全由服务端管控,用户无法篡改,确保免费额度的真实性,保障付费转化逻辑。
- 劣势:需要额外部署云函数,增加少量开发和运维成本,但对于该场景来说开销极低。
推荐最优方案:服务端核心+客户端辅助
兼顾安全性与用户体验,建议采用混合模式:
- 核心逻辑依赖Cloud Function:这是底线,所有计数校验和更新必须在服务端完成,彻底杜绝用户作弊。
- 客户端做体验优化:
- 页面加载时从服务端拉取最新剩余计数,缓存到前端
- 用户点击保存时,前端先做本地判断并给出提示(如“剩余X次免费保存”),但最终权限以服务端返回结果为准
- 服务端处理成功后,同步更新前端的计数显示
- 订阅后的逻辑:用户通过Stripe订阅成功后,用Cloud Function将用户的
isSubscribed设为true,后续保存操作直接跳过计数校验。
代码示例
Cloud Function(Node.js)
const functions = require("firebase-functions"); const admin = require("firebase-admin"); admin.initializeApp(); exports.validateFreeSave = functions.firestore .document("prompts/{promptId}") .onCreate(async (snap, context) => { const userId = snap.data().userId; if (!userId) { await snap.ref.delete(); return functions.https.HttpsError("invalid-argument", "User ID missing"); } const userRef = admin.firestore().collection("users").doc(userId); const userDoc = await userRef.get(); if (!userDoc.exists) { await snap.ref.delete(); return functions.https.HttpsError("not-found", "User not found"); } const userData = userDoc.data(); // 订阅用户直接放行 if (userData.isSubscribed) return null; const remainingSaves = userData.freeSavesRemaining ?? 5; if (remainingSaves <= 0) { await snap.ref.delete(); return functions.https.HttpsError("permission-denied", "Free saves exhausted. Subscribe to continue."); } // 更新剩余次数 await userRef.update({ freeSavesRemaining: remainingSaves - 1 }); return null; });
React客户端辅助逻辑
import { useState, useEffect } from "react"; const PromptSaver = () => { const [remainingFreeSaves, setRemainingFreeSaves] = useState(5); const [promptText, setPromptText] = useState(""); const currentUserId = "当前用户ID"; // 替换为实际获取用户ID的逻辑 // 加载时获取最新剩余次数 useEffect(() => { const fetchRemaining = async () => { const res = await fetch(`/api/user/${currentUserId}`); // 替换为你的用户数据接口 const user = await res.json(); setRemainingFreeSaves(user.freeSavesRemaining || 5); }; fetchRemaining(); }, [currentUserId]); const handleSave = async () => { if (!promptText.trim()) return; // 前端预提示,不做最终校验 if (remainingFreeSaves <= 0) { alert("免费保存次数已用完,请订阅继续使用"); // 跳转到Stripe订阅页面 return; } try { await fetch("/api/save-prompt", { // 替换为你的保存接口 method: "POST", body: JSON.stringify({ text: promptText, userId: currentUserId }) }); // 更新前端计数 setRemainingFreeSaves(prev => prev - 1); setPromptText(""); } catch (err) { if (err.message.includes("permission-denied")) { alert("免费保存次数已用完,请订阅继续使用"); // 跳转到订阅页面 } else { alert("保存失败,请稍后重试"); } } }; return ( <div className="prompt-saver"> <p className="remaining-count">剩余免费保存次数: {remainingFreeSaves}</p> <textarea value={promptText} onChange={(e) => setPromptText(e.target.value)} placeholder="输入你的提示内容..." rows={4} /> <button onClick={handleSave} disabled={!promptText.trim()}> 保存提示 </button> </div> ); }; export default PromptSaver;
内容的提问来源于stack exchange,提问作者AhmedSalem016
相关产品推荐
相关产品推荐

