简介:本资源是一份面向计算机专业本科生的毕业设计文档,聚焦SpringBoot与Vue前后端分离架构下的地方美食分享网站开发实践,适用于Web全栈开发入门与课程设计参考。文档完整覆盖项目背景、SpringBoot后端框架与Vue.js前端框架的技术原理、B/S架构选型依据、用户/美食/评论/收藏等核心模块的功能设计、MySQL数据库表结构设计,以及系统实现与测试要点,内容兼具理论阐述与工程落地性。资源为单个3.23MB的Word文档(.docx),含中英文摘要、目录、六章正文及规范格式排版,结构清晰,可直接用于开题、中期与答辩材料准备。目前已有116人学习下载,读者可获取一套逻辑严谨、技术主流、模块完整的地方特色Web应用设计范例,尤其适合理解前后端协同开发流程与毕业论文写作规范。
1. 用 SpringBoot + Vue 搭建地方美食分享网站,不是堆技术,而是解决「本地化内容冷启动难、用户互动弱、图片视频加载卡顿」三个真实痛点
一个县城小吃摊主拍了 20 条油炸臭豆腐的短视频,却发不到主流平台——规则太严、流量太散;一位退休教师整理了 37 道本地方言叫法的糕点做法,文档存了十年没公开;游客搜“绍兴黄酒配什么菜”,结果跳出的是全国连锁餐厅的标准化菜单。这些不是数据缺失,而是地域性美食知识缺乏轻量级、可自主运营、带多媒体承载能力的发布载体。SpringBoot + Vue 的组合,恰恰在 Java 生态稳定性、Vue 的组件化交互效率、以及二者对静态资源(尤其是 m3u8 视频流、高分辨率菜品图)的友好支持上形成闭环。它不追求大而全的 SaaS 架构,而是让区县文旅部门、高校社团、甚至单个餐饮协会,用一套可离线部署、MySQL 单库支撑、前后端分离清晰的代码,快速上线一个「能发图文、能播本地视频、能按乡镇打标签、能导出 PDF 菜谱」的专属站点。本文聚焦从零跑通这个闭环:后端用 SpringBoot 3.2+MyBatis-Plus 实现结构化菜品管理与 API 服务,前端用 Vue 3 + Pinia + Element Plus 构建响应式交互,重点解决 m3u8 视频在 Vue 中的兼容播放、MySQL 中地理标签的高效查询、以及 SpringBoot 启动时 banner 乱码等高频实操问题。
2. SpringBoot 后端:用 MyBatis-Plus 实现菜品、店铺、标签三级关联模型,避开 XML 配置与手写 SQL 的低效陷阱
2.1 为什么选 MyBatis-Plus 而非 JPA 或纯 JDBC?——直击地方美食数据的「半结构化」本质
地方美食数据天然带有强地域属性和弱规范性:同一道“梅干菜扣肉”,绍兴用霉干菜、台州用芥菜干、宁波加虾米,字段不能硬编码为ingredient_1/ingredient_2;店铺地址可能填“东山弄口第三棵梧桐树下”,而非标准经纬度;用户上传的视频格式混杂(MP4、HLS/m3u8、甚至手机直接录的 MOV)。JPA 的强对象映射在此场景下会频繁触发@Lob字段膨胀或@ElementCollection嵌套异常;纯 JDBC 则需为每种模糊查询(如“找慈溪产的杨梅相关菜品”)手写LIKE '%慈溪%',性能堪忧。MyBatis-Plus 的QueryWrapper动态条件构建 +@TableField(exist = false)灵活扩展字段 +LambdaQueryWrapper类型安全,恰好匹配这种“核心字段固定(名称、简介、所属地区)、扩展字段动态(方言叫法、推荐酒水、非遗认证编号)”的需求。其内置分页插件也避免了 MySQLLIMIT在大数据量下的偏移性能衰减。
2.2 建表语句与实体类:用@TableName和@TableId显式声明,杜绝驼峰自动转换导致的字段错位
MySQL 建表必须显式定义字符集与排序规则,尤其涉及方言文本(如“㸆”“㸆”“㸆”等生僻字):
CREATE TABLE `dish` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `name` varchar(100) NOT NULL COMMENT '菜名', `description` text COMMENT '简介(支持HTML)', `region_code` varchar(10) NOT NULL COMMENT '所属行政区划代码(GB/T 2260)', `video_url` varchar(500) DEFAULT NULL COMMENT 'm3u8视频地址或MP4路径', `cover_image` varchar(500) DEFAULT NULL COMMENT '封面图URL', `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1-启用,0-禁用', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_region_status` (`region_code`,`status`) -- 地域+状态联合索引,加速首页筛选 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='菜品主表';对应 SpringBoot 实体类需严格匹配:
import com.baomidou.mybatisplus.annotation.*; @TableName("dish") // 显式指定表名,避免默认下划线转驼峰失败 @Data public class Dish { @TableId(type = IdType.ASSIGN_ID) // 使用雪花算法生成ID,避免MySQL自增瓶颈 private Long id; @TableField("name") private String name; // 字段名与数据库完全一致,不依赖自动映射 @TableField("description") private String description; @TableField("region_code") private String regionCode; // 行政区划代码,用于后续GIS关联 @TableField("video_url") private String videoUrl; // 存储m3u8地址,如 /videos/shaoxing/m3u8/123/index.m3u8 @TableField("cover_image") private String coverImage; @TableField("status") private Integer status; @TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; @TableField(fill = FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }提示:
@TableField必须标注所有字段,包括id。若省略,MyBatis-Plus 可能因找不到id字段而报Unknown column 'id' in 'field list'。FieldFill.INSERT_UPDATE确保更新时update_time自动刷新,无需手动 set。
2.3 Mapper 接口与 Service 层:用 LambdaQueryWrapper 实现「按乡镇查特色菜」的精准查询
地方美食的核心查询逻辑是“地域穿透”:游客输入“柯桥区安昌镇”,需返回该镇所有在售菜品,并按热度排序。传统 SQL 的WHERE region_code LIKE '330603%'(柯桥区代码为 330603)效率低下。MyBatis-Plus 提供apply()方法嵌入原生 SQL 片段:
// DishMapper.java public interface DishMapper extends BaseMapper<Dish> { // 自定义方法:按行政区划前缀查询(如'330603'匹配柯桥区所有下级乡镇) @Select("SELECT * FROM dish WHERE region_code LIKE CONCAT(#{prefix}, '%') AND status = 1 ORDER BY create_time DESC LIMIT #{limit}") List<Dish> selectByRegionPrefix(@Param("prefix") String prefix, @Param("limit") Integer limit); } // DishService.java @Service public class DishService { @Autowired private DishMapper dishMapper; public List<Dish> listByTown(String townCode, Integer limit) { // townCode 示例:'330603001'(安昌镇),取前6位'330603'作为区级前缀 String regionPrefix = townCode.substring(0, 6); return dishMapper.selectByRegionPrefix(regionPrefix, limit); } }此方案比LIKE '%330603%'更高效,且避免了region_code字段存储冗余层级(如“浙江省_绍兴市_柯桥区_安昌镇”),保持数据扁平化。
3. Vue 前端:用 Video.js + hls.js 播放 m3u8,绕过浏览器原生限制,实现本地美食视频秒开
3.1 为什么 Vue 原生<video>标签无法直接播放 m3u8?——解析 HLS 协议的底层依赖
m3u8 是苹果提出的 HTTP Live Streaming(HLS)协议索引文件,本质是文本列表(如#EXTINF:10.0,+segment0.ts),浏览器原生<video>仅支持 MP4/WebM 等封装格式,无法解析并下载 ts 分片。强行设置src="xxx.m3u8"会导致 404 或静音黑屏。必须引入 JavaScript 解析器,将 m3u8 转为浏览器可识别的媒体源(MediaSource)。Video.js 是最成熟的解决方案,其videojs-contrib-hls插件(新版已集成至 core)专为此设计,且对 Vue 3 的 Composition API 支持良好。
3.2 Vue 3 组件中集成 Video.js:用onMounted生命周期挂载,避免 SSR 渲染报错
<template> <div class="video-container"> <video ref="videoRef" class="video-js vjs-default-skin" controls preload="auto" :data-setup='{"fluid": true, "aspectRatio": "16:9"}' /> </div> </template> <script setup> import { ref, onMounted, onUnmounted } from 'vue' import videojs from 'video.js' import 'video.js/dist/video-js.css' const props = defineProps({ src: { type: String, required: true } // 传入m3u8地址,如 '/api/videos/123.m3u8' }) const videoRef = ref(null) let player = null onMounted(() => { // 确保DOM渲染完成后再初始化Player if (videoRef.value) { player = videojs(videoRef.value, { sources: [{ src: props.src, type: 'application/x-mpegURL' // 关键:声明HLS类型 }], html5: { hls: { overrideNative: true // 强制使用hls.js,禁用浏览器原生HLS(Safari除外) } }, // 加载优化:预加载首段,减少首帧延迟 preload: 'metadata' }) } }) onUnmounted(() => { if (player) { player.dispose() // 销毁实例,释放内存 } }) </script> <style scoped> .video-container { width: 100%; max-width: 800px; margin: 0 auto; } .video-js { width: 100% !important; height: auto !important; } </style>注意:
overrideNative: true是关键参数。Chrome/Firefox 默认禁用原生 HLS 支持,此配置强制启用hls.js解析;Safari 浏览器则会自动回退到原生支持,保证兼容性。preload: 'metadata'仅加载视频元数据(时长、分辨率),而非全部分片,显著提升首屏速度。
3.3 后端提供 m3u8 文件的 Nginx 配置:解决跨域与 MIME 类型错误
SpringBoot 默认不识别.m3u8和.ts后缀,需在application.yml中添加:
spring: web: resources: static-locations: classpath:/static/,file:/opt/app/videos/ # 将视频目录映射为静态资源Nginx 配置必须声明正确 MIME 类型,否则浏览器拒绝加载:
location /videos/ { alias /opt/app/videos/; # 添加m3u8和ts的MIME类型 types { application/vnd.apple.mpegurl m3u8; video/mp2t ts; } # 允许跨域(若前端域名与后端不同) add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; }验证方式:访问http://your-domain.com/videos/shaoxing/m3u8/123/index.m3u8,响应头应包含Content-Type: application/vnd.apple.mpegurl。
4. MySQL 与 SpringBoot 协同优化:用全文索引加速方言菜名搜索,用tinyint替代枚举字符串降低存储开销
4.1 方言菜名搜索为何不能只用LIKE '%臭豆腐%'?——全文索引(FULLTEXT)的实战配置
用户搜索“臭卤”“灰汁团”“印糕”等方言词时,LIKE查询会全表扫描,10 万条数据下响应超 2 秒。MySQL 5.6+ 的FULLTEXT索引专为文本搜索设计,支持自然语言模式(MATCH ... AGAINST)和布尔模式(+臭 -豆腐)。但需注意:FULLTEXT仅支持MyISAM和InnoDB引擎,且对中文需配合ngram解析器:
-- 创建全文索引(InnoDB引擎) ALTER TABLE dish ADD FULLTEXT(name, description); -- 查询示例:搜索包含“臭豆腐”或“卤味”的菜品 SELECT id, name, description FROM dish WHERE MATCH(name, description) AGAINST('臭豆腐 卤味' IN NATURAL LANGUAGE MODE);提示:
ngram解析器需在 MySQL 配置文件my.cnf中启用:[mysqld] ngram_token_size=2 ft_min_word_len=2重启 MySQL 后,
FULLTEXT索引才能正确切分中文词汇(如“臭豆腐”切为“臭豆”“豆腐”)。
4.2 用tinyint存储状态字段,比varchar(20)节省 95% 存储空间
dish.status字段若定义为varchar(20)存储 “启用”“禁用”“审核中”,每个记录占用 20 字节;改为tinyint(1)(范围 -128~127),仅占 1 字节。100 万条数据节省约 19MB 存储,且tinyint比较速度远快于字符串。SpringBoot 实体类中用枚举映射:
public enum DishStatus { ENABLED((byte) 1), DISABLED((byte) 0); private final byte value; DishStatus(byte value) { this.value = value; } public byte getValue() { return value; } } // Dish.java 中 @TableField("status") private Byte status; // 直接映射为byte,避免Integer包装类开销 // 查询时 queryWrapper.eq("status", DishStatus.ENABLED.getValue());4.3 SpringBoot 连接 MySQL 8.0 的关键配置:解决Public Key Retrieval is not allowed错误
MySQL 8.0 默认启用caching_sha2_password认证插件,SpringBoot 2.3+ 的mysql-connector-java驱动需显式允许公钥检索:
spring: datasource: url: jdbc:mysql://localhost:3306/food_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&characterEncoding=utf8mb4 username: root password: your_password driver-class-name: com.mysql.cj.jdbc.DriverallowPublicKeyRetrieval=true参数必不可少,否则启动报错Public Key Retrieval is not allowed。characterEncoding=utf8mb4确保方言生僻字(如“㸆”)不乱码。
5. 本地开发联调技巧:用 IDEA 快速创建 SpringBoot 项目,用 Vue CLI 代理解决跨域,用 MySQL Workbench 直观调试查询
5.1 IDEA 创建 SpringBoot 项目时的 3 个必选依赖
在 Spring Initializr 页面(https://start.spring.io/)选择:
- Spring Web:提供 RESTful API 基础
- Spring Boot DevTools:热部署,修改 Java 代码后
Ctrl+F9即生效,无需重启 - MySQL Driver:连接数据库(注意:勾选后会自动添加
mysql-connector-java依赖)
避坑:不要勾选
Spring Data JPA,与 MyBatis-Plus 冲突;Lombok可选,但需安装 IDEA Lombok 插件并启用 annotation processing。
5.2 Vue 项目中配置vue.config.js代理,让/api请求无缝转发到 SpringBoot
开发时前端运行在http://localhost:8080,后端在http://localhost:8081,直接调用fetch('/api/dish')会触发 CORS。Vue CLI 的devServer.proxy可透明转发:
// vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8081', // SpringBoot端口 changeOrigin: true, // 修改请求头中的host为target pathRewrite: { '^/api': '' // 去掉/api前缀,后端接收 /dish 而非 /api/dish } } } } }此时前端代码fetch('/api/dish/list')实际请求http://localhost:8081/dish/list,无跨域报错。
5.3 用 MySQL Workbench 执行慢查询分析:定位region_code查询瓶颈
当listByTown接口响应慢时,在 Workbench 中执行:
-- 开启慢查询日志(临时) SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 0.1; -- 记录超过0.1秒的查询 -- 查看最近慢查询 SELECT * FROM mysql.slow_log ORDER BY start_time DESC LIMIT 5; -- 对具体SQL分析执行计划 EXPLAIN SELECT * FROM dish WHERE region_code LIKE '330603%' AND status = 1;若type列显示ALL(全表扫描),说明idx_region_status索引未生效,需检查region_code字段是否为varchar类型(text类型无法使用索引前缀)。
5.4 SpringBoot Banner 乱码解决方案:用 UTF-8 编码生成 ASCII 艺术字
SpringBoot 启动时的 banner 若显示方块,是 IDEA 控制台编码非 UTF-8。在src/main/resources/banner.txt中,用在线工具(如 https://www.bootschool.net/ascii)生成 UTF-8 编码的 ASCII 字:
██████╗ ██████╗ ██╗ ██╗███████╗██████╗ ██╔══██╗██╔══██╗╚██╗ ██╔╝██╔════╝██╔══██╗ ██████╔╝██████╔╝ ╚████╔╝ █████╗ ██████╔╝ ██╔═══╝ ██╔══██╗ ╚██╔╝ ██╔══╝ ██╔══██╗ ██║ ██║ ██║ ██║ ███████╗██║ ██║ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚══════╝╚═╝ ╚═╝并在application.yml中指定编码:
spring: main: banner-mode: console output: ansi: always确保 IDEA 的File → Settings → Editor → File Encodings中Global Encoding和Project Encoding均设为UTF-8。
本文还有配套的精品资源,点击获取