diff --git a/.env.example b/.env.example index 9c5ba1c..dec314d 100644 --- a/.env.example +++ b/.env.example @@ -2,13 +2,13 @@ # 管理员账户(首次启动时自动创建) ADMIN_USERNAME=admin -ADMIN_PASSWORD=change_me_please +ADMIN_PASSWORD=admin0905 # JWT 密钥(请使用随机字符串,可用 openssl rand -hex 32 生成) JWT_SECRET=your_random_secret_key_here # AI 食物识别(可选,二选一) # 阿里云通义千问 -DASHSCOPE_API_KEY=your_dashscope_api_key_here +DASHSCOPE_API_KEY=sk-e3b36e63947445c8bb51a87a7ef15850dashscope_api_key_here # DeepSeek DEEPSEEK_API_KEY=your_deepseek_api_key_here diff --git a/README.md b/README.md index 881f171..41fb4a3 100644 --- a/README.md +++ b/README.md @@ -345,17 +345,88 @@ vitals/ ## 开发进度 -### 已完成功能 +### 已完成功能 ✅ -- [x] **P1-1** 运动页面 - 运动记录列表、统计图表、数据录入 -- [x] **P1-2** 饮食页面 - 饮食记录、营养统计、日历视图 -- [x] **P1-3** 睡眠/体重页面 - 睡眠趋势、体重曲线、BMI 计算 -- [x] **P1-4** Web 数据录入 - 所有数据类型的表单录入 -- [x] **P1-5** AI 智能识别 - 通义千问/DeepSeek 食物识别 -- [x] **P2-1** 数据导出 - JSON/CSV 格式导出 -- [x] **P2-2** 自动备份 - 数据库备份与恢复 -- [x] **P3-1** 定时提醒 - 系统通知、定时任务 -- [x] **公网部署** - JWT 认证、邀请码注册、管理员面板、Docker 部署 +#### 核心数据追踪 +- [x] **运动记录** - 类型、时长、距离、卡路里、心率追踪,支持 Garmin/Codoon/CSV 导入 +- [x] **饮食记录** - 三餐+加餐,营养数据(卡路里/蛋白质/碳水/脂肪),照片上传 +- [x] **睡眠记录** - 入睡/起床时间、时长、质量评分(1-5)、深睡时长 +- [x] **体重记录** - 体重(kg)、体脂率(%)、肌肉量,趋势分析 +- [x] **阅读记录** - 书名、作者、封面(自动获取)、时长、心情(5种表情)、读后感 + +#### 用户认证与管理 +- [x] **JWT 认证** - 服务端认证 + Cookie,支持 1 天/30 天过期 +- [x] **密码安全** - bcrypt 哈希加密 +- [x] **多用户支持** - 用户档案、数据隔离、用户切换 +- [x] **邀请码注册** - 控制用户注册,支持有效期设置 +- [x] **管理员面板** - 用户管理(启用/禁用/删除)、邀请码管理 + +#### Web 应用 +- [x] **完整页面** - 登录、注册、管理员、首页、运动、饮食、睡眠、体重、阅读、报告、设置 +- [x] **认证中间件** - 保护所有页面(登录/注册除外) +- [x] **REST API** - 40+ 个接口,完整的 CRUD 操作 +- [x] **照片管理** - 饮食照片上传和静态文件服务 + +#### 数据管理 +- [x] **数据导出** - JSON 和 CSV 格式 +- [x] **数据导入** - JSON 文件导入(带验证) +- [x] **自动备份** - 数据写入后自动备份,7 天保留策略 +- [x] **备份恢复** - 从备份文件恢复数据 +- [x] **数据清理** - 按日期范围或数据类型删除,支持预览 + +#### AI 集成 +- [x] **食物识别** - 阿里通义千问(Qwen VL)、DeepSeek Vision、Claude Vision +- [x] **卡路里估算** - 基于食物描述的营养值计算 + +#### 数据导入器 +- [x] **Garmin 导入** - 从 Garmin 设备导入运动数据 +- [x] **Codoon 导入** - 支持咔豆运动数据导入 +- [x] **CSV 导入** - 通用 CSV 导入(运动/饮食/睡眠/体重) + +#### 报告与分析 +- [x] **今日概览** - 所有健康指标汇总 +- [x] **本周汇总** - 7 天趋势分析 +- [x] **阅读统计** - 时长趋势、心情分布、书库展示 +- [x] **图表展示** - 运动趋势、营养统计、睡眠热力图 + +#### CLI 工具 +- [x] **数据记录** - `vitals log` 记录各类数据 +- [x] **数据查看** - `vitals show` 显示汇总 +- [x] **数据导出** - `vitals export` 导出 JSON/CSV +- [x] **数据备份** - `vitals backup` 管理备份 + +#### 部署 +- [x] **Docker 支持** - Dockerfile + docker-compose.yml +- [x] **环境变量配置** - 灵活的配置管理 +- [x] **健康检查** - Docker 容器健康检查 + +### 待完善功能 🚧 + +#### 高优先级 +- [ ] **提醒系统完善** - 定时提醒功能已有框架,但系统集成需要完善 +- [ ] **数据编辑功能** - 目前仅支持删除,需要添加编辑已有记录的功能 +- [ ] **错误处理优化** - 前端错误提示和异常处理需要更友好 + +#### 中优先级 +- [ ] **月度报告** - PDF 报告生成功能(已有框架,需完善) +- [ ] **目标设定** - 运动目标、饮食目标、体重目标的设定和追踪 +- [ ] **数据统计增强** - 更丰富的图表和趋势分析 +- [ ] **移动端优化** - 响应式设计改进,更好的移动端体验 +- [ ] **离线支持** - PWA 支持,离线数据缓存 + +#### 低优先级 +- [ ] **国际化(i18n)** - 多语言支持 +- [ ] **云同步** - 跨设备数据同步 +- [ ] **社交功能** - 数据分享、好友挑战 +- [ ] **智能建议** - 基于数据的个性化健康建议 +- [ ] **Apple Health 集成** - 与 Apple Health 数据同步 +- [ ] **深色模式** - UI 深色主题支持 + +### 已知问题 🐛 + +- [ ] 大量数据时列表加载性能需优化 +- [ ] 部分页面在小屏幕上布局需要调整 +- [ ] 书籍封面搜索有时无法找到匹配结果 ## License diff --git a/src/vitals/web/app.py b/src/vitals/web/app.py index 8eb43e2..d73307a 100644 --- a/src/vitals/web/app.py +++ b/src/vitals/web/app.py @@ -528,7 +528,7 @@ def get_current_user_id(authorization: Optional[str] = Header(None)) -> int: # ===== 认证 API ===== @app.post("/api/auth/login") -async def login(data: LoginInput, response: Response): +async def login(data: LoginInput): """用户登录""" user = db.get_user_by_name(data.username) if not user: @@ -543,6 +543,17 @@ async def login(data: LoginInput, response: Response): # 创建 token(根据 remember_me 设置不同的过期时间) token = create_token(user.id, user.name, user.is_admin, data.remember_me) + # 创建响应并设置 Cookie + response = JSONResponse(content={ + "token": token, + "user": { + "id": user.id, + "name": user.name, + "is_admin": user.is_admin, + "is_disabled": user.is_disabled, + } + }) + # 设置 HTTPOnly Cookie max_age = get_token_expire_seconds(data.remember_me) if data.remember_me else None response.set_cookie( @@ -552,21 +563,14 @@ async def login(data: LoginInput, response: Response): secure=False, # HTTP 环境,生产环境应设为 True samesite="lax", max_age=max_age, # None 表示会话 Cookie + path="/", # 确保所有路径都能使用此 Cookie ) - return { - "token": token, - "user": { - "id": user.id, - "name": user.name, - "is_admin": user.is_admin, - "is_disabled": user.is_disabled, - } - } + return response @app.post("/api/auth/register") -async def register(data: RegisterInput, response: Response): +async def register(data: RegisterInput): """用户注册(需要邀请码)""" # 验证邀请码 invite = db.get_invite_by_code(data.invite_code) @@ -599,18 +603,9 @@ async def register(data: RegisterInput, response: Response): # 获取创建的用户 user = db.get_user(user_id) - # 生成 token 并设置 Cookie + # 生成 token 并创建响应 token = create_token(user.id, user.name, user.is_admin) - response.set_cookie( - key="auth_token", - value=token, - httponly=True, - secure=False, - samesite="lax", - max_age=get_token_expire_seconds(False), # 注册默认 1 天 - ) - - return { + response = JSONResponse(content={ "token": token, "user": { "id": user.id, @@ -618,7 +613,20 @@ async def register(data: RegisterInput, response: Response): "is_admin": user.is_admin, "is_disabled": user.is_disabled, } - } + }) + + # 设置 Cookie + response.set_cookie( + key="auth_token", + value=token, + httponly=True, + secure=False, + samesite="lax", + max_age=get_token_expire_seconds(False), # 注册默认 1 天 + path="/", + ) + + return response @app.post("/api/auth/logout") @@ -2228,17 +2236,24 @@ def get_login_page_html() -> str: const data = await response.json(); if (response.ok) { - // 保存用户信息(非敏感) + // 保存 token 和用户信息 + localStorage.setItem('token', data.token); localStorage.setItem('user', JSON.stringify(data.user)); // 显示成功状态 btn.textContent = '✓ 登录成功'; btn.classList.add('success'); - // 跳转 + // 跳转(管理员跳转到管理页面) setTimeout(() => { const params = new URLSearchParams(window.location.search); - const redirect = params.get('redirect') || '/'; + let redirect = params.get('redirect'); + + // 如果没有指定重定向,管理员默认跳转到 /admin + if (!redirect) { + redirect = data.user.is_admin ? '/admin' : '/'; + } + window.location.href = redirect; }, 500); } else { @@ -2566,7 +2581,8 @@ def get_register_page_html() -> str: const data = await response.json(); if (response.ok) { - // 保存用户信息 + // 保存 token 和用户信息 + localStorage.setItem('token', data.token); localStorage.setItem('user', JSON.stringify(data.user)); // 显示成功 @@ -2959,18 +2975,28 @@ def get_admin_page_html() -> str: } catch (e) { // 忽略错误 } + localStorage.removeItem('token'); localStorage.removeItem('user'); window.location.href = '/login'; } + function getToken() { + return localStorage.getItem('token'); + } + async function apiRequest(url, options = {}) { + const token = getToken(); + const headers = { + 'Content-Type': 'application/json', + ...options.headers, + }; + if (token) { + headers['Authorization'] = `Bearer ${token}`; + } const response = await fetch(url, { ...options, credentials: 'same-origin', - headers: { - 'Content-Type': 'application/json', - ...options.headers, - } + headers }); if (response.status === 401) { logout(); @@ -7133,6 +7159,26 @@ def get_settings_page_html() -> str: + +
当前登录账户管理
+ +