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:
bujinyuan
2026-03-02 11:31:24 +08:00
parent e9bd4dd204
commit 4d03215fe4
63 changed files with 15908 additions and 102 deletions

View 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
}

View File

@@ -0,0 +1,140 @@
/**
* 本地存储工具模块
*
* 封装 uni.setStorageSync / uni.getStorageSync提供
* - Token 存取Access Token + Refresh Token
* - 用户信息缓存
* - 通用存储操作
*
* 存储方式:
* - H5 环境localStorage
* - 小程序/Appuni 内置存储
* - 均通过 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
}