ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Java Web学生选课系统:从技术选型到并发控制与跨浏览器支持的设计与实现

Java Web学生选课系统:从技术选型到并发控制与跨浏览器支持的设计与实现 简介这份资源是面向计算机相关专业学生与Java Web初学者的一套学生选课管理系统完整实现方案可作为课程设计、毕业设计或自学练手项目。压缩包共205个文件约7.23MB包含57个Java源文件、36个JSP页面、56张项目截图、11个SQL脚本及若干XML、CSS、JS与Jar依赖另附毕业论文Word文档覆盖需求分析、数据库设计到前后端编码的完整链路。项目基于Java 1.8、Tomcat 9.0.14与MySQL 5.7开发在Windows10配合IntelliJ IDEA即可运行涵盖课程管理、选课、退课、成绩查询等核心模块。已有2244人学习下载读者可参照源码理解MVC分层结构、JSP与Servlet交互及数据库表设计并借助截图与论文快速完成环境搭建与功能复现适合作为入门实战与答辩参考。1. 从一份“学生选课管理系统”说起Java Web 项目到底该怎么落地每年毕业季基于 Java Web 的学生选课管理系统设计与实现都是计算机专业出现频率最高的题目之一。它看起来简单——学生、课程、教师、选课记录四张表就能跑起来但真正动手做的时候很多人会卡在同一个地方环境能跑业务逻辑却经不起推敲。比如两个学生同时抢最后一个名额系统显示都选上了比如退课之后名额没释放课程容量对不上比如换台电脑打开页面样式全乱。这些问题不是靠堆代码能解决的它需要你在动手之前就想清楚分层结构、并发控制和跨浏览器支持的设计与实现策略。这篇文章面向三类人正在做课程设计的学生、需要快速交付一个可演示系统的开发者、以及想拿这个题目练手 Java Web 完整链路的人。我会按真实项目的推进顺序从技术选型和目录结构讲起再到数据库设计、核心业务实现、并发与事务处理最后落到调试技巧和验收标准。中间会给出可以直接复制的代码片段和配置参数也会标注哪些地方容易翻车。读完之后你应该能独立搭出一个能演示、能答辩、能扛住基本并发场景的选课系统。2. 技术选型与标准目录结构别一上来就堆框架2.1 为什么我建议从 Servlet JSP 起步而不是直接上 Spring Boot很多同学一看到“Java Web 项目”就去搜 Spring Boot 教程结果光依赖配置就耗掉两天。如果你的目标是理解 Web 请求从浏览器到数据库再返回的完整链路Servlet JSP JDBC 的组合反而是最透明的。你能看到 HttpServletRequest 里到底装了什么能手动控制 Session能自己写 Filter 做编码处理。这些经验在后期换任何框架都用得上。当然如果你的项目要求里明确写了“使用主流框架”或者时间充裕想顺便学 Spring Boot那直接上 Spring Boot MyBatis Thymeleaf 也没问题。但要注意框架帮你省掉的配置工作会在调试时变成黑匣子。我一般会建议先用原生方式跑通一个登录功能再决定要不要换框架。选型决策可以参考下面这张表方案适合场景学习成本调试难度答辩加分Servlet JSP JDBC课程设计、理解原理低低报错直观中Spring Boot MyBatis企业级项目、时间充裕中中需要看框架日志高Spring Boot Vue 前后端分离想展示工程能力高高跨域和接口调试耗时高我的建议是如果距离答辩还有两周以上选 Spring Boot如果只剩一周Servlet JSP 更稳。不要为了“看起来高级”选一个自己调不通的方案。2.2 标准目录结构把代码放在该放的地方Java Web 项目的目录结构直接影响后期维护和排错效率。我见过太多人把所有 Servlet 放在一个包下JSP 散落在 WebContent 根目录最后改一个功能要翻五个文件夹。下面是一个经过多个项目验证的结构适用于原生 Servlet 项目src/ com.university.course/ dao/ # 数据访问层只负责 SQL 执行 StudentDao.java CourseDao.java SelectionDao.java service/ # 业务逻辑层处理事务和规则 SelectionService.java CourseService.java servlet/ # 控制层接收请求、调用 Service、转发 LoginServlet.java SelectCourseServlet.java entity/ # 实体类与数据库表对应 Student.java Course.java Selection.java util/ # 工具类 DBUtil.java EncodingFilter.java WebContent/ WEB-INF/ web.xml # 部署描述符 lib/ # 第三方 jar 包 css/ js/ login.jsp course_list.jsp这个结构的核心原则是DAO 只做增删改查Service 做业务判断和事务控制Servlet 只做参数接收和页面跳转。三层各司其职出问题时能快速定位是哪一层的责任。比如选课人数不对先看 Service 有没有做容量判断再看 DAO 的 SQL 有没有写错条件。如果你用的是 Maven 项目目录会变成 src/main/java 和 src/main/webapp但包结构保持一致。Maven 的 pom.xml 里至少需要这些依赖dependencies !-- Servlet APIscope 必须是 provided因为 Tomcat 自带 -- dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency !-- JSP API同样 provided -- dependency groupIdjavax.servlet.jsp/groupId artifactIdjavax.servlet.jsp-api/artifactId version2.3.3/version scopeprovided/scope /dependency !-- MySQL 驱动compile 范围打包时需要 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency !-- JSTL用于 JSP 页面标签 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency /dependencies参数说明servlet-api 和 jsp-api 的 scope 设为 provided 是关键否则打包时会把 Tomcat 自带的类也打进去部署时报 ClassCastException。MySQL 驱动版本要和本地数据库版本匹配8.x 驱动类名是 com.mysql.cj.jdbc.Driver5.x 是 com.mysql.jdbc.Driver写错会报找不到驱动。2.3 数据库连接工具类一个容易埋雷的地方DBUtil 看起来简单但很多项目在这里翻车。最常见的问题是每次操作都新建连接选课高峰期连接数暴涨数据库直接拒绝服务。正确做法是用连接池但课程设计里手写连接池不现实我一般用 HikariCP 或者 Druid 的简化配置。public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/course_selection ?useUnicodetruecharacterEncodingutf8useSSLfalse serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD your_password; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL 驱动加载失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, PreparedStatement ps, ResultSet rs) { // 关闭顺序ResultSet - PreparedStatement - Connection // 每个 close 都要单独 try-catch避免一个异常导致后面的关不掉 try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (ps ! null) ps.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }逻辑说明URL 里的 characterEncodingutf8 和 serverTimezone 是必加参数否则中文乱码和时间差八小时的问题会同时出现。close 方法里每个资源单独捕获异常是因为如果 rs.close() 抛异常后面的 ps 和 conn 就不会执行连接泄漏就是这么来的。3. 数据库设计与选课核心逻辑把规则写进表结构和事务里3.1 四张核心表的设计与索引策略选课系统的数据库设计决定了后期业务逻辑的复杂度。我见过有人把选课记录直接塞进学生表的字段里用逗号分隔课程 ID结果查询“某课程有多少人选”时只能全表扫描。正确的做法是独立出选课关系表。-- 学生表 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL, password VARCHAR(64) NOT NULL COMMENT 存储 SHA-256 哈希值, major VARCHAR(50), grade INT ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表 CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_code VARCHAR(20) NOT NULL UNIQUE, course_name VARCHAR(100) NOT NULL, teacher_name VARCHAR(50), credit DECIMAL(3,1), capacity INT NOT NULL DEFAULT 0 COMMENT 课程容量, selected_count INT NOT NULL DEFAULT 0 COMMENT 已选人数, semester VARCHAR(20) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 选课记录表 CREATE TABLE selection ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, status TINYINT DEFAULT 1 COMMENT 1-已选 0-已退, UNIQUE KEY uk_student_course (student_id, course_id), INDEX idx_course (course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;参数说明selection 表上的唯一索引 uk_student_course 是防止重复选课的第一道防线比在 Java 代码里查一次再插入更可靠。idx_course 索引用于加速“查询某课程已选人数”的操作。course 表里的 selected_count 是冗余字段目的是避免每次选课都去 count 选课记录表但必须和 selection 表的插入放在同一个事务里更新。3.2 选课接口的完整实现从 Servlet 到 Service 到 DAO选课是系统的核心操作涉及三个动作检查容量、插入选课记录、更新已选人数。这三个动作必须在同一个事务里完成否则会出现超选或人数对不上的问题。先看 Servlet 层它只负责接收参数和返回结果WebServlet(/selectCourse) public class SelectCourseServlet extends HttpServlet { private SelectionService selectionService new SelectionService(); protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 从 Session 获取当前登录学生避免前端伪造 studentId Student student (Student) request.getSession().getAttribute(currentStudent); if (student null) { response.sendRedirect(login.jsp); return; } int courseId Integer.parseInt(request.getParameter(courseId)); try { String result selectionService.selectCourse(student.getId(), courseId); request.setAttribute(message, result); } catch (Exception e) { request.setAttribute(message, 选课失败 e.getMessage()); } request.getRequestDispatcher(course_list.jsp).forward(request, response); } }逻辑说明从 Session 取学生 ID 而不是从请求参数取是防止越权操作的基本手段。如果前端传 studentId别人改一下参数就能替别人选课。异常统一捕获后转发到页面显示避免把堆栈直接暴露给用户。Service 层是事务控制的核心public class SelectionService { private CourseDao courseDao new CourseDao(); private SelectionDao selectionDao new SelectionDao(); public String selectCourse(int studentId, int courseId) throws SQLException { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 检查是否已经选过该课程 if (selectionDao.exists(conn, studentId, courseId)) { conn.rollback(); return 你已经选过这门课; } // 2. 检查课程容量使用 SELECT ... FOR UPDATE 锁定行 Course course courseDao.getByIdForUpdate(conn, courseId); if (course null) { conn.rollback(); return 课程不存在; } if (course.getSelectedCount() course.getCapacity()) { conn.rollback(); return 课程已满; } // 3. 插入选课记录 selectionDao.insert(conn, studentId, courseId); // 4. 更新已选人数 courseDao.incrementSelectedCount(conn, courseId); conn.commit(); return 选课成功; } catch (SQLIntegrityConstraintViolationException e) { // 唯一索引冲突说明并发下有人先插入了 if (conn ! null) conn.rollback(); return 你已经选过这门课; } catch (Exception e) { if (conn ! null) conn.rollback(); throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } } }参数说明conn.setAutoCommit(false) 开启事务后所有 SQL 都在同一个连接上执行。courseDao.getByIdForUpdate 对应的 SQL 是 SELECT * FROM course WHERE id ? FOR UPDATE这个行锁会阻塞其他事务对同一行课程的读取直到当前事务提交。这是防止超选的关键。唯一索引冲突的捕获是第二道防线即使行锁没拦住数据库层面也会拒绝重复插入。DAO 层的两个关键方法public class CourseDao { public Course getByIdForUpdate(Connection conn, int courseId) throws SQLException { String sql SELECT * FROM course WHERE id ? FOR UPDATE; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, courseId); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { Course c new Course(); c.setId(rs.getInt(id)); c.setCapacity(rs.getInt(capacity)); c.setSelectedCount(rs.getInt(selected_count)); return c; } } } return null; } public void incrementSelectedCount(Connection conn, int courseId) throws SQLException { String sql UPDATE course SET selected_count selected_count 1 WHERE id ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, courseId); ps.executeUpdate(); } } }逻辑说明FOR UPDATE 必须在事务内才有意义如果 autocommit 是 true锁会立即释放。incrementSelectedCount 用 SQL 层面的自增而不是先查再写避免并发下的丢失更新。3.3 退课逻辑别让名额释放变成玄学退课看起来简单但很多项目在这里出问题退课后 selected_count 没减或者减了但选课记录没标记。正确的退课流程和选课对称也需要事务public String dropCourse(int studentId, int courseId) throws SQLException { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 检查选课记录是否存在且状态为已选 Selection sel selectionDao.getActiveSelection(conn, studentId, courseId); if (sel null) { conn.rollback(); return 你没有选这门课; } // 2. 更新选课记录状态为已退 selectionDao.updateStatus(conn, sel.getId(), 0); // 3. 减少已选人数注意不能减到负数 String sql UPDATE course SET selected_count selected_count - 1 WHERE id ? AND selected_count 0; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, courseId); int affected ps.executeUpdate(); if (affected 0) { conn.rollback(); return 退课失败人数异常; } } conn.commit(); return 退课成功; } catch (Exception e) { if (conn ! null) conn.rollback(); throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }参数说明UPDATE 语句里的 AND selected_count 0 是防止人数被减成负数。如果 affected 为 0说明数据已经不一致此时回滚比继续执行更安全。退课记录不删除而是改状态是为了保留操作日志方便后期统计和排查。4. 跨浏览器支持与前端交互别让样式在答辩现场崩掉4.1 跨浏览器支持的设计与实现三个必须处理的差异“跨浏览器支持的设计与实现”是热搜里反复出现的词也是答辩时容易被问到的点。很多项目在 Chrome 上跑得好好的换到 Firefox 或 Edge 就出问题。常见差异集中在三个方面CSS 默认样式、表单提交行为和 JavaScript API 兼容性。第一个差异是 CSS 盒模型。不同浏览器对 padding 和 border 是否计入 width 的默认处理不同。解决方案是在全局样式里强制统一/* 全局重置确保所有浏览器盒模型一致 */ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } /* 表单元素在不同浏览器下的字体和边框差异 */ input, select, textarea, button { font-family: inherit; font-size: 100%; line-height: 1.15; border: 1px solid #ccc; border-radius: 4px; padding: 6px 8px; } /* 表格边框合并避免 Firefox 下双线 */ table { border-collapse: collapse; width: 100%; }逻辑说明box-sizing: border-box 让 width 包含 padding 和 border这是目前最一致的盒模型方案。表单元素继承字体是因为浏览器默认使用系统字体不统一会导致布局错位。border-collapse 解决的是 Firefox 下表格边框显示为双线的问题。第二个差异是表单提交。IE 和旧版 Edge 对 form 的 submit 事件处理不同如果用了 event.preventDefault() 但没做兼容会导致表单重复提交。我一般用 addEventListener 并检查 event 是否存在document.getElementById(selectForm).addEventListener(submit, function(event) { // 兼容旧浏览器的事件对象获取方式 event event || window.event; if (event.preventDefault) { event.preventDefault(); } else { event.returnValue false; // 旧版 IE 的阻止方式 } // 执行选课请求 submitSelection(); });参数说明event.returnValue false 是 IE8 及以下的写法虽然现在很少遇到但加上不影响现代浏览器。submitSelection 函数里用 fetch 发请求时要注意 IE 不支持 fetch如果项目要求兼容 IE需要换成 XMLHttpRequest 或引入 polyfill。第三个差异是 JavaScript 的 Date 解析。不同浏览器对 new Date(“2024-01-01 12:00:00”) 的解析结果不同Safari 会返回 Invalid Date。解决方案是用斜杠分隔或者手动解析// 不推荐Safari 下可能返回 Invalid Date // var d new Date(2024-01-01 12:00:00); // 推荐手动解析所有浏览器一致 function parseDate(str) { var parts str.split(/[- :]/); return new Date(parts[0], parts[1] - 1, parts[2], parts[3] || 0, parts[4] || 0, parts[5] || 0); }4.2 选课页面的交互细节从课程列表到确认弹窗选课页面的核心是课程列表的展示和选课按钮的响应。课程列表需要显示课程名、教师、学分、容量和已选人数并且已满的课程要禁用按钮。这些逻辑在 JSP 里用 JSTL 实现% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table thead tr th课程代码/thth课程名称/thth教师/th th学分/thth容量/thth已选/thth操作/th /tr /thead tbody c:forEach items${courseList} varcourse tr td${course.courseCode}/td td${course.courseName}/td td${course.teacherName}/td td${course.credit}/td td${course.capacity}/td td${course.selectedCount}/td td c:choose c:when test${course.selectedCount course.capacity} button disabled已满/button /c:when c:otherwise button onclickconfirmSelect(${course.id}, ${course.courseName}) 选课 /button /c:otherwise /c:choose /td /tr /c:forEach /tbody /table逻辑说明c:choose 根据已选人数和容量决定按钮状态前端禁用只是体验优化真正的容量判断在 Service 层。confirmSelect 函数弹出确认框后再提交避免误操作function confirmSelect(courseId, courseName) { if (confirm(确认选择《 courseName 》吗)) { // 用隐藏表单提交避免 URL 拼接参数 var form document.createElement(form); form.method POST; form.action selectCourse; var input document.createElement(input); input.type hidden; input.name courseId; input.value courseId; form.appendChild(input); document.body.appendChild(form); form.submit(); } }参数说明用 POST 提交而不是 GET是因为选课会修改数据GET 请求可能被浏览器预加载或缓存。动态创建表单而不是用 window.location是为了避免参数暴露在地址栏。5. 避坑与排查那些答辩前夜才发现的翻车现场5.1 中文乱码三个位置都要设 UTF-8现象登录页面输入中文姓名提交后数据库里显示问号或者 JSP 页面显示的中文变成乱码。原因乱码可能出现在三个环节——请求编码、响应编码、数据库连接编码。只设其中一个不够。解决第一在 web.xml 里配置编码过滤器或者用 WebFilter 注解WebFilter(/*) public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }第二JSP 页面顶部加 % page contentType“text/html;charsetUTF-8” language“java” %。第三数据库连接 URL 里加 characterEncodingutf8。三处都设了乱码基本不会出现。5.2 选课超员没有加锁的容量判断等于没判断现象课程容量 30已选 29两个学生同时点选课结果两个都选上了已选人数变成 31。原因Service 层先查 selected_count 再判断两个线程可能同时查到 29都认为还有名额然后都执行插入和更新。解决用 SELECT ... FOR UPDATE 锁定课程行让第二个事务等待第一个提交后再读取。或者用乐观锁在 UPDATE 时加条件UPDATE course SET selected_count selected_count 1 WHERE id ? AND selected_count capacity;如果 affected rows 为 0说明被抢完了回滚事务并提示用户。乐观锁适合并发不高的场景实现比 FOR UPDATE 简单但高并发下重试率会上升。5.3 连接泄漏Tomcat 跑一天就报 Too many connections现象本地测试正常部署到服务器后运行几小时数据库报连接数超限重启 Tomcat 才能恢复。原因某个 DAO 方法里 Connection 没有在 finally 里关闭或者关闭时抛异常导致后面的连接没关。解决所有数据库操作必须用 try-with-resources 或者在 finally 里关闭。检查 DBUtil.close 方法是否每个资源单独捕获异常。另外可以在 MySQL 里执行 SHOW PROCESSLIST 查看当前连接如果大量连接处于 Sleep 状态且时间很长基本可以确定是泄漏。5.4 Session 失效选课选到一半跳回登录页现象学生登录后浏览课程列表点选课时跳转到登录页提示未登录。原因Session 超时时间设置太短或者 Tomcat 重启导致 Session 丢失。默认 Session 超时是 30 分钟如果学生填表慢了就会过期。解决在 web.xml 里延长超时时间或者用 Cookie 记住登录状态session-config session-timeout60/session-timeout /session-config参数说明60 表示 60 分钟。如果用了 Tomcat 集群还需要配置 Session 共享否则用户请求被分发到另一台服务器时会找不到 Session。课程设计一般单机部署设长一点就行。5.5 前端按钮重复提交网络慢的时候学生连点五次现象选课请求发出后页面没反应学生连续点击按钮结果产生多条选课记录如果唯一索引没生效或者多次更新人数。原因按钮点击后没有立即禁用网络延迟期间用户可以重复点击。解决在 confirmSelect 函数里提交前禁用按钮function confirmSelect(courseId, courseName, btn) { if (confirm(确认选择《 courseName 》吗)) { btn.disabled true; btn.textContent 提交中...; // 后续提交逻辑 } }同时后端唯一索引是最终防线。前端禁用只是体验优化不能替代数据库约束。6. 验收前的自检清单与一个提效技巧6.1 答辩前必须跑通的六个场景在把项目交出去之前我一般会按下面这个清单过一遍。每个场景都要在 Chrome 和 Firefox 里各跑一次确保跨浏览器支持不是纸面功夫。场景操作步骤预期结果检查点登录输入正确学号和密码跳转课程列表Session 里有 currentStudent登录失败输入错误密码提示密码错误不暴露具体是学号还是密码错选课成功选一门未满课程提示成功人数加一数据库 selection 和 course 同步更新选课已满选一门已满课程提示课程已满人数不变重复选课对已选课程再选一次提示已选过唯一索引生效退课退掉已选课程提示成功人数减一人数不为负如果这六个场景都通过基本功能就没问题了。答辩时老师最常问的是“并发怎么处理的”和“事务在哪里控制的”提前准备好这两处的代码位置和解释。6.2 一个提效技巧用过滤器统一登录校验最后一个技巧是关于代码整洁度的。很多项目在每个 Servlet 里都写一遍“检查 Session 是否有用户”重复且容易漏。我一般用一个 LoginFilter 统一处理WebFilter(urlPatterns {/selectCourse, /dropCourse, /myCourses}) public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); if (session null || session.getAttribute(currentStudent) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }参数说明urlPatterns 里列出所有需要登录才能访问的路径login.jsp 和登录接口不包含在内。getSession(false) 表示如果不存在 Session 就返回 null不新建。这样新增功能时只要把路径加进 urlPatterns 就行不用改业务代码。这个习惯是我做了几个项目之后才养成的。一开始觉得过滤器麻烦后来发现漏写登录检查导致越权操作被老师当场指出来才回头补上。现在我的习惯是任何需要登录的路径先加过滤器再写业务。希望这个思路能帮到你。本文还有配套的精品资源点击获取
返回列表