2fish 的部落格

歡迎來到我的部落格!這裡是我記錄開發點滴的地方,希望你會喜歡😌

0%

[ 2024 IT 鐵人賽 DAY20 ] 後端專案實戰:鸚鵡飲食計算 API 開發全紀錄

嗨,大家好!昨天我們學會了如何定義鮮食的 Model,以及如何建立一個 API 來取得資料庫中的鮮食資料。相信大家對於如何在 Express 專案中建立 API,以及如何與 MongoDB 互動,已經有了一個基本的了解了。
今天,我們要繼續探索,學習如何建立一個計算每日應攝取量的 API。這個 API 會根據使用者輸入的體重和活動量,以及選擇的鮮食,計算出每日應攝取的營養素量和熱量。這樣一來,我們就可以給使用者一個參考,讓他們知道自己的鸚鵡每天應該吃多少食物,攝取多少營養素。

在開始寫程式之前,我們先來了解一下如何計算鸚鵡的每日所需熱量和營養素吧!

房東不給養鸚鵡 - 那就用 Nuxt + Express + MongoDB 30 天寫一個全端鸚鵡專案。

挑戰人生第一次鐵人賽,就來個佛心分享 side project,從專案發想、規劃、設計、資料庫、開發到部署,技術使用 Nuxt 3、Tailwind CSS、Pinia、Axios、Express、MongoDB,製作一個前後端分離的鸚鵡專案,功能主要介紹食物計算機和聯絡我們,希望可以讓更多人瞭解專案的完整流程 ✨

IT 全文章連結

計算鸚鵡每日所需熱量

要計算鸚鵡每日所需的熱量,我們需要知道以下幾個數據:

  1. 鸚鵡的體重(單位:克)
  2. 鸚鵡的活動量(低、中、高)
  3. 鸚鵡的基礎代謝率(BMR)
    其中,BMR 是指在完全休息狀態下,維持身體基本功能所需的最低熱量。我們可以使用一個專門針對鸚鵡設計的 BMR 計算公式來計算鸚鵡的 BMR:
1
BMR = K * (weight / 1000)^0.75

其中,K 是一個常數,代表鸚鵡的代謝系數,通常取 175。weight 是鸚鵡的體重,單位是克。我們將體重除以 1000,轉換為公斤,然後取 0.75 次方。
有了 BMR 後,我們還需要根據鸚鵡的活動量來調整 BMR,得到鸚鵡每日所需的總熱量。我們可以定義三個活動量等級,每個等級對應一個調整係數:

  • 低活動量:1.2
  • 中活動量:1.4
  • 高活動量:1.6
    我們將 BMR 乘以對應的調整係數,就得到了鸚鵡每日所需的總熱量。

計算鸚鵡每日所需營養素

除了熱量,我們還需要計算鸚鵡每日所需的三大營養素:蛋白質、脂肪、碳水化合物。我們可以假設這三大營養素在總熱量中所占的比例如下:

  • 蛋白質:20%
  • 脂肪:20%
  • 碳水化合物:60%
    我們將每日所需總熱量乘以這些百分比,就得到了每種營養素所需的熱量。然後,我們再將這些熱量除以每克營養素的熱量(蛋白質和碳水化合物每克 4 卡,脂肪每克 9 卡),就得到了每日所需的三大營養素的克數。

計算鮮食可提供的營養素和熱量

現在,我們知道了如何計算鸚鵡每日所需的熱量和營養素。接下來,我們要計算選擇的鮮食可以提供多少營養素和熱量。
首先,我們需要從鮮食的 Model 中取出以下數據:

  • 食物的熱量(每 100 克)
  • 食物的蛋白質含量(每 100 克)
  • 食物的脂肪含量(每 100 克)
  • 食物的碳水化合物含量(每 100 克)
  • 食物的最大攝取量(單位:克)
    然後,我們可以計算在最大攝取量下,這個食物可以提供多少熱量。同時,我們也可以計算在不超過每日所需熱量的情況下,這個食物最多可以攝取多少克。
    有了食物的最大攝取量後,我們可以計算這個食物可以提供多少克的三大營養素。我們將最大攝取量乘以食物的營養素含量百分比,就得到了每種營養素的攝取量。

