feat: Phase 1 完成 — 基础设施 + 用户认证 + 管理端 + 端到端验证
Phase 1 (基础设施与用户认证) 全部 11 个 Task 开发完成: 后端 (Go): - Auth 模块: 注册/登录/JWT(有状态)/Profile/密码修改 - Admin 模块: 用户列表/详情/禁用/启用/角色分配/创建用户 - 中间件: JWT认证 + RBAC角色权限 + 请求日志 + CORS + Panic恢复 - Dockerfile 多阶段构建 + Docker Compose 全栈部署 前台 (uni-app): - 登录/注册页面 + 自定义 TabBar + 个人中心 - 请求封装 + 状态管理 (Pinia) 管理端 (Vue 3 + Element Plus): - 登录/仪表盘/用户列表/用户详情 - Axios 封装 + 路由守卫 + Pinia 状态管理 验证: - 全流程 API 端到端测试通过 - Playwright 页面自动化验证通过 - code-reviewer 代码审查通过 Made-with: Cursor
This commit is contained in:
217
frontend/src/store/user.js
Normal file
217
frontend/src/store/user.js
Normal file
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
* 用户状态管理 Store
|
||||
*
|
||||
* 管理用户认证状态和个人信息,提供:
|
||||
* - 登录/注册/登出操作
|
||||
* - Token 自动管理(存储 + 刷新)
|
||||
* - 用户信息缓存与更新
|
||||
* - 持久化存储(通过 pinia-plugin-persistedstate,H5 用 localStorage,小程序用 uni 存储)
|
||||
*
|
||||
* 对应后端 API:/api/v1/auth/*
|
||||
* 接口文档见:docs/api/frontend/auth.md
|
||||
*/
|
||||
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import authApi from '@/api/auth'
|
||||
import {
|
||||
saveToken,
|
||||
getToken,
|
||||
getRefreshToken,
|
||||
removeToken,
|
||||
saveUserInfo,
|
||||
getUserInfo,
|
||||
removeUserInfo,
|
||||
clearAll
|
||||
} from '@/utils/storage'
|
||||
|
||||
/**
|
||||
* 用户 Store(Composition API 风格)
|
||||
*
|
||||
* state:
|
||||
* - token: Access Token
|
||||
* - userInfo: 用户信息对象
|
||||
*
|
||||
* getters:
|
||||
* - isLoggedIn: 是否已登录
|
||||
* - username: 用户名快捷访问
|
||||
* - roles: 用户角色列表
|
||||
*
|
||||
* actions:
|
||||
* - login: 用户登录
|
||||
* - register: 用户注册
|
||||
* - logout: 退出登录
|
||||
* - getProfile: 获取/刷新用户信息
|
||||
* - updateProfile: 更新个人信息
|
||||
* - changePassword: 修改密码
|
||||
* - refreshUserToken: 刷新 Token
|
||||
*/
|
||||
export const useUserStore = defineStore('user', () => {
|
||||
// ==================== State ====================
|
||||
|
||||
/** Access Token,用于 API 认证 */
|
||||
const token = ref(getToken() || '')
|
||||
|
||||
/** 用户信息对象,与后端 dto.UserInfo 结构一致 */
|
||||
const userInfo = ref(getUserInfo() || null)
|
||||
|
||||
// ==================== Getters ====================
|
||||
|
||||
/** 是否已登录(Token 存在视为已登录) */
|
||||
const isLoggedIn = computed(() => !!token.value)
|
||||
|
||||
/** 用户名快捷访问 */
|
||||
const username = computed(() => userInfo.value?.username || '')
|
||||
|
||||
/** 用户角色列表 */
|
||||
const roles = computed(() => userInfo.value?.roles || [])
|
||||
|
||||
// ==================== Actions ====================
|
||||
|
||||
/**
|
||||
* 处理登录/注册成功后的通用逻辑
|
||||
* 保存 Token 和用户信息到 Store 和本地存储
|
||||
*
|
||||
* @param {Object} data - 后端返回的登录响应 { token, refresh_token, expires_in, user }
|
||||
*/
|
||||
const _handleAuthSuccess = (data) => {
|
||||
token.value = data.token
|
||||
userInfo.value = data.user
|
||||
saveToken(data.token, data.refresh_token, data.expires_in)
|
||||
saveUserInfo(data.user)
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登录
|
||||
*
|
||||
* @param {Object} loginData - { account, password }
|
||||
* @returns {Promise<Object>} 登录响应数据
|
||||
*/
|
||||
const login = async (loginData) => {
|
||||
const res = await authApi.login(loginData)
|
||||
_handleAuthSuccess(res.data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册(注册成功后自动登录)
|
||||
*
|
||||
* @param {Object} registerData - { username, email, password, nickname? }
|
||||
* @returns {Promise<Object>} 注册响应数据
|
||||
*/
|
||||
const register = async (registerData) => {
|
||||
const res = await authApi.register(registerData)
|
||||
_handleAuthSuccess(res.data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
* 1. 调用后端 API 从 Redis 删除 Token(使 Token 立即失效)
|
||||
* 2. 清除本地存储的 Token 和用户信息
|
||||
* 3. 重置 Store 状态
|
||||
*/
|
||||
const logout = async () => {
|
||||
try {
|
||||
await authApi.logout()
|
||||
} catch (e) {
|
||||
// 即使后端请求失败,也要清除本地状态(避免用户被卡住)
|
||||
console.warn('登出 API 调用失败,仍然清除本地状态', e)
|
||||
}
|
||||
token.value = ''
|
||||
userInfo.value = null
|
||||
clearAll()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取/刷新用户信息
|
||||
* 从后端获取最新的用户资料并更新 Store 和本地缓存
|
||||
*
|
||||
* @returns {Promise<Object>} 用户信息
|
||||
*/
|
||||
const getProfile = async () => {
|
||||
const res = await authApi.getProfile()
|
||||
userInfo.value = res.data
|
||||
saveUserInfo(res.data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新个人信息
|
||||
*
|
||||
* @param {Object} data - 需要更新的字段 { nickname?, avatar?, gender?, phone? }
|
||||
* @returns {Promise<Object>} 更新后的用户信息
|
||||
*/
|
||||
const updateProfile = async (data) => {
|
||||
const res = await authApi.updateProfile(data)
|
||||
userInfo.value = res.data
|
||||
saveUserInfo(res.data)
|
||||
return res.data
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码
|
||||
*
|
||||
* @param {Object} data - { old_password, new_password }
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
const changePassword = async (data) => {
|
||||
return await authApi.changePassword(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新 Token
|
||||
* 使用 Refresh Token 获取新的 Access Token
|
||||
* 新 Token 会覆盖 Redis 中的旧值(实现单设备登录)
|
||||
*
|
||||
* @returns {Promise<boolean>} 刷新是否成功
|
||||
*/
|
||||
const refreshUserToken = async () => {
|
||||
const refreshTokenValue = getRefreshToken()
|
||||
if (!refreshTokenValue) return false
|
||||
|
||||
try {
|
||||
const res = await authApi.refreshToken(refreshTokenValue)
|
||||
_handleAuthSuccess(res.data)
|
||||
return true
|
||||
} catch (e) {
|
||||
// Refresh Token 也过期或无效,需要重新登录
|
||||
token.value = ''
|
||||
userInfo.value = null
|
||||
clearAll()
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
// state
|
||||
token,
|
||||
userInfo,
|
||||
// getters
|
||||
isLoggedIn,
|
||||
username,
|
||||
roles,
|
||||
// actions
|
||||
login,
|
||||
register,
|
||||
logout,
|
||||
getProfile,
|
||||
updateProfile,
|
||||
changePassword,
|
||||
refreshUserToken
|
||||
}
|
||||
}, {
|
||||
/**
|
||||
* Pinia 持久化配置
|
||||
* H5 环境使用 localStorage,小程序/App 使用 uni 存储
|
||||
* 仅持久化 token 和 userInfo,避免存储过多无关数据
|
||||
*/
|
||||
persist: {
|
||||
key: 'echo-user-store',
|
||||
storage: {
|
||||
getItem: (key) => uni.getStorageSync(key),
|
||||
setItem: (key, value) => uni.setStorageSync(key, value)
|
||||
},
|
||||
paths: ['token', 'userInfo']
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user