基于 InstantDB 的 Stripe 一次性购买模式Token 先行、Webhook 记账与字段级权限解锁【免费下载链接】instantInstant is the best backend for AI-coded apps. You get auth, permissions, storage, presence, and streams — everything you need to ship apps your users will love.项目地址: https://gitcode.com/gh_mirrors/inst/instant导读本文讲解examples/stripe-one-off示例中沉淀的一套「买断制数字商品」支付模式用户在 Stripe Checkout 付费前先在客户端生成一个随机 token 并存入 localStorage通过 metadata 随结账会话传递给 Stripe支付完成后由 Webhook 以该 token 为凭证写入购买记录最后由 InstantDB 字段级权限规则决定「持有有效 token 的用户才能看到受保护内容」。读完本文你将掌握无需账号体系即可完成数字商品售卖、购买恢复与服务端强制访问控制的完整可落地方案。整体思路为什么 token 要在付款前生成这套模式的核心只有一条原则token 在付款之前生成而不是付款之后。完整流程如下1. 用户点击 Buy 2. 生成一个 token保存到 localStorage 3. 通过 metadata 把 token 传给 Stripe Checkout 4. Webhook 用该 token 创建购买记录 5. 用户落地到成功页 —— token 已在 localStorage 中 6. 携带 token 查询 → 受保护内容被解锁流程原文见 stripe-strategy.md关键收益是消除了竞态条件由于 token 先于支付存在Webhook 触发时只是「激活」了它创建购买记录而成功页早已持有同一 token无需等待 Webhook、无需额外的 API 调用即可立刻渲染已购内容。若改为付款后再生成 token成功页与 Webhook 之间就会出现时序竞争。这套模式由三个移动部件构成下文逐一展开并给出仓库中对应的真实实现文件。三个移动部件之一Buy 按钮客户端用户在首页点击购买按钮时客户端组件需要完成三件事生成 UUID、写入 localStorage、把 token 交给结账 API。仓库实现见 BuyButton.tsxuse client; import { useState } from react; import { TOKEN_KEY } from /lib/constants; export function BuyButton() { const [isLoading, setIsLoading] useState(false); const handleBuy async () { setIsLoading(true); try { // Generate token BEFORE checkout and save to localStorage const token crypto.randomUUID(); localStorage.setItem(TOKEN_KEY, token); const response await fetch(/api/checkout, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ token }), }); const data await response.json(); if (data.url) { window.location.href data.url; } } catch (error) { console.error(Checkout error:, error); setIsLoading(false); } }; return ( button onClick{handleBuy} disabled{isLoading} {isLoading ? Processing... : Buy Full Pack — $5} /button ); }其中TOKEN_KEY在 constants.ts 中定义为wallpaper_pack_token。仓库还封装了 usePurchaseToken.ts hook统一提供token、saveToken、clearToken三个能力供成功页、恢复页等复用 localStorage 中的 token。三个移动部件之二Checkout API服务端结账 API 路由接收客户端传来的 token并将其写入 Stripe Checkout 会话的metadata。仓库实现见 checkout/route.tsimport { NextResponse } from next/server; import { stripe } from /lib/stripe; export async function POST(request: Request) { try { const { origin } new URL(request.url); const { token } await request.json(); if (!token) { return NextResponse.json({ error: Token required }, { status: 400 }); } const session await stripe.checkout.sessions.create({ allow_promotion_codes: true, payment_method_types: [card], line_items: [ { price_data: { currency: usd, product_data: { name: Premium Wallpaper Pack, description: 9 high-resolution wallpapers, }, unit_amount: 500, }, quantity: 1, }, ], mode: payment, success_url: ${origin}/success, cancel_url: ${origin}, metadata: { token }, }); return NextResponse.json({ url: session.url }); } catch (error) { console.error(Checkout error:, error); return NextResponse.json( { error: Failed to create checkout session }, { status: 500 } ); } }要点说明metadata: { token }是 token 传递到 Webhook 的唯一通道这是本模式的关键一行unit_amount: 500表示 5.00 美元以美分为单位allow_promotion_codes: true与后文「生产环境测试」一节配合使用Stripe 客户端在 stripe.ts 中创建密钥只允许出现在服务端代码API 路由中绝不能暴露给客户端组件。三个移动部件之三Webhook记账的唯一入口支付完成后Stripe 向服务器发送 Webhook此时才创建购买记录。仓库实现见 webhook/stripe/route.tsimport { NextResponse } from next/server; import { headers } from next/headers; import { stripe } from /lib/stripe; import { createPurchase, findPurchaseBySessionId } from /lib/purchases; export async function POST(request: Request) { const body await request.text(); const headersList await headers(); const signature headersList.get(stripe-signature)!; let event; // Verify the webhook signature try { event stripe.webhooks.constructEvent( body, signature, process.env.STRIPE_WEBHOOK_SECRET! ); } catch (err) { console.error(Webhook signature verification failed:, err); return NextResponse.json({ error: Invalid signature }, { status: 400 }); } if (event.type checkout.session.completed) { const session event.data.object; const token session.metadata?.token; if (!token) { console.error(No token in session metadata); return NextResponse.json({ error: Missing token }, { status: 400 }); } // Check for duplicate const existing await findPurchaseBySessionId(session.id); if (existing) { return NextResponse.json({ received: true, duplicate: true }); } await createPurchase({ token, email: session.customer_details?.email || , stripeSessionId: session.id, stripePaymentIntentId: (session.payment_intent as string) || , amount: session.amount_total || 500, currency: session.currency || usd, }); } return NextResponse.json({ received: true }); }两步防御缺一不可签名校验stripe.webhooks.constructEvent(body, signature, secret)证明请求确实来自 Stripe如果不校验任何人都可以伪造 Webhook 白嫖内容。幂等处理Stripe 可能重复投递同一 WebhookfindPurchaseBySessionId(session.id)已存在记录时直接返回duplicate: true避免重复创建购买。createPurchase的实现见 purchases.ts它先查询全部壁纸再创建一条与所有壁纸关联的购买记录export async function createPurchase(params: CreatePurchaseParams) { // Get all wallpapers to link to the purchase const { wallpapers } await adminDb.query({ wallpapers: {} }); const wallpaperIds wallpapers.map((w) w.id); const purchaseId id(); await adminDb.transact( adminDb.tx.purchases[purchaseId] .update({ token: params.token, email: params.email, stripeSessionId: params.stripeSessionId, stripePaymentIntentId: params.stripePaymentIntentId, amount: params.amount, currency: params.currency, status: completed, createdAt: Date.now(), }) .link({ wallpapers: wallpaperIds }) ); return params.token; }这里的adminDb来自 adminDb.ts使用instantdb/admin初始化需要服务端环境变量INSTANT_APP_ID与INSTANT_APP_ADMIN_TOKEN——管理端 API 拥有绕过权限规则的完全写权限因此只应在服务端使用。数据模型purchases 实体与关联在 InstantDB 中购买记录被建模为purchases实体并通过purchaseWallpapers链接与wallpapers建立多对多关系。完整 schema 见 instant.schema.tsconst _schema i.schema({ entities: { // ... existing entities purchases: i.entity({ token: i.string().unique().indexed(), email: i.string().indexed(), stripeSessionId: i.string().unique().indexed(), stripePaymentIntentId: i.string().optional(), amount: i.number(), currency: i.string(), status: i.string().indexed(), createdAt: i.number().indexed(), }), }, links: { // ... existing links purchaseWallpapers: { forward: { on: purchases, has: many, label: wallpapers }, reverse: { on: wallpapers, has: many, label: purchases }, }, }, });各字段语义与约束以仓库源码为准字段类型与约束用途tokenstring().unique().indexed()购买凭证存于用户 localStorageemailstring().indexed()购买时填写的邮箱用于恢复流程stripeSessionIdstring().unique().indexed()幂等去重防止重复 WebhookstripePaymentIntentIdstring().optional()支付意图 ID对账用amountnumber()支付金额currencystring()币种如usdstatusstring().indexed()记录状态示例中写入completedcreatedAtnumber().indexed()创建时间戳schema 变更后执行推送npx instant-cli push schema --yes访问控制字段级权限实现服务端强制解锁这是整套方案安全性的根基。InstantDB 的权限规则在服务端执行客户端无论怎样修改代码都无法绕过。权限定义见 instant.perms.tsconst rules { wallpapers: { allow: { view: true, // Everyone can see wallpapers create: false, update: false, delete: false, }, fields: { // Only return fullResUrl if token matches a linked purchase fullResUrl: ruleParams.token in data.ref(purchases.token), }, }, purchases: { allow: { // Viewable if authenticated users email matches view: data.email auth.email, create: false, update: false, delete: false, }, }, };语义拆解壁纸的缩略图等元数据对所有人可见view: true但fullResUrl字段受字段级规则保护ruleParams.token in data.ref(purchases.token)—— 只有当调用方通过ruleParams传入的 token 存在于与该壁纸关联的某条购买的token字段中时fullResUrl才会被返回purchases实体仅允许邮箱匹配的已认证用户查看自己的购买记录用于恢复流程。权限推送命令npx instant-cli push perms --yes客户端查询时通过ruleParams携带 token见 success/page.tsxconst { data, isLoading: queryLoading } db.useQuery( { wallpapers: { $: { order: { order: asc } } } }, token ? { ruleParams: { token } } : undefined ); // If token is valid, wallpaper.fullResUrl exists // If token is invalid or missing, fullResUrl is omitted const isUnlocked !!wallpaper.fullResUrl;客户端db在 db.ts 中通过instantdb/react初始化使用公开的NEXT_PUBLIC_INSTANT_APP_ID。最终效果是有效 token → 返回fullResUrl无效或缺失 token → 该字段被省略解锁判断直接由字段是否存在决定。购买恢复无账号体系下的邮箱魔法码找回用户清空 localStorage 或更换设备后需要通过邮箱找回购买。方案不引入独立账号体系而是利用 InstantDB 自带的魔法码magic code认证做一次性邮箱所有权验证。流程见 recover/page.tsx用户输入邮箱 →db.auth.sendMagicCode({ email })发送验证码用户输入验证码 →db.auth.signInWithMagicCode({ email, code })完成认证认证后db.useQuery(user ? { purchases: {} } : null)查询该邮箱名下的购买记录受data.email auth.email权限约束命中第一条购买记录后将purchase.token写回 localStorage然后立即db.auth.signOut()登出——认证只是临时验证邮箱归属产品本身仍不需要账号。const handleSendCode async () { await db.auth.sendMagicCode({ email }); setStep(code); }; const handleVerifyCode async () { await db.auth.signInWithMagicCode({ email, code }); // Auth triggers the useQuery above };整个过程无需外部邮件服务InstantDB 已内置处理。测试与常见错误测试模式免费Stripe 测试模式下使用官方测试卡卡号结果4242 4242 4242 4242成功4000 0000 0000 0002被拒绝任何未来到期日、任意 3 位 CVC、任意邮编均可。生产环境测试在 Stripe Dashboardlive 模式创建 100% 折扣优惠券Checkout 会话中启用allow_promotion_codes: true仓库的 checkout/route.ts 已默认开启结账时使用该优惠券完成真实流程测试结束后移除该配置。本地与生产的 Webhook 配置本地开发使用 Stripe CLI 转发# Install Stripe CLI brew install stripe/stripe-cli/stripe # Login (one time) stripe login # Forward webhooks to your local server stripe listen --forward-to localhost:3000/api/webhook/stripeCLI 会输出一个whsec_...签名密钥写入.env.local的STRIPE_WEBHOOK_SECRET。生产环境则需在 Stripe Dashboard → Developers → Webhooks 添加端点URL 指向https://your-app.com/api/webhook/stripe事件选择checkout.session.completed。忘记配置生产 Webhook 是上线后最常见的漏项。六个高频踩坑点忘记校验 Webhook 签名——JSON.parse(body)会让任何人伪造请求必须用stripe.webhooks.constructEvent(body, signature, secret)。未处理重复 Webhook——Stripe 可能多次投递同一事件务必用findPurchaseBySessionId做幂等。只信任 localStorage——localStorage 里有 token 不代表支付成功用户可能中途取消结账应以「查询结果中是否存在fullResUrl」为准即wallpapers.some((w) !!w.fullResUrl)。付款后才生成 token——会引入 Webhook 与成功页之间的竞态必须在跳转 Checkout 前生成并落盘。生产环境未配置 Webhook 端点——本地能跑通不代表线上能记账。在客户端暴露 Stripe 密钥——sk_...只能用于 API 路由等服务端代码。方案小结回顾这套 buy-once 模式的四个设计支柱Token 先行付款前生成 token 并落盘成功页零等待、无竞态Webhook 唯一记账入口只有 Stripe 签名合法且去重通过的请求才能创建购买单一事实来源权限服务端强制字段级规则在 InstantDB 服务端执行任何客户端手段都无法绕过localStorage 邮箱恢复全程无需账号系统用户体验轻量。完整的按步骤实现指南见 tutorial.md示例工程的整体说明见 README.md。仓库中还提供了 link-purchases.ts 与 seed-wallpapers.ts 等配套脚本可参考其完成初始数据填充与购买关联的批量处理。若要扩展订阅、多商品或按量计费可在此基础上沿用同一套「metadata 传凭证 Webhook 记账 权限解锁」骨架。【免费下载链接】instantInstant is the best backend for AI-coded apps. You get auth, permissions, storage, presence, and streams — everything you need to ship apps your users will love.项目地址: https://gitcode.com/gh_mirrors/inst/instant创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
企业数字化 ERP 产品动态
相关推荐
用 GN 构建 Skottie iOS 示例应用:Metal / CPU / OpenGL 三种后端完整指南 图形学 【免费下载链接】skia Skia is a complete 2D graphic library for drawing Text, Geometries, and Images. See documentation for contribution instructions. 项目地址: https://gitcode.com/gh_mirrors/ski/skia 点击查看 免费下载 导读
本文以 Skia 仓… · 2026/9/24 16:40:42
ParlAI 中的 CNN/DM 摘要任务:从数据构建到 Teacher 实现的完整解析 NLP人工智能深度学习 【免费下载链接】ParlAI A framework for training and evaluating AI models on a variety of openly available dialogue datasets. 项目地址: https://gitcode.com/gh_mirrors/pa/ParlAI 点击查看 免费下载 本文围绕 ParlAI 任务库中的 cnn… · 2026/9/24 16:40:42
EasyWeChat 3.x 用户管理指南:基于 openid 的用户信息获取、列表与备注更新 后端即时通讯 【免费下载链接】easywechat 📦 一个 PHP 微信 SDK 项目地址: https://gitcode.com/gh_mirrors/ea/easywechat 点击查看 免费下载 用户信息的获取是微信公众平台开发中最常用的功能之一。本指南围绕 EasyWeChat 3.x 的 $app->user 用户… · 2026/9/24 17:15:41
xmlstream快速入门教程:3步编译构建并运行你的第一个XML反序列化程序 xmlstream快速入门教程:3步编译构建并运行你的第一个XML反序列化程序 【免费下载链接】xml_stream 提供 XML 操作相关的 StAX 风格接口,符合 XML 1.0 规范,支持命名空间。 项目地址: https://gitcode.com/Cangjie-TPC/xml_stream
xmls… · 2026/9/24 17:15:41
Utopia 属性规则引擎:读取属性事实、派生出类型结论的设计与实现 后端前端人工智能RAG知识图谱知识管理搜索引擎 【免费下载链接】utopia Worlds first open-source enterprise world model. 项目地址: https://gitcode.com/gh_mirrors/ont/utopia 点击查看 免费下载 本篇文章以 Utopia 的决策记录 0021-a-rule-reads-attributes-… · 2026/9/24 17:15:41
【在线五子棋对战】数据管理模块实现 目录
表结构
Register
Login
SelectByName
SelectById
SelectOne
WIn
Lose 数据管理模块用于对登录与注册用户进行管理,这里采用以表为单位,每个可操作的表为一个类,构建该表的接口
表结构
用于存储用户的表为gomoku数据库下的user… · 2026/9/24 17:15:41
【轻量 Web 架构三维 GIS 平台】 有人说:一个人从1岁活到80岁很平凡,但如果从80岁倒着活,那么一半以上的人都可能不凡。 生活没有捷径,我们踩过的坑都成为了生活的经验,这些经验越早知道,你要走的弯路就会越少。 · 2026/9/24 17:15:35
基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程 简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为… · 2026/9/24 0:00:13
1D-CNN时间序列建模实战:从Conv1d原理到工业落地 简介:面向时间序列数据建模的一维卷积神经网络完整实现,适合深度学习入门者及需要快速验证时序模型的研究者,能够从音频、文本、传感器或股价等序列中挖掘局部特征与时间依赖。压缩包体积很小,只有3KB,内含3个Python脚… · 2026/9/24 0:00:26
柔软的L:汉语语流中被忽视的舌肌张力控制 1. 这个“L”不是字母表里的L,而是舌尖上的L最近在几个方言群和语音教学社群里,反复看到有人发一句:“也说字母L:柔软的长舌”。初看以为是英语发音课笔记,点开才发现全是方言爱好者、播音系学生、语言康复师甚至戏曲演… · 2026/9/24 0:00:44