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

feat: 虚拟支付失败错误上报后端日志,便于定位支付未完成根因

Xiaogang Liao 1 месяц назад
Родитель
Сommit
149d13b3ac

+ 20 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/VirtualPayController.java

@@ -2,10 +2,12 @@ package com.etotem.cfc.controller;
 
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.etotem.cfc.common.Result;
 import com.etotem.cfc.service.VirtualPayService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMethod;
 import org.springframework.web.bind.annotation.RequestParam;
@@ -107,10 +109,26 @@ public class VirtualPayController {
         }
     }
 
+    /**
+     * 前端虚拟支付失败上报(诊断用)。
+     * wx.requestVirtualPayment fail 时前端 fire-and-forget 上报错误码/订单号,
+     * 让支付失败原因在后端日志可见(正式版小程序前端 console 无法远程查看)。
+     * /api/internal/ 前缀已在 JwtInterceptor.PUBLIC_PATHS 公开,无需登录态。
+     */
+    @PostMapping("/error-report")
+    public Result<Boolean> errorReport(@RequestBody Map<String, Object> body) {
+        String outTradeNo = body.get("outTradeNo") != null ? String.valueOf(body.get("outTradeNo")) : "";
+        String scene = body.get("scene") != null ? String.valueOf(body.get("scene")) : "";
+        Object errCode = body.get("errCode");
+        String errMsg = body.get("errMsg") != null ? String.valueOf(body.get("errMsg")) : "";
+        log.error("前端虚拟支付失败上报: scene={}, outTradeNo={}, errCode={}, errMsg={}",
+                scene, outTradeNo, errCode, errMsg);
+        return Result.success(true);
+    }
+
     /** iOS App Store 退款问询应答(3秒内应答;3次未应答默认"不确定"交Apple裁决) */
     @PostMapping("/ios-refund-query")
-    public Map<String, Object> iosRefundQuery(HttpServletRequest request) {
-        try {
+    public Map<String, Object> iosRefundQuery(HttpServletRequest request) {        try {
             BufferedReader reader = request.getReader();
             StringBuilder sb = new StringBuilder();
             String line;

+ 10 - 2
cfc-frontend/pages/assessment/purchase.vue

@@ -306,8 +306,16 @@ export default {
             uni.showToast({ title: '支付成功,确认中…', icon: 'none' })
             self.pollOrderStatus(orderNo)
           },
-          fail: function() {
-            uni.showToast({ title: '支付未完成', icon: 'none' })
+          fail: function(err) {
+            console.error('[测评支付] requestVirtualPayment 失败', err)
+            var errCode = err && err.errCode
+            var msg = ''
+            if (errCode === -15007 || errCode === -15005) msg = '登录态已过期,请重新登录后支付'
+            else if (errCode === -15010) msg = '商品未发布,请联系客服'
+            else if (errCode === -15013) msg = '商品价格异常'
+            else if (errCode === -2) msg = '支付已取消'
+            else if (err && err.errMsg) msg = err.errMsg
+            uni.showToast({ title: msg || '支付未完成', icon: 'none' })
           }
         })
       } catch (e) {

+ 20 - 4
cfc-frontend/pages/membership/upgrade.vue

@@ -358,8 +358,16 @@ export default {
               uni.showToast({ title: '支付成功,开通中…', icon: 'none' })
               self.pollOrderStatus()
             },
-            fail: function() {
-              uni.showToast({ title: '支付未完成', icon: 'none' })
+            fail: function(err) {
+              console.error('[会员支付] requestVirtualPayment 失败', err)
+              var errCode = err && err.errCode
+              var msg = ''
+              if (errCode === -15007 || errCode === -15005) msg = '登录态已过期,请重新登录后支付'
+              else if (errCode === -15010) msg = '商品未发布,请联系客服'
+              else if (errCode === -15013) msg = '商品价格异常'
+              else if (errCode === -2) msg = '支付已取消'
+              else if (err && err.errMsg) msg = err.errMsg
+              uni.showToast({ title: msg || '支付未完成', icon: 'none' })
             }
           })
         } else {
@@ -419,8 +427,16 @@ export default {
               uni.showToast({ title: '支付成功,续费中…', icon: 'none' })
               self.pollOrderStatus()
             },
-            fail: function() {
-              uni.showToast({ title: '支付未完成', icon: 'none' })
+            fail: function(err) {
+              console.error('[会员续费] requestVirtualPayment 失败', err)
+              var errCode = err && err.errCode
+              var msg = ''
+              if (errCode === -15007 || errCode === -15005) msg = '登录态已过期,请重新登录后支付'
+              else if (errCode === -15010) msg = '商品未发布,请联系客服'
+              else if (errCode === -15013) msg = '商品价格异常'
+              else if (errCode === -2) msg = '支付已取消'
+              else if (err && err.errMsg) msg = err.errMsg
+              uni.showToast({ title: msg || '支付未完成', icon: 'none' })
             }
           })
         } else {

+ 29 - 1
cfc-frontend/utils/api.js

@@ -883,6 +883,29 @@ export const createVirtualPayOrder = (orderNo, goodsType) => {
   return Promise.resolve({ orderNo: orderNo, goodsType: goodsType })
 }
 
+// 虚拟支付失败上报(诊断用):wx.requestVirtualPayment fail 时 fire-and-forget 上报到后端,
+// 让支付失败原因在后端日志可见(正式版小程序前端 console 无法远程查看)。
+// 独立 uni.request 实现,不弹 toast、不影响主流程。
+export const reportVirtualPayError = (outTradeNo, errCode, errMsg, scene) => {
+  try {
+    uni.request({
+      url: BASE_URL + '/api/internal/virtual-pay/error-report',
+      method: 'POST',
+      data: {
+        outTradeNo: outTradeNo,
+        errCode: errCode,
+        errMsg: errMsg,
+        scene: scene
+      },
+      header: { 'Content-Type': 'application/json' },
+      success: function(res) {
+        if (res.data && res.data.code !== 200) console.warn('[虚拟支付] 失败上报接口异常', res.data)
+      },
+      fail: function() { /* 上报失败静默处理,不打扰用户 */ }
+    })
+  } catch (e) { /* 上报异常静默处理 */ }
+}
+
 // 拉起微信小程序虚拟支付(官方 API 形态;基础库 2.19.2+,回调风格)
 // params = VirtualPayParamsDTO { orderInfo, sign, signature }
 export const requestVirtualPayment = (params, callbacks) => {
@@ -899,7 +922,12 @@ export const requestVirtualPayment = (params, callbacks) => {
     signature: dto.signature || '',
     mode: 'short_series_goods',
     success: function(res) { if (callbacks && callbacks.success) callbacks.success(res) },
-    fail: function(err) { if (callbacks && callbacks.fail) callbacks.fail(err) }
+    fail: function(err) {
+      console.error('[虚拟支付] wx.requestVirtualPayment fail', err, 'signData=', JSON.stringify(dto.orderInfo || {}))
+      var orderNo = dto.orderInfo && dto.orderInfo.outTradeNo
+      reportVirtualPayError(orderNo, err && err.errCode, err && err.errMsg, 'requestVirtualPayment')
+      if (callbacks && callbacks.fail) callbacks.fail(err)
+    }
   })
 }