ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring Boot校园外卖点餐系统毕设实战:数据库设计、订单状态机与答辩避坑指南

Spring Boot校园外卖点餐系统毕设实战:数据库设计、订单状态机与答辩避坑指南 每年毕业设计季总有一批人会直接下载一个带编号的源码包改个包名就交上去。作为帮学弟排过不少坑的人我必须说校园外卖点餐系统这类项目如果只停留在“能跑就行”答辩很容易被问住。但反过来看它又确实是Spring Boot入门和毕设的稳妥选题——业务真实、表结构经典、能覆盖登录、权限、增删改查、关联查询、订单状态流转这些核心技能。这篇文章就基于我用Spring Boot从零搭建这个系统的经历把需求拆解、技术选型、数据库设计、主链路实现和最容易翻车的细节一次讲清楚。1. 为什么“校园外卖点餐系统”是毕设的稳妥之选1.1 选题的真实理由业务闭环完整技术点刚好够用选毕设题目最怕什么最怕两类一类是纯管理系统只有CRUD做完了自己都觉得没分量另一类是一上来就搞微服务、分布式事务、消息队列结果大半年连环境都没搭明白。校园外卖点餐系统的好处在于它处在一个“中间态”用户点餐、商家接单、平台管理三方角色齐全但业务复杂度完全可控。从数据流来看一套完整的外卖系统天然包含用户表、店铺表、菜品表、购物车表、订单表、订单明细表、地址表、分类表这已经覆盖了“一对多”“多对一”几乎所有常见的表关系。订单状态从“待支付”到“已支付”“商家接单”“配送中”“已完成”又要求你理解状态机的基本思想。你会用到关联查询、条件分页、事务、枚举映射、全局异常处理这些恰好是Spring Boot开发里最高频的能力。所以这个题目不是“太简单”而是“该有的都有但都不算深”。对本科生来说能讲清楚整个业务闭环再有一两个亮点比如并发扣库存、XSS过滤、订单超时取消答辩就非常稳了。1.2 需求范围控制先砍掉不该做的功能很多同学拿到题目后第一反应是“我要做App、小程序、商家端、骑手端、管理后台、优惠券、会员积分……”。我建议你做减法。当初我第一次做这个项目也差点失控后来把需求收敛成三个端用户端登录注册、浏览菜品、加入购物车、下单、支付模拟、订单查询、地址管理、个人中心。商家端菜品管理、接单/拒单、订单状态更新、简单统计。管理后台用户管理、店铺审核、分类管理、订单查询、基础数据看板。至于骑手端和实时定位完全可以放到论文的“不足与展望”里。毕设考察的是你能否独立实现一个完整系统不是能否复刻美团外卖。功能每多一个前后端联调和数据库设计的成本会翻倍没有足够的调试时间最后交上去的反而是一个处处是Bug的半成品。1.3 源码能跑起来只是起点重要的是能讲清楚标题里带编号的毕设源码网上确实很多但坦白讲质量参差不齐。有的代码没有注释有的数据库脚本和实体类对不上有的配置文件里甚至残留着别人的数据库密码。你如果只是把源码跑起来答辩时老师随机问一个“订单状态是怎么流转的”就可能卡壳。更稳妥的思路是把源码当作参考自己动手把核心表建一遍把主流程写一遍哪怕你是在别人代码基础上重构也至少每行都能讲明白。源码编号68913本身不重要重要的是这个编号对应的项目结构能不能成为你自己知识体系的一部分。带着“我要理解它”的心态去做而不是“我要交差”的心态。2. Spring Boot 项目骨架搭建版本、依赖和分层的取舍2.1 Spring Boot 版本到底怎么选热词里反复出现“spring boot 2.7.18”“springboot版本太高”“idea不能创建springboot项目不能使用jdk1.8”这些我都遇到过。我的实际建议是如果复习资料、学校教学环境、你本地的JDK还是1.8那请你老老实实选Spring Boot 2.7.18。这个版本是目前兼容JDK 8和JDK 11的最后一个稳定大版本网上教程最多遇到问题一搜就有答案。如果非要追新用Spring Boot 3.x那JDK最低也得17同时你用的MyBatis-Plus、SpringDoc等依赖都要换成新坐标版本兼容上容易出幺蛾子。毕业设计不是公司生产环境不需要追求技术新稳定跑通比什么都重要。一个很实际的经验是创建项目的时候不要选择“从 start.spring.io 拉最新的SNAPSHOT版本”直接手动在pom.xml里锁定parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent这能避免很多因为Spring Boot版本集体变迁带来的连锁问题。2.2 核心依赖清单别什么都往pom里塞校园外卖点餐系统最常用的依赖其实不多我的pom.xml里一般只保留这些依赖组作用说明lombok简化实体类代码基础必加但要注意团队规范spring-boot-starter-webWeb开发基础内置Tomcatmybatis-plus-boot-starter数据库ORM和分页比JPA更容易控制SQLmysql-connector-javaMySQL驱动版本要和MySQL匹配spring-boot-starter-data-redis缓存/购物车/分布式锁不强制但能加分jjwtJWT登录令牌前后端分离必备spring-boot-starter-validation参数校验必加减少冗余判断knife4j 或 springdoc接口文档答辩演示利器hutool工具类非必选但很方便不要为了凑技术栈去加RocketMQ、Elasticsearch除非你真的能说清楚它们在这个系统里解决了什么问题。当初我见过一个同学给外卖系统上了RabbitMQ问他就说“用来处理订单消息”但为什么不用数据库状态轮询他完全答不上来——这种反而会成为答辩减分项。2.3 项目分层包结构直接决定你能不能睡好觉很多毕设源码的包结构非常混乱service里写SQLcontroller里写业务逻辑一旦报错根本定位不了。这里我给出一套适合毕设的分层规范com.campus.order ├── common // 统一返回结果、异常处理、常量 ├── config // 配置类分页插件、WebMvc、Cors ├── controller // 接口层 ├── service // 业务层接口实现 ├── mapper // MyBatis-Plus的Mapper接口 ├── entity // 数据库实体 ├── dto // 接收前端的参数对象 ├── vo // 返回给前端的视图对象 └── utils // JWT、日期等工具类这样做的好处是答辩时老师问你“订单金额在哪里计算的”你能直接说“在service层先查购物车再计算总价折扣最后生成订单”而不是在controller里翻了半天。我用MyBatis-Plus比较多因为它的BaseMapper自带单表CRUD能大幅减少重复代码多表查询再手写XML。注意MyBatis-Plus版本不要和Spring Boot 3混用2.7版本的Spring Boot配mybatis-plus-boot-starter 3.5.x用起来很舒服。2.4 从application.yml开始把环境一次配顺资源目录下的application.yml我习惯拆成三份application.yml通用、application-dev.yml开发环境、application-prod.yml生产环境演示。毕设虽然不用很复杂但如果你把数据库密码写在通用配置里传到Git上被老师检查到是很扣分的。关键的配置项主要有这些spring: datasource: url: jdbc:mysql://localhost:3306/campus_order?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath*:mapper/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0注意serverTimezoneAsia/Shanghai这个不加本地跑没感觉部署到服务器后查出来时间差8小时容易在演示时闹笑话。还有map-underscore-to-camel-case要开否则数据库字段create_time映射到createTime会很痛苦。3. 数据库设计核心表关系梳理与实操细节3.1 表不是越多越好先看主链路需要哪几张很多参考源码动辄二十几张表看的头皮发麻。其实你从头梳理一条“用户下单”的主链路就能推导出核心表用户要登录需要user表。用户要选店铺和菜品需要shop表、category表、dish表。用户把菜加入购物车需要cart表也可以直接用Redis缓存但落库更直观。用户下单需要orders表保存订单主表需要order_detail表保存菜品快照。用户需要收货地址需要address表。商家需要管理自己的菜品shop表里加user_id字段关联商家账号。管理员需要看所有订单已经有orders表足够了。我是这样设计字段风格的主键用id bigint自增业务字段用下划线命名时间字段统一为create_time、update_time逻辑删除用deleted tinyint。以下是一张简化后的订单表结构供参考字段名类型说明idbigint主键order_novarchar(32)订单编号业务上唯一user_idbigint下单用户shop_idbigint店铺total_amountdecimal(10,2)总金额statustinyint订单状态0待支付1已支付2已接单3配送中4已完成5已取消address_idbigint收货地址remarkvarchar(255)备注pay_timedatetime支付时间create_timedatetime创建时间update_timedatetime更新时间deletedtinyint逻辑删除这里你要注意order_no不能省。订单表用自增主键没问题但对外展示和异步回调时需要一个唯一的订单号一般用“时间戳随机数”生成或者直接用雪花算法。3.2 订单状态字段不要用字符串到处飘订单状态是外卖系统的灵魂。很多新手的做法是status字段直接写字符串“已支付”“待支付”然后前端展示时显示字符串。这样问题很大一是数据库存储空间浪费二是状态判断容易写错三是没法做统计。更合理的做法是status用tinyint同时在代码里定义一个枚举类public enum OrderStatus { UNPAID(0, 待支付), PAID(1, 已支付), ACCEPTED(2, 已接单), DELIVERING(3, 配送中), COMPLETED(4, 已完成), CANCELLED(5, 已取消); private final int value; private final String desc; OrderStatus(int value, String desc) { this.value value; this.desc desc; } }这样改的好处是你在service层可以用枚举来做状态流转判断比如只有PAID状态才能ACCEPTED逻辑一目了然。答辩时老师看到你用了枚举而不是魔法数印象分会高不少。3.3 逻辑删除与时间字段这些小细节别偷懒MyBatis-Plus支持全局逻辑删除我上面给了配置。这里有个坑如果你用了逻辑删除那么所有查询MyBatis-Plus会自动追加deleted0的条件但你自己手写的XML里也要记得加条件否则会出现“已删除的数据还能查出来”的Bug。我就在踩过一次后把所有手写SQL都检查了一遍。时间字段建议直接用LocalDateTime配合MyBatis-Plus的自动填充Component public class MyMetaObjectHandler implements MetaObjectHandler { Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, createTime, LocalDateTime.class, LocalDateTime.now()); this.strictInsertFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, updateTime, LocalDateTime.class, LocalDateTime.now()); } }实体类上对应字段加TableField(fill FieldFill.INSERT)和TableField(fill FieldFill.INSERT_UPDATE)就不用每次手动set时间了。3.4 从ER设计到SQL脚本准备一份能直接导入的数据库毕设代码包里一定要附带一份campus_order.sql并且要保证这份SQL能直接导入MySQL后项目跑起来。我见过很多源码里SQL脚本是残缺的或者有外键约束顺序不对导致导入失败。最稳妥的做法是你本地把整个系统测完然后用mysqldump导出一份最新的完整脚本手动再导入测试一遍。SQL脚本里建议包含建库语句建表语句基础数据如管理员账号、测试店铺、几个菜品有这仨老师拿到项目后从零初始化很快你演示时也不用手忙脚乱地造数据。4. 主链路功能实现从登录到下单再到订单完成的完整逻辑4.1 登录认证JWT的前后端分离实践校园外卖点餐系统如果是前后端分离的登录状态不能依赖Session。我用的是JWT流程很简单用户输入账号密码后端校验通过后生成token。token里只放userId和角色信息不放大段敏感数据。前端把token存在localStorage请求时放在Authorization头里。后端写一个拦截器或过滤器解析token并放入ThreadLocal。注意两个坑第一JWT的密钥要硬编码在配置里不要写死业务代码中第二JWT是无状态的一旦签发很难主动失效所以你需要在用户表加一个status字段被禁用时拦截器里检查一下。如果不想太复杂最简单的处理是登录后把token存一份到Redis设置过期时间每次请求时校验Redis中是否存在这样管理后台就能强制退出用户了。核心拦截器代码大概长这样public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { token token.substring(7); } // 校验token Claims claims JwtUtil.parseToken(token); if (claims null) { response.setStatus(401); return false; } UserContext.setUserId(claims.get(userId, Long.class)); return true; } Override public void afterCompletion(...) { UserContext.clear(); } }4.2 点餐流程购物车、菜品库存与价格校验用户端点餐的常规路径是展示店铺分类和菜品列表用户把菜品加入购物车。购物车我建议先存Redis因为它的特点是高频读写、实时性要求高用Redis的Hash结构很舒服key: cart:userId:shopId field: dishId value: quantity这样用户切换店铺时也方便做“清空购物车”或者“提示是否清空”。但是如果你的毕设想减少复杂度购物车直接建一张cart表存数据库也完全没问题查询和修改都更直观。对于毕设来说我更倾向于数据库表方案因为论文里好画E-R图答辩也好解释“购物车数据持久化”。下单时最关键的是价格和库存的校验。很多源码只校验了“菜品是否为空”却没有校验价格是否被前端篡改。正确流程应该是根据购物车里的菜品ID重新从数据库查出单价。单价乘以数量累加成总金额。校验菜品是否在售、状态是否正常。生成订单主表和订单明细表。扣减库存。这里要特别提醒库存扣减一定不能放在前端传参里告诉后端“扣多少”而是后端根据购物车数量计算。这里我用一个简单的事务注解Transactional(rollbackFor Exception.class) public Order createOrder(Long userId, Long shopId, Long addressId, String remark) { // 1. 查询购物车 // 2. 计算总价 // 3. 保存订单主表 // 4. 保存订单明细 // 5. 扣减库存注意并发问题 // 6. 清空购物车 }4.3 支付模块模拟支付和真实支付的取舍校园外卖系统接入微信支付/支付宝对毕设来说成本和流程都很重。我建议做“模拟支付”在订单表增加pay_status字段点击“支付”后走一个模拟回调接口直接把订单状态从“待支付”改成“已支付”。这在数据库层面还是保留pay_time、transaction_id字段方便以后扩展。如果你的论文想写“接入了微信支付”那至少要有一个沙箱环境而且要有完整的回调验签逻辑。这里我不建议自研因为支付回调涉及安全性做不好反而被老师追问出漏洞。用模拟支付你在论文里就写“为了演示方便本系统采用模拟支付方式真实环境中可替换为第三方支付接口”这样既诚实又合理。4.4 订单状态机与超时取消订单不是“支付成功”就结束了商家端还要接单、配送、完成。我建议你把状态流转抽成一个独立方法保证非法状态切换能被拦截public boolean canTransfer(int currentStatus, int targetStatus) { switch (currentStatus) { case 0: return targetStatus 1 || targetStatus 5; // 待支付 - 已支付/取消 case 1: return targetStatus 2 || targetStatus 5; // 已支付 - 已接单/取消 case 2: return targetStatus 3 || targetStatus 5; // 已接单 - 配送中/取消 case 3: return targetStatus 4; // 配送中 - 已完成 default: return false; } }“用户下单后不支付订单一直占着库存”也是外卖系统必须处理的点。最简单的方案是用Spring的Scheduled定时扫描每30秒查一次“待支付且创建时间超过30分钟”的订单将其置为取消并回补库存。这里要注意定时任务只适合单机应用多实例部署时会有重复执行问题答辩时你可以主动提到“生产环境可以用RabbitMQ延迟消息”但实际代码里做一个定时任务就足够了。5. 实战中的隐藏坑点XSS过滤、分页查询与并发扣库存5.1 全局过滤器处理XSS攻击这个细节很加分热词里提到“springboot项目全局过滤器处理上传pdf文件时xss攻击”虽然具体场景是上传PDF但XSS防御在表单提交场景更常见尤其是外卖系统里有“店铺介绍”“菜品描述”“订单备注”这些文本字段很容易被插入script标签。最简单的做法是写一个过滤器对请求体中的字符串做HTML转义public class XssFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { chain.doFilter(new XssHttpServletRequestWrapper((HttpServletRequest) request), response); } } // XssHttpServletRequestWrapper 中重写 getParameter/getHeader/getInputStream // 将 script 等字符转义为 lt;scriptgt;同时要注意后端不能只在控制器里处理因为很多参数是JSON格式你还需要重写getInputStream把body里的JSON字符串做过滤。我通常在CommonConfig里注册这个过滤器并设置拦截所有路径。5.2 MyBatis-Plus分页插件的配置误区热词里有“mybatis的分页插件的用法 springboot”这个几乎每个项目都会用。MyBatis-Plus分页本身不复杂但很多人漏了分页插件配置结果Page对象查出来不带总数Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }这个Bean不配置分页查询的total一直是0列表页没法做分页组件。还有一种常见问题联表查询时分页总额不对。用MyBatis-Plus的自定义SQL时建议写成select 主表需要的字段不要select *并且分页插件会自动生成count语句如果count语句复杂可能报错可以在XML里单独写一个countSql。实际使用分页时我一般是PageOrderVO page new Page(pageNum, pageSize); LambdaQueryWrapperOrder wrapper new LambdaQueryWrapper(); wrapper.eq(Order::getUserId, userId).orderByDesc(Order::getCreateTime); PageOrderVO result orderMapper.selectOrderPage(page, wrapper);这样controller就能拿到records、total、current、size前端直接对接即可。5.3 并发下单时的库存超卖问题“秒杀”场景在校园外卖里虽然不常见但高峰期多个用户同时点同一道菜时库存扣减会产生超卖。最常见也最容易实现的方案是用乐观锁。在dish表增加version字段扣库存时执行UPDATE dish SET stock stock - #{quantity}, version version 1 WHERE id #{dishId} AND version #{oldVersion} AND stock #{quantity}然后检查更新行数如果为0说明库存不足或版本冲突就抛出业务异常。基于Spring的Transactional这个方案在毕设项目里已经足够。如果想更高级可以用Redis的decr配合lua脚本扣库存但答辩时你不一定能解释清楚所以还是优先选数据库乐观锁。5.4 时间时区与前端显示不一致这个问题在很多毕设项目里都存在。后端返回的LocalDateTime如果带T格式前端直接用字符串展示会很丑。我的做法是在application.yml配置统一格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai同时在VO里用JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8)作为双重保障。这种细节老师一眼就能看出来你是有工程经验的。6. 毕业设计答辩准备与项目演示的加分细节6.1 论文章节怎么跟代码对应论文不要写成“目录版用户手册”建议按“需求分析 - 系统设计 - 数据库设计 - 核心功能实现 - 系统测试”来组织。代码实现那一章不要贴大段代码而是贴“关键方法和流程图”并用文字说明逻辑。比如讲订单模块贴createOrder方法和状态机流转描述比贴完整类更有说服力。我在带学弟时常用一个笨办法论文里每一节提到“系统实现了XXX”就在代码里找一两个核心方法作为证据。评审老师看论文时会突击翻代码如果你论文里的命名和代码里对不上印象分一下子掉很多。6.2 演示前准备一套能自洽的数据演示翻车多数出在数据上登录密码不对、订单列表是空的、图片加载不出来。我建议本地准备一份“演示数据脚本”一个管理员账号admin / admin123一个用户账号student / student123两个测试店铺每个店铺至少有5个菜品每个店铺至少有一个已完成的订单和一个待支付订单演示顺序也最好定死先演示登录然后是用户端浏览菜品加购物车接着提交订单模拟支付再切到商家端接单最后管理后台看数据。这一步一步必然是连贯的如果中途靠手动改数据库来造假一旦被老师看出来就是事故。6.3 常见答辩问题和回应思路外卖系统答辩最容易被追问的问题我提前整理一下老师可能会问建议回答思路订单状态为什么会用枚举避免魔法数集中管理状态流转校验非法状态切换库存超卖怎么解决乐观锁/版本号同时检查更新行数JWT和Session有什么区别JWT无状态可扩展适合前后端分离但注意续期和注销问题Redis在你的系统里用在哪购物车/验证码/缓存讲清楚缓存和数据库的一致性如果用户下单后一直不支付怎么办定时任务扫描超时订单取消并回补库存前端传的价格你能信吗不信后端重新查库计算价格回答时不用背“标准答案”抓住“我实际是怎么做的”和“这个方案有什么局限”这两点老师会很认可你的工程思维。6.4 这个项目后续还能怎么扩展答辩最后如果老师问“你后续打算怎么改进”你可以说这几个方向接入真实第三方支付、引入延迟消息队列实现订单超时取消、增加首页推荐算法、多角色骑手配送调度、用Docker部署。但记住说出来的每个扩展点你至少要知道大概实现原理不要吹得太大。我的个人建议是哪怕论文提交了也可以继续把项目部署到云服务器用NginxSpring Boot jar包跑起来手机端能访问。这样面试时你直接给对方演示在线地址比在本地IDE里跑效果好得多。部署过程本身也会让你更理解Linux、防火墙、数据库远程连接这些真实开发中的东西。最后再分享一个小技巧整个系统完成后记得给项目写一个README.md把技术栈、运行环境、启动步骤、测试账号、常见问题都写清楚。这不仅是给老师看的也是你几个月后回来复习时的救命文档。我每带一个学弟做这类毕设都会先让他把README写个初稿再对照着把它跑通。你会发现写清楚一套启动文档比应付答辩更能检验你是不是真的理解了这个项目。
返回列表