拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从零搭建常见病中医食疗食谱健康平台:Flask全栈开发实践

从零搭建常见病中医食疗食谱健康平台:Flask全栈开发实践

去年冬天家里老人出院之后,一家人关于“到底能吃什么、不能吃什么”爆发了激烈的争论:百度出来的食疗文章互相打架,老一辈口口相传的偏方和医院营养师的忌口建议也经常冲突。上午查感冒,说风寒要喝生姜红糖水;下午查高血压,又说少吃辛辣刺激,转头却在另一篇文章里看到鸡汤老母鸭是温补佳品。信息多并不等于信息有效,那段时间我几乎把市面上常用的健康App下了一遍,要么是纯菜谱站,要么是碎片化的中药百科,几乎没有哪个产品能把“常见病—中医辨证—中药食材—具体食谱”这条链路串起来。所以我干脆自己动手,用Python和Flask从零搭了一个常见病中医中药食疗食谱健康平台。这个项目面向两类人群:一类是对Flask全栈开发有兴趣、想找一个业务逻辑完整可复现练手项目的开发者;另一类是从事食疗内容运营、想做健康科普数据结构化的产品经理。它解决的核心问题,是把零散的食疗信息变成“按病检索、按证型推荐、按食谱落地”的标准化知识库。

1. 为什么做这个平台:食疗信息过载背后的真实痛点

1.1 网上食疗信息的核心问题是脱离辨证

我最初调研了三十多篇关于失眠、高血压、便秘的食疗文章,发现一个高频现象:同一味食材,在不同文章里被说得完全相反。以枸杞为例,有的文章强调它滋补肝肾适合所有人,有的文章却明确标注“外感实热、脾虚有湿者不宜”。两种说法都有中医理论依据,区别在于语境:一个默认读者是肝肾阴虚人群,另一个默认读者是湿热体质。问题在于没有哪篇文章会在标题里告诉你适用范围。中医食疗最基本的原则是辨证施食,同样的食材放在不同证型的人身上,结论完全不同。脱离证型谈食材功效,本质上和脱离数据库索引谈查询速度一样,都是只给结论不给约束条件。

1.2 平台定位:做检索和推荐工具,不做诊断结论

这个认知直接决定了平台的定位。我没有试图做一个“输入症状就告诉你得什么病”的在线诊断系统,这种功能在合规上风险极高,也超出了个人项目的边界。平台只做两件事:第一,把常见病按中医分型整理清楚,说明每个证型的典型表现;第二,在证型明确的前提下,提供对应的中药食材和可执行的食疗食谱。用户必须先知道自己属于哪种状态,而这通常需要结合自身感受甚至医师判断。平台在显著位置放了一句话:所有内容仅作健康科普与饮食参考,不能替代执业医师的辨证论治。把边界划清楚,产品反而更可信。

2. 平台整体功能地图与完整的检索链路设计

2.1 六个核心模块一张图看懂

整个平台拆成了六个模块:疾病库、证型库、中药库、食材库、食谱库,以及贯穿全局的检索推荐引擎。疾病库按内科、外科、妇科、儿科等分类,目前重点做的是失眠、感冒、高血压、便秘、贫血、痛经这几种高频常见病。每种疾病关联一到多个证型,比如感冒分为风寒束表、风热犯肺、暑湿伤表;便秘分为热秘、气秘、虚秘、冷秘。证型是整个平台的枢纽,它同时关联中药、食材和食谱。

中药库记录的字段包括性味(寒热温凉平)、归经、功效简述、使用注意和禁忌人群。食材库相对轻量,核心字段是食性(温、热、寒、凉、平)和主要营养成分。食谱库则是一份可执行的菜谱:原料配比、做法步骤、适用证型、不适合人群、功效说明。这个六模块结构最关键的决策是:不直接建“疾病到食谱”的关联表,而是让疾病先关联证型,再由证型关联食谱。多一跳看似冗余,却让数据组织符合中医逻辑,也为后面做推荐算法留出了空间。

2.2 一次完整用户会话的流转过程

用户进入平台后的典型路径是这样的。访问首页,看到常见病分类入口;点击“失眠”,进入疾病详情页,左侧是证型概览(心脾两虚、肝火扰心、心肾不交等),右侧是疾病介绍和日常调理建议;点击“心脾两虚”证型标签,系统自动聚合出该证型下的中药推荐(如酸枣仁、龙眼肉、茯苓)和食谱推荐(如龙眼莲子粥);点击具体食谱进入详情页,看到用料、步骤、功效解析和禁忌提示。整个链路全部由后端的关联数据驱动,不需要运营人员人工配置组合页面。

