目录
一、前言
二、后端调整
1.实体类调整
2.菜单相关接口
3.用户相关接口
4.新增工具类
5.新增菜单树返回类
6.配置类、拦截器
三、前端调整
1.请求调整
2.页面布局、样式调整
1.user.vue
2.index.vue
3.请求拦截
四、开发过程中的问题
五、附:源码
1.源码下载地址
六、结语
此文章在上次的基础上进行了部分调整,并根据用户体验(我自己)确认了页面整体布局和数据呈现,暂定就先这样,后续有需要或者有不协调的地方再调整。
此项目是在我上一个文章的后续开发, 需要的同学可以关注一下,文章链接如下:SpringBoot+Mybatis+MySQL+Vue+ElementUI前后端分离版:项目搭建(一)
(注:源码我会在文章结尾提供gitee连接,需要的同学可以去自行下载)
1.完善UserEntity.java
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.util.Date;
@Data
public class UserEntity extends BaseEntity{
/**
* id 主键
*/
private Integer id;
/**
* name 姓名
*/
private String name;
/**
* age 年龄
*/
private Integer age;
/**
* birthday 生日
*/
@JsonFormat(pattern = "yyyy-MM-dd")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private Date birthday;
}
2.新增菜单实体类MenuEntity.java
import lombok.Data;
/**
* 菜单表
* @TableName menu
*/
@Data
public class MenuEntity extends BaseEntity {
/**
* 主键
*/
private Integer id;
/**
* 菜单名称
*/
private String menuName;
/**
* 父菜单ID
*/
private Integer parentId;
/**
* 路由路径
*/
private String path;
/**
* 组件路径
*/
private String component;
/**
* 权限标识
*/
private String perms;
/**
* 图标
*/
private String icon;
/**
* 排序
*/
private Integer sort;
/**
* 是否显示(0隐藏,1显示)
*/
private Integer visible;
}
这里在数据库新建menu表,并添加几条测试数据。
CREATE TABLE `menu` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主键',
`menu_name` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '菜单名称',
`parent_id` int DEFAULT '0' COMMENT '父菜单ID',
`path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '路由路径',
`component` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '组件路径',
`perms` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '图标',
`sort` int DEFAULT '0' COMMENT '排序',
`visible` tinyint(1) DEFAULT '1' COMMENT '是否显示(0隐藏,1显示)',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`create_by` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '创建人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`update_by` varchar(120) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '更新人',
`del_flag` int(10) unsigned zerofill DEFAULT '0000000000' COMMENT '删除标识0未删除,1已删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='菜单表';
-- 插入数据
INSERT INTO `menu` (id, menu_name, parent_id, path, component, perms, icon, sort, visible, create_time, create_by, update_time, update_by, del_flag)
VALUES
(1, '权限管理', 0, '/permission', '', '', 'lock', 1, 1, NOW(), 'admin', NULL, NULL, 0),
(2, '用户管理', 1, '/user', 'src/view/user.vue', 'user:list', 'user', 1, 1, NOW(), 'admin', NULL, NULL, 0),
(3, '角色管理', 1, '/role', 'src/view/role.vue', 'role:list', 'role', 2, 1, NOW(), 'admin', NULL, NULL, 0),
(4, '菜单管理', 1, '/menu', 'src/view/menu.vue', 'menu:list', 'menu', 3, 1, NOW(), 'admin', NULL, NULL, 0);
3.对于实体类公共字段,我提取了一个BaseEntity.java,后续实体类都继承此实体类。
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.util.Date;
/**
* 实体类公共字段
* @Author: wal
* @Date: 2025/6/26
*/
@Data
public class BaseEntity {
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/**
* 创建人
*/
private String createBy;
/**
* 修改时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/**
* 修改人
*/
private String updateBy;
/**
* 删除标记0未删除1已删除(逻辑删除)
*/
private Integer delFlag;
}
1.MenuController.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.wal.userdemo.DTO.resp.TreeDataResp;
import org.wal.userdemo.service.MenuService;
import org.wal.userdemo.utils.Result;
import java.util.List;
@RestController
@RequestMapping("/api/menu")
public class MenuController {
@Autowired
private MenuService menuService;
@GetMapping("/getMenuList")
public Result> getMenuList() {
return Result.success(menuService.getMenuList(""));
}
}
2.MenuService.java
import org.wal.userdemo.DTO.resp.TreeDataResp;
import java.util.List;
public interface MenuService {
List getMenuList(String userId);
}
3.MenuServiceImpl.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.wal.userdemo.DTO.resp.TreeDataResp;
import org.wal.userdemo.entity.MenuEntity;
import org.wal.userdemo.mapper.MenuMapper;
import org.wal.userdemo.service.MenuService;
import org.wal.userdemo.utils.BeanUtils;
import java.util.*;
@Service
public class MenuServiceImpl implements MenuService {
@Autowired
private MenuMapper menuMapper;
/**
* 获取用户菜单列表
* @param userId
* @return
*/
@Override
public List getMenuList(String userId) {
List menuList = menuMapper.getMenuList(userId);
List treeDataRespList =BeanUtils.copyAsList(menuList, TreeDataResp.class);
return buildMenuTree(treeDataRespList);
}
/**
* 构建菜单树
* @param menus
* @return
*/
public List buildMenuTree(List menus) {
Map menuMap = new HashMap<>();
menus.forEach(menu -> menuMap.put(menu.getId(), menu));
List rootMenus = new ArrayList<>();
menus.forEach(menu -> {
Integer parentId = menu.getParentId();
if (parentId == null || parentId == 0) {
rootMenus.add(menu);
} else {
TreeDataResp parent = menuMap.get(parentId);
if (parent != null) {
if (parent.getChildren() == null) {
parent.setChildren(new ArrayList<>());
}
parent.getChildren().add(menu);
}
}
});
return rootMenus;
}
}
4.MenuMapper.java
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.wal.userdemo.entity.MenuEntity;
import java.util.List;
/**
* @author Administrator
* @description 针对表【menu(菜单表)】的数据库操作Mapper
* @createDate 2025-07-07 00:12:30
* @Entity org.wal.userdemo.entity.Menu
*/
@Mapper
public interface MenuMapper {
List getMenuList(@Param("userId") String userId);
}
5.MenuMapper.xml
1.UserController.java
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.wal.userdemo.DTO.req.QueryUserReq;
import org.wal.userdemo.entity.UserEntity;
import org.wal.userdemo.service.UserService;
import org.wal.userdemo.utils.Result;
import java.util.List;
@Slf4j
@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
private UserService userService;
/**
* 获取所有用户信息
*
* @return List
*/
@PostMapping("/getUserList")
public Result getUserList(@RequestBody QueryUserReq queryUserReq) {
List dataList = userService.getUserList(queryUserReq);
Integer total = userService.getUserCount(queryUserReq);
return Result.page(dataList, total);
}
}
2.定义通用分页Result.java(前文已体现,只是新增一个分页构造函数)
public static Result page(List list, Integer total) {
Result result = new Result<>();
result.setCode(200);
result.setData(list);
result.setTotal(total);
result.setMessage("success");
return result;
}
3.UserService.java(新增两个接口)
/**
* 查询所有用户
*
* @return
*/
List getUserList(QueryUserReq queryUserReq);
/**
* 查询用户数量
*
* @return
*/
Integer getUserCount(QueryUserReq queryUserReq);
4.UserServiceImpl.java(新增两个实现方法)
/**
* 获取所有用户信息
*
* @return List
*/
@Override
public List getUserList(QueryUserReq queryUserReq) {
List resp = userMapper.getUserList(queryUserReq);
return resp;
}
/**
* 获取用户数量
*
* @return Integer
*/
@Override
public Integer getUserCount(QueryUserReq queryUserReq) {
return userMapper.getUserCount(queryUserReq);
}
5.UserMapper.java(新增两个mapper接口)
/**
* 查询所有用户
*
* @return
*/
List getUserList(QueryUserReq queryUserReq);
/**
* 查询用户数量
*
* @return
*/
Integer getUserCount(QueryUserReq queryUserReq);
6.UserMapper.xml(新增两个sql)
1.新增工具类BeanUtils.java,具体体现在MenuServiceImpl.java类中copy菜单树,如下:
/**
* 获取用户菜单列表
* @param userId
* @return
*/
@Override
public List getMenuList(String userId) {
List menuList = menuMapper.getMenuList(userId);
List treeDataRespList =BeanUtils.copyAsList(menuList, TreeDataResp.class);
return buildMenuTree(treeDataRespList);
}
(为什么不直接用MenuEntity.java来构建树结构?,为了确保entity无属性、字段、方法侵入,解耦entity,声明resp类更容易理解和维护)。
此工具类是对org.springframework.beans.BeanUtils的封装。有需要的同学可以去一下链接查找:
gitee地址dev-utils分支,此分支是我用来实现和调试、测试工具类的分支。
import lombok.Data;
import java.util.List;
@Data
public class TreeDataResp {
/**
* 主键
*/
private Integer id;
/**
* 菜单名称
*/
private String menuName;
/**
* 父菜单ID
*/
private Integer parentId;
/**
* 路由路径
*/
private String path;
/**
* 组件路径
*/
private String component;
/**
* 权限标识
*/
private String perms;
/**
* 图标
*/
private String icon;
/**
* 排序
*/
private Integer sort;
/**
* 是否显示(0隐藏,1显示)
*/
private Integer visible;
/**
* 子菜单
*/
private List children;
}
1.新增JwtInterceptor.java拦截web请求,校验token信息。
import io.jsonwebtoken.JwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.wal.userdemo.utils.JwtUtil;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Slf4j
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token != null && token.startsWith("Bearer ")) {
token = token.substring(7);
try {
String username = JwtUtil.parseUsername(token);
// 可以将 username 存入 request 或 SecurityContext
log.info("用户 {} 使用正确的token访问了后端接口", username);
return true;
} catch (JwtException e) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "无效 Token");
return false;
}
} else {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "缺少 Token");
return false;
}
}
}
2.新增WebConfig.java类,针对特定路由接口挂载JwtInterceptor拦截器,忽略登录接口。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.wal.userdemo.interceptor.JwtInterceptor;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtInterceptor;
/**
* 添加拦截器
* 拦截路径为/api/**的请求,除了 /api/auth/login请求
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor).addPathPatterns("/api/**").excludePathPatterns("/api/auth/login");
}
}
至此,后端的调整暂时就这样。
1.重写login.vue的js部分,抽离请求体,在src创建api目录,在api下创建login.js,在js部分引入
import { login } from '@/api/login';
export default {
name: 'UserLogin',
data() {
return {
formData: {
username: '',
password: ''
},
rules: {
username: [
{ required: true, message: '用户名不能为空', trigger: 'blur' }
],
password: [
{ required: true, message: '密码不能为空', trigger: 'blur' }
],
}
};
},
methods: {
async login() {
try {
const res = await login(this.formData);
console.log('res.code', res)
if (res.data.code === 200) {
const token = res.data.data;
localStorage.setItem('token', token);
this.$router.push('/');
this.$message.success('登录成功');
} else {
this.$message.error(res.data.message || '登录失败');
}
} catch (error) {
this.$message.error('请求异常,请检查网络或服务端状态');
}
}
}
};
2.login.js如下:
// src/api/login.js
import request from '@/utils/request';
/**
* 用户登录
* @param {Object} data - 登录参数,如用户名和密码
* @returns {Promise}
*/
export function login(data) {
return request({
url: '/auth/login',
method: 'post',
data,
});
}
/**
* 用户退出(登出)
* @returns {Promise}
*/
export function logout() {
return request({
url: '/auth/logout',
method: 'post',
});
}
1.user.vue布局调整
查询
重置
编辑
删除
2.同样的,js请求抽出来到user.js下,目录在src/api/permission/下,
import request from '@/utils/request';
/**
* 查询用户列表(分页)
* @param {Object} params - 请求参数,如 page, limit 等
*/
export function getUserList(params) {
return request({
url: '/user/getUserList',
method: 'post',
data : params,
});
}
3. user.vue作为后续页面的参考页面,所以我把CSS部分抽出来到src/assets/css/global.css如下:
.query-border-container {
border: 1px dashed #dcdcdc;
border-radius: 8px;
padding: 8px 16px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
margin-bottom: 12px;
}
.table-border-container {
border: 1px dashed #dcdcdc;
border-radius: 8px;
padding: 8px 16px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
margin-bottom: 12px;
}
.page-border-container {
border: 1px dashed #dcdcdc;
border-radius: 8px;
padding: 8px 16px 8px 16px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
margin-bottom: 1px;
}
.el-table .el-table__cell {
padding: 5px 0px !important;
}
.el-form-item {
margin-bottom: 2px !important;
}
CSS作为全局页面的样式需要在main.js中挂载,添加如下代码:
import '@/assets/css/global.css';
1.index.vue页面调整,主要是抽取js请求,调整布局和响应式菜单,如下:
我的管理系统
{{ menu.menuName }}
{{ child.menuName }}
欢迎,Admin
退出
2.index.vue抽取的js在src/api/permission/下,menu.js如下:
import request from '@/utils/request';
/**
* 查询菜单列表
* @param {Object} userId {可选} - 用户ID
}
*/
export function getMenuList() {
return request({
url: '/menu/getMenuList',
method: 'get',
});
}
1.在src下新建utils目录,在utils下新建request.js,所以请求js都要导入request.js,在request.js中声明请求配置、请求拦截器,如下:
import axios from 'axios';
const service = axios.create({
baseURL: process.env.VUE_APP_BASE_API || '/api', // 使用环境变量或默认值
timeout: 5000,
});
// 请求拦截器:添加 token 到 header
service.interceptors.request.use(
config => {
const token = localStorage.getItem('token');
if (token) {
config.headers['Authorization'] = 'Bearer ' + token;
}
return config;
},
error => {
return Promise.reject(error);
}
);
export default service;
至此,前端布局、请求调整到此结束。
1.code review
在调试过程中,不断的重启后端项目,导致token失效,请求都是401未授权访问。
解决方案:在request.js中定义响应拦截器,把遇到error = 401重新跳转到登录页。
//响应拦截器(可选启用)
service.interceptors.response.use(
response => {
return response;
},
error => {
if(error.response.data.error == 'Unauthorized'){
console.error('token已失效请重新登录');
localStorage.removeItem('token');
window.location.href = '/login';
}
console.error('网络异常:', error);
return Promise.reject(error.message);
}
);
https://gitee.com/wangaolin/user-demo.git
同学们有需要可以自行下载查看,此文章是dev-vue分支。
此次开发+调整只是为了后续开发有个参照,下一篇文章具体开发首页和权限管理,有需要的同学可以关注我。
(注:接定制化开发前后端分离项目,私我)