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=falseusername: rootpassword: 111111driver-class-name: com.mysql.cj.jdbc.Drivermybatis:# 配置 mybatis xml 的⽂件路径,在 resources/mapper 创建所有表的 xml ⽂件mapper-locations: classpath:mapper/*Mapper.xmlconfiguration:# 配置打印 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<>();@Resourceprivate 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 {@ResourceMessageMapper 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=falseusername: rootpassword: 111111driver-class-name: com.mysql.cj.jdbc.Drivermybatis:# 配置 mybatis xml 的⽂件路径,在 resources/mapper 创建所有表的 xml ⽂件mapper-locations: classpath:mapper/*Mapper.xmlconfiguration:# 配置打印 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 {@ResourceUserService 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 {@ResourceUserMapper 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"><pathd="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;// 根据当前页和每页的个数, 计算 offsetpublic 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) {//获取复选框的idvar 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"><pathd="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) {//获取复选框的idvar 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.mzph.cn/bicheng/73927.shtml

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

相关文章

Godot读取json配置文件

概述 在Godot 4.3中读取JSON配置文件&#xff0c;可以通过以下步骤实现&#xff1a; 步骤说明 读取文件内容&#xff1a;使用FileAccess类打开并读取JSON文件。 解析JSON数据&#xff1a;使用JSON类解析读取到的文本内容。 错误处理&#xff1a;处理文件不存在或JSON格式错…

RabbitMQ八股文

RabbitMQ 核心概念与组件 1. RabbitMQ 核心组件及其作用 1.1 生产者&#xff08;Producer&#xff09; 作用&#xff1a;创建并发送消息到交换机。特点&#xff1a;不直接将消息发送到队列&#xff0c;而是通过交换机路由。 1.2 交换机&#xff08;Exchange&#xff09; 作…

C语言每日一练——day_7

引言 针对初学者&#xff0c;每日练习几个题&#xff0c;快速上手C语言。第七天。&#xff08;连续更新中&#xff09; 采用在线OJ的形式 什么是在线OJ&#xff1f; 在线判题系统&#xff08;英语&#xff1a;Online Judge&#xff0c;缩写OJ&#xff09;是一种在编程竞赛中用…

C#原型模式:通过克隆对象来优化创建过程

在软件开发中&#xff0c;创建对象是非常常见的操作。然而&#xff0c;在某些情况下&#xff0c;构造对象的过程可能非常复杂或耗时&#xff0c;特别是当对象的创建涉及多个步骤或者需要初始化大量数据时。为了解决这个问题&#xff0c;**原型模式&#xff08;Prototype Patter…

ArcGIS10. 8简介与安装,附下载地址

目录 ArcGIS10.8 1. 概述 2. 组成与功能 3. 10.8 特性 下载链接 安装步骤 1. 安装准备 2. 具体步骤 3.补丁 其他版本安装 ArcGIS10.8 1. 概述 ArcGIS 10.8 是由美国 Esri 公司精心研发的一款功能强大的地理信息系统&#xff08;GIS&#xff09;平台。其核心功能在于…

Mac:JMeter 下载+安装+环境配置(图文详细讲解)

&#x1f4cc; 下载JMeter 下载地址&#xff1a;https://jmeter.apache.org/download_jmeter.cgi &#x1f4cc; 无需安装 Apache官网下载 JMeter 压缩包&#xff0c;无需安装&#xff0c;下载解压后放到自己指定目录下即可。 按我自己的习惯&#xff0c;我会在用户 jane 目…

【PCB工艺】基础:电子元器件

电子原理图&#xff08;Schematic Diagram&#xff09;是电路设计的基础&#xff0c;理解电子元器件和集成电路&#xff08;IC&#xff09;的作用&#xff0c;是画好原理图的关键。 本专栏将系统讲解 电子元器件分类、常见 IC、电路设计技巧&#xff0c;帮助你快速掌握电子电路…

nvm 安装某个node.js版本后不能使用或者报错,或不能使用npm的问题

安装了nvm之后发现不能使用某个版本的node.js&#xff0c;报错之后&#xff0c;不能使用npm这个命令。可以这样解决&#xff1a; 1、再node.js官网直接下载node.js 的压缩包。 找到nvm的安装目录 2、直接将文件夹解压到这个安装目录中修改一下名字即可。

【MySQL数据库】多表查询(笛卡尔积现象,联合查询、内连接、左外连接、右外连接、子查询)-通过练习快速掌握法

