移动端支付已完成

This commit is contained in:
GeekMaster
2025-08-30 18:05:38 +08:00
parent 3c065b99fb
commit c83c88ef27
8 changed files with 186 additions and 143 deletions

View File

@@ -112,6 +112,7 @@ func (h *PaymentHandler) SyncOrders() error {
} }
for _, order := range orders { for _, order := range orders {
time.Sleep(time.Second * 1)
//超时15分钟的订单直接标记为已关闭 //超时15分钟的订单直接标记为已关闭
if time.Now().After(order.CreatedAt.Add(time.Minute * 15)) { if time.Now().After(order.CreatedAt.Add(time.Minute * 15)) {
h.DB.Model(&model.Order{}).Where("id", order.Id).Update("checked", true) h.DB.Model(&model.Order{}).Where("id", order.Id).Update("checked", true)

View File

@@ -131,7 +131,7 @@
cursor: pointer; cursor: pointer;
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
margin-bottom: 24px; margin-bottom: 24px;
border: 1px solid rgba(255, 255, 255, 0.1); border: 1px solid var(--theme-border-primary);
position: relative; position: relative;
.product-header { .product-header {
@@ -270,7 +270,7 @@
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 8px; gap: 8px;
padding: 12px 16px; padding: 10px 16px;
border: none; border: none;
border-radius: 12px; border-radius: 12px;
font-size: 14px; font-size: 14px;

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="form"> <div class="form px-3">
<div class="text-center" v-if="email !== ''">当前已绑定邮箱{{ email }}</div> <div class="text-center" v-if="email !== ''">当前已绑定邮箱{{ email }}</div>
<el-form label-position="top"> <el-form label-position="top">
<el-form-item label="邮箱地址"> <el-form-item label="邮箱地址">

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="form"> <div class="form px-3">
<div class="text-center" v-if="mobile !== ''">当前已绑手机号{{ mobile }}</div> <div class="text-center" v-if="mobile !== ''">当前已绑手机号{{ mobile }}</div>
<el-form label-position="top"> <el-form label-position="top">
<el-form-item label="手机号"> <el-form-item label="手机号">

View File

@@ -4,12 +4,12 @@
v-model="showDialog" v-model="showDialog"
:close-on-click-modal="true" :close-on-click-modal="true"
:show-close="true" :show-close="true"
style="max-width: 600px" style="max-width: 400px"
:before-close="close" :before-close="close"
title="修改密码" title="修改密码"
> >
<div class="form" id="password-form"> <div class="form px-3" id="password-form">
<el-form :model="form" label-width="120px"> <el-form :model="form" label-position="top">
<el-form-item label="原始密码"> <el-form-item label="原始密码">
<el-input v-model="form['old_pass']" type="password" /> <el-input v-model="form['old_pass']" type="password" />
</el-form-item> </el-form-item>

View File

@@ -10,7 +10,7 @@
:append-to-body="true" :append-to-body="true"
> >
<div class="form"> <div class="form">
<el-form :model="form" label-width="80px" label-position="left"> <el-form :model="form" label-width="80px" label-position="top">
<el-tabs v-model="form.type" class="demo-tabs"> <el-tabs v-model="form.type" class="demo-tabs">
<el-tab-pane label="手机号验证" name="mobile"> <el-tab-pane label="手机号验证" name="mobile">
<el-form-item label="手机号"> <el-form-item label="手机号">

View File

@@ -55,7 +55,7 @@
</div> </div>
<div class="product-title"> <div class="product-title">
<h3 class="name">{{ item.name }}</h3> <h3 class="name">{{ item.name }}</h3>
<p class="description">{{ item.description || '全模型通用算力' }}</p> <p class="description">算力值{{ item.power }}</p>
</div> </div>
</div> </div>
@@ -169,12 +169,12 @@ import RedeemVerify from '@/components/RedeemVerify.vue'
import UserOrder from '@/components/UserOrder.vue' import UserOrder from '@/components/UserOrder.vue'
import { checkSession, getSystemInfo } from '@/store/cache' import { checkSession, getSystemInfo } from '@/store/cache'
import { useSharedStore } from '@/store/sharedata' import { useSharedStore } from '@/store/sharedata'
import { closeLoading, showLoading } from '@/utils/dialog'
import { httpGet, httpPost } from '@/utils/http' import { httpGet, httpPost } from '@/utils/http'
import { isMobile } from '@/utils/libs'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import QRCode from 'qrcode' import QRCode from 'qrcode'
import { onMounted, ref, onUnmounted } from 'vue' import { onMounted, onUnmounted, ref } from 'vue'
import { showLoading, closeLoading } from '@/utils/dialog'
import { isMobile } from '@/utils/libs'
const list = ref([]) const list = ref([])
const vipImg = ref('/images/menu/member.png') const vipImg = ref('/images/menu/member.png')

View File

@@ -4,14 +4,6 @@
<!-- 产品套餐 --> <!-- 产品套餐 -->
<div class="products-section"> <div class="products-section">
<div class="text-center bg-[#7c3aed] text-white rounded-lg p-3 mb-4">充值套餐</div> <div class="text-center bg-[#7c3aed] text-white rounded-lg p-3 mb-4">充值套餐</div>
<!-- <div class="info-alert" v-if="vipInfoText">
<van-notice-bar
:text="vipInfoText"
color="#1989fa"
background="#ecf9ff"
:scrollable="false"
/>
</div> -->
<div class="products-grid" v-if="list.length > 0"> <div class="products-grid" v-if="list.length > 0">
<div v-for="item in list" :key="item.id" class="product-card"> <div v-for="item in list" :key="item.id" class="product-card">
@@ -24,40 +16,29 @@
<div class="product-info"> <div class="product-info">
<div class="price-info"> <div class="price-info">
<div class="original-price"> <div class="discount-price">
<span class="label">原价</span> <span class="label">商品价格</span>
<span class="price">{{ item.price }}</span> <span class="price">{{ item.price }}</span>
</div> </div>
<div class="discount-price">
<span class="label">优惠价</span>
<span class="price">{{ item.discount }}</span>
</div>
</div>
<div class="product-details"> <div class="product-details">
<div class="detail-item"> <div class="detail-item">
<span class="label">有效期</span> <span class="label">算力值</span>
<span class="value">{{ item.days > 0 ? item.days + '天' : '长期有效' }}</span> <span class="value">{{ item.power }}</span>
</div> </div>
<div class="detail-item">
<span class="label">算力值</span>
<span class="value">{{ item.power }}</span>
</div> </div>
</div> </div>
<div class="payment-methods"> <div class="payment-methods">
<div class="methods-grid"> <div class="methods-grid">
<van-button <button class="payment-btn wechat-btn" @click="wxPay(item)">
v-for="payWay in payWays" <i class="iconfont icon-wechat-pay"></i>
:key="payWay.pay_type" <span>微信支付</span>
size="small" </button>
:color="getPayButtonColor(payWay.pay_type)" <button class="payment-btn alipay-btn" @click="alipay(item)">
@click="pay(item, payWay)" <i class="iconfont icon-alipay"></i>
class="pay-button" <span>支付宝</span>
> </button>
<i class="iconfont" :class="getPayIcon(payWay.pay_type)"></i>
{{ getPayButtonText(payWay.pay_type) }}
</van-button>
</div> </div>
</div> </div>
</div> </div>
@@ -90,29 +71,27 @@
<!-- 支付弹窗 --> <!-- 支付弹窗 -->
<van-dialog <van-dialog
v-model="showPayDialog" :show="showPayDialog"
title="支付确认" :title="title"
:show-cancel-button="false" :show-cancel-button="false"
confirm-button-text="确认支付成功"
:close-on-click-overlay="false" :close-on-click-overlay="false"
@confirm="paySuccess"
> >
<div class="pay-dialog-content"> <div class="pay-dialog-content">
<div v-if="qrImg" class="qr-section"> <div v-if="qrImg" class="qr-section">
<p class="pay-tip">请使用微信扫码支付</p>
<div class="qr-container"> <div class="qr-container">
<van-image :src="qrImg" width="200" height="200" /> <van-image :src="qrImg" width="200" height="200" />
</div> </div>
<p class="pay-amount">{{ currentPrice }}</p> <p class="pay-amount">{{ currentPrice }}</p>
</div> <p class="pay-tip">支付成功之后点击确定按钮</p>
<div class="pay-actions">
<van-button type="success" @click="payCallback(true)">支付成功</van-button>
<van-button type="danger" @click="payCallback(false)">支付失败</van-button>
</div> </div>
</div> </div>
</van-dialog> </van-dialog>
<!-- 卡密兑换弹窗 --> <!-- 卡密兑换弹窗 -->
<van-dialog <van-dialog
v-model:show="showRedeemVerifyDialog" :show="showRedeemVerifyDialog"
title="卡密兑换" title="卡密兑换"
:show-cancel-button="false" :show-cancel-button="false"
:show-confirm-button="false" :show-confirm-button="false"
@@ -134,7 +113,7 @@ import { useSharedStore } from '@/store/sharedata'
import { httpGet, httpPost } from '@/utils/http' import { httpGet, httpPost } from '@/utils/http'
import QRCode from 'qrcode' import QRCode from 'qrcode'
import { showFailToast, showLoadingToast, showSuccessToast } from 'vant' import { showFailToast, showLoadingToast, showSuccessToast } from 'vant'
import { computed, onMounted, ref } from 'vue' import { onMounted, onUnmounted, ref } from 'vue'
// 响应式数据 // 响应式数据
const list = ref([]) const list = ref([])
@@ -143,7 +122,6 @@ const isLogin = ref(false)
const loading = ref(true) const loading = ref(true)
const loadingText = ref('加载中...') const loadingText = ref('加载中...')
const vipInfoText = ref('') const vipInfoText = ref('')
const payWays = ref([])
const userOrderKey = ref(0) const userOrderKey = ref(0)
// 弹窗控制 // 弹窗控制
@@ -154,53 +132,17 @@ const showPayDialog = ref(false)
const qrImg = ref('') const qrImg = ref('')
const currentPrice = ref(0) const currentPrice = ref(0)
const currentProduct = ref(null) const currentProduct = ref(null)
const currentPayWay = ref(null) const selectedPid = ref(0)
const orderTimeout = ref(1800)
const handler = ref(null)
const title = ref('')
const store = useSharedStore() const store = useSharedStore()
// 支付按钮颜色
const getPayButtonColor = (payType) => {
const colors = {
alipay: '#15A6E8',
wxpay: '#07C160',
qqpay: '#12B7F5',
paypal: '#0070BA',
jdpay: '#E1251B',
douyin: '#000000',
}
return colors[payType] || '#1989fa'
}
// 支付按钮图标
const getPayIcon = (payType) => {
const icons = {
alipay: 'icon-alipay',
wxpay: 'icon-wechat-pay',
qqpay: 'icon-qq',
paypal: 'icon-paypal',
jdpay: 'icon-jd-pay',
douyin: 'icon-douyin',
}
return icons[payType] || 'icon-money'
}
// 支付按钮文本
const getPayButtonText = (payType) => {
const texts = {
alipay: '支付宝',
wxpay: '微信支付',
qqpay: 'QQ钱包',
paypal: 'PayPal',
jdpay: '京东支付',
douyin: '抖音支付',
}
return texts[payType] || '支付'
}
// 初始化 // 初始化
onMounted(() => { onMounted(() => {
checkSession() checkSession()
.then((user) => { .then(() => {
isLogin.value = true isLogin.value = true
}) })
.catch(() => { .catch(() => {
@@ -222,64 +164,82 @@ onMounted(() => {
getSystemInfo() getSystemInfo()
.then((res) => { .then((res) => {
vipInfoText.value = res.data['vip_info_text'] vipInfoText.value = res.data['vip_info_text']
if (res.data['order_pay_timeout'] > 0) {
orderTimeout.value = res.data['order_pay_timeout']
}
}) })
.catch((e) => { .catch((e) => {
console.error('获取系统配置失败:', e.message) console.error('获取系统配置失败:', e.message)
}) })
// 获取支付方式
httpGet('/api/payment/payWays')
.then((res) => {
payWays.value = res.data
})
.catch((e) => {
showFailToast('获取支付方式失败:' + e.message)
})
}) })
// 支付处理 // 支付处理
const pay = (product, payWay) => { const wxPay = (product) => {
if (!isLogin.value) { if (!isLogin.value) {
store.setShowLoginDialog(true) store.setShowLoginDialog(true)
return return
} }
selectedPid.value = product.id
currentProduct.value = product currentProduct.value = product
currentPayWay.value = payWay currentPrice.value = Number(product.price)
currentPrice.value = Number(product.discount) title.value = '请打开微信扫码支付'
showLoadingToast({ showLoadingToast({
message: '正在生成支付订单...', message: '正在生成支付订单...',
forbidClick: true, forbidClick: true,
}) })
let host = import.meta.env.VITE_API_HOST // 生成支付订单
if (host === '') { GenerateOrder('wxpay')
host = `${location.protocol}//${location.host}` }
const alipay = (product) => {
if (!isLogin.value) {
store.setShowLoginDialog(true)
return
} }
httpPost(`${import.meta.env.VITE_API_HOST}/api/payment/doPay`, { selectedPid.value = product.id
product_id: product.id, currentProduct.value = product
pay_way: payWay.pay_way, currentPrice.value = Number(product.price)
pay_type: payWay.pay_type, title.value = '请打开支付宝扫码支付'
user_id: 0, // 移除用户ID依赖
host: host, showLoadingToast({
device: 'mobile', message: '正在生成支付订单...',
forbidClick: true,
})
// 生成支付订单
GenerateOrder('alipay')
}
function GenerateOrder(payWay) {
// 生成支付订单
httpPost('/api/payment/create', {
pid: selectedPid.value,
pay_way: payWay,
domain: `${window.location.protocol}//${window.location.host}`,
device: 'pc',
}) })
.then((res) => { .then((res) => {
if (payWay.pay_way === 'wechat') { if (res.data.pay_url) {
// 生成二维码 // 生成二维码
QRCode.toDataURL(res.data, { width: 200, height: 200, margin: 2 }, (error, url) => { QRCode.toDataURL(res.data.pay_url, { width: 200, height: 200, margin: 2 }, (error, url) => {
if (error) { if (error) {
showFailToast('生成二维码失败') showFailToast('生成二维码失败')
} else { } else {
qrImg.value = url qrImg.value = url
showPayDialog.value = true showPayDialog.value = true
// 开始查询订单状态
if (handler.value) {
clearTimeout(handler.value)
}
handler.value = setTimeout(() => queryOrder(res.data.order_no), 3000)
} }
}) })
} else { } else {
// 跳转支付 showFailToast('支付链接生成失败')
window.open(res.data, '_blank')
} }
}) })
.catch((e) => { .catch((e) => {
@@ -287,25 +247,47 @@ const pay = (product, payWay) => {
}) })
} }
// 支付回调 // 查询订单状态
const payCallback = (success) => { const queryOrder = async (orderNo) => {
try {
const res = await httpGet('/api/order/query?order_no=' + orderNo)
if (res?.data.status === 2) {
paySuccess(true)
} else {
// 继续查询,但设置最大查询次数
const maxQueries = Math.floor(orderTimeout.value / 3) // 每3秒查询一次
if (handler.value && maxQueries > 0) {
handler.value = setTimeout(() => queryOrder(orderNo), 3000)
} else {
// 查询超时
showFailToast('支付超时,请重新发起支付')
showPayDialog.value = false
qrImg.value = ''
}
}
} catch (error) {
console.error('查询订单状态失败:', error)
// 继续查询,但设置最大重试次数
if (handler.value) {
handler.value = setTimeout(() => queryOrder(orderNo), 3000)
}
}
}
const paySuccess = () => {
showPayDialog.value = false showPayDialog.value = false
qrImg.value = '' showSuccessToast('支付成功!')
clearTimeout(handler.value)
if (success) { userOrderKey.value += 1
showSuccessToast('支付成功!')
userOrderKey.value += 1
}
} }
// 卡密兑换回调 // 组件卸载时清理定时器
const redeemCallback = (success) => { onUnmounted(() => {
showRedeemVerifyDialog.value = false if (handler.value) {
clearTimeout(handler.value)
if (success) { handler.value = null
showSuccessToast('卡密兑换成功!')
} }
} })
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -456,14 +438,46 @@ const redeemCallback = (success) => {
.methods-grid { .methods-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 8px; gap: 12px;
.pay-button { .payment-btn {
height: 36px; height: 44px;
font-size: 12px; border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.iconfont { .iconfont {
margin-right: 4px; font-size: 18px;
}
&.wechat-btn {
background: #07c160;
color: white;
&:hover {
background: #06ad56;
}
}
&.alipay-btn {
background: #15a6e8;
color: white;
&:hover {
background: #1395d1;
}
} }
} }
} }
@@ -493,16 +507,42 @@ const redeemCallback = (success) => {
font-size: 16px; font-size: 16px;
color: var(--van-text-color); color: var(--van-text-color);
margin-bottom: 16px; margin-bottom: 16px;
font-weight: 500;
} }
.qr-container { .qr-container {
margin-bottom: 12px; margin-bottom: 12px;
padding: 16px;
background: #f8f9fa;
border-radius: 12px;
display: inline-block;
} }
.pay-amount { .pay-amount {
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
color: #ff6b35; color: #ff6b35;
margin-bottom: 16px;
}
.pay-status {
margin-top: 15px;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px;
background: #f0f9ff;
border-radius: 8px;
border: 1px solid #e0f2fe;
.success-status {
display: flex;
align-items: center;
gap: 8px;
color: #07c160;
font-weight: 500;
}
} }
} }
@@ -510,6 +550,8 @@ const redeemCallback = (success) => {
display: flex; display: flex;
gap: 12px; gap: 12px;
justify-content: center; justify-content: center;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
} }
} }
} }