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

目录

项目分析

实现页面

功能描述

页面预览

准备工作

数据准备

创建数据库

用户表

创建项目

导入前端页面

测试前端页面

后端代码实现

项目公共模块

实体类

公共层

统一结果返回

统一异常处理

业务实现

持久层

用户登录

用户注册

密码加密验证

添加图书

图书列表

修改图书

删除图书

批量删除

强制登录

令牌生成

拦截器


在学习了 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.mzph.cn/news/860346.shtml

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

相关文章

Cesium默认bing地图数据,还支持哪些地图的数据源呢?

传统的前端开发增长乏力了&#xff0c;新兴的web3D方向前端开发需求旺盛&#xff0c;这一块在国外很成熟&#xff0c;在国内兴起不久&#xff0c; 甚至很多前端老铁都没听过&#xff0c;没见过&#xff0c;没有意识到&#xff0c;前端除了框架、vue、uniapp这些烂大街的&#x…

黑马苍穹外卖7 用户下单+订单支付(微信小程序支付流程图)

地址簿 数据库表设计 就是基本增删改查&#xff0c;与前面的类似。 用户下单 用户点餐业务流程&#xff1a; 购物车-订单提交-订单支付-下单成功 展示购物车数据&#xff0c;不需要提交到后端 数据库设计&#xff1a;两个表【订单表orders&#xff0c;订单明细表order_d…

cnpm run dev 报错 Error: Cannot find module ‘fs/promises’

主要原因是babel版本冲突 卸载以下依赖可以解决问题&#xff1a; 之后重新安装babel-loader依赖 可能会报以下错误&#xff1a; 接着安装babel-core依赖 项目顺利启动

【启明智显分享】低成本RISC-V工业级HMI方案推荐

伴随着工业4.0的迅猛发展&#xff0c;工业HMI以方便、快捷的特点逐渐成为工业的日常应用&#xff0c;成为备受追捧的全新多媒体交互设备。 什么是工业HMI&#xff1f;工业HMI是用于工业自动化系统中的人机交互界面&#xff0c;通常由触摸屏、按钮、指示灯、显示器等组成&#…

如何正确使用C#短信接口发送招生短信

群发短信对教育机构来讲虽然是个不错的招生工具,但怎么使用决定着生源转化效率,如果是为了单纯的发短信而发短信效率当然不好,那么如何正确使用招生群发短信呢?技巧才是关键! 教育短信发送较多的就是招生群发短信内容,而运营商对教育行业内容审核一般比较严格,需要短信公司特殊…

新媒体矩阵系统是什么?怎么搭建矩阵系统?

目录 前言&#xff1a; 一、新媒体矩阵分别是什么&#xff1f; 1、横向矩阵 2、 纵向矩阵 二、新媒体矩阵的作用&#xff1f; 1、多元化发展&#xff0c;吸引目标 2、多平台协同&#xff0c;放大宣传效果 3、多平台运营&#xff0c;分散风险 三、怎么做矩阵系统&…

【摄像头标定】双目摄像头标定及矫正-opencv(python)

双目摄像头标定及矫正 棋盘格标定板标定矫正 棋盘格标定板 本文使用棋盘格标定板&#xff0c;可以到这篇博客中下载&#xff1a;https://blog.csdn.net/qq_39330520/article/details/107864568 标定 要进行标定首先需要双目拍的棋盘格图片&#xff0c;20张左右&#xff0c;…

企业微信内嵌H5项目接入聊天功能

