☰
SpringBoot校园食堂订餐系统全套实战:从设计到答辩
2026/10/6 10:28:21 网站建设 项目流程

“米果智能食堂管理系统”——这个名字估计很多正在做Java毕设的同学都翻到过。说到底它就是一套基于Java+SpringBoot的Web版校园食堂线上订餐管理系统,覆盖了学生登录、浏览菜品、线上下单支付、食堂接单出餐、管理员统计营收的完整链路。我完整地从选题、设计、开发到答辩走了一遍,这里把项目背后真正的分析思路和技术实现细节整理出来,包括数据库怎么建、接口怎么写、前端怎么打包、答辩怎么讲,全部说透。

这篇内容适合三类人。第一类是正在纠结毕设选题和实现难度的计算机专业本专科学生,需要的是一个能完成、能讲清楚、能演示的完整系统;第二类是已经选定类似题目、但不知道从哪下手的同学,可以直接照着这里的工程结构和代码思路做;第三类是想通过一个完整JavaWeb项目来巩固SpringBoot、Redis、MyBatis、Spring Security这些实战技能的开发者。文中的代码和配置都能直接抄作业,但更重要的是搞清楚每一步背后的取舍逻辑。

1. 项目整体设计与思路拆解

1.1 校园食堂线上化到底解决什么问题

教学楼的课表和铃声把午饭时间牢牢压缩在了一个小时里。当几千名学生同时涌向食堂,排队就成了每天必然经历的过程。这种背景下,校园食堂线上订餐系统的价值就非常明确:把“人挤在窗口前点餐”变为“人提前点餐、食堂提前备餐、到店即取”,效率提升是立竿见影的。

除了效率,还有信息透明的问题。在线系统可以把今日菜品、剩余数量、价格、评价全部前置展示,学生点餐不再靠到窗口前碰运气。对食堂经营者而言,订单数据沉淀下来以后可以做非常有价值的分析:哪道菜是销量王、哪个窗口在哪个时段客流最大、菜品定价是否合理,这些都能从过去的经验判断变成数据决策。

我在需求分析阶段,还专门去本校食堂蹲了几个用餐高峰期,记了一些真实数据:高峰时段一个窗口平均排队12人,每个学生从排队到结算平均耗时4分钟以上。这些数据后来被我写进了论文的研究背景和答辩PPT的前几页,答辩老师明显对这部分更感兴趣。很多同学论文开头喜欢写“随着互联网技术的快速发展”这类套话,但最打动人的往往是来自真实场景的调查数据。系统分析不是喊口号,得让人看到你真的理解了这个场景。

1.2 三类角色与核心用例梳理

用例建模是毕业设计的标配环节,但也是最容易被糊弄过去的部分。简单画几个椭圆连几条线就完事,答辩老师一问“系统有哪些角色,每个角色能做什么”,就答不上来。

我的系统划分为三类角色,对应不同的登录入口和操作集合:

角色登录入口核心功能关键页面
学生用户学生端首页菜品浏览、加入购物车、在线下单、订单查询、菜品评价首页、菜品列表、购物车、我的订单
食堂管理员食堂管理后台菜品上架/下架、库存维护、接单出餐、每日营收统计菜品管理、订单管理、统计报表
系统管理员总后台用户管理、食堂信息维护、公告发布、全系统订单监管用户列表、食堂管理、公告管理

毕业设计的角色模型不需要太复杂,但每个用例都必须能闭环。以学生下单为例,主流程是:登录 → 浏览菜品 → 加入购物车 → 提交订单 → 模拟支付 → 查看订单状态;异常流程同样要画出来,比如购物车里的菜品被下架导致库存校验失败、提交订单时Redis中该菜品库存不足、支付超时后订单自动取消。在论文里把主流程和异常流程都讲清楚,是“你理解业务复杂性”的最直接证据。

我特别不建议把角色拆得太碎,比如再增加一个“配送员”角色、一个“财务角色”。每多一个角色,就要多一套权限逻辑、多一组测试用例、多几张数据表,代码量和文档量都会指数级上涨。毕设项目的核心标准是“功能完整、能演示、能讲清楚”,而不是“功能多到能开公司”。

2. 技术选型解析:为什么SpringBoot成为主角

2.1 技术栈全景图

