做养老信息化这个方向有一阵子了,手头最大的体会是:很多所谓「智慧养老」项目,真正难的不是算法,而是把一堆模糊的线下规则变成明确的线上数据。就拿今天要聊的这套基于 Python、Flask 和 Django 框架的智慧养老服务平台——老人饮食推荐系统来说,表面上它就是个「给老人推荐吃什么」的 Web 系统,实际做进去才会发现,核心是一套约束很多、而且必须足够稳的规则引擎:这位老人有糖尿病,不能喝熬得稀烂的白粥;那位老人牙口不好,只能吃软食;还有人对花生过敏、有痛风史要避开豆制品和高嘌呤汤底。你要是靠记忆和 Excel 排菜单,翻车是迟早的事。
这篇文章我不打算讲太多产品理念,重点说清楚一件事:用 Flask 做推荐引擎,用 Django 做业务平台,这个组合怎么落地、数据表怎么设计、规则引擎怎么写、上线前后踩过哪些坑。适合正在做养老信息化、健康管理类系统,或者刚开始接触 Flask/Django 想找一个完整项目练手的朋友参考。
1. 先想清楚:这个系统解决的到底是谁的什么问题
很多人一听到「智能推荐」,第一反应就是上协同过滤、上深度学习模型。但养老场景下的饮食推荐和电商推荐有本质区别:电商推荐错了顶多浪费一次点击,饮食推荐错了可能让人吃出问题。所以在写第一行代码之前,得先把利益相关方和业务边界理清楚。
1.1 三个用户角色,三种「不知道吃什么」的痛点
这套系统表面上是给老人用的,实际真正天天操作的往往是三类人:
第一类是营养师或护理人员。他们负责给不同健康状况的老人配餐,面对几十上百位老人,每个人约束都不一样,靠脑子记根本不现实。他们的痛点是「我记不住那么多人的忌口,也无法靠人工完成每餐的禁忌排查」。
第二类是食堂或后厨。他们需要一份能直接照着做的菜单,最好还能看到每道菜的备注,比如「这位老人只能吃碎菜粥,不能加盐」。他们的痛点是「菜单给我没问题,但能不能告诉我为什么给这位老人上这道菜」。
第三类是家属。他们不常驻养老院,但想知道父母每天吃了什么、营养够不够。这一类不需要登录系统做复杂操作,往往看一眼家属端的小程序或日报就够了。
这个需求链路决定了一件事:系统不能只做一个推荐算法塞进去,必须包含老人档案管理、健康指标记录、菜品库、推荐引擎、反馈回收五个模块,缺一个,系统就转不起来。
1.2 核心业务闭环:从建档到反馈,缺一环都会翻车
我在设计这套系统时,把业务流程画成了闭环:建档 → 健康评估 → 生成餐单 → 执行反馈 → 调整迭代。
第一步建档,录入老人的基础信息,包括年龄、性别、身高、体重、口腔状况、过敏原、患病史。第二步健康评估,把体检报告里的血压、血糖、尿酸等指标维护进系统,形成一份「当前健康快照」。第三步就是推荐引擎基于这份快照,结合菜品库生成三餐餐单。第四步由护理人员或食堂执行餐单,老人吃完后记录反馈。第五步反馈回到推荐引擎,作为下次推荐的负向或正向因子。
这个闭环看起来平淡无奇,但我实际做下来发现,决定项目成败的往往是最容易被忽略的第三步和第四步之间的衔接:推荐引擎给出的菜单,后厨到底能不能看懂、能不能照着做?所以后来我在推荐结果里加了「推荐理由」字段,比如「低盐适配高血压」「软食适配口腔状况」,护理人员哪怕不知道老人的完整病历,也能判断这道菜合不合适。
1.3 换个框架视角:为什么这活不能靠Excel硬抗
有人会问,百来位老人,Excel 拉个表也能管理吧?我见过不少养老机构确实是这么干的,但实际用起来有三个绕不开的问题。
第一,禁忌与菜品的联动没法自动化。主食、菜品、汤品叠加起来,每一道都要和每位老人的禁忌做交叉判断,Excel 做不到自动排除。第二,推荐溯源困难。如果某位老人因为吃了不合适的菜出了问题,需要交代清楚「为什么这道菜会出现在他的餐单里」,Excel 记录几乎无法追溯。第三,迭代调整跟不上。老人的健康状况是动态的,血糖控制好了可以放宽某些约束,Excel 表改起来容易漏。
用 Python 的 Web 框架来做,是因为这件事本质上是一个「数据 + 规则 + 反馈」的闭环。Django 适合承载业务数据和后台管理,Flask 适合把推荐算法独立封装成一个轻量服务,两个框架搭在一起,正好把整个闭环串起来。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. Flask和Django同台:框架分工决定项目骨架
看到标题里同时出现 Flask 和 Django,很多人第一反应是:这两个框架不是二选一吗?确实,大多数项目不需要同时用,但在这套系统里,两个框架的定位完全不同,它们解决的是不同层面的问题。
2.1 两个框架的定位差异:全功能和微服务的取舍
先看一张对比表,这是我项目初期梳理框架选型时整理出来的:
| 对比维度 | Flask | Django |
|---|---|---|
| 定位 | 轻量微框架 | 全功能大框架 |
| 自带组件 | 路由、模板,ORM/Admin需要自行集成 | ORM、Admin后台、认证、表单全部自带 |
| 启动速度 | 秒级启动,适合独立小服务 | 启动稍慢,自带较多中间件 |
| 适合场景 | 算法服务、纯API、轻量Web | 业务系统、后台管理、数据密集型应用 |
| 学习曲线 | 低,几行代码就能跑起来 | 中高,需要理解MTV结构和中间件机制 |
| 数据库操作 | 需要搭配SQLAlchemy等ORM | Django ORM直接用,Admin后台快速搭建 |
这个对比不是要分谁好谁坏,而是为了说明:这套系统需要的是一个「重后台 + 轻算法」的组合。业务管理端要处理老人档案、菜品库、权限、记录,Django 的 Admin 后台能省掉大量 CRUD 页面开发时间;而推荐算法是纯计算逻辑,用 Flask 写一个独立服务最轻便,启动快、路由简单、不依赖 Django 全家桶,后续想换 FastAPI 或重构算法也不影响主业务。
2.2 我的分工方案:Django管业务数据,Flask管推荐算法
我的最终方案是:Django 作为主平台,承载全部业务数据和后台管理;Flask 作为推荐引擎网关,只暴露一个 HTTP 接口接收推荐请求。
为什么不让 Django 的视图函数直接写推荐逻辑?两方面的考虑。一方面,推荐引擎里包含营养计算、禁忌过滤、评分排序,这部分逻辑是纯函数式的,不和 Django 的 Model 层直接耦合更容易测试。另一方面,养老系统后续很可能要把推荐能力独立出去,比如对接外部营养师工具、给家属小程序提供推荐结果,独立成一个 Flask 服务,接口边界清清楚楚。
整体架构就是三块:
- Django 平台:老人档案模块、健康快照模块、菜品库模块、推荐记录模块、反馈模块。
- Flask 服务:推荐引擎,读入老人健康快照和菜品池,输出符合约束的三餐组合。
- MySQL 数据库:Django 和 Flask 共享,Flask 通过 SQLAlchemy 或直接读接口取数。
实际开发中,Flask 服务不直接写业务表,它通过 Django 暴露的 JSON 接口拿菜品池和老人档案,计算出结果后调用 Django 的接口写回推荐记录。这样数据入口统一,不会出现两边各写一套逻辑导致数据不一致的情况。
2.3 环境准备:VSCode建Flask项目、Django虚拟环境的几个坑
先说一个很多人卡住的地方:PyCharm 社区版不能直接创建 Flask 项目,导致有人以为「Flask 必须专业版才能用」。其实不是,社区版只是没有可视化模板,你完全可以手动创建一个 app.py,写上 from flask import Flask,然后配置解释器就能跑。如果用的是 VSCode,流程反而更顺。
我建议的目录结构是这样的:
code复制eldercare/
├── platform/ # Django主项目
│ ├── manage.py
│ ├── elderly/ # 老人档案app
│ ├── dish/ # 菜品库app
│ ├── recommend/ # 推荐记录app
│ └── config/ # Django配置文件
├── recommend_service/ # Flask推荐服务
│ ├── app.py # Flask入口
│ ├── engine.py # 推荐引擎核心逻辑
│ ├── nutrition.py # 营养计算
│ └── requirements.txt
└── venv/ # Python虚拟环境
虚拟环境这里有个高频坑:很多人在服务器上执行 python -m venv venv,结果创建出来的环境用的是系统自带的旧版本 Python,项目要求的 Django 4.x 装不上去。建议创建前先确认 Python 版本,用 python3 -m venv venv 显式指定,进入虚拟环境后再用 pip list 看一下基础版本是否正常。
VSCode 里创建 Flask 项目后,记得把解释器指向虚拟环境:Ctrl+Shift+P 打开命令面板,选择 Python: Select Interpreter,然后选 ./venv/bin/python。不然会出现「在终端里能跑 flask run,一按 F5 就报模块找不到」的问题。
2.4 内部接口对接:Django如何调用Flask的推荐服务
两个框架通过 HTTP JSON 接口对接,这是最简单可靠的方式。Flask 服务只监听本地端口,Django 通过 requests 库调用。
Flask 侧推荐接口:
python复制from flask import Flask, request, jsonify
from engine import RecommendEngine
app = Flask(__name__)
@app.route("/health", methods=["GET"])
def health():
return jsonify({"status": "ok"})
@app.route("/recommend", methods=["POST"])
def recommend():
data = request.get_json()
elder_id = data.get("elder_id")
meal_date = data.get("meal_date")
engine_result = RecommendEngine(elder_id, meal_date).run()
return jsonify(engine_result)
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5001)
Django 侧封装一个调用方法:
python复制import requests
from django.conf import settings
def get_recommendation(elder_id, meal_date):
resp = requests.post(
f"{settings.FLASK_SERVICE_URL}/recommend",
json={"elder_id": elder_id, "meal_date": meal_date},
timeout=10,
)
resp.raise_for_status()
return resp.json()
这里有个注意点:FLASK_SERVICE_URL 写进 Django 的 settings.py,不要写死在业务代码里。开发环境指向 http://127.0.0.1:5001,生产环境如果两个服务在同一台服务器,也一样走 127.0.0.1 内网地址,不要暴露外网访问。
跨框架调用还有一个隐患是 CSRF。Django 默认对 POST 请求做 CSRF 校验,但这里 Django 是「主动调用别人」,不涉及别人往 Django 提交 POST,所以没有影响。反过来,只要 Flask 接口不被浏览器页面直接 AJAX 调用,也不需要处理 CORS。我见过有人一上来就给 Flask 装了 flask-cors 全开跨域,结果把内部服务暴露在了前端页面下,增加了安全风险。
3. 推荐引擎核心:把「吃不得」和「该吃够」变成代码
这套系统最核心的模块就是推荐引擎。整个引擎的逻辑可以浓缩成两句话:先保证不犯错,再考虑吃得好。「不犯错」指的是硬性排除所有禁忌菜,「吃得好」指的是在剩余候选里按营养和偏好评分排序。
3.1 老人饮食约束体系:不止是过敏和忌口
我在一开始想得很简单,以为禁忌就是疾病、过敏、个人口味三类。建模之后才发现,至少得算五类。
第一类是疾病禁忌。糖尿病要控制高GI食材和精制糖;高血压要限制钠摄入;痛风要避开高嘌呤食物;慢性肾病要根据分期控制蛋白质总量。第二类是过敏原,花生、鸡蛋、牛奶、海鲜,这几样在老人群体里相当常见。第三类是口腔与吞咽能力,这决定了食物质地:正常普食、软食、半流质、全流质。第四类是个人偏好和习惯,有些人就是不吃香菜、不吃羊肉,这类约束不影响健康,但影响满意度。第五类是特殊医嘱,比如术后需要高蛋白、贫血需要补铁,这类是正向约束。
刚开始做约束表时,我直接用菜品的二级分类判断,比如「痛风老人不吃豆制品」就直接在豆制品分类里全部排除。但后来发现太粗糙:黄豆是高嘌呤,而豆制品加工后的嘌呤含量差异很大,必须落到具体菜品标签上。
所以最终的设计是:每道菜维护一个 tags 数组,比如 ["低盐", "低GI", "软食", "高蛋白"];每位老人的约束也归一化为标签集合,比如糖尿病老人的禁忌标签是 ["高糖", "高GI"]、高血压老人是 ["高盐", "高钠"]。推荐时做集合交集判断。
3.2 热量需求与宏量营养比例的计算
推荐不是把所有不能吃的排除就完事了,还得保证营养摄入达标。这里我用的是经典的 Harris-Benedict 公式计算基础代谢率 BMR,再乘活动系数得到一日总热量需求。
python复制def calc_bmr(age, gender, height_cm, weight_kg):
"""计算基础代谢率"""
if gender == "male":
return 88.362 + (13.397 * weight_kg) + (4.799 * height_cm) - (5.677 * age)
return 447.593 + (9.247 * weight_kg) + (3.098 * height_cm) - (4.330 * age)
def calc_tdee(bmr, activity_level="light"):
"""根据活动水平计算每日总热量需求"""
activity_factor = {
"sedentary": 1.2, # 久坐
"light": 1.375, # 轻度活动
"moderate": 1.55, # 中度活动
}
return bmr * activity_factor.get(activity_level, 1.375)
算完总热量之后按餐次比例分配:早餐 30%、午餐 40%、晚餐 30%,这是医院膳食和养老机构常用的分配比例。宏量营养素方面,老年人群我建议碳水 50%-55%、蛋白质 15%-20%、脂肪 25%-30%,其中蛋白质比例可以适度偏高,因为老人肌肉衰减问题比较普遍。
每道菜的 calories、protein、sodium 字段存的是每份的量。推荐时先累加候选组合的总热量和蛋白质,接近目标值的组合得分更高。注意这里不用做线性规划那么复杂,用评分法足够,因为菜品库规模有限,组合空间可控。
3.3 推荐流程四步走:过滤、约束、评分、排序
整个引擎跑起来分四步:过滤、约束、评分、排序。
第一步是硬性过滤。把命中老人任何一条禁忌标签的菜品直接剔除。第二步是纹理匹配。根据老人的口腔状况,把食物质地带不匹配的菜品剔除,比如吞咽困难老人不能吃普通硬度食物。第三步是营养评分。计算剩余菜品与目标热量、蛋白质的接近程度,越接近得分越高。第四步是多样性排序。同一类别菜品不重复,过去三天出现过的菜品排名后置。
我特意把「过滤」和「评分」分开,是因为这两步的优先级完全不同。过滤是安全底线,任何情况下不能破;评分是体验优化,可以在后续迭代中调整权重。用代码实现时,每一步都是一个独立函数,方便单测。
3.4 最小可用引擎代码:先跑通,再谈智能
我第一个版本的引擎非常朴素,核心逻辑大概这样:
python复制class RecommendEngine:
def __init__(self, elder_profile, dish_pool):
self.elder = elder_profile
self.dishes = dish_pool
def _build_forbidden_tags(self):
# 把疾病、过敏、食物质地约束统一映射为禁忌标签集合
disease_tag_map = {
"diabetes": {"高糖", "高GI"},
"hypertension": {"高盐", "高钠"},
"gout": {"高嘌呤"},
"kidney_disease": {"高蛋白"},
}
forbidden = set()
for disease in self.elder["diseases"]:
forbidden |= disease_tag_map.get(disease, set())
forbidden |= set(self.elder["allergens"])
return forbidden
def run(self):
forbidden = self._build_forbidden_tags()
candidates = []
for dish in self.dishes:
if forbidden & set(dish["tags"]):
continue
if dish["texture_level"] not in self.elder["allowed_texture_levels"]:
continue
score = self._nutrition_score(dish)
candidates.append({"dish": dish, "score": score})
candidates.sort(key=lambda x: x["score"], reverse=True)
return {"meal": [c["dish"] for c in candidates[:6]]}
这段代码能跑,但离「好用」还差不少。后面我加了营养评分函数、多样性约束和反馈因子,最终版比这复杂得多。核心想表达的意思是:不要一开始就想着搞机器学习、搞知识图谱,先用规则引擎把业务跑通,数据积累到一定程度再谈「智能」。
4. 数据模型设计:档案、菜品、推荐记录如何联动
数据模型是整个系统的基础。我在设计表结构时反复强调一个原则:先想清楚要支持哪些查询,再建表。这套系统的查询场景无非三类——某位老人的约束条件是什么、某道菜的标签和营养数据是什么、某天某位老人吃了什么以及为什么推荐这些。
4.1 老人档案与健康快照表
老人档案表存的是相对稳定的基础信息。健康指标是动态变化的,不能直接塞进档案表里,要单独建一张健康快照表,每次体检或更新生成一条新记录。
Django 模型大致长这样:
python复制from django.db import models
class Elderly(models.Model):
name = models.CharField("姓名", max_length=50)
age = models.IntegerField("年龄")
gender = models.CharField("性别", max_length=10, choices=[("male", "男"), ("female", "女")])
height_cm = models.FloatField("身高(cm)")
weight_kg = models.FloatField("体重(kg)")
texture_level = models.CharField(
"食物质地带", max_length=20, default="normal",
choices=[("normal", "普食"), ("soft", "软食"), ("half_fluid", "半流质"), ("fluid", "全流质")]
)
allergens = models.JSONField("过敏原", default=list)
created_at = models.DateTimeField(auto_now_add=True)
class HealthSnapshot(models.Model):
elder = models.ForeignKey(Elderly, on_delete=models.CASCADE, related_name="snapshots")
record_date = models.DateField("记录日期")
diseases = models.JSONField("患病史", default=list)
blood_pressure = models.CharField("血压", max_length=20, blank=True)
blood_sugar = models.FloatField("空腹血糖", null=True, blank=True)
uric_acid = models.FloatField("尿酸", null=True, blank=True)
note = models.TextField("备注", blank=True)
健康快照表用 record_date 做时间维度,推荐引擎拿最新的快照进行计算。患病史用 JSONField 存疾病编码列表,和菜品标签体系做映射。字段冗余没关系,关键是查询快。
4.2 菜品库与标签体系
菜品库是整个推荐系统的供给侧,表结构直接决定推荐引擎好不好写。菜品表除了基础信息,最关键的是 tags 字段和 texture_level 字段。
tags 一定要用统一的受控词表,不要给用户自由输入。我在项目初期让营养师手动填标签,结果「低盐」「少盐」「清淡」混着用,推荐引擎根本没法匹配。后来改成标签库 + 多选录入,才彻底解决这个问题。
python复制class DishTag(models.Model):
name = models.CharField("标签名", max_length=30, unique=True)
category = models.CharField("分类", max_length=20, choices=[("health", "健康属性"), ("allergen", "过敏原"), ("texture", "质地属性")])
class Dish(models.Model):
name = models.CharField("菜名", max_length=100)
category = models.CharField("餐次", max_length=20, choices=[("breakfast", "早餐"), ("lunch", "午餐"), ("dinner", "晚餐")])
calories = models.FloatField("热量(kcal/份)")
protein = models.FloatField("蛋白质(g/份)")
sodium = models.FloatField("钠(mg/份)")
texture_level = models.CharField("质地", max_length=20, default="normal")
tags = models.ManyToManyField(DishTag, blank=True)
is_active = models.BooleanField("是否启用", default=True)
字段里的 is_active 很重要。菜品下架或季节缺货时直接改状态,而不是删除记录,这样历史推荐记录仍然能关联到菜品名称。
4.3 推荐记录和反馈闭环
推荐记录表保存每次生成的推荐结果。注意这里要保存一份「推荐时点的菜品快照」,因为菜品的营养数据和热量可能会被后续修改,如果只存关联 ID,等审核时发现数据对不上,追溯就困难了。
python复制class RecommendRecord(models.Model):
elder = models.ForeignKey(Elderly, on_delete=models.CASCADE, related_name="records")
meal_date = models.DateField("用餐日期")
meal_type = models.CharField("餐次", max_length=10)
dish = models.ForeignKey(Dish, on_delete=models.CASCADE)
score = models.FloatField("推荐评分", default=0)
reason = models.TextField("推荐理由", blank=True)
created_at = models.DateTimeField(auto_now_add=True)
class Feedback(models.Model):
elder = models.ForeignKey(Elderly, on_delete=models.CASCADE)
record = models.ForeignKey(RecommendRecord, on_delete=models.CASCADE)
rating = models.IntegerField("评分", default=3, choices=[(1, "不吃了"), (2, "吃了一点"), (3, "正常"), (4, "喜欢"), (5, "非常喜欢")])
comment = models.TextField("备注", blank=True)
created_at = models.DateTimeField(auto_now_add=True)
推荐理由字段我放到了二级关注度,但实际运行下来它非常重要。护理人员看到「低盐适配高血压」才知道这道菜为什么上,而不是机械执行。
4.4 用Django Admin把录入成本压到最低
Django 自带的 Admin 后台在这套系统里发挥的作用被严重低估了。菜品库、标签库、老人档案的日常维护全部在 Admin 里完成,不用额外写管理页面。
菜品录入时,标签用 filter_horizontal 做多选,Nutrition 字段做成只读或带单位提示,录入员一个月就能熟练操作。老人档案的过敏原字段用 JSONField,在 Admin 里看起来是文本框,但实际录入时可以做成多选框的表单字段。这块虽然是小细节,但直接影响一线人员愿不愿意用系统。
5. 上线前踩过的坑:四处典型问题的完整排查链路
任何系统上线前都会踩坑,这套系统也不例外。我挑四个最典型的问题,把排查链路完整写出来,如果你也在做类似项目,可以少走弯路。
5.1 「低盐」和「少盐」不是同一个标签:菜品标签不统一的坑
上线两周后,营养师反馈一位高血压老人吃到了炒咸菜。我看推荐日志,发现这个菜品根本没有被过滤掉。查了菜品表,发现这道菜的标签是「少盐」,而推荐引擎的高血压禁忌标签是「高盐」。按逻辑,只有命中「高盐」标签的才被排除,「少盐」当然不会被拦截。
问题根源在于标签录入没有受控。营养师录入时有的写「低盐」、有的写「少盐」、有的写「清淡」、还有的写「咸淡适中」,同一个语义拆成了好几个标签,推荐引擎只能按精确匹配,自然失效。
排查完我做了两件事。第一,在代码层做标签归一化:把「少盐」「清淡」「低钠」统一映射为「低盐」。第二,把菜品标签录入改成下拉多选,从源头杜绝自由文本。这一步做完,类似问题基本不再出现。
5.2 身高写成1.7,体重输成70斤:单位混乱让BMI变成2.3
某天测试时发现一位老人的 BMI 算出来只有 2.3,一看就是单位错了。排查发现数据录入界面同时存在厘米和米两种单位,录入员在「身高cm」字段填了 1.7,在「体重kg」字段填了 35(因为老人习惯说斤,录入员没换算)。BMI 公式一算就是 35/(1.7^2)=12.1,另一条记录更离谱,直接算成 2.3。
这个问题靠人工提醒是防不住的,必须在服务端做范围校验。Django 模型里给 height_cm 加 MinValueValidator(130) 和 MaxValueValidator(200),给 weight_kg 加 MinValueValidator(30) 和 MaxValueValidator(150)。一旦超出范围直接拒绝保存,从入口把脏数据拦截掉。另外在录入表单里加单位提示,明确标注「身高 cm」「体重 kg」,条件允许的话做成联动换算组件。
5.3 Django的CSRF和Flask的裸接口:跨框架调用的安全问题
项目里有段时间前端页面一直报 403,排查后发现是 Django 的 CSRF 中间件拦了 POST 请求。这个问题在开发环境不明显,因为很多教程直接注释掉了 CSRF,但上线前恢复中间件后立刻暴露。解决方案不是全局关掉 CSRF,而是在视图函数上加 @csrf_exempt,并且把接口限制为仅内网可用。
同时,Flask 服务因为是裸接口,没有任何鉴权机制,如果监听地址绑定了 0.0.0.0,外部网络可以直接调用推荐接口。我把 Flask 的 host 改成 127.0.0.1,再配合防火墙只允许本机访问,才算把安全漏洞堵上。这里有个经验:跨框架内部调用,安全一定要放在部署层面解决,而不是在业务代码里写一堆判断。
5.4 部署到服务器:虚拟环境、Django静态文件、uWSGI配置
首次部署到服务器时我踩了一连串坑。
第一是 Python 命令版本。服务器上 python 指向 2.7,执行 python -m venv venv 创建出来的环境解释器也是 2.7,后面 pip install django 装出来的版本完全不兼容。解决方法是显式用 python3 -m venv venv 创建环境,进入虚拟环境后先用 python -V 确认版本。
第二是 Django 静态文件 404。开发时 DEBUG=True 能正常显示后台样式,关掉 DEBUG 后 Admin 后台直接变成纯文本页面。原因是没执行 collectstatic,或者 STATIC_ROOT 配置对不上。我在 settings.py 里配了 STATIC_ROOT = BASE_DIR / "staticfiles",执行 python manage.py collectstatic,再把 nginx 的静态文件路径指过去才解决。
第三是 uWSGI 和 Flask 的配置。Django 用 uWSGI 跑没问题,Flask 服务我建议用 gunicorn -w 2 -b 127.0.0.1:5001 app:app 起,两条服务独立管理,不要混在一个进程里。nginx 配置里,外部请求转发到 Django 的 8000 端口,Flask 服务不留公网入口。
6. 从能跑到好用:系统上线后的运维与迭代
系统跑起来只是开始,真正让它好用还需要几个层面的持续迭代。这个阶段我的核心经验是:先让使用者觉得「省事」,再谈「智能」。
6.1 用定时任务每天生成次日菜单
推荐引擎单次调用只能生成一餐的推荐,实际操作中需要提前一天把次日三餐全部生成好。我用了 Django 的定时任务方案,每天早上 7 点跑一次,拉取所有老人的最新健康快照,逐人生成三餐推荐,写进推荐记录表。
定时任务的核心逻辑:
python复制# tasks.py(Django app内)
from django.core.management.base import BaseCommand
from datetime import date, timedelta
class Command(BaseCommand):
def handle(self, *args, **options):
meal_date = date.today() + timedelta(days=1)
for elder in Elderly.objects.filter(is_active=True):
snapshot = elder.snapshots.order_by("-record_date").first()
if snapshot:
# 调用 Flask 推荐服务
result = get_recommendation(elder.id, meal_date)
# 结果写回 RecommendRecord
这里有个坑:定时任务直接放在 Django 的 manage.py 里跑会阻塞请求。建议用 django-crontab 或系统的 crontab 调用 python manage.py generate_menu,这样独立于 Web 进程。
6.2 给推荐结果加缓存
推荐引擎每次都要遍历菜品池、做标签匹配和评分,老人数量多了以后 MySQL 压力明显上升。我在 Flask 服务里加了 Redis 缓存:菜品池缓存 1 天,老人健康快照缓存 30 分钟,同一个老人短时间内重复请求直接返回缓存结果。
菜品池数据量不大,几百道菜完全可以在 Flask 启动时加载一次到内存,不需要每次请求都查库。推荐结果本身不建议缓存太久,因为老人的健康快照可能会被营养师更新,餐单最好在每次生成时重新计算。
6.3 反馈数据才是「智能」的来源
系统上线后,真正让推荐结果越来越准的不是算法本身,而是 Feedback 表里的数据。我后期给评分函数加了一个因子:过去一周内该菜品在同类老人中的平均评分。评分低的菜品自动降权,评分高的菜品排序靠前。
这一步做起来不难,但效果非常明显。食堂阿姨反馈的数据比任何营养模型都接近真实需求:王爷爷今天没吃完这道菜,可能是因为牙口确实嚼不动,也可能是单纯不爱吃。这些信号通过评分因子进入推荐引擎,系统就会越来越贴合实际。
6.4 接下来可做的扩展方向
这套系统现在的状态是「规则引擎驱动」,后续有数据积累后可以做三件事。
第一,对接可穿戴设备。如果老人的手环能实时上报血糖、心率,健康快照就可以从日维度细化到小时维度,推荐引擎根据实时指标动态调整当日餐单。第二,家属端小程序。家属最关心的是父母每天吃了什么、营养够不够,把推荐记录和拍照留档的餐食图片输出成日报,能极大提升家属的信任感。第三,食材季节性和成本的结合。后端接入采购系统后,推荐引擎可以根据当日库存优先推荐食材,减少浪费。
最后分享一点实际体会。系统功能做得再全,如果一线护理人员用起来觉得麻烦,最后还是会被弃用。我后来把推荐菜单打印成一张 A4 纸,每餐一栏,菜品后面标注老人编号和注意原因,食堂阿姨拿到纸就能干活,根本不用打开电脑。技术方案的本质是解决人的问题,使用者认可了,这套系统才算真正落地。
