当前位置: 首页 > article >正文

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

目录

最终效果展示

代码 Gitee 地址

1. 引言

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

 2.1 准备工作 - 配置相关

2.2 创建留言表

2.3 创建 Java 类

2.4 定义 Mapper 接口

2.5 controller

2.6 service 

3. 图书管理系统

3.1 准备工作 - 配置相关

3.2 创建数据库表

3.2.1 创建用户表 + 图书表

3.3 创建 Java 类

3.4 校验用户登录接口

3.5 添加图书

3.5.1 约定前后端交互接口

3.5.2 后端接口

3.5.3 前端代码

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

 3.6.1 约定前后端交互接口

3.6.2 后端接口

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

3.6.2.2 编写 Mapper 层方法

3.6.2.3 controller + service

3.6.3 前端代码

3.7 更新图书信息

3.7.1 约定前后端交互接口

3.7.2 后端接口

3.7.2.1 Mapper 层

3.7.2.2 controller + service

3.7.3 前段代码

3.8 删除图书信息

3.8.1 约定前后端交互接口

3.8.2 后端接口 

3.8.3 前端代码

3.9 批量删除图书信息

3.9.1 约定前后端交互接口

3.9.2 后端接口

3.9.3 前端代码

3.10 强制登录机制

3.10.1 后端接口

3.10.1.1 封装常量

 3.10.1.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 驱动的相关依赖.

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

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):

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 实体类来和表中字段相映射.

@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 接口中定义两个方法(这里使用注解完成数据库相关操作):

@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 层返回结果后, 再将结果返回给前端.

@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 层:

@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 驱动的相关依赖.

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

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 创建用户表 + 图书表

-- 创建数据库
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 实体类.

@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;
}
@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 层将校验结果返回给前端, 前端再进行相关处理将结果展示给用户.

@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;
    }
}
@Service
public class UserService {
    @Resource
    UserMapper userMapper;
    public UserInfo selectUserInfoByName(String name) {
        return userMapper.selectUserInfoByName(name);
    }
}

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

@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 属性中.

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

<!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 进行分页查询.

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

@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>(当前页中的图书信息) 两个属性, 因此, 可以新建一个类, 返回该类的对象作为响应结果:

@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 的信息(包括 ? ):

<!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 操作.

 核心框架如下:

<!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 前端代码

            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 前端代码

            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


http://www.kler.cn/a/590955.html

相关文章:

  • ESMFold 安装教程
  • (6)用于无GPS导航的Nooploop
  • 音视频处理的“瑞士军刀”与“积木”:FFmpeg 与 GStreamer 的深度揭秘
  • leetcode236-二叉树的公共祖先
  • Django REST Framework中的序列化器类和视图类
  • 基于深度学习的OCR+NLP,医疗化验单智能识别方案
  • MPC算法路径跟踪_Matlab实现
  • HTML基础内容2
  • C#运算符与表达式:从入门到游戏伤害计算实践
  • debian12.9 kde切到x11安装搜狗输入法遇到的问题
  • Docker Compose 和 Kubernetes(K8s)对比
  • 工作记录 2017-02-03
  • redis十大应用数据类型具体使用及其应用
  • 蓝桥杯每日五题第一日
  • 每日学习Java之一万个为什么(待补充)
  • LeetCode算法题(Go语言实现)_04
  • 【AVRCP】蓝牙协议栈深度解析:AVCTP互操作性核心机制与实现细节
  • 安全帽二维码:如何提升施工现场的安全管控效果
  • linux 命令 touch
  • 提示词工程化学习,方便用于dify