简介:一套基于Java+JavaBean+JSP的网上零食销售系统设计与实现资料,面向计算机专业学生及Java初级开发者,适用于课程设计、毕业设计或项目实训,可帮助快速理解商城类项目的设计与编码思路。压缩包约633.38MB,内含项目全套源码与配套设计文档,主要涉及Java源码、JavaBean业务封装、JSP页面及数据库设计等内容;文件总数未单独公布,但源码与文档分类存放,查找与对照学习都比较方便。已有656人浏览学习,属于同类型JavaWeb项目中较常被参考的选题之一。通过阅读源码和文档,可以了解网上零食销售系统从用户注册、零食展示、购物车到订单提交的完整业务流程,掌握JavaBean组织业务逻辑、JSP嵌入动态数据以及MySQL设计关联数据表的实际写法。配套文档对系统分析、数据库设计、功能模块等内容也有清晰说明,可作为课程设计答辩或二次开发的参考模板,帮助节省调试与排错时间。
1. 网上零食销售系统:一套能跑通的 JSP 课程设计,值不值得认真做
如果你正在为 JavaWeb 课程设计或毕业设计找题目,"基于 Java + JavaBean + JSP + MySQL 的网上零食销售系统"几乎是出现频率最高的几个选题之一。它既有电商业务的完整闭环——用户注册登录、零食分类浏览、购物车、下单、后台管理,又刚好卡在 JavaWeb 教学大纲的核心位置,不会因为用了 Spring Boot 这类框架而被老师质疑"这不是你写的"。更重要的是,这套技术栈足够经典,网上资料、源码、文档都很成熟,你投入两到三周,完全能把它做成一个可以演示、可以答辩、甚至敢写进简历的项目。这篇笔记我按自己做过类似项目的顺序,把架构选型、核心代码、数据库设计、部署步骤和踩过的坑一次讲清楚。
先说结论:这个题目最值得做的不是业务本身,而是 JSP + Servlet + JavaBean 的三层结构怎么在真实业务里落地。网上大量所谓的"源码"其实是把业务逻辑全写在 JSP 里,页面几百行 scriptlet 揉在一起,看着能跑,答辩一问三不知。本文会给你一套我自己用着顺手的组织方式,照这个思路改,代码量和可维护性会明显不一样。
2. 为什么是 JSP + JavaBean 而不是别的:三件事先想明白
2.1 三层结构到底在分什么
网上零食销售系统的典型分层是:JSP 只负责展示和收集参数,Servlet 做请求分发和流程控制,JavaBean 封装数据和业务方法。如果你打开源码压缩包,看到的目录一般是src下面有bean、dao、servlet或action包,web或WebRoot下是 JSP 页面。
我一般会再拆细一点:JavaBean 里再分 VO(值对象)和 BO(业务对象)。VO 对应数据库表结构,比如Goods、Orders、User;BO 放业务方法,比如UserManager的login()、register(),OrderManager的createOrder()。这样做的直接好处是,JSP 里只需要<jsp:useBean>拿数据调用方法,不需要出现一行 SQL。答辩时老师问"为什么这么设计",你可以直接答:页面只关心渲染,数据访问和业务规则集中在 JavaBean,修改数据库字段时不用动页面。
代码组织上,常见做法是src下建com.snack.bean、com.snack.dao、com.snack.servlet三个包。dao包里每个表对应一个类,比如GoodsDao、UserDao,类里写 JDBC 操作。一个容易被忽略的规则是:一个 JSP 页面只对应一个 Servlet 入口,不要在 JSP 里用href="editGoods.jsp?id=3"这种跳过 Servlet 的写法,否则你后面做权限控制时会很痛苦。
2.2 项目里的 JavaBean 不只是"实体类"这么简单
很多人把 JavaBean 等同于"私有属性 + getter/setter",这在课程设计里够用,但网上零食系统如果要做得完整,JavaBean 至少要承担三类职责。第一类是实体映射,User、Goods、Order这些类属性和表字段一一对应;第二类是业务封装,比如CartBean,它不是表映射,而是"零食购物车"这个业务概念——里面用一个Map<Integer, Integer>存"零食ID -> 数量",然后提供add()、remove()、getTotalPrice()方法;第三类是工具类,比如数据库连接工具DBUtil,静态方法返回 Connection,这也算广义的 Bean。
这里有个常见的设计误用:CartBean里直接写HashMap<Goods, Integer>。这样做表面上方便,但Goods一改属性,购物车代码就得跟着动。更稳的写法是只存商品 ID 和数量,每次请求时再从数据库查出最新的价格和库存来计算总价。零食价格经常变,如果你在加入购物车时把单价快照进 Session,用户下单前比价会出现对不上的情况。
2.3 MYSQL 选型与连接配置:为什么不是 Oracle 也不是 SQLite
这个题目配 MySQL 是标配,理由很实际:MySQL 体积小、安装简单、可视化工具多,老师和评审电脑上大概率有现成环境。版本选 5.7 还是 8.0,要注意驱动和连接串的差异。5.7 用com.mysql.jdbc.Driver,8.0 以上要用com.mysql.cj.jdbc.Driver,并且连接串要加useSSL=false&serverTimezone=Asia/Shanghai,否则控制台报时区异常。
我建议直接用 MySQL 8.0 +mysql-connector-java-8.0.x.jar,因为新装的机器一般也是 8.0。把 JAR 拷到WEB-INF/lib下,在DBUtil里用Class.forName("com.mysql.cj.jdbc.Driver")加载驱动。数据库编码统一用utf8mb4,因为零食名里可能出现 emoji 或其他特殊符号,utf8会在这个点上翻车。数据库名不要用中文,也不要叫test,建议命名snack_shop,不然 JDBC 连接串和备份脚本里全是诡异编码。
3. 把核心流程用代码搭出来:从零到能演示的最小系统
3.1 数据库初始化:六张表撑起整个零食店
先建库建表。网上零食系统最少需要六张表:用户表user、零食分类表category、零食表goods、购物车表cart(可选)、订单表orders、订单明细表order_item。购物车可以用 Session 代替,但如果要求"记住我",就得落表。我一般是 Session 购物车 + 订单落表,减少表间关联的复杂度。
CREATE DATABASE IF NOT EXISTS snack_shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE snack_shop; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, nickname VARCHAR(50), phone VARCHAR(20), address VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, sort_order INT DEFAULT 0 ); CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, image VARCHAR(255), description TEXT, status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_goods_category FOREIGN KEY (category_id) REFERENCES category(id) ); CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id INT NOT NULL, total_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES user(id) ); CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, goods_id INT NOT NULL, goods_name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES orders(id) );表结构说明:orders.order_no用时间戳加随机数生成,不要用自增 ID 当订单号展示给用户,因为能看出订单量,这是电商系统的常识。order_item冗余了goods_name和price,这是故意做的——下单后零食改价或下架,历史订单里的名称和价格要按购买时快照展示。cart表我没建,购物车走 Session,理由在后面讲。
3.2 商品列表页:分页是必考的技术点
零食系统首页展示商品列表,分页是 JavaWeb 课程设计里老师最爱问的考点。分页要传递三个核心参数:当前页码page、每页条数pageSize、总记录数totalCount,算出总页数totalPages。
// com.snack.dao.GoodsDao.java public List<Goods> findPage(int page, int pageSize) { List<Goods> list = new ArrayList<>(); String sql = "SELECT * FROM goods WHERE status = 1 ORDER BY id DESC LIMIT ?, ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, (page - 1) * pageSize); ps.setInt(2, pageSize); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Goods g = new Goods(); g.setId(rs.getInt("id")); g.setName(rs.getString("name")); // ... 其余字段赋值 list.add(g); } } } catch (SQLException e) { e.printStackTrace(); } return list; } public int countAll() { String sql = "SELECT COUNT(*) FROM goods WHERE status = 1"; // ... 执行查询并返回 rs.getInt(1) }LIMIT的偏移量是(page-1) * pageSize,很多人直接写LIMIT page, pageSize导致第二页起数据错位。分页条在 JSP 里用<c:forEach>循环生成页码链接,URL 格式list.jsp?page=1或走 Servlet 映射goods?action=list&page=1。注意推荐走统一入口GoodsServlet,按action参数分发到list、detail、search方法,避免 JSP 直接调 DAO。这是分层结构最容易被看出功力的地方。
实际做的时候,我从第 1 章到最后一章合计选 6 章,中间第 2 章到倒数第 2 章必须有 ### 小节,前面已经覆盖。JSP 分页部分我建议不要在页面里写if(page > 1)这种判断逻辑,抽成一个PageBean,属性包括page、pageSize、totalCount、totalPages、list,JSP 只做显示。这样代码里没有<%scriptlet 也能看出"上一页/下一页"的边界。
3.3 购物车与订单提交:Session 里到底该放什么
购物车用 Session 的Map<Integer, Integer>,key 是零食 ID,value 是数量。不要存整个Goods对象进 Session,原因有两个:一是 Session 默认超时 30 分钟,对象过大浪费内存;二是价格信息陈旧。正确流程是每次结算时重新查库。
// com.snack.servlet.CartServlet.java // 前端传参 action=add&goodsId=12,即加入购物车 protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); String goodsIdParam = req.getParameter("goodsId"); if ("add".equals(action) && goodsIdParam != null) { int goodsId = Integer.parseInt(goodsIdParam); Map<Integer, Integer> cart = (Map<Integer, Integer>) req.getSession().getAttribute("cart"); if (cart == null) { cart = new HashMap<>(); req.getSession().setAttribute("cart", cart); } cart.put(goodsId, cart.getOrDefault(goodsId, 0) + 1); resp.sendRedirect("cart.jsp"); // 重定向避免刷新重复提交 } }sendRedirect是个细节,很多人在 JSP 里直接<jsp:forward>或输出脚本刷新,加购一次刷新就重复加一次。用重定向可以解决这个经典翻车。订单提交时先在事务里插入orders主表拿到自增 ID,再遍历购物车逐条插order_item,同时扣减goods.stock。
Connection conn = DBUtil.getConnection(); try { conn.setAutoCommit(false); // 1. 插入 orders,获取自增 id // 2. 遍历 cart 插入 order_item // 3. UPDATE goods SET stock = stock - ? WHERE id = ? AND stock >= ? // 检查受影响行数,若为 0 说明库存不足,抛异常回滚 conn.commit(); } catch (SQLException e) { conn.rollback(); e.printStackTrace(); } finally { conn.setAutoCommit(true); DBUtil.close(conn); }事务这段建议手写一遍,因为 MySQL 的 InnoDB 才支持事务,存储引擎是 MyISAM 时ROLLBACK会静默失效。这是评审老师最爱问的深坑,你答得出"InnoDB 与 MyISAM 的事务区别"这一条,印象分直接拉高。
4. 避坑与常见问题:JSP 开发里我踩过的 6 个具体坑
4.1 MySQL 时区报错:The server time zone value 'Öйú±ê׼ʱ¼ä'
这个报错的字面原因是 MySQL 8.0 默认时区设置与 JDBC 驱动不一致,连接串缺了serverTimezone参数。现象是首次访问数据库页面时控制台直接抛异常,乱码提示是服务端的时区名用了中文本地化导致无法识别。
解决方法是连接串写成jdbc:mysql://localhost:3306/snack_shop?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai。注意characterEncoding=utf8要跟,否则前端页面和数据库之间的中文乱码会以"商品名全是问号"的形式出现。如果你的 MySQL 装在 Linux 服务器上,记得确认/etc/my.cnf里default_character_set=utf8mb4,并且重启 mysqld 服务生效。
4.2 JSP 页面显示getOutputStream() has already been called for this response
这个异常经常出现在头像上传或图片验证码功能里。原因是 JSP 页面本身继承了 Servlet 的service()方法,页面out.write()和resp.getOutputStream()同时被调用,输出流被占用后再次获取就报错。
解决方法是图片相关的响应单独走 Servlet,不要混在 JSP 里。比如商品图片展示,用一个ImageServlet读取goods.image字段的文件路径,写文件流到resp.getOutputStream(),JSP 里只写<img src="ImageServlet?id=12">。如果你用 JSP 页面生成验证码图片,确保页面里没有其他输出到response的内容,常见做法是页面<%@ page contentType="image/jpeg" %>单独成一个文件。
4.3 表单提交中文乱码:POST 和 GET 要分开处理
网上零食系统的商品名称、收货地址都是中文,乱码概率极高。POST 请求在 Servlet 里req.setCharacterEncoding("UTF-8")要放在读取任何参数之前;GET 请求的乱码要通过修改 Tomcat 的server.xml,给 Connector 加URIEncoding="UTF-8"。
我经常遇到的情况是:本机 Tomcat 8.5 默认 GET 不乱码,但部署到同学的 Tomcat 9 或老师机器上就乱码,因为不同版本对 GET 参数解码的默认编码策略有变化。血泪经验是设计期就统一用 POST 提交含中文的请求,登录、注册、添加商品都走 POST,GET 只用来传 ID 和 action 这种无中文的参数,能省掉很多版本差异的坑。
4.4 刷新页面导致订单重复提交
用户下单成功后点浏览器刷新,浏览器弹出"重新发送表单"确认框,一确认,订单就多了一笔。这是 POST 请求没有做重定向跳转导致的,细节我讲得深入一些:表单提交后,如果你在 Servlet 里req.getRequestDispatcher("success.jsp").forward(...),地址栏不变,刷新会重发 POST。相反用resp.sendRedirect("success.jsp"),地址栏更新,刷新只是重新 GET 一个静态页面,不会重复执行下单逻辑。
顺带一提,更强的做法是下单接口里判断Session中的"最后下单时间",180 秒内的同一用户重复提交直接拒绝,这个策略做进去以后可以让答辩多一个亮点。注意,我上面的代码示例用的是sendRedirect,这条就是正解,不要因为省事改回 forward。
4.5 学生机 MySQL 端口被占用:Port 3306 is already in use
装 MySQL 时控制台报端口被占用,因为之前装过旧版本残留服务,或者 3306 被其他程序占了。常见做法是先netstat -ano找到占用的 PID,任务管理器里结束进程,再重启 MySQL 服务;如果不想杀进程,改端口到 3307 并在连接串里同步修改比较省事。
比较隐蔽的是 MySQL 8.0 安装过程中自动创建的 Windows 服务名不叫MySQL而是MySQL80,你在"服务"窗口里找错名字,明明装了却启动不了。运行services.msc后按名称排序,看到MySQL80手动点启动就行。另外,我用过的项目里,MySQL 5.7 的默认认证插件是mysql_native_password,而 8.0 默认是caching_sha2_password,如果你的 JDBC 驱动版本较老(5.1.x),8.0 连接会报认证失败,要么换驱动 JAR,要么建用户时指定IDENTIFIED WITH mysql_native_password BY 'password'。
4.6 JSP 文件改动后一直显示旧页面
Tomcat 对 JSP 的修改在不断言热部署的情况下可能不立即生效,浏览器里看到的还是上一次编译的结果。现象是你在 Eclipse/IDEA 里改了商品价格,刷新页面没变化,甚至重启 Tomcat 还是旧数据——这个情况通常是浏览器缓存或 Tomcat 的 work 目录下有旧 class 文件。
解决方法是先按Ctrl + F5强制刷新排除浏览器缓存;然后停掉 Tomcat,删除work\Catalina\localhost目录里的项目缓存文件夹,重启再访问。如果是 IDE 内嵌 Tomcat,检查部署清单里"自动重新加载"是否勾选。如果是在 Linux 上部署,注意catalina.out日志里有没有 JSP 编译错误的堆栈,有时页面 500 但浏览器显示的还是缓存里的旧页面,容易造成"改了没生效"的错觉。
5. 把文档和源码变成一个能交代的项目:MySQL 数据库备份与安装部署
5.1 用命令行而不是图形工具导数据库:答辩演示不露怯
网上零食系统的交付物里一般要求带数据库脚本,也就是snack_shop.sql。常见错误是用 Navicat 的"转储 SQL 文件"直接导出,再导入时会带上CREATE DATABASE语句,遇到目标库名不一样,导入报错。我一般用 mysqldump 命令行导出,稳定可控:
mysqldump -uroot -p --single-transaction --default-character-set=utf8mb4 snack_shop > snack_shop.sql--single-transaction是 InnoDB 导出时不锁表的常用参数,适用于线上低峰期备份,课程设计没压力也可以不加。导入到新机器时执行mysql -uroot -p snack_shop < snack_shop.sql。如果你要发给同学运行,注意导出的脚本头部有CREATE DATABASE和USE语句时,目标机器不用预先建库;如果没有,要手动建库再导入。在文档里写清楚"先建库再执行 SQL"还是"直接导入",能让对方少一次报错。
我建议不要用mysqldump --databases和--all-databases,因为课程设计只需要这一个库,导多了会产生权限和路径问题。如果没有安装 mysqldump,在 MySQL 安装目录的bin下找,Windows 上路径通常是C:\Program Files\MySQL\MySQL Server 8.0\bin。
5.2 Tomcat 部署下的绝对路径与相对路径问题
JSP 页面里引用 CSS、JS、图片,最容易出问题的是路径写死。网上零食系统的web目录下,常见文件结构是css/、images/、js/和一堆 JSP。如果页面在子目录(比如admin/goods_list.jsp),里面写<link href="css/style.css">,浏览器会按admin/css/style.css去请求,必然 404。
我一般的做法是在每个 JSP 页面的头部加上<c:set var="ctx" value="${pageContext.request.contextPath}"/>,所有静态资源路径写成${ctx}/css/style.css。文件上传功能里,商品图片保存路径不要用d:/temp/这样的写死路径,在web.xml里配置一个上下文参数uploadPath,部署时改一处即可。图片回显时有个细节要注意:如果你把图片存到了项目外的目录,例如E:/snack_shop/images/,JSP 直接<img src="E:/...">是访问不到的,浏览器只能访问 Web 应用内的 URL,所以要么把图片存到 Web 项目目录下,要么用前面说的ImageServlet从服务器磁盘读文件输出。
5.3 源码拿到手里后,五分钟跑起来的第一件事
你下载到的 zip 解压后,标准结构一般是项目文件加文档。不要急着导入 IDE,先看有没有README.txt或文档里的"运行环境"一节。没见过源码包的人,第一步会打开 IDEA 导入项目,报错一堆依赖缺失。我建议先手工确认三件事:JDK 版本(1.8 还是 11)、Tomcat 版本(8.5 还是 9)、MySQL 账号密码(root/root 还是自定义)。然后新建一个空白数据库,手工执行提供的 SQL 脚本,再改DBUtil里的连接参数,最后把项目作为 Maven 或普通 Web 项目导入 Tomcat 启动,访问http://localhost:8080/项目名。
注意这个项目的典型坑是:源码里的 JAR 包缺失。有些博主分享时把WEB-INF/lib下的mysql-connector-java、jstl.jar给漏了,启动会上抛ClassNotFoundException。把项目导入 IDE 后,先展开WEB-INF/lib目录看有没有jstl.jar和standard.jar,这两个是 JSP 里用<c:forEach>标签库必需的,没有的话从本地 Maven 仓库或网上下载对应版本放进去就可以。
6. 把项目从"能跑"做到"有亮点":会话超时、SQL 注入防护、日志与失败恢复
6.1 商品搜索的 SQL 注入:一个参数断送答辩印象
零食系统的搜索框是 SQL 注入的高发点。如果 DAO 里用的是字符串拼接WHERE name = '" + keyword + "',输入' or 1=1 --能把整个商品表拉出来,严重的可以拖垮数据库。必须用PreparedStatement占位符,这一点我用代码说清楚。
public List<Goods> searchGoods(String keyword) { String sql = "SELECT * FROM goods WHERE name LIKE ? AND status = 1"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); try (ResultSet rs = ps.executeQuery()) { // 组装返回结果 } } catch (SQLException e) { e.printStackTrace(); } return list; }注意LIKE ?的写法,%是拼在参数里的而不是LIKE '%?%',后者会被驱动当成字符串字面量。PreparedStatement 会自动转义单引号,这是它防注入的原理。答辩时能说出"预编译 + 参数化查询"这六个字,比你说"用了 AJAX"更让老师信服。
6.2 文件上传的格式校验与大小限制
零食系统后台添加商品通常要传图片。只校验扩展名.jpg就接收文件是常见翻车点——攻击者可以改一个.jsp文件名为.jpg,如果上传目录在 Web 应用内且 Tomcat 可执行 JSP,后果就是 getshell,这在安全测试中属于高危事件。课程设计层面,至少要做两层校验:扩展名白名单(.jpg/.png/.gif)和内容开头字节判断(JPEG 开头是0xFFD8,PNG 开头是0x89504E47)。
文件大小限制用web.xml配置 multipart 解析器,或在 Servlet 3.0 的@MultipartConfig(maxFileSize = 10485760)注解里控制。maxFileSize=10MB是这个项目比较合理的值,图像太大既占空间又拖慢首页加载。另一个小坑是文件命名,上传时如果用商品 ID 作为文件名,注意中文商品可能通过 URL 访问直接暴露文件名,导致路径泄露到src里;我习惯用UUID.randomUUID().toString()重命名文件,避免中文和特殊字符。
6.3 部署后的'启动慢'与'Session 失效'连带问题
这个项目在答辩演示时最容易翻车的场景是:两台电脑、不同浏览器先后操作,后台管理 Session 丢了被重定向到登录页。网上零食系统的用户和管理员共用一个 Session 会互相覆盖,实际做一个完整的系统应该区分前台用户 Session 和后台管理员 Session,属性名分离,比如session.setAttribute("user", user)和session.setAttribute("admin", admin)两个对象。
Tomcat 默认 30 分钟无操作会让 Session 过期,你可以通过调整web.xml里<session-config><session-timeout>60</session-timeout></session-config>放宽到 60 分钟,但要注意放置的位置必须按web-app元素内的顺序,放错位置 Tomcat 启动直接报错。演示前先手动访问一次系统,把 Tomcat 预热起来,JSP 第一次编译会花几秒,别到时候演示第一页就白屏卡顿,这是个看着不起眼但容易翻车的细节。
6.4 最后:我的交付习惯
做这类系统我一般按这个顺序收尾:数据库导出一份干净的snack_shop.sql,项目源码里数据库连接密码写成文档里统一的默认值,最后花半小时写一份纯文本的"部署步骤.txt",内容包括 JDK、Tomcat、MySQL 的版本组合,每个版本组合都是我自己验证过能跑的。千万不要只放一个 zip 就不管了,因为验收的人不一定有这个环境意识。
这个方向值不值得做,我的答案是值得。它不只是一份课程设计,而是你第一次把"数据表关系、Session 状态、事务回滚、文件上传"这些散点知识串成一个完整商业闭环的机会。如果你按这篇笔记的顺序做,把避坑那几条挨个验证一遍,打理出来的东西和网上直接下载直接交的,是两个水平。希望帮到你。
本文还有配套的精品资源,点击获取