更多文章:https://mp.weixin.qq.com/mp/appmsgalbum?__biz=Mzg2NDY3NjY5NA==&action=getalbum&album_id=2053253027934863360#wechat_redirect
hello我是索奇,本套项目对应bilibili视频,大家可以结合视频看哈,有些基础的只看笔记也可,这篇笔记做了很久,也拓展了很多东西,对小白和积累经验的伙伴们都有帮助~
有用的话可以关注点赞收藏一波哈~
项目概述
1. 目标
通过学习本项目,深刻理解前后端分离的思想,具备独立搭建前后端分离项目的能力及功能扩展能力
2. 开发模式

3. 技术栈
| 前端技术 | 说明 | 
|---|---|
| Vue | 前端框架 | 
| Vuex | 全局状态管理框架 | 
| ElementUI | 前端UI框架 | 
| Axios | 前端HTTP框架 | 
| vue-element-admin | 项目脚手架 | 
| 后端技术 | 说明 | 
|---|---|
| SpringBoot | 容器+MVC框架 | 
| MyBatis | ORM框架 | 
| MyBatis-plus | MyBatis增强工具 | 
| Redis | 非关系型数据库 | 
Redis是处理缓存的非关系型数据库,在这里先有个印象
数据库
数据库xdb
1. 用户表
tips
在第一章节仅需用户表即可满足开发需要,但为了后期繁琐,重新回来创建表,建议还是创建吧

 CREATE TABLE `x_user` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `username` varchar(50) NOT NULL,
   `password` varchar(100) DEFAULT NULL,
   `email` varchar(50) DEFAULT NULL,
   `phone` varchar(20) DEFAULT NULL,
   `status` int(1) DEFAULT NULL,
   `avatar` varchar(200) DEFAULT NULL,
    `deleted` INT(1) DEFAULT 0,
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
 
 insert into `x_user` (`id`, `username`, `password`, `email`, `phone`, `status`, `avatar`, `deleted`) values('1','admin','123456','super@aliyun.com','18677778888','1','https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif','0');
 insert into `x_user` (`id`, `username`, `password`, `email`, `phone`, `status`, `avatar`, `deleted`) values('2','zhangsan','123456','zhangsan@gmail.com','13966667777','1','https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif','0');
 insert into `x_user` (`id`, `username`, `password`, `email`, `phone`, `status`, `avatar`, `deleted`) values('3','lisi','123456','lisi@gmail.com','13966667778','1','https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif','0');
 insert into `x_user` (`id`, `username`, `password`, `email`, `phone`, `status`, `avatar`, `deleted`) values('4','wangwu','123456','wangwu@gmail.com','13966667772','1','https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif','0');
 insert into `x_user` (`id`, `username`, `password`, `email`, `phone`, `status`, `avatar`, `deleted`) values('5','zhaoer','123456','zhaoer@gmail.com','13966667776','1','https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif','0');
 insert into `x_user` (`id`, `username`, `password`, `email`, `phone`, `status`, `avatar`, `deleted`) values('6','songliu','123456','songliu@gmail.com','13966667771','1','https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif','0'); 
