如何在Next.js+Stripe单API中实现多订阅套餐?
多套餐订阅解决方案:单个API实现套餐选择
完全可以在单个API文件里实现多套餐选择,不用拆分多个API。你的问题根源是当前API里的line_items同时包含了两个套餐,所以Stripe会把金额相加。下面是具体修改步骤:
1. 前端修改:添加套餐选择功能
更新PlanModal组件,让用户能选择Plume Pro或Plume Plus套餐,点击订阅时把选中的套餐类型传给API:
"use client"; import { usePlanModal } from "@/hooks/use-plan-modal"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from "../ui/dialog"; import { Separator } from "../ui/separator"; import { Button } from "../ui/button"; import { useToast } from "../ui/use-toast"; import axios from "axios"; import { useState } from "react"; export const PlanModal = () => { const planModal = usePlanModal(); const { toast } = useToast(); const [loading, setLoading] = useState(false); // 保存选中的套餐,默认选Plus const [selectedPlan, setSelectedPlan] = useState("plus"); const onSubscribe = async () => { try { setLoading(true); // 把选中的套餐作为query参数传给API const response = await axios.get(`/api/stripe?plan=${selectedPlan}`); window.location.href = response.data.url; } catch (error) { toast({ variant: "destructive", description: "Oops! Something went wrong.", }); } finally { setLoading(false); } }; // 套餐配置,方便后续扩展 const plans = [ { id: "plus", name: "Plume Plus", price: "$39.99", description: "基础权限", }, { id: "pro", name: "Plume Pro", price: "$79.99", description: "完整权限", }, ]; return ( <Dialog open={planModal.isOpen} onOpenChange={planModal.onClose}> <DialogContent> <DialogHeader className="space-y-4"> <DialogTitle className="text-center">Upgrade your Plan</DialogTitle> <DialogDescription className="text-center space-y-2"> Choose a plan that meets your needs. </DialogDescription> </DialogHeader> <Separator /> {/* 套餐选择区域 */} <div className="space-y-3 my-4"> {plans.map((plan) => ( <div key={plan.id} className={`flex items-center justify-between p-3 rounded-lg border cursor-pointer ${ selectedPlan === plan.id ? "border-blue-500 bg-blue-50" : "border-gray-200" }`} onClick={() => setSelectedPlan(plan.id)} > <div> <p className="font-medium">{plan.name}</p> <p className="text-sm text-gray-500">{plan.description}</p> </div> <div className="text-right"> <p className="text-xl font-medium">{plan.price}</p> <p className="text-xs text-gray-500">/月</p> </div> </div> ))} </div> <Button size="md" onClick={onSubscribe} disabled={loading} className="w-full"> {loading ? "Processing..." : "Subscribe"} </Button> </DialogContent> </Dialog> ); };
2. API修改:根据参数生成对应套餐会话
更新/api/stripe的GET方法,获取前端传的plan参数,生成对应的订阅商品:
// api/stripe import { auth, currentUser } from "@clerk/nextjs/server"; import { NextResponse } from "next/server"; import { prismadb } from "@/lib/prismadb"; import { stripe } from "@/lib/stripe"; import { absoluteUrl } from "@/lib/utils"; const billingUrl = absoluteUrl("/billing"); // 套餐配置映射 const planConfig = { plus: { name: "Plume Plus", description: "Gain Basic Access", unitAmount: 3999, }, pro: { name: "Plume Pro", description: "Gain Full Access", unitAmount: 7999, }, }; export async function GET(request: Request) { try { const { userId } = auth(); const user = await currentUser(); if (!userId || !user) { return new NextResponse("Unauthorized", { status: 401 }); } const userSubscription = await prismadb.userSubscription.findUnique({ where: { userId, }, }); if (userSubscription && userSubscription.stripeCustomerId) { const stripeSession = await stripe.billingPortal.sessions.create({ customer: userSubscription.stripeCustomerId, return_url: billingUrl, }); return new NextResponse(JSON.stringify({ url: stripeSession.url })); } // 获取query参数里的plan const { searchParams } = new URL(request.url); const plan = searchParams.get("plan") || "plus"; // 验证套餐是否存在 if (!planConfig[plan as keyof typeof planConfig]) { return new NextResponse("Invalid Plan", { status: 400 }); } const selectedPlan = planConfig[plan as keyof typeof planConfig]; const stripeSession = await stripe.checkout.sessions.create({ success_url: billingUrl, cancel_url: billingUrl, payment_method_types: ["card", "Paypal"], mode: "subscription", billing_address_collection: "auto", customer_email: user.emailAddresses[0].emailAddress, // 只添加选中的套餐到line_items line_items: [ { price_data: { currency: "USD", product_data: { name: selectedPlan.name, description: selectedPlan.description, }, unit_amount: selectedPlan.unitAmount, recurring: { interval: "month", }, }, quantity: 1, }, ], metadata: { userId, plan, // 把套餐类型存在metadata里,后续webhook可以用 }, }); return new NextResponse(JSON.stringify({ url: stripeSession.url })); } catch (error) { console.log("[STRIPE_GET]", error); return new NextResponse("Internal Error", { status: 500 }); } }
关键说明
- 之前金额相加是因为
line_items数组里同时包含了两个套餐的配置,Stripe会默认把所有line item的金额相加。 - 通过前端传
plan参数给API,API根据参数生成对应的单个line item,就能实现单套餐选择。 - 用配置对象
planConfig管理套餐信息,后续新增套餐只要修改配置即可,不用动核心逻辑,扩展性更好。
内容的提问来源于stack exchange,提问作者Jean Mako
相关产品推荐
相关产品推荐

