一、 系统核心设计理念

本系统的核心是 “用户画像 × 营养学模型 × 智能算法” 的三元驱动模式。

  1. 精准化: 基于详细的用户画像,提供高度个性化的推荐。

  2. 场景化: 结合用户的时间、地点、季节等场景因素。

  3. 科学化: 所有推荐均基于权威营养学指南和食品数据库。

  4. 互动化: 通过用户反馈持续优化推荐结果,形成闭环。


二、 用户群体分析与核心营养需求(用户画像基础)

这是推荐系统的基石。我们需要为每个群体定义核心特征和营养需求。

受众群体 核心特征与需求 重点关注营养素/食材 推荐场景举例
儿童 生长发育、挑食、食品安全 钙、铁、锌、DHA、蛋白质、维生素 促进骨骼发育的早餐、增强免疫力的加餐
青少年 学业繁重、快速生长、运动量大 高蛋白、复合碳水、钙、B族维生素 备考营养餐、运动后恢复餐、防痘食谱
学生群体 预算有限、时间紧张、食堂为主 经济、便捷、高能量、营养均衡 宿舍快手菜、食堂选餐指南、熬夜宵夜
上班族 久坐、压力大、外卖依赖、亚健康 高纤维、护眼、抗疲劳、低脂低卡 办公室健康零食、快手午餐便当、护肝食谱
孕妇 特殊生理阶段、对食物敏感 叶酸、铁、钙、蛋白质、DHA 缓解孕吐食谱、孕期各阶段营养补充、控糖餐
老年人 代谢慢、慢性病、咀嚼消化能力弱 高钙、优质蛋白、膳食纤维、低钠低脂 三高(高血压/高血脂/高血糖)调理餐、易消化食谱
马拉松运动员 极限能量消耗、肌肉修复 极高碳水、电解质、蛋白质、抗氧化 赛前糖原负荷餐、赛中能量补给、赛后恢复餐
过敏群体 对特定食物(如坚果、海鲜、乳糖)过敏 严格规避过敏原,同时保证营养替代 无麸质食谱、乳糖不耐受替代品、素食者补铁方案

三、 系统架构与技术栈

这是一个典型的微服务架构,可以保证系统的可扩展性和稳定性。

+-------------------+    +-----------------------+
|   前端 Client     | -> |   API Gateway         |
+-------------------+    +----------+------------+
                                    |
                                    v
+-------------------------------------------------------------------------+
|                         后端 Backend (Microservices)                    |
| +-------------+  +-------------+  +--------------+  +-----------------+ |
| | 用户服务    |  | 推荐服务    |  | 内容服务     |  |  数据与分析服务 | |
| | User Service|  | Rec Service |  | Content Service| | Analytics Service| |
| +-------------+  +-------------+  +--------------+  +-----------------+ |
+-------------------------------------------------------------------------+
                                    |
                                    v
+-------------------------------------------------------------------------+
|                           数据层 Data Layer                             |
| +--------------+  +----------------+  +----------------+  +-----------+ |
| |  用户画像DB  |  |  食谱&营养DB  |  |   日志与行为DB |  |  模型DB   | |
| | (MySQL/PostgreSQL) | (MongoDB/ES)  |    (ClickHouse)  |  (Redis)   | |
| +--------------+  +----------------+  +----------------+  +-----------+ |
+-------------------------------------------------------------------------+

技术栈选型:

  • 前端: React Native / Flutter(跨端App),Vue.js(管理后台)

  • 后端: Python(Django/FastAPI)或 Java(Spring Boot)。Python在数据科学和AI集成上更有优势,是本项目的首选。

  • 数据库:

    • MySQL/PostgreSQL: 存储用户基本信息、关系型数据。

    • MongoDB/Elasticsearch: 存储非结构化的食谱、文章内容,便于复杂查询。

    • Redis: 缓存热点数据、用户会话、推荐结果。

    • ClickHouse: 存储用户行为日志,用于大数据分析。

  • 算法与AI:

    • 协同过滤: “喜欢A食谱的人也喜欢B食谱”。

    • 内容基于推荐: 根据食谱的标签(高蛋白、低卡)与用户画像匹配。

    • 知识图谱: 构建“用户-症状-营养素-食材-食谱”之间的关系网络,实现更深度的推理推荐。

  • 部署与运维: Docker, Kubernetes, Nginx, CI/CD(Jenkins/GitLab CI)


四、 核心功能模块设计

