Compare commits

...

6 Commits

Author SHA1 Message Date
dark 4f61f0de68 docs: 更新 agents 与 mall 设计文档 1 month ago
dark 0ec6f8aee7 refactor(web): 移除商城路由改为外链,api 抽取至 src/api 1 month ago
dark 4c784134be chore: 前端统一启动脚本 dev.js,支持 web/mall 分启与端口清理 1 month ago
dark a6e646a9e3 feat(mall): 商城前端独立项目 Vue3+TS+Element Plus,公开浏览+按需登录 1 month ago
dark 493ace4b3e refactor(backend): 后端由 server 迁移至 backend (go-zero) 1 month ago
dark 9cb2125ef0 chore: 忽略 .agents、.cursor、test-results、tests/tests 1 month ago
  1. 7
      .gitignore
  2. 249
      agents.md
  3. 460
      backend/BACKEND.md
  4. 29
      backend/healthapi/etc/healthapi-api.yaml
  5. 68
      backend/healthapi/go.mod
  6. 166
      backend/healthapi/go.sum
  7. 931
      backend/healthapi/healthapi.api
  8. 50
      backend/healthapi/healthapi.go
  9. 40
      backend/healthapi/internal/config/config.go
  10. 115
      backend/healthapi/internal/database/database.go
  11. 28
      backend/healthapi/internal/handler/add_cart_handler.go
  12. 28
      backend/healthapi/internal/handler/batch_select_cart_handler.go
  13. 29
      backend/healthapi/internal/handler/batch_submit_allergy_handler.go
  14. 29
      backend/healthapi/internal/handler/batch_submit_family_history_handler.go
  15. 29
      backend/healthapi/internal/handler/batch_submit_medical_history_handler.go
  16. 28
      backend/healthapi/internal/handler/cancel_order_handler.go
  17. 21
      backend/healthapi/internal/handler/clear_cart_handler.go
  18. 21
      backend/healthapi/internal/handler/complete_survey_handler.go
  19. 28
      backend/healthapi/internal/handler/confirm_receive_handler.go
  20. 28
      backend/healthapi/internal/handler/create_address_handler.go
  21. 29
      backend/healthapi/internal/handler/create_conversation_handler.go
  22. 28
      backend/healthapi/internal/handler/create_order_handler.go
  23. 28
      backend/healthapi/internal/handler/delete_address_handler.go
  24. 29
      backend/healthapi/internal/handler/delete_allergy_record_handler.go
  25. 28
      backend/healthapi/internal/handler/delete_cart_handler.go
  26. 29
      backend/healthapi/internal/handler/delete_conversation_handler.go
  27. 29
      backend/healthapi/internal/handler/delete_family_history_handler.go
  28. 29
      backend/healthapi/internal/handler/delete_medical_history_handler.go
  29. 28
      backend/healthapi/internal/handler/get_address_handler.go
  30. 21
      backend/healthapi/internal/handler/get_addresses_handler.go
  31. 21
      backend/healthapi/internal/handler/get_allergy_records_handler.go
  32. 21
      backend/healthapi/internal/handler/get_assessment_history_handler.go
  33. 21
      backend/healthapi/internal/handler/get_assessment_result_handler.go
  34. 21
      backend/healthapi/internal/handler/get_basic_profile_handler.go
  35. 21
      backend/healthapi/internal/handler/get_cart_handler.go
  36. 21
      backend/healthapi/internal/handler/get_categories_handler.go
  37. 21
      backend/healthapi/internal/handler/get_constitution_products_handler.go
  38. 29
      backend/healthapi/internal/handler/get_conversation_handler.go
  39. 21
      backend/healthapi/internal/handler/get_conversations_handler.go
  40. 21
      backend/healthapi/internal/handler/get_family_history_handler.go
  41. 28
      backend/healthapi/internal/handler/get_featured_products_handler.go
  42. 21
      backend/healthapi/internal/handler/get_grouped_questions_handler.go
  43. 21
      backend/healthapi/internal/handler/get_health_profile_handler.go
  44. 21
      backend/healthapi/internal/handler/get_lifestyle_handler.go
  45. 28
      backend/healthapi/internal/handler/get_mall_product_detail_handler.go
  46. 28
      backend/healthapi/internal/handler/get_mall_products_handler.go
  47. 21
      backend/healthapi/internal/handler/get_medical_history_handler.go
  48. 21
      backend/healthapi/internal/handler/get_member_info_handler.go
  49. 28
      backend/healthapi/internal/handler/get_order_handler.go
  50. 28
      backend/healthapi/internal/handler/get_orders_handler.go
  51. 28
      backend/healthapi/internal/handler/get_points_records_handler.go
  52. 29
      backend/healthapi/internal/handler/get_product_handler.go
  53. 29
      backend/healthapi/internal/handler/get_product_list_handler.go
  54. 29
      backend/healthapi/internal/handler/get_products_by_category_handler.go
  55. 29
      backend/healthapi/internal/handler/get_purchase_history_handler.go
  56. 21
      backend/healthapi/internal/handler/get_questions_handler.go
  57. 21
      backend/healthapi/internal/handler/get_recommend_products_handler.go
  58. 21
      backend/healthapi/internal/handler/get_recommendations_handler.go
  59. 21
      backend/healthapi/internal/handler/get_survey_status_handler.go
  60. 21
      backend/healthapi/internal/handler/get_user_profile_handler.go
  61. 21
      backend/healthapi/internal/handler/health_check_handler.go
  62. 30
      backend/healthapi/internal/handler/login_handler.go
  63. 28
      backend/healthapi/internal/handler/pay_order_handler.go
  64. 28
      backend/healthapi/internal/handler/preview_order_handler.go
  65. 29
      backend/healthapi/internal/handler/refresh_token_handler.go
  66. 30
      backend/healthapi/internal/handler/register_handler.go
  67. 410
      backend/healthapi/internal/handler/routes.go
  68. 28
      backend/healthapi/internal/handler/search_mall_products_handler.go
  69. 29
      backend/healthapi/internal/handler/search_products_handler.go
  70. 30
      backend/healthapi/internal/handler/send_code_handler.go
  71. 29
      backend/healthapi/internal/handler/send_message_handler.go
  72. 311
      backend/healthapi/internal/handler/send_message_stream_handler.go
  73. 28
      backend/healthapi/internal/handler/set_default_address_handler.go
  74. 29
      backend/healthapi/internal/handler/submit_allergy_handler.go
  75. 29
      backend/healthapi/internal/handler/submit_assessment_handler.go
  76. 29
      backend/healthapi/internal/handler/submit_basic_info_handler.go
  77. 29
      backend/healthapi/internal/handler/submit_family_history_handler.go
  78. 29
      backend/healthapi/internal/handler/submit_lifestyle_handler.go
  79. 29
      backend/healthapi/internal/handler/submit_medical_history_handler.go
  80. 29
      backend/healthapi/internal/handler/sync_purchase_handler.go
  81. 28
      backend/healthapi/internal/handler/update_address_handler.go
  82. 28
      backend/healthapi/internal/handler/update_cart_handler.go
  83. 29
      backend/healthapi/internal/handler/update_health_profile_handler.go
  84. 29
      backend/healthapi/internal/handler/update_lifestyle_handler.go
  85. 29
      backend/healthapi/internal/handler/update_user_profile_handler.go
  86. 135
      backend/healthapi/internal/logic/add_cart_logic.go
  87. 48
      backend/healthapi/internal/logic/batch_select_cart_logic.go
  88. 60
      backend/healthapi/internal/logic/batch_submit_allergy_logic.go
  89. 59
      backend/healthapi/internal/logic/batch_submit_family_history_logic.go
  90. 61
      backend/healthapi/internal/logic/batch_submit_medical_history_logic.go
  91. 94
      backend/healthapi/internal/logic/cancel_order_logic.go
  92. 40
      backend/healthapi/internal/logic/clear_cart_logic.go
  93. 52
      backend/healthapi/internal/logic/complete_survey_logic.go
  94. 56
      backend/healthapi/internal/logic/confirm_receive_logic.go
  95. 76
      backend/healthapi/internal/logic/create_address_logic.go
  96. 54
      backend/healthapi/internal/logic/create_conversation_logic.go
  97. 256
      backend/healthapi/internal/logic/create_order_logic.go
  98. 46
      backend/healthapi/internal/logic/delete_address_logic.go
  99. 49
      backend/healthapi/internal/logic/delete_allergy_record_logic.go
  100. 46
      backend/healthapi/internal/logic/delete_cart_logic.go

7
.gitignore

@ -103,6 +103,13 @@ local.properties
*.db *.db
server/data/ server/data/
# 本地/IDE 与测试产物
.agents/
.ai-context/
.cursor/
test-results/
tests/tests/
# 密钥配置 # 密钥配置
alikey.md alikey.md
*key*.md *key*.md

249
agents.md