这个系统最终确定的技术栈如下:

  • 后端基础框架:SpringBoot 2.7.18,基于Spring 5.3.x,稳定且资料全网最多
  • 持久层框架:MyBatis-Plus 3.5.3.2,单表CRUD零SQL,复杂查询用注解或XML自己写
  • 数据库:MySQL 8.0,字符集utf8mb4,支持中文排序和emoji字符
  • 缓存层:Redis 6.x,用于验证码、token版本、热门菜品缓存
  • 认证与鉴权:Spring Security + JWT(jjwt 0.11.5)
  • 前端方案:Vue3 + Vite + Element Plus,最终打包进SpringBoot的static目录
  • 项目管理:Maven 3.8.x,单模块工程
  • 开发环境:IntelliJ IDEA,JDK 1.8

这套技术栈的“性价比”很高。组件全部是当前企业开发的主流组合,学完之后可以直接平移到真实项目;对毕设而言,每一块又都有海量资料可以查。选择这套技术不是因为它看起来很厉害,而是因为它在“可实现性”和“技术深度”之间取了平衡。

2.2 关键取舍:为什么不是SSM,也不是微服务

很多同学觉得SSM比SpringBoot更“底层”、更能体现水平,于是纠结要不要用传统SSM框架做。实际上SpringBoot并没有重新发明一套东西,它底层依然是Spring容器的IOC和AOP机制,只是通过自动配置把原本要在web.xml、spring.xml里反复写的重复配置全部收敛了。你用SpringBoot,反而能把时间精力放在业务逻辑和系统设计上。

另一个常见的误区是一上来就上微服务。微服务架构需要有明确的业务驱动:团队拆分的需要、独立部署的需求、差异化技术选型的需求、流量峰值隔离的需求。校园食堂管理系统这种规模,单体应用在开发效率、调试便利性、部署成本上全面占优。我在论文的技术选型章节专门写了一段论证,解释为什么当前规模选择单体架构是合理的。这个论证比堆砌技术名词更能体现架构判断力。

还有一点必须提醒:版本别追新。SpringBoot 2.7.x是最稳妥的路线,网上资料多、坑少、插件兼容好。SpringBoot 3.x对Java版本有硬性要求,很多旧版本的代码生成器、插件和教程都不适配,会把大量时间花在环境问题上。毕设的底线是稳定跑通,不是版本号最新。

2.3 工程构建:Maven配置与依赖管理

Maven是JavaWeb项目的“地基工程”。我最开始从网上抄了一份pom.xml,结果因为版本冲突调了一下午。后来摸清了依赖管理的基本思路,才发现问题完全可以避免。下面是我这个项目的核心依赖配置:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <properties> <java.version>1.8</java.version> <mybatis-plus.version>3.5.3.2</mybatis-plus.version> <jjwt.version>0.11.5</jjwt.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>${jjwt.version}</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>${jjwt.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

依赖冲突的排查思路很重要:如果编译或运行时提示某个类找不到、某个方法不存在,先不要怀疑你的代码逻辑,打开IDEA的Maven窗口跑一遍mvn dependency:tree,八成是传递依赖把版本覆盖了。我做订单导出Excel功能时,就是POI依赖引入了一个旧版本的commons-collections,导致另一个模块运行时报错。这种情况不解决,哪怕源码一行不改,换台电脑也会炸。解决办法是在引入POI时排除冲突项,或者全局统一指定版本号。

3. 数据库设计与核心功能实现

3.1 核心数据表设计

数据库是业务的地基,表结构设计得差,后面所有业务代码都在给表“还债”。我最终设计了核心的8张表:用户表、食堂表、菜品表、购物车表、订单表、订单明细表、评价表、公告表。下面挑最关键的几张详细讲。

用户表的核心是角色和密码。密码不用多说,必须是用BCryptPasswordEncoder加盐哈希之后的结果,不能明文存储,更不能用什么简单的MD5直接存。role字段用数字表示0-学生、1-食堂管理员、2-系统管理员,这是一个典型的RBAC简化方案,权限判断放在拦截器里。

CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(50) NOT NULL COMMENT '登录名', `password` varchar(100) NOT NULL COMMENT 'bcrypt加密后的密码', `real_name` varchar(50) DEFAULT NULL COMMENT '姓名', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `role` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0-学生 1-食堂管理员 2-系统管理员', `avatar` varchar(255) DEFAULT NULL COMMENT '头像地址', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '1-正常 0-禁用', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

菜品表要特别讲一下“库存”的设计。我采用“每日库存模式”:每天凌晨由定时任务把菜品库存重置为当日备餐数量,当天售罄即自动下架。表里stock表示当天剩余可售数量,sold表示当天已售数量,两者之和就是当日总备餐量。菜品表不存累计销量,累计销量从订单明细表里统计,避免菜品表无限膨胀。

订单表必须有一个独立的业务主键order_no。很多同学直接用自增id对外展示,这有两个问题:一是订单号规律容易被猜,二是排查问题时难以快速定位。我用的订单号格式是“yyyyMMddHHmmss + 用户id后四位 + 4位随机数”,比如20241208123045000123,一眼就能看出下单时间,配送和售后都方便。

订单明细表是订单与菜品多对多关系的解耦。一个订单包含多个菜品,一个菜品出现在多个订单,如果不拆明细表,订单表里就要存逗号拼接的菜品id字符串,后面统计销量、计算金额、退款拆分全是噩梦。明细表里还有一个关键细节:下单时要保存菜品价格的“快照”。菜品价格后来改了,历史订单金额不能跟着变,所以冗余一份下单时的价格在明细里。

核心表结构整理如下:

表名核心字段设计要点
userid, username, password, role, status密码bcrypt加盐,role区分三类权限
canteenid, name, location, phone食堂基础信息,location用于取餐点展示
dishid, canteen_id, name, category, price, stock, sold, status每日库存模式,售罄自动下架
cartid, user_id, dish_id, quantity, selected以用户维度存储,和前端购物车联动
ordersid, order_no, user_id, canteen_id, total_amount, status, create_time独立订单号,状态机流转
order_itemid, order_id, dish_id, dish_name, price, quantity, subtotal价格快照,防止改价影响历史订单
commentid, order_id, user_id, dish_id, content, rating评分限制1-5星,展示在菜品详情页
noticeid, title, content, author, create_time首页公告轮播

这八张表覆盖了系统的全部业务闭环。答辩时如果被问到“为什么这么设计”,可以从数据冗余、业务封闭、查询效率三个角度去讲,尤其是“价格快照”和“订单号独立”这两个设计点,很容易让老师眼前一亮。

3.2 登录鉴权与权限控制的完整链路

登录认证我选择的是JWT + Spring Security + Redis三方配合的方案。整体流程是这样的:

  1. 用户提交用户名和密码
  2. 后端用BCryptPasswordEncoder的matches方法校验密码
  3. 校验通过后,用jjwt生成token,payload里包含userId、role、tokenVersion
  4. 生成token的同时,在Redis里以login:token:{userId}为key保存tokenVersion,并设置与token相同的过期时间
  5. 前端收到token存到localStorage,后续请求都在Authorization头携带Bearer {token}
  6. 后端拦截器解析并校验token,再比对Redis里的版本号,一致才放行

如果只在拦截器里校验JWT的签名和过期时间,不禁用Redis的版本号,功能上确实也能跑通,但存在一个明显的安全缺口:用户退出登录之后,token本身仍然是有效的,只要有人截获了token,依然能冒充该用户继续请求。Redis版本号机制就是为了解决“服务端主动让token失效”的问题——退出登录或者管理员禁用用户时,只需删除或改变Redis中的版本号,该token立刻失效。

JwtAuthenticationInterceptor是整套认证逻辑的核心,参考实现如下:

@Component public class JwtAuthenticationInterceptor implements HandlerInterceptor { private final StringRedisTemplate stringRedisTemplate; public JwtAuthenticationInterceptor(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } // 从请求头获取token String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { throw new BizException(401, "未登录或登录已过期"); } token = token.substring(7); // 解析token Claims claims = JwtUtil.parseToken(token); if (claims == null) { throw new BizException(401, "无效的登录凭证"); } // 校验Redis中的token版本号 Long userId = Long.valueOf(claims.get("userId").toString()); String cachedVersion = stringRedisTemplate.opsForValue().get("login:token:" + userId); String currentVersion = claims.get("tokenVersion").toString(); if (cachedVersion == null || !cachedVersion.equals(currentVersion)) { throw new BizException(401, "登录状态已失效,请重新登录"); } // 把用户信息放入上下文 UserContext.set(UserContext.UserInfo.builder() .userId(userId) .role(Integer.valueOf(claims.get("role").toString())) .build()); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); } }

Spring Security配置里还要关闭csrf和session机制。前后端分离项目主要靠token认证,不需要session保存登录态;csrf防护在这种模式下反而会增加联调复杂度,直接关掉。

3.3 点餐下单与库存扣减的核心逻辑

下单接口是系统里业务逻辑最重的部分,也是答辩老师最爱追问的地方。整个流程我拆成五步:

  • 生成订单号:使用时间戳拼接用户id和随机数,保证高并发下不重复
  • 对所有参与下单的菜品id依次获取Redis分布式锁,避免并发下单导致超卖
  • 校验菜品状态和库存,库存不足直接抛业务异常
  • 用数据库原子UPDATE语句扣减库存,而不是先SELECT再UPDATE
  • 在同一个事务里插入订单表和订单明细表,全部成功才提交,否则整体回滚

用MyBatis-Plus时原子扣减的写法很关键:

/** * 扣减库存使用 set stock = stock - #{quantity} 保证原子性, * 并通过 stock >= #{quantity} 条件防止库存扣成负数。 */ @Update("UPDATE dish SET stock = stock - #{quantity}, sold = sold + #{quantity} " + "WHERE id = #{dishId} AND stock >= #{quantity}") int decreaseStock(@Param("dishId") Long dishId, @Param("quantity") Integer quantity);

为什么不用“先查后改”?因为“先查后改”在并发场景下存在经典超卖问题:两个请求同时查到库存还剩1份,然后都通过校验,分别执行库存减1,最终库存变成-1。而上面的SQL让数据库自己判断stock >= #{quantity},如果不满足条件,UPDATE影响行数为0,service层判断影响行数为0就说明库存被抢完,直接抛异常。利用数据库的行锁和原子操作处理并发,比在应用层用synchronized更靠谱,因为synchronized只对单个JVM实例有效,多实例部署后就失效了。

订单创建完成后需要支付。真实接入微信或支付宝支付对个人开发者门槛很高,需要营业执照和商户资质,所以毕设一般用模拟支付:前端点击“模拟支付”后,调用后端接口把订单状态从“待付款”改为“待出餐”,同时记录支付时间。这个方案在论文里写明“模拟支付”即可,不需要真正对接第三方网关。

3.4 基于Redis的查询性能优化

学生端首页的点击量最大,菜品列表、食堂列表这些数据“读多写少、实时性要求不高”,非常适合用Redis做缓存。我设计的策略是:

  • 热门菜品缓存:key为cache:hotDish:{canteenId},value为菜品DTO的JSON列表,过期时间30分钟
  • 公告缓存:key为cache:notice,过期时间1小时
  • 菜品分类缓存:key为cache:category:{canteenId},过期时间30分钟

但这里必须克制。很多同学看了一篇“缓存提升性能”的文章,就恨不得把每个查询都包一层缓存。订单状态、购物车数量这类高频变化、个性强、实时性要求高的数据,加了缓存反而会引入缓存更新、缓存穿透、缓存和DB不一致的麻烦。我的经验标准很朴素:只要接口平均响应时间在100ms以内且数据库压力不大,就不要为了在PPT里写“用到Redis”而强行加缓存。技术是服务业务的,不是用来凑字数的。

4. 实操过程:从项目初始化到前后端整合

4.1 工程结构与初始化步骤

很多人拿到项目第一件事就是埋头写代码,结果写到一半发现包结构乱成一团,前端代码和后端代码混在一起。我强烈建议先想清楚目录结构再动手。下面是我最终使用的工程结构:

miguo-canteen/ ├── pom.xml ├── src/main/java/com/miguo/canteen/ │ ├── CanteenApplication.java // 启动类 │ ├── config/ // SecurityConfig, RedisConfig, WebMvcConfig │ ├── controller/ // UserController, DishController, OrderController │ ├── service/ // 业务接口及实现 │ ├── mapper/ // MyBatis Mapper接口 │ ├── entity/ // 与表结构对应的实体类 │ ├── dto/ // 入参对象 │ ├── vo/ // 出参对象 │ ├── common/ // Result封装、常量、工具类 │ └── exception/ // 全局异常处理器 └── src/main/resources/ ├── application.yml // 核心配置文件 ├── mapper/ // XML文件存放目录 └── static/ // 放置Vue打包后的前端文件

Controller一定要越薄越好。我给自己定的规矩是:Controller里不允许出现超过10行的业务逻辑,只负责取参数、调Service、把结果封装成Result对象返回。这样做的直接好处是,答辩时被问到“某个业务怎么实现”,我可以直接从Service层找到对应方法讲,不用在一堆视图代码里翻找。

初始化步骤大致是:在IDEA里用Spring Initializr创建项目骨架,选Web、Redis、Security依赖;接着写application.yml,配置数据源、Redis连接、MyBatis-Plus的mapper-locations;然后创建数据库并执行初始化SQL脚本;最后写一个测试接口验证项目能启动。这一步顺利的话,后面的开发会轻松很多。

关于返回结果的封装,我用统一的Result类:

@Data public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }

统一返回结构的好处是,前端可以写一个公共的axios拦截器统一处理响应码。401统一跳到登录页,200直接取data,接口层面的错误处理逻辑只维护一份就行。

4.2 核心接口与前端页面整合

前后端联调是最容易翻车的地方。我选择把Vue打包后放进SpringBoot的static目录这种方式,最大的好处是毕设演示时只需要单机启动一个jar包,浏览器直接访问http://localhost:8080,页面和接口都在同一个服务里,不需要额外配置Nginx或者处理跨域。

步骤很简单:

  • 前端执行npm run build,得到dist目录
  • 把dist目录下所有文件复制到SpringBoot的src/main/resources/static/目录下
  • 重新打包运行,访问8080端口即看到完整系统

这里有一个非常关键的坑:如果前端用了Vue Router的history模式,刷新页面或直接输入深层路由地址时,后端会返回404,因为SpringBoot的静态资源映射找不到对应的物理文件。最简单的方案是把路由改成hash模式,URL上多一个#符号,但刷新、前进、后退都不会发真实资源请求,完全规避404问题。毕设演示追求稳定,不需要为了美观硬撑history模式。

如果你确实想用history模式,就需要给SpringBoot加一个转发规则,把非接口的路径转发到index.html,但要小心别把真正的接口路径误伤。我的实测经验是:这个兜底配置在不同SpringBoot版本下行为有差异,容易顺手把静态资源路径也一起转发了,处理起来非常煎熬。对毕设而言,用hash模式是性价比最高的选择。

4.3 模拟数据与演示脚本

毕设演示最怕遇到点一个按钮页面白屏、报错的情况。为了确保现场不翻车,我在答辩前准备了一套固定的模拟数据和演示路线:

  • 提前准备好3个食堂、20个菜品、5个测试账号:2个学生、2个食堂管理员、1个系统管理员
  • 让每个食堂都存在不同状态的订单:待付款、待出餐、已完成、已取消各一条
  • 用固定路线演练:先用学生账号完成“浏览 → 下单 → 支付 → 评价”全流程,再切换食堂管理员账号演示“接单 → 出餐”,最后用系统管理员账号查看统计报表
  • 额外录一段屏幕操作视频作为备份,万一现场网络出问题可以播放

这种预演的价值非常大。很多同学觉得自己代码没问题就进场演示,一遇到小问题就开始紧张,越紧张手越抖,演示效果大打折扣。固定脚本加充分预演,能把不可控因素降到最低。

5. 常见问题排查与避坑清单

5.1 环境与配置类问题速查

环境类问题占了开发期报错的一大半,每个都遇到过一次,这里直接给你排查路径。

端口被占用是最常见的启动报错:Port 8080 was already in use。排查方法很简单,Windows执行netstat -ano | findstr 8080,Linux或Mac执行lsof -i:8080,找到PID后强制结束进程。如果是系统里常驻了别的服务占端口,可以换一个应用端口,但别养成瞎换端口的习惯,更推荐找出占用的真凶。

数据库连接失败Communications link failure通常不是代码问题。90%的原因有三个:MySQL服务没启动、连接地址或端口写错、账号权限不对。先去命令行用mysql -u root -p -h 127.0.0.1 -P 3306验证一遍,如果命令行能连上而应用连不上,再去检查application.yml。特别要注意MySQL 8.x必须配置时区参数,否则会报Server returns invalid timezone。我的配置如下:

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

MyBatis的Invalid bound statement (not found)报错也有固定排查顺序:确认mapper接口是否加了@Mapper注解或启动类是否加了@MapperScan;确认XML文件里的namespace是否和接口全限定名一致;确认XML文件放到了resources/mapper/目录,并在配置中声明了mybatis-plus.mapper-locations。

5.2 业务与数据层问题实录

业务层的坑比环境配置更有技术含量,特别是这几个。

库存变成负数,几乎可以肯定是用了“先查再改”模式。解决办法就是前面说的原子UPDATE写法,外加Redis分布式锁做初步拦截。这套组合在毕设场景下完全够用。

金额计算出现奇怪的小数,那一定是用Double或Float存了金额。Java的浮点运算0.1+0.2并不等于0.3,这是二进制存储的天然缺陷。金额计算切换到BigDecimal,并且注意用BigDecimal.valueOf(0.1)或new BigDecimal("0.1"),不要用new BigDecimal(0.1),后者会把浮点数的二进制近似值原封不动带进去。如果金额始终以“分”为单位,直接用Long类型最省心。

列表接口响应很慢,先怀疑N+1查询。用MyBatis-Plus做一对多查询时,很多人会在循环里逐条查子表,本来一条SQL能解决的问题变成了几十条。正确做法是用selectBatchIds一次性查出关联数据,再在内存中按主键分组组装。

前端如果单独起开发服务器,比如Vite默认的5173端口,请求SpringBoot的8080端口必然触发浏览器跨域拦截。解决方案是后端配置CORS允许对应来源,或者使用Vite的proxy配置做服务端代理转发。部署时因为前后端同源,这个问题自然消失,所以不要浪费大量时间在联调阶段折腾跨域。

5.3 避坑清单速查表

场景最容易犯的错正确做法
金额存储用double/floatBigDecimal或分单位Long
密码存储明文 / 简单MD5BCrypt加盐哈希
库存扣减先查库存再UPDATE原子UPDATE + 影响行数判断
订单号直接暴露自增ID独立order_no字段
缓存使用所有查询都加缓存只缓存读多写少、一致性要求低的数据
前端部署强行前后端分离部署整合进static目录 + hash路由
异常处理catch后吞掉不处理全局异常处理器统一返回业务错误

6. 个人实操体会与建议

6.1 时间节奏与论文联动

毕设最容易犯的错是把编码和论文完全割裂开:前两个月闷头写代码,最后两周突击写论文,结果论文里的描述和实际代码多处对不上。我的安排是:第一周做需求和原型,第二周画数据库ER图和用例图,第三周开始写代码时就同步把系统设计章节的初稿写好,后面每完成一个模块就补一小节实现说明。到了写论文的阶段,几乎只是润色和补充截图。整个项目从开始到答辩用了八周,最后一周还能从容地打磨PPT。

6.2 答辩演示的加分细节

我在答辩前重点准备了三个可能被追问的问题,每个都跟项目内部机制强相关:

  • 库存超卖怎么解决?答:Redis分布式锁防止并发下单超卖,数据库层用原子UPDATE语句做最终兜底,双重保险
  • 缓存和数据库一致性如何保证?答:对一致性要求高的订单数据不缓存,只缓存热门菜品和公告这类读多写少的数据;缓存设置了短过期时间,允许秒级延迟
  • 如果用户量再大十倍,系统哪里会成为瓶颈?答:单机MySQL的连接数、Redis内存容量、Web应用的线程池大小,对应的解法是分库分表、Redis集群、水平扩展实例

这三个答案是文末部分了……但都基于真实的理解才可信。只要代码确实是你自己写的,这些问题反而是展示深度理解的最好机会。如果是背网络上的答辩题库,一追问就露馅。

6.3 最后分享一个小技巧

开发过程中我把git reflog用得很熟。这个命令可以查看所有分支的历史操作记录,包括已经被reset掉的那些commit。有一次我在联调时不小心改坏了一个关键的金额计算工具类,发现时已经过了好几天,但靠着git reflog找到了改动前的commit,用cherry-pick把它恢复回来,省下了大量重写时间。做毕设的同学一定要养成频繁commit的习惯,哪怕一天只提交一次。你的每一次commit,都是在给自己买一份后悔药。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询