SpringBoot+Vue开发流程 这节课目的是分享一下我自己的开发流程,如果需要深入学习还是得自己在b站看视频边看边学的。
这里演示一个登录注册的小demo吧
后端项目初始化 环境准备 然后启动一下看看是否报错
配置maven 
整合依赖 MyBatis-Plus 官网:快速开始 | MyBatis-Plus 
1、在pom.xml添加下面的依赖并且删除MyBatis的依赖防止依赖冲突
SpringBoot2.x
1 2 3 4 5 <dependency >     <groupId > com.baomidou</groupId >      <artifactId > mybatis-plus-boot-starter</artifactId >      <version > 3.5.10.1</version >  </dependency > 
 
SpringBoot3.x
1 2 3 4 5 <dependency >     <groupId > com.baomidou</groupId >      <artifactId > mybatis-plus-spring-boot3-starter</artifactId >      <version > 3.5.10.1</version >  </dependency > 
 
添加好依赖之后,在启动文件添加包扫描注解
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 package  com.zhbit;import  org.mybatis.spring.annotation.MapperScan;import  org.springframework.boot.SpringApplication;import  org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication @MapperScan("com.zhbit.mapper") public  class  SpringbootModelApplication  {    public  static  void  main (String[] args)  {         SpringApplication.run(SpringbootModelApplication.class, args);     } } 
 
配置mybatis-plus
1 2 3 4 5 6 7 8 9 10 mybatis-plus:   type-aliases-package:  com.zhbit.entity     configuration:      map-underscore-to-camel-case:  false       log-impl:  org.apache.ibatis.logging.stdout.StdOutImpl     global-config:      db-config:        logic-delete-field:  isDelete         logic-delete-value:  1         logic-not-delete-value:  0   
 
官网:入门和安装 
1、在pom.xml中添加下面的依赖
1 2 3 4 5 <dependency >     <groupId > cn.hutool</groupId >      <artifactId > hutool-all</artifactId >      <version > 5.8.26</version >  </dependency > 
 
Knife4j 官网:快速开始 | Knife4j 
springboot3.x 
1 2 3 4 5 <dependency >     <groupId > com.github.xiaoymin</groupId >      <artifactId > knife4j-openapi3-jakarta-spring-boot-starter</artifactId >      <version > 4.4.0</version >  </dependency > 
 
springboot2.x 
1 2 3 4 5 <dependency >     <groupId > com.github.xiaoymin</groupId >      <artifactId > knife4j-openapi2-spring-boot-starter</artifactId >      <version > 4.4.0</version >  </dependency > 
 
根据官网配置接口文档
1 2 3 4 5 6 7 8 9 10 11 knife4j:   enable:  true    openapi:      title:  "接口文档"      version:  1.0      group:        default:          api-rule:  package          api-rule-resources:            -  com.zhbit.controller  
 
访问
1 http://localhost:8123/api/doc.html 
 
就可以看到接口文档
其他依赖 1、aop切面
1 2 3 4 <dependency > 	<groupId > org.springframework.boot</groupId >  	<artifactId > spring-boot-starter-aop</artifactId >  </dependency > 
 
2、在主类上加上
1 @EnableAspectJAutoProxy(exposeProxy = true) 
 
加上之后可以获取当前类的代理对象
开启Spring AOP代理的创建 :使得Spring容器中的所有bean都能被AOP代理。 
 
3、springboot热部署
热部署环境配置,需要开发者工具-DevTools
devtools 可以实现页面热部署 (即页面修改后会立即生效,这个可以直接在 application.properties 文件中配置 spring.thymeleaf.cache=false 来实现),实现类文件热部署 (类文件修改后不会立即生效),实现对属性文件的热部署 。即 devtools 会监听 classpath 下的文件变动,并且会立即重启应用(发生在保存时机),注意:因为其采用的虚拟机机制,该项重启是很快的。配置了后在修改 java 文件后也就支持了热启动,不过这种方式是属于项目重启(速度比较快的项目重启),会清空 session 中的值,也就是如果有用户登陆的话,项目重启后需要重新登陆。
    默认情况下,/META-INF/maven,/META-INF/resources,/resources,/static,/templates,/public 这些文件夹下的文件修改不会使应用重启,但是会重新加载( devtools 内嵌了一个 LiveReload server,当资源发生改变时,浏览器刷新)
添加pom依赖
1 2 3 4 5 6 7 8 9 <dependency >     <groupId > org.springframework.boot</groupId >      <artifactId > spring-boot-devtools</artifactId >           <scope > runtime</scope >           <optional > true</optional >  </dependency > 
 
