【营养分析引擎】计算个性化卡路里建议:给《灵犀厨房》装上"营养大脑"
摘要:从"爱吃什么"到"该吃什么",是《灵犀厨房》进化的关键一步。上一篇我们刚打通了 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 公式。我们可以把它理解为计算身体"基础油耗"的黄金法则。
一辆汽车停在原地开空调,每小时会消耗固定量的汽油,这就是它的"基础能耗"。人体的基础代谢率(BMR,Basal Metabolic Rate)也是同理:你在极度安静下,仅维持生命所需的最低热量。Mifflin-St Jeor 公式为我们提供了这个精准的"油耗计算公式":
- 男性 BMR =
10 × 体重(kg) + 6.25 × 身高(cm) - 5 × 年龄(岁) + 5 - 女性 BMR =
10 × 体重(kg) + 6.25 × 身高(cm) - 5 × 年龄(岁) - 161
算出基础油耗后,我们还要考虑你每天"开多快、跑多远"。这就引入了活动系数,从"久坐不动(1.2)"到"极度活跃(1.9)",最终算出每日总消耗(TDEE,Total Daily Energy Expenditure):
TDEE = BMR × 活动系数
在 HarmonyOS 中,这个过程就是一个纯函数的数据转换管道:我们输入 UserHealthProfile(性别、年龄、身高、体重、运动等级),经过 HealthServiceHelper 的计算,就能输出一个包含每日热量、蛋白质、碳水、脂肪的 NutritionBudget 对象。这个过程干净、纯粹,不依赖任何UI,极具可测试性。
三、架构设计:营养引擎的"位置感"
我们必须时刻谨记架构的分层原则,才能让代码健壮且易于维护。营养分析引擎在《灵犀厨房》中的位置,如下图所示:
Foundation层
UserHealthProfile
Gender
ActivityLevel
Services层
HealthServiceHelper
Mifflin-St Jeor 计算
Business层
NutritionAnalyzer
calculateBudget
assessMealBalance
filterByCalorieBudget
ViewModel层
HealthDashboardViewModel
ProfileViewModel
UI层
HealthDashboardPage
NutritionRadarCard
ProfilePage
健康档案表单
这个分层依赖关系清晰明了:上层依赖下层,下层对上层无知。Services 层只做数学计算,Business 层聚合业务规则,ViewModel 层管理UI状态,UI 层只负责呈现。这种结构让我们的"营养大脑"可以被任何页面复用,无论是仪表盘、推荐列表还是未来的报告功能。
四、实战:为《灵犀厨房》注入"卡路里智慧"
我们直接进入代码实战,分四步走,为 App 打造完整的营养分析链路。
Step 1:扩充 Foundation 数据模型,定义健康"基因蓝图"
首先,我们需要在 foundation/model/UserPreference.ets 中定义用户健康档案的"基因蓝图"。
typescript
// foundation/model/UserPreference.ets
// 性别枚举
export enum Gender {
MALE = 'male',
FEMALE = 'female'
}
// 运动等级(对应不同活动系数)
export enum ActivityLevel {
SEDENTARY = 'sedentary', // 久坐不动: 1.2
LIGHT = 'light', // 轻度运动: 1.375
MODERATE = 'moderate', // 中度运动: 1.55
ACTIVE = 'active', // 积极运动: 1.725
VERY_ACTIVE = 'veryActive' // 极度活跃: 1.9
}
// 用户健康档案接口
export interface UserHealthProfile {
gender: Gender;
age: number;
height: number; // 单位: cm
weight: number; // 单位: kg
activityLevel: ActivityLevel;
}
核心点解读 :
我们遵循"从抽象到具体"的原则。
Gender和ActivityLevel这样的枚举类型,比裸用string或number更安全,能避免魔法数字,并获得编译时检查。UserHealthProfile接口就是营养引擎的标准化输入,是后续一切计算的起点。
Step 2:构建 Services 层,实现医学公式的"纯函数"
在 services/HealthServiceHelper.ets 中,我们将 Mifflin-St Jeor 公式封装成一个纯净的服务。
typescript
// services/HealthServiceHelper.ets
import { Gender, ActivityLevel, UserHealthProfile } from '../foundation/model/UserPreference';
// 活动系数映射表
const ACTIVITY_FACTOR_MAP: Record<ActivityLevel, number> = {
[ActivityLevel.SEDENTARY]: 1.2,
[ActivityLevel.LIGHT]: 1.375,
[ActivityLevel.MODERATE]: 1.55,
[ActivityLevel.ACTIVE]: 1.725,
[ActivityLevel.VERY_ACTIVE]: 1.9,
};
// 营养预算结果接口
export interface NutritionBudget {
recommendedCalories: number;
bmr: number;
proteinGrams: number;
carbGrams: number;
fatGrams: number;
activityFactor: number;
}
export class HealthServiceHelper {
// 核心计算方法
static calculateNutritionBudget(profile: UserHealthProfile): NutritionBudget {
// 1. 计算基础代谢率 BMR
let bmr = 10 * profile.weight + 6.25 * profile.height - 5 * profile.age;
if (profile.gender === Gender.MALE) {
bmr += 5;
} else {
bmr -= 161;
}
// 2. 获取活动系数
const activityFactor = ACTIVITY_FACTOR_MAP[profile.activityLevel] ?? 1.2;
// 3. 计算每日推荐热量 TDEE = BMR × 活动系数
const recommendedCalories = Math.round(bmr * activityFactor);
// 4. 三大宏量素分配:蛋白质 25%,碳水 50%,脂肪 25%
const proteinGrams = Math.round((recommendedCalories * 0.25) / 4); // 1g蛋白质=4kcal
const carbGrams = Math.round((recommendedCalories * 0.50) / 4); // 1g碳水=4kcal
const fatGrams = Math.round((recommendedCalories * 0.25) / 9); // 1g脂肪=9kcal
return {
recommendedCalories,
bmr: Math.round(bmr),
proteinGrams,
carbGrams,
fatGrams,
activityFactor
};
}
}
核心点解读 :
HealthServiceHelper.calculateNutritionBudget是一个纯函数,它的输出完全由输入决定,没有任何副作用。这让它极易进行单元测试。例如,我们可以轻易地为"30岁、70kg、175cm的轻度运动男性"写一个测试用例,验证其BMR是否约为1690,每日预算是否约为2323。这种确定性是医疗健康类功能的核心要求。
Step 3:构建 Business 层,组装业务逻辑
business/NutritionAnalyzer.ets 负责聚合业务规则,它像一个指挥官,调用底层服务并做出决策。
typescript
// business/NutritionAnalyzer.ets
import { Recipe } from '../foundation/model/Recipe';
import { UserHealthProfile } from '../foundation/model/UserPreference';
import { HealthServiceHelper, NutritionBudget } from '../services/HealthServiceHelper';
// 膳食均衡状态枚举
export enum BalanceStatus {
GOOD = 'good',
WARNING = 'warning',
EXCEEDED = 'exceeded'
}
// 均衡评估结果接口
export interface DietBalanceResult {
status: BalanceStatus;
suggestion: string;
consumptionPercent: number;
}
export class NutritionAnalyzer {
// 1. 计算每日营养预算
static calculateBudget(profile: UserHealthProfile): NutritionBudget {
return HealthServiceHelper.calculateNutritionBudget(profile);
}
// 2. 评估单餐均衡度
static assessMealBalance(
recipe: Recipe,
budget: NutritionBudget,
consumedToday: number
): DietBalanceResult {
const totalAfterMeal = consumedToday + recipe.calories;
const consumptionPercent = Math.round((totalAfterMeal / budget.recommendedCalories) * 100);
let status: BalanceStatus;
let suggestion: string;
const remaining = budget.recommendedCalories - totalAfterMeal;
if (consumptionPercent <= 70) {
status = BalanceStatus.GOOD;
suggestion = `这道菜热量适中,你还有 ${remaining} kcal 的配额,吃得优雅~`;
} else if (consumptionPercent <= 100) {
status = BalanceStatus.WARNING;
suggestion = '热量摄入接近上限,建议下一餐选择低卡菜谱。';
} else {
status = BalanceStatus.EXCEEDED;
const excess = totalAfterMeal - budget.recommendedCalories;
suggestion = `⚠️ 热量已超出每日预算 ${excess} kcal,请谨慎选择。`;
}
return { status, suggestion, consumptionPercent };
}
// 3. 按热量预算筛选菜谱
static filterByCalorieBudget(
recipes: Recipe[],
budget: NutritionBudget,
maxMealPercent: number = 50
): Recipe[] {
const mealLimit = Math.round(budget.recommendedCalories * maxMealPercent / 100);
return recipes.filter(r => r.calories <= mealLimit);
}
}
核心点解读 :
NutritionAnalyzer将多个原子能力组装成业务场景。assessMealBalance方法不仅给出了数值,还给出了人性化的suggestion,这让UI层可以展示贴心的提示,而不仅仅是冰冷的数字。filterByCalorieBudget则是连接营养引擎和推荐列表的关键桥梁。
Step 4:ViewModel 与 UI 集成,让数据"活"起来
ViewModel 作为 UI 和业务逻辑的桥梁,负责管理状态和调用分析器。
typescript
// viewmodel/HealthDashboardViewModel.ets
import { NutritionAnalyzer, NutritionBudget } from '../business/NutritionAnalyzer';
import { UserHealthProfile, Gender, ActivityLevel } from '../foundation/model/UserPreference';
export class HealthDashboardViewModel {
nutritionBudget: NutritionBudget | null = null;
// 当用户档案更新或页面加载时调用
loadNutritionData(): void {
// 模拟从 Health Kit 或 个人中心获取的档案
const mockProfile: UserHealthProfile = {
gender: Gender.MALE,
age: 30,
height: 175,
weight: 70,
activityLevel: ActivityLevel.LIGHT
};
this.nutritionBudget = NutritionAnalyzer.calculateBudget(mockProfile);
console.info('[HealthDashboardVM] 营养预算已计算:', JSON.stringify(this.nutritionBudget));
}
}
对应的 NutritionRadarCard 组件接收 NutritionBudget 并渲染。
typescript
// components/NutritionRadarCard.ets
@Component
export struct NutritionRadarCard {
@Prop budget: 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数据暂没有真实接入,这里暂时使用固定数值优先展示效果。为了更清晰地展示数据流和验证逻辑,我们补充一个模拟数据加载与计算的完整流程说明,具体刷新日志信息显示如下 :log[HealthDashboardVM] 开始加载健康数据... [HealthServiceHelper] 使用模拟健康数据(需上架后切换 Health Kit) [HealthServiceHelper] 使用模拟运动数据 [HealthServiceHelper] 使用模拟睡眠数据 [HealthDashboardVM] 健康数据: 步数=8432, 心率=72, 睡眠=7.5h [NutritionAnalyzer] 开始计算个性化营养预算... [HealthServiceHelper] 营养预算计算完成: BMR=1568 kcal, TDEE=2155 kcal, 蛋白=135g, 碳水=269g, 脂肪=60g [NutritionAnalyzer] 计算结果: TDEE=2155 kcal, 蛋白=135g, 碳水=269g, 脂肪=60g
后续真实数据日志预期 Log 输出:
text
[HealthDashboardVM] 营养预算已计算: {"recommendedCalories":2319,"bmr":1686,"proteinGrams":145,"carbGrams":290,"fatGrams":64,"activityFactor":1.375}
修改体重为85kg后,再次加载:
text
[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 仓库