diff --git a/zhiyin-app/src/pages/interview/interview.vue b/zhiyin-app/src/pages/interview/interview.vue index 7a782c0..1dd218e 100644 --- a/zhiyin-app/src/pages/interview/interview.vue +++ b/zhiyin-app/src/pages/interview/interview.vue @@ -120,6 +120,7 @@ import { onLoad, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app' import { onLoad } from '@dcloudio/uni-app' // #endif import { api, API_ENDPOINTS } from '../../config' +import { checkAuth } from '../../utils/auth' import DigitalHuman from '../../components/digital-human.vue' // #ifdef MP-WEIXIN onShareAppMessage(() => ({ title: '职引 - AI模拟面试 | 数字人面试官实战练习', path: '/pages/interview/interview' })) @@ -238,6 +239,8 @@ const startInterview = async () => { messages.value.push({ role: 'ai', content: errMsg + ' 👉 购买后可继续面试' }) isComplete.value = true completedReason.value = 'noCredits' + } else if (checkAuth(res)) { + return // token 过期,已清除并跳转登录 } else { const msg = res.data?.message || '创建面试失败' messages.value.push({ role: 'ai', content: msg }) @@ -285,6 +288,8 @@ const sendAnswer = async () => { messages.value.push({ role: 'ai', content: errMsg + ' 👉 购买后可继续面试' }) isComplete.value = true completedReason.value = 'noCredits' + } else if (checkAuth(res)) { + return } else { 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) { aiAudioUrl.value = api(API_ENDPOINTS.TTS.AUDIO(synthRes.data.hash)) aiAmplitudeData.value = synthRes.data?.amplitudeData || [] + } else { + checkAuth(synthRes) } } catch {} } @@ -369,6 +376,8 @@ function startRecord() { uni.vibrateShort({ type: 'light' }) return } + } else if (checkAuth(uploadRes)) { + return } } catch (e) { console.error('[ASR] upload error:', e?.message || e) diff --git a/zhiyin-app/src/pages/member/member.vue b/zhiyin-app/src/pages/member/member.vue index 1414377..09ab760 100644 --- a/zhiyin-app/src/pages/member/member.vue +++ b/zhiyin-app/src/pages/member/member.vue @@ -113,6 +113,7 @@ import { ref, onMounted } from 'vue' import { onLoad, onShow } from '@dcloudio/uni-app' import { api } from '../../config' +import { checkAuth } from '../../utils/auth' const isLoggedIn = ref(false) const isMp = ref(false) @@ -177,6 +178,8 @@ const refreshState = async () => { plan.value = u.plan || 'free' gravity.value = u.gravity ?? 0 currentPlanName.value = ({ free: '免费版', growth: '成长版', sprint: '冲刺版' })[plan.value] || '免费版' + } else if (checkAuth(ures)) { + return // token 过期,已清除并跳转登录 } } catch (e) { /* silent */ } } @@ -284,6 +287,7 @@ const startGravityPay = async () => { }) } else { const msg = res.data?.message || res.data?.msg || `请求失败(${res.statusCode})` + if (checkAuth(res)) return payError.value = msg console.error('[VP] 订单创建失败:', res.statusCode, JSON.stringify(res.data)) } @@ -306,6 +310,7 @@ const startGravityPay = async () => { currentOutTradeNo.value = res.data.outTradeNo pollPayResult(res.data.outTradeNo, 'growth') } else { + if (checkAuth(res)) return payError.value = res.data?.message || '创建订单失败' } } catch (e) { @@ -354,6 +359,7 @@ const startPlanPay = async (selectedPlan) => { console.log('[VP-plan] 创建订单:', res.statusCode, JSON.stringify(res.data).slice(0, 500)) payLoading.value = false + if (checkAuth(res)) return if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.paySig) { const vp = res.data currentOutTradeNo.value = vp.outTradeNo @@ -403,6 +409,7 @@ const startPlanPay = async (selectedPlan) => { currentOutTradeNo.value = res.data.outTradeNo pollPayResult(res.data.outTradeNo, selectedPlan) } else { + if (checkAuth(res)) return payError.value = res.data?.message || '创建订单失败' } } catch (e) { @@ -423,6 +430,7 @@ const pollPayResult = (outTradeNo, selectedPlan) => { url: api(`/payment/check/${outTradeNo}`), method: 'GET', header: { 'Authorization': `Bearer ${token()}` }, }) + if (checkAuth(res)) return if (res.statusCode >= 200 && res.statusCode < 300 && res.data?.status === 'success') { await activatePlan(outTradeNo, selectedPlan) return @@ -450,6 +458,7 @@ const activatePlan = async (outTradeNo, selectedPlan) => { uni.showToast({ title: '🎉 开通成功!', icon: 'success' }) refreshState() } else { + if (checkAuth(res)) return uni.showToast({ title: res.data?.message || '激活失败', icon: 'none' }) } } catch (e) { diff --git a/zhiyin-app/src/pages/user/user.vue b/zhiyin-app/src/pages/user/user.vue index 142fd6c..bd6a8a2 100644 --- a/zhiyin-app/src/pages/user/user.vue +++ b/zhiyin-app/src/pages/user/user.vue @@ -170,6 +170,7 @@ import { onShow, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app' import { onShow } from '@dcloudio/uni-app' // #endif import { api } from '../../config' +import { checkAuth } from '../../utils/auth' // #ifdef MP-WEIXIN const shareCodeRef = ref('') @@ -216,7 +217,10 @@ async function preCreateShare() { data: { type: 'app', title: '我在AI磁场·职引练习面试', description: 'AI模拟面试+简历优化,快来一起提升吧' }, 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 if (data.shareCode) { shareCodeRef.value = data.shareCode @@ -231,6 +235,8 @@ const fetchUserInfo = async () => { if (res.statusCode === 200 && res.data) { userInfo.value = res.data uni.setStorageSync('userInfo', JSON.stringify(res.data)) + } else if (checkAuth(res)) { + return // token 过期,已清除并跳转登录 } } 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}` } }) 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 } + } else if (checkAuth(res)) { + return } } catch(e) { /* silent */ } } @@ -251,7 +259,11 @@ const loadMemberStatus = async () => { const loadStats = async () => { try { 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) } } diff --git a/zhiyin-app/src/utils/auth.ts b/zhiyin-app/src/utils/auth.ts new file mode 100644 index 0000000..420be86 --- /dev/null +++ b/zhiyin-app/src/utils/auth.ts @@ -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 +}