Files

304 lines
9.9 KiB
Python

import json
import os
from collections import defaultdict
class NutritionService:
def __init__(self, food_db_path, config_path):
with open(food_db_path, "r", encoding="utf-8") as f:
self.food_db = json.load(f)
with open(config_path, "r", encoding="utf-8") as f:
self.config = json.load(f)
base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
eer_path = os.path.join(base_dir, "config", "female_energy_eer.json")
with open(eer_path, "r", encoding="utf-8") as f:
self.female_eer = json.load(f)
def build_report_data(self, front_json):
user = front_json.get("user", {})
meals = front_json.get("meals", [])
auto_evaluation = front_json.get("autoEvaluation", True)
merged_meals = self._merge_meals(meals)
food_details, nutrition_total = self._calculate_nutrition(merged_meals, user)
category_result = self._build_category_result_from_foods(food_details)
suggestions = self._generate_suggestions(category_result)
interpretation = None
if not auto_evaluation:
manual = front_json.get("manualEvaluations") or {}
category_result, suggestions, nutrition_total, interpretation = (
self._apply_manual_evaluations(
category_result, suggestions, nutrition_total, manual
)
)
report = {
"user": user,
"date": front_json.get("date"),
"foods": food_details,
"category_result": category_result,
"suggestions": suggestions,
"nutrition": nutrition_total,
"autoEvaluation": auto_evaluation,
}
if interpretation is not None:
report["interpretation"] = interpretation
return report
def build_intake_preview(self, front_json):
"""复用营养计算逻辑,返回各类摄入量与营养素合计(供手动评价参考)"""
user = front_json.get("user", {})
meals = front_json.get("meals", [])
merged_meals = self._merge_meals(meals)
food_details, nutrition_total = self._calculate_nutrition(merged_meals, user)
category_result = self._build_category_result_from_foods(food_details)
return {
"category_result": category_result,
"nutrition": nutrition_total,
"foods": food_details,
}
def _apply_manual_evaluations(self, category_result, suggestions, nutrition_total, manual):
category_evals = manual.get("categoryEvaluations") or {}
for cat, info in category_result.items():
if cat in category_evals and str(category_evals[cat]).strip():
info["evaluation"] = str(category_evals[cat]).strip()
overall = str(manual.get("overallSuggestions", "")).strip()
suggestions = [
line.strip()
for line in overall.splitlines()
if line.strip()
]
energy_eval = str(manual.get("energyEvaluation", "")).strip()
if energy_eval:
nutrition_total["energy"]["evaluation"] = energy_eval
calcium_eval = str(manual.get("calciumEvaluation", "")).strip()
if calcium_eval:
nutrition_total["calcium"]["evaluation"] = calcium_eval
interpretation = str(manual.get("interpretation", "")).strip()
return category_result, suggestions, nutrition_total, interpretation
def _merge_meals(self, meals):
meal_sum = defaultdict(float)
for item in meals:
name = str(item.get("name", "")).strip()
amount = item.get("amount", 0)
if not name:
continue
try:
amount = float(amount)
except Exception:
amount = 0
meal_sum[name] += amount
return meal_sum
def _calculate_nutrition(self, merged_meals, user=None):
details = []
total_energy = 0.0
total_protein = 0.0
total_fat = 0.0
total_carb = 0.0
total_calcium = 0.0
for food_name, grams in merged_meals.items():
food_info = self.food_db.get(food_name)
if not food_info:
continue
factor = grams / 100.0
energy = self._safe_float(food_info.get("energy")) * factor
protein = self._safe_float(food_info.get("protein")) * factor
fat = self._safe_float(food_info.get("fat")) * factor
carbohydrate = self._safe_float(food_info.get("carbohydrate")) * factor
calcium = self._safe_float(food_info.get("calcium")) * factor
total_energy += energy
total_protein += protein
total_fat += fat
total_carb += carbohydrate
total_calcium += calcium
details.append({
"name": food_name,
"amount": round(grams, 1),
"category": food_info.get("category", ""),
"nutrition": {
"energy": round(energy, 2),
"protein": round(protein, 2),
"fat": round(fat, 2),
"carbohydrate": round(carbohydrate, 2),
"calcium": round(calcium, 2)
}
})
energy_standard = self._lookup_energy_eer(user or {})
if energy_standard is not None:
energy_eval = self._eval_energy_against_standard(total_energy, energy_standard)
else:
energy_standard = None
energy_eval = self._eval_nutrition(total_energy, "energy")
nutrition_total = {
"energy": {
"value": round(total_energy, 2),
"standard": energy_standard,
"evaluation": energy_eval
},
"protein": round(total_protein, 2),
"fat": round(total_fat, 2),
"carbohydrate": round(total_carb, 2),
"calcium": {
"value": round(total_calcium, 2),
"evaluation": self._eval_nutrition(total_calcium, "calcium")
}
}
return details, nutrition_total
def _lookup_energy_eer(self, user):
gender = str(user.get("gender", "")).strip()
if gender != "female":
return None
age = self._safe_float(user.get("age"), default=-1)
weight = self._safe_float(user.get("weight"))
activity = str(user.get("activityLevel", "")).strip()
if age < 0 or not activity:
return None
age_group = None
for group in self.female_eer.get("age_groups", []):
if group["age_min"] <= age < group["age_max"]:
age_group = group
break
if not age_group:
return None
level_data = age_group.get("levels", {}).get(activity)
if not level_data:
return None
if level_data.get("type") == "per_kg":
if weight <= 0:
return None
return round(level_data["kcal"] * weight, 1)
standard = level_data["kcal"]
stage = str(user.get("physiologicalStage", "")).strip()
stages = self.female_eer.get("physiological_stages", {})
if stage and stage in stages:
standard += stages[stage].get("bonus_kcal", 0)
return standard
def _eval_energy_against_standard(self, value, standard):
rules = self.female_eer.get("evaluation_rules", {})
under_ratio = rules.get("under_ratio", 0.9)
over_ratio = rules.get("over_ratio", 1.1)
try:
value = float(value)
standard = float(standard)
except Exception:
return "-"
if standard <= 0:
return "-"
ratio = value / standard
if ratio < under_ratio:
return rules.get("under_label", "不足")
if ratio <= over_ratio:
return rules.get("adequate_label", "适宜")
return rules.get("over_label", "偏高")
def _build_category_result_from_foods(self, food_details):
# 1. 先把配置里的所有分类都初始化为 0
category_sum = {
category: 0.0
for category in self.config["food_category_map"].keys()
}
# 2. 把实际食物摄入量累加进去
for item in food_details:
category = item.get("category", "").strip()
amount = self._safe_float(item.get("amount", 0))
if category in category_sum:
category_sum[category] += amount
# 3. 所有分类都生成评价结果
result = {}
for cat, val in category_sum.items():
result[cat] = {
"value": round(val, 1),
"evaluation": self._evaluate_category(cat, val)
}
return result
def _evaluate_category(self, category, value):
rules = self.config["category_evaluation"].get(
category,
self.config["category_evaluation"].get("default", [])
)
for r in rules:
if r["min"] <= value <= r["max"]:
return r["label"]
return "-"
def _generate_suggestions(self, category_result):
rules = self.config["suggestion_rules"]
res = []
for cat, info in category_result.items():
label = info["evaluation"]
if label in rules and rules[label]:
res.append(rules[label].replace("{category}", cat))
return res
def _eval_nutrition(self, value, key):
rules = self.config["nutrition_eval"][key]
try:
value = float(value)
except Exception:
return "-"
for r in rules:
if r["min"] <= value <= r["max"]:
return r["label"]
return "-"
def _safe_float(self, value, default=0.0):
try:
return float(value)
except Exception:
return default