
去年冬天家里老人出院之后一家人关于“到底能吃什么、不能吃什么”爆发了激烈的争论百度出来的食疗文章互相打架老一辈口口相传的偏方和医院营养师的忌口建议也经常冲突。上午查感冒说风寒要喝生姜红糖水下午查高血压又说少吃辛辣刺激转头却在另一篇文章里看到鸡汤老母鸭是温补佳品。信息多并不等于信息有效那段时间我几乎把市面上常用的健康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 app4. 数据库建模疾病、证型、中药、食谱的四层关联关系4.1 四层数据模型的核心表设计如果说功能页面是平台的皮肉数据库建模就是骨架。这层关系我前后重构过三次最终稳定在四张核心业务表加上三张关联表的结构。疾病表、证型表、中药表、食谱表是实体表疾病与证型的多对多关联、证型与中药的多对多关联、证型与食谱的多对多关联是关联表。食材不用单独建表直接作为食谱表的组成部分但食材的关键属性食性、禁忌会冗余在食谱表里用来支撑搜索和筛选。# app/models/syndrome.py from app.models import db class Disease(db.Model): __tablename__ disease id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(50), uniqueTrue, nullableFalse) category db.Column(db.String(20), indexTrue) # 内科/外科/妇科等 description db.Column(db.Text) syndromes db.relationship(Syndrome, secondarydisease_syndrome, backrefdiseases) class Syndrome(db.Model): __tablename__ syndrome id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(50), nullableFalse) key_features db.Column(db.Text) # 典型表现 herbs db.relationship(Herb, secondarysyndrome_herb, backrefsyndromes) recipes db.relationship(Recipe, secondarysyndrome_recipe, backrefsyndromes)关联表没有用独立的模型类而是直接通过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, limit10): syndrome Syndrome.query.get(syndrome_id) candidates Recipe.query.filter( Recipe.review_status 1, Recipe.is_public True ).all() ranked sorted(candidates, keylambda r: score_recipe(r, syndrome), reverseTrue) 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_modebootstrap4) 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 部署选型与NginxGunicorn配置平台部署我选了一台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_KEYFlask的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在这里更像一个轻便可靠的骨架真正让它变得有意义的是你往里面填的数据和想清楚的那套业务规则。如果你也想做一个类似的内容平台建议先用一周时间梳理出数据关系图再写第一行代码这个顺序不要反。花在前期建模上的时间会在开发和维护阶段十倍回报你。