目录
前⾯图书管理系统, 咱们只完成了⽤⼾登录和图书列表, 并且数据是模拟的. 接下来我们把其他功能进⾏完善.
这里没图书管理系统之前模板的可以参考我之前博客[SpringBoot]Spring MVC(6.0)----图书管理系统(初)-CSDN博客
在那基础上,我们继续进行代码书写
用户登录
第一步引入依赖,以及配置yml文件
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/book_test?characterEncoding=utf8&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
configuration: # ???? MyBatis ??? SQL
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case: true #??????
# ???? MyBatis ??? SQL
logging:
file:
name: logs/springboot.log
引入依赖需要我们加上一个插件
然后在pom.xml文件中右键
然后进行选择即可,这里我们需要选择我右面的依赖
其次,我们书写Controller--Service--Mapper结构
package com.example.demo.controller;
import com.example.demo.model.UserInfo;
import com.example.demo.service.UserService;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/user")
@RestController
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login")
public boolean login(String userName, String password, HttpSession session) {
//校验用户信息是否合法.
if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {
return false;
}
UserInfo userInfo=userService.queryUserByName(userName);
if(userInfo==null)
{
return false;
}
//判断用户名和密码是否正确
//理论上应该从数据库中获取, 但是目前还没学习 mybatis, 所以先这么写.
if(userInfo!=null&&password.equals(userInfo.getPassword())) {
return true;
}
userInfo.setPassword("");
session.setAttribute("userName",userName);
return false;
}
}
为什么要把userInfo的密码设置为空再设置session呢?
因为存储到session中了.你如果不将密码设置为空的话,在获取到session就能看到密码
import com.example.demo.model.UserInfo;
import com.example.demo.model.UserInfoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.RequestMapping;
@Service
public class UserService{
@Autowired
private UserInfoMapper userInfoMapper;
public UserInfo queryUserByName(String name) {
return userInfoMapper.queryUserByName(name);
}
}
package com.example.demo.model;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface UserInfoMapper {
@Select("select * from user_info where delete_flag=0 and user_name = #{name}")
UserInfo queryUserByName(String name);
}
这样我们就可以完成一个登录,数据库中存储信息
同时我们需要修改前端代码(因为我们将返回类型改变了)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="stylesheet" href="css/bootstrap.min.css">
<link rel="stylesheet" href="css/login.css">
<script type="text/javascript" src="js/jquery.min.js"></script>
</head>
<body>
<div class="container-login">
<div class="container-pic">
<img src="pic/computer.png" width="350px">
</div>
<div class="login-dialog">
<h3>登陆</h3>
<div class="row">
<span>用户名</span>
<input type="text" name="userName" id="userName" class="form-control">
</div>
<div class="row">
<span>密码</span>
<input type="password" name="password" id="password" class="form-control">
</div>
<div class="row">
<button type="button" class="btn btn-info btn-lg" onclick="login()">登录</button>
</div>
</div>
</div>
<script src="js/jquery.min.js"></script>
<script>
function login() {
$.ajax({
url : "/user/login",
type : "post",
data : {
userName : $("#userName").val(),
password : $("#password").val(),
},
success:function(result) {
if(result==false) {
alert("用户名或密码错误,请重新输入");
}else {
location.href = "book_list.html";
}
}
});
}
</script>
</body>
</html>
添加图书
控制层:
@RequestMapping("/addBook")
public String addBook(BookInfo bookInfo){
System.out.println("添加图书"+bookInfo);
if (!StringUtils.hasLength(bookInfo.getBookName())
|| !StringUtils.hasLength(bookInfo.getAuthor())
|| bookInfo.getCount()==null
|| bookInfo.getPrice()==null
|| !StringUtils.hasLength(bookInfo.getPublish())
|| bookInfo.getStatus() ==null
){
return "输⼊参数不合法, 请检查⼊参!";
}
try {
bookService.addBook(bookInfo);
return "";
}
catch (Exception e){
System.out.println("增添错误");
return e.getMessage();
}
}
业务层
public void addBook(BookInfo bookInfo) {
bookInfoMapper.insertBook(bookInfo);
}
数据层:
@Mapper
public interface BookInfoMapper {
@Insert("insert into book_info (book_name,author,count,price,publish,status) " + "values (#{bookName},#{author},#{count},#{price},#{publish},#{status})")
Integer insertBook(BookInfo bookInfo);
}
前端修改代码
function add() {
$.ajax({
type: "post",
url: "/book/addBook",
data: $("#addBook").serialize(),
success: function (result) {
if (result == "") {
location.href = "book_list.html"
} else {
console.log(result);
alert("添加失败:" + result);
}
},
error: function (error) {
console.log(error);
}
});
}
然后页面就可以正常访问了
前后对比:

