- {'V' if calorie_diff_ok else '!'} 热量控制{'良好' if calorie_diff_ok else '需注意'}
- {'V' if steps_ok else '!'} 步数{'达标' if steps_ok else '可提高'}
- V 连续达标 {streak_days} 天
-
{advice_html}
#!/usr/bin/env python3 """ AI陪伴减肥 - 核心计算引擎 v2.0 功能: 热量计算、BMR计算、脂肪增量、目标步数、膳食评价、蹲起消耗、围餐计算、食物估算 输出: JSON格式供智能体解析 """ import argparse import json import math import re from typing import Dict, List, Any, Optional, Tuple # ============================================================ # 食物热量数据库(100g标准单位) # ============================================================ FOOD_DATABASE = { # 主食类 '米饭': {'cal': 116, 'carb': 25.9, 'unit': '小碗(150g)', 'small': 174, 'medium': 232, 'large': 348}, '面条': {'cal': 284, 'carb': 59.5, 'unit': '小碗(200g)', 'small': 340, 'medium': 568, 'large': 852}, '馒头': {'cal': 223, 'carb': 47.0, 'unit': '个(100g)', 'small': 167, 'medium': 223, 'large': 335}, '包子': {'cal': 218, 'carb': 25.0, 'unit': '个(80g)', 'small': 174, 'medium': 218, 'large': 327}, '饺子': {'cal': 242, 'carb': 26.0, 'unit': '10个(120g)', 'small': 145, 'medium': 242, 'large': 363}, '煎饼': {'cal': 298, 'carb': 50.0, 'unit': '个(100g)', 'small': 224, 'medium': 298, 'large': 447}, '油条': {'cal': 386, 'carb': 51.0, 'unit': '根(50g)', 'small': 193, 'medium': 386, 'large': 579}, '面包': {'cal': 246, 'carb': 41.0, 'unit': '片(50g)', 'small': 123, 'medium': 246, 'large': 369}, '蛋糕': {'cal': 350, 'carb': 45.0, 'unit': '块(100g)', 'small': 175, 'medium': 350, 'large': 525}, # 蛋白质类 '鸡胸肉': {'cal': 133, 'protein': 31.0, 'unit': '块(150g)', 'small': 100, 'medium': 200, 'large': 300}, '鸡腿': {'cal': 181, 'protein': 26.0, 'unit': '个(120g)', 'small': 109, 'medium': 217, 'large': 325}, '牛肉': {'cal': 106, 'protein': 20.0, 'unit': '份(100g)', 'small': 80, 'medium': 160, 'large': 240}, '猪肉': {'cal': 143, 'protein': 21.0, 'unit': '份(100g)', 'small': 107, 'medium': 214, 'large': 321}, '三文鱼': {'cal': 183, 'protein': 22.0, 'unit': '份(100g)', 'small': 138, 'medium': 275, 'large': 412}, '虾': {'cal': 85, 'protein': 18.0, 'unit': '份(100g)', 'small': 64, 'medium': 128, 'large': 192}, '鸡蛋': {'cal': 144, 'protein': 13.0, 'unit': '个(60g)', 'small': 72, 'medium': 144, 'large': 216}, '豆腐': {'cal': 81, 'protein': 8.0, 'unit': '块(150g)', 'small': 91, 'medium': 122, 'large': 182}, # 蔬菜类 '西兰花': {'cal': 34, 'carb': 4.3, 'unit': '份(100g)', 'small': 25, 'medium': 51, 'large': 76}, '菠菜': {'cal': 24, 'carb': 3.6, 'unit': '份(100g)', 'small': 18, 'medium': 36, 'large': 54}, '青菜': {'cal': 14, 'carb': 1.5, 'unit': '份(100g)', 'small': 11, 'medium': 21, 'large': 32}, '黄瓜': {'cal': 15, 'carb': 2.9, 'unit': '根(200g)', 'small': 18, 'medium': 30, 'large': 45}, '番茄': {'cal': 18, 'carb': 3.5, 'unit': '个(150g)', 'small': 14, 'medium': 27, 'large': 40}, '土豆': {'cal': 76, 'carb': 17.0, 'unit': '个(150g)', 'small': 86, 'medium': 114, 'large': 171}, '红薯': {'cal': 99, 'carb': 20.0, 'unit': '个(150g)', 'single': 149}, '玉米': {'cal': 112, 'carb': 23.0, 'unit': '根(150g)', 'single': 168}, # 炒菜类 '番茄炒蛋': {'cal': 120, 'carb': 5.0, 'unit': '份(200g)', 'small': 120, 'medium': 180, 'large': 240}, '青椒肉丝': {'cal': 180, 'carb': 6.0, 'unit': '份(200g)', 'small': 135, 'medium': 270, 'large': 405}, '宫保鸡丁': {'cal': 210, 'carb': 8.0, 'unit': '份(200g)', 'small': 158, 'medium': 315, 'large': 473}, '红烧肉': {'cal': 280, 'carb': 5.0, 'unit': '3块(100g)', 'small': 93, 'medium': 280, 'large': 420}, '清炒时蔬': {'cal': 60, 'carb': 4.0, 'unit': '份(200g)', 'small': 45, 'medium': 90, 'large': 135}, '地三鲜': {'cal': 150, 'carb': 15.0, 'unit': '份(200g)', 'small': 113, 'medium': 225, 'large': 338}, # 水果类 '苹果': {'cal': 52, 'carb': 13.8, 'unit': '个(200g)', 'small': 52, 'medium': 104, 'large': 156}, '香蕉': {'cal': 93, 'carb': 22.8, 'unit': '根(100g)', 'small': 70, 'medium': 93, 'large': 140}, '橙子': {'cal': 47, 'carb': 11.8, 'unit': '个(150g)', 'small': 35, 'medium': 71, 'large': 106}, '西瓜': {'cal': 30, 'carb': 7.5, 'unit': '块(500g)', 'small': 150, 'medium': 300, 'large': 600, 'basketball': 750}, '葡萄': {'cal': 67, 'carb': 17.3, 'unit': '10颗(50g)', 'small': 34, 'medium': 67, 'large': 101}, '草莓': {'cal': 32, 'carb': 7.7, 'unit': '10颗(150g)', 'small': 24, 'medium': 48, 'large': 72}, # 饮品类 '牛奶': {'cal': 54, 'carb': 3.4, 'unit': '盒(250ml)', 'small': 54, 'medium': 108, 'large': 162}, '酸奶': {'cal': 72, 'carb': 10.0, 'unit': '杯(200ml)', 'small': 72, 'medium': 144, 'large': 216}, '奶茶': {'cal': 80, 'carb': 12.0, 'unit': '杯(500ml)', 'small': 60, 'medium': 120, 'large': 180}, '可乐': {'cal': 42, 'carb': 10.6, 'unit': '罐(330ml)', 'single': 139}, # 火锅类(按人头估算) '火锅': {'cal': 600, 'carb': 20.0, 'unit': '人', 'small': 450, 'medium': 600, 'large': 800}, '麻辣烫': {'cal': 400, 'carb': 30.0, 'unit': '份(500g)', 'small': 300, 'medium': 400, 'large': 600}, # 零食类 '薯片': {'cal': 548, 'carb': 50.0, 'unit': '小包(30g)', 'single': 165}, '饼干': {'cal': 435, 'carb': 70.0, 'unit': '片(10g)', 'single': 44}, '瓜子': {'cal': 597, 'carb': 17.0, 'unit': '把(20g)', 'single': 119}, '花生': {'cal': 589, 'carb': 20.0, 'unit': '把(15g)', 'single': 88}, '巧克力': {'cal': 550, 'carb': 60.0, 'unit': '块(30g)', 'single': 165}, # 快餐类 '披萨': {'cal': 266, 'carb': 33.0, 'unit': '片(100g)', 'small': 200, 'medium': 266, 'large': 400, 'quarter': 66}, '炸鸡': {'cal': 298, 'carb': 10.0, 'protein': 26.0, 'unit': '块(100g)', 'small': 150, 'medium': 298, 'large': 450}, '汉堡': {'cal': 295, 'carb': 24.0, 'protein': 17.0, 'unit': '个(100g)', 'small': 220, 'medium': 295, 'large': 400}, '薯条': {'cal': 312, 'carb': 41.0, 'unit': '份(100g)', 'small': 200, 'medium': 312, 'large': 450}, '炸薯条': {'cal': 312, 'carb': 41.0, 'unit': '份(100g)', 'small': 200, 'medium': 312, 'large': 450}, # 甜品类 '冰淇淋': {'cal': 207, 'carb': 24.0, 'unit': '份(100g)', 'small': 100, 'medium': 207, 'large': 310}, '布丁': {'cal': 130, 'carb': 22.0, 'unit': '杯(100g)', 'single': 130}, '蛋挞': {'cal': 277, 'carb': 30.0, 'unit': '个(50g)', 'single': 139}, } # 份量关键词映射 PORTION_KEYWORDS = { # 小份 '小': 'small', '小份': 'small', '小碗': 'small', '小个': 'small', '小把': 'small', # 中份 '中': 'medium', '中份': 'medium', '中碗': 'medium', '中个': 'medium', # 大份 - 包含篮球等特殊描述 '大': 'large', '大份': 'large', '大碗': 'large', '大个': 'large', '一大': 'large', '篮球': 'large', '篮球大小': 'large', '篮球大的': 'large', '一个': 'medium', # 分数描述 '四分之一': 'quarter', '四分之': 'quarter', '1/4': 'quarter', '¼': 'quarter', } # 饱腹感系数 SATIETY_FACTORS = { 6: 0.6, '六分饱': 0.6, '六成饱': 0.6, 7: 0.7, '七分饱': 0.7, '七成饱': 0.7, 8: 0.8, '八分饱': 0.8, '八成饱': 0.8, '八成': 0.8, 9: 0.9, '九分饱': 0.9, '九成饱': 0.9, 10: 1.0, '十分饱': 1.0, '十成饱': 1.0, '吃撑': 1.1, } # 数量词处理 NUMBER_MAP = { '一': 1, '二': 2, '两': 2, '三': 3, '四': 4, '五': 5, '六': 6, '七': 7, '八': 8, '九': 9, '十': 10, '半': 0.5, '个': 1, '份': 1, '碗': 1, '杯': 1, '根': 1, '块': 1, } def parse_number(text: str) -> float: """解析中文数字""" for cn, num in NUMBER_MAP.items(): text = text.replace(cn, str(num)) try: return float(text) except ValueError: return 1.0 # ============================================================ # 运动消耗数据库(单位:kcal/小时,基于60kg体重) # ============================================================ EXERCISE_DATABASE = { '走路': { '慢走': {'cal_per_hour': 200, 'speed': '4km/h', 'emoji': '🚶'}, '快走': {'cal_per_hour': 350, 'speed': '6km/h', 'emoji': '🚶♂️'}, '暴走': {'cal_per_hour': 450, 'speed': '8km/h', 'emoji': '💨'}, }, '跑步': { '慢跑': {'cal_per_hour': 450, 'speed': '8km/h', 'emoji': '🏃'}, '快跑': {'cal_per_hour': 600, 'speed': '12km/h', 'emoji': '🏃♂️'}, }, '骑行': { '骑行': {'cal_per_hour': 400, 'speed': '15km/h', 'emoji': '🚴'}, '动感单车': {'cal_per_hour': 500, 'emoji': '🚴♂️'}, }, '游泳': { '蛙泳': {'cal_per_hour': 450, 'emoji': '🏊'}, '自由泳': {'cal_per_hour': 550, 'emoji': '🏊♀️'}, '慢游': {'cal_per_hour': 350, 'emoji': '🏊♂️'}, }, '球类': { '羽毛球': {'cal_per_hour': 350, 'emoji': '🏸'}, '乒乓球': {'cal_per_hour': 280, 'emoji': '🏓'}, '篮球': {'cal_per_hour': 450, 'emoji': '🏀'}, '足球': {'cal_per_hour': 500, 'emoji': '⚽'}, '网球': {'cal_per_hour': 400, 'emoji': '🎾'}, }, '健身': { '跳绳': {'cal_per_hour': 600, 'emoji': '🪢'}, '瑜伽': {'cal_per_hour': 150, 'emoji': '🧘'}, '普拉提': {'cal_per_hour': 250, 'emoji': '🧘♀️'}, ' HIIT': {'cal_per_hour': 550, 'emoji': '💪'}, '健身操': {'cal_per_hour': 400, 'emoji': '💃'}, }, '力量': { '蹲起': {'cal_per_hour': 300, 'emoji': '🏋️'}, '俯卧撑': {'cal_per_hour': 250, 'emoji': '💪'}, '平板支撑': {'cal_per_hour': 200, 'emoji': '🤸'}, '引体向上': {'cal_per_hour': 350, 'emoji': '🏋️♂️'}, }, '日常': { '爬楼梯': {'cal_per_hour': 500, 'emoji': '🪜'}, '家务': {'cal_per_hour': 200, 'emoji': '🧹'}, '逛街': {'cal_per_hour': 250, 'emoji': '🛍️'}, }, } def calculate_exercise_calories(exercise_type: str, duration_min: int, weight_kg: float = 60) -> Dict: """ 计算运动消耗热量 返回: 消耗热量(kcal)、等效步数 """ # 查找运动类型 exercise_data = None for category, exercises in EXERCISE_DATABASE.items(): if exercise_type in exercises: exercise_data = exercises[exercise_type] break if not exercise_data: return {'found': False, 'calories': 0, 'equivalent_steps': 0} # 按体重调整 weight_factor = weight_kg / 60 base_cal_per_hour = exercise_data['cal_per_hour'] actual_cal = base_cal_per_hour * weight_factor * duration_min / 60 # 计算等效步数(以快走为基准,约每步0.04kcal) steps_per_hour = 6000 # 快走速度 calories_per_step = 0.04 * (weight_kg / 60) equivalent_steps = int(actual_cal / calories_per_step) return { 'found': True, 'name': exercise_type, 'emoji': exercise_data.get('emoji', '🏃'), 'duration_min': duration_min, 'calories': round(actual_cal, 1), 'equivalent_steps': equivalent_steps, 'weight_factor': round(weight_factor, 2) } def get_exercise_equivalents(target_calories: float, weight_kg: float = 60) -> List[Dict]: """ 获取等效运动方案 消耗指定热量需要做哪些运动 """ equivalents = [] weight_factor = weight_kg / 60 for category, exercises in EXERCISE_DATABASE.items(): for name, data in exercises.items(): cal_per_hour = data['cal_per_hour'] * weight_factor duration_needed = target_calories / cal_per_hour * 60 # 分钟 if duration_needed <= 120: # 最多2小时 equivalents.append({ 'name': name, 'category': category, 'emoji': data.get('emoji', '🏃'), 'duration_min': int(duration_needed), 'calories': target_calories }) # 按耗时排序 equivalents.sort(key=lambda x: x['duration_min']) return equivalents[:10] # 返回前10个最优方案 def steps_to_exercise(steps: int, weight_kg: float = 60) -> Dict[str, str]: """ 将步数转换为等效运动 """ # 步数消耗的热量 cal_per_step = 0.04 * (weight_kg / 60) total_cal = steps * cal_per_step # 推荐的等效运动 recommendations = [] for category, exercises in EXERCISE_DATABASE.items(): for name, data in exercises.items(): cal_per_hour = data['cal_per_hour'] * weight_kg / 60 duration = total_cal / cal_per_hour * 60 if 10 <= duration <= 90: # 10分钟到1.5小时 recommendations.append({ 'name': name, 'emoji': data.get('emoji', '🏃'), 'duration': int(duration) }) return { 'steps': steps, 'calories': round(total_cal, 1), 'recommendations': recommendations[:5] } def estimate_food(food_text: str) -> Tuple[float, float, float, List[str]]: """ 估算食物热量 返回: (热量kcal, 碳水g, 蛋白质g, 匹配信息) """ total_cal = 0 total_carb = 0 total_protein = 0 matched = [] text = food_text.lower() # 尝试匹配火锅/围餐 if '火锅' in text: matched.append('火锅') portions = {'small': 450, 'medium': 600, 'large': 800} portion = 'medium' for kw, size in PORTION_KEYWORDS.items(): if kw in text: portion = size break total_cal = portions.get(portion, 600) total_carb = 20 # 尝试匹配数据库食物 for name, data in FOOD_DATABASE.items(): if name in text: cal = data.get('cal', 0) # 判断份量 portion_key = 'medium' # 默认中份 for kw, size in PORTION_KEYWORDS.items(): if kw in text: portion_key = size break # 特殊处理:西瓜+篮球描述 → 使用basketball份量 if 'basketball' in data and ('篮球' in text or '西瓜' in text and '大' in text): portion_key = 'basketball' # 处理数量 num_match = re.search(r'([零一二两三四五六七八九十半]+|[0-9.]+)\s*[个碗份杯根块把]?', text) quantity = 1 if num_match: quantity = parse_number(num_match.group(1)) # 处理分数描述(如四分之一披萨) if portion_key == 'quarter' and 'quarter' in data: food_cal = data['quarter'] elif 'single' in data: food_cal = data['single'] * quantity else: food_cal = data.get(portion_key, data.get('cal', 0)) * quantity total_cal += food_cal total_carb += data.get('carb', 0) * quantity / 10 total_protein += data.get('protein', 0) * quantity / 10 matched.append(f"{name}({portion_key}×{quantity})") # 如果没有匹配到,返回默认估算 if total_cal == 0: # 通用估算:每餐约500-800kcal matched.append('通用估算') total_cal = 600 return total_cal, total_carb, total_protein, matched def calculate_step_length(height_cm: float) -> float: """计算步长(cm) - 优化公式""" return height_cm / 4 + 30 def calculate_bmr(weight_kg: float, height_cm: float, age: int, gender: str) -> float: """计算基础代谢率(BMR)""" if gender.lower() in ['female', 'f', '女']: return 655 + 9.6 * weight_kg + 1.8 * height_cm - 4.7 * age else: return 66 + 13.7 * weight_kg + 5 * height_cm - 6.8 * age def calculate_tdee(bmr: float, activity_level: str = 'sedentary') -> float: """计算每日总消耗(TDEE) 活动系数: - 久坐(sedentary): 1.2 - 轻度活动(light): 1.375 - 中度活动(moderate): 1.55 - 高强度(active): 1.725 """ activity_coefficients = { 'sedentary': 1.2, # 久坐少动 'light': 1.375, # 轻度活动 'moderate': 1.55, # 中度活动 'active': 1.725 # 高强度 } coeff = activity_coefficients.get(activity_level, 1.2) return bmr * coeff def calculate_calorie_diff(total_calories: float, tdee: float) -> float: """计算净热量差: 净热量差 = 摄入总热量 × 0.9 - TDEE 考虑食物热效应(TEF约10%)后的净热量 """ net_calories = total_calories * 0.9 # 扣除食物热效应 return net_calories - tdee def calculate_fat_change(calorie_diff: float) -> float: """计算脂肪增量(克),负数表示减脂 公式: F = 净热量差 / 7700 × 1000 依据: 1kg脂肪 ≈ 7700kcal """ return calorie_diff / 7700 * 1000 def calculate_calories_per_1000_steps(weight_kg: float) -> float: """计算每千步消耗热量 公式: 体重kg × 0.42 """ return weight_kg * 0.42 def calculate_target_steps(calorie_diff: float, weight_kg: float, max_steps: int = 12000) -> Dict[str, Any]: """计算目标步数(热量差→步数闭环核心) 【核心逻辑】昨日热量差 = 今日目标步数 热量差的来源: - 热量差 = 昨日摄入热量 × 0.9(TEF) - TDEE 热量差 → 步数转换: - 每千步消耗 = 体重 × 0.42 kcal - 热量差 > 0(超标):需要额外走路消耗 - 热量差 ≤ 0(缺口/平衡):只需基础保障步数 公式: - 热量差 < 0(减脂模式): 目标 = 基础步数(6000) - 热量差 = 0(平衡模式): 目标 = 基础步数(6000) - 热量差 > 0(补偿模式): 目标 = 基础步数 + 额外步数 参数: calorie_diff: 净热量差(C0 = 摄入×0.9 - TDEE) weight_kg: 体重(kg) max_steps: 最大步数上限(默认12000步≈8公里) 返回: 字典包含: - target_steps: 目标步数 - base_steps: 基础保障步数 - extra_steps: 额外需要的步数 - mode: 模式(reduce/balance/compensate) - calories_per_1000_steps: 每千步消耗热量 - calories_to_burn: 需要消耗的热量(正数) """ base_steps = 6000 # 基础保障步数 calories_per_1000_steps = weight_kg * 0.42 # 每千步消耗 if calorie_diff <= 0: # 热量缺口或平衡:只需基础步数 return { 'target_steps': base_steps, 'base_steps': base_steps, 'extra_steps': 0, 'mode': 'reduce' if calorie_diff < 0 else 'balance', 'calories_per_1000_steps': round(calories_per_1000_steps, 1), 'calories_to_burn': 0, 'mode_label': '减脂中' if calorie_diff < 0 else '平衡态', 'mode_emoji': '📉' if calorie_diff < 0 else '⚖️', 'reason': '热量缺口良好,基础走路即可' if calorie_diff < 0 else '吃动平衡,保持现状' } else: # 热量超标:基础步数 + 额外步数 extra_1000_steps = calorie_diff / calories_per_1000_steps extra_steps = int(extra_1000_steps * 1000) calculated_steps = base_steps + extra_steps target_steps = min(calculated_steps, max_steps) # 如果超过上限,重新计算实际能消耗的热量 actual_extra_steps = target_steps - base_steps calories_burned = actual_extra_steps * calories_per_1000_steps / 1000 return { 'target_steps': target_steps, 'base_steps': base_steps, 'extra_steps': actual_extra_steps, 'mode': 'compensate', 'calories_per_1000_steps': round(calories_per_1000_steps, 1), 'calories_to_burn': round(calories_burned, 1), 'mode_label': '需补偿', 'mode_emoji': '⚠️', 'reason': f'超标{calorie_diff:.0f}kcal,需走{actual_extra_steps}步消耗', 'calorie_diff': calorie_diff, 'max_reached': calculated_steps > max_steps # 是否触发上限 } def calculate_squat_calories(weight_kg: float, height_m: float) -> float: """计算每次蹲起消耗热量(kcal)""" return 0.00239 * weight_kg * height_m def calculate_squats_needed(calorie_diff: float, squat_calories: float) -> int: """计算达到热量差需要的蹲起次数""" if squat_calories <= 0: return 0 return max(int(abs(calorie_diff) / squat_calories), 0) # ============================================================ # 激励机制函数 # ============================================================ STREAK_BADGES = { 3: {'name': '铜牌战士', 'emoji': '🥉', 'color': '#CD7F32'}, 7: {'name': '银牌达人', 'emoji': '🥈', 'color': '#C0C0C0'}, 14: {'name': '金牌冠军', 'emoji': '🥇', 'color': '#FFD700'}, 30: {'name': '钻石会员', 'emoji': '💎', 'color': '#B9F2FF'}, 60: {'name': '王者段位', 'emoji': '👑', 'color': '#FF69B4'}, 100: {'name': '传奇大师', 'emoji': '🌟', 'color': '#9400D3'}, } def calculate_streak_badge(streak_days: int) -> Dict[str, Any]: """计算连续达标徽章""" current_badge = None next_badge = None progress = 0 # 找到当前和下一个徽章 sorted_milestones = sorted(STREAK_BADGES.keys()) for milestone in sorted_milestones: if streak_days >= milestone: current_badge = STREAK_BADGES[milestone] else: if not next_badge: next_badge = {'days': milestone, **STREAK_BADGES[milestone]} break # 计算进度 if next_badge: # 找到上一个里程碑 prev_milestone = 0 for m in sorted_milestones: if m < next_badge['days']: prev_milestone = m progress = (streak_days - prev_milestone) / (next_badge['days'] - prev_milestone) return { 'current_badge': current_badge, 'next_badge': next_badge, 'progress': round(progress * 100, 1), 'streak_days': streak_days } def generate_weekly_report(daily_records: List[Dict]) -> Dict[str, Any]: """生成周报""" if not daily_records: return {'error': '暂无数据'} total_days = len(daily_records) achieved_days = sum(1 for r in daily_records if r.get('steps_achieved', False)) total_cal_diff = sum(r.get('calorie_diff', 0) for r in daily_records) total_steps = sum(r.get('actual_steps', 0) for r in daily_records) avg_score = sum(r.get('score', 0) for r in daily_records) / total_days if total_days > 0 else 0 # 体重变化 weights = [r.get('weight_morning') for r in daily_records if r.get('weight_morning')] weight_change = 0 if len(weights) >= 2: weight_change = weights[-1] - weights[0] # 最佳/最差日 sorted_by_score = sorted(daily_records, key=lambda x: x.get('score', 0), reverse=True) best_day = sorted_by_score[0] if sorted_by_score else None worst_day = sorted_by_score[-1] if sorted_by_score else None return { 'period': f"{daily_records[0].get('date', '?')} 至 {daily_records[-1].get('date', '?')}", 'total_days': total_days, 'achieved_days': achieved_days, 'achieved_rate': round(achieved_days / total_days * 100, 1) if total_days > 0 else 0, 'total_cal_diff': round(total_cal_diff, 1), 'avg_cal_diff': round(total_cal_diff / total_days, 1) if total_days > 0 else 0, 'total_steps': total_steps, 'avg_steps': int(total_steps / total_days) if total_days > 0 else 0, 'avg_score': round(avg_score, 1), 'weight_change': round(weight_change, 2), 'best_day': best_day, 'worst_day': worst_day, 'streak_days': daily_records[-1].get('streak_days', 0) if daily_records else 0 } def generate_monthly_report(weekly_reports: List[Dict]) -> Dict[str, Any]: """生成月报""" if not weekly_reports: return {'error': '暂无数据'} total_achieved = sum(w.get('achieved_days', 0) for w in weekly_reports) total_days = sum(w.get('total_days', 0) for w in weekly_reports) total_cal_diff = sum(w.get('total_cal_diff', 0) for w in weekly_reports) total_steps = sum(w.get('total_steps', 0) for w in weekly_reports) avg_score = sum(w.get('avg_score', 0) for w in weekly_reports) / len(weekly_reports) if weekly_reports else 0 return { 'total_weeks': len(weekly_reports), 'total_days': total_days, 'total_achieved': total_achieved, 'achieved_rate': round(total_achieved / total_days * 100, 1) if total_days > 0 else 0, 'total_cal_diff': round(total_cal_diff, 1), 'total_steps': total_steps, 'avg_score': round(avg_score, 1), 'weight_change': sum(w.get('weight_change', 0) for w in weekly_reports), 'best_week': max(weekly_reports, key=lambda x: x.get('avg_score', 0)) if weekly_reports else None, 'worst_week': min(weekly_reports, key=lambda x: x.get('avg_score', 0)) if weekly_reports else None } # ============================================================ # 营养补剂数据库 # ============================================================ SUPPLEMENT_DATABASE = { # 基础代谢类 '基础代谢支持': { 'desc': '提升基础代谢,加速燃脂', 'supplements': [ {'name': '左旋肉碱', 'dosage': '2-5g/天', 'timing': '运动前30分钟', 'effect': '促进脂肪燃烧,提升运动表现', 'evidence': '⭐⭐⭐⭐'}, {'name': '咖啡因', 'dosage': '100-200mg/天', 'timing': '早晨或运动前', 'effect': '提升代谢3-11%,抑制食欲', 'evidence': '⭐⭐⭐⭐'}, {'name': '绿茶提取物(EGCG)', 'dosage': '500mg/天', 'timing': '分2次随餐', 'effect': '促进脂肪氧化,增强能量消耗', 'evidence': '⭐⭐⭐'}, ] }, # 食欲控制类 '食欲控制': { 'desc': '减少饥饿感,稳定血糖', 'supplements': [ {'name': '白芸豆提取物', 'dosage': '500-1000mg/餐前', 'timing': '餐前10分钟', 'effect': '阻断淀粉吸收,减少碳水摄入', 'evidence': '⭐⭐⭐'}, {'name': '葡甘露聚糖', 'dosage': '1-3g/餐前', 'timing': '餐前30分钟配大杯水', 'effect': '增加饱腹感,延缓胃排空', 'evidence': '⭐⭐⭐'}, {'name': '5-HTP', 'dosage': '50-100mg/天', 'timing': '睡前或餐前', 'effect': '提升血清素,减少情绪性进食', 'evidence': '⭐⭐'}, ] }, # 肌肉保护类 '肌肉保护': { 'desc': '防止肌肉流失,保护基础代谢', 'supplements': [ {'name': '支链氨基酸(BCAA)', 'dosage': '5-10g/天', 'timing': '运动前后', 'effect': '减少肌肉分解,促进恢复', 'evidence': '⭐⭐⭐⭐'}, {'name': '谷氨酰胺', 'dosage': '5-10g/天', 'timing': '睡前或运动后', 'effect': '维护肠道健康,提升免疫力', 'evidence': '⭐⭐⭐'}, {'name': 'HMB', 'dosage': '3g/天', 'timing': '分3次', 'effect': '抑制肌肉蛋白分解', 'evidence': '⭐⭐⭐'}, ] }, # 营养补充类 '营养补充': { 'desc': '补充减肥期间容易缺乏的营养素', 'supplements': [ {'name': '复合维生素B', 'dosage': '1片/天', 'timing': '早餐后', 'effect': '支持能量代谢,预防疲劳', 'evidence': '⭐⭐⭐⭐⭐'}, {'name': '维生素D3', 'dosage': '2000-4000IU/天', 'timing': '随含脂肪的餐', 'effect': '促进钙吸收,支持肌肉功能', 'evidence': '⭐⭐⭐'}, {'name': 'omega-3鱼油', 'dosage': '1-2g/天', 'timing': '随餐', 'effect': '抗炎、支持心血管健康', 'evidence': '⭐⭐⭐'}, {'name': '镁', 'dosage': '400mg/天', 'timing': '睡前', 'effect': '改善睡眠质量,减少肌肉痉挛', 'evidence': '⭐⭐⭐'}, {'name': '锌', 'dosage': '15-30mg/天', 'timing': '随餐', 'effect': '支持代谢酶功能,维持免疫', 'evidence': '⭐⭐⭐'}, ] }, # 水分代谢类 '水分代谢': { 'desc': '促进体内水分平衡,加速代谢废物排出', 'supplements': [ {'name': '钾', 'dosage': '1000-3500mg/天', 'timing': '分次随餐', 'effect': '调节水分平衡,支持肌肉功能', 'evidence': '⭐⭐⭐'}, {'name': '电解质粉', 'dosage': '按产品说明', 'timing': '大量出汗后', 'effect': '补充钠、钾、镁等电解质', 'evidence': '⭐⭐⭐'}, ] }, # 皮肤紧致类 '皮肤紧致': { 'desc': '减少皮肤松弛,保持弹性', 'supplements': [ {'name': '胶原蛋白肽', 'dosage': '5-10g/天', 'timing': '睡前或运动后', 'effect': '改善皮肤弹性,减少松弛', 'evidence': '⭐⭐⭐'}, {'name': '维生素C', 'dosage': '500-1000mg/天', 'timing': '分2次随餐', 'effect': '促进胶原蛋白合成,抗氧化', 'evidence': '⭐⭐⭐⭐'}, {'name': '透明质酸', 'dosage': '100-200mg/天', 'timing': '随餐', 'effect': '保持皮肤水分,提升弹性', 'evidence': '⭐⭐'}, ] }, # 情绪支持类 '情绪支持': { 'desc': '缓解减肥期间的情绪波动', 'supplements': [ {'name': '南非醉茄', 'dosage': '300-600mg/天', 'timing': '早晨或睡前', 'effect': '降低皮质醇,减少压力性暴食', 'evidence': '⭐⭐⭐'}, {'name': 'L-茶氨酸', 'dosage': '200mg/天', 'timing': '早晨或需要放松时', 'effect': '减轻焦虑,提升专注力', 'evidence': '⭐⭐⭐'}, {'name': '褪黑素', 'dosage': '0.5-3mg/天', 'timing': '睡前30分钟', 'effect': '改善睡眠质量,促进恢复', 'evidence': '⭐⭐⭐'}, ] }, # 运动表现类 '运动表现': { 'desc': '提升运动能力,增加消耗', 'supplements': [ {'name': 'β-丙氨酸', 'dosage': '3-6g/天', 'timing': '分次服用', 'effect': '提升耐力,减少疲劳', 'evidence': '⭐⭐⭐⭐'}, {'name': '肌酸', 'dosage': '5g/天', 'timing': '训练日运动后', 'effect': '增加力量,提升运动表现', 'evidence': '⭐⭐⭐⭐'}, {'name': '牛磺酸', 'dosage': '1-3g/天', 'timing': '运动前', 'effect': '提升运动耐力,促进脂肪燃烧', 'evidence': '⭐⭐⭐'}, ] } } def get_supplement_recommendation(weight_kg: float, bmr: float, score: float, streak_days: int, target_steps: int, actual_steps: int, calorie_diff: float, nutrition_score: float, days_count: int = 0) -> Dict[str, Any]: """ 根据用户状态推荐营养补剂方案 参数: weight_kg: 体重(kg) bmr: 基础代谢率 score: 当日评分 streak_days: 连续达标天数 target_steps: 目标步数 actual_steps: 实际步数 calorie_diff: 热量差 nutrition_score: 营养评分(0-4) days_count: 减肥总天数 返回: 补剂推荐方案 """ recommendations = { 'must_have': [], # 必备 'recommended': [], # 推荐 'optional': [], # 可选 'tips': [] # 小贴士 } # ============================================================ # 科学优化函数(7项) # ============================================================ # -------------------- 1. BMR公式优化 - Katch-McArdle -------------------- def calculate_bmr_advanced(weight_kg: float, height_cm: float, age: int, gender: str, body_fat_percent: float = None) -> float: """计算基础代谢率(高级版,支持体脂率修正) 参数: weight_kg: 体重(kg) height_cm: 身高(cm) age: 年龄 gender: 性别 'male'/'female' body_fat_percent: 体脂率%(可选),如果不提供则使用简化公式 返回: BMR(kcal/天) """ if body_fat_percent is not None and 5 <= body_fat_percent <= 50: # Katch-McArdle公式(更精确,需要体脂率) lean_mass = weight_kg * (1 - body_fat_percent / 100) bmr = 370 + (21.6 * lean_mass) else: # Mifflin-St Jeor公式(简化版,无体脂率) if gender == 'female': bmr = 655 + (9.6 * weight_kg) + (1.8 * height_cm) - (4.7 * age) else: bmr = 66 + (13.7 * weight_kg) + (5 * height_cm) - (6.8 * age) return round(bmr, 1) def estimate_body_fat_from_bmi(bmi: float, age: int, gender: str) -> float: """根据BMI估算体脂率(间接法) 参数: bmi: 体质指数 age: 年龄 gender: 性别 返回: 估算体脂率% """ # Deurenberg公式 if gender == 'female': bf = (1.20 * bmi) + (0.23 * age) - 5.4 else: bf = (1.20 * bmi) + (0.23 * age) - 16.2 # 限制合理范围 return max(5.0, min(50.0, round(bf, 1))) # -------------------- 2. TDEE用步数反推 -------------------- def calculate_tdee_from_steps(weight_kg: float, actual_steps: int, bmr: float) -> float: """根据实际步数反推TDEE 原理:步数反映了日常活动水平 参数: weight_kg: 体重(kg) actual_steps: 实际步数 bmr: 基础代谢率 返回: TDEE(kcal/天) """ # 每日静坐消耗 = BMR × 0.2 (约占20%) sedentary_burn = bmr * 0.2 # 步数消耗(每千步约消耗体重×0.42kcal) steps_burn = actual_steps * (weight_kg * 0.42) / 1000 # TDEE = BMR + 静坐消耗 + 步数消耗 tdee = bmr + sedentary_burn + steps_burn return round(tdee, 1) def estimate_activity_level_from_steps(actual_steps: int) -> float: """根据步数估算活动系数(用于参考) 参数: actual_steps: 日均步数 返回: 活动系数 """ if actual_steps < 3000: return 1.2 # 久坐 elif actual_steps < 6000: return 1.375 # 轻度 elif actual_steps < 10000: return 1.55 # 中度 else: return 1.725 # 高强度 # -------------------- 3. 个性化基础步数 -------------------- def calculate_personalized_base_steps(weight_kg: float, age: int, gender: str) -> int: """计算个性化基础步数 考虑因素: - 年龄:年龄越大,基础步数适当减少 - 体重:体重越大,关节负担越重,适当减少 - 性别:男女有差异 参数: weight_kg: 体重(kg) age: 年龄 gender: 性别 返回: 个性化基础步数 """ base = 6000 # 年龄修正:30岁为基准 if age < 30: age_factor = 1.0 elif age < 45: age_factor = 0.95 elif age < 60: age_factor = 0.85 else: age_factor = 0.75 # 体重修正:60kg为基准 if weight_kg < 50: weight_factor = 1.0 elif weight_kg < 80: weight_factor = 0.95 elif weight_kg < 100: weight_factor = 0.85 else: weight_factor = 0.75 # 性别修正 gender_factor = 1.0 if gender == 'male' else 0.95 # 计算最终值 final_steps = int(base * age_factor * weight_factor * gender_factor) # 限制合理范围 3000-8000 return max(3000, min(8000, final_steps)) # -------------------- 4. 体重预测 - Logistic模型 -------------------- def predict_weight_logistic(weights: List[float], days: List[int], target_weight: float = None) -> Dict[str, Any]: """使用Logistic模型预测体重变化趋势 Logistic模型特点: - 初期快速下降 - 逐渐趋于平稳 - 不会无限下降 参数: weights: 历史体重列表(kg) days: 对应天数(从第1天开始) target_weight: 目标体重(可选) 返回: 预测结果字典 """ if len(weights) < 3: return {'error': '需要至少3天数据', 'predictions': []} current_weight = weights[-1] # 简单Logistic拟合(使用初始体重和当前体重估算参数) initial_weight = weights[0] # 估算饱和值(使用目标体重或当前体重-10kg) if target_weight: saturation = target_weight else: saturation = max(current_weight, initial_weight - 10) # 简单线性估算斜率 if len(weights) >= 2: daily_change = (weights[-1] - weights[0]) / len(weights) else: daily_change = -0.1 # 生成预测 predictions = [] for i in range(1, 29): # 预测未来4周 t = len(weights) + i # 简化的S型曲线 if daily_change < 0: # 减脂趋势 remaining = current_weight - saturation if remaining > 0.1: k = 0.1 # 衰减率 predicted = saturation + remaining * math.exp(-k * i) else: predicted = current_weight else: predicted = current_weight predictions.append({ 'day': i, 'date': f'+{i}天', 'weight': round(predicted, 1) }) # 估算达成目标时间 goal_date = None if target_weight and daily_change < 0: remaining = current_weight - target_weight if remaining > 0: days_needed = int(remaining / abs(daily_change)) if daily_change != 0 else 999 goal_date = f'约{days_needed}天' return { 'current_weight': round(current_weight, 1), 'saturation_weight': round(saturation, 1), 'daily_change': round(daily_change, 3), 'goal_date': goal_date, 'predictions': predictions } # -------------------- 5. 热量估算校准 -------------------- class CalorieCalibrator: """热量估算校准器""" def __init__(self): self.food_calibrations = {} # 食物ID -> 校准因子 self.user_feedback_log = [] # 用户反馈记录 def calibrate(self, food_name: str, reported_calories: float, actual_weight_change: float, days: int) -> float: """根据用户反馈校准热量估算 参数: food_name: 食物名称 reported_calories: 报告的热量 actual_weight_change: 实际体重变化(kg) days: 天数 返回: 校准后的估算因子 """ # 计算每日热量偏差 expected_cal_diff = actual_weight_change * 7700 / days calorie_deviation = expected_cal_diff # 简单校准:调整报告热量 if food_name not in self.food_calibrations: self.food_calibrations[food_name] = 1.0 # 更新校准因子 adjustment = 1.0 - (calorie_deviation / reported_calories) if reported_calories > 0 else 1.0 self.food_calibrations[food_name] = ( 0.7 * self.food_calibrations[food_name] + 0.3 * adjustment ) return self.food_calibrations[food_name] def get_calibrated_calorie(self, food_name: str, base_calorie: float) -> float: """获取校准后的热量""" factor = self.food_calibrations.get(food_name, 1.0) return round(base_calorie * factor, 1) # -------------------- 6. 碳水量精确计算 -------------------- def calculate_carb_from_foods(foods: List[str]) -> Dict[str, float]: """从食物成分表精确计算碳水化合物 参数: foods: 食物名称列表 返回: {'total_carb_g': 总碳水(g), 'carb_ratio': 碳水比例%, 'details': {...}} """ # 常见食物碳水含量(g/100g) CARB_DB = { '米饭': 28.0, '面条': 25.0, '馒头': 22.0, '面包': 40.0, '土豆': 17.0, '红薯': 20.0, '玉米': 19.0, '苹果': 13.0, '香蕉': 22.0, '橙子': 11.0, '鸡胸肉': 0.0, '牛肉': 0.0, '猪肉': 0.0, '鱼': 0.0, '蛋': 0.7, '菠菜': 3.6, '白菜': 2.0, '西兰花': 4.3, '西红柿': 3.5, '牛奶': 5.0, '酸奶': 9.0, '豆浆': 1.8, '油': 0.0, '肥肉': 0.0, } # 份量估算(g) PORTION_EST = { '米饭': 150, '面条': 200, '馒头': 100, '面包': 50, '土豆': 150, '红薯': 150, '玉米': 150, '苹果': 200, '香蕉': 100, '橙子': 150, '鸡胸肉': 150, '牛肉': 150, '猪肉': 150, '鱼': 150, '蛋': 60, '菠菜': 150, '白菜': 200, '西兰花': 150, '西红柿': 150, '牛奶': 250, '酸奶': 200, '豆浆': 300, '油': 10, '肥肉': 50, } total_carb = 0.0 details = {} for food in foods: # 模糊匹配 food_lower = food.lower() matched = None for name in CARB_DB.keys(): if name in food_lower or food_lower in name: matched = name break if matched: portion = PORTION_EST.get(matched, 100) carb_g = CARB_DB[matched] * portion / 100 total_carb += carb_g details[food] = {'carb_g': round(carb_g, 1), 'portion_g': portion} else: details[food] = {'carb_g': 0, 'portion_g': 0, 'note': '未识别'} return { 'total_carb_g': round(total_carb, 1), 'details': details } def calculate_carb_ratio_from_calories(total_calories: float, carb_g: float) -> float: """计算碳水比例% 参数: total_calories: 总热量(kcal) carb_g: 碳水(g) 返回: 碳水比例% """ if total_calories <= 0: return 0.0 # 碳水每克4kcal carb_calories = carb_g * 4 return round(carb_calories / total_calories * 100, 1) # -------------------- 7. 平台期自动识别 -------------------- def detect_plateau(weights: List[float], days: List[int], threshold: float = 0.1, window: int = 14) -> Dict[str, Any]: """自动识别体重平台期 平台期定义:连续N天体重变化小于阈值 参数: weights: 体重列表(kg) days: 天数列表 threshold: 变化阈值(kg),默认0.1kg window: 窗口天数,默认14天 返回: 平台期分析结果 """ if len(weights) < window: return { 'is_plateau': False, 'reason': '数据不足', 'message': '需要至少14天数据才能判断平台期' } # 取最近window天的数据 recent_weights = weights[-window:] recent_days = days[-window:] # 计算变化 total_change = recent_weights[-1] - recent_weights[0] daily_avg_change = total_change / (len(recent_weights) - 1) if len(recent_weights) > 1 else 0 # 判断是否平台期 is_plateau = abs(total_change) < threshold # 生成建议 suggestions = [] if is_plateau: suggestions = [ '试试变换运动方式,如从走路换成游泳', '调整饮食结构,增加蛋白质比例', '保证充足睡眠,7-8小时', '多喝水,促进代谢', '耐心坚持,平台期是正常的' ] return { 'is_plateau': is_plateau, 'period_days': window if is_plateau else 0, 'total_change_kg': round(total_change, 2), 'daily_avg_change_kg': round(daily_avg_change, 3), 'start_weight': round(recent_weights[0], 1), 'end_weight': round(recent_weights[-1], 1), 'suggestions': suggestions, 'message': '已进入平台期' if is_plateau else '体重仍在变化中,继续加油!' } # -------------------- 科学计算主函数 -------------------- def calculate_scientific(user_info: Dict, intake_calories: float, actual_steps: int, weight_history: List[float] = None, carb_g: float = None, target_weight: float = None) -> Dict[str, Any]: """综合科学计算主函数 整合7项科学优化 参数: user_info: 用户信息 {'weight', 'height', 'age', 'gender', 'body_fat_percent'} intake_calories: 摄入热量(kcal) actual_steps: 实际步数 weight_history: 体重历史(可选) carb_g: 碳水克数(可选) target_weight: 目标体重(可选) 返回: 综合计算结果 """ weight = user_info.get('weight', 60) height = user_info.get('height', 165) age = user_info.get('age', 30) gender = user_info.get('gender', 'female') body_fat = user_info.get('body_fat_percent') # 1. BMR(高级版) bmr = calculate_bmr_advanced(weight, height, age, gender, body_fat) # 2. TDEE(用步数反推) tdee = calculate_tdee_from_steps(weight, actual_steps, bmr) # 3. 个性化基础步数 base_steps = calculate_personalized_base_steps(weight, age, gender) # 4. 热量计算 net_calorie = intake_calories * 0.9 # 扣除TEF calorie_diff = net_calorie - tdee # 5. 脂肪变化 fat_change_g = calorie_diff / 7.7 # 6. 目标步数(热量差→步数闭环核心) # 使用统一的calculate_target_steps函数,保持一致性 steps_result = calculate_target_steps(calorie_diff, weight) target_steps = steps_result['target_steps'] # 7. 碳水比例 if carb_g: carb_ratio = calculate_carb_ratio_from_calories(intake_calories, carb_g) else: carb_ratio = 50.0 # 默认值 # 8. 体重预测(如果有历史数据) weight_prediction = None if weight_history and len(weight_history) >= 3: days = list(range(1, len(weight_history) + 1)) weight_prediction = predict_weight_logistic(weight_history, days, target_weight) # 9. 平台期检测(如果有历史数据) plateau_status = None if weight_history and len(weight_history) >= 14: days = list(range(1, len(weight_history) + 1)) plateau_status = detect_plateau(weight_history, days) return { 'bmr': bmr, 'tdee': tdee, 'net_calorie': round(net_calorie, 1), 'calorie_diff': round(calorie_diff, 1), 'fat_change_g': round(fat_change_g, 1), 'base_steps': steps_result['base_steps'], 'target_steps': target_steps, 'extra_steps': steps_result['extra_steps'], 'steps_mode': steps_result['mode'], 'mode_label': steps_result['mode_label'], 'mode_emoji': steps_result['mode_emoji'], 'calories_per_1000_steps': steps_result['calories_per_1000_steps'], 'calories_to_burn': steps_result.get('calories_to_burn', 0), 'carb_ratio': carb_ratio, 'weight_prediction': weight_prediction, 'plateau_status': plateau_status, # 热量差→步数闭环核心公式 'step_loop': { 'formula': '昨日热量差 = 今日目标步数', 'calorie_diff': round(calorie_diff, 1), 'target_steps': target_steps, 'description': steps_result['reason'] }, 'formulas_used': { 'bmr': 'Katch-McArdle' if body_fat else 'Mifflin-St Jeor', 'tdee': '步数反推', 'tef': '×0.9', 'fat': '/7700×1000', 'step_loop': '热量差→步数闭环' } } # -------------------- 导出接口 -------------------- def scientific_analysis(user_info: Dict, intake_calories: float, actual_steps: int, weight_history: List[float] = None, carb_g: float = None, target_weight: float = None) -> str: """科学分析输出接口(供智能体调用) 返回格式化的分析报告 """ result = calculate_scientific(user_info, intake_calories, actual_steps, weight_history, carb_g, target_weight) output = f"""🔬 科学分析报告 ━━━━━━━━━━━━━━━ 【基础数据】 • BMR: {result['bmr']} kcal (公式: {result['formulas_used']['bmr']}) • TDEE: {result['tdee']} kcal (基于步数反推) • 净热量: {result['net_calorie']} kcal (已扣除TEF) 【热量分析】 • 热量差: {result['calorie_diff']} kcal • 脂肪变化: {result['fat_change_g']}g 【步数目标】 • 个性化基础: {result['base_steps']} 步 • 今日目标: {result['target_steps']} 步 【碳水摄入】 • 碳水比例: {result['carb_ratio']}% """ if result['weight_prediction']: wp = result['weight_prediction'] output += f""" 【体重预测】 • 当前体重: {wp['current_weight']} kg • 变化趋势: {wp['daily_change']:.3f} kg/天 • {wp.get('goal_date', '继续坚持!')} """ if result['plateau_status']: ps = result['plateau_status'] output += f""" 【平台期检测】 • 状态: {'⚠️ 已进入平台期' if ps['is_plateau'] else '✅ 正常减脂中'} • 近14天变化: {ps['total_change_kg']} kg • 建议: {'/'.join(ps['suggestions'][:2]) if ps['is_plateau'] else '继续保持!'} """ return output # 计算热量缺口百分比 cal_deficit_pct = abs(calorie_diff) / bmr * 100 if bmr > 0 else 0 # 1. 热量缺口大(>20%) → 肌肉保护类必备 if cal_deficit_pct > 20: recommendations['must_have'].extend([ {'category': '肌肉保护', 'priority': 'high', 'reason': f'热量缺口{cal_deficit_pct:.0f}%较大,需保护肌肉', 'supplements': SUPPLEMENT_DATABASE['肌肉保护']['supplements'][:2]} ]) recommendations['must_have'].extend([ {'category': '营养补充', 'priority': 'high', 'reason': '减肥期间营养需求增加,需全面补充', 'supplements': SUPPLEMENT_DATABASE['营养补充']['supplements'][:3]} ]) # 2. 营养评分低 → 营养补充必备 if nutrition_score < 2: recommendations['must_have'].extend([ {'category': '营养补充', 'priority': 'high', 'reason': '饮食结构不均衡,需要补充关键营养素', 'supplements': SUPPLEMENT_DATABASE['营养补充']['supplements']} ]) # 3. 连续达标里程碑 → 对应补剂 milestone_supplements = { 3: ('基础代谢支持', '连续3天达标,基础代谢正在适应,可以适当提升'), 7: ('肌肉保护', '连续7天达标!开始需要保护肌肉,建议添加BCAA'), 14: ('皮肤紧致', '连续14天!减肥效果显现,需要开始关注皮肤弹性'), 30: ('综合强化', '一个月!进入关键期,建议全面补充') } for milestone, (category, reason) in milestone_supplements.items(): if streak_days >= milestone: recommendations['recommended'].append({ 'category': category, 'priority': 'milestone', 'milestone': f'连续{milestone}天达标', 'reason': reason, 'supplements': SUPPLEMENT_DATABASE.get(category, SUPPLEMENT_DATABASE['营养补充'])['supplements'] }) # 4. 步数完成率低 → 运动表现类 step_completion = actual_steps / target_steps if target_steps > 0 else 1 if step_completion < 0.5 and streak_days >= 3: recommendations['recommended'].append({ 'category': '运动表现', 'priority': 'medium', 'reason': f'步数完成率{step_completion*100:.0f}%,需要提升运动动力', 'supplements': SUPPLEMENT_DATABASE['运动表现']['supplements'][:2] }) # 5. 食欲控制困难 → 食欲控制类 if score < 6: recommendations['recommended'].append({ 'category': '食欲控制', 'priority': 'medium', 'reason': '饱腹感受到挑战,需要帮助控制食欲', 'supplements': SUPPLEMENT_DATABASE['食欲控制']['supplements'][:2] }) # 6. 基础必备(适用于所有减肥人群) recommendations['optional'].append({ 'category': '基础必备', 'priority': 'basic', 'reason': '减肥基础支持', 'supplements': [ {'name': '复合维生素B', 'dosage': '1片/天', 'timing': '早餐后', 'effect': '支持能量代谢', 'evidence': '⭐⭐⭐⭐⭐'}, {'name': '维生素D3', 'dosage': '2000IU/天', 'timing': '随含脂肪的餐', 'effect': '促进钙吸收', 'evidence': '⭐⭐⭐'}, {'name': 'omega-3鱼油', 'dosage': '1g/天', 'timing': '随餐', 'effect': '抗炎支持', 'evidence': '⭐⭐⭐'} ] }) # 7. 生成小贴士 tips = [] if cal_deficit_pct > 25: tips.append('⚠️ 热量缺口较大,建议适当增加100-200kcal摄入,避免代谢适应性下降') if nutrition_score < 1: tips.append('🥗 饮食结构需改善,增加蛋白质和蔬菜比例') if step_completion < 0.7: tips.append('🚶 尝试分解步数目标,如上下班各走20分钟') if days_count > 14 and days_count % 7 == 0: tips.append('📊 本周结束前可以做一次饮食回顾,调整下周的方案') recommendations['tips'] = tips return recommendations def format_supplement_card(recommendations: Dict[str, Any], streak_days: int, milestone_reached: bool = False) -> str: """格式化补剂推荐卡片""" card_lines = [] card_lines.append("┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓") card_lines.append("┃ 💊 营养补剂推荐方案 ┃") card_lines.append("┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛") # 里程碑提示 if milestone_reached: card_lines.append(f"\n🏆 恭喜达成连续{streak_days}天目标!") card_lines.append(" 根据你的里程碑状态,推荐以下补剂:\n") # 必备补剂 if recommendations['must_have']: card_lines.append("【🔴 必备推荐】") for rec in recommendations['must_have']: card_lines.append(f" 📌 {rec['reason']}") for supp in rec['supplements'][:2]: card_lines.append(f" • {supp['name']}: {supp['dosage']}") card_lines.append(f" 效果: {supp['effect']}") card_lines.append("") # 推荐补剂 if recommendations['recommended']: card_lines.append("【🟡 推荐考虑】") for rec in recommendations['recommended']: milestone = rec.get('milestone', '') if milestone: card_lines.append(f" 🏅 {milestone} - {rec['reason']}") else: card_lines.append(f" 📌 {rec['reason']}") for supp in rec['supplements'][:2]: card_lines.append(f" • {supp['name']}: {supp['dosage']}") card_lines.append(f" 效果: {supp['effect']} ({supp.get('evidence', '⭐⭐')})") card_lines.append("") # 可选补剂 if recommendations['optional']: card_lines.append("【⚪ 可选基础包】") for rec in recommendations['optional']: card_lines.append(f" 📌 {rec['reason']}") for supp in rec['supplements'][:3]: card_lines.append(f" • {supp['name']}: {supp['dosage']}") card_lines.append("") # 小贴士 if recommendations['tips']: card_lines.append("【💡 使用建议】") for tip in recommendations['tips']: card_lines.append(f" {tip}") card_lines.append("\n📝 注:补剂不能替代均衡饮食,") card_lines.append(" 如有特殊情况请咨询医生。") return "\n".join(card_lines) def generate_encouragement_message(score: float, streak_days: int, calorie_diff: float) -> str: """生成鼓励消息""" messages = [] # 根据评分 if score >= 9: messages.append("🎊 完美!今天的饮食堪称教科书级别!") elif score >= 7: messages.append("🌟 很棒!继续保持这个节奏!") elif score >= 5: messages.append("😊 还不错,明天可以做得更好!") elif score >= 3: messages.append("🤔 有进步空间,一起加油!") else: messages.append("💪 没关系!每天都是新的开始!") # 根据连续达标 if streak_days >= 7: messages.append(f"🏆 连续达标{streak_days}天!太厉害了!") elif streak_days >= 3: messages.append(f"🔥 连续{streak_days}天,继续保持!") # 根据热量 if calorie_diff < -300: messages.append("📉 热量缺口不错,瘦得稳稳的~") elif calorie_diff > 300: messages.append("⚠️ 今天超标了,明天注意控制哦~") return " ".join(messages) # ============================================================ # 零食热量数据库(常见零食100g标准单位) # ============================================================ SNACK_DATABASE = { # 甜点类 '蛋糕': {'cal': 350, 'category': '甜点', 'emoji': '🍰'}, '奶油蛋糕': {'cal': 350, 'category': '甜点', 'emoji': '🍰'}, '慕斯': {'cal': 280, 'category': '甜点', 'emoji': '🍮'}, '布丁': {'cal': 180, 'category': '甜点', 'emoji': '🍮'}, '饼干': {'cal': 450, 'category': '甜点', 'emoji': '🍪'}, '曲奇': {'cal': 500, 'category': '甜点', 'emoji': '🍪'}, '月饼': {'cal': 400, 'category': '甜点', 'emoji': '🥮'}, '蛋黄酥': {'cal': 380, 'category': '甜点', 'emoji': '🥮'}, '泡芙': {'cal': 320, 'category': '甜点', 'emoji': '🥐'}, '甜甜圈': {'cal': 400, 'category': '甜点', 'emoji': '🍩'}, '马卡龙': {'cal': 420, 'category': '甜点', 'emoji': '🧁'}, '冰淇淋': {'cal': 200, 'category': '甜点', 'emoji': '🍦'}, '雪糕': {'cal': 180, 'category': '甜点', 'emoji': '🍦'}, # 零食类 '薯片': {'cal': 548, 'category': '零食', 'emoji': '🍟'}, '薯条': {'cal': 312, 'category': '零食', 'emoji': '🍟'}, '爆米花': {'cal': 387, 'category': '零食', 'emoji': '🍿'}, '瓜子': {'cal': 600, 'category': '零食', 'emoji': '🌻'}, '花生': {'cal': 560, 'category': '零食', 'emoji': '🥜'}, '坚果': {'cal': 600, 'category': '零食', 'emoji': '🥜'}, '开心果': {'cal': 560, 'category': '零食', 'emoji': '🌰'}, '杏仁': {'cal': 580, 'category': '零食', 'emoji': '🌰'}, '腰果': {'cal': 553, 'category': '零食', 'emoji': '🥜'}, '巧克力': {'cal': 550, 'category': '零食', 'emoji': '🍫'}, '士力架': {'cal': 490, 'category': '零食', 'emoji': '🍫'}, '威化饼干': {'cal': 380, 'category': '零食', 'emoji': '🍪'}, '肉干': {'cal': 310, 'category': '零食', 'emoji': '🥩'}, '牛肉干': {'cal': 310, 'category': '零食', 'emoji': '🥩'}, '猪肉铺': {'cal': 290, 'category': '零食', 'emoji': '🥩'}, '鱿鱼丝': {'cal': 310, 'category': '零食', 'emoji': '🦑'}, '海苔': {'cal': 350, 'category': '零食', 'emoji': '🥬'}, # 饮品类 '奶茶': {'cal': 250, 'category': '饮品', 'emoji': '🧋'}, '珍珠奶茶': {'cal': 300, 'category': '饮品', 'emoji': '🧋'}, '奶盖': {'cal': 280, 'category': '饮品', 'emoji': '🥤'}, '拿铁': {'cal': 150, 'category': '饮品', 'emoji': '☕'}, '卡布奇诺': {'cal': 180, 'category': '饮品', 'emoji': '☕'}, '可乐': {'cal': 42, 'category': '饮品', 'emoji': '🥤'}, '雪碧': {'cal': 45, 'category': '饮品', 'emoji': '🥤'}, '橙汁': {'cal': 45, 'category': '饮品', 'emoji': '🍊'}, '果汁': {'cal': 50, 'category': '饮品', 'emoji': '🍹'}, '酸奶': {'cal': 72, 'category': '饮品', 'emoji': '🥛'}, '养乐多': {'cal': 70, 'category': '饮品', 'emoji': '🥛'}, '啤酒': {'cal': 43, 'category': '饮品', 'emoji': '🍺'}, '白酒': {'cal': 280, 'category': '饮品', 'emoji': '🥃'}, '红酒': {'cal': 75, 'category': '饮品', 'emoji': '🍷'}, # 快餐类 '炸鸡': {'cal': 300, 'category': '快餐', 'emoji': '🍗'}, '炸鸡腿': {'cal': 280, 'category': '快餐', 'emoji': '🍗'}, '炸鸡翅': {'cal': 260, 'category': '快餐', 'emoji': '🍗'}, '汉堡': {'cal': 450, 'category': '快餐', 'emoji': '🍔'}, '薯条': {'cal': 312, 'category': '快餐', 'emoji': '🍟'}, '披萨': {'cal': 266, 'category': '快餐', 'emoji': '🍕'}, '热狗': {'cal': 290, 'category': '快餐', 'emoji': '🌭'}, '炸鱼': {'cal': 280, 'category': '快餐', 'emoji': '🐟'}, '炸虾': {'cal': 290, 'category': '快餐', 'emoji': '🦐'}, # 甜品饮品 '珍珠': {'cal': 100, 'category': '配料', 'emoji': '🟤'}, '芋圆': {'cal': 120, 'category': '配料', 'emoji': '🟤'}, '波霸': {'cal': 110, 'category': '配料', 'emoji': '🟤'}, '椰果': {'cal': 50, 'category': '配料', 'emoji': '🥥'}, # 水果类(高糖) '榴莲': {'cal': 147, 'category': '水果', 'emoji': '🥭'}, '芒果': {'cal': 65, 'category': '水果', 'emoji': '🥭'}, '荔枝': {'cal': 71, 'category': '水果', 'emoji': '🍒'}, '龙眼': {'cal': 60, 'category': '水果', 'emoji': '🍒'}, '葡萄': {'cal': 67, 'category': '水果', 'emoji': '🍇'}, '西瓜': {'cal': 30, 'category': '水果', 'emoji': '🍉'}, '香蕉': {'cal': 93, 'category': '水果', 'emoji': '🍌'}, # 夜宵类 '烧烤': {'cal': 350, 'category': '夜宵', 'emoji': '🍖'}, '烤肉': {'cal': 320, 'category': '夜宵', 'emoji': '🥓'}, '火锅': {'cal': 300, 'category': '夜宵', 'emoji': '🍲'}, '串串': {'cal': 280, 'category': '夜宵', 'emoji': '🍢'}, '麻辣烫': {'cal': 200, 'category': '夜宵', 'emoji': '🍲'}, '泡面': {'cal': 400, 'category': '夜宵', 'emoji': '🍜'}, '关东煮': {'cal': 150, 'category': '夜宵', 'emoji': '🍢'}, '小龙虾': {'cal': 85, 'category': '夜宵', 'emoji': '🦞'}, } def calculate_snack_exchange(snack_name: str, quantity: float = 1.0, weight_kg: float = 60) -> Dict[str, Any]: """ 计算零食兑换步数 参数: snack_name: 零食名称 quantity: 数量(个/份,零食默认按1份约50g估算) weight_kg: 体重kg 返回: 热量、等效步数、多种运动方案 """ # 查找零食 snack_data = None for name, data in SNACK_DATABASE.items(): if name in snack_name or snack_name in name: snack_data = data matched_name = name break if not snack_data: return {'found': False} # 估算一份的热量(默认50g) portion_grams = 50 cal_per_gram = snack_data['cal'] / 100 total_cal = cal_per_gram * portion_grams * quantity # 体重调整因子 weight_factor = weight_kg / 60 # 计算等效步数(以快走为基准) cal_per_step = 0.04 * weight_factor equivalent_steps = int(total_cal / cal_per_step) # 计算等效运动时间 walk_speed = 6 # km/h,快走 steps_per_minute = 100 walk_minutes = equivalent_steps / steps_per_minute # 其他运动等效 exercises = { '快走': int(walk_minutes), '慢跑': int(walk_minutes * 0.6), '跳绳': int(walk_minutes * 0.5), '游泳': int(walk_minutes * 0.7), '骑行': int(walk_minutes * 0.8), '蹲起': int(total_cal / (0.14 * weight_factor)), } return { 'found': True, 'snack_name': matched_name, 'emoji': snack_data['emoji'], 'category': snack_data['category'], 'portion_grams': portion_grams * quantity, 'calories': round(total_cal, 0), 'equivalent_steps': equivalent_steps, 'exercises': {k: round(v, 0) for k, v in exercises.items()}, 'weight_factor': round(weight_factor, 2) } def format_snack_exchange_card(result: Dict) -> str: """格式化零食兑换卡片(估算,非真实记录)""" if not result['found']: return "这个零食我不太熟悉呢... 🤔 你能告诉我大概吃了多少吗?" card_lines = [] card_lines.append(f"\n📌 【估算】根据常识估算,非真实记录") card_lines.append(f"{result['emoji']} {result['snack_name']} ({result['portion_grams']:.0f}g)") card_lines.append(f"热量约:{result['calories']:.0f} kcal 🔥") card_lines.append(f"\n🚶 要消耗这些热量,你需要:") card_lines.append(f"━━━━━━━━━━━━━━━") for exercise, minutes in result['exercises'].items(): if exercise == '蹲起': card_lines.append(f" 🏋️ {exercise}:约 {minutes:.0f} 次") else: card_lines.append(f" {exercise}:约 {minutes:.0f} 分钟") card_lines.append(f"━━━━━━━━━━━━━━━") # 判断是否值得吃 if result['calories'] < 100: card_lines.append("💚 热量较低,偶尔吃吃问题不大~") elif result['calories'] < 200: card_lines.append("💛 热量中等,今天多走几步就能消耗啦!") elif result['calories'] < 300: card_lines.append("🧡 热量有点高哦,确定要吃吗?") else: card_lines.append("❤️ 高热量零食!吃之前再想想?") card_lines.append(f"\n⚠️ 注意:此为估算,如需记录请告诉我\"我吃了xxx\"") return "\n".join(card_lines) def calculate_party_mode(meal_type: str, people_count: int = 4, weight_kg: float = 60) -> Dict[str, Any]: """ 计算聚餐模式热量 参数: meal_type: 聚餐类型(火锅/烧烤/自助餐/炒菜) people_count: 用餐人数 weight_kg: 体重 返回: 估算热量和建议 """ party_calories = { '火锅': {'base': 600, 'emoji': '🍲', 'tips': '多点蔬菜少吃肉,蘸料少放麻酱~'}, '烧烤': {'base': 650, 'emoji': '🍖', 'tips': '优先选瘦肉,海鲜更佳,少喝啤酒~'}, '自助餐': {'base': 800, 'emoji': '🍽️', 'tips': '先喝汤再吃菜,细嚼慢咽~'}, '炒菜': {'base': 500, 'emoji': '🥘', 'tips': '荤素搭配,主食减半~'}, '日料': {'base': 400, 'emoji': '🍣', 'tips': '刺身最健康,寿司别蘸太多酱油~'}, '韩料': {'base': 450, 'emoji': '🥘', 'tips': '烤肉适量,石锅拌饭不错~'}, '西餐': {'base': 550, 'emoji': '🥩', 'tips': '牛排选菲力,主食换沙拉~'}, '火锅串串': {'base': 500, 'emoji': '🍢', 'tips': '数签子算热量,蔬菜串多吃~'}, } # 查找匹配 data = None matched_type = None for ptype, pdata in party_calories.items(): if ptype in meal_type or meal_type in ptype: data = pdata matched_type = ptype break if not data: data = {'base': 500, 'emoji': '🍽️', 'tips': '注意控制份量哦~'} matched_type = '普通聚餐' # 体重调整 weight_factor = weight_kg / 60 per_person_cal = data['base'] * weight_factor return { 'meal_type': matched_type, 'emoji': data['emoji'], 'per_person_cal': round(per_person_cal, 0), 'total_cal': round(per_person_cal * people_count, 0), 'people_count': people_count, 'tips': data['tips'] } def format_party_mode_card(result: Dict) -> str: """格式化聚餐模式卡片(估算,非真实记录)""" card_lines = [] card_lines.append(f"\n📌 【估算】根据常识估算,非真实记录") card_lines.append(f"{result['emoji']} {result['meal_type']} 热量估算") card_lines.append(f"━━━━━━━━━━━━━━━") card_lines.append(f"👥 {result['people_count']}人用餐") card_lines.append(f"🍽️ 人均热量:约 {result['per_person_cal']:.0f} kcal") card_lines.append(f"📊 总热量:约 {result['total_cal']:.0f} kcal") card_lines.append(f"━━━━━━━━━━━━━━━") card_lines.append(f"💡 建议:{result['tips']}") card_lines.append(f"\n⚠️ 注意:聚餐后请告诉我\"我吃了xxx\"来记录真实数据") return "\n".join(card_lines) def calculate_dynamic_adjustment(weight_change_per_week: float, current_target_steps: int, current_bmr: float) -> Dict[str, Any]: """ 计算动态目标调整 参数: weight_change_per_week: 每周体重变化(kg),负数表示减重 current_target_steps: 当前目标步数 current_bmr: 当前基础代谢 返回: 调整后的目标和说明 """ # 每周减重0.5-1kg是健康范围 healthy_range = (-1.0, -0.5) # 健康减重范围 adjustments = { 'too_fast': { 'reason': '减重太快,建议调整', 'action': '适当增加热量摄入100-200kcal', 'step_change': -1000, 'message': '⚠️ 减重速度过快,可能影响基础代谢,建议适当增加营养摄入~' }, 'too_slow': { 'reason': '减重太慢或体重稳定', 'action': '适当增加运动量或减少热量摄入', 'step_change': 1000, 'message': '💪 减重进入平台期,适当增加运动量效果更好~' }, 'weight_gain': { 'reason': '体重上升', 'action': '减少热量摄入或增加运动', 'step_change': 1500, 'message': '📈 体重有所上升,注意控制饮食和增加运动哦~' }, 'healthy': { 'reason': '减重节奏良好', 'action': '保持当前方案', 'step_change': 0, 'message': '✅ 减重节奏良好,继续保持!' }, 'no_data': { 'reason': '数据不足', 'action': '继续记录一周后再评估', 'step_change': 0, 'message': '📊 数据还不够,持续记录一周后再做调整评估~' } } # 判断减重状态 if weight_change_per_week is None or weight_change_per_week == 0: status = 'no_data' elif weight_change_per_week < healthy_range[0]: status = 'too_fast' elif weight_change_per_week > healthy_range[1] and weight_change_per_week < 0: status = 'too_slow' elif weight_change_per_week >= 0: status = 'weight_gain' else: status = 'healthy' adjustment = adjustments[status] new_target = max(4000, min(20000, current_target_steps + adjustment['step_change'])) return { 'status': status, 'current_target': current_target_steps, 'new_target': new_target, 'step_change': adjustment['step_change'], 'reason': adjustment['reason'], 'action': adjustment['action'], 'message': adjustment['message'], 'bmr': current_bmr, 'weekly_cal_adjustment': -adjustment['step_change'] * 0.04 if adjustment['step_change'] != 0 else 0 } def format_dynamic_adjustment_card(result: Dict) -> str: """格式化动态调整卡片""" card_lines = [] card_lines.append(f"\n📊 周目标评估") card_lines.append(f"━━━━━━━━━━━━━━━") card_lines.append(f"当前基础代谢:{result['bmr']:.0f} kcal/天") if result['step_change'] != 0: direction = "↑" if result['step_change'] > 0 else "↓" card_lines.append(f"目标步数调整:{abs(result['step_change']):,} 步 {direction}") card_lines.append(f"新目标:{result['new_target']:,} 步") else: card_lines.append(f"目标步数:保持 {result['current_target']:,} 步") card_lines.append(f"━━━━━━━━━━━━━━━") card_lines.append(result['message']) card_lines.append(f"📌 {result['action']}") return "\n".join(card_lines) def format_makeup_checkin_guide() -> str: """生成补打卡对话引导""" return """ 📝 补打卡流程引导 想补录之前的饮食记录吗?很简单! 请按以下格式告诉我: ━━━━━━━━━━━━━━━━━ 【日期】+【早/午/晚吃了什么】 例如: • "补录昨天:早餐粥和包子,午餐米饭和鱼,晚餐没吃" • "上周三的记录:早上面条,中午炸鸡" • "昨天三餐都没记,帮我补上" ━━━━━━━━━━━━━━━━━ 我会帮你: 1. 估算补录的热量 2. 计算当时的脂肪变化 3. 更新你的历史数据 随时可以补打卡,别担心遗漏~ 😊 """ def format_progress_report(daily_records: List[Dict], days: int = 7) -> str: """生成周报/阶段进步亮点""" if not daily_records: return "📊 数据不足,需要至少记录几天才能生成报告哦~" recent = daily_records[-days:] if len(daily_records) >= days else daily_records # 统计 total_days = len(recent) achieved_days = sum(1 for r in recent if r.get('target_achieved', False)) avg_steps = sum(r.get('actual_steps', 0) for r in recent) / total_days if total_days > 0 else 0 # 体重变化 weights = [r.get('weight_morning') for r in recent if r.get('weight_morning')] weight_change = weights[-1] - weights[0] if len(weights) > 1 else None # 热量统计 avg_cal = sum(r.get('total_calories', 0) for r in recent) / total_days if total_days > 0 else 0 # 评分 scores = [r.get('score', 0) for r in recent] avg_score = sum(scores) / len(scores) if scores else 0 # 亮点分析 highlights = [] if weight_change and weight_change < -0.5: highlights.append("🏆 体重下降明显,继续保持!") if avg_score >= 7: highlights.append("⭐ 饮食控制越来越好了!") if achieved_days >= total_days * 0.8: highlights.append("🎯 运动目标达成率高!") if avg_cal < 1400: highlights.append("⚠️ 注意热量摄入可能偏低哦~") card_lines = [] card_lines.append(f"\n📊 {'本周' if days == 7 else f'最近{days}天'}数据回顾") card_lines.append(f"━━━━━━━━━━━━━━━") card_lines.append(f"📅 记录天数:{total_days}天") card_lines.append(f"✅ 达标天数:{achieved_days}天 ({achieved_days/total_days*100:.0f}%)") card_lines.append(f"🚶 日均步数:{avg_steps:,.0f}步") card_lines.append(f"🍽️ 日均热量:{avg_cal:,.0f} kcal") card_lines.append(f"⭐ 平均评分:{avg_score:.1f}/10") if weight_change is not None: arrow = "↓" if weight_change < 0 else "↑" card_lines.append(f"⚖️ 体重变化:{arrow}{abs(weight_change):.1f}kg") card_lines.append(f"━━━━━━━━━━━━━━━") if highlights: card_lines.append("🌟 本周亮点:") for h in highlights: card_lines.append(f" {h}") # 下周建议 if avg_score < 6: card_lines.append(f"💡 下周建议:注意增加蛋白质和蔬菜摄入~") elif achieved_days < total_days * 0.5: card_lines.append(f"💡 下周建议:尝试分解步数目标,分散完成~") else: card_lines.append(f"💡 下周建议:保持当前节奏,你已经很棒了!") return "\n".join(card_lines) # ============================================================ # 历史分析函数 # ============================================================ def analyze_weight_trend(daily_records: List[Dict], days: int = 7) -> Dict[str, Any]: """分析体重趋势""" recent = daily_records[-days:] if len(daily_records) >= days else daily_records weights = [(r.get('date'), r.get('weight_morning')) for r in recent if r.get('weight_morning')] if len(weights) < 2: return {'trend': 'insufficient_data', 'change': 0, 'data': weights} changes = [] for i in range(1, len(weights)): if weights[i][1] and weights[i-1][1]: changes.append(weights[i][1] - weights[i-1][1]) avg_change = sum(changes) / len(changes) if changes else 0 total_change = weights[-1][1] - weights[0][1] if weights[-1][1] and weights[0][1] else 0 # 判断趋势 if avg_change < -0.1: trend = '下降 📉' elif avg_change > 0.1: trend = '上升 📈' else: trend = '平稳 ➡️' return { 'trend': trend, 'avg_change_per_day': round(avg_change, 2), 'total_change': round(total_change, 2), 'data': weights } def analyze_calorie_trend(daily_records: List[Dict], days: int = 7) -> Dict[str, Any]: """分析热量趋势""" recent = daily_records[-days:] if len(daily_records) >= days else daily_records data = [(r.get('date'), r.get('total_calories', 0), r.get('target_steps', 0), r.get('actual_steps', 0)) for r in recent] if not data: return {'trend': 'insufficient_data'} avg_cal = sum(d[1] for d in data) / len(data) avg_target = sum(d[2] for d in data) / len(data) avg_actual = sum(d[3] for d in data) / len(data) completion_rate = avg_actual / avg_target * 100 if avg_target > 0 else 0 return { 'avg_daily_calories': round(avg_cal, 1), 'avg_target_steps': int(avg_target), 'avg_actual_steps': int(avg_actual), 'completion_rate': round(completion_rate, 1), 'data': data } def evaluate_diet(total_calories: float, bmr: float, carb_ratio: float, food_items: List[Dict[str, Any]], food_count: int) -> Dict[str, Any]: """膳食评价(10分制)+ 具体建议""" scores = {'nutrition': 0.0, 'calorie_control': 0.0, 'carb_ratio': 0.0, 'diversity': 0.0} suggestions = [] # 营养均衡度(4分) protein_keywords = ['鸡', '肉', '鱼', '蛋', '豆', '奶', '蛋白', '三文', '虾', '牛', '猪'] carb_keywords = ['米', '面', '馒', '包', '面', '土', '玉', '薯', '红'] veg_keywords = ['菜', '蔬', '青', '西兰', '菠菜', '番', '黄', '茄'] has_protein = any(any(k in item.get('name', '') for k in protein_keywords) for item in food_items if food_items) has_carb = any(any(k in item.get('name', '') for k in carb_keywords) for item in food_items if food_items) has_veg = any(any(k in item.get('name', '') for k in veg_keywords) for item in food_items if food_items) if has_protein and has_veg: scores['nutrition'] = 4.0 elif has_protein: scores['nutrition'] = 2.5 suggestions.append("加点蔬菜更营养均衡~") elif has_veg: scores['nutrition'] = 2.0 suggestions.append("记得补充蛋白质哦,鸡胸肉、鱼虾都是好选择~") else: scores['nutrition'] = 1.0 suggestions.append("蔬菜和蛋白质都要有才更健康!") # 总热量控制(3分) calorie_ratio = total_calories / bmr if 0.85 <= calorie_ratio <= 1.15: scores['calorie_control'] = 3.0 elif 0.7 <= calorie_ratio < 0.85: scores['calorie_control'] = 2.5 suggestions.append("吃得有点少哦,长期会掉基础代谢~") elif 1.15 < calorie_ratio <= 1.3: scores['calorie_control'] = 2.0 suggestions.append("热量稍微超标,动一动就平衡啦~") elif calorie_ratio > 1.3: scores['calorie_control'] = 1.0 suggestions.append("今天吃多了!晚上多走8000步弥补一下~") else: scores['calorie_control'] = 1.5 suggestions.append("摄入严重不足,小心基础代谢下降!") # 碳水比例(2分) if 45 <= carb_ratio <= 55: scores['carb_ratio'] = 2.0 elif 40 <= carb_ratio < 45 or 55 < carb_ratio <= 60: scores['carb_ratio'] = 1.5 suggestions.append("碳水比例可以再优化一下~") elif 30 <= carb_ratio < 40 or 60 < carb_ratio <= 70: scores['carb_ratio'] = 1.0 suggestions.append("碳水比例偏差有点大,需要调整!") else: scores['carb_ratio'] = 0.5 suggestions.append("碳水要么太多要么太少,明天注意调整主食量!") # 食物多样性(1分) scores['diversity'] = min(1.0, food_count / 5.0) if food_count < 3: suggestions.append("食物种类太单一了,建议每天吃够5种以上~") total_score = sum(scores.values()) return { 'total_score': round(total_score, 1), 'nutrition': f"{scores['nutrition']}/4", 'calorie_control': f"{scores['calorie_control']}/3", 'carb_ratio': f"{scores['carb_ratio']}/2", 'diversity': f"{scores['diversity']}/1", 'suggestions': suggestions[:3] # 最多3条建议 } def generate_summary_card(user_info: Dict, calc_results: Dict, exercise: Dict, evaluation: Dict, foods: List[str]) -> str: """生成总结卡片 - 优化阅读体验""" gender_display = "女" if str(user_info.get('gender', '')).lower() in ['female', 'f', '女'] else "男" fat_change_g = calc_results['fat_change_g'] # 单位已经是克 # 根据热量差显示不同状态 if calc_results['calorie_diff'] > 0: diff_emoji = "📈" diff_text = f"超标 +{calc_results['calorie_diff']:.0f} kcal" diff_status = "need_exercise" else: diff_emoji = "📉" diff_text = f"缺口 {abs(calc_results['calorie_diff']):.0f} kcal" diff_status = "good" # 根据脂肪变化显示 if fat_change_g > 0: fat_emoji = "🥺" fat_text = f"+{fat_change_g:.0f}g (会长胖哦~)" else: fat_emoji = "🎉" fat_text = f"{fat_change_g:.0f}g (在减脂呢~)" # 根据评分显示星级 total = evaluation['total_score'] stars = "⭐" * int(total // 2) + ("🌙" if total % 2 else "") # 评分详情emoji nutrition_full = int(float(evaluation['nutrition'].split('/')[0])) calorie_full = int(float(evaluation['calorie_control'].split('/')[0])) carb_full = int(float(evaluation['carb_ratio'].split('/')[0])) diversity_full = int(float(evaluation['diversity'].split('/')[0])) nutrition_bar = "▓" * nutrition_full + "░" * (4 - nutrition_full) calorie_bar = "▓" * calorie_full + "░" * (3 - calorie_full) carb_bar = "▓" * carb_full + "░" * (2 - carb_full) diversity_bar = "▓" * diversity_full + "░" * (1 - diversity_full) card = f""" ╭─────────────────────────────────────────╮ │ 🌟 🌟 AI陪伴减肥报告 🌟 🌟 │ ╰─────────────────────────────────────────╯ 👤 【基本档案】 身高: {user_info['height']}cm 体重: {user_info['weight']}kg 年龄: {user_info['age']}岁 性别: {gender_display} 🍽️ 【今日饮食】 食物: {', '.join(foods[:4])}{'...' if len(foods) > 4 else ''} 热量: {calc_results['total_calories']:.0f} kcal 💫 碳水: {calc_results['carb_ratio']:.0f}% 📊 ╭─────────────────────────────────────────╮ │ {diff_emoji} 热量状态: {diff_text} │ │ {fat_emoji} 脂肪变化: {fat_text} │ ╰─────────────────────────────────────────╯ 🔥 【代谢数据】 🫀 基础代谢(BMR): {calc_results['bmr']:.0f} kcal/天 ⚡ 总消耗(TDEE): {calc_results['tdee']:.0f} kcal/天 (活动系数: {calc_results.get('activity_level', 'sedentary')}) 📐 步长: {calc_results['step_length_cm']:.1f} cm 🦶 每千步消耗: {calc_results['cal_per_1000_steps']:.1f} kcal 👟 【今日目标】 🚶 建议步数: {calc_results['target_steps']:,} 步 🏋️ 蹲起消耗: {exercise['squats_needed']:,} 次 (每次 {exercise['squat_calories']:.3f} kcal) ╭─────────────────────────────────────────╮ │ {stars} 综合评分: {total}/10 │ ├─────────────────────────────────────────┤ │ 🥗 营养均衡 [{nutrition_bar}] {evaluation['nutrition']} │ │ 🔥 热量控制 [{calorie_bar}] {evaluation['calorie_control']} │ │ 🍞 碳水比例 [{carb_bar}] {evaluation['carb_ratio']} │ │ 🌈 食物多样 [{diversity_bar}] {evaluation['diversity']} │ ╰─────────────────────────────────────────╯ """ return card def generate_daily_summary(user_info: Dict, calc_results: Dict, exercise: Dict, evaluation: Dict, foods: List[str], streak_days: int = 0, weight_change: float = 0, target_steps: int = 6000) -> str: """生成日终总结卡片 - 优化版模板""" from datetime import datetime gender_display = "女" if str(user_info.get('gender', '')).lower() in ['female', 'f', '女'] else "男" fat_change_g = calc_results['fat_change_g'] # 单位已经是克 today = datetime.now().strftime('%Y年%m月%d日') # 热量差状态 if calc_results['calorie_diff'] > 0: diff_emoji = "📈" diff_text = f"超标 +{calc_results['calorie_diff']:.0f} kcal" elif calc_results['calorie_diff'] < -200: diff_emoji = "📉" diff_text = f"缺口 {abs(calc_results['calorie_diff']):.0f} kcal" else: diff_emoji = "📊" diff_text = f"吃动平衡" # 脂肪变化状态 if fat_change_g > 0: fat_emoji = "🥺" fat_text = f"+{fat_change_g:.0f}g" else: fat_emoji = "🎉" fat_text = f"{fat_change_g:.0f}g" # 步数 actual_steps = exercise.get('actual_steps', 0) # 热量 intake = calc_results['total_calories'] bmr = calc_results['bmr'] total = evaluation['total_score'] card = f""" ╭─────────────────────────────────────────╮ │ 🌟 今日营养处方 🌟 │ │ 📅 {today} │ ╰─────────────────────────────────────────╯ 【🍽️ 今日饮食】 • 食物: {', '.join(foods[:5])}{'...' if len(foods) > 5 else ''} • 今日总热量: {intake:.0f} kcal • 碳水比例: {calc_results['carb_ratio']:.0f}% 【🔥 今日运动】 • 步数: {actual_steps:,} 步 • 运动消耗: 约 {exercise.get('exercise_calories', 0):.0f} kcal 【📊 热量分析】 • 摄入热量: {intake:.0f} kcal • 热量状态: {diff_emoji} {diff_text} • 脂肪变化: {fat_emoji} 约 {fat_text} 【⚖️ 体重追踪】 • 当前体重: {user_info['weight']:.1f} kg • 累计减重: ↓ {abs(weight_change):.2f} kg 【⭐ 今日评分】{total:.1f}/10 ┌─────────────┬──────────┬─────┐ │ 营养均衡 │ {evaluation['nutrition']} │ 满 │ │ 热量控制 │ {evaluation['calorie_control']} │ 满 │ │ 碳水比例 │ {evaluation['carb_ratio']} │ 满 │ │ 食物多样 │ {evaluation['diversity']} │ 满 │ └─────────────┴──────────┴─────┘ 【🎯 明日目标】 • 步数目标: {target_steps:,} 步 • 体重打卡: 明早空腹 • 继续保持健康饮食 --- 💡 Kite小结:{'今天表现很棒!' if total >= 8 else '今天还不错,明天继续加油!'} 继续坚持,向目标冲刺!💪 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━ *本建议仅供日常参考,效果因人而异* """ return card def generate_html_prescription(user_info: Dict, calc_results: Dict, exercise: Dict, evaluation: Dict, foods: List[str], streak_days: int = 0, weight_change: float = 0, target_steps: int = 6000, supplement_recommendations: List[Dict] = None) -> str: """生成HTML格式的【营养处方】""" from datetime import datetime fat_change_g = calc_results.get('fat_change_g', 0) today = datetime.now() date_str = today.strftime('%Y年%m月%d日') weekday = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期日'][today.weekday()] intake = calc_results.get('total_calories', 0) actual_steps = exercise.get('actual_steps', 0) total = evaluation.get('total_score', 0) stars = "⭐" * int(total) + "🌙" * (10 - int(total)) # 营养建议生成 advice_items = [] # 基于评分生成建议 nutrition_score = float(evaluation.get('nutrition', '0/4').split('/')[0]) diversity_score = float(evaluation.get('diversity', '0/1').split('/')[0]) if nutrition_score < 2: advice_items.append({'icon': '🥗', 'text': '蛋白质摄入不足,建议增加肉类、蛋类或豆制品'}) if diversity_score < 0.5: advice_items.append({'icon': '🌈', 'text': '食物种类单一,建议增加蔬菜和水果'}) if calc_results.get('carb_ratio', 50) < 40: advice_items.append({'icon': '🍞', 'text': '碳水比例偏低,可适当增加主食摄入'}) elif calc_results.get('carb_ratio', 50) > 60: advice_items.append({'icon': '🍞', 'text': '碳水比例偏高,建议减少精制碳水'}) if not advice_items: advice_items.append({'icon': '✅', 'text': '饮食结构良好,继续保持!'}) # 基于连续达标天数生成补剂推荐 if streak_days >= 30: supplement_items = supplement_recommendations or [ {'name': '复合维生素B族', 'dose': '1片/天', 'reason': '长期热量控制需补充'}, {'name': '维生素D3', 'dose': '2000IU/天', 'reason': '支持代谢和骨骼健康'}, {'name': 'Omega-3鱼油', 'dose': '1000mg/天', 'reason': '抗炎和心血管保护'}, {'name': '镁元素', 'dose': '400mg/天', 'reason': '改善睡眠和肌肉恢复'}, {'name': '左旋肉碱', 'dose': '2g/天', 'reason': '加速脂肪燃烧'} ] elif streak_days >= 14: supplement_items = supplement_recommendations or [ {'name': '支链氨基酸(BCAA)', 'dose': '5g/天', 'reason': '保护肌肉不流失'}, {'name': '复合维生素', 'dose': '1片/天', 'reason': '弥补饮食限制造成的营养缺口'}, {'name': '左旋肉碱', 'dose': '2g/天', 'reason': '辅助脂肪代谢'} ] elif streak_days >= 7: supplement_items = supplement_recommendations or [ {'name': '乳清蛋白', 'dose': '20g/天', 'reason': '补充优质蛋白'}, {'name': '复合维生素B', 'dose': '1片/天', 'reason': '支持能量代谢'} ] elif streak_days >= 3: supplement_items = supplement_recommendations or [ {'name': '电解质粉', 'dose': '适量', 'reason': '运动后补充流失矿物质'} ] else: supplement_items = supplement_recommendations or [ {'name': '暂无推荐', 'dose': '-', 'reason': '先建立健康的饮食习惯'} ] advice_html = ''.join([f'