接著,我們可以計算這個食物提供的總熱量。我們將每種營養素的攝取量乘以其熱量,然後相加,就得到了總熱量。

最後,我們可以計算這個食物提供的熱量與每日所需熱量的差異。這個差異可以讓使用者知道,如果只吃這一種食物,是否能滿足每日所需的熱量。

開始寫程式

好了,現在我們對計算鸚鵡每日所需熱量和營養素的方法有了一個大致的了解。讓我們開始寫程式吧!

第一步:定義路由和 Swagger 註解

首先,我們要定義一個 POST 請求的路由,路徑是 /calculatefood。這個路由會接收一個 JSON 格式的請求體,其中包含了以下幾個欄位:

  • weight:鸚鵡的體重,單位是克。
  • activity:鸚鵡的活動量,可以是 "low""medium""high"
  • foodId:要計算的鮮食的 ID。
    我們使用 Swagger 的註解來描述這個 API 的詳細信息,包括請求體的結構。這樣,其他人在使用我們的 API 時,就可以直接在 Swagger UI 中看到這些信息,方便他們理解和使用我們的 API。
1
2
3
4
5
6
router.post(
"/calculatefood",
handleErrorAsync(async (req, res, next) => {
// 路由處理函數的內容
})
);

第二步:取得請求參數並進行驗證

進入路由處理函數後,我們首先從請求體中取出 weightactivityfoodId 這三個參數。然後,我們對這些參數進行驗證,確保它們都是必填的,並且 foodId 對應的鮮食是存在的。如果驗證失敗,我們就使用 appError 函數返回一個錯誤訊息。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 取得參數
const { weight, activity, foodId } = req.body;

// 驗證必填欄位
if (!weight || !activity || !foodId) {
return next(appError(400, "weight, activity, foodId 為必填"));
}

// 驗證食物是否存在
const freshFood = await FreshFood.findById(foodId);
if (!freshFood) {
return next(appError(400, "食物不存在"));
}

第三步:計算 BMR 和每日所需熱量

接下來,我們開始計算鸚鵡的基礎代謝率(BMR)。我們將鸚鵡的體重轉換為公斤,然後帶入 BMR 計算公式計算出 BMR。
然後,我們根據鸚鵡的活動量來調整 BMR。我們使用一個 switch 語句,根據 activity 的值來決定活動量對應的係數。然後,我們將 BMR 乘以這個係數,得到調整後的 BMR。
調整後的 BMR 就是鸚鵡每日所需的總熱量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 使用新的 BMR 計算公式
const K = 175; // 鸚鵡的 K 值
const BMR = K * Math.pow(weight / 1000, 0.75); // 體重轉換為公斤並計算基礎代謝率

// 根據活動水平調整BMR
let activityLevel = 1;
switch (activity) {
case "low":
activityLevel = 1.2;
break;
case "medium":
activityLevel = 1.4;
break;
case "high":
activityLevel = 1.6;
break;
}
const adjustedBMR = BMR * activityLevel; // 調整後的BMR

// 計算每日所需熱量
let dailyCalories = adjustedBMR;

第四步:計算每日所需營養素

有了每日所需熱量後,我們可以計算鸚鵡每日所需的三大營養素(蛋白質、脂肪、碳水化合物)的量。我們假設蛋白質和脂肪各占總熱量的 20%,碳水化合物占 60%。
然後,我們將每日所需熱量乘以這些百分比,再除以每克營養素的熱量(蛋白質和碳水化合物每克 4 卡,脂肪每克 9 卡),就得到了每日所需的三大營養素的克數。