devtools配置
1 2 3 4 5 6 7 8 9 spring:      devtools:      restart:        enabled:  true                 additional-paths:  src/main/java               exclude:  WEB-INF/**  
 
依赖配置好之后,我们就开始CRUD了吗?
先写一写,通用的基础代码。
基础代码 自定义异常 自定义异常,对错误的问题细化,便于前端统一处理。
首先:自定义错误码
在Exception包下面新建一个ErrorCode枚举类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package  com.zhbit.exception;import  lombok.Getter;@Getter public  enum  ErrorCode  {    SUCCESS(0 , "ok" ),     PARAMS_ERROR(40000 , "请求参数错误" ),     NOT_LOGIN_ERROR(40100 , "未登录" ),     NO_AUTH_ERROR(40101 , "无权限" ),     NOT_FOUND_ERROR(40400 , "请求数据不存在" ),     FORBIDDEN_ERROR(40300 , "禁止访问" ),     SYSTEM_ERROR(50000 , "系统内部异常" ),     OPERATION_ERROR(50001 , "操作失败" );          private  final  int  code;          private  final  String message;     ErrorCode(int  code, String message) {         this .code = code;         this .message = message;     } } 
 
然后自定义业务异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 package  com.zhbit.exception;import  lombok.Getter;@Getter public  class  BusinessException  extends  RuntimeException  {         private  final  int  code;     public  BusinessException (int  code, String message)  {         super (message);         this .code = code;     }     public  BusinessException (ErrorCode errorCode)  {         super (errorCode.getMessage());         this .code = errorCode.getCode();     }     public  BusinessException (ErrorCode errorCode, String message)  {         super (message);         this .code = errorCode.getCode();     } } 
 
那么我们抛异常的时候是这样抛吗?还能不能更简单一点?
1 2 3 if ( 1  != 0 ){	new  BussinessException (ErrorCode.PARAMS_ERROR); } 
 
我们再自定义一个抛异常的工具类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 public  class  ThrowUtils  {         public  static  void  throwIf (boolean  condition, RuntimeException runtimeException)  {         if  (condition) {             throw  runtimeException;         }     }          public  static  void  throwIf (boolean  condition, ErrorCode errorCode)  {         throwIf(condition, new  BusinessException (errorCode));     }          public  static  void  throwIf (boolean  condition, ErrorCode errorCode, String message)  {         throwIf(condition, new  BusinessException (errorCode, message));     } } 
 
那我们以后抛异常是不是可以
1 ThrowUtils.throwIf(1  != 0 , new  Bussinession (ErrorCode.PARAMS_ERROR,"参数异常" )); 
 
通用响应类 那还有一个问题,我们返回的格式乱七八糟的,如何统一格式?这里的返回可能是String,User,或者是List(User)等等五花八门
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Controller public  class  testController  {    @GetMapping("/hello")      @ResponseBody      public  String hello ()  {         return  "hello" ;     }     @GetMapping("/hello")      @ResponseBody      public  int  hello ()  {         return  1 ;     } } 
 
那么我们可以封装统一的是响应结果类,便于前端统一获取这些信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 @Data public  class  BaseResponse <T> implements  Serializable  {    private  int  code;     private  T data;     private  String message;     public  BaseResponse (int  code, T data, String message)  {         this .code = code;         this .data = data;         this .message = message;     }     public  BaseResponse (int  code, T data)  {         this (code, data, "" );     }     public  BaseResponse (ErrorCode errorCode)  {         this (errorCode.getCode(), null , errorCode.getMessage());     } } 
 
以后我们相应的时候需要再装一层
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Controller public  class  testController  {    @GetMapping("/hello")      @ResponseBody      public  BaseResponse<String> hello ()  {         return  new  BaseResponse <>(200 ,"hello" );     }     @GetMapping("/hello")      @ResponseBody      public  BaseResponse<Integer> hello ()  {         return  new  BaseResponse <>(200 ,1 );;     } } 
 
但是还是不够方便,每次接口返回值的时候,都要手动new一个BaseResponse对象并传入参数,比较麻烦,我们可以新建一个工具类,提供成功调用和失败调用的方法,支持灵活的传参,简化调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public  class  ResultUtils  {         public  static  <T> BaseResponse<T> success (T data)  {         return  new  BaseResponse <>(0 , data, "ok" );     }          public  static  BaseResponse<?> error(ErrorCode errorCode) {         return  new  BaseResponse <>(errorCode);     }          public  static  BaseResponse<?> error(int  code, String message) {         return  new  BaseResponse <>(code, null , message);     }          public  static  BaseResponse<?> error(ErrorCode errorCode, String message) {         return  new  BaseResponse <>(errorCode.getCode(), null , message);     } } 
 
这个时候我们再写接口返回值的时候怎么写?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Controller public  class  testController  {    @GetMapping("/hello")      @ResponseBody      public  BaseResponse<String> hello ()  {         return  new  BaseResponse <>(200 ,"hello" );     }     @GetMapping("/hello")      @ResponseBody      public  BaseResponse<Integer> hello ()  {         return  ResultUtils.success("hello" );     } } 
 
现在又有一个问题,如果我们乱七八糟的调用,是不是会返回一些乱七八糟的数据给前端,这是不允许的吧。
1 [localhost:8888/api/doc.](http://localhost:8888/api/doc.) 
 
这时候需要一个全局异常处理的东西,利用AOP切面对全局业务异常和RuntimeException进行捕获
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @RestControllerAdvice @Slf4j public  class  GlobalExceptionHandler  {    @ExceptionHandler(BusinessException.class)      public  BaseResponse<?> businessExceptionHandler(BusinessException e) {         log.error("BusinessException" , e);         return  ResultUtils.error(e.getCode(), e.getMessage());     }     @ExceptionHandler(RuntimeException.class)      public  BaseResponse<?> runtimeExceptionHandler(RuntimeException e) {         log.error("RuntimeException" , e);         return  ResultUtils.error(ErrorCode.SYSTEM_ERROR, "系统错误" );     } } 
 
全局跨域请求配置 为什么要设置这个? 
因为浏览器有一个同源策略,拦截的是客户端发出去的请求成功后返回的数据。也就是说即使你发送请求成功了,服务器也相应了但是浏览器拒绝接受该数据。
比如我从一个前端的网址localhost:5173/user的网址访问localhost:8080/api/user/userLogin的服务,这两个是不同源的就会访问失败,所以需要设置这个全局跨域请求配置。
什么是同源 
一般我们输入浏览器的网址包含协议+域名+端口号+具体的目标文件路径,前三个只要有一个不同的就是不同源的。
没有同源策略会怎么样 
如果没有同源策略,任何网站都可以通过js脚本访问其他网站的数据。
例子:用户A正在使用浏览器访问他的网上银行(例如 https://bank.example),并且此时他还打开了另一个网页,该网页由攻击者控制(例如 http://evil.example)。
有同源策略:在当前存在同源策略的情况下,即使恶意网站尝试通过JavaScript脚本来读取或修改来自网上银行网站的数据,浏览器会阻止这种行为。例如,如果恶意网站试图发起一个AJAX请求到 https://bank.example/account/balance 来获取用户的账户余额信息,或者尝试通过JavaScript直接访问并篡改银行网页的DOM结构,这些操作都会被浏览器根据同源策略禁止,因为 http://evil.example 和 https://bank.example 属于不同的源(域名、协议或端口不同)。
弄个隐藏的表单,利用用户已登录的状态向银行网站提供非法转账请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 package  com.zhbit.config;import  org.springframework.context.annotation.Configuration;import  org.springframework.web.servlet.config.annotation.CorsRegistry;import  org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration public  class  CorsConfig  implements  WebMvcConfigurer  {    @Override      public  void  addCorsMappings (CorsRegistry registry)  {                  registry.addMapping("/**" )                          .allowCredentials(true )                          .allowedOriginPatterns("*" )             .allowedMethods("GET" , "POST" , "PUT" , "DELETE" , "OPTIONS" )             .allowedHeaders("*" )             .exposedHeaders("*" );     } } 
 
至此我们就基本初始化完成了。
前端项目初始化 Vue.js 本身并不是强制性的单页面应用框架,但它非常适用于构建单页面应用。单页面应用是指整个应用只有一个 HTML 页面,通过动态更新这个页面的内容来模拟多页面应用的效果。Vue.js 通过其核心的响应式系统和组件化开发模式,使得开发者可以轻松地创建复杂的单页面应用。
Vue 被描述为“渐进式”的原因在于它能够逐步集成到现有项目中,并且可以根据项目的需求灵活调整使用方式。
环境准备 前端 Node.js版本安装>=18.12,安装好npm前端包管理器。
可以跟着vue官网快速创建:快速上手 | Vue.js 
在终端输入命令:
 
npm会自动安装create-vue工具:
这里类似于springboot的快速模板创建一样。
然后我们直接安装Vue Router路由,Pinia全局状态管理等使用类库。
然后用 WebStorm 打开项目,先在终端执行 npm install 安装依赖,然后执行 npm run dev 能访问网页就成功了。
为了开发效率更高,你可能想关闭由于 ESLint 校验导致的编译错误,同样可以在开发工具中禁用 ESLint:
快速开发我们引入组件库
Ant Design Vue 组件库,这里可以参考Ant Design Vue-官方文档 我这里使用的版本是 4.2.6,
执行安装
1 npm i --save ant-design-vue@4.2.6 
 
改变主入口文件 main.ts,全局注册组件(为了方便):
1 2 3 4 5 6 7 8 9 10 11 import  App  from  './App.vue' import  router from  './router' import  Antd  from  "ant-design-vue" ;import  "ant-design-vue/dist/reset.css" ;const  app = createApp (App )app.use (Antd ); app.use (createPinia ()) app.use (router) app.mount ('#app' ) 
 
我们可以先尝试应用一下看看有没有引用成功。
开发规范 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <template>   <div id="xxPage">   </div> </template> <script setup lang="ts"> </script> <style scoped> #xxPage { } </style> 
 
修改页面基本信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 <!DOCTYPE html > <html  lang ="" >   <head >      <meta  charset ="UTF-8" >      <link  rel ="icon"  href ="/favicon.ico" >      <meta  name ="viewport"  content ="width=device-width, initial-scale=1.0" >      <title > 登录注册</title >         </head >    <body >      <div  id ="app" > </div >      <script  type ="module"  src ="/src/main.ts" > </script >    </body >  </html > 
 
还可以替换public目录下的默认的ico图标为自己的。
很多现成的ico制作网站,可以自己搜索在线制作ico图标 比特虫 - Bitbug.net 
开发全局通用布局 
基础布局结构 
 
在layouts目录下新建一个布局BasicLayout.vue,在App.vue全局页面入口文件中引入。
1 2 3 4 5 6 7 8 9 <template>   <div id="app">     <BasicLayout />   </div> </template> <script setup lang="ts"> import BasicLayout from "@/layouts/BasicLayout.vue"; </script> 
 
2、开发基础页面
利用组件库的布局组件,完成基础的布局
1 2 3 4 5 <a-layout>     <a-layout-header :style="headerStyle">Header</a-layout-header>     <a-layout-content :style="contentStyle">Content</a-layout-content>     <a-layout-footer :style="footerStyle">Footer</a-layout-footer> </a-layout> 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <template>   <div id="basicLayout">     <a-layout style="min-height: 100vh">       <a-layout-header>Header</a-layout-header>       <a-layout-content>Content</a-layout-content>       <a-layout-footer>Footer</a-layout-footer>     </a-layout>   </div> </template> <script setup lang="ts"> const msg = '开始!!!!' </script> <style scoped> </style> 
 
这里可以删除一些自动生成的样式文件,来保证我们写的不会被自动生成的css文件给污染,并且给布局文件设置100vh的高度铺满整个可见区域。
全局底部栏 通常用于展示版权信息
1 2 3 <a-layout-footer class="footer">     <a>登录注册 by oyy0v0</a> </a-layout-footer> 
 
样式
1 2 3 4 5 6 7 8 9 #basicLayout .footer  {   background : #efefef;   padding : 16px;   position : fixed;    bottom : 0 ;   left : 0 ;   right : 0 ;   text-align : center; } 
 
那作为一个单页面应用,如何动态的替换内容而不改变页面呢?
这里就使用到我们的Vue Router路由库,可以在router/index.ts配置路由,能够根据访问的页面地址找到不同的文件并进行加载渲染。
修改BasicLayout
修改BasicLayout内容部分的代码
1 2 3 <a-layout-content class ="content" >   <router-view  />  </a-layout-content> 
 
修改样式,要和底部栏保持一定的外边距,否则页面太小的时候底部内容会被遮住
1 2 3 4 5 6 7 8 <style scoped> #basicLayout .content  {   background : linear-gradient (to right, #fefefe, #fff);   margin-bottom : 28px;   padding : 20px; } </style> 
 
全局顶部栏 由于顶部栏的开发相对复杂,这里使用AntDesign的菜单组件来创建GlobalHeader全局顶部栏组件,组件统一放在components目录中 
先直接复制现成的组件示例代码到GlobalHeader中
然后在基础布局中引入顶部栏组件,引入完之后发现两边有黑框,然后修改一下样式。
1 2 3 4 5 6 #basicLayout .header  {   padding-inline : 20px;   margin-bottom : 16px;   color : unset;   background : white; } 
 
接下来修改GlobalHeader组件,完善更多内容。
1、先给菜单外套一层元素,用于整体样式控制
1 2 3 <div id="globalHeader">   <a-menu v-model:selectedKeys="current" mode="horizontal" :items="items" /> </div> 
 
2、根据我们的需求修改菜单配置,key为要跳转的URL路径
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <script lang="ts" setup> import { h, ref } from 'vue' import { HomeOutlined } from '@ant-design/icons-vue' import { MenuProps } from 'ant-design-vue' const current = ref<string[]>(['home']) const items = ref<MenuProps['items']>([   {     key: '/',     icon: () => h(HomeOutlined),     label: '主页',     title: '主页',   },   {     key: '/about',     label: '关于',     title: '关于',   }, ]) </script> 
 
3、完善全局顶部栏,左侧补充网站图标和标题。(考虑布局)
先把logo.png放在src/assets目录下,替换原本的默认logo
修改GlobalHeader代码,补充HTML
1 2 3 4 5 6 <RouterLink to="/">   <div class="title-bar">     <img class="logo" src="../assets/logo.png" alt="logo" />     <div class="title">登录注册</div>   </div> </RouterLink> 
 
其中RouterLink组件的作用是支持超链接跳转(不刷新页面)
补充css样式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <style scoped> .title-bar  {  display : flex;   align-items : center; } .title  {  color : black;   font-size : 18px ;   margin-left : 16px ; } .logo  {  height : 48px ; } </style> 
 
4、继续完善顶部栏,右侧展示当前用户的登录状态(暂时先用登录按钮替代)
1 2 3 <div class="user-login-status">   <a-button type="primary" href="/user/login">登录</a-button> </div> 
 
5、优化导航栏的布局,采用栅格组件的自适应布局(左中右结构,左侧右侧宽度固定,中间菜单栏自适应)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <a-row :wrap="false">   <a-col flex="200px">     <RouterLink to="/">       <div class="title-bar">         <img class="logo" src="../assets/logo.png" alt="logo" />         <div class="title">登录注册</div>       </div>     </RouterLink>   </a-col>   <a-col flex="auto">     <a-menu       v-model:selectedKeys="current"       mode="horizontal"       :items="items"     />   </a-col>   <a-col flex="120px">     <div class="user-login-status">       <a-button type="primary" href="/user/login">登录</a-button>     </div>   </a-col> </a-row> 
 
路由配置 现在我们点击导航栏,内容并没有发生任何变化,这是因为我们没有配置好路由。
目标:点击菜单项后,可以跳转到对应的页面;并且刷新页面后,对应的菜单自动高亮
1、修改路由配置
2、路由跳转
给GlobalHeader的菜单组件绑定跳转事件
1 2 3 4 5 6 7 8 9 import  { useRouter } from  "vue-router" ;const  router = useRouter ();const  doMenuClick  = ({ key }: { key: string  } ) => {  router.push ({     path : key,   }); }; 
 
修改HTML模板,绑定事件
1 2 3 4 5 6 7 <a-menu   v-model:selectedKeys="current"   mode="horizontal"   :items="items"   @click="doMenuClick" /> 
 
3、高亮同步
刷新页面后,发现当前菜单项并没有高亮,所以需要同步路由的更新到菜单项高亮。
原理:点击菜单式,组件已经通过v-model绑定了current变量实现了高亮,点击刷新页面的是偶需要获取当前URL路径,然后修改current变量的值,从而实现同步。
1 2 3 4 5 6 7 8 const  router = useRouter ();const  current = ref<string []>([]);router.afterEach ((to, from , next ) =>  {   current.value  = [to.path ]; }); 
 
请求 
引入Axios库
 
一般情况下,前端只负责界面展示和动效互动,尽量避免写复杂的逻辑。当需要获取数据时,通常是向后端提供的接口发送请求,然后后端执行操作,(比如查找用户)并响应数据给前端。
那么前端如何发送请求呢?最传统的方式AJAX技术,但是代码有些复杂,我们可以使用第三方的封装库,来简化发送请求的代码。
比如Axios.
1、请求工具库 安装Axios,Getting Started | Axios Docs 
 
前端要向后端发送很多请求
比如
1 2 3 localhost:8080/api/user/get localhost:8080/api/user/login localhost:8080/api/user/register 
 
那么如果这时候地址改变了,是不是全部都要重新改。
2、全局自定义请求 跟后端一样,在开发后端的时候,自己封装了一些自定义响应类,还有全局异常处理,那么前端也有类似的逻辑 。
参考Axios官方文档,编写请求配置文件,request.ts,包括全局接口请求地址、超时时间、自定义请求响应拦截器等等。
响应拦截器应用场景:我们需要对接口的通用相应进行统一处理,比如从request中取出data,或者根据code取集中处理错误,这样不用在每个接口请求中去写相同的逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 import  axios from  "axios" ;const  myAxios = axios.create ({  baseURL : "http://localhost:8123" ,   timeout : 10000 ,   withCredentials : true , }); myAxios.interceptors .request .use (   function  (config ) {          return  config;   },   function  (error ) {          return  Promise .reject (error);   } ); myAxios.interceptors .response .use (   function  (response ) {               console .log (response);     const  { data } = response;     console .log (data);          if  (data.code  === 40100 ) {              if  (         !response.request .responseURL .includes ("user/current" ) &&         !window .location .pathname .includes ("/user/login" )       ) {         window .location .href  = `/user/login?redirect=${window .location.href} ` ;       }     }     return  response;   },   function  (error ) {               return  Promise .reject (error);   } ); export  default  myAxios;
 
我们测试一下,在src目录下新建api/user.ts,存放所有有关用户的API接口。
1 2 3 4 5 6 7 8 import  myAxios from  '@/request' export  const  testHealth  = async  (params : any  ) => {  const  res = myAxios.request ({     url : "/api/health" ,     method : 'get' ,   }) } 
 
然后在主页面用一个按钮测试一下
1 <button @click="testHealth">测试按钮</button> 
 
然后按f12查看是否发送请求成功。
以后但凡要请求都要自己编辑一个,比较麻烦,然后就有了自动请求代码的工具了。
3、自动生成请求代码 如果采用传统的开发方式,针对每个请求都要单独编写代码,很麻烦。
推荐使用OpenAPI工具,直接自动生成即可,@umijs/openapi - npm 
按照官方文档的步骤,先安装
1 npm i --save-dev @umijs/openapi 
 
安装完之后,写配置文件
在项目的根目录 创建openapi.config.js,根据自己的需求定制生成的代码。
1 2 3 4 5 6 7 import  { generateService } from  '@umijs/openapi' generateService ({  requestLibPath : "import request from '@/request'" ,   schemaPath : 'http://localhost:8123/api/v2/api-docs' ,   serversPath : './src' , }) 
 
注意,要将schemaPath改为自己后端服务提供的Swagger接口文档的地址 
在package.json的script中添加”openapi”: “node openapi.config.js” 
执行即可生成请求代码,还包括TypeScript类型,以后每次后端接口变更的时候,只需要重新生成一遍就好,非常方便。
那么有人问了:那不用api自动生成是怎么请求的呢?待会我展示一下,手动和自动生成,但是效果是一样的。
vue+axios实现登录注册-CSDN博客 
全局状态管理 所有页面都需要共享的变量,而不是在某一个页面中。
举个例子:在登陆一个系统之后就不需要再次登录了,(每个页面都需要用)
Pinia是一个主流的状态管理库,相比较Vuex来说使用更简单,可以参考官方文档引入。
引入pinia 我们之前已经用脚手架引入了,无需手动引入了。
定义状态 那我们后端是怎么实现全局状态管理的?
定义一个常量类,每次需要使用的时候就调用这个常量类即可。
前端也一样。
那又有人问了,那我们写一个js文件不就好了,为什么还要用pinia?
因为假如说我们还需要有一些额外的功能,比如说加入变量的值变了之后我们同时更新页面展示的信息,因为js是写死的嘛,那要实现这个额外的功能我们就需要使用第三方的工具了。
在src/stores目录下定义user模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 import  { defineStore } from  "pinia" ;import  { ref } from  "vue" ;export  const  useLoginUserStore = defineStore ("loginUser" , () =>  {  const  loginUser = ref<any >({     userName : "未登录" ,   });   async  function  fetchLoginUser ( ) {                            }   function  setLoginUser (newLoginUser : any  ) {     loginUser.value  = newLoginUser;   }   return  { loginUser, setLoginUser, fetchLoginUser }; }); 
 
使用状态 可以直接使用store中导出的状态变量和函数
在首次进入页面时,一般我们会尝试获取登录用户信息,修改globalHeader.vue,编写远程获取数据代码
1 2 3 const loginUserStore = useLoginUserStore() loginUserStore.fetchLoginUser() 
 
修改全局顶部栏组件,应用用户信息来控制页面
1 2 3 4 5 6 7 8 9 <div class="user-login-status">   <div v-if="loginUserStore.loginUser.id">     {{ loginUserStore.loginUser.userName ?? '无名' }}   </div>   <div v-else>     <a-button type="primary" href="/user/login">登录</a-button>   </div> </div> 
 
可以在userStore中编写测试代码,测试用户状态的假登录
1 2 3 4 5 6 7 async  function  fetchLoginUser ( ) {     setTimeout (() =>  {     loginUser.value  = { userName : '测试用户' , id : 1  }   }, 3000 ) } 
 
页面开发流程 1、新建src/pages目录,用于存放所有的页面文件
在page目录下新建页面文件,将所有页面按照url层级进行创建,并且页面名称尽量做到“见名知意”。
其中,/user/login地址就对应了UserLoginPage.
2、每次新建页面的时候,需要在router/index.ts中配置路由,比如欢迎页的路由为
1 2 3 4 5 6 7 8 9 const  routes : Array <RouteRecordRaw > = [  {     path : "/" ,     name : "home" ,     component : HomeView ,   },   ... ] 
 
开发流程 一、需求分析 对于一个登录注册的用户模块,需要有什么功能?
用户注册
 
用户登录
 
获取当前登录用户
 
用户注销
 
用户权限控制
 
 
具体分析每个需求:
1、用户注册:用户可以通过输入账号,密码,确认密码进行注册
2、用户登录:用户可以通过输入账号和密码进行登录
3、获取当前登录用户:得到当前已经登录的用户信息(不用重复登录)
4、用户注销:用户可以退出登录状态
5、用户权限控制:用户可以分为普通用户和管理员,管理员拥有整个系统的最高权限,比如可以管理其他用户
二、方案设计 库表设计
用户登录流程
如何对用户权限进行控制?
库表设计 
库名:oyy_project
表名: user(用户表)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 create table  if not  exists  user (     id           bigint  auto_increment comment 'id'  primary key ,     userAccount  varchar (256 )                           not null  comment '账号' ,     userPassword varchar (512 )                           not null  comment '密码' ,     userName     varchar (256 )                           null  comment '用户昵称' ,     userAvatar   varchar (1024 )                          null  comment '用户头像' ,     userProfile  varchar (512 )                           null  comment '用户简介' ,     userRole     varchar (256 ) default  'user'             not null  comment '用户角色:user/admin' ,     editTime     datetime     default  CURRENT_TIMESTAMP  not null  comment '编辑时间' ,     createTime   datetime     default  CURRENT_TIMESTAMP  not null  comment '创建时间' ,     updateTime   datetime     default  CURRENT_TIMESTAMP  not null  on  update  CURRENT_TIMESTAMP  comment '更新时间' ,     isDelete     tinyint      default  0                  not null  comment '是否删除' ,     UNIQUE  KEY uk_userAccount (userAccount),     INDEX idx_userName (userName) ) comment '用户'  collate  =  utf8mb4_unicode_ci; 
 
给唯一值添加了唯一索引,比如账号userAccount,利用数据库天然防重复,还可以增加查询效率
给经常用于查询的字段添加索引,比如用户昵称userName,可以增加查询效率
用户登录流程 
在讲这个之前,先了解一下cookie和session。
为什么要用到cookie和session?
http是一个无状态,即每次关闭页面再打开,每次都要重新登录。我们之前开发的项目中每次都是需要一次登录才能使用功能。
那么如果我想要十天后或者一个星期后才需要再次登录,期间一直可以保持登录态呢?
那么每次http请求都自动携带数据给服务器的技术就是cookie了。
流程是这样的,浏览器向后端发送请求,后端会返回给浏览器一个set-cookie携带name和value,然后浏览器会保存为cookie以后每次都会携带这个数据发送http请求。
好了那么又有一个问题出现了?我要把我的账号和密码都写在cookie里面吗?是不是会不安全?
那么为了解决这个问题又有了一个东西叫session.
浏览器访问后端就是会话的开始,那么会话的结束时间是自己定义的。因此不同的网站对每个用户的会话都设定了时间以及唯一的SessionID。
好现在又有个问题?为什么有了用户名还要弄个SessionID?
我们回顾一下登录流程,第一次登录的时候发送账号和密码,然后服务器这个时候利用set-cookie把信息放到cookie里面但是放进去的不是用户名和密码了,而是放会话结束时间和SessionID这样我们的用户名和密码就不会直接记录在cookie里面了,以后的每次访问都会携带这个cookie来发送请求。
1、建立初始会话:前端与服务器建立连接后,服务器会为该客户创建一个初始的匿名Session,并将其状态保存下来。
这个Session的ID会作为唯一标识,返回给前端。
2、登录成功,更新会话信息:当前用户在前端输入正确的账号密码并提交到后端验证成功后,后端会更新该用户的Session,将该用户的登录信息(比如用户ID、用户名等)保存到与该Session关联的存储中。同时,服务器会生成一个Set-Cookie的相应头,指示前端保存该用户的Seesion ID。
3、前端保存Cookie:前端接受到后端的响应后,浏览器会自动根据Set-Cookie指令,将Session ID存储到浏览器的Cookie中,与该域名绑定。
4、带Cookie的后续请求:当前端再次向相同域名的服务器发送请求的时候,浏览器会自动在请求头中附带之前保存的Cookie,其中包含SessionID。
5、后端验证会话,服务器接收到请求后,从请求头提取SessionID,找到对应的Seesion数据
6、获取会话中存储的信息:后端通过该Session获取之前存储的用户信息(比如登录名,权限等),从而识别用户身份并执行相应的业务逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 sequenceDiagram     participant 前端     participant 后端     participant 浏览器     前端->>后端: 1. 建立连接     后端->>后端: 创建匿名Session     后端-->>前端: 2. 返回SessionID(Set-Cookie)     浏览器->>浏览器: 3. 存储SessionID到Cookie     前端->>后端: 4. 提交账号密码     后端->>后端: 5. 验证密码,更新Session     后端-->>前端: 6. 登录成功响应     前端->>后端: 7. 后续请求(带Cookie)     后端->>后端: 8. 提取SessionID,验证会话     后端->>后端: 9. 获取用户权限数据     后端-->>前端: 10. 执行业务逻辑并响应 
 
如何对用户权限进行控制? 
可以将接口分为4种权限
未登录也可以使用
登录才能使用
未登录也可以使用,但是登录用户能使用更多
仅管理员才能使用
传统的权限控制方法:在每个接口内单独编写逻辑:先获取到当前登录用户信息,然后判断用户的权限是否符合要求。
这种方法最灵活,但是会写很多重复的代码,而且其他开发者无法一眼得知接口所需要的权限。
权限校验一般会通过SpringAOP切面+自定义权限校验注解 实现统一的接口拦截和权限校验,如果有特殊的权限校验,再单独在接口中编码。
如果需要更复杂灵活的权限控制,可以引入Shiro/Spring  Security /Sa -Token等专门的权限管理框架
三、开始后端开发 首先执行SQL脚本创建数据库 
然后开发我们的数据访问层,一般包括实体类,Mybatis的Mapper类,XML类等。比起手动编写,这里使用MyBatisX代码生成插件,可以快速得到这些文件。
然后我们就能看到生成的代码了,把这些代码移动到项目的对应的位置
1、实体类修改
生成的代码也许不能完全满足我们的要求,比如数据库实体类,我们可以手动更改字段配置,制定主键策略和逻辑删除。
1、id默认是连续生成的,容易被爬虫抓取,所以我们修改策略为ASSIGN_ID雪花算法
2、数据删除的时候默认为彻底删除记录,如果数据出现误删,将难以恢复,所以采用逻辑删除–通过修改isDelete字段为1表示已经失效的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @TableName(value ="user") @Data public  class  User  implements  Serializable  {         @TableId(type = IdType.ASSIGN_ID)      private  Long id;                    @TableLogic      private  Integer isDelete; } 
 
2、定义枚举类
对于用户角色这样的值,数量有限个,可枚举的字段,最好定义一个枚举类,便于在项目中获取值,减少枚举值输入错误的情况。
在model.enums包下新建UserRoleEnum
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Getter public  enum  UserRoleEnum  {    USER("用户" , "user" ),     ADMIN("管理员" , "admin" );     private  final  String text;     private  final  String value;     UserRoleEnum(String text, String value) {         this .text = text;         this .value = value;     }          public  static  UserRoleEnum getEnumByValue (String value)  {         if  (ObjUtils.isEmpty(value)) {             return  null ;         }         for  (UserRoleEnum anEnum : UserRoleEnum.values()) {             if  (anEnum.value.equals(value)) {                 return  anEnum;             }         }         return  null ;     } } 
 
如果枚举值特别多,可以Map缓存所有枚举值来加速查找,而不是遍历列表。
用户注册开始开发 1、建立数据模型
在model.dto.user下新建用于接受请求参数的类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Data public  class  UserRegisterRequest  implements  Serializable  {    private  static  final  long  serialVersionUID  =  3191241716373120793L ;          private  String userAccount;          private  String userPassword;          private  String checkPassword; } 
 
dto表示接受对象,用于接受前端传来的值。
之后每开发一个接口都新建一个类。
为什么这样做?
1 2 3 4 5 @GetMapping("/health")     public  BaseResponse<String> health (String username,String password)  {                  return  ResultUtils.success("ok123" );     } 
 
这样不行吗?
首先是每个字段都这样写首先是麻烦,定义一个对象传递更容易,如果多个请求,有些字段你感觉可以复用,你就把一堆参数放在一个类里面,这样子前端开发的时候看到参数的时候会觉得特别多,而且也不知道哪些是有用的哪些是不必要的。
所以我们给每一个请求都定义一个请求类。
2、业务逻辑层 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 @Override public  long  userRegister (String userAccount, String userPassword, String checkPassword)  {         if  (StrUtil.hasBlank(userAccount, userPassword, checkPassword)) {         throw  new  BusinessException (ErrorCode.PARAMS_ERROR, "参数为空" );     }     if  (userAccount.length() < 4 ) {         throw  new  BusinessException (ErrorCode.PARAMS_ERROR, "用户账号过短" );     }     if  (userPassword.length() < 8  || checkPassword.length() < 8 ) {         throw  new  BusinessException (ErrorCode.PARAMS_ERROR, "用户密码过短" );     }     if  (!userPassword.equals(checkPassword)) {         throw  new  BusinessException (ErrorCode.PARAMS_ERROR, "两次输入的密码不一致" );     }          QueryWrapper<User> queryWrapper = new  QueryWrapper <>();     queryWrapper.eq("userAccount" , userAccount);     long  count  =  this .baseMapper.selectCount(queryWrapper);     if  (count > 0 ) {         throw  new  BusinessException (ErrorCode.PARAMS_ERROR, "账号重复" );     }          String  encryptPassword  =  getEncryptPassword(userPassword);          User  user  =  new  User ();     user.setUserAccount(userAccount);     user.setUserPassword(encryptPassword);     user.setUserName("无名" );     user.setUserRole(UserRoleEnum.USER.getValue());     boolean  saveResult  =  this .save(user);     if  (!saveResult) {         throw  new  BusinessException (ErrorCode.SYSTEM_ERROR, "注册失败,数据库错误" );     }     return  user.getId(); } 
 
条件构造器 | MyBatis-Plus 
我们之前编写了ThrowUtils这里应用一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 package  com.zhbit.service.impl;import  cn.hutool.core.util.StrUtil;import  com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;import  com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import  com.zhbit.exception.BusinessException;import  com.zhbit.exception.ErrorCode;import  com.zhbit.exception.ThrowUtils;import  com.zhbit.model.entity.User;import  com.zhbit.model.enums.UserRoleEnum;import  com.zhbit.service.UserService;import  com.zhbit.mapper.UserMapper;import  org.springframework.stereotype.Service;import  org.springframework.util.DigestUtils;@Service public  class  UserServiceImpl  extends  ServiceImpl <UserMapper, User>    implements  UserService {     @Override      public  long  userRegister (String userAccount, String userPassword, String checkPassword)  {                  ThrowUtils.throwIf(StrUtil.hasBlank(userAccount, userPassword, checkPassword), ErrorCode.PARAMS_ERROR, "参数为空" );         ThrowUtils.throwIf(!userPassword.equals(checkPassword), ErrorCode.PARAMS_ERROR, "两次输入的密码不一致" );         ThrowUtils.throwIf(userAccount.length() < 4 , ErrorCode.PARAMS_ERROR, "账号过短" );                  QueryWrapper<User> queryWrapper = new  QueryWrapper <>();         queryWrapper.eq("userAccount" , userAccount);         long  count  =  this .baseMapper.selectCount(queryWrapper);         ThrowUtils.throwIf(count > 0 , ErrorCode.PARAMS_ERROR, "账号重复" );                  String  encryptPassword  =  getEncryptPassword(userPassword);                  User  user  =  new  User ();         user.setUserAccount(userAccount);         user.setUserPassword(encryptPassword);         user.setUserName("无名" );         user.setUserRole(UserRoleEnum.USER.getValue());         boolean  saveResult  =  this .save(user);         ThrowUtils.throwIf(!saveResult, ErrorCode.SYSTEM_ERROR, "注册失败,数据库错误" );         return  user.getId();     }          @Override      public  String getEncryptPassword (String userPassword)  {                  final  String  SALT  =  "oyy0v0" ;         return  DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());     } } 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @RestController @RequestMapping("/user") public  class  UserController  {    @Resource      private  UserService userService;          @PostMapping("/register")      public  BaseResponse<Long> userRegister (@RequestBody  UserRegisterRequest userRegisterRequest)  {         ThrowUtils.throwIf(userRegisterRequest == null , ErrorCode.PARAMS_ERROR);         String  userAccount  =  userRegisterRequest.getUserAccount();         String  userPassword  =  userRegisterRequest.getUserPassword();         String  checkPassword  =  userRegisterRequest.getCheckPassword();         long  result  =  userService.userRegister(userAccount, userPassword, checkPassword);         return  ResultUtils.success(result);     } } 
 
用户登录 1、数据模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Data public  class  UserLoginRequest  implements  Serializable  {    private  static  final  long  serialVersionUID  =  3191241716373120793L ;          private  String userAccount;          private  String userPassword; } 
 
2、服务开发
1 2 3 4 5 6 7 8 9 10 LoginUserVO userLogin (String userAccount, String userPassword, HttpServletRequest request) ; 
 
为什么我这里加了个LoginUserVO类,因为这里是将数据库查到的所有信息都返回给了前端(包括密码),可能存在信息泄露的安全风险。因此,我们需要对返回结果进行脱敏处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 @Data public  class  LoginUserVO  implements  Serializable  {         private  Long id;          private  String userAccount;          private  String userName;          private  String userAvatar;          private  String userProfile;          private  String userRole;          private  Date createTime;          private  Date updateTime;     private  static  final  long  serialVersionUID  =  1L ; } 
 
实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 package  com.zhbit.service.impl;import  cn.hutool.core.util.StrUtil;import  com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;import  com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;import  com.zhbit.exception.BusinessException;import  com.zhbit.exception.ErrorCode;import  com.zhbit.exception.ThrowUtils;import  com.zhbit.model.entity.User;import  com.zhbit.model.enums.UserRoleEnum;import  com.zhbit.model.vo.LoginUserVO;import  com.zhbit.service.UserService;import  com.zhbit.mapper.UserMapper;import  lombok.extern.slf4j.Slf4j;import  org.springframework.beans.BeanUtils;import  org.springframework.stereotype.Service;import  org.springframework.util.DigestUtils;import  javax.servlet.http.HttpServletRequest;@Service @Slf4j public  class  UserServiceImpl  extends  ServiceImpl <UserMapper, User>    implements  UserService {     @Override      public  long  userRegister (String userAccount, String userPassword, String checkPassword)  {                  ThrowUtils.throwIf(StrUtil.hasBlank(userAccount, userPassword, checkPassword), ErrorCode.PARAMS_ERROR, "参数为空" );         ThrowUtils.throwIf(!userPassword.equals(checkPassword), ErrorCode.PARAMS_ERROR, "两次输入的密码不一致" );         ThrowUtils.throwIf(userAccount.length() < 4 , ErrorCode.PARAMS_ERROR, "账号过短" );                  QueryWrapper<User> queryWrapper = new  QueryWrapper <>();         queryWrapper.eq("userAccount" , userAccount);         long  count  =  this .baseMapper.selectCount(queryWrapper);         ThrowUtils.throwIf(count > 0 , ErrorCode.PARAMS_ERROR, "账号重复" );                  String  encryptPassword  =  getEncryptPassword(userPassword);                  User  user  =  new  User ();         user.setUserAccount(userAccount);         user.setUserPassword(encryptPassword);         user.setUserName("无名" );         user.setUserRole(UserRoleEnum.USER.getValue());         boolean  saveResult  =  this .save(user);         ThrowUtils.throwIf(!saveResult, ErrorCode.SYSTEM_ERROR, "注册失败,数据库错误" );         return  user.getId();     }          @Override      public  String getEncryptPassword (String userPassword)  {                  final  String  SALT  =  "oyy0v0" ;         return  DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());     }          @Override      public  LoginUserVO userLogin (String userAccount, String userPassword, HttpServletRequest request)  {                  if  (StrUtil.hasBlank(userAccount, userPassword)) {             throw  new  BusinessException (ErrorCode.PARAMS_ERROR, "参数为空" );         }         if  (userAccount.length() < 4 ) {             throw  new  BusinessException (ErrorCode.PARAMS_ERROR, "账号错误" );         }         if  (userPassword.length() < 8 ) {             throw  new  BusinessException (ErrorCode.PARAMS_ERROR, "密码错误" );         }                  String  encryptPassword  =  getEncryptPassword(userPassword);                  QueryWrapper<User> queryWrapper = new  QueryWrapper <>();         queryWrapper.eq("userAccount" , userAccount);         queryWrapper.eq("userPassword" , encryptPassword);         User  user  =  this .baseMapper.selectOne(queryWrapper);                  if  (user == null ) {             log.info("user login failed, userAccount cannot match userPassword" );             throw  new  BusinessException (ErrorCode.PARAMS_ERROR, "用户不存在或密码错误" );         }                  request.getSession().setAttribute("USER_LOGIN_STATE" , user);         return  this .getLoginUserVO(user);     }          @Override      public  LoginUserVO getLoginUserVO (User user)  {         LoginUserVO  loginUserVO  =  new  LoginUserVO ();         BeanUtils.copyProperties(user, loginUserVO);         return  loginUserVO;     } } 
 
小细节:如果在记录用户登录态的时候,程序员不小心少打一个代码,是不是就取不到这个用户登录态了
所以我们把这个key值提取为常量,便于后续获取。
可以把Session理解为一个Map,给Map设置key和value,每个不同的SessionID对应的Session存储的都是不同的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public  interface  UserConstant  {         String  USER_LOGIN_STATE  =  "user_login" ;               String  DEFAULT_ROLE  =  "user" ;          String  ADMIN_ROLE  =  "admin" ;           } 
 
开发接口 
1 2 3 4 5 6 7 8 9 @PostMapping("/login") public  BaseResponse<LoginUserVO> userLogin (@RequestBody  UserLoginRequest userLoginRequest, HttpServletRequest request)  {    ThrowUtils.throwIf(userLoginRequest == null , ErrorCode.PARAMS_ERROR);     String  userAccount  =  userLoginRequest.getUserAccount();     String  userPassword  =  userLoginRequest.getUserPassword();     LoginUserVO  loginUserVO  =  userService.userLogin(userAccount, userPassword, request);     return  ResultUtils.success(loginUserVO); } 
 
获取当前登录用户 可以从request请求对象对应的Session中直接获取到之前保存的登录用户的信息,无需其他请求参数。
1、服务开发
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Override public  User getLoginUser (HttpServletRequest request)  {         Object  userObj  =  request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);     User  currentUser  =  (User) userObj;     if  (currentUser == null  || currentUser.getId() == null ) {         throw  new  BusinessException (ErrorCode.NOT_LOGIN);     }          long  userId  =  currentUser.getId();     currentUser = this .getById(userId);     if  (currentUser == null ) {         throw  new  BusinessException (ErrorCode.NOT_LOGIN);     }     return  currentUser; } 
 
为什么这里需要再查一次数据库,这里是为了保证获取到的数据始终是最新的,先从Session中获取登录用户的id,然后再从数据库中查询最新的结果。
接口开发 
1 2 3 4 5 6 7 8     @GetMapping("/get/login")      public  BaseResponse<LoginUserVO> getLoginUser (HttpServletRequest request)  {         User  loginUser  =  userService.getLoginUser(request);         return  ResultUtils.success(userService.getLoginUserVO(loginUser));     } 
 
用户注销 可以从request请求对象中把Session中直接获取到之前保存的登录用户信息删除来完成注销。
1、接口
1 2 3 4 5 6 7 8 boolean  userLogout (HttpServletRequest request) ;
 
2、实现
1 2 3 4 5 6 7 8 9 10 11 12 @Override public  boolean  userLogout (HttpServletRequest request)  {         Object  userObj  =  request.getSession().getAttribute(USER_LOGIN_STATE);     if  (userObj == null ) {         throw  new  BusinessException (ErrorCode.OPERATION_ERROR, "未登录" );     }          request.getSession().removeAttribute(USER_LOGIN_STATE);     return  true ; } 
 
3、接口开发
1 2 3 4 5 6 7 @PostMapping("/logout") public  BaseResponse<Boolean> userLogout (HttpServletRequest request)  {    ThrowUtils.throwIf(request == null , ErrorCode.PARAMS_ERROR);     boolean  result  =  userService.userLogout(request);     return  ResultUtils.success(result); } 
 
用户权限控制 1、权限校验注解
1 2 3 4 5 6 7 8 9 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public  @interface  AuthCheck {         String mustRole ()  default  "" ; } 
 
2、权限校验切面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 @Aspect @Component public  class  AuthInterceptor  {    @Resource      private  UserService userService;          @Around("@annotation(authCheck)")      public  Object doInterceptor (ProceedingJoinPoint joinPoint, AuthCheck authCheck)  throws  Throwable {         String  mustRole  =  authCheck.mustRole();         RequestAttributes  requestAttributes  =  RequestContextHolder.currentRequestAttributes();         HttpServletRequest  request  =  ((ServletRequestAttributes) requestAttributes).getRequest();                  User  loginUser  =  userService.getLoginUser(request);         UserRoleEnum  mustRoleEnum  =  UserRoleEnum.getEnumByValue(mustRole);                  if  (mustRoleEnum == null ) {             return  joinPoint.proceed();         }                           UserRoleEnum  userRoleEnum  =  UserRoleEnum.getEnumByValue(loginUser.getUserRole());                  if  (userRoleEnum == null ) {             throw  new  BusinessException (ErrorCode.NO_AUTH_ERROR);         }                  if  (UserRoleEnum.ADMIN.equals(mustRoleEnum) && !UserRoleEnum.ADMIN.equals(userRoleEnum)) {             throw  new  BusinessException (ErrorCode.NO_AUTH_ERROR);         }                  return  joinPoint.proceed();     } } 
 
分三种情况:不需要登录就能使用的接口,不需要使用该注解,添加了这个注解就必须要登录,设置了mustRole为管理员,只有管理员才能使用。
四、前端开发 新建页面和路由
修改router/index.ts的路由配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 import  { createRouter, createWebHistory } from  'vue-router' import  HomeView  from  '../views/HomeView.vue' import  HomePage  from  '@/pages/HomePage.vue' import  UserRegisterPage  from  '@/pages/user/UserRegisterPage.vue' import  UserLoginPage  from  '@/pages/user/UserLoginPage.vue' const  router = createRouter ({  history : createWebHistory (import .meta .env .BASE_URL ),   routes : [     {       path : '/' ,       name : '主页' ,       component : HomePage ,     },     {       path : '/user/login' ,       name : '用户登录' ,       component : UserLoginPage ,     },     {       path : '/user/register' ,       name : '用户注册' ,       component : UserRegisterPage ,     },   ], }) export  default  router
 
记得执行一下openapi命令生成接口对应的请求代码,每次请求后端改动的时候都需要这么做。
获取当前登录用户 之前已经创建了前端登录用户的状态管理文件useLoginUserStore.ts。现在后端提供了获取当前登录页用户的接口,直接修改fetchLoginUser函数即可:
1 2 3 4 5 6 async  function  fetchLoginUser ( ) {  const  res = await  getLoginUserUsingGet ()   if  (res.data .code  === 0  && res.data .data ) {     loginUser.value  = res.data .data    } } 
 
由于之前已经生成了代码,可以看到已经帮我们生成了后端的请求对象比如LoginUserVO
1 2 3 const  loginUser = ref<API .LoginUserVO >({    userName : "未登录" ,   }); 
 
这样我们修改一下类型。
开发用户登录页面 搜索组件库,找到表单组件,然后无脑复制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 import React from 'react'; import type { FormProps } from 'antd'; import { Button, Checkbox, Form, Input } from 'antd'; type FieldType = {   username?: string;   password?: string;   remember?: string; }; const onFinish: FormProps<FieldType>['onFinish'] = (values) => {   console.log('Success:', values); }; const onFinishFailed: FormProps<FieldType>['onFinishFailed'] = (errorInfo) => {   console.log('Failed:', errorInfo); }; const App: React.FC = () => (   <Form     name="basic"     labelCol={{ span: 8 }}     wrapperCol={{ span: 16 }}     style={{ maxWidth: 600 }}     initialValues={{ remember: true }}     onFinish={onFinish}     onFinishFailed={onFinishFailed}     autoComplete="off"   >     <Form.Item<FieldType>       label="Username"       name="username"       rules={[{ required: true, message: 'Please input your username!' }]}     >       <Input />     </Form.Item>     <Form.Item<FieldType>       label="Password"       name="password"       rules={[{ required: true, message: 'Please input your password!' }]}     >       <Input.Password />     </Form.Item>     <Form.Item<FieldType> name="remember" valuePropName="checked" label={null}>       <Checkbox>Remember me</Checkbox>     </Form.Item>     <Form.Item label={null}>       <Button type="primary" htmlType="submit">         Submit       </Button>     </Form.Item>   </Form> ); export default App; 
 
然后进行修改
最终效果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 <template>   <div id="userLoginPage">     <h2 class="title">登录注册</h2>     <div class="desc">登录注册</div>     <a-form :model="formState" name="basic" autocomplete="off" @finish="handleSubmit">       <a-form-item name="userAccount" :rules="[{ required: true, message: '请输入账号' }]">         <a-input v-model:value="formState.userAccount" placeholder="请输入账号" />       </a-form-item>       <a-form-item         name="userPassword"         :rules="[           { required: true, message: '请输入密码' },           { min: 8, message: '密码不能小于 8 位' },         ]"       >         <a-input-password v-model:value="formState.userPassword" placeholder="请输入密码" />       </a-form-item>       <div class="tips">         没有账号?         <RouterLink to="/user/register">去注册</RouterLink>       </div>       <a-form-item>         <a-button type="primary" html-type="submit" style="width: 100%">登录</a-button>       </a-form-item>     </a-form>   </div> </template> <script setup lang="ts"> import { reactive } from 'vue' import { useRouter } from 'vue-router' import { useLoginUserStore } from '@/stores/useLoginUserStore' import { userLoginUsingPost } from '@/api/userController' import { message } from 'ant-design-vue' //定义一个响应式变量来接受表单输入的值 const formState = reactive<API.UserLoginRequest>({   userAccount: '',   userPassword: '', }) const router = useRouter() const loginUserStore = useLoginUserStore() /**  * 提交表单  * @param values  */ const handleSubmit = async (values: any) => {   const res = await userLoginUsingPost(values)   // 登录成功,把登录态保存到全局状态中   if (res.data.code === 0 && res.data.data) {     await loginUserStore.fetchLoginUser()     message.success('登录成功')     router.push({       path: '/',       replace: true,     })   } else {     message.error('登录失败,' + res.data.message)   } } </script> <style scoped> #userLoginPage {   max-width: 360px;   margin: 0 auto; } .title {   text-align: center;   margin-bottom: 16px; } .desc {   text-align: center;   color: #bbb;   margin-bottom: 16px; } .tips {   margin-bottom: 16px;   color: #bbb;   font-size: 13px;   text-align: right; } </style> 
 
开发用户注册页面 跟上面一样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 <template>   <div id="userRegisterPage">     <h2 class="title">登录注册</h2>     <div class="desc">登录注册</div>     <a-form       :model="formState"       name="basic"       label-align="left"       autocomplete="off"       @finish="handleSubmit"     >       <a-form-item name="userAccount" :rules="[{ required: true, message: '请输入账号' }]">         <a-input v-model:value="formState.userAccount" placeholder="请输入账号" />       </a-form-item>       <a-form-item         name="userPassword"         :rules="[           { required: true, message: '请输入密码' },           { min: 8, message: '密码不能小于 8 位' },         ]"       >         <a-input-password v-model:value="formState.userPassword" placeholder="请输入密码" />       </a-form-item>       <a-form-item         name="checkPassword"         :rules="[           { required: true, message: '请输入确认密码' },           { min: 8, message: '确认密码不能小于 8 位' },         ]"       >         <a-input-password v-model:value="formState.checkPassword" placeholder="请输入确认密码" />       </a-form-item>       <div class="tips">         已有账号?         <RouterLink to="/user/login">去登录</RouterLink>       </div>       <a-form-item>         <a-button type="primary" html-type="submit" style="width: 100%">注册</a-button>       </a-form-item>     </a-form>   </div> </template> 
 
定义表单信息变量
1 2 3 4 5 6 const formState = reactive<API.UserRegisterRequest>({   userAccount: '',   userPassword: '',   checkPassword: '', }) 
 
编写表单提交函数,可以增加一些前端校验,并且在注册成功后跳转到用户登录页。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 const router = useRouter() /**  * 提交表单  * @param values  */ const handleSubmit = async (values: any) => {   // 判断两次输入的密码是否一致   if (formState.userPassword !== formState.checkPassword) {     message.error('二次输入的密码不一致')     return   }   const res = await userRegisterUsingPost(values)   // 注册成功,跳转到登录页面   if (res.data.code === 0 && res.data.data) {     message.success('注册成功')     router.push({       path: '/user/login',       replace: true,     })   } else {     message.error('注册失败,' + res.data.message)   } } 
 
用户注销 一般鼠标悬浮在右上角用户头像时,会展示包含用户注销(退出登录功能)的下拉菜单
先编写页面结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <div v-if="loginUserStore.loginUser.id">   <a-dropdown>     <ASpace>       <a-avatar :src="loginUserStore.loginUser.userAvatar" />       {{ loginUserStore.loginUser.userName ?? '无名' }}     </ASpace>     <template #overlay>       <a-menu>         <a-menu-item @click="doLogout">           <LogoutOutlined />           退出登录         </a-menu-item>       </a-menu>     </template>   </a-dropdown> </div> 
 
编写用户注销事件函数,退出登录后跳转到登录页。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // 用户注销 const doLogout = async () => {   const res = await userLogoutUsingPost()   console.log(res)   if (res.data.code === 0) {     loginUserStore.setLoginUser({       userName: '未登录',     })     message.success('退出登录成功')     await router.push('/user/login')   } else {     message.error('退出登录失败,' + res.data.message)   } } 
 
最终版本的顶部栏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 <template>   <a-row :wrap="false">     <a-col flex="200px">       <RouterLink to="/">         <div class="title-bar">           <img class="logo" src="../assets/logo.png" alt="logo" />           <div class="title">登录注册</div>         </div>       </RouterLink>     </a-col>     <a-col flex="auto">       <a-menu         v-model:selectedKeys="current"         mode="horizontal"         :items="items"       />     </a-col>     <a-col flex="120px">       <div class="user-login-status">         <div v-if="loginUserStore.loginUser.id">           <a-dropdown>             <ASpace>               <a-avatar :src="loginUserStore.loginUser.userAvatar" />               {{ loginUserStore.loginUser.userName ?? '无名' }}             </ASpace>             <template #overlay>               <a-menu>                 <a-menu-item @click="doLogout">                   <LogoutOutlined />                   退出登录                 </a-menu-item>               </a-menu>             </template>           </a-dropdown>         </div>         <div v-else>           <a-button type="primary" href="/user/login">登录</a-button>         </div>       </div>     </a-col>   </a-row> </template> <script lang="ts" setup> import { h, ref } from 'vue' import { HomeOutlined ,LogoutOutlined} from '@ant-design/icons-vue' import { type MenuProps, message } from 'ant-design-vue' import { useRouter } from 'vue-router' import { useLoginUserStore } from '@/stores/useLoginUserStore' import { userLogoutUsingPost } from '@/api/userController' const items = ref<MenuProps['items']>([   {     key: '/',     icon: () => h(HomeOutlined),     label: '主页',     title: '主页'   },   {     key: '/about',     label: '关于',     title: '关于'   } ]); //引入登录用户仓库 const loginUserStore = useLoginUserStore() const router = useRouter(); //路由跳转事件 const doMenuClick = ({ key }) => {   router.push({     path: key   }) } //当前要高亮的菜单 const current = ref<string[]>([]) //监听路由变化更新高亮菜单 router.afterEach((to,from,next) => {   current.value = [to.path] }) // 用户注销 const doLogout = async () => {   const res = await userLogoutUsingPost()   console.log(res)   if (res.data.code === 0) {     loginUserStore.setLoginUser({       userName: '未登录',     })     message.success('退出登录成功')     await router.push('/user/login')   } else {     message.error('退出登录失败,' + res.data.message)   } } </script> <style scoped> .title-bar {   display: flex;   align-items: center; } .title {   color: black;   font-size: 18px;   margin-left: 16px; } .logo {   height: 48px; } </style>