检索入口同样重要。首页顶部有搜索框,支持按疾病名、症状关键词、食材名、中药名模糊搜索。比如直接搜“小米”,结果里除了小米对应的食谱,还会展示小米适合的证型以及该证型关联的疾病,这种搜索体验比单纯返回菜谱列表有价值得多。检索链路背后不是一条SQL硬查,而是先在倒排索引表里命中实体类型,再按不同实体类型走不同的聚合逻辑。

3. Flask框架选型思考与项目骨架搭建细节

3.1 为什么是Flask而不是FastAPI

项目启动前我特意在FastAPI和Flask之间纠结了两个晚上,顺便把社区里关于两者比较的讨论翻了一遍。FastAPI的类型提示、自动接口文档和异步支持确实很现代,但我要做的是一个以服务端渲染为主、兼顾少量API的内容型站点。Flask的Jinja2模板生态非常成熟,加上Flask-Admin、Flask-SQLAlchemy、Flask-WTF这些配套库,几乎不需要自己拼轮子就能把后台、表单、登录全部搞定。更重要的是Flask的文档和中文资料量级完全不同,遇到问题搜一下基本都能解决,这对个人项目来说非常现实。

如果这个平台之后要重点做小程序端、大量暴露API给外部系统,我会认真考虑FastAPI。但当前阶段页面端优先,团队只有我一个人维护,选Flask能让我用最少的代码覆盖最多的功能面。下面这个对比是我当时做的记录。

维度FlaskFastAPI
模板渲染Jinja2成熟稳定需要额外对接,非强项
后台管理Flask-Admin开箱即用生态相对弱
异步支持需扩展,配合Gunicorn也能跑原生async,性能上限高
类型校验手动或WTFormsPydantic自动校验
学习成本低,资料多中,API风格更现代

3.2 蓝图化目录结构与配置分离

项目目录不能把路由全堆在app.py里,否则业务涨起来之后维护就是噩梦。我采用的是按业务域拆分布蓝图的结构,既能保持模块边界清晰,又方便后续把某一模块独立出去做成微服务。

health_platform/ ├── run.py # 应用入口 ├── config.py # 环境配置 ├── requirements.txt ├── app/ │ ├── __init__.py # 创建app,注册蓝图和扩展 │ ├── models/ # SQLAlchemy模型 │ │ ├── disease.py │ │ ├── syndrome.py │ │ ├── herb.py │ │ ├── food.py │ │ └── recipe.py │ ├── views/ # 蓝图路由 │ │ ├── main.py # 首页、检索 │ │ ├── disease.py # 疾病页 │ │ ├── recipe.py # 食谱页 │ │ └── admin.py # 后台管理 │ ├── services/ # 业务逻辑层 │ │ ├── search.py │ │ └── recommend.py │ ├── templates/ # Jinja2模板 │ ├── static/ # CSS/JS/图片 │ └── utils/ └── data/ # 初始化数据脚本

配置分离的核心是无环境不代码。config.py里我定义了一个基础类,再分别派生DevConfig和ProdConfig,数据库地址、SECRET_KEY、是否开启调试都从环境变量读取,密钥绝不写死在代码里。开发时加载DevConfig,部署时通过环境变量指定ProdConfig。蓝图注册在__init__.py的create_app工厂函数里完成,这样测试实例和线上实例可以共用同一套初始化逻辑。

# app/__init__.py from flask import Flask from config import get_config def create_app(): app = Flask(__name__) app.config.from_object(get_config()) from app.models import db, login_manager db.init_app(app) from app.views.main import main_bp from app.views.disease import disease_bp from app.views.recipe import recipe_bp from app.views.admin import admin_bp app.register_blueprint(main_bp) app.register_blueprint(disease_bp) app.register_blueprint(recipe_bp) app.register_blueprint(admin_bp, url_prefix='/admin') with app.app_context(): db.create_all() return app

4. 数据库建模:疾病、证型、中药、食谱的四层关联关系

4.1 四层数据模型的核心表设计

