ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot2+Vue3汽车销售系统实战:前后端分离项目从零搭建

SpringBoot2+Vue3汽车销售系统实战:前后端分离项目从零搭建 点进这篇文章的朋友应该正在研究怎么用 SpringBoot2 Vue3 这套主流技术栈把一个实际的业务系统从零搭起来。靓车汽车销售系统就是我之前完整做过的这样一个前后端分离项目功能涵盖了汽车展示、车型分类、预约试驾、下单购买、后台管理等一个销售网站该有的全部主干流程技术栈正好卡在 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0 这组黄金组合上还带了完整的项目文档。这篇文章我尽量不写废话把项目从架构设计、核心模块到环境部署、踩坑记录一次性讲透适合正在做毕业设计、课程设计、或者想快速上手企业级开发流程的同学收藏。这套系统的核心价值在于它不是那种只有一个登录注册的玩具项目而是把真实销售网站的主干业务都走通了前台用户能看车、筛车、下订单后台管理员能管理车辆信息、处理订单、维护分类权限也做了区分。很多同学自己搭项目的时候最头疼的就是“没有完整业务可练手”这个项目正好补上这块拿来改改就能当课程设计交也可以作为简历上的实战项目来写。1. 项目整体架构与核心设计思路1.1 为什么是前后端分离结构以前做 Java Web 项目传统做法是 JSP Servlet JSTL页面由服务端渲染前后端代码混在一起一人改前端样式可能都要在 IDEA 里折腾半天。这次直接用前后端分离是当前企业开发的主流形态前端一套工程Vue3后端一套工程SpringBoot两边只通过 JSON 接口通信。这样设计的直接好处有三个。第一职责清晰前端只管页面渲染和交互后端只管业务逻辑和数据各改各的互不影响。第二接口复用同一个后端接口将来如果要做移动端或者小程序不需要额外开发直接复用现有接口就行。第三部署灵活前端可以扔到 Nginx后端打成 jar 包独立运行扩展性比单体 JSP 项目强太多。前端的入口是根目录下的index.htmlVue 应用挂载在这个页面上。工程结构上分为src/router路由、src/views页面组件、src/api接口封装、src/components公共组件这些目录。后端的入口是Application.java这种带有SpringBootApplication注解的启动类内部的包结构按照 controller、service、mapper、entity 来分层。两层之间通过 HTTP 的 JSON 格式数据沟通前端调用axios发起请求后端用RestController接收并返回统一格式的结果。前后端分离之后一个最直接的感受就是开发效率高了很多。前端不用等后端完全写好接口才能开工两边可以并行开发只要提前把接口文档约定好就行。项目里带的文档规范其实就起到了接口约定说明书的作用。1.2 技术栈选型为什么是这四件套技术选型这件事很多人不太在意觉得能跑就行。但真正到了写简历或者面试的时候面试官一定会问你为什么选这个框架它的优势和坑点是什么。这套项目的选型思路我直接说理由。SpringBoot2 是当前国内中小型项目用得最多的后端框架生态成熟资料齐全踩坑成本低。相比 SpringBoot32.x 对 JDK8 的支持更好而 JDK8 又是大多数公司和学校的标配不用担心版本兼容问题。SpringBoot 内嵌 Tomcat 容器不需要单独部署 war 包到外部服务器一个 jar 包就能跑起来。Vue3 是目前前端框架里最热门的选择组合式 API 写起来比 Vue2 的选项式 API 更灵活代码复用性也更好。配合 Vite 开发服务器热更新速度比 Webpack 快一个量级改完代码保存一下页面就刷新了开发体验非常好。MyBatis-Plus 是在 MyBatis 基础上做的增强工具没有改变 MyBatis 的底层机制但把单表的增删改查操作封装到了极致。你最直观的感受是写一个单表查询不再需要写 XML 里的 SQL 映射直接用BaseMapper接口提供的方法就能完成查询条件用QueryWrapper构造器链式拼接代码量至少减少一半。MySQL8.0 是当前主流的关系型数据库版本相比 5.7官方在性能、安全、窗口函数、通用表表达式CTE这些方面做了很多升级。项目里用到的 utf8mb4 字符集、JSON 字段支持、更严格的 SQL 模式都是 8.0 的良好特性。用这四件套还有一个实际考虑就是国内社区资料足够多。遇到问题的时候一搜就能找到解决方案而不是像用一些偏门框架那样卡壳半天只能自己摸索。对新入行的开发者来说这一点往往比某个框架的某些高级特性更重要。1.3 数据库表结构设计背后的业务思考一个销售网站的核心表大体上就是用户表、车辆表、订单表、车辆分类表这几张。表结构设计得好不好直接决定了业务逻辑写起来省不省力。见过不少初学者把图片地址直接存成 base64 文本或者只存一张图片。这类做法在真实项目里是很不方便的。这辆车可能有车头图、侧身图、内饰图至少需要存多张图片地址合理的方案是单独建一张车辆图片表用car_id关联车辆主表。这样车辆信息的增删改查逻辑与图片解耦后台上传图片时可以先保存到服务器目录再把 URL 写入数据库。用户表与角色表之间通过中间表关联这是经典的 RBAC 权限模型。用户可能是普通顾客也可能是管理员在业务流程上普通用户只能看到前台页面和自己名下的订单管理员才有权限进入后台管理界面。如果当初把“角色”直接写进用户表的一个字段后面扩展角色类型时就要改表结构灵活度差很多。订单表是另一个需要想清楚的地方。订单状态不能只存一个字符串而是应该用数字或者枚举值来代表不同状态比如待支付、已支付、试驾中、已完成、已取消。一方面是存储空间小另一方面是程序里可以用 switch 或者枚举类来管理状态流转逻辑更清晰。提示数据库逻辑外键一定要建但物理外键能不用就不用。物理外键在并发插入或删除时容易造成锁等待而且一旦业务逻辑变动外键约束就成了修改表结构时的负担。靠 service 层保证数据完整性比数据库层的外键约束灵活得多。2. 后端核心逻辑与业务落地细节2.1 后端分层结构与请求处理链路项目启动之后一次典型的请求链路是这样的前端发送 HTTP 请求 - SpringBoot 的 DispatcherServlet 根据 URL 匹配到对应的 Controller - Controller 调用 Service 接口 - Service 实现类处理业务逻辑并调用 Mapper 接口 - Mapper 接口MyBatis-Plus 的 BaseMapper操作数据库 - 数据层层返回 - Controller 将结果包装成统一 JSON 格式返回给前端。这样的分层没有捷径可走Controller 里不要写业务逻辑Service 里不要写 SQL每一层只做自己该做的事。写多了就发现遵循这种规范的项目后期维护的时候定位问题特别快——页面数据不对先看控制台请求是否返回 500再看 Service 层逻辑基本不用从头到尾翻代码。Controller 层有个细节值得注意接口的路径设计要遵循 RESTful 风格比如/api/car/list表示获取车辆列表/api/order/update表示更新订单。虽然很多课程设计里会写成/getCarList这种动词式的路径但项目文档里采用了 RESTful 风格这也是当前业界通用的规范能让接口从 URL 上就很好理解。接下来看 Service 层。每辆车从录入到展示会走的完整流程大概是管理员提交车辆信息车型、品牌、价格、库存、上架状态— 车辆图片上传到服务器 — 车辆信息存入数据库 — 前端首页调用接口拿到上架车辆列表 — 用户点击查看详情 — 用户提交预约或者下单。在这一条链路上Service 层要处理的业务规则有很多比如库存不足时怎么提示、车辆下架后前端还能不能看到详情、预约试驾的时间冲突怎么判断。这些判断逻辑都应该写在 Service 层而不是散落在 Controller 里。最后一层的 Mapper因为用了 MyBatis-Plus单表操作基本不用写 SQL。一个 CarMapper 接口继承BaseMapperCar之后自动就有了selectById、selectList、insert、updateById、deleteById这些方法代码量非常少。多表关联的复杂查询才需要手写而且用注解Select就能搞定不需要维护一大堆 XML 文件。2.2 MyBatis-Plus 写得爽的关键条件构造器和分页MyBatis-Plus 真正让人省心的地方是它的QueryWrapper条件构造器。拿前台车辆筛选来说用户可能根据品牌筛选、价格区间筛选、车型分类筛选组合情况很多。如果用原生的 MyBatis得在 XML 里写一堆if动态 SQL 标签一旦条件多起来XML 文件就很少有人愿意维护。用 QueryWrapper 就直观得多QueryWrapperCar queryWrapper new QueryWrapper(); queryWrapper.eq(StringUtils.hasText(carName), car_name, carName) .eq(carTypeId ! null, car_type_id, carTypeId) .between(minPrice ! null maxPrice ! null, price, minPrice, maxPrice) .orderByDesc(create_time); carMapper.selectList(queryWrapper);第一个参数是布尔值为 true 时才会追加这个查询条件。这样前端传什么参数就按什么条件筛选不传就忽略完美解决了 动态条件拼接 的痛点。这个方法我在自己的项目里反复用最大的体会就是可读性极强看到代码就知道查询逻辑是什么。分页是电商列表页的刚需。MyBatis-Plus 的分页插件配置方式很简单往配置类里注入一个PaginationInnerInterceptor就行。配置完成后使用分页就变成两行代码的事PageCar page carMapper.selectPage(new Page(current, size), queryWrapper);这里有个非常容易踩的坑我必须多说一句。如果你只加了mybatis-plus-boot-starter依赖却没有引入分页拦截器那selectPage方法并不会真正执行分页 SQL而是查出来全量数据再在内存里做假分页。数据量小的时候没有感觉数据量大了页面会明显变慢。所以一定要在配置类里把拦截器加上并且配置好数据库类型为 MySQL。2.3 登录认证与权限控制的落地实现登录认证方案这个项目用的是 JWT 令牌无状态的方案在前后端分离项目里是比较常见的选择。用户登录成功后后端生成一个 token 返回给前端前端存起来放到请求头里之后每次请求都带上。后端通过拦截器校验 token 是否有效有效才放行。JWT 本身由三部分组成Header、Payload、Signature分别存放加密算法和令牌类型、用户信息、签名校验。这个方案的好处是服务端不需要存储 session多个后端实例之间天然共享登录状态水平扩展的时候不用考虑 session 同步的问题。拦截器的注册有个关键点不是所有接口都需要登录比如首页车辆列表、车辆详情这些是公开数据需要直接放行。项目里的拦截器配置会排除/api/car/**下的公开接口而/api/order/**、/api/user/**下的敏感接口则需要校验 token。后台管理的接口还要校验当前用户的角色是不是管理员这一步在拦截器校验完之后在 Controller 里通过自定义注解或者角色判断来完成。注意token 里的用户信息不要放敏感数据比如密码就绝对不能放进去。JWT 是明文签名而不是加密任何人拿到 token 都能解码出 Payload 部分的内容。用户ID、用户名这种级别可以密码一定不要往里放。2.4 核心业务流从看车到下单的完整链路把业务串起来看系统的主干流程是这样运转的。第一步前台加载车辆列表。前端调用/api/car/page接口传当前页码、每页条数、筛选条件后端根据条件查询数据库中“上架状态为 1”的车辆分页返回。列表页每一张车辆卡片都展示车辆名称、品牌、分类、价格、主图。点击进入详情页后前端再调用/api/car/detail/{id}拿到车辆全量信息包括图片列表、车型参数、库存数量、车主评价等。第二步用户注册登录。游客可以浏览车辆但要下订单、预约试驾就必须登录。注册时后端要做密码加密这里用的是 BCrypt 算法同一个密码每次生成的密文都不同可以有效防止彩虹表攻击。这是很多课程设计都容易忽略的地方直接用 MD5 存密码的写法不建议学安全性差太多。第三步提交预约试驾。用户选择车辆、填写试驾时间和联系方式后端生成一条试驾预约记录。这里要校验试驾时间不能是过去时间同一辆车在同一时间段不能被重复预约。库存表里虽然不直接关联试驾但这里控制并发预约的校验逻辑是放在 Service 层完成的在单机环境下通过同步锁可以解决工程上够用就好。第四步下单购车。用户确认车辆配置和价格后提交订单后端判断库存是否充足充足则扣减库存并生成订单状态为“待付款”不充足则直接返回“库存不足”。用户支付成功后订单状态改为“已完成”管理员可以在后台看到所有订单并按状态筛选处理。整个链路里最核心的业务规则就是库存扣减和状态流转。订单状态的枚举设计这里强烈建议用Enum而不是魔法数字代码可读性和后期维护性都会好很多。3. 前端 Vue3 核心页面与交互实现3.1 用 Vite 创建 Vue3 项目与工程目录规划Vue3 项目推荐用 Vite 作为构建工具创建命令就一行npm create vitelatest car-frontend -- --template vue装完依赖之后第一件事是规划目录。按照目前 Vue3 项目的惯例src/api专门放接口请求模块每个页面一个文件夹公共样式抽到assets/styles公共组件放到src/components。这样随着项目越写越大目录仍然可以保持清晰。Vite 的路径别名需要配置默认指向根目录。在vite.config.js里加resolve.alias配置让/可以直接指向src/。不然写组件引用的时候../../../这种相对路径能把人写到崩溃。import { defineConfig } from vite import vue from vitejs/plugin-vue import path from path export default defineConfig({ plugins: [vue()], resolve: { alias: { : path.resolve(__dirname, ./src) } }, server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这里面还有个很有用的配置就是server.proxy代理。开发环境中前端跑在 5173 端口后端跑在 8080 端口存在跨域问题。但通过 Vite 把/api开头的请求代理到http://localhost:8080前端代码里直接请求/api/xxx开发阶段就没有跨域困扰了。生产环境再把 Nginx 上做同样的反向代理配置就行。3.2 车辆列表与详情页的交互细节车辆列表页是前台最重要的一个页面UI 上会展示轮播图、分类筛选栏、车辆卡片网格和分页组件。考虑到每张车辆卡片的内容一致、结构相似抽取成一个CarCard组件是明智的选择。组件接收一个car对象作为 prop内部渲染车辆名称、图片、价格、品牌标签。点击卡片时触发点击事件通过路由跳转到车辆详情页。template div classcar-card clickgoDetail el-image :srccar.mainImage fitcover / h3{{ car.carName }}/h3 p classprice{{ car.price }}/p span classtag{{ car.brandName }}/span /div /template详情页需要展示这辆车的图片列表。图片列表这块如果后端返回的是imageUrls字符串数组前端直接用v-for渲染就行。注意主图的位置要放在数组第一位并且给el-image组件加上preview-src-list属性让用户点击图片能放大预览这个小交互对提升用户体验非常明显。列表页的分页逻辑直接对接后端的Page接口前端维护pageNum和pageSize两个变量切换页码时重新拉取列表数据。分类筛选按钮变化时把categoryId清掉并重置页码为 1再重新请求数据。3.3 路由配置与登录权限守卫Vue Router 4 是 Vue3 配套的路由库配置方式与 Vue2 区别不大但要注意组合式 API 的写法差异。项目的页面路由分两块前台页面和后台管理页面分开后台的/admin路由嵌套在一个带侧边栏布局的父路由下。第一个核心细节是路由懒加载。后台管理的页面比较多如果全部打包进初始 bundle首屏加载会明显变慢。用 import 函数动态导入组件只在使用到该路由时才会加载对应 JS 文件首屏体积能小不少。第二个核心细节是全局前置守卫。用户访问后台管理页面时先检查本地的 token 是否存在不存在就跳转到登录页。存在的话再检查用户信息里的角色字段不是管理员就提示“无权限访问”。router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.path.startsWith(/admin)) { if (!token) { next(/login) } else { const userInfo JSON.parse(localStorage.getItem(userInfo)) if (userInfo userInfo.role ADMIN) { next() } else { next(/403) } } } else { next() } })这样处理之后普通用户即使手动改 URL 也进不去后台安全边界在后端拦截器那里还会再兜底一层。3.4 Pinia 状态管理与组件通信方案Vue3 配套的状态管理库是 Pinia它比 Vuex 更简洁没有mutations那一层修改状态直接调用 action 里的方法就行。项目里用户登录后的用户信息、token 都放在 Pinia 的 user store 里任何组件要读取用户信息直接调用 store 就能拿到。Pinia 的使用方式很简单在src/store/user.js里定义import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: JSON.parse(localStorage.getItem(userInfo) || {}) }), actions: { setLoginData(token, userInfo) { this.token token this.userInfo userInfo localStorage.setItem(token, token) localStorage.setItem(userInfo, JSON.stringify(userInfo)) }, logout() { this.token this.userInfo {} localStorage.removeItem(token) localStorage.removeItem(userInfo) } } })组件间通信这块父子组件用 props 和 emit 是常规操作。非父子组件之间的通信可以先试试 provide/inject或者直接用 Pinia 来解决。比如后台管理员修改了车辆信息后希望列表页同步刷新这个场景里可以用事件总线或者直接刷新路由但最简单的方式还是通过调用接口重新获取数据状态由后端保证一致性。4. 环境搭建、部署与版本兼容性避坑指南4.1 MySQL8.0 安装与数据库初始化MySQL8.0 的安装Windows 环境有图形化安装包Linux 服务器则用 apt 或 yum 安装。这里只说两个最容易出坑的点。第一个是安装完成后一定记得设置 root 密码并且把认证插件设置为mysql_native_password。MySQL8.0 默认的认证插件是caching_sha2_password某些旧版本的客户端和驱动不支持连接时会报Unable to load authentication plugin。这个坑在本地开发环境很常见因为很多人电脑里装的数据库可视化工具比如老版本的 Navicat版本太老就连不上。解决办法有两个一是升级工具版本二是在创建用户时指定认证插件。第二个是时区问题。连接 MySQL8.0 时JDBC 驱动的 URL 里必须加上serverTimezoneAsia/Shanghai不然程序一连接就把时区错误打在控制台上中文乱码也可能会出现。同时数据库连接串里要指定useUnicodetruecharacterEncodingutf8保证中文字符不乱码。项目源码里通常带着建表 SQL 脚本根据文档里的说明直接执行即可。执行前建议本地先建一个独立的数据库比如car_sale_db再用source命令或者客户端工具导入脚本。注意脚本里的字符集和排序规则建库语句尽量写成CREATE DATABASE car_sale_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci。4.2 后端项目启动JDK版本与Maven依赖后端代码是 SpringBoot2JDK 版本推荐 1.8 或 11。首先用 IDEA 打开项目等待 Maven 下载完全部依赖。这一步在国内网络环境下经常会很慢解决方法是给 Maven 的settings.xml配置阿里云镜像。mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror依赖下载完之后修改application.yml里的数据库连接信息改成自己本地的数据库地址、账号、密码。然后启动Application.java控制台出现Started Application字样就表示启动成功。这里最容易被忽略的是 Lombok。项目里大量使用了 Lombok 的Data、Slf4j注解IDEA 里必须安装 Lombok 插件并开启注解处理器不然会一直报找不到符号 getCarName()这种错误很多人就会误以为代码有问题。其实大部分时候根本不是代码有问题是 IDE 没处理注解。4.3 前端项目启动npm安装与跨域配置前端启动命令是npm install npm run devnpm install同样有网络慢的问题推荐设置淘宝镜像源安装速度能提升很多。npm config set registry https://registry.npmmirror.com安装完成进入开发服务器后地址是http://localhost:5173。页面上看车辆列表没有任何数据大概率是接口请求跨域了。前端开发环境的跨域解决方式前面已经提到是在vite.config.js里配置 proxy 代理。确认配置生效的办法是打开浏览器开发者工具查看 Network 面板如果请求路径显示的是/api/...并且状态码是 200说明代理生效了。4.4 生产环境部署思路毕业设计或者个人项目部署到云服务器上的大体流程是前端打包npm run build生成dist目录后端打包mvn package生成 jar 包。服务器先装好 MySQL8.0导入数据库脚本再装好 JDK1.8用java -jar命令启动后端 jar 包最后用 Nginx 部署前端dist目录同时配置反向代理把/api开头的请求转发到后端 8080 端口。参考一份 Nginx 配置server { listen 80; server_name your-domain.com; root /usr/share/nginx/html/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }try_files那行非常关键。前端路由是 history 模式刷新页面时 Nginx 如果找不到对应的真实文件就会返回 404。配置了try_files之后Nginx 会回退到index.html由前端路由自己解析当前地址应该渲染哪个页面。5. 常见问题与排查技巧实录5.1 项目启动与运行中的高频报错速查在实际带着项目跑的过程中我遇到过不少报错下面整理几个出现频率最高的以及对应的解决思路。错误现象根本原因解决方法启动时报Access denied for user rootlocalhost数据库连接账号或密码错误检查 application.yml 的连接配置页面中文显示乱码数据库连接串缺少编码参数在 JDBC URL 加characterEncodingutf8前端请求接口报 401token 过期或未登录重新登录获取 token检查请求拦截器是否正常附加 token分页数据不正确页面返回所有记录分页插件未配置在 MyBatis-Plus 配置类中注入分页拦截器前端页面刷新后 404Nginx 未配置 try_files按上面的 Nginx 配置补上 location /后端启动端口被占用8080 端口被其他进程占用用netstat -ano查看占用进程杀掉或者改端口排查逻辑其实有一套固定的流程先看后端控制台有没有异常堆栈然后再看前端浏览器的 Network 请求面板确认接口是否返回预期状态码。绝大多数问题都可以通过这两步定位到大致范围。5.2 本地开发环境搭建的两个独家经验第一个经验是关于数据库版本。如果本地之前装过 MySQL5.7想要换 8.0一定要把旧版本卸载干净包括注册表、服务、残留的数据目录。很常见的情况是两个版本的服务互相冲突或者端口被占用。更稳妥的方案是用 Docker 跑一个 MySQL8.0 容器完全隔离不干扰宿主机环境。第二个经验是关于 IDEA 的缓存。改完 Maven 依赖或者切换分支后IDEA 偶尔会抽风报一些明明代码没问题的错误。这种时候先不要慌执行File - Invalidate Caches / Restart清理一下缓存再重新打开大概率就能解决。动手改代码之前先确认是不是 IDE 本身的缓存问题这个习惯能帮你省掉大量排查时间。5.3 数据初始化与测试账号说明项目文档里通常会附带一份初始化的测试数据里面会有一个管理员的账号和密码以及若干车辆数据方便演示和测试。实际拿到项目后先用管理员账号登录后台熟悉一下车辆管理、订单管理的操作流程再从前台逛一圈看车、筛车、下单的完整流程确认整个系统都跑通了再开始动手改代码。如果自己导入数据后页面空白大概率是数据表里缺少必要字段或状态值不对。比如车辆表里status字段的值设置成了 0前台就查不到这辆车。车辆图片的 URL 地址如果是相对路径还要确认图片文件是否已经复制到后端的静态资源目录里。提示测试数据只是为了帮你快速跑通链路真正要拿这套项目做课程设计或者面试项目还是建议把数据清空后自己手动走一遍后台添加车辆、前台下单的完整流程这样即使被问到业务细节也能回答得上来。6. 写在最后这套项目还能怎么玩如果只是跟着这篇文章把项目跑起来那还远远不够。我能给的比较实在的建议是拿到源码后先动动手做两轮改造一轮是功能层面一轮是技术层面。功能层面可以给系统加上一个“收藏/关注”功能让用户能把喜欢的车型收藏起来。这个功能看似简单但能串起用户表、车辆表、收藏表的三表关联还能练习联表查询和逻辑删除的用法。或者加一个后台数据统计页面用 ECharts 展示每日订单量、热门车型排行这也是面试时很有亮点的东西。技术层面可以给自己的代码加上统一的异常处理和日志记录用RestControllerAdvice捕获全局异常用Slf4j输出关键业务日志。别小看这两件事真实的项目开发里没有异常兜底和日志追踪出了问题基本靠猜。把这些能力补上这个项目的质量就从一个“能运行的demo”提升到了“接近生产标准”的水平。前后端分离这套技术栈真正跑通一个业务闭环能学到的东西比看一百遍教程都多。照着代码完整敲一遍再根据自己的想法去加功能、改交互、做部署折腾多了你对 SpringBoot2 和 Vue3 的理解会完全不一样。
返回列表