JavaWeb+MySQL图书馆管理系统源码部署与借阅事务实践
简介这份 JavaWeb 图书馆管理系统源码面向高校课程设计、毕业设计与 JavaWeb 入门实践者基于 Servlet、JSP、JavaBean 与 MVC 分层配合 MySQL 实现图书信息、读者账户、借阅归还及管理员权限等核心业务可帮助读者理解请求处理、业务逻辑与数据库读写的完整链路适合需要熟悉传统 Web 项目结构、准备课程作业或进行二次开发的学生与开发者。压缩包共 209 个文件约 1.91MB内含 51 个 JSP 页面、32 个 Java 源文件和 32 个 class 文件、9 个 jar 依赖并附带 SQL 脚本、properties/XML 配置及 MySQL 数据文件覆盖视图、控制层、DAO、实体类等模块。已有 359 人学习。项目按图书、读者、借阅、管理员等业务拆分代码读者可参考数据库表设计与建库脚本、登录鉴权与借还流程、JSP 与 Servlet 交互及分层目录组织用于课设复现、功能扩展和代码对照。目录层次较清晰便于按模块定位文件对图书馆业务建模与权限控制也有较好参考价值。1. 资料室那本借阅登记册被一套 JavaWeb 图书馆管理系统接管了某高校资料室的管理员拿着一本卷边的借阅登记册谁借了什么书、哪天该还全靠一支笔和记性。有人提议做个系统预算为零能用的只有一台装了 JDK 的旧机器。这种场景下一份开箱即用的 JavaWeb 图书馆管理系统源码就是最省事的选择。这份 MySQL 版本的源码走的是很老派但很扎实的路线Servlet 接请求、JSP 出页面、JDBC 直连数据库没有 Spring 那层封装也没有复杂的前端构建解压、配库、丢进 Tomcat 就能跑。它适合两类人一类是刚学完 Servlet 和 JSP、想找个字段齐全的项目把增删改查打通的人另一类是要给小型图书室、班级书架、内部资料库做一套内网管理工具的人。它不追求高并发追求的是每个功能都能在源码里找到对应的一行 SQL改起来不用猜框架在背后替你做了什么。2. 技术栈与目录结构Servlet JSP JDBC 的三层怎么落地2.1 一次请求从浏览器走到 MySQL 的完整链路拿到源码先别急着点运行先把请求链路在脑子里过一遍后面任何报错都能定位到具体一层。这类项目的标准走向是浏览器提交表单 → Tomcat 按web.xml或WebServlet找到对应 Servlet → 前面的 Filter 先过一遍编码和登录校验 → Servlet 取参数、做基本校验 → 调 Service → 调 DAO → DAO 拼 SQL 走 JDBC 打到 MySQL → 结果集映射成实体类 → Servlet 塞进 request 域 → forward 到 JSP → JSTL 渲染成 HTML。层次典型类/文件干的事出问题时的典型症状视图层WebContent/jsp/*.jspJSTL 渲染只做展示页面空白、${}原样打印控制层servlet/BookServlet收参数、校验、转发404、参数永远是 null业务层service/BorrowService借阅上限、库存规则借阅数量对不上持久层dao/BookDaoSQL 与 ResultSet 映射SQLException、字段串位工具层util/DBUtil读配置、拿连接项目启动就抛异常链路里有两处最容易出问题。一是 Filter 的顺序编码过滤器必须排在登录校验过滤器前面否则校验失败跳转时带出去的中文提示会变乱码二是 forward 和 redirect 的混用登录成功后如果还用 forward浏览器地址栏停在/login用户一刷新就会重复提交表单。常见的写法是登录成功后sendRedirect到列表页其余内部跳转统一用 forward。2.2 目录树里每个包到底放什么老项目没有 Maven 的标准目录包名和路径全靠约定。解压之后先对着下面这棵树看一遍心里就有数了library-system/ ├── src/ │ ├── com/library/bean/ # 实体类和表字段一一对应 │ │ ├── Book.java │ │ ├── Reader.java │ │ └── BorrowRecord.java │ ├── com/library/dao/ # 只写 SQL不掺业务判断 │ │ ├── BaseDao.java # 增删改查的公共封装 │ │ ├── BookDao.java │ │ └── BorrowDao.java │ ├── com/library/service/ # 业务规则借阅上限、库存扣减 │ ├── com/library/servlet/ # 接参数、调 service、转发 JSP │ ├── com/library/filter/ # 编码过滤、登录校验 │ └── com/library/util/ # DBUtil、日期工具、分页工具 ├── WebContent/ # 部分 IDE 里叫 web 或 webapp │ ├── WEB-INF/ │ │ ├── web.xml # 早期 Servlet 在这里注册 │ │ └── lib/mysql-connector-java-8.0.28.jar │ ├── jsp/ # 业务页面放在 WEB-INF 下更安全 │ ├── css/ js/ │ └── index.jsp # 登录入口 └── sql/library.sql # 建表 初始数据判断一份源码分层是否干净有个很土但很准的办法打开servlet目录如果里面有PreparedStatement或者ResultSet说明业务逻辑漏到了控制层后面加功能会越来越乱反过来打开dao目录如果里面有HttpServletRequest那是另一方向的穿透DAO 一旦依赖 Web 容器就没法写单元测试。合格的做法是 DAO 只认基本类型和实体类参数进、实体出。2.3 数据库表设计与建表脚本图书管理这行的表结构几十年没变过核心就四张图书、读者、借阅记录、管理员再加上一张分类做字典。这份 SQL 是整个项目的地基字段类型定错后面 Java 里再怎么写都别别扭扭。-- 图书表total 是馆藏总数stock 是当前可借数两者分开存 CREATE TABLE book ( book_id int NOT NULL AUTO_INCREMENT COMMENT 主键, isbn varchar(20) NOT NULL COMMENT 业务唯一号, book_name varchar(100) NOT NULL COMMENT 书名, author varchar(50) DEFAULT NULL, category_id int DEFAULT NULL COMMENT 分类外键, publisher varchar(80) DEFAULT NULL, price decimal(8,2) DEFAULT 0.00, total int NOT NULL DEFAULT 0 COMMENT 馆藏总数, stock int NOT NULL DEFAULT 0 COMMENT 可借数量, PRIMARY KEY (book_id), UNIQUE KEY uk_isbn (isbn), KEY idx_name (book_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表; -- 借阅记录表state 用一个字节表达归还状态和逾期状态 CREATE TABLE borrow_record ( record_id int NOT NULL AUTO_INCREMENT, book_id int NOT NULL, reader_id int NOT NULL, borrow_time datetime NOT NULL COMMENT 借出时间, due_time datetime NOT NULL COMMENT 应还时间, return_time datetime DEFAULT NULL COMMENT 实际归还时间, state tinyint NOT NULL DEFAULT 0 COMMENT 0在借 1已还 2逾期, PRIMARY KEY (record_id), KEY idx_reader (reader_id), KEY idx_book (book_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT借阅记录表;total和stock分开存不是冗余是刚需。图书馆会一次买五本同样的书但对外只有 ISBN 一个号用book_id做借阅外键、用stock表示实时可借数比每本书建一条记录再按 ISBN 聚合要简单得多。字符集统一utf8mb4别用utf8后者在 MySQL 里其实是三字节的utf8mb3遇到生僻字或者课程名里的特殊符号会直接插入失败。提示导入脚本前先确认自己的 MySQL 版本。8.0 默认字符集已经是utf8mb4但 5.7 建库时如果不显式指定表会跟着服务器默认走 latin1中文写进去就是一堆问号。3. 运行环境与启动流程从改配置到登录首页3.1 JDK、Tomcat、MySQL 的版本对齐新手跑这套源码八成的时间耗在版本上。这份代码用的是javax.servlet.*这套包名这个细节决定了上限组件建议版本说明JDK8 或 118 上最稳源码里的语法不会踩到模块化限制Tomcat8.5 或 9.0不要用 Tomcat 10包名换成了jakarta.*全项目编译不过MySQL5.7 或 8.08.0 要换驱动类和 URL 参数JDBC 驱动5.7 配 5.1.x8.0 配 8.0.x驱动版本和服务器版本不匹配会连不上IDE任意需支持 Web 项目Eclipse 用 Dynamic Web ProjectIDEA 用 Web FacetTomcat 10 这一条是血泪经验。如果你从官网首页直接下最新版解压部署后控制台会刷一片ClassNotFoundException: javax.servlet.http.HttpServlet代码一个字没错纯粹是包名换代了。省事的做法是直接找 9.0.x 的压缩包解压即用不写注册表不装服务。3.2 导入 SQL、改 jdbc.properties 与 web.xml第一步建库导数据。命令行里两句就够注意库名要和配置文件里对上# 建库时显式指定字符集避免继承服务器默认值 mysql -uroot -p -e CREATE DATABASE library DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; # 导入建表脚本和初始数据 mysql -uroot -p library sql/library.sql第二步改数据库连接配置。这类项目通常把连接信息抽到src/jdbc.properties如果源码里是硬编码在DBUtil里的我一般顺手抽出来后面换环境不用改代码# MySQL 8.0 的写法注意时区和 SSL 参数 jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://127.0.0.1:3306/library?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password你的密码 # MySQL 5.7 用下面这套驱动类和 URL 都不一样 # jdbc.drivercom.mysql.jdbc.Driver # jdbc.urljdbc:mysql://127.0.0.1:3306/library?useUnicodetruecharacterEncodingutf8serverTimezone这个参数在 MySQL 8.0 的驱动上是硬要求不写会抛The server time zone value ???? is unrecognized因为驱动拿不到服务器时区就不知道怎么解析datetime。allowPublicKeyRetrievaltrue是为了让 8.0 默认的caching_sha2_password认证方式在非 SSL 连接下能走通本地开发开着无妨上生产要慎重。第三步确认web.xml里的欢迎页指向登录页并且WEB-INF/lib下确实放了驱动 jar。很多人把 jar 加进了 IDE 的 Build Path 却没放进libIDE 里跑得好好的一导 war 包部署到独立 Tomcat 就报找不到驱动——因为 IDE 是拿自己的 classpath 顶上的。3.3 部署与第一个能登录的页面部署方式二选一。用 IDE 的话Eclipse 里右键项目 → Run As → Run on Server选好 Tomcat 9 即可IDEA 里配一个 Tomcat 的 Run ConfigurationDeployment 页签加 Artifact。想脱离 IDE 就直接把编译产物和WebContent打包成 war扔进TOMCAT_HOME/webapps启动后访问http://localhost:8080/项目名/。启动成功的标志是控制台没有异常堆栈并且能在浏览器里看到登录页。如果页面出来了但样式全丢看 Network 面板里css/bootstrap.min.css是不是 404——通常是 JSP 里用了相对路径而页面被 forward 到/jsp/子目录后基准路径变了。稳妥写法是在每个 JSP 头部加一句%-- 用上下文路径拼绝对地址forward 到任何子目录都不会丢样式 --% link relstylesheet href${pageContext.request.contextPath}/css/bootstrap.min.css script src${pageContext.request.contextPath}/js/jquery.min.js/script${pageContext.request.contextPath}取的是应用上下文部署成 ROOT 时是空串部署成/library时是/library两种情况都能拼对。这套源码里的 JSP 如果写的是hrefcss/xxx.css这种裸相对路径建议一次性全改掉比一个个排查省事。默认账号一般在sql/library.sql的INSERT语句里管理员表里躺着一条admin/123456之类的初始数据登进去看到图书列表环境就算通了。4. 核心模块拆解登录鉴权、图书分页、借阅事务4.1 登录从明文比对到 Session 与过滤器登录是整个系统的入口也是最容易被写歪的地方。这份源码里典型的写法是 Servlet 直接查库比对明文密码逻辑短但漏洞也明显。WebServlet(/login) public class LoginServlet extends HttpServlet { private final AdminDao adminDao new AdminDao(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String pwd req.getParameter(password); Admin admin adminDao.findByUsername(username); // 用户名不存在和密码错误给同一句提示避免被枚举出有效账号 if (admin null || !admin.getPassword().equals(pwd)) { req.setAttribute(msg, 用户名或密码错误); req.getRequestDispatcher(/jsp/login.jsp).forward(req, resp); return; } // 登录态放 Session后续所有页面从这儿取当前用户 req.getSession().setAttribute(loginAdmin, admin); // 重定向而不是转发防止刷新重复提交表单 resp.sendRedirect(req.getContextPath() /book?actionlist); } }光有登录还不够用户绕过登录页直接敲/book?actionlist照样能进。所以必须配一个登录校验过滤器把需要保护的路径统一拦下来WebFilter(/*) public class LoginFilter implements Filter { // 白名单登录页、登录接口、静态资源放行 private static final String[] WHITE {/login, /jsp/login.jsp, /css/, /js/}; Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; String uri req.getRequestURI(); for (String w : WHITE) { if (uri.contains(w)) { chain.doFilter(request, response); // 白名单直接放行 return; } } if (req.getSession().getAttribute(loginAdmin) null) { resp.sendRedirect(req.getContextPath() /jsp/login.jsp); return; } chain.doFilter(request, response); // 已登录继续往下走 } }这里两个参数值得说清楚WebFilter(/*)的/*是前缀匹配会拦下所有请求包括图片和 CSS所以白名单里必须放静态资源目录否则登录页自己都加载不出样式。uri.contains(w)用包含判断而不是startsWith是因为getRequestURI()返回的是带上下文路径的全路径比如/library/login直接startsWith(/login)永远匹配不上。另外过滤器里跳转必须用sendRedirect用 forward 的话浏览器地址栏不变用户按 F5 会重新提交一次登录请求。4.2 图书列表的分页与模糊搜索图书列表是访问量最大的页面也是最容易写出性能问题的地方。新手版本常见两种写法SELECT * FROM book全量查出来在 Java 里subList或者拼字符串WHERE book_name LIKE % keyword %。前者书一多就卡后者直接是 SQL 注入的活靶子。public ListBook page(String keyword, int pageNo, int pageSize) { ListBook list new ArrayList(); // 用 ? 占位LIKE 的通配符拼在参数值上而不是 SQL 里 String sql SELECT b.*, c.category_name FROM book b LEFT JOIN category c ON b.category_id c.category_id WHERE b.book_name LIKE ? OR b.author LIKE ? ORDER BY b.book_id DESC LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setString(2, % keyword %); ps.setInt(3, (pageNo - 1) * pageSize); // 偏移量从 0 开始 ps.setInt(4, pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { list.add(mapRow(rs)); // 结果集到实体的映射单独抽方法 } } } catch (SQLException e) { throw new RuntimeException(分页查询图书失败, e); } return list; }几个参数的门道pageNo从 1 开始传偏移量算成(pageNo - 1) * pageSize如果业务代码里直接传 0 或者负数MySQL 会抛语法错误而不是返回空列表。pageSize一般取 10 到 20太大前端表格撑不住太小翻页次数多。ORDER BY book_id DESC不能省没有明确排序时 MySQL 返回顺序不保证稳定翻页会出现同一本书在两页都出现或者干脆漏掉的情况。分页还得配一个 count 查询拿总数SELECT COUNT(*) FROM book WHERE book_name LIKE ? OR author LIKE ?条件和列表查询严格保持一致否则总页数算错最后一页点进去是空的。封装一个PageBean把list、totalCount、pageNo、totalPage装一起传回 JSP页面上的上一页下一页按钮直接用${page.totalPage}判断边界。4.3 借书还书必须手动控制的事务借书这个动作背后是三条 SQL扣库存、写借阅记录、改读者的已借数量。三条要么全成要么全不成中间断一条就会出现库存扣了但没记录或者记录写了但书还在架上这种对不上账的情况。JDBC 默认自动提交每次executeUpdate都是一个独立事务必须手动关掉。public boolean borrow(int bookId, int readerId) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关键关掉自动提交三条 SQL 捆成一个原子操作 // 1. 带条件的库存扣减stock 0 才更新得动天然挡住并发超借 try (PreparedStatement ps conn.prepareStatement( UPDATE book SET stock stock - 1 WHERE book_id ? AND stock 0)) { ps.setInt(1, bookId); if (ps.executeUpdate() 0) { conn.rollback(); return false; // 影响行数为 0 说明库存不足直接结束 } } // 2. 写借阅记录应还时间给 30 天 try (PreparedStatement ps conn.prepareStatement( INSERT INTO borrow_record(book_id, reader_id, borrow_time, due_time, state) VALUES(?, ?, NOW(), DATE_ADD(NOW(), INTERVAL 30 DAY), 0))) { ps.setInt(1, bookId); ps.setInt(2, readerId); ps.executeUpdate(); } // 3. 更新读者已借数量超过上限就整体回滚 try (PreparedStatement ps conn.prepareStatement( UPDATE reader SET borrowed borrowed 1 WHERE reader_id ? AND borrowed max_borrow)) { ps.setInt(1, readerId); if (ps.executeUpdate() 0) { conn.rollback(); return false; // 达到借阅上限 } } conn.commit(); return true; } catch (SQLException e) { rollbackQuietly(conn); // 异常务必回滚否则连接归还后脏数据还在 throw new RuntimeException(借书失败, e); } finally { closeQuietly(conn); // 关连接前先复位 autoCommit 由连接池负责 } }这段代码的核心技巧是把判断条件塞进UPDATE的WHERE里而不是先SELECT stock再判断再更新。后者在两个人同时借最后一本书时会双双读到stock 1然后都减成功库存变 -1。把stock 0写进WHEREMySQL 的行锁会保证只有一个UPDATE影响行数为 1另一个为 0业务上就是手慢了。这个写法不需要额外加锁也不需要SELECT ... FOR UPDATE性价比最高。还书是逆过程把记录的state改成 1、填return_time、库存加 1、读者已借数减 1同样一个事务包住。逾期判断不用定时任务在查询时用due_time NOW() AND state 0动态算出来展示就行真正要落状态再跑一个每日的批处理。5. 避坑与排查这套源码跑不起来时的五个高频现场5.1 现象控制台刷ClassNotFoundException: com.mysql.cj.jdbc.Driver原因基本是驱动 jar 没进WEB-INF/lib只加到了 IDE 的构建路径里。IDE 内部跑用的是它自己的 classpath打包部署到独立 Tomcat 时用的是lib目录两边不是一回事。解决办法是手动把mysql-connector-java-8.0.x.jar复制进WebContent/WEB-INF/lib然后在 IDE 里刷新项目并重新部署。如果用的是 5.7 的服务器却配了 8.0 的驱动类名报的也是这个错反过来同理改jdbc.properties里的jdbc.driver即可。判断依据很简单URL 里带serverTimezone就用com.mysql.cj.jdbc.Driver不带就用com.mysql.jdbc.Driver。5.2 现象页面中文全是问号数据库里存进去的也是问号三个地方的编码有一处没对齐就会这样JSP 页面没写% page contentTypetext/html;charsetUTF-8 %、请求体没设编码、数据库或表不是utf8mb4。乱码出现在提交后说明是请求侧出现在查询后说明是响应侧或存储侧。最省事的修法是加一个编码过滤器并且保证它是过滤器链上的第一个WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); // 只对 POST 请求体生效 resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }注意setCharacterEncoding对 GET 请求的查询串无效那个由 Tomcat 的server.xml里 Connector 的URIEncoding决定8.0 之后默认就是 UTF-8一般不用动。如果数据库层已经是乱码存量数据改配置救不回来只能重新导入脚本。5.3 现象登录成功跳到了列表页一刷新又弹回登录页原因通常是 Session 没存住。排查顺序是先看登录 Servlet 里是不是用了req.getSession()又没setAttribute再看过滤器是不是把重定向后的目标路径也拦了最后看浏览器是不是禁了 Cookie——JSESSIONID 默认靠 Cookie 传禁了就每次都是新会话。还有一种隐蔽情况项目部署在两台 Tomcat 上做负载Session 不共享用户每次请求落到不同实例就掉登录。单机部署不会遇到但如果你照着这套源码往生产搬这一点要提前想清楚后面第 6 章会给改造方向。5.4 现象借完书库存变成负数或者一本书被借了两次原因是先查再改的写法没加事务两个请求同时读到同样的库存值。也可能是事务开了但异常分支里忘了rollbackcatch块只打了日志就return连接被连接池回收时行为不可预期。解决就是把判断条件写进UPDATE ... WHERE stock 0用影响行数判断成败同时在catch里强制回滚。另外检查一下 MySQL 的存储引擎如果建表脚本里写的是ENGINEMyISAM事务根本不生效必须改成InnoDB。这个坑很隐蔽代码写得再对也没用因为引擎不支持。5.5 现象改了 JSP 或 Java 代码重启后页面还是老样子JSP 改完不生效多半是 Tomcat 的work目录里缓存了编译后的 Servlet清掉TOMCAT_HOME/work/Catalina/localhost/项目名再启动即可。Java 类改完不生效是 IDE 没有重新编译或者部署包没更新Eclipse 里检查 Project → Build Automatically 是否勾上IDEA 里看 Artifact 是不是设成了 exploded war。还有一种情况是浏览器缓存了 CSS 和 JS页面结构变了样式没变开无痕窗口验证一下就能区分。养成习惯改完代码先 CtrlF5 强刷再判断是不是服务端的问题能省掉一半的自我怀疑。6. 二次开发把它从「能跑」改成「敢用」这套源码跑通之后最大的价值是它足够简单任何一处改造都能立刻看到效果。但简单也意味着它离能给别人用还有距离下面是我拿到这类项目后固定要动的几刀。6.1 先堵密码这个最大的窟窿明文存密码是这类教学项目最普遍的问题改造成本却极低。库里加两个字段salt和password长度给 64注册或改密时生成随机盐存SHA-256(salt 明文)登录时用同样的规则算一遍再比对。// 改密时盐和摘要一起入库 String salt UUID.randomUUID().toString().replace(-, ); String hash sha256(salt plainPassword); // 登录时取出该用户的盐算完比对数据库里永远不出现明文 if (sha256(admin.getSalt() inputPwd).equals(admin.getPassword())) { /* 通过 */ }顺手把权限也补上。现在多半只有一张admin表加一个role字段区分超级管理员和普通管理员普通管理员隐藏删除按钮、后台接口里再校验一次。前端隐藏只是体验后端校验才是防线。6.2 补一层 Service 与连接池DAO 里每个方法都DriverManager.getConnection()意味着每次查询都建一次 TCP 连接压测下 QPS 上不去还容易把 MySQL 的连接数打满。换成 Druid 或 HikariCP只改DBUtil.getConnection()的实现上层代码一行不用动改造项改前改后收益连接获取DriverManager每次新建连接池getConnection()单次查询省 10~30ms事务边界散落在 Servlet收敛到 Service 方法异常回滚不漏密码存储明文加盐摘要库被拖走也不至于全裸分页总数每次COUNT(*)缓存或近似值大表翻页不卡再往下就是结构性的改动把 Servlet 里的业务判断抽到 Service让 Servlet 只负责取参数和转发把web.xml里的 Servlet 注册换成注解把 JSP 里的 Java 小脚本% %全部换成 JSTL 和 EL。这几步做完项目才算有了继续长大的骨架。我自己的习惯是每接一份这类源码第一步不是改功能而是先写一个能跑通登录—新增图书—借书—还书四条链路的冒烟脚本把改造前后的结果对一遍。有一次我图快直接改了库存扣减的 SQL 却没重跑还书流程上线后才发现还书时库存加不回去靠着借阅记录一条条手工对账才补平。从那以后我每次动到事务相关的代码都强制把借书、还书、超期这三条路径手工走一遍再提交。希望帮到你。本文还有配套的精品资源点击获取