feat(payment): 添加PayPal支付功能

- 实现PayPal订单创建、查询和捕获的API接口
- 创建PayPal支付取消页面,提供订单信息展示和继续支付功能
- 开发PayPal支付成功页面,处理支付回调和订单状态更新
- 集成PayPal订单状态检查和捕获流程
- 添加支付结果的用户界面和错误处理机制
- 实现订单信息的实时更新和状态同步
This commit is contained in:
2025-12-24 09:22:00 +08:00
parent 45f6a5020d
commit 2dfd0c13a8
3 changed files with 382 additions and 0 deletions

40
src/api/paypal.js Normal file
View File

@@ -0,0 +1,40 @@
import request from './request'
/**
* 创建PayPal订单
*/
export function createPayPalOrder(data) {
return request({
url: '/paypal/orders',
method: 'post',
data
})
}
/**
* 查询PayPal订单详情
*/
export function getPayPalOrder(orderId) {
return request({
url: `/paypal/orders/${orderId}`,
method: 'get'
})
}
/**
* 捕获PayPal订单完成支付
* @param orderId PayPal订单ID
* @param erpOrderNo ERP订单号可选
*/
export function capturePayPalOrder(orderId, erpOrderNo) {
const params = {}
if (erpOrderNo) {
params.erpOrderNo = erpOrderNo
}
return request({
url: `/paypal/orders/${orderId}/capture`,
method: 'post',
params
})
}

106
src/views/PayPalCancel.vue Normal file
View File