如果说功能页面是平台的皮肉,数据库建模就是骨架。这层关系我前后重构过三次,最终稳定在四张核心业务表加上三张关联表的结构。疾病表、证型表、中药表、食谱表是实体表,疾病与证型的多对多关联、证型与中药的多对多关联、证型与食谱的多对多关联是关联表。食材不用单独建表,直接作为食谱表的组成部分,但食材的关键属性(食性、禁忌)会冗余在食谱表里,用来支撑搜索和筛选。

# app/models/syndrome.py from app.models import db class Disease(db.Model): __tablename__ = 'disease' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), unique=True, nullable=False) category = db.Column(db.String(20), index=True) # 内科/外科/妇科等 description = db.Column(db.Text) syndromes = db.relationship('Syndrome', secondary='disease_syndrome', backref='diseases') class Syndrome(db.Model): __tablename__ = 'syndrome' id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(50), nullable=False) key_features = db.Column(db.Text) # 典型表现 herbs = db.relationship('Herb', secondary='syndrome_herb', backref='syndromes') recipes = db.relationship('Recipe', secondary='syndrome_recipe', backref='syndromes')

关联表没有用独立的模型类,而是直接通过secondary参数交给SQLAlchemy处理,因为关联表中不需要额外字段。这里有个设计教训:一开始我为“疾病与证型关联表”加了sort_order排序字段,后来发现证型的展示顺序可以直接用证型表里的display_order控制,关联表加字段只会让查询和写入变得更麻烦。能用实体表字段解决的问题,不要放进关联表。

4.2 数据从哪里来:筛选、审核与版本管理

做内容型平台,代码只是起点,数据质量才是护城河。我整理了一批公开的中医食疗资料,但没有任何一个来源可以不加筛选直接导入。我的流程是两轮整理:第一轮由我对照三本公开出版的食疗著作和权威科普文章,抽取疾病、证型、中药、食材的描述,统一字段格式;第二轮请一位中医内科的朋友做内容复核,重点检查证型描述的准确性、中药禁忌是否完备、食谱用量是否在安全范围内。所有内容保留来源字段和review状态,只有标记为已审核的数据才在前台展示。

{ "herb_name": "酸枣仁", "nature": "平", "flavor": "甘、酸", "meridian": "心、肝、胆经", "indications": "养心补肝,宁心安神,敛汗生津", "taboo": "有实邪郁火者慎服", "source": "《中华本草》公开条目", "review_status": 1 }

数据版本管理用的是最朴素的办法:data目录下按日期存放JSON导入脚本,每次修改数据生成新的导入文件,通过脚本执行增量更新。这样线上数据出问题时,可以快速回滚到上一个导入文件,不至于改错一条内容就要去数据库里手工翻记录。

5. 核心推荐逻辑:基于辨证标签的加权匹配与兜底策略

5.1 从“证型”到“食性”的映射设计

推荐逻辑是整个平台最有含金量的部分。基础思路是给每条食谱打上多维标签,包括食性标签(温、热、寒、凉、平)、证型标签(关联到具体证型)、主诉关键词(如“安神”“健脾”“润肠”),然后根据用户当前所处的证型上下文计算匹配度。举例来说,心脾两虚型失眠对应的食谱,理想状态是食性偏温平、食材含龙眼肉和莲子、标签含“安神”和“健脾”。这就像搜索引擎对文档做相关度打分,只不过这里的“文档”是食谱,“查询”是证型上下文。

5.2 加权匹配的Python实现

匹配分数由三部分构成:证型直接关联的食谱基础分、食材属性与证型适宜食性的一致度加分、禁忌词命中扣分。具体实现时我没有引入复杂算法,用一组权重配置实现了可解释的排序逻辑,便于后续运营人员调整策略。

# app/services/recommend.py def score_recipe(recipe, syndrome): score = 0 # 基础分:食谱与证型直接关联 if syndrome in recipe.syndromes: score += 100 # 食性匹配加分:依据证型的适宜食性倾向 suitable_natures = syndrome.suitable_natures() # 如 ['温', '平'] if recipe.food_nature in suitable_natures: score += 30 # 关键词命中加分 for kw in syndrome.keywords: if kw in recipe.tags: score += 10 * recipe.tags.count(kw) # 禁忌扣分 for taboo in recipe.taboos: for kw in syndrome.keywords: if taboo in kw or kw in taboo: score -= 50 return score def recommend(syndrome_id, limit=10): syndrome = Syndrome.query.get(syndrome_id) candidates = Recipe.query.filter( Recipe.review_status == 1, Recipe.is_public == True ).all() ranked = sorted(candidates, key=lambda r: score_recipe(r, syndrome), reverse=True) return [r for r in ranked if score_recipe(r, syndrome) > 0][:limit]