1
2
3
4
5
6
7
8
9
// 營養素需求計算
const proteinNeed = dailyCalories * 0.2; // 蛋白質需求占總熱量的20%
const fatNeed = dailyCalories * 0.2; // 脂肪需求占總熱量的20%
const carbsNeed = dailyCalories * 0.6; // 碳水化合物需求占總熱量的60%

// 營養素需求計算 (每日所需熱量的20%為蛋白質需求,20%為脂肪需求,60%為碳水化合物需求)
const dailyProteinNeed = (dailyCalories * 0.2) / 4; // 蛋白質每克4卡
const dailyFatNeed = (dailyCalories * 0.2) / 9; // 脂肪每克9卡
const dailyCarbsNeed = (dailyCalories * 0.6) / 4; // 碳水化合物每克4卡

第五步:計算鮮食可提供的營養素和熱量

接下來,我們要計算選擇的鮮食可以提供多少營養素和熱量。我們從鮮食的 Model 中取出食物的熱量、蛋白質、脂肪、碳水化合物含量,以及最大攝取量。
然後,我們計算在最大攝取量下,這個食物可以提供多少熱量。同時,我們也計算在不超過每日所需熱量的情況下,這個食物最多可以攝取多少克。
有了食物的最大攝取量後,我們可以計算這個食物可以提供多少克的三大營養素。我們將最大攝取量乘以食物的營養素含量百分比,就得到了每種營養素的攝取量。
然後,我們計算這個食物提供的總熱量。我們將每種營養素的攝取量乘以其熱量,然後相加,就得到了總熱量。
接著,我們計算這個食物提供的熱量與每日所需熱量的差異。這個差異可以讓使用者知道,如果只吃這一種食物,是否能滿足每日所需的熱量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 計算該食物可攝取量
const foodCalories = freshFood.calories;
const foodProtein = freshFood.protein;
const foodFat = freshFood.fat;
const foodCarbs = freshFood.carbs;
const foodMaxIntake = freshFood.maxIntake;

// 計算該食物每日可攝取量
let maxCaloriesFromFood = foodCalories * (foodMaxIntake / 100); // 每日最大攝取量下的熱量
let maxFoodIntake = Math.min(
foodMaxIntake,
(dailyCalories / foodCalories) * 100
); // 不超過每日所需熱量的最大攝取量(g)

// 計算食物提供的營養素量
let proteinIntake = (maxFoodIntake * (foodProtein / 100)).toFixed(2);
let fatIntake = (maxFoodIntake * (foodFat / 100)).toFixed(2);
let carbsIntake = (maxFoodIntake * (foodCarbs / 100)).toFixed(2);

// 計算食物提供的總熱量
let foodProvidedCalories = (
proteinIntake * 4 +
fatIntake * 9 +
carbsIntake * 4
).toFixed(2);

// 計算與每日所需熱量的差異
let caloriesDifference = (dailyCalories - foodProvidedCalories).toFixed(2);

第六步:返回計算結果

最後,我們將所有計算結果整理到一個物件中,並使用 handleSuccess 函數返回給客戶端。這個物件包含了使用者輸入的體重和活動量、選擇的食物、計算出的 BMR 和調整後的 BMR、每日所需熱量和三大營養素量、食物的最大攝取量和實際攝取量、食物提供的營養素量和總熱量、熱量差異,以及每種營養素提供的詳細熱量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 返回結果
let data = {
weight,
activity,
food: freshFood,
BMR: BMR.toFixed(2), // 基礎代謝率 (BMR)
adjustedBMR: adjustedBMR.toFixed(2), // 調整後的 BMR
dailyCalories: dailyCalories.toFixed(2), // 每日所需熱量
dailyProteinNeed: dailyProteinNeed.toFixed(2), // 每日所需蛋白質
dailyFatNeed: dailyFatNeed.toFixed(2), // 每日所需脂肪
dailyCarbsNeed: dailyCarbsNeed.toFixed(2), // 每日所需碳水化合物
maxIntake: foodMaxIntake.toFixed(2), // 最大攝取量
foodIntake: maxFoodIntake.toFixed(2), // 實際攝取量
nutrientsProvided: {
protein: proteinIntake, // 每日食物中的蛋白質克數
fat: fatIntake, // 每日食物中的脂肪克數
carbs: carbsIntake, // 每日食物中的碳水化合物克數
},
foodProvidedCalories: foodProvidedCalories, // 食物提供的總熱量
caloriesDifference: caloriesDifference, // 熱量差異
detailedNutrientsCalories: {
protein: proteinCalories, // 蛋白質提供的熱量
fat: fatCalories, // 脂肪提供的熱量
carbs: carbsCalories, // 碳水化合物提供的熱量
},
};

