【实战】动态 SQL + 统一 Result + 登录校验:图书管理系统(下)
文章目录
- 功能实现
- (四) 修改图书
- 1.约定前后端交互接口
- 2.实现服务端代码
- 3.实现客户端代码
- (五) 删除图书
- 1.约定前后端交互接口
- 2. 实现客户端代码
- (六) 批量删除
- 1.约定前后端交互接口
- 2.实现服务器代码
- (七) 强制登录
- 实现服务端代码
- 实现客户端代码
功能实现
(四) 修改图书
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表单的形式来提交数据
服务器返回处理结果,返回""表示添加图书成功,否则,返回失败信息.
2.实现服务端代码
- BookController
@RequestMapping("/queryBookById")public BookInfo queryBookById(Integer bookId){log.info("查询图书信息,bookId:" + bookId);return bookService.queryBookById(bookId);}@RequestMapping("/updateBook")public String updateBook(BookInfo bookInfo){log.info("修改图书,bookInfo: {}",bookInfo);try{bookService.updateBook(bookInfo);//成功return "";}catch (Exception e){log.error("修改图书发生异常, e:",e);return "修改图书发生异常";}}
- 业务层:
BookService
public void updateBook(BookInfo bookInfo){bookMapper.updateBook(bookInfo);}public Integer batchDelete(List<Integer> ids){return bookMapper.batchDelete(ids);}
- 数据层:
@Select("select * from book_info where `status` != 0 and id = #{bookId}")BookInfo queryBookById(Integer bookId);
- xml实现 更新
创建BookInfoMapper.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.chaoxin.book.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><delete id="batchDelete">update book_info set status=0 where id in<foreach collection="list" item="id" open="(" close=")" separator=",">#{id}</foreach></delete>
</mapper>

3.实现客户端代码
http://127.0.0.1:8080/book_update.html?bookId=25 (25为对应的图书ID)
finalHtml += '<a href="book_update.html?bookId=' + book.id + '">修改</a>';
点击[修改]链接时,就会自动跳转到 http://127.0.0.1:8080/book_update.html?bookId=25 (25为对应的图书ID)
进入[修改图书]页面时,需要先从后端拿到当前图书的信息,显示在页面上
//进⼊[修改图书]⻚⾯时需要先从后端拿到当前图书的信息,显⽰在⻚⾯上getBookInfo();function getBookInfo(){$.ajax({type: "get",url: "/book/queryBookById" + location.search,success: function(result){//TODO 检验非常简陋var bookInfo = result.data;if(bookInfo != null){$("#bookId").val(bookInfo.id);$("#bookName").val(bookInfo.bookName);$("#bookAuthor").val(bookInfo.author);$("#bookStock").val(bookInfo.count);$("#bookPrice").val(bookInfo.price);$("#bookPublisher").val(bookInfo.publish);$("#bookStatus").val(bookInfo.status);}else{console.log(result);}},error: function(error){if(error.status == 401){//用户未登录location.href = "login.html";}}});}//修改图书function update() {$.ajax({type: "post",url: "/book/updateBook",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);}});// alert("更新成功");// location.href = "book_list.html"}
我们修改图书信息,是根据图书ID来修改的,所以需要前端传递的参数中,包含图书ID.
有两种方式:
- 获取url中参数的值(比较复杂,需要拆分url)
- 在form表单中,再增加一个隐藏输入框,存储图书ID,随
$("#updateBook").serialize()一起提交到后端
我们采用第二种方式
在form表单中,添加隐藏输入框
<form id="updateBook"><input type="hidden" class="form-control" id="bookId" name="id"><div class="form-group"><label for="bookName">图书名称:</label><input type="text" class="form-control" id="bookName" name="bookName"></div><div class="form-group"><label for="bookAuthor">图书作者</label><input type="text" class="form-control" id="bookAuthor" name="author"/></div><div class="form-group"><label for="bookStock">图书库存</label><input type="text" class="form-control" id="bookStock" name="count"/></div><div class="form-group"><label for="bookPrice">图书定价:</label><input type="number" class="form-control" id="bookPrice" name="price"></div><div class="form-group"><label for="bookPublisher">出版社</label><input type="text" id="bookPublisher" class="form-control" name="publish"/></div><div class="form-group"><label for="bookStatus">图书状态</label><select class="custom-select" id="bookStatus" name="status"><option value="1" selected>可借阅</option><option value="2">不可借阅</option></select></div><div class="form-group" style="text-align: right"><button type="button" class="btn btn-info btn-lg" onclick="update()">确定</button><button type="button" class="btn btn-secondary btn-lg" onclick="javascript:history.back()">返回</button></div></form>
hidden 类型的
<input>元素
隐藏表单,用户不可见、不可改的数据,在用户提交表单时,这些数据会一并发送出
使用场景: 正被请求或编辑的内容的 ID. 这些隐藏的 input 元素在渲染完成的页面中完全不可见,而且没有方法可以使它重新变为可见.
页面加载时,给该hidden框赋值
$("#bookId").val(book.id);

(五) 删除图书
1.约定前后端交互接口
删除分为逻辑删除和物理删除
逻辑删除
逻辑删除也称为软删除、假删除、Soft Delete,即不真正删除数据,而在某行数据上增加类型is_deleted的删除标识,一般使用UPDATE语句
物理删除
物理删除也称为硬删除,从数据库表中删除某一行或某一集合数据,一般使用DELETE语句
删除图书的两种实现方式
逻辑删除
update book_info set status=0 where id = 1
物理删除
delete from book_info where id=25
数据是公司的重要财产,通常情况下,我们采用逻辑删除的方式,当然也可以采用[物理删除+归档]的方式
物理删除并归档
- 创建一个与原表差不多结构,记录删除时间,实现INSERT … SELECT即可 [SQL INSERT INTO SELECT 语句 | 菜鸟教程](SQL INSERT INTO SELECT 语句 | 菜鸟教程链接)
- 插入和删除操作,放在同一个事务中执行
物理删除+归档的方式实现有些复杂,咱们采用逻辑删除的方式
逻辑删除的话,依然是更新逻辑,我们可以直接使用修改图书的接口/book/updateBook
[请求]
/book/updateBook
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
[参数]
id=1&status=0
[响应]
"" //失败信息,成功时返回空字符串
2. 实现客户端代码
function deleteBook(id) {var isDelete = confirm("确认删除?");if (isDelete) {$.ajax({type: "post",url: "/book/deleteBook?bookId=" + id,success: function (result) {if (result.code == "SUCCESS" && result.data == "") {location.href = "book_list.html?currentPage=1";} else {alert(result.errMsg);}}});}}

(六) 批量删除
批量删除,其实就是批量修改数据
1.约定前后端交互接口
/book/batchDeleteBook
Content-Type: application/x-www-form-urlencoded; charset=UTF-8[参数][响应]
"" //失败信息,成功时返回空字符串
点击[批量删除]按钮时,只需要把复选框选中的图书的ID,发送到后端即可
多个id,我们使用List的形式来传递参数
2.实现服务器代码
- 控制层:
BookController
@RequestMapping("/batchDelete")public Boolean batchDelete(@RequestParam List<Integer> ids) {log.info("批量删除图书, ids:{}", ids);try {bookService.batchDelete(ids);return true;} catch (Exception e) {log.error("批量删除图书失败, e:", e);return false;}}
- 业务层:
public Integer batchDelete(List<Integer> ids){return bookMapper.batchDelete(ids);}
- 数据层:
批量删除需要用到动态SQL,我们这里都用xml来实现
BookInfoMapper.java
接口定义
void batchDeleteBook(List<Integer> ids);
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.chaoxin.book.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><delete id="batchDelete">update book_info set status=0 where id in<foreach collection="list" item="id" open="(" close=")" separator=",">#{id}</foreach></delete><select id="queryBookListByPage" resultType="com.chaoxin.book.model.BookInfo"></select>
</mapper>


(七) 强制登录
虽然我们做了用户登录,但是我们发现,用户不登录,依然可以操作图书.
这是有极大风险的.所以我们需要进行强制登录.
如果用户未登录就访问图书列表或者添加图书等页面,强制跳转到登录页面.
实现思路分析
用户登录时,我们已经把登录用户的信息存储在了Session中.那就可以通过Session中的信息来判断用户是否登录.
- 如果Session中可以取到登录用户的信息,说明用户已经登录了,可以进行后续操作
- 如果Session中取不到登录用户的信息,说明用户未登录,则跳转到登录页面.
直接访问接口,无需登录就可以对书籍进行删除添加
我们需要在增加一个属性告知后端的状态以及后端出错的原因:
@Data
public class PageResult<T> {private int status;private String errorMessage;private int total;//所有记录数private List<T> records; // 当前页数据private PageRequest pageRequest;public PageResult(Integer total, PageRequest pageRequest, List<T> records) {this.total = total;this.pageRequest = pageRequest;this.records = records;}
}
后端数据所有的接口都要跟着修改,这样修改代码效率低,不放对所有后端返回的数据进行一个封装
@Data
public class Result<T> {private int status;private String errorMessage;private T data;
}
data为之前接口返回的数据
status为后端业务处理的状态码,也可以使用枚举来表示
public enum ResultStatus {SUCCESS(200),UNLOGIN(-1),FAIL(-2);private Integer code;ResultStatus(int code) {this.code = code;}public Integer getCode() {return code;}
}
修改 Result 添加常用方法
@Data
public class Result<T> {private ResultCodeEnum code; //-1 未登录 200 正常 -2 出错private String errMsg;private T data;public static <T> Result success(T data){Result result = new Result();result.setCode(ResultCodeEnum.SUCCESS);result.setErrMsg("");result.setData(data);return result;}public static <T> Result fail(String errMsg){Result result = new Result();result.setCode(ResultCodeEnum.FAIL);result.setErrMsg(errMsg);result.setData(null);return result;}public static <T> Result fail(String errMsg, T data){Result result = new Result();result.setCode(ResultCodeEnum.FAIL);result.setErrMsg(errMsg);result.setData(data);return result;}public static <T> Result unlogin(){Result result = new Result();result.setCode(ResultCodeEnum.UNLOGIN);result.setErrMsg("用户未登录");return result;}
}
实现服务端代码
修改图书列表接口,进行登录校验
@RequestMapping("/getListByPage")
public Result getListByPage(PageRequest pageRequest, HttpSession session) {log.info("获取图书列表, pageRequest:{}", pageRequest);//判断用户是否登录if (session.getAttribute("session_user_key")==null){return Result.unlogin();}UserInfo userInfo = (UserInfo) session.getAttribute("session_user_key");if (userInfo==null || userInfo.getId()<0 ||"".equals(userInfo.getUserName())){return Result.unlogin();}//用户登录,返回图书列表PageResult<BookInfo> pageResult = bookService.getBookListByPage(pageRequest);return Result.success(pageResult);
}
问题:如果修改常量session的key,就需要修改所有使用到这个key的地方,出于高内聚低耦合的思想,我们把常量集中在一个类里
创建类:Constants
public class Constants {public static final String SESSION_USER_KEY = "session_user_key";
}
常量命名规则:
常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要在意名字长度.
正例: MAX_STOCK_COUNT / CACHE_EXPIRED_TIME
反例: COUNT / TIME
实现客户端代码
由于后端接口发生变化,所以前端接口也需要进行调整
这也就是为什么前后端交互接口一旦定义好,尽量不要发生变化.
所以后端接口返回的数据类型一般不定义为基本类型,包装类型或者集合类等,而是定义为自定义对象.方便后续做扩展
修改图书列表的方法(下面代码为修改部分):
function getBookList() {//...success: function (result) {//真实前端代码需要分的更细一点,此处不做判断if (result == null || result.data == null) {location.href = "login.html";return;}//....var data = result.data;$("#pageContainer").jqPaginator({totalCounts: data.total, //总记录数pageSize: 10, //每页的个数visiblePages: 5, //可视页数currentPage: data.pageRequest.currentPage, //当前页码//....});}
}
