ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java Web选课系统实战:JSP+Servlet+MySQL分层架构与并发控制

Java Web选课系统实战:JSP+Servlet+MySQL分层架构与并发控制 简介面向Java Web初学者和毕业设计人群这是一份基于JavaJSPServletMySQL的学生选课管理系统完整项目。系统覆盖登录权限、课程管理、选课冲突检测、成绩录入和数据备份恢复等核心模块代码按MVC模式分层业务逻辑、数据模型与页面显示分离便于阅读和二次开发。资源包共408个文件包含25个JSP动态页面、43个Java源码与class编译文件以及JS/CSS前端脚本、GIF操作演示图、SQL脚本和备份配置压缩包约6.59MB。目前已有6906人浏览学习项目结构完整可导入常用IDE查看调试适合课程设计、毕业设计或选课系统的功能改造。整体可作为Java Web开发入门的综合练习材料。1. 为什么这套「老技术栈」做学生选课管理系统依然划算如果你正在为课程设计、毕业设计或一个中小型校内项目选型看到的推荐多半是 Spring Boot MyBatis Vue。但标题里这组 Java JSP Servlet MySQL 组合在“学生选课管理系统”这个具体场景下反而是最不容易翻车的选择。它没有复杂的依赖注入、没有前后端分离的跨域问题页面由 JSP 在服务端直接渲染选课流程里的登录校验、课程余量扣减、已选列表展示都能用最直白的代码讲清楚。这套方案适合三类人Java Web 刚起步、想弄清请求到底怎么被处理的初学者需要快速交付且能现场讲明白设计的在校生以及维护 legacy 项目时必须能看懂 JSP 和 Servlet 的在职工程师。它不是最强方案但它是让业务先跑起来、坑又足够少的方案。2. 选课系统的三层架构JSP、Servlet、MySQL 到底各管什么2.1 MVC 分层JSP 只做视图Servlet 只做控制别把业务代码堆在页面上学生选课管理系统最容易被写成“JSP 里连数据库、JSP 里写业务判断”的单体页面这种写法响应快但后患无穷。常见做法是严格按 MVC 拆Servlet 接收请求、调用业务方法、决定跳转到哪个 JSPJSP 页面上只出现 HTML 和 JSTL/EL 标签不出现 Java 代码数据库访问单独放到 DAO 层。这样做的好处不只是结构好看——选课逻辑余量判断、冲突检测、事务提交是系统里唯一复杂的地方只有把它从页面里挪出来你才能在出问题时用单元测试去定位而不是在满屏% %里翻找。一个典型的请求流程是这样的浏览器提交选课请求到 ServletServlet 解析参数、调用选课 ServiceService 里开启事务、扣减余量、插入选课记录最后根据结果转发到success.jsp或error.jsp。JSP 页面本身不关心数据库长什么样它只通过 EL 表达式读取 request 域里的数据。这个分层逻辑建议一开始就搭好不要等代码写到 2000 行再回头重构那个成本比你想的高得多。2.2 数据库三张核心表学生、课程、选课记录的关键字段设计学生选课管理系统的数据模型核心是学生表、课程表、选课记录表三张表。学生表字段相对固定student_id学号主键、name、password、major、grade。课程表要有course_id主键、course_name、teacher、credit、capacity总容量、selected_count已选人数。这里的一个关键设计是把“已选人数”冗余到课程表里用selected_count capacity来判断是否还能选而不是每次实时COUNT(*)选课记录——虽然量小时两种做法都能跑但冗余字段配合事务里的行级锁是你后面处理并发超卖的基础。选课记录表是整个系统的核心字段包括id自增主键、student_id、course_id、select_time。必须为学生和课程建联合唯一索引防止同一个人重复选同一门课——这一步能在数据库层面兜住代码里漏掉的判断。建议给这三张表设置字符集为utf8mb4排序规则用utf8mb4_general_ci否则后面中文乱码会一直困扰你。外键可以不加用应用层维护逻辑关系选课系统数据量不大外键的约束收益远低于它带来的删改麻烦。2.3 为什么选 Servlet 而不是直接上 Spring MVC你可能会问既然分层都分了为什么不直接学 Spring MVC我的实际看法是选课管理系统这种体量Servlet 的原生生命周期反而帮助你理解 HTTP 的本质。你写一个HttpServlet子类重写doGet和doPost你需要自己处理编码、自己决定转发还是重定向、自己从 request 里拿参数。这套流程走一遍之后再去看 Spring MVC 里的RequestMapping、DispatcherServlet你会立刻明白它帮你做了什么。反过来如果一开始就从 Spring Boot 起步请求如何路由、参数如何绑定、视图如何解析对你来说都是黑匣子出了问题连日志都看不懂。另外很多校内服务器环境里装的是老版本 TomcatJSPServlet 的兼容性天然优于 Spring Boot 打的 fat jar。你拿一个原生 Servlet 项目部署到 Tomcat 的webapps目录几乎不会遇到 jar 包冲突或启动失败的事。对需要现场演示、现场答辩的场景来说这一点很实在。3. 环境搭建与第一步可运行代码从 Tomcat 到 MySQL 连通3.1 版本选择JDK 8、Tomcat 9、MySQL 5.7 还是 8.0环境版本直接决定你踩多少坑。我最常用的搭配是 JDK 8 Tomcat 9 MySQL 5.7。JDK 8 是兼容性最好的版本无论你之后学什么框架都还有退路Tomcat 9 对应 Servlet 4.0比 Tomcat 8.5 新一些且配置方式一致MySQL 5.7 不是因为它最好而是因为如果你从 MySQL 官网下载安装包5.7 的安装和初始化流程在网上的教程最多、踩坑记录最全出了问题搜得到答案。如果你的机器上必须装 MySQL 8.0也一样能用但要注意两点驱动要用com.mysql.cj.jdbc.Driver连接串里必须带serverTimezoneAsia/Shanghai同时useSSLfalse要显式声明否则会提示 SSL 连接警告甚至直接报错。Tomcat 的端口默认是 8080如果你本机装了其他服务占用了这个端口可以到conf/server.xml里改Connector port8080。还有一个容易被忽略的点Tomcat 9 要求 JDK 8 以上如果你机器上还留有 JDK 7 的环境变量启动时会直接报 UnsupportedClassVersionError排查半天才发现是 JAVA_HOME 指错了。3.2 用 JDBC 连接 MySQL从 DriverManager 到连接池先不急着上框架第一步是让 Servlet 能通过 JDBC 查到数据库里的数据。最直白的写法是DriverManager.getConnection(url, username, password)但生产哪怕是小系统我也建议直接用连接池。选课系统最常见的并发场景是选课开放瞬间几十个学生同时点选课如果用 DriverManager 每次都重新建立连接数据库会疲于握手响应时间直线上升连接池就是为了解决这个问题的。连接池选型上DBCP2 比 C3P0 配置简单HikariCP 性能最好但老 Tomcat 项目我一般用 dbcp2因为commons-dbcp2和commons-pool2两个 jar 放进去就完事不需要额外写配置类。下面是一个最小可用的连接池配置import org.apache.commons.dbcp2.BasicDataSource; public class DBUtil { private static final BasicDataSource dataSource new BasicDataSource(); static { dataSource.setDriverClassName(com.mysql.jdbc.Driver); dataSource.setUrl(jdbc:mysql://localhost:3306/course_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai); dataSource.setUsername(root); dataSource.setPassword(your_password); dataSource.setInitialSize(5); dataSource.setMaxTotal(20); dataSource.setMaxIdle(10); dataSource.setMinIdle(5); dataSource.setMaxWaitMillis(3000); } public static BasicDataSource getDataSource() { return dataSource; } }这段代码的核心逻辑是把连接池初始化为一个单例启动时建立 5 个空闲连接最大允许 20 个连接超过 3 秒拿不到连接就抛异常。setMaxWaitMillis(3000)很关键否则选课高峰期间连接耗尽时请求会无限期挂起。注意驱动类这里写的是com.mysql.jdbc.Driver对应 MySQL 5.7 的旧版驱动如果你用 MySQL 8.0要改成com.mysql.cj.jdbc.Driver并且连接串里的serverTimezone参数不能缺。3.3 第一个能跑的 Servlet查询课程列表并渲染到 JSP现在写第一个能跑通全流程的 Servlet。它的任务是查出所有课程放进 request 域转发到course_list.jsp展示。这是选课系统的最小闭环也用来验证你的环境是否全部就绪。WebServlet(/course/list) public class CourseListServlet extends HttpServlet { private CourseDao courseDao new CourseDao(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); ListCourse courseList courseDao.findAllCourses(); req.setAttribute(courseList, courseList); req.getRequestDispatcher(/jsp/course_list.jsp).forward(req, resp); } }WebServlet(/course/list)是 Servlet 3.0 之后的注解式映射省掉了 web.xml 里的servlet配置。doGet 里第一行setCharacterEncoding(UTF-8)是为了处理请求参数的编码防止后续选课提交时中文乱码。findAllCourses()走 DAO 层查数据库返回的ListCourse放到 request 域后通过forward跳转到 JSP。这里必须用forward不能用sendRedirect因为sendRedirect是重定向request 域里的数据会丢。对应的 JSP 页面用 JSTL 来遍历输出课程列表% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % page contentTypetext/html;charsetUTF-8 languagejava % html body h2可选课程列表/h2 table border1 tr th课程号/th th课程名/th th教师/th th学分/th th余量/th th操作/th /tr c:forEach items${courseList} varcourse tr td${course.courseId}/td td${course.courseName}/td td${course.teacher}/td td${course.credit}/td td${course.capacity - course.selectedCount}/td td a href${pageContext.request.contextPath}/course/select?courseId${course.courseId}选课/a /td /tr /c:forEach /table /body /htmlc:forEach里的${course.courseId}是 EL 表达式它自动调用 Course 对象的 getter 方法所以你的 Course 类必须有对应的getCourseId()方法否则这里会直接报PropertyNotFoundException。${course.capacity - course.selectedCount}直接在页面上算余量这是 EL 表达式支持的算术操作省去了在 Servlet 里单独封装 DTO 的麻烦。这里还要注意pageContext.request.contextPath的作用它动态获取项目部署路径避免硬编码否则项目名一改所有链接全部 404。4. 选课核心业务实现登录拦截、事务边界与防超卖4.1 登录认证Session 与 Filter 的配合方式学生选课系统的绝大多数页面都需要登录后才能访问。最简单可靠的方案是用户登录成功后把student对象塞进 Session然后写一个LoginFilter拦截所有需要认证的 URL如果 Session 里没有用户就重定向到登录页。WebFilter(/course/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; HttpSession session req.getSession(false); if (session null || session.getAttribute(student) null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }这里的拦截路径WebFilter(/course/*)表示所有选课相关操作都先过这道过滤器。req.getSession(false)的false参数很讲究如果 Session 不存在它返回 null 而不是新建一个。这样才能区分“未登录”和“登录状态”。拦截到未登录用户时用sendRedirect跳转登录页并且带上req.getContextPath()前缀防止多项目部署时路径错乱。登录接口本身在/login路径下不受这个过滤器管否则会出现“登录页都访问不了”的死循环。4.2 选课与退课事务里完成余量扣减和选课记录插入选课操作是整个系统里唯一必须开事务的地方。一次选课涉及两步更新课程表的selected_count加 1插入选课记录表。两步之间如果任何一步失败都会造成数据不一致——比如扣了余量但没插入记录或者插入了记录但余量没扣。JDBC 事务的标准写法是关闭自动提交执行 SQL提交或回滚最后在 finally 里恢复自动提交。public boolean selectCourse(String studentId, String courseId) { Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getDataSource().getConnection(); conn.setAutoCommit(false); String checkSql SELECT capacity, selected_count FROM course WHERE course_id ? FOR UPDATE; ps conn.prepareStatement(checkSql); ps.setString(1, courseId); rs ps.executeQuery(); if (!rs.next()) { conn.rollback(); return false; } int capacity rs.getInt(capacity); int selectedCount rs.getInt(selected_count); if (selectedCount capacity) { conn.rollback(); return false; } String updateSql UPDATE course SET selected_count selected_count 1 WHERE course_id ?; ps conn.prepareStatement(updateSql); ps.setString(1, courseId); ps.executeUpdate(); String insertSql INSERT INTO select_record (student_id, course_id, select_time) VALUES (?, ?, NOW()); ps conn.prepareStatement(insertSql); ps.setString(1, studentId); ps.setString(2, courseId); ps.executeUpdate(); conn.commit(); return true; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } e.printStackTrace(); return false; } finally { if (rs ! null) { try { rs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (ps ! null) { try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }SELECT ... FOR UPDATE是这整段代码的关键。它在查询课程行时对该行加排他锁直到事务提交才释放。这样两个学生同时选同一门课还剩最后一个名额时第二个事务会阻塞在查询语句上等第一个事务提交后它读到的是更新后的selected_count然后发现余量不足回滚返回 false。这是防超卖最直接的方法。代码里还有一个细节conn来自连接池close()其实只是把连接归还给池子不是真正断开所以必须把setAutoCommit(true)放回连接归还之前否则连接池里的连接可能残留false的自动提交状态下一个使用者就得踩坑。4.3 退课操作先删记录再减余量顺序不能反退课的逻辑比选课简单但顺序有讲究。我的习惯是先删除选课记录再扣减课程表的selected_count。原因是删除记录是主键定位几乎不会失败而扣减余量如果放在前面万一删除记录失败比如记录不存在就会出现余量被减、但学生仍然选着课的脏数据。public boolean dropCourse(String studentId, String courseId) { Connection conn null; PreparedStatement ps null; try { conn DBUtil.getDataSource().getConnection(); conn.setAutoCommit(false); String deleteSql DELETE FROM select_record WHERE student_id ? AND course_id ?; ps conn.prepareStatement(deleteSql); ps.setString(1, studentId); ps.setString(2, courseId); int rows ps.executeUpdate(); if (rows 0) { conn.rollback(); return false; } String updateSql UPDATE course SET selected_count selected_count - 1 WHERE course_id ? AND selected_count 0; ps conn.prepareStatement(updateSql); ps.setString(1, courseId); ps.executeUpdate(); conn.commit(); return true; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } return false; } finally { if (ps ! null) { try { ps.close(); } catch (SQLException e) { e.printStackTrace(); } } if (conn ! null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }UPDATE course SET selected_count selected_count - 1 WHERE course_id ? AND selected_count 0里的AND selected_count 0是个防御性条件。正常情况下删除记录成功就说明这个学生确实选了课余量必然大于 0但加上这个条件后即使数据出现了莫名的不一致也不会把余量扣成负数。退课操作同样需要事务因为删除记录和扣减余量是两步操作。最后在 Servlet 里无论选课还是退课操作完成后都要重定向而不是转发——防止用户按 F5 刷新导致重复提交。4.4 我的选课页面已选课程、学分汇总与冲突检测除了单纯选课学生选课后应该能查看自己已选的课程以及总计多少学分。已选列表的查询在 MySQL 里很简单SELECT c.* FROM course c JOIN select_record r ON c.course_id r.course_id WHERE r.student_id ?。学分汇总可以直接查询时用聚合函数或者查完列表后在 Java 里累加——数据量小用 Java 累加就可以了省去写复杂 SQL 的成本。冲突检测有两种做法。简单做法是选课前查一下该学生的选课记录看时间是否与新课冲突。但学校的排课一般只分星期几和节次所以课程表里通常存储day_of_week和period字段。冲突检测的 SQL 可以写成查出学生已选课程中是否存在day_of_week和period与新课相同的记录。如果存在提示“该时段已有课程”。这一步可以在 Service 层用一条查询完成但要注意它和选课事务之间有竞态窗口检测完到插入记录之间另一个请求可能已经插入了同时间段的课。要严格防冲突需要在选课事务里加锁或者用联合唯一索引兜底。对学生选课系统来说我会用联合唯一索引方案兜底代码里先做检测如果真的撞了唯一索引捕获异常后给用户一个友好提示。5. 常见问题与避坑从 404 到中文乱码的排查清单5.1 页面全部 404 或 500先分清是映射问题还是部署问题现象项目启动后访问http://localhost:8080/course/list返回 404控制台没有任何异常。原因404 分两种。第一种是 Servlet 映射路径写错了访问的 URL 与WebServlet注解里写的路径不一致第二种是项目根本没有部署到 Tomcat 的 webapps 目录IDEA 里你可能只是启动了 Tomcat 但没把 artifact 加上。解决45% 的情况出在大小写和路径少斜杠上。WebServlet(/course/list)的 URL 必须是完整路径少了前导斜杠 Tomcat 直接报ServletMapping解析失败。剩下 55% 的情况建议直接在浏览器地址栏一个个试先访问http://localhost:8080/项目名/看 Tomcat 默认页面是否出来再访问具体的 Servlet 路径。如果 Tomcat 首页正常但项目页面 404到webapps目录下看 war 包解压出来的文件夹名是否和访问路径一致路径对不上 Tomcat 是不可能找到资源的。5.2 中文乱码请求参数、响应页面各管各的编码现象页面标题正常但从数据库读出来的课程名显示为问号或者往数据库插入中文后用命令行查看是乱码页面显示也乱。原因编码链路有四段哪一段断了都会乱。第一段是 JSP 页面本身的contentType要带charsetUTF-8第二段是 Servlet 里设置了request.setCharacterEncoding(UTF-8)位置必须在读取任何参数之前第三段是数据库连接串里带characterEncodingutf8第四段是数据库表自身的字符集要是utf8mb4。很多人只设了其中一两处所以表现为「插入正常但显示乱码」或「显示正常但入库后乱码」。解决我的习惯是开局就把四段全部写上不给自己留判断的余地。其中最容易漏的是 JSP 顶部那行% page contentTypetext/html;charsetUTF-8 languagejava %漏掉它页面响应头里没有 charset浏览器会用自己的默认编码去解析几乎必乱。还有一个隐蔽的坑resp.setCharacterEncoding(UTF-8)必须在resp.getWriter()之前调用一旦你先把 Writer 取出来了再设置编码就晚了。5.3 JDBC 连不上 MySQL驱动版本、时区与 SSL 警告现象Tomcat 启动正常一访问查询课表的 Servlet 就报ClassNotFoundException: com.mysql.jdbc.Driver或者Communications link failure或者The server time zone value ... is unrecognized。原因ClassNotFoundException说明 mysql-connector-java 的 jar 没有放到WEB-INF/lib目录下注意是WEB-INF/lib不是项目根目录的某个 lib 文件夹。Tomcat 只认前者。Communications link failure多数是数据库服务没启动或者连接串里 localhost 和端口写错了MySQL 默认端口 3306改过的话需要显式声明。时区报错是 MySQL 8.0 的经典问题旧驱动不认新版 MySQL 的时区格式。解决先确认 jar 在WEB-INF/lib下同时右键 jar 选“Add as Library”让 IDEA 识别。然后确认连接串带serverTimezoneAsia/Shanghai和useSSLfalse。如果你的 MySQL 是 5.7驱动用com.mysql.jdbc.Driver没问题如果是 8.0驱动类改成com.mysql.cj.jdbc.Driverjar 换成 8.x 版本。还有一个容易被忽略的权限问题MySQL 8.0 默认的 root 授权方式不允许 JDBC 远程连接需要在命令行执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 密码;这是 MySQL 8.0 独有的坑5.7 没有。5.4 选课超卖余量显示有 1 个名额两个人同时提交都成功了现象课程剩余 1 个名额A 和 B 同时点选课两个人都收到了成功提示最终selected_count变成 2超过capacity。原因这是并发场景下的竞态条件。如果你直接SELECT selected_count然后在 Java 里判断余量大于 0 再做更新两个线程同时读到selected_count capacity - 1同时认为可以选先后执行 UPDATE自然超卖。这是最常见的“写了业务逻辑但没考虑并发”的翻车现场。解决用前面 4.2 节里的SELECT ... FOR UPDATE方案把余量查询和更新放到同一个事务里查询时锁住课程行让第二个请求排队等待等第一个事务提交后它读到的余量已经变成 0回滚并提示“课程已满”。如果不想用悲观锁也可以用乐观锁在课程表加一个version字段更新时SET selected_count selected_count 1, version version 1 WHERE version ?如果更新的影响行数为 0说明版本号冲突事务重试。对学生选课系统来说悲观锁实现更直白也更好向老师解释。5.5 我的页面点击“选课”后一直转圈不跳转线程池与连接池耗尽的双重陷阱现象选课开放后多个请求卡住页面一直转圈最后 Tomcat 报连接超时控制台出现Connection pool exhausted或Timeout: Pool empty。原因两个池子都满了。Tomcat 默认的线程池大小为 200连接池最大 20。当 20 个连接被慢 SQL 占住不放时后续所有请求都在连接池的maxWaitMillis3000里排队3 秒后抛异常但异常没被好好处理连接没归还进一步拖垮线程池。很多时候根因其实是某个 SQL 没有索引全表扫描导致查询耗时过长把整个连接池拖死。解决先给select_record表的student_id和course_id分别建索引这是选课系统最高频的查询条件没有索引时连接一多必然出事。然后在 DAO 层代码里用 try-with-resources 或 finally 确保连接、语句、结果集一定关闭任何一个地方漏了conn.close()连接池都会被慢慢耗尽。最后检查一下maxWaitMillis的报错日志如果频繁出现Pool empty把maxTotal从 20 提到 50 也能缓解但治标不治本索引才是关键。6. 让选课系统经得起验证并发模拟、日志与异常留痕这个项目做到能选课、能退课、能看已选列表只是“能用”。要让它经得起答辩、经得起真实使用你还需要两个升级日志和并发验证。日志方面至少要在 Service 层记下每次选课和退课的关键信息。在选课的catch块里打印studentId和courseId这样用户报“我明明选上了却没有记录”时你能从日志里看到他的请求到底走到哪一步失败了而不是让用户去回忆自己点了什么。我自己早期做选课系统就没打日志上线后收到“选课显示成功但列表里没有”的反馈全靠猜极其痛苦。后来我养成了习惯所有写操作在入口处记录参数、在事务提交后记录结果问题定位速度提升了不只一个量级。并发验证方面最简单的做法是写一个用 Java 的ExecutorService模拟 50 个线程同时选同一门只剩 5 个名额的课断言最终成功数等于 5选课记录表里也正好 5 条。这个测试不需要引入 JUnit 的并发库用 CountDownLatch 就能让所有线程同时起跑测完你对自己写的FOR UPDATE事务有没有生效心里就有底了。ExecutorService pool Executors.newFixedThreadPool(10); CountDownLatch latch new CountDownLatch(10); AtomicInteger successCount new AtomicInteger(0); for (int i 0; i 10; i) { pool.submit(() - { latch.await(); if (courseService.selectCourse(S001, C001)) { successCount.incrementAndGet(); } return null; }); } pool.shutdown();这段代码的要点是latch.await()让 10 个线程统一等到信号释放后同时发起选课制造真正的并发场景。AtomicInteger用来安全地累计成功次数。如果最终successCount大于课程剩余名额说明你的防超卖逻辑没有生效需要回到事务和锁的代码里去排查。我做完这个测试后发现现实情况比想象刺激得多——即使写了 FOR UPDATE如果事务里查询和更新用的是不同连接锁是不会生效的因为每个连接的事务是独立的。这算是我在并发问题上最深刻的翻车教训写出来也算给你留个参考。希望帮到你。本文还有配套的精品资源点击获取
返回列表