2. 角色表
 CREATE TABLE `x_role` (
   `role_id` int(11) NOT NULL AUTO_INCREMENT,
   `role_name` varchar(50) DEFAULT NULL,
   `role_desc` varchar(100) DEFAULT NULL,
   PRIMARY KEY (`role_id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4;
 
 insert into `x_role` (`role_id`, `role_name`, `role_desc`) values('1','admin','超级管理员');
 insert into `x_role` (`role_id`, `role_name`, `role_desc`) values('2','hr','人事专员');
 insert into `x_role` (`role_id`, `role_name`, `role_desc`) values('3','normal','普通员工'); 
3. 菜单表
CREATE TABLE `x_menu` ( `menu_id` int(11) NOT NULL AUTO_INCREMENT, `component` varchar(100) DEFAULT NULL, `path` varchar(100) DEFAULT NULL, `redirect` varchar(100) DEFAULT NULL, `name` varchar(100) DEFAULT NULL, `title` varchar(100) DEFAULT NULL, `icon` varchar(100) DEFAULT NULL, `parent_id` int(11) DEFAULT NULL, `is_leaf` varchar(1) DEFAULT NULL, `hidden` tinyint(1) DEFAULT NULL, PRIMARY KEY (`menu_id`) ) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4;  insert into `x_menu`(`menu_id`,`component`,`path`,`redirect`,`name`,`title`,`icon`,`parent_id`,`is_leaf`,`hidden`) values (1,'Layout','/user','/user/list','userManage','用户管理','userManage',0,'N',0),(2,'user/user','list',NULL,'userList','用户列表','userList',1,'Y',0),(3,'user/role','role',NULL,'roleList','角色列表','role',1,'Y',0),(4,'user/permission','permission',NULL,'permissionList','权限列表','permission',1,'Y',0);
4. 用户角色映射表
 CREATE TABLE `x_user_role` (
   `id` int(11) NOT NULL AUTO_INCREMENT,
   `user_id` int(11) DEFAULT NULL,
   `role_id` int(11) DEFAULT NULL,
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;
 
 insert into `x_user_role` (`id`, `user_id`, `role_id`) values('1','1','1'); 
5. 角色菜单映射表
CREATE TABLE `x_role_menu` ( `id` int(11) NOT NULL AUTO_INCREMENT, `role_id` int(11) DEFAULT NULL, `menu_id` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;
前端笔记
1. node环境
官网:Node.js

注意,node可以比这个稍低,但不要更高
2. 下载vue-admin-template
-  
下载 & 指南
 
https://panjiachen.gitee.io/vue-element-admin-site/zh/guide/
3. 项目初始化
-  
解压至非中文无空格目录下(防止出现异常)
 -  
vscode、idea等工具打开项目
 
我这里用的是idea

拓展
控制台输入 ctrl + c 终止服务
这里需要用到node.js
不想要被配置bug弄头疼的看这里!
-  
安装的时候选择node.js版本16.12以下版本,防止后期各种错误,麻烦的更改配置(脚手架不支持新版本)
 -  
自己新建一个目录,别把nodejs的文件混杂了
 
安装完之后的目录是这样的

-  
设置淘宝镜像,加速
 
注意:npm install命令必须在package.json文件所在的同级目录下执行。这是因为npm install命令会根据package.json文件中的依赖信息来安装相应的包。如果package.json文件不在当前目录下,npm将无法找到它并执行安装。
首次设置镜像,便于加速
npm config set registry http://registry.npm.taobao.org/
npm install
-  
运行测试
部署看一下有没有问题
npm run dev
默认打开登录页面,登录之后成功进入主页面


 -  
配置修改
 
语法校验:lintOnSave
-  
表示当前为开发环境,此时ESLint将会在保存文件时进行代码检查。不开启的话可以改为false
 
默认打开浏览器:true
title:自己改喜欢的名字~
mock:用于模拟后端数据(后端建立之后可以删除了)
-  
以下是视频中配置的内容,我这里除了名字,其它都保持一致,方便和视频效果一样
 

idea中点击Navigator可以在这里找到相应的文件更改名字
-  
快捷键ctrl+shift+n
 -  
"symbol"(符号)指的是代码中的标识符,例如类名、方法名、变量名等。当您使用搜索功能时,可以选择搜索符号,从而查找与特定标识符相关的代码。我们可以选择symbols定位title等内容
快捷键ctrl+shift+alt+n
 


都改好之后重启服务测试
4. 登录页修改


都改好之后重启服务测试
4. 登录页修改
-  
中文描述
 -  
背景图
 -  
中文描述
 -  
背景图
 -  
所有的英文我们都可以根据英文查找进行选择性修改
图片放在assets里面,然后修改.login-container(完整项目已经打包放进去了,)
background-image: url('../../assets/bg.jpeg'); 1登录框调整
 

-  
登录用户名取消限制


 
5. 修改右上角用户下拉菜单
-  
src/layout/components/Navbar.vue
 -  
索奇感觉自带的这几个下拉菜单还不错,这里没有做任何修改
 
//下拉菜单 <el-dropdown-item>

6. 首页面包屑导航

7. 菜单初始化
-  
在src\views目录下创建sys模块目录、test模块目录(充数用,后续可用作权限分配测试)
 -  
在sys下创建user.vue、role.vue两个组件文件
在test下创建test1.vue、test2.vue、test3.vue
 -  
修改路由配置
{ path: '/sys', component: Layout, redirect: '/sys/user', name: 'sys', meta: { title: '系统管理', icon: 'sys' }, children: [ { path: 'user', name: 'user', component: () => import('@/views/sys/user'), meta: { title: '用户管理', icon: 'userManage' } }, { path: 'role', name: 'role', component: () => import('@/views/sys/role'), meta: { title: '角色管理', icon: 'roleManage' } } ] },  { path: '/test', component: Layout, redirect: '/test/test1', name: 'test', meta: { title: '功能测试', icon: 'form' }, children: [ { path: 'test1', name: 'test1', component: () => import('@/views/test/test1'), meta: { title: '测试点一', icon: 'form' } }, { path: 'test2', name: 'test2', component: () => import('@/views/test/test2'), meta: { title: '测试点二', icon: 'form' } }, { path: 'test3', name: 'test3', component: () => import('@/views/test/test3'), meta: { title: '测试点三', icon: 'form' } } ] }图标svg文件可上 iconfont-阿里巴巴矢量图标库 下载
 
8. 标签栏导航
-  
@/layout/components/AppMain.vue

<keep-alive :include="cachedViews"> <router-view :key="key" /> </keep-alive>cachedViews() { return this.$store.state.tagsView.cachedViews }
 -  
复制vue-element-admin项目中的文件到相应的目录中
-  
idea直接复制粘贴即可(VsCode打开文件夹粘贴)
 
@/layout/components/TagsView @/store/modules/tagsView.js @/store/modules/permission.js

 -  
 -  
修改文件@store/getters.js

visitedViews: state => state.tagsView.visitedViews, cachedViews: state => state.tagsView.cachedViews, permission_routes: state => state.permission.routes

 -  
修改文件@store/index.js

import Vue from 'vue' import Vuex from 'vuex' import getters from './getters' import app from './modules/app' import settings from './modules/settings' import user from './modules/user' import tagsView from './modules/tagsView'  Vue.use(Vuex)  const store = new Vuex.Store({ modules: { app, settings, user, tagsView }, getters })  export default store  -  
修改文件@\layout\index.vue
-  
导入、注册
 

 -  
 -  
修改文件@layout\components\index.js
在index.js下面导出组件,其他模块可以通过导入(import)TagsView组件来使用它。
export { default as TagsView } from './TagsView' -  
Affix 固钉 当在声明路由是 添加了 Affix 属性,则当前tag会被固定在 tags-view中(不可被删除)

 
报错处理
-  
deep报错将其改为
:: v-deep::v-deep { .el-scrollbar__bar { bottom: 0px; } .el-scrollbar__wrap { height: 49px; } } 
9. 登录接口梳理
-  
准备对接后端
 -  
在调试页面的 Network 标签中,可以查看浏览器发送和接收的所有网络请求。每个请求都是一个条目
 -  
下面的url可以更改(保证名字有意义)
 
| 接口 | url | method | 
|---|---|---|
| 登录 | /user/login | post | 
| 获取用户信息 | /user/info | get | 
| 注销 | /user/logout | post | 
-  
code:HTTP 状态码,表示请求的返回码。20000 表示成功,其他代码表示不同类型的错误。(可以改前端成功对应的码)
 -  
data:是请求返回的数据。
 -  
在预览中可以查看这些数据
 
{
	"code": 20000,
	"data": {
		"token": "admin-token"
	}
}
{
	"code": 20000,
	"data": {
		"roles": ["admin"],
		"introduction": "I am a super administrator",
		"avatar": "https://wpimg.wallstcn.com/f778738c-e4f8-4870-b634-56703b4acafe.gif",
		"name": "Super Admin"
	}
}
{
	"code": 20000,
	"data": "success"
}
 
拓展
json.cn转换为json格式,易于观察
-  
JSON在线解析及格式化验证 - JSON.cn
 -  
在线JSON校验格式化工具(Be JSON)
 
10. 对接后端接口
-  
修改 .env.development 中的base api,打包部署的话要修改.env.production
VUE_APP_BASE_API = 'http://localhost:9999'
 -  
修改vue.config.js,屏蔽mock请求

 -  
修改src\api\user.js,将url中的/vue-admin-template去掉

 -  
测试,预期会出现跨域错误
 -  
后端做跨域处理测试应该成功,并可在调试窗口观察接口调用情况
 
11. 用户管理
预览

-  
用户查询
-  
定义userManager.js
 -  
分页序号处理
<template slot-scope="scope"> {{(searchModel.pageNo-1) * searchModel.pageSize + scope.$index + 1}} </template> 123 
 -  
 -  
用户新增
-  
窗口关闭后数据还在
监听close,清理表单
 -  
表单数据验证
常规验证
自定义验证
 -  
窗口关闭后上次验证结果还在
 
 -  
 -  
用户修改
 -  
用户删除
 
后端
1. 项目初始化
-  
创建springboot项目:2.7.8
 -  
pom依赖
<!-- web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- mysql --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> </dependency> <!-- mybatis-plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.2</version> </dependency> <!-- freemarker --> <dependency> <groupId>org.freemarker</groupId> <artifactId>freemarker</artifactId> </dependency> <!-- lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
拓展依赖
mybatis-plus-generator:这个依赖项是 MyBatis-Plus 的代码生成器模块。使用这个模块,你可以根据数据库表自动生成 MyBatis-Plus 的实体类、Mapper 接口和 XML 映射文件。这个模块可以大大简化开发人员的工作,减少手动编写重复代码的工作量。
freeMarker:FreeMarker 是一个功能强大的模板引擎,它可以帮助我们将数据模型和模板文件结合起来,生成各种文本输出,例如 HTML 页面、电子邮件、配置文件等等。以下是 FreeMarker 的主要用途:
-  
Web 应用程序视图渲染:FreeMarker 可以作为 Web 应用程序中的模板引擎,帮助我们将数据模型和模板文件结合起来,生成 HTML 页面。
 -  
邮件模板:FreeMarker 可以帮助我们生成电子邮件的内容,例如邮件正文、邮件标题等等。
 -  
报表生成:FreeMarker 可以帮助我们生成各种类型的报表,例如 PDF、Excel、Word 等等。
 -  
代码生成:FreeMarker 可以帮助我们根据模板生成代码文件,例如 Java 类、XML 文件等等。
 -  
配置文件生成:FreeMarker 可以帮助我们生成各种类型的配置文件,例如 XML 配置文件、属性文件等等。
 
 -  
 -  
yml
-  
把配置文件改为
yml格式,并更改使用下面代码 
server: port: 9999 spring: datasource: username: root password: 123456 url: jdbc:mysql:///xdb redis: port: 6379 host: localhost logging: level: # 想要输出那个目录下面的debug日志,就配置哪一个,我设置的groupid是suoqi,这里和视频不一样,自己别出错了~ com.suoqi: debug -  
 
拓展
jdbc:mysql:///database 表示连接到本地默认端口(3306)的 MySQL 数据库中的 database 数据库。这种方式相当于使用主机名为 localhost 或 127.0.0.1 的地址连接 MySQL 数据库。
如果要连接到其他主机上的 MySQL 数据库,可以将主机名和端口号添加到 URL 中,例如:
jdbc:mysql://hostname:port/database
其中,hostname 是要连接的 MySQL 服务器的主机名或 IP 地址,port 是 MySQL 服务器的端口号,默认是 3306。
测试
-  
连接池使用默认的即可,不需要设置
spring.datasource.type。如果你使用的是较早的版本,需要手动设置。默认 HikariCP 在性能和稳定性方面都表现非常出色,是目前最快的连接池之一。但是,并不是说 HikariCP 在所有情况下都是最快的,因为连接池的性能还受到许多其他因素的影响,例如数据库类型、数据库驱动程序、JVM 版本、操作系统等等。
 
spring:
  datasource:
    url: jdbc:mysql:///xdb
    username: root
    password: 123456
    type: com.zaxxer.hikari.HikariDataSource
#    hikari:
#      maximum-pool-size: 20
#      connection-timeout: 5000 这些是可选值,不配置使用默认的也行,为了让大家了解还有很多参数可以配置 
拓展
driver-class-name 是 JDBC 驱动程序的完整类名,它用于告诉应用程序使用哪个 JDBC 驱动程序与数据库建立连接。在 Spring Boot 的数据源配置中,如果使用的是 Spring Boot 默认支持的数据库,如 MySQL、PostgreSQL 等,就不需要再显式配置 driver-class-name,因为 Spring Boot 会自动根据 JDBC URL 推断出驱动程序的类名。例如,如果 JDBC URL 是 jdbc:mysql://localhost:3306/mydb,那么 Spring Boot 就会自动使用 MySQL 驱动程序(即 com.mysql.jdbc.Driver)。但是,如果使用的是其他数据库,或者 JDBC URL 中没有包含数据库类型信息,那么就需要手动配置 driver-class-name。
配置的话,如下格式, Oracle 数据库,可以这样配置数据源:
spring:
  datasource:
    url: jdbc:oracle:thin:@localhost:1521:mydb
    username: myuser
    password: mypassword
    driver-class-name: oracle.jdbc.driver.OracleDriver 
 
在这个配置中,driver-class-name 显式指定了 Oracle JDBC 驱动程序的类名。
2. Mybatis-plus代码生成
官网
MyBatis-Plus
生成器代码(MybatisPlus官网最新代码和项目中不适配,如果报错可以删除新版本的代码,模板如下)
public class CodeGenerator {
    public static void main(String[] args) {
        String url = "jdbc:mysql:///xdb";
        String username = "root";
        String password = "123456";
        String author = "suoqi";
        // 定义的是src下面的java目录的绝对路径
        String outPath = "F:\\projects\\java\\Springboot+Vue管理系统01\\backend-admin-template-4.4.0\\src\\main\\java";
        String parentPackage = "com.suoqi";
        String moduleName = "sys";
//      // 复制类路径resources的绝对路径 后面加上mapper+模块名字
        String mapperLocation = "F:\\projects\\java\\Springboot+Vue管理系统01\\backend-admin-template-4.4.0\\src\\main\\resources\\mapper\\sys"; 
        FastAutoGenerator.create("url", "username", "password")
                .globalConfig(builder -> {
                    builder.author(author) // 设置作者
                            //.enableSwagger() // 开启 swagger 模式 (这里我们不需要生成Swagger相关的代码)
                            //.fileOverride() // 覆盖已生成文件
                            .outputDir(outPath); // 指定输出目录
                })
                .packageConfig(builder -> {
                    builder.parent(parentPackage) // 设置父包名
                            .moduleName(moduleName) // 设置父包模块名
                            .pathInfo(Collections.singletonMap(OutputFile.xml, mapperLocation)); // 设置mapperXml生成路径
                })
                .strategyConfig(builder -> {
                    builder.addInclude("t_simple") // 设置需要生成的表名
                            .addTablePrefix("t_", "c_"); // 设置过滤表前缀
                })
                .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
                .execute();
    }
} 
注意
mybatis-plus: # mapper-locations: classpath*:mapper/*.xml # global-config: # db-config: # id-type: auto # field-strategy: not_empty # table-prefix: mp_ # logic-delete-value: 1 # logic-not-delete-value: 0 # logic-delete-field: is_deleted
-  
mapper目录需要和配置一致才能够生效
 

-  
使用快速代码生成器-在Test下面创建一个类CodeGenerator
 
-  
package com.suoqi;  import com.baomidou.mybatisplus.generator.FastAutoGenerator; import com.baomidou.mybatisplus.generator.config.OutputFile; import com.baomidou.mybatisplus.generator.config.rules.DbColumnType; import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;  import java.sql.Types; import java.util.Collections;  /** * @author 即兴小索奇 * @version 1.0 * @date 2023/6/25 1:05 * @description */ public class CodeGenerator { public static void main(String[] args) { String url = "jdbc:mysql:///xdb"; String username = "root"; String password = "123456"; String author = "suoqi"; // 定义的是src下面的java目录的绝对路径 String outPath = "F:\\projects\\java\\Springboot+Vue管理系统01\\backend-admin-template-4.4.0\\src\\main\\java"; String parentPackage = "com.suoqi"; String moduleName = "sys"; // // 复制类路径resources的绝对路径 String mapperLocation = "F:\\projects\\java\\Springboot+Vue管理系统01\\backend-admin-template-4.4.0\\src\\main\\resources\\mapper\\sys"; // 带,分割代表多个表 String tables = "x_user,x_role,x_menu,x_user_role,x_role_menu"; FastAutoGenerator.create(url, username, password) .globalConfig(builder -> { builder.author(author) // 设置作者 //.enableSwagger() // 开启 swagger 模式 (这里我们不需要生成Swagger相关的代码) //.fileOverride() // 覆盖已生成文件 .outputDir(outPath); // 指定输出目录 }) .packageConfig(builder -> { builder.parent(parentPackage) // 设置父包名 .moduleName(moduleName) // 设置父包模块名 .pathInfo(Collections.singletonMap(OutputFile.xml, mapperLocation)); // 设置mapperXml生成路径 }) .strategyConfig(builder -> { builder.addInclude(tables) // 设置需要生成的表名 // 设置过滤表前缀 ,比如设置了过滤x_即 x_user = user .addTablePrefix("x_"); // 设置过滤表前缀 }) .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板 .execute(); } } 运行即可生成相应的文件


 -  
启动类加注解
//表示扫描com.suoqi包及其子包下所有以Mapper结尾的接口,并将其注册到MyBatis中 @MapperScan("com.suoqi.*.mapper") -  
测试
启动类
/** * RestController=Controller+ResponseBody * 这里的/user/all 是自定义的 */ @RestController @RequestMapping("/user") public class UserController { @Autowired private IUserService userService; @GetMapping("/all") public List<User> getAllUser() { List<User> list = userService.list(); return list; } }
 
拓展
@Resources 注解
@Resource(基于类的名称)注解与@Autowired注解类似,也是用来进行依赖注入的,@Resource是Java层面所提供的注解(不依赖Spring框架)
-  
它有一个name属性,@Resource如果name属性有值,那么Spring会直接根据所指定的name值去Spring容器找Bean对象,如果找到了则成功,如果没有找到则报错。
 -  
查找顺序如下:
按照名称查找:如果指定了
name属性,则按照该名称查找 Bean。按照类型查找:如果没有指定
name属性,则按照属性类型查找 Bean。如果找到多个同类型的 Bean,则会抛出异常。 
@Autowired(基于类型type)是Spring所提供的注解,它们依赖注入的底层实现逻辑也不同。
-  
按照类型byType的方式查找:如果属性的类型在 Spring 容器中只有一个 Bean,则自动装配该 Bean。
 -  
按照名称查找:如果属性的类型在 Spring 容器中有多个 Bean,则按照属性名称与 Bean 的名称进行匹配。如果匹配到了一个同名的 Bean,则自动装配该 Bean;否则抛出异常。
 
@Resource 注解是 Java EE 规范中定义的注解,不依赖于 Spring 框架,因此可以在任何 Java EE 应用程序中使用。但是,在 Spring 应用程序中,建议使用 @Autowired 或 @Inject 注解来进行 Bean 的自动装配。
3. 公共响应类

保证每一个接口返回的格式与前端格式保持一致,需要创建格式统一的类并附带三个参数:
-  
code
 -  
message(描述)
 -  
data(类型不确定)
 
src/main/java/com/common/vo/Result.java
-  
定义成功和失败的方法
 
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    private Integer code;
    private String message;
    private T data;
    public static<T>  Result<T> success(){
        return new Result<>(20000,"success",null);
    }
    public static<T>  Result<T> success(T data){
        return new Result<>(20000,"success",data);
    }
    public static<T>  Result<T> success(T data, String message){
        return new Result<>(20000,message,data);
    }
    public static<T>  Result<T> success(String message){
        return new Result<>(20000,message,null);
    }
    public static<T>  Result<T> fail(){
        return new Result<>(20001,"fail",null);
    }
    public static<T>  Result<T> fail(Integer code){
        return new Result<>(code,"fail",null);
    }
    public static<T>  Result<T> fail(Integer code, String message){
        return new Result<>(code,message,null);
    }
    public static<T>  Result<T> fail( String message){
        return new Result<>(20001,message,null);
    }
} 
拓展session、token、Cookie
拓展
什么是单体项目?
-  
单体项目是指整个应用程序都部署在一个单独的进程中,所有的功能和模块都在同一个代码库中。
 
什么是微服务架构?
-  
微服务架构是一种将一个大型应用程序拆分成多个小型的服务,每个服务都独立运行在自己的进程中,服务之间通过网络进行通信。每个服务都有自己的代码库和数据库,可以独立部署和扩展。微服务架构可以提高系统的可伸缩性和可维护性,但也会增加系统的复杂性和运维成本。
 
什么是前后端分离架构?
-  
前后端分离是一种架构模式,它将应用程序的前端和后端分开开发、部署和维护。在前后端分离架构中,前端主要负责展示和交互逻辑,后端主要负责数据处理和业务逻辑。前端和后端之间通过API进行通信,前端通过API调用后端提供的服务获取数据,后端通过API接收前端传递的请求并返回处理结果。
 
前后端分离session还有用吗
前后端完全分离后,由于前端不再接受后端传来的渲染好的HTML页面,而是后端只提供RESTful API接口,前端通过AJAX请求后台返回的
JSON数据自己进行渲染,所以后台不再需要用session来保存状态,前端使用session效果不好甚至有风险,采用令牌(token)的方式来代替session。使用令牌的优势在于前后端各自按照约定来生成、验证、传递、存储令牌,实现了无状态无session的前后端分离。
在Vue前后端分离架构中,同样采用令牌(token)的方式来代替session。在Vue应用中,可以将令牌存储在Vuex或者localstorage中,在每次请求API时携带对应的令牌来获取响应数据。
Cookie、Token和Session区别
-  
这里写一篇文章
 
hello,我是索奇~
精心写了一篇Cookie、Session和Token的 vivid 文章,并分享给大家
我们可以把Cookie、Token和Session看作是三个好基友,它们都是用来跟踪用户的身份和状态的,但是它们之间有一些区别和使用场景。
Cookie
-  
Cookie:曲奇饼,小甜饼;……样的人;(浏览网页后存储在计算机的)缓存文件;<苏格兰>淡面包;漂亮的女孩
啊,不是让你翻译~ 是让你介绍计算机中Cookie~(不过也学会了一个单词)
 


Cookie就像是你的小秘书,它的主要作用是保存用户的偏好和浏览历史。比如说,你在网上买了一件衣服,但是还没决定是否买下,这时候你可以把这件衣服放进购物车,Cookie就会帮你记住这个购物车里有什么。等到你下次再来这个网站的时候,Cookie就会帮你把购物车里的东西显示出来,让你可以继续购物。
Cookie的数据存储在客户端的浏览器中,不会占用服务器的资源
在浏览器的控制台中,可以直接输入:document.Cookie来查看Cookie。Cookie是一个由键值对构成的字符串,出于安全考虑
httponly类型的获取不到的,不要找半天httponly发现找不到
又多一个名词,再探索一下?
httponly又是什么啊?
HttpOnly就是一个设置在HTTP响应头中的标志,它可以防止某些类型的客户端脚本(如JavaScript)访问cookie。当服务器向客户端发送带有HttpOnly标志的cookie时,客户端的JavaScript代码将无法通过document.cookie访问该cookie,这可以有效地提高Web应用程序的安全性。
如果给某个 cookie 设置了 httponly 属性,则无法通过 JS 脚本
-  
读取到该 cookie 的信息,但还是能通过Application 中手动修改 cookie, 所以只是在一定程度上可以防止 XSS 攻击,不是绝对的安全
 -  
Cookie主要用于跟踪用户的偏好和行为,以便提供个性化的体验。例如,在网站上保存用户的登录状态、购物车信息等。
啊,平时刷视频、逛tb、个性化广告等等的信息居然就是这样被页面记录、推送的
 

还有一个大家都在讨论(众说纷纭)的话题就是-我们平时的浏览记录等信息会被记录吗?
-  
答案是不确定(不保证一定不被记录,不保证一定被记录)
Cookie本身是存储在客户端的,而不是服务器端,所以服务器不需要把
Cookie记录保存到数据库中
但至于记录个人的爱好、浏览记录等信息是否被记录到数据库,如何被记录到数据库,这取决于具体的软件、网站、隐私政策和数据收集方式..
 
Session
Session就像是你的个人档案,它的主要作用是保存用户的状态和权限。比如说,你在网站上登录之后,服务器就会为你创建一个Session,里面保存了你的登录状态和购物车信息等等。这样,当你在浏览网站的时候,服务器就会根据Session来提供个性化的体验,比如显示你的购物车里有什么,或者显示你最近浏览过的商品。
也可以理解为是一个比较特殊的map ,除了可以像其它map一样存取数据,它还有过期时间、唯一的id区分不同session,
创建该session时,会同时创建一个Cookie,Cookie的key为JSESSIONID,而Cookie的value是该session的id。
又遇到不懂的了吗?Cookie的key是啥东西?
JSESSIONID是一种用于在客户端和服务器端之间传递会话信息的Cookie名称。当用户在浏览器中访问一个需要登录的网站时,服务器会
在后台创建一个会话,并生成一个唯一的Session ID,将其存储在服务器端的Session中,同时,服务器会将Session ID通过Cookie的方式发送给客户端,通常使用的Cookie名称就是OBSESSION

-  
Session的数据信息存放在服务器上,Session的数据只能被服务器访问,因此相对来说比较安全,但是需要占用服务器的资源,
 -  
Session主要用于跟踪用户的状态和权限,以便提供个性化的体验。例如,你搜索的内容、在网站上保存用户的登录状态、购物车信息等。
 -  
对于Session并没有上限,但出于对服务器端的性能考虑,Session内不要存放过多的东西
 
Token

Token就像是你的身份证,它的主要作用是用于身份验证和授权。比如说,你在使用某个APP的时候,需要登录才能使用一些功能,这时候APP就会颁发给你一个Token(令牌),你需要在每个请求中携带这个Token,服务器会通过验证Token来确定你的身份和权限,以确保你只能访问你有权访问的内容。

比如用户已经登录了系统, 我给他发一个token, 里边包含了这个用户的 user id, 下一次这个用户再次通过Http 请求访问我的时候, 把这个token 通过Http header 带过来就可以了。
但是这时候感觉和session没区别啊,万一有人伪造做假攻击呢?于是就用算法对数据做了签名,用签名+数据 = token ,签名不知道,也就无法伪造token了
这个token 不保存, 当用户把这个token 给我发过来的时候,我再用同样的算法和同样的密钥,对数据再计算一次签名, 和token 中的签名做个比较, 如果相同, 我就知道用户已经登录过了,并且可以直接取到用户的user id , 如果不相同, 数据部分肯定被人篡改过, 就知道这个人是冒充货,返给它没有认证的信息

Token是一种无状态的身份验证机制,意味着服务器不需要保存Token的状态(这不是大大的减轻了服务器的压力~),前后端分离架构中前端无法直接访问后端的Session。但是,前后端分离架构中依然可以使用Session来存储应用程序的其他状态信息,例如购物车数据等,只是不能用来保存用户的登录状态。
-  
既可以保存在服务器也可以在客户端
 -  
Token是一种无状态的身份验证机制,它可以在多个服务器之间共享,而Session则需要在每个服务器上都保存一份。使用Token可以避免Session共享和Session过期等问题,同时也可以降低服务器的负担。
 -  
Token 中的数据是明文保存的, 还是可以被别人看到的, 所以我不能在其中保存像密码这样的敏感信息
 -  
基于Token的身份验证是无状态的,我们不将用户信息存在服务器或Session中。
 -  
大多数使用Web API的互联网公司中,它是Tokens多用户下处理认证的最佳方式
 -  
被攻击是不是很烦恼! Token通常用于API身份验证等场景,可以有效避免跨站请求伪造(CSRF)等攻击~
 
拓展一下Token的身份验证过程
-  
用户在客户端进行登录操作,将用户名和密码发送到服务器端。
 -  
服务器端通过验证用户名和密码的正确性,生成一个Token,并将Token返回给客户端。
 -  
客户端将Token保存在本地,例如在浏览器的Cookie或localStorage中。
 -  
客户端在后续的请求中,将Token发送给服务器端进行身份验证。
 -  
服务器端接收到请求后,从请求中获取Token,并对Token进行解密和验证。
 -  
如果Token验证通过,服务器端将响应请求并返回所需的数据,否则返回身份验证失败的错误信息。
 
在身份验证过程中,服务器端通常会对Token进行解密、验证签名、检查Token是否过期等操作,以确保Token的有效性和安全性
栩栩如生、通俗易懂~ 重点讲完了!
简单记一些知识
看完了没,啥也没懂?好吧,无奈,简单记一下区别吧,面试时候不能哑口无言吧

-  
Session和Token是在服务器端保存数据的机制,而Cookie是在客户端保存数据的机制
通常情况单个Cookie保存的数据在4KB以内(面试官:这都知道,给你offer!欣喜若狂的自己:太好了!)
 -  
Session和Token通常用于身份验证和状态管理,而Cookie通常用于跟踪用户的偏好和行为
 -  
Session和Token通常用于敏感数据的存储和传输,而Cookie通常用于非敏感数据的存储和传输。
 -  
Session和Token需要服务器端进行管理和维护,而Cookie可以由客户端自行管理和维护。
 -  
Token可以跨域使用,而Session通常只能在同一个域名下使用;Token可以在分布式系统中使用,而Session通常只能在单一服务器上使用。
 
(可以忽略)写着写着又想要拓展了,哈哈哈,想要探索的伙伴们,一定想要知道单个站点可以存储的Cookie数量,
这里有疑惑?
国际互联网标准是每个网站可以存储的 Cookie 数量不得超过 300 个,具体还是根据不同的浏览器来定,
发现部分博主说单个站点最多保存20个Cookie,这是不合理的,也有近100点赞
网上一连串的信息是复制的,有时我们不能轻易的相信,要学会自己去探索,去验证!不然就误人耳目了


这里是仅仅是为了说明下Cookie的数量,帮助更多伙伴学会探索知识,对原博主没有任何恶意哈
4. 登录相关接口
4.1 登录
-  
登录的信息放到Redis中
 
| 接口属性 | 值 | 
|---|---|
| url | /user/login | 
| method | post | 
| 请求参数 | username password | 
| 返回参数 |   
  | 
controller
 /**
      *
      * @param user
      * SpringMVC默认情况下,请求体中的数据是以 JSON 格式传输的。如果你不使用 @RequestBody 注解,
      * SpringMVC 在处理请求时就不会将请求体中的数据解析成 JSON 对象,也就无法将请求体中的数据转化为 User 对象。
      * @return
      */
     @PostMapping("/login")
     public Result<Map<String,Object>> login(@RequestBody User user){
         // 根据用户名和密码在数据库中遍历,如果存在表示信息正确,具体的登录逻辑在UserServiceImpl业务层中实现
         Map<String,Object> data = userService.login(user);
         if (data!= null){
             return Result.success(data);
         }
         // 可以自行拓展为枚举类
         return Result.fail(20002,"用户名和密码错误");
     } 
service
  @Autowired
 private RedisTemplate redisTemplate;
     /**
      * @param user
      * @return
      * @description 根据用户名和密码查询
      */
     @Override
     public Map<String, Object> login(User user) {
         // 结果不为空,生成token,为空,则将信息写入Redis
         LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(User::getUsername, user.getUsername());
         wrapper.eq(User::getPassword, user.getPassword());
         User loginUser = this.baseMapper.selectOne(wrapper);
         if (loginUser != null) {
             // 简单项目用UUID,可以改为更好的jwt方案
             String key = "user:" + UUID.randomUUID();
             // 存入redis
             // 防止密码存入redis中
             loginUser.setPassword(null);
             /*
             redisTemplate.opsForValue()是RedisTemplate提供的一个操作字符串类型数据的方法
             它返回一个ValueOperations对象,
             可以用来对Redis中的字符串类型数据进行操作可以使用Redis中的set、get、delete等操作字符串类型数据的命令。
              */
             redisTemplate.opsForValue().set(key, loginUser, 30, TimeUnit.MINUTES);
             //返回数据
             Map<String, Object> data = new HashMap<>();
             data.put("token", key);
             return data;
         }
 
         return null;
     }
 } 
-  
测试的时候用post方法,由于浏览器发送的是get请求,会报错
 
Resolved [org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'GET' not supported]
-  
我们需要用postman这个工具进行post请求
 


整合redis
-  
整合redis需要用启动redis服务
 -  
这里只是简单的redis功能,具体看其它项目实现,也可以系统的学习redis
 

-  
pom
<!-- redis --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
 -  
yml
spring: redis: host: localhost port: 6379
 
配置类
-  
代码中的
StringRedisSerializer是 RedisTemplate 默认的 key 和 value 的序列化器。在使用StringRedisSerializer进行序列化时,它会将字符串对象转换为字节数组,并将其存储到 Redis 中。在读取数据时,它会将字节数组反序列化为字符串对象。 -  
Jackson2JsonRedisSerializer可以将 Java 对象序列化为 JSON 格式的字符串,并将其存储到 Redis 中。在读取数据时,它可以将 JSON 格式的字符串反序列化为 Java 对象; 
 @Configuration
 public class MyRedisConfig {
     //用于创建Redis连接的接口,不需要关心底层 Redis 连接实现的细节,就直接可以使用 Redis 进行数据存储和缓存。
     @Resource
     private RedisConnectionFactory factory;
 
     @Bean
     public RedisTemplate redisTemplate(){
         RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
          //改变序列化器
         redisTemplate.setKeySerializer(new StringRedisSerializer());
         //设置为RedisTemplate的连接工厂可以让这个对象与Redis服务器进行交互
         redisTemplate.setConnectionFactory(connectionFactory);
         // 类型不确定所以用Object
         Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
         redisTemplate.setValueSerializer(serializer);
 
         ObjectMapper om = new ObjectMapper();
         om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
         om.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
         om.setTimeZone(TimeZone.getDefault());
         om.configure(MapperFeature.USE_ANNOTATIONS, false);
         om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
         om.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
         om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
         om.setSerializationInclusion(JsonInclude.Include.NON_NULL);
         serializer.setObjectMapper(om);
 
         return redisTemplate;
     }
 } 

4.2 获取用户信息

| 接口属性 | 值 | 
|---|---|
| url | /user/info?token=xxx | 
| method | get | 
| 请求参数 | token | 
| 返回参数 | ![]()  | 
-  
其中的roles角色表我们在下一节才会讲到
 -  
avator(化身):头像的地址
 -  
name:登录的用户名
 
controller
-  
返回值 Result<Map<String,Object>> 在这里写具体的或者?都可以
 
 @GetMapping("/info")
     public Result<Map<String,Object>> getUserInfo(@RequestParam("token") String token){
         // 根据token获取用户信息
         Map<String,Object> data = userService.getUserInfo(token);
         if (data!= null){
             return Result.success(data);
         }
         // 可以自行拓展为枚举类
         return Result.fail(20003,"用户信息无效,请重新登陆");
     } 
service
 @Override
     public Map<String, Object> getUserInfo(String token) {
         // 根据token获取用户信息,redis
         Object obj = redisTemplate.opsForValue().get(token);
         // 在 redisConfig中已经做了序列化,所以需要用抽象类JSON反序列化取出来,转换成User对象(也可以用其它的实现)
         if(obj!=null){
             User loginUser = JSON.parseObject(JSON.toJSONString(obj), User.class);
             Map<String,Object> data = new HashMap<>();
             data.put("name",loginUser.getUsername());
             data.put("avatar",loginUser.getAvatar());
             List<String> roleList = this.baseMapper.getRoleNameByUserId(loginUser.getId());
             //角色,一个人可能有多个角色
             data.put("roles", roleList);
             return data;
 
         }
         return null;
     } 
-  
视频项目中用的是SQL来进行多表联查
 
UserMapper
 public interface UserMapper extends BaseMapper<User> {
     public List<String> getRoleNameByUserId(Integer userId);
 
 } 
 
UserMapper.xml
 <select id="getRoleNamesByUserId" parameterType="Integer" resultType="String">
     SELECT
     b.role_name
     FROM x_user_role a,x_role b
     WHERE a.`user_id` = #{userId}
     AND a.`role_id` = b.`role_id`
 </select> 
-  
用postman测试时候先http://localhost:9999/user/login请求token,复制token新建get请求http://localhost:9999/user/info?token写入token成功证明你没错~
 
4.3 注销
| 接口属性 | 值 | 
|---|---|
| url | /user/logout | 
| method | post | 
| 请求参数 | |
| 返回参数 | [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DwV5GD4c-1675952251553)(C:\Users\dacai\AppData\Roaming\Typora\typora-user-images\image-20230203171855151.png)] | 
controller
-  
前面是把token保存到了Redis中,把它清除掉即可
 -  
前端设置的token名叫x-token
 
 @PostMapping("/logout")
 public Result<?> logout(@RequestHeader("X-Token") String token){
     userService.logout(token);
     return Result.success("注销成功");
 } 
service
 public void logout(String token) {
     redisTemplate.delete(token);
 } 
 
前端中有这一个代码
 before: require('./mock/mock-server.js')  
-  
前后端对接的时候应该被移除,或者注释掉
 
这段代码是在Vue项目中使用mock数据的一种方式。mock数据是指在前端开发过程中,模拟后端接口返回的数据,用于前端开发和调试。在这段代码中,require('./mock/mock-server.js')表示引入mock-server.js文件,该文件中定义了mock数据的生成规则和接口拦截规则。在开发环境中,通过这种方式可以使用mock数据来替代后端接口,方便前端开发和测试。在生产环境中,这段代码应该被移除,以避免不必要的性能损耗。
6. 跨域处理
跨域是指在浏览器中,当前网页所在的域名与当前请求所访问的域名不同,即跨域请求。
举个栗子:如果当前网页的URL为https://www.example.com,则同源策略要求发送请求的URL也必https://www.example.com,否则就会被拦截。
-  
Access-Control-Allow-Origin
 

这里设置的全局跨域处理,不建议使用注解局部方式
 @Configuration
 public class MyCorsConfig {
 //     当前跨域请求最大有效时长,这里默认1天
 //    private static final long MAX_AGE = 24 * 60 * 60;
     @Bean
     public CorsFilter corsFilter() {
         //1.添加CORS配置信息
         CorsConfiguration config = new CorsConfiguration();
         //1) 允许的域,不要写*,否则Cookie就无法使用了
         //这里填写请求的前端服务器
         config.addAllowedOrigin("http://localhost:8888");
         //2) 是否发送Cookie信息
         config.setAllowCredentials(true);
         //3) 允许的请求方式
         config.addAllowedMethod("*");
 
 //        config.setMaxAge(MAX_AGE);
         // 4)允许的所有的请求头
         config.addAllowedHeader("*");
         //2.添加映射路径,我们拦截一切请求
         UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
 //        接收的是接收CorsConfiguration类型的参数,
         urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", config);
 
         //3.返回新的CorsFilter.
         return new CorsFilter(urlBasedCorsConfigurationSource);
     }
 }
  
CorsFilter源码大致如下
 public CorsFilter(CorsConfigurationSource configSource, CorsProcessor processor) {
     Assert.notNull(configSource, "CorsConfigurationSource must not be null");
     Assert.notNull(processor, "CorsProcessor must not be null");
     this.configSource = configSource;
     this.processor = processor;
 } 
注意:
如果使用的是 Spring Boot 2.4 及以上版本,您还需要在 application.properties 或 application.yml 文件中添加以下配置项,以允许跨域请求携带 Cookie:
spring: mvc: cors: allow-credentials: true
这个配置项会告诉 Spring Boot 在跨域请求中允许携带 Cookie。
提示:脚手架版本用的是:2.13.2 ,所以建议把element更改为2.13.2版本,避免bug

如果分页面是英文,可在main.js下面更改为中文zh-CN

7. 用户管理接口
| 接口 | 说明 | 
|---|---|
| 查询用户列表 | 分页查询 | 
| 新增用户 | |
| 根据id查询用户 | |
| 修改用户 | |
| 删除用户 | 逻辑删除 | 
7.1 查询用户列表
-  
controller
@GetMapping("/list") public Result<?> getUserListPage(@RequestParam(value = "username", required = false) String username, @RequestParam(value = "phone", required = false) String phone, @RequestParam("pageNo") Long pageNo, @RequestParam("pageSize") Long pageSize) { LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper(); wrapper.eq(username != null, User::getUsername, username); wrapper.eq(phone != null, User::getPhone, phone); Page<User> page = new Page<>(pageNo, pageSize); userService.page(page, wrapper); Map<String, Object> data = new HashMap<>(); data.put("total", page.getTotal()); data.put("rows", page.getRecords());  return Result.success(data); }IService源码
default <E extends IPage<T>> E page(E page, Wrapper<T> queryWrapper) { return this.getBaseMapper().selectPage(page, queryWrapper); } -  
分页拦截器配置
复制过来的别忘了把
new PaginationInnerInterceptor(DbType.MYSQL)这里改为我们的MYSQL数据库
@Configuration public class MpConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } } 
测试

对接前后端
更改api文件中进行对接后端
7.2 新增用户
密码不能是明文,需要加密处理,用BCryptPasswordEncoder,涉及登录逻辑改动
    @PostMapping
     public Result<?> addUser(@RequestBody User user){
         user.setPassword(passwordEncoder.encode(user.getPassword()));
         userService.save(user);
         return Result.success("新增用户成功");
     } 
7.3 修改用户
此处不提供密码更新,大家自行扩展,可以去实现前端右上角菜单的个人信息功能
修改展示
注意这里response.data不加括号,它不是方法!这点容易忽略
 saveUser() {
       // 触发表单验证
       this.$refs.userFormRef.validate((valid) => {
         if (valid) {
           // 提交给后台
           userApi.saveUser(this.userForm).then(response => {
           // 成功提示
             this.$message({
               message: response.message,
               type: 'success'
             })
             // 关闭对话框
             this.dialogFormVisible = false
             // 刷新表格
             this.getUserList()
           })
         } else {
           console.log('error submit!!')
           return false
         }
       })
     }, 
 
   saveUser(user) {
     if (user.id == null || user.id === undefined) {
       return this.addUser(user)
     }
     return this.updateUser(user)
   } 
 
   getUserById(id) {
     return request({
       url: `/user/'+${id}`,
       method: 'get',
       data: user
     })
   } 

