<el-page-header
@back="handleBack"
:content="paymentType === 'vip' ? '会员支付' : '停车缴费'"
/>
<el-card class="payment-info-card">
支付信息
支付类型:
{{ paymentType === 'vip' ? '会员充值' : '停车费用' }}
充值时长:
{{ days }} 天
车牌号:
{{ plateNumber }}
停车时长:
{{ parkingDuration }}
优惠券抵扣:
-¥{{ couponDiscount.toFixed(2) }}
应付金额:
¥{{ payableAmount.toFixed(2) }}
</el-card>
<el-card class="payment-method-card">
<template #header>
选择支付方式
</template>
<el-radio-group v-model="selectedMethod" class="payment-methods">
<el-radio label="wechat" class="method-item">
</el-radio>
<el-radio label="alipay" class="method-item">
</el-radio>
<el-radio
label="balance"
class="method-item"
v-if="hasBalance"
:disabled="balance < payableAmount"
>
余额支付
当前余额:¥{{ balance.toFixed(2) }}
(余额充足)
(余额不足)
</el-radio>
</el-radio-group>
</el-card>
<el-card class="coupon-card" v-if="hasCoupons && paymentType === 'parking'">
<template #header>
选择优惠券
<el-select
v-model="couponFilter"
placeholder="筛选优惠券"
size="small"
@change="handleCouponFilterChange"
>
<el-option label="全部可用" value="all" />
<el-option label="即将过期" value="expiring" />
<el-option label="面额>10元" value="over10" />
</el-select>
<el-button
type="text"
@click="showAllCoupons = !showAllCoupons"
v-if="filteredCoupons.length > 2"
>
{{ showAllCoupons ? '收起' : '查看更多' }}
</el-button>
</template>
<el-radio-group v-model="selectedCouponId">
<el-radio :label="0" class="coupon-item">
不使用优惠券
</el-radio>
<el-radio
v-for="coupon in (showAllCoupons ? filteredCoupons : filteredCoupons.slice(0, 2))"
:key="coupon.id"
:label="coupon.id"
class="coupon-item"
:disabled="coupon.value > amount || isCouponExpired(coupon.expireDate)"
>
¥{{ coupon.value }}
{{ coupon.description }}
有效期至:{{ coupon.expireDate }}
(即将过期)
(已过期)
</el-radio>
</el-radio-group>
暂无可用优惠券
<el-button
type="text"
@click="goToGetCoupons"
class="get-coupon-btn"
>
去领券中心获取
</el-button>
</el-card>
<el-card class="receipt-card">
<template #header>
电子账单
</template>
<el-checkbox v-model="needReceipt" class="receipt-checkbox">
需要下载电子账单(PDF格式)
</el-checkbox>
<el-form
:model="receiptInfo"
label-width="100px"
:rules="receiptRules"
ref="receiptFormRef"
size="small"
>
<el-form-item label="账单类型" prop="type">
<el-radio-group v-model="receiptInfo.type">
<el-radio label="personal">个人</el-radio>
<el-radio label="company">企业</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item
label="备注信息"
v-if="receiptInfo.type === 'company'"
prop="remark"
>
<el-input
v-model="receiptInfo.remark"
placeholder="请输入企业名称等信息"
maxlength="100"
show-word-limit
/>
</el-form-item>
<el-form-item>
支付成功后可下载PDF格式电子账单
</el-form-item>
</el-form>
</el-card>
<el-button
type="primary"
size="large"
class="pay-button"
@click="handlePayment"
:loading="isLoading"
:disabled="payableAmount <= 0 || isDisabledPayment"
>
<template v-if="payableAmount <= 0">
无需支付
</template>
<template v-else>
确认支付 ¥{{ payableAmount.toFixed(2) }}
</template>
</el-button>
金额已全部抵扣,无需支付
<el-dialog
title="支付超时提醒"
v-model="showTimeoutDialog"
:close-on-click-modal="false"
width="300px"
>
您的支付已超过{{ paymentTimeout }}分钟未完成,是否继续?
<template #footer>
<el-button @click="handleTimeoutCancel">取消支付</el-button>
<el-button type="primary" @click="handleTimeoutContinue">继续支付</el-button>
</template>
</el-dialog>
<el-dialog
title="支付结果"
v-model="showResultDialog"
:close-on-click-modal="false"
:show-close="false"
width="300px"
>
{{ paymentSuccess ? '支付成功' : '支付失败' }}
{{ paymentType === 'vip' ? '会员已生效,快去享受权益吧!' : '缴费成功,可正常离场' }}
<el-button
type="text"
@click="downloadReceipt"
class="download-btn"
:loading="isDownloading"
>
点击下载电子账单(PDF)
</el-button>
{{ errorMessage || '支付过程中出现错误,请重试' }}
<el-button
type="text"
@click="viewPaymentDetails"
class="details-btn"
v-if="billData?.orderNo"
>
查看详情
</el-button>
<template #footer>
<el-button type="primary" @click="handleResultConfirm">
{{ paymentSuccess ? '完成' : '重新支付' }}
</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, computed, onMounted, reactive, watch, onUnmounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/store/userStore.js'
import axios from '@/utils/axios'
import { ElMessage, ElMessageBox } from 'element-plus'
const router = useRouter()
const route = useRoute()
const userStore = useUserStore()
// 支付参数 - 增加类型校验和默认值
const paymentType = ref(['vip', 'parking'].includes(route.query.type) ? route.query.type : 'parking')
const amount = ref(Math.max(0, Number(route.query.amount) || 0))
const days = ref(Math.max(1, Number(route.query.days) || 0))
const plateNumber = ref(String(route.query.plateNumber || ''))
const packageId = ref(String(route.query.packageId || ''))
const parkingDuration = ref(String(route.query.duration || ''))
// 支付状态
const selectedMethod = ref('wechat')
const selectedCouponId = ref(0)
const showAllCoupons = ref(false)
const isLoading = ref(false)
const isLoadingAssets = ref(false)
const showResultDialog = ref(false)
const paymentSuccess = ref(false)
const errorMessage = ref('')
const billData = ref(null)
const isDownloading = ref(false)
const paymentTimeout = ref(15) // 分钟
const showTimeoutDialog = ref(false)
const timeoutIntervalId = ref(null) // 使用 setInterval 的 id
// 优惠券筛选
const couponFilter = ref('all')
const originalCoupons = ref([])
const coupons = ref([])
// 电子账单相关
const needReceipt = ref(false)
const receiptInfo = reactive({
type: 'personal',
remark: ''
})
const receiptFormRef = ref(null)
const receiptRules = {
remark: [
{ required: true, message: '请输入企业名称', trigger: 'blur' },
{ max: 100, message: '企业名称不能超过100个字符', trigger: 'blur' }
]
}
// 用户资产
const balance = ref(0)
const hasBalance = computed(() => balance.value > 0)
const hasCoupons = computed(() => coupons.value.length > 0)
// 应付金额
const payableAmount = computed(() => {
const value = amount.value - couponDiscount.value
return value > 0 ? Number(value.toFixed(2)) : 0
})
// 支付按钮禁用状态
const isDisabledPayment = computed(() => {
return selectedMethod.value === 'balance' && balance.value < payableAmount.value;
})
// 优惠券折扣计算
const couponDiscount = computed(() => {
if (selectedCouponId.value === 0) return 0
const selected = coupons.value.find(c => c.id === selectedCouponId.value)
return selected ? Math.min(Number(selected.value || 0), amount.value) : 0
})
// 过滤后的优惠券(根据筛选条件)
const filteredCoupons = computed(() => {
let result = [...coupons.value]
switch (couponFilter.value) {
case 'expiring':
result = result.filter(coupon => isCouponExpiringSoon(coupon.expireDate))
break
case 'over10':
result = result.filter(coupon => Number(coupon.value) > 10)
break
default:
break
}
return result
})
// 检查优惠券是否已过期
const isCouponExpired = (expireDate) => {
if (!expireDate) return true
const now = new Date()
const expire = new Date(expireDate)
return expire < now
}
// 检查优惠券是否即将过期(7天内)
const isCouponExpiringSoon = (expireDate) => {
if (!expireDate || isCouponExpired(expireDate)) return false
const now = new Date()
const expire = new Date(expireDate)
const daysDiff = (expire - now) / (1000 * 60 * 60 * 24)
return daysDiff > 0 && daysDiff <= 7
}
// 处理优惠券筛选变化
const handleCouponFilterChange = () => {
showAllCoupons.value = false
}
// 获取用户资产信息(带加载状态)
const fetchUserAssets = async () => {
if (!userStore.isLoggedIn) return
isLoadingAssets.value = true
try {
const res = await axios.get('/api/user/assets')
balance.value = Number(res.data?.data?.balance) || 0
originalCoupons.value = [...(res.data?.data?.coupons || [])]
coupons.value = originalCoupons.value.filter(coupon => !isCouponExpired(coupon.expireDate))
// 若当前选中的优惠券已不可用,则回退为不使用
if (selectedCouponId.value !== 0) {
const stillExists = coupons.value.some(c => c.id === selectedCouponId.value)
if (!stillExists) selectedCouponId.value = 0
}
} catch (error) {
console.error('获取用户资产失败', error)
ElMessage.error('获取优惠券和余额失败,请刷新页面重试')
} finally {
isLoadingAssets.value = false
}
}
// 启动支付超时计时器
const startPaymentTimeoutTimer = () => {
if (timeoutIntervalId.value) {
clearInterval(timeoutIntervalId.value)
timeoutIntervalId.value = null
}
let remainingMinutes = Number(paymentTimeout.value) || 15
timeoutIntervalId.value = setInterval(() => {
remainingMinutes -= 1
if (remainingMinutes <= 0) {
clearInterval(timeoutIntervalId.value)
timeoutIntervalId.value = null
showTimeoutDialog.value = true
}
}, 60000)
}
// 处理支付
const handlePayment = async () => {
// 表单验证
if (needReceipt.value && receiptInfo.type === 'company') {
try {
await receiptFormRef.value?.validate()
} catch {
return
}
}
if (!userStore.isLoggedIn) {
ElMessage.warning('请先登录')
router.push(`/login?redirect=${encodeURIComponent(route.fullPath)}`)
return
}
// 零元支付直接成功(按钮已置灰,此分支作为兜底)
if (payableAmount.value <= 0) {
paymentSuccess.value = true
showResultDialog.value = true
return
}
// 余额不足检查(双重保障)
if (selectedMethod.value === 'balance' && balance.value < payableAmount.value) {
ElMessage.warning('余额不足,请选择其他支付方式')
return
}
isLoading.value = true
try {
const payload = {
type: paymentType.value,
amount: payableAmount.value,
paymentMethod: selectedMethod.value,
couponId: selectedCouponId.value || null,
receipt: needReceipt.value
? { type: receiptInfo.type, remark: receiptInfo.remark }
: null
}
if (paymentType.value === 'vip') {
payload.days = days.value
payload.packageId = packageId.value
} else {
payload.plateNumber = plateNumber.value
payload.duration = parkingDuration.value
}
const res = await axios.post('/api/payments/generate-bill', payload)
billData.value = res.data?.data || null
// 启动支付超时计时器
startPaymentTimeoutTimer()
if (['wechat', 'alipay'].includes(selectedMethod.value)) {
const payUrl = billData.value?.payUrl
if (payUrl) {
const payWindow = window.open(payUrl, '_blank')
if (!payWindow) {
ElMessage.warning('请允许弹出窗口以完成支付')
}
const checkPaymentStatus = setInterval(async () => {
// 窗口被关闭则认为用户完成/终止了支付,拉取结果
if (!payWindow || payWindow.closed) {
clearInterval(checkPaymentStatus)
if (timeoutIntervalId.value) {
clearInterval(timeoutIntervalId.value)
timeoutIntervalId.value = null
}
if (billData.value?.orderNo) {
await checkPaymentResult(billData.value.orderNo)
} else {
paymentSuccess.value = false
errorMessage.value = '支付订单异常,请重试'
showResultDialog.value = true
}
}
}, 3000)
} else {
ElMessage.error('支付链接生成失败,请重试')
if (timeoutIntervalId.value) {
clearInterval(timeoutIntervalId.value)
timeoutIntervalId.value = null
}
}
} else {
// 余额支付直接成功
paymentSuccess.value = true
showResultDialog.value = true
if (timeoutIntervalId.value) {
clearInterval(timeoutIntervalId.value)
timeoutIntervalId.value = null
}
}
} catch (error) {
paymentSuccess.value = false
errorMessage.value = error?.response?.data?.msg || '支付失败,请重试'
showResultDialog.value = true
if (timeoutIntervalId.value) {
clearInterval(timeoutIntervalId.value)
timeoutIntervalId.value = null
}
} finally {
isLoading.value = false
}
}
// 处理超时对话框-取消支付
const handleTimeoutCancel = () => {
showTimeoutDialog.value = false
if (timeoutIntervalId.value) {
clearInterval(timeoutIntervalId.value)
timeoutIntervalId.value = null
}
if (billData.value?.orderNo) {
cancelPayment(billData.value.orderNo)
}
router.back()
}
// 处理超时对话框-继续支付
const handleTimeoutContinue = () => {
showTimeoutDialog.value = false
startPaymentTimeoutTimer()
}
// 取消支付
const cancelPayment = async (orderNo) => {
try {
await axios.post(`/api/payments/${orderNo}/cancel`)
ElMessage.success('已取消支付')
} catch (error) {
console.error('取消支付失败', error)
}
}
// 检查支付结果
const checkPaymentResult = async (orderNo) => {
try {
const res = await axios.get(`/api/payments/${orderNo}/status`)
const status = res.data?.data?.status
if (status === 'SUCCESS') {
paymentSuccess.value = true
if (needReceipt.value) {
const billRes = await axios.get(`/api/payments/${orderNo}/bill`)
billData.value = billRes.data?.data || billData.value
}
} else if (status === 'FAILED') {
paymentSuccess.value = false
errorMessage.value = res.data?.data?.message || '支付失败'
} else {
paymentSuccess.value = false
errorMessage.value = '支付已取消或超时'
}
showResultDialog.value = true
} catch (error) {
console.error('查询支付结果失败', error)
paymentSuccess.value = false
errorMessage.value = '查询支付结果失败,请稍后重试'
showResultDialog.value = true
}
}
// 查看支付详情
const viewPaymentDetails = () => {
if (billData.value?.orderNo) {
router.push(`/payment-details?orderNo=${billData.value.orderNo}`)
showResultDialog.value = false
}
}
// 下载电子账单
const downloadReceipt = async () => {
if (!billData.value?.billId) {
ElMessage.warning('账单信息不存在')
return
}
isDownloading.value = true
try {
const response = await axios.get(
`/api/payments/bills/${billData.value.billId}/download`,
{ responseType: 'blob' }
)
const blob = new Blob([response.data], { type: 'application/pdf' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `账单_${billData.value.orderNo || Date.now()}.pdf`
document.body.appendChild(a)
a.click()
setTimeout(() => {
document.body.removeChild(a)
URL.revokeObjectURL(url)
ElMessage.success('账单下载成功')
}, 100)
} catch (error) {
console.error('下载账单失败', error)
ElMessage.error('账单下载失败,请重试')
} finally {
isDownloading.value = false
}
}
// 支付结果确认
const handleResultConfirm = () => {
showResultDialog.value = false
if (paymentSuccess.value) {
router.push(paymentType.value === 'vip' ? '/vip-page' : '/parking-records')
}
}
// 返回上一页
const handleBack = () => {
if (isLoading.value) {
ElMessage.warning('正在处理支付,请稍后')
return
}
const hasInput = selectedCouponId.value !== 0 || needReceipt.value
if (!hasInput) {
router.back()
return
}
ElMessageBox.confirm(
'确定要返回吗?当前支付信息可能会丢失',
'确认返回',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
router.back()
}).catch(() => {})
}
// 跳转到领券中心
const goToGetCoupons = () => {
router.push('/coupons')
}
// 监听登录状态变化
watch(
() => userStore.isLoggedIn,
(newVal) => {
if (newVal) {
fetchUserAssets()
} else {
router.push(`/login?redirect=${encodeURIComponent(route.fullPath)}`)
}
},
{ immediate: true }
)
// 当优惠券列表变化时,兜底处理已选优惠券失效
watch(coupons, (list) => {
if (selectedCouponId.value && !list.some(c => c.id === selectedCouponId.value)) {
selectedCouponId.value = 0
}
})
// 离开页面提示(可选:避免处理中误操作关闭)
const beforeUnloadHandler = (e) => {
if (isLoading.value) {
e.preventDefault()
e.returnValue = ''
}
}
onMounted(() => {
if (amount.value <= 0 && paymentType.value === 'parking') {
ElMessage.warning('支付金额异常')
router.back()
return
}
window.addEventListener('beforeunload', beforeUnloadHandler)
})
onUnmounted(() => {
if (timeoutIntervalId.value) {
clearInterval(timeoutIntervalId.value)
timeoutIntervalId.value = null
}
window.removeEventListener('beforeunload', beforeUnloadHandler)
})
</script>
<style scoped>
.payment-info-card,
.payment-method-card,
.coupon-card,
.receipt-card {
margin-bottom: 16px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
.payment-summary {
padding: 16px 0;
}
.payment-summary h3 {
margin-bottom: 16px;
font-size: 16px;
color: #333;
}
.summary-item {
display: flex;
margin-bottom: 12px;
font-size: 14px;
}
.summary-item .label {
flex: 0 0 100px;
color: #666;
}
.summary-item .value {
flex: 1;
color: #333;
}
.total-amount {
margin-top: 16px;
padding-top: 16px;
border-top: 1px dashed #eee;
}
.total-amount .value {
font-size: 18px;
font-weight: bold;
}
.amount {
color: #f56c6c;
}
.payment-methods {
display: flex;
flex-direction: column;
gap: 12px;
padding: 8px 0;
}
.method-item {
display: flex;
align-items: center;
padding: 12px;
border-radius: 6px;
border: 1px solid #eee;
transition: all 0.2s;
}
.method-item:hover {
border-color: #ddd;
background-color: #fafafa;
}
.method-icon {
width: 40px;
height: 40px;
margin-right: 12px;
border-radius: 50%;
background-size: contain;
background-repeat: no-repeat;
background-position: center;
}
.wechat-icon {
background-image: url('/https/wenku.csdn.net/icons/wechat.png');
background-color: #07c160;
}
.alipay-icon {
background-image: url('/https/wenku.csdn.net/icons/alipay.png');
background-color: #1677ff;
}
.balance-icon {
background-image: url('/https/wenku.csdn.net/icons/balance.png');
background-color: #ff9f1c;
}
.method-name {
font-size: 16px;
margin-bottom: 4px;
}
.method-desc {
font-size: 12px;
color: #666;
}
.card-header-flex {
display: flex;
justify-content: space-between;
align-items: center;
}
.coupon-filter {
width: 160px;
}
.coupon-list {
padding: 8px 0;
}
.coupon-item {
display: block;
padding: 12px;
margin-bottom: 8px;
border-radius: 6px;
border: 1px solid #eee;
transition: all 0.2s;
}
.coupon-item:hover {
border-color: #ddd;
background-color: #fafafa;
}
.coupon-content {
display: flex;
justify-content: space-between;
align-items: center;
}
.coupon-value {
font-size: 18px;
font-weight: bold;
color: #f56c6c;
min-width: 60px;
}
.coupon-desc {
flex: 1;
margin: 0 16px;
color: #333;
}
.coupon-expire {
font-size: 12px;
color: #666;
text-align: right;
min-width: 140px;
}
.coupon-expiring-soon {
color: #faad14;
margin-left: 5px;
font-size: 12px;
}
.coupon-expired {
color: #f56c6c;
margin-left: 5px;
font-size: 12px;
}
.no-coupon {
padding: 16px;
text-align: center;
color: #666;
font-size: 14px;
}
.get-coupon-btn {
color: #409eff;
margin-left: 8px;
padding: 0;
}
.get-coupon-btn:hover {
color: #6aa8ff;
}
.el-radio.is-disabled .coupon-item {
opacity: 0.6;
cursor: not-allowed;
}
.el-radio__input.is-checked + .el-radio__label .coupon-item {
border-color: #409eff;
background-color: #f5f9ff;
}
.receipt-settings {
padding: 8px 0;
}
.receipt-checkbox {
margin-bottom: 16px;
}
.receipt-form {
padding: 16px;
background-color: #f5f7fa;
border-radius: 6px;
}
.receipt-hint {
margin-top: 8px;
font-size: 12px;
color: #666;
}
.payment-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
padding: 16px;
background-color: #fff;
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
text-align: center;
}
.pay-button {
width: 100%;
height: 48px;
font-size: 16px;
}
.payment-note {
margin-top: 12px;
font-size: 12px;
color: #666;
}
.result-content {
text-align: center;
padding: 16px 0;
}
.result-icon {
width: 64px;
height: 64px;
margin: 0 auto 16px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
}
.result-icon.success {
background-color: #f0f9eb;
color: #52c41a;
}
.result-icon.fail {
background-color: #fff2f0;
color: #f56c6c;
}
.result-text {
font-size: 18px;
font-weight: bold;
margin-bottom: 8px;
}
.result-desc {
font-size: 14px;
color: #666;
margin-bottom: 16px;
}
.download-btn, .details-btn {
color: #409eff;
padding: 0;
margin-left: 8px;
}
/* 响应式调整 */
@media (max-width: 768px) {
.payment-container {
padding-bottom: 100px;
}
.coupon-content {
flex-direction: column;
align-items: flex-start;
}
.coupon-value, .coupon-expire {
margin-bottom: 8px;
min-width: auto;
}
.coupon-desc {
margin: 8px 0;
width: 100%;
}
}
</style>package com.example.parking.controller;
import com.example.parking.common.Result;
import com.example.parking.dto.PaymentDTO;
import com.example.parking.entity.Bill;
import com.example.parking.entity.PaymentRecord;
import com.example.parking.service.BillService;
import com.example.parking.service.PaymentService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/payments")
public class PaymentController {
@Autowired
private PaymentService paymentService;
@Autowired // 新增BillService注入
private BillService billService;
// 查询用户待缴费记录
@GetMapping("/pending")
public Result<List<PaymentRecord>> getPendingPayments(HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
List<PaymentRecord> records = paymentService.getPendingPayments(userId);
return Result.success(records);
}
// 查询用户缴费历史
@GetMapping("/history")
public Result<List<PaymentRecord>> getPaymentHistory(HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
List<PaymentRecord> records = paymentService.getPaymentHistory(userId);
return Result.success(records);
}
// 提交缴费(保留现有方法,与新增支付方法区分路径或逻辑)
@PostMapping
public Result<PaymentRecord> submitPayment(@Validated @RequestBody PaymentDTO dto, HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
PaymentRecord record = paymentService.processPayment(userId, dto);
return Result.success("缴费成功", record);
}
// 新增:处理支付并生成账单(若需要单独的支付接口可调整路径)
@PostMapping("/generate-bill") // 新增路径避免与现有POST冲突
public Result<Bill> payParkingFee(@RequestBody PaymentDTO dto, HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId"); // 需验证用户身份
Bill bill = paymentService.processPaymentWithBill(userId, dto); // 假设Service层有此方法
return Result.success("支付成功,账单已生成", bill);
}
// 新增:下载账单PDF
@GetMapping("/bills/{id}/download")
public void downloadBill(@PathVariable Long id, HttpServletResponse response, HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId"); // 验证用户权限
billService.generatePdfBill(id, userId, response); // 增加用户ID校验防止越权
}
// 获取缴费详情
@GetMapping("/{id}")
public Result<PaymentRecord> getPaymentDetail(@PathVariable Long id, HttpServletRequest request) {
Long userId = (Long) request.getAttribute("userId");
PaymentRecord record = paymentService.getPaymentDetail(id, userId);
return Result.success(record);
}
}vue和controller对应写,且排版合理一点