本文还有配套的精品资源,点击获取
简介:一套开箱即用的智能菜谱推荐解决方案,结合知识图谱结构化建模与大语言模型生成能力,支持按口味偏好、营养目标、现有食材、饮食禁忌等多维度动态生成个性化菜谱。前端采用React框架开发,已组织好页面路由(pages)、可复用组件(components)、通用工具函数(common)和响应式布局(layouts),集成Umi构建工具与TypeScript类型支持;后端由Python实现核心逻辑,main.py负责知识图谱查询、语义匹配及LLM驱动的菜谱生成调度;配套publish.sh等脚本支持一键部署,pnpm-lock.yaml和package.保障依赖稳定,.umirc.ts和tsconfig.提供标准化开发配置。所有源码、配置文件、构建产物目录(如.umi)和包管理缓存(.pnpm-store)均已就位,适配本地开发调试与生产环境发布,可直接嵌入健康管理App、智能厨电系统或营养服务平台,省去知识建模、推理链路和前后端联调环节。
1. 项目概述:这不是一个“玩具Demo”,而是一套可嵌入真实产品的智能饮食决策引擎
我做过三年营养科技方向的工程落地,也带团队交付过五款面向C端用户的健康类App。见过太多所谓“AI菜谱推荐”——要么是关键词匹配的规则引擎,要么是调用公开API的简单封装,真正能把知识图谱的语义推理能力、大模型的生成可控性、前端交互的流畅体验三者拧成一股绳的完整工程,少之又少。这个资源包,就是我在2024年参与某头部智能厨电厂商联合研发时沉淀下来的最小可行产品(MVP)基线代码,不是教学Demo,也不是概念验证,而是已经跑通本地调试→沙箱测试→小规模灰度上线全流程的生产级骨架。
它解决的核心问题很实在:当用户说“我冰箱里有鸡胸肉、西兰花、胡萝卜,最近在控糖,想吃点清淡但不寡淡的晚餐”,系统不该只返回3条相似菜谱链接,而要理解“控糖”背后是碳水摄入限制与升糖指数(GI)敏感,“清淡”在营养学中对应低钠、少添加糖、保留食材本味,“鸡胸肉+西兰花+胡萝卜”构成优质蛋白+十字花科+根茎类膳食纤维的黄金组合——这些隐含约束,必须靠结构化知识来锚定,再由大模型完成符合逻辑、可执行、有烟火气的语言生成。整个过程,前端要实时反馈推理路径(比如高亮显示“已排除含蔗糖酱料的菜式”),后端要保证每次生成都可追溯、可干预、可审计。
关键词里的“智能菜谱推荐、知识图谱、大模型生成、React前端、Python后端”,不是并列的五个标签,而是一个闭环链条:知识图谱是骨骼(定义食材-营养素-烹饪法-疾病禁忌之间的硬约束),大模型是血肉(在骨骼框架内填充语言表达、步骤细节、口味微调),React是神经末梢(把推理过程可视化、把用户反馈即时传导回后端),Python是中枢(调度图谱查询、调用LLM、校验生成结果合规性)。这套设计,让系统既不会像纯LLM方案那样“胡说八道”(比如推荐糖尿病患者喝蜂蜜柚子茶),也不会像纯图谱方案那样“死板僵硬”(比如只返回教科书式标准菜谱,缺乏人情味)。它适合直接集成进健康管理App的“今日饮食”模块,也能作为智能灶具的语音交互后端,甚至能给社区营养师提供辅助决策建议——关键在于,你拿到手就能跑,不用再花两周搭环境、配依赖、调接口。
我第一次在本地跑通这个系统时,输入“家里有豆腐、菠菜、鸡蛋,孩子3岁,对花生过敏,需要补铁”,它在8秒内返回了《菠菜豆腐蛋羹》——不仅标注了“无花生成分”“含血红素铁+维生素C促进吸收”,还在步骤里特别提醒“菠菜焯水去草酸,避免影响铁吸收”。这种细节能落地,靠的不是玄学,而是图谱里预置的“草酸-铁吸收抑制”关系边、LLM提示词中强制要求的“儿童安全剂量说明”,以及前端组件对营养标签的自动渲染逻辑。下面,我就带你一层层拆开这个“黑盒子”,告诉你每一行代码为什么这么写,每个配置文件背后藏着什么工程权衡。
2. 整体架构设计与技术选型逻辑:为什么是这套组合,而不是别的?
2.1 架构分层:三层解耦,但绝不割裂
整个系统采用清晰的三层架构,但和教科书式的“前后端分离”不同,它的每一层都带着明确的领域意图:
表现层(React + Umi):不是简单的页面渲染器,而是“饮食意图翻译器”。它把用户口语化的输入(如“今天不想吃太油腻的”)转化为结构化query参数,再把后端返回的JSON结果(含图谱推理路径、生成菜谱的置信度评分、营养成分表)翻译成用户能感知的视觉语言。比如,当图谱查到某道菜含用户禁忌的“虾”,前端不会简单报错,而是触发“替代方案生成”流程,并在UI上用渐变色块标出“原方案被过滤,已启用安全替代”。
业务逻辑层(Python main.py):这是真正的“大脑”。它不直接调用LLM,而是先走知识图谱查询(Neo4j或本地RDF库),获取满足硬约束的候选食材组合与烹饪方式集合;再将这些结构化结果注入LLM提示词,引导模型生成符合营养学逻辑、烹饪实操可行、语言自然的菜谱文本;最后用轻量级规则引擎校验生成结果——比如检查是否包含用户明确拒绝的食材、热量是否超出当日预算、步骤是否超过5步(适配快节奏生活)。这个顺序不能颠倒:先图谱后LLM,是为了把“不可为”边界划清楚,避免大模型在无效空间里浪费token。
数据层(知识图谱 + LLM API):图谱采用混合存储策略——核心实体关系(食材-营养素、疾病-饮食禁忌、烹饪法-温度/时间)存于轻量级SQLite中(便于移动端离线使用),动态扩展关系(如用户自定义的“我家孩子不吃香菜”)存在内存图谱中。LLM则通过统一网关调用,支持切换OpenAI、Qwen、GLM等不同后端,main.py里用工厂模式隔离具体实现,避免绑定单一服务商。
提示:不要试图把所有知识塞进图谱。我见过团队把“番茄炒蛋该放多少盐”这种经验性知识也建模,结果图谱膨胀到无法维护。正确的做法是:图谱管“能不能做”(硬约束),LLM管“怎么做更好”(软建议)。比如图谱定义“高血压患者应限钠”,LLM在生成步骤时自动补充“本菜谱使用低钠酱油,盐量减少30%”。
2.2 前端选型:Umi + TypeScript,不是为了炫技,而是为了“零配置交付”
很多人疑惑:为什么不用Vite?为什么坚持Umi?答案很务实——降低集成方的接入成本。Umi的约定式路由、插件生态(特别是@umijs/plugin-access权限控制、@umijs/plugin-qiankun微前端支持)让这套前端能无缝嵌入现有App的WebView容器,或者作为独立PWA部署。更重要的是,.umirc.ts里预置了针对健康类应用的特殊配置:
define: { __IS_HEALTH_APP__: true }:编译时注入全局常量,让组件能区分运行环境(比如在厨电设备上隐藏“分享到微信”按钮);chainWebpack: (config) => { config.optimization.splitChunks({ chunks: 'all', cacheGroups: { health: { name: 'health', test: /[\\/]src[\\/](components|pages)[\\/]/, priority: 20 } } }); }:把营养计算、食材识别等高频模块单独打包,方便后续按需加载;proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true } }:开发期代理到Python后端,避免CORS问题,且代理规则写死在配置里,新人拉代码即跑通。
TypeScript的作用更关键。typings.d.ts里不仅定义了API响应类型(interface RecipeResponse { id: string; steps: string[]; nutrients: Nutrient[]; reasoningPath: string[] }),还为图谱查询结果做了强约束——比如interface GraphQueryResult { matchedIngredients: string[]; excludedBy: { rule: string; evidence: string }[] }。这使得前端开发者在写useEffect处理响应时,IDE能自动提示res.excludedBy[0].evidence,而不是靠文档猜字段名。我们曾用这套类型定义,在一次紧急迭代中,仅用半天就完成了“新增素食偏好过滤”的功能,因为所有类型变更都在TS编译阶段暴露,没留runtime隐患。
2.3 后端选型:Python + Flask轻量框架,胜在“可调试性”
main.py没有用FastAPI或Django,选择Flask最核心的原因是:调试友好,日志透明,便于营养师参与验证。在实际交付中,营养专家需要反复查看“为什么这个菜谱被推荐/被过滤”,Flask的app.logger.info()能直接输出图谱查询的Cypher语句、LLM请求的完整prompt、生成结果的token分布,这些信息被结构化写入logs/reasoning.log,营养师用Excel打开就能分析。如果是异步框架,日志会乱序,根本没法对齐推理链路。
更关键的是依赖管理。publish.sh脚本里有一段被注释掉的代码:
# pip install --no-deps -r requirements.txt # 禁用依赖传递,确保图谱库版本锁定 # pip install rdflib==6.3.0 # 强制指定RDF库版本,避免SPARQL解析差异这是因为我们在测试中发现,rdflib 7.x版本对中文字符的URI编码处理有变化,导致“枸杞”在图谱中被解析为%E6%9E%B8%E6%9E%9A而非枸杞,进而影响匹配。这种细节,只有在Flask这种同步、显式控制流的框架里才能快速定位。而requirements.txt里llm-client==0.2.1这个包,是我们自己封装的LLM网关SDK,它做了三件事:自动重试(网络抖动时)、token计费统计(对接内部结算系统)、生成结果安全过滤(移除医疗建议类敏感表述)——这些都不是通用LLM SDK提供的,而是业务必需。
2.4 工程化设计:那些藏在脚本和配置里的“隐形需求”
publish.sh表面看是部署脚本,实则承载着三个隐形需求:
- 环境一致性:它先执行
pnpm install --frozen-lockfile(对应pnpm-lock.yaml),确保前端依赖版本与开发机完全一致;再运行pip install -r requirements.txt --constraint constraints.txt,其中constraints.txt锁定了numpy==1.24.4(因图谱计算库依赖特定版本); - 产物瘦身:
publish_oss.sh里有find dist -name "*.map" -delete,删除source map文件——健康类App对包体积敏感,用户不会调试你的前端代码; - 合规审计:
publish.sh末尾调用python audit.py,扫描生成的dist/目录,检查是否包含未授权的字体文件(某些商用字体需额外授权)、是否泄露敏感路径(如.git残留),输出audit-report.json供法务审核。
.npmrc里的registry=https://registry.npmmirror.com不是随便写的。我们服务的某海外客户要求所有依赖必须来自中国镜像源,避免因网络波动导致CI失败。而.inscode文件(非标准命名)其实是Insecure Code Scanner的配置,它禁用了对eval()的扫描——因为前端有个动态公式计算器(计算每日蛋白质需求),必须用Function构造器,这是业务刚需,不是漏洞。
3. 核心模块深度解析:从知识图谱构建到大模型生成的全链路
3.1 知识图谱:不是“堆数据”,而是构建营养领域的“语义语法树”
图谱数据存放在data/knowledge/目录下,核心是三个TTL文件:
ingredients.ttl:定义食材实体及其属性(ex:chickenBreast a ex:Ingredient ; ex:proteinContent "22g/100g" ; ex:giIndex "45" ; ex:allergen ex:none .)nutritionRules.ttl:定义营养学规则(ex:DiabetesRule a ex:NutritionRule ; ex:appliesTo ex:Diabetes ; ex:constraint [ ex:on ex:carbohydrate ; ex:maxValue "45g" ] .)cookingMethods.ttl:定义烹饪法与营养保留关系(ex:steaming a ex:CookingMethod ; ex:nutrientPreservation [ ex:for ex:vitaminC ; ex:rate "85%" ] .)
关键不在数据量,而在关系的设计哲学。比如ex:allergen属性,我们没用简单的字符串("peanut"),而是指向一个实体ex:peanutAllergy,这样就能扩展:“花生过敏者应避免接触花生油(ex:peanutOil),但可食用精炼花生油(ex:refinedPeanutOil)”,因为图谱里定义了ex:refinedPeanutOil ex:allergen ex:none。这种设计让“过敏过滤”不再是布尔开关,而是可配置的强度等级。
图谱查询逻辑在main.py的KnowledgeGraphEngine类里。以“控糖”为例,用户输入触发:
def query_for_diabetes(user_profile): # 步骤1:获取用户疾病标签 disease = self.graph.query(f"MATCH (u:User {{id:'{user_id}'}})-[:HAS_CONDITION]->(d:Disease) RETURN d.name") # 步骤2:查找适用规则 rules = self.graph.query(f"MATCH (r:NutritionRule) WHERE r.appliesTo = '{disease}' RETURN r") # 步骤3:提取硬约束(结构化) constraints = [] for rule in rules: for constraint in rule['constraints']: if constraint['on'] == 'carbohydrate': constraints.append(('carb_max', float(constraint['maxValue'].split('g')[0]))) return constraints # 返回 [('carb_max', 45.0)]这个constraints列表,就是喂给LLM的“安全护栏”。LLM的prompt模板里有固定段落:
【硬约束】 - 每餐碳水化合物不超过45克 - 禁用蔗糖、葡萄糖浆等添加糖 - 优先选择低GI食材(GI<55) 请严格遵守以上约束生成菜谱,若无法满足,请返回空列表并说明原因。注意:图谱查询结果必须是结构化数据(字典/列表),不能是自然语言。我踩过的坑是早期用SPARQL返回“碳水限制45克”,LLM有时会误读为“45克碳水是目标值”,而非“上限值”。改成
('carb_max', 45.0)后,prompt里用{constraints[0][1]}克插入,彻底杜绝歧义。
3.2 大模型生成:可控生成的三重保险机制
main.py里的LLMGenerator类不是简单调用API,而是实施三重保险:
第一重:Prompt Engineering精细化分层
-system_prompt定义角色:“你是一名注册营养师,擅长为慢性病患者设计家常菜谱,语言亲切,步骤详尽,避免专业术语”
-user_prompt分三块:
1. 用户画像(结构化):“年龄35,性别女,糖尿病,当前血糖7.2mmol/L,冰箱库存:鸡胸肉、西兰花、胡萝卜”
2. 图谱约束(结构化):“碳水≤45g/餐,禁用添加糖,优先低GI食材”
3. 生成指令(明确):“生成1道主菜,包含名称、食材清单(精确到克)、详细步骤(5步以内)、营养亮点(1句话)”
第二重:输出后处理(Post-processing)
LLM返回JSON后,立即执行:
def validate_and_enrich(recipe_json): # 检查必填字段 assert 'name' in recipe_json and recipe_json['name'], "菜谱名称缺失" # 校验食材是否在库存中(模糊匹配) for item in recipe_json['ingredients']: if not any(fuzz.ratio(item['name'], stock) > 80 for stock in user_stock): raise ValueError(f"食材'{item['name']}'不在用户库存中") # 补充营养计算(调用本地营养数据库) recipe_json['nutrients'] = calculate_nutrients(recipe_json['ingredients']) return recipe_json这里用fuzz.ratio做模糊匹配,解决“鸡胸肉”vs“鸡胸”、“西兰花”vs“绿菜花”的问题,比精确匹配更符合真实场景。
第三重:Fallback机制
当LLM生成失败(超时/返回空/违反约束),系统不报错,而是降级到图谱驱动的规则生成:
try: return llm_generate(...) except Exception as e: logger.warning(f"LLM fallback triggered: {e}") return rule_based_fallback(constraints, user_stock)rule_based_fallback从图谱中检索“鸡胸肉+西兰花+胡萝卜”组合的最优烹饪法(蒸/煮),再从预置模板库里选一个匹配的菜谱(如《清蒸鸡胸西兰花》),填充基础步骤。虽然不如LLM生成生动,但100%可靠,保证用户体验不中断。
3.3 React前端:让“智能”可感知、可信任
前端src/pages/RecipeRecommend.tsx是核心交互页,它的设计哲学是:把AI的“黑盒推理”变成用户可理解的“白盒旅程”。
页面顶部是用户输入区,但不是简单textarea:
- 用<AutoComplete>组件,输入“控糖”时自动联想“糖尿病”“胰岛素抵抗”“血糖偏高”等医学术语,避免用户描述不准;
- 库存录入支持拍照识别(调用src/common/ocr.ts),识别结果自动映射到图谱中的标准食材ID(如“胡萝卜”→ex:carrot),解决口语与标准名差异。
主体区域分三栏:
-左栏(推理路径):动态渲染图谱查询过程,如“✅ 匹配糖尿病饮食规则 → ✅ 过滤高GI食材(土豆、南瓜) → ✅ 筛选库存食材组合(鸡胸肉+西兰花+胡萝卜)”,每一步点击可展开Cypher语句;
-中栏(生成结果):菜谱卡片,重点突出<NutrientBadge type="carb" value="38g" max="45g" />这样的可视化营养标签,用进度条直观显示“碳水占用率”;
-右栏(交互反馈):“这个菜谱太咸了”“步骤太复杂”按钮,点击后触发feedbackApi.submit({recipeId, feedback: 'too-salty'}),数据存入feedback.db,用于后续LLM微调。
最关键的细节在src/components/RecipeStep.tsx:步骤描述不是静态文本,而是动态渲染。比如步骤3“加入适量盐”,组件会根据用户档案(高血压患者)自动替换为“加入1/4茶匙低钠盐”,这个替换逻辑在src/common/nutritionUtils.ts里,基于图谱中的ex:lowSodiumSalt实体和用户疾病标签实时计算。
4. 实操部署与调试指南:从零开始跑通的完整路径
4.1 本地开发环境搭建(5分钟速通)
前置条件:Python 3.9+、Node.js 18+、pnpm(npm install -g pnpm)
步骤1:初始化后端
cd food-react-master # 进入根目录 python -m venv venv source venv/bin/activate # Windows用 venv\Scripts\activate pip install -r requirements.txt # 启动后端(默认端口8000) python main.py此时访问http://localhost:8000/healthz应返回{"status":"ok"},证明Flask服务正常。
步骤2:启动前端
# 在另一个终端 pnpm install # 注意:必须用pnpm,lock文件是pnpm格式 pnpm devUmi会启动在http://localhost:8001,自动代理/api请求到后端。
验证关键路径:
1. 打开http://localhost:8001,在搜索框输入“豆腐 菠菜 鸡蛋 孩子3岁 花生过敏 补铁”
2. 查看浏览器Network,确认/api/recommend返回200,响应体含reasoningPath数组
3. 检查Console,应看到[GRAPH] Query executed: MATCH (i:Ingredient)...日志
实操心得:如果前端报
504 Gateway Timeout,大概率是后端LLM调用超时。临时解决方案:编辑main.py,将LLM_TIMEOUT=30改为10,或注释掉LLM调用,启用rule_based_fallback。真正的解决方法是配置好LLM API KEY(见config.py)。
4.2 知识图谱数据加载:从零构建你的领域知识
图谱数据默认为空,需手动加载。data/knowledge/目录下有load_graph.py脚本:
python data/knowledge/load_graph.py --ttl-dir data/knowledge/ --db-path data/graph.db该脚本会:
- 解析所有.ttl文件,构建RDF三元组;
- 将三元组存入SQLite(data/graph.db),使用rdflib的SQLAlchemy后端;
- 创建索引:CREATE INDEX idx_ingredient_name ON ingredients(name);
自定义知识扩展:想添加新食材?编辑data/knowledge/ingredients.ttl,追加:
ex:quinoa a ex:Ingredient ; ex:name "藜麦" ; ex:proteinContent "14g/100g" ; ex:giIndex "53" ; ex:allergen ex:none .然后重新运行load_graph.py。注意ex:giIndex必须是数字,字符串会导致查询失败——这是图谱schema的硬约束。
4.3 大模型接入配置:支持多后端的灵活切换
LLM配置在config.py:
LLM_CONFIG = { "provider": "openai", # 可选:openai, qwen, glm "model": "gpt-4o-mini", "api_key": os.getenv("OPENAI_API_KEY", ""), "base_url": "https://api.openai.com/v1" }安全实践:API KEY绝不硬编码。启动时:
export OPENAI_API_KEY="sk-xxx" python main.py或使用.env文件(已加入.gitignore)。llm-client包会自动读取环境变量。
本地调试技巧:开发期可用llm-mock模式:
export LLM_MOCK=true python main.py此时LLMGenerator返回预置的JSON样本,绕过真实API调用,加速前端联调。
4.4 一键部署到生产环境
publish.sh是为Linux服务器设计的:
./publish.sh --env prod --host 192.168.1.100 --port 80执行流程:
1.pnpm build生成dist/静态文件;
2.python -m http.server 8000 --directory dist验证前端产物;
3.gunicorn -w 4 -b 0.0.0.0:8000 main:app启动后端(需提前安装gunicorn);
4. 配置Nginx反向代理,将/api指向http://127.0.0.1:8000,静态文件由Nginx直接服务。
生产环境加固:
-publish.sh会自动创建logs/目录并设置权限chmod 755 logs;
- 后端启动前执行ulimit -n 65536,避免高并发时文件描述符耗尽;
- 前端dist/index.html被注入<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline';">,平衡安全性与内联脚本需求。
5. 常见问题排查与避坑指南:那些文档里不会写的实战教训
5.1 图谱查询性能瓶颈:当“查不到”其实是“查太慢”
现象:用户输入后,前端长时间等待,Network显示/api/recommendpending。
排查路径:
1. 查看后端日志,搜索[GRAPH] Query start和[GRAPH] Query end时间戳;
2. 若间隔>2s,执行EXPLAIN命令(SQLite不支持,需改用Neo4j);
3. 最常见原因:ingredients.ttl中食材名未标准化,导致MATCH (i:Ingredient) WHERE i.name CONTAINS '鸡胸'全表扫描。
解决方案:
- 在load_graph.py中增加清洗步骤:name = re.sub(r'[^\w\u4e00-\u9fff]', '', name),移除括号、单位等干扰字符;
- 为name字段建立全文索引:CREATE VIRTUAL TABLE ingredient_fts USING fts5(name); INSERT INTO ingredient_fts SELECT name FROM ingredients;;
- 查询时改用SELECT * FROM ingredient_fts WHERE ingredient_fts MATCH '鸡胸';。
我踩过的坑:曾用
LIKE '%鸡胸%',在10万食材数据下查询耗时8秒。换成FTS5后降至0.03秒。记住:图谱不是数据库,但查询性能必须按数据库标准优化。
5.2 LLM生成结果“幻觉”:当模型编造不存在的食材
现象:返回菜谱中出现“南极磷虾粉”(用户库存没有,图谱也没定义)。
根源:LLM在自由生成时,脱离了图谱约束。我们的三重保险中,第二重(后处理)没覆盖到。
修复方案:
在validate_and_enrich函数中,增加食材白名单校验:
# 获取图谱中所有食材名 all_ingredients = set(self.graph.query("MATCH (i:Ingredient) RETURN i.name")) for item in recipe_json['ingredients']: # 模糊匹配,容错率85% if not any(fuzz.ratio(item['name'], std_name) > 85 for std_name in all_ingredients): # 触发修正:用图谱中最接近的食材替换 closest = max(all_ingredients, key=lambda x: fuzz.ratio(item['name'], x)) logger.warning(f"Ingredient '{item['name']}' replaced with '{closest}'") item['name'] = closest这个逻辑让系统“宁可保守,也不胡说”,比单纯报错更友好。
5.3 前端TypeScript类型错误:当npm install后IDE仍报错
现象:VS Code提示Cannot find module 'xxx',但pnpm run dev能正常启动。
本质:pnpm的硬链接机制导致TypeScript的node_modules解析路径与IDE不一致。
终极解法:
1. 在项目根目录创建.vscode/settings.json:
{ "typescript.preferences.includePackageJsonAutoImports": "auto", "typescript.preferences.useLabelDetails": true, "typescript.suggest.autoImports": true, "typescript.preferences.importModuleSpecifier": "relative" }- 运行
pnpm exec tsc --noEmit验证TS配置; - 如果仍有问题,执行
pnpm store status检查pnpm-store完整性,必要时pnpm store prune清理缓存。
实操心得:这个坑让我花了3小时。根本原因是pnpm 8.x的
--frozen-lockfile在Windows上偶尔失效,导致node_modules链接损坏。解决方案是:永远用pnpm install --strict-peer-dependencies,并在CI中加入pnpm exec tsc --noEmit作为准入检查。
5.4 部署后样式错乱:当CSS在生产环境“消失”
现象:pnpm build后,dist/目录中CSS文件存在,但浏览器加载404。
原因:Umi的publicPath配置与Nginx的location不匹配。
检查清单:
-src/.umirc.ts中publicPath: '/'(根路径);
- Nginx配置中location / { root /path/to/dist; try_files $uri $uri/ /index.html; };
- 关键:try_files必须包含/index.html,否则React Router的history模式会404。
快速验证:在服务器上执行curl -I http://localhost:8000/static/css/xxx.css,看HTTP状态码。如果是404,检查Nginx的root路径是否指向dist目录,而非dist/static。
5.5 安全合规红线:必须规避的三个高风险点
- 用户健康数据存储:
main.py中所有用户profile数据(血糖值、疾病史)均不落库,仅存于内存user_cache中,超时自动清除。publish.sh脚本禁止打包user_cache目录; - LLM输出医疗建议:
llm-client的safe_filter函数会扫描生成文本,移除“应服用XX药物”“建议就医”等表述,替换为“具体用药请遵医嘱”; - 第三方字体版权:
src/assets/fonts/中只保留开源字体(如思源黑体),publish.sh会扫描dist/中所有.woff文件,比对SHA256哈希值,若匹配商业字体库则中止部署。
最后分享一个小技巧:在
src/common/utils.ts里,我写了isHealthyFood(foodName: string): boolean函数,它不调用图谱,而是用预置规则(如含“油炸”“奶油”“糖霜”关键词即返回false)。这个函数在前端快速过滤明显不健康选项,减轻后端压力,也提升用户感知速度——真正的工程智慧,往往藏在这些不起眼的细节里。
本文还有配套的精品资源,点击获取
简介:一套开箱即用的智能菜谱推荐解决方案,结合知识图谱结构化建模与大语言模型生成能力,支持按口味偏好、营养目标、现有食材、饮食禁忌等多维度动态生成个性化菜谱。前端采用React框架开发,已组织好页面路由(pages)、可复用组件(components)、通用工具函数(common)和响应式布局(layouts),集成Umi构建工具与TypeScript类型支持;后端由Python实现核心逻辑,main.py负责知识图谱查询、语义匹配及LLM驱动的菜谱生成调度;配套publish.sh等脚本支持一键部署,pnpm-lock.yaml和package.保障依赖稳定,.umirc.ts和tsconfig.提供标准化开发配置。所有源码、配置文件、构建产物目录(如.umi)和包管理缓存(.pnpm-store)均已就位,适配本地开发调试与生产环境发布,可直接嵌入健康管理App、智能厨电系统或营养服务平台,省去知识建模、推理链路和前后端联调环节。
本文还有配套的精品资源,点击获取