feat(支付): 重构微信支付逻辑并添加支付倒计时功能
将微信支付逻辑提取到独立工具函数中,实现支付失败后可重试 在预约卡片组件中添加支付倒计时显示和重新支付功能 优化支付失败后的用户提示和交互流程
Showing
4 changed files
with
235 additions
and
35 deletions
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2024-01-24 16:38:13 | 2 | * @Date: 2024-01-24 16:38:13 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2026-01-13 15:04:21 | 4 | + * @LastEditTime: 2026-01-16 19:54:03 |
| 5 | * @FilePath: /xyxBooking-weapp/src/components/reserveCard.vue | 5 | * @FilePath: /xyxBooking-weapp/src/components/reserveCard.vue |
| 6 | * @Description: 预约记录卡组件 | 6 | * @Description: 预约记录卡组件 |
| 7 | --> | 7 | --> |
| ... | @@ -22,16 +22,20 @@ | ... | @@ -22,16 +22,20 @@ |
| 22 | <view class="booking-price">支付金额:<text>¥ {{ reserve_info.total_amt }}</text></view> | 22 | <view class="booking-price">支付金额:<text>¥ {{ reserve_info.total_amt }}</text></view> |
| 23 | <view class="booking-time">下单时间:<text>{{ reserve_info.order_time }}</text></view> | 23 | <view class="booking-time">下单时间:<text>{{ reserve_info.order_time }}</text></view> |
| 24 | </view> | 24 | </view> |
| 25 | - <view class="booking-list-item-footer"> | 25 | + <view v-if="is_pay_pending" class="booking-list-item-footer" @tap.stop> |
| 26 | - <!-- 倒计时逻辑省略,如果需要可添加 --> | 26 | + <view v-if="countdown_seconds > 0" class="countdown">剩余支付时间:{{ countdown_text }}</view> |
| 27 | + <view v-else class="countdown timeout">支付已超时</view> | ||
| 28 | + <view v-if="countdown_seconds > 0" class="repay-btn" @tap.stop="onRepay">重新支付</view> | ||
| 27 | </view> | 29 | </view> |
| 28 | </view> | 30 | </view> |
| 29 | </template> | 31 | </template> |
| 30 | 32 | ||
| 31 | <script setup> | 33 | <script setup> |
| 32 | -import { computed } from 'vue' | 34 | +import { computed, ref, watch, onUnmounted } from 'vue' |
| 35 | +import Taro from '@tarojs/taro' | ||
| 33 | import { IconFont } from '@nutui/icons-vue-taro' | 36 | import { IconFont } from '@nutui/icons-vue-taro' |
| 34 | import { useGo } from '@/hooks/useGo' | 37 | import { useGo } from '@/hooks/useGo' |
| 38 | +import { wechat_pay } from '@/utils/wechatPay' | ||
| 35 | 39 | ||
| 36 | const go = useGo(); | 40 | const go = useGo(); |
| 37 | 41 | ||
| ... | @@ -64,6 +68,116 @@ const CodeStatus = { | ... | @@ -64,6 +68,116 @@ const CodeStatus = { |
| 64 | REFUNDING: '11' | 68 | REFUNDING: '11' |
| 65 | } | 69 | } |
| 66 | 70 | ||
| 71 | +/** | ||
| 72 | + * 是否支付待处理状态 | ||
| 73 | + */ | ||
| 74 | + | ||
| 75 | +const is_pay_pending = computed(() => { | ||
| 76 | + if (is_offline.value) return false | ||
| 77 | + return reserve_info.value?.status === CodeStatus.APPLY && !!reserve_info.value?.pay_id | ||
| 78 | +}) | ||
| 79 | + | ||
| 80 | +const countdown_seconds = ref(0) | ||
| 81 | + | ||
| 82 | +const format_two_digits = (n) => { | ||
| 83 | + const num = Number(n) || 0 | ||
| 84 | + return num < 10 ? `0${num}` : String(num) | ||
| 85 | +} | ||
| 86 | + | ||
| 87 | +const countdown_text = computed(() => { | ||
| 88 | + const seconds = Number(countdown_seconds.value) || 0 | ||
| 89 | + const minutes = Math.floor(seconds / 60) | ||
| 90 | + const remain = seconds % 60 | ||
| 91 | + return `${format_two_digits(minutes)}:${format_two_digits(remain)}` | ||
| 92 | +}) | ||
| 93 | + | ||
| 94 | +const parse_created_time_ms = (created_time) => { | ||
| 95 | + const raw = String(created_time || '') | ||
| 96 | + if (!raw) return 0 | ||
| 97 | + const fixed = raw.replace(/-/g, '/') | ||
| 98 | + const date = new Date(fixed) | ||
| 99 | + const time = date.getTime() | ||
| 100 | + return Number.isFinite(time) ? time : 0 | ||
| 101 | +} | ||
| 102 | + | ||
| 103 | +let countdown_timer = null | ||
| 104 | +/** | ||
| 105 | + * 停止倒计时 | ||
| 106 | + */ | ||
| 107 | + | ||
| 108 | +const stop_countdown = () => { | ||
| 109 | + if (countdown_timer) { | ||
| 110 | + clearInterval(countdown_timer) | ||
| 111 | + countdown_timer = null | ||
| 112 | + } | ||
| 113 | +} | ||
| 114 | + | ||
| 115 | +const update_countdown = () => { | ||
| 116 | + const start_ms = parse_created_time_ms(reserve_info.value?.created_time) | ||
| 117 | + if (!start_ms) { | ||
| 118 | + countdown_seconds.value = 0 | ||
| 119 | + stop_countdown() | ||
| 120 | + return | ||
| 121 | + } | ||
| 122 | + | ||
| 123 | + const end_ms = start_ms + 10 * 60 * 1000 | ||
| 124 | + const diff_ms = end_ms - Date.now() | ||
| 125 | + const seconds = Math.max(0, Math.floor(diff_ms / 1000)) | ||
| 126 | + countdown_seconds.value = seconds | ||
| 127 | + | ||
| 128 | + if (seconds <= 0) { | ||
| 129 | + stop_countdown() | ||
| 130 | + } | ||
| 131 | +} | ||
| 132 | + | ||
| 133 | +const start_countdown = () => { | ||
| 134 | + stop_countdown() | ||
| 135 | + update_countdown() | ||
| 136 | + if (countdown_seconds.value <= 0) return | ||
| 137 | + countdown_timer = setInterval(update_countdown, 1000) | ||
| 138 | +} | ||
| 139 | + | ||
| 140 | +let is_showing_pay_modal = false | ||
| 141 | +const show_pay_modal = async (content) => { | ||
| 142 | + if (is_showing_pay_modal) return false | ||
| 143 | + is_showing_pay_modal = true | ||
| 144 | + try { | ||
| 145 | + const res = await Taro.showModal({ | ||
| 146 | + title: '提示', | ||
| 147 | + content: content || '支付未完成', | ||
| 148 | + showCancel: true, | ||
| 149 | + cancelText: '取消', | ||
| 150 | + confirmText: '继续支付', | ||
| 151 | + }) | ||
| 152 | + return !!res?.confirm | ||
| 153 | + } finally { | ||
| 154 | + is_showing_pay_modal = false | ||
| 155 | + } | ||
| 156 | +} | ||
| 157 | + | ||
| 158 | +/** | ||
| 159 | + * 重新支付 | ||
| 160 | + */ | ||
| 161 | + | ||
| 162 | +const onRepay = async () => { | ||
| 163 | + if (!is_pay_pending.value) return | ||
| 164 | + if (countdown_seconds.value <= 0) { | ||
| 165 | + Taro.showToast({ title: '支付已超时', icon: 'none' }) | ||
| 166 | + return | ||
| 167 | + } | ||
| 168 | + | ||
| 169 | + let should_continue = true | ||
| 170 | + while (should_continue) { | ||
| 171 | + const pay_id = reserve_info.value?.pay_id | ||
| 172 | + const pay_res = await wechat_pay({ pay_id }) | ||
| 173 | + if (pay_res && pay_res.code == 1) { | ||
| 174 | + go('/success', { pay_id }) | ||
| 175 | + return | ||
| 176 | + } | ||
| 177 | + should_continue = await show_pay_modal(pay_res?.msg || '支付未完成') | ||
| 178 | + } | ||
| 179 | +} | ||
| 180 | + | ||
| 67 | const formatStatus = (status) => { | 181 | const formatStatus = (status) => { |
| 68 | switch (status) { | 182 | switch (status) { |
| 69 | case CodeStatus.APPLY: | 183 | case CodeStatus.APPLY: |
| ... | @@ -116,6 +230,23 @@ const goToDetail = (item) => { | ... | @@ -116,6 +230,23 @@ const goToDetail = (item) => { |
| 116 | go(props.detail_path, { pay_id: item.pay_id }); | 230 | go(props.detail_path, { pay_id: item.pay_id }); |
| 117 | } | 231 | } |
| 118 | } | 232 | } |
| 233 | + | ||
| 234 | +/** | ||
| 235 | + * 监听支付待处理状态变化 | ||
| 236 | + */ | ||
| 237 | + | ||
| 238 | +watch(is_pay_pending, (val) => { | ||
| 239 | + if (val) { | ||
| 240 | + start_countdown() | ||
| 241 | + } else { | ||
| 242 | + countdown_seconds.value = 0 | ||
| 243 | + stop_countdown() | ||
| 244 | + } | ||
| 245 | +}, { immediate: true }) | ||
| 246 | + | ||
| 247 | +onUnmounted(() => { | ||
| 248 | + stop_countdown() | ||
| 249 | +}) | ||
| 119 | </script> | 250 | </script> |
| 120 | 251 | ||
| 121 | <style lang="less"> | 252 | <style lang="less"> |
| ... | @@ -187,5 +318,29 @@ const goToDetail = (item) => { | ... | @@ -187,5 +318,29 @@ const goToDetail = (item) => { |
| 187 | } | 318 | } |
| 188 | } | 319 | } |
| 189 | } | 320 | } |
| 321 | + | ||
| 322 | + .booking-list-item-footer { | ||
| 323 | + display: flex; | ||
| 324 | + justify-content: space-between; | ||
| 325 | + align-items: center; | ||
| 326 | + margin-top: 16rpx; | ||
| 327 | + | ||
| 328 | + .countdown { | ||
| 329 | + color: #A67939; | ||
| 330 | + font-size: 28rpx; | ||
| 331 | + | ||
| 332 | + &.timeout { | ||
| 333 | + color: #999; | ||
| 334 | + } | ||
| 335 | + } | ||
| 336 | + | ||
| 337 | + .repay-btn { | ||
| 338 | + padding: 8rpx 20rpx; | ||
| 339 | + border-radius: 12rpx; | ||
| 340 | + background-color: #A67939; | ||
| 341 | + color: #FFF; | ||
| 342 | + font-size: 28rpx; | ||
| 343 | + } | ||
| 344 | + } | ||
| 190 | } | 345 | } |
| 191 | </style> | 346 | </style> | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2024-01-16 11:37:10 | 2 | * @Date: 2024-01-16 11:37:10 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2026-01-13 11:43:13 | 4 | + * @LastEditTime: 2026-01-16 19:41:35 |
| 5 | * @FilePath: /xyxBooking-weapp/src/pages/bookingList/index.vue | 5 | * @FilePath: /xyxBooking-weapp/src/pages/bookingList/index.vue |
| 6 | * @Description: 预约记录列表页 | 6 | * @Description: 预约记录列表页 |
| 7 | --> | 7 | --> | ... | ... |
| 1 | <!-- | 1 | <!-- |
| 2 | * @Date: 2024-01-15 16:25:51 | 2 | * @Date: 2024-01-15 16:25:51 |
| 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com | 3 | * @LastEditors: hookehuyr hookehuyr@gmail.com |
| 4 | - * @LastEditTime: 2026-01-16 18:05:18 | 4 | + * @LastEditTime: 2026-01-16 19:47:01 |
| 5 | * @FilePath: /xyxBooking-weapp/src/pages/submit/index.vue | 5 | * @FilePath: /xyxBooking-weapp/src/pages/submit/index.vue |
| 6 | * @Description: 预约人员信息 | 6 | * @Description: 预约人员信息 |
| 7 | --> | 7 | --> |
| ... | @@ -59,13 +59,15 @@ | ... | @@ -59,13 +59,15 @@ |
| 59 | import { ref, computed } from 'vue' | 59 | import { ref, computed } from 'vue' |
| 60 | import Taro, { useDidShow, useRouter as useTaroRouter } from '@tarojs/taro' | 60 | import Taro, { useDidShow, useRouter as useTaroRouter } from '@tarojs/taro' |
| 61 | import { IconFont } from '@nutui/icons-vue-taro' | 61 | import { IconFont } from '@nutui/icons-vue-taro' |
| 62 | -import { useGo } from '@/hooks/useGo' | 62 | +import { useGo, useReplace } from '@/hooks/useGo' |
| 63 | import icon_check1 from '@/assets/images/多选01@2x.png' | 63 | import icon_check1 from '@/assets/images/多选01@2x.png' |
| 64 | import icon_check2 from '@/assets/images/多选02@2x.png' | 64 | import icon_check2 from '@/assets/images/多选02@2x.png' |
| 65 | -import { personListAPI, addReserveAPI, wxPayAPI } from '@/api/index' | 65 | +import { personListAPI, addReserveAPI } from '@/api/index' |
| 66 | +import { wechat_pay } from '@/utils/wechatPay' | ||
| 66 | 67 | ||
| 67 | const router = useTaroRouter(); | 68 | const router = useTaroRouter(); |
| 68 | const go = useGo(); | 69 | const go = useGo(); |
| 70 | +const replace = useReplace(); | ||
| 69 | 71 | ||
| 70 | const visitorList = ref([]); | 72 | const visitorList = ref([]); |
| 71 | const date = ref(''); | 73 | const date = ref(''); |
| ... | @@ -154,12 +156,14 @@ const showPayErrorModal = async (content) => { | ... | @@ -154,12 +156,14 @@ const showPayErrorModal = async (content) => { |
| 154 | if (is_showing_pay_modal) return; | 156 | if (is_showing_pay_modal) return; |
| 155 | is_showing_pay_modal = true; | 157 | is_showing_pay_modal = true; |
| 156 | try { | 158 | try { |
| 157 | - await Taro.showModal({ | 159 | + const res = await Taro.showModal({ |
| 158 | title: '提示', | 160 | title: '提示', |
| 159 | content: content || '支付失败,请稍后再试', | 161 | content: content || '支付失败,请稍后再试', |
| 160 | - showCancel: false, | 162 | + showCancel: true, |
| 161 | - confirmText: '我知道了', | 163 | + cancelText: '离开', |
| 164 | + confirmText: '继续支付', | ||
| 162 | }); | 165 | }); |
| 166 | + return !!res?.confirm; | ||
| 163 | } finally { | 167 | } finally { |
| 164 | is_showing_pay_modal = false; | 168 | is_showing_pay_modal = false; |
| 165 | } | 169 | } |
| ... | @@ -204,36 +208,23 @@ const submitBtn = async () => { | ... | @@ -204,36 +208,23 @@ const submitBtn = async () => { |
| 204 | 208 | ||
| 205 | // 以接口返回的 need_pay 为准:1=需要支付,0=不需要支付 | 209 | // 以接口返回的 need_pay 为准:1=需要支付,0=不需要支付 |
| 206 | if (Number(need_pay) === 1 || need_pay === true) { | 210 | if (Number(need_pay) === 1 || need_pay === true) { |
| 207 | - Taro.showLoading({ title: '支付准备中...' }); | 211 | + let should_continue = true; |
| 208 | - let payParams = null; | 212 | + // 循环支付直到支付成功或用户取消支付 |
| 209 | - try { | 213 | + while (should_continue) { |
| 210 | - payParams = await wxPayAPI({ pay_id }); // 参数接口 | 214 | + const pay_res = await wechat_pay({ pay_id }) |
| 211 | - } finally { | 215 | + if (pay_res && pay_res.code == 1) { |
| 212 | - Taro.hideLoading(); | ||
| 213 | - } | ||
| 214 | - | ||
| 215 | - if (payParams && payParams.code == 1) { | ||
| 216 | - let pay_params = payParams.data; | ||
| 217 | - Taro.requestPayment({ | ||
| 218 | - timeStamp: pay_params.timeStamp, | ||
| 219 | - nonceStr: pay_params.nonceStr, | ||
| 220 | - package: pay_params.package, | ||
| 221 | - signType: pay_params.signType, | ||
| 222 | - paySign: pay_params.paySign, | ||
| 223 | - success(res) { | ||
| 224 | pending_pay_id.value = null; | 216 | pending_pay_id.value = null; |
| 225 | pending_need_pay.value = null; | 217 | pending_need_pay.value = null; |
| 226 | go('/success', { pay_id }); | 218 | go('/success', { pay_id }); |
| 227 | - }, | 219 | + return |
| 228 | - fail(res) { | ||
| 229 | - refreshVisitorList({ reset_checked: true }).catch(() => {}); | ||
| 230 | - showPayErrorModal('支付未完成,可再次点击提交订单继续支付').catch(() => {}); | ||
| 231 | } | 220 | } |
| 232 | - }) | 221 | + // 刷新参观者列表, 清除已预约标记 |
| 233 | - } else { | ||
| 234 | refreshVisitorList({ reset_checked: true }).catch(() => {}); | 222 | refreshVisitorList({ reset_checked: true }).catch(() => {}); |
| 235 | - showPayErrorModal(payParams?.msg || '获取支付信息失败,请稍后再试').catch(() => {}); | 223 | + should_continue = await showPayErrorModal(pay_res?.msg || '支付未完成,可再次点击提交订单继续支付') |
| 236 | } | 224 | } |
| 225 | + | ||
| 226 | + replace('/bookingList') | ||
| 227 | + return | ||
| 237 | } else { | 228 | } else { |
| 238 | pending_pay_id.value = null; | 229 | pending_pay_id.value = null; |
| 239 | pending_need_pay.value = null; | 230 | pending_need_pay.value = null; | ... | ... |
src/utils/wechatPay.js
0 → 100644
| 1 | +/* | ||
| 2 | + * @Date: 2026-01-16 19:41:09 | ||
| 3 | + * @LastEditors: hookehuyr hookehuyr@gmail.com | ||
| 4 | + * @LastEditTime: 2026-01-16 19:56:36 | ||
| 5 | + * @FilePath: /xyxBooking-weapp/src/utils/wechatPay.js | ||
| 6 | + * @Description: 微信支付工具函数 | ||
| 7 | + */ | ||
| 8 | +import Taro from '@tarojs/taro' | ||
| 9 | +import { wxPayAPI } from '@/api/index' | ||
| 10 | + | ||
| 11 | +/** | ||
| 12 | + * @description 微信支付 | ||
| 13 | + * @param {*} pay_id 订单号 | ||
| 14 | + * @returns {*} 支付结果 | ||
| 15 | + */ | ||
| 16 | + | ||
| 17 | +export const wechat_pay = async ({ pay_id }) => { | ||
| 18 | + const normalized_pay_id = String(pay_id || '') | ||
| 19 | + if (!normalized_pay_id) { | ||
| 20 | + return { code: 0, data: null, msg: '缺少订单号' } | ||
| 21 | + } | ||
| 22 | + | ||
| 23 | + Taro.showLoading({ title: '支付准备中...' }) | ||
| 24 | + let pay_params_res = null | ||
| 25 | + try { | ||
| 26 | + pay_params_res = await wxPayAPI({ pay_id: normalized_pay_id }) | ||
| 27 | + } finally { | ||
| 28 | + Taro.hideLoading() | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + if (!pay_params_res || pay_params_res.code != 1) { | ||
| 32 | + return { code: 0, data: null, msg: pay_params_res?.msg || '获取支付信息失败,请稍后再试' } | ||
| 33 | + } | ||
| 34 | + | ||
| 35 | + const pay_params = pay_params_res?.data || {} | ||
| 36 | + | ||
| 37 | + const pay_result = await new Promise((resolve) => { | ||
| 38 | + Taro.requestPayment({ | ||
| 39 | + timeStamp: pay_params.timeStamp, | ||
| 40 | + nonceStr: pay_params.nonceStr, | ||
| 41 | + package: pay_params.package, | ||
| 42 | + signType: pay_params.signType, | ||
| 43 | + paySign: pay_params.paySign, | ||
| 44 | + success: (res) => resolve({ ok: true, res }), | ||
| 45 | + fail: (err) => resolve({ ok: false, err }), | ||
| 46 | + }) | ||
| 47 | + }) | ||
| 48 | + | ||
| 49 | + if (pay_result?.ok) { | ||
| 50 | + return { code: 1, data: pay_result.res || null, msg: '支付成功' } | ||
| 51 | + } | ||
| 52 | + | ||
| 53 | + return { code: 0, data: pay_result?.err || null, msg: pay_result?.err?.errMsg || '支付未完成' } | ||
| 54 | +} |
-
Please register or login to post a comment