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、role(student、merchant、admin)、status(正常/禁用)、create_time。
商家表可以独立出来,也可以和用户表通过 role 区分。我建议独立一张 shop 表:id、merchant_id(外键关联用户表)、shop_name、shop_logo、notice、delivery_fee、起送价 min_price、营业状态 open_status、评分 score。
订单主表 orders:id、order_no(订单号,唯一)、user_id、shop_id、total_amount、pay_amount、discount_amount、status(待支付/已支付/已接单/配送中/已完成/已取消/退款中)、pay_time、finish_time、remark、create_time。订单明细表 order_detail:id、order_id、food_id、food_name(冗余菜品名称,防止菜品被删除后订单显示异常)、food_image、price(下单时的价格快照)、quantity、sub_total。
菜品表 food:id、shop_id、category_id、food_name、food_description、cover_image、price、stock、sales、status(上架/下架)、create_time。
分类表 category 建议做成两级,一级分类比如“家常菜”“面食”“饮品”,二级可以是具体菜品名,但不要无限嵌套,增加复杂度却没有实际收益。
还要单独说一个很容易被忽略的表:购物车表 cart。校园点餐的购物车和电商购物车不太一样,时效性非常强,用户可能中午下单前加购,吃完后购物车就清空。所以设计上可以直接用 Redis 存,用哈希结构 key 为 userId,field 为 foodId,value 为数量。这样性能好,而且天然有过期时间,省一张 MySQL 表。但如果你希望用户在不同设备上购物车同步,那就得老老实实建一张 MySQL 表,按 user_id 和 food_id 做唯一索引。
2.2 一张表看懂订单状态流转
订单状态是整个点餐系统里最容易出错的地方,先看下状态机:
| 状态码 | 状态名称 | 触发动作 | 下一状态 |
|---|---|---|---|
| 0 | 待支付 | 用户下单成功 | 支付成功 → 1;超时 → 6 |
| 1 | 已支付/待接单 | 用户完成支付,推送给商家 | 商家接单 → 2;商家拒单 → 7 |
| 2 | 已接单/制作中 | 商家确认接单 | 出餐完成 → 3 |
| 3 | 待取餐/配送中 | 商家出餐,通知用户 | 用户确认取餐 → 4 |
| 4 | 已完成 | 用户确认取餐或超时自动确认 | 终态 |
| 5 | 已取消 | 用户取消或系统自动取消 | 终态 |
| 6 | 退款中 | 仅已支付状态发起 | 退款成功 → 7 |
| 7 | 已退款/已关闭 | 商家拒单/用户退款 | 终态 |
这个状态机要在代码里严格约束,每个状态只允许特定动作触发转移。比如已取消的订单绝不能再次进入“已支付”,否则对账的时候会乱成一团。实现上,可以在后端定义一个状态流转校验工具类,把合法流转关系配成 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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>4.4.0</version> </dependency> </dependencies>数据库连接配置直接写在 application.yml 里:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/campus_order?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/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 支持,不用额外引入组件。但如果你希望系统能支持前后端分离,以后多端共用一套接口,JWT(JSON 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. 遍历菜品,计算金额,校验库存 List<OrderDetail> 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:categoryId,value 是 JSON 字符串,过期时间 30 分钟。
缓存更新策略用最简单的 Cache Aside 模式:查询菜品时先读缓存,缓存没命中再查数据库并回填;商家修改菜品、上下架菜品时,主动删除对应商家的缓存 key。这里要注意删除缓存和更新数据库的顺序,一定要先更新数据库成功,再删除缓存,避免缓存先删了、数据库还没更新完成,用户读到旧数据的情况。
4.2 购物车用 Redis 还是 MySQL
购物车我建议直接采用 Redis 的 Hash 结构,key 是 cart:userId,field 是 foodId,value 是数量。核心操作代码:
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.active=prod > app.log 2>&1 &这里建议配置文件做多环境区分,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 做了统一的操作日志记录。任何一个管理员或者商家执行了关键操作(比如审核商家、上下架菜品、手动改订单状态),都会在日志表里留一条记录。上线一段时间之后回头看这些日志,是排查问题、追溯责任的最好材料。很多新手项目不做这一块,等真正上线出了问题,查半天都不知道是谁在什么时候改了什么数据。这个习惯一定要从第一个项目就养成。
写到这里,整个校园点餐系统从设计、编码到部署、排查的完整链路已经打通了。如果你正在开发同类项目,希望能照着这个思路一步一步落地。代码不在多,关键业务链路写稳了,你从中获得的经验比项目的“完成度”更有价值。