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.rhkb.cn/news/36327.html

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

相关文章

PCAN安装驱动、使用PcanView监听发送报文

首先将PCAN插入电脑USB接口&#xff0c;winR快捷键输入compmgmt.msc&#xff0c;弹出计算机管理界面&#xff0c;装过驱动则显示PCAN-USB。未装过驱动的会显示XCAN-USB&#xff0c;按照如片步骤安装驱动&#xff0c;这里不在文字赘述。 PCAN Windows驱动下载&#xff1a; htt…

Pytest项目_day01(HTTP接口)

HTTP HTTP是一个协议&#xff08;服务器传输超文本到浏览器的传送协议&#xff09;&#xff0c;是基于TCP/IP通信协议来传输数据&#xff08;HTML文件&#xff0c;图片文件&#xff0c;查询结果等&#xff09;。 访问域名 例如www.baidu.com就是百度的域名&#xff0c;我们想…

利用knn算法实现手写数字分类

利用knn算法实现手写数字分类 1.作者介绍2.KNN算法2.1KNN&#xff08;K-Nearest Neighbors&#xff09;算法核心思想2.2KNN算法的工作流程2.3优缺点2.4 KNN算法图示介绍 3.实验过程3.1安装所需库3.2 MNIST数据集3.3 导入手写数字图像进行分类3.4 完整代码3.5 实验结果 1.作者介…

HTML中滚动加载的实现

设置div的overflow属性&#xff0c;可以使得该div具有滚动效果&#xff0c;下面以div中包含的是table来举例。 当table的元素较多&#xff0c;以至于超出div的显示范围的话&#xff0c;观察下该div元素的以下3个属性&#xff1a; clientHeight是div的显示高度&#xff0c;scrol…

ubuntu20.04系统没有WiFi图标解决方案_安装Intel网卡驱动

文章目录 1. wifi网卡配置1.1 安装intel官方网卡驱动backport1.1.1 第四步可能会出现问题 1.2 ubuntu官方的驱动1.3 重启 1. wifi网卡配置 我的电脑是华硕天选4&#xff08;i7&#xff0c;4060&#xff09;&#xff0c;网卡型号intel ax201 ax211 ax210通用。 参考文章&#…

Anacoda进入自己的集成环境CLI中

鼠标左键单击那个"播放"图标&#xff0c;在弹出的菜单中选择"Open Terminal"&#xff0c;即可进入。 还有一种是通过Scripts/active.bat文件的方式&#xff0c;是Windows下的方案&#xff0c;在当前目录下执行cmd&#xff0c;输入active切换Anacoda集成环…

设备健康管理系统是什么,设备健康管理系统多少钱?

想象一下&#xff0c;你的汽车在仪表盘报警前 3 天&#xff0c;手机就收到 “发动机轴承剩余寿命 1500 公里” 的提醒 —— 这就是 ** 设备健康管理系统&#xff08;EHM&#xff09;** 的日常。在制造业&#xff0c;设备故障每年造成全球 3.4 万亿美元损失&#xff0c;而 80% 的…

解锁智慧养老新可能,全面提升养老生活质量

在老龄化浪潮席卷全球的今天&#xff0c;如何让老年人的生活更加安全、便捷、丰富多彩&#xff0c;成为了我们共同的责任与追求。辉视智慧养老方案&#xff0c;正是这样一款以老年人需求为核心&#xff0c;集信息查询、活动参与、紧急对讲与安全保障于一体的智慧养老解决方案。…

Error: The project seems to require pnpm but it‘s not installed.

Error: The project seems to require pnpm but it‘s not installed 原因 该错误信息表明你的项目需要使用 pnpm 作为包管理工具&#xff0c;但系统中尚未安装 pnpm。 解决方法 【1】删除pnpm.lock 【2】npm install -g pnpm 之后再重新启动

Zabbix监控自动化(Zabbix Mnitoring Automation)

​​​​​​zabbix监控自动化 1、自动化监控(网络发现与自动注册只能用其一) 1.1 ansible安装zabbix agent 新采购100台服务器&#xff1a; 1、安装操作系统 2、初始化操作系统 3、安装zabbix agent 1.手动部暑 2.脚本部暑(shell expect) 3.ansible 4、纳入监控 1.…

