Преглед изворни кода

feat: 前端接入小程序虚拟支付

api.js新增createAssessmentOrder/getAssessmentOrderDetail/createVirtualPayOrder/requestVirtualPayment,升级页与测评购买页假支付改真支付
asus пре 3 месеци
родитељ
комит
f6804b8c28

+ 79 - 24
cfc-frontend/pages/assessment/purchase.vue

@@ -85,7 +85,7 @@
 </template>
 
 <script>
-import { getAvailableGuides, getPackagePrice, createPackageOrder } from '../../utils/api.js'
+import { getAvailableGuides, getPackagePrice, createAssessmentOrder, createVirtualPayOrder, requestVirtualPayment, getAssessmentOrderDetail } from '../../utils/api.js'
 
 export default {
   data() {
@@ -246,43 +246,98 @@ export default {
         uni.showToast({ title: '请选择任务模板', icon: 'none' })
         return
       }
+      if (!this.selectedGuideId) {
+        uni.showToast({ title: '请选择成长规划师', icon: 'none' })
+        return
+      }
+      var childId = uni.getStorageSync('currentChildId')
+      if (!childId) {
+        uni.showToast({ title: '请先选择孩子', icon: 'none' })
+        return
+      }
 
       try {
         uni.showLoading({ title: '创建订单中...' })
 
-        const userId = uni.getStorageSync('userId')
-        const familyId = uni.getStorageSync('familyId')
+        var userId = uni.getStorageSync('userId')
+        var familyId = uni.getStorageSync('familyId')
+        var self = this
 
-        const res = await createPackageOrder({
-          userId: userId,
+        var createRes = await createAssessmentOrder({
           familyId: familyId,
-          packageId: this.selectedPackageId,
+          userId: userId,
+          childId: childId,
           guideId: this.selectedGuideId,
-          inviteCode: this.inviteCode,
-          price: this.totalPrice,
-          payMethod: 'wechat'
+          guideName: this.selectedGuideName,
+          packageId: this.selectedPackageId,
+          packageName: this.selectedPackage ? this.selectedPackage.name : '',
+          totalPrice: this.totalPrice,
+          discountAmount: this.discountAmount
         })
-
         uni.hideLoading()
 
-        if (res.code === 200) {
-          // 跳转到支付页面
-          uni.navigateTo({
-            url: `/pages/assessment/purchase?orderNo=${res.data.orderNo}&totalPrice=${this.totalPrice}`
-          })
-        } else {
-          uni.showToast({
-            title: res.message || '创建订单失败',
-            icon: 'none'
-          })
+        if (createRes.code !== 200 || !createRes.data || !createRes.data.orderNo) {
+          uni.showToast({ title: '创建订单失败', icon: 'none' })
+          return
+        }
+
+        var orderNo = createRes.data.orderNo
+        // 支付(内联返回 VirtualPayParamsDTO;testMode 返回 null 表示 mock 直接支付成功)
+        var payRes = await createVirtualPayOrder(orderNo, 'ASSESSMENT')
+        if (payRes.code !== 200) {
+          uni.showToast({ title: payRes.message || '支付失败', icon: 'none' })
+          return
         }
+
+        if (!payRes.data) {
+          // testMode:mock 已直接标记支付成功 → 跳预约页
+          uni.showToast({ title: '支付成功', icon: 'success' })
+          setTimeout(function() {
+            uni.navigateTo({ url: '/pages/assessment/apply' })
+          }, 800)
+          return
+        }
+
+        // 真实模式:拉起虚拟支付
+        requestVirtualPayment(payRes.data, {
+          success: function() {
+            uni.showToast({ title: '支付成功,确认中…', icon: 'none' })
+            self.pollOrderStatus(orderNo)
+          },
+          fail: function() {
+            uni.showToast({ title: '支付未完成', icon: 'none' })
+          }
+        })
       } catch (e) {
         uni.hideLoading()
-        uni.showToast({
-          title: '创建订单失败',
-          icon: 'none'
-        })
+        if (e.code === 46001) {
+          uni.showToast({ title: '该商品暂未开放购买', icon: 'none' })
+        } else {
+          uni.showToast({ title: e.message || '支付失败', icon: 'none' })
+        }
       }
+    },
+    pollOrderStatus: function(orderNo) {
+      var self = this
+      var times = 0
+      var timer = setInterval(function() {
+        times++
+        getAssessmentOrderDetail(orderNo).then(function(res) {
+          var order = res.data || {}
+          if (order.status === 'paid') {
+            clearInterval(timer)
+            uni.showToast({ title: '支付成功', icon: 'success' })
+            setTimeout(function() {
+              uni.navigateTo({ url: '/pages/assessment/apply' })
+            }, 800)
+          } else if (times >= 5) {
+            clearInterval(timer)
+            uni.navigateTo({ url: '/pages/assessment/apply' })
+          }
+        }).catch(function() {
+          if (times >= 5) { clearInterval(timer); uni.navigateTo({ url: '/pages/assessment/apply' }) }
+        })
+      }, 2000)
     }
   }
 }

+ 65 - 7
cfc-frontend/pages/membership/upgrade.vue

@@ -184,7 +184,7 @@
 </template>
 
 <script>
-import { getMyMembership, getMembershipLevels, getUpgradeRecords, upgradeMember, createOrder, getCouponList } from '../../utils/api.js'
+import { getMyMembership, getMembershipLevels, getUpgradeRecords, upgradeMember, createOrder, getCouponList, requestVirtualPayment } from '../../utils/api.js'
 
 export default {
   data() {
@@ -350,12 +350,51 @@ export default {
     doUpgrade: function() {
       var self = this
       createOrder(self.selectedLevel, 'pay', self.selectedPeriod).then(function(res) {
-        uni.showToast({ title: '开通成功', icon: 'success' })
-        self.loadData()
+        var data = res.data || {}
+        if (data.orderInfo && data.sign) {
+          // 真实模式:内联返回 VirtualPayParamsDTO → 拉起虚拟支付
+          requestVirtualPayment(data, {
+            success: function() {
+              uni.showToast({ title: '支付成功,开通中…', icon: 'none' })
+              self.pollOrderStatus()
+            },
+            fail: function() {
+              uni.showToast({ title: '支付未完成', icon: 'none' })
+            }
+          })
+        } else {
+          // testMode/trial:下单接口直接返回订单实体(mock 已开通)→ 沿用旧成功逻辑
+          uni.showToast({ title: '开通成功', icon: 'success' })
+          self.loadData()
+        }
       }).catch(function(e) {
-        uni.showToast({ title: e.message || '开通失败', icon: 'none' })
+        if (e.code === 46001) {
+          uni.showToast({ title: '该商品暂未开放购买', icon: 'none' })
+        } else {
+          uni.showToast({ title: e.message || '开通失败', icon: 'none' })
+        }
       })
     },
+    pollOrderStatus: function() {
+      var self = this
+      var times = 0
+      var timer = setInterval(function() {
+        times++
+        getMyMembership().then(function(res) {
+          var m = res.data || {}
+          if (m.membership && m.membership.isActive) {
+            clearInterval(timer)
+            uni.showToast({ title: '开通成功', icon: 'success' })
+            self.loadData()
+          } else if (times >= 5) {
+            clearInterval(timer)
+            self.loadData()
+          }
+        }).catch(function() {
+          if (times >= 5) { clearInterval(timer); self.loadData() }
+        })
+      }, 2000)
+    },
     handleRenew: function() {
       var self = this
       var priceText = '¥' + this.getPriceForPeriod(this.selectedPeriod) + this.getPeriodUnit()
@@ -372,10 +411,29 @@ export default {
     doRenew: function() {
       var self = this
       createOrder(self.selectedLevel, 'pay', this.selectedPeriod).then(function(res) {
-        uni.showToast({ title: '续费成功', icon: 'success' })
-        self.loadData()
+        var data = res.data || {}
+        if (data.orderInfo && data.sign) {
+          // 真实模式:内联返回 VirtualPayParamsDTO → 拉起虚拟支付
+          requestVirtualPayment(data, {
+            success: function() {
+              uni.showToast({ title: '支付成功,续费中…', icon: 'none' })
+              self.pollOrderStatus()
+            },
+            fail: function() {
+              uni.showToast({ title: '支付未完成', icon: 'none' })
+            }
+          })
+        } else {
+          // testMode/trial:下单接口直接返回订单实体(mock 已开通)→ 沿用旧成功逻辑
+          uni.showToast({ title: '续费成功', icon: 'success' })
+          self.loadData()
+        }
       }).catch(function(e) {
-        uni.showToast({ title: e.message || '续费失败', icon: 'none' })
+        if (e.code === 46001) {
+          uni.showToast({ title: '该商品暂未开放购买', icon: 'none' })
+        } else {
+          uni.showToast({ title: e.message || '续费失败', icon: 'none' })
+        }
       })
     },
     formatDate(date) {

+ 55 - 0
cfc-frontend/utils/api.js

@@ -836,6 +836,61 @@ export const payAssessmentOrder = (orderNo, payType) => {
   return request('/api/dan-assessment/order/pay', 'POST', { orderNo, payType })
 }
 
+// ===================== 虚拟支付 =====================
+
+// 创建测评订单(返回 AssessmentOrder,含 orderNo)
+export const createAssessmentOrder = (data) => {
+  return request('/api/dan-assessment/order/create', 'POST', data)
+}
+
+// 获取测评订单详情(轮询支付状态用;status: pending/paid/cancelled/refunded)
+export const getAssessmentOrderDetail = (orderNo) => {
+  return request('/api/dan-assessment/order/detail', 'POST', null, { orderNo })
+}
+
+// 虚拟支付参数薄包装(决策#7:各链路下单接口内联返回 VirtualPayParamsDTO,无独立端点)
+// ASSESSMENT → 测评订单支付(/order/pay 内联返回 DTO;testMode 返回 null)
+// MEMBERSHIP/SUBSCRIPTION → 下单接口已内联返回,调用方直接使用下单返回值,此处透传 orderNo
+export const createVirtualPayOrder = (orderNo, goodsType) => {
+  if (goodsType === 'ASSESSMENT') {
+    return payAssessmentOrder(orderNo, 'wechat')
+  }
+  return Promise.resolve({ orderNo: orderNo, goodsType: goodsType })
+}
+
+// 拉起微信小程序虚拟支付(官方 API 形态;基础库 2.19.2+,回调风格)
+// params = VirtualPayParamsDTO { orderInfo, sign, signature }
+export const requestVirtualPayment = (params, callbacks) => {
+  if (!wx.canIUse('requestVirtualPayment')) {
+    if (callbacks && callbacks.fail) {
+      callbacks.fail({ errMsg: '当前微信版本不支持虚拟支付' })
+    }
+    return
+  }
+  var dto = params || {}
+  wx.requestVirtualPayment({
+    signData: JSON.stringify(dto.orderInfo || {}),
+    paySig: dto.sign || '',
+    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) }
+  })
+}
+
+// 虚拟支付环境(官方仅 0=正式/1=沙箱;signData 内 env 由后端按 virtual-pay-env 生成)
+// 本函数为计划保留的工具函数(日志/调试用);develop→1(沙箱)、trial→1(沙箱)、release→0(正式)
+export const getVirtualPaymentEnv = () => {
+  try {
+    var info = uni.getAccountInfoSync()
+    var env = info.miniProgram && info.miniProgram.envVersion
+    if (env === 'develop' || env === 'trial') return 1
+    return 0
+  } catch (e) {
+    return 0
+  }
+}
+
 // 璁板綍娴嬭瘎缁撴灉
 export const recordAssessmentResult = (data) => {
   return request('/api/dan-assessment/result/record', 'POST', data)