news 2026/10/31 5:07:30

Java外卖系统源码解析:Spring Boot与Vue前后端分离实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java外卖系统源码解析:Spring Boot与Vue前后端分离实战

简介:一份基于Java技术栈的外卖系统完整源码,适合正在学习Java Web开发、准备课程设计或毕业设计的学生,以及想了解餐饮管理系统业务闭环的开发者。资源共171个文件,包含69个Java源文件、26个JavaScript脚本、15个HTML页面、9个SQL脚本及9个CSS样式等,压缩包整体仅5.61MB,配合yml配置文件和Maven相关文件,基本覆盖后端、前端、数据库与部署配置。目前已有624人学习下载,源码模块划分清晰,覆盖员工管理、菜品管理、分类管理、套餐管理和订单记录等核心功能。通过阅读与实践,可掌握Spring Boot、Spring MVC、MyBatis等框架的整合方式,理解数据库表设计、前后端交互及订单状态流转逻辑,是一份难得的综合实战参考。

1. 这份 Java 外卖系统源码到底能干什么:先搞清楚值不值得解压

如果你搜“Java 外卖系统源码”,多半是课程设计快交、或者面试前需要一套能讲清楚的完整业务。这个 zip 躺在下载列表里,最怕解压完发现只有几个 CSS 文件和一个 mvnw.cmd,跑也跑不起来。我的判断是:它不是一个只能展示首页的静态页面,而是一套具备员工管理、菜品分类、套餐组合、订单流转四个核心闭环的 Spring Boot + MyBatis 后台系统,前端用了 Vue + Element UI,所以才会出现 element-index.css、login.css 这类样式文件。适合三类人:做毕设要交差的学生、转行想找个全栈练手的开发、以及想快速梳理订单类和权限类业务模型的 Java 工程师。先别急着解压,往下看它实际能帮你解决什么问题。

2. 拆开压缩包:从 mvnw.cmd 和一堆 CSS 反推项目结构

拿到 zip 第一件事不是双击解压,而是看一眼根目录都有什么。原始资源列表里有 mvnw.cmd、common.css、element-index.css、index.css、demo.css、style.css、page.css、login.css、iconfont.css。这些名字能透露出不少信息:mvnw.cmd 是 Maven Wrapper 的 Windows 启动脚本,说明后端是一个标准 Maven 工程;element-index.css 是 Element UI 的样式索引,iconfont.css 是阿里图标字体,说明前端不是裸 HTML,而是基于 Vue 生态的后台管理界面。这是一个典型的前后端分离项目,只是压缩时可能把 node_modules 和 target 目录清理掉了,只保留了容易被忽略的静态资源。下面我按最常见的工程布局把这个包的结构补全,你在自己机器上按照这个目录校对。

2.1 先别急着跑,把文件清单读懂

先看核心文件的作用,我整理了一张表:

文件作用
mvnw.cmdMaven Wrapper,Windows 下通过它调用固定版本的 Maven,避免本机 Maven 版本不一致导致构建失败
common.css / index.css / page.css公共样式、首页样式、通用页面样式,对应后台管理页面布局
demo.css / style.css示例样式和主题样式,通常用于组件预览
login.css登录页面专用样式,配合登录组件使用
element-index.cssElement UI 的完整样式文件,说明前端组件库是 Element UI
iconfont.css字体图标,用于菜单和操作按钮的图标渲染

从这张表可以确认,这个资源包的核心资产是「Spring Boot 后端 + Vue 前端静态资源」。后端代码在压缩包里可能被省略了,但基于我拆过类似外卖系统的经验,完整工程应该长这样:

food-takeout/ ├── mvnw.cmd ├── pom.xml ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── com/example/food/ │ │ │ ├── controller/ │ │ │ │ ├── EmployeeController.java │ │ │ │ ├── CategoryController.java │ │ │ │ ├── DishController.java │ │ │ │ ├── SetmealController.java │ │ │ │ └── OrderController.java │ │ │ ├── service/ │ │ │ ├── mapper/ │ │ │ ├── config/ │ │ │ └── common/ │ │ └── resources/ │ │ ├── application.yml │ │ ├── mapper/ # MyBatis XML 映射文件 │ │ └── db/ │ │ └── food.sql │ └── webapp/ │ └── frontend/ # 前端 Vue 工程 │ ├── src/ │ │ ├── views/ │ │ │ ├── login.vue │ │ │ ├── dish.vue │ │ │ ├── setmeal.vue │ │ │ └── order.vue │ │ └── styles/ │ │ ├── index.css │ │ ├── login.css │ │ └── common.css