JVM垃圾回收

1. Java垃圾回收机制 为了让程序员更专注于代码的实现&#xff0c;而不用过多的考虑内存释放的问题&#xff0c;所以&#xff0c;在Java语言中&#xff0c;有了自动的垃圾回收机制&#xff0c;也就是我们熟悉的GC(Garbage Collection)。有了垃圾回收机制后&#xff0c;程序员只…

jmeter--(吞吐量控制器)逻辑控制器

在 JMeter 中&#xff0c;吞吐量控制器&#xff08;Throughput Controller&#xff09; 是一种逻辑控制器&#xff0c;用于控制其子节点&#xff08;请求、逻辑控制器等&#xff09;的执行次数或百分比&#xff0c;从而调整测试计划的吞吐量。它通常用于模拟不同比例的用户行为…

SpringBoot3实战(SpringBoot3+Vue3基本增删改查、前后端通信交互、配置后端跨域请求、数据批量删除(超详细))(3)

目录 一、从0快速搭建SpringBoot3工程、SpringBoot3集成MyBatis、PageHelper分页查询的详细教程。(博客链接) 二、实现前端与后端通信对接数据。(axios工具) &#xff08;1&#xff09;安装axios。(vue工程目录) &#xff08;2&#xff09;封装请求工具类。(request.js) <1&…

Atom of Thoughts for Markov LLM Test-Time Scaling论文解读

近年来&#xff0c;大型语言模型在训练规模的扩展上取得了显著的性能提升。然而&#xff0c;随着模型规模和数据量的增长遇到瓶颈&#xff0c;测试时扩展&#xff08;test-time scaling&#xff09;成为进一步提升模型能力的新方向。传统的推理方法&#xff0c;如思维链&#x…

前端字段名和后端不一致?解锁 JSON 映射的“隐藏规则” !!!

&#x1f680; 前端字段名和后端不一致&#xff1f;解锁 JSON 映射的“隐藏规则” &#x1f31f; 嘿&#xff0c;技术冒险家们&#xff01;&#x1f44b; 今天我们要聊一个开发中常见的“坑”&#xff1a;前端传来的 JSON 参数字段名和后端对象字段名不一致&#xff0c;会发生…

AI训练如何获取海量数据,论平台的重要性

引言&#xff1a;数据——AI时代的“新石油” 在人工智能和大模型技术飞速发展的今天&#xff0c;数据已成为驱动技术进步的 “ 燃料 ”。无论是训练聊天机器人、优化推荐算法&#xff0c;还是开发自动驾驶系统&#xff0c;都需要海量、多样化的数据支持。 然而&#xff0c;获…

k8s的存储

一 configmap 1.1 configmap的功能 configMap用于保存配置数据&#xff0c;以键值对形式存储。 configMap 资源提供了向 Pod 注入配置数据的方法。 镜像和配置文件解耦&#xff0c;以便实现镜像的可移植性和可复用性。 etcd限制了文件大小不能超过1M 1.2 configmap的使用…

递归、搜索与回溯第三讲:综合练习

递归、搜索与回溯第三讲&#xff1a;综合练习 1.找出所有子集的异或总和再求和2.全排列3.电话号码的字母组合4.组合5.目标和6.组合总和7.字母大小写全排列8.优美的排列9.N皇后10.有效的数独11.括号生成12.解数独13.单词搜索14.黄金矿工15.不同路径III 有决策树的递归总结&#…

Excel 小黑第12套

对应大猫13 涉及金额修改 -数字组 -修改会计专用 VLOOKUP函数使用&#xff08;查找目标&#xff0c;查找范围&#xff08;F4 绝对引用&#xff09;&#xff0c;返回值的所在列数&#xff0c;精确查找或模糊查找&#xff09;双击填充柄就会显示所有值 这个逗号要中文的不能英…

AI重构工程设计、施工、总承包行业:从智能优化到数字孪生的产业革命

摘要 AI正深度重构工程设计、施工与总承包行业&#xff0c;推动从传统经验驱动向数据智能驱动的转型。本文系统性解析AI当前在智能优化设计、施工过程管理、全生命周期数字孪生等场景的应用&#xff0c;展望未来AI在自动化决策、跨域协同等领域的潜力&#xff0c;并从投入产出…