Procházet zdrojové kódy

refactor(payment): JSAPI支付改用WeixinJSBridge.invoke(官方V3推荐)

按照微信支付V3文档(4012791870),将wx.chooseWXPay(JSSDK)
改为WeixinJSBridge.invoke('getBrandWXPayRequest')。

变更:
- 前端: 移除initWxJsSdk() + wx.config + wx.ready 整套JSSDK初始化
- 前端: 改用WeixinJSBridge.invoke直接调起支付
- 前端: 监听WeixinJSBridgeReady事件确保Bridge就绪
- 后端: 移除/payment/wechat/jsapi-config端点
- 后端: 移除getWechatAccessToken/getWechatJsapiTicket/getWechatJsapiConfig

优势:无需JSSDK签名配置,无需公众号JS安全域名,更简洁可靠
MyFramework User před 3 měsíci
rodič
revize
abd1200f93

+ 62 - 135
my-uniapp-vue3/src/pages/payment-confirm/index.vue

@@ -134,65 +134,10 @@ const tokenPackQuantity = ref<number>(0);
 const paymentMethod = ref<'alipay' | 'wechat'>('alipay');
 const inWechat = ref(false);
 const openid = ref('');
-const wxSdkReady = ref(false);
 const paying = ref(false);
 const lastPayClick = ref(0); // 防重:记录上次点击时间
 let checkTimer: ReturnType<typeof setInterval> | null = null;
 
