فهرست منبع

feat(payment): 优化H5支付结果轮询及商品添加防重复逻辑

- 新增H5支付结果轮询函数pollOrderPaySuccess,支持超时与间隔自定义
- 在多个支付相关页面集成轮询结果判断,提升支付状态反馈体验
- 添加购物车添加操作中状态判断,防止重复操作并提供提示
- 优化景点预约日期处理,增加日期解析及格式化函数,提升日期校验准确性
- 调整购物车移除商品提示,避免重复日志打印
- 替换部分样式颜色为主题色变量,统一界面风格
- 清理manifest.json多余空行,保持文件整洁
- 调整开发环境配置注释,启用测试代理地址
zhangtao 6 روز پیش
والد
کامیت
6a39879bd4

+ 2 - 2
src/config/index.ts

@@ -11,10 +11,10 @@ const mapEnvVersion = {
   // develop: 'http://192.168.0.11:8081', // 王
   // develop: 'http://192.168.1.21:8080', // 田
   // develop: 'http://74949mkfh190.vicp.fun', // 付
-  // develop: 'http://47.109.84.152:8081', // 测试代理
+  develop: 'http://47.109.84.152:8081', // 测试代理
   // develop: 'https://5ed0f7cc.r9.vip.cpolar.cn',
   // develop: 'https://25740642.r3.cpolar.top',
-  develop: 'https://smqjh.api.zswlgz.com',
+  // develop: 'https://smqjh.api.zswlgz.com',
   /**
    * 体验版
    */

+ 14 - 5
src/store/cart.ts

@@ -46,13 +46,18 @@ export const useSmqjhCartStore = defineStore('smqjh-cart', {
      */
     async addCart(skuId: number, num: number, shopId: number, businessType: string) {
       return new Promise((resolve, reject) => {
+        if (this.isAddingCart) {
+          useGlobalToast().show({ msg: '操作中,请稍后' })
+          reject(new Error('操作中,请稍后'))
+          return
+        }
         if (!skuId) {
           useGlobalToast().show({ msg: '请选择商品规格' })
           return reject(new Error('请选择商品规格'))
         }
+        this.isAddingCart = true
         const { userInfo } = storeToRefs(useUserStore())
         useUserStore().checkLogin().then(() => {
-          this.isAddingCart = true
           Apis.common.addShoppingCart({
             data: {
               businessType,
@@ -64,10 +69,14 @@ export const useSmqjhCartStore = defineStore('smqjh-cart', {
           }).then((res) => {
             this.getCartList('XSB')
             resolve(res)
-          }).finally(() => {
+          }).catch((err) => {
             this.isAddingCart = false
+            reject(err)
           })
-        }).catch(err => reject(err))
+        }).catch((err) => {
+          this.isAddingCart = false
+          reject(err)
+        })
       })
     },
     /**
@@ -92,6 +101,7 @@ export const useSmqjhCartStore = defineStore('smqjh-cart', {
       })
       this.isCartAllChecked = false
       this.totalProduct = null
+      this.isAddingCart = false
       useTabbar().setTabbarItem('smqjh-cart', this.getTotalNum)
     },
     /**
@@ -193,11 +203,10 @@ export const useSmqjhCartStore = defineStore('smqjh-cart', {
         useGlobalToast().show({ msg: '移除商品中,请稍后' })
         return
       }
-      console.log(item.num, '===============================')
-
       if (item.num === 1) {
         useGlobalMessage().confirm({
           msg: '是否删除该商品?',
+          closeOnClickModal: false,
           success: async ({ action }) => {
             if (action === 'confirm') {
               await this.addCart(item.skuId, -1, item.shopId, 'XSB')

+ 39 - 0
src/store/user.ts

@@ -173,6 +173,45 @@ export const useUserStore = defineStore('user', {
           reject(err)
         })
       })
+    },
+    /**
+     * H5支付后轮询通用订单支付状态
+     * @param orderNumber 订单号
+     * @param interval 轮询间隔,默认3秒
+     * @param timeout 超时时间,默认90秒
+     */
+    async pollOrderPaySuccess(orderNumber: string, interval = 3000, timeout = 90000): Promise<boolean> {
+      if (!orderNumber) {
+        useGlobalToast().show({ msg: '订单号为空!请联系管理员' })
+        return false
+      }
+
+      const startedAt = Date.now()
+      const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
+
+      while (Date.now() - startedAt <= timeout) {
+        try {
+          const res = await Apis.xsb.orderInfo({
+            data: {
+              orderNo: orderNumber,
+            },
+          })
+          const orderInfo = res.data
+          if (orderInfo?.isPayed === 1 || (orderInfo?.hbOrderStatus !== undefined && orderInfo.hbOrderStatus !== OrderStatus.PaddingPay)) {
+            return true
+          }
+        }
+        catch (error) {
+          console.error('轮询订单支付状态失败', error)
+        }
+
+        if (Date.now() - startedAt + interval > timeout)
+          break
+
+        await sleep(interval)
+      }
+
+      return false
     }, /**
         *
         * @param freightFee

+ 9 - 1
src/subPack-attractions/attractionsDetail/attractionsDetail.vue

@@ -95,12 +95,20 @@ function handleOrder() {
   router.push({ name: 'attractions-tabbar', params: { tabbar: '1' } })
 }
 
+function formatDateParam(dateValue: Date) {
+  const year = dateValue.getFullYear()
+  const month = String(dateValue.getMonth() + 1).padStart(2, '0')
+  const day = String(dateValue.getDate()).padStart(2, '0')
+  return `${year}-${month}-${day}`
+}
+
 function handleBook() {
+  const travelDate = selectedDate.value instanceof Date ? selectedDate.value : new Date()
   router.push({
     name: 'attractions-reservation-info',
     params: {
       productNo: productNo.value,
-      selectDate: selectedDate.value || new Date(),
+      selectDate: formatDateParam(travelDate),
       productName: attractionDetail.value?.productName || '',
       isSingle: String(attractionDetail.value?.isSingle),
       price: String(attractionDetail.value?.salePrice),

+ 11 - 0
src/subPack-attractions/attractionsOrderPay/attractionsOrderPay.vue

@@ -28,6 +28,16 @@ async function getPayPreview() {
   payPreviewInfo.value = res.data
 }
 
+async function handleH5PayResult(orderNumber: string) {
+  const isPaySuccess = await useUserStore().pollOrderPaySuccess(orderNumber)
+  if (isPaySuccess) {
+    router.replace({ name: 'attractions-tabbar', params: { tabbar: '1' } })
+    return
+  }
+
+  useGlobalToast().show({ msg: '暂未查询到支付成功,请稍后在订单列表查看' })
+}
+
 async function submitPay() {
   const payMent = await useUserStore().getPayMent(orderNo.value)
   if (payMent.payType !== 'point') {
@@ -39,6 +49,7 @@ async function submitPay() {
       // #endif
       // #ifdef H5
       useUserStore().handleCommonWechatPay(orderNo.value)
+      await handleH5PayResult(orderNo.value)
       // #endif
     }
     catch {

+ 64 - 14
src/subPack-attractions/attractionsReservation/attractionsReservation.vue

@@ -69,24 +69,69 @@ watch(quantity, (newVal) => {
 const orderPopup = ref(false)
 const orderMemo = ref('')
 
+interface ParsedDate {
+  year: number
+  month: string
+  day: string
+}
+
+function formatDateParts(date: Date): ParsedDate | null {
+  if (Number.isNaN(date.getTime()))
+    return null
+
+  return {
+    year: date.getFullYear(),
+    month: String(date.getMonth() + 1).padStart(2, '0'),
+    day: String(date.getDate()).padStart(2, '0'),
+  }
+}
+
+function parseSelectDate(value: string): ParsedDate | null {
+  const dateText = value.trim()
+  if (!dateText)
+    return null
+
+  const dateMatch = dateText.match(/^(\d{4})[-/](\d{1,2})[-/](\d{1,2})/)
+  if (dateMatch) {
+    const year = Number(dateMatch[1])
+    const month = Number(dateMatch[2])
+    const day = Number(dateMatch[3])
+    const parsedDate = new Date(year, month - 1, day)
+
+    if (
+      parsedDate.getFullYear() !== year
+      || parsedDate.getMonth() !== month - 1
+      || parsedDate.getDate() !== day
+    ) {
+      return null
+    }
+
+    return {
+      year,
+      month: String(month).padStart(2, '0'),
+      day: String(day).padStart(2, '0'),
+    }
+  }
+
+  return formatDateParts(new Date(dateText))
+}
+
+const parsedSelectDate = computed(() => parseSelectDate(selectDate.value))
+
 const formattedDate = computed(() => {
-  if (!selectDate.value)
+  if (!parsedSelectDate.value)
     return ''
-  const date = new Date(selectDate.value)
-  const year = date.getFullYear()
-  const month = String(date.getMonth() + 1).padStart(2, '0')
-  const day = String(date.getDate()).padStart(2, '0')
+
+  const { year, month, day } = parsedSelectDate.value
   return `${year}年${month}月${day}日`
 })
 
 // 接口需要的日期格式 yyyy-MM-dd
 const travelDate = computed(() => {
-  if (!selectDate.value)
+  if (!parsedSelectDate.value)
     return ''
-  const date = new Date(selectDate.value)
-  const year = date.getFullYear()
-  const month = String(date.getMonth() + 1).padStart(2, '0')
-  const day = String(date.getDate()).padStart(2, '0')
+
+  const { year, month, day } = parsedSelectDate.value
   return `${year}-${month}-${day}`
 })
 
@@ -104,6 +149,11 @@ function handlePeopleCreditConfirm(index: number, { value: selectedValue }: { va
 
 // 表单校验
 function validateForm(): boolean {
+  if (!travelDate.value) {
+    useGlobalToast().show({ msg: '请选择游玩日期' })
+    return false
+  }
+
   // 联系人姓名校验
   if (!linkMan.value.trim()) {
     useGlobalToast().show({ msg: '请输入联系人姓名' })
@@ -203,7 +253,7 @@ async function handleSubmit() {
         <view class="flex items-center gap-24rpx">
           <view
             class="h-36rpx w-36rpx rounded-50% text-center text-28rpx font-600 line-height-[36rpx]"
-            :class="quantity > 1 ? 'bg-#E8FFA7 text-#9ED605' : 'bg-#F0F0F0 text-#AAAAAA'"
+            :class="quantity > 1 ? 'bg-[var(--them-color)] text-white' : 'bg-#F0F0F0 text-#AAAAAA'"
             @click="handleMinus"
           >
             -
@@ -212,7 +262,7 @@ async function handleSubmit() {
             {{ quantity }}
           </view>
           <view
-            class="h-36rpx w-36rpx rounded-50% bg-#E8FFA7 text-center text-28rpx text-#9ED605 font-600 line-height-[36rpx]"
+            class="h-36rpx w-36rpx rounded-50% bg-[var(--them-color)] text-center text-28rpx text-white font-600 line-height-[36rpx]"
             @click="handlePlus"
           >
             +
@@ -263,7 +313,7 @@ async function handleSubmit() {
         </view>
         <view class="h-2rpx w-full bg-#F0F0F0" />
         <view>
-          <wd-picker v-model="people.linkCreditType" required :columns="columns" label="证件类型" @confirm="(e: { value: number }) => handlePeopleCreditConfirm(index, e)" />
+          <wd-picker v-model="people.linkCreditType" required :columns="columns" label="证件类型" @confirm="e => handlePeopleCreditConfirm(index, e)" />
         </view>
         <view class="h-2rpx w-full bg-#F0F0F0" />
         <view>
@@ -306,7 +356,7 @@ async function handleSubmit() {
           <wd-icon name="arrow-up" size="18px" />
         </view>
         <view
-          class="h-80rpx w-180rpx rounded-40rpx bg-#9ED605 text-center text-28rpx text-#FFF font-bold line-height-[80rpx]"
+          class="h-80rpx w-180rpx rounded-40rpx bg-[var(--them-color)] text-center text-28rpx text-#FFF font-bold line-height-[80rpx]"
           @click="handleSubmit"
         >
           提交

+ 11 - 0
src/subPack-charge/chargeVoucher/chargeVoucher.vue

@@ -48,6 +48,16 @@ function getSelectedLevel(): Api.RechargeLevel | undefined {
   return rechargeLevels.value[selectIndex.value]
 }
 
+async function handleH5PayResult(orderNumber: string) {
+  const isPaySuccess = await useUserStore().pollOrderPaySuccess(orderNumber)
+  if (isPaySuccess) {
+    router.replace({ name: 'charge-buy-a-ticket-list' })
+    return
+  }
+
+  useGlobalToast().show({ msg: '暂未查询到支付成功,请稍后在订单列表查看' })
+}
+
 /**
  * 提交支付
  */
@@ -87,6 +97,7 @@ async function submitPay() {
     // #ifdef H5
     uni.hideLoading()
     useUserStore().handleCommonWechatPay(orderNumber, 'wx')
+    await handleH5PayResult(orderNumber)
     // #endif
   }
   catch (error) {

+ 15 - 0
src/subPack-djk/confirmOrder/index.vue

@@ -1,4 +1,6 @@
 <script setup lang="ts">
+import router from '@/router'
+
 definePage({
   name: 'djk-confirmOrder',
   islogin: true,
@@ -28,6 +30,18 @@ async function getGoodsDetaile() {
   orderInfo.value = res.data
   getCore()
 }
+async function handleH5PayResult(orderNumber: string) {
+  const isPaySuccess = await useUserStore().pollOrderPaySuccess(orderNumber)
+  if (isPaySuccess) {
+    isPay.value = false
+    router.replace({ name: 'djk-homeTabbar', params: { pay: '1' } })
+    return
+  }
+
+  useGlobalToast().show({ msg: '暂未查询到支付成功,请稍后在订单列表查看' })
+  isPay.value = false
+}
+
 async function handlePay() {
   if (!orderInfo.value) {
     useGlobalToast().show({ msg: '网络异常!请联系客服' })
@@ -46,6 +60,7 @@ async function handlePay() {
         // #endif
         // #ifdef H5
         useUserStore().handleCommonWechatPay(data.data)
+        await handleH5PayResult(data.data)
         // #endif
         isPay.value = false
       }

+ 17 - 3
src/subPack-xsb/confirmOrder/index.vue

@@ -217,6 +217,18 @@ function confirmCoupon() {
   couponPopup.value = false
 }
 
+async function handleH5PayResult(orderNumber: string): Promise<boolean> {
+  const isPaySuccess = await useUserStore().pollOrderPaySuccess(orderNumber)
+  if (isPaySuccess) {
+    router.replace({ name: 'xsb-order' })
+    return true
+  }
+
+  useGlobalToast().show({ msg: '暂未查询到支付成功,请稍后在订单列表查看' })
+  isPay.value = false
+  return false
+}
+
 async function handlePay() {
   if (!isSelfPickup.value && !selectedAddress.value) {
     useGlobalToast().show({ msg: '请选择收货地址' })
@@ -276,19 +288,21 @@ async function handlePay() {
         : undefined,
     )
     const payMent = await useUserStore().getPayMent(orderNumber)
+    const skuList = orderInfo.value?.skuList || []
+    await useUserStore().clearCart(skuList)
+    totalProduct.value = null
+    isPay.value = false
     if (payMent.payType !== 'point') {
       try {
         // #ifdef H5
         useUserStore().handleCommonWechatPay(orderNumber)
+        await handleH5PayResult(orderNumber)
         // #endif
         // #ifdef MP-WEIXIN
         const res = await useUserStore().handleCommonPayMent(orderNumber)
         await useUserStore().getWxCommonPayment(res)
         await useUserStore().paySuccess('xsb-order', 'subPack-xsb/commonTab/index')
         // #endif
-        await useUserStore().clearCart(orderInfo.value.skuList)
-        totalProduct.value = null
-        isPay.value = false
       }
       catch {
         await useUserStore().payError('xsb-order', 'subPack-xsb/commonTab/index')