PAYATHON 2026

如何使用云函数实现支付宝小程序支付功能?

支付阿杰

明确结论

可以。云函数本身就是服务端运行环境,能够代替自建服务器创建订单、调用支付宝开放平台接口、验签并查询支付结果。

支付流程可以这样设计:

  1. 小程序调用云函数创建业务订单。
  2. 云函数调用 alipay.trade.create,获取支付宝交易号 trade_no。
  3. 小程序通过 my.tradePay 打开收银台。
  4. 支付结束后,云函数通过异步通知或 alipay.trade.query 确认最终支付状态。

注意,my.tradePay 返回成功只适合用于前端展示,不能直接作为发货或开通服务的依据。最终支付结果必须由服务端确认,在这里也就是由云函数确认。

前置条件

编码前需要准备:

  • 已创建并上线的支付宝小程序。
  • 已签约适用于当前业务的支付产品。
  • 支付宝开放平台应用已配置应用公钥。
  • 已取得应用私钥和支付宝公钥。
  • 云函数可以访问公网。
  • 云开发平台能够安全地配置环境变量。
  • 如果需要接收异步支付通知,云平台还要能够提供公网 HTTPS HTTP 函数或 API 网关地址。

不同支付产品、开放平台配置方式和 SDK 版本可能有差异。具体权限和参数应以当前支付宝开放平台文档及控制台显示为准。

整体支付流程

支付宝小程序
    │
    │ 调用 createPay 云函数
    ▼
云函数创建本地订单
    │
    │ 调用 alipay.trade.create
    ▼
支付宝开放平台
    │
    │ 返回 trade_no
    ▼
支付宝小程序
    │
    │ my.tradePay({ tradeNO })
    ▼
支付宝收银台
    │
    │ 支付完成
    ▼
异步通知或主动查询
    │
    ▼
云函数确认订单并更新状态

服务端至少需要提供以下三项能力:

  • createPay:创建业务订单并取得 trade_no。
  • queryPay:主动查询支付宝订单状态。
  • payNotify:接收支付宝异步通知。如果云平台不方便提供 HTTP 入口,可以先使用主动查询方案,但正式业务通常仍应接入异步通知。

安装 Node.js SDK

在云函数项目中安装支付宝 Node.js SDK:

npm install alipay-sdk

不同版本的 alipay-sdk 可能采用不同的导入方式、返回值格式和验签方法。下面的代码用于说明完整实现思路。部署时,需要根据实际安装版本的类型定义和官方示例进行调整。

以下信息建议存入云函数环境变量:

ALIPAY_APP_ID
ALIPAY_PRIVATE_KEY
ALIPAY_PUBLIC_KEY
ALIPAY_NOTIFY_URL

私钥中的换行在环境变量里经常会变成 \n,初始化时可以将其还原:

function restoreKey(value = '') {
  return value.replace(/\\n/g, '\n');
}

创建支付宝 SDK 客户端

初始化代码可以放在公共模块中:

const AlipaySdkModule = require('alipay-sdk');

const AlipaySdk =
  AlipaySdkModule.default ||
  AlipaySdkModule.AlipaySdk ||
  AlipaySdkModule;

const alipaySdk = new AlipaySdk({
  appId: process.env.ALIPAY_APP_ID,
  privateKey: process.env.ALIPAY_PRIVATE_KEY.replace(/\\n/g, '\n'),
  alipayPublicKey: process.env.ALIPAY_PUBLIC_KEY.replace(/\\n/g, '\n'),
  signType: 'RSA2',
  gateway: 'https://openapi.alipay.com/gateway.do'
});

module.exports = alipaySdk;

生产环境应使用正式网关。沙箱网关、沙箱账号及可用能力要以支付宝开放平台当前配置为准。不能只替换网关地址,就默认所有小程序支付能力都能在沙箱中使用。

云函数创建支付订单

云函数不能直接采用小程序传入的金额。前端只应提交商品或订单标识,实际金额必须由云函数从数据库或可信配置中读取。

下面是一个简化示例:

const crypto = require('crypto');
const alipaySdk = require('./alipay');

function createOrderNo() {
  return `${Date.now()}${crypto.randomBytes(6).toString('hex')}`;
}

