资讯详情 Django+Flask搭建智慧养老饮食推荐系统:从禁忌过滤到推荐引擎实践
📅 2026/10/10 7:35:23
前阵子接手了一个智慧养老服务平台的后端改造任务其中工作量最大的不是管理后台的权限模块也不是报表统计而是“老人饮食推荐系统”这一块。项目技术栈定的是Django Flask都是Python生态里的老熟人但要把两者配合好把一个“今天吃什么”的问题做出医疗级的严谨和食堂级的实用中间挖了不少坑。这套系统说白了就三件事把老人档案管起来、把健康禁忌算进去、把一周菜谱推出来。技术本身不算新难在数据怎么建模、规则怎么设计、结果怎么让人信服。写这篇文章我想把整个设计和落地过程拆开讲清楚包括Django和Flask各自负责什么、推荐引擎怎么一步步做出来、以及实际运行中踩过的那些坑。适合准备做智慧养老方向项目的开发者也适合正在做饮食推荐但不知道从哪下手的同学参考。1. 项目定位与技术选型为什么是Django Flask1.1 需求拆解智慧养老平台的饮食推荐到底要解决什么很多第一次接触这类项目的人会想当然地把饮食推荐理解成一个“菜谱大全加个筛选功能”。真正拿到需求就会发现完全不是这么回事。一个可用的老人饮食推荐系统至少要覆盖四个层面第一层是档案层。老人不是普通用户年龄、身高体重、慢病情况、用药情况、过敏史、牙齿咀嚼能力、口味偏好全都是推荐结果的影响因子。一名糖尿病老人和一名术后恢复老人的餐单后端特征完全不一样。第二层是规则层。饮食禁忌是硬性的不能靠算法“猜”。比如痛风老人不适合高嘌呤食材高血压老人需要低钠厨师出餐前必须看到醒目的禁忌标记。这一层一旦出错就不是体验问题而是安全问题。第三层是推荐层。在满足硬性禁忌的前提下从食材库和菜谱库里组合出一日三餐、一周菜谱保证营养均衡、荤素搭配、口味多样同类型菜品不能连续出现。第四层是运营层。平台不是推完就完事了食堂要照着菜谱备餐护理员要能查看老人的饮食禁忌家属可能还要看到每周的营养报告。所以推荐结果必须可解释、可追溯。我当时接到的核心需求就一句话系统要根据老人的健康档案和饮食偏好自动生成下一周的每日菜谱并在出餐前对禁忌食材做二次拦截。这个定位直接把方案定死了禁忌过滤必须独立于推荐模块而且权限上要高于任何用户偏好。1.2 Django和Flask各干各的不是重复造轮子团队讨论技术栈的时候有人问过既然都用Python直接用Django不就行了为什么还要再上Flask这里得说清楚。Django确实强大Admin后台、ORM、认证、中间件一套全齐用来做管理端、老人档案、员工权限、数据报表非常顺手。但推荐引擎是另一回事。它需要频繁改动算法逻辑、随时测试新的评分规则如果耦合在Django的大工程里每次改个权重都要重新走一遍整个项目流程还要担心影响其他模块。Flask的优势是轻、灵活、边界清晰。把推荐服务独立出来变成一个只负责“输入老人ID输出推荐菜谱”的微服务后续算法优化、模型替换、接口压测都方便得多。两个服务之间通过HTTP接口通信数据各自落库互不干扰。打个比方Django像是养老机构的接待大厅管人管档案管流程Flask像是后厨的营养师工作站只出食谱和营养分析。两者只要把菜单递明白就行。最后拍板的架构是Django做主平台Flask做推荐引擎Redis做缓存Celery做异步任务。后端语言统一Python团队成员上手成本低后期维护也简单。1.3 整体架构与核心数据流整个系统的数据流是这样的老人信息在Django后台录入包括基础档案、体检指标、饮食习惯。录入完成后系统触发一次饮食画像计算把老人特征标准化成一份JSON画像存入Redis。每天晚上Celery定时任务扫描所有需要次日餐谱的老人调用Flask推荐服务的接口批量生成第二天的三餐和加餐建议。生成结果写回Django数据库食堂工作人员在管理端确认后发布。这个流程里有几个细节值得注意。画像计算不是每次推荐都做而是档案变更时触发否则性能扛不住。推荐结果的生成也不是实时计算而是夜间批量跑不然每天早上八点所有老人同时请求推荐服务直接打爆。我建议做同类项目的同学在设计之初就把“实时推荐”和“离线预生成”分开。老人的饮食结构变化很慢一天一次甚至一周一次的更新频率完全足够没必要追求那种“每次打开都重新算一遍”的效果。2. 核心数据模型与饮食画像构建2.1 老人档案和健康指标怎么建表推荐系统的地基是数据模型。模型设计得好后面写过滤规则和评分算法都顺设计得不好后面到处补字段、写兼容逻辑苦不堪言。我按职责把核心表拆成了四张表名职责关键字段ElderProfile老人基础档案姓名、年龄、性别、身高、体重、联系方式HealthRecord健康与体检记录慢病类型、血压、血糖、尿酸、用药情况DietTaboo饮食禁忌表禁忌类型、禁忌食材、过敏原、禁忌等级DietPreference饮食偏好表口味偏好、菜品偏好、忌口、软硬程度要求Django模型里大概是这个样子from django.db import models class ElderProfile(models.Model): name models.CharField(max_length50) age models.IntegerField() gender models.CharField(max_length2, choices[(M, 男), (F, 女)]) height models.FloatField() weight models.FloatField() tooth_status models.CharField(max_length20, default正常) class HealthRecord(models.Model): elder models.OneToOneField(ElderProfile, on_deletemodels.CASCADE) hypertension models.BooleanField(defaultFalse) diabetes models.BooleanField(defaultFalse) hyperuricemia models.BooleanField(defaultFalse) systolic models.IntegerField(nullTrue, blankTrue) diastolic models.IntegerField(nullTrue, blankTrue) fasting_glucose models.FloatField(nullTrue, blankTrue) class DietTaboo(models.Model): elder models.ForeignKey(ElderProfile, on_deletemodels.CASCADE) taboo_type models.CharField(max_length20) ingredient models.CharField(max_length50) level models.CharField(max_length10, default禁止)有个设计经验分享给你禁忌表和偏好表一定要分开建模。原因很简单两者的更新频率和权威性不同。禁忌来自医嘱或家属告知基本不能由老人自己随便改偏好则是老人自己的选择今天想吃软一点明天想换个口味都很正常。混在一张表里容易出现“护理员改偏好时不小心动到禁忌”这种低级事故。分开之后禁忌表在管理后台设置为高权限字段只有护士站账号能修改。2.2 食材、菜品的营养标签体系有了老人档案还得有菜品档案。菜品的建模决定了推荐结果的专业上限。每道菜只存名字和图片是远远不够的还要有一组标准化标签。我的做法是给食材和菜品都打上营养与健康标签。标签分两类一是营养成分类比如高蛋白、高纤维、低脂、低盐、低糖、高钾、高钙二是适老属性类比如软烂、易咀嚼、少刺、清淡、少油。这些标签不是拍脑袋定的而是根据食材营养成分表折算出来的。举个例子100克鸡胸肉蛋白质含量约23克在食材库中标记为“高蛋白”带骨鱼块刺多就会标记为“含刺”。菜品是食材的集合所以菜品标签可以由食材标签聚合而来同时允许人工修正。标签体系还有个大作用它让推荐系统具备可解释性。系统推荐“清蒸鲈鱼”时可以明确告诉你是因为它“高蛋白、低脂、少刺、适口”老人家属看了也安心。实际做的时候给现有菜谱打标签是最费人力的环节。我们一开始用人工标注一盘菜要标十几个维度效率很低。后来改成“食材自动聚合营养师复核”的半自动方式才把几百道菜的标注工作压到一周内完成。2.3 饮食画像的计算与标准化画像就是把老人的档案、体检、禁忌、偏好整合成一个结构化结果方便推荐服务统一消费。画像的JSON结构大概长这样{ elder_id: 1001, age: 76, bmi: 24.3, diseases: [糖尿病, 高血压], hard_taboos: [糖, 高GI水果, 动物内脏, 腌制食品], soft_constraints: [少盐, 低脂], preferences: { taste: 清淡, avoid_ingredients: [香菜], texture: 软烂 }, nutrition_focus: [高蛋白, 高纤维, 控糖] }画像的计算逻辑在Django里单独封装成一个模块任何一次档案变更都会触发重算。重算不只是拼字段还包含一些推导规则。比如BMI低于18.5的老人营养重点自动加上“高蛋白”收缩压超过140的老人自动加上“低钠”约束糖尿病老人的推荐主食会偏向粗粮类。这里有一点要特别提醒画像里的“hard_taboos”和“soft_constraints”是两种完全不同的约束。前者是不可碰的红线后者是尽量满足但不强制的要求。推荐引擎在处理时必须区分对待不然会出现因为“清淡偏好”而把所有肉菜都过滤掉的可笑结果。3. 饮食推荐引擎的设计与实现3.1 推荐流程四步走推荐引擎是整个系统的大脑我把它拆成了四个步骤保证每一步都可检查、可回退候选集召回从菜品库里拉出当日可售菜品作为候选池。硬性禁忌过滤删除所有包含老人禁忌食材或不符合红线要求的菜品。营养与偏好评分对剩余菜品按营养匹配度、口味匹配度、软硬程度进行打分。重排与多样性控制防止同类型菜连续出现控制重复频次选出最终组合。这个流程必须在代码里保持清晰的阶段边界不要在过滤阶段顺手做排序也不要在排序阶段又把禁忌拉回来。每个阶段结束后写日志方便排查“某道菜为什么出现在结果里”或者“某道菜为什么被过滤掉”。3.2 硬性禁忌过滤的代码实现硬过滤的核心是“一个都不能漏”。实现起来不复杂关键是规则要枚举完整。HARD_TABOO_RULES { 糖尿病: lambda ing: ing.get(gi_level, 中) ! 高 and 糖 not in ing.get(tags, []), 高血压: lambda ing: ing.get(sodium_level, 中) ! 高, 痛风: lambda ing: ing.get(purine_level, 中) ! 高 and 动物内脏 not in ing.get(tags, []), } def hard_filter(dishes, profile): result [] for dish in dishes: ok True for disease in profile[diseases]: rule HARD_TABOO_RULES.get(disease) if rule: for ing in dish[ingredients]: if not rule(ing): ok False break if not ok: break if ok: result.append(dish) return result这份代码很简单但我强烈建议在规则之上再加一层“食材黑名单”配置表让护士站在系统里可以自定义禁用食材。因为有些老人的禁忌是医嘱里单独说明的比如“对花生过敏”“术后忌羊肉”这些没办法写进通用规则只能靠配置兜底。踩过的坑是一开始把禁忌规则写在代码里护理员反馈老人吃了某道菜不舒服排查发现是刚添加的新菜含了花生碎而花生不在规则枚举里。后来改成“通用规则自定义禁忌词表”双保险问题才彻底解决。3.3 营养评分模型怎么定权重过滤完的候选菜还是很多得给它们排个优先级。我给每道菜打三个维度的分再按权重汇总营养匹配度权重0.5菜品营养标签与老人“nutrition_focus”的重合度偏好匹配度权重0.3菜品口味、质地与老人偏好是否一致多样性系数权重0.2该菜品上次出现距今的天数越久越加分def score_dish(dish, profile, days_since_last_served): nutrition_score 0 for tag in profile[nutrition_focus]: if tag in dish[nutrition_tags]: nutrition_score 1 nutrition_score min(nutrition_score / max(len(profile[nutrition_focus]), 1), 1.0) preference_score 0 if dish[taste] profile[preferences][taste]: preference_score 0.6 if dish[texture] profile[preferences][texture]: preference_score 0.4 diversity_score min(days_since_last_served / 7.0, 1.0) total 0.5 * nutrition_score 0.3 * preference_score 0.2 * diversity_score return total权重比例是根据运营反馈调出来的。一开始偏好权重太高结果食堂天天做老人爱吃的红烧肉营养师看了直摇头。后来把“营养匹配度”提到0.5压过“偏好匹配度”系统推出来的菜谱才兼顾了健康和满意度。调权重没有一劳永逸的答案推荐的做法是把权重做成配置项放Django后台的“推荐策略”设置页里运营人员每隔一段时间微调一次而不是每次改代码。3.4 协同过滤的补充与冷启动处理纯规则推荐有个明显的毛病候选池固定推荐结果千篇一律。为了给老人发现新菜品的机会我加了一层基于相似老人的协同过滤。思路不复杂先算出老人的画像向量再用余弦相似度找画像最接近的5位老人看看他们点赞率高、但当前老人没吃过的菜品作为“惊喜候选”加入召回池。def find_similar_elder(target, elder_profiles, top_k5): max_sim -1 similar [] for elder in elder_profiles: if elder.id target.id: continue sim cosine_similarity(target.vector, elder.vector) similar.append((elder, sim)) similar.sort(keylambda x: x[1], reverseTrue) return [e for e, _ in similar[:top_k]]这里有个必须注意的冷启动问题新入住的老人没有任何历史行为数据。解决办法是放弃协同过滤退回到纯规则推荐等老人打卡、点赞、反馈超过一定数量后再启用协同过滤。前期宁可保守不要让“惊喜”变成“惊吓”。4. Flask推荐服务与Django平台的对接实操4.1 搭建Flask推荐服务Flask服务我用的是应用工厂模式核心路由只有三个POST /api/recommend/daily生成一日三餐POST /api/recommend/weekly生成一周菜谱POST /api/explain返回推荐结果的解释说明接口代码很简洁from flask import Flask, request, jsonify app Flask(__name__) app.post(/api/recommend/daily) def recommend_daily(): data request.get_json() elder_id data[elder_id] profile get_profile_from_cache(elder_id) dishes load_candidate_dishes() filtered hard_filter(dishes, profile) ranked sorted(filtered, keylambda d: score_dish(d, profile, get_days_since(d[id])), reverseTrue) meal_plan build_meal_plan(ranked, profile) return jsonify({code: 0, data: meal_plan})一个容易忽略的点是接口要做幂等和参数校验。老人ID不存在、画像为空、候选菜品不足这些异常都要在Flask服务内拦截不要等Django那边报错再去倒排查。4.2 Django调用推荐服务的降级策略Django这边通过requests调用Flask远远不够实际生产环境必须有超时控制、重试机制和降级方案。合一遍代码大概是import requests from django.core.cache import cache def get_daily_recommendation(elder_id): cache_key frecommend:{elder_id}:{date.today()} cached cache.get(cache_key) if cached: return cached try: resp requests.post( RECOMMEND_SERVICE_URL /api/recommend/daily, json{elder_id: elder_id}, timeout3, ) resp.raise_for_status() result resp.json()[data] except Exception: result fallback_plan(elder_id) cache.set(cache_key, result, 60 * 60 * 6) return result降级方案是我重点做的。Flask服务万一挂了系统不能连推荐功能一起挂掉。兜底方案就一条从菜品库里按标签匹配程度取最常规的“低盐低油套餐”尽管个性化程度低但保证安全不出错。饭点前推荐服务不可用食堂至少有标准餐可以准备。4.3 用Celery做夜间批量生成实时调用只满足零星查询养老机构每天需要的周菜谱靠一条条请求不现实。用Celery做了一个定时任务每天21点扫描所有有次日供餐需求的老人批量生成第二天的三餐建议。Celery Beat配置大概是from celery import Celery from celery.schedules import crontab app Celery(tasks, brokerredis://redis:6379/0) app.conf.beat_schedule { generate-daily-meal-plan: { task: tasks.generate_daily_meals, schedule: crontab(hour21, minute0), } } app.task def generate_daily_meals(): elders get_active_elder_ids() for elder_id in elders: generate_for_elder.delay(elder_id)这里有一个细节不要在一个任务里同步处理所有老人。单线程跑几百个任务网络IO和数据库IO都扛不住。拆成异步任务队列每个老人一个task通过多worker并行消费速度提升非常明显。5. 部署实践与性能优化5.1 轻量部署方案这类平台大多部署在养老机构的内网服务器上不需要复杂的高可用方案但稳定性要求很高。项目X用的是Docker Compose一键编排包含五个容器Django应用、Flask推荐服务、Redis、Celery Worker、Celery Beat。services: django: build: ./django_app ports: - 8000:8000 depends_on: - redis - flask flask: build: ./flask_app ports: - 5000:5000 redis: image: redis:7 ports: - 6379:6379 worker: build: ./django_app command: celery -A config worker -l info depends_on: - redis beat: build: ./django_app command: celery -A config beat -l info depends_on: - redisDjango用Gunicorn跑Flask因为内部逻辑简单并发不高用内置开发服务器在正式环境其实不推荐但为了省资源直接复用Django环境的Gunicorn也没问题。Nginx反代外面一层同时承担静态文件服务。老人健康数据涉及个人隐私部署时必须给管理后台配上HTTPS数据库密码不能写死敏感字段做脱敏展示。这些合规要求一开始就加进去别等上线审计再补。5.2 推荐耗时和缓存优化第一轮实测单老人的实时推荐大约需要300到500毫秒看起来不慢但夜间批量跑几百个老人就很痛苦了。排查发现瓶颈不在Flask计算本身而在菜品库的实时加载——每次请求都把全部菜品从MySQL读一遍再逐个计算标签。优化方案分了三条路走一是把菜品基础数据和标签在应用启动时加载进内存因为菜品数据属于低频变化数据二是推荐结果落Redis缓存当天重复请求直接命中三是给数据库加索引重点优化禁忌查询的JOIN条件。优化后单次推荐降到30毫秒以内夜间批量任务也从十几分钟压缩到两分钟。其实很多性能问题不是算法复杂度造成的而是把不该实时读的数据反复读了很久。5.3 我踩过的坑清单把这个项目里最有代表性的坑列出来都是常规文档里不会写的东西。坑表现原因与处理禁忌规则只写死在代码里新增菜品含过敏原没拦住增加数据库级别的自定义禁忌词表代码规则配置规则双校验菜品标签全部人工标注上线前标注积压了三百多道菜改成食材标签聚合菜品标签人工只负责复核推荐结果连续三天出现同款菜多样性系数权重太低调大多样性权重并加一道“同菜品七天内不重复”的硬规则Flask服务重启导致推荐瞬间超时食堂开饭前服务挂了全部请求排队加超时、重试、降级套餐兜底方案保证饭点不断菜老人画像缓存过期时间设得太长血糖指标改了推荐结果还是按旧档案生成画像缓存过期时间设6小时档案变更时主动删除缓存6. 常见问题排查与调优速查表整理一份问题排查速查表运行维护阶段会频繁用到。现象可能原因排查思路推荐结果为空硬性过滤条件过严候选集被清空查看过滤日志统计每个规则过滤掉的菜品数确认是否出现“禁忌和偏好混用”结果里的菜不符合老人口味偏好画像缺失默认值设置不合理检查DietPreference表是否有数据画像JSON中preferences是否为空同一菜品反复出现多样性评分失效检查days_since_last_served是否正常返回频次硬规则是否生效周一凌晨批量任务堆积Celery Worker数量不足或数据库锁等待查看worker日志增加并发数给生成任务表加唯一索引防重跑后台修改档案后推荐结果没变画像缓存没有及时失效检查档案变更信号是否触发缓存删除必要时手动清Redis对应key给新接手这类项目的同学一个建议日志打点一定要从第一天就规范起来。“过滤阶段删了多少道菜、因为什么规则删的、最终排序前五名是哪几道”这三行日志能解决你未来80%的排查问题。项目上线三个月后我们最常看的不是数据库而是推荐服务的结构化日志。最后分享一点个人体会做完整套系统再回头看最复杂的不是Django和Flask这两个框架怎么整合也不是推荐算法有多高深而是“规则”和“数据”的整理。饮食推荐在智慧养老场景里容错率极低一道禁忌菜品的失误会直接伤到老人身体。所以我的体会是先保证不犯错再追求好吃和丰富。把禁忌过滤做成最高优先级把推荐解释做成标配功能让护理员和家属都看得懂为什么推荐这道菜这个系统才算真正落地。这个方向后续还能扩展营养摄入报告、家属小程序端、食堂反浪费统计但不管加什么内核都还是今天聊的这套数据建模和规则引擎。