1. 用户画像系统
  • 注册问卷: 用户首次使用时,通过精心设计的问卷收集信息:

    • 基础信息:年龄、性别、身高、体重、目标(增肌/减脂/维持健康)。

    • 所属群体:从预设的七大群体中选择。

    • 健康状况:慢性病(三高)、过敏原、饮食习惯(素食/清真)。

    • 生活方式:活动水平、睡眠时间、压力水平、烹饪条件。

  • 动态画像: 通过用户日常的浏览、收藏、评分、分享行为,持续更新其偏好。

2. 内容(食谱)管理系统
  • 结构化食谱数据: 每个食谱包含:标题、图片、食材清单、步骤、烹饪时间、难度。

  • 营养标签: 自动计算或人工标注每个食谱的:卡路里、蛋白质、脂肪、碳水化合物、钠、钙等含量,并打上“高蛋白”、“低GI”、“补铁”等标签。

  • 关联知识: 关联相关的健康知识文章,如“为什么孕妇需要补充叶酸?”

3. 智能推荐引擎(核心)

推荐策略是混合的,根据不同场景触发不同的算法。

  • 策略一:基于规则的冷启动

    • 当新用户注册或系统对新用户了解不多时,直接根据其选择的“受众群体”和“健康目标”,从内容库中推送符合该群体营养需求的标准化食谱。

  • 策略二:协同过滤

    • Item-CF(物品协同): “购买了鸡胸肉的用户,也经常购买西兰花和糙米”,推荐相关食材或食谱。

    • User-CF(用户协同): “和您画像相似的另一个孕妇用户,喜欢这个缓解孕吐的柠檬姜茶食谱”。

  • 策略三:内容基于推荐

    • 将用户画像(需要补钙)与食谱标签(高钙)进行匹配。这是实现个性化安全性(如规避过敏原)的关键。

  • 策略四:基于知识图谱的推荐

    • 构建一个图谱:用户 -> 有“失眠”症状 -> 需要“镁”和“色氨酸” -> 富含于“香蕉”、“牛奶” -> 关联食谱“香蕉牛奶燕麦粥”

    • 这种方法能提供可解释的推荐,例如:“我们为您推荐香蕉牛奶燕麦粥,因为它富含镁和色氨酸,有助于改善您的睡眠问题。”

  • 推荐结果融合与排序

    • 将以上多种策略产生的推荐结果进行加权融合。

    • 引入多样性机制,避免推荐内容过于单一。

    • 考虑时效性(推荐时令食材)和流行度

4. 个性化推送与交互系统
  • 推送时机: 早餐前推送早餐建议,周末推送需要长时间准备的食谱。

  • 推送渠道: App内消息、短信、电子邮件、微信小程序。

  • 反馈闭环: 提供“喜欢”、“不感兴趣”、“收藏”、“分享”等按钮。用户的负面反馈(“不感兴趣”)对优化算法至关重要。


五、 针对不同群体的推荐策略示例

  1. 孕妇:

    • 触发词: “孕早期”、“孕吐”

    • 推荐逻辑: 内容基于推荐 -> 搜索标签为“缓解孕吐”、“易消化”的食谱,如姜糖、苏打饼干。同时,推送知识文章《孕早期营养注意事项》。

  2. 马拉松运动员:

    • 触发词: “赛前3天”

    • 推荐逻辑: 规则推荐 -> 调用“赛前糖原负荷”规则,推送高碳水化合物、低脂的食谱,如意大利面、米饭、香蕉。

  3. 过敏群体:

    • 触发词: 用户画像中标记“花生过敏”

    • 推荐逻辑: 强规则过滤 -> 在所有推荐流程的第一步,严格过滤掉所有含有花生或可能交叉污染的食谱。这是最高优先级的规则。

  4. 上班族:

    • 触发词: “工作日午餐”、“15分钟”

    • 推荐逻辑: 内容基于 + 协同过滤 -> 推荐标签为“快手”、“便当”、“护眼”的食谱,同时参考其他上班族的高收藏食谱。

系统架构与技术栈

