如何在 BigCommerce 自定义 checkout page 和支付网关?
结论
不能只靠 JavaScript 在 BigCommerce Checkout 中加入支付选项。支付方式不只是页面上的按钮,还必须对应一个已在 BigCommerce 后端注册并启用、能够处理订单的支付提供商。强行在前端插入按钮最多只能把用户带到支付页面,Checkout SDK 无法据此创建支付记录、验证交易或推进订单状态。
通常有三种可行方案:
- 开发正式的 BigCommerce 支付集成,并完成相应的接入或审核流程。
- 使用 BigCommerce 已支持的支付网关。
- 完全接管结账流程,由自己的后端创建订单、发起支付跳转、验证回调并处理购物车。这种方案的实现成本和风险都更高。
无论选择哪种方案,都不能根据用户是否回到网站来判断支付成功,也不应在跳转前清空购物车。支付结果必须由服务端通过回调或主动查询确认。
为什么通过 JavaScript 添加支付选项无法工作
BigCommerce Checkout 显示哪些支付方式,取决于后端返回的可用支付方法。正常的支付流程通常包括:
- BigCommerce 根据店铺配置、币种、地区和订单信息返回可用支付方式。
- Checkout SDK 加载所选支付方式对应的支付策略。
- 后端创建订单或支付会话。
- 支付提供商处理付款。
- BigCommerce 根据支付结果更新订单状态。
通过 DOM 操作添加的单选项或按钮,既没有对应的支付策略,也没有合法的支付方法标识。即使用户可以点击并跳转,BigCommerce 仍然无法:
- 创建订单并建立关联;
- 记录交易;
- 验证付款结果;
- 处理失败、取消或重复付款;
- 更新库存和订单状态;
- 显示正确的订单确认页面。
自定义 Checkout 页面只能改变界面和流程编排,不能让店铺获得注册任意支付网关的能力。
推荐实现:正式支付集成
如果需要让自定义网关和其他支付方式一样出现在 BigCommerce Checkout 中,应将其开发为平台认可的支付提供商,而不是修改 Checkout DOM。
典型流程如下:
Checkout 加载可用支付方式
↓
用户选择 Custom Gateway
↓
BigCommerce/集成后端创建待支付订单
↓
后端向网关创建 payment session
↓
浏览器重定向到网关
↓
网关通过 webhook 通知支付结果
↓
后端验证签名和金额
↓
更新 BigCommerce 订单状态
↓
用户返回订单确认页
店铺能否自行注册支付提供商、需要哪些合作资质,以及目前开放哪些接口,可能取决于店铺方案、结账架构和 BigCommerce 的合作政策。实施前应向 BigCommerce 支持或支付合作团队确认:
- 该店铺是否允许接入自定义 payment provider;
- 是否需要成为支付合作伙伴;
- 是否提供测试店铺和沙箱认证流程;
- 是否支持店铺当前使用的 Optimized One-Page Checkout;
- 是否允许通过 Payments API 处理该支付方式;
- 对 PCI DSS、3-D Secure、退款和争议处理有哪些要求。
BigCommerce Payments API 通常用于处理平台已经识别并配置的支付方式。这不代表调用方可以传入任意自定义网关名称。
备选方案:接管外部支付流程
如果无法开发正式的支付集成,可以搭建自己的结账服务。但不要先将用户带到支付页面,再在付款后临时拼装订单。更可靠的处理顺序是:
- 通过 Checkout API 获取商品、地址、配送、折扣、税费和总金额。
- 在服务端创建待支付订单,并保存
order_id。 - 向支付网关创建支付会话,将
order_id写入不可篡改的商户参考字段。 - 将用户重定向到支付网关。
- 通过 webhook 接收支付结果。
- 校验签名、商户号、币种、金额和交易状态。
- 以幂等方式更新订单。
- 将返回的用户引导到订单结果页。
- 确认订单成功后再处理购物车。
下面是简化的服务端伪代码:
app.post('/checkout/start-payment', async (req, res) => {
const checkout = await loadCheckoutFromBigCommerce(req.body.checkoutId);
// 金额必须从可信的服务端数据获取,不能采用浏览器提交的金额。
const order = await createPendingOrder(checkout);
const paymentSession = await customGateway.createPayment({
amount: checkout.grandTotal,
currency: checkout.currency.code,
merchantReference: String(order.id),
returnUrl: `${APP_URL}/payment/return?order_id=${order.id}`,
webhookUrl: `${APP_URL}/webhooks/custom-gateway`
});
res.json({
redirectUrl: paymentSession.redirectUrl
});
});
支付回调负责最终确认:
app.post('/webhooks/custom-gateway', async (req, res) => {
const event = verifyGatewayWebhook(req);
if (!event.valid) {
return res.sendStatus(401);
}
const order = await loadBigCommerceOrder(event.merchantReference);
if (
event.currency !== order.currency_code ||
event.amount !== order.total_inc_tax
) {
return res.sendStatus(400);
}
if (await transactionAlreadyProcessed(event.transactionId)) {
return res.sendStatus(200);
}
if (event.status === 'paid') {
await markOrderAsPaid(order.id, event.transactionId);
} else if (event.status === 'failed') {
await markOrderPaymentFailed(order.id, event.transactionId);
}
res.sendStatus(200);
});
markOrderAsPaid 和 markOrderPaymentFailed 只是示意名称。实际使用的订单状态 ID、交易记录写入方式以及可调用的 API,需要根据店铺配置和 BigCommerce 当前开放的接口确定,不能假定某个固定状态 ID 始终适用。
返回网站后如何清空购物车
最稳妥的做法是让 BigCommerce 的标准下单流程完成购物车到订单的转换。这样,购物车、订单确认页和结账状态都由平台统一处理,无需手动修改浏览器数据。
如果使用完全自定义的外部流程,应在满足以下两个条件后再清空购物车:
- 服务端已经验证支付成功;
- BigCommerce 订单已经成功创建或更新。
在同域 Storefront 环境中,可以通过 Storefront Cart API 删除指定购物车。示意代码如下:
async function deleteCart(cartId) {
const response = await fetch(
`/api/storefront/carts/${encodeURIComponent(cartId)}`,
{
method: 'DELETE',
credentials: 'same-origin'
}
);
if (!response.ok && response.status !== 404) {
throw new Error(`Unable to delete cart: ${response.status}`);
}
}
不能因为用户访问了返回 URL 就执行删除操作。返回页面可以被伪造,用户也可能在支付完成前关闭页面。建议按以下流程处理:
网关 webhook 确认付款
↓
服务端将订单标记为已支付
↓
返回页向服务端查询订单结果
↓
服务端确认该订单属于当前 checkout/cart
↓
删除或重置购物车
还需要处理多标签页的情况。用户可能在支付期间修改了购物车,如果无条件删除当前购物车,后来加入的商品也可能被一并删除。因此,创建支付会话时应保存原始 cart_id 或 checkout_id,支付成功后只处理与该订单关联的购物车。
返回页不能用于验证付款
下面的做法不安全:
const params = new URLSearchParams(location.search);
if (params.get('status') === 'success') {
await deleteCart(cartId);
}
用户可以修改查询参数。返回页应向自己的后端查询支付状态:
const response = await fetch(
`/api/payment-status?order_id=${encodeURIComponent(orderId)}`,
{ credentials: 'same-origin' }
);
const result = await response.json();
if (result.status === 'paid') {
await deleteCart(result.cartId);
location.assign(result.confirmationUrl);
}
后端应根据 webhook 已验证的交易记录返回结果,不能信任前端提供的 status、金额或交易编号。
实施时还需注意
- 使用幂等键或网关交易号,避免 webhook 重试导致重复处理。
- 核对订单金额、币种、商户号和订单引用,不能只检查“支付成功”字段。
- 明确定义
pending、paid、failed、cancelled和refunded对应的订单状态。 - 不要在浏览器中保存网关密钥或 BigCommerce 管理 API Token。
- 信用卡数据应由合规的托管支付页面或 tokenization 组件采集。
- 用户付款成功后可能不会返回网站,业务结果应以 webhook 为准。
- 用户返回网站时,webhook 可能尚未到达。结果页可以短暂轮询服务端。
- 如果自行通过 Orders API 创建订单,需要确认税费、优惠券、运费、库存和订单邮件没有遗漏,也不会被重复处理。
- Checkout SDK 可以定制结账体验,但不能绕过后端对支付方法的注册和验证。
如果网关必须原生出现在 BigCommerce 的支付选项中,应开发正式的支付提供商集成。如果只是短期接入外部支付页,则应先创建待支付订单,由服务端验证回调,最后再处理购物车。仅修改 Checkout 页面的 JavaScript,无法构成完整可靠的支付集成。
备注:内容仅供参考。
