"""数据模型定义""" 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_id(null 表示未使用) 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, }