博主最近在给几个准备秋招的学员做项目辅导时发现一个很有意思的现象问起想做什么项目十个里有八个说“外卖点单系统”或者“图书管理”再做下去就是“商城秒杀”。不是说这些题目不行而是做得太滥了面试官一听就知道你是照着网课敲的问两句就露馅。反观“饮食营养管理信息系统”这个选题看起来不起眼但如果你真把它做扎实了里面藏着几条很容易出彩的技术线——营养数据的建模、按天聚合计算、健康目标动态计算、ECharts 可视化联动。它不像电商那样要求高并发但足够把 Java Web 全栈的完整链路走通而且业务逻辑有明显的计算复杂度比纯增删改查高出一个档次。这篇文章我就以 SpringBoot Vue2 前后端分离的思路把这个项目的系统设计、数据库建模、后端计算逻辑、前端可视化、部署联调完整拆开讲一遍。适合四类人准备课程设计的在校生、想做完整项目充实简历的 Java 学习者、想转型全栈的开发以及单纯想了解营养类系统怎么设计的人。全文偏实战跟着走完你手里就有一套能跑、能演示、能讲清楚的完整源码。1. 这个选题为什么能打业务价值和技术收益的双重账1.1 别小看“记账”类业务营养记录比想象中复杂很多人一听“饮食管理”就觉得是记流水账今天吃了什么记一下完了。真设计过才知道这个业务有一个天然的复杂度——同一份食物不同的人吃不同的量最终摄入的营养素是完全不同的。比如红烧肉100 克和 300 克热量差了整整三倍。所以营养类系统的基础模型必须是“食物营养数据库”加上“用户食用量”的组合这就引出了第一层设计问题食物的营养成分按什么基准存用户记录的是克数还是份数计算时怎么换算再加上用户还有健康目标。减脂的人每天的热量缺口应该是多少增肌的人蛋白质需要多高的比例这不是简单的累加而是要把用户的性别、年龄、身高、体重、活动强度、目标类型全部纳入一个公式里计算。这一层业务逻辑一旦展开CRUD 立马就不够用了。饮食管理这个领域本身也在持续增长轻食、健身餐、慢性病饮食控制都是热门话题。做一个能算热量、能看趋势、能给建议的小系统既有现实意义又有技术深度。1.2 技术端的真实收益清单从 Java 后端的技术成长角度看这个项目覆盖的技术点非常完整技术层面具体内容在这个项目里的落地场景Web框架SpringBoot、SpringMVCRESTful 接口、参数校验、统一异常处理持久层MyBatis-Plus MySQL用户表、食物表、饮食记录表的 CRUD 与聚合查询安全认证JWT 拦截器登录注册、Token 鉴权、接口保护业务计算BMR 公式、营养聚合基础代谢计算、每日营养汇总、目标热量动态调整前端工程Vue2 Element UI Axios单页应用、路由守卫、Token 自动携带可视化ECharts近 7 天热量趋势、三大营养素供能占比、体重趋势部署联调Maven、npm、反向代理前后端分离下的代理转发、跨域处理这个技术组合的好处是“正统”。SpringBoot 是当前 Java 后端的事实标准Vue2 在存量项目中的占有率依然很高企业里大量老项目就是 SpringBoot Vue2 的组合。你在简历上写这套技术栈面试官不会觉得陌生反而能顺着项目问出一串有价值的问题。1.3 什么人最适合拿它练手说实话如果面试官问“你项目里最有挑战的一件事是什么”很多人憋半天只能说“我解决了跨域”。但如果你做的是饮食营养管理系统你可以讲营养计算的幂等性设计可以讲怎么用一条聚合 SQL 把一周的趋势取出来可以讲体测数据变化对目标热量的影响逻辑——这些都是有信息量的话题。最适合拿这个项目练手的是三类人第一类是准备毕业设计的学生。这个选题不烂大街业务完整度适中数据库表三到五张就能讲清楚论文写起来也顺。第二类是学了 SpringBoot 但没做过完整前后端分离项目的人通过它把 Vue 和 Java 真正串起来。第三类是想要在简历里体现“业务思考”的初级开发通过膳食记录、营养分析这些功能点展示自己不只是会写接口还能设计业务模型。2. 先把业务模型捋清楚营养数据到底怎么组织才算好设计2.1 核心业务闭环与功能模块地图不管代码怎么写业务模型先行。我画过很多版功能图最终沉淀下来的核心闭环是四步用户维护个人档案性别、年龄、身高、体重、活动强度、目标——系统根据档案计算每日热量和营养素推荐值——用户每天按餐次记录吃了什么、吃了多少克——系统汇总实际摄入与推荐值对比并可视化展示。围绕这个闭环功能模块可以拆成这么几块登录注册模块基于 JWT 的账号体系注册时要求填写身体档案因为后续所有计算都依赖这些参数。食物管理模块内置常见食物的营养成分每条食物包含热量、蛋白质、脂肪、碳水化合物、膳食纤维按类别组织。膳食记录模块按日期和餐次记录饮食支持增删改查这是整个系统最核心的数据入口。营养分析模块按天汇总营养素实际摄入量通过柱状图和折线图与推荐值对比。个人健康概览模块展示 BMI、BMR、每日推荐热量、当前目标进度。别小看这些模块之间的依赖关系。食物管理是基础数据膳食记录依赖食物和用户营养分析依赖膳食记录健康概览依赖用户档案和营养分析结果。这条依赖链清楚了后端的 Service 层怎么拆、前端的页面怎么组织自然就都顺了。2.2 数据库设计的三个核心表合理的表结构是这个项目的地基。我这里给出经过实测的三张核心表你可以直接拿来建库CREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 用户名, password varchar(100) NOT NULL COMMENT 密码(BCrypt加密), nickname varchar(50) DEFAULT NULL, gender tinyint(1) DEFAULT NULL COMMENT 1男 0女, age int(11) DEFAULT NULL COMMENT 年龄, height decimal(5,2) DEFAULT NULL COMMENT 身高cm, weight decimal(5,2) DEFAULT NULL COMMENT 体重kg, activity_level tinyint(1) DEFAULT NULL COMMENT 1久坐 2轻度 3中度 4高强度, target_type tinyint(1) DEFAULT NULL COMMENT 1减脂 2增肌 3维持, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE food ( id bigint(20) NOT NULL AUTO_INCREMENT, name varchar(100) NOT NULL COMMENT 食物名称, category varchar(50) DEFAULT NULL COMMENT 分类:主食/肉蛋/蔬菜/水果/奶制品/零食, calories decimal(8,2) DEFAULT NULL COMMENT 热量 每100g 千卡, protein decimal(8,2) DEFAULT NULL COMMENT 蛋白质 每100g 克, fat decimal(8,2) DEFAULT NULL COMMENT 脂肪 每100g 克, carbohydrate decimal(8,2) DEFAULT NULL COMMENT 碳水化合物 每100g 克, fiber decimal(8,2) DEFAULT NULL COMMENT 膳食纤维 每100g 克, PRIMARY KEY (id), KEY idx_category (category) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE meal_record ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL COMMENT 用户ID, food_id bigint(20) NOT NULL COMMENT 食物ID, meal_type tinyint(1) NOT NULL COMMENT 1早餐 2午餐 3晚餐 4加餐, record_date date NOT NULL COMMENT 记录日期, quantity decimal(8,2) NOT NULL COMMENT 食用量 克, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_date (user_id, record_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个细节值得说meal_record表里没有冗余任何营养成分字段只存了food_id和quantity。原因很简单食物的营养数据是相对稳定的查询时通过 join 关联即可如果你把营养值冗余到记录表里反而会出现“食物数据修正后历史记录还是旧值”的一致性问题。做这类系统尽量保持数据源单一。2.3 这里的建模难点营养素如何以“每100克”为基准食物营养数据的建模有个约定俗成的习惯以“每 100 克可食部分”为基准来存储。你要记录一只 50 克的鸡蛋实际计算蛋白质时就得先把鸡蛋的重量除以 100得到系数 0.5再乘以食物表里的每百克蛋白质含量。BigDecimal factor quantity.divide(new BigDecimal(100), 4, RoundingMode.HALF_UP); BigDecimal actualProtein food.getProtein().multiply(factor);这个换算逻辑是整个系统计算层面的基石理解它之后后面所有聚合计算都是在这个基础上的乘法叠加。为什么不直接用份来记录因为“一份米饭”“一碗面条”的量化口径因人而异而“克”是唯一没有歧义的物理单位。所以数据录入时哪怕用户只能选“份”系统最终也要换算成克来落库。这套模型还有一个好处当你后续想扩展“营养素摄入排行”“不同餐次占比分析”时基础数据已经足够不需要改表结构。3. SpringBoot后端CRUD只占三成营养计算才是核心3.1 项目分层与包结构后端我采用经典的四层结构没有花哨的微服务因为这种单体式业务用微服务纯属自找麻烦com.example.nutrition ├── controller # 请求入口只做参数接收和结果返回 ├── service # 业务逻辑层营养计算、目标管理 │ └── impl ├── mapper # MyBatis-Plus的Mapper接口 ├── entity # 数据库实体类 ├── dto # 前端交互的数据对象避免直接暴露实体 ├── config # 跨域配置、拦截器注册 ├── common # 统一返回结果、异常处理、常量 └── utils # JWT工具、营养计算工具我在项目里有个习惯Controller 层尽量不写业务逻辑只负责接收参数、调用 Service、返回统一 Result。像“根据日期范围查询营养趋势”这种操作Controller 里只写三行真正的聚合计算全部下沉到 Service。这样做的直接好处是JUnit 单元测试可以只测 Service不依赖 Web 容器。实体类可以直接用 MyBatis-Plus 注解映射避免写 XML。比如Food实体Data TableName(food) public class Food { TableId(type IdType.AUTO) private Long id; private String name; private String category; private BigDecimal calories; private BigDecimal protein; private BigDecimal fat; private BigDecimal carbohydrate; private BigDecimal fiber; }3.2 热量目标计算BMR和活动系数的联动逻辑这是营养系统区别于普通 CRUD 系统最核心的一个点。系统要根据用户档案算出每日推荐热量业界最常用的是 Mifflin-St Jeor 公式男性基础代谢 BMR 10 × 体重(kg) 6.25 × 身高(cm) - 5 × 年龄(岁) 5 女性基础代谢 BMR 10 × 体重(kg) 6.25 × 身高(cm) - 5 × 年龄(岁) - 161算出来的 BMR 只是基础代谢还要乘上活动系数才是每日总消耗 TDEE活动强度系数典型场景久坐1.2办公室办公几乎不运动轻度活动1.375每周运动 1-3 次中度活动1.55每周运动 3-5 次高强度活动1.725每周运动 6-7 次最后根据目标类型调整推荐摄入量。减脂建议在 TDEE 基础上减 400 千卡增肌加 300 千卡维持不变。整套逻辑封装在HealthCalculator工具类里public static BigDecimal calculateTargetCalories(Integer gender, Integer age, BigDecimal height, BigDecimal weight, Integer activityLevel, Integer targetType) { BigDecimal bmr; if (gender 1) { bmr new BigDecimal(10).multiply(weight) .add(new BigDecimal(6.25).multiply(height)) .subtract(new BigDecimal(5).multiply(new BigDecimal(age))) .add(new BigDecimal(5)); } else { bmr new BigDecimal(10).multiply(weight) .add(new BigDecimal(6.25).multiply(height)) .subtract(new BigDecimal(5).multiply(new BigDecimal(age))) .subtract(new BigDecimal(161)); } BigDecimal[] activityRatios {null, new BigDecimal(1.2), new BigDecimal(1.375), new BigDecimal(1.55), new BigDecimal(1.725)}; BigDecimal tdee bmr.multiply(activityRatios[activityLevel]).setScale(0, RoundingMode.HALF_UP); if (targetType 1) { return tdee.subtract(new BigDecimal(400)); } else if (targetType 2) { return tdee.add(new BigDecimal(300)); } return tdee; }用BigDecimal而不是double是因为健康类数据对精度有要求浮点运算容易出现 0.1 0.2 0.30000000000000004 这种问题。这一点在面试时如果主动提出来是很加分的细节。3.3 当日营养汇总的聚合查询写法假设用户记录了早餐 100 克燕麦、午餐 200 克鸡胸肉系统要根据日期把当天所有记录的营养素汇总。MyBatis-Plus 的 LambdaQueryWrapper 做不了跨表聚合我一般是直接在 Service 里用自定义 SQL 处理Mapper public interface MealRecordMapper extends BaseMapperMealRecord { Select(SELECT COALESCE(SUM(f.calories * r.quantity / 100), 0) AS totalCalories, COALESCE(SUM(f.protein * r.quantity / 100), 0) AS totalProtein, COALESCE(SUM(f.fat * r.quantity / 100), 0) AS totalFat, COALESCE(SUM(f.carbohydrate * r.quantity / 100), 0) AS totalCarbs, COALESCE(SUM(f.fiber * r.quantity / 100), 0) AS totalFiber FROM meal_record r LEFT JOIN food f ON r.food_id f.id WHERE r.user_id #{userId} AND r.record_date #{date}) NutritionSummaryDTO getDailySummary(Param(userId) Long userId, Param(date) String date); }营养汇总这个逻辑写清楚之后一周趋势就顺理成章了只要把 group by 条件从日期改成日期字段用 CASE WHEN 或直接按日期分组即可。聚合 SQL 能完成的不要拖到内存里循环跑这是个非常基础但很多人做不对的性能意识。3.4 权限控制与接口安全登录模块我用的 JWT 方案工具类负责生成和解析 Token拦截器负责统一鉴权。值得注意的坑是注册和登录接口必须放行否则用户连 Token 都拿不到。放行列表可以在WebMvcConfig里配置像这样Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns(/**) .excludePathPatterns(/api/user/login, /api/user/register); }密码存储务必使用 BCrypt 加密不要用 MD5。MD5 在彩虹表面前几乎等于明文而 BCrypt 每次加密结果都带随机盐相同密码两次加密结果不同安全性不是一个量级。统一返回结果ResultT也需要在项目一开始就定好比如 code 为 200 表示成功401 表示未认证500 表示业务异常。前后端分离项目如果不统一结果格式前端每个请求都要单独判错误维护成本极高。4. Vue2前端图表驱动体验页面围绕“记录-查看-反馈”来组织4.1 为什么这儿选Vue2而不是Vue3虽然 Vue3 已经是主流新项目选择但这个项目选择 Vue2 有它的现实考量存量企业项目里 SpringBoot Vue2 的组合依然大量存在很多公司的老中台系统都是这个技术栈。做这个项目的人如果以就业为导向Vue2 的实战经验反而是企业需要的。另一个原因是生态成熟。Element UI 在 Vue2 下极其稳定ECharts 与 Vue2 的集成方案遍地都是遇到问题几乎都能搜到现成答案。Vue3 对应的 Element Plus 版本号一路从 1.x 跳到 2.x早期版本有各种兼容性问题新手容易卡在环境上而不是业务上。如果你后续有余力可以在这个项目基础上做一版 Vue3 重构对比一下 Composition API 和 Options API 的组织方式差异。但第一版建议老老实实用 Vue2把业务跑通。4.2 核心页面拆解与开发顺序前端页面不要一上来就写先把路由和菜单定下来。我建议的开发顺序是按数据流方向走登录注册页表单校验、调用登录接口、存储 Token。食物管理页表格展示食物列表、按分类筛选、新增和编辑食物弹窗这页主要练 Element UI 的 Table 和 Dialog。膳食记录页左侧日期选择器中间按餐次分组的卡片列表右上角“添加记录”按钮点击后弹出食物选择器。这是最重要的页面交互密度最高。营养分析页日期范围选择器加多个图表展示热量趋势、营养素占比、餐次热量分布。个人中心页展示用户身体档案和健康指标支持编辑。数据流非常清晰用户档案从个人信息页维护食物从管理页维护膳食记录页消费这两部分数据营养分析页再消费膳食记录数据。前端组件之间的依赖与后端 Service 的依赖完全对应这也是这个项目逻辑规整的一个体现。4.3 axios封装、路由守卫和Token处理Vue2 项目一般用 vue-cli 创建网络请求用 axios。一定要在最开始就做好 axios 实例封装而不是每个页面单独引入 axios 使用否则后患无穷import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { Message.error(res.msg || 请求失败) return Promise.reject(new Error(res.msg)) } return res }, error { if (error.response error.response.status 401) { localStorage.removeItem(token) router.push(/login) } Message.error(网络异常请稍后重试) return Promise.reject(error) } ) export default service路由守卫里也要做一层校验未登录用户访问任何业务页面都重定向到登录页。双重保障前端不依赖后端报错才知道用户没登录。4.4 用ECharts把营养数据“说”出来营养分析页是整个系统最出彩的地方也是演示时最容易抓眼球的页面。ECharts 在 Vue2 里的用法不复杂import * as echarts from echarts export function renderTrendChart(el, dates, intakeList, targetList) { const chart echarts.init(el) chart.setOption({ tooltip: { trigger: axis }, legend: { data: [实际摄入, 推荐摄入] }, xAxis: { type: category, data: dates }, yAxis: { type: value, name: 千卡 }, series: [ { name: 实际摄入, type: bar, data: intakeList, barWidth: 20 }, { name: 推荐摄入, type: line, data: targetList, lineStyle: { type: dashed } } ] }) return chart }柱状图加折线图的组合能一眼看出连续几天是吃超了还是吃少了。第二个图表用饼图展示三大营养素供能占比计算逻辑是蛋白质、脂肪、碳水化合物各自的热量贡献占全天总热量的比例。这里有个知识点蛋白质和碳水化合物一克供热 4 千卡脂肪一克供热 9 千卡不要直接用克数做占比要用热量做占比。组件卸载时记得调用chart.dispose()否则页面频繁切换会累积内存泄漏。这是新手常忽略的细节。5. 从源码到跑通环境配置、数据初始化与前后端联调5.1 本地环境准备与启动全流程我把这个项目的环境依赖列出来版本直接给你我实测过的组合软件版本说明JDK1.8 或 11如果 SpringBoot 用 2.xJDK 1.8 即可Maven3.6项目构建和依赖管理MySQL5.7 或 8.0推荐 8.0注意驱动差异Node.js14.x 或 16.xVue2 项目的 node-sass 对 Node 版本敏感IDEIDEA VSCode后端用 IDEA前端用 VSCode 或 HBuilderX 均可启动顺序有讲究先启动 MySQL执行初始化脚本建库建表再启动后端 SpringBoot 服务看到端口 8080 启动成功日志最后在前端项目目录执行npm install然后npm run serve默认端口一般是 8080和前端端口撞了要处理。很多新手在这里会卡住后端占用了 8080前端也想用 8080冲突。最简单的处理方案是后端改成 8081前端不动。在application.yml改一行就行server: port: 80815.2 食物基础数据从哪里来项目跑起来之后食物数据不能靠用户手动一条条录你得准备一份初始化 SQL内置日常生活中最常见的 100 到 200 种食物。数据来源一般是《中国食物成分表》这是国内营养学最权威的基础数据源。我建议食物分类至少包含这几类主食类米饭、面条、馒头、燕麦、红薯、肉蛋类鸡胸肉、瘦猪肉、牛肉、鸡蛋、鱼肉、蔬菜类西兰花、菠菜、生菜、西红柿、水果类苹果、香蕉、橙子、奶制品牛奶、酸奶、零食饮料坚果、可乐、咖啡。每条食物数据的热量、蛋白质、脂肪、碳水都要按每 100 克可食部分的标准格式录入。这个初始化数据一来方便演示二来方便测试营养计算逻辑是否正确。测试时可以选一个已知热量的食物记录 100 克验证汇总结果和食物表数值是否一致。5.3 跨域问题的处理与联调配置前后端分离的项目本地联调时最常遇到的就是跨域。所谓跨域就是浏览器安全策略规定一个源协议、域名、端口任一不同下的网页不能随意请求另一个源下的接口。本地开发解决跨域的首选方案不是在后端加CrossOrigin而是在前端项目里配置 devServer 代理。Vue2 项目的vue.config.js这样写module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } }前端请求/api/user/login代理会自动转发到http://localhost:8081/api/user/login浏览器看起来是同源请求跨域问题自然消失。后端不需要额外配置跨域头省心很多。生产环境的联调配置类似Nginx 里做一层反向代理把/api前缀的请求转发到后端服务即可。6. 实测中的坑与优化建议写给第一次做全家桶的人6.1 版本相关的三个坑我替你踩过了第一个坑是 SpringBoot 3.x 和 JDK 8 不兼容。社区里的很多旧教程还是javax.servlet的包名到了 SpringBoot 3.x 全变成jakarta.servlet了。如果你照着旧教程写代码直接编译报错。建议新手直接用 SpringBoot 2.7.x JDK 1.8 的经典组合跑通第一版再考虑升级。第二个坑是 MySQL 8.0 的驱动包名变了。com.mysql.jdbc.Driver是 5.x 的写法8.0 要写成com.mysql.cj.jdbc.DriverURL 里还需要加上serverTimezoneAsia/Shanghai否则日期时区会出问题。第三个坑是 Node 版本和 node-sass 不匹配。Vue2 项目如果用了node-sassNode 14 和 Node 18 需要不同版本的 node-sass安装时常报错。我的建议是能不用 node-sass 就不用样式全用原生 CSS 配合 Element UI 足够或者用sass替代。6.2 营养计算中容易踩的精度问题这个项目的所有金额、重量、营养数据我强烈建议一律用BigDecimal包括数据库字段类型也用DECIMAL。如果图省事直接上double吃了 200 克鸡胸肉蛋白质计算结果可能是 46.00000000000001 克展示在页面上极其尴尬。大数值精度问题发生在除法上。100 除以 3 是无限小数而营养计算里恰恰充满了重量除以 100 的换算必须指定精度和舍入模式BigDecimal factor quantity.divide(new BigDecimal(100), 4, RoundingMode.HALF_UP);统一用 4 位小数做中间精度最终展示时再保留 1 到 2 位。这个一致性约定如果在项目里所有人都遵守就不会出现前后端数据对不上的情况。6.3 这套系统的后续扩展空间项目跑通、能演示之后不要急着投简历留出时间做一到两个亮点扩展比堆功能有用得多。我给几个方向供参考第一个是饮食建议模块。每天对比实际摄入和推荐摄入自动生成文字建议。比如蛋白质摄入不足时提示“今日蛋白质摄入偏低建议增加 100 克鸡胸肉或 2 个鸡蛋”。这个功能不强但能体现你关注用户体验。第二个是历史体重曲线。用户在个人中心记录每周体重系统绘制趋势图。把体重变化和热量摄入趋势叠加在一起看减脂效果一目了然。这个功能涉及 TDD 里常见的“多数据源关联分析”面试时很好聊。第三个是管理员端。加一个简单的后台页面维护食物数据库支持批量导入 Excel 数据。普通用户专心用管理员维护基础数据身份分离会让系统更像一个真实产品。每个扩展方向都是独立的业务闭环想清楚了再动手比盲目加功能好得多。我见过太多人把简单系统做成四不像菜单塞满了按钮但核心的“记录-计算-反馈”链路反而没说明白。最后补一个很实用的建议做这类全栈项目务必养成随手写 README 的习惯把技术栈、启动步骤、账号信息、核心接口文档都丢进去。别小看这个文件自己在本地折腾了三天再隔一个月看没有 README 连你自己都不知道怎么启动。把 README 写清楚项目才算真正收尾。