图书管理系统(详解版 附源码)

目录

项目分析

实现页面

功能描述

页面预览

准备工作

数据准备

创建数据库

用户表

创建项目

导入前端页面

测试前端页面

后端代码实现

项目公共模块

实体类

公共层

统一结果返回

统一异常处理

业务实现

持久层

用户登录

用户注册

密码加密验证

添加图书

图书列表

修改图书

删除图书

批量删除

强制登录

令牌生成

拦截器


在学习了 Spring 框架 和 MyBatis 相关知识后,我们来尝试实现一个简单的图书管理系统,完成图书管理系统项目的后端开发

项目分析

使用SSM框架(Spring、Spring MVC、Mybaits)实现一个简单的图书管理系统

实现页面

1. 用户登录

2. 用户注册

2. 图书列表页

3. 添加图书页

4. 修改图书页

功能描述

用户进行登录,若是未注册账号则点击注册,注册成功后,返回登录页面进行登录,成功登录后,进入图书列表页,可对图书进行增、删、查、改等操作(未登录前不能访问图书相关页面)

页面预览

用户登录:

用户注册:


图书列表页:

添加图书页:

修改图书页:

准备工作

数据准备

创建数据库

-- 创建数据库
DROP DATABASE IF EXISTS book_test;
CREATE DATABASE book_test DEFAULT CHARACTER SET utf8mb4;

在图书管理系统中,涉及两张表

用户表(包含用户id、账号、密码等信息

图书表(包含图书id、图书名、作者等信息

用户表

-- 创建用户表
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 = '用户表';

向用户表中插入一些数据,作为初始化数据:

INSERT INTO user_info ( user_name, PASSWORD ) VALUES ( "admin", "admin" );
INSERT INTO user_info ( user_name, PASSWORD ) VALUES ( "zhangsan", "zhangsan" );

图书表

-- 创建图书表
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 `book_info` (book_name,author,count, price, publish) VALUES ('图书1', '作者1', 29, 22.00, '出版社1');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书2', '作者2', 30, 23.40, '出版社2');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书3', '作者3', 59, 26.00, '出版社3');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书4', '作者4', 99, 52.00, '出版社2');

创建项目

创建SpringBoot项目,添加对应依赖

连接数据库:

# 数据库连接配置
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/book_test?characterEncoding=utf8&useSSL=falseusername: rootpassword: rootdriver-class-name: com.mysql.cj.jdbc.Driver
mybatis:configuration:map-underscore-to-camel-case: true #配置驼峰自动转换log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印sql语句mapper-locations: classpath:mapper/**Mapper.xml
# 配置日志文件的文件名
logging:file:name: logs/spring-book.log

在这里使用的是 application.yml 进行配置(也可以使用 application.properties 进行配置)

导入前端页面

前端页面存放在:

前端代码/图书管理系统 · Echo/project - 码云 - 开源中国 (gitee.com)

将前端页面导入到static目录下:

测试前端页面

我们运行程序,访问前端页面:登录icon-default.png?t=N7T8http://127.0.0.1:8080/login.html

(其他页面就不再一一展示了,大家自行进行测试)

前端页面正确显示,项目的准备工作完成

后端代码实现

项目可分为 控制层(Controller),服务层(Service),持久层(Mapper),还有实体类公共层

我们首先根据需求实现项目公共模块,即 实体类公共层

项目公共模块

实体类

需要创建两个实体:UserInfo类 BookInfo类

创建 model 目录,在 model 目录下根据数据表创建 UserInfoBookInfo

UserInfo:

import lombok.Data;
import java.util.Date;@Data
public class UserInfo {private Integer id;private String userName;private String password;private Integer deleteFlag;private Date createTime;private Date updateTime;
}

BookInfo:

@Data
public class BookInfo {private Integer id;private String bookName;private String author;private Integer count;private Double price;private String publish;private Integer status; // 0:已删除 1:正常 2:不允许借阅private String statusCN; // 状态描述信息private Date createTime;private Date updateTime;
}

公共层

统一结果返回

我们首先创建 统一返回结果实体类 Result

code:业务码(200:业务处理成功,-1:业务处理失败,-2:用户未登录)

errorMessage:业务处理失败时,返回的错误信息

data:业务返回数据

实现业务码时,我们可以定义 final常量

public class Constants {public static final int RESULT_CODE_SUCCESS = 200;public static final int RESULT_CODE_FAIL = -1;public static final int RESULT_CODE_UNLOGIN = -2;
}

也可以使用枚举类型

public enum ResultStatus {SUCCESS(200),FAIL(-1),NOLOGIN(-2);private Integer code;ResultStatus(Integer code) {this.code = code;}
}

在这里,我们选择使用枚举类型,创建enums目录,在目录下创建ResultStatus类

此外,业务返回数据data,我们可以选择使用Object类型,也可以使用泛型,在这里,我们使用泛型

并实现业务处理成功、业务处理失败的方法,由于我们后续会实现强制登录功能,因此,在这里我们也一起实现用户未登录时的处理方法

@Data
public class Result<T> {private ResultStatus code;private T data;private String errorMessage;// 业务成功处理public static <T> Result success(T data) {Result result = new Result();result.code = ResultStatus.SUCCESS;result.data = data;result.errorMessage = "";return result;}// 用户未登录public static <T> Result noLogin() {Result result = new Result();result.code = ResultStatus.NOLOGIN;result.errorMessage = "用户未登录!";return result;}// 业务处理失败,返回错误信息public static <T> Result fail(String errorMessage) {Result result = new Result();result.code = ResultStatus.FAIL;result.errorMessage = errorMessage;return result;}// 业务处理失败,返回错误信息和数据public static <T> Result fail(String errorMessage, T data) {Result result = new Result();result.code = ResultStatus.FAIL;result.data = data;result.errorMessage = errorMessage;return result;}
}

统一返回结果:

统一数据返回格式使用 @ControllerAdvice(控制器通知类) 和 ResponseBodyAdvice 实现

添加类 ResponseAdvice,实现 ResponseBodyAdvice 接口,并在类上添加 @ControllerAdvice 注解

@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Autowiredprivate ObjectMapper objectMapper;// supports方法,用于判断是否要执行beforeBodyWrite方法,true为执行,false不执行,// 可以通过supports方法选择哪些类或哪些方法的response需要进行处理,哪些不需要进行处理@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {// 所有方法都进行处理return true;}@SneakyThrows@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {// 方法返回的结果已经是Result类型,直接返回Resultif(body instanceof Result) {return body;}// 返回的结果是String类型,使用SpringBoot内置提供的Jackson来实现信息的序列化if(body instanceof String) {return objectMapper.writeValueAsString(Result.success(body));}// 其他情况,调用Result.success方法,返回Result类型数据return Result.success(body);}
}

使用统一结果返回方便前端接收和解析后端接口返回的数据,也有利于项目统一数据的维护和修改

