From 2839a780d12a4b95a83b3f0fe4abae13158fb8ad Mon Sep 17 00:00:00 2001 From: dark Date: Sun, 1 Feb 2026 19:13:57 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=8C=E5=96=84=E5=89=8D=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=20API=20=E9=9B=86=E6=88=90=E4=B8=8E=E9=94=99=E8=AF=AF?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要更新: - 修复 Token 存储和认证流程(localStorage/AsyncStorage) - 修复后端响应数据结构适配(token、user字段) - 优化对话列表和聊天页面的状态管理 - 支持流式和非流式消息响应 - 添加空值检查防止 undefined 错误 - 新增 API 接口清单文档 - 提取 AI Agent 系统提示词文档 Co-authored-by: Cursor --- TODOS/API-接口清单.md | 449 ++++++++++++++++++++++++++ ai-agent-prompt.md | 192 +++++++++++ server/config.yaml | 2 +- server/data/health.db | Bin 217088 -> 311296 bytes server/docs/API.md | 123 ++++++- server/internal/api/handler/auth.go | 42 +++ server/internal/api/handler/health.go | 100 ++++++ server/internal/api/router.go | 3 + server/internal/service/ai/aliyun.go | 91 +++--- server/internal/service/health.go | 154 +++++++++ 10 files changed, 1107 insertions(+), 49 deletions(-) create mode 100644 TODOS/API-接口清单.md create mode 100644 ai-agent-prompt.md 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 61f37f2b26a0b53ea2b8cf7b8348fd29f3ea819f..d6b6b17dfdc22ac529d910b5729386fe6eb1b46b 100644 GIT binary patch literal 311296 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

VR#u1#Y=GrUV?-1VjP4Q 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 delta 61 zcmV-D0K)%(fD?eA4UifJ05Sjo05y>yJ^}zTv0$Vh2m%k{01x&L; **注意**: 当前为演示版本,验证码固定为 `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 +}