7.4 删除用户
利用MyBatisPlus做逻辑删除处理(MybatisPlus官网上有配置也可以复制哈)
yml(别忘记重启项目)
mybatis-plus: global-config: db-config: logic-delete-field: delted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) logic-delete-value: 1 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
Controller
   @DeleteMapping("/{id}")
     public Result<User> deleteUserById(@PathVariable("id") Integer id) {
         userService.removeById(id);
         return Result.success("删除成功");
     } 
我们要学会善于查阅文档,不能局限于笔记、视频中的说明,也要结合别人的理解,善于查阅官方文档,这样才能够进一步的打通自己的任通二脉,找到自己的路~ 加油,未来可期
补充
便于大家快速查阅,这里留存一些整个类的文档
Entity
User
 @TableName("x_user")
 public class User implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
     //主键字段名为id,主键生成策略为自增长。
     @TableId(value = "id", type = IdType.AUTO)
     private Integer id;
 
     private String username;
 
     private String password;
 
     private String email;
 
     private String phone;
 
     private Integer status;
 
     private String avatar;
 
     private Integer deleted;
 
     public Integer getId() {
         return id;
     }
 
     public void setId(Integer id) {
         this.id = id;
     }
     public String getUsername() {
         return username;
     }
 
     public void setUsername(String username) {
         this.username = username;
     }
     public String getPassword() {
         return password;
     }
 
     public void setPassword(String password) {
         this.password = password;
     }
     public String getEmail() {
         return email;
     }
 
     public void setEmail(String email) {
         this.email = email;
     }
     public String getPhone() {
         return phone;
     }
 
     public void setPhone(String phone) {
         this.phone = phone;
     }
     public Integer getStatus() {
         return status;
     }
 
     public void setStatus(Integer status) {
         this.status = status;
     }
     public String getAvatar() {
         return avatar;
     }
 
     public void setAvatar(String avatar) {
         this.avatar = avatar;
     }
     public Integer getDeleted() {
         return deleted;
     }
 
     public void setDeleted(Integer deleted) {
         this.deleted = deleted;
     }
 
     @Override
     public String toString() {
         return "User{" +
             "id=" + id +
             ", username=" + username +
             ", password=" + password +
             ", email=" + email +
             ", phone=" + phone +
             ", status=" + status +
             ", avatar=" + avatar +
             ", deleted=" + deleted +
         "}";
     }
 }
  
