轻量化低代码一周交付:国外支付渠道集成实战细节

前言
两周前,一个朋友问我能不能帮他的海外 SAAS 项目做 MVP。需求很简单------用户注册、订阅付费、使用工具、管理订单。传统做法至少三周,还要配一个后端开发和运维。
我说:一周交付。
他不信。一周后我把原型链接发过去,他试了试支付流程,沉默了一会儿说:"你这真的是一个人一周做出来的?"
真的。这篇文章就拆解我是怎么用轻量化低代码的思路,在一周内完成国外支付渠道集成的。
一、核心思路:轻量化低代码的集成策略
1.1 什么场景适合一周交付
graph LR
A["标准 SAAS 产品"] --> B["用户管理"]
A --> C["支付集成"]
A --> D["核心业务逻辑"]
A --> E["管理后台"]
B --> F["Clerk / NextAuth"]
C --> G["Stripe Checkout"]
D --> H["业务代码"]
E --> I["shadcn/ui 数据表格"]
style A fill:#8b5cf6,color:#fff
style F fill:#10b981,color:#fff
style G fill:#f59e0b,color:#fff
适合一周交付的产品通常具备这些特征:
- 业务逻辑清晰:不需要复杂的算法或规则引擎
- 支付流程标准:订阅制或一次性购买,不需要定制化账单
- 用户量预期不大:MVP 阶段几百个用户,不需要分布式架构
- 前端为主:核心价值在前端交互,后端只是数据持久化
1.2 一周时间分配
| 天数 | 工作内容 | 产出 |
|---|---|---|
| Day 1 | 项目初始化 + 用户系统 | 注册、登录、个人中心 |
| Day 2 | 核心业务功能 | 产品主体功能 |
| Day 3 | Stripe 支付集成 | Checkout 会话 + Webhook |
| Day 4 | 用户权限系统 | 付费/免费功能隔离 |
| Day 5 | 管理后台 | 订单管理、用户管理 |
| Day 6 | 支付渠道扩展 | PayPal 渠道补充 |
| Day 7 | 部署上线 + 测试 | 正式环境配置 |
二、核心实现:国外支付渠道集成细节
2.1 Stripe 标准集成
Stripe Checkout 是最快的集成方式,但有几个容易被忽略的细节:
typescript
// app/api/stripe/create-checkout/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export async function POST(req: NextRequest) {
const { 方案ID, 用户ID, 用户邮箱 } = await req.json();
const session = await stripe.checkout.sessions.create({
mode: 'subscription',
customer_email: 用户邮箱,
client_reference_id: 用户ID,
line_items: [{ price: 方案ID, quantity: 1 }],
success_url: `${process.env.NEXT_PUBLIC_APP_URL}/dashboard?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
subscription_data: {
metadata: { 用户ID },
},
});
return NextResponse.json({ url: session.url });
}
关键细节:
client_reference_id和subscription_data.metadata都传了用户 ID------前者用于通用关联,后者会透传到订阅对象上。success_url里带了{CHECKOUT_SESSION_ID}模板变量------用户支付完成后,前端可以用这个 ID 查询支付结果,不用等 webhook。
2.2 Webhook 的工程细节
typescript
// app/api/stripe/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
import Stripe from 'stripe';
import { supabase } from '@/lib/supabase';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
export async function POST(req: NextRequest) {
const body = await req.text();
const signature = req.headers.get('stripe-signature');
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, endpointSecret);
} catch {
return NextResponse.json({ error: '签名验证失败' }, { status: 400 });
}
// 先检查事件是否已处理(幂等性)
const { data: 已存在 } = await supabase
.from('webhook_事件')
.select('id')
.eq('stripe_event_id', event.id)
.single();
if (已存在) {
return NextResponse.json({ status: 'already_processed' });
}
// 记录事件
await supabase.from('webhook_事件').insert({
stripe_event_id: event.id,
类型: event.type,
创建时间: new Date(event.created * 1000).toISOString(),
状态: 'processing',
});
try {
switch (event.type) {
case 'checkout.session.completed':
await 处理支付完成(event.data.object);
break;
case 'invoice.payment_succeeded':
await 处理续费成功(event.data.object);
break;
case 'invoice.payment_failed':
await 处理续费失败(event.data.object);
break;
case 'customer.subscription.deleted':
await 处理订阅取消(event.data.object);
break;
}
// 更新事件状态
await supabase.from('webhook_事件').update({ 状态: 'completed' }).eq('stripe_event_id', event.id);
} catch (error) {
await supabase.from('webhook_事件').update({ 状态: 'failed', 错误信息: error.message }).eq('stripe_event_id', event.id);
return NextResponse.json({ error: '处理失败' }, { status: 500 });
}
return NextResponse.json({ status: 'ok' });
}
2.3 PayPal 渠道补充
虽然 Stripe 已经覆盖了信用卡支付,但海外用户习惯于用 PayPal 的比例不低。我用轻量化的方式集成了 PayPal:
typescript
// app/api/paypal/create-order/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const { 方案ID, 金额 } = await req.json();
const accessToken = await 获取PayPal令牌();
const response = await fetch('https://api-m.paypal.com/v2/checkout/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({
intent: 'CAPTURE',
purchase_units: [{
amount: { currency_code: 'USD', value: 金额.toFixed(2) },
custom_id: 方案ID,
}],
}),
});
const order = await response.json();
return NextResponse.json({ orderID: order.id });
}
async function 获取PayPal令牌() {
const auth = Buffer.from(
`${process.env.PAYPAL_CLIENT_ID}:${process.env.PAYPAL_CLIENT_SECRET}`
).toString('base64');
const res = await fetch('https://api-m.paypal.com/v1/oauth2/token', {
method: 'POST',
headers: {
Authorization: `Basic ${auth}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'grant_type=client_credentials',
});
const data = await res.json();
return data.access_token;
}
2.4 统一支付状态管理
有了两个支付渠道后,需要统一管理:
typescript
// lib/payment-status.ts
type 支付渠道 = 'stripe' | 'paypal';
interface 订阅记录 {
用户ID: string;
渠道: 支付渠道;
渠道订阅ID: string;
状态: 'active' | 'cancelled' | 'past_due' | 'expired';
当前方案: string;
下次扣费时间: string | null;
创建时间: string;
}
export async function 同步订阅状态(用户ID: string) {
const { data: 记录 } = await supabase
.from('订阅')
.select('*')
.eq('用户ID', 用户ID)
.single();
if (!记录) return { 层级: 'free', 状态: 'inactive' };
// 检查 Stripe 订阅状态
if (记录.渠道 === 'stripe') {
const subscription = await stripe.subscriptions.retrieve(记录.渠道订阅ID);
return {
层级: 记录.当前方案,
状态: 订阅状态映射(subscription.status),
};
}
return { 层级: 记录.当前方案, 状态: 记录.状态 };
}
function 订阅状态映射(stripe状态: string): string {
const 映射 = {
active: 'active',
past_due: 'past_due',
canceled: 'cancelled',
unpaid: 'expired',
incomplete: 'pending',
};
return 映射[stripe状态] || 'unknown';
}
三、网页系统的工程细节
3.1 定价页面的状态管理
typescript
// app/pricing/page.tsx
'use client';
import { useState } from 'react';
const 方案 = [
{ id: 'free', 名称: '免费版', 价格: 0, 功能: ['3 个项目', '基础功能'] },
{ id: 'pro', 名称: '专业版', 价格: 9.9, stripe价格ID: 'price_pro_monthly', paypal方案ID: 'pro-plan', 功能: ['无限项目', '全部功能', '优先支持'] },
{ id: 'enterprise', 名称: '企业版', 价格: 29.9, stripe价格ID: 'price_enterprise_monthly', paypal方案ID: 'enterprise-plan', 功能: ['无限项目', '全部功能', '专属支持', '自定义域名'] },
];
export default function 定价页() {
const [支付方式, 设置支付方式] = useState<'stripe' | 'paypal'>('stripe');
const [加载中, 设置加载中] = useState<string | null>(null);
const 订阅 = async (方案ID: string) => {
设置加载中(方案ID);
if (支付方式 === 'stripe') {
const res = await fetch('/api/stripe/create-checkout', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ 方案ID, 用户ID: user.id, 用户邮箱: user.email }),
});
const { url } = await res.json();
window.location.href = url;
} else {
const 金额 = 方案.find(p => p.id === 方案ID).价格;
const res = await fetch('/api/paypal/create-order', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ 方案ID, 金额 }),
});
const { orderID } = await res.json();
// 跳转到 PayPal 支付
}
设置加载中(null);
};
return (
<div className="max-w-5xl mx-auto py-12 px-4">
<div className="flex justify-center gap-4 mb-8">
<button onClick={() => 设置支付方式('stripe')}
className={支付方式 === 'stripe' ? 'text-indigo-500 font-bold' : ''}>
Credit Card
</button>
<button onClick={() => 设置支付方式('paypal')}
className={支付方式 === 'paypal' ? 'text-indigo-500 font-bold' : ''}>
PayPal
</button>
</div>
<div className="grid grid-cols-3 gap-6">
{方案.filter(p => p.id !== 'free').map((方案) => (
<div key={方案.id} className="border rounded-xl p-6">
<h3 className="text-lg font-bold">{方案.名称}</h3>
<p className="text-3xl font-bold mt-2">${方案.价格}<span className="text-sm font-normal text-gray-500">/月</span></p>
<ul className="mt-4 space-y-2">
{方案.功能.map((f) => <li key={f}>{f}</li>)}
</ul>
<button
onClick={() => 订阅(方案.id)}
disabled={加载中 === 方案.id}
className="w-full mt-6 py-2 bg-indigo-500 text-white rounded-lg">
{加载中 === 方案.id ? '处理中...' : '开始订阅'}
</button>
</div>
))}
</div>
</div>
);
}
3.2 订单管理后台
typescript
// app/admin/orders/page.tsx
'use client';
import { useEffect, useState } from 'react';
export default function 订单管理() {
const [订单列表, 设置订单列表] = useState([]);
useEffect(() => {
fetch('/api/admin/orders')
.then(r => r.json())
.then(设置订单列表);
}, []);
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-4">订单管理</h1>
<table className="w-full border-collapse">
<thead>
<tr className="border-b">
<th className="text-left p-2">订单ID</th>
<th className="text-left p-2">用户</th>
<th className="text-left p-2">金额</th>
<th className="text-left p-2">渠道</th>
<th className="text-left p-2">状态</th>
<th className="text-left p-2">时间</th>
</tr>
</thead>
<tbody>
{订单列表.map((订单) => (
<tr key={订单.id} className="border-b hover:bg-gray-50">
<td className="p-2 text-sm">{订单.id}</td>
<td className="p-2">{订单.用户邮箱}</td>
<td className="p-2">${订单.金额}</td>
<td className="p-2">{订单.支付渠道}</td>
<td className="p-2">
<span className={订单.状态 === 'paid' ? 'text-green-500' : 'text-yellow-500'}>
{订单.状态}
</span>
</td>
<td className="p-2 text-sm text-gray-500">{订单.创建时间}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
四、一周交付的经验总结
4.1 哪些可以快,哪些不能省
| 可以快的 | 不能省的 |
|---|---|
| 前端页面搭建(Tailwind 组件库) | Webhook 签名验证 |
| 用户系统(用三方服务) | 支付重试逻辑 |
| 管理后台(表格 + 筛选) | 数据备份 |
| API 路由(Serverless Function) | 错误日志记录 |
4.2 关于"快"的真相
一周交付不是奇迹,而是取舍。我砍掉了所有 MVP 阶段不需要的东西------SEO、多语言、邮件模板定制、数据导出。这些等到产品验证了再补。
如果你现在也在做海外市场的产品,我的建议是:先用 Stripe 把支付跑通,再考虑其他支付渠道。大部分用户都用信用卡,Stripe 一个渠道就能覆盖 80% 的支付场景。
剩下的 20%,等产品活下来再说。