【从0带敲,手把手教】撸一个SpringBoot3+Vue3 脚手架
程序员小孟,微信:codemeng
定制开发,咨询,有问题可以找我
学习网站:https://www.pdxmw.com/ (opens new window)
2026 毕设指导!一分钱不花,手把手教。
欢迎进入钻石 VIP 学习,一次上车永久学习:
钻石 VIP 学习
- 本期项目目的
本期视频会带领大家从数据库设计开始,从 0 到 1 的搭建一个属于自己的 SpringBoot3+Vue3 脚手架项目,脚手架是一个项目的基石,后续咱们可以基于这个脚手架开发自己的毕设或者是商业项目,而非每次都从 0 开始。
大家只要跟着视频一步一步学习,就可以独立完成脚手架项目的搭建。
这个项目的文档和教程全是免费学习,**一键三连 + 关注** 获取视频配套的核心资料。
我们出视频的目的,就是带大家学习. ****
适用人群
- 学习技术的大学生
- 做毕设的小伙伴
- 想学习 SpringBoot+Vue 的小伙伴
# 项目功能&技术栈介绍
现在网上有很多的很不错的基于 SpringBoot3+Vue3 的脚手架项目,功能也很强大,但也正是因为功能太多,导致入门学习的时候不很友好,想改东西就是找不到,所以这期课程我会带领大家从 0 到 1 搭建一个基础的脚手架项目。
# 项目功能
- ✅ 用户管理:用户的增删改查
- ✅ 角色管理:角色的增删改查
- ✅ 权限管理:权限的增删改查(有多级权限)
- ✅ 用户角色关联:一个用户可以拥有多个角色
- ✅ 角色权限关联【角色授权】:一个角色可以拥有多个权限
- ✅ JWT 认证:基于 Token 的身份认证
- ✅ 登录鉴权
- 路由导航守卫:前端路由权限控制
- 后端鉴权:JWT
- ✅ 动态路由
- ✅ 动态按钮
# 开发技术栈
后端
- Spring Boot 3.x
- MyBatis Plus
- MySQL 8.0+
- JWT (jjwt 0.12.3)
- Hutool 5.8.23
前端
- Vue 3.3.4
- Element Plus 2.4.4
- Vue Router 4.2.5
- Pinia 2.1.7
- Axios 1.6.2
- Vite 5.0.8
# 数据库设计
脚手架项目其实就是一个基础的权限管理系统,包含的数据表有如下几个:
- 用户表: sys_user
- 角色表:sys_role
- 权限表:sys_permission
- 用户角色关联表:sys_user_role
- 角色权限关联表:sys_role_permission
创建权限模块数据表的 sql 语句如下:
-- 创建数据库
CREATE DATABASE IF NOT EXISTS `auth_db_02` DEFAULT CHARACTER SET utf8 COLLATE utf8_unicode_ci;
USE `auth_db_02`;
-- 用户表
CREATE TABLE IF NOT EXISTS `sys_user` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`username` VARCHAR(50) NOT NULL COMMENT '用户名',
`password` VARCHAR(255) NOT NULL COMMENT '密码(MD5加密)',
`nickname` VARCHAR(50) DEFAULT NULL COMMENT '昵称',
`email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
`phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
`status` TINYINT DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` TINYINT DEFAULT 0 COMMENT '是否删除:0-未删除,1-已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`),
KEY `idx_status` (`status`),
KEY `idx_deleted` (`deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';
-- 角色表
CREATE TABLE IF NOT EXISTS `sys_role` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`role_code` VARCHAR(50) NOT NULL COMMENT '角色编码',
`role_name` VARCHAR(50) NOT NULL COMMENT '角色名称',
`description` VARCHAR(255) DEFAULT NULL COMMENT '描述',
`status` TINYINT DEFAULT 1 COMMENT '状态:0-禁用,1-启用',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` TINYINT DEFAULT 0 COMMENT '是否删除:0-未删除,1-已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_code` (`role_code`),
KEY `idx_status` (`status`),
KEY `idx_deleted` (`deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色表';
-- 权限表
CREATE TABLE IF NOT EXISTS `sys_permission` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`permission_code` VARCHAR(50) NOT NULL COMMENT '权限编码', -- system:user:add
`permission_name` VARCHAR(50) NOT NULL COMMENT '权限名称',
`description` VARCHAR(255) DEFAULT NULL COMMENT '描述',
`parent_id` BIGINT DEFAULT NULL COMMENT '父权限ID',
`resource_type` VARCHAR(20) DEFAULT 'menu' COMMENT '资源类型:menu-菜单,button-按钮',
`path` VARCHAR(255) DEFAULT NULL COMMENT '路径',
`component` VARCHAR(255) DEFAULT NULL COMMENT '组件路径',
`icon` varcahr(50) comement '图标',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted` TINYINT DEFAULT 0 COMMENT '是否删除:0-未删除,1-已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_permission_code` (`permission_code`),
KEY `idx_parent_id` (`parent_id`),
KEY `idx_deleted` (`deleted`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='权限表';
-- 用户角色关联表
CREATE TABLE IF NOT EXISTS `sys_user_role` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`user_id` BIGINT NOT NULL COMMENT '用户ID',
`role_id` BIGINT NOT NULL COMMENT '角色ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_role` (`user_id`, `role_id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_role_id` (`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户角色关联表';
-- 角色权限关联表
CREATE TABLE IF NOT EXISTS `sys_role_permission` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`role_id` BIGINT NOT NULL COMMENT '角色ID',
`permission_id` BIGINT NOT NULL COMMENT '权限ID',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_role_permission` (`role_id`, `permission_id`),
KEY `idx_role_id` (`role_id`),
KEY `idx_permission_id` (`permission_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='角色权限关联表';
-- 初始化数据
-- 插入默认管理员用户(密码:123456,MD5:e10adc3949ba59abbe56e057f20f883e)
INSERT INTO `sys_user` (`username`, `password`, `nickname`, `email`, `status`, `create_time`, `deleted`)
VALUES ('admin', 'e10adc3949ba59abbe56e057f20f883e', '管理员', 'admin@example.com', 1, NOW(), 0);
-- 插入默认角色
INSERT INTO `sys_role` (`role_code`, `role_name`, `description`, `status`, `create_time`, `deleted`)
VALUES
('admin', '管理员', '系统管理员', 1, NOW(), 0),
('user', '普通用户', '普通用户角色', 1, NOW(), 0);
-- 插入默认权限
BEGIN;
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (1, 'system:management', '系统管理', '系统管理模块', NULL, 'menu', '/system', 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:46', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (2, 'content:management', '内容管理', '内容管理模块', NULL, 'menu', '/content', 'Profile', 'document', '2025-11-10 16:19:34', '2025-11-12 17:26:02', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (3, 'user:management', '用户管理', '用户管理菜单', 1, 'menu', 'user', 'User', 'User', '2025-11-10 16:19:34', '2025-11-13 23:51:09', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (4, 'role:management', '角色管理', '角色管理菜单', 1, 'menu', 'role', 'Role', 'Grid', '2025-11-10 16:19:34', '2025-11-13 23:54:54', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (5, 'permission:management', '权限管理', '权限管理菜单', 1, 'menu', 'permission', 'Permission', 'Promotion', '2025-11-10 16:19:34', '2025-11-13 23:55:08', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (6, 'article:management', '文章管理', '文章管理菜单', 2, 'menu', 'article', 'Profile', 'article', '2025-11-10 16:19:34', '2025-11-13 15:08:48', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (7, 'category:management', '分类管理', '分类管理菜单', 2, 'menu', 'category', 'Profile', 'Bicycle', '2025-11-10 16:19:34', '2025-11-13 15:08:52', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (8, 'user:add', '新增用户', '新增用户按钮', 3, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (9, 'user:edit', '修改用户', '修改用户按钮', 3, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (10, 'user:delete', '删除用户', '删除用户按钮', 3, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (11, 'user:import', '导入用户', '导入用户按钮', 3, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (12, 'user:export', '导出用户', '导出用户按钮', 3, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (13, 'role:add', '新增角色', '新增角色按钮', 4, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (14, 'role:edit', '修改角色', '修改角色按钮', 4, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (15, 'role:delete', '删除角色', '删除角色按钮', 4, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (16, 'role:import', '导入角色', '导入角色按钮', 4, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (17, 'role:export', '导出角色', '导出角色按钮', 4, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (18, 'permission:add', '新增权限', '新增权限按钮', 5, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (19, 'permission:edit', '修改权限', '修改权限按钮', 5, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (20, 'permission:delete', '删除权限', '删除权限按钮', 5, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (21, 'permission:import', '导入权限', '导入权限按钮', 5, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (22, 'permission:export', '导出权限', '导出权限按钮', 5, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (23, 'article:add', '新增文章', '新增文章按钮', 6, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (24, 'article:edit', '修改文章', '修改文章按钮', 6, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (25, 'article:delete', '删除文章', '删除文章按钮', 6, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (26, 'article:import', '导入文章', '导入文章按钮', 6, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (27, 'article:export', '导出文章', '导出文章按钮', 6, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (28, 'category:add', '新增分类', '新增分类按钮', 7, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (29, 'category:edit', '修改分类', '修改分类按钮', 7, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (30, 'category:delete', '删除分类', '删除分类按钮', 7, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (31, 'category:import', '导入分类', '导入分类按钮', 7, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (32, 'category:export', '导出分类', '导出分类按钮', 7, 'button', NULL, 'Profile', 'Setting', '2025-11-10 16:19:34', '2025-11-13 23:53:06', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (33, 'picture:list', '轮播图管理', '管理首页轮播图', 2, 'menu', '/caracoul', 'Profile', 'Upload', '2025-11-10 22:38:30', '2025-11-12 17:26:02', 1);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (34, 'category:list', '轮播图管理', '', 2, 'menu', '/lunbotu', 'Profile', 'Tools', '2025-11-11 09:54:06', '2025-11-12 17:26:02', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (35, 'system:news', '新闻资讯管理', '', 2, 'menu', 'news', 'News', 'Basketball', '2025-11-17 14:52:51', '2025-11-17 14:52:51', 0);
INSERT INTO `sys_permission` (`id`, `permission_code`, `permission_name`, `description`, `parent_id`, `resource_type`, `path`, `component`, `icon`, `create_time`, `update_time`, `deleted`) VALUES (36, 'lunbotu', '轮播图管理', '', 2, 'menu', '/lunbotu', 'LunBoTu', 'Briefcase', '2025-11-17 15:33:28', '2025-11-17 15:33:28', 0);
COMMIT;
-- 为管理员用户分配管理员角色
INSERT INTO `sys_user_role` (`user_id`, `role_id`)
SELECT u.id, r.id FROM `sys_user` u, `sys_role` r
WHERE u.username = 'admin' AND r.role_code = 'admin';
-- 为管理员角色分配所有权限
INSERT INTO `sys_role_permission` (`role_id`, `permission_id`)
SELECT r.id, p.id FROM `sys_role` r, `sys_permission` p
WHERE r.role_code = 'admin';
# 基于 SpringBoot3 搭建后端 API 接口项目
这节课我们来搭建一个 基于 Springboot3 的后端 API 接口项目,这个项目中将包含一些通用的操作:
- 统一的返回结果
- 全局异常处理
- 自定义异常以及捕获
- 文件上传和下载的通用 Controller
# 创建 Springboot3 项目

# 封装返回统一结构数据
前后端分离开发的项目,后端要约定返回固定格式的数据,通常包括:code(状态码),message(消息),data(数据),可能还有 timestamp(时间戳)
common/Result.java
package com.maomao.auth.common;
import lombok.Data;
@Data
public class Result implements Serializable{
private Integer code; //状态码
private String message; //提示信息
private Object data; // 数据
private Long timestamp; //时间戳
//构造方法私有化
private Result() {
this.timestamp = System.currentTimeMillis();
}
public static Result success(Object data) {
Result result = new Result();
result.setCode(200);
result.setMessage("操作成功");
result.setData(data);
return result;
}
public static Result success() {
return success(null);
}
public static Result error(String message) {
Result result = new Result();
result.setCode(500);
result.setMessage(message);
result.setData(null);
return result;
}
public static Result error(Integer code, String message) {
Result result = new Result();
result.setCode(code);
result.setMessage(message);
result.setData(null);
return result;
}
}
# 全局异常处理
如果异常不进行处理,就会抛出下图所示的异常页面,这样不友好,也不安全,会暴露你的代码逻辑。

common/GlobalExceptionHandler.java
package com.maomao.auth.common;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
public class GlobleExceptionHandler {
private static Logger logger = LoggerFactory.getLogger(GlobleExceptionHandler.class);
@ExceptionHandler(Exception.class)
@ResponseBody
public Result error(Exception e) {
logger.error("系统异常",e);
return Result.error(e.getMessage());
}
}
# 自定义异常处理
全局异常虽然能捕获异常信息,但是给用户的提示太过模糊,如果想提示具体一点的错误提示,比如账号冻结等,咱们就需要自定义异常(自定义异常提示信息).
common/CustomException.java
package com.maomao.auth.common;
@Data
public class CustomException extends RuntimeException{
private int code;
public CustomException(int code, String message) {
super(message);
this.code = code;
}
public CustomException(String message) {
super(message);
this.code = 500;
}
}
在全局异常处理的类中,增加一个异常处理的方法
package com.maomao.auth.common;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
@ControllerAdvice
public class GlobleExceptionHandler {
private static Logger logger = LoggerFactory.getLogger(GlobleExceptionHandler.class);
@ExceptionHandler(Exception.class)
@ResponseBody
public Result error(Exception e) {
logger.error("系统异常",e);
return Result.error("系统异常");
}
@ExceptionHandler(CustomException.class)
@ResponseBody
public Result error(CustomException e) {
logger.error("自定义异常",e);
return Result.error(e.getCode(),e.getMessage());
}
}
# 通用的文件上传功能
1️⃣** pom.xml: 导入 Hutool 依赖**
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.35</version>
</dependency>
2️⃣ contoller/FileController.java : 编写文件上传的 api 接口 /upload
package com.maomao.auth.authapi.controller;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.springboot.learn.springbootlearn.common.Result;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.util.UUID;
@RestController
public class FileController {
@Value("${files.upload.path}")
private String uploadPath;//文件上传的目录
@PostMapping("/upload")
public Result uploadFile(HttpServletRequest request, @RequestParam("file")MultipartFile file) throws Exception{
//获取文件的后缀名(类型)
String ext = FileUtil.extName(file.getOriginalFilename());
//创建文件存储目录,每天一个文件夹,用 yyyyMMdd来命名
String datePath = DateUtil.format(new Date(),"yyyyMMdd") + "/";
File dir = new File(uploadPath + datePath);
if (dir.exists() == false){
dir.mkdirs();
}
//生成唯一标识码(文件名)
String uuid = UUID.randomUUID().toString();
//保存文件
File dest = new File(uploadPath +datePath+ uuid + "." + ext);
file.transferTo(dest);
String url = StrUtil.removeSuffix(request.getRequestURL(),"/upload") + "/api/"+datePath + uuid + "." + ext;
return Result.success(url);
}
}
3️⃣ application.yml : 配置文件上传的目录和文件上传的大小
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 20MB
files:
upload:
path: /Users/yaoyaomice/learning/
4️⃣ 使用 PostMan 测试文件上传的功能

5️⃣ 配置静态资源访问: config/WebConfig.java
package com.maomao.auth.authapi.common;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Value("${files.upload.path}")
private String uploadPath;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/api/**").addResourceLocations("file:" + uploadPath);
}
}
6️⃣ 使用 PostMan 访问上传的资源文件,当然浏览器访问也是可以的

# 通用的文件下载功能
1️⃣ 在 contoller/FileController.java 中增加下载的 api 接口方法
package com.springboot.learn.springbootlearn.controller;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.springboot.learn.springbootlearn.common.CustomException;
import com.springboot.learn.springbootlearn.common.Result;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.net.URLEncoder;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.UUID;
@RestController
public class FileController {
@Value("${files.download.path}")
private String downloadPath;
@GetMapping("/download/{filename}")
public ResponseEntity<Resource> downloadFile(@PathVariable String filename) throws Exception {
//获取文件路径
Path path = Paths.get(downloadPath).resolve(filename).normalize();
Resource resource = new UrlResource(path.toUri());
if (resource.exists() && resource.isReadable()){
return ResponseEntity.ok()
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.header(HttpHeaders.CONTENT_DISPOSITION,"attachment;filename=\""+ URLEncoder.encode(resource.getFilename(),"utf-8") +"\"")
.body(resource);
}else{
throw new CustomException("文件不存在!");
}
}
}
代码解释:
通过Paths.get(FILE_DIRECTORY).resolve(fileName).normalize()方法获取文件路径,并使用UrlResource类加载文件资源。
如果文件存在且可读,则返回一个包含文件内容的ResponseEntity对象,并设置适当的 HTTP 头部(如Content-Disposition)以指示浏览器下载文件。如果文件不存在或不可读,则返回 404 Not Found 响应
2️⃣ 在 application.yml 中配置下载文件的目录
files:
upload:
path: /Users/yaoyaomice/learning/
download:
path: /Users/yaoyaomice/learning/
# 编写用户管理的 API 接口&PostMan 测试
# API 接口编写规范
每个模块管理的 API 接口都至少包含入了如下 5 个 API 接口,使用 Restful 风格的 URL.
用户管理模块的模块名称为: user
- 添加接口:post 请求 映射名称: /user
- 修改接口:put 请求 映射名称: /user/{id}
- 删除接口(支持批量删除): delete 请求 映射名称: /user/{ids}
- 查询接口(多条件分页查询): get 请求 映射名称: /user/list
- 查询单个: get 请求 映射名称: /user/{id}
# 集成 MyBatisPlus 框架
MyBatis-Plus (opens new window) 是一个 MyBatis (opens new window) 的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。
MybatisPlus 的官方文档: https://baomidou.com/getting-started/ (opens new window)
① 添加依赖
<!--MyBatisPlus 依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.14</version>
</dependency>
<!-- MyBatisPlus 分页插件 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
<version>3.5.14</version>
</dependency>
<!--MySQL 驱动包-->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.32</version>
</dependency>
② application.yml 中配置数据源
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/auth_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: 12345678
3️⃣ 配置分页插件
config/MybatisPlusConfig.java
package com.maomao.auth.authapi.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MybatisPlusConfig {
/**
* 添加分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 如果配置多个插件, 切记分页最后添加
// 如果有多数据源可以不配具体类型, 否则都建议配上具体的 DbType
return interceptor;
}
}
4️⃣ 启动类上添加 @MapperScan 注解
@SpringBootApplication
@MapperScan("com.maomao.auth.authapi.mapper")
public class AuthApiApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApiApplication.class, args);
}
}
# API 接口编写顺序
① 编写实体类
② 编写数据访问接口 mapper
③ 编写业务逻辑类
④ 编写控制器(api 接口)
⑤ 使用 postman 测试 API 接口
# 编写角色管理的 API 接口&PostMan 测试
角色管理模块的模块名称为: role
- 添加接口:post 请求 映射名称: /role
- 修改接口:put 请求 映射名称: /role/{id}
- 删除接口(支持批量删除): delete 请求 映射名称: /role/{ids}
- 查询接口(多条件分页查询): get 请求 映射名称: /role/list
- 查询单个: get 请求 映射名称: /role/{id}
- 角色授权:post 请求 映射名称:/role/assign
# 编写权限管理的 API 接口&PostMan 测试
权限管理模块的模块名称为: permission
- 添加接口:post 请求 映射名称: /permission
- 修改接口:put 请求 映射名称: /permission/{id}
- 删除接口(支持批量删除): delete 请求 映射名称: /permission/{ids}
- 查询接口(多条件分页查询): get 请求 映射名称: /permission/list
- 查询单个: get 请求 映射名称: /permission/{id}
# 基于 Vue3 和 ElementPlus 搭建前端项目(主页面布局)
# 创建 Vue3 项目&项目瘦身
1️⃣ 创建 Vue3 项目,项目名称为:auth-vue
npm create vue@latest
cd auth-vue
npm install
npm run dev

2️⃣ 项目瘦身
只保留一个默认首页:Login.vue,其余的都不保留
3️⃣ 404 页面配置
① 创建一个 404.vue 页面

<template>
<div class="container">
<img src="@/assets/img/404.png" alt="" />
<div class="back_home"><a href=" /">返回首页</a></div>
</div>
</template>
<style scoped>
.container {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
.container img {
height: 350px;
}
.container .back_home a {
font-size: 20px;
color: cadetblue;
}
</style>
② 配置路由 router/index.js
import { createRouter, createWebHistory } from "vue-router";
import HomeView from "../views/Home.vue";
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: "/",
name: "home",
component: HomeView,
},
{
path: "/notFound",
name: "notFound",
component: () => import("../views/404.vue"),
},
{
path: "/:pathMatch(.*)",
redirect: "/notFound",
},
],
});
export default router;
# Vue3 集成 ElementPlus 组件库
ElementPlus 是一款基于 Vue 3,面向设计师和开发者的组件库,也可以成为前端 UI 框架,让开发者可以快速的构建精美的页面。
官网: https://element-plus.org/zh-CN/ (opens new window)
国内镜像:https://cn.element-plus.org/zh-CN/ (opens new window)
① 安装依赖
npm install element-plus --save
② 在 main.js 中引入 element-plus
import { createApp } from "vue";
import ElementPlus from "element-plus";
import "element-plus/dist/index.css";
import App from "./App.vue";
const app = createApp(App);
app.use(ElementPlus);
app.mount("#app");
# ElementPlus 中 icon 图标的使用
① 安装 icon-vue 依赖
npm install @element-plus/icons-vue
② 全局注册图标组件
import * as ElementPlusIconsVue from "@element-plus/icons-vue";
const app = createApp(App);
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.component(key, component);
}
# ElementPlus 国际化处理
main.js
import ElementPlus from "element-plus";
import zhCn from "element-plus/es/locale/lang/zh-cn";
app.use(ElementPlus, {
locale: zhCn,
});
# 基于 ElementPlus 组件库搭建管理员主页面布局
需要学习的组件 el-container : https://element-plus.org/zh-CN/component/container (opens new window)
用到的组件:
- el-aside: 侧边栏
- el-menu: 菜单
- el-breadcrumb: 面包屑
- el-dropdown: 下拉菜单
- router-view
logo 图片: 

layout/MainLayout.vue
# 实现用户管理的功能(增删改查)

# axios 简介
Axios 是一个基于 promise 的网络请求库,可以用于浏览器和 node.js
Axios 使用简单,包尺寸小且提供了易于扩展的接口
官方网址: https://www.axios-http.cn/ (opens new window)
① 安装 axios
npm install axios
② 使用 axios 发送请求
import axios from "axios";
//发送Get请求
axios.get("http://localhost:8080/sysuser/list").then((res) => {
console.log(res);
});
# 处理跨域问题
由于浏览器的同源策略,为了安全,禁止发送 Ajax 请求到其他域中, 5173 --> 8080

一般情况都在服务端进行跨域处理,局部跨域处理可以在控制器上使用 @CrossOrigin 注解实现,但方便起见一般都会进行全局的跨域处理。
confit/CorsConfig.java
package com.maomao.auth.authapi.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**") // 匹配所有路径 /login /register /sysuser/list
.allowedOriginPatterns("*") // 允许所有源,你也可以配置特定的源,如"http://localhost:51730"
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的方法
.allowedHeaders("*") // 允许所有头
.allowCredentials(true) // 允许发送cookie
.maxAge(3600); // 预检请求的缓存时间(秒)
}
}
# 封装 axios 操作
util/request.js
import axios from "axios";
import { ElMessage } from "element-plus";
const request = axios.create({
baseURL: import.meta.env.VITE_APP_API_BASE_URL,
timeout: 30000, //请求超时时间
});
//request拦截器
request.interceptors.request.use(
(config) => {
//取出本地存储中的用户信息
let user = JSON.parse(sessionStorage.getItem("login_user"));
if (user) {
config.headers["token"] = user.token;
}
return config;
},
(error) => {
return Promise.reject(error);
},
);
//response拦截器
request.interceptors.response.use(
(response) => {
return response.data;
},
(error) => {
if (error.response) {
if (error.response.status == 404) {
ElMessage.error("未找到请求接口");
} else if (error.response.status == 500) {
ElMessage.error("接口调用异常,请查看后台接口日志");
} else {
ElMessage.error(error.message);
}
} else {
ElMessage.error(error.message);
}
return Promise.reject(error);
},
);
export default request;
# 编写前端的 API 接口调用类
api/user.js
import http from "@/utils/request.js";
export function listUsers(params) {
return http.get("/user/list", {
params: params,
});
}
export function getUser(id) {
return http.get(`/user/${id}`);
}
export function createUser(data) {
return http.post("/user", data);
}
export function updateUser(id, data) {
return http.put(`/user/${id}`, data);
}
export function deleteUsers(ids) {
// Backend accepts comma-separated path variable
const path = Array.isArray(ids) ? ids.join(",") : String(ids);
return http.delete(`/user/${path}`);
}
# 编写视图页面 User.vue & 配置路由
<template>
<div style="padding: 16px;">
<el-card>
<template #header>
<div
style="display:flex;align-items:center;justify-content:space-between;"
>
<span>用户管理</span>
</div>
</template>
<div style="margin-bottom:12px;display:flex;gap:8px;flex-wrap:wrap;">
<el-input
v-model="query.username"
placeholder="用户名"
clearable
style="width:200px;"
/>
<el-input
v-model="query.nickname"
placeholder="昵称"
clearable
style="width:200px;"
/>
<el-select
v-model="query.status"
placeholder="状态"
clearable
style="width:140px;"
>
<el-option :value="1" label="启用" />
<el-option :value="0" label="禁用" />
</el-select>
<el-select
v-model="query.roleIds"
placeholder="角色"
clearable
style="width:200px;"
multiple
>
<el-option
v-for="item in roles"
:key="item.id"
:value="item.id"
:label="item.roleName"
/>
</el-select>
<el-button type="primary" @click="loadData(1)">查询</el-button>
<el-button @click="resetQuery">重置</el-button>
</div>
<div style="margin: 15px 0px;">
<el-button type="primary" :icon="Plus" @click="handleAdd"
>新增</el-button
>
<el-button
type="primary"
:icon="Edit"
:disabled="selectedIds.length !== 1"
@click="handleEdit"
>编辑</el-button
>
<el-button
type="danger"
:icon="Delete"
:disabled="!selectedIds.length"
@click="handleDelete"
>批量删除</el-button
>
<el-button type="success" :icon="Download" @click="handleExport"
>导出</el-button
>
<el-button type="warning" :icon="Upload" @click="handleImport"
>导入</el-button
>
</div>
<el-table
:data="tableData"
border
stripe
@selection-change="onSelectionChange"
>
<el-table-column type="selection" width="45" />
<el-table-column prop="id" label="ID" width="80" />
<el-table-column prop="username" label="用户名" min-width="140" />
<el-table-column prop="nickname" label="昵称" min-width="140" />
<el-table-column prop="email" label="邮箱" min-width="180" />
<el-table-column prop="phone" label="手机号" min-width="140" />
<el-table-column label="状态" width="100">
<template #default="{ row }">
<el-tag :type="row.status === 1 ? 'success' : 'info'">{{
row.status === 1 ? "启用" : "禁用"
}}</el-tag>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间" min-width="180">
<template #default="{ row }">{{ row.createTime }}</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="180">
<template #default="{ row }">
<el-button type="primary" text @click="handleEdit(row)"
>编辑</el-button
>
<el-button type="danger" text @click="handleDelete(row)"
>删除</el-button
>
</template>
</el-table-column>
</el-table>
<div style="margin-top:12px;display:flex;justify-content:flex-end;">
<el-pagination
background
layout="total, sizes, prev, pager, next, jumper"
:total="total"
:page-sizes="[10, 20, 50, 100]"
v-model:page-size="query.pageSize"
v-model:current-page="query.pageNum"
@size-change="loadData"
@current-change="loadData"
/>
</div>
</el-card>
<el-dialog
v-model="dialogVisible"
:title="dialogMode === 'create' ? '新增用户' : '编辑用户'"
width="520px"
>
<el-form :model="form" :rules="rules" ref="formRef" label-width="88px">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" />
</el-form-item>
<el-form-item
label="密码"
v-if="dialogMode === 'create'"
prop="password"
>
<el-input v-model="form.password" type="password" />
</el-form-item>
<el-form-item label="昵称" prop="nickname">
<el-input v-model="form.nickname" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" />
</el-form-item>
<el-form-item label="状态" prop="status">
<el-select v-model="form.status" placeholder="请选择">
<el-option :value="1" label="启用" />
<el-option :value="0" label="禁用" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="dialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitForm">确定</el-button>
</template>
</el-dialog>
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from "vue";
import {
listUsers,
createUser,
updateUser,
deleteUsers,
getUser,
} from "@/api/user.js";
import { listRoles } from "@/api/role.js";
import { ElMessage, ElMessageBox } from "element-plus";
import { Plus, Edit, Delete, Download, Upload } from "@element-plus/icons-vue";
import { download } from "@/utils/request.js";
const query = reactive({
username: "",
nickname: "",
status: "",
roleIds: [],
pageNum: 1,
pageSize: 10,
});
const tableData = ref([]);
const total = ref(0);
const selectedIds = ref([]);
const roles = ref([]);
const dialogVisible = ref(false);
const dialogMode = ref("create"); // 'create' | 'edit'
const formRef = ref();
const form = ref({
id: undefined,
username: "",
password: "",
nickname: "",
email: "",
phone: "",
status: 1,
});
const rules = {
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
nickname: [{ required: true, message: "请输入昵称", trigger: "blur" }],
};
const loadData = async () => {
const res = await listUsers(query);
tableData.value = res.data?.list || [];
total.value = res.data?.total || 0;
};
const resetQuery = () => {
query.username = "";
query.nickname = "";
query.status = undefined;
query.pageNum = 1;
query.pageSize = 10;
query.roleIds = [];
loadData();
};
const onSelectionChange = (rows) => {
selectedIds.value = rows.map((r) => r.id);
};
const handleAdd = () => {
dialogMode.value = "create";
form.value = {
id: undefined,
username: "",
password: "",
nickname: "",
email: "",
phone: "",
status: 1,
};
dialogVisible.value = true;
};
const handleEdit = async (row) => {
let ids = row.id || selectedIds.value;
dialogMode.value = "edit";
let res = await getUser(ids);
form.value = res.data;
dialogVisible.value = true;
};
const submitForm = () => {
formRef.value.validate(async (valid) => {
if (!valid) return;
if (dialogMode.value === "create") {
await createUser(form.value);
ElMessage.success("添加成功");
} else {
const id = form.value.id;
delete form.value.id;
await updateUser(id, form.value);
ElMessage.success("更新成功");
}
dialogVisible.value = false;
loadData();
});
};
const handleDelete = async (row) => {
const ids = row.id || selectedIds.value;
await ElMessageBox.confirm(`确认删除编号【${ids}】吗?`, "提示", {
type: "warning",
});
await deleteUsers(ids);
ElMessage.success("删除成功");
loadData();
};
const handleExport = () => {
download("/user/export", query);
};
const handleImport = () => {};
onMounted(() => {
loadData();
listRoles({ pageSize: 100 }).then((res) => {
roles.value = res.data.list;
});
});
</script>
<style scoped></style>
# 实现用户角色的关联
- 列表中显示关联的角色名称
- 列表中根据选中的角色查询用户
- 添加时添加用户的角色
- 修改时回显用户的角色
# 实现用户的导出功能
添加依赖
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>5.4.1</version>
</dependency>
UserController.java 增加 /export 的 api 接口
@RequestMapping("/export")
public void export(HttpServletResponse response, SysUser sysUser) throws IOException {
QueryWrapper<SysUser> wrapper = new QueryWrapper<>();
wrapper.like(StrUtil.isNotEmpty(sysUser.getUsername()),"username",sysUser.getUsername());
List<SysUser> list = sysUserService.list(wrapper);
ExcelUtils.export(response,list,"用户数据");
}
ExcelUtil.java 的工具类
package com.maomao.authapi.utils;
import cn.hutool.core.io.IoUtil;
import cn.hutool.poi.excel.ExcelUtil;
import cn.hutool.poi.excel.ExcelWriter;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;
import java.util.List;
public class ExcelUtils {
/**
* 导出Excel
* @param response 响应对象
* @param list 导出的数据集合
* @param filename 导出的文件名
*/
public static void export(HttpServletResponse response, List list, String filename){
try {
ExcelWriter writer = ExcelUtil.getWriter();
writer.write(list,true);
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
response.setHeader("Content-Disposition","attachement;filename="+ URLEncoder.encode(filename,"UTF-8") +".xlsx");
ServletOutputStream os = response.getOutputStream();
writer.flush(os,true);
writer.close();
IoUtil.close(os);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
前端调用后端的 export 接口,由于要下载文件,前端不能发送 Ajax 请求,直接访问后端接口地址。
修改 uitl/request.js, 增加 download 的方法:
export function download(url, params) {
//构建查询参数
let searchParam = new URLSearchParams();
for (let key in params) {
searchParam.append(key, params[key]);
}
//?name=lisi&nickname=zhangsan
//通过 url 传递 token
let user = JSON.parse(sessionStorage.getItem("login_user"));
if (user) {
searchParam.append("token", user.token);
}
const base_url = import.meta.env.VITE_APP_API_BASE_URL;
window.location = base_url + url + "?" + searchParam.toString();
}
给导出按钮添加 click 事件
const handleExport = () => {
download("/user/export", query);
};
# 实现用户的导入功能
编写数据导入的 api 接口
@PostMapping("/import")
public Result importData(HttpServletResponse response,@RequestParam("file") MultipartFile file) throws IOException {
ExcelReader reader = ExcelUtil.getReader(file.getInputStream());
//调用Hutool中ExcelReader的readAll方法,将读取的数据封装成List集合
List<SysUser> list = reader.readAll(SysUser.class);
//批量保存到数据库
Integer count = 0;
for (SysUser item : list) {
item.setId(null);
sysUserService.save(item);
count ++;
}
Map<String,Object> map = new HashMap<>();
map.put("count",count);
return Result.success(map);
}
使用 el-upload 重构导入的按钮
<el-upload
style="margin-left: 10px;"
:show-file-list="false"
:on-success="handleImportSuccess"
:action="importUrl"
>
<el-button type="warning" icon="Upload">导入</el-button>
</el-upload>
const importUrl = ref("");
importUrl.value = import.meta.env.VITE_APP_API_BASE_URL + "/user/import";
const handleImportSuccess = (res) => {
if (res.code == 200) {
ElMessage.success("导入成功!共导入" + res.data.count + "条数据。");
loadData();
} else {
ElMessage.error("导入失败!" + res.message);
}
};
# 实现角色管理的功能(增删改查+授权)
# 类比用户管理实现角色管理的功能
- 编写 调用后端接口的 api 接口: api/role.js
- 编写视图页面: Role.vue
# 实现角色授权功能: 显示权限菜单
使用 el-tree 组件渲染显示权限(菜单)的信息。

el-tree 组件要求的数据结构:
{
id: 1,
label: 'Level one 2',
children: [
{
id: 2,
label: 'Level two 2-1',
children: [
{
id: 3,
label: 'Level three 2-1-1',
},
],
}
}
1️⃣ 编写后端接口,返回 el-tree 所需的数据
contoller/PermissionController.java
@GetMapping("/treelist")
public Result treeList() {
List<SysPermission> permissions = sysPermissionService.selectPermissionList();
return Result.success(permissions);
}
service/impl/PermissionServiceImpl.java
@Override
public List<SysPermission> selectPermissionList() {
List<SysPermission> list = baseMapper.selectList(null);
return this.buildMenuTree(list);
}
private List<SysPermission> buildMenuTree(List<SysPermission> menus)
{
List<SysPermission> returnList = new ArrayList<SysPermission>();
List<Long> tempList = new ArrayList<Long>();
for (SysPermission dept : menus)
{
tempList.add(dept.getId());
}
for (Iterator<SysPermission> iterator = menus.iterator(); iterator.hasNext();)
{
SysPermission menu = (SysPermission) iterator.next();
// 如果是顶级节点, 遍历该父节点的所有子节点
if (!tempList.contains(menu.getParentId()))
{
recursionFn(menus, menu);
returnList.add(menu);
}
}
if (returnList.isEmpty())
{
returnList = menus;
}
return returnList;
}
private void recursionFn(List<SysPermission> list, SysPermission t)
{
// 得到子节点列表
List<SysPermission> childList = getChildList(list, t);
t.setChildren(childList);
for (SysPermission tChild : childList)
{
if (hasChild(list, tChild))
{
recursionFn(list, tChild);
}
}
}
/**
* 得到子节点列表
*/
private List<SysPermission> getChildList(List<SysPermission> list, SysPermission t)
{
List<SysPermission> tlist = new ArrayList<SysPermission>();
Iterator<SysPermission> it = list.iterator();
while (it.hasNext())
{
SysPermission n = (SysPermission) it.next();
if (n.getParentId() != null && n.getParentId().longValue() == t.getId().longValue())
{
tlist.add(n);
}
}
return tlist;
}
/**
* 判断是否有子节点
*/
private boolean hasChild(List<SysPermission> list, SysPermission t)
{
return getChildList(list, t).size() > 0;
}
2️⃣ 实现 el-tree 的数据渲染
<el-dialog v-model="dialogVisibleAssign" title="角色授权" width="520px">
<el-tree
ref="treeRef"
:data="permissionData"
:default-expanded-keys="[1, 2]"
show-checkbox
node-key="id"
:props="defaultProps"
/>
<template #footer>
<el-button @click="dialogVisibleAssign = false">取消</el-button>
<el-button type="primary" @click="submitAssignForm">确定</el-button>
</template>
</el-dialog>
const handleAssign = (row) => {
dialogVisibleAssign.value = true;
role.value = row;
treelist().then((res) => {
permissionData.value = res.data;
});
};
3️⃣ 实现授权操作
给确定按钮绑定点击事件
const submitAssignForm = () => {
let nodes = treeRef.value.getCheckedNodes(false, true);
assignPermissions({
id: role.value.id,
permissionIds: nodes.map((item) => item.id),
}).then((res) => {
ElMessage.success("授权成功");
dialogVisibleAssign.value = false;
});
};
# 实现菜单授权功能:回显角色权限
1️⃣ 编写后端 api 接口,用于返回角色拥有的权限列表
contoller/PermissionController.java
@GetMapping("/role/{id}")
public Result rolePermission(@PathVariable Long id) {
List<SysRolePermission> rolePermissions = sysRolePermissionMapper.selectList(new QueryWrapper<SysRolePermission>().eq("role_id", id));
return Result.success(rolePermissions);
}
2️⃣ 打开授权对话框时加载角色权限并选中
//角色授权
const handleAssign = (row) => {
dialogVisibleAssign.value = true;
role.value = row;
treelist().then((res) => {
permissionData.value = res.data;
//查询当前角色的权限
listbyrole(row.id).then((r) => {
//使用这种方式不行,子节点会级联选中
//treeRef.value.setCheckedKeys(r.data.map(item => item.permissionId));
r.data.forEach((item) => {
treeRef.value.setChecked(item.permissionId, true, false);
});
});
});
};
# 实现权限管理的功能
# 树形表格显示

1️⃣ 去掉分页, 增加 row-key 属性
<el-table :data="tableData" row-key="id" border stripe>
<el-table-column prop="permissionName" label="权限名称" min-width="140" />
<el-table-column prop="permissionCode" label="权限编码" min-width="140" />
</el-table>
2️⃣ 修改 api 接口,返回树形结构的数据(children)
@GetMapping("/list")
public Result list(@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize,
SysPermission permission) {
QueryWrapper<SysPermission> queryWrapper = new QueryWrapper<>();
List<SysPermission> permissions = sysPermissionService.list(queryWrapper);
return Result.success(permissions);
}
//PermissionServiceImpl.java
@Override
public List<SysPermission> list(Wrapper<SysPermission> queryWrapper) {
List<SysPermission> list = super.list(queryWrapper);
return this.buildMenuTree(list); //构建树形菜单数据
}
# TreeSelect 组件实现父级菜单的选择

<el-form-item label="上级菜单" prop="parentId">
<el-tree-select
v-model="form.parentId"
:data="treeDataList"
:props="props"
check-strictly
node-key="id"
:render-after-expand="false"
/>
</el-form-item>
const props = {
label: "permissionName",
};
onMounted(() => {
treelist().then((res) => {
treeDataList.value = res.data;
});
});
# 封装图标选择器

1️⃣ 在 main.js 中注册所有的组件且存入数组
app.config.globalProperties.$icons = [];
for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
app.config.globalProperties.$icons.push(key);
app.component(key, component);
}
2️⃣ 定义组件 @/components/ElIconPicker.vue
<script setup>
import { defineEmits, defineProps, getCurrentInstance } from "vue";
const {
appContext: {
app: {
config: { globalProperties },
},
},
} = getCurrentInstance();
const props = defineProps({
modelValue: {
type: String,
default: "",
},
});
const emits = defineEmits(["update:modelValue"]);
</script>
<template>
<el-popover trigger="focus" :width="256">
<template #reference>
<el-button :icon="modelValue">{{ modelValue }}</el-button>
</template>
<div class="el-icon-picker">
<component
v-for="icon in globalProperties.$icons"
:key="icon"
:class="[icon, 'icon', { 'icon-active': icon == modelValue }]"
:is="icon"
@click="emits('update:modelValue', icon)"
>
</component>
</div>
</el-popover>
</template>
<style scoped>
.el-icon-picker {
height: 256px;
overflow-y: scroll;
display: flex;
justify-content: space-around;
flex-wrap: wrap;
}
.icon {
display: inline-block;
width: 24px;
height: 24px;
color: var(--el-text-color-regular);
font-size: 20px;
border-radius: 4px;
cursor: pointer;
text-align: center;
line-height: 45px;
margin: 5px;
}
.icon:hover {
color: var(--el-color-primary);
}
.icon-active {
color: var(--el-color-primary);
}
</style>
3️⃣ 在 Permission.vue 中使用组件
<el-icon-picker v-model="form.icon"></el-icon-picker>
<script setup>
import ElIconPicker from "@/components/ElIconPicker.vue";
</script>
# 实现用户登录、注销、修改资料、修改密码功能
# 编写登录的 API 接口
在前后的分离的项目中,登录鉴权一般都使用的是 JWT 的技术,所以后端的登录 API 接口中,用户登录成功后要签发一个 token,返回给前端, 前端每次发送 request 请求都会通过请求头(header)或者 url 地址来携带 token。
1️⃣ 添加 JWT 的依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
2️⃣ 编写 JWT 操作的工具类:utils/JwtUtil.java
package com.maomao.auth.authapi.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8));
}
public String generateToken(Long userId, String username) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
return Jwts.builder()
.claims(claims) // 替换 setClaims()
.subject(username) // 替换 setSubject()
.issuedAt(new Date()) // 替换 setIssuedAt()
.expiration(new Date(System.currentTimeMillis() + expiration)) // 替换 setExpiration()
.signWith(getSigningKey()) // 移除了 SignatureAlgorithm 参数
.compact();
}
public Claims getClaimsFromToken(String token) {
try {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token) // 替换 parseClaimsJws()
.getPayload();
} catch (Exception e) {
return null;
}
}
public Long getUserIdFromToken(String token) {
Claims claims = getClaimsFromToken(token);
if (claims != null) {
return claims.get("userId", Long.class);
}
return null;
}
public String getUsernameFromToken(String token) {
Claims claims = getClaimsFromToken(token);
if (claims != null) {
return claims.getSubject();
}
return null;
}
public boolean isTokenExpired(String token) {
Claims claims = getClaimsFromToken(token);
if (claims == null) {
return true;
}
return claims.getExpiration().before(new Date());
}
public boolean validateToken(String token) {
return !isTokenExpired(token);
}
}
3️⃣ 在 application.yml 中配置 jwt 的密钥和过期时间
# JWT配置
jwt:
secret: auth-system-secret-key-2024-1234
expiration: 86400000 # 24小时(毫秒)
4️⃣ 在 UserService 中添加登录验证的方法:login
SysUserService.java
/**
* 用户登录
* @param user
* @return
*/
public SysUser login(SysUser user);
SysUserServiceImpl.java
需要在 SysUser 中添加一个 token 属性。
@Override
public SysUser login(SysUser user) {
SysUser sysUser = baseMapper.selectOne(new QueryWrapper<SysUser>().eq("username", user.getUsername()));
if (sysUser == null){
throw new CustomException("用户不存在");
}
String md5Password = DigestUtils.md5DigestAsHex(user.getPassword().getBytes());
if (!sysUser.getPassword().equals(md5Password)){
throw new CustomException("密码不正确");
}
if (sysUser.getStatus() == 0) {
throw new CustomException("用户已被禁用");
}
//查询用户拥有的角色编号
List<SysUserRole> userRoleList = sysUserRoleMapper.selectList(new QueryWrapper<SysUserRole>().eq("user_id", sysUser.getId()));
List<Long> roleIds = userRoleList.stream().map(sysUserRole -> sysUserRole.getRoleId()).collect(Collectors.toList());
sysUser.setRoleIds(ArrayUtil.toArray(roleIds, Long.class));
String token = jwtUtil.generateToken(sysUser.getId(), sysUser.getUsername());
sysUser.setToken(token);
return sysUser;
}
5️⃣ 新建一个控制器,LoginController.java, 编写登录的 api 接口
@RestController
public class LoginController {
@Autowired
private SysUserService sysUserService;
@PostMapping("/login")
public Result login(@RequestBody SysUser user) {
SysUser sysUser = sysUserService.login(user);
return Result.success(sysUser);
}
}
# pinia 简介
Pinia 是 Vue 的专属状态管理库,它允许你跨组件或页面共享状态。
pinia 官网:https://pinia.vuejs.org/zh/introduction.html (opens new window)
1️⃣ 安装 pinia
npm install pinia
2️⃣ 在 main.js 中注册使用 pinia
import { createPinia } from "pinia";
app.use(pinia);
3️⃣ 定义一个 用户的 store
Store (如 Pinia) 是一个保存状态和业务逻辑的实体,它并不与你的组件树绑定。换句话说,它承载着全局状态。它有点像一个永远存在的组件,每个组件都可以读取和写入它。它有三个概念,state、getter 和 action,我们可以假设这些概念相当于组件中的 data、 computed 和 methods。
store/user.js
import { defineStore } from "pinia";
import http from "@/utils/request.js";
export const useUserStore = defineStore("user", {
state: () => ({
userInfo: JSON.parse(localStorage.getItem("userInfo")) || null,
token: localStorage.getItem("token") || null,
}),
actions: {
setUserInfo(userInfo) {
localStorage.setItem("userInfo", JSON.stringify(userInfo));
this.userInfo = userInfo;
},
setToken(token) {
localStorage.setItem("token", token);
this.token = token;
},
async login(data) {
const res = await http.post("/login", {
username: data.username,
password: data.password,
});
if (res.code == 200) {
this.setUserInfo(res.data);
this.setToken(res.data.token);
return true;
}
return false;
},
logout() {
this.setUserInfo(null);
this.setToken("");
localStorage.removeItem("token");
},
},
});
# 实现用户登录和注销功能
1️⃣ 编写登录的视图页面 Login.vue
<template>
<div class="login-container">
<div class="login-box">
<h2>通用后台管理系统</h2>
<el-form
ref="loginFormRef"
:model="loginForm"
:rules="rules"
class="login-form"
>
<el-form-item prop="username">
<el-input
v-model="loginForm.username"
placeholder="用户名"
size="large"
prefix-icon="User"
/>
</el-form-item>
<el-form-item prop="password">
<el-input
v-model="loginForm.password"
type="password"
placeholder="密码"
size="large"
prefix-icon="Lock"
@keyup.enter="handleLogin"
/>
</el-form-item>
<el-form-item>
<el-button
type="primary"
size="large"
:loading="loading"
@click="handleLogin"
style="width: 100%"
>
登录
</el-button>
</el-form-item>
</el-form>
<div style="text-align: right; color: #343434; font-size: 13px;">
Author By @程序员毛毛@程序员小孟
</div>
</div>
</div>
</template>
<script setup>
import { ref, reactive } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import { login } from "@/api/login.js";
import { useUserStore } from "@/store/user.js";
const router = useRouter();
const userStore = useUserStore();
const loginFormRef = ref(null);
const loading = ref(false);
const loginForm = reactive({
username: "admin",
password: "123456",
});
const rules = {
username: [{ required: true, message: "请输入用户名", trigger: "blur" }],
password: [{ required: true, message: "请输入密码", trigger: "blur" }],
};
const handleLogin = async () => {
if (!loginFormRef.value) return;
await loginFormRef.value.validate(async (valid) => {
if (valid) {
loading.value = true;
const res = await userStore.login(loginForm);
if (res) {
ElMessage.success("登录成功");
router.push("/users");
} else {
ElMessage.error("账号或者密码错误");
}
loading.value = false;
}
});
};
</script>
<style scoped>
.login-container {
width: 100%;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
//background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
background: url("../assets/img/login_bg.jpg") no-repeat center center;
background-size: cover;
}
.login-box {
width: 400px;
padding: 40px;
background: white;
border-radius: 10px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.1);
}
.login-box h2 {
text-align: center;
margin-bottom: 30px;
color: #333;
}
.login-form {
margin-top: 20px;
}
</style>
2️⃣ 调用 store 中暴露的登录方法,进行登录校验
const handleLogin = async () => {
if (!loginFormRef.value) return;
await loginFormRef.value.validate(async (valid) => {
if (valid) {
loading.value = true;
const res = await userStore.login(loginForm);
if (res) {
ElMessage.success("登录成功");
router.push("/users");
} else {
ElMessage.error("账号或者密码错误");
}
loading.value = false;
}
});
};
3️⃣ 在主页面显示登录用户的信息
<el-dropdown style="outline: none; cursor: pointer;">
<span style="display: flex;align-items: center;outline: none;">
<img src="@/assets/img/avatar.jpg" alt="" style="width: 30px;height: 30px;border-radius: 50%; margin-right: 5px;">
欢迎 {{userStore.userInfo?.nickname}}
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="router.push('/profile')">个人中心</el-dropdown-item>
<el-dropdown-item @click="router.push('/changePwd')">修改密码</el-dropdown-item>
<el-dropdown-item divided @click="handleLogout">注销登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<script setup>
import { useUserStore } from "@/store/user";
const userStore = useUserStore();
</script>
4️⃣ 调用 store 的 logout 方法实现注销功能
import { useUserStore } from "@/store/user";
const userStore = useUserStore();
const handleLogout = () => {
ElMessageBox.confirm("确定要退出登录吗?", "提示", {
type: "warning",
}).then(() => {
userStore.logout();
router.push("/login");
});
};
# 实现修改个人资料功能
1️⃣ 编写个人资料的页面 Profile.vue, 从 pinia 中取出用户资料并显示

