TUTORIAL

第三方支付接口对接全流程

第三方支付接口对接全流程

第三方支付接口对接全流程

电商型官网几乎都离不开在线支付。微信支付、支付宝是国内两大主流渠道,它们的基本对接思路高度相似:商户后台下单 → 拉起支付 → 用户付款 → 异步回调通知结果。但支付涉及真金白银,任何一个环节出错都可能导致掉单、重复扣款,容不得半点马虎。本篇以尧图项目实战为基础,把支付对接的全流程拆给你看。

一、统一下单与签名生成

支付的起点是"下单"。用户在前端点击付款,你的服务器向支付平台发起统一下单请求,带上订单号、金额、商品描述、回调地址等参数。支付平台校验通过后返回一个预支付凭证(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% 的情况(比如用户已扣款但订单被关闭),需要人工对账——定期拉取支付平台的对账单,与本地订单逐笔核对,发现差异人工处理。支付对接没有"一劳永逸",监控、对账、兜底机制缺一不可,这套流程在尧图多个电商官网稳定运行多年,值得参考。

返回教程列表