quling
2026-08-21 2524f08f0c53930a92e4c3e04eb58a595c6ab327
fix0806: 发起支付
1个文件已添加
1个文件已修改
233 ■■■■■ 已修改文件
traditionalApp/pages/webview/webview.vue 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/utils/weizan-pay-bridge.js 224 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/webview/webview.vue
@@ -11,6 +11,7 @@
import { ref } from 'vue'
import { onLoad, onUnload } from '@dcloudio/uni-app'
import { clearWebViewData } from '@/utils/webview-cache.js'
import { registerWeizanPayBridge, unregisterWeizanPayBridge } from '@/utils/weizan-pay-bridge.js'
const webViewUrl = ref('')
let currentWebviewInstance: any = null
@@ -44,15 +45,23 @@
    setTimeout(() => {
        currentWebviewInstance = plus.webview.currentWebview()
        console.log('[WebView] 页面加载完成,URL:', resolvedUrl)
        // 仅微赞直播间入口(clearCache=1)注册支付 JSBridge;其他外链行为不变
        if (shouldClearCache && currentWebviewInstance) {
            registerWeizanPayBridge(currentWebviewInstance)
        }
    }, 100)
    // #endif
})
/**
 * 页面卸载时不再清理缓存:清理集中在进入直播间前与退出登录时执行。
 * 反注册微赞支付 JSBridge,避免 webview 复用导致重复回调。
 */
onUnload(() => {
    console.log('[WebView] 页面卸载')
    if (currentWebviewInstance) {
        unregisterWeizanPayBridge(currentWebviewInstance)
    }
})
/**
traditionalApp/utils/weizan-pay-bridge.js
New file
@@ -0,0 +1,224 @@
/**
 * 微赞 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
/**
 * 给指定 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 接口
            try {
                plus.android.invoke(webviewInstance, '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
    const safeParams = normalizePayParams(params)
    if (!safeParams.username) {
        uni.showToast({ title: '支付参数缺失', icon: 'none' })
        return
    }
    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) {
    try {
        const JsInterface = plus.android.implements('io.dcloud.js.WeizanPayJsInterface', {
            [PAY_BRIDGE_NAME]: function (paramsJson) {
                // JS 接口回调在原生子线程,必须切回 UI 线程才能拉起微信 SDK
                plus.android.runOnUiThread(() => {
                    const params = normalizePayParams(parseJsonSafe(paramsJson))
                    if (typeof onPay === 'function') {
                        try { onPay(params) } catch (e) {}
                    }
                    jumpToMiniProgramPay(params)
                })
            }
        })
        plus.android.invoke(webviewInstance, 'addJavascriptInterface', JsInterface, PAY_BRIDGE_ANDROID_KEY)
        console.log('[weizanPay][Android] JSBridge 已注册')
    } catch (e) {
        console.error('[weizanPay][Android] 注册失败:', e)
    }
}
// ===== iOS 实现:注册 window.webkit.messageHandlers.jumpToMiniProgramPay =====
function registerIOSPayBridge(webviewInstance, onPay) {
    try {
        // 实现 WKScriptMessageHandler 协议
        const MessageHandler = plus.ios.implements('NSObject<WKScriptMessageHandler>', {
            'userContentController:didReceiveScriptMessage:': function (controller, message) {
                if (!message) {
                    return
                }
                // message.name === 'jumpToMiniProgramPay',message.body 是 H5 传的参数
                let raw = message
                try {
                    const body = plus.ios.invoke(message, 'body')
                    if (body !== undefined && body !== null) {
                        raw = body
                    }
                } catch (e) {}
                const params = normalizePayParams(parseJsonSafe(raw))
                if (typeof onPay === 'function') {
                    try { onPay(params) } catch (e) {}
                }
                jumpToMiniProgramPay(params)
            }
        })
        // 取底层 WKWebView:plus.webview 实例 → valueForKey('webView') 或直接复用
        const wkWebView = safeIosInvoke(webviewInstance, 'valueForKey:', 'webView') || webviewInstance
        const configuration = safeIosInvoke(wkWebView, 'configuration')
        if (!configuration) {
            console.error('[weizanPay][iOS] 未拿到 WKWebView configuration')
            return
        }
        const userContentController = safeIosInvoke(configuration, 'userContentController')
        if (!userContentController) {
            console.error('[weizanPay][iOS] 未拿到 userContentController')
            return
        }
        plus.ios.invoke(userContentController, 'addScriptMessageHandler:name:', MessageHandler, PAY_BRIDGE_NAME)
        console.log('[weizanPay][iOS] JSBridge 已注册')
    } catch (e) {
        console.error('[weizanPay][iOS] 注册失败:', e)
    }
}
// ===== 辅助函数 =====
/**
 * 把 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
    }
}