Bläddra i källkod

fix: 去掉中间授权按钮,扫码后直接拉起微信支付分官方确认页

改用 uni.request 回调方式替代 async/await,使 openBusinessView
保持在用户手势调用链中。流程简化为:扫码 → API → 官方确认页 → 开门。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 2 veckor sedan
förälder
incheckning
ee94750ad7
2 ändrade filer med 166 tillägg och 287 borttagningar
  1. 92 198
      haha-mp/src/pages/index/index.vue
  2. 74 89
      haha-mp/src/pages/products/products.vue

+ 92 - 198
haha-mp/src/pages/index/index.vue

@@ -8,23 +8,13 @@
 
     <!-- 核心操作区 -->
     <view class="action-section">
-      <!-- 状态1:扫码开门 -->
-      <button v-if="!authReady" class="scan-button" @click="scanCode">
+      <button class="scan-button" @click="scanCode" :disabled="busy">
         <view class="scan-button-inner">
           <image class="scan-icon" src="/static/icons/scan.svg" mode="aspectFill"></image>
-          <text class="scan-text">扫码开门</text>
+          <text class="scan-text">{{ busy ? '处理中...' : '扫码开门' }}</text>
         </view>
         <view class="scan-button-ripple"></view>
       </button>
-
-      <!-- 状态2:API 返回后,展示授权按钮 -->
-      <button v-else class="auth-button" @click="invokePayScore" :disabled="authBusy">
-        <view class="auth-button-inner">
-          <image class="auth-btn-icon" src="/static/icons/pay-socre-logo.png" mode="aspectFit"></image>
-          <text class="auth-btn-text">{{ authBusy ? '跳转中...' : '确认支付分授权' }}</text>
-          <text class="auth-btn-sub">点击后调起微信支付分</text>
-        </view>
-      </button>
     </view>
 
     <!-- 快捷功能区 -->
@@ -49,11 +39,6 @@
       </view>
     </view>
 
-    <!-- 授权就绪时,取消按钮 -->
-    <view v-if="authReady" class="cancel-auth-wrap">
-      <text class="cancel-auth-link" @click="cancelAuth">取消,返回扫码</text>
-    </view>
-
     <!-- 底部信息卡片 -->
     <view class="info-card">
       <view class="info-item">
@@ -78,16 +63,15 @@
 <script setup lang="ts">
 import { ref } from 'vue'
 import { onShow } from '@dcloudio/uni-app'
+import { API_CONFIG } from '../../utils/config'
 import { scanDoor } from '../../api/device'
-import { preCreatePayscoreOrder, queryPayscoreByOutNo } from '../../api/payscore'
+import { queryPayscoreByOutNo } from '../../api/payscore'
 import { isLoggedIn, getToken } from '../../utils/auth'
 import { logger } from '../../utils/logger'
 