<template>
<div style="padding: 16px;">
<el-card>
<template #header>
<div
style="display:flex;align-items:center;justify-content:space-between;"
>
<span>个人中心</span>
</div>
</template>
<el-form :model="form" ref="formRef" label-width="88px">
<el-form-item label="用户名" prop="username">
<el-input v-model="form.username" />
</el-form-item>
<el-form-item label="昵称" prop="nickname">
<el-input v-model="form.nickname" />
</el-form-item>
<el-form-item label="邮箱" prop="email">
<el-input v-model="form.email" />
</el-form-item>
<el-form-item label="手机号" prop="phone">
<el-input v-model="form.phone" />
</el-form-item>
<el-form-item label="角色" prop="roleIds">
<el-tag
style="margin-right: 10px;"
type="warning"
v-for="item in roleList"
>{{ item.roleName }}</el-tag
>
</el-form-item>
<el-form-item label="状态" prop="status">
<el-tag type="primary" v-if="form.status == 1">启用</el-tag>
<el-tag type="danger" v-else>禁用</el-tag>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">确认修改</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { useUserStore } from "@/store/user.js";
import { listRoles } from "@/api/role.js";
const userStore = useUserStore();
const form = ref({});
const roleList = ref([]);
onMounted(() => {
Object.assign(form.value, userStore.userInfo);
listRoles({ pageSize: 100 }).then((res) => {
console.log(userStore.userInfo);
roleList.value = res.data.list.filter((item) =>
userStore.userInfo.roleIds.includes(item.id),
);
});
});
</script>
2️⃣ 调用 store 中的 updateUser 方法修改用户资料
const onSubmit = () => {
userStore.updateUser(form.value).then((res) => {
if (res) ElMessage.success("修改成功");
else ElMessage.error("修改失败");
});
};
store/user.js
async updateUser(data) {
delete data.roleIds
let res = await updateUser(data.id, data)
if (res.code == 200){
let u = await getUser(data.id);
this.setUserInfo(u.data)
return true
}
return false;
}
# 实现修改密码功能
1️⃣ 编写视图页面 ChangePwd.vue

