1. 项目整体定位与技术选型
1.1 为什么是“智能食堂管理系统”而不是普通订餐网站
先聊个有意思的现象:很多同学做毕设时,一看到“食堂订餐”就容易掉进“电商网站”的思维定式——加购物车、下单、支付、等着收货。这套逻辑搬到校园食堂场景里,其实撑不住,因为食堂和外卖电商的核心痛点完全不一样。
外卖电商的核心痛点是“骑手配送效率”,而校园食堂的核心痛点其实是“高峰时段的拥堵、备餐的确定性、以及食堂运营方的数据盲区”。饭点就那么一两个小时,几千号人同时涌进来,窗口排队十分钟不只是学生难受,食堂管理方更难受——他们完全不知道哪个窗口会爆单、哪个窗口没人去、什么菜应该多做一百份、什么菜做出来只能倒掉。
所以这个项目把名字定为“智能食堂管理系统”,而不是“在线订餐网站”。“智能”这两个字不是营销话术,而是整个系统的设计主线:线上预定、备餐联动、数据驱动运营决策。Java + SpringBoot作为后端技术栈,Web端作为访问入口,组合起来就是一个能同时解决学生端体验和食堂端管理效率的完整闭环。
1.2 技术栈选型的深层逻辑
标题里已经明确写了Java和SpringBoot,这里就不再纠结语言层面的事情,但选型背后的理由值得展开说。很多同学做毕设都习惯“哪个火选哪个”,但实际上是“哪个你答辩时说得出所以然,哪个才该选”。
SpringBoot这套东西,核心价值在于“约定优于配置”。做毕设最怕的不是功能多,而是环境配置和项目搭建消耗掉大半时间。SpringBoot内嵌Tomcat,不用单独部署容器,写个启动类直接跑起来,这对毕设来说能省下巨量时间。而且SpringBoot生态极其成熟,Spring MVC做Web层、Spring Data JPA或MyBatis操作数据库、Spring Security做权限控制,全部是一套生态的东西,写起来顺手,答辩评委问起来你也答得清楚。
校园食堂这个场景,有几个技术需求是需要认真对待的:
- 高并发场景:饭点峰值流量是一波一波的,虽然不是双十一级别的量级,但几百上千人同时抢着订餐,对后端接口的响应速度和数据库的压力是有要求的。
- 业务状态流转:订单从“已下单”到“备餐中”到“已完成”,每个状态的转换触发什么逻辑,这是毕设展示“业务建模能力”的最佳位置。
- 数据可视化需求:既然叫“智能”,前端要展示菜品销量分析、窗口热度趋势,后端就要有数据统计接口。
基于这几个需求,选型就很自然了:SpringBoot 2.x做基础框架,MyBatis-Plus操作MySQL数据库,Redis做缓存和Session共享,Vue + Element UI做管理后台页面,微信小程序或移动H5做订餐端。这套组合在Java Web方向非常主流,网上资料丰富,遇到问题能找到解决方案,对毕设来说本身就是一种隐性保障。
1.3 这个系统解决的真实痛点
在动手写代码之前,我建议你先想清楚一件事:这个系统到底给谁用?解决了他们什么具体问题?
我给出的答案是三端角色,对应三套核心痛点:
学生端:痛点是不想排队。在线查看今日菜品、实时查看窗口排队人数、提前下单预约取餐时间、到点直接去窗口取走餐品。这个流程能大幅压缩排队时间,不用再在窗口前纠结吃什么。
食堂窗口端:痛点是备餐没有预判。传统模式下,厨师只能靠经验预估今天做什么菜、做多少份,做多了浪费做少了不够卖。通过系统,学生在饭点之前就完成了下单,窗口根据订单量动态调整备餐计划,什么菜多炒、什么菜少炒,数据说了算。
系统管理端:痛点是运营数据缺失。哪个窗口销量最高、哪个菜品点击率远超实际销量、哪个时间段订单量最集中、用户的消费偏好是什么,这些数据在传统食堂模式下几乎不可能采集。有了系统,这些数据实时生成,食堂经营者可以在数据驱动下优化菜品结构。
明确了这三层痛点,整个系统的功能边界就划清楚了——不是做一个花哨的订餐平台,而是做一个能落地的、解决问题闭环的智能管理系统。这也是答辩时最重要的“项目价值”论述基础。
2. 系统功能模块与核心设计思路
2.1 功能模块全景拆解
一个合格的毕设项目,功能模块不能太少显得空,也不能堆得太多做不完。我推荐的模块划分是这样的:
用户模块:
- 用户注册、登录(学生用户、食堂窗口管理员、系统超级管理员)
- 个人信息维护、收货信息管理(实际上就是取餐人、联系电话、备注口味偏好)
- 密码修改、头像上传
菜品与档口模块:
- 档口管理(食堂下辖多个档口/窗口,每个窗口绑定自己的菜品列表)
- 菜品分类管理(素菜、荤菜、汤品、主食、饮料等)
- 菜品信息维护(图片、价格、描述、每日可售数量)
- 菜品上下架管理(售完自动标记、或者窗口主动下架)
订餐交易模块:
- 菜品浏览与筛选(按档口、按分类、按价格区间、按销量排序)
- 购物车管理(加购、删减、批量结算)
- 订单生成与状态流转(待支付、已支付、备餐中、待取餐、已完成、已取消)
- 模拟支付功能(毕设不需要真的接支付宝微信支付,做一个余额充值 + 模拟扣款即可,但设计上要考虑未来对接真实支付接口的扩展性)
排队与取餐模块:
- 预约取餐时间段(例如11:00-11:15、11:15-11:30这样的时间槽)
- 排队号生成(用户在某个时间段内下单,自动分配一个取餐号)
- 叫号状态推送(Web端轮询或者WebSocket实时推送备餐状态变化)
数据统计模块:
- 菜品销量排行榜(按日、按周、按月)
- 档口营收统计(应对管理端的数据看板需求)
- 用户消费行为分析(高频菜品、复购率、客单价)
- 订单量峰谷时段分析(用于指导食堂备餐时间安排)
系统管理模块:
- 用户管理(冻结/解冻、角色分配)
- 档口与菜品审核
- 订单异常处理(超时未取餐自动标记、退款申请处理)
- 数据字典维护
这套模块划分的合理性在于:每个模块都对应一个真实业务场景,业务边界清晰,表结构设计的时候也不会纠结“这个字段到底放哪张表”的问题。
2.2 为什么选择单体架构而不是微服务
这个决定我必须重点讲,因为这是毕设答辩时评审老师几乎必问的一个问题:你为什么不把系统拆成微服务。
答案很简单:场景不需要,复杂度不允许。
微服务架构要处理服务注册发现、配置中心、分布式事务、链路追踪、网关路由、容器编排,光把这些基础设施写好代码量就已经远超毕设本身了。而校园食堂这个场景,单机部署的SpringBoot应用加一个MySQL,完全能够支撑业务需求。
但“不用微服务”不等于“完全没有架构意识”。单体项目里依然要做好模块化设计,代码按功能分包,业务逻辑层(Service)、控制层(Controller)、数据访问层(Mapper)严格分离。这样即使以后业务规模扩大,也能按模块平滑拆分到独立服务。我在答辩材料里给这一块留了一个专门的说明页,讲清楚“基于当前业务规模和团队规模,单体架构是最优选择,同时为未来演进保留了模块化基础”。这个思路很加分,很容易让老师看出你是真的思考过架构问题,而不是只会用现成脚手架。
2.3 数据库表设计要点
数据库设计是毕设项目中直接展示功底的环节。我建议表结构至少包含以下内容:
核心业务表设计参考
| 表名 | 核心字段 | 关键备注 |
|---|---|---|
| sys_user | id, username, password, role, status | 用户表,role区分学生/窗口/管理员 |
| canteen | id, name, location, open_time | 食堂信息表 |
| stall | id, canteen_id, name, manager_id | 档口表,关联到具体食堂 |
| dish | id, stall_id, category, name, price, image, stock | 菜品表,stock表示每日可售数量 |
| cart | id, user_id, dish_id, quantity | 购物车表 |
| orders | id, order_no, user_id, total_amount, status, pick_up_time | 订单主表 |
| order_item | id, order_id, dish_id, dish_name, price, quantity | 订单明细表(冗余菜品快照) |
| queuing | id, user_id, stall_id, queue_no, status | 排队叫号表 |
| payment_record | id, order_id, amount, channel, transaction_id | 支付流水表 |
| consumption_stat | id, dish_id, order_count, revenue, stat_date | 每日经营统计表 |
这里有两个细节必须强调:
订单明细表一定要存菜品快照。菜品价格和名称可能随时调整,如果订单明细只存一个菜品ID,第二天菜品改名了,历史订单显示就会对不上。保存dish_name和price快照,允许冗余,就是为了保证历史订单的不可变性。
关于数据统计表,我建议设计一张日汇总表。很多同学做到统计功能时,直接用SQL对订单表做聚合查询。短期看没问题,但当订单量积累到几十万条之后,每次打开看板都做全表聚合,MySQL会非常吃力。设计一张每日跑批的统计表,记录每个菜品当天的订单量和营收,报表页面只查这张汇总表。毕设的数据量可能不大,但这个设计思路要写出来,体现你考虑过性能和扩展性。
2.4 订单状态机的设计细节
订单状态变化是整个系统最核心的业务逻辑,它本质上是一个有限状态机。我建议把状态定义为一个枚举类,在代码里明确状态流转的合法性。
状态流转图(文字描述版):
- 待支付:用户提交订单但未支付。超过15分钟未支付则自动关闭,释放菜品库存。
- 已支付:支付成功后进入此状态,同时扣减菜品库存,向食堂窗口端推送新的备餐任务。
- 备餐中:窗口管理员看到新订单后,点击“开始制作”,状态从已支付变为备餐中。
- 待取餐:窗口完成制作,点击“出餐”,状态变为待取餐,系统生成取餐码,通知用户凭码取餐。
- 已完成:用户在取餐时点击“确认取餐”,或者窗口端点击“确认交付”,订单完结。
- 已取消:用户主动取消(仅限待支付和已支付状态),或后台管理员异常处理。
这套状态机看着简单,但有几个坑点必须处理干净:
超时关闭的定时任务:待支付超时关闭,可以用Spring的@Scheduled做定时扫描,每分钟跑一次,把创建时间超过15分钟且状态为待支付的订单改为已取消,同时回滚库存。这里要注意,回滚库存的SQL必须是原子操作,用UPDATE dish SET stock = stock + 1 WHERE id = ? AND stock < daily_limit这样的条件更新,防止并发下单时数据不一致。
并发扣除库存的处理:学生A和学生B同时抢最后一个菜品,两个请求同时扣减stock,如果不做控制,很可能两个订单都显示成功,但菜品实际只剩一个。最简单的处理方式是在SQL层做条件更新:UPDATE dish SET stock = stock - 1 WHERE id = ? AND stock > 0,返回受影响行数为0则说明库存不足,直接提示用户。这个方案虽然简单,但确实是处理秒杀场景最基本的手段,写进论文里也拿得出手。
3. 核心功能模块实现与代码拆解
3.1 登录鉴权模块:JWT + Spring Security
校园食堂系统的角色权限差异明显,学生、窗口管理员、超级管理员各有一套菜单权限和操作权限。这里我用Spring Security + JWT做了一套无状态认证方案。
先解释为什么不用传统的Session方案:Web端前后端分离,前端Vue部署在一台服务器,后端SpringBoot部署在另一台服务器。如果用Session,就得开启Spring Session支持,通过Redis做Session共享。而JWT天然适合前后端分离,后端只负责签发和验签,不保存会话状态,水平扩展的时候完全无状态。
核心实现步骤:
第一步,集成JWT工具类。生成Token时把用户ID、角色、过期时间等关键信息写进claims,用HMAC256算法签名加密,密钥配置在application.yml里:
public String generateToken(Long userId, String role) { Date nowDate = new Date(); Date expireDate = new Date(nowDate.getTime() + 24 * 60 * 60 * 1000L); // 24小时过期 return Jwts.builder() .setHeaderParam("typ", "JWT") .setSubject(userId.toString()) .claim("role", role) .setIssuedAt(nowDate) .setExpiration(expireDate) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }第二步,配置Spring Security的过滤链。写一个JWT认证过滤器,继承OncePerRequestFilter,在每个请求进来时从请求头的Authorization字段里取出Token,验签通过后把用户ID和角色放到SecurityContext里:
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) { String token = request.getHeader("Authorization"); if (StringUtils.hasText(token)) { try { Claims claims = Jwts.parserBuilder() .setSigningKey(secretKey) .build() .parseClaimsJws(token.replace("Bearer ", "")) .getBody(); Long userId = Long.valueOf(claims.getSubject()); String role = (String) claims.get("role"); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userId, null, AuthorityUtils.commaSeparatedStringToAuthorityList(role)); SecurityContextHolder.getContext().setAuthentication(authentication); } catch (Exception e) { // Token无效或过期,不设置Authentication,后续会被Security拒绝 } } chain.doFilter(request, response); }第三步,角色权限控制。在Controller层直接用@PreAuthorize("hasRole('ADMIN')")这样的注解控制接口权限,学生用户调不了管理接口,管理用户也碰不了学生的下单流程。这一套做下来,权限模型清晰,答辩时可以说“基于RBAC模型实现了角色的最小权限控制”。
需要注意的一个细节:密码存储不要用明文。Spring Security的BCryptPasswordEncoder是业界推荐方案,注册用户时加密存储,登录校验时用bcrypt的matches方法对比。答辩时如果老师问你“密码怎么存的”,你答“BCrypt加盐哈希,不可反解出明文”,这是一个很加分的点。
3.2 订餐核心流程:购物车到下单到支付的完整链路
这个流程是整个系统的高潮部分,代码量最大、业务逻辑最密集。我按一个“学生用户完成一次订餐”的全过程拆解,每个环节该做什么、注意什么、为什么这么做,一次讲清楚。
环节一:加购菜品
用户点击菜品详情页的“加入购物车”,前端发POST请求到/cart/add,参数是菜品ID和数量。后端的逻辑分两步:
- 第一步,查菜品状态,确认菜在售、库存大于0,如果当日限量已经售完,直接返回“今日已售罄”。
- 第二步,写购物车表。这里要注意幂等性:同一个用户反复点击“加购”,不应该产生多条相同的购物车记录。我的实现是先查一遍用户购物车里是否已有该菜品,有则累加数量,没有就新增一行。这虽然多了一次数据库查询,但能保证购物车表的数据干净。
环节二:订单生成
用户从购物车点击“去结算”,后端做的事比较多,我按顺序列一下:
- 查询购物车里的所有菜品,校验“是否有菜品已下架”和“是否有菜品库存变动”。
- 按档口维度把购物车拆分成多个订单,也就是说用户一次性买了一家麻辣烫窗口和三兄弟盖浇饭窗口的东西,系统会生成两个订单,分别推送给对应的档口。这是食堂场景的特殊之处——外卖电商是一单对应一个配送方,食堂是一单对应多个出餐窗口。
- 计算总金额,插入orders表和order_item表,订单初始状态设为“待支付”。
- 生成订单号,注意订单号不能用数据库自增ID直接给用户看。我用的是时间戳加随机数的策略:
yyyyMMddHHmmss加四位数随机码,保证用户层面看不到订单之间的关联关系。
生成订单的时候,事务边界一定要划分清楚。@Transactional加在service层的方法上,购物车检查、订单头插入、订单明细插入、购物车清空必须在一个事务里完成,任何一个环节报错都要整体回滚。这是我见过很多同学最容易漏掉的地方——订单创建了,购物车没清空,或者订单明细丢了,数据一致性直接崩掉。
环节三:模拟支付
支付这块毕设不接真实渠道,但设计逻辑要自洽。我的方案是用户账户里有余额,提前做充值操作(模拟银行卡充值),下单时从余额里扣款,生成一条支付流水记录。
扣款操作要特别小心重复支付的问题。前端极有可能因为跳转异常导致用户重复点击支付按钮,后端必须保证“一个订单只能支付成功一次”。我的做法是在扣款SQL上再加一个状态条件:
@Update("UPDATE orders SET status = 'paid', payment_time = now() " + "WHERE id = #{orderId} AND status = 'pending'") int markOrderPaid(Long orderId);如果返回值为0,说明这单已经不是待支付状态,直接往上层抛异常。加上用户余额扣款也要校验余额充足,否则提示“余额不足,请先充值”。三个操作(扣余额、插流水、改订单状态)再包一个事务,逻辑闭环就完整了。
环节四:通知档口备餐
订单支付成功之后,系统要立刻让对应档口知道“来了新活儿”。这里我用的是Redis发布订阅,支付完成接口里发送一条消息到频道stall:${stallId},档口管理端的WebSocket监听这个频道,收到消息后前端弹窗提醒。这块用Redis做发布订阅,比直接用WebSocket点对点推送要轻量,也顺便给档口号和WebSocket连接做一个解耦:后续就算前端技术栈换掉,后端的消息通知逻辑完全不用改动。
3.3 排队叫号与取餐状态推送
排队叫号是食堂场景里一个比较有特色的功能模块,做得好会让整个系统的“智能”属性大幅提升。
我的实现思路是:每个档口每天维护一个自增的队列计数器,用户支付成功后,系统为该订单生成一个取餐号,号段格式是C01-015,C01表示档口ID,015表示第15个订单。取餐号按时间顺序递增,用户端实时显示“前面还有几单”,窗口端显示器显示“当前呼叫到C01-012”。
前端有两种方案可以拿到状态变化:
- 方案A:前端轮询。每3秒调一次查询接口,拿当前队列进度。简单稳定,但3秒一次请求在几百人同时使用时,对后端压力还是有点大。
- 方案B:WebSocket推送。后端在状态流转的关键节点(出餐完成、呼叫取餐)主动推送消息给对应排队用户的页面。实时性强,体验好,但代码复杂度明显提升。
毕设阶段我推荐方案B,因为WebSocket是高频考点。SpringBoot里用原生TextWebSocketHandler实现一个WebSocket处理器,加上WebSocketConfigurer配置类注册WebSocket端点:
@Configuration public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(queueNotifyHandler(), "/ws/queueNotify") .setAllowedOrigins("*"); } }用户登录时前端把JWT Token作为WebSocket连接的参数传过来,WebSocket握手阶段用拦截器校验Token,把用户ID映射到WebSocket Session。后面需要给指定用户推送消息时,根据用户ID找到对应Session,直接发送JSON消息,前端收到后触发取餐弹窗。
这里有一个经验之谈:WebSocket连接断开之后要记得清理Session映射。用户关掉页面不需要通知,但如果不清理Map的条目,长此以往会内存泄漏,导致服务器内存越堆越高。我一般会在afterConnectionClosed回调里做remove操作,这也是一个能在答辩时被追问到的“细节加分项”。
3.4 管理端数据看板与统计报表
系统的“智能”程度,最终要从管理端数据看板上体现出来。如果只是把订单查出来罗列在表格里,那不叫智能,叫存证系统。真正的智能是要把数据变成决策参考。
我做了一套四块核心指标的数据看板:
档口热度排行榜:按近7天订单量和营业额排出各档口的热度,用柱状图展示。这个指标直接指导食堂管理方考虑窗口位置调整和资源分配。
菜品销售排行TOP10:按销量排名,同时展示“点击率/下单率”的比值。如果一个菜被浏览了很多次但下单很少,大概率是价格、图片或描述出了问题,属于潜在优化对象。
经营时段分布图:把一天的订单量按小时聚合,画出走势图。正常情况下能看到上午10点到12点、下午5点到7点的两个峰,峰值数据能指导排班和备餐计划。
库存预警与售罄分析:统计当天各菜品的售罄时间,提前售罄说明备餐量不够,经常剩菜说明量备多了。这个数据对后厨采购计划而言,价值非常高。
这些统计接口的实现,主要靠给orders表和order_item表写聚合SQL。我的建议是SQL聚合逻辑放在Mapper层用注解或XML写清楚,不要用简单的循环遍历去累加统计,性能和代码可读性天差地别。举个例子,查“近7天档口营业额”:
SELECT s.id AS stall_id, s.name AS stall_name, SUM(oi.price * oi.quantity) AS total_revenue FROM orders o LEFT JOIN order_item oi ON o.id = oi.order_id LEFT JOIN stall s ON oi.stall_id = s.id WHERE o.status IN ('paid', 'ready', 'completed') AND o.payment_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY s.id, s.name ORDER BY total_revenue DESC;再配合前端的ECharts组件,把接口返回的JSON直接绑到series.data上,图表就出来了。整个看板的开发效率很高,后端接口一回车前端就有图可看。
4. 系统部署环境配置与实操记录
4.1 本地开发环境搭建
很多同学在这一步就卡住了,卡的原因不是不懂Java,而是环境版本的排列组合出了问题。我给出一份实测可用的环境组合,这个组合是我用来跑通整个项目的方案:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 8 或 11 | SpringBoot 2.7.x 最稳定 |
| Maven | 3.6.3 及以上 | 统一管理项目依赖 |
| MySQL | 5.7 或 8.0 | 字符集必须配置为 utf8mb4 |
| Redis | 5.0 及以上 | 用于缓存与发布订阅 |
| Node.js | 14 及以上 | 前端Vue项目的构建环境 |
| IDE | IntelliJ IDEA | 社区版做毕设完全够用 |
一个容易被忽略的坑是MySQL的字符集配置。如果数据库字符集不是utf8mb4,在存储用户昵称、菜品描述的emoji表情时直接报错。我建议建库SQL里就显式指定:
CREATE DATABASE smart_canteen DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;后端项目结构我推荐保持maven的经典结构:
com.canteen ├── configuration # 全局配置类(Redis、WebSocket、跨域等) ├── controller # 控制层 ├── service # 业务逻辑层 ├── mapper # MyBatis接口层 ├── entity # 数据库实体类 ├── common # 通用工具类(JWT、Result封装、异常处理) └── CanteenApplication.java # SpringBoot启动类包结构的整齐程度直接关系到答辩印象分。很多同学把实体类、DTO、VO、工具类全堆在一个包下,代码查起来让人头大。分好包之后,阅读代码的人第一眼就能感知到项目规范。
4.2 后端核心配置文件精讲
application.yml是这个系统运转的“总控室”,这里把最核心的几项配置和参数逐一说明:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/smart_canteen?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=GMT%2B8 username: root password: 你的数据库密码 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 timeout: 3000ms servlet: multipart: max-file-size: 10MB max-request-size: 10MB mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 jwt: secret: 你自己的高强度随机字符串 expire-hours: 24几个容易踩坑的关键点:
MySQL连接串里的serverTimezone=GMT%2B8必须加上,否则会报时区错误。其中%2B是加号的URL编码,直接用+在某些环境下会被解析成空格,导致连接失败。
MyBatis-Plus的逻辑删除配置。实体类里加上@TableLogic注解的字段,删除数据的时候自动变成UPDATE逻辑删除标记,而不是物理DELETE。这个方案保留了数据完整性,也方便数据回查,是当前企业级Java项目的常用做法。
文件上传大小限制。菜品图片上传是刚需,默认的1MB限制太小,菜品图片动辄2到3MB。这里配置了10MB上限,基本覆盖所有场景。
4.3 前后端联调与部署实操
前端代码我用的Vue 2.x加Element UI,后台管理系统和管理端页面混在一个项目里,通过路由区分。和SpringBoot后端联调时,最大的坑是跨域问题。
开发环境下,前端跑在localhost:8081,后端跑在localhost:8080,两个端口不同必然触发跨域。我建议在后端做一个全局CORS配置类,统一处理所有跨域请求:
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }这里有个细节:.allowedOriginPatterns("*")和allowCredentials(true)必须搭配使用。如果只是allowedOrigins("*"),浏览器会拒绝带Cookie的跨域请求,因为这样配置存在安全风险,浏览器层就会直接拦截。别问我为什么知道这个问题,我调试了大半个下午才定位到这一行。
打包部署环节,后端直接执行:
mvn clean package -DskipTests在target目录下生成一个可执行的Jar包,通过nohup java -jar canteen-0.0.1-SNAPSHOT.jar > canteen.log 2>&1 &方式在服务器后台启动。前端构建执行npm run build,生成dist目录,扔到Nginx的html目录下,然后配置Nginx做反向代理:
server { listen 80; server_name your-domain-or-ip; location / { root /usr/share/nginx/html; index index.html; 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; } }Nginx配置里的try_files $uri $uri/ /index.html这行非常关键。Vue是单页应用,前端路由是history模式,刷新/dashboard页面时,Nginx如果按物理路径找文件会返回404,加了这行配置就会把所有不存在的路径都指向index.html,由前端路由接管,页面才能正常刷新。
生产环境上线时,记得在Nginx里配置gzip on;开启压缩,静态JS和CSS体积能压缩掉一半以上,页面加载速度提升非常明显。
5. 常见问题排查与避坑指南
5.1 开发期高发问题速查表
把我在实测过程中遇到的典型问题整理成一张速查表,这些问题在毕设答辩项目演示环节最容易翻车,提前排查能省很多麻烦。
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
启动报Failed to configure a DataSource | 数据库连接参数错误或MySQL没启动 | 检查application.yml的数据源配置,先手动用客户端工具连接一次 |
| 跨域请求失败 | 前端端口与后端端口不一致,或CORS配置缺失 | 在后端加全局CORS配置类 |
| 登录接口正常但前端拿不到用户信息 | JWT未正确返回或前端没存Token | 检查登录接口返回体,确认Token放在响应里,前端登录后存入localStorage |
| 下单报库存超卖 | 扣库存SQL没有加条件守卫 | 扣库存SQL加AND stock > 0条件判断 |
| 中文乱码 | MySQL连接参数未配置characterEncoding=utf8 | 连接串加上characterEncoding=utf8并确保数据库字符集为utf8 |
| WebSocket连不上 | 握手拦截器里JWT校验失败,或跨域配置缺OPTIONS放行 | 在拦截器里放行OPTIONS预检请求,校验失败的Session直接关闭 |
| 图片上传404 | 静态资源映射没配置,或上传目录不存在 | 通过WebMvcConfigurer映射/**/upload/**到本地目录 |
5.2 Redis 缓存下线的“三连坑”
这个要单独拎出来讲。我的项目里Redis承担了三份职责:Session共享的替代(JWT其实不需要Redis)、菜品列表的缓存、WebSocket消息的发布订阅。Redis一旦挂掉,系统不会直接崩溃,但会出现三类令人摸不着头脑的故障:
- 菜品列表接口变慢,因为缓存失效,每次都要查数据库。
- WebSocket的消息收不到,因为发布订阅通道断掉后,订阅方不会收到补发消息。
- 登录状态不受影响(得益于JWT),造成一种“系统好像没问题”的错觉。
排查Redis是否正常,最简单的命令是redis-cli ping,返回PONG就是活着。我还建议在配置里加一个简单的健康检查逻辑,启动时测试一下Redis连接,连不上就打印一条醒目的警告日志。毕设演示时如果Redis没启动而系统照常跑,这种感觉是最折磨人的——你根本不知道问题出在哪一层。
5.3 答辩演示时最应该预演的三个场景
答辩现场演示翻车是最高频的意外事件。我说的不是代码写不出来那种翻车,而是环境问题导致的“不配合”。三个场景必须提前预演:
场景一:从零启动后端项目。
关闭所有服务,清空MySQL和Redis中项目相关数据,重新启动SpringBoot,确认能正常初始化。很多同学平时开发都不关服务,导致真实演示时启动报错。我特别建议在演示前做一次“冷启动测试”,列表数据为空也好,至少系统跑起来了,你能顺手演示菜品初始化模块,反而是一个自然的过渡环节。
场景二:演示“售罄”逻辑。
这是最能体现系统“智能”属性的功能点。把某个菜品的可售数量直接改为1,然后用两个不同账号同时下这个菜的订单,第一个订单成功,第二个订单弹窗提示“已售罄,试试其他美食吧”。这个场景演示完,再把库存调大,操作一个菜品从“售罄”到“可售”的完整过程,评委能看到前后端的联动,系统实时性一目了然。
场景三:演示WebSocket实时叫号。
打开档口管理页面和学生订餐页面两个窗口,学生端支付完成后,档口端立刻弹出新订单提醒,状态从“待支付”变成“已支付”,点击“出餐”,学生端的取餐状态实时变成“待取餐”。整个链路用不了两分钟,但把系统的核心亮点全部展现了——订单实时流转、状态机驱动、WebSocket推送。
6. 从毕设到项目:这套系统还能怎么扩展
项目做到当前阶段,功能闭环已经完整,可以顺利收官。但如果你时间富裕、想在答辩时再多展示一些亮点,或者在项目描述里增加一些有深度的内容,有三个扩展方向非常合适:
方向一:接入推荐算法,做“今日推荐”栏目。
现在菜品列表只是按分类和销量排列,说不上“智能”推荐。你可以基于用户的历史订单数据计算“常点的档口”“常吃的菜品类目”,再结合当前时段的销量热度,给每个用户生成一张个性化的推荐列表。落地方式也很轻量,不需要深度学习相关的东西,在service层做一个简单的多因子加权算法即可,但代码量不大,故事价值很大。
方向二:定时任务自动生成经营日报。
当前的数据看板是手动刷新查出来的,扩展方案是把经营统计做成每日凌晨自动跑批,汇总前一天的订单数据到consumption_stat表,生成一份格式化的经营日报表,推送给管理员的微信或邮件。这个功能非常适合体现SpringBoot的定时任务调度能力。
方向三:针对“备餐等待时间”做预测。
系统记录了每个档口每个订单从“已支付”到“备餐完成”的耗时数据。基于这份数据,计算每个档口当前的平均备餐时间,在学生下单选取餐时间时展示“预计等待15分钟”。这个功能看似不起眼,但它把一个静态的订餐系统升级成了有时间感知能力的动态系统。
这三个扩展方向都基于现有数据和框架,不需要引入额外技术栈,但叙事上能把项目从“一个简单的管理系统”升维到“一个数据驱动的智能决策平台”。
我在实际操作中最大的体会是:毕设项目的价值不在于你用了多酷炫的技术,而在于你能否把一个真实的业务痛点讲清楚,再用合理的技术方案完整解决它。“食堂线上订餐”这个场景,胜在人人有感、场景真实、痛点明确,技术选型和业务需求高度匹配,这套逻辑本身就足够支撑高质量的项目呈现。落到代码上,核心链路清晰、状态机严谨、数据闭环完整,只要老老实实做扎实,答辩时你就有底气说“这是我独立设计并实现的一个完整系统”。