-const authReady = ref(false)
-const authBusy = ref(false)
-const authDeviceId = ref('')
-const authPackage = ref('')
-const authOutOrderNo = ref('')
+const busy = ref(false)
+let pollingDeviceId = ''
+let pollingOutOrderNo = ''
 let pollTimer: ReturnType<typeof setInterval> | null = null
 
 onShow(() => {
@@ -95,7 +79,9 @@ onShow(() => {
   logger.log('[首页 onShow] token:', token ? '存在' : '不存在')
 })
 
-const scanCode = async () => {
+const scanCode = () => {
+  if (busy.value) return
+
   if (!isLoggedIn()) {
     uni.showModal({
       title: '提示',
@@ -109,7 +95,7 @@ const scanCode = async () => {
   }
 
   uni.scanCode({
-    success: async function (res) {
+    success: function (res) {
       logger.log('扫码结果:', res.result);
 
       let deviceId = '';
@@ -126,7 +112,6 @@ const scanCode = async () => {
             deviceId = res.result;
           }
         }
-
         if (!deviceId) throw new Error('无法解析设备ID');
         logger.log('提取到设备ID:', deviceId);
       } catch (error) {
@@ -135,7 +120,9 @@ const scanCode = async () => {
         return;
       }
 
-      await prepareAuth(deviceId);
+      // 关键:在 scanCode 回调中直接调用 API + openBusinessView
+      // 全程不用 async/await,保持在用户手势链中
+      preCreateAndOpenBusinessView(deviceId);
     },
     fail: function (err) {
       logger.log('扫码取消:', err);
@@ -143,137 +130,122 @@ const scanCode = async () => {
   });
 };
 
-const prepareAuth = async (deviceId: string) => {
-  uni.showLoading({ title: '准备中...', mask: true });
-
-  try {
-    const result = await preCreatePayscoreOrder({ deviceId });
-    uni.hideLoading();
-
-    if (!result.success || !result.package) {
-      uni.showToast({
-        title: result.errorMsg || '创建支付分订单失败',
-        icon: 'none',
-      });
-      return;
-    }
-
-    logger.log('[支付分] 预创建成功, package 已就绪');
-
-    authDeviceId.value = deviceId;
-    authPackage.value = result.package;
-    authOutOrderNo.value = result.outOrderNo;
-    authReady.value = true;
-  } catch (error: any) {
-    uni.hideLoading();
-    logger.error('[支付分] 预创建失败', error);
-    uni.showToast({
-      title: error?.message || '创建支付分订单失败',
-      icon: 'none',
-    });
-  }
-};
-
 /**
- * 用户点击按钮 → 直接触发 wx.openBusinessView
- * 此函数由 button @click 同步调用,满足微信的用户手势要求
+ * 预创建支付分订单 → 直接拉起官方确认页
+ * 使用 uni.request 回调方式,避免 async/await 打破手势链
  */
-const invokePayScore = () => {
-  if (authBusy.value) return;
-  authBusy.value = true;
-
-  const pkg = authPackage.value;
-
-  logger.log('[支付分] invokePayScore, package 长度:', pkg?.length);
-
-  if (!pkg) {
-    authBusy.value = false;
-    uni.showModal({
-      title: '参数异常',
-      content: 'package 为空,请重新扫码',
-      showCancel: false,
-    });
-    return;
-  }
+const preCreateAndOpenBusinessView = (deviceId: string) => {
+  busy.value = true;
+  uni.showLoading({ title: '准备中...', mask: true });
 
-  wx.openBusinessView({
-    businessType: 'wxpayScoreUse',
-    extraData: {
-      package: pkg,
-    },
-    success: () => {
-      // 用户从支付分页面返回,但不确定是确认还是取消
-      // 轮询后端查支付分订单状态来判断
-      logger.log('[支付分] 从支付分页面返回,开始轮询确认状态');
-      pollPayScoreStatus();
+  uni.request({
+    url: API_CONFIG.baseUrl + '/payscore/pre-create',
+    method: 'POST',
+    header: {
+      'Content-Type': 'application/json',
+      'accessToken': getToken()
     },
-    fail: (err: any) => {
-      authBusy.value = false;
-      const errMsg = err?.errMsg || JSON.stringify(err);
-      logger.error('[支付分] openBusinessView fail:', errMsg);
+    data: { deviceId },
+    success: (apiRes: any) => {
+      uni.hideLoading();
+
+      const body = apiRes.data;
+      if (body.code !== 200 && body.code !== 0) {
+        busy.value = false;
+        uni.showToast({ title: body.message || '创建失败', icon: 'none' });
+        return;
+      }
 
-      if (errMsg.includes('cancel')) {
+      const data = body.data;
+      if (!data || !data.package) {
+        busy.value = false;
+        uni.showToast({ title: data?.errorMsg || '创建支付分订单失败', icon: 'none' });
         return;
       }
 
-      uni.showModal({
-        title: '调起支付分失败',
-        content: errMsg,
-        showCancel: false,
+      logger.log('[支付分] 预创建成功, 直接拉起官方授权页');
+
+      // 记录信息供轮询使用
+      pollingDeviceId = deviceId;
+      pollingOutOrderNo = data.outOrderNo;
+
+      // 在 uni.request 的 success 回调中调用 openBusinessView
+      // 这里是原生回调,仍处于用户手势上下文中
+      wx.openBusinessView({
+        businessType: 'wxpayScoreUse',
+        extraData: {
+          package: data.package,
+        },
+        success: () => {
+          logger.log('[支付分] 从授权页返回,开始轮询确认状态');
+          startPolling();
+        },
+        fail: (err: any) => {
+          busy.value = false;
+          const errMsg = err?.errMsg || JSON.stringify(err);
+          logger.error('[支付分] openBusinessView fail:', errMsg);
+          if (!errMsg.includes('cancel')) {
+            uni.showModal({
+              title: '调起支付分失败',
+              content: errMsg,
+              showCancel: false,
+            });
+          }
+        },
       });
     },
+    fail: (err: any) => {
+      uni.hideLoading();
+      busy.value = false;
+      logger.error('[支付分] API 请求失败', err);
+      uni.showToast({ title: '网络异常,请重试', icon: 'none' });
+    }
   });
 };
 
 /**
- * 轮询支付分订单状态,判断用户确认还是取消
- * DOING = 用户已确认,CREATED = 用户取消/未操作
+ * 轮询后端确认用户是否在授权页点了"确认"
  */
-const pollPayScoreStatus = () => {
+const startPolling = () => {
   uni.showLoading({ title: '确认结果...', mask: true });
 
-  const outOrderNo = authOutOrderNo.value;
   let attempts = 0;
   const maxAttempts = 10;
-  const interval = 1000; // 1s
 
   if (pollTimer) clearInterval(pollTimer);
 
   pollTimer = setInterval(async () => {
     attempts++;
     try {
-      const res = await queryPayscoreByOutNo(outOrderNo);
+      const res = await queryPayscoreByOutNo(pollingOutOrderNo);
       const state = res?.state;
 
       logger.log(`[支付分] 轮询第 ${attempts} 次, state:`, state);
 
       if (state === 'DOING') {
-        // 用户已确认
         clearPollTimer();
         uni.hideLoading();
-        authReady.value = false;
-        authBusy.value = false;
-        logger.log('[支付分] 用户已确认,开始开门');
-        doOpenDoor(authDeviceId.value);
+        busy.value = false;
+        logger.log('[支付分] 用户已确认,开门');
+        doOpenDoor(pollingDeviceId);
         return;
       }
 
       if (attempts >= maxAttempts) {
-        // 超时未确认,视为取消
         clearPollTimer();
         uni.hideLoading();
-        authBusy.value = false;
-        logger.log('[支付分] 轮询超时,用户未确认');
+        busy.value = false;
+        logger.log('[支付分] 轮询超时,用户取消或未确认');
       }
     } catch (e) {
       logger.error('[支付分] 轮询异常', e);
       if (attempts >= maxAttempts) {
         clearPollTimer();
         uni.hideLoading();
-        authBusy.value = false;
+        busy.value = false;
       }
     }
-  }, interval);
+  }, 1000);
 };
 
 const clearPollTimer = () => {
@@ -283,18 +255,10 @@ const clearPollTimer = () => {
   }
 };
 
-const cancelAuth = () => {
-  clearPollTimer();
-  authReady.value = false;
-  authBusy.value = false;
-  logger.log('[支付分] 用户取消,返回扫码');
-};
-
-const doOpenDoor = async (deviceId: string) => {
+const doOpenDoor = (deviceId: string) => {
   uni.showLoading({ title: '正在开门...', mask: true });
 
-  try {
-    const response = await scanDoor(deviceId);
+  scanDoor(deviceId).then(response => {
     uni.hideLoading();
     uni.showToast({ title: '开门成功', icon: 'success' });
 
@@ -306,10 +270,10 @@ const doOpenDoor = async (deviceId: string) => {
     setTimeout(() => {
       uni.navigateTo({ url: '/pages/shopping/shopping' });
     }, 1000);
-  } catch (error: any) {
+  }).catch((error: any) => {
     uni.hideLoading();
     logger.error('开门失败:', error);
-  }
+  });
 };
 
 const goToMy = () => uni.navigateTo({ url: '/pages/my/my' })
@@ -358,7 +322,6 @@ const onContactSuccess = () => logger.log('[首页] 客服会话已打开')
   padding: $spacing-xl 0;
 }
 
-/* 扫码按钮 */
 .scan-button {
   position: relative;
   width: 400rpx;
@@ -375,10 +338,13 @@ const onContactSuccess = () => logger.log('[首页] 客服会话已打开')
   overflow: hidden;
   box-sizing: border-box;
   flex-shrink: 0;
-  line-height: 1;
 
   &::after { border: none; }
 
+  &[disabled] {
+    opacity: 0.9;
+  }
+
   &-inner {
     width: 100%;
     height: 100%;
@@ -427,78 +393,6 @@ const onContactSuccess = () => logger.log('[首页] 客服会话已打开')
   letter-spacing: 4rpx;
 }
 
-/* 授权按钮 */
-.auth-button {
-  width: 400rpx;
-  height: 400rpx;
-  min-width: 400rpx;
-  max-width: 400rpx;
-  min-height: 400rpx;
-  max-height: 400rpx;
-  border-radius: 50%;
-  background: transparent;
-  border: none;
-  padding: 0;
-  margin: 0;
-  overflow: hidden;
-  box-sizing: border-box;
-  flex-shrink: 0;
-  line-height: 1;
-
-  &::after { border: none; }
-
-  &-inner {
-    width: 100%;
-    height: 100%;
-    border-radius: 50%;
-    background: linear-gradient(135deg, #07C160 0%, #06AD56 100%);
-    box-shadow: 0 8rpx 32rpx rgba(7, 193, 96, 0.35);
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    justify-content: center;
-    transition: all 0.3s ease;
-
-    &:active {
-      transform: scale(0.95);
-    }
-  }
-
-  &[disabled] .auth-button-inner {
-    opacity: 0.7;
-  }
-}
-
-.auth-btn-icon {
-  width: 100rpx;
-  height: 100rpx;
-  margin-bottom: $spacing-sm;
-}
-
-.auth-btn-text {
-  font-size: 36rpx;
-  font-weight: 600;
-  color: #fff;
-}
-
-.auth-btn-sub {
-  font-size: 22rpx;
-  color: rgba(255, 255, 255, 0.8);
-  margin-top: 8rpx;
-}
-
-/* 取消授权 */
-.cancel-auth-wrap {
-  text-align: center;
-  margin-top: -$spacing-lg;
-}
-
-.cancel-auth-link {
-  font-size: 26rpx;
-  color: $color-text-secondary;
-  padding: $spacing-sm $spacing-lg;
-}
-
 /* ========== 快捷功能区 ========== */
 .quick-actions {
   display: flex;

+ 74 - 89
haha-mp/src/pages/products/products.vue

@@ -106,11 +106,8 @@
       </view>
       <view class="bottom-actions">
         <view class="btn-back" @click="goBack">返回</view>
-        <!-- 未授权时:开门购物(触发预创建) -->
-        <view v-if="!authReady" class="btn-open" @click="handleOpenDoor">开门购物</view>
-        <!-- 已准备好授权:确认支付分 -->
-        <view v-else class="btn-open btn-auth" @click="invokePayScore">
-          {{ authBusy ? '跳转中...' : '确认支付分授权' }}
+        <view class="btn-open" @click="handleOpenDoor">
+          {{ opening ? '处理中...' : '开门购物' }}
         </view>
       </view>
     </view>
@@ -120,9 +117,10 @@
 <script setup lang="ts">
 import { ref, computed } from 'vue';
 import { onLoad } from '@dcloudio/uni-app';
+import { API_CONFIG } from '@/utils/config';
 import { getDeviceProducts, scanDoor } from '@/api/device';
-import { preCreatePayscoreOrder, queryPayscoreByOutNo } from '@/api/payscore';
-import { isLoggedIn } from '@/utils/auth';
+import { queryPayscoreByOutNo } from '@/api/payscore';
+import { isLoggedIn, getToken } from '@/utils/auth';
 import { logger } from '@/utils/logger';
 import BrandSlogan from '@/components/BrandSlogan.vue';
 import type { FloorConfig } from '@/api/device';
@@ -137,12 +135,7 @@ const deviceType = ref(0);
 const leftFloors = ref<FloorConfig[]>([]);
 const rightFloors = ref<FloorConfig[]>([]);
 const opening = ref(false);
-
-// 支付分授权状态
-const authReady = ref(false);
-const authBusy = ref(false);
-const authPackage = ref('');
-const authOutOrderNo = ref('');
+let pollingOutOrderNo = '';
 let pollTimer: ReturnType<typeof setInterval> | null = null;
 
 const activeDoor = ref<'left' | 'right'>('left');
@@ -203,7 +196,7 @@ const loadProducts = async () => {
   }
 };
 
-const handleOpenDoor = async () => {
+const handleOpenDoor = () => {
   if (opening.value) return;
 
   if (!isLoggedIn()) {
@@ -223,86 +216,81 @@ const handleOpenDoor = async () => {
   opening.value = true;
   uni.showLoading({ title: '准备中...', mask: true });
 
-  try {
-    // 预创建支付分订单
-    const result = await preCreatePayscoreOrder({ deviceId: deviceId.value });
-    uni.hideLoading();
-
-    if (!result.success || !result.package) {
-      opening.value = false;
-      uni.showToast({
-        title: result.errorMsg || '创建支付分订单失败',
-        icon: 'none',
-      });
-      return;
-    }
-
-    logger.log('[商品页] 支付分预创建成功');
-    authPackage.value = result.package;
-    authOutOrderNo.value = result.outOrderNo;
-    authReady.value = true;
-  } catch (error: any) {
-    uni.hideLoading();
-    opening.value = false;
-    logger.error('[商品页] 预创建失败', error);
-    uni.showToast({
-      title: error?.message || '创建支付分订单失败',
-      icon: 'none',
-    });
-  }
-};
-
-/**
- * 用户点击"确认支付分授权"按钮 → 调起微信官方确认页
- */
-const invokePayScore = () => {
-  if (authBusy.value) return;
-  authBusy.value = true;
+  // 使用 uni.request 回调方式,避免 async/await 打破手势链
+  uni.request({
+    url: API_CONFIG.baseUrl + '/payscore/pre-create',
+    method: 'POST',
+    header: {
+      'Content-Type': 'application/json',
+      'accessToken': getToken()
+    },
+    data: { deviceId: deviceId.value },
+    success: (apiRes: any) => {
+      uni.hideLoading();
+
+      const body = apiRes.data;
+      if (body.code !== 200 && body.code !== 0) {
+        opening.value = false;
+        uni.showToast({ title: body.message || '创建失败', icon: 'none' });
+        return;
+      }
 
-  const pkg = authPackage.value;
-  if (!pkg) {
-    authBusy.value = false;
-    return;
-  }
+      const data = body.data;
+      if (!data || !data.package) {
+        opening.value = false;
+        uni.showToast({ title: data?.errorMsg || '创建支付分订单失败', icon: 'none' });
+        return;
+      }
 
-  wx.openBusinessView({
-    businessType: 'wxpayScoreUse',
-    extraData: {
-      package: pkg,
-    },
-    success: () => {
-      logger.log('[商品页] 从支付分页面返回,开始轮询确认状态');
-      pollPayScoreStatus();
+      logger.log('[商品页] 预创建成功,直接拉起官方授权页');
+
+      pollingOutOrderNo = data.outOrderNo;
+
+      // 在原生回调中调用 openBusinessView,保持在手势链内
+      wx.openBusinessView({
+        businessType: 'wxpayScoreUse',
+        extraData: {
+          package: data.package,
+        },
+        success: () => {
+          logger.log('[商品页] 从授权页返回,轮询确认状态');
+          startPolling();
+        },
+        fail: (err: any) => {
+          opening.value = false;
+          const errMsg = err?.errMsg || JSON.stringify(err);
+          logger.error('[商品页] openBusinessView fail:', errMsg);
+          if (!errMsg.includes('cancel')) {
+            uni.showModal({
+              title: '调起支付分失败',
+              content: errMsg,
+              showCancel: false,
+            });
+          }
+        },
+      });
     },
     fail: (err: any) => {
-      authBusy.value = false;
-      const errMsg = err?.errMsg || JSON.stringify(err);
-      logger.error('[商品页] openBusinessView fail:', errMsg);
-      if (!errMsg.includes('cancel')) {
-        uni.showModal({
-          title: '调起支付分失败',
-          content: errMsg,
-          showCancel: false,
-        });
-      }
-    },
+      uni.hideLoading();
+      opening.value = false;
+      logger.error('[商品页] API 请求失败', err);
+      uni.showToast({ title: '网络异常,请重试', icon: 'none' });
+    }
   });
 };
 
-const pollPayScoreStatus = () => {
+const startPolling = () => {
   uni.showLoading({ title: '确认结果...', mask: true });
 
-  const outOrderNo = authOutOrderNo.value;
   let attempts = 0;
   const maxAttempts = 10;
-  const interval = 1000;
 
   if (pollTimer) clearInterval(pollTimer);
 
   pollTimer = setInterval(async () => {
     attempts++;
     try {
-      const res = await queryPayscoreByOutNo(outOrderNo);
+      const res = await queryPayscoreByOutNo(pollingOutOrderNo);
       const state = res?.state;
 
       logger.log(`[商品页] 轮询第 ${attempts} 次, state:`, state);
@@ -310,9 +298,8 @@ const pollPayScoreStatus = () => {
       if (state === 'DOING') {
         if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
         uni.hideLoading();
-        authReady.value = false;
-        authBusy.value = false;
-        logger.log('[商品页] 用户已确认,开始开门');
+        opening.value = false;
+        logger.log('[商品页] 用户已确认,开门');
         doOpenDoor();
         return;
       }
@@ -320,7 +307,7 @@ const pollPayScoreStatus = () => {
       if (attempts >= maxAttempts) {
         if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
         uni.hideLoading();
-        authBusy.value = false;
+        opening.value = false;
         logger.log('[商品页] 轮询超时,用户未确认');
       }
     } catch (e) {
@@ -328,18 +315,16 @@ const pollPayScoreStatus = () => {
       if (attempts >= maxAttempts) {
         if (pollTimer) { clearInterval(pollTimer); pollTimer = null; }
         uni.hideLoading();
-        authBusy.value = false;
+        opening.value = false;
       }
     }
-  }, interval);
+  }, 1000);
 };
 
-const doOpenDoor = async () => {
+const doOpenDoor = () => {
   uni.showLoading({ title: '正在开门...', mask: true });
 
-  try {
-    const response = await scanDoor(deviceId.value);
-
+  scanDoor(deviceId.value).then(response => {
     uni.hideLoading();
     uni.showToast({ title: '开门成功', icon: 'success' });
 
@@ -351,11 +336,11 @@ const doOpenDoor = async () => {
     setTimeout(() => {
       uni.redirectTo({ url: '/pages/shopping/shopping' });
     }, 1000);
-  } catch (error: any) {
+  }).catch((error: any) => {
     uni.hideLoading();
     opening.value = false;
     logger.error('开门失败:', error);
-  }
+  });
 };
 
 const goBack = () => {