尧图精选

基于Python+Django的饮食健康推荐系统:从数据库到推荐算法实战

🕒 发布时间:2026/10/2 3:23:23 📁 来源:尧图网络
做这个基于PythonDjango的饮食健康推荐系统起因其实很朴素疫情之后我明显感觉身边不少朋友不是吃太多就是吃太少外卖和深夜食堂把大家的作息和营养都打乱了。光靠手机里的卡路里计算App记录是能记录但没人告诉你明天该吃什么、怎么搭配。于是我用Django做了这么一套系统让用户输入自己的身高、体重、活动量系统结合营养学标准和食材数据库自动推荐一日三餐的搭配方案。这个项目的核心价值在于打通了“数据收集—健康评估—智能推荐—效果跟踪”的完整闭环。对于正在学Python和Django的开发者来说它也是一个特别合适的练手项目既有常规的CRUD和用户认证又有推荐算法和数据分析还涉及部署上线几乎把Web开发的主要场景都覆盖了。本文我会从数据库设计讲到底层推荐逻辑再聊到线上部署的坑全程附带可以直接抄作业的代码。1. 项目要做什么为什么选Django1.1 核心需求拆解做推荐系统之前先得搞清楚推荐什么、依据什么推荐。饮食健康推荐和电商推荐有本质区别买错的商品顶多退货吃错的东西直接影响身体状态。所以这套系统不能只考虑“用户喜欢吃什么”还要考虑“用户应该吃什么”。我梳理出四类核心需求用户健康档案记录身高、体重、年龄、性别、运动频率计算BMI和每日热量消耗。食材与菜品数据管理维护食材的营养成分表热量、蛋白质、脂肪、碳水、膳食纤维等菜品由食材组成营养成分自动聚合。个性化推荐引擎根据用户的健康数据和历史饮食记录推荐合适的菜品组合按早中晚三餐输出。饮食记录与数据可视化用户记录每日摄入系统展示营养摄入是否达标热量是否超标并用图表呈现趋势。这四个模块拆开看都不复杂但合在一起就需要一个MVC能力完整的后端框架来支撑。Django正好合适ORM管数据模板引擎管页面自带Admin后台管数据录入认证系统管用户登录开发效率非常高。1.2 技术选型背后的思考有人问我为啥不用Spring Boot或者Flask我的理由有两个第一Django的“全家桶”模式极其适合这种业务型项目。一个饮食推荐系统重心应该在业务逻辑和推荐算法上而不是花大量时间是搭框架、写Session管理、拼ORM。Django把这些基础设施都内置好了用rest_framework写API也顺手前后端分离时照样能用。第二Python的生态对数据处理和算法支持太友好了。推荐系统就算不用复杂的深度学习框架直接用Python写协同过滤、写相似度计算代码量都远小于Java。后面如果要把推荐算法升级成基于numpy的矩阵分解或者接入pandas做用户画像分析都是顺手的事。技术栈我最终定了这一套分层技术选型用途后端Django 4.2 SQLiteWeb框架与默认数据库开发期零配置APIDjango REST Framework提供JSON接口方便前端调用前端Django Template Bootstrap 5服务端渲染主页面兼顾SEO和开发效率图表Chart.js前端绘制摄入趋势、营养占比图数据pandas numpy推荐算法中的数据处理与相似度运算SQLite适合开发期部署时我换成了PostgreSQL后面会讲为什么要换。2. 系统设计与数据库建模2.1 功能模块怎么拆整个系统我按领域拆成五个模块对应Django的五个appusers用户注册、登录、健康档案管理。直接用Django自带User扩展出一个Profile。ingredients食材管理包含食材的营养成分字段。这块是数据库的数据基础。dishes菜品管理菜品由多种食材组成支持多对多关联自动计算营养总量。records饮食记录用户每天吃了哪些菜品吃了多少份。recommender推荐引擎负责计算BMR、收集偏好、生成推荐方案。模块之间单向依赖records依赖dishesdishes依赖ingredientsrecommender依赖前面所有模块。这种分层方式的好处是职责清晰单测也好写不会出现互相import导致循环引用的问题。2.2 核心模型设计Django的ORM建模是整个项目的地基。我踩过一次坑一开始把营养字段直接塞进菜品表结果同样一份红烧肉不同厨师做的营养差距很大最后只能把食材和菜品拆开。下面是我反复改过之后的模型代码from django.db import models from django.contrib.auth.models import User # 用户健康档案 class UserProfile(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_nameprofile) gender models.CharField(max_length10, choices[(male, 男), (female, 女)]) age models.IntegerField(default25) height models.FloatField(help_text单位厘米) weight models.FloatField(help_text单位千克) activity_level models.IntegerField( choices[(1, 久坐), (2, 轻度运动), (3, 中度运动), (4, 高强度运动), (5, 极高强度)], default2 ) created_at models.DateTimeField(auto_now_addTrue) def bmi(self): height_m self.height / 100 return round(self.weight / (height_m ** 2), 1) # 食材表 class Ingredient(models.Model): name models.CharField(max_length50, uniqueTrue) calories models.FloatField(help_text千卡/100g) protein models.FloatField(help_text克/100g) fat models.FloatField(help_text克/100g) carbohydrate models.FloatField(help_text克/100g) fiber models.FloatField(help_text克/100g, default0) category models.CharField(max_length20, choices[ (meat, 肉蛋类), (vegetable, 蔬菜类), (staple, 主食类), (fruit, 水果类), (dairy, 奶制品), (bean, 豆类) ]) # 菜品表多对多关联食材 class Dish(models.Model): name models.CharField(max_length100) ingredients models.ManyToManyField(Ingredient, throughDishIngredient, related_namedishes) cuisine_type models.CharField(max_length20, blankTrue, default) cooking_method models.CharField(max_length20, blankTrue, default) # 菜品和食材的中间表记录每份菜品用多少克食材 class DishIngredient(models.Model): dish models.ForeignKey(Dish, on_deletemodels.CASCADE) ingredient models.ForeignKey(Ingredient, on_deletemodels.CASCADE) amount models.FloatField(help_text克) class Meta: unique_together (dish, ingredient)中间表DishIngredient是这轮设计的核心。amount字段表示做这道菜需要的食材克数这样能准确计算每份菜品的营养def get_dish_nutrition(dish): total {calories: 0, protein: 0, fat: 0, carbohydrate: 0, fiber: 0} for di in dish.dishingredient_set.select_related(ingredient): ratio di.amount / 100 for key in total: total[key] getattr(di.ingredient, key) * ratio return {k: round(v, 1) for k, v in total.items()}字段类型上我全部用了FloatField而不是DecimalField原因是推荐算法里要做浮点运算FloatField性能更好食物营养成分本身也是估算值不需要银行级精度。但这里有个坑需要注意FloatField在计算连续相乘时会积累误差所以展示给用户之前一定做round()。2.3 推荐逻辑规则引擎还是协同过滤推荐系统这一节我觉得值得单独说。饮食推荐和电影推荐不一样食物有明确的健康约束不能纯粹“猜你喜欢”。我的做法是两层推荐混合策略规则层基于营养学和用户健康指标做硬性筛选。BMI偏高的人系统自动过滤高热量重油菜品优先推荐高蛋白低脂组合BMI偏低的人推荐热量密度更高的食物。协同过滤层在规则筛选出的候选集合里再用“相似用户吃过什么”来排序保证推荐结果既健康又符合真实口味偏好。这个思路和工业界常见的“粗排精排”有点类似只是规模小不需要那么重的架构。对于课程设计、毕业设计或个人项目来说这种混合策略性价比非常高逻辑透明答辩时能讲清楚每一步为什么这么做又比纯规则引擎有技术含量。3. 核心功能实现与实操细节3.1 环境搭建与项目初始化Django项目的初始化我不多废话但有几个细节容易踩坑。首先Python版本建议3.10以上然后创建虚拟环境python -m venv venv source venv/bin/activate # Windows用 venv\Scripts\activate pip install django djangorestframework pandas numpy django-admin startproject diet_health cd diet_health python manage.py startapp users ingredients dishes records recommender这里有个新手常踩的坑忘记在settings.py的INSTALLED_APPS里注册app。你创建了app但没注册Django不会报错只是表建不出来迁移时静默跳过后面访问模型时会报no such table。所以创建完app第一件事就是注册。settings.py里还有几个关键配置INSTALLED_APPS [ django.contrib.admin, django.contrib.auth, django.contrib.contenttypes, django.contrib.sessions, django.contrib.messages, django.contrib.staticfiles, rest_framework, users.apps.UsersConfig, ingredients.apps.IngredientsConfig, dishes.apps.DishesConfig, records.apps.RecordsConfig, recommender.apps.RecommenderConfig, ] # 中文支持和时区 LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai # 登录跳转 LOGIN_URL /users/login/ LOGIN_REDIRECT_URL /3.2 用户健康档案与基础计算用户注册后系统要求完善健康档案。这部分逻辑很简单其中一个计算值得展开每日总热量消耗TDEE。我用的是哈里斯-本尼迪克特公式先算基础代谢率BMR再乘活动系数男性BMR 88.362 (13.397 x 体重kg) (4.799 x 身高cm) - (5.677 x 年龄)女性BMR 447.593 (9.247 x 体重kg) (3.098 x 身高cm) - (4.330 x 年龄)活动系数参考值久坐1.2轻度运动1.375中度运动1.55高强度运动1.725极高强度1.9。我封装成一个服务函数def calculate_tdee(profile): if profile.gender male: bmr 88.362 13.397 * profile.weight 4.799 * profile.height - 5.677 * profile.age else: bmr 447.593 9.247 * profile.weight 3.098 * profile.height - 4.330 * profile.age activity_mapping {1: 1.2, 2: 1.375, 3: 1.55, 4: 1.725, 5: 1.9} return round(bmr * activity_mapping[profile.activity_level])这个数值会作为推荐引擎的热量预算。比如某个用户TDEE是2200千卡那么推荐的早中晚三餐加起来应该在2200千卡上下浮动10%。如果连续一周推荐量大于预算用户就会莫名其妙长胖反馈不好系统流失率会很高。3.3 推荐引擎的实现推荐引擎是整个项目最核心的部分我拆成两个文件来写base.py放基础数据函数engine.py放推荐算法。先看规则引擎部分。它的任务是从所有菜品里筛出符合用户当前健康状态的候选集def rule_filter(profile, all_dishes): bmi profile.bmi() candidates [] for dish in all_dishes: nutrition get_dish_nutrition(dish) score 0 # BMI偏高24低热量菜品加分 if bmi 24 and nutrition[calories] 400: score 3 # BMI偏低18.5高热量菜品加分 if bmi 18.5 and nutrition[calories] 500: score 2 # 蛋白质优先减脂期需要充足蛋白 if profile.activity_level 3 and nutrition[protein] 15: score 2 # 高纤维蔬菜加分 if nutrition[fiber] 3: score 1 if score 0: candidates.append((dish, score)) candidates.sort(keylambda x: x[1], reverseTrue) return [dish for dish, _ in candidates[:20]]这个筛选阈值400千卡、15g蛋白质、3g纤维不是随便定的我参考了《中国居民膳食指南2022》的常见标准。你实际开发时可以根据自己食材库的情况调整原则是让健康约束可解释。协同过滤层我用基于物品的协同过滤ItemCF。逻辑很简单先根据用户的历史饮食记录找出用户偏好菜品再找这些菜品的相似菜品来推荐。核心计算是余弦相似度import numpy as np from sklearn.metrics.pairwise import cosine_similarity def build_dish_vectors(all_dishes): 把每个菜品转成营养特征向量 dish_ids [] vectors [] for dish in all_dishes: n get_dish_nutrition(dish) dish_ids.append(dish.id) # 特征向量热量/100蛋白质脂肪碳水纤维 vectors.append([ n[calories] / 100, n[protein], n[fat], n[carbohydrate], n[fiber] ]) return dish_ids, np.array(vectors) def itemcf_recommend(profile, all_dishes, user_history, top_n5): dish_ids, vectors build_dish_vectors(all_dishes) sim_matrix cosine_similarity(vectors) user_liked set(user_history) # 用户吃过的菜品ID scores {dish_id: 0 for dish_id in dish_ids} for liked in user_liked: if liked not in dish_ids: continue idx dish_ids.index(liked) for j, dish_id in enumerate(dish_ids): if dish_id in user_liked: continue # 吃过的不再推荐 scores[dish_id] sim_matrix[idx][j] sorted_dishes sorted(scores.items(), keylambda x: x[1], reverseTrue) return [dish_id for dish_id, _ in sorted_dishes[:top_n]]如果你不想依赖sklearn可以自己手写余弦相似度。我之前在CSDN看到不少版本逻辑都是numerator A·Bdenominator ||A||x||B||两个矩阵都转成numpy数组就行。手写版本可控性更强也方便答辩时解释原理我建议新手自己写一版def cosine_similarity_matrix(vectors): norm np.linalg.norm(vectors, axis1, keepdimsTrue) normalized vectors / norm return np.dot(normalized, normalized.T)推荐引擎的完整流程是先从用户饮食记录里统计出高频菜品作为“偏好信号”然后规则筛选候选集最后在候选集内做ItemCF排序生成当日推荐def generate_daily_recommendation(user_id): profile UserProfile.objects.get(user_iduser_id) all_dishes list(Dish.objects.all()) history get_user_recent_dish_ids(user_id) candidates rule_filter(profile, all_dishes) final itemcf_recommend(profile, candidates, history, top_n9) # 9道菜按早中晚分组 return final3.4 前端数据展示前端我用的Django模板Bootstrap。核心页面有首页仪表盘、每日推荐页、食材库管理页、饮食记录页、数据统计页。推荐页的核心功能是展示系统推荐的菜品卡片包含菜品名称、热量、蛋白质、脂肪、碳水和推荐理由比如“高蛋白”“低脂”“高纤维”。这个推荐理由字段很重要用户需要知道系统为什么推荐它信任度会大幅提升。数据统计页我用Chart.js画两个图近7天热量摄入曲线、三餐营养占比环形图。在Django模板里引入Chart.js要注意静态文件路径。我习惯把图表数据通过View透传到前端用json_script过滤器安全输出# view里 trend_data [{date: r.date, calories: r.total_calories} for r in weekly_records] context[trend_json] json.dumps(trend_data){{ trend_json|json_script:trend-data }} script const raw JSON.parse(document.getElementById(trend-data).textContent); // 渲染Chart.js图表 /scriptjson_script是Django 2.1之后提供的安全模板过滤器会把JSON对象放到一个script标签里避免手动拼接字符串导致的XSS风险。这个细节很多人不注意但实际项目上线前做安全扫描时会被提出来。用这个方案前后端都不用分离数据渲染安全又高效。3.5 饮食记录功能与营养统计用户记录三餐时选择菜品和份数。我建了一个MealRecord模型class MealRecord(models.Model): user models.ForeignKey(User, on_deletemodels.CASCADE, related_namerecords) dish models.ForeignKey(Dish, on_deletemodels.CASCADE) meal_type models.CharField(max_length10, choices[ (breakfast, 早餐), (lunch, 午餐), (dinner, 晚餐), (snack, 加餐) ]) date models.DateField(auto_now_addTrue) servings models.FloatField(default1, help_text份数)统计每日摄入营养时要注意Django的聚合查询效率。直接循环遍历user.records.all()再调用get_dish_nutrition()会有N1查询问题。我建议用select_related和prefetch_related优化records MealRecord.objects.filter( useruser, datetoday ).select_related(dish).prefetch_related( dish__dishingredient_set__ingredient )这样一次查询就能把该关联的都查出来避免循环中多次访问数据库。饮食记录越多的用户性能差距越明显。实测下来100条记录的情况下优化前查询次数超过500次优化后降到个位数。4. 部署上线与常见问题排查4.1 从SQLite切换到PostgreSQL开发期我用SQLite但部署到云服务器后我换成了PostgreSQL原因有两个一是SQLite在高并发读写时容易锁库二是生产环境用MySQL或PostgreSQL更常规面试/答辩时也更经得起问。切换数据库改settings.py就行DATABASES { default: { ENGINE: django.db.backends.postgresql, NAME: diet_health, USER: diet_user, PASSWORD: 你的密码, HOST: localhost, PORT: 5432, } }注意切换数据库之后必须重新执行python manage.py makemigrations和python manage.py migrate然后把本地数据重新导入。千万不要在SQLite里migrate完直接换库生产环境会报错。4.2 常见报错与解决方案速查表我在开发和部署过程中踩过不少坑挑几个高频的整理成表报错/问题原因解决方案no such table: dishes_dishapp未注册或未迁移settings.py注册app执行makemigrations和migrateCSRF verification failed表单缺少CSRF令牌模板form里加{% csrf_token %}静态文件404CSS/JS不显示DEBUGFalse时未收集静态文件python manage.py collectstatic配置STATIC_ROOTdjango.db.utils.IntegrityError外键关联数据被删除或重复创建检查null和on_delete设置中文乱码数据库字符集不是utf8PostgreSQL确保UTF8编码模板加meta charsetutf-8OperationalError: database is lockedSQLite并发写锁开发期减少长事务生产期立即换PostgreSQL时区错乱TIME_ZONE没配对settings.py设置TIME_ZONEAsia/Shanghai和USE_TZTrue4.3 性能与算法优化经验项目上线跑了一周后我发现两个需要优化的点第一个是推荐算法的冷启动问题。新用户没有任何历史饮食记录ItemCF算出来全是0分推荐结果无法按偏好排序。我加了一个兜底策略冷启动用户直接按规则引擎的分数排序只有用户产生了至少3条饮食记录后才启用协同过滤。这个阈值设为3是因为实测3条记录能大致勾勒出用户的口味偏好。第二个是菜品营养素计算的缓存。菜品和食材的关联不会频繁变化但每个用户推荐一次就重新计算一遍所有菜品的营养值浪费CPU。我用Django的cache框架加了一层缓存from django.core.cache import cache def get_dish_nutrition_cached(dish): key fdish_nutrition_{dish.id} result cache.get(key) if result is None: result get_dish_nutrition(dish) cache.set(key, result, timeout60*60*24) # 缓存24小时 return result实测优化后推荐接口响应时间从1.2秒降到了400毫秒左右提升明显。如果你还想往下扩展有几个方向很值得做加一个记录食材保质期和囤货管理模块把推荐和冰箱库存打通推荐结果会更有实用价值加一个用Apriori算法做食材搭配分析的功能能分析出“买鸡胸肉的人通常还会买西兰花”这属于关联规则挖掘正好也是推荐系统的经典话题算法层面可以用矩阵分解SVD替代ItemCF。我用surprise库跑过一个离线实验SVD在评分预测的RMSE上比ItemCF低了约15%虽然用户量级小的时候差异没那么明显但这是工业界更主流的方向适合作为论文或毕设的亮点来写。最后再分享一个小技巧做这类项目不要一上来就埋头写代码。我推荐先把数据库ER图画出来把所有字段列清楚再用两天时间把Django模型搭好之后写视图和模板会非常顺。我一开始就是没画ER图直接开写结果中间表改了三次整个项目的时间有三分之一耗在改数据库上了。数据模型是Web应用的底盘底盘稳了上面的逻辑才有机会跑起来。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →