☰
基于Python+Django的饮食健康推荐系统:从数据库到推荐算法实战
2026/10/2 3:23:19 网站建设 项目流程

做这个基于Python+Django的饮食健康推荐系统,起因其实很朴素:疫情之后我明显感觉身边不少朋友不是吃太多就是吃太少,外卖和深夜食堂把大家的作息和营养都打乱了。光靠手机里的卡路里计算App,记录是能记录,但没人告诉你明天该吃什么、怎么搭配。于是我用Django做了这么一套系统,让用户输入自己的身高、体重、活动量,系统结合营养学标准和食材数据库,自动推荐一日三餐的搭配方案。

这个项目的核心价值在于打通了“数据收集—健康评估—智能推荐—效果跟踪”的完整闭环。对于正在学Python和Django的开发者来说,它也是一个特别合适的练手项目:既有常规的CRUD和用户认证,又有推荐算法和数据分析,还涉及部署上线,几乎把Web开发的主要场景都覆盖了。本文我会从数据库设计讲到底层推荐逻辑,再聊到线上部署的坑,全程附带可以直接抄作业的代码。

1. 项目要做什么,为什么选Django

1.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的五个app:

  • users:用户注册、登录、健康档案管理。直接用Django自带User扩展出一个Profile。
  • ingredients:食材管理,包含食材的营养成分字段。这块是数据库的数据基础。
  • dishes:菜品管理,菜品由多种食材组成,支持多对多关联,自动计算营养总量。
  • records:饮食记录,用户每天吃了哪些菜品,吃了多少份。
  • recommender:推荐引擎,负责计算BMR、收集偏好、生成推荐方案。

模块之间单向依赖:records依赖dishes,dishes依赖ingredients,recommender依赖前面所有模块。这种分层方式的好处是职责清晰,单测也好写,不会出现互相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_delete=models.CASCADE, related_name='profile') gender = models.CharField(max_length=10, choices=[('male', '男'), ('female', '女')]) age = models.IntegerField(default=25) height = models.FloatField(help_text='单位:厘米') weight = models.FloatField(help_text='单位:千克') activity_level = models.IntegerField( choices=[(1, '久坐'), (2, '轻度运动'), (3, '中度运动'), (4, '高强度运动'), (5, '极高强度')], default=2 ) created_at = models.DateTimeField(auto_now_add=True) def bmi(self): height_m = self.height / 100 return round(self.weight / (height_m ** 2), 1) # 食材表 class Ingredient(models.Model): name = models.CharField(max_length=50, unique=True) 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', default=0) category = models.CharField(max_length=20, choices=[ ('meat', '肉蛋类'), ('vegetable', '蔬菜类'), ('staple', '主食类'), ('fruit', '水果类'), ('dairy', '奶制品'), ('bean', '豆类') ]) # 菜品表(多对多关联食材) class Dish(models.Model): name = models.CharField(max_length=100) ingredients = models.ManyToManyField(Ingredient, through='DishIngredient', related_name='dishes') cuisine_type = models.CharField(max_length=20, blank=True, default='') cooking_method = models.CharField(max_length=20, blank=True, default='') # 菜品和食材的中间表:记录每份菜品用多少克食材 class DishIngredient(models.Model): dish = models.ForeignKey(Dish, on_delete=models.CASCADE) ingredient = models.ForeignKey(Ingredient, on_delete=models.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(key=lambda x: x[1], reverse=True) 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_n=5): 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(), key=lambda x: x[1], reverse=True) return [dish_id for dish_id, _ in sorted_dishes[:top_n]]

如果你不想依赖sklearn,可以自己手写余弦相似度。我之前在CSDN看到不少版本,逻辑都是numerator = A·B,denominator = ||A||x||B||,两个矩阵都转成numpy数组就行。手写版本可控性更强,也方便答辩时解释原理,我建议新手自己写一版:

def cosine_similarity_matrix(vectors): norm = np.linalg.norm(vectors, axis=1, keepdims=True) normalized = vectors / norm return np.dot(normalized, normalized.T)

推荐引擎的完整流程是:先从用户饮食记录里统计出高频菜品作为“偏好信号”,然后规则筛选候选集,最后在候选集内做ItemCF排序,生成当日推荐:

def generate_daily_recommendation(user_id): profile = UserProfile.objects.get(user_id=user_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_n=9) # 9道菜,按早中晚分组 return final

3.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图表 </script>

json_script是Django 2.1之后提供的安全模板过滤器,会把JSON对象放到一个script标签里,避免手动拼接字符串导致的XSS风险。这个细节很多人不注意,但实际项目上线前做安全扫描时会被提出来。用这个方案,前后端都不用分离,数据渲染安全又高效。

3.5 饮食记录功能与营养统计

用户记录三餐时,选择菜品和份数。我建了一个MealRecord模型:

class MealRecord(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='records') dish = models.ForeignKey(Dish, on_delete=models.CASCADE) meal_type = models.CharField(max_length=10, choices=[ ('breakfast', '早餐'), ('lunch', '午餐'), ('dinner', '晚餐'), ('snack', '加餐') ]) date = models.DateField(auto_now_add=True) servings = models.FloatField(default=1, help_text='份数')

统计每日摄入营养时,要注意Django的聚合查询效率。直接循环遍历user.records.all()再调用get_dish_nutrition(),会有N+1查询问题。我建议用select_related和prefetch_related优化:

records = MealRecord.objects.filter( user=user, date=today ).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和migrate
CSRF verification failed表单缺少CSRF令牌模板form里加{% csrf_token %}
静态文件404(CSS/JS不显示)DEBUG=False时未收集静态文件python manage.py collectstatic,配置STATIC_ROOT
django.db.utils.IntegrityError外键关联数据被删除或重复创建检查null和on_delete设置
中文乱码数据库字符集不是utf8PostgreSQL确保UTF8编码,模板加<meta charset="utf-8">
OperationalError: database is lockedSQLite并发写锁开发期减少长事务,生产期立即换PostgreSQL
时区错乱TIME_ZONE没配对settings.py设置TIME_ZONE='Asia/Shanghai'和USE_TZ=True

4.3 性能与算法优化经验

项目上线跑了一周后,我发现两个需要优化的点:

第一个是推荐算法的冷启动问题。新用户没有任何历史饮食记录,ItemCF算出来全是0分,推荐结果无法按偏好排序。我加了一个兜底策略:冷启动用户直接按规则引擎的分数排序,只有用户产生了至少3条饮食记录后,才启用协同过滤。这个阈值设为3,是因为实测3条记录能大致勾勒出用户的口味偏好。

第二个是菜品营养素计算的缓存。菜品和食材的关联不会频繁变化,但每个用户推荐一次就重新计算一遍所有菜品的营养值,浪费CPU。我用Django的cache框架加了一层缓存:

from django.core.cache import cache def get_dish_nutrition_cached(dish): key = f'dish_nutrition_{dish.id}' result = cache.get(key) if result is None: result = get_dish_nutrition(dish) cache.set(key, result, timeout=60*60*24) # 缓存24小时 return result

实测优化后,推荐接口响应时间从1.2秒降到了400毫秒左右,提升明显。

如果你还想往下扩展,有几个方向很值得做:加一个记录食材保质期和囤货管理模块,把推荐和冰箱库存打通,推荐结果会更有实用价值;加一个用Apriori算法做食材搭配分析的功能,能分析出“买鸡胸肉的人通常还会买西兰花”,这属于关联规则挖掘,正好也是推荐系统的经典话题;算法层面可以用矩阵分解(SVD)替代ItemCF。我用surprise库跑过一个离线实验,SVD在评分预测的RMSE上比ItemCF低了约15%,虽然用户量级小的时候差异没那么明显,但这是工业界更主流的方向,适合作为论文或毕设的亮点来写。

最后再分享一个小技巧:做这类项目不要一上来就埋头写代码。我推荐先把数据库ER图画出来,把所有字段列清楚,再用两天时间把Django模型搭好,之后写视图和模板会非常顺。我一开始就是没画ER图直接开写,结果中间表改了三次,整个项目的时间有三分之一耗在改数据库上了。数据模型是Web应用的底盘,底盘稳了,上面的逻辑才有机会跑起来。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询