UserRole
 @TableName("x_user_role")
 public class UserRole implements Serializable {
 
     private static final long serialVersionUID = 1L;
 
     @TableId(value = "id", type = IdType.AUTO)
     private Integer id;
 
     private Integer userId;
 
     private Integer roleId;
 
     public Integer getId() {
         return id;
     }
 
     public void setId(Integer id) {
         this.id = id;
     }
     public Integer getUserId() {
         return userId;
     }
 
     public void setUserId(Integer userId) {
         this.userId = userId;
     }
     public Integer getRoleId() {
         return roleId;
     }
 
     public void setRoleId(Integer roleId) {
         this.roleId = roleId;
     }
 
     @Override
     public String toString() {
         return "UserRole{" +
             "id=" + id +
             ", userId=" + userId +
             ", roleId=" + roleId +
         "}";
     }
 }
  
 
Controller
UserController
 @RestController
 @RequestMapping("/user")
 public class UserController {
     @Autowired
     private IUserService userService;
     @Autowired
     private PasswordEncoder passwordEncoder;
     @GetMapping("/all")
     public Result<List<User>> getAllUser() {
         List<User> list = userService.list();
         return Result.success(list, "查询成功");
     }
 
     /**
      * @param user SpringMVC默认情况下,请求体中的数据是以 JSON 格式传输的。如果你不使用 @RequestBody 注解,
      *             SpringMVC 在处理请求时就不会将请求体中的数据解析成 JSON 对象,也就无法将请求体中的数据转化为 User 对象。
      * @return
      */
     @PostMapping("/login")
     public Result<Map<String, Object>> login(@RequestBody User user) {
         // 根据用户名和密码在数据库中遍历,如果存在表示信息正确,具体的登录逻辑在UserServiceImpl业务层中实现
         Map<String, Object> data = userService.login(user);
         if (data != null) {
             return Result.success(data);
         }
         // 可以自行拓展为枚举类
         return Result.fail(20002, "用户名和密码错误");
     }
 
     /**
      * @param token
      * @return
      * @description 将名为 "token" 的 HTTP 请求参数绑定到方法参数 token 上。
      */
     @GetMapping("/info")
     public Result<Map<String, Object>> getUserInfo(@RequestParam("token") String token) {
         // 根据token获取用户信息
         Map<String, Object> data = userService.getUserInfo(token);
         if (data != null) {
             return Result.success(data);
         }
         // 可以自行拓展为枚举类
         return Result.fail(20003, "用户信息无效,请重新登陆");
     }
 
     @PostMapping("logout")
     public Result<?> logout(@RequestHeader("X-Token") String token) {
         userService.logout(token);
         return Result.success();
 
     }
 
     @GetMapping("/list")
     public Result<Map<String, Object>> getUserList (@RequestParam(value = "username", required = false) String username,
                                                @RequestParam(value = "phone", required = false) String phone,
                                                @RequestParam("pageNo") Long pageNo,
                                                @RequestParam("pageSize") Long pageSize) {
         LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper();
         wrapper.eq(StringUtils.hasLength(username),User::getUsername, username);
         wrapper.eq(StringUtils.hasLength(phone),User::getPhone, phone);
         wrapper.orderByDesc(User::getId);
         Page<User> page = new Page<>(pageNo,pageSize);
         userService.page(page,wrapper);
         Map<String,Object> data = new HashMap<>();
         data.put("total",page.getTotal());
         data.put("rows",page.getRecords());
         return Result.success(data);
     }
 
     /**
      * 新增用户
      * @return
      */
     @PostMapping
     public Result<?> addUser(@RequestBody User user){
         user.setPassword(passwordEncoder.encode(user.getPassword()));
         userService.save(user);
         return Result.success("新增用户成功");
     }
     @PutMapping
     public Result<?> updateUser(@RequestBody User user){
         user.setPassword(null);
         userService.updateById(user);
         return Result.success("修改用户成功");
     }
     @GetMapping("/{id}")
     public Result<User> getUserById(@PathVariable("id") Integer id) {
         User user = userService.getById(id);
         return Result.success(user);
     }
     @DeleteMapping("/{id}")
     public Result<User> deleteUserById(@PathVariable("id") Integer id) {
         userService.removeById(id);
         return Result.success("删除成功");
     }
 } 