统一异常处理

统一异常处理使用的是 @ControllerAdvice(控制权通知类)和 @ExceptionHandler(异常处理器),两个结合表示当出现异常时执行某个通知(也就是执行某个方法事件)

@ControllerAdvice
@ResponseBody
@Slf4j
public class ExceptionAdvice {@ExceptionHandlerpublic Result handlerException(Exception e) {log.info("发生异常e:", e);return Result.fail("内部错误,请联系管理员!");}
}

当代码出现了 Exception 异常(包括 Exception类的子类),就返回一个Result 对象(也可以针对不同的异常,返回不同的结果)

业务实现

持久层

根据需求,先大致计算有哪些 DB 操作,完成持久层初步代码,后续再根据业务需求进行完善

大致需要的DB操作有:

1. 用户登录页

  根据用户名查询用户信息

2. 用户注册页

  根据用户注册信息添加用户信息

3. 图书列表页

   查询所有图书列表

   当点击删除时,根据图书id删除图书信息

4. 添加图书页

   插入新的图书信息

5. 修改图书页

根据图书id修改图书信息 

我们首先实现与 user_info 表相关操作:

1. 根据用户名查询用户信息(由于用户名是唯一的,因此可以通过用户名查询到唯一用户信息

2. 根据用户注册信息添加用户信息

由于操作比较简单,我们直接使用注解的方式实现: 

创建mapper,实现接口UserInfoMapper

@Mapper
public interface UserInfoMapper {// 根据用户名查询用户信息@Select("select id, user_name, password, delete_flag, create_time, update_time from user_info where user_name = #{userName}")UserInfo selectByName(String userName);// 根据用户输入信息添加用户信息@Insert("insert into user_info (user_name, password) values(#{userName}, #{password})")int insertUser(String userName, String password);
}

编写完代码后,我们编写测试用例,简单进行单元测试

@SpringBootTest
class UserInfoMapperTest {@Autowiredprivate UserInfoMapper userInfoMapper;@Testvoid selectByName() {userInfoMapper.selectByName("admin");}@Testvoid insertUser() {System.out.println(userInfoMapper.insertUser("lisi", "123456"));}
}

测试通过后,我们继续实现与 book_info 表相关操作:

1. 获取所有图书列表

2. 当点击删除时,根据图书 id 删除图书信息

3. 插入新的图书信息

4. 根据图书id修改图书信息

在实现删除图书信息时,我们采用 逻辑删除,即 将 status的值修改为 0,而不是直接将图书信息从表中删除,因此,删除图书信息时,可使用修改图书信息的sql语句 

@Mapper
public interface BookInfoMapper {/*** 获取图书列表*/@Select("select id, book_name, author, count, price, publish, `status`, delete_flag, create_time, update_time from book_info ")List<BookInfo> selectAllBook();/*** 插入新的图书信息*/@Insert("insert into book_info (book_name, author, count, price, publish, `status`) " +"values (#{bookName}, #{author}, #{count}, #{price}, #{publish}, #{status})")Integer insertBook(BookInfo bookInfo);/*** 根据图书id修改图书信息*/Integer updateBook(BookInfo bookInfo);}

在修改图书信息时,修改的内容是可选的(如:选择只修改bookName或只修改status),因此我们需要使用动态SQL,由于使用注解实现时,是进行的字符串拼接,不易检查出错误,因此在这里我们选择使用 xml 实现(后面实现)

我们先对 获取用户列表、插入新的图书信息 进行单元测试:

@SpringBootTest
class BookInfoMapperTest {@Autowiredprivate BookInfoMapper bookInfoMapper;@Testvoid selectAllBook() {bookInfoMapper.selectAllBook();}@Testvoid insertBook() {BookInfo bookInfo = new BookInfo();bookInfo.setBookName("图书5");bookInfo.setAuthor("作者5");bookInfo.setCount(11);bookInfo.setPrice(12.5);bookInfo.setPublish("出版社5");bookInfo.setStatus(1);bookInfoMapper.insertBook(bookInfo);}
}

测试通过后,我们使用 xml 的方式实现修改图书信息:

由于我们配置的路径为:

因此,在resources目录下添加文件夹 mapper,然后添加文件 bookInfoMapper:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.springbook.mapper.BookInfoMapper"><update id="updateBook">update book_info<set><if test="bookName != null">book_name = #{bookName},</if><if test="author != null">author = #{author},</if><if test="count != null">count = #{count},</if><if test="publish != null">publisht = #{publish},</if><if test="status != null">status = #{status},</if></set>where id = #{id}</update>
</mapper>

然后测试修改图书信息和删除图书信息:

    @Testvoid updateBook() {// 修改图书信息BookInfo bookInfo = new BookInfo();bookInfo.setId(2);bookInfo.setBookName("图书222");bookInfoMapper.updateBook(bookInfo);// 删除图书信息BookInfo bookInfo1 = new BookInfo();bookInfo1.setId(1);bookInfo1.setStatus(0);bookInfoMapper.updateBook(bookInfo1);}

测试成功后,关于持久层的初步代码就实现完毕,若后续以上代码不能满足需求,我们再根据需求进行完善即可

接下来,我们就继续实现控制层和服务层相关代码,并补全前端代码

用户登录

约定前后端交互接口:

[URL]

POST /user/login

[请求参数]

userName=admin&password=admin

[响应]

{

    "code": "SUCCESS",

    "data": "",

    "errorMessage": ""

}

 当登录成功时,返回数据为空字符串 "",登录失败时,返回错误信息(可自行进行定义)

实现服务端代码

创建controller目录,再在目录下创建UserController类

UserController中补充代码:

先进行参数校验,校验通过后查询用户信息

无论前端是否进行了参数校验,后端一律需要进行校验(这是因为后端接口可能会被黑客攻击,不通过前端来访问,若后端不进行校验,就会产生脏数据)

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {@Autowiredprivate UserService userService;@RequestMapping("/login")public Result<String> login(String userName, String password) {log.info("用户登录,获取参数userName:{}, password:{}", userName, password);// 参数校验if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {return Result.fail("用户名或密码为空!");}// 根据用户名进行查询UserInfo userInfo = userService.selectByName(userName);if(userInfo == null) {return Result.fail("用户名或密码错误!");}if(!password.equals(userInfo.getPassword())) {return Result.fail("密码错误!");}return Result.success("");}
}

业务层:

创建service目录,再在目录下创建UserService

UserService中补充代码:

@Service
public class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;public UserInfo selectByName(String userName) {return userInfoMapper.selectByName(userName);}
}

接着我们运行程序,使用浏览器或 postman 对接口进行测试:

分别测试 用户名或密码为空用户名错误密码错误成功登录情况下是否正确响应

修改客户端代码

修改login.html function login()中代码:

在用户点击登录后使用ajax向服务器发送HTTP请求

服务器返回的响应是一个 JSON 格式的数据,根据响应数据构造页面内容

    <script>function login() {$.ajax({url: "/user/login",type: "post",data: {userName: $("#userName").val(),password: $("#password").val()},success: function(result) {if(result.code == "SUCCESS" && result.data == "") {location.href = "book_list.html";}else {alert(result.errorMessage);}}});}</script>

此时,我们再次运行程序,联动前端一起进行测试:

当输入正确的用户名和密码时,进行跳转;其他异常情况,页面弹窗警告

用户注册

[URL]

POST /user/register

[请求参数]

userName=wangwu&password=wangwu

[响应]

{

    "code": "SUCCESS",

    "data": "",

    "errorMessage": ""

}

 当注册成功时,返回数据为空字符串 "",注册失败时,返回错误信息

实现服务端代码

在 UserController 中补充代码:

先进行参数校验,校验通过后添加用户信息

    /*** 用户注册*/@RequestMapping("/register")public Result<String> register(String userName, String password) {log.info("用户注册");// 参数校验if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {return Result.fail("用户名或密码为空!");}// 添加用户信息try {int result = userService.insertUser(userName, password);if(result > 0) {return Result.success("添加成功!");}}catch (Exception e) {log.error("添加失败, e", e);}return Result.fail("用户名已存在!");}

业务层:

UserService中补充代码

    public int insertUser(String userName, String password) {return userInfoMapper.insertUser(userName, password);}

在这里,就不单独对后端代码进行测试了,实现前端代码后一起进行测试(大家可自行使用浏览器或postman进行测试)

修改客户端代码

修改register.html  function register()中代码:

    <script>function register() {if($("#password").val() != $("#confirmPassword").val()) {alert("密码不一致");return;}$.ajax({url: "/user/register",type: "post",data: {userName: $("#userName").val(),password: $("#password").val()},success: function(result) {if(result.code == "SUCCESS" && result.data == "") {location.href = "login.html";}else {alert(result.errorMessage);}}});}</script>

此时再次运行程序,进行测试:

测试成功登录情况下能否正确跳转,密码不一致或用户名已存在情况下是否弹出提示

与用户相关的操作(用户登录注册)我们就实现完毕了

但是,由于我们在数据库中使用明文对用户密码进行存储,非常不安全,因此我们需要对用户密码进行加密,在这里,我们使用 MD5 对密码进行加密

密码加密验证

使用MD5对密码进行加密和验证过程如下图:

创建目录 utils,然后在目录下创建 SecurityUtils 

接下来我们在 SecurityUtils 中实现对密码的加密和验证:

public class SecurityUtils {/*** 对用户注册密码进行加密* @param password password 用户注册密码* @return 数据库中存储信息(密文 + 盐值)*/public static String encipher(String password) {// 生成随机盐值String salt = UUID.randomUUID().toString().replace("-", "");// 将 盐值 + 明文进行加密String secretPassword = DigestUtils.md5DigestAsHex((salt + password).getBytes());// 返回 密文 + 盐值return secretPassword + salt;}/*** 验证密码是否正确* @param inputPassword 用户登录时输入的密码* @param sqlPassword 数据库中存储的密码(密文 + 盐值)* @return 密码是否正确*/public static Boolean verity(String inputPassword, String sqlPassword) {// 校验用户输入的密码if(!StringUtils.hasLength(inputPassword)) {return false;}// 校验数据库中保存的密码if(!StringUtils.hasLength(sqlPassword) || sqlPassword.length() != 64) {return false;}// 解析盐值String salt = sqlPassword.substring(32, 64);// 生成哈希值(盐值 + 明文)String secretPassword = DigestUtils.md5DigestAsHex((salt + inputPassword).getBytes());// 判断密码是否正确return secretPassword.equals(sqlPassword.substring(0, 32));}
}

接下来,我们修改 注册登录 相关代码:

注册:生成密钥

登录 :进行验证

重新运行程序,进行测试:

此时进行登录,存储的密码则为密文

我们将之前添加的用户的密码都修改为密文:

class SecurityUtilsTest {@Testvoid encipher() {System.out.println(SecurityUtils.encipher("admin"));System.out.println(SecurityUtils.encipher("zhangsan"));System.out.println(SecurityUtils.encipher("123456"));System.out.println(SecurityUtils.encipher("wangwu"));}
}

 运行,得到加密后的密码:

我们直接修改数据库中的密码 

关于密码加密和验证,可参考之前的文章:http://t.csdnimg.cn/Cf3zo

在实现用户登录注册后,我们继续实现图书相关页面

添加图书

[URL]

POST /book/addBook

[请求参数]

bookName=图书11&author=作者11&count=23&price=12.3&publish=出版社11&status=1

[响应]

{

    "code": "SUCCESS",

    "data": "",

    "errorMessage": ""

}

 当添加成功时,返回数据为空字符串 "",添加失败时,返回错误信息

实现服务端代码

在 controller 目录下创建 BookController

BookController中补充代码

先进行参数校验,校验通过后添加图书信息:

public class BookController {@Autowiredprivate BookService bookService;@RequestMapping("/addBook")public Result<String> addBook(BookInfo bookInfo) {log.info("添加图书,接收到参数bookInfo:{}", bookInfo);// 参数校验if(!StringUtils.hasLength(bookInfo.getBookName()) ||!StringUtils.hasLength(bookInfo.getAuthor()) ||bookInfo.getCount() == null || bookInfo.getCount() < 0 ||bookInfo.getPrice() == null || bookInfo.getPrice() < 0 ||!StringUtils.hasLength(bookInfo.getPublish()) ||bookInfo.getStatus() == null) {return Result.fail("输入参数不合法!");}// 添加图书try {Integer result = bookService.insertBook(bookInfo);if(result > 0) {return Result.success("");}}catch (Exception e) {log.error("添加图书失败,e", e);}return Result.fail("添加失败!");}
}

业务层:

在 service 目录下创建BookService

BookService中补充代码:

@Service
public class BookService {@Autowiredprivate BookInfoMapper bookInfoMapper;public Integer insertBook(BookInfo bookInfo) {return bookInfoMapper.insertBook(bookInfo);}
}

同样,我们补全前端代码后一起进行测试

修改客户端代码
    <script>function add() {$.ajax({url: "/book/addBook",type: "post",data: $("#addBook").serialize(),success: function(result) {if(result.code == 'SUCCESS' && result.data == "") {// 添加成功,返回图书列表页location.href = "book_list.html";} else {alert(result.data);}}})}</script>

测试:

添加成功:

图书列表

在添加图书后,跳转到图书列表页面,并没有显示刚添加的图书信息,接下来,我们实现图书列表页面

由于图书列表中的数据可能会很多,此时将数据全部展示出来是不现实的,因此我们可以使用分页来解决这个问题,每次只显示一页的数据(一页显示5条数据),若想查看其他的数据,可以通过点击页码进行查看

分页时,数据如何进行显示呢?

第一页:显示 1-5 条数据

第二页:显示 6 - 10 条数据

...

要想实现这个功能,需要从数据库中进行分页查询,使用 LIMIT 关键字,格式为

limit 开始索引(开始索引从0开始), 每页显示的条数

要想显示分页效果,需要更多的数据,因此我们先伪造更多的数据:

INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书1', '作者1', 29, 22.00, '出版社1');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书2', '作者2', 30, 23.40, '出版社2');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书3', '作者3', 59, 26.00, '出版社3');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书4', '作者4', 99, 52.00, '出版社4');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书1', '作者1', 29, 22.00, '出版社1');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书2', '作者2', 30, 23.40, '出版社2');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书3', '作者3', 59, 26.00, '出版社3');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书4', '作者4', 99, 52.00, '出版社4');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书1', '作者1', 29, 22.00, '出版社1');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书2', '作者2', 30, 23.40, '出版社2');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书3', '作者3', 59, 26.00, '出版社3');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书4', '作者4', 99, 52.00, '出版社4');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书1', '作者1', 29, 22.00, '出版社1');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书2', '作者2', 30, 23.40, '出版社2');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书3', '作者3', 59, 26.00, '出版社3');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书4', '作者4', 99, 52.00, '出版社4');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书4', '作者4', 99, 52.00, '出版社4');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书1', '作者1', 29, 22.00, '出版社1');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书2', '作者2', 30, 23.40, '出版社2');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书3', '作者3', 59, 26.00, '出版社3');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('图书4', '作者4', 99, 52.00, '出版社4');

