Web 小项目: 网页版图书管理系统
目录
最终效果展示
代码 Gitee 地址
1. 引言
2. 留言板 [热身小练习]
2.1 准备工作 - 配置相关
2.2 创建留言表
2.3 创建 Java 类
2.4 定义 Mapper 接口
2.5 controller
2.6 service
3. 图书管理系统
3.1 准备工作 - 配置相关
3.2 创建数据库表
3.2.1 创建用户表 + 图书表
3.3 创建 Java 类
3.4 校验用户登录接口
3.5 添加图书
3.5.1 约定前后端交互接口
3.5.2 后端接口
3.5.3 前端代码
3.6 展示图书列表(分页展示)
3.6.1 约定前后端交互接口
3.6.2 后端接口
3.6.2.1 准备工作 - 参数接收和响应返回
3.6.2.2 编写 Mapper 层方法
3.6.2.3 controller + service
3.6.3 前端代码
3.7 更新图书信息
3.7.1 约定前后端交互接口
3.7.2 后端接口
3.7.2.1 Mapper 层
3.7.2.2 controller + service
3.7.3 前段代码
3.8 删除图书信息
3.8.1 约定前后端交互接口
3.8.2 后端接口
3.8.3 前端代码
3.9 批量删除图书信息
3.9.1 约定前后端交互接口
3.9.2 后端接口
3.9.3 前端代码
3.10 强制登录机制
3.10.1 后端接口
3.10.1.1 封装常量
3.10.1.2 封装响应结果
3.10.2 前端代码
最终效果展示
QQ2025318-205420-HD
代码 Gitee 地址
网页版 - 图书管理系统
1. 引言
在之前 Spring MVC 阶段的案例练习中, 我们只使用了 MVC 的知识来和前端进行交互, 没有对数据进行持久化的处理, 当重启服务器后, 所有的数据都会丢失.
之前练习的案例在这篇博客中:
Spring MVC:综合练习 - 深刻理解前后端交互过程-CSDN博客
而目前, 基于 MyBatis 知识的学习, 再来对之前的练习进行一下改造, 将数据持久化的保存到数据库中.
注意: 由于这些案例的部分接口已经在之前的博客中约定好了, 并且已经完成了前端代码, 因此在本篇博客中就不再赘述.
2. 留言板 [热身小练习]
在之前的代码中, 用户每发送一条留言, 前端会将这些留言追加到页面显示给用户, 并且我们的后端是会数据存储到 List 中, 当用户刷新页面时, 前端调用后端的接口, 将 List 中的数据返回给前端, 前端再将数据展示到页面上, 以到达用户刷新页面时, 之前发布的留言不会丢失的目的.
虽然之前的代码, 能够保证用户刷新页面时数据不丢失, 但是由于 List 是保存在内存中的, 服务器重启后, List 中的数据依旧会丢失.
要实现数据的持久化处理, 需要将数据保存到数据库中.
2.1 准备工作 - 配置相关
首先, 引入 MyBatis 和 MySQL 驱动的相关依赖.
接着, 进行数据库连接和其他相关配置.
spring:application:name: springboot-demo# 数据库配置datasource:url: jdbc:mysql://127.0.0.1:3306/mybatis_test?characterEncoding=utf8&useSSL=falseusername: rootpassword: 111111driver-class-name: com.mysql.cj.jdbc.Drivermybatis:# 配置 mybatis xml 的⽂件路径,在 resources/mapper 创建所有表的 xml ⽂件mapper-locations: classpath:mapper/*Mapper.xmlconfiguration:# 配置打印 MyBatis⽇志log-impl: org.apache.ibatis.logging.stdout.StdOutImpl# 配置驼峰自动转换map-underscore-to-camel-case: true
# 将默认的日志级别修改为 info
logging:level:root: info
2.2 创建留言表
将留言信息保存到数据库中, 首先需要创建一个留言表(message_info):
DROP TABLE IF EXISTS message_info;
CREATE TABLE `message_info` (`id` INT ( 11 ) NOT NULL AUTO_INCREMENT,`from` VARCHAR ( 127 ) NOT NULL,`to` VARCHAR ( 127 ) NOT NULL,`message` VARCHAR ( 256 ) NOT NULL,`delete_flag` TINYINT ( 4 ) DEFAULT 0 COMMENT '0-正常, 1-删除',`create_time` DATETIME DEFAULT now(),`update_time` DATETIME DEFAULT now() ON UPDATE now(),
PRIMARY KEY ( `id` )
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;
注意: delete_flag 字段是为了实现逻辑删除而设定的.
- 逻辑删除: 不使用 delete 进行删除, 而是使用额外的字段(如: delete_flag)对记录进行标记, 表示不再使用该记录(不是真正的删除)
- 物理删除: 使用 delete 删除记录(真正的删除)
在实际工作中, 要尽量避免使用 delete, 以免带来不必要的损失.
2.3 创建 Java 类
创建完数据库表后, 需要创建一个 Java 实体类来和表中字段相映射.
@Data
public class MessageInfo {private int id;private String from;private String to;private String message;private int deleteFlag;private Date createTime;private Date updateTime;
}
2.4 定义 Mapper 接口
在这个练习中, 涉及到以下两个操作:
- 存储留言信息 => 将留言 insert 到 message_info 表中
- 查询留言信息 => 从 message_info 表中 select 数据
因此, 需要在 Mapper 接口中定义两个方法(这里使用注解完成数据库相关操作):
@Mapper
public interface MessageMapper {@Select("select * from message_info where delete_flag = 0")List<MessageInfo> selectAll();// 注意: from 和 to 是关键字, 要使用 ` 引起来@Insert("insert into message_info (`from`, `to`, message) values (#{from}, #{to}, #{message})")Integer insert(MessageInfo messageInfo);
}
注意: 表中的 from 字段和 to 字段是 MySQL 的关键字, 因此若指定这两个字段进行 sql 操作时, 需要使用反引号(`)引起来.
2.5 controller
controller 层接收前端传来的参数, 对参数进行简单校验后, 将参数传递给 service 层, service 层返回结果后, 再将结果返回给前端.
@RestController
@RequestMapping("/message")
public class MessageController {// 保存留言板信息
// List<MessageInfo> list = new ArrayList<>();@Resourceprivate MessageService messageService;// 接口一: 用户发表留言@PostMapping(value = "/publish", produces = "application/json")public String publish(@RequestBody MessageInfo messageInfo) {if(!StringUtils.hasLength(messageInfo.getFrom())|| !StringUtils.hasLength(messageInfo.getTo())|| !StringUtils.hasLength(messageInfo.getMessage())) {return "{\"ok\": 0}";}
// list.add(messageInfo);int affectedRows = messageService.insert(messageInfo);return "{\"ok\": 1}";}// 接口二: 获取留言信息@GetMapping("/getList")public List<MessageInfo> getList() {return messageService.selectAll();}
}
2.6 service
service 层接收 controller 传来的数据, 调用 mapper 层完成数据库操作, 并将结果返回给 controller 层:
@Service
public class MessageService {@ResourceMessageMapper messageMapper;public List<MessageInfo> selectAll() {return messageMapper.selectAll();}public Integer insert(MessageInfo messageInfo) {return messageMapper.insert(messageInfo);}
}
controller, service, mapper 层的编写顺序并无要求, 根据个人习惯编写即可.
完成以上操作, 就对数据进行了持久化处理, 将数据保存到数据库中了. 即使重启服务器, 数据也不会丢失.
3. 图书管理系统
3.1 准备工作 - 配置相关
首先, 依旧需要引入 MyBatis 和 MySQL 驱动的相关依赖.
接着, 进行数据库连接和其他相关配置.
spring:application:name: springboot-demo# 数据库配置datasource:url: jdbc:mysql://127.0.0.1:3306/book_test?characterEncoding=utf8&useSSL=falseusername: rootpassword: 111111driver-class-name: com.mysql.cj.jdbc.Drivermybatis:# 配置 mybatis xml 的⽂件路径,在 resources/mapper 创建所有表的 xml ⽂件mapper-locations: classpath:mapper/*Mapper.xmlconfiguration:# 配置打印 MyBatis⽇志log-impl: org.apache.ibatis.logging.stdout.StdOutImpl# 配置驼峰自动转换map-underscore-to-camel-case: true
# 将默认的日志级别修改为 info
logging:level:root: info
3.2 创建数据库表
3.2.1 创建用户表 + 图书表
-- 创建数据库
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 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, '民主与建设出版社');
3.3 创建 Java 类
为图书表和用户表创建相映射的 Java 实体类.
@Data
public class UserInfo {private int id;private String username;private String password;private int delete_flag; // 0-正常 1-删除private Date createTime;private Date updateTime;
}
@Data
public class BookInfo {private Integer id;private String bookName;private String author;private Integer count;private BigDecimal price;private String publish;// 状态信息, 习惯上使用数字private Integer status; // 0-删除, 1-正常, 2-不允许借阅// 图书状态的中文表示// 开发中, 一般交给前端处理. 由于学习, 在后端这里直接就处理了private String statusCN;private String createTime;private String updateTime;
}
3.4 校验用户登录接口
用户登录时, 输入账号密码, 前端接收数据并通过 Ajax 请求将参数传递给后端接口, 后端 controller 层接收参数, 并传递给 service 层, service 调用 mapper 层查询数据库数据, 校验账号密码是否正确, 最终由 controller 层将校验结果返回给前端, 前端再进行相关处理将结果展示给用户.
@RequestMapping("/user")
@RestController
public class UserController {@ResourceUserService userService;// 登录验证接口@RequestMapping("/login")public boolean login(String name, String password, HttpSession session) {if(!StringUtils.hasLength(name) || !StringUtils.hasLength(password)) {return false;}UserInfo userInfo = userService.selectUserInfoByName(name);if(userInfo != null && userInfo.getPassword().equals(password)) {// 登录成功, 将用户信息保存在 Session 中// 保存之前. 隐藏用户密码(可选)userInfo.setPassword("****");session.setAttribute("user", userInfo);return true;}return false;}
}
@Service
public class UserService {@ResourceUserMapper userMapper;public UserInfo selectUserInfoByName(String name) {return userMapper.selectUserInfoByName(name);}
}
在 Mapper 接口中, 对于校验用户登录, 需要定义一个根据用户名查询用户信息的方法:
@Mapper
public interface UserMapper {/*** 校验用户登录 : 根据用户名查询用户信息* @param name* @return*/@Select("select * from user_info where user_name = #{name}")UserInfo selectUserInfoByName(String name);
}
3.5 添加图书
3.5.1 约定前后端交互接口
3.5.2 后端接口
添加图书, 就是将新图书的信息插入到图书表中.
前端收到用户所添加图书的图书信息后, 调用后端接口并传递图书信息, 后端接口在 controller service 层对图书信息进行校验, 最终在 Mapper 层将新图书信息插入到图书表中.
注意: 后端添加图书的接口, 使用的是一个 bookInfo 对象来接收的, 但是这并不意味着前端传来的就是 JSON 数据, 当前端传递的是多个参数的时, 后端也可以使用对象来接收:
- 前端传递多个参数, 放到 queryString 中或者以 form 表单的形式传递 => 后端对象接收(不使用注解)
- queryString 和 form 表单的数据传输格式都为: key1=value1&key2=value2&.... 但 queryString 位于 URL 中(GET 请求), form 表单数据位于 body 中(POST 请求)
- 前端传递 JSON 数据, 放到 body 中进行传递(POST 请求) => 后端对象接收(使用 @RequestBody)
3.5.3 前端代码
由于我们主攻后端, 这里就讲解一下前端代码中的核心部分:
这里使用了 JQuery 的 serialize 函数(序列化), 自动将选中的 form 中的数据导入到了 data 属性中.
图书添加成功后, 就会跳转到图书列表.
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>添加图书</title><link rel="stylesheet" href="css/bootstrap.min.css"><link rel="stylesheet" href="css/add.css"></head><body><div class="container"><div class="form-inline"><h2 style="text-align: left; margin-left: 10px;"><svg xmlns="http://www.w3.org/2000/svg" width="40"fill="#17a2b8" class="bi bi-book-half" viewBox="0 0 16 16"><pathd="M8.5 2.687c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492V2.687zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783z" /></svg><span>添加图书</span></h2></div><form id="addBook"><div class="form-group"><label for="bookName">图书名称:</label><input type="text" class="form-control" placeholder="请输入图书名称" id="bookName" name="bookName"></div><div class="form-group"><label for="bookAuthor">图书作者</label><input type="text" class="form-control" placeholder="请输入图书作者" id="bookAuthor" name="author" /></div><div class="form-group"><label for="bookStock">图书库存</label><input type="text" class="form-control" placeholder="请输入图书库存" id="bookStock" name="count"/></div><div class="form-group"><label for="bookPrice">图书定价:</label><input type="number" class="form-control" placeholder="请输入价格" id="bookPrice" name="price"></div><div class="form-group"><label for="bookPublisher">出版社</label><input type="text" id="bookPublisher" class="form-control" placeholder="请输入图书出版社" 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="add()">确定</button><button type="button" class="btn btn-secondary btn-lg" onclick="javascript:history.back()">返回</button></div></form></div><script type="text/javascript" src="js/jquery.min.js"></script><!-- 实现前后端交互 --><script>function add() {// 此时, 前端应进行参数校验, 此处省略// 前端向后端接口发送 Ajax 请求$.ajax({type: "post",url: "/book/addBook",data: $("#addBook").serialize(),success: function(body) {if(body == "") {alert("添加成功");location.assign("book_list.html");}else {alert(body);}}});}</script>
</body></html>
3.6 展示图书列表(分页展示)
当用户登录成功后, 就会来到图书列表界面.
图书系统中可能存储着大量的书籍, 在一个网页中是展示不完的, 因此需要对图书列表进行分页:
3.6.1 约定前后端交互接口
3.6.2 后端接口
首先, 先来回顾下分页查询的 sql 语句:
MySQL 中, 使用 LIMIT 关键字进行分页查询, 后面跟两个参数:
- 第一个参数为 offset, 表示偏移量(从第几个记录开始往后进行查询, 不包含 offset 本身)
- 第二个参数为 limit, 表示从 offset 后, 要查询的个数(每页中数据的个数)
并且, 可以根据页数和每页个数计算得出 offset. 偏移量 = (当前页数 - 1) * (每页个数)
3.6.2.1 准备工作 - 参数接收和响应返回
后端接口必定需要接收 页码(currentPage) 以及每页的记录数(pageSize) 这两个参数, 因为只有知道了这两个参数, 才能计算得出 offset, 才能编写 sql 进行分页查询.
因此, 可以新建一个类专门用来接收请求中的参数:
@Data
public class RequestPage {// 当前端没有传值时, 默认当前页是 1, 默认一页的大小是 10 条记录// 查询哪一页private int currentPage = 1;// 每页中有多少条记录private int pageSize = 10;// 计算得到偏移量private int offset;// 根据当前页和每页的个数, 计算 offsetpublic int getOffset() {return this.offset = (this.currentPage - 1) * this.pageSize;}
}
注意: 如果 Mapper 方法参数是一个对象, 那么 #{} 是根据 get 方法获取对象属性值的, 因此我们在 offset 的 get 方法中, 计算得到 offset 返回即可.
注意: offset 的值必须通过 get 方法得到, 不能在构造方法中计算 offset 的值, 因为构造方法只能执行一次, currentPage 和 pageSize 已经有了默认值, 那么在构造对象时, offset 就会在构造方法中根据 currentPage 和 pageSize 的默认值被计算定型(offset 就会始终为 (1 - 1) * 10 = 0!!), 即使后续前端对 currentPage 和 pageSize 值进行了传递更改, offset 的值仍然不会改变!! 而通过 get 方法获取 offset, 每次获取到的都是最新值!!
此外, 根据接口文档, 响应结果包含了 total(表中记录总数) 和 List<BookIfo>(当前页中的图书信息) 两个属性, 因此, 可以新建一个类, 返回该类的对象作为响应结果:
@AllArgsConstructor
@NoArgsConstructor
@Data
public class ResponseResult<T> {// 表中记录的总数private int total;// 当前页中的图书信息private List<T> records;// 把请求内容放到响应中返回, 以便前端后续查询private RequestPage requestPage;
}
3.6.2.2 编写 Mapper 层方法
有了以上准备后, 就可以编写 Mapper 层了:
3.6.2.3 controller + service
接收到前端传来的 currentPage 和 pageSize 后, 我们直接在 service 层调用 Mapper 方法, 进行 count 计数和分页查询, 并将结果打包到 ResponseBody 对象中返回即可.
此外, 在 service 中, 还需要对分页查询得到的图书的 statusCN 属性依据 status 的值进行处理(这里通过枚举类):
枚举类:
3.6.3 前端代码
前端代码中, 这里使用了一个分页组件: https://jqpaginator.keenwon.com/
这里仍然只讲一下前端代码中的核心逻辑:
组件相关:
location.search 可以获取 URL 中 queryString 的信息(包括 ? ):
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>图书列表展示</title><link rel="stylesheet" href="css/bootstrap.min.css"><link rel="stylesheet" href="css/list.css"><script type="text/javascript" src="js/jquery.min.js"></script><script type="text/javascript" src="js/bootstrap.min.js"></script><script src="js/jq-paginator.js"></script></head><body><div class="bookContainer"><h2>图书列表展示</h2><div class="navbar-justify-between"><div><button class="btn btn-outline-info" type="button" onclick="location.href='book_add.html'">添加图书</button><button class="btn btn-outline-info" type="button" onclick="batchDelete()">批量删除</button></div></div><table><thead><tr><td>选择</td><td class="width100">图书ID</td><td>书名</td><td>作者</td><td>数量</td><td>定价</td><td>出版社</td><td>状态</td><td class="width200">操作</td></tr></thead><tbody></tbody></table><div class="demo"><ul id="pageContainer" class="pagination justify-content-center"></ul></div><script>getBookList();function getBookList() {$.ajax({url: "/book/getListByPage" + location.search,type: "get",success: function(res) {if(res == null || res.records == null) {return;}var books = res.records;var newHtml = '';for(var book of books) {newHtml += '<tr>';newHtml += '<td><input type="checkbox"name="selectBook" value="' + book.id + '" id="selectBook" class="book-select"></td>';newHtml += '<td>' + book.id + '</td>';newHtml += '<td>' + book.bookName + '</td>';newHtml += '<td>' + book.author + '</td>';newHtml += '<td>' + book.count + '</td>';newHtml += '<td>' + book.price + '</td>';newHtml += '<td>' + book.publish + '</td>';newHtml += '<td>' + book.statusCN + '</td>';newHtml += '<td><div class="op">';newHtml += '<a href="book_update.html?id=' + book.id + '">修改</a>';newHtml += '<a href="javascript:void(0)" onclick="deleteBook(' + book.id + ')">删除</a>';newHtml += '</div></td></tr>';}// .html => 置换 tbody 标签里面的内容$("tbody").html(newHtml);//翻页信息$("#pageContainer").jqPaginator({totalCounts: res.total, //总记录数pageSize: 10, //每页的个数visiblePages: 5, //可视页数currentPage: res.requestPage.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) {if(type == "change") {location.assign("book_list.html?currentPage=" + page);}}});}});}function deleteBook(id) {var isDelete = confirm("确认删除?");if (isDelete) {$.ajax({url: "/book/deleteBookById?id=" + id,type: "post",success: function(result) {if(result == "") {//删除图书alert("删除成功!!");location.assign("book_list.html");}else {alert("删除失败!! " + result);}}});}}function batchDelete() {var isDelete = confirm("确认批量删除?");if (isDelete) {//获取复选框的idvar ids = [];$("input:checkbox[name='selectBook']:checked").each(function () {ids.push($(this).val());});console.log(ids);alert("批量删除成功");}}</script></div>
</body></html>
3.7 更新图书信息
3.7.1 约定前后端交互接口
进入更新图书信息的页面时, 需要先展示原来的图书信息, 然后用户选择性的对图书信息进行更新.
因此我们后端需要提供两个接口:
- 根据 id 查询图书信息接口
- 更新图书信息接口
3.7.2 后端接口
3.7.2.1 Mapper 层
根据 id 查询图书信息的接口, 不必多说.
但是更新图书信息的接口, 由于用户是选择性的更新图书信息, 因此需要编写动态 sql 来完成:
3.7.2.2 controller + service
3.7.3 前段代码
- 更新图书信息的 html 文件中, 首先需要调用后端 selectById 接口, 通过 id 查询图书信息, 将原本的图书信息展示在页面中
- 用户填写要修改内容, 前端将这些数据发送给后端, 后端接口进行 update 操作.
核心框架如下:
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>修改图书</title><link rel="stylesheet" href="css/bootstrap.min.css"><link rel="stylesheet" href="css/add.css">
</head><body><div class="container"><div class="form-inline"><h2 style="text-align: left; margin-left: 10px;"><svg xmlns="http://www.w3.org/2000/svg" width="40"fill="#17a2b8" class="bi bi-book-half" viewBox="0 0 16 16"><pathd="M8.5 2.687c.654-.689 1.782-.886 3.112-.752 1.234.124 2.503.523 3.388.893v9.923c-.918-.35-2.107-.692-3.287-.81-1.094-.111-2.278-.039-3.213.492V2.687zM8 1.783C7.015.936 5.587.81 4.287.94c-1.514.153-3.042.672-3.994 1.105A.5.5 0 0 0 0 2.5v11a.5.5 0 0 0 .707.455c.882-.4 2.303-.881 3.68-1.02 1.409-.142 2.59.087 3.223.877a.5.5 0 0 0 .78 0c.633-.79 1.814-1.019 3.222-.877 1.378.139 2.8.62 3.681 1.02A.5.5 0 0 0 16 13.5v-11a.5.5 0 0 0-.293-.455c-.952-.433-2.48-.952-3.994-1.105C10.413.809 8.985.936 8 1.783z" /></svg><span>修改图书</span></h2></div><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></div><script type="text/javascript" src="js/jquery.min.js"></script><script>getBookInfo();function getBookInfo() {// 进入修改页面后, 先展示该图书原来的信息$.ajax({// 从路径中获取要修改的图书的 id(从 book_list.html 的 "修改" 超链接跳转过来的)url: "/book/selectById" + location.search,success: function(result) {if(result == null) {return;}// 将要修改的图书 id, 记录在隐藏标签中, 以便后续根据 id 进行 update 操作$("#bookId").val(result.id),$("#bookName").val(result.bookName),$("#bookAuthor").val(result.author),$("#bookStock").val(result.count),$("#bookPrice").val(result.price),$("#bookPublisher").val(result.publish),$("#bookStatus").val(result.status)}});}// 将用户做出的修改, 发送给后端接口function update() {// 此处, 应对用户的输入进行校验, 这里暂且忽略.$.ajax({url: "/book/updateBook",type: "post",data: $("#updateBook").serialize(),success: function(result) {if(result == "") {alert("更新成功!!");location.assign("book_list.html");}else {alert("更新失败!!");location.assign("book_list.html");}}});}</script>
</body></html>
3.8 删除图书信息
3.8.1 约定前后端交互接口
3.8.2 后端接口
删除图信息, 本质是上就将图书对象中的 status 属性修改为 0.
可以和更新图书信息操作共用一个 Mapper 接口, 仅对 status 属性进行修改即可.
因此, Mapper 层可以不做修改, 只需封装一个 controller 和 service 即可.
3.8.3 前端代码
function deleteBook(id) {var isDelete = confirm("确认删除?");if (isDelete) {$.ajax({url: "/book/deleteBookById?id=" + id,type: "post",success: function(result) {if(result == "") {//删除图书alert("删除成功!!");location.assign("book_list.html");}else {alert("删除失败!! " + result);}}});}}
3.9 批量删除图书信息
3.9.1 约定前后端交互接口
3.9.2 后端接口
批量删除图书和删除图书, 本质上都是 update 操作, 都是将图书表中对应图书的 status 字段设为 0.
不同的是, 批量删除图书需要根据用户选择的图书, 批量的进行删除, 也就需要编写动态 SQL.
3.9.3 前端代码
function batchDelete() {var isDelete = confirm("确认批量删除?");if (isDelete) {//获取复选框的idvar ids = [];$("input:checkbox[name='selectBook']:checked").each(function () {ids.push($(this).val());});console.log(ids);$.ajax({type: "post",url: "/book/batchDelete?ids=" + ids,success: function(result) {if(result) {location.assign("book_list.html");}else {alert("删除失败!!");}}});}}
3.10 强制登录机制
到目前为止, 图书管理系统的所有功能已经完成了, 但是有一个明显的 bug --- 即使用户没有登录, 也可以通过 book_list.html 路径或者后端接口路径直接访问图书管理系统:
这是一个非常严重的安全问题, 我们需要实现对用户进行强制登录的功能.
3.10.1 后端接口
解决以上安全问题, 就需要借助 Cookie-Session 机制.
3.10.1.1 封装常量
其实我们在校验用户登录接口中, 已经将用户信息存储到了 Session 中:
但是之前存储用户 Session 时, 我们是直接将 key 设置为一个字符串("user"), 这个方法是不友好的, 因为后续接口是也通过 key 来获取 Session 的, 当这个字符串(key)改变时, 那接口中 getAttribute 中的 key 也得做出相应的改变, 因此, 我们可以将这个常量 key(当然不限于此)封装到一个类中, 实现 key 值对代码的解耦:
3.10.1.2 封装响应结果
我们可以借助服务端的 Session 和客户端的 Sessionid, 完成强制登录操作.
当用户后续向后端服务器发送请求时, 请求中都会携带 Sessionid, 那我们就可以在后端接口中, 根据用户的 Sessionid 进行用户校验操作, 即根据 Sessionid 查找对应的 Session, 如果 Session 存在, 那么就说明该用户登录过, 后端就给予正常响应; 否则, 对用户进行强制登录操作.
当无法从 Session 获取到用户信息时(上图第3步), 说明用户未登录, 此时我们应该返回一些错误提示信息, 告诉前端用户还未登录, 应将页面跳转到登录页面, 对用户进行强制登录操作.
当然, 响应结果不是只有用户未登录这一个结果, 当然还有后端接口内部错误, 以及用户已登录并操作成功(如添加图书成功)等等...
因此, 我们可以对返回结果再次进行封装:
-
code: 业务状态码, 不同的值代表不同状态
-
errMsg: 错误信息描述, 告诉前端错误原因是什么
-
data: 真实的业务数据(上文的 ResponseBody)
假设本例(图书系统)中的业务状态码含义如下:
-
code = 200: 结果正常/操作成功
-
code = 0: 用户未登录
-
code = -1: 后端内部错误
因此, 我们可以将 code 使用枚举类进行封装:
此时, 后端接口返回响应时, 返回一个 Result 对象即可.
但是每个接口返回时, 都需要 new 一个 Result 对象, 这样观感上会觉得代码冗余. 因此为了简化代码, 我们可以将不同的响应结果都封装到 Result 类中(不同结果对应一个 Result 方法), 接口返回响应时, 直接调用 Result 中的方法即可, 不需在接口中 new Result 再返回:
这里只展示查询图书列表接口的强制登录代码, 剩下接口的代码就不一一展示了.
到这里, 我们就完成了强制用户登陆的后端代码, 我们可以通过 postman/浏览器 在还未登录的情况下, 访问后端查询图书列表的接口, 观察结果(提示用户未登录):
然后, 我们进行登录操作, 再次访问该后端接口:
登录完毕后, 服务器就创建了 Session 并存储了用户信息, 并通过 set-cookie 向客户端发送了 Sessionid, 用户再次发起请求时, 请求中就会携带 Sessionid, 服务器通过 Sessionid 找到对应的 Session , 就能识别到用户已登录, 就能正常响应结果了.
3.10.2 前端代码
由于我们对后端响应结果进行了封装, 因此前端接收的结果也就发生了改变, 我们需要对前端代码进行简单调整, 并对未登录且越权访问的用户跳转到登录界面实施强转登录操作:
到这里, 图书管理系统大功告成!!
END
相关文章:
Web 小项目: 网页版图书管理系统
目录 最终效果展示 代码 Gitee 地址 1. 引言 2. 留言板 [热身小练习] 2.1 准备工作 - 配置相关 2.2 创建留言表 2.3 创建 Java 类 2.4 定义 Mapper 接口 2.5 controller 2.6 service 3. 图书管理系统 3.1 准备工作 - 配置相关 3.2 创建数据库表 3.2.1 创建用户表…...
1221. 四平方和 -蓝桥杯真题-哈希函数思想
原题链接:1221. 四平方和 - AcWing题库 四平方和定理,又称为拉格朗日定理: 每个正整数都可以表示为至多 44 个正整数的平方和。 如果把 00 包括进去,就正好可以表示为 44 个数的平方和。 比如: 对于一个给定的正整…...
为什么要学习人工智能(AI)?—— 未来已来,AI引领时代变革
未来已来,AI引领时代变革 在这个日新月异的时代,人工智能(AI)正以不可阻挡之势重塑着我们的世界。从教育的深耕细作到科研的突破创新,从行政的效率提升到管理的智慧化转型,AI技术如同一股强大的潮流&#x…...
Markdig:强大的 .NET Markdown 解析器详解
在现代开发中,Markdown 已经成为了一种广泛使用的轻量级标记语言,特别是在文档、博客和内容管理系统中,Markdown 为开发者提供了快速、简洁的格式化文本方式。而在 .NET 生态中,Markdig 是一款非常强大的 Markdown 解析器…...
云计算迁移革命:企业如何摆脱“单一云”锁定,构建自主云未来?
一场价值690亿美元的行业地震 2023年,博通(Broadcom)以690亿美元完成对VMware的收购,这不仅是企业IT历史上的一次天价并购,更在全球云计算市场掀起了一场深远的地震。VMware长期以来是企业数据中心的核心支柱…...
蓝桥杯篇---按键长按与双击
文章目录 前言1. 新增全局变量和宏定义解释1.1宏定义KEY_EVENT_*DEBOUNCE_TIMEHOLD_TIMEDOUBLE_TIMEMULTI_TIME 1.2全局变量Key_ValKey_OldKey_DownKey_Upsys_tickkey_eventkey_pressedkey_press_startkey_last_releaseclick_cnt 2. 定时器初始化(1ms中断࿰…...
created在vue3 script setup中的写法
在 Vue 2 里,created 是一个生命周期钩子函数,会在实例已经创建完成之后被调用,主要用于在实例初始化之后、数据观测和 event/watcher 事件配置之前执行代码。而在 Vue 3 的 <script setup> 语法糖里,不再有像 Vue 2 那样直…...
基于springboot的房屋租赁系统(008)
摘 要 社会的发展和科学技术的进步,互联网技术越来越受欢迎。网络计算机的生活方式逐渐受到广大人民群众的喜爱,也逐渐进入了每个用户的使用。互联网具有便利性,速度快,效率高,成本低等优点。 因此,构建符…...
Linux上的`i2c-tools`工具集的编译构建和安装
源码复制到Ubuntu系统中并解压 的i2c-tools工具集的源码百度网盘下载链接: https://pan.baidu.com/s/1XNuMuT1auT1dMzYo3LAFmw?pwdi6xe 终端进入源码目录 cd /home/book/mybuild/i2c-tools-4.2执行编译构建命令 运行下面的命令进行编译构建 make CC${CROSS_COM…...
java项目之基于ssm的社区流浪动物救助领养系统
项目简介 社区流浪动物救助领养系统实现了以下功能: 本社区流浪动物救助领养系统分为管理员还有用户两个权限,管理员可以管理用户的基本信息内容,可以管理回访信息以及回访的租赁信息,能够与用户进行相互交流等操作,…...
网络空间安全(34)安全防御体系
前言 安全防御体系是一个多层次、多维度的系统,旨在保护组织或个人的信息资产免受各种网络攻击和威胁。 一、技术层面 网络边界防御 防火墙:部署在网络边界,通过设定规则允许或阻止特定流量的进出,保护内部网络不受外部攻击。入侵…...
《Linux 网络架构:基于 TCP 协议的多人聊天系统搭建详解》
一、系统概述 本系统是一个基于 TCP 协议的多人聊天系统,由一个服务器和多个客户端组成。客户端可以连接到服务器,向服务器发送消息,服务器接收到消息后将其转发给其他客户端,实现多人之间的实时聊天。系统使用 C 语言编写&#x…...
知识蒸馏:让大模型“瘦身”的魔法
知识蒸馏:让大模型“瘦身”的魔法 什么是蒸馏模型?AI界的“知识浓缩术”核心定义传统训练 vs 知识蒸馏关键优势 DeepSeek的蒸馏“三步魔法”骨架提取——搭建“迷你版大脑”知识灌注——模仿教师的“思考过程”微调优化——针对场景“查漏补缺” DeepSee…...
MySQL数据库精研之旅第一期:开启数据管理新旅程
专栏:MySQL数据库成长记 个人主页:手握风云 目录 一、数据库简介 1.1. 数据库的概念 1.2. 数据库和数据结构的关系 1.3. 主流数据库 1.3.1. 关系型数据库 1.3.2. 非关系型数据库 1.4. 关系型数据库的概念 二、MySQL配置 2.1. mysqld服务端程序 …...
Linux复习——基础IO,认识文件描述符、软硬件链接
1.复习C文件接口 1.1 fopen FILE *fopen(const char *path, const char *mode); path:带路径的文件名称(待打开的文件) mode: r:以可读方式打开,不可写,文件不存在,则报错 r&…...
【Java集合夜话】第1篇:拨开迷雾,探寻集合框架的精妙设计
欢迎来到Java集合框架系列的第一篇文章!🌹 本系列文章将以通俗易懂的语言,结合实际开发经验,带您深入理解Java集合框架的设计智慧。🌹 若文章中有任何不准确或需要改进的地方,欢迎大家指出,让我…...
Prometheus使用
介绍:Prometheus 是一个开源的 监控与告警系统,主要用于采集和存储时间序列数据(Time Series Data) Prometheus的自定义查询语言PromQL Metric类型 为了能够帮助用户理解和区分这些不同监控指标之间的差异,Prometheu…...
Java学习打卡-Day19-Set、HashSet、LinkedHashSet
Set 接口 无序(添加和取出顺序不一致)(但取出顺序固定)。没有索引。不允许重复,所以最多一个null。遍历方式 迭代器增强for循环不能使用普通for循环索引方式。 HashSet 实现了Set接口,具有相应特征。底…...
冯・诺依曼架构深度解析
一、历史溯源:计算机科学的革命性突破 1.1 前冯・诺依曼时代 在 1940 年代之前,计算机领域呈现 "百家争鸣" 的格局: 哈佛 Mark I(1944):采用分离的指令存储与数据存储ENIAC(1946&a…...
单片机学完开发板,如何继续提升自己的技能?
很多人学完开发板后都会卡在一个尴尬的阶段:觉得自己会的东西不少,但又不知道下一步该干啥。会点C语言,能烧录程序,能点亮LED,玩转按键,搞定串口等等,能用开发板做点小玩意儿,但面对…...
Nginx 日志格式
默认日志格式配置 log_format main $remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for";该格式记录了客户端IP、用户、时间、请求、状态…...
Spring Boot 整合 Elasticsearch 实践:从入门到上手
引言 Elasticsearch 是一个开源的分布式搜索引擎,广泛用于日志分析、搜索引擎、数据分析等场景。本文将带你通过一步步的教程,在 Spring Boot 项目中整合 Elasticsearch,轻松实现数据存储与查询。 1. 创建 Spring Boot 项目 首先ÿ…...
STM32 —— 嵌入式系统、通用计算机系统、物联网三层架构
目录 一、嵌入式系统的概念 二、通用计算机系统与嵌入式系统的比较 用途 硬件 软件 性能与功耗 开发与维护 三、嵌入式系统与物联网的关系 四、物联网的三层架构 1. 感知层(Perception Layer) 2. 网络层(Network Layer) …...
SARAD 解读
出处:NIPS 2024 代码链接:https://github.com/daidahao/SARAD/ 一 文章动机 ① 时间建模(Temporal Modeling)的局限性: a. 时间维度上 感受野极小;b. 变量间时间戳错位 (时间建模、空间建模不统一) →…...
【愚公系列】《高效使用DeepSeek》017-知识点思维导图生成
🌟【技术大咖愚公搬代码:全栈专家的成长之路,你关注的宝藏博主在这里!】🌟 📣开发者圈持续输出高质量干货的"愚公精神"践行者——全网百万开发者都在追更的顶级技术博主! 👉 江湖人称"愚公搬代码",用七年如一日的精神深耕技术领域,以"…...
【linux】scp和rsync
scp 和 rsync 都是 Linux 系统中用于文件传输的命令行工具,它们都可以通过网络在本地和远程主机之间传输文件。 scp 命令 定义 scp 是 “secure copy” 的缩写,它是一个基于 SSH 协议的文件传输工具,用于在本地和远程主机之间安全地复制文…...
软件需求分类、需求获取(高软46)
系列文章目录 软件需求分类,需求获取 文章目录 系列文章目录前言一、软件需求二、获取需求三、真题总结 前言 本节讲明软件需求分类、需求获取的相关知识。 一、软件需求 二、获取需求 三、真题 总结 就是高软笔记,大佬请略过!...
蓝桥杯单片机之AT24C02(基于自己对AT24C02的学习和理解)
一、先用抽象法说明原理,让原理变得简单易懂: 1、向AT24C02写入数据: 有个关系户,他想安排自己的儿子进某个大厦里某个楼层的公司,那么他就要先找到这个公司的地址,然后再找到该公司是第几楼,最…...
【Qt】Qt + Modbus 服务端学习笔记
《Qt Modbus 服务端学习笔记》 1.因为项目的需要,要写一个modbus通信,csdn上感觉有些回答,代码是人工智能生成的,有些细节不对。我这个经过实测,是可以直接用的。 首先要包含Qt 的相关模块 Qt Modbus 模块主要包含以…...
抖音用户视频批量下载工具开发全解析
一、逆向工程原理剖析 1.1 抖音Web端防护体系 抖音采用五层防御机制保护数据接口: graph LRA[浏览器指纹检测] --> B[请求参数签名]B --> C[Cookie动态验证]C --> D[请求频率限制]D --> E[IP信誉评级] 1.2 核心参数解密 参数名称作用原理生成方式有效期x-bogu…...
DeepSeek写打台球手机小游戏
DeepSeek写打台球手机小游戏 提问 根据提的要求,让DeepSeek整理的需求,进行提问,内容如下: 请生成一个包含以下功能的可运行移动端打台球小游戏H5文件: 要求 可以重新开始游戏 可以暂停游戏 有白球和其他颜色的球&am…...
清晰易懂的 Swift 安装与配置教程
初学者也能看懂的 Swift 安装与配置教程 本教程将手把手教你如何在 macOS 系统上安装 Swift,配置依赖包缓存位置,并指出新手容易踩坑的细节。即使你是零基础小白,也能快速上手! 一、安装 Swift(macOS 环境)…...
Post-Training Quantization, PTQ
Post-Training Quantization(PTQ) 是 模型训练完成后,对其参数(权重 & 激活值)进行量化 的方法,目的是 减少存储占用 & 提高推理速度,同时尽可能保持模型精度。 相比于 量化感知训练&a…...
linux Redhat9.5采用DNS主从实现跨网段解析
文章目录 主从服务器DNS实现跨网段解析一、服务器规划二、主服务器配置1、安装bind2、修改主配置文件3、配置区域配置文件4、配置正向解析文件5、配置反向解析文件6、检查并启动服务 三、从服务器配置1、安装bind2、配置主配置文件3、修改区域配置文件4、检查并启动服务 四、路…...
Python个人学习笔记(18):模块(异常处理、traceback、日志记录)
七、异常处理 语法错误不属于异常,处理的是程序运行时的一些意外情况 代码: a int(input(>>>:)) b int(input(>>>:)) print(a / b) # 在运行的时候由于数据不对,导致出错 # 此时程序会中断 prin…...
记一次MyBatis分页莫名其妙的失效,首次执行合适,后续执行分页失效且异常
代码几乎一样,为啥这个xml配置的就会出现莫名其妙的问题呢 org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.type.TypeException: Could not set parameters for mapping: ParameterMapping{propertymybatis_plus_first, modeI…...
Claude是什么?适合哪些场景?
Claude 是由人工智能公司 Anthropic 开发的一款 大型语言模型(LLM),旨在通过自然语言交互帮助用户完成复杂任务。以下是关于 Claude 的核心信息: 1. 核心定位 • 安全可靠: 采用 Constitutional AI(宪法AI…...
基于yolov11的持刀检测系统python源码+pytorch模型+评估指标曲线+精美GUI界面
【算法介绍】 基于YOLOv11的持刀检测系统 随着公共安全问题的日益突出,特别是在公共场所如机场、车站、学校等地,持刀等危险行为频发,对人们的生命财产安全构成严重威胁。传统的监控手段往往依赖于人工观察,但这种方式不仅效率低…...
openEuler24.03 LTS下安装Hive3
目录 前提条件 安装MySQL 卸载原有mysql及mariadb 下载mysql 解压mysql 安装mysql 启动mysql服务 开机自启动mysql服务 登录mysql 修改mysql密码 远程连接mysql 安装Hive 下载安装包 解压 设置环境变量 解决日志包冲突 将mysql驱动拷贝到lib目录 配置Hive 创…...
13-动态规划-最长公共子序列
题目 来源 24. 最长公共子序列 思路 不想打字,援引自最长公共子序列 (LCS) 详解例题模板(全)-CSDN博客 图示举例: 其余详见代码 代码 #include<bits/stdc.h> using namespace std; const int N110; int f[N][N]; int m…...
golang 生成单元测试报告
在 Go 语言中,你可以使用 go test 生成单元测试报告。以下是几种方法: 1. 生成基本测试报告(文本格式) go test -v ./... > test_report.txt-v:显示详细的测试信息./...:递归测试所有子目录> test_r…...
让AI看见世界:MCP协议与服务器的工作原理
让AI看见世界:MCP协议与服务器的工作原理 MCP(Model Context Protocol)是一种创新的通信协议,旨在让大型语言模型能够安全、高效地与外部资源进行交互。在AI技术快速发展的今天,MCP正成为连接AI与现实世界的重要桥梁。…...
设备健康管理系统是什么,设备健康管理系统多少钱?
想象一下,你的汽车在仪表盘报警前 3 天,手机就收到 “发动机轴承剩余寿命 1500 公里” 的提醒 —— 这就是 ** 设备健康管理系统(EHM)** 的日常。在制造业,设备故障每年造成全球 3.4 万亿美元损失,而 80% 的…...
设计模式(创建型)-抽象工厂模式
摘要 在软件开发的复杂世界中,设计模式作为解决常见问题的最佳实践方案,一直扮演着至关重要的角色。抽象工厂模式,作为一种强大的创建型设计模式,在处理创建一系列或相关依赖对象的场景时,展现出了独特的优势和灵活性。它通过提供一个创建对象的接口,让开发者能够在不指定…...
docker 部署elk 设置账号密码
1. 先把 kibana 停掉 2.进入es 容器 docker exec -it 75895a078cbc /bin/bash 找到 bin 目录 执行 ./elasticsearch-setup-passwords interactive 全部设置一样的密码 ,不一样自己要记住,设置成功会输出如下内容 Changed password for user [apm_system] Chang…...
<table>内有两行<tr>,第一行设定高度为60,剩余第二行,和右侧元素高度补齐。
实现 <table> 内第一行高度设定为 60px,第二行和右侧元素高度补齐的效果,你可以通过 CSS 样式来控制。示例: 为第一行 <tr> 设置固定高度 60px。对于右侧元素,假设它是一个 <div> 或者其他容器,将其…...
QT5.15.2加载pdf为QGraphicsScene的背景
5.15.2使用pdf 必须要安装QT源码,可以看到编译器lib目录已经有pdf相关的lib文件,d是debug 1.找到源码目录:D:\soft\QT\5.15.2\Src\qtwebengine\include 复制这两个文件夹到编译器的包含目录中:D:\soft\QT\5.15.2\msvc2019_64\include 2.找…...
常见的工具和技术
Mockito Mockito 是一个流行的 Java Mocking 框架,用于创建和配置模拟对象(Mock Objects),以便在单元测试中模拟复杂依赖关系的行为 使用场景:单元测试、隔离测试 EclipseEclipse 是一个开源的集成开发环境(…...
Linux怎样源码安装Nginx
1. 安装必要的依赖 在编译 Nginx 之前,你需要安装一些必要的依赖包,像编译工具和库文件等。以 CentOS 系统为例,可借助yum命令来安装: bash sudo yum install -y gcc pcre-devel zlib-devel openssl-devel要是使用的是 Ubuntu 系…...
汇编指令(20250319)
SOC常用总线 AHB(Advanced High-performance Bus):先进高性能总线,连接RAM,ROM等高速设备APB(Advanced Peripheral Bus):先进外设总线,连接外设等一些低速设备 CISC和R…...