Просмотр исходного кода

fix: 修复微信JSAPI支付调起失败问题,使用wx.chooseWXPay替代WeixinJSBridge.invoke,并添加wx.config配置

MyFramework User 3 месяцев назад
Родитель
Сommit
92ba27b493

+ 3 - 3
deploy-prod.sh

@@ -4,7 +4,7 @@
 # 部署到 8.159.134.106:/data/ai/audio
 # 前端域名: book.rrbrr.com
 # 后端 API 域名: bookapi.rrbrr.com
-# 后端端口: 22622
+# 后端端口: 3100
 
 set -e
 
@@ -19,7 +19,7 @@ echo "时间: $(date)"
 echo "主机: $REMOTE_HOST"
 echo "路径: $REMOTE_PATH"
 echo "前端: book.rrbrr.com"
-echo "后端: bookapi.rrbrr.com:22622"
+echo "后端: bookapi.rrbrr.com:3100"
 echo ""
 
 # 1. 构建前端(后端使用已有的 dist)
@@ -168,7 +168,7 @@ pm2 start dist/app.js --name server --env production
 pm2 save
 pm2 startup
 
-echo "后端服务已启动 (端口 22622)!"
+echo "后端服务已启动 (端口 3100)!"
 ENDSSH
 
 echo ""

+ 79 - 42
my-uniapp-vue3/src/pages/payment-confirm/index.vue

