From 7eeadec17dd3be9f76ba6f1aed5a38fcc991d8fd Mon Sep 17 00:00:00 2001 From: DU Date: Tue, 5 May 2026 14:05:57 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AF=B9=E6=8E=A5=20=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E7=BB=93=E6=9E=9C=E3=80=81=E5=85=B3=E9=97=AD?= =?UTF-8?q?=E6=94=AF=E4=BB=98=E8=AE=A2=E5=8D=95=E3=80=81=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E7=AC=AC=E4=B8=89=E6=96=B9=E8=AE=A2=E5=8D=95=20=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/order.ts | 56 ++++++++++++++++++++++++ src/pageScan/couponStart/index.tsx | 60 ++++++++++++------------- src/pageScan/renewFee/index.tsx | 56 +++++++++++------------- src/pageScan/selfStart/index.tsx | 59 ++++++++++++------------- src/pageScan/timedStart/index.tsx | 58 ++++++++++++------------ src/pageScan/turnOffRestart/index.tsx | 58 +++++++++++------------- src/types/order.ts | 63 +++++++++++++++++++++++++++ 7 files changed, 256 insertions(+), 154 deletions(-) create mode 100644 src/api/order.ts create mode 100644 src/types/order.ts diff --git a/src/api/order.ts b/src/api/order.ts new file mode 100644 index 0000000..5c7bc61 --- /dev/null +++ b/src/api/order.ts @@ -0,0 +1,56 @@ +/** + * 订单/支付相关 API + */ +import { get, post } from '../services/request' +import { PaymentResult, QueryPaymentRequest, CloseOrderDto } from '../types/order' + +/** + * 查询支付结果(通用,通过 outTradeNo 或 tradeNo 查询) + * @param dto 查询参数 + */ +export const queryPaymentResult = (dto: QueryPaymentRequest) => { + return post('/Order/query', dto) +} + +/** + * 关闭支付订单(用户取消或支付失败时调用,避免产生悬挂订单) + * @param outTradeNo 商户订单号 + */ +export const closeOrder = (outTradeNo: string) => { + return post('/Order/close', { outTradeNo } as CloseOrderDto) +} + +/** + * 查询第三方支付结果(通过消费记录ID查询,适用于开台/续费/关灯重开等场景) + * @param consumptionId 消费记录ID(来自 startDevice 响应) + */ +export const queryPaymentByConsumptionId = (consumptionId: number) => { + return get(`/Order/query/${consumptionId}`) +} + +/** + * 轮询支付结果,直到确认成功或超时 + * 判断依据:data.success === true 或 data.tradeState === 'SUCCESS' + * @param consumptionId 消费记录ID + * @param maxRetries 最大轮询次数(默认 5 次) + * @param intervalMs 每次间隔毫秒(默认 2000ms) + * @returns 是否支付成功 + */ +export const pollTablePaymentStatus = async ( + consumptionId: number, + maxRetries = 5, + intervalMs = 2000 +): Promise => { + for (let i = 0; i < maxRetries; i++) { + await new Promise(resolve => setTimeout(resolve, intervalMs)) + try { + const res = await queryPaymentByConsumptionId(consumptionId) + if (res.data?.success || res.data?.tradeState === 'SUCCESS') { + return true + } + } catch (e) { + console.warn(`查询支付状态第${i + 1}次异常:`, e) + } + } + return false +} diff --git a/src/pageScan/couponStart/index.tsx b/src/pageScan/couponStart/index.tsx index 9fbf5b7..37d05df 100644 --- a/src/pageScan/couponStart/index.tsx +++ b/src/pageScan/couponStart/index.tsx @@ -6,6 +6,7 @@ import Taro, { useLoad, useRouter } from '@tarojs/taro' import { useState } from 'react' import { observer } from 'mobx-react' import { couponStart, invokeWechatPay } from '../../api/startDevice' +import { closeOrder, pollTablePaymentStatus } from '../../api/order' import { userStore, tableStore } from '../../store' import './index.scss' @@ -173,53 +174,50 @@ const CouponStart = observer(() => { tableStore.setCurrentOperationId(result.data.consumptionId) } - // 判断是否需要支付(团购券一般不需要支付,但可能需要补差价) + const consumptionId = result.data.consumptionId + const outTradeNo = result.data.paymentResponseDto?.outTradeNo + + // 判断是否需要支付(团购券一般无需支付,但可能需要补差价) if (result.data.paymentResponseDto?.payParameters) { // 调起微信支付 try { await invokeWechatPay(result.data.paymentResponseDto.payParameters) - - // 支付成功 - Taro.showToast({ - title: '开台成功', - icon: 'success', - duration: 2000 - }) - // 跳转到关台页面 - setTimeout(() => { - Taro.redirectTo({ - url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${result.data.consumptionId}` + // 轮询服务端确认支付结果 + Taro.showLoading({ title: '确认支付中...', mask: true }) + const paid = await pollTablePaymentStatus(consumptionId) + Taro.hideLoading() + + if (paid) { + Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 }) + setTimeout(() => { + Taro.redirectTo({ + url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}` + }) + }, 2000) + } else { + Taro.showModal({ + title: '支付确认中', + content: '支付结果尚未到账,请稍后至"关台"页查看状态,如有疑问请联系店长。', + showCancel: false, + confirmText: '知道了', }) - }, 2000) + } } catch (payError: any) { console.log('支付错误:', payError) - // 用户取消支付或支付失败 + if (outTradeNo) closeOrder(outTradeNo).catch(() => {}) if (payError.errMsg?.includes('cancel')) { - Taro.showToast({ - title: '已取消支付', - icon: 'none', - duration: 2000 - }) + Taro.showToast({ title: '已取消支付', icon: 'none', duration: 2000 }) } else { - Taro.showToast({ - title: payError.errMsg || '支付失败', - icon: 'none', - duration: 2000 - }) + Taro.showToast({ title: payError.errMsg || '支付失败', icon: 'none', duration: 2000 }) } } } else { // 无需支付 - Taro.showToast({ - title: '兑换成功,开台成功', - icon: 'success', - duration: 2000 - }) - + Taro.showToast({ title: '兑换成功,开台成功', icon: 'success', duration: 2000 }) setTimeout(() => { Taro.redirectTo({ - url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${result.data.consumptionId}` + url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}` }) }, 2000) } diff --git a/src/pageScan/renewFee/index.tsx b/src/pageScan/renewFee/index.tsx index f52cc00..27cdff4 100644 --- a/src/pageScan/renewFee/index.tsx +++ b/src/pageScan/renewFee/index.tsx @@ -7,6 +7,7 @@ import { useState } from 'react' import { observer } from 'mobx-react' import { Stepper } from '@taroify/core' import { renewFee, invokeWechatPay, PayType } from '../../api/startDevice' +import { closeOrder, pollTablePaymentStatus } from '../../api/order' import { userStore, tableStore } from '../../store' import './index.scss' @@ -100,51 +101,44 @@ const RenewFee = observer(() => { console.log('续费接口返回:', result) + const consumptionId = result.data.consumptionId + const outTradeNo = result.data.paymentResponseDto?.outTradeNo + // 判断是否需要支付 if (result.data.paymentResponseDto?.payParameters) { // 调起微信支付 try { await invokeWechatPay(result.data.paymentResponseDto.payParameters) - - // 支付成功 - Taro.showToast({ - title: '续费成功', - icon: 'success', - duration: 2000 - }) - // 返回关台页面 - setTimeout(() => { - Taro.navigateBack() - }, 2000) + // 轮询服务端确认支付结果 + Taro.showLoading({ title: '确认支付中...', mask: true }) + const paid = await pollTablePaymentStatus(consumptionId) + Taro.hideLoading() + + if (paid) { + Taro.showToast({ title: '续费成功', icon: 'success', duration: 2000 }) + setTimeout(() => Taro.navigateBack(), 2000) + } else { + Taro.showModal({ + title: '支付确认中', + content: '支付结果尚未到账,请稍后刷新页面查看,如有疑问请联系店长。', + showCancel: false, + confirmText: '知道了', + }) + } } catch (payError: any) { console.log('支付错误:', payError) - // 用户取消支付或支付失败 + if (outTradeNo) closeOrder(outTradeNo).catch(() => {}) if (payError.errMsg?.includes('cancel')) { - Taro.showToast({ - title: '已取消支付', - icon: 'none', - duration: 2000 - }) + Taro.showToast({ title: '已取消支付', icon: 'none', duration: 2000 }) } else { - Taro.showToast({ - title: payError.errMsg || '支付失败', - icon: 'none', - duration: 2000 - }) + Taro.showToast({ title: payError.errMsg || '支付失败', icon: 'none', duration: 2000 }) } } } else { // 无需支付 - Taro.showToast({ - title: '续费成功', - icon: 'success', - duration: 2000 - }) - - setTimeout(() => { - Taro.navigateBack() - }, 2000) + Taro.showToast({ title: '续费成功', icon: 'success', duration: 2000 }) + setTimeout(() => Taro.navigateBack(), 2000) } } catch (error: any) { console.error('续费失败:', error) diff --git a/src/pageScan/selfStart/index.tsx b/src/pageScan/selfStart/index.tsx index 20eba3d..2b67207 100644 --- a/src/pageScan/selfStart/index.tsx +++ b/src/pageScan/selfStart/index.tsx @@ -7,6 +7,7 @@ import { useState } from 'react' import { observer } from 'mobx-react' import { Stepper } from '@taroify/core' import { depositStart, balanceStart, invokeWechatPay, PayType } from '../../api/startDevice' +import { closeOrder, pollTablePaymentStatus } from '../../api/order' import { userStore, tableStore } from '../../store' import './index.scss' @@ -142,53 +143,51 @@ const SelfStart = observer(() => { tableStore.setCurrentOperationId(result.data.consumptionId) } + const consumptionId = result.data.consumptionId + const outTradeNo = result.data.paymentResponseDto?.outTradeNo + // 判断是否需要支付 if (result.data.paymentResponseDto?.payParameters) { // 调起微信支付 try { await invokeWechatPay(result.data.paymentResponseDto.payParameters) - - // 支付成功 - Taro.showToast({ - title: '开台成功', - icon: 'success', - duration: 2000 - }) - // 跳转到关台页面 - setTimeout(() => { - Taro.redirectTo({ - url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${result.data.consumptionId}` + // 轮询服务端确认支付结果 + Taro.showLoading({ title: '确认支付中...', mask: true }) + const paid = await pollTablePaymentStatus(consumptionId) + Taro.hideLoading() + + if (paid) { + Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 }) + setTimeout(() => { + Taro.redirectTo({ + url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}` + }) + }, 2000) + } else { + Taro.showModal({ + title: '支付确认中', + content: '支付结果尚未到账,请稍后至"关台"页查看状态,如有疑问请联系店长。', + showCancel: false, + confirmText: '知道了', }) - }, 2000) + } } catch (payError: any) { console.log('支付错误:', payError) - // 用户取消支付或支付失败 + // 关闭服务端未支付订单,避免产生悬挂订单 + if (outTradeNo) closeOrder(outTradeNo).catch(() => {}) if (payError.errMsg?.includes('cancel')) { - Taro.showToast({ - title: '已取消支付', - icon: 'none', - duration: 2000 - }) + Taro.showToast({ title: '已取消支付', icon: 'none', duration: 2000 }) } else { - Taro.showToast({ - title: payError.errMsg || '支付失败', - icon: 'none', - duration: 2000 - }) + Taro.showToast({ title: payError.errMsg || '支付失败', icon: 'none', duration: 2000 }) } } } else { // 无需支付(余额支付等场景) - Taro.showToast({ - title: '开台成功', - icon: 'success', - duration: 2000 - }) - + Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 }) setTimeout(() => { Taro.redirectTo({ - url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${result.data.consumptionId}` + url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}` }) }, 2000) } diff --git a/src/pageScan/timedStart/index.tsx b/src/pageScan/timedStart/index.tsx index 1de4916..5b365a2 100644 --- a/src/pageScan/timedStart/index.tsx +++ b/src/pageScan/timedStart/index.tsx @@ -6,6 +6,7 @@ import Taro, { useLoad, useRouter } from '@tarojs/taro' import { useState } from 'react' import { observer } from 'mobx-react' import { timedStart, invokeWechatPay, PayType } from '../../api/startDevice' +import { closeOrder, pollTablePaymentStatus } from '../../api/order' import { userStore, tableStore } from '../../store' import './index.scss' @@ -118,53 +119,50 @@ const TimedStart = observer(() => { tableStore.setCurrentOperationId(result.data.consumptionId) } + const consumptionId = result.data.consumptionId + const outTradeNo = result.data.paymentResponseDto?.outTradeNo + // 判断是否需要支付 if (result.data.paymentResponseDto?.payParameters) { // 调起微信支付 try { await invokeWechatPay(result.data.paymentResponseDto.payParameters) - - // 支付成功 - Taro.showToast({ - title: '开台成功', - icon: 'success', - duration: 2000 - }) - // 跳转到关台页面 - setTimeout(() => { - Taro.redirectTo({ - url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${result.data.consumptionId}` + // 轮询服务端确认支付结果 + Taro.showLoading({ title: '确认支付中...', mask: true }) + const paid = await pollTablePaymentStatus(consumptionId) + Taro.hideLoading() + + if (paid) { + Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 }) + setTimeout(() => { + Taro.redirectTo({ + url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}` + }) + }, 2000) + } else { + Taro.showModal({ + title: '支付确认中', + content: '支付结果尚未到账,请稍后至"关台"页查看状态,如有疑问请联系店长。', + showCancel: false, + confirmText: '知道了', }) - }, 2000) + } } catch (payError: any) { console.log('支付错误:', payError) - // 用户取消支付或支付失败 + if (outTradeNo) closeOrder(outTradeNo).catch(() => {}) if (payError.errMsg?.includes('cancel')) { - Taro.showToast({ - title: '已取消支付', - icon: 'none', - duration: 2000 - }) + Taro.showToast({ title: '已取消支付', icon: 'none', duration: 2000 }) } else { - Taro.showToast({ - title: payError.errMsg || '支付失败', - icon: 'none', - duration: 2000 - }) + Taro.showToast({ title: payError.errMsg || '支付失败', icon: 'none', duration: 2000 }) } } } else { // 无需支付 - Taro.showToast({ - title: '开台成功', - icon: 'success', - duration: 2000 - }) - + Taro.showToast({ title: '开台成功', icon: 'success', duration: 2000 }) setTimeout(() => { Taro.redirectTo({ - url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${result.data.consumptionId}` + url: `/pageScan/closeTable/index?grpSn=${grpSn}&operationId=${consumptionId}` }) }, 2000) } diff --git a/src/pageScan/turnOffRestart/index.tsx b/src/pageScan/turnOffRestart/index.tsx index 4e60d0f..8d84ec4 100644 --- a/src/pageScan/turnOffRestart/index.tsx +++ b/src/pageScan/turnOffRestart/index.tsx @@ -6,6 +6,7 @@ import Taro, { useLoad, useRouter } from '@tarojs/taro' import { useState } from 'react' import { observer } from 'mobx-react' import { turnOffRestart, invokeWechatPay } from '../../api/startDevice' +import { closeOrder, pollTablePaymentStatus } from '../../api/order' import { userStore, tableStore } from '../../store' import './index.scss' @@ -86,51 +87,44 @@ const TurnOffRestart = observer(() => { console.log('关灯重开接口返回:', result) + const consumptionId = result.data.consumptionId + const outTradeNo = result.data.paymentResponseDto?.outTradeNo + // 判断是否需要支付 if (result.data.paymentResponseDto?.payParameters) { // 调起微信支付 try { await invokeWechatPay(result.data.paymentResponseDto.payParameters) - - // 支付成功 - Taro.showToast({ - title: '结算成功', - icon: 'success', - duration: 2000 - }) - - // 返回关台页面或跳转到首页 - setTimeout(() => { - Taro.navigateBack() - }, 2000) + + // 轮询服务端确认支付结果 + Taro.showLoading({ title: '确认支付中...', mask: true }) + const paid = await pollTablePaymentStatus(consumptionId) + Taro.hideLoading() + + if (paid) { + Taro.showToast({ title: '结算成功', icon: 'success', duration: 2000 }) + setTimeout(() => Taro.navigateBack(), 2000) + } else { + Taro.showModal({ + title: '支付确认中', + content: '支付结果尚未到账,请稍后刷新页面查看,如有疑问请联系店长。', + showCancel: false, + confirmText: '知道了', + }) + } } catch (payError: any) { console.log('支付错误:', payError) - // 用户取消支付或支付失败 + if (outTradeNo) closeOrder(outTradeNo).catch(() => {}) if (payError.errMsg?.includes('cancel')) { - Taro.showToast({ - title: '已取消支付', - icon: 'none', - duration: 2000 - }) + Taro.showToast({ title: '已取消支付', icon: 'none', duration: 2000 }) } else { - Taro.showToast({ - title: payError.errMsg || '支付失败', - icon: 'none', - duration: 2000 - }) + Taro.showToast({ title: payError.errMsg || '支付失败', icon: 'none', duration: 2000 }) } } } else { // 无需支付 - Taro.showToast({ - title: '结算成功', - icon: 'success', - duration: 2000 - }) - - setTimeout(() => { - Taro.navigateBack() - }, 2000) + Taro.showToast({ title: '结算成功', icon: 'success', duration: 2000 }) + setTimeout(() => Taro.navigateBack(), 2000) } } catch (error: any) { console.error('关灯重开失败:', error) diff --git a/src/types/order.ts b/src/types/order.ts new file mode 100644 index 0000000..550b861 --- /dev/null +++ b/src/types/order.ts @@ -0,0 +1,63 @@ +/** + * 订单/支付相关类型定义 + */ + +/** + * 支付渠道 + */ +export type PaymentChannel = 'WechatPay' | 'Alipay' | 'UnionPay' | 'Balance' | 'BankCard' + +/** + * 支付查询结果 + */ +export interface PaymentResult { + /** 是否成功 */ + success: boolean + /** 消息 */ + message: string + /** 商户订单号 */ + outTradeNo: string + /** 支付渠道订单号 */ + tradeNo: string + /** 支付金额 */ + amount: number + /** 支付时间 */ + payTime: string + /** 支付渠道 */ + channel: PaymentChannel + /** 前端调起支付所需参数 */ + payParameters: unknown + /** 支付URL(扫码支付等) */ + payUrl: string + /** H5支付URL */ + mwebUrl: string + /** 预支付ID(小程序/JSAPI支付) */ + prepayId: string + /** + * 交易状态 + * 微信标准值:SUCCESS-成功 / REFUND-退款 / NOTPAY-未支付 / CLOSED-关闭 / REVOKED-撤销 / USERPAYING-支付中 / PAYERROR-支付失败 + */ + tradeState: string + /** 原始响应数据 */ + rawResponse: unknown +} + +/** + * 查询支付结果请求参数(POST /Order/query) + */ +export interface QueryPaymentRequest { + /** 商户订单号 */ + outTradeNo?: string + /** 微信支付订单号 */ + tradeNo?: string + /** 支付渠道 */ + channel?: PaymentChannel +} + +/** + * 关闭支付订单请求参数(POST /Order/close) + */ +export interface CloseOrderDto { + /** 商户订单号 */ + outTradeNo: string +}