# docker-compose.yml
version: '3.8'
services:
  # 后端服务
  api-gateway:
    build: ./services/gateway
    ports:
      - "8000:8000"
    environment:
      - NODE_ENV=production
    depends_on:
      - user-service
      - recommendation-service
      - content-service

  user-service:
    build: ./services/user
    environment:
      - DATABASE_URL=postgresql://user:pass@postgres:5432/nutriai
      - REDIS_URL=redis://redis:6379

  recommendation-service:
    build: ./services/recommendation
    environment:
      - ML_MODEL_PATH=/models/nutrition_model.h5

  content-service:
    build: ./services/content
    environment:
      - MONGODB_URI=mongodb://mongo:27017/nutriai

  # 数据库
  postgres:
    image: postgres:14
    environment:
      - POSTGRES_DB=nutriai
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
    volumes:
      - postgres_data:/var/lib/postgresql/data

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data

  mongo:
    image: mongo:6
    volumes:
      - mongo_data:/data/db

  # 前端
  frontend:
    build: ./frontend
    ports:
      - "3000:3000"
    depends_on:
      - api-gateway

volumes:
  postgres_data:
  redis_data:
  mongo_data:

数据库模型设计

-- services/user/schema.sql
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- 用户表
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    email VARCHAR(255) UNIQUE NOT NULL,
    phone VARCHAR(20),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- 用户画像表
CREATE TABLE user_profiles (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID REFERENCES users(id) ON DELETE CASCADE,
    age INTEGER NOT NULL,
    gender VARCHAR(10) CHECK (gender IN ('male', 'female', 'other')),
    height_cm DECIMAL(5,2),
    weight_kg DECIMAL(5,2),
    target_weight_kg DECIMAL(5,2),
    activity_level VARCHAR(20) CHECK (activity_level IN ('sedentary', 'light', 'moderate', 'active', 'very_active')),
    dietary_restrictions TEXT[], -- 过敏原和饮食限制
    health_conditions TEXT[], -- 健康状况
    user_group VARCHAR(50) NOT NULL CHECK (
        user_group IN ('child', 'teenager', 'student', 'office_worker', 
                      'pregnant', 'elderly', 'athlete', 'allergy')
    ),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- 用户目标表
CREATE TABLE user_goals (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    user_id UUID REFERENCES users(id) ON DELETE CASCADE,
    goal_type VARCHAR(50) NOT NULL,
    target_value DECIMAL(10,2),
    current_value DECIMAL(10,2),
    deadline_date DATE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

核心后端服务

API Gateway (Node.js + Express)

// services/gateway/src/app.js
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const { createProxyMiddleware } = require('http-proxy-middleware');

const app = express();

// 安全中间件
app.use(helmet());
app.use(cors());
app.use(express.json());

// 限流
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15分钟
  max: 100 // 限制每个IP每15分钟100次请求
});
app.use(limiter);

// 服务路由
const services = {
  '/users': 'http://user-service:3001',
  '/recommendations': 'http://recommendation-service:3002',
  '/content': 'http://content-service:3003'
};

// 设置代理中间件
Object.keys(services).forEach(route => {
  app.use(route, createProxyMiddleware({
    target: services[route],
    changeOrigin: true,
    pathRewrite: {
      [`^${route}`]: ''
    },
    onError: (err, req, res) => {
      console.error(`Proxy error for ${route}:`, err);
      res.status(503).json({
        error: 'Service temporarily unavailable',
        message: `The ${route.slice(1)} service is currently unavailable`
      });
    }
  }));
});

// 健康检查端点
app.get('/health', (req, res) => {
  res.status(200).json({
    status: 'OK',
    timestamp: new Date().toISOString(),
    uptime: process.uptime()
  });
});

// 错误处理中间件
app.use((err, req, res, next) => {
  console.error('Global error handler:', err);
  res.status(500).json({
    error: 'Internal server error',
    message: process.env.NODE_ENV === 'production' 
      ? 'Something went wrong' 
      : err.message
  });
});

const PORT = process.env.PORT || 8000;
app.listen(PORT, () => {
  console.log(`API Gateway running on port ${PORT}`);
});

用户服务 (Python + FastAPI)

# services/user/app/main.py
from fastapi import FastAPI, Depends, HTTPException, status
from sqlalchemy.orm import Session
from typing import List, Optional
import uuid
from datetime import datetime

from .database import get_db, engine
from . import models, schemas, crud, auth

models.Base.metadata.create_all(bind=engine)

app = FastAPI(title="User Service", version="1.0.0")

@app.post("/users/", response_model=schemas.User)
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
    db_user = crud.get_user_by_email(db, email=user.email)
    if db_user:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Email already registered"
        )
    return crud.create_user(db=db, user=user)

