资讯动态

SpringBoot3+Vue3糖尿病饮食推荐系统:营养约束求解与工程落地

发布时间:2026/10/27 20:10:57 来源:尧图企业网站定制
简介这份资源是面向高校计算机专业毕业设计与课程设计场景的糖尿病患者饮食推荐系统完整项目编号25303采用SpringBoot3后端与Vue.js3前端的前后端分离架构数据库选用MySQL8适合正在准备毕设或需要医疗健康类实战项目的开发者参考。压缩包共6个文件约39.22MB包含源码压缩包、数据库SQL脚本、需求文档PDF以及系统录屏MP4覆盖从代码到文档再到演示的完整链路。管理后台支持营养师录入患者信息、健康状况与饮食偏好并制定个性化饮食建议用户前台可查看推荐食谱、记录每日饮食并评价反馈前后端交互与数据持久化逻辑清晰。资源还附带启动教程与录屏演示便于快速部署和二次开发。目前已有130人学习适合需要完整赛题方案、数据库设计与实操演示的读者。字数统计约300字。1. 糖尿病患者饮食推荐系统SpringBoot3Vue.js3 这套组合拳到底能不能打糖尿病饮食管理这件事难的不是“知道该少吃糖”而是每天三顿饭到底吃什么、吃多少、怎么搭配。我见过太多患者拿着医生给的“控制碳水”四个字回家对着冰箱发呆。这个毕业设计题目要做的就是把这四个字翻译成一份能直接照着吃的食谱。技术栈选的是 SpringBoot3 加 Vue.js32025 届计算机毕业设计里这套组合出现的频率很高原因也简单后端够稳、前端够快、生态够熟。但问题在于很多同学做到最后发现系统能跑推荐出来的东西却没法看——要么全是水煮白菜要么热量算错要么用户填完问卷就再也没打开过。这篇笔记不讲虚的就按我实际带过几届毕设的经验把从建表到推荐算法落地的完整路径拆开告诉你哪些地方必须自己写、哪些地方可以偷懒、哪些坑踩一次就够。2. 先搞清楚推荐逻辑不是协同过滤是营养约束求解2.1 为什么直接上协同过滤会翻车很多同学一看到“推荐系统”四个字第一反应就是 UserCF 或 ItemCF。放在电商场景没问题放在糖尿病饮食场景就是灾难。原因有三第一糖尿病患者的数据极度稀疏一个用户可能只填了一次问卷没有历史行为协同过滤根本跑不起来第二饮食推荐有硬约束比如碳水摄入量必须控制在某个区间协同过滤只关心“相似用户喜欢什么”不关心“这个食物能不能吃”第三医疗场景对可解释性要求高你推荐一道菜得说清楚为什么推荐它协同过滤给不出这个理由。我一般会跟学生讲这个系统的推荐核心是基于规则的营养约束求解协同过滤只能作为辅助排序手段。具体来说先根据用户的身体指标算出每日营养目标再从食物库里筛选出满足约束的候选集最后按某种策略排序输出。2.2 营养目标怎么算Mifflin-St Jeor 公式落地每日能量需求的计算业内常用 Mifflin-St Jeor 公式。男性基础代谢率 BMR 10 × 体重(kg) 6.25 × 身高(cm) - 5 × 年龄 5女性 BMR 10 × 体重(kg) 6.25 × 身高(cm) - 5 × 年龄 - 161。然后乘以活动系数得到每日总消耗再根据患者当前体重状态偏瘦、正常、超重调整摄入量。// NutritionCalculator.java public class NutritionCalculator { /** * 计算每日推荐能量摄入 * param gender 性别MALE/FEMALE * param weightKg 体重千克 * param heightCm 身高厘米 * param age 年龄 * param activityLevel 活动系数1.2久坐 / 1.375轻度 / 1.55中度 / 1.725高强度 * param bmiStatus 体重状态UNDERWEIGHT/NORMAL/OVERWEIGHT * return 每日推荐千卡 */ public static double calculateDailyCalories(String gender, double weightKg, double heightCm, int age, double activityLevel, String bmiStatus) { double bmr; if (MALE.equals(gender)) { bmr 10 * weightKg 6.25 * heightCm - 5 * age 5; } else { bmr 10 * weightKg 6.25 * heightCm - 5 * age - 161; } double tdee bmr * activityLevel; // 根据体重状态调整超重减 500kcal偏瘦加 300kcal switch (bmiStatus) { case OVERWEIGHT: return tdee - 500; case UNDERWEIGHT: return tdee 300; default: return tdee; } } }这段代码的关键参数是活动系数和体重状态调整值。活动系数不要自己拍脑袋直接查表久坐办公 1.2每周运动 1-3 次 1.3753-5 次 1.556-7 次 1.725。体重状态调整值我一般设超重减 500、偏瘦加 300这个数值来自临床营养指南的常见建议但你要在论文里注明来源别直接写“经验值”。算出总热量后还要拆成三大宏量营养素碳水化合物占 45%-60%蛋白质占 15%-20%脂肪占 20%-30%。糖尿病患者的碳水比例建议取下限也就是 45% 左右并且优先选择低升糖指数GI的碳水来源。2.3 食物库怎么建别用爬虫用标准食物成分表食物库是这个系统最耗时的部分。我见过有同学写爬虫去抓某美食网站的数据结果字段缺失、单位混乱、GI 值大量为空。正确做法是找一份权威的食物成分表比如《中国食物成分表》标准版把数据整理成结构化格式导入数据库。食物表的核心字段至少包括食物名称、类别谷薯/蔬菜/水果/肉蛋/豆类/油脂、每 100g 热量、碳水、蛋白质、脂肪、膳食纤维、GI 值、GL 值血糖负荷、是否适合糖尿病患者。其中 GI 值不是所有食物都有缺失的可以按类别给默认值比如绿叶蔬菜默认 15精白米面默认 80。-- food_item 表结构 CREATE TABLE food_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 食物名称, category VARCHAR(50) NOT NULL COMMENT 类别, calories DECIMAL(8,2) COMMENT 每100g热量(kcal), carbs DECIMAL(8,2) COMMENT 每100g碳水(g), protein DECIMAL(8,2) COMMENT 每100g蛋白质(g), fat DECIMAL(8,2) COMMENT 每100g脂肪(g), fiber DECIMAL(8,2) COMMENT 每100g膳食纤维(g), gi_value INT COMMENT 升糖指数, gl_value DECIMAL(8,2) COMMENT 血糖负荷, suitable TINYINT DEFAULT 1 COMMENT 是否适合糖尿病患者1是 0否, INDEX idx_category (category), INDEX idx_gi (gi_value) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表时注意suitable字段不是简单按含糖量判断而是要结合 GI 和 GL。比如西瓜 GI 很高72但每 100g 碳水只有 6.8gGL 其实很低约 4.9适量吃是可以的。所以suitable的判断逻辑应该是GL ≤ 10 为适合10 GL ≤ 20 为适量GL 20 为不适合。3. SpringBoot3 后端从接口设计到推荐引擎实现3.1 项目骨架怎么搭别用默认生成器一把梭SpringBoot3 要求 JDK 17 起步这个没得商量。用 IDEA 的 Spring Initializr 生成骨架时依赖勾选要克制Spring Web、Spring Data JPA、MySQL Driver、Lombok、Validation 这五个就够了。别把 Spring Security 也勾上除非你真的要做权限管理否则光是配 Security 就能耗掉你两天。生成后的pom.xml里SpringBoot 版本选 3.2.x 或 3.3.x 都行但要注意 SpringBoot3 把javax.*全部换成了jakarta.*如果你从网上抄了旧版代码所有import javax.persistence.*都要改成import jakarta.persistence.*这个坑每年都有学生踩。!-- pom.xml 关键依赖 -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.5/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependenciesapplication.yml里数据库连接串记得加时区参数serverTimezoneAsia/Shanghai否则插入时间数据时会差 8 小时。JPA 的ddl-auto建议设为update开发阶段方便上线前改成validate。3.2 推荐接口怎么写三步走策略推荐接口是整个系统的核心我一般把它拆成三步第一步根据用户 ID 查出健康档案和营养目标第二步从食物库中筛选候选集第三步按约束条件组合成一日三餐。// RecommendationService.java Service public class RecommendationService { Autowired private FoodItemRepository foodRepo; Autowired private UserProfileRepository profileRepo; /** * 生成一日三餐推荐 * param userId 用户ID * return 三餐食物组合及营养汇总 */ public DailyMealPlan recommend(Long userId) { UserProfile profile profileRepo.findByUserId(userId) .orElseThrow(() - new RuntimeException(请先完善健康档案)); // 1. 计算营养目标 double dailyCalories NutritionCalculator.calculateDailyCalories( profile.getGender(), profile.getWeightKg(), profile.getHeightCm(), profile.getAge(), profile.getActivityLevel(), profile.getBmiStatus()); // 2. 按餐次分配热量早餐30% 午餐40% 晚餐30% double breakfastCal dailyCalories * 0.3; double lunchCal dailyCalories * 0.4; double dinnerCal dailyCalories * 0.3; // 3. 每餐从候选集中选3-4种食物满足热量和GI约束 ListFoodItem breakfast pickFoods(breakfastCal, 早餐); ListFoodItem lunch pickFoods(lunchCal, 午餐); ListFoodItem dinner pickFoods(dinnerCal, 晚餐); return new DailyMealPlan(breakfast, lunch, dinner, dailyCalories); } /** * 贪心策略选食物优先选GL低的热量逼近目标值 */ private ListFoodItem pickFoods(double targetCal, String mealType) { ListFoodItem candidates foodRepo.findBySuitableAndCategoryIn( 1, getCategoriesForMeal(mealType)); // 按GL升序排列GL相同按热量降序 candidates.sort(Comparator.comparing(FoodItem::getGlValue) .thenComparing(Comparator.comparing(FoodItem::getCalories).reversed())); ListFoodItem selected new ArrayList(); double accumulated 0; for (FoodItem item : candidates) { if (accumulated item.getCalories() targetCal * 1.1) { selected.add(item); accumulated item.getCalories(); } if (selected.size() 4) break; } return selected; } }这段代码里pickFoods用的是贪心策略不是最优解但胜在简单可控、可解释。targetCal * 1.1是允许 10% 的热量浮动避免因为食物组合凑不齐而返回空列表。getCategoriesForMeal根据餐次返回不同类别比如早餐包含谷薯、肉蛋、豆类午餐和晚餐在此基础上增加蔬菜。参数方面每餐食物数量控制在 3-4 种太少营养不均衡太多用户执行起来麻烦。GL 升序排列保证优先选低血糖负荷的食物这是糖尿病饮食推荐和普通推荐最大的区别。3.3 用户档案接口字段校验别偷懒用户档案的字段校验是保证推荐质量的第一道关。体重、身高、年龄、活动系数这些字段如果用户乱填后面算出来的热量目标就是错的。用 Jakarta Validation 做参数校验别在 Controller 里写一堆 if-else。// UserProfileDTO.java public class UserProfileDTO { NotNull(message 性别不能为空) private String gender; DecimalMin(value 20.0, message 体重不能低于20kg) DecimalMax(value 300.0, message 体重不能超过300kg) private Double weightKg; DecimalMin(value 100.0, message 身高不能低于100cm) DecimalMax(value 250.0, message 身高不能超过250cm) private Double heightCm; Min(value 1, message 年龄不能小于1岁) Max(value 120, message 年龄不能大于120岁) private Integer age; DecimalMin(value 1.0, message 活动系数不能低于1.0) DecimalMax(value 2.0, message 活动系数不能高于2.0) private Double activityLevel; }校验注解里的数值范围不是随便写的体重 20-300kg、身高 100-250cm、年龄 1-120 岁这些边界值覆盖了绝大多数真实用户同时能拦住明显的误输入。活动系数限制在 1.0-2.0 之间因为再高就是专业运动员级别了不在这个系统的服务范围内。4. Vue.js3 前端把推荐结果做成用户愿意看的页面4.1 组合式 API 怎么写才不乱Vue.js3 的组合式 API 比选项式 API 灵活但灵活意味着容易写乱。我的习惯是按功能拆分setup逻辑每个功能块用独立的ref和函数最后统一return。别把所有东西都塞在一个setup里那样和选项式 API 的data堆砌没区别。// RecommendView.vue script setup import { ref, onMounted, computed } from vue import { getRecommendation } from /api/recommend const loading ref(false) const mealPlan ref(null) const errorMsg ref() // 计算总热量用于页面展示 const totalCalories computed(() { if (!mealPlan.value) return 0 return mealPlan.value.dailyCalories?.toFixed(0) || 0 }) async function fetchRecommendation() { loading.value true errorMsg.value try { const res await getRecommendation() mealPlan.value res.data } catch (e) { errorMsg.value e.response?.data?.message || 获取推荐失败请先完善健康档案 } finally { loading.value false } } onMounted(() { fetchRecommendation() }) /scriptcomputed在这里用来做展示层的格式化不要把业务逻辑塞进模板表达式里。fetchRecommendation里的错误处理要区分“网络错误”和“业务错误”后端返回的message字段直接展示给用户比前端写死“加载失败”有用得多。4.2 三餐展示组件用卡片还是表格推荐结果的展示形式直接影响用户的使用意愿。我试过表格和卡片两种方案最后选了卡片式布局。原因很简单表格适合看数据卡片适合做决策。用户打开页面是想知道“今天吃什么”不是来研究营养数据的。每张卡片展示一餐卡片头部是餐次名称和热量小计卡片主体是食物列表每个食物显示名称、份量、热量和 GI 值。GI 值用颜色标签区分绿色GI ≤ 55、黄色55 GI ≤ 70、红色GI 70。这个颜色映射关系要写在前端常量里别硬编码在模板中。// constants/giLevel.js export const GI_LEVELS [ { max: 55, label: 低GI, color: #52c41a }, { max: 70, label: 中GI, color: #faad14 }, { max: 999, label: 高GI, color: #f5222d } ] export function getGiLevel(gi) { return GI_LEVELS.find(level gi level.max) || GI_LEVELS[GI_LEVELS.length - 1] }这个映射函数在多个组件里都会用到抽成独立模块比在每个组件里重复写if-else干净得多。颜色值我直接用了 Ant Design 的色板如果你项目里用了别的 UI 库换成对应的颜色变量就行。4.3 和 SpringBoot3 联调跨域和代理怎么配前后端分离项目联调时跨域问题是必踩的坑。Vue.js3 项目用 Vite 构建在vite.config.js里配代理比在后端配 CORS 更省事。// vite.config.js import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } })代理配置的关键是rewrite把前端请求的/api前缀去掉再转发给后端。这样后端接口不用加/api前缀前端调用时统一加/api两边都清爽。changeOrigin: true必须加否则后端收到的 Host 头是前端地址某些安全校验会拦截。如果联调时遇到 401 或 403先检查后端有没有加 Spring Security。如果加了要么配permitAll要么在前端请求头里带 Token。我一般建议毕设项目别上 Security用简单的拦截器做登录校验就够了省下来的时间花在推荐算法上更值。5. 避坑指南这五个问题每年都有人踩5.1 推荐结果全是“水煮白菜”现象系统跑通了但推荐出来的食物翻来覆去就是那几样用户看一眼就关掉。原因候选集筛选条件太严suitable 1加上类别限制后剩下的食物可能只有十几种。贪心策略每次都从同一个排序列表里取结果自然重复。解决在候选集筛选时引入随机因子。具体做法是在pickFoods里先对候选集做一次洗牌再按 GL 排序这样每次推荐的组合会有变化。另外食物库的规模要保证在 200 条以上低于这个数怎么调都是巧妇难为无米之炊。5.2 热量算出来是负数现象用户填完档案推荐页面显示的热量目标是负数。原因Mifflin-St Jeor 公式里超重用户要减 500kcal如果用户基础代谢本来就低比如体重轻、年龄大、久坐减完 500 就成负数了。解决在calculateDailyCalories里加一个下限保护男性不低于 1200kcal女性不低于 1000kcal。这是临床营养学的安全底线低于这个值任何饮食方案都不应该推荐。// 在 calculateDailyCalories 返回前加保护 double result ...; double minCalories MALE.equals(gender) ? 1200 : 1000; return Math.max(result, minCalories);5.3 前端页面白屏控制台报 “Cannot read property of undefined”现象推荐页面打开后白屏F12 控制台一堆红色报错。原因异步请求还没返回模板里已经在访问mealPlan.value.breakfast了。Vue.js3 的模板对undefined属性访问不会自动兜底。解决模板里所有可能为空的属性访问都加可选链?.或者用v-if包一层。更彻底的做法是在setup里给mealPlan一个初始结构保证模板渲染时字段存在。const mealPlan ref({ breakfast: [], lunch: [], dinner: [], dailyCalories: 0 })5.4 数据库插入中文乱码现象食物名称在数据库里显示正常但前端页面显示成问号或方块。原因MySQL 建库时字符集没设对或者 JDBC 连接串没指定编码。解决建库语句用CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci连接串加useUnicodetruecharacterEncodingutf8。注意是utf8mb4不是utf8MySQL 的utf8是阉割版存不了 emoji 和部分生僻字。5.5 推荐接口响应超过 3 秒现象点击“生成推荐”后转圈很久用户体验差。原因pickFoods里每次都对全表做排序食物库数据量大了之后排序开销明显。解决在food_item表的gl_value和category字段上建联合索引查询时用WHERE suitable 1 AND category IN (...)先过滤再排序。另外推荐结果可以缓存同一个用户同一天的健康档案不变推荐结果就没必要重复计算。用 Spring Cache 加Cacheable注解缓存 key 用userId 日期。6. 进阶技巧把推荐结果导出成可打印的食谱6.1 用后端生成 PDF 还是前端生成推荐结果最终要落到“用户能带走”这个点上。我试过两种方案前端用 jsPDF 生成后端用 iText 生成。前端方案的问题是中文字体处理麻烦jsPDF 默认不支持中文要额外加载字体文件打包体积大。后端方案用 iText 7中文字体通过PdfFontFactory.createFont加载系统字体或项目内置字体文件稳定得多。我一般会在后端加一个/api/recommend/export接口返回application/pdf流前端用window.open直接打开下载。这样用户可以把食谱打印出来贴在冰箱上使用场景就闭环了。6.2 导出接口的关键参数// PdfExportService.java public byte[] exportMealPlan(Long userId) { DailyMealPlan plan recommendationService.recommend(userId); ByteArrayOutputStream baos new ByteArrayOutputStream(); PdfWriter writer new PdfWriter(baos); PdfDocument pdf new PdfDocument(writer); Document document new Document(pdf); // 加载中文字体字体文件放在 resources/fonts/ 下 PdfFont font PdfFontFactory.createFont(fonts/NotoSansSC-Regular.ttf, PdfEncodings.IDENTITY_H); document.setFont(font); document.add(new Paragraph(一日食谱).setFontSize(18).setBold()); document.add(new Paragraph(每日总热量 plan.getDailyCalories() kcal)); // 三餐分别输出 addMealSection(document, 早餐, plan.getBreakfast()); addMealSection(document, 午餐, plan.getLunch()); addMealSection(document, 晚餐, plan.getDinner()); document.close(); return baos.toByteArray(); }字体文件建议用 Noto Sans SC开源免费覆盖常用汉字。PdfEncodings.IDENTITY_H是处理中文必须的编码参数不写这个中文会变成乱码。字体文件放在src/main/resources/fonts/目录下打包时会自动包含在 jar 里。6.3 一个我踩过的坑导出功能上线后有用户反馈 PDF 打开是空白页。排查了半天发现是字体文件路径问题开发环境用相对路径能读到打成 jar 包后fonts/NotoSansSC-Regular.ttf这个路径就失效了。正确做法是用ClassPathResource加载。// 正确的字体加载方式 ClassPathResource fontResource new ClassPathResource(fonts/NotoSansSC-Regular.ttf); PdfFont font PdfFontFactory.createFont( fontResource.getInputStream().readAllBytes(), PdfEncodings.IDENTITY_H);这个坑让我养成了一个习惯所有resources目录下的文件一律用ClassPathResource加载不用new File()。后来带学生做毕设第一件事就是让他们把这条写进开发规范里。做这个系统最大的体会是技术栈选什么其实不是最关键的SpringBoot3 也好Vue.js3 也好都是工具。真正决定系统好不好用的是你有没有认真对待“糖尿病患者到底需要什么”这个问题。我见过用 JSP 做的系统拿优秀毕设也见过用最新技术栈堆出来的系统被答辩老师问得哑口无言。把营养计算做准、把食物库做扎实、把推荐结果做得让用户愿意照着吃这三点做到了技术栈只是锦上添花。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