Logo
RavenSaaS Docs

支付集成

支付概述

目前 RavenSaaS 支持 Stripe 网页支付功能。其他支付渠道逐步对接。

配置 Stripe 支付

1. 开通 Stripe 商户

请确保在 RavenSaaS 配置 Stripe 支付前,你已经开通了 Stripe 商户。

2. 创建支付密钥

在 Stripe 开发者控制台,创建支付密钥。

如果是本地调试,你可以选择进入沙盒模式,获取一对测试密钥。

Stripe Keys
在 Stripe 控制台获取 API 密钥

3. 配置环境变量

修改 RavenSaaS 项目配置,开启 Stripe 支付。

配置环境变量,根据自己项目的需求修改支付成功/支付失败/支付取消的回调地址。

.env.development
1#
2STRIPE_PUBLIC_KEY = "pk_test_xxx"
3STRIPE_PRIVATE_KEY = "sk_test_xxx"
4
5NEXT_PUBLIC_PAY_SUCCESS_URL = "http://localhost:3000/my-orders"
6NEXT_PUBLIC_PAY_FAIL_URL = "http://localhost:3000/#pricing"
7NEXT_PUBLIC_PAY_CANCEL_URL = "http://localhost:3000/#pricing"

4. 创建订单表

创建订单表之前,请确保你已经参考 数据库 一章的步骤,配置好了数据库存储和连接信息。

并且执行 sql 创建了 users 用户信息表。

复制 data/install.sql orders orders 表的建表语句,在你的数据库里面创建订单信息表。

data/install.sql
1CREATE TABLE orders (
2    id SERIAL PRIMARY KEY,
3    order_no VARCHAR(255) UNIQUE NOT NULL,
4    created_at timestamptz,
5    user_uuid VARCHAR(255) NOT NULL DEFAULT '',
6    user_email VARCHAR(255) NOT NULL DEFAULT '',
7    amount INT NOT NULL,
8    interval VARCHAR(50),
9    expired_at timestamptz,
10    status VARCHAR(50) NOT NULL,
11    stripe_session_id VARCHAR(255),
12    credits INT NOT NULL,
13    currency VARCHAR(50),
14    sub_id VARCHAR(255),
15    sub_interval_count int,
16    sub_cycle_anchor int,
17    sub_period_end int,
18    sub_period_start int,
19    sub_times int,
20    product_id VARCHAR(255),
21    product_name VARCHAR(255),
22    valid_months int,
23    order_detail TEXT,
24    paid_at timestamptz,
25    paid_email VARCHAR(255),
26    paid_detail TEXT
27);

5. 配置价格表

RavenSaaS 内置了一个价格表组件(Pricing):src/components/blocks/pricing.tsx,通过配置数据,展示价格表,默认支持多语言。

src/i18n/messages/en.json你可以根据自己的需求,修改 pricing 字段下的价格表信息。

价格表配置示例
价格表组件配置示例

6. 预览价格表

配置完成后,打开网站首页,可以看到配置好的价格表。

价格表预览
网站首页的价格表展示效果

7. 测试支付

点击价格表的下单按钮,跳转到支付控制台。

如果是测试环境,可以在 Stripe 测试卡 页面,复制一张测试卡号,填写到 Stripe 支付表单,进行支付测试。

常用测试卡号

4242 4242 4242 4242 - Visa 卡(成功支付)

4000 0000 0000 0002 - 卡被拒绝

过期日期:任意未来日期,CVC:任意3位数字

Stripe 支付页面
Stripe Checkout 支付页面

8. 处理支付结果

支付成功后,默认跳转到 /pay-success/xxx 页面,同步处理支付回调。

app/[locale]/pay-success/[session_id]/page.tsx
1import Stripe from "stripe";
2import { handleOrderSession } from "@/services/order";
3import { redirect } from "next/navigation";
4
5export default async function ({ params }: { params: Promise<{ session_id: string }> }) {
6  const { session_id } = await params;
7
8  try {
9    const stripe = new Stripe(process.env.STRIPE_PRIVATE_KEY || "");
10    const session = await stripe.checkout.sessions.retrieve(session_id);
11    
12    await handleOrderSession(session);
13
14  } catch (e) {
15    const failUrl = process.env.NEXT_PUBLIC_PAY_FAIL_URL || "/";
16    redirect(failUrl);
17  }
18
19  const successUrl = process.env.NEXT_PUBLIC_PAY_SUCCESS_URL || "/my-orders";
20  redirect(successUrl);
21}

更新完订单状态后,再跳转到配置文件中设置的 NEXT_PUBLIC_PAY_SUCCESS_URL 地址。默认情况下,支付成功后的处理逻辑,只会更新订单的状态和支付信息。

你也可以修改这里的逻辑,加上你自己的业务逻辑。比如发邮件 / 发通知 / 加积分等。

