简介:这是一份基于Java的图书管理系统完整源代码,采用MVC设计模式,覆盖图书录入、查询、借阅、归还、续借及状态跟踪等核心业务,适合Java初学者、课程设计或毕业设计参考。压缩包共212个文件,包含31个Java源文件、112个编译后的class文件、56张JPG界面截图、数据库文件(db)以及依赖jar包等,整体大小4.01MB,目录结构清晰,便于按模块对应学习。系统采用分层架构,模型层负责数据存储,视图层负责界面展示,控制器协调交互,并整合关系型数据库进行图书、读者及借阅记录的管理。已有1872人学习下载。源码涵盖数据库设计、后端业务逻辑、前端交互、用户认证等完整链路,并附有界面截图和UML模型文件,可帮助读者从代码和设计文档两个维度理解图书借阅流程与Java工程化开发思路。
1. 为什么每个 Java 初学者都应该自己重写一遍图书管理系统源码
搜“Java图书管理系统源代码”的人,多半是卡在课程设计或毕业设计的第一关:网上随便一搜就有整套包,下载解压、配个数据库、点一下运行,功能齐全,甚至带答辩PPT。但真正带着这套源码走进答辩教室时,很多人会被一个问题问住:“借书时如果两个人同时点,库存怎么保证不变成负数?”我见过太多拿着现成源码却讲不清三层架构的候选人,而图书管理系统恰好是讲清这件事的最小载体。它把登录鉴权、CRUD、分页查询、状态流转、事务控制这五块 Java 后端面试必考的内容揉进了一个两百页不到的小项目里。这篇笔记不打算替你做选择题,而是把一套能跑通、能讲清、能二次开发的源码方案拆给你看,适合正在做课程设计的学生、准备软考或蓝桥杯的考生,以及想系统补一遍 Java Web 基础的一线工程师。
2. 把功能模块先拆对:三层架构、四张核心表与包结构
2.1 图书管理系统的标准功能清单:不止增删改查
很多从网上下载的源码,功能列表长得吓人,但真正经得起追问的只有几条主线。图书管理系统最核心的闭环是“图书—读者—借还记录”这三者的关系,功能上可以收敛成五组:管理员登录与会话管理,图书的添加、编辑、下架与库存维护,读者的注册、禁用与借阅上限控制,借书和还书两条带状态流转的业务链路,以及图书查询和借阅记录查询的分页列表。至于图书分类树、滞纳金计算、公告栏,都属于锦上添花,不建议在第一次做的时候塞进核心链路。
我建议你在动手前先画一张简单的数据流草图:管理员登录后维护图书和读者,读者发起借书请求,系统检查读者状态、借阅数量和图书库存,然后扣减库存并生成一条待归还记录;还书时反向操作,恢复库存并把记录状态置为已归还。这张草图的价值在于,它能直接映射出后面你要建的四张表和写的那几个事务方法。如果一上来就照着网上的源码抄,你大概率会漏掉“读者借阅上限”和“图书上下架状态”这两个隐藏需求,而它们恰恰是答辩时老师最爱追问的点。
2.2 四张核心表的设计:字段、约束与索引
数据库表设计决定了后面所有 SQL 的写法。常见的做法是用四张表:管理员表 admin、图书表 book、读者表 reader、借阅记录表 borrow_record。不要把读者和用户混成一张表,也别把借阅记录嵌进图书表里,否则分页查询和统计会非常痛苦。下面是建表语句,我把字段注释和关键约束直接写在 SQL 里:
CREATE TABLE admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(255) NOT NULL COMMENT '推荐存MD5或BCrypt摘要', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='管理员表'; CREATE TABLE book ( id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL UNIQUE COMMENT 'ISBN,重复录入拦截', book_name VARCHAR(100) NOT NULL, author VARCHAR(50), publisher VARCHAR(100), category VARCHAR(30) COMMENT '分类,可做简单分组', total_stock INT NOT NULL DEFAULT 0 COMMENT '总库存', available_stock INT NOT NULL DEFAULT 0 COMMENT '当前可借库存', status TINYINT DEFAULT 1 COMMENT '1上架 0下架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_book_name (book_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书表'; CREATE TABLE reader ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(255) NOT NULL, real_name VARCHAR(50), max_borrow INT NOT NULL DEFAULT 5 COMMENT '最大可借数量', status TINYINT DEFAULT 1 COMMENT '1正常 0禁用', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='读者表'; CREATE TABLE borrow_record ( id INT PRIMARY KEY AUTO_INCREMENT, reader_id INT NOT NULL, book_id INT NOT NULL, borrow_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, due_time DATETIME NOT NULL COMMENT '应还时间,由业务层计算', return_time DATETIME DEFAULT NULL, status TINYINT DEFAULT 0 COMMENT '0借出 1已还 2逾期归还', KEY idx_reader_status (reader_id, status), KEY idx_book_id (book_id), CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader(id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='借阅记录表';这段 SQL 里值得注意的有三处。一是所有表都显式指定了 InnoDB 和 utf8mb4,前者保证后续借书事务里的行锁生效,后者避免生僻字和 Emoji 存不进去。二是在 borrow_record 上建了 (reader_id, status) 联合索引,因为“查询某个读者当前未还的借阅数量”会高频出现,走这个索引比全表扫描快得多。三是密码字段我注释了“推荐存摘要”,明文存密码在答辩里属于硬伤,哪怕你不写注册功能也建议把这条规范带上。
2.3 代码包结构怎么分:从 controller 到 dao 的依赖方向
拿到源码先把包结构看明白,比自己从头建要快得多。标准的 Java Web 图书管理系统会按三层架构拆包:web/servlet 层负责接收请求和返回响应,service 层放业务规则,dao 层封装 JDBC 或 MyBatis 的数据访问。实体类单独放在 entity/pojo 包,工具类和过滤器放在 util/filter 包。依赖方向必须是 servlet → service → dao,禁止反向调用,这是答辩时最容易讲清也最容易被挑刺的地方。
com.library ├── web # Servlet、Filter、监听器 ├── service # 业务接口和实现类 ├── dao # 数据访问接口和实现类 ├── entity # Book、Reader、BorrowRecord 等实体 └── util # DBUtil、MD5Util、分页工具类如果你选的技术栈是 Spring Boot + MyBatis,包结构会变成 controller、service、mapper、entity 四层,职责边界和传统三层是等价的。我的建议是:课程设计优先用 JSP + Servlet + JDBC 的经典组合,因为它能把 HTTP、会话、JDBC 这些底层机制暴露在你面前,面试官问起来你有的说。如果项目要求里写了“采用 SSM 或 Spring Boot”,再切换也不难,业务逻辑代码基本可以平移。
3. 从 0 到 1 搭出可运行的最小系统:版本选型、登录链路与关键配置
3.1 版本选型:JDK、Tomcat、MySQL 的常见搭配与兼容性
环境配置是整条链路里最玄学的一环,很多源码跑不起来不是代码问题,而是 JDK 和 Tomcat 版本打架。下面的表格是我整理过的常见稳定搭配,照着选能少踩一半坑:
| 技术栈 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 或 11 | 老项目源码用 JDK 8 最稳,JDK 11 也兼容大部分旧代码 |
| Tomcat | 8.5(JDK8) / 9.0 | Tomcat 10 把 javax 改成了 jakarta,旧源码会直接编译报错 |
| MySQL | 5.7 或 8.0 | 5.7 轻量,8.0 的 Connector/J 驱动名和时区参数有变化 |
| MySQL Connector/J | 5.1.49(配5.7)/ 8.0.33(配8.0) | 驱动别混用,8.0 驱动连 5.7 会有 SSL 警告,但不影响功能 |
| 代码结构 | Maven 或纯 Web 项目 | 有 Maven 就用 Maven,没有就手动导 jar,别两个都来 |
这里最容易翻车的是 Tomcat 10 和 javax.servlet 包名不兼容:网上下载的老源码用了 javax.servlet.http.HttpServlet,放到 Tomcat 10 下编译直接报错。我的习惯是写任何 Java Web 项目之前,先在 pom.xml 或 lib 目录里确认 Servlet API 的版本来源。如果是纯 Servlet 项目,直接建一个 Dynamic Web Project 然后选 Tomcat 8.5 运行时,能省掉一晚上的排查时间。
3.2 登录链路怎么写:从浏览器请求到数据库校验
登录功能是图书管理系统里唯一涉及会话管理的模块,也是最能体现“三层架构”的演示片段。我见过不少源码为了省事,在 JSP 里直接写 JDBC 连接和 SQL 查询,看起来也能跑,但答辩时老师一眼就能看出你没理解分层。这里给出一个标准的登录链路,从 Servlet 接收参数开始:
// web/LoginServlet.java @WebServlet("/login") public class LoginServlet extends HttpServlet { private AdminService adminService = new AdminServiceImpl(); @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); // 基础校验:空值直接打回,避免无效请求打到数据库 if (username == null || username.trim().isEmpty() || password == null || password.isEmpty()) { req.setAttribute("error", "用户名和密码不能为空"); req.getRequestDispatcher("/login.jsp").forward(req, resp); return; } // 业务层校验:这里应该包含密码摘要比对和状态检查 Admin admin = adminService.login(username, password); if (admin != null) { HttpSession session = req.getSession(); session.setAttribute("admin", admin); resp.sendRedirect(req.getContextPath() + "/book/list"); } else { req.setAttribute("error", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }这段代码的逻辑分三段:先做参数校验,再调 service 层完成业务校验,最后根据结果决定是跳转成功页还是返回登录页。注意我用了 req.getRequestDispatcher().forward() 而不是 sendRedirect() 返回错误信息,因为 forward 可以携带 request 作用域里的 error 属性到 JSP 页面渲染,而 redirect 会丢掉。如果你想让失败时地址栏不变,这种写法是标准做法。成功路径使用 sendRedirect 是避免表单重复提交的常见手段。
对应地,service 层的 login 方法只做一件事:调用 dao 查询,比对密码,返回实体或 null。这里有一个值得养成习惯的细节:密码比对不要在 Servlet 里做,而是放在 service 层,因为“密码摘要算法如果从 MD5 换成 SHA-256,只有 service 层需要改动”。JSP 页面永远不要出现“数据库”三个字。
3.3 分页与模糊查询:两条最常用 SQL 的写法与参数绑定
图书列表是系统的门面,而分页是几乎每一个课程设计都会被追问的点。分页 SQL 本身不难,难的是参数计算和总条数查询的封装。常见的做法是先查 totalCount,再算 offset 查当前页数据,两条 SQL 放在同一个 dao 方法里返回一个 Page 对象:
// dao/BookDao.java public Page<Book> findBooksByPage(int pageNum, int pageSize, String keyword) { // 1. 计算偏移量,注意 pageNum 从 1 开始 int offset = (pageNum - 1) * pageSize; // 2. 查询总条数,关键词使用 CONCAT 拼接 % 符号,防止用户输入通配符 String countSql = "SELECT COUNT(*) FROM book WHERE status = 1 AND book_name LIKE CONCAT('%', ?, '%')"; int totalCount = queryForInt(countSql, keyword); // 3. 查询当前页数据,按创建时间倒序 String dataSql = "SELECT * FROM book WHERE status = 1 AND book_name LIKE CONCAT('%', ?, '%') " + "ORDER BY create_time DESC LIMIT ?, ?"; List<Book> list = queryForList(dataSql, keyword, offset, pageSize); return new Page<>(pageNum, pageSize, totalCount, list); }这里推荐用 CONCAT('%', ?, '%') 而不是直接在传参时拼 "%" + keyword + "%",原因是前者不会把用户输入里的 % 和 _ 当成通配符,能减少一类 SQL 注入和模糊查询结果异常的问题。LIMIT 后面两个参数都用占位符 ? 传 int 值,offset 必须在 SQL 层计算好,别在 SQL 里写 LIMIT (page-1)*pageSize, pageSize,MySQL 不支持这种表达式语法,那会导致每次翻页都报语法错误。
4. 借书与还书链路:状态机、并发和行锁的三个考验
4.1 借书时的“先查后扣”为什么会有并发翻车
借书的业务规则看起来就四步:查读者是否存在且状态正常、查读者未还数量是否达到上限、查图书是否上架且库存大于零、扣库存并插入借阅记录。但没有任何并发保护的实现,在两个人同时借同一本书的最后一本时,会双双通过库存检查,然后都把库存减一,结果是库存变成负一,货架上凭空多出一本“负数”的书。这就是经典的丢失更新问题,也是面试官最爱让你手写代码的场景。
4.2 用状态字段控制借阅记录的生命周期
借阅记录表里的 status 字段承担了状态机的角色:0 表示借出、1 表示已还、2 表示逾期归还。状态流转的方向是 0 -> 1 或 0 -> 2,不允许从 1 回到 0。还有一个容易被忽略的约束:同一本书在相同读者手里只能有一条 status=0 的记录,否则读者可以“重复借同一本书”,库存会被重复扣。这个约束可以在 SQL 层面用部分唯一索引解决,但 MySQL 不支持部分索引,所以常见做法是查重之后再插入,配合事务保证原子性。
4.3 事务与行锁:三行代码保住库存不为负
借书方法必须放在同一个事务里,任何一步失败都要回滚全部操作。在 Spring 中使用注解是最简洁的写法,如果是纯 Servlet + JDBC 项目,就得手动管理 Connection 的提交和回滚:
// service/impl/BorrowServiceImpl.java @Transactional(rollbackFor = Exception.class) public void borrowBook(int readerId, int bookId) { // 1. 查读者并锁定该行,防止并发修改读者状态 Reader reader = readerDao.selectByIdForUpdate(readerId); if (reader == null || reader.getStatus() == 0) { throw new BusinessException("读者不存在或已被禁用"); } // 2. 统计未还数量,达到上限则拒绝 int borrowingCount = borrowDao.countByReaderAndStatus(readerId, 0); if (borrowingCount >= reader.getMaxBorrow()) { throw new BusinessException("已达到最大借阅数量"); } // 3. 查图书并加行锁,只锁这一行而不是锁全表 Book book = bookDao.selectByIdForUpdate(bookId); if (book == null || book.getStatus() == 0 || book.getAvailableStock() <= 0) { throw new BusinessException("图书不存在、已下架或库存不足"); } // 4. 扣库存 + 插入借阅记录,两步一起成功 bookDao.decreaseStock(bookId); borrowDao.insert(new BorrowRecord(readerId, bookId, new Date(), dueTime(readerId), null, 0)); }这段代码的关键在 selectByIdForUpdate 这两个方法。SELECT ... FOR UPDATE 是 MySQL InnoDB 提供的行级排他锁,事务提交或回滚时才释放。这样两个并发请求同时进来时,第二个会阻塞在第一步的行锁上,等第一个事务提交后再执行它的库存检查,发现库存已经是零就会被业务规则拒绝。这里要注意 FOR UPDATE 必须和事务在同一线程、同一连接里生效,Spring 的声明式事务能保证这一点,但手动 JDBC 时如果开连接的代码没包住整个方法,锁是锁不住的。
另外一个常见误用是把整个图书表锁住:SELECT * FROM book FOR UPDATE 会触发 InnoDB 对多条记录加锁,在表没有索引的情况下甚至退化成表锁,借任何一本书都会阻塞其他所有书的借阅操作。正确的做法是 WHERE id = ? 精确锁定一行,并有主键索引。
5. 真正常见的 5 个坑:数据库连接、中文乱码、端口占用与内存溢出
5.1 数据库连不上:驱动、URL 和防火墙的排错顺序
现象:Tomcat 启动后访问页面报空白或 500,控制台出现 Communications link failure 或 Access denied for user,看起来是数据库没连上。
原因分三类:驱动版本和 MySQL 版本不匹配、连接串参数不对、账号权限或 IP 访问受限。驱动不匹配最典型的报错是 ClassNotFoundException 或 No suitable driver;连接串不对最常见的坑是没带时区参数,MySQL 8.0 驱动会直接拒绝 serverTimezone 为空的连接;账号权限问题则表现为 Access denied。
解决:按顺序排查。先用命令行工具 mysql -u root -p 确认账号密码能登录;第二步在代码里打印连接串,确认 jdbc:mysql://localhost:3306/library_db?characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai 这段地址、端口、库名没有拼错;第三步检查驱动 jar 是不是真的被塞进了 WEB-INF/lib,很多 Maven 项目里依赖声明了但没刷新,导致运行时找不到类。最后再考虑防火墙拦截远端数据库连接的情况,本地开发一般遇不到。
5.2 控制台和页面全是中文乱码:三处编码要统一
现象:查询结果在控制台打印正常,但在 JSP 页面上显示成问号或乱码;或者反过来,控制台乱码但页面正常。更隐蔽的是从浏览器传到后端的中文参数在数据库里变成乱码。
原因:编码不一致的根源是“请求解码字符集、响应编码字符集、数据库连接字符集、数据库表字符集”四者没有统一成 UTF-8。Tomcat 8 以后请求体的解码默认就是 UTF-8 吗?不是,这取决于你是否设置了 CharacterEncodingFilter,而 GET 请求的 URI 解码还受 server.xml 的 URIEncoding 控制。
解决:三处设置一次到位。第一处是所有 Servlet 或 Filter 里设置 request.setCharacterEncoding("UTF-8"),并且必须放在读取任何参数之前;第二处是 JSP 页面顶部写 <%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>;第三处是 MySQL 连接串末尾带 characterEncoding=utf8 并确认表结构是 utf8mb4,而不是把数据库建完就动用 ALTER TABLE 一边一边改。这里有条血泪经验:如果你只改了 JSP 页面编码而连接串没带字符集参数,数据库里会存进已经乱码的数据,这时候再改什么都救不回已经坏掉的行。
5.3 端口被占用导致启动失败:别急着改端口
现象:Tomcat 启动报错 Port 8080 required by Tomcat v9.0 Server is already in use,常见于之前没关干净服务或者别的开发工具占了端口。
原因:端口被上一个残留在后台运行的系统进程占住,也可能是你用 IDE 启动过多个 Tomcat 实例,其中一个没有完全停止。直接把 Tomcat 默认端口改成 8081 是最常见的“绕过”手法,能用但很丑,而且会给后面所有拼接了 8080 地址的代码和演示带来麻烦。
解决:先定位是谁占了端口。Linux 和 macOS 上执行 lsof -i:8080,Windows 上执行 netstat -ano | findstr 8080,拿到 PID 后在任务管理器或使用 kill 命令结束它。然后回到 IDE 看 Servers 视图里是不是有旧实例还在 Running,全部 Stop 后重新启动。如果问题反复出现,检查你是否同时安装了多个版本的 Tomcat 且都配置成了启动时自动运行。
5.4 404 和 500 的差别:路径写错与空指针的定位方向
现象:访问 http://localhost:8080/library/book/list 返回 404,而调用登录接口返回 500 并伴有 NullPointerException 堆栈。
原因:404 八成是请求路径和 Servlet 的 @WebServlet 注解值对不上,比如注解是 /book/list 但 JSP 里表单 action 写了 /bookList;这里还涉及项目上下文路径(ContextPath)的问题,如果你没有用 req.getContextPath() 拼路径,部署后的根路径一变就全乱。500 则九成是空指针:session 里取不到值、查询结果返回 null 后直接调用了属性的 getter、或者请求参数名和 Servlet 里 getParameter 的名字拼写不一致。
解决:404 先看 IDE 控制台的启动日志里 Context path 是什么,把地址栏 URL 和它对齐;再看 JSP 里所有 action 和链接,统一用 ${pageContext.request.contextPath} 作为前缀。500 就看堆栈里第一行 java.lang.NullPointerException 下面的 “at com.library.dao.BookDaoImpl.xxx” 指向哪个方法,往回找是哪条链路传了 null。记住一个总原则:先看 URL 和注解是否匹配,再看日志里由前端到后端的传参链路。
5.5 JVM 内存溢出:调大堆大小只是后悔药
现象:系统运行一段时间后,页面响应越来越慢,最后日志里出现 java.lang.OutOfMemoryError: Java heap space。
原因:代码里存在对象得不到释放的泄漏点,最常见的是 ResultSet、Statement、Connection 没有在 finally 里关闭,或者开启 session 后没有提交和关闭;另一个常见场景是每次请求都 new 一个很大的对象放进 session。调大 JVM 的 -Xmx 参数只能推迟崩溃时间,不能根治问题。
解决:把项目里所有获取连接和查询的代码过一遍,确保 Connection、PreparedStatement、ResultSet 全部在 finally 块中关闭,Java 7 之后可以用 try-with-resources 语法自动关闭。若是 Spring 工程,数据源和 MyBatis 框架会替你把连接放回池里,但你自己手写的 JDBC 代码仍然是泄漏重灾区。排查时可以用 jstat -gcutil 观察每次 Full GC 后的内存曲线,如果持续上升而不回落,就是有对象一直没被回收。
6. 从能跑到能答辩:三个扩展方向、一份自测清单和 Git 提交习惯
6.1 三个值得做的扩展:逾期计算、热门榜单和批量导入
如果基础功能已经跑通,我建议你按“成本递增”的顺序挑一个扩展做。最简单的是逾期计算:借阅记录表里 already 有 due_time,加一个定时任务或在还书方法里计算超过应还天数的记录并把状态置为 2。其次是热门图书榜:在借阅记录表上按 book_id 分组、COUNT 取 Top 10,这条 SQL 简单到只有五行,却能引出 GROUP BY 和 ORDER BY 的组合用法,适合放进简历的“技术亮点”一栏。成本最高的是批量导入,用 Apache POI 读 Excel 后逐条调用新增图书接口,它涉及文件解析、异常回滚和数据校验,工作量翻倍,但如果你的项目描述需要“企业级”三个字,这是最能撑场面的一项。
6.2 提交前必做的自测清单:按正常路径和异常路径分别过一遍
答辩演示最怕的不是功能缺失,而是演示时现场翻车。我把自己踩过坑总结成一张清单,提交前照着过一遍。正常路径:管理员登录成功、新增图书成功、分页翻页和跳页正常、借书成功后库存减一、还书成功后库存加一且记录状态变化。异常路径:登录密码错误、借一本书超过读者上限、库存为零时借书、重复借同一本书、修改图书库存为负数、查询关键字为空和包含特殊字符。每条路径都要在演示前手动走一次,因为代码里最常见的隐藏 bug 是“异常路径被吞掉”:捕获了异常却没有处理,界面显示成功但数据没变。
6.3 一个我后来才养成的习惯:从第一天就用 Git 管理源码
多数人从网上下载的源码包里只有一堆解压后的文件和一份数据库脚本,没有提交历史。我后来自己重写图书管理系统时,第一件事是 git init,每完成一个功能模块就提交一次,比如 feat(book): 实现图书分页查询。这个习惯的回报在答辩时体现得最明显:老师问“这个功能你写了几版”,你可以直接调出 Git 历史展示从硬编码到参数绑定的演进过程,这份“看得到的过程”比成品本身更能证明代码是你自己写出来的,而不是从某个压缩包里解压出来的。当然,你也该为它加上一个亿级用户的幻想:这个两三万行的项目不会真的上线,但它能帮你把“从需求到表结构、从表结构到接口、从接口到页面”的整条链路在面试中讲得滴水不漏。希望帮到你。
本文还有配套的精品资源,点击获取