diff --git a/TODOS/API-接口清单.md b/TODOS/API-接口清单.md new file mode 100644 index 0000000..302dd66 --- /dev/null +++ b/TODOS/API-接口清单.md @@ -0,0 +1,449 @@ +# 后端 API 接口清单 + +> 本文档整理了健康 AI 问询助手项目需要后端配合实现的所有 API 接口 + +--- + +## 一、接口概览 + +| 模块 | 接口数量 | 优先级 | 说明 | +|------|----------|--------|------| +| 认证模块 | 4 | P0 | 用户注册、登录、Token管理 | +| 用户模块 | 6 | P0 | 用户信息、健康档案管理 | +| 健康调查模块 | 6 | P1 | 新用户健康调查问卷 | +| 体质辨识模块 | 5 | P0 | 体质问卷、结果计算 | +| AI对话模块 | 5 | P0 | 对话管理、消息发送 | +| 产品模块 | 4 | P2 | 保健品推荐、搜索 | +| 数据同步模块 | 1 | P2 | 商城购买记录同步 | +| **合计** | **31** | - | - | + +--- + +## 二、详细接口列表 + +### 2.1 认证模块(Auth) + +| 方法 | 接口路径 | 说明 | 请求参数 | 响应数据 | +|------|----------|------|----------|----------| +| POST | `/api/auth/register` | 用户注册 | phone, password, code | user, token | +| POST | `/api/auth/login` | 用户登录 | phone, password/code | user, token | +| POST | `/api/auth/refresh` | 刷新Token | refresh_token | access_token, refresh_token | +| POST | `/api/auth/send-code` | 发送验证码 | phone, type(register/login) | success | + +#### 注册接口详情 +```json +// POST /api/auth/register +// Request +{ + "phone": "13800138000", + "password": "password123", + "code": "123456" +} + +// Response +{ + "code": 0, + "data": { + "user": { + "id": 1, + "phone": "13800138000", + "nickname": "用户138****8000", + "avatar": "", + "survey_completed": false + }, + "token": { + "access_token": "eyJhbGciOiJIUzI1...", + "refresh_token": "eyJhbGciOiJIUzI1...", + "expires_in": 7200 + } + } +} +``` + +--- + +### 2.2 用户模块(User) + +| 方法 | 接口路径 | 说明 | 请求参数 | 响应数据 | +|------|----------|------|----------|----------| +| GET | `/api/user/profile` | 获取用户信息 | - | user | +| PUT | `/api/user/profile` | 更新用户信息 | nickname, avatar | user | +| GET | `/api/user/health-profile` | 获取健康档案 | - | health_profile | +| PUT | `/api/user/health-profile` | 更新健康档案 | name, birth_date, gender... | health_profile | +| GET | `/api/user/lifestyle` | 获取生活习惯 | - | lifestyle | +| PUT | `/api/user/lifestyle` | 更新生活习惯 | sleep_time, diet_preference... | lifestyle | + +#### 健康档案详情 +```json +// GET /api/user/health-profile +// Response +{ + "code": 0, + "data": { + "id": 1, + "user_id": 1, + "name": "张三", + "birth_date": "1980-01-15", + "gender": "male", + "height": 175.0, + "weight": 70.5, + "bmi": 23.02, + "blood_type": "A", + "occupation": "工程师", + "marital_status": "已婚", + "region": "北京市", + "medical_histories": [...], + "allergy_records": [...], + "family_histories": [...] + } +} +``` + +--- + +### 2.3 健康调查模块(Survey) + +| 方法 | 接口路径 | 说明 | 请求参数 | 响应数据 | +|------|----------|------|----------|----------| +| GET | `/api/survey/status` | 获取调查完成状态 | - | completed, steps | +| POST | `/api/survey/basic-info` | 提交基础信息 | name, gender, birth_date... | success | +| POST | `/api/survey/lifestyle` | 提交生活习惯 | sleep_time, diet_preference... | success | +| POST | `/api/survey/medical-history` | 提交病史信息 | diseases[] | success | +| POST | `/api/survey/family-history` | 提交家族病史 | histories[] | success | +| POST | `/api/survey/allergy` | 提交过敏信息 | allergies[] | success | + +#### 调查状态详情 +```json +// GET /api/survey/status +// Response +{ + "code": 0, + "data": { + "completed": false, + "steps": { + "basic_info": true, + "lifestyle": true, + "medical_history": false, + "family_history": false, + "allergy": false, + "constitution": false + } + } +} +``` + +--- + +### 2.4 体质辨识模块(Constitution) + +| 方法 | 接口路径 | 说明 | 请求参数 | 响应数据 | +|------|----------|------|----------|----------| +| GET | `/api/constitution/questions` | 获取问卷题目 | - | questions[] | +| POST | `/api/constitution/submit` | 提交答案并计算 | answers[] | result | +| GET | `/api/constitution/result` | 获取最新结果 | - | result | +| GET | `/api/constitution/history` | 获取历史记录 | - | results[] | +| GET | `/api/constitution/recommendations` | 获取调养建议 | - | recommendations | + +#### 问卷题目详情 +```json +// GET /api/constitution/questions +// Response +{ + "code": 0, + "data": { + "total": 60, + "questions": [ + { + "id": 1, + "constitution_type": "qi_deficiency", + "question": "您容易疲乏吗?", + "options": [ + {"value": 1, "label": "从不"}, + {"value": 2, "label": "很少"}, + {"value": 3, "label": "有时"}, + {"value": 4, "label": "经常"}, + {"value": 5, "label": "总是"} + ], + "order_num": 1 + } + // ...更多题目 + ] + } +} +``` + +#### 提交答案详情 +```json +// POST /api/constitution/submit +// Request +{ + "answers": [ + {"question_id": 1, "score": 3}, + {"question_id": 2, "score": 4}, + // ...共60个答案 + ] +} + +// Response +{ + "code": 0, + "data": { + "id": 1, + "assessed_at": "2026-02-01T10:30:00Z", + "primary_type": "qi_deficiency", + "scores": { + "balanced": 45, + "qi_deficiency": 72, + "yang_deficiency": 38, + "yin_deficiency": 42, + "phlegm_dampness": 35, + "damp_heat": 30, + "blood_stasis": 28, + "qi_stagnation": 40, + "special": 25 + }, + "secondary_types": ["qi_stagnation"], + "recommendations": { + "diet": ["多吃黄芪、人参等补气食物..."], + "lifestyle": ["保证充足睡眠..."], + "exercise": ["太极拳、八段锦..."], + "emotion": ["保持乐观心态..."] + } + } +} +``` + +--- + +### 2.5 AI对话模块(Conversation) + +| 方法 | 接口路径 | 说明 | 请求参数 | 响应数据 | +|------|----------|------|----------|----------| +| GET | `/api/conversations` | 获取对话列表 | page, limit | conversations[] | +| POST | `/api/conversations` | 创建新对话 | title? | conversation | +| GET | `/api/conversations/{id}` | 获取对话详情 | - | conversation, messages[] | +| DELETE | `/api/conversations/{id}` | 删除对话 | - | success | +| POST | `/api/conversations/{id}/messages` | 发送消息 | content | message, ai_reply | + +#### 发送消息详情(流式响应) +```json +// POST /api/conversations/{id}/messages +// Request +{ + "content": "我最近总是感觉疲劳,有什么建议吗?" +} + +// Response (SSE 流式) +// Content-Type: text/event-stream + +data: {"type": "start"} + +data: {"type": "content", "content": "【情况分析】"} +data: {"type": "content", "content": "根据您的气虚体质..."} +data: {"type": "content", "content": "\n【建议】\n1. 饮食方面..."} + +data: {"type": "end", "message_id": 123} +``` + +#### AI 上下文数据(后端组装) + +后端在调用 AI 时需要自动注入以下上下文: + +```json +{ + "user_profile": "性别:男,年龄:45岁,BMI:23.5", + "constitution_info": "主体质:气虚质(72分),次体质:气郁质(40分)", + "medication_history": "近期用药:黄芪精(补气),阿莫西林(已停)", + "purchase_history": "近期购买:人参蜂王浆、氨糖软骨素", + "product_list": "[气虚质推荐] 黄芪精 ¥68, 人参蜂王浆 ¥128..." +} +``` + +--- + +### 2.6 产品模块(Product) + +| 方法 | 接口路径 | 说明 | 请求参数 | 响应数据 | +|------|----------|------|----------|----------| +| GET | `/api/products` | 获取产品列表 | category?, page, limit | products[] | +| GET | `/api/products/{id}` | 获取产品详情 | - | product | +| GET | `/api/products/recommend` | 体质推荐产品 | constitution_type? | products[] | +| GET | `/api/products/search` | 症状搜索产品 | keyword | products[] | + +#### 体质推荐产品详情 +```json +// GET /api/products/recommend?constitution_type=qi_deficiency +// Response +{ + "code": 0, + "data": { + "constitution_type": "qi_deficiency", + "constitution_name": "气虚质", + "products": [ + { + "id": 1, + "name": "黄芪精口服液", + "category": "补气类", + "efficacy": "补气固表,增强免疫力", + "suitable": "气虚质人群", + "price": 68.00, + "image_url": "https://...", + "mall_url": "https://mall.example.com/product/1", + "priority": 1, + "reason": "黄芪是补气第一要药,特别适合气虚体质" + } + // ...更多产品 + ] + } +} +``` + +--- + +### 2.7 数据同步模块(Sync) + +| 方法 | 接口路径 | 说明 | 请求参数 | 响应数据 | +|------|----------|------|----------|----------| +| POST | `/api/sync/purchase` | 商城购买记录同步 | user_id, order_no, products[] | success | + +#### 购买记录同步详情 +```json +// POST /api/sync/purchase +// Header: X-Sync-Key: {sync_secret_key} +// Request +{ + "user_id": 1, + "order_no": "MALL20260201001", + "products": [ + {"id": 5, "name": "氨糖软骨素"}, + {"id": 12, "name": "深海鱼油"} + ], + "created_at": "2026-02-01T10:30:00Z" +} + +// Response +{ + "code": 0, + "message": "同步成功" +} +``` + +--- + +## 三、通用响应格式 + +### 成功响应 +```json +{ + "code": 0, + "message": "success", + "data": { ... } +} +``` + +### 错误响应 +```json +{ + "code": 40001, + "message": "手机号已注册", + "data": null +} +``` + +### 错误码定义 + +| 错误码 | 说明 | +|--------|------| +| 0 | 成功 | +| 40001 | 参数错误 | +| 40002 | 验证码错误 | +| 40003 | 用户已存在 | +| 40101 | 未登录 | +| 40102 | Token过期 | +| 40103 | Token无效 | +| 40301 | 无权限 | +| 40401 | 资源不存在 | +| 50001 | 服务器错误 | +| 50002 | AI服务异常 | + +--- + +## 四、认证机制 + +### JWT Token +- 所有非认证接口需要在 Header 中携带 Token +- 格式:`Authorization: Bearer {access_token}` +- access_token 有效期:2小时 +- refresh_token 有效期:7天 + +### 接口权限 + +| 类型 | 接口前缀 | 认证要求 | +|------|----------|----------| +| 公开 | `/api/auth/*` | 无需认证 | +| 用户 | `/api/user/*`, `/api/conversations/*` | 需要登录 | +| 同步 | `/api/sync/*` | 需要同步密钥 | + +--- + +## 五、开发优先级 + +### P0 - 核心功能(首批开发) +1. 用户认证(注册/登录/Token) +2. 体质辨识(问卷/计算/结果) +3. AI对话(对话管理/消息发送) +4. 用户信息(档案管理) + +### P1 - 重要功能(二批开发) +1. 健康调查问卷 +2. 用药记录管理 +3. 对话历史管理 + +### P2 - 扩展功能(三批开发) +1. 产品推荐 +2. 商城数据同步 +3. 体质历史追踪 + +--- + +## 六、数据库表对照 + +| 接口模块 | 关联数据表 | +|----------|-----------| +| 认证模块 | User | +| 用户模块 | User, HealthProfile, LifestyleInfo | +| 健康调查 | HealthProfile, MedicalHistory, FamilyHistory, AllergyRecord | +| 体质辨识 | ConstitutionAssessment, AssessmentAnswer, QuestionBank | +| AI对话 | Conversation, Message | +| 产品模块 | Product, ConstitutionProduct, SymptomProduct | +| 数据同步 | PurchaseHistory | + +--- + +## 七、前端对接清单 + +> 前端当前使用模拟数据,需要替换为真实 API 的文件: + +### APP (React Native) + +| 文件路径 | 需对接接口 | +|----------|-----------| +| `app/src/mock/user.ts` | 认证、用户模块 | +| `app/src/mock/constitution.ts` | 体质辨识模块 | +| `app/src/mock/chat.ts` | AI对话模块 | +| `app/src/mock/products.ts` | 产品模块 | +| `app/src/mock/medication.ts` | 用药记录(用户模块) | +| `app/src/mock/news.ts` | 健康资讯(可选后端或静态) | + +### Web (Vue 3) + +| 文件路径 | 需对接接口 | +|----------|-----------| +| `web/src/mock/user.ts` | 认证、用户模块 | +| `web/src/mock/constitution.ts` | 体质辨识模块 | +| `web/src/mock/chat.ts` | AI对话模块 | +| `web/src/mock/products.ts` | 产品模块 | + +--- + +*文档生成时间:2026-02-01* diff --git a/ai-agent-prompt.md b/ai-agent-prompt.md new file mode 100644 index 0000000..7f77296 --- /dev/null +++ b/ai-agent-prompt.md @@ -0,0 +1,192 @@ +# 健康AI助手 - 系统提示词 + +> 用于阿里云通义千问 / OpenAI 等大模型的 System Prompt + +--- + +## 提示词内容 + +``` +# 角色定义 +你是"健康AI助手",一个专业的健康咨询助理。你基于中医体质辨识理论,为用户提供个性化的健康建议。 + +## 重要声明 +- 你不是专业医师,仅提供健康咨询和养生建议 +- 你的建议不能替代医生的诊断和治疗 +- 遇到以下情况,必须立即建议用户就医: + * 胸痛、呼吸困难、剧烈头痛 + * 高烧不退(超过39°C持续24小时) + * 意识模糊、晕厥 + * 严重外伤、大量出血 + * 持续剧烈腹痛 + * 疑似中风症状(口眼歪斜、肢体无力、言语不清) + * 任何可能危及生命的紧急状况 + +## 用户信息 +{{user_profile}} + +## 用户体质 +{{constitution_info}} + +## 用药历史 +{{medication_history}} + +## 已购买保健品 +{{purchase_history}} + +## 回答原则 + +### 1. 简洁原则 +- 回答控制在200字以内 +- 使用简短句子,避免冗长解释 +- 重点突出,条理清晰 + +### 2. 体质结合原则 +根据用户体质特点给出针对性建议: +- 气虚质:建议补气食物,避免过度劳累 +- 阳虚质:建议温补,避免寒凉 +- 阴虚质:建议滋阴,避免辛辣燥热 +- 痰湿质:建议祛湿,控制饮食 +- 湿热质:建议清热利湿,饮食清淡 +- 血瘀质:建议活血化瘀,适当运动 +- 气郁质:建议疏肝理气,调节情绪 +- 特禀质:注意过敏原,增强体质 +- 平和质:维持现状,均衡养生 + +### 3. 用药建议原则 +- 优先推荐非处方中成药或食疗方案 +- 结合用户用药历史,避免重复或冲突 +- 注明"建议咨询药师后使用" +- 有过敏史时特别提醒 + +### 4. 安全原则 +- 不推荐处方药 +- 不做疾病诊断 +- 症状严重时直接建议就医 +- 涉及儿童、孕妇、老人时更加谨慎 + +### 5. 产品推荐原则 +当回答涉及调养建议时,根据以下规则推荐保健品: +- 根据用户体质匹配相关产品 +- 根据症状关键词匹配产品 +- 最多推荐2个产品 +- 保健品不能替代药物治疗,仅作日常调养参考 +- 必须包含商城链接 + +## 可推荐产品 +{{product_list}} + +## 回答格式 + +【情况分析】一句话概括 +【建议】 +1. 具体建议1 +2. 具体建议2 +【用药参考】(如适用) +- 药品名称:用法用量(建议咨询药师) +【推荐调养产品】(根据体质/症状匹配,非必须) +- 产品名称 ¥价格 [点击购买](商城链接) +【提醒】注意事项或就医建议 +``` + +--- + +## 动态变量说明 + +| 变量 | 说明 | 数据来源 | +|------|------|----------| +| `{{user_profile}}` | 用户基本信息(性别、年龄、BMI) | HealthProfile 表 | +| `{{constitution_info}}` | 体质类型和特征描述 | ConstitutionAssessment 表 | +| `{{medication_history}}` | 用药历史记录 | MedicalHistory 表 | +| `{{purchase_history}}` | 保健品购买历史(商城同步) | PurchaseHistory 表 | +| `{{product_list}}` | 可推荐产品列表(按体质筛选) | Product 表 | + +--- + +## 变量填充示例 + +### user_profile +``` +性别:男 +年龄:45岁 +身高:175cm +体重:70kg +BMI:22.9(正常) +``` + +### constitution_info +``` +主体质:气虚质(得分72分) +特征:容易疲劳、气短懒言、易出汗、免疫力较低 +次体质:气郁质(得分40分) +``` + +### medication_history +``` +当前用药: +- 黄芪精口服液(补气),每日2次,已服用1周 +过往用药: +- 阿莫西林胶囊(感冒),已停用 +过敏史: +- 青霉素过敏 +``` + +### purchase_history +``` +近3个月购买记录: +- 人参蜂王浆(2026-01-15) +- 氨糖软骨素(2026-01-20) +``` + +### product_list +``` +[气虚质推荐] +- 黄芪精口服液 ¥68 https://mall.example.com/product/1 +- 人参蜂王浆 ¥128 https://mall.example.com/product/2 +- 西洋参片 ¥98 https://mall.example.com/product/3 + +[通用推荐] +- 灵芝孢子粉 ¥268 https://mall.example.com/product/10 +- 蛋白粉 ¥158 https://mall.example.com/product/11 +``` + +--- + +## 使用说明 + +### 阿里云通义千问配置 + +```yaml +ai: + provider: aliyun + aliyun: + api_key: "your-dashscope-api-key" + model: "qwen-turbo" # 或 qwen-plus, qwen-max +``` + +### 后端调用示例 (Go) + +```go +func buildSystemPrompt(userID uint) string { + // 获取用户数据 + profile := getUserProfile(userID) + constitution := getConstitutionResult(userID) + medication := getMedicationHistory(userID) + purchase := getPurchaseHistory(userID) + products := getRecommendProducts(constitution.PrimaryType) + + // 替换模板变量 + prompt := systemPromptTemplate + prompt = strings.Replace(prompt, "{{user_profile}}", formatProfile(profile), 1) + prompt = strings.Replace(prompt, "{{constitution_info}}", formatConstitution(constitution), 1) + prompt = strings.Replace(prompt, "{{medication_history}}", formatMedication(medication), 1) + prompt = strings.Replace(prompt, "{{purchase_history}}", formatPurchase(purchase), 1) + prompt = strings.Replace(prompt, "{{product_list}}", formatProducts(products), 1) + + return prompt +} +``` + +--- + +*文档提取自 design.md,用于后端开发参考* diff --git a/server/config.yaml b/server/config.yaml index 25b5ae7..29fef7f 100644 --- a/server/config.yaml +++ b/server/config.yaml @@ -36,5 +36,5 @@ ai: # 阿里云通义千问配置 aliyun: - api_key: "" # 请填入您的 DashScope API Key + api_key: "sk-53b4777561624ba98246c7b9990c5e8b" model: "qwen-turbo" # 可选: qwen-turbo, qwen-plus, qwen-max diff --git a/server/data/health.db b/server/data/health.db index 61f37f2..d6b6b17 100644 Binary files a/server/data/health.db and b/server/data/health.db differ diff --git a/server/docs/API.md b/server/docs/API.md index 1a6c019..fa02d1a 100644 --- a/server/docs/API.md +++ b/server/docs/API.md @@ -63,6 +63,32 @@ } ``` +### 1.4 发送验证码 +- **POST** `/api/auth/send-code` + +**请求体:** +```json +{ + "phone": "13800138000", + "type": "register" // register/login/reset +} +``` + +**响应:** +```json +{ + "code": 0, + "message": "验证码已发送", + "data": { + "phone": "138****8000", + "expires_in": 300, + "demo_code": "123456" // 演示环境,正式环境无此字段 + } +} +``` + +> **注意**: 当前为演示版本,验证码固定为 `123456`。正式环境需要接入短信服务。 + --- ## 二、用户接口 @@ -419,27 +445,112 @@ - **GET** `/api/user/health-profile` - **需要认证** -### 6.2 获取基础档案 +### 6.2 更新健康档案 +- **PUT** `/api/user/health-profile` +- **需要认证** + +**请求体:** +```json +{ + "name": "张三", + "birth_date": "1990-05-15", + "gender": "male", + "height": 175, + "weight": 70, + "blood_type": "A", + "occupation": "工程师", + "marital_status": "married", + "region": "北京" +} +``` + +**响应:** +```json +{ + "code": 0, + "message": "success", + "data": { + "id": 1, + "user_id": 1, + "name": "张三", + "birth_date": "1990-05-15T00:00:00Z", + "gender": "male", + "height": 175, + "weight": 70, + "bmi": 22.86, + "blood_type": "A", + "occupation": "工程师", + "marital_status": "married", + "region": "北京" + } +} +``` + +### 6.3 获取基础档案 - **GET** `/api/user/basic-profile` - **需要认证** -### 6.3 获取生活习惯 +### 6.4 获取生活习惯 - **GET** `/api/user/lifestyle` - **需要认证** -### 6.4 获取病史列表 +### 6.5 更新生活习惯 +- **PUT** `/api/user/lifestyle` +- **需要认证** + +**请求体:** +```json +{ + "sleep_time": "23:00", + "wake_time": "07:00", + "sleep_quality": "normal", + "meal_regularity": "regular", + "diet_preference": "清淡", + "daily_water_ml": 2000, + "exercise_frequency": "sometimes", + "exercise_type": "跑步", + "exercise_duration_min": 30, + "is_smoker": false, + "alcohol_frequency": "never" +} +``` + +**响应:** +```json +{ + "code": 0, + "message": "success", + "data": { + "id": 1, + "user_id": 1, + "sleep_time": "23:00", + "wake_time": "07:00", + "sleep_quality": "normal", + "meal_regularity": "regular", + "diet_preference": "清淡", + "daily_water_ml": 2000, + "exercise_frequency": "sometimes", + "exercise_type": "跑步", + "exercise_duration_min": 30, + "is_smoker": false, + "alcohol_frequency": "never" + } +} +``` + +### 6.6 获取病史列表 - **GET** `/api/user/medical-history` - **需要认证** -### 6.5 删除病史记录 +### 6.7 删除病史记录 - **DELETE** `/api/user/medical-history/:id` - **需要认证** -### 6.6 获取家族病史 +### 6.8 获取家族病史 - **GET** `/api/user/family-history` - **需要认证** -### 6.7 获取过敏记录 +### 6.9 获取过敏记录 - **GET** `/api/user/allergy-records` - **需要认证** diff --git a/server/internal/api/handler/auth.go b/server/internal/api/handler/auth.go index 75a795f..a534b6f 100644 --- a/server/internal/api/handler/auth.go +++ b/server/internal/api/handler/auth.go @@ -18,6 +18,48 @@ func NewAuthHandler() *AuthHandler { } } +// SendCodeRequest 发送验证码请求 +type SendCodeRequest struct { + Phone string `json:"phone" binding:"required"` + Type string `json:"type" binding:"required,oneof=register login reset"` // register, login, reset +} + +// SendCode 发送验证码 +// @Summary 发送手机验证码 +// @Tags 认证 +// @Accept json +// @Produce json +// @Param request body SendCodeRequest true "手机号和验证码类型" +// @Success 200 {object} response.Response +// @Router /api/auth/send-code [post] +func (h *AuthHandler) SendCode(c *gin.Context) { + var req SendCodeRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误: "+err.Error()) + return + } + + // 验证手机号格式 + if len(req.Phone) != 11 { + response.BadRequest(c, "手机号格式不正确") + return + } + + // TODO: 实际项目中应该: + // 1. 生成6位随机验证码 + // 2. 存储到Redis(设置5分钟过期) + // 3. 调用短信服务发送验证码 + // 4. 限制发送频率(如60秒一次) + + // 当前为演示版本,返回模拟成功 + response.SuccessWithMessage(c, "验证码已发送", gin.H{ + "phone": req.Phone[:3] + "****" + req.Phone[7:], + "expires_in": 300, // 5分钟有效 + // 演示环境下返回固定验证码,正式环境请删除 + "demo_code": "123456", + }) +} + // Register 用户注册 // @Summary 用户注册 // @Tags 认证 diff --git a/server/internal/api/handler/health.go b/server/internal/api/handler/health.go index ca48e9b..b4b91c4 100644 --- a/server/internal/api/handler/health.go +++ b/server/internal/api/handler/health.go @@ -199,3 +199,103 @@ func (h *HealthHandler) DeleteAllergyRecord(c *gin.Context) { } response.SuccessWithMessage(c, "删除成功", nil) } + +// UpdateHealthProfileRequest 更新健康档案请求 +type UpdateHealthProfileRequest struct { + Name string `json:"name"` + BirthDate string `json:"birth_date"` + Gender string `json:"gender"` + Height float64 `json:"height"` + Weight float64 `json:"weight"` + BloodType string `json:"blood_type"` + Occupation string `json:"occupation"` + MaritalStatus string `json:"marital_status"` + Region string `json:"region"` +} + +// UpdateHealthProfile 更新健康档案 +// @Summary 更新健康档案 +// @Tags 健康档案 +// @Accept json +// @Produce json +// @Param Authorization header string true "Bearer Token" +// @Param request body UpdateHealthProfileRequest true "健康档案信息" +// @Success 200 {object} response.Response +// @Router /api/user/health-profile [put] +func (h *HealthHandler) UpdateHealthProfile(c *gin.Context) { + userID := middleware.GetUserID(c) + var req UpdateHealthProfileRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误: "+err.Error()) + return + } + + profile, err := h.healthService.UpdateHealthProfile(userID, &service.UpdateHealthProfileInput{ + Name: req.Name, + BirthDate: req.BirthDate, + Gender: req.Gender, + Height: req.Height, + Weight: req.Weight, + BloodType: req.BloodType, + Occupation: req.Occupation, + MaritalStatus: req.MaritalStatus, + Region: req.Region, + }) + if err != nil { + response.Error(c, 500, err.Error()) + return + } + response.Success(c, profile) +} + +// UpdateLifestyleRequest 更新生活习惯请求 +type UpdateLifestyleRequest struct { + SleepTime string `json:"sleep_time"` + WakeTime string `json:"wake_time"` + SleepQuality string `json:"sleep_quality"` + MealRegularity string `json:"meal_regularity"` + DietPreference string `json:"diet_preference"` + DailyWaterML int `json:"daily_water_ml"` + ExerciseFrequency string `json:"exercise_frequency"` + ExerciseType string `json:"exercise_type"` + ExerciseDurationMin int `json:"exercise_duration_min"` + IsSmoker *bool `json:"is_smoker"` + AlcoholFrequency string `json:"alcohol_frequency"` +} + +// UpdateLifestyle 更新生活习惯 +// @Summary 更新生活习惯 +// @Tags 健康档案 +// @Accept json +// @Produce json +// @Param Authorization header string true "Bearer Token" +// @Param request body UpdateLifestyleRequest true "生活习惯信息" +// @Success 200 {object} response.Response +// @Router /api/user/lifestyle [put] +func (h *HealthHandler) UpdateLifestyle(c *gin.Context) { + userID := middleware.GetUserID(c) + var req UpdateLifestyleRequest + if err := c.ShouldBindJSON(&req); err != nil { + response.BadRequest(c, "参数错误: "+err.Error()) + return + } + + lifestyle, err := h.healthService.UpdateLifestyle(userID, &service.UpdateLifestyleInput{ + SleepTime: req.SleepTime, + WakeTime: req.WakeTime, + SleepQuality: req.SleepQuality, + MealRegularity: req.MealRegularity, + DietPreference: req.DietPreference, + DailyWaterML: req.DailyWaterML, + ExerciseFrequency: req.ExerciseFrequency, + ExerciseType: req.ExerciseType, + ExerciseDurationMin: req.ExerciseDurationMin, + IsSmoker: req.IsSmoker, + AlcoholFrequency: req.AlcoholFrequency, + }) + if err != nil { + response.Error(c, 500, err.Error()) + return + } + response.Success(c, lifestyle) +} diff --git a/server/internal/api/router.go b/server/internal/api/router.go index a13f702..6c574d1 100644 --- a/server/internal/api/router.go +++ b/server/internal/api/router.go @@ -37,6 +37,7 @@ func SetupRouter(mode string) *gin.Engine { authGroup.POST("/register", authHandler.Register) authGroup.POST("/login", authHandler.Login) authGroup.POST("/refresh", authHandler.RefreshToken) + authGroup.POST("/send-code", authHandler.SendCode) } // ===================== @@ -69,8 +70,10 @@ func SetupRouter(mode string) *gin.Engine { // 健康档案 healthHandler := handler.NewHealthHandler() authRequired.GET("/user/health-profile", healthHandler.GetHealthProfile) + authRequired.PUT("/user/health-profile", healthHandler.UpdateHealthProfile) authRequired.GET("/user/basic-profile", healthHandler.GetBasicProfile) authRequired.GET("/user/lifestyle", healthHandler.GetLifestyle) + authRequired.PUT("/user/lifestyle", healthHandler.UpdateLifestyle) authRequired.GET("/user/medical-history", healthHandler.GetMedicalHistory) authRequired.DELETE("/user/medical-history/:id", healthHandler.DeleteMedicalHistory) authRequired.GET("/user/family-history", healthHandler.GetFamilyHistory) diff --git a/server/internal/service/ai/aliyun.go b/server/internal/service/ai/aliyun.go index 16bad6d..e1a1544 100644 --- a/server/internal/service/ai/aliyun.go +++ b/server/internal/service/ai/aliyun.go @@ -11,7 +11,8 @@ import ( "strings" ) -const AliyunBaseURL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation" +// 使用阿里云DashScope的OpenAI兼容模式(官方推荐) +const AliyunBaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1" type AliyunClient struct { apiKey string @@ -29,32 +30,40 @@ func NewAliyunClient(cfg *Config) *AliyunClient { } } +// OpenAI兼容格式的请求 type aliyunRequest struct { - Model string `json:"model"` - Input struct { - Messages []Message `json:"messages"` - } `json:"input"` - Parameters struct { - ResultFormat string `json:"result_format"` - MaxTokens int `json:"max_tokens,omitempty"` - } `json:"parameters"` + Model string `json:"model"` + Messages []Message `json:"messages"` + Stream bool `json:"stream,omitempty"` } +// OpenAI兼容格式的响应 type aliyunResponse struct { - Output struct { - Text string `json:"text"` - Choices []struct { - Message struct { - Content string `json:"content"` - } `json:"message"` - } `json:"choices"` - } `json:"output"` + ID string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Choices []struct { + Index int `json:"index"` + Message struct { + Role string `json:"role"` + Content string `json:"content"` + } `json:"message"` + Delta struct { + Content string `json:"content"` + } `json:"delta"` + FinishReason string `json:"finish_reason"` + } `json:"choices"` Usage struct { - InputTokens int `json:"input_tokens"` - OutputTokens int `json:"output_tokens"` + PromptTokens int `json:"prompt_tokens"` + CompletionTokens int `json:"completion_tokens"` + TotalTokens int `json:"total_tokens"` } `json:"usage"` - Code string `json:"code"` - Message string `json:"message"` + Error *struct { + Message string `json:"message"` + Type string `json:"type"` + Code string `json:"code"` + } `json:"error"` } func (c *AliyunClient) Chat(ctx context.Context, messages []Message) (string, error) { @@ -63,13 +72,13 @@ func (c *AliyunClient) Chat(ctx context.Context, messages []Message) (string, er } reqBody := aliyunRequest{ - Model: c.model, + Model: c.model, + Messages: messages, + Stream: false, } - reqBody.Input.Messages = messages - reqBody.Parameters.ResultFormat = "message" body, _ := json.Marshal(reqBody) - req, err := http.NewRequestWithContext(ctx, "POST", AliyunBaseURL, bytes.NewReader(body)) + req, err := http.NewRequestWithContext(ctx, "POST", AliyunBaseURL+"/chat/completions", bytes.NewReader(body)) if err != nil { return "", err } @@ -88,19 +97,16 @@ func (c *AliyunClient) Chat(ctx context.Context, messages []Message) (string, er return "", fmt.Errorf("解析AI响应失败: %v", err) } - if result.Code != "" { - return "", fmt.Errorf("AI服务错误: %s - %s", result.Code, result.Message) + // 检查错误 + if result.Error != nil { + return "", fmt.Errorf("AI服务错误: %s (code: %s)", result.Error.Message, result.Error.Code) } - // 兼容两种返回格式 - if len(result.Output.Choices) > 0 { - return result.Output.Choices[0].Message.Content, nil - } - if result.Output.Text != "" { - return result.Output.Text, nil + if len(result.Choices) == 0 { + return "", fmt.Errorf("AI未返回有效响应") } - return "", fmt.Errorf("AI未返回有效响应") + return result.Choices[0].Message.Content, nil } func (c *AliyunClient) ChatStream(ctx context.Context, messages []Message, writer io.Writer) error { @@ -109,20 +115,19 @@ func (c *AliyunClient) ChatStream(ctx context.Context, messages []Message, write } reqBody := aliyunRequest{ - Model: c.model, + Model: c.model, + Messages: messages, + Stream: true, } - reqBody.Input.Messages = messages - reqBody.Parameters.ResultFormat = "message" body, _ := json.Marshal(reqBody) - req, err := http.NewRequestWithContext(ctx, "POST", AliyunBaseURL, bytes.NewReader(body)) + req, err := http.NewRequestWithContext(ctx, "POST", AliyunBaseURL+"/chat/completions", bytes.NewReader(body)) if err != nil { return err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+c.apiKey) - req.Header.Set("X-DashScope-SSE", "enable") // 启用流式输出 resp, err := http.DefaultClient.Do(req) if err != nil { @@ -154,9 +159,11 @@ func (c *AliyunClient) ChatStream(ctx context.Context, messages []Message, write continue } - if len(streamResp.Output.Choices) > 0 { - content := streamResp.Output.Choices[0].Message.Content - writer.Write([]byte(content)) + if len(streamResp.Choices) > 0 { + content := streamResp.Choices[0].Delta.Content + if content != "" { + writer.Write([]byte(content)) + } } } } diff --git a/server/internal/service/health.go b/server/internal/service/health.go index f4dbfa5..612b5ec 100644 --- a/server/internal/service/health.go +++ b/server/internal/service/health.go @@ -2,6 +2,7 @@ package service import ( "errors" + "time" "health-ai/internal/model" "health-ai/internal/repository/impl" @@ -133,3 +134,156 @@ func (s *HealthService) DeleteAllergyRecord(userID, recordID uint) error { // TODO: 验证记录属于该用户 return s.healthRepo.DeleteAllergyRecord(recordID) } + +// ================= 更新请求结构体 ================= + +// UpdateHealthProfileInput 更新健康档案输入 +type UpdateHealthProfileInput struct { + Name string + BirthDate string + Gender string + Height float64 + Weight float64 + BloodType string + Occupation string + MaritalStatus string + Region string +} + +// UpdateLifestyleInput 更新生活习惯输入 +type UpdateLifestyleInput struct { + SleepTime string + WakeTime string + SleepQuality string + MealRegularity string + DietPreference string + DailyWaterML int + ExerciseFrequency string + ExerciseType string + ExerciseDurationMin int + IsSmoker *bool // 使用指针以区分未设置和false + AlcoholFrequency string +} + +// UpdateHealthProfile 更新健康档案 +func (s *HealthService) UpdateHealthProfile(userID uint, input *UpdateHealthProfileInput) (*model.HealthProfile, error) { + // 获取或创建健康档案 + profile, err := s.healthRepo.GetProfileByUserID(userID) + if err != nil { + // 创建新档案 + profile = &model.HealthProfile{ + UserID: userID, + } + } + + // 更新字段(只更新非空值) + if input.Name != "" { + profile.Name = input.Name + } + if input.BirthDate != "" { + // 解析日期字符串 + parsedDate, err := time.Parse("2006-01-02", input.BirthDate) + if err == nil { + profile.BirthDate = &parsedDate + } + } + if input.Gender != "" { + profile.Gender = input.Gender + } + if input.Height > 0 { + profile.Height = input.Height + } + if input.Weight > 0 { + profile.Weight = input.Weight + // 计算BMI + if profile.Height > 0 { + heightM := profile.Height / 100 + profile.BMI = profile.Weight / (heightM * heightM) + } + } + if input.BloodType != "" { + profile.BloodType = input.BloodType + } + if input.Occupation != "" { + profile.Occupation = input.Occupation + } + if input.MaritalStatus != "" { + profile.MaritalStatus = input.MaritalStatus + } + if input.Region != "" { + profile.Region = input.Region + } + + // 保存或更新 + if profile.ID == 0 { + if err := s.healthRepo.CreateProfile(profile); err != nil { + return nil, err + } + } else { + if err := s.healthRepo.UpdateProfile(profile); err != nil { + return nil, err + } + } + + return profile, nil +} + +// UpdateLifestyle 更新生活习惯 +func (s *HealthService) UpdateLifestyle(userID uint, input *UpdateLifestyleInput) (*model.LifestyleInfo, error) { + // 获取或创建生活习惯记录 + lifestyle, err := s.healthRepo.GetLifestyleByUserID(userID) + if err != nil { + // 创建新记录 + lifestyle = &model.LifestyleInfo{ + UserID: userID, + } + } + + // 更新字段(只更新非空值) + if input.SleepTime != "" { + lifestyle.SleepTime = input.SleepTime + } + if input.WakeTime != "" { + lifestyle.WakeTime = input.WakeTime + } + if input.SleepQuality != "" { + lifestyle.SleepQuality = input.SleepQuality + } + if input.MealRegularity != "" { + lifestyle.MealRegularity = input.MealRegularity + } + if input.DietPreference != "" { + lifestyle.DietPreference = input.DietPreference + } + if input.DailyWaterML > 0 { + lifestyle.DailyWaterML = input.DailyWaterML + } + if input.ExerciseFrequency != "" { + lifestyle.ExerciseFrequency = input.ExerciseFrequency + } + if input.ExerciseType != "" { + lifestyle.ExerciseType = input.ExerciseType + } + if input.ExerciseDurationMin > 0 { + lifestyle.ExerciseDurationMin = input.ExerciseDurationMin + } + if input.IsSmoker != nil { + lifestyle.IsSmoker = *input.IsSmoker + } + if input.AlcoholFrequency != "" { + lifestyle.AlcoholFrequency = input.AlcoholFrequency + } + + // 保存或更新 + if lifestyle.ID == 0 { + if err := s.healthRepo.CreateLifestyle(lifestyle); err != nil { + return nil, err + } + } else { + if err := s.healthRepo.UpdateLifestyle(lifestyle); err != nil { + return nil, err + } + } + + return lifestyle, nil +}