
Spring Boot 做校园点餐系统不难但能不能做到被学校食堂、外卖店、学生三边都夸“好用”就完全是另一回事了。这个“springboot123校内校园点餐系统”标题里藏着的信息量其实很大它既是一个典型的 Java 全栈练手项目也是一个能直接落地到真实生活场景的管理系统。今天我就以实际开发和部署的角度把这个项目的完整玩法、架构思路、核心代码设计、踩坑记录全部拆给你看。先说一下这个系统到底能干什么。简单来说它就是把一个校内校园点餐的全流程搬到线上学生登录后能浏览食堂或商家的菜品加入购物车下单支付再跟踪订单状态商家端要处理接单、备餐、出餐管理员端负责菜品分类、商家审核、数据统计。技术上用的是 Spring Boot 作为后端框架搭配主流的关系型数据库存数据前端可以用 Vue 或者 Thymeleaf 模板引擎整个系统就是一个典型的前后端分离或者半分离的架构。适合谁看如果你正在做毕业设计、课程项目或者刚学会 Spring Boot 基础想找一个完整的实战项目练手这篇内容能直接帮你跳过大量弯路。1. 整体设计思路一个校园点餐系统到底该怎么拆模块很多人拿到这种系统需求第一反应就是赶紧建表、赶紧写接口。我的建议是反过来先把角色的使用路径走一遍把“谁在什么时间用什么功能”梳理清楚再去动代码。校园点餐系统的核心角色其实就三类学生C 端用户、商家B 端商户、管理员后台运营方。每一类角色背后的功能诉求是完全不一样的数据库设计、权限设计、接口粒度都要跟着角色走。1.1 核心需求拆解三类角色三条主链路学生端最核心的链路是登录 → 浏览菜品 → 加入购物车 → 生成订单 → 支付 → 等待出餐 → 确认取餐。这条链路看起来简单但里面涉及了一个非常容易被学生项目忽视的点订单状态流转。订单状态不能是一条死线它必须支持回退和异常处理。比如用户下单后后悔了要有取消订单的能力商家出餐超时了系统该有提醒机制如果用户支付了但商家因为原料不足拒单了还得有自动退款流程。我见过不少项目把订单状态设计成单纯的一个 int 字段0 未支付、1 已支付、2 已完成然后写死在代码里。这种方式应付课设答辩没问题但一旦进入真实场景状态机缺失会让整个系统变得极其难维护。商家端的主链路是商家登录 → 菜品管理 → 接收新订单 → 确认接单 → 出餐完成。商家最在意的不是界面的花哨程度而是操作够不够快、订单信息够不够清晰。设计商家端时一个核心原则是减少点击次数。比如新订单推送消息里直接带上“接单”和“拒单”两个按钮就比让商家进入订单详情页再操作高效得多。管理员端的功能相对杂用户管理、商家审核、菜品分类管理、订单统计、营业额报表。这里要注意的是很多新手会把管理员端和商家端混在一个角色体系里导致权限判断到处是 if 嵌套。正确的做法是用 Spring Security 或自定义拦截器做角色分级把对接口的访问权限精细到方法级别。1.2 技术选型为什么是这些Spring Boot 不是唯一答案但它是首选校园点餐系统这种场景备选方案其实不少Python 的 FastAPI、Go 的 Gin甚至直接用 Node.js 写也不是不行。但 Spring Boot 在高校场景里的优势非常明显。第一生态成熟。Spring Boot 本身就整合了 Spring MVC、Spring Security、MyBatis 或 JPA 的配置开发起来不用像传统 SSM 框架那样手写大量 XML。对于第二个部署运维层面Spring Boot 内嵌了 Tomcat一个 java -jar 命令就能启动对服务器要求低租一台小型云服务器就能跑起来完全够一个学校内部的点餐系统使用。第三资料和社区深度足够。哪怕你卡在 Redis 缓存穿透这种细节上也能快速找到对应的解决方案。数据库选型上我用的是 MySQL 8.0搭配 MyBatis-Plus 做持久层框架。为什么不用 JPA因为点餐系统的查询需求非常杂尤其是后台的数据统计模块各种分组聚合查询、多表联查MyBatis 写 SQL 的控制力非常强更适合这种业务场景。缓存方面建议引入 Redis用来存放菜单列表、购物车临时数据、登录 Token减少数据库压力。1.3 功能模块地图别漏掉这些“不起眼但很关键”的子功能很多课设项目做点餐系统把注意力全放在下单支付上结果到了验收的时候导师问一句“用户忘记密码怎么办”就卡壳了。一个完整的校内点餐系统功能地图应该是这样的用户侧注册登录、找回密码、浏览菜品分类、菜品搜索、购物车管理、生成订单、在线支付、订单状态跟踪、历史订单查询、个人资料修改、评价功能。商家侧店铺信息管理、菜品增删改查、库存设置、订单管理接单/拒单/完成、营业统计、评价管理。管理侧用户管理封禁/解封、商家入驻审核、菜品分类管理、公告管理、数据看板、订单总览。支付这一环如果是课设演示可以用模拟支付前端直接跳转到“支付成功”页面如果是要真实使用可以接入微信支付或支付宝沙箱环境。我的建议是两套都做通过配置切换演示的时候用模拟支付上线的时候切换真实支付渠道。2. 数据库设计详解订单表的核心字段决定系统上限数据库是整个系统的地基订单表和菜品表的设计直接决定了系统的上限。我见过最糟糕的设计是把订单的所有信息全塞进一张大表里下单商品列表用逗号分隔的字符串存进去。这样写起来确实快但后查数据、做统计时就得写一堆奇葩的字符串处理代码。正确的做法是采用主表和子表分离的设计订单主表存核心信息订单明细表存菜品快照。2.1 核心数据表结构与字段解释用户表建议不要直接叫 user因为 user 在 MySQL 里本身是保留关键字。可以用 member 或 sys_user。必要字段包括id、username、password加密存储、real_name、phone、avatar、rolestudent、merchant、admin、status正常/禁用、create_time。商家表可以独立出来也可以和用户表通过 role 区分。我建议独立一张 shop 表id、merchant_id外键关联用户表、shop_name、shop_logo、notice、delivery_fee、起送价 min_price、营业状态 open_status、评分 score。订单主表 ordersid、order_no订单号唯一、user_id、shop_id、total_amount、pay_amount、discount_amount、status待支付/已支付/已接单/配送中/已完成/已取消/退款中、pay_time、finish_time、remark、create_time。订单明细表 order_detailid、order_id、food_id、food_name冗余菜品名称防止菜品被删除后订单显示异常、food_image、price下单时的价格快照、quantity、sub_total。菜品表 foodid、shop_id、category_id、food_name、food_description、cover_image、price、stock、sales、status上架/下架、create_time。分类表 category 建议做成两级一级分类比如“家常菜”“面食”“饮品”二级可以是具体菜品名但不要无限嵌套增加复杂度却没有实际收益。还要单独说一个很容易被忽略的表购物车表 cart。校园点餐的购物车和电商购物车不太一样时效性非常强用户可能中午下单前加购吃完后购物车就清空。所以设计上可以直接用 Redis 存用哈希结构 key 为 userIdfield 为 foodIdvalue 为数量。这样性能好而且天然有过期时间省一张 MySQL 表。但如果你希望用户在不同设备上购物车同步那就得老老实实建一张 MySQL 表按 user_id 和 food_id 做唯一索引。2.2 一张表看懂订单状态流转订单状态是整个点餐系统里最容易出错的地方先看下状态机状态码状态名称触发动作下一状态0待支付用户下单成功支付成功 → 1超时 → 61已支付/待接单用户完成支付推送给商家商家接单 → 2商家拒单 → 72已接单/制作中商家确认接单出餐完成 → 33待取餐/配送中商家出餐通知用户用户确认取餐 → 44已完成用户确认取餐或超时自动确认终态5已取消用户取消或系统自动取消终态6退款中仅已支付状态发起退款成功 → 77已退款/已关闭商家拒单/用户退款终态这个状态机要在代码里严格约束每个状态只允许特定动作触发转移。比如已取消的订单绝不能再次进入“已支付”否则对账的时候会乱成一团。实现上可以在后端定义一个状态流转校验工具类把合法流转关系配成 Map 或枚举对照表每次更新订单状态前先校验。2.3 数据库层的三个性能优化细节校园点餐的并发量确实不会像电商大促那么夸张但中午饭点的峰值还是能到几百甚至上千并发数据库扛不住就完了。这三件事在开发时就应该做好第一订单号不要用自增 id要用雪花算法或时间戳加随机数生成。因为自增 id 会暴露订单量而且在高并发插入时会产生热点写竞争。订单号一般用 18 位左右前几位是时间标识后面是随机数。第二菜品表要建立 (shop_id, status) 联合索引这是菜单列表查询最常用的查询条件。订单表要建立 (user_id, create_time) 联合索引支撑用户查历史订单同时建立 (shop_id, status) 支撑商家端查订单。第三库存字段的更新不能用先查再减的写法要直接用 SQL 原子操作 UPDATE food SET stock stock - 1 WHERE id ? AND stock 0防止超卖。3. Spring Boot 后端核心实现从启动类到具体接口的完整拆解这一节直接上干货梳理一下 Spring Boot 项目搭建的完整流程以及点餐系统里最核心的几个接口的实现思路。项目整体结构用的是标准的三层架构Controller 层写接口Service 层写业务逻辑Mapper 层操作数据库实体类对应数据表。不要把所有逻辑都塞在 Controller 里否则后续想加权限、加缓存改动量会非常大。3.1 项目初始化与依赖配置创建一个 Spring Boot 项目版本选 2.7.x 或 3.x 都可以。考虑到大部分高校环境用的 JDK 还是 8建议直接选 Spring Boot 2.7 系列JDK 8 兼容性最好。如果你对版本选择没有特别偏好又不确定 3.x 的依赖变化会不会给你带来困扰2.7 是最稳妥的选择。pom.xml 里最关键的依赖就是这几个dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependency /dependencies数据库连接配置直接写在 application.yml 里server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/campus_order?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto连接 MySQL 时serverTimezone 参数一定要配成 Asia/Shanghai否则日期字段和系统时区的偏差会在查订单时间时把你坑死。map-underscore-to-camel-case 这个配置建议打开可以把数据库里的 create_time 自动映射成实体类的 createTime省去很多手写映射代码的麻烦。3.2 登录鉴权与权限控制用 JWT 还是 Session登录鉴权方案的选择说简单也简单说复杂也复杂。传统的校园课设项目最省事的方式是 Session Cookie因为 Spring Boot 内置 Tomcat 支持不用额外引入组件。但如果你希望系统能支持前后端分离以后多端共用一套接口JWTJSON Web Token是更好的方案。JWT 的思路就是用户登录成功后服务端签发一个 Token把用户 id、角色、过期时间加密到 Token 里面。客户端后续请求在请求头里带上 Authorization: Bearer xxx后端通过拦截器解析 Token 获取用户身份。核心拦截器代码可以这样写public class AuthInterceptor implements HandlerInterceptor { Autowired private StringRedisTemplate redisTemplate; 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 )) { token token.substring(7); try { JWTVerifier verifier JWT.require(Algorithm.HMAC256(your-secret-key)).build(); DecodedJWT decodedJWT verifier.verify(token); Integer userId decodedJWT.getClaim(userId).asInt(); String role decodedJWT.getClaim(role).asString(); request.setAttribute(userId, userId); request.setAttribute(userRole, role); return true; } catch (Exception e) { response.setStatus(401); return false; } } response.setStatus(401); return false; } }JWT 的密钥千万不要硬编码在代码里建议配置在 application.yml 的配置项中后续上线可以通过环境变量覆盖。Token 过期时间我一般设置为 24 小时对于校园点餐这种低频系统足够了。如果你希望用户退出登录后 Token 立即失效那就得在 Redis 里再加一层黑名单缓存否则 JWT 在过期前一直是有效的这是 JWT 方案天然的短板。角色权限这块可以用 Spring Security也可以用拦截器自行判断。如果不想引入 Spring Security 的一堆配置可以在拦截器里校验角色做一个手动版本public class RoleInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String requiredRole (String) request.getAttribute(requiredRole); String userRole (String) request.getAttribute(userRole); if (!requiredRole.equals(userRole)) { response.setStatus(403); return false; } return true; } }通过 WebMvcConfigurer 注册拦截器并给不同的接口路径配置不同的权限要求。比如 /api/merchant/ 开头的接口只允许 merchant 角色访问/api/admin/ 开头只允许 admin 访问。3.3 点餐核心链路实现下单接口怎么设计才能不出事故点餐系统最核心的接口是下单。一个合格的下单接口必须做到事务性、防超卖、防重复提交三件事并行。事务性指的是生成订单主表、生成订单明细、扣减库存、清空购物车这几步要么全成功要么全失败。接口参数大概是这样{ shopId: 1, items: [ { foodId: 12, quantity: 2 }, { foodId: 15, quantity: 1 } ], remark: 不要辣 }Service 层的实现思路Transactional(rollbackFor Exception.class) public OrderVO createOrder(Long userId, CreateOrderDTO dto) { // 1. 查询商家是否存在且在营业 Shop shop shopMapper.selectById(dto.getShopId()); if (shop null || shop.getOpenStatus() ! 1) { throw new BusinessException(店铺不存在或已打烊); } // 2. 遍历菜品计算金额校验库存 ListOrderDetail detailList new ArrayList(); BigDecimal totalAmount new BigDecimal(0); for (OrderItemDTO item : dto.getItems()) { Food food foodMapper.selectById(item.getFoodId()); if (food null || food.getStatus() ! 1) { throw new BusinessException(菜品已下架: item.getFoodId()); } // 原子扣减库存 int updated foodMapper.decreaseStock(item.getFoodId(), item.getQuantity()); if (updated 0) { throw new BusinessException(库存不足); } // 计算小计 BigDecimal subTotal food.getPrice().multiply(new BigDecimal(item.getQuantity())); totalAmount totalAmount.add(subTotal); // 保存明细快照 OrderDetail detail new OrderDetail(); detail.setFoodId(food.getId()); detail.setFoodName(food.getFoodName()); detail.setFoodImage(food.getCoverImage()); detail.setPrice(food.getPrice()); detail.setQuantity(item.getQuantity()); detail.setSubTotal(subTotal); detailList.add(detail); } // 3. 校验起送价 if (totalAmount.compareTo(shop.getMinPrice()) 0) { throw new BusinessException(未达到起送价 shop.getMinPrice() 元); } // 4. 生成订单号 String orderNo generateOrderNo(); // 5. 保存订单 Order order new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setShopId(shop.getId()); order.setTotalAmount(totalAmount); order.setPayAmount(totalAmount); order.setStatus(0); // 待支付 orderMapper.insert(order); // 6. 保存订单明细 for (OrderDetail detail : detailList) { detail.setOrderId(order.getId()); orderDetailMapper.insert(detail); } return convertToVO(order); }注意Transactional 注解的 rollbackFor 属性一定要设为 Exception.class。因为 Spring 默认只在 RuntimeException 回滚如果你抛的是自定义的检查异常不加这个属性事务不会回滚订单可能只生成了一半就卡住了。防重复提交可以用 Redis 的 setnx 命令用一个以 userId 加 shopId 为 key 的锁下单前尝试加锁下单后释放锁并设置短时间过期。这样即使用户疯狂点击提交按钮系统也能保证同一用户同一时间只有一个订单在创建。3.4 支付回调与订单状态更新的安全落地真实支付渠道接入后支付结果是通过异步回调通知后端的。这一步非常容易踩坑因为支付平台的回调可能因为网络原因重复推送如果后端不去重订单状态就会被重复更新。支付回调接口的正确写法是收到回调后先根据订单号查订单状态如果订单已经是“已支付”直接返回成功响应不再做任何更新。然后把回调数据落一条日志表方便对账排查。模拟支付的话更简单直接提供一个接口把订单从待支付状态改为已支付并且触发后续的商家通知逻辑。不过要注意模拟支付接口一定只能在校内测试环境开启上线前必须关闭否则任何人都可以把自己订单改成已支付状态。3.5 商家端与用户端的消息通知机制校内点餐系统里的消息通知不需要做到像即时通讯那样实时推送用轮询就能解决。商家端有新订单时前端每 10 秒拉一次待接单列表用户端下单后每 10 秒查一次订单状态。这个方案在用户量不大的校园场景下非常稳定实现简单也不用引入消息推送组件。另外在 Redis 里可以做一个简单的提醒渠道。下单成功后把 shopId 作为 key往一个 List 里 push 一条消息商家端的接口从 List 里 pop 消息。这样比前端直接轮询数据库压力小得多而且响应时间几乎是即时的。4. Redis 在点餐系统里的实际应用缓存、购物车与防重Redis 在点餐系统里是性价比很高的一环。它的应用点主要集中在三个地方菜品列表缓存、登录 Token 存储、购物车临时数据。不要一开始就把 MySQL 的数据全部塞进缓存而是针对热点数据做缓存。4.1 菜品列表缓存减少数据库压力最直接的手段中午饭点的时候大量用户同时打开同一个商家的菜单数据库查询压力集中在一个点。可以在查询菜品列表的 Service 层加一层 Redis 缓存。key 设计成 menu:shopId:categoryIdvalue 是 JSON 字符串过期时间 30 分钟。缓存更新策略用最简单的 Cache Aside 模式查询菜品时先读缓存缓存没命中再查数据库并回填商家修改菜品、上下架菜品时主动删除对应商家的缓存 key。这里要注意删除缓存和更新数据库的顺序一定要先更新数据库成功再删除缓存避免缓存先删了、数据库还没更新完成用户读到旧数据的情况。4.2 购物车用 Redis 还是 MySQL购物车我建议直接采用 Redis 的 Hash 结构key 是 cart:userIdfield 是 foodIdvalue 是数量。核心操作代码public void addToCart(Long userId, Long foodId, Integer quantity) { String key cart: userId; redisTemplate.opsForHash().increment(key, foodId.toString(), quantity); }读取购物车时需要把 foodId 列表查一遍数据库拿到菜品详情再拼接成完整购物车对象。这里有一个性能点如果购物车里菜品数量很多不要一个菜一个菜查数据库直接把所有 foodId 收集起来用 in 查询一次拿回所有菜品数据。4.3 分布式锁防重复提交和数据一致性下单接口的防重我用的是 Redis 分布式锁public boolean tryLock(String key, String requestId, long expireTime) { return redisTemplate.opsForValue().setIfAbsent(key, requestId, Duration.ofSeconds(expireTime)); }锁的 key 用 userId 加 shopId。同一用户在同一个店铺同时只能有一个订单在创建中锁过期时间设置 5 秒就够了。如果用户提交订单后支付页面卡住超过 5 秒理论上可以再次提交但这属于极端场景对于校园系统来说影响不大。Redis 在支付回调去重里也能派上用场。用 SETNX 把 orderNo 作为 key 存一个标识回调处理前尝试加锁如果加锁失败说明已经处理过直接返回成功。这个方法比查数据库判断状态更高效还能有效防止并发回调时的脏写。5. 前端设计与接口联调分层渲染还是前后端分离前端这块有两种主流做法一种是用 Thymeleaf 模板引擎做服务端渲染另一种是 Vue Element UI 做前后端分离。对于校园点餐这种管理系统我个人的建议是如果目标是快速出成果、应付答辩直接用 Thymeleaf 最省事Spring Boot 内置支持不用另起前端服务如果目标是做出一个像样的多端系统以后还想扩展到小程序端那就用 Vue 写独立前端项目后端只出 JSON 接口。5.1 基于 Vue 的前端架构与核心页面结构前端项目用 Vue 2 还是 Vue 3取决于你熟悉的程度。Vue 3 是当下主流组合式 API 开发起来更清爽。项目初始化用 Vite配代理解决接口跨域问题// vite.config.js export default defineConfig({ plugins: [vue()], server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })页面大概需要这些用户端登录注册页、首页店铺列表/轮播、菜单页分类菜品列表、购物车页、订单结算页、支付页、订单列表页、订单详情页、个人中心页。商家端工作台新订单提醒、菜品管理页表格弹窗编辑、订单管理页、营业统计页。管理端用户列表页、商家审核页、菜品分类页、订单总览页。路由配置里要注意权限控制前端路由守卫根据本地存储的 token 和角色做基础的页面跳转拦截。后端接口的权限校验才是真正的安全防线前端拦截只是优化体验。5.2 接口返回格式统一前端联调不吵架的关键接口格式统一是开发效率的关键。我习惯用统一的响应结构{ code: 200, message: success, data: {} }用全局异常处理器把业务异常转成标准格式。定义好公共返回对象之后前端可以封装统一的 axios 拦截器code 非 200 时直接弹出错误提示不用每个接口单独处理错误逻辑。5.3 分页查询与搜索交互列表页性能优化菜品列表和订单列表离不开分页。MyBatis-Plus 内置分页插件配置很简单Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }查询订单列表时建议只查主表字段不要关联商家表和用户表。需要展示商家名称或用户昵称时一次性收集对应的 id 再批量查或者直接在订单表冗余一个商家名称字段和用户昵称字段。冗余虽然听起来不够“范式规范”但在这种业务场景里减少联查对查询性能的提升是立竿见影的。6. 项目部署上线从本地跑通到校内公网可访问很多学生的项目最后都卡在部署环节代码写得再漂亮跑不起来等于零。Spring Boot 项目的部署方式特别多我推荐最朴素且最稳定的方式打成 jar 包用 systemd 或 nohup 跑在服务器上配合 Nginx 做反向代理。6.1 环境准备与打包构建打包之前要先把 application.yml 里的数据库地址改成生产环境的地址。用 Maven 打包执行命令mvn clean package -DskipTests打包完成后target 目录下会生成一个可执行 jar 包。启动命令nohup java -jar campus-order-system.jar --spring.profiles.activeprod app.log 21 这里建议配置文件做多环境区分application-dev.yml 用于本地开发application-prod.yml 用于生产。如果数据库地址、Redis 地址这些信息不想硬编码在配置文件里可以加上环境变量引用用 ${DB_HOST} 这种占位符部署时通过 export 设置环境变量。6.2 Nginx 反向代理配置与前后端分离部署Nginx 在这里的核心作用有两个一是静态文件托管二是 API 反向代理。前端 Vue 项目打包成静态文件后直接交给 Nginx 托管server { listen 80; server_name your-domain; # 前端静态文件 root /var/www/campus-order-front; index index.html; location / { try_files $uri $uri/ /index.html; } # 后端 API 反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }注意 try_files 这行的作用。Vue Router 默认用的是 History 模式刷新页面时 Nginx 会在文件系统里找对应的路径找不到就会返回 404所以必须让所有未知路由都重写到 index.html由前端路由接管。6.3 部署后的例行检查与数据备份项目上线不是终点持续稳定运行才是目标。建议定期做这几件事每天凌晨自动备份 MySQL 数据库备份命令用 mysqldump 就好备份文件保留最近 7 天定期查看 app.log 和 Nginx 的 error.log排查后台异常每周检查一次磁盘空间防止日志文件把服务器空间占满。给服务器配一个简单的定时任务比如每天凌晨 3 点执行数据库备份的 shell 脚本就能省下大量手工运维的时间。7. 常见问题与排查技巧实录这个系统开发过程中踩过的坑、帮人排查过的问题确保已经记录在案。下面这些情况你在做类似项目时大概率也会碰到提前给你打个预防针。7.1 接口报 500 错误的排查思路All bills are looked up step by step先看后端的启动日志定位是哪一行代码抛出异常。比如常见的数据库连接失败检查 MySQL 服务是否启动用户名密码是否正确远程连接权限是否开启。再比如 mybatis-plus 查询时报 invalid bound statement多半是 Mapper 接口和 XML 文件的命名空间不匹配。遇到这种错误别慌先看控制台最前面的几行堆栈信息十有八九能定位到问题。7.2 前端请求跨域报错的解决记录前后端分离开发时Vite 代理配置正确一般不会出现跨域问题。但如果后端直接暴露接口调试就会报 Access-Control-Allow-Origin 错误。解决办法是后端加一个 CORS 配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(Registry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这种方式只能用于开发环境上线后必须由 Nginx 反向代理接管关掉后端的跨域配置。注意 allowCredentials(true) 时allowedOriginPatterns 不能用 *否则某些浏览器版本下会直接拒绝响应。7.3 应用启动慢与内存吃紧的问题排查Spring Boot 项目内嵌 Tomcat本身启动就比传统 Servlet 容器慢。如果服务器内存只有 1G可以在启动命令里加上 JVM 参数限制内存java -Xms256m -Xmx512m -jar campus-order-system.jar如果启动还是很慢检查是不是把 Redis 的序列化器配置错了导致每个缓存对象序列化传输特别慢。Redis 的 key 和 value 序列化方式建议统一用 StringRedisSerializer 或 GenericJackson2JsonRedisSerializer避免默认 JDK 序列化产生乱七八糟的乱码前缀。7.4 订单状态不一致问题排查点餐系统最常见的故障就是订单状态和数据对不上。排查第一步查 Redis 或数据库中是否存在脏数据比如订单状态已显示“已完成”但支付时间字段为空。第二步看回调接口有没有做幂等性校验。第三步看前端有没有做竞态处理。一个非常经典的坑是用户在支付成功后立刻刷新页面前端用初始的订单状态把后端更新的新状态覆盖了。解决办法是在前端路由跳转后用最新接口刷新订单状态不要依赖本地缓存的状态值。7.5 测试与验收课设答辩时怎么展示项目测试阶段建议造一批真实的测试数据5 个商家、每个商家 20 个菜品、10 个测试学生账号、50 条不同状态的订单。这样演示的时候打开商家端能看到琳琅满目的菜品列表打开管理端能看到各种统计报表评委浏览体验会好很多。答辩时先展示项目结构再演示核心流程重点演示用户登录 → 浏览商家 → 加购物车 → 结算 → 支付 → 商家接单 → 出餐 → 完成订单。整套流程走完整个系统的完整性就立住了。8. 经验总结与后续扩展方向这个系统我自己做下来最深的体会是把基础功能做扎实远比堆砌花哨功能有价值。校园点餐系统的核心就在订单状态的准确流转、并发场景下的稳定性、角色权限的清晰控制这三个点上把它们打磨透了项目就能拿得出手。技术扩展上后续可以加的方向很多。比如接入导航地图把餐品配送轨迹做出来引入 WebSocket让商家新订单提醒和用户订单状态更新更加实时加一个用户积分体系通过消费次数发放优惠券做一个小程序端学生从小程序直接下单使用体验比 H5 页面更顺畅。数据库层面如果真的要支撑全校几千人同时用可以考虑读写分离但这不是必须的先把单机性能优化到极致再说。最后再分享一个小细节我在项目里通过 AOP 做了统一的操作日志记录。任何一个管理员或者商家执行了关键操作比如审核商家、上下架菜品、手动改订单状态都会在日志表里留一条记录。上线一段时间之后回头看这些日志是排查问题、追溯责任的最好材料。很多新手项目不做这一块等真正上线出了问题查半天都不知道是谁在什么时候改了什么数据。这个习惯一定要从第一个项目就养成。写到这里整个校园点餐系统从设计、编码到部署、排查的完整链路已经打通了。如果你正在开发同类项目希望能照着这个思路一步一步落地。代码不在多关键业务链路写稳了你从中获得的经验比项目的“完成度”更有价值。