【营养分析引擎】计算个性化卡路里建议给《灵犀厨房》装上“营养大脑”摘要从“爱吃什么”到“该吃什么”是《灵犀厨房》进化的关键一步。上一篇我们刚打通了 Health Kit 数据今天我们就要基于 Mifflin-St Jeor 医学公式为每个用户装上专属的“营养大脑”。这篇文章将带你一步步拆解如何计算精准的每日热量预算如何用营养雷达图呈现三大宏量素并让推荐出的每一道菜都经过卡路里“安检”。这不仅仅是写代码这是在用 ArkTS 为每一个身体编写独一无二的“能量使用说明书”。说明后续文章将基于【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战之补充【架构进化】灵犀厨房四层分层设计给鸿蒙 App 搭一副坚不可摧的骨架文章所设计的架构进行编撰。一、从“推荐菜谱”到“营养把关”在前面11篇文章里我们并肩作战已经让《灵犀厨房》从一个想法长成了一个功能完备的应用首页推荐第4-5篇拍照识别食材第6篇AI 推荐引擎第7篇菜谱详情与食材勾选第8-9篇智能购物清单第10篇Health Kit数据打通第11篇这些功能完美地回答了“今天吃什么”。但一个真正“懂你”的AI厨艺助手必须能回答更关键的问题“我该吃多少”一个60kg的办公室白领和一个85kg的健身爱好者对“红烧肉780 kcal”的需求是天差地别的。不加以区分地推荐不是关爱而是失职。本篇我们就要解决这个问题给推荐引擎装上“营养分析”的大脑让每一份推荐都有据可依。二、核心原理与底层机制深度解读身体的“能量方程式”营养分析引擎的核心不是凭空捏造而是忠实地实现一个被全球营养学界广泛认可的“能量方程式”——Mifflin-St Jeor 公式。我们可以把它理解为计算身体“基础油耗”的黄金法则。一辆汽车停在原地开空调每小时会消耗固定量的汽油这就是它的“基础能耗”。人体的基础代谢率BMRBasal Metabolic Rate也是同理你在极度安静下仅维持生命所需的最低热量。Mifflin-St Jeor 公式为我们提供了这个精准的“油耗计算公式”男性 BMR10 × 体重(kg) 6.25 × 身高(cm) - 5 × 年龄(岁) 5女性 BMR10 × 体重(kg) 6.25 × 身高(cm) - 5 × 年龄(岁) - 161算出基础油耗后我们还要考虑你每天“开多快、跑多远”。这就引入了活动系数从“久坐不动1.2”到“极度活跃1.9”最终算出每日总消耗TDEETotal Daily Energy ExpenditureTDEE BMR × 活动系数在 HarmonyOS 中这个过程就是一个纯函数的数据转换管道我们输入UserHealthProfile性别、年龄、身高、体重、运动等级经过HealthServiceHelper的计算就能输出一个包含每日热量、蛋白质、碳水、脂肪的NutritionBudget对象。这个过程干净、纯粹不依赖任何UI极具可测试性。三、架构设计营养引擎的“位置感”我们必须时刻谨记架构的分层原则才能让代码健壮且易于维护。营养分析引擎在《灵犀厨房》中的位置如下图所示Foundation层UserHealthProfileGenderActivityLevelServices层HealthServiceHelperMifflin-St Jeor 计算Business层NutritionAnalyzercalculateBudgetassessMealBalancefilterByCalorieBudgetViewModel层HealthDashboardViewModelProfileViewModelUI层HealthDashboardPageNutritionRadarCardProfilePage健康档案表单这个分层依赖关系清晰明了上层依赖下层下层对上层无知。Services层只做数学计算Business层聚合业务规则ViewModel层管理UI状态UI层只负责呈现。这种结构让我们的“营养大脑”可以被任何页面复用无论是仪表盘、推荐列表还是未来的报告功能。四、实战为《灵犀厨房》注入“卡路里智慧”我们直接进入代码实战分四步走为 App 打造完整的营养分析链路。Step 1扩充 Foundation 数据模型定义健康“基因蓝图”首先我们需要在foundation/model/UserPreference.ets中定义用户健康档案的“基因蓝图”。// foundation/model/UserPreference.ets// 性别枚举exportenumGender{MALEmale,FEMALEfemale}// 运动等级对应不同活动系数exportenumActivityLevel{SEDENTARYsedentary,// 久坐不动: 1.2LIGHTlight,// 轻度运动: 1.375MODERATEmoderate,// 中度运动: 1.55ACTIVEactive,// 积极运动: 1.725VERY_ACTIVEveryActive// 极度活跃: 1.9}// 用户健康档案接口exportinterfaceUserHealthProfile{gender:Gender;age:number;height:number;// 单位: cmweight:number;// 单位: kgactivityLevel:ActivityLevel;}核心点解读我们遵循“从抽象到具体”的原则。Gender和ActivityLevel这样的枚举类型比裸用string或number更安全能避免魔法数字并获得编译时检查。UserHealthProfile接口就是营养引擎的标准化输入是后续一切计算的起点。Step 2构建 Services 层实现医学公式的“纯函数”在services/HealthServiceHelper.ets中我们将 Mifflin-St Jeor 公式封装成一个纯净的服务。// services/HealthServiceHelper.etsimport{Gender,ActivityLevel,UserHealthProfile}from../foundation/model/UserPreference;// 活动系数映射表constACTIVITY_FACTOR_MAP:RecordActivityLevel,number{[ActivityLevel.SEDENTARY]:1.2,[ActivityLevel.LIGHT]:1.375,[ActivityLevel.MODERATE]:1.55,[ActivityLevel.ACTIVE]:1.725,[ActivityLevel.VERY_ACTIVE]:1.9,};// 营养预算结果接口exportinterfaceNutritionBudget{recommendedCalories:number;bmr:number;proteinGrams:number;carbGrams:number;fatGrams:number;activityFactor:number;}exportclassHealthServiceHelper{// 核心计算方法staticcalculateNutritionBudget(profile:UserHealthProfile):NutritionBudget{// 1. 计算基础代谢率 BMRletbmr10*profile.weight6.25*profile.height-5*profile.age;if(profile.genderGender.MALE){bmr5;}else{bmr-161;}// 2. 获取活动系数constactivityFactorACTIVITY_FACTOR_MAP[profile.activityLevel]??1.2;// 3. 计算每日推荐热量 TDEE BMR × 活动系数constrecommendedCaloriesMath.round(bmr*activityFactor);// 4. 三大宏量素分配蛋白质 25%碳水 50%脂肪 25%constproteinGramsMath.round((recommendedCalories*0.25)/4);// 1g蛋白质4kcalconstcarbGramsMath.round((recommendedCalories*0.50)/4);// 1g碳水4kcalconstfatGramsMath.round((recommendedCalories*0.25)/9);// 1g脂肪9kcalreturn{recommendedCalories,bmr:Math.round(bmr),proteinGrams,carbGrams,fatGrams,activityFactor};}}核心点解读HealthServiceHelper.calculateNutritionBudget是一个纯函数它的输出完全由输入决定没有任何副作用。这让它极易进行单元测试。例如我们可以轻易地为“30岁、70kg、175cm的轻度运动男性”写一个测试用例验证其BMR是否约为1690每日预算是否约为2323。这种确定性是医疗健康类功能的核心要求。Step 3构建 Business 层组装业务逻辑business/NutritionAnalyzer.ets负责聚合业务规则它像一个指挥官调用底层服务并做出决策。// business/NutritionAnalyzer.etsimport{Recipe}from../foundation/model/Recipe;import{UserHealthProfile}from../foundation/model/UserPreference;import{HealthServiceHelper,NutritionBudget}from../services/HealthServiceHelper;// 膳食均衡状态枚举exportenumBalanceStatus{GOODgood,WARNINGwarning,EXCEEDEDexceeded}// 均衡评估结果接口exportinterfaceDietBalanceResult{status:BalanceStatus;suggestion:string;consumptionPercent:number;}exportclassNutritionAnalyzer{// 1. 计算每日营养预算staticcalculateBudget(profile:UserHealthProfile):NutritionBudget{returnHealthServiceHelper.calculateNutritionBudget(profile);}// 2. 评估单餐均衡度staticassessMealBalance(recipe:Recipe,budget:NutritionBudget,consumedToday:number):DietBalanceResult{consttotalAfterMealconsumedTodayrecipe.calories;constconsumptionPercentMath.round((totalAfterMeal/budget.recommendedCalories)*100);letstatus:BalanceStatus;letsuggestion:string;constremainingbudget.recommendedCalories-totalAfterMeal;if(consumptionPercent70){statusBalanceStatus.GOOD;suggestion这道菜热量适中你还有${remaining}kcal 的配额吃得优雅;}elseif(consumptionPercent100){statusBalanceStatus.WARNING;suggestion热量摄入接近上限建议下一餐选择低卡菜谱。;}else{statusBalanceStatus.EXCEEDED;constexcesstotalAfterMeal-budget.recommendedCalories;suggestion⚠️ 热量已超出每日预算${excess}kcal请谨慎选择。;}return{status,suggestion,consumptionPercent};}// 3. 按热量预算筛选菜谱staticfilterByCalorieBudget(recipes:Recipe[],budget:NutritionBudget,maxMealPercent:number50):Recipe[]{constmealLimitMath.round(budget.recommendedCalories*maxMealPercent/100);returnrecipes.filter(rr.caloriesmealLimit);}}核心点解读NutritionAnalyzer将多个原子能力组装成业务场景。assessMealBalance方法不仅给出了数值还给出了人性化的suggestion这让UI层可以展示贴心的提示而不仅仅是冰冷的数字。filterByCalorieBudget则是连接营养引擎和推荐列表的关键桥梁。Step 4ViewModel 与 UI 集成让数据“活”起来ViewModel 作为 UI 和业务逻辑的桥梁负责管理状态和调用分析器。// viewmodel/HealthDashboardViewModel.etsimport{NutritionAnalyzer,NutritionBudget}from../business/NutritionAnalyzer;import{UserHealthProfile,Gender,ActivityLevel}from../foundation/model/UserPreference;exportclassHealthDashboardViewModel{nutritionBudget:NutritionBudget|nullnull;// 当用户档案更新或页面加载时调用loadNutritionData():void{// 模拟从 Health Kit 或 个人中心获取的档案constmockProfile:UserHealthProfile{gender:Gender.MALE,age:30,height:175,weight:70,activityLevel:ActivityLevel.LIGHT};this.nutritionBudgetNutritionAnalyzer.calculateBudget(mockProfile);console.info([HealthDashboardVM] 营养预算已计算:,JSON.stringify(this.nutritionBudget));}}对应的NutritionRadarCard组件接收NutritionBudget并渲染。// components/NutritionRadarCard.etsComponentexportstruct NutritionRadarCard{Propbudget:NutritionBudget|null;build(){Column(){Text(每日营养预算).fontSize(18).fontWeight(FontWeight.Bold)if(this.budget){Text(${this.budget.recommendedCalories}kcal).fontSize(36).fontWeight(FontWeight.Bold).fontColor(#FF6B6B)Text(基础代谢率 (BMR):${this.budget.bmr}kcal).fontSize(14)// ... 蛋白质/碳水/脂肪的进度条UI}else{Text(加载中...)}}.width(100%).padding(20).borderRadius(16).backgroundColor(Color.White)}}核心点解读我们利用 ArkUI 的Prop实现父组件到子组件的单向数据流。当 ViewModel 中的nutritionBudget对象被整体替换或使用ObjectLink更新属性时UI会自动刷新。记住在 HarmonyOS 开发中UI是状态的函数。五、运行与结果验证现在让我们在模拟器或真机上运行并观察 Log 输出。操作步骤启动应用进入“健康”Tab。观察NutritionRadarCard卡片应显示推荐热量。进入“我的”Tab修改个人档案如将体重改为 85kg。返回“健康”Tab下拉刷新观察数值变化。特别说明当前由于Health Service Kit数据暂没有真实接入这里暂时使用固定数值优先展示效果。为了更清晰地展示数据流和验证逻辑我们补充一个模拟数据加载与计算的完整流程说明具体刷新日志信息显示如下[HealthDashboardVM] 开始加载健康数据... [HealthServiceHelper] 使用模拟健康数据需上架后切换 Health Kit [HealthServiceHelper] 使用模拟运动数据 [HealthServiceHelper] 使用模拟睡眠数据 [HealthDashboardVM] 健康数据: 步数8432, 心率72, 睡眠7.5h [NutritionAnalyzer] 开始计算个性化营养预算... [HealthServiceHelper] 营养预算计算完成: BMR1568 kcal, TDEE2155 kcal, 蛋白135g, 碳水269g, 脂肪60g [NutritionAnalyzer] 计算结果: TDEE2155 kcal, 蛋白135g, 碳水269g, 脂肪60g后续真实数据日志预期 Log 输出[HealthDashboardVM] 营养预算已计算: {recommendedCalories:2319,bmr:1686,proteinGrams:145,carbGrams:290,fatGrams:64,activityFactor:1.375}修改体重为85kg后再次加载[HealthDashboardVM] 营养预算已计算: {recommendedCalories:2538,bmr:1846,proteinGrams:159,carbGrams:317,fatGrams:70,activityFactor:1.375}日志解读从日志可见当体重从70kg变为85kg后BMR从1686升至1846 kcal每日总推荐也从2319升至2538 kcal。这完美验证了我们引擎的灵敏度和正确性。我们的“营养大脑”已经可以忠实地反映用户身体状况的变化这是“千人千面”推荐的基础。六、本阶段总结与下篇预告今天我们为《灵犀厨房》成功植入了“营养大脑”。我们不仅学习了 Mifflin-St Jeor 这一医学界公认的“能量公式”更将其完美融入到了 HarmonyOS 的分层架构中从 Foundation 层的“基因蓝图”定义到 Services 层的“纯函数”实现再到 Business 层的“指挥官”式调度最终在 UI 上化为一张易懂的营养雷达卡片。至此我们的推荐引擎已经能感知用户的健康需求。但厨房里的智慧远不止于此。烹饪过程中我们还需要与各种电器打交道。下篇预告我们将进入全新的维度——【智能厨电模拟】用代码模拟发现与控制设备。我将带你用纯代码构建一个虚拟的智能厨房模拟发现烤箱、电磁炉等设备并进行控制为后续的分布式流转和语音控制埋下第一个伏笔。敬请期待 本系列持续更新中下一篇将带你玩转智能厨电模拟用代码“凭空”创造一整套厨房设备。专栏入口[《从0到1开发灵犀厨房App》合集] | ⭐源码Gitee 仓库