如何使用云函数实现支付宝小程序支付功能?
明确结论
可以。云函数本身就是服务端运行环境,能够代替自建服务器创建订单、调用支付宝开放平台接口、验签并查询支付结果。
支付流程可以这样设计:
- 小程序调用云函数创建业务订单。
- 云函数调用
alipay.trade.create,获取支付宝交易号trade_no。 - 小程序通过
my.tradePay打开收银台。 - 支付结束后,云函数通过异步通知或
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
处理通知时,需要完成以下步骤:
- 读取支付宝提交的表单参数。
- 使用支付宝公钥验证签名。
- 检查
app_id是否属于当前应用。 - 检查
out_trade_no是否存在。 - 检查通知金额是否与本地订单金额一致。
- 判断
trade_status。 - 以幂等方式更新订单。
- 处理成功后,返回支付宝要求的成功响应。
伪代码如下:
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 地址,可以先使用主动查询方案:
- 小程序完成支付或关闭收银台后调用
queryPay。 - 云函数调用
alipay.trade.query。 - 对仍处于支付中的订单进行有限次数的重试。
- 使用定时云函数补查长时间未确认的订单。
这种方式能够完成基本的支付闭环,但可能出现支付已经成功,小程序却没有再次发起查询的情况。业务涉及发货、充值或数字权益时,最好使用支持公网 HTTP 触发器的云函数接收异步通知,同时通过主动查询进行补偿。
安全与业务注意事项
私钥只能保存在云端
应用私钥不能放在小程序代码、前端配置、数据库公开集合或网络请求参数中。小程序包可以被下载和分析,因此把密钥放在前端就等于泄露。
金额必须在云端计算
前端可以传入 productId、优惠券编号等业务参数,但不能决定最终金额。云函数需要重新读取商品价格、优惠规则和库存,并在云端计算 total_amount。
金额建议统一使用精确的十进制字符串,例如:
const totalAmount = '99.00';
不要用未经处理的浮点数计算金额:
// 不建议
const amount = 0.1 + 0.2;
复杂金额可以用以“分”为单位的整数计算,最后再格式化为元。
防止重复创建和重复发货
网络重试可能导致创建订单接口被多次调用。可以让前端传入一次性业务请求号,或在数据库中为业务订单号建立唯一索引。
支付通知也可能重复发送,所以发货逻辑必须具备幂等性:
CREATED -> PAYING -> PAID -> FULFILLED
只有订单状态从未支付成功变为已支付时,才执行一次权益发放。
不要只信任前端回调
以下信息都不能单独作为到账依据:
my.tradePay的成功回调。- 页面跳转结果。
- 前端上传的支付状态。
- 前端上传的
tradeNo。 - 用户截图。
可信的支付结果应来自验签通过的支付宝异步通知,或云函数调用 alipay.trade.query 后取得的支付状态。
日志中不要输出敏感信息
不要记录:
- 应用私钥。
- 完整签名原文。
- 用户完整身份信息。
- 完整支付请求或通知中的敏感字段。
可以记录业务订单号、支付宝交易号、接口错误码和必要的链路标识,方便排查问题。
推荐的最小实现顺序
可以按以下顺序实现:
- 配置支付宝应用密钥和支付权限。
- 创建云数据库订单表。
- 编写
createPay云函数。 - 在小程序中调用
my.tradePay。 - 编写
queryPay云函数确认结果。 - 增加 HTTP 异步通知和验签。
- 增加幂等更新、超时关单、退款及定时补查。
不必为了支付功能专门购买传统服务器,但服务端逻辑仍不可缺少。云函数承担的就是这个角色,负责保存密钥、签名请求、创建交易、验证通知并维护订单状态。
备注:内容仅供参考。
