feat(payment): 添加PayPal支付功能
- 实现PayPal订单创建、查询和捕获的API接口 - 创建PayPal支付取消页面,提供订单信息展示和继续支付功能 - 开发PayPal支付成功页面,处理支付回调和订单状态更新 - 集成PayPal订单状态检查和捕获流程 - 添加支付结果的用户界面和错误处理机制 - 实现订单信息的实时更新和状态同步
This commit is contained in:
40
src/api/paypal.js
Normal file
40
src/api/paypal.js
Normal 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
106
src/views/PayPalCancel.vue
Normal 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
236
src/views/PayPalSuccess.vue
Normal 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返回的token(orderId)
|
||||
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user