# Store 登录状态管理增强说明 ## 📋 问题背景 在多设备登录踢出场景中,仅依赖 API 拦截器**被动响应**是不够的,Store 层面也需要: 1. **主动检查**登录状态是否有效 2. **标记状态**区分正常登出和被踢出 3. **提供方法**供其他组件调用检查 --- ## ✅ 已实现的增强功能 ### 1. **新增状态字段** ```javascript export const userStore = reactive({ user: null, isLoggedIn: false, isKickedOut: false, // 🆕 标记是否被其他设备踢出 lastCheckTime: null, // 🆕 最后一次检查登录状态的时间 // ... }) ``` ### 2. **增强 `logout()` 方法** 支持标记被踢出状态: ```javascript // 正常登出 userStore.logout() // 被踢出时登出 userStore.logout(true) // 会标记 isKickedOut = true ``` **实现代码:** ```javascript logout(isKickedOut = false) { // 如果是被踢出,标记状态 if (isKickedOut) { this.isKickedOut = true console.log('[安全] 账号在其他设备登录,当前会话已被踢出') } this.user = null this.isLoggedIn = false this.lastCheckTime = null // 清除本地存储... } ``` ### 3. **增强 `adminLogout()` 方法** 同样支持标记被踢出: ```javascript adminLogout(isKickedOut = false) { if (isKickedOut) { this.isKickedOut = true console.log('[安全] 管理员账号在其他设备登录,当前会话已被踢出') } // 清除状态和存储... } ``` ### 4. **新增 `checkLoginStatus()` 方法** 🔥 主动检查登录状态是否仍然有效: ```javascript async checkLoginStatus() { // 1️⃣ 如果没有登录,直接返回 if (!this.isLoggedIn) { return { valid: false, reason: 'not_logged_in' } } // 2️⃣ 避免频繁检查(10秒内只检查一次) const now = Date.now() if (this.lastCheckTime && (now - this.lastCheckTime) < 10000) { return { valid: true, reason: 'recently_checked' } } try { // 3️⃣ 调用后端接口验证登录状态 const response = await lotteryApi.getLoginUser() if (response.success === true) { // ✅ 登录状态有效 this.lastCheckTime = now this.isKickedOut = false return { valid: true, reason: 'verified' } } else { // ❌ 登录状态无效 const message = response.message || '' // 检查是否是被踢出 if (message.includes('其他设备登录') || message.includes('当前会话已失效')) { this.logout(true) // 标记为被踢出 return { valid: false, reason: 'kicked_out', message } } else { this.logout(false) return { valid: false, reason: 'session_expired', message } } } } catch (error) { // ⚠️ 网络错误等情况,不清除登录状态,让用户继续使用 console.error('[Store] 检查登录状态失败:', error) return { valid: true, reason: 'check_failed', error } } } ``` ### 5. **新增 `resetKickedOutStatus()` 方法** 用于重新登录后重置被踢出状态: ```javascript resetKickedOutStatus() { this.isKickedOut = false } ``` ### 6. **增强 `login()` 方法** 登录时自动重置被踢出状态: ```javascript login(userInfo) { // ... 设置用户信息 this.isLoggedIn = true this.isKickedOut = false // 🆕 重置被踢出状态 this.lastCheckTime = Date.now() // 🆕 记录登录时间 // 保存到本地存储... } ``` --- ## 🎯 使用场景 ### 场景 1:组件中主动检查登录状态 ```javascript // 在关键操作前检查登录状态 async function performCriticalAction() { const status = await userStore.checkLoginStatus() if (!status.valid) { if (status.reason === 'kicked_out') { ElMessage.warning('您的账号已在其他设备登录') // 跳转到登录页 router.push('/login') } else { ElMessage.error('登录已过期,请重新登录') router.push('/login') } return } // 继续执行操作 // ... } ``` ### 场景 2:路由守卫中使用 ```javascript // router/index.js router.beforeEach(async (to, from, next) => { if (to.meta.requiresAuth) { const status = await userStore.checkLoginStatus() if (!status.valid) { next('/login') return } } next() }) ``` ### 场景 3:定期心跳检测(可选) ```javascript // 在 App.vue 中设置定期检查 import { onMounted, onUnmounted } from 'vue' import { userStore } from '@/store/user' let heartbeatTimer = null onMounted(() => { // 每 5 分钟检查一次登录状态 heartbeatTimer = setInterval(async () => { if (userStore.isLoggedIn) { await userStore.checkLoginStatus() } }, 5 * 60 * 1000) // 5分钟 }) onUnmounted(() => { if (heartbeatTimer) { clearInterval(heartbeatTimer) } }) ``` ### 场景 4:判断被踢出状态 ```javascript // 在登录页面显示提示 if (userStore.isKickedOut) { ElMessage.warning({ message: '您的账号已在其他设备登录,请重新登录', duration: 5000 }) userStore.resetKickedOutStatus() // 重置状态 } ``` --- ## 🔄 完整流程 ### 被动检测(API 拦截器) ``` 用户操作 → API 请求 → 后端检测 token 不一致 → 返回错误 ↓ API 拦截器捕获 → 调用 userStore.logout(true) ↓ 标记 isKickedOut = true → 显示提示 → 跳转登录页 ``` ### 主动检测(Store 方法) ``` 组件调用 checkLoginStatus() ↓ 10秒内检查过? ├─ 是 → 返回缓存结果 └─ 否 → 调用后端 API ↓ token 有效? ├─ 是 → 更新检查时间,返回 valid: true └─ 否 → 检查错误类型 ├─ 被踢出 → logout(true), 返回 kicked_out └─ 其他 → logout(false), 返回 session_expired ``` --- ## 🛡️ 防护机制 ### 1. **防止频繁检查** - 10 秒内只检查一次 - 避免给后端造成压力 ### 2. **容错处理** - 网络错误时不清除登录状态 - 让用户继续使用,避免误判 ### 3. **状态标记** - 明确区分正常登出和被踢出 - 便于显示不同的提示信息 ### 4. **自动恢复** - 登录时自动重置被踢出状态 - 提供手动重置方法 --- ## 📊 返回值说明 `checkLoginStatus()` 方法返回对象: ```typescript interface CheckResult { valid: boolean // 登录状态是否有效 reason: string // 原因 message?: string // 错误消息(可选) error?: Error // 错误对象(可选) } ``` ### Reason 枚举值: | Reason | 说明 | valid | |--------|------|-------| | `not_logged_in` | 未登录 | false | | `recently_checked` | 10秒内已检查过 | true | | `verified` | 后端验证通过 | true | | `kicked_out` | 被其他设备踢出 | false | | `session_expired` | 会话过期 | false | | `check_failed` | 检查失败(网络错误等) | true | --- ## ✅ 配合 API 拦截器 API 拦截器已更新,会在检测到被踢出时调用: ```javascript // src/api/index.js userStore.logout(true) // 前台用户 userStore.adminLogout(true) // 后台管理员 ``` 这样确保了**被动响应**和**主动检查**两种机制的完美配合。 --- ## 🎨 最佳实践 ### ✅ 推荐 1. **关键操作前检查**:支付、提交表单等操作前主动检查 2. **路由守卫检查**:切换到需要登录的页面时检查 3. **显示友好提示**:根据 `isKickedOut` 显示不同的提示信息 4. **定期心跳检测**:长时间停留在页面时定期检查(可选) ### ❌ 避免 1. **过度频繁检查**:利用 10 秒缓存机制,避免每次操作都检查 2. **忽略返回值**:检查失败时要及时处理,引导用户登录 3. **混淆状态**:不区分正常登出和被踢出,提示信息不明确 --- ## 🔧 相关文件 - ✅ `src/store/user.js` - Store 状态管理(已增强) - ✅ `src/api/index.js` - API 拦截器(已更新) - 📄 `前端多设备登录处理方案.md` - 总体方案说明 --- ## 🎉 总结 通过在 Store 层面添加: - ✅ 被踢出状态标记 `isKickedOut` - ✅ 主动检查方法 `checkLoginStatus()` - ✅ 增强的登出方法 `logout(isKickedOut)` - ✅ 防频繁检查机制(10秒缓存) 实现了**被动响应 + 主动检查**的双重保障机制,确保多设备登录场景下的安全性和用户体验!