前言

过去半年里,我一直在为 源码商城 开发配套的支付宝小程序,从支付下单到订单查询,过程中踩了无数坑。支付宝小程序的文档虽然全面,但藏得深、更新慢、很多细节要靠血泪教训才能发现。

这篇文章把我踩过的坑、填过的洞、以及最终的解决方案全部整理出来,希望能让后来的开发者少走弯路。文章涵盖支付接入、用户授权、审核上架、真机调试四大板块,每个坑都附有代码示例和正确做法。

坑一:支付接入时 tradeNO 为空

问题描述

调用 my.tradePay 发起支付时,传入从服务端获取的 tradeNO(交易号),结果弹窗报错"订单信息无效",控制台打印的 tradeNOundefined

原因分析

大多数情况下,问题出在服务端返回的 tradeNO 字段名大小写不一致。支付宝服务端 SDK 返回的字段名是 trade_no(下划线命名),而小程序端期望的是 tradeNO(驼峰命名)。如果后端直接透传,小程序拿到的就是 undefined

正确做法

服务端返回前做一次字段映射:

# Python 示例 — 创建订单接口
import alipay_sdk

def create_payment_order(order_id, amount, subject):
    alipay = alipay_sdk.AliPay(
        appid='YOUR_APP_ID',
        app_notify_url='https://your-domain.com/notify',
        app_private_key_string=open('private_key.pem').read(),
        alipay_public_key_string=open('alipay_public_key.pem').read()
    )
    
    # 创建支付宝订单
    result = alipay.api_alipay_trade_precreate(
        subject=subject,
        out_trade_no=order_id,
        total_amount=amount
    )
    
    # 关键:字段名映射
    return {
        'tradeNO': result.get('trade_no'),  # 下划线 → 驼峰
        'outTradeNo': order_id,
        'totalAmount': amount
    }

小程序端拿到 tradeNO 后直接传给 my.tradePay

// 小程序端调用支付
my.request({
    url: 'https://your-api.com/create-order',
    data: { amount: '0.01', subject: '源码商城测试订单' },
    success: (res) => {
        my.tradePay({
            tradeNO: res.data.tradeNO,  // 一定是驼峰!
            success: (payRes) => {
                // 支付成功回调
                my.showToast({ content: '支付成功' });
            },
            fail: (err) => {
                // 支付失败
                console.error('支付失败:', err);
            }
        });
    }
});

坑二:用户授权弹窗被"静默"拒绝

问题描述

首次打开小程序,调用 my.getAuthCode 获取用户授权码时,弹窗一闪而过,没有弹出授权确认框,直接返回了 authCode: '' 空值。

原因分析

支付宝小程序在 iOS 上有一种"静默拒绝"机制。如果用户之前拒绝过授权,系统会将此行为记忆为"永久拒绝",后续调用不再弹窗询问。此外,如果在 onLaunch 中立即请求授权,有时也会因为页面尚未完全加载而被系统拦截。

正确做法

不要在 App.onLaunch 中直接调授权。改为在页面中由用户点击按钮触发:

// 页面代码 — 正确的授权流程
Page({
    data: {
        hasAuth: false
    },
    
    onLoad() {
        // 先检查是否已有缓存授权
        const cached = my.getStorageSync({ key: 'authCode' });
        if (cached.data) {
            this.setData({ hasAuth: true });
        }
    },
    
    // 用户点击"授权登录"按钮触发
    handleAuthTap() {
        my.getAuthCode({
            scopes: ['auth_user'],
            success: (res) => {
                if (res.authCode) {
                    my.setStorageSync({ key: 'authCode', data: res.authCode });
                    this.setData({ hasAuth: true });
                    my.showToast({ content: '授权成功' });
                } else {
                    // 被拒绝 — 引导用户手动开启
                    my.showToast({ 
                        content: '请在设置中开启授权',
                        duration: 3000
                    });
                }
            },
            fail: () => {
                // 跳转设置页引导
                my.confirm({
                    title: '需要授权',
                    content: '请在"我的 → 设置"中开启用户信息授权',
                    confirmButtonText: '去设置',
                    success: (r) => {
                        if (r.confirm) my.openSetting();
                    }
                });
            }
        });
    }
});

坑三:回调通知验签失败

问题描述

支付宝支付成功后,服务端收不到异步通知,或者收到的通知签名校验失败,导致订单状态无法更新,用户付了款却显示"未支付"。

原因分析

支付宝异步通知的签名验签有多个容易出错的细节:

  1. 参数排序问题:验签前必须按字典序排序参数,包括 signsign_type 以外的所有参数
  2. URL 解码问题:通知中的参数值是 URL 编码的,需要先解码再参与验签
  3. notify_url 必须是公网可访问:不能是 localhost 或内网 IP
  4. 响应内容必须是 success(纯小写,无空格):返回其他内容支付宝会重试最多 7 次