handleSuccess(res, data, "計算鮮食可攝取量成功");

在這個物件中,我們包含了許多有用的資訊:

  • weight:使用者輸入的體重
  • activity:使用者選擇的活動量
  • food:使用者選擇的鮮食
  • BMR:計算出的基礎代謝率
  • adjustedBMR:根據活動量調整後的 BMR
  • dailyCalories:每日所需總熱量
  • dailyProteinNeeddailyFatNeeddailyCarbsNeed:每日所需的三大營養素量
  • maxIntake:鮮食的最大攝取量
  • foodIntake:在不超過每日所需熱量的情況下,鮮食的實際攝取量
  • nutrientsProvided:鮮食提供的三大營養素克數
  • foodProvidedCalories:鮮食提供的總熱量
  • caloriesDifference:鮮食提供的熱量與每日所需熱量的差異
  • detailedNutrientsCalories:每種營養素提供的詳細熱量
    最後,我們使用 handleSuccess 函數將這個物件返回給客戶端,並附上一個成功的訊息:”計算鮮食可攝取量成功”。
    這樣,我們就完成了整個 API 的實作。讓我們用 Postman 來測試一下吧!

Postman 測試

專案記得要 RUN 起來歐!

  1. 打開 Postman ,創建一個新的請求。
    https://ithelp.ithome.com.tw/upload/images/20240929/20159686z32JCzXM0L.png

  2. 將請求方法設為 POST,請求的 URL 設為 http://127.0.0.1:3000/foods/calculatefood

  3. 點擊 Body 標籤,選擇 raw 格式,然後在文本框中輸入以下 JSON:

    1
    2
    3
    4
    5
    {
    "weight": 500,
    "activity": "medium",
    "foodId": "66c8c33f41748cc91ee1f348"
    }

    這個 JSON 中,我們設置了一隻體重為 500 克、活動量為中等的鸚鵡,並選擇了一種鮮食(foodId 需要替換為你資料庫中存在的鮮食 ID)。

  4. 點擊 Send 按鈕,發送請求。如果一切正常,你應該會收到類似下面這樣的回應:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    {
    "statusCode": 200,
    "status": "success",
    "message": "計算鮮食可攝取量成功",
    "data": {
    "weight": "500",
    "activity": "medium",
    "food": {
    "_id": "66c8c33f41748cc91ee1f348",
    "name": "向日葵籽",
    "calories": 584,
    "protein": 20.78,
    "fat": 51.46,
    "carbs": 20,
    "maxIntake": 10,
    "note": "生食",
    "nutrition": "向日葵籽富含健康脂肪、蛋白質及纖維,有助於心血管健康和提供長時間的能量。"
    },
    "BMR": "104.06",
    "adjustedBMR": "145.68",
    "dailyCalories": "145.68",
    "dailyProteinNeed": "7.28",
    "dailyFatNeed": "3.24",
    "dailyCarbsNeed": "21.85",
    "maxIntake": "10.00",
    "foodIntake": "10.00",
    "nutrientsProvided": {
    "protein": "2.08",
    "fat": "5.15",
    "carbs": "2.00"
    },
    "foodProvidedCalories": "62.67",
    "caloriesDifference": "83.01",
    "detailedNutrientsCalories": {
    "protein": "8.32",
    "fat": "46.35",
    "carbs": "8.00"
    }
    }
    }

    在回應中,你可以看到我們計算出的各種數據,包括鸚鵡的 BMR、每日所需熱量和營養素、選擇的鮮食的各種資訊、鮮食可提供的營養素和熱量等。
    如果你收到了類似的回應,那麼恭喜你,你的 API 已經可以正常工作了!

