| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668 |
- <template>
- <view class="payment-result-page">
- <!-- 支付状态图标 -->
- <view class="status-icon">
- <view v-if="paymentStatus === 'success'" class="icon success-icon">
- <Icon name="icon-chenggong" size="120" color="#07c160" />
- </view>
- <view v-else-if="paymentStatus === 'failed'" class="icon failed-icon">
- <Icon name="icon-shibai" size="120" color="#ff4d4f" />
- </view>
- <view v-else class="icon loading-icon">
- <u-loading-icon mode="circle" size="80" color="#1890ff"></u-loading-icon>
- </view>
- </view>
- <!-- 支付状态文字 -->
- <view class="status-text">
- <text v-if="paymentStatus === 'success'" class="success-text">购买成功</text>
- <text v-else-if="paymentStatus === 'failed'" class="failed-text">支付失败</text>
- <text v-else class="checking-text">正在查询支付结果...</text>
- </view>
- <!-- 服务包信息(支付成功时显示) -->
- <view class="service-info" v-if="paymentStatus === 'success' && serviceResult">
- <view class="service-header">
- <text class="service-name">{{ serviceResult.mc }}</text>
- <text class="service-deadline">有效期至:{{ formattedDeadline }}</text>
- </view>
- <!-- 服务项目列表 -->
- <view class="service-list" v-if="formattedServiceList.length > 0">
- <view class="service-list-title">包含服务项目</view>
- <view class="service-items">
- <view
- v-for="(item, index) in formattedServiceList"
- :key="index"
- class="service-item"
- >
- <view class="item-name">{{ item.displayName }}</view>
- <view class="item-details">
- <text v-if="item.sfmf === 1" class="item-tag free">免费</text>
- <text v-if="item.zdsc > 0" class="item-tag">{{ item.zdsc }}分钟</text>
- <text v-if="item.zdcs > 0" class="item-tag">{{ item.zdcs }}次</text>
- <text v-if="item.zdll > 0" class="item-tag">{{ item.zdll }}MB</text>
- </view>
- </view>
- </view>
- </view>
- </view>
- <!-- 订单信息(失败或查询中时显示) -->
- <view class="order-info" v-if="orderInfo && paymentStatus !== 'success'">
- <view class="info-item">
- <text class="label">订单号:</text>
- <text class="value">{{ orderInfo.orderId || orderId }}</text>
- </view>
- <view class="info-item" v-if="orderInfo.amount">
- <text class="label">支付金额:</text>
- <text class="value amount">¥{{ orderInfo.amount }}</text>
- </view>
- </view>
- <!-- 失败原因 -->
- <view class="error-message" v-if="paymentStatus === 'failed' && errorMessage">
- <text>{{ errorMessage }}</text>
- </view>
- <!-- 操作按钮 -->
- <view class="action-btns">
- <button
- v-if="paymentStatus === 'success'"
- class="btn primary-btn"
- @click="goToHome"
- >
- 完成
- </button>
- <button
- v-if="paymentStatus === 'failed'"
- class="btn primary-btn"
- @click="retryPayment"
- >
- 重新支付
- </button>
- <button
- v-if="paymentStatus === 'checking'"
- class="btn secondary-btn"
- @click="stopChecking"
- >
- 稍后查看
- </button>
- </view>
- <!-- 底部提示 -->
- <view class="bottom-tips">
- <text v-if="paymentStatus === 'success'">服务包已生效,可以开始使用了</text>
- <text v-else-if="paymentStatus === 'failed'">如有疑问,请联系客服</text>
- <text v-else>正在确认支付结果,请稍候...</text>
- </view>
- </view>
- </template>
- <script setup>
- import { ref, computed, onUnmounted } from 'vue'
- import { onLoad } from '@dcloudio/uni-app'
- import { grfwApi } from '@/api/grfw'
- import Icon from '@/components/icon/index.vue'
- import { toChineseDate } from '@/utils/date'
- // 个人服务项目码映射表
- const SERVICE_NAME_MAP = {
- 1: '到校通知',
- 5: '离校通知',
- 11: '进入校车通知',
- 15: '离开校车通知',
- 101: '实时校车位置',
- 111: '音频电话',
- 115: '视频电话',
- 121: '文本留言',
- 122: '图片留言',
- 123: '音频留言',
- 124: '视频留言'
- }
- const normalizeTradeState = (state) => {
- if (state === undefined || state === null) {
- return ''
- }
- return String(state).toUpperCase()
- }
- const formatAmountValue = (amountInfo) => {
- if (amountInfo === undefined || amountInfo === null) {
- return ''
- }
- if (typeof amountInfo === 'number') {
- return amountInfo
- }
- if (typeof amountInfo === 'string') {
- const num = Number(amountInfo)
- return Number.isNaN(num) ? amountInfo : num
- }
- if (typeof amountInfo === 'object') {
- if (typeof amountInfo.total === 'number') {
- // 微信金额单位为分,转回元
- return (amountInfo.total / 100).toFixed(2)
- }
- if (typeof amountInfo.amount === 'number') {
- return amountInfo.amount
- }
- }
- return ''
- }
- const formatDateTime = (value) => {
- if (value === undefined || value === null || value === '') {
- return ''
- }
- let dateObj = null
- if (value instanceof Date) {
- dateObj = value
- } else if (typeof value === 'number') {
- const num = value < 1e12 ? value * 1000 : value
- dateObj = new Date(num)
- } else if (typeof value === 'string') {
- const direct = new Date(value)
- if (!Number.isNaN(direct.getTime())) {
- dateObj = direct
- } else {
- const normalized = value.replace(/-/g, '/').replace('T', ' ')
- const parsed = Date.parse(normalized)
- if (!Number.isNaN(parsed)) {
- dateObj = new Date(parsed)
- }
- }
- }
- if (!dateObj || Number.isNaN(dateObj.getTime())) {
- return typeof value === 'string' ? value : ''
- }
- const pad = (num) => String(num).padStart(2, '0')
- const year = dateObj.getFullYear()
- const month = pad(dateObj.getMonth() + 1)
- const day = pad(dateObj.getDate())
- const hour = pad(dateObj.getHours())
- const minute = pad(dateObj.getMinutes())
- const second = pad(dateObj.getSeconds())
- return `${year}-${month}-${day} ${hour}:${minute}:${second}`
- }
- const getTradeStateMessage = (tradeState, fallbackDesc = '') => {
- if (!tradeState) {
- return fallbackDesc || ''
- }
- const map = {
- SUCCESS: '支付成功',
- REFUND: '订单已退款,金额将原路退回',
- REFUNDED: '订单已退款,金额将原路退回',
- NOTPAY: '订单未支付,等待用户付款',
- USERPAYING: '用户支付中,请稍候',
- CLOSED: '订单已关闭,如有疑问请联系客服',
- REVOKED: '订单已撤销,请重新下单',
- PAYERROR: '支付失败,请联系客服或稍后再试'
- }
- return map[tradeState] || fallbackDesc || ''
- }
- /**
- * 规范化支付状态数据
- * 作用:将后端返回的各种不同格式的支付状态数据,统一转换成前端需要的标准格式
- *
- * 为什么需要这个函数:
- * 1. 后端字段名可能是 snake_case (trade_state) 或 camelCase (tradeState)
- * 2. 订单号字段可能叫 outTradeNo、out_trade_no、orderId、order_id 等多种名字
- * 3. 金额字段可能在 amount、total、payAmount 等不同位置
- * 4. 支付状态码不统一:SUCCESS、PAID、FINISHED 都表示成功
- *
- * @param {Object} res - 后端返回的原始数据
- * @param {string} fallbackOrderId - 备用订单号(如果后端没返回订单号)
- * @returns {Object} 返回规范化后的数据:
- * - status: 'success' | 'failed' | 'checking' - 统一的支付状态
- * - order: { orderId, amount, currency, createTime, payTime, statusDesc, raw } - 订单信息
- * - message: 状态描述文字
- * - tradeState: 原始的交易状态码(大写)
- */
- const normalizePaymentStatus = (res, fallbackOrderId = '') => {
- const source = res?.data || res || {}
- // 1. 提取支付状态:兼容 snake_case (trade_state) 和 camelCase (tradeState)
- const tradeState = normalizeTradeState(source.tradeState || source.trade_state || source.status)
- const statusMessage = getTradeStateMessage(tradeState, source.tradeStateDesc || source.trade_state_desc)
- // 2. 提取订单号:尝试多种可能的字段名
- const orderId =
- source.outTradeNo ||
- source.out_trade_no ||
- source.orderId ||
- source.order_id ||
- source.orderNo ||
- source.order_no ||
- source.id ||
- source.billNo ||
- source.bill_no ||
- fallbackOrderId
- // 3. 构建标准化的订单对象
- const normalizedOrder = {
- orderId,
- amount: formatAmountValue(source.amount ?? source.total ?? source.payAmount),
- currency: source.amount?.currency,
- createTime: formatDateTime(source.createTime || source.timeEnd || ''),
- payTime: formatDateTime(source.successTime || source.payTime || ''),
- statusDesc: statusMessage || source.message || '',
- raw: source // 保存原始数据,方便调试
- }
- // 4. 判断支付状态:将各种状态码统一映射为 success/failed/checking
- let normalizedStatus = 'checking'
- if (['SUCCESS', 'PAID', 'FINISHED', 'PAY_SUCCESS', 'COMPLETED', '1'].includes(tradeState)) {
- normalizedStatus = 'success'
- } else if (
- ['FAIL', 'FAILED', 'PAYERROR', 'CLOSED', 'REVOKED', 'REFUND', 'REFUNDED', '-1'].includes(tradeState)
- ) {
- normalizedStatus = 'failed'
- }
- return {
- status: normalizedStatus,
- order: normalizedOrder,
- message: statusMessage || normalizedOrder.statusDesc || '',
- tradeState
- }
- }
- const orderId = ref('') // 订单ID
- const grfwbid = ref('') // 个人服务包ID
- const paymentStatus = ref('checking') // 支付状态: checking, success, failed
- const orderInfo = ref(null) // 订单详情
- const serviceResult = ref(null) // 服务包确认结果
- const errorMessage = ref('') // 错误信息
- let checkTimer = null // 轮询定时器
- let checkCount = 0 // 查询次数
- const MAX_CHECK_COUNT = 20 // 最大查询次数(20次 * 2秒 = 40秒)
- // 格式化有效期日期
- const formattedDeadline = computed(() => {
- if (!serviceResult.value || !serviceResult.value.jzsj) {
- return ''
- }
- return toChineseDate(serviceResult.value.jzsj)
- })
- // 格式化服务列表(翻译 grfwxmm 服务项目码)
- const formattedServiceList = computed(() => {
- if (!serviceResult.value || !serviceResult.value.grfwList) {
- return []
- }
- return serviceResult.value.grfwList.map(item => ({
- ...item,
- displayName: SERVICE_NAME_MAP[item.grfwxmm] || `未知服务(${item.grfwxmm})`
- }))
- })
- // 页面加载
- onLoad((options) => {
- console.log('支付结果页参数:', options)
- orderId.value = options.orderId
- grfwbid.value = options.grfwbid
- if (!orderId.value) {
- uni.showToast({
- title: '订单号不存在',
- icon: 'none'
- })
- setTimeout(() => {
- uni.navigateBack()
- }, 1500)
- return
- }
- // 开始轮询查询订单状态
- startCheckPaymentStatus()
- })
- // 开始轮询查询支付状态
- const startCheckPaymentStatus = () => {
- console.log('开始查询支付状态')
- checkPaymentStatus()
- // 每2秒查询一次
- checkTimer = setInterval(() => {
- checkCount++
- if (checkCount >= MAX_CHECK_COUNT) {
- // 超过最大查询次数,停止查询
- console.log('查询超时,停止查询')
- stopChecking()
- uni.showModal({
- title: '提示',
- content: '支付结果查询超时,请稍后在订单列表中查看',
- showCancel: false,
- success: () => {
- uni.navigateBack()
- }
- })
- return
- }
- checkPaymentStatus()
- }, 2000)
- }
- // 查询支付状态
- const checkPaymentStatus = async () => {
- try {
- const res = await grfwApi.chkWechatpayBySs({
- outTradeNo: orderId.value
- })
- console.log('查询支付状态结果:', res)
- // 数据在 res.data.ssData 里
- const paymentData = res?.data?.ssData || res?.data || res
- const normalized = normalizePaymentStatus({ data: paymentData }, orderId.value)
- orderInfo.value = normalized.order
- if (normalized.status === 'success') {
- paymentStatus.value = 'success'
- clearCheckTimer()
- // 支付成功后,调用确认服务接口
- await confirmService()
- } else if (normalized.status === 'failed') {
- paymentStatus.value = 'failed'
- errorMessage.value = normalized.message || '支付失败'
- clearCheckTimer()
- } else {
- console.log('订单未完成,继续查询... 当前状态:', normalized.tradeState)
- }
- } catch (error) {
- console.error('查询支付状态失败:', error)
- // 查询失败不立即停止,继续尝试
- }
- }
- // 确认服务包购买
- const confirmService = async () => {
- if (!grfwbid.value) {
- console.log('没有 grfwbid,跳过确认服务')
- return
- }
- try {
- console.log('调用确认服务接口,grfwbid:', grfwbid.value)
- const res = await grfwApi.grfw_endGrfwbBuy({
- grfwbid: grfwbid.value
- })
- console.log('确认服务返回:', res)
- // 解析返回数据
- if (res && res.data && res.data.ssData) {
- const ssData = res.data.ssData
- // ssData 可能是数组或对象
- const resultData = Array.isArray(ssData) ? ssData[0] : ssData
- serviceResult.value = resultData
- console.log('服务包确认结果:', serviceResult.value)
- }
- } catch (error) {
- console.error('确认服务失败:', error)
- }
- }
- // 清除定时器
- const clearCheckTimer = () => {
- if (checkTimer) {
- clearInterval(checkTimer)
- checkTimer = null
- }
- }
- // 停止查询
- const stopChecking = () => {
- clearCheckTimer()
- uni.navigateBack()
- }
- // 返回首页
- const goToHome = () => {
- uni.reLaunch({
- url: '/pages/main/index'
- })
- }
- // 重新支付
- const retryPayment = () => {
- // 返回充值页面
- uni.navigateBack()
- }
- // 页面卸载时清除定时器
- onUnmounted(() => {
- clearCheckTimer()
- })
- </script>
- <style lang="scss" scoped>
- .payment-result-page {
- min-height: 100vh;
- background: #f5f5f5;
- padding: 80rpx 30rpx 30rpx;
- display: flex;
- flex-direction: column;
- align-items: center;
- }
- .status-icon {
- margin-bottom: 40rpx;
- .icon {
- width: 160rpx;
- height: 160rpx;
- display: flex;
- align-items: center;
- justify-content: center;
- }
- }
- .status-text {
- margin-bottom: 60rpx;
- text {
- font-size: 40rpx;
- font-weight: bold;
- }
- .success-text {
- color: #07c160;
- }
- .failed-text {
- color: #ff4d4f;
- }
- .checking-text {
- color: #1890ff;
- }
- }
- .service-info {
- width: 100%;
- background: #fff;
- border-radius: 20rpx;
- padding: 40rpx 30rpx;
- margin-bottom: 30rpx;
- }
- .service-header {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding-bottom: 30rpx;
- border-bottom: 1rpx solid #f0f0f0;
- }
- .service-name {
- font-size: 36rpx;
- font-weight: bold;
- color: #07c160;
- margin-bottom: 16rpx;
- }
- .service-deadline {
- font-size: 26rpx;
- color: #666;
- }
- .service-list {
- padding-top: 30rpx;
- }
- .service-list-title {
- font-size: 28rpx;
- color: #333;
- font-weight: bold;
- margin-bottom: 20rpx;
- }
- .service-items {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 16rpx;
- }
- .service-item {
- display: flex;
- flex-direction: column;
- gap: 8rpx;
- padding: 16rpx;
- background: #f6ffed;
- border-radius: 8rpx;
- border: 1rpx solid #b7eb8f;
- }
- .item-name {
- font-size: 28rpx;
- color: #333;
- font-weight: 500;
- }
- .item-details {
- display: flex;
- flex-wrap: wrap;
- gap: 8rpx;
- }
- .item-tag {
- font-size: 22rpx;
- color: #666;
- background: #fff;
- padding: 4rpx 10rpx;
- border-radius: 4rpx;
- border: 1rpx solid #e5e5e5;
- }
- .item-tag.free {
- color: #52c41a;
- background: #fff;
- border-color: #52c41a;
- }
- .order-info {
- width: 100%;
- background: #fff;
- border-radius: 20rpx;
- padding: 40rpx 30rpx;
- margin-bottom: 30rpx;
- }
- .info-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding: 20rpx 0;
- border-bottom: 1rpx solid #f0f0f0;
- &:last-child {
- border-bottom: none;
- }
- }
- .info-item .label {
- font-size: 28rpx;
- color: #666;
- }
- .info-item .value {
- font-size: 28rpx;
- color: #333;
- &.amount {
- font-size: 36rpx;
- font-weight: bold;
- color: #ff4d4f;
- }
- }
- .error-message {
- width: 100%;
- padding: 20rpx 30rpx;
- background: #fff2f0;
- border: 1rpx solid #ffccc7;
- border-radius: 12rpx;
- margin-bottom: 30rpx;
- text {
- font-size: 26rpx;
- color: #ff4d4f;
- line-height: 40rpx;
- }
- }
- .action-btns {
- width: 100%;
- margin-top: 40rpx;
- margin-bottom: 40rpx;
- }
- .btn {
- width: 100%;
- height: 88rpx;
- line-height: 88rpx;
- border-radius: 44rpx;
- font-size: 32rpx;
- font-weight: bold;
- border: none;
- margin-bottom: 20rpx;
- &::after {
- border: none;
- }
- }
- .primary-btn {
- background: linear-gradient(135deg, #07c160 0%, #05a050 100%);
- color: #fff;
- box-shadow: 0 8rpx 20rpx rgba(7, 193, 96, 0.3);
- }
- .secondary-btn {
- background: #fff;
- color: #666;
- border: 2rpx solid #e5e5e5;
- }
- .bottom-tips {
- margin-top: 20rpx;
- text {
- font-size: 26rpx;
- color: #999;
- line-height: 40rpx;
- }
- }
- </style>
|