资讯动态

学生成绩管理系统实战:Servlet+JSP+MySQL+JDBC开发全解析

发布时间:2026/9/12 2:28:17 来源:尧图企业网站定制
简介基于Java技术栈的学生成绩管理系统项目整合Servlet、JSP、Bootstrap与MySQL适合Java Web初学者及毕业设计参考。系统覆盖学生信息维护、成绩录入、成绩查询、统计分析等典型功能并体现MVC分层思想。资源打包为zip格式共575个文件大小9.17MB。其中85个Java源文件与170个class文件对应业务逻辑与编译结果74个JSP页面负责动态展示122个JS、56个CSS及Bootstrap组件完善前端交互12个jar包提供依赖支持另有SQL脚本可初始化数据库。包内还包含9个XML配置与3个properties文件便于理解Web应用配置TTF、WOFF、SVG、PNG等前端资源用于支撑页面图标与字体样式。已有3115人学习下载。项目代码结构清晰包含DAO层、Servlet控制层与JSP视图层可直接导入IDE运行适合用于课程设计、期末项目或系统学习Java Web开发全流程。1. 学生成绩管理系统还在用 Servlet 组合栈说明什么在网上搜“学生成绩管理系统”会得到一堆 Spring Boot Vue 的现成架子但真正被课程设计、本科毕业设计和企业内部老旧系统选中的恰恰是 Java Servlet JSP Bootstrap MySQL 这套组合。原因不复杂它把从前端页面到数据库表的整条链路全部摊开没有 MyBatis 帮你拼 SQL没有 Spring MVC 帮你隐式传参每一个 HTTP 请求怎么进来、参数怎么取、数据库连接怎么开、结果怎么渲染都直接写在代码里。作为学习者能依靠这套系统理解 Web 应用最底层的请求—响应模型作为维护者改一个字段名不需要顺着三层注解追半天。本文要讲的就是这样一个系统的落地过程从表结构设计、JDBC 封装、Servlet 路由到 JSP 渲染和 Bootstrap 布局再到上线前常见的连接池、中文乱码、404 路径问题全部按可复现的方式走一遍。适合刚学完 Java SE 想做完整 Web 项目的人也适合接手此类老旧系统时快速理清整改路径的从业者。2. 环境搭建与 MySQL 表结构先定数据模型再写代码2.1 开发环境版本选择JDK 8 和 Tomcat 8.5 为什么是稳妥起点学生成绩管理系统这种三层结构的传统项目对环境版本的敏感度比 Spring Boot 项目还高。JDK 8 是兼容性最好的基准Tomcat 则建议选 8.5 或 9.0这两个版本支持 Servlet 3.1/4.0 规范恰好覆盖 JSP EL 表达式和 Servlet 注解开发的所有需求。如果你装了 JDK 17 再配上 Tomcat 10会遇到javax.servlet包名整体变成jakarta.servlet的问题教材里几乎所有示例代码都要改 import对做课设的人来说得不偿失。Eclipse 用户创建 Dynamic Web Project 时Target Runtime 一栏直接选 Tomcat 8.5Dynamic web module version 保持 3.1这样生成的 web.xml 头文件不会引入不兼容的 schema。这里常见的整条链路问题来自 IDE 内置浏览器缓存JSP 改动后 Tomcat 自动重编译但浏览器还在读旧 CSS后面排错章专门讲这个问题。2.2 MySQL 建库建表成绩表怎么设计才能避免数据冗余成绩管理系统的核心是学生、课程、成绩三张表但很多初版设计会把学生姓名和课程名称直接冗余到成绩表里。表面上看查询方便实际上只要学生改名虽然少见或课程改名就要 UPDATE 成绩表里所有行。我一般这样设计CREATE DATABASE IF NOT EXISTS student_score DEFAULT CHARSET utf8mb4; USE student_score; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL, class_name VARCHAR(50), gender ENUM(男, 女) DEFAULT 男, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB; CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_code VARCHAR(20) NOT NULL UNIQUE, course_name VARCHAR(50) NOT NULL, credit DECIMAL(3,1) DEFAULT 2.0 ) ENGINEInnoDB; CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,1) NOT NULL, exam_date DATE, UNIQUE KEY uk_stu_course (student_id, course_id), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(id), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINEInnoDB;这段建表 SQL 里有三个值得注意的设计选择。第一score表里不存学生姓名和课程名只存两个外键 ID查询时用 JOIN 把名字带出来第二UNIQUE KEY uk_stu_course保证同一个学生同一门课只有一条成绩记录这是应用层防止重复录入的底线第三DECIMAL(5,1)比 FLOAT/DOUBLE 更适合存成绩因为浮点数在比较和求和时会出现 89.9999 这类精度误差。ENGINEInnoDB是硬性要求外键约束需要事务和行级锁支持MyISAM 在这套系统里没有使用场景。2.3 MySQL 驱动与 JDBC 连接别把密码写在每个 DAO 里需要准备好 MySQL 的 JDBC 驱动 jar 包通常使用mysql-connector-java不同版本对应不同 MySQL Server。把驱动 jar 放进WEB-INF/lib目录后写一个统一的连接工具类是这套项目的规范做法public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/student_score ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(MySQL Driver not found); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (ps ! null) { try { ps.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }这段代码的要点集中在 URL 参数上。serverTimezoneAsia/Shanghai解决 MySQL 8.0 的时区校验问题不带这个参数会报The server time zone value错误characterEncodingutf8配合数据库的utf8mb4字符集让中文姓名从 JDBC 到 Servlet 再到 JSP 的编码链路保持统一。密码硬编码在这里是课程设计的常态但我建议把 DB 配置抽到db.properties里通过Properties.load()读取这样后续改库密码不必重新编译 Java 源码。关于连接管理DriverManager.getConnection()每次调用都新建物理连接Tomcat 层面更合理的方案是配置 JNDI DataSource后面第 5 章展开说。3. Servlet 路由与业务逻辑把请求分发给对应的处理器3.1 用 WebServlet 注解代替 web.xml 映射的好处传统 Servlet 项目里每个 Servlet 都要在 web.xml 中注册网页多了之后 web.xml 会变成一长串servlet与servlet-mapping的配对。Servlet 3.0 之后可以用WebServlet(/student/list)直接挂在类上Tomcat 启动时自动扫描注解完成注册。学生成绩管理系统建议按资源类型拆 ServletStudentServlet负责学生增删改查CourseServlet负责课程管理ScoreServlet负责成绩录入和统计。每个 Servlet 内部通过action参数区分具体操作WebServlet(/student) public class StudentServlet extends HttpServlet { private StudentDAO studentDAO new StudentDAO(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (action null) action list; switch (action) { case edit: showEditPage(req, resp); break; case delete: deleteStudent(req, resp); break; default: listStudents(req, resp); } } Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (save.equals(action)) { saveStudent(req, resp); } } private void listStudents(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { ListStudent list studentDAO.findAll(); req.setAttribute(studentList, list); req.getRequestDispatcher(/jsp/student_list.jsp).forward(req, resp); } }关于 GET 和 POST 的分工这里有一个典型的设计取舍查询、删除这类写操作放在 GET 里确实违背了 REST 语义删除操作应该用 POST但课程设计系统追求的是页面跳转简单a hrefstudent?actiondeleteid3直接就能触发不需要写表单。实际生产环境必须把删除改成 POST 确认框否则搜索引擎爬虫或预加载插件会误删数据。req.getRequestDispatcher(...).forward()与resp.sendRedirect()的区别在于forward 是服务器内部跳转地址栏不变request 域里的 Attribute 还能继续读重定向是二次请求必须通过 URL 参数或 Session 传递数据。表单提交成功后我建议用重定向避免刷新页面时重复提交表单。3.2 DAO 层用 PreparedStatement 防 SQL 注入成绩管理系统的查询条件经常是动态拼接的比如按学生姓名模糊查询、按班级筛选、按分数区间过滤。最容易写错的方式是String sql SELECT * FROM student WHERE name LIKE % keyword %;这种字符串拼接会把单引号变成 SQL 语法的分界符输入 OR 11就能把整个表导出来。正确写法是使用带占位符的PreparedStatementpublic ListStudent searchByName(String keyword) { ListStudent result new ArrayList(); String sql SELECT s.id, s.student_no, s.name, s.class_name FROM student s WHERE s.name LIKE ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Student stu new Student(); stu.setId(rs.getInt(id)); stu.setStudentNo(rs.getString(student_no)); stu.setName(rs.getString(name)); stu.setClassName(rs.getString(class_name)); result.add(stu); } } } catch (SQLException e) { e.printStackTrace(); } return result; }代码里用了 Java 7 的 try-with-resources 语法Connection、PreparedStatement、ResultSet声明在 try 括号内方法结束后自动 close不再需要手动在 finally 里判空关闭。LIKE ?配合ps.setString(1, % keyword %)通配符作为参数值传入而不是拼进 SQL 文本这是防注入的关键。注意 MySQL 的LIKE对中文索引不友好数据量过万后建议把搜索需求拆成前缀匹配或改用全文索引这套课设系统的数据量一般碰不到这层瓶颈。3.3 成绩统计的 SQL一次查出平均分、最高分和及格率成绩模块比学生、课程模块多一个统计需求点击某个学生时要展示其所有选课成绩同时显示已修课程平均分点击某门课时要展示选课人数、最高分、最低分、平均分和不及格人数。逐条查出成绩再在 Java 里做统计是可行的但一个GROUP BY就能完成的事不该写几十行循环SELECT c.course_name, COUNT(sc.id) AS total_students, MAX(sc.score) AS max_score, MIN(sc.score) AS min_score, ROUND(AVG(sc.score), 1) AS avg_score, SUM(CASE WHEN sc.score 60 THEN 1 ELSE 0 END) AS fail_count FROM course c LEFT JOIN score sc ON c.id sc.course_id GROUP BY c.id, c.course_name;这里用LEFT JOIN而不是INNER JOIN是为了把还没人选修的课程也列出来统计字段显示 NULL 或 0。SUM(CASE WHEN ...)是行转列的经典写法比查出所有成绩后在 Java 里if (score 60) failCount干净得多。ROUND(AVG(sc.score), 1)保留一位小数这个精度在成绩单上已经够看。对应这个查询的 DAO 方法返回值应该是一个MapString, Object列表字段名直接对应 SQL 里的别名JSP 页面用scoreStat.key取值时不会因为大小写问题找漏。4. JSP 页面渲染与 Bootstrap 集成后端拼好数据前端负责分层4.1 JSP 中的 EL 表达式和 JSTL替代 Java 脚本片段很多初学者写 JSP 喜欢在%%里嵌 Java 代码比如循环输出学生表格。这种页面在 Tomcat 编译时会有性能损耗且页面里 Java 源码和 HTML 混杂后续调整样式时会看得头大。JSP 2.0 之后的标准做法是 EL 表达式取变量、JSTL 标签库控制循环和条件% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % !DOCTYPE html html langzh-CN head meta charsetUTF-8 title学生列表/title link relstylesheet href${pageContext.request.contextPath}/bootstrap-3.4.1/css/bootstrap.min.css /head body div classcontainer h2学生信息管理/h2 div classrow div classcol-md-8 form classform-inline methodget action${pageContext.request.contextPath}/student input typehidden nameaction valuesearch div classform-group input typetext classform-control namekeyword placeholder学生姓名 /div button typesubmit classbtn btn-primary搜索/button /form /div /div table classtable table-striped table-bordered table-hover thead tr th学号/thth姓名/thth班级/thth操作/th /tr /thead tbody c:forEach items${studentList} varstu tr td${stu.studentNo}/td td${stu.name}/td td${stu.className}/td td a href${pageContext.request.contextPath}/student?actioneditid${stu.id} classbtn btn-sm btn-warning编辑/a a href${pageContext.request.contextPath}/student?actiondeleteid${stu.id} classbtn btn-sm btn-danger onclickreturn confirm(确认删除该学生);删除/a /td /tr /c:forEach /tbody /table /div /body /html页面要点有三个。第一所有资源路径都用${pageContext.request.contextPath}作为前缀这样项目部署到 Tomcat 的任意名字的 context 下CSS、图标、链接都不会因为多一层路径而 404。第二${stu.studentNo}这种写法遵循 JavaBean 命名规范自动调用getStudentNo()方法如果实体类的属性是studentNo那么 JSP 里的 EL 变量名不区分大小写直接对应属性名。第三表单和链接的 action 都指向/student这个 Servlet 路径而之前的/student/list这种映射在本项目里用不了因为 Servlet 的WebServlet只注册了一个同名路径。4.2 表单回显和编辑页面老数据怎么重新填进输入框编辑学生信息是成绩管理系统里最典型的回显场景。点击列表中的“编辑”按钮后Servlet 查出这条学生记录setAttribute 放进 requestforward 到student_edit.jsp。页面上的输入框需要用 EL 表达式设置value属性form methodpost action${pageContext.request.contextPath}/student input typehidden nameaction valuesave input typehidden nameid value${student.id} div classform-group label学号/label input typetext classform-control namestudentNo value${student.studentNo} /div div classform-group label姓名/label input typetext classform-control namename value${student.name} /div button typesubmit classbtn btn-success保存/button a href${pageContext.request.contextPath}/student classbtn btn-default返回列表/a /form这个页面里的两个隐藏域是整个编辑功能的关键。nameid的 hidden input 保证保存时知道更新的是哪一行nameaction的 hidden input 让同一个 Servlet 的 doPost 知道这是新增还是修改。value${student.id}在新增页面会输出空字符串因为新增时 request 里没有student这个属性EL 对不存在的属性返回 null 然后输出空串。处理保存的 Servlet 代码里要判断id参数是否为空从而决定执行 INSERT 还是 UPDATE这是很易错的分支逻辑。4.3 Bootstrap 表格和弹窗把确认框和模态框用对如果只是把 Bootstrap 的 CSS 引进来然后继续用浏览器原生confirm()那 Bootstrap 只发挥了一半价值。成绩管理系统常见的使用场景是从成绩列表点击“录入成绩”按钮弹出一个模态框里面包含学生下拉框、课程下拉框、分数输入框。这个模态框需要 Bootstrap 的 JavaScript 组件驱动!-- 模态框触发按钮 -- button typebutton classbtn btn-primary>WebFilter(/*) public class EncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }这段过滤器放在 Servlet 之前执行对所有请求做编码统一处理。req.setCharacterEncoding(UTF-8)只对请求体的 POST 参数生效URL 路径里带的 GET 参数编码由 Tomcat 的URIEncodingUTF-8属性控制需要在server.xml里改 Connector 配置。第二处乱码如果发生在 AJAX 返回 JSON 时需要多写一行resp.setContentType(application/json;charsetUTF-8)否则浏览器按页面编码解析会读出鎴愮哗这类字符。第三处乱码根源通常是 JDBC URL 没加characterEncodingutf8前面 DBUtil 里已经处理。5. 页面布局、分页查询和请求流程的调整细节5.1 用 Bootstrap 导航条把所有功能入口串起来成绩管理系统的功能模块如果直接堆成链接列表用户在浏览器里看到的界面会比较混乱。常见做法是使用 Bootstrap 导航条作为整体框架上面放学生管理、课程管理、成绩管理三个一级菜单每个菜单通过下拉二级菜单展开具体操作。这个导航条放在header.jsp里所有页面用% include fileheader.jsp %静态引入。nav classnavbar navbar-default div classcontainer-fluid div classnavbar-header a classnavbar-brand href${pageContext.request.contextPath}/index.jsp学生成绩管理系统/a /div ul classnav navbar-nav li classactivea href${pageContext.request.contextPath}/student学生管理/a/li lia href${pageContext.request.contextPath}/course课程管理/a/li lia href${pageContext.request.contextPath}/score成绩管理/a/li /ul ul classnav navbar-nav navbar-right lia href${pageContext.request.contextPath}/logout.jsp退出/a/li /ul /div /nav所有页面复用同一个导航条可以保证菜单高亮逻辑一致但注意这里的高亮是写死在classactive里的如果访问成绩页面学生管理菜单仍然高亮。改进方案是在每个 JSP 顶部加pageContext.setAttribute(activeMenu, score)然后导航条中每个li用 EL 判断输出 active。静态 include 和动态 include 的选择值得提一句% include %在编译期把文件内容嵌入当前 JSP性能好但没法传参数jsp:include在运行期动态执行目标 JSP适合带参数引入片段比如 header 里要显示当前登录用户的信息时——这必须依赖sessionScope.user动态决定显示什么。5.2 分页的 SQL 和 JSP 交互LIMIT 子句的边界用法学生表数据超过 200 行时列表页全量加载已经不舒服超过了 1000 行会让浏览器出现明显的卡顿。分页是成绩系统的常见加分项。最简单可靠的分页方案仍然是 MySQL 的LIMIT子句配合两个参数offset, count。假设每页显示 10 条当前第 3 页的 offset 是(3-1)*10 20public ListStudent findPage(int page, int pageSize) { String sql SELECT id, student_no, name, class_name FROM student ORDER BY id LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, (page - 1) * pageSize); ps.setInt(2, pageSize); // 执行查询并封装对象 } } public int countAll() { String sql SELECT COUNT(*) FROM student; // 执行查询返回总数 }LIMIT的第一个参数从 0 开始所以第 1 页的 offset 是 0 而不是 10。JSP 分页条可以用一端判断上一页/下一页是否可用。如果查询条件带WHERE name LIKE ?分页 SQL 必须复用相同的 WHERE 条件COUNT(*)也要用相同条件查询才能算出正确总页数。很多初版代码把 search 和分页分开实现搜索之后点击第 2 页搜条件丢失这需要在请求里同时保留当前的关键词参数和页码参数文件在 Servlet 里拼到跳转的 URL 上。5.3 启动即报错 404 和 500 的处理方式成绩管理系统部署之后最常遇到这两类错误。404 一般是请求路径和 Servlet/JSP 路径不匹配F12 Network 里能看到哪个资源 404 了重点检查两处WebServlet注解里的 URL 是否以/开头JSP 中form action...里的路径是否带contextPath。500 错误里 Servlet 异常的堆栈信息会直接输出到 Tomcat 的 catalina.out 文件里一般不显示在浏览器页面中这时候要养成查看日志的习惯。如果加载不了Class.forName(com.mysql.cj.jdbc.Driver)多半是驱动 jar 包没进WEB-INF/libEclipse 里Build Path引入的 jar 如果没勾选 Deploy Assembly部署到 Tomcat 后是不存在于 WebContent/WEB-INF/lib 下的这个导致运行时才报 ClassNotFound 的问题很隐蔽。6. 连接池改造与成绩导出的组合收尾6.1 把 JDBC 改成内嵌 HikariCP 连接池的几个改动如果这套系统有机会写成期末报告把DriverManager.getConnection()升级成 HikariCP 连接池是最能体现工程素养的改动。HikariCP 是当前生产环境使用率最高的连接池之一改造只涉及 DBUtil把依赖 jar 放进 lib然后初始化一个HikariDataSourceprivate static HikariDataSource dataSource; static { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:mysql://localhost:3306/student_score ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8); config.setUsername(root); config.setPassword(123456); config.setMaximumPoolSize(20); config.setMinimumIdle(5); config.setConnectionTimeout(30000); dataSource new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); }maximumPoolSize在开发环境 10-20 即可不需要盲目调大每次连接占用 MySQL 侧一个线程过大的连接数会导致数据库负载反而上升。改成连接池之后conn.close()的语义从关闭物理连接变成归还到池里try-with-resources仍然适用DAO 代码不需要改动这个特性让改造风险最小。minimumIdle预热几个空闲连接可以降低前几次请求的延迟。整个系统里的 DAO 方法通过getConnection()获取的都是池化连接连接数满了之后新请求会在connectionTimeout时间内等待超时抛 SQLException。这个阶段关注HikariCP日志里连接获取淡慢更多场景来自单条 SQL 太慢把池占满。6.2 检查 Score 统计查询是否用对了索引大型系统上线前 DBA 会用EXPLAIN看执行计划成绩管理系统虽然数据量小但这个习惯值得学。给score表加上学生和课程两个外键上的索引之后JOIN的查询路径可以从全表扫描变成索引查找EXPLAIN SELECT c.course_name, COUNT(sc.id) FROM course c LEFT JOIN score sc ON c.id sc.course_id GROUP BY c.id, c.course_name;EXPLAIN结果里如果 type 是ALL说明走的全表扫描加上index或ref就是走了索引。score表上的uk_stu_course联合索引已经覆盖了(student_id, course_id)的组合查询按课程分组统计时会用到这个索引的 course 部分——最左前缀规则下只有以 student_id 开头才能命中完整索引所以涉及某门课的成绩统计时可以加一个INDEX(course_id)。实际优化加索引时要根据衡量查询模式去加不能给每个列都建索引索引写放大反而影响 INSERT/UPDATE 性能。6.3 成绩导出 Excel 时 JSP 用 response 输出流的注意点成绩列表导出 Excel 是这类系统的高频附加功能。常见方案是自己拼 CSV,因为 CSV 可以由 Excel 直接打开不需要额外依赖。另一种是用 POI 生成真实.xlsx代码量更大。resp.setContentType(text/csv;charsetUTF-8); resp.setHeader(Content-Disposition, attachment; filenamescores.csv); PrintWriter out resp.getWriter(); out.write(\ufeff); // BOM防止 Excel 打开 CSV 时中文乱码 out.println(学号,姓名,课程,成绩); for (ScoreVO vo : scoreList) { out.println(vo.getStudentNo() , vo.getStudentName() , vo.getCourseName() , vo.getScore()); } out.flush();这段代码里有两个工程细节。Content-Disposition的attachment告诉浏览器这是下载而不是页面渲染filename后的后缀名决定文件类型。写 CSV 时加\ufeffBOM 头是处理 Excel 打开乱码的标准做法——Excel 通过 BOM 识别 UTF-8 编码不加这个字符它默认按本地编码解析。如果改成 POI 导出同样需要设置响应头的Content-Disposition。导出功能在 Servlet 里输出时要确保之前没有调用过getWriter()执行过任何 HTML 输出否则响应流里已经叠加了页面内容下载的文件会损坏。此时可以结合 Controller 里做输出前检查此请求来自表单提交的链接而不会夹带已有的 JSP 片段。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价