简介:这套基于Spring Boot和MySQL的家具销售电商平台项目,配套完整源码与设计文档,面向毕业设计、课程设计以及Java Web学习者,可快速掌握电商系统的需求分析、数据库设计与前后端交互方式。压缩包大小约24.89MB,内含Spring Boot典型分层源码、设计说明文档与演示文稿、readme说明、开发环境配置清单,目录划分清楚,适合按模块阅读和二次开发。目前已有537人学习浏览,是同类课程设计中较常被参考的项目之一。项目覆盖用户注册登录、家具商品浏览检索、购物车管理、订单提交处理以及后台商品与订单维护等核心功能。文档中提供系统需求分析、数据库表结构、接口设计等内容,可深入了解Spring Boot的自动配置与业务逻辑封装,以及MySQL对用户、商品、订单等数据的持久化方案。跟着项目完整走一遍,既能锻炼电商平台的整体架构能力,也为课程答辩和后续扩展提供了实用范本。
1. 基于Spring Boot+MySQL的家具销售电商平台:一张订单表背后藏着完整Web开发链路
Spring Boot+MySQL的家具销售电商平台,在毕业设计里属于“铁打的选题”那一类:需求好解释,模块数量适中,做完能覆盖Web开发常用的建表、查询、事务、文件上传、权限控制全链路。它解决的问题也很直接——用户进前台浏览家具、搜索型号、加购物车、下订单,管理员进后台维护商品、处理订单状态,数据库用MySQL存商品和订单,后端用Spring Boot把这一切串成接口。难点不在单个功能,而在于模块之间的数据一致性,比如并发下库存会不会超卖、订单生成一半失败了怎么办。下面按我实际做过的顺序来讲:先建表、再搭工程、写核心交易、列常见的翻车点,最后给一份可以直接照做的验证清单。想拿这套源码和文档练手或者准备答辩的,照着推就能跑通。
2. 系统设计先行:五张核心表怎么建,Spring Boot工程怎么搭
2.1 为什么这个选题偏好Spring Boot + MySQL:选型逻辑与版本坑
Spring Boot在这个选题里的角色很明确:它把“写接口”这件事的成本降到最低。内嵌Tomcat意味着不用单独装容器,starter依赖帮你把数据源、JSON处理、参数校验这些常用组件自动装配好,一个main方法启动就能跑。对以“做一个能稳定演示的系统”为目标的场景来说,这个特性比性能更重要,毕竟课堂上没人关心并发上限,大家关心的是“别在我演示的时候崩”。
MySQL负责把数据落下来。家具、用户、购物车、订单这几类数据结构稳定、关系清晰,用关系型数据库存天然合适。更重要的是社区资料极其丰富,小到mysql安装教程,大到MySQL 5.7.44安装过程的详细步骤都能搜到,出问题不愁没人踩过。相比NoSQL,MySQL的SQL语法是通用技能,文档里写“系统采用MySQL存储业务数据”也更好答辩。
组合本身不难,版本配对才是第一个坑。如果你用IntelliJ IDEA社区版做Spring Boot开发,它没有Spring Initializr入口,需要去start.spring.io生成工程再导入,这不影响后续开发。真正影响的是JDK和Boot版本:JDK8配Spring Boot 2.7.x,JDK17以上配Spring Boot 3.x;MySQL连接驱动也要和服务端版本对应。很多旧教程里的驱动类名是com.mysql.jdbc.Driver,那是5.x的老写法,连MySQL 8.0会直接报认证相关错误。这条展开说就是黑匣子,所以我一般直接用2.7.18 + mysql-connector-j 8.x这套组合,报错都能搜到答案。
2.2 五张核心表:家具、用户、购物车、订单、订单明细这样建
先建库。字符集一定用utf8mb4,别用utf8,否则商品名称里带个特殊符号就会在插入时报错。引擎用InnoDB,只有它能撑住后面的事务回滚。建表SQL是这套系统的地基,一次建对能省后面大量改表时间。
CREATE DATABASE IF NOT EXISTS furniture_mall DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE furniture_mall; CREATE TABLE user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL COMMENT '建议存BCrypt密文,别存明文', phone VARCHAR(20), address VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE furniture ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, category VARCHAR(50), price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, image VARCHAR(255) COMMENT '图片URL或磁盘上传路径', description TEXT, sales INT DEFAULT 0, status TINYINT DEFAULT 1 COMMENT '1上架 0下架', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; CREATE TABLE cart_item ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, furniture_id BIGINT NOT NULL, quantity INT NOT NULL DEFAULT 1, checked TINYINT DEFAULT 1 COMMENT '是否勾选下单', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_furniture (user_id, furniture_id) ) ENGINE=InnoDB; CREATE TABLE orders ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE, user_id BIGINT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0 COMMENT '0待支付 1已支付 2已发货 3已完成 4已取消', receiver_name VARCHAR(50), receiver_phone VARCHAR(20), receiver_address VARCHAR(200), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, pay_time DATETIME ) ENGINE=InnoDB; CREATE TABLE order_item ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_id BIGINT NOT NULL, furniture_id BIGINT NOT NULL, furniture_name VARCHAR(100), price DECIMAL(10,2) COMMENT '下单时的成交价快照', quantity INT NOT NULL, subtotal DECIMAL(10,2) ) ENGINE=InnoDB;逻辑说明:这五张表是电商系统最常见的划分方式。user存账号和收货信息,furniture存商品,cart_item承载“用户和家具”的多对多关联,orders是订单主表记录整单金额、状态、收货地址,order_item是订单明细表记录每个家具的下单信息。两个细节要单独讲。
第一,order_item里为什么冗余furniture_name和price:家具名称和价格以后都可能改,但历史订单必须保留成交的那一刻信息,这叫订单快照。如果只存furniture_id,商品改名改价后,你的订单数据也跟着变,这在数据一致性上是硬伤。第二,orders为什么用status整数字段而不是一堆布尔值:订单至少有“待支付、已支付、已发货、已完成、已取消”五个状态,一个int字段既能按状态筛选,也方便以后扩展成状态机。这种字段设计写在文档里,比单纯贴需求有说服力得多。
参数说明:price和total_amount这类型必须用DECIMAL(10,2),意思是最大10位数字、小数占2位,刚好覆盖万元级家具价格。stock用INT并给默认值0,避免空指针。create_time用DATETIME DEFAULT CURRENT_TIMESTAMP,这样插入数据时不用手动塞时间。另外外键我建议不要建物理外键,用逻辑外键就够了——毕设阶段物理外键会让删除操作变麻烦,初始化导入数据时还容易因外键约束失败。表之间靠user_id、furniture_id、order_id这些字段关联就行。
2.3 工程骨架:pom.xml与application.yml一次性配好
建表后搭工程。常见做法是去start.spring.io生成Maven工程,依赖选Spring Web、MyBatis Framework、MySQL Driver三个,生成后直接导入IDEA。dependency部分建议写成这样:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> </dependencies>逻辑说明:Spring Boot 2.7.x的父依赖已经管理了mysql-connector-j的版本,所以不用写version。注意artifactId是mysql-connector-j而不是mysql-connector-java,后者是8.0.31之前的老坐标,新项目直接用前者。mybatis-spring-boot-starter不归Boot管理,必须自己写版本号,2.3.2配Boot 2.7.x是经过验证的组合,换3.x版本时这个starter也要跟着升到3.x。
接下来是application.yml,这是整个项目的“总闸门”,配置错了应用直接起不来。
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/furniture_mall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: 123456 servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.furniture.entity参数说明:serverTimezone=Asia/Shanghai解决MySQL 8.0时区问题,不加的话数据库时间会比北京时间差8小时;useSSL=false避免本机连接时SSL握手警告;allowPublicKeyRetrieval=true是MySQL 8.0的caching_sha2_password认证必须项,不加会直接报Public Key Retrieval is not allowed。机器上MySQL端口不是3306时,url里顺手改掉。multipart配置是给家具图片上传预留的,限制单文件10MB。mybatis.mapper-locations这行提前配好,否则后面写XML时MyBatis默认扫描不到,接口全报Invalid bound statement。
验证骨架:直接运行主类,看到Tomcat started on port 8080就是成功;如果报Access denied,优先检查用户名密码;如果报Public Key,检查url那三个参数是否齐全。到这里,数据层和运行环境就绪,可以开始写业务链路了。
3. 核心交易链路:商品浏览、购物车、下单扣库存的代码实现
3.1 商品列表:从Mapper到Controller的最小查询链路
商品模块是前台的门面。典型接口是分页查询:传pageNum、pageSize和可选关键词,返回家具列表。常见做法是Controller→Service→Mapper三层,Mapper用XML写SQL,比起注解方式更便于后期加复杂查询。
先建实体类Furniture,字段与表对齐,价格用BigDecimal。Mapper接口这样写:
@Mapper public interface FurnitureMapper { List<Furniture> selectPage(@Param("keyword") String keyword, @Param("offset") int offset, @Param("pageSize") int pageSize); Furniture selectById(@Param("id") Long id); }XML里对应SQL:
<select id="selectPage" resultType="com.example.furniture.entity.Furniture"> SELECT id, name, category, price, stock, image, description, sales, status FROM furniture <where> <if test="keyword != null and keyword != ''"> AND name LIKE CONCAT('%', #{keyword}, '%') </if> AND status = 1 </where> ORDER BY sales DESC LIMIT #{offset}, #{pageSize} </select>逻辑说明:<where>标签会自动处理条件拼接,keyword为空时把AND去掉;ORDER BY sales DESC是电商列表的默认排序,销量高排前面,这也是MySQL排序在业务里的常见用法;LIMIT做物理分页,offset计算是(pageNum-1)*pageSize。条件值全部用#{}预编译占位符,避免SQL注入;如果写成${}直接拼接,用户传keyword时就能注入恶意SQL,这个点经常在答辩时被追问。
Controller接口:
@RestController @RequestMapping("/api/furniture") public class FurnitureController { @Resource private FurnitureService furnitureService; @GetMapping("/list") public Result list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String keyword) { return Result.success(furnitureService.queryPage(pageNum, pageSize, keyword)); } }参数说明:defaultValue让前端不传页码也能跑;keyword用required=false表示搜索词可选;Result是统一返回包装类,里面固定code、msg、data三个字段,接口结构稳定后前端对接不用猜。到这里,浏览器里直接访问/api/furniture/list?keyword=沙发就能验证列表。前台页面如果不想单独写前端,用Thymeleaf模板渲染也行;但现代毕设更常见的是前端用Vue或原生HTML调JSON接口,后端只管出数据。我习惯保留Thymeleaf只做后台管理页,前台全部走JSON,这样文档里能多写一节“前后端交互设计”。
3.2 购物车操作:先查再插不如一条SQL解决
购物车存储有两种常见做法。一是Redis,以userId为key存家具id和数量,读写快、过期自动清理;二是直接落MySQL,用cart_item表。对这套系统,我的建议是落库。理由很现实:源码里多一张表的设计能写进系统设计文档,还能演示关联查询;最关键的是不用额外部署Redis,降低运行环境复杂度。如果导师要求体现“新技术”,再单独把Redis作为升级点写一章,那是加分项而不是必选项。
加购接口最常见的问题是重复加购同款家具。教科书写法是先SELECT判断,再决定UPDATE还是INSERT,但这条路径在并发下可能插出重复记录。更稳的写法是靠建表时的唯一索引配合一条MySQL语法:
INSERT INTO cart_item(user_id, furniture_id, quantity) VALUES(#{userId}, #{furnitureId}, #{quantity}) ON DUPLICATE KEY UPDATE quantity = quantity + VALUES(quantity)逻辑说明:user_id和furniture_id有联合唯一索引,第一次插入正常执行;再买同款时触发唯一键冲突,MySQL自动执行UPDATE把数量累加。从两条SQL变成一条,同时消除了“查询到插入”之间的时间差,这是并发场景下非常典型的优化思路。
查购物车时不能只返回cart_item裸数据,要连表把家具名称、图片、单价一起取出来,前端才能直接渲染:
SELECT c.id, c.furniture_id, c.quantity, f.name, f.price, f.image FROM cart_item c JOIN furniture f ON c.furniture_id = f.id WHERE c.user_id = #{userId}参数说明:JOIN查询避免逐条查商品的N+1问题,一次拿全;WHERE用user_id筛当前用户。删除购物车项时,接口要带上user_id条件,防止用户A通过猜测id删掉用户B的数据,这是接口越权的基础防范。前端每次勾选商品时同步更新checked字段,下单逻辑只处理勾选中的条目。这个字段看着简单,实际是把“交互状态”落进数据模型,系统设计的文档里写一笔会加分。
3.3 下单与扣库存:@Transactional事务这样写才不回滚
下单是整套系统里最需要谨慎的接口,它牵扯四件事:校验购物车、扣库存、生成订单、清空购物车。任何一步失败,前面步骤都必须撤销,否则会出现“订单没生成但库存被扣”或者“订单生成了但库存没扣”的数据不一致。这就要靠数据库事务,Spring里用@Transactional声明。
@Transactional(rollbackFor = Exception.class) public OrderVO createOrder(Long userId) { List<CartItemVO> items = cartMapper.selectCheckedItems(userId); if (items.isEmpty()) { throw new BusinessException("购物车中没有选中商品"); } String orderNo = "F" + System.currentTimeMillis() + userId; BigDecimal total = BigDecimal.ZERO; for (CartItemVO item : items) { int rows = furnitureMapper.deductStock(item.getFurnitureId(), item.getQuantity()); if (rows == 0) { throw new BusinessException("商品[" + item.getFurnitureName() + "]库存不足"); } total = total.add(item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } orders order = new Orders(); order.setOrderNo(orderNo); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(0); orderMapper.insert(order); orderItemMapper.batchInsert(order.getId(), items); cartMapper.clearCheckedItems(userId); return new OrderVO(orderNo, total); }库存扣减SQL是重点:
<update id="deductStock"> UPDATE furniture SET stock = stock - #{quantity} WHERE id = #{id} AND stock >= #{quantity} </update>逻辑说明:deductStock里AND stock >= #{quantity}把“检查库存”和“扣库存”合并成一个原子操作。MySQL执行UPDATE时会对命中的行加行锁,其他线程的扣减必须等锁释放,这就从机制上保证了并发下不会超卖。如果库存不够,UPDATE影响行数为0,代码里rows==0就抛异常触发回滚。关于MySQL锁的分类,行锁、间隙锁在面试里会被问,但实现层面InnoDB在UPDATE时已经自动加了行锁,课设阶段不需要手动SELECT FOR UPDATE,写多了反而影响并发性能。
事务要生效,有三件事必须检查。第一,createOrder方法不能被同类内部调用,比如Controller调OrderService.createOrder没问题,但OrderService另一个方法里调this.createOrder(),@Transactional就失效了,因为Spring代理对象没介入;第二,异常不能被try/catch吞掉,有人习惯在方法里catch Exception打日志,事务感知不到失败就直接提交了,数据对不上查半天才发现;第三,rollbackFor = Exception.class必须写明,因为Spring默认只回滚RuntimeException,如果你自定义的BusinessException是受检异常,默认情况下抛出去也不会回滚。这三点是血泪经验,尤其第二条,踩过一次就长记性。
订单号用时间戳加userId拼接够课设级别,但同一毫秒并发下单可能重复,稳妥做法是加随机数或走Redis自增。这里不展开,知道有这个隐患即可。
4. 避坑排查:Spring Boot+MySQL项目最容易翻车的5个问题
这套系统里报错最多的不是业务逻辑,而是环境、字段、SQL关键字这一类“看起来很小但能卡半天”的问题。下面五条按出现频率排,每一条都给出现象、原因和解决步骤。
4.1 启动就连不上数据库:Access denied还是Public Key Retrieval
现象:应用启动时控制台报Access denied for user 'root'@'localhost',或者报Public Key Retrieval is not allowed。
原因:Access denied说明用户名密码不对,或MySQL 8.0默认使用caching_sha2_password认证而驱动版本与账号认证方式不匹配。Public Key Retrieval is not allowed则是url里没加allowPublicKeyRetrieval=true。很多人用Docker起MySQL时还会遇到端口冲突,宿主机3306被本地服务占着,容器端口映射失败也会造成连不上。
解决:先确认密码本身是否正确,注意按mysql安装教程配置root时可能设过特殊字符密码;再确认连接的端口是不是本地实例的端口。认证方式问题可以在MySQL命令行执行:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;同时把url里的allowPublicKeyRetrieval=true和serverTimezone=Asia/Shanghai一次配好,两招一起用基本能根治。
4.2 时间差8小时、金额多出一串小数:时区、精度与字段类型
现象:数据库里create_time比实际时间晚8小时;计算总价后出现0.30000000000000004这类浮点数尾巴。
原因:数据源url没指定serverTimezone,驱动取了系统默认时区;金额字段用了float或double,浮点数二进制存储天然存在精度损失。
解决:url加serverTimezone=Asia/Shanghai即可修正时区。金额一律用BigDecimal接收,数据库字段用DECIMAL(10,2),实体类属性也是BigDecimal,从数据库到Java类型全程不用浮点类型。时间字段建议用LocalDateTime,不要用java.util.Date,MyBatis对LocalDateTime的映射更干净。MySQL 5.7和8.0在DATETIME默认值行为上也有差异,8.0支持DEFAULT CURRENT_TIMESTAMP更友好,这块在文档里可以提一句体现对比。
4.3 表名和关键字撞车:order、desc这种“合法”的表名
现象:建表语句不报错,执行SELECT时却在order附近报语法错误,提示You have an error in your SQL syntax。
原因:order是SQL的ORDER BY关键字,直接当表名或字段名用,MySQL解析时就会出错。desc、group也是高频撞车的名字。
解决:最省事的方案是建表时统一加前缀或直接避开关键字,我在这套系统里把订单表命名为orders,完美避开。如果已经在代码里大量使用order,SQL里可以用反引号包裹:
SELECT * FROM `order`;但反引号要贯穿所有SQL,稍微漏一个就报错,所以我建议直接改名,代价最小。审查建表脚本时顺便把keyword这类字段名也扫一遍,避免后天返工。
4.4 加购同款家具出现重复行:唯一索引与ON DUPLICATE KEY UPDATE
现象:同一用户把同一件家具加购两次,购物车列表出现两条记录,数量分别是1和1,而不是一条数量为2的记录。
原因:代码实现是“先SELECT再INSERT”,并发请求时两个请求都查到“不存在”,然后各自插入成功,就产生了重复数据。这是典型的竞态条件。
解决:建表时给(user_id, furniture_id)加唯一索引,插入改用INSERT ... ON DUPLICATE KEY UPDATE,从数据库层面兜底。如果线上已经产生脏数据,先查出来再清理:
SELECT user_id, furniture_id, COUNT(*) FROM cart_item GROUP BY user_id, furniture_id HAVING COUNT(*) > 1;把重复行合并成一条并更新数量,再补唯一索引,防止再次发生。这套“先清后堵”的思路在工作中同样适用。
4.5 上传图片本地正常、打包后404:静态资源映射的玄学
现象:IDEA里运行,上传商品图片后能正常显示;用mvn package打成jar再运行,上传的图片刷新页面就404。
原因:Spring Boot默认静态资源目录是classpath:/static/,本地运行时上传到项目target目录还能访问;但jar包运行时classpath是只读的,上传目录写不进jar,默认映射也不覆盖外部磁盘路径。
解决:把图片上传到绝对路径,比如/Users/xxx/furniture-mall/upload,再注册一个资源映射配置:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + uploadPath + "/"); } }uploadPath从application.yml里读取,换机器只改配置。注意addResourceHandler和addResourceLocations末尾的斜杠不能丢,丢了就是这个配置静默失效,属于典型的“看着没错但就是404”。这类问题排查时先看控制台有没有No mapping for GET日志,有就优先怀疑映射配置。
5. 从能跑到能演示:验证清单、打包部署与三个升级方向
5.1 一份可以直接照着点的验证清单
| 模块 | 操作步骤 | 预期结果 |
|---|---|---|
| 注册登录 | 注册新账户并登录 | 密码以密文入库,登录成功进入前台 |
| 商品浏览 | 首页列表按销量排序,搜索“沙发” | 能搜到结果且分页正常 |
| 购物车 | 同一件家具连续加购两次 | 数量累加为2,列表无重复行 |
| 下单支付 | 勾选商品去结算,模拟支付 | 库存扣减,订单状态变为已支付 |
| 后台管理 | 下架一件商品 | 前台列表立即看不到该商品 |
| 订单流转 | 后台发货,前台查看订单 | 订单状态从已支付变为已发货 |
这套清单适合演示前5分钟快速过完,任何一步不通,说明链路里还有没合上的地方。
5.2 打包部署:从IDEA到java -jar
mvn clean package -DskipTests java -jar target/furniture-mall-0.0.1-SNAPSHOT.jar-DskipTests跳过测试类,避免有些环境test编译失败;jar包名按pom里artifactId和version拼接。如果生产机用的是MySQL 5.7.44安装的实例,驱动用mysql-connector-j 8.x一样兼容,但url里驱动类名必须保持com.mysql.cj.jdbc.Driver,不要改回老的com.mysql.jdbc.Driver。部署后验证一个接口:curl http://localhost:8080/api/furniture/list?pageNum=1&pageSize=10,返回JSON就说明工程正常。
5.3 三个值得做的升级方向
第一个升级是登录认证。很多模板用session存登录态,前后端分离时跨域处理很别扭,可以换Spring Security或拦截器加JWT,接口权限立刻清晰。第二个升级是性能。商品详情和首页列表加Redis缓存,热点商品库存用Redis预热,再用spring boot admin做监控,看一眼内存和接口耗时就能定位瓶颈。第三个升级是前端重构,把Thymeleaf模板换成Vue3+Element Plus,走真正的前后端分离,接口文档和代码结构会更接近企业项目。
最后说个教训:做这类系统,第一次跑通核心链路后立刻做完整备份,源码和数据库都导出。我当年改购物车逻辑加字段时,一个DROP语句把表删了,幸好有备份,否则一整天的调优全白费。用Git做本地提交也行,这是成本最低的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取