最近在帮学弟学妹看毕业设计,发现很多同学选择用 Spring Boot 做订餐系统,想法很好,但做出来的东西往往“能跑就行”,离一个合格的工程项目还有不小差距。我自己也经历过这个阶段,所以想结合经验,聊聊怎么从零搭建一个结构清晰、代码规范、考虑周全的订餐系统,希望能帮你避开那些常见的“坑”。
1. 先聊聊新手最容易踩的“坑”
很多同学一开始热情很高,直接上手写代码,结果项目越写越乱。我总结了几类最常见的问题:
- 没有事务管理:用户下单涉及扣库存、生成订单、更新用户余额等多个操作。如果不用事务,万一中间步骤失败,数据就“半吊子”了,比如库存扣了但订单没生成,这绝对是重大缺陷。
- 到处都是硬编码:比如把“管理员”角色ID
1、订单状态“待支付”0直接写在业务逻辑里。一旦数据库里这些值变了,你得把所有代码翻个遍去改,维护起来简直是噩梦。 - 异常处理一团糟:满屏的
try-catch,或者干脆不处理,让异常直接抛给用户,显示一堆看不懂的英文错误栈。这不仅体验差,也给系统安全埋雷。 - 接口设计随心所欲:
/getUser、/addNewFood、/delete_order_by_id…… 命名不规范,GET、POST乱用,返回的数据格式也五花八门。 - 忽视安全性:用户密码用明文存数据库,SQL语句用字符串拼接(坐等被SQL注入),接口谁都能调用。
如果你的项目有上述任何一点,那可得好好优化一下了。下面我们就一步步来构建一个更规范的系统。
2. 技术栈选型:为什么是它们?
技术选型不是追新,而是选择最适合、最稳定、学习资料最丰富的组合。对于本科毕设的订餐系统,我强烈推荐Spring Boot + MyBatis + MySQL + JWT这个“黄金组合”。
- Spring Boot vs 传统SSM:Spring Boot的“约定大于配置”理念,能让你免去大量繁琐的XML配置,快速搭建一个可运行的Web应用。这对于时间有限的毕设来说,是巨大的效率提升。
- MyBatis vs JPA (Hibernate):这是一个关键选择。JPA更“面向对象”,自动化程度高,但学习曲线稍陡,对于复杂SQL的优化需要更多理解。MyBatis则更“面向SQL”,你需要自己写SQL语句,灵活性极高,也更容易理解和调试。对于业务逻辑相对固定但又有复杂查询(如多表关联查订单详情)的订餐系统,MyBatis能让你的控制力更强,也更容易写出高效的查询。而且,MyBatis的学习成本对新手更友好。
- MySQL:关系型数据库的绝对主流,资料多,生态成熟。订餐系统的数据(用户、菜品、订单)关系明确,用MySQL非常合适。
- JWT (JSON Web Token) vs Session:传统的Session需要服务器存储用户状态,在集群环境下比较麻烦。JWT是一种无状态的认证方式,用户信息加密在Token里,每次请求带上即可。它更适合前后端分离的项目,也是目前的主流实践。
简单说,这个组合能让你把精力集中在业务逻辑上,而不是配置和底层细节。
3. 核心模块实现与Clean Code
我们挑几个最核心的模块,看看怎么写代码才算是“干净”的。
3.1 用户认证(JWT实现)
首先,别明文存密码!用Spring Security的BCryptPasswordEncoder进行哈希加密。
@Service public class UserServiceImpl implements UserService { @Autowired private BCryptPasswordEncoder passwordEncoder; @Override public void register(User user) { // 注册时对密码进行加密存储 user.setPassword(passwordEncoder.encode(user.getPassword())); userMapper.insert(user); } @Override public String login(String username, String password) { User user = userMapper.selectByUsername(username); if (user != null && passwordEncoder.matches(password, user.getPassword())) { // 生成JWT Token (需引入jjwt库) return JwtUtil.generateToken(username); } throw new BusinessException("用户名或密码错误"); } }3.2 菜品CRUD与MyBatis使用
使用MyBatis的注解或XML映射文件。这里展示注解方式,更简洁。
@Mapper public interface DishMapper { // 插入并返回自增主键 @Insert("INSERT INTO dish(name, price, stock, description) VALUES(#{name}, #{price}, #{stock}, #{description})") @Options(useGeneratedKeys = true, keyProperty = "id") int insert(Dish dish); // 使用Provider实现动态SQL更新(避免更新null字段) @UpdateProvider(type = DishSqlProvider.class, method = "updateSelective") int updateSelective(Dish dish); // 复杂的多条件分页查询建议使用XML,这里用注解简单示例 @Select("<script>" + "SELECT * FROM dish WHERE 1=1 " + "<if test='name != null'> AND name LIKE CONCAT('%', #{name}, '%')</if>" + "<if test='minPrice != null'> AND price >= #{minPrice}</if>" + "<if test='maxPrice != null'> AND price <= #{maxPrice}</if>" + " ORDER BY id DESC" + "</script>") List<Dish> selectByCondition(DishQuery query); }3.3 下单流程与事务管理(重点!)
这是业务核心,必须保证数据一致性。
@Service @Transactional(rollbackFor = Exception.class) // 声明式事务,任何异常都回滚 public class OrderServiceImpl implements OrderService { @Autowired private DishMapper dishMapper; @Autowired private OrderMapper orderMapper; @Override public Order createOrder(OrderCreateRequest request) { // 1. 校验菜品是否存在及库存 Dish dish = dishMapper.selectById(request.getDishId()); if (dish == null) { throw new BusinessException("菜品不存在"); } if (dish.getStock() < request.getQuantity()) { throw new BusinessException("库存不足"); } // 2. 扣减库存(使用乐观锁或悲观锁防止超卖) int updatedRows = dishMapper.decreaseStock(request.getDishId(), request.getQuantity()); if (updatedRows == 0) { // 乐观锁更新失败,说明库存已被其他请求修改,通常需要重试或直接失败 throw new BusinessException("下单失败,请重试"); } // 3. 计算总价(应在后端计算,避免前端传值被篡改) BigDecimal totalPrice = dish.getPrice().multiply(new BigDecimal(request.getQuantity())); // 4. 生成订单 Order order = new Order(); order.setUserId(SecurityUtil.getCurrentUserId()); // 从安全上下文中获取当前用户 order.setDishId(dish.getId()); order.setQuantity(request.getQuantity()); order.setTotalAmount(totalPrice); order.setStatus(OrderStatus.WAITING_FOR_PAYMENT); // 使用枚举,而非魔法数字 orderMapper.insert(order); // 5. 后续可能还有创建支付记录等操作... return order; } }对应的Mapper方法,使用乐观锁:
@Update("UPDATE dish SET stock = stock - #{quantity}, version = version + 1 WHERE id = #{dishId} AND stock >= #{quantity}") int decreaseStock(@Param("dishId") Long dishId, @Param("quantity") Integer quantity);4. 安全与性能:不能忽视的底线
4.1 安全性
- SQL注入防护:坚持使用MyBatis的
#{}预编译占位符,永远不要用${}进行字符串拼接来拼接用户输入。 - 密码加密:前面说了,用
BCryptPasswordEncoder。 - 接口鉴权:使用Spring Security或拦截器,配合JWT,对需要登录的接口进行拦截。区分用户角色(如普通用户和管理员),实现访问控制。
- XSS防护:对用户输入(如评论、地址)进行转义或过滤,或者使用像Thymeleaf这样的模板引擎,它们通常有自动转义功能。
- 日志脱敏:在打印日志时,敏感信息如手机号、身份证号、密码等,一定要进行部分屏蔽(如
138****1234)。
4.2 性能
- 数据库连接池:Spring Boot默认使用HikariCP,性能很好。但在
application.yml中最好根据实际情况调整一下参数,比如最大连接数、超时时间等,别用默认值上生产环境(虽然毕设可能不部署)。 - 避免N+1查询:这是ORM中常见的性能问题。比如查询订单列表时,每条订单都要再发一次SQL查询用户信息。解决方法是使用MyBatis的
<association>或<collection>进行关联查询,一次SQL查出所有需要的数据。 - 接口限流/降级:对于高频接口(如查询菜品列表),可以考虑使用Guava的
RateLimiter或引入Sentinel进行简单的限流,防止被意外刷爆。这在毕设中是个加分项。 - 索引优化:在数据库表中,为经常作为查询条件的字段(如
user_id,dish_id,create_time)建立索引,能极大提升查询速度。
5. 生产环境思维:一些实用的避坑指南
即使只是毕设,用生产环境的标准要求自己,收获会大不一样。
- 配置文件分离:把
application.yml拆成application-dev.yml(开发)、application-prod.yml(生产)。用spring.profiles.active来切换。生产环境的数据库密码、密钥等绝不能写在代码里。 - 统一的响应封装:设计一个像
Result<T>这样的类,包含code、msg、data字段。所有控制器都返回它,这样前端处理起来格式统一。 - 全局异常处理器:用
@ControllerAdvice和@ExceptionHandler捕获和处理各种异常,返回友好的错误信息,而不是一堆红色错误栈。 - 善用日志:合理使用
@Slf4j注解记录日志,不同级别(INFO, WARN, ERROR)用于不同场景。排查问题时,日志是你的第一手资料。 - 接口文档:一定要写!用 Swagger 或 Knife4j 自动生成API文档,省去前后端扯皮的时间,也显得项目更专业。
- 单元测试:为Service层的核心逻辑(如下单、扣库存)编写单元测试(JUnit),保证代码质量,修改代码时也有底气。
写在最后
按照上面的思路走下来,你的订餐系统应该已经是一个结构清晰、代码规范、考虑了一定的安全性和性能的“正经项目”了,足够让你在答辩时从容不迫。
如果你想再进一步,提升项目的深度和亮点,我建议可以思考或尝试这两个方向:
- 扩展为微服务架构:如果订单服务压力大,能否把“用户服务”、“菜品服务”、“订单服务”拆分开?拆开后服务之间如何调用(Feign/RestTemplate)?如何管理配置(Nacos)?如何实现认证(OAuth2 + Gateway)?这能让你对分布式系统有初步认识。
- 模拟支付回调:实现一个完整的支付闭环。用户下单后,跳转到一个模拟的支付页面,支付成功后,模拟的第三方支付平台如何回调你的系统?你的回调接口如何保证幂等性(防止重复处理)?如何安全地验证回调请求?这个流程在电商系统中非常经典。
毕业设计不仅是完成任务,更是一次宝贵的工程实践。希望这篇指南能帮你少走弯路,做出一个让自己满意、也让老师眼前一亮的作品。加油!