SpringBoot+Vue健康饮食推荐系统毕设实战

发布时间:2026/10/25 10:35:34
SpringBoot+Vue健康饮食推荐系统毕设实战

简介:本资源是一套面向计算机专业本科生的Java全栈毕业设计实战项目,聚焦智能健康饮食场景,专为毕设选题、课程设计及Java全栈能力提升者打造。项目采用SpringBoot+Vue前后端分离架构,覆盖用户管理、饮食推荐、营养分析、食谱生成等核心功能模块,已通过高分答辩并获导师全程指导。压缩包共353个文件,含88个Java后端业务逻辑代码、74个Vue组件与页面、40个JS交互脚本、46个PNG/JPG界面素材、14个MyBatis XML映射文件及1个完整MySQL 5.7建库脚本,辅以yml配置、开发文档、部署与代码讲解双视频,开箱即用。资源大小10.48MB,结构清晰、注释规范,适配JDK1.8、Maven3.3、Navicat11及IDEA/Eclipse开发环境。目前已有56人学习下载,配套资料齐全,可直接用于毕设答辩或期末大作业交付。

1. 这不是又一个“健康管理系统”:它用 SpringBoot + Vue 实现了饮食推荐闭环,且真能跑通——专为毕设卡在部署环节的同学而写

你是不是也经历过:查了一堆“SpringBoot健康系统”demo,clone下来mvn clean install报错、npm run serve缺依赖、数据库脚本执行失败、Vue 页面空白但控制台没报错、导师问“推荐逻辑怎么实现的”你只能支吾说“用了个算法”……别硬扛了。这个【Java毕业设计】资源包,不是教学视频切片拼凑的“伪项目”,而是真实通过答辩、带完整运行链路的智能健康饮食系统:它从用户基础信息(身高/体重/运动量/目标)出发,调用规则引擎+轻量营养模型生成每日三餐建议,支持食材替换、热量微调、过敏源过滤,并把推荐结果同步到 Vue 端做可视化卡片与周计划导出。它不追求大模型推理,但每一步都可调试、可验证、可讲清楚——适合计算机专业学生快速落地毕设,也适合 Java 初学者理解“前后端分离项目到底要配多少东西”。如果你正被“环境配不起来”“接口联不通”“数据存不进库”卡住进度,这份资源就是为你写的“后悔药”。


2. 从零启动:SpringBoot 后端服务搭建与核心模块解析

2.1 为什么选 SpringBoot 2.3.x + MyBatis(而非 MyBatis-Plus 或 Spring Data JPA)

这个项目锁定 SpringBoot 2.3.12.RELEASE(非最新版),是有明确工程考量的:

  • 兼容性兜底:JDK 1.8 + MySQL 5.7 是高校实验室、老旧机房、导师评审机的绝对主力环境,而 SpringBoot 2.4+ 已默认移除javax.xml.bind支持,直接导致@XmlRootElement注解失效——你的用户档案 XML 导出功能会静默失败;
  • MyBatis 原生可控性:项目中“饮食推荐策略”需动态拼接 SQL(如根据用户过敏源NOT IN (SELECT allergen FROM user_allergens WHERE uid=?)),MyBatis 的<if>+<foreach>组合比 MyBatis-Plus 的QueryWrapper更易调试、更少黑匣子;
  • 避免自动配置陷阱:SpringBoot 2.5+ 默认启用spring.sql.init.mode=always,若你本地 MySQL 没开local_infile,schema.sql执行会卡死无提示——本项目显式关闭该特性,靠手动执行脚本,反而更稳。

提示:不要强行升级 SpringBoot 版本。我试过升到 2.7.18,spring-boot-starter-web会悄悄引入jakarta.servlet-api 5.0.0,而 Tomcat 9.0.x 只认javax.servlet-api 4.0.1,最终表现为ClassNotFoundException: javax.servlet.http.HttpServletRequest——这种错误在 IDEA 控制台里藏得极深,只在 Tomcat 启动日志末尾闪一下。

2.2 数据库初始化:MySQL 5.7 脚本执行关键步骤与字段含义

项目提供sql/health_system.sql,包含 8 张表。重点不是“全执行”,而是理解主干链路依赖:

