资讯动态

SpringBoot+Vue汽车销售网站毕业设计:前后端分离开发实战解析

发布时间:2026/9/17 4:11:26 来源:尧图企业网站定制
每年到了选毕业设计题目的季节总有人在群里问“Java毕设做什么好”。如果你不想选那种烂大街的图书管理、学生管理系统又怕一上来做商城业务太复杂那 SpringBoot Vue 的汽车销售网站是一个非常合适的选择。靓车汽车销售网站平台就是这样一套前后端分离的 Java Web 毕设项目带完整源码、SQL 脚本和接口文档。下面我会按选题逻辑、数据库设计、后端接口、前端页面、本地部署和答辩思路六个部分把这套项目完整讲一遍想拿它做毕设或者想快速上手前后端分离开发的都能用得上。1. 为什么汽车销售网站适合做Java Web毕设选题思路先想清楚1.1 三个角色把业务边界划得刚刚好做毕设最容易翻车的地方是选了一个看起来简单、但业务边界模糊的题目比如“学生信息管理系统”一做就是十几个功能模块做到最后自己都分不清重点在哪。汽车销售网站的一个好处是业务围绕“卖车”这件事展开参与者只有三类人边界非常清楚游客打开网站看到首页轮播、品牌列表、热门车型、新闻公告点进车型详情能看到参数和图片想下单就必须先注册登录。注册用户登录后可以浏览车型、预约试驾或者直接下单买车、查看自己的订单、取消未支付的订单、对已提车辆进行评论。管理员进后台管理汽车信息的上下架和编辑、处理订单状态确认收款、确认交车、发布公告、管理用户和评论。这样的角色划分对应到数据库表就是用户表、车辆表、订单表、公告表、评论表一张表对应一个业务模块讲起来非常顺。相比电商商城项目汽车销售不涉及物流跟踪、商品SKU、多级促销优惠这些复杂概念订单流程从“下单”到“交车”就结束了业务量刚好够一篇毕设不会被复杂的规则带偏。1.2 技术栈为什么定成 SpringBoot Vue现在很多高校的Java Web课程还在教 JSP Servlet但企业里实际项目已经很少用 JSP 了。毕业设计如果还用 JSP 写页面做出来效果比较老气答辩时也容易被问“为什么不用前后端分离”。这套项目选择 SpringBoot Vue有很实际的理由SpringBoot 解决了原来 SpringMVC 项目里大量 XML 配置的问题内嵌 Tomcat开发期一个 main 方法就能启动部署也简单非常适合毕设这种追求“尽快跑起来”的场景。Vue 做前端页面组件化开发页面效果比 JSP 模板高出不少而且 Vue 的学习曲线在主流前端框架里相对友好复制粘贴改改就能上手。前后端分离以后后端只需要提供 JSON 接口前端调接口渲染页面两者职责清晰写代码和写文档的粒度都更好控制。具体版本上后端用 SpringBoot 2.x MyBatis-Plus或 MyBatis前端用 Vue 2 Element UI。现在网上能找到的成熟源码大多基于这套组合出问题时搜解决方案也容易。如果你非要用 Vue 3 Element Plus也不是不行但毕设阶段尽量别给自己加戏稳定跑通比追新更重要。1.3 项目目录结构源码包里装了什么一个合格的毕设源码包不应该只扔一堆 Java 文件而是要有清晰分层。这套项目的目录结构是这样的love-car/ ├── backend/ # SpringBoot 后端工程 │ ├── src/main/java/ │ │ └── com/car/sale/ │ │ ├── common/ # 统一返回结果、全局异常处理 │ │ ├── config/ # 跨域配置、拦截器注册、静态资源映射 │ │ ├── controller/ # 接口层 │ │ ├── entity/ # 数据库实体类 │ │ ├── mapper/ # MyBatis Mapper 接口 │ │ ├── service/ # 业务逻辑层 │ │ ├── utils/ # JWT、MD5 等工具类 │ │ └── vo/ # 给前端展示用的组装对象 │ ├── src/main/resources/ │ │ ├── mapper/ # MyBatis XML 文件复杂SQL放这里 │ │ └── application.yml │ └── pom.xml ├── frontend/ # Vue 前端工程 │ ├── src/ │ │ ├── api/ # 接口请求封装 │ │ ├── assets/ # 静态资源、样式 │ │ ├── components/ # 公共组件 │ │ ├── router/ # 路由配置 │ │ ├── views/ # 页面 │ │ ├── App.vue │ │ └── main.js │ ├── package.json │ └── vue.config.js ├── sql/ │ └── car_sale.sql # 建库建表 初始化数据 └── 接口文档.md这个结构的核心思想是“约定优于配置”后端按 controller/service/mapper 分层前端按页面和接口分目录任何人拿到源码都能很快找对文件。很多同学答辩被问“你这个项目的标准目录结构是什么”其实就是看你对这个分层的理解而不是真的背一条规范。2. 数据库设计6张核心表把“卖车”这件事串起来2.1 核心表清单与字段设计数据库是整个项目的底座表设计合理后面接口能省一半事。这套项目核心就是 7 张表表名说明关键字段t_admin管理员表id, username, passwordt_user注册用户表id, username, password, nickname, phone, create_timet_brand车辆品牌表id, brand_name, brand_logot_car汽车信息表id, brand_id, car_name, series, price, dealer_price, displacement, gearbox, fuel_type, seats, stock, image_url, images, description, status, create_timet_order订单表id, order_no, user_id, car_id, car_name, car_image, price, quantity, total_price, status, receiver_name, receiver_phone, receiver_address, create_time, pay_time, finish_timet_comment评论表id, user_id, car_id, content, score, create_timet_news新闻公告表id, title, content, author, create_time汽车信息表里有几个字段要重点说明。brand_id关联品牌表通过它可以在前端做“按品牌筛选”price是指导价dealer_price是经销商报价两个价格字段同时存在页面可以展示“厂商指导价”和“优惠价”的对比效果image_url是封面图images存多张展示图多条 URL 用逗号拼接前端拿到后 split 成数组渲染成图片列表status标记是否上架管理员下架的车辆在用户端不可见但后台还能查到。订单表里有一个细节car_name、car_image、price这三个字段看起来冗余因为车辆信息表里已经能查到了为什么还要在订单表里存一份这就是典型的快照冗余设计。用户下单那一刻车型名称和价格是确定的如果之后管理员修改了车辆价格或名称用户的“历史订单”不应该跟着变。订单里冗余一份快照就保证了历史数据不可变这比每次显示订单都要去关联查询车辆表更合理也更符合真实的销售业务。2.2 订单状态机一条订单从生到死有几种状态订单模块是毕设里最容易讲出彩的部分因为订单业务天然有“状态”概念。这套项目里订单状态很简单用一个 int 字段表示0 待支付下单成功但未付款用户可以在个人中心取消。1 已支付 / 待交车模拟支付成功等待管理员确认交车。2 已完成管理员在后台将订单置为“已交车”订单流程结束。3 已取消用户主动取消仅限待支付状态或者超时未支付由系统/管理员取消。状态流转方向很明确前端根据状态显示不同的按钮后端在处理状态变更时要做判断比如一个“已完成”的订单不允许被取消。用最简单的 if 判断就能挡住大部分非法操作这也是答辩时老师比较关注的“业务逻辑完整性”问题。2.3 库存扣减防超卖的最简写法车辆库存虽然没有电商秒杀那么大的并发压力但“扣库存”这个动作还是应该写对。最直接但也最容易出问题的写法是Car car carMapper.selectById(carId); if (car.getStock() count) { throw new RuntimeException(库存不足); } car.setStock(car.getStock() - count); carMapper.updateById(car);这段代码在单用户场景下没问题但一旦有并发请求两个线程同时读到库存是 1都判断“足够”然后各自减 1库存就变成 -1 了。毕设虽然并发量不大但代码写出来要体现思路。可以用一条原子更新的 SQL 解决UPDATE t_car SET stock stock - #{count} WHERE id #{carId} AND stock #{count}如果影响行数为 0说明库存不足直接抛异常回滚订单。这样即使同一时间有多个下单请求也不会超卖。下面讲订单接口时还会配合事务再做一遍完整处理。2.4 SQL脚本里的初始化数据和字符集坑拿到 SQL 脚本后很多人第一件事就是直接 source 导入结果出现乱码或表结构异常。SQL 文件里要注意几点建库语句建议用CREATE DATABASE IF NOT EXISTS car_sale DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;尽量用 utf8mb4 而不是 utf8。因为 utf8mb4 才是完整的 UTF-8 编码可以存表情符号评论里万一有 emoji用老 utf8 会报错。初始化数据里一般会预置一个管理员账号 admin / 123456几个测试用户以及大众、丰田、本田、比亚迪等常见品牌的若干车型数据。这些初始化数据不是为了凑数而是让你项目启动后页面就有内容可看不会被答辩老师追问“为什么没数据”。3. 后端SpringBoot接口拆解从用户登录到订单创建的完整链路3.1 统一返回结果和全局异常让前端省心后端接口设计的第一件事是给前端一个统一的返回格式。如果每个接口返回结构都不一样前端处理起来会非常痛苦。这套项目用了一个ResultT泛型类public class ResultT { private Integer code; // 200 成功500 失败401 未登录 private String msg; // 提示信息 private T data; // 业务数据 }Controller 里所有接口都返回 Result成功直接Result.success(data)失败就Result.error(参数错误)。配合一个全局异常处理器把业务异常和系统异常统一转换为 Result 返回这样前端 axios 拦截器只需要判断 code 是否为 200 就能统一处理成功和失败不用每个接口单独写防御逻辑。这个设计在答辩时也是一个不错的亮点可以主动说一句“我用统一返回体和全局异常处理保证了接口的分层一致性”。3.2 登录注册JWT 令牌和拦截器的配合用户模块是几乎所有接口的前置依赖登录逻辑必须讲明白。流程是这样的前端把用户名和密码提交到POST /api/user/login。后端根据 username 查用户对比密码。密码存数据库前经过 MD5 加盐处理这里不推荐明文存储。校验通过后用 JWT 工具生成一个 token包含 userId、username、过期时间返回给前端。前端把 token 存在 localStorage之后每次请求在请求头加Authorization: token。后端通过拦截器拦截需要登录的接口从请求头取出 token 并解析解析失败就返回 401。拦截器的核心代码大致是这样public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !JwtUtils.validate(token)) { response.setStatus(401); return false; } request.setAttribute(userId, JwtUtils.getUserId(token)); return true; } }这里有一个很容易踩的坑跨域请求会先发一个 OPTIONS 预检请求如果你的拦截器拦截了 OPTIONS前端会发现请求一直报跨域错误而 GET/POST 都没问题。需要在拦截器里加一行判断如果是 OPTIONS 请求直接放行。3.3 车型分页与多条件筛选一台车怎么被“筛”出来车型列表是所有用户都看得见的公开接口也是整个项目被点击最多的接口。前端列表页会传这些参数过来页码、每页条数、品牌 id、关键词、最低价、最高价。后端 Service 层用 MyBatis-Plus 的 LambdaQueryWrapper 动态拼条件代码很直观LambdaQueryWrapperCar wrapper new LambdaQueryWrapper(); wrapper.eq(brandId ! null, Car::getBrandId, brandId) .like(StringUtils.hasText(keyword), Car::getCarName, keyword) .ge(minPrice ! null, Car::getDealerPrice, minPrice) .le(maxPrice ! null, Car::getDealerPrice, maxPrice) .eq(Car::getStatus, 1); PageCar page carMapper.selectPage(new Page(pageNum, pageSize), wrapper);这里用条件构造器的好处是条件无论传不传拼接逻辑都写在明面上不会出现写死 SQL 导致筛选失效的问题。如果项目用的是原版 MyBatis 而不是 MyBatis-Plus就需要在 XML 里写if testbrandId ! null之类的动态 SQL 了。分页查询返回给前端的数据除了当前页列表还应该包含总条数 total方便前端渲染分页组件的总页数。MyBatis-Plus 的 Page 对象里已经带了 total直接放进 Result 返回即可。3.4 创建订单事务和状态机的闭环创建订单是后端逻辑最完整的一段也是毕设答辩时最能拿来讲“项目含金量”的地方。接口定义是POST /api/order/create参数包含 carId、数量、提车人姓名、电话和地址。Service 层代码如下Transactional(rollbackFor Exception.class) public Order createOrder(Long userId, OrderCreateDTO dto) { // 1. 查询车辆判断是否存在且已上架 Car car carMapper.selectById(dto.getCarId()); if (car null || car.getStatus() ! 1) { throw new BusinessException(车辆不存在或已下架); } // 2. 原子扣库存防止超卖 int rows carMapper.deductStock(dto.getCarId(), dto.getQuantity()); if (rows 0) { throw new BusinessException(库存不足); } // 3. 生成订单保存快照信息 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setCarId(car.getId()); order.setCarName(car.getCarName()); order.setCarImage(car.getImageUrl()); order.setPrice(car.getDealerPrice()); order.setQuantity(dto.getQuantity()); order.setTotalPrice(car.getDealerPrice() * dto.getQuantity()); order.setStatus(0); order.setReceiverName(dto.getReceiverName()); order.setReceiverPhone(dto.getReceiverPhone()); order.setReceiverAddress(dto.getReceiverAddress()); orderMapper.insert(order); return order; }注意两点。第一Transactional保证扣库存和生成订单要么都成功要么都失败不会出现库存扣了但订单没生成的中间状态。第二订单编号用时间戳加随机数生成格式一般是20250601123045001虽然这不是严格的全局唯一方案但毕设场景完全够用。用户取消订单的接口同样要注意状态判断只有状态为 0待支付的订单才允许用户取消并且取消时要恢复库存UPDATE t_car SET stock stock 1 WHERE id ?。这两个点放到一起讲老师会觉得你是真的理解了订单业务而不是照着 CRUD 抄了一通。3.5 后台管理接口与图片上传后台管理接口的套路比较统一基本就是增删改查POST /api/admin/car/save新增或编辑车辆。判断 id 是否为空为空就是新增否则是更新。DELETE /api/admin/car/{id}删除车辆。POST /api/admin/order/updateStatus确认收款、确认交车。POST /api/admin/news/save公告发布。图片上传是很多同学容易卡住的点。前端用 el-upload 选图后需要先调后台上传接口把图片保存到服务器后端再把可访问的图片 URL 返回给前端前端把它填到表单的 imageUrl 字段里一起提交。后台上传接口一般长这样PostMapping(/api/upload) public ResultString upload(MultipartFile file) { String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String fileName UUID.randomUUID() ext; String savePath uploadDir File.separator fileName; file.transferTo(new File(savePath)); return Result.success(/upload/ fileName); }为了让/upload/xxx能直接通过浏览器访问还需要在配置类里做虚拟路径映射把磁盘上的 upload 目录映射到 URL 的/upload/**。这里有个毕设常见的坑上传目录如果写在项目相对路径下用 IDEA 启动没问题但打成 jar 包运行时路径可能就不对了。稳妥的做法是在 application.yml 里配置一个绝对目录比如D:/car_upload/映射也指向它。3.6 接口文档长什么样接口文档不是应付检查的文档它真正的作用是后端没写完时前端可以先按文档约定并行开发。这套项目的接口文档按模块组织每个接口描述包含路径、请求方式、请求参数、返回结果示例。比如接口方法参数返回/api/user/loginPOSTusername, passwordcode, msg, data.token/api/user/registerPOSTusername, password, phonecode, msg/api/car/pageGETpageNum, pageSize, brandId, keyword, minPrice, maxPricecode, msg, data.total, data.records/api/car/detail/{id}GET路径参数 idcode, msg, data(车型详情)/api/order/createPOSTcarId, quantity, receiverName, receiverPhone, receiverAddresscode, msg, data(订单)/api/order/listGET无从 token 取 userIdcode, msg, data(订单列表)/api/admin/car/savePOST车辆所有字段code, msg/api/uploadPOSTfilecode, msg, data(图片URL)写文档时建议每个接口配一个返回 JSON 示例前端照着字段取数基本不会出大错。4. 前端Vue实现页面怎么拼接口怎么接4.1 路由规划与登录守卫前端目录里的 router/index.js 定义了整个网站的页面跳转关系。这套项目的基础路由设计如下const routes [ { path: /, component: Home }, { path: /cars, component: CarList }, { path: /car/:id, component: CarDetail }, { path: /login, component: Login }, { path: /register, component: Register }, { path: /user/orders, component: UserOrders, meta: { requiresAuth: true } }, { path: /admin/cars, component: AdminCars, meta: { requiresAuth: true, role: admin } }, { path: /admin/orders, component: AdminOrders, meta: { requiresAuth: true, role: admin } }, ];登录守卫是毕设里必须体现的一个细节。所谓路由守卫就是进入某些页面之前先检查是否有登录态没有就跳去登录页。Vue Router 里通过beforeEach钩子实现router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next(/login); } else { next(); } });这样用户在未登录状态下访问“我的订单”或后台管理会被自动拦到登录页。这个机制非常直观答辩时一句话就能讲明白。4.2 首页、列表页、详情页的呈现要点首页不必花哨但结构要完整。一般包含三块顶部轮播图、热门车型推荐、最新公告列表。轮播图建议用 Element UI 的el-carousel组件图片地址从后端返回的热门车型中取。热门车型接口可以简洁地定义为“按创建时间倒序取前 6 台上架车辆”数据量小性能不用考虑。车型列表页是核心页面左侧放品牌筛选右侧放车辆卡片列表底部放分页。用户点击某个品牌时重新带上 brandId 参数请求接口。列表页要注意切换筛选条件时把页码重置为 1否则会出现“点筛选后分页停在旧页码导致列表空白”的问题这是一个非常常见的细节 bug。车型详情页逻辑更聚合一些顶部是车型图片和价格信息中间是参数表格排量、变速箱、燃油类型、座位数右侧是操作按钮“立即购买”和“预约试驾”。立即购买直接调用创建订单接口预约试驾可以简化为提交留言或者直接弹窗提示“预约成功”。详情页从 URL 里拿 id 调/api/car/detail/{id}这个接口除了返回车型信息还可以顺便返回该车型的评论列表。4.3 axios封装和跨域处理前端每个页面都直接写 axios 会非常乱所以一般会在 src/api/request.js 里做一个 axios 实例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( res { if (res.data.code 401) { localStorage.removeItem(token); window.location.href /login; } return res.data; }, err { return Promise.reject(err); } ); export default request;baseURL 配成/api而不是http://localhost:8080是前后端分离项目里非常关键的一个设计。开发环境下用 Vue CLI 的 devServer 代理把请求转发到后端避免跨域// vue.config.js module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, }, }, }, };这样开发时前端跑在 8081 端口后端跑在 8080 端口但前端请求的是相对路径/api代理帮你把请求转发过去浏览器的同源限制就不会触发。等到项目打包部署后再用 Nginx 把/api代理到后端代码不用改。4.4 后台管理页面的增删改查套路后台管理页面的写法在毕设项目里高度重复掌握一个页面模板其他模块照抄即可。以车辆管理为例页面由四部分组成顶部的搜索条件、中间的el-table表格、右下角的分页、弹出来的el-dialog表单弹窗。车辆管理的核心流程是页面加载时调用分页接口把表格数据填进去。点击“新增”按钮弹窗里的表单清空点击“编辑”按钮把当前行数据回填到表单。表单提交时调用保存接口根据表单有没有 id 决定是新增还是更新。操作成功后关闭弹窗重新拉取列表。删除操作要加this.$confirm确认框避免误删数据。整个过程中的状态管理不用 Vuex 那些重型方案页面内用 data 维护即可毕设的粒度完全够用。5. 从零跑通全流程环境版本对照、SQL导入与高频报错5.1 环境版本别乱装这是整套项目能不能跑起来最关键的一步很多人卡在“明明代码一样为什么我这边报错”就是因为版本问题。基于这套项目的技术栈我建议环境版本对照如下组件推荐版本说明JDK1.8最重要别用 17/21 跑老项目Maven3.6.33.8 有时下载依赖会卡MySQL5.7 或 8.08.0 需要配置驱动Node.js14.x 或 16.x版本太高可能不兼容 node-sassSpringBoot2.5 ~ 2.7别用 3.x坑在下面细说Vue CLI4.x 或 5.x配合项目 package.json 即可5.2 导入SQL和配置application.yml导入 SQL 用命令行最稳打开终端执行mysql -u root -p CREATE DATABASE IF NOT EXISTS car_sale DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE car_sale; SOURCE /你的路径/car_sale.sql;用 Navicat 或其他图形工具导也可以但记得先手动创建数据库并选对字符集直接双击导入 SQL 文件有时会因为库不存在报错。导入之后验证一下表数量和数据条数比如SELECT * FROM t_brand;能查出品牌数据就说明导入成功。后端启动之前要检查 application.yml 的配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/car_sale?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你自己的密码 mybatis-plus: mapper-locations: classpath*:mapper/*.xml configuration: map-underscore-to-camel-case: true如果 MySQL 是 8.0 以上driver-class-name用com.mysql.cj.jdbc.Driver如果是 5.7用com.mysql.jdbc.Driver也行但 cj 驱动同样兼容统一用 cj 即可。serverTimezone必须配否则报时区错误。5.3 SpringBoot版本太高导致的三个报错这是最近两年毕设翻车重灾区。很多同学从网上下载模板时用的是 SpringBoot 2.7但 IDE 新建项目默认会拉 3.x 版本一启动就是一堆红色异常。第一个坑是 JDK 不兼容。SpringBoot 3.x 强制要求 JDK 17如果你的机器装的是 JDK 8项目直接启动不起来。解决方法是把 pom.xml 里的 spring-boot-starter-parent 版本改回 2.7.x。第二个坑是 javax 变 jakarta。SpringBoot 3.x 里原来的javax.servlet包全部改成了jakarta.servlet如果你下载的代码里用到了import javax.servlet.*启动时会报ClassNotFoundException。第三个坑是 MyBatis-Plus 适配问题。SpringBoot 3.x 需要更高版本的 mybatis-plus而且引入的依赖要加spring-boot-starter-jdbc不然会出现奇怪的循环依赖或找不到数据源。所以最省事的做法是毕设不要追求版本新老老实实把 SpringBoot 降到 2.7MyBatis-Plus 用 3.5.x基本一次跑通。5.4 Vue依赖安装、启动和打包前端启动相对简单但依赖安装的坑也不小。项目根目录执行npm install npm run serve如果npm install很慢先设置镜像npm config set registry https://registry.npmmirror.com比较常见的是报node-sass安装失败。node-sass 这个库已经不怎么维护了对 Node 版本极其敏感。遇到这种报错直接把 package.json 里的node-sass: ...替换成sass: ^1.63.0然后在 vue.config.js 或全局样式配置里处理兼容即可。如果项目依赖不深很多情况下直接删掉 node-sass 换用 dart-sass 就能解决。打包命令是npm run build。打包后如果打开 index.html 看到空白页面或者布局错乱大概率是资源路径问题。可以检查 vue.config.js 里的publicPath改成相对路径publicPath: ./而不是默认的/。如果用了 Vue Router 的 history 路由模式打包后直接双击 index.html 打开会发现路由失效最简单的处理是换成 hash 模式也就是在路由配置里写mode: hash。在 Nginx 部署场景下history 模式需要额外配置 try_files 回退对于毕设来说用 hash 模式最省心。5.5 接口文档的正确用法先Postman后联调很多人拿到接口文档不知道从哪开始。我的建议是后端启动后先把接口文档里所有接口在 Postman 里过一遍确定返回结构正常再打开前端页面联调。如果你一上来就直接开前端页面遇到报错时很难分清是前端写错了还是后端接口有问题。先跑通后端接口相当于把问题排查范围缩小了一半。比如登录接口在 Postman 里发一个 POST 请求body 选择 raw-JSON{ username: admin, password: 123456 }返回里能看到 token 字段再用这个 token 去请求需要登录的接口验证拦截器是否生效。这个过程走通了再回到页面里点登录如果还报错那问题大概率在 axios 封装或者 token 存储的代码上定位起来就非常快。6. 答辩讲解思路与三个容易加的扩展点6.1 讲项目的主线逻辑答辩时不要在 PPT 上罗列一堆页面截图老师更想听的是你对代码逻辑的理解。建议按一条“请求链路”来讲用户在登录页输入用户名密码前端把请求发到后端/api/user/login后端先查用户表校验密码通过后用 JWT 生成 token 返回前端存到 localStorage之后再访问“我的订单”时路由守卫判断有没有 token后端拦截器再校验 token 是否有效有效才返回订单列表。把这条链路讲清楚比单纯说“我用了 SpringBoot 和 Vue”有说服力得多。然后再重点讲一个业务闭环比如“创建订单时如何扣库存、如何生成订单快照、取消订单如何恢复库存”这一段就是项目含金量的体现。6.2 老师大概率会问的问题根据我陪很多同学模拟答辩的经验老师围绕汽车销售网站常问的问题大概有这么几个为什么用 JWT 而不用 Session答前后端分离后后端不维护会话状态JWT 无状态、可扩展前端存 token每次请求带在请求头里。如何防止库存超卖答使用带条件的 UPDATE 原子扣减配合事务保证扣库存和生成订单的一致性。分页查询怎么实现的答MyBatis-Plus 的 Page 对象底层是 LIMIT前端传入 pageNum 和 pageSize返回 total 和 records。数据库哪些字段建了索引答建议在用户表的 username、车辆表的 brand_id 和 status、订单表的 user_id 上建普通索引检索频率高。这些问题都不深但对应答者水平的分辨率很高。被问住了不要慌说清楚自己项目的实现版本就好比如“我的项目里用 XX 解决了库存问题”比背标准答案更让老师信服。6.3 可以快速落地的扩展方案如果时间和精力允许给项目加一两个扩展点答辩的观感会好很多。我推荐三个成本不高、但效果明显的方向第一给热门车型加 Redis 缓存。详情页或首页的车型列表是热点数据查询逻辑简单非常适合缓存。用一个StringRedisTemplate缓存 JSON 字符串首次查询加载数据库并写入缓存之后直接取缓存。这个扩展点能引出“缓存穿透”“缓存一致性”这些词容易加分。第二加一个 Excel 导入导出功能。后台车辆管理支持导出当前列表到 Excel或者通过上传 Excel 批量导入车辆。用 EasyExcel 工具类几千行代码就够但体现的技术点很完整包括文件上传、解析、批量插入。第三对接一个沙箱支付。虽然真实支付需要商户号但支付宝沙箱和微信支付沙箱环境可以完全模拟支付流程给订单接上支付回调订单状态从“待支付”变为“已支付”。这个扩展点能直接提升项目的完整度不过时间成本会高一些量力而行。最后再分享一个实际经验。这套项目我前前后后帮人排查过很多次印象最深的一次是配环境搞了一天都没跑起来最后发现只是 pom.xml 里的 SpringBoot 版本用了 3.x导致启动类上的一堆注解全失效。所以如果你也想拿这套代码当毕设或者学习材料第一步一定是对照第 5 章的版本表检查环境不要急着看代码。跑通之后再按自己的思路去改页面、加功能你会明显感觉到“前后端分离开发”这件事没有想象中那么复杂。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价