你页面里只是“网页代码(Vue H5)”,它自己没有能力去调用手机里的支付宝 App 或原生 SDK。
uni-app 的 5+ 容器给这个网页注入了一个“原生桥 plus”,你通过 plus.payment.request 把“支付意图”和“已签名的订单字符串 orderString”交给原生层;
原生层再去调用支付宝官方 SDK(Android 的 PayTask.payV2 / iOS 对应能力)唤起支付宝 App,支付结束把结果带回原生,再通过 plus 把结果回传给你的网页。
下面按“从前端到后端、从 H5 到原生”的顺序详细解释原理与链路。
一、为什么纯前端(Vue3)不能直接调支付宝支付
- 支付宝 App 支付有两条常见路线:
- App 支付:使用“alipay.trade.app.pay”。核心是一个由你的服务端用私钥签名生成的 orderString。只有原生 SDK(PayTask.payV2)或支付宝 JSAPI(在支付宝内部 WebView)能正确拉起客户端。
- WAP 支付:使用“alipay.trade.wap.pay”。前端跳转支付宝的收银台网页(表单跳转或 URL),用户在支付宝 H5 收银台完成支付。
- 浏览器里的普通网页没有权限/能力打开系统级支付 App 并拿到支付结果(除非在支付宝内置浏览器)。因此纯 Vue3 H5 不能直接调用 PayTask.payV2,它是原生代码(Java/Objective‑C/Swift)。
二、5+ 容器是怎么“给网页加原生能力”的
- uni-app 打包 App 的底层是 5+ Runtime,它本质是在一个原生 WebView 上加载你的网页,同时往里“注入”了一个全局对象 window.plus。
- plus 是一组 JS API,它们通过“JS Bridge(桥)”把你的 JS 调用转发给原生代码,再把原生回调透传回来。
- 支付相关的桥就是 plus.payment:
- plus.payment.getChannels:告诉你哪些支付通道可用(alipay、wxpay 等)。
- plus.payment.request(channel, orderInfo, success, fail):把 orderString 交给原生,原生再调支付宝 SDK,拉起支付宝 App。
- 所以你在 H5 里看到的 plus.payment.request,底层其实是:JS -> 5+ JS Bridge -> Android/iOS 原生层 -> 支付宝 SDK -> 支付宝 App -> 返回结果 -> 原生层 -> JS Bridge -> 你的网页回调。
三、后端为什么必须生成并签名 orderString
- App 支付的参数不是简单的 JSON 或表单,而是一个“签过名”的长字符串(orderString),包括:
- app_id、method=alipay.trade.app.pay、charset、sign_type=RSA2、timestamp、version、notify_url、biz_content(里面有 out_trade_no、total_amount、subject、product_code=QUICK_MSECURITY_PAY 等);
- sign:用你的商户私钥对以上参数按固定规则签名的结果。
- 签名必须在你的服务端完成,原因:
- 私钥不能放在前端(被抓包或反编译就泄露了,会被伪造支付请求)。
- 订单金额、商户号、回调地址等都要在服务端做“可信控制”。
- 前端拿到的就是 orderString 这个“成品”,只负责把它交给原生去唤起支付宝。
四、一次完整支付的时序(App 支付)
- 前端(H5/你页面):
- 调你自己后端接口,请求创建订单并返回 orderString。
- 调 plus.payment.request('alipay', orderString, ...)。
- 5+ 原生容器: 3) 收到请求后在原生层调用支付宝 SDK(Android 的 PayTask.payV2)。 4) SDK 发起与支付宝 App 的交互(Intent/URL Scheme),拉起支付宝 App。
- 支付宝 App: 5) 展示收银台,用户付款。 6) 完成后把结果返回给调用方 App(你的 5+ 容器)。
- 5+ 原生容器: 7) 原生收到结果,回调给 plus.payment.request 的 success/fail。
- 服务端(强制性可靠确认): 8) 支付宝服务器以 notify_url 异步通知你的后端付款结果(这个才是最终可信结果)。 9) 你的后端更新订单状态;前端可轮询/查询最终状态。
五、为什么要等 plusready
- 5+ 在启动 WebView 后才把 plus 对象注入进去,中间有一点点时间差。
- 只有在 plusready 事件触发后,window.plus 才可用。否则你在页面很早期就调用 plus.payment.request 会报“plus 未定义”。
六、uni.requestPayment 和 plus.payment 的关系
- 在“原生 App 端”的 uni-app 项目里,uni.requestPayment(provider: 'alipay') 底层就是去调用 plus.payment.request。
- 但你现在的形态是“独立打包的 Vue3 H5 产物”塞入 5+ App WebView 运行,很可能页面内没有 uni 全局对象(除非你在 uni 的工程里构建)。这种情况下,直接用 plus.payment 是最稳的。
- 两者都依赖同一个原生支付模块(支付宝 SDK),所以都要求打包时在 manifest.json 勾选“支付宝支付”模块,并用包含该模块的基座/打包产物运行。
七、App 支付 vs WAP 支付 vs 支付宝内 H5
- App 支付(你现在用的):alipay.trade.app.pay + 原生 SDK(plus.payment/uni.requestPayment 调用)。体验最好,但要求打包到 App,且安装支付宝客户端。
- WAP 支付(普通浏览器):alipay.trade.wap.pay。前端跳转到支付宝 H5 收银台,付款后再返回你的页面。无需 App 原生模块,但体验略差、可被浏览器环境影响。
- 支付宝内置浏览器 H5:可用 AlipayJSBridge.call('tradePay', { orderStr }),因为支付宝自己的 WebView 注入了 AlipayJSBridge(另一种“桥”)。
八、常见问题与排查
- 页面提示 plus 未定义
- 说明当前不是 5+ App 环境或 plusready 未触发。需要真机运行在 5+ 容器里,且在 plusready 后再调用。
- 提示未找到支付宝支付通道
- manifest.json 未勾选“支付宝支付”,或调试基座不包含支付模块。需用“自定义调试基座”或“云打包”,确保包含支付模块。
- 拉起了支付宝但你的前端没收到最终状态
- 客户端回调只代表客户端侧“发起/返回”,最终支付是否成功要以后端的异步通知 notify_url 为准,或你在前端引导用户回到订单页后让后端查询。
- iOS 返回不了 App
- 一般由原生层配置 URL Scheme/Universal Links 处理。5+ 打包模板会替你处理,但必须用包含支付模块的正确打包配置。
九、工作流程
- 你从后端拿到 orderString。
- 你调用 payWithPlusAlipay(orderString):内部
- 等 plusready
- plus.payment.getChannels 找到 'alipay'
- plus.payment.request('alipay', orderString, success, fail)
- 原生 SDK 调起支付宝 App,用户完成支付后返回
- 原生把结果回调给 success/fail,再回到你的 JS Promise resolve/reject
- 你在 then/catch 里提示用户,同时让后端以 notify_url 最终落账。
十、示例代码
// 支付宝支付 - 仅支持Android APP
/**
* 等待 5+ 运行时(plus)准备就绪。
* 在 HBuilderX 打包的 App 中,plus 对象由原生层注入,可能晚于 JS 执行。
* 若 plus 已存在则立即 resolve;否则监听 plusready 事件,事件触发后 resolve。
* @returns {Promise<void>} Promise 对象,resolve 表示 plus 已可用
*/
function ensurePlusReady() {
return new Promise((resolve) => {
// 如果 plus 已存在,直接返回
if (window.plus) return resolve();
// 监听 plusready 事件,触发后移除监听器并 resolve
document.addEventListener('plusready', function onReady() {
document.removeEventListener('plusready', onReady);
resolve();
});
});
}
/**
* 全局缓存:已获取的支付宝原生支付通道(5+ API)。
* 避免重复调用 plus.payment.getChannels,提升性能。
*/
let __alipayChannel = null;
/**
* 获取支付宝原生支付通道(仅 5+ App 环境有效)。
* 首次调用时通过 5+ API 枚举所有支付通道,并缓存支付宝通道。
* @returns {Promise<Object>} 支付宝通道对象(含 id、description 等字段)
* @throws {Error} 若当前非 5+ 环境或打包时未勾选“支付宝支付”模块,则抛出异常
*/
async function getAlipayChannel() {
// 确保 plus API 已就绪(避免在 H5 或非 App 环境调用)
await ensurePlusReady();
// 若已缓存,直接返回,避免重复枚举
if (__alipayChannel) return __alipayChannel;
// 通过 5+ API 异步获取所有支付通道
const channels = await new Promise((resolve, reject) => {
plus.payment.getChannels(resolve, reject);
});
// 在所有通道中筛选出支付宝通道
const ali = channels.find(c => c.id === 'alipay');
// 若找不到,说明打包时未集成支付宝模块,给出明确提示
if (!ali) {
throw new Error('未找到支付宝支付通道,请确认打包时勾选了“支付宝支付”模块');
}
// 缓存并返回
__alipayChannel = ali;
return ali;
}
/**
* 在 5+ App(HBuilderX 打包的 App)环境下,
* 使用原生支付通道调用支付宝 SDK 完成支付。
* @param {string} orderInfo 后端返回的、已签名的支付宝 orderString
* @returns {Promise<any>} 支付结果,resolve 表示客户端侧“调起成功”,reject 表示“调起失败”
*/
async function payWithPlusAlipay(orderInfo) {
// 参数校验:orderInfo 必须是字符串
if (!orderInfo || typeof orderInfo !== 'string') {
throw new Error('orderString 无效');
}
// 获取支付宝原生支付通道(5+ API)
const ali = await getAlipayChannel();
// 返回 Promise,方便外部使用 async/await 或 .then/.catch
return new Promise((resolve, reject) => {
// 5+ 支付接口:channel 为支付通道,orderInfo 为订单字符串
plus.payment.request(ali, orderInfo, resolve, reject);
});
}
/**
* 支付宝支付主函数
* 1. 计算订单总价与商品名称
* 2. 开启加载动画并关闭底部弹窗
* 3. 向后端请求已签名的 orderString(alipay.trade.app.pay)
* 4. 仅在 5+ App 环境内调用原生支付宝 SDK 完成支付
* 5. 支付成功/失败均给出提示,最终订单状态以服务端异步通知或主动查询为准
* 6. 无论成功失败,最终关闭加载动画
*/
let funzhifub = async () => {
/* -------------------- ① 准备支付参数 -------------------- */
// 计算订单总价:商品单价 × 购买数量
const jiage = Number(dingdan.value.now) * Number(isnumber);
// 商品名称,用于支付订单描述
const shoppingname = dingdan.value.name;
/* -------------------- ② 交互状态控制 -------------------- */
// 打开“正在处理支付”的全屏加载动画
isLoading.value = true;
// 关闭底部支付方式选择弹窗
showBottom.value = false;
try {
/* ---------------- ③ 向后端获取签名后的订单字符串 --------------- */
const iszhifubao = await request({
url: '你的请求地址',
});
// 取出后端返回的 orderString(已签名,可直接给支付宝 SDK)
const orderInfo = iszhifubao.data.orderString;
if (!orderInfo) throw new Error('未获取到 orderString');
/* ---------------- ④ 环境校验 & 调起支付宝 SDK --------------- */
// 仅在 5+ App(HBuilderX 打包)环境内才能调用原生支付
if (!window.plus) {
throw new Error('当前非 5+ App 环境,无法调用支付宝支付');
}
// 使用前面封装的 payWithPlusAlipay 调起支付宝
const res = await payWithPlusAlipay(orderInfo);
/* ---------------- ⑤ 客户端侧结果提示(非最终)--------------- */
// 走到这里仅表示“调起成功”,用户是否真正付款成功,
// 需以后端异步通知或主动查询订单接口为准
ElMessage.success('已拉起支付宝,请完成支付');
// 可选:立即主动查询一次订单状态
// await request({ url: '/order/query', method: 'get', params: { orderId: oredrId.value } });
return res;
} catch (error) {
/* ---------------- ⑥ 异常处理 & 用户提示 ---------------- */
console.error('支付宝支付失败/异常:', error);
ElMessage.error(error.message || '支付调用失败,请稍后重试');
// 继续向外抛出,方便调用方统一处理
throw error;
} finally {
/* ---------------- ⑦ 最终状态清理 ---------------- */
// 无论成功或失败,都要关闭加载动画
isLoading.value = false;
}
};

参与讨论
(Participate in the discussion)
参与讨论
没有发现评论
暂无评论