珠海网站建设推广服务,网站建设ip,电子商务网站开发报价,科迪兔网站建设目录
项目分析
实现页面
功能描述
页面预览
准备工作
数据准备
创建数据库
用户表
创建项目
导入前端页面
测试前端页面
后端代码实现
项目公共模块
实体类
公共层
统一结果返回
统一异常处理
业务实现
持久层
用户登录
用户注册
密码加密验证
添加图书…目录
项目分析
实现页面
功能描述
页面预览
准备工作
数据准备
创建数据库
用户表
创建项目
导入前端页面
测试前端页面
后端代码实现
项目公共模块
实体类
公共层
统一结果返回
统一异常处理
业务实现
持久层
用户登录
用户注册
密码加密验证
添加图书
图书列表
修改图书
删除图书
批量删除
强制登录
令牌生成
拦截器 在学习了 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?characterEncodingutf8useSSLfalseusername: 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目录下 测试前端页面
我们运行程序访问前端页面登录http://127.0.0.1:8080/login.html 其他页面就不再一一展示了大家自行进行测试
前端页面正确显示项目的准备工作完成
后端代码实现
项目可分为 控制层Controller服务层Service持久层Mapper还有实体类和公共层 我们首先根据需求实现项目公共模块即 实体类 和 公共层
项目公共模块
实体类
需要创建两个实体UserInfo类 和 BookInfo类
创建 model 目录在 model 目录下根据数据表创建 UserInfo 和 BookInfo
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 ResultT {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;}SneakyThrowsOverridepublic 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 )ListBookInfo 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 version1.0 encodingUTF-8?
!DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd
mapper namespacecom.example.springbook.mapper.BookInfoMapperupdate idupdateBookupdate book_infosetif testbookName ! nullbook_name #{bookName},/ifif testauthor ! nullauthor #{author},/ifif testcount ! nullcount #{count},/ifif testpublish ! nullpublisht #{publish},/ifif teststatus ! nullstatus #{status},/if/setwhere 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 [请求参数] userNameadminpasswordadmin [响应] { code: SUCCESS, data: , errorMessage: } 当登录成功时返回数据为空字符串 登录失败时返回错误信息可自行进行定义
实现服务端代码
创建controller目录再在目录下创建UserController类
在UserController中补充代码
先进行参数校验校验通过后查询用户信息
无论前端是否进行了参数校验后端一律需要进行校验这是因为后端接口可能会被黑客攻击不通过前端来访问若后端不进行校验就会产生脏数据
Slf4j
RestController
RequestMapping(/user)
public class UserController {Autowiredprivate UserService userService;RequestMapping(/login)public ResultString 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 格式的数据根据响应数据构造页面内容 scriptfunction 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 [请求参数] userNamewangwupasswordwangwu [响应] { code: SUCCESS, data: , errorMessage: } 当注册成功时返回数据为空字符串 注册失败时返回错误信息
实现服务端代码
在 UserController 中补充代码
先进行参数校验校验通过后添加用户信息 /*** 用户注册*/RequestMapping(/register)public ResultString 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()中代码 scriptfunction 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图书11author作者11count23price12.3publish出版社11status1 [响应] { code: SUCCESS, data: , errorMessage: } 当添加成功时返回数据为空字符串 添加失败时返回错误信息
实现服务端代码
在 controller 目录下创建 BookController
在BookController中补充代码
先进行参数校验校验通过后添加图书信息
public class BookController {Autowiredprivate BookService bookService;RequestMapping(/addBook)public ResultString 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);}
}
同样我们补全前端代码后一起进行测试
修改客户端代码 scriptfunction 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 PageResultT {private Integer total; //总记录数private ListT records; // 当前页数据private PageRequest pageRequest;
}
currentPage 封装在 PageRequest 中因此我们直接将 PageRequest 封装在 PageResult 中
接着基于上述分析我们来约定前后端交互接口 [URL] GET /book/getListByPage?currentPage1 [请求参数] [响应] { 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.00000:00, updateTime: 2024-06-17T08:28:22.00000:00 }, ....., ] }, errorMessage: 当浏览器给服务器发送一个 /book/getListByPage 请求时通过 currentPage 参数告诉服务器当前请求为第几页数据后端根据请求参数返回对应页的数据第一页可以不传参currentPage默认值为1
实现服务端代码
完善BookController中代码 RequestMapping(/getListByPage )public ResultPageResultBookInfo getListByPage(PageRequest pageRequest) {log.info(获取图书列表, 接收到参数pageRequest:{}, pageRequest);PageResultBookInfo pageResult bookService.getListByPage(pageRequest);if(pageResult null) {return Result.fail(获取图书列表失败!);}return Result.success(pageResult);}
业务层
BookService public PageResultBookInfo getListByPage(PageRequest pageRequest) {// 获取总记录数Integer total bookInfoMapper.count();// 获取当前页记录ListBookInfo 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 PageResultBookInfo getListByPage(PageRequest pageRequest) {// 获取总记录数Integer total bookInfoMapper.count();// 获取当前页记录ListBookInfo 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})ListBookInfo 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?currentPage1 成功获取记录 1 - 5条记录按照id进行降序排列也可以改为升序
修改客户端代码 访问第一页图书的前端url为http://127.0.0.1:8080/book_list.html?pageNum1 访问第二页图书的前端url为http://127.0.0.1:8080/book_list.html?pageNum2 当浏览器访问book_list.html页面时就请求后端将后端返回的数据显示在页面上
调用后端请求 /book/getListByPage?currentPage1
修改js将后端请求方法修改为 /book/getListByPage?currentPage1
// 获取图书列表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 tdinput typecheckbox nameselectBook value book.id idselectBook classbook-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 classop;bookHtml a hrefbook_update.html?bookId book.id 修改/a;bookHtml a hrefjavascript:void(0) onclickdeleteBook( 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 tdinput typecheckbox nameselectBook value book.id idselectBook classbook-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 classop;bookHtml a hrefbook_update.html?bookId book.id 修改/a;bookHtml a hrefjavascript:void(0) onclickdeleteBook( 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 classpage-itema classpage-link首页/a/li,prev: li classpage-itema classpage-link hrefjavascript:void(0);上一页\/a\/li,next: li classpage-itema classpage-link hrefjavascript:void(0);下一页\/a\/li,last: li classpage-itema classpage-link hrefjavascript:void(0);最后一页\/a\/li,page: li classpage-itema classpage-link hrefjavascript: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 时我们跳转到对应分页若不进行判断则会在初始化时一直进行跳转 注意对应保持一致
此时再次运行程序访问图书列表展示http://127.0.0.1:8080/book_list.html 页码正确显示
点击页码进行跳转 成功跳转
修改图书
在进入修改页面时需要显示当前图书信息 根据图书id获取当前图书信息 [URL] GET /book/queryById?bookId10 [请求参数] [响应] { code: SUCCESS, data: { id: 10, bookName: 图书4, author: 作者4, count: 99, price: 52.0, publish: 出版社4, status: 1, createTime: 2024-06-17T08:28:22.00000:00, updateTime: 2024-06-17T08:28:22.00000:00 }, errorMessage: } 获取成功返回获取图书信息获取失败返回错误信息
点击修改修改图书信息 [URL] POST /book/updateBook [请求参数] id10bookName图书222 [响应] { code: SUCCESS, data: , errorMessage: } 修改成功返回空字符串修改失败返回错误信息
实现服务端代码
BookController /*** 根据图书id获取图书信息*/RequestMapping(/queryById)public ResultBookInfo 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 ResultString 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 [请求参数] id10 [响应] { code: SUCCESS, data: , errorMessage: } 删除成功返回空字符串删除失败返回错误信息
实现服务器端代码
BookController: /*** 删除图书信息*/RequestMapping(/deleteBook)public ResultString 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 [请求参数] id25id26 [响应] { code: SUCCESS, data: , errorMessage: } 当点击 批量删除 按钮时只需要将复选框选中的图书id发送到后端即可
此时有多个id因此我们使用List的形式来传递参数
实现服务端代码
BookController /*** 批量删除图书信息*/RequestMapping(value /batchDeleteBook, produces application/json)public Boolean batchDeleteBook(RequestParam ListInteger 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(ListInteger ids) {return bookInfoMapper.batchDeleteBook(ids);}
由于删除的id是可选的因此我们使用xml的方式实现 update idbatchDeleteBookupdate book_info set status 0where id inforeach collectionids open( itemid close) separator,#{id}/foreach/update
修改客户端代码 function batchDelete() {var isDelete confirm(确认批量删除?);if (isDelete) {//获取复选框的idvar ids [];$(input:checkbox[nameselectBook]: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 --dependencygroupIdio.jsonwebtoken/groupIdartifactIdjjwt-api/artifactIdversion0.11.5/version/dependency!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl --dependencygroupIdio.jsonwebtoken/groupIdartifactIdjjwt-impl/artifactIdversion0.11.5/versionscoperuntime/scope/dependencydependencygroupIdio.jsonwebtoken/groupIdartifactIdjjwt-jackson/artifactId !-- or jjwt-gson if Gson is preferred --version0.11.5/versionscoperuntime/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);}
}
运行得到密钥
PNYvhIto8tbYtRWiWHGusQeb8AO5TdCl9zRlqcJToo
以运行结果作为密钥
Slf4j
public class JwtUtils {// 设置令牌过期时间为1hprivate static final long JWT_EXPIRATION 60 * 60 * 1000;// 密钥private static final String secretStr PNYvhIto8tbYtRWiWHGusQeb8AO5TdCl9zRlqcJToo;// 生成密钥private static final Key key Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretStr));/*** 生成令牌*/public static String genJwt(MapString, Object claim) {// 生成令牌String token Jwts.builder().setClaims(claim) // 自定义信息.setExpiration(new Date(System.currentTimeMillis() JWT_EXPIRATION)) // 过期时间.signWith(key).compact();return token;}
}
测试 Testvoid genJwt() {MapString, 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 ResultString 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(密码错误!);}// 密码正确返回tokenMapString, 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 中 scriptfunction 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 srcjs/common.js/script 修改book_add.html添加失败处理代码使用 location.href 进行页面跳转 script typetext/javascript srcjs/jquery.min.js/scriptscript srcjs/common.js/scriptscriptfunction 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)