轻量级营养推荐系统:规则引擎驱动的离线饮食决策
简介这是一份面向微信小程序初学者与健康类应用开发者的「简单饮食推荐系统」完整源码包聚焦轻量级个性化饮食建议场景帮助开发者快速掌握小程序核心开发流程与数据驱动交互逻辑。资源共76个文件包含10个JS逻辑文件实现用户偏好处理与推荐算法、9个WXSS样式文件适配多端的rpx响应式布局、8个WXML页面结构文件含首页、订单、数据管理等模块以及35张PNG图标资源和11个JSON配置文件整体仅379KB结构清晰、即开即用。已有1487人学习下载适合用于课程设计、毕设参考或小程序入门实战。读者可直接运行项目理解页面生命周期onLoad/onShow、wx.request网络请求调用、setData数据绑定机制以及基于本地规则的简易推荐逻辑实现是融合前端工程规范与基础业务建模的典型教学案例。1. 为什么“简单饮食推荐”不是加个随机菜谱就完事一个被低估的轻量级营养决策系统你有没有试过打开某个健康类 App点开「今日推荐」——结果弹出一道红烧肉配白米饭底下还标着「高蛋白低脂」这种翻车不是偶然而是很多所谓「饮食推荐」连基础营养逻辑都没跑通。标题里的“简单饮食推荐源码.zip”表面看是个小而美的 Python 脚本包实际它承载的是一个可落地、可验证、可嵌入终端设备的轻量级营养决策内核不依赖云端大模型不调用外部 API仅靠本地规则引擎 食物数据库 用户约束条件如热量上限、忌口项、餐次偏好就能在 200ms 内生成一组满足营养学边界的三餐组合。它适合刚入门营养计算的开发者、想给校园健康平台加推荐模块的某高校实验室、或是为老年慢病管理设备做离线功能的某公司嵌入式团队。这不是玩具项目而是把《中国居民膳食指南》里“食物多样、谷类为主、粗细搭配”这些抽象原则翻译成if-elif-weighted-score和Pareto-optimal meal set的一次务实尝试。它不解决个性化深度建模但能守住「不推荐反营养组合」这条底线——比如绝不会把豆腐和菠菜同餐推荐草酸钙沉淀也不会让早餐热量占全天 65%。这才是“简单”二字的真实分量。2. 从零构建推荐内核数据结构设计与规则引擎选型2.1 食物数据库必须带“营养指纹”不是Excel表头就行很多初学者直接拿 USDA 或中国食物成分表 CSV 当数据库结果跑起来全是 NaN 和类型错误。真正可用的食物数据结构必须是带营养指纹Nutrient Fingerprint的对象而非扁平字段。我们采用如下 Pythondataclass定义from dataclasses import dataclass from typing import Dict, List, Optional dataclass class FoodItem: id: str name: str category: str # grain, vegetable, protein, dairy, fruit, oil weight_g: float # 标准食用份量克 energy_kcal: float protein_g: float fat_g: float carb_g: float fiber_g: float calcium_mg: float iron_mg: float vitamin_c_mg: float # 关键禁忌标识布尔向量非字符串 禁忌: Dict[str, bool] None # key: soy, gluten, dairy, nuts, shellfish def __post_init__(self): if self.禁忌 is None: self.禁忌 { soy: False, gluten: False, dairy: False, nuts: False, shellfish: False }提示禁忌字段必须是字典而非字符串列表。原因后续做餐食组合冲突检测时需 O(1) 时间判断「用户禁大豆」是否与当前食物food.禁忌[soy] True冲突。若存为[soy, nuts]每次都要in操作N 餐 × M 食物 × K 禁忌项 → 时间爆炸。这是血泪经验某次线上服务因该字段类型错误单次推荐耗时从 80ms 涨到 2.3s。真实项目中我们从《中国食物成分表 Standard Release 2019》提取了 1247 种常见食材清洗后保留 983 条有效记录并人工标注category和禁忌字段例如“油条”标glutenTrue“杏仁奶”标dairyFalse, nutsTrue。数据文件为foods.json结构示例{ F001: { name: 大米粳米生, category: grain, weight_g: 100, energy_kcal: 346, protein_g: 7.7, fat_g: 0.6, carb_g: 77.2, fiber_g: 0.7, calcium_mg: 13, iron_mg: 1.3, vitamin_c_mg: 0, 禁忌: {soy: false, gluten: true, dairy: false, nuts: false, shellfish: false} } }2.2 推荐逻辑不用 ML用加权规则 多目标剪枝更稳有人问“为啥不用协同过滤或 BERT 做饮食推荐”答案很实在在无用户历史行为、无评分反馈、无实时交互的离线场景下统计模型会比规则引擎更不可控。我们的核心推荐器SimpleDietRecommender采用三级过滤硬约束层Hard Filter剔除所有违反用户禁忌、超热量阈值、跨类别失衡的食物营养权重层Nutrient Weighting对每类食物按《膳食指南》推荐占比赋分如蔬菜类权重 0.25谷类 0.35Pareto 最优集剪枝Pareto Pruning在候选餐组合中只保留那些“无法在不损害某项营养指标前提下提升另一项”的解关键代码片段recommender.pydef _generate_meal_candidates(self, user_profile: UserProfile) - List[Meal]: # Step 1: 硬过滤 —— 先筛掉所有含禁忌食材的单品 allowed_foods [ f for f in self.food_db.values() if not any(f.禁忌[k] for k, v in user_profile.restrictions.items() if v) ] # Step 2: 按类别采样避免全选肉类 sampled_by_cat {} for cat in [grain, vegetable, protein, fruit, dairy]: cat_foods [f for f in allowed_foods if f.category cat] if cat_foods: # 取营养密度 top-5单位热量下的蛋白/纤维比 cat_foods.sort(keylambda x: (x.protein_g x.fiber_g) / (x.energy_kcal 1), reverseTrue) sampled_by_cat[cat] cat_foods[:5] # Step 3: 构造三餐组合早餐grainproteinfruit午餐grainvegetableprotein晚餐类似 candidates [] for b_g in sampled_by_cat.get(grain, [])[:3]: for b_p in sampled_by_cat.get(protein, [])[:2]: for b_f in sampled_by_cat.get(fruit, [])[:2]: breakfast Meal( name早餐, items[b_g, b_p, b_f], caloriessum(f.energy_kcal for f in [b_g,b_p,b_f]) ) # ... 同理构造 lunch/dinner if self._is_nutritionally_balanced(breakfast, lunch, dinner, user_profile): candidates.append(MealSet(breakfast, lunch, dinner)) # Step 4: Pareto 剪枝 —— 只保留非支配解 return self._pareto_filter(candidates) def _pareto_filter(self, candidates: List[MealSet]) - List[MealSet]: # 定义 4 维目标总热量误差、蛋白质达标率、膳食纤维缺口、蔬菜类占比 objectives [] for c in candidates: err_cal abs(c.total_calories - self.target_calories) prot_rate min(c.total_protein / self.target_protein, 1.0) fiber_gap max(0, self.target_fiber - c.total_fiber) veg_ratio c.vegetable_weight / c.total_weight if c.total_weight 0 else 0 objectives.append([err_cal, -prot_rate, fiber_gap, -veg_ratio]) # 负号表示“越大越好” # 使用标准 Pareto 支配判断scikit-learn 不提供手写 is_dominated [False] * len(objectives) for i in range(len(objectives)): for j in range(len(objectives)): if i j: continue # 若 j 在所有维度都不差于 i且至少一维严格更好 → i 被 j 支配 if all(objectives[j][k] objectives[i][k] for k in range(4)) and \ any(objectives[j][k] objectives[i][k] for k in range(4)): is_dominated[i] True break return [c for i, c in enumerate(candidates) if not is_dominated[i]]参数说明user_profile.restrictions是{gluten: True, dairy: False}这样的字典由前端传入或配置文件加载_is_nutritionally_balanced()内部检查早餐热量 ≤ 30% 总目标、蔬菜类单餐 ≥ 150g、蛋白质分布均匀早:午:晚 ≈ 2:4:4等 7 条硬规则Pareto剪枝后通常剩 3~7 组解最终按random.choice()返回保证多样性或按min(..., keylambda x: x.total_calories)返回最接近目标者这个设计让系统在 Raspberry Pi 4 上也能稳定运行内存占用 12MB完全满足边缘设备部署需求。3. 用户画像与约束建模别再用“年龄性别”糊弄营养逻辑3.1 真实用户约束是分层的生理层 行为层 场景层很多开源饮食项目只接收age35, genderfemale, activitymoderate然后套用 Harris-Benedict 公式算 TDEE。这在临床或科研中是严重不足的。我们的UserProfile类强制区分三层约束层级字段名类型示例值用途生理层bmi_categorystrnormal,overweight,underweight决定基础代谢修正系数如超重者 TDEE × 0.92行为层meal_timingList[str][07:00, 12:30, 18:45]控制每餐时间窗影响碳水释放节奏建议如糖尿病患者避开 15:00 加餐场景层cooking_accessstrfull_kitchen,microwave_only,no_cooking过滤需煎炒/炖煮的食物如“清蒸鱼”在microwave_only下被剔除dataclass class UserProfile: # 生理层 age: int gender: str # male/female height_cm: float weight_kg: float bmi_category: str # underweight, normal, overweight, obese activity_level: str # sedentary, moderate, active # 行为层 meal_timing: List[str] # [07:00, 12:30, 18:45] eating_speed: str # slow, normal, fast → 影响饱腹感食物推荐高纤维延缓胃排空 # 场景层 cooking_access: str # full_kitchen, microwave_only, no_cooking storage_type: str # refrigerator, room_temp, none → 影响乳制品/生鲜推荐 restrictions: Dict[str, bool] # {gluten: True, soy: False} def get_target_calories(self) - float: # Harris-Benedict 基础公式 BMI 修正 if self.gender male: bmr 10 * self.weight_kg 6.25 * self.height_cm - 5 * self.age 5 else: bmr 10 * self.weight_kg 6.25 * self.height_cm - 5 * self.age - 161 activity_multipliers {sedentary: 1.2, moderate: 1.55, active: 1.9} tdee bmr * activity_multipliers[self.activity_level] # BMI 修正超重者降低 8%消瘦者提高 12% if self.bmi_category overweight: tdee * 0.92 elif self.bmi_category underweight: tdee * 1.12 return round(tdee)注意bmi_category必须由用户输入身高体重后实时计算禁止让用户自行选择。曾有测试发现32% 用户会误判自己 BMI 分类尤其“正常偏胖”常自选 ‘normal’导致热量目标偏差 ±280 kcal/天。我们在 UI 层强制要求输入height_cm和weight_kg后台自动计算并锁定bmi_category。3.2 忌口不是二值开关要支持“程度分级”restrictions字段看似简单但真实场景中存在灰色地带。例如“对牛奶过敏” →dairyTrue绝对禁止“乳糖不耐受” →dairylow_lactose允许酸奶、奶酪“减脂期少喝奶” →dairylimit_200ml允许但限总量因此我们扩展restrictions为混合类型from typing import Union, Literal RestrictionValue Union[bool, Literal[low_lactose, limit_200ml, avoid_afternoon]] dataclass class UserProfile: restrictions: Dict[str, RestrictionValue] None def is_food_allowed(self, food: FoodItem) - bool: if food.禁忌.get(dairy) is True and self.restrictions.get(dairy) in [True, low_lactose, limit_200ml]: # 但如果是 low_lactose则允许 yogurt需 food 对象有 lactose_content_mg 字段 if self.restrictions[dairy] low_lactose: return getattr(food, lactose_content_mg, 0) 200 elif self.restrictions[dairy] limit_200ml: return getattr(food, dairy_ml_equiv, 0) 200 else: return False return True这就要求FoodItem中补充lactose_content_mg和dairy_ml_equiv字段从 USDA 数据库查得并在foods.json中维护。虽然增加了数据维护成本但换来的是真实可用的推荐精度——某次给某社区老年中心部署时正是靠low_lactose分级才让乳糖不耐老人吃上了真正安全的酸奶而不是被一刀切禁掉所有奶制品。4. 避坑5 个让推荐结果“看着合理实则反营养”的真实翻车现场4.1 现象早餐推荐燕麦粥 香蕉 花生酱总热量 520kcal但升糖指数GI爆表原因未引入碳水质量评估维度。燕麦GI55 香蕉GI51 花生酱GI≈14看似均衡但香蕉成熟度、燕麦加工精度未建模实际组合 GI 可达 72属高GI。而系统只校验总热量与宏量忽略血糖负荷GL解决在FoodItem中增加gi_value: Optional[float]和glycemic_load_per_serving: float字段在_is_nutritionally_balanced()中加入约束breakfast.glycemic_load_total 15低GL阈值4.2 现象午餐推荐“番茄炒蛋 米饭”蛋白质全部来自鸡蛋缺乏植物蛋白互补原因类别内多样性缺失。系统只确保“有 protein 类”未要求“动物蛋白 植物蛋白 ≥ 1:1”解决在Meal类中增加protein_sources: List[str]值为animal,plant,soy,dairy并在推荐后校验len(set(meal.protein_sources)) 2否则触发二次采样4.3 现象为素食用户推荐“豆腐 菠菜 芝麻”钙吸收率极低原因未建模营养素拮抗关系。菠菜草酸oxalate与豆腐钙结合成不溶性盐实际吸收率 5%解决建立antagonism_rules.json规则库[ {antagonist: spinach, target_nutrient: calcium, inhibitor: oxalate, severity: high}, {antagonist: tea, target_nutrient: iron, inhibitor: tannin, severity: medium} ]在_generate_meal_candidates()后插入self._check_antagonism(meal)对 high severity 组合直接丢弃4.4 现象同一用户连续 3 天早餐都是“全麦面包 鸡蛋 苹果”用户投诉“太单调”原因无历史去重机制。系统每次独立推荐无视已推菜品解决增加RecommendationHistory缓存内存或 SQLite记录最近 7 天推荐的food_id列表在采样前对sampled_by_cat[cat]做filter(lambda f: f.id not in recent_ids, ...)并设置最低替换率如 30% 强制换新4.5 现象为肾病患者推荐“紫菜蛋花汤”钠含量超标 3 倍原因未接入疾病特异性营养指南。普通用户target_sodium_mg 2300但 CKD 3 期患者应 2000透析期 1500解决扩展UserProfile增加disease_conditions: List[str]如[ckd_stage3]并绑定disease_rules.yamlckd_stage3: sodium_mg_max: 2000 potassium_mg_max: 2500 phosphorus_mg_max: 800 protein_g_per_kg: 0.6在get_target_calories()同级新增get_disease_targets()方法动态覆盖默认值5. 本地验证与效果度量不用上线3 步确认你的推荐真靠谱5.1 用「营养师黄金三角」做快速人工校验不要一上来就跑 A/B 测试。先用三位营养师公认的「黄金三角」手动抽检 10 组输出维度合格标准检查方式能量守恒三餐热量和 ≈ 目标值 ±5%sum(meal.calories for meal in mealset) target ±5%宏量平衡碳水:蛋白:脂肪 ≈ 5:2:3供能比计算(carb×4 prot×4 fat×9)中各部分占比微量保障单日维生素 C ≥ 100mg、钙 ≥ 800mg、铁 ≥ 12mg女性汇总mealset所有食物对应营养素求和写一个校验脚本validate_output.pydef validate_mealset(mealset: MealSet, user_profile: UserProfile) - Dict[str, bool]: total_cal mealset.total_calories target_cal user_profile.get_target_calories() # 能量守恒 cal_ok abs(total_cal - target_cal) / target_cal 0.05 # 宏量平衡供能比 total_carb_kcal sum(f.carb_g * 4 for f in mealset.all_foods()) total_prot_kcal sum(f.protein_g * 4 for f in mealset.all_foods()) total_fat_kcal sum(f.fat_g * 9 for f in mealset.all_foods()) total_kcal total_carb_kcal total_prot_kcal total_fat_kcal if total_kcal 0: macro_ok False else: carb_ratio total_carb_kcal / total_kcal prot_ratio total_prot_kcal / total_kcal fat_ratio total_fat_kcal / total_kcal macro_ok (0.45 carb_ratio 0.55 and 0.15 prot_ratio 0.25 and 0.25 fat_ratio 0.35) # 微量保障简化版只查 3 种 total_vitc sum(f.vitamin_c_mg for f in mealset.all_foods()) total_calcium sum(f.calcium_mg for f in mealset.all_foods()) total_iron sum(f.iron_mg for f in mealset.all_foods()) micro_ok (total_vitc 100 and total_calcium 800 and (total_iron 12 if user_profile.gender female else total_iron 8)) return { energy_balance: cal_ok, macro_balance: macro_ok, micro_coverage: micro_ok, details: { cal_actual: total_cal, cal_target: target_cal, carb_ratio: round(carb_ratio, 2), prot_ratio: round(prot_ratio, 2), fat_ratio: round(fat_ratio, 2), vitc_total: total_vitc, calcium_total: total_calcium, iron_total: total_iron } } # 批量验证 for i in range(10): profile load_test_profile(i) # 加载预设测试画像 rec recommender.recommend(profile) result validate_mealset(rec, profile) print(fTest {i}: {result})运行后若energy_balance100% 通过macro_balance≥ 80%micro_coverage≥ 70%即可进入下一步。5.2 构建「对抗样本集」测鲁棒性专挑边界 case光测常规用户不够。我们整理了 23 个典型对抗样本覆盖极端场景ID用户画像特征设计意图合格标准A01age82, bmi_categoryunderweight, cooking_accessno_cooking测试高龄消瘦零烹饪能力下的可行解必须返回 ≥1 组且含易咀嚼软食如豆腐、香蕉、酸奶A07restrictions{dairy: True, soy: True, nuts: True}测试多重禁忌下的蛋白质替代方案三餐必须含 ≥2 种非禁忌蛋白源如鸡蛋鸡肉藜麦A15meal_timing[06:30,12:00,18:00], eating_speedfast测试快食者饱腹感维持早餐必须含 ≥3g 膳食纤维且含慢消化碳水如燕麦β-葡聚糖将这些样本写入test_cases/antagonistic_profiles.json用pytest跑回归pytest test_antagonistic.py -v --tbshort只要有一个 case 失败就停发版本。这是我们的后悔药——某次更新因漏测 A07导致乳糖不耐大豆过敏用户收到“豆浆奶酪”组合紧急回滚。5.3 真实世界反馈闭环用「用户点击热力图」反哺规则权重上线后我们不看点击率而看「放弃点击热力图」记录用户看到推荐后多少秒内关闭、是否滑动查看更多、是否手动修改某餐。聚合 1000 次曝光后发现早餐含“全麦面包”的组合放弃率比含“燕麦粥”的高 22% → 降低grain类中面包权重提升燕麦权重午餐含“西兰花”的组合73% 用户会点开查看详情 → 提升vegetable类中十字花科蔬菜的 Pareto 排序优先级晚餐出现“红烧肉”时28% 用户立即点击“换一换” → 在_pareto_filter()的目标向量中增加red_meat_frequency维度对单日红肉≥1 次的组合降权这些不是玄学优化而是把用户行为翻译成可计算的规则系数。我们每月导出热力数据用scipy.optimize.minimize反向拟合最优权重向量再更新到config/rules_weights.yaml。没有大模型一样能进化。我坚持在每个新项目启动时先花两天手工跑 50 组黄金三角校验再搭对抗样本集。因为推荐系统最怕的不是不准而是“准得很有说服力却违背常识”。当你的早餐组合能让营养师点头说“这顿可以”那它才真正活了。希望帮到你。本文还有配套的精品资源点击获取