支付
目前 ShipAny 支持 Stripe 网页支付功能。其他支付渠道逐步对接。
配置 Stripe 支付
-
请确保在 ShipAny 配置 Stripe 支付前,你已经开通了 Stripe 商户。
-
在 Stripe 开发者控制台,创建支付密钥。
如果是本地调试,你可以选择开启 test mode,获取一对测试密钥。
- 修改 ShipAny 项目配置,开启 Stripe 支付。
配置环境变量,根据自己项目的需求修改支付成功 / 支付失败 / 支付取消的回调地址。
STRIPE_PUBLIC_KEY = "pk_test_xxx"
STRIPE_PRIVATE_KEY = "sk_test_xxx"
NEXT_PUBLIC_PAY_SUCCESS_URL = "http://localhost:3000/my-orders"
NEXT_PUBLIC_PAY_FAIL_URL = "http://localhost:3000/#pricing"
NEXT_PUBLIC_PAY_CANCEL_URL = "http://localhost:3000/#pricing"
- 创建订单表
创建订单表之前,请确保你已经参考:数据库 一章的步骤,配置好了数据库存储和连接信息。
并且执行 sql 创建了 users
用户信息表。
复制 data/install.sql
文件里的 orders
表的建表语句,在你的数据库里面创建订单信息表。
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
order_no VARCHAR(255) UNIQUE NOT NULL,
created_at timestamptz,
user_uuid VARCHAR(255) NOT NULL DEFAULT '',
user_email VARCHAR(255) NOT NULL DEFAULT '',
amount INT NOT NULL,
interval VARCHAR(50),
expired_at timestamptz,
status VARCHAR(50) NOT NULL,
stripe_session_id VARCHAR(255),
credits INT NOT NULL,
currency VARCHAR(50),
sub_id VARCHAR(255),
sub_interval_count int,
sub_cycle_anchor int,
sub_period_end int,
sub_period_start int,
sub_times int,
product_id VARCHAR(255),
product_name VARCHAR(255),
valid_months int,
order_detail TEXT,
paid_at timestamptz,
paid_email VARCHAR(255),
paid_detail TEXT
);
- 配置价格表
ShipAny 内置了一个价格表组件(Pricing):
components/blocks/pricing/index.tsx
,
通过配置数据,展示价格表,默认支持多语言。
比如默认的英文价格表配置位于:
i18n/pages/landing/en.json
你可以根据自己的需求,修改 pricing
字段下的价格表信息。
- 预览价格表
配置完成后,打开网站首页,可以看到配置好的价格表。
- 测试支付
点击价格表的下单按钮,跳转到支付控制台。
如果是测试环境,可以在 Stripe 测试卡 页面,复制一张测试卡号,
填写到 Stripe 支付表单,进行支付测试。
- 处理支付结果
支付成功后,默认跳转到 /pay-success/xxx
页面,同步处理支付回调。
import Stripe from "stripe";
import { handleOrderSession } from "@/services/order";
import { redirect } from "next/navigation";
export const runtime = "edge";
export default async function ({ params }: { params: { session_id: string } }) {
try {
const stripe = new Stripe(process.env.STRIPE_PRIVATE_KEY || "");
const session = await stripe.checkout.sessions.retrieve(params.session_id);
await handleOrderSession(session);
redirect(process.env.NEXT_PUBLIC_PAY_SUCCESS_URL || "/");
} catch (e) {
redirect(process.env.NEXT_PUBLIC_PAY_FAIL_URL || "/");
}
}
更新完订单状态后,再跳转到配置文件中设置的 NEXT_PUBLIC_PAY_SUCCESS_URL
地址。
默认情况下,支付成功后的处理逻辑,只会更新订单的状态和支付信息。
你可以修改这里的逻辑,加上你自己的业务逻辑。比如发邮件 / 发通知 / 加积分等。
import { findOrderByOrderNo, updateOrderStatus } from "@/models/order";
import Stripe from "stripe";
import { getIsoTimestr } from "@/lib/time";
export async function handleOrderSession(session: Stripe.Checkout.Session) {
try {
if (
!session ||
!session.metadata ||
!session.metadata.order_no ||
session.payment_status !== "paid"
) {
throw new Error("invalid session");
}
const order_no = session.metadata.order_no;
const paid_email =
session.customer_details?.email || session.customer_email || "";
const paid_detail = JSON.stringify(session);
const order = await findOrderByOrderNo(order_no);
if (!order || order.status !== "created") {
throw new Error("invalid order");
}
const paid_at = getIsoTimestr();
await updateOrderStatus(order_no, "paid", paid_at, paid_email, paid_detail);
console.log(
"handle order session successed: ",
order_no,
paid_at,
paid_email,
paid_detail
);
} catch (e) {
console.log("handle order session failed: ", e);
throw e;
}
}
支付结果异步通知
同步处理支付结果是不可靠的,可能出现的情况是在跳转过程中,用户误操作关闭了浏览器页面,导致更新订单状态和支付信息的逻辑没办法执行。用户后续看到的,还是订单未支付成功状态,引起争议。
项目正式上线之前,建议配置支付异步通知。
- Stripe 后台配置 Webhook
本地调试,通过 stripe cli 监听事件:
stripe listen --events checkout.session.completed,invoice.payment_succeeded --forward-to localhost:3000/api/stripe-notify
把用户支付后的 Stripe 回调事件,转发到本地的 ShipAny 服务的 /api/stripe-notify
接口。
- 修改配置文件
上一步在本地监听成功后,会得到一个 webhook signing secret
,把这个参数的值填写到 ShipAny 项目的配置文件中:
STRIPE_WEBHOOK_SECRET = "whsec_cexxx"
- 支付回调测试
在之前配置的价格表页面下单,支付成功后。本地监听的服务会收到回调通知。
- 处理支付回调结果
你可以按照自己的实际需求,修改默认的支付回调处理逻辑:
import Stripe from "stripe";
import { handleOrderSession } from "@/services/order";
import { respOk } from "@/lib/resp";
export const runtime = "edge";
export async function POST(req: Request) {
try {
const stripePrivateKey = process.env.STRIPE_PRIVATE_KEY;
const stripeWebhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
if (!stripePrivateKey || !stripeWebhookSecret) {
throw new Error("invalid stripe config");
}
const stripe = new Stripe(stripePrivateKey);
const sign = req.headers.get("stripe-signature") as string;
const body = await req.text();
if (!sign || !body) {
throw new Error("invalid notify data");
}
const event = await stripe.webhooks.constructEventAsync(
body,
sign,
stripeWebhookSecret
);
console.log("stripe notify event: ", event);
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object;
await handleOrderSession(session);
break;
}
default:
console.log("not handle event: ", event.type);
}
return respOk();
} catch (e: any) {
console.log("stripe notify failed: ", e);
return Response.json(
{ error: `handle stripe notify failed: ${e.message}` },
{ status: 500 }
);
}
}
- 在上线到生产环境之前,你需要在 Stripe 生产环境配置支付回调 Webhook。
支付定制化
- 分组价格表
修改价格表配置文件,添加 groups
数据,配置多个价格分组。并在 pricing.items
下为每一个价格方案,设置一个 group
名称。
配置完成后,点击价格分组,将根据 group
字段,切换显示不同的价格方案。
- 订阅支付
ShipAny 默认支持三种支付方案
- 一次性扣费:
one-time
- 按月订阅扣费:
month
- 按年订阅扣费:
year
你只需要修改价格表配置,把每个价格方案的 interval
字段,设置成上述三个值之一。
同时,按需修改价格(amount) / 积分(credits) / 有效期(valid_months) 等字段。
举例:按月订阅扣费,月付 $99,购买后得到 30 个积分,有效期 1 个月,则核心的价格表配置信息为:
"interval": "month",
"amount": 9900,
"currency": "USD",
"credits": 30,
"valid_months": 1
- 设置折扣码
先在 Stripe 后台,创建折扣码。
再修改 app/api/checkout/route.ts
文件中的 options
参数。
- 允许用户输入自定义的折扣码
options.allow_promotion_codes = true;
- 使用系统默认的折扣码
options.allow_promotion_codes = false;
options.discounts = [
{
coupon: "HAPPY-NEW-YEAR",
},
];
- 不使用折扣
options.allow_promotion_codes = false;
options.discounts = [];
- 人民币支付
首先,在 Stripe 后台,设置支付方式。
开通支付宝和微信支付。
然后,修改价格表配置文件,在每个价格方案下添加一个 cn_amount
字段,即可支持人民币支付。
比如,产品售价,$99,人民币支付价格为 699 元,核心配置信息为:
"amount": 9900,
"cn_amount": 69900,
"currency": "USD"
配置完成后,在价格表下单按钮上方,将会显示一个人民币支付图标。
ShipAny 未能适配所有的支付场景。请根据你的实际业务需求,自行修改
- 价格表组件:
components/blocks/pricing/index.tsx
- 支付下单接口:
app/api/checkout/route.ts
- 支付回调逻辑:
app/api/stripe-notify/route.ts