@@ -326,45 +326,86 @@ async function handleWechatJsapiPay() {
     orderInfo.value.orderNo = result.orderNo;
     uni.hideLoading();
 
-    // 使用 WeixinJSBridge.invoke 调起支付
+    // 使用 wx.chooseWXPay 调起支付(微信官方推荐方式)
     const currentResult = result; // 保存引用,避免闭包问题
-    const onBridgeReady = () => {
-      if (!currentResult || !currentResult.payParams) {
-        console.error('[WeChat Pay] payParams 不存在:', currentResult);
-        uni.showToast({ title: '支付参数异常,请重试', icon: 'none' });
-        paying.value = false;
-        return;
+    
+    // 检查wx对象是否存在
+    if (typeof (window as any).wx === 'undefined') {
+      console.error('[WeChat Pay] wx对象不存在,可能未引入微信JS-SDK');
+      uni.showToast({ title: '支付环境异常,请刷新页面重试', icon: 'none' });
+      paying.value = false;
+      return;
+    }
+
+    // 1. 先获取JS-SDK签名并配置wx.config
+    const currentUrl = window.location.href.split('#')[0]; // 去掉hash部分
+    console.log('[WeChat Pay] 获取JS-SDK签名,url:', currentUrl);
+    
+    try {
+      const signResult = await get<any>('/payment/wechat/jssdk-sign', { url: currentUrl });
+      
+      if (!signResult || !signResult.appId) {
+        throw new Error('获取JS-SDK签名失败');
       }
-      (window as any).WeixinJSBridge.invoke(
-        'getBrandWXPayRequest',
-        {
-          "appId": currentResult.payParams.appId,
-          "timeStamp": currentResult.payParams.timeStamp,
-          "nonceStr": currentResult.payParams.nonceStr,
-          "package": currentResult.payParams.package,
-          "signType": currentResult.payParams.signType || "RSA",
-          "paySign": currentResult.payParams.paySign,
+
+      console.log('[WeChat Pay] JS-SDK签名获取成功,准备配置wx.config');
+      
+      // 配置wx.config
+      (window as any).wx.config({
+        debug: false, // 调试时可以改为true
+        appId: signResult.appId,
+        timestamp: signResult.timestamp,
+        nonceStr: signResult.nonceStr,
+        signature: signResult.signature,
+        jsApiList: ['chooseWXPay'] // 需要使用的JS接口列表
+      });
+
+      // 等待wx.ready
+      await new Promise<void>((resolve, reject) => {
+        (window as any).wx.ready(() => {
+          console.log('[WeChat Pay] wx.ready 成功');
+          resolve();
+        });
+        
+        (window as any).wx.error((err: any) => {
+          console.error('[WeChat Pay] wx.error:', err);
+          reject(new Error('wx.config失败: ' + JSON.stringify(err)));
+        });
+      });
+
+      console.log('[WeChat Pay] 开始调起wx.chooseWXPay');
+      
+      // 2. 使用 wx.chooseWXPay 调起支付
+      (window as any).wx.chooseWXPay({
+        timestamp: currentResult.payParams.timeStamp,
+        nonceStr: currentResult.payParams.nonceStr,
+        package: currentResult.payParams.package,
+        signType: currentResult.payParams.signType || "RSA",
+        paySign: currentResult.payParams.paySign,
+        success: async (res: any) => {
+          console.log('[WeChat Pay] wx.chooseWXPay 成功:', JSON.stringify(res));
+          // 官方要求:必须调用查询订单API确认最终状态
+          const orderStatus = await checkWechatOrderStatus(currentResult.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' });
+          }
         },
-        async (res: any) => {
-          console.log('[WeChat Pay] WeixinJSBridge 回调:', JSON.stringify(res));
+        fail: async (err: any) => {
+          console.error('[WeChat Pay] wx.chooseWXPay 失败:', JSON.stringify(err));
           // 官方要求:必须调用查询订单API确认最终状态
           const orderStatus = await checkWechatOrderStatus(currentResult.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 if (res.err_msg === "get_brand_wcpay_request:cancel") {
-            uni.showToast({ title: '已取消支付', icon: 'none' });
-          } else if (orderStatus === 'SUCCESS') {
+          if (orderStatus === 'SUCCESS') {
             // 桥回调失败但订单已支付(延迟回调)
             uni.showModal({
               title: '支付成功',
@@ -373,10 +414,9 @@ async function handleWechatJsapiPay() {
               success: () => uni.switchTab({ url: '/pages/mine/index' })
             });
           } else {
-            console.error('[WeChat Pay] 支付失败,err_msg:', res.err_msg);
             uni.showModal({
               title: '支付失败',
-              content: `微信支付调起失败 (${res.err_msg || '未知错误'}),是否使用扫码支付?`,
+              content: `微信支付调起失败 (${err.errMsg || err.err_msg || '未知错误'}),是否使用扫码支付?`,
               confirmText: '扫码支付',
               cancelText: '取消',
               success: (modalRes: any) => {
@@ -388,14 +428,11 @@ async function handleWechatJsapiPay() {
             });
           }
         }
-      );
-    };
-
-    // WeixinJSBridge 可能尚未就绪,监听就绪事件
-    if (typeof (window as any).WeixinJSBridge === 'undefined') {
-      document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
-    } else {
-      onBridgeReady();
+      });
+    } catch (configError: any) {
+      console.error('[WeChat Pay] wx.config 失败:', configError);
+      uni.showToast({ title: '支付配置失败: ' + (configError.message || '未知错误'), icon: 'none' });
+      paying.value = false;
     }
   } catch (error: any) {
     paying.value = false;

+ 120 - 0
server/src/middleware/UserscaoygAppDataLocalTempsecurity_b64.txt

@@ -0,0 +1,120 @@
+-----BEGIN CERTIFICATE-----
+LyoqDQogKiDlronlhajkuK3pl7Tku7YNCiAqIOWMheWQq++8mlhTUyDpmLLmiqTj
+gIFTUUwg5rOo5YWl6Ziy5oqk44CB5pWP5oSf5pWw5o2u6ISx5pWPDQogKi8NCg0K
+Ly8gWFNTIOmYsuaKpA0KZXhwb3J0IGZ1bmN0aW9uIHhzc1Byb3RlY3Rpb24oKSB7
+DQogIHJldHVybiBhc3luYyAoY3R4OiBhbnksIG5leHQ6IGFueSkgPT4gew0KICAg
+IC8vIOi/h+a7pOivt+axguS9k+S4reeahCBYU1MNCiAgICBpZiAoY3R4LnJlcXVl
+c3QuYm9keSAmJiB0eXBlb2YgY3R4LnJlcXVlc3QuYm9keSA9PT0gJ29iamVjdCcp
+IHsNCiAgICAgIHNhbml0aXplT2JqZWN0KGN0eC5yZXF1ZXN0LmJvZHkpOw0KICAg
+IH0NCg0KICAgIC8vIOi/h+a7pOafpeivouWPguaVsOS4reeahCBYU1MNCiAgICBp
+ZiAoY3R4LnJlcXVlc3QucXVlcnkgJiYgdHlwZW9mIGN0eC5yZXF1ZXN0LnF1ZXJ5
+ID09PSAnb2JqZWN0Jykgew0KICAgICAgc2FuaXRpemVPYmplY3QoY3R4LnJlcXVl
+c3QucXVlcnkpOw0KICAgIH0NCg0KICAgIC8vIOiuvue9ruWuieWFqOWTjeW6lOWk
+tA0KICAgIGN0eC5zZXQoJ1gtWFNTLVByb3RlY3Rpb24nLCAnMTsgbW9kZT1ibG9j
+aycpOw0KICAgIGN0eC5zZXQoJ1gtQ29udGVudC1UeXBlLU9wdGlvbnMnLCAnbm9z
+bmlmZicpOw0KICAgIGN0eC5zZXQoJ1gtRnJhbWUtT3B0aW9ucycsICdERU5ZJyk7
+DQogICAgY3R4LnNldCgnQ29udGVudC1TZWN1cml0eS1Qb2xpY3knLCAiZGVmYXVs
+dC1zcmMgJ3NlbGYnIik7DQoNCiAgICBhd2FpdCBuZXh0KCk7DQogIH07DQp9DQoN
+Ci8qKg0KICog6YCS5b2S5riF55CG5a+56LGh5Lit55qEIFhTUw0KICovDQpmdW5j
+dGlvbiBzYW5pdGl6ZU9iamVjdChvYmo6IGFueSk6IHZvaWQgew0KICBmb3IgKGNv
+bnN0IGtleSBpbiBvYmopIHsNCiAgICBpZiAodHlwZW9mIG9ialtrZXldID09PSAn
+c3RyaW5nJykgew0KICAgICAgb2JqW2tleV0gPSBzYW5pdGl6ZVN0cmluZyhvYmpb
+a2V5XSk7DQogICAgfSBlbHNlIGlmICh0eXBlb2Ygb2JqW2tleV0gPT09ICdvYmpl
+Y3QnICYmIG9ialtrZXldICE9PSBudWxsKSB7DQogICAgICBzYW5pdGl6ZU9iamVj
+dChvYmpba2V5XSk7DQogICAgfQ0KICB9DQp9DQoNCi8qKg0KICog5riF55CG5a2X
+56ym5Liy5Lit55qEIFhTUyDmlLvlh7vku6PnoIENCiAqLw0KZnVuY3Rpb24gc2Fu
+aXRpemVTdHJpbmcoc3RyOiBzdHJpbmcpOiBzdHJpbmcgew0KICByZXR1cm4gc3Ry
+DQogICAgLnJlcGxhY2UoLzwvZywgJyZsdDsnKQ0KICAgIC5yZXBsYWNlKC8+L2cs
+ICcmZ3Q7JykNCiAgICAucmVwbGFjZSgvIi9nLCAnJnF1b3Q7JykNCiAgICAucmVw
+bGFjZSgvJy9nLCAnJiN4Mjc7JykNCiAgICAucmVwbGFjZSgvXC8vZywgJyYjeDJG
+OycpDQogICAgLnJlcGxhY2UoL2phdmFzY3JpcHQ6L2dpLCAnJykNCiAgICAucmVw
+bGFjZSgvb25cdys9L2dpLCAnJykNCiAgICAucmVwbGFjZSgvPHNjcmlwdFtePl0q
+Pltcc1xTXSo/PFwvc2NyaXB0Pi9naSwgJycpDQogICAgLnJlcGxhY2UoLzxpZnJh
+bWVbXj5dKj5bXHNcU10qPzxcL2lmcmFtZT4vZ2ksICcnKQ0KICAgIC5yZXBsYWNl
+KC88b2JqZWN0W14+XSo+W1xzXFNdKj88XC9vYmplY3Q+L2dpLCAnJykNCiAgICAu
+cmVwbGFjZSgvPGVtYmVkW14+XSo+W1xzXFNdKj88XC9lbWJlZD4vZ2ksICcnKTsN
+Cn0NCg0KLy8gU1FMIOazqOWFpemYsuaKpA0KZXhwb3J0IGZ1bmN0aW9uIHNxbElu
+amVjdGlvblByb3RlY3Rpb24oKSB7DQogIHJldHVybiBhc3luYyAoY3R4OiBhbnks
+IG5leHQ6IGFueSkgPT4gew0KICAgIC8vID09PT09IERFQlVHOiDorrDlvZXmiYDm
+nInor7fmsYIgPT09PT0NCiAgICBpZiAoY3R4LnBhdGg/LmluY2x1ZGVzKCcvd2Vj
+aGF0L2pzYXBpJykpIHsNCiAgICAgIGNvbnNvbGUubG9nKCdbU0VDVVJJVFktREVC
+VUddIEpTQVBJ6K+35rGC5Yiw6L6+c2VjdXJpdHnkuK3pl7Tku7YnKTsNCiAgICAg
+IGNvbnNvbGUubG9nKCdbU0VDVVJJVFktREVCVUddIG1ldGhvZDonLCBjdHgubWV0
+aG9kKTsNCiAgICAgIGNvbnNvbGUubG9nKCdbU0VDVVJJVFktREVCVUddIHBhdGg6
+JywgY3R4LnBhdGgpOw0KICAgICAgY29uc29sZS5sb2coJ1tTRUNVUklUWS1ERUJV
+R10gcXVlcnk6JywgSlNPTi5zdHJpbmdpZnkoY3R4LnJlcXVlc3QucXVlcnkpKTsN
+CiAgICAgIGNvbnNvbGUubG9nKCdbU0VDVVJJVFktREVCVUddIGJvZHk6JywgSlNP
+Ti5zdHJpbmdpZnkoY3R4LnJlcXVlc3QuYm9keSkpOw0KICAgICAgY29uc29sZS5s
+b2coJ1tTRUNVUklUWS1ERUJVR10gaGVhZGVyc1tjb250ZW50LXR5cGVdOicsIGN0
+eC5yZXF1ZXN0LmhlYWRlcnNbJ2NvbnRlbnQtdHlwZSddKTsNCiAgICB9DQoNCiAg
+ICAvLyDmo4Dmn6Xor7fmsYLlj4LmlbANCiAgICBjb25zdCBwYXJhbXMgPSB7DQog
+ICAgICAuLi5jdHgucmVxdWVzdC5xdWVyeSwNCiAgICAgIC4uLihjdHgucmVxdWVz
+dC5ib2R5IHx8IHt9KSwNCiAgICB9Ow0KDQogICAgZm9yIChjb25zdCBrZXkgaW4g
+cGFyYW1zKSB7DQogICAgICBpZiAodHlwZW9mIHBhcmFtc1trZXldID09PSAnc3Ry
+aW5nJykgew0KICAgICAgICBpZiAoZGV0ZWN0U1FMSW5qZWN0aW9uKHBhcmFtc1tr
+ZXldKSkgew0KICAgICAgICAgIGNvbnNvbGUubG9nKCdbU0VDVVJJVFktREVCVUdd
+IOKdjCBKU0FQSeivt+axguiiq1NRTOazqOWFpeaLpuaIqiwga2V5OicsIGtleSwg
+J3ZhbHVlOicsIHBhcmFtc1trZXldLnN1YnN0cmluZygwLCAxMDApKTsNCiAgICAg
+ICAgICBjdHguc3RhdHVzID0gNDAwOw0KICAgICAgICAgIGN0eC5ib2R5ID0gew0K
+ICAgICAgICAgICAgY29kZTogNDAwLA0KICAgICAgICAgICAgbWVzc2FnZTogJ+iv
+t+axguWMheWQq+mdnuazleWtl+espicsDQogICAgICAgICAgfTsNCiAgICAgICAg
+ICByZXR1cm47DQogICAgICAgIH0NCiAgICAgIH0NCiAgICB9DQoNCiAgICBpZiAo
+Y3R4LnBhdGg/LmluY2x1ZGVzKCcvd2VjaGF0L2pzYXBpJykpIHsNCiAgICAgIGNv
+bnNvbGUubG9nKCdbU0VDVVJJVFktREVCVUddIEpTQVBJ6K+35rGC6YCa6L+Hc2Vj
+dXJpdHnkuK3pl7Tku7bvvIzov5vlhaVuZXh0KCknKTsNCiAgICB9DQoNCiAgICBh
+d2FpdCBuZXh0KCk7DQogIH07DQp9DQoNCi8qKg0KICog5qOA5rWLIFNRTCDms6jl
+haUNCiAqIOazqOaEj++8muS9v+eUqCBQcmlzbWEgT1JNIOW3suWPguaVsOWMluaJ
+gOacieafpeivou+8jOatpOS4remXtOS7tuS4uua3seW6pumYsuW+oQ0KICog6YG/
+5YWN6L+H5LqO5r+A6L+b55qE5q2j5YiZ5a+86Ie05q2j5bi46K+35rGC6KKr6K+v
+5oum77yI5aaCIEAg5ZyoIGVtYWlsIOS4reW+iOW4uOinge+8iQ0KICovDQpmdW5j
+dGlvbiBkZXRlY3RTUUxJbmplY3Rpb24oc3RyOiBzdHJpbmcpOiBib29sZWFuIHsN
+CiAgY29uc3Qgc3FsUGF0dGVybnMgPSBbDQogICAgLy8gU1FMIOazqOmHiuagh+iu
+sO+8iOWni+e7iOWPr+eWke+8iQ0KICAgIC9cL1wqLio/XCpcLy8sDQogICAgLy0t
+XHMvLA0KICAgIC8vIFNRTCDor63lj6XloIblj6DvvIjliIblj7cgKyDmnInmlYgg
+U1FMIOWFs+mUruWtl++8iQ0KICAgIC87XHMqKFNFTEVDVHxJTlNFUlR8VVBEQVRF
+fERFTEVURXxEUk9QfENSRUFURXxBTFRFUnxFWEVDfEVYRUNVVEV8VU5JT04pXGIv
+aSwNCiAgICAvLyBTUUwg57O757uf5Y+Y6YePDQogICAgL0BAXHcrLywNCiAgICAv
+LyDnu4/lhbggU1FMIOazqOWFpeawuOecn+W8jw0KICAgIC8oXGJPUlxiXHMrXGQr
+XHMqPVxzKlxkKykvaSwNCiAgICAvKCd8IilccypPUlxzKignfCIpXHMqJC9pLA0K
+ICAgIC8oJ3wiKVxzKk9SXHMrXGQrXHMqPVxzKlxkKy9pLA0KICAgIC8vIFVOSU9O
+IOazqOWFpQ0KICAgIC9cYlVOSU9OXHMrKEFMTFxzKyk/U0VMRUNUXGIvaSwNCiAg
+ICAvLyDml7bpl7Tnm7Lms6gNCiAgICAvXGJXQUlURk9SXHMrREVMQVlcYi9pLA0K
+ICAgIC9cYkJFTkNITUFSS1xiL2ksDQogICAgL1NMRUVQXHMqXCgvaSwNCiAgXTsN
+Cg0KICBjb25zdCBtYXRjaGVkID0gc3FsUGF0dGVybnMuc29tZShwYXR0ZXJuID0+
+IHBhdHRlcm4udGVzdChzdHIpKTsNCiAgaWYgKG1hdGNoZWQpIHsNCiAgICBjb25z
+b2xlLndhcm4oYFtTZWN1cml0eV0gU1FM5rOo5YWl5qOA5rWL6Kem5Y+RLCDovpPl
+haU6ICIke3N0ci5zdWJzdHJpbmcoMCwgMjAwKX0iYCk7DQogIH0NCiAgcmV0dXJu
+IG1hdGNoZWQ7DQp9DQoNCi8vIOaVj+aEn+aVsOaNruiEseaVjw0KZXhwb3J0IGZ1
+bmN0aW9uIHNlbnNpdGl2ZURhdGFNYXNraW5nKCkgew0KICByZXR1cm4gYXN5bmMg
+KGN0eDogYW55LCBuZXh0OiBhbnkpID0+IHsNCiAgICBhd2FpdCBuZXh0KCk7DQoN
+CiAgICAvLyDohLHmlY/lk43lupTkuK3nmoTmlY/mhJ/mlbDmja4NCiAgICBpZiAo
+Y3R4LmJvZHkgJiYgdHlwZW9mIGN0eC5ib2R5ID09PSAnb2JqZWN0Jykgew0KICAg
+ICAgbWFza1NlbnNpdGl2ZURhdGEoY3R4LmJvZHkpOw0KICAgIH0NCiAgfTsNCn0N
+Cg0KLyoqDQogKiDohLHmlY/mlY/mhJ/mlbDmja4NCiAqLw0KZnVuY3Rpb24gbWFz
+a1NlbnNpdGl2ZURhdGEob2JqOiBhbnkpOiB2b2lkIHsNCiAgY29uc3Qgc2Vuc2l0
+aXZlS2V5cyA9IFsncGFzc3dvcmQnLCAndG9rZW4nLCAnc2VjcmV0JywgJ2tleScs
+ICdjb29raWUnLCAnYXV0aG9yaXphdGlvbiddOw0KDQogIGZvciAoY29uc3Qga2V5
+IGluIG9iaikgew0KICAgIGlmIChzZW5zaXRpdmVLZXlzLmluY2x1ZGVzKGtleS50
+b0xvd2VyQ2FzZSgpKSkgew0KICAgICAgaWYgKHR5cGVvZiBvYmpba2V5XSA9PT0g
+J3N0cmluZycgJiYgb2JqW2tleV0ubGVuZ3RoID4gOCkgew0KICAgICAgICBvYmpb
+a2V5XSA9IG9ialtrZXldLnN1YnN0cmluZygwLCA0KSArICcqKioqJyArIG9ialtr
+ZXldLnN1YnN0cmluZyhvYmpba2V5XS5sZW5ndGggLSA0KTsNCiAgICAgIH0gZWxz
+ZSB7DQogICAgICAgIG9ialtrZXldID0gJyoqKionOw0KICAgICAgfQ0KICAgIH0g
+ZWxzZSBpZiAodHlwZW9mIG9ialtrZXldID09PSAnb2JqZWN0JyAmJiBvYmpba2V5
+XSAhPT0gbnVsbCkgew0KICAgICAgbWFza1NlbnNpdGl2ZURhdGEob2JqW2tleV0p
+Ow0KICAgIH0NCiAgfQ0KfQ0KDQovLyDlr4bnoIHliqDlr4blt6XlhbcNCmV4cG9y
+dCBmdW5jdGlvbiBtYXNrUGFzc3dvcmQocGFzc3dvcmQ6IHN0cmluZyk6IHN0cmlu
+ZyB7DQogIGlmICghcGFzc3dvcmQgfHwgcGFzc3dvcmQubGVuZ3RoIDwgOCkgcmV0
+dXJuICcqKioqJzsNCiAgcmV0dXJuIHBhc3N3b3JkLnN1YnN0cmluZygwLCAyKSAr
+ICcqKioqJyArIHBhc3N3b3JkLnN1YnN0cmluZyhwYXNzd29yZC5sZW5ndGggLSAy
+KTsNCn0NCg0KLy8g5omL5py65Y+36ISx5pWPDQpleHBvcnQgZnVuY3Rpb24gbWFz
+a1Bob25lKHBob25lOiBzdHJpbmcpOiBzdHJpbmcgew0KICBpZiAoIXBob25lIHx8
+IHBob25lLmxlbmd0aCAhPT0gMTEpIHJldHVybiBwaG9uZTsNCiAgcmV0dXJuIHBo
+b25lLnN1YnN0cmluZygwLCAzKSArICcqKioqJyArIHBob25lLnN1YnN0cmluZyg3
+KTsNCn0NCg0KLy8g6YKu566x6ISx5pWPDQpleHBvcnQgZnVuY3Rpb24gbWFza0Vt
+YWlsKGVtYWlsOiBzdHJpbmcpOiBzdHJpbmcgew0KICBpZiAoIWVtYWlsIHx8ICFl
+bWFpbC5pbmNsdWRlcygnQCcpKSByZXR1cm4gZW1haWw7DQogIGNvbnN0IFtuYW1l
+LCBkb21haW5dID0gZW1haWwuc3BsaXQoJ0AnKTsNCiAgaWYgKG5hbWUubGVuZ3Ro
+IDw9IDIpIHJldHVybiBgKipAJHtkb21haW59YDsNCiAgcmV0dXJuIG5hbWUuc3Vi
+c3RyaW5nKDAsIDIpICsgJyoqKionICsgJ0AnICsgZG9tYWluOw0KfQ0K
+-----END CERTIFICATE-----

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

@@ -355,6 +355,29 @@ router.post('/wechat/openid', async (ctx: Context) => {
   };
 });
 
+// 获取微信 JS-SDK 签名(用于 wx.config)
+router.get('/wechat/jssdk-sign', async (ctx: Context) => {
+  const { url } = ctx.query as { url?: string };
+  
+  if (!url) {
+    ctx.body = { code: -1, message: '缺少url参数', data: null };
+    return;
+  }
+
+  const signature = await PaymentService.generateJsSdkSignature(url);
+  
+  if (!signature) {
+    ctx.body = { code: -1, message: '生成签名失败', data: null };
+    return;
+  }
+
+  ctx.body = {
+    code: 0,
+    message: 'success',
+    data: signature
+  };
+});
+
 // 创建微信 JSAPI 支付订单(公众号内支付)
 router.post('/wechat/jsapi', authMiddleware, async (ctx: Context) => {
   const userId = safeParseInt(ctx.state.user.userId);

+ 96 - 1
server/src/modules/payment/payment.service.ts

@@ -910,4 +910,99 @@ export async function generateWechatJsapiPayment(
 }
 
 // 初始化 SDK
-initPaymentSdks();
+initPaymentSdks();
+
+// ==================== 微信 JS-SDK 签名 ====================
+// 获取 jsapi_ticket(需要缓存)
+let jsapiTicketCache: { ticket: string; expireTime: number } | null = null;
+
+async function getJsapiTicket(): Promise<string | null> {
+  // 检查缓存
+  if (jsapiTicketCache && jsapiTicketCache.expireTime > Date.now()) {
+    return jsapiTicketCache.ticket;
+  }
+
+  const appId = process.env.WECHAT_PUBLIC_APP_ID || process.env.WECHAT_APP_ID;
+  const appSecret = process.env.WECHAT_PUBLIC_APP_SECRET || process.env.WECHAT_APP_SECRET;
+
+  if (!appId || !appSecret) {
+    console.error('[JS-SDK] 未配置 WECHAT_APP_ID 或 WECHAT_APP_SECRET');
+    return null;
+  }
+
+  try {
+    // 1. 获取 access_token
+    const tokenUrl = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}`;
+    const tokenRes = await new Promise<any>((resolve, reject) => {
+      https.get(tokenUrl, (res) => {
+        let data = '';
+        res.on('data', (chunk) => data += chunk);
+        res.on('end', () => resolve(JSON.parse(data)));
+      }).on('error', reject);
+    });
+
+    if (tokenRes.errcode) {
+      console.error('[JS-SDK] 获取 access_token 失败:', tokenRes);
+      return null;
+    }
+
+    const accessToken = tokenRes.access_token;
+
+    // 2. 获取 jsapi_ticket
+    const ticketUrl = `https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=${accessToken}&type=jsapi`;
+    const ticketRes = await new Promise<any>((resolve, reject) => {
+      https.get(ticketUrl, (res) => {
+        let data = '';
+        res.on('data', (chunk) => data += chunk);
+        res.on('end', () => resolve(JSON.parse(data)));
+      }).on('error', reject);
+    });
+
+    if (ticketRes.errcode !== 0) {
+      console.error('[JS-SDK] 获取 jsapi_ticket 失败:', ticketRes);
+      return null;
+    }
+
+    // 缓存 ticket(提前5分钟过期)
+    jsapiTicketCache = {
+      ticket: ticketRes.ticket,
+      expireTime: Date.now() + (ticketRes.expires_in - 300) * 1000
+    };
+
+    return ticketRes.ticket;
+  } catch (error: any) {
+    console.error('[JS-SDK] 获取 jsapi_ticket 异常:', error);
+    return null;
+  }
+}
+
+// 生成 JS-SDK 签名
+export async function generateJsSdkSignature(url: string): Promise<{
+  appId: string;
+  timestamp: string;
+  nonceStr: string;
+  signature: string;
+} | null> {
+  const ticket = await getJsapiTicket();
+  if (!ticket) {
+    return null;
+  }
+
+  const nonceStr = Math.random().toString(36).substring(2, 15);
+  const timestamp = Math.floor(Date.now() / 1000).toString();
+
+  // 签名字符串:jsapi_ticket=XX&noncestr=XX&timestamp=XX&url=XX
+  const signatureStr = `jsapi_ticket=${ticket}&noncestr=${nonceStr}&timestamp=${timestamp}&url=${url}`;
+  const signature = crypto.createHash('sha1').update(signatureStr).digest('hex');
+
+  const appId = process.env.WECHAT_PUBLIC_APP_ID || process.env.WECHAT_APP_ID || '';
+
+  console.log('[JS-SDK] 生成签名:', { url, nonceStr, timestamp, signature: signature.substring(0, 20) + '...' });
+
+  return {
+    appId,
+    timestamp,
+    nonceStr,
+    signature
+  };
+}