项目4-图书管理系统1

1.创建项目

流程与之前的项目一致,不再进行赘述。

2.需求定义

需求:
1. 登录: ⽤⼾输⼊账号,密码完成登录功能
2. 列表展⽰: 展⽰图书

3.前端界面测试

无法启动!!!--->记得加入mysql相关操作记得在yml进行配置

配置后启动成功!!

我们在测试前端的时候同时可以对用户的需求进行分析。

3.1 需求分析

3.1.1 用户登录

3.1.2 图书列表 

根据需求可以得知, 后端需要提供两个接口
1. 账号密码校验接口: 根据输入用户名和密码校验登录是否通过
获取参数:用户名和密码
返回:true或者false
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对于这种"统⼀问题"的处理办法.

  • 6
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值