@app.get("/users/{user_id}", response_model=schemas.User)
def read_user(user_id: uuid.UUID, db: Session = Depends(get_db)):
    db_user = crud.get_user(db, user_id=user_id)
    if db_user is None:
        raise HTTPException(status_code=404, detail="User not found")
    return db_user

@app.post("/users/{user_id}/profile", response_model=schemas.UserProfile)
def create_user_profile(
    user_id: uuid.UUID, 
    profile: schemas.UserProfileCreate,
    db: Session = Depends(get_db)
):
    return crud.create_user_profile(db=db, user_id=user_id, profile=profile)

@app.get("/users/{user_id}/recommendation-profile")
def get_recommendation_profile(user_id: uuid.UUID, db: Session = Depends(get_db)):
    """为推荐系统提供用户画像数据"""
    user_profile = crud.get_user_profile(db, user_id=user_id)
    if not user_profile:
        raise HTTPException(status_code=404, detail="User profile not found")
    
    return {
        "user_id": user_id,
        "user_group": user_profile.user_group,
        "dietary_restrictions": user_profile.dietary_restrictions or [],
        "health_conditions": user_profile.health_conditions or [],
        "age": user_profile.age,
        "gender": user_profile.gender,
        "bmi": calculate_bmi(user_profile.height_cm, user_profile.weight_kg),
        "activity_level": user_profile.activity_level
    }

def calculate_bmi(height_cm: float, weight_kg: float) -> Optional[float]:
    if height_cm and weight_kg:
        height_m = height_cm / 100
        return round(weight_kg / (height_m ** 2), 1)
    return None

# services/user/app/schemas.py
from pydantic import BaseModel, EmailStr, validator
from typing import Optional, List
import uuid
from datetime import datetime

class UserBase(BaseModel):
    email: EmailStr
    phone: Optional[str] = None

class UserCreate(UserBase):
    pass

class User(UserBase):
    id: uuid.UUID
    created_at: datetime
    updated_at: datetime

    class Config:
        orm_mode = True

class UserProfileBase(BaseModel):
    age: int
    gender: str
    height_cm: Optional[float] = None
    weight_kg: Optional[float] = None
    target_weight_kg: Optional[float] = None
    activity_level: str
    dietary_restrictions: Optional[List[str]] = None
    health_conditions: Optional[List[str]] = None
    user_group: str

    @validator('user_group')
    def validate_user_group(cls, v):
        allowed_groups = ['child', 'teenager', 'student', 'office_worker', 
                         'pregnant', 'elderly', 'athlete', 'allergy']
        if v not in allowed_groups:
            raise ValueError(f'User group must be one of {allowed_groups}')
        return v

class UserProfileCreate(UserProfileBase):
    pass

class UserProfile(UserProfileBase):
    id: uuid.UUID
    user_id: uuid.UUID
    created_at: datetime
    updated_at: datetime

    class Config:
        orm_mode = True

推荐服务 (Python + ML)

# services/recommendation/app/main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Dict, Any
import numpy as np
import pandas as pd
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
import redis
import json
import logging

app = FastAPI(title="Recommendation Service", version="1.0.0")

# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Redis连接
redis_client = redis.Redis(host='redis', port=6379, decode_responses=True)

class RecommendationRequest(BaseModel):
    user_id: str
    user_profile: Dict[str, Any]
    limit: int = 10
    context: Optional[Dict[str, Any]] = None

class Recipe(BaseModel):
    id: str
    title: str
    ingredients: List[str]
    nutrition_facts: Dict[str, float]
    tags: List[str]
    cooking_time: int
    difficulty: str
    user_group: List[str]