这里有个实际拆包习惯:我不太相信压缩包里的目录名,而是先看pom.xml的依赖、再看application.yml的配置,最后用tree命令列一遍 src 目录,确认 controller、service、mapper 三个层都在。你解压后如果发现没有src目录,缺的就是 Java 代码,那就需要去原发布页找配套源码。

2.2 前后端分离的请求链路:从登录页 CSS 到员工管理接口

很多新手被mvnw.cmd和各色 CSS 弄懵,不知道入口在哪。我一般会先讲清楚请求链路:浏览器加载login.css渲染登录页,用户输入账号密码后,Vue 组件里发一个POST /api/system/employee/login请求;后端EmployeeController接收 JSON,调 Service 层校验数据库里的员工表,成功后签发 token。这个链路里,前端只负责展示和搜集参数,后端才是业务核心。

后端入口在application.yml,我通常把端口、数据源、MyBatis 配置固定成下面这样:

server: port: 8080 servlet: context-path: /api spring: datasource: url: jdbc:mysql://localhost:3306/food_takeout?useUnicode=true&characterEncoding=utf-8&serverTimezone=Asia/Shanghai username: root password: root1234 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 10MB max-request-size: 20MB mybatis: mapper-locations: classpath*:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

注意context-path: /api这一项非常关键。前端所有请求都要以/api开头,和后端 Controller 里的@RequestMapping("/system/employee")拼起来就是完整的/api/system/employee/login。如果改成不配 context-path,前端那边就得改 axios 的 baseURL,很容易出现 404。我在做课程设计演示时,一般保持这个路径不动。

前端 axios 的封装也直接影响联调。Vue 工程里通常有一个request.js,里面会设 baseURL 并统一加 token:

import axios from 'axios' const request = axios.create({ baseURL: '/api', timeout: 5000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }) export default request

baseURL: '/api'和后端的context-path一一对应。开发环境里 Vue 的 devServer 还要配 proxy,把带/api的请求转发到 8080 端口:

devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

如果没配 proxy,浏览器直接访问http://localhost:8081/api/xxx会 404,因为 8081 端口上根本没有这个接口。这是前后端分离最常见的联调事故,先确认 proxy 再查代码。

2.3 数据库与表结构:从六个核心表反推业务模型

外卖系统本质上是「商品 + 订单」模型。员工属于后台管理角色,商品被分类管理,套餐是菜品的组合,订单记录则把商品和顾客串起来。所以数据库至少要有这几张表:employee、category、dish、setmeal、setmeal_dish、orders、order_detail。其中setmeal_dish是套餐与菜品的关联表,用来表达多对多关系——一个套餐包含多个菜品,一个菜品可以出现在多个套餐里。

建表脚本通常长这样:

CREATE TABLE `employee` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `username` VARCHAR(32) NOT NULL COMMENT '登录名', `password` VARCHAR(100) NOT NULL COMMENT 'BCrypt 加密后的密码', `status` INT DEFAULT 1 COMMENT '1启用 0禁用', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `idx_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='员工表'; CREATE TABLE `setmeal_dish` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `setmeal_id` BIGINT NOT NULL, `dish_id` BIGINT NOT NULL, `copies` INT NOT NULL DEFAULT 1, PRIMARY KEY (`id`), KEY `idx_setmeal` (`setmeal_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='套餐菜品关联表';

这里有几个细节值得注意。密码字段我用VARCHAR(100)而不是 32,因为 BCrypt 加密后的密文是 60 位,MD5 才是 32 位;如果你沿用老项目的VARCHAR(32),后面改 BCrypt 就得改表结构,这是典型的历史包袱。所有表都使用utf8mb4,否则 emoji 和生僻字会乱码。setmeal_dish上建idx_setmeal索引,是为了在「根据套餐查菜品」时避免全表扫描,这个查询在订单确认页会被高频触发。

