hejianhao
2026-08-25 42691111b0cc9723a28a6c806a8c557a84772b67
fix0806: 修改进入直播间逻辑
1个文件已添加
7个文件已修改
610 ■■■■■ 已修改文件
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/service/AppVzanLiveEntryService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/vo/AppVzanLiveEntryVO.java 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/home/service/AppVzanLiveEntryServiceTest.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/App.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/home/home.vue 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/pages/webview/webview.vue 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/utils/visitorDevice.js 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
traditionalApp/utils/weizan-pay-bridge.js 381 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/service/AppVzanLiveEntryService.java
@@ -27,6 +27,7 @@
@Service
public class AppVzanLiveEntryService {
    private static final String VZAN_AUTH_SECRET = "2b4fefe91d8cba50";
    private static final String VZAN_WEB_PAY_UA = "webchatPay/wx074c1f0cf5a0ecf4 InAppPay";
    private static final int USER_STATUS_ENABLED = 1;
    private final LiveRoomSettingService liveRoomSettingService;
@@ -68,7 +69,7 @@
                .encode(StandardCharsets.UTF_8)
                .toUriString();
        log.info("微赞直播生成URL: {}", entryUrl);
        return new AppVzanLiveEntryVO(entryUrl);
        return new AppVzanLiveEntryVO(entryUrl, VZAN_WEB_PAY_UA);
    }
    private String resolveFollowPhone(Long clinicId) {
@@ -126,4 +127,4 @@
        String wechatAvatar = user == null ? null : StrUtil.trim(user.getWechatAvatar());
        return StrUtil.isNotBlank(wechatAvatar) ? wechatAvatar : "";
    }
}
}
tcm-training-admin-backend/src/main/java/com/tcm/training/admin/app/home/vo/AppVzanLiveEntryVO.java
@@ -8,4 +8,6 @@
public class AppVzanLiveEntryVO {
    private String url;
}
    private String ua;
}
tcm-training-admin-backend/src/test/java/com/tcm/training/admin/app/home/service/AppVzanLiveEntryServiceTest.java
New file
@@ -0,0 +1,64 @@
package com.tcm.training.admin.app.home.service;
import com.tcm.training.admin.agent.mapper.ProvincialAgentMapper;
import com.tcm.training.admin.app.home.vo.AppVzanLiveEntryVO;
import com.tcm.training.admin.live.entity.Clinic;
import com.tcm.training.admin.live.mapper.ClinicMapper;
import com.tcm.training.admin.live.service.LiveRoomSettingService;
import com.tcm.training.admin.live.vo.LiveRoomSettingVO;
import com.tcm.training.admin.member.entity.AppUser;
import com.tcm.training.admin.member.mapper.UserMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class AppVzanLiveEntryServiceTest {
    @Mock
    private LiveRoomSettingService liveRoomSettingService;
    @Mock
    private UserMapper userMapper;
    @Mock
    private ClinicMapper clinicMapper;
    @Mock
    private ProvincialAgentMapper provincialAgentMapper;
    private AppVzanLiveEntryService service;
    @BeforeEach
    void setUp() {
        service = new AppVzanLiveEntryService(
                liveRoomSettingService, userMapper, clinicMapper, provincialAgentMapper);
    }
    @Test
    void createEntryReturnsUrlAndUserAgentForNativePlugin() {
        LiveRoomSettingVO setting = new LiveRoomSettingVO();
        setting.setLiveRoomUrl("https://example.com/live?old=1");
        when(liveRoomSettingService.current()).thenReturn(setting);
        AppUser user = new AppUser();
        user.setId(12L);
        user.setClinicId(34L);
        user.setDeleted(0);
        user.setStatus(1);
        user.setAvatar("https://example.com/avatar.png");
        when(userMapper.selectById(12L)).thenReturn(user);
        Clinic clinic = new Clinic();
        clinic.setContactPhone("13800000000");
        when(clinicMapper.selectById(34L)).thenReturn(clinic);
        AppVzanLiveEntryVO result = service.createEntry(12L);
        assertTrue(result.getUrl().startsWith("https://example.com/live?old=1&data="));
        assertEquals("webchatPay/wx074c1f0cf5a0ecf4 InAppPay", result.getUa());
    }
}
traditionalApp/App.vue
@@ -19,8 +19,6 @@
        initOpeninstall,
        registerOpeninstallWakeUp
    } from '@/utils/openinstall.js'
    import { HardwareAPI } from '@/utils/visitorDevice.js'
    let lastHandledInvitePayload = ''
    /**
@@ -28,15 +26,6 @@
     */
    onLaunch(() => {
        // #ifdef APP-PLUS
        HardwareAPI.init().then(() => {
            console.log('[App] 硬件插件全局初始化完成')
            HardwareAPI.startScan((result) => {
                console.log('[App] startScan 回调参数:', result)
                uni.$emit('Hardware-Scan', result)
            })
        }).catch((error) => {
            console.error('[App] 硬件插件全局初始化失败:', error)
        })
        initJPushLifecycle()
        initOpeninstall()
        registerOpeninstallWakeUp(handleOpeninstallPayload)
traditionalApp/pages/home/home.vue
@@ -139,7 +139,7 @@
</template>
<script setup lang="ts">
    import { computed, ref, onMounted, onUnmounted } from "vue";
    import { computed, ref, onMounted } from "vue";
    import { onPullDownRefresh, onShow } from "@dcloudio/uni-app";
    import { createLiveRoomEntry, getBannerList, getHomeFeaturedContent, getHomeLiveCourseList, getLiveRoomSetting } from "./service";
    import { HardwareAPI } from '@/utils/visitorDevice.js';
@@ -224,27 +224,10 @@
        return liveCourseList.value.filter((item) => String(item.courseId) !== String(selectedCourseId));
    });
    const handleHardwareScan = (result : unknown) => {
        console.log('[home] Hardware-Scan 参数:', result);
        let content = '';
        try {
            content = typeof result === 'string' ? result : JSON.stringify(result);
        } catch (error) {
            content = String(result);
        }
        uni.showModal({
            title: 'startScan 回调参数',
            content: content || '空参数',
            showCancel: false
        });
        jumpToMiniProgramPay(result);
    };
    /**
     * 初始化导航栏所需系统信息。
     */
    onMounted(() => {
        uni.$on('Hardware-Scan', handleHardwareScan);
        uni.getSystemInfo({
            success: (res) => {
                statusBarHeight.value = res.statusBarHeight || 0;
@@ -253,10 +236,6 @@
        setTimeout(() => {
            refreshHomeData();
        }, 0);
    });
    onUnmounted(() => {
        uni.$off('Hardware-Scan', handleHardwareScan);
    });
    /**
@@ -368,17 +347,17 @@
        console.log('[goWeiZanLive] 点击微赞直播入口');
        console.log('[goWeiZanLive] weiZanLiveH5Url:', weiZanLiveH5Url.value);
        // 未登录直接跳转登录页,不弹「请先登录」提示。
        // 直播间地址未配置时静默处理,不弹「直播间地址暂未配置」提示。
        if (!weiZanLiveH5Url.value.trim()) {
            console.log('[goWeiZanLive] 直播间地址未配置,忽略点击');
            return;
        }
        // 仅配置直播间后才检查登录态,确保未配置时点击没有任何副作用。
        const userId = resolveCurrentAppUserId();
        if (!userId) {
            console.log('[goWeiZanLive] 用户未登录,跳转登录页');
            goLoginPage();
            return;
        }
        // 直播间地址未配置时静默处理,不弹「直播间地址暂未配置」提示。
        if (!weiZanLiveH5Url.value.trim()) {
            console.log('[goWeiZanLive] 直播间地址未配置,忽略点击');
            return;
        }
        if (weiZanLiveOpening.value) {
@@ -394,22 +373,25 @@
            console.log('[goWeiZanLive] createLiveRoomEntry 返回:', JSON.stringify(res));
            const targetUrl = res?.data?.url?.trim();
            const userAgent = res?.data?.ua?.trim();
            console.log('[goWeiZanLive] 解析目标URL:', targetUrl);
            if (res?.code !== 0 || !targetUrl) {
            if (res?.code !== 0 || !targetUrl || !userAgent) {
                console.log('[goWeiZanLive] 获取跳转地址失败,code:', res?.code, 'msg:', res?.msg);
                uni.showToast({ title: res?.msg || '获取直播间地址失败', icon: 'none' });
                return;
            }
            const webPayOptions = {
                ua: 'webchatPay/wx074c1f0cf5a0ecf4 InAppPay',
                url: targetUrl,
                title: '微赞直播'
                ua: userAgent,
                url: targetUrl
            };
            console.log('[goWeiZanLive] 准备调用 openWebPay, options:', JSON.stringify(webPayOptions));
            // #ifdef APP-PLUS
            HardwareAPI.openWebPay(webPayOptions).catch((error) => {
            HardwareAPI.openWebPay(webPayOptions, (result) => {
                console.log('[goWeiZanLive] openWebPay 回调参数:', result);
                jumpToMiniProgramPay(result);
            }).catch((error) => {
                console.error('[goWeiZanLive] openWebPay 调用失败:', error);
            });
            return;
traditionalApp/pages/webview/webview.vue
@@ -11,13 +11,11 @@
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'
// 微信开放平台 appId(见 manifest.json app-plus.distribute.sdkConfigs.share.weixin)
const WEIXIN_APP_ID = 'wx074c1f0cf5a0ecf4'
const webViewUrl = ref('')
let currentWebviewInstance: any = null
// 进入微赞直播页前记录原始 UA,退出时还原,避免全局 UA 常驻影响其它 webview
let originalUserAgent = ''
@@ -49,28 +47,14 @@
        })
    }
    // #ifdef APP-PLUS
    // 获取当前 WebView 实例
    setTimeout(() => {
        currentWebviewInstance = plus.webview.currentWebview()
        console.log('[WebView] 页面加载完成,URL:', resolvedUrl)
        // 仅微赞直播间入口(clearCache=1)注册支付 JSBridge;其他外链行为不变
        if (shouldClearCache && currentWebviewInstance) {
            registerWeizanPayBridge(currentWebviewInstance)
        }
    }, 100)
    // #endif
    console.log('[WebView] 页面加载完成,URL:', resolvedUrl)
})
/**
 * 页面卸载时不再清理缓存:清理集中在进入直播间前与退出登录时执行。
 * 反注册微赞支付 JSBridge,避免 webview 复用导致重复回调。
 */
onUnload(() => {
    console.log('[WebView] 页面卸载')
    if (currentWebviewInstance) {
        unregisterWeizanPayBridge(currentWebviewInstance)
    }
    // #ifdef APP-PLUS
    // 还原全局 UA,避免影响之后创建的其它 webview
    restoreUserAgent()
traditionalApp/utils/visitorDevice.js
@@ -3,9 +3,6 @@
if (typeof uni !== 'undefined' && typeof uni.requireNativePlugin === 'function') {
  HardwarePlugin = uni.requireNativePlugin('business-module');
}
let initPromise = null;
let initResult = null;
const getPluginMethod = (methodNames = []) => {
  if (!HardwarePlugin) {
    return null;
@@ -15,76 +12,7 @@
};
export const HardwareAPI = {
  init(options = {}) {
    if (initResult) {
      return Promise.resolve(initResult);
    }
    if (initPromise) {
      return initPromise;
    }
    initPromise = new Promise((resolve, reject) => {
      // #ifdef APP-PLUS
      const methodName = getPluginMethod(['init']);
      if (!methodName) {
        initPromise = null;
        reject(new Error('当前插件未提供初始化能力'));
        return;
      }
      let isSettled = false;
      let fallbackTimer = null;
      const finishInit = (result) => {
        if (isSettled) {
          return;
        }
        isSettled = true;
        if (fallbackTimer) {
          clearTimeout(fallbackTimer);
        }
        initResult = result;
        resolve(result);
      };
      try {
        HardwarePlugin[methodName](options, (result) => {
          if (result) {
            finishInit(result);
          }
        });
        fallbackTimer = setTimeout(() => {
          finishInit({ code: 200, msg: 'init_called_without_callback' });
        }, 300);
      } catch (error) {
        initPromise = null;
        reject(error);
      }
      // #endif
      // #ifndef APP-PLUS
      initResult = { code: 200, msg: 'success' };
      resolve(initResult);
      // #endif
    });
    return initPromise;
  },
  startScan(callback) {
    // #ifdef APP-PLUS
    const methodName = getPluginMethod(['startScan']);
    if (!methodName) {
      throw new Error('当前插件未提供 startScan 能力');
    }
    HardwarePlugin[methodName]({ duration: 2000 }, (result) => callback?.(result));
    return;
    // #endif
    // #ifndef APP-PLUS
    throw new Error('当前平台不支持 startScan');
    // #endif
  },
  openWebPay(options = {}) {
  openWebPay(options = {}, callback) {
    return new Promise((resolve, reject) => {
      // #ifdef APP-PLUS
      console.log('[HardwareAPI] openWebPay 准备调用, options:', JSON.stringify(options));
@@ -99,6 +27,9 @@
      try {
        HardwarePlugin[methodName](options, (result) => {
          console.log('[HardwareAPI] openWebPay 原生回调:', result);
          if (typeof callback === 'function') {
            callback(result);
          }
          resolve(result);
        });
      } catch (error) {
traditionalApp/utils/weizan-pay-bridge.js
@@ -1,93 +1,20 @@
/**
 * 微赞 H5 唤起小程序支付桥接。
 * 处理 openWebPay 原生回调并拉起微信小程序支付页。
 *
 * 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)。
 * @param {Object|string} result openWebPay 回调参数
 */
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) {
export function jumpToMiniProgramPay(result) {
    // #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))
    console.log('[weizanPay] openWebPay 原生回调:', result)
    const params = normalizePayParams(result)
    if (!params.username || !params.path) {
        console.error('[weizanPay] 小程序支付参数缺失:', params)
        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')
        const wx = (services || []).find((service) => service && service.id === 'weixin')
        if (!wx) {
            uni.showToast({ title: '当前环境不支持微信', icon: 'none' })
            return
@@ -96,288 +23,50 @@
            uni.showToast({ title: '请先安装微信客户端', icon: 'none' })
            return
        }
        wx.launchMiniProgram({
            id: safeParams.username,
            path: safeParams.path || '',
            type: Number(safeParams.miniprogramType) || 0,
            id: params.username,
            path: params.path,
            type: params.miniprogramType,
        }, () => {
            console.log('[weizanPay] 唤起小程序成功, orderId=', safeParams.orderId)
        }, (err) => {
            console.error('[weizanPay] 唤起失败', err)
            const errMsg = String(err && (err.message || err.errMsg) || '')
            console.log('[weizanPay] 小程序支付页拉起成功, orderId:', params.orderId)
        }, (error) => {
            console.error('[weizanPay] 小程序支付页拉起失败:', error)
            const message = String(error && (error.message || error.errMsg) || '')
            uni.showToast({
                title: errMsg.indexOf('badprogram') >= 0 ? '请检查小程序原始ID及关联配置' : '唤起支付失败',
                title: message.includes('badprogram') ? '请检查小程序原始ID及关联配置' : '唤起支付失败',
                icon: 'none',
            })
        })
    }, (err) => {
        console.error('[weizanPay] 微信服务不可用', err)
    }, (error) => {
        console.error('[weizanPay] 获取微信服务失败:', error)
        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()。
 * 兼容原生插件返回对象或 JSON 字符串。
 */
function getAndroidNativeWebView(webview) {
    if (!webview) {
        return null
    }
    // 1. 官方 API:WebviewObject.nativeInstanceObject() 返回原生 WebView
    if (typeof webview.nativeInstanceObject === 'function') {
function normalizePayParams(result) {
    let source = result
    if (typeof result === 'string') {
        try {
            const nativeObj = webview.nativeInstanceObject()
            if (nativeObj) {
                return nativeObj
            }
        } catch (e) {
            console.warn(LOG_TAG, '[Android] nativeInstanceObject 调用失败:', e && e.message)
            source = JSON.parse(result)
        } catch (error) {
            console.error('[weizanPay] 回调参数不是有效 JSON:', error)
            source = {}
        }
    }
    // 2. 兼容:部分运行时通过 obtainWebview 取原生 WebView
    if (typeof webview.obtainWebview === 'function') {
        try {
            const obtained = webview.obtainWebview()
            if (obtained) {
                return obtained
            }
        } catch (e) {}
    if (!source || typeof source !== 'object') {
        source = {}
    }
    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<WKScriptMessageHandler>', {
            '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)
    }
    const miniProgramType = Number(source.miniprogramType)
    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
        username: String(source.username || ''),
        path: String(source.path || ''),
        miniprogramType: Number.isFinite(miniProgramType) ? miniProgramType : 0,
        orderId: String(source.orderId || ''),
    }
}