ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Spring Boot外卖点餐系统:从订单并发到权限控制的完整实战解析

Spring Boot外卖点餐系统:从订单并发到权限控制的完整实战解析 简介一份基于Spring Boot框架结合Mysql、Java与JSP技术的外卖点餐系统毕业设计项目面向计算机、通信、人工智能、自动化等专业的本专科学生及从业者适合用于期末课程设计、课程大作业或毕业设计参考。系统完整覆盖管理员、商家、用户、骑手四类角色包含菜品分类管理、订单管理、配送单管理、商品评价管理、我的收藏管理等核心业务模块能够较清晰展示前后端交互与数据库设计思路。压缩包整体大小约35.41MB内含可运行源码、数据库脚本、开发文档、LW论文及PPT等内容代码均经过调试测试可直接运行。目前已有98人学习浏览项目结构清晰具备较高的学习借鉴价值既适合初学者入门理解Spring Boot项目构建也可供有一定基础者在此基础上扩展功能或二次开发。1. 外卖点餐系统一份Spring Boot单体项目的完整解剖很多同学拿到外卖点餐系统的源码第一步是启动项目看到登录页就点几个按钮然后写进报告。但真正影响答辩成绩的往往是按钮背后那几个问题订单状态在并发操作下会不会丢失、点单时价格能不能被客户端篡改、骑手接单之后商家还能不能继续操作。这套基于Spring Boot MySQL Java JSP的外卖点餐系统覆盖了管理员、商家、用户、骑手四个角色功能链路上有菜品管理、订单流转、配送单分配、商品评价正好把这些容易被忽略的细节全部暴露出来。对课程设计、毕业设计来说是足够完整的案例对想复习Spring Boot核心机制的从业者也有明确的参考坐标。2. 技术栈定位与项目结构Spring Boot JSP MySQL的配合方式2.1 这套技术栈为什么还能打Spring Boot 2.x之后JSP在官方文档里已经退居边缘但在实际教学场景和中小型内部系统中它依然有稳定的存量市场。核心原因有三个第一Spring Boot的自动配置把数据源、事务管理器、视图解析器的装配成本降到了最低一个application.yml就能把所有运行参数钉死第二JSP服务端渲染天然没有跨域问题页面直出、源码可见适合课程设计和期末大作业这种需要“把代码讲清楚”的交付场景第三MySQL在Windows和Linux下的安装配置教程非常多环境复现成本低老师拿到项目也能快速跑起来。从面试角度来看Spring Boot面试题里常考的自动配置原理、Transactional失效场景、拦截器与过滤器的区别在这个项目里都有明确的落点。比如订单提交接口上不加Transactional多表更新就会处于“部分成功”的不一致状态拦截器配置不对未登录用户直接访问/user/order/list就会暴露数据。这套项目不是只用来演示CRUD而是能帮你把Spring Boot的核心机制串起来。2.2 目录结构与分层约定项目包结构是标准的Controller-Service-Mapper三层src/main/java ├── com.example.order │ ├── controller │ ├── service │ │ └── impl │ ├── mapper │ ├── entity │ └── config src/main/resources ├── mapper ├── static └── application.yml src/main/webapp └── WEB-INF └── jsp ├── user ├── merchant ├── admin └── riderController层只做参数接收和结果封装业务逻辑全部下沉到Service实现类Mapper层通过MyBatis的XML文件操作MySQL。这里有一个容易被新手踩的坑JSP页面必须放在src/main/webapp/WEB-INF/jsp目录下而不是resources/templates。templates是Thymeleaf等模板引擎的默认目录Spring Boot默认不会处理放在那里的JSP文件启动时不报错访问时直接404。application.yml里的核心配置如下server: port: 8080 servlet: jsp: init-parameters: development: true spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/order_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 mvc: view: prefix: /WEB-INF/jsp/ suffix: .jsp mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.order.entityspring.mvc.view.prefix和suffix决定了Controller返回字符串时JSP的查找路径比如return user/list实际解析到/WEB-INF/jsp/user/list.jsp。development: true开启JSP热加载改完页面不用重启Tomcat就能看到效果这是调试页面的利器但部署到生产环境必须关掉否则每次请求都会重新编译JSP性能损耗明显。2.3 依赖配置JSP集成Spring Boot的边界条件很多人在Spring Boot里用JSP失败问题出在依赖缺失。这个项目的pom.xml里需要同时引入两个关键依赖dependency groupIdorg.apache.tomcat.embed/groupId artifactIdtomcat-embed-jasper/artifactId /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId /dependencytomcat-embed-jasper负责把JSP编译成Servlet并执行jstl提供c:forEach、c:if等标签库。少了前者项目能启动但访问页面返回500少了后者JSP里用到JSTL标签直接编译失败。如果使用IDEA创建Spring Boot项目时选择了Jar打包方式还要注意pom.xml里的packaging必须改成war否则WEB-INF目录压根不会进入构建产物启动后同样找不到页面。3. 数据库设计与表关联订单、配送、评价如何闭环3.1 核心表结构与字段选型拿到源码后数据库脚本是整个资源里最先要看的内容。这个外卖系统涉及用户、商家、骑手、管理员四个角色以及菜品、订单、配送单、评价、收藏五类业务数据。先拆三张核心表的设计逻辑。用户表CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录名, password varchar(100) NOT NULL COMMENT 密码MD5加密存储, nickname varchar(50) DEFAULT NULL, phone varchar(20) DEFAULT NULL, address varchar(200) DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;菜品表CREATE TABLE dish ( id bigint(20) NOT NULL AUTO_INCREMENT, merchant_id bigint(20) NOT NULL COMMENT 所属商家ID, category_id bigint(20) NOT NULL COMMENT 分类ID, name varchar(100) NOT NULL, price decimal(10,2) NOT NULL, image varchar(255) DEFAULT NULL, status tinyint(4) DEFAULT 1 COMMENT 1上架 0下架, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_merchant_id (merchant_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;两个关键选型细节字符集用utf8mb4而不是utf8因为utf8在MySQL里最多存3字节遇到生僻字或表情符号会直接报错价格字段用decimal(10,2)而不是float或double浮点数在金额累计时存在精度丢失0.1 0.2 ! 0.3的问题在订单总额计算里会被放大。这两个点既影响功能正确性也是数据库设计答辩时的高频提问拿decimal和浮点对比说事比背结论有说服力。3.2 订单明细与配送单的快照设计订单表是整个业务网的枢纽它的上下游关系如下orders(id, user_id, merchant_id, order_no, total_amount, status, create_time) order_item(id, order_id, dish_id, dish_name, price, quantity) delivery(id, order_id, rider_id, status, pickup_time, complete_time) comment(id, order_id, user_id, merchant_id, rating, content, create_time)order_item表里冗余了dish_name和price。菜品在后台被修改名称或调整价格之后历史订单的明细显示必须保持下单那一刻的快照如果联表实时查菜品表订单页面显示的就是改过的名字和价格这是业务上不能接受的。这种冗余字段的做法在电商系统里叫“快照存储”课程设计里能自己解释清楚这个设计意图说明不是纯抄代码。配送单和订单是一对一关系通过order_id外键关联。配送单的核心状态是待接单、配送中、已完成。骑手端看到的数据完全来自这张表商家端无法干预配送单的状态变更这种表结构层面的职责隔离比在代码里做判断要可靠得多。3.3 索引设计按状态与时间检索的查询优化后台订单管理的核心操作是“按状态查今天的订单”这类的SQL频率最高需要索引支撑ALTER TABLE orders ADD INDEX idx_status_create (status, create_time); ALTER TABLE delivery ADD INDEX idx_rider_status (rider_id, status);idx_status_create覆盖了“查待接单订单”“查已完成订单”这两类高频场景。索引左前缀原则下单查status也能命中性能优于只给create_time加索引。idx_rider_status让骑手查询自己名下待配送订单时快速定位数据量超过十万行后效果明显。MySQL调优面试题里经常会问联合索引和最左匹配拿这个真实表结构举例比背八股文印象深刻。4. 核心链路实现下单、接单、配送的状态流转与权限收敛4.1 状态机设计与并发安全外卖订单的生命周期必须用状态机约束不能允许订单从“配送中”直接跳到“已完成”以外的状态。这张表定义了这个项目里的全部状态流转状态值含义允许流转到0待接单1、51待配送2、52配送中3、53已完成44已评价无5已取消无状态流转在代码里不能直接写成UPDATE orders SET status 1而是要在更新条件里带上当前状态保证并发场景下状态不被覆盖。Service层实现如下Override Transactional(rollbackFor Exception.class) public boolean confirmOrder(Long orderId, Integer expectStatus, Integer targetStatus) { int rows orderMapper.updateStatusIfMatch(orderId, expectStatus, targetStatus); return rows 0; }对应SQLUPDATE orders SET status #{targetStatus} WHERE id #{orderId} AND status #{expectStatus}WHERE status #{expectStatus}是乐观锁的简化实现。商家PC端和骑手手机端同时操作同一个订单时只有一个操作能匹配到当前状态另一个的rows返回0业务层可以明确感知冲突并给出“订单状态已变化请刷新页面”的提示。很多课程设计在状态更新时不加这个条件数据量小的时候没事一到多人联调测试就出现状态互相覆盖的问题。4.2 下单接口事务边界与价格信任问题下单是链路最长的操作涉及订单主表插入、订单明细批量插入两个步骤必须放在同一个事务里PostMapping(/api/order/submit) ResponseBody public Result submit(RequestBody OrderSubmitDTO dto, HttpSession session) { User user (User) session.getAttribute(user); Order order new Order(); order.setUserId(user.getId()); order.setMerchantId(dto.getMerchantId()); order.setOrderNo(generateOrderNo()); BigDecimal total calculateTotal(dto.getItems()); order.setTotalAmount(total); order.setStatus(0); orderMapper.insert(order); ListOrderItem items dto.getItems().stream().map(item - { OrderItem oi new OrderItem(); oi.setOrderId(order.getId()); oi.setDishId(item.getDishId()); Dish dish dishMapper.selectById(item.getDishId()); oi.setDishName(dish.getName()); oi.setPrice(dish.getPrice()); oi.setQuantity(item.getQuantity()); return oi; }).collect(Collectors.toList()); orderItemMapper.batchInsert(items); return Result.success(order); }generateOrderNo()生成订单号建议用时间戳加用户ID加随机数拼成保证高并发同秒内不冲突。价格计算calculateTotal的前置条件是菜品单价必须从数据库读取前端传过来的价格字段不能信任。如果直接累加前端传参的price攻击者用Postman改一个请求就能把十元的菜变成一分钱下单。接口开发的原则是“前端传ID后端查价格”这个项目里dishId里查dish.getPrice()的写法本身就是正确的防御姿态。事务边界也很关键Transactional同时覆盖订单主表插入和明细批量插入任何一个失败都会回滚避免出现“有订单头没有明细行”的脏数据。这里还要注意Transactional的默认回滚条件是RuntimeException如果业务里捕获了异常但没有抛出事务就失效了。4.3 多角色权限拦截器与数据级过滤系统有管理员、商家、用户、骑手四种角色权限边界差异大。项目用的是拦截器加角色校验的方式在config包下注册Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new UserInterceptor()) .addPathPatterns(/user/**) .addPathPatterns(/order/**) .excludePathPatterns(/login, /register, /dish/list); registry.addInterceptor(new MerchantInterceptor()) .addPathPatterns(/merchant/**); } }拦截器统一校验Session中保存的角色标识未登录或角色不匹配直接重定向到登录页。excludePathPatterns放行了登录、注册和菜品浏览接口这三个路径不需要认证。拦截器方案在Spring Boot单体项目里性能好、代码直观比引入Spring Security配置更轻量适合课程设计场景。但拦截器只能做到URL级别的权限控制管不了“商家A修改商家B的菜品”这种数据越权。所以Service层还要做数据级过滤商家端修改菜品时强制把当前登录商家的ID作为条件拼进UPDATE语句的WHERE保证只能操作自己的数据。URL级权限用拦截器、数据级权限在SQL层拼ID这种组合在单体项目里是最实用的权限收敛方案。5. 部署验证与坑位清单从Tomcat启动到JSP页面修复5.1 环境准备与数据库导入环境要求是JDK 8或11、MySQL 5.7或8.0、Maven 3.6以上。MySQL安装好后用命令行直接导入源码里的SQL脚本mysql -uroot -p123456 order_db.sql导入后查一下user表里是否已有测试账号没有就手动插入一条。启动项目用Maven命令mvn clean package -DskipTests -Pwar java -jar target/order-system.war也可以直接用IDEA运行Application.java的main方法。浏览器访问http://localhost:8080能看到登录页说明Tomcat和Spring Boot都正常工作。如果MySQL是8.0以上版本注意application.yml里的驱动类名必须是com.mysql.cj.jdbc.Driver旧驱动会出现ClassNotFoundException。5.2 三类高频报错的快速定位报错现象原因解决方案页面404启动无异常JSP文件放到了resources/templates移动到webapp/WEB-INF/jsp页面500日志提示JasperException缺少tomcat-embed-jasper依赖在pom.xml中补齐依赖数据库连接失败MySQL时区不匹配url加serverTimezoneAsia/ShanghaiJSP编译报错还有一个隐蔽场景c:forEach标签无法解析。控制台会报“According to TLD, tag forEach is invalid”此时检查JSP页面顶部是否漏了% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %。这个标签引入是JSP页面使用JSTL的前提漏掉它整个页面直接编译不过属于新手高频失误。5.3 用JMeter验证订单接口的并发安全跑通编辑器之后建议做一次并发下单价测试用JMeter创建一个50线程的线程组对/api/order/submit接口持续加压60秒。重点关注两个指标一是响应时间分布观察P90是否超过2000毫秒二是数据库里orders表的订单号是否唯一、order_item是否每条订单都有明细。如果出现“订单主表插入成功但明细缺失”的情况说明事务没有生效如果订单号重复说明generateOrderNo()的随机段取值范围需要加大。把JMeter的聚合报告里的TPS和异常率截图放进课程设计文档里比贴几行CRUD代码更有说服力。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进