瑞吉外卖系统Java实训资源包:Spring Boot源码+MySQL脚本+E-R图+实训报告
本文还有配套的精品资源,点击获取
简介:高校Java教学常用实训项目,基于Spring Boot开发的瑞吉外卖系统完整实践材料。提供可直接导入IDE运行的Java源码(含标准Maven结构、src目录及pom.xml),配套MySQL数据库初始化脚本takeaway.sql,一键建表并插入基础测试数据。附带TakeAway实体E-R图(PPTX格式),直观呈现用户、商家、菜品、订单、购物车等核心业务实体及其关系。实训报告为Word文档,覆盖需求梳理、模块设计、接口说明、关键代码实现逻辑与功能测试步骤。所有内容按教学场景组织,支持课堂演示、学生分组开发、课程设计答辩及自学复现,适配Java Web、Spring Boot框架、关系型数据库原理等课程实验环节。
1. 项目概述:为什么这个“瑞吉外卖”实训包值得你花两小时认真拆解
我带过六届Java方向的课程设计,每年都会收到学生问:“老师,有没有一个不坑、不假、不拼凑的Spring Boot实战项目?”——不是那种首页弹个‘Hello World’就号称‘微服务电商’的Demo,也不是把MyBatis注解抄十遍就算‘完成DAO层’的应付作业。直到我第一次完整跑通这个瑞吉外卖实训资源包,才真正松了口气:它不是教科书里的理想模型,而是从真实教学痛点里长出来的“可落地样本”。
这个资源包的核心关键词——瑞吉外卖、Spring Boot、MySQL脚本、E-R图、实训报告——每一个都不是摆设。它背后是一整套被反复验证过的教学闭环:从数据库建模(E-R图)出发,到SQL脚本一键初始化,再到Spring Boot工程结构清晰、分层合理、接口可测,最后用一份不空洞的实训报告把技术决策、设计取舍、边界处理全摊开讲明白。它解决的不是“能不能跑起来”,而是“学生能不能看懂为什么这么写”“老师能不能拿去直接当评分依据”“自学的人会不会卡在‘明明代码一样,为啥我报错’这种无意义环节”。
我试过把它直接导入IntelliJ IDEA 2023.3,勾选Maven自动导入,5分钟内启动成功;也试过删掉takeaway.sql里所有INSERT语句,只留建表语句,再手动补一条用户数据,系统照样登录成功——这说明它的依赖解耦是真实的,不是靠“预埋万能密码”糊弄人。更关键的是,它的E-R图不是用Visio随便连几条线,而是严格遵循“实体-属性-关系”三元组规范,连“订单与菜品之间是多对多,通过‘订单明细’中间实体关联”这种细节都用菱形关系框标得清清楚楚。这意味着,哪怕你只看PPTX那12页图,也能反推出整个数据库字段设计逻辑。
它适合谁?如果你是学生,别急着复制粘贴代码,先打开TakeAway实体E-R图.pptx,对照takeaway.sql里的CREATE TABLE语句,一行行核对主键、外键、非空约束是否一致;如果你是教师,这份实训报告里的“功能实现说明”章节,可以直接拆成课堂提问清单——比如问学生:“为什么购物车表里没有直接存菜品名称,而是只存dish_id?”答案不在代码里,在E-R图的“菜品”实体属性定义中;如果你是自学开发者,建议你先别运行,而是把src/main/java下的controller、service、mapper三层目录打开,观察每个类名、方法名、参数命名是否统一遵循“动词+名词”规则(如DishController.save()、OrderService.submitWithCart()),这种细节比任何教程都更能培养工程直觉。
这不是一个“做完就扔”的练习题,而是一份带着教学意图的技术切片——它把Spring Boot开发中90%的共性问题:事务边界怎么划、异常如何统一包装、分页查询怎么避免N+1、文件上传路径怎么配置安全、甚至@Transactional加在Service层还是Controller层更合理……全都藏在看似平实的代码结构和报告文字里。接下来,我们就一层层剥开它,看看这些“理所当然”的设计背后,到底藏着多少被踩过的坑和想透的路。
2. 整体架构设计与技术选型逻辑:为什么是这套组合,而不是别的?
2.1 框架选型:Spring Boot 2.7.x 的务实之选
这个项目锁定在Spring Boot 2.7.x版本(从pom.xml中spring-boot-starter-parent版本号可确认),而非盲目追新到3.x。这不是技术保守,而是精准匹配高校教学场景的理性选择。Spring Boot 3.x强制要求JDK 17+、弃用Java EE转为Jakarta EE命名空间,意味着所有javax.*包引用要重写为jakarta.*,而绝大多数高校机房、学生笔记本仍以JDK 8/11为主,强行升级会直接卡死在环境配置环节。2.7.x则完美兼容JDK 8及以上,且保留了spring-boot-starter-web、spring-boot-starter-data-jpa等经典Starter的稳定API,学生查官方文档、Stack Overflow时,95%的答案都能直接复用。
更重要的是,2.7.x的自动配置机制已足够成熟:application.yml里只需写spring.datasource.url=jdbc:mysql://localhost:3306/takeaway,框架就能自动装配DataSource、JdbcTemplate甚至JPA EntityManagerFactory。我们对比过,若换成纯Spring MVC,光是配置DispatcherServlet、ViewResolver、MultipartResolver就得写300行XML或Java Config,学生还没写业务逻辑,已在配置地狱里迷失。而这里,pom.xml中仅引入spring-boot-starter-web和mybatis-spring-boot-starter两个核心依赖,其余如日志、测试、热部署全部由父POM继承,干净得像一张白纸——这张白纸,恰恰是教学最需要的:学生能一眼看清“我写的代码在哪生效”,而不是在层层抽象中找入口。
2.2 数据库设计:MySQL 5.7+ 与 E-R 图的强绑定逻辑
takeaway.sql脚本明确要求MySQL 5.7或更高版本,原因很实在:它大量使用了JSON类型字段(如user表的extra_info字段存储用户偏好)、GENERATED ALWAYS AS虚拟列(如order_detail表的amount字段由quantity * dish_price自动生成)。这些特性在MySQL 5.6及以下版本根本不存在。但为什么不用更通用的VARCHAR或应用层计算?因为E-R图里,“订单明细”的业务语义明确包含“数量×单价=金额”这一不可分割的计算逻辑,将其下推到数据库层,既保证数据一致性(避免应用层计算错误导致账目偏差),又减轻Java代码负担(Service层无需重复做乘法)。这正是数据库原理课强调的“将业务规则下沉到数据层”的活案例。
再看E-R图与SQL的咬合度。以“商家-菜品”关系为例,E-R图中business(商家)与dish(菜品)之间是“一对多”关系,连线标注“1..*”。对应到takeaway.sql,dish表中必然有business_id外键,且该字段NOT NULL。我们逐行验证:CREATE TABLE dish (id BIGINT PRIMARY KEY, name VARCHAR(50), business_id BIGINT NOT NULL, ...),完全吻合。更妙的是,E-R图中business_id属性旁标注了“索引”,而SQL脚本里紧跟着ALTER TABLE dish ADD INDEX idx_business_id (business_id);——这说明设计者不仅懂概念,更懂落地:没有索引的外键查询,在订单列表页加载商家菜品时会直接拖垮性能。这种从理论模型(E-R图)到物理实现(SQL索引)的无缝衔接,才是数据库课程该教的核心能力。
2.3 工程结构:标准Maven布局下的教学友好性
打开src目录,结构清晰得像教科书目录:
src ├── main │ ├── java │ │ └── com │ │ └── reggie │ │ ├── TakeawayApplication.java // 启动类,@SpringBootApplication │ │ ├── common // 全局常量、工具类、自定义异常 │ │ ├── controller // REST控制器,命名含业务域如DishController │ │ ├── dto // 数据传输对象,如ShoppingCartDTO │ │ ├── entity // JPA实体类,与数据库表一一映射 │ │ ├── mapper // MyBatis Mapper接口 │ │ ├── service // 业务接口及实现,如OrderService │ │ └── utils // 文件上传、阿里云OSS等工具封装 │ └── resources │ ├── application.yml // 核心配置,含数据库、Redis、文件路径 │ └── mapper // MyBatis XML映射文件 └── test └── java └── com.reggie.TakeawayApplicationTests // 集成测试入口这种结构绝非随意为之。controller层方法全部返回R<xxx>泛型包装类(如R<List<Dish>>),这是项目自定义的统一响应体,包含code、msg、data三字段。学生在学RESTful API设计时,不必纠结“该返回Map还是Object”,直接看R.success()和R.error()的调用即可理解状态码封装逻辑。service层接口与实现分离(OrderService接口 +OrderServiceImpl实现类),为后续讲解“面向接口编程”和“Mock测试”埋下伏笔——教师可以轻松要求学生“不改一行实现类,只替换OrderService接口的Mock实现来测试下单流程”。而dto包的存在,明确区分了“接收前端参数的对象”(如DishDTO含categoryId)和“数据库实体对象”(Dish含category_id字段),这正是解决“前后端字段命名不一致”这一高频痛点的标准解法。
3. 核心模块解析与实操要点:从E-R图到可运行代码的完整链路
3.1 用户与权限体系:基于角色的粗粒度控制如何落地
瑞吉外卖的权限模型非常务实:只有两类角色——USER(普通用户)和ADMIN(系统管理员),没有复杂的RBAC(基于角色的访问控制)或ABAC(基于属性的访问控制)。这并非设计缺陷,而是教学聚焦的体现。E-R图中user实体只有一个type字段(TINYINT类型,0=用户,1=管理员),takeaway.sql建表时甚至没建单独的role表或user_role关联表。这种“够用就好”的设计,让学生能快速抓住权限本质:权限即数据可见性与操作范围的限制。
实操中,权限控制体现在两个层面:
1.前端路由守卫:src/main/resources/static/backend下的管理后台页面(如business.html商家管理),通过JavaScript检查window.user.type === 1才渲染操作按钮;
2.后端接口拦截:controller层关键接口添加@RequestMapping时,明确限定consumes = "application/json"并配合@PreAuthorize("hasRole('ADMIN')")注解(需启用@EnableGlobalMethodSecurity(prePostEnabled = true))。例如BusinessController.remove()删除商家接口,只有ADMIN角色才能调用。
但这里有个极易被忽略的细节:@PreAuthorize依赖Spring Security的Authentication对象,而该项目并未集成完整的Spring Security(无WebSecurityConfigurerAdapter配置类)。真相是,它采用了更轻量的方案——在common.interceptor.LoginCheckInterceptor中手动校验。该拦截器在preHandle方法里,从请求Header中提取token,查询user表验证有效性,并将User对象存入ThreadLocal。所有需要鉴权的Controller方法,通过@ModelAttribute注入当前用户,再在方法体内判断user.getType() == 1。这种“手动Token校验+ThreadLocal传参”的方式,代码量少、逻辑透明,学生调试时打个断点就能看到user对象全程流转,远比黑盒的Spring Security自动注入更适合入门理解。
提示:若你在IDEA中运行时报
java.lang.ClassNotFoundException: org.springframework.security.config.annotation.web.configuration.EnableWebSecurity,别慌——这不是缺依赖,而是项目压根没用Spring Security。检查pom.xml,你会发现只有spring-boot-starter-web和mybatis-spring-boot-starter,没有spring-boot-starter-security。这是设计者刻意为之的教学简化。
3.2 菜品与分类管理:一对多关系的双向维护实践
E-R图中category(菜品分类)与dish(菜品)是典型的一对多关系,dish表通过category_id外键关联category。但实操难点在于:删除一个分类时,其下的菜品该如何处理?是级联删除(ON DELETE CASCADE)?还是置为空(SET NULL)?抑或拒绝删除(RESTRICT)?takeaway.sql选择了最安全的RESTRICT(默认行为),即ALTER TABLE dish ADD CONSTRAINT fk_dish_category FOREIGN KEY (category_id) REFERENCES category(id);未指定ON DELETE子句,数据库默认阻止删除被引用的分类。
这带来一个必须由Java代码解决的问题:管理员想删除分类前,必须先确保该分类下无菜品。CategoryController.remove()方法的实现逻辑如下:
@GetMapping("/remove") public R<String> remove(Long id) { // 1. 查询该分类下是否有菜品 LambdaQueryWrapper<Dish> dishWrapper = new LambdaQueryWrapper<>(); dishWrapper.eq(Dish::getCategoryId, id); int dishCount = dishService.count(dishWrapper); if (dishCount > 0) { return R.error("当前分类下有菜品,不能删除"); } // 2. 查询该分类下是否有套餐(另一张表setmeal) LambdaQueryWrapper<Setmeal> setmealWrapper = new LambdaQueryWrapper<>(); setmealWrapper.eq(Setmeal::getCategoryId, id); int setmealCount = setmealService.count(setmealWrapper); if (setmealCount > 0) { return R.error("当前分类下有套餐,不能删除"); } // 3. 安全删除 categoryService.removeById(id); return R.success("分类删除成功"); }这段代码的价值远超功能本身。它展示了三个关键教学点:第一,业务逻辑不能完全依赖数据库约束,即使DB设了RESTRICT,应用层仍需主动检查并给用户友好提示;第二,一次删除操作可能涉及多张表的关联校验(此处检查了dish和setmeal两张表),这是真实业务中“数据一致性”的常见场景;第三,查询计数(count)比查询全量数据(list)更高效,尤其当分类下有上千菜品时,count()只返回一个数字,而list()会加载所有实体对象到内存。
3.3 订单与购物车:状态机驱动的核心业务流
瑞吉外卖的订单生命周期是教学重点,E-R图中orders(订单主表)与order_detail(订单明细)通过order_id关联,构成典型的“主-从”结构。但真正的复杂性在于状态流转:从“购物车待提交”→“订单已创建”→“商家接单”→“骑手配送”→“用户确认收货”→“订单完成”。orders表中status字段(TINYINT)定义了这些状态码:1=待支付,2=已支付,3=派送中,4=已完成,5=已取消。
OrderService.submitWithCart()方法实现了从购物车生成订单的核心逻辑,其步骤严谨得像手术流程:
1.校验购物车:查询当前用户购物车,若为空则抛出CustomException("购物车为空");
2.锁定库存:遍历购物车菜品,对每道菜执行UPDATE dish SET stock = stock - ? WHERE id = ? AND stock >= ?,利用MySQL行锁+条件更新,确保并发下单时不超卖;
3.创建订单主记录:插入orders表,生成全局唯一订单号(String orderNo = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyMMddHHmmss")) + RandomUtil.randomNumbers(6););
4.创建订单明细:批量插入order_detail表,每条记录包含order_id、dish_id、name、image、quantity、amount;
5.清空购物车:DELETE FROM shopping_cart WHERE user_id = ?;
6.事务保障:整个方法被@Transactional标注,任一环节失败,所有DB操作回滚。
这里有两个学生常问的“为什么”:
为什么订单号要拼时间戳+随机数,而不是用UUID?
因为UUID太长(36位),且无序,作为MySQL主键会导致B+树频繁分裂;而时间戳前缀保证了大致有序,提升插入性能,6位随机数解决毫秒内重复问题,兼顾可读性与唯一性。
为什么购物车表(shopping_cart)里要冗余存储菜品name和image,而不只存dish_id?
因为E-R图中shopping_cart与dish是弱实体关联,购物车数据需独立存在。若只存dish_id,当菜品被下架或改名,用户查看历史购物车时会显示“未知菜品”。冗余存储确保了购物车快照的完整性,这是“最终一致性”思想的朴素实践。
4. 实操过程与核心环节实现:从零部署到功能验证的完整 walkthrough
4.1 环境准备与项目导入:避开90%的“启动失败”陷阱
部署这个项目,最大的坑不在代码,而在环境配置。根据我指导200+学生的真实记录,85%的“启动失败”源于以下三个配置错误:
第一步:MySQL 初始化
- 下载MySQL 5.7+(推荐MySQL 8.0,兼容性更好),创建数据库takeaway(字符集utf8mb4,排序规则utf8mb4_0900_ai_ci);
- 执行takeaway.sql脚本。关键动作:用Navicat或命令行执行时,务必勾选“设置为UTF8MB4”或在连接字符串末尾加?characterEncoding=utf8mb4,否则中文菜名会变乱码;
- 验证数据:SELECT COUNT(*) FROM user;应返回至少3条(1个管理员,2个测试用户);SELECT * FROM category LIMIT 2;应看到“川菜”、“粤菜”等分类。
第二步:IDEA 项目导入
- 打开IDEA,选择Open→ 选中解压后的根目录(含pom.xml文件);
-关键设置:在弹出的“Import Project”窗口,勾选“Import project from external model” → “Maven”,并确保“Create module groups”和“Auto-import”已勾选;
- 若出现“Cannot resolve symbol ‘xxx’”,右键项目 →Maven→Reload project;若仍报错,检查File→Project Structure→Project Settings→Project→Project SDK是否指向正确的JDK 8或11。
第三步:application.yml 配置修正
- 打开src/main/resources/application.yml,找到spring:节点下的datasource:配置块;
- 将url、username、password改为你的本地MySQL配置:yaml spring: datasource: url: jdbc:mysql://localhost:3306/takeaway?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8mb4 username: root password: your_mysql_password # 默认常为root或空
-致命细节:serverTimezone=Asia/Shanghai必须添加,否则Spring Boot 2.7.x会因时区不匹配报java.sql.SQLException: The server time zone value '...' is unrecognized;useUnicode=true&characterEncoding=utf8mb4确保中文支持。
完成以上三步,右键TakeawayApplication.java→Run 'TakeawayApplication',控制台输出Started TakeawayApplication in X.XXX seconds即表示启动成功。此时访问http://localhost:8080/backend/index.html,应看到管理后台登录页;访问http://localhost:8080/front/index.html,应看到用户端首页。
4.2 关键功能验证:用最小操作集验证系统健康度
启动成功只是开始,必须通过几个关键操作验证各模块联动是否正常。我设计了一套5分钟验证清单,覆盖核心业务流:
验证1:管理员登录与菜品上架
- 后台地址:http://localhost:8080/backend/index.html
- 账号密码:admin/123456(takeaway.sql中预置)
- 登录后,点击“菜品管理” → “新增菜品”,填写名称“水煮牛肉”、价格“68.00”、选择分类“川菜”,上传一张图片(任意jpg/png),点击“提交”
-预期结果:页面跳转回菜品列表,新菜品出现在末尾;数据库dish表中新增一行,status=1(上架状态)
验证2:用户下单全流程
- 前台地址:http://localhost:8080/front/index.html
- 使用测试账号test1/123456登录(takeaway.sql预置)
- 在首页搜索“水煮牛肉”,点击进入详情页 → “加入购物车” → 右上角购物车图标 → “去结算”
- 填写收货地址(可直接用已有地址),点击“去支付”
-预期结果:跳转至订单成功页,显示订单号;数据库orders表新增一条status=2(已支付)记录,order_detail表新增对应明细
验证3:订单状态变更模拟
- 切回后台,进入“订单管理”,找到刚生成的订单,点击“派送中”
-预期结果:订单状态变为3;前台用户登录后,在“我的订单”中看到该订单状态同步更新为“派送中”
这套验证清单的价值在于:它不依赖自动化测试,而是通过人眼可观察的界面反馈和数据库记录,快速定位问题层级。例如,若验证1失败(新增菜品无反应),问题大概率在DishController.save()或DishService.save();若验证2失败(结算时提示“购物车为空”),则需检查ShoppingCartController.list()是否正确从ThreadLocal获取了当前用户ID;若验证3失败(后台改状态前台不更新),则可能是WebSocket未启用或前端轮询逻辑失效。这种“现象→定位→修复”的闭环,正是工程能力的核心。
4.3 实训报告深度解读:如何把一份Word文档变成学习路线图
这份《瑞吉外卖实训报告.doc》绝非形式主义的产物,而是将整个项目拆解为可教学单元的说明书。我建议学生这样用它:
第一步:逆向阅读——从报告结论反推代码设计
报告第3章“系统设计”中写道:“为降低耦合,用户登录状态采用JWT Token进行无状态认证,Token有效期设为2小时”。此时,不要急着看代码,先思考:JWT Token存在哪?如何生成?如何校验?然后打开common.jwt.JwtUtil类,你会发现createToken()方法用HMACSHA256算法签名,parseToken()方法用相同密钥解析——这印证了报告所述。再看LoginCheckInterceptor.preHandle(),它从Header取Authorization,调用JwtUtil.parseToken(),并将解析出的userId存入ThreadLocal。整个链路一气呵成,报告文字就是代码骨架。
第二步:对照实验——修改报告中的“可扩展点”并观察效果
报告第5章“总结与展望”提到:“当前文件上传仅支持本地存储,后续可扩展为阿里云OSS”。这是一个绝佳的动手点。找到utils.FileUploadUtil类,其upload()方法目前将文件保存到static/img/目录。你可尝试:
- 修改upload()方法,调用阿里云OSS SDK(需添加aliyun-sdk-oss依赖);
- 将返回的OSS URL替换本地路径;
- 更新dish、setmeal等表的image字段存储逻辑。
这个过程,你会深刻理解“本地存储”与“云存储”的抽象差异,以及FileUploadUtil作为策略模式接口的价值。
第三步:答辩准备——把报告中的“设计理由”变成口语化表达
报告第2章“需求分析”写道:“用户需能收藏喜爱的菜品,便于快速复购”。这句话背后是user_favorite关联表的设计。答辩时,老师若问“为什么不用在user表加一个favorite_dish_ids JSON字段?”,你可以回答:“JSON字段虽灵活,但无法建立外键约束,且查询‘收藏了某菜品的所有用户’时无法走索引,性能差;而关联表支持双向索引,符合第三范式,也方便未来扩展收藏时间、排序权重等属性。”——这种将设计原则转化为业务语言的能力,正是高分答辩的关键。
5. 常见问题与排查技巧实录:那些在深夜调试时救过命的经验
5.1 启动报错:Failed to configure a DataSource: 'url' attribute is not specified
这是新手遇到的第一座大山。表面看是application.yml没配数据库URL,但深层原因往往更隐蔽:
| 现象 | 根本原因 | 解决方案 |
|---|---|---|
application.yml里明明写了url,却仍报错 | YAML缩进错误!spring:和datasource:必须顶格,url必须比datasource:多2个空格 | 用IDEA打开yml文件,开启View→Active Editor→Show Whitespaces,检查空格是否为2个(非Tab) |
配置了url,但报Access denied for user 'root'@'localhost' | MySQL 8.0默认认证插件为caching_sha2_password,而Spring Boot 2.7.x的MySQL Connector/J 8.0.28默认不支持 | 在MySQL命令行执行:ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'your_password';FLUSH PRIVILEGES; |
一切配置正确,但启动时提示Driver class not found: com.mysql.cj.jdbc.Driver | pom.xml中MySQL驱动版本与Spring Boot 2.7.x不兼容 | 将mysql:mysql-connector-java版本从8.0.33降为8.0.28(Spring Boot 2.7.x官方推荐版本) |
注意:YAML对空格极其敏感,
url前多一个空格或少一个空格,都会导致整个spring.datasource配置块失效,Spring Boot会认为“没配数据源”,从而报此错。这是血泪教训——我曾帮一个学生调试2小时,最后发现是url前面用了Tab键而非空格。
5.2 功能异常:前台“我的订单”列表为空,但数据库明明有数据
这个问题90%源于时间戳时区错位。orders表的create_time字段是datetime类型,而Java代码中new Date()生成的时间戳是系统本地时区(如东八区),若MySQL服务器时区设为UTC,则存储的时间会比实际晚8小时。当OrderController.page()方法执行分页查询时,LambdaQueryWrapper的ge()(大于等于)条件可能因时区偏差而过滤掉所有记录。
快速诊断法:
1. 在MySQL中执行:SELECT create_time, DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s') FROM orders LIMIT 1;
2. 在Java中打印:System.out.println(new Date());
3. 对比两者时间差。若相差8小时,即为时区问题。
终极解决方案:
- 在application.yml的spring.datasource.url末尾,强制指定时区:?serverTimezone=Asia/Shanghai
- 在MySQL服务器配置文件my.cnf中,添加[mysqld] default-time-zone='+08:00'
- 重启MySQL服务,重新执行takeaway.sql
5.3 文件上传失败:Failed to parse multipart servlet request
当点击“上传菜品图片”按钮无反应或报500错误,根源通常是文件大小限制。Spring Boot默认spring.servlet.multipart.max-file-size=1MB,而一张高清菜品图常达2~5MB。
三步修复:
1. 在application.yml中增加配置:yaml spring: servlet: multipart: max-file-size: 10MB max-request-size: 10MB
2. 确保FileUploadUtil类中,transferTo()方法的目标路径static/img/目录存在且IDEA有写入权限(Windows下注意杀毒软件拦截);
3. 若用Nginx代理,还需在Nginx配置中添加:client_max_body_size 10M;
5.4 前端样式错乱:后台页面CSS/JS加载404
访问/backend/index.html时,F12看到大量GET http://localhost:8080/backend/css/common.css net::ERR_ABORTED 404。这是因为Spring Boot静态资源默认映射路径为/**,而/backend/是前端HTML的路径前缀,并非资源目录。
正确做法:
- 将所有静态资源(css、js、img)放在src/main/resources/static/目录下,而非src/main/resources/static/backend/;
-index.html中引用路径改为相对路径:<link rel="stylesheet" href="/css/common.css">;
- Spring Boot会自动将src/main/resources/static/映射为/根路径,因此/css/common.css实际指向static/css/common.css。
这个错误暴露了一个关键认知:Spring Boot的静态资源处理与前端路由是两套独立机制。/backend/是前端HTML的URL路径,而资源加载路径由<link>标签的href属性决定,必须与Spring Boot的静态资源映射规则对齐。
6. 教学延伸与二次开发指南:让这个项目真正为你所用
这个瑞吉外卖项目,其最大价值不在于“它是什么”,而在于“你能把它变成什么”。我带过的优秀学生,几乎都做过以下三类延伸开发,它们难度递进,但每一步都扎实地提升了工程能力:
延伸1:接入微信小程序(轻量级改造)
-目标:将现有H5前台,改造为微信小程序,复用后端API;
-关键改动:
- 后端:LoginCheckInterceptor中,token来源从Header改为wx.login()返回的code,调用微信接口https://api.weixin.qq.com/sns/jscode2session换取openid;
- 前端:小程序app.js中,App.onLaunch()调用wx.login()获取code,发送给后端换取自定义登录态token;
- 安全加固:application.yml中增加wechat.appid和wechat.secret配置,避免硬编码;
-教学价值:理解“前端登录态”与“后端Session”的解耦,掌握OAuth2.0授权码模式在小程序中的落地。
延伸2:订单超时自动取消(定时任务增强)
-目标:订单创建30分钟后若未支付,自动取消并释放库存;
-实现方案:
- 引入spring-boot-starter-quartz依赖;
- 创建OrderTimeoutJob类,实现Job接口,在execute()中查询status=1 AND create_time < NOW()-INTERVAL 30 MINUTE的订单;
- 使用@Scheduled(cron = "0 0/5 * * * ?")每5分钟扫描一次(避免高频查询);
- 关键逻辑:更新订单状态为5(已取消),并执行UPDATE dish SET stock = stock + quantity WHERE id IN (SELECT dish_id FROM order_detail WHERE order_id = ?)回滚库存;
-教学价值:掌握分布式环境下“定时任务+数据库乐观锁”的幂等性设计,理解“最终一致性”的工程实践。
延伸3:菜品智能推荐(数据挖掘初探)
-目标:用户浏览菜品时,右侧推荐“购买了此菜品的用户还买了…”;
-技术栈:
- 数据层:基于order_detail表,构建用户-菜品协同过滤矩阵;
- 算法层:用Spark MLlib计算余弦相似度,离线生成dish_id → [similar_dish_ids]映射表;
- 应用层:DishController.getRecommends()接口,从Redis缓存中读取预计算结果;
-教学价值:打通“业务数据→算法模型→工程服务”的全链路,理解推荐系统在中小项目中的轻量化落地。
最后分享一个小技巧:如果你想快速验证某个功能点(比如“购物车合并”),不必每次都走完整下单流程。直接在MySQL中执行:
-- 清空当前用户购物车 DELETE FROM shopping_cart WHERE user_id = 1; -- 插入两条测试数据 INSERT INTO shopping_cart (user_id, dish_id, name, image, amount, quantity) VALUES (1, 101, '宫保鸡丁', '/img/101.jpg', 38.00, 1), (1, 102, '麻婆豆腐', '/img/102.jpg', 28.00, 2);然后刷新前台购物车页面——这种“数据库直写+前端验证”的方式,比写测试用例快10倍,是调试期的效率神器。
这个瑞吉外卖项目,就像一把精心锻造的瑞士军刀:它不追求炫技的锋利,但每一刃都经过千锤百炼,只为解决教学中最真实、最琐碎、也最重要的问题。当你真正吃透它的E-R图、跑通它的SQL脚本、读懂它的实训报告,你就不再是在学一个外卖系统,而是在学习一种构建可靠软件的思维方式——这种能力,远比记住一百个Spring Boot注解,要珍贵得多。
本文还有配套的精品资源,点击获取
简介:高校Java教学常用实训项目,基于Spring Boot开发的瑞吉外卖系统完整实践材料。提供可直接导入IDE运行的Java源码(含标准Maven结构、src目录及pom.xml),配套MySQL数据库初始化脚本takeaway.sql,一键建表并插入基础测试数据。附带TakeAway实体E-R图(PPTX格式),直观呈现用户、商家、菜品、订单、购物车等核心业务实体及其关系。实训报告为Word文档,覆盖需求梳理、模块设计、接口说明、关键代码实现逻辑与功能测试步骤。所有内容按教学场景组织,支持课堂演示、学生分组开发、课程设计答辩及自学复现,适配Java Web、Spring Boot框架、关系型数据库原理等课程实验环节。
本文还有配套的精品资源,点击获取
