《JavaEE进阶》----21.<基于Spring图书管理系统②(图书列表+删除图书+更改图书)>

PS:

开闭原则

定义和背景‌

‌开闭原则(Open-Closed Principle, OCP)‌,也称为开放封闭原则,是面向对象设计中的一个基本原则。该原则强调软件中的模块、类或函数应该对扩展开放,对修改封闭。这意味着一个软件实体应该在不修改现有代码的基础上,能够适应新的变化和需求。

核心思想

开闭原则的核心思想是:

  • ‌对扩展开放‌:当新的需求或变化出现时,可以通过扩展现有代码来适应新的情况,而不是修改现有的代码。
  • ‌对修改封闭‌:一旦类或模块设计完成,就应该能够独立完成其工作,不再对其进行任何修改。(能够兼容之前的版本)

针对先上线的程序而言

比如后端接口从A改成了B。(前端接口也要修改)

如果后端先上线:前端调用就会出错,找不到A接口

如果前端先上线,前端调用依然会报错,找不到B接口。因为此时后端还没上线。

正确的做法:后端对之前的接口进行兼容,如果兼容则同时存在A和B接口

实现方式

实现开闭原则的方式主要包括:

  • ‌抽象编程‌:通过面向对象的继承和多态机制,实现对抽象体的继承和方法的覆写,从而实现新的扩展方法。
  • ‌依赖抽象‌:类依赖于固定的抽象,而不是具体的实现,这样可以保证类的稳定性。

相关设计原则

开闭原则与其他设计原则密切相关,包括:

  • ‌里氏替换原则‌:子类对象能够替代父类对象出现的地方,并且保证原有逻辑行为不变。
  • ‌接口隔离原则‌:接口调用方和使用者只关心自己相关的接口,不依赖于不需要的接口。
  • ‌依赖反转原则‌:高模块不直接依赖低模块,而是通过抽象来互相依赖。
  • ‌单一职责原则‌:一个类或模块只负责完成一个职责或功能。

一、图书列表展示功能

1.1 实现分页功能

提到展示图书列表,就不得不提到分页了

分页时,数据是如何展示的呢

第1页:显示1-10 条的数据

第2页:显示11-20 条的数据

第3页:显示 21-30 条的数据

以此类推...

要想实现这个功能,从数据库中进行分页查询,我们要使用 LIMIT 关键字,格式为:limit 开始索引 每页显示的条数(开始索引从0开始)。

sql 复制代码
select * from book_info where status <> 0 limit 0,10;
sql 复制代码
select * from book_info where status <> 0 limit 10,10;
sql 复制代码
select * from book_info where status <> 0 limit 20,10;

我们发现只有开始索引在改变。每页显示的条数是固定的。

开始索引的计算公式:开始索引 = (当前页码 - 1) * 每页显示条数。

因此:

1.前端发起查询请求时,需要向服务器端传递的参数。

currentPage 当前页码 :默认值为1

pageSize 每页显示条数 默认值为10

注:

为了项目更好的扩展性,通常不设置固定值,而是是以参数的形式来进行传递

**扩展性:**软件系统具备面对未来需求变化而进行扩展的能力。 比如当前需求一页显示10条,后期需求改为一页显示20条,

后端代码不需要任何修改。

  1. 后端响应时,需要响应给前端的数据。

**records :**所查询到的数据列表(存储到List集合中)

