Compare commits

...

2 Commits

Author SHA1 Message Date
dark 12db1bb9fb chore: 更新前端子模块与服务端代码 3 days ago
dark 2839a780d1 feat: 完善前后端 API 集成与错误处理 3 days ago
  1. 449
      TODOS/API-接口清单.md
  2. 192
      ai-agent-prompt.md
  3. 2
      app
  4. 2
      server/config.yaml
  5. BIN
      server/data/health.db
  6. 123
      server/docs/API.md
  7. 42
      server/internal/api/handler/auth.go
  8. 100
      server/internal/api/handler/health.go
  9. 3
      server/internal/api/router.go
  10. 91
      server/internal/service/ai/aliyun.go
  11. 154
      server/internal/service/health.go

449
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*

192
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,用于后端开发参考*

2
app

@ -1 +1 @@
Subproject commit c44696ea7262b895945c31d230f4db0eaae9109a Subproject commit f17111e186475083360ebb9bdadfe75be6163c11

2
server/config.yaml

@ -36,5 +36,5 @@ ai:
# 阿里云通义千问配置 # 阿里云通义千问配置
aliyun: aliyun:
api_key: "" # 请填入您的 DashScope API Key api_key: "sk-53b4777561624ba98246c7b9990c5e8b"
model: "qwen-turbo" # 可选: qwen-turbo, qwen-plus, qwen-max model: "qwen-turbo" # 可选: qwen-turbo, qwen-plus, qwen-max

BIN
server/data/health.db

Binary file not shown.

