news 2026/10/9 22:37:23

Spring Boot + Vue 全栈电商项目实战:从环境搭建到业务改造踩坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot + Vue 全栈电商项目实战:从环境搭建到业务改造踩坑指南

简介:一套基于Springboot+Vue的在线购物平台完整项目,适用于计算机专业毕业设计、课程设计与期末大作业等场景,也适合正在准备毕设的学生和需要项目实战的Java学习者。项目以高分通过并获得导师指导,运行环境为JDK1.8、MySQL5.7与Maven3.3,可在Eclipse或IDEA中直接部署。压缩包共607个文件,主要包含124个Java后端源码、94个Vue前端页面、63个JavaScript脚本,另附数据库脚本、开发说明文档、部署视频、代码讲解视频及图标图片素材等,整体约20.79MB,目录划分清晰,便于按需查阅。目前已有58人学习浏览。除可直接运行的完整代码外,还提供了多段操作录屏和代码讲解,可帮助理解前后端数据交互、数据库设计思路与项目部署细节,适合直接作为毕设交付或在此基础上继续扩展。

1. 在线购物平台项目:Spring Boot + Vue 全栈样板,跑到能改才是你的

解压 .rar 的人通常分两种:一种想快速拿到一个能演示的在线购物平台,另一种想让这套 Spring Boot + Vue 的代码变成自己能改、能讲、能部署的东西。这个标题里藏的是一个典型的前后端分离电商项目:后端用 Spring Boot 提供商品、购物车、订单、登录等接口,前端用 Vue 渲染页面并调用接口,MySQL 负责业务数据持久化。它不算生产级系统,但覆盖了电商业务最核心的完整链路。先说结论:把它跑起来只需要半小时,真正拉开差距的是你能不能从“能跑”走到“能改”。接下来按拆结构、启动、读业务、避坑、进阶的顺序展开,每一步都能落到实处。

2. 技术选型拆解:Spring Boot 后端与 Vue 前端各自的职责边界

2.1 后端分层:Controller / Service / Mapper 为什么这样拆

几乎每个基于 Spring Boot 的在线购物平台,后端都会采用 Controller、Service、Mapper 三层结构。这个习惯不是为了代码好看,而是让职责可控:Controller 只负责参数接收和结果返回,Service 负责业务规则与事务管理,Mapper 只操作数据库。就拿“下单”这个高频动作来说,校验库存、扣减库存、生成订单、清空购物车四个操作必须在一个事务里完成,这个事务边界只能放在 Service 层,Controller 放不下也不该放。

@RestController @RequestMapping("/api/order") public class OrderController { @Autowired private OrderService orderService; @PostMapping("/create") public Result<OrderVO> create(@RequestBody OrderDTO dto) { return Result.success(orderService.createOrder(dto)); } }

这段代码里没有任何业务判断,它只负责把 JSON 转成 OrderDTO,然后调用 Service。Controller 越薄,代码越容易测试;业务规则集中在 Service 层,出问题时也只需要在一个地方排查。Mapper 层通常配合 MyBatis-Plus 使用,因为它内置了 selectById、selectPage 这些基础操作,购物平台八成以上的查询都是单表或简单联表,完全不需要手写 XML 映射。

还要注意 Spring Boot 版本带来的差异:2.x 用的是 javax.* 包,3.x 全部换成 jakarta.* 包。很多课程项目基于 2.x,如果你本地装的是 3.x 脚手架,直接把代码拷贝过来会出现大量 import 报错。判断项目版本最快的方法是看 pom.xml 里 spring-boot-starter-parent 的版本号,而不是看代码。

2.2 前端工程:Vue Router 管理页面,Axios 封装请求

前端部分,老项目用 Vue 2 + Element UI,新项目用 Vue 3 + Element Plus,但工程骨架高度一致:src/router 放页面路由,src/api 放接口请求封装,src/views 放页面组件。购物平台的页面集比较固定:商品列表、商品详情、购物车、订单确认、个人中心,正好由一套路由撑起来。前端最值得先读的文件不是某个页面,而是封装请求的公共模块,因为所有接口的鉴权头都靠它统一添加。

