Node.js 对接支付宝小程序支付:alipay.trade.create 完整指南
做小程序支付最怕的不是写代码而是被一堆密钥、签名和异步通知按在地上摩擦。以前我处理公众号支付时还算顺手换成支付宝小程序之后才发现除了文档绕、示例少还会经常碰到“验签失败”“订单创建成功但前端拉起不了收银台”这类匪夷所思的情况。这一篇就围绕 Node.js 环境下的 alipay-sdk 对接 alipay.trade.create 这个接口把从密钥配置到下单再到回调验签的完整链路拆开讲一遍包括我在真实项目里踩过的坑和验证过的写法。如果你正准备在支付宝小程序里接一个支付功能或者已经接了一半卡在莫名其妙的地方这篇文章可以直接当排查手册用。我会把为什么用 alipay.trade.create、SDK 怎么初始化、参数怎么填、前端怎么拉起收银台以及遇到报错怎么定位全部按实操顺序串起来。1. 先搞清楚 alipay.trade.create 到底是干嘛的1.1 三种下单接口怎么选支付宝开放平台里和“下单”相关的接口有好几个最容易混淆的就是alipay.trade.create、alipay.trade.precreate和alipay.trade.page.pay。我刚开始就选错了接口去用了 page.pay结果小程序里根本没法直接用。三个接口的核心区别可以这样记接口使用场景核心特点alipay.trade.create小程序、App 等需要提前创建订单的场景需要知道买家支付宝用户标识buyer_id返回支付宝交易号alipay.trade.precreate扫码支付、线下收银生成二维码链接用户扫码后付款alipay.trade.page.payPC 网站、H5 跳转支付返回一段表单或跳转链接适合网页端对于支付宝小程序来说用户已经在支付宝 App 内完成了登录授权后端比较容易拿到buyer_id所以官方推荐先调alipay.trade.create创建订单拿到支付宝交易号trade_no之后再在小程序端通过my.tradePay拉起原生收银台。1.2 为什么必须走“先创建、再支付”两步小程序端不能像网页那样直接跳转支付页面所以必须在后端先把订单“锁定”拿到一个支付宝侧的交易号前端再用这个交易号唤起收银台。这套逻辑和微信支付的 prepay 订单很相似先有预支付单再拉起收银台。alipay.trade.create的作用就是这层“预支付”它本身不扣钱只是让支付宝侧记录一笔待支付订单。真正扣款发生在用户确认支付之后而支付结果则通过异步通知告诉你的后端。这个模型看似多了一步实际上带来一个好处后端可以完全控制订单的生成时机、金额和关联业务数据而不是让前端直接把订单内容拼接出来安全性会好很多。2. 环境准备与SDK接入2.1 开通支付能力和配置密钥先到支付宝开放平台创建一个小程序应用提交审核通过后在功能列表里开通“当面付”或者“电脑网站支付”等对应的能力。这里要注意支付能力不会自动生成必须确认开通状态否则后面下单接口会直接返回“产品能力未开通”。接下来是密钥配置这也是新手最容易翻车的地方。支付宝现在的签名方式默认是 RSA2需要你自己生成一对 RSA 密钥openssl genrsa -out app_private_key.pem 2048 openssl rsa -in app_private_key.pem -pubout -out app_public_key.pem生成之后用文本编辑器打开app_public_key.pem把内容完整复制到支付宝开放平台的“应用公钥”输入框里保存后平台会返回一串“支付宝公钥”。这串支付宝公钥要复制下来存好它和你的私钥是配对关系后面 SDK 初始化时会用到。注意应用公钥是你的支付宝公钥是支付宝的两者不能搞混。我见过太多人把应用公钥当成支付宝公钥传给 SDK结果签名校验永远过不了。密钥建议用环境变量或者配置文件管理不要硬编码到代码里提交到仓库。私钥文件里的换行符也要保留如果用环境变量注入记得把\n替换成真实的换行。2.2 安装 alipay-sdk 并初始化Node.js 生态里支付宝官方提供了 alipay-sdk直接通过 npm 安装npm install alipay-sdk --save安装完成后初始化方式非常简单const AlipaySdk require(alipay-sdk).default; const fs require(fs); const alipaySdk new AlipaySdk({ appId: process.env.ALIPAY_APP_ID, privateKey: fs.readFileSync(./app_private_key.pem, utf8), alipayPublicKey: process.env.ALIPAY_PUBLIC_KEY, // 注意是支付宝公钥 gateway: https://openapi.alipay.com/gateway.do, signType: RSA2, charset: utf-8 }); module.exports alipaySdk;这段代码里最关键的是alipayPublicKey。如果是在沙箱环境调试沙箱的支付宝公钥和线上不一样不要搞混。signType确定用 RSA2这是目前最安全的配置别再用 RSA1 了。3. 核心参数与签名机制3.1 bizContent 参数对照表alipay.trade.create的业务参数都放在bizContent对象里下面是必填和常用字段参数类型是否必填说明out_trade_noString是商户侧订单号必须唯一建议用时间戳随机数生成total_amountString是订单总金额单位是元保留两位小数subjectString是订单标题展示在用户账单/收银台上buyer_idString条件必填买家支付宝用户标识小程序支付时建议传notify_urlString否支付结果异步通知地址必须公网可访问seller_idString否卖家支付宝账号一般不需要传默认是APPID对应主体total_amount是最容易出问题的字段。数据库里存的是“分”比如 100 分传给支付宝时一定要转成1.00。不要直接除以 100 后用了浮点数JavaScript 里100 / 100没问题但199 / 100可能变成1.9899999999999998最后签名用的字符串和实际参数对不上支付宝直接拒绝。3.2 签名和验签到底是怎么回事支付宝的所有请求参数都会参与签名SDK 内部用你的私钥对请求内容生成签名支付宝收到后用你的应用公钥验签确认请求确实来自你。反过来支付宝返回的响应和异步通知也会用支付宝私钥签名你用支付宝公钥验签。听起来复杂但 alipay-sdk 把这些都封装好了。你要做的只是保证初始化时传对私钥和支付宝公钥剩下的签名、验签、拼接参数都是 SDK 的事。不过有一个细节需要知道错误信息里的“验签失败”可能来自两个方向。一个是你的应用公钥没有正确配置到开放平台另一个是你把支付宝公钥配置错了。排查时先用支付宝官方提供的工具手动验签一次能快速确认是哪一边的问题。4. 手把手写完一个可跑通的创建订单接口4.1 后端创建订单代码假设你用的是 Express创建一个下单接口大概是这样的const express require(express); const router express.Router(); const alipaySdk require(../utils/alipaySdk); const { createOrderNo, generateAuthCode } require(../utils/orderHelper); router.post(/create, async (req, res) { try { const { userId, amount, subject } req.body; // amount 传过来的是分这里转成元的字符串 const totalAmount (amount / 100).toFixed(2); const outTradeNo createOrderNo(); const result await alipaySdk.exec(alipay.trade.create, { bizContent: { out_trade_no: outTradeNo, total_amount: totalAmount, subject: subject, buyer_id: userId, product_code: GENERAL_WITHHOLDING_P }, notify_url: https://api.yourdomain.com/pay/notify }); if (result.code 10000) { res.json({ success: true, trade_no: result.trade_no, out_trade_no: outTradeNo }); } else { res.json({ success: false, msg: result.sub_msg || result.msg }); } } catch (err) { console.error(创建订单失败, err); res.status(500).json({ success: false, msg: err.message }); } }); module.exports router;这里有几个细节值得展开讲。第一result.code 10000是支付宝网关返回成功时的标准状态码只有这个值能当作“下单成功”的依据。很多开发者只看 HTTP 200但 HTTP 200 只代表请求到了支付宝业务执行失败也照样返回 200。第二buyer_id怎么获取。小程序端通过my.getAuthCode拿到授权码后端再调用alipay.system.oauth.token换取用户唯一标识。有些开发者图省事从my.getUserInfo里拿 userId那个不是支付宝用户标识签名也对不上最后会报“买家不存在”。4.2 小程序前端拉起收银台后端创建订单成功之后返回给小程序两个关键字段支付宝交易号trade_no和商户订单号out_trade_no。小程序端用my.tradePay拉起收银台my.tradePay({ tradeNO: tradeNo, success: (res) { // res.resultCode 表示收银台结果 // 9000 表示支付成功但最终以服务端通知为准 if (res.resultCode 9000) { // 点击“完成”按钮 } }, fail: (err) { console.log(支付失败, err); } });这里要提醒一个原则前端支付回调永远只是体验层的不能作为订单状态更新的依据。用户可能支付成功但网络断了可能支付成功但前端没收到回调也可能用户支付完就杀掉了小程序。真正的状态更新必须以后端异步通知为准。4.3 金额换算和订单号生成金额换算前面提到了再补充一个稳妥的写法function fenToYuan(fen) { return (fen / 100).toFixed(2); }不要用parseFloat之后直接转字符串toFixed(2)能保证结果始终是两位小数。订单号生成建议用时间戳加随机数组合function createOrderNo() { const timestamp Date.now().toString(); const random Math.floor(Math.random() * 1000000).toString().padStart(6, 0); return timestamp random; }订单号不要用自增数字容易被撞单。也不要包含特殊字符支付宝对out_trade_no的字符集有要求最好只用数字和字母。5. 异步通知回调验签5.1 通知参数与验签方法支付完成后支付宝会向notify_url发一个 POST 请求里面带着订单状态、交易号、买家标识等一堆参数。这一步必须做验签否则任何人都可以伪造通知来把订单改成已支付。alipay-sdk 提供了验签方法const alipaySdk require(../utils/alipaySdk); router.post(/notify, async (req, res) { const params req.body; const isValid alipaySdk.checkNotifySign(params); if (!isValid) { res.send(fail); return; } const { trade_status, out_trade_no, trade_no, total_amount } params; // 只有交易成功才处理其他状态可以忽略 if (trade_status TRADE_SUCCESS) { // 更新本地订单状态为已支付 // 注意这里要做幂等处理同一个通知可能发多次 await updateOrderPaid(out_trade_no, trade_no, total_amount); } // 必须返回 success支付宝才会停止重试 res.send(success); });checkNotifySign会自动把支付宝返回的sign字段和其他业务参数拼起来验签。这里要注意 Express 的 body parser 配置支付宝的通知请求是application/x-www-form-urlencoded别用express.json()去解析不然验签时取到的参数对不上。5.2 通知处理的幂等设计支付宝的异步通知有个特点不保证只发一次。如果网络抖动或者你的接口返回不是success支付宝会重复发送通知。所以回调处理必须幂等最稳妥的做法是用out_trade_no去数据库里查订单如果已经是已支付状态直接返回success不要重复修改数据。我见过一个案例同事在回调里没做幂等用户支付成功后因为重复通知订单被更新了两次金额也叠加了最后对账对不上。这个问题很隐蔽因为测试环境很少触发重复通知线上量大了之后才开始出现。6. 常见问题与排查实录6.1 问题速查表现象大概率原因处理方式报错“验签失败”支付宝公钥配置错误检查应用公钥和支付宝公钥是否弄反返回“产品能力未开通”应用没开通支付能力到开放平台确认支付功能状态下单成功但 my.tradePay 没反应trade_no 字段没取对确认后端返回的是支付宝交易号而不是商户订单号金额显示不对分和元转换出错统一在接口层用 toFixed(2)异步通知一直收不到notify_url 不可公网访问用内网穿透工具或正式域名排查通知验签失败body 解析格式不对确认使用 urlencoded 解析6.2 几个容易踩的坑第一个坑是童话般的沙箱环境。沙箱环境和线上环境的应用、密钥、公钥都不通用很多人在沙箱跑通了之后切线上忘记换支付宝公钥结果线上一直验签失败。建议在代码里把环境和密钥配置拆开不要让“换环境”这件事依赖人工记忆。第二个坑是时区和时间格式。create 接口虽然没有强制要求时间参数但如果你在某些场景下传了time_expire一定要用yyyy-MM-dd HH:mm:ss格式并且是北京时间。否则前面创建订单看着成功后面用户端会出现订单无法支付的情况。第三个坑是product_code参数。不同接口对于product_code的要求不同alipay.trade.create在小程序场景下有时不需要传但某些子渠道会要求必须传。我建议先不传跑一版如果报“产品码不存在”再补不要盲目复制网上代码。第四个坑是日志。支付链路中支付宝返回错误时sub_code和sub_msg才是真正解决问题的关键。我每次下单接口都会把完整的响应参数打日志尤其是code、sub_code、sub_msg这三个字段。很多网上找不到答案的问题就是靠日志里的sub_code慢慢查出来的。7. 一些实操心得最后分享几个我对支付模块的真实体会。第一支付模块一定要有“对账视角”。不要只盯着创建订单和回调成功这两个点要记录每一笔订单的创建时间、下单参数、通知时间和最终状态。出了问题能把时间轴拉出来看比翻代码猜原因高效得多。第二异步通知的重试机制不是 bug而是一种保护。如果你的接口返回非success支付宝会间隔一段时间重试最多重试几次后停止。这意味着你必须有日志记录每一次通知的到达时间不然很难判断到底是通知丢了还是回调逻辑挂了。第三金额精度问题值得在项目初期就定好规范。我现在的约定是所有金额在存储和计算时都用分只在传给支付宝和展示给用户时转换成分转元字符串。这样做的好处是后续即使加退款、加优惠也不会出现浮点误差。第四如果是刚接触支付宝小程序支付建议先用官方沙箱把一个最小闭环跑通再接入真实商品和真实金额。支付不是“写完代码就完了”的事它涉及资金流转多一份谨慎都不为过。这些经验是我在多个项目里反复踩坑之后沉淀下来的。如果你正准备对接alipay.trade.create按这个流程走一遍大部分问题都能提前避开。做了支付之后你会明白真正难的往往不是支付本身而是那些藏在签名、金额、回调里的细节。