123
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` - **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` - **GET** `/api/user/basic-profile`
- **需要认证** - **需要认证**
### 6.3 获取生活习惯 ### 6.4 获取生活习惯
- **GET** `/api/user/lifestyle` - **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` - **GET** `/api/user/medical-history`
- **需要认证** - **需要认证**
### 6.5 删除病史记录 ### 6.7 删除病史记录
- **DELETE** `/api/user/medical-history/:id` - **DELETE** `/api/user/medical-history/:id`
- **需要认证** - **需要认证**
### 6.6 获取家族病史 ### 6.8 获取家族病史
- **GET** `/api/user/family-history` - **GET** `/api/user/family-history`
- **需要认证** - **需要认证**
### 6.7 获取过敏记录 ### 6.9 获取过敏记录
- **GET** `/api/user/allergy-records` - **GET** `/api/user/allergy-records`
- **需要认证** - **需要认证**

42
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 用户注册 // Register 用户注册
// @Summary 用户注册 // @Summary 用户注册
// @Tags 认证 // @Tags 认证

100
server/internal/api/handler/health.go

@ -199,3 +199,103 @@ func (h *HealthHandler) DeleteAllergyRecord(c *gin.Context) {
} }
response.SuccessWithMessage(c, "删除成功", nil) 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)
}

3
server/internal/api/router.go

@ -37,6 +37,7 @@ func SetupRouter(mode string) *gin.Engine {
authGroup.POST("/register", authHandler.Register) authGroup.POST("/register", authHandler.Register)
authGroup.POST("/login", authHandler.Login) authGroup.POST("/login", authHandler.Login)
authGroup.POST("/refresh", authHandler.RefreshToken) authGroup.POST("/refresh", authHandler.RefreshToken)
authGroup.POST("/send-code", authHandler.SendCode)
} }
// ===================== // =====================
@ -69,8 +70,10 @@ func SetupRouter(mode string) *gin.Engine {
// 健康档案 // 健康档案
healthHandler := handler.NewHealthHandler() healthHandler := handler.NewHealthHandler()
authRequired.GET("/user/health-profile", healthHandler.GetHealthProfile) authRequired.GET("/user/health-profile", healthHandler.GetHealthProfile)
authRequired.PUT("/user/health-profile", healthHandler.UpdateHealthProfile)
authRequired.GET("/user/basic-profile", healthHandler.GetBasicProfile) authRequired.GET("/user/basic-profile", healthHandler.GetBasicProfile)
authRequired.GET("/user/lifestyle", healthHandler.GetLifestyle) authRequired.GET("/user/lifestyle", healthHandler.GetLifestyle)
authRequired.PUT("/user/lifestyle", healthHandler.UpdateLifestyle)
authRequired.GET("/user/medical-history", healthHandler.GetMedicalHistory) authRequired.GET("/user/medical-history", healthHandler.GetMedicalHistory)
authRequired.DELETE("/user/medical-history/:id", healthHandler.DeleteMedicalHistory) authRequired.DELETE("/user/medical-history/:id", healthHandler.DeleteMedicalHistory)
authRequired.GET("/user/family-history", healthHandler.GetFamilyHistory) authRequired.GET("/user/family-history", healthHandler.GetFamilyHistory)

91
server/internal/service/ai/aliyun.go

@ -11,7 +11,8 @@ import (
"strings" "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 { type AliyunClient struct {
apiKey string apiKey string
@ -29,32 +30,40 @@ func NewAliyunClient(cfg *Config) *AliyunClient {
} }
} }
// OpenAI兼容格式的请求
type aliyunRequest struct { type aliyunRequest struct {
Model string `json:"model"` Model string `json:"model"`
Input struct { Messages []Message `json:"messages"`
Messages []Message `json:"messages"` Stream bool `json:"stream,omitempty"`
} `json:"input"`
Parameters struct {
ResultFormat string `json:"result_format"`
MaxTokens int `json:"max_tokens,omitempty"`
} `json:"parameters"`
} }
// OpenAI兼容格式的响应
type aliyunResponse struct { type aliyunResponse struct {
Output struct { ID string `json:"id"`
Text string `json:"text"` Object string `json:"object"`
Choices []struct { Created int64 `json:"created"`
Message struct { Model string `json:"model"`
Content string `json:"content"` Choices []struct {
} `json:"message"` Index int `json:"index"`
} `json:"choices"` Message struct {
} `json:"output"` 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 { Usage struct {
InputTokens int `json:"input_tokens"` PromptTokens int `json:"prompt_tokens"`
OutputTokens int `json:"output_tokens"` CompletionTokens int `json:"completion_tokens"`
TotalTokens int `json:"total_tokens"`
} `json:"usage"` } `json:"usage"`
Code string `json:"code"` Error *struct {
Message string `json:"message"` 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) { 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{ reqBody := aliyunRequest{
Model: c.model, Model: c.model,
Messages: messages,
Stream: false,
} }
reqBody.Input.Messages = messages
reqBody.Parameters.ResultFormat = "message"
body, _ := json.Marshal(reqBody) 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 { if err != nil {
return "", err return "", err
} }
@ -88,19 +97,16 @@ func (c *AliyunClient) Chat(ctx context.Context, messages []Message) (string, er
return "", fmt.Errorf("解析AI响应失败: %v", err) 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.Choices) == 0 {
if len(result.Output.Choices) > 0 { return "", fmt.Errorf("AI未返回有效响应")
return result.Output.Choices[0].Message.Content, nil
}
if result.Output.Text != "" {
return result.Output.Text, nil
} }
return "", fmt.Errorf("AI未返回有效响应") return result.Choices[0].Message.Content, nil
} }
func (c *AliyunClient) ChatStream(ctx context.Context, messages []Message, writer io.Writer) error { 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{ reqBody := aliyunRequest{
Model: c.model, Model: c.model,
Messages: messages,
Stream: true,
} }
reqBody.Input.Messages = messages
reqBody.Parameters.ResultFormat = "message"
body, _ := json.Marshal(reqBody) 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 { if err != nil {
return err return err
} }
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+c.apiKey) req.Header.Set("Authorization", "Bearer "+c.apiKey)
req.Header.Set("X-DashScope-SSE", "enable") // 启用流式输出
resp, err := http.DefaultClient.Do(req) resp, err := http.DefaultClient.Do(req)
if err != nil { if err != nil {
@ -154,9 +159,11 @@ func (c *AliyunClient) ChatStream(ctx context.Context, messages []Message, write
continue continue
} }
if len(streamResp.Output.Choices) > 0 { if len(streamResp.Choices) > 0 {
content := streamResp.Output.Choices[0].Message.Content content := streamResp.Choices[0].Delta.Content
writer.Write([]byte(content)) if content != "" {
writer.Write([]byte(content))
}
} }
} }
} }

154
server/internal/service/health.go

@ -2,6 +2,7 @@ package service
import ( import (
"errors" "errors"
"time"
"health-ai/internal/model" "health-ai/internal/model"
"health-ai/internal/repository/impl" "health-ai/internal/repository/impl"
@ -133,3 +134,156 @@ func (s *HealthService) DeleteAllergyRecord(userID, recordID uint) error {
// TODO: 验证记录属于该用户 // TODO: 验证记录属于该用户
return s.healthRepo.DeleteAllergyRecord(recordID) 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
}

Loading…
Cancel
Save