Spring Boot图书管理系统实战:从建表到答辩全流程
简介面向Java开发学习者这份基于Spring Boot的图书管理系统完整项目代码适用于课程设计、毕业设计及企业级开发入门。系统采用Spring BootVueMyBatisPlusMySQL技术栈涵盖用户信息管理、图片素材管理、视频素材管理、公告信息管理等模块完整演示了从数据库设计到前后端联调的项目流程。压缩包共743个文件整体约19.72MB除java源码、vue组件、js脚本、css样式、svg图标外还包含数据库脚本、maven配置和install/run等启动批处理便于本地快速部署运行。项目结构按后端、前端、数据库、部署脚本分层便于按需查阅并附有说明文档辅助理解设计思路。目前已有104人学习下载可作为理解B/S架构、RESTful接口开发以及ElementUI界面集成的参考范本帮助读者快速掌握图书管理场景下的业务逻辑与代码组织方式。1. 图书管理系统不是玩具项目Spring Boot 毕设为什么值得认真做图书馆、课程设计、毕业设计这三个词放一起很多人第一反应是又是一个老掉牙的 CRUD。但基于 Springboot 的图书管理系统恰恰是 Java 后端入门到进阶之间最稳的跳板需求边界清楚又有库存、借阅状态、超期计算这些天然的业务规则比纯增删改查能多聊出三层东西。同样是做毕设有人交上去的是表加查询有人交的是一个能讲清楚状态流转和事务边界的系统分数差距就是这么拉开的。这篇文章适合两类人一类是拿它当 java 课程设计案例源码来参考的学生想从零跑通一个完整项目再往里加自己的东西另一类是已经会写 Spring Boot CRUD、但想搞明白借阅状态机、分页插件配置、事务失效这些看起来会、一查全是坑的细节的人。我会把建表、实体映射、借还书事务、超期罚金、分页配置到答辩前收尾的完整路径走一遍所有代码照着抄就能在本地起服务。后面每一章解决一个具体问题中途踩的坑我也会用真实案例讲清楚。2. 先立骨架为什么选 Spring Boot MyBatis-Plus以及工程怎么摆2.1 技术栈选型的理由Spring Boot 3.x/2.x MyBatis-Plus 为什么是主流答案图书管理系统在 GitHub 和各类毕设仓库里出现频率最高的组合是 Spring Boot MyBatis-Plus MySQL。早些年的教材还在教 SSMSpring Spring MVC MyBatis配置 XML 文件能写一屏。Spring Boot 把自动配置做掉之后你只需要管业务代码和少量自定义配置这对一个要在一学期里同时应付论文、答辩和找工作的学生来说省下来的时间非常可观。MyBatis-Plus 存在的理由是消灭单表 CRUD 的样板代码。图书、读者、借阅记录这三张核心表增删改查全是套路MyBatis-Plus 的 BaseMapper 直接帮你把方法生成了你只需要写那些真正有业务规则的 SQL比如扣库存查超期记录。它的分页插件也是现成的配合 Page 对象一行就能搞定分页查询这也是它比原生 MyBatis 更合适做课设的原因——不是炫技是省时间。对比一下几套老方案技术栈配置成本CRUD 效率业务代码自由度答辩时能讲的东西SSHStruts2 Spring Hibernate高XML 多中受框架约束多基本只剩历史包袱SSM中要手动整合中较高讲整合过程但内容偏老Spring Boot MyBatis-Plus低自动配置高高能专注讲事务、状态、性能优化我的建议是直接用 Spring Boot 2.x 或 3.x 都行配合 mysql-connector-j 8.x 驱动和 MySQL 8.0 数据库。如果你所在学校的实验环境还在用 MySQL 5.7代码层面基本不用改只有连接 URL 里的时区参数要注意这个我在第五章会专门讲。2.2 用 IDEA 新建 Spring Boot 项目的三步走避开初始化器的两个坑常见做法是打开 IDEA 的 Spring Initializr 直接生成工程但有两个坑。第一个坑是 Initializr 默认选择的 Spring Boot 版本可能很新和本机 JDK 版本不匹配启动直接报 UnsupportedClassVersionError第二个坑是很多人勾选了 Web 依赖就忘掉数据库驱动后面连数据库时才发现少了 jar。我一般会先确认本机 JDK 版本再决定选哪个 Spring Boot 版本JDK 8 就选 2.xJDK 17 以上可以选 3.x。项目生成之后建议把 pom.xml 里依赖精简一下删掉用不到的。最小依赖集合长这样dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这段配置里没有写具体版本号是因为 Spring Boot 的父 POM 已经统一管理了版本写版本反而可能引发冲突。Lombok 是为了省掉 getter/setter如果你们学院不允许用 Lombok就去掉这个依赖自己生成方法。MyBatis-Plus 的 starter 会自动引入 MyBatis 和 Spring JDBC不需要再额外加 spring-boot-starter-jdbc。生成完项目第一件事应该是先把 application.yml 配好。下面这个配置是最小可用版本server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/library_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto注意 driver-class-name 是 com.mysql.cj.jdbc.Driver不是老项目里的 com.mysql.jdbc.Driver8.x 驱动里后者已经废弃。url 里必须带 serverTimezoneAsia/Shanghai否则连接 MySQL 8.0 会直接报 CST 时区错误。log-impl 打开是为了开发时能直接在控制台看到 MyBatis 执行的 SQL答辩前记得关掉不然演示时满屏日志很掉价。2.3 包结构别乱堆按实体-映射-服务-控制四层摆很多翻车项目的问题不是代码写不出来而是所有类都堆在 controller 里一个 Controller 文件三四百行。图书管理系统的业务虽然不复杂但借阅、归还、续借、罚款这些规则交织在一起不分层后期改需求会让你想骂人。我会按下面这个结构组织com.example.library ├── LibraryApplication.java ├── controller │ ├── BookController.java │ ├── ReaderController.java │ └── BorrowController.java ├── service │ ├── BookService.java │ ├── BorrowService.java │ └── impl │ ├── BookServiceImpl.java │ └── BorrowServiceImpl.java ├── mapper │ ├── BookMapper.java │ ├── ReaderMapper.java │ └── BorrowRecordMapper.java ├── entity │ ├── Book.java │ ├── Reader.java │ └── BorrowRecord.java ├── config │ ├── MybatisPlusConfig.java │ └── CorsConfig.java └── common ├── Result.java └── BusinessException.javaController 层只做参数接收和结果包装业务判断全部放到 Service 层。mapper 层只放数据库交互不要在 mapper 里写 if else。common 包里放统一返回体 Result 和自定义异常 BusinessException后面借书库存不足、读者不存在这些情况都要靠它返回前端友好提示。这个结构看起来简单但它决定了你答辩时能不能从容地说出我的项目分层清晰。3. 把表建对后面能少改 80% 的代码3.1 五张核心表的字段设计从图书、读者到借阅记录图书管理系统的表结构我见过至少三种版本。最省事的是三张表图书、读者、借阅记录但一旦要加分类统计就得改表最臃肿的是七八张表加了权限、操作日志、预约结果自己都理不清关系。我的建议是五张表图书分类表、图书表、读者表、借阅记录表、管理员表。再多就不适合课设体量再少有些业务没法完整覆盖。图书表和分类表是主从关系借阅记录表同时关联图书和读者这是整个系统的枢纽。下面这张表是借阅记录表的核心字段设计字段名类型说明idBIGINT主键自增reader_idBIGINT读者 ID外键关联读者表book_idBIGINT图书 ID外键关联图书表borrow_timeDATETIME借出时间due_timeDATETIME应还时间借出时自动计算return_timeDATETIME实际归还时间空表示未还statusTINYINT0 在借1 已还2 超期未还fineDECIMAL(10,2)超期罚金未超期为 03.2 建表 SQL为什么借阅记录要单独建一个表直接把借阅信息塞在图书表里是最常见的错误设计比如在图书表加一个借阅人字段。这样做图书一次性只能被一个人借而且历史记录全丢了。正确的做法是把借阅行为抽成独立的记录表图书表只保留库存字段每次借还都往借阅记录表插一行这样同一本书的不同借阅历史都能追溯。下面这份 SQL 是整套系统的地基我加了注释照着执行即可CREATE TABLE t_category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分类名, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_book ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 书名, author VARCHAR(50) COMMENT 作者, isbn VARCHAR(20) COMMENT ISBN 号, category_id BIGINT COMMENT 分类 ID, stock INT NOT NULL DEFAULT 0 COMMENT 库存数量, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_category (category_id) ); CREATE TABLE t_reader ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 读者姓名, phone VARCHAR(20) COMMENT 手机号, max_borrow INT DEFAULT 5 COMMENT 最大可借数量, status TINYINT DEFAULT 1 COMMENT 1 正常0 停用, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_borrow_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, reader_id BIGINT NOT NULL, book_id BIGINT NOT NULL, borrow_time DATETIME NOT NULL, due_time DATETIME NOT NULL, return_time DATETIME, status TINYINT DEFAULT 0 COMMENT 0 在借1 已还, fine DECIMAL(10,2) DEFAULT 0.00, KEY idx_reader (reader_id), KEY idx_book (book_id) ); CREATE TABLE t_admin ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );建表时有几个细节值得注意。所有表名都加了 t_ 前缀避免和 MySQL 保留字冲突。借阅记录表没有建立外键约束只加了普通索引原因是课设阶段用逻辑关联足够物理外键会在删除测试数据时带来一堆麻烦答辩时可以主动说我用了逻辑外键保证扩展性这是一个加分回答。due_time 在插入记录时由程序计算一般默认 30 天不要放在 SQL 里写死因为不同的读者类型可能允许不同的借阅时长。3.3 实体类与 MyBatis-Plus 映射三行注解搞定字段对应数据库字段用下划线命名Java 字段用驼峰命名MyBatis-Plus 默认开启了驼峰映射所以 book_name 会自动对应 bookName不需要写 TableField。只有主键和表名需要显式指定。下面以 Book 实体为例Data TableName(t_book) public class Book { TableId(type IdType.AUTO) private Long id; private String name; private String author; private String isbn; private Long categoryId; private Integer stock; private LocalDateTime createTime; }TableName 把实体类和 t_book 表绑定TableId(type IdType.AUTO) 表示主键自增和数据库的自增主键对应。剩下字段不用加注解MyBatis-Plus 会自动把驼峰转下划线。LocalDateTime 对应数据库的 DATETIME 类型这是 Java 8 时间类型的标准做法别再用 java.util.Date 了后面算超期天数会用 LocalDateTime 的 API比 Date 好用得多。借阅记录表的实体类同理但有一个字段要单独处理Data TableName(t_borrow_record) public class BorrowRecord { TableId(type IdType.AUTO) private Long id; private Long readerId; private Long bookId; private LocalDateTime borrowTime; private LocalDateTime dueTime; private LocalDateTime returnTime; private Integer status; private BigDecimal fine; }BigDecimal 是金额字段的标准类型千万别用 Double 或者 Float超期罚金的精度问题会让你在答辩时被问到哑口无言。前端展示时如果想直接看到书名和读者名常见做法是让 VO 类里额外加两个非数据库字段用 TableField(exist false) 标注再在 Service 层手动填充这个技巧后面分页查询时会用到。4. 让业务转起来借书、还书、超期与分页的核心代码4.1 借书逻辑扣库存和插记录必须在一个事务里借书这个动作看起来简单前端传一个 bookId 和 readerId后端做三件事查库存、减库存、插借阅记录。如果这三步没有事务保护减库存成功但插记录失败图书就凭空少了一本。这个 bug 在并发请求下才暴露平时单用户测试根本发现不了。我给出的 Service 实现是这样的Service public class BorrowServiceImpl implements BorrowService { Resource private BookMapper bookMapper; Resource private BorrowRecordMapper borrowRecordMapper; Override Transactional(rollbackFor Exception.class) public void borrowBook(Long bookId, Long readerId) { // 1. 查图书锁定库存 Book book bookMapper.selectById(bookId); if (book null || book.getStock() 0) { throw new BusinessException(图书不存在或库存不足); } // 2. 扣减库存 int updated bookMapper.reduceStock(bookId); if (updated 0) { throw new BusinessException(库存扣减失败); } // 3. 插入借阅记录默认借期 30 天 BorrowRecord record new BorrowRecord(); record.setBookId(bookId); record.setReaderId(readerId); record.setBorrowTime(LocalDateTime.now()); record.setDueTime(LocalDateTime.now().plusDays(30)); record.setStatus(0); record.setFine(BigDecimal.ZERO); borrowRecordMapper.insert(record); } }注意几个关键点。Transactional(rollbackFor Exception.class) 里写不写 rollbackFor 差别很大Spring 默认只对 RuntimeException 回滚自定义的 BusinessException 如果继承的是 Exception不加 rollbackFor 就不会触发回滚。reduceStock 是写在 BookMapper 里的一条自定义 SQLUPDATE t_book SET stock stock - 1 WHERE id #{id} AND stock 0这种写法比先查出来再 updateById 安全因为 stock 0 条件能防止超卖这也是答辩时能拿出来讲的细节。updateById 是 MyBatis-Plus 自带的但 reduceStock 这种带条件的更新必须自己写 SQL。到这里借书的核心就闭环了。4.2 还书逻辑超期天数与罚金的计算方式还书比借书多一个业务点判断是否超期超期则计算罚金。计算规则一般有两种一种是按自然日算一种是按工作日算。课设阶段按自然日算就够了规则简单答辩好解释。罚金标准我习惯设 0.5 元/天这个值放在常量里方便后期改。Override Transactional(rollbackFor Exception.class) public void returnBook(Long recordId) { BorrowRecord record borrowRecordMapper.selectById(recordId); if (record null || record.getStatus() ! 0) { throw new BusinessException(借阅记录不存在或已归还); } LocalDateTime now LocalDateTime.now(); record.setReturnTime(now); record.setStatus(1); // 判断是否超期 if (record.getDueTime().isBefore(now)) { long overdueDays ChronoUnit.DAYS.between(record.getDueTime(), now); record.setFine(BigDecimal.valueOf(overdueDays).multiply(OVERDUE_FINE_PER_DAY)); } else { record.setFine(BigDecimal.ZERO); } // 更新记录并恢复库存 borrowRecordMapper.updateById(record); bookMapper.increaseStock(record.getBookId()); }ChronoUnit.DAYS.between 是计算两个 LocalDateTime 之间天数的标准 APIbetween 方法直接算整数的天数差。这里有个细节如果归还时间比应还时间晚 1 小时between 返回 0 而不是 1也就是说当天还不算超期。如果规则要求哪怕超一小时也要算一天可以先把时间转成 LocalDate 再比较或者用 plusDays(1) 的边界判断。增加库存的 SQL 是UPDATE t_book SET stock stock 1 WHERE id #{id}这个方法返回 int如果影响行数为 0说明图书记录被删了理论上不该发生但写上判断总没坏处。超期未还的记录还要单独提供一个查询接口用 status 2 过滤这部分可以写成接口给前端展示超期列表。4.3 分页查询MyBatis-Plus 分页插件的正确打开方式分页是图书管理系统列表页的刚需MyBatis-Plus 默认的分页是假分页如果不注册分页插件Page 对象只会查出全部数据再内存里切数据量一大就卡。网上很多半吊子教程没这步导致分页看着能跑实际上是假分页。分页插件要先注册成 Spring BeanConfiguration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor pagination new PaginationInnerInterceptor(DbType.MYSQL); pagination.setMaxLimit(50L); interceptor.addInnerInterceptor(pagination); return interceptor; } }DbType.MYSQL 必须和数据库类型一致不写或写错会导致 SQL 方言拼接错误Oracle 和 MySQL 的分页语法完全不同。setMaxLimit(50L) 的意思是单页最大 50 条防一手别人传一个 pageSize999999 直接把数据库打崩这属于防御性编程答辩时提一句很加分。Service 层的分页查询长这样Override public IPageBookVO pageBooks(int pageNum, int pageSize, String keyword) { PageBook page new Page(pageNum, pageSize); LambdaQueryWrapperBook wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(keyword), Book::getName, keyword) .orderByDesc(Book::getCreateTime); IPageBook bookPage bookMapper.selectPage(page, wrapper); // 转换为带分类名称的 VO IPageBookVO voPage bookPage.convert(book - { BookVO vo new BookVO(); BeanUtils.copyProperties(book, vo); return vo; }); return voPage; }LambdaQueryWrapper 的 like 条件里第一个参数是布尔值keyword 为空就不拼接这个条件这是 MyBatis-Plus 的推荐写法能省掉一堆 if 判断。convert 方法把查询结果从 Book 转成 BookVO转换逻辑里可以顺便查分类名填进去。分页插件做好之后前端只需要传 current 和 size 两个参数。5. 避坑与排查从启动失败到答辩翻车的五个真实案例5.1 数据库连接失败mysql 8 驱动与 UTC 时区问题现象项目启动时控制台报 communications link failure 或者 The server time zone value CST is unrecognized。原因MySQL 8.0 之后连接校验变了同时驱动类名从 com.mysql.jdbc.Driver 改成了 com.mysql.cj.jdbc.Driver另外连接串里没指定 serverTimezone 时MySQL 8 会报 CST 无法识别的错误。解决驱动类名改成 com.mysql.cj.jdbc.Driverurl 后面拼 ?serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8。如果数据库服务器不在本机还要检查防火墙和 bind-address 配置这个坑在局域网部署时会碰到。5.2 分页查不到数据或数据错乱现象selectPage 执行后 total 一直是 0或者第一页和第二页数据重复。翻车原因多半是分页插件没注册或者注册了但 DbType 写成了 OTHER。原因MyBatis-Plus 的物理分页依赖 PaginationInnerInterceptor精确匹配数据库方言。没有插件时Page 参数会被当成普通参数传递SQL 里不会有 LIMIT 语句查询结果会是全表total 自然不对。解决确认 MybatisPlusConfig 被 Configuration 扫描到检查 DbType.MYSQL 是否正确。验证方法很简单打开 log-impl 的 SQL 日志看控制台输出的 SQL 末尾有没有 LIMIT没有就是插件没生效。5.3 借书事务没生效事务自调用是隐蔽杀手现象强行在 bookMapper.reduceStock 之后抛一个 BusinessException结果发现库存竟然扣掉了记录也没回滚。原因Transactional 通过 AOP 代理生效如果你在同一个类的另一个方法里直接调用 this.borrowBook()调用发生在代理对象内部事务注解根本不会生效。还有一种情况是 rollbackFor 没写BusinessException 继承的是 Exception默认不回滚。解决事务方法必须从外部类调用或者通过注入自身的代理对象调用。同时把 Transactional(rollbackFor Exception.class) 写全。可以在测试里故意抛异常验证这是最朴素也最有效的事务自检方法。5.4 前端传日期字符串后端 LocalDateTime 解析 400现象还书接口用 POST 传一个 returnTime2025-06-01 12:30:00后端参数是 LocalDateTime直接报解析失败。原因Spring 默认的 LocalDateTime 反序列化格式是 ISO 标准格式 2025-06-01T12:30:00前端传的是带空格的格式对不上。这个坑在前后端分离项目里几乎必现。解决加一个全局的 Jackson 配置类Configuration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer customizer() { return builder - { builder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); builder.deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); }; } }这样前端传带空格的格式就能正常解析返回给前端的日期也是带空格的双方不用再为格式打架。5.5 Vue 前端跨域报错CORS 配置不是写在前端现象前端用 Vue 开发服务器访问 localhost:8080浏览器控制台报 Access-Control-Allow-Origin 错误企业微信或钉钉里打开的页面更是必现。原因跨域拦截发生在浏览器后端没返回 CORS 响应头前端做任何配置都解决不了。Spring Boot 里跨域要写一个过滤器或配置类而不是在 Controller 上加 CrossOrigin 注解一个个方法加。解决推荐加一个 WebMvcConfigurer 配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }allowedOriginPatterns() 配合 allowCredentials(true) 是现在的标准写法直接用 allowedOrigins() 会和 allowCredentials 冲突。答辩演示时如果是本地前后端分离这一步不做前端永远调不通接口。6. 交付前的最后一公里演示数据、答辩稿与二十分钟收尾技巧系统能跑只是第一步答辩和演示才是决定成绩的地方。我见过太多项目功能齐全但演示时手忙脚乱的案例这里给你三个具体的收尾技巧每天花二十分钟连续做三天效果立竿见影。第一个技巧是造演示数据要看起来真实。不要用书1书2这种测试数据直接用真实书名比如《深入理解Java虚拟机》《代码整洁之道》读者起名也用张伟李娜这种常见名字。演示时评委扫一眼页面第一印象就是这个学生认真做过。造数据可以用一个简单的 for 循环往数据库插也可以手写 SQL 插几十条重点是让列表分页效果看起来饱满。第二个技巧是准备一条主动讲的主线。借阅流程是必演项目演示顺序建议是先查一本书确认库存大于 0执行借阅展示库存减一再执行归还展示库存恢复和借阅记录状态变化。整个过程最好控制在三分钟以内语气不要念稿子用我先查一下这本书的库存然后借出去你们注意看库存字段的变化这种自然表达。第三个技巧是给系统加一个全局 XSS 过滤器。这个点很小但很能体现安全意识。图书管理系统的书名、读者姓名都是用户输入如果直接在页面上展示存在存储型 XSS 的风险。常见的做法是定义一个 Filter用 Jsoup 的 clean 方法清洗请求参数去掉 script 标签Component public class XssFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { XssHttpServletRequestWrapper wrappedRequest new XssHttpServletRequestWrapper((HttpServletRequest) request); chain.doFilter(wrappedRequest, response); } }配合一个包装类重写 getParameter 和 getHeader 方法在返回值前做去标签处理。不要小看这个过滤器答辩时评委问你怎么防 XSS大多数人答不上来你能说出 Jsoup 清洗方案已经超过九成同行。最后说一个我的习惯每次部署完项目我都会把 IDEA 的干净启动页面截图存一份连同正常借还的录屏一起放桌面答辩前一晚再完整走一遍流程。这不是玄学是防止演示时数据库被改乱、服务起不来这种突发情况。这个项目值得认真投入因为它的每段代码都能在答辩时讲出设计理由而不是背课本。希望这份实践笔记帮到你也祝你答辩顺利。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →