2026/10/14 6:59:23

基于Spring Boot和Vue的蛋糕店管理系统毕设实战解析

基于Spring Boot和Vue的蛋糕店管理系统毕设实战解析 1. 项目概述与选题价值1.1 为什么“蛋糕店管理系统”是典型的毕设优选题拿到“基于Spring Boot Vue的蛋糕店管理系统”这个题目时我第一反应是——这题选得聪明。蛋糕店管理系统这个业务场景看起来是个“小生意”但它背后覆盖的技术链条一点都不浅。用户端有商品浏览、下单、支付、订单跟踪管理端有商品管理、分类管理、订单处理、会员管理、数据统计这几乎就是一个标准电商系统的“最小可行版本”。很多同学做毕设时会纠结到底是做一个“学生管理系统”还是“图书借阅系统”说实话这类系统太常见了答辩老师看一眼题目就知道你要做什么很难做出差异化。但蛋糕店管理系统有它的特殊性业务实体完整商品、库存、订单、用户、会员、流程闭环从浏览到下单到配送/自提、数据展示有亮点销售统计、月度报表、热销榜单这些天然适合做前后端分离架构的功能展示和数据可视化。更重要的是Spring Boot Vue这套组合在毕业设计里几乎是最快出成果、也最能体现“全栈能力”的方案。Java后端有成熟的生态MyBatis-Plus让数据库操作变得非常直接Vue前端配合Element PlusUI组件开箱即用甚至不需要自己去手写弹窗、表格、表单校验。对需要兼顾论文、答辩、实习找工作的毕业生来说这是一个性价比极高的选型。从指导过的几届毕设来看做这类系统的同学大多能顺利通过答辩而且代码量、技术深度、演示效果都容易达标。前提是——你得真正理解这个项目每一行代码在做什么而不是把模板源码跑起来就不管了。这篇博客就从需求分析、技术落地、数据库设计、前后端实现、部署避坑这几个维度把这个项目彻底讲透。1.2 这套技术栈解决了什么问题在开始动手之前我想先帮大家建立一个框架性的认知——Spring Boot和Vue在整套系统里分别扮演什么角色。后端Spring Boot负责的是“数据业务层”的活。客户在页面上点击“加入购物车”前端只是把这个动作翻译成一个HTTP请求真正完成“验证库存够不够、把商品加入用户的购物车表、重新计算总价”这些工作的是后端的Controller、Service、Mapper这一条链路。Spring Boot帮开发者省掉了大量的配置工作——内嵌Tomcat、自动配置、起步依赖一条spring-boot-starter-web就能跑起来一个web项目这在十年前需要写一整套XML配置才能做到。前端Vue负责的是“用户交互层”的活。它的核心是组件化和响应式数据绑定。页面上的商品卡片、购物车列表、订单进度条每一个环节都是一个独立的Vue组件用户点击“加购”后data里的cartList数组一旦变化页面上所有依赖它的DOM会自动更新这就是响应式带来的开发效率提升不需要像原生JavaScript那样手动去操作DOM。前后端之间通过JSON格式的接口数据通信。前端发请求、接数据、渲染页面后端收请求、查数据库、返回结果。所以你会看到项目中到处都是axios.get(/api/product/list)这种调用而Spring Boot端的Controller则用RestController和RequestMapping来承接这些请求。如果只是运行别人写好的源码你永远只能看到“它跑起来了”看不到“它为什么能跑起来”。下面我会带着大家先从整体设计入手再一步步拆解后端核心代码、前端页面实现最后把联调和部署环节可能踩的坑全部铺开。2. 整体设计与业务拆解2.1 三类角色的权限边界设计蛋糕店管理系统虽然业务不算复杂但依然涉及三类角色顾客前台用户、店长管理员、系统维护者超级管理员。在毕设项目里一般不会把“系统维护者”单独做成一个角色而是通过管理员的接口多写几个管理菜单来体现。换句话说系统的核心角色是两个普通用户和管理员。普通用户负责前台店面的浏览操作管理员负责后台的运营管理操作。从业务逻辑上我们可以把功能模块整理成下面这张表角色功能模块核心操作普通用户商品浏览查看分类、商品列表、商品详情、搜索、筛选普通用户购物车加购、修改数量、删除、结算普通用户订单管理提交订单、取消订单、查看订单状态、确认收货普通用户个人中心注册登录、个人信息修改、收货地址管理管理员商品管理商品增删改查、上下架、库存调整、图片上传管理员分类管理分类维护、排序管理员订单管理查看全部订单、发货、完成订单、订单统计管理员用户管理查看用户列表、禁用账号管理员数据统计销售金额统计、热销商品排行、订单趋势图这里有一个容易被忽略的设计点前后台的数据权限必须分离。前台用户不应该能通过改接口URL的方式访问到后端的订单管理接口。这个分离要从前端路由守卫和后端拦截器两层做缺一不可。很多毕设代码只做了前端隐藏菜单后端所有接口都裸奔答辩时老师直接打开浏览器控制台调用一下接口就露馅了。2.2 数据库表结构的规划思路蛋糕店管理系统的数据库通常至少包含六张核心表。我直接给出一套我常用且逻辑清晰的设计方案大家可以在此基础上根据业务调整。**用户表**字段包含id、username、passwordBCrypt加密后的密文、nickname、phone、avatar、status0正常1禁用、create_time。密码一定不要存明文这是技术底线问题答辩时如果老师看到明文密码印象分直接掉一档。**商品分类表**字段包含id、name、sort排序权重、status。分类和商品是一对多的关系——一个分类下可以有多个商品一台蛋糕机可以做不同尺寸的蛋糕。前台展示分类时一般只查status1的分类。**商品表**字段包含id、category_id、name、subtitle副标题用于列表页一行字描述、main_image、detail富文本详情用于商品详情页、price用BigDecimal存不要用double/float避免精度问题、stock、status1上架0下架、sales模拟销量用于热销榜、create_time。**购物车表**字段包含id、user_id、product_id、quantity、checked是否选中结算、create_time、update_time。这里唯一约束建议是user_idproduct_id保证同一用户同一商品只会有购物车里的一行记录用户再加购时是更新数量而不是新增一条脏数据。**订单表**字段包含id、order_no订单编号建议用时间戳随机数生成、user_id、total_price、status0待付款1已付款待发货2已发货3已完成4已取消、receiver_name、receiver_phone、receiver_address、create_time、pay_time、deliver_time、finish_time。**订单明细表**字段包含id、order_id、product_id、product_name快照字段防止商品改名后订单历史记录乱掉、product_image、price下单时单价快照、quantity、total_price。这里要特别提醒一个设计细节订单里的商品名称和价格必须冗余存储不能通过product_id去关联查商品表。因为商品可能下架、改名、调价但用户买了就是买了订单记录必须保持历史事实。这类经验在真实企业开发中是硬性要求在毕设里写出来会明显体现出你的工程素养。除了这六张核心表视业务扩展程度还可以加一张轮播图表和一张系统公告表做成后台可维护的内容前台首页动态加载。这两张表虽然简单但能让你在前端页面做得更丰满也是答辩时“这个功能我额外扩展了”的加分项。2.3 前后端分离架构的通信链路在这个项目里前后端分离的具体表现是前端项目和后端项目是两个独立的工程分别跑在不同端口上。后端默认端口是8080前端开发环境默认端口是5173Vite或8081Vue CLI。前端通过代理或直接请求的方式访问后端接口。开发模式下最推荐的做法是在Vite或Vue CLI的配置文件里配置开发服务器代理把/api前缀的请求转发到http://localhost:8080。这样前端代码里只需要写/api/product/list不需要写完整的http://localhost:8080/api/product/list联调时就不会被跨域问题卡住。生产模式下有两种常见部署方案。第一种是前端构建后的dist文件夹直接丢到Nginx里Nginx配置location /api反向代理到后端服务。第二种是后端Spring Boot的src/main/resources/static目录里直接放前端构建产物用一个jar包把前后端都跑起来。我个人更推荐第一种因为Nginx反向代理是实际工作中每天都要打交道的技能毕设里用上能在系统部署文档里写一笔“采用Nginx处理静态资源并反代后端接口”答辩老师会觉得你有真实工程经验。第二种适合纯为了简单、不想装Nginx的环境。3. 后端实现核心模块剖析3.1 Spring Boot项目骨架搭建创建Spring Boot项目时推荐使用Spring Initializr快速生成或者直接用IDEA自带的脚手架。需要勾选的依赖包括Spring Web提供MVC框架和嵌入式服务器MySQL Driver驱动MySQL数据库MyBatis FrameworkMyBatis持久层框架Lombok简化实体类代码Spring Validation参数校验Java后端开发非常实用的组件Spring Security或JWT相关依赖用于登录鉴权有一点我要特别强调不要用Spring Security的完整实现而是用JWT 拦截器的方式实现登录鉴权。原因很简单——Spring Security虽然强大但对于毕设项目来说配置过于庞杂CSRF、Session管理、过滤器链这些概念会让很多同学花大量时间在配置上而不是业务逻辑上。JWT方案更轻量核心代码只要几十行而且面试时聊JWT比聊Spring Security配置更容易展示你理解“无状态鉴权”这个核心思想。项目骨架结构建议这样组织src/main/java/com/example/cake/ ├── config/ │ ├── WebMvcConfig.java // 注册拦截器、配置跨域 │ └── JwtInterceptor.java // JWT校验拦截器 ├── controller/ │ ├── UserController.java // 前台用户接口 │ ├── AdminProductController.java // 后台商品管理接口 │ ├── CartController.java │ ├── OrderController.java │ └── AdminStatisticsController.java // 数据统计接口 ├── service/ │ ├── UserService.java │ ├── UserServiceImpl.java │ ├── ProductService.java │ ├── CartService.java │ └── OrderService.java ├── mapper/ │ ├── UserMapper.java │ ├── ProductMapper.java │ ├── CartMapper.java │ └── OrderMapper.java ├── entity/ │ ├── User.java │ ├── Product.java │ ├── CartItem.java │ ├── Order.java │ └── OrderItem.java ├── vo/ // 视图对象专门给前端返回的封装类 ├── common/ // 统一返回结果、异常处理 └── util/ // JWT工具类、订单号生成工具这种分层结构的好处是各层职责单一、路径清晰写论文里的“系统架构图”和“模块设计”章节时可以直接对应不需要额外编造设计。Java后端领域的分层思想Controller层只做参数接收和结果返回Service层专注业务逻辑Mapper层负责数据库交互这是所有企业级Spring项目的底层共识。3.2 用户登录与JWT鉴权的落地登录注册是几乎所有系统都要有的模块。这块要做到既正确又优雅核心步骤如下。用户注册时后端接收用户名、密码等参数。密码经过BCryptPasswordEncoder加密后再存入数据库。BCrypt的一个好处是每次加密的盐值都不同即使两个用户密码相同密文也不一样安全性比MD5加盐高得多。校验时用matches(明文密码, 数据库密文)方法来判断。用户登录成功后后端生成一个JWT令牌返回给前端。JWT的结构是三段式Header加密算法声明、Payload用户信息、过期时间、Signature签名。我这里给一个常用的JWT工具类核心代码基于jjwt库0.9.1这个版本很稳定public class JwtUtil { // 实际项目中这一行应从配置文件读取示例中简化 private static final String SECRET_KEY your-secret-key-for-jwt-signature; public static String generateToken(Integer userId, String username, String role) { return Jwts.builder() .setSubject(username) .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() 24 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, SECRET_KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET_KEY) .parseClaimsJws(token) .getBody(); } }这套代码的核心价值在于登录接口不需要维护Session状态服务器重启后令牌依然有效只要在过期时间内。前端每次请求时在HTTP请求头里带上Authorization: Bearer token后端拦截器从请求头中取出令牌并校验签名和过期时间。拦截器配置是鉴权模块的核心。我写一个简单的拦截器用来校验需要登录的接口Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { try { Claims claims JwtUtil.parseToken(token.substring(7)); request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { // 令牌非法或过期 } } response.setStatus(401); return false; } }在WebMvcConfig里注册拦截器时要设置addPathPatterns和excludePathPatterns。放行的路径包括登录接口、注册接口、商品列表查询接口、商品详情查询接口。拦截的路径是所有/api/cart/**、/api/order/**、/api/user/**。管理员的接口和用户接口之间在做角色校验时从token里解析出role字段再判断因为管理员的登录接口是一个独立路径/api/admin/login返回的token里可以携带role: admin标识。这块逻辑写完之后我在实际运行中遇到最多的问题是前端axios拦截器在请求返回401时没有做统一跳转导致用户登录过期后页面还是停留在原页面但按钮点了没反应。这个问题我会放到后面“常见问题”部分单独讲。3.3 商品管理与图片上传的实现细节商品管理是管理员端最核心的模块这里的核心价值不在“增删改查”本身而在两个细节图片上传和富文本内容处理。图片上传的经典方案是前端用Element Plus的Upload组件选择图片后前端调用后端的/api/upload接口后端将文件保存到服务器的某个静态资源目录然后返回可访问的URL路径。这里我建议在后端配置文件里设置一个自定义路径而不是把图片存到Spring Boot项目的classpath里否则每次重新打包时图片会丢。在application.yml里可以这样配置spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB file: upload-dir: /usr/local/upload/cake然后在配置类里映射静态资源路径Configuration public class WebMvcConfig implements WebMvcConfigurer { Value(${file.upload-dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceHandler(file: uploadDir /); } }这样前端上传成功后拿到的路径是类似/upload/20240815/xxxxxx.jpg这样的相对路径然后把这个路径存到商品的main_image字段。图片展示时前端只需要拼接服务器地址开发时是http://localhost:8080生产时是Nginx配置的域名或IP即可。在图片访问上还有个容易被忽视的问题——后端接口返回的图片路径是相对路径前端页面部署在另一个端口时直接img :srcproduct.mainImage会请求到前端服务器去导致图片404。正确做法是前端用process.env.VITE_API_BASEURL或配置文件里定义全局的baseUrl然后拼接完整URL。这个小坑在联调时几乎必踩提前说清楚能帮你少折腾一晚上。3.4 下单流程与事务控制的思路下单是整个系统里最值得细心设计的部分因为它牵涉到多张表的联动更新。一个完整的下单流程是这样的第一步前端提交结算请求带上购物车中勾选商品的ID列表和收货信息。后端先根据userId查出购物车记录同时关联查询商品信息。第二步校验商品状态是否上架、库存是否足够。如果有商品库存不足直接抛出业务异常整个下单流程回滚。第三步生成订单主表记录状态为待付款。生成订单明细表记录把商品名称、单价、图片做快照。第四步扣除库存。UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}——这个SQL里的AND stock #{quantity}条件判断很重要它利用数据库行锁防止并发下的超卖问题比先查询再判断更安全、更高效。第五步清空购物车中已下单的商品。这五个步骤必须放在同一个事务里。Spring Boot中使用Transactional注解即可。在实际项目中还有一种常见做法是“下单即支付”和“下单后支付”两种模式。毕设系统一般选择“下单即创建待付款订单在模拟支付页面点击确认支付后把订单状态更新为已付款”。这个流程的好处是多了一个状态流转前端可以做一个“我的订单”页面展示各个状态后台可以按状态筛选订单页面层级更丰富演示效果也更好。事务这块是答辩老师特别喜欢问的考点——大概率会问“如果库存扣减后清空购物车失败怎么办”。回答的思路就是同事务内的方法异常统一回滚任何一步失败前面的操作全部撤销。从这个角度切入回答比背一段概念要有说服力得多。4. 前端页面与交互实现4.1 前端项目结构与路由设计前端使用Vue 3组合式API Vite Element Plus Pinia Vue Router这套现代组合。Vite相比Vue CLI更快而且对Vue 3生态的支持更彻底也贴近行业当前的主流使用习惯。前端的目录结构建议如下src/ ├── api/ // 封装所有接口请求 │ ├── product.js │ ├── cart.js │ ├── order.js │ └── user.js ├── assets/ ├── components/ // 通用组件 │ ├── NavBar.vue │ ├── ProductCard.vue │ └── Pagination.vue ├── router/ // 路由配置 │ └── index.js ├── store/ // Pinia状态管理 │ ├── user.js │ └── cart.js ├── views/ │ ├── home/Home.vue │ ├── product/ProductList.vue │ ├── product/ProductDetail.vue │ ├── cart/Cart.vue │ ├── order/OrderConfirm.vue │ ├── order/OrderList.vue │ └── user/Login.vue │ └── admin/AdminLayout.vue │ └── admin/ManageProduct.vue │ └── admin/ManageOrder.vue │ └── admin/Dashboard.vue └── main.js路由设计上要明确区分前台用户端和后台管理层。前台路径不带前缀例如/是首页/product/:id是商品详情/cart是购物车。后台路径统一带/admin前缀并且嵌套在一个AdminLayout布局组件内这样左侧菜单栏和顶部导航只渲染一次切换子页面时只替换右侧内容区域。配套的路由守卫是必须的。管理员后台的所有路由需要有meta: { requiresAdmin: true }标记在前端路由守卫里判断当前登录用户的角色router.beforeEach((to, from, next) { const userStore useUserStore(); if (to.meta.requiresAdmin (!userStore.isLogin || userStore.role ! admin)) { next(/login); } else { next(); } });这里虽然前端做了路由守卫但再次强调后端接口必须也做权限校验否则只是“看起来安全”接口照样能被外部调用。4.2 axios请求封装与拦截器配置前端项目里建议统一封装一个axios实例而不是每个组件里单独import axios然后发请求。封装的好处有三个统一前缀管理、统一错误处理、统一附加token。src/api/request.js的核心代码大致如下import axios from axios; import { ElMessage } from element-plus; import router from /router; import { useUserStore } from /store/user; const request axios.create({ baseURL: /api, timeout: 10000 }); // 请求拦截器 request.interceptors.request.use(config { const userStore useUserStore(); if (userStore.token) { config.headers[Authorization] Bearer userStore.token; } return config; }); // 响应拦截器 request.interceptors.response.use( response { const res response.data; // 后端统一返回 { code: 200, data: {}, msg: success } if (res.code ! 200) { ElMessage.error(res.msg || 请求失败); return Promise.reject(new Error(res.msg)); } return res.data; }, error { if (error.response error.response.status 401) { ElMessage.error(登录已过期请重新登录); router.push(/login); } else { ElMessage.error(网络错误请稍后重试); } return Promise.reject(error); } ); export default request;有了这层封装后具体接口文件里只写业务端点。比如src/api/product.jsimport request from ./request; export const getProductList (params) request.get(/product/list, { params }); export const getProductDetail (id) request.get(/product/detail/${id});这样封装以后后面所有页面的接口调用都保持简洁遇到接口报错、登录过期这类统一问题只需要改拦截器一处代码即可不用满项目找报错弹窗。这也是实际项目开发中必须养成的习惯——统一出口永远比散弹式调用更易维护。4.3 首页数据展示与商品列表渲染首页是这个系统对外最直观的“门面”。通常包括导航栏、轮播图、商品分类入口、热销商品推荐、新品上市模块。前端如何把这些数据组织起来展示是很多同学觉得“页面不够精致”的改进点。首页推荐的商品模块我的建议是后端写一个/api/product/recommend接口该接口内部逻辑是“按销量排序取前8个商品”。这里的SQL就可以用ORDER BY sales DESC LIMIT 8。销售数字在商品表里维护然后每次真实订单完成或模拟支付成功后自增。分类入口的前端实现很简单分类列表接口返回[{id, name, icon}]数组页面用CSS Grid布局六个方格即可。点击某个分类后跳转到商品列表页并携带分类ID参数列表页再调用/api/product/list?categoryId1pageNum1pageSize12获取对应分类的数据。商品列表页还需要支持排序排序方式一般有三种默认综合、按价格升序、按销量降序。这三种排序本质上都是后端SQL的排序子句在变-- 按价格升序 SELECT * FROM product WHERE status 1 ORDER BY price ASC; -- 按销量降序 SELECT * FROM product WHERE status 1 ORDER BY sales DESC;前端只需要根据用户选择的排序方式给后端传一个sort参数后端在分页查询里动态拼接排序条件。这块是用MyBatis-Plus的QueryWrapper.orderByDesc(sales)或者XML动态SQL都容易实现的。4.4 购物车和订单结算页的状态管理购物车的数据状态是前端里最值得用Pinia做全局状态管理的业务场景。原因是购物车图标上的数字角标几乎在每个页面都要显示如果每个页面各自调用接口获取购物车数量不仅重复而且容易数据不一致。用Pinia做一个全局的cartStore存储当前用户的购物车商品列表并提供addToCart、removeFromCart、updateQuantity、clearCart这些方法其他页面组件只需要读取store里的数据。实际下单结算页面中需要区分“购物车结算”和“直接购买”两种入口。购物车结算时前端把勾选的购物车项ID列表传给后端直接购买时前端需要临时创建一个包含单个商品的对象把商品ID和数量传给后端。后端接口可以统一设计为接收一个ListCartItemRequest参数其中CartItemRequest包含productId和quantity后端先根据用户ID查出用户信息再校验商品、生成订单。这样一套接口同时满足两个入口不需要写两个订单创建的接口。订单确认页里收货地址一般是让用户选择已有的地址或者允许临时填写新地址。毕设项目中一般不做“用户地址管理”这个独立模块直接在确认页下面放三个输入框收货人、联系电话、详细地址。虽然简单但足以支撑整个流程闭环。如果想加分可以加一张address表做地址管理的增删改查演示用户再次下单时一键选择之前的地址——这是很多高端毕设的标准加分项。4.5 后台管理端布局与数据可视化后台管理端的布局我推荐使用Element Plus的Container布局顶部有Logo和“欢迎管理员”字样左边是菜单栏商品管理、订单管理、用户管理、数据统计右侧为内容区域。数据统计页是整个管理端的演示亮点建议使用ECharts来画三个图表近30天销售额折线图、热销商品TOP10柱状图、分类销售占比饼图。这三个图表的数据来源后端可以写一个统计接口分别执行三条SQL-- 近30天每日销售额订单状态为已支付及之后的订单 SELECT DATE(pay_time) AS day, SUM(total_price) AS amount FROM orders WHERE pay_time DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY DATE(pay_time) ORDER BY day; -- 热销商品TOP10 SELECT product_id, product_name, SUM(quantity) AS total_sales FROM order_items GROUP BY product_id, product_name ORDER BY total_sales DESC LIMIT 10; -- 分类销售占比 SELECT c.name, SUM(oi.total_price) AS amount FROM order_items oi JOIN product p ON oi.product_id p.id JOIN category c ON p.category_id c.id GROUP BY c.id, c.name;后端拿到数据后按ECharts需要的数据结构整理返回前端封装成一个Statistics.vue组件在mounted里拉取数据并初始化图表。用ECharts时要注意一个特别容易坑人的点如果你的ECharts是用npm install echarts后按需引入一定不要忘了引入用到的图表类型。import * as echarts from echarts这种全量引入方式虽然包体积大但在毕设项目里最简单稳定不会遇到“图表页空白但控制台无报错”的诡异问题。5. 数据库初始化与接口联调文档5.1 建库建表脚本与初始数据填充在项目交付物里数据库文件通常会以.sql文件形式给出包含建表语句和必要的初始数据。我建议每个表和关键操作都写清楚注释这不仅是项目规范更是帮你快速回顾业务结构的索引。例如商品分类表可以先插几条常见的分类数据生日蛋糕、慕斯蛋糕、芝士蛋糕、纸杯蛋糕、饼干点心、饮品。商品表则准备15到20条样例数据覆盖几个关键场景有的商品库存充足、有的商品库存为0用来演示“暂时缺货”置灰的前端交互、有1到2个商品处于下架状态用来演示管理员上下架功能。这里还要注意Decimal类型字段的初始化写法。比如价格99.00在SQL文件里写99.00即可但是Java实体类对应字段类型要用BigDecimal否则从数据库查出来转成double之后在前端展示时可能出现99.0而不是99.00的尴尬情况。干脆从源头避免统一用BigDecimal。5.2 统一返回结果与全局异常处理前后端联调中最大的痛点之一是“返回的数据格式不统一”。有的接口返回{ code: 200, data: [] }有的接口直接返回一个数组前端处理时就会异常别扭。这个问题在毕设起步阶段就应该定好规范。我建议所有接口统一返回Result对象结构固定为code200成功500业务失败401未登录、data、msg三个字段。后端定义一个通用类Data public class ResultT { private Integer code; private T data; private String msg; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setData(data); result.setMsg(success); return result; } public static T ResultT error(String msg) { ResultT result new Result(); result.setCode(500); result.setMsg(msg); return result; } }配合RestControllerAdvice做全局异常处理Controller层代码会非常清爽。例如订单创建时库存不足Service层抛出BizException(商品库存不足)全局异常处理器捕获后自动包装成Result.error返回前端axios拦截器会读取code并弹出错误提示。这套机制把错误处理从“每个接口单独try-catch”变成统一规则大大简化了开发量。5.3 Postman / Apifox测试与Api文档生成接口写完了联调除了前端页面调用外还应该用接口测试工具把所有接口完整测一遍这一步是很多容易漏掉但极其重要的工作。我最常用的工具是Apifox原因很简单它兼容Postman的接口调试方式又能把接口文档、Mock服务、调试工具放在一个平台里方便答辩演示时快速找到某个接口。测试时重点关注这三个场景未登录请求受保护接口应返回401。已登录用户请求他人订单详情应返回“无权访问”的提示。管理员接口普通用户token请求应返回403或无权限提示。这些场景能直接检验你设计的鉴权体系是否有效。很多同学的毕设代码“表面没问题”但在权限校验上漏洞百出——普通用户的token也能调用管理员接口。这一类问题在答辩演示时一旦被老师发现技术分基本要从“优秀”降到“及格边缘”。6. 部署上线与常见问题排查6.1 本地运行与打包部署流程整个项目的部署环节我建议按下面流程操作能避免90%的环境问题后端部分修改application.yml里的数据库连接信息确认MySQL已经启动并且执行过.sql脚本。然后直接用IDEA运行主类或者执行mvn clean package打出jar包再用java -jar cake-shop-admin-0.0.1-SNAPSHOT.jar启动。前端部分先在.env文件里配置VITE_API_BASEURL为/api然后执行npm run build构建产物在dist目录。开发模式则执行npm run dev。生产部署时我的习惯是这样的Nginx作为入口监听80端口location /匹配前端dist目录下的静态文件location /api/则通过proxy_pass转发到后端服务。Nginx配置片段参考如下server { listen 80; server_name localhost; root /usr/share/nginx/html/cake; index index.html; location / { try_files $uri $uri/ /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 /upload/ { proxy_pass http://127.0.0.1:8080; } }这里try_files $uri $uri/ /index.html;是SPA单页应用路由的核心——Vue Router使用history模式时页面上的路由路径在Nginx看来可能是/product/1这种不存在的物理路径这笔配置的目的就是把这些路径请求全部转回index.html由前端路由接管。6.2 跨域问题与请求代理的排查清单前后端分离开发时跨域问题几乎每个人都会遇到。前端http://localhost:5173请求后端http://localhost:8080浏览器会拦截跨域请求。解决方案有三层按优先级排列第一Vite开发代理。在vite.config.js里配置export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } });这样前端请求/api/product/list时Vite开发服务器会自动转发到http://localhost:8080/api/product/list。这种配置只在开发模式下生效不需要后端做任何跨域开放因为什么“跨域”根本不发生——请求是同源的。第二后端开启CORS。Spring Boot中可以通过配置CorsFilter或CrossOrigin注解来允许跨域。如果是开发时前端端口经常变动用这种方式更省事Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }第三生产环境通过Nginx反向代理解决跨域。排查跨域问题时不要一上来就往后端加CORS先确认你的请求是走代理还是直连后端很多同学在开发模式下关了代理、同时后端又没开CORS自然就报跨域错误。6.3 常见报错与配置失误速查表把我在实际带毕设过程中收集到的高频报错整理成一张速查表方便大家遇到问题时对号入座现象根本原因解决方案前端请求接口返回404后端Controller的RequestMapping路径与前端请求路径不一致打开接口文档逐一比对URL路径是否完全一致上传图片后访问返回404静态资源映射路径配置不对检查addResourceHandlers中的路径和实际文件路径是否匹配登录后刷新页面登录状态丢失前端没有从本地存储恢复登录状态初始化时从localStorage读取token再调用一次getUserInfo接口控制台报跨域错误开发代理未生效或后端CORS未配置检查Vite配置是否重新启动或后端加CORS过滤器时间显示为2024-08-15T12:00:00前后端时间格式序列化不一致后端在application.yml配置spring.jackson.date-format或用JsonFormat注解部署后刷新页面404SPA路由没有回退到index.htmlNginx中加入try_files $uri $uri/ /index.html;启动时数据库连接失败MySQL未启动或连接串配置错误检查url、用户名、密码先用命令行工具试一下连接JWT过期后页面无提示axios拦截器没有做401统一处理参考上文的响应拦截器配置还有一条经验是关于数据库时区的。连接串建议写成spring: datasource: url: jdbc:mysql://localhost:3306/cake_shop?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai第一次连数据库的同学经常会因为时区参数缺失报Server time zone value错误加上这一段可以直接规避。6.4 代码提交与文档撰写的建议最后想提醒的是毕设的“非代码”部分。源码、数据库、文档这三件套中很多同学把精力全部放在代码上到了提交前才开始赶文档结果文档写得干巴巴、逻辑混乱严重影响评阅成绩。设计文档至少需要包含的内容包括需求分析含用例图、系统架构设计含架构图、功能模块图、数据库设计含ER图、表结构说明、核心接口清单、部署说明。系统架构图可以直接用Visio或者draw.io画实体关系图建议用工具从数据库里反向生成再手动调整布局。论文里的“核心功能实现”章节不要简单贴整段代码而是挑2到3个最有代表性的功能做“要点说明关键代码效果描述”的组合。比如订单事务控制的Transactional、JWT拦截器的实现、ECharts图表的配置。给每一段代码配上一两句话的解释说明“这里为什么这样设计”“这一步解决了什么问题”这样的论文才是老师愿意给高分的内容。7. 写在最后的一些实际操作经验我个人这几年看过的毕设项目里最明显的分水岭不在于项目有多宏大、技术栈有多难而在于有没有把“边界情况”处理好。同样是蛋糕店管理系统有的同学只会按演示流程走一遍换一个场景就哑火有的同学能把“商品缺货置灰”“订单超时取消”“库存扣减失败回滚”这些边角情况讲得头头是道。后者在答辩时天然占优势因为评委老师最想看到的不是你把功能做出来了而是你真正理解代码背后的约束和逻辑。一个值得玩味的经验是写代码前先用纸笔画出完整业务闭环。你从用户注册、登录、浏览商品、加购、下单、支付、商家发货、确认收货这条主链路走一遍在每一个节点上问自己“这里如果数据异常了系统应该怎么表现”。这个习惯能让你在写普通的增删改查之外自然衍生出异常处理和状态机的设计而这些恰恰是论文和答辩中的加分亮点。如果你正准备做类似的毕设项目拿到一套源码后我建议先不要急着修改代码而是按照这几个步骤来消化它先把数据库表结构看懂理清表与表之间的关系然后跑一个完整流程从前台到后台逐一观察请求与响应的对应关系再找到下单接口一行一行跟读事务和状态流转代码最后再动手去改样式、加功能。这三个步骤走完之后这套代码就已经变成你自己的东西了。做毕设这件事本质上是一次“拿着一个真实业务场景走一遍企业级开发流程”的演练。代码能不能跑是底线懂不懂设计逻辑才是上限。希望这篇拆解能帮你把这个上限再往上推一层。