Browse Source

feat: 微信 OAuth 回调流程优化

- App.vue 全局处理微信 OAuth 回调
- payment-confirm 页面优化 openid 获取逻辑
- 保存/恢复页面上下文避免 OAuth 后丢失路由状态
- redirect_uri 使用不含 hash 的基础 URL

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
MyFramework User 3 months ago
parent
commit
320968e872
3 changed files with 120 additions and 29 deletions
  1. 12 0
      my-uniapp-vue3/run.sh
  2. 59 0
      my-uniapp-vue3/src/App.vue
  3. 49 29
      my-uniapp-vue3/src/pages/payment-confirm/index.vue

+ 12 - 0
my-uniapp-vue3/run.sh

@@ -0,0 +1,12 @@
+#!/bin/bash
+# uni-app 项目专用:使用 HBuilderX 内置 Node v18
+# 不影响全局 Node.js v22,仅对当前项目生效
+
+HBX_NODE="/c/soft/HBuilderX/plugins/node"
+if [ ! -f "$HBX_NODE/node.exe" ]; then
+  echo "❌ HBuilderX Node 未找到: $HBX_NODE/node.exe" >&2
+  exit 1
+fi
+
+export PATH="$HBX_NODE:$PATH"
+"$@"

+ 59 - 0
my-uniapp-vue3/src/App.vue

@@ -9,6 +9,62 @@ import MiniPlayer from './components/MiniPlayer.vue';
 // 初始化全局调试工具
 initDebug();
 
