电商型官网几乎都离不开在线支付。微信支付、支付宝是国内两大主流渠道,它们的基本对接思路高度相似:商户后台下单 → 拉起支付 → 用户付款 → 异步回调通知结果。但支付涉及真金白银,任何一个环节出错都可能导致掉单、重复扣款,容不得半点马虎。本篇以尧图项目实战为基础,把支付对接的全流程拆给你看。
一、统一下单与签名生成
支付的起点是"下单"。用户在前端点击付款,你的服务器向支付平台发起统一下单请求,带上订单号、金额、商品描述、回调地址等参数。支付平台校验通过后返回一个预支付凭证(prepay_id),前端用它拉起收银台。这里的关键是签名——所有请求参数要按规则拼接、用商户密钥加密生成签名,防止参数被篡改。
// 微信支付统一下单(简化版)
function createOrder(orderNo, amount, openid) {
const params = {
appid: 'wx1234567890',
mch_id: '1600000001',
nonce_str: randomStr(32),
body: '尧图建站服务套餐',
out_trade_no: orderNo, // 商户订单号
total_fee: amount, // 金额:分
spbill_create_ip: getClientIp(),
notify_url: 'https://ldpk.cn/pay/wechat/notify',
trade_type: 'JSAPI',
openid: openid
};
// 生成签名:参数按key排序拼接 + 密钥 → MD5
params.sign = generateSign(params, API_KEY);
const xml = toXml(params);
const resp = http.post('https://api.mch.weixin.qq.com/pay/unifiedorder', xml);
return parseXml(resp); // 返回 prepay_id
}
// 签名算法
function generateSign(params, key) {
const sorted = Object.keys(params).sort();
const str = sorted.map(k => `${k}=${params[k]}`).join('&') + `&key=${key}`;
return md5(str).toUpperCase();
}
签名生成要注意三个细节:第一,参数值要 URL 编码但签名计算用原值;第二,sign 字段本身不参与签名;第三,金额单位是"分"而非"元",整数传递避免浮点精度问题。尧图曾遇到过金额传 0.01 元结果被当成 1 分钱扣款失败的案例,根因就是单位搞错。
二、异步回调与验签
用户付款成功后,支付平台会向你配置的 notify_url 发送异步通知。这是更新订单状态的关键环节,但也是风险点——万一有人伪造回调通知呢?所以必须验签:用同样的签名算法校验回调内容,确认它确实来自支付平台。验签通过后再更新订单,否则直接忽略。
// 支付回调处理
app.post('/pay/wechat/notify', (req, res) => {
const data = parseXml(req.body);
// 1. 验签:校验签名是否合法
const sign = data.sign;
delete data.sign;
if (generateSign(data, API_KEY) !== sign) {
return res.send(failXml('签名校验失败'));
}
// 2. 幂等校验:订单是否已处理过
const order = db.findOrder(data.out_trade_no);
if (order.status === 'paid') {
return res.send(successXml()); // 已处理,直接返回成功
}
// 3. 金额校验:回调金额与订单金额是否一致
if (order.amount !== data.total_fee) {
log.error('金额不一致', order.amount, data.total_fee);
return res.send(failXml('金额不一致'));
}
// 4. 更新订单状态
db.updateOrder(order.id, {
status: 'paid',
trade_no: data.transaction_id, // 支付平台流水号
pay_time: now()
});
// 5. 返回成功,告诉支付平台"我收到了"
res.send(successXml());
});
回调处理有三个铁律必须遵守:一是验签,不验签的回调等于裸奔;二是幂等,支付平台可能重发多次回调,必须保证重复处理不会重复发货;三是金额校验,防止攻击者构造一个真实签名但金额是 1 分的假订单。另外,回调处理要尽快返回成功(5 秒内),否则支付平台会认为通知失败而重试,增加系统负担。
三、订单状态同步与掉单处理
异步回调虽然可靠,但并非 100% 万无一失——网络抖动、服务器重启都可能导致回调丢失。所以不能只依赖回调,必须主动查单兜底。做法是:订单创建后进入"待支付"状态,超过 30 分钟未收到回调,主动调用支付平台的查询接口确认状态。
// 定时任务:每5分钟扫描待支付订单
cron.schedule('*/5 * * * *', () => {
const pendingOrders = db.query(`
SELECT * FROM orders
WHERE status = 'pending'
AND create_time < NOW() - INTERVAL 30 MINUTE
LIMIT 100`);
for (const order of pendingOrders) {
// 主动查询支付平台
const result = wechat.queryOrder(order.order_no);
if (result.trade_state === 'SUCCESS') {
db.updateOrder(order.id, { status: 'paid', trade_no: result.transaction_id });
} else if (result.trade_state === 'CLOSED' || result.trade_state === 'NOTPAY') {
// 长时间未支付,关闭订单释放库存
db.updateOrder(order.id, { status: 'closed' });
wechat.closeOrder(order.order_no);
}
}
});
主动查单 + 关闭超时订单这套组合拳,能解决 99% 的掉单问题。剩下 1% 的情况(比如用户已扣款但订单被关闭),需要人工对账——定期拉取支付平台的对账单,与本地订单逐笔核对,发现差异人工处理。支付对接没有"一劳永逸",监控、对账、兜底机制缺一不可,这套流程在尧图多个电商官网稳定运行多年,值得参考。