做毕设选“基于Java+Vue的饮食营养管理系统”这个题目的人,多半是看中了它业务清晰、不复杂、演示起来也直观。但真正要把源码、数据库、文档完整跑通,并且拿到答辩桌上讲明白,我发现很多人卡住的地方根本不在CRUD,而是那几个“你以为会了”的环节:数据库表结构怎么建最合理、营养数据到底怎么算、Vue环境为什么一次次配不起来、前后端联调时跨域怎么拦路。这篇文章把我从零到一完成这个项目的真实过程拆开来讲,包含数据库建模、后端营养计算逻辑、Vue环境配置与页面开发,以及交付时源码和文档的组织方式。如果你正在做类似的毕设或作品集项目,这篇文章可以直接当参考线来用。
1. 这些模块才是饮食营养管理系统的核心骨架
1.1 业务闭环:从“吃了什么”到“该吃什么”
作为毕设题目,饮食营养管理系统最容易被做窄。我见过很多人的版本就是三个列表:食物列表、用户列表、记录列表,做完之后发现这只是一个换了皮的后台管理系统,跟“营养管理”四个字没什么关系。我动手前做的第一件事不是写代码,而是先把业务骨架画出来——这个系统里真正有价值的闭环是什么?
闭环是这样的:用户登录后,在食物库里选择今天吃的食物,填写食用克数,后端根据食物表里“每100克可食部分的营养素含量”,算出这一餐的热量、蛋白质、脂肪、碳水化合物;然后系统按天汇总,对照用户的基础代谢和活动水平,给出当日摄入是否达标的结论;再往前一步,营养师可以维护食物库、发布食谱,把“吃了什么”延伸到“建议吃什么”。
这个闭环里最关键的词是“算”。增删改查只是基础能力,营养计算才是系统区别于普通管理系统的灵魂。如果你把这个逻辑想清楚了,项目就不会做成空洞的后台。
1.2 角色划分与功能清单
角色我最终定为三种:普通用户、营养师、管理员。普通用户主要操作饮食记录、查看报告;营养师负责食物库维护和食谱发布;管理员管用户状态、系统字典。如果只是毕设答辩,可以把营养师合并到管理员角色里,两个角色足够演示,但接口层面建议预留,别把后面的扩展路堵死。
我最后落地的功能清单是这样的:
| 模块 | 功能点 | 说明 |
|---|---|---|
| 用户模块 | 注册、登录、个人信息维护、目标体重设置 | 登录密码使用BCrypt加密 |
| 食物模块 | 食物检索、分类查询、营养成分详情 | 管理员和营养师维护 |
| 饮食记录 | 新增、修改、删除记录,近一周记录展示 | 核心业务模块 |
| 营养分析 | 每日热量汇总、三大营养素占比、趋势图 | 系统差异化价值所在 |
| 食谱模块 | 食谱列表、详情、按场景推荐 | 可选的加分扩展 |
| 系统管理 | 用户状态管理、后台登录、基础统计 | 管理员使用 |
这个清单的好处是:每一块都能在答辩时讲出“为什么需要”,而不是干巴巴地念字段名。
1.3 适合谁来参考这个项目
如果你正在做毕业设计,或者想通过一个端到端项目把Java和Vue串起来,这个题目是很合适的。它不涉及支付、消息队列、分布式这类高复杂度场景,但覆盖了前后端分离项目最常见的问题:权限控制、表设计、统计查询、跨域联调、环境配置。把这些基础问题吃透,比硬上微服务架构更能锻炼工程能力。这篇文章里的所有方案,我都是在单机部署、一次性交付源码数据库文档的假设下做的。
2. 技术选型与工程结构:为什么是Spring Boot + Vue 3
2.1 后端技术栈的取舍
后端我选了Spring Boot 2.7 + MyBatis-Plus 3.5 + MySQL 8.0,JDK用的1.8。为什么用MyBatis-Plus而不是JPA?我的实际感受是,MyBatis-Plus的LambdaQueryWrapper写条件查询非常直白,比如wrapper.like(Food::getFoodName, keyword),一眼就能看懂在查什么。更重要的是,答辩时讲代码,你可以直接指着Mapper接口说“SQL在哪”,这比JPA那种自动拼接的查询方式更容易解释清楚。
JPA的自动建表确实方便,但一旦涉及多表关联和分组统计,写起来就不那么直观了。饮食营养管理系统的核心需求恰恰是“按日期分组汇总”,这类SQL在MyBatis-Plus里写起来就是标准的Mapper XML,可控性高很多。MySQL 8.0主要是为了用utf8mb4字符集和更友好的运维体验,5.7也不是不行,但8.0更省心。
2.2 前端技术栈的取舍
前端选了Vue 3 + Vite + Element Plus + Vue Router + Pinia + Axios。Vue 3的组合式API是现在的主流习惯,Element Plus就是配套Vue 3的组件库,不需要像老项目那样在Vue 2和Element UI之间来回磨合。Vite的启动速度比Webpack快得多,最直观的感受是改一行代码,浏览器几乎同步刷新,开发体验完全不一样。
可能有人纠结要不要用Pinia。我的建议是:如果你只有登录态和用户信息需要全局共享,Pinia是够用的,而且比Vuex少了很多样板代码。在我的项目里,Pinia只存了token和userInfo,没有过度设计。组件通信尽量用props和emit,保持逻辑简单。
2.3 工程目录:交付时别人能不能快速看懂
前后端分离的工程,我最怕的就是拿到手的源码没有目录边界。我最终采用的结构是这样的:
diet-nutrition-system/ ├── diet-server/ # 后端 Spring Boot 工程 │ ├── src/main/java/ │ │ ├── controller/ │ │ ├── service/ │ │ ├── mapper/ │ │ ├── entity/ │ │ └── config/ │ ├── src/main/resources/ │ │ ├── mapper/ # MyBatis XML 文件 │ │ └── application.yml │ └── pom.xml ├── diet-web/ # 前端 Vue 3 工程 │ ├── src/ │ │ ├── api/ │ │ ├── router/ │ │ ├── views/ │ │ ├── components/ │ │ ├── store/ │ │ └── main.js │ ├── vite.config.js │ └── package.json ├── sql/ # 数据库初始化脚本 │ ├── init.sql │ └── data.sql └── docs/ # 项目文档后端按controller/service/mapper/entity分层,前端按api/router/views/components分。前后端目录严格分开,sql和docs单独放。任何人拿到源码,能在一个小时内把结构和入口找清楚。
3. 数据库建模:食物营养成分表应该怎么设计
3.1 核心表结构:用户、食物、记录、食谱
数据库设计是这个项目最值得花时间的部分。我最终沉淀了四张核心表,再加一张食物状态字典表。
第一张是用户表sys_user:
CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(128) NOT NULL, real_name VARCHAR(50), gender TINYINT DEFAULT 0 COMMENT '0男 1女', age INT DEFAULT 25, height DOUBLE DEFAULT 170 COMMENT '身高cm', weight DOUBLE DEFAULT 65 COMMENT '当前体重kg', target_weight DOUBLE DEFAULT 65, activity_level TINYINT DEFAULT 1 COMMENT '1久坐 2轻度 3中度 4高度', role_code VARCHAR(20) DEFAULT 'user', status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );注意到我存了身高、体重、年龄、活动水平,这四个字段直接决定了后面的BMR基础代谢计算。很多人设计表的时候忽略这些,等到做营养报告才发现没有数据可算。
第二张是食物表food:
CREATE TABLE food ( id BIGINT PRIMARY KEY AUTO_INCREMENT, food_name VARCHAR(100) NOT NULL, category VARCHAR(50), food_state TINYINT DEFAULT 0 COMMENT '0生重 1熟重', calories DOUBLE COMMENT '每100g热量kcal', protein DOUBLE COMMENT '每100g蛋白质g', fat DOUBLE COMMENT '每100g脂肪g', carbohydrate DOUBLE COMMENT '每100g碳水化合物g', fiber DOUBLE COMMENT '每100g膳食纤维g', sodium DOUBLE COMMENT '每100g钠mg', status TINYINT DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );food_state这个字段是我踩过坑之后加进去的,后面会在坑位章节详细说。它解决了米饭这类食物生重和熟重营养差距极大的问题。
第三张是饮食记录表diet_record:
CREATE TABLE diet_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, food_id BIGINT NOT NULL, meal_type TINYINT COMMENT '1早餐 2午餐 3晚餐 4加餐', quantity DOUBLE COMMENT '实际摄入克数', record_date DATE NOT NULL, total_calories DOUBLE, total_protein DOUBLE, total_fat DOUBLE, total_carbohydrate DOUBLE, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );这里有个设计细节:为什么要把total_calories等计算结果冗余到记录表里?因为营养分析页面要按日期分组汇总,如果每次查询都JOIN食物表再计算一遍,数据量上来之后性能会很难看。既然在写入记录时就能算出结果,那就直接冗余存储。这是个典型的空间换时间设计,答辩时讲出来反而是加分项。
第四张是食谱表recipe,字段大致是食谱名称、适用餐次、总热量、蛋白质、脂肪、碳水、制作步骤、封面图。食谱表主要是给“推荐吃什么”这个场景用的,优先级低于前三张表。
3.2 由实体类生成建表SQL:MyBatis-Plus的边界在哪
网上经常有人搜“mybatisplus根据java实体类生成创建表的sql语句”,我也是被这个需求带着走的。先说结论:MyBatis-Plus官方并没有提供“实体类反向生成DDL”的能力,它的代码生成器是正向的,也就是先有数据库表,再用AutoGenerator生成实体类。标题里的场景正好反过来。
我当时的做法是写了一个小工具,用Java反射读取实体类上的注解来拼接DDL。实体类这样写:
@TableName("food") public class Food { @TableId(type = IdType.AUTO) private Long id; @TableField("food_name") private String foodName; private String category; private Double calories; // 省略其余字段 }工具启动时,通过Class对象拿到@TableName的值作为表名,遍历所有字段,把@TableField的值作为列名,再根据Java类型映射到MySQL类型:String映射为VARCHAR(255),Integer/Long映射为BIGINT,Double映射为DOUBLE,LocalDateTime映射为DATETIME。这套映射规则虽然粗糙,但开发初期把表快速拉起来是够用的。
我要给一个明确建议:这个工具只适合开发阶段。项目交付时,一定要在sql/目录里放一份手写的、干净的建表脚本。因为反射工具生成的DDL缺少索引、注释、默认值这些细节,生产可维护性很差。如果你还想更规范一点,直接用Flyway管理SQL脚本是更好的选择。一句话总结:实体类生成SQL是聪明的小技巧,但手写SQL才是工程上的成熟做法。
3.3 食物营养数据的准备
建好表之后,最耗时间的其实是食物数据。我找了一份常见食物营养成分表,整理成data.sql导入,字段对齐food表。这里有个建议:不需要一开始就导入几千条数据,先把日常食物覆盖了,比如米饭、面条、鸡蛋、牛奶、苹果、鸡胸肉——这些数据足够演示完整流程。每100克的热量、蛋白质、脂肪、碳水必须准确,因为后面所有计算都以它为基础。
我测试时发现,如果食物数据有误,整个营养报表都是错的。所以我又写了一个简单的数据校验接口,批量检查calories是否有负数、protein是否大于100(每100克食物蛋白质不可能超过100克)。这种细节看起来不起眼,但能避免后续分析时出现诡异的数字。
4. 后端实现:增删改查只是基础,营养计算才是灵魂
4.1 统一CRUD封装:少写重复代码
后端开发的第一步是用MyBatis-Plus把基础CRUD封装起来。我定义了一个BaseService,继承自MyBatis-Plus的IService,所有业务Service都继承它,通用方法直接用。食物模块的Service是这样:
@Service public class FoodServiceImpl extends ServiceImpl<FoodMapper, Food> implements FoodService { @Override public IPage<Food> pageFoods(int pageNum, int pageSize, String keyword) { LambdaQueryWrapper<Food> wrapper = Wrappers.lambdaQuery(Food.class); if (StringUtils.hasText(keyword)) { wrapper.like(Food::getFoodName, keyword) .or() .like(Food::getCategory, keyword); } wrapper.orderByDesc(Food::getId); return page(new Page<>(pageNum, pageSize), wrapper); } }一段代码同时完成了分页、模糊查询、排序和状态过滤。这里的LambdaQueryWrapper不需要手写SQL,编译期就能检查字段名,比字符串拼条件安全得多。如果你在答辩时被问到“数据库增删改查怎么实现的”,就可以从MyBatis-Plus的BaseMapper讲起,再补充你这个分页查询的封装逻辑。
4.2 营养计算:从食物克数到各项营养素
核心计算逻辑其实不复杂,难在把规则定清楚。我的规则是:food表里所有营养值都按“每100克可食部分”存储,用户录入的quantity是实际摄入的克数。那么某项营养素的摄入量就是:
摄入热量 = food.calories / 100 * quantity 摄入蛋白质 = food.protein / 100 * quantity我写了一个独立的方法来处理,没有把计算逻辑散落在Controller里:
public DietRecord buildRecord(DietRecordDTO dto) { Food food = foodService.getById(dto.getFoodId()); DietRecord record = new DietRecord(); record.setUserId(dto.getUserId()); record.setFoodId(food.getId()); record.setMealType(dto.getMealType()); record.setQuantity(dto.getQuantity()); record.setRecordDate(dto.getRecordDate()); double ratio = dto.getQuantity() / 100.0; record.setTotalCalories(round(food.getCalories() * ratio)); record.setTotalProtein(round(food.getProtein() * ratio)); record.setTotalFat(round(food.getFat() * ratio)); record.setTotalCarbohydrate(round(food.getCarbohydrate() * ratio)); return record; }这里我保留一位小数,四舍五入用BigDecimal.setScale(1, RoundingMode.HALF_UP),避免浮点数精度问题。有人觉得浮点误差无所谓,但营养报表一旦出现类似“0.30000000000000004”这种数字,演示效果非常尴尬。
4.3 个性化目标:BMR基础代谢与每日能量需求
比“算了多少”更进一步的,是“跟目标比是多了还是少了”。这一步需要结合用户资料计算基础代谢率(BMR)。我用的公式是学界常见的Mifflin-St Jeor公式:
- 男性:
BMR = 88.362 + 13.397 * 体重(kg) + 4.799 * 身高(cm) - 5.677 * 年龄 - 女性:
BMR = 447.593 + 9.247 * 体重(kg) + 3.098 * 身高(cm) - 4.330 * 年龄
算出BMR之后,再乘以活动系数得到每日能量总消耗(TDEE)。活动水平我在用户表里分了几档:久坐1.2、轻度运动1.375、中度运动1.55、高强度1.725。减脂就在TDEE基础上减300到500千卡,增肌则加200到300千卡。这些系数不至于让系统复杂化,又足够支撑“个性化建议”这个卖点。
我建议把这段计算做成一个独立的NutritionCalculator组件,不要写在Controller里。这样单元测试也好写,后面对计算方法做调整也不会影响接口逻辑。当时我花了两个小时把这个计算器抽出来,之后写报告功能时省了很多事。
4.4 趋势统计接口:一次查询返回七天数据
营养分析页面需要按天展示近7天的热量和三大营养素趋势。我用一个聚合查询解决:
@GetMapping("/nutrition/stats") public Result<List<NutritionStatVO>> weekStats(@RequestParam Long userId) { LocalDate end = LocalDate.now(); LocalDate start = end.minusDays(6); List<DietRecord> records = dietRecordService.list( Wrappers.lambdaQuery(DietRecord.class) .eq(DietRecord::getUserId, userId) .between(DietRecord::getRecordDate, start, end) ); // 按 recordDate 分组,累计热量和营养素 }这个接口返回的VO包含日期、总热量、总蛋白质、总脂肪、总碳水五个月段,前端直接画折线图。用between查询加Java内存分组,对单用户级别的数据量完全够用。
5. 前端实战:Vue环境搭建、路由与饮食记录页面
5.1 Vue安装及环境配置的完整流程
前端部分,我被环境问题卡过两次,这里把我验证过的流程完整写出来。第一步是安装Node.js,Vue 3 + Vite要求Node版本至少16.18,我直接用18.16 LTS,省心。装完之后用两个命令确认:
node -v npm -v第二步配置npm镜像。如果不配,在国内网络环境下npm install很容易卡住:
npm config set registry https://registry.npmmirror.com第三步用Vite创建项目:
npm create vite@latest diet-web -- --template vue cd diet-web npm install第四步安装项目依赖:
npm install vue-router@4 element-plus axios pinia装完之后我习惯性的做一次版本检查:npm list vue-router element-plus,确认没有版本冲突。Element Plus本身比较大,如果首屏加载慢,可以后面按需引入,开发阶段全量引入问题不大。
5.2 路由设计与登录守卫
路由我分成两块:公共路由和需要登录的路由。/login是公共的,其余页面必须在登录态下访问。核心代码是:
import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/login', component: () => import('@/views/Login.vue') }, { path: '/', component: () => import('@/layout/MainLayout.vue'), redirect: '/dashboard', children: [ { path: 'dashboard', component: () => import('@/views/Dashboard.vue') }, { path: 'food/list', component: () => import('@/views/food/FoodList.vue') }, { path: 'diet/record', component: () => import('@/views/diet/DietRecord.vue') }, { path: 'nutrition/analysis', component: () => import('@/views/nutrition/NutritionAnalysis.vue') } ] } ] const router = createRouter({ history: createWebHistory(), routes }) router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path !== '/login' && !token) { next('/login') } else if (to.path === '/login' && token) { next('/') } else { next() } })路由守卫是前端权限控制的第一道防线。虽然真正的接口安全要靠后端token校验,但前端这么做能避免用户强行输入URL跳进没权限的页面。跳转方式我用的命令式router.push,没有用<a>标签跳转,避免整页刷新。
5.3 饮食记录页面:表单、表格和插槽的实际应用
饮食记录页面是整个前端交互最重的页面。顶部是录入表单——选择食物、选择餐次、填写克数、选择日期;底部是当天记录表格。食物选择我用的是el-select加filterable属性,支持搜索过滤,因为食物库一旦数据多了,纯下拉根本选不过来。
<el-form :model="recordForm" inline> <el-form-item label="食物"> <el-select v-model="recordForm.foodId" filterable placeholder="搜索食物"> <el-option v-for="f in foodList" :key="f.id" :label="f.foodName" :value="f.id" /> </el-select> </el-form-item> <el-form-item label="食用量(克)"> <el-input-number v-model="recordForm.quantity" :min="1" :max="2000" /> </el-form-item> <el-form-item label="餐次"> <el-select v-model="recordForm.mealType"> <el-option label="早餐" :value="1" /> <el-option label="午餐" :value="2" /> <el-option label="晚餐" :value="3" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="submitRecord">添加记录</el-button> </el-form-item> </el-form>表格部分,操作列我用了Element Plus的插槽来渲染按钮。el-table-column默认只展示字段值,但操作列需要拿到整行数据去处理编辑和删除,这时插槽就是必须的:
<el-table :data="todayRecords"> <el-table-column prop="foodName" label="食物" width="160" /> <el-table-column prop="quantity" label="克数" width="100" /> <el-table-column prop="totalCalories" label="热量(kcal)" width="120" /> <el-table-column prop="mealType" label="餐次" width="100" /> <el-table-column label="操作" width="160"> <template #default="{ row }"> <el-button type="primary" size="small" @click="editRecord(row)"> 编辑 </el-button> <el-button type="danger" size="small" @click="deleteRecord(row.id)"> 删除 </el-button> </template> </el-table-column> </el-table>#default="{ row }"这种作用域插槽写法,可以拿到当前行的数据对象,不用自己再去维护一个currentRow。如果你对Vue插槽不熟,这是练习作用域插槽最典型的场景。
5.4 Axios封装与后端联调
前端调用后端接口,统一走到封装好的Axios实例里。我最开始直接axios.post('http://localhost:8080/api/...'),结果跨域问题一堆。后来把请求统一到/api前缀,并在Vite里配置代理转发:
// vite.config.js export default { server: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } }前端的请求路径用/api/diet-record,开发时Vite把请求转发到后端8080端口,浏览器看到的始终是同源请求,彻底绕开CORS。这个方案比后端开@CrossOrigin更干净,部署到服务器后,Nginx同样做一次反向代理就能复用。
6. 开发中印象最深的三个坑:实测排查过程
6.1 坑一:实体类生成建表SQL后,字段默认值报错
我在用反射工具生成DDL时,遇到过一次很典型的错误。系统启动时报“Invalid default value for 'quantity'”,一开始我以为是MySQL 8的严格模式问题,查了sql_mode没发现异常。后来去翻生成的建表语句,发现quantity DOUBLE NOT NULL后面没有默认值,但是插入时我没给quantity传值,数据库直接拒绝写入。
排查链路是这样的:先看启动日志定位到建表SQL,然后对照实体类,发现quantity字段在Java里是Double类型,反射工具把它映射成了DOUBLE,但没处理默认值。而quantity在实际业务里必须由前端传值,不应该有默认值。
解决方式是在DDL生成工具里做规则补充:LocalDateTime类型映射为DATETIME DEFAULT CURRENT_TIMESTAMP,整数类型映射为DEFAULT 0,Double类型映射为DEFAULT NULL。另外我还在实体类的quantity字段上加了@NotNull校验,后端在写入前就对空值拦截。
6.2 坑二:Node版本和npm install卡住
前端环境的坑就没断过。最开始我机器上是Node 14,Vite 4启动时直接报错,提示Node版本必须>=16.18。当时我第一反应是Vite太新了,想降级Vite,但后来想想,与其迁就旧版本,不如用nvm切换Node版本。切到Node 18.16 LTS之后,项目启动顺畅了。
第二个问题是npm install卡在idealTree阶段,半天没反应。这个在校园网环境下特别常见,不是代码问题,是npm默认源访问慢。解决办法就是前面写的:
npm config set registry https://registry.npmmirror.com配置完源之后,重新删除node_modules和package-lock.json再装一次,就顺利通过了。如果还卡,可以设置npm config set fetch-retries=5增加重试次数。
6.3 坑三:生重和熟重混用,热量报表突然失真
这个坑最隐蔽。食物表里米饭的热量是“每100克生米”约346千卡,但用户实际吃的是熟米饭,100克熟饭的热量大约只有116千卡。如果用户选了“米饭”然后填了200克,前端直接按346去算,一天的热量结果能高出一倍多。
我当时排查的过程是:用户反馈“我一天只吃了三顿饭,怎么热量显示3000多”。我去看数据库记录,发现用户填了200克米饭,后台按生米热量一乘,直接算出692千卡,一顿饭就超了半天的预算。
解决方案就是前面提到的food_state字段。我在食物表里区分了“生重”和“熟重”两套数据逻辑,录入食物时明确标注状态,计算时根据状态选择对应的热量基准。前端在选择食物时会显示“生重/熟重”标签,后端接口在返回食物详情时也带上这个字段,避免前端瞎猜。这个坑其实属于业务设计问题,但如果不实际跑数据,很难提前发现。
7. 项目交付:源码目录、数据库脚本与文档的完整组织
7.1 源码组织:让别人一小时跑起来
项目交付和项目开发是两种心态。开发时你可以按自己的习惯随手放文件,但交付时必须考虑接收方——可能是答辩老师、可能是实习mentor、可能是三年后的你自己。我交付源码时,在README.md里写清楚了以下内容:JDK版本要求、Node版本要求、MySQL版本要求、后端启动步骤、前端启动步骤、默认登录账号。
后端启动步骤只有三条:
# 1. 导入 sql/init.sql 和 sql/data.sql 到 MySQL # 2. 修改 application.yml 里的数据库连接信息 # 3. 在 diet-server 目录执行 mvn spring-boot:run前端也是三条:
cd diet-web npm install npm run dev一套流程走下来,别人不会卡在环境上。
7.2 数据库脚本:初始化脚本与测试数据
数据库脚本我放在独立的sql/目录,分两个文件。init.sql只管建库建表,包含所有表的DDL语句和索引定义;data.sql只放基础数据,包括系统管理员账号、几份食物数据和一份示例食谱。这样分开的好处是,如果用户只需要空表结构,可以只执行init.sql;如果要做演示,两个都执行。
MySQL 8下建表时我用utf8mb4,避免中文乱码:
CREATE DATABASE IF NOT EXISTS diet_nutrition DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;数据脚本里我特意加了两条典型的熟食和生食例子(熟米饭、生鸡胸肉),方便验收方快速看到生熟两套计算逻辑的效果。
7.3 文档应该包含哪些东西
文档不是越厚越好,而是要让读者能按图索骥。我交付的文档分三份:
第一份是部署文档,对应README里的启动步骤,在此基础上补充了常见问题,比如端口占用、MySQL连接失败、Node版本不匹配。第二份是数据库设计文档,包含每张表的字段说明、用途和ER图。第三份是接口文档,我用knife4j根据注解自动生成,省去了手写接口文档的大量劳动。如果你不想引入额外工具,也可以用Postman导出JSON接口集合作为替代。
接口文档里我建议至少包含登录、食物分页查询、新增饮食记录、近七天统计这四个核心接口的请求响应示例。答辩时老师盯着接口文档问“这个参数是什么意思”,你能快速指出来,印象分会好很多。
7.4 演示前必须检查的三个细节
交付之前,我建议按下述清单过一遍演示环境。第一,用新建的普通用户账号走一遍注册、录饮食、看报告的全流程,不要用管理员账号演示用户功能,否则角色逻辑说不清。第二,提前准备几个有明确营养数值的食物数据,比如“一个水煮蛋50克、蛋白质6克”,演示时边录边口算验证,系统算出的数值和常识对得上,说服力极强。第三,检查系统时间。diet_record表按record_date过滤数据,如果服务器时区不对,可能导致“今天录的记录查不出来”。我当时就在application.yml里显式配置了serverTimezone=Asia/Shanghai,避免时区问题在演示当天爆雷。
我个人做这个项目最大的体会是:这类管理系统真正的坑从来不在某个单独的技术点上,而在数据语义和业务闭环之间。把“每100克”和“实际克数”的关系理清,把生熟状态纳入设计,把BMR公式落到代码里,项目的完成度立刻就不一样了。最后再分享一个小建议:交付前花半小时用全新的机器或干净环境跑一遍启动文档,确认每一步都真实可行,这一遍跑完,你就再也不会被“在我电脑上能跑”这句话困住了。