SpringBoot+Mybatis+MySQL+Vue+ElementUI前后端分离版:整体布局、架构调整(二)

目录

一、前言

二、后端调整 

1.实体类调整

2.菜单相关接口

3.用户相关接口

4.新增工具类

5.新增菜单树返回类

6.配置类、拦截器 

三、前端调整

1.请求调整

2.页面布局、样式调整

1.user.vue 

2.index.vue 

3.请求拦截

四、开发过程中的问题

五、附:源码

1.源码下载地址

六、结语

一、前言

此文章在上次的基础上进行了部分调整,并根据用户体验(我自己)确认了页面整体布局和数据呈现,暂定就先这样,后续有需要或者有不协调的地方再调整。
此项目是在我上一个文章的后续开发, 需要的同学可以关注一下,文章链接如下:SpringBoot+Mybatis+MySQL+Vue+ElementUI前后端分离版:项目搭建(一)

(注:源码我会在文章结尾提供gitee连接,需要的同学可以去自行下载)

二、后端调整 

1.实体类调整

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;

}

2.菜单相关接口

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





    
            
            
            
            
            
            
            
            
            
            
            
            
            
            
    
    


3.用户相关接口

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)

    
    

    

4.新增工具类

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分支,此分支是我用来实现和调试、测试工具类的分支。

5.新增菜单树返回类


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

6.配置类、拦截器 

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.请求调整

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',
  });
}

2.页面布局、样式调整

1.user.vue 

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';
2.index.vue 

1.index.vue页面调整,主要是抽取js请求,调整布局和响应式菜单,如下:





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',
  });
}

3.请求拦截

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);
  }
);

五、附:源码

1.源码下载地址

https://gitee.com/wangaolin/user-demo.git

同学们有需要可以自行下载查看,此文章是dev-vue分支。

六、结语

此次开发+调整只是为了后续开发有个参照,下一篇文章具体开发首页和权限管理,有需要的同学可以关注我。

(注:接定制化开发前后端分离项目,私我)

你可能感兴趣的:(前后端分离,SpringBoot,Spring,spring,boot,mybatis,mysql,vue.js,elementui)