图书列表
可以看到, 添加图书之后, 跳转到图书列表⻚⾯, 并没有显⽰刚才添加的图书信息, 接下来我们来实现图 书列表
我们之前做的表⽩墙查询功能,是将数据库中所有的数据查询出来并展⽰到⻚⾯上,试想如果数据库 中的数据有很多(假设有⼗⼏万条)的时候,将数据全部展⽰出来肯定不现实,那如何解决这个问题呢?
使⽤分⻚解决这个问题。每次只展⽰⼀⻚的数据,⽐如:⼀⻚展⽰10条数据,如果还想看其他的数
据,可以通过点击⻚码进⾏查询

1.添加数据
INSERT INTO `book_info` ( book_name, author, count, price, publish )
VALUES
( '图书2', '作者2', 29, 22.00, '出版社2' ),( '图书3', '作者2', 29, 22.00, '出版社
3' ),
( '图书4', '作者2', 29, 22.00, '出版社1' ),( '图书5', '作者2', 29, 22.00, '出版社
1' ),
( '图书6', '作者2', 29, 22.00, '出版社1' ),( '图书7', '作者2', 29, 22.00, '出版社
1' ),
( '图书8', '作者2', 29, 22.00, '出版社1' ),( '图书9', '作者2', 29, 22.00, '出版社
1' ),
( '图书10', '作者2', 29, 22.00, '出版社1'),( '图书11', '作者2', 29, 22.00, '出版 社1'),
( '图书12', '作者2', 29, 22.00, '出版社1'),( '图书13', '作者2', 29, 22.00, '出版 社1'),
( '图书14', '作者2', 29, 22.00, '出版社1'),( '图书15', '作者2', 29, 22.00, '出版 社1'),
( '图书16', '作者2', 29, 22.00, '出版社1'),( '图书17', '作者2', 29, 22.00, '出版 社1'),
( '图书18', '作者2', 29, 22.00, '出版社1'),( '图书19', '作者2', 29, 22.00, '出版 社1'),
( '图书20', '作者2', 29, 22.00, '出版社1'),( '图书21', '作者2', 29, 22.00, '出版 社1');
2.了解分页的sql原理
查找第1到10条
SELECT * FROM book_info LIMIT 0,10
3.算法可知:开始索引 = (当前⻚码 - 1) * 每⻚显⽰条数
4.前端在发起查询请求时,需要向服务端传递的参数
currentPage 当前⻚码 //默认值为1
pageSize 每⻚显⽰条数 //默认值为10
5后端响应时, 需要响应给前端的数据
records 所查询到的数据列表(存储到List 集合中)
total 总记录数 (⽤于告诉前端显⽰多少⻚, 显⽰⻚数为: (total + pageSize -1)/pageSize
显⽰⻚数totalPage 计算公式为 : total % pagesize == 0 ? total / pagesize : (total /
pagesize)+1;)
6.翻⻚请求和响应部分, 我们通常封装在两个对象中
翻页请求对象
@Data
public class PageRequest {
private int currentPage = 1; // 当前⻚
private int pageSize = 10; // 每⻚中的记录数 }
我们需要根据currentPage 和pageSize ,计算出来开始索引
所以PageRequest修改为
package com.example.demo.model;
import lombok.Data;
@Data
public class PageRequest {
private int currentPage = 1; // 当前⻚
private int pageSize = 10; // 每⻚中的记录数
private int offset=1;
public int getOffset() {
return (currentPage-1) * pageSize;
}
}
翻⻚列表结果类:
import lombok.Data;
import java.util.List;
@Data
public class PageResult<T> {
private int total;//所有记录数
private List<T> records; // 当前⻚数据
public PageResult(Integer total, List<T> records) {
this.total = total;
this.records = records;
}
}
7.约定前后端交互接⼝
我们约定, 浏览器给服务器发送⼀个 /book/getListByPage 这样的 HTTP 请求, 通过
currentPage 参数告诉服务器, 当前请求为第⼏⻚的数据, 后端根据请求参数, 返回对应⻚的数据
第⼀⻚可以不传参数, currentPage默认值为 1
7. 创建enmus⽬录, 创建BookStatus类:
package com.example.demo.model;
public enum BookStatus {
DELETED(0,"⽆效"),
NORMAL(1,"可借阅"),
FORBIDDEN(2,"不可借阅");
private Integer code;
private String name;
BookStatus(int code, String name) {
this.code = code;
this.name = name;
}
public static BookStatus getNameByCode(Integer code){
switch (code){
case 0: return DELETED;
case 1: return NORMAL;
case 2: return FORBIDDEN;
}
return null;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
8.完善PageResult类
package com.example.demo.model;
import lombok.Data;
import java.util.List;
@Data
public class PageResult<T> {
public int total;//所有记录数
private List<T> records; // 当前⻚数据
private PageRequest pageRequest;
public PageResult(Integer total, PageRequest pageRequest, List<T> records)
{
this.total = total;
this.pageRequest = pageRequest;
this.records = records;
}
}
将PageRequest类引入,便于后面的分页插件获取信息
分⻚组件需要提供⼀些信息: totalCounts: 总记录数, pageSize: 每⻚的个数, visiblePages: 可视⻚数 ,currentPage: 当前⻚码
这些信息中, pageSize 和 visiblePages 前端直接设置即可. totalCounts 后端已经提供, currentPage 也 可以从参数中取到, 但太复杂了, 咱们直接由后端返回即可.
9.完善 BookController,BookService,BookInfoMapper
@RequestMapping("/getListByPage")
public PageResult<BookInfo> getListByPage(PageRequest pageRequest) {
log.info("获取图书列表{}",pageRequest);
PageResult<BookInfo> pageResult = bookService.getBookListByPage(pageRequest);
return pageResult;
}
public PageResult<BookInfo> getBookListByPage(PageRequest pageRequest) {
Integer count = bookInfoMapper.count();
List<BookInfo> books = bookInfoMapper.queryBookListByPage(pageRequest);
for (BookInfo book:books){
book.setStatusCN(BookStatus.getNameByCode(book.getStatus()).getName());
}
return new PageResult<>(count,pageRequest, books);
}
@Mapper
public interface BookInfoMapper {
@Insert("insert into book_info (book_name,author,count,price,publish,status) " + "values (#{bookName},#{author},#{count},#{price},#{publish},#{status})")
Integer insertBook(BookInfo bookInfo);
@Select("select count(1) from book_info where status<>0")
Integer count();
@Select("select * from book_info where status !=0 order by id desc limit #{offset}, #{pageSize}")
List<BookInfo> queryBookListByPage(PageRequest pageRequest);
}
10.接下来处理分⻚信息
jQuery 分页插件 : jqPaginator | 菜鸟教程
使用教程,修改里面的部分值即可
onPageChange :回调函数,当换⻚时触发(包括初始化第⼀⻚的时候),会传⼊两个参数:
1、"⽬标⻚"的⻚码page,Number类型
2、触发类型type,可能的值:"init"(初始化),"change"(点击分⻚)
11.编写前端代码(前后端交互,个人认为最为难人的部分,需要重点理解)
<!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>
function getQueryParam(name) {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(name);
}
// 修复:设置默认参数currentPage = 1,避免初始调用时参数缺失
function getBookList(currentPage=1) {
$.ajax({
type: "get",
// 修复:确保参数正确拼接,避免出现undefined
url: "/book/getListByPage?currentPage=" + currentPage,
success: function (result) {
console.log(result);
if (result != null && result.records) { // 修复:字段名records(原resords拼写错误)
var finalHtml = "";
// 遍历图书数据(修复字段名resords -> records)
for (var book of result.records) {
finalHtml += '<tr>';
finalHtml += '<td><input type="checkbox" name="selectBook" value="' + book.id + '" class="book-select"></td>';
finalHtml += '<td>' + book.id + '</td>';
finalHtml += '<td>' + book.bookName + '</td>';
finalHtml += '<td>' + book.author + '</td>';
finalHtml += '<td>' + book.count + '</td>';
finalHtml += '<td>' + book.price + '</td>';
finalHtml += '<td>' + book.publish + '</td>';
finalHtml += '<td>' + book.statusCN + '</td>';
finalHtml += '<td><div class="op">';
finalHtml += '<a href="book_update.html?bookId=' + book.id + '">修改</a>'; // 修复:链接中去除空格(bookId后无空格)
finalHtml += '<a href="javascript:void(0)" onclick="deleteBook(' + book.id + ')">删除</a>';
finalHtml += '</div></td>';
finalHtml += "</tr>";
}
$("tbody").html(finalHtml);
// 初始化分页插件
$("#pageContainer").jqPaginator({
totalCounts: result.total, // 总记录数(需与后端返回字段一致)
pageSize: 10, // 每页条数
visiblePages: 5, // 可见页码数
currentPage: result.pageRequest.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>',
// 修复:分页切换时直接调用getBookList加载数据,而非跳转页面
onPageChange: function (Page, type) {
if (type != 'init') {
location.href = "book_list.html?currentPage=" + Page;
// getBookList(Page);
}
}
});
}
},
error: function (xhr) {
console.error("请求失败:", xhr.responseText); // 增加错误日志,便于调试
}
});
}
// 初始调用时无需传参(函数已设置默认值)
const initialPage = parseInt(getQueryParam("currentPage")) || 1;
getBookList(initialPage);
// getBookList();
function deleteBook(id) {
var isDelete = confirm("确认删除?");
if (isDelete) {
//删除图书
alert("删除成功");
}
}
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>


修改图书
进⼊修改⻚⾯, 需要显⽰当前图书的信息
[请求]
/book/queryBookById?bookId=25
[参数] ⽆[响应] {
"id": 25,
"bookName": "图书21",
"author": "作者2",
"count": 999,
"price": 222.00,
"publish": "出版社1",
"status": 2,
"statusCN": null,
"createTime": "2023-09-04T04:01:27.000+00:00",
"updateTime": "2023-09-05T03:37:03.000+00:00"
}
点击修改按钮, 修改图书信息
[请求]
/book/updateBook
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
[参数]
id=1&bookName=图书1&author=作者1&count=23&price=36&publish=出版社1&status=1
[响应]
"" //失败信息, 成功时返回空字符串
我们约定, 浏览器给服务器发送⼀个 /book/updateBook 这样的 HTTP 请求, form表单的形式来 提交数据
服务器返回处理结果, 返回""表⽰添加图书成功, 否则, 返回失败信息
实现服务器代码
BookController代码
@RequestMapping("/queryBookById")
public BookInfo queryBookById(Integer bookId) {
if (bookId == null || bookId <= 0) {
return new BookInfo();
}
BookInfo bookInfo = bookService.queryBookById(bookId);
return bookInfo;
}
@RequestMapping("/updateBook")
public String updateBook(BookInfo bookInfo) {
log.info("修改图书:{}", bookInfo);
try {
bookService.updateBook(bookInfo);
return "";
} catch (Exception e) {
log.error("修改图书失败", e);
return e.getMessage();
}
}
BookService代码
public BookInfo queryBookById(Integer bookId) {
return bookInfoMapper.queryBookById(bookId);
}
public void updateBook(BookInfo bookInfo) {
bookInfoMapper.updateBook(bookInfo);
}
BookInfoMapper代码
@Select("select id, book_name, author, count, price, publish, `status`,
create_time, update_time " +
"from book_info where id=#{bookId} and status<>0")
BookInfo queryBookById(Integer bookId);
前端代码
<!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>
$.ajax({
type: "get",
url: "/book/queryBookById" + location.search,
success: function (book) {
if (book != null) {
$("#bookId").val(book.id);
$("#bookName").val(book.bookName);
$("#bookAuthor").val(book.author);
$("#bookStock").val(book.count);
$("#bookPrice").val(book.price);
$("#bookPublisher").val(book.publish);
$("#bookStatus").val(book.status);
}
}
});
function update() {
$.ajax({
type: "post",
url: "/book/updateBook",
data: $("#updateBook").serialize(),
success: function (result) {
if (result == "") {
location.href = "book_list.html"
} else {
console.log(result);
alert("修改失败:" + result);
}
},
error: function (error) {
console.log(error);
}
});
}
</script>
</body>
</html>
我们修改图书信息, 是根据图书ID来修改的, 所以需要前端传递的参数中, 包含图书ID
在form表单中, 再增加⼀个隐藏输⼊框, 存储图书ID, 随 $("#updateBook").serialize()
⼀起提交到后端
- 获取url中参数的值(⽐较复杂, 需要拆分url)
-
在form表单中, 再增加⼀个隐藏输⼊框, 存储图书ID, 随 $("#updateBook").serialize()
<form id="updateBook">
⼀起提交到后端
我们采⽤第⼆种⽅式
在form表单中,添加隐藏输⼊框<label for="bookName">图书名称:</label>
hidden 类型的 <input> 元素
隐藏表单, ⽤⼾不可⻅、不可改的数据,在⽤⼾提交表单时,这些数据会⼀并发送出
使⽤场景: 正被请求或编辑的内容的 ID. 这些隐藏的 input 元素在渲染完成的⻚⾯中完全不可⻅,⽽ 且没有⽅法可以使它重新变为可⻅
⻚⾯加载时, 给该hidden框赋值
$("#bookId").val(book.id);
删除图书
删除分为 逻辑删除 和物理删除
逻辑删除
逻辑删除也称为软删除、假删除、Soft Delete,即不真正删除数据,⽽在某⾏数据上增加类型 is_deleted的删除标识,⼀般使⽤UPDATE语句
物理删除
物理删除也称为硬删除,从数据库表中删除某⼀⾏或某⼀集合数据,一般用DELETE语句
数据是公司的重要财产, 通常情况下, 我们采⽤逻辑删除的⽅式, 当然也可以采⽤[物理删除+归档]
实现客⼾端代码
function deleteBook(id) {
var isDelete = confirm("确认删除?");
if (isDelete) {
//删除图书
$.ajax({
type: "post",
url: "/book/updateBook",
data: {
id: id,
status: 0
},
success: function () {
//重新刷新⻚⾯
location.href = "book_list.html"
}
});
}
}
批量删除
批量删除, 其实就是批量修改数据
点击[批量删除]按钮时, 只需要把复选框选中的图书的ID,发送到后端即可
多个id, 我们使⽤List的形式来传递参数
实现服务器代码
@RequestMapping("/batchDeleteBook")
public boolean batchDeleteBook(@RequestParam List<Integer> ids){
log.info("批量删除图书, ids:{}",ids);
try{
bookService.batchDeleteBook(ids);
}catch (Exception e){
log.error("批量删除异常,e:",e);
return false;
}
return true;
}
public void batchDeleteBook(List<Integer> ids) {
bookInfoMapper.batchDeleteBook(ids);
void batchDeleteBook(List<Integer> ids);
xml
<update id="batchDeleteBook">
update book_info set status=0 where id in
<foreach collection="ids" item="id" separator="," open="(" close=")">
#{id}
</foreach>
</update>
拦截器
🍃前言
前面我们基本实现了图书股管理系统的功能,但是我们依旧存在一个问题。
就是我们不论是否登录,我们直接访问图书列表。也可以进行访问及修改
而我们希望达到的效果是,必须要进行登录后才能进行一系列操作
这里我们使用拦截器来完成着一系列操作
什么是拦截器?
拦截器是Spring框架提供的核⼼功能之⼀,主要⽤来拦截⽤⼾的请求,在指定⽅法前后,根据业务需要执⾏预先设定的代码.
也就是说,允许开发⼈员提前预定义⼀些逻辑,在⽤⼾的请求响应前后执⾏.也可以在⽤⼾请求前阻⽌其执⾏.
在拦截器当中,开发⼈员可以在应⽤程序中做⼀些通⽤性的操作,⽐如通过拦截器来拦截前端发来的请求,判断Session中是否有登录⽤⼾的信息.如果有就可以放⾏,如果没有就进⾏拦截.

就⽐如我们去银⾏办理业务,在办理业务前后,就可以加⼀些拦截操作,办理业务之前,先取号,如果带⾝份证了就取号成功。业务办理结束,给业务办理⼈员的服务进⾏评价.这些就是"拦截器"做的⼯作
拦截器的基本使用
拦截器的使⽤步骤分为两步:
- 定义拦截器
- 注册配置拦截器
自定义拦截器
⾃定义拦截器:实现HandlerInterceptor接⼝,并重写其所有⽅法
package com.example.demo.configuration;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("执行目标方法前的代码");
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("目标执行完后的代码");
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("目标试图渲染后的代码");
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
这里涉及到的三个方法
preHandle()⽅法:⽬标⽅法执⾏前执⾏. 返回true:继续执⾏后续操作;返回false:中断后续操作.
postHandle()⽅法:⽬标⽅法执⾏后执⾏
afterCompletion()⽅法:视图渲染完毕后执⾏,最后执⾏(后端开发现在⼏乎不涉及视图,暂不了解)
注册配置拦截器
注册配置拦截器:实现WebMvcConfigurer接⼝,并重写addInterceptors⽅法
WebMvcConfigurer这个接口并不是只给拦截器使用的,而是WebMVC相关的配置都在这里
package com.example.demo.configuration;
import com.example.demo.model.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**");
}
}
启动服务,试试访问任意请求,观察后端⽇志

们把拦截器中preHandle⽅法的返回值改为false,再观察运⾏结果


运行结果:可以看到,拦截器拦截了请求,没有进⾏响应
拦截路径
关于注册配置拦截器的拦截路劲,拦截路径是指我们定义的这个拦截器,对哪些请求⽣效.我们在注册配置拦截器的时候,通过addPathPatterns() ⽅法指定要拦截哪些请求.也可以通过
excludePathPatterns() 指定不拦截哪些请求.上述代码中,我们配置的是 /** ,表⽰拦截所有的请求.
拦截器中除了可以设置 /** 拦截所有资源外,还有⼀些常⻅拦截路径设置,比如在该项目中

拦截器执行流程
我们先来看一下正常的执行流程

当我们有了拦截器以后,我们的执行流程为

1.添加拦截器后,执⾏Controller的⽅法之前,请求会先被拦截器拦截住.执⾏ preHandle() ⽅法,这个⽅法需要返回⼀个布尔类型的值.如果返回true,就表⽰放⾏本次操作,继续访问controller中的⽅法.如果返回false,则不会放⾏(controller中的⽅法也不会执⾏).
2.controller当中的⽅法执⾏完毕后,再回过来执⾏ postHandle() 这个⽅法以及afterCompletion() ⽅法,执⾏完毕之后,最终给浏览器响应数据.
项目实现统一拦截
定义拦截器
首先定义一个常量类来存放我们的sessionid常量等等
package com.example.demo.model;
public class Constants {
public static final String SESSION_USER_KEY="userName";
}
再来就是拦截器(逻辑简单,查看有没有现在的这个session即可)
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
log.info("登录拦截器校验...");
HttpSession session = request.getSession();
UserInfo userInfo = (UserInfo) session.getAttribute(Constants.SESSION_USER_KEY);
if (userInfo!=null && userInfo.getId()>=0){
return true;
}
response.setStatus(401);//401 表示未认证登录
return false;
}
}
代码解释如下:
- 对服务中所存在的session进行判断,如果存在,则返回true,放行
- 若不存在,则使用setStatus()方法设置http状态码为401,前端收到响应进行跳转到登录页面
当前按照上述配置拦截器的代码来看,会拦截所有的路径,那么此时在没有登录的情况下,访问每个接口都会进行拦截,包括登录接口


所以我们需要把上述配置拦截器中的拦截路径重新配置一下
注册配置拦截器
注意:拦截器也会拦截前端请求
@Configuration
public class WebConfig implements WebMvcConfigurer {
//⾃定义的拦截器对象
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//注册⾃定义拦截器对象
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")//设置拦截器拦截的请求路径(/**表⽰拦截所有请求
.excludePathPatterns("/user/login")//设置拦截器排除拦截的路径
.excludePathPatterns("/**/*.js") //排除前端静态资源
.excludePathPatterns("/**/*.css")
.excludePathPatterns("/**/*.png")
.excludePathPatterns("/**/*.html");
}
}
访问用户登录接口:此时就可以访问了


希望你们可以从这一个简单项目中学到一个项目的基础思路知识