<template>
<div style="padding: 16px;">
<el-card>
<template #header>
<div
style="display:flex;align-items:center;justify-content:space-between;"
>
<span>修改密码</span>
</div>
</template>
<el-form :model="form" ref="formRef" label-width="88px">
<el-form-item label="原密码" prop="password">
<el-input type="password" v-model="form.password" />
</el-form-item>
<el-form-item label="新密码" prop="newpassword">
<el-input type="password" v-model="form.newpassword" />
</el-form-item>
<el-form-item label="确认密码" prop="repassword">
<el-input type="password" v-model="form.repassword" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">确认修改</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</template>
<script setup>
import { ref } from "vue";
const form = ref({});
</script>
2️⃣ 编写后端修改密码 api 接口
@PostMapping("/changePwd")
public Result changePwd(HttpServletRequest request, @RequestBody Map<String, String> map) {
//查询登录用户的信息
String token = request.getHeader("token");
Long userId = jwtUtil.getUserIdFromToken(token);
SysUser user = sysUserService.getById(userId);
if (!user.getPassword().equals(DigestUtil.md5Hex(map.get("password").getBytes()))){
throw new CustomException("原密码错误");
}
sysUserService.updatePassword(userId,DigestUtil.md5Hex(map.get("newpassword").getBytes()));
return Result.success();
}
3️⃣ 调用后端 api 接口,实现密码修改
const onSubmit = () => {
formRef.value.validate((valid) => {
if (valid) {
changePassword(form.value).then((res) => {
if (res.code == 200) {
ElMessage.success("密码修改成功");
} else {
ElMessage.error(res.message);
}
});
}
});
};
# 实现登录后的动态路由和菜单的加载
# 思路分析
登录成功后,根据登录用户的角色查询对应的权限菜单, 根据所拥有的菜单动态显示侧边栏菜单并且生成动态路由。
# 编写后端的 API 接口查询用户所授权的菜单
controller/PermissionController.java
@GetMapping("/getRouters")
public Result getRouters(HttpServletRequest request) {
String token = request.getHeader("token");
Long userId = jwtUtil.getUserIdFromToken(token);
List<SysPermission> permissions = sysPermissionService.selectPermissionByUserId(userId);
return Result.success(permissions);
}
service.impl/PermissionServiceImpl.java
@Override
public List<SysPermission> selectPermissionByUserId(Long userId) {
QueryWrapper<SysPermission> queryWrapper = new QueryWrapper<>();
queryWrapper.in("id",baseMapper.selectPermissionIdsByUserId(userId));
List<SysPermission> list = baseMapper.selectList(queryWrapper);
return this.buildMenuTree(list);
}
PermissionMapper.java
@Mapper
public interface SysPermissionMapper extends BaseMapper<SysPermission> {
@Select("SELECT permission_id from sys_role_permission where role_id in (SELECT role_id from sys_user_role where user_id = #{userId})")
public List<Long> selectPermissionIdsByUserId(@Param("userId") Long userId);
}
# 创建 permissionStore,用于存储用户的路由信息
store/permission.js
import { defineStore } from "pinia";
import { getRouters } from "@/api/permission.js";
import { constantRoutes } from "@/router/index.js";
export const usePermissionStore = defineStore("permission", {
state: () => ({
routers: [],
}),
actions: {
setRouters(routes) {
this.routers = routes;
},
//生成路由
generateRoutes() {
return new Promise(async (resolve) => {
const accessRouters = await getRouters();
const array = new Array();
//转换格式,将返回的权限菜单转成前端路由所需要的格式
accessRouters.data.forEach((item) => {
let tem = {
path: item.path,
name: item.name,
meta: {
title: item.permissionName, //标题文字
icon: item.icon, //图标
},
component: () => import("../layout/MainLayout.vue"),
children: item.children.map((child) => {
return {
path: child.path,
name: child.name,
meta: {
title: child.permissionName,
icon: child.icon,
},
component: () => import("../views/" + child.component + ".vue"),
};
}),
};
array.push(tem);
});
const routes = [...constantRoutes, ...array];
this.setRouters(routes);
resolve(array);
});
},
},
});
# 在路由导航守卫中,调用 store 中的方法添加动态路由
router/index.js
import { createRouter, createWebHistory } from "vue-router";
import Login from "../views/Login.vue";
import { usePermissionStore } from "@/store/permission.js";
//静态路由列表
export const constantRoutes = [
{
path: "/login",
name: "login",
hidden: true,
component: Login,
},
{
path: "/",
name: "home",
redirect: "/dashborad",
component: () => import("../layout/MainLayout.vue"),
children: [
{
path: "dashborad",
name: "dashboard",
meta: {
title: "首页",
icon: "HomeFilled",
},
component: () => import("../views/Dashboard.vue"),
},
{
path: "profile",
name: "profile",
hidden: true,
meta: {
title: "个人中心",
},
component: () => import("../views/Profile.vue"),
},
{
path: "changePwd",
name: "changePwd",
hidden: true,
meta: {
title: "修改密码",
},
component: () => import("../views/ChangePwd.vue"),
},
],
},
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: constantRoutes,
});
router.beforeEach(async (to, from, next) => {
if (to.path != "/login") {
const permissionStore = usePermissionStore();
if (permissionStore.routers.length == 0) {
console.log("加载动态路由.....");
//加载动态路由
const routes = await permissionStore.generateRoutes();
routes.forEach((route) => {
router.addRoute(route);
});
if (!to.redirectedFrom) {
next({ ...to, replace: true });
} else {
next();
}
} else {
next();
}
} else {
next();
}
});
export default router;
# 编写侧边栏组件 SidebarMenu, 根据路由动态渲染侧边栏
layout/SidebarMenu.vue
<template>
<el-menu
:default-active="activeMenu"
:collapse="isCollapse"
router
class="sidebar-menu"
active-text-color="#ffd04b"
background-color="#042E55"
text-color="#ffffff"
>
<sidebar-item
v-for="route in menuRoutes"
:key="route.path"
:item="route"
:base-path="route.path"
/>
</el-menu>
</template>
<script setup>
import { defineProps, ref, computed, onMounted } from "vue";
import { useRoute, useRouter } from "vue-router";
import { constantRoutes } from "@/router/index.js";
import SidebarItem from "@/layout/SidebarItem.vue";
import { usePermissionStore } from "@/store/permission.js";
const menuRoutes = ref([]);
const route = useRoute();
const router = useRouter();
const permissionStore = usePermissionStore();
const activeMenu = computed(() => {
const { meta, path } = route;
return meta.activeMenu || path;
});
//定义属性
defineProps({
isCollapse: {
type: Boolean,
default: false,
},
});
onMounted(() => {
menuRoutes.value = permissionStore.routers;
});
</script>
<style scoped></style>
layout/SidebarItem.vue
<!-- components/Layout/SidebarItem.vue -->
<template>
<div v-if="!item.hidden">
<template
v-if="
hasOneShowingChild(item.children, item) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren)
"
>
<el-menu-item :index="resolvePath(onlyOneChild.path)">
<el-icon v-if="onlyOneChild.meta?.icon">
<component :is="onlyOneChild.meta.icon" />
</el-icon>
<template #title>
{{ onlyOneChild.meta?.title }}
</template>
</el-menu-item>
</template>
<el-sub-menu v-else :index="resolvePath(item.path)">
<template #title>
<el-icon v-if="item.meta?.icon">
<component :is="item.meta.icon" />
</el-icon>
<span>{{ item.meta?.title }}</span>
</template>
<sidebar-item
v-for="child in item.children"
:key="child.path"
:item="child"
:base-path="resolvePath(child.path)"
/>
</el-sub-menu>
</div>
</template>
<script setup>
import { computed, defineProps } from "vue";
import { resolve } from "path-browserify";
const props = defineProps({
item: {
type: Object,
required: true,
},
basePath: {
type: String,
required: true,
},
});
const onlyOneChild = computed(() => {
if (!props.item.children) {
props.item.children = [];
}
const children = props.item.children.filter((item) => !item.hidden) || [];
if (children.length === 1 && !children[0].children) {
return {
...children[0],
path: resolve(props.basePath, children[0].path),
noShowingChildren: true,
};
}
return {
...props.item,
path: props.basePath,
noShowingChildren: !children || children.length === 0,
};
});
const hasOneShowingChild = (children = [], parent) => {
if (!children) {
children = [];
}
const showingChildren = children.filter((item) => !item.hidden);
if (showingChildren.length === 1) {
return true;
}
if (showingChildren.length === 0) {
return true;
}
return false;
};
const resolvePath = (routePath) => {
return resolve(props.basePath, routePath);
};
</script>
# 实现按钮的控制、面包屑控件
# 根据路由实现动态的面包屑组件


