【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(十二)之【营养分析引擎】计算个性化卡路里建议:给《灵犀厨房》装上“营养大脑”

【营养分析引擎】计算个性化卡路里建议:给《灵犀厨房》装上"营养大脑"

摘要:从"爱吃什么"到"该吃什么",是《灵犀厨房》进化的关键一步。上一篇我们刚打通了 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;
}

核心点解读

我们遵循"从抽象到具体"的原则。GenderActivityLevel 这样的枚举类型,比裸用 stringnumber 更安全,能避免魔法数字,并获得编译时检查。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 输出。

操作步骤

  1. 启动应用,进入"健康"Tab。

  2. 观察 NutritionRadarCard 卡片,应显示推荐热量。

  3. 进入"我的"Tab,修改个人档案(如将体重改为 85kg)。

  4. 返回"健康"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 仓库


相关推荐
若兰幽竹2 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战之补充【架构进化】灵犀厨房四层分层设计:给鸿蒙 App 搭一副坚不可摧的骨架
架构·鸿蒙系统·harmonyos6.1.0·灵犀厨房
若兰幽竹8 天前
【HarmonyOS 6.1 全场景实战】《灵犀厨房》实战(三):ArkTS 高效开发:TypeScript 核心与 API 23 新规
harmonyos·鸿蒙系统·harmonyos6.1.0
若兰幽竹17 天前
【HarmonyOS 6.1 全场景实战】开篇词:打造消除“吃饭焦虑”的《灵犀厨房》
harmonyos·鸿蒙开发·华为鸿蒙系统