// src/utils/request.js —— 前端统一请求封装 import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = token } return config }) request.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(error) } ) export default request

这个文件里有两个容易被忽略的细节。baseURL 写成/api而不是后端完整地址,是为了配合开发服务器的代理;token 从 localStorage 里取,是为了让刷新页面后登录状态还在。这两个点前后端必须对齐,否则就会出现后面要专门讲的“登录成功但其他接口全部 401”现象。

前端组件组织上,商品列表页通常由搜索框、商品卡片列表、分页器三部分组成;购物车页只需要一张表格加一个结算按钮。用 Vue Router 时,要注意路由的懒加载写法:component 里用 () => import('@/views/GoodsList.vue'),否则打包后首屏加载会明显变慢。这一点在课程项目里经常被忽略,但并不难改。

2.3 接口约定:统一返回结构、状态码与鉴权方式

前后端能不能顺畅协作,取决于接口约定是否稳定。购物平台项目里一般会有一个统一的 Result 包装类,把状态码、提示信息和业务数据包在一起返回。常见约定:

code含义前端处理
200请求成功直接读取 data
400参数不合法弹出 msg 提示
401未登录或 token 失效清除本地 token 并跳登录页
500服务端异常弹出 msg 并保留当前页

前端拿到任何非 200 的 code,都应该统一交给拦截器处理,而不是在每个页面里各自判断。鉴权部分,登录成功后后端返回一个 token,前端存进 localStorage,后续每个请求都从本地取出来放到请求头里。后端用拦截器统一校验,白名单放行登录、注册和商品浏览等公开接口,其余路径全部要求有效 token:

// 后端拦截器放行规则:公开接口不进鉴权 String[] allowUrls = { "/api/user/login", "/api/user/register", "/api/goods/**", "/api/category/**" };

白名单配置是经验活:少放一个接口,前端就会莫名报 401;多放一个,安全边界就出现缺口。拿到项目后先读这一处,基本就能判断作者对鉴权流程理解到哪一层。还有一个常见细节是登录接口本身如果也被拦截器拦了,就会出现“登录接口返回 401”的自锁现象,这种问题在项目里屡见不鲜。

3. 把项目拉起来:从 .rar 解压到本地全栈启动的最小步骤

3.1 解压后的目录结构:先分清后端、前端和数据库脚本

拿到 .rar 之后,第一件事不是双击打开 IDE,而是先把压缩包里的东西认清楚。典型的 Spring Boot + Vue 项目包含三块:一个 Maven 后端工程、一个 Vue 前端工程、一份 SQL 初始化脚本,有时还带个说明文档。很多新手一上来就只把后端目录拖进 IDE,数据库没建就点运行,然后在报错里绕圈子。先用目录树确认结构:

# 解压后常见的工程布局 online-shopping/ ├── springboot-server/ # 后端:pom.xml 所在目录 ├── vue-web/ # 前端:package.json 所在目录 └── db/ └── shop.sql # 数据库初始化脚本

如果解压出来只有一个后端工程而没找到前端目录,检查是不是嵌套了一层文件夹;如果连 SQL 脚本都没有,说明原项目依赖 ORM 自动建表,这时要核对实体字段与数据库类型,避免字段对不上。把三块内容定好位后,再按数据库、后端、前端的顺序启动。顺序即成功率:数据库没就绪就启动后端,日志会一直报连接错误,容易让人误判成代码问题。

3.2 数据库初始化:SQL 脚本导入与连接配置

数据库是第一个最容易翻车的环节。SQL 脚本可能是从 MySQL 8.0 导出的,也可能是用 5.7 写的,直接导入轻则报排序规则错误,重则建表一半中断。建议先看脚本头部有没有 CREATE DATABASE,没有就先手动建库再导入,避免库名与连接配置不一致。我用命令行演示一遍最稳妥的导入流程:

mysql -uroot -p --default-character-set=utf8mb4

进入客户端后执行:

