feat: 添加任务状态级联触发器,优化支付和做同款功能
主要更新: - 添加 MySQL 触发器实现 task_status 表到其他表的状态级联 - 移除控制器中的多表状态检查代码 - 完善做同款功能,支持参数传递 - 支付宝 USD 转 CNY 汇率转换 - 修复状态枚举映射问题 注意: 触发器仅在 task_status 更新时触发,部分代码仍直接更新业务表
This commit is contained in:
@@ -46,11 +46,18 @@
|
||||
<!-- 支付宝二维码区域 -->
|
||||
<div v-if="selectedMethod === 'alipay'" class="qr-section">
|
||||
<div class="qr-code">
|
||||
<img id="qr-code-img" style="display: none; width: 200px; height: 200px; margin: 0; padding: 0; border: none; object-fit: contain; background: #1a1a1a;" alt="支付二维码" />
|
||||
<div ref="qrPlaceholder" class="qr-placeholder">
|
||||
<div class="qr-grid">
|
||||
<div class="qr-dot" v-for="i in 64" :key="i"></div>
|
||||
</div>
|
||||
<img v-if="showQrCode && qrCodeUrl" :src="qrCodeUrl" style="width: 200px; height: 200px; margin: 0; padding: 0; border: none; object-fit: contain; background: #1a1a1a;" alt="支付二维码" />
|
||||
<div v-if="!showQrCode" class="qr-placeholder">
|
||||
<svg width="200" height="200" viewBox="0 0 360 360" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<foreignObject x="-5.8" y="-5.8" width="371.6" height="371.6"><div xmlns="http://www.w3.org/1999/xhtml" style="backdrop-filter:blur(2.9px);clip-path:url(#bgblur_0_605_316_clip_path);height:100%;width:100%"></div></foreignObject>
|
||||
<rect data-figma-bg-blur-radius="5.8" width="360" height="360" rx="10" fill="#0F0F12" fill-opacity="0.9"/>
|
||||
<defs>
|
||||
<clipPath id="bgblur_0_605_316_clip_path" transform="translate(5.8 5.8)"><rect width="360" height="360" rx="10"/></clipPath>
|
||||
</defs>
|
||||
<!-- 加载动画 -->
|
||||
<text x="180" y="165" text-anchor="middle" fill="rgba(255,255,255,0.6)" font-size="28" font-family="Arial" font-weight="500">正在生成二维码</text>
|
||||
<text x="180" y="210" text-anchor="middle" fill="rgba(255,255,255,0.4)" font-size="22" font-family="Arial">请稍候...</text>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="qr-tip">支付前请阅读《Vionow支付服务条款》</div>
|
||||
@@ -83,6 +90,9 @@
|
||||
<p>请使用支付宝扫描上方二维码完成支付</p>
|
||||
<p class="tip-small">支付完成后页面将自动更新</p>
|
||||
</div>
|
||||
<button class="check-payment-btn" @click="manualCheckPayment" :disabled="!currentPaymentId">
|
||||
我已完成支付
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 底部链接 -->
|
||||
@@ -126,28 +136,43 @@ const visible = ref(false)
|
||||
const selectedMethod = ref('alipay')
|
||||
const loading = ref(false)
|
||||
const currentPaymentId = ref(null)
|
||||
const qrCodeUrl = ref('') // 二维码URL
|
||||
const showQrCode = ref(false) // 是否显示二维码
|
||||
let paymentPollingTimer = null
|
||||
let isPaymentStarted = false // 防止重复调用
|
||||
|
||||
// 监听 modelValue 变化
|
||||
watch(() => props.modelValue, (newVal) => {
|
||||
visible.value = newVal
|
||||
// 当模态框打开时,自动开始支付流程
|
||||
if (newVal) {
|
||||
// 当模态框打开时,自动开始支付流程(只调用一次)
|
||||
if (newVal && !isPaymentStarted) {
|
||||
isPaymentStarted = true
|
||||
handlePay()
|
||||
}
|
||||
// 关闭时重置标志
|
||||
if (!newVal) {
|
||||
isPaymentStarted = false
|
||||
}
|
||||
})
|
||||
|
||||
// 监听 visible 变化
|
||||
watch(visible, (newVal) => {
|
||||
emit('update:modelValue', newVal)
|
||||
// 如果模态框关闭,停止轮询
|
||||
// 如果模态框关闭,停止轮询并重置状态
|
||||
if (!newVal) {
|
||||
stopPaymentPolling()
|
||||
qrCodeUrl.value = ''
|
||||
showQrCode.value = false
|
||||
}
|
||||
})
|
||||
|
||||
// 选择支付方式
|
||||
const selectMethod = (method) => {
|
||||
// 如果切换了支付方式,需要重置 paymentId,因为支付方式不同需要创建新的支付记录
|
||||
if (selectedMethod.value !== method) {
|
||||
currentPaymentId.value = null
|
||||
console.log('切换支付方式,重置 paymentId')
|
||||
}
|
||||
selectedMethod.value = method
|
||||
}
|
||||
|
||||
@@ -156,6 +181,31 @@ const handlePay = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
|
||||
// 如果还没有创建支付记录,先创建
|
||||
if (!currentPaymentId.value) {
|
||||
// 生成唯一的订单ID,加上时间戳避免重复
|
||||
const uniqueOrderId = `${props.orderId}_${Date.now()}`
|
||||
const paymentData = {
|
||||
orderId: uniqueOrderId,
|
||||
amount: props.amount.toString(),
|
||||
method: selectedMethod.value === 'paypal' ? 'PAYPAL' : 'ALIPAY',
|
||||
description: `${props.title} - ${selectedMethod.value === 'paypal' ? 'PayPal' : '支付宝'}支付`
|
||||
}
|
||||
|
||||
console.log('=== 创建支付记录 ===')
|
||||
console.log('支付数据:', paymentData)
|
||||
|
||||
const createResponse = await createPayment(paymentData)
|
||||
console.log('创建支付订单响应:', createResponse)
|
||||
|
||||
if (createResponse.data && createResponse.data.success) {
|
||||
currentPaymentId.value = createResponse.data.data.id
|
||||
console.log('支付记录创建成功,ID:', currentPaymentId.value)
|
||||
} else {
|
||||
throw new Error(createResponse.data?.message || '创建支付记录失败')
|
||||
}
|
||||
}
|
||||
|
||||
// 根据选择的支付方式处理
|
||||
if (selectedMethod.value === 'paypal') {
|
||||
await handlePayPalPayment()
|
||||
@@ -179,125 +229,57 @@ const handlePay = async () => {
|
||||
|
||||
// 处理支付宝支付
|
||||
const handleAlipayPayment = async () => {
|
||||
ElMessage.info('正在创建支付订单...')
|
||||
// 重置二维码显示状态
|
||||
showQrCode.value = false
|
||||
qrCodeUrl.value = ''
|
||||
|
||||
// 创建支付订单数据
|
||||
const paymentData = {
|
||||
orderId: props.orderId,
|
||||
amount: props.amount.toString(),
|
||||
method: 'ALIPAY',
|
||||
description: `${props.title} - 支付宝支付`
|
||||
}
|
||||
|
||||
ElMessage.info('正在生成支付二维码...')
|
||||
|
||||
const paymentId = currentPaymentId.value
|
||||
console.log('=== 开始支付宝支付流程 ===')
|
||||
console.log('支付数据:', paymentData)
|
||||
console.log('使用已创建的支付ID:', paymentId)
|
||||
|
||||
// 创建支付宝支付
|
||||
const alipayResponse = await createAlipayPayment({ paymentId })
|
||||
console.log('支付宝支付响应:', alipayResponse)
|
||||
|
||||
if (alipayResponse.data && alipayResponse.data.success) {
|
||||
// 显示二维码
|
||||
const qrCode = alipayResponse.data.data.qrCode
|
||||
console.log('支付宝二维码:', qrCode)
|
||||
|
||||
// 先创建支付记录
|
||||
console.log('1. 创建支付订单...')
|
||||
const createResponse = await createPayment(paymentData)
|
||||
console.log('创建支付订单响应:', createResponse)
|
||||
// 使用QuickChart API生成二维码
|
||||
qrCodeUrl.value = `https://quickchart.io/qr?text=${encodeURIComponent(qrCode)}&size=200&margin=0&dark=ffffff&light=1a1a1a`
|
||||
|
||||
if (createResponse.data && createResponse.data.success) {
|
||||
const paymentId = createResponse.data.data.id
|
||||
currentPaymentId.value = paymentId
|
||||
console.log('2. 支付订单创建成功,ID:', paymentId)
|
||||
|
||||
ElMessage.info('正在生成支付宝二维码...')
|
||||
|
||||
console.log('3. 创建支付宝支付...')
|
||||
// 创建支付宝支付
|
||||
const alipayResponse = await createAlipayPayment({ paymentId })
|
||||
console.log('支付宝支付响应:', alipayResponse)
|
||||
console.log('支付宝支付响应数据:', alipayResponse.data)
|
||||
console.log('支付宝支付响应数据详情:', JSON.stringify(alipayResponse.data, null, 2))
|
||||
|
||||
if (alipayResponse.data && alipayResponse.data.success) {
|
||||
// 显示二维码
|
||||
const qrCode = alipayResponse.data.data.qrCode
|
||||
console.log('4. 支付宝二维码:', qrCode)
|
||||
|
||||
// 使用在线API生成二维码图片(直接使用支付宝返回的URL生成二维码)
|
||||
try {
|
||||
console.log('开始生成二维码,内容:', qrCode)
|
||||
|
||||
// 使用QuickChart API生成二维码,完全去除白边
|
||||
// 直接使用支付宝返回的URL作为二维码内容
|
||||
const qrCodeUrl = `https://quickchart.io/qr?text=${encodeURIComponent(qrCode)}&size=200&margin=0&dark=ffffff&light=1a1a1a`
|
||||
|
||||
console.log('5. 二维码图片URL已生成')
|
||||
|
||||
// 更新二维码显示
|
||||
const qrCodeElement = document.querySelector('#qr-code-img')
|
||||
if (qrCodeElement) {
|
||||
qrCodeElement.src = qrCodeUrl
|
||||
qrCodeElement.style.display = 'block'
|
||||
console.log('6. 二维码图片已设置')
|
||||
}
|
||||
|
||||
// 隐藏模拟二维码
|
||||
const qrPlaceholder = document.querySelector('.qr-placeholder')
|
||||
if (qrPlaceholder) {
|
||||
qrPlaceholder.style.display = 'none'
|
||||
console.log('7. 模拟二维码已隐藏')
|
||||
}
|
||||
|
||||
ElMessage.success('二维码已生成,请使用支付宝扫码支付')
|
||||
console.log('=== 支付流程完成,开始轮询支付状态 ===')
|
||||
|
||||
// 开始轮询支付状态
|
||||
startPaymentPolling(paymentId)
|
||||
} catch (error) {
|
||||
console.error('生成二维码失败:', error)
|
||||
ElMessage.error('生成二维码失败,请重试')
|
||||
}
|
||||
|
||||
} else {
|
||||
console.error('支付宝响应失败:', alipayResponse)
|
||||
ElMessage.error(alipayResponse.data?.message || '生成二维码失败')
|
||||
emit('pay-error', new Error(alipayResponse.data?.message || '生成二维码失败'))
|
||||
}
|
||||
|
||||
} else {
|
||||
console.error('创建支付订单失败:', createResponse)
|
||||
ElMessage.error(createResponse.data?.message || '创建支付订单失败')
|
||||
emit('pay-error', new Error(createResponse.data?.message || '创建支付订单失败'))
|
||||
}
|
||||
console.log('二维码图片URL已生成:', qrCodeUrl.value)
|
||||
|
||||
// 显示二维码
|
||||
showQrCode.value = true
|
||||
console.log('二维码已显示')
|
||||
|
||||
// 开始轮询支付状态
|
||||
console.log('=== 开始轮询支付状态 ===')
|
||||
startPaymentPolling(paymentId)
|
||||
} else {
|
||||
console.error('支付宝响应失败:', alipayResponse)
|
||||
ElMessage.error(alipayResponse.data?.message || '生成二维码失败')
|
||||
emit('pay-error', new Error(alipayResponse.data?.message || '生成二维码失败'))
|
||||
}
|
||||
}
|
||||
|
||||
// 处理PayPal支付
|
||||
const handlePayPalPayment = async () => {
|
||||
ElMessage.info('正在创建PayPal支付...')
|
||||
|
||||
// 从sessionStorage获取用户信息
|
||||
const userStr = sessionStorage.getItem('user')
|
||||
let username = 'guest'
|
||||
|
||||
if (userStr) {
|
||||
try {
|
||||
const user = JSON.parse(userStr)
|
||||
username = user.username || user.name || 'guest'
|
||||
} catch (e) {
|
||||
console.error('解析用户信息失败:', e)
|
||||
}
|
||||
}
|
||||
|
||||
const paymentData = {
|
||||
username: username,
|
||||
orderId: props.orderId,
|
||||
amount: props.amount.toString(),
|
||||
method: 'PAYPAL'
|
||||
}
|
||||
|
||||
const paymentId = currentPaymentId.value
|
||||
console.log('=== 开始PayPal支付流程 ===')
|
||||
console.log('支付数据:', paymentData)
|
||||
console.log('使用已创建的支付ID:', paymentId)
|
||||
|
||||
const response = await createPayPalPayment(paymentData)
|
||||
const response = await createPayPalPayment({ paymentId })
|
||||
console.log('PayPal支付响应:', response)
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
const paymentUrl = response.data.paymentUrl
|
||||
const paymentId = response.data.paymentId
|
||||
currentPaymentId.value = paymentId
|
||||
|
||||
console.log('PayPal支付URL:', paymentUrl)
|
||||
ElMessage.success('正在跳转到PayPal支付页面...')
|
||||
@@ -317,7 +299,8 @@ const startPaymentPolling = (paymentId) => {
|
||||
stopPaymentPolling()
|
||||
|
||||
let pollCount = 0
|
||||
const maxPolls = 60 // 最多轮询60次(10分钟,每10秒一次)
|
||||
const maxPolls = 200 // 最多轮询200次(10分钟,每3秒一次)
|
||||
const pollInterval = 3000 // 3秒轮询一次,更快响应支付成功
|
||||
|
||||
const poll = async () => {
|
||||
if (pollCount >= maxPolls) {
|
||||
@@ -329,13 +312,22 @@ const startPaymentPolling = (paymentId) => {
|
||||
try {
|
||||
console.log(`轮询支付状态 (${pollCount + 1}/${maxPolls}),支付ID:`, paymentId)
|
||||
const response = await getPaymentById(paymentId)
|
||||
console.log('轮询响应:', response.data)
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
const payment = response.data.data
|
||||
const status = payment.status
|
||||
console.log('支付状态:', status, '状态说明:', getStatusDescription(status))
|
||||
// 兼容枚举可能是字符串或对象的情况
|
||||
const rawStatus = payment.status
|
||||
// 更全面地解析状态:可能是字符串、对象、或者对象的属性
|
||||
let status = rawStatus
|
||||
if (typeof rawStatus === 'object' && rawStatus !== null) {
|
||||
status = rawStatus.name || rawStatus.value || rawStatus.toString()
|
||||
}
|
||||
// 转为大写以便比较
|
||||
const statusUpper = String(status).toUpperCase()
|
||||
console.log('支付状态原始值:', rawStatus, '类型:', typeof rawStatus, '解析后:', status, '大写:', statusUpper)
|
||||
|
||||
if (status === 'SUCCESS' || status === 'COMPLETED') {
|
||||
if (statusUpper === 'SUCCESS' || statusUpper === 'COMPLETED') {
|
||||
console.log('✅ 支付成功!支付数据:', payment)
|
||||
stopPaymentPolling()
|
||||
ElMessage.success('支付成功!')
|
||||
@@ -345,44 +337,42 @@ const startPaymentPolling = (paymentId) => {
|
||||
visible.value = false
|
||||
}, 2000)
|
||||
return
|
||||
} else if (status === 'FAILED' || status === 'CANCELLED') {
|
||||
} else if (statusUpper === 'FAILED' || statusUpper === 'CANCELLED') {
|
||||
console.log('支付失败或取消')
|
||||
stopPaymentPolling()
|
||||
ElMessage.warning('支付已取消或失败')
|
||||
emit('pay-error', new Error('支付已取消或失败'))
|
||||
return
|
||||
} else if (status === 'PROCESSING') {
|
||||
} else if (statusUpper === 'PROCESSING') {
|
||||
console.log('支付处理中...')
|
||||
// PROCESSING 状态继续轮询,但可以给用户提示
|
||||
if (pollCount % 6 === 0) { // 每60秒提示一次
|
||||
ElMessage.info('支付处理中,请稍候...')
|
||||
}
|
||||
} else if (status === 'PENDING') {
|
||||
// PROCESSING 状态继续轮询
|
||||
} else if (statusUpper === 'PENDING') {
|
||||
console.log('支付待处理中(等待支付宝回调)...')
|
||||
// PENDING 状态继续轮询
|
||||
if (pollCount % 6 === 0) { // 每60秒提示一次
|
||||
ElMessage.info('等待支付确认,请确保已完成支付...')
|
||||
}
|
||||
} else {
|
||||
console.log('未知状态:', statusUpper, '继续轮询...')
|
||||
}
|
||||
} else {
|
||||
console.warn('轮询响应失败:', response.data)
|
||||
}
|
||||
|
||||
// 继续轮询
|
||||
pollCount++
|
||||
paymentPollingTimer = setTimeout(poll, 10000) // 每10秒轮询一次
|
||||
paymentPollingTimer = setTimeout(poll, pollInterval)
|
||||
} catch (error) {
|
||||
console.error('轮询支付状态失败:', error)
|
||||
// 错误时也继续轮询,直到达到最大次数
|
||||
pollCount++
|
||||
if (pollCount < maxPolls) {
|
||||
paymentPollingTimer = setTimeout(poll, 10000)
|
||||
paymentPollingTimer = setTimeout(poll, pollInterval)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 开始轮询(等待5秒后开始第一次轮询)
|
||||
// 开始轮询(等待2秒后开始第一次轮询,更快响应)
|
||||
setTimeout(() => {
|
||||
poll()
|
||||
}, 5000)
|
||||
}, 2000)
|
||||
}
|
||||
|
||||
// 停止轮询支付状态
|
||||
@@ -394,6 +384,49 @@ const stopPaymentPolling = () => {
|
||||
}
|
||||
}
|
||||
|
||||
// 手动检查支付状态
|
||||
const manualCheckPayment = async () => {
|
||||
if (!currentPaymentId.value) {
|
||||
ElMessage.warning('请先生成支付二维码')
|
||||
return
|
||||
}
|
||||
|
||||
ElMessage.info('正在查询支付状态...')
|
||||
|
||||
try {
|
||||
const response = await getPaymentById(currentPaymentId.value)
|
||||
|
||||
if (response.data && response.data.success) {
|
||||
const payment = response.data.data
|
||||
// 兼容枚举可能是字符串或对象的情况
|
||||
const rawStatus = payment.status
|
||||
// 更全面地解析状态
|
||||
let status = rawStatus
|
||||
if (typeof rawStatus === 'object' && rawStatus !== null) {
|
||||
status = rawStatus.name || rawStatus.value || rawStatus.toString()
|
||||
}
|
||||
const statusUpper = String(status).toUpperCase()
|
||||
console.log('手动查询支付状态原始值:', rawStatus, '类型:', typeof rawStatus, '解析后:', status, '大写:', statusUpper)
|
||||
|
||||
if (statusUpper === 'SUCCESS' || statusUpper === 'COMPLETED') {
|
||||
stopPaymentPolling()
|
||||
ElMessage.success('支付成功!')
|
||||
emit('pay-success', payment)
|
||||
setTimeout(() => {
|
||||
visible.value = false
|
||||
}, 1500)
|
||||
} else if (statusUpper === 'FAILED' || statusUpper === 'CANCELLED') {
|
||||
ElMessage.warning('支付已取消或失败')
|
||||
} else {
|
||||
ElMessage.info(`支付尚未完成(状态:${statusUpper}),请完成支付后再试`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('查询支付状态失败:', error)
|
||||
ElMessage.error('查询失败,请稍后重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
const handleClose = () => {
|
||||
stopPaymentPolling()
|
||||
@@ -866,55 +899,21 @@ const showAgreement = () => {
|
||||
}
|
||||
|
||||
.qr-placeholder {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2a2a2a 100%);
|
||||
border-radius: 8px;
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
background: transparent;
|
||||
border-radius: 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.qr-placeholder::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border: 3px solid #4a9eff;
|
||||
border-radius: 8px;
|
||||
transform: translate(-50%, -50%);
|
||||
opacity: 0.6;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 0.6;
|
||||
transform: translate(-50%, -50%) scale(1);
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
transform: translate(-50%, -50%) scale(1.05);
|
||||
}
|
||||
}
|
||||
|
||||
.qr-placeholder::after {
|
||||
content: '扫码支付';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, calc(-50% + 40px));
|
||||
color: white !important;
|
||||
font-size: 12px !important;
|
||||
font-weight: 500 !important;
|
||||
opacity: 0.8 !important;
|
||||
line-height: 1.5 !important;
|
||||
letter-spacing: 0.3px !important;
|
||||
.qr-placeholder svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.qr-tip {
|
||||
@@ -1010,6 +1009,34 @@ const showAgreement = () => {
|
||||
color: #3a8bdf !important;
|
||||
}
|
||||
|
||||
/* 我已完成支付按钮 */
|
||||
.check-payment-btn {
|
||||
width: 100%;
|
||||
padding: 12px 24px;
|
||||
margin-top: 16px;
|
||||
background: linear-gradient(135deg, #10b981 0%, #059669 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
|
||||
}
|
||||
|
||||
.check-payment-btn:hover:not(:disabled) {
|
||||
background: linear-gradient(135deg, #059669 0%, #047857 100%);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4);
|
||||
}
|
||||
|
||||
.check-payment-btn:disabled {
|
||||
background: #4a4a4a;
|
||||
cursor: not-allowed;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.payment-modal :deep(.el-dialog) {
|
||||
|
||||
Reference in New Issue
Block a user