这段逻辑看起来不长,但实际调试时发现两个坑。第一个坑是禁忌匹配的误伤:有些食谱的禁忌描述是“脾胃虚寒者慎用”,而证型关键词里恰好含“虚寒”,这本来应该加分(说明食谱对证对症),却被禁忌逻辑扣了50分。后来我调整了语义,把禁忌拆成“忌”和“慎”两档,只有“忌”才触发强扣分,“慎”只做提示不做否决。第二个坑是食谱同时属于多个证型时,直接关联基础分会重复累加,导致热门食谱永远排前面。修正办法是每一种食谱与证型的关联只计一次基础分,仔细去重后才进入排序。

5.3 冷启动与数据稀疏时的降级策略

新平台最怕内容不足时直接输出空洞的推荐。我在推荐逻辑后面加了三层降级:第一层走加权匹配;如果匹配结果少于三条,第二层退回该疾病下所有已审核食谱按浏览量排序;第三层如果连疾病下都没有内容,就返回同食性下的通用食谱,并标注“通用食谱,请结合自身体质辨证选用”。这套兜底保证用户在内容稀少时也能看到东西,而不是面对一个空白页面。

6. 管理后台与内容生产流程:让中药和食谱数据可维护

6.1 基于Flask-Admin的半小时后台搭建

平台对运营后台的需求很明确:录入食谱、维护证型、审核内容、管理用户。Flask-Admin搭配Flask-Login,半小时就能跑通基础版本。把四个核心模型注册进后台,配置默认的列展示和搜索字段,再为食谱模型做一个自定义的编辑视图,把原料字段拆成结构化列表,方便按食材检索。

# app/views/admin.py from flask_admin import Admin from flask_admin.contrib.sqla import ModelView from app.models import Disease, Syndrome, Herb, Recipe, db class BaseModelView(ModelView): can_create = True can_edit = True can_delete = False # 内容资料优先软删除 page_size = 50 class RecipeView(BaseModelView): column_searchable_list = ['name', 'ingredients', 'tags'] column_filters = ['review_status', 'food_nature'] form_excluded_columns = ['views'] admin = Admin(app, name='健康平台后台', template_mode='bootstrap4') admin.add_view(BaseModelView(Disease, db.session, name='疾病管理')) admin.add_view(BaseModelView(Syndrome, db.session, name='证型管理')) admin.add_view(BaseModelView(Herb, db.session, name='中药管理')) admin.add_view(RecipeView(Recipe, db.session, name='食谱管理'))

这里有一个容易忽视的细节:can_delete要关掉。内容型数据一旦被用户收藏或引用,物理删除会把前端页面打成404。我用的是加is_deleted字段的软删除方案,前台查询统一带is_deleted == False过滤条件。虽然Flask-Admin默认的删除按钮不能直接支持软删除,但可以通过重写delete_model方法实现,代码多写几行,线上运维的后悔药就多了一颗。

6.2 录入校验、用药禁忌与关联完整性

后台越方便,越容易产生脏数据。我为表单加了三类校验:第一类是必填字段校验,食谱名称、证型关联、原料列表不能为空;第二类是枚举字段校验,食性和证型名称必须取自预置字典,避免运营人员自创“微温”“大寒”这类不标准表述;第三类是关联完整性校验,比如说创建食谱时必须选择至少一个证型,否则食谱永远无法被推荐引擎命中,等于静默丢失。这部分直接依赖Flask-WTF做表单验证。

中药禁忌字段很值得说。我在数据库里设置了一个text字段存禁忌描述,但前台推荐逻辑需要结构化的禁忌项才能做扣分计算,所以录入时强制禁止项使用分号分隔的固定短语,例如“阴虚火旺者忌;孕妇慎用”。这样做牺牲了一部分输入自由,换来了推荐模块不用做自然语言解析的省心。结构化数据在内容规模不大的时候看不出优势,一旦内容涨到几千条,谁用谁舒服。

7. 从开发机到公网:部署Flask平台的真实记录与踩坑

7.1 部署选型与Nginx+Gunicorn配置