@@ -0,0 +1,106 @@
<template>
<div class="paypal-cancel">
<el-card>
<el-result
icon="warning"
title="支付已取消"
sub-title="您已取消PayPal支付订单已保存您可以稍后继续支付"
>
<template #extra>
<el-button type="primary" @click="continuePay">继续支付</el-button>
<el-button @click="goHome">返回首页</el-button>
</template>
</el-result>
<div class="order-summary" v-if="order">
<h3>订单信息</h3>
<el-descriptions :column="2" border>
<el-descriptions-item label="订单号">{{ order.orderNo }}</el-descriptions-item>
<el-descriptions-item label="订单金额">
<span class="order-amount">{{ order.currency }} {{ formatPrice(order.totalAmount) }}</span>
</el-descriptions-item>
<el-descriptions-item label="商品名称" :span="2">{{ order.productName }}</el-descriptions-item>
<el-descriptions-item label="订单状态" :span="2">
<el-tag type="warning">待支付</el-tag>
</el-descriptions-item>
</el-descriptions>
</div>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { getOrderByOrderNo } from '../api/order'
import { formatAmount } from '../utils/helpers'
const router = useRouter()
const route = useRoute()
const order = ref(null)
// 格式化价格
const formatPrice = (price) => {
return formatAmount(price)
}
// 继续支付
const continuePay = () => {
if (order.value) {
router.push({
path: '/order/confirm',
query: { orderNo: order.value.orderNo }
})
}
}
// 返回首页
const goHome = () => {
router.push('/')
}
// 加载订单信息
onMounted(async () => {
const orderNo = route.query.orderNo
if (orderNo) {
try {
const response = await getOrderByOrderNo(orderNo)
if (response.code === '0000' && response.data) {
order.value = response.data
}
} catch (error) {
console.error('获取订单信息失败:', error)
}
}
})
</script>
<style scoped>
.paypal-cancel {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.order-summary {
margin-top: 30px;
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.order-summary h3 {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 15px;
}
.order-amount {
font-size: 18px;
font-weight: 700;
color: #f56c6c;
}
</style>

236
src/views/PayPalSuccess.vue Normal file
View File

@@ -0,0 +1,236 @@
<template>
<div class="paypal-success">
<el-card v-if="processing">
<div class="processing-container">
<el-icon class="is-loading" style="font-size: 48px; color: #409eff">
<Loading />
</el-icon>
<p>正在处理支付结果...</p>
</div>
</el-card>
<el-card v-else-if="order">
<template #header>
<div class="card-header">
<span>支付结果</span>
<el-tag :type="paymentStatus === 'PAID' ? 'success' : 'warning'" size="large">
{{ paymentStatus === 'PAID' ? '支付成功' : '支付处理中' }}
</el-tag>
</div>
</template>
<div class="result-content">
<el-result
:icon="paymentStatus === 'PAID' ? 'success' : 'warning'"
:title="paymentStatus === 'PAID' ? '支付成功' : '支付处理中'"
:sub-title="paymentStatus === 'PAID' ? '您的订单已支付成功我们将尽快为您发货' : '正在确认支付结果请稍候...'"
>
<template #extra>
<el-button type="primary" @click="viewOrder">查看订单</el-button>
<el-button @click="goHome">返回首页</el-button>
</template>
</el-result>
<div class="order-summary" v-if="order">
<h3>订单信息</h3>
<el-descriptions :column="2" border>
<el-descriptions-item label="订单号">{{ order.orderNo }}</el-descriptions-item>
<el-descriptions-item label="订单金额">
<span class="order-amount">{{ order.currency }} {{ formatPrice(order.totalAmount) }}</span>
</el-descriptions-item>
<el-descriptions-item label="商品名称" :span="2">{{ order.productName }}</el-descriptions-item>
<el-descriptions-item label="支付状态" :span="2">
<el-tag :type="order.paymentStatus === 'PAID' ? 'success' : 'warning'">
{{ order.paymentStatus === 'PAID' ? '已支付' : '未支付' }}
</el-tag>
</el-descriptions-item>
</el-descriptions>
</div>
</div>
</el-card>
<el-card v-else>
<el-result
icon="error"
title="支付失败"
sub-title="无法获取订单信息请稍后重试或联系客服"
>
<template #extra>
<el-button type="primary" @click="goHome">返回首页</el-button>
</template>
</el-result>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { ElMessage } from 'element-plus'
import { Loading } from '@element-plus/icons-vue'
import { getOrderByOrderNo } from '../api/order'
import { getPayPalOrder, capturePayPalOrder } from '../api/paypal'
import { formatAmount } from '../utils/helpers'
const router = useRouter()
const route = useRoute()
const processing = ref(true)
const order = ref(null)
const paymentStatus = ref('UNPAID')
const paypalOrderId = ref(null)
// 格式化价格
const formatPrice = (price) => {
return formatAmount(price)
}
// 查看订单
const viewOrder = () => {
if (order.value) {
router.push({
path: '/order/confirm',
query: { orderNo: order.value.orderNo }
})
}
}
// 返回首页
const goHome = () => {
router.push('/')
}
// 处理PayPal支付回调步骤7-11
const handlePayPalCallback = async () => {
const orderNo = route.query.orderNo
const token = route.query.token // PayPal返回的tokenorderId
if (!orderNo) {
ElMessage.error('订单号不能为空')
processing.value = false
return
}
try {
// 加载ERP订单信息
const orderResponse = await getOrderByOrderNo(orderNo)
if (orderResponse.code === '0000' && orderResponse.data) {
order.value = orderResponse.data
} else {
ElMessage.error('获取订单信息失败')
processing.value = false
return
}
// 如果有token说明是从PayPal跳转回来的
if (token) {
paypalOrderId.value = token
// 步骤8查询PayPal订单状态
const paypalOrderResponse = await getPayPalOrder(token)
if (paypalOrderResponse.code === '0000' && paypalOrderResponse.data) {
const paypalOrder = paypalOrderResponse.data
// 步骤9-10检查订单状态
if (paypalOrder.status === 'APPROVED') {
// 步骤11捕获订单完成支付
// 传递ERP订单号后端会自动更新订单状态
const captureResponse = await capturePayPalOrder(token, orderNo)
if (captureResponse.code === '0000' && captureResponse.data) {
const capturedOrder = captureResponse.data
// 检查捕获状态
if (capturedOrder.status === 'COMPLETED') {
paymentStatus.value = 'PAID'
ElMessage.success('支付成功!订单状态已更新')
// 重新加载订单信息以获取最新状态
const updatedOrderResponse = await getOrderByOrderNo(orderNo)
if (updatedOrderResponse.code === '0000' && updatedOrderResponse.data) {
order.value = updatedOrderResponse.data
}
} else {
ElMessage.warning('支付处理中,请稍候...')
}
} else {
ElMessage.error('捕获订单失败,请稍后重试')
}
} else if (paypalOrder.status === 'COMPLETED') {
// 订单已完成
paymentStatus.value = 'PAID'
ElMessage.success('支付成功!')
} else {
ElMessage.warning('支付状态:' + paypalOrder.status)
}
} else {
ElMessage.error('查询PayPal订单失败')
}
} else {
// 没有token可能是直接访问页面
ElMessage.warning('缺少支付信息')
}
} catch (error) {
console.error('处理PayPal回调失败:', error)
ElMessage.error('处理支付结果失败,请稍后重试')
} finally {
processing.value = false
}
}
onMounted(() => {
handlePayPalCallback()
})
</script>
<style scoped>
.paypal-success {
max-width: 1000px;
margin: 0 auto;
padding: 20px;
}
.processing-container {
text-align: center;
padding: 60px 20px;
}
.processing-container p {
margin-top: 20px;
font-size: 16px;
color: #606266;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 18px;
font-weight: bold;
}
.result-content {
padding: 20px 0;
}
.order-summary {
margin-top: 30px;
padding: 20px;
background: #f5f7fa;
border-radius: 8px;
}
.order-summary h3 {
font-size: 16px;
font-weight: 600;
color: #303133;
margin-bottom: 15px;
}
.order-amount {
font-size: 18px;
font-weight: 700;
color: #f56c6c;
}
</style>