简介:这是一份面向 Java Web 初学者与课堂教学的简易购物车项目源码包,围绕 JSP、Servlet、Tomcat、HTML 与 Session 展开,覆盖商品展示、加入购物车、删除条目及会话状态保持等基础业务,适合用作课程实验、毕业设计前期练手或 Web 开发入门对照案例。压缩包共 62 个文件、约 916KB,以 class 与 java 源码为主体,配有 jsp 页面、xml 配置、jar 依赖及少量 js、html 静态资源,能够较完整地呈现分层结构与运行所需文件。目前已有 671 人学习下载,说明其作为入门范例具有一定参考价值。读者可通过源码理解 Servlet 处理请求、Session 保存购物车状态以及 JSP 渲染页面的协作方式,体会控制器、视图与业务逻辑分层协作的方式,并借助现有目录结构快速导入 Tomcat 调试,梳理从页面操作到后端响应的完整链路,为后续扩展数据库持久化与安全优化打下基础。
1. 先搞清楚:这份「购物车案例」到底是个什么东西
学到 Java 的集合和循环之后,很多人会停在一个尴尬的位置:语法看得懂,但让他做一个「能在浏览器里点开、点一下有反应」的东西,就不知道从哪下手了。书上的例子要么是控制台打印,要么一上来就是一整套框架,中间那段没人带。这份「购物车案例【简单版】」补的正是这一段:它是一个用 JSP + Java + Tomcat + HTML + Session 搭起来的小项目,功能只有四件事——看商品、加进购物车、看自己选了几件、删掉不要的。它不好看,功能也不全,但它把「浏览器发请求、服务器算一算、页面把结果画出来」这条链路完整走了一遍。适合刚学完 Java 基础、知道 if 和 for 怎么写,却还没做出过一个能被浏览器访问的网页的零基础新手。
2. 为什么入门第一课推荐购物车:三个绕不开的理由
2.1 购物车是 Java Web 的「全流程缩影」
一个看着不起眼的小购物车,实际上把 Java Web 入门阶段最核心的四件事全串起来了:数据怎么展示(HTML 负责画表格,JSP 负责往里塞数据)、用户操作怎么传回服务器(点链接、带参数)、服务器怎么记住这个人刚才选了什么(Session 会话)、处理完怎么回到页面上(跳转)。这四件事就是 Java Web 的全部骨架,剩下的框架、数据库、缓存,都是往这个骨架上挂东西。
如果你一上来就去学一个成熟的框架,最典型的感受是「能跑,但不知道哪一步在干什么」,整个流程变成一个黑匣子,出错了只能猜。而用 JSP 写出来的简单版购物车,每一步都摊开在页面上,你能看到请求参数是怎么拿的、Session 里到底存了个什么对象、跳转去了哪个文件。这种感觉就像学开车先摸手动挡:麻烦,但离合器、油门、挡位之间的关系你心里有数,后面换自动挡只会更轻松,不会更慌。
还有一点容易被忽略:购物车天然带「状态」。列表页不需要记住你是谁,但购物车必须记住——你加到一半去看了别的页面再回来,东西还得在。这个「记住」的需求,恰好把 Session 这个概念逼出来了。很多新手学 Session 学不明白,就是因为只看定义没有场景;放到购物车里,它是刚需,不用背也记得住。
2.2 拆开看:五个关键词各管一摊
在动手之前,先把案例里出现的五个词落到具体位置上。不用背定义,知道「谁负责什么、出问题先看谁」就够了。
| 关键词 | 在案例里干的活 | 出问题时先看哪里 |
|---|---|---|
| HTML | 画表格、画链接、决定长什么样 | 页面结构是不是写错了标签 |
| JSP | 在 HTML 里嵌 Java 代码,负责把数据填进去 | <% %>有没有漏闭合 |
| Java | 定义数据、循环、算总价 | 类型转换、空指针 |
| Tomcat | 把 JSP 编译成 Servlet 并跑起来,对外提供地址 | 部署目录、端口、日志 |
| Session | 在服务器端记住「这个人的购物车」 | 取出来是 null 还是类型不对 |
HTML 和 JSP 在简单版里其实是同一个文件:静态的部分是 HTML,<% %>里的部分是 JSP 语法,<%= %>负责把 Java 变量的值输出到页面上。你可以把它理解成一张表格模板,JSP 负责把行一个个填进去。Java 在这里不是写一个完整的程序,而是写「几行处理逻辑」:把商品放进一个 List,遍历它,算单价乘数量。Tomcat 是你电脑上的一个「小服务器」,你把文件放进它认识的目录,它就能把页面变成一个网址。Session 则是服务器端的一个储物柜,每个浏览器来访时发一个柜子号(Cookie 里的 JSESSIONID),服务器按号取柜子里的东西。
提示:这五个词里,新手最容易轻视的是 Tomcat。很多人以为代码写完页面就能打开,结果双击 JSP 文件用浏览器打开,看到的是源码。JSP 必须有服务器编译才能跑,这是第一道坎。
2.3 动手前先备齐三样东西
第一样是 JDK,版本 8 或更高都行。装完之后在命令行里敲java -version,能打出版本号就说明通了。第二样是 Tomcat,下载后解压到一个纯英文、不带空格的路径下,别放在桌面或者「我的文档」这种带中文和空格的目录里——这是很多血泪经验的起点,路径带空格会让启动脚本出各种莫名其妙的问题。第三样是一个放代码的文件夹,同样遵守「纯英文、无空格」的原则。
目录可以先建好,名字用shoppingcart,暂时是个空文件夹,后面会往里放 JSP 文件。这三样东西装完,先别急着写代码,把 Tomcat 单独启动一次,浏览器访问http://localhost:8080,能看到 Tomcat 的欢迎页,说明环境这一关过了。这一步看着多余,但它是分界线:欢迎页出不来,后面写的所有代码都不用看了,先解决环境。
注意:Tomcat 默认端口是 8080。如果启动时黑窗口一闪就关了,或者提示端口被占用,先看看是不是已经有一个 Tomcat 在跑,或者别的软件占了这个端口。改端口在
conf/server.xml里,找到 8080 改成 8081 就行。
3. 第一个能跑的例子:把商品列表页跑起来
3.1 先看目录:文件是怎么摆的
简单版案例不搞复杂的分层,所有 JSP 文件平铺在一个文件夹里,只有商品数据抽出来做公共文件。这样做的目的是让你一眼看到全貌,缺点是不利于扩展,等跑通之后再改。
# Tomcat 解压目录下的 webapps 里新建项目文件夹 tomcat/webapps/shoppingcart/ ├── data.jsp # 公共商品数据,被其他页面引入 ├── index.jsp # 商品列表页(首页) ├── addToCart.jsp # 处理「加入购物车」,不显示页面,只做跳转 ├── cart.jsp # 购物车页面 ├── remove.jsp # 删除某一项 └── clear.jsp # 清空购物车webapps是 Tomcat 默认认的目录,放进这里的文件夹会自动变成一个「应用」,文件夹名就是访问路径里的一段。所以shoppingcart这个文件夹对应网址http://localhost:8080/shoppingcart/index.jsp。这一点必须记牢:你在浏览器里访问的路径,和你硬盘上的目录结构是一一对应的,404 大部分时候就是这两者对不上。
data.jsp是个特殊文件,它不单独访问,只被别的页面用<%@ include file="data.jsp" %>引进去。这样商品数据只写一份,改价格的时候不用满项目找。
3.2 第一个 JSP:把商品列表画出来
先写data.jsp,它只有一段 Java 代码,把商品塞进一个 List,每一项是一个三元素数组:编号、名称、单价。用数组是因为简单,不用额外建类。
<%@ page import="java.util.*" %> <% // 商品数据先写死在页面里,够用就行,后面再换成数据库 List<String[]> goodsList = new ArrayList<String[]>(); goodsList.add(new String[]{"1", "手冲咖啡豆 250g", "68"}); goodsList.add(new String[]{"2", "陶瓷马克杯", "39"}); goodsList.add(new String[]{"3", "滤纸 100 张", "15"}); goodsList.add(new String[]{"4", "手摇磨豆机", "199"}); %>这段代码没有输出任何 HTML,它只做一件事:定义一个叫goodsList的变量。被其他页面引入之后,那个页面就能直接用这个变量。单价这里故意存成字符串,是为了演示后面的类型转换,真实项目里建议直接用double或BigDecimal。
接着写index.jsp,把goodsList循环出来画成表格。
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.util.*" %> <%@ include file="data.jsp" %> <!DOCTYPE html> <html> <head><meta charset="UTF-8"><title>商品列表</title></head> <body> <h2>商品列表</h2> <table border="1" cellpadding="8"> <tr><th>编号</th><th>名称</th><th>单价</th><th>操作</th></tr> <% // 遍历商品,每一行输出一个 tr for (String[] g : goodsList) { %> <tr> <td><%= g[0] %></td> <td><%= g[1] %></td> <td>¥<%= g[2] %></td> <td><a href="addToCart.jsp?id=<%= g[0] %>">加入购物车</a></td> </tr> <% } %> </table> <a href="cart.jsp">查看购物车</a> </body> </html>第一行的contentType="text/html;charset=UTF-8"是中文不乱码的前提,它告诉浏览器「这个页面用 UTF-8 解码」。<%@ include file="data.jsp" %>是静态包含,相当于把data.jsp的代码原样抄进来,所以这里能直接用goodsList。<%= g[0] %>是输出表达式,等于把变量的值打印到 HTML 里。链接里用?id=1这种形式传参,这是 GET 请求最朴素的用法,点一下就等于告诉服务器「我要编号为 1 的那个」。
3.3 部署到 Tomcat:三步把页面送进浏览器
第一步,把写好的三个文件(data.jsp、index.jsp、addToCart.jsp先占个位,内容后面补)复制到webapps/shoppingcart/下面。别用拖拽的方式复制到 Tomcat 正在运行的目录里,有时候会复制不完整,稳妥的做法是先停掉 Tomcat 再复制。
第二步,启动 Tomcat。Windows 下双击bin/startup.bat,Mac 或 Linux 下执行bin/startup.sh。启动过程中会刷一堆日志,看到类似「Server startup in xxx ms」就说明起来了。这个黑窗口不要关,关了服务就停了,它是 Tomcat 的运行窗口,不是你程序出错才弹出来的。
第三步,浏览器访问:
http://localhost:8080/shoppingcart/index.jsp如果看到一张带四个商品的表格,恭喜,你已经完成了 Java Web 最关键的第一次。如果看到 404,先检查三件事:Tomcat 是不是真的在跑(访问http://localhost:8080看欢迎页)、文件夹名字是不是shoppingcart、文件名是不是index.jsp(大小写敏感,Index.jsp在有些系统上认不出来)。
3.4 跑通之后改三个地方,理解会更深
第一处,把data.jsp里再加一个商品,刷新页面看列表是不是多了一行。这一步验证的是「数据源改了,页面跟着变」,你要亲眼看到这个因果关系。
第二处,把index.jsp的charset=UTF-8临时改成GBK,刷新看看中文变成什么样,再改回来。乱码长什么样,你得见过一次,以后排查才有画面感。改完记得改回来,别把这个错误留到后面。
第三处,把cellpadding="8"改成"2",看表格是不是变挤了。这一步是在提醒你:HTML 部分和 Java 部分是分开的两层,改样式不影响逻辑。新手常见的问题是分不清「这行代码是在算数据还是在画界面」,多改几次就有边界感了。
提示:改了 JSP 之后一般刷新浏览器就能看到新效果,Tomcat 会自动重新编译。但如果改的是
web.xml这类配置文件,必须重启 Tomcat。分不清的时候,重启一次最省事。
4. 让购物车真正「记住」东西:Session 是怎么回事
4.1 Session 到底是什么:用超市寄存柜打个比方
HTTP 协议本身是不记事的:你请求一次,服务器回一次,然后就忘了你是谁。这在商品列表页没问题,但购物车不行——你加了东西去别的页面逛一圈回来,东西必须还在。Session 就是解决这个问题的机制。
把它想成超市的寄存柜:你第一次进店,服务台给你一个柜子,给你一张带柜号的小票(这个就是 Cookie 里的 JSESSIONID,浏览器自动帮你带着)。之后你每次来,凭小票开同一个柜子,里面的东西还在。柜子本身在服务台(服务器内存里),小票在你手上(浏览器里)。所以你在代码里做的事很简单:从柜子里取东西、往里放东西、把某个东西拿出来扔掉。
对应到 JSP 里就三个方法:session.getAttribute("cart")取、session.setAttribute("cart", 对象)存、session.removeAttribute("cart")清。就这三个,购物车的全部记忆功能都靠它们。
新手最容易犯的错是把购物车存在request里。request的生命周期只有一次请求,页面一跳转就没了,表现出来就是「加进去的商品刷新一下就消失」。这不叫玄学,这叫用错了容器。记住一句话:要跨页面活着的东西放 session,只在当前这次请求里用的东西放 request。
4.2 加入购物车:第一段真正「记住东西」的代码
购物车用什么数据结构存?用Map<String, Integer>,键是商品名,值是数量。选它的理由很实在:同一个商品加两次,不是存两条记录,而是数量加一;删除的时候按名字删一次就干净了。LinkedHashMap比HashMap多一个好处——它按插入顺序遍历,你加商品的顺序就是购物车里的显示顺序,看着舒服。
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.util.*" %> <%@ include file="data.jsp" %> <% // 1. 拿到链接里传来的商品编号 String id = request.getParameter("id"); // 2. 按编号回商品表里找到商品名 String name = null; if (id != null) { for (String[] g : goodsList) { if (g[0].equals(id)) { name = g[1]; break; } } } // 3. 从 session 取购物车,第一次没有就新建一个 Map<String, Integer> cart = (Map<String, Integer>) session.getAttribute("cart"); if (cart == null) { cart = new LinkedHashMap<String, Integer>(); session.setAttribute("cart", cart); } // 4. 数量加一:已经有就 +1,没有就从 1 开始 if (name != null) { Integer count = cart.get(name); cart.put(name, count == null ? 1 : count + 1); } // 5. 处理完跳回购物车页面 response.sendRedirect("cart.jsp"); %>这段代码一行 HTML 都没有,因为它不需要给用户看,它只做处理和跳转。request.getParameter("id")拿的是 URL 上?id=1里的那个值,拿不到就是null,所以第二步做了判空。第三步的强转(Map<String, Integer>)是必须的,因为getAttribute返回的是Object,你不告诉它是什么类型,它就没法当 Map 用。
第四步的count == null ? 1 : count + 1是一个三元表达式,等价于「如果没有这个键就放 1,有就放原来的值加 1」。第五步的sendRedirect是重定向,浏览器地址栏会跳成cart.jsp,用户按 F5 刷新也不会重复加购——如果用转发(forward),刷新就会重复提交,这是很经典的翻车点。
注意:
cart.get(name)返回Integer而不是int,是为了能判null。如果你写成int count = cart.get(name),商品第一次加的时候会直接抛空指针,因为 Map 里根本没有这个键。
4.3 购物车页面:把 Session 里的东西读出来算总价
cart.jsp要干两件事:把 Session 里的 Map 遍历出来,以及算出总件数和总价。总价需要单价,而 Map 里只存了名称和数量,所以还得回头去goodsList里按名字查一次价格。
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.util.*" %> <%@ include file="data.jsp" %> <% // 取出购物车,注意第一次访问时它是 null Map<String, Integer> cart = (Map<String, Integer>) session.getAttribute("cart"); if (cart == null) { cart = new LinkedHashMap<String, Integer>(); } int totalCount = 0; // 总件数 double totalPrice = 0.0; // 总价 %> <!DOCTYPE html> <html> <head><meta charset="UTF-8"><title>我的购物车</title></head> <body> <h2>我的购物车</h2> <% if (cart.isEmpty()) { %> <p>购物车还是空的,<a href="index.jsp">去逛逛</a></p> <% } else { %> <table border="1" cellpadding="8"> <tr><th>商品</th><th>单价</th><th>数量</th><th>小计</th><th>操作</th></tr> <% for (Map.Entry<String, Integer> e : cart.entrySet()) { String name = e.getKey(); int count = e.getValue(); double price = 0.0; for (String[] g : goodsList) { if (g[1].equals(name)) { price = Double.parseDouble(g[2]); // 字符串转数字 break; } } totalCount += count; totalPrice += price * count; %> <tr> <td><%= name %></td> <td>¥<%= price %></td> <td><%= count %></td> <td>¥<%= String.format("%.2f", price * count) %></td> <td><a href="remove.jsp?name=<%= java.net.URLEncoder.encode(name, "UTF-8") %>">删除</a></td> </tr> <% } %> </table> <p>共 <%= totalCount %> 件,合计 ¥<%= String.format("%.2f", totalPrice) %></p> <a href="index.jsp">继续购物</a> | <a href="clear.jsp">清空购物车</a> <% } %> </body> </html>cart.entrySet()是遍历 Map 的标准写法,每一项的getKey()是商品名,getValue()是数量。Double.parseDouble把data.jsp里存的字符串价格转成数字,不转的话乘出来是字符串拼接,价格会变成「688」这种诡异结果。String.format("%.2f", ...)是保留两位小数,不加的话总价可能出现117.30000000000001这类浮点数尾巴。
删除链接里的URLEncoder.encode是给中文商品名编码。商品名里有中文,直接拼到 URL 里,不同浏览器和服务器的处理方式不一致,容易出现「点了删除没反应」或者删错了的情况。编码一下,取的时候再解码,最稳。
4.4 删除、清空与参数说明
remove.jsp负责删一项,逻辑只有三步:拿参数、解码、从 Map 里移除。
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ page import="java.util.*" %> <% String name = request.getParameter("name"); if (name != null) { // 链接里的中文是编码过的,取出来要先解码 name = java.net.URLDecoder.decode(name, "UTF-8"); Map<String, Integer> cart = (Map<String, Integer>) session.getAttribute("cart"); if (cart != null) { cart.remove(name); } } response.sendRedirect("cart.jsp"); %>这里有个细节值得说:cart.remove(name)改的是 Session 里那个 Map 对象本身,Map 是引用类型,改了之后不需要再setAttribute一次,Session 里保存的就是同一个对象。如果你重新 new 一个 Map 再 set 回去,也可以,但没必要。
clear.jsp更简单,一句话把整个柜子清空:session.removeAttribute("cart"),然后跳回购物车页面显示「还是空的」。
三个处理页面的参数对比:
| 文件 | 输入参数 | 改的东西 | 跳转目标 |
|---|---|---|---|
| addToCart.jsp | id(商品编号) | 数量 +1 | cart.jsp |
| remove.jsp | name(商品名,需解码) | 删除一项 | cart.jsp |
| clear.jsp | 无 | 清空整个 shoppingcart | cart.jsp |
把这几个文件凑齐,一个能加、能看、能删、能清空的购物车就完整跑通了。只有跑完这一整套,你才算真正摸过 JSP、HTML、Java、Tomcat、Session 这五个词。
5. 新手最容易翻车的五个地方:避坑与排查
5.1 启动和访问阶段:页面打不开怎么查
现象一:双击 JSP 文件,浏览器里显示的是代码或者直接下载文件。原因:JSP 不是静态网页,它必须先被 Tomcat 编译成 Servlet 才能运行,直接用file://打开等于打开一个文本文件。 解决:所有访问都必须走http://localhost:8080/项目名/文件名.jsp这个路径,确认 Tomcat 的黑窗口是开着的。
现象二:访问地址报 404,但确定文件就在那儿。原因:九成是路径对不上。要么文件夹名写错了,要么漏了项目名这一层,要么文件名大小写不一致。还有一种情况是把文件放到了webapps/ROOT下面,那访问时就不该带项目名。 解决:先在浏览器访问http://localhost:8080看欢迎页通不通,再逐个字符比对 URL 和硬盘上的目录名,重点看shoppingcart有没有拼错、有没有多一层文件夹。实在不确定,就去 Tomcat 的webapps目录下看,那个文件夹名就是你 URL 里该写的名字。
现象三:页面能打开,但中文全变成问号或者方块。原因:编码没统一。JSP 页面的pageEncoding、contentType里的charset、以及响应给浏览器的编码,三者不一致就会乱码。 解决:在每一个 JSP 的第一行都写完整的<%@ page contentType="text/html;charset=UTF-8" language="java" %>,文件本身也另存为 UTF-8 编码。别只在首页写,其他页面漏一个就乱一个。
5.2 数据阶段:加进去的东西不对怎么查
现象四:点了「加入购物车」,跳到购物车页面还是空的。原因:最常见的是把购物车存到了request里而不是session里,请求一结束数据就没了。其次是把cart存在了局部变量里,方法执行完就销毁。还有一种隐蔽的:链接里?id=后面的参数名和getParameter里的名字不一致,拿到的永远是null。 解决:先在所有用到的地方统一写成session.setAttribute("cart", cart)和session.getAttribute("cart"),检查request和session有没有混用。再在addToCart.jsp里临时加一句<%= id %>输出参数值,看看参数到底有没有传进来。这一步排查很土,但非常有效。
现象五:刷新页面之后,数量突然暴涨或者报空指针。原因:数量暴涨通常是处理页面用了转发而不是重定向,刷新等于重新提交一次。空指针多半是cart.get(name)拿到的Integer被直接当成int用,第一次加购时 Map 里没有这个键,返回null,拆箱就炸。 解决:处理类页面统一用response.sendRedirect跳转,不用forward。取值的时候一律用Integer接收并判空,写count == null ? 1 : count + 1,不要图省事直接加。
现象六:删除了某一项,购物车里其他项也跟着消失或者删错了。原因:传给后台的商品名被编码过,后台没解码就直接拿去当键用了,cart.remove找不到对应的键,或者因为中文乱码删到了别的项。 解决:前端用URLEncoder.encode(name, "UTF-8")编码,后端用URLDecoder.decode(name, "UTF-8")解码,两边配对。排查时可以在remove.jsp里先输出解码前后的名字对比一下,问题一眼就能看出来。
提示:遇到问题先看 Tomcat 黑窗口里的日志,它会把异常堆栈打出来,包括出错的文件名和行号。新手常犯的错是只盯着浏览器页面猜,其实答案一直在那个黑窗口里。
6. 跑通之后下一步学什么:从简单购物车往前走的路线
这一份案例的价值在于「通」,不在于「全」。你已经能让浏览器、服务器、会话三者配合起来干一件事了,接下来往哪走,我给一个顺序建议,按这个顺序走不容易迷路。
第一步,把 JSP 里的 Java 代码搬出去。现在index.jsp里混着<% for %>这种东西,页面又长又难改。试着把「查商品」和「算总价」写成独立的 Java 类,JSP 只负责输出。这一步会逼你理解什么叫「分层」,也是从 JSP 走向 Servlet 的起点。搬完之后再看 Servlet,你会发现它就是「只写 Java、不写 HTML」的版本,一点都不神秘。
第二步,把<%= %>换成 EL 表达式和 JSTL 标签。比如<%= name %>换成${name},循环换成<c:forEach>。好处是页面里不再有 Java 代码,改版式的人不用懂 Java。这一步的收益在后期特别明显,我第一次把项目里的 scriptlet 全清掉之后,页面文件的行数少了一半。
第三步,把商品数据从页面里挪到数据库。用 JDBC 建一张商品表,一张购物车表,把goodsList换成一次查询。这时候你会遇到连接池、事务、SQL 注入这些问题,但因为你已经有完整的流程认知,学起来是「往熟悉的位置填东西」,而不是从零开始。
第四步,把 Session 的边界搞清楚。试试设置会话超时时间,试试关掉浏览器再打开看购物车还在不在,试试用两个不同的浏览器同时访问看会不会串号。这些实验能让你真正理解「Session 是服务器端的,Cookie 是浏览器端的」这句话,而不是背下来。
| 阶段 | 现在做的 | 该学的东西 |
|---|---|---|
| 已跑通 | JSP 里嵌 Java、Session 存 Map | 保持,别急着换框架 |
| 下一步 | 逻辑抽成类 | Servlet、分层思想 |
| 再下一步 | 页面去掉 Java 代码 | EL、JSTL |
| 往后 | 数据落地 | JDBC、连接池 |
最后说一个我自己踩过的坑。当年我第一次跑通这个案例,觉得「能跑就行了」,于是后面的项目里继续把 SQL、业务判断、页面输出全塞在一个 JSP 里,写到第三个页面就彻底改不动了,一个字段名改了要翻五个文件。从那以后我每次写完一个能跑的 Demo,都强制自己先做一件事:把里面所有跟「算数据」有关的代码挑出来,单独放一个地方。这个习惯比学会任何一个框架都值钱。希望帮到你。
本文还有配套的精品资源,点击获取