1. 为什么校园订餐外卖系统值得自己动手做一版
我做这个项目纯属偶然。当时在某高校信息化小组帮忙,发现学生订餐高峰期食堂排队时间长,周边商家又没法精准触达校内用户,就萌生了做一个校园订餐外卖系统的想法。市面上现成的外卖平台很多,但通用平台在校园场景下有几个绕不开的痛点:校外骑手进校难、取餐点分散、学生订单集中在饭点前后形成明显波峰、商家需要按宿舍楼或教学楼分组配送。通用平台根本没有针对这些场景的字段设计,于是我决定基于SpringBoot从零搭建一套。
这个项目做下来,收益远超预期。它不是一个“玩具项目”,而是真正覆盖了电商类系统最典型的一整条链路:用户端浏览菜品、加入购物车、下单支付、订单状态流转、商家端接单出餐、管理员端审核与统计。无论你是刚学完SpringBoot想做课程设计,还是想把Spring生态的常用组件系统过一遍,这套“校园订餐外卖系统”都是很好的练手对象。
先说一下我最终的技术选型和项目规模。后端采用SpringBoot 2.x + MyBatis-Plus,数据库用MySQL 8.x,前端使用Vue + Element UI搭建管理后台,用户端使用微信小程序承载。整个项目包含三个核心角色:学生用户、商家、系统管理员,另外还有一套简单的配送状态管理。采用前后端分离架构,后端提供RESTful API,前端通过HTTP接口调用。
标题里提到“附源码81793”,这个编号是源码包内部的项目标识。源码包内包含了完整的后端工程、前端管理后台、小程序端代码以及SQL初始化脚本,拿到手之后按顺序导入数据库、改完配置就能直接跑起来。下面我从设计思路、数据库建模、核心模块实现、部署避坑几个环节逐一拆解,把自己实际开发过程中踩过的坑也一并交代清楚。
2. 技术选型与总体架构:SpringBoot + MyBatis-Plus的配置逻辑
2.1 为什么选这套技术栈
校园订餐外卖系统在技术上没有太大挑战,但它非常适合作为SpringBoot实战项目,因为业务场景足够完整。我选择SpringBoot 2.7作为基础框架,原因很简单:它的自动配置机制极大降低了整合成本,内嵌Tomcat让部署变成单jar包运行。对于课程设计或中小型项目,没有必要上微服务那套复杂治理,单体应用配合好分层结构反而更容易维护。
持久层我选择了MyBatis-Plus而不是原生MyBatis。两者差别很大:原生MyBatis需要手写大量XML映射文件,而MyBatis-Plus内置通用Mapper,单表CRUD几乎不用写SQL。实际开发中我统计过,订单、用户、菜品这些基础表的增删改查,MyBatis-Plus帮我省掉了大概60%的持久层代码。尤其是分页查询,一页代码就搞定,比手写PageHelper配置或者自己拼LIMIT语句要省心。
身份认证方面,我没有引入Spring Security或Shiro那套重量级方案。原因在于校园订餐系统只有简单的角色区分,用拦截器加复杂组件反而增加学习门槛。我最终选择用拦截器 + Session + 自定义注解实现三端统一登录校验,同时用ThreadLocal保存当前登录用户上下文。这种轻量方案在中小型项目中非常实用,也更容易让初学者看懂。
Redis我引入了,但用得克制。主要缓存菜品分类列表、首页轮播图信息以及热点菜品的访问计数。购物车和订单核心数据放在MySQL,因为校园场景的并发量远没有达到必须用Redis扛购物车的程度。刚开始我也想过把所有数据都放Redis,但后来发现项目可维护性会变差,最终调整为“MySQL持久化为主,Redis缓存为辅”的模式。
2.2 工程目录与职责划分
源码包中的后端工程按标准分层结构组织,每个层级职责清晰:
com.campus.order ├── controller # 接口层,接收前端请求,返回统一响应体 ├── service # 业务层,处理具体逻辑事务 │ └── impl # 业务接口实现 ├── mapper # MyBatis-Plus持久层接口 ├── entity # 数据库实体映射 ├── dto # 前端交互数据对象 ├── vo # 视图返回对象 ├── config # 全局配置类 ├── interceptor # 登录拦截器 ├── common # 统一返回结果、异常处理 └── utils # 工具类这种分层是我多次实践后觉得最舒服的结构。Controller层只做参数接收和结果返回,不含任何业务逻辑;Service层承担核心业务处理;Mapper层对接数据库。需要注意的是,有的同学习惯把逻辑全写在Controller里,页面一多、权限一复杂就完全失控。别学那种写法,宁可前期多建几个DTO,也别让Controller变成大杂烩。
2.3 统一返回结构与全局异常处理
前后端联调时最容易出现的问题就是接口返回格式不统一。有的接口返回成功就只给个true,有的失败时字段名又不一样,前端只能到处写分支判断。我从一开始就定义了统一的返回体:
{ "code": 200, "message": "操作成功", "data": null }对应的Java类是Result<T>,包含code、message、data三个字段,提供静态方法Result.success(data)、Result.error(code, message)来构造响应。所有Controller的返回值类型都必须是Result<T>,不准直接返回裸数据。这样约定之后,前端小程序的请求封装只需要写一套就全项目通用。
全局异常处理也是必须做的。我在common包下创建了GlobalExceptionHandler类,加上@RestControllerAdvice注解,内部定义了几个@ExceptionHandler方法分别处理业务异常BusinessException、参数校验异常MethodArgumentNotValidException以及兜底的Exception。
这里有一个实际经验:业务异常和系统异常一定要分开处理。业务异常比如“库存不足”“订单已取消”这类,需要在响应中返回友好提示且HTTP状态码保持200,方便前端直接弹出消息。真正系统异常和数据库连接异常则要记录完整堆栈日志到服务端文件。最忌讳的就是把系统堆栈直接返回给前端,既暴露内部结构又让用户看到一堆看不懂的英文报错信息。
2.4 跨域配置与端口规划
前后端分离后第一个遇到的问题就是跨域。我一开始的配置是在Controller类上加@CrossOrigin注解,但接口一多就发现每个类都得重复加,后来统一改用WebMvcConfigurer全局配置:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOriginPatterns("*") .allowCredentials(true) .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .maxAge(3600); } }端口规划方面,后端服务固定8080端口,前端管理后台使用9528端口,小程序端没有固定端口。生产环境部署时我把后端打包成jar,在服务器上用nohup java -jar方式后台启动,用Nginx做反向代理统一入口。前端静态文件由Nginx托管,/api前缀的请求反向代理到本机8080端口,这样浏览器端完全没有跨域问题。
3. 数据库设计:从菜品表到订单状态机的核心思路
3.1 核心表结构与业务字段
这套系统的数据库一共12张表,最核心的是围绕用户、商户、菜品、订单这四条主线展开。我直接给出建表时的核心字段设计,这些都是我自己反复斟酌后敲定的。
用户表(user):主键id、用户名、密码、手机号、角色(1学生/2商家/3管理员)、学校id、宿舍/收货地址、头像、注册时间。密码字段必须有盐值或至少用MD5加盐处理。有的系统直接用明文存密码,那是给自己留坑。这里是课程设计无所谓,真有同学毕设答辩时被问到安全问题,回答“密码明文存储”会非常减分。
商家表(merchant):主键id、用户id外键、店铺名称、公告、起送价、配送费、营业状态(1营业/0休息)、评分、图片。起送价和配送费是订单计算金额时的关键字段,我在设计中把它们单独抽出来,避免了每次查询用户关联商家再取价格的性能损耗。
菜品表(dish):主键id、商家id、分类id、菜品名称、图片、价格(单位分)、描述、月售数量、库存状态、上架状态(1上架/0下架)。价格我强烈建议用整数型的“分”来存储,而不是小数型的“元”。浮点数在金额累加过程中会出现精度丢失,两个9.9元的菜加一起可能等于19.79999。实际项目里还要结合分页查询、模糊搜索等操作,整数金额可以避免大量边界问题。
菜品分类表(category):主键id、商家id、分类名称、排序字段。分类表挂在商家下而不是全局共享,因为不同商家的分类五花八门,比如炸鸡店是“鸡排/小吃/饮品”,面馆是“汤面/拌面/小菜”,全局共享分类反而无从挂载。
购物车表(cart):主键id、用户id、商家id、菜品id、菜品名称(快照)、菜品图片(快照)、单价(分快照)、数量、选中状态。这里有一个关键设计点:购物车表存了菜品名称和图片的“快照”字段。这样菜品菜单被商家修改甚至删除后,用户购物车里的历史信息仍然能正常展示。这种做法和订单表的“商品快照”思路一脉相承,问题是很多人容易忽略。
订单表(orders):主键id、订单编号、用户id、商家id、总金额(分)、配送地址、联系电话、备注、订单状态、支付状态、创建时间、支付时间、完成时间。订单状态我用int存储,配合一套常量类定义状态含义:0待支付、1待接单、2已接单/制作中、3配送中、4已送达/完成、5已取消、6退款中/已退款。
订单明细表(order_item):主键id、订单id、菜品id、菜品名称(快照)、菜品图片(快照)、单价(分快照)、数量、小计金额。这个表用来展开订单详情里用户买了哪些菜。快照字段在订单表这层同样是重点,因为订单生成后商家改价、改名都不应该影响订单历史数据。
地址表(address):主键id、用户id、收货人姓名、联系电话、所在校区、宿舍楼栋/具体地址、是否为默认地址。校园场景非常特殊,收货地址直接绑定了宿舍楼栋或教学楼,配送员不需要像社会外卖那样精确到门牌号,而是送到楼栋门口后由用户下来取。
其他如轮播图表、公告表、管理员操作日志表这类辅助表就不一一展开了。
3.2 订单状态机的流转设计
订单状态逻辑是整个系统的核心难点。很多同学习惯用if-else在代码里层层判断状态,后面状态一多就乱套。我在这个项目里用状态机思维重构了订单生命周期,每种状态下只允许固定的动作触发固定的状态变更:
0待支付 → (支付成功) → 1待接单 0待支付 → (超时未支付/用户主动取消) → 5已取消 1待接单 → (商家接单) → 2已接单/制作中 1待接单 → (商家拒单/超时未接单) → 5已取消 2已接单/制作中 → (商家出餐) → 3配送中 3配送中 → (用户确认收货/超时自动确认) → 4已送达/完成 2已接单/制作中 → (用户申请退款/商家同意) → 6退款中/已退款我把这个状态机实现为一个订单状态机常量类,每个状态变更操作在Service层最终通过统一方法校验。状态一旦从“待支付”变为“已接单”,就再不允许直接跳转到“完成”,必须严格经过中间状态。这样做的好处是,查询订单列表时需要统计各状态数量,SQL里只需要GROUP BY order_status就能快速得出结果,不会出现同一订单同时处于两个状态的分裂数据。
3.3 学校与商户的关联设计
校园订餐和常规外卖的另一个差异是“学校”这个维度。同一个系统可能被不同高校部署,不同学校的学生只能看到本校范围内的商家。最简单的设计是user表加school_id字段,merchant表也加school_id字段,查询菜品时通过商家关联过滤学校。
我在实际项目中还加了一层“校区”的概念,比如某高校有东校区和西校区,学生默认有一个主校区,可以切换。配送费在跨校区时会发生变化。由于系统规模不大,我用一套school表和campus表配合外键字段campus_id存储。这个设计在演示时是一个不错的加分点,能体现你对业务场景的理解深度,而不是仅仅实现了通用的CRUD。
4. 登录与权限:学生、商家、管理员三种身份的Session处理
4.1 单表多角色还是分表存储
这是我踩过的一个重要设计坑。最开始我把学生、商家、管理员分别建了user、merchant、admin三张表,登录接口时分三套逻辑去验证,结果写出来的代码大量冗余,拦截器也不好处理。
后来我重构为单用户表加role字段统管三种身份。用户表中role字段取值为1、2、3,分别对应学生、商家、管理员。商家额外有merchant表补充店铺维度的信息,user表只存登录账号和基础字段。登录接口只需要执行一次验证逻辑,根据返回用户实体里的role字段再做后续分支处理。登录成功后将用户对象放入Session中,后续请求通过拦截器统一识别当前登录用户身份及角色。
这里涉及权限控制的层面。学生端接口要求role=1,商家端接口要求role=2,管理员接口要求role=3。我将注解@RequireRole标注在Controller的方法上,在拦截器里读取该注解并进行权限校验。
4.2 拦截器实现细节
拦截器的核心逻辑分为三步:第一步从请求中取出JSESSIONID,进而还原Session中的登录用户;第二步根据路径前缀判断请求归属哪一端;第三步根据角色注解校验权限。部分接口比如菜品浏览、轮播图查询允许游客访问,就在注解上标记permitAll。
Session会话过期时间要做好规划。学生用户可能中午订完餐下午继续刷页面,Session过期时间我设置为2小时;商家端操作频繁但通常是店员操作,设置为1小时;管理端后台操作相对低频,也设置2小时。实际开发中要注意浏览器端刷新页面时Session还在,但小程序端的Session机制需要自己在请求封装里携带凭证信息。小程序是前后端分离的典型场景,服务端同时开启基于Session和基于自定义Token两种方式并无必要,我最后统一用Session方式,小程序端保持了同一会话上下文。
身份认证这块最容易被忽视的环节是“未登录状态下的接口返回”。如果拦截器发现未登录用户直接返回401,前端需要在请求封装里全局拦截401并跳转到登录页。我初期项目没做统一处理,每个页面都单独写判断,后来封装了一请求工具类加上对应的拦截函数,问题彻底解决。
5. 点餐下单的核心流程:购物车、订单、支付模拟与超时处理
5.1 购物车的前后端交互设计
学生端点餐的第一步是加购物车。设计中,购物车以商家为维度进行隔离。这个维度在代码中体现为,用户加购物车时必须携带商家id,前端封装的加购接口会自动判断当前购物车里是否存在其他商家的商品,如果存在就弹窗提示“清空购物车或直接下单”的选择。
购物车接口我设计成四个清理的端点:
GET /api/cart/list 前端购物车角标用的数量统计 POST /api/cart/add 添加菜品到购物车 PUT /api/cart/update 修改菜品数量 DELETE /api/cart/empty 清空购物车实现过程中我遇到一个边界问题:购物车里的同一个菜品如果重复添加,要不要合并数量。我之前遇到过用户反复点加购按钮加了10次同一个菜,购物车列表显示10条重复记录,结算时数量计算就乱了。后来在add接口里加了合并逻辑:同一用户、同一商家、同一菜品已经存在时只改数量不加新记录。这个操作虽然简单,但直接影响前端展示的整洁度。
5.2 下单接口的业务事务处理
下单是整个系统中业务逻辑最集中、事务要求最高的接口。核心操作链路如下:
- 从请求体中获取配送地址id、用户备注、购物车中选中的商品列表;
- 查询商家信息,校验营业状态和起送价;
- 计算商品总金额(遍历每个菜品单价乘以数量累加,再参考购物车快照字段);
- 计算配送费和包装费。包装费我在商家表里并没有单独设置,实际是菜品自带的“打包价”额外计算,每份菜打包价从0到1元不等;
- 生成订单列表数据并计算总金额。订单总额 = 商品总金额 + 配送费 + 包装费。校验是否达到商家起送价;
- 减库存。实际库存字段我并没有做得非常复杂,只做了简单的可用库存判断:如果某菜品已售罄或剩余库存不满足购买数量,则直接抛出异常;
- 生成订单编号,订单状态置为0待支付;
- 清空购物车中已结算的菜品;
- 返回订单详情供前端跳转支付页。
生成订单编号我最初用的时间戳格式,比如2025000112100001这种,但同一秒生成两单可能重复。后来改成“日期 + 用户id后四位 + 6位随机数”的组合,并在数据库为订单编号建立唯一索引,兜底保证不重复。
第6步减库存这里隐藏了一个并发问题。如果两个用户同时下单购买同一款只剩下1份的菜品,可能出现超卖。我在菜品表增加了一个库存字段stock,下单时使用UPDATE dish SET stock = stock - ? WHERE id = ? AND stock >= ?这种条件更新SQL确保减库存操作原子化。受影响行数为0时表示库存不足,立即返回异常。
5.3 支付模块的实现选择
校园项目中接入真实的支付宝或微信支付需要商户资质,绝大多数课程设计根本不具备这些条件。我建议用一种“模拟支付”来实现:在支付接口里不做真实扣款,只校验订单状态和金额后直接将订单状态从待支付改为待接单,并记录模拟支付时间。
如果将来要接入真实支付,替换点也已经预留好,集中在PayService接口,目前提供的是MockPayServiceImpl。要注意的坑是支付回调是异步的,真实支付环境下服务器需要接收支付网关的回调通知,这时候不能用Session判断用户身份,只能通过商户订单号关联订单。模拟支付接口里我保留了orderNo入参,就是为了以后对接真实支付时无需改动Controller层。
5.4 超时未支付自动取消订单
校园用户经常把订单加进购物车后忘记支付,导致商家收到大量待支付垃圾订单,占用备餐资源。因此我实现了超时取消机制。
方案有几种:定时任务扫描数据库、Redis延迟队列、或者下单时写入过期时间。最稳妥且容易实现的方式是Spring的@Scheduled定时任务,每30秒扫描一次订单表,将创建时间超过15分钟且处于待支付状态的订单自动置为已取消,并恢复对应菜品的库存。
代码如下:
@Scheduled(fixedDelay = 30000) public void cancelExpiredOrders() { LocalDateTime deadline = LocalDateTime.now().minusMinutes(15); List<Orders> expiredOrders = ordersMapper.selectList( new LambdaQueryWrapper<Orders>() .eq(Orders::getOrderStatus, 0) .lt(Orders::getCreateTime, deadline) ); for (Orders order : expiredOrders) { cancelOrder(order); } }恢复库存时要注意在事务里操作,防止取消订单过程中出现部分库存已恢复、部分因异常导致数据不一致的情况。这个定时任务的调度方式在单体项目里足够。后期如果订单量上来,可以考虑改用RabbitMQ延迟队列,那就是后话了。
6. 商家端与配送逻辑:接单、出餐、送达的状态流转
6.1 商家端订单列表与状态操作
商家端是整个业务闭环中极易被忽略的一个环节。很多外卖系统Demo只做了用户点餐和后台管理,商家端却缺失,导致订单状态永远卡在“待接单”无法流转。我在设计时把商家端单独作为一个H5页面嵌入管理后台,商家登录后可以看到所有属于自己店铺的订单,并按状态tab分类。
商家可执行的操作有四个:接单、拒单、出餐、完成。接单本质是将订单从1待接单改为2已接单/制作中;拒单则是将订单改为5已取消,同时给用户发送一条退款/取消通知。出餐操作改为3配送中,表示餐品已打包好等待骑手或自提。这里需要注意出餐操作是在“已接单”之后才能触发,前端按钮也要根据状态动态展示。
6.2 校内配送还是自提
校园场景和社区外卖的差异在配送环节表现最为明显。考虑到校园内禁止社会骑手进入,我设计了两套配送模式:
- 商家自配送:商家自己配送至宿舍楼下或教学楼固定取餐点,适合校内商家。
- 用户自提:订单完成后用户收到取餐通知,自行前往商家窗口核对取餐码拿餐。
我在订单表里增加了字段delivery_type,值为1自配送、2自提。这个字段在订单提交时选择,商家接单后根据类型展示不同操作。如果是自提,出餐后订单直接变为4已送达/完成,然后在商家端待取餐列表显示取餐码。如果是自配送,出餐后变为3配送中,由配送员确认送达后变为4已送达。
这里有一个很细节的逻辑:自提模式下“出餐”和“送达”是同一个动作,但页面展示上仍然要区分。用户看到的状态变化应该是“商家已出餐,请到窗口取餐”,而不是直接跳到“已送达”。我通过把取餐码下发到用户端和商家端,双方对照取餐码完成提货,在数不清的演示项目里这一环最容易做漏。
6.3 订单统计与商家端数据看板
商家首页需要展示今日营业额、今日订单数、待处理订单数、热销菜品TOP5这几个核心指标。SQL层面用一次聚合查询就能得到大多数指标,但热销菜品需要关联订单明细表按菜品分组求和。由于菜品名称做了快照,分组维度直接用菜品名称即可,非常干净。
统计接口也有几个坑要提醒。一是时区问题,MyBatis-Plus查询当日订单时如果用前端传入的字符串日期,需要统一转换为LocalDateTime区间,注意数据库时区和服务器时区要保持一致。二是聚合统计要加商家id条件过滤,防止商家A看到商家B的数据。这个问题初版确实出现过,原因就是统计SQL漏了商户维度过滤,后来排查时用两个商家账号交替登录比对数据才发现。
7. 部署、测试与避坑实录:从本地启动到服务器上线
7.1 本地环境准备与启动步骤
拿到源码后,第一步是导入数据库脚本。SQL文件在源码包的db目录下,建议用Navicat或命令行执行整个脚本,它会自动建库建表并插入初始数据。初始数据包含一个演示商家账号和若干测试菜品,方便直接体验完整流程。
第二步修改后端配置文件application.yml中的数据库连接信息和上传文件保存路径:
spring: datasource: url: jdbc:mysql://localhost:3306/campus_order?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: yourpassword servlet: multipart: max-file-size: 10MB max-request-size: 20MB第三步启动后端主类,确认8080端口未被占用。最后将前端管理后台项目用npm run dev启动,小程序端用微信开发者工具打开并修改项目根目录的接口地址为http://localhost:8080。
需要注意的是,微信开发者工具中默认不能访问localhost,需要勾选“不校验合法域名”选项。小程序端的网络请求封装我已经在utils/request.js里写好了,把baseURL改成你本机的局域网IP即可,真机预览时也用这个地址。
7.2 我踩过的坑和排查过程
第一个坑是前端上传菜品图片时一直报404。后来排查发现图片保存路径配置写成了相对路径./upload,SpringBoot打包成jar后工作目录是临时目录,重启后文件就没了。正确做法是在配置文件中指定一个绝对路径,比如/home/ubuntu/campus/upload/,同时配置一个虚拟路径映射到磁盘目录。
第二个坑是中文乱码。MySQL连接串里如果没有characterEncoding=utf8,菜品名称和商家公告会出现问号。我在application.yml中加上了参数,同时建表时统一使用utf8mb4字符集。utf8mb4和utf8的区别在于前者能存emoji表情,用户备注里如果带表情,utf8会直接报错。这个坑浪费了我一下午。
第三个坑是跨域导致登录Session丢失。管理后台和小程序分别调用接口,axios默认不携带cookie,设置withCredentials: true后前端才能把Session标识带给后端。我一开始只配置了后端的跨域允许,忘记调整axios的withCredentials,导致用户明明登录成功,再点其他接口时又被拦截下来。检查了半天才发现是这个原因。
第四个坑是默认密码安全问题。系统初始化创建的管理员账号密码均为admin,商家账号密码均为123456。部署到公网时必须修改初始密码,我遇到过不修改默认密码导致服务器数据库内容被篡改的真实事件,这个真不能懒。
7.3 Linux服务器部署步骤
服务器部分我只用一台2核4G的云主机,系统为Ubuntu。部署步骤大致如下:
安装JDK 8或11,设置JAVA_HOME环境变量。安装MySQL 8并导入初始化脚本。安装Nginx做反向代理。
后端打包发布:
cd campus-order mvn clean package -DskipTests nohup java -jar target/campus-order-0.0.1-SNAPSHOT.jar \ --spring.profiles.active=prod > /usr/local/logs/order.log 2>&1 &前端项目构建后把dist目录放到Nginx的html目录下,Nginx配置里加入:
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; }如果部署的是小程序端,后端域名必须是HTTPS,微信官方要求所有请求为HTTPS协议。我自己最初只用了HTTP,小程序真机预览时请求全部被拦截,后来用云厂商的免费SSL证书配置到Nginx才解决。这一步在正式部署时千万不能跳过。
部署完成后,我习惯先跑一遍核心链路:学生用户登录→浏览商家→加购菜品→下单→模拟支付→商家端接单→出餐→送达→后台统计查看。这个链路走通,部署就算成功了。
8. 从课程设计到真实产品:这套系统还能怎么扩展
做完这套系统之后,我对校园订餐外卖业务有了更深的理解。很多同学拿到源码跑通后就以为结束了,但真正能让简历或答辩出彩的,往往是在基础版本之上再做一层有价值的扩展。
基于当前架构和源码,几条比较顺畅的扩展路径如下:
第一,引入消息推送机制。现在用户只能在订单详情页刷新看状态,体验不够流畅。如果引入微信小程序订阅消息,订单状态每变更一次就向用户推送一条模板消息,用户感知会好很多。微信订阅消息的接入需要在后端调用接口获取access_token,再把订单状态变更事件和用户openid绑定,逻辑上并不复杂,就是HTTP请求加Token管理。
第二,升级为Redis + 延迟队列处理超时订单。当前定时任务扫描方案在数据量大时会有分钟级延迟,且每次扫描全表比较浪费。用Redis存储订单ID和过期时间戳,配合延迟队列或者Redisson的看门狗机制可以做到秒级取消,也大大降低数据库压力。
第三,增加营销模块。比如满减优惠、新用户立减券、限时秒杀。满减计算要叠加在订单总额上,这意味着订单表需要增加“优惠金额”字段和“优惠明细”扩展表。在数据库设计中预留冗余字段并做好优惠快照,这个方向能在项目演示时大幅提升商业完成度。
第四,完善配送人员管理。如果要做真正的配送调度,还需要骑手表、配送任务表、位置上报接口。目前订单表中只存了配送方式和状态,缺少配送员的关联字段。增加一个delivery_person_id字段,扩展接口是件顺理成章的事。
第五,接入真实支付。正如前文所说,PayService已经留好了接口边界,只需要实现RealPayServiceImpl,接入支付宝的沙箱环境即可完成真实支付闭环。支付宝沙箱不需要真实商户资质,是练习接入第三方支付接口非常好的途径。
扩展建议这块我个人的看法是:别贪多,选一个方向做深。把消息推送和营销模块做透,已经足够在答辩或面试中讲出完整的故事了。贪多嚼不烂,反而容易哪个都做不好。
最后分享一个实际开发中的体会:校园订餐这类系统,真正的难点从来不是写几个CRUD接口,而是想清楚业务状态怎么流转、数据快照怎么设计、边界条件怎么处理。我刚动手时也走了不少弯路,最开始订单表没有快照字段,商家改价后历史订单金额跟着变,被测试的同事连续吐槽了好几天。后来又重新设计了订单明细表,才把这个问题彻底根治。
做项目就是这样,踩一次坑记住一次。这套源码不是终点,把它当成一个可以“折腾”的半成品,多改改、多打补丁,技术能力反而提升得更快。如果你打算拿这个项目参加课程设计或毕业设计,我建议先完整跑通主流程,然后针对你感兴趣的模块做深度优化,而不是把全部时间花在堆功能数量上。一个模块做到极致,远比十个模块都做得一般更让评审老师印象深刻。