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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 16:55:00