微信小程序要接支付宝沙箱支付,第一波劝退你的往往不是签名算法,而是标题里“http请求”这四个字。很多同学拿着支付宝官方的 OpenAPI 文档,准备在小程序里直接wx.request沙箱网关,然后被域名校验、签名、回调轮番教育,最后卡在“为什么我的请求一直报错”这一步。
这篇就把链路彻底讲透:从沙箱应用申请、RSA2 密钥、后端下单接口,到小程序端wx.request怎么调、web-view怎么开收银台,再到支付宝异步回调验签,每一步的“为什么”都给你拆开。适合正在做小程序商城、想先走通沙箱再上生产的后端或全栈开发者阅读。
1. 先理清一条链路:小程序为什么不能直接“http请求”支付宝沙箱
1.1 这里的“http请求”到底指什么
很多人第一次看这类教程,会误以为“http请求”是让小程序直接请求支付宝沙箱的网关地址。实际上微信小程序里的wx.request有两个硬性限制:第一,请求地址只能是 HTTPS;第二,这个域名必须在小程序后台配置成合法域名,而且必须完成 ICP 备案。
支付宝沙箱网关openapi-sandbox.dl.alipaydev.com是你没法在小程序后台配的,原因很简单——它本身就不是备案域名。所以“http请求支付宝沙箱”这个说法,准确的理解应该是:小程序端用wx.request请求你自己后端的 HTTP 接口,再由后端去和支付宝沙箱网关通信。
1.2 微信小程序与支付宝网关之间的三堵墙
即使你在开发者工具里勾上了“不校验合法域名”,真正把请求发到支付宝沙箱网关,还会撞上另外几堵墙:
wx.request的 Content-Type 和响应格式限制:支付宝网关要求application/x-www-form-urlencoded或表单提交,返回的是表单 HTML 或 Gateway 响应。小程序直接解析这类响应特别别扭。- 签名不能暴露在前端:支付宝接口签名用的是你的应用私钥,这个东西如果放在小程序里,等于把支付安全直接送给别人。
- 微信小程序无法唤起支付宝 App:支付宝官方 SDK 在小程序环境里根本没法用,微信也不会允许你跳转到支付宝客户端完成支付。这是生态层面的互斥,技术上也绕不过去。
1.3 最终选型:后端中转 + web-view 收银台
基于上面这些限制,实际可落地的方案基本是统一的:
- 小程序端
wx.request调用自己后端的“创建订单”接口。 - 后端拿着订单号、金额、商品标题,按支付宝开放平台要求拼参数、做 RSA2 签名,请求沙箱网关
alipay.trade.wap.pay。 - 支付宝沙箱返回一个 H5 收银台地址或一段自动提交表单的 HTML。
- 小程序端用
web-view加载这个收银台地址,用户在沙箱收银台里输入虚拟买家账号密码完成支付。 - 支付宝异步通知你的后端接口,后端验签后把订单状态改成已支付。
这套链路里,“http请求”就是第一步和第五步,真正核心的是后半段跟支付宝网关的签名通信。下面的内容全部围绕这条链路展开。
2. 沙箱环境准备:应用、密钥、虚拟买家一个都不能少
2.1 创建沙箱应用
打开支付宝开放平台,登录后进入“控制台 - 沙箱环境 - 沙箱应用”。首次使用会让你创建一个沙箱应用,创建成功后会给你一个 16 位的APP_ID,类似2021000123456789,这就是后面所有接口调用都要用的应用标识。
沙箱应用默认会开通一系列产品能力,其中和我们最相关的两个:
- 手机网站支付:对应
alipay.trade.wap.pay,适合 web-view 加载 H5 收银台。 - 电脑网站支付:对应
alipay.trade.page.pay,适合 PC 端,小程序里用不到,但沙箱里顺手开着也没问题。
这里要特别注意,沙箱环境的入口和个人账号体系是独立的。你在沙箱控制台看到的APP_ID、网关地址、密钥,全部只能在沙箱环境里用,不要拿生产环境的支付宝开放平台账号去混淆。
2.2 用支付宝密钥工具生成RSA2密钥对
支付宝开放平台的接口签名方式推荐 RSA2(即 SHA256withRSA)。你需要一对密钥:应用私钥、应用公钥。生成方式有两种,这里说最省事的一种——下载支付宝官方的“支付宝开放平台密钥工具”。
打开工具后选择“生成密钥”,会自动生成应用私钥和应用公钥,格式上有 PKCS1 和 PKCS8 两种选择。如果你是 Java 后端,选 PKCS8;如果你用 Go,也建议选 PKCS8,Go 标准库解析 PKCS8 更顺手。密钥工具生成的内容长这样:
- 应用私钥:
-----BEGIN PRIVATE KEY-----开头的一长串内容。 - 应用公钥:
-----BEGIN PUBLIC KEY-----开头的一长串内容。
拿到这两串之后,回到沙箱应用的“接口加签方式”配置里,选择“公钥模式”,把应用公钥粘贴进去。保存后支付宝会返回一个支付宝公钥,这个和你的应用公钥不是一回事,验签时用的是支付宝公钥。
2.3 关键配置项速查表
| 配置项 | 沙箱环境取值 | 说明 |
|---|---|---|
| 网关地址 | https://openapi-sandbox.dl.alipaydev.com/gateway.do | 沙箱专用,生产环境是https://openapi.alipay.com/gateway.do |
| 应用 ID | 沙箱控制台里的APP_ID | 每个沙箱应用独立 |
| 应用私钥 | 密钥工具生成的私钥 | 下单签名用,只能放在后端 |
| 支付宝公钥 | 在沙箱应用里配置公钥后由支付宝返回 | 异步通知验签用 |
| 回调地址 | 你后端可公网访问的接口地址 | 在沙箱应用的功能配置里设置 |
2.4 沙箱买家账号的拿法
沙箱支付不能用你自己的真实支付宝账号去付款。在沙箱控制台“沙箱账号”区域,支付宝会给你一个虚拟买家账号,通常是一个手机号,同时有登录密码和支付密码。支付时在 H5 收银台里输入这个虚拟账号和密码,输入支付密码,就能模拟完成一笔真实支付流程,但不会产生任何资金流水。
如果你发现收银台提示账号不存在,或者支付密码错误,大概率是拿成真实支付宝账号了。这个细节第一遍跑通时最容易踩,后面避坑清单再细说。
3. 后端下单接口:把小程序的 HTTP 请求翻译成支付宝 OpenAPI 调用
3.1 接口设计
后端要提供给小程序的接口很简单,我按 gin 框架写一个最小可用的示例,接口路径叫/api/alipay/create,支持 POST JSON。它接收两个参数:商品标题subject和支付金额total_amount,然后返回给前端一个pay_url。
为什么后端不能直接替用户完成支付,而是返回一个pay_url?因为支付动作需要用户在支付宝收银台输入密码确认,这是一个“人”的交互过程,不是简单接口调用。后端能做的,是生成一个能让用户去支付的地址或表单。
3.2 请求支付宝沙箱网关的签名核心步骤
支付宝开放平台接口的调用逻辑本质上是:把业务参数放进biz_content,再把公共参数和biz_content一起做签名,最后用 HTTP 请求发到网关。签名是整条链路里最容易出错的一环,拆开看其实就四步:
- 按参数名 ASCII 码从小到大排序。
- 过滤掉值为空的参数,用
key1=value1&key2=value2的方式拼接成待签名字符串。 - 用应用私钥对待签名字符串做 SHA256withRSA 签名,结果做 Base64 编码。
- 把签名放进请求参数里的
sign字段,调接口时一起提交。
这个签名逻辑几乎适用于所有支付宝 OpenAPI 接口。你可以直接用现成 SDK,但搞懂原理后排查问题会快很多。下面代码展示的就是这个手写流程,用 Go 实现。
3.3 Go 代码实现(gin 示例)
先定义配置项:
package main import ( "crypto" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/json" "encoding/pem" "fmt" "net/url" "sort" "strings" "time" "github.com/gin-gonic/gin" ) var ( appId = "2021000123456789" // 沙箱应用APP_ID privateKey = `-----BEGIN PRIVATE KEY----- ...你的应用私钥... -----END PRIVATE KEY-----` alipayPublicKey = `-----BEGIN PUBLIC KEY----- ...支付宝公钥... -----END PUBLIC KEY-----` notifyUrl = "https://yourdomain.com/api/alipay/notify" gatewayUrl = "https://openapi-sandbox.dl.alipaydev.com/gateway.do" )然后是创建订单接口:
type CreateOrderReq struct { Subject string `json:"subject"` TotalAmount string `json:"total_amount"` } type CreateOrderResp struct { PayURL string `json:"pay_url"` } func main() { r := gin.Default() r.POST("/api/alipay/create", createOrder) r.POST("/api/alipay/notify", alipayNotify) r.Run(":8080") } func createOrder(c *gin.Context) { var req CreateOrderReq if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"message": "参数错误"}) return } // 生产环境这里应该先落库,生成唯一订单号 orderNo := fmt.Sprintf("T%d", time.Now().UnixNano()/1e6) bizContent, _ := json.Marshal(map[string]string{ "out_trade_no": orderNo, "total_amount": req.TotalAmount, // 单位是元,字符串类型 "subject": req.Subject, "product_code": "QUICK_WAP_WAY", // 手机网站支付固定值 }) params := map[string]string{ "app_id": appId, "method": "alipay.trade.wap.pay", "format": "JSON", "charset": "utf-8", "sign_type": "RSA2", "timestamp": time.Now().Format("2006-01-02 15:04:05"), "version": "1.0", "notify_url": notifyUrl, "biz_content": string(bizContent), } sign, err := signParams(params, privateKey) if err != nil { c.JSON(500, gin.H{"message": "签名失败"}) return } params["sign"] = sign // 构造支付宝收银台URL query := sortedQueryString(params) payURL := gatewayUrl + "?" + query c.JSON(200, CreateOrderResp{PayURL: payURL}) }3.4 签名与排序细节
签名函数单独拎出来看:
func signParams(params map[string]string, privateKeyStr string) (string, error) { // 1. 过滤空值 filtered := make(map[string]string) for k, v := range params { if v != "" { filtered[k] = v } } // 2. ASCII码排序 keys := make([]string, 0, len(filtered)) for k := range filtered { keys = append(keys, k) } sort.Strings(keys) // 3. 拼接待签名字符串 var sb strings.Builder for i, k := range keys { if i > 0 { sb.WriteByte('&') } sb.WriteString(k) sb.WriteByte('=') sb.WriteString(filtered[k]) } signStr := sb.String() // 4. 私钥签名 block, _ := pem.Decode([]byte(privateKeyStr)) if block == nil { return "", fmt.Errorf("private key decode error") } key, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { return "", err } hashed := sha256.Sum256([]byte(signStr)) sig, err := rsa.SignPKCS1v15(rand.Reader, key.(*rsa.PrivateKey), crypto.SHA256, hashed[:]) if err != nil { return "", err } return base64.StdEncoding.EncodeToString(sig), nil } func sortedQueryString(params map[string]string) string { keys := make([]string, 0, len(params)) for k := range params { keys = append(keys, k) } sort.Strings(keys) var sb strings.Builder for i, k := range keys { if i > 0 { sb.WriteByte('&') } sb.WriteString(url.QueryEscape(k)) sb.WriteByte('=') sb.WriteString(url.QueryEscape(params[k])) } return sb.String() }代码里有两个点要特别说明。
第一,签名前拼接的字符串里,biz_content是完整 JSON 字符串,不能做 URL 编码,签完名之后拼 URL 时才可以编码。
第二,timestamp格式必须是2006-01-02 15:04:05,时区用本地时区即可。有很多人在这里格式写错,导致支付宝返回isv.invalid-signature之类的错误码。
sortedQueryString里用url.QueryEscape处理参数值,因为biz_content里有 JSON 的特殊字符,不转义的话拼到 URL 里会被截断或解析错乱。
3.5 返回给小程序的是什么
后端返回给小程序的是pay_url,也就是一个可以直接在浏览器打开的支付宝收银台地址。小程序端拿到这个地址后,不需要再发什么 http 请求,直接把地址丢给web-view组件加载即可。用户支付完,支付宝会跳转到return_url(如果在接口里配了的话)或关闭页面。
这里注意,支付宝手机网站支付在沙箱环境里,用 GET 方式访问gateway.do?拼接参数是能弹出收银台的。如果你在测试时发现收银台一直白屏或报错,可以退回去用响应表单的方式:后端返回一个 HTML 页面,里面放一个自动提交的 form 表单,web-view加载这个页面地址。不过多数沙箱测试下,直接拼 URL 是可行的,先从这个简单方案开始。
4. 小程序端 wx.request 与 web-view:开发调试怎么绕过域名限制
4.1 wx.request 合法域名规则
小程序里的wx.request在正式环境要求非常严格:
- 域名必须已经通过 ICP 备案。
- 域名必须是 HTTPS。
- 域名必须在小程序管理后台“开发管理 - 服务器域名 - request 合法域名”里配置过。
这意味着后端开发阶段如果只在本地起一个 8080 服务,用http://localhost:8080去请求,生产环境是百分之百不可用的。但在本地开发调试阶段,为了不卡脖子,微信开发者工具提供了一个开关,接下来马上说。
4.2 调试期勾选“不校验合法域名”
在微信开发者工具里,点击右上角“详情 - 本地设置”,勾选“不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书”。勾上之后,wx.request就能请求http://localhost:8080或者局域网 IP 了。
如果你要真机预览,手机和电脑连同一个 Wi-Fi,把接口地址改成电脑的局域网 IP,比如http://192.168.1.100:8080/api/alipay/create,同时手机上的小程序也需要是开发版本。沙箱调试阶段这个方案最直接。
4.3 小程序端代码示例
小程序端逻辑非常简单:调用后端接口,拿到pay_url,然后渲染web-view。
Page({ data: { payUrl: '' }, createOrder() { wx.request({ url: 'http://localhost:8080/api/alipay/create', method: 'POST', data: { subject: '沙箱测试商品', total_amount: '0.01' }, success: (res) => { if (res.data && res.data.pay_url) { this.setData({ payUrl: res.data.pay_url }) } else { wx.showToast({ title: '下单失败', icon: 'none' }) } }, fail: () => { wx.showToast({ title: '请求失败,请检查后端服务', icon: 'none' }) } }) } })页面里放一个按钮触发createOrder,然后条件渲染web-view:
<button bindtap="createOrder">发起沙箱支付</button> <web-view wx:if="{{payUrl}}" src="{{payUrl}}"></web-view>这里有个容易踩的细节:web-view组件要求src是一个合法的 URL,不能直接放一段 HTML 字符串。所以后端设计时一定返回可访问的pay_url,而不是把收银台表单 HTML 拼在 JSON 里返回给小程序。
4.4 web-view 打开支付宝收银台的注意事项
web-view在小程序里本质是一个全屏的浏览器组件,但它不是万能的。开发调试阶段你可能会遇到这几个现象:
- 开发者工具里
web-view加载支付宝沙箱收银台可能显示正常,也可能因为沙箱环境不稳定导致白屏。我建议优先在开发者工具里看 Network 面板确认pay_url是不是能正常打开,再把同样的地址粘贴到电脑浏览器里访问,两边对照。 - 真机上
web-view对支付宝 H5 收银台的支持整体而言是偏弱的,尤其沙箱环境,真机上经常会被支付宝风控拦截,提示“当前环境不支持”之类的错误。这不是你代码的 bug,而是沙箱环境和 H5 页面自身限制。我能给的可行建议是:功能验证优先在开发者工具里做,真机验证放到生产环境用真实支付宝账号去测。 web-view的业务域名校验在正式环境同样存在。开发调试关闭校验后随便什么 URL 都可以加载,但生产环境web-view加载的域名必须在“业务域名”里配置,而且需要下载校验文件放到域名根目录。如果你的后端是给你自己的支付中间页用的,要把这个域名配进去。
5. 异步回调验签:支付结果不能只看小程序返回
5.1 为什么必须验签
用户在小程序里完成支付后,你后端怎么知道这笔钱已经付了?支付宝的同步返回(用户在收银台支付成功后跳转回return_url)只是浏览器行为,数据可以被伪造,也可能因为网络原因丢失,绝对不能作为订单状态更新的依据。
支付宝真正的“结果通知”是异步通知:支付成功或退款成功后,支付宝服务器会主动 POST 请求你配置的notify_url,通知里带着out_trade_no、trade_no、trade_status、total_amount等参数。因为这个通知可能被第三方恶意构造,所以后端第一件事必须是验签——用支付宝公钥验证通知里的sign是否合法。验签通过后,再核对订单号和金额,才能把订单改成已支付。
5.2 异步通知参数与验签流程
异步通知的参数和请求签名方式类似,区别在于:
- 验签用的是支付宝公钥,不是应用私钥。
- 参数中要剔除
sign和sign_type两个字段后再排序拼接。 - 签名算法还是 RSA2。
验签通过之后,还要做三项关键校验:
- 检查
out_trade_no是不是你系统里真实存在的订单号。 - 检查
total_amount是不是和下单时一致,防止有人篡改金额。 - 检查
trade_status,只有TRADE_SUCCESS或TRADE_FINISHED才能算支付成功。
处理完业务逻辑后,接口必须返回纯文本success,否则支付宝会按照策略多次重试通知,直到你返回success为止。这个细节也是新手容易漏的。
5.3 Go 验签代码
下面是一个针对 gin 的异步通知处理函数:
func alipayNotify(c *gin.Context) { // 支付宝异步通知是 x-www-form-urlencoded 格式 c.Request.ParseForm() values := c.Request.PostForm // 1. 先验签 ok, err := verifyNotify(values, alipayPublicKey) if err != nil || !ok { c.String(200, "fail") return } // 2. 校验业务字段 outTradeNo := values.Get("out_trade_no") tradeStatus := values.Get("trade_status") totalAmount := values.Get("total_amount") // 这里要查你自己的订单库,比对金额 // if totalAmount != order.TotalAmount { ... } if tradeStatus != "TRADE_SUCCESS" && tradeStatus != "TRADE_FINISHED" { c.String(200, "success") return } // 3. 业务处理,修改订单状态 // updateOrderStatus(outTradeNo, "paid") c.String(200, "success") } func verifyNotify(values url.Values, alipayPublicKeyStr string) (bool, error) { sign := values.Get("sign") values.Del("sign") values.Del("sign_type") keys := make([]string, 0, len(values)) for k := range values { keys = append(keys, k) } sort.Strings(keys) var sb strings.Builder for i, k := range keys { vs := values[k] if len(vs) == 0 { continue } if i > 0 { sb.WriteByte('&') } sb.WriteString(k) sb.WriteByte('=') sb.WriteString(vs[0]) } hashed := sha256.Sum256([]byte(sb.String())) decodedSign, _ := base64.StdEncoding.DecodeString(sign) block, _ := pem.Decode([]byte(alipayPublicKeyStr)) if block == nil { return false, fmt.Errorf("alipay public key decode error") } pub, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { return false, err } err = rsa.VerifyPKCS1v15(pub.(*rsa.PublicKey), crypto.SHA256, hashed[:], decodedSign) return err == nil, nil }这里需要提一句:支付宝异步通知的参数里有passback_params之类的字段,如果值带特殊字符,拼接验签时要用原始值,不能先做 URL 解码。按上面的代码从PostForm里取出来的就是解码后的值,和支付宝官方验签逻辑一致,可以正常工作。
5.4 回调地址怎么配置(公网可访问)
沙箱应用里要配置“接口加签方式”和“回调地址”,其中notify_url必须是你后端能收到的地址。本地开发时,一个比较实用的做法是用内网穿透工具把localhost:8080映射成一个临时公网地址,比如https://yourname.free.ngrok.cn/api/alipay/notify,把这个地址填到沙箱应用的授权回调地址里。
我用过几次之后发现,支付宝沙箱对回调地址的域名要求比生产环境宽松,但要求必须是能公网访问的地址,不能是localhost或内网 IP。配置完记得点保存,然后在下单参数里也填一样的notify_url,两边一致才能收到通知。
6. 沙箱调试避坑清单
6.1 签名相关
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
返回isv.invalid-signature | 签名参数拼错、排序不对、私钥格式不对 | 按本文 3.4 节重新检查;Go 后端统一用 PKCS8 私钥 |
返回sign check fail | 验签用的公钥配错 | 确认用的是支付宝公钥,不是应用公钥,也不是应用私钥 |
返回Required argument missing | 公共参数缺失 | 检查app_id、method、timestamp、version是否都有 |
签名这一块最容易出问题的其实是换行符。粘贴私钥时前后多了空格、换行,或者Begin Private Key和End Private Key没保留,pem.Decode解析出来就是nil,然后各种报错。建议直接把密钥放在独立配置文件里,从文件读取,减少手工粘贴带来的问题。
6.2 金额/订单号相关
total_amount必须是字符串,单位是元,不能传整数类型的1,应该传"1.00"。很多语言里 JSON 序列化会自动把1.00变成1,所以下单接口里我建议统一用字符串接收金额。out_trade_no必须是商户系统唯一订单号,同一个订单号不能重复下单。沙箱测试时如果你一直用一个固定订单号,第二次请求会报ACQ.TRADE_HAS_SUCCESS之类的错误。我测试时会在前面拼毫秒时间戳来保证唯一。- 订单号长度不能超过 64 位,纯数字和字母组合即可。
6.3 沙箱环境特有坑
沙箱支付最大的坑就是账号。你必须在沙箱控制台找到虚拟买家账号,用这个账号在收银台登录。如果用真实支付宝账号,会直接提示无法支付或者账号不存在。另外,沙箱买家账号登录时,网站一般会要求你用“沙箱工具”或扫码方式登录,实际用下来最简单的方式是:在收银台页面直接输入沙箱账号和密码,不用走扫码。
另一个常见问题:沙箱收银台在web-view里加载后,支付密码输对了也可能一直转圈。这种情况多半是支付宝沙箱环境对 web-view 的兼容性问题。先不要急着改代码,把pay_url复制到普通浏览器里重试一次,如果普通浏览器能正常支付,说明链路没问题,问题在 web-view 环境本身。
6.4 环境切换时容易漏的项
我在从沙箱切生产环境时反复漏过几样东西,列成清单提醒你:
- 网关地址:
openapi-sandbox.dl.alipaydev.com换成openapi.alipay.com。 APP_ID:沙箱应用 ID 换成真实应用 ID。- 应用私钥:换成生产环境密钥,最好重新生成一对,不要用沙箱的。
- 支付宝公钥:换成生产环境应用的支付宝公钥。
- 回调地址:沙箱里临时内网穿透地址换成生产 HTTPS 域名。
- 微信小程序后台:
request合法域名、web-view业务域名都要同步更新。
这些配置任何一个没换,都会导致支付流程断在半路。而且报错往往不太直观,比如支付成功但回调不到、下单时一直签名失败,排查起来很费时间。
7. 从沙箱切到生产环境需要过的一关又一关
7.1 需要替换的配置项
生产环境和沙箱环境之间,不是改一个开关那么简单。我把常用的参数整理成下面这张对照表,每上线一个新项目都会对着它检查:
| 配置项 | 沙箱环境 | 生产环境 |
|---|---|---|
| 网关地址 | https://openapi-sandbox.dl.alipaydev.com/gateway.do | https://openapi.alipay.com/gateway.do |
| 应用 ID | 沙箱应用APP_ID | 真实应用APP_ID |
| 应用私钥 | 沙箱密钥 | 生产密钥,妥善保管 |
| 支付宝公钥 | 沙箱应用配置后返回 | 真实应用配置后返回 |
| 回调地址 | 允许公网 http/内网穿透 | 必须是 HTTPS 备案域名 |
| 支付账号 | 沙箱虚拟买家 | 真实支付宝用户 |
7.2 生产环境小程序端的域名与合规检查
小程序端的web-view有一个容易被忽略的点:业务域名要求文件校验,你需要把平台生成的校验文件放到域名根目录,确保通过 HTTPS 能访问到。如果你准备用“后端生成收银台 URL,小程序web-view直接加载支付宝域名”,那还得把支付宝 H5 支付的域名加入业务域名。但支付宝的域名你没法放置校验文件,所以这种方案在生产环境基本走不通。
实际在生产项目里,更稳的路径是:后端生成一个属于自己的支付中间页 URL,这个中间页通过服务端渲染或前端跳转把用户带到支付宝收银台。这样小程序web-view加载的是你自己已校验的域名,域名跳转到支付宝 H5 页面时因为是从 web-view 内部发起的顶层跳转,相对不容易被拦截。这个方案需要后端多渲染一个页面,代码量不大,但能避开一堆线上问题。
7.3 一个实测稳定的上线顺序建议
按下面顺序做,能最大程度减少上线时的“最后一跳”问题:
- 先用本文的沙箱链路把开发者工具里的
pay_url跑通。 - 在内网穿透环境下,把异步回调验签完整跑通,确认
success返回和订单状态更新正确。 - 申请并配置生产环境的支付宝应用、密钥、回调地址。
- 后端加一个环境配置开关,沙箱和生产配置分离,避免手误改错。
- 小程序后台配置真实
request合法域名和业务域名,正式包发布前用体验版完整支付一单。 - 全部通了之后,再用真机支付 0.01 元商品做最终验证。
我个人强烈建议在第 5、6 步之间留出时间,因为真机上的 H5 收银台表现和开发者工具差别很大,尤其是 web-view 的兼容性,需要提前确认。
最后再分享一个我自己的习惯:沙箱联调时,不要一上来就写完整业务代码,先写一个最小可用的下单接口和一个最简单的回调打印,手动curl一下pay_url,再把支付宝异步通知打出来的所有参数存到日志里。日志里的参数和签名就是最好的排查依据,比任何文档都直观。这个思路帮我少走了很多弯路,你也可以试试。