/** * 微赞 H5 唤起小程序支付桥接。 * * H5 下单时通过原生 JSBridge 调用 APP 暴露的钩子方法: * - Android: window.android.jumpToMiniProgramPay(JSON.stringify(params)) * - iOS: window.webkit.messageHandlers.jumpToMiniProgramPay.postMessage(JSON.stringify(params)) * * params 字段(由微赞 H5 透传): * - orderId: 订单号(用于日志/埋点,不参与拉起小程序) * - path: 小程序支付页路径 * - username: 小程序原始 ID(gh_ 开头) * - miniprogramType:小程序类型,固定 0 * * 由 APP 用 plus.share.launchMiniProgram 拉起微信小程序完成支付(仅易宝支付)。 * 微信 appId: wx074c1f0cf5a0ecf4(见 manifest.json app-plus.distribute.sdkConfigs)。 */ const PAY_BRIDGE_NAME = 'jumpToMiniProgramPay' const PAY_BRIDGE_ANDROID_KEY = 'android' // H5 调 window.android.xxx const LOG_TAG = '[weizanPay]' /** * 给指定 webview 注册微赞支付 JSBridge。 * 仅在 APP-PLUS 环境生效;非 APP 环境(H5/小程序)静默 no-op。 * * @param {Object} webviewInstance plus.webview 实例(plus.webview.currentWebview() 拿到) * @param {Function} [onPay] 收到 H5 回调时透传 (params),可在此做埋点/校验,不影响唤起 */ export function registerWeizanPayBridge(webviewInstance, onPay) { // #ifdef APP-PLUS if (!webviewInstance) { return } const platform = String(uni.getSystemInfoSync().platform || '').toLowerCase() if (platform === 'android') { registerAndroidPayBridge(webviewInstance, onPay) } else if (platform === 'ios') { registerIOSPayBridge(webviewInstance, onPay) } // #endif } /** * 反注册微赞支付 JSBridge,避免 webview 复用导致的内存泄漏与重复回调。 * * @param {Object} webviewInstance plus.webview 实例 */ export function unregisterWeizanPayBridge(webviewInstance) { // #ifdef APP-PLUS if (!webviewInstance) { return } try { const platform = String(uni.getSystemInfoSync().platform || '').toLowerCase() if (platform === 'android') { // Android 移除注入的 JS 接口(同样要作用在原生 WebView 上) try { const nativeWebView = getAndroidNativeWebView(webviewInstance) if (nativeWebView) { plus.android.invoke(nativeWebView, 'removeJavascriptInterface', PAY_BRIDGE_ANDROID_KEY) } } catch (e) {} } // iOS:WKUserContentController 的 messageHandler 通常随 webview 销毁自动回收, // 显式 removeScriptMessageHandlerForName 在 plus.ios 上不一定支持,这里不强行调用 } catch (e) { console.error('[weizanPay] unregister fail:', e) } // #endif } /** * 钩子方法:唤起微信小程序完成支付。 * orderId / path / username / miniprogramType 由微赞 H5 回调时传入。 * * @param {Object} params { orderId, path, username, miniprogramType } */ export function jumpToMiniProgramPay(params) { // #ifdef APP-PLUS console.log('[weizanPay] >>> jumpToMiniProgramPay 被调用, 原始入参:', params, '类型:', typeof params) const safeParams = normalizePayParams(params) console.log('[weizanPay] 归一化后参数:', JSON.stringify(safeParams)) if (!safeParams.username) { console.warn('[weizanPay] username 缺失,终止唤起。safeParams=', JSON.stringify(safeParams)) uni.showToast({ title: '支付参数缺失', icon: 'none' }) return } console.log('[weizanPay] 参数校验通过,开始调用 plus.share.getServices 唤起小程序') plus.share.getServices((services) => { const wx = services && services.find((s) => s && s.id === 'weixin') if (!wx) { uni.showToast({ title: '当前环境不支持微信', icon: 'none' }) return } if (!wx.nativeClient) { uni.showToast({ title: '请先安装微信客户端', icon: 'none' }) return } wx.launchMiniProgram({ id: safeParams.username, path: safeParams.path || '', type: Number(safeParams.miniprogramType) || 0, }, () => { console.log('[weizanPay] 唤起小程序成功, orderId=', safeParams.orderId) }, (err) => { console.error('[weizanPay] 唤起失败', err) const errMsg = String(err && (err.message || err.errMsg) || '') uni.showToast({ title: errMsg.indexOf('badprogram') >= 0 ? '请检查小程序原始ID及关联配置' : '唤起支付失败', icon: 'none', }) }) }, (err) => { console.error('[weizanPay] 微信服务不可用', err) uni.showToast({ title: '微信服务不可用', icon: 'none' }) }) // #endif } // ===== Android 实现:注入 window.android.jumpToMiniProgramPay ===== function registerAndroidPayBridge(webviewInstance, onPay) { console.log(LOG_TAG, '[Android] 开始注册 JSBridge,注入 key=', PAY_BRIDGE_ANDROID_KEY, '方法名=', PAY_BRIDGE_NAME) console.log(LOG_TAG, '[Android] webviewInstance=', webviewInstance ? '[Object]' : 'null') // plus.webview.create 返回的是 JS 包装对象,不能直接反射; // 必须先取底层原生 WebView(5+ ADWebView,是 android.webkit.WebView 的子类)再反射, // 否则 getSettings / addJavascriptInterface 都会失败。 const nativeWebView = getAndroidNativeWebView(webviewInstance) console.log(LOG_TAG, '[Android] nativeInstanceObject=', nativeWebView ? '[Object]' : 'null') // 反射验证是否是真原生 WebView(应暴露 getSettings) const settings = safeAndroidInvoke(nativeWebView, 'getSettings') console.log(LOG_TAG, '[Android] getSettings 反射结果=', settings ? '[Object]' : 'null') if (!settings) { console.error(LOG_TAG, '[Android] 未取到原生 WebView 或不暴露 getSettings,注册中止') console.error(LOG_TAG, '[Android] 请确认调用方传入的是 plus.webview.create 创建的子 webview,而不是 currentWebview()') return } try { // java.io.Serializable 一定存在、无强制方法,足以承载动态代理 // H5 调 window.android.xxx 时由 Proxy 拦截转发到 JS 函数 const JsInterface = plus.android.implements('java.io.Serializable', { [PAY_BRIDGE_NAME]: function (paramsJson) { console.log(LOG_TAG, '===== [Android] H5 调用 window.android.' + PAY_BRIDGE_NAME + ' =====') console.log(LOG_TAG, '[Android] 原始 paramsJson=', safeStringify(paramsJson)) // JS 接口回调在原生子线程,必须切回 UI 线程才能拉起微信 SDK plus.android.runOnUiThread(() => { console.log(LOG_TAG, '[Android] 已进入 UI 线程,开始解析参数') const params = normalizePayParams(parseJsonSafe(paramsJson)) console.log(LOG_TAG, '[Android] 解析后 params=', safeStringify(params)) if (typeof onPay === 'function') { try { onPay(params) } catch (e) {} } jumpToMiniProgramPay(params) }) } }) console.log(LOG_TAG, '[Android] JsInterface 已创建:', JsInterface ? '[Object]' : 'null') // 注入到原生 WebView(而非 JS 包装对象),H5 端才能拿到 window.android.xxx plus.android.invoke(nativeWebView, 'addJavascriptInterface', JsInterface, PAY_BRIDGE_ANDROID_KEY) console.log(LOG_TAG, '[Android] addJavascriptInterface 完成,window.' + PAY_BRIDGE_ANDROID_KEY + ' 已注入') // 通过 evalJS 验证 H5 端 window.android 是否真的能访问到 try { if (typeof webviewInstance.evalJS === 'function') { webviewInstance.evalJS(` try { if (typeof window.android === 'undefined' || window.android === null) { console.log('[weizanPay][H5] window.android 不存在'); } else { console.log('[weizanPay][H5] window.android 存在, keys=' + Object.keys(window.android).join(',')); console.log('[weizanPay][H5] window.android.${PAY_BRIDGE_NAME} 类型=' + typeof window.android.${PAY_BRIDGE_NAME}); } } catch (e) { console.log('[weizanPay][H5] 检查 window.android 异常: ' + e.message); } `) } } catch (e) { console.warn(LOG_TAG, '[Android] evalJS 验证失败:', e && e.message) } } catch (e) { console.error(LOG_TAG, '[Android] 注册 JSBridge 失败:', e) console.error(LOG_TAG, '[Android] 异常 message=', e && e.message) } } /** * 取 plus.webview 实例底层的原生 android.webkit.WebView。 * plus.webview.create 返回的是 JS 包装对象,直接反射 getSettings/addJavascriptInterface 会失败, * 必须先取到原生 WebView。优先用官方 nativeInstanceObject(),失败再退回 obtainWebview()。 */ function getAndroidNativeWebView(webview) { if (!webview) { return null } // 1. 官方 API:WebviewObject.nativeInstanceObject() 返回原生 WebView if (typeof webview.nativeInstanceObject === 'function') { try { const nativeObj = webview.nativeInstanceObject() if (nativeObj) { return nativeObj } } catch (e) { console.warn(LOG_TAG, '[Android] nativeInstanceObject 调用失败:', e && e.message) } } // 2. 兼容:部分运行时通过 obtainWebview 取原生 WebView if (typeof webview.obtainWebview === 'function') { try { const obtained = webview.obtainWebview() if (obtained) { return obtained } } catch (e) {} } return null } /** * 安全调用 plus.android.invoke,失败返回 null。 */ function safeAndroidInvoke(obj, methodName, ...args) { try { return plus.android.invoke(obj, methodName, ...args) } catch (e) { return null } } // ===== iOS 实现:注册 window.webkit.messageHandlers.jumpToMiniProgramPay ===== function registerIOSPayBridge(webviewInstance, onPay) { console.log(LOG_TAG, '[iOS] 开始注册 JSBridge,handler name=', PAY_BRIDGE_NAME) console.log(LOG_TAG, '[iOS] webviewInstance=', webviewInstance ? '[Object]' : 'null') try { // 实现 WKScriptMessageHandler 协议 const MessageHandler = plus.ios.implements('NSObject', { 'userContentController:didReceiveScriptMessage:': function (controller, message) { console.log(LOG_TAG, '===== [iOS] didReceiveScriptMessage 被调用 =====') if (!message) { console.warn(LOG_TAG, '[iOS] message 为空,跳过') return } // message.name === 'jumpToMiniProgramPay',message.body 是 H5 传的参数 let raw = message try { const body = plus.ios.invoke(message, 'body') console.log(LOG_TAG, '[iOS] message.body=', safeStringify(body)) if (body !== undefined && body !== null) { raw = body } } catch (e) {} console.log(LOG_TAG, '[iOS] 原始 raw=', safeStringify(raw)) const params = normalizePayParams(parseJsonSafe(raw)) console.log(LOG_TAG, '[iOS] 解析后 params=', safeStringify(params)) if (typeof onPay === 'function') { try { onPay(params) } catch (e) {} } jumpToMiniProgramPay(params) } }) console.log(LOG_TAG, '[iOS] MessageHandler 已创建:', MessageHandler ? '[Object]' : 'null') // 取底层 WKWebView:plus.webview 实例 → valueForKey('webView') 或直接复用 console.log(LOG_TAG, '[iOS] 尝试 valueForKey:webView') const wkWebView = safeIosInvoke(webviewInstance, 'valueForKey:', 'webView') || webviewInstance console.log(LOG_TAG, '[iOS] wkWebView=', wkWebView ? '[Object]' : 'null') const configuration = safeIosInvoke(wkWebView, 'configuration') console.log(LOG_TAG, '[iOS] configuration=', configuration ? '[Object]' : 'null') if (!configuration) { console.error(LOG_TAG, '[iOS] 未拿到 WKWebView configuration,注册中止') console.error(LOG_TAG, '[iOS] 请确认传入的是 plus.webview.create 创建的子 webview') return } const userContentController = safeIosInvoke(configuration, 'userContentController') console.log(LOG_TAG, '[iOS] userContentController=', userContentController ? '[Object]' : 'null') if (!userContentController) { console.error(LOG_TAG, '[iOS] 未拿到 userContentController,注册中止') return } plus.ios.invoke(userContentController, 'addScriptMessageHandler:name:', MessageHandler, PAY_BRIDGE_NAME) console.log(LOG_TAG, '[iOS] addScriptMessageHandler 完成,window.webkit.messageHandlers.' + PAY_BRIDGE_NAME + ' 已注册') // 验证 H5 端是否能访问到 messageHandlers try { if (typeof webviewInstance.evalJS === 'function') { webviewInstance.evalJS(` try { if (!window.webkit || !window.webkit.messageHandlers) { console.log('[weizanPay][H5] window.webkit.messageHandlers 不存在'); } else { var names = Object.keys(window.webkit.messageHandlers); console.log('[weizanPay][H5] messageHandlers names=' + names.join(',')); var handler = window.webkit.messageHandlers.${PAY_BRIDGE_NAME}; console.log('[weizanPay][H5] ${PAY_BRIDGE_NAME} handler 类型=' + typeof handler); } } catch (e) { console.log('[weizanPay][H5] 检查 messageHandlers 异常: ' + e.message); } `) } } catch (e) { console.warn(LOG_TAG, '[iOS] evalJS 验证失败:', e && e.message) } } catch (e) { console.error(LOG_TAG, '[iOS] 注册 JSBridge 失败:', e) console.error(LOG_TAG, '[iOS] 异常 message=', e && e.message) } } // ===== 辅助函数 ===== /** * 安全序列化任意值,用于日志打印。原生对象、循环引用、undefined 都不会抛错。 */ function safeStringify(value) { try { if (value === undefined) { return 'undefined' } if (value === null) { return 'null' } if (typeof value === 'string') { return value } if (typeof value === 'number' || typeof value === 'boolean') { return String(value) } if (typeof value === 'function') { return '[Function]' } return JSON.stringify(value) } catch (e) { try { return String(value) } catch (e2) { return '[Unserializable]' } } } /** * 把 H5 传过来的 params 归一化为标准结构。 * 同时兼容下划线命名(order_id)与驼峰命名(orderId)。 * * @param {Object|string} input * @returns {{ orderId: string, path: string, username: string, miniprogramType: number }} */ function normalizePayParams(input) { let params = {} if (input && typeof input === 'object') { params = input } else if (typeof input === 'string') { params = parseJsonSafe(input) } return { orderId: String(params.orderId || params.order_id || ''), path: String(params.path || ''), username: String(params.username || params.userName || params.user_name || ''), miniprogramType: Number.isFinite(Number(params.miniprogramType)) ? Number(params.miniprogramType) : 0, } } /** * 安全解析 JSON 字符串,失败返回空对象。 */ function parseJsonSafe(text) { if (typeof text !== 'string') { return text || {} } try { return JSON.parse(text) } catch (e) { return {} } } /** * 安全调用 plus.ios.invoke,失败返回 null。 */ function safeIosInvoke(obj, selector, ...args) { try { return plus.ios.invoke(obj, selector, ...args) } catch (e) { return null } }