订单表设计也要提前想清楚。orders主表放订单总金额、状态、用户信息,order_detail放某个菜品快照——注意是快照,不是外键关联 dish 表。因为菜品价格和名称随时会改,订单详情必须把下单那一刻的商品信息固定下来,否则历史订单的价格会被菜品编辑拖累。状态字段用status INT,配合代码里的枚举类使用,不要直接存「待支付」这种字符串,否则后续状态流转判断很难写。

CREATE TABLE `orders` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `order_number` VARCHAR(32) NOT NULL, `user_id` BIGINT NOT NULL, `amount` DECIMAL(10, 2) NOT NULL, `status` INT NOT NULL DEFAULT 1, `address` VARCHAR(255) NOT NULL, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `pay_time` DATETIME DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_number` (`order_number`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

整个数据库的表关系,一句话总结:employee 和 category 是基础数据,dish 挂在 category 下,setmeal 通过 setmeal_dish 关联 dish,orders 通过 order_detail 记录下单时的菜品快照。理解了这套关系,你看 Controller 和 Service 代码时就不容易迷路。

3. 把它跑起来:从 JDK 配置到前端页面显示

环境搭建是很多 Java 初学者第一个翻车点,尤其是拿到的源码里只有 mvnw.cmd 没有完整环境时。我复现这套外卖系统用的环境组合是:JDK 1.8(或 11)、Maven 3.6.3、MySQL 5.7(或 8.0)、Vue CLI 4.x。下面步骤按我的操作顺序写,每一步都会告诉你命令含义和可能的失败信号。

3.1 环境检查:先把三行命令跑通

不要一上来就启动项目,先确认基础工具路径正常。Windows 下打开 CMD,依次输入:

java -version mvn -version mysql --version

第一行输出java version "1.8.0_xxx"说明 JDK 8 可用;如果提示不是内部或外部命令,就是JAVA_HOME环境变量没配置。第二行会显示 Maven 版本和 Java 版本,重点看是否和 JDK 匹配——Maven 3.6 配 JDK 8 没问题,配 JDK 17 某些旧插件会报兼容错误。第三行确认 MySQL 客户端能连上本地服务。这三行里任何一行失败,都不要继续往下走,否则后面 90% 的错误都会指向环境问题。

我见过最典型的情况是:Java 装的是 17,项目的pom.xml里放的还是spring-boot-starter-parent 2.3.x,结果启动时直接报UnsupportedClassVersionError。所以拿到源码第一眼看pom.xml里的 Spring Boot 版本,如果< 2.5,JDK 用 8 最稳。

如果java -version失败,把 JAVA_HOME 配好。Windows 10 在「系统属性 → 环境变量」里新增:

JAVA_HOME = C:\Program Files\Java\jdk1.8.0_202 Path = %JAVA_HOME%\bin

配完后重新开一个 CMD,再执行java -version验证。注意只改 Path 不动 JAVA_HOME,某些 IDE 还是会找不到 JDK,两个都要设。

3.2 导入数据库并修改数据源配置

后端跑起来前必须把数据库建好。假设food.sql在src/main/resources/db/下,导入命令:

mysql -uroot -p123456 -e "CREATE DATABASE IF NOT EXISTS food_takeout DEFAULT CHARSET utf8mb4;" mysql -uroot -p123456 food_takeout < src/main/resources/db/food.sql

第一条命令创建库,-uroot -p123456是账号密码,等号后面是你的本机密码。第二条命令把表结构和初始化数据导入。如果food.sql文件里已经有CREATE DATABASE语句,第二步的库名可以省略,但我建议还是显式写上,避免脚本里写死了不存在的库名。

导入后修改application.yml,把数据库地址、账号密码改成你自己的。特别注意serverTimezone=Asia/Shanghai,如果省掉这一句,MySQL 8 会因时区问题抛Server returns invalid timezone,这是我复现时必踩的一条。

3.3 启动后端服务

在项目根目录执行:

mvnw.cmd clean package -DskipTests java -jar target/food-takeout-1.0-SNAPSHOT.jar

第一行直接用项目自带的 Maven Wrapper 打包,好处是不依赖本机 Maven 路径,-DskipTests跳过单元测试,加快构建。第二行启动 jar 包。如果用的是 MySQL 8 而 pom 里依赖的是mysql-connector-java 5.1.x,会报Public Key Retrieval is not allowed,需要在 JDBC URL 上加allowPublicKeyRetrieval=true&useSSL=false。看到Started FoodTakeoutApplication和Tomcat started on port 8080就说明后端起来了。

还有更快的启动方式:

mvnw.cmd spring-boot:run

两种方式效果一样,但第一种先打包再运行更接近生产习惯,也方便排查 target 里有没有缺资源文件。我建议新手用第二种,因为日志会实时打印在控制台,改完代码不用重复打包。

启动阶段常见的失败信号我整理成了表:

日志关键字含义处置
UnsupportedClassVersionErrorJDK 版本过新换 JDK 8
Connection refused数据库没起或端口不对确认 MySQL 服务已启动
Unknown database数据库名写错按 3.2 创建库
Port 8080 was already in use端口被占用看第 5.1 节
FileNotFound: mapper/*.xmlMapper XML 路径错误检查mybatis.mapper-locations

3.4 启动前端页面

前端如果是一个独立的 Vue 工程,在frontend目录执行:

npm install npm run dev

npm install会安装package.json里声明的依赖,网络差时可能要跑几分钟;npm run dev启动开发服务器,默认端口 8081。注意前端端口和后端 8080 不能一样,否则会冲突。如果只有一个静态资源页面,没有package.json,那你就把common.css、element-index.css等文件放到后端的src/main/resources/static目录下,启动后端后直接访问http://localhost:8080/index.html。我拿到精简版源码时经常这么干,能省去装 Node 环境的麻烦。

生产环境或演示环境,我更推荐用 Nginx 托管前端静态文件,再反向代理后端接口。这样前端页面不再依赖 Node 服务:

server { listen 80; server_name localhost; location / { root /data/food-frontend/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; } }

try_files $uri $uri/ /index.html是为了兼容 Vue Router 的 history 模式,刷新页面时不会 404。location /api/里的proxy_pass必须以/结尾,否则会把/api前缀吃进去,导致后端收到/system/employee/login而不是/api/system/employee/login。这个细节我调试了一下午,血泪教训。

3.5 第一个接口验证:管理员登录

页面能打开不代表系统通了,真正验证要调接口。我用 curl 测后端登录:

curl -X POST http://localhost:8080/api/system/employee/login \ -H "Content-Type: application/json" \ -d "{\"username\":\"admin\",\"password\":\"123456\"}"

返回 JSON 里带着 token 和员工信息,说明数据库、密码校验、序列化链路都正常。这里有个小技巧:很多外卖系统初始化管理员密码统一是123456,但源码里可能用 BCrypt 存了密文。你从 SQL 里SELECT password FROM employee看长度,如果是 60 位就是 BCrypt,如果只有 32 位则是 MD5,这两种加密方式对应不同的校验代码,别混着猜。

curl 验证通过后,再打开浏览器走一遍登录页。如果页面报跨域,看 3.4 的 proxy 配置;如果报 401,看 4.1 的拦截器路径。这些坑后面单独讲。

4. 核心业务模块拆解:员工、分类、套餐、订单

当系统跑起来后,就该看代码是怎么写的。这一章我挑四个最具代表性的模块:员工权限、菜品分类、套餐组合、订单状态流转。每个模块都会给出关键代码片段,以及为什么这样设计。

4.1 员工管理:JWT 拦截器与 BCrypt 密码校验

员工管理表面上是 CRUD,真正值钱的是权限控制。一般做法是:登录成功后签发 JWT,后续请求头带上 token,后端用一个拦截器统一校验,没带 token 直接返回 401。Spring Boot 里的配置常常长这样:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new JwtInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/system/employee/login", "/frontend/**", "/index.html", "/css/**", "/js/**"); } }

addPathPatterns("/**")表示所有接口都拦,excludePathPatterns放行登录接口和静态资源。注意登录接口的路径是/system/employee/login,因为这里配置的拦截器路径是相对context-path的,如果写成/api/system/employee/login反而匹配不上,这是很多人配了拦截器发现登录也 401 的原因。

密码校验推荐用BCryptPasswordEncoder:

if (!passwordEncoder.matches(rawPassword, employee.getPassword())) { throw new RuntimeException("用户名或密码错误"); }

matches方法会把明文密码和数据库里的 BCrypt 密文做比对,不需要解密,这是安全上更推荐的方式。如果你在代码里看到MD5Util.md5(rawPassword).equals(...),说明老项目用的是 MD5,购买或下载源码时记住:MD5 无盐值容易被彩虹表撞库,以后要迁移到 BCrypt。

4.2 菜品分类管理:树形结构与图片上传的边界

分类管理可以用一棵树来表示:category表里parent_id = 0表示顶级分类,非 0 表示子分类。前端展示时递归渲染成菜单,后端查询时一次性把全部类别返回,由前端 Vue 里做树形转换。后端接口:

@GetMapping("/category/list") public Result<List<Category>> list() { return Result.success(categoryService.getTree()); }

getTree()的常见实现是先查全部category,内存中根据parent_id组装成List<Category>的父子结构。数据量在几百条时这么做没问题,超过几千条再考虑在 SQL 里用递归查询或者做懒加载。

前端拿到扁平列表转树时,Element UI 的 el-tree 需要children字段。我常在 Vue 里写这样一个转换方法:

buildTree(list) { const map = {} list.forEach(item => { map[item.id] = { ...item, children: [] } }) const tree = [] list.forEach(item => { if (item.parentId === 0) { tree.push(map[item.id]) } else { map[item.parentId].children.push(map[item.id]) } }) return tree }

这里两个 forEach 就完成 O(n) 建树,避免嵌套递归导致深层分类卡顿。如果后端返回的是嵌套结构,前端就不需要这个工具函数,但接口设计上扁平结构更容易复用,我倾向后端直接返回树。

菜品图片上传是另一个坑点。常见的 Controller:

@PostMapping("/dish/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) { String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String newName = UUID.randomUUID().toString().replace("-", "") + ext; String path = uploadDir + "/" + newName; file.transferTo(new File(path)); return Result.success("/upload/" + newName); }

这里uploadDir通常来自application.yml自定义配置,比如file.upload-dir=D:/upload/。返回的 URL 要能被前端访问,所以要么把 uploadDir 映射为静态资源路径,要么用 Nginx 对/upload/做 alias。新手最容易在这个地方踩坑:文件确实存到了本地磁盘,但页面拿/upload/xxx.jpg去请求时 404,就是因为 Spring Boot 默认没有把磁盘目录映射到静态资源。

解决方式是在配置类里加资源映射,通常都会跟 5.4 节联动:

@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:D:/upload/"); }

这样图片 URL 才能被访问到。真实项目还会做图片压缩和格式校验,不只是存文件,简易版源码能跑通就行。

4.3 套餐管理:多对多关联与事务回滚

套餐是「一个套餐包含多个菜品,一个菜品可被多个套餐引用」的多对多模型,必须用中间表setmeal_dish存储关系。新增套餐时,要同时插入setmeal主表和setmeal_dish关联表,两步必须要么都成、要么都回滚,用@Transactional是最简洁的写法:

@Transactional(rollbackFor = Exception.class) public Long saveSetmeal(SetmealDTO dto) { Setmeal setmeal = new Setmeal(); BeanUtils.copyProperties(dto, setmeal); setmealMapper.insert(setmeal); List<SetmealDish> dishList = dto.getDishList(); for (SetmealDish item : dishList) { item.setSetmealId(setmeal.getId()); setmealDishMapper.insert(item); } return setmeal.getId(); }

rollbackFor = Exception.class很关键:如果不指定,只有 RuntimeException 才会触发回滚,而自定义的业务异常如果继承 Exception,就不会回滚,导致主表有记录、关联表没有,套餐点进去是空的。我习惯在 Service 层再手动检查一遍:

if (dishList == null || dishList.size() == 0) { throw new RuntimeException("套餐至少包含一个菜品"); }

放在事务方法开头,避免插入空套餐。事务方法内部也不要捕获异常再吞掉,要让异常抛出才能回滚。

修改套餐和新增不同,需要先删掉中间表里所有旧关联记录,再重新插入。这个操作同样要保证事务:

@Transactional(rollbackFor = Exception.class) public void updateSetmeal(SetmealDTO dto) { setmealMapper.update(dto); setmealDishMapper.deleteBySetmealId(dto.getId()); for (SetmealDish item : dto.getDishList()) { item.setSetmealId(dto.getId()); setmealDishMapper.insert(item); } }

先删后插的写法简单,但并发时会有一瞬间套餐是空的。要求高的话可以改成先校验再批量覆盖,课程设计阶段先删后插完全够用。

从数据库层面看,中间表的联合唯一索引也应加上,防止同一套餐重复添加同一菜品。修改建表 SQL:

ALTER TABLE setmeal_dish ADD UNIQUE KEY `uk_setmeal_dish` (`setmeal_id`, `dish_id`);

索引还能加速根据 dish_id 反查哪些套餐包含该菜品,这个操作在「菜品已售罄需要批量下架套餐」时特别有用。

4.4 订单流程:状态枚举与并发扣库存

订单是外卖系统的核心,状态不能靠手写数字,一定要用枚举管理。常见状态定义:

public enum OrderStatus { PENDING_PAYMENT(1, "待支付"), PAID(2, "已支付"), DELIVERING(3, "配送中"), COMPLETED(4, "已完成"), CANCELLED(5, "已取消"); private final Integer code; private final String desc; OrderStatus(Integer code, String desc) { this.code = code; this.desc = desc; } }

枚举的好处是状态流转的代码里有语义,不会出现if (order.getStatus() == 2)这种魔法数字。后续加状态只改枚举类,引用处自动感知。

订单创建和库存扣减必须放同一个事务里。并发环境下直接先查库存再 update 是危险的,两个线程同时查到库存 1,都去扣就变成 -1。我推荐用条件更新的原子 SQL:

UPDATE dish SET stock = stock - #{num} WHERE id = #{dishId} AND stock >= #{num}

这段 SQL 的WHERE stock >= #{num}是并发兜底:如果库存不够,影响行数为 0,Java 里再判断:

int rows = dishMapper.decreaseStock(dishId, num); if (rows == 0) { throw new RuntimeException("库存不足"); }

这是比悲观锁更轻的乐观锁思路,很适合外卖这类读多写少的场景。注意stock字段别用BIGINT以外类型,否则扣到负数还查不出来。

支付回调更要注意幂等:支付宝或微信回调可能发送多次,所以订单表的支付状态更新必须带上原状态条件:

UPDATE orders SET status = 2, pay_time = NOW() WHERE id = #{orderId} AND status = 1

更新行数为 0 说明订单已被处理过,直接返回成功,不要再下发配送消息。这类幂等判断是线上事故高发地,我在演示源码时经常单独拉出来讲。

订单超时未支付的处理也很典型。简单方案是定时任务每分钟扫一次:

@Scheduled(fixedDelay = 60000) public void closeExpiredOrders() { orderMapper.closeByStatusAndTime(OrderStatus.PENDING_PAYMENT.getCode(), LocalDateTime.now().minusMinutes(15)); }

批量把超过 15 分钟还没支付的订单改成已取消,同时把锁定的库存释放。正式项目会用延迟队列或消息队列,但源码讲解阶段,定时器能说明白这个道理就够。

5. 避坑笔记:从数据库乱码到端口占用,六条真实踩坑记录

这一章是我在多个环境里复现这套源码后攒下的经验。每条都按「现象 → 原因 → 解决」整理,你照着这个列表排查,比看十篇报错文章快。

5.1 启动后端立刻提示端口被占用

现象:java -jar启动后马上报Port 8080 was already in use.。 原因:本机已有其他服务占用了 8080,或者上一次启动的后台进程没杀掉。 解决:先查占用:

netstat -ano | findstr 8080

拿到 PID 后执行taskkill /F /PID 1234强制结束。如果 8080 上有其他不想杀的服务,那就改application.yml的server.port为 8082,同时同步修改前端 axios 的 baseURL 和 Nginx 的 proxy_pass。注意前端开发服务器默认 8081,前后端两个端口不要撞。

5.2 MySQL 8 报 Public Key Retrieval is not allowed

现象:启动时抛java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed。 原因:MySQL 8 默认使用caching_sha2_password认证,在 SSL 未开启时不允许客户端直接获取公钥,而 pom 里依赖的驱动版本又不够新。 解决:两条路选一条,优先改 JDBC URL:

spring: datasource: url: jdbc:mysql://localhost:3306/food_takeout?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai

如果还不行,检查pom.xml里 MySQL 驱动是否是com.mysql:mysql-connector-j8.0 系列,5.1 的旧驱动即使加了参数也经常不认。

5.3 登录后访问其他接口返回 401,登录接口也被拦截

现象:curl 登录接口返回 401,明明已经在拦截器放行了登录路径。 原因:拦截器配置的放行路径少了context-path前缀,或者路径写多了/api。Spring Boot 中excludePathPatterns匹配的是去掉context-path之后的 URL。 解决:如果server.servlet.context-path=/api,拦截器里写/system/employee/login。如果还是 401,把拦截器放行写在addPathPatterns("/**")之前,并加一行日志打印请求 URI,看拦截器拦截到的真实路径长什么样。我一般用request.getRequestURI()输出到控制台,数字段就知道前缀问题了。

5.4 菜品图片上传成功但页面显示裂图

现象:上传接口返回了/upload/xxx.jpg,但浏览器访问该路径 404。 原因:文件写入了本地磁盘 D:/upload,但 Spring Boot 没有把/upload/**映射到那个目录,前端拿到的 URL 无法路由到静态资源处理器。 解决:在配置类里加资源映射:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:D:/upload/"); } }

注意D:/upload/最后必须带斜杠,不带斜杠 Spring 会当成文件而不是目录,引起新的 404。如果是部署在 Linux,路径写成file:/home/user/upload/。

5.5 控制台中文乱码,SQL 查询返回问号

现象:后端日志打印出的中文全是???,数据库里SELECT * FROM dish也是一堆问号。 原因:三处编码不统一,最常见的是 MySQL 连接串没指定characterEncoding=utf-8,或者建库时不是utf8mb4,又或者 Windows 控制台默认 GBK。 解决:先保证 JDBC URL 里有characterEncoding=utf-8,再检查建表语句的DEFAULT CHARSET=utf8mb4,最后在pom.xml里给 Spring Boot 加启动参数-Dfile.encoding=UTF-8。如果是 IDEA 运行,在 VM options 里填-Dfile.encoding=UTF-8;Windows 控制台运行则执行chcp 65001切换 UTF-8 代码页。这三处都改完,乱码基本绝迹。

5.6 Maven 依赖下载超时,构建卡死或 jar 包损坏

现象:mvnw.cmd clean package一直卡在下载依赖,或者报Could not transfer artifact、ZipFile invalid。 原因:Maven 默认中央仓库在国外,网络不稳;断点续传机制不完善,下载一半的.lastUpdated文件被 Maven 当缓存用,导致后面的构建一直在拿坏文件重试。 解决:换国内镜像源。在Maven 安装目录/conf/settings.xml的<mirrors>标签里加:

<mirror> <id>aliyun</id> <url>https://maven.aliyun.com/repository/public</url> <mirrorOf>central</mirrorOf> </mirror>

如果已经下载了一半,需要强制更新快照:

mvnw.cmd clean package -U

-U参数会强制刷新 SNAPSHOT 依赖。项目里如果使用项目自带的 Maven Wrapper,镜像源配置在~/.m2/settings.xml中,别改错位置。

以上六条覆盖了从启动到使用的绝大多数坑。排查的时候记住一个原则:只改一个变量,重启验证。别同时改端口又改编码又改数据库,到时候报错都不知道是哪个引起的。

6. 进阶:把订单模块改造成 Redis 缓存 + 微服务就靠这三步

这套系统跑通之后,如果想让它在简历上更有分量,别停在「能启动」这一步。我一般会按三步做改造,每一步都能单独拿出来讲。

第一步是给分类接口加 Redis 缓存。菜品分类是典型的读多写少,每次请求都查数据库很浪费。用 Spring Data Redis 很简单:

@GetMapping("/category/list") public Result<List<Category>> list() { String key = "food:category:tree"; String cached = stringRedisTemplate.opsForValue().get(key); if (cached != null) { return Result.success(JSON.parseArray(cached, Category.class)); } List<Category> tree = categoryService.getTree(); stringRedisTemplate.opsForValue().set(key, JSON.toJSONString(tree), 30, TimeUnit.MINUTES); return Result.success(tree); }

这里设置了 30 分钟过期时间。改造后,分类接口在缓存命中时耗时从几十毫秒降到个位数毫秒。面试提这个点时,要强调缓存穿透和一致性问题:后台修改分类时必须删除缓存,否则前台永远显示旧分类。

第二步是给下单接口加分布式幂等。前面 4.4 的订单状态幂等只针对支付回调,真正的高并发场景下,用户连点两下提交按钮会产生两个订单。更优雅的方案是前端生成幂等 key,后端用 Redis 判断是否处理过:

public void createOrder(String idempotentKey, OrderDTO dto) { Boolean success = stringRedisTemplate.opsForValue() .setIfAbsent("order:idem:" + idempotentKey, "1", 5, TimeUnit.MINUTES); if (!success) { throw new RuntimeException("请勿重复提交"); } // 继续订单创建逻辑 }

setIfAbsent是 Redis 的 setnx,同一个 key 只能写入一次,天然防止重复提交。

第三步是把订单模块抽成独立微服务。源码里订单和员工耦合在一起,拆服务时先按包名边界划分,把OrderController、OrderService、OrderMapper迁移到新工程,再通过 Feign 调用用户服务和菜品服务。这一步需要引入 Spring Cloud,但属于锦上添花,课程设计做到第二步已经足够优秀。

最后说点实在的。我每次拿到新源码,都会强制走一遍「看目录 → 配数据库 → 跑登录 → 看日志」四件事,全部记录成笔记,这样即使源码被裁剪过也能快速复原。这个习惯救过我好几次。希望你复现这套外卖系统时少走弯路,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 5:06:37

Python图书馆大数据可视化分析系统实战

简介&#xff1a;本资源是一套完整的图书馆大数据可视化分析系统毕业设计源码&#xff0c;面向计算机专业本科生及Python Web开发初学者&#xff0c;聚焦高校图书馆借阅行为、读者画像与馆藏热度等典型数据分析场景。系统基于Django框架构建&#xff0c;集成前端可视化&#xf…

作者头像 李华
网站建设 2026/10/31 5:05:18

C++图形绘制实战:帧缓冲、光栅化与呈现优化

图形绘制这块内容我在带新人的时候反复讲&#xff0c;讲到最后发现真正卡住人的从来不是某个接口怎么调&#xff0c;而是坐标系怎么对、边界怎么判、性能热区在哪。C 实例&#xff08;十二&#xff09;图形绘制的题目看着像个交作业的练习题&#xff0c;但它背后其实压着一条相…

作者头像 李华
网站建设 2026/10/31 5:05:05

音乐知识图谱推荐系统:从数据建模到GNN排序实战

简介&#xff1a;本资源是一套面向计算机、人工智能及相关专业本科生的毕业设计级项目&#xff0c;聚焦知识图谱驱动的音乐推荐系统研发&#xff0c;兼顾学术规范性与工程可落地性。资源包含完整Python源码、配套技术文档及定稿论文&#xff0c;适用于课程设计、毕设选题或AI推…

作者头像 李华
网站建设 2026/10/31 5:04:29

大数据智能决策系统架构与实战指南

1. 大数据智能决策支持系统概述在数据爆炸式增长的今天&#xff0c;企业每天产生的数据量已经达到PB甚至EB级别。传统的数据处理方式就像用勺子舀干海水一样力不从心&#xff0c;而大数据智能决策支持系统正是为解决这一困境而生的"超级抽水机"。这类系统通过整合数据…

作者头像 李华
网站建设 2026/10/31 5:03:57

HTML5基础与核心特性详解:从语义化到现代Web开发

1. HTML基础概念与核心特性 HTML&#xff08;HyperText Markup Language&#xff09;作为构建万维网的基石语言&#xff0c;其核心功能在于定义网页内容的结构与语义。与CSS负责表现、JavaScript负责行为不同&#xff0c;HTML专注于内容的组织与关联。这种分工明确的体系使得W…

作者头像 李华
网站建设 2026/10/31 5:03:57

Python开发经典吃豆人游戏:从入门到实战

1. 项目概述&#xff1a;Python版吃豆人游戏开发全解析吃豆人&#xff08;Pac-Man&#xff09;作为1980年Namco公司推出的经典街机游戏&#xff0c;至今仍是游戏开发入门的绝佳练手项目。这个黄色圆形角色在迷宫内吃豆子、躲避幽灵的核心玩法&#xff0c;恰好涵盖了游戏开发中最…

作者头像 李华