【40000字】!最适合新手的Springboot+Vue项目

更多文章: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','[email protected]','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','[email protected]','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','[email protected]','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','[email protected]','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','[email protected]','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','[email protected]','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. 项目初始化

  1. 解压至非中文无空格目录下(防止出现异常)

  2. 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
  1. 运行测试

    部署看一下有没有问题

     npm run dev

    默认打开登录页面,登录之后成功进入主页面

  2. 配置修改

语法校验:lintOnSave

  • 表示当前为开发环境,此时ESLint将会在保存文件时进行代码检查。不开启的话可以改为false

默认打开浏览器:true

title:自己改喜欢的名字~

mock:用于模拟后端数据(后端建立之后可以删除了)

  • 以下是视频中配置的内容,我这里除了名字,其它都保持一致,方便和视频效果一样

idea中点击Navigator可以在这里找到相应的文件更改名字

  • 快捷键ctrl+shift+n

  • "symbol"(符号)指的是代码中的标识符,例如类名、方法名、变量名等。当您使用搜索功能时,可以选择搜索符号,从而查找与特定标识符相关的代码。我们可以选择symbols定位title等内容

    快捷键ctrl+shift+alt+n

都改好之后重启服务测试

4. 登录页修改

都改好之后重启服务测试

4. 登录页修改

  1. 中文描述

  2. 背景图

  3. 中文描述

  4. 背景图

  5. 所有的英文我们都可以根据英文查找进行选择性修改

    图片放在assets里面,然后修改.login-container(完整项目已经打包放进去了,)

    background-image: url('../../assets/bg.jpeg');
    1

    登录框调整

  1. 登录用户名取消限制

5. 修改右上角用户下拉菜单

  • src/layout/components/Navbar.vue

  • 索奇感觉自带的这几个下拉菜单还不错,这里没有做任何修改

//下拉菜单
<el-dropdown-item>

6. 首页面包屑导航

7. 菜单初始化

  1. 在src\views目录下创建sys模块目录、test模块目录(充数用,后续可用作权限分配测试)

  2. 在sys下创建user.vue、role.vue两个组件文件

    在test下创建test1.vue、test2.vue、test3.vue

  3. 修改路由配置

     {
         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. 标签栏导航

  1. @/layout/components/AppMain.vue

    <keep-alive :include="cachedViews">
        <router-view :key="key" />
    </keep-alive>
    cachedViews() {
        return this.$store.state.tagsView.cachedViews
    }

  2. 复制vue-element-admin项目中的文件到相应的目录中

    • idea直接复制粘贴即可(VsCode打开文件夹粘贴)

    @/layout/components/TagsView @/store/modules/tagsView.js @/store/modules/permission.js

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

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

  4. 修改文件@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
     ​

  5. 修改文件@\layout\index.vue

    • 导入、注册

  6. 修改文件@layout\components\index.js

    在index.js下面导出组件,其他模块可以通过导入(import)TagsView组件来使用它。

    export { default as TagsView } from './TagsView'
  7. 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格式,易于观察

10. 对接后端接口

  1. 修改 .env.development 中的base api,打包部署的话要修改.env.production

    VUE_APP_BASE_API = 'http://localhost:9999'
  2. 修改vue.config.js,屏蔽mock请求

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

  4. 测试,预期会出现跨域错误

  5. 后端做跨域处理测试应该成功,并可在调试窗口观察接口调用情况

11. 用户管理

预览

  • 用户查询

    1. 定义userManager.js

    2. 分页序号处理

       <template slot-scope="scope">
           {
            
            {(searchModel.pageNo-1) * searchModel.pageSize + scope.$index + 1}}
       </template>
       123
  • 用户新增

    1. 窗口关闭后数据还在

      监听close,清理表单

    2. 表单数据验证

      常规验证

      自定义验证

    3. 窗口关闭后上次验证结果还在

  • 用户修改

  • 用户删除

后端

1. 项目初始化

  1. 创建springboot项目:2.7.8

  2. 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 配置文件、属性文件等等。

  3. 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 数据库。这种方式相当于使用主机名为 localhost127.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

  1.  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();
         }
     }
     ​

    运行即可生成相应的文件

  2. 启动类加注解

    //表示扫描com.suoqi包及其子包下所有以Mapper结尾的接口,并将其注册到MyBatis中
    @MapperScan("com.suoqi.*.mapper")
  3. 测试

    启动类

    /**
     * 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的keyJSESSIONID,而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>

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 查询用户列表

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

  2. 分页拦截器配置

    复制过来的别忘了把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>

 ​

猜你喜欢

转载自blog.csdn.net/m0_64880608/article/details/131485810
今日推荐