Service
IUserService
IUserService
 public interface IUserService extends IService<User> {
 
     Map<String, Object> login(User user);
 
     Map<String, Object> getUserInfo(String token);
 
     void logout(String token);
 } 
UserServiceImpl
UserServiceImpl
 @Service
 public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
     @Autowired
     private RedisTemplate redisTemplate;
     @Autowired
     private PasswordEncoder passwordEncoder;
     /**
      * @param user
      * @return
      * @description 根据用户名查询,加密后处理
      */
     @Override
     public Map<String, Object> login(User user) {
         // 结果不为空并且匹配传入的密码,生成token,为空,则将信息写入Redis
         LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(User::getUsername, user.getUsername());
         User loginUser = this.baseMapper.selectOne(wrapper);
         if (loginUser != null && passwordEncoder.matches(user.getPassword(),loginUser.getPassword())) {
             // 简单项目用UUID,可以改为更好的jwt方案
             String key = "user:" + UUID.randomUUID();
             // 存入redis2
             // 防止密码存入redis中
             loginUser.setPassword(null);
 
 //            redisTemplate.opsForValue()是RedisTemplate提供的一个操作字符串类型数据的方法
 //            它返回一个ValueOperations对象,
 //            可以用来对Redis中的字符串类型数据进行操作可以使用Redis中的set、get、delete等操作字符串类型数据的命令。
 
             redisTemplate.opsForValue().set(key, loginUser, 30, TimeUnit.MINUTES);
             //返回数据
             Map<String, Object> data = new HashMap<>();
             data.put("token", key);
             return data;
         }
 
         return null;
     }
 //    /**
 //     * @param user
 //     * @return
 //     * @description 根据用户名和密码查询
 //     */
 //    @Override
 //    public Map<String, Object> login(User user) {
 //        // 结果不为空,生成token,为空,则将信息写入Redis
 //        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
 //        wrapper.eq(User::getUsername, user.getUsername());
 //        wrapper.eq(User::getPassword, user.getPassword());
 //        User loginUser = this.baseMapper.selectOne(wrapper);
 //        if (loginUser != null) {
 //            // 简单项目用UUID,可以改为更好的jwt方案
 //            String key = "user:" + UUID.randomUUID();
 //            // 存入redis2
 //            // 防止密码存入redis中
 //            loginUser.setPassword(null);
 //
 //            /*redisTemplate.opsForValue()是RedisTemplate提供的一个操作字符串类型数据的方法
 //            它返回一个ValueOperations对象,
 //            可以用来对Redis中的字符串类型数据进行操作可以使用Redis中的set、get、delete等操作字符串类型数据的命令。*/
 //
 //            redisTemplate.opsForValue().set(key, loginUser, 30, TimeUnit.MINUTES);
 //            //返回数据
 //            Map<String, Object> data = new HashMap<>();
 //            data.put("token", key);
 //            return data;
 //        }
 //
 //        return null;
 //    }
 
     @Override
     public Map<String, Object> getUserInfo(String token) {
         // 根据token获取用户信息,redis
         Object obj = redisTemplate.opsForValue().get(token);
         // 在 redisConfig中已经做了序列化,所以需要用抽象类JSON反序列化取出来,转换成User对象(也可以用其它的实现)
         if (obj != null) {
             //将一个Java对象转换为JSON字符串,然后再将JSON字符串转换回Java对象可以将数据格式标准化
             // JSON.parseObject第一个参数是要转换的JSON字符串,第二个参数是要转换成的Java对象类型。
             User loginUser = JSON.parseObject(JSON.toJSONString(obj), User.class);
             Map<String, Object> data = new HashMap<>();
             data.put("name", loginUser.getUsername());
             data.put("avatar", loginUser.getAvatar());
             List<String> roleList = this.baseMapper.getRoleNameByUserId(loginUser.getId());
 
             //角色,一个人可能有多个角色
             data.put("roles", roleList);
             return data;
         }
         return null;
     }
 
     @Override
     public void logout(String token) {
         redisTemplate.delete(token);
     }
 } 
