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.pswp.cn/bicheng/73927.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/73927.shtml
英文地址,請注明出處:http://en.pswp.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;還難以保證美觀性和一致性。本文將深入解析如何實現自動布…