class RecommendationEngine:
    def __init__(self):
        self.vectorizer = TfidfVectorizer(stop_words='english')
        self.recipes_df = self._load_recipes()
        self._build_similarity_matrix()
        
    def _load_recipes(self) -> pd.DataFrame:
        """从数据库加载食谱数据"""
        # 这里应该是从内容服务获取数据
        # 为演示目的,我们创建一些示例数据
        recipes_data = [
            {
                "id": "1",
                "title": "高蛋白鸡胸肉沙拉",
                "ingredients": ["鸡胸肉", "生菜", "番茄", "橄榄油", "柠檬汁"],
                "nutrition_facts": {"calories": 350, "protein": 35, "carbs": 12, "fat": 18},
                "tags": ["高蛋白", "低卡", "健身", "午餐"],
                "cooking_time": 15,
                "difficulty": "简单",
                "user_group": ["athlete", "office_worker"]
            },
            {
                "id": "2", 
                "title": "孕妇营养早餐",
                "ingredients": ["燕麦", "牛奶", "香蕉", "坚果", "蜂蜜"],
                "nutrition_facts": {"calories": 420, "protein": 18, "carbs": 65, "fat": 12},
                "tags": ["孕妇", "早餐", "高钙", "高纤维"],
                "cooking_time": 10,
                "difficulty": "简单", 
                "user_group": ["pregnant"]
            }
        ]
        return pd.DataFrame(recipes_data)
    
    def _build_similarity_matrix(self):
        """构建食谱相似度矩阵"""
        recipe_texts = [
            ' '.join(recipe['tags'] + recipe['ingredients'])
            for _, recipe in self.recipes_df.iterrows()
        ]
        self.tfidf_matrix = self.vectorizer.fit_transform(recipe_texts)
        self.similarity_matrix = cosine_similarity(self.tfidf_matrix, self.tfidf_matrix)
    
    def get_recommendations(self, request: RecommendationRequest) -> List[Recipe]:
        """获取个性化推荐"""
        user_group = request.user_profile['user_group']
        dietary_restrictions = request.user_profile.get('dietary_restrictions', [])
        
        # 过滤符合用户群体的食谱
        filtered_recipes = self.recipes_df[
            self.recipes_df['user_group'].apply(
                lambda x: user_group in x
            )
        ]
        
        # 应用饮食限制
        if dietary_restrictions:
            filtered_recipes = filtered_recipes[
                ~filtered_recipes['ingredients'].apply(
                    lambda ingredients: any(
                        restriction in ' '.join(ingredients) 
                        for restriction in dietary_restrictions
                    )
                )
            ]
        
        # 基于内容的推荐
        recommendations = self._content_based_filtering(
            filtered_recipes, request.user_profile
        )
        
        # 应用业务规则
        recommendations = self._apply_business_rules(
            recommendations, request.user_profile
        )
        
        return recommendations[:request.limit]
    
    def _content_based_filtering(self, recipes: pd.DataFrame, user_profile: Dict) -> List[Recipe]:
        """基于内容的过滤算法"""
        scored_recipes = []
        
        for _, recipe in recipes.iterrows():
            score = 0
            
            # 根据用户群体匹配
            if user_profile['user_group'] in recipe['user_group']:
                score += 3
            
            # 根据营养需求评分
            score += self._calculate_nutrition_score(recipe, user_profile)
            
            # 根据烹饪时间偏好调整分数
            if user_profile.get('activity_level') in ['sedentary', 'light']:
                if recipe['cooking_time'] <= 20:
                    score += 2
            
            scored_recipes.append((recipe, score))
        
        # 按分数排序
        scored_recipes.sort(key=lambda x: x[1], reverse=True)
        return [recipe for recipe, _ in scored_recipes]
    
    def _calculate_nutrition_score(self, recipe: pd.Series, user_profile: Dict) -> float:
        """计算营养匹配分数"""
        score = 0
        nutrition = recipe['nutrition_facts']
        user_group = user_profile['user_group']
        
        # 根据不同用户群体的营养需求评分
        nutrition_rules = {
            'athlete': {
                'protein': (30, 50, 2),  # 最小, 最大, 权重
                'carbs': (40, 70, 1.5)
            },
            'pregnant': {
                'protein': (20, 35, 1.5),
                'calories': (300, 500, 1)
            },
            'elderly': {
                'protein': (15, 30, 2),
                'calories': (200, 400, 1.5)
            }
        }
        
        if user_group in nutrition_rules:
            rules = nutrition_rules[user_group]
            for nutrient, (min_val, max_val, weight) in rules.items():
                if nutrient in nutrition:
                    value = nutrition[nutrient]
                    if min_val <= value <= max_val:
                        score += weight
        
        return score
    
    def _apply_business_rules(self, recipes: List, user_profile: Dict) -> List:
        """应用业务规则"""
        filtered_recipes = []
        
        for recipe in recipes:
            # 排除含有过敏原的食谱
            if self._contains_allergens(recipe, user_profile.get('dietary_restrictions', [])):
                continue
                
            # 根据健康状况过滤
            if not self._is_healthy_for_conditions(recipe, user_profile.get('health_conditions', [])):
                continue
                
            filtered_recipes.append(recipe)
        
        return filtered_recipes
    
    def _contains_allergens(self, recipe: pd.Series, allergens: List[str]) -> bool:
        """检查是否含有过敏原"""
        ingredients_text = ' '.join(recipe['ingredients']).lower()
        return any(allergen.lower() in ingredients_text for allergen in allergens)
    
    def _is_healthy_for_conditions(self, recipe: pd.Series, conditions: List[str]) -> bool:
        """根据健康状况检查食谱适宜性"""
        nutrition = recipe['nutrition_facts']
        
        health_rules = {
            'diabetes': nutrition.get('carbs', 0) < 60,
            'hypertension': nutrition.get('sodium', 0) < 500,
            'obesity': nutrition.get('calories', 0) < 400
        }
        
        for condition in conditions:
            if condition in health_rules and not health_rules[condition]:
                return False
        
        return True