CREATE DATABASE IF NOT EXISTS shop DEFAULT CHARACTER SET utf8mb4; USE shop; SOURCE /path/to/shop.sql;

导入完成后,后端连接配置要和库名、账号、密码完全对齐。打开 application.yml,确认这一处和本地环境匹配:

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/shop?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码

如果启动时提示时区错误,多半是 url 里少了 serverTimezone;连接失败则优先检查端口是 3306 还是被改成了别的。数据库没就绪之前,不要动任何后端代码,这是启动阶段最重要的顺序约定。

3.3 启动后端:Maven 构建与启动日志定位

数据库就绪后,再用 IDE 打开后端工程,等 Maven 把依赖拉完。也可以完全用命令行跑,两条路线效果一样,区别只是 IDE 把日志做了可视化,命令行更贴近线上操作方式:

cd springboot-server mvn clean package -DskipTests java -jar target/online-shopping-0.0.1-SNAPSHOT.jar

看到 Tomcat started on port(s): 8080 这行输出,后端才算真正起来。失败的话,先看日志里的第一段异常,大多数情况下那是根因;后面跟着的十几行堆栈经常是同一个问题的连锁反应。常见的启动失败场景就集中在三处:端口被占用、数据库连接参数不对、pom.xml 依赖版本冲突。拿到日志先搜 Caused by 能省很多时间。

补充一个判断:如果 pom.xml 里的 packaging 是 war,启动命令要换成 mvn spring-boot:run 或在 IDE 里直接运行主类;默认 jar 就用 java -jar 方式。还有就是第一次用 Maven 构建时,依赖下载可能花很长时间,这不是卡死,是中央仓库连接慢,耐心等或换镜像源都可以。

3.4 启动前端:npm install 与开发代理配置

后端起来后,前端启动也有固定工序:先装依赖,再起开发服务器。有些压缩包里故意不携带 node_modules,就是为了跨机器重装;但重装有重装的坑,Node 版本对不上就会卡在依赖编译上。标准操作是:

cd vue-web npm install npm run serve

npm install 后看到类似 vue-cli-service serve 的启动日志,就说明编译成功。然后访问前端端口,比如 8081 或 5173,能看到首页才叫前端起来了。但页面能开只是第一步,接口能不能通是另一回事:前端开发服务器的 /api 请求需要被代理到后端 8080,配置在 vue.config.js(对应 Vue CLI)或 vite.config.js(对应 Vite)里:

module.exports = { devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }

如果页面能开但接口全部 404,检查代理 target 的后端端口是否写错;如果接口返回 502,说明代理生效了但后端没起来。这三类现象分别对应前端问题、代理问题、后端问题,排查路径完全不同,先分清再动手。还有一个容易踩到的点是:后端改了端口但前端代理没改,这时候页面和接口各自都正常,放在一起就不通,非常迷惑人。

4. 核心业务实现:商品、购物车、订单的三段代码走读

4.1 商品列表与分页:MyBatis-Plus 的 Lambda 查询

商品是购物平台的起点,列表页和详情页几乎覆盖了所有流量入口。绝大多数课程项目都用 MyBatis-Plus 操作数据库,因为它自带分页插件,把“查第几页、每页几条、按什么条件筛选”封装得非常简洁。商品模块的 Service 实现类里,核心方法大概长这样:

public Page<Goods> pageGoods(Integer pageNum, Integer pageSize, String keyword) { Page<Goods> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Goods> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(Goods::getStatus, 1) .like(StringUtils.hasText(keyword), Goods::getName, keyword) .orderByDesc(Goods::getCreateTime); return goodsMapper.selectPage(page, wrapper); }

这里 eq 表示精确匹配,like 表示模糊查询,StringUtils.hasText 这个条件决定了 keyword 为空时整段 like 不生效。orderByDesc 控制列表默认按创建时间倒序。如果商品表里有销量字段,想改成按销量排序,把排序字段换掉即可,Mapper 和 SQL 都不用动。这段代码看懂后,分类筛选、价格区间筛选都是同样的套路,往 LambdaQueryWrapper 里加条件就行。

