从 PDP 按钮到订单生成,中间发生了什么?一个前端工程师需要知道的支付链路全貌
这篇文章是我梳理支付这条链路的过程,目标是:读完之后,你能知道怎么在一个已有的商品详情页或课程详情页里,接入一个完整的支付模块。
先建立心智模型:支付链路全貌
在写任何代码之前,先把这张图看懂:
前端(你的页面) 你的后端 支付网关(Stripe) 银行
| | | |
| 1. 点击"购买" | | |
|----------------->| | |
| | 2. 创建支付意向 | |
| |------------------>| |
| |<-- client_secret --| |
|<-- client_secret-| | |
| | | |
| 3. 拉起支付界面 | | |
|------------------------------------ >| |
| | | 4. 和银行通信 |
| | |----------------->|
| | |<-- 授权结果 ------|
|<-- 重定向回页面--| | |
| | | |
| |<-- 5. Webhook 通知(payment.succeeded)
| | 6. create order | |
| | 7. 扣款(Capture)|----------------->|
几个关键认知:
前端不碰卡号。 信用卡信息直接进入 Stripe 的表单,你的代码完全接触不到。这不是限制,是设计——如果卡号经过你的服务器,你需要通过 PCI DSS 合规认证,成本极高。Stripe 帮你把这个问题消灭了。
订单在支付成功之后才创建。 不是用户点击"购买"时创建,而是收到 Stripe 的 Webhook 通知后才创建。钱到了,才有订单——订单是交易完成的凭证,不是交易的开始。
授权和扣款是两步。 Stripe 先向银行确认"这张卡有没有这笔钱"(Authorization),真正把钱划走(Capture)可以延迟到发货时。这对实物电商很重要,对数字商品通常合并成一步。
前端只需要做三件事
理解了全貌,前端的工作其实很聚焦:初始化 SDK、拉起支付、处理回调。
第一件事:初始化 Stripe SDK
// 环境:Next.js,需安装 @stripe/stripe-js
// 在应用入口处初始化,避免重复加载
import { loadStripe } from '@stripe/stripe-js'
// NEXT_PUBLIC_ 前缀表示这是可以暴露给前端的公钥
// 私钥只能在后端使用,绝对不能出现在前端代码里
const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_KEY!)
loadStripe 会异步加载 Stripe 的 JS 文件,返回一个 Promise。把它定义在模块级别(组件外部),确保整个应用只初始化一次。
第二件事:拉起支付界面
点击"购买"按钮时,需要先告诉后端"我要买什么",后端创建支付意向,前端拿到凭证后拉起 Stripe:
// 环境:React 组件
// 场景:PDP 或课程详情页的购买按钮
async function handlePurchase(productId: string, price: number) {
try {
// 第一步:让后端创建支付意向
// 后端调用 Stripe API,返回 sessionId
const response = await fetch('/api/checkout/create-session', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId, price }),
})
if (!response.ok) {
throw new Error('创建支付会话失败')
}
const { sessionId } = await response.json()
// 第二步:跳转到 Stripe 托管的支付页面
const stripe = await stripePromise
const { error } = await stripe!.redirectToCheckout({ sessionId })
// 只有跳转失败才会执行到这里
if (error) {
console.error('跳转支付页面失败:', error.message)
}
} catch (err) {
console.error('支付流程出错:', err)
}
}
// 在组件里使用
function ProductDetailPage({ product }: { product: Product }) {
return (
<div>
<h1>{product.name}</h1>
<p>¥{product.price}</p>
<button onClick={() => handlePurchase(product.id, product.price)}>
立即购买
</button>
</div>
)
}
第三件事:处理支付回调
用户在 Stripe 完成支付后,Stripe 会把用户重定向回你配置的页面。通常需要两个 URL:
// 环境:Next.js API Route(后端部分)
// 场景:创建 Stripe Checkout Session
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function POST(request: Request) {
const { productId, price } = await request.json()
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency: 'cny',
product_data: { name: productId },
unit_amount: price * 100, // Stripe 使用最小货币单位(分)
},
quantity: 1,
},
],
mode: 'payment',
// 支付成功后跳回的页面
success_url: `${process.env.NEXT_PUBLIC_BASE_URL}/order/success?session_id={CHECKOUT_SESSION_ID}`,
// 用户取消支付后跳回的页面
cancel_url: `${process.env.NEXT_PUBLIC_BASE_URL}/checkout/cancelled`,
})
return Response.json({ sessionId: session.id })
}
成功页面读取 session_id,展示订单确认信息:
// 环境:Next.js,pages/order/success.tsx
// 场景:支付成功回调页面
export default function OrderSuccess() {
const router = useRouter()
const { session_id } = router.query
// 用 session_id 查询订单状态,展示确认信息
// 真正的订单创建发生在后端 Webhook 里,不在这里
return (
<div>
<h1>支付成功</h1>
<p>订单正在处理中,稍后会收到确认邮件</p>
</div>
)
}
注意:这个页面只用来展示"支付成功"的反馈,不要在这里 create order。原因是用户可能关掉页面、网络中断,导致这个页面根本不会被执行。真正可靠的 order 创建,必须在 Webhook 里做。
Webhook:支付链路里最容易被忽视的环节
很多教程到"跳转成功页"就结束了,但这是不完整的。
Webhook 是 Stripe 主动通知你的后端"支付成功了"的机制。不管用户的浏览器发生了什么,这个通知都会到达:
// 环境:Next.js API Route
// 场景:接收 Stripe Webhook 通知
import Stripe from 'stripe'
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!)
export async function POST(request: Request) {
const body = await request.text()
const signature = request.headers.get('stripe-signature')!
let event: Stripe.Event
try {
// 验证这个请求确实来自 Stripe,而不是伪造的
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET!
)
} catch (err) {
return new Response('Webhook 签名验证失败', { status: 400 })
}
// 根据事件类型处理
switch (event.type) {
case 'checkout.session.completed':
const session = event.data.object as Stripe.Checkout.Session
// 在这里 create order,触发库存扣减、发送确认邮件等
await createOrder(session)
break
case 'payment_intent.payment_failed':
// 支付失败,通知用户
await handlePaymentFailure(event.data.object)
break
}
return new Response('ok', { status: 200 })
}
两种接入方式:跳转 vs 内嵌
上面的代码用的是 Hosted Checkout——用户跳到 Stripe 的页面填卡。这是集成最简单的方式,适合快速上线。
另一种方式是 Stripe Elements——把卡号表单嵌在你自己的页面里,用户不需要跳出去:
// 环境:React,需安装 @stripe/react-stripe-js
// 场景:在自己页面内嵌入支付表单
import { Elements, CardElement, useStripe, useElements } from '@stripe/react-stripe-js'
function CheckoutForm() {
const stripe = useStripe()
const elements = useElements()
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
if (!stripe || !elements) return
const cardElement = elements.getElement(CardElement)!
// confirmCardPayment 把卡信息直接发给 Stripe,不经过你的服务器
const { error, paymentIntent } = await stripe.confirmCardPayment(
clientSecret, // 从后端获取
{ payment_method: { card: cardElement } }
)
if (error) {
console.error('支付失败:', error.message)
} else if (paymentIntent.status === 'succeeded') {
// 支付成功,等待 Webhook 创建订单
}
}
return (
<form onSubmit={handleSubmit}>
<CardElement /> {/* Stripe 的安全 iframe,你无法读取其中的卡号 */}
<button type="submit">确认支付</button>
</form>
)
}
// 用 Elements Provider 包裹
function PaymentPage({ clientSecret }: { clientSecret: string }) {
return (
<Elements stripe={stripePromise} options={{ clientSecret }}>
<CheckoutForm />
</Elements>
)
}
怎么选:
| Hosted Checkout | Stripe Elements | |
|---|---|---|
| 集成复杂度 | 低,几行代码 | 中,需要处理表单状态 |
| 用户体验 | 跳出页面,有跳出感 | 留在你的页面,体验流畅 |
| 样式定制 | 有限 | 完全可定制 |
| 适合场景 | 快速上线、MVP | 对体验要求高的产品 |
国内场景:支付宝和微信支付
如果你的用户在国内,流程是一样的,只是 SDK 和 API 名字不同:
Stripe 概念 → 国内对应
─────────────────────────────────────────
loadStripe() → wx.config() / AlipayJSBridge.ready()
Payment Intent → 统一下单接口(prepay_id / trade_no)
redirectToCheckout → wx.requestPayment() / 跳转支付宝收银台
Webhook → 支付结果异步通知(需要验签)
client_secret → prepay_id(微信)/ out_trade_no(支付宝)
国内支付比 Stripe 复杂的地方在于多端适配——同一个支付功能,在 PC 浏览器、手机浏览器、微信内 H5、微信小程序里,拉起支付的方式都不同:
// 场景:判断环境,选择对应的支付拉起方式
function launchWechatPay(prepayId: string) {
const ua = navigator.userAgent.toLowerCase()
const isWechat = ua.includes('micromessenger')
const isMiniProgram = window.__wxjs_environment === 'miniprogram'
if (isMiniProgram) {
// 小程序环境
wx.requestPayment({ /* 支付参数 */ })
} else if (isWechat) {
// 微信浏览器,使用 JSAPI
WeixinJSBridge.invoke('getBrandWCPayRequest', { /* 参数 */ })
} else {
// 普通浏览器,跳转收银台或展示二维码
window.location.href = `https://wx.tenpay.com/...`
}
}
这也是国内电商前端比较有挑战的部分——同一套支付逻辑要处理很多种环境。
小结
从 PDP 按钮到订单生成,前端实际需要理解和处理的只有几个关键点:
- 不碰卡号——所有敏感信息交给 SDK 处理
- 后端创建支付意向——前端只是发起请求,拿凭证
- Webhook 才是可靠的订单创建时机——不要依赖前端回调
- 授权和扣款是两步——理解这个,才能处理各种异常情况
支付链路本身不复杂,复杂的是各种异常情况的处理:支付超时、重复支付、退款、对账。这些是下一个层次的话题,也是真实电商项目里花时间最多的地方。
还没想清楚的地方
支付完成的那一刻,是整条电商链路里信息密度最高的时刻——用户真实购买意图第一次被确认,品类偏好、价格敏感度、购买时机全部显现。
这份数据,AI 能做什么?
是在支付成功页推荐关联商品,还是在 Webhook 触发时更新用户画像,还是用来预测下一次复购时机?
参考资料
- Stripe Checkout 官方文档 — Hosted Checkout 快速上手
- Stripe Elements 官方文档 — 内嵌表单集成指南
- Stripe Webhooks — Webhook 接入与签名验证
- 微信支付开发文档 — 微信支付各场景接入说明
- 支付宝开放平台 — 支付宝网页支付接入文档