hookehuyr

feat(支付): 重构微信支付逻辑并添加支付倒计时功能

将微信支付逻辑提取到独立工具函数中,实现支付失败后可重试
在预约卡片组件中添加支付倒计时显示和重新支付功能
优化支付失败后的用户提示和交互流程
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(); 216 + pending_pay_id.value = null;
213 - } 217 + pending_need_pay.value = null;
214 - 218 + go('/success', { pay_id });
215 - if (payParams && payParams.code == 1) { 219 + return
216 - let pay_params = payParams.data; 220 + }
217 - Taro.requestPayment({ 221 + // 刷新参观者列表, 清除已预约标记
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;
225 - pending_need_pay.value = null;
226 - go('/success', { pay_id });
227 - },
228 - fail(res) {
229 - refreshVisitorList({ reset_checked: true }).catch(() => {});
230 - showPayErrorModal('支付未完成,可再次点击提交订单继续支付').catch(() => {});
231 - }
232 - })
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;
......
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 +}