[Spring] SpringBoot统一功能处理与图书管理系统

news2024/11/16 19:32:25

🌸个人主页:https://blog.csdn.net/2301_80050796?spm=1000.2115.3001.5343
🏵️热门专栏:
🧊 Java基本语法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12615970.html?spm=1001.2014.3001.5482
🍕 Collection与数据结构 (92平均质量分)https://blog.csdn.net/2301_80050796/category_12621348.html?spm=1001.2014.3001.5482
🧀线程与网络(96平均质量分) https://blog.csdn.net/2301_80050796/category_12643370.html?spm=1001.2014.3001.5482
🍭MySql数据库(93平均质量分)https://blog.csdn.net/2301_80050796/category_12629890.html?spm=1001.2014.3001.5482
🍬算法(97平均质量分)https://blog.csdn.net/2301_80050796/category_12676091.html?spm=1001.2014.3001.5482
🍃 Spring(97平均质量分)https://blog.csdn.net/2301_80050796/category_12724152.html?spm=1001.2014.3001.5482
感谢点赞与关注~~~
在这里插入图片描述

目录

  • 1. 拦截器
    • 1.1 概念
    • 1.2 拦截器的基本使用
    • 1.3 拦截器的执行流程
    • 1.5 设计模式: 适配器模式
      • 1.5.1 定义
  • 2. 统一数据返回格式
  • 3. 统一异常处理
  • 4. 图书管理系统(应用)
    • 4.1 准备数据库
    • 4.2 创建实体类
    • 4.3 用户登录接口
    • 4.4 添加图书接口
    • 4.5 图书列表与翻页请求
    • 4.6 修改图书接口
    • 4.7 删除图书接口
    • 4.8 批量删除接口
    • 4.9 强制登录

1. 拦截器

1.1 概念

首先什么是拦截器?拦截器主要用来拦截用户请求,在指定方法前后,根据业务需要执行的预先设定的代码.也就是允许开发人员预先设定一些代码,在用户请求响应前后执行.也可以在用户请求之前组织其执行.
在这里插入图片描述
就像小区门口的保安一样,会对外面的形形色色的人进行拦截一样.

1.2 拦截器的基本使用

拦截器的使用分为两步:

  1. 定义拦截器
  2. 注册配置拦截器.

首先,我们设计一个自定义拦截器.这个自定义拦截器需要实现HandlerInterceptor接口,并重写其中的所有方法.

@Component
@Slf4j
public class Interceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        log.info("在执行目标方法之前");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("在执行目标方法后");
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("在视图渲染完成之后");
    }
}
  • 第一个preHandle方法就是在目标方法执行前执行的代码,如果返回true,继续执行后面的操作,如果返回false,拦截当前请求.
  • 第二个postHandle表示的是在目标方法执行后要执行的操作.
  • 第三个afterCompletion表示的是在视图渲染完成之后执行的操作.第三个方法暂时不需要了解.

定义拦截器就好像定义一个保安要做什么,比如在一些奇奇怪怪的人要进来之前,保安需要在小区门口将其拦截.在业主回家的时候,保安会放行通过,并来上一句"欢迎业主回家".

其次,我们需要注册配置拦截器.需要在一个类中实现WebMvcConfigurer接口,并重写其中的addInterceptor方法.

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private Interceptor interceptor;
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(interceptor)
                .addPathPatterns("/**");
    }
}

在重写的方法中,我们需要为形式参数添加对应的拦截器,之后再在拦截器的后面配置要拦截的路径,这里的/**代表的是拦截所有路径下的所有请求.
除此之外,还有一些常见的拦截路径设置.

拦截路径含义举例
/*⼀级路径 能匹配/user,/book,/login,不能匹配/user/login
/**任意级路径 能匹配/user,/user/login,/user/reg
/book/*/book下的⼀级路径 能匹配/book/addBook,不能匹配/book/addBook/1,/book
/book/**/book下的任意级路径 能匹配/book,/book/addBook,/book/addBook/2,不能匹配/user/login

[注意] 以上拦截的规则不仅仅可以是文件路径,还可以是接口的URL.

1.3 拦截器的执行流程

在这里插入图片描述

  1. 添加拦截器后,执行Controller的方法之前,请求会先被拦截器拦截住.执行 preHandle() 方法,这个方法需要返回⼀个布尔类型的值.如果返回true,就表示放行本次操作,继续访问controller中的方法.如果返回false,则不会放行(controller中的⽅法也不会执行).
  2. controller当中的方法执行完毕后,再回过来执行 postHandle() 这个方法以及afterCompletion() 方法,执行完毕之后,最终给浏览器响应数据.

1.5 设计模式: 适配器模式

上面拦截器的实现原理中,源码中的HandlerAdapter在SpringMVC中使用的就是适配器模式.

1.5.1 定义

