1.创建项目
流程与之前的项目一致,不再进行赘述。
2.需求定义
需求:
1. 登录: ⽤⼾输⼊账号,密码完成登录功能
2. 列表展⽰: 展⽰图书
3.前端界面测试
无法启动!!!--->记得加入mysql相关操作记得在yml进行配置
配置后启动成功!!
我们在测试前端的时候同时可以对用户的需求进行分析。
3.1 需求分析
3.1.1 用户登录
3.1.2 图书列表
4.接口定义
4.1 登录接口
[URL]POST /user/login[ 请求参数 ]name=admin&password=admin[ 响应 ]true // 账号密码验证成功false// 账号密码验证失败
4.2 图书列表展示
[URL]POST /book/getList[ 请求参数 ]⽆[ 响应 ]返回图书列表[{"id": 1,"bookName": " 活着 ","author": " 余华 ","count": 270,"price": 20,"publish": " 北京⽂艺出版社 ","status": 1,"statusCN": " 可借阅 "},...]
5.服务器代码
5.1 建包
目前我们先建立四个包,
由于现在还没有引入数据库,
dao是mock的数据。
controller是表现层,
model是图书类,
逻辑是controller调用service(写一些逻辑代码),service调用controller。
5.2 建立数据类
5.2.1 图书添加接口
5.2.1.1 图书类(model)
//@Data 注解会帮助我们⾃动⼀些⽅法, 包含getter/setter, equals, toString等
package com.example.demo.model;
import lombok.Data;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.Date;
@Data
@Component
public class BookInfo {
//图书ID
private Integer id;
//书名
private String bookName;
//作者
private String author;
//数量
private Integer count;
//定价
private BigDecimal price;
//出版社
private String publish;
//状态 0-⽆效 1-允许借阅 2-不允许借阅
private Integer status;
private String statusCN;
//创建时间
private Date createTime;
//更新时间
private Date updateTime;
}
5.2.1.2 数据层(dao/mapper)
//数据访问层: 负责数据访问操作,包括数据的增、删、改、查
//mock了一些假信息
package com.example.demo.dao;
import com.example.demo.model.BookInfo;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class BookDao {
public List<BookInfo> mockData(){
List<BookInfo> books = new ArrayList<>();
for (int i=0;i<5;i++){
BookInfo book = new BookInfo();
book.setId(i);
book.setBookName("书籍"+i);
book.setAuthor("作者"+i);
book.setCount(i*5+3);
book.setPrice(new BigDecimal(new Random().nextInt(100)));
book.setPublish("出版社"+i);
book.setStatus(1);
books.add(book);
}
return books;
}
}
5.2.1.3 业务逻辑层(service)
业务逻辑层: 处理具体的业务逻辑
package com.example.demo.service;
import com.example.demo.dao.BookDao;
import com.example.demo.model.BookInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.awt.print.Book;
import java.util.List;
@Service
public class BookService {
BookDao bookDao=new BookDao();
public List<BookInfo> getBookList(){
List<BookInfo> bookInfos=bookDao.mockData();
for(BookInfo bookInfo:bookInfos){
if(bookInfo.getStatus()==1){
bookInfo.setStatusCN("可借阅");
}else{
bookInfo.setStatusCN("不可借阅");
}
}
return bookInfos;
}
}
5.2.1.4 控制层(controller)
@RestController
@RequestMapping("/book")
public class BookController {
@RequestMapping("/getList")
public List<BookInfo> getList(){
BookService bookService=new BookService();
return bookService.getBookList();
}
}
5.2.1.5 测试前端接口
上面完成了图书页面展示,我们先来测试以下前端接口
成功!!!
5.2.1.6 前后端交互
我们想要页面加载时就显示图书信息,可以直接写ajax相关语句
为了美观,我们定义一个函数,在函数内部写相关语句,在进行调用。
getBookList();
function getBookList() {
$.ajax({
type: "post",
url: "/book/getList",
//我们想把相关语句,存入<tbody>内,循环
success: function(result){
console.log(result);//控制台进行打印,方便我们进行检查,看我们是否从服务器拿到信息,可以定位错误
if(result!=null){
var finalHtml="";
for(var book of result){
finalHtml += '<tr>';
finalHtml += '<td><input type="checkbox" name="selectBook" value="1" id="selectBook" class="book-select"></td>';
finalHtml += '<td>' + book.id + '</td>';
finalHtml += '<td>' + book.bookName + '</td>';
finalHtml += '<td>' + book.author + '</td>';
finalHtml += '<td>' + book.count + '</td>';
finalHtml += '<td>' + book.price + '</td>';
finalHtml += '<td>' + book.publish + '</td>';
finalHtml += '<td>' + book.statusCN + '</td>';
finalHtml += '<td><div class="op">';
finalHtml += '<a href="book_update.html?bookId=' + book.id +'">修改</a>';
finalHtml += '<a href="javascript:void(0)" onclick="deleteBook('+book.id+')">删除</a>';
finalHtml += '</div></td>';
finalHtml += "</tr>";
}
}
$("tbody").html(finalHtml);
}
});
}
正确!!!
5.2.2 用户登录接口
5.2.2.1 控制层
@RequestMapping("/login")
public Boolean login(String password, String admin, HttpSession session){
//账号为空,错误
if(!StringUtils.hasLength(password)||!StringUtils.hasLength(admin)){
return false;
}
if("admin".equals(admin)&&"password".equals(password)){
session.setAttribute("admin",admin);
return true;
}
return false;
}
5.2.2.2 测试
成功!!!
5.2.2.3 前后端交互
function login() {
$.ajax({
type: "post",
url: "/user/login",
data:{
admin: $("#userName").val(),
password: $("#password").val()
},
success: function(result){
if(result==true){
location.href = "book_list.html";
}else{
alert("账号或密码不正确!!!");
}
}
});
}
输入密码正确,成功!!!
6.引入Mybatis
前面我们的数据是Mock的,我们为了让数据永久存储,引入数据库内容。
6.1 数据库表设计
数据库表设计是依据业务需求来设计的. 如何设计出优秀的数据库表, 与经验有很⼤关系.
数据库表通常分两种: 实体表和关系表.
分析我们的需求, 图书管理系统相对来说⽐较简单, 只有两个实体: ⽤⼾和图书, 并且⽤⼾和图书之间没有关联关系
表的具体字段设计, 也与需求有关.
⽤⼾表有⽤⼾名和密码即可(复杂的业务可能还涉及昵称, 年龄等资料)
图书表有哪些字段, 也是参考需求⻚⾯(通常不是⼀个⻚⾯决定的, ⽽是要对整个系统进⾏全⾯分析观察后定的)
6.1.1 创建数据库 book_test
6.1.1.2 用户表
-- 创建数据库
DROP DATABASE IF EXISTS book_test;
CREATE DATABASE book_test DEFAULT CHARACTER SET utf8mb4;
USE book_test;
-- 创建用户表
DROP TABLE IF EXISTS user_info;
CREATE TABLE user_info (
`id` INT NOT NULL PRIMARY KEY 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(),
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", "123456" );
6.1.1.3 图书表
-- 图书表
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 ('活
着', '余华', 29, 22.00, '北京文艺出版社');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('平凡的
世界', '路遥', 5, 98.56, '北京文艺出版社');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('三
体', '刘慈欣', 9, 102.67, '重庆出版社');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('金字塔
原理', '麦肯锡', 16, 178.00, '民主与建设出版社');
INSERT INTO `book_info` (book_name,author,count, price, publish) VALUES ('二十四而立',
'张艺兴', 16, 107.00, '民主与建设出版社');
6.2 引⼊MyBatis 和MySQL 驱动依赖
6.3 配置数据库&⽇志
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/book_test?characterEncoding=utf8&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
# configuration-properties:
map-underscore-to-camel-case: true #配置驼峰⾃动转换
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印sql语句
# 设置⽇志⽂件的⽂件名
logging:
file:
name: spring-book.log
6.4 服务器代码
6.4.1 Model包
//要与数据库相对应
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;
}
package com.example.demo.model;
import lombok.Data;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.Date;
@Data
@Component
public class BookInfo {
//图书ID
private Integer id;
//书名
private String bookName;
//作者
private String author;
//数量
private Integer count;
//定价
private BigDecimal price;
//出版社
private String publish;
//状态 0-⽆效 1-允许借阅 2-不允许借阅
private Integer status;
private String statusCN;
//创建时间
private Date createTime;
//更新时间
private Date updateTime;
}
6.4.2 用户登录
6.4.2.1 约定前后端交互接口
[请求]
/user/login
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
[参数]
name=zhangsan&password=123456
[响应]
true //账号密码验证正确, 否则返回false
浏览器给服务器发送 /user/login 这样的 HTTP 请求, 服务器给浏览器返回了⼀个Boolean 类型的数据. 返回true, 表示账号密码验证正确
实现服务器代码
6.4.2.2 Mapper包
记住:url也要与数据库相互对应
@Mapper
public interface UserInfoMapper {
@Select("select * from user_info where delete_flag=0 and user_name=#{userName}")
List<UserInfo> queryUserName(String userName);
}
6.4.2.2.1 测试
package com.example.demo.mapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class UserInfoMapperTest {
@Autowired
private UserInfoMapper userInfoMapper;
@Test
void queryUserName() {
userInfoMapper.queryUserName("admin");
}
}
成功!!!
6.4.2.3 Service层
package com.example.demo.service;
import com.example.demo.mapper.UserInfoMapper;
import com.example.demo.model.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
@Autowired
private UserInfoMapper userInfoMapper;
public UserInfo queryUserName(String userName){
return userInfoMapper.queryUserName(userName);
}
}
6.4.2.4 Controller包
package com.example.demo.controller;
import com.example.demo.Constants.Constants;
import com.example.demo.model.UserInfo;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login")
public Boolean login(String password, String admin, HttpSession session){
//账号为空,错误
if(!StringUtils.hasLength(password)||!StringUtils.hasLength(admin)){
return false;
}
UserInfo userInfo=userService.queryUserName(admin);
if(userInfo==null){
return false;
}
if((userInfo.getPassword()).equals(password)){
//存储在session中
userInfo.setPassword("");//将密码置为空,防止泄露
session.setAttribute(Constants.session_user_key,userInfo.getUserName());//把后面的存在前面的变量中
return true;
}
return false;
}
}
6.4.2.5 后端接口测试
6.4.2.6 前后端交互测试
6.4.3 添加图书和查询图书
6.4.3.1 约定前后端交互接口
[请求]
/book/addBook
Content-Type: application/x-www-form-urlencoded; charset=UTF-8[参数]
bookName=图书1&author=作者1&count=23&price=36&publish=出版社1&status=1
[响应]
"" //失败信息, 成功时返回空字符串
我们约定, 浏览器给服务器发送⼀个 /book/addBook 这样的 HTTP 请求, form表单的形式来提交
数据服务器返回处理结果, 返回""表⽰添加图书成功, 否则, 返回失败信息.
6.4.3.2 Mapper包
package com.example.demo.mapper;
import com.example.demo.model.BookInfo;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface BookInfoMapper {
@Insert("insert into book_info (book_name, author, count, price, publish) " +
"values (#{bookName}, #{author}, #{count}, #{price}, #{publish} )")
Integer insertBook(BookInfo bookInfo);
@Select("select * from book_info")
List<BookInfo> queryBook();
}
6.4.3.2.1 测试
@SpringBootTest
class BookInfoMapperTest {
@Autowired
private BookInfoMapper bookInfoMapper;
@Test
void insertBook() {
BigDecimal intStr = new BigDecimal("21.111111");
BookInfo bookInfo=new BookInfo();
bookInfo.setAuthor("烟酒与猫");
bookInfo.setBookName("二锅水");
bookInfo.setCount(9);
bookInfo.setPrice(intStr);
bookInfo.setPublish("漫播");
bookInfoMapper.insertBook(bookInfo);
}
}
6.4.3.3 Service层
//查询图书
@Service
public class BookService {
@Autowired
private BookInfoMapper bookInfoMapper;
public List<BookInfo> getBookList(){
List<BookInfo> bookInfos=bookInfoMapper.queryBook();
for(BookInfo bookInfo:bookInfos){
if(bookInfo.getStatus()==1){
bookInfo.setStatusCN("可借阅");
}else{
bookInfo.setStatusCN("不可借阅");
}
}
return bookInfos;
}
}
//添加图书
public void addBook(BookInfo bookInfo){
bookInfoMapper.insertBook(bookInfo);
}
6.4.3.4 Controller层
//查询图书与之前没有变化
//添加图书
@RequestMapping("/addBook")
public String addBook(BookInfo bookInfo){
log.info("添加图书{}",bookInfo);
if(!StringUtils.hasLength(bookInfo.getBookName())||
!StringUtils.hasLength(bookInfo.getAuthor())||
bookInfo.getStatus()==null||
bookInfo.getCount()==null||
bookInfo.getPrice()==null||
!StringUtils.hasLength(bookInfo.getPublish())){
return "输入参数不合法,请重新输入参数";
}
try {
bookService.addBook(bookInfo);
return "";
}catch (Exception e){
log.error("添加图书失败",e);
return e.getMessage();
}
}
6.4.3.5 前端测试
//查询图书
发现有的为空,经检查,配置项有问题
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/book_test?characterEncoding=utf8&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
configuration:
map-underscore-to-camel-case: true #配置驼峰⾃动转换
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印sql语句
# configuration:
# 设置⽇志⽂件的⽂件名
logging:
file:
name: spring-book.log
//添加图书
6.4.3.6 前后端交互界面测试
//页面展示,成功!!!
6.4.3.6.1 前后端交互代码
<script>
function add() {
$.ajax({
url: "/book/addBook",
type: "post",
data: $("#addBook").serialize(),
success: function(result){
if(result==""){
location.href = "book_list.html";
}else{
console.log("添加失败"+result);
alert("添加失败!"+result);
}
},
error: function(error){
console.log(error);
}
});
}
</script>
6.4.4 实现查询图书分页操作
如果数据库中的数据有很多(假设有⼗⼏万条)的时候,将数据全部展⽰出来肯定不现实,那如何解决这个问题呢?
使⽤分⻚解决这个问题。每次只展⽰⼀⻚的数据,⽐如:⼀⻚展⽰10条数据,如果还想看其他的数据,可以通过点击⻚码进⾏查询
分⻚时, 数据是如何展⽰的呢
第1⻚: 显⽰1-10 条的数据
第2⻚: 显⽰11-20 条的数据
第3⻚: 显⽰21-30 条的数据
以此类推...
要想实现这个功能, 从数据库中进⾏分⻚查询,我们要使⽤ LIMIT 关键字,格式为:limit 开始索引每⻚显⽰的条数(开始索引从0开始)
查询第1⻚的SQL语句:SELECT * FROM book_info LIMIT 0,10
查询第2⻚的SQL语句:SELECT * FROM book_info LIMIT 10,10
查询第3⻚的SQL语句:SELECT * FROM book_info LIMIT 20,10
观察以上SQL语句,发现: 开始索引⼀直在改变, 每⻚显⽰条数是固定的
开始索引的计算公式: 开始索引 = (当前⻚码 - 1) * 每⻚显⽰条数
我们继续基于前端页面, 继续分析, 得出以下结论:
1. 前端在发起查询请求时,需要向服务端传递的参数
◦ currentPage 当前页码 //默认值为1
◦ pageSize 每页显示条数 //默认值为10
为了项⽬更好的扩展性, 通常不设置固定值, ⽽是以参数的形式来进⾏传递
扩展性: 软件系统具备⾯对未来需求变化⽽进⾏扩展的能⼒
⽐如当前需求⼀⻚显⽰10条, 后期需求改为⼀⻚显⽰20条, 后端代码不需要任何修改
2. 后端响应时, 需要响应给前端的数据
◦ records 所查询到的数据列表(存储到List 集合中)
◦ total 总记录数 (⽤于告诉前端显⽰多少⻚, 显⽰⻚数为: (total + pageSize -1)/pageSize
显⽰⻚数totalPage 计算公式为 : total % pagesize == 0 ? total / pagesize : (total /pagesize)+1 ;pagesize - 1 是 total / pageSize 的最大的余数,所以(total + pagesize -1) / pagesize就得到总⻚数
翻⻚请求和响应部分, 我们通常封装在两个对象中
翻⻚请求对象
@Data
public class PageRequest {
private int currentPage = 1; // 当前⻚
private int pageSize = 10; // 每⻚中的记录数
}
我们需要根据currentPage 和pageSize ,计算出来开始索引
PageRequest修改为:
@Data
public class PageRequest {
private int currentPage = 1; // 当前⻚
private int pageSize = 10; // 每⻚中的记录数
private int offset;
public int getOffset() {
return (currentPage-1)*pageSize;
}
}
翻页列表结果类
package com.example.demo.model;
import lombok.Data;
import java.util.List;
@Data
public class PageResult<T> {
private int total;//记录数
private List<T> records;//当前页数据
//返回结果中, 使⽤泛型来定义记录的类型
public PageResult(int total, List<T> records) {
this.total = total;
this.records = records;
}
}
6.4.4.1 Mapper包
//count(1) 会统计表中的所有的记录数,包含字段为null 的记录。
@Select("select count(1) from book_info where status!=0")
Integer count();
@Select("select * from book_info where status !=0 order by id desc limit #{offset},#{pageSize}")
List<BookInfo> queryBook(PageRequest pageRequest);
6.4.4.2 Service包
//分页操作
public PageResult<BookInfo> getBookList(PageRequest pageRequest){
Integer count=bookInfoMapper.count();
List<BookInfo> bookInfos=bookInfoMapper.queryBook(pageRequest);
for(BookInfo bookInfo:bookInfos){
if(bookInfo.getStatus()==1){
bookInfo.setStatusCN("可借阅");
}else if(bookInfo.getStatus()==0){
bookInfo.setStatusCN("无效");
}else{
bookInfo.setStatusCN("不可借阅");
}
}
//构造函数
return new PageResult<>(count,bookInfos);
}
6.4.4.3 Controller包
@RequestMapping("/getList")
public PageResult<BookInfo> getBookList(PageRequest pageRequest){
log.info("图书列表信息, pageRequest:{}",pageRequest);
PageResult<BookInfo> pageResult=bookService.getBookList(pageRequest);
return pageResult;
}
1.翻页信息需要返回数据的总数和列表信息, 需要查两次SQL
2. 图书状态: 图书状态和数据库存储的status有⼀定的对应关系
如果后续状态码有变动, 我们需要修改项⽬中所有涉及的代码, 这种情况, 通常采⽤枚举类来处理映射关系public enum BookStatus { DELETED(0,"⽆效"), NORMAL(1,"可借阅"), FORBIDDEN(2,"不可借阅"); private Integer code; private String name; public static BookStatus getNameByCode(Integer code){ switch (code){ case 0: return DELETED; case 1: return NORMAL; case 2: return FORBIDDEN; } return null; } BookStatus(Integer code, String name) { this.code = code; this.name = name; } public Integer getCode() { return code; } public String getName() { return name; } public void setCode(Integer code) { this.code = code; } public void setName(String name) { this.name = name; } }
将上述代码替换为下述代码
getNameByCode: 通过code来获取对应的枚举, 以获取该枚举对应的中⽂名称
后续如果有状态变更, 只需要修改该枚举类即可
6.4.4.4 后端接口测试
6.4.4.5 前后端交互
getBookList();
function getBookList() {
$.ajax({
type: "post",
url: "/book/getList",
//我们想把相关语句,存入<tbody>内,循环
success: function(result){
console.log(result);//控制台进行打印,方便我们进行检查,看我们是否从服务器拿到信息,可以定位错误
if(result!=null){
result=result.records;
var finalHtml="";
for(var book of result){
finalHtml += '<tr>';
finalHtml += '<td><input type="checkbox" name="selectBook" value="1" id="selectBook" class="book-select"></td>';
finalHtml += '<td>' + book.id + '</td>';
finalHtml += '<td>' + book.bookName + '</td>';
finalHtml += '<td>' + book.author + '</td>';
finalHtml += '<td>' + book.count + '</td>';
finalHtml += '<td>' + book.price + '</td>';
finalHtml += '<td>' + book.publish + '</td>';
finalHtml += '<td>' + book.statusCN + '</td>';
finalHtml += '<td><div class="op">';
finalHtml += '<a href="book_update.html?bookId=' + book.id +'">修改</a>';
finalHtml += '<a href="javascript:void(0)" onclick="deleteBook('+book.id+')">删除</a>';
finalHtml += '</div></td>';
finalHtml += "</tr>";
}
}
$("tbody").html(finalHtml);
}
});
}
此时, url还未设置 currentPage 参数
我们直接使⽤ location.search 从url中获取参数信息即可
location.search : 获取url的查询字符串 (包含问号)
如:
url: http://127.0.0.1:8080/book_list.html?currentPage=1
location.search : ?currentPage=1
所以, 把上述url改为: "/book/getListByPage" + location.search
6.4.4.5.1 处理分页信息
分页插件
本案例中, 分页代码采用了⼀个分⻚组件
分⻚组件⽂档介绍: jqPaginator分页组件
使⽤时, 只需要按照 [使⽤说明]部分的⽂档, 把代码复制粘贴进来就可以了(提供的前端代码中, 已经包含该部分内容)简单介绍下使⽤onPageChange :回调函数,当换⻚时触发(包括初始化第⼀⻚的时候),会传⼊两个参数:
1、"⽬标⻚"的⻚码,Number类型
2、触发类型,可能的值:"init"(初始化),"change"(点击分⻚)我们在图书列表信息加载之后, 需要分⻚信息, 同步加载
分⻚组件需要提供⼀些信息:totalCounts: 总记录数,
pageSize: 每⻚的个数,
visiblePages: 可视⻚数
currentPage: 当前⻚码
这些信息中, pageSize 和 visiblePages 前端直接设置即可.totalCounts 后端已经提供,
currentPage 也可以从参数中取到, 但太复杂了, 咱们直接由后端返回即可
因为前端界面需要这些信息,所以我们修改后端的返回数据
//不断出现分页栏显示不出的情况
//经查找,发现是下述部分 有问题
6.4.4.5.2 后端代码
getBookList();
function getBookList() {
$.ajax({
type: "post",
url: "/book/getList"+location.search,
//我们想把相关语句,存入<tbody>内,循环
success: function(resultall){
console.log(resultall);//控制台进行打印,方便我们进行检查,看我们是否从服务器拿到信息,可以定位错误
if(resultall!=null){
var result=resultall.records;
var finalHtml="";
for(var book of result){
finalHtml += '<tr>';
finalHtml += '<td><input type="checkbox" name="selectBook" value="1" id="selectBook" class="book-select"></td>';
finalHtml += '<td>' + book.id + '</td>';
finalHtml += '<td>' + book.bookName + '</td>';
finalHtml += '<td>' + book.author + '</td>';
finalHtml += '<td>' + book.count + '</td>';
finalHtml += '<td>' + book.price + '</td>';
finalHtml += '<td>' + book.publish + '</td>';
finalHtml += '<td>' + book.statusCN + '</td>';
finalHtml += '<td><div class="op">';
finalHtml += '<a href="book_update.html?bookId=' + book.id +'">修改</a>';
finalHtml += '<a href="javascript:void(0)" onclick="deleteBook('+book.id+')">删除</a>';
finalHtml += '</div></td>';
finalHtml += "</tr>";
}
$("tbody").html(finalHtml);
//翻页信息
$("#pageContainer").jqPaginator({
totalCounts: resultall.total, //总记录数
pageSize: 10, //每页的个数
visiblePages: 5, //可视页数
currentPage: resultall.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) {
console.log("第"+page+"页, 类型:"+type);
if (type == "change") {
// console.log("book_list.html"+location.search);
location.href = "book_list.html?currentPage=" + page;
}
});
}
}
});
}
6.4.4.5.3 后端测试
成功!!!
6.4.5 修改图书
6.4.5.1 约定前后端交互接口
进⼊修改页面, 需要显示当前图书的信息
[请求]
/book/queryBookById?bookId=25
[参数]
⽆
[响应]
{
"id": 25,
"bookName": "图书21",
"author": "作者2",
"count": 999,
"price": 222.00,
"publish": "出版社1",
"status": 2,
"statusCN": null,
"createTime": "2023-09-04T04:01:27.000+00:00",
"updateTime": "2023-09-05T03:37:03.000+00:00"
}//根据图书ID, 获取当前图书的信息
点击修改按钮, 修改图书信息
【请求】
/book/updateBook
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
【参数】
id=1&bookName=图书1&author=作者1&count=23&price=36&publish=出版社1&status=1【响应】
"" //失败信息, 成功时返回空字符串
我们约定, 浏览器给服务器发送⼀个 /book/updateBook 这样的 HTTP 请求,form表单的形式来提交数据
服务器返回处理结果, 返回""表⽰添加图书成功,否则, 返回失败信息.
6.4.5.2 查询接口
6.4.5.2.1 mapper包
//修改图书
//1.根据图书id查询图书信息
@Select("select * from book_info where status!=0 and id=#{id}")
BookInfo queryBookID(Integer id);
6.4.5.2.2 service包
//查询图书
public BookInfo queryBookID(Integer id){
return bookInfoMapper.queryBookID(id);
}
6.4.5.2.3 controller包
//查询图书
@RequestMapping("/queryBookById")
public BookInfo queryBookId(Integer id){
if(id==null || id<=0){
return new BookInfo();
}
BookInfo bookInfo=bookService.queryBookID(id);
return bookInfo;
}
6.4.5.2.4 后端接口测试
成功!!!
6.4.5.2.5 前后端交互
//发现后端未从前端获取数据
//发现是由于此处拼接有误
//有误时就不能从前端获取相应的数据
//要一一对应
// 一进来就像让他加载出来
$.ajax({
url: "/book/queryBookById"+location.search,
type: "get",
success: function(book){
if(book!=null){
$("#bookName").val(book.bookName);
$("#bookAuthor").val(book.author);
$("#bookStock").val(book.count);
$("#bookPrice").val(book.price);
$("#bookPublisher").val(book.publish);
$("#bookStatus").val(book.status);
// 括号内有东西是赋值,没有东西是取值
}
}
});
6.4.5.2.6 前后端测试
6.4.5.3 修改图书接口
6.4.5.3.1 mapper包
由于用到动态sql,我们用xml方式进行操作
xml在此目录下
配置记得加后一行
6.4.5.3.2 service包
//2.修改图书
public void updateBook(BookInfo bookInfo){
bookInfoMapper.updateBook(bookInfo);
}
6.4.5.3.3 controller包
//修改图书
@RequestMapping("/updateBook")
public String updateBook(BookInfo bookInfo){
log.info("修改图书:{}",bookInfo);
try {
bookService.updateBook(bookInfo);
return "";
}catch (Exception e){
log.error("修改图书失败",e);
return e.getMessage();
}
}
6.4.5.3.4 后端接口测试
成功!!!
6.4.5.3.5 前后端交互
我们修改图书信息, 是根据图书ID来修改的, 所以需要前端传递的参数中, 包含图书ID.
有两种⽅式:
1. 获取url中参数的值(⽐较复杂, 需要拆分url)
2. 在form表单中, 再增加⼀个隐藏输⼊框, 存储图书ID, 随 $("#updateBook").serialize()
⼀起提交到后端
我们采⽤第⼆种⽅式
在form表单中,添加隐藏输⼊框
hidden 类型的 <input> 元素
隐藏表单, ⽤⼾不可⻅、不可改的数据,在⽤⼾提交表单时,这些数据会⼀并发送出
使⽤场景: 正被请求或编辑的内容的 ID. 这些隐藏的 input 元素在渲染完成的⻚⾯中完全不可⻅,⽽且没有⽅法可以使它重新变为可⻅
⻚⾯加载时, 给该hidden框赋值即可
function update() {
$.ajax({
url: "/book/updateBook",
type: "get",
data: $("#updateBook").serialize(),
success: function(result){
if (result == "") {
location.href = "book_list.html"
} else {
console.log(result);
alert("修改失败:" + result);
}
},
error: function(error){
console.log(error);
}
});
}
6.4.5.3.6 前后端测试
成功!!!
6.4.5.4 删除图书
约定前后端交互接⼝
删除分为 逻辑删除 和物理删除
逻辑删除
逻辑删除也称为软删除、假删除、Soft Delete,即不真正删除数据,⽽在某⾏数据上增加类型is_deleted的删除标识,⼀般使⽤UPDATE语句
物理删除物理删除也称为硬删除,从数据库表中删除某⼀⾏或某⼀集合数据,⼀般使⽤DELETE语句
删除图书的两种实现⽅式
逻辑删除
update book_info set status=0 where id = 1
物理删除
delete from book_info where id=25
数据是公司的重要财产, 通常情况下, 我们采⽤逻辑删除的⽅式。
物理删除+归档的⽅式实现有些复杂, 咱们采⽤逻辑删除的⽅式
逻辑删除的话, 依然是更新逻辑, 我们可以直接使⽤修改图书的接⼝
/book/updateBook
[请求]
/book/updateBook
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
[参数]
id=1&status=0
[响应]
"" //失败信息, 成功时返回空字符串
6.5.5.4.1 前后端交互
function deleteBook(id){
var isDelete = confirm("确定删除吗?");
if(isDelete){
$.ajax({
type: "get",
url: "/book/updateBook",
data: {
id: id,
status: 0
},
success: function(){
location.reload();
location.href("book_list.html")
}});
}
}
6.5.5.4.2 前后端测试
成功!!!
6.4.5.5 批量删除
批量删除, 其实就是批量修改数据
6.4.5.5.1 约定前后端交互接⼝
[请求]
/book/batchDeleteBook
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
[参数]
[响应]
"" //失败信息, 成功时返回空字符串
点击[批量删除]按钮时, 只需要把复选框选中的图书的ID,发送到后端即可
多个id, 我们使⽤List的形式来传递参数
6.4.5.5.2 后端代码
mapper包
service包
controller包
6.4.5.5.3 后端测试
6.4.5.5.4 前后端交互代码
function batchDelete() {
var isDelete = confirm("确认批量删除?");
if (isDelete) {
var ids = [];
$("input:checkbox[name='selectBook']:checked").each(function () {
ids.push($(this).val());//像数组添加元素
});
var idstr=ids.join(',');//将数组元素连接起来以构建一个字符串
alert(idstr)
console.log(idstr);
$.ajax({
type: "post",
url: "/book/batchDeleteBook?ids=" + idstr,
success: function (result) {
if (result=="") {
//删除成功
location.href = "book_list.html";
} else {
alert("删除失败, 请联系管理员");
}
}
});
}
}
6.4.5.5.5 前后端交互测试
//开始测试,一直都无法删除
//删除的id=25,26结果删除的只有1
//通过打印发现复选框选中的只有1
//最后发现是前面的book.id设置的是1,不是一个变量
//改正之后成功
6.4.5.6 强制登录
虽然我们做了⽤⼾登录, 但是我们发现, ⽤⼾不登录, 依然可以操作图书.
这是有极⼤⻛险的. 所以我们需要进⾏强制登录.
如果⽤⼾未登录就访问图书列表或者添加图书等⻚⾯, 强制跳转到登录⻚⾯.
6.4.5.6.1 实现思路分析
⽤⼾登录时, 我们已经把登录⽤⼾的信息存储在了Session中. 那就可以通过Session中的信息来判断⽤⼾都是登录.
1. 如果Session中可以取到登录⽤⼾的信息, 说明⽤⼾已经登录了, 可以进⾏后续操作
2. 如果Session中取不到登录⽤⼾的信息, 说明⽤⼾未登录, 则跳转到登录⻚⾯.
以图书列表为例
现在图书列表接⼝返回的内容如下:{ "total": 25, "records": [{ "id": 25, "bookName": "图书21", "author": "作者2", "count": 29, "price": 22.00, "publish": "出版社1", "status": 1, "statusCN": "可借阅" }, { ...... } ] }
这个结果上, 前端没办法确认⽤⼾是否登录了. 并且后端返回数据为空时, 前端也⽆法确认是后端⽆数据, 还是后端出错了.
我们需要再增加⼀个属性告知后端的状态以及后端出错的原因. 修改如下:
但是当前只是图书列表⽽已, 图书的增加, 修改, 删除接⼝都需要跟着修改, 添加两个字段. 这对我们的代码修改是巨⼤的.
我们不妨对所有后端返回的数据进⾏⼀个封装
public class Result<T> {
private int status;
private String errorMessage;
private T data;
//data为之前接⼝返回的数据
}
status 为后端业务处理的状态码, 也可以使⽤枚举来表⽰
public enum ResultStatus {
SUCCCESS(200),
UNLOGIN(-1),
FAIL(-2);
private Integer code;
ResultStatus(Integer code) {
this.code = code;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
}
修改Result, 并添加⼀些常⽤⽅法
@Data
public class Result {
private ResultStatus status;
private String errorMessage;
private Object data;//data为之前接⼝返回的数据
public static Result success(Object data){
Result result=new Result();
result.setStatus(ResultStatus.SUCCCESS);
result.setErrorMessage("");
result.setData(data);
return result;
}
public static Result fail(String msg){
Result result=new Result();
result.setStatus(ResultStatus.FAIL);
result.setErrorMessage(msg);
result.setData("");
return result;
}
public static Result unlogin(){
Result result=new Result();
result.setStatus(ResultStatus.UNLOGIN);
result.setErrorMessage("用户未登录");
result.setData(null);
return result;
}
}
6.4.5.6.2 服务器代码修改
6.4.5.6.3 修改客户端代码
6.4.5.6.4 测试
成功!!!
7.思考-需要升级的部分
强制登录的模块, 我们只实现了⼀个图书列表, 上述还有图书修改, 图书删除等接⼝, 也需要⼀⼀实现.
如果应⽤程序功能更多的话, 这样写下来会⾮常浪费时间, 并且容易出错.
有没有更简单的处理办法呢?
我们学习SpringBoot对于这种"统⼀问题"的处理办法.