@ -3,6 +3,30 @@
- 涉及到任何代码修改,记得更新此文档和设计文档 - 涉及到任何代码修改,记得更新此文档和设计文档
- 需求记录文档: [`docs/REQUIREMENTS.md`](docs/REQUIREMENTS.md) - 需求记录文档: [`docs/REQUIREMENTS.md`](docs/REQUIREMENTS.md)
## 前端开发启动
使用统一启动脚本,自动检查端口占用并清理后启动:
```bash
# 同时启动 健康APP(5173) + 商城(5174)
npm run dev # 或 node scripts/dev.js
# 仅启动健康APP
npm run dev:web # 或 node scripts/dev.js web
# 仅启动商城
npm run dev:mall # 或 node scripts/dev.js mall
# 仅关闭占用端口的进程
npm run dev:kill # 或 node scripts/dev.js --kill
```
| 项目 | 目录 | 端口 | 说明 |
|------|------|------|------|
| 健康APP (web) | `web/` | 5173 | 健康AI助手前端 |
| 健康商城 (mall) | `mall/` | 5174 | 独立商城前端 |
| 后端 API | `server/` | 8080 | Go-Zero 后端 |
## 前端自动化测试规范 ## 前端自动化测试规范
**重要**: 前端进行功能更新和修改后,必须使用 Playwright 进行自动化测试验证。 **重要**: 前端进行功能更新和修改后,必须使用 Playwright 进行自动化测试验证。
@ -17,6 +41,9 @@ tests/
├── constitution.test.js # 体质分析功能测试 ├── constitution.test.js # 体质分析功能测试
├── profile.test.js # "我的"页面功能测试 ├── profile.test.js # "我的"页面功能测试
├── health-profile-complete.test.js # 健康档案完整功能测试(推荐) ├── health-profile-complete.test.js # 健康档案完整功能测试(推荐)
├── chat.test.js # 问答页对话管理与流式输出测试
├── mall.test.js # 商城前端测试(53项,含公开浏览+登录守卫,API Mock)
├── mall-real.test.js # 商城前端真实数据测试(52项,需后端)
└── screenshots/ # 测试截图目录 └── screenshots/ # 测试截图目录
``` ```
@ -408,3 +435,225 @@ const result = {
- 过敏记录: 4 个字段(过敏类型、过敏原、严重程度、过敏反应描述) - 过敏记录: 4 个字段(过敏类型、过敏原、严重程度、过敏反应描述)
- 测试结果: **58/58 通过** - 测试结果: **58/58 通过**
- 测试截图: `tests/screenshots/hp-*.png` - 测试截图: `tests/screenshots/hp-*.png`
---
### 2026-02-02: AI 问答功能增强
**修改内容**:
1. **等待动画**: 添加 LoadingDots 组件,在等待 AI 回复和思考过程中显示动态加载动画
2. **思考过程显示**: 支持显示 AI 思考过程(需要模型支持 `enable_thinking`
3. **放开思考过程过滤**: 移除之前添加的关键词过滤逻辑,完整显示 AI 思考内容
**修改文件**:
- `app/src/screens/chat/ChatDetailScreen.tsx`:
- 添加 `LoadingDots` 组件,实现三个点的脉冲动画
- 在 AI 回复等待时显示 "AI 正在回复..."
- 在思考过程等待时显示 "💭 思考中..."
- `server/internal/service/ai/aliyun.go`:
- 移除 `thinkingFilterKeywords` 关键词列表
- 移除 `shouldFilterThinking` 过滤函数
- 简化思考过程流式输出,直接转发不过滤
- `server/config.yaml`:
- 注释掉 `app_id` 配置(应用 API 格式与 OpenAI 兼容模式不同,暂不支持)
**LoadingDots 组件示例**:
```tsx
const LoadingDots = ({ text = "思考中", fontSize = 15 }) => {
const [dotCount, setDotCount] = useState(1);
useEffect(() => {
const interval = setInterval(() => {
setDotCount((prev) => (prev >= 3 ? 1 : prev + 1));
}, 400);
return () => clearInterval(interval);
}, []);
return (
<Text style={{ fontSize, color: colors.textSecondary }}>
{text}
{".".repeat(dotCount)}
</Text>
);
};
```
**备注**:
- 阿里云百炼应用 API 需要使用专用的 `/api/v1/apps/{APP_ID}/completion` 端点,与当前 OpenAI 兼容模式实现不同
- 思考过程功能需要使用支持 `enable_thinking` 参数的模型(如 qwen-plus)
---
### 2026-02-03: 商城后端 API 开发
**内容**: 在现有健康AI后端基础上扩展商城功能,共享用户体系和JWT认证
**新增模型**:
- `ProductCategory` - 商品分类
- `ProductSku` - 商品SKU
- `PointsRecord` - 积分变动记录
- `CartItem` - 购物车
- `Address` - 收货地址
- `Order` / `OrderItem` - 订单
**新增API路由** (`/api/mall`):
- 公开路由: 分类列表、商品列表/详情/搜索/推荐
- 认证路由: 会员信息、积分、购物车CRUD、地址CRUD、订单全流程、体质推荐
**API定义**: `backend/healthapi/healthapi.api` (商城部分在文件末尾)
**测试**: `backend/healthapi/tests/mall_api_test.go` - 30个Go集成测试全部通过
---
### 2026-02-07: 商城 Web H5 前端开发 → 独立项目拆分
**内容**: 基于 Vue 3 + TypeScript + Element Plus 开发商城 Web H5 前端,后拆分为独立项目
**项目结构调整**:
商城从 `web/` 项目中拆分为独立的 `mall/` 项目:
- `web/` (端口 5173) - 健康AI助手前端
- `mall/` (端口 5174) - 健康商城前端
- 两个项目共享同一后端(端口 8080)和 JWT Token(通过 localStorage 共享)
- 双向跳转通过 `VITE_MALL_URL` / `VITE_HEALTH_AI_URL` 环境变量配置
**mall/ 项目文件结构**:
```
mall/
├── package.json # 独立依赖管理
├── vite.config.ts # Vite 配置(端口 5174)
├── tsconfig.json # TypeScript 配置
├── .env # 环境变量(API地址、健康AI地址)
├── index.html
└── src/
├── main.ts # 入口文件
├── App.vue
├── router/index.ts # 路由(根路径 / 开头)
├── api/
│ ├── request.ts # Axios 封装(共享Token拦截器)
│ ├── auth.ts # 认证 API
│ └── mall.ts # 商城 API(商品/购物车/订单/地址/会员)
├── stores/
│ ├── auth.ts # 认证状态(从 localStorage 恢复)
│ ├── constitution.ts # 体质数据(从 localStorage 读取)
│ ├── mall.ts # 购物车和分类
│ ├── order.ts # 订单
│ └── member.ts # 会员信息
├── types/index.ts # 商城类型定义 + 体质类型
├── utils/healthAI.ts # 健康AI跳转工具
├── styles/index.scss # 全局样式
└── views/
├── layout/MallLayout.vue # H5布局
└── mall/ # 10个页面
├── MallHomeView.vue # 首页
├── CategoryView.vue # 分类
├── ProductDetailView.vue # 商品详情
├── SearchView.vue # 搜索
├── CartView.vue # 购物车
├── CheckoutView.vue # 结算
├── OrderListView.vue # 订单列表
├── OrderDetailView.vue # 订单详情
├── AddressListView.vue # 地址管理
└── MemberView.vue # 会员中心
```
**web/ 项目变更**:
- 移除所有商城代码(views/mall/、stores/mall|order|member、api/mall、utils/healthAI等)
- `web/src/router/index.ts` - 移除 `/mall` 路由组
- `web/src/views/home/HomeView.vue` - "健康商城"改为外部链接跳转
- `web/src/views/profile/ProfileView.vue` - "健康商城"改为外部链接跳转
- `web/src/types/index.ts` - 移除商城类型定义
- `web/.env` - 新增 `VITE_MALL_URL` 环境变量
**认证策略(按需登录)**:
商城采用"公开浏览 + 按需登录"策略,用户可自由浏览商品,仅在涉及个人数据的操作时要求登录:
| 页面/操作 | 是否需要登录 | 说明 |
|-----------|:----------:|------|
| 首页、分类、商品详情、搜索 | 否 | 自由浏览 |
| 加入购物车、立即购买 | 是 | 弹窗引导登录 |
| 购物车、结算、订单 | 是 | 路由守卫重定向 |
| 地址管理、会员中心 | 是 | 路由守卫重定向 |
- 路由守卫:仅 `meta.requiresAuth` 的路由会重定向到 `/login`
- 操作拦截:`useAuthCheck().requireAuth()` 弹窗引导登录
- 401 处理:API 返回 401 时清除 token 并跳转到本地登录页
**跨项目跳转机制**:
```
健康AI (web/) ←→ 商城 (mall/)
├── 共享: localStorage 中的 token 和 user 信息
├── web → mall: window.open(VITE_MALL_URL)
├── mall → web: jumpToHealthAI() / goToHealthAI()
└── mall 独立登录: /login 页面(不再依赖 web 登录页)
```
**启动方式**:
```bash
# 健康AI前端
cd web && npm run dev # http://localhost:5173
# 商城前端
cd mall && npm run dev # http://localhost:5174
# 后端
cd backend && go run . # http://localhost:8080
```
**设计规范**:
- 主色调: #52C41A (绿色/健康主题)
- 价格色: #FF4D4F
- H5 布局 max-width: 750px
- 面向中老年用户:大字体、大按钮、简洁流程
- 与健康AI双向跳转集成(AI咨询悬浮按钮、体质推荐等)
---
### 2026-02-07: 商城认证策略优化 — 公开浏览 + 按需登录
**需求**: 商城应允许用户自由浏览商品,仅在生成订单或支付时才需要登录
**修改内容**:
1. **路由守卫改造** (`mall/src/router/index.ts`):
- 取消全局登录拦截,改为按路由标记 `meta.requiresAuth`
- 公开页面(首页/分类/商品详情/搜索)无需登录
- 受保护页面(购物车/订单/结算/地址/会员)需要登录
2. **操作级登录引导** (`mall/src/utils/auth.ts`):
- 新增 `useAuthCheck()` composable
- `requireAuth(message)` 方法:未登录时弹 `ElMessageBox.confirm` 引导登录
- 用于商品详情页的"加入购物车"和"立即购买"按钮
3. **商品详情页** (`mall/src/views/mall/ProductDetailView.vue`):
- `handleAddToCart()``handleBuyNow()` 执行前调用 `requireAuth()` 检查
4. **测试更新** (`tests/mall.test.js`):
- 新增 Phase 7A: 未登录公开浏览测试(首页/分类/商品/搜索可正常访问)
- 更新 Phase 7B: 受保护页面重定向测试(5个路由均重定向到 /login)
- 53项测试全部通过
**新增文件**:
- `mall/src/utils/auth.ts` - 认证检查 composable
**修改文件**:
- `mall/src/router/index.ts` - 路由守卫改造
- `mall/src/views/mall/ProductDetailView.vue` - 按钮登录检查
- `tests/mall.test.js` - 测试更新

460
backend/BACKEND.md

@ -0,0 +1,460 @@
# 健康 AI 助手 - 后端服务文档
> 基于 go-zero 微服务框架重构
---
## 一、服务概述
### 1.1 原架构(Gin)
```
server/
├── cmd/server/main.go # 入口
├── internal/
│ ├── api/handler/ # HTTP 处理器
│ ├── api/middleware/ # 中间件
│ ├── api/router.go # 路由定义
│ ├── config/ # 配置
│ ├── database/ # 数据库初始化
│ ├── model/ # GORM 模型
│ ├── repository/ # 数据访问层
│ └── service/ # 业务逻辑层
└── pkg/ # 公共包
```
### 1.2 目标架构(go-zero)
```
backend/
├── health-api/ # REST API 服务
│ ├── etc/config.yaml # 配置
│ ├── health.api # API 定义
│ ├── internal/
│ │ ├── config/ # 配置结构
│ │ ├── handler/ # Handler 层
│ │ ├── logic/ # Logic 业务层
│ │ ├── svc/ # ServiceContext
│ │ └── types/ # 请求/响应类型
│ └── health.go # 入口
├── health-rpc/ # RPC 服务(可选)
└── model/ # 数据模型
```
---
## 二、功能模块
### 2.1 认证模块 (Auth)
| API | 方法 | 路径 | 描述 |
| ---------- | ---- | ------------------- | ------------------- |
| 注册 | POST | /api/auth/register | 手机号/邮箱注册 |
| 登录 | POST | /api/auth/login | 登录获取 Token |
| 刷新 Token | POST | /api/auth/refresh | 刷新 JWT Token |
| 发送验证码 | POST | /api/auth/send-code | 发送短信/邮箱验证码 |
**请求/响应示例:**
```go
// 注册请求
type RegisterReq struct {
Phone string `json:"phone,optional"`
Email string `json:"email,optional"`
Password string `json:"password"`
Code string `json:"code,optional"`
}
// 登录响应
type LoginResp struct {
Token string `json:"token"`
ExpiresAt int64 `json:"expires_at"`
User UserInfo `json:"user"`
}
```
### 2.2 用户模块 (User)
| API | 方法 | 路径 | 描述 | 认证 |
| -------- | ---- | ----------------- | ---------------- | ---- |
| 获取信息 | GET | /api/user/profile | 获取用户基本信息 | ✅ |
| 更新资料 | PUT | /api/user/profile | 更新昵称/头像 | ✅ |
### 2.3 健康档案模块 (Health)
| API | 方法 | 路径 | 描述 | 认证 |
| ------------ | ------ | ----------------------------- | ------------------ | ---- |
| 获取完整档案 | GET | /api/user/health-profile | 获取所有健康信息 | ✅ |
| 更新档案 | PUT | /api/user/health-profile | 更新健康档案 | ✅ |
| 获取基础档案 | GET | /api/user/basic-profile | 获取基础信息 | ✅ |
| 获取生活习惯 | GET | /api/user/lifestyle | 获取生活习惯 | ✅ |
| 更新生活习惯 | PUT | /api/user/lifestyle | 更新生活习惯 | ✅ |
| 获取病史 | GET | /api/user/medical-history | 获取病史列表 | ✅ |
| 删除病史 | DELETE | /api/user/medical-history/:id | 删除单条病史 | ✅ |
| 获取家族史 | GET | /api/user/family-history | 获取家族病史 | ✅ |
| 删除家族史 | DELETE | /api/user/family-history/:id | 删除单条家族史 | ✅ |
| 获取过敏记录 | GET | /api/user/allergy-records | 获取过敏记录 | ✅ |
| 删除过敏记录 | DELETE | /api/user/allergy-records/:id | 删除单条过敏记录 | ✅ |
| 获取购买历史 | GET | /api/user/purchase-history | 获取保健品购买记录 | ✅ |
### 2.4 健康调查模块 (Survey)
| API | 方法 | 路径 | 描述 | 认证 |
| -------------- | ---- | --------------------------------- | ---------------- | ---- |
| 获取状态 | GET | /api/survey/status | 获取调查完成状态 | ✅ |
| 提交基础信息 | POST | /api/survey/basic-info | 提交基础健康信息 | ✅ |
| 提交生活习惯 | POST | /api/survey/lifestyle | 提交生活习惯 | ✅ |
| 提交病史 | POST | /api/survey/medical-history | 提交单条病史 | ✅ |
| 批量提交病史 | POST | /api/survey/medical-history/batch | 批量提交病史 | ✅ |
| 提交家族史 | POST | /api/survey/family-history | 提交单条家族史 | ✅ |
| 批量提交家族史 | POST | /api/survey/family-history/batch | 批量提交家族史 | ✅ |
| 提交过敏信息 | POST | /api/survey/allergy | 提交单条过敏信息 | ✅ |
| 批量提交过敏 | POST | /api/survey/allergy/batch | 批量提交过敏信息 | ✅ |
| 完成调查 | POST | /api/survey/complete | 标记调查完成 | ✅ |
### 2.5 体质辨识模块 (Constitution)
| API | 方法 | 路径 | 描述 | 认证 |
| ------------ | ---- | ----------------------------------- | ---------------- | ---- |
| 获取问卷 | GET | /api/constitution/questions | 获取所有题目 | ✅ |
| 获取分组问卷 | GET | /api/constitution/questions/grouped | 按体质分组获取 | ✅ |
| 提交测评 | POST | /api/constitution/submit | 提交问卷答案 | ✅ |
| 获取结果 | GET | /api/constitution/result | 获取最新测评结果 | ✅ |
| 获取历史 | GET | /api/constitution/history | 获取测评历史 | ✅ |
| 获取建议 | GET | /api/constitution/recommendations | 获取调养建议 | ✅ |
**体质类型:**
- pinghe (平和质)
- qixu (气虚质)
- yangxu (阳虚质)
- yinxu (阴虚质)
- tanshi (痰湿质)
- shire (湿热质)
- xueyu (血瘀质)
- qiyu (气郁质)
- tebing (特禀质)
### 2.6 AI 对话模块 (Conversation)
| API | 方法 | 路径 | 描述 | 认证 |
| -------------- | ------ | -------------------------------------- | -------------------- | ---- |
| 获取列表 | GET | /api/conversations | 获取对话列表 | ✅ |
| 创建对话 | POST | /api/conversations | 创建新对话 | ✅ |
| 获取详情 | GET | /api/conversations/:id | 获取对话消息 | ✅ |
| 删除对话 | DELETE | /api/conversations/:id | 删除对话 | ✅ |
| 发送消息 | POST | /api/conversations/:id/messages | 发送消息(非流式) | ✅ |
| 发送消息(流式) | POST | /api/conversations/:id/messages/stream | 发送消息(SSE 流式) | ✅ |
**AI 服务配置:**
- 阿里云通义千问(主要)
- OpenAI 兼容接口(备选)
### 2.7 产品模块 (Product)
| API | 方法 | 路径 | 描述 | 认证 |
| ---------- | ---- | ----------------------- | ---------------- | ------- |
| 获取列表 | GET | /api/products | 分页获取产品 | ❌ |
| 获取详情 | GET | /api/products/:id | 获取产品详情 | ❌ |
| 按分类获取 | GET | /api/products/category | 按分类筛选 | ❌ |
| 搜索产品 | GET | /api/products/search | 关键词搜索 | ❌ |
| 获取推荐 | GET | /api/products/recommend | 基于体质推荐 | ✅ |
| 同步购买 | POST | /api/sync/purchase | 商城同步购买记录 | API Key |
---
## 三、数据模型
### 3.1 用户相关
```sql
-- 用户表
CREATE TABLE users (
id BIGINT PRIMARY KEY,
phone VARCHAR(20) UNIQUE,
email VARCHAR(100) UNIQUE,
password_hash VARCHAR(255) NOT NULL,
nickname VARCHAR(50),
avatar VARCHAR(255),
survey_completed BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP,
updated_at TIMESTAMP,
deleted_at TIMESTAMP
);
-- 健康档案表
CREATE TABLE health_profiles (
id BIGINT PRIMARY KEY,
user_id BIGINT UNIQUE NOT NULL,
name VARCHAR(50),
birth_date DATE,
gender VARCHAR(10),
height DECIMAL(5,2),
weight DECIMAL(5,2),
bmi DECIMAL(4,2),
blood_type VARCHAR(10),
occupation VARCHAR(50),
marital_status VARCHAR(20),
region VARCHAR(100)
);
-- 生活习惯表
CREATE TABLE lifestyle_infos (
id BIGINT PRIMARY KEY,
user_id BIGINT UNIQUE NOT NULL,
sleep_time TIME,
wake_time TIME,
sleep_quality VARCHAR(20),
meal_regularity VARCHAR(20),
diet_preference VARCHAR(20),
daily_water_ml INT,
exercise_frequency VARCHAR(20),
exercise_type VARCHAR(50),
exercise_duration_min INT,
is_smoker BOOLEAN,
alcohol_frequency VARCHAR(20)
);
```
### 3.2 健康记录
```sql
-- 病史表
CREATE TABLE medical_histories (
id BIGINT PRIMARY KEY,
health_profile_id BIGINT NOT NULL,
disease_name VARCHAR(100) NOT NULL,
disease_type VARCHAR(50),
diagnosed_date DATE,
status VARCHAR(20),
notes TEXT
);
-- 家族病史表
CREATE TABLE family_histories (
id BIGINT PRIMARY KEY,
health_profile_id BIGINT NOT NULL,
relation VARCHAR(20) NOT NULL,
disease_name VARCHAR(100) NOT NULL,
notes TEXT
);
-- 过敏记录表
CREATE TABLE allergy_records (
id BIGINT PRIMARY KEY,
health_profile_id BIGINT NOT NULL,
allergy_type VARCHAR(50) NOT NULL,
allergen VARCHAR(100) NOT NULL,
severity VARCHAR(20),
reaction_desc TEXT
);
```
### 3.3 体质辨识
```sql
-- 体质测评表
CREATE TABLE constitution_assessments (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
assessed_at TIMESTAMP NOT NULL,
scores JSON, -- 各体质得分
primary_constitution VARCHAR(20),
secondary_constitutions JSON, -- 兼夹体质
recommendations JSON -- 调养建议
);
-- 测评答案表
CREATE TABLE assessment_answers (
id BIGINT PRIMARY KEY,
assessment_id BIGINT NOT NULL,
question_id INT NOT NULL,
score INT CHECK (score >= 1 AND score <= 5)
);
-- 问卷题库表
CREATE TABLE question_banks (
id INT PRIMARY KEY,
constitution_type VARCHAR(20) NOT NULL,
question_text TEXT NOT NULL,
options JSON,
order_num INT
);
```
### 3.4 对话
```sql
-- 对话表
CREATE TABLE conversations (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
title VARCHAR(200),
created_at TIMESTAMP,
updated_at TIMESTAMP,
deleted_at TIMESTAMP
);
-- 消息表
CREATE TABLE messages (
id BIGINT PRIMARY KEY,
conversation_id BIGINT NOT NULL,
role VARCHAR(20) NOT NULL, -- user/assistant/system
content TEXT NOT NULL,
created_at TIMESTAMP
);
```
### 3.5 产品
```sql
-- 产品表
CREATE TABLE products (
id BIGINT PRIMARY KEY,
name VARCHAR(200) NOT NULL,
category VARCHAR(50),
description TEXT,
efficacy TEXT,
suitable TEXT,
price DECIMAL(10,2),
image_url VARCHAR(500),
mall_url VARCHAR(500),
is_active BOOLEAN DEFAULT TRUE
);
-- 体质-产品关联表
CREATE TABLE constitution_products (
id BIGINT PRIMARY KEY,
constitution_type VARCHAR(20) NOT NULL,
product_id BIGINT NOT NULL,
priority INT DEFAULT 0,
reason TEXT
);
-- 购买历史表
CREATE TABLE purchase_histories (
id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL,
order_no VARCHAR(50),
product_id BIGINT,
product_name VARCHAR(200),
purchased_at TIMESTAMP,
source VARCHAR(50)
);
```
---
## 四、配置说明
### 4.1 服务配置
```yaml
Name: health-api
Host: 0.0.0.0
Port: 8080
Mode: dev
# JWT 认证
Auth:
AccessSecret: health-ai-secret-key-change-in-production
AccessExpire: 86400 # 24小时
# 数据库
Database:
Driver: sqlite
DataSource: ./data/health.db
# PostgreSQL
# Driver: postgres
# DataSource: host=localhost port=5432 user=postgres password=xxx dbname=health_app sslmode=disable
# AI 服务
AI:
Provider: aliyun
MaxHistoryMessages: 10
MaxTokens: 2000
Aliyun:
ApiKey: sk-xxx
Model: qwen-plus
OpenAI:
ApiKey: sk-xxx
BaseUrl: https://api.openai.com/v1
Model: gpt-3.5-turbo
```
### 4.2 中间件
1. **JWT 认证中间件**
- 验证 Authorization Header
- 解析 Token 获取 userID
- 注入上下文
2. **CORS 中间件**
- 允许跨域请求
- 配置允许的方法和头部
3. **日志中间件**
- 请求日志记录
- 响应时间统计
---
## 五、API 统计
| 模块 | API 数量 | 需认证 | 说明 |
| -------- | -------- | ------ | --------------------- |
| 认证 | 4 | 0 | 注册/登录/刷新/验证码 |
| 用户 | 2 | 2 | 获取/更新资料 |
| 健康档案 | 12 | 12 | 完整健康信息 CRUD |
| 健康调查 | 10 | 10 | 分步提交流程 |
| 体质辨识 | 6 | 6 | 问卷/测评/结果 |
| AI 对话 | 6 | 6 | 对话管理/消息 |
| 产品 | 6 | 1 | 产品查询/推荐 |
| **总计** | **46** | **37** | |
---
## 六、迁移计划
### 6.1 阶段一:基础框架 ✅ 已完成
- [x] 创建 backend 目录
- [x] 使用 mcp-zero 创建 API 服务结构
- [x] 定义 .api 文件(46 个 API 端点)
- [x] 生成基础代码(47 个 Handler + 47 个 Logic)
### 6.2 阶段二:核心功能
- [ ] 迁移认证模块
- [ ] 迁移用户模块
- [ ] 迁移健康档案模块
### 6.3 阶段三:业务功能
- [ ] 迁移体质辨识模块
- [ ] 迁移 AI 对话模块
- [ ] 迁移产品模块
### 6.4 阶段四:优化完善
- [ ] 添加弹性模式(熔断/限流)
- [ ] 优化数据库访问
- [ ] 添加监控和日志
---
## 七、技术栈对比
| 项目 | 原架构 (Gin) | 目标架构 (go-zero) |
| -------- | -------------------------- | ------------------- |
| Web 框架 | Gin | go-zero rest |
| ORM | GORM | sqlx / goctl model |
| 配置 | Viper | go-zero conf |
| 路由 | Gin Router | .api 文件生成 |
| 中间件 | Gin Middleware | go-zero Middleware |
| 依赖注入 | 手动 | ServiceContext |
| 代码生成 | 无 | goctl |
| 分层架构 | Handler/Service/Repository | Handler/Logic/Model |

29
backend/healthapi/etc/healthapi-api.yaml

@ -0,0 +1,29 @@
Name: healthapi-api
Host: 0.0.0.0
Port: 8080
Mode: dev
Timeout: 300000 # 5分钟超时,用于 AI 流式响应
# JWT 认证配置
Auth:
AccessSecret: health-ai-secret-key-change-in-production
AccessExpire: 86400 # 24小时
# 数据库配置
Database:
Driver: sqlite
DataSource: ./data/health.db
# AI 服务配置
AI:
Provider: aliyun
MaxHistoryMessages: 10
MaxTokens: 2000
Aliyun:
ApiKey: sk-53b4777561624ba98246c7b9990c5e8b
Model: qwen-plus
AppID: "" # 可选:百炼应用 ID(与原 server 保持一致,暂不启用)
OpenAI:
ApiKey: ${AI_OPENAI_API_KEY}
BaseUrl: https://api.openai.com/v1
Model: gpt-3.5-turbo

68
backend/healthapi/go.mod

@ -0,0 +1,68 @@
module healthapi
go 1.25.5
require (
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/zeromicro/go-zero v1.9.4
golang.org/x/crypto v0.47.0
gorm.io/driver/mysql v1.6.0
gorm.io/driver/postgres v1.6.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-sql-driver/mysql v1.9.0 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grafana/pyroscope-go v1.2.7 // indirect
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.4 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.17.11 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/openzipkin/zipkin-go v0.4.3 // indirect
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
github.com/prometheus/client_golang v1.21.1 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect
go.opentelemetry.io/otel v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 // indirect
go.opentelemetry.io/otel/exporters/zipkin v1.24.0 // indirect
go.opentelemetry.io/otel/metric v1.24.0 // indirect
go.opentelemetry.io/otel/sdk v1.24.0 // indirect
go.opentelemetry.io/otel/trace v1.24.0 // indirect
go.opentelemetry.io/proto/otlp v1.3.1 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect
golang.org/x/text v0.33.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect
google.golang.org/grpc v1.65.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

166
backend/healthapi/go.sum

@ -0,0 +1,166 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-sql-driver/mysql v1.9.0 h1:Y0zIbQXhQKmQgTp44Y1dp3wTXcn804QoTptLZT1vtvo=
github.com/go-sql-driver/mysql v1.9.0/go.mod h1:pDetrLJeA3oMujJuvXc8RJoasr589B6A9fwzD3QMrqw=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grafana/pyroscope-go v1.2.7 h1:VWBBlqxjyR0Cwk2W6UrE8CdcdD80GOFNutj0Kb1T8ac=
github.com/grafana/pyroscope-go v1.2.7/go.mod h1:o/bpSLiJYYP6HQtvcoVKiE9s5RiNgjYTj1DhiddP2Pc=
github.com/grafana/pyroscope-go/godeltaprof v0.1.9 h1:c1Us8i6eSmkW+Ez05d3co8kasnuOY813tbMN8i/a3Og=
github.com/grafana/pyroscope-go/godeltaprof v0.1.9/go.mod h1:2+l7K7twW49Ct4wFluZD3tZ6e0SjanjcUUBPVD/UuGU=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/openzipkin/zipkin-go v0.4.3 h1:9EGwpqkgnwdEIJ+Od7QVSEIH+ocmm5nPat0G7sjsSdg=
github.com/openzipkin/zipkin-go v0.4.3/go.mod h1:M9wCJZFWCo2RiY+o1eBCEMe0Dp2S5LDHcMZmk3RmK7c=
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
github.com/prashantv/gostub v1.1.0/go.mod h1:A5zLQHz7ieHGG7is6LLXLz7I8+3LZzsrV0P1IAHhP5U=
github.com/prometheus/client_golang v1.21.1 h1:DOvXXTqVzvkIewV/CDPFdejpMCGeMcbGCQ8YOmu+Ibk=
github.com/prometheus/client_golang v1.21.1/go.mod h1:U9NM32ykUErtVBxdvD3zfi+EuFkkaBvMb09mIfe0Zgg=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/zeromicro/go-zero v1.9.4 h1:aRLFoISqAYijABtkbliQC5SsI5TbizJpQvoHc9xup8k=
github.com/zeromicro/go-zero v1.9.4/go.mod h1:a17JOTch25SWxBcUgJZYps60hygK3pIYdw7nGwlcS38=
go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo=
go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo=
go.opentelemetry.io/otel/exporters/jaeger v1.17.0 h1:D7UpUy2Xc2wsi1Ras6V40q806WM07rqoCWzXu7Sqy+4=
go.opentelemetry.io/otel/exporters/jaeger v1.17.0/go.mod h1:nPCqOnEH9rNLKqH/+rrUjiMzHJdV1BlpKcTwRTyKkKI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 h1:t6wl9SPayj+c7lEIFgm4ooDBZVb01IhLB4InpomhRw8=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0/go.mod h1:iSDOcsnSA5INXzZtwaBPrKp/lWu/V14Dd+llD0oI2EA=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0 h1:Mw5xcxMwlqoJd97vwPxA8isEaIoxsta9/Q51+TTJLGE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.24.0/go.mod h1:CQNu9bj7o7mC6U7+CA/schKEYakYXWr79ucDHTMGhCM=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0 h1:Xw8U6u2f8DK2XAkGRFV7BBLENgnTGX9i4rQRxJf+/vs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.24.0/go.mod h1:6KW1Fm6R/s6Z3PGXwSJN2K4eT6wQB3vXX6CVnYX9NmM=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0 h1:s0PHtIkN+3xrbDOpt2M8OTG92cWqUESvzh2MxiR5xY8=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.24.0/go.mod h1:hZlFbDbRt++MMPCCfSJfmhkGIWnX1h3XjkfxZUjLrIA=
go.opentelemetry.io/otel/exporters/zipkin v1.24.0 h1:3evrL5poBuh1KF51D9gO/S+N/1msnm4DaBqs/rpXUqY=
go.opentelemetry.io/otel/exporters/zipkin v1.24.0/go.mod h1:0EHgD8R0+8yRhUYJOGR8Hfg2dpiJQxDOszd5smVO9wM=
go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI=
go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco=
go.opentelemetry.io/otel/sdk v1.24.0 h1:YMPPDNymmQN3ZgczicBY3B6sf9n62Dlj9pWD3ucgoDw=
go.opentelemetry.io/otel/sdk v1.24.0/go.mod h1:KVrIYw6tEubO9E96HQpcmpTKDVn9gdv35HoYiQWGDFg=
go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI=
go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU=
go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0=
go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d h1:kHjw/5UfflP/L5EbledDrcG4C2597RtymmGRZvHiCuY=
google.golang.org/genproto/googleapis/api v0.0.0-20240711142825-46eb208f015d/go.mod h1:mw8MG/Qz5wfgYr6VqVCiZcHe/GJEfI+oGGDCohaVgB0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY=
google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc=
google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 h1:pUdcCO1Lk/tbT5ztQWOBi5HBgbBP1J8+AsQnQCKsi8A=
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=

931
backend/healthapi/healthapi.api

@ -0,0 +1,931 @@
syntax = "v1"
info (
title: "健康AI助手API"
desc: "健康AI问询助手后端服务"
author: "healthApps"
version: "v1"
)
// ==================== 公共类型 ====================
type (
// 通用响应
CommonResp {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data,omitempty"`
}
// 分页请求
PageReq {
Page int `form:"page,default=1"`
PageSize int `form:"page_size,default=10"`
}
// 分页响应
PageInfo {
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
)
// ==================== 认证模块 ====================
type (
// 注册请求
RegisterReq {
Phone string `json:"phone,optional"`
Email string `json:"email,optional"`
Password string `json:"password"`
Code string `json:"code,optional"`
}
// 登录请求
LoginReq {
Phone string `json:"phone,optional"`
Email string `json:"email,optional"`
Password string `json:"password"`
}
// 登录响应
LoginResp {
Token string `json:"token"`
ExpiresAt int64 `json:"expires_at"`
User UserInfo `json:"user"`
}
// 用户信息
UserInfo {
ID uint `json:"id"`
Phone string `json:"phone,omitempty"`
Email string `json:"email,omitempty"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
SurveyCompleted bool `json:"survey_completed"`
}
// 刷新Token请求
RefreshTokenReq {
Token string `json:"token"`
}
// 发送验证码请求
SendCodeReq {
Phone string `json:"phone,optional"`
Email string `json:"email,optional"`
Type string `json:"type"` // register/login/reset
}
// 更新用户资料请求
UpdateProfileReq {
Nickname string `json:"nickname,optional"`
Avatar string `json:"avatar,optional"`
}
)
// ==================== 健康档案模块 ====================
type (
// 健康档案
HealthProfile {
ID uint `json:"id"`
UserID uint `json:"user_id"`
Name string `json:"name"`
BirthDate string `json:"birth_date"`
Gender string `json:"gender"`
Height float64 `json:"height"`
Weight float64 `json:"weight"`
BMI float64 `json:"bmi"`
BloodType string `json:"blood_type"`
Occupation string `json:"occupation"`
MaritalStatus string `json:"marital_status"`
Region string `json:"region"`
}
// 生活习惯
LifestyleInfo {
ID uint `json:"id"`
UserID uint `json:"user_id"`
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"`
}
// 病史
MedicalHistory {
ID uint `json:"id"`
HealthProfileID uint `json:"health_profile_id"`
DiseaseName string `json:"disease_name"`
DiseaseType string `json:"disease_type"`
DiagnosedDate string `json:"diagnosed_date"`
Status string `json:"status"`
Notes string `json:"notes"`
}
// 家族病史
FamilyHistory {
ID uint `json:"id"`
HealthProfileID uint `json:"health_profile_id"`
Relation string `json:"relation"`
DiseaseName string `json:"disease_name"`
Notes string `json:"notes"`
}
// 过敏记录
AllergyRecord {
ID uint `json:"id"`
HealthProfileID uint `json:"health_profile_id"`
AllergyType string `json:"allergy_type"`
Allergen string `json:"allergen"`
Severity string `json:"severity"`
ReactionDesc string `json:"reaction_desc"`
}
// 完整健康档案响应
FullHealthProfileResp {
Profile HealthProfile `json:"profile"`
Lifestyle LifestyleInfo `json:"lifestyle"`
MedicalHistory []MedicalHistory `json:"medical_history"`
FamilyHistory []FamilyHistory `json:"family_history"`
AllergyRecords []AllergyRecord `json:"allergy_records"`
}
// 更新健康档案请求
UpdateHealthProfileReq {
Name string `json:"name,optional"`
BirthDate string `json:"birth_date,optional"`
Gender string `json:"gender,optional"`
Height float64 `json:"height,optional"`
Weight float64 `json:"weight,optional"`
BloodType string `json:"blood_type,optional"`
Occupation string `json:"occupation,optional"`
MaritalStatus string `json:"marital_status,optional"`
Region string `json:"region,optional"`
}
// 更新生活习惯请求
UpdateLifestyleReq {
SleepTime string `json:"sleep_time,optional"`
WakeTime string `json:"wake_time,optional"`
SleepQuality string `json:"sleep_quality,optional"`
MealRegularity string `json:"meal_regularity,optional"`
DietPreference string `json:"diet_preference,optional"`
DailyWaterML int `json:"daily_water_ml,optional"`
ExerciseFrequency string `json:"exercise_frequency,optional"`
ExerciseType string `json:"exercise_type,optional"`
ExerciseDurationMin int `json:"exercise_duration_min,optional"`
IsSmoker bool `json:"is_smoker,optional"`
AlcoholFrequency string `json:"alcohol_frequency,optional"`
}
// ID路径参数
IdPathReq {
Id uint `path:"id"`
}
)
// ==================== 健康调查模块 ====================
type (
// 调查状态响应
SurveyStatusResp {
Completed bool `json:"completed"`
BasicInfo bool `json:"basic_info"`
Lifestyle bool `json:"lifestyle"`
MedicalHistory bool `json:"medical_history"`
FamilyHistory bool `json:"family_history"`
Allergy bool `json:"allergy"`
}
// 提交基础信息请求
SubmitBasicInfoReq {
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,optional"`
Occupation string `json:"occupation,optional"`
MaritalStatus string `json:"marital_status,optional"`
Region string `json:"region,optional"`
}
// 提交生活习惯请求
SubmitLifestyleReq {
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,optional"`
DailyWaterML int `json:"daily_water_ml,optional"`
ExerciseFrequency string `json:"exercise_frequency"`
ExerciseType string `json:"exercise_type,optional"`
ExerciseDurationMin int `json:"exercise_duration_min,optional"`
IsSmoker bool `json:"is_smoker"`
AlcoholFrequency string `json:"alcohol_frequency"`
}
// 提交病史请求
SubmitMedicalHistoryReq {
DiseaseName string `json:"disease_name"`
DiseaseType string `json:"disease_type,optional"`
DiagnosedDate string `json:"diagnosed_date,optional"`
Status string `json:"status,optional"`
Notes string `json:"notes,optional"`
}
// 批量提交病史请求
BatchMedicalHistoryReq {
Items []SubmitMedicalHistoryReq `json:"items"`
}
// 提交家族史请求
SubmitFamilyHistoryReq {
Relation string `json:"relation"`
DiseaseName string `json:"disease_name"`
Notes string `json:"notes,optional"`
}
// 批量提交家族史请求
BatchFamilyHistoryReq {
Items []SubmitFamilyHistoryReq `json:"items"`
}
// 提交过敏信息请求
SubmitAllergyReq {
AllergyType string `json:"allergy_type"`
Allergen string `json:"allergen"`
Severity string `json:"severity,optional"`
ReactionDesc string `json:"reaction_desc,optional"`
}
// 批量提交过敏信息请求
BatchAllergyReq {
Items []SubmitAllergyReq `json:"items"`
}
)
// ==================== 体质辨识模块 ====================
type (
// 问卷题目
Question {
ID int `json:"id"`
ConstitutionType string `json:"constitution_type"`
QuestionText string `json:"question_text"`
Options []string `json:"options"`
OrderNum int `json:"order_num"`
}
// 问卷题目列表响应
QuestionsResp {
Questions []Question `json:"questions"`
}
// 分组问卷响应
GroupedQuestionsResp {
Groups map[string][]Question `json:"groups"`
}
// 提交测评请求
SubmitAssessmentReq {
Answers []AnswerItem `json:"answers"`
}
// 答题项
AnswerItem {
QuestionID int `json:"question_id"`
Score int `json:"score"` // 1-5
}
// 测评结果
AssessmentResult {
ID uint `json:"id"`
UserID uint `json:"user_id"`
AssessedAt string `json:"assessed_at"`
Scores map[string]float64 `json:"scores"`
PrimaryConstitution string `json:"primary_constitution"`
SecondaryConstitutions []string `json:"secondary_constitutions"`
Recommendations map[string]string `json:"recommendations"`
}
// 测评历史响应
AssessmentHistoryResp {
History []AssessmentResult `json:"history"`
}
// 调养建议响应
RecommendationsResp {
Constitution string `json:"constitution"`
Recommendations map[string]string `json:"recommendations"`
}
)
// ==================== AI对话模块 ====================
type (
// 对话列表项
ConversationItem {
ID uint `json:"id"`
Title string `json:"title"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// 对话列表响应
ConversationListResp {
Conversations []ConversationItem `json:"conversations"`
}
// 创建对话请求
CreateConversationReq {
Title string `json:"title,optional"`
}
// 消息
Message {
ID uint `json:"id"`
ConversationID uint `json:"conversation_id"`
Role string `json:"role"` // user/assistant/system
Content string `json:"content"`
CreatedAt string `json:"created_at"`
}
// 对话详情响应
ConversationDetailResp {
ID uint `json:"id"`
Title string `json:"title"`
Messages []Message `json:"messages"`
CreatedAt string `json:"created_at"`
}
// 对话ID路径参数
ConversationIdReq {
Id uint `path:"id"`
}
// 发送消息请求
SendMessageReq {
Id uint `path:"id"`
Content string `json:"content"`
}
// 消息响应
MessageResp {
UserMessage Message `json:"user_message"`
AssistantMessage Message `json:"assistant_message"`
}
)
// ==================== 产品模块 ====================
type (
// 产品
Product {
ID uint `json:"id"`
Name string `json:"name"`
Category string `json:"category"`
Description string `json:"description"`
Efficacy string `json:"efficacy"`
Suitable string `json:"suitable"`
Price float64 `json:"price"`
ImageURL string `json:"image_url"`
MallURL string `json:"mall_url"`
IsActive bool `json:"is_active"`
}
// 产品列表请求
ProductListReq {
Page int `form:"page,default=1"`
PageSize int `form:"page_size,default=10"`
Category string `form:"category,optional"`
}
// 产品列表响应
ProductListResp {
Products []Product `json:"products"`
PageInfo PageInfo `json:"page_info"`
}
// 产品搜索请求
ProductSearchReq {
Keyword string `form:"keyword"`
Page int `form:"page,default=1"`
PageSize int `form:"page_size,default=10"`
}
// 推荐产品响应
RecommendProductsResp {
Products []Product `json:"products"`
Constitution string `json:"constitution"`
Reason string `json:"reason"`
}
// 购买历史
PurchaseHistory {
ID uint `json:"id"`
UserID uint `json:"user_id"`
OrderNo string `json:"order_no"`
ProductID uint `json:"product_id"`
ProductName string `json:"product_name"`
PurchasedAt string `json:"purchased_at"`
Source string `json:"source"`
}
// 购买历史响应
PurchaseHistoryResp {
History []PurchaseHistory `json:"history"`
PageInfo PageInfo `json:"page_info"`
}
// 同步购买请求
SyncPurchaseReq {
OrderNo string `json:"order_no"`
ProductID uint `json:"product_id"`
ProductName string `json:"product_name"`
UserID uint `json:"user_id"`
PurchasedAt string `json:"purchased_at"`
Source string `json:"source"`
}
)
// ==================== 路由定义 ====================
// 无需认证的路由
@server (
prefix: /api
)
service healthapi-api {
// 健康检查
@handler HealthCheckHandler
get /health returns (CommonResp)
// 认证模块
@handler RegisterHandler
post /auth/register (RegisterReq) returns (LoginResp)
@handler LoginHandler
post /auth/login (LoginReq) returns (LoginResp)
@handler RefreshTokenHandler
post /auth/refresh (RefreshTokenReq) returns (LoginResp)
@handler SendCodeHandler
post /auth/send-code (SendCodeReq) returns (CommonResp)
// 产品模块(公开)
@handler GetProductListHandler
get /products (ProductListReq) returns (ProductListResp)
@handler GetProductHandler
get /products/:id (IdPathReq) returns (Product)
@handler GetProductsByCategoryHandler
get /products/category (ProductListReq) returns (ProductListResp)
@handler SearchProductsHandler
get /products/search (ProductSearchReq) returns (ProductListResp)
// 商城同步(需要API Key,在handler中验证)
@handler SyncPurchaseHandler
post /sync/purchase (SyncPurchaseReq) returns (CommonResp)
}
// 需要认证的路由
@server (
prefix: /api
jwt: Auth
)
service healthapi-api {
// 用户模块
@handler GetUserProfileHandler
get /user/profile returns (UserInfo)
@handler UpdateUserProfileHandler
put /user/profile (UpdateProfileReq) returns (UserInfo)
// 健康档案模块
@handler GetHealthProfileHandler
get /user/health-profile returns (FullHealthProfileResp)
@handler UpdateHealthProfileHandler
put /user/health-profile (UpdateHealthProfileReq) returns (HealthProfile)
@handler GetBasicProfileHandler
get /user/basic-profile returns (HealthProfile)
@handler GetLifestyleHandler
get /user/lifestyle returns (LifestyleInfo)
@handler UpdateLifestyleHandler
put /user/lifestyle (UpdateLifestyleReq) returns (LifestyleInfo)
@handler GetMedicalHistoryHandler
get /user/medical-history returns ([]MedicalHistory)
@handler DeleteMedicalHistoryHandler
delete /user/medical-history/:id (IdPathReq) returns (CommonResp)
@handler GetFamilyHistoryHandler
get /user/family-history returns ([]FamilyHistory)
@handler DeleteFamilyHistoryHandler
delete /user/family-history/:id (IdPathReq) returns (CommonResp)
@handler GetAllergyRecordsHandler
get /user/allergy-records returns ([]AllergyRecord)
@handler DeleteAllergyRecordHandler
delete /user/allergy-records/:id (IdPathReq) returns (CommonResp)
@handler GetPurchaseHistoryHandler
get /user/purchase-history (PageReq) returns (PurchaseHistoryResp)
// 健康调查模块
@handler GetSurveyStatusHandler
get /survey/status returns (SurveyStatusResp)
@handler SubmitBasicInfoHandler
post /survey/basic-info (SubmitBasicInfoReq) returns (CommonResp)
@handler SubmitLifestyleHandler
post /survey/lifestyle (SubmitLifestyleReq) returns (CommonResp)
@handler SubmitMedicalHistoryHandler
post /survey/medical-history (SubmitMedicalHistoryReq) returns (CommonResp)
@handler BatchSubmitMedicalHistoryHandler
post /survey/medical-history/batch (BatchMedicalHistoryReq) returns (CommonResp)
@handler SubmitFamilyHistoryHandler
post /survey/family-history (SubmitFamilyHistoryReq) returns (CommonResp)
@handler BatchSubmitFamilyHistoryHandler
post /survey/family-history/batch (BatchFamilyHistoryReq) returns (CommonResp)
@handler SubmitAllergyHandler
post /survey/allergy (SubmitAllergyReq) returns (CommonResp)
@handler BatchSubmitAllergyHandler
post /survey/allergy/batch (BatchAllergyReq) returns (CommonResp)
@handler CompleteSurveyHandler
post /survey/complete returns (CommonResp)
// 体质辨识模块
@handler GetQuestionsHandler
get /constitution/questions returns (QuestionsResp)
@handler GetGroupedQuestionsHandler
get /constitution/questions/grouped returns (GroupedQuestionsResp)
@handler SubmitAssessmentHandler
post /constitution/submit (SubmitAssessmentReq) returns (AssessmentResult)
@handler GetAssessmentResultHandler
get /constitution/result returns (AssessmentResult)
@handler GetAssessmentHistoryHandler
get /constitution/history returns (AssessmentHistoryResp)
@handler GetRecommendationsHandler
get /constitution/recommendations returns (RecommendationsResp)
// AI对话模块
@handler GetConversationsHandler
get /conversations returns (ConversationListResp)
@handler CreateConversationHandler
post /conversations (CreateConversationReq) returns (ConversationItem)
@handler GetConversationHandler
get /conversations/:id (ConversationIdReq) returns (ConversationDetailResp)
@handler DeleteConversationHandler
delete /conversations/:id (ConversationIdReq) returns (CommonResp)
@handler SendMessageHandler
post /conversations/:id/messages (SendMessageReq) returns (MessageResp)
// 流式消息需要特殊处理,在handler中实现SSE
@handler SendMessageStreamHandler
post /conversations/:id/messages/stream (SendMessageReq)
// 产品推荐(需要认证,基于用户体质)
@handler GetRecommendProductsHandler
get /products/recommend returns (RecommendProductsResp)
}
// ==================== 会员系统模块 ====================
type (
// 会员信息
MemberInfo {
Level string `json:"level"` // normal/silver/gold/diamond
LevelName string `json:"level_name"` // 等级名称
TotalSpent float64 `json:"total_spent"` // 累计消费
Points int `json:"points"` // 当前积分
MemberSince string `json:"member_since"` // 首次消费时间
NextLevel string `json:"next_level"` // 下一等级
NextLevelSpent float64 `json:"next_level_spent"` // 升级还需消费
Discount float64 `json:"discount"` // 当前折扣
PointsMultiplier float64 `json:"points_multiplier"` // 积分倍率
FreeShippingMin float64 `json:"free_shipping_min"` // 包邮门槛
}
// 积分记录
PointsRecord {
ID uint `json:"id"`
Type string `json:"type"` // earn/spend/expire/adjust
Points int `json:"points"` // 变动积分
Balance int `json:"balance"` // 变动后余额
Source string `json:"source"` // order/activity/system
Remark string `json:"remark"`
CreatedAt string `json:"created_at"`
}
// 积分记录列表响应
PointsRecordsResp {
Records []PointsRecord `json:"records"`
PageInfo PageInfo `json:"page_info"`
}
// 积分兑换请求(积分抵扣现金)
UsePointsReq {
Points int `json:"points"` // 使用积分数
OrderNo string `json:"order_no,optional"` // 关联订单
}
)
// ==================== 商城商品模块 ====================
type (
// 商品分类
ProductCategory {
ID uint `json:"id"`
Name string `json:"name"`
ParentID uint `json:"parent_id"`
Icon string `json:"icon"`
Description string `json:"description"`
Sort int `json:"sort"`
}
// 分类列表响应
CategoryListResp {
Categories []ProductCategory `json:"categories"`
}
// 商品SKU
ProductSku {
ID uint `json:"id"`
ProductID uint `json:"product_id"`
SkuCode string `json:"sku_code"`
Name string `json:"name"`
Attributes string `json:"attributes"`
Price float64 `json:"price"`
Stock int `json:"stock"`
Image string `json:"image"`
}
// 商品详情(包含SKU)
ProductDetail {
ID uint `json:"id"`
CategoryID uint `json:"category_id"`
Name string `json:"name"`
Description string `json:"description"`
MainImage string `json:"main_image"`
Images []string `json:"images"`
Price float64 `json:"price"`
OriginalPrice float64 `json:"original_price"`
Stock int `json:"stock"`
SalesCount int `json:"sales_count"`
IsFeatured bool `json:"is_featured"`
ConstitutionTypes []string `json:"constitution_types"`
HealthTags []string `json:"health_tags"`
Efficacy string `json:"efficacy"`
Ingredients string `json:"ingredients"`
Usage string `json:"usage"`
Contraindications string `json:"contraindications"`
Skus []ProductSku `json:"skus"`
}
)
// ==================== 购物车模块 ====================
type (
// 购物车项
CartItem {
ID uint `json:"id"`
ProductID uint `json:"product_id"`
SkuID uint `json:"sku_id"`
ProductName string `json:"product_name"`
SkuName string `json:"sku_name"`
Image string `json:"image"`
Price float64 `json:"price"`
Quantity int `json:"quantity"`
Selected bool `json:"selected"`
Stock int `json:"stock"` // 当前库存
}
// 购物车响应
CartResp {
Items []CartItem `json:"items"`
TotalCount int `json:"total_count"`
SelectedCount int `json:"selected_count"`
TotalAmount float64 `json:"total_amount"`
}
// 添加购物车请求
AddCartReq {
ProductID uint `json:"product_id"`
SkuID uint `json:"sku_id,optional"`
Quantity int `json:"quantity,default=1"`
}
// 更新购物车请求
UpdateCartReq {
Id uint `path:"id"`
Quantity int `json:"quantity,optional"`
Selected bool `json:"selected,optional"`
}
// 批量选择请求
BatchSelectCartReq {
Ids []uint `json:"ids"`
Selected bool `json:"selected"`
}
)
// ==================== 收货地址模块 ====================
type (
// 收货地址
Address {
ID uint `json:"id"`
ReceiverName string `json:"receiver_name"`
Phone string `json:"phone"`
Province string `json:"province"`
City string `json:"city"`
District string `json:"district"`
DetailAddr string `json:"detail_addr"`
PostalCode string `json:"postal_code"`
IsDefault bool `json:"is_default"`
Tag string `json:"tag"` // home/company/other
}
// 地址列表响应
AddressListResp {
Addresses []Address `json:"addresses"`
}
// 创建/更新地址请求
SaveAddressReq {
ReceiverName string `json:"receiver_name"`
Phone string `json:"phone"`
Province string `json:"province"`
City string `json:"city"`
District string `json:"district"`
DetailAddr string `json:"detail_addr"`
PostalCode string `json:"postal_code,optional"`
IsDefault bool `json:"is_default,optional"`
Tag string `json:"tag,optional"`
}
// 地址ID请求
AddressIdReq {
Id uint `path:"id"`
}
)
// ==================== 订单模块 ====================
type (
// 订单商品项
OrderItem {
ID uint `json:"id"`
ProductID uint `json:"product_id"`
SkuID uint `json:"sku_id"`
ProductName string `json:"product_name"`
SkuName string `json:"sku_name"`
Image string `json:"image"`
Price float64 `json:"price"`
Quantity int `json:"quantity"`
TotalAmount float64 `json:"total_amount"`
}
// 订单
Order {
ID uint `json:"id"`
OrderNo string `json:"order_no"`
Status string `json:"status"`
TotalAmount float64 `json:"total_amount"`
DiscountAmount float64 `json:"discount_amount"`
ShippingFee float64 `json:"shipping_fee"`
PayAmount float64 `json:"pay_amount"`
PointsUsed int `json:"points_used"`
PointsEarned int `json:"points_earned"`
PayMethod string `json:"pay_method"`
PayTime string `json:"pay_time"`
ShipTime string `json:"ship_time"`
ReceiveTime string `json:"receive_time"`
ReceiverName string `json:"receiver_name"`
ReceiverPhone string `json:"receiver_phone"`
ReceiverAddr string `json:"receiver_addr"`
ShippingCompany string `json:"shipping_company"`
TrackingNo string `json:"tracking_no"`
Remark string `json:"remark"`
CancelReason string `json:"cancel_reason"`
Items []OrderItem `json:"items"`
CreatedAt string `json:"created_at"`
}
// 订单列表响应
OrderListResp {
Orders []Order `json:"orders"`
PageInfo PageInfo `json:"page_info"`
}
// 订单列表请求
OrderListReq {
Page int `form:"page,default=1"`
PageSize int `form:"page_size,default=10"`
Status string `form:"status,optional"` // 筛选状态
}
// 创建订单请求
CreateOrderReq {
AddressID uint `json:"address_id"`
CartItemIDs []uint `json:"cart_item_ids"` // 购物车项ID
PointsUsed int `json:"points_used,optional"` // 使用积分
Remark string `json:"remark,optional"`
}
// 订单预览(结算页)
OrderPreview {
Items []CartItem `json:"items"`
TotalAmount float64 `json:"total_amount"`
DiscountAmount float64 `json:"discount_amount"`
ShippingFee float64 `json:"shipping_fee"`
PayAmount float64 `json:"pay_amount"`
MaxPointsUse int `json:"max_points_use"` // 最多可用积分
PointsDiscount float64 `json:"points_discount"` // 积分可抵扣金额
}
// 订单预览请求
OrderPreviewReq {
CartItemIDs []uint `json:"cart_item_ids"`
AddressID uint `json:"address_id,optional"`
}
// 订单ID请求
OrderIdReq {
Id uint `path:"id"`
}
// 支付订单请求
PayOrderReq {
Id uint `path:"id"`
PayMethod string `json:"pay_method"` // wechat/alipay
}
// 取消订单请求
CancelOrderReq {
Id uint `path:"id"`
Reason string `json:"reason,optional"`
}
)
// ==================== 商城公开路由 ====================
@server (
prefix: /api/mall
)
service healthapi-api {
// 商品分类
@handler GetCategoriesHandler
get /categories returns (CategoryListResp)
// 商品列表(支持分类筛选)
@handler GetMallProductsHandler
get /products (ProductListReq) returns (ProductListResp)
// 商品详情
@handler GetMallProductDetailHandler
get /products/:id (IdPathReq) returns (ProductDetail)
// 搜索商品
@handler SearchMallProductsHandler
get /products/search (ProductSearchReq) returns (ProductListResp)
// 推荐/热门商品
@handler GetFeaturedProductsHandler
get /products/featured (PageReq) returns (ProductListResp)
}
// ==================== 商城认证路由 ====================
@server (
prefix: /api/mall
jwt: Auth
)
service healthapi-api {
// ===== 会员 =====
@handler GetMemberInfoHandler
get /member/info returns (MemberInfo)
@handler GetPointsRecordsHandler
get /member/points/records (PageReq) returns (PointsRecordsResp)
// ===== 购物车 =====
@handler GetCartHandler
get /cart returns (CartResp)
@handler AddCartHandler
post /cart (AddCartReq) returns (CartItem)
@handler UpdateCartHandler
put /cart/:id (UpdateCartReq) returns (CartItem)
@handler DeleteCartHandler
delete /cart/:id (IdPathReq) returns (CommonResp)
@handler BatchSelectCartHandler
post /cart/batch-select (BatchSelectCartReq) returns (CommonResp)
@handler ClearCartHandler
delete /cart/clear returns (CommonResp)
// ===== 收货地址 =====
@handler GetAddressesHandler
get /addresses returns (AddressListResp)
@handler GetAddressHandler
get /addresses/:id (AddressIdReq) returns (Address)
@handler CreateAddressHandler
post /addresses (SaveAddressReq) returns (Address)
@handler UpdateAddressHandler
put /addresses/:id (AddressIdReq)
@handler DeleteAddressHandler
delete /addresses/:id (AddressIdReq) returns (CommonResp)
@handler SetDefaultAddressHandler
put /addresses/:id/default (AddressIdReq) returns (CommonResp)
// ===== 订单 =====
@handler GetOrdersHandler
get /orders (OrderListReq) returns (OrderListResp)
@handler GetOrderHandler
get /orders/:id (OrderIdReq) returns (Order)
@handler PreviewOrderHandler
post /orders/preview (OrderPreviewReq) returns (OrderPreview)
@handler CreateOrderHandler
post /orders (CreateOrderReq) returns (Order)
@handler PayOrderHandler
post /orders/:id/pay (PayOrderReq) returns (CommonResp)
@handler CancelOrderHandler
post /orders/:id/cancel (CancelOrderReq) returns (CommonResp)
@handler ConfirmReceiveHandler
post /orders/:id/receive (OrderIdReq) returns (CommonResp)
// ===== 基于体质的商品推荐 =====
@handler GetConstitutionProductsHandler
get /products/constitution-recommend returns (RecommendProductsResp)
}

50
backend/healthapi/healthapi.go

@ -0,0 +1,50 @@
package main
import (
"flag"
"fmt"
"net/http"
"healthapi/internal/config"
"healthapi/internal/handler"
"healthapi/internal/svc"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/rest"
)
var configFile = flag.String("f", "etc/healthapi-api.yaml", "the config file")
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
// 创建服务器并配置 CORS
server := rest.MustNewServer(c.RestConf, rest.WithCors("*"))
defer server.Stop()
// 添加 CORS 中间件处理 OPTIONS 预检请求
server.Use(func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS, PATCH")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
w.Header().Set("Access-Control-Max-Age", "86400")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next(w, r)
}
})
ctx := svc.NewServiceContext(c)
handler.RegisterHandlers(server, ctx)
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
server.Start()
}

40
backend/healthapi/internal/config/config.go

@ -0,0 +1,40 @@
package config
import "github.com/zeromicro/go-zero/rest"
type Config struct {
rest.RestConf
Auth AuthConfig
Database DatabaseConfig
AI AIConfig
}
type AuthConfig struct {
AccessSecret string
AccessExpire int64
}
type DatabaseConfig struct {
Driver string
DataSource string
}
type AIConfig struct {
Provider string
MaxHistoryMessages int
MaxTokens int
Aliyun AliyunConfig
OpenAI OpenAIConfig
}
type AliyunConfig struct {
ApiKey string
Model string
AppID string // 百炼应用 ID(可选)
}
type OpenAIConfig struct {
ApiKey string
BaseUrl string
Model string
}

115
backend/healthapi/internal/database/database.go

@ -0,0 +1,115 @@
package database
import (
"healthapi/internal/config"
"healthapi/internal/model"
"github.com/zeromicro/go-zero/core/logx"
"golang.org/x/crypto/bcrypt"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// NewDB 创建数据库连接
func NewDB(cfg config.DatabaseConfig) (*gorm.DB, error) {
var dialector gorm.Dialector
switch cfg.Driver {
case "sqlite":
dialector = sqlite.Open(cfg.DataSource)
case "mysql":
dialector = mysql.Open(cfg.DataSource)
case "postgres":
dialector = postgres.Open(cfg.DataSource)
default:
dialector = sqlite.Open(cfg.DataSource)
}
db, err := gorm.Open(dialector, &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
return nil, err
}
// 自动迁移
if err := model.AutoMigrate(db); err != nil {
return nil, err
}
logx.Info("Database connected successfully")
return db, nil
}
// SeedQuestionBank 初始化问卷题库
func SeedQuestionBank(db *gorm.DB) error {
var count int64
db.Model(&model.QuestionBank{}).Count(&count)
if count > 0 {
return nil // 已有数据,跳过
}
questions := []model.QuestionBank{
// 平和质问题
{ConstitutionType: model.ConstitutionPinghe, QuestionText: "您精力充沛吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 1},
{ConstitutionType: model.ConstitutionPinghe, QuestionText: "您容易疲乏吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 2},
{ConstitutionType: model.ConstitutionPinghe, QuestionText: "您说话声音低弱无力吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 3},
// 气虚质问题
{ConstitutionType: model.ConstitutionQixu, QuestionText: "您容易气短,呼吸短促吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 1},
{ConstitutionType: model.ConstitutionQixu, QuestionText: "您容易心慌吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 2},
{ConstitutionType: model.ConstitutionQixu, QuestionText: "您容易头晕或站起时晕眩吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 3},
// 阳虚质问题
{ConstitutionType: model.ConstitutionYangxu, QuestionText: "您手脚发凉吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 1},
{ConstitutionType: model.ConstitutionYangxu, QuestionText: "您胃脘部、背部或腰膝部怕冷吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 2},
{ConstitutionType: model.ConstitutionYangxu, QuestionText: "您比一般人耐受不了寒冷吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 3},
// 阴虚质问题
{ConstitutionType: model.ConstitutionYinxu, QuestionText: "您感到手脚心发热吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 1},
{ConstitutionType: model.ConstitutionYinxu, QuestionText: "您感觉身体、脸上发热吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 2},
{ConstitutionType: model.ConstitutionYinxu, QuestionText: "您口唇干吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 3},
// 痰湿质问题
{ConstitutionType: model.ConstitutionTanshi, QuestionText: "您感到胸闷或腹部胀满吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 1},
{ConstitutionType: model.ConstitutionTanshi, QuestionText: "您感到身体沉重不轻松吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 2},
{ConstitutionType: model.ConstitutionTanshi, QuestionText: "您腹部肥满松软吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 3},
// 湿热质问题
{ConstitutionType: model.ConstitutionShire, QuestionText: "您面部或鼻部有油腻感或者油亮发光吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 1},
{ConstitutionType: model.ConstitutionShire, QuestionText: "您容易生痤疮或疮疖吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 2},
{ConstitutionType: model.ConstitutionShire, QuestionText: "您感到口苦或嘴里有异味吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 3},
// 血瘀质问题
{ConstitutionType: model.ConstitutionXueyu, QuestionText: "您皮肤常在不知不觉中出现青紫瘀斑吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 1},
{ConstitutionType: model.ConstitutionXueyu, QuestionText: "您两颧部有细微红丝吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 2},
{ConstitutionType: model.ConstitutionXueyu, QuestionText: "您身体上有哪里疼痛吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 3},
// 气郁质问题
{ConstitutionType: model.ConstitutionQiyu, QuestionText: "您感到闷闷不乐、情绪低沉吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 1},
{ConstitutionType: model.ConstitutionQiyu, QuestionText: "您容易精神紧张、焦虑不安吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 2},
{ConstitutionType: model.ConstitutionQiyu, QuestionText: "您多愁善感、容易感到害怕吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 3},
// 特禀质问题
{ConstitutionType: model.ConstitutionTebing, QuestionText: "您没有感冒时也会打喷嚏吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 1},
{ConstitutionType: model.ConstitutionTebing, QuestionText: "您没有感冒时也会鼻塞、流鼻涕吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 2},
{ConstitutionType: model.ConstitutionTebing, QuestionText: "您对某些药物、食物、气味或花粉过敏吗?", Options: `["没有","很少","有时","经常","总是"]`, OrderNum: 3},
}
return db.Create(&questions).Error
}
// SeedTestUser 创建测试用户
func SeedTestUser(db *gorm.DB) error {
var count int64
db.Model(&model.User{}).Count(&count)
if count > 0 {
return nil
}
// 测试用户,密码: 123456
passwordHash, _ := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost)
testUser := model.User{
Phone: "13800138000",
Email: "test@example.com",
PasswordHash: string(passwordHash),
Nickname: "测试用户",
}
return db.Create(&testUser).Error
}

28
backend/healthapi/internal/handler/add_cart_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func AddCartHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AddCartReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewAddCartLogic(r.Context(), svcCtx)
resp, err := l.AddCart(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

28
backend/healthapi/internal/handler/batch_select_cart_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func BatchSelectCartHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.BatchSelectCartReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewBatchSelectCartLogic(r.Context(), svcCtx)
resp, err := l.BatchSelectCart(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

29
backend/healthapi/internal/handler/batch_submit_allergy_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func BatchSubmitAllergyHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.BatchAllergyReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewBatchSubmitAllergyLogic(r.Context(), svcCtx)
resp, err := l.BatchSubmitAllergy(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

29
backend/healthapi/internal/handler/batch_submit_family_history_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func BatchSubmitFamilyHistoryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.BatchFamilyHistoryReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewBatchSubmitFamilyHistoryLogic(r.Context(), svcCtx)
resp, err := l.BatchSubmitFamilyHistory(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

29
backend/healthapi/internal/handler/batch_submit_medical_history_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func BatchSubmitMedicalHistoryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.BatchMedicalHistoryReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewBatchSubmitMedicalHistoryLogic(r.Context(), svcCtx)
resp, err := l.BatchSubmitMedicalHistory(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

28
backend/healthapi/internal/handler/cancel_order_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func CancelOrderHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.CancelOrderReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewCancelOrderLogic(r.Context(), svcCtx)
resp, err := l.CancelOrder(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

21
backend/healthapi/internal/handler/clear_cart_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
)
func ClearCartHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewClearCartLogic(r.Context(), svcCtx)
resp, err := l.ClearCart()
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

21
backend/healthapi/internal/handler/complete_survey_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/pkg/response"
)
func CompleteSurveyHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewCompleteSurveyLogic(r.Context(), svcCtx)
resp, err := l.CompleteSurvey()
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

28
backend/healthapi/internal/handler/confirm_receive_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func ConfirmReceiveHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.OrderIdReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewConfirmReceiveLogic(r.Context(), svcCtx)
resp, err := l.ConfirmReceive(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

28
backend/healthapi/internal/handler/create_address_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func CreateAddressHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SaveAddressReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewCreateAddressLogic(r.Context(), svcCtx)
resp, err := l.CreateAddress(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

29
backend/healthapi/internal/handler/create_conversation_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func CreateConversationHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.CreateConversationReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewCreateConversationLogic(r.Context(), svcCtx)
resp, err := l.CreateConversation(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

28
backend/healthapi/internal/handler/create_order_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func CreateOrderHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.CreateOrderReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewCreateOrderLogic(r.Context(), svcCtx)
resp, err := l.CreateOrder(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

28
backend/healthapi/internal/handler/delete_address_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func DeleteAddressHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AddressIdReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewDeleteAddressLogic(r.Context(), svcCtx)
resp, err := l.DeleteAddress(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

29
backend/healthapi/internal/handler/delete_allergy_record_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func DeleteAllergyRecordHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.IdPathReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewDeleteAllergyRecordLogic(r.Context(), svcCtx)
resp, err := l.DeleteAllergyRecord(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

28
backend/healthapi/internal/handler/delete_cart_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func DeleteCartHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.IdPathReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewDeleteCartLogic(r.Context(), svcCtx)
resp, err := l.DeleteCart(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

29
backend/healthapi/internal/handler/delete_conversation_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func DeleteConversationHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.ConversationIdReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewDeleteConversationLogic(r.Context(), svcCtx)
resp, err := l.DeleteConversation(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

29
backend/healthapi/internal/handler/delete_family_history_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func DeleteFamilyHistoryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.IdPathReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewDeleteFamilyHistoryLogic(r.Context(), svcCtx)
resp, err := l.DeleteFamilyHistory(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

29
backend/healthapi/internal/handler/delete_medical_history_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func DeleteMedicalHistoryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.IdPathReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewDeleteMedicalHistoryLogic(r.Context(), svcCtx)
resp, err := l.DeleteMedicalHistory(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

28
backend/healthapi/internal/handler/get_address_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func GetAddressHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AddressIdReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewGetAddressLogic(r.Context(), svcCtx)
resp, err := l.GetAddress(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

21
backend/healthapi/internal/handler/get_addresses_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
)
func GetAddressesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewGetAddressesLogic(r.Context(), svcCtx)
resp, err := l.GetAddresses()
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

21
backend/healthapi/internal/handler/get_allergy_records_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/pkg/response"
)
func GetAllergyRecordsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewGetAllergyRecordsLogic(r.Context(), svcCtx)
resp, err := l.GetAllergyRecords()
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

21
backend/healthapi/internal/handler/get_assessment_history_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/pkg/response"
)
func GetAssessmentHistoryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewGetAssessmentHistoryLogic(r.Context(), svcCtx)
resp, err := l.GetAssessmentHistory()
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

21
backend/healthapi/internal/handler/get_assessment_result_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/pkg/response"
)
func GetAssessmentResultHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewGetAssessmentResultLogic(r.Context(), svcCtx)
resp, err := l.GetAssessmentResult()
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

21
backend/healthapi/internal/handler/get_basic_profile_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/pkg/response"
)
func GetBasicProfileHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewGetBasicProfileLogic(r.Context(), svcCtx)
resp, err := l.GetBasicProfile()
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

21
backend/healthapi/internal/handler/get_cart_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
)
func GetCartHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewGetCartLogic(r.Context(), svcCtx)
resp, err := l.GetCart()
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

21
backend/healthapi/internal/handler/get_categories_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
)
func GetCategoriesHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewGetCategoriesLogic(r.Context(), svcCtx)
resp, err := l.GetCategories()
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

21
backend/healthapi/internal/handler/get_constitution_products_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
)
func GetConstitutionProductsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewGetConstitutionProductsLogic(r.Context(), svcCtx)
resp, err := l.GetConstitutionProducts()
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

29
backend/healthapi/internal/handler/get_conversation_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func GetConversationHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.ConversationIdReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewGetConversationLogic(r.Context(), svcCtx)
resp, err := l.GetConversation(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

21
backend/healthapi/internal/handler/get_conversations_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/pkg/response"
)
func GetConversationsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewGetConversationsLogic(r.Context(), svcCtx)
resp, err := l.GetConversations()
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

21
backend/healthapi/internal/handler/get_family_history_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/pkg/response"
)
func GetFamilyHistoryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewGetFamilyHistoryLogic(r.Context(), svcCtx)
resp, err := l.GetFamilyHistory()
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

28
backend/healthapi/internal/handler/get_featured_products_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func GetFeaturedProductsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.PageReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewGetFeaturedProductsLogic(r.Context(), svcCtx)
resp, err := l.GetFeaturedProducts(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

21
backend/healthapi/internal/handler/get_grouped_questions_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/pkg/response"
)
func GetGroupedQuestionsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewGetGroupedQuestionsLogic(r.Context(), svcCtx)
resp, err := l.GetGroupedQuestions()
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

21
backend/healthapi/internal/handler/get_health_profile_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/pkg/response"
)
func GetHealthProfileHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewGetHealthProfileLogic(r.Context(), svcCtx)
resp, err := l.GetHealthProfile()
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

21
backend/healthapi/internal/handler/get_lifestyle_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/pkg/response"
)
func GetLifestyleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewGetLifestyleLogic(r.Context(), svcCtx)
resp, err := l.GetLifestyle()
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

28
backend/healthapi/internal/handler/get_mall_product_detail_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func GetMallProductDetailHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.IdPathReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewGetMallProductDetailLogic(r.Context(), svcCtx)
resp, err := l.GetMallProductDetail(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

28
backend/healthapi/internal/handler/get_mall_products_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func GetMallProductsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.ProductListReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewGetMallProductsLogic(r.Context(), svcCtx)
resp, err := l.GetMallProducts(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

21
backend/healthapi/internal/handler/get_medical_history_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/pkg/response"
)
func GetMedicalHistoryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewGetMedicalHistoryLogic(r.Context(), svcCtx)
resp, err := l.GetMedicalHistory()
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

21
backend/healthapi/internal/handler/get_member_info_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
)
func GetMemberInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewGetMemberInfoLogic(r.Context(), svcCtx)
resp, err := l.GetMemberInfo()
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

28
backend/healthapi/internal/handler/get_order_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func GetOrderHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.OrderIdReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewGetOrderLogic(r.Context(), svcCtx)
resp, err := l.GetOrder(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

28
backend/healthapi/internal/handler/get_orders_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func GetOrdersHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.OrderListReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewGetOrdersLogic(r.Context(), svcCtx)
resp, err := l.GetOrders(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

28
backend/healthapi/internal/handler/get_points_records_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func GetPointsRecordsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.PageReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewGetPointsRecordsLogic(r.Context(), svcCtx)
resp, err := l.GetPointsRecords(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

29
backend/healthapi/internal/handler/get_product_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func GetProductHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.IdPathReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewGetProductLogic(r.Context(), svcCtx)
resp, err := l.GetProduct(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

29
backend/healthapi/internal/handler/get_product_list_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func GetProductListHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.ProductListReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewGetProductListLogic(r.Context(), svcCtx)
resp, err := l.GetProductList(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

29
backend/healthapi/internal/handler/get_products_by_category_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func GetProductsByCategoryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.ProductListReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewGetProductsByCategoryLogic(r.Context(), svcCtx)
resp, err := l.GetProductsByCategory(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

29
backend/healthapi/internal/handler/get_purchase_history_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func GetPurchaseHistoryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.PageReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewGetPurchaseHistoryLogic(r.Context(), svcCtx)
resp, err := l.GetPurchaseHistory(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

21
backend/healthapi/internal/handler/get_questions_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/pkg/response"
)
func GetQuestionsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewGetQuestionsLogic(r.Context(), svcCtx)
resp, err := l.GetQuestions()
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

21
backend/healthapi/internal/handler/get_recommend_products_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/pkg/response"
)
func GetRecommendProductsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewGetRecommendProductsLogic(r.Context(), svcCtx)
resp, err := l.GetRecommendProducts()
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

21
backend/healthapi/internal/handler/get_recommendations_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/pkg/response"
)
func GetRecommendationsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewGetRecommendationsLogic(r.Context(), svcCtx)
resp, err := l.GetRecommendations()
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

21
backend/healthapi/internal/handler/get_survey_status_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/pkg/response"
)
func GetSurveyStatusHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewGetSurveyStatusLogic(r.Context(), svcCtx)
resp, err := l.GetSurveyStatus()
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

21
backend/healthapi/internal/handler/get_user_profile_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/pkg/response"
)
func GetUserProfileHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewGetUserProfileLogic(r.Context(), svcCtx)
resp, err := l.GetUserProfile()
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

21
backend/healthapi/internal/handler/health_check_handler.go

@ -0,0 +1,21 @@
package handler
import (
"net/http"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/pkg/response"
)
func HealthCheckHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
l := logic.NewHealthCheckLogic(r.Context(), svcCtx)
resp, err := l.HealthCheck()
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

30
backend/healthapi/internal/handler/login_handler.go

@ -0,0 +1,30 @@
package handler
import (
"net/http"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
"github.com/zeromicro/go-zero/rest/httpx"
)
func LoginHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.LoginReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewLoginLogic(r.Context(), svcCtx)
resp, err := l.Login(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

28
backend/healthapi/internal/handler/pay_order_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func PayOrderHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.PayOrderReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewPayOrderLogic(r.Context(), svcCtx)
resp, err := l.PayOrder(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

28
backend/healthapi/internal/handler/preview_order_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func PreviewOrderHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.OrderPreviewReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewPreviewOrderLogic(r.Context(), svcCtx)
resp, err := l.PreviewOrder(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

29
backend/healthapi/internal/handler/refresh_token_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func RefreshTokenHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.RefreshTokenReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewRefreshTokenLogic(r.Context(), svcCtx)
resp, err := l.RefreshToken(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

30
backend/healthapi/internal/handler/register_handler.go

@ -0,0 +1,30 @@
package handler
import (
"net/http"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
"github.com/zeromicro/go-zero/rest/httpx"
)
func RegisterHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.RegisterReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewRegisterLogic(r.Context(), svcCtx)
resp, err := l.Register(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

410
backend/healthapi/internal/handler/routes.go

@ -0,0 +1,410 @@
// Code generated by goctl. DO NOT EDIT.
// goctl 1.8.4
package handler
import (
"net/http"
"healthapi/internal/svc"
"github.com/zeromicro/go-zero/rest"
)
func RegisterHandlers(server *rest.Server, serverCtx *svc.ServiceContext) {
server.AddRoutes(
[]rest.Route{
{
Method: http.MethodPost,
Path: "/auth/login",
Handler: LoginHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/auth/refresh",
Handler: RefreshTokenHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/auth/register",
Handler: RegisterHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/auth/send-code",
Handler: SendCodeHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/health",
Handler: HealthCheckHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/products",
Handler: GetProductListHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/products/:id",
Handler: GetProductHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/products/category",
Handler: GetProductsByCategoryHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/products/search",
Handler: SearchProductsHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/sync/purchase",
Handler: SyncPurchaseHandler(serverCtx),
},
},
rest.WithPrefix("/api"),
)
server.AddRoutes(
[]rest.Route{
{
Method: http.MethodGet,
Path: "/constitution/history",
Handler: GetAssessmentHistoryHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/constitution/questions",
Handler: GetQuestionsHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/constitution/questions/grouped",
Handler: GetGroupedQuestionsHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/constitution/recommendations",
Handler: GetRecommendationsHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/constitution/result",
Handler: GetAssessmentResultHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/constitution/submit",
Handler: SubmitAssessmentHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/conversations",
Handler: GetConversationsHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/conversations",
Handler: CreateConversationHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/conversations/:id",
Handler: GetConversationHandler(serverCtx),
},
{
Method: http.MethodDelete,
Path: "/conversations/:id",
Handler: DeleteConversationHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/conversations/:id/messages",
Handler: SendMessageHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/conversations/:id/messages/stream",
Handler: SendMessageStreamHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/products/recommend",
Handler: GetRecommendProductsHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/survey/allergy",
Handler: SubmitAllergyHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/survey/allergy/batch",
Handler: BatchSubmitAllergyHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/survey/basic-info",
Handler: SubmitBasicInfoHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/survey/complete",
Handler: CompleteSurveyHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/survey/family-history",
Handler: SubmitFamilyHistoryHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/survey/family-history/batch",
Handler: BatchSubmitFamilyHistoryHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/survey/lifestyle",
Handler: SubmitLifestyleHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/survey/medical-history",
Handler: SubmitMedicalHistoryHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/survey/medical-history/batch",
Handler: BatchSubmitMedicalHistoryHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/survey/status",
Handler: GetSurveyStatusHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/user/allergy-records",
Handler: GetAllergyRecordsHandler(serverCtx),
},
{
Method: http.MethodDelete,
Path: "/user/allergy-records/:id",
Handler: DeleteAllergyRecordHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/user/basic-profile",
Handler: GetBasicProfileHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/user/family-history",
Handler: GetFamilyHistoryHandler(serverCtx),
},
{
Method: http.MethodDelete,
Path: "/user/family-history/:id",
Handler: DeleteFamilyHistoryHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/user/health-profile",
Handler: GetHealthProfileHandler(serverCtx),
},
{
Method: http.MethodPut,
Path: "/user/health-profile",
Handler: UpdateHealthProfileHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/user/lifestyle",
Handler: GetLifestyleHandler(serverCtx),
},
{
Method: http.MethodPut,
Path: "/user/lifestyle",
Handler: UpdateLifestyleHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/user/medical-history",
Handler: GetMedicalHistoryHandler(serverCtx),
},
{
Method: http.MethodDelete,
Path: "/user/medical-history/:id",
Handler: DeleteMedicalHistoryHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/user/profile",
Handler: GetUserProfileHandler(serverCtx),
},
{
Method: http.MethodPut,
Path: "/user/profile",
Handler: UpdateUserProfileHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/user/purchase-history",
Handler: GetPurchaseHistoryHandler(serverCtx),
},
},
rest.WithJwt(serverCtx.Config.Auth.AccessSecret),
rest.WithPrefix("/api"),
)
server.AddRoutes(
[]rest.Route{
{
Method: http.MethodGet,
Path: "/categories",
Handler: GetCategoriesHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/products",
Handler: GetMallProductsHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/products/:id",
Handler: GetMallProductDetailHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/products/featured",
Handler: GetFeaturedProductsHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/products/search",
Handler: SearchMallProductsHandler(serverCtx),
},
},
rest.WithPrefix("/api/mall"),
)
server.AddRoutes(
[]rest.Route{
{
Method: http.MethodGet,
Path: "/addresses",
Handler: GetAddressesHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/addresses",
Handler: CreateAddressHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/addresses/:id",
Handler: GetAddressHandler(serverCtx),
},
{
Method: http.MethodPut,
Path: "/addresses/:id",
Handler: UpdateAddressHandler(serverCtx),
},
{
Method: http.MethodDelete,
Path: "/addresses/:id",
Handler: DeleteAddressHandler(serverCtx),
},
{
Method: http.MethodPut,
Path: "/addresses/:id/default",
Handler: SetDefaultAddressHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/cart",
Handler: GetCartHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/cart",
Handler: AddCartHandler(serverCtx),
},
{
Method: http.MethodPut,
Path: "/cart/:id",
Handler: UpdateCartHandler(serverCtx),
},
{
Method: http.MethodDelete,
Path: "/cart/:id",
Handler: DeleteCartHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/cart/batch-select",
Handler: BatchSelectCartHandler(serverCtx),
},
{
Method: http.MethodDelete,
Path: "/cart/clear",
Handler: ClearCartHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/member/info",
Handler: GetMemberInfoHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/member/points/records",
Handler: GetPointsRecordsHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/orders",
Handler: GetOrdersHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/orders",
Handler: CreateOrderHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/orders/:id",
Handler: GetOrderHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/orders/:id/cancel",
Handler: CancelOrderHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/orders/:id/pay",
Handler: PayOrderHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/orders/:id/receive",
Handler: ConfirmReceiveHandler(serverCtx),
},
{
Method: http.MethodPost,
Path: "/orders/preview",
Handler: PreviewOrderHandler(serverCtx),
},
{
Method: http.MethodGet,
Path: "/products/constitution-recommend",
Handler: GetConstitutionProductsHandler(serverCtx),
},
},
rest.WithJwt(serverCtx.Config.Auth.AccessSecret),
rest.WithPrefix("/api/mall"),
)
}

28
backend/healthapi/internal/handler/search_mall_products_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func SearchMallProductsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.ProductSearchReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewSearchMallProductsLogic(r.Context(), svcCtx)
resp, err := l.SearchMallProducts(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

29
backend/healthapi/internal/handler/search_products_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func SearchProductsHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.ProductSearchReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewSearchProductsLogic(r.Context(), svcCtx)
resp, err := l.SearchProducts(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

30
backend/healthapi/internal/handler/send_code_handler.go

@ -0,0 +1,30 @@
package handler
import (
"net/http"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
"github.com/zeromicro/go-zero/rest/httpx"
)
func SendCodeHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SendCodeReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewSendCodeLogic(r.Context(), svcCtx)
resp, err := l.SendCode(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

29
backend/healthapi/internal/handler/send_message_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func SendMessageHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SendMessageReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewSendMessageLogic(r.Context(), svcCtx)
resp, err := l.SendMessage(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

311
backend/healthapi/internal/handler/send_message_stream_handler.go

@ -0,0 +1,311 @@
package handler
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
"healthapi/internal/logic"
"healthapi/internal/model"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/ai"
"healthapi/pkg/errorx"
"github.com/zeromicro/go-zero/rest/httpx"
)
func SendMessageStreamHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SendMessageReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
// 获取用户 ID
userID, err := logic.GetUserIDFromCtx(r.Context())
if err != nil {
httpx.ErrorCtx(r.Context(), w, errorx.ErrUnauthorized)
return
}
// 验证对话属于该用户
var conversation model.Conversation
if err := svcCtx.DB.Where("id = ? AND user_id = ?", req.Id, userID).First(&conversation).Error; err != nil {
httpx.ErrorCtx(r.Context(), w, errorx.ErrNotFound)
return
}
// 保存用户消息
userMessage := model.Message{
ConversationID: conversation.ID,
Role: model.RoleUser,
Content: req.Content,
}
if err := svcCtx.DB.Create(&userMessage).Error; err != nil {
httpx.ErrorCtx(r.Context(), w, errorx.ErrServerError)
return
}
// 设置 SSE 响应头(与原 server 保持一致)
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("X-Accel-Buffering", "no") // 禁用 nginx 缓冲
w.Header().Set("Access-Control-Allow-Origin", "*")
// 发送用户消息 ID
msgData, _ := json.Marshal(map[string]interface{}{
"type": "user_message",
"message_id": userMessage.ID,
})
fmt.Fprintf(w, "data: %s\n\n", msgData)
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
// 获取历史消息
var historyMessages []model.Message
svcCtx.DB.Where("conversation_id = ?", conversation.ID).
Order("created_at DESC").
Limit(svcCtx.Config.AI.MaxHistoryMessages).
Find(&historyMessages)
// 构建系统提示
systemPrompt := buildSystemPromptForStream(svcCtx, userID)
// 构建 AI 消息
aiMessages := []ai.Message{{Role: "system", Content: systemPrompt}}
for i := len(historyMessages) - 1; i >= 0; i-- {
aiMessages = append(aiMessages, ai.Message{
Role: historyMessages[i].Role,
Content: historyMessages[i].Content,
})
}
// 创建收集器
collector := &responseCollector{writer: w}
// 调用 AI 流式服务
err = svcCtx.AIClient.ChatStream(r.Context(), aiMessages, collector)
if err != nil {
errData, _ := json.Marshal(map[string]interface{}{
"type": "error",
"error": err.Error(),
})
fmt.Fprintf(w, "data: %s\n\n", errData)
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
return
}
// 保存 AI 回复
assistantMessage := model.Message{
ConversationID: conversation.ID,
Role: model.RoleAssistant,
Content: collector.content,
}
svcCtx.DB.Create(&assistantMessage)
// 更新对话标题
if conversation.Title == "新对话" {
title := req.Content
if len(title) > 50 {
title = title[:50] + "..."
}
svcCtx.DB.Model(&conversation).Update("title", title)
}
// 发送完成消息(使用 "end" 类型,与原 server 和前端保持一致)
endData, _ := json.Marshal(map[string]interface{}{
"type": "end",
"message_id": assistantMessage.ID,
})
fmt.Fprintf(w, "data: %s\n\n", endData)
if flusher, ok := w.(http.Flusher); ok {
flusher.Flush()
}
}
}
// responseCollector 收集响应内容(使用缓冲区按行解析,与原 server 一致)
type responseCollector struct {
writer http.ResponseWriter
content string
buffer string
}
func (c *responseCollector) Write(p []byte) (n int, err error) {
// 累积数据到 buffer
c.buffer += string(p)
// 按行处理
for {
idx := strings.Index(c.buffer, "\n")
if idx == -1 {
break
}
line := c.buffer[:idx]
c.buffer = c.buffer[idx+1:]
// 解析 SSE 数据提取内容
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "data: ") {
jsonStr := strings.TrimPrefix(line, "data: ")
jsonStr = strings.TrimSpace(jsonStr)
if jsonStr != "" && jsonStr != "[DONE]" {
var data struct {
Type string `json:"type"`
Content string `json:"content"`
}
if err := json.Unmarshal([]byte(jsonStr), &data); err == nil {
if data.Type == "content" {
c.content += data.Content
}
}
}
}
}
// 同时写入原始 writer
return c.writer.Write(p)
}
// 系统提示词模板(与原 server 保持一致)
const systemPromptTemplate = `# 用户相关信息
## 用户信息
%s
## 用户体质
%s
## 用户病史
%s
## 用户家族病史
%s
## 用户过敏记录
%s
`
func buildSystemPromptForStream(svcCtx *svc.ServiceContext, userID uint) string {
var userProfile, constitutionInfo, medicalInfo, familyInfo, allergyInfo string
// 获取用户健康档案
var profile model.HealthProfile
if err := svcCtx.DB.Where("user_id = ?", userID).First(&profile).Error; err == nil && profile.ID > 0 {
// 基本信息
age := calculateAge(profile.BirthDate)
gender := "未知"
if profile.Gender == "male" {
gender = "男"
} else if profile.Gender == "female" {
gender = "女"
}
bmi := float64(0)
if profile.Height > 0 && profile.Weight > 0 {
heightM := float64(profile.Height) / 100
bmi = float64(profile.Weight) / (heightM * heightM)
}
userProfile = fmt.Sprintf("性别:%s,年龄:%d岁,BMI:%.1f", gender, age, bmi)
// 获取病史记录
var medicalHistories []model.MedicalHistory
svcCtx.DB.Where("health_profile_id = ?", profile.ID).Find(&medicalHistories)
if len(medicalHistories) > 0 {
var items []string
for _, h := range medicalHistories {
status := "治疗中"
if h.Status == "cured" {
status = "已治愈"
} else if h.Status == "controlled" {
status = "已控制"
}
items = append(items, fmt.Sprintf("- %s(%s,%s)", h.DiseaseName, h.DiagnosedDate, status))
}
medicalInfo = fmt.Sprintf("共%d条记录:\n%s", len(medicalHistories), strings.Join(items, "\n"))
} else {
medicalInfo = "暂无病史记录"
}
// 获取家族病史
var familyHistories []model.FamilyHistory
svcCtx.DB.Where("health_profile_id = ?", profile.ID).Find(&familyHistories)
if len(familyHistories) > 0 {
var items []string
for _, h := range familyHistories {
relation := h.Relation
switch relation {
case "father":
relation = "父亲"
case "mother":
relation = "母亲"
case "grandparent":
relation = "祖父母"
case "sibling":
relation = "兄弟姐妹"
}
items = append(items, fmt.Sprintf("- %s:%s", relation, h.DiseaseName))
}
familyInfo = fmt.Sprintf("共%d条记录:\n%s", len(familyHistories), strings.Join(items, "\n"))
} else {
familyInfo = "暂无家族病史"
}
// 获取过敏记录
var allergyRecords []model.AllergyRecord
svcCtx.DB.Where("health_profile_id = ?", profile.ID).Find(&allergyRecords)
if len(allergyRecords) > 0 {
var items []string
for _, r := range allergyRecords {
severity := "轻度"
if r.Severity == "moderate" {
severity = "中度"
} else if r.Severity == "severe" {
severity = "重度"
}
items = append(items, fmt.Sprintf("- %s(%s,%s)", r.Allergen, r.AllergyType, severity))
}
allergyInfo = fmt.Sprintf("共%d条记录:\n%s", len(allergyRecords), strings.Join(items, "\n"))
} else {
allergyInfo = "暂无过敏记录"
}
} else {
userProfile = "暂无基本信息"
medicalInfo = "暂无病史记录"
familyInfo = "暂无家族病史"
allergyInfo = "暂无过敏记录"
}
// 获取用户体质信息
var assessment model.ConstitutionAssessment
if err := svcCtx.DB.Where("user_id = ?", userID).Order("assessed_at DESC").First(&assessment).Error; err == nil && assessment.ID > 0 {
constitutionName := model.ConstitutionNames[assessment.PrimaryConstitution]
description := model.ConstitutionDescriptions[assessment.PrimaryConstitution]
constitutionInfo = fmt.Sprintf("主体质:%s\n特征:%s", constitutionName, description)
} else {
constitutionInfo = "暂未进行体质测评"
}
return fmt.Sprintf(systemPromptTemplate, userProfile, constitutionInfo, medicalInfo, familyInfo, allergyInfo)
}
// calculateAge 计算年龄
func calculateAge(birthDate *time.Time) int {
if birthDate == nil {
return 0
}
now := time.Now()
age := now.Year() - birthDate.Year()
if now.YearDay() < birthDate.YearDay() {
age--
}
return age
}

28
backend/healthapi/internal/handler/set_default_address_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func SetDefaultAddressHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AddressIdReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewSetDefaultAddressLogic(r.Context(), svcCtx)
resp, err := l.SetDefaultAddress(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

29
backend/healthapi/internal/handler/submit_allergy_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func SubmitAllergyHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SubmitAllergyReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewSubmitAllergyLogic(r.Context(), svcCtx)
resp, err := l.SubmitAllergy(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

29
backend/healthapi/internal/handler/submit_assessment_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func SubmitAssessmentHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SubmitAssessmentReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewSubmitAssessmentLogic(r.Context(), svcCtx)
resp, err := l.SubmitAssessment(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

29
backend/healthapi/internal/handler/submit_basic_info_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func SubmitBasicInfoHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SubmitBasicInfoReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewSubmitBasicInfoLogic(r.Context(), svcCtx)
resp, err := l.SubmitBasicInfo(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

29
backend/healthapi/internal/handler/submit_family_history_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func SubmitFamilyHistoryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SubmitFamilyHistoryReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewSubmitFamilyHistoryLogic(r.Context(), svcCtx)
resp, err := l.SubmitFamilyHistory(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

29
backend/healthapi/internal/handler/submit_lifestyle_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func SubmitLifestyleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SubmitLifestyleReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewSubmitLifestyleLogic(r.Context(), svcCtx)
resp, err := l.SubmitLifestyle(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

29
backend/healthapi/internal/handler/submit_medical_history_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func SubmitMedicalHistoryHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SubmitMedicalHistoryReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewSubmitMedicalHistoryLogic(r.Context(), svcCtx)
resp, err := l.SubmitMedicalHistory(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

29
backend/healthapi/internal/handler/sync_purchase_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func SyncPurchaseHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.SyncPurchaseReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewSyncPurchaseLogic(r.Context(), svcCtx)
resp, err := l.SyncPurchase(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

28
backend/healthapi/internal/handler/update_address_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func UpdateAddressHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.AddressIdReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewUpdateAddressLogic(r.Context(), svcCtx)
err := l.UpdateAddress(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.Ok(w)
}
}
}

28
backend/healthapi/internal/handler/update_cart_handler.go

@ -0,0 +1,28 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
)
func UpdateCartHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.UpdateCartReq
if err := httpx.Parse(r, &req); err != nil {
httpx.ErrorCtx(r.Context(), w, err)
return
}
l := logic.NewUpdateCartLogic(r.Context(), svcCtx)
resp, err := l.UpdateCart(&req)
if err != nil {
httpx.ErrorCtx(r.Context(), w, err)
} else {
httpx.OkJsonCtx(r.Context(), w, resp)
}
}
}

29
backend/healthapi/internal/handler/update_health_profile_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func UpdateHealthProfileHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.UpdateHealthProfileReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewUpdateHealthProfileLogic(r.Context(), svcCtx)
resp, err := l.UpdateHealthProfile(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

29
backend/healthapi/internal/handler/update_lifestyle_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func UpdateLifestyleHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.UpdateLifestyleReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewUpdateLifestyleLogic(r.Context(), svcCtx)
resp, err := l.UpdateLifestyle(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

29
backend/healthapi/internal/handler/update_user_profile_handler.go

@ -0,0 +1,29 @@
package handler
import (
"net/http"
"github.com/zeromicro/go-zero/rest/httpx"
"healthapi/internal/logic"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/response"
)
func UpdateUserProfileHandler(svcCtx *svc.ServiceContext) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var req types.UpdateProfileReq
if err := httpx.Parse(r, &req); err != nil {
response.Error(w, err)
return
}
l := logic.NewUpdateUserProfileLogic(r.Context(), svcCtx)
resp, err := l.UpdateUserProfile(&req)
if err != nil {
response.Error(w, err)
} else {
response.Success(w, resp)
}
}
}

135
backend/healthapi/internal/logic/add_cart_logic.go

@ -0,0 +1,135 @@
package logic
import (
"context"
"errors"
"healthapi/internal/model"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/errorx"
"github.com/zeromicro/go-zero/core/logx"
"gorm.io/gorm"
)
type AddCartLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewAddCartLogic(ctx context.Context, svcCtx *svc.ServiceContext) *AddCartLogic {
return &AddCartLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *AddCartLogic) AddCart(req *types.AddCartReq) (resp *types.CartItem, err error) {
userID, err := GetUserIDFromCtx(l.ctx)
if err != nil {
return nil, errorx.ErrUnauthorized
}
// 验证商品存在且上架
var product model.Product
if err := l.svcCtx.DB.First(&product, req.ProductID).Error; err != nil {
return nil, errorx.NewCodeError(404, "商品不存在")
}
if !product.IsActive {
return nil, errorx.NewCodeError(400, "商品已下架")
}
price := product.Price
stock := product.Stock
skuName := ""
image := product.MainImage
// 如果有 SKU,验证 SKU
if req.SkuID > 0 {
var sku model.ProductSku
if err := l.svcCtx.DB.First(&sku, req.SkuID).Error; err != nil {
return nil, errorx.NewCodeError(404, "商品规格不存在")
}
if sku.ProductID != req.ProductID {
return nil, errorx.NewCodeError(400, "商品规格不匹配")
}
if !sku.IsActive {
return nil, errorx.NewCodeError(400, "该规格已下架")
}
price = sku.Price
stock = sku.Stock
skuName = sku.Name
if sku.Image != "" {
image = sku.Image
}
}
// 检查库存
if stock < req.Quantity {
return nil, errorx.NewCodeError(400, "库存不足")
}
// 检查是否已在购物车中
var existingItem model.CartItem
query := l.svcCtx.DB.Where("user_id = ? AND product_id = ?", userID, req.ProductID)
if req.SkuID > 0 {
query = query.Where("sku_id = ?", req.SkuID)
} else {
query = query.Where("sku_id = 0 OR sku_id IS NULL")
}
if err := query.First(&existingItem).Error; err == nil {
// 已存在,更新数量
newQty := existingItem.Quantity + req.Quantity
if newQty > stock {
return nil, errorx.NewCodeError(400, "库存不足")
}
existingItem.Quantity = newQty
l.svcCtx.DB.Save(&existingItem)
resp = &types.CartItem{
ID: uint(existingItem.ID),
ProductID: existingItem.ProductID,
SkuID: existingItem.SkuID,
ProductName: product.Name,
SkuName: skuName,
Image: image,
Price: price,
Quantity: existingItem.Quantity,
Selected: existingItem.Selected,
Stock: stock,
}
return resp, nil
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
// 新增购物车项
cartItem := model.CartItem{
UserID: userID,
ProductID: req.ProductID,
SkuID: req.SkuID,
Quantity: req.Quantity,
Selected: true,
}
if err := l.svcCtx.DB.Create(&cartItem).Error; err != nil {
return nil, err
}
resp = &types.CartItem{
ID: uint(cartItem.ID),
ProductID: cartItem.ProductID,
SkuID: cartItem.SkuID,
ProductName: product.Name,
SkuName: skuName,
Image: image,
Price: price,
Quantity: cartItem.Quantity,
Selected: cartItem.Selected,
Stock: stock,
}
return resp, nil
}

48
backend/healthapi/internal/logic/batch_select_cart_logic.go

@ -0,0 +1,48 @@
package logic
import (
"context"
"healthapi/internal/model"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/errorx"
"github.com/zeromicro/go-zero/core/logx"
)
type BatchSelectCartLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewBatchSelectCartLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BatchSelectCartLogic {
return &BatchSelectCartLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *BatchSelectCartLogic) BatchSelectCart(req *types.BatchSelectCartReq) (resp *types.CommonResp, err error) {
userID, err := GetUserIDFromCtx(l.ctx)
if err != nil {
return nil, errorx.ErrUnauthorized
}
if len(req.Ids) == 0 {
// 全选/全不选
l.svcCtx.DB.Model(&model.CartItem{}).Where("user_id = ?", userID).Update("selected", req.Selected)
} else {
// 批量选择指定项
l.svcCtx.DB.Model(&model.CartItem{}).
Where("user_id = ? AND id IN ?", userID, req.Ids).
Update("selected", req.Selected)
}
return &types.CommonResp{
Code: 0,
Message: "操作成功",
}, nil
}

60
backend/healthapi/internal/logic/batch_submit_allergy_logic.go

@ -0,0 +1,60 @@
package logic
import (
"context"
"healthapi/internal/model"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/errorx"
"github.com/zeromicro/go-zero/core/logx"
)
type BatchSubmitAllergyLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewBatchSubmitAllergyLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BatchSubmitAllergyLogic {
return &BatchSubmitAllergyLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *BatchSubmitAllergyLogic) BatchSubmitAllergy(req *types.BatchAllergyReq) (resp *types.CommonResp, err error) {
userID, err := GetUserIDFromCtx(l.ctx)
if err != nil {
return nil, errorx.ErrUnauthorized
}
var profile model.HealthProfile
if err := l.svcCtx.DB.Where("user_id = ?", userID).First(&profile).Error; err != nil {
return nil, errorx.NewCodeError(errorx.CodeBadRequest, "请先填写基础信息")
}
// 清除旧数据
l.svcCtx.DB.Where("health_profile_id = ?", profile.ID).Delete(&model.AllergyRecord{})
// 创建新数据
if len(req.Items) > 0 {
records := make([]model.AllergyRecord, len(req.Items))
for i, a := range req.Items {
records[i] = model.AllergyRecord{
HealthProfileID: profile.ID,
AllergyType: a.AllergyType,
Allergen: a.Allergen,
Severity: a.Severity,
ReactionDesc: a.ReactionDesc,
}
}
if err := l.svcCtx.DB.Create(&records).Error; err != nil {
return nil, errorx.ErrServerError
}
}
return &types.CommonResp{Code: 0, Message: "提交成功"}, nil
}

59
backend/healthapi/internal/logic/batch_submit_family_history_logic.go

@ -0,0 +1,59 @@
package logic
import (
"context"
"healthapi/internal/model"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/errorx"
"github.com/zeromicro/go-zero/core/logx"
)
type BatchSubmitFamilyHistoryLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewBatchSubmitFamilyHistoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BatchSubmitFamilyHistoryLogic {
return &BatchSubmitFamilyHistoryLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *BatchSubmitFamilyHistoryLogic) BatchSubmitFamilyHistory(req *types.BatchFamilyHistoryReq) (resp *types.CommonResp, err error) {
userID, err := GetUserIDFromCtx(l.ctx)
if err != nil {
return nil, errorx.ErrUnauthorized
}
var profile model.HealthProfile
if err := l.svcCtx.DB.Where("user_id = ?", userID).First(&profile).Error; err != nil {
return nil, errorx.NewCodeError(errorx.CodeBadRequest, "请先填写基础信息")
}
// 清除旧数据
l.svcCtx.DB.Where("health_profile_id = ?", profile.ID).Delete(&model.FamilyHistory{})
// 创建新数据
if len(req.Items) > 0 {
histories := make([]model.FamilyHistory, len(req.Items))
for i, h := range req.Items {
histories[i] = model.FamilyHistory{
HealthProfileID: profile.ID,
Relation: h.Relation,
DiseaseName: h.DiseaseName,
Notes: h.Notes,
}
}
if err := l.svcCtx.DB.Create(&histories).Error; err != nil {
return nil, errorx.ErrServerError
}
}
return &types.CommonResp{Code: 0, Message: "提交成功"}, nil
}

61
backend/healthapi/internal/logic/batch_submit_medical_history_logic.go

@ -0,0 +1,61 @@
package logic
import (
"context"
"healthapi/internal/model"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/errorx"
"github.com/zeromicro/go-zero/core/logx"
)
type BatchSubmitMedicalHistoryLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewBatchSubmitMedicalHistoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BatchSubmitMedicalHistoryLogic {
return &BatchSubmitMedicalHistoryLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *BatchSubmitMedicalHistoryLogic) BatchSubmitMedicalHistory(req *types.BatchMedicalHistoryReq) (resp *types.CommonResp, err error) {
userID, err := GetUserIDFromCtx(l.ctx)
if err != nil {
return nil, errorx.ErrUnauthorized
}
var profile model.HealthProfile
if err := l.svcCtx.DB.Where("user_id = ?", userID).First(&profile).Error; err != nil {
return nil, errorx.NewCodeError(errorx.CodeBadRequest, "请先填写基础信息")
}
// 清除旧数据
l.svcCtx.DB.Where("health_profile_id = ?", profile.ID).Delete(&model.MedicalHistory{})
// 创建新数据
if len(req.Items) > 0 {
histories := make([]model.MedicalHistory, len(req.Items))
for i, h := range req.Items {
histories[i] = model.MedicalHistory{
HealthProfileID: profile.ID,
DiseaseName: h.DiseaseName,
DiseaseType: h.DiseaseType,
DiagnosedDate: h.DiagnosedDate,
Status: h.Status,
Notes: h.Notes,
}
}
if err := l.svcCtx.DB.Create(&histories).Error; err != nil {
return nil, errorx.ErrServerError
}
}
return &types.CommonResp{Code: 0, Message: "提交成功"}, nil
}

94
backend/healthapi/internal/logic/cancel_order_logic.go

@ -0,0 +1,94 @@
package logic
import (
"context"
"fmt"
"healthapi/internal/model"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/errorx"
"github.com/zeromicro/go-zero/core/logx"
"gorm.io/gorm"
)
type CancelOrderLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewCancelOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CancelOrderLogic {
return &CancelOrderLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *CancelOrderLogic) CancelOrder(req *types.CancelOrderReq) (resp *types.CommonResp, err error) {
userID, err := GetUserIDFromCtx(l.ctx)
if err != nil {
return nil, errorx.ErrUnauthorized
}
var order model.Order
if err := l.svcCtx.DB.Where("id = ? AND user_id = ?", req.Id, userID).First(&order).Error; err != nil {
return nil, errorx.NewCodeError(404, "订单不存在")
}
if order.Status != model.OrderStatusPending {
return nil, errorx.NewCodeError(400, "只能取消待支付订单")
}
err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error {
// 更新订单状态
if err := tx.Model(&order).Updates(map[string]interface{}{
"status": model.OrderStatusCancelled,
"cancel_reason": req.Reason,
}).Error; err != nil {
return err
}
// 恢复库存
var items []model.OrderItem
tx.Where("order_id = ?", order.ID).Find(&items)
for _, item := range items {
if item.SkuID > 0 {
tx.Model(&model.ProductSku{}).Where("id = ?", item.SkuID).
Update("stock", gorm.Expr("stock + ?", item.Quantity))
} else {
tx.Model(&model.Product{}).Where("id = ?", item.ProductID).
Update("stock", gorm.Expr("stock + ?", item.Quantity))
}
}
// 退还积分
if order.PointsUsed > 0 {
var user model.User
tx.First(&user, userID)
tx.Model(&user).Update("points", gorm.Expr("points + ?", order.PointsUsed))
tx.Create(&model.PointsRecord{
UserID: userID,
Type: model.PointsTypeAdjust,
Points: order.PointsUsed,
Balance: user.Points + order.PointsUsed,
Source: model.PointsSourceRefund,
ReferenceID: uint(order.ID),
Remark: fmt.Sprintf("订单 %s 取消,退还积分", order.OrderNo),
})
}
return nil
})
if err != nil {
return nil, err
}
return &types.CommonResp{
Code: 0,
Message: "订单已取消",
}, nil
}

40
backend/healthapi/internal/logic/clear_cart_logic.go

@ -0,0 +1,40 @@
package logic
import (
"context"
"healthapi/internal/model"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/errorx"
"github.com/zeromicro/go-zero/core/logx"
)
type ClearCartLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewClearCartLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ClearCartLogic {
return &ClearCartLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ClearCartLogic) ClearCart() (resp *types.CommonResp, err error) {
userID, err := GetUserIDFromCtx(l.ctx)
if err != nil {
return nil, errorx.ErrUnauthorized
}
l.svcCtx.DB.Where("user_id = ?", userID).Delete(&model.CartItem{})
return &types.CommonResp{
Code: 0,
Message: "购物车已清空",
}, nil
}

52
backend/healthapi/internal/logic/complete_survey_logic.go

@ -0,0 +1,52 @@
package logic
import (
"context"
"healthapi/internal/model"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/errorx"
"github.com/zeromicro/go-zero/core/logx"
)
type CompleteSurveyLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewCompleteSurveyLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CompleteSurveyLogic {
return &CompleteSurveyLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *CompleteSurveyLogic) CompleteSurvey() (resp *types.CommonResp, err error) {
userID, err := GetUserIDFromCtx(l.ctx)
if err != nil {
return nil, errorx.ErrUnauthorized
}
// 检查基础信息
var profile model.HealthProfile
if err := l.svcCtx.DB.Where("user_id = ?", userID).First(&profile).Error; err != nil {
return nil, errorx.NewCodeError(errorx.CodeBadRequest, "请先完成基础信息填写")
}
// 检查生活习惯
var lifestyle model.LifestyleInfo
if err := l.svcCtx.DB.Where("user_id = ?", userID).First(&lifestyle).Error; err != nil {
return nil, errorx.NewCodeError(errorx.CodeBadRequest, "请先完成生活习惯填写")
}
// 标记调查完成
if err := l.svcCtx.DB.Model(&model.User{}).Where("id = ?", userID).Update("survey_completed", true).Error; err != nil {
return nil, errorx.ErrServerError
}
return &types.CommonResp{Code: 0, Message: "调查完成"}, nil
}

56
backend/healthapi/internal/logic/confirm_receive_logic.go

@ -0,0 +1,56 @@
package logic
import (
"context"
"time"
"healthapi/internal/model"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/errorx"
"github.com/zeromicro/go-zero/core/logx"
)
type ConfirmReceiveLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewConfirmReceiveLogic(ctx context.Context, svcCtx *svc.ServiceContext) *ConfirmReceiveLogic {
return &ConfirmReceiveLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *ConfirmReceiveLogic) ConfirmReceive(req *types.OrderIdReq) (resp *types.CommonResp, err error) {
userID, err := GetUserIDFromCtx(l.ctx)
if err != nil {
return nil, errorx.ErrUnauthorized
}
var order model.Order
if err := l.svcCtx.DB.Where("id = ? AND user_id = ?", req.Id, userID).First(&order).Error; err != nil {
return nil, errorx.NewCodeError(404, "订单不存在")
}
if order.Status != model.OrderStatusShipped {
return nil, errorx.NewCodeError(400, "只能确认已发货订单")
}
now := time.Now()
if err := l.svcCtx.DB.Model(&order).Updates(map[string]interface{}{
"status": model.OrderStatusCompleted,
"receive_time": now,
}).Error; err != nil {
return nil, err
}
return &types.CommonResp{
Code: 0,
Message: "确认收货成功",
}, nil
}

76
backend/healthapi/internal/logic/create_address_logic.go

@ -0,0 +1,76 @@
package logic
import (
"context"
"healthapi/internal/model"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/errorx"
"github.com/zeromicro/go-zero/core/logx"
)
type CreateAddressLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewCreateAddressLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateAddressLogic {
return &CreateAddressLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *CreateAddressLogic) CreateAddress(req *types.SaveAddressReq) (resp *types.Address, err error) {
userID, err := GetUserIDFromCtx(l.ctx)
if err != nil {
return nil, errorx.ErrUnauthorized
}
// 如果设为默认,先取消其他默认地址
if req.IsDefault {
l.svcCtx.DB.Model(&model.Address{}).Where("user_id = ?", userID).Update("is_default", false)
}
// 检查是否是第一个地址,如果是则自动设为默认
var count int64
l.svcCtx.DB.Model(&model.Address{}).Where("user_id = ?", userID).Count(&count)
if count == 0 {
req.IsDefault = true
}
addr := model.Address{
UserID: userID,
ReceiverName: req.ReceiverName,
Phone: req.Phone,
Province: req.Province,
City: req.City,
District: req.District,
DetailAddr: req.DetailAddr,
PostalCode: req.PostalCode,
IsDefault: req.IsDefault,
Tag: req.Tag,
}
if err := l.svcCtx.DB.Create(&addr).Error; err != nil {
return nil, err
}
resp = &types.Address{
ID: uint(addr.ID),
ReceiverName: addr.ReceiverName,
Phone: addr.Phone,
Province: addr.Province,
City: addr.City,
District: addr.District,
DetailAddr: addr.DetailAddr,
PostalCode: addr.PostalCode,
IsDefault: addr.IsDefault,
Tag: addr.Tag,
}
return resp, nil
}

54
backend/healthapi/internal/logic/create_conversation_logic.go

@ -0,0 +1,54 @@
package logic
import (
"context"
"healthapi/internal/model"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/errorx"
"github.com/zeromicro/go-zero/core/logx"
)
type CreateConversationLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewCreateConversationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateConversationLogic {
return &CreateConversationLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *CreateConversationLogic) CreateConversation(req *types.CreateConversationReq) (resp *types.ConversationItem, err error) {
userID, err := GetUserIDFromCtx(l.ctx)
if err != nil {
return nil, errorx.ErrUnauthorized
}
title := req.Title
if title == "" {
title = "新对话"
}
conversation := model.Conversation{
UserID: userID,
Title: title,
}
if err := l.svcCtx.DB.Create(&conversation).Error; err != nil {
return nil, errorx.ErrServerError
}
return &types.ConversationItem{
ID: uint(conversation.ID),
Title: conversation.Title,
CreatedAt: conversation.CreatedAt.Format("2006-01-02 15:04:05"),
UpdatedAt: conversation.UpdatedAt.Format("2006-01-02 15:04:05"),
}, nil
}

256
backend/healthapi/internal/logic/create_order_logic.go

@ -0,0 +1,256 @@
package logic
import (
"context"
"fmt"
"time"
"healthapi/internal/model"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/errorx"
"github.com/zeromicro/go-zero/core/logx"
"gorm.io/gorm"
)
type CreateOrderLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewCreateOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateOrderLogic {
return &CreateOrderLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *CreateOrderLogic) CreateOrder(req *types.CreateOrderReq) (resp *types.Order, err error) {
userID, err := GetUserIDFromCtx(l.ctx)
if err != nil {
return nil, errorx.ErrUnauthorized
}
// 获取用户信息(会员等级)
var user model.User
if err := l.svcCtx.DB.First(&user, userID).Error; err != nil {
return nil, errorx.ErrUserNotFound
}
levelConfig := model.MemberLevelConfigs[user.MemberLevel]
if levelConfig.Level == "" {
levelConfig = model.MemberLevelConfigs[model.MemberLevelNormal]
}
// 验证收货地址
var address model.Address
if err := l.svcCtx.DB.Where("id = ? AND user_id = ?", req.AddressID, userID).First(&address).Error; err != nil {
return nil, errorx.NewCodeError(404, "收货地址不存在")
}
// 获取购物车项
var cartItems []model.CartItem
if err := l.svcCtx.DB.Where("id IN ? AND user_id = ?", req.CartItemIDs, userID).Find(&cartItems).Error; err != nil {
return nil, err
}
if len(cartItems) == 0 {
return nil, errorx.NewCodeError(400, "购物车为空")
}
// 计算订单金额
var totalAmount float64
orderItems := make([]model.OrderItem, 0, len(cartItems))
for _, cartItem := range cartItems {
var product model.Product
if err := l.svcCtx.DB.First(&product, cartItem.ProductID).Error; err != nil {
return nil, errorx.NewCodeError(404, fmt.Sprintf("商品 %d 不存在", cartItem.ProductID))
}
if !product.IsActive {
return nil, errorx.NewCodeError(400, fmt.Sprintf("商品 %s 已下架", product.Name))
}
price := product.Price
stock := product.Stock
skuName := ""
image := product.MainImage
if cartItem.SkuID > 0 {
var sku model.ProductSku
if err := l.svcCtx.DB.First(&sku, cartItem.SkuID).Error; err != nil {
return nil, errorx.NewCodeError(404, "商品规格不存在")
}
price = sku.Price
stock = sku.Stock
skuName = sku.Name
if sku.Image != "" {
image = sku.Image
}
}
if stock < cartItem.Quantity {
return nil, errorx.NewCodeError(400, fmt.Sprintf("商品 %s 库存不足", product.Name))
}
itemTotal := price * float64(cartItem.Quantity)
totalAmount += itemTotal
orderItems = append(orderItems, model.OrderItem{
ProductID: cartItem.ProductID,
SkuID: cartItem.SkuID,
ProductName: product.Name,
SkuName: skuName,
Image: image,
Price: price,
Quantity: cartItem.Quantity,
TotalAmount: itemTotal,
})
}
// 计算会员折扣
discountAmount := totalAmount * (1 - levelConfig.Discount)
// 计算运费
shippingFee := float64(0)
afterDiscount := totalAmount - discountAmount
if afterDiscount < levelConfig.FreeShippingMin {
shippingFee = 10 // 默认运费
}
// 计算积分抵扣(100积分=1元)
pointsDiscount := float64(0)
pointsUsed := req.PointsUsed
if pointsUsed > 0 {
if pointsUsed > user.Points {
pointsUsed = user.Points
}
maxPointsDiscount := afterDiscount * 0.2 // 最多抵扣20%
pointsDiscount = float64(pointsUsed) / 100
if pointsDiscount > maxPointsDiscount {
pointsDiscount = maxPointsDiscount
pointsUsed = int(maxPointsDiscount * 100)
}
}
// 最终支付金额
payAmount := afterDiscount + shippingFee - pointsDiscount
// 生成订单号
orderNo := fmt.Sprintf("%s%d%04d", time.Now().Format("20060102150405"), userID, time.Now().Nanosecond()%10000)
// 计算获得积分
pointsEarned := int(payAmount * levelConfig.PointsMultiplier)
// 创建订单(使用事务)
order := model.Order{
UserID: userID,
OrderNo: orderNo,
Status: model.OrderStatusPending,
TotalAmount: totalAmount,
DiscountAmount: discountAmount,
ShippingFee: shippingFee,
PayAmount: payAmount,
PointsUsed: pointsUsed,
PointsEarned: pointsEarned,
ReceiverName: address.ReceiverName,
ReceiverPhone: address.Phone,
ReceiverAddr: fmt.Sprintf("%s%s%s%s", address.Province, address.City, address.District, address.DetailAddr),
Remark: req.Remark,
}
err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error {
// 创建订单
if err := tx.Create(&order).Error; err != nil {
return err
}
// 创建订单项
for i := range orderItems {
orderItems[i].OrderID = uint(order.ID)
}
if err := tx.Create(&orderItems).Error; err != nil {
return err
}
// 扣减库存
for _, cartItem := range cartItems {
if cartItem.SkuID > 0 {
if err := tx.Model(&model.ProductSku{}).Where("id = ?", cartItem.SkuID).
Update("stock", gorm.Expr("stock - ?", cartItem.Quantity)).Error; err != nil {
return err
}
} else {
if err := tx.Model(&model.Product{}).Where("id = ?", cartItem.ProductID).
Update("stock", gorm.Expr("stock - ?", cartItem.Quantity)).Error; err != nil {
return err
}
}
}
// 删除购物车项
if err := tx.Where("id IN ?", req.CartItemIDs).Delete(&model.CartItem{}).Error; err != nil {
return err
}
// 扣除积分
if pointsUsed > 0 {
if err := tx.Model(&user).Update("points", gorm.Expr("points - ?", pointsUsed)).Error; err != nil {
return err
}
// 记录积分变动
tx.Create(&model.PointsRecord{
UserID: userID,
Type: model.PointsTypeSpend,
Points: -pointsUsed,
Balance: user.Points - pointsUsed,
Source: model.PointsSourceOrder,
ReferenceID: uint(order.ID),
Remark: fmt.Sprintf("订单 %s 使用积分", orderNo),
})
}
return nil
})
if err != nil {
return nil, err
}
// 构建响应
respItems := make([]types.OrderItem, 0, len(orderItems))
for _, item := range orderItems {
respItems = append(respItems, types.OrderItem{
ID: uint(item.ID),
ProductID: item.ProductID,
SkuID: item.SkuID,
ProductName: item.ProductName,
SkuName: item.SkuName,
Image: item.Image,
Price: item.Price,
Quantity: item.Quantity,
TotalAmount: item.TotalAmount,
})
}
resp = &types.Order{
ID: uint(order.ID),
OrderNo: order.OrderNo,
Status: order.Status,
TotalAmount: order.TotalAmount,
DiscountAmount: order.DiscountAmount,
ShippingFee: order.ShippingFee,
PayAmount: order.PayAmount,
PointsUsed: order.PointsUsed,
PointsEarned: order.PointsEarned,
ReceiverName: order.ReceiverName,
ReceiverPhone: order.ReceiverPhone,
ReceiverAddr: order.ReceiverAddr,
Remark: order.Remark,
Items: respItems,
CreatedAt: order.CreatedAt.Format("2006-01-02 15:04:05"),
}
return resp, nil
}

46
backend/healthapi/internal/logic/delete_address_logic.go

@ -0,0 +1,46 @@
package logic
import (
"context"
"healthapi/internal/model"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/errorx"
"github.com/zeromicro/go-zero/core/logx"
)
type DeleteAddressLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewDeleteAddressLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteAddressLogic {
return &DeleteAddressLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *DeleteAddressLogic) DeleteAddress(req *types.AddressIdReq) (resp *types.CommonResp, err error) {
userID, err := GetUserIDFromCtx(l.ctx)
if err != nil {
return nil, errorx.ErrUnauthorized
}
result := l.svcCtx.DB.Where("id = ? AND user_id = ?", req.Id, userID).Delete(&model.Address{})
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected == 0 {
return nil, errorx.NewCodeError(404, "地址不存在")
}
return &types.CommonResp{
Code: 0,
Message: "删除成功",
}, nil
}

49
backend/healthapi/internal/logic/delete_allergy_record_logic.go

@ -0,0 +1,49 @@
package logic
import (
"context"
"healthapi/internal/model"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/errorx"
"github.com/zeromicro/go-zero/core/logx"
)
type DeleteAllergyRecordLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewDeleteAllergyRecordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteAllergyRecordLogic {
return &DeleteAllergyRecordLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *DeleteAllergyRecordLogic) DeleteAllergyRecord(req *types.IdPathReq) (resp *types.CommonResp, err error) {
userID, err := GetUserIDFromCtx(l.ctx)
if err != nil {
return nil, errorx.ErrUnauthorized
}
var profile model.HealthProfile
if err := l.svcCtx.DB.Where("user_id = ?", userID).First(&profile).Error; err != nil {
return nil, errorx.ErrNotFound
}
var record model.AllergyRecord
if err := l.svcCtx.DB.Where("id = ? AND health_profile_id = ?", req.Id, profile.ID).First(&record).Error; err != nil {
return nil, errorx.ErrNotFound
}
if err := l.svcCtx.DB.Delete(&record).Error; err != nil {
return nil, errorx.ErrServerError
}
return &types.CommonResp{Code: 0, Message: "删除成功"}, nil
}

46
backend/healthapi/internal/logic/delete_cart_logic.go

@ -0,0 +1,46 @@
package logic
import (
"context"
"healthapi/internal/model"
"healthapi/internal/svc"
"healthapi/internal/types"
"healthapi/pkg/errorx"
"github.com/zeromicro/go-zero/core/logx"
)
type DeleteCartLogic struct {
logx.Logger
ctx context.Context
svcCtx *svc.ServiceContext
}
func NewDeleteCartLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteCartLogic {
return &DeleteCartLogic{
Logger: logx.WithContext(ctx),
ctx: ctx,
svcCtx: svcCtx,
}
}
func (l *DeleteCartLogic) DeleteCart(req *types.IdPathReq) (resp *types.CommonResp, err error) {
userID, err := GetUserIDFromCtx(l.ctx)
if err != nil {
return nil, errorx.ErrUnauthorized
}
result := l.svcCtx.DB.Where("id = ? AND user_id = ?", req.Id, userID).Delete(&model.CartItem{})
if result.Error != nil {
return nil, result.Error
}
if result.RowsAffected == 0 {
return nil, errorx.NewCodeError(404, "购物车项不存在")
}
return &types.CommonResp{
Code: 0,
Message: "删除成功",
}, nil
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save