当前位置: 首页 > news >正文

(Nodejs or Bun) + 支付宝支付

目录

Nodejs版

Bun版

坑1:异步通知,老不通知

原因1:内网穿透的域名不安全

原因2:没有在控制台配置

原因3:该死的内网穿透有广告


Nodejs版

这我没什么好说的,直接上sdk,sdk就已经包含了很多操作了。

​支付宝 Nodejs SDK

Bun版

为什么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 签名 / 验签 // ============================================= /** 对内容进行 RSA2(SHA256WithRSA)签名,返回 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 排序后拼接为 key=value&... 格式 */ function buildSignStr(params: Record<string, 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: Record<string, 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 type="hidden" name="${k}" value="${v.replace(/"/g, "&quot;")}">`).join("\n "); return `<!DOCTYPE html> <html> <head><meta charset="utf-8"><title>支付跳转中...</title></head> <body> <p>正在跳转到支付宝,请稍候...</p> <form id="payForm" action="${config.gateway}" method="POST"> ${fields} </form> <script>document.getElementById("payForm").submit();</script> </body> </html>`; } /** * 验证支付宝异步通知的签名 * @param body URL-encoded 格式的 POST body 字符串 */ function verifyNotify(body: string): { valid: boolean; params: Record<string, string> } { const params: Record<string, 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> <head><meta charset="utf-8"><title>支付宝沙箱支付 Demo</title></head> <body> <h2>支付宝沙箱支付 Demo</h2> <form action="/alipay/pay" method="POST"> <p>商品:测试商品</p> <p>金额:0.01 元</p> <button type="submit">立即支付</button> </form> </body> </html>`; return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-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; charset=utf-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> <head><meta charset="utf-8"><title>支付结果</title></head> <body> <h2>支付成功(同步回调)</h2> <p>订单号:${outTradeNo}</p> <p>支付宝交易号:${tradeNo}</p> <p style="color:gray;font-size:12px">注意:真实业务请以异步通知 /alipay/notify 为准</p> </body> </html>`; return new Response(html, { headers: { "Content-Type": "text/html; charset=utf-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: Record<string, 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个原因。如果没有问题的话,那基本就成了。

好,感谢大家的观看,有什么问题欢迎评论区留言。

http://www.cnnetsun.cn/news/1323472.html

相关文章:

  • Java社招面试题:Zookeeper 负载均衡和 Nginx 负载均衡有什么区别?
  • 新手必看:如何用sys.path.append()解决Python模块导入失败问题(附真实案例)
  • 一种融合Circle混沌映射、Levy飞行策略与透镜成像折射学习的改进长鼻浣熊优化算法--MA...
  • linux cifs架构
  • gemini使用命令
  • 星图AI算力平台训练PETRV2-BEV模型:保姆级教程,5步搞定自动驾驶感知
  • 电商智能客服数据存储方案:关系型数据库 vs 向量数据库的技术选型与实战
  • 02 今日内容大纲
  • 振温传感器特征值及其作用
  • 告别数据残留:微信聊天记录与图片文件永久销毁的正确操作指南
  • 选型指南:一文解锁和芯星通GNSS芯片模块产品选型
  • PowerPaint-V1 Gradio与SpringBoot整合实战:企业级图像处理平台搭建
  • 发财运势计算器,简易程序!
  • 语音分离新突破:MossFormer模型在ICASSP 2023上的表现与实战调优指南
  • AnythingtoRealCharacters2511惊艳效果展示:日漫风→写实光影→电影级质感全流程案例
  • 【Skills实战1】:自动生成报告(包括配图)-附skill文件
  • Golang实现AI智能体权限最小化与动态沙箱系统
  • Asian Beauty Z-Image Turbo镜像免配置:内置TensorRT加速选项与ONNX导出工具链
  • Qwen3-ASR-0.6B语音识别入门必看:自动语言检测+多格式音频支持详解
  • 西门子1200使用信号板(CB 1241 RS485)实现ModbusRTU源码分享
  • 2026年亲测:合肥系统门窗厂家真实案例分享
  • MarkItDown:多格式文档转换解决方案的实战指南
  • InstructPix2Pix效果展示集:油画风、复古胶片感,指令生成惊艳作品
  • GLM-OCR在MATLAB中的调用:打通深度学习模型与科学计算环境
  • 2026年实测3款矩阵管理工具,它凭AI+全链路能力破解企业运营痛点
  • MogFace人脸检测模型CSDN技术博客写作:如何展示你的部署与应用成果
  • translategemma-27b-it效果展示:电商主图中文文案→12国语言本地化翻译作品集
  • 使用 `srvany.exe` 创建 Windows 系统服务的详细教程
  • 鸿蒙应用开发全流程指南
  • M-LLM视频帧选择技术解析