1. Config 表用户隔离 - 添加 user_id 字段,复合主键 (user_id, key) - 现有数据归属 ID=1 用户 - 所有 get_config/save_config 调用传入 user_id 2. 食物文字智能识别 - 本地数据库优先匹配(快速) - 识别失败时自动调用通义千问 AI(准确) - 有配置 API Key 才调用,否则返回本地结果 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
221 lines
8.0 KiB
Python
221 lines
8.0 KiB
Python
"""数据导出/导入模块(JSON/CSV)"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import csv
|
||
import json
|
||
from dataclasses import asdict, is_dataclass
|
||
from datetime import date, time
|
||
from pathlib import Path
|
||
from typing import Literal, Optional
|
||
|
||
from . import database as db
|
||
from .models import Exercise, Meal, Sleep, UserConfig, Weight
|
||
|
||
|
||
ExportType = Literal["exercise", "meal", "sleep", "weight"]
|
||
|
||
|
||
def _json_default(value):
|
||
"""json.dumps 默认序列化器:处理 date/time/dataclass"""
|
||
if isinstance(value, (date, time)):
|
||
return value.isoformat()
|
||
if is_dataclass(value):
|
||
return asdict(value)
|
||
raise TypeError(f"Object of type {type(value).__name__} is not JSON serializable")
|
||
|
||
|
||
def export_all_data_json(output_path: Path, user_id: int = 1) -> None:
|
||
"""导出所有数据为 JSON"""
|
||
data = {
|
||
"version": "1.0",
|
||
"export_date": date.today().isoformat(),
|
||
"exercises": [e.to_dict() for e in db.get_exercises()],
|
||
"meals": [m.to_dict() for m in db.get_meals()],
|
||
"sleep": [s.to_dict() for s in db.get_sleep_records()],
|
||
"weight": [w.to_dict() for w in db.get_weight_records()],
|
||
"config": db.get_config(user_id).to_dict(),
|
||
}
|
||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||
output_path.write_text(json.dumps(data, ensure_ascii=False, indent=2, default=_json_default), encoding="utf-8")
|
||
|
||
|
||
def export_to_csv(
|
||
data_type: ExportType,
|
||
output_path: Path,
|
||
start_date: Optional[date] = None,
|
||
end_date: Optional[date] = None,
|
||
) -> None:
|
||
"""导出指定类型数据为 CSV(字段与导入器兼容)"""
|
||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||
|
||
if data_type == "exercise":
|
||
records = db.get_exercises(start_date=start_date, end_date=end_date)
|
||
fieldnames = ["date", "type", "duration", "calories", "distance", "heart_rate_avg", "notes"]
|
||
rows = [
|
||
{
|
||
"date": r.date.isoformat(),
|
||
"type": r.type,
|
||
"duration": r.duration,
|
||
"calories": r.calories,
|
||
"distance": r.distance or "",
|
||
"heart_rate_avg": r.heart_rate_avg or "",
|
||
"notes": r.notes or "",
|
||
}
|
||
for r in records
|
||
]
|
||
elif data_type == "meal":
|
||
records = db.get_meals(start_date=start_date, end_date=end_date)
|
||
fieldnames = ["date", "meal_type", "description", "calories", "protein", "carbs", "fat"]
|
||
rows = [
|
||
{
|
||
"date": r.date.isoformat(),
|
||
"meal_type": r.meal_type,
|
||
"description": r.description,
|
||
"calories": r.calories,
|
||
"protein": r.protein or "",
|
||
"carbs": r.carbs or "",
|
||
"fat": r.fat or "",
|
||
}
|
||
for r in records
|
||
]
|
||
elif data_type == "sleep":
|
||
records = db.get_sleep_records(start_date=start_date, end_date=end_date)
|
||
fieldnames = ["date", "bedtime", "wake_time", "duration", "quality", "deep_sleep_mins", "notes"]
|
||
rows = [
|
||
{
|
||
"date": r.date.isoformat(),
|
||
"bedtime": r.bedtime.isoformat() if r.bedtime else "",
|
||
"wake_time": r.wake_time.isoformat() if r.wake_time else "",
|
||
"duration": r.duration,
|
||
"quality": r.quality,
|
||
"deep_sleep_mins": r.deep_sleep_mins or "",
|
||
"notes": r.notes or "",
|
||
}
|
||
for r in records
|
||
]
|
||
elif data_type == "weight":
|
||
records = db.get_weight_records(start_date=start_date, end_date=end_date)
|
||
fieldnames = ["date", "weight_kg", "body_fat_pct", "muscle_mass", "notes"]
|
||
rows = [
|
||
{
|
||
"date": r.date.isoformat(),
|
||
"weight_kg": r.weight_kg,
|
||
"body_fat_pct": r.body_fat_pct or "",
|
||
"muscle_mass": r.muscle_mass or "",
|
||
"notes": r.notes or "",
|
||
}
|
||
for r in records
|
||
]
|
||
else:
|
||
raise ValueError(f"不支持的数据类型: {data_type}")
|
||
|
||
with open(output_path, "w", newline="", encoding="utf-8") as f:
|
||
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||
writer.writeheader()
|
||
for row in rows:
|
||
writer.writerow(row)
|
||
|
||
|
||
def import_from_json(input_path: Path, user_id: int = 1) -> dict[str, int]:
|
||
"""从 JSON 导入数据(最小实现:覆盖性导入,不做去重)"""
|
||
data = json.loads(input_path.read_text(encoding="utf-8"))
|
||
|
||
stats = {"exercise": 0, "meal": 0, "sleep": 0, "weight": 0}
|
||
|
||
# config(可选)
|
||
config = data.get("config")
|
||
if isinstance(config, dict):
|
||
# 仅持久化可写字段
|
||
db.save_config(
|
||
user_id,
|
||
UserConfig(
|
||
age=config.get("age"),
|
||
gender=config.get("gender"),
|
||
height=config.get("height"),
|
||
weight=config.get("weight"),
|
||
activity_level=config.get("activity_level", "moderate"),
|
||
goal=config.get("goal", "maintain"),
|
||
)
|
||
)
|
||
|
||
# exercises
|
||
for item in data.get("exercises", []):
|
||
try:
|
||
ex = Exercise(
|
||
date=date.fromisoformat(item["date"]),
|
||
type=item.get("type", "其他"),
|
||
duration=int(item.get("duration", 0)),
|
||
calories=int(item.get("calories", 0)),
|
||
distance=float(item["distance"]) if item.get("distance") not in (None, "") else None,
|
||
heart_rate_avg=int(item["heart_rate_avg"])
|
||
if item.get("heart_rate_avg") not in (None, "")
|
||
else None,
|
||
source=item.get("source", "手动"),
|
||
notes=item.get("notes"),
|
||
)
|
||
db.add_exercise(ex)
|
||
stats["exercise"] += 1
|
||
except Exception:
|
||
continue
|
||
|
||
# meals
|
||
for item in data.get("meals", []):
|
||
try:
|
||
meal = Meal(
|
||
date=date.fromisoformat(item["date"]),
|
||
meal_type=item.get("meal_type", "午餐"),
|
||
description=item.get("description", ""),
|
||
calories=int(item.get("calories", 0)),
|
||
protein=float(item["protein"]) if item.get("protein") not in (None, "") else None,
|
||
carbs=float(item["carbs"]) if item.get("carbs") not in (None, "") else None,
|
||
fat=float(item["fat"]) if item.get("fat") not in (None, "") else None,
|
||
photo_path=item.get("photo_path"),
|
||
)
|
||
db.add_meal(meal)
|
||
stats["meal"] += 1
|
||
except Exception:
|
||
continue
|
||
|
||
# sleep
|
||
for item in data.get("sleep", []):
|
||
try:
|
||
s = Sleep(
|
||
date=date.fromisoformat(item["date"]),
|
||
bedtime=time.fromisoformat(item["bedtime"]) if item.get("bedtime") else None,
|
||
wake_time=time.fromisoformat(item["wake_time"]) if item.get("wake_time") else None,
|
||
duration=float(item.get("duration", 0)),
|
||
quality=int(item.get("quality", 3)),
|
||
deep_sleep_mins=int(item["deep_sleep_mins"])
|
||
if item.get("deep_sleep_mins") not in (None, "")
|
||
else None,
|
||
source=item.get("source", "手动"),
|
||
notes=item.get("notes"),
|
||
)
|
||
db.add_sleep(s)
|
||
stats["sleep"] += 1
|
||
except Exception:
|
||
continue
|
||
|
||
# weight
|
||
for item in data.get("weight", []):
|
||
try:
|
||
w = Weight(
|
||
date=date.fromisoformat(item["date"]),
|
||
weight_kg=float(item.get("weight_kg", 0)),
|
||
body_fat_pct=float(item["body_fat_pct"])
|
||
if item.get("body_fat_pct") not in (None, "")
|
||
else None,
|
||
muscle_mass=float(item["muscle_mass"])
|
||
if item.get("muscle_mass") not in (None, "")
|
||
else None,
|
||
notes=item.get("notes"),
|
||
)
|
||
db.add_weight(w)
|
||
stats["weight"] += 1
|
||
except Exception:
|
||
continue
|
||
|
||
return stats
|
||
|