戰(zhàn):三步跑通 Stripe 訂閱付費(fèi)與 Webhook 回調(diào))
open-saas 支付接入實(shí)戰(zhàn)三步跑通 Stripe 訂閱付費(fèi)與 Webhook 回調(diào)【免費(fèi)下載鏈接】open-saasA 100% free modern JS SaaS boilerplate (React, NodeJS, Prisma). Full-featured: Auth (email, google, github, slack, MS), Email sending, Background jobs, Landing page, Payments (Stripe, Polar.sh), Shadcn UI, S3 file upload. AI-ready with tailored AGENTS.md, skills, and Claude Code plugin. One cmd deploy. Powered by Wasp full-stack framework.項(xiàng)目地址: https://gitcode.com/GitHub_Trending/op/open-saas假設(shè)你的 SaaS 產(chǎn)品已經(jīng)做完現(xiàn)在要加上訂閱付費(fèi)還希望用戶(hù)能上傳文件——但不想從零寫(xiě)支付流程、簽名校驗(yàn)和訂閱狀態(tài)流轉(zhuǎn)。open-saasReact Node.js Prisma 的免費(fèi) SaaS 模板已經(jīng)把這些都實(shí)現(xiàn)了。讀完這篇你會(huì)知道付費(fèi)模塊該看哪幾個(gè)文件、Stripe 的 Key 填在哪、回調(diào)怎么接才能自動(dòng)更新訂閱狀態(tài)、前端的購(gòu)買(mǎi)按鈕到底是怎么調(diào)通后端的。? 架構(gòu)全景一次購(gòu)買(mǎi)請(qǐng)求怎么走一圈先建立地圖前端只發(fā)買(mǎi)哪個(gè)計(jì)劃這一個(gè)參數(shù)后端 operations.ts 先做登錄態(tài)和參數(shù)校驗(yàn)再把活兒交給 Stripe 處理器創(chuàng)建結(jié)賬會(huì)話(huà)并返回支付 URL用戶(hù)付完款Stripe 會(huì)異步回調(diào)/payments-webhook后端驗(yàn)簽后把訂閱狀態(tài)寫(xiě)回?cái)?shù)據(jù)庫(kù)。整個(gè)流程不經(jīng)過(guò)任何輪詢(xún)支付結(jié)果的臟活。分層文件職責(zé)前端頁(yè)面payment/PricingPage.tsx渲染定價(jià)卡片、觸發(fā)購(gòu)買(mǎi)、打開(kāi)客戶(hù)門(mén)戶(hù)路由與操作注冊(cè)payment/payment.wasp.ts掛載 /pricing、/checkout 頁(yè)面和 webhook業(yè)務(wù)邏輯payment/operations.ts401/403 權(quán)限校驗(yàn) Zod 參數(shù)校驗(yàn)計(jì)劃定義payment/plans.tsHobby、Pro、Credits10 三個(gè)計(jì)劃及其效果Stripe 實(shí)現(xiàn)payment/stripe/結(jié)賬創(chuàng)建、計(jì)費(fèi)門(mén)戶(hù)、回調(diào)處理回調(diào)入口payment/webhook.ts把請(qǐng)求轉(zhuǎn)發(fā)給當(dāng)前處理器的 webhook關(guān)鍵設(shè)計(jì)是 paymentProcessor.ts 里的接口抽象Stripe、Lemon Squeezy、Polar.sh 三家實(shí)現(xiàn)了同一套接口換支付渠道只改一行export const paymentProcessor: PaymentProcessor stripePaymentProcessor; // export const paymentProcessor: PaymentProcessor lemonSqueezyPaymentProcessor; // export const paymentProcessor: PaymentProcessor polarPaymentProcessor;? 動(dòng)手路線(xiàn)后端填環(huán)境變量讀三個(gè)關(guān)鍵文件先打開(kāi) payment/plans.ts它定義了三個(gè)計(jì)劃Hobby 和 Pro 是訂閱subscription 效果Credits10 是一次性積分充值credits 效果。你要增刪計(jì)劃就改這里的枚舉和 paymentPlans 對(duì)象再走一遍完整流程驗(yàn)證。再打開(kāi) .env把付費(fèi)相關(guān)的環(huán)境變量填齊全部有 Zod 校驗(yàn)缺一個(gè)應(yīng)用直接起不來(lái)PAYMENTS_HOBBY_SUBSCRIPTION_PLAN_IDprice_1xxx PAYMENTS_PRO_SUBSCRIPTION_PLAN_IDprice_1yyy PAYMENTS_CREDITS_10_PLAN_IDprice_1zzz STRIPE_API_KEYsk_test_xxx STRIPE_WEBHOOK_SECRETwhsec_xxx前三個(gè)是 Stripe 控制臺(tái)的 Price ID后兩個(gè)分別是密鑰和 webhook 簽名密鑰。自測(cè)方法啟動(dòng)應(yīng)用用測(cè)試賬號(hào)登錄后調(diào)用 getCustomerPortalUrl 操作未付過(guò)款的用戶(hù)應(yīng)返回 null 而不是報(bào)錯(cuò)。前端購(gòu)買(mǎi)按鈕怎么接不用自己包 API 請(qǐng)求Wasp 會(huì)根據(jù) payment.wasp.ts 里注冊(cè)的 operations 自動(dòng)生成類(lèi)型安全的客戶(hù)端前端不寫(xiě) fetch。PricingPage.tsx 里立即購(gòu)買(mǎi)的核心邏輯就三步const checkoutSession await generateCheckoutSession(paymentPlanId); if (checkoutSession?.sessionUrl) { window.open(checkoutSession.sessionUrl, _self); }管理訂閱按鈕則用useQuery(getCustomerPortalUrl)拿門(mén)戶(hù)地址且設(shè)了enabled: isUserSubscribed只有已訂閱用戶(hù)才發(fā)請(qǐng)求。你通常只需要改paymentPlanCards里的價(jià)格和功能文案請(qǐng)求邏輯一行都不用動(dòng)。聯(lián)調(diào)本地把回調(diào)閉環(huán)跑通回調(diào)是最容易漏的一環(huán)。開(kāi)一個(gè)終端跑 Stripe 本地轉(zhuǎn)發(fā)stripe listen --forward-to localhost:3000/payments-webhook把終端打印的 whsec_ 開(kāi)頭的簽名密鑰復(fù)制進(jìn) STRIPE_WEBHOOK_SECRET。注意 webhook.ts 用express.raw解析原始請(qǐng)求體——Stripe 驗(yàn)簽必須基于原始字節(jié)換成 JSON 解析驗(yàn)簽必掛。驗(yàn)簽通過(guò)后按事件分發(fā)switch (event.type) { case invoice.paid: /* 寫(xiě)訂閱狀態(tài)或加積分 */ break; case customer.subscription.updated: /* 狀態(tài)流轉(zhuǎn)取消時(shí)發(fā)挽留郵件 */ break; case customer.subscription.deleted: /* 標(biāo)記為 deleted */ break; }聯(lián)調(diào)流程打開(kāi)定價(jià)頁(yè) → 立即購(gòu)買(mǎi) → 用測(cè)試卡 4242 4242 4242 4242 付款 → 回到 /checkout 成功頁(yè) → 查數(shù)據(jù)庫(kù)subscriptionStatus 應(yīng)為 active。最后去計(jì)費(fèi)門(mén)戶(hù)取消訂閱觀察狀態(tài)變?yōu)?cancel_at_period_end并確認(rèn)收到挽留郵件。?? 踩坑清單現(xiàn)象webhook 一直 400報(bào)簽名驗(yàn)證失敗。原因把 STRIPE_API_KEY 的值填進(jìn)了 STRIPE_WEBHOOK_SECRET兩者不是一回事。改法webhook secret 必須是 whsec_ 開(kāi)頭的簽名密鑰本地用stripe listen打印的那個(gè)?,F(xiàn)象付款成功但用戶(hù) subscriptionStatus 沒(méi)變。原因Stripe 控制臺(tái)給 webhook 勾選的事件太多后端遇到?jīng)]處理的事件會(huì)拋 UnhandledWebhookEventError而你要的事件被淹沒(méi)在日志里。改法控制臺(tái)只勾選上面那 3 個(gè)事件然后盯后端日志確認(rèn)事件真的到了。現(xiàn)象一次性積分購(gòu)買(mǎi)沒(méi)有任何回調(diào)觸發(fā)。原因Stripe 只對(duì)訂閱自動(dòng)生成 invoice.paid一次性支付必須顯式開(kāi)啟 invoice 創(chuàng)建。改法stripe/checkoutUtils.ts 已對(duì) payment 模式啟用invoice_creation你照抄這份文件時(shí)別把這行刪掉?,F(xiàn)象調(diào)購(gòu)買(mǎi)接口返回 403 User needs an email to make a payment。原因你的認(rèn)證方式是 usernameAndPassword拿不到郵箱而 Stripe 建客戶(hù)必須用郵箱。改法換成帶郵箱的認(rèn)證方式如郵箱密碼或 OAuth?,F(xiàn)象換 Lemon Squeezy 后支付報(bào)Unknown payment processor plan ID。原因paymentProcessorPlans.ts 用環(huán)境變量把內(nèi)部計(jì)劃映射到渠道側(cè)的計(jì)劃 ID換渠道后這三個(gè)值沒(méi)換。改法把三個(gè) PAYMENTS_*_PLAN_ID 重填為新渠道的 ID。? 驗(yàn)收清單付費(fèi)環(huán)境變量全部填齊三個(gè) PLAN_ID 與 Stripe 控制臺(tái) Price ID 一致webhook 只配置 3 個(gè)事件本地觸發(fā)后日志無(wú) Unhandled 報(bào)錯(cuò)測(cè)試卡購(gòu)買(mǎi)后用戶(hù) subscriptionStatus 變?yōu)?active管理后臺(tái)能看到收入計(jì)費(fèi)門(mén)戶(hù)取消訂閱后狀態(tài)變?yōu)?cancel_at_period_end并收到挽留郵件一次性積分計(jì)劃購(gòu)買(mǎi)后用戶(hù)積分余額正確增加未登錄調(diào)購(gòu)買(mǎi)接口返回 401非管理員調(diào)用戶(hù)管理接口返回 403文件上傳白名單 ALLOWED_FILE_TYPES 已改成你業(yè)務(wù)需要的類(lèi)型S3 桶已建好下一步打開(kāi) template/blog 的 docs/guides 目錄讀支付集成章節(jié)逐項(xiàng)核對(duì)你的環(huán)境變量然后在 template/app 目錄跑npm run dev把購(gòu)買(mǎi) → 回調(diào) → 取消完整閉環(huán)走一遍跑通后就可以基于 subscriptionStatus 字段寫(xiě)你自己的付費(fèi)功能門(mén)控邏輯了?!久赓M(fèi)下載鏈接】open-saasA 100% free modern JS SaaS boilerplate (React, NodeJS, Prisma). Full-featured: Auth (email, google, github, slack, MS), Email sending, Background jobs, Landing page, Payments (Stripe, Polar.sh), Shadcn UI, S3 file upload. AI-ready with tailored AGENTS.md, skills, and Claude Code plugin. One cmd deploy. Powered by Wasp full-stack framework.項(xiàng)目地址: https://gitcode.com/GitHub_Trending/op/open-saas創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考