# 初始化推荐引擎
recommendation_engine = RecommendationEngine()

@app.post("/recommendations/")
async def get_recommendations(request: RecommendationRequest):
    """获取个性化膳食推荐"""
    try:
        # 检查缓存
        cache_key = f"recommendations:{request.user_id}"
        cached_result = redis_client.get(cache_key)
        
        if cached_result:
            logger.info(f"Returning cached recommendations for user {request.user_id}")
            return json.loads(cached_result)
        
        # 生成推荐
        recommendations = recommendation_engine.get_recommendations(request)
        
        # 缓存结果(5分钟)
        redis_client.setex(
            cache_key, 
            300,  # 5分钟
            json.dumps([rec.to_dict() for rec in recommendations])
        )
        
        logger.info(f"Generated {len(recommendations)} recommendations for user {request.user_id}")
        return recommendations
        
    except Exception as e:
        logger.error(f"Error generating recommendations: {str(e)}")
        raise HTTPException(status_code=500, detail="Error generating recommendations")

@app.get("/health")
async def health_check():
    return {"status": "healthy", "service": "recommendation"}

前端React组件

// frontend/src/components/UserProfileForm.jsx
import React, { useState } from 'react';
import {
  Box,
  Container,
  Typography,
  TextField,
  Select,
  MenuItem,
  FormControl,
  InputLabel,
  Button,
  Chip,
  Grid,
  Card,
  CardContent
} from '@mui/material';

const USER_GROUPS = {
  child: { name: '儿童', minAge: 3, maxAge: 12 },
  teenager: { name: '青少年', minAge: 13, maxAge: 19 },
  student: { name: '学生', minAge: 18, maxAge: 25 },
  office_worker: { name: '上班族', minAge: 22, maxAge: 60 },
  pregnant: { name: '孕妇', minAge: 20, maxAge: 45 },
  elderly: { name: '老年人', minAge: 60, maxAge: 100 },
  athlete: { name: '运动员', minAge: 18, maxAge: 50 },
  allergy: { name: '过敏人群', minAge: 1, maxAge: 100 }
};

const ALLERGENS = [
  '牛奶', '鸡蛋', '花生', '坚果', '大豆', '小麦', '鱼类', '贝类',
  '芝麻', '芹菜', '芥末', '二氧化硫', '羽扇豆', '软体动物'
];

const HEALTH_CONDITIONS = [
  '糖尿病', '高血压', '高血脂', '心脏病', '肾病', '肝病',
  '肠胃敏感', '骨质疏松', '贫血', '甲状腺问题'
];