适配器模式,也叫包装器模式,是将一个类的接口,转换为客户期望的接口,适配器让原本不兼容的类之间可以合作.
比如下面两个接口,本身就是不兼容的(比如参数类型不一样,参数个数不一样等等)
在这里插入图片描述
但是我们可以通过适配器使其兼容.
在这里插入图片描述
在我们日常生活中,适配器的例子也非常常见.比如转换插头:这是我们出国旅行前必备的一个装备.
在这里插入图片描述
适配器模式的角色:
• Target:目标接口(可以是抽象类或接口),客户希望直接用的接口
• Adaptee:适配者,但是与Target不兼容
• Adapter:适配器类,此模式的核心.通过继承或者引用适配者的对象,把适配者转为目标接口
• client:需要使用适配器的对象
场景: 我们在前面学习日志的时候,slf4j就使用了适配器模式,slf4j提供的一系列打印日志的API,都是底层调用log4j或者是logback来实现的.我们在调用的时候,只需要调用slf4j的API即可.

//slf4j的接口
public interface Slf4j {
    void print(String s);
}
//底层打印日志的log4j
public class Log4j {
    public void print(String s){
        System.out.println(s);
    }
}
//适配器
public class Slf4jLog4jAdaptor implements Slf4j{
    private Log4j log4j;

    public Slf4jLog4jAdaptor(Log4j log4j) {
        this.log4j = log4j;
    }

    @Override
    public void print(String s) {
        log4j.print(s);//最核心的是这一步,调用log4j的打印方法,
        //在外部调用的时候,调用的是适配器的print方法.
    }

    public static void main(String[] args) {
        Slf4j slf4j = new Slf4jLog4jAdaptor(new Log4j());//调用方->适配器->底层log4j
        slf4j.print("打印日志");
    }
}

适配器模式的应用场景:
⼀般来说,适配器模式可以看作⼀种"补偿模式",用来补救设计上的缺陷.应用这种模式算是"无奈之举",如果在设计初期,我们就能协调规避接口不兼容的问题,就不需要使用适配器模式了.

2. 统一数据返回格式

在一些场景下,我们接口的返回结果的格式是一样的,这时候我们就需要对接口的返回格式的几种不同的类型进行封装,之后在统一返回数据格式中对相应结果进行一些列逻辑判断与处理,最终由统一数据格式返回决定返回该格式中那种类型的数据.

统一数据返回格式用@ControllerAdvice和实现ResponseBodyAdvice接口来完成.

@ControllerAdvice
public class ResponseBody implements ResponseBodyAdvice {
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        return null;
    }

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }
}
  • support方法: 判断是否需要执行beforeBodyWrite方法,true为执行,false不执行.通过该方法可以选择哪些类或者那些方法的Response需要进行处理,那些不需要处理.
    我们在获取类名和方法名的时候,需要用到反射机制,从returnType中获取:
//获取执⾏的类 
Class<?> declaringClass = returnType.getMethod().getDeclaringClass();
//获取执⾏的⽅法 
Method method = returnType.getMethod();
  • deforeBodyWrite方法: 对Response方法进行具体操作处理,决定统一返回结果的内容.

3. 统一异常处理

统一异常处理使用的是@ControllerAdvice +@ExceptionHandler 来实现的,@ControllerAdvice 表示控制器通知类,@ExceptionHandler 是异常处理器.
具体代码如下:

@RestControllerAdvice
public class ErrorAdvice {
    @ExceptionHandler
    public String handler(Exception e){
        return e.getMessage();
    }
}

当然,我们也可以针对不同类型的异常返回不同的结果:

@RestControllerAdvice
public class ErrorAdvice {
    @ExceptionHandler
    public String handler(Exception e){
        return e.getMessage();
    }
    @ExceptionHandler
    public String handler1(NullPointerException e){
        return "发生空指针异常"+e.getMessage();
    }
    @ExceptionHandler
    public String handler2(IndexOutOfBoundsException e){
        return "发生数组越界异常"+e.getMessage();
    }
}

[注意] 在异常类型进行匹配的时候,先检查带有@ExceptionHandler注解的有没有与之匹配的异常类型,如果没有,依次向父类检查,直到匹配到对应的父类为止.

4. 图书管理系统(应用)

4.1 准备数据库

首先,我们需要为这个系统准备数据库和数据表,并为数据库创建一些数据.
数据库表的设计与业务逻辑有关系,数据库表一般分为两种:实体表和关系表.
图书管理系统相对比较简单,只有两个实体:用户和图书,而且用户和图书之间没有关联关系.
关于表的字段的设计,也与具体的业务逻辑有关系,但是三个字段是必不可少的,创建时间,更新时间,id.
用户表有用户名,密码,删除标志,图书表有图书名称,作者,售价,图书状态(可借阅,不可借阅,删除),库存数量,出版社.

  • 创建数据库表,并添加对应数据
