From 464e2decf05129b7b6f3264266a7c7bc6787a52a Mon Sep 17 00:00:00 2001 From: feige996 <1020102647@qq.com> Date: Fri, 22 Aug 2025 17:08:40 +0800 Subject: [PATCH] =?UTF-8?q?feat(token):=20=E6=B7=BB=E5=8A=A0token=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E5=88=A4=E6=96=AD=E5=92=8C=E8=BF=87=E6=9C=9F=E5=A4=84?= =?UTF-8?q?=E7=90=86=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加单/双token类型判断函数 实现token过期时间计算和存储 增加token有效性检查和自动刷新功能 完善登录/登出/刷新token的错误处理 --- src/api/types/login.ts | 18 ++++ src/store/token.ts | 232 +++++++++++++++++++++++++++++++++++------ 2 files changed, 217 insertions(+), 33 deletions(-) diff --git a/src/api/types/login.ts b/src/api/types/login.ts index 5b1af02..d703fd8 100644 --- a/src/api/types/login.ts +++ b/src/api/types/login.ts @@ -77,3 +77,21 @@ export interface IUpdatePassword { newPassword: string confirmPassword: string } + +/** + * 判断是否为单Token响应 + * @param tokenRes 登录响应数据 + * @returns 是否为单Token响应 + */ +export function isSingleTokenRes(tokenRes: IAuthLoginRes): tokenRes is ISingleTokenRes { + return 'token' in tokenRes && !('refreshToken' in tokenRes) +} + +/** + * 判断是否为双Token响应 + * @param tokenRes 登录响应数据 + * @returns 是否为双Token响应 + */ +export function isDoubleTokenRes(tokenRes: IAuthLoginRes): tokenRes is IDoubleTokenRes { + return 'accessToken' in tokenRes && 'refreshToken' in tokenRes +} diff --git a/src/store/token.ts b/src/store/token.ts index 641b95c..b6fe449 100644 --- a/src/store/token.ts +++ b/src/store/token.ts @@ -1,6 +1,6 @@ -import type { IAuthLoginRes, IDoubleTokenRes, ISingleTokenRes } from '@/api/types/login' +import type { IAuthLoginRes } from '@/api/types/login' import { defineStore } from 'pinia' -import { ref } from 'vue' +import { computed, ref } from 'vue' // 修复:导入 computed import { login as _login, logout as _logout, @@ -8,9 +8,13 @@ import { wxLogin as _wxLogin, getWxCode, } from '@/api/login' +import { isDoubleTokenRes, isSingleTokenRes } from '@/api/types/login' import { isDoubleTokenMode } from '@/utils' import { useUserStore } from './user' +// 修复:添加 isSingleTokenMode 变量 +export const isSingleTokenMode = !isDoubleTokenMode + // 初始化状态 const tokenInfoState = isDoubleTokenMode ? { @@ -32,8 +36,54 @@ export const useTokenStore = defineStore( // 设置用户信息 const setTokenInfo = (val: IAuthLoginRes) => { tokenInfo.value = val + + // 计算并存储过期时间 + const now = Date.now() + if (isSingleTokenRes(val)) { + // 单token模式 + const expireTime = now + val.expiresIn * 1000 + uni.setStorageSync('accessTokenExpireTime', expireTime) + } + else if (isDoubleTokenRes(val)) { + // 双token模式 + const accessExpireTime = now + val.accessExpiresIn * 1000 + const refreshExpireTime = now + val.refreshExpiresIn * 1000 + uni.setStorageSync('accessTokenExpireTime', accessExpireTime) + uni.setStorageSync('refreshTokenExpireTime', refreshExpireTime) + } } + /** + * 判断token是否过期 + */ + const isTokenExpired = computed(() => { + const now = Date.now() + const expireTime = uni.getStorageSync('accessTokenExpireTime') + + if (!expireTime) + return true + return now >= expireTime + }) + + /** + * 判断refreshToken是否过期 + */ + const isRefreshTokenExpired = computed(() => { + if (!isDoubleTokenMode) + return true + + const now = Date.now() + const refreshExpireTime = uni.getStorageSync('refreshTokenExpireTime') + + if (!refreshExpireTime) + return true + return now >= refreshExpireTime + }) + + /** + * 登录成功后处理逻辑 + * @param tokenInfo 登录返回的token信息 + */ async function _postLogin(tokenInfo: IAuthLoginRes) { setTokenInfo(tokenInfo) const userStore = useUserStore() @@ -43,7 +93,7 @@ export const useTokenStore = defineStore( /** * 用户登录 * @param credentials 登录参数 - * @returns R + * @returns 登录结果 */ const login = async (credentials: { username: string @@ -51,62 +101,178 @@ export const useTokenStore = defineStore( code: string uuid: string }) => { - const res = await _login(credentials) - console.log('普通登录-res: ', res) - await _postLogin(res.data) - uni.showToast({ - title: '登录成功', - icon: 'success', - }) + try { + const res = await _login(credentials) + console.log('普通登录-res: ', res) + await _postLogin(res.data) + uni.showToast({ + title: '登录成功', + icon: 'success', + }) + return res + } + catch (error) { + console.error('登录失败:', error) + uni.showToast({ + title: '登录失败,请重试', + icon: 'error', + }) + throw error + } } /** * 微信登录 + * @returns 登录结果 */ const wxLogin = async () => { - // 获取微信小程序登录的code - const code = await getWxCode() - console.log('微信登录-code: ', code) - const res = await _wxLogin(code) - console.log('微信登录-res: ', res) - await _postLogin(res.data) - uni.showToast({ - title: '登录成功', - icon: 'success', - }) + try { + // 获取微信小程序登录的code + const code = await getWxCode() + console.log('微信登录-code: ', code) + const res = await _wxLogin(code) + console.log('微信登录-res: ', res) + await _postLogin(res.data) + uni.showToast({ + title: '登录成功', + icon: 'success', + }) + return res + } + catch (error) { + console.error('微信登录失败:', error) + uni.showToast({ + title: '微信登录失败,请重试', + icon: 'error', + }) + throw error + } } /** * 退出登录 并 删除用户信息 */ const logout = async () => { - _logout() - const userStore = useUserStore() - await userStore.removeUserInfo() + try { + await _logout() + + // 清除存储的过期时间 + uni.removeStorageSync('accessTokenExpireTime') + uni.removeStorageSync('refreshTokenExpireTime') + } + catch (error) { + console.error('退出登录失败:', error) + } + finally { + // 无论成功失败,都需要清除本地token信息 + const userStore = useUserStore() + await userStore.removeUserInfo() + } } /** * 刷新token + * @returns 刷新结果 */ const refreshToken = async () => { - const refreshToken = (tokenInfo.value as IDoubleTokenRes).refreshToken - const res = await _refreshToken(refreshToken) - console.log('刷新token-res: ', res) - setTokenInfo(res.data) + if (!isDoubleTokenMode) { + console.error('单token模式不支持刷新token') + throw new Error('单token模式不支持刷新token') + } + + try { + // 安全检查,确保refreshToken存在 + if (!isDoubleTokenRes(tokenInfo.value) || !tokenInfo.value.refreshToken) { + throw new Error('无效的refreshToken') + } + + const refreshToken = tokenInfo.value.refreshToken + const res = await _refreshToken(refreshToken) + console.log('刷新token-res: ', res) + setTokenInfo(res.data) + return res + } + catch (error) { + console.error('刷新token失败:', error) + throw error + } } - const hasLogin = computed(() => isDoubleTokenMode - ? (tokenInfo.value as IDoubleTokenRes).accessToken - : (tokenInfo.value as ISingleTokenRes).token, - ) + /** + * 获取有效的token + * 注意:在computed中不直接调用异步函数,只做状态判断 + * 实际的刷新操作应由调用方处理 + */ + const getValidToken = computed(() => { + // token已过期,返回空 + if (isTokenExpired.value) { + return '' + } + + if (isSingleTokenMode) { + return isSingleTokenRes(tokenInfo.value) ? tokenInfo.value.token : '' + } + else { + return isDoubleTokenRes(tokenInfo.value) ? tokenInfo.value.accessToken : '' + } + }) + + /** + * 检查是否有登录信息(不考虑token是否过期) + */ + const hasLoginInfo = computed(() => { + if (isDoubleTokenMode) { + return isDoubleTokenRes(tokenInfo.value) && !!tokenInfo.value.accessToken + } + else { + return isSingleTokenRes(tokenInfo.value) && !!tokenInfo.value.token + } + }) + + /** + * 检查是否已登录且token有效 + */ + const hasValidLogin = computed(() => { + return hasLoginInfo.value && !isTokenExpired.value + }) + + /** + * 尝试获取有效的token,如果过期且可刷新,则刷新token + * @returns 有效的token或空字符串 + */ + const tryGetValidToken = async (): Promise => { + if (!getValidToken.value && isDoubleTokenMode && !isRefreshTokenExpired.value) { + try { + await refreshToken() + return getValidToken.value + } + catch (error) { + console.error('尝试刷新token失败:', error) + return '' + } + } + return getValidToken.value + } return { - tokenInfo, - hasLogin, + // 核心API方法 login, wxLogin, logout, + + // 认证状态判断(最常用的) + hasLogin: hasValidLogin, + + // 内部系统使用的方法 refreshToken, + tryGetValidToken, + + // 调试或特殊场景可能需要直接访问的信息 + tokenInfo, } }, + { + // 添加持久化配置,确保刷新页面后token信息不丢失 + persist: true, + }, )