uniapp多端商城中支付宝小程序端如何实现支付宝支付?
结论
支付宝小程序不能直接复用 H5 的支付宝支付代码。H5 一般通过网页跳转、提交表单或唤起支付宝 App 完成支付;支付宝小程序则需要由服务端创建交易,再通过小程序中的 my.tradePay,或当前框架版本支持的 uni.requestPayment,调起支付宝收银台。
完整流程如下:
- 小程序向商城后端提交业务订单号。
- 后端校验订单,调用支付宝服务端接口创建交易。
- 后端把支付宝返回的
trade_no传给小程序。 - 小程序调用
my.tradePay发起支付。 - 后端接收支付宝异步通知,确认最终支付结果。
不能只根据前端回调判断支付是否成功。最终结果必须以支付宝异步通知或主动查询结果为准,并且通知内容需要经过后端验签。
H5 支付代码为什么不能直接复用
支付宝 H5 支付和支付宝小程序支付处在不同的运行环境中:
- H5 支付通常调用
alipay.trade.wap.pay,由接口返回跳转地址或 HTML 表单。 - 支付宝小程序运行在支付宝客户端内,不适合通过网页表单跳转支付。
- 小程序需要取得支付宝交易号,再调用小程序支付 API 唤起收银台。
- 商户私钥、签名和订单金额校验必须放在服务端,不能写进 uni-app 前端代码。
商城原有的业务订单、金额计算、支付记录和回调处理逻辑可以继续使用,但支付接口和前端唤起方式需要针对小程序单独适配。
接入前的准备
接入前需要完成支付宝开放平台的相关配置:
- 创建支付宝小程序应用,或使用已有应用。
- 为应用开通对应的支付能力并完成商户签约。
- 配置应用公钥或支付宝公钥证书。
- 将商户私钥保存在服务端,不能交给前端。
- 配置支付异步通知地址
notify_url。 - 确认小程序应用、支付宝商户账号和支付产品之间已经正确绑定。
支付宝开放平台的应用类型、签名模式和 SDK 版本不同,所需配置也可能不同。具体应以当前应用后台显示的能力和支付宝开放平台文档为准。
整体支付流程
支付宝小程序
↓ 提交商城订单号
商城后端
↓ 校验订单状态、金额和用户
支付宝服务端支付接口
↓ 返回 trade_no
商城后端
↓ 返回 trade_no
支付宝小程序
↓ my.tradePay({ tradeNO })
支付宝收银台
↓ 异步通知 notify_url
商城后端
↓ 验签并更新订单
小程序查询订单最终状态
前端只需提交商城自己的订单号,例如 orderId,不能由前端决定实际支付金额。后端收到请求后,应从数据库重新读取订单金额。
uni-app 前端实现
可以使用条件编译,为支付宝小程序编写单独的支付逻辑:
<script>
export default {
methods: {
async pay(orderId) {
// #ifdef MP-ALIPAY
await this.payByAlipayMiniProgram(orderId)
// #endif
// #ifdef H5
await this.payByAlipayH5(orderId)
// #endif
},
async payByAlipayMiniProgram(orderId) {
try {
const [requestError, response] = await uni.request({
url: 'https://api.example.com/payment/alipay/mini/create',
method: 'POST',
data: {
orderId
},
header: {
Authorization: `Bearer ${uni.getStorageSync('token')}`
}
})
if (requestError) {
throw requestError
}
const result = response.data
if (!result || result.code !== 0 || !result.data?.tradeNo) {
throw new Error(result?.message || '创建支付宝交易失败')
}
const tradeNo = result.data.tradeNo
my.tradePay({
tradeNO: tradeNo,
success: async (payResult) => {
/*
* 前端 success 仅表示支付流程返回,
* 不能直接据此将商城订单标记为已支付。
*/
await this.confirmOrderStatus(orderId)
},
fail: (error) => {
console.error('支付宝支付调用失败:', error)
uni.showToast({
title: '支付未完成',
icon: 'none'
})
}
})
} catch (error) {
console.error('创建支付失败:', error)
uni.showToast({
title: error.message || '支付请求失败',
icon: 'none'
})
}
},
async confirmOrderStatus(orderId) {
const [error, response] = await uni.request({
url: `https://api.example.com/orders/${orderId}/payment-status`,
method: 'GET',
header: {
Authorization: `Bearer ${uni.getStorageSync('token')}`
}
})
if (error) {
uni.showToast({
title: '支付结果确认中,请稍后查看订单',
icon: 'none'
})
return
}
const paid = response.data?.data?.paid === true
if (paid) {
uni.showToast({
title: '支付成功',
icon: 'success'
})
uni.redirectTo({
url: `/pages/order/detail?id=${encodeURIComponent(orderId)}`
})
} else {
uni.showToast({
title: '支付结果确认中',
icon: 'none'
})
}
},
async payByAlipayH5(orderId) {
// 保留商城原有的 H5 支付实现
}
}
}
</script>
my.tradePay 是支付宝小程序的原生 API,相关代码只能编译到 MP-ALIPAY 环境。否则在 H5、微信小程序等平台中会出现 my is not defined。
如果当前 uni-app 版本明确支持支付宝小程序的 uni.requestPayment,也可以改用统一接口,但需要按照对应版本的文档传入 orderInfo。不同版本对支付宝小程序支付参数的封装可能有差异,因此在实际项目中,直接使用 my.tradePay 往往更清楚。
服务端创建支付宝交易
服务端应通过支付宝官方 SDK 调用适用于小程序场景的交易创建接口。下面的代码只展示基本处理结构,应用配置、SDK 初始化方式和具体响应类型,需要根据项目使用的支付宝 SDK 版本调整。
@PostMapping("/payment/alipay/mini/create")
public ApiResponse<CreatePaymentResponse> createPayment(
@RequestBody CreatePaymentRequest request,
LoginUser loginUser) throws Exception {
// 1. 从数据库读取订单,不能使用前端传入的金额
Order order = orderService.getById(request.getOrderId());
if (order == null || !order.getUserId().equals(loginUser.getId())) {
throw new BusinessException("订单不存在");
}
if (order.isPaid()) {
throw new BusinessException("订单已经支付");
}
if (order.isClosed()) {
throw new BusinessException("订单已关闭");
}
// 2. 生成或读取稳定的商户支付单号
String outTradeNo = paymentService.getOrCreateOutTradeNo(order);
AlipayTradeCreateRequest alipayRequest =
new AlipayTradeCreateRequest();
alipayRequest.setNotifyUrl(
"https://api.example.com/payment/alipay/notify"
);
AlipayTradeCreateModel model = new AlipayTradeCreateModel();
model.setOutTradeNo(outTradeNo);
model.setTotalAmount(order.getPayAmount().toPlainString());
model.setSubject("商城订单-" + order.getOrderNo());
/*
* buyerId 应由用户在支付宝小程序中授权后,
* 由服务端使用 authCode 换取并绑定到商城用户。
*
* 部分新接口或应用配置可能使用其他用户标识字段,
* 应以实际采用的支付宝接口模型为准。
*/
model.setBuyerId(loginUser.getAlipayUserId());
alipayRequest.setBizModel(model);
AlipayTradeCreateResponse alipayResponse =
alipayClient.execute(alipayRequest);
if (!alipayResponse.isSuccess()) {
throw new BusinessException(
"创建支付宝交易失败:" + alipayResponse.getSubMsg()
);
}
return ApiResponse.success(
new CreatePaymentResponse(alipayResponse.getTradeNo())
);
}
常用的业务参数包括:
{
"out_trade_no": "商城支付单号",
"total_amount": "实际支付金额",
"subject": "订单标题",
"buyer_id": "支付宝用户标识"
}
out_trade_no 必须唯一,并且在同一笔支付中保持稳定。用户对同一笔商城订单重复点击支付时,后端不应每次都创建新的支付单,否则容易引发重复交易和对账问题。
获取支付宝用户标识
如果交易创建接口要求提供支付宝用户标识,可以先在小程序端获取授权码:
my.getAuthCode({
scopes: ['auth_user'],
success: async ({ authCode }) => {
const [error, response] = await uni.request({
url: 'https://api.example.com/auth/alipay',
method: 'POST',
data: {
authCode
}
})
if (error) {
console.error('支付宝授权失败:', error)
return
}
console.log('登录或绑定结果:', response.data)
},
fail: (error) => {
console.error('用户授权失败:', error)
}
})
服务端收到 authCode 后,通过支付宝服务端授权接口换取用户身份信息,再把支付宝用户标识与商城账号绑定。
authCode 有时效限制,通常也只能按照规定使用。它不能长期保存在前端,更不能直接作为支付交易号传给 my.tradePay。
处理异步通知
支付完成后,支付宝会请求服务端配置的 notify_url。后端至少需要完成以下校验和处理:
- 使用支付宝公钥或证书验证通知签名。
- 检查
app_id是否属于当前应用。 - 检查
out_trade_no是否存在。 - 对比
total_amount和数据库中的订单金额,确保两者完全一致。 - 检查商户身份相关字段。
- 确认交易状态属于支付成功状态。
- 在数据库事务中更新支付单和商城订单。
- 保证重复通知也能安全处理,即接口需要具备幂等性。
- 处理成功后,按照支付宝协议返回指定的成功响应。
伪代码如下:
@PostMapping("/payment/alipay/notify")
public String receiveNotify(HttpServletRequest request) {
Map<String, String> params = extractAlipayParams(request);
boolean valid = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
if (!valid) {
return "failure";
}
String outTradeNo = params.get("out_trade_no");
String tradeNo = params.get("trade_no");
String tradeStatus = params.get("trade_status");
BigDecimal totalAmount =
new BigDecimal(params.get("total_amount"));
Payment payment = paymentService.getByOutTradeNo(outTradeNo);
if (payment == null) {
return "failure";
}
if (payment.getAmount().compareTo(totalAmount) != 0) {
return "failure";
}
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
paymentService.markPaidIdempotently(
payment.getId(),
tradeNo,
params
);
}
return "success";
}
示例中的验签参数只用于说明处理方式。证书模式、公钥模式以及不同 SDK 版本使用的验签方法可能不同,实际代码必须与项目配置保持一致。
查询支付结果
用户完成支付后,可能马上关闭页面,也可能遇到支付宝异步通知尚未到达后端的情况。因此,前端收到支付回调后,可以继续查询商城后端的订单状态:
async function queryPaymentStatus(orderId) {
const [error, response] = await uni.request({
url: `https://api.example.com/orders/${orderId}/payment-status`,
method: 'GET'
})
if (error) {
throw error
}
return response.data
}
如果商城后端暂时仍显示未支付,前端可以在短时间内轮询几次,但要限制轮询次数和间隔。必要时,后端也可以调用支付宝交易查询接口进行补偿确认。
常见问题和注意事项
商户私钥不能放进小程序
小程序代码可以被下载和分析。商户私钥、应用私钥以及支付宝 SDK 的服务端签名逻辑,都必须保留在后端。
不能相信前端传入的金额
前端只提交商城订单号。后端需要重新读取商品价格、优惠、运费和实际应付金额,同时检查订单所属用户和订单状态。
不能只根据 my.tradePay 回调发货
前端回调可能因网络中断、用户退出页面或其他异常流程而不完整。发货、充值和积分发放等操作,只能在服务端确认真实支付结果后执行。
注意参数名称
支付宝小程序调用 my.tradePay 时使用以下参数:
{
tradeNO: '支付宝交易号'
}
参数名是 tradeNO。它的值是支付宝交易号,不是商城的 orderId,也不是商户订单号 out_trade_no。
控制重复支付
支付按钮需要避免连续点击,后端也要检查以下内容:
- 订单是否已经支付;
- 支付单是否已经创建;
- 原交易是否还能继续支付;
- 是否需要先关闭旧交易,再创建新交易。
前端防抖只能改善操作体验。真正的重复支付控制必须由服务端完成。
为不同平台拆分支付接口
后端可以按照支付场景拆分接口,例如:
/payment/alipay/h5/create
/payment/alipay/mini/create
/payment/wechat/mini/create
各支付渠道可以共用订单和支付记录,但创建交易所需的参数、接口返回值和前端唤起方式应分别处理。这样既不会影响现有的 H5 支付,也方便以后增加退款、关闭交易和对账功能。
备注:内容仅供参考。