DROP DATABASE IF EXISTS book_test;
CREATE DATABASE book_test DEFAULT CHARACTER SET utf8mb4;
-- ⽤⼾表 
DROP TABLE IF EXISTS user_info;
CREATE TABLE user_info (
 `id` INT NOT NULL AUTO_INCREMENT,
 `user_name` VARCHAR ( 128 ) NOT NULL,
 `password` VARCHAR ( 128 ) NOT NULL,
 `delete_flag` TINYINT ( 4 ) NULL DEFAULT 0,
 `create_time` DATETIME DEFAULT now(),
 `update_time` DATETIME DEFAULT now() ON UPDATE now(),
 PRIMARY KEY ( `id` ),
UNIQUE INDEX `user_name_UNIQUE` ( `user_name` ASC )) ENGINE = INNODB DEFAULT 
CHARACTER 
SET = utf8mb4 COMMENT = '⽤⼾表';
-- 图书表 
DROP TABLE IF EXISTS book_info;
CREATE TABLE `book_info` (
 `id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
 `book_name` VARCHAR ( 127 ) NOT NULL,
 `author` VARCHAR ( 127 ) NOT NULL,
 `count` INT ( 11 ) NOT NULL,
 `price` DECIMAL (7,2 ) NOT NULL,
 `publish` VARCHAR ( 256 ) NOT NULL,
 `status` TINYINT ( 4 ) DEFAULT 1 COMMENT '0-⽆效, 1-正常, 2-不允许借阅',
 `create_time` DATETIME DEFAULT now(),
 `update_time` DATETIME DEFAULT now() ON UPDATE now(),
PRIMARY KEY ( `id` ) 
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;
-- 初始化数据 
INSERT INTO user_info ( user_name, PASSWORD ) VALUES ( "admin", "admin" );
INSERT INTO user_info ( user_name, PASSWORD ) VALUES ( "zhangsan", "123456" );
-- 初始化图书数据 
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('活
着', '余华', 29, 22.00, '北京⽂艺出版社');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('平凡的
世界', '路遥', 5, 98.56, '北京⼗⽉⽂艺出版社');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('三
体', '刘慈欣', 9, 102.67, '重庆出版社');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('⾦字塔
原理', '⻨肯锡', 16, 178.00, '⺠主与建设出版社');
  • 创建项目,引入MySql驱动依赖和MyBatis依赖.
<dependency>
	 <groupId>org.mybatis.spring.boot</groupId>
	 <artifactId>mybatis-spring-boot-starter</artifactId>
	 <version>3.0.3</version>
</dependency>
<dependency>
	 <groupId>com.mysql</groupId>
	 <artifactId>mysql-connector-j</artifactId>
	 <scope>runtime</scope>
</dependency>
  • 配置数据库信息,数据库日志打印,驼峰转换,MyBatis xml文件路径.
spring:
  application:
    name: books
# 数据库连接配置
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/books?characterEncoding=utf8&useSSL=false
    username: root
    password: qwe123524
    driver-class-name: com.mysql.cj.jdbc.Driver
# 配置 mybatis xml 的⽂件路径,在 resources/mapper 创建所有表的 xml ⽂件
mybatis:
  mapper-locations: classpath:mapper/**Mapper.xml
  configuration:
    map-underscore-to-camel-case: true #配置驼峰⾃动转换
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

4.2 创建实体类

实体类分为两种,一种是用户,一种是图书.定义类属性的时候,注意把数据库字段的蛇形命名法转换为小驼峰命名法.

  • 用户类
@Data
public class UserInfo {
    public Integer id;
    public String userName;
    public String password;
    public Integer deleteFlag;
    public Date createTime;
    public Date updateTime;
}
  • 图书类
@Data
public class BookInfo {
    public Integer id;
    public String bookName;
    public String author;
    public Integer count;
    public BigDecimal price;
    public String publish;
    //0-无效,1-正常,2-不可借阅
    public Integer status;
    public String statusCN;
    public Date createTime;
    public Date updateTime;
}

4.3 用户登录接口

  • 接口文档
[请求]
/user/login
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
[参数]
name=zhangsan&password=123456
[响应]
true //账号密码验证正确, 否则返回false

浏览器给服务器发送 /user/login 这样的HTTP请求,服务器给浏览器返回了⼀个Boolean类型的数据.返回true,表示账号密码验证正确.

  • Controller层
@RestController
@RequestMapping("/user")
public class UserInfoController {
    @Autowired
    private UserInfoService userInfoService;
    @RequestMapping("/login")
    public Boolean login(String name, String password, HttpSession session){
        //用户名或者密码为空
        if (!StringUtils.hasLength(name) || !StringUtils.hasLength(password)){
            return false;
        }
        UserInfo userInfo = userInfoService.selectUserByName(name);
        //没有查询到用户
        if (userInfo == null){
            return false;
        }
        if (userInfo != null && password.equals(userInfo.getPassword())){
            userInfo.setPassword("");//先把密码设置为空,以免被抓包
            //把用户信息存入session中,session中是以json的格式存储的对象
            session.setAttribute(Constant.SESSION_USER_KEY,userInfo);
            return true;
        }
        //密码错误
        return false;
    }
}
public class Constant {
    public static final String SESSION_USER_KEY = "SESSION_USER_KEY";
}

首先,确保用户输入了用户名和密码,首先判断用户名和密码不为空,如果为空,返回false.
之后,把用户输入的用户名传给Service层,返回一个用户类.
判断返回的用户类是否为null,如果为null,说明该用户不存在,返回false.如果不为null,说明用户存在,用户名存在.
用户存在之后,再判断用户输入的密码与数据库返回的用户密码是否一致,如果一致,把用户的密码设置为空字符串,再在session中存储用户信息.并返回true.否则返回false.
在设置session的时候,我们建议把session键值对的key拿出来,作为一个静态变量独立封装.

  • Service层
@Service
public class UserInfoService {
    @Autowired
    public UserInfoMapper userInfoMapper;
    public UserInfo selectUserByName(String name){
        return userInfoMapper.selectUserInfo(name);
    }
}

在业务逻辑层,我们把Controller层的用户名传给Mapper层(数据库层),之后返回数据库层返回的用户信息.

  • Mapper层
@Mapper
public interface UserInfoMapper {
    @Select("select * from user_info where user_name = #{name} and delete_flag = 0")
    public UserInfo selectUserInfo(String name);
}

在Mapper层,我们根据Service层传过来的用户名在数据库中搜索用户,并返回用户类.需要注意的是,需要限定有效的用户,已经删除的用户不包含在内.

  • 测试接口:data返回true
    在这里插入图片描述

4.4 添加图书接口

  • 接口文档
[请求]
/book/addBook
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
[参数]
bookName=图书1&author=作者1&count=23&price=36&publish=出版社1&status=1
[响应]
true or false
  • Controller层
@Autowired
private BookInfoService bookInfoService;
@RequestMapping("/addBook")
public Boolean addBook(BookInfo bookInfo){
    //有的图书信息为空
    if (!StringUtils.hasLength(bookInfo.getBookName())
    || !StringUtils.hasLength(bookInfo.getAuthor())
    || bookInfo.getCount() == null
    || bookInfo.getPrice() == null
    || !StringUtils.hasLength(bookInfo.getPublish())
    || bookInfo.getStatus() == null){
        return false;
    }
    if (bookInfoService.addBook(bookInfo) == false){
        log.warn("添加图书失败");
        return false;
    }
    log.info("添加图书成功");
    return true;
}

首先判断用户在添加图书的时候,图书信息输入全部合法,即不可以有任何一栏是空数据.否则返回false.
之后把图书数据传给Service层,根据Service层返回来的结果做出最终的日志打印和返回结果.

  • Service层
public Boolean addBook(BookInfo bookInfo){
    Integer i = bookInfoMapper.insertBook(bookInfo);
    if (i == 0) return false;
    return true;
}

调用Mapper层,把图书信息传递给Mapper层.
之后看Mapper返回的结果,如果返回的结果是0,说明数据库没有数据改变,插入失败,如果返回结果大于0,说明插入成功,返回true.

  • Mapper层
@Insert("insert into book_info " +
        "(book_name, author, count, price, publish,status) " +
        "values (#{bookName},#{author},#{count},#{price},#{publish},#{status})")
Integer insertBook(BookInfo bookInfo);

从形式参数的对象中获取对应的参数,插入数据库中.

  • 测试
    在这里插入图片描述

4.5 图书列表与翻页请求

  • 需求分析
    如果数据库中的图书有成百上千条,我们不可能在一个页面中展示所有的图书,我们需要对图书进行分页.
    比如我们一页展示10条数据.如果还想看其他数据,可以通过点击页码进行查询.
    分页的时候,数据是这样展示的:
    第一页:显示1-10条数据
    第二页:显示11-20条数据
    第三页:显示21-30条数据

    首先我们需要有更多的数据,我们在数据库中添加更多的数据.
INSERT INTO `book_info` ( book_name, author, count, price, publish )
VALUES
( '图书2', '作者2', 29, 22.00, '出版社2' ),( '图书3', '作者2', 29, 22.00, '出版社
3' ),
( '图书4', '作者2', 29, 22.00, '出版社1' ),( '图书5', '作者2', 29, 22.00, '出版社
1' ),
( '图书6', '作者2', 29, 22.00, '出版社1' ),( '图书7', '作者2', 29, 22.00, '出版社
1' ),
( '图书8', '作者2', 29, 22.00, '出版社1' ),( '图书9', '作者2', 29, 22.00, '出版社
1' ),
( '图书10', '作者2', 29, 22.00, '出版社1'),( '图书11', '作者2', 29, 22.00, '出版
社1'),
( '图书12', '作者2', 29, 22.00, '出版社1'),( '图书13', '作者2', 29, 22.00, '出版
社1'),
( '图书14', '作者2', 29, 22.00, '出版社1'),( '图书15', '作者2', 29, 22.00, '出版
社1'),
( '图书16', '作者2', 29, 22.00, '出版社1'),( '图书17', '作者2', 29, 22.00, '出版
社1'),
( '图书18', '作者2', 29, 22.00, '出版社1'),( '图书19', '作者2', 29, 22.00, '出版
社1'),
( '图书20', '作者2', 29, 22.00, '出版社1'),( '图书21', '作者2', 29, 22.00, '出版
社1');

前端在发起查询的时候,需要向服务端传递的参数:首先是currentPage 当前页码,默认值为1.其次是PageSize,每页显示的条数,默认值为10.
后端响应时,需要响应给前端的数据,records所查询到的数据列表(存储到List集合中).total总记录数(用于告诉前端显示多少页,显示页数为:(total + pageSize -1)/pageSize.其次后端需要计算出每一页的图书id的看开始索引,公式为:(currentPage-1) * pageSize.

  • 返回model
    首先是翻页请求对象:
    传入的是当前页码和每一页中展示图书的数目.
@Data
public class PageRequest {
	private int currentPage = 1; // 当前⻚ 
	private int pageSize = 10; // 每⻚中的记录数 
}

其次根据当前页码和每页要显示的页数,计算每页开始的索引.

@Data
public class PageRequest {
    public Integer currentPage = 1;
    public Integer pageSize = 10;

    public Integer getOffset(){
        return (currentPage-1)*pageSize;
    }
}

其次是翻页图书列表,用作返回结果:

@Data
public class PageResult<T>{
    public Integer total;//图书总数
    public List<T> records;//每页的图书列表
}

在每一页的返回结果中,列表中的参数类型使用泛型.

  • 接口文档
[请求]
/book/getListByPage?currentPage=1&pageSize=10
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
[参数] currentPage=1&pageSize=10
[响应]
Content-Type: application/json
{
	 "total": 25,
	 "records": [{
		 "id": 25,
		 "bookName": "图书21",
		 "author": "作者2",
		 "count": 29,
		 "price": 22.00,
		 "publish": "出版社1",
		 "status": 1,
		 "statusCN": "可借阅"
 }, {
 ......
 } ]
}

第一页可以不传参数,因为有默认值,为1.

  • Controller层
@RequestMapping("/getListByPage")
public PageResult<BookInfo> getListByPage(PageRequest pageRequest){
    log.info("获取图书列表,{}",pageRequest);
    return bookInfoService.getBookListByPage(pageRequest);
}

拿到前端传过来的参数,一个是当前页面,一个是每页的数目,把参数传递给后端,后端拿到参数之后,把参数传递给业务逻辑层,并打印日志.返回的是我们提前封装好的翻页图书列表类.

  • Service层
public PageResult<BookInfo> getBookListByPage(PageRequest pageRequest){
    PageResult<BookInfo> bookList = new PageResult<>();
    Integer total = bookInfoMapper.count();
    bookList.total = total;
    //为数据库的图书设置图书状态
    List<BookInfo> list = bookInfoMapper.selectBookByPage(pageRequest.getOffset(),pageRequest.getPageSize());
    for (BookInfo bookInfo:list){
        BookStatus nameByCode = BookStatus.getNameByCode(bookInfo.getStatus());
        if (nameByCode == null){
            throw new NullPointerException("book status is unknown");
        }
        bookInfo.setStatusCN(nameByCode.getStatus());
    }
    bookList.records = list;
    return bookList;
}

创建一个PageResult类型,之后在对象中添加图书总数.之后从对象中获取到每一页开始的索引和每一页展示的个数.获取到图书列表之后,设置图书字符串,字符串是根据每一本书的状态码来设置的.如果状态码是错误的,我们就抛出异常.
针对图书的状态,我们可以自定义一个枚举类来解决.设置完图书的状态信息之后,我们就需要把图书列表给bookList中的records信息.

@AllArgsConstructor
public enum BookStatus {
    DELETE(0,"无效"),
    NORMAL(1,"正常"),
    FORBIDDEN(2,"不可借阅");

    @Getter
    private Integer code;
    @Getter
    private String status;

    /**
     * 根据图书的状态码获取枚举对象
     * @param code 图书状态码
     * @return 返回枚举对象
     */
    public static BookStatus getNameByCode(Integer code){
        for (BookStatus bookStatus:BookStatus.values()){//遍历所有枚举对象
            if (bookStatus.getCode().equals(code)){
                return bookStatus;
            }
        }
        return null;
    }
}

在枚举类中,我们首先要定义枚举对象的属性,一个是状态码(code),一个是状态信息(msg).之后是枚举对象,枚举对象中需要传入枚举对象的属性.
之后我们需要把图书的状态码和枚举对象的状态码对上,我们可以使用BookStatus.values()获取到枚举类中的所有枚举对象,之后进行遍历,只要枚举对象的状态码和图书的状态码对得上,就返回对应枚举对象中的信息.否则返回null.

  • Mapper层
@Select("select count(*) from book_info where status != 0")
Integer count();

@Select("select * from book_info where status != 0 order by id desc " +
        "limit #{pageSize} offset #{offset}")
List<BookInfo> selectBookByPage(Integer offset,Integer pageSize);

Mapper层要做的是,查询图书的总数,根据页码和每一页显示的图书本数,返回查询的图书列表.

  • 测试
    在这里插入图片描述

4.6 修改图书接口

  • 接口文档
[请求]
/book/queryBookById?bookId=25
[参数]
无
[响应]
{
	 "id": 25,
	 "bookName": "图书21",
	 "author": "作者2",
	 "count": 999,
	 "price": 222.00,
	 "publish": "出版社1",
	 "status": 2,
	 "statusCN": null,
	 "createTime": "2023-09-04T04:01:27.000+00:00",
	 "updateTime": "2023-09-05T03:37:03.000+00:00"
}

首先我们需要根据图书ID,获取到当前图书的信息.
点击修改按钮之后,修改图书信息.

[请求]
/book/updateBook
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
[参数]
id=1&bookName=图书1&author=作者1&count=23&price=36&publish=出版社1&status=1
[响应]
true or false

我们约定,浏览器给服务器发送⼀个 /book/updateBook 这样的HTTP请求, form表单的形式来提交数据.
这里的参数就和添加图书是一样的.

  • Controller层
@RequestMapping("/queryBookById")
public BookInfo getBookById(Integer bookId){
    BookInfo bookInfo = bookInfoService.getBookById(bookId);
    if (bookInfo == null){
        log.warn("图书Id有误");
        return new BookInfo();
    }
    return bookInfo;
}

@RequestMapping("/updateBook")
public Boolean updateBook(BookInfo bookInfo){
    if (bookInfo.getId() == null){//在根据如数id查询图书的时候,有可能返回的是空对象
        log.error("图书Id不能为空");
        return false;
    }
    Integer ret = bookInfoService.updateBook(bookInfo);
    if (ret > 0){
        return true;
    }
    log.warn("添加图书失败");
    return false;
}

首先根据传入的图书id来查询到图书的信息.如果根据id没有查询到图书信息,则返回空图书类型.查询到则返回查询结果.
之后更新图书信息的时候,如果图书id为空的时候,就返回false,之后把更新后的图书对象传给Service层,如果返回的类型是大于0,则说明更新成功,否则更新失败.

  • Service层
public BookInfo getBookById(Integer bookId){
     return bookInfoMapper.selectBookById(bookId);
 }

 public Integer updateBook(BookInfo bookInfo){
     return bookInfoMapper.updateBook(bookInfo);
 }

查询图书的时候,只需要把图书id传给Mapper层即可.更新图书一样,也是直接把图书对象传给Mapper层即可.

  • Mapper层
@Select("select * from book_info where id = #{bookId}")
BookInfo selectBookById(Integer bookId);

Integer updateBook(BookInfo bookInfo);

查询图书的时候,我们使用注解来完成sql语句,但是更新图书的时候,只有id是必备选项,而其他属性都可以选择性更新.所以我们使用xml来完成sql.我们在这里需要使用到标签.

<!--    更新的时候可以选择项目更新,不一定全部要更新-->
<update id="updateBook">
    update book_info
    <set>
        <if test="bookName != null">
            book_name = #{bookName}
        </if>
        <if test="author != null">
            author = #{author}
        </if>
        <if test="count != null">
            count = #{count}
        </if>
        <if test="price!= null">
            price = #{price}
        </if>
        <if test="publish != null">
            publish = #{publish}
        </if>
        <if test="status != null">
            status = #{status}
        </if>
    </set> where id = #{id}
</update>

在传入的参数给对象中的属性赋值的时候,即对象拥有该属性的时候,就更新该字段,否则不更新.

4.7 删除图书接口

我们前面提到,删除分为逻辑删除和物理删除.我们这里使用的是逻辑删除.这里的删除就是把图书的状态(status)修改一下.

  • 接口文档
[请求]
/book/updateBook
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
[参数]
id=1&status=0
[响应]
true or false

由于这里的逻辑和更新图书的逻辑完全一样,我们不再赘述.

4.8 批量删除接口

批量删除图书,其实就是批量修改图书的状态.

  • 接口文档
[请求]
/book/batchDeleteBook
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
[参数] 
[响应]
true or false

点击[批量删除]按钮时,只需要把复选框选中的图书的ID,发送到后端即可.多个id,我们使用List的形式来传递参数

  • Controller层
@RequestMapping("/batchDeleteBook")
public Boolean beachDeleteBook(List<Integer> ids){
    if (ids == null || ids.isEmpty()){
        return true;//没有要删除的图书
    }
    Integer ret = bookInfoService.beachDeleteBooks(ids);
    if (ret == 0){
        log.error("批量删除失败");
        return false;
    }
    return true;
}

传入需要删除的图书id,之后判断ids是否为null或者ids为空,说明没有要删除的图书,返回true.之后把ids传给Service层,要是返回结果是0,说名没有图书被删除,返回false.否者返回true.

  • Service层
public Integer beachDeleteBooks(List<Integer> ids){
   return bookInfoMapper.beachDeleteBooks(ids);
}

直接把id传给Mapper层

  • Mapper层
    Mapper层我们依然使用xml的方式实现.因为需要遍历list,我们需要使用<foreach>标签.
Integer beachDeleteBooks(List<Integer> ids);
<update id="beachDeleteBooks">
    update book_info set status = 0 where id in
    <foreach collection="ids" open="(" close=")" separator="," item="id">
        #{id}
    </foreach>
</update>

4.9 强制登录

虽然我们做了用户登录,但是我们发现,用户不登录,依然可以操作图书.
这是有极大风险的.所以我们需要进行强制登录.
如果用户未登录就访问图书列表或者添加图书等页面,强制跳转到登录页面.

  • 实现思路
    在用户登录的时候,用户把用户的信息存储在了session中,这时候我们使用统一功能中的拦截器,对除了登录页面的接口统一拦截.判断session中是否存在用户的信息.存在就说明用户已经登录,没有就说明没用登录.
  • 返回结果封装
    我们不妨对所有后端的数据进行封装为Result.并添加对不同的状态的方法,方法中对Result的属性进行设置.
@Data
public class Results<T> {
    //200-成功 -1-用户未登录 -2后端结果错误
    public Status code;//状态码
    public String msg;//状态信息
    public T data;//返回页面的数据

    public static <T> Results<T> success(T data){
        Results<T> results = new Results<>();
        results.setCode(Status.SUCCESS);
        results.setMsg("正常");
        results.setData(data);
        return results;
    }
    public static <T> Results<T> noLogin(){
        Results<T> results = new Results<>();
        results.setCode(Status.NOLOGIN);
        results.setMsg("用户未登录");
        return results;
    }
    public static <T> Results<T> error(String s){
        Results<T> results = new Results<>();
        results.setCode(Status.ERROR);
        results.setMsg("内部错误" + s);
        return results;
    }
}

其中Status是后端逻辑处理的状态码,我们可以通过一个枚举类来封装.

@AllArgsConstructor
public enum Status {
    SUCCESS(200,"成功"),
    NOLOGIN(-1,"用户未登录"),
    ERROR(-2,"后端返回结果错误");

    @Getter
    private Integer code;
    @Getter
    private String msg;
}
  • 定义拦截器
/**
 * 登录拦截器
 */
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HttpSession session = request.getSession(false);
        if (session == null || session.getAttribute(Constant.SESSION_USER_KEY) == null){
            log.error("用户未登录");
            response.getOutputStream().write(Results.noLogin().getMsg().getBytes(StandardCharsets.UTF_8));
            response.setStatus(401);
            return false;
        }
        return true;
    }
}

实现HandlerInterceptor接口,重写preHandle方法.
首先从request中拿到session,其中参数要设置为false,防止session不存在自动创建.从session中获取用户信息,用户信息存储在SESSION_USER_KEY中,如果该字段的信息为空,或者是session不存在的时候,说明没有登录,返回false进行拦截.设置Response的状态码为401,在Response的字节流中输入错误信息.该错误信息由Result类中的nologin方法生成.
如果获取到了用户信息,则放行.

  • 注册拦截器
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
        		.addPathPatterns("/**")
                .excludePathPatterns("/user/login")
                .excludePathPatterns("/css/**")
                .excludePathPatterns("/js/**")
                .excludePathPatterns("/pic/**")
                .excludePathPatterns("**/*.html");
    }
}

实现WebMvcConfigurer接口,并重写addInterceptors方法,之后在形式参数中,使用addInterceptor方法添加拦截器,并使用excludePathPatternsaddPathPatterns对该拦截器的进行配置.拦截的可以是url,也可以是文件路径.不拦截登录页面和前端的所有页面.

  • 统一返回结果
@RestControllerAdvice
@Slf4j
public class ResponseAdvice implements ResponseBodyAdvice {
    @Autowired
    private ObjectMapper mapper;
    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        //如果返回结果是String类型的,需要转换为json格式
        if (body instanceof String){
            return mapper.writeValueAsString(Results.success(body));
        }
        //返回结果是Results类型,则不需要对结果进行封装
        if (body instanceof Results<?>){
            return body;
        }
        return Results.success(body);
    }

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return true;
    }
}

实现ResponseBodyAdvice接口,重写beforeBodyWrite方法.
这里我们需要注意的是对返回Body是字符串类型的需要进行特殊处理,需要把返回结果转换为json格式.要是返回的结果就是Result类型,直接返回body,如果是其他的类型,需要body封装进Result类型中,并调用success方法.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1975424.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

分销商城小程序系统如何开发

uni-app框架&#xff1a;使用Vue.js开发跨平台应用的前端框架&#xff0c;编写一套代码&#xff0c;可编译到Android、小程序等平台。 框架支持:springboot/Ssm/thinkphp/django/flask/express均支持 前端开发:vue.js 可选语言&#xff1a;pythonjavanode.jsphp均支…

EDI是什么:EDI系统功能介绍

EDI全称Electronic Data Interchange&#xff0c;也被称为“无纸化贸易”。EDI实现企业间&#xff08;B2B&#xff09;自动化通信&#xff0c;帮助贸易伙伴和组织完成更多的工作、加快物流时间并消除人为错误。 EDI遵从国际报文标准&#xff0c;使得业务数据按照结构化或是标准…

音频文件怎么转换成mp3?这5种方法快速转换

音频文件格式繁多&#xff0c;从WAV到FLAC&#xff0c;从AAC到OGG&#xff0c;每一种都有其独特的优势和应用场景。但当我们需要将音频文件分享给朋友、上传到网络平台或进行跨设备播放时&#xff0c;MP3格式因其广泛的兼容性和较小的文件体积&#xff0c;往往成为首选。给大家…

「字符串」实现Trie(字典树|前缀树)的功能 / 手撕数据结构(C++)

概述 在浏览器搜索栏里输入几个字&#xff0c;就弹出了以你的输入为开头的一系列句子。浏览器是怎么知道你接下来要输什么的&#xff1f; 来看看字典树干了什么。 字典树是一种高效记录字符串和查找字符串的数据结构。它以每个字符作为一个节点对字符串进行分割记录&#xff0c…

48 集合应用案例

编写代码时除了要准确地实现功能之外&#xff0c;还要考虑代码的优化&#xff0c;尽量找到一种更快、更好的方法实现预定功能。Python 字典和集合都使用哈希表来存储元素&#xff0c;元素查找速度非常快&#xff0c;关键字 in 作用于字典和集合时比作用于列表要快得多。 impor…

【数据结构之单链表的实现(不带头)】

1.单链表 1.1概念与结构 链表是一种物理存储结构上非连续&#xff0c;非顺序的存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的指针连接次序实现的。 可以用下图便于理解 节&#xff08;结&#xff09;点&#xff1a; 与顺序表不同的是&#xff0c;链表里面的每节“车…

三十种未授权访问漏洞合集

未授权访问漏洞介绍 未授权访问可以理解为需要安全配置或权限认证的地址、授权页面存在缺陷&#xff0c;导致其他用户可以直接访问&#xff0c;从而引发重要权限可被操作、数据库、网站目录等敏感信息泄露。---->目录遍历 目前主要存在未授权访问漏洞的有:NFS服务&a…

百度飞桨 OCR识别

百度飞桨 OCR识别代码 import warnings import time import cv2 as cv import paddlehub as hub # Load the image img cv.imread("1.jpg") height, width, channels img.shape imglist [img] ocr hub.Module(name"ch_pp-ocrv3", enable_mkldnnTrue) …

从Axure入门,开始了解产品

​不少想要求职产品经理的小伙伴在问一个问题&#xff1a;我是一个纯小白&#xff0c;一点基础都没有&#xff0c;我该如何入门产品呢&#xff1f;当然想要入门产品&#xff0c;很多人都有自己的一套方法&#xff0c;这里推荐其中的一种方法&#xff0c;从原型工具&#xff0c;…

ModuleNotFoundError: No Module Named openai

题意&#xff1a;Python 无法在环境中找到名为 openai 的模块 问题背景&#xff1a; import requests from bs4 import BeautifulSoup import openai #write each line of nuclear.txt to a list with open(nuclear.txt, r) as f:lines f.readlines()#remove the newline cha…

Spring源码-ClassPathXmlApplicationContext的refresh()都做了什么?

AbstractApplicationContext的refresh方法 /*** 用给定的父类创建一个新的ClassPathXmlApplicationContext* Create a new ClassPathXmlApplicationContext with the given parent,* 从给定的XML文件加载定义* loading the definitions from the given XML files.* param confi…

UE5 从零开始制作跟随的大鹅

文章目录 二、绑定骨骼三、创建 ControlRig四、创建动画五、创建动画蓝图六、自动寻路七、生成 goose八、碰撞 和 Physics Asset缺点 # 一、下载模型 首先我们需要下载一个静态网格体&#xff0c;这里我们可以从 Sketchfab 中下载&#xff1a;Goose Low Poly - Download Free …

十条线路:畅享张北草原天路玩法

2024年6月6日&#xff0c;张家口市政府新闻办召开新闻发布会&#xff0c;发布10条草原天路精品旅游线路&#xff0c;同时就草原天路今年改造提升重点工作进行介绍。其中&#xff0c;10条精品旅游线路包含5条玩转天路经典线路和5条穿越天路新玩法线路。 1、寻“天路之巅”网红打…

Java并发编程 使用锁和状态位来控制线程的执行顺序

Java线程生命周期的认识 对于线程的生命周期&#xff0c;在Java和操作系统中&#xff0c;在概念上有一点小小的不同。 在操作系统层面上&#xff0c;线程的生命周期如下&#xff1a; 1.新建 2.就绪 3.阻塞 4.运行 5.终止 而在Java层面上&#xff0c;则把线程的阻塞状态又划分…

详细分析Flask部署云服务器(图文介绍)

目录 前言1. 安装配置2. 代码部署3. 服务配置4. 自启动前言 Nginx信息补充阅读: Nginx从入门到精通(全)Nginx配置静态网页访问(图文界面)本文着重提供思路逻辑 1. 安装配置 最好的方式是安装docker,通过docker安装nginx,推荐阅读:Docker零基础从入门到精通(全)包环…

与用户有关的接口

1.获取用户详细信息 跟着黑马程序员继续学习SpringBoot3Vue3 用户登录成功之后跳转到首页&#xff0c;需要获取用户的详细信息 打开接口文档 使用Token令牌解析得到用户名 我们需要根据用户名查询用户&#xff0c;获取详细信息 但是请求参数是无&#xff0c;由于都需要携…

标题生成器:开启创意写作的新篇章

文章目录 角色与目标标题生成器的功能标题生成器的优势指导原则限制与澄清应用场景对创意写作的影响智能体发布到微信公众号配置公众号菜单配置自动回复自动回复文本链接自动回复二维码图片 标题生成器的未来发展总结 博主介绍&#xff1a;全网粉丝10w、CSDN合伙人、华为云特邀…

C++入门基本语法(1)

一、命名空间namespace 定义变量、函数时&#xff0c;定义的名称可能会和头文件中或者自己重复使用的名称冲突&#xff1b;namespace可以对标识符的名称进行本地化&#xff0c;以避免冲突的问题&#xff1b; ## 例如&#xff1a; ## 出现这种问题的原因&#xff1a; &#x…

MySQL系列之--详细安装教程和启动方法

文章目录 安装教程打开或关闭方式方式1&#xff1a;方式2&#xff1a; 客户端连接方式客户端连接方式1&#xff1a;客户端连接方式2&#xff1a;MySQL环境变量的配置 安装教程 1、mysql官网下载最新的符合本系统的版本 2、点击.msi文件进入安装页面 选择默认的选项开发者安…

品味食家巷蛋奶酪饼,感受西北美食魅力

在广袤的西北大地&#xff0c;美食的丰富多样令人叹为观止。而食家巷蛋奶酪饼&#xff0c;宛如一颗璀璨的明珠&#xff0c;散发着独特的魅力。 这款蛋奶酪饼&#xff0c;是传统工艺与现代口味的完美融合。而当你继续品尝&#xff0c;鸡蛋的鲜嫩和奶酪的浓郁醇厚便会在口中交融…