简介:这是一份面向高校Java Web初学者的JSP课程设计实践资源,聚焦基础动态网页开发能力训练,适用于JSP+JavaBean+Access技术栈入门学习与课程作业参考。资源以简易留言本为核心功能,完整实现用户留言、UBB代码解析(含正则表达式实现)、头像表情支持、管理员登录(集成图形验证码)及MD5密码加密等典型Web模块,无需配置数据源即可在Tomcat中直接部署运行。压缩包共194个文件,含13个核心JSP页面、5个编译后Class文件(如MD5.class、UbbCode.class、FaceFilter.class等)、5个CSS样式表、4个Access数据库文件(.mdb)、4个JS脚本及大量GIF表情资源,整体仅621KB,轻量易用。目前已有157人学习下载,提供从页面布局、业务逻辑封装、数据库交互到安全防护的全流程代码范例,特别适合理解JavaBean分层思想、Access本地数据库集成及基础Web安全实践。
1. 项目概述:一个经典的JSP课程设计实战复盘
最近在整理旧硬盘,翻出来一个大学时期做的课程设计压缩包,名字就叫jspbook.rar。解压开来,一个典型的、带着浓厚时代气息的JSP项目跃然眼前。它几乎涵盖了那个年代学习Java Web开发的所有核心要素:JSP页面、JavaBean封装业务逻辑、Access数据库、原生的SQL操作,以及一个现在看来颇为简陋但五脏俱全的登陆模块。这不仅仅是一个作业,更像是一个时间胶囊,封装了早期Web开发的典型技术栈和设计思路。今天,我就以这个项目为蓝本,结合我后来十多年的开发经验,为你深度拆解一个完整的、可运行的JSP课程设计应该如何从零搭建,并剖析其中每一个技术点的现代意义与避坑指南。无论你是正在完成课设的学生,还是想了解传统MVC模式演变历史的开发者,这篇文章都能给你带来一次“考古”与“重构”并行的实战之旅。
2. 项目核心架构与设计思路拆解
2.1 技术栈选型背后的时代逻辑
这个项目采用了非常经典的JSP Model 1架构的变体。简单来说,就是JSP页面负责显示和接收用户请求,JavaBean(一个普通的Java类)作为模型(Model)封装数据和业务逻辑,然后通过JDBC直接操作Access数据库。为什么是这套组合?
首先,JSP(Java Server Pages)是当时Servlet技术的自然演进。它允许我们在HTML中直接嵌入Java代码(脚本片段<% ... %>),对于初学者来说,直观易懂,能够快速看到动态效果。项目里的login.jsp、showBook.jsp等文件就是典型代表。
其次,JavaBean的引入是迈向结构清晰的关键一步。在早期纯JSP脚本的混乱中,JavaBean承担了“模型”的角色。例如,一个UserBean可能封装了用户的用户名、密码属性和验证登录的方法。它的核心价值在于:1)数据封装:将零散的数据字段组织成一个对象;2)业务逻辑集中:将与用户相关的操作(如密码校验)从JSP页面剥离,提高了代码的可维护性。
再者,数据库选择Microsoft Access,这几乎是当时课程设计的“标配”。原因无他:轻量、无需独立安装数据库服务、与Windows系统集成好。对于一个小型的、单机演示的图书管理系统或学生信息管理系统来说,它足够简单。项目中的.mdb文件就是整个系统的数据仓库。
最后,直接使用JDBC执行SQL语句进行数据库交互。这是最原始也是最直接的数据持久化方式。你会看到代码中充斥着Class.forName("sun.jdbc.odbc.JdbcOdbcDriver")和DriverManager.getConnection这样的语句,通过Statement或PreparedStatement执行SELECT、INSERT等操作。
注意:这套技术栈在今天看来已显陈旧,尤其是Access和原生JDBC在Web项目中的使用。但理解它,你就理解了Web开发从“页面混合逻辑”到“分层架构”演进的第一步。现代框架如Spring MVC,其核心思想(MVC)在此已见雏形。
2.2 典型功能模块与数据流分析
基于jspbook.rar这个命名,我们可以合理推断这是一个与“图书”或“书籍”相关的管理系统。其核心功能模块和数据流大致如下:
用户认证模块:这是入口。
login.jsp页面提供表单,用户提交用户名和密码后,数据被提交到LoginServlet或直接由login_process.jsp处理。该处理页面会实例化一个UserBean,调用其验证方法,该方法内部通过JDBC连接Access数据库,执行SQL查询匹配用户记录。验证成功则跳转到主页面(如main.jsp),失败则返回登录页并提示错误。图书信息管理模块:这是核心业务。主页面可能包含图书的增删改查链接。
- 查(列表/详情):
showBook.jsp或bookList.jsp页面,会调用BookBean的getAllBooks()方法,该方法返回一个ArrayList<BookBean>,页面再用JSP脚本或JSTL循环展示。 - 增/删/改:会有对应的
addBook.jsp、editBook.jsp页面和deleteBook.jsp处理逻辑。表单数据提交后,由JavaBean封装并执行相应的SQLINSERT、UPDATE、DELETE语句。
- 查(列表/详情):
数据流转示意图(以登录为例):
浏览器 -> 提交表单 -> login.jsp (视图View) | v login_process.jsp (控制器Controller雏形) | v 实例化 UserBean (模型Model) | v UserBean.checkUser() 方法 | v 内部:JDBC连接 -> Access数据库执行SQL | v 返回结果 true/false | v login_process.jsp 根据结果跳转 (控制流转)这个流程清晰地展示了数据从视图到模型,再返回视图的过程,虽然控制器角色(由JSP兼任)并不纯粹,但分层的思想已经存在。
3. 环境搭建与关键技术点实操解析
3.1 开发环境复原与配置要点
要运行或学习这样一个项目,你需要搭建一个特定的环境。这里我推荐使用Eclipse IDE for Enterprise Java Developers配合Apache Tomcat服务器,这最接近当年的经典组合。
JDK配置:安装JDK 8(Java 8)是一个安全的选择,它对旧项目兼容性最好。配置好
JAVA_HOME环境变量。集成开发环境(IDE):安装Eclipse后,需要安装用于Web开发的插件(如WTP,通常企业版已内置)。创建一个“Dynamic Web Project”项目,这是运行JSP的基础。
Web服务器部署:将Tomcat服务器集成到Eclipse中。关键一步是将你的项目添加到Tomcat的部署列表。右键项目 -> Properties -> Project Facets,确保勾选了“Dynamic Web Module”和“Java”。然后将项目
Add and Remove...到Tomcat服务器上。Access数据库连接配置(核心难点):这是最容易出问题的地方。Access需要通过JDBC-ODBC桥连接,而高版本Windows(如Win10/11)和JDK已不再默认支持。
- 方案一(传统,可能失效):理论上,需要在Windows的ODBC数据源管理器中配置一个系统DSN,指向你的
.mdb文件。JDBC URL类似:jdbc:odbc:Driver={Microsoft Access Driver (*.mdb)};DBQ=你的数据库路径.mdb。 - 方案二(推荐,使用UCanAccess驱动):放弃旧的桥接方式,使用纯Java的JDBC驱动。这是一个现代解决方案。
- 下载
ucanaccess的jar包集合(通常包括ucanaccess-x.x.x.jar,jackcess-x.x.jar,hsqldb.jar,commons-lang3.jar,commons-logging.jar)。 - 将这些jar包全部放入你项目的
WebContent/WEB-INF/lib目录下。 - 连接代码示例:
Class.forName("net.ucanaccess.jdbc.UcanaccessDriver"); Connection conn = DriverManager.getConnection("jdbc:ucanaccess://C:/path/to/your/jspbook.mdb"); - 下载
- 将数据库文件放在正确位置:通常将
.mdb文件放在项目源码目录下(如WebContent/WEB-INF或项目根目录),然后在代码中使用相对路径或通过ServletContext.getRealPath()方法获取其绝对路径。
- 方案一(传统,可能失效):理论上,需要在Windows的ODBC数据源管理器中配置一个系统DSN,指向你的
实操心得:在Eclipse中,经常遇到修改了JSP页面但刷新浏览器看不到变化的情况。这通常是因为Eclipse没有自动将修改同步到Tomcat的部署目录(wtpwebapps)。解决方法是:在Servers视图中,右键Tomcat服务器,选择“Clean...”,然后“Publish”。或者,更彻底地,在项目上右键 -> Refresh。
3.2 JavaBean的设计与编写规范
JavaBean在这个项目中是灵魂。一个设计良好的JavaBean应遵循以下规范:
- 属性私有化:所有字段(如
bookId,bookName)使用private修饰。 - 提供公共的Getter/Setter方法:这是JavaBean的约定,便于JSP页面使用
<jsp:useBean>和<jsp:getProperty>标签访问。 - 实现
Serializable接口:虽然对于简单应用不是必须,但这是一个好习惯,意味着对象可以被序列化。 - 包含无参构造方法:JSP标签实例化Bean时需要。
示例:BookBean.java
public class BookBean implements Serializable { private String id; private String name; private String author; private double price; // 无参构造 public BookBean() {} // 带参构造(方便使用) public BookBean(String id, String name, String author, double price) { this.id = id; this.name = name; this.author = author; this.price = price; } // Getter and Setter 方法 public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } // ... 其他属性的getter/setter // 业务方法:例如,获取所有图书列表 public static ArrayList<BookBean> getAllBooks(Connection conn) throws SQLException { ArrayList<BookBean> list = new ArrayList<>(); String sql = "SELECT * FROM books"; try (Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery(sql)) { while (rs.next()) { BookBean book = new BookBean( rs.getString("id"), rs.getString("name"), rs.getString("author"), rs.getDouble("price") ); list.add(book); } } return list; } }在JSP页面中,可以这样使用:
<jsp:useBean id="book" class="com.yourpackage.BookBean" scope="page"/> <jsp:setProperty name="book" property="*"/> <%-- 自动匹配同名表单参数 --%> 图书名称:<jsp:getProperty name="book" property="name"/>3.3 JSP页面的结构、脚本与内置对象
JSP页面混合了HTML和Java代码。理解其构成至关重要。
指令(Directive):通常位于页面顶部,如
<%@ page contentType="text/html;charset=UTF-8" language="java" %>定义页面属性。<%@ include file="header.jsp" %>用于静态包含。声明(Declaration):
<%! ... %>,用于声明成员变量或方法。但强烈不建议在JSP中声明方法,这违背了分离原则,应放在JavaBean中。脚本片段(Scriptlet):
<% ... %>,里面是纯Java代码。这是旧项目中最常见的部分,用于写业务逻辑。例如,在showBook.jsp中循环显示图书列表:<% Connection conn = ... // 获取数据库连接 ArrayList<BookBean> bookList = BookBean.getAllBooks(conn); for (BookBean book : bookList) { %> <tr> <td><%= book.getId() %></td> <%-- 表达式,输出值 --%> <td><%= book.getName() %></td> </tr> <% } conn.close(); %>表达式(Expression):
<%= ... %>,用于输出变量或表达式的值到HTML中。JSP内置对象:这是JSP的便利之处,无需声明即可使用。
request:代表HTTP请求,常用request.getParameter("name")获取表单数据。response:代表HTTP响应,用于重定向response.sendRedirect("url")。session:用户会话,用于在多次请求间保存用户信息,如session.setAttribute("user", username)。application:Servlet上下文,整个Web应用共享。out:输出流对象,相当于response.getWriter()。
注意事项:过度使用脚本片段(
<% ... %>)会导致JSP页面难以维护,这就是所谓的“JSP Model 1”的弊端。现代最佳实践是使用JSTL标签和EL表达式来替代绝大多数脚本片段,使页面更接近纯展示层。例如,上面的循环用JSTL可以写成:<c:forEach var="book" items="${bookList}"> <tr><td>${book.id}</td><td>${book.name}</td></tr> </c:forEach>清晰且无Java代码。
4. 数据库操作:从Access连接到SQL安全
4.1 Access数据库的连接、查询与事务处理
如前所述,使用UCanAccess驱动是现代连接Access的可靠方式。这里详细说明数据库操作的全过程。
1. 建立连接与资源管理数据库连接是昂贵资源,必须确保在使用后关闭。推荐使用try-with-resources语法(Java 7+),它可以自动关闭实现了AutoCloseable接口的资源。
public Connection getConnection() throws ClassNotFoundException, SQLException { // 驱动加载(高版本JDBC可省略Class.forName,但显式加载更清晰) Class.forName("net.ucanaccess.jdbc.UcanaccessDriver"); // 假设数据库文件放在 WEB-INF/db 目录下 String dbPath = getServletContext().getRealPath("/WEB-INF/db/jspbook.mdb"); String url = "jdbc:ucanaccess://" + dbPath; return DriverManager.getConnection(url); } // 在JSP或Servlet中使用 try (Connection conn = getConnection(); Statement stmt = conn.createStatement(); ResultSet rs = stmt.executeQuery("SELECT * FROM users")) { while (rs.next()) { // 处理结果集 } } catch (Exception e) { e.printStackTrace(); }2. 使用PreparedStatement防止SQL注入并提升性能绝对不要使用字符串拼接来构造SQL语句,这是SQL注入攻击的根源。PreparedStatement是必须掌握的安全操作方式。
// 用户登录验证 String sql = "SELECT * FROM users WHERE username = ? AND password = ?"; try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, request.getParameter("username")); pstmt.setString(2, request.getParameter("password")); // 注意:密码应加密存储,此处仅为演示 try (ResultSet rs = pstmt.executeQuery()) { if (rs.next()) { // 登录成功 session.setAttribute("user", rs.getString("username")); } else { // 登录失败 } } }3. 简单的事务处理对于需要保证原子性的操作(如转账、同时插入多张表),需要使用事务。
Connection conn = null; try { conn = getConnection(); conn.setAutoCommit(false); // 关闭自动提交,开启事务 PreparedStatement pstmt1 = conn.prepareStatement("UPDATE account SET balance = balance - ? WHERE id = ?"); PreparedStatement pstmt2 = conn.prepareStatement("UPDATE account SET balance = balance + ? WHERE id = ?"); // 设置参数... pstmt1.executeUpdate(); pstmt2.executeUpdate(); conn.commit(); // 提交事务 } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } // 回滚事务 } e.printStackTrace(); } finally { if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }4.2 登陆模块的详细实现与会话管理
登陆模块是Web应用的守门人,其实现需要兼顾功能、安全和用户体验。
1. 数据库表设计一个最简单的用户表可能如下:
CREATE TABLE users ( id INT PRIMARY KEY AUTOINCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, -- 注意:长度要足够存储加密后的密码 email VARCHAR(100) );2. 密码的安全存储切勿明文存储密码!必须使用哈希算法进行加密。MD5和SHA-1已不安全,推荐使用BCrypt或SHA-256加盐。
// 使用Apache Commons Codec库进行SHA-256加盐哈希示例 import org.apache.commons.codec.digest.DigestUtils; import java.security.SecureRandom; import java.util.Base64; public class PasswordUtil { public static String generateSalt() { SecureRandom random = new SecureRandom(); byte[] salt = new byte[16]; random.nextBytes(salt); return Base64.getEncoder().encodeToString(salt); } public static String hashPassword(String password, String salt) { // 将盐和密码组合后哈希 String combined = salt + password; return DigestUtils.sha256Hex(combined); } // 验证密码 public static boolean verifyPassword(String inputPassword, String storedSalt, String storedHash) { String inputHash = hashPassword(inputPassword, storedSalt); return inputHash.equals(storedHash); } }在注册时,生成一个随机的盐(salt),计算hashPassword(明文密码, 盐),将盐和哈希值一起存入数据库。登录时,取出该用户的盐,计算输入密码的哈希值,与数据库存储的哈希值比对。
3. 登陆流程JSP实现login.jsp(视图):
<form action="loginCheck.jsp" method="post"> 用户名:<input type="text" name="username"><br> 密码:<input type="password" name="password"><br> <input type="submit" value="登录"> </form>loginCheck.jsp(控制器/处理逻辑):
<% String username = request.getParameter("username"); String inputPassword = request.getParameter("password"); // 1. 参数校验 if(username == null || inputPassword == null || username.trim().isEmpty()){ response.sendRedirect("login.jsp?error=empty"); return; } // 2. 查询数据库 String sql = "SELECT password_hash, salt FROM users WHERE username = ?"; try (Connection conn = getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, username); ResultSet rs = pstmt.executeQuery(); if (rs.next()) { String storedHash = rs.getString("password_hash"); String storedSalt = rs.getString("salt"); // 3. 验证密码 if (PasswordUtil.verifyPassword(inputPassword, storedSalt, storedHash)) { // 4. 登录成功,创建会话 session.setAttribute("currentUser", username); // 5. 重定向到主页,使用重定向而非转发,避免重复提交 response.sendRedirect("main.jsp"); } else { response.sendRedirect("login.jsp?error=invalid"); } } else { response.sendRedirect("login.jsp?error=notfound"); } } catch (SQLException e) { e.printStackTrace(); response.sendRedirect("login.jsp?error=system"); } %>4. 会话管理与权限控制登录成功后,用户信息被存入session。在其他需要权限的页面(如main.jsp),必须在开头进行会话检查:
<% String user = (String) session.getAttribute("currentUser"); if (user == null) { // 用户未登录,跳转回登录页 response.sendRedirect("login.jsp"); return; // 重要:立即终止后续页面渲染 } %> <!-- 这里是受保护的主页内容 --> 欢迎您,<%= user %>!退出登录时,需要销毁会话:
<% session.invalidate(); // 销毁当前session response.sendRedirect("login.jsp"); %>5. 课程设计项目扩展与常见问题深度排查
5.1 从课程设计到更健壮的系统:改进建议
如果你基于此项目进行扩展或重构,以下建议可以大幅提升其代码质量和可维护性:
引入Servlet作为真正的控制器:将
loginCheck.jsp、addBook.jsp中的处理逻辑全部迁移到Servlet中。JSP只负责显示。这是向Model 2(MVC)架构演进的关键一步。例如,创建LoginServlet,在doPost方法中处理登录逻辑,最后通过RequestDispatcher转发到结果JSP页面或使用sendRedirect。使用连接池管理数据库连接:频繁创建和关闭连接非常消耗资源。可以使用像Apache DBCP或HikariCP这样的连接池。在Servlet的
init()方法中初始化连接池,在整个应用中共享。采用DAO模式分层:将数据库操作进一步抽象。创建
UserDao、BookDao接口及其实现类,专门负责CRUD操作。Service层调用DAO,Servlet调用Service。这样结构更清晰,便于测试和维护。用JSTL+EL替换JSP脚本:彻底清除JSP页面中的
<% ... %>和<%= ... %>。使用JSTL核心标签库(c:forEach,c:if)和EL表达式(${})来展示数据和控制逻辑,使JSP页面真正成为干净的视图。增加前端验证与用户体验:在表单提交前使用JavaScript进行非空、格式等基本验证。在后台处理失败时,使用
request.setAttribute("errorMsg", "错误信息")传递错误信息,并在JSP页面中显示,而不是简单重定向。
5.2 高频问题与故障排查实录
在开发和运行这类老式JSP项目时,你几乎一定会遇到下面这些问题。这里是我的排查笔记:
问题1:启动Tomcat后,访问JSP页面出现“HTTP Status 404 - Not Found”
- 可能原因:项目未成功部署到Tomcat。
- 排查步骤:
- 检查Eclipse的Servers视图,你的项目是否在Tomcat服务器下?如果没有,右键服务器 -> Add and Remove... 添加项目。
- 检查项目部署路径。在Servers视图双击Tomcat,查看“Server Locations”和“Deploy Path”。确保项目被部署到了正确的
webapps或wtpwebapps目录。 - 检查URL是否正确。如果项目上下文路径(Context Root)是
/jspbook,那么访问地址应为http://localhost:8080/jspbook/login.jsp。
问题2:JSP页面显示乱码
- 可能原因:字符编码不统一。
- 解决方案:
- 确保每个JSP页面顶部有:
<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8"%>。 - 对于POST请求,在获取参数前设置请求编码:
request.setCharacterEncoding("UTF-8");。注意:此方法对GET请求无效,需要在Tomcat的server.xml中配置URIEncoding="UTF-8"。 - 数据库连接字符串中指定编码(如果数据库支持),如使用MySQL时:
jdbc:mysql://...?useUnicode=true&characterEncoding=UTF-8。
- 确保每个JSP页面顶部有:
问题3:Java.lang.ClassNotFoundException: sun.jdbc.odbc.JdbcOdbcDriver
- 原因:高版本JDK(通常从JDK 8开始)移除了JDBC-ODBC桥。
- 解决:放弃ODBC桥接方案,采用前文推荐的UCanAccess驱动。
问题4:数据库连接成功,但执行查询时出现“操作必须使用一个可更新的查询”或“No current record”等错误
- 可能原因:
- 数据库文件路径问题:Access数据库文件路径包含空格或中文,或者进程没有写入权限。确保路径正确且应用有读写权限。
- 数据库被独占锁定:可能是Access文件正在被Microsoft Access程序打开。关闭所有打开该文件的程序。
- UCanAccess驱动版本或依赖不全:确保引入了所有必需的jar包。
- 排查:将数据库文件复制到一个简单的英文路径(如
C:/temp/book.mdb)再尝试连接。
问题5:登录成功后,Session信息在跳转后丢失
- 可能原因:
- 浏览器Cookie被禁用:Session依赖Cookie(JSESSIONID),检查浏览器设置。
- URL重写问题:如果跳转时使用了
response.encodeRedirectURL()或response.encodeURL(),而你的Tomcat配置了<Context path="" ...>导致Session ID无法通过Cookie传递,且URL重写未生效。确保跳转使用的是response.sendRedirect("main.jsp"),并且Tomcat配置正常。 - Session超时:在
web.xml中检查<session-config>下的<session-timeout>(单位:分钟)。调试时可设置长一些。
问题6:JSP页面中无法使用EL表达式(${}不解析,原样输出)
- 原因:Servlet 2.3 / JSP 1.2之前的规范默认EL表达式是关闭的,或者页面指令禁用了EL。
- 解决:
- 确保
web.xml头部的声明是Servlet 2.4及以上版本。 - 在JSP页面顶部添加指令启用EL:
<%@ page isELIgnored="false" %>。
- 确保
问题速查表
| 现象 | 可能原因 | 快速排查/解决方向 |
|---|---|---|
| 404错误 | 项目未部署,URL错误 | 检查Servers视图,检查项目上下文路径,检查URL拼写 |
| 500错误(JSP编译) | JSP语法错误,Java类找不到 | 查看Tomcat日志(catalina.out或localhost日志),定位具体错误行 |
| 500错误(运行时) | 空指针,数据库连接失败,SQL错误 | 查看控制台异常堆栈信息,检查数据库连接参数,检查SQL语句 |
| 中文乱码 | 编码不一致 | 统一设置为UTF-8:JSP页面、request、response、数据库连接 |
| ClassNotFoundException | 缺少驱动jar包,驱动类名错误 | 检查WEB-INF/lib下是否有对应jar,检查Class.forName的字符串 |
| 数据库连接失败 | 驱动不对,URL格式错误,文件路径错,无权限 | 换用UCanAccess,检查URL格式,检查.mdb文件路径和权限 |
| SQL注入漏洞 | 使用Statement拼接SQL | 全部改用PreparedStatement,并使用参数化查询 |
| 修改JSP不生效 | 缓存问题 | 清理Tomcat工作目录,重启服务器,或使用<%= new java.util.Date() %>测试 |
回顾这个“jspbook”项目,它像一本老旧的编程教科书,虽然纸张泛黄,但基础概念历久弥新。通过亲手搭建、调试和扩展这样一个系统,你能深刻理解Web请求的生命周期、数据如何从表单流向数据库再渲染回页面、会话如何维持状态,以及为什么我们需要MVC、DAO这些设计模式。这些理解,是日后学习Spring Boot、MyBatis等现代框架最扎实的地基。如果你在复现过程中卡在了某个环节,别急着搜索,多看看Tomcat的控制台日志,那里通常藏着最直接的答案。编程的很多经验,正是在解决这些看似琐碎的“坑”的过程中积累起来的。
本文还有配套的精品资源,点击获取