feat: 添加任务状态级联触发器,优化支付和做同款功能

主要更新:
- 添加 MySQL 触发器实现 task_status 表到其他表的状态级联
- 移除控制器中的多表状态检查代码
- 完善做同款功能,支持参数传递
- 支付宝 USD 转 CNY 汇率转换
- 修复状态枚举映射问题

注意: 触发器仅在 task_status 更新时触发,部分代码仍直接更新业务表
This commit is contained in:
AIGC Developer
2025-12-08 13:54:02 +08:00
parent 624d560fb4
commit 3c37006ebd
84 changed files with 5325 additions and 1668 deletions

View File

@@ -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) {