在DQL的基础查询中&#xff0c;我们已经学过了多表查询的一种&#xff1a;联合查询&#xff08;union&#xff09;。本文我们将系统的讲解多表查询。 笛卡尔积现象 首先&#xff0c;我们想要查询emp表和stu表两个表&#xff0c;按照我们之前的知识栈&#xff0c;我们直接使用…

Java:Apache HttpClient中HttpRoute用法的介绍

当使用Apache HttpClient组件时&#xff0c;经常会用到它的连接池组件。典型的代码如下&#xff1a; PoolingHttpClientConnectionManager connectionManager new PoolingHttpClientConnectionManager();connectionManager.setMaxTotal(httpConfig.getMaxPoolTotal());connect…

RHCE(RHCSA复习:npm、dnf、源码安装实验)

七、软件管理 7.1 rpm 安装 7.1.1 挂载 [rootlocalhost ~]# ll /mnt total 0 drwxr-xr-x. 2 root root 6 Oct 27 21:32 hgfs[rootlocalhost ~]# mount /dev/sr0 /mnt #挂载 mount: /mnt: WARNING: source write-protected, mounted read-only. [rootlocalhost ~]# [rootlo…

分布式的消息流平台之Pulsar

Pulsar 流处理详解 Apache Pulsar 是一个分布式的消息流平台&#xff0c;集成了**消息队列&#xff08;MQ&#xff09;和流处理&#xff08;Stream Processing&#xff09;**能力。Pulsar 不仅提供低延迟、高吞吐的消息传输能力&#xff0c;还支持基于 Pulsar Functions、Flin…

【C++多线程】thread

C中的std::thread是C11引入的线程库的一部分&#xff0c;提供了创建和管理线程的能力。它封装了操作系统的线程接口&#xff0c;使得在C中更方便地进行多线程编程。 1. std::thread 的定义 std::thread 类位于<thread>头文件中&#xff0c;定义在std命名空间下&#xff…

【css酷炫效果】纯CSS实现故障文字特效

【css酷炫效果】纯CSS实现故障文字特效 缘创作背景html结构css样式完整代码基础版进阶版(3D效果) 效果图 想直接拿走的老板&#xff0c;链接放在这里&#xff1a;https://download.csdn.net/download/u011561335/90492053 缘 创作随缘&#xff0c;不定时更新。 创作背景 刚…

uniapp配置代理解决跨域问题

两种方式&#xff1a; 1、manifest.json中配置 "h5" : {"template" : "static/index.html","devServer" : {"port" : 9090,"https" : false,"proxy":{"/prod-api":{"target":&quo…

物联网为什么用MQTT不用 HTTP 或 UDP?

先来两个代码对比&#xff0c;上传温度数据给服务器。 MQTT代码示例 // MQTT 客户端连接到 MQTT 服务器 mqttClient.connect("mqtt://broker.server.com:8883", clientId) // 订阅特定主题 mqttClient.subscribe("sensor/data", qos1) // …

Flutter:页面滚动,导航栏背景颜色过渡动画

记录&#xff1a;导航默认透明&#xff0c;页面发生滚动后&#xff0c;导航背景色由0-1&#xff0c;过渡到白色背景。 view import package:ducafe_ui_core/ducafe_ui_core.dart; import package:flutter/material.dart; import package:get/get.dart; import package:redo…

STM32 —— MCU、MPU、ARM、FPGA、DSP

在嵌入式系统中&#xff0c;MCU、MPU、ARM、FPGA和DSP是核心组件&#xff0c;各自在架构、功能和应用场景上有显著差异。以下从专业角度详细解析这些概念&#xff1a; 一、 MCU&#xff08;Microcontroller Unit&#xff0c;微控制器单元&#xff09; 核心定义 集成系统芯片&a…

批量删除 PPT 空白幻灯片页面

如果我们需要删除 PPT 文档中的空白幻灯片页面&#xff0c;我们可以借助 Office 工具来完成&#xff0c;但是如果是大量的 PPT 文档需要批量删除空白幻灯片页面&#xff0c;那就需要使用专业的批量处理工具来完成&#xff0c;今天就给大家介绍一种批量删除 PPT 空白幻灯片页面的…

【canvas】一键自动布局:如何让流程图节点自动找到最佳位置

一键自动布局&#xff1a;如何让流程图节点自动找到最佳位置 引言 在流程图、拓扑图和系统架构图设计中&#xff0c;节点布局往往是最令人头疼的问题。如果手动调整每个节点位置&#xff0c;不仅耗时费力&#xff0c;还难以保证美观性和一致性。本文将深入解析如何实现自动布…