Files
DDUp/src/vitals/core/models.py

329 lines
9.6 KiB
Python
Raw Normal View History

2026-01-22 12:57:26 +08:00
"""数据模型定义"""
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 # 目标体重(公斤)
2026-01-22 12:57:26 +08:00
@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,
2026-01-22 12:57:26 +08:00
"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,
}