fix: token 过期时前端自动清除登录态并跳转登录

根因: 页面使用 uni.request() 直接请求,401 时只有静默 catch,
不清除本地 token,导致界面显示已登录但后端拒接。

修复:
- 新增 utils/auth.ts: clearAuth() + checkAuth() 统一处理 401
- member.vue: refreshState / startGravityPay / startPlanPay /
  pollPayResult / activatePlan 全部添加 401 检测
- user.vue: fetchUserInfo / loadMemberStatus / loadStats /
  preCreateShare 添加 401 检测
- interview.vue: startInterview / sendAnswer / speakAiText /
  startRecord 添加 401 检测
This commit is contained in:
yuzhiran
2026-07-02 12:52:43 +08:00
parent b8aaa51eb1
commit f955ecc71d
4 changed files with 56 additions and 2 deletions
@@ -120,6 +120,7 @@ import { onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { onLoad } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app'
// #endif // #endif
import { api, API_ENDPOINTS } from '../../config' import { api, API_ENDPOINTS } from '../../config'
import { checkAuth } from '../../utils/auth'
import DigitalHuman from '../../components/digital-human.vue' import DigitalHuman from '../../components/digital-human.vue'
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
onShareAppMessage(() => ({ title: '职引 - AI模拟面试 | 数字人面试官实战练习', path: '/pages/interview/interview' })) onShareAppMessage(() => ({ title: '职引 - AI模拟面试 | 数字人面试官实战练习', path: '/pages/interview/interview' }))
@@ -238,6 +239,8 @@ const startInterview = async () => {
messages.value.push({ role: 'ai', content: errMsg + ' 👉 购买后可继续面试' }) messages.value.push({ role: 'ai', content: errMsg + ' 👉 购买后可继续面试' })
isComplete.value = true isComplete.value = true
completedReason.value = 'noCredits' completedReason.value = 'noCredits'
} else if (checkAuth(res)) {
return // token 过期,已清除并跳转登录
} else { } else {
const msg = res.data?.message || '创建面试失败' const msg = res.data?.message || '创建面试失败'
messages.value.push({ role: 'ai', content: msg }) messages.value.push({ role: 'ai', content: msg })
@@ -285,6 +288,8 @@ const sendAnswer = async () => {
messages.value.push({ role: 'ai', content: errMsg + ' 👉 购买后可继续面试' }) messages.value.push({ role: 'ai', content: errMsg + ' 👉 购买后可继续面试' })
isComplete.value = true isComplete.value = true
completedReason.value = 'noCredits' completedReason.value = 'noCredits'
} else if (checkAuth(res)) {
return
} else { } else {
messages.value.push({ role: 'ai', content: res.data?.message || '回答提交失败' }) messages.value.push({ role: 'ai', content: res.data?.message || '回答提交失败' })
} }
@@ -311,6 +316,8 @@ async function speakAiText(text, ttsHash, ttsAmplitude) {
if (synthRes.statusCode === 200 && synthRes.data?.hash) { if (synthRes.statusCode === 200 && synthRes.data?.hash) {
aiAudioUrl.value = api(API_ENDPOINTS.TTS.AUDIO(synthRes.data.hash)) aiAudioUrl.value = api(API_ENDPOINTS.TTS.AUDIO(synthRes.data.hash))
aiAmplitudeData.value = synthRes.data?.amplitudeData || [] aiAmplitudeData.value = synthRes.data?.amplitudeData || []
} else {
checkAuth(synthRes)
} }
} catch {} } catch {}
} }
@@ -369,6 +376,8 @@ function startRecord() {
uni.vibrateShort({ type: 'light' }) uni.vibrateShort({ type: 'light' })
return return
} }
} else if (checkAuth(uploadRes)) {
return
} }
} catch (e) { } catch (e) {
console.error('[ASR] upload error:', e?.message || e) console.error('[ASR] upload error:', e?.message || e)
+9
View File
@@ -113,6 +113,7 @@
import { ref, onMounted } from 'vue' import { ref, onMounted } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app' import { onLoad, onShow } from '@dcloudio/uni-app'
import { api } from '../../config' import { api } from '../../config'
import { checkAuth } from '../../utils/auth'
const isLoggedIn = ref(false) const isLoggedIn = ref(false)
const isMp = ref(false) const isMp = ref(false)
@@ -177,6 +178,8 @@ const refreshState = async () => {
plan.value = u.plan || 'free' plan.value = u.plan || 'free'
gravity.value = u.gravity ?? 0 gravity.value = u.gravity ?? 0
currentPlanName.value = ({ free: '免费版', growth: '成长版', sprint: '冲刺版' })[plan.value] || '免费版' currentPlanName.value = ({ free: '免费版', growth: '成长版', sprint: '冲刺版' })[plan.value] || '免费版'
} else if (checkAuth(ures)) {
return // token 过期,已清除并跳转登录
} }
} catch (e) { /* silent */ } } catch (e) { /* silent */ }
} }
@@ -284,6 +287,7 @@ const startGravityPay = async () => {
}) })
} else { } else {
const msg = res.data?.message || res.data?.msg || `请求失败(${res.statusCode})` const msg = res.data?.message || res.data?.msg || `请求失败(${res.statusCode})`
if (checkAuth(res)) return
payError.value = msg payError.value = msg
console.error('[VP] 订单创建失败:', res.statusCode, JSON.stringify(res.data)) console.error('[VP] 订单创建失败:', res.statusCode, JSON.stringify(res.data))
} }
@@ -306,6 +310,7 @@ const startGravityPay = async () => {
currentOutTradeNo.value = res.data.outTradeNo currentOutTradeNo.value = res.data.outTradeNo
pollPayResult(res.data.outTradeNo, 'growth') pollPayResult(res.data.outTradeNo, 'growth')
} else { } else {
if (checkAuth(res)) return
payError.value = res.data?.message || '创建订单失败' payError.value = res.data?.message || '创建订单失败'
} }
} catch (e) { } catch (e) {
@@ -354,6 +359,7 @@ const startPlanPay = async (selectedPlan) => {
console.log('[VP-plan] 创建订单:', res.statusCode, JSON.stringify(res.data).slice(0, 500)) console.log('[VP-plan] 创建订单:', res.statusCode, JSON.stringify(res.data).slice(0, 500))
payLoading.value = false payLoading.value = false
if (checkAuth(res)) return
if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.paySig) { if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.paySig) {
const vp = res.data const vp = res.data
currentOutTradeNo.value = vp.outTradeNo currentOutTradeNo.value = vp.outTradeNo
@@ -403,6 +409,7 @@ const startPlanPay = async (selectedPlan) => {
currentOutTradeNo.value = res.data.outTradeNo currentOutTradeNo.value = res.data.outTradeNo
pollPayResult(res.data.outTradeNo, selectedPlan) pollPayResult(res.data.outTradeNo, selectedPlan)
} else { } else {
if (checkAuth(res)) return
payError.value = res.data?.message || '创建订单失败' payError.value = res.data?.message || '创建订单失败'
} }
} catch (e) { } catch (e) {
@@ -423,6 +430,7 @@ const pollPayResult = (outTradeNo, selectedPlan) => {
url: api(`/payment/check/${outTradeNo}`), method: 'GET', url: api(`/payment/check/${outTradeNo}`), method: 'GET',
header: { 'Authorization': `Bearer ${token()}` }, header: { 'Authorization': `Bearer ${token()}` },
}) })
if (checkAuth(res)) return
if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.status === 'success') { if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.status === 'success') {
await activatePlan(outTradeNo, selectedPlan) await activatePlan(outTradeNo, selectedPlan)
return return
@@ -450,6 +458,7 @@ const activatePlan = async (outTradeNo, selectedPlan) => {
uni.showToast({ title: '🎉 开通成功!', icon: 'success' }) uni.showToast({ title: '🎉 开通成功!', icon: 'success' })
refreshState() refreshState()
} else { } else {
if (checkAuth(res)) return
uni.showToast({ title: res.data?.message || '激活失败', icon: 'none' }) uni.showToast({ title: res.data?.message || '激活失败', icon: 'none' })
} }
} catch (e) { } catch (e) {
+14 -2
View File
@@ -170,6 +170,7 @@ import { onShow, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app'
import { onShow } from '@dcloudio/uni-app' import { onShow } from '@dcloudio/uni-app'
// #endif // #endif
import { api } from '../../config' import { api } from '../../config'
import { checkAuth } from '../../utils/auth'
// #ifdef MP-WEIXIN // #ifdef MP-WEIXIN
const shareCodeRef = ref('') const shareCodeRef = ref('')
@@ -216,7 +217,10 @@ async function preCreateShare() {
data: { type: 'app', title: '我在AI磁场·职引练习面试', description: 'AI模拟面试+简历优化,快来一起提升吧' }, data: { type: 'app', title: '我在AI磁场·职引练习面试', description: 'AI模拟面试+简历优化,快来一起提升吧' },
header: { Authorization: `Bearer ${token.value}` }, header: { Authorization: `Bearer ${token.value}` },
}) })
if (res.statusCode < 200 || res.statusCode >= 300) return if (res.statusCode < 200 || res.statusCode >= 300) {
checkAuth(res)
return
}
const data = res.data?.data || res.data const data = res.data?.data || res.data
if (data.shareCode) { if (data.shareCode) {
shareCodeRef.value = data.shareCode shareCodeRef.value = data.shareCode
@@ -231,6 +235,8 @@ const fetchUserInfo = async () => {
if (res.statusCode === 200 && res.data) { if (res.statusCode === 200 && res.data) {
userInfo.value = res.data userInfo.value = res.data
uni.setStorageSync('userInfo', JSON.stringify(res.data)) uni.setStorageSync('userInfo', JSON.stringify(res.data))
} else if (checkAuth(res)) {
return // token 过期,已清除并跳转登录
} }
} catch(e) { /* silent */ } } catch(e) { /* silent */ }
} }
@@ -244,6 +250,8 @@ const loadMemberStatus = async () => {
const res = await uni.request({ url: api('/member/status'), method: 'GET', header: { 'Authorization': `Bearer ${token.value}` } }) const res = await uni.request({ url: api('/member/status'), method: 'GET', header: { 'Authorization': `Bearer ${token.value}` } })
if (res.statusCode >= 200 && res.statusCode < 300 && res.data) { if (res.statusCode >= 200 && res.statusCode < 300 && res.data) {
memberInfo.value = { plan: res.data.plan || 'free', planName: res.data.planName || '免费版', remaining: res.data.remaining ?? 0, gravity: res.data.gravity ?? 0 } memberInfo.value = { plan: res.data.plan || 'free', planName: res.data.planName || '免费版', remaining: res.data.remaining ?? 0, gravity: res.data.gravity ?? 0 }
} else if (checkAuth(res)) {
return
} }
} catch(e) { /* silent */ } } catch(e) { /* silent */ }
} }
@@ -251,7 +259,11 @@ const loadMemberStatus = async () => {
const loadStats = async () => { const loadStats = async () => {
try { try {
const res = await uni.request({ url: api('/interview/stats/mine'), method: 'GET', header: { 'Authorization': `Bearer ${token.value}` } }) const res = await uni.request({ url: api('/interview/stats/mine'), method: 'GET', header: { 'Authorization': `Bearer ${token.value}` } })
if (res.statusCode === 200) stats.value = res.data if (res.statusCode === 200) {
stats.value = res.data
} else if (checkAuth(res)) {
return
}
} catch(e) { console.error(e) } } catch(e) { console.error(e) }
} }
+24
View File
@@ -0,0 +1,24 @@
/**
* 清除登录状态 —— token 过期 / 未授权时调用
*/
export function clearAuth() {
uni.removeStorageSync('token')
uni.removeStorageSync('userInfo')
uni.showToast({ title: '登录已过期,请重新登录', icon: 'none' })
// 延迟跳转,避免和当前页面操作冲突
setTimeout(() => {
uni.navigateTo({ url: '/pages/login/login' })
}, 800)
}
/**
* 检查 uni.request 的响应是否为 401,是则自动清除登录态
* 返回 true 表示已处理 401(调用方应停止后续逻辑)
*/
export function checkAuth(res: any): boolean {
if (res.statusCode === 401) {
clearAuth()
return true
}
return false
}