正确做法

# Python Flask 异步通知处理
from flask import Flask, request
import hashlib, json

app = Flask(__name__)

ALIPAY_PUBLIC_KEY = open('alipay_public_key.pem').read()

@app.route('/notify', methods=['POST'])
def alipay_notify():
    # 1. 获取所有参数
    params = request.form.to_dict()
    sign = params.pop('sign', '')
    sign_type = params.pop('sign_type', '')
    
    # 2. 排序并拼接
    sorted_keys = sorted(params.keys())
    sign_str = '&'.join([
        f'{k}={params[k]}' for k in sorted_keys
    ])
    
    # 3. 验证签名(使用支付宝公钥)
    from Crypto.PublicKey import RSA
    from Crypto.Signature import PKCS1_v1_5
    from Crypto.Hash import SHA256
    
    key = RSA.import_key(ALIPAY_PUBLIC_KEY)
    verifier = PKCS1_v1_5.new(key)
    h = SHA256.new(sign_str.encode('utf-8'))
    
    if verifier.verify(h, bytes.fromhex(sign)):
        # 验签通过,处理订单
        out_trade_no = params.get('out_trade_no')
        trade_status = params.get('trade_status')
        
        if trade_status == 'TRADE_SUCCESS':
            update_order_status(out_trade_no, 'paid')
            return 'success'  # 必须返回纯小写
        return 'success'
    else:
        # 验签失败,记录日志
        app.logger.error(f'Sign verify failed: {out_trade_no}')
        return 'fail'
记得把 notify_url 配置为公网可访问的地址。如果支付系统部署在香港服务器,可以参考我们之前整理的香港服务器支付中转指南

坑四:上传图片返回 403

问题描述

使用 my.uploadFile 上传用户头像或商品图片时,服务端收到请求但返回 403 Forbidden,或者客户端收到 HTTP 403

原因分析

支付宝小程序的 my.uploadFile 会在请求头中自动添加 referer,某些服务端安全策略(如 Nginx 的 valid_referers)会拒绝这个 referer。另外,上传目标域名必须在支付宝小程序后台的"服务器域名白名单"中配置。

解决方案

# Nginx 配置 — 允许支付宝小程序的 referer
server {
    listen 443 ssl;
    server_name api.your-domain.com;
    
    # 支付宝小程序的 referer 格式:https://render.alipay.com/...
    if ($http_referer ~* "alipay\\.com|render\\.alipay") {
        set $allow_upload 1;
    }
    
    location /upload/ {
        # 支付宝小程序 referer 放行
        if ($allow_upload = 1) {
            add_header Access-Control-Allow-Origin '*';
        }
        
        # 同时校验 token,防止恶意上传
        proxy_pass http://127.0.0.1:8080;
    }
}

小程序端上传时也要注意合理超时设置:

// 上传文件代码
my.uploadFile({
    url: 'https://api.your-domain.com/upload/avatar',
    fileType: 'image',
    fileName: 'avatar.jpg',
    filePath: filePath,
    timeout: 30000,  // 30秒超时
    success: (res) => {
        // res.data 是服务端返回的 JSON 字符串
        const data = JSON.parse(res.data);
        console.log('上传成功:', data.url);
    },
    fail: (err) => {
        my.showToast({ content: '上传失败: ' + err.errorMessage });
    }
});

坑五:审核被拒 — "页面内容与描述不符"

问题描述

提交审核后,第二天收到驳回通知,理由是"页面内容与服务类目不符"或"描述与实际功能不符"。这类驳回往往不给出具体哪一页,全靠开发者自己猜。

常见驳回原因

  • 类目选择错误:小程序涉及支付就必须选择"电商平台"或"在线支付"类目,且需要对应 转卡码系统 这类实际产品的经营资质
  • 测试账号未配置:审核人员点开需要登录的页面,发现无法正常访问
  • Webview 内容不符:小程序中使用了 my.webview 打开外部链接,链接内容与申请类目不一致
  • 虚拟商品说明不清:如果你在卖 Codex Desktop 这类虚拟商品,必须明确说明是"源码/授权码",不能用模糊描述

应对策略

// app.js — 为审核人员准备专用测试入口
App({
    onLaunch() {
        // 检测是否为审核环境
        const env = my.getEnvInfoSync();
        if (env.platform === 'alipay' && env.scene === 1001) {
            // 审核场景 — 跳过登录,直接进入演示页
            this.globalData.isReviewMode = true;
        }
    }
});
// 审核专用页面 — 展示所有核心功能
Page({
    // 审核模式下自动填充演示数据
    onLoad() {
        const app = getApp();
        if (app.globalData.isReviewMode) {
            this.setData({
                demoOrders: [
                    { id: 'DEMO001', product: '转卡码系统 V3', amount: '199.00', status: '已支付' },
                    { id: 'DEMO002', product: 'Codex Desktop', amount: '99.00', status: '已支付' },
                    { id: 'DEMO003', product: 'AI 工具包', amount: '49.00', status: '待支付' }
                ]
            });
        }
    }
});
小窍门:审核描述里明确写清楚"本小程序与源码商城 greenfield.ltd/store/ 配合使用",并附上后台管理地址,能大幅降低被拒概率。