exports.main = async function createPay(event, context) {
  const userId = context.userId;
  const productId = event.productId;

  if (!userId) {
    throw new Error('用户未登录');
  }

  if (!productId) {
    throw new Error('缺少 productId');
  }

  // 示例数据。正式项目必须从数据库查询商品和价格。
  const product = await getProductFromDatabase(productId);

  if (!product || !product.enabled) {
    throw new Error('商品不存在或已下架');
  }

  const outTradeNo = createOrderNo();

  // 先保存业务订单,初始状态为 CREATED。
  await createOrderInDatabase({
    outTradeNo,
    userId,
    productId: product.id,
    amount: product.price,
    status: 'CREATED'
  });

  try {
    const result = await alipaySdk.exec('alipay.trade.create', {
      notify_url: process.env.ALIPAY_NOTIFY_URL,
      biz_content: {
        out_trade_no: outTradeNo,
        total_amount: product.price,
        subject: product.name,
        buyer_id: event.buyerId
      }
    });

    const tradeNo =
      result.trade_no ||
      result.tradeNo ||
      result.alipay_trade_create_response?.trade_no;

    if (!tradeNo) {
      throw new Error(`未取得 trade_no:${JSON.stringify(result)}`);
    }

    await updateOrderInDatabase(outTradeNo, {
      tradeNo,
      status: 'PAYING'
    });

    return {
      outTradeNo,
      tradeNo
    };
  } catch (error) {
    await updateOrderInDatabase(outTradeNo, {
      status: 'CREATE_FAILED'
    });

    throw error;
  }
};

示例中的 getProductFromDatabase、createOrderInDatabase 和 updateOrderInDatabase,需要替换为当前云数据库对应的 API。

buyer_id 是否必填、应从哪里获取,取决于当前支付产品、应用授权方式和接口要求。不要允许客户端随意提交其他用户的标识。该标识应通过可信的登录流程取得并完成校验。如果当前接口场景不要求这个字段,则按官方接口文档处理。

部分 SDK 使用 execute 而不是 exec,也可能要求使用驼峰参数:

const result = await alipaySdk.execute('alipay.trade.create', {
  notifyUrl: process.env.ALIPAY_NOTIFY_URL,
  bizContent: {
    outTradeNo,
    totalAmount: product.price,
    subject: product.name,
    buyerId
  }
});

两种写法不能混用,应以实际 SDK 版本的接口定义为准。

小程序打开支付宝收银台

小程序取得云函数返回的 tradeNo 后,可以调用 my.tradePay:

async function startPayment(productId) {
  try {
    const response = await callCloudFunction('createPay', {
      productId
    });

    const tradeNO = response.tradeNo;

    const payResult = await new Promise((resolve, reject) => {
      my.tradePay({
        tradeNO,
        success: resolve,
        fail: reject
      });
    });

    // 即使前端显示成功,也要向云函数查询最终状态。
    const queryResult = await callCloudFunction('queryPay', {
      outTradeNo: response.outTradeNo
    });

    if (queryResult.paid) {
      my.showToast({
        type: 'success',
        content: '支付成功'
      });
    } else {
      my.showToast({
        content: '支付结果确认中'
      });
    }
  } catch (error) {
    console.error('支付失败', error);

    my.showToast({
      content: '支付未完成'
    });
  }
}

callCloudFunction 不是支付宝提供的固定 API,需要替换成当前云开发平台的云函数调用方法。

不要根据某个固定的前端结果码,直接把订单改为已支付。用户可能在支付过程中关闭收银台,也可能遇到前端回调丢失、网络中断或支付结果延迟。

云函数查询支付结果

订单状态可以通过 alipay.trade.query 查询:

const alipaySdk = require('./alipay');

exports.main = async function queryPay(event, context) {
  const userId = context.userId;
  const outTradeNo = event.outTradeNo;

  if (!userId || !outTradeNo) {
    throw new Error('请求参数不完整');
  }

  const order = await getOrderFromDatabase(outTradeNo);

  if (!order || order.userId !== userId) {
    throw new Error('订单不存在');
  }

  const result = await alipaySdk.exec('alipay.trade.query', {
    biz_content: {
      out_trade_no: outTradeNo
    }
  });

  const tradeStatus =
    result.trade_status ||
    result.tradeStatus ||
    result.alipay_trade_query_response?.trade_status;

  const paid =
    tradeStatus === 'TRADE_SUCCESS' ||
    tradeStatus === 'TRADE_FINISHED';

  if (paid && order.status !== 'PAID') {
    await markOrderPaidSafely(outTradeNo, {
      tradeNo:
        result.trade_no ||
        result.tradeNo ||
        result.alipay_trade_query_response?.trade_no,
      paidAt: new Date().toISOString()
    });
  }

  return {
    paid,
    tradeStatus: tradeStatus || 'UNKNOWN'
  };
};

更新订单时,应使用数据库事务、条件更新或唯一约束,确保同一个订单只会执行一次发货、充值或权益开通操作。

接收支付宝异步通知

如果云开发平台支持 HTTP 云函数,可以把通知地址配置为公开的 HTTPS 地址:

https://example.com/alipay/pay-notify