count:总记录数(用于告诉前端显示多少页,

显示页数为:(count + pageSize -1)/pageSize

翻页请求和响应部分, 我们通常封装在两个对象中

1.1.1 翻页请求对象PageRequest

创建PageRequest

前端进行请求

1.会请求当前页 和 每页显示的个数。

2.由上面两个数据计算出offset,用作参数传递给SQL语句

sql 复制代码
package com.qiyangyang.springbook.demos.model;
import lombok.Data;
@Data
public class PageRequest {
    private Integer currentPage = 1;//当前页
    private Integer pageSize = 10;//每页显示个数
    private Integer offset;
    /**
     * 从多少条记录开始查询
     * @return
     */
    public Integer getOffset() {
        return (currentPage-1) * pageSize;
    }
}

1.1.2 翻页响应对象

java 复制代码
package com.qiyangyang.springbook.demos.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

/**
 * 定义一个泛型类,就是,这个地方不定义具体类型
 * 我们在进行对象的生成的时候,它才有具体的类型。
 * @param <T>
 */
//上面两个注解用来创建构造方法
@AllArgsConstructor
@NoArgsConstructor
@Data
public class PageResult<T> {
    /**
     * 返回的结果也是一个泛型
     * 不定义具体类型,在对象的创建才会有具体类型
     */
    private List<T> records; //当前页数据
    private Integer count; //所有记录数
    private PageRequest pageRequest; //小驼峰,用来返回给前端当前页数
    // 这里将整个对象告诉result。用来给前端获取多少页

}

返回结果中, 使用泛型来定义记录的类型

后端定义参数。

offset(起始序号)和limit(显示多少条)

MySQL语句

前端根据总记录数,来显示分了多少页。

1.2使用枚举来处理status/stateCN字段。 (可借阅/不可借阅)

1.创建enums文件夹

2.创建BookStatusEnums类

java 复制代码
package com.qiyangyang.springbook.demos.enums;

/**
 * 枚举类
 * 可以列举出来的,是一个有限的个数,我们将他们定义成枚举类
 * 方便定义类似
 * 根据状态设置描述
 *             if(bookInfo.getStatus() == 1){
 *                 bookInfo.setStateCN("可借阅");
 *             } else if (bookInfo.getStatus() == 2) {
 *                 bookInfo.setStateCN("不可借阅");
 *             }else {
 *                 bookInfo.setStateCN("无效");
 *             }
 */
public enum BookStatusEnums {
    DELETE(0,"无效"), //删除
    NORMAL(1,"可借阅"), //有效的
    FORBIDDEN(2,"不可借阅"), //禁止
    ;
    private int code;
    private String desc;

    BookStatusEnums(int code, String desc) {
        this.code = code;
        this.desc = desc;
    }

    /**
     * 我们将这个封装成一个方法
     * 根据code获取描述
     *
     * @return
     */
    public static  BookStatusEnums getDescByCode(int code){
        switch (code){
            case 0: return BookStatusEnums.DELETE;
            case 1: return BookStatusEnums.NORMAL;
            case 2: return BookStatusEnums.FORBIDDEN;
        }
        return BookStatusEnums.DELETE;
    }
    public int getCode() {
        return code;
    }

    public String getDesc() {
        return desc;
    }
}

稍后在业务层我们会对这个方法进行调用

1.3约定前后端交互接口

接口定义:

url:/book/getListByPage?currentPage=1

Content-Type: application/x-www-form-urlencoded; charset=UTF-8

参数:

当前页数

返回结果:

当前页的数据+总记录数(决定前端显示多少页数)
我们约定,浏览器给服务器发送一个

/book/getListByPage 这样的 HTTP 请求,

通过 currentPage 参数告诉服务器,当前请求为第几页的数据,

后端根据请求参数,返回对应页的数据

第一页可以不传参数, currentPage默认值为1。

1.4实现服务器代码

1.4.1控制层:

完善 BookController

sql 复制代码
    @RequestMapping("getListByPage")
    public PageResult<BookInfo> getListByPage(PageRequest pageRequest){
        log.info("查询列表信息,pageRequest:{}",pageRequest);
        if(pageRequest.getCurrentPage()< 1){
            return null;
        }//这里返回null。会导致前端不知道是没有数据为null。还是当前页错误返回null
        //先不管,后续改进
        /**
         * 通过Service来去调用数据库
         */
        return bookService.getListByPage(pageRequest);
    }

1.4.2 业务层 :

完善 BookService

java 复制代码
    public PageResult<BookInfo> getListByPage(PageRequest pageRequest) {
        /**
         * 1.查询记录的总数
         * 2.查询当前页的数据
         */
        Integer count = bookInfoMapper.count();
        //bookInfos来接收查询到的数据
        List<BookInfo> bookInfos = bookInfoMapper.queryListByPage(pageRequest);

        for(BookInfo bookInfo : bookInfos){
            /**
             * 根据book状态设置描述(stateCN)
             */                    
        bookInfo.setStateCN(BookStatusEnums.getDescByCode(bookInfo.getStatus()).getDesc());
        }

        return new PageResult<>(bookInfos,count);
    }

1.4.3 数据层 :

完善 BookInfoMapper

java 复制代码
    /**
     * 查询总数
     * @return
     */
    //count(1):返回满足条件的记录数(即行数)。count(1) 和 count(*) 基本等效,都是用于统计记录数。
    @Select(("select count(1) from book_info where status <> 0"))
    Integer count();

    //希望把新添加的图书放到下面,因此order by id desc降序。

    @Select("select * from book_info where status <> 0 order by id desc limit #{offset},#{pageSize}")
    List<BookInfo> queryListByPage(PageRequest pageRequest);

1.5校验后端

不用传参也行,因为我们默认currentPage 为1。且pageSize为5。

我们发现返回正确。

总记录数也返回正确。为46。

我们发现后端接口没有问题。

1.6实现前端代码

1.6.1显示图书数据的内容

将前端<tbody>标签中的内容,也就是

html 复制代码
        <tbody>
          //这里的内容我们用findHtml变量拼接并传送到这个标签里了

        </tbody>

我们写在ajax中使用findHtml变量进行拼接。并用如下方法传送到这个标签中。

javascript 复制代码
$("tbody").html(findHtml); //塞到tbody这个标签里面
javascript 复制代码
            success: function (result) {
              var books = result.records;
              console.log(books); //如果前端没有报错,那么我们打印日志。观察后端返回结果对不对
              var findHtml = "";  //用这个变量来拼接HTML
              for (var book of books) {
                //拼接html。假如后端返回10个tr那么直接for循环拼接在这里面。findHtml
                //我们用单引号拼接,因为里面有双引号
                findHtml += '<tr>';
                findHtml += '<td><input type="checkbox" name="selectBook" value="' +book.id +'" id="selectBook" class="book-select"></td>';
                findHtml += "<td>" + book.id + "</td>";
                findHtml += "<td>" + book.bookName + "</td>";
                findHtml += "<td>" + book.author + "</td>";
                findHtml += "<td>" + book.count + "</td>";
                findHtml += "<td>" + book.price + "</td>";
                findHtml += "<td>" + book.publish + "</td>";
                findHtml += "<td>" + book.stateCN + "</td>";
                findHtml += "<td>";
                findHtml += '<div class="op">';
                findHtml +=
                  '<a href="book_update.html?bookId=' + book.id + '">修改</a>';
                findHtml +=
                  '<a href="javascript:void(0)" onclick="deleteBook('+book.id +')">删除</a>';
                findHtml += "</div>";
                findHtml += "</td>";
                findHtml += "</tr>";
              }
              $("tbody").html(findHtml); //塞到tbody这个标签里面


​

1.6.2处理翻页信息

我们需要在前端head标签中引入jquery 和 paginator

相当于引入插件

html 复制代码
    <script type="text/javascript" src="js/jquery.min.js"></script>
    <script type="text/javascript" src="js/bootstrap.min.js"></script>
    <script src="js/jq-paginator.js"></script>
html 复制代码
      <div class="demo">
        <ul id="pageContainer" class="pagination justify-content-center"></ul>
      </div>
javascript 复制代码
              //处理翻页信息
                console.log(result);
                console.log(result.pageRequest);
              //翻页信息
              $("#pageContainer").jqPaginator({
                totalCounts: result.count, //总记录数
                pageSize: 10, //每页的个数
                visiblePages: 5, //可视页数
                currentPage: result.pageRequest.currentPage, //当前页码
                first:'<li class="page-item"><a class="page-link">首页</a></li>',
                prev: '<li class="page-item"><a class="page-link" href="javascript:void(0);">上一页<\/a><\/li>',
                next: '<li class="page-item"><a class="page-link" href="javascript:void(0);">下一页<\/a><\/li>',
                last: '<li class="page-item"><a class="page-link" href="javascript:void(0);">最后一页<\/a><\/li>',
                page: '<li class="page-item"><a class="page-link" href="javascript:void(0);">{{page}}<\/a><\/li>',
                //页面初始化和页码点击时都会执行
                onPageChange: function (page, type) {
                  console.log("第" + page + "页, 类型:" + type);
                  if(type == "change"){
                    location.href = "book_list.html?currentPage="+page;
                  }
                },
              });

1.7校验前后端

成功实现图书列表显示以及翻页功能。

二、修改图书列表功能

2.1约定前后端交互接口

1.进入修改页面,需要显示当前 Id 图书的信息

请求

/book/queryBookById?bookId=25

参数

bookId

响应

{

"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,获取当前图书的信息

2.点击修改按钮,修改图书信息

请求

/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/false
我们约定,

浏览器给服务器发送一个 /book/updateBook 这样的HTTP请求,

form表单的形式来 提交数据

服务器返回处理结果,

返回"修改成功"修改图书成功,否则,返回失败信息.

2.2实现服务器端代码

2.2.1控制层:

java 复制代码
    @RequestMapping("/queryBookById")
    public BookInfo queryBookById(Integer bookId){
        log.info("查询图书信息,bookId:"+bookId);
        if(bookId == null || bookId<=0){
            return new BookInfo();
        }
        return bookService.queryBookById(bookId);
    }
    @RequestMapping("/updateBook")
    //先使用boolean类型返回。后续我们还会再进行完善。
    public boolean upDateBook(BookInfo bookInfo){
        log.info("修改图书信息, updateBook{}:",bookInfo);
        if(!StringUtils.hasLength(bookInfo.getBookName())
                || !StringUtils.hasLength(bookInfo.getAuthor())
                || !StringUtils.hasLength(bookInfo.getPublish())
                || bookInfo.getCount() <=0
                || bookInfo.getPrice()==null){
            return false;
        }
        try {
            Integer result = bookService.updateBook(bookInfo);
            if(result <= 0){
                return false;
            }
        }catch (Exception e){
            log.error("更新图书失败");
            return false;
        }
        return true;
    }

2.2.2 业务层 :

java 复制代码
    public BookInfo queryBookById(Integer bookId) {
        return bookInfoMapper.queryBookById(bookId);
    }

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

2.2.3 数据层 :

java 复制代码
    /**
     * 根据Id查询图书信息
     * @param id
     * @return
     */
    @Select("select * from book_info where status <> 0 and  id = #{id}")
    BookInfo queryBookById(Integer id);

    /**
     * 根据Id修改图书信息
     */
    Integer updateBook(BookInfo bookInfo);

根据 Id 修改图书信息 我们使用的是XML方式实现的SQL

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.qiyangyang.springbook.demos.mapper.BookInfoMapper">
    <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>
</mapper>

2.3校验后端

我发现这个接口都校验成功。

2.3.1校验queryBookById接口

2.3.2校验updateBook接口

修改成功!

2.4实现前端代码

注意:前端传递数据的时候记得加上id。

XML 复制代码
    <script type="text/javascript" src="js/jquery.min.js"></script>
    <script>
        //查询当前ID图书
        $.ajax({
            type: "get",
            url: "book/queryBookById"+location.search,
            success:function(book){
                if(book!=null){
                    $("#bookId").val(book.id);
                    $("#bookName").val(book.bookName);
                    $("#bookAuthor").val(book.author);
                    $("#bookStock").val(book.count);
                    $("#bookPrice").val(book.price);
                    $("#bookPublisher").val(book.publish);
                    $("#bookStatus").val(book.status);
                }
            }
        });
        //更新当前Id图书
        function update() {
            $.ajax({
                type: "get",
                url: "/book/updateBook",
                data:$("#updateBook").serialize(),//提交整个表单
                success:function(result){
                    if(result == true){
                        alert("更新成功");
                        location.href = "book_list.html"
                    }else{
                        alert("更新失败");
                    }
                }
                
            });
        }
    </script>

2.5整体测试

比如我们要将图书ID为135的图书

作者 修改为 洋洋

数量 修改为 888

价格 修改为 666

出版社 修改为 人民出版社

可借阅 修改为 不可借阅

修改成功!!!!!

三、逻辑删除图书

删除图书分为

逻辑删除(update):

从逻辑上进行删除,数据并没有真实删除

物理删除(delete语句):

数据真实删除。

但数据并没有真实清空,只是数据库上看不到了。

硬件存储上还是存在的

删除并归档(操作交为复杂):insert into... select....语句

1.删除(delete or update)

2.归档(把已经删除的数据存储下来)

3.1约定前后端交互接口

逻辑删除的话,

依然是更新逻辑,我们可以直接使用修改图书的接口

请求

/book/updateBook

Content-Type: application/x-www-form-urlencoded; charset=UTF-8

参数

id=1&status=0

响应

true / false

3.2实现服务器代码

3.2.1控制层:

由于我们在upadate接口中只需要传递 id 和 status 两个参数。

因此我们需要修改控制层中的校验参数的步骤

java 复制代码
    @RequestMapping("/updateBook")
    //先使用boolean类型返回。后续我们还会再进行完善。
    public boolean upDateBook(BookInfo bookInfo){
        log.info("修改图书信息, updateBook{}:",bookInfo);
        if(!StringUtils.hasLength(bookInfo.getBookName())
                || !StringUtils.hasLength(bookInfo.getAuthor())
                || !StringUtils.hasLength(bookInfo.getPublish())
                || bookInfo.getCount() <=0
                || bookInfo.getPrice()==null){
            return false;
        }
        try {
            Integer result = bookService.updateBook(bookInfo);
            if(result <= 0){
                return false;
            }
        }catch (Exception e){
            log.error("更新图书失败");
            return false;
        }
        return true;
    }

修改为

java 复制代码
    @RequestMapping("/updateBook")
    //先使用boolean类型返回。后续我们还会再进行完善。
    public boolean updateBook(BookInfo bookInfo){
        log.info("修改图书信息, updateBook{}:",bookInfo);
        if(bookInfo.getId()<0){
            return false;
        }
        try {
            Integer result = bookService.updateBook(bookInfo);
            if(result <= 0){
                return false;
            }
        }catch (Exception e){
            log.error("更新图书失败");
            return false;
        }
        return true;
    }

3.2.2 业务层 :

同之前的updateBook一样

java 复制代码
    public Integer updateBook(BookInfo bookInfo) {
        return bookInfoMapper.updateBook(bookInfo);
    }

3.2.3 数据层 :

同之前的update一样

java 复制代码
    /**
     * 根据Id修改图书信息
     */
    Integer updateBook(BookInfo bookInfo);
XML 复制代码
    <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>

校验后端接口

134号图书 status 成功修改为0

3.3实现前端代码

javascript 复制代码
        function deleteBook(id) {
            //删除图书
          var isDelete = confirm("确认删除?");
          if (isDelete) {
            $.ajax({
              type: "post",
              url: "/book/updateBook",
              data:{
                id: id,
                status: 0
              },
              success:function(result){
                if(result == true){
                  alert("删除成功");
                  location.href = "book_list.html";
                }
              }
            });
          }
        }

3.4整体校验

删除132号图书三体

成功删除!!!!!!!!!!!

四、批量逻辑删除

4.1定义前后端交互接口

请求:

/book/batchDeleteBook

参数:

响应:

true/false

4.2实现服务器端代码

4.2.1控制层:

注意加上:注解@RequestParam

java 复制代码
    @RequestMapping("/batchDelete")
    public boolean batchDelete(@RequestParam List<Integer> ids){
        log.info("批量删除数据,ids:{}",ids);
        try {
            Integer result = bookService.batchDelete(ids);
            if(result <= 0){
                return false;
            }
        }catch (Exception e){
            log.info("批量删除失败,id:{},e:{}", ids, e);
            return false;
        }
        return true;
    }

4.2.2 业务层 :

java 复制代码
    public Integer batchDelete(List<Integer> ids) {
        return bookInfoMapper.batchDelete(ids);
    }

4.2.3 数据层 :

注意加上注解:

@Param("ids")

java 复制代码
    Integer batchDelete(@Param("ids") List<Integer> ids);
XML 复制代码
    <update id="batchDelete">
        update book_info
        set status = 0
        where id in
        <foreach collection="ids" open="(" close=")" item="id" separator=",">
            #{id}
        </foreach>
    </update>

4.3后端校验

后端操作成功

4.4实现前端代码

java 复制代码
        function batchDelete() {
          var isDelete = confirm("确认批量删除?");
          if (isDelete) {
            //获取复选框的id
            var ids = [];
            $("input:checkbox[name='selectBook']:checked").each(function () {
              ids.push($(this).val());
            });
            console.log(ids);
            $.ajax({
              type: "post",
              url: "/book/batchDelete?ids="+ids,
              success:function(result){
                if(result == true){
                  alert("批量删除成功");
                  location.href = "book_list.html";
                }else{
                  alert("删除失败,请联系管理员!");
                }
              }
            });
          }
        }

4.5整体测试

测试成功!!!!

五、强制登录

这个功能的实现我们下一篇文章再讲哦!!!!

到这里其实这个图书管理系统的功能就基本实现完成了。

不过对于这个图书管理系统。

我们没有进行登录也可以进行操作。

因此我们下一篇文章会详细讲解强制登录功能。

并且后续会讲到统一功能!!!!!!!!!!!!!!!

相关推荐
Chenyiax几秒前
从一次请求看懂 OkHttp:架构、调度与连接管理
后端
爱勇宝1 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
AskHarries1 小时前
工具失败时怎么办:重试、回滚、人工确认和风险提示
后端·程序员
苏三说技术3 小时前
Claude Code从失控到起飞,只用了这些技巧
后端
长栎4 小时前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode4 小时前
Redis 在生产项目的使用
前端·后端
用户559822481224 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode4 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战4 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha4 小时前
迄今為止,最簡單的編程語言 Nolang
前端·后端