Web 小项目: 网页版图书管理系统

目录

最终效果展示

[代码 Gitee 地址](#代码 Gitee 地址)

[1. 引言](#1. 引言)

[2. 留言板 [热身小练习]](#2. 留言板 [热身小练习])

[2.1 准备工作 - 配置相关](#2.1 准备工作 - 配置相关)

[2.2 创建留言表](#2.2 创建留言表)

[2.3 创建 Java 类](#2.3 创建 Java 类)

[2.4 定义 Mapper 接口](#2.4 定义 Mapper 接口)

[2.5 controller](#2.5 controller)

[2.6 service](#2.6 service)

[3. 图书管理系统](#3. 图书管理系统)

[3.1 准备工作 - 配置相关](#3.1 准备工作 - 配置相关)

[3.2 创建数据库表](#3.2 创建数据库表)

[3.2.1 创建用户表 + 图书表](#3.2.1 创建用户表 + 图书表)

[3.3 创建 Java 类](#3.3 创建 Java 类)

[3.4 校验用户登录接口](#3.4 校验用户登录接口)

[3.5 添加图书](#3.5 添加图书)

[3.5.1 约定前后端交互接口](#3.5.1 约定前后端交互接口)

[3.5.2 后端接口](#3.5.2 后端接口)

[3.5.3 前端代码](#3.5.3 前端代码)

[3.6 展示图书列表(分页展示)](#3.6 展示图书列表(分页展示))

[3.6.1 约定前后端交互接口](#3.6.1 约定前后端交互接口)

[3.6.2 后端接口](#3.6.2 后端接口)

[3.6.2.1 准备工作 - 参数接收和响应返回](#3.6.2.1 准备工作 - 参数接收和响应返回)

[3.6.2.2 编写 Mapper 层方法](#3.6.2.2 编写 Mapper 层方法)

[3.6.2.3 controller + service](#3.6.2.3 controller + service)

[3.6.3 前端代码](#3.6.3 前端代码)

[3.7 更新图书信息](#3.7 更新图书信息)

[3.7.1 约定前后端交互接口](#3.7.1 约定前后端交互接口)

[3.7.2 后端接口](#3.7.2 后端接口)

[3.7.2.1 Mapper 层](#3.7.2.1 Mapper 层)

[3.7.2.2 controller + service](#3.7.2.2 controller + service)

[3.7.3 前段代码](#3.7.3 前段代码)

[3.8 删除图书信息](#3.8 删除图书信息)

[3.8.1 约定前后端交互接口](#3.8.1 约定前后端交互接口)

[3.8.2 后端接口](#3.8.2 后端接口)

[3.8.3 前端代码](#3.8.3 前端代码)

[3.9 批量删除图书信息](#3.9 批量删除图书信息)

[3.9.1 约定前后端交互接口](#3.9.1 约定前后端交互接口)

[3.9.2 后端接口](#3.9.2 后端接口)

[3.9.3 前端代码](#3.9.3 前端代码)

[3.10 强制登录机制](#3.10 强制登录机制)

[3.10.1 后端接口](#3.10.1 后端接口)

[3.10.1.1 封装常量](#3.10.1.1 封装常量)

[3.10.1.2 封装响应结果](#3.10.1.2 封装响应结果)

[3.10.2 前端代码](#3.10.2 前端代码)


最终效果展示

QQ2025318-205420-HD

代码 Gitee 地址

网页版 - 图书管理系统

1. 引言

在之前 Spring MVC 阶段的案例练习中, 我们只使用了 MVC 的知识来和前端进行交互, 没有对数据进行持久化的处理, 当重启服务器后, 所有的数据都会丢失.

之前练习的案例在这篇博客中:

Spring MVC:综合练习 - 深刻理解前后端交互过程-CSDN博客

而目前, 基于 MyBatis 知识的学习, 再来对之前的练习进行一下改造, 将数据持久化的保存到数据库中.

注意: 由于这些案例的部分接口已经在之前的博客中约定好了, 并且已经完成了前端代码, 因此在本篇博客中就不再赘述.

2. 留言板 [热身小练习]

在之前的代码中, 用户每发送一条留言, 前端会将这些留言追加到页面显示给用户, 并且我们的后端是会数据存储到 List 中, 当用户刷新页面时, 前端调用后端的接口, 将 List 中的数据返回给前端, 前端再将数据展示到页面上, 以到达用户刷新页面时, 之前发布的留言不会丢失的目的.

虽然之前的代码, 能够保证用户刷新页面时数据不丢失, 但是由于 List 是保存在内存中的, 服务器重启后, List 中的数据依旧会丢失.

要实现数据的持久化处理, 需要将数据保存到数据库中.

2.1 准备工作 - 配置相关

首先, 引入 MyBatis 和 MySQL 驱动的相关依赖.

接着, 进行数据库连接和其他相关配置.

java 复制代码
spring:
  application:
    name: springboot-demo
  # 数据库配置
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=false
    username: root
    password: 111111
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  # 配置 mybatis xml 的⽂件路径,在 resources/mapper 创建所有表的 xml ⽂件
  mapper-locations: classpath:mapper/*Mapper.xml
  configuration:
    # 配置打印 MyBatis⽇志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    # 配置驼峰自动转换
    map-underscore-to-camel-case: true
#   将默认的日志级别修改为 info
logging:
  level:
    root: info

2.2 创建留言表

将留言信息保存到数据库中, 首先需要创建一个留言表(message_info):

sql 复制代码
DROP TABLE IF EXISTS message_info;
CREATE TABLE `message_info` (
        `id` INT ( 11 ) NOT NULL AUTO_INCREMENT,
        `from` VARCHAR ( 127 ) NOT NULL,
        `to` VARCHAR ( 127 ) NOT NULL,
        `message` VARCHAR ( 256 ) NOT NULL,
        `delete_flag` TINYINT ( 4 ) DEFAULT 0 COMMENT '0-正常, 1-删除',
        `create_time` DATETIME DEFAULT now(),
        `update_time` DATETIME DEFAULT now() ON UPDATE now(),
PRIMARY KEY ( `id` ) 
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;

注意: delete_flag 字段是为了实现逻辑删除而设定的.

  1. 逻辑删除: 不使用 delete 进行删除, 而是使用额外的字段(如: delete_flag)对记录进行标记, 表示不再使用该记录(不是真正的删除)
  2. 物理删除: 使用 delete 删除记录(真正的删除)

在实际工作中, 要尽量避免使用 delete, 以免带来不必要的损失.

2.3 创建 Java 类

创建完数据库表后, 需要创建一个 Java 实体类来和表中字段相映射.

java 复制代码
@Data
public class MessageInfo {
    private int id;
    private String from;
    private String to;
    private String message;
    private int deleteFlag;
    private Date createTime;
    private Date updateTime;
}

2.4 定义 Mapper 接口

在这个练习中, 涉及到以下两个操作:

  1. 存储留言信息 => 将留言 insert 到 message_info 表中
  2. 查询留言信息 => 从 message_info 表中 select 数据

因此, 需要在 Mapper 接口中定义两个方法(这里使用注解完成数据库相关操作):

java 复制代码
@Mapper
public interface MessageMapper {
    @Select("select * from message_info where delete_flag = 0")
    List<MessageInfo> selectAll();

    // 注意: from 和 to 是关键字, 要使用 ` 引起来
    @Insert("insert into message_info (`from`, `to`, message) values (#{from}, #{to}, #{message})")
    Integer insert(MessageInfo messageInfo);
}

注意: 表中的 from 字段和 to 字段是 MySQL 的关键字, 因此若指定这两个字段进行 sql 操作时, 需要使用反引号(`)引起来.

2.5 controller

controller 层接收前端传来的参数, 对参数进行简单校验后, 将参数传递给 service 层, service 层返回结果后, 再将结果返回给前端.

java 复制代码
@RestController
@RequestMapping("/message")
public class MessageController {
    // 保存留言板信息
//    List<MessageInfo> list = new ArrayList<>();
    @Resource
    private MessageService messageService;
    // 接口一: 用户发表留言
    @PostMapping(value = "/publish", produces = "application/json")
    public String publish(@RequestBody MessageInfo messageInfo) {
        if(!StringUtils.hasLength(messageInfo.getFrom())
                || !StringUtils.hasLength(messageInfo.getTo())
                || !StringUtils.hasLength(messageInfo.getMessage())) {
            return "{\"ok\": 0}";
        }
//        list.add(messageInfo);
        int affectedRows = messageService.insert(messageInfo);
        return "{\"ok\": 1}";
    }
    // 接口二: 获取留言信息
    @GetMapping("/getList")
    public List<MessageInfo> getList() {
        return messageService.selectAll();
    }
}

2.6 service

service 层接收 controller 传来的数据, 调用 mapper 层完成数据库操作, 并将结果返回给 controller 层:

java 复制代码
@Service
public class MessageService {
    @Resource
    MessageMapper messageMapper;

    public List<MessageInfo> selectAll() {
        return messageMapper.selectAll();
    }

    public Integer insert(MessageInfo messageInfo) {
        return messageMapper.insert(messageInfo);
    }
}

controller, service, mapper 层的编写顺序并无要求, 根据个人习惯编写即可.

完成以上操作, 就对数据进行了持久化处理, 将数据保存到数据库中了. 即使重启服务器, 数据也不会丢失.

3. 图书管理系统

3.1 准备工作 - 配置相关

首先, 依旧需要引入 MyBatis 和 MySQL 驱动的相关依赖.

接着, 进行数据库连接和其他相关配置.

java 复制代码
spring:
  application:
    name: springboot-demo
  # 数据库配置
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/book_test?characterEncoding=utf8&useSSL=false
    username: root
    password: 111111
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  # 配置 mybatis xml 的⽂件路径,在 resources/mapper 创建所有表的 xml ⽂件
  mapper-locations: classpath:mapper/*Mapper.xml
  configuration:
    # 配置打印 MyBatis⽇志
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
    # 配置驼峰自动转换
    map-underscore-to-camel-case: true
#   将默认的日志级别修改为 info
logging:
  level:
    root: info

3.2 创建数据库表

3.2.1 创建用户表 + 图书表

sql 复制代码
-- 创建数据库
DROP DATABASE IF EXISTS book_test;

CREATE DATABASE book_test DEFAULT CHARACTER SET utf8mb4;

USE book_test;

-- 用户表
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, '民主与建设出版社');

3.3 创建 Java 类

为图书表和用户表创建相映射的 Java 实体类.

java 复制代码
@Data
public class UserInfo {
    private int id;
    private String username;
    private String password;
    private int delete_flag; // 0-正常 1-删除
    private Date createTime;
    private Date updateTime;
}
sql 复制代码
@Data
public class BookInfo {
    private Integer id;
    private String bookName;
    private String author;
    private Integer count;
    private BigDecimal price;
    private String publish;
    // 状态信息, 习惯上使用数字
    private Integer status; // 0-删除, 1-正常, 2-不允许借阅
    // 图书状态的中文表示
    // 开发中, 一般交给前端处理. 由于学习, 在后端这里直接就处理了
    private String statusCN;
    private String createTime;
    private String updateTime;
}

3.4 校验用户登录接口

用户登录时, 输入账号密码, 前端接收数据并通过 Ajax 请求将参数传递给后端接口, 后端 controller 层接收参数, 并传递给 service 层, service 调用 mapper 层查询数据库数据, 校验账号密码是否正确, 最终由 controller 层将校验结果返回给前端, 前端再进行相关处理将结果展示给用户.

java 复制代码
@RequestMapping("/user")
@RestController
public class UserController {
    @Resource
    UserService userService;
    // 登录验证接口
    @RequestMapping("/login")
    public boolean login(String name, String password, HttpSession session) {
        if(!StringUtils.hasLength(name) || !StringUtils.hasLength(password)) {
            return false;
        }
        UserInfo userInfo = userService.selectUserInfoByName(name);
        if(userInfo != null && userInfo.getPassword().equals(password)) {
            // 登录成功, 将用户信息保存在 Session 中
            // 保存之前. 隐藏用户密码(可选)
            userInfo.setPassword("****");
            session.setAttribute("user", userInfo);
            return true;
        }
        return false;
    }
}
java 复制代码
@Service
public class UserService {
    @Resource
    UserMapper userMapper;
    public UserInfo selectUserInfoByName(String name) {
        return userMapper.selectUserInfoByName(name);
    }
}

在 Mapper 接口中, 对于校验用户登录, 需要定义一个根据用户名查询用户信息的方法:

java 复制代码
@Mapper
public interface UserMapper {
    /**
     * 校验用户登录 : 根据用户名查询用户信息
     * @param name
     * @return
     */
    @Select("select * from user_info where user_name = #{name}")
    UserInfo selectUserInfoByName(String name);
}

3.5 添加图书

3.5.1 约定前后端交互接口

3.5.2 后端接口

添加图书, 就是将新图书的信息插入到图书表中.

前端收到用户所添加图书的图书信息后, 调用后端接口并传递图书信息, 后端接口在 controller service 层对图书信息进行校验, 最终在 Mapper 层将新图书信息插入到图书表中.

注意: 后端添加图书的接口, 使用的是一个 bookInfo 对象来接收的, 但是这并不意味着前端传来的就是 JSON 数据, 当前端传递的是多个参数的时, 后端也可以使用对象来接收:

  1. 前端传递多个参数, 放到 queryString 中或者以 form 表单的形式传递 => 后端对象接收(不使用注解)
  2. queryString 和 form 表单的数据传输格式都为: key1=value1&key2=value2&.... 但 queryString 位于 URL 中(GET 请求), form 表单数据位于 body 中(POST 请求)
  3. 前端传递 JSON 数据, 放到 body 中进行传递(POST 请求) => 后端对象接收(使用 @RequestBody)

3.5.3 前端代码

由于我们主攻后端, 这里就讲解一下前端代码中的核心部分:

这里使用了 JQuery 的 serialize 函数(序列化), 自动将选中的 form 中的数据导入到了 data 属性中.

图书添加成功后, 就会跳转到图书列表.

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>添加图书</title>
    <link rel="stylesheet" href="css/bootstrap.min.css">
    <link rel="stylesheet" href="css/add.css">

</head>

<body>

    <div class="container">

        <div class="form-inline">
            <h2 style="text-align: left; margin-left: 10px;"><svg xmlns="http://www.w3.org/2000/svg" width="40"
                    fill="#17a2b8" class="bi bi-book-half" viewBox="0 0 16 16">
                    <path
                        d="M8.5 2.687c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492V2.687zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783z" />
                </svg>
                <span>添加图书</span>
            </h2>
        </div>

        <form id="addBook">
            <div class="form-group">
                <label for="bookName">图书名称:</label>
                <input type="text" class="form-control" placeholder="请输入图书名称" id="bookName" name="bookName">
            </div>
            <div class="form-group">
                <label for="bookAuthor">图书作者</label>
                <input type="text" class="form-control" placeholder="请输入图书作者" id="bookAuthor" name="author" />
            </div>
            <div class="form-group">
                <label for="bookStock">图书库存</label>
                <input type="text" class="form-control" placeholder="请输入图书库存" id="bookStock" name="count"/>
            </div>

            <div class="form-group">
                <label for="bookPrice">图书定价:</label>
                <input type="number" class="form-control" placeholder="请输入价格" id="bookPrice" name="price">
            </div>

            <div class="form-group">
                <label for="bookPublisher">出版社</label>
                <input type="text" id="bookPublisher" class="form-control" placeholder="请输入图书出版社" name="publish" />
            </div>
            <div class="form-group">
                <label for="bookStatus">图书状态</label>
                <select class="custom-select" id="bookStatus" name="status">
                    <option value="1" selected>可借阅</option>
                    <option value="2">不可借阅</option>
                </select>
            </div>

            <div class="form-group" style="text-align: right">
                <button type="button" class="btn btn-info btn-lg" onclick="add()">确定</button>
                <button type="button" class="btn btn-secondary btn-lg" onclick="javascript:history.back()">返回</button>
            </div>
        </form>
    </div>
    <script type="text/javascript" src="js/jquery.min.js"></script>
    <!-- 实现前后端交互 -->
    <script>
        function add() {
            // 此时, 前端应进行参数校验, 此处省略
            // 前端向后端接口发送 Ajax 请求
            $.ajax({
                type: "post",
                url: "/book/addBook",
                data: $("#addBook").serialize(),
                success: function(body) {
                    if(body == "") {
                        alert("添加成功");
                        location.assign("book_list.html");
                    }else {
                        alert(body);
                    }
                }
            });
        }
    </script>
</body>

</html>

3.6 展示图书列表(分页展示)

当用户登录成功后, 就会来到图书列表界面.

图书系统中可能存储着大量的书籍, 在一个网页中是展示不完的, 因此需要对图书列表进行分页:

3.6.1 约定前后端交互接口

3.6.2 后端接口

首先, 先来回顾下分页查询的 sql 语句:

MySQL 中, 使用 LIMIT 关键字进行分页查询, 后面跟两个参数:

  1. 第一个参数为 offset, 表示偏移量(从第几个记录开始往后进行查询, 不包含 offset 本身)
  2. 第二个参数为 limit, 表示从 offset 后, 要查询的个数(每页中数据的个数)

并且, 可以根据页数和每页个数计算得出 offset.偏移量 = (当前页数 - 1) * (每页个数)

3.6.2.1 准备工作 - 参数接收和响应返回

后端接口必定需要接收 页码(currentPage) 以及每页的记录数(pageSize) 这两个参数, 因为只有知道了这两个参数, 才能计算得出 offset, 才能编写 sql 进行分页查询.

因此, 可以新建一个类专门用来接收请求中的参数:

java 复制代码
@Data
public class RequestPage {
    // 当前端没有传值时, 默认当前页是 1, 默认一页的大小是 10 条记录
    // 查询哪一页
    private int currentPage = 1;
    // 每页中有多少条记录
    private int pageSize = 10;
    // 计算得到偏移量
    private int offset;

    // 根据当前页和每页的个数, 计算 offset
    public int getOffset() {
        return this.offset = (this.currentPage - 1) * this.pageSize;
    }
}

注意: 如果 Mapper 方法参数是一个对象, 那么 #{} 是根据 get 方法获取对象属性值的, 因此我们在 offset 的 get 方法中, 计算得到 offset 返回即可.

注意: offset 的值必须通过 get 方法得到, 不能在构造方法中计算 offset 的值, 因为构造方法只能执行一次, currentPage 和 pageSize 已经有了默认值, 那么在构造对象时, offset 就会在构造方法中根据 currentPage 和 pageSize 的默认值被计算定型(offset 就会始终为 (1 - 1) * 10 = 0!!), 即使后续前端对 currentPage 和 pageSize 值进行了传递更改, offset 的值仍然不会改变!! 而通过 get 方法获取 offset, 每次获取到的都是最新值!!

此外, 根据接口文档, 响应结果包含了 total(表中记录总数) 和 List<BookIfo>(当前页中的图书信息) 两个属性, 因此, 可以新建一个类, 返回该类的对象作为响应结果:

java 复制代码
@AllArgsConstructor
@NoArgsConstructor
@Data
public class ResponseResult<T> {
    // 表中记录的总数
    private int total;
    // 当前页中的图书信息
    private List<T> records;
    // 把请求内容放到响应中返回, 以便前端后续查询
    private RequestPage requestPage;
}
3.6.2.2 编写 Mapper 层方法

有了以上准备后, 就可以编写 Mapper 层了:

3.6.2.3 controller + service

接收到前端传来的 currentPage 和 pageSize 后, 我们直接在 service 层调用 Mapper 方法, 进行 count 计数和分页查询, 并将结果打包到 ResponseBody 对象中返回即可.

此外, 在 service 中, 还需要对分页查询得到的图书的 statusCN 属性依据 status 的值进行处理(这里通过枚举类):

枚举类:

3.6.3 前端代码

前端代码中, 这里使用了一个分页组件: https://jqpaginator.keenwon.com/

这里仍然只讲一下前端代码中的核心逻辑:

组件相关:

location.search 可以获取 URL 中 queryString 的信息(包括 ? ):

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>图书列表展示</title>
    <link rel="stylesheet" href="css/bootstrap.min.css">

    <link rel="stylesheet" href="css/list.css">
    <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>

</head>

<body>
    <div class="bookContainer">
        <h2>图书列表展示</h2>
        <div class="navbar-justify-between">
            <div>
                <button class="btn btn-outline-info" type="button" onclick="location.href='book_add.html'">添加图书</button>
                <button class="btn btn-outline-info" type="button" onclick="batchDelete()">批量删除</button>
            </div>
        </div>

        <table>
            <thead>
                <tr>
                    <td>选择</td>
                    <td class="width100">图书ID</td>
                    <td>书名</td>
                    <td>作者</td>
                    <td>数量</td>
                    <td>定价</td>
                    <td>出版社</td>
                    <td>状态</td>
                    <td class="width200">操作</td>
                </tr>
            </thead>
            <tbody>
                
            </tbody>
        </table>

        <div class="demo">
            <ul id="pageContainer" class="pagination justify-content-center"></ul>
        </div>
        <script>

            getBookList();
            function getBookList() {
                $.ajax({
                    url: "/book/getListByPage" + location.search,
                    type: "get",
                    success: function(res) {
                        if(res == null || res.records == null) {
                            return;
                        }
                        var books = res.records;
                        var newHtml = '';
                        for(var book of books) {
                            newHtml += '<tr>';
                            newHtml += '<td><input type="checkbox"name="selectBook" value="' + book.id + '" id="selectBook" class="book-select"></td>';
                            newHtml += '<td>' + book.id + '</td>';
                            newHtml += '<td>' + book.bookName + '</td>';
                            newHtml += '<td>' + book.author + '</td>';
                            newHtml += '<td>' + book.count + '</td>';
                            newHtml += '<td>' + book.price + '</td>';
                            newHtml += '<td>' + book.publish + '</td>';
                            newHtml += '<td>' + book.statusCN + '</td>';
                            newHtml += '<td><div class="op">';
                            newHtml += '<a href="book_update.html?id=' + book.id + '">修改</a>';
                            newHtml += '<a href="javascript:void(0)" onclick="deleteBook(' + book.id + ')">删除</a>';
                            newHtml += '</div></td></tr>';
                        }
                        // .html => 置换 tbody 标签里面的内容
                        $("tbody").html(newHtml);
                        //翻页信息
                        $("#pageContainer").jqPaginator({
                            totalCounts: res.total, //总记录数
                            pageSize: 10,    //每页的个数
                            visiblePages: 5, //可视页数
                            currentPage: res.requestPage.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) {
                                if(type == "change") {
                                    location.assign("book_list.html?currentPage=" + page);
                                }
                            }
                        });
                    }
                });
            }
    
            function deleteBook(id) {
                var isDelete = confirm("确认删除?");
                if (isDelete) {
                    $.ajax({
                        url: "/book/deleteBookById?id=" + id,
                        type: "post",
                        success: function(result) {
                            if(result == "") {
                                //删除图书
                                alert("删除成功!!");
                                location.assign("book_list.html");
                            }else {
                                alert("删除失败!! " + result);
                            }
                        }
                    });
                    
                }
            }
            function batchDelete() {
                var isDelete = confirm("确认批量删除?");
                if (isDelete) {
                    //获取复选框的id
                    var ids = [];
                    $("input:checkbox[name='selectBook']:checked").each(function () {
                        ids.push($(this).val());
                    });
                    console.log(ids);
                    alert("批量删除成功");
                }
            }

        </script>
    </div>
</body>

</html>

3.7 更新图书信息

3.7.1 约定前后端交互接口

进入更新图书信息的页面时, 需要先展示原来的图书信息, 然后用户选择性的对图书信息进行更新.

因此我们后端需要提供两个接口:

  1. 根据 id 查询图书信息接口
  2. 更新图书信息接口

3.7.2 后端接口

3.7.2.1 Mapper 层

根据 id 查询图书信息的接口, 不必多说.

但是更新图书信息的接口, 由于用户是选择性的更新图书信息, 因此需要编写动态 sql 来完成:

3.7.2.2 controller + service

3.7.3 前段代码

  1. 更新图书信息的 html 文件中, 首先需要调用后端 selectById 接口, 通过 id 查询图书信息, 将原本的图书信息展示在页面中
  2. 用户填写要修改内容, 前端将这些数据发送给后端, 后端接口进行 update 操作.

核心框架如下:

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>修改图书</title>
    <link rel="stylesheet" href="css/bootstrap.min.css">
    <link rel="stylesheet" href="css/add.css">
</head>

<body>

    <div class="container">
        <div class="form-inline">
            <h2 style="text-align: left; margin-left: 10px;"><svg xmlns="http://www.w3.org/2000/svg" width="40"
                    fill="#17a2b8" class="bi bi-book-half" viewBox="0 0 16 16">
                    <path
                        d="M8.5 2.687c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492V2.687zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783z" />
                </svg>
                <span>修改图书</span>
            </h2>
        </div>

        <form id="updateBook">
            <input type="hidden" class="form-control" id="bookId" name="id">
            <div class="form-group">
                <label for="bookName">图书名称:</label>
                <input type="text" class="form-control" id="bookName" name="bookName">
            </div>
            <div class="form-group">
                <label for="bookAuthor">图书作者</label>
                <input type="text" class="form-control" id="bookAuthor" name="author"/>
            </div>
            <div class="form-group">
                <label for="bookStock">图书库存</label>
                <input type="text" class="form-control" id="bookStock" name="count"/>
            </div>
            <div class="form-group">
                <label for="bookPrice">图书定价:</label>
                <input type="number" class="form-control" id="bookPrice" name="price">
            </div>
            <div class="form-group">
                <label for="bookPublisher">出版社</label>
                <input type="text" id="bookPublisher" class="form-control" name="publish"/>
            </div>
            <div class="form-group">
                <label for="bookStatus">图书状态</label>
                <select class="custom-select" id="bookStatus" name="status">
                    <option value="1" selected>可借阅</option>
                    <option value="2">不可借阅</option>
                </select>
            </div>
            <div class="form-group" style="text-align: right">
                <button type="button" class="btn btn-info btn-lg" onclick="update()">确定</button>
                <button type="button" class="btn btn-secondary btn-lg" onclick="javascript:history.back()">返回</button>
            </div>
        </form>
    </div>
    <script type="text/javascript" src="js/jquery.min.js"></script>
    <script>
        getBookInfo();
        function getBookInfo() {
            // 进入修改页面后, 先展示该图书原来的信息
            $.ajax({
                // 从路径中获取要修改的图书的 id(从 book_list.html 的 "修改" 超链接跳转过来的)
                url: "/book/selectById" + location.search,
                success: function(result) {
                    if(result == null) {
                        return;
                    }
                    // 将要修改的图书 id, 记录在隐藏标签中, 以便后续根据 id 进行 update 操作
                    $("#bookId").val(result.id),
                    $("#bookName").val(result.bookName),
                    $("#bookAuthor").val(result.author),
                    $("#bookStock").val(result.count),
                    $("#bookPrice").val(result.price),
                    $("#bookPublisher").val(result.publish),
                    $("#bookStatus").val(result.status)
                }
            });
        }
        // 将用户做出的修改, 发送给后端接口
        function update() {
            // 此处, 应对用户的输入进行校验, 这里暂且忽略.
            $.ajax({
                url: "/book/updateBook",
                type: "post",
                data: $("#updateBook").serialize(),
                success: function(result) {
                    if(result == "") {
                        alert("更新成功!!");
                        location.assign("book_list.html");
                    }else {
                        alert("更新失败!!");
                        location.assign("book_list.html");
                    }
                }
            });
        }
    </script>
</body>

</html>

3.8 删除图书信息

3.8.1 约定前后端交互接口

3.8.2 后端接口

删除图信息, 本质是上就将图书对象中的 status 属性修改为 0.

可以和更新图书信息操作共用一个 Mapper 接口, 仅对 status 属性进行修改即可.

因此, Mapper 层可以不做修改, 只需封装一个 controller 和 service 即可.

3.8.3 前端代码

javascript 复制代码
            function deleteBook(id) {
                var isDelete = confirm("确认删除?");
                if (isDelete) {
                    $.ajax({
                        url: "/book/deleteBookById?id=" + id,
                        type: "post",
                        success: function(result) {
                            if(result == "") {
                                //删除图书
                                alert("删除成功!!");
                                location.assign("book_list.html");
                            }else {
                                alert("删除失败!! " + result);
                            }
                        }
                    });
                }
            }

3.9 批量删除图书信息

3.9.1 约定前后端交互接口

3.9.2 后端接口

批量删除图书和删除图书, 本质上都是 update 操作, 都是将图书表中对应图书的 status 字段设为 0.

不同的是, 批量删除图书需要根据用户选择的图书, 批量的进行删除, 也就需要编写动态 SQL.

3.9.3 前端代码

javascript 复制代码
            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) {
                                location.assign("book_list.html");
                            }else {
                                alert("删除失败!!");
                            }
                        }
                    });
                }
            }

3.10 强制登录机制

到目前为止, 图书管理系统的所有功能已经完成了, 但是有一个明显的 bug --- 即使用户没有登录, 也可以通过 book_list.html 路径或者后端接口路径直接访问图书管理系统:

这是一个非常严重的安全问题, 我们需要实现对用户进行强制登录的功能.

3.10.1 后端接口

解决以上安全问题, 就需要借助Cookie-Session 机制.

3.10.1.1 封装常量

其实我们在校验用户登录接口中, 已经将用户信息存储到了 Session 中:

但是之前存储用户 Session 时, 我们是直接将 key 设置为一个字符串("user"), 这个方法是不友好的, 因为后续接口是也通过 key 来获取 Session 的, 当这个字符串(key)改变时, 那接口中 getAttribute 中的 key 也得做出相应的改变, 因此, 我们可以将这个常量 key(当然不限于此)封装到一个类中, 实现 key 值对代码的解耦:

3.10.1.2 封装响应结果

我们可以借助服务端的 Session 和客户端的 Sessionid, 完成强制登录操作.

当用户后续向后端服务器发送请求时, 请求中都会携带 Sessionid, 那我们就可以在后端接口中, 根据用户的 Sessionid 进行用户校验操作, 即根据 Sessionid 查找对应的 Session, 如果 Session 存在, 那么就说明该用户登录过, 后端就给予正常响应; 否则, 对用户进行强制登录操作.

当无法从 Session 获取到用户信息时(上图第3步), 说明用户未登录, 此时我们应该返回一些错误提示信息, 告诉前端用户还未登录, 应将页面跳转到登录页面, 对用户进行强制登录操作.

当然, 响应结果不是只有用户未登录这一个结果, 当然还有后端接口内部错误, 以及用户已登录并操作成功(如添加图书成功)等等...

因此, 我们可以对返回结果再次进行封装:

  1. code: 业务状态码, 不同的值代表不同状态

  2. errMsg: 错误信息描述, 告诉前端错误原因是什么

  3. data: 真实的业务数据(上文的 ResponseBody)

假设本例(图书系统)中的业务状态码含义如下:

  1. code = 200: 结果正常/操作成功

  2. code = 0: 用户未登录

  3. code = -1: 后端内部错误

因此, 我们可以将 code 使用枚举类进行封装:

此时, 后端接口返回响应时, 返回一个 Result 对象即可.

但是每个接口返回时, 都需要 new 一个 Result 对象, 这样观感上会觉得代码冗余. 因此为了简化代码, 我们可以将不同的响应结果都封装到 Result 类中(不同结果对应一个 Result 方法), 接口返回响应时, 直接调用 Result 中的方法即可, 不需在接口中 new Result 再返回:

这里只展示查询图书列表接口的强制登录代码, 剩下接口的代码就不一一展示了.

到这里, 我们就完成了强制用户登陆的后端代码, 我们可以通过 postman/浏览器 在还未登录的情况下, 访问后端查询图书列表的接口, 观察结果(提示用户未登录):

然后, 我们进行登录操作, 再次访问该后端接口:

登录完毕后, 服务器就创建了 Session 并存储了用户信息, 并通过 set-cookie 向客户端发送了 Sessionid, 用户再次发起请求时, 请求中就会携带 Sessionid, 服务器通过 Sessionid 找到对应的 Session , 就能识别到用户已登录, 就能正常响应结果了.

3.10.2 前端代码

由于我们对后端响应结果进行了封装, 因此前端接收的结果也就发生了改变, 我们需要对前端代码进行简单调整, 并对未登录且越权访问的用户跳转到登录界面实施强转登录操作:

到这里, 图书管理系统大功告成!!


END

相关推荐
Eugene__Chen11 分钟前
java IO/NIO/AIO
java·python·nio
xixixin_25 分钟前
【uniapp】uni.setClipboardData 方法失效 bug 解决方案
java·前端·uni-app
工业互联网专业29 分钟前
基于springboot+vue的校园二手物品交易平台
java·vue.js·spring boot·毕业设计·源码·课程设计·校园二手物品交易平台
isfox36 分钟前
一文拆解 Java CAS:从原理到避坑全攻略
java
JPC客栈43 分钟前
LeetCode面试经典 150 题(Java题解)
java·leetcode·面试
HyperAI超神经1 小时前
【vLLM 学习】Aqlm 示例
java·开发语言·数据库·人工智能·学习·教程·vllm
异常驯兽师1 小时前
IntelliJ IDEA 项目导入后 Java 文件图标显示为红色小写 j 的解决方法
java·路径配置
纪元A梦1 小时前
华为OD机试真题——数据分类(2025A卷:100分)Java/python/JavaScript/C++/C语言/GO六种最佳实现
java·javascript·c++·python·华为od·go·华为od机试题
常年游走在bug的边缘1 小时前
基于spring boot 集成 deepseek 流式输出 的vue3使用指南
java·spring boot·后端·ai
熙客2 小时前
Java并发:线程池
java