完整程式碼

為了方便你查看和學習,這裡我再次提供完整的程式碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
const express = require("express");
const router = express.Router();
const FreshFood = require("../models/freshFood");
const handleSuccess = require("../utils/handleSuccess");
const handleErrorAsync = require("../utils/handleErrorAsync");
const appError = require("../utils/appError");

// * 每日鮮食計算
router.post(
"/calculatefood",
/* #swagger.tags = ['Food']
#swagger.summary = '每日鮮食計算'
#swagger.description = '每日鮮食計算'
#swagger.parameters['body'] = {
in: 'body',
required: true,
schema:{
$memberId:'會員 ID',
$weight:'體重',
$activity: '活動量',
$foodId:'食物 ID',
}
}
*/
handleErrorAsync(async (req, res, next) => {
// 取得參數
const { weight, activity, foodId } = req.body;

// 驗證必填欄位
if (!weight || !activity || !foodId) {
return next(appError(400, "weight, activity, foodId 為必填"));
}

// 驗證食物是否存在
const freshFood = await FreshFood.findById(foodId);
if (!freshFood) {
return next(appError(400, "食物不存在"));
}

// 使用新的 BMR 計算公式
const K = 175; // 鸚鵡的 K 值
const BMR = K * Math.pow(weight / 1000, 0.75); // 體重轉換為公斤並計算基礎代謝率

// 根據活動水平調整BMR
let activityLevel = 1;
switch (activity) {
case "low":
activityLevel = 1.2;
break;
case "medium":
activityLevel = 1.4;
break;
case "high":
activityLevel = 1.6;
break;
}
const adjustedBMR = BMR * activityLevel; // 調整後的BMR

// 計算每日所需熱量
let dailyCalories = adjustedBMR;

// 營養素需求計算
const proteinNeed = dailyCalories * 0.2; // 蛋白質需求占總熱量的20%
const fatNeed = dailyCalories * 0.2; // 脂肪需求占總熱量的20%
const carbsNeed = dailyCalories * 0.6; // 碳水化合物需求占總熱量的60%

// 營養素需求計算 (每日所需熱量的20%為蛋白質需求,20%為脂肪需求,60%為碳水化合物需求)
const dailyProteinNeed = (dailyCalories * 0.2) / 4; // 蛋白質每克4卡
const dailyFatNeed = (dailyCalories * 0.2) / 9; // 脂肪每克9卡
const dailyCarbsNeed = (dailyCalories * 0.6) / 4; // 碳水化合物每克4卡

// 計算該食物可攝取量
const foodCalories = freshFood.calories;
const foodProtein = freshFood.protein;
const foodFat = freshFood.fat;
const foodCarbs = freshFood.carbs;
const foodMaxIntake = freshFood.maxIntake;

// 計算該食物每日可攝取量
let maxCaloriesFromFood = foodCalories * (foodMaxIntake / 100); // 每日最大攝取量下的熱量
let maxFoodIntake = Math.min(
foodMaxIntake,
(dailyCalories / foodCalories) * 100
); // 不超過每日所需熱量的最大攝取量(g)

// 計算食物提供的營養素量
let proteinIntake = (maxFoodIntake * (foodProtein / 100)).toFixed(2);
let fatIntake = (maxFoodIntake * (foodFat / 100)).toFixed(2);
let carbsIntake = (maxFoodIntake * (foodCarbs / 100)).toFixed(2);

// 計算食物提供的總熱量
let foodProvidedCalories = (
proteinIntake * 4 +
fatIntake * 9 +
carbsIntake * 4
).toFixed(2);

// 計算與每日所需熱量的差異
let caloriesDifference = (dailyCalories - foodProvidedCalories).toFixed(2);

// 計算食物提供的營養素熱量
let proteinCalories = (proteinIntake * 4).toFixed(2); // 蛋白質提供的熱量
let fatCalories = (fatIntake * 9).toFixed(2); // 脂肪提供的熱量
let carbsCalories = (carbsIntake * 4).toFixed(2); // 碳水化合物提供的熱量

// 返回結果
let data = {
weight,
activity,
food: freshFood,
BMR: BMR.toFixed(2), // 基礎代謝率 (BMR)
adjustedBMR: adjustedBMR.toFixed(2), // 調整後的 BMR
dailyCalories: dailyCalories.toFixed(2), // 每日所需熱量
dailyProteinNeed: dailyProteinNeed.toFixed(2), // 每日所需蛋白質
dailyFatNeed: dailyFatNeed.toFixed(2), // 每日所需脂肪
dailyCarbsNeed: dailyCarbsNeed.toFixed(2), // 每日所需碳水化合物
maxIntake: foodMaxIntake.toFixed(2), // 最大攝取量
foodIntake: maxFoodIntake.toFixed(2), // 實際攝取量
nutrientsProvided: {
protein: proteinIntake, // 每日食物中的蛋白質克數
fat: fatIntake, // 每日食物中的脂肪克數
carbs: carbsIntake, // 每日食物中的碳水化合物克數
},
foodProvidedCalories: foodProvidedCalories, // 食物提供的總熱量
caloriesDifference: caloriesDifference, // 熱量差異
detailedNutrientsCalories: {
protein: proteinCalories, // 蛋白質提供的熱量
fat: fatCalories, // 脂肪提供的熱量
carbs: carbsCalories, // 碳水化合物提供的熱量
},
};

handleSuccess(res, data, "計算鮮食可攝取量成功");
})
);