坑六:真机调试报错 "invalid ip"

问题描述

在 IDE 中一切正常,一上真机调试就报 "invalid ip" 或 "request fail: 网络错误"。排查了半天,发现是 IDE 自动忽略了 HTTPS 校验,而真机要求严格 HTTPS。

解决方案

支付宝小程序真机环境强制要求:

  1. 必须 HTTPS:所有请求域名都必须支持 HTTPS,不能使用 HTTP
  2. 域名必须备案且绑定 SSL 证书:自签名证书不被信任
  3. Webview 加载的页面也必须是 HTTPS
  4. 本地开发时可以用 my.httpRequest 代替 my.request 临时绕过(仅开发阶段)

这里也推荐使用国内服务器部署后端。如果你需要搭建支付中转,可以参考我们的香港服务器支付中转部署指南

坑七:小程序包体积超限

问题描述

开发到后期,本地资源文件(图片、字体)越来越多,打包时发现主包体积超过 2MB 限制,无法上传。

解决方案

# 1. 使用 appx 包分析工具
npm install -g @alipay/appx
appx-cli analyze ./dist

# 2. 常用优化策略
  • 图片全部上传 CDN:本地只留占位图,真实图片通过 CDN 加载
  • 分包加载:将支付页、商品详情页等不常访问的页面放入分包
  • 删除未使用的组件:支付宝小程序即使 import 了但未使用的组件也会被打包
  • 字体用系统默认:不要引入自定义字体文件
// app.json 分包配置示例
{
  "pages": [
    "pages/index/index",
    "pages/product/product",
    "pages/order/order"
  ],
  "subPackages": [
    {
      "root": "package-pay",
      "pages": ["pages/pay/pay", "pages/result/result"]
    },
    {
      "root": "package-user",
      "pages": ["pages/profile/profile", "pages/history/history"]
    }
  ],
  "preloadRule": {
    "pages/index/index": {
      "packages": ["package-pay"]
    }
  }
}

坑八:onShow 重复触发导致支付状态混乱

问题描述

用户支付成功后返回小程序,onShow 回调会触发多次(每次页面切入前台都会被触发),导致重复查询订单状态,界面闪烁,或者出现"支付成功"弹窗连续弹出多次。

解决方案

// 使用防抖机制处理 onShow
Page({
    data: {
        orderChecked: false
    },
    
    onShow() {
        if (this.data.orderChecked) return;
        
        // 检查是否为支付回调
        const pages = getCurrentPages();
        const currentPage = pages[pages.length - 1];
        const options = currentPage.options || {};
        
        if (options.from === 'pay') {
            this.setData({ orderChecked: true });
            
            // 查询订单状态
            this.queryOrderStatus(options.orderId).then(status => {
                if (status === 'paid') {
                    my.showToast({ content: '支付成功!' });
                }
                // 重置标记,避免影响下次访问
                setTimeout(() => {
                    this.setData({ orderChecked: false });
                }, 3000);
            });
        }
    },
    
    // 页面卸载时重置
    onUnload() {
        this.setData({ orderChecked: false });
    }
});

踩坑汇总清单

#坑点核心解决优先级
1tradeNO 为空服务端字段名转驼峰⭐⭐⭐
2授权弹窗静默拒绝按钮触发 + 引导跳设置⭐⭐⭐
3回调通知验签失败参数排序 + URL 解码⭐⭐⭐
4上传图片 403Nginx 放行 referer + 域名白名单⭐⭐
5审核被拒测试入口 + 类目匹配⭐⭐⭐
6真机 invalid ip全站 HTTPS⭐⭐⭐
7包体积超限CDN 图片 + 分包加载⭐⭐
8onShow 重复触发防抖标记 + 状态检查⭐⭐

写在最后

支付宝小程序开发最让人头疼的不是技术难度本身,而是那些藏在文档角落里的边界条件和那些"玄学"一样的审核标准。但一旦把这些坑填平了,开发效率会大大提升。

如果你正在开发一个涉及支付的小程序,推荐直接在 源码商城 上选购成熟的支付系统源码——比如我们的 转卡码系统 V3Codex Desktop,配合 智能入口池加权分发系统,可以让你省去大量调支付接口的时间,专注于业务逻辑开发。