恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
SpringBoot+Vue无人智慧超市管理系统开发实战
首页
资讯中心
/
SpringBoot+Vue无人智慧超市管理系统开发实战
SpringBoot+Vue无人智慧超市管理系统开发实战
发布时间:2026/10/5 7:10:40
做无人智慧超市这个选题之前我刚把前后端分离的作业项目写完打回三次原因都不复杂——接口路径乱写、异常没处理、前端拿着后端字段对不上。这次做“SpringBootVue 无人智慧超市管理系统平台”我特意在动手前把整个项目当成一个真实的交付物来规划而不是只想着“把功能做出来”。项目源码里带 SQL脚本、接口文档、前端Vue工程、后端SpringBoot工程这四块恰好对应了一套完整商业系统从设计到落地的全部环节。这篇文章我就按这套交付结构把需求拆分、表结构设计、接口逻辑、前端页面、打包部署和排查记录完整梳理一遍。这个项目适合三类人第一类是正在选Java Web毕业设计题目的在校生无人超市这个业务够新、够具象但不会难到手写算法或模型训练那个级别第二类是已经学过SpringBoot和Vue基础、想通过一个完整项目把技术点串起来的开发者第三类是准备做毕设答辩需要把“为什么要这么设计”讲清楚的同学。我尽量把每个选择背后的原因也写出来保证你被老师追问的时候顶得住。1. 项目到底在解决什么问题——需求拆分与功能设计思路1.1 无人超市的业务本质是什么传统超市的运营链条里顾客从进门到出门要经历找商品、排队结账、扫码付款、开发票、带走小票。超市老板要干的活儿更多每天盘点库存、核对销售数据、调整价格标签、防止漏单被盗。无人智慧超市的核心思路是把顾客自助和后台自动管理这两件事用一套Web系统串起来。顾客端看到的是一套能浏览商品、加购物车、模拟扫码结算、查看订单记录的页面管理端看到的是一套能维护商品信息、管理分类、查看订单流水、处理库存预警的后台。业务看起来简单但落到系统设计上它涉及用户权限体系、商品上下架状态、库存扣减一致性、订单状态流转、前端交互状态同步等问题每个点都能展开做深度优化。我当时做需求梳理时给自己提了一个标准这个系统的业务流程必须自洽不能有“能下单但库存不改”或者“订单生成但没有商品明细”这种逻辑漏洞。无人超市的钱货关系是顾客付款 → 订单生成 → 库存扣减 → 销售数据更新。这四个动作必须是一个完整的事务链条这也是整个系统设计的核心约束。1.2 功能模块怎么拆边界画在哪按角色划分系统可以分成两条主业务线。第一条线是顾客自助购物线对应的功能模块有用户注册登录、商品分类浏览、商品详情查看、购物车管理、订单提交、订单列表与详情查询。第二条线是管理员运营线对应的功能模块有管理员登录、商品管理增删改查、上下架、分类管理、订单管理查看、发货/完成、用户管理、统计概览销售数量、销售额、库存预警。模块的边界我是按“数据归属”来画的。顾客线只操作自己的购物车、订单和个人信息管理员线只操作商品、分类和全局订单。商品库存字段被订单模块引用但库存数值只有结算方法会更新管理员手动改库存也走独立的库存调整接口避免多路并发写同一个字段导致数据异常。还有一个容易忽略的模块是“模拟支付”。毕业设计里一般不做真实支付对接我会在结算接口里加入一个支付状态字段默认置为已支付但接口路径上保留“创建订单”和“支付回调”两个接口方便演示和扩展。这样既体现你对业务的理解又不会因为没接支付宝被老师扣分。1.3 为什么这个选题适合Java Web毕设毕业设计最怕的不是功能多而是“能做但在答辩时讲不清楚”。无人超市系统最大的优势在于每一行代码都能对应到一个具体的业务解释。比如你写了一个InventoryService.deductStock()方法你可以解释为“当用户提交订单时系统必须原子性地扣减对应商品的库存防止超卖”你写了一段Vue的watch监听购物车数量变化你可以解释为“前端需要实时计算总金额并在数量变化时同步订单明细”。这种“代码—业务”的一一映射关系在答辩时非常重要能让你在评委提问时引回自己熟悉的模块而不是被带进陌生的技术细节。从技术覆盖面来说这个项目也压得足够满SpringBoot的自动配置、AOP日志、统一异常处理、MyBatis映射、事务管理、JWT登录鉴权、Vue组件通信、路由守卫、Axios拦截器、数据库设计范式。没有深度学习路径的依赖不用GPU服务器一台开发机从零到部署完成大概一周能做完属于“投入产出比”很高的选题。2. 技术选型与工程结构——SpringBoot和Vue为什么这么搭2.1 后端选SpringBoot而不是SSH/Servlet的原因毕业设计的后端框架每年都有同学在SpringBoot和SSH之间纠结。我的建议很直接除非老师指定旧框架否则一定选SpringBoot。SSH那个年代的问题在于配置繁琐——Struts的XML配置、Spring的Bean配置、Hibernate的映射配置一套下来光配置文件就能写上百行业务代码反而被淹没。SpringBoot通过自动配置把绝大多数默认行为都处理好了你只需要关注自己的业务代码这在写毕设时能节约大量的调试时间。更关键的是SpringBoot的生态资料太丰富了。你遇到的90%的问题都能直接在社区里找到对应的解决方案比如跨域配置、拦截器注册、MyBatis的Mapper扫描、全局异常处理这些都是高频问题搜一下就有完整答案。做毕设时时间本来就紧没必要在框架配置上跟编译器较劲。环境版本方面我当时用的是 Spring Boot 2.7.x JDK 1.8为什么不用Spring Boot 3因为Spring Boot 3强制要求JDK 17而你见过的老教程、学长留下的笔记、部分框架组件版本大都是基于JDK 8的。为了减少踩坑成本2.7.x是我目前最推荐的生产力版本稳定、兼容性广、资料齐全。2.2 前端选Vue而不是JSP/Thymeleaf的原因JSP和Thymeleaf这类服务端渲染方案确实能让Java后端同学上手快因为页面直接嵌在后端工程里不用考虑跨域。但问题在于前后端职责混在一起改个页面样式可能要重启后端服务遇到前端报错还得去后端日志里找维护体验很差。Vue把前端变成了一个独立的工程有自己的一整套开发流程npm install装依赖npm run dev起本地开发服务器开发时可以直接热更新页面和后端通过HTTP接口通信。这样的前后端分离结构更接近真实企业开发的标准。而且Vue的组件化思想对毕设的代码组织很友好——一个商品卡片组件可以同时用在“商品列表页”和“搜索结果页”管理员后台的表格也可以复用分页组件减少重复代码量。Vue工程本身也带路由和状态管理路由解决“页面跳转”状态管理解决“多个页面共享数据”。比如用户在商品页把商品加入购物车跳转到购物车页面后需要显示同一个购物车数据这就必须使用Pinia或Vuex来维护一份共享状态而不是每次切换页面都重新请求后端。用JSP时代这种跨页面数据共享只能靠Session或者URL参数非常别扭。2.3 前后端分离项目结构到底怎么摆我先说一个常见的反面案例很多同学把前端打包后的dist目录直接塞进后端工程的static文件夹然后当作单一项目交给老师。这样做完全失去了前后端分离的意义而且在开发阶段非常痛苦——前端改一行代码都要重新打包一次。正确的做法是建立两个独立工程目录例如project-root/ ├── backend/ # SpringBoot后端工程 │ ├── src/main/java/com/supermarket/ │ │ ├── controller/ # 接口层 │ │ ├── service/ # 业务逻辑层 │ │ ├── mapper/ # MyBatis的Mapper接口 │ │ ├── entity/ # 实体类 │ │ ├── config/ # 配置类跨域、拦截器、Swagger等 │ │ ├── common/ # 统一返回结果、全局异常、工具类 │ │ └── SupermarketApplication.java │ ├── src/main/resources/ │ │ ├── mapper/ # MyBatis的XML文件 │ │ ├── application.yml │ │ └── sql/ # 项目的SQL脚本备份 │ └── pom.xml └── frontend/ # Vue前端工程 ├── src/ │ ├── api/ # axios接口封装 │ ├── router/ # 路由配置 │ ├── views/ # 页面组件 │ ├── components/ # 公共组件 │ ├── store/ # Pinia状态管理 │ └── main.js ├── package.json └── vite.config.js两个工程独立开发、独立启动开发阶段前端localhost:5173、后端localhost:8080通过Vite的代理把/api请求转发到后端。这样前后端各自跑各自的互不干扰。最终交付时再介绍“Vue打包后如何放入SpringBoot一起运行”的部署方案我会在第6章详细说。3. 数据库设计与SQL脚本——订单、库存、结算怎么建模3.1 核心表结构和关键字段数据库设计是整个项目的基础表结构如果在前期设计错了后期功能都会跟着跑偏。无人超市系统我建议至少设计以下这些表表名用途关键字段user用户/管理员id,username,password,role,create_timecategory商品分类id,name,sort_order,statusproduct商品信息id,category_id,name,price,stock,image,statuscart购物车id,user_id,product_id,quantityorder订单主表id,order_no,user_id,total_amount,status,pay_time,create_timeorder_item订单明细表id,order_id,product_id,product_name,price,quantitystock_log库存变动日志id,product_id,change_amount,type,create_time需要注意几个设计细节第一密码字段不要存明文。用BCryptPasswordEncoder加密后存储长度设为60。就算毕设项目不要求高安全性这也是数据库设计的基本素养答辩时能加分。第二价格字段用DECIMAL(10,2)不要用FLOAT或DOUBLE。浮点数在Java和数据库精度对比时会有奇怪的问题比如0.1加0.2不等于0.3商品定价这种敏感数据必须保证精度。第三订单号需要唯一标识。我使用“日期随机数/自增ID”拼接的方式生成比如20250607153000123456确保并发环境下订单号不重复。第四order_item里冗余了product_name和price字段。为什么这么做因为商品名称和价格后续可能会改如果我们只存product_id等用户重新查看历史订单时会发现商品名对不上。把下单那一刻的商品快照冗余进订单明细是商业系统的标准做法。3.2 库存扣减与订单生成的顺序问题无人超市里最容易出问题的是“库存扣减”和“订单生成”这一步。如果先扣库存再生成订单订单生成失败时库存就丢了如果先生成订单再扣库存扣减失败时用户会有一张无效订单。正确做法是在同一个数据库事务里先插入订单主表和订单明细表然后在库存充足的前提下扣减库存最后提交事务。过程中任何一个环节报错通过Transactional注解让整个事务回滚保证数据的原子性。这部分对应代码如下Transactional public Long createOrder(OrderCreateVO vo) { // 1. 生成订单号并写入订单主表 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(vo.getUserId()); order.setTotalAmount(calculateTotal(vo.getItems())); order.setStatus(0); // 0-待支付, 1-已支付, 2-已完成, 3-已取消 orderMapper.insert(order); // 2. 写入订单明细 for (OrderItemVO item : vo.getItems()) { orderItemMapper.insert(...); } // 3. 扣减库存库存不足则抛异常触发回滚 for (OrderItemVO item : vo.getItems()) { int rows productMapper.deductStock(item.getProductId(), item.getQuantity()); if (rows 0) { throw new RuntimeException(商品库存不足); } } // 4. 记录库存变动日志 stockLogMapper.insert(...); return order.getId(); }这里使用了productMapper.deductStock(id, quantity)的SQL进行条件更新核心写法是UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}通过stock quantity这个条件来保证不会扣成负数。UPDATE影响行数为0就说明库存不足抛出异常回滚整个事务。这种“乐观扣减事务回滚”的方式足够应对毕设级别的并发场景。3.3 SQL脚本文件里容易被忽略的细节项目交付时附带的supermarket.sql脚本不只是建表语句这么简单。一份“老师体验良好”的SQL脚本至少应该包含三部分内容第一部分是建库建表语句统一使用UTF-8字符集CREATE DATABASE IF NOT EXISTS supermarket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE supermarket; CREATE TABLE user ( id int(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(100) NOT NULL, role tinyint(2) NOT NULL DEFAULT 1 COMMENT 1-顾客 2-管理员, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4;第二部分是初始化数据也就是管理员和测试商品数据。防止评审老师拿到项目后还要自己手动录入商品才能演示这会直接拉低印象分。我一般会准备10条左右的商品记录覆盖三个分类保证页面打开就有数据可看。第三部分是演示用的说明注释用--注释写明“管理员账号admin / 123456顾客测试账号user / 123456”这样拿到源码的人第一分钟就能跑起来。这里有一个特别容易踩的坑MySQL 5.7和8.0的认证插件不一致。如果你本地是MySQL 8.0生成的SQL用了caching_sha2_password插件拿到MySQL 5.7导入时会报认证失败。稳妥的办法是创建用户时明确指定mysql_native_password或者在application.yml里配置useSSLfalseserverTimezoneAsia/Shanghai并确认数据库连接串没有多余的空格。关于时区的设置也要注意不配置serverTimezone的话JDBC连接可能直接报时区错误。4. 后端接口实现与接口文档——从登录鉴权到无人结算闭环4.1 接口文档到底怎么写才有用很多同学交接口文档就是给后端每个Controller截个图或者导出一份Swagger的JSON老师看了毫无头绪。真正的接口文档核心是让人看完能够独立调用这个系统。我推荐的接口文档结构是这样的接口文档 ├── 1. 通用说明 │ ├── 1.1 BaseURLhttp://localhost:8080/api │ ├── 1.2 统一返回结构code, message, data │ └── 1.3 鉴权方式请求头Header携带 token ├── 2. 用户模块 │ ├── 2.1 用户注册接口 POST /api/user/register │ ├── 2.2 用户登录接口 POST /api/user/login │ └── 2.3 获取用户信息接口 GET /api/user/info ├── 3. 商品模块 │ ├── 3.1 分页查询商品列表 GET /api/product/page │ ├── 3.2 查询商品详情 GET /api/product/{id} │ └── 3.3 管理员新增商品 POST /api/admin/product ├── 4. 购物车模块 │ ├── 4.1 加入购物车 POST /api/cart/add │ ├── 4.2 查看购物车 GET /api/cart/list │ └── 4.3 修改购物车数量 PUT /api/cart/update └── 5. 订单模块 ├── 5.1 创建订单 POST /api/order/create ├── 5.2 模拟支付 POST /api/order/pay/{orderId} └── 5.3 查询订单列表 GET /api/order/list每个接口下需要写明请求参数字段名、类型、是否必填、说明、响应示例、可能出现的错误码。统一返回结构建议写成{ code: 200, message: 操作成功, data: {} }其中code200表示成功401表示未登录或token失效500表示业务异常。前端拿到返回后只看code判断是否成功不再依赖HTTP状态码。这里建议全项目用同一个Result类避免每个接口返回结构不一致。4.2 核心接口串联一次结算请求会经过哪些接口无人超市的结算链路是整个系统里最能体现设计能力的部分。用户在前端点击“去结算”按钮后前端依次调用的接口是这样的第一步校验登录状态调用GET /api/user/info携带请求头token: xxx后端从JWT解析出用户ID。这一步失败会直接跳回登录页。第二步查询购物车调用GET /api/cart/list返回当前用户的购物车明细列表包括商品ID、数量、价格前端展示确认信息。第三步提交结算调用POST /api/order/create请求体包含用户ID和购物车里被勾选商品的明细。后端在事务中执行“写订单主表 → 写订单明细 → 扣库存 → 记库存日志”返回订单ID和订单号。第四步模拟支付调用POST /api/order/pay/{orderId}把订单状态从0-待支付改成1-已支付。这一步在真实系统中会对接支付网关毕设里直接调用一个模拟接口即可。第五步查询订单详情调用GET /api/order/detail/{orderId}前端展示“支付成功”页面和订单信息同时清空本地购物车状态。这个串联流程在接口文档里如果只用文字描述还有点抽象更直观的做法是基于一个统计表格步骤接口作用成功后状态1GET /api/user/info鉴权并获取用户ID进入结算页2GET /api/cart/list加载购物车数据显示结算清单3POST /api/order/create创建订单并扣库存生成待支付订单4POST /api/order/pay/{id}模拟支付成功订单已支付5GET /api/order/detail/{id}加载订单详情支付完成页4.3 JWT鉴权与权限控制的实际配置无人超市系统有顾客和管理员两种角色后台管理接口不能随便让顾客调用。工程里我采用了JWTJSON Web Token来做无状态鉴权。依赖直接引dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependencyJWT的逻辑是用户登录成功后后端用密钥签发一个包含用户ID和角色信息的token前端把token存到localStorage每次请求通过Axios拦截器自动携带。后端写一个拦截器在请求进入Controller之前检查tokenpublic class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(token); if (token null || !JwtUtil.validate(token)) { response.setStatus(401); return false; } // 把用户ID放到request中后面Controller可以直接用 Integer userId JwtUtil.parseToken(token); request.setAttribute(userId, userId); return true; } }注册拦截器时还要把“不需要登录”的接口排除掉比如注册登录、商品列表、商品详情。用addPathPatterns(/api/**)拦截所有接口再用excludePathPatterns白名单放行registry.addInterceptor(new JwtInterceptor()) .addPathPatterns(/api/**) .excludePathPatterns( /api/user/login, /api/user/register, /api/product/page, /api/product/detail/** );角色的权限控制我直接在Service层判断比如管理员添加商品的服务方法里验证request.getAttribute(role)是否等于管理员值不通过则抛业务异常。毕业设计用这种方式完全够不需要引入Spring Security这种重型框架因为Spring Security的SecurityContext和过滤链对初学者来说是个巨大的理解成本。5. 前端Vue页面实现——浏览、加购、结算、后台管理的交互闭环5.1 前端工程初始化与路由设计Vue前端我推荐用Vite来搭建工程命令行执行npm create vitelatest frontend -- --template vue创建完成后需要安装路由和状态管理依赖npm install vue-router4 pinia axios element-plus这个组合是Vue 3生态下的主流方案路由用createRouter状态管理用defineStoreUI组件库用Element Plus。如果你用的是Vue 2那对应的是vue-router3和vuex写法稍有差异别混用。路由设计上建议把页面按角色分组const routes [ { path: /login, component: LoginView }, { path: /, component: Layout, children: [ { path: , component: HomeView }, // 商品浏览首页 { path: cart, component: CartView }, // 购物车 { path: order, component: OrderListView }, // 我的订单 { path: order/:id, component: OrderDetailView }, { path: admin, component: AdminView, meta: { role: 2 } } // 管理后台 ]} ];路由守卫也很重要未登录用户访问需要鉴权的路由时直接重定向到登录页router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.path /login) { next(); } else if (!token) { next(/login); } else { next(); } });这里有一个实测心得不要把用户信息全部存到localStorage涉及角色的判断建议存JSON.stringify后的对象但token单独存一个key因为Axios拦截器取token的频率远高于用户信息。5.2 Axios怎么封装才不用到处写重复代码如果每个页面都直接import axios from axios然后写一遍请求配置项目代码会非常冗余而且统一改接口地址时你会想哭。我强烈建议建一个src/api/request.js做统一封装import axios from axios; import { ElMessage } from element-plus; const request axios.create({ baseURL: /api, timeout: 10000 }); // 请求拦截器自动携带token request.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[token] token; } return config; }); // 响应拦截器统一处理错误码 request.interceptors.response.use( response { const res response.data; if (res.code 200) { return res.data; } else if (res.code 401) { localStorage.removeItem(token); window.location.href /login; } else { ElMessage.error(res.message); return Promise.reject(new Error(res.message)); } }, error { ElMessage.error(网络请求失败); return Promise.reject(error); } ); export default request;注意baseURL: /api这里留了个伏笔。开发环境通过Vite代理转发生产环境通过后端Nginx或SpringBoot的路径映射转发前端代码不用改。对应的Vite代理配置// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } });每个业务模块再建一个独立的API文件比如src/api/product.jsimport request from ./request; export function getProductList(params) { return request({ url: /product/page, method: get, params }); } export function getProductDetail(id) { return request({ url: /product/detail/${id}, method: get }); }这样页面里调用就非常清爽import { getProductList } from /api/product数据加载逻辑全被隔离在API层页面只管渲染。5.3 从商城页到结算页的完整状态流转无人超市这个场景里前端最核心的状态管理是购物车数据。用户从商品列表点击“加入购物车”此时前端需要做两件事调用后端接口把购物车数据入库同时在Pinia里更新本地的购物车数量让导航栏的购物车角标立即变化。我维护了一个cartStoreexport const useCartStore defineStore(cart, { state: () ({ cartCount: 0, cartList: [] }), actions: { async fetchCart() { const res await getCartList(); this.cartList res; this.cartCount res.reduce((sum, item) sum item.quantity, 0); }, async addCart(productId, quantity) { await addCartItem({ productId, quantity }); await this.fetchCart(); // 重新拉服务器数据保证一致性 } } });首页商品卡片组件里addCart完成后调用cartStore.fetchCart()这样无论用户在哪个页面加入购物车导航栏的角标都能同步。这里有一个我之前踩过的坑如果只在前端本地累加数量而不重新拉取用户连续快速点击两次“加入购物车”第二次点击拿到的本地库存可能还是旧值导致提示库存不足但购物车里数量加多了。重新拉取服务器数据虽然多一次请求但换来的数据一致性对无人超市这种场景更值得。结算页的流程是展示cartList用户勾选要结算的商品点击“去结算”按钮。前端调用创建订单接口后拿到订单ID再调用模拟支付接口成功之后跳转订单详情页同时清空购物车并重新拉取cartList。这样整个“购物—结算—支付—完成”的闭环在前端就完整地串起来了。6. 部署与联调——Vue打包进SpringBoot的两种方式6.1 本地开发环境的联调配置前后端分离开发时最让人头痛的就是跨域问题。浏览器默认不允许一个域名下的页面去请求另一个域名的接口所以后端必须允许跨域。我通常在SpringBoot里写一个全局配置类Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedHeader(*); config.addAllowedMethod(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }但这种方法有两个隐患一是addAllowedOriginPattern(*)不能和setAllowCredentials(true)同时使用二是生产环境放开所有跨域有安全风险。所以我推荐的开发联调方式是前端不用真实跨域而是通过Vite的代理把请求转发到后端这样浏览器看到的始终是同源请求后端甚至不需要配置跨域。前端启动npm run dev后访问的是http://localhost:5173请求/api/user/login时Vite代理自动转发到http://localhost:8080/api/user/login前端代码里根本不需要写后端的完整地址。这种方式是本地联调的首选干净又安全。6.2 生产环境打包部署的实操步骤生产部署时第一步是构建前端静态资源npm run build构建完成后frontend/dist目录下会生成index.html、assets等静态文件。你直接把dist目录下的内容复制到后端工程的src/main/resources/static目录下然后重新启动SpringBoot。由于SpringBoot默认会扫描classpath:/static/作为静态资源目录浏览器访问http://localhost:8080时SpringBoot会把index.html返回给前端这个方案适合演示和交作业。但这里有一个必须处理的问题——前端路由刷新404。Vue Router如果开启的是history模式刷新/order/123这个路径时后端没有/order/123这个Controller会直接返回404。解决方法有两个第一种Vue路由使用默认的hash模式URL里会多一个#例如http://localhost:8080/#/order/123这种模式刷新不会404因为#后面的内容不会发送到服务器。做毕设完全可以用这个模式稳定性最高。第二种如果坚持用history模式后端需要额外配置一个Controller把所有非API和静态资源的路径转发到index.html。SpringBoot里可以写Controller public class ForwardController { RequestMapping(value {/, /login, /order/**, /admin/**}) public String forward() { return forward:/index.html; } }我个人建议毕设直接用hash模式省心省力。你要是想在答辩时多一个技术亮点可以提“我了解history模式和hash模式的区别毕设用了hash模式避免刷新404”这就够了。6.3 部署后静态资源与API路径冲突问题把前端打包放进SpringBoot后最容易出现一个诡异的问题接口请求路径和静态资源路径冲突。比如你后端的接口是GET /api/product/page前端构建产物里有个文件路径也可能叫/api开头的某个静态目录这样SpringBoot在处理请求时可能把这个请求当成静态资源请求导致接口返回的不是JSON而是文件内容。解决方法是严格约定所有后端接口统一放在/api前缀下所有前端静态资源路径由构建工具自动生成在/assets目录这样两者互不干扰。如果Vue页面里还有图标、图片等静态文件也统一放在public目录下构建时用/assets前缀引用。另外有一个部署细节SpringBoot打成的JAR包如果直接java -jar运行内置Tomcat监听8080端口。如果服务器上8080已经被占用需要在application.yml里改端口server: port: 8081启动后再用http://localhost:8081访问前端请求还是走同一个端口不用改前端代码。这里提醒一下改了端口后Vite代理的target也要一起改否则开发环境又联调不上了。7. 踩坑记录——毕设开发中最容易遇到的10类问题7.1 前后端联调期的典型问题联调阶段最常遇到的第一个问题是跨域报错浏览器控制台报CORS policy。我们前面用了Vite代理方案这里测下来很稳定。如果你没走代理而是直连后端地址那必须检查后端是否配置了CorsFilter另外注意OPTIONS预检请求也需要能被后端处理。第二个问题是JSON序列化循环引用。如果Order实体里有一个ListOrderItem属性OrderItem里又有一个Order属性直接返回这个对象时Jackson序列化会报 “Could not write JSON: Infinite recursion” 错误。解决办法是给实体类加上JsonIgnoreProperties或在字段上用JsonIgnore更规范的是一开始就设计VO类不让实体类直接暴露给前端。这个问题我当初排查了整整一个下午最后发现就是两个双向关联字段导致的无限递归。第三个问题是日期格式。后端返回的LocalDateTime默认序列化成2025-06-07T15:30:00中间有个T前端显示不友好。统一处理方式是配置Jacksonspring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这样接口返回的时间就变成了2025-06-07 15:30:00。7.2 数据库与SQL执行期的典型问题SQL脚本导入报错最常见的原因是字符集不一致。Windows上直接用记事本打开SQL文件另存为UTF-8可能带上BOM头MySQL导入时会报第一个SQL语句错误。建议使用Navicat、DataGrip这类专业工具直接运行SQL脚本不要用命令行去碰文件编码问题。第二个常见问题是外键约束导致删除顺序不对。你的表设计里如果order_item引用了order表的主键那删除订单时必须先删除order_item再删除order。很多同学手动测试时会先删主表结果被外键卡住。毕设项目里如果对数据一致性要求没那么高我建议可以不加物理外键而是在应用层维护关联关系。这种方式在互联网大厂也很常用因为外键在高并发场景下会影响写入性能而且删除数据非常麻烦。第三类是分页查询的total问题。用MyBatis写分页时如果你用PageHelper插件需要注意它和自定义SQL的兼容性。尤其是包含子查询或者多表连接时PageHelper自动生成的count语句可能会查错表。建议所有分页查询都写一个简单的count查询别完全依赖PageHelper的自动count。7.3 接口文档与实际代码不一致的问题这个问题的根源是开发时不断改字段文档却没有同步。我的习惯是先把接口文档写成一个接口清单表格每写完一个接口就对照清单打个勾接口参数变了立刻更新文档。文档不是最后补的而是从始至终和代码同步维护的。实际操作时还可以用一个轻量方案在SpringBoot里集成springdoc-openapiSwagger 3自动扫描Controller生成接口文档。启动项目后访问http://localhost:8080/swagger-ui.html就能看到在线可测试的接口文档这样虽然看着“偷懒”但接口和代码完全由同一个来源生成绝对不会出现“文档写着name参数实际代码用username”这种问题。最后再分享一个毕设答辩时的经验如果老师打开你的接口文档发现响应格式是统一的{code, message, data}前端页面又都是通过message展示用户提示这本身就是一套完整的前后端协作约定。你可以在答辩时讲“为了保证前后端团队高效协作我们约定了统一的返回结构和错误码规范”这句话能让答辩老师觉得你不是在做一个课程作业而是有真实的工程意识。做毕设不追求炫技把每个环节做扎实、讲清楚这个项目就成功了。