前置说明
这个项目涵盖内容如下:
1、前端使用Vue + Element-plus
2、后端使用:SpringBoot3 + Spirng Security + JWT + Redis + Mysql + MyBatis+MyBatis分页 + EasyExcel文件上传下载
本项目的所有构建过程详细记录如下,其中项目中初始用户 admin 密码是:123456
一、创建Vue前端项目架子
使用vite脚手架工具创建Vue项目工程:
第一步:创建项目
命令:npm create vite@latest
第二步:安装前端依赖
把生成的front项目放目录:code
使用idea打开code,使用Terminal
1、进入front目录:cd front
2、安装前端依赖:npm install
安装完成后,前端项目中会多出一个目录:node_modules
此时可以看项目结构如下图所示,并且此时我们点击:package.json中的"dev": "vite"
就可以运行项目了
第三步:修改前端的端口号
修改前端front项目下的vite.config.js,修改完后的内容如下:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
// https://vite.dev/config/
export default defineConfig({
plugins: [vue()],
server: {
host: '0.0.0.0', // 监听所有地址
port: 8081, // 端口号
open: false // 不自动打开浏览器
}
})
二、创建springboot后端项目架子
在code下新增子模块,Springboot模块,backend
先暂时只添加web依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
三、前端开发
安装element-plus
我们前端使用组件库element-plus
安装element-plus命令:npm install element-plus --save
在项目中使用element-plus
修改前端项目中src/main.js
import { createApp } from 'vue'
import './style.css'
/* 导入element-plus相关组件及css */
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
/*
使用use方法使用element-plus
注意:mount(挂载)是最后一步,所以下面的use一定是要在它前面的
*/
createApp(App).use(ElementPlus).mount('#app')
登录页开发
前端项目的src下新增一个文件夹:view,未来这个目录中放我们的页面。
在view目录下新增一个View Component
组件名称为:LoginView,使用Options API(选项式API)
LoginView.vue修改为如下:
<!-- 数据/行为 -->
<script>
export default {
name: "LoginView"
}
</script>
<!-- 页面结构 -->
<template>
<el-container>
<el-container>
<el-aside width="200px">Aside</el-aside>
<el-main>
Main
</el-main>
</el-container>
</el-container>
</template>
<!-- 样式 -->
<style scoped>
</style>
此时我们要使用这个组件,把这个组件中的内容挂载到index.html当中,需要修改main.js后变为如下:
import { createApp } from 'vue'
/* 默认的全局样式,如果对我们有影响可以注释 */
// import './style.css'
/* 导入element-plus相关组件及css */
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
/* App.vue是前端的脚手架帮我们生成的,我们不需要了 */
// import App from './App.vue'
/* 导入登录组件LoginView.vue */
import LoginView from "./view/LoginView.vue";
/*
使用use方法使用element-plus
注意:mount(挂载)是最后一步,所以下面的use一定是要在它前面的
此时createApp时使用的是LoginView这个我们自定义的组件
*/
createApp(LoginView).use(ElementPlus).mount('#app')
记得同步把前端项目中index.html页面中的title修改为:"vue + springboot 前后端分离",如果需要的话可以把默认显示的icon图标修改掉。
为了避免页面上的左边上边出现空白间隙,我们在index中添加一个样式处理
修改后的index.html如下:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<!-- 图标使用.png -->
<link rel="icon" type="image/png" href="../assets/favicon.png" />
<!-- <link rel="icon" type="" href="/vite.svg" />-->
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>vue + springboot 前后端分离</title>
<style>
body {
margin: 0;
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
登录表单添加相关的校验,操作的步骤:
第一步:在<el-form>
中定义:rules="xxx"
第二步:在需要验证的的<el-form-item>
中添加prop="xxxx",这里xxxx要保证与内部表单元素绑定的属性名相同
第三步:在data()中定义规则,也就是第一步中指定的规则xxx,要进行定义
最终LoginView.vue修改为如下:
<!-- 数据/行为 -->
<script>
export default {
name: "LoginView",
/* 定义页面中需要使用到的变量 */
data() {
return {
user:{},
/* 定义表单的验证规则对象 */
loginRules: {
// 登录账号字段校验
loginAct: [
{required: true, message: '请输入登录账号', trigger: 'blur'}
],
loginPwd: [
{required: true, message: '请输入登录密码', trigger: 'blur'}
]
}
}
},
}
</script>
<!-- 页面结构 -->
<template>
<el-container>
<el-container>
<!-- 左侧 -->
<el-aside width="200px">
<img src="../assets/login.png" alt="">
<p>
欢迎使用XXX系统
</p>
</el-aside>
<!-- 右侧 -->
<el-main>
<div class="login-title">欢迎登录</div>
<el-form ref="loginRefForm" :model="user" label-width="auto" :rules="loginRules">
<el-form-item label="账号" prop="loginAct">
<el-input v-model="user.loginAct"/>
</el-form-item>
<el-form-item label="密码" prop="loginPwd">
<el-input v-model="user.loginPwd" show-password/>
</el-form-item>
<el-form-item class="action-button">
<el-button type="primary">登录</el-button>
<el-button type="primary">注册</el-button>
</el-form-item>
<el-form-item>
<el-checkbox label="记住我" name="rememberMe" v-model="user.rememberMe"/>
</el-form-item>
</el-form>
</el-main>
</el-container>
</el-container>
</template>
<!-- 样式 -->
<style scoped>
/* element-plus中加样式直接使用 .标签名 */
.el-aside {
background-color: #aaaaaa;
width: 40%;
text-align: center;
/* .el-aside下的img元素样式 */
img {
margin-top: 100px;
height: 400px;
}
p {
color: #213547;
font-size: 28px;
}
}
.el-main {
background-color: #eeeeee;
/* 高度动态计算屏幕高 */
height: calc(100vh);
.login-title {
text-align: center;
font-size: 25px;
font-weight: bold;
margin-top: 100px;
margin-bottom: 20px;
}
.el-form {
width: 40%;
margin: auto;
.action-button {
display: flex;
justify-content: center;
align-items: center;
gap: 10px;
}
}
}
</style>
前端登录axios发送请求
前端安装axios依赖
axios可以用来做异步ajax的请求发送
安装命令:npm install axios --save
使用axios封装请求方法
在项目的src目录下新增一个http目录,其中新增一个js文件:httpRequest.js
// 导入axios
import axios from "axios";
// 定义后端接口的前缀
axios.defaults.baseURL = "http://localhost:8080/";
/* 我们定义的函数需要在其地方使用,所以需要导出 */
/**
* 发送get请求
* @param url
* @param params
* @returns {*}
*/
export function doGet(url, params) {
return axios({
method: 'get',
url: url,
params: params,
dataType: 'json'
})
}
/**
* 发送post请求
* @param url
* @param data
* @returns {*}
*/
export function doPost(url, data) {
return axios({
method: 'post',
url: url,
data: data, // post请求参数是data
dataType: 'json'
})
}
/**
* 发送put请求
* @param url
* @param data
* @returns {*}
*/
export function doPut(url,data) {
return axios({
method: 'put',
url: url,
data: data, // put请求参数是data
dataType: 'json'
})
}
/**
* 发送delete请求
* @param url
* @param params
* @returns {*}
*/
export function doDelete(url, params) {
return axios({
method: 'delete',
url: url,
params: params, // delete请求参数是params
dataType: 'json'
})
}
四、后端新增业务处理
数据库及表准备
数据库:vue_springboot
相关表数据参考后端项目:db_script
pom依赖
后端项目添加如下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter-test</artifactId>
<version>3.0.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
yml配置
server:
port: 8080
servlet:
context-path: /
# session不持久化
session:
persistent: false
# 数据源配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/vue_springboot?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
hikari:
# 最大连接,默认值是10
maximum-pool-size: 30
# 最小空闲连接,默认值是10
minimum-idle: 30
# 连接超时时间,默认值是30s
connection-timeout: 5000
# 空闲连接超时时间,默认值是60s,这里配为0,表示不回收
idle-timeout: 0
# 连接最大生命周期,默认值是30min,这里的配置最好不要超过8小时
max-lifetime: 18000000
data:
redis:
host: 127.0.0.1
port: 6379
database: 1
# mybatis配置
mybatis:
mapper-locations: classpath*:mapper/**.xml
# 驼峰命名
configuration:
map-underscore-to-camel-case: true
使用mybatis插件生成代码
略
完成后端项目的基础包结构
Spring Security使用
第一步:实体TUser实现UserDetails,新增角色和权限标识list,并实现UserDetails中的方法
TUser实现UserDetails后,新增两个list及7个实现方法,新增内容如下:
/* 角色 */
private List<String> roleList;
/* 权限标示符 */
private List<String> permissionList;
// UserDetails接口有7个方法
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return this.getAccountNoExpired() == 1;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return this.getAccountNoLocked() == 1;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return this.getCredentialsNoExpired() == 1;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return this.getAccountEnabled() == 1;
}
@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> list = new ArrayList<>();
// 角色
if(!ObjectUtils.isEmpty(this.getRoleList())) {
this.getRoleList().forEach(role -> list.add(new SimpleGrantedAuthority(role)));
}
// 权限
if(!ObjectUtils.isEmpty(this.getPermissionList())) {
this.getPermissionList().forEach(permission -> list.add(new SimpleGrantedAuthority(permission)));
}
return list;
}
@JsonIgnore
@Override
public String getPassword() {
return this.getLoginPwd();
}
@JsonIgnore
@Override
public String getUsername() {
return this.getLoginAct();
}
第二步:提供UserService接口继承UserDetailService
public interface UserService extends UserDetailsService {
}
@Service
@RequiredArgsConstructor
public class UserServiceImpl implements UserService {
private final TUsermapper tUsermapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
TUser tUser = tUsermapper.selectByLoginAct(username);
if(tUser == null) {
throw new UsernameNotFoundException("用户不存在");
}
return tUser;
}
}
第三步:后端前置准备(相关的配置、工具类)
Redis序列化配置
添加这个配置的目的防止我们存入Redis中的数据出现乱码的情况
新增一个类RedisSerializer,让宽实现CommandLineRunner,这样的话它就会在SpringBoot项目启动完后执行这个类中的run()方法
此时我们新增一个包:config,RedisSerializer类放在这个包下
@Component
@Slf4j
public class RedisSerializer implements CommandLineRunner {
@Resource
private RedisTemplate<String,Object> redisTemplate;
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
static {
try {
// 设置对象里的字段的可见性为字段级别
OBJECT_MAPPER.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
// 设置多态行为
OBJECT_MAPPER.activateDefaultTyping(OBJECT_MAPPER.getPolymorphicTypeValidator(),
ObjectMapper.DefaultTyping.EVERYTHING);
} catch (Exception e) {
log.error("初始化ObjectMapper失败...");
throw new RuntimeException(e);
}
}
@Override
public void run(String... args) throws Exception {
log.info("Redis序列化开始...");
// key序列化
redisTemplate.setKeySerializer(new StringRedisSerializer());
// 创建jackson序列化器
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(OBJECT_MAPPER,
Object.class);
// 值序列化
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// hashKey序列化
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
// hashValue序列化
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
log.info("Redis序列化完成...");
}
}
封装一个处理Redis的公共组件
因为是公共组件所以我们把这个类定义为:RedisManager,并把它放在manager包中
@Component
public class RedisManager {
@Resource
private RedisTemplate<String,Object> redisTemplate;
public void setValue(String key,Object value) {
redisTemplate.opsForValue().set(key, value);
}
public Object getValue(String key) {
return redisTemplate.opsForValue().get(key);
}
public Boolean delete (String key) {
return redisTemplate.delete(key);
}
public Boolean expire(String key, long timeout, TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
// 获取一个唯一值,编号
public String getOnlyNumber(String key) {
LocalDateTime localDateTime = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
String dateTimeStr = localDateTime.format(formatter);
Long incrValue = redisTemplate.opsForValue().increment(key);
return dateTimeStr + incrValue;
}
}
定义一个公共常量类
常量类用来配置操作过程中需要用到的常量信息,这样可以避免在业务代码中产生过多硬编码。未来只要修改这个常量信息的值就可以对项目中所有引用到的地方统一调整。
常量类我们定义一个包:consant,在其中新增类:Constants
/**
* 常量类
*/
public class Constants {
/**
* JWT 密钥
*/
public static final String JWT_SECRET = "w3osER)wTxc0>pTr03ryP";
/**
登录用户在redis中的key,一般命名为:“项目名:功能模块:xxx功能”
*/
public static final String LOGIN_USER_KEY = "vs:user:login:";
/**
* 前端登录选择记住我的 jwt token过期时间:7天
*/
public static final long REMEMBER_ME_EXPIRE_TIME = 7 * 24 * 60 * 60L;
/**
* jwt token的默认过期时间:1小时
*/
public static final long DEFAULT_EXPIRE_TIME = 60 * 60L;
/**
* 记住我的参数名
*/
public static final String REMEMBER_ME = "rememberMe";
}
统一返回R及其枚举类
返回给前端的信息,我们把这个放到domain.resp包中
枚举类:ReturnCode
public enum ReturnCode {
SUCCESS(200, "成功"),
FAIL(400, "失败"),
INTERNAL_SERVER_ERROR(500, "服务器内部错误");
private Integer code;
private String msg;
ReturnCode(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
R类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class R {
private Integer code;
private String msg;
private Object data;
public static R ok(Object data) {
return new R(ReturnCode.SUCCESS.getCode(), ReturnCode.SUCCESS.getMsg(), data);
}
public static R ok(String msg) {
return new R(ReturnCode.SUCCESS.getCode(), msg, null);
}
public static R ok(String msg, Object data) {
return new R(ReturnCode.SUCCESS.getCode(), msg, data);
}
public static R ok(ReturnCode returnCode) {
return new R(returnCode.getCode(), returnCode.getMsg(), null);
}
public static R ok(ReturnCode returnCode, Object data) {
return new R(returnCode.getCode(), returnCode.getMsg(), data);
}
public static R fail(Object data) {
return new R(ReturnCode.FAIL.getCode(), ReturnCode.FAIL.getMsg(), data);
}
public static R fail(String msg) {
return new R(ReturnCode.FAIL.getCode(), msg, null);
}
public static R fail(Integer code, String msg) {
return new R(code, msg, null);
}
public static R fail(String msg, Object data) {
return new R(ReturnCode.FAIL.getCode(), msg, data);
}
public static R fail(ReturnCode returnCode) {
return new R(returnCode.getCode(), returnCode.getMsg(),null);
}
public static R fail(ReturnCode returnCode, Object data) {
return new R(returnCode.getCode(), returnCode.getMsg(), data);
}
public static R fail(ReturnCode returnCode, String msg) {
return new R(returnCode.getCode(), msg, null);
}
}
工具类 - JSONUtils
工具类我