feat: payment refund support + admin payment management
- Add refund()/queryRefund()/downloadPlatformCerts() to WechatPayService - Add refundId field to PaymentOrder schema - Fix WeChat Pay callback to auto-download platform certs on verification failure - Fix syncOrder to handle sprint plan properly - Add admin refund, refund-query, order-detail endpoints - Add refund UI (button, modal, query) to admin.vue orders tab - Fix member.vue MP payment: pass outTradeNo instead of prepayId to pollPayResult
This commit is contained in:
@@ -142,8 +142,10 @@
|
||||
{{ o.status === 'success' ? '已支付' : o.status === 'refunded' ? '已退款' : '待支付' }}
|
||||
</view>
|
||||
<text class="order-time">{{ o.createdAt ? o.createdAt.slice(0,16).replace('T',' ') : '--' }}</text>
|
||||
<view class="order-actions" v-if="o.status === 'pending'">
|
||||
<text class="sync-btn" @click="syncOrder(o.outTradeNo)">同步</text>
|
||||
<view class="order-actions">
|
||||
<text class="sync-btn" v-if="o.status === 'pending'" @click="syncOrder(o.outTradeNo)">同步</text>
|
||||
<text class="refund-btn" v-if="o.status === 'success'" @click="openRefundModal(o)">退款</text>
|
||||
<text class="sync-btn" v-if="o.status === 'refunded'" @click="queryRefund(o.outTradeNo)">查询</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -338,6 +340,29 @@
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 退款弹窗 -->
|
||||
<view class="modal-mask" v-if="refundModal.show" @click="closeRefundModal">
|
||||
<view class="modal-content" @click.stop>
|
||||
<text class="modal-title">退款 - {{ refundModal.order?.outTradeNo }}</text>
|
||||
<view class="cfg-row">
|
||||
<text>订单金额</text>
|
||||
<text class="cfg-val">¥{{ ((refundModal.order?.amount || 0) / 100).toFixed(1) }}</text>
|
||||
</view>
|
||||
<view class="cfg-row">
|
||||
<text>退款金额(元)</text>
|
||||
<input class="cfg-input" type="digit" v-model.number="refundAmount" :placeholder="((refundModal.order?.amount || 0) / 100).toFixed(1)" />
|
||||
</view>
|
||||
<view class="cfg-row">
|
||||
<text>退款原因</text>
|
||||
<input class="cfg-input" style="width:300rpx" v-model="refundReason" placeholder="选填" />
|
||||
</view>
|
||||
<view class="modal-actions">
|
||||
<button class="modal-btn cancel" @click="closeRefundModal">取消</button>
|
||||
<button class="modal-btn confirm" style="background:#EF4444" @click="doRefund">确认退款</button>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 管理员 -->
|
||||
<view v-if="tab === 'admins'" class="section">
|
||||
<view class="search-bar">
|
||||
@@ -448,6 +473,67 @@ const creditTypes = ref([
|
||||
{ key: 'shareCredits', label: '分享积分', value: 0 },
|
||||
])
|
||||
|
||||
// Refund modal
|
||||
const refundModal = ref({ show: false, order: null })
|
||||
const refundAmount = ref(0)
|
||||
const refundReason = ref('')
|
||||
|
||||
const openRefundModal = (order) => {
|
||||
refundModal.value = { show: true, order }
|
||||
refundAmount.value = order.amount / 100
|
||||
refundReason.value = ''
|
||||
}
|
||||
|
||||
const closeRefundModal = () => {
|
||||
refundModal.value = { show: false, order: null }
|
||||
}
|
||||
|
||||
const doRefund = async () => {
|
||||
const order = refundModal.value.order
|
||||
if (!order) return
|
||||
uni.showModal({
|
||||
title: '确认退款', content: `确定对订单 ${order.outTradeNo} 退款 ¥${refundAmount.value.toFixed(1)}?`,
|
||||
success: async (r) => {
|
||||
if (!r.confirm) return
|
||||
try {
|
||||
const res = await apiAdmin('/order/refund', {
|
||||
method: 'POST',
|
||||
body: { outTradeNo: order.outTradeNo, amount: Math.round(refundAmount.value * 100), reason: refundReason.value },
|
||||
})
|
||||
if (res.statusCode === 200) {
|
||||
uni.showToast({ title: '退款成功', icon: 'success' })
|
||||
closeRefundModal()
|
||||
loadOrders()
|
||||
} else {
|
||||
uni.showToast({ title: res.data?.message || '退款失败', icon: 'none' })
|
||||
}
|
||||
} catch (e) {
|
||||
uni.showToast({ title: '退款失败', icon: 'none' })
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
const queryRefund = async (outTradeNo) => {
|
||||
uni.showToast({ title: '查询中...', icon: 'none' })
|
||||
try {
|
||||
const res = await apiAdmin('/order/refund/' + outTradeNo)
|
||||
if (res.statusCode === 200) {
|
||||
const wx = res.data.wxRefund
|
||||
if (wx) {
|
||||
uni.showModal({
|
||||
title: '退款状态',
|
||||
content: `微信状态: ${wx.status || '--'}\n退款金额: ¥${(wx.amount?.refund || 0) / 100}\n建议以微信侧为准`,
|
||||
})
|
||||
} else {
|
||||
uni.showToast({ title: '本地状态: ' + (res.data.localStatus || '--'), icon: 'none' })
|
||||
}
|
||||
} else {
|
||||
uni.showToast({ title: '查询失败', icon: 'none' })
|
||||
}
|
||||
} catch { uni.showToast({ title: '查询失败', icon: 'none' }) }
|
||||
}
|
||||
|
||||
const token = () => uni.getStorageSync('token') || ''
|
||||
|
||||
const apiAdmin = (path, opts = {}) => {
|
||||
@@ -860,6 +946,7 @@ onMounted(() => { doVerify() })
|
||||
.order-time { font-size: 20rpx; color: var(--color-text-tertiary); flex: 1; text-align: right; }
|
||||
.order-actions { }
|
||||
.sync-btn { font-size: 20rpx; color: var(--color-primary); padding: 4rpx 12rpx; border: 2rpx solid var(--color-primary); border-radius: var(--radius-round); }
|
||||
.refund-btn { font-size: 20rpx; color: #EF4444; padding: 4rpx 12rpx; border: 2rpx solid #EF4444; border-radius: var(--radius-round); }
|
||||
.config-card { background: #FFF; border-radius: var(--radius-sm); padding: 20rpx; margin-bottom: 12rpx; }
|
||||
.cfg-title { font-size: 24rpx; font-weight: 600; color: var(--color-text); margin-bottom: 12rpx; }
|
||||
.cfg-row { display: flex; justify-content: space-between; padding: 8rpx 0; font-size: 22rpx; color: var(--color-text-secondary); align-items: center; }
|
||||
|
||||
Reference in New Issue
Block a user