Result
Result
 @Data
 @NoArgsConstructor
 @AllArgsConstructor
 public class Result<T> {
     private Integer code;
     private String message;
     private T data;
 
     public static <T>  Result<T> success(){
         return new Result<>(20000,"success",null);
     }
 
     public static<T>  Result<T> success(T data){
         return new Result<>(20000,"success",data);
     }
 
     public static<T>  Result<T> success(T data, String message){
         return new Result<>(20000,message,data);
     }
 
     public static<T>  Result<T> success(String message){
         return new Result<>(20000,message,null);
     }
 
     public static<T>  Result<T> fail(){
         return new Result<>(20001,"fail",null);
     }
 
     public static<T>  Result<T> fail(Integer code){
         return new Result<>(code,"fail",null);
     }
 
     public static<T>  Result<T> fail(Integer code, String message){
         return new Result<>(code,message,null);
     }
 
     public static<T>  Result<T> fail( String message){
         return new Result<>(20001,message,null);
     }
 
 }
  
config
MpConfig
 @Configuration
 public class MpConfig {
     @Bean
     public MybatisPlusInterceptor mybatisPlusInterceptor() {
         MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
         interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
         return interceptor;
     }
 } 
MyCorsConfig
 @Configuration
 public class MyCorsConfig {
 //     当前跨域请求最大有效时长,这里默认1天
 //    private static final long MAX_AGE = 24 * 60 * 60;
     @Bean
     public CorsFilter corsFilter() {
         //1.添加CORS配置信息
         CorsConfiguration config = new CorsConfiguration();
         //1) 允许的域,不要写*,否则Cookie就无法使用了
         //这里填写请求的前端服务器
         config.addAllowedOrigin("http://localhost:8888");
         //2) 是否发送Cookie信息
         config.setAllowCredentials(true);
         //3) 允许的请求方式
         config.addAllowedMethod("*");
 
 //        config.setMaxAge(MAX_AGE);
         // 4)允许的所有的请求头
         config.addAllowedHeader("*");
         //2.添加映射路径,我们拦截一切请求
         UrlBasedCorsConfigurationSource urlBasedCorsConfigurationSource = new UrlBasedCorsConfigurationSource();
 //        接收的是接收CorsConfiguration类型的参数,
         urlBasedCorsConfigurationSource.registerCorsConfiguration("/**", config);
 
         //3.返回新的CorsFilter.
         return new CorsFilter(urlBasedCorsConfigurationSource);
     }
 }
  
