ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot图书借阅管理系统:从项目结构到核心代码实战解析

SpringBoot图书借阅管理系统:从项目结构到核心代码实战解析 简介基于Spring Boot与MySQL实现的一套图书借阅管理系统毕业设计项目适合高校计算机相关专业学生用于毕业设计、课程设计或期末大作业也适合初学Java Web的开发者参考完整项目结构。压缩包共122个文件包含43个Java后端源码、15个HTML页面、19个JavaScript脚本与6个CSS样式文件以及SQL数据库脚本、MyBatis映射XML、Maven配置等整体仅547KB轻量完整、开箱即用。系统具备图书管理、借阅管理等核心业务模块代码分层清晰前端通过jQuery与jQuery Validate实现交互和表单校验后端基于Spring Boot整合MyBatis清晰演示前后端数据交互与SSM开发流程。SQL脚本可直接完成数据库初始化MyBatis映射文件展示了数据访问层的典型写法便于快速导入运行和二次开发。目前已有229人学习下载项目经导师指导获高分评价下载后无需修改即可运行是快速落地毕业设计的实用参考。1. 图书借阅管理系统SpringBoot 项目拿到手先从哪里看毕业设计季最常收到的消息是“帮我看看这个图书借阅管理系统能不能跑”而实际把 zip 解压后大部分人卡在第一步目录里没有 idea 工程文件只有一堆 mvnw.cmd、jquery-ui 和 html 页面看起来不像后端项目。这套基于 SpringBoot MySQL 的图书借阅管理系统源码里前端走的是前后端分离的模板方案jquery-ui 管弹窗和日期jquery-validate 管表单校验后端由 SpringBoot 提供 REST 接口MyBatis 做数据访问。它适合两类人一是需要交毕设和课程设计的学生希望下载后改改配置就能演示二是刚接触 SpringBoot 的开发者想借一个完整业务链路理解控制器、服务、Mapper 三层怎么协作。项目功能覆盖图书分类维护、图书上架、借阅与归还、读者管理页面数量不多但后台结构是完整可复现的值得拆开看。2. 目录结构与启动链路mvnw.cmd 到 MySQL 连接细节2.1 先搞懂 zip 里这几类文件各自承担什么职责解压后的文件分三组。第一组是 mvnw.cmd、mvnw、.mvn 目录这是 Maven Wrapper作用是不依赖本机全局安装的 Maven直接用项目内置版本执行构建对换电脑演示很有用。第二组是静态资源jquery-ui.min.css、jquery-ui.structure.min.css、jquery-ui.theme.min.css、myUIstyle.css、style.css、updateUser.css这些决定页面控件样式jquery-ui 三个 css 分别对应核心样式、结构样式和主题样式修改主题只需替换第三个文件。第三组是页面文件showBooks.html 负责图书列表展示与条件检索addCategory.html 负责分类新增这两个页面恰好覆盖了借阅管理系统里最典型的“列表查询”和“表单提交”场景。├── mvnw.cmd ├── mvnw ├── .mvn/ ├── src/main/java/com/example/library/ │ ├── controller/ # 控制器层 │ ├── service/ # 业务逻辑层 │ ├── mapper/ # MyBatis Mapper 接口 │ └── model/ # 实体类 ├── src/main/resources/ │ ├── mapper/ # MyBatis XML 文件 │ ├── application.yml # 数据源、端口等配置 │ └── static/ │ ├── jquery-ui.min.css │ ├── style.css │ ├── showBooks.html │ └── addCategory.html └── pom.xmlMaven Wrapper 的执行逻辑很简单mvnw.cmd 先检查本地是否配置了 maven 仓库和 JDK然后把依赖下载到用户目录下的 .m2 文件夹。Windows 上直接执行mvnw.cmd spring-boot:run就能启动Linux 或 Mac 用./mvnw spring-boot:run。如果你本机已经有 Maven 3.6 以上版本也可以直接用mvn spring-boot:run效果一致。注意spring-boot:run是 Spring Boot 专有插件目标不代表 Maven 全局配置所以推荐直接用 Wrapper减少环境问题。2.2 application.yml 里必须改的三个数据源参数项目默认连的是本地 MySQLyml 文件里spring.datasource.url那一行是启动时最先报错的地方。常见错误是Access denied for user或Unknown database前者是账号密码不对后者是库名没建。下面是一份可直接替换的最小配置我一般会保留其中的连接池参数不要删掉。server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/library?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.library.model参数说明useUnicodetruecharacterEncodingutf8控制中文写入不乱码MySQL 8 以上还必须带serverTimezoneAsia/Shanghai否则 JDBC 驱动会报时区错误。useSSLfalse是本地开发关掉安全连接减少握手开销。driver-class-name在 MySQL 8.x 下必须是com.mysql.cj.jdbc.Driver老项目里写com.mysql.jdbc.Driver会提示 deprecated虽然能跑但不建议。MyBatis 的mapper-locations指向 resources/mapper 下的 XML如果你把 SQL 写在注解里这段可以省略。2.3 数据库导入先建库再执行 sql 文件项目压缩包里通常带一个 .sql 文件这是完整备份包含建库、建表和初始数据。用命令行导入时有个先后顺序坑必须先登录 MySQL 创建空库再指定库执行 SQL否则文件里的CREATE DATABASE语句如果存在且你的账号没有建库权限会直接中断。mysql -uroot -p CREATE DATABASE library DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; exit; mysql -uroot -p library /path/to/library.sql日常导入导出的参数含义-uroot和-p表示用户名和密码密码可紧跟-p123456但裸奔在终端会暴露我习惯-p后回车再输入。第二条命令中library放在前面表示目标库如果漏写mysql 会提示未选择数据库。导入完成后检查一下book_info、borrow_record、category三张表是否有行数常见问题是 sql 文件编码与库不一致导致中文变成???此时检查文件头部有没有SET NAMES utf8没有就在导入前执行SET NAMES utf8。配置项常见取值出错表现port8080 / 8081端口占用BindExceptionurl 时区Asia/Shanghai 或 UTC日期时间差 8 小时driver-classcom.mysql.cj.jdbc.DriverNo suitable drivermybatis mapperclasspath:mapper/*.xmlInvalid bound statement3. 图书分类管理addCategory 背后的控制器与 Mapper 一整套链路3.1 分类表设计为什么单列 category_name 不够用图书分类在借阅系统里属于基础数据表结构设计很能体现项目完成度。低级做法是只存一个名字字段高级做法是加上父分类 ID、排序号和状态位。我在这类毕业设计里最常看到的是三字段版本够用但不抗扩展。下面是一份更合理的建表语句兼容原项目的 Mapper 映射只是多两个辅助字段不影响原有页面展示。CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, category_name VARCHAR(50) NOT NULL, category_code VARCHAR(20) DEFAULT NULL, parent_id INT DEFAULT 0, sort_order INT DEFAULT 0, status TINYINT DEFAULT 1 ) ENGINEInnoDB DEFAULT CHARSETutf8;parent_id默认 0 表示一级分类做两级分类时子分类的parent_id指向父分类的id。sort_order控制前端展示顺序addCategory.html 表单里通常有一个“排序”输入框对应字段就是它。status为 1 显示、0 隐藏做软删除时用这个字段代替物理删除这样借阅记录里关联的历史分类不会因为删除而丢失。实际原项目可能只有id、category_name这不影响你理解——它的核心只是下拉框取值。3.2 Controller 接收 POST 参数时容易忽略的类型转换问题addCategory.html 是一个典型的 jQuery AJAX 提交页。表单里包含分类名称、排序值提交时序列化成 JSON 发给后端。Controller 的写法在 SpringBoot 项目里几乎固定需要注意RequestBody与RequestParam的使用差异。RestController RequestMapping(/api/category) public class CategoryController { Autowired private CategoryService categoryService; PostMapping(/add) public Result add(RequestBody Category category) { if (category.getCategoryName() null || category.getCategoryName().trim().isEmpty()) { return Result.error(分类名称不能为空); } int row categoryService.insertCategory(category); return row 0 ? Result.success() : Result.error(新增失败); } }RequestBody要求前端发送Content-Type: application/json如果是表单提交application/x-www-form-urlencoded后端就要改成RequestParam或直接用一个 POJO 接收不加注解具体取决于前端发送格式。这段代码里的核心逻辑是空值校验放在了 Controller 层属于最基础的防线更规范的做法是加Valid注解配合实体类上的NotBlank但原项目通常没引入额外的校验依赖手写判断是更稳妥的兼容方式。Result是统一返回体包含 code、msg、data 三个字段前端通过data.code判断是否成功。3.3 MyBatis 的 XML 映射文件里最容易写错的标签顺序Service 层调用 Mapper 接口真正执行 SQL 发生在 XML 文件里。图书管理系统的 Mapper XML 一般放在resources/mapper/CategoryMapper.xml下面这段是插入语句的标准写法。insert idinsertCategory parameterTypecom.example.library.model.Category useGeneratedKeystrue keyPropertyid INSERT INTO category (category_name, category_code, parent_id, sort_order, status) VALUES (#{categoryName}, #{categoryCode}, #{parentId}, #{sortOrder}, #{status}) /insertuseGeneratedKeystrue配合keyPropertyid的作用是数据库自增主键生成后MyBatis 会把生成的值回填到传入对象的id属性上这样后续如果再插入关联表比如图书表可以直接category.getId()取到值。parameterType可以省略因为接口方法上已经有Param或泛型推断。最容易出的问题是 XML 里if标签包裹字段时where条件后面多一个AND导致拼接出WHERE AND的 SQL 语法错误解决办法是where标签会自动去掉首尾多余的AND或OR。3.4 前端下拉框联动页面加载即请求分类接口addCategory.html 这个页面除了新增很多时候还要负责在编辑弹窗里回显分类树或二级联动。常见实现是页面加载完成后发一次 GET 请求拿全量分类列表再渲染到select中。$(document).ready(function () { $.ajax({ url: /api/category/listAll, type: GET, dataType: json, success: function (res) { if (res.code 200) { var options option value请选择父分类/option; res.data.forEach(function (item) { options option value item.id item.categoryName /option; }); $(#parentId).html(options); } } }); });代码逻辑document.ready保证 DOM 结构加载完成后执行$.ajax的dataType: json告诉 jQuery 把响应体解析成 JSON 对象res.data是Result.success()里塞进的数据列表。forEach遍历后拼出option串一次性赋值给#parentId下拉框。如果你发现页面里下拉框出不来优先在浏览器开发者工具 Network 面板查看/api/category/listAll的响应状态码404 通常是 Controller 路径写错500 则是 SQL 查不到表用curl http://localhost:8080/api/category/listAll可以绕过页面快速复现。4. 图书借阅与归还核心业务流程的状态设计和事务边界4.1 借阅记录表设计一次借阅行为需要落哪些字段图书借阅管理系统的核心表是借阅记录表它把用户、图书、时间三个维度串起来。字段设计直接影响归还、续借、超期计算等功能的实现难度。很多毕设把这几张表合并成一张“借阅表”导致续借时无法记录历史我建议按下面四张表的思路核对原项目。表名关键字段职责userid, username, password, student_no读者与管理员的公共账号表book_infoid, book_name, isbn, category_id, stock图书基本信息与库存borrow_recordid, user_id, book_id, borrow_time, due_time, return_time, status每一笔借阅行为penaltyid, record_id, days, amount, status超期罚金记录borrow_record的status是关键0 表示借出中1 表示已归还2 表示已续借过3 表示超期未还。也可以用单独字段renew_count记录续借次数但原项目通常没有这个字段用 status 区分是更贴合现有结构的做法。due_time用于计算是否超期return_time为空表示尚未归还。4.2 借书接口事务注解要加对位置库存扣减才有意义借书操作涉及两步写操作往borrow_record插入一行同时book_info表的stock减一。两步要么同时成功要么同时失败否则会出现记录说借了但库存没减的情况。SpringBoot 里用Transactional解决这个注解要加在 service 层的公开方法上而不是 controller 方法上——加在 controller 上虽然能生效但事务切面属于 service 层职责且 controller 做事务会扩大锁的生命周期。Service public class BorrowService { Autowired private BorrowRecordMapper borrowRecordMapper; Autowired private BookInfoMapper bookInfoMapper; Transactional(rollbackFor Exception.class) public Result borrowBook(Integer userId, Integer bookId) { BookInfo book bookInfoMapper.selectById(bookId); if (book null) { return Result.error(图书不存在); } if (book.getStock() 0) { return Result.error(库存不足); } BorrowRecord record new BorrowRecord(); record.setUserId(userId); record.setBookId(bookId); record.setBorrowTime(new Date()); record.setDueTime(DateUtils.addDays(new Date(), 30)); record.setStatus(0); borrowRecordMapper.insert(record); bookInfoMapper.decreaseStock(bookId); return Result.success(); } }rollbackFor Exception.class的含义是任何异常包括NullPointerException这类运行时异常都触发回滚。如果不写rollbackFor默认只在RuntimeException上回滚检查异常如IOException不会中断事务这是很多事务不生效的根因。decreaseStock的 SQL 建议写成UPDATE book_info SET stock stock - 1 WHERE id #{id} AND stock 0这样在并发场景下如果库存已被抢到 0更新行数为 0可以去代码里判断row 0返回“库存不足”比先查后更新更安全。4.3 归还接口判断超期天数状态流转要可追溯归还操作是借书逆流程要同时更新记录状态和加回库存。超期天数按自然日计算数据库里due_time存的是datetime计算时用return_time减去due_time即可。Transactional(rollbackFor Exception.class) public Result returnBook(Integer recordId) { BorrowRecord record borrowRecordMapper.selectById(recordId); if (record null || record.getStatus() ! 0) { return Result.error(借阅记录不存在或已归还); } Date now new Date(); record.setReturnTime(now); record.setStatus(1); borrowRecordMapper.updateStatus(record); long diff now.getTime() - record.getDueTime().getTime(); long days TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS); if (days 0) { Penalty penalty new Penalty(); penalty.setRecordId(recordId); penalty.setDays((int) days); penalty.setAmount(days * 0.5); penalty.setStatus(0); penaltyMapper.insert(penalty); } bookInfoMapper.increaseStock(record.getBookId()); return Result.success(); }TimeUnit.DAYS.convert是 Java 里计算两个毫秒数之间天数差的常用方式参数第一个是要转换的数值第二个是原始单位。注意这里传入的是diff单位是毫秒所以第二个参数是MILLISECONDS。罚金金额我按每天 0.5 元算你可以替换成项目里的实际费率。这段业务里最关键的校验是record.getStatus() ! 0它的作用是防止重复归还同一本书如果已经归还再点一次还书不会给库存累加两次。原项目如果没写这个判断你去演示时连续点两次还书库存就会虚高这是答辩时最容易被老师问出的 bug。4.4 列表查询里最容易被问到的 SQL联表后分页图书借阅管理的列表页通常显示借阅人姓名、书名、借书时间这些字段分布在三张表联表 SQL 是躲不掉的。很多人的分页参数在联表后总数不对根源是用了LIMIT分页但忘记统计总数。SELECT r.id, u.username, b.book_name, r.borrow_time, r.due_time, r.return_time, r.status FROM borrow_record r LEFT JOIN user u ON r.user_id u.id LEFT JOIN book_info b ON r.book_id b.id WHERE r.status #{status} ORDER BY r.borrow_time DESC LIMIT #{offset}, #{pageSize}LEFT JOIN保证即使借阅记录里user_id或book_id有外键悬空记录本身也不会被过滤掉。实际业务里常见的是一个页面同时显示多条记录且每页 10 条offset的计算方式是(pageNum - 1) * pageSize比如第 3 页 10 条就是(3-1)*10 20条开始取。如果想同时返回总数需要再执行一条SELECT COUNT(1) FROM borrow_record WHERE status #{status}如果原项目用的是 PageHelper 分页插件则不用手写 count它会自动拦截改写 SQL但前提是 pom.xml 里引入了pagehelper-spring-boot-starter且版本匹配 SpringBoot 2.x。如果你在查询结果里发现总页数对不上优先检查总数 SQL 里是否少了和查询条件一致的WHERE status 限制。5. jQuery Validate 表单校验图书信息维护页的完整配置方案5.1 为什么这个项目选 jQuery Validate 而不是 Element UI图书借阅管理系统这种以 html jQuery 为主的前端表单校验最适合的方案就是 jQuery Validate 插件。它的优势不是功能多而是零依赖、体积小、按名字匹配表单字段不需要额外构建工具。项目里 jquery-ui 和 jquery-validate 同时出现说明页面既要弹窗交互又要做输入校验jquery-validate 负责的是 addCategory.html、addBook.html 这类新增页面中的必填项、长度、格式检查而 jquery-ui 的dialog组件负责图书详情弹窗。两者搭配是前后端未分离时代的经典组合放在毕设里完全够用老师也不会问为什么不用 Vue。5.2 校验规则的三种写法写在 HTML 属性里还是 JS 里jQuery Validate 有一个特性容易被忽略它支持先定义好rules对象再绑定到表单的id上而不是把规则散落在每个input标签里。集中式管理在字段数量增多后可维护性更好。$(#bookForm).validate({ rules: { bookName: { required: true, minlength: 2, maxlength: 50 }, isbn: { required: true, isbnFormat: true }, stock: { required: true, digits: true, min: 0 }, categoryId: { required: true } }, messages: { bookName: { required: 请输入书名, minlength: 书名至少 2 个字符, maxlength: 书名不能超过 50 个字符 } }, submitHandler: function (form) { var formData $(form).serialize(); $.post(/api/book/add, formData, function (res) { if (res.code 200) { alert(添加成功); window.location.href showBooks.html; } else { alert(res.msg); } }, json); } });required对应非空校验minlength和maxlength限制字符长度digits限定只能输入非负整数min设置最小值。submitHandler是表单验证通过后的回调在这里发起 AJAX 提交。$(form).serialize()会把表单内的 input、select 全部序列化成a1b2的查询字符串格式后端用RequestParam或 POJO 接收时不需要手动解码。isbnFormat这个自定义校验方式是在 validate 初始化前通过$.validator.addMethod定义的用于校验 ISBN 是否为 10 位或 13 位数字加横线格式原项目可能不认识这个规则代码里过不去的话删掉这一行即可。5.3 日期选择与校验的配合jquery-ui 的 datepicker 触发验证时机借阅和归还都要选日期jquery-ui 的datepicker是常见搭配。问题在于 datepicker 改变值后不会自动触发 jQuery Validate 的重新验证导致用户选择了日期仍提示“请输入日期”。解决办法是在 datepicker 的onSelect回调里手动触发校验。$(#borrowDate).datepicker({ dateFormat: yy-mm-dd, changeMonth: true, changeYear: true, onSelect: function () { $(this).valid(); } });dateFormat: yy-mm-dd是 jquery-ui 格式注意这里的yy代表四位年份YYYY是 Java 的 SimpleDateFormat 格式两者写法不同写错会出现年份多一位或少一位。changeMonth和changeYear为 true 时日期控件顶部会出现月份和年份下拉框方便快速跳转。$(this).valid()的作用是只触发当前元素项的校验相当于element.form.validate().element(element)的简写。实际开发中我遇到过一个问题datepicker 初始化了但点击没有弹出日历原因是 HTML 里 input 的id与初始化器里的选择器不一致或者 jquery-ui.js 文件在 jquery.js 之前引入导致$(#borrowDate).datepicker is not a function。控制台报这个错时先检查 js 引入顺序jquery.js 必须在最前面然后是 jquery-ui.js、jquery.validate.js。5.4 回显老数据时的校验处理编辑页面要绕过 required图书信息和分类的编辑场景中页面上会先显示已有数据此时如果仍用 required 规则用户不修改也会校验通过这没问题。但有一种情况编辑页回显时日期字段格式是yyyy-MM-dd HH:mm:ss而 datepicker 的yy-mm-dd只有日期如果回填的值带了时间jquery-ui 的 datepicker 会认为字符串不是有效日期导致不渲染。常见做法是回显时先截断时间部分。var rowDate data.borrowTime ? data.borrowTime.substring(0, 10) : ; $(#borrowDate).val(rowDate);substring(0, 10)从2025-06-15 14:30:00截取出2025-06-15。这里有个隐患如果data.borrowTime是 null 或 undefined直接调用substring会抛 TypeError所以前面要先做三元判断。实际项目中我还见过后端返回的日期格式是时间戳数字前端需要先把时间戳new Date(timestamp)转换成 Date 对象再用toISOString().substring(0, 10)取日期部分但这依赖时区可能出现日期少一天的坑。6. 答辩前自查启动失败、数据中文乱码、页面样式丢失的排查优先级6.1 端口占用与 MySQL 连接失败的快速判断命令答辩演示时最怕启动瞬间报错。如果控制台出现APPLICATION FAILED TO START第一眼看端口是否被占用第二眼看数据源配置。Windows 下排查命令是netstat -ano | findstr :8080能看到占用进程的 PID再用tasklist | findstr PID看是哪个程序占用的最粗暴的解决是换server.port。Linux/Mac 用lsof -i :8080最后一列就是 PIDkill -9 PID可以强制释放。MySQL 连接类错误里Access denied for user rootlocalhost (using password: YES)是最常见的几乎可以断定是 application.yml 里的password写错或没改。解决时先用命令行确认库和账号是否正常再对比 yml。另一个高频问题是Unknown database library说明 SQL 脚本没有成功创建库重新执行一遍建库语句和导入语句注意导入前先USE library检查库存在不要直接导入后看都没看就启动。6.2 中文乱码排查三步走别急着去改数据库编码数据库里显示中文正常但页面乱码和页面显示乱码但数据库没问题是两类不同的问题。排查顺序是先看页面响应头有没有charsetUTF-8再看数据库连接串有没有characterEncodingutf8最后看表结构编码。curl -I http://localhost:8080/showBooks.html如果响应头里Content-Type: text/html后面没有 charset就需要在 SpringBoot 的静态资源配置里强制指定编码或者在 HTML 文件的head里写meta charsetutf-8。如果数据库表是 utf8 但数据本身是乱码大概率是 SQL 文件里没带SET NAMES utf8重新用mysql --default-character-setutf8 -uroot -p library library.sql导入一遍。注意--default-character-set要放在-u前面顺序错了 mysql 会忽略这个参数。6.3 样式不加载的隐藏原因静态资源路径带 out 前缀showBooks.html 里引用了../css/style.css这种相对路径如果页面能打开但 CSS 完全没有加载打开浏览器 F12 看 Network 里 css 文件的状态码。404 说明路径不对403 说明 Spring Security 拦截了静态资源访问——如果原项目里引入了安全框架必须在配置类里放行/css/**、/js/**、/images/**和所有.html页面。如果是 200 但样式错乱检查 html 文件的link标签有没有写relstylesheet漏掉这个属性浏览器不会加载样式表。还有一类情况是项目里多个 CSS 文件同时生效jquery-ui 的ui-state-default会覆盖 backgroundColor导致按钮颜色异常此时在 style.css 里对同一个选择器加!important是最快的兜底方案但正确做法是检查 HTML 里引入 css 的先后顺序后引入的样式优先级高于先引入的。6.4 演示前的一页纸三条 curl 验证核心接口可通答辩现场时间紧张与其点页面点错位置不如提前用 curl 验证接口这几个命令可以证明后端是通的。curl -X POST http://localhost:8080/api/category/add \ -H Content-Type: application/json \ -d {categoryName:计算机科学,sortOrder:1} curl -X GET http://localhost:8080/api/book/listAll?pageNum1pageSize5 curl -X POST http://localhost:8080/api/borrow/borrowBook \ -H Content-Type: application/json \ -d {userId:1,bookId:3}第一条命令验证新增分类接口预期响应体里code为 200。第二条验证分页查询注意pageNum和pageSize必须能匹配 Mapper 里的offset计算逻辑如果查询报错说参数类型不匹配检查RequestParam里的 defaultValue 是否配置。第三条验证借书接口返回库存不足时先去看 book_info 表里 bookId 为 3 的书是否存在、stock 是否为 0。这三条命令的顺序也对应了数据依赖先建分类再查图书最后借书一条链路走通了答辩时基本不会被后端问题难住。现场如果数据库是临时恢复的记得先执行 SQL 脚本里的初始化数据否则 user 表和 book 表是空的接口返回的全是“不存在”类错误。本文还有配套的精品资源点击获取
返回列表