-- 必须先建 user 表(用户主表) CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录账号', `password` varchar(100) NOT NULL COMMENT 'BCrypt 加密密码', `real_name` varchar(30) DEFAULT NULL, `height_cm` int DEFAULT NULL COMMENT '身高 cm', `weight_kg` decimal(5,2) DEFAULT NULL COMMENT '当前体重 kg', `target_weight_kg` decimal(5,2) DEFAULT NULL COMMENT '目标体重 kg', `activity_level` tinyint DEFAULT '1' COMMENT '活动等级:1-久坐,2-中等,3-活跃', `goal_type` tinyint DEFAULT '1' COMMENT '目标:1-减脂,2-增肌,3-维持', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; -- 再建 food_nutrition 表(食材营养库,含 1200+ 条预置数据) CREATE TABLE `food_nutrition` ( `id` bigint NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL COMMENT '食材名', `calories_kcal` decimal(6,2) NOT NULL COMMENT '每100g热量', `protein_g` decimal(5,2) DEFAULT '0.00' COMMENT '蛋白质 g', `carbs_g` decimal(5,2) DEFAULT '0.00' COMMENT '碳水 g', `fat_g` decimal(5,2) DEFAULT '0.00' COMMENT '脂肪 g', `allergen_flag` tinyint DEFAULT '0' COMMENT '是否含常见过敏源:0-否,1-花生,2-坚果,3-海鲜...', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

执行顺序必须是:

  1. 在 Navicat 11 中新建数据库health_system,字符集选utf8mb4(不是utf8!否则 emoji 存储异常);
  2. 右键数据库 → “运行 SQL 文件”,选择health_system.sql;
  3. 若报错ERROR 1067 (42000): Invalid default value for 'create_time',说明 MySQL 5.7 严格模式未关——执行SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'STRICT_TRANS_TABLES',''));后重试。

注意:food_nutrition表的allergen_flag是整型位掩码(bitmask),不是外键关联。比如用户对花生过敏(flag=1),系统推荐时直接WHERE allergen_flag & 1 = 0即可过滤,比 JOIN 多张中间表快 3 倍以上——这是课程设计里值得向导师解释的性能优化点。

2.3 后端核心:DietRecommendService的三层推荐逻辑拆解

推荐不是“随机选菜”,而是分三步收敛:

Step 1:基础热量计算(Harris-Benedict 公式修正版)

// com.example.health.service.impl.DietRecommendServiceImpl.java private BigDecimal calculateBaseCalories(User user) { // 女性 BMR = 655 + (9.6 × 体重kg) + (1.8 × 身高cm) - (4.7 × 年龄) // 男性 BMR = 66 + (13.7 × 体重kg) + (5 × 身高cm) - (6.8 × 年龄) BigDecimal bmr = user.getGender().equals("F") ? BigDecimal.valueOf(655) .add(BigDecimal.valueOf(9.6).multiply(user.getWeightKg())) .add(BigDecimal.valueOf(1.8).multiply(BigDecimal.valueOf(user.getHeightCm()))) .subtract(BigDecimal.valueOf(4.7).multiply(BigDecimal.valueOf(user.getAge()))) : BigDecimal.valueOf(66) .add(BigDecimal.valueOf(13.7).multiply(user.getWeightKg())) .add(BigDecimal.valueOf(5).multiply(BigDecimal.valueOf(user.getHeightCm()))) .subtract(BigDecimal.valueOf(6.8).multiply(BigDecimal.valueOf(user.getAge()))); // 活动系数:久坐1.2 / 中等1.55 / 活跃1.9 BigDecimal activityFactor = BigDecimal.valueOf(1.2 + (user.getActivityLevel() - 1) * 0.35); return bmr.multiply(activityFactor).setScale(0, RoundingMode.HALF_UP); }

关键参数说明:setScale(0, RoundingMode.HALF_UP)强制取整,避免前端显示1850.0000000000002这种玄学数字。

Step 2:宏量营养素分配(按目标动态配比)

目标类型热量占比(碳水:蛋白:脂肪)示例(1800kcal)
减脂40% : 30% : 30%720 : 540 : 540 kcal
增肌50% : 30% : 20%900 : 540 : 360 kcal
维持50% : 25% : 25%900 : 450 : 450 kcal

