简介:本资源是基于SpringBoot与Mybatis-Plus开发的完整外卖平台项目源码及配套数据库,面向Java后端初学者与中级开发者,聚焦Web应用全栈实践与高并发场景下的技术整合。项目已实现用户注册登录(优化为邮箱验证)、商家管理、菜品浏览、下单支付等核心业务,并集成Redis缓存提升响应性能,采用Linux环境部署与主从数据库架构保障数据可靠性,是学习现代企业级Java Web开发的典型实战案例。压缩包共228个文件,含69个Java业务逻辑类、21个HTML页面、22个JS交互脚本、19个JPG/JPEG商品图、18个CSS样式文件及1个SQL建库脚本,整体32.01MB,结构清晰、模块分明,便于按层(controller/service/mapper)或功能(user/order/address)快速定位学习。目前已有5187人下载学习,附带完整可运行代码、数据库初始化脚本与多终端适配样式,开箱即用,适合动手复现、二次开发与技术点专项研习。
1. 瑞吉外卖项目源码及数据库资源:一个真实可跑的 SpringBoot + MyBatis-Plus 全栈教学级实战工程
你是不是也遇到过这种情况:网上搜“SpringBoot 实战项目”,点开十篇有八篇是半成品——没数据库脚本、没接口文档、连登录都卡在 JWT 密钥配置上;或者更糟,代码里硬编码了本地 MySQL 路径、Redis 密码写死在 yml 里,一拉下来就报Connection refused。瑞吉外卖不是那种“看着很全、跑不起来”的 demo。它是一套完整闭环的、面向真实开发流程打磨过的教学级工程:从 Linux 环境下部署 MySQL 5.7 + Redis 6.2 的最小可行组合,到 SpringBoot 2.7.x 主干 + MyBatis-Plus 3.5.x 的分层结构(controller → service → mapper → entity),再到包含管理员后台与移动端接口的双入口设计。它不追求炫技,但每行代码都经得起mvn clean compile和./gradlew build双验证;它适合刚学完 SpringBoot 基础、想用一个“有血有肉”的系统把 IOC、AOP、事务管理、分页插件、逻辑删除、自动填充这些概念串起来的开发者;也适合带新人的导师,直接丢给 A 同学一个压缩包,配一句:“今天下班前,把首页轮播图接口调通,数据从你本地数据库里查出来”。
2. 项目结构与技术栈选型:为什么是 SpringBoot 2.7.x + MyBatis-Plus 3.5.x + Linux 部署组合
2.1 项目层级拆解:从 src/main/java 到 resources 下的真实分工
瑞吉外卖采用标准 Maven 多模块扁平结构(非父子模块嵌套),整个工程根目录下直接包含src/、resources/、sql/、docs/四大核心区域。这种结构看似简单,实则规避了新手最易踩的“模块依赖错乱”坑——比如 service 层误引用了 controller 的注解,或 mapper 接口被放在 test 包下导致启动时扫描不到。
src/main/java下的包结构严格遵循 DDD 分层思想:
com.ruiji是根包名,所有类以此开头;controller包内只放@RestController类,每个类对应一个业务域(如EmployeeController处理员工登录/登出,SetmealController管理套餐 CRUD);service包下分impl子包,所有实现类均实现对应接口(如EmployeeServiceImpl实现EmployeeService),且明确标注@Service;mapper包中全是@Mapper接口,不写 XML 文件,全部通过 MyBatis-Plus 的BaseMapper<T>继承实现通用操作;entity包中实体类字段命名与数据库表列名完全一致(snake_case → camelCase 自动映射已开启),并统一使用@TableName("xxx")、@TableId(type = IdType.ASSIGN_ID)显式声明主键策略。
提示:MyBatis-Plus 的
IdType.ASSIGN_ID使用雪花算法生成 Long 型 ID,避免 MySQL 自增主键在分库分表时的冲突风险。这是瑞吉外卖在早期模拟高并发场景时就埋下的扩展伏笔,不是为了炫技,而是让 A 同学第一次接触分布式 ID 时,代码就在他眼皮底下跑着。
2.2 核心依赖版本锁定:SpringBoot 2.7.x 与 MyBatis-Plus 3.5.x 的协同边界
项目pom.xml中关键依赖版本如下(已去除无关插件,仅保留运行必需项):
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <!-- 注意:非最新 3.x,因 3.x 移除了 javax.servlet --> <relativePath/> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> <!-- 与 SpringBoot 2.7.x 官方兼容矩阵认证版 --> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> </dependencies>这个组合不是随意选的。SpringBoot 2.7.x 是最后一个支持javax.servlet.*包的主干系列,而大量企业遗留中间件(如某高校实验室自研的权限网关 SDK)仍强依赖此包;MyBatis-Plus 3.5.3.1 则是该系列中修复了LambdaQueryWrapper在嵌套条件中isNull()误判空字符串的最后一个稳定版。如果你强行升级到 MP 4.x,queryWrapper.isNull("phone")会把"phone": ""也当成 null 处理——这在瑞吉外卖的“手机号为空则不发短信”业务逻辑里,会导致真实翻车。
2.3 Linux 部署适配性:为什么强调 CentOS 7 / Ubuntu 20.04 而非 Windows
项目application.yml中数据库与 Redis 配置默认使用 Linux 风格路径与服务名:
spring: datasource: url: jdbc:mysql://localhost:3306/ruiji?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull username: root password: 123456 redis: host: 127.0.0.1 port: 6379 password: database: 0注意两点:第一,serverTimezone=Asia/Shanghai是必须项,否则 Linux 系统时区为 UTC 时,MySQL 读取datetime字段会偏移 8 小时;第二,zeroDateTimeBehavior=convertToNull解决 MySQL 5.7 严格模式下'0000-00-00'插入失败问题——这个参数在 Windows 下常被忽略,但在 CentOS 7 的 MySQL 5.7.39 默认安装中,它是开启的。瑞吉外卖的employee表中update_time字段允许为 NULL,但初始化值设为'0000-00-00 00:00:00',没有这个参数,INSERT INTO employee (...) VALUES (...);会直接抛Incorrect datetime value异常。
3. 数据库初始化与环境准备:从 SQL 脚本到 Linux 下 MySQL 5.7 安装验证
3.1sql/ruiji.sql脚本结构解析:四张核心表与逻辑删除设计
项目根目录下的sql/文件夹中,ruiji.sql是唯一需要执行的建库建表脚本。它不包含任何存储过程或触发器,纯粹是 CREATE TABLE + INSERT 语句,适配 MySQL 5.7 语法。核心四张业务表如下:
| 表名 | 主要字段 | 逻辑删除字段 | 说明 |
|---|---|---|---|
employee | id, name, username, password, status, create_time, update_time | is_deleted TINYINT(1) DEFAULT 0 | 员工账号表,status=1 表示启用,is_deleted=1 表示软删 |
category | id, type, name, sort, status, create_time, update_time | is_deleted TINYINT(1) DEFAULT 0 | 分类表,type=1 餐品分类,type=2 套餐分类 |
dish | id, name, category_id, price, image, description, status, create_time, update_time | is_deleted TINYINT(1) DEFAULT 0 | 菜品表,关联 category_id |
setmeal | id, name, category_id, price, image, description, status, create_time, update_time | is_deleted TINYINT(1) DEFAULT 0 | 套餐表,同样关联 category_id |
注意:所有表均未使用外键约束(
FOREIGN KEY)。这是瑞吉外卖刻意为之——在教学场景中,外键会掩盖“级联删除需手动在 service 层实现”的真实业务逻辑。例如删除一个分类时,需先校验该分类下是否有菜品或套餐,若有则禁止删除;若无,则分别更新dish和setmeal表中对应category_id的记录为is_deleted=1。这个过程在CategoryServiceImpl.remove()方法中清晰体现,比ON DELETE CASCADE更利于理解数据一致性保障机制。
3.2 Linux 下 MySQL 5.7 快速安装与字符集校准(以 Ubuntu 20.04 为例)
瑞吉外卖要求 MySQL 字符集为utf8mb4,而非旧版utf8(后者实际只支持 3 字节 UTF-8 字符,无法存 emoji)。在 Ubuntu 20.04 上,执行以下命令完成最小化安装与配置:
# 1. 安装 MySQL 5.7(官方 apt 源) sudo apt update sudo apt install mysql-server-5.7 # 2. 启动服务并设置开机自启 sudo systemctl start mysql sudo systemctl enable mysql # 3. 修改 MySQL 配置文件,强制 utf8mb4 sudo nano /etc/mysql/mysql.conf.d/mysqld.cnf在[mysqld]段落末尾追加:
[mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci skip-character-set-client-handshake = true保存后重启服务:
sudo systemctl restart mysql验证是否生效:
mysql -u root -p -e "SHOW VARIABLES LIKE 'character_set%';" # 正确输出应包含:character_set_server | utf8mb4 # collation_server | utf8mb4_unicode_ci提示:
skip-character-set-client-handshake = true是关键。它强制客户端连接时忽略自身声明的字符集,统一使用 server 级别配置。否则 SpringBoot 应用连接时若未显式指定useUnicode=true&characterEncoding=utf-8,仍可能因客户端默认 latin1 导致中文乱码。
3.3 执行 SQL 脚本与初始数据注入
确保 MySQL 服务运行后,执行建库与导入:
# 创建数据库(显式指定字符集) mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS ruiji CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;" # 导入 SQL(假设 ruiji.sql 在当前目录) mysql -u root -p ruiji < ruiji.sql导入完成后,检查关键数据是否存在:
mysql -u root -p ruiji -e "SELECT COUNT(*) FROM employee;" # 应返回 1(默认管理员账号:admin / 123456) mysql -u root -p ruiji -e "SELECT COUNT(*) FROM category WHERE type=1;" # 应返回 3(默认菜品分类:川菜、粤菜、湘菜)如果COUNT(*)返回 0,大概率是ruiji.sql文件开头缺少USE ruiji;语句,或导入时未指定数据库名。此时需手动进入 MySQL 执行USE ruiji;后再SOURCE ./ruiji.sql;。
4. 项目启动与接口验证:从 IDEA 运行到 Postman 测试管理员登录
4.1 IDEA 中正确配置 SpringBoot 启动参数
在 IDEA 中打开项目后,不要直接点击绿色三角形运行。瑞吉外卖依赖application.yml中的 profile 配置,需显式指定 active profile:
- 点击右上角
Edit Configurations... - 在
Spring Boot类型配置中,找到Active profiles输入框 - 填入
dev(对应application-dev.yml,该文件已预置数据库与 Redis 地址) - 确保
Working directory设置为项目根目录(含pom.xml的路径)
注意:
application-dev.yml中spring.profiles.active: dev是冗余的,真正起效的是启动时传入的--spring.profiles.active=dev。IDEA 配置界面填的就是这个参数。如果漏填,SpringBoot 会加载application.yml默认配置,而该文件中数据库 URL 是空的,必然报Unknown database 'ruiji'。
4.2 启动日志关键信息解读:如何判断是否真成功
启动过程中,关注以下三处日志输出,缺一不可:
# 1. MyBatis-Plus 成功扫描 mapper 接口 [main] INFO c.b.m.s.mapper.ClassPathMapperScanner - Register interface com.ruiji.mapper.EmployeeMapper as MapperScanner # 2. 数据库连接池初始化完成(HikariCP) [main] INFO com.zaxxer.hikari.HikariDataSource - HikariPool-1 - Start completed. # 3. SpringBoot 内嵌 Tomcat 启动成功 [main] INFO o.s.b.w.e.t.TomcatWebServer - Tomcat started on port(s): 8080 (http) with context path ''如果看到Failed to bind properties to DataSourceProperties或Cannot determine embedded database driver class for database type NONE,说明application-dev.yml中spring.datasource配置有语法错误(常见于冒号后少了空格,或引号未闭合)。
4.3 Postman 验证管理员登录接口:/employee/login
瑞吉外卖的登录接口是POST /employee/login,接收 JSON 请求体:
{ "username": "admin", "password": "123456" }正确响应应为:
{ "code": 1, "msg": "登录成功", "data": { "id": 1, "username": "admin", "name": "管理员", "phone": "13812345678", "sex": "1", "idNumber": "110101199003072817", "avatar": "", "status": 1, "createTime": "2022-04-25T17:00:00", "updateTime": "2022-04-25T17:00:00", "createUser": 1, "updateUser": 1, "isDeleted": 0 } }关键点:
code: 1表示成功(瑞吉外卖约定 1=成功,0=失败),data中返回完整员工信息,且status: 1表示账号启用。如果返回code: 0且msg: "密码错误",请确认数据库中employee表的password字段值是否为123456的 BCrypt 加密结果(脚本中已预置,无需手动加密)。
5. 常见问题排查:五条血泪经验总结的启动失败原因清单
5.1 现象:启动时报java.lang.ClassNotFoundException: javax.servlet.Filter
原因:使用了 SpringBoot 3.x 的 starter 依赖,但项目基于 SpringBoot 2.7.x 开发,其 Servlet API 仍为javax.*包,而 3.x 已迁移到jakarta.servlet.*。
解决:检查pom.xml,确保spring-boot-starter-web版本与 parent 的2.7.18严格匹配,删除任何spring-boot-starter-webflux或spring-boot-starter-tomcat的显式声明(它们会被 parent 自动引入)。
5.2 现象:登录接口返回{"code":0,"msg":"系统异常"},控制台无堆栈
原因:Redis 服务未启动,或application-dev.yml中spring.redis.host地址错误(如写成localhost但在 Docker 网络中需用宿主机 IP)。
解决:先执行redis-cli ping确认 Redis 可达;若使用 Docker,将host改为宿主机在 Docker 网络中的 IP(Ubuntu 下常用172.17.0.1);或临时注释掉RedisConfig.java中的@Bean方法,绕过 Redis 初始化。
5.3 现象:访问/backend/index.html页面空白,浏览器控制台报GET http://localhost:8080/backend/index.html 404
原因:静态资源路径配置错误。瑞吉外卖的前端页面放在src/main/resources/static/backend/下,但application.yml中spring.web.resources.static-locations未覆盖该路径。
解决:在application-dev.yml中添加:
spring: web: resources: static-locations: classpath:/static/,classpath:/public/,classpath:/resources/,classpath:/META-INF/resources/确保classpath:/static/在列表首位。
5.4 现象:MyBatis-Plus 分页查询返回空列表,但数据库明明有数据
原因:PageHelper与MyBatis-Plus的分页插件冲突。瑞吉外卖使用的是 MP 自带的PaginationInnerInterceptor,若项目中误引入pagehelper-spring-boot-starter,会导致分页失效。
解决:检查pom.xml,彻底删除所有pagehelper相关依赖;确认MybatisPlusConfig.java中@Bean注册的确实是PaginationInnerInterceptor。
5.5 现象:Linux 下启动报Caused by: java.io.FileNotFoundException: class path resource [application-dev.yml] cannot be resolved
原因:Maven 打包时未将application-dev.yml复制到target/classes/目录。常见于 IDEA 的Build project automatically未开启,或Resources目录未被标记为Sources Root。
解决:在 IDEA 中右键src/main/resources→Mark Directory as→Resources Root;然后File→Synchronize;最后Build→Rebuild Project。
6. 进阶技巧:如何快速验证业务逻辑正确性与自定义扩展点
6.1 用单元测试验证核心 service 方法(以 EmployeeService 为例)
瑞吉外卖虽未内置完整测试套件,但预留了test目录结构。我们可快速为EmployeeServiceImpl添加一个登录逻辑验证测试:
@SpringBootTest class EmployeeServiceTest { @Autowired private EmployeeService employeeService; @Test void shouldLoginSuccessWithCorrectPassword() { // given Employee employee = new Employee(); employee.setUsername("admin"); employee.setPassword("123456"); // 注意:此处是明文,MP 会自动 BCrypt 加密比对 // when Employee result = employeeService.login(employee); // then assertThat(result).isNotNull(); assertThat(result.getId()).isEqualTo(1L); assertThat(result.getUsername()).isEqualTo("admin"); } }运行此测试前,确保application-test.yml已创建并配置了测试专用数据库(如ruiji_test),避免污染开发库。这个测试的价值在于:它把“密码校验逻辑”从 HTTP 接口层下沉到 service 层,让你能脱离网络、Redis、前端,纯粹验证业务规则是否成立。
6.2 修改菜品状态的两种方式对比:RESTful 接口 vs 直接 SQL 更新
瑞吉外卖中,菜品上下架通过PUT /dish/status/{status}接口实现,其中status为 0(停售)或 1(起售)。该接口最终调用DishService.updateStatus(id, status)。我们来对比两种修改方式的差异:
| 方式 | 操作步骤 | 是否触发业务逻辑 | 是否记录操作日志 | 是否更新 update_time |
|---|---|---|---|---|
| RESTful 接口调用 | curl -X PUT "http://localhost:8080/dish/status/1?id=1" | ✅ 调用DishServiceImpl.updateStatus(),含状态合法性校验 | ✅update_time字段由 MP 自动填充 | ✅update_time字段由 MP 自动填充 |
| 直接 SQL 更新 | UPDATE dish SET status=1 WHERE id=1; | ❌ 绕过所有 Java 层校验(如:是否允许对已售罄菜品起售?) | ❌ 无日志,无审计痕迹 | ❌update_time不变,除非手动 SET |
这就是为什么瑞吉外卖的
Dish实体类中,update_time字段标注了@TableField(fill = FieldFill.UPDATE)。它告诉 MP:“每次执行 UPDATE 操作时,自动填充此字段为当前时间”。这个注解配合MetaObjectHandler实现,是比在每个 service 方法里手写dish.setUpdateTime(new Date())更可靠、更不易遗漏的方案。
6.3 自定义扩展:为员工表增加邮箱字段并同步更新 MyBatis-Plus 配置
假设业务需求新增员工邮箱字段,需四步走:
Step 1:数据库 ALTER TABLE
ALTER TABLE employee ADD COLUMN email VARCHAR(50) NULL COMMENT '邮箱';Step 2:Entity 类添加字段
@TableField(value = "email", fill = FieldFill.INSERT_UPDATE) private String email;注意
fill = FieldFill.INSERT_UPDATE,表示插入和更新时均自动填充(若只需插入时填,用INSERT)
Step 3:Mapper 接口无需改动(MP 会自动识别新字段)
Step 4:更新 MetaObjectHandler在MyMetaObjectHandler.java中补充:
@Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "email", String.class, null); // 起始版本 3.3.0+ } @Override public void updateFill(MetaObject metaObject) { this.strictUpdateFill(metaObject, "email", String.class, null); }从那以后我每次给实体类加字段,都强制走一遍这四步 checklist:改库 → 改 Entity → 查 Handler → 本地跑个save()测试。少一步,上线后就可能出现“字段加了但永远存不进数据库”的玄学问题。希望帮到你。
本文还有配套的精品资源,点击获取