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

React订阅应用Firebase免费试用计数器实现方案咨询

技术方案建议:免费保存计数器的实现选择

两种方案的优缺点分析

客户端处理

  • 优势:开发成本低,无需额外部署资源,用户操作能得到即时反馈,无网络延迟感。
  • 致命问题:完全不可靠。用户可通过修改前端代码、篡改本地存储或抓包等方式绕过计数限制,直接破坏免费额度规则,影响付费转化。

事件型Cloud Function(服务端触发)

这是保障规则有效性的核心方案,所有逻辑在服务端执行:

  1. 在用户文档中新增freeSavesRemaining字段,默认值设为5;同时可添加isSubscribed字段标记付费状态。
  2. 当用户创建新的prompt文档时,触发Cloud Function:
    • 先校验用户是否存在,若为订阅用户则直接放行
    • 未订阅用户读取当前剩余免费次数,若次数>0则递减计数并更新用户文档,允许保存;若次数≤0则删除刚创建的prompt并返回权限错误。
  • 优势:规则完全由服务端管控,用户无法篡改,确保免费额度的真实性,保障付费转化逻辑。
  • 劣势:需要额外部署云函数,增加少量开发和运维成本,但对于该场景来说开销极低。

推荐最优方案:服务端核心+客户端辅助

兼顾安全性与用户体验,建议采用混合模式:

  1. 核心逻辑依赖Cloud Function:这是底线,所有计数校验和更新必须在服务端完成,彻底杜绝用户作弊。
  2. 客户端做体验优化:
    • 页面加载时从服务端拉取最新剩余计数,缓存到前端
    • 用户点击保存时,前端先做本地判断并给出提示(如“剩余X次免费保存”),但最终权限以服务端返回结果为准
    • 服务端处理成功后,同步更新前端的计数显示
  3. 订阅后的逻辑:用户通过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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 15:40:28