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:
173
frontend/src/utils/request.js
Normal file
173
frontend/src/utils/request.js
Normal file
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* HTTP 请求封装模块
|
||||
*
|
||||
* 基于 uni.request 封装统一的请求方法,提供:
|
||||
* - 基础 URL 配置(区分开发/生产环境)
|
||||
* - 请求拦截:自动附加 Authorization Header
|
||||
* - 响应拦截:统一处理错误码(401 自动跳转登录等)
|
||||
* - Promise 化的请求接口
|
||||
*
|
||||
* 对应后端 API 规范见 docs/api/README.md
|
||||
*/
|
||||
|
||||
import { getToken, removeToken } from './storage'
|
||||
|
||||
// 基础 URL 配置,根据环境自动切换
|
||||
const BASE_URL = process.env.NODE_ENV === 'development'
|
||||
? 'http://localhost:8085'
|
||||
: '' // 生产环境由 Nginx 反向代理,使用相对路径
|
||||
|
||||
// 请求超时时间(毫秒)
|
||||
const TIMEOUT = 15000
|
||||
|
||||
/**
|
||||
* 统一请求方法
|
||||
*
|
||||
* @param {Object} options - 请求配置
|
||||
* @param {string} options.url - 请求路径(不含 BASE_URL,如 /api/v1/auth/login)
|
||||
* @param {string} [options.method='GET'] - HTTP 方法
|
||||
* @param {Object} [options.data] - 请求体数据
|
||||
* @param {Object} [options.params] - URL Query 参数
|
||||
* @param {Object} [options.header] - 自定义请求头
|
||||
* @param {boolean} [options.needAuth=true] - 是否需要自动附加 Token
|
||||
* @returns {Promise<Object>} 响应中的 data 字段(已解包)
|
||||
*/
|
||||
const request = (options) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// 构建请求头
|
||||
const header = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.header
|
||||
}
|
||||
|
||||
// 自动附加 Authorization Header(默认需要认证)
|
||||
const needAuth = options.needAuth !== false
|
||||
if (needAuth) {
|
||||
const token = getToken()
|
||||
if (token) {
|
||||
header['Authorization'] = `Bearer ${token}`
|
||||
}
|
||||
}
|
||||
|
||||
uni.request({
|
||||
url: `${BASE_URL}${options.url}`,
|
||||
method: options.method || 'GET',
|
||||
data: options.data,
|
||||
header,
|
||||
timeout: TIMEOUT,
|
||||
success: (res) => {
|
||||
const { statusCode, data } = res
|
||||
|
||||
// HTTP 状态码 2xx 视为成功
|
||||
if (statusCode >= 200 && statusCode < 300) {
|
||||
// 后端统一响应格式:{ code: 0, message: "success", data: ... }
|
||||
if (data.code === 0) {
|
||||
resolve(data)
|
||||
} else {
|
||||
// code 非 0 表示业务逻辑错误
|
||||
uni.showToast({
|
||||
title: data.message || '请求失败',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
reject(data)
|
||||
}
|
||||
} else if (statusCode === 401) {
|
||||
// Token 过期或无效,清除本地 Token 并跳转登录页
|
||||
removeToken()
|
||||
uni.showToast({
|
||||
title: '登录已过期,请重新登录',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
// 延迟跳转,让 Toast 有时间显示
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: '/pages/auth/login' })
|
||||
}, 1500)
|
||||
reject({ code: 401, message: '认证已失效' })
|
||||
} else if (statusCode === 403) {
|
||||
uni.showToast({
|
||||
title: data.message || '没有访问权限',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
reject(data)
|
||||
} else {
|
||||
// 其他 HTTP 错误(400/404/500 等)
|
||||
uni.showToast({
|
||||
title: data.message || `请求错误 (${statusCode})`,
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
reject(data)
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
// 网络错误或请求超时
|
||||
uni.showToast({
|
||||
title: '网络异常,请检查网络连接',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
reject({ code: -1, message: err.errMsg || '网络异常' })
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* GET 请求快捷方法
|
||||
*
|
||||
* @param {string} url - 请求路径
|
||||
* @param {Object} [data] - Query 参数
|
||||
* @param {Object} [options] - 额外配置
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
const get = (url, data, options = {}) => {
|
||||
return request({ url, method: 'GET', data, ...options })
|
||||
}
|
||||
|
||||
/**
|
||||
* POST 请求快捷方法
|
||||
*
|
||||
* @param {string} url - 请求路径
|
||||
* @param {Object} [data] - 请求体数据
|
||||
* @param {Object} [options] - 额外配置
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
const post = (url, data, options = {}) => {
|
||||
return request({ url, method: 'POST', data, ...options })
|
||||
}
|
||||
|
||||
/**
|
||||
* PUT 请求快捷方法
|
||||
*
|
||||
* @param {string} url - 请求路径
|
||||
* @param {Object} [data] - 请求体数据
|
||||
* @param {Object} [options] - 额外配置
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
const put = (url, data, options = {}) => {
|
||||
return request({ url, method: 'PUT', data, ...options })
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE 请求快捷方法
|
||||
*
|
||||
* @param {string} url - 请求路径
|
||||
* @param {Object} [data] - 请求体数据
|
||||
* @param {Object} [options] - 额外配置
|
||||
* @returns {Promise<Object>}
|
||||
*/
|
||||
const del = (url, data, options = {}) => {
|
||||
return request({ url, method: 'DELETE', data, ...options })
|
||||
}
|
||||
|
||||
export {
|
||||
request,
|
||||
get,
|
||||
post,
|
||||
put,
|
||||
del,
|
||||
BASE_URL
|
||||
}
|
||||
140
frontend/src/utils/storage.js
Normal file
140
frontend/src/utils/storage.js
Normal file
@@ -0,0 +1,140 @@
|
||||
/**
|
||||
* 本地存储工具模块
|
||||
*
|
||||
* 封装 uni.setStorageSync / uni.getStorageSync,提供:
|
||||
* - Token 存取(Access Token + Refresh Token)
|
||||
* - 用户信息缓存
|
||||
* - 通用存储操作
|
||||
*
|
||||
* 存储方式:
|
||||
* - H5 环境:localStorage
|
||||
* - 小程序/App:uni 内置存储
|
||||
* - 均通过 uni.setStorageSync/getStorageSync 统一调用
|
||||
*/
|
||||
|
||||
// 存储 Key 常量,集中管理避免硬编码
|
||||
const STORAGE_KEYS = {
|
||||
/** Access Token,用于 API 认证 */
|
||||
ACCESS_TOKEN: 'echo_access_token',
|
||||
/** Refresh Token,用于刷新 Access Token */
|
||||
REFRESH_TOKEN: 'echo_refresh_token',
|
||||
/** Access Token 过期时间戳(毫秒) */
|
||||
TOKEN_EXPIRE_TIME: 'echo_token_expire_time',
|
||||
/** 用户信息缓存 */
|
||||
USER_INFO: 'echo_user_info'
|
||||
}
|
||||
|
||||
// ==================== Token 操作 ====================
|
||||
|
||||
/**
|
||||
* 保存 Token 信息(登录/注册成功后调用)
|
||||
*
|
||||
* @param {string} accessToken - Access Token
|
||||
* @param {string} refreshToken - Refresh Token
|
||||
* @param {number} expiresIn - Access Token 有效期(秒)
|
||||
*/
|
||||
const saveToken = (accessToken, refreshToken, expiresIn) => {
|
||||
uni.setStorageSync(STORAGE_KEYS.ACCESS_TOKEN, accessToken)
|
||||
uni.setStorageSync(STORAGE_KEYS.REFRESH_TOKEN, refreshToken)
|
||||
// 计算并存储过期时间戳,用于前端主动判断是否需要刷新
|
||||
const expireTime = Date.now() + expiresIn * 1000
|
||||
uni.setStorageSync(STORAGE_KEYS.TOKEN_EXPIRE_TIME, expireTime)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Access Token
|
||||
*
|
||||
* @returns {string|null} Access Token,不存在时返回 null
|
||||
*/
|
||||
const getToken = () => {
|
||||
return uni.getStorageSync(STORAGE_KEYS.ACCESS_TOKEN) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Refresh Token
|
||||
*
|
||||
* @returns {string|null} Refresh Token,不存在时返回 null
|
||||
*/
|
||||
const getRefreshToken = () => {
|
||||
return uni.getStorageSync(STORAGE_KEYS.REFRESH_TOKEN) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断 Access Token 是否即将过期(提前 5 分钟视为过期)
|
||||
* 用于在请求前主动触发 Token 刷新
|
||||
*
|
||||
* @returns {boolean} true 表示已过期或即将过期
|
||||
*/
|
||||
const isTokenExpired = () => {
|
||||
const expireTime = uni.getStorageSync(STORAGE_KEYS.TOKEN_EXPIRE_TIME)
|
||||
if (!expireTime) return true
|
||||
// 提前 5 分钟(300秒)判定为过期,给刷新操作留出时间窗口
|
||||
return Date.now() >= expireTime - 300 * 1000
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除所有 Token(登出时调用)
|
||||
* 同时清除服务端 Redis 中的 Token(通过 logout API),这里只清除客户端缓存
|
||||
*/
|
||||
const removeToken = () => {
|
||||
uni.removeStorageSync(STORAGE_KEYS.ACCESS_TOKEN)
|
||||
uni.removeStorageSync(STORAGE_KEYS.REFRESH_TOKEN)
|
||||
uni.removeStorageSync(STORAGE_KEYS.TOKEN_EXPIRE_TIME)
|
||||
}
|
||||
|
||||
// ==================== 用户信息操作 ====================
|
||||
|
||||
/**
|
||||
* 缓存用户信息
|
||||
*
|
||||
* @param {Object} userInfo - 用户信息对象(与 dto.UserInfo 结构一致)
|
||||
*/
|
||||
const saveUserInfo = (userInfo) => {
|
||||
uni.setStorageSync(STORAGE_KEYS.USER_INFO, JSON.stringify(userInfo))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存的用户信息
|
||||
*
|
||||
* @returns {Object|null} 用户信息对象,不存在时返回 null
|
||||
*/
|
||||
const getUserInfo = () => {
|
||||
const data = uni.getStorageSync(STORAGE_KEYS.USER_INFO)
|
||||
if (!data) return null
|
||||
try {
|
||||
return JSON.parse(data)
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除用户信息缓存
|
||||
*/
|
||||
const removeUserInfo = () => {
|
||||
uni.removeStorageSync(STORAGE_KEYS.USER_INFO)
|
||||
}
|
||||
|
||||
// ==================== 通用存储操作 ====================
|
||||
|
||||
/**
|
||||
* 清除所有应用数据(重置应用状态)
|
||||
* 谨慎使用,会清除所有本地存储
|
||||
*/
|
||||
const clearAll = () => {
|
||||
removeToken()
|
||||
removeUserInfo()
|
||||
}
|
||||
|
||||
export {
|
||||
STORAGE_KEYS,
|
||||
saveToken,
|
||||
getToken,
|
||||
getRefreshToken,
|
||||
isTokenExpired,
|
||||
removeToken,
|
||||
saveUserInfo,
|
||||
getUserInfo,
|
||||
removeUserInfo,
|
||||
clearAll
|
||||
}
|
||||
Reference in New Issue
Block a user