欢迎关注个人主页:逸狼
创造不易,可以点点赞吗
如有错误,欢迎指出~
前⾯图书管理系统,咱们只完成了⽤⼾登录和图书列表,并且数据是Mock的.接下来我们把其他功能进 ⾏完善.
功能列表: 1. ⽤⼾登录 2. 图书列表 3. 图书的增删改查 4. 翻⻚功能
创建数据库book_test
-- 创建数据库
DROP DATABASE IF EXISTS book_test;
CREATE DATABASE book_test DEFAULT CHARACTER SET utf8mb4;
-- ⽤⼾表
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 = '⽤⼾表';
-- 图书表
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 user_info ( user_name, PASSWORD ) VALUES ( "admin", "admin" );
INSERT INTO user_info ( user_name, PASSWORD ) VALUES ( "zhangsan", "123456" );
-- 初始化图书数据
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, '⺠主与建设出版社');
引⼊MyBatis和MySQL驱动依赖
修改pom⽂件
<dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>3.0.3</version>
</dependency>
<dependency><groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId><scope>runtime</scope>
</dependency>
配置数据库&⽇志
# 数据库连接配置
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/book_test?characterEncoding=utf8&useSSL=falseusername: rootpassword: 111111driver-class-name: com.mysql.cj.jdbc.Driverapplication:name: demo-book
mybatis:configuration:map-underscore-to-camel-case: true #配置驼峰自动转换log-impl: org.apache.ibatis.logging.stdout.StdOutImpl #打印sql语句
logging:file:name: spring-book.log
Model创建
UserInfo
import lombok.Data;
import java.util.Date;
@Datapublic class UserInfo {private Integer id;private String userName;private String password;private Integer deleteFlag;private Date createTime;private Date updateTime;
}
BookInfo
@Datapublic 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;
}
接口1: ⽤⼾登录
约定前后端交互接⼝
- [请求] /user/login Content-Type: application/x-www-form-urlencoded; charset=UTF-8
- [参数] name=zhangsan&password=123456
- [响应] true //账号密码验证正确, 否则返回false
浏览器给服务器发送 /user/login 这样的HTTP请求,服务器给浏览器返回了⼀个Boolean类型 的数据.返回true,表⽰账号密码验证正确
实现服务器代码
从数据库中,根据名称查询⽤⼾,如果可以查到,并且密码⼀致,就认为登录成功
控制层UserController
@Slf4j
@RestController
@RequestMapping("/user")public class UserController {@Autowiredprivate UserService userService;@RequestMapping(value = "/login", method = RequestMethod.POST)public boolean login(String userName, String password, HttpSession session){//参数输入,打印日志log.info("接收到参数: " + userName);Boolean result = userService.checkUserAndPassword(userName, password, session);log.info("用户登入结果: name:{}, password:{}, 结果: {}", userName,password , result);return result;}
}
业务层userService
package com.example.demo.service;import com.example.demo.Constants;
import com.example.demo.Mapper.UserInfoMapper;
import com.example.demo.model.UserInfo;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;@Service
public class UserService {@Autowiredprivate UserInfoMapper userInfoMapper;public Boolean checkUserAndPassword(String userName, String password, HttpSession session){//账号, 密码为空if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){return false;}//在数据库中查询是否存在用户名UserInfo userInfo = userInfoMapper.queryUserByName(userName);if(userInfo == null){return false;}if( password.equals(userInfo.getPassword())){//存储在Session中//账号密码正确session.setAttribute(Constants.SESSION_USER_KEY,userName);return true;}return false;}
}
数据层userInfoMapper
@Mapper
public interface UserInfoMapper {@Select("select * from user_info where delete_flag = 0 and user_name = #{name}")UserInfo queryUserByName(String name);
}
访问数据库,使⽤MyBatis来实现,所以把之前dao路径下的⽂件可以删掉,⽤mapper⽬录来代替,创 建UserInfoMapper 当然,继续使⽤dao⽬录也可以, dao和mapper通常都被认为是数据库层
接口2: 添加图书
约定前后端交互接⼝
- [请求] /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表单的形式来提交 数据 服务器返回处理结果,返回""表⽰添加图书成功,否则,返回失败信息.
实现服务器代码控制层: 在BookController补充代码 先进⾏参数校验,校验通过了进⾏图书添加
实际开发中,后端开发⼈员不关注前端是否进⾏了参数校验,⼀律进⾏校验 ,原因是:后端接⼝可能会被⿊客攻击,不通过前端来访问,如果后端不进⾏校验,会产⽣脏数据
实现服务器代码
BookController
//添加图书@RequestMapping(value = "/addBook", produces = "application/json")public ResultVO<String> addBook(BookInfo bookInfo) {log.info("添加图书, bookInfo:{}", bookInfo);//参数校验if (!StringUtils.hasLength(bookInfo.getBookName())||!StringUtils.hasLength(bookInfo.getAuthor())|| bookInfo.getCount() == null|| bookInfo.getPrice() == null||!StringUtils.hasLength(bookInfo.getPublish())) {return new ResultVO<>(400, "参数不合法,图书名称、作者、库存、价格、出版社均为必填项", "");}//2,插入数据try {Integer result = bookService.insertBook(bookInfo);if (result == 1) {return new ResultVO<>(200, "图书添加成功", "");}return new ResultVO<>(500, "图书添加失败,插入结果异常", "");} catch (Exception e) {log.error("数据插入发生异常, e: ", e);return new ResultVO<>(500, "数据插入失败,请联系管理员,具体异常:" + e.getMessage(), "");}}
BookService
public Integer insertBook(BookInfo bookInfo) {return bookMapper.insertBook(bookInfo);}
BookMapper
Integer insertBook(BookInfo bookInfo);
ResultVO类
package com.example.demo.model;import lombok.AllArgsConstructor;
import lombok.Data;@Data
@AllArgsConstructor
public class ResultVO<T> {private Integer code; // 状态码,例如 200 表示成功,其他表示失败private String message; // 提示信息private T data; // 数据部分,如果有需要返回的数据
}
接口3: 图书列表
可以看到,添加图书之后,跳转到图书列表⻚⾯,并没有显⽰刚才添加的图书信息,接下来我们来实现图 书列表 需求分析
我们之前做的表⽩墙查询功能,是将数据库中所有的数据查询出来并展⽰到⻚⾯上,试想如果数据库 中的数据有很多(假设有⼗⼏万条)的时候,将数据全部展⽰出来肯定不现实,那如何解决这个问题呢? 使⽤分⻚解决这个问题。每次只展⽰⼀⻚的数据,⽐如:⼀⻚展⽰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
观察以上SQL语句,发现:开始索引⼀直在改变,每⻚显⽰条数是固定的 开始索引的计算公式:开始索引=(当前⻚码currentPage-1)*每⻚显⽰条数pageSize
对于后端而言,前端需要提供参数: currentPage, pageSize
后端给前端返回: 当前页的记录
实现服务器代码
BookController
//查询图书信息@RequestMapping("/getListByPage")public PageResponse<BookInfo> getListByPage(PageRequest pageRequest, HttpServletRequest request) {log.info("获取图书列表, pageRequest: {}", pageRequest);////参数校验省略PageResponse<BookInfo> bookInfoPageResponse = bookService.getListByPage(pageRequest);return bookInfoPageResponse;}
BookService
//查询图书信息public PageResponse<BookInfo> getListByPage(PageRequest pageRequest){//1, 总记录数Integer count = bookMapper.count();// List<BookInfo> bookInfos = bookDao.mockData();// //2. 当前页记录数List<BookInfo> bookInfos = bookMapper.queryBookByPage(pageRequest.getOffset(),pageRequest.getPageSize());for(BookInfo bookInfo : bookInfos){
// if(bookInfo.getStatus() == 0){
// bookInfo.setStatusCN("删除");
// }else if(bookInfo.getStatus() == 1){
// bookInfo.setStatusCN("可借阅");
// }else{
// bookInfo.setStatusCN("不可借阅");
// }bookInfo.setStatusCN(BookStatusEnum.getStatusByCode(bookInfo.getStatus()).getDesc());}return new PageResponse<BookInfo>(count,bookInfos,pageRequest);}
BookMapper
//查询图书信息@Select("select * from book_info order by id limit #{offset}, #{limit}")List<BookInfo> queryBookByPage(Integer offset, Integer limit);
接口4: 修改图书
约定前后端交互接⼝
进⼊修改⻚⾯,需要显⽰当前图书的信息
[请求] /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表单的形式来 提交数据 服务器返回处理结果,返回""表⽰添加图书成功,否则,返回失败信息. 实现服务器代码
实现服务器代码
BookController
//更新图书@RequestMapping("/updateBook")public ResultVO<String> updateBook(BookInfo bookInfo) {log.info("更新图书, bookInfo: {}", bookInfo);try {Integer result = bookService.updateBook(bookInfo);if (result > 0) {return new ResultVO<>(200, "图书更新成功", "");} else if (result == 0) {return new ResultVO<>(200, "图书内容未发生变化,无需更新", "");}return new ResultVO<>(500, "图书更新失败", "");} catch (Exception e) {log.error("更新图书失败,e:", e);return new ResultVO<>(500, "数据库操作失败:" + e.getMessage(), "");}}
BookService
//更新图书public Integer updateBook(BookInfo bookInfo) {return bookMapper.updateBook(bookInfo);}
BookMapper
//更新图书,用xml的方式 注解和xml的方式可以混用Integer updateBook(BookInfo bookInfo);
BookMapperInfo.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.demo.Mapper.BookMapper"><!-- 更新图书--><update id="updateBook">update book_info<set><if test="bookName!=null">book_name =#{bookName},</if><if test="author!=null">author =#{author},</if><if test="count!=null">count =#{count},</if><if test="price!=null">price =#{price},</if><if test="publish!=null">publish =#{publish},</if><if test="status!=null">status =#{status},</if></set>where id = #{id}</update>
</mapper>
接口5: 通过id查询图书
实现服务器代码
BookController
//通过id查询图书@RequestMapping("/queryBookById")public BookInfo queryBookById(Integer bookId){log.info("获取图书信息, bookId: "+ bookId);//参数校验,不能为null,不能<=0...省略return bookService.queryBookById(bookId);}
BookService
//通过id查询图书public BookInfo queryBookById(Integer bookId) {return bookMapper.queryBookById(bookId);}
BookMapper
//通过id查询图书@Select("select * from book_info where id = #{bookId}")BookInfo queryBookById(Integer bookId);
接口6:删除图书
约定前后端交互接⼝ 删除分为逻辑删除和物理删除
- 逻辑删除也称为软删除、假删除、SoftDelete,即不真正删除数据,⽽在某⾏数据上增加类型 is_deleted的删除标识,⼀般使⽤UPDATE语句
- 物理删除也称为硬删除,从数据库表中删除某⼀⾏或某⼀集合数据,⼀般使⽤DELETE语句 删除图书的两种实现⽅式
逻辑删除: update book_info set status=0 where id = 1
物理删除: delete from book_info where id=25
实现服务器代码
BookController
@RequestMapping("/deleteBook")public String deleteBook(Integer bookId){log.info("删除图书, bookId: {}", bookId);try{BookInfo bookInfo = new BookInfo();bookInfo.setId(bookId);bookInfo.setStatus(BookStatusEnum.DELETED.getCode());Integer result = bookService.updateBook(bookInfo);if(result > 0){//1return "";}return "图书删除失败";}catch(Exception e){log.error("删除图书失败,e:",e);return "数据库操作失败";}}
接口5: 批量删除图书
批量删除,其实就是批量修改数据
约定前后端交互接⼝
- [请求] /book/batchDeleteBook Content-Type: application/x-www-form-urlencoded; charset=UTF-8
- [参数] ids = [1, 2, 3, 4]
- [响应] boolean true //删除成功 false //删除失败
点击[批量删除]按钮时,只需要把复选框选中的图书的ID,发送到后端即可 多个id,我们使⽤List的形式来传递参数
实现服务器代码
BookController
//批量删除@RequestMapping("/batchDeleteBook")public Boolean batchDeleteBook(@RequestParam List<Integer> ids){log.info("批量删除图书, ids:{}", ids);try{//执行sqlbookService.batchDeleteBook(ids);return true;}catch(Exception e){log.error("批量删除图书, ids:{}", ids);}return false;}
BookService
public void batchDeleteBook(List<Integer> ids) {bookMapper.batchDelete(ids);}
BookMapper
void batchDelete(List<Integer> ids);
BookMapperInfo.xml
<!-- 批量删除--><update id="batchDelete">update book_infoset status = 0where id in<foreach collection="ids" item="id" open="(" close=")" separator=",">#{id}</foreach></update>
接口6: 强制登录
虽然我们做了⽤⼾登录,但是我们发现,⽤⼾不登录,依然可以操作图书. 这是有极⼤⻛险的.所以我们需要进⾏强制登录. 如果⽤⼾未登录就访问图书列表或者添加图书等⻚⾯,强制跳转到登录⻚⾯.
实现思路分析 ⽤⼾登录时,我们已经把登录⽤⼾的信息存储在了Session中.那就可以通过Session中的信息来判断⽤ ⼾都是登录.
1. 如果Session中可以取到登录⽤⼾的信息,说明⽤⼾已经登录了,可以进⾏后续操作
2. 如果Session中取不到登录⽤⼾的信息,说明⽤⼾未登录,则跳转到登录⻚⾯. 以图书列表为例 现在图书列表接⼝返回的内容如下:
实现服务器代码
BookController
//查询图书信息,翻页使用@RequestMapping("/getListByPage")public Result<PageResponse<BookInfo>> getListByPage(PageRequest pageRequest, HttpServletRequest request) {log.info("获取图书列表, pageRequest: {}", pageRequest);HttpSession session = request.getSession(false);//如果拿不到session就会返回nullif(session == null|| session.getAttribute(Constants.SESSION_USER_KEY) == null|| !StringUtils.hasLength((String)session.getAttribute(Constants.SESSION_USER_KEY))){//用户未登录return Result.unlogin();}//参数校验省略PageResponse<BookInfo> bookInfoPageResponse = new PageResponse<>();try{bookInfoPageResponse = bookService.getListByPage(pageRequest);}catch(Exception e){log.error("获取图书列表失败");return Result.fail();}return Result.success(bookInfoPageResponse);}
Constants
package com.example.demo.constants;public class Constants {//成功public static final int SUCCESS_CODE = 200;//程序出错public static final int FAIL_CODE = -2;//未登录public static final int UNLOGIN_CODE = -1;public static final String SESSION_USER_KEY = "session_user_key";}
Result
package com.example.demo.model;import com.example.demo.constants.Constants;
import lombok.Data;@Data
public class Result<T> {private int code; //200-成功 -1 用户未登录 -2 程序出错 业务状态码, 非http状态码private String errMsg;private T data;public static <T> Result success(T data){Result result = new Result();result.setCode(Constants.SUCCESS_CODE);result.setErrMsg("");result.setData(data);return result;}public static <T> Result unlogin(){Result result = new Result();result.setCode(Constants.UNLOGIN_CODE);result.setErrMsg("用户未登录");return result;}public static <T> Result fail(T data){Result result = new Result();result.setCode(Constants.FAIL_CODE);result.setErrMsg("程序发生错误!");return result;}public static <T> Result fail(String errMsg){Result result = new Result();result.setCode(Constants.FAIL_CODE);result.setErrMsg(errMsg);return result;}public static <T> Result fail(String errMsg, int code){Result result = new Result();result.setCode(code);result.setErrMsg(errMsg);return result;}public static <T> Result fail(){Result result = new Result();result.setCode(Constants.FAIL_CODE);result.setErrMsg("程序发生错误!");return result;}
}