services/order.ts
1import { findOrderByOrderNo, updateOrderStatus } from "@/models/order";
2import Stripe from "stripe";
3import { getIsoTimestr } from "@/lib/time";
4
5export async function handleOrderSession(session: Stripe.Checkout.Session) {
6  try {
7    if (
8      !session ||
9      !session.metadata ||
10      !session.metadata.order_no ||
11      session.payment_status !== "paid"
12    ) {
13      throw new Error("invalid session");
14    }
15 
16    const order_no = session.metadata.order_no;
17    const paid_email =
18      session.customer_details?.email || session.customer_email || "";
19    const paid_detail = JSON.stringify(session);
20 
21    const order = await findOrderByOrderNo(order_no);
22    if (!order || order.status !== "pending") {
23      throw new Error("invalid order");
24    }
25 
26    const paid_at = getIsoTimestr();
27    await updateOrderStatus(order_no, "paid", paid_at, paid_email, paid_detail);
28
29    console.log(
30      "handle order session successed: ",
31      order_no,
32      paid_at,
33      paid_email,
34      paid_detail
35    );
36  } catch (e) {
37    console.log("handle order session failed: ", e);
38    throw e;
39  }
40}

支付结果异步通知

同步处理支付结果是不可靠的,可能出现的情况是在跳转过程中,用户误操作关闭了浏览器页面,导致更新订单状态和支付信息的逻辑没办法执行。项目正式上线之前,建议配置支付异步通知。

1. Stripe 后台配置 Webhook

参考 Stripe Webhook 配置文档,配置 Webhook。

本地调试,通过 stripe cli 监听事件:

Terminal
1stripe listen --events checkout.session.completed,invoice.payment_succeeded --forward-to localhost:3000/api/payment/stripe/notify

把用户支付后的 Stripe 回调事件,转发到本地的 RavenSaaS 服务的 /api/payment/stripe/notify 接口。

2. 修改配置文件

上一步在本地监听成功后,会得到一个 webhook signing secret,把这个参数的值填写到 RavenSaaS 项目的配置文件中:

.env.development
1STRIPE_WEBHOOK_SECRET = "whsec_cexxx"
Stripe CLI 监听事件
使用 Stripe CLI 监听支付事件

3. 处理支付回调结果

你可以按照自己的实际需求,修改默认的支付回调处理逻辑:

app/api/payment/stripe/notify/route.ts
1import Stripe from "stripe";
2import { respOk } from "@/lib/response";
3 
4export const runtime = "edge";
5 
6export async function POST(req: Request) {
7  try {
8    const stripePrivateKey = process.env.STRIPE_PRIVATE_KEY;
9    const stripeWebhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
10 
11    if (!stripePrivateKey || !stripeWebhookSecret) {
12      throw new Error("invalid stripe config");
13    }
14 
15    const stripe = new Stripe(stripePrivateKey);
16 
17    const sign = req.headers.get("stripe-signature") as string;
18    const body = await req.text();
19    if (!sign || !body) {
20      throw new Error("invalid notify data");
21    }
22 
23    const event = await stripe.webhooks.constructEventAsync(
24      body,
25      sign,
26      stripeWebhookSecret
27    );
28 
29    console.log("stripe notify event: ", event);
30 
31    switch (event.type) {
32      case "checkout.session.completed": {
33        const session = event.data.object;
34
35        const handleResponse = await fetch(`${process.env.NEXT_PUBLIC_WEB_URL}/api/payment/stripe/handle-session`, {
36          method: 'POST',
37          headers: {
38            'Content-Type': 'application/json',
39          },
40          body: JSON.stringify({ session }),
41        });
42
43        if (!handleResponse.ok) {
44          throw new Error(`Failed to handle session: ${handleResponse.statusText}`);
45        }
46
47        break;
48      }
49
50      default:
51        console.log("not handle event: ", event.type);
52    }
53 
54    return respOk();
55  } catch (e: any) {
56    console.log("stripe notify failed: ", e);
57    return Response.json(
58      { error: `handle stripe notify failed: ${e.message}` },
59      { status: 500 }
60    );
61  }
62}

在上线到生产环境之前,你需要在 Stripe 生产环境配置支付回调 Webhook。

Stripe Webhook
Stripe Webhook 配置

支付定制化

订阅支付

RavenSaaS 默认支持三种支付方案:

  • 一次性扣费:one-time
  • 按月订阅扣费:month
  • 按年订阅扣费:year

你只需要修改价格表配置,把每个价格方案的 paymentType 字段,设置成上述三个值之一。

同时,按需修改价格 amount / credits / valid_months 等字段。

举例:按月订阅扣费,月付 $99,购买后得到 30 个积分,有效期 1 个月,则核心的价格表配置信息为:

Terminal
1{
2  "interval": "month",
3  "amount": 9900,
4  "credits": 30,
5  "valid_months": 1
6}

最佳实践

RavenSaaS 未能适配所有的支付场景。请根据你的实际业务需求,自行修改:

  • 价格表组件:src/components/blocks/pricing.tsx
  • 支付下单接口:src/app/api/checkout/route.ts
  • 支付回调逻辑:src/app/api/payment/stripe/notify/route.ts

参考

Stripe 支付文档

查看文档

Last updated on 2025年1月5日