查询第一页的SQL语句为:

select * from book_info limit 0, 5;

查询第二页的SQL语句为:

select * from book_info limit 5, 5;

观察上述SQL语句,我们可以发现:开始索引一直在改变,每页显示的条数是固定的

开始索引 = (当前页面 - 1)* 每页显示的条数

 因此,

前端在发起查询请求时,需要向服务端传递的参数有:

currentPage:当前页码(默认值为1)

pageSize:每页显示的条数(默认值为5)

为了项目更好的扩展性(软件系统具备面对未来需求变化而进行扩展的能力),通常不设置固定值,而是通过参数的形式进行传递,例如,当前需求为一页显示 5 条数据,后期需求为一页显示 10 条数据,此时后端代码不需要进行任何修改

后端在进行响应时,需要响应给前端的数据有:

records:所查询到的数据列表(存储到List集合中)

total:总记录数,用于告诉前端显示多少页

当前显示页数:告诉前端当前显示的页码为 currentPage

对于翻页请求和响应,我们将其封装在两个对象中:

翻页请求对象:

@Data
public class PageRequest {private Integer currentPage = 1; // 当前页private Integer pageSize = 5; // 每页显示条数private int offset; // 索引// 计算索引public int getOffset() {return (currentPage - 1) * pageSize;}
}

 翻页响应对象:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult<T> {private Integer total; //总记录数private List<T> records; // 当前页数据private PageRequest pageRequest;
}

