如何选择易于集成的 payment gateway?
结论
如果希望尽快、安全地接入信用卡支付,应优先考虑具备以下能力的支付网关:
- 托管收银台或官方支付组件
- 成熟的服务端 SDK
- 完整的测试环境
- Webhook 通知
- 退款、对账和争议处理接口
- 支持目标国家、币种和结算账户
只要服务范围符合要求,Stripe 通常比较容易集成。需要处理复杂的企业支付、多个收单机构或全球本地支付方式时,可以评估 Adyen。如果项目已经使用 PayPal 生态,也可以考虑 Braintree。对于中国大陆业务,应结合经营主体、结算地区和支付场景,选择支持银行卡收单的持牌支付机构或聚合支付服务商。
没有一个 payment gateway 适合所有项目。费率当然需要考虑,但接入难度、支付成功率、风控、结算周期和售后支持,往往更直接地影响日常运营。
先确认支付网关是否适合你的业务
选型前,至少需要确认以下问题:
-
服务地区
服务商是否接受你的公司注册地,能否把资金结算到你的银行账户。 -
客户所在地区
是否支持客户常用的信用卡品牌、币种和本地支付方式。 -
支付模式
业务需要一次性付款、订阅扣款、平台分账,还是保存卡片供以后使用。不同模式可能对应不同的产品和资质要求。 -
合规要求
是否支持 3-D Secure、强客户认证,以及业务所在地要求的身份验证、税务和消费者保护流程。 -
行业限制
数字商品、金融服务、成人内容、博彩和虚拟资产等行业通常受到额外限制。接入前,应查看服务商最新的受限业务政策。 -
费用和结算
除交易费之外,还要了解跨境费、换汇费、退款费、拒付费、最低收费、保证金和结算周期。
服务地区、费率和业务限制可能随时间变化,最终应以服务商当前的官方文档和合同为准。
为什么优先使用托管收银台
最省事的做法通常不是自己开发信用卡输入框,而是使用支付服务商提供的 Hosted Checkout。常见流程如下:
Browser
│
│ 创建订单
▼
Application Server
│
│ 创建 Checkout Session
▼
Payment Gateway
│
│ 返回收银台 URL
▼
Browser redirects to Hosted Checkout
│
│ 用户完成支付
▼
Payment Gateway sends Webhook
│
▼
Application Server marks order as paid
这种方式的好处很直接:
- 信用卡号通常不会经过你的服务器。
- PCI DSS 合规范围相对较小,但不代表完全没有合规责任。
- 3-D Secure、验证码和部分风控流程可以交给服务商处理。
- 支付页面和 SDK 所需的维护工作更少。
- 前端实现错误导致敏感卡片数据泄露的风险更低。
除非业务确实需要高度定制的结账体验,否则不要直接接收或保存完整卡号、有效期和 CVV。
推荐的实施步骤
1. 建立自己的订单记录
用户点击付款后,先在数据库中创建订单,并保存:
- 内部订单号
- 金额
- 币种
- 用户 ID
- 商品信息
- 支付状态
- 网关返回的 session ID 或 payment ID
金额必须由服务端根据商品和优惠规则重新计算,不能直接采用浏览器提交的金额。
const order = await db.orders.create({
userId: currentUser.id,
amount: 2999,
currency: "usd",
status: "pending"
});
金额通常以最小货币单位表示。例如,2999 美分代表 29.99 USD。不过,并非所有币种都有两位小数,具体规则应查看网关文档。
2. 在服务端创建支付会话
下面用 Stripe Checkout 的 Node.js 写法说明基本结构。这段代码主要展示集成方式,实际使用时,应按照当前 SDK 文档安装依赖并核对参数。
import express from "express";
import Stripe from "stripe";
const app = express();
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
app.use(express.json());
app.post("/api/checkout", async (req, res) => {
const order = await loadOrderForCurrentUser(req);
if (!order || order.status !== "pending") {
return res.status(400).json({ error: "Invalid order" });
}
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [
{
price_data: {
currency: order.currency,
unit_amount: order.amount,
product_data: {
name: order.productName
}
},
quantity: 1
}
],
metadata: {
orderId: String(order.id)
},
success_url:
"https://example.com/payment/success?session_id={CHECKOUT_SESSION_ID}",
cancel_url: "https://example.com/payment/cancelled"
});
await saveGatewaySessionId(order.id, session.id);
res.json({ checkoutUrl: session.url });
});
密钥必须保存在服务端环境变量中:
STRIPE_SECRET_KEY=your_secret_key
不要把 secret key 放进前端代码、Git 仓库或任何可以公开下载的配置文件。
3. 从前端跳转到收银台
async function startCheckout(orderId) {
const response = await fetch("/api/checkout", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ orderId })
});
if (!response.ok) {
throw new Error("Unable to create checkout session");
}
const { checkoutUrl } = await response.json();
window.location.href = checkoutUrl;
}
服务端仍然要验证当前用户是否有权支付这笔订单。不能只根据前端传来的 orderId 查找并操作任意订单。
4. 使用 Webhook 确认支付结果
浏览器跳转到 success_url,不能作为付款成功的依据。用户可能在付款后关闭页面,攻击者也可以直接访问成功页面。
更可靠的方式是接收支付网关发送的 Webhook,并验证签名:
app.post(
"/webhooks/stripe",
express.raw({ type: "application/json" }),
async (req, res) => {
let event;
try {
event = stripe.webhooks.constructEvent(
req.body,
req.headers["stripe-signature"],
process.env.STRIPE_WEBHOOK_SECRET
);
} catch {
return res.status(400).send("Invalid webhook signature");
}
if (event.type === "checkout.session.completed") {
const session = event.data.object;
const orderId = session.metadata?.orderId;
await markOrderPaidIdempotently({
orderId,
gatewaySessionId: session.id,
gatewayEventId: event.id
});
}
res.sendStatus(200);
}
);
在 Express 中,Webhook 路由通常需要读取原始请求体。应把它放在全局 express.json() 之前,或者单独调整该路由的中间件,否则签名验证可能失败。
Webhook 处理必须具备幂等性。同一个事件可能被多次发送,系统不能因此重复发货、重复充值或多次发放权益。
比较服务商时不要只看 API
可以从三到五个候选服务商中做一张评分表:
| 评估项 | 建议关注内容 |
|---|---|
| 地区覆盖 | 公司注册地、客户地区和结算银行是否支持 |
| 支付方式 | Visa、Mastercard、American Express 及所需本地方式 |
| 开发体验 | SDK、测试卡、错误信息、Webhook 调试工具 |
| 支付能力 | 一次性付款、订阅、退款、预授权、分账 |
| 安全合规 | PCI DSS、3-D Secure、令牌化和风控能力 |
| 成本 | 交易、跨境、换汇、退款、拒付及月费 |
| 资金管理 | 结算周期、保证金、提现币种和对账文件 |
| 运营支持 | 中文支持、响应时间、争议和拒付协助 |
| 稳定性 | 状态页面、故障通知和接口变更政策 |
如果两个网关的功能比较接近,可以各做一个最小原型,实际测试创建支付、支付失败、退款和 Webhook 重试等流程。文档简单易懂,并不代表异常处理和对账同样容易。
上线前必须检查的事项
- 全站使用 HTTPS。
- 不记录卡号、CVV、支付密钥或完整的支付请求。
- 测试环境和生产环境使用不同的密钥。
- 限制密钥权限,并建立轮换机制。
- 由服务端校验金额、币种、订单归属和订单状态。
- 使用 idempotency key,防止网络重试造成重复支付。
- 验证所有 Webhook 的签名。
- 对 Webhook 事件去重,并允许失败后安全重试。
- 不要只在浏览器中保存订单 ID、支付状态或金额。
- 为退款、部分退款、拒付和支付超时设计相应状态。
- 保存网关交易 ID,方便对账和客服查询。
- 正式上线前,完成服务商要求的身份、企业和银行账户验证。
实际选型建议
如果普通 Web 应用只需要一次性信用卡付款,可以先采用”托管收银台 + 服务端创建会话 + Webhook 确认结果”的方案。只要目标地区和业务类型在服务范围内,Stripe 通常值得优先测试。
如果业务覆盖大量国家,需要接入多种本地支付方式、复杂的收单路由或企业级资金管理,可以把 Adyen 列入候选名单。已经深度使用 PayPal 的项目,可以同时测试 Braintree。业务集中在特定国家或地区时,本地支付机构可能更适合当地的支付成功率、结算和客服需求。
选型仍要回到真实的业务条件。先排除无法服务公司注册地、所属行业或结算账户的提供商,再比较集成成本、支付成功率、综合费率和运营支持。这样的结果通常比单纯比较哪个 API 最简单更可靠。
备注:内容仅供参考。