平台部署我选了一台2核4G的云服务器,系统是Ubuntu。部署Stack是Gunicorn加Nginx,这是Flask应用最经典也最稳的生产组合。Gunicorn负责运行Flask应用,Nginx负责接收外部请求、处理静态文件、做反向代理。之所以不直接裸跑flask run,是因为内置开发服务器既不稳定也不支持并发,生产环境必须交给专用WSGI服务器。

# Gunicorn启动配置 gunicorn -w 4 -b 127.0.0.1:8000 --timeout 60 run:app
# /etc/nginx/sites-available/health_platform server { listen 80; server_name your_domain_or_ip; location /static { alias /var/www/health_platform/app/static/; expires 7d; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }

进程守护用systemd配置一个service文件,让Gunicorn在服务器重启后自动拉起,同时负责崩溃后的自动重启。这一步不能省,否则一次内存溢出就能让平台在半夜静默宕机,而你在第二天早上打开手机才发现打不开网页。

7.2 生产环境里踩过的三个坑

写这段时我特意翻了下当时的部署记录,三个问题最有代表性。第一个是静态文件404,开发时Flask自己处理静态资源没问题,上线后Nginx接管了/static路径,但项目里的静态文件实际存放路径和Nginx配置文件里的alias路径不一致,导致所有CSS和图片全部丢失。排查方法不复杂,先看Nginx错误日志,再去服务器上确认目录是否存在,最后发现是软链接没建对。第二个是SECRET_KEY,Flask的session机制依赖它,本地和线上不一致时,用户一刷新登录态就丢失,后台完全没法正常使用。最开始我在config里写死了密钥,部署时忘了改成环境变量读取,排查了很久才意识到。第三个坑是数据库连接池,SQLite在低并发开发环境没问题,但线上Gunicorn开了四个worker,同时写库时经常报database is locked。最终解决办法是切换到MySQL,并把每个worker的数据库连接回收时间调短。这个坑在Flask部署里太典型了,项目只要有点并发量,SQLite基本都会成为瓶颈。

8. 上线实测、用户反馈与后续扩展方向

8.1 公测后的真实数据与用户反馈

平台上线后我邀请了一批亲友和几个健康群做了小范围公测,大概五十多人,跑了一个多月。数据方面最有意思的发现是食谱详情页的平均停留时间有1分40秒,高于普通内容页,说明用户是真的在看步骤和原料,而不是刷完标题就走。搜索词排行里除了疾病名,出现了大量食材名,比如“小米”“山药”“红枣”,这验证了当初把食材作为独立搜索入口的判断。

用户访谈里反馈最集中的是两点:第一,一个疾病下证型太多,普通用户不知道选哪个。我之前的证型描述用了不少中医术语,对小白不友好。后来给每个证型加了“典型表现”的通俗描述,并在页面加了一个简单自测对照表,把关键表现列成了勾选项,用户对照勾选后能初步锁定可疑证型,再结合医师确诊使用。第二,很多用户希望食谱能按“难度”和“时间”筛选。上班族根本没时间炖两个小时的汤,他们更想要二十分钟能做完的日常菜。这两个需求都在第二版里做了实现,给每道食谱加了difficulty和cook_time字段。

8.2 接下来想做的几件事

短期规划里排第一的是小程序适配。Flask后端天然适合做API输出,已经有现成的数据模型和推荐逻辑,只需要再写一套接口层。第二个方向是把食材-证型的中药属性做成可视化图谱,让用户能直观看到“温性食材推荐”“寒性食材慎用”的分布。第三个方向是引入更细粒度的个人体质档案,让用户填写体质倾向和过敏原,在推荐结果里做二次过滤。这需要更严谨的数据支撑和医学审核,不能拍脑袋做,所以进度不会太快。

把这个项目从头到尾做完,我最大的感受是做内容型应用难的不是框架、不是部署,而是数据结构和业务逻辑的相互成就。食疗信息一旦被拆解成“疾病—证型—中药—食谱”的结构化关系,推荐、检索、后台维护这些功能就都有了支点;反过来,好用的推荐算法也逼着你把内容整理得更规范。Flask在这里更像一个轻便可靠的骨架,真正让它变得有意义的,是你往里面填的数据和想清楚的那套业务规则。如果你也想做一个类似的内容平台,建议先用一周时间梳理出数据关系图,再写第一行代码,这个顺序不要反。花在前期建模上的时间,会在开发和维护阶段十倍回报你。

返回列表