欢迎关注个人主页:逸狼
创造不易,可以点点赞吗
如有错误,欢迎指出~
前⾯图书管理系统,咱们只完成了⽤⼾登录和图书列表,并且数据是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=false
username: root
password: 111111
driver-class-name: com.mysql.cj.jdbc.Driver
application:
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;
@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 {
//图书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 {
@Autowired
private 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 {
@Autowired
private 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){//1
return "";
}
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{
//执行sql
bookService.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_info
set status = 0
where 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就会返回null
if(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;
}
}