-// ==================== 微信 JS-SDK 初始化 ====================
-async function initWxJsSdk(): Promise<boolean> {
-  // #ifdef H5
-  const wx = (window as any).wx;
-  if (!wx) {
-    console.warn('[WeChat Pay] 微信 JS-SDK 未加载');
-    return false;
-  }
-
-  try {
-    // 获取 JS-SDK 配置签名
-    const configRes = await post<{
-      debug: boolean;
-      appId: string;
-      timestamp: string;
-      nonceStr: string;
-      signature: string;
-      jsApiList: string[];
-    }>('/payment/wechat/jsapi-config', {
-      // 使用当前页面的完整 URL(不含 OAuth code 参数)
-      url: window.location.origin + window.location.pathname + window.location.hash
-    });
-
-    return new Promise((resolve) => {
-      wx.config({
-        debug: false,
-        appId: configRes.appId,
-        timestamp: configRes.timestamp,
-        nonceStr: configRes.nonceStr,
-        signature: configRes.signature,
-        jsApiList: configRes.jsApiList || ['chooseWXPay'],
-      });
-
-      wx.ready(() => {
-        console.log('[WeChat Pay] JS-SDK 初始化成功');
-        wxSdkReady.value = true;
-        resolve(true);
-      });
-
-      wx.error((err: any) => {
-        console.error('[WeChat Pay] JS-SDK 初始化失败:', err);
-        resolve(false);
-      });
-    });
-  } catch (e) {
-    console.error('[WeChat Pay] 获取JS-SDK配置失败:', e);
-    return false;
-  }
-  // #endif
-  // #ifndef H5
-  return false;
-  // #endif
-}
-
 // ==================== 微信 OAuth 处理 ====================
 async function handleWechatOAuth() {
   // #ifdef H5
@@ -280,10 +225,6 @@ onMounted(async () => {
     paymentMethod.value = 'wechat';
     // 处理 OAuth 回调
     await handleWechatOAuth();
-    // 如果有 openid,提前初始化 JS-SDK
-    if (openid.value) {
-      initWxJsSdk(); // 异步初始化,不阻塞
-    }
     // 微信环境不预创建订单(JSAPI 支付时另外创建)
   } else {
     // 非微信环境:进入页面时直接创建订单
@@ -350,8 +291,9 @@ async function checkWechatOrderStatus(orderNo: string): Promise<string | null> {
 }
 
 // ==================== 微信 JSAPI 支付(公众号内) ====================
+// 使用 WeixinJSBridge.invoke 直接调起(官方 V3 推荐方式,无需 JSSDK 配置)
+// 参考: https://pay.weixin.qq.com/doc/v3/merchant/4012791870
 async function handleWechatJsapiPay() {
-  // #ifdef H5
   // 如果没有 openid,需要先授权
   if (!openid.value) {
     await redirectToWechatOAuth();
@@ -381,87 +323,73 @@ async function handleWechatJsapiPay() {
     orderInfo.value.orderNo = result.orderNo;
     uni.hideLoading();
 
-    const wx = (window as any).wx;
-
-    // 确保 JS-SDK 已初始化
-    if (!wxSdkReady.value) {
-      const ok = await initWxJsSdk();
-      if (!ok) {
-        uni.showToast({ title: '微信支付初始化失败,请使用扫码支付', icon: 'none' });
-        paying.value = false;
-        return;
-      }
-    }
-
-    // 调起微信支付
-    wx.chooseWXPay({
-      appId: result.payParams.appId,
-      timestamp: result.payParams.timeStamp,
-      nonceStr: result.payParams.nonceStr,
-      package: result.payParams.package,
-      signType: result.payParams.signType,
-      paySign: result.payParams.paySign,
-      success: async () => {
-        console.log('[WeChat Pay] wx.chooseWXPay 返回成功,查询订单确认...');
-        // 官方要求:必须调用查询订单API确认最终状态,避免依赖回调直接展示
-        const orderStatus = await checkWechatOrderStatus(result.orderNo);
-        paying.value = false;
-
-        if (orderStatus === 'SUCCESS') {
-          uni.showModal({
-            title: '支付成功',
-            content: productType.value === 'token-pack' ? '积分包购买成功!' : '恭喜您订阅成功!',
-            showCancel: false,
-            success: () => {
-              uni.switchTab({ url: '/pages/mine/index' });
-            }
-          });
-        } else {
-          uni.showToast({ title: '订单确认中,请稍后查看', icon: 'none' });
-        }
-      },
-      cancel: () => {
-        console.log('[WeChat Pay] 用户取消支付');
-        paying.value = false;
-        uni.showToast({ title: '已取消支付', icon: 'none' });
-      },
-      fail: async (err: any) => {
-        console.error('[WeChat Pay] wx.chooseWXPay fail:', err);
-        // fail 可能并不是真正的失败(用户取消也可能进这里),查询订单确认
-        const orderStatus = await checkWechatOrderStatus(result.orderNo);
-        paying.value = false;
-
-        if (orderStatus === 'SUCCESS') {
-          uni.showModal({
-            title: '支付成功',
-            content: productType.value === 'token-pack' ? '积分包购买成功!' : '恭喜您订阅成功!',
-            showCancel: false,
-            success: () => {
-              uni.switchTab({ url: '/pages/mine/index' });
+    // 使用 WeixinJSBridge.invoke 调起支付
+    const onBridgeReady = () => {
+      (window as any).WeixinJSBridge.invoke(
+        'getBrandWXPayRequest',
+        {
+          "appId": result.payParams.appId,
+          "timeStamp": result.payParams.timeStamp,
+          "nonceStr": result.payParams.nonceStr,
+          "package": result.payParams.package,
+          "signType": result.payParams.signType || "RSA",
+          "paySign": result.payParams.paySign,
+        },
+        async (res: any) => {
+          console.log('[WeChat Pay] WeixinJSBridge 回调:', JSON.stringify(res));
+          // 官方要求:必须调用查询订单API确认最终状态
+          const orderStatus = await checkWechatOrderStatus(result.orderNo);
+          paying.value = false;
+
+          if (res.err_msg === "get_brand_wcpay_request:ok") {
+            if (orderStatus === 'SUCCESS') {
+              uni.showModal({
+                title: '支付成功',
+                content: productType.value === 'token-pack' ? '积分包购买成功!' : '恭喜您订阅成功!',
+                showCancel: false,
+                success: () => uni.switchTab({ url: '/pages/mine/index' })
+              });
+            } else {
+              uni.showToast({ title: '订单确认中,请稍后查看', icon: 'none' });
             }
-          });
-        } else {
-          // 确认支付失败,引导使用扫码支付
-          uni.showModal({
-            title: '支付失败',
-            content: '微信支付调起失败,是否使用扫码支付?',
-            confirmText: '扫码支付',
-            cancelText: '取消',
-            success: (res) => {
-              if (res.confirm && result.orderNo) {
-                inWechat.value = false;
-                handlePay();
+          } else if (res.err_msg === "get_brand_wcpay_request:cancel") {
+            uni.showToast({ title: '已取消支付', icon: 'none' });
+          } else if (orderStatus === 'SUCCESS') {
+            // 桥回调失败但订单已支付(延迟回调)
+            uni.showModal({
+              title: '支付成功',
+              content: productType.value === 'token-pack' ? '积分包购买成功!' : '恭喜您订阅成功!',
+              showCancel: false,
+              success: () => uni.switchTab({ url: '/pages/mine/index' })
+            });
+          } else {
+            uni.showModal({
+              title: '支付失败',
+              content: '微信支付调起失败,是否使用扫码支付?',
+              confirmText: '扫码支付',
+              cancelText: '取消',
+              success: (modalRes: any) => {
+                if (modalRes.confirm && result.orderNo) {
+                  inWechat.value = false;
+                  handlePay();
+                }
               }
-            }
-          });
+            });
+          }
         }
-      }
-    });
+      );
+    };
+
+    // WeixinJSBridge 可能尚未就绪,监听就绪事件
+    if (typeof (window as any).WeixinJSBridge === 'undefined') {
+      document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
+    } else {
+      onBridgeReady();
+    }
   } catch (error: any) {
     paying.value = false;
     uni.hideLoading();
     console.error('[WeChat Pay] 创建JSAPI支付失败:', error);
-    // 如果 JSAPI 支付失败(可能是 openid 过期),清除缓存让用户重新授权
     if (error.message && error.message.includes('openid')) {
       localStorage.removeItem('wx_openid');
       openid.value = '';
@@ -470,7 +398,6 @@ async function handleWechatJsapiPay() {
     }
     uni.showToast({ title: error.message || '支付失败', icon: 'none' });
   }
-  // #endif
 }
 
 async function handlePay() {

+ 0 - 30
server/src/modules/payment/payment.controller.ts

@@ -348,36 +348,6 @@ router.post('/wechat/openid', async (ctx: Context) => {
   };
 });
 
-// 获取微信 JS-SDK 配置(用于 wx.config)
-router.post('/wechat/jsapi-config', async (ctx: Context) => {
-  const { url } = ctx.request.body as { url: string };
-
-  if (!url) {
-    ctx.body = { code: -1, message: '请提供页面URL', data: null };
-    return;
-  }
-
-  const config = await PaymentService.getWechatJsapiConfig(url);
-
-  if (!config) {
-    ctx.body = { code: -1, message: 'JS-SDK配置获取失败', data: null };
-    return;
-  }
-
-  ctx.body = {
-    code: 0,
-    message: 'success',
-    data: {
-      debug: false,
-      appId: config.appId,
-      timestamp: config.timestamp,
-      nonceStr: config.nonceStr,
-      signature: config.signature,
-      jsApiList: ['chooseWXPay'] // 只需要支付接口
-    }
-  };
-});
-
 // 创建微信 JSAPI 支付订单(公众号内支付)
 router.post('/wechat/jsapi', authMiddleware, async (ctx: Context) => {
   const userId = safeParseInt(ctx.state.user.userId);

+ 0 - 93
server/src/modules/payment/payment.service.ts

@@ -8,13 +8,6 @@ let AlipaySdk: any = null;
 let WeChatPay: any = null;
 let sdkInitialized = false;
 
-// ==================== 微信 JSAPI 支付相关缓存 ====================
-let cachedAccessToken: string | null = null;
-let accessTokenExpireAt: number = 0;
-
-let cachedJsapiTicket: string | null = null;
-let jsapiTicketExpireAt: number = 0;
-
 // 初始化支付 SDK
 async function initPaymentSdks() {
   if (sdkInitialized) return;
@@ -812,92 +805,6 @@ function httpsGet(url: string): Promise<any> {
 }
 
 // 获取微信公众号 access_token(带缓存)
-export async function getWechatAccessToken(): Promise<string | null> {
-  const now = Date.now();
-  if (cachedAccessToken && now < accessTokenExpireAt) {
-    return cachedAccessToken;
-  }
-
-  const appId = process.env.WECHAT_APP_ID;
-  const appSecret = process.env.WECHAT_APP_SECRET;
-
-  if (!appId || !appSecret) {
-    console.warn('[WeChat JSAPI] 未配置 WECHAT_APP_ID 或 WECHAT_APP_SECRET');
-    return null;
-  }
-
-  try {
-    const data = await httpsGet(
-      `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}`
-    );
-    if (data.errcode) {
-      console.error('[WeChat JSAPI] 获取 access_token 失败:', data);
-      return null;
-    }
-    cachedAccessToken = data.access_token;
-    // 提前 5 分钟过期
-    accessTokenExpireAt = now + (data.expires_in - 300) * 1000;
-    console.log('[WeChat JSAPI] access_token 刷新成功, 过期时间:', new Date(accessTokenExpireAt).toISOString());
-    return cachedAccessToken;
-  } catch (e) {
-    console.error('[WeChat JSAPI] 获取 access_token 异常:', e);
-    return null;
-  }
-}
-
-// 获取微信 JS-SDK ticket(带缓存)
-export async function getWechatJsapiTicket(): Promise<string | null> {
-  const now = Date.now();
-  if (cachedJsapiTicket && now < jsapiTicketExpireAt) {
-    return cachedJsapiTicket;
-  }
-
-  const accessToken = await getWechatAccessToken();
-  if (!accessToken) return null;
-
-  try {
-    const data = await httpsGet(
-      `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${accessToken}&type=jsapi`
-    );
-    if (data.errcode !== 0) {
-      console.error('[WeChat JSAPI] 获取 jsapi_ticket 失败:', data);
-      return null;
-    }
-    cachedJsapiTicket = data.ticket;
-    // 提前 5 分钟过期
-    jsapiTicketExpireAt = now + (data.expires_in - 300) * 1000;
-    console.log('[WeChat JSAPI] jsapi_ticket 刷新成功');
-    return cachedJsapiTicket;
-  } catch (e) {
-    console.error('[WeChat JSAPI] 获取 jsapi_ticket 异常:', e);
-    return null;
-  }
-}
-
-// 生成微信 JS-SDK 签名配置
-export async function getWechatJsapiConfig(url: string): Promise<{
-  appId: string;
-  timestamp: string;
-  nonceStr: string;
-  signature: string;
-} | null> {
-  const ticket = await getWechatJsapiTicket();
-  if (!ticket) return null;
-
-  const nonceStr = crypto.randomBytes(16).toString('hex');
-  const timestamp = Math.floor(Date.now() / 1000).toString();
-  const signStr = `jsapi_ticket=${ticket}&noncestr=${nonceStr}&timestamp=${timestamp}&url=${url}`;
-
-  const signature = crypto.createHash('sha1').update(signStr).digest('hex');
-
-  return {
-    appId: process.env.WECHAT_APP_ID || '',
-    timestamp,
-    nonceStr,
-    signature,
-  };
-}
-
 // 通过 OAuth code 获取用户 openid
 export async function getWechatOpenid(code: string): Promise<string | null> {
   const appId = process.env.WECHAT_APP_ID;