This commit is contained in:
2025-09-03 09:13:29 +08:00
parent 8d50c6dadf
commit acaed047ce
6 changed files with 550 additions and 31 deletions

View File

@@ -131,6 +131,15 @@ const routes = [
hideForAuth: true
}
},
{
path: '/payment',
name: 'Payment',
component: () => import('@/views/Payment.vue'),
meta: {
title: '支付激活',
requiresAuth: true
}
},
{
path: '/profile',
name: 'Profile',
@@ -357,6 +366,18 @@ router.beforeEach(async (to, from, next) => {
return
}
}
// 检查支付状态(管理员除外)
if (userStore.user && userStore.user.role !== 'admin' && userStore.user.payment_status === 'unpaid') {
// 如果当前不在支付页面,静默重定向到支付页面(不显示额外通知)
if (to.name !== 'Payment') {
next({
name: 'Payment',
query: { redirect: to.fullPath }
})
return
}
}
}
}

View File

@@ -41,15 +41,37 @@ export const useUserStore = defineStore('user', () => {
loading.value = true
const response = await api.post('/auth/login', credentials)
if (response.data.token) {
if (response.data.success && response.data.token) {
setToken(response.data.token)
setUser(response.data.user)
startStatusCheck() // 登录成功后开始状态检查
ElMessage.success(response.data.message || '登录成功')
return { success: true, data: response.data }
} else if (response.data.needPayment) {
// 用户需要支付激活,不显示错误消息,由前端页面处理
return {
success: false,
needPayment: true,
userId: response.data.userId,
message: response.data.message
}
} else {
const message = response.data.message || '登录失败'
ElMessage.error(message)
return { success: false, message }
}
} catch (error) {
const message = error.response?.data?.message || '登录失败'
const errorData = error.response?.data
if (errorData?.needPayment) {
// 处理403状态码返回的需要支付情况
return {
success: false,
needPayment: true,
userId: errorData.userId,
message: errorData.message
}
}
const message = errorData?.message || '登录失败'
ElMessage.error(message)
return { success: false, message }
} finally {
@@ -63,11 +85,28 @@ export const useUserStore = defineStore('user', () => {
loading.value = true
const response = await api.post('/auth/register', userData)
if (response.data.token) {
setToken(response.data.token)
setUser(response.data.user)
ElMessage.success(response.data.message || '注册成功')
return { success: true, data: response.data }
if (response.data.success) {
// 检查是否需要支付
if (response.data.needPayment) {
// 需要支付的情况,返回成功状态和支付相关信息
return {
success: true,
needPayment: true,
token: response.data.token,
user: response.data.user,
message: response.data.message
}
} else {
// 直接注册成功的情况
setToken(response.data.token)
setUser(response.data.user)
ElMessage.success(response.data.message || '注册成功')
return { success: true, data: response.data }
}
} else {
const message = response.data.message || '注册失败'
ElMessage.error(message)
return { success: false, message }
}
} catch (error) {
const message = error.response?.data?.message || '注册失败'

View File

@@ -88,6 +88,13 @@ api.interceptors.response.use(
delete api.defaults.headers.common['Authorization']
router.push({ name: 'Login' })
ElMessage.error(data.message || '账户已被拉黑,请联系管理员')
} else if (data.code === 'PAYMENT_REQUIRED') {
// 需要支付,跳转到支付页面
// 只在不是支付页面时才跳转和显示消息,避免重复通知
if (router.currentRoute.value.name !== 'Payment') {
router.push({ name: 'Payment' })
ElMessage.warning(data.message || '您的账户尚未激活,请完成支付后再使用')
}
} else {
ElMessage.error(data.message || '权限不足')
}

View File

@@ -179,6 +179,16 @@ const handleLogin = async () => {
// 登录成功,跳转到目标页面或转账管理
const redirectPath = route.query.redirect || '/transfers'
router.push(redirectPath)
} else if (result.needPayment) {
// 用户需要支付激活,直接跳转到支付页面
ElMessage.info('账户尚未激活,正在跳转到支付页面...')
router.push({
path: '/payment',
query: {
userId: result.userId,
from: 'login'
}
})
}
} catch (error) {
console.error('登录失败:', error)

446
src/views/Payment.vue Normal file
View File

@@ -0,0 +1,446 @@
<template>
<div class="payment-container">
<div class="payment-card">
<div class="payment-header">
<h2>完成支付激活账户</h2>
<p class="payment-desc">请完成支付以激活您的账户</p>
</div>
<div class="payment-info">
<div class="info-item">
<span class="label">用户名</span>
<span class="value">{{ userInfo.username }}</span>
</div>
<div class="info-item">
<span class="label">手机号</span>
<span class="value">{{ userInfo.phone }}</span>
</div>
<div class="info-item">
<span class="label">支付金额</span>
<span class="value price">¥{{ paymentAmount }}</span>
</div>
</div>
<div class="payment-methods">
<div class="method-item" :class="{ active: selectedMethod === 'wechat' }" @click="selectedMethod = 'wechat'">
<div class="method-icon">
<svg viewBox="0 0 24 24" width="24" height="24">
<path fill="#07C160" d="M8.5 6.5c-1.4 0-2.5 1.1-2.5 2.5s1.1 2.5 2.5 2.5 2.5-1.1 2.5-2.5-1.1-2.5-2.5-2.5zm7 0c-1.4 0-2.5 1.1-2.5 2.5s1.1 2.5 2.5 2.5 2.5-1.1 2.5-2.5-1.1-2.5-2.5-2.5zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8 0-1.12.23-2.18.64-3.15.41-.97 1.01-1.85 1.78-2.62.77-.77 1.65-1.37 2.62-1.78C9.82 4.23 10.88 4 12 4s2.18.23 3.15.64c.97.41 1.85 1.01 2.62 1.78.77.77 1.37 1.65 1.78 2.62.41.97.64 2.03.64 3.15 0 4.41-3.59 8-8 8z"/>
</svg>
</div>
<span class="method-name">微信支付</span>
<div class="method-check">
<el-icon v-if="selectedMethod === 'wechat'"><Check /></el-icon>
</div>
</div>
</div>
<div class="payment-actions">
<el-button size="large" @click="goBack">返回</el-button>
<el-button
type="primary"
size="large"
:loading="paymentLoading"
@click="handlePayment"
:disabled="!selectedMethod"
>
{{ paymentLoading ? '处理中...' : '立即支付' }}
</el-button>
</div>
<div class="payment-status" v-if="paymentStatus">
<div class="status-item" :class="paymentStatus">
<el-icon v-if="paymentStatus === 'success'"><SuccessFilled /></el-icon>
<el-icon v-else-if="paymentStatus === 'failed'"><CircleCloseFilled /></el-icon>
<el-icon v-else><Loading /></el-icon>
<span>{{ getStatusText() }}</span>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { ElMessage, ElMessageBox } from 'element-plus'
import { Check, SuccessFilled, CircleCloseFilled, Loading } from '@element-plus/icons-vue'
import { useUserStore } from '@/stores/user'
import api from '@/utils/api'
const route = useRoute()
const router = useRouter()
const userStore = useUserStore()
// 响应式数据
const selectedMethod = ref('wechat')
const paymentLoading = ref(false)
const paymentStatus = ref('')
const paymentAmount = ref(99) // 注册费用
const userInfo = reactive({
username: '',
phone: ''
})
// 获取用户信息
const getUserInfo = async () => {
try {
// 优先从用户store获取信息
if (userStore.user) {
userInfo.username = userStore.user.username
userInfo.phone = userStore.user.phone
return
}
// 如果store中没有用户信息尝试从URL参数获取
const token = route.query.token
const userId = route.query.userId
if (token && userId) {
// 设置临时token以获取用户信息
const response = await api.get(`/users/${userId}`, {
headers: {
Authorization: `Bearer ${token}`
}
})
if (response.data.success) {
userInfo.username = response.data.user.username
userInfo.phone = response.data.user.phone
}
} else {
// 如果没有URL参数尝试获取当前用户信息
const response = await api.get('/auth/me')
if (response.data.success) {
userInfo.username = response.data.user.username
userInfo.phone = response.data.user.phone
}
}
} catch (error) {
console.error('获取用户信息失败:', error)
ElMessage.error('获取用户信息失败')
}
}
// 处理支付
const handlePayment = async () => {
if (!selectedMethod.value) {
ElMessage.error('请选择支付方式')
return
}
paymentLoading.value = true
paymentStatus.value = 'processing'
try {
// 优先使用URL参数中的token和userId注册流程
let token = route.query.token
let userId = route.query.userId
// 如果没有URL参数使用当前登录用户信息
if (!token || !userId) {
token = userStore.token
userId = userStore.user?.id
}
if (!userId) {
throw new Error('无法获取用户信息,请重新登录')
}
// 创建支付订单
const response = await api.post('/wechat-pay/create-registration-order', {
userId: userId,
amount: paymentAmount.value,
description: '用户注册激活费用'
}, token ? {
headers: {
Authorization: `Bearer ${token}`
}
} : {})
if (response.data.success) {
const { h5_url, out_trade_no } = response.data.data
// 跳转到微信H5支付页面
window.location.href = h5_url
// 开始轮询支付状态
startPaymentStatusPolling(out_trade_no, token)
} else {
throw new Error(response.data.message || '创建支付订单失败')
}
} catch (error) {
console.error('支付失败:', error)
ElMessage.error(error.message || '支付失败,请重试')
paymentStatus.value = 'failed'
} finally {
paymentLoading.value = false
}
}
// 轮询支付状态
const startPaymentStatusPolling = (outTradeNo, token) => {
const pollInterval = setInterval(async () => {
try {
const response = await api.get(`/wechat-pay/query-status/${outTradeNo}`, token ? {
headers: {
Authorization: `Bearer ${token}`
}
} : {})
if (response.data.success) {
const status = response.data.data.trade_state
if (status === 'SUCCESS') {
clearInterval(pollInterval)
paymentStatus.value = 'success'
ElMessage.success('支付成功!账户已激活')
// 如果是已登录用户,刷新用户信息并跳转到主页
if (userStore.isAuthenticated) {
await userStore.checkAuth() // 刷新用户信息,更新支付状态
setTimeout(() => {
router.push('/transfers')
}, 2000)
} else {
// 如果是注册流程,跳转到登录页面
setTimeout(() => {
router.push('/login')
}, 2000)
}
} else if (status === 'CLOSED' || status === 'REVOKED' || status === 'PAYERROR') {
clearInterval(pollInterval)
paymentStatus.value = 'failed'
ElMessage.error('支付失败,请重试')
}
}
} catch (error) {
console.error('查询支付状态失败:', error)
}
}, 3000) // 每3秒查询一次
// 5分钟后停止轮询
setTimeout(() => {
clearInterval(pollInterval)
if (paymentStatus.value === 'processing') {
paymentStatus.value = 'timeout'
ElMessage.warning('支付超时,请检查支付状态或重新支付')
}
}, 300000)
}
// 获取状态文本
const getStatusText = () => {
switch (paymentStatus.value) {
case 'processing':
return '支付处理中...'
case 'success':
return '支付成功!'
case 'failed':
return '支付失败'
case 'timeout':
return '支付超时'
default:
return ''
}
}
// 返回上一页
const goBack = () => {
ElMessageBox.confirm(
'返回将取消当前支付,确定要返回吗?',
'确认返回',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}
).then(() => {
router.push('/register')
})
}
// 页面加载时获取用户信息
onMounted(() => {
getUserInfo()
})
</script>
<style scoped>
.payment-container {
min-height: 100vh;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.payment-card {
background: white;
border-radius: 16px;
padding: 40px;
width: 100%;
max-width: 500px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
}
.payment-header {
text-align: center;
margin-bottom: 30px;
}
.payment-header h2 {
color: #333;
margin-bottom: 8px;
font-size: 24px;
font-weight: 600;
}
.payment-desc {
color: #666;
font-size: 14px;
margin: 0;
}
.payment-info {
background: #f8f9fa;
border-radius: 12px;
padding: 20px;
margin-bottom: 30px;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.info-item:last-child {
margin-bottom: 0;
}
.info-item .label {
color: #666;
font-size: 14px;
}
.info-item .value {
color: #333;
font-weight: 500;
}
.info-item .price {
color: #e74c3c;
font-size: 18px;
font-weight: 600;
}
.payment-methods {
margin-bottom: 30px;
}
.method-item {
display: flex;
align-items: center;
padding: 16px;
border: 2px solid #e9ecef;
border-radius: 12px;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 12px;
}
.method-item:last-child {
margin-bottom: 0;
}
.method-item:hover {
border-color: #667eea;
background: #f8f9ff;
}
.method-item.active {
border-color: #667eea;
background: #f8f9ff;
}
.method-icon {
margin-right: 12px;
}
.method-icon svg {
width: 24px;
height: 24px;
}
.method-name {
flex: 1;
font-weight: 500;
color: #333;
}
.method-check {
color: #667eea;
}
.payment-actions {
display: flex;
gap: 16px;
margin-bottom: 20px;
}
.payment-actions .el-button {
flex: 1;
height: 48px;
font-size: 16px;
border-radius: 12px;
}
.payment-status {
text-align: center;
padding: 16px;
border-radius: 12px;
background: #f8f9fa;
}
.status-item {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
font-weight: 500;
}
.status-item.processing {
color: #3498db;
}
.status-item.success {
color: #27ae60;
}
.status-item.failed {
color: #e74c3c;
}
.status-item.timeout {
color: #f39c12;
}
@media (max-width: 768px) {
.payment-card {
padding: 24px;
margin: 16px;
}
.payment-header h2 {
font-size: 20px;
}
.payment-actions {
flex-direction: column;
}
}
</style>

View File

@@ -57,15 +57,7 @@
</div>
</el-form-item>
<el-form-item prop="registrationCode">
<el-input
v-model="registerForm.registrationCode"
placeholder="请输入激活码"
size="large"
:prefix-icon="Ticket"
clearable
/>
</el-form-item>
<el-form-item prop="city">
<el-select
@@ -213,7 +205,7 @@ import { ref, reactive, computed, onMounted } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { ElMessage, ElMessageBox } from 'element-plus'
import { User, Lock, Message, Edit, ChatDotRound, CreditCard, Ticket, Location } from '@element-plus/icons-vue'
import { User, Lock, Message, Edit, ChatDotRound, CreditCard, Location } from '@element-plus/icons-vue'
import Captcha from '@/components/Captcha.vue'
const router = useRouter()
@@ -228,7 +220,7 @@ const captchaRef = ref()
const registerForm = reactive({
username: '',
phone: '',
registrationCode: '',
city: '',
district_id: '',
password: '',
@@ -321,10 +313,7 @@ const registerRules = {
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的手机号', trigger: 'blur' }
],
registrationCode: [
{ required: true, message: '请输入激活码', trigger: 'blur' },
{ min: 6, message: '激活码长度不能少于6位', trigger: 'blur' }
],
smsCode: [
{ required: true, message: '请输入短信验证码', trigger: 'blur' },
{ pattern: /^\d{6}$/, message: '短信验证码为6位数字', trigger: 'blur' }
@@ -441,7 +430,6 @@ const handleRegister = async () => {
const registerData = {
username: registerForm.username,
phone: registerForm.phone,
registrationCode: registerForm.registrationCode,
city: registerForm.city,
district_id: registerForm.district_id,
password: registerForm.password,
@@ -453,8 +441,21 @@ const handleRegister = async () => {
const result = await userStore.register(registerData)
if (result.success) {
ElMessage.success('注册成功!请登录')
router.push('/login')
// 检查是否需要支付
if (result.needPayment) {
ElMessage.success('用户信息创建成功,请完成支付以激活账户')
// 跳转到支付页面
router.push({
path: '/payment',
query: {
token: result.token,
userId: result.user.id
}
})
} else {
ElMessage.success('注册成功!请登录')
router.push('/login')
}
}
} catch (error) {
console.error('注册失败:', error)
@@ -524,12 +525,7 @@ onMounted(() => {
router.push(redirectPath)
}
// 从URL参数中获取邀请码并自动填入
const inviteCode = route.query.code
if (inviteCode) {
registerForm.registrationCode = inviteCode
ElMessage.success('已自动填入邀请码')
}
// 获取地区数据
fetchRegions()