const UserProfileForm = () => {
  const [formData, setFormData] = useState({
    age: '',
    gender: '',
    height: '',
    weight: '',
    targetWeight: '',
    activityLevel: '',
    userGroup: '',
    dietaryRestrictions: [],
    healthConditions: []
  });

  const handleSubmit = async (e) => {
    e.preventDefault();
    
    try {
      const response = await fetch('/api/users/profile', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(formData),
      });

      if (response.ok) {
        alert('个人信息保存成功!');
        // 跳转到推荐页面
      } else {
        alert('保存失败,请重试');
      }
    } catch (error) {
      console.error('Error saving profile:', error);
      alert('网络错误,请重试');
    }
  };

  const handleChipSelect = (field, value) => {
    setFormData(prev => ({
      ...prev,
      [field]: prev[field].includes(value)
        ? prev[field].filter(item => item !== value)
        : [...prev[field], value]
    }));
  };

  return (
    <Container maxWidth="md" sx={{ py: 4 }}>
      <Card>
        <CardContent>
          <Typography variant="h4" component="h1" gutterBottom align="center">
            个性化营养配置
          </Typography>
          
          <Box component="form" onSubmit={handleSubmit} sx={{ mt: 3 }}>
            <Grid container spacing={3}>
              <Grid item xs={12} sm={6}>
                <TextField
                  required
                  fullWidth
                  label="年龄"
                  type="number"
                  value={formData.age}
                  onChange={(e) => setFormData({...formData, age: e.target.value})}
                />
              </Grid>
              
              <Grid item xs={12} sm={6}>
                <FormControl fullWidth required>
                  <InputLabel>性别</InputLabel>
                  <Select
                    value={formData.gender}
                    label="性别"
                    onChange={(e) => setFormData({...formData, gender: e.target.value})}
                  >
                    <MenuItem value="male">男</MenuItem>
                    <MenuItem value="female">女</MenuItem>
                    <MenuItem value="other">其他</MenuItem>
                  </Select>
                </FormControl>
              </Grid>

              <Grid item xs={12}>
                <FormControl fullWidth required>
                  <InputLabel>用户群体</InputLabel>
                  <Select
                    value={formData.userGroup}
                    label="用户群体"
                    onChange={(e) => setFormData({...formData, userGroup: e.target.value})}
                  >
                    {Object.entries(USER_GROUPS).map(([key, group]) => (
                      <MenuItem key={key} value={key}>
                        {group.name}
                      </MenuItem>
                    ))}
                  </Select>
                </FormControl>
              </Grid>

              <Grid item xs={12}>
                <Typography variant="h6" gutterBottom>
                  过敏原限制
                </Typography>
                <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
                  {ALLERGENS.map(allergen => (
                    <Chip
                      key={allergen}
                      label={allergen}
                      clickable
                      color={formData.dietaryRestrictions.includes(allergen) ? 
                        'primary' : 'default'}
                      onClick={() => handleChipSelect('dietaryRestrictions', allergen)}
                    />
                  ))}
                </Box>
              </Grid>

              <Grid item xs={12}>
                <Typography variant="h6" gutterBottom>
                  健康状况
                </Typography>
                <Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 1 }}>
                  {HEALTH_CONDITIONS.map(condition => (
                    <Chip
                      key={condition}
                      label={condition}
                      clickable
                      color={formData.healthConditions.includes(condition) ? 
                        'primary' : 'default'}
                      onClick={() => handleChipSelect('healthConditions', condition)}
                    />
                  ))}
                </Box>
              </Grid>

              <Grid item xs={12}>
                <Button
                  type="submit"
                  fullWidth
                  variant="contained"
                  size="large"
                  sx={{ mt: 2 }}
                >
                  开始个性化推荐
                </Button>
              </Grid>
            </Grid>
          </Box>
        </CardContent>
      </Card>
    </Container>
  );
};

export default UserProfileForm;

机器学习模型服务

# services/recommendation/app/ml_models.py
import tensorflow as tf
import numpy as np
from typing import List, Dict
import joblib

class NutritionRecommendationModel:
    def __init__(self):
        self.model = self._build_model()
        self.scaler = joblib.load('/models/scaler.pkl')
        self.label_encoders = joblib.load('/models/label_encoders.pkl')
    
    def _build_model(self):
        """构建深度学习推荐模型"""
        model = tf.keras.Sequential([
            tf.keras.layers.Dense(128, activation='relu', input_shape=(20,)),
            tf.keras.layers.Dropout(0.3),
            tf.keras.layers.Dense(64, activation='relu'),
            tf.keras.layers.Dropout(0.2),
            tf.keras.layers.Dense(32, activation='relu'),
            tf.keras.layers.Dense(1, activation='sigmoid')
        ])
        
        model.compile(
            optimizer='adam',
            loss='binary_crossentropy',
            metrics=['accuracy']
        )
        
        return model
    
    def preprocess_features(self, user_features: Dict, recipe_features: Dict) -> np.ndarray:
        """预处理特征"""
        # 编码分类特征
        encoded_features = []
        
        # 用户特征
        encoded_features.extend([
            user_features['age'] / 100,  # 归一化
            1 if user_features['gender'] == 'male' else 0,
            self.label_encoders['activity_level'].transform([user_features['activity_level']])[0],
            self.label_encoders['user_group'].transform([user_features['user_group']])[0]
        ])
        
        # 食谱特征
        encoded_features.extend([
            recipe_features['calories'] / 1000,
            recipe_features['protein'] / 100,
            recipe_features['carbs'] / 100,
            recipe_features['fat'] / 100,
            recipe_features['cooking_time'] / 60
        ])
        
        return np.array([encoded_features])
    
    def predict_preference(self, user_features: Dict, recipe_features: Dict) -> float:
        """预测用户对食谱的偏好程度"""
        features = self.preprocess_features(user_features, recipe_features)
        prediction = self.model.predict(features, verbose=0)
        return float(prediction[0][0])

