Files
DDUp/src/vitals/core/models.py
liweiliang0905@gmail.com 842998893a feat: 添加目标体重设置功能
- UserConfig 模型添加 target_weight 字段
- 新增 POST /api/config 端点用于更新用户配置
- 设置页面添加目标体重输入框及目标 BMI 自动计算
- BMI 分析使用用户设置的目标体重计算达成预估
- 修复测试 fixture 的 save_config 参数问题

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 17:39:24 +08:00

329 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""数据模型定义"""
from dataclasses import dataclass, field
from datetime import date, time, datetime
from enum import Enum
from typing import Optional
import json
class MealType(str, Enum):
BREAKFAST = "早餐"
LUNCH = "午餐"
DINNER = "晚餐"
SNACK = "加餐"
class ExerciseType(str, Enum):
RUNNING = "跑步"
SWIMMING = "游泳"
CYCLING = "骑行"
STRENGTH = "力量训练"
WALKING = "健走"
YOGA = "瑜伽"
OTHER = "其他"
class DataSource(str, Enum):
MANUAL = "手动"
GARMIN = "garmin"
CODOON = "codoon"
CSV = "csv"
class ActivityLevel(str, Enum):
SEDENTARY = "sedentary" # 久坐
LIGHT = "light" # 轻度活动
MODERATE = "moderate" # 中度活动
ACTIVE = "active" # 活跃
VERY_ACTIVE = "very_active" # 非常活跃
class Goal(str, Enum):
LOSE = "lose" # 减重
MAINTAIN = "maintain" # 维持
GAIN = "gain" # 增肌
@dataclass
class Exercise:
"""运动记录"""
id: Optional[int] = None
date: date = field(default_factory=date.today)
type: str = ExerciseType.OTHER.value
duration: int = 0 # 分钟
calories: int = 0
distance: Optional[float] = None # 公里
heart_rate_avg: Optional[int] = None
source: str = DataSource.MANUAL.value
raw_data: Optional[dict] = None
notes: Optional[str] = None
def to_dict(self) -> dict:
return {
"id": self.id,
"date": self.date.isoformat(),
"type": self.type,
"duration": self.duration,
"calories": self.calories,
"distance": self.distance,
"heart_rate_avg": self.heart_rate_avg,
"source": self.source,
"raw_data": json.dumps(self.raw_data) if self.raw_data else None,
"notes": self.notes,
}
@dataclass
class Meal:
"""饮食记录"""
id: Optional[int] = None
date: date = field(default_factory=date.today)
meal_type: str = MealType.LUNCH.value
description: str = ""
calories: int = 0
protein: Optional[float] = None # 克
carbs: Optional[float] = None # 克
fat: Optional[float] = None # 克
photo_path: Optional[str] = None
food_items: Optional[list] = None
def to_dict(self) -> dict:
return {
"id": self.id,
"date": self.date.isoformat(),
"meal_type": self.meal_type,
"description": self.description,
"calories": self.calories,
"protein": self.protein,
"carbs": self.carbs,
"fat": self.fat,
"photo_path": self.photo_path,
"food_items": json.dumps(self.food_items, ensure_ascii=False) if self.food_items else None,
}
@dataclass
class Sleep:
"""睡眠记录"""
id: Optional[int] = None
date: date = field(default_factory=date.today)
bedtime: Optional[time] = None
wake_time: Optional[time] = None
duration: float = 0 # 小时
quality: int = 3 # 1-5
deep_sleep_mins: Optional[int] = None
source: str = DataSource.MANUAL.value
notes: Optional[str] = None
def to_dict(self) -> dict:
return {
"id": self.id,
"date": self.date.isoformat(),
"bedtime": self.bedtime.isoformat() if self.bedtime else None,
"wake_time": self.wake_time.isoformat() if self.wake_time else None,
"duration": self.duration,
"quality": self.quality,
"deep_sleep_mins": self.deep_sleep_mins,
"source": self.source,
"notes": self.notes,
}
@dataclass
class Weight:
"""体重记录"""
id: Optional[int] = None
date: date = field(default_factory=date.today)
weight_kg: float = 0
body_fat_pct: Optional[float] = None
muscle_mass: Optional[float] = None
notes: Optional[str] = None
def to_dict(self) -> dict:
return {
"id": self.id,
"date": self.date.isoformat(),
"weight_kg": self.weight_kg,
"body_fat_pct": self.body_fat_pct,
"muscle_mass": self.muscle_mass,
"notes": self.notes,
}
@dataclass
class UserConfig:
"""用户配置"""
age: Optional[int] = None
gender: Optional[str] = None # male / female
height: Optional[float] = None # 厘米
weight: Optional[float] = None # 公斤
activity_level: str = ActivityLevel.MODERATE.value
goal: str = Goal.MAINTAIN.value
target_weight: Optional[float] = None # 目标体重(公斤)
@property
def bmr(self) -> Optional[int]:
"""计算基础代谢率 (Mifflin-St Jeor 公式)"""
if not all([self.age, self.gender, self.height, self.weight]):
return None
if self.gender == "male":
return int(10 * self.weight + 6.25 * self.height - 5 * self.age + 5)
else:
return int(10 * self.weight + 6.25 * self.height - 5 * self.age - 161)
@property
def tdee(self) -> Optional[int]:
"""计算每日总消耗 (TDEE)"""
if not self.bmr:
return None
multipliers = {
ActivityLevel.SEDENTARY.value: 1.2,
ActivityLevel.LIGHT.value: 1.375,
ActivityLevel.MODERATE.value: 1.55,
ActivityLevel.ACTIVE.value: 1.725,
ActivityLevel.VERY_ACTIVE.value: 1.9,
}
return int(self.bmr * multipliers.get(self.activity_level, 1.55))
def to_dict(self) -> dict:
return {
"age": self.age,
"gender": self.gender,
"height": self.height,
"weight": self.weight,
"activity_level": self.activity_level,
"goal": self.goal,
"target_weight": self.target_weight,
"bmr": self.bmr,
"tdee": self.tdee,
}
@dataclass
class User:
"""用户档案"""
id: Optional[int] = None
name: str = ""
created_at: datetime = field(default_factory=datetime.now)
is_active: bool = False
gender: Optional[str] = None # male / female
height_cm: Optional[float] = None # 身高(厘米)
weight_kg: Optional[float] = None # 体重(公斤)
age: Optional[int] = None # 年龄
# 认证相关字段
password_hash: Optional[str] = None # 密码哈希bcrypt
email: Optional[str] = None # 邮箱(可选)
is_admin: bool = False # 是否管理员
is_disabled: bool = False # 是否禁用
@property
def bmi(self) -> Optional[float]:
"""计算 BMI = 体重(kg) / 身高(m)²"""
if not self.height_cm or not self.weight_kg:
return None
height_m = self.height_cm / 100
return round(self.weight_kg / (height_m * height_m), 1)
@property
def bmi_status(self) -> Optional[str]:
"""BMI 状态评估"""
bmi = self.bmi
if bmi is None:
return None
if bmi < 18.5:
return "偏瘦"
elif bmi < 24:
return "正常"
elif bmi < 28:
return "偏胖"
else:
return "肥胖"
def to_dict(self) -> dict:
return {
"id": self.id,
"name": self.name,
"created_at": self.created_at.isoformat(),
"is_active": self.is_active,
"gender": self.gender,
"height_cm": self.height_cm,
"weight_kg": self.weight_kg,
"age": self.age,
"bmi": self.bmi,
"bmi_status": self.bmi_status,
"email": self.email,
"is_admin": self.is_admin,
"is_disabled": self.is_disabled,
}
@dataclass
class Invite:
"""邀请码"""
id: Optional[int] = None
code: str = "" # 8位随机字符串
created_by: int = 0 # 创建者 user_id
used_by: Optional[int] = None # 使用者 user_idnull 表示未使用)
created_at: datetime = field(default_factory=datetime.now)
expires_at: Optional[datetime] = None # 过期时间(可选)
@property
def is_used(self) -> bool:
return self.used_by is not None
@property
def is_expired(self) -> bool:
if self.expires_at is None:
return False
return datetime.now() > self.expires_at
def to_dict(self) -> dict:
return {
"id": self.id,
"code": self.code,
"created_by": self.created_by,
"used_by": self.used_by,
"created_at": self.created_at.isoformat(),
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
"is_used": self.is_used,
"is_expired": self.is_expired,
}
@dataclass
class DataClearRequest:
"""数据清除请求"""
user_id: int = 0
mode: str = "all" # "range" | "type" | "all"
date_from: Optional[date] = None
date_to: Optional[date] = None
data_types: Optional[list] = None # ["exercise", "meal", "sleep", "weight"]
@dataclass
class Reading:
"""阅读记录"""
id: Optional[int] = None
user_id: int = 1
date: date = field(default_factory=date.today)
title: str = "" # 书名
author: Optional[str] = None # 作者
cover_url: Optional[str] = None # 封面 URL
duration: int = 0 # 阅读时长(分钟)
mood: Optional[str] = None # 心情(😄😊😐😔😢)
notes: Optional[str] = None # 读后感
def to_dict(self) -> dict:
return {
"id": self.id,
"user_id": self.user_id,
"date": self.date.isoformat(),
"title": self.title,
"author": self.author,
"cover_url": self.cover_url,
"duration": self.duration,
"mood": self.mood,
"notes": self.notes,
}