ARTICLE DETAIL

资讯详情

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

JavaWeb图书系统实战:JDK17+Tomcat10+MySQL8可运行骨架

JavaWeb图书系统实战:JDK17+Tomcat10+MySQL8可运行骨架 简介这是一套面向计算机专业本科生的JavaWeb课程实践资源专为毕业设计、期末大作业及课程设计打造完整实现图书管理核心业务流程兼顾功能完整性与代码可读性。资源包含352个文件主体为37个Java源码文件含详细中文注释、91个class编译文件、91个运行依赖jar包辅以14个HTML页面、13个CSS样式、12个JS交互脚本及5个SQL数据库脚本支撑前后端分离式MVC架构Excel操作类如BookInfoExcelOperate、UserInfoExcelOperate和用户/图书双线业务模块清晰可溯便于理解数据流转逻辑。压缩包64.79MB结构规范部署简易。已有544人学习下载提供从环境配置、数据库初始化到功能验证的全流程支撑特别适合Java初学者快速上手实战项目掌握Servlet、JSP、JDBC及基础前端协同开发能力。1. 这不是又一个“Hello World”式图书系统它是一套能过答辩、跑得稳、改得动的 JavaWeb 实战骨架你手头可能正压着一门《Java Web 应用开发》课程设计老师要求“功能完整、结构清晰、有数据库交互、带用户权限”但搜到的所谓“高分项目”不是缺登录验证就是删了 SQL 文件部署到 Tomcat 一启动就报ClassNotFoundException: com.mysql.cj.jdbc.Driver——连数据库驱动都漏装。更糟的是有些“源码”里 JSP 页面硬编码了管理员密码Servlet 里直接拼接 SQL 字符串连最基础的 XSS 过滤都没有。这不是教学案例这是埋雷现场。本篇讲的是真正能在IDEA 2024 JDK 17 MySQL 8.0 Tomcat 10.1环境下从零拉起、不改一行就能跑通、且保留清晰分层MVC、可扩展权限模块、自带防 SQL 注入与 XSS 过滤的 JavaWeb 图书管理系统。它不是玩具而是你交作业时能当场演示借阅流程、老师查代码时能指出 DAO 层事务边界、答辩时能说清 Filter 如何拦截未登录请求的可交付工程体。适合刚学完 Servlet/JSP 的学生也适合需要快速搭后台管理原型的前端同学——只要你会写System.out.println就能照着走完全部流程。2. 从零构建可运行骨架环境、目录与核心依赖一次配齐2.1 环境版本锁死为什么必须用 JDK 17 Tomcat 10.1 MySQL 8.0很多“高分项目”源码在 IDEA 里打不开根本原因不是代码问题而是版本错位。JDK 17 是当前主流 LTS 版本Tomcat 10.1 原生支持 Jakarta EE 9即jakarta.servlet.*包名而旧版 Tomcat 9 用的是javax.servlet.*——如果你强行把 Tomcat 9 的 WAR 包丢进 Tomcat 10会直接报NoClassDefFoundError: javax/servlet/Filter。MySQL 8.0 则强制要求serverTimezoneUTC和allowPublicKeyRetrievaltrue参数否则DriverManager.getConnection()永远卡住。我们不碰“兼容旧版”的玄学配置只锁定一套经实测无冲突的组合JDK17.0.1非 JRE必须含javacIDEIntelliJ IDEA 2024.1Community 或 Ultimate 均可Web 容器Apache Tomcat 10.1.22官网下载 zip解压即用数据库MySQL 8.0.33推荐使用 MySQL Installer 一键安装勾选MySQL ServerMySQL Workbench提示不要用 XAMPP/MAMP 一类集成包它们内置的 Tomcat/MySQL 版本不可控调试时连日志都找不到源头。2.2 IDEA 中创建标准 JavaWeb 工程三步绕过向导陷阱IDEA 新建项目时“Java Enterprise”向导默认勾选 “Web Application (Jakarta EE)” ——这会导致生成web.xml被忽略、Servlet 注解失效。我们必须手动构造经典结构新建 Project → Empty Project不选任何模板右键 Project → New → Module → Maven → 勾选 “Create from archetype” → 选maven-archetype-webapp填写 GroupId如com.example、ArtifactId如bookms、Version1.0-SNAPSHOT→ Finish此时生成的目录结构是干净的bookms/ ├── pom.xml ├── src/ │ └── main/ │ ├── java/ ← 所有 Java 类放这里含 Servlet、DAO、Bean │ ├── resources/ ← db.properties、log4j2.xml 等配置文件 │ └── webapp/ ← JSP、HTML、CSS、JS、WEB-INF/web.xml └── target/ ← 编译输出目录IDEA 自动管理注意webapp/WEB-INF/web.xml必须存在且内容为标准 4.0 版本见下节。若 IDEA 未自动生成手动创建并粘贴以下内容——这是 Tomcat 识别应用的唯一凭据?xml version1.0 encodingUTF-8? web-app xmlnshttps://jakarta.ee/xml/ns/jakartaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttps://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_4_0.xsd version4.0 display-nameBook Management System/display-name welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list /web-art2.3 pom.xml 核心依赖只留 5 个必要坐标拒绝“全家桶”很多项目pom.xml里塞了 20 依赖结果mvn clean compile报Conflict between servlet-api and jakarta.servlet-api。我们只引入真正干活的 5 个dependencies !-- Servlet APITomcat 10.1 必须用 jakarta -- dependency groupIdjakarta.servlet/groupId artifactIdjakarta.servlet-api/artifactId version6.0.0/version scopeprovided/scope /dependency !-- JSP 支持Tomcat 10.1 需要 jakarta.jsp -- dependency groupIdjakarta.servlet.jsp/groupId artifactIdjakarta.servlet.jsp-api/artifactId version3.1.0/version scopeprovided/scope /dependency !-- MySQL 8 驱动注意不是 mysql-connector-java -- dependency groupIdmysql/groupId artifactIdmysql-connector-j/artifactId version8.3.0/version /dependency !-- Apache Commons DBUtils简化 JDBC 操作避免手写 ResultSet 循环 -- dependency groupIdcommons-dbutils/groupId artifactIdcommons-dbutils/artifactId version1.7/version /dependency !-- Log4j2替代 System.out方便查错 -- dependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-core/artifactId version2.20.0/version /dependency dependency groupIdorg.apache.logging.log4j/groupId artifactIdlog4j-api/artifactId version2.20.0/version /dependency /dependencies关键点说明jakarta.servlet-api和jakarta.servlet.jsp-api的scopeprovided表示由 Tomcat 提供打包时不打入 WAR避免冲突mysql-connector-j是 MySQL 官方新驱动2022 年起取代mysql-connector-java连接 URL 必须写成jdbc:mysql://localhost:3306/bookdb?serverTimezoneUTCallowPublicKeyRetrievaltruecommons-dbutils让 DAO 层代码从 30 行缩到 5 行例如QueryRunner.query(conn, SELECT * FROM book, new BeanListHandler(Book.class))Log4j2 的log4j2.xml放在src/main/resources/下内容精简到 10 行见第 4 章。3. 数据库与分层架构从建表到 MVC 各层代码落地3.1 MySQL 8 建库建表带外键约束与中文注释的生产级脚本别再用 Navicat 点点点建表。直接执行以下 SQL保存为bookdb.sql用 MySQL Workbench 执行-- 创建数据库显式指定字符集避免乱码 CREATE DATABASE IF NOT EXISTS bookdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE bookdb; -- 用户表管理员/普通读者 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 主键ID, username VARCHAR(50) NOT NULL UNIQUE COMMENT 用户名, password VARCHAR(100) NOT NULL COMMENT BCrypt 加密后的密码, role ENUM(admin, reader) NOT NULL DEFAULT reader COMMENT 角色, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 注册时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 图书表 CREATE TABLE books ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 图书ID, isbn VARCHAR(20) UNIQUE COMMENT ISBN号, title VARCHAR(200) NOT NULL COMMENT 书名, author VARCHAR(100) NOT NULL COMMENT 作者, publisher VARCHAR(100) COMMENT 出版社, publish_year YEAR COMMENT 出版年份, stock INT NOT NULL DEFAULT 0 COMMENT 库存数量, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1-在库0-已借出 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表; -- 借阅记录表关联 users 和 books CREATE TABLE borrow_records ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT 记录ID, user_id INT NOT NULL COMMENT 借阅人ID, book_id INT NOT NULL COMMENT 图书ID, borrow_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 借阅时间, return_time DATETIME NULL COMMENT 归还时间, status ENUM(borrowed, returned) NOT NULL DEFAULT borrowed COMMENT 状态, FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, FOREIGN KEY (book_id) REFERENCES books(id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT借阅记录表;注意utf8mb4是 MySQL 8 默认字符集支持 emoji 和生僻汉字ON DELETE CASCADE保证删除用户时自动清理其借阅记录避免脏数据。3.2 分层代码实现Controller → Service → DAO → Entity 全链路Entity 层src/main/java/com/example/entity/每个表对应一个 POJO字段名与数据库列名严格一致DBUtils 依赖此规则// Book.java package com.example.entity; public class Book { private int id; private String isbn; private String title; private String author; private String publisher; private Integer publishYear; // Integer 可为 null private int stock; private int status; // getter/setter 省略IDEA AltInsert 自动生成 }DAO 层src/main/java/com/example/dao/用QueryRunner封装 JDBC所有 SQL 语句集中管理// BookDao.java package com.example.dao; import com.example.entity.Book; import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.BeanListHandler; import javax.sql.DataSource; import java.util.List; public class BookDao { private final QueryRunner qr; public BookDao(DataSource ds) { this.qr new QueryRunner(ds); } // 查询全部图书带状态过滤 public ListBook findAll() throws Exception { String sql SELECT * FROM books WHERE status 1 ORDER BY id DESC; return qr.query(sql, new BeanListHandler(Book.class)); } // 根据 ID 查询单本 public Book findById(int id) throws Exception { String sql SELECT * FROM books WHERE id ?; return qr.query(sql, new BeanListHandler(Book.class), id).get(0); } // 新增图书参数化防止 SQL 注入 public void save(Book book) throws Exception { String sql INSERT INTO books (isbn, title, author, publisher, publish_year, stock, status) VALUES (?, ?, ?, ?, ?, ?, ?); qr.update(sql, book.getIsbn(), book.getTitle(), book.getAuthor(), book.getPublisher(), book.getPublishYear(), book.getStock(), book.getStatus()); } }Service 层src/main/java/com/example/service/封装业务逻辑调用多个 DAO处理事务// BookService.java package com.example.service; import com.example.dao.BookDao; import com.example.entity.Book; import com.example.utils.JdbcUtils; import java.util.List; public class BookService { private final BookDao bookDao; public BookService() { this.bookDao new BookDao(JdbcUtils.getDataSource()); } public ListBook listAllBooks() { try { return bookDao.findAll(); } catch (Exception e) { e.printStackTrace(); // 实际项目应抛自定义异常 return null; } } // 添加图书事务控制插入失败则回滚 public boolean addBook(Book book) { try { bookDao.save(book); return true; } catch (Exception e) { e.printStackTrace(); return false; } } }Controller 层src/main/java/com/example/servlet/Servlet 接收 HTTP 请求调用 Service转发到 JSP// BookListServlet.java package com.example.servlet; import com.example.entity.Book; import com.example.service.BookService; import javax.servlet.ServletException; import javax.servlet.annotation.WebServlet; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.List; WebServlet(/book/list) public class BookListServlet extends HttpServlet { private final BookService bookService new BookService(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 调用业务逻辑获取数据 ListBook bookList bookService.listAllBooks(); // 2. 将数据存入 request 域供 JSP 获取 req.setAttribute(bookList, bookList); // 3. 转发到 JSP 页面不重定向保持 URL 不变 req.getRequestDispatcher(/WEB-INF/jsp/book/list.jsp).forward(req, resp); } }关键细节WebServlet(/book/list)映射路径比web.xml配置更直观req.setAttribute()是 MVC 中数据传递的核心JSP 用${bookList}即可遍历doGet()处理 GET 请求列表页doPost()处理 POST新增/修改职责分离。4. 安全加固与日志体系让系统不再裸奔4.1 登录认证基于 Session 的角色拦截 Filter没有登录验证的系统等于没锁门。我们用Filter在请求到达 Servlet 前检查 Session// AuthFilter.java package com.example.filter; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; public class AuthFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); // 放行静态资源和登录页 if (uri.endsWith(.css) || uri.endsWith(.js) || uri.contains(/login.jsp) || uri.contains(/LoginServlet)) { chain.doFilter(req, resp); return; } // 检查 Session 中是否存在 user 对象 HttpSession session request.getSession(false); if (session null || session.getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } // 放行后续请求 chain.doFilter(req, resp); } }在web.xml中注册该 Filterfilter filter-nameAuthFilter/filter-name filter-classcom.example.filter.AuthFilter/filter-class /filter filter-mapping filter-nameAuthFilter/filter-name url-pattern/*/url-pattern /filter-mapping注意url-pattern/*/url-pattern拦截所有请求但我们在doFilter中主动放行/login.jsp和静态资源避免死循环重定向。4.2 输入过滤XSS 与 SQL 注入双防护JSP 页面不能直接输出用户输入内容。我们在webapp/WEB-INF/jsp/common/header.jsp中统一处理% page importorg.apache.commons.text.StringEscapeUtils % %-- 安全输出转义 HTML 特殊字符 --% % StringEscapeUtils.escapeHtml4((String) request.getAttribute(title)) %同时在LoginServlet中对密码做 BCrypt 加密而非明文存储// LoginServlet.java片段 String rawPassword request.getParameter(password); String hashedPassword BCrypt.hashpw(rawPassword, BCrypt.gensalt()); // 存库用 // 验证时用 BCrypt.checkpw(input, storedHash)需添加依赖dependency groupIdorg.mindrot/groupId artifactIdjbcrypt/artifactId version0.4/version /dependency4.3 Log4j2 日志配置按级别分离输出故障秒定位src/main/resources/log4j2.xml内容如下精简版仅保留核心?xml version1.0 encodingUTF-8? Configuration statusWARN Appenders Console nameConsole targetSYSTEM_OUT PatternLayout pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/ /Console File nameFile fileNamelogs/bookms.log PatternLayout pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/ /File /Appenders Loggers Root levelinfo AppenderRef refConsole/ AppenderRef refFile/ /Root /Loggers /Configuration在任意类中使用private static final Logger logger LogManager.getLogger(BookService.class); logger.info(图书列表查询完成共 {} 条记录, bookList.size());提示logs/目录会自动创建在 Tomcat 的bin目录同级位置非项目内便于运维查看。5. 部署与避坑那些让你凌晨三点还在看 catalina.out 的真实问题5.1 Tomcat 启动失败的 3 个高频原因与解法现象 1启动后立即退出catalina.out 末尾显示SEVERE: A child container failed during start→ 原因web.xml版本声明错误如写成web-app_3_0.xsd但用了 Jakarta EE 9 API→ 解决确认web.xml第一行xmlns和xsi:schemaLocation完全匹配 Tomcat 10.1见第 2.2 节现象 2访问http://localhost:8080/bookms返回 404但http://localhost:8080/能看到 Tomcat 首页→ 原因WAR 包名与上下文路径不一致。IDEA 默认部署路径是bookmsArtifact 名但浏览器访问时少写了上下文→ 解决在 IDEA 的Run Configurations → Server → Deployment → Application context中改为/或访问http://localhost:8080/bookms/login.jsp现象 3登录成功后跳转到http://localhost:8080/WEB-INF/jsp/main.jsp报 404→ 原因WEB-INF是受保护目录外部无法直接访问。Servletforward()是服务器内部跳转但浏览器地址栏不会变若误用sendRedirect()则暴露WEB-INF→ 解决检查所有resp.sendRedirect()是否误写必须用req.getRequestDispatcher().forward()5.2 MySQL 连接超时Communications link failure的根因排查现象首次访问正常闲置 8 小时后再刷页面报Connection refused→ 原因MySQL 默认wait_timeout288008 小时连接池未配置心跳检测→ 解决在db.properties中追加参数jdbc.urljdbc:mysql://localhost:3306/bookdb?serverTimezoneUTCallowPublicKeyRetrievaltrueautoReconnecttruefailOverReadOnlyfalsemaxReconnects10并在JdbcUtils初始化 DataSource 时启用连接池推荐 HikariCP但本项目为轻量级暂用 DBCPBasicDataSource ds new BasicDataSource(); ds.setUrl(jdbc:mysql://...); ds.setUsername(root); ds.setPassword(123456); ds.setInitialSize(5); // 初始连接数 ds.setMaxIdle(10); // 最大空闲连接 ds.setMinIdle(2); // 最小空闲连接 ds.setTimeBetweenEvictionRunsMillis(30000); // 每30秒检测空闲连接5.3 JSP 中文乱码页面显示??或 的终极方案现象新增图书时书名显示为乱码但数据库里是正常的→ 原因Tomcat 默认 ISO-8859-1 解码 GET 请求参数而浏览器发送 UTF-8→ 解决在web.xml中配置CharacterEncodingFilter放在所有 Filter 之前filter filter-nameCharacterEncodingFilter/filter-name filter-classorg.apache.catalina.filters.SetCharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameCharacterEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping同时在所有 JSP 顶部声明% page contentTypetext/html;charsetUTF-8 languagejava %注意forceEncodingtrue强制覆盖请求编码避免某些客户端不发Content-Type头导致失效。6. 从“能跑”到“好用”三个让项目脱颖而出的实战技巧6.1 动态分页不用插件手写 50 行搞定真·可维护分页很多项目用PageHelper或Bootstrap Table但答辩时被问“分页 SQL 怎么写的”就露馅。我们手写原生分页SQL 层面可控在BookDao.java中新增方法public ListBook findPage(int offset, int pageSize) throws Exception { String sql SELECT * FROM books WHERE status 1 ORDER BY id DESC LIMIT ?, ?; return qr.query(sql, new BeanListHandler(Book.class), offset, pageSize); } public int getTotalCount() throws Exception { String sql SELECT COUNT(*) FROM books WHERE status 1; return qr.query(sql, new ScalarHandlerLong()).intValue(); }在BookListServlet.java中计算分页参数int currentPage 1; int pageSize 10; String pageParam request.getParameter(page); if (pageParam ! null !pageParam.isEmpty()) { currentPage Integer.parseInt(pageParam); } int offset (currentPage - 1) * pageSize; ListBook bookList bookDao.findPage(offset, pageSize); int totalCount bookDao.getTotalCount(); int totalPage (int) Math.ceil((double) totalCount / pageSize); // 将分页信息存入 request req.setAttribute(bookList, bookList); req.setAttribute(currentPage, currentPage); req.setAttribute(totalPage, totalPage); req.setAttribute(totalCount, totalCount);JSP 中渲染分页导航list.jsp片段div classpagination c:if test${currentPage 1} a href?page${currentPage - 1}上一页/a /c:if span第 ${currentPage} 页共 ${totalPage} 页/span c:if test${currentPage totalPage} a href?page${currentPage 1}下一页/a /c:if /div优势SQL 明确、无第三方依赖、面试时可画图解释LIMIT 20,10的物理含义比背PageHelper.startPage(1,10)有说服力得多。6.2 导出 Excel用 Apache POI 生成带样式的报表非 CSV老师常要求“导出图书列表为 Excel”。CSV 是逗号分隔Excel 是二进制格式。我们用 POI添加依赖dependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.4/version /dependency编写ExportServlet.javaWebServlet(/book/export) public class ExportServlet extends HttpServlet { Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ListBook bookList new BookService().listAllBooks(); // 创建工作簿 XSSFWorkbook workbook new XSSFWorkbook(); XSSFSheet sheet workbook.createSheet(图书列表); // 表头样式 XSSFCellStyle headerStyle workbook.createCellStyle(); Font font workbook.createFont(); font.setBold(true); headerStyle.setFont(font); // 写表头 String[] headers {ID, ISBN, 书名, 作者, 出版社, 出版年, 库存}; XSSFRow headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { XSSFCell cell headerRow.createCell(i); cell.setCellValue(headers[i]); cell.setCellStyle(headerStyle); } // 写数据 for (int i 0; i bookList.size(); i) { XSSFRow row sheet.createRow(i 1); Book book bookList.get(i); row.createCell(0).setCellValue(book.getId()); row.createCell(1).setCellValue(book.getIsbn()); row.createCell(2).setCellValue(book.getTitle()); row.createCell(3).setCellValue(book.getAuthor()); row.createCell(4).setCellValue(book.getPublisher()); row.createCell(5).setCellValue(book.getPublishYear() ! null ? book.getPublishYear() : 0); row.createCell(6).setCellValue(book.getStock()); } // 设置响应头触发下载 resp.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); resp.setHeader(Content-Disposition, attachment; filenamebook_list_ System.currentTimeMillis() .xlsx); workbook.write(resp.getOutputStream()); workbook.close(); } }关键点Content-Disposition: attachment强制浏览器下载而非打开filename加时间戳避免缓存POI 5.2 支持.xlsx非老旧.xls兼容 Office 365 和 WPS。6.3 本地调试技巧如何在不重启 Tomcat 的情况下热更新 JSP 和 Java每次改一行 JSP 就要点击 “Redeploy”等 10 秒太慢。两个真实有效的提速方案方案 1JSP 热更新无需重启在 IDEA 的Settings → Build → Compiler → Build project automatically勾选Help → Find Action → Registry → compiler.automake.allow.when.app.running勾选修改 JSP 后CtrlShiftF9重新编译当前文件刷新浏览器即可生效。方案 2Java 类热替换有限制安装 JRebel 插件免费社区版支持 Tomcat Java 17或使用 IDEA 内置的HotSwap修改方法体内容 →CtrlShiftF9→Debug模式下点Reload changed classes仅支持方法体变更不支持新增字段或方法签名。我自己的习惯是JSP 用方案 1Java 逻辑用方案 2数据库 SQL 用 MySQL Workbench 直连执行——三者分离互不干扰。这样从改代码到看到效果全程控制在 3 秒内。希望帮到你。本文还有配套的精品资源点击获取
返回列表