1️⃣ 获取到当前匹配的路由信息: MainLayOut.vue
import { useRoute } from "vue-router";
const route = useRoute();
const breadcrumbList = ref([]);
const updateBreadcrumb = () => {
breadcrumbList.value = route.matched.filter((item) => item.meta?.title);
};
onMounted(() => {
updateBreadcrumb();
});
watch(
() => route.path,
(newPath, oldPath) => {
updateBreadcrumb();
},
{ immediate: true },
);
2️⃣ 动态渲染面包屑组件
<el-breadcrumb separator="/" style="flex: 1;">
<el-breadcrumb-item :to="{ path: '/home' }">首页</el-breadcrumb-item>
<el-breadcrumb-item
v-for="item in breadcrumbList"
:key="item.path"
:to="item.path">
{{ item.meta.title }}
</el-breadcrumb-item>
</el-breadcrumb>
# 实现功能按钮的控制
权限分配可以具体到模块的增删改查的功能。
1️⃣ 修改用户登录的 api 接口,登录成功返回用户拥有的权限信息
修改 UserServiceImpl.java 的 login 方法
@Override
public SysUser login(SysUser user) {
SysUser sysUser = baseMapper.selectOne(new QueryWrapper<SysUser>().eq("username", user.getUsername()));
if (sysUser == null){
throw new CustomException("用户不存在");
}
String md5Password = DigestUtils.md5DigestAsHex(user.getPassword().getBytes());
if (!sysUser.getPassword().equals(md5Password)){
throw new CustomException("密码不正确");
}
if (sysUser.getStatus() == 0) {
throw new RuntimeException("用户已被禁用");
}
//查询用户角色
List<SysUserRole> userRoleList = sysUserRoleMapper.selectList(new QueryWrapper<SysUserRole>().eq("user_id", sysUser.getId()));
List<Long> roleIds = userRoleList.stream().map(sysUserRole -> sysUserRole.getRoleId()).collect(Collectors.toList());
sysUser.setRoleIds(ArrayUtil.toArray(roleIds, Long.class));
//查询用户拥有的权限字符串
List<SysRolePermission> rolePermissionList = sysRolePermissionMapper.selectList(new QueryWrapper<SysRolePermission>().in("role_id", roleIds));
List<SysPermission> permissionList = sysPermissionMapper.selectList(new QueryWrapper<SysPermission>().in("id", rolePermissionList.stream().map(sysRolePermission -> sysRolePermission.getPermissionId()).collect(Collectors.toList())));
sysUser.setPermissions(permissionList.stream().map(sysPermission -> sysPermission.getPermissionCode()).collect(Collectors.toList()));
String token = jwtUtil.generateToken(sysUser.getId(), sysUser.getUsername());
sysUser.setToken(token);
return sysUser;
}
2️⃣ 修改 store/user.js, 在 state 中增加 permissions 用于存储权限信息
state: () => ({
userInfo: JSON.parse(localStorage.getItem("userInfo")) || null,
token: localStorage.getItem("token") || null,
permissions: localStorage.getItem("permissions").split("_") || [],
});
3️⃣ 自定义 Vue 指令并进行全局注册
directive/hasPermi.js
import { useUserStore } from "@/store/user.js";
export default {
mounted(el, binding, vnode) {
const userStore = useUserStore();
const permissions = userStore.permissions;
const { value } = binding;
if (value && value instanceof Array && value.length > 0) {
const permissionFlag = value;
const hasPermissions = permissions.some((permission) => {
return permissionFlag.includes(permission);
});
if (!hasPermissions) {
el.parentNode && el.parentNode.removeChild(el);
}
}
},
};
main.js
import hasPermi from "@/directive/hasPermi.js";
app.directive("hasPermi", hasPermi);
4️⃣ 在视图页面中使用自定义指令
<el-button
type="primary"
v-has-permi="['user:add']"
:icon="Plus"
@click="handleAdd"
>新增</el-button>
<el-button
type="primary"
v-has-permi="['user:edit']"
:icon="Edit"
:disabled="selectedIds.length !== 1"
@click="handleEdit"
>编辑</el-button>
<el-button
type="danger"
v-has-permi="['user:delete']"
:icon="Delete"
:disabled="!selectedIds.length"
@click="handleDelete"
>批量删除</el-button>
<el-button
type="success"
v-has-permi="['user:export']"
:icon="Download"
@click="handleExport"
>导出</el-button>
# 实现前后端的权限拦截控制
# 后端权限拦截校验
添加 JWT 的拦截器,判断前端是否传递了 token,如果没有 token 则提示无权访问。
common/JwtInterceptor.java
package com.maomao.auth.authapi.common;
import com.maomao.auth.authapi.utils.JwtUtil;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
@Component
public class JWTInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 跨域预检请求直接放行
if ("OPTIONS".equals(request.getMethod())) {
return true;
}
// 获取token
String token = request.getHeader("token");
if (token == null){
token = request.getParameter("token");
}
if (token == null || token.isEmpty()) {
throw new CustomException(401, "您无权操作!");
}
// 验证token
if (!jwtUtil.validateToken(token)) {
throw new CustomException(401, "您无权操作!");
}
return true;
}
}
config/JWTConfig.java 注册拦截器并设置白名单
package com.maomao.auth.authapi.config;
import com.maomao.auth.authapi.common.JWTInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class JWTConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login","/register","/upload","/download/**","/api/**"
);
}
@Bean
public JWTInterceptor jwtInterceptor(){
return new JWTInterceptor();
}
}
# 前端权限拦截
前端配置权限拦截主要在路由导航守卫中来进行
router/index.js
//定义白名单
const whiteList = ["/login"];
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore();
if (!userStore.token && !whiteList.includes(to.path)) {
next("/login");
}
if (to.path != "/login") {
const permissionStore = usePermissionStore();
if (permissionStore.routers.length == 0) {
console.log("加载动态路由.....");
//加载动态路由
const routes = await permissionStore.generateRoutes();
routes.forEach((route) => {
router.addRoute(route);
});
if (!to.redirectedFrom) {
next({ ...to, replace: true });
} else {
next();
}
} else {
next();
}
} else {
next();
}
});