module.exports = router;
## 總結

終於把這個計算鸚鵡每日應攝取量的 API 完成了!雖然這個功能看起來很簡單,但實際實現起來還真不是一件容易的事情。
一開始,我對如何計算鸚鵡的每日所需營養完全沒有概念。為了找到最合適的計算方法,我查了不少資料,也試了好幾種不同的公式。過程中難免遇到一些挫折,但我知道,只要持續不斷地嘗試,就有機會找到最佳解決方案。
最後,我選擇了一個包含 K 值的公式。這個 K 值是最近才被研究出來的,它考慮了鸚鵡的特殊生理特性。能夠將最新的研究成果應用到自己的專案中,我還挺自豪的。
有了計算公式,接下來的任務就是把它轉化為 API。這一部分相對來說比較常規,畢竟之前的文章中我們已經學過如何定義路由,如何進行參數驗證,如何使用 Swagger 等等。不過,透過這個實際的案例,我對這些知識有了更深入的理解和運用。
現在,這個 API 已經可以正常運作了。它可以根據鸚鵡的體重和活動量,計算出每日所需的熱量和各種營養素,並根據選擇的食物,計算出適合的攝取量。雖然它可能還有一些地方可以優化,但作為一個初始版本,我已經很滿意了。

明天我們把專案部署到 Render 上吧,就不只是在本機跑,而是可以讓別人直接使用你的 API 囉!
大家有沒有想要更詳細補充的部分呢?歡迎在下方留言分享喔!我們一起學習、一起成長。明天見啦,掰掰~




希望這篇文章有幫助到你,謝謝你的觀看,若有想看的系列也歡迎告訴我 😉