currentPage 封装在 PageRequest 中,因此,我们直接将 PageRequest 封装在 PageResult 中

接着,基于上述分析,我们来约定前后端交互接口:

[URL]

GET /book/getListByPage?currentPage=1

[请求参数]

[响应]

{

    "code": "SUCCESS",

    "data": {

        "total": 23,

        "records": [

            {

               "id": 27,

                "bookName": "图书4",

                "author": "作者4",

                "count": 99,

                "price": 52.0,

                "publish": "出版社4",

                "status": 1,

                "statusCN": "可借阅",

                "createTime": "2024-06-17T08:28:22.000+00:00",

                "updateTime": "2024-06-17T08:28:22.000+00:00"

            },

            .....,

        ]

    },

    "errorMessage": ""

当浏览器给服务器发送一个 /book/getListByPage 请求时,通过 currentPage 参数告诉服务器,当前请求为第几页数据,后端根据请求参数返回对应页的数据(第一页可以不传参,currentPage默认值为1

实现服务端代码

完善BookController中代码:

    @RequestMapping("/getListByPage ")public Result<PageResult<BookInfo>> getListByPage(PageRequest pageRequest) {log.info("获取图书列表, 接收到参数pageRequest:{}", pageRequest);PageResult<BookInfo> pageResult = bookService.getListByPage(pageRequest);if(pageResult == null) {return Result.fail("获取图书列表失败!");}return Result.success(pageResult);}

业务层:

BookService:

    public PageResult<BookInfo> getListByPage(PageRequest pageRequest) {// 获取总记录数Integer total = bookInfoMapper.count();// 获取当前页记录List<BookInfo> bookInfoList= bookInfoMapper.selectByPage(pageRequest.getOffset(), pageRequest.getPageSize());return new PageResult(total, bookInfoList, pageRequest);}

注意:

由于我们需要在列表中显示 图书状态,因此,在返回之前我们需要处理图书的状态描述 statusCN,图书的状态描述与 图书状态(status)有对应关系,在这里我们使用 枚举类型 来表示不同的状态描述,这样,如果后续状态码有变动,我们也只需要修改 BookStatus 中的代码

enums 目录下创建 BookStatus

public enum BookStatus {DELETE(0, "删除"),NORMAL(1, "可借阅"),FORBIDDEN(2, "不可借阅"),;BookStatus(Integer code, String desc) {this.code = code;this.desc = desc;}private Integer code;private String desc;/*** 根据Code, 返回描述信息*/public static BookStatus getDescByCode(Integer code){switch (code){case 0: return DELETE;case 1: return NORMAL;case 2:default:return FORBIDDEN;}}public Integer getCode() {return code;}public void setCode(Integer code) {this.code = code;}public String getDesc() {return desc;}public void setDesc(String desc) {this.desc = desc;}
}

 在返回结果前处理状态:

    public PageResult<BookInfo> getListByPage(PageRequest pageRequest) {// 获取总记录数Integer total = bookInfoMapper.count();// 获取当前页记录List<BookInfo> bookInfoList= bookInfoMapper.selectByPage(pageRequest.getOffset(), pageRequest.getPageSize());// 处理状态for (BookInfo bookInfo: bookInfoList) {bookInfo.setStatusCN(BookStatus.getDescByCode(bookInfo.getStatus()).getDesc());}return new PageResult(total, bookInfoList, pageRequest);}

使用 getDescByCode 方法,通过code获取对应枚举,再使用 getDesc 获取对应的状态描述

翻页信息需要返回图书数据总数列表信息,需要查询两次

由于前面我们在编写持久层代码时,并未实现查询所有图书数量和获取当前页数据,因此我们需要完善持久层代码 

持久层:

    /*** 获取当前页图书数据*/@Select("select id, book_name, author, count, price, publish, `status`, create_time, update_time from book_info" +" where status != 0" +" order by id desc" +" limit #{offset}, #{pageSize}")List<BookInfo> selectByPage(int offset, Integer pageSize);/*** 获取未被删除的所有图书数量*/@Select("select count(1) from book_info where status != 0")Integer count();

需要注意的是:查询的图书都是未被删除的图书,因此 status 不能为0 

启动服务,访问后端程序:http://127.0.0.1:8080/book/getListByPage?currentPage=1

成功获取记录 1 - 5条记录(按照id进行降序排列,也可以改为升序) 

修改客户端代码

访问第一页图书的前端url为:http://127.0.0.1:8080/book_list.html?pageNum=1

访问第二页图书的前端url为:http://127.0.0.1:8080/book_list.html?pageNum=2

当浏览器访问book_list.html页面时,就请求后端,将后端返回的数据显示在页面上,

调用后端请求: /book/getListByPage?currentPage=1

修改js,将后端请求方法修改为  /book/getListByPage?currentPage=1

// 获取图书列表getBookList();function getBookList() {$.ajax({url: "/book/getListByPage" + location.search,type: "get",success: function(result) {console.log(result)if(result.code == "SUCCESS" && result.data != null && result.data.records != null) {var bookHtml = "";for (var book of result.data.records) {bookHtml += '<tr>';bookHtml += '<td><input type="checkbox" name="selectBook" value="' + book.id + '" id="selectBook" class="book-select"></td>';bookHtml += '<td>' + book.id + '</td>';bookHtml += '<td>' + book.bookName + '</td>';bookHtml += '<td>' + book.author + '</td>';bookHtml += '<td>' + book.count + '</td>';bookHtml += '<td>' + book.price + '</td>';bookHtml += '<td>' + book.publish + '</td>';bookHtml += '<td>' + book.statusCN + '</td>';bookHtml += '<td>';bookHtml += '<div class="op">';bookHtml += '<a href="book_update.html?bookId=' + book.id + '">修改</a>';bookHtml += '<a href="javascript:void(0)" onclick="deleteBook(' + book.id + ')">删除</a>';bookHtml += '</div></td></tr>';}$("tbody").html(bookHtml);}}})}

url中的 currentPage 参数,我们直接使用 location.search(查询url的查询字符串,包含问号) 从url中获取参数信息即可

接下来,我们实现分页

在这里,我们使用了分页插件:jqPaginator分页组件 (keenwon.com)

我们按照 使用说明 文档实现分页

因此,我们继续修改前端代码:

            // 获取图书列表getBookList();function getBookList() {$.ajax({url: "/book/getListByPage" + location.search,type: "get",success: function(result) {console.log(result)console.log(location.search)if(result.code == "SUCCESS" && result.data != null && result.data.records != null) {var bookHtml = "";for (var book of result.data.records) {bookHtml += '<tr>';bookHtml += '<td><input type="checkbox" name="selectBook" value="' + book.id + '" id="selectBook" class="book-select"></td>';bookHtml += '<td>' + book.id + '</td>';bookHtml += '<td>' + book.bookName + '</td>';bookHtml += '<td>' + book.author + '</td>';bookHtml += '<td>' + book.count + '</td>';bookHtml += '<td>' + book.price + '</td>';bookHtml += '<td>' + book.publish + '</td>';bookHtml += '<td>' + book.statusCN + '</td>';bookHtml += '<td>';bookHtml += '<div class="op">';bookHtml += '<a href="book_update.html?bookId=' + book.id + '">修改</a>';bookHtml += '<a href="javascript:void(0)" onclick="deleteBook(' + book.id + ')">删除</a>';bookHtml += '</div></td></tr>';}$("tbody").html(bookHtml);var data = result.data;$("#pageContainer").jqPaginator ({totalCounts: data.total,  // 总记录数pageSize: 5,  // 每页记录数visiblePages: 5,  // 可视页数currentPage: data.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>',//页面初始化和页码点击时都会执行onPageChange: function (page, type) {if(type != 'init'){location.href = "book_list.html?currentPage=" + page;}}});}}})}

 当加载图书列表信息时,同步加载分页信息:

其中分页组件需要:

totalCounts:总记录数

pageSize:每页记录数

visiblePages:可视页数

currentPage:当前页码

在这些信息中,pageSize 和 visiblePages 由前端直接设置,totalCounts、currentPage 直接从后端返回结果中获取(currentPage 也可以从参数中获取,但比较复杂,因此我们使用后端返回的)

 其中,onPageChange:回调函数,当触发换页时(包括初始化第一页),会传入两个参数:

page:目标页的页码,Number类型

type:触发类型,可为 init(初始化),change(点击分页)

 当触发类型不为 init 时,我们跳转到对应分页(若不进行判断,则会在初始化时一直进行跳转)

注意对应保持一致

此时,再次运行程序,访问图书列表展示icon-default.png?t=N7T8http://127.0.0.1:8080/book_list.html

页码正确显示

点击页码,进行跳转:

 

成功跳转 

修改图书

在进入修改页面时,需要显示当前图书信息:

根据图书id,获取当前图书信息

[URL]

GET /book/queryById?bookId=10

[请求参数]

[响应]

{

    "code": "SUCCESS",

    "data": {

        "id": 10,

        "bookName": "图书4",

        "author": "作者4",

        "count": 99,

        "price": 52.0,

        "publish": "出版社4",

        "status": 1,

        "createTime": "2024-06-17T08:28:22.000+00:00",

        "updateTime": "2024-06-17T08:28:22.000+00:00"

    },

    "errorMessage": ""

}

获取成功,返回获取图书信息;获取失败,返回错误信息

点击修改,修改图书信息

[URL]

POST /book/updateBook

[请求参数]

id=10&bookName=图书222

[响应]

{

    "code": "SUCCESS",

    "data": "",

    "errorMessage": ""

}

修改成功,返回空字符串"";修改失败,返回错误信息 

实现服务端代码

BookController:

    /*** 根据图书id获取图书信息*/@RequestMapping("/queryById")public Result<BookInfo> queryById(Integer bookId) {log.info("根据图书id获取图书信息, 接收参数id:{}", bookId);// 参数校验if(bookId == null || bookId <= 0) {return Result.fail("参数错误!");}try {BookInfo bookInfo = bookService.selectById(bookId);if(bookInfo != null) {return Result.success(bookInfo);}else {return Result.fail("获取图书信息失败!");}}catch (Exception e) {log.info("获取图书信息失败, e", e);}return Result.fail("获取图书信息失败!");}/*** 修改图书信息*/@RequestMapping("/updateBook")public Result<String> updateBook(BookInfo bookInfo) {log.info("修改图书信息, 获取参数bookInfo:{}", bookInfo);// 参数校验if(bookInfo.getId() == null || bookInfo.getId() < 0) {return Result.fail("图书id有误!");}// 修改图书int result = bookService.updateById(bookInfo);if(result > 0) {return Result.success("");}else {return Result.fail("修改失败!");}}

BookService:

    public BookInfo selectById(Integer id) {return bookInfoMapper.selectById(id);}public int updateById(BookInfo bookInfo) {return bookInfoMapper.updateBook(bookInfo);}

由于前面我们在编写持久层代码时,并未实现根据图书id查询图书信息,因此我们需要完善持久层代码

    @Select("select id, book_name, author, count, price, publish, `status`, create_time, update_time from book_info" +" where id = #{id} and status != 0")BookInfo selectById(Integer id);
修改客户端代码
    <script>// 获取图书信息$.ajax({url: "/book/queryById" + location.search,type: "get",success: function(result) {if(result.code == "SUCCESS" && result.data != null) {var book = result.data;$("#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({url: "/book/updateBook",type: "post",data: $("#updateBook").serialize(),success: function (result) {console.log(result)if(result.code == "SUCCESS" && result.data == "") {location.href = "book_list.html";} else {alert(result.data);}}});}</script>

我们需要根据图书id来对图书信息进行修改,因此前端需要传递图书id

获取图书id有两种方式:

1. 获取url中参数的值(需要拆分url)

2. 在form表单中,添加一个隐藏输入框,存储图书id,就可以使用 $("#updateBook").serialize() 将图书id与其他信息一起提交给后端

在这里,我们选择第二种方式,即在 form 表单中添加一个隐藏输入框 

重新运行程序, 我们修改id = 27的图书信息:

点击修改后,原图书信息正确显示:

进行修改:

修改成功:

删除图书

删除分为 逻辑删除 物理删除

逻辑删除(软删除,假删除,Soft Delete):即不真正删除数据,而在某行数据上增加类型is_deleted的删除标识,一般使用update语句

物理删除(硬删除):从数据库表中删除一行或一集合数据,一般使用delete语句

因此,删除图书有两种实现方式:

逻辑删除:

update book_info set status = 0 where id = 10

物理删除:

delete from book_info where id = 10

 通常情况下,我们采用逻辑删除的方式,也可以采用 物理删除+归档 的方式

在这里,我们采用 逻辑删除 的方式

因此,此时依旧是更新逻辑,我们可以直接使用修改图书中的代码

[URL]

POST /book/deleteBook

[请求参数]

id=10

[响应]

{

    "code": "SUCCESS",

    "data": "",

    "errorMessage": ""

}

删除成功,返回空字符串"",删除失败,返回错误信息 

实现服务器端代码

BookController:

    /*** 删除图书信息*/@RequestMapping("/deleteBook")public Result<String> deleteBook(BookInfo bookInfo) {log.info("删除图书信息, 获取参数bookInfo:{}", bookInfo);return this.updateBook(bookInfo);}

直接调用 updateBook 方法实现删除

修改客户端代码
            function deleteBook(id) {var isDelete = confirm("确认删除?");if (isDelete) {//删除图书$.ajax({url: "/book/deleteBook",type: "post",data:  {id : id,status: 0 },success: function(result) {if(result.code == "SUCCESS" && result.data == "") {location.href = "book_list.html";}else {alert("删除失败,请联系管理员");}}})}}

当删除成功时,返回图书列表页,删除失败时,弹出提示框 

测试: 

我们删除 id = 27 的图书信息:

删除成功 

批量删除

批量删除,也就是批量修改数据

约定前后端交互接口:

[URL]

POST /book/deleteBook

[请求参数]

id=25&id=26

[响应]

{

    "code": "SUCCESS",

    "data": "",

    "errorMessage": ""

}

当点击 批量删除 按钮时,只需要将复选框选中的图书id发送到后端即可

此时有多个id,因此我们使用List的形式来传递参数

实现服务端代码

BookController:

    /*** 批量删除图书信息*/@RequestMapping(value = "/batchDeleteBook", produces = "application/json")public Boolean batchDeleteBook(@RequestParam List<Integer> ids) {log.info("批量删除图书, ids:{}", ids);try {int result = bookService.batchDeleteBook(ids);}catch (Exception e) {log.error("批量删除图书异常, e", e);return false;}return true;}

在接收集合时,需要使用 @RequestParam 绑定参数关系 

 业务层代码:

BookService:

    public int batchDeleteBook(List<Integer> ids) {return bookInfoMapper.batchDeleteBook(ids);}

由于删除的id是可选的,因此我们使用xml的方式实现

    <update id="batchDeleteBook">update book_info set status = 0where id in<foreach collection="ids" open="(" item="id" close=")" separator=",">#{id}</foreach></update>
修改客户端代码
            function batchDelete() {var isDelete = confirm("确认批量删除?");if (isDelete) {//获取复选框的idvar ids = [];$("input:checkbox[name='selectBook']:checked").each(function () {ids.push($(this).val());});$.ajax({url: "/book/batchDeleteBook?ids=" + ids,type: "post",success: function(result) {if(result.code == "SUCCESS" && result.data == true) {location.href = "book_list.html";}else {alert("批量删除失败,请联系管理员!");}}})}}

重新运行程序,进行测试: 

强制登录

当用户未登录时,不能访问图书相关页面,因此我们使用拦截器拦截前端发来的请求,判断用户是否进行登录,若已登录,则放行;若未登录,则进行拦截,自动跳转到登录页面

在判断用户是否进行登录时,我们可以使用 cookie 和 session ,也可以使用前面学习的JWT令牌:http://t.csdnimg.cn/5toZg,

在这里,我们使用JWT令牌

令牌生成

我们首先引入 JWT令牌的依赖:

		<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>0.11.5</version></dependency><!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>0.11.5</version><scope>runtime</scope></dependency><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred --><version>0.11.5</version><scope>runtime</scope></dependency>

utils 目录下创建 JwtUtils 类:

JwtUtils 中实现令牌的生成和校验:

我们首先实现密钥(密钥是进行签名计算的关键)生成:

我们在 test 目录下实现密钥的生成:

@SpringBootTest
public class JwtUtils {// 生成随机密钥@Testvoid genKey() {SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);String secretStr = Encoders.BASE64.encode(secretKey.getEncoded());System.out.println(secretStr);}
}

运行,得到密钥:

PNYvhIto8tbYt+RWiWHGusQeb8AO5TdCl9zRlqcJToo=

以运行结果作为密钥:

@Slf4j
public class JwtUtils {// 设置令牌过期时间为1hprivate static final long JWT_EXPIRATION = 60 * 60 * 1000;// 密钥private static final String secretStr = "PNYvhIto8tbYt+RWiWHGusQeb8AO5TdCl9zRlqcJToo=";// 生成密钥private static final Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretStr));/*** 生成令牌*/public static String genJwt(Map<String, Object> claim) {// 生成令牌String token = Jwts.builder().setClaims(claim) // 自定义信息.setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION)) // 过期时间.signWith(key).compact();return token;}
}

测试:

    @Testvoid genJwt() {Map<String, Object> claim = new HashMap<>();claim.put("id", 1);claim.put("userName", "zhangsan");System.out.println(JwtUtils.genJwt(claim));}

运行结果:

eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlck5hbWUiOiJ6aGFuZ3NhbiIsImV4cCI6MTcxODY3NzgxMn0.6dz5aMSxXMu_yi9izmVxDgzphPwV3a_a2_7aJCi8qNk

将运行结果复制到官网进行解析:

校验通过

接下来,我们实现令牌的校验:

    /*** 令牌校验*/public static Claims parseToken(String token) {JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();Claims claims = null;try {claims = build.parseClaimsJws(token).getBody();}catch (Exception e) {log.error("解析token失败, e", e);return null;}return claims;}

 我们进行测试,解析刚才生成的令牌:

    @Testvoid parseToken() {String token = "eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwidXNlck5hbWUiOiJ6aGFuZ3NhbiIsImV4cCI6MTcxODY3ODYxNH0.af6Jcqp8PjZUpaA6jn2wX12XACTu7eLvp1sZDNZa3CQ";Claims claims = JwtUtils.parseToken(token);System.out.println(claims);}

运行结果:

与我们在官网解析的结果一致

实现了令牌的生成和校验后,我们就可以实现拦截器了

拦截器

添加拦截器

创建 interceptor 目录,在 interceptor目录下创建 LoginInterceptor

从 header中获取token,并校验token是否合法

@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info("LoginInterceptor preHandle...");// 获取tokenString token = request.getHeader(Constants.REQUEST_HEADER_TOKEN);log.info("从header中获取token:{}", token);// 校验token, 判断是否放行Claims claims = JwtUtils.parseToken(token);if(claims == null) {// 校验失败response.setStatus(401);return false;}// 校验成功,放行return true;}
}

我们将 getHeader 中的字符串作为常量,放到Constans中,若后续修改字符串,我们就只需修改 Constans中的字符串

创建 constant 目录,在目录下创建 Constants:

public class Constants {public static final String REQUEST_HEADER_TOKEN = "user_token_header";}

配置拦截器:

config 目录下创建 WebConfig 类:

并配置拦截路径:

@Component
public class WebConfig implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/user/login").excludePathPatterns("/user/register").excludePathPatterns("/css/**").excludePathPatterns("/js/**").excludePathPatterns("/pic/**").excludePathPatterns("/**/*.html");}
}

在用户登录时发放令牌:

我们在令牌中存放 用户id 和 用户名,同样,我们将这两个 key值存放到 Constants 中:

public class Constants {public static final String REQUEST_HEADER_TOKEN = "user_token_header";public static final String TOKEN_ID = "id";public static final String TOKEN_USERNAME = "userName";
}

修改登录代码: 

    /*** 用户登录*/@RequestMapping("/login")public Result<String> login(String userName, String password) {log.info("用户登录,获取参数userName:{}, password:{}", userName, password);// 参数校验if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) {return Result.fail("用户名或密码为空!");}// 根据用户名进行查询UserInfo userInfo = userService.selectByName(userName);if(userInfo == null) {return Result.fail("用户名或密码错误!");}if(!SecurityUtils.verity(password, userInfo.getPassword())) {return Result.fail("密码错误!");}// 密码正确,返回tokenMap<String, Object> claim = new HashMap<>();claim.put(Constants.TOKEN_ID, userInfo.getId());claim.put(Constants.TOKEN_USERNAME, userName);String token = JwtUtils.genJwt(claim);log.info("UserController 返回token:{}", token);return Result.success(token);}

进行测试:

接下来,我们修改前端代码:

修改 login.html,完善登录方法,前端收到 token 后,将其保存在 localstorage

   <script>function login() {$.ajax({url: "/user/login",type: "post",data: {userName: $("#userName").val(),password: $("#password").val()},success: function(result) {if(result.code == "SUCCESS" && result.data != null) {localStorage.setItem("user_token", result.data);location.href = "book_list.html";}else {alert(result.errorMessage);}}});}</script>

由于我们访问图书列表页、添加图书页、修改图书页都需要获取浏览器保存的令牌,因此,我们将代码提取到 common.js 中,

js 目录下创建 common.js,在 common.js 中添加 ajaxSend() 方法

ajaxSend()方法是在ajax请求开始时执行的函数,其中

e:包含 event 对象

xhr:包含 XMLHttpRequest 对象

opt:包含 ajax 请求中使用的选项

$(document).ajaxSend(function(e, xhr, opt){var token = localStorage.getItem("user_token");xhr.setRequestHeader("user_token_header", token);
});

然后在对应页面(book_list.html、book_add.html、book_update.html)引入 common.js

<script src="js/common.js"></script>

 修改book_add.html,添加失败处理代码,使用 location.href 进行页面跳转

    <script type="text/javascript" src="js/jquery.min.js"></script><script src="js/common.js"></script><script>function add() {$.ajax({url: "/book/addBook",type: "post",data: $("#addBook").serialize(),success: function(result) {if(result.code == 'SUCCESS' && result.data == "") {// 添加成功,返回图书列表页location.href = "book_list.html";} else {alert(result.data);}}, error: function (error) {if(error != null && error.status == 401) {location.href = "login.html";}}})}</script>

book_list.html、book_update.html 页面也是相同修改方式

修改完成后,我们再次运行程序,进行测试: 

我们尝试直接访问图书列表页:127.0.0.1:8080/book_list.html

此时由于未登录,因此跳转到登录页面:

进行登录,此时成功跳转,前端存储token:

关于 图书管理系统,本篇文章到此为止,关于更多的功能(搜索图书、退出登录等),大家可以自行继续实现

完整代码存放在:

项目完整代码/图书管理系统/spring-book · Echo/project - 码云 - 开源中国 (gitee.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/360308.html

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

相关文章

Mac 安装HomeBrew(亲测成功)

1、终端安装命令&#xff1a; /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"执行后&#xff0c;没有安装git&#xff0c;会先安装&#xff0c;安装后再执行一下命令。 2、根据中文选择源安装 3、相关命令 查看版本号&a…

本地服务怎么发布成rpc服务

目录 1.引入 2.user.proto 3.userservice.cc 1.引入 example文件夹作为我们框架项目的使用实例&#xff0c;在example文件夹下创建callee和caller两个文件夹 callee是RPC服务的提供者。在callee创建一个文件&#xff1a;userservice.cc 我们有没有这样一个框架&#xff0c;把…

【面试题】MySQL数据库

目录 什么是视图&#xff0c;视图的作用是什么&#xff1f;什么是索引&#xff1f;MySQL中有哪些类型的索引&#xff1f;简述索引设计原则&#xff1f;简述索引的数据结构&#xff1f;简述Hash 和 B 树索引的区别&#xff1f;列出MySQL中导致索引失效的情况&#xff1f;简述数据…

mysql窗口函数排名查询 与 连续出现的数字查询

排名查询 学会这一个查询&#xff0c;我们应该对该类型的查询 方法就能有一个了解&#xff0c;不然 如果下次遇到该类型的查询&#xff0c;我们依然分析不出 给你一张表&#xff0c;里面有id 和score字段&#xff0c;根据score的分数大小 排序 &#xff0c;假如有相同的分数&…

【山东】2024年夏季高考文化成绩一分一段表

文末有图片版&#xff0c;可直接保存下载&#xff01;&#xff01; 2024年夏季高考文化成绩一分一段表分数段全体-选考物理-选考化学-选考生物-选考思想政治-选考历史-选考地理分数段本段人数累计人数本段人数累计人数本段人数累计人数本段人数累计人数本段人数累计人数本段人…

Upload-Labs-Linux1 使用 一句话木马

解题步骤&#xff1a; 1.新建一个php文件&#xff0c;编写内容&#xff1a; <?php eval($_REQUEST[123]) ?> 2.将编写好的php文件上传&#xff0c;但是发现被阻止&#xff0c;网站只能上传图片文件。 3.解决方法&#xff1a; 将php文件改为图片文件&#xff08;例…

毕业生离校系统

摘 要 随着信息技术的快速发展和普及&#xff0c;越来越多的高校开始利用信息化手段来提升管理和服务效率。毕业生离校是高校管理工作中的一个重要环节&#xff0c;涉及到毕业生的个人信息、学业成绩、离校手续等多个方面。传统的离校流程往往繁琐、耗时&#xff0c;且容易出现…

Apple - Framework Programming Guide

本文翻译自&#xff1a;Framework Programming Guide&#xff08;更新日期&#xff1a;2013-09-17 https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Frameworks.html#//apple_ref/doc/uid/10000183i 文章目录 一、框架编程指南简介…

永洪bi里topN的设置/用法

要实现的效果&#xff1a;实现通过输入参数&#xff0c;进行图表top的排序筛选 图示&#xff1a; 筛选前&#xff1a; 输入3&#xff0c;看top3的值&#xff1a; 输入-3&#xff0c;看倒数3个的值&#xff1a; 设置步骤&#xff1a; 1️⃣&#xff1a;添加一个“文本参数组件…

打造智能家居:用ESP32轻松实现无线控制与环境监测

ESP32是一款集成了Wi-Fi和蓝牙功能的微控制器&#xff0c;广泛应用于物联网项目。它由Espressif Systems公司开发&#xff0c;具有强大的处理能力和丰富的外设接口。下面我们将详细介绍ESP32的基础功能和引脚功能&#xff0c;并通过具体的实例项目展示其应用。 主要功能 双核处…

找不到mfc140u.dll怎么修复,mfc140u.dll丢失的多种修复方法

计算机丢失mfc140u.dll文件会导致依赖该文件的软件无法正常运行。mfc140u.dll是Microsoft Visual C 2015的可再发行组件之一&#xff0c;它属于Microsoft Foundation Class (MFC) 库&#xff0c;许多使用MFC开发的程序需要这个DLL文件来正确执行。丢失了mfc140u.dll文件。会导致…

数据结构需要每个都具体实现吗?

在开始前刚好我有一些资料&#xff0c;是我根据网友给的问题精心整理了一份「数据结构的资料从专业入门到高级教程」&#xff0c; 点个关注在评论区回复“666”之后私信回复“666”&#xff0c;全部无偿共享给大家&#xff01;&#xff01;&#xff01;用c的stl能刷算法题是不…

水浅王八多

今天有三个被自媒体和韭菜们转疯的视频。 &#xff08;1&#xff09; 财政部公布&#xff1a;今年1-5月份证券交易印花税&#xff0c;同比去年1-5月份&#xff0c;降低50.8%。 其实是&#xff1a;2023年8月27日&#xff0c;为活跃资本市场&#xff0c;财政部、证监会和三大交易…

wondershaper 一款限制 linux 服务器网卡级别的带宽工具

文章目录 一、关于wondershaper二、文档链接三、源码下载四、限流测试五、常见报错1. /usr/local/sbin/wondershaper: line 145: tc: command not found2. Failed to download metadata for repo ‘appstream‘: Cannot prepare internal mirrorlist: No URLs.. 一、关于wonder…

【银河麒麟】云平台查看内存占用与实际内存占用不一致,分析处理过程,附代码

1.需求/问题描述 发现云平台查看内存占用与实际内存占用不一致。 2.分析过程 在系统中获取虚拟机内存使用率目前主要有两种方式&#xff0c;一种是通过virsh dommemstat获取&#xff0c;另外一种是通过qga接口获取。由于之前修复界面虚拟机cpu使用率时为qga接口获取&#xff…

MCP2515汽车CAN总线支持SPI接口的控制器芯片替代型号DPC15

器件概述 DPC15是一款独立CAN控制器&#xff0c;可简化需要与CAN总线连接的应用。可以完全替代兼容MCP2515 图 1-1 简要显示了 DPC15 的结构框图。该器件主要由三个部分组成&#xff1a; 1. CAN 模块&#xff0c;包括 CAN 协议引擎、验收滤波寄存 器、验收屏蔽寄存器、发送和接…

SpringBoot2+Vue3开发博客管理系统

项目介绍 博客管理系统&#xff0c;可以帮助使用者管理自己的经验文章、学习心得、知识文章、技术文章&#xff0c;以及对文章进行分类&#xff0c;打标签等功能。便于日后的复习和回忆。 架构介绍 博客管理系统采用前后端分离模式进行开发。前端主要使用技术&#xff1a;Vu…

VBA技术资料MF165:关闭当前打开的所有工作簿

我给VBA的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。“VBA语言専攻”提供的教程一共九套&#xff0c;分为初级、中级、高级三大部分&#xff0c;教程是对VBA的系统讲解&#…

宠物空气净化器哪家强?希喂、小米、安德迈谁最具性价比?

猫咪掉毛是一种正常的生理现象&#xff0c;每只猫咪都会周期性地更换毛发。但是&#xff0c;当您发现家里的沙发、地毯、衣物、甚至空气中都漂浮着难以清理的猫浮毛时。还是会很烦恼&#xff0c;最重要的是空气中的浮毛如果不及时清理的话长时间停留在空气中会对身体造成一定威…

2021数学建模C题目– 生产企业原材料的订购与运输

C 题——生产企业原材料的订购与运输 思路&#xff1a;该题主要是通过对供应商的供货能力和运送商的运货能力进行估计&#xff0c;给出合适的材料订购方案 程序获取 第一题问题思路与结果&#xff1a; 对 402 家供应商的供货特征进行量化分析&#xff0c;建立反映保障企业生…