import { defineStore } from 'pinia' import { ref } from 'vue' import { login as loginApi, register as registerApi, refresh as refreshApi, type LoginRequest, type RegisterRequest } from '@/api/modules/account' import { getUserInfo, updateUserInfo as updateUserInfoApi, uploadAvatar as uploadAvatarApi, deleteAvatar as deleteAvatarApi, updateStatus as updateStatusApi, type UserStatusType, type UpdateUserInfoRequest } from '@/api/modules/user' /** 账户信息 */ export interface AccountState { /** 用户唯一标识(UUID) */ userId?: string /** 用户昵称 */ nickName?: string /** 头像 */ avatar?: string | null /** 个性签名 */ signature?: string | null /** 年龄 */ age?: number | null /** 性别:1=男,2=女 */ sex?: number | null /** 生日 */ birthday?: string | null /** 在线状态:ONLINE / AWAY / DND / INVISIBLE */ status?: UserStatusType } export const useAccountStore = defineStore( 'account', () => { /** 账户信息 */ const accountInfo = ref(null) /** Access Token(30 分钟有效,每次请求通过 saToken 头携带) */ const accessToken = ref('') /** Refresh Token(30 天有效,仅用于刷新 Access Token) */ const refreshToken = ref('') /** token 是否已验证有效 */ const isTokenValid = ref(false) /** 是否已完成初始验证(防止重复请求) */ const isInitialized = ref(false) /** * 用户登录 * 调用登录 API,成功后存储双 Token 并获取用户信息 * @param params - 登录请求参数(用户名、密码) */ const login = async (params: LoginRequest) => { const res = await loginApi(params) accessToken.value = res.data.accessToken refreshToken.value = res.data.refreshToken await fetchUserInfo() isTokenValid.value = true } /** * 用户注册 * 调用注册 API,成功后自动登录(存储双 Token 并获取用户信息) * @param params - 注册请求参数(用户名、密码) */ const register = async (params: RegisterRequest) => { const res = await registerApi(params) accessToken.value = res.data.accessToken refreshToken.value = res.data.refreshToken await fetchUserInfo() isTokenValid.value = true } /** * 退出登录 * 清除令牌、账户信息和验证状态 */ const logout = () => { clearAuth() } /** * 获取当前登录用户信息 * 需要先有 Access Token 才能调用 */ const fetchUserInfo = async () => { const res = await getUserInfo() accountInfo.value = res.data } /** * 刷新 Access Token(带并发锁) * * 由刷新拦截器的 doRefreshToken 和 SSE 的 onUnauthorized 调用。 * 返回新的 Access Token 字符串,方便调用方直接使用。 * * 并发控制:若已有刷新正在进行中,返回同一个 Promise, * 确保无论并发触发多少次,实际只发出一次 /account/refresh 请求。 * * @returns 新的 Access Token */ /** 正在进行的刷新 Promise(null 表示无刷新进行中) */ let refreshPromise: Promise | null = null const refreshAccessToken = async (): Promise => { // 已有刷新进行中 → 复用同一个 Promise,避免并发重复请求 if (refreshPromise) { return refreshPromise } if (!refreshToken.value) { throw new Error('无 Refresh Token,无法刷新') } refreshPromise = (async () => { try { const res = await refreshApi({ refreshToken: refreshToken.value }) accessToken.value = res.data.accessToken if (res.data.refreshToken) { refreshToken.value = res.data.refreshToken } isTokenValid.value = true return accessToken.value } finally { refreshPromise = null } })() return refreshPromise } /** * 更新当前用户的个人资料 * 调用后端接口持久化,并同步更新本地 accountInfo * @param data - 要更新的字段(所有字段可选) */ const updateProfile = async (data: UpdateUserInfoRequest) => { const res = await updateUserInfoApi(data) accountInfo.value = res.data } /** * 上传用户头像 * 调用后端接口上传文件并更新 DB,同步更新本地 accountInfo * @param file - 头像文件 */ const uploadAvatar = async (file: File) => { const res = await uploadAvatarApi(file) accountInfo.value = res.data } /** * 删除用户头像 * 调用后端接口删除文件并清空 DB,同步更新本地 accountInfo */ const removeAvatar = async () => { const res = await deleteAvatarApi() accountInfo.value = res.data } /** * 更新当前用户的在线状态 * 调用后端接口持久化,并同步更新本地 accountInfo * @param status - 新状态值 */ const updateUserStatus = async (status: UserStatusType) => { await updateStatusApi({ status }) if (accountInfo.value) { accountInfo.value = { ...accountInfo.value, status } } } /** 清除所有认证状态 */ const clearAuth = () => { accessToken.value = '' refreshToken.value = '' accountInfo.value = null isTokenValid.value = false isInitialized.value = false } console.log('useAccountStore', accessToken.value) return { accountInfo, accessToken, refreshToken, login, register, logout, refreshAccessToken, fetchUserInfo, updateProfile, uploadAvatar, removeAvatar, updateUserStatus } }, { persist: { pick: ['accessToken', 'refreshToken'] } } )