更多文章: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>