目录
🚩项目所需要的技术栈
该项目是一个针对于SpringBoot+Mybatis+SpringMVC的基础运用项目适合初学者来检验水平测试能力,该项目所需技术栈如下:
>* SpringBoot:作为项目的框架,使用Maven托管代码
>* Mybatis:使用Mybatis框架操纵数据库,其中使用了xml和注解两种方式去操作数据库
>* 前端ajax:前后端的交互使用的是ajax作为前端为后端发送数据以及接收数据
>* 项目分层:项目分为前端页面+control(与前端建立连接的控制层)+Service(服务层供control层进行调用)+Mapper(操纵数据库实现数据与后端代码的 交互)+model(需要实现的主类)。
🚩项目准备工作
🎈环境准备
项目的创建需要选好项目名,项目路径,语言为java,type是基于maven构建,jdk可以选择17以上的(切记最好不要用jdk8),packing是打成jar包。
此时项目创建成功。MySQL Driver和MyBatis Framework引⼊MyBatis 和 MySQL驱动依赖
也可以手动引入依赖,上面只是更简单。
这是围绕整个项目的配置文件,没有该配置文件,是无法运行成功的,没有它们你就完成不了一个项目。我们依赖该pom.xml文件,让我们能完成该项目。
SpringBookt配置文件,统一使用yml格式 application.yml
很多项⽬或者框架的配置信息也放在配置⽂件中, ⽐如:
- • 项⽬的启动端⼝
- • 数据库的连接信息(包含⽤⼾名和密码的设置)
-
• ⽤于发现和定位问题的普通⽇志和异常⽇志等
server:
port: 8080
#配置数据库
spring:
datasource:
url: jdbc:mysql://127.0.0.1:3306/book_system?characterEncoding=utf8&useSSL=false
username: root
password:
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
configuration:
#配置驼峰自动转换
map-underscore-to-camel-case: true
#sql日志(打印出来让我们可以清楚自己的sql语句是否正确)
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#Spring Boot可以正确找到并加载位于 classpath:mapper/目录下的 xxxxMapper XML 文件,
#从而在应用程序中使用这些 Mapper 进行数据库操作。(因为有时候sql语句需要动态)
mapper-locations: classpath:mapper/BookMapper.xml
#将日志记录到一个文件可以通过在配置文件中指定日志文件的位置和名称来实现。
logging:
file:
name: spring-book.log
🎈数据库准备
- 数据库表的设计是应用程序开发的一个重要的环节。设计数据库表是根据业务需求相关的。
- 数据库表通常分成两种:实体表和关系表,就如图书管理系统来说,图书馆里系统相对是简单的,只有两个实体:用户和图书,并且用户和图书之间没有关联关系。
- 表的具体字段设计,也与需求相关。
**⽤⼾表:**有⽤⼾名和密码即可(复杂的业务可能还涉及昵称, 年龄等资料)
**图书表:**有哪些字段, 也是参考需求⻚⾯(通常不是⼀个⻚⾯决定的, ⽽是要对整个 系统进⾏全⾯分析观察后定的)
创建数据库book_system 用户表user_info 图书表 book_info
DROP DATABASE IF EXISTS book_system;
CREATE DATABASE book_system DEFAULT CHARACTER SET utf8mb4;
use book_system;
-- ⽤⼾表
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, status)
VALUES
('深入理解计算机系统', 'Randal E. Bryant', 18, 98.00, '机械工业出版社', 1),
('现代操作系统', 'Andrew S. Tanenbaum', 10, 89.00, '清华大学出版社', 1),
('计算机网络:自顶向下方法', 'James Kurose', 14, 85.00, '电子工业出版社', 1),
('数据库系统概念', 'Abraham Silberschatz', 7, 110.00, '机械工业出版社', 1),
('算法导论', 'Thomas H. Cormen', 5, 130.00, '机械工业出版社', 1),
('机器学习', '周志华', 20, 120.00, '清华大学出版社', 1),
('Python编程:从入门到实践', 'Eric Matthes', 25, 65.00, '电子工业出版社', 1),
('深度学习', 'Ian Goodfellow', 12, 180.00, '人民邮电出版社', 1),
('计算机图形学', 'John F. Hughes', 9, 95.00, '机械工业出版社', 1),
('操作系统真相还原', '史蒂文·穆查', 15, 80.00, '电子工业出版社', 1);
Model创建 BookInfo UserInfo
package com.example.cl.model;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@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;
}
package com.example.cl.model;
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;
}
🚩前后端交互分析
🎈登录
<div class="container-login">
<div class="container-pic">
<img src="pic/computer.png" width="350px">
</div>
<div class="login-dialog">
<h3>登陆</h3>
<div class="row">
<span>用户名</span>
<input type="text" name="userName" id="userName" class="form-control">
</div>
<div class="row">
<span>密码</span>
<input type="password" name="password" id="password" class="form-control">
</div>
<div class="row">
<button type="button" class="btn btn-info btn-lg" onclick="login()">登录</button>
</div>
</div>
</div>
前端登录有两个表单,输入用户名和密码,表单中name属性是用于表单提交时用于标识表单数据的属性,服务器端通过'name'属性来获取提交的数据。此时前端的id属性来获取客户端输入的用户名和密码给后端。
📝前后端交互
function login() {
// var userName= $("#userName").val();
// var password = $("#password").val()
$.ajax({
url: "/user/login",
type: "post",
data:{
userName: $("#userName").val(),
password: $("#password").val()
}
}
前端发出请求:
url:/user/login
type:post
参数:
data:userName=$("#userName").val()【admin】
password=$("#password").val()【admin】
响应:
true (密码或者用户名正确,返回true)
function login() { // var userName= $("#userName").val(); // var password = $("#password").val() $.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?pageNum=1"; }else{ alert(result.errMsg); } } }); }
因为登录页面基本上错误出现在用户名和密码上,如果用户名和密码出现问题,其实并没有必要返回error信息,如果服务器返回的结果码是"SUCCESS"和返回的结果数据是"",那么就表明密码正确,否则就弹窗 结果的errMsg错误信息。
📝实现服务器代码
我们要知道三层架构,mapper------service------controller
control(与前端建立连接的控制层)
Service(服务层供control层进行调用)
Mapper(操纵数据库实现数据与后端代码的 交互)
model(需要实现的主类)。
我们该如何实现服务器代码呢?首先,我们需要获取到delet_flag=0对应的userName因为,如果已经删除了,那么就也得返回null,如果没有找到服务器收到的userName那么返回null,如果找到了并且并没有删除该用户,那么就成功。
🎓数据层
package com.example.cl.mapper;
import com.example.cl.model.UserInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@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通常都被认为是数据库层
🎓业务层
package com.example.cl.service;
import com.example.cl.mapper.UserInfoMapper;
import com.example.cl.model.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class UserInfoService {
@Autowired
private UserInfoMapper userInfoMapper;
public UserInfo queryUserByName(String userName){
return userInfoMapper.queryUserByName(userName);
}
}
🎓控制层
我们需要进行前后端交互
我们需要创建一个类Result,里面包含errMsg(显示什么导致错误的信息),code(业务码), data(服务器返回给前端的数据)。
而code业务码,是需要通过键值对的形式创建,比如我们设定200标识succss,-1表示fail,-2表示未登录功能,我们需要创建一个一个枚举类。
创建一个枚举类StatusResult,枚举三种状态。
🍭Result类
package com.example.cl.model;
import com.example.cl.enums.ResultStatus;
import lombok.Data;
@Data
public class Result<T> {
private ResultStatus code; //业务码 不是Http状态码 200-成功 -2 失败 -1 未登录
private String errMsg; //错误信息, 如果业务成功, errMsg为空
private T data;
public static <T> Result success(T data){
Result result=new Result<>();
result.setCode(ResultStatus.SUCCESS);
result.setData(data);
return result;
}
public static Result fail(String msg){
Result result=new Result<>();
result.setCode(ResultStatus.Fail);
result.setErrMsg(msg);
return result;
}
}
🍭StatusResult枚举类
枚举类不能使用@Data注解
package com.example.cl;
public enum StatusResult {
SUCCESS(200),//成功返回200
FAIL(-1),//错误返回-1
NOLOGIN(-2)//未登录返回-2
;
private int code;
StatusResult(int code) {
this.code=code;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
}
package com.example.cl.controller;
import com.example.cl.constant.constants;
import com.example.cl.model.Result;
import com.example.cl.model.UserInfo;
import com.example.cl.service.UserInfoService;
import jakarta.servlet.http.HttpSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserInfoController {
@Autowired
private UserInfoService userInfoService;
@RequestMapping(value = "/login",produces = "application/json")
public Result login(String userName, String password, HttpSession session){
//1.效验参数
if(!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)){
//如果长度其中一个为空,
return Result.fail("用户名或者密码为空");
}
//账号和密码是否正确
//获取查找当前userName的用户所有信息
UserInfo userInfo=userInfoService.queryUserByName(userName);
if(userInfo==null){
return Result.fail("用户不存在");
}
if(!password.equals(userInfo.getPassword())){
return Result.fail("密码错误");
}
//3.正确情况
session.setAttribute(constants.USER_SESSION_KEY,userInfo);
return Result.success("");//成功返回空串
}
}
📝测试前后端代码是否正确
后端通过postman进行检查,最后返回的结果正确,说明后端代码是没有问题的, 如果后端代码有问题会返回505错误码。
我们可以看到如果成功,errMsg为空,data返回给前端也是空,code是success
检查前端代码
🎈添加图书
📝前后端交互
前端发出请求:
url:/book/addBook
type:post
参数:
date:传给服务器的数据 ,用到的$("#addBook").serialize() 表示form表单中所有的数据
响应:
"" //失败信息,成功返回空串
function 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.code=="NOLOGIN"&&error.data==null) { location.href = "login.html"; } } }); }
如果没有返回错误,那么就判断result的data是否为空,如果为空,我们就跳转到博客列表页,如果不等于"",我们就弹框
如果返回错误,说明是用户未登录,那么就要判断error中的数据是否是未登录页码
📝实现服务器代码
插入图书,还是之前的三层架构方式,再mapper层中,我们要插入book_name, author, `count`, price, publish, `status`,这几个字段,因为创建时间和更新时间是以当时时间为准的,id是自增的。
🎓数据层
package com.example.cl.mapper;
import com.example.cl.model.BookInfo;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface BookInfoMapper {
/**
* 插入图书
*/
@Insert("insert into book_info (book_name, author, `count`, price, publish, `status`) "
+ "values (#{bookName}, #{author}, #{count}, #{price}, #{publish}, #{status})")
Integer insertBook(BookInfo bookInfo);
}
🎓业务层
package com.example.cl.service;
import com.example.cl.mapper.BookInfoMapper;
import com.example.cl.mapper.UserInfoMapper;
import com.example.cl.model.BookInfo;
import com.example.cl.model.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class BookInfoService {
@Autowired
private BookInfoMapper bookInfoMapper;
public Integer insertBook(BookInfo bookInfo){
return bookInfoMapper.insertBook(bookInfo);
}
}
🎓控制层
添加@Slf4j可以让我们再运行的时候,控制台会进行报出日志消息。
在后面我们是对图书进行增删改查的功能,这些功能的前提是我们需要用户必须是登录的,所以我们在效验参数之前,我们需要进行判断是否用户登录,如果没有登录返回error信息,设置状态码为NOLOGIN,并且错误信息就是"用户未登录"。
public static<T> Result nologin(String errMsg){
Result result=new Result();
result.setErrMsg(errMsg);
result.setCode(StatusResult.NOLOGIN);
return result;
}
如果用户登录了,就按照正常的流程进行添加图书,由于添加图书的返回值是int类型,如果添加图书的数量是>0的我们就表示添加成功,然后我们返回success成功码,如果没有返回成功,就会报错。
/**
* 增加图书
*/
@RequestMapping(value = "/addBook",produces = "application/json")
public Result<String> addBook(BookInfo bookInfo, HttpSession session){
Result<String> ans=new Result<>();
//1.判断用户是否登录
//如果用户信息为空, 说明用户未登录
UserInfo loginUserInfo = (UserInfo) session.getAttribute(constants.USER_SESSION_KEY);
if (loginUserInfo==null || loginUserInfo.getId()<=0){
return Result.nologin("用户未登录");
}
//1.效验参数
log.info("添加图书,接收到的参数bookInfo:{}",bookInfo);
if (!StringUtils.hasLength(bookInfo.getBookName())
|| !StringUtils.hasLength(bookInfo.getAuthor())
|| bookInfo.getCount()==null
|| bookInfo.getPrice()==null
|| !StringUtils.hasLength(bookInfo.getPublish())
|| bookInfo.getStatus()==null){
ans.setData("输入错误");
ans.setCode(StatusResult.FAIL);
return ans;
}
//添加图书
try {
Integer result=bookInfoService.insertBook(bookInfo);
if(result>0){
ans.setData("");
ans.setCode(StatusResult.SUCCESS);
return ans;
}
}catch (Exception e){
log.error("添加图书失败");
}
ans.setCode(StatusResult.FAIL);
ans.setData("添加失败");
return ans;
}
📝测试前后端代码是否正确
我们先看用户登录状态,首先用postman进行发出登录请求
然后通过postman进行发出添加图书请求,返回SUCCESS状态码
我们再来看用户未登录状态:返回的错误信息"errMsg",code为LOGIN
检查前端代码
此时查看数据库中的数据,此时增加了一条。
🎈图书列表
可以看到, 添加图书之后, 跳转到图书列表⻚⾯, 并没有显⽰刚才添加的图书信息, 接下来我们来实现图书列表。
需求分析
我们之前做的表⽩墙查询功能,是将数据库中所有的数据查询出来并展⽰到⻚⾯上,试想如果数据库中的数据有很多(假设有⼗⼏万条)的时候,将数据全部展⽰出来肯定不现实,那如何解决这个问题呢?
使⽤分⻚解决这个问题。每次只展⽰⼀⻚的数据,⽐如:⼀⻚展⽰10条数据,如果还想看其他的数
据,可以通过点击⻚码进⾏查询
分⻚时, 数据是如何展⽰的呢
第1⻚: 显⽰1-10 条的数据
第2⻚: 显⽰11-20 条的数据
第3⻚: 显⽰21-30 条的数据
以此类推...
要想实现这个功能, 从数据库中进⾏分⻚查询,我们要使⽤ LIMIT 关键字,格式为:
limit 开始索引每⻚显⽰的条数(开始索引从0开始)
我们先增加一些数据,让每一页都更加的明显显示出来。
第一页的开始索引是0 (1-0)*10
第二页的开始索引是10,(2-1)*10
观察以上SQL语句,发现: 开始索引⼀直在改变, 每⻚显⽰条数是固定的
开始索引的计算公式: 开始索引 = (当前⻚码 - 1) * 每⻚显⽰条数
我们继续基于前端⻚⾯, 继续分析, 得出以下结论:
前端发出请求,需要向服务器传递参数
- currentPage 当前⻚码 //默认值为1
- ◦ pageSize 每⻚显⽰条数 //默认值为10
为了项⽬更好的扩展性, 通常不设置固定值, ⽽是以参数的形式来进⾏传递
扩展性: 软件系统具备⾯对未来需求变化⽽进⾏扩展的能⼒ ,⽐如当前需求⼀⻚显⽰10条, 后期需求改为⼀⻚显⽰20条, 后端代码不需要任何修改
后端响应时, 需要响应给前端的数据- records 所查询到的数据列表(存储到List 集合中)
-
◦ total 总记录数 (⽤于告诉前端显⽰多少⻚, 显⽰⻚数为: (total + pageSize -1)/pageSize
翻⻚请求和响应部分, 我们通常封装在两个对象中:
🎓翻⻚请求对象package com.example.cl.model;
import lombok.Data;
@Data
public class PageRequest {
private Integer pageNum =1;//当前页
private Integer pageSize = 10;//每页中的记录数
private Integer offset;//起始索引public Integer getOffset() { return (pageNum-1) * pageSize; }
}
通过当前页和每页的记录数,记录当前的起始索引。
🎓翻⻚列表结果类:
package com.example.cl.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
@AllArgsConstructor //生成构造方法
@NoArgsConstructor //生成无构造方法
@Data
public class PageResult<T>{
private List<T> records;//当前页的数据
private Integer count;//所有的记录数
private PageRequest pageRequest;//从哪个下标开始,显示多少本书
}
📝前后端交互
前端发出请求
url:
/book/getListByPage?currentPage=1&pageSize=10
Content-Type: application/x-www-form-urlencoded; charset=UTF-8type: get
参数:
响应:
{"errMsg":"","data":"","code":""}
我们约定, 浏览器给服务器发送⼀个 /book/getListByPage 这样的 HTTP 请求, 通过
currentPage 参数告诉服务器, 当前请求为第⼏⻚的数据, 后端根据请求参数, 返回对应⻚的数据
第⼀⻚可以不传参数, currentPage默认值为 1
📝实现服务器代码
🎓数据层
/**
* 获取当前页数据
*/@Select("select * from book_info where status !=0 order by id asc limit #{offset}, #{pageSize}")//升序
List<BookInfo> queryBookListByPage(PageRequest pageRequest);//查询当前页所有未借阅的书
//offset索引起始 pageSize一页展示多少数据
@Select("select count(1) from book_info where status<>0")
Integer count();//统计未借阅的书的本数
🎓业务层
-
翻⻚信息需要返回数据的总数和列表信息, 需要查两次SQL
-
图书状态: 图书状态和数据库存储的status有⼀定的对应关系 ,如果后续状态码有变动, 我们需要修改项⽬中所有涉及的代码, 这种情况, 通常采⽤枚举类来处理映射关系
package com.example.cl.enums;
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 PageResult<BookInfo> queryBookListByPage(PageRequest pageRequest){
//获取所有未借阅的书的总数
Integer count=bookInfoMapper.count();
//获取当前页的记录
List<BookInfo>bookInfos=bookInfoMapper.queryBookListByPage(pageRequest.getOffset(), pageRequest.getPageSize());
//3.处理状态(0表示删除,1表示可借阅,2表示未借阅,之前用数字代替,现在需要转成string形式
for (BookInfo bookInfo:bookInfos) {
bookInfo.setStatus(BookStatus.getDescByCode(bookInfo.getStatus()).getCode());
}
return new PageResult(bookInfos,count,pageRequest);
}
🎓控制层
/**
* 获取当前页数据
*/
@RequestMapping("/getBookListByPage")
public Result<PageResult<BookInfo>> getBookListByPage(PageRequest pageRequest,HttpSession session){
Result<PageResult<BookInfo>> ans=new Result<>();
//1.判断用户是否登录
//如果用户信息为空, 说明用户未登录
UserInfo loginUserInfo = (UserInfo) session.getAttribute(constants.USER_SESSION_KEY);
if (loginUserInfo==null || loginUserInfo.getId()<=0){
return Result.nologin("用户未登录");
}
//
PageResult<BookInfo> bookList=bookInfoService.queryBookListByPage(pageRequest);
return Result.success(bookList);
}
public static <T> Result success(T data){
Result result=new Result();
result.setCode(StatusResult.SUCCESS);
result.setData(data);
return result;
}
此时结果返回bookList类型的。
📝测试前后端代码是否正确
先测后端
一共有32个书没有被借阅。
前端部分需要给博客列表做出来之后即可看到。
🎈修改图书
📝前后端交互
进⼊修改⻚⾯, 需要显⽰当前图书的信息
[请求]
/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表单的形式来
提交数据 ,服务器返回处理结果, 返回""表⽰添加图书成功, 否则, 返回失败信息.
📝实现服务器代码
🎓数据层
数据层我们需要先寻找该书通过id,因为当我们再点击修改的时候,我们就可以获取到该书的id号,然后再对该书进行更新。
/**
* 修改图书
*/
//通过id查询图书信息
@Select("select * from book_info where id=#{bookid} and status!=0")
BookInfo queryBookById(Integer bookid);
Integer updateBookById(BookInfo bookInfo);
通过点击修改按钮,之后跳转到更新页面的时候,就相当于通过id获取到图书信息显示再表单中,然后我们就可以再文本框中输入值,进行更改数据。
<?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.cl.mapper.BookMapper">
<update id="updateBookById">
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>
因为有些数据我们可能是不想改的,所以我们再更新图书的时候进行了动态sql,用了xml文件进行实现动态sql。
🎓业务层
/*
更改图书
*/
public Integer updateBookById(BookInfo bookInfo){
return bookInfoMapper.updateBookById(bookInfo);
}
public BookInfo queryBookById(Integer bookid){
return bookInfoMapper.queryBookById(bookid);
}
🎓控制层
/**
* 查询图书信息
*/
@RequestMapping("/queryBookById")
public Result<BookInfo> queryBookById(Integer bookId,HttpSession session) {
Result<BookInfo>ans=new Result<>();
//校验用户信息是否为空,说明是未登录状态
UserInfo loginUserInfo = (UserInfo) session.getAttribute(constants.USER_SESSION_KEY);
if (loginUserInfo==null || loginUserInfo.getId()<=0){
ans.setData(null);
ans.setCode(StatusResult.NOLOGIN);
return ans;
}
log.info("根据ID查询图书信息, id:"+bookId);
long start=System.currentTimeMillis();
BookInfo bookInfo=bookInfoService.queryBookById(bookId);
ans.setCode(StatusResult.SUCCESS);
ans.setData(bookInfo);
log.info("queryBookById 耗时: "+ (System.currentTimeMillis()-start) + "ms");
return ans;
}
/**
* 更新图书
*/
@RequestMapping(value = "/updateBook", produces = "application/json")
public Result<String> updateBook(BookInfo bookInfo,HttpSession session) {
Result<String>ans=new Result<>();
//效验参数是否登录
UserInfo loginUserInfo = (UserInfo) session.getAttribute(constants.USER_SESSION_KEY);
if (loginUserInfo==null || loginUserInfo.getId()<=0){
ans.setCode(StatusResult.NOLOGIN);
ans.setData(null);
return ans;
}
log.info("更新图书, bookInfo: {}", bookInfo);
try {
Integer result=bookInfoService.updateBookById(bookInfo);
if(result>0){
ans.setData("");
ans.setCode(StatusResult.SUCCESS);
return ans;
}
}catch (Exception e){
log.error("更新图书失败");
}
ans.setData(null);
ans.setCode(StatusResult.FAIL);
return ans;
}
首先通过id查询图书信息,返回的数据是BookInfo类型的,因为我们需要把书返回给客户端。
然后通过id进行修改该书。如果更新的结果行数大于0,说明更新成功,如果没有,那么就更新失败。返回null数据。
📝测试前后端代码是否正确
先测后端代码,首先是未登录状态
登录状态:
通过id获取图书信息
通过id更改图书信息
检查前端代码:
刚刚后端测试的时候进行修改的,此时我们通过前端修改作者,然后会跳转到博客列表页
🎈删除图书
📝约定前后端交互接⼝
删除分为 逻辑删除 和物理删除
逻辑删除
逻辑删除也称为软删除、假删除、Soft Delete,即不真正删除数据,⽽在某⾏数据上增加类型 is_deleted的删除标识,⼀般使⽤UPDATE语句
物理删除
物理删除也称为硬删除,从数据库表中删除某⼀⾏或某⼀集合数据,⼀般使⽤DELETE语句
删除图书的两种实现⽅式
- 逻辑删除 update book_info set status=0 where id = 1
- 物理删除 delete from book_info where id=25
物理删除+归档的⽅式实现有些复杂, 咱们采⽤逻辑删除的⽅式
逻辑删除的话, 依然是更新逻辑, 我们可以直接使⽤修改图书的接⼝
/book/deleteBook
[ 请求 ]
/book/deleteBook
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
[ 参数 ]
id=1&status=0
[ 响应 ]
"" // 失败信息 , 成功时返回空字符串
实现客⼾端代码
当后端接收到bookid的时候,数据层通过id删除该图书,由于删除的sql语句,返回的是int类型,如果删除成功,执行的长度>0,并且返回""数据,并且设置字节码为successs,然后跳转到列表页,如果执行失败,长度为0,返回的是null数据,并且设置字节码fail。失败的情况是未登录状态,如果返回的数据是空并且字节码是nologin,就返回的到登录页面
📝实现服务器代码
🎓数据层
删除我们是按照id进行删除的,并且我们是按照逻辑删除的,所以就相当于通过id进行更新。
🎓服务层
/**
* 删除图书
*/
public Integer deleteBook(Integer bookId){
BookInfo bookInfo=new BookInfo();
bookInfo.setId(bookId);
bookInfo.setStatus(0);
return bookInfoMapper.updateBookById(bookInfo);
}
我们需要给定id,并且给状态设置为0,然后进行通过id进行修改即可。
🎓控制层
/**
* 删除图书
*/
@RequestMapping("/deleteBook")
public Result<Boolean> deleteBook(Integer bookId,HttpSession session){
Result<Boolean>ans=new Result<>();
//效验参数是否登录
UserInfo loginUserInfo = (UserInfo) session.getAttribute(constants.USER_SESSION_KEY);
if (loginUserInfo==null || loginUserInfo.getId()<=0){
ans.setCode(StatusResult.NOLOGIN);
ans.setData(null);
return ans;
}
log.info("删除图书,bookId:{}",bookId);
Integer result=bookInfoService.deleteBook(bookId);
if(result>0){
ans.setData(true);
ans.setCode(StatusResult.SUCCESS);
return ans;
}
ans.setData(false);
ans.setCode(StatusResult.FAIL);
return ans;
}
控制层返回的数据是返回给客户端代码中。
📝测试前后端代码是否正确
先测后端代码,首先是未登录状态
登录状态:
通过id删除图书:
检查前端代码:
此时删除成功,返回的列表页。
🎈批量删除图书
📝约定前后端交互接⼝
/book/batchDeleteBook
[ 请求 ]
/book/batchDeleteBook
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
[ 参数 ]
id=1&status=0
[ 响应 ]
"" // 失败信息 , 成功时返回空字符串
function batchDelete() {
var isDelete = confirm("确认批量删除?");
if (isDelete) {
//获取复选框的id
var ids = [];
//已经选中的元素
$("input:checkbox[name='selectBook']:checked").each(function () {
ids.push($(this).val());
});
console.log(ids);
$.ajax({
url: "/book/batchDeleteBook?ids=" + ids,
type: "post",
success: function (result) {
if (result.code == "SUCCESS" && result.data == "") {
//删除成功
location.href = "book_list.html";
} else {
alert(result.data);
}
},
error: function (error) {
//用户未登录
if (error.data == null && error.data=="NOLOGIN") {
location.href = "login.html";
}
}
});
// alert("批量删除成功");
}
}
前端将url和type发送给服务器。
📝实现服务器代码
🎓数据层
Integer batchDeleteBookByIds(List<Integer> ids);
批量删除,我们需要将选中的id对应的图书都删除掉。用到动态sql语句
<delete id="batchDeleteBookByIds">
update book_info set status=0
where id in
<foreach collection="ids" open="(" close=")" item="id" separator=",">
#{id}
</foreach>
</delete>
🎓业务层
/**
* 批量删除
*/
public Integer batchDeleteBookByIds(List<Integer> ids){
return bookInfoMapper.batchDeleteBookByIds(ids);
}
🎓控制层
/**
* 批量删除 ids
*/
@RequestMapping(value = "/batchDeleteBook", produces = "application/json")
public Result<String> batchDelete(@RequestParam List<Integer> ids, HttpSession session) {
Result<String>ans=new Result<>();
//效验参数是否登录
UserInfo loginUserInfo = (UserInfo) session.getAttribute(constants.USER_SESSION_KEY);
if (loginUserInfo==null || loginUserInfo.getId()<=0){
ans.setCode(StatusResult.NOLOGIN);
ans.setData(null);
return ans;
}
Integer result= bookInfoService.batchDeleteBookByIds(ids);
if(result>0){
ans.setData("");
ans.setCode(StatusResult.SUCCESS);
return ans;
}
ans.setData("批量删除失败");
ans.setCode(StatusResult.FAIL);
return ans;
}
📝测试前后端代码是否正确
先测后端代码,首先是未登录状态
登录状态:
通过ids批量删除图书:
检查前端代码:
此时批量删除成功!
永远有优秀的代码,永远有提升的空间,比如,我们次实现都要调用一个接口,是不是很复杂,如果有更多的功能接口,那么就会更复杂。我们想想会有什么方法来优化呢?后续会有统一功能等等,让这个项目做进一步的优化。
披星戴月走过的路必将繁华似锦!