MyBatis-Plus 还有一个需要注意的地方:分页要配置 PaginationInterceptor 或 MybatisPlusInterceptor 才真正生效,否则 selectPage 返回的数据是全量查询后在内存里切的假分页。项目里如果只看到 MP 依赖而没有配置拦截器,列表接口在数据量大时会越跑越慢,这一点值得专门检查。

4.2 购物车数据结构:表设计比业务逻辑更值得先想

购物车逻辑本身不难:加购、减数量、删商品、清空列表。难点在于设计放哪里。常见做法是设计一张数据库表,以 user_id 和 goods_id 为联合唯一键,保证同一个用户对同一个商品只存在一条记录。这样加购时不需要先查一次再决定 insert 还是 update,数据库自己就能处理重复插入:

CREATE TABLE cart ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL COMMENT '用户ID', goods_id INT NOT NULL COMMENT '商品ID', count INT DEFAULT 1 COMMENT '数量', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_goods (user_id, goods_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

配合唯一键,后端在“加入购物车”时只需要一条 SQL 完成累加或新增,不需要先 select 再决定操作类型:

INSERT INTO cart (user_id, goods_id, count) VALUES (1, 10, 1) ON DUPLICATE KEY UPDATE count = count + 1;

购物车表里为什么不存商品名称和价格?因为它们属于商品表,购物车只保存 user_id 和 goods_id,每次展示时再联表或查询商品表获取最新价格。这样做的好处是商品改价后,购物车会自动展示最新价格,不用做数据同步。

提示:如果项目把购物车放到 Redis 里,通常是用 Hash 结构以 user_id 为 key 存商品 ID 与数量,好处是读写快,坏处是要自己解决持久化和过期策略。对于学习和课程设计,数据库表方案更直观,也更容易排查。

4.3 订单流程:事务边界与扣库存的时序问题

订单是电商项目里最能体现水平的部分。一个下单动作涉及四件事:校验库存、扣减库存、生成订单、清空购物车,它们必须同时成功或同时失败。Spring Boot 的做法是在 Service 方法上加 @Transactional,让方法内的所有数据库操作处于同一个事务中:

@Transactional public OrderVO createOrder(OrderDTO dto) { Goods goods = goodsMapper.selectById(dto.getGoodsId()); if (goods.getStock() < dto.getCount()) { throw new BusinessException("库存不足"); } goodsMapper.reduceStock(dto.getGoodsId(), dto.getCount()); Order order = new Order(); order.setUserId(dto.getUserId()); order.setGoodsId(dto.getGoodsId()); order.setCount(dto.getCount()); order.setStatus(0); orderMapper.insert(order); return OrderVO.from(order, goods); }

这段代码能跑通课程设计级别的流程,但有一个资深工程师一眼就能看到的并发隐患:先查库存再扣库存之间存在时间差,两个请求同时通过校验时,库存就会被扣成负数。把它往生产方向改的第一步,是把“校验库存 + 扣减”合并成一次原子更新:

UPDATE goods SET stock = stock - #{count} WHERE id = #{goodsId} AND stock >= #{count}

这条 SQL 的影响行数为 0 时说明库存不足,直接抛异常,把原来的判断和扣减两步缩成一步,就把并发窗口期堵上了。订单状态用整型字段表示:0 待付款、1 已付款、2 已发货、3 已完成、4 已取消,状态流转在 Service 层集中判断,避免前后端各写一套状态逻辑的隐患。看订单模块的顺序,建议是:表结构 → 事务注解 → 扣库存 SQL → 状态流转。

5. 从启动到登录:5 个实战翻车点与排查思路

5.1 数据库脚本导入报错:Unknown collation

现象:Navicat 或命令行导入 SQL 时弹出 Unknown collation: 'utf8mb4_0900_ai_ci',导入中断。原因:脚本是从 MySQL 8.0 导出的,本地装的是 5.7,后者不支持 0900 系列排序规则。解决:先确认本地 MySQL 版本,本地是 8.0 就检查是不是连错了实例;是 5.7 就做一次文本替换,把排序规则降级:

sed -i 's/utf8mb4_0900_ai_ci/utf8mb4_general_ci/g' shop.sql

另外检查脚本开头有没有 CREATE DATABASE,没有就先手动建库。导入后务必用 SELECT 抽查关键表数据,确认不是空库——空表会让后端启动正常但页面拿不到任何商品,这种“假成功”比报错更难排查。

5.2 Maven 依赖下载缓慢或构建失败

现象:执行 mvn clean package 时长时间停在 downloading,最后报 Could not resolve dependencies。原因:连接默认中央仓库不稳定,或本地 Maven 仓库里缺了项目依赖的某个构件。解决:在 Maven 的 settings.xml 中配置国内镜像源,再用 -U 强刷快照:

mvn clean package -DskipTests -U

如果构建失败信息里出现某个具体依赖坐标,先确认 pom.xml 里有没有版本号缺失。用 IDE 的话,顺手把 Maven 本地仓库路径改到非系统盘,能避免权限问题引起的神秘失败。这里的老经验是:不要只盯着最后一个报错看,往上翻几行,真正缺失的依赖名称通常写得很明确。

5.3 后端启动时端口被占用

现象:日志里出现 Port 8080 was already in use,Spring Boot 启动线程直接退出。原因:本机有其他 Java 进程占用了 8080,或者上一次调试的后端进程没有完全停止。解决:先找进程再决定杀还是改:

netstat -ano | findstr :8080 # Windows 找 PID lsof -i :8080 # macOS/Linux 找 PID

杀进程后重新启动;如果该端口被其他重要服务占用,就改 application.yml 里的 server.port,同时同步改前端代理的 target。建议开发环境统一端口,减少联调时出现“前端连到别人的服务上”这种玄学问题。还有一种隐蔽情况是 IDE 里旧实例还在运行,新实例启动时端口冲突日志一闪而过,实际上旧进程没被终止。

5.4 前端 npm install 报 node-sass 编译错误

现象:npm install 执行到 node-sass 时报 gyp ERR,或者长时间卡在编译环节不动。原因:项目锁定的 node-sass 版本与本地 Node 版本不匹配,node-sass 在老版本依赖下对 Node 版本非常挑剔。解决:把 package.json 里的 node-sass 替换为 sass,然后重装依赖:

rm -rf node_modules package-lock.json npm install

替换成 sass 后,代码里如果出现 import '~xxx' 这种前缀写法需要微调。如果不想改代码,就换一个与项目年份匹配的 Node 版本,但用 sass 方案更省事,也能让以后的依赖安装不被版本绑架。吃过这个亏的人都明白,node-sass 的编译问题不是靠耐心能解决的,它是版本组合硬约束。

5.5 登录成功但其他接口全部 401

现象:前端登录页能正常登录并跳转,但每调用一个业务接口就返回 401 或“未登录”。原因:token 没有成功存进 localStorage,或 axios 请求拦截器没生效,也可能是后端拦截器白名单没把公开接口放行。解决:按顺序排查。先打开浏览器 Network 面板,看业务请求的请求头里有没有 Authorization。看不到 token 就是前端问题;能看到 token 但后端仍报 401,就是后端校验问题。前端检查登录成功后是否执行了 localStorage.setItem('token', ...),后端检查拦截器白名单,确认 /api/user/login 和 /api/goods/** 在放行列表里。另一个隐蔽原因是登录接口本身也要求带 token,导致登录请求拿不到正常响应,这种自锁现象在项目里并不少见,排查时可以把登录接口临时加进白名单验证。

6. 进阶:给平台加 Redis 缓存与库存锁,再用 JMeter 压测验证

跑通并不是终点,把项目改成能扛一点压力的样子,才算真正吸收。先挑改动最小、收益最明显的接口:商品详情。这个接口请求频率高、数据更新不频繁,是最适合加缓存的场景。key 按商品 ID 设计,先读 Redis,没有就回源数据库,写入时给一个过期时间,防止数据长期不刷新:

public Goods getDetail(Long id) { String key = "goods:" + id; Goods goods = redisTemplate.opsForValue().get(key); if (goods == null) { goods = goodsMapper.selectById(id); redisTemplate.opsForValue().set(key, goods, 30, TimeUnit.MINUTES); } return goods; }

商品更新或删除时,记得主动删除对应 key,否则用户看到的还是旧数据。这个细节比缓存本身更容易被忽略,也是排查“为什么改了价格页面不变”时要优先想到的方向。扣库存那一步的并发问题,用 Redis 分布式锁来堵:锁的 key 用商品 ID,抢到锁才执行库存校验和扣减,执行完再释放,并设置合理的过期时间防止线程崩溃导致死锁。锁的粒度按商品维度拆,不会互相阻塞,对购物平台这种多商品并发的场景最合适。

改造后用 JMeter 建一个下单接口的压测场景:线程数设 10,循环次数设 50,监听器里看聚合报告。重点看三个指标:吞吐量(TPS)、平均响应时间、错误率。改造前如果错误率里混着库存超卖导致的异常,改造后这类错误会明显归零;TPS 和响应时间的变化则能看出缓存和锁有没有拖慢接口。压测时把后端日志级别调成 WARN,否则控制台刷日志会严重拖慢 JVM。

我每次拿到购物平台项目,都会先翻登录鉴权和白名单配置,那里最暴露代码的成熟度;再追一遍订单事务和库存扣减,基本就能判断出这套代码值不值得继续加深。这套流程没有捷径,但压测是验证一切改动是否真的有效的最实在方式。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 22:37:02

宫颈癌YOLOv5检测数据集训练全攻略:格式拆解、调参与避坑

简介&#xff1a;面向目标检测与医学影像识别学习者的宫颈癌YOLOv5检测数据集&#xff0c;定位为可直接投入模型训练与验证的YOLO格式数据包。数据按YOLOv5标准文件夹保存&#xff0c;cancer单一类别&#xff0c;训练集816张、验证集216张&#xff0c;图像为640640 RGB大分辨率…

作者头像 李华
网站建设 2026/10/9 22:34:20

数据库课设实战:进销存系统表结构设计与库存事务避坑指南

简介&#xff1a;这份数据库课程设计资源面向高校计算机及相关专业学生&#xff0c;围绕某商店进销存管理系统展开&#xff0c;适合正在完成数据库原理课程设计、需要参考完整案例的学习者。资源包共3个文件&#xff0c;包含1个bak数据库备份、1个sql脚本和1个doc课程设计报告&…

作者头像 李华
网站建设 2026/10/9 22:31:27

开源PHP支付网关部署对接指南:YPayV7自托管聚合支付实战

简介&#xff1a;这份资源为源支付YPayV7全套开源版V1.8.9&#xff0c;整合SePay、MPay、Epay等常见支付系统源码&#xff0c;面向需要搭建或二次开发支付平台的开发者与企业技术人员&#xff0c;可解决多渠道支付接入、云端免挂等问题。压缩包共两千个文件&#xff0c;约六十九…

作者头像 李华
网站建设 2026/10/9 22:31:19

SQL Server 2014 安装前必看:版本、实例与排序规则选择指南

简介&#xff1a;这份资源是面向数据库初学者与运维人员的 SQL SERVER 2014 安装图解教程&#xff0c;以图文并茂的 PDF 形式呈现&#xff0c;帮助读者在虚拟机环境中顺利完成数据库部署&#xff0c;解决安装过程中常见的组件缺失与配置报错问题。压缩包内仅含 1 个 PDF 文件&a…

作者头像 李华
网站建设 2026/10/9 22:31:16

PHP心理测试源码拆解:计分规则、部署与题库替换实战指南

简介&#xff1a;这是一份面向网站开发初学者与心理测试类站点运营者的静态页面源码包&#xff0c;版本为 v1.0&#xff0c;围绕情感、性格、社交等主题搭建了完整测试栏目&#xff0c;涵盖测试空间、情爱测试、心理测试、社交测试、成功测试、性格测试、性爱测试、个性测试、异…

作者头像 李华