# 高级推荐策略
class AdvancedRecommendationStrategy:
    def __init__(self):
        self.nutrition_model = NutritionRecommendationModel()
        self.knowledge_graph = self._build_knowledge_graph()
    
    def _build_knowledge_graph(self):
        """构建营养学知识图谱"""
        return {
            'nutrient_deficiencies': {
                'fatigue': ['iron', 'b12', 'vitamin_d'],
                'weak_bones': ['calcium', 'vitamin_d'],
                'poor_vision': ['vitamin_a', 'zinc'],
                'immune_weakness': ['vitamin_c', 'zinc', 'vitamin_d']
            },
            'food_sources': {
                'iron': ['spinach', 'red_meat', 'lentils', 'tofu'],
                'calcium': ['milk', 'cheese', 'yogurt', 'tofu', 'broccoli'],
                'vitamin_c': ['oranges', 'strawberries', 'bell_peppers', 'broccoli'],
                'protein': ['chicken', 'fish', 'eggs', 'tofu', 'lentils']
            }
        }
    
    def get_knowledge_based_recommendations(self, user_profile: Dict) -> List[Dict]:
        """基于知识图谱的推荐"""
        recommendations = []
        
        # 分析用户可能的营养需求
        inferred_needs = self._infer_nutritional_needs(user_profile)
        
        for nutrient, priority in inferred_needs.items():
            if priority > 0.7:  # 高优先级需求
                food_sources = self.knowledge_graph['food_sources'].get(nutrient, [])
                recommendations.extend(
                    self._find_recipes_with_ingredients(food_sources, user_profile)
                )
        
        return recommendations
    
    def _infer_nutritional_needs(self, user_profile: Dict) -> Dict[str, float]:
        """推断用户营养需求"""
        needs = {}
        user_group = user_profile['user_group']
        health_conditions = user_profile.get('health_conditions', [])
        
        # 基于用户群体的基础需求
        group_needs = {
            'child': {'protein': 0.8, 'calcium': 0.9, 'iron': 0.7},
            'teenager': {'protein': 0.9, 'calcium': 0.8, 'iron': 0.8},
            'pregnant': {'protein': 0.9, 'iron': 0.9, 'calcium': 0.8, 'folic_acid': 1.0},
            'elderly': {'protein': 0.8, 'calcium': 0.9, 'vitamin_d': 0.9},
            'athlete': {'protein': 1.0, 'carbs': 0.9, 'electrolytes': 0.8}
        }
        
        needs.update(group_needs.get(user_group, {}))
        
        # 基于健康状况调整
        condition_adjustments = {
            'anemia': {'iron': 1.0, 'vitamin_c': 0.8},
            'osteoporosis': {'calcium': 1.0, 'vitamin_d': 0.9},
            'diabetes': {'fiber': 0.9, 'complex_carbs': 0.8}
        }
        
        for condition in health_conditions:
            if condition in condition_adjustments:
                needs.update(condition_adjustments[condition])
        
        return needs

部署配置和监控

# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: recommendation-service
  labels:
    app: recommendation-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: recommendation-service
  template:
    metadata:
      labels:
        app: recommendation-service
    spec:
      containers:
      - name: recommendation-service
        image: nutriai/recommendation-service:latest
        ports:
        - containerPort: 3002
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: database-url
        - name: REDIS_URL
          value: "redis://redis-service:6379"
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 3002
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 3002
          initialDelaySeconds: 5
          periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
  name: recommendation-service
spec:
  selector:
    app: recommendation-service
  ports:
  - port: 3002
    targetPort: 3002
  type: ClusterIP
  1. 微服务架构 - 可扩展、可维护

  2. 机器学习推荐 - 个性化智能推荐

  3. 实时数据处理 - Redis缓存和消息队列

  4. 健康监控 - 完善的健康检查机制

  5. 安全防护 - 输入验证、速率限制

  6. 前端界面 - 现代化的用户界面

  7. 容器化部署 - Docker和Kubernetes支持

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