处理通知时,需要完成以下步骤:

  1. 读取支付宝提交的表单参数。
  2. 使用支付宝公钥验证签名。
  3. 检查 app_id 是否属于当前应用。
  4. 检查 out_trade_no 是否存在。
  5. 检查通知金额是否与本地订单金额一致。
  6. 判断 trade_status。
  7. 以幂等方式更新订单。
  8. 处理成功后,返回支付宝要求的成功响应。

伪代码如下:

exports.handler = async function payNotify(request) {
  const params = parseFormBody(request.body);

  const verified = verifyAlipaySignature(params);

  if (!verified) {
    return {
      statusCode: 400,
      body: 'failure'
    };
  }

  const order = await getOrderFromDatabase(params.out_trade_no);

  if (!order) {
    return {
      statusCode: 404,
      body: 'failure'
    };
  }

  if (params.app_id !== process.env.ALIPAY_APP_ID) {
    return {
      statusCode: 400,
      body: 'failure'
    };
  }

  if (String(params.total_amount) !== String(order.amount)) {
    return {
      statusCode: 400,
      body: 'failure'
    };
  }

  if (
    params.trade_status === 'TRADE_SUCCESS' ||
    params.trade_status === 'TRADE_FINISHED'
  ) {
    await markOrderPaidSafely(params.out_trade_no, {
      tradeNo: params.trade_no,
      paidAt: params.gmt_payment
    });
  }

  return {
    statusCode: 200,
    headers: {
      'content-type': 'text/plain'
    },
    body: 'success'
  };
};

parseFormBody 和 verifyAlipaySignature 的具体实现,取决于云函数的 HTTP 事件格式和 SDK 版本。异步通知通常不是 JSON,需要确认云平台是否已经解析 application/x-www-form-urlencoded 请求体。

验签必须使用支付宝公钥,不能使用应用公钥或应用私钥。还要保留支付宝发送的原始参数值,避免在验签前改变字符编码、空格或字段内容。

没有 HTTP 云函数怎么办

如果当前云开发环境只能由小程序主动调用,无法提供公网 HTTP 地址,可以先使用主动查询方案:

  1. 小程序完成支付或关闭收银台后调用 queryPay。
  2. 云函数调用 alipay.trade.query。
  3. 对仍处于支付中的订单进行有限次数的重试。
  4. 使用定时云函数补查长时间未确认的订单。

这种方式能够完成基本的支付闭环,但可能出现支付已经成功,小程序却没有再次发起查询的情况。业务涉及发货、充值或数字权益时,最好使用支持公网 HTTP 触发器的云函数接收异步通知,同时通过主动查询进行补偿。

安全与业务注意事项

私钥只能保存在云端

应用私钥不能放在小程序代码、前端配置、数据库公开集合或网络请求参数中。小程序包可以被下载和分析,因此把密钥放在前端就等于泄露。

金额必须在云端计算

前端可以传入 productId、优惠券编号等业务参数,但不能决定最终金额。云函数需要重新读取商品价格、优惠规则和库存,并在云端计算 total_amount。

金额建议统一使用精确的十进制字符串,例如:

const totalAmount = '99.00';

不要用未经处理的浮点数计算金额:

// 不建议
const amount = 0.1 + 0.2;

复杂金额可以用以“分”为单位的整数计算,最后再格式化为元。

防止重复创建和重复发货

网络重试可能导致创建订单接口被多次调用。可以让前端传入一次性业务请求号,或在数据库中为业务订单号建立唯一索引。

支付通知也可能重复发送,所以发货逻辑必须具备幂等性:

CREATED -> PAYING -> PAID -> FULFILLED

只有订单状态从未支付成功变为已支付时,才执行一次权益发放。

不要只信任前端回调

以下信息都不能单独作为到账依据:

  • my.tradePay 的成功回调。
  • 页面跳转结果。
  • 前端上传的支付状态。
  • 前端上传的 tradeNo。
  • 用户截图。

可信的支付结果应来自验签通过的支付宝异步通知,或云函数调用 alipay.trade.query 后取得的支付状态。

日志中不要输出敏感信息

不要记录:

  • 应用私钥。
  • 完整签名原文。
  • 用户完整身份信息。
  • 完整支付请求或通知中的敏感字段。

可以记录业务订单号、支付宝交易号、接口错误码和必要的链路标识,方便排查问题。

推荐的最小实现顺序

可以按以下顺序实现:

  1. 配置支付宝应用密钥和支付权限。
  2. 创建云数据库订单表。
  3. 编写 createPay 云函数。
  4. 在小程序中调用 my.tradePay。
  5. 编写 queryPay 云函数确认结果。
  6. 增加 HTTP 异步通知和验签。
  7. 增加幂等更新、超时关单、退款及定时补查。

不必为了支付功能专门购买传统服务器,但服务端逻辑仍不可缺少。云函数承担的就是这个角色,负责保存密钥、签名请求、创建交易、验证通知并维护订单状态。

备注:内容仅供参考。