产品需求是,在列表中把符合条件的列表接入聊天功能,以下是详细步骤: 1.引入企业微信 <script src"https://res.wx.qq.com/wwopen/js/jsapi/jweixin-1.0.0.js"></script> 2.获取wx签名(必须要) /*** 获取wx签名**/ export function getWxJsApi(data) {r…

pdf压缩,pdf压缩在线,pdf文件太大怎么变小

在数字化时代&#xff0c;PDF文档因其跨平台、保持原样、易于阅读和打印等特点&#xff0c;成为了我们日常工作和生活中不可或缺的一部分。然而&#xff0c;随着PDF文件的不断累积&#xff0c;存储空间逐渐变得紧张&#xff0c;特别是在处理大量大型PDF文件时&#xff0c;如何有…

若依前后端分离项目整合shardingjdbc分表(详细,分片字段订单id)

文章目录 1. 引入Maven依赖2.引入配置文件3.兼容之前的数据库源,使用现在的sharding数据库源&#xff08;shardingjdbc默认的数据源&#xff09;&#xff0c;但是配置好文件之后是没有生效的&#xff0c;需要加配置文件覆盖4. 检测是否成功5. 如何使用&#xff0c;在需要使用的…

qt开发-14_QListwidget 仿qq好友列表制作

QListWidget 继承 QListView。QListWidget 类提供了一个基于项的列表小部件。QListWidg et 是一个便捷的类&#xff0c;它提供了一个类似于 QListView&#xff08;下一小节将讲到&#xff09;提供的列表视图&#xff0c;但 是提供了一个用于添加和删除项目的基于项目的经典接口…

第10章 启动过程组 (启动过程组的重点工作)

第10章 启动过程组 10.3启动过程组的重点工作&#xff0c;在第三版教材第362~364页&#xff1b; 文字图片音频方式 第一个知识点&#xff1a;项目启动会议 1、作用 标志着对项目经理责权的定义结果的正式公布&#xff0c;通常由项目经理负责组织和召开。2、目的 使项目各…

分享:Wordpress插件-AI Image Pro v2.6.0中文版语言包

AI Image Pro是一个wordpress插件,可将您的 WordPress 网站直接与最佳图像生成 A模型(DALL.E和稳定扩散)集成&#xff0c;并允许您利用 AI 生成图像、编辑(内画)图像和创建图像变体。最重要的是&#xff0c;它配备了许多一键式滤镜和许多微调选项,您可以使用它们来增强和创建令…

黑匣子问题:大语言模型的内部工作原理

像 GPT-3 这样的大型语言模型 (LLM) 已经展示了令人印象深刻的自然语言能力&#xff0c;但它们的内部工作原理仍然知之甚少。这种“黑匣子”性质使得ChatGPT在敏感的现实应用程序中部署时可能会出现问题。 什么是LLM黑匣子问题&#xff1f; 语言学习模型 (LLM) 是强大的工具&…

要求全国70%中医院设置康复科!康复科门诊服务这样建设!

近日&#xff0c;国家中医药管理局印发《国家中医药管理局关于进一步加强中医医院康复科建设的通知》提出&#xff0c;中医医院应根据当地人口规模及中医药康复服务需求设置康复科并作为独立科室进行建设和管理。有条件的二级以上中医医院应当按照《中医医院康复科推荐配置标准…

软件质量保证与测试

目录 一、测试流程 二、测试用例 2.1概念 2.2用例编写格式 三、设计测试点 3.1等价类 3.1.1概念 3.1.2案例 3.1.3适用场景 3.1.4执行用例 3.2边界值 3.2.1概念 3.2.2案例 3.2.3使用场景 3.3判定表 3.3.1判定表使用原因 3.3.2概念 3.3.3案例 3.3.4使用场景 …

Day4: 两两交换链表中的节点 24 删除链表的倒数第N个节点 19 链表相交 02.07 环形链表II 142

题目24. 两两交换链表中的节点 - 力扣&#xff08;LeetCode&#xff09; /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(nullptr) {}* …

新鲜出炉的信息化一机两用方案

在信息化日益发展的今天&#xff0c;网络安全问题愈发凸显其重要性。尤其是在政府和企事业单位中&#xff0c;如何在保证业务流畅和工作效率的同时&#xff0c;确保信息高安全性&#xff0c;成为了一个亟待解决的问题。而“一机两用”政策&#xff0c;正是针对这一需求而提出的…

QT布局管理(分割窗口QSplitter类、停靠窗口QDockWidget类、堆栈窗体QStackedWidget类、基本布局QLayout)

此片文章简单介绍布局管理的使用方法。通过实例先分别介绍分隔窗口QSplitter类、停靠窗口QDockWidget类及QStackedWidget类的使用&#xff0c;最后再通过一个实例介绍QLayout的使用。 分割窗口QSplitter类 分隔窗口可以灵活地布局窗口&#xff0c;可以用在文件资源管理器地窗…

【2024最新华为OD-C/D卷试题汇总】[支持在线评测] 任务积分优化问题(100分) - 三语言AC题解(Python/Java/Cpp)

🍭 大家好这里是清隆学长 ,一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-C/D卷的三语言AC题解 💻 ACM银牌🥈| 多次AK大厂笔试 | 编程一对一辅导 👏 感谢大家的订阅➕ 和 喜欢💗 📎在线评测链接 任务积分优化问题(100分) 🌍 评测功能需要 订阅专栏 后私信…