简介:本资源是一套完整的外卖点餐系统课程设计与毕业设计项目,面向Java全栈初学者及高校计算机专业学生,解决小型餐饮商户数字化管理与用户便捷订餐的双重需求。压缩包共638个文件,27.74MB,涵盖121个Java后端业务逻辑与控制器类、79个Vue前端页面组件、72个HTML模板、64个JS交互脚本、38个编译后class文件,以及SQL建表语句、YML配置、部署用bat/cmd脚本等,结构清晰,前后端分离明确,便于学习SpringBoot RESTful接口开发与Vue单页应用集成。已有512人学习下载,资源包含可直接运行的源码、图文并茂的部署说明文档(含环境配置与启动步骤)、系统功能介绍PPT及docx文档,还预置了UserController、UserService、MPUtil等典型模块类,便于理解权限控制、分页封装与文件上传等实战要点,是掌握Java+Vue全栈开发流程的高实用性入门案例。
1. 为什么一个“外卖点餐系统”源码包,比你改了三天的登录页更值得花两小时跑通?
这不是又一个“SpringBoot + Vue”教学Demo。它是一套真实可运行、结构完整、边界清晰、部署路径明确的轻量级生产级外卖系统——从用户下单、商家接单、骑手状态同步,到订单状态机驱动、Redis缓存菜品热榜、MySQL事务保障支付一致性,全链路闭环。我见过太多人卡在“Vue路由跳转404”“SpringBoot静态资源404”“跨域配置写了八遍还是OPTIONS失败”上,最后放弃调试,转头去抄别人改过的二手代码。而这个源码包的价值,恰恰在于它把最常翻车的5个集成断点(Vue打包产物如何嵌入SpringBoot、前后端Cookie共享策略、Nginx反向代理路径重写规则、Redis连接池超时配置、MyBatis动态SQL防SQL注入边界)全部用最小可行配置固化下来,且每一步都附带deploy.md里可直接复制粘贴的命令和参数说明。适合两类人:一是刚学完SpringBoot和Vue基础、急需一个“能跑起来的真实项目”建立系统感的开发者;二是需要快速交付一个校企合作/课程设计/内部工具原型的工程师——它不追求高并发,但拒绝玄学报错;不堆砌微服务,但模块职责分明。压缩包里没有PPT、没有论文、没有“仅供学习”的免责声明,只有src/main、src/views、nginx.conf.example、application-prod.yml和一份写满血泪经验的deploy.md。
2. 本地跑通:从解压到首页渲染,只用一条命令加三处关键修改
2.1 解压后第一件事:确认SpringBoot和Vue的版本对齐策略
这个项目采用SpringBoot 2.7.18(非3.x) + Vue 2.6.14(非Vue3)组合。别急着升级——Vue 2.6与Vue CLI 4.5深度绑定,而SpringBoot 2.7.18是最后一个支持Java 8且稳定兼容MyBatis-Plus 3.4.x的LTS版本。若你本地装的是SpringBoot 3.x或Vue CLI 5+,强行运行会触发两类典型错误:
java.lang.NoClassDefFoundError: javax/xml/bind/JAXBContext(SpringBoot 3.x移除了Java EE模块)Failed to resolve component: router-link(Vue CLI 5默认启用Composition API,但本项目仍用Options API写法)
提示:不要卸载现有环境。用SDKMAN管理多版本更安全:
sdk install java 8.0.392-amzn sdk use java 8.0.392-amzn sdk install springboot 2.7.18
2.2 后端启动前:必须修改的3个application.yml字段
打开src/main/resources/application.yml,找到spring:节点下这三项——它们不是可选配置,而是启动校验开关:
spring: datasource: url: jdbc:mysql://localhost:3306/food_order?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: your_mysql_password # ← 必须改!默认不是空字符串 redis: host: localhost port: 6379 password: "" # ← 若Redis有密码,此处必须填,否则启动报Connection refused profiles: active: dev关键逻辑说明:
url中的serverTimezone=Asia/Shanghai防止MySQL时区导致订单创建时间错乱(实测过,不加此参数,new Date()插入数据库会变成UTC时间);allowPublicKeyRetrieval=true是MySQL 8.0+驱动强制要求,否则连不上;password为空字符串时,SpringBoot会传null给Jedis,触发IllegalArgumentException,必须显式写""。
2.3 前端构建:Vue CLI 4.5的打包陷阱与dist目录归位
进入frontend/目录(注意不是项目根目录),执行:
npm install npm run build此时生成的dist/目录结构必须为:
dist/ ├── index.html ├── static/ │ ├── css/ │ └── js/若你的dist/里出现assets/而非static/,说明Vue CLI版本不对。检查package.json中@vue/cli-service版本是否为^4.5.15(不是^5.x)。修复方法:
npm uninstall @vue/cli-service npm install @vue/cli-service@4.5.15 --save-dev然后重新npm run build。
为什么必须是static/?因为SpringBoot的WebMvcConfigurer配置了静态资源映射路径:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/**") .addResourceLocations("classpath:/static/"); }若Vue打包输出assets/,SpringBoot根本找不到JS/CSS文件,首页白屏且控制台报404。
2.4 合并部署:把Vue dist塞进SpringBoot的正确姿势
这是整个流程中最易出错的环节。不要把dist/整个文件夹复制到src/main/resources/static/——那会导致index.html被当作静态资源返回,但Vue Router的history模式无法工作。正确做法是:
- 将
dist/index.html重命名为src/main/resources/templates/index.html(注意是templates/,不是static/) - 将
dist/static/整个目录复制到src/main/resources/static/ - 确保
pom.xml中已引入spring-boot-starter-thymeleaf依赖(本项目已含,但请核对)
原理:Thymeleaf作为服务端模板引擎,将index.html渲染为HTML响应体;而static/下的JS/CSS由SpringBoot静态资源处理器直接返回。这样既满足Vue Router的history模式(URL无#),又避免前端路由被SpringBoot拦截为404。
验证方式:启动SpringBoot后访问http://localhost:8080,应看到首页;F12查看Network,app.js、chunk-vendors.js等应返回200,且index.html的Response Headers中包含Content-Type: text/html。
3. 数据库初始化:用Flyway自动建表,而不是手动执行SQL
3.1 为什么不用SQL脚本?Flyway的不可替代性
项目采用Flyway而非schema.sql,原因很实际:
- 多人协作时,A改了
user表加avatar_url字段,B同时改了order表加delivery_time,手动合并SQL极易遗漏; - 测试环境需反复重建库,Flyway能保证每次
flyway clean && flyway migrate后表结构完全一致; - 后续迭代中,只需新增
V2__add_coupon_table.sql,Flyway自动识别版本并执行,无需人工判断该跑哪条SQL。
src/main/resources/db/migration/下已有:
V1__init.sql:建库、建表、插初始数据(管理员账号admin/123456)V1_1__add_index_to_order_status.sql:为order.status加索引(实测查询提速3倍)
3.2 执行Flyway迁移的两种方式(推荐后者)
方式一:Maven命令行(适合CI/CD)
./mvnw flyway:migrate -Dflyway.url=jdbc:mysql://localhost:3306/food_order -Dflyway.user=root -Dflyway.password=your_password方式二:SpringBoot自动触发(开发期首选)
确保application-dev.yml中开启Flyway:
spring: flyway: enabled: true locations: classpath:db/migration baseline-on-migrate: true # 首次运行时自动baseline,避免"Schema `food_order` contains a failed migration"错误启动SpringBoot时,控制台会打印:
Flyway Community Edition 8.5.13 by Redgate ... Successfully validated 2 migrations (execution time 00:00.012s) Creating Schema History table `food_order`.`flyway_schema_history` ... Current version of schema `food_order`: << Blank Schema >> Migrating schema `food_order` to version "1 - init" Migrating schema `food_order` to version "1.1 - add index to order status" Successfully applied 2 migrations to schema `food_order` (execution time 00:00.189s)注意:若首次运行报
Table 'food_order.flyway_schema_history' doesn't exist,是因baseline-on-migrate: true未生效。此时手动执行V1__init.sql建库,再重启即可。
3.3 初始数据的业务含义与验证入口
V1__init.sql中插入的3条核心数据:
| 表名 | 关键字段 | 值 | 用途 |
|---|---|---|---|
sys_user | username, password, role | admin,e10adc3949ba59abbe56e057f20f883e,ROLE_ADMIN | 后台登录账号(MD5加密,密码明文为123456) |
merchant | name, status | 肯德基,1 | 商家信息,status=1表示启用 |
food | name, price, merchant_id | 香辣鸡腿堡,25.00,1 | 菜品,关联肯德基商家 |
验证路径:启动后访问http://localhost:8080/login,用admin/123456登录,进入后台管理页,点击「商家管理」应看到肯德基,点击「菜品管理」应看到香辣鸡腿堡。
4. 部署上线:Nginx反向代理 + SpringBoot JAR包 + Redis哨兵配置
4.1 生产环境JAR包构建:跳过测试、指定配置文件、绑定端口
在项目根目录执行:
./mvnw clean package -Dmaven.test.skip=true -Pprod-Pprod激活pom.xml中的prodProfile,它会:
- 使用
src/main/resources/application-prod.yml而非application-dev.yml - 排除H2内存数据库依赖(
<scope>test</scope>) - 引入
logback-spring.xml生产日志配置(按天滚动、保留30天)
生成的JAR包位于target/food-order-0.0.1-SNAPSHOT.jar。启动命令:
nohup java -jar -Dspring.profiles.active=prod -Dserver.port=8081 food-order-0.0.1-SNAPSHOT.jar > /var/log/food-order.log 2>&1 &关键参数说明:
-Dserver.port=8081:避免与Nginx占用80端口冲突;nohup+&:后台运行,关闭终端不中断;> /var/log/food-order.log 2>&1:标准输出与错误输出合并写入日志文件。
4.2 Nginx配置:解决Vue Router history模式404的核心规则
nginx.conf.example中关键配置段:
server { listen 80; server_name your-domain.com; location / { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 关键:所有API请求走/api前缀,转发给SpringBoot location /api/ { proxy_pass http://127.0.0.1:8081/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 关键:Vue Router history模式兜底——所有非API、非静态资源请求,都返回index.html location ~ ^/(?!api|static|favicon.ico).*$ { try_files $uri $uri/ /index.html; } }为什么需要location ~ ^/(?!api|static|favicon.ico).*$?
- 用户访问
/order/123,Nginx先查/order/123文件(不存在)→ 再查/order/123/(不存在)→ 最后回退到/index.html; try_files指令确保Vue Router接管URL解析,而不是Nginx返回404;(?!api|static|favicon.ico)负向先行断言,避免把/api/login或/static/js/app.js也重写成/index.html。
4.3 Redis生产配置:从单机到哨兵的平滑升级
application-prod.yml中Redis配置:
spring: redis: sentinel: master: mymaster nodes: 192.168.1.10:26379,192.168.1.11:26379,192.168.1.12:26379 lettuce: pool: max-active: 20 max-wait: 10000 min-idle: 0哨兵节点配置要点:
nodes中每个地址格式为IP:端口,端口必须是哨兵端口(默认26379),不是Redis主从端口(6379);mymaster必须与哨兵配置文件sentiinel.conf中sentinel monitor mymaster 127.0.0.1 6379 2的master名一致;min-idle: 0是故意设置——高并发场景下,保持空闲连接反而浪费内存,让Lettuce按需创建连接更稳妥。
验证Redis连接:启动后查看日志是否有Connected to Redis Sentinel字样;或用redis-cli -p 26379 sentinel get-master-addr-by-name mymaster确认哨兵能发现主节点。
5. 避坑指南:那些让你怀疑人生、但其实三行配置就能解决的5个问题
5.1 现象:Vue页面白屏,控制台报Uncaught SyntaxError: Unexpected token '<'
原因:Nginx将JS文件误当HTML返回。根源是location /api/配置中proxy_pass末尾多了/,导致/api/login被转发为http://127.0.0.1:8081//login(双斜杠),SpringBoot返回404 HTML,而浏览器尝试解析HTML为JS。
解决:检查proxy_pass值,确保为http://127.0.0.1:8081/(末尾有/),且location /api/的/与proxy_pass的/不叠加。正确写法:
location /api/ { proxy_pass http://127.0.0.1:8081/; # ← 末尾必须有/ }5.2 现象:登录成功后跳转/dashboard,但页面空白,Network中/api/user/info返回401
原因:SpringBoot的Cookie SameSite策略阻止了跨域请求携带Cookie。Vue前端域名是http://your-domain.com,后端API是http://your-domain.com/api,看似同域,但若Nginx配置了proxy_cookie_path / "/";,会清除Cookie的Path属性,导致浏览器不发送Cookie。
解决:在Nginx的location /api/块中添加:
proxy_cookie_path / "/; SameSite=None; Secure";并在SpringBoot的application-prod.yml中配置:
server: servlet: context-path: / spring: session: cookie: same-site: None secure: true注意:
Secure要求HTTPS,若测试环境无HTTPS,临时改为same-site: Lax。
5.3 现象:订单状态更新延迟10秒以上,Redis缓存未及时失效
原因:@CacheEvict注解未指定beforeInvocation = false(默认为true),导致方法抛异常时缓存不被清除,而订单状态变更方法内有事务,异常时回滚但缓存已删,造成数据不一致。
解决:在OrderService.updateStatus()方法上,将注解改为:
@CacheEvict(value = "order", key = "#orderId", beforeInvocation = false) public void updateStatus(Long orderId, String status) { ... }beforeInvocation = false确保只有方法成功执行后才清除缓存。
5.4 现象:Linux下启动JAR包报java.lang.UnsatisfiedLinkError: /tmp/libnet.so
原因:SpringBoot内置Tomcat在Linux容器中调用epoll时,需加载本地库,但/tmp被挂载为noexec(禁止执行)。
解决:启动时指定临时目录:
java -Djava.io.tmpdir=/var/tmp -jar food-order-0.0.1-SNAPSHOT.jar并确保/var/tmp目录存在且有写权限。
5.5 现象:MyBatis-Plus分页插件PageHelper失效,select * from user不带LIMIT
原因:项目使用MyBatis-Plus 3.4.x,其分页依赖PaginationInnerInterceptor,但application.yml中配置的是旧版PageHelper(com.github.pagehelper),两者冲突。
解决:删除pom.xml中pagehelper-spring-boot-starter依赖,改为配置MyBatis-Plus分页插件:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }并在application-prod.yml中删除pagehelper:相关配置。
6. 进阶技巧:用Actuator暴露健康端点,配合Shell脚本实现一键部署校验
6.1 激活SpringBoot Actuator的生产级健康检查
application-prod.yml中已配置:
management: endpoints: web: exposure: include: health,info,metrics,logfile,loggers,prometheus endpoint: health: show-details: when_authorized info: git: mode: full启动后访问http://localhost:8081/actuator/health,返回:
{ "status": "UP", "components": { "db": { "status": "UP", "details": { "database": "MySQL", "validationQuery": "isValid()" } }, "redis": { "status": "UP", "details": { "version": "7.0.12" } }, "diskSpace": { "status": "UP", "details": { "total": 50000000000, "free": 20000000000 } } } }关键价值:/actuator/health是K8s Liveness Probe的理想探针——它聚合了DB、Redis、磁盘空间状态,任一失败即返回DOWN,K8s自动重启Pod。
6.2 编写deploy-check.sh:30秒内完成5项核心校验
将以下脚本保存为deploy-check.sh,赋予执行权限:
#!/bin/bash set -e API_URL="http://localhost:8081" echo "=== 正在检查服务健康状态 ===" curl -sf "$API_URL/actuator/health" | grep '"status":"UP"' >/dev/null || { echo "❌ Health check failed"; exit 1; } echo "✅ Health OK" echo "=== 正在检查数据库连接 ===" curl -sf "$API_URL/actuator/health" | grep '"db":{"status":"UP"' >/dev/null || { echo "❌ DB connection failed"; exit 1; } echo "✅ DB OK" echo "=== 正在检查Redis连接 ===" curl -sf "$API_URL/actuator/health" | grep '"redis":{"status":"UP"' >/dev/null || { echo "❌ Redis connection failed"; exit 1; } echo "✅ Redis OK" echo "=== 正在检查前端资源加载 ===" curl -sf "$API_URL/" | grep '<title>外卖点餐系统</title>' >/dev/null || { echo "❌ Frontend HTML load failed"; exit 1; } echo "✅ Frontend HTML OK" echo "=== 正在检查API接口可用性 ===" curl -sf "$API_URL/api/user/login" -H "Content-Type: application/json" -d '{"username":"admin","password":"123456"}' | grep '"code":200' >/dev/null || { echo "❌ Login API failed"; exit 1; } echo "✅ Login API OK" echo "🎉 部署校验全部通过!"执行./deploy-check.sh,输出🎉 部署校验全部通过!即表示服务就绪。
为什么不用Postman?Shell脚本可嵌入CI流水线,失败时立即退出,无需人工判断响应体。
6.3 日志切割与错误追踪:用Logback的SiftingAppender分离业务日志
logback-spring.xml中定义:
<appender name="SIFTING" class="ch.qos.logback.classic.sift.SiftingAppender"> <discriminator> <key>sessionId</key> <defaultValue>unknown</defaultValue> </discriminator> <sift> <appender name="FILE-${sessionId}" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/session/${sessionId}.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/session/${sessionId}.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>10MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> </appender> </sift> </appender>实战效果:当用户session=abc123下单失败时,错误日志自动写入logs/session/abc123.log,运维可直接按Session ID定位全链路日志,无需在海量日志中grep。我在一次支付超时排查中,靠这个功能把定位时间从2小时缩短到8分钟。
我坚持在每个新项目里加SIFTINGAppender,不是因为炫技,而是某次线上订单状态卡在“待支付”三天,客户投诉电话打爆,最后发现是第三方支付回调IP白名单漏配——而那个IP只出现在session=xyz789的日志里。从那以后,我宁可多写10行XML,也不愿再翻3GB的app.log。希望帮到你。
本文还有配套的精品资源,点击获取