(Nodejs or Bun) + 支付宝支付

发布时间:2026/7/7 6:45:00

(Nodejs or Bun) + 支付宝支付 目录Nodejs版Bun版坑1异步通知老不通知原因1内网穿透的域名不安全原因2没有在控制台配置原因3该死的内网穿透有广告Nodejs版这我没什么好说的直接上sdksdk就已经包含了很多操作了。​支付宝 Nodejs SDKBun版为什么Bun运行时环境不能用Nodejs呢这很好理解就是我们无法知道Bun是否对Nodejs有彻底的支持。所以我建议手搓来进行调用支付宝的一些功能。首先声明一下这里是一个沙箱版本的也就是不是正式版的。不过也差不多了。这里拥有详细的步骤https://opendocs.alipay.com/common/02kkv7总而言之呢就是你没有认证也可以去模拟支付宝的支付。然后这里我把写好的案例代码已经放进了这里大家可以自行下载查看。// config.ts export default { // 你的沙箱APPID appId: , // 应用私钥沙箱 → 开发信息 → 应用私钥PKCS1格式 privateKey: -----BEGIN RSA PRIVATE KEY----- 应用私钥 -----END RSA PRIVATE KEY-----, // 支付宝公钥沙箱 → 开发信息 → 支付宝公钥 alipayPublicKey: -----BEGIN PUBLIC KEY----- 支付宝公钥 -----END PUBLIC KEY-----, // 沙箱网关 gateway: https://openapi-sandbox.dl.alipaydev.com/gateway.do, // 回调地址需要能被支付宝访问本地开发可用 SakuraFrp内网穿透 returnUrl: http://localhost:3000/alipay/return, // 这里换成安全域名可访问的 notifyUrl: http://localhost:3000/alipay/notify, // 这里换成安全域名可访问的 };// index.ts import { createSign, createVerify } from node:crypto; // // 沙箱配置 - 从支付宝开放平台沙箱获取 // https://open.alipay.com/develop/sandbox/app // import config from ./config; // // RSA2 签名 / 验签 // /** 对内容进行 RSA2SHA256WithRSA签名返回 base64 */ function rsaSign(content: string, privateKey: string): string { const signer createSign(RSA-SHA256); signer.update(content, utf8); return signer.sign(privateKey, base64); } /** 验证支付宝回调签名 */ function rsaVerify(content: string, sign: string, publicKey: string): boolean { const verifier createVerify(RSA-SHA256); verifier.update(content, utf8); return verifier.verify(publicKey, sign, base64); } // // 支付宝请求构建工具 // /** 将参数对象按 key ASCII 排序后拼接为 keyvalue... 格式 */ function buildSignStr(params: Recordstring, string): string { return Object.keys(params) .sort() .filter((k) params[k] ! params[k] ! null) .map((k) ${k}${params[k]}) .join(); } /** 获取当前时间戳格式YYYY-MM-DD HH:mm:ss */ function getTimestamp(): string { const now new Date(); const pad (n: number) String(n).padStart(2, 0); return ${now.getFullYear()}-${pad(now.getMonth() 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}; } /** * 生成支付宝电脑网站支付的自动提交 HTML 表单 * 接口alipay.trade.page.pay */ function buildPayForm(order: { outTradeNo: string; // 商户订单号唯一 totalAmount: string; // 金额单位元如 0.01 subject: string; // 商品标题 }): string { const bizContent JSON.stringify({ out_trade_no: order.outTradeNo, total_amount: order.totalAmount, subject: order.subject, product_code: FAST_INSTANT_TRADE_PAY, }); const params: Recordstring, string { app_id: config.appId, method: alipay.trade.page.pay, charset: utf-8, sign_type: RSA2, timestamp: getTimestamp(), version: 1.0, return_url: config.returnUrl, notify_url: config.notifyUrl, biz_content: bizContent, }; // 签名 const signStr buildSignStr(params); params.sign rsaSign(signStr, config.privateKey); // 构建自动提交的 HTML 表单 const fields Object.entries(params).map(([k, v]) input typehidden name${k} value${v.replace(//g, quot;)}).join(\n ); return !DOCTYPE html html headmeta charsetutf-8title支付跳转中.../title/head body p正在跳转到支付宝请稍候.../p form idpayForm action${config.gateway} methodPOST ${fields} /form scriptdocument.getElementById(payForm).submit();/script /body /html; } /** * 验证支付宝异步通知的签名 * param body URL-encoded 格式的 POST body 字符串 */ function verifyNotify(body: string): { valid: boolean; params: Recordstring, string } { const params: Recordstring, string {}; new URLSearchParams(body).forEach((v, k) { params[k] v; }); const { sign, sign_type, ...rest } params; if (!sign || sign_type ! RSA2) return { valid: false, params }; // 验签字符串排除 sign 和 sign_type const signStr buildSignStr(rest); const valid rsaVerify(signStr, sign, config.alipayPublicKey); return { valid, params }; } // // Bun HTTP 服务 // const server Bun.serve({ port: 3000, routes: { // 首页模拟商品下单页面 /: { GET: () { const html !DOCTYPE html html headmeta charsetutf-8title支付宝沙箱支付 Demo/title/head body h2支付宝沙箱支付 Demo/h2 form action/alipay/pay methodPOST p商品测试商品/p p金额0.01 元/p button typesubmit立即支付/button /form /body /html; return new Response(html, { headers: { Content-Type: text/html; charsetutf-8 }, }); }, }, // 发起支付生成订单并跳转到支付宝 /alipay/pay: { POST: () { const outTradeNo ORDER_${Date.now()}; const form buildPayForm({ outTradeNo, totalAmount: 0.01, subject: 测试商品, }); console.log([Pay] 创建订单: ${outTradeNo}); return new Response(form, { headers: { Content-Type: text/html; charsetutf-8 }, }); }, }, // 同步回调用户支付完成后支付宝跳回此页面仅用于展示不可信 /alipay/return: { GET: (req) { const url new URL(req.url); const tradeNo url.searchParams.get(trade_no); const outTradeNo url.searchParams.get(out_trade_no); console.log([Return] out_trade_no${outTradeNo}, trade_no${tradeNo}); const html !DOCTYPE html html headmeta charsetutf-8title支付结果/title/head body h2支付成功同步回调/h2 p订单号${outTradeNo}/p p支付宝交易号${tradeNo}/p p stylecolor:gray;font-size:12px注意真实业务请以异步通知 /alipay/notify 为准/p /body /html; return new Response(html, { headers: { Content-Type: text/html; charsetutf-8 }, }); }, }, // 异步通知支付宝服务端推送这里才是真正的支付结果 /alipay/notify: { POST: async (req) { const body await req.text(); const { valid, params } verifyNotify(body); if (!valid) { console.error([Notify] 签名验证失败); console.error([Notify] 验签内容: ${buildSignStr((() { const p: Recordstring, string {}; new URLSearchParams(body).forEach((v, k) { p[k] v; }); const { sign, sign_type, ...rest } p; return rest; })())}); return new Response(fail); } const { out_trade_no, trade_no, trade_status, total_amount } params; console.log([Notify] 验签通过); console.log( 订单号: ${out_trade_no}); console.log( 支付宝交易号: ${trade_no}); console.log( 交易状态: ${trade_status}); console.log( 金额: ${total_amount}); if (trade_status TRADE_SUCCESS || trade_status TRADE_FINISHED) { // TODO: 在这里更新数据库订单状态 console.log([Notify] 订单 ${out_trade_no} 支付成功请更新业务数据); } // 必须返回 success 字符串否则支付宝会持续重试通知 return new Response(success); }, }, }, fetch(req) { return new Response(Not Found, { status: 404 }); }, }); console.log(Server is running on http://localhost:${server.port});然后有一些坑吧下面我来说到说到。坑1异步通知老不通知原因1内网穿透的域名不安全就像这样不安全的域名是不会发的。另外http和https都行的。原因2没有在控制台配置沙箱控制台地址把红框框里面的东西换成上面代码 config.ts 中 notifyUrl 变量值即可那里也必须放内网穿透后的url。原因3该死的内网穿透有广告这是我没想到的一开始我用 花生壳 内网穿透工具结果排查后发现对方只要是第一次访问就会被花生壳拦住。没错就是它你可以使用其它的内网穿透工具只要不像花生壳这么恶心就行了。你确定好之后再检查是否还会出现这个3个原因。如果没有问题的话那基本就成了。好感谢大家的观看有什么问题欢迎评论区留言。

相关新闻