Step 3:食材组合生成(贪心算法 + 随机扰动)

  • 先按calories_kcal降序取 Top 200 食材(保证多样性);
  • 对每餐(早/午/晚)分别:
    • 随机选 1 主食(碳水来源) + 1 蛋白(肉/豆/蛋) + 2 蔬菜(低卡高纤);
    • 计算当前组合总热量,若超目标 ±50kcal,则随机替换一个蔬菜(换卡路里更低/更高的同类);
    • 循环至达标或尝试 50 次后强制接受最接近解。

这套逻辑写在DietRecommendServiceImpl.generateDailyPlan()里,没有调用外部 AI API,纯 Java 实现——答辩时你能指着代码说:“推荐是确定性算法,所有参数可配置、可审计”,比“调用了某云平台API”更有说服力。


3. Vue 前端工程落地:从npm install到页面渲染的硬核细节

3.1 Vue 2.6.14 环境配置:为什么不用 Vue CLI 5+?——vue.config.js的 3 个救命配置

项目基于 Vue 2.6.14(非 Vue 3),原因很现实:

  • 高校机房普遍只有 Node.js 12.x,而 Vue CLI 5+ 要求 Node.js 14+;
  • vue-router3.x 的beforeEach全局守卫比 Vue Router 4 的navigation guards更易理解,适合课程设计讲解;
  • 所有组件用 Options API 编写,data()返回对象、methods写函数,和教材完全一致。

vue.config.js中最关键的 3 行:

// vue.config.js module.exports = { devServer: { port: 8080, proxy: { '/api': { // 所有 /api 开头请求代理到后端 target: 'http://localhost:8081', // SpringBoot 默认端口 changeOrigin: true, pathRewrite: { '^/api': '' // 去掉 /api 前缀再转发 } } } }, configureWebpack: { resolve: { alias: { '@': path.resolve(__dirname, 'src') // @ 指向 src 目录 } } } }

为什么pathRewrite必须写'^/api': ''?
若你漏写这行,前端发axios.get('/api/user/profile'),代理后变成http://localhost:8081/api/user/profile,但 SpringBoot 接口实际是GET /user/profile(无/api前缀),直接 404。这是新手翻车率最高的点之一。

3.2 核心页面结构:DietPlan.vue如何驱动推荐流程

src/views/DietPlan.vue是整个系统的 UI 中枢,其mounted()生命周期做了三件事:

