# 设置页面实现计划 > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** 为 Vitals 应用添加设置页面,包含用户管理(档案切换)和系统管理(体重录入、数据清除)功能 **Architecture:** 新增 User 模型和 users 表,为现有数据表添加 user_id 字段实现多用户隔离。通过 Cookie 存储当前用户 ID,所有 API 自动根据用户过滤数据。 **Tech Stack:** FastAPI, SQLite, Pydantic, Vanilla JavaScript --- ## Task 1: 新增 User 模型 **Files:** - Modify: `src/vitals/core/models.py` - Test: `tests/test_models.py` **Step 1: 编写 User 模型测试** 在 `tests/test_models.py` 末尾添加: ```python class TestUser: """用户模型测试""" def test_user_creation(self): """测试创建用户""" from datetime import datetime from src.vitals.core.models import User user = User( name="小明", created_at=datetime.now(), ) assert user.name == "小明" assert user.is_active == False assert user.id is None def test_user_to_dict(self): """测试用户转换为字典""" from datetime import datetime from src.vitals.core.models import User user = User( id=1, name="小红", created_at=datetime(2026, 1, 19, 10, 0, 0), is_active=True, ) d = user.to_dict() assert d["id"] == 1 assert d["name"] == "小红" assert d["is_active"] == True assert "created_at" in d ``` **Step 2: 运行测试确认失败** ```bash pytest tests/test_models.py::TestUser -v ``` Expected: FAIL - `ImportError: cannot import name 'User'` **Step 3: 实现 User 模型** 在 `src/vitals/core/models.py` 末尾添加: ```python @dataclass class User: """用户档案""" id: Optional[int] = None name: str = "" created_at: datetime = field(default_factory=datetime.now) is_active: bool = False def to_dict(self) -> dict: return { "id": self.id, "name": self.name, "created_at": self.created_at.isoformat(), "is_active": self.is_active, } ``` 同时在文件顶部的导入中添加 `datetime`: ```python from datetime import date, time, datetime ``` **Step 4: 运行测试确认通过** ```bash pytest tests/test_models.py::TestUser -v ``` Expected: PASS **Step 5: 提交** ```bash git add src/vitals/core/models.py tests/test_models.py git commit -m "feat: add User model for multi-user support" ``` --- ## Task 2: 新增 DataClearRequest 模型 **Files:** - Modify: `src/vitals/core/models.py` - Test: `tests/test_models.py` **Step 1: 编写 DataClearRequest 测试** 在 `tests/test_models.py` 末尾添加: ```python class TestDataClearRequest: """数据清除请求模型测试""" def test_clear_by_range(self): """测试按时间范围清除""" from src.vitals.core.models import DataClearRequest req = DataClearRequest( user_id=1, mode="range", date_from=date(2026, 1, 1), date_to=date(2026, 1, 15), ) assert req.mode == "range" assert req.date_from == date(2026, 1, 1) def test_clear_by_type(self): """测试按类型清除""" from src.vitals.core.models import DataClearRequest req = DataClearRequest( user_id=1, mode="type", data_types=["exercise", "meal"], ) assert req.mode == "type" assert "exercise" in req.data_types def test_clear_all(self): """测试清除全部""" from src.vitals.core.models import DataClearRequest req = DataClearRequest( user_id=1, mode="all", ) assert req.mode == "all" ``` **Step 2: 运行测试确认失败** ```bash pytest tests/test_models.py::TestDataClearRequest -v ``` Expected: FAIL **Step 3: 实现 DataClearRequest 模型** 在 `src/vitals/core/models.py` 末尾添加: ```python @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"] ``` **Step 4: 运行测试确认通过** ```bash pytest tests/test_models.py::TestDataClearRequest -v ``` Expected: PASS **Step 5: 提交** ```bash git add src/vitals/core/models.py tests/test_models.py git commit -m "feat: add DataClearRequest model" ``` --- ## Task 3: 数据库 - 创建 users 表 **Files:** - Modify: `src/vitals/core/database.py` - Test: `tests/test_database.py` **Step 1: 编写 users 表测试** 在 `tests/test_database.py` 末尾添加: ```python class TestUserDB: """用户数据库测试""" def test_add_user(self): """测试添加用户""" from src.vitals.core.models import User user = User(name="测试用户") user_id = db.add_user(user) assert user_id > 0 def test_get_users(self): """测试获取用户列表""" from src.vitals.core.models import User db.add_user(User(name="用户1")) db.add_user(User(name="用户2")) users = db.get_users() assert len(users) == 2 def test_get_user_by_id(self): """测试按 ID 获取用户""" from src.vitals.core.models import User user = User(name="小明") user_id = db.add_user(user) fetched = db.get_user(user_id) assert fetched is not None assert fetched.name == "小明" def test_update_user(self): """测试更新用户""" from src.vitals.core.models import User user = User(name="原名") user_id = db.add_user(user) user.id = user_id user.name = "新名" db.update_user(user) fetched = db.get_user(user_id) assert fetched.name == "新名" def test_delete_user(self): """测试删除用户""" from src.vitals.core.models import User user_id = db.add_user(User(name="待删除")) db.delete_user(user_id) fetched = db.get_user(user_id) assert fetched is None def test_set_active_user(self): """测试设置激活用户""" from src.vitals.core.models import User id1 = db.add_user(User(name="用户1")) id2 = db.add_user(User(name="用户2")) db.set_active_user(id1) user1 = db.get_user(id1) user2 = db.get_user(id2) assert user1.is_active == True assert user2.is_active == False # 切换激活用户 db.set_active_user(id2) user1 = db.get_user(id1) user2 = db.get_user(id2) assert user1.is_active == False assert user2.is_active == True def test_get_active_user(self): """测试获取激活用户""" from src.vitals.core.models import User id1 = db.add_user(User(name="用户1")) db.set_active_user(id1) active = db.get_active_user() assert active is not None assert active.id == id1 ``` **Step 2: 运行测试确认失败** ```bash pytest tests/test_database.py::TestUserDB -v ``` Expected: FAIL **Step 3: 在 init_db() 中添加 users 表** 在 `src/vitals/core/database.py` 的 `init_db()` 函数中,在 `config` 表创建后添加: ```python # 用户表 cursor.execute(""" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, created_at TEXT NOT NULL, is_active INTEGER DEFAULT 0 ) """) ``` **Step 4: 实现用户 CRUD 函数** 在 `src/vitals/core/database.py` 末尾添加: ```python # ===== 用户管理 ===== def add_user(user: User) -> int: """添加用户""" with get_connection() as conn: cursor = conn.cursor() cursor.execute(""" INSERT INTO users (name, created_at, is_active) VALUES (?, ?, ?) """, ( user.name, user.created_at.isoformat(), 1 if user.is_active else 0, )) return cursor.lastrowid def get_users() -> list[User]: """获取所有用户""" with get_connection() as conn: cursor = conn.cursor() cursor.execute("SELECT * FROM users ORDER BY id") return [ User( id=row["id"], name=row["name"], created_at=datetime.fromisoformat(row["created_at"]), is_active=bool(row["is_active"]), ) for row in cursor.fetchall() ] def get_user(user_id: int) -> Optional[User]: """按 ID 获取用户""" with get_connection() as conn: cursor = conn.cursor() cursor.execute("SELECT * FROM users WHERE id = ?", (user_id,)) row = cursor.fetchone() if row: return User( id=row["id"], name=row["name"], created_at=datetime.fromisoformat(row["created_at"]), is_active=bool(row["is_active"]), ) return None def update_user(user: User): """更新用户""" with get_connection() as conn: cursor = conn.cursor() cursor.execute(""" UPDATE users SET name = ?, is_active = ? WHERE id = ? """, (user.name, 1 if user.is_active else 0, user.id)) def delete_user(user_id: int): """删除用户""" with get_connection() as conn: cursor = conn.cursor() cursor.execute("DELETE FROM users WHERE id = ?", (user_id,)) def set_active_user(user_id: int): """设置激活用户(同时取消其他用户的激活状态)""" with get_connection() as conn: cursor = conn.cursor() cursor.execute("UPDATE users SET is_active = 0") cursor.execute("UPDATE users SET is_active = 1 WHERE id = ?", (user_id,)) def get_active_user() -> Optional[User]: """获取当前激活的用户""" with get_connection() as conn: cursor = conn.cursor() cursor.execute("SELECT * FROM users WHERE is_active = 1") row = cursor.fetchone() if row: return User( id=row["id"], name=row["name"], created_at=datetime.fromisoformat(row["created_at"]), is_active=bool(row["is_active"]), ) return None ``` 同时在文件顶部导入添加: ```python from .models import Exercise, Meal, Sleep, Weight, UserConfig, User ``` 以及: ```python from datetime import date, time, datetime ``` **Step 5: 运行测试确认通过** ```bash pytest tests/test_database.py::TestUserDB -v ``` Expected: PASS **Step 6: 提交** ```bash git add src/vitals/core/database.py tests/test_database.py git commit -m "feat: add users table and CRUD functions" ``` --- ## Task 4: 数据库 - 添加 user_id 字段和迁移 **Files:** - Modify: `src/vitals/core/database.py` - Test: `tests/test_database.py` **Step 1: 编写迁移测试** 在 `tests/test_database.py` 末尾添加: ```python class TestUserIdMigration: """user_id 迁移测试""" def test_ensure_default_user_creates_user(self): """测试 ensure_default_user 创建默认用户""" db.ensure_default_user() users = db.get_users() assert len(users) >= 1 # 应有一个激活用户 active = db.get_active_user() assert active is not None def test_existing_data_gets_default_user_id(self): """测试现有数据关联到默认用户""" from src.vitals.core.models import Exercise # 先添加一条运动记录(无 user_id) exercise = Exercise( date=date(2026, 1, 18), type="跑步", duration=30, calories=200, ) db.add_exercise(exercise) # 运行迁移 db.ensure_default_user() # 获取默认用户的数据 active = db.get_active_user() exercises = db.get_exercises(user_id=active.id) assert len(exercises) >= 1 ``` **Step 2: 运行测试确认失败** ```bash pytest tests/test_database.py::TestUserIdMigration -v ``` Expected: FAIL **Step 3: 修改 init_db() 添加 user_id 字段** 修改 `src/vitals/core/database.py` 中的 `init_db()` 函数,在各表定义中添加 `user_id` 字段: ```python def init_db(): """初始化数据库表""" with get_connection() as conn: cursor = conn.cursor() # 用户表 cursor.execute(""" CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, created_at TEXT NOT NULL, is_active INTEGER DEFAULT 0 ) """) # 运动记录表 cursor.execute(""" CREATE TABLE IF NOT EXISTS exercise ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER DEFAULT 1, date DATE NOT NULL, type TEXT NOT NULL, duration INTEGER NOT NULL, calories INTEGER DEFAULT 0, distance REAL, heart_rate_avg INTEGER, source TEXT DEFAULT '手动', raw_data TEXT, notes TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ) """) # 饮食记录表 cursor.execute(""" CREATE TABLE IF NOT EXISTS meal ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER DEFAULT 1, date DATE NOT NULL, meal_type TEXT NOT NULL, description TEXT, calories INTEGER DEFAULT 0, protein REAL, carbs REAL, fat REAL, photo_path TEXT, food_items TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ) """) # 睡眠记录表 cursor.execute(""" CREATE TABLE IF NOT EXISTS sleep ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER DEFAULT 1, date DATE NOT NULL, bedtime TEXT, wake_time TEXT, duration REAL NOT NULL, quality INTEGER DEFAULT 3, deep_sleep_mins INTEGER, source TEXT DEFAULT '手动', notes TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ) """) # 体重记录表 cursor.execute(""" CREATE TABLE IF NOT EXISTS weight ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER DEFAULT 1, date DATE NOT NULL, weight_kg REAL NOT NULL, body_fat_pct REAL, muscle_mass REAL, notes TEXT, FOREIGN KEY (user_id) REFERENCES users(id) ) """) # 用户配置表 cursor.execute(""" CREATE TABLE IF NOT EXISTS config ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER DEFAULT 1, key TEXT NOT NULL, value TEXT, UNIQUE(user_id, key), FOREIGN KEY (user_id) REFERENCES users(id) ) """) # 创建索引 cursor.execute("CREATE INDEX IF NOT EXISTS idx_exercise_date ON exercise(date)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_exercise_user ON exercise(user_id)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_meal_date ON meal(date)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_meal_user ON meal(user_id)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_sleep_date ON sleep(date)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_sleep_user ON sleep(user_id)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_weight_date ON weight(date)") cursor.execute("CREATE INDEX IF NOT EXISTS idx_weight_user ON weight(user_id)") ``` **Step 4: 实现 ensure_default_user() 迁移函数** 在 `src/vitals/core/database.py` 末尾添加: ```python def ensure_default_user(): """确保存在默认用户,并将无 user_id 的数据关联到默认用户""" with get_connection() as conn: cursor = conn.cursor() # 检查是否已有用户 cursor.execute("SELECT COUNT(*) as count FROM users") count = cursor.fetchone()["count"] if count == 0: # 创建默认用户 cursor.execute(""" INSERT INTO users (name, created_at, is_active) VALUES (?, ?, 1) """, ("默认用户", datetime.now().isoformat())) # 获取激活用户(如果没有则设置第一个用户为激活) cursor.execute("SELECT id FROM users WHERE is_active = 1") active = cursor.fetchone() if not active: cursor.execute("SELECT id FROM users ORDER BY id LIMIT 1") first = cursor.fetchone() if first: cursor.execute("UPDATE users SET is_active = 1 WHERE id = ?", (first["id"],)) active = first if active: default_user_id = active["id"] # 迁移现有数据(将 user_id 为 NULL 或不存在的记录关联到默认用户) for table in ["exercise", "meal", "sleep", "weight"]: # 检查表是否有 user_id 列 cursor.execute(f"PRAGMA table_info({table})") columns = [col["name"] for col in cursor.fetchall()] if "user_id" not in columns: # 添加 user_id 列 cursor.execute(f"ALTER TABLE {table} ADD COLUMN user_id INTEGER DEFAULT 1") # 更新 NULL 的 user_id cursor.execute(f"UPDATE {table} SET user_id = ? WHERE user_id IS NULL", (default_user_id,)) ``` **Step 5: 修改现有查询函数支持 user_id** 修改 `get_exercises` 函数: ```python def get_exercises(start_date: Optional[date] = None, end_date: Optional[date] = None, user_id: Optional[int] = None) -> list[Exercise]: """查询运动记录""" with get_connection() as conn: cursor = conn.cursor() query = "SELECT * FROM exercise WHERE 1=1" params = [] if user_id: query += " AND user_id = ?" params.append(user_id) if start_date: query += " AND date >= ?" params.append(start_date.isoformat()) if end_date: query += " AND date <= ?" params.append(end_date.isoformat()) query += " ORDER BY date DESC" cursor.execute(query, params) return [ Exercise( id=row["id"], date=date.fromisoformat(row["date"]), type=row["type"], duration=row["duration"], calories=row["calories"], distance=row["distance"], heart_rate_avg=row["heart_rate_avg"], source=row["source"], raw_data=json.loads(row["raw_data"]) if row["raw_data"] else None, notes=row["notes"], ) for row in cursor.fetchall() ] ``` 类似地修改 `get_meals`, `get_sleep_records`, `get_weight_records` 函数,添加 `user_id` 参数。 同时修改 `add_exercise`, `add_meal`, `add_sleep`, `add_weight` 函数,添加 `user_id` 参数: ```python def add_exercise(exercise: Exercise, user_id: int = 1) -> int: """添加运动记录""" with get_connection() as conn: cursor = conn.cursor() cursor.execute(""" INSERT INTO exercise (user_id, date, type, duration, calories, distance, heart_rate_avg, source, raw_data, notes) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, ( user_id, exercise.date.isoformat(), exercise.type, exercise.duration, exercise.calories, exercise.distance, exercise.heart_rate_avg, exercise.source, json.dumps(exercise.raw_data) if exercise.raw_data else None, exercise.notes, )) return cursor.lastrowid ``` **Step 6: 运行测试确认通过** ```bash pytest tests/test_database.py::TestUserIdMigration -v ``` Expected: PASS **Step 7: 运行所有数据库测试确保没有破坏现有功能** ```bash pytest tests/test_database.py -v ``` Expected: ALL PASS **Step 8: 提交** ```bash git add src/vitals/core/database.py tests/test_database.py git commit -m "feat: add user_id to data tables with migration support" ``` --- ## Task 5: 数据库 - 数据清除函数 **Files:** - Modify: `src/vitals/core/database.py` - Test: `tests/test_database.py` **Step 1: 编写数据清除测试** 在 `tests/test_database.py` 末尾添加: ```python class TestDataClear: """数据清除测试""" def test_preview_delete_all(self): """测试预览删除全部""" from src.vitals.core.models import Exercise, Meal, User # 创建用户和数据 user_id = db.add_user(User(name="测试用户")) db.add_exercise(Exercise(date=date(2026, 1, 10), type="跑步", duration=30, calories=200), user_id) db.add_exercise(Exercise(date=date(2026, 1, 11), type="游泳", duration=45, calories=300), user_id) db.add_meal(Meal(date=date(2026, 1, 10), meal_type="午餐", description="米饭", calories=500), user_id) counts = db.preview_delete(user_id, mode="all") assert counts["exercise"] == 2 assert counts["meal"] == 1 assert counts["sleep"] == 0 assert counts["weight"] == 0 assert counts["total"] == 3 def test_preview_delete_by_range(self): """测试预览按时间范围删除""" from src.vitals.core.models import Exercise, User user_id = db.add_user(User(name="测试用户")) db.add_exercise(Exercise(date=date(2026, 1, 5), type="跑步", duration=30, calories=200), user_id) db.add_exercise(Exercise(date=date(2026, 1, 10), type="跑步", duration=30, calories=200), user_id) db.add_exercise(Exercise(date=date(2026, 1, 15), type="跑步", duration=30, calories=200), user_id) counts = db.preview_delete(user_id, mode="range", date_from=date(2026, 1, 8), date_to=date(2026, 1, 12)) assert counts["exercise"] == 1 assert counts["total"] == 1 def test_preview_delete_by_type(self): """测试预览按类型删除""" from src.vitals.core.models import Exercise, Meal, User user_id = db.add_user(User(name="测试用户")) db.add_exercise(Exercise(date=date(2026, 1, 10), type="跑步", duration=30, calories=200), user_id) db.add_meal(Meal(date=date(2026, 1, 10), meal_type="午餐", description="米饭", calories=500), user_id) counts = db.preview_delete(user_id, mode="type", data_types=["exercise"]) assert counts["exercise"] == 1 assert counts["meal"] == 0 assert counts["total"] == 1 def test_clear_data_all(self): """测试清除全部数据""" from src.vitals.core.models import Exercise, Meal, User user_id = db.add_user(User(name="测试用户")) db.add_exercise(Exercise(date=date(2026, 1, 10), type="跑步", duration=30, calories=200), user_id) db.add_meal(Meal(date=date(2026, 1, 10), meal_type="午餐", description="米饭", calories=500), user_id) db.clear_data(user_id, mode="all") assert len(db.get_exercises(user_id=user_id)) == 0 assert len(db.get_meals(user_id=user_id)) == 0 def test_clear_data_by_range(self): """测试按时间范围清除""" from src.vitals.core.models import Exercise, User user_id = db.add_user(User(name="测试用户")) db.add_exercise(Exercise(date=date(2026, 1, 5), type="跑步", duration=30, calories=200), user_id) db.add_exercise(Exercise(date=date(2026, 1, 10), type="跑步", duration=30, calories=200), user_id) db.add_exercise(Exercise(date=date(2026, 1, 15), type="跑步", duration=30, calories=200), user_id) db.clear_data(user_id, mode="range", date_from=date(2026, 1, 8), date_to=date(2026, 1, 12)) exercises = db.get_exercises(user_id=user_id) assert len(exercises) == 2 dates = [e.date for e in exercises] assert date(2026, 1, 10) not in dates def test_clear_data_by_type(self): """测试按类型清除""" from src.vitals.core.models import Exercise, Meal, User user_id = db.add_user(User(name="测试用户")) db.add_exercise(Exercise(date=date(2026, 1, 10), type="跑步", duration=30, calories=200), user_id) db.add_meal(Meal(date=date(2026, 1, 10), meal_type="午餐", description="米饭", calories=500), user_id) db.clear_data(user_id, mode="type", data_types=["exercise"]) assert len(db.get_exercises(user_id=user_id)) == 0 assert len(db.get_meals(user_id=user_id)) == 1 ``` **Step 2: 运行测试确认失败** ```bash pytest tests/test_database.py::TestDataClear -v ``` Expected: FAIL **Step 3: 实现 preview_delete 和 clear_data 函数** 在 `src/vitals/core/database.py` 末尾添加: ```python def preview_delete( user_id: int, mode: str = "all", date_from: Optional[date] = None, date_to: Optional[date] = None, data_types: Optional[list] = None, ) -> dict: """预览将删除的数据量""" tables = ["exercise", "meal", "sleep", "weight"] if mode == "type" and data_types: tables = [t for t in tables if t in data_types] counts = {} total = 0 with get_connection() as conn: cursor = conn.cursor() for table in ["exercise", "meal", "sleep", "weight"]: if mode == "type" and data_types and table not in data_types: counts[table] = 0 continue query = f"SELECT COUNT(*) as count FROM {table} WHERE user_id = ?" params = [user_id] if mode == "range" and date_from and date_to: query += " AND date >= ? AND date <= ?" params.extend([date_from.isoformat(), date_to.isoformat()]) cursor.execute(query, params) count = cursor.fetchone()["count"] counts[table] = count total += count counts["total"] = total return counts def clear_data( user_id: int, mode: str = "all", date_from: Optional[date] = None, date_to: Optional[date] = None, data_types: Optional[list] = None, ): """清除数据""" tables = ["exercise", "meal", "sleep", "weight"] if mode == "type" and data_types: tables = [t for t in tables if t in data_types] with get_connection() as conn: cursor = conn.cursor() for table in tables: query = f"DELETE FROM {table} WHERE user_id = ?" params = [user_id] if mode == "range" and date_from and date_to: query += " AND date >= ? AND date <= ?" params.extend([date_from.isoformat(), date_to.isoformat()]) cursor.execute(query, params) ``` **Step 4: 运行测试确认通过** ```bash pytest tests/test_database.py::TestDataClear -v ``` Expected: PASS **Step 5: 提交** ```bash git add src/vitals/core/database.py tests/test_database.py git commit -m "feat: add data preview and clear functions" ``` --- ## Task 6: 用户管理 API **Files:** - Modify: `src/vitals/web/app.py` - Test: `tests/test_web.py` **Step 1: 添加用户相关 Pydantic 模型** 在 `src/vitals/web/app.py` 的 Pydantic 模型部分添加: ```python class UserResponse(BaseModel): id: int name: str created_at: str is_active: bool class UserInput(BaseModel): name: str @field_validator("name") @classmethod def validate_name(cls, value: str) -> str: if not value or len(value.strip()) == 0: raise ValueError("用户名不能为空") if len(value) > 20: raise ValueError("用户名不能超过 20 个字符") return value.strip() ``` **Step 2: 实现用户管理 API 端点** 在 `src/vitals/web/app.py` 的 API 路由部分添加: ```python # ===== 用户管理 API ===== @app.get("/api/users") async def get_users(): """获取所有用户""" db.ensure_default_user() users = db.get_users() return [UserResponse( id=u.id, name=u.name, created_at=u.created_at.isoformat(), is_active=u.is_active, ) for u in users] @app.get("/api/users/current") async def get_current_user(): """获取当前激活用户""" db.ensure_default_user() user = db.get_active_user() if not user: raise HTTPException(status_code=404, detail="未找到激活用户") return UserResponse( id=user.id, name=user.name, created_at=user.created_at.isoformat(), is_active=user.is_active, ) @app.post("/api/users") async def create_user(user_input: UserInput): """创建新用户""" from ..core.models import User user = User(name=user_input.name) user_id = db.add_user(user) created = db.get_user(user_id) return UserResponse( id=created.id, name=created.name, created_at=created.created_at.isoformat(), is_active=created.is_active, ) @app.put("/api/users/{user_id}") async def update_user(user_id: int, user_input: UserInput): """更新用户""" user = db.get_user(user_id) if not user: raise HTTPException(status_code=404, detail="用户不存在") user.name = user_input.name db.update_user(user) return UserResponse( id=user.id, name=user.name, created_at=user.created_at.isoformat(), is_active=user.is_active, ) @app.delete("/api/users/{user_id}") async def delete_user(user_id: int): """删除用户及其所有数据""" user = db.get_user(user_id) if not user: raise HTTPException(status_code=404, detail="用户不存在") # 检查是否是最后一个用户 users = db.get_users() if len(users) <= 1: raise HTTPException(status_code=400, detail="不能删除最后一个用户") # 删除用户数据 db.clear_data(user_id, mode="all") # 删除用户 db.delete_user(user_id) # 如果删除的是激活用户,激活第一个用户 if user.is_active: remaining = db.get_users() if remaining: db.set_active_user(remaining[0].id) return {"message": "用户已删除"} @app.post("/api/users/{user_id}/switch") async def switch_user(user_id: int): """切换到指定用户""" user = db.get_user(user_id) if not user: raise HTTPException(status_code=404, detail="用户不存在") db.set_active_user(user_id) return UserResponse( id=user.id, name=user.name, created_at=user.created_at.isoformat(), is_active=True, ) ``` **Step 3: 在 models.py 导入中添加 User** 确保 `src/vitals/web/app.py` 顶部导入包含 User: ```python from ..core.models import Exercise, Meal, Sleep, UserConfig, Weight, User ``` **Step 4: 运行测试** ```bash pytest tests/test_web.py -v ``` Expected: PASS **Step 5: 提交** ```bash git add src/vitals/web/app.py git commit -m "feat: add user management API endpoints" ``` --- ## Task 7: 数据清除 API **Files:** - Modify: `src/vitals/web/app.py` **Step 1: 添加数据清除 Pydantic 模型** 在 `src/vitals/web/app.py` 的 Pydantic 模型部分添加: ```python class DataClearInput(BaseModel): user_id: int mode: str # "range" | "type" | "all" date_from: Optional[str] = None date_to: Optional[str] = None data_types: Optional[list[str]] = None confirm_text: str @field_validator("mode") @classmethod def validate_mode(cls, value: str) -> str: if value not in ["range", "type", "all"]: raise ValueError("无效的清除模式") return value @field_validator("confirm_text") @classmethod def validate_confirm(cls, value: str) -> str: if value != "确认删除": raise ValueError("确认文字不正确") return value ``` **Step 2: 实现数据清除 API 端点** ```python # ===== 数据清除 API ===== @app.post("/api/data/preview-delete") async def preview_delete( user_id: int = Query(...), mode: str = Query("all"), date_from: Optional[str] = Query(None), date_to: Optional[str] = Query(None), data_types: Optional[str] = Query(None), ): """预览将删除的数据量""" from datetime import date as date_type df = date_type.fromisoformat(date_from) if date_from else None dt = date_type.fromisoformat(date_to) if date_to else None types = data_types.split(",") if data_types else None counts = db.preview_delete( user_id=user_id, mode=mode, date_from=df, date_to=dt, data_types=types, ) return counts @app.delete("/api/data/clear") async def clear_data(clear_input: DataClearInput): """执行数据清除""" from datetime import date as date_type df = date_type.fromisoformat(clear_input.date_from) if clear_input.date_from else None dt = date_type.fromisoformat(clear_input.date_to) if clear_input.date_to else None # 预览并返回删除数量 counts = db.preview_delete( user_id=clear_input.user_id, mode=clear_input.mode, date_from=df, date_to=dt, data_types=clear_input.data_types, ) # 执行删除 db.clear_data( user_id=clear_input.user_id, mode=clear_input.mode, date_from=df, date_to=dt, data_types=clear_input.data_types, ) return {"message": "数据已清除", "deleted": counts} ``` **Step 3: 运行测试** ```bash pytest tests/test_web.py -v ``` Expected: PASS **Step 4: 提交** ```bash git add src/vitals/web/app.py git commit -m "feat: add data clear API endpoints" ``` --- ## Task 8: 修改现有 API 支持多用户 **Files:** - Modify: `src/vitals/web/app.py` **Step 1: 创建获取当前用户 ID 的辅助函数** 在 `src/vitals/web/app.py` 添加辅助函数: ```python def get_current_user_id() -> int: """获取当前激活用户的 ID""" db.ensure_default_user() user = db.get_active_user() return user.id if user else 1 ``` **Step 2: 修改现有 API 端点使用 user_id** 修改 `/api/today` 端点: ```python @app.get("/api/today") async def get_today_summary(): """获取今日摘要""" user_id = get_current_user_id() today = date.today() meals = db.get_meals(start_date=today, end_date=today, user_id=user_id) # ... 其余代码使用 user_id 参数 ``` 类似地修改以下端点,添加 `user_id=user_id` 参数: - `/api/week` - `/api/exercises` - `/api/meals` - `/api/sleep` - `/api/weight` - `POST /api/exercise` - `POST /api/meal` - `POST /api/sleep` - `POST /api/weight` **Step 3: 运行所有测试** ```bash pytest -v ``` Expected: ALL PASS **Step 4: 提交** ```bash git add src/vitals/web/app.py git commit -m "feat: update existing APIs to support multi-user" ``` --- ## Task 9: 设置页面 - 路由和基础 HTML **Files:** - Modify: `src/vitals/web/app.py` **Step 1: 添加设置页面路由** ```python @app.get("/settings") async def settings_page(): """设置页面""" return HTMLResponse(content=get_settings_page_html(), status_code=200) ``` **Step 2: 实现设置页面 HTML 生成函数** 在 `src/vitals/web/app.py` 末尾添加 `get_settings_page_html()` 函数。由于代码较长,这里分为几个部分: **CSS 样式部分:** ```python def get_settings_page_html() -> str: """生成设置页面 HTML""" return """
管理用户和系统配置
当前用户: 加载中...