MyRedisConfig
 @Configuration
 public class MyRedisConfig {
     @Resource
     private RedisConnectionFactory factory;
 
     @Bean
     public RedisTemplate redisTemplate(){
         RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
         redisTemplate.setConnectionFactory(factory);
         redisTemplate.setKeySerializer(new StringRedisSerializer());
 
         Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
         redisTemplate.setValueSerializer(serializer);
 
         ObjectMapper om = new ObjectMapper();
         om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
         om.setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
         om.setTimeZone(TimeZone.getDefault());
         om.configure(MapperFeature.USE_ANNOTATIONS, false);
         om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
         om.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
         om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
         om.setSerializationInclusion(JsonInclude.Include.NON_NULL);
         serializer.setObjectMapper(om);
 
         return redisTemplate;
     }
 } 
 
yml
yml
server: port: 9999  spring: datasource: username: root password: 123456 url: jdbc:mysql:///xdb redis: port: 6379 host: localhost  logging: level: com.suoqi: debug  mybatis-plus: global-config: db-config: logic-delete-field: delted # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2) logic-delete-value: 1 # 逻辑已删除值(默认为 1) logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
mapper
UserMapper
 public interface UserMapper extends BaseMapper<User> {
     public List<String> getRoleNameByUserId(Integer userId);
 } 
xml
UserMapper.xml
 <?xml version="1.0" encoding="UTF-8"?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.suoqi.sys.mapper.UserMapper">
     <select id="getRoleNameByUserId" parameterType="Integer" resultType="String">
         SELECT b.role_name
         FROM x_user_role a,
              x_role b
         WHERE a.`user_id` = #{userId}
           AND a.`role_id` = b.`role_id`
     </select>
 </mapper>
 
                









![数据结构07:查找[C++][红黑二叉排序树RBT]](https://img-blog.csdnimg.cn/9ff79cfb24c24e3ab0cb282e6b1574fd.png)