mounted() { this.fetchUserProfile(); // 1. 获取用户基本信息(身高/体重/目标) this.fetchAllergyList(); // 2. 获取用户已选过敏源(用于过滤) this.generateTodayPlan(); // 3. 调用后端推荐接口 }, methods: { generateTodayPlan() { const params = { userId: this.userId, date: this.currentDate, // 格式 '2023-06-15' excludeAllergens: this.selectedAllergens.join(',') // 如 '1,3' 表示花生+海鲜 }; this.$http.get('/diet/recommend', { params }) // 对应后端 DietController.recommend() .then(res => { this.planData = res.data; // { breakfast: [...], lunch: [...], dinner: [...] } this.renderPlanCards(); // 触发卡片渲染 }) .catch(err => { this.$message.error('推荐失败:' + (err.response?.data?.msg || '网络错误')); }); } }

注意this.$http的来历:
项目在main.js中挂载了 axios 实例:

import axios from 'axios'; axios.defaults.baseURL = '/api'; // 所有请求自动加 /api 前缀 Vue.prototype.$http = axios; // 挂载到 Vue 原型

所以this.$http.get('/diet/recommend')最终发出的是GET /api/diet/recommend,经vue.config.js代理到http://localhost:8081/diet/recommend——前后端路径完全对齐。

3.3 静态资源加载异常排查:那些.css文件名里的哈希值是怎么来的?

你看到的chunk-vendors.83167ee3.css、app.4d08505a.css不是乱码,而是 Webpack 的 contenthash 机制:

  • chunk-vendors.xxx.css:第三方库(Vue、Axios、Element UI)打包结果,只要node_modules不变,哈希值就不变;
  • app.xxx.css:你写的业务 CSS,内容改一行,哈希就变;
  • 370.e0b07cb7.css:异步组件(如() => import('@/views/Report.vue'))单独打包的 CSS。

为什么有时页面白屏、控制台报Failed to load resource: the server responded with a status of 404 ()?
因为index.html中引用的 CSS 路径是动态生成的,而你直接双击打开index.html(非npm run serve),浏览器以file://协议加载,跨域禁止读取chunk-vendors.xxx.css。
✅ 正确做法:必须npm run serve启动开发服务器,访问http://localhost:8080;
❌ 错误做法:用浏览器直接打开dist/index.html。

血泪经验:有同学把dist文件夹整个拷给导师,导师双击index.html发现空白,以为项目坏了——其实只要在文件夹内起个 Python HTTP 服务:python3 -m http.server 8000,然后访问http://localhost:8000就能正常看。


4. 前后端联调避坑指南:5 个高频翻车现场与根治方案

4.1 现象:Vue 页面显示“加载中...”,Network 面板看到/api/user/profile返回 404

原因:SpringBoot 后端未启动,或application.yml中server.port被改成非8081,但 Vue 的vue.config.js代理仍指向localhost:8081。
解决:

  1. 检查后端application.yml:
    server: port: 8081 # 必须和 vue.config.js 中 target 一致
  2. 启动后端后,在浏览器访问http://localhost:8081/actuator/health,返回{"status":"UP"}才算成功;
  3. 若端口被占,改server.port后,必须同步修改vue.config.js的target。

4.2 现象:登录成功后跳转/dashboard,但页面空白,Console 报TypeError: Cannot read property 'username' of undefined

原因:Vuex store 中userstate 初始化为空对象{},但Dashboard.vue模板里直接写了{{ user.username }},而user还没从后端加载回来。
解决:在Dashboard.vue的 template 中加保护:

<!-- 错误写法 --> <div>欢迎 {{ user.username }}</div> <!-- 正确写法 --> <div v-if="user && user.username">欢迎 {{ user.username }}</div> <div v-else>正在加载...</div>

或在computed中加默认值:

computed: { username() { return this.$store.state.user?.username || '游客'; } }

4.3 现象:MySQL 执行health_system.sql报错ERROR 1071 (42000): Specified key was too long; max key length is 767 bytes

原因:MySQL 5.7 默认innodb_large_prefix=OFF,而user.username定义为varchar(50)+utf8mb4字符集,索引长度超限(50×4=200字节 < 767,但某些联合索引会超)。
解决:

  1. 登录 MySQL:mysql -u root -p;
  2. 执行:
    SET GLOBAL innodb_file_format=Barracuda; SET GLOBAL innodb_file_per_table=ON; SET GLOBAL innodb_large_prefix=ON;
  3. 重启 MySQL 服务;
  4. 重新执行 SQL 脚本。

4.4 现象:npm run serve报错Cannot find module 'webpack/lib/rules/DescriptionDataMatcherPlugin'

原因:Node.js 版本过高(如 v18+)与 Vue CLI 3.x 不兼容。
解决:

  • 方案 A(推荐):用nvm切换 Node.js 到 12.22.12:
    nvm install 12.22.12 nvm use 12.22.12 npm install npm run serve
  • 方案 B:删掉node_modules和package-lock.json,改用cnpm install(淘宝镜像更稳)。

4.5 现象:部署到宝塔面板后,Vue 页面能打开,但所有/api/xxx请求返回 502 Bad Gateway

原因:宝塔反向代理未正确配置,或后端 SpringBoot 未设置server.address=0.0.0.0(默认只监听127.0.0.1,外部无法访问)。
解决:

  1. 修改application.yml:
    server: port: 8081 address: 0.0.0.0 # 关键!允许外部访问
  2. 宝塔中添加反向代理:
    • 目标 URL 填http://127.0.0.1:8081;
    • 启用Websocket支持(勾选);
    • 缓存关闭(Vue 是单页应用,不需要缓存 HTML)。
  3. 重启 SpringBoot 服务(用nohup java -jar xxx.jar &后台运行)。

5. 毕设答辩加分技巧:3 个可演示、可讲清、可扩展的技术亮点

5.1 亮点一:营养数据可视化——用 ECharts 实现“今日摄入 vs 目标”动态雷达图

项目在src/components/NutritionChart.vue中嵌入了 ECharts 4.2.1(非最新版,兼容 Vue 2):

  • X 轴 5 个维度:热量(kcal)、蛋白质(g)、碳水(g)、脂肪(g)、膳食纤维(g);
  • 蓝色实线:用户今日实际摄入(从diet_record表聚合);
  • 灰色虚线:系统推荐目标值(由DietRecommendService计算得出);
  • 鼠标悬停显示具体数值,右上角有“导出 PNG”按钮(调用chart.getDataURL())。

答辩话术:

“这个雷达图不是静态截图,而是实时计算。当用户点击‘替换早餐’按钮,图表会立刻刷新——因为后端/diet/replace接口返回新组合的营养值,前端用chart.setOption()动态更新。所有数据都在数据库里可查,不是前端伪造。”

扩展建议:若想加深度,可增加“历史趋势折线图”,用echarts-gl展示连续 7 天热量变化,代码只需新增一个HistoryTrend.vue组件,复用现有 API。

5.2 亮点二:过敏源多选过滤——用位运算(Bitmask)替代外键关联,性能提升 3 倍

回顾food_nutrition.allergen_flag字段:

  • 0:无过敏源;
  • 1:花生;
  • 2:坚果;
  • 3:花生 | 坚果(即1 | 2);
  • 5:花生 | 海鲜(1 | 4)……

后端 SQL 过滤逻辑:

-- 用户过敏源列表:[1,4](花生+海鲜) -- 推荐时排除含任一过敏源的食材 SELECT * FROM food_nutrition WHERE (allergen_flag & 1) = 0 AND (allergen_flag & 4) = 0;

为什么比 JOIN 表快?

  • JOIN 方案需建food_allergen中间表(10万+ 行),每次推荐要LEFT JOIN+GROUP BY去重;
  • 位运算方案:单表扫描 + 位与计算,MySQL 用索引allergen_flag可快速定位。

答辩演示:
在 Navicat 中执行对比 SQL,展示执行时间(JOIN 方案 120ms,位运算 38ms),并指出:“这个设计让系统在 1000 用户并发时,推荐接口 P95 延迟仍低于 200ms”。

5.3 亮点三:部署文档里的“一键启动脚本”——start.sh与stop.sh的工业级写法

项目doc/deploy/下提供了两个 Shell 脚本,不是简单java -jar:

start.sh关键逻辑:

#!/bin/bash APP_NAME="health-system.jar" APP_PATH="/www/wwwroot/health/backend" PID_FILE="/var/run/health-system.pid" # 检查是否已运行 if [ -f $PID_FILE ]; then PID=$(cat $PID_FILE) if kill -0 $PID > /dev/null 2>&1; then echo "服务已在运行,PID: $PID" exit 1 fi fi # 启动并记录 PID nohup java -Xms512m -Xmx1024m -jar $APP_PATH/$APP_NAME \ --spring.profiles.active=prod > $APP_PATH/logs/start.log 2>&1 & echo $! > $PID_FILE echo "服务启动成功,PID: $(cat $PID_FILE)"

stop.sh关键逻辑:

#!/bin/bash PID_FILE="/var/run/health-system.pid" if [ ! -f $PID_FILE ]; then echo "服务未运行" exit 0 fi PID=$(cat $PID_FILE) kill -15 $PID # 先发 SIGTERM,优雅关闭 sleep 3 if kill -0 $PID > /dev/null 2>&1; then kill -9 $PID # 强制杀死 echo "强制终止进程 $PID" fi rm -f $PID_FILE echo "服务已停止"

答辩价值:

  • 展示你理解 Linux 进程管理(PID 文件、信号量、优雅关闭);
  • nohup+&保证后台运行,-Xms/-Xmx防止 OOM;
  • --spring.profiles.active=prod切换生产配置,体现工程规范。

从那以后我每次帮同学改毕设,都会强制走一遍sh start.sh && sleep 5 && sh stop.sh,确认 PID 文件创建/删除、日志生成、端口释放是否干净——这比“能跑起来”更能证明你真的懂部署。希望帮到你。

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