+// ==================== 微信 OAuth 回调处理 ====================
+async function handleWechatOAuthCallback() {
+  // 只在 H5 环境处理
+  // #ifdef H5
+  const searchParams = new URLSearchParams(window.location.search);
+  const code = searchParams.get('code');
+  const state = searchParams.get('state');
+
+  if (!code) return; // 不是 OAuth 回调,跳过
+
+  console.log('[App] 检测到微信 OAuth 回调,处理 code...');
+  uni.showLoading({ title: '授权中...' });
+
+  try {
+    const { post } = await import('./utils/request');
+    const result = await post<{ openid: string }>('/payment/wechat/openid', { code });
+
+    if (result.openid) {
+      localStorage.setItem('wx_openid', result.openid);
+      console.log('[App] OpenID 获取成功');
+
+      // 恢复之前保存的页面上下文
+      let returnUrl: string | null = null;
+      try {
+        returnUrl = sessionStorage.getItem('wx_oauth_return');
+        sessionStorage.removeItem('wx_oauth_return');
+      } catch (_) {}
+
+      if (returnUrl) {
+        // 跳回原始页面
+        window.location.href = returnUrl;
+      } else {
+        // 没有上下文,清除 URL 参数后停留在当前页
+        searchParams.delete('code');
+        searchParams.delete('state');
+        const newSearch = searchParams.toString();
+        const newUrl = window.location.origin + window.location.pathname
+          + (newSearch ? '?' + newSearch : '');
+        window.history.replaceState({}, '', newUrl);
+      }
+    }
+  } catch (e) {
+    console.error('[App] OAuth 回调处理失败:', e);
+    // 清除 URL 参数
+    searchParams.delete('code');
+    searchParams.delete('state');
+    const newSearch = searchParams.toString();
+    const newUrl = window.location.origin + window.location.pathname
+      + (newSearch ? '?' + newSearch : '');
+    window.history.replaceState({}, '', newUrl);
+  } finally {
+    uni.hideLoading();
+  }
+  // #endif
+}
+
 onLaunch(() => {
   console.log('App Launch');
   const userStore = useUserStore();
@@ -19,6 +75,9 @@ onLaunch(() => {
 
   // 初始化统计
   initAnalytics();
+
+  // 处理微信 OAuth 回调(必须在 initUser 之后)
+  handleWechatOAuthCallback();
 });
 
 onShow(() => {

+ 49 - 29
my-uniapp-vue3/src/pages/payment-confirm/index.vue

@@ -96,6 +96,28 @@ function isInWechat(): boolean {
   // #endif
 }
 
+// 获取不含 hash 的基础URL(用于OAuth redirect_uri)
+function getBaseUrl(): string {
+  // #ifdef H5
+  return window.location.origin + window.location.pathname;
+  // #endif
+  return '';
+}
+
+// 保存当前页面上下文到 sessionStorage(OAuth 来回会丢失 hash 路由状态)
+function savePageContext() {
+  try {
+    sessionStorage.setItem('wx_oauth_return', window.location.href);
+  } catch (_) {}
+}
+
+// 恢复页面上下文
+function restorePageContext(): string | null {
+  try {
+    return sessionStorage.getItem('wx_oauth_return');
+  } catch (_) { return null; }
+}
+
 const orderInfo = ref<{
   orderNo: string;
   amount: number;
@@ -173,43 +195,37 @@ async function initWxJsSdk(): Promise<boolean> {
 // ==================== 微信 OAuth 处理 ====================
 async function handleWechatOAuth() {
   // #ifdef H5
-  // 检测 URL 中是否有 OAuth 回调的 code 参数
-  const urlParams = new URLSearchParams(window.location.search);
-  const code = urlParams.get('code');
+  // OAuth 回调已在 App.vue 全局处理,这里只需读取缓存的 openid
+  const cachedOpenid = localStorage.getItem('wx_openid');
+  if (cachedOpenid) {
+    openid.value = cachedOpenid;
+    console.log('[WeChat Pay] 使用缓存的 openid');
+    return;
+  }
 
+  // 双重保险:检查 URL search 中的 code(如果 App.vue 未拦截到)
+  const searchParams = new URLSearchParams(window.location.search);
+  const code = searchParams.get('code');
   if (code) {
-    // OAuth 回调:用 code 换取 openid
     uni.showLoading({ title: '授权中...' });
     try {
       const result = await post<{ openid: string }>('/payment/wechat/openid', { code });
       openid.value = result.openid;
-      // 保存 openid 到 localStorage(下次不用重新授权)
       localStorage.setItem('wx_openid', result.openid);
       console.log('[WeChat Pay] openid 获取成功');
 
-      // 清除 URL 中的 code 参数,避免重复处理
-      urlParams.delete('code');
-      urlParams.delete('state');
-      const newSearch = urlParams.toString();
+      // 清除 URL 中的 code/state
+      searchParams.delete('code');
+      searchParams.delete('state');
+      const newSearch = searchParams.toString();
       const newUrl = window.location.origin + window.location.pathname
-        + (newSearch ? '?' + newSearch : '')
-        + window.location.hash;
+        + (newSearch ? '?' + newSearch : '');
       window.history.replaceState({}, '', newUrl);
     } catch (e) {
       console.error('[WeChat Pay] 获取 openid 失败:', e);
-      uni.showToast({ title: '微信授权失败,请使用扫码支付', icon: 'none' });
-      inWechat.value = false;
     } finally {
       uni.hideLoading();
     }
-    return;
-  }
-
-  // 没有 code 参数,检查是否有缓存的 openid
-  const cachedOpenid = localStorage.getItem('wx_openid');
-  if (cachedOpenid) {
-    openid.value = cachedOpenid;
-    console.log('[WeChat Pay] 使用缓存的 openid');
   }
   // #endif
 }
@@ -218,9 +234,12 @@ async function handleWechatOAuth() {
 async function redirectToWechatOAuth() {
   // #ifdef H5
   try {
-    const currentUrl = window.location.href;
+    // 保存当前完整 URL(含 hash 路由参数),OAuth 回来时恢复
+    savePageContext();
+    // 用不含 # 的 baseUrl 作为 redirect_uri,避免微信报"非法字符"
+    const baseUrl = getBaseUrl();
     const result = await get<{ oauthUrl: string; state: string }>('/payment/wechat/oauth-url', {
-      redirect: currentUrl
+      redirect: baseUrl
     });
     window.location.href = result.oauthUrl;
   } catch (e) {
@@ -260,15 +279,16 @@ onMounted(async () => {
     paymentMethod.value = 'wechat';
     // 处理 OAuth 回调
     await handleWechatOAuth();
-    // 初始化 JS-SDK(如果有 openid)
+    // 如果有 openid,提前初始化 JS-SDK
     if (openid.value) {
       initWxJsSdk(); // 异步初始化,不阻塞
     }
-  }
-
-  // 进入页面时直接创建订单
-  if (productType.value === 'token-pack' || planId.value) {
-    await createOrder();
+    // 微信环境不预创建订单(JSAPI 支付时另外创建)
+  } else {
+    // 非微信环境:进入页面时直接创建订单
+    if (productType.value === 'token-pack' || planId.value) {
+      await createOrder();
+    }
   }
 });