diff --git a/backend/BACKEND.md b/backend/BACKEND.md new file mode 100644 index 0000000..7d15a49 --- /dev/null +++ b/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 | diff --git a/backend/healthapi/etc/healthapi-api.yaml b/backend/healthapi/etc/healthapi-api.yaml new file mode 100644 index 0000000..e362be9 --- /dev/null +++ b/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 diff --git a/backend/healthapi/go.mod b/backend/healthapi/go.mod new file mode 100644 index 0000000..494affb --- /dev/null +++ b/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 +) diff --git a/backend/healthapi/go.sum b/backend/healthapi/go.sum new file mode 100644 index 0000000..bffad55 --- /dev/null +++ b/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= diff --git a/backend/healthapi/healthapi.api b/backend/healthapi/healthapi.api new file mode 100644 index 0000000..8dd6313 --- /dev/null +++ b/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) +} + diff --git a/backend/healthapi/healthapi.go b/backend/healthapi/healthapi.go new file mode 100644 index 0000000..c62604d --- /dev/null +++ b/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() +} diff --git a/backend/healthapi/internal/config/config.go b/backend/healthapi/internal/config/config.go new file mode 100644 index 0000000..2b254c2 --- /dev/null +++ b/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 +} diff --git a/backend/healthapi/internal/database/database.go b/backend/healthapi/internal/database/database.go new file mode 100644 index 0000000..8f28afb --- /dev/null +++ b/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 +} diff --git a/backend/healthapi/internal/handler/add_cart_handler.go b/backend/healthapi/internal/handler/add_cart_handler.go new file mode 100644 index 0000000..2eb8455 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/batch_select_cart_handler.go b/backend/healthapi/internal/handler/batch_select_cart_handler.go new file mode 100644 index 0000000..8306b4a --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/batch_submit_allergy_handler.go b/backend/healthapi/internal/handler/batch_submit_allergy_handler.go new file mode 100644 index 0000000..4be839c --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/batch_submit_family_history_handler.go b/backend/healthapi/internal/handler/batch_submit_family_history_handler.go new file mode 100644 index 0000000..6933913 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/batch_submit_medical_history_handler.go b/backend/healthapi/internal/handler/batch_submit_medical_history_handler.go new file mode 100644 index 0000000..ac986da --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/cancel_order_handler.go b/backend/healthapi/internal/handler/cancel_order_handler.go new file mode 100644 index 0000000..e5cfd54 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/clear_cart_handler.go b/backend/healthapi/internal/handler/clear_cart_handler.go new file mode 100644 index 0000000..ff0c343 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/complete_survey_handler.go b/backend/healthapi/internal/handler/complete_survey_handler.go new file mode 100644 index 0000000..6ca33c1 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/confirm_receive_handler.go b/backend/healthapi/internal/handler/confirm_receive_handler.go new file mode 100644 index 0000000..09dae50 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/create_address_handler.go b/backend/healthapi/internal/handler/create_address_handler.go new file mode 100644 index 0000000..3aa6145 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/create_conversation_handler.go b/backend/healthapi/internal/handler/create_conversation_handler.go new file mode 100644 index 0000000..8660ab3 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/create_order_handler.go b/backend/healthapi/internal/handler/create_order_handler.go new file mode 100644 index 0000000..562c4ce --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/delete_address_handler.go b/backend/healthapi/internal/handler/delete_address_handler.go new file mode 100644 index 0000000..732ab10 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/delete_allergy_record_handler.go b/backend/healthapi/internal/handler/delete_allergy_record_handler.go new file mode 100644 index 0000000..c1c691e --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/delete_cart_handler.go b/backend/healthapi/internal/handler/delete_cart_handler.go new file mode 100644 index 0000000..e2f84e9 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/delete_conversation_handler.go b/backend/healthapi/internal/handler/delete_conversation_handler.go new file mode 100644 index 0000000..61e5caf --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/delete_family_history_handler.go b/backend/healthapi/internal/handler/delete_family_history_handler.go new file mode 100644 index 0000000..e3abfd2 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/delete_medical_history_handler.go b/backend/healthapi/internal/handler/delete_medical_history_handler.go new file mode 100644 index 0000000..914d714 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_address_handler.go b/backend/healthapi/internal/handler/get_address_handler.go new file mode 100644 index 0000000..65731ab --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_addresses_handler.go b/backend/healthapi/internal/handler/get_addresses_handler.go new file mode 100644 index 0000000..09f746d --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_allergy_records_handler.go b/backend/healthapi/internal/handler/get_allergy_records_handler.go new file mode 100644 index 0000000..752f0b7 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_assessment_history_handler.go b/backend/healthapi/internal/handler/get_assessment_history_handler.go new file mode 100644 index 0000000..6eb24d5 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_assessment_result_handler.go b/backend/healthapi/internal/handler/get_assessment_result_handler.go new file mode 100644 index 0000000..f01f123 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_basic_profile_handler.go b/backend/healthapi/internal/handler/get_basic_profile_handler.go new file mode 100644 index 0000000..e801936 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_cart_handler.go b/backend/healthapi/internal/handler/get_cart_handler.go new file mode 100644 index 0000000..e29a573 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_categories_handler.go b/backend/healthapi/internal/handler/get_categories_handler.go new file mode 100644 index 0000000..171a09a --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_constitution_products_handler.go b/backend/healthapi/internal/handler/get_constitution_products_handler.go new file mode 100644 index 0000000..7c156ea --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_conversation_handler.go b/backend/healthapi/internal/handler/get_conversation_handler.go new file mode 100644 index 0000000..b71753c --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_conversations_handler.go b/backend/healthapi/internal/handler/get_conversations_handler.go new file mode 100644 index 0000000..0eb8fb9 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_family_history_handler.go b/backend/healthapi/internal/handler/get_family_history_handler.go new file mode 100644 index 0000000..521ba57 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_featured_products_handler.go b/backend/healthapi/internal/handler/get_featured_products_handler.go new file mode 100644 index 0000000..b238ee1 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_grouped_questions_handler.go b/backend/healthapi/internal/handler/get_grouped_questions_handler.go new file mode 100644 index 0000000..2d83445 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_health_profile_handler.go b/backend/healthapi/internal/handler/get_health_profile_handler.go new file mode 100644 index 0000000..2d39d3f --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_lifestyle_handler.go b/backend/healthapi/internal/handler/get_lifestyle_handler.go new file mode 100644 index 0000000..f9e8621 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_mall_product_detail_handler.go b/backend/healthapi/internal/handler/get_mall_product_detail_handler.go new file mode 100644 index 0000000..ab90e12 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_mall_products_handler.go b/backend/healthapi/internal/handler/get_mall_products_handler.go new file mode 100644 index 0000000..bb43e1e --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_medical_history_handler.go b/backend/healthapi/internal/handler/get_medical_history_handler.go new file mode 100644 index 0000000..018f337 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_member_info_handler.go b/backend/healthapi/internal/handler/get_member_info_handler.go new file mode 100644 index 0000000..6ef80b2 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_order_handler.go b/backend/healthapi/internal/handler/get_order_handler.go new file mode 100644 index 0000000..6d81327 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_orders_handler.go b/backend/healthapi/internal/handler/get_orders_handler.go new file mode 100644 index 0000000..2c83e7e --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_points_records_handler.go b/backend/healthapi/internal/handler/get_points_records_handler.go new file mode 100644 index 0000000..b7dcc04 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_product_handler.go b/backend/healthapi/internal/handler/get_product_handler.go new file mode 100644 index 0000000..c060e40 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_product_list_handler.go b/backend/healthapi/internal/handler/get_product_list_handler.go new file mode 100644 index 0000000..be784be --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_products_by_category_handler.go b/backend/healthapi/internal/handler/get_products_by_category_handler.go new file mode 100644 index 0000000..e62edee --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_purchase_history_handler.go b/backend/healthapi/internal/handler/get_purchase_history_handler.go new file mode 100644 index 0000000..e88fbb6 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_questions_handler.go b/backend/healthapi/internal/handler/get_questions_handler.go new file mode 100644 index 0000000..526abce --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_recommend_products_handler.go b/backend/healthapi/internal/handler/get_recommend_products_handler.go new file mode 100644 index 0000000..ba202d0 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_recommendations_handler.go b/backend/healthapi/internal/handler/get_recommendations_handler.go new file mode 100644 index 0000000..9a93198 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_survey_status_handler.go b/backend/healthapi/internal/handler/get_survey_status_handler.go new file mode 100644 index 0000000..56d333d --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/get_user_profile_handler.go b/backend/healthapi/internal/handler/get_user_profile_handler.go new file mode 100644 index 0000000..44b4308 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/health_check_handler.go b/backend/healthapi/internal/handler/health_check_handler.go new file mode 100644 index 0000000..6973b27 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/login_handler.go b/backend/healthapi/internal/handler/login_handler.go new file mode 100644 index 0000000..1be0e90 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/pay_order_handler.go b/backend/healthapi/internal/handler/pay_order_handler.go new file mode 100644 index 0000000..ba69bc3 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/preview_order_handler.go b/backend/healthapi/internal/handler/preview_order_handler.go new file mode 100644 index 0000000..187ff15 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/refresh_token_handler.go b/backend/healthapi/internal/handler/refresh_token_handler.go new file mode 100644 index 0000000..1672523 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/register_handler.go b/backend/healthapi/internal/handler/register_handler.go new file mode 100644 index 0000000..24c47e7 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/routes.go b/backend/healthapi/internal/handler/routes.go new file mode 100644 index 0000000..59d52b9 --- /dev/null +++ b/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"), + ) +} diff --git a/backend/healthapi/internal/handler/search_mall_products_handler.go b/backend/healthapi/internal/handler/search_mall_products_handler.go new file mode 100644 index 0000000..f171e68 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/search_products_handler.go b/backend/healthapi/internal/handler/search_products_handler.go new file mode 100644 index 0000000..1971361 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/send_code_handler.go b/backend/healthapi/internal/handler/send_code_handler.go new file mode 100644 index 0000000..90164e3 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/send_message_handler.go b/backend/healthapi/internal/handler/send_message_handler.go new file mode 100644 index 0000000..6a3daae --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/send_message_stream_handler.go b/backend/healthapi/internal/handler/send_message_stream_handler.go new file mode 100644 index 0000000..33e15c8 --- /dev/null +++ b/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 +} diff --git a/backend/healthapi/internal/handler/set_default_address_handler.go b/backend/healthapi/internal/handler/set_default_address_handler.go new file mode 100644 index 0000000..54234d2 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/submit_allergy_handler.go b/backend/healthapi/internal/handler/submit_allergy_handler.go new file mode 100644 index 0000000..e81e28e --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/submit_assessment_handler.go b/backend/healthapi/internal/handler/submit_assessment_handler.go new file mode 100644 index 0000000..84f4268 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/submit_basic_info_handler.go b/backend/healthapi/internal/handler/submit_basic_info_handler.go new file mode 100644 index 0000000..40f004d --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/submit_family_history_handler.go b/backend/healthapi/internal/handler/submit_family_history_handler.go new file mode 100644 index 0000000..5f014b3 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/submit_lifestyle_handler.go b/backend/healthapi/internal/handler/submit_lifestyle_handler.go new file mode 100644 index 0000000..cce1656 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/submit_medical_history_handler.go b/backend/healthapi/internal/handler/submit_medical_history_handler.go new file mode 100644 index 0000000..ceef1e3 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/sync_purchase_handler.go b/backend/healthapi/internal/handler/sync_purchase_handler.go new file mode 100644 index 0000000..afed33b --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/update_address_handler.go b/backend/healthapi/internal/handler/update_address_handler.go new file mode 100644 index 0000000..54396c6 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/update_cart_handler.go b/backend/healthapi/internal/handler/update_cart_handler.go new file mode 100644 index 0000000..25e9f95 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/update_health_profile_handler.go b/backend/healthapi/internal/handler/update_health_profile_handler.go new file mode 100644 index 0000000..fa27f8a --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/update_lifestyle_handler.go b/backend/healthapi/internal/handler/update_lifestyle_handler.go new file mode 100644 index 0000000..a1741c9 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/handler/update_user_profile_handler.go b/backend/healthapi/internal/handler/update_user_profile_handler.go new file mode 100644 index 0000000..c21ba26 --- /dev/null +++ b/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) + } + } +} diff --git a/backend/healthapi/internal/logic/add_cart_logic.go b/backend/healthapi/internal/logic/add_cart_logic.go new file mode 100644 index 0000000..d01815b --- /dev/null +++ b/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 +} diff --git a/backend/healthapi/internal/logic/batch_select_cart_logic.go b/backend/healthapi/internal/logic/batch_select_cart_logic.go new file mode 100644 index 0000000..a2c2042 --- /dev/null +++ b/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 +} diff --git a/backend/healthapi/internal/logic/batch_submit_allergy_logic.go b/backend/healthapi/internal/logic/batch_submit_allergy_logic.go new file mode 100644 index 0000000..71a0801 --- /dev/null +++ b/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 +} diff --git a/backend/healthapi/internal/logic/batch_submit_family_history_logic.go b/backend/healthapi/internal/logic/batch_submit_family_history_logic.go new file mode 100644 index 0000000..7ba2063 --- /dev/null +++ b/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 +} diff --git a/backend/healthapi/internal/logic/batch_submit_medical_history_logic.go b/backend/healthapi/internal/logic/batch_submit_medical_history_logic.go new file mode 100644 index 0000000..96b9f50 --- /dev/null +++ b/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 +} diff --git a/backend/healthapi/internal/logic/cancel_order_logic.go b/backend/healthapi/internal/logic/cancel_order_logic.go new file mode 100644 index 0000000..49f9fe8 --- /dev/null +++ b/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 +} diff --git a/backend/healthapi/internal/logic/clear_cart_logic.go b/backend/healthapi/internal/logic/clear_cart_logic.go new file mode 100644 index 0000000..9fa8cb0 --- /dev/null +++ b/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 +} diff --git a/backend/healthapi/internal/logic/complete_survey_logic.go b/backend/healthapi/internal/logic/complete_survey_logic.go new file mode 100644 index 0000000..e2683c6 --- /dev/null +++ b/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 +} diff --git a/backend/healthapi/internal/logic/confirm_receive_logic.go b/backend/healthapi/internal/logic/confirm_receive_logic.go new file mode 100644 index 0000000..dbda3de --- /dev/null +++ b/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 +} diff --git a/backend/healthapi/internal/logic/create_address_logic.go b/backend/healthapi/internal/logic/create_address_logic.go new file mode 100644 index 0000000..f9486dc --- /dev/null +++ b/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 +} diff --git a/backend/healthapi/internal/logic/create_conversation_logic.go b/backend/healthapi/internal/logic/create_conversation_logic.go new file mode 100644 index 0000000..4d2ec9f --- /dev/null +++ b/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 +} diff --git a/backend/healthapi/internal/logic/create_order_logic.go b/backend/healthapi/internal/logic/create_order_logic.go new file mode 100644 index 0000000..d30e1dc --- /dev/null +++ b/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 +} diff --git a/backend/healthapi/internal/logic/delete_address_logic.go b/backend/healthapi/internal/logic/delete_address_logic.go new file mode 100644 index 0000000..5d7aaa8 --- /dev/null +++ b/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 +} diff --git a/backend/healthapi/internal/logic/delete_allergy_record_logic.go b/backend/healthapi/internal/logic/delete_allergy_record_logic.go new file mode 100644 index 0000000..cba3171 --- /dev/null +++ b/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 +} diff --git a/backend/healthapi/internal/logic/delete_cart_logic.go b/backend/healthapi/internal/logic/delete_cart_logic.go new file mode 100644 index 0000000..971cdbf --- /dev/null +++ b/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 +} diff --git a/backend/healthapi/internal/logic/delete_conversation_logic.go b/backend/healthapi/internal/logic/delete_conversation_logic.go new file mode 100644 index 0000000..68cfe16 --- /dev/null +++ b/backend/healthapi/internal/logic/delete_conversation_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 DeleteConversationLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewDeleteConversationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteConversationLogic { + return &DeleteConversationLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteConversationLogic) DeleteConversation(req *types.ConversationIdReq) (resp *types.CommonResp, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + var conversation model.Conversation + if err := l.svcCtx.DB.Where("id = ? AND user_id = ?", req.Id, userID).First(&conversation).Error; err != nil { + return nil, errorx.ErrNotFound + } + + // 删除消息 + l.svcCtx.DB.Where("conversation_id = ?", conversation.ID).Delete(&model.Message{}) + + // 删除对话 + if err := l.svcCtx.DB.Delete(&conversation).Error; err != nil { + return nil, errorx.ErrServerError + } + + return &types.CommonResp{Code: 0, Message: "删除成功"}, nil +} diff --git a/backend/healthapi/internal/logic/delete_family_history_logic.go b/backend/healthapi/internal/logic/delete_family_history_logic.go new file mode 100644 index 0000000..36fc04e --- /dev/null +++ b/backend/healthapi/internal/logic/delete_family_history_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 DeleteFamilyHistoryLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewDeleteFamilyHistoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteFamilyHistoryLogic { + return &DeleteFamilyHistoryLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteFamilyHistoryLogic) DeleteFamilyHistory(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 history model.FamilyHistory + if err := l.svcCtx.DB.Where("id = ? AND health_profile_id = ?", req.Id, profile.ID).First(&history).Error; err != nil { + return nil, errorx.ErrNotFound + } + + if err := l.svcCtx.DB.Delete(&history).Error; err != nil { + return nil, errorx.ErrServerError + } + + return &types.CommonResp{Code: 0, Message: "删除成功"}, nil +} diff --git a/backend/healthapi/internal/logic/delete_medical_history_logic.go b/backend/healthapi/internal/logic/delete_medical_history_logic.go new file mode 100644 index 0000000..331d0e7 --- /dev/null +++ b/backend/healthapi/internal/logic/delete_medical_history_logic.go @@ -0,0 +1,50 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type DeleteMedicalHistoryLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewDeleteMedicalHistoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *DeleteMedicalHistoryLogic { + return &DeleteMedicalHistoryLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *DeleteMedicalHistoryLogic) DeleteMedicalHistory(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 history model.MedicalHistory + if err := l.svcCtx.DB.Where("id = ? AND health_profile_id = ?", req.Id, profile.ID).First(&history).Error; err != nil { + return nil, errorx.ErrNotFound + } + + if err := l.svcCtx.DB.Delete(&history).Error; err != nil { + return nil, errorx.ErrServerError + } + + return &types.CommonResp{Code: 0, Message: "删除成功"}, nil +} diff --git a/backend/healthapi/internal/logic/get_address_logic.go b/backend/healthapi/internal/logic/get_address_logic.go new file mode 100644 index 0000000..77e450d --- /dev/null +++ b/backend/healthapi/internal/logic/get_address_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 GetAddressLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetAddressLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAddressLogic { + return &GetAddressLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAddressLogic) GetAddress(req *types.AddressIdReq) (resp *types.Address, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + var addr model.Address + if err := l.svcCtx.DB.Where("id = ? AND user_id = ?", req.Id, userID).First(&addr).Error; err != nil { + return nil, errorx.NewCodeError(404, "地址不存在") + } + + 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 +} diff --git a/backend/healthapi/internal/logic/get_addresses_logic.go b/backend/healthapi/internal/logic/get_addresses_logic.go new file mode 100644 index 0000000..8082363 --- /dev/null +++ b/backend/healthapi/internal/logic/get_addresses_logic.go @@ -0,0 +1,57 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetAddressesLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetAddressesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAddressesLogic { + return &GetAddressesLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAddressesLogic) GetAddresses() (resp *types.AddressListResp, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + var addresses []model.Address + l.svcCtx.DB.Where("user_id = ?", userID).Order("is_default DESC, created_at DESC").Find(&addresses) + + resp = &types.AddressListResp{ + Addresses: make([]types.Address, 0, len(addresses)), + } + + for _, addr := range addresses { + resp.Addresses = append(resp.Addresses, 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 +} diff --git a/backend/healthapi/internal/logic/get_allergy_records_logic.go b/backend/healthapi/internal/logic/get_allergy_records_logic.go new file mode 100644 index 0000000..b805c61 --- /dev/null +++ b/backend/healthapi/internal/logic/get_allergy_records_logic.go @@ -0,0 +1,55 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetAllergyRecordsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetAllergyRecordsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAllergyRecordsLogic { + return &GetAllergyRecordsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAllergyRecordsLogic) GetAllergyRecords() (resp []types.AllergyRecord, 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.CodeNotFound, "请先填写基础信息") + } + + var records []model.AllergyRecord + l.svcCtx.DB.Where("health_profile_id = ?", profile.ID).Find(&records) + + resp = make([]types.AllergyRecord, 0, len(records)) + for _, r := range records { + resp = append(resp, types.AllergyRecord{ + ID: uint(r.ID), + HealthProfileID: r.HealthProfileID, + AllergyType: r.AllergyType, + Allergen: r.Allergen, + Severity: r.Severity, + ReactionDesc: r.ReactionDesc, + }) + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_assessment_history_logic.go b/backend/healthapi/internal/logic/get_assessment_history_logic.go new file mode 100644 index 0000000..d86ea5e --- /dev/null +++ b/backend/healthapi/internal/logic/get_assessment_history_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 GetAssessmentHistoryLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetAssessmentHistoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAssessmentHistoryLogic { + return &GetAssessmentHistoryLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAssessmentHistoryLogic) GetAssessmentHistory() (resp *types.AssessmentHistoryResp, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + var assessments []model.ConstitutionAssessment + l.svcCtx.DB.Where("user_id = ?", userID).Order("assessed_at DESC").Limit(10).Find(&assessments) + + resp = &types.AssessmentHistoryResp{ + History: make([]types.AssessmentResult, 0, len(assessments)), + } + + for _, a := range assessments { + resp.History = append(resp.History, *parseAssessment(&a)) + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_assessment_result_logic.go b/backend/healthapi/internal/logic/get_assessment_result_logic.go new file mode 100644 index 0000000..d088034 --- /dev/null +++ b/backend/healthapi/internal/logic/get_assessment_result_logic.go @@ -0,0 +1,61 @@ +package logic + +import ( + "context" + "encoding/json" + "time" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetAssessmentResultLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetAssessmentResultLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetAssessmentResultLogic { + return &GetAssessmentResultLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetAssessmentResultLogic) GetAssessmentResult() (resp *types.AssessmentResult, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + var assessment model.ConstitutionAssessment + if err := l.svcCtx.DB.Where("user_id = ?", userID).Order("assessed_at DESC").First(&assessment).Error; err != nil { + return nil, errorx.NewCodeError(errorx.CodeNotFound, "暂无体质测评记录") + } + + return parseAssessment(&assessment), nil +} + +func parseAssessment(assessment *model.ConstitutionAssessment) *types.AssessmentResult { + var scores map[string]float64 + var secondaryTypes []string + var recommendations map[string]string + + json.Unmarshal([]byte(assessment.Scores), &scores) + json.Unmarshal([]byte(assessment.SecondaryConstitutions), &secondaryTypes) + json.Unmarshal([]byte(assessment.Recommendations), &recommendations) + + return &types.AssessmentResult{ + ID: uint(assessment.ID), + AssessedAt: assessment.AssessedAt.Format(time.RFC3339), + Scores: scores, + PrimaryConstitution: assessment.PrimaryConstitution, + SecondaryConstitutions: secondaryTypes, + Recommendations: recommendations, + } +} diff --git a/backend/healthapi/internal/logic/get_basic_profile_logic.go b/backend/healthapi/internal/logic/get_basic_profile_logic.go new file mode 100644 index 0000000..b5a23b2 --- /dev/null +++ b/backend/healthapi/internal/logic/get_basic_profile_logic.go @@ -0,0 +1,58 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetBasicProfileLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetBasicProfileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetBasicProfileLogic { + return &GetBasicProfileLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetBasicProfileLogic) GetBasicProfile() (resp *types.HealthProfile, 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.CodeNotFound, "健康档案不存在") + } + + birthDate := "" + if profile.BirthDate != nil { + birthDate = profile.BirthDate.Format("2006-01-02") + } + + return &types.HealthProfile{ + ID: uint(profile.ID), + UserID: profile.UserID, + Name: profile.Name, + BirthDate: birthDate, + Gender: profile.Gender, + Height: profile.Height, + Weight: profile.Weight, + BMI: profile.BMI, + BloodType: profile.BloodType, + Occupation: profile.Occupation, + MaritalStatus: profile.MaritalStatus, + Region: profile.Region, + }, nil +} diff --git a/backend/healthapi/internal/logic/get_cart_logic.go b/backend/healthapi/internal/logic/get_cart_logic.go new file mode 100644 index 0000000..c7ad5bb --- /dev/null +++ b/backend/healthapi/internal/logic/get_cart_logic.go @@ -0,0 +1,85 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetCartLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetCartLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetCartLogic { + return &GetCartLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetCartLogic) GetCart() (resp *types.CartResp, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + var cartItems []model.CartItem + l.svcCtx.DB.Where("user_id = ?", userID).Order("created_at DESC").Find(&cartItems) + + resp = &types.CartResp{ + Items: make([]types.CartItem, 0, len(cartItems)), + TotalCount: 0, + SelectedCount: 0, + TotalAmount: 0, + } + + for _, item := range cartItems { + // 获取商品信息 + var product model.Product + if err := l.svcCtx.DB.First(&product, item.ProductID).Error; err != nil { + continue // 跳过已下架的商品 + } + + cartItem := types.CartItem{ + ID: uint(item.ID), + ProductID: item.ProductID, + SkuID: item.SkuID, + ProductName: product.Name, + Image: product.MainImage, + Price: product.Price, + Quantity: item.Quantity, + Selected: item.Selected, + Stock: product.Stock, + } + + // 如果有 SKU,获取 SKU 信息 + if item.SkuID > 0 { + var sku model.ProductSku + if err := l.svcCtx.DB.First(&sku, item.SkuID).Error; err == nil { + cartItem.SkuName = sku.Name + cartItem.Price = sku.Price + cartItem.Stock = sku.Stock + if sku.Image != "" { + cartItem.Image = sku.Image + } + } + } + + resp.Items = append(resp.Items, cartItem) + resp.TotalCount += item.Quantity + if item.Selected { + resp.SelectedCount += item.Quantity + resp.TotalAmount += cartItem.Price * float64(item.Quantity) + } + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_categories_logic.go b/backend/healthapi/internal/logic/get_categories_logic.go new file mode 100644 index 0000000..a254810 --- /dev/null +++ b/backend/healthapi/internal/logic/get_categories_logic.go @@ -0,0 +1,47 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetCategoriesLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetCategoriesLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetCategoriesLogic { + return &GetCategoriesLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetCategoriesLogic) GetCategories() (resp *types.CategoryListResp, err error) { + var categories []model.ProductCategory + l.svcCtx.DB.Where("is_active = ?", true).Order("sort ASC, id ASC").Find(&categories) + + resp = &types.CategoryListResp{ + Categories: make([]types.ProductCategory, 0, len(categories)), + } + + for _, c := range categories { + resp.Categories = append(resp.Categories, types.ProductCategory{ + ID: uint(c.ID), + Name: c.Name, + ParentID: c.ParentID, + Icon: c.Icon, + Description: c.Description, + Sort: c.Sort, + }) + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_constitution_products_logic.go b/backend/healthapi/internal/logic/get_constitution_products_logic.go new file mode 100644 index 0000000..74edf0d --- /dev/null +++ b/backend/healthapi/internal/logic/get_constitution_products_logic.go @@ -0,0 +1,112 @@ +package logic + +import ( + "context" + "fmt" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetConstitutionProductsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetConstitutionProductsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetConstitutionProductsLogic { + return &GetConstitutionProductsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetConstitutionProductsLogic) GetConstitutionProducts() (resp *types.RecommendProductsResp, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + // 获取用户最新体质评估 + var assessment model.ConstitutionAssessment + if err := l.svcCtx.DB.Where("user_id = ?", userID).Order("assessed_at DESC").First(&assessment).Error; err != nil { + // 没有体质评估结果,返回热门商品 + var products []model.Product + l.svcCtx.DB.Where("is_active = ?", true).Order("sales_count DESC").Limit(10).Find(&products) + + resp = &types.RecommendProductsResp{ + Products: make([]types.Product, 0, len(products)), + Constitution: "", + Reason: "您还未进行体质评估,为您推荐热门商品", + } + + for _, p := range products { + resp.Products = append(resp.Products, types.Product{ + ID: uint(p.ID), + Name: p.Name, + Category: p.Category, + Description: p.Description, + Efficacy: p.Efficacy, + Suitable: p.Suitable, + Price: p.Price, + ImageURL: p.MainImage, + MallURL: p.MallURL, + IsActive: p.IsActive, + }) + } + + return resp, nil + } + + constitutionType := assessment.PrimaryConstitution + constitutionName := model.ConstitutionNames[constitutionType] + + // 通过 constitution_products 关联表查找推荐商品 + var productIDs []uint + l.svcCtx.DB.Model(&model.ConstitutionProduct{}). + Where("constitution_type = ?", constitutionType). + Order("priority DESC"). + Pluck("product_id", &productIDs) + + var products []model.Product + if len(productIDs) > 0 { + l.svcCtx.DB.Where("id IN ? AND is_active = ?", productIDs, true).Find(&products) + } + + // 如果关联表没有数据,通过体质类型在商品的 constitution_types 字段中搜索 + if len(products) == 0 { + keyword := fmt.Sprintf("%%%s%%", constitutionType) + l.svcCtx.DB.Where("is_active = ? AND (constitution_types LIKE ? OR suitable LIKE ?)", + true, keyword, keyword). + Limit(10). + Find(&products) + } + + resp = &types.RecommendProductsResp{ + Products: make([]types.Product, 0, len(products)), + Constitution: constitutionName, + Reason: fmt.Sprintf("根据您的%s体质,为您推荐以下调理产品", constitutionName), + } + + for _, p := range products { + resp.Products = append(resp.Products, types.Product{ + ID: uint(p.ID), + Name: p.Name, + Category: p.Category, + Description: p.Description, + Efficacy: p.Efficacy, + Suitable: p.Suitable, + Price: p.Price, + ImageURL: p.MainImage, + MallURL: p.MallURL, + IsActive: p.IsActive, + }) + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_conversation_logic.go b/backend/healthapi/internal/logic/get_conversation_logic.go new file mode 100644 index 0000000..2406e7e --- /dev/null +++ b/backend/healthapi/internal/logic/get_conversation_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 GetConversationLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetConversationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetConversationLogic { + return &GetConversationLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetConversationLogic) GetConversation(req *types.ConversationIdReq) (resp *types.ConversationDetailResp, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + var conversation model.Conversation + if err := l.svcCtx.DB.Where("id = ? AND user_id = ?", req.Id, userID).First(&conversation).Error; err != nil { + return nil, errorx.ErrNotFound + } + + var messages []model.Message + l.svcCtx.DB.Where("conversation_id = ?", conversation.ID).Order("created_at ASC").Find(&messages) + + resp = &types.ConversationDetailResp{ + ID: uint(conversation.ID), + Title: conversation.Title, + Messages: make([]types.Message, 0, len(messages)), + CreatedAt: conversation.CreatedAt.Format("2006-01-02 15:04:05"), + } + + for _, m := range messages { + resp.Messages = append(resp.Messages, types.Message{ + ID: uint(m.ID), + ConversationID: m.ConversationID, + Role: m.Role, + Content: m.Content, + CreatedAt: m.CreatedAt.Format("2006-01-02 15:04:05"), + }) + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_conversations_logic.go b/backend/healthapi/internal/logic/get_conversations_logic.go new file mode 100644 index 0000000..b0a0099 --- /dev/null +++ b/backend/healthapi/internal/logic/get_conversations_logic.go @@ -0,0 +1,51 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetConversationsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetConversationsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetConversationsLogic { + return &GetConversationsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetConversationsLogic) GetConversations() (resp *types.ConversationListResp, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + var conversations []model.Conversation + l.svcCtx.DB.Where("user_id = ?", userID).Order("updated_at DESC").Find(&conversations) + + resp = &types.ConversationListResp{ + Conversations: make([]types.ConversationItem, 0, len(conversations)), + } + + for _, c := range conversations { + resp.Conversations = append(resp.Conversations, types.ConversationItem{ + ID: uint(c.ID), + Title: c.Title, + CreatedAt: c.CreatedAt.Format("2006-01-02 15:04:05"), + UpdatedAt: c.UpdatedAt.Format("2006-01-02 15:04:05"), + }) + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_family_history_logic.go b/backend/healthapi/internal/logic/get_family_history_logic.go new file mode 100644 index 0000000..38f621d --- /dev/null +++ b/backend/healthapi/internal/logic/get_family_history_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 GetFamilyHistoryLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetFamilyHistoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetFamilyHistoryLogic { + return &GetFamilyHistoryLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetFamilyHistoryLogic) GetFamilyHistory() (resp []types.FamilyHistory, 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.CodeNotFound, "请先填写基础信息") + } + + var histories []model.FamilyHistory + l.svcCtx.DB.Where("health_profile_id = ?", profile.ID).Find(&histories) + + resp = make([]types.FamilyHistory, 0, len(histories)) + for _, h := range histories { + resp = append(resp, types.FamilyHistory{ + ID: uint(h.ID), + HealthProfileID: h.HealthProfileID, + Relation: h.Relation, + DiseaseName: h.DiseaseName, + Notes: h.Notes, + }) + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_featured_products_logic.go b/backend/healthapi/internal/logic/get_featured_products_logic.go new file mode 100644 index 0000000..95c21a1 --- /dev/null +++ b/backend/healthapi/internal/logic/get_featured_products_logic.go @@ -0,0 +1,62 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetFeaturedProductsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetFeaturedProductsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetFeaturedProductsLogic { + return &GetFeaturedProductsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetFeaturedProductsLogic) GetFeaturedProducts(req *types.PageReq) (resp *types.ProductListResp, err error) { + query := l.svcCtx.DB.Model(&model.Product{}).Where("is_active = ? AND is_featured = ?", true, true) + + var total int64 + query.Count(&total) + + var products []model.Product + offset := (req.Page - 1) * req.PageSize + query.Order("sort DESC, sales_count DESC").Offset(offset).Limit(req.PageSize).Find(&products) + + resp = &types.ProductListResp{ + Products: make([]types.Product, 0, len(products)), + PageInfo: types.PageInfo{ + Total: total, + Page: req.Page, + PageSize: req.PageSize, + }, + } + + for _, p := range products { + resp.Products = append(resp.Products, types.Product{ + ID: uint(p.ID), + Name: p.Name, + Category: p.Category, + Description: p.Description, + Efficacy: p.Efficacy, + Suitable: p.Suitable, + Price: p.Price, + ImageURL: p.MainImage, + MallURL: p.MallURL, + IsActive: p.IsActive, + }) + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_grouped_questions_logic.go b/backend/healthapi/internal/logic/get_grouped_questions_logic.go new file mode 100644 index 0000000..0cff74f --- /dev/null +++ b/backend/healthapi/internal/logic/get_grouped_questions_logic.go @@ -0,0 +1,48 @@ +package logic + +import ( + "context" + "encoding/json" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetGroupedQuestionsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetGroupedQuestionsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetGroupedQuestionsLogic { + return &GetGroupedQuestionsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetGroupedQuestionsLogic) GetGroupedQuestions() (resp *types.GroupedQuestionsResp, err error) { + var questions []model.QuestionBank + l.svcCtx.DB.Order("constitution_type, order_num").Find(&questions) + + groups := make(map[string][]types.Question) + for _, q := range questions { + var options []string + json.Unmarshal([]byte(q.Options), &options) + + question := types.Question{ + ID: int(q.ID), + ConstitutionType: q.ConstitutionType, + QuestionText: q.QuestionText, + Options: options, + OrderNum: q.OrderNum, + } + groups[q.ConstitutionType] = append(groups[q.ConstitutionType], question) + } + + return &types.GroupedQuestionsResp{Groups: groups}, nil +} diff --git a/backend/healthapi/internal/logic/get_health_profile_logic.go b/backend/healthapi/internal/logic/get_health_profile_logic.go new file mode 100644 index 0000000..4214dcf --- /dev/null +++ b/backend/healthapi/internal/logic/get_health_profile_logic.go @@ -0,0 +1,120 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetHealthProfileLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetHealthProfileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetHealthProfileLogic { + return &GetHealthProfileLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetHealthProfileLogic) GetHealthProfile() (resp *types.FullHealthProfileResp, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + resp = &types.FullHealthProfileResp{} + + // 获取基础档案 + var profile model.HealthProfile + if err := l.svcCtx.DB.Where("user_id = ?", userID).First(&profile).Error; err == nil { + resp.Profile = types.HealthProfile{ + ID: uint(profile.ID), + UserID: profile.UserID, + Name: profile.Name, + Gender: profile.Gender, + Height: profile.Height, + Weight: profile.Weight, + BMI: profile.BMI, + BloodType: profile.BloodType, + Occupation: profile.Occupation, + MaritalStatus: profile.MaritalStatus, + Region: profile.Region, + } + if profile.BirthDate != nil { + resp.Profile.BirthDate = profile.BirthDate.Format("2006-01-02") + } + + // 获取病史 + var medicalHistories []model.MedicalHistory + l.svcCtx.DB.Where("health_profile_id = ?", profile.ID).Find(&medicalHistories) + for _, mh := range medicalHistories { + resp.MedicalHistory = append(resp.MedicalHistory, types.MedicalHistory{ + ID: uint(mh.ID), + HealthProfileID: mh.HealthProfileID, + DiseaseName: mh.DiseaseName, + DiseaseType: mh.DiseaseType, + DiagnosedDate: mh.DiagnosedDate, + Status: mh.Status, + Notes: mh.Notes, + }) + } + + // 获取家族病史 + var familyHistories []model.FamilyHistory + l.svcCtx.DB.Where("health_profile_id = ?", profile.ID).Find(&familyHistories) + for _, fh := range familyHistories { + resp.FamilyHistory = append(resp.FamilyHistory, types.FamilyHistory{ + ID: uint(fh.ID), + HealthProfileID: fh.HealthProfileID, + Relation: fh.Relation, + DiseaseName: fh.DiseaseName, + Notes: fh.Notes, + }) + } + + // 获取过敏记录 + var allergyRecords []model.AllergyRecord + l.svcCtx.DB.Where("health_profile_id = ?", profile.ID).Find(&allergyRecords) + for _, ar := range allergyRecords { + resp.AllergyRecords = append(resp.AllergyRecords, types.AllergyRecord{ + ID: uint(ar.ID), + HealthProfileID: ar.HealthProfileID, + AllergyType: ar.AllergyType, + Allergen: ar.Allergen, + Severity: ar.Severity, + ReactionDesc: ar.ReactionDesc, + }) + } + } + + // 获取生活习惯 + var lifestyle model.LifestyleInfo + if err := l.svcCtx.DB.Where("user_id = ?", userID).First(&lifestyle).Error; err == nil { + resp.Lifestyle = types.LifestyleInfo{ + ID: uint(lifestyle.ID), + UserID: lifestyle.UserID, + SleepTime: lifestyle.SleepTime, + WakeTime: lifestyle.WakeTime, + SleepQuality: lifestyle.SleepQuality, + MealRegularity: lifestyle.MealRegularity, + DietPreference: lifestyle.DietPreference, + DailyWaterML: lifestyle.DailyWaterML, + ExerciseFrequency: lifestyle.ExerciseFrequency, + ExerciseType: lifestyle.ExerciseType, + ExerciseDurationMin: lifestyle.ExerciseDurationMin, + IsSmoker: lifestyle.IsSmoker, + AlcoholFrequency: lifestyle.AlcoholFrequency, + } + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_lifestyle_logic.go b/backend/healthapi/internal/logic/get_lifestyle_logic.go new file mode 100644 index 0000000..846ba28 --- /dev/null +++ b/backend/healthapi/internal/logic/get_lifestyle_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 GetLifestyleLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetLifestyleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetLifestyleLogic { + return &GetLifestyleLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetLifestyleLogic) GetLifestyle() (resp *types.LifestyleInfo, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + var lifestyle model.LifestyleInfo + if err := l.svcCtx.DB.Where("user_id = ?", userID).First(&lifestyle).Error; err != nil { + return nil, errorx.NewCodeError(errorx.CodeNotFound, "生活习惯信息不存在") + } + + return &types.LifestyleInfo{ + ID: uint(lifestyle.ID), + UserID: lifestyle.UserID, + SleepTime: lifestyle.SleepTime, + WakeTime: lifestyle.WakeTime, + SleepQuality: lifestyle.SleepQuality, + MealRegularity: lifestyle.MealRegularity, + DietPreference: lifestyle.DietPreference, + DailyWaterML: lifestyle.DailyWaterML, + ExerciseFrequency: lifestyle.ExerciseFrequency, + ExerciseType: lifestyle.ExerciseType, + ExerciseDurationMin: lifestyle.ExerciseDurationMin, + IsSmoker: lifestyle.IsSmoker, + AlcoholFrequency: lifestyle.AlcoholFrequency, + }, nil +} diff --git a/backend/healthapi/internal/logic/get_mall_product_detail_logic.go b/backend/healthapi/internal/logic/get_mall_product_detail_logic.go new file mode 100644 index 0000000..34597ef --- /dev/null +++ b/backend/healthapi/internal/logic/get_mall_product_detail_logic.go @@ -0,0 +1,95 @@ +package logic + +import ( + "context" + "encoding/json" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetMallProductDetailLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetMallProductDetailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetMallProductDetailLogic { + return &GetMallProductDetailLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetMallProductDetailLogic) GetMallProductDetail(req *types.IdPathReq) (resp *types.ProductDetail, err error) { + var product model.Product + if err := l.svcCtx.DB.First(&product, req.Id).Error; err != nil { + return nil, errorx.NewCodeError(404, "商品不存在") + } + + if !product.IsActive { + return nil, errorx.NewCodeError(404, "商品已下架") + } + + // 解析 JSON 字段 + var images []string + if product.Images != "" { + json.Unmarshal([]byte(product.Images), &images) + } + + var constitutionTypes []string + if product.ConstitutionTypes != "" { + json.Unmarshal([]byte(product.ConstitutionTypes), &constitutionTypes) + } + + var healthTags []string + if product.HealthTags != "" { + json.Unmarshal([]byte(product.HealthTags), &healthTags) + } + + // 获取 SKU 列表 + var skus []model.ProductSku + l.svcCtx.DB.Where("product_id = ? AND is_active = ?", product.ID, true).Find(&skus) + + respSkus := make([]types.ProductSku, 0, len(skus)) + for _, sku := range skus { + respSkus = append(respSkus, types.ProductSku{ + ID: uint(sku.ID), + ProductID: sku.ProductID, + SkuCode: sku.SkuCode, + Name: sku.Name, + Attributes: sku.Attributes, + Price: sku.Price, + Stock: sku.Stock, + Image: sku.Image, + }) + } + + resp = &types.ProductDetail{ + ID: uint(product.ID), + CategoryID: product.CategoryID, + Name: product.Name, + Description: product.Description, + MainImage: product.MainImage, + Images: images, + Price: product.Price, + OriginalPrice: product.OriginalPrice, + Stock: product.Stock, + SalesCount: product.SalesCount, + IsFeatured: product.IsFeatured, + ConstitutionTypes: constitutionTypes, + HealthTags: healthTags, + Efficacy: product.Efficacy, + Ingredients: product.Ingredients, + Usage: product.Usage, + Contraindications: product.Contraindications, + Skus: respSkus, + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_mall_products_logic.go b/backend/healthapi/internal/logic/get_mall_products_logic.go new file mode 100644 index 0000000..1884e49 --- /dev/null +++ b/backend/healthapi/internal/logic/get_mall_products_logic.go @@ -0,0 +1,66 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetMallProductsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetMallProductsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetMallProductsLogic { + return &GetMallProductsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetMallProductsLogic) GetMallProducts(req *types.ProductListReq) (resp *types.ProductListResp, err error) { + query := l.svcCtx.DB.Model(&model.Product{}).Where("is_active = ?", true) + + if req.Category != "" { + query = query.Where("category = ? OR category_id IN (SELECT id FROM product_categories WHERE name = ?)", req.Category, req.Category) + } + + var total int64 + query.Count(&total) + + var products []model.Product + offset := (req.Page - 1) * req.PageSize + query.Order("sort DESC, sales_count DESC").Offset(offset).Limit(req.PageSize).Find(&products) + + resp = &types.ProductListResp{ + Products: make([]types.Product, 0, len(products)), + PageInfo: types.PageInfo{ + Total: total, + Page: req.Page, + PageSize: req.PageSize, + }, + } + + for _, p := range products { + resp.Products = append(resp.Products, types.Product{ + ID: uint(p.ID), + Name: p.Name, + Category: p.Category, + Description: p.Description, + Efficacy: p.Efficacy, + Suitable: p.Suitable, + Price: p.Price, + ImageURL: p.MainImage, + MallURL: p.MallURL, + IsActive: p.IsActive, + }) + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_medical_history_logic.go b/backend/healthapi/internal/logic/get_medical_history_logic.go new file mode 100644 index 0000000..8d83ff5 --- /dev/null +++ b/backend/healthapi/internal/logic/get_medical_history_logic.go @@ -0,0 +1,56 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetMedicalHistoryLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetMedicalHistoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetMedicalHistoryLogic { + return &GetMedicalHistoryLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetMedicalHistoryLogic) GetMedicalHistory() (resp []types.MedicalHistory, 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.CodeNotFound, "请先填写基础信息") + } + + var histories []model.MedicalHistory + l.svcCtx.DB.Where("health_profile_id = ?", profile.ID).Find(&histories) + + resp = make([]types.MedicalHistory, 0, len(histories)) + for _, h := range histories { + resp = append(resp, types.MedicalHistory{ + ID: uint(h.ID), + HealthProfileID: h.HealthProfileID, + DiseaseName: h.DiseaseName, + DiseaseType: h.DiseaseType, + DiagnosedDate: h.DiagnosedDate, + Status: h.Status, + Notes: h.Notes, + }) + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_member_info_logic.go b/backend/healthapi/internal/logic/get_member_info_logic.go new file mode 100644 index 0000000..c3c0fa8 --- /dev/null +++ b/backend/healthapi/internal/logic/get_member_info_logic.go @@ -0,0 +1,85 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetMemberInfoLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetMemberInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetMemberInfoLogic { + return &GetMemberInfoLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetMemberInfoLogic) GetMemberInfo() (resp *types.MemberInfo, 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] + } + + // 计算下一等级信息 + nextLevel := "" + nextLevelSpent := float64(0) + switch user.MemberLevel { + case model.MemberLevelNormal: + nextLevel = model.MemberLevelSilver + nextLevelSpent = 500 - user.TotalSpent + case model.MemberLevelSilver: + nextLevel = model.MemberLevelGold + nextLevelSpent = 2000 - user.TotalSpent + case model.MemberLevelGold: + nextLevel = model.MemberLevelDiamond + nextLevelSpent = 5000 - user.TotalSpent + case model.MemberLevelDiamond: + nextLevel = "" + nextLevelSpent = 0 + } + if nextLevelSpent < 0 { + nextLevelSpent = 0 + } + + memberSince := "" + if user.MemberSince != nil { + memberSince = user.MemberSince.Format("2006-01-02") + } + + resp = &types.MemberInfo{ + Level: user.MemberLevel, + LevelName: levelConfig.Name, + TotalSpent: user.TotalSpent, + Points: user.Points, + MemberSince: memberSince, + NextLevel: nextLevel, + NextLevelSpent: nextLevelSpent, + Discount: levelConfig.Discount, + PointsMultiplier: levelConfig.PointsMultiplier, + FreeShippingMin: levelConfig.FreeShippingMin, + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_order_logic.go b/backend/healthapi/internal/logic/get_order_logic.go new file mode 100644 index 0000000..38962ab --- /dev/null +++ b/backend/healthapi/internal/logic/get_order_logic.go @@ -0,0 +1,95 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetOrderLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetOrderLogic { + return &GetOrderLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetOrderLogic) GetOrder(req *types.OrderIdReq) (resp *types.Order, 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, "订单不存在") + } + + // 获取订单商品 + var items []model.OrderItem + l.svcCtx.DB.Where("order_id = ?", order.ID).Find(&items) + + orderItems := make([]types.OrderItem, 0, len(items)) + for _, item := range items { + orderItems = append(orderItems, 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, + }) + } + + payTime, shipTime, receiveTime := "", "", "" + if order.PayTime != nil { + payTime = order.PayTime.Format("2006-01-02 15:04:05") + } + if order.ShipTime != nil { + shipTime = order.ShipTime.Format("2006-01-02 15:04:05") + } + if order.ReceiveTime != nil { + receiveTime = order.ReceiveTime.Format("2006-01-02 15:04:05") + } + + 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, + PayMethod: order.PayMethod, + PayTime: payTime, + ShipTime: shipTime, + ReceiveTime: receiveTime, + ReceiverName: order.ReceiverName, + ReceiverPhone: order.ReceiverPhone, + ReceiverAddr: order.ReceiverAddr, + ShippingCompany: order.ShippingCompany, + TrackingNo: order.TrackingNo, + Remark: order.Remark, + CancelReason: order.CancelReason, + Items: orderItems, + CreatedAt: order.CreatedAt.Format("2006-01-02 15:04:05"), + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_orders_logic.go b/backend/healthapi/internal/logic/get_orders_logic.go new file mode 100644 index 0000000..f76b118 --- /dev/null +++ b/backend/healthapi/internal/logic/get_orders_logic.go @@ -0,0 +1,113 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetOrdersLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetOrdersLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetOrdersLogic { + return &GetOrdersLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetOrdersLogic) GetOrders(req *types.OrderListReq) (resp *types.OrderListResp, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + query := l.svcCtx.DB.Model(&model.Order{}).Where("user_id = ?", userID) + if req.Status != "" { + query = query.Where("status = ?", req.Status) + } + + var total int64 + query.Count(&total) + + var orders []model.Order + offset := (req.Page - 1) * req.PageSize + query.Order("created_at DESC").Offset(offset).Limit(req.PageSize).Find(&orders) + + resp = &types.OrderListResp{ + Orders: make([]types.Order, 0, len(orders)), + PageInfo: types.PageInfo{ + Total: total, + Page: req.Page, + PageSize: req.PageSize, + }, + } + + for _, order := range orders { + // 获取订单商品 + var items []model.OrderItem + l.svcCtx.DB.Where("order_id = ?", order.ID).Find(&items) + + orderItems := make([]types.OrderItem, 0, len(items)) + for _, item := range items { + orderItems = append(orderItems, 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, + }) + } + + payTime, shipTime, receiveTime := "", "", "" + if order.PayTime != nil { + payTime = order.PayTime.Format("2006-01-02 15:04:05") + } + if order.ShipTime != nil { + shipTime = order.ShipTime.Format("2006-01-02 15:04:05") + } + if order.ReceiveTime != nil { + receiveTime = order.ReceiveTime.Format("2006-01-02 15:04:05") + } + + resp.Orders = append(resp.Orders, 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, + PayMethod: order.PayMethod, + PayTime: payTime, + ShipTime: shipTime, + ReceiveTime: receiveTime, + ReceiverName: order.ReceiverName, + ReceiverPhone: order.ReceiverPhone, + ReceiverAddr: order.ReceiverAddr, + ShippingCompany: order.ShippingCompany, + TrackingNo: order.TrackingNo, + Remark: order.Remark, + CancelReason: order.CancelReason, + Items: orderItems, + CreatedAt: order.CreatedAt.Format("2006-01-02 15:04:05"), + }) + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_points_records_logic.go b/backend/healthapi/internal/logic/get_points_records_logic.go new file mode 100644 index 0000000..a00a663 --- /dev/null +++ b/backend/healthapi/internal/logic/get_points_records_logic.go @@ -0,0 +1,67 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetPointsRecordsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetPointsRecordsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPointsRecordsLogic { + return &GetPointsRecordsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetPointsRecordsLogic) GetPointsRecords(req *types.PageReq) (resp *types.PointsRecordsResp, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + var total int64 + l.svcCtx.DB.Model(&model.PointsRecord{}).Where("user_id = ?", userID).Count(&total) + + var records []model.PointsRecord + offset := (req.Page - 1) * req.PageSize + l.svcCtx.DB.Where("user_id = ?", userID). + Order("created_at DESC"). + Offset(offset). + Limit(req.PageSize). + Find(&records) + + resp = &types.PointsRecordsResp{ + Records: make([]types.PointsRecord, 0, len(records)), + PageInfo: types.PageInfo{ + Total: total, + Page: req.Page, + PageSize: req.PageSize, + }, + } + + for _, r := range records { + resp.Records = append(resp.Records, types.PointsRecord{ + ID: uint(r.ID), + Type: r.Type, + Points: r.Points, + Balance: r.Balance, + Source: r.Source, + Remark: r.Remark, + CreatedAt: r.CreatedAt.Format("2006-01-02 15:04:05"), + }) + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_product_list_logic.go b/backend/healthapi/internal/logic/get_product_list_logic.go new file mode 100644 index 0000000..59a8538 --- /dev/null +++ b/backend/healthapi/internal/logic/get_product_list_logic.go @@ -0,0 +1,65 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetProductListLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetProductListLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetProductListLogic { + return &GetProductListLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetProductListLogic) GetProductList(req *types.ProductListReq) (resp *types.ProductListResp, err error) { + var products []model.Product + var total int64 + + query := l.svcCtx.DB.Model(&model.Product{}).Where("is_active = ?", true) + if req.Category != "" { + query = query.Where("category = ?", req.Category) + } + + query.Count(&total) + offset := (req.Page - 1) * req.PageSize + query.Offset(offset).Limit(req.PageSize).Find(&products) + + resp = &types.ProductListResp{ + Products: make([]types.Product, 0, len(products)), + PageInfo: types.PageInfo{ + Total: total, + Page: req.Page, + PageSize: req.PageSize, + }, + } + + for _, p := range products { + resp.Products = append(resp.Products, types.Product{ + ID: uint(p.ID), + Name: p.Name, + Category: p.Category, + Description: p.Description, + Efficacy: p.Efficacy, + Suitable: p.Suitable, + Price: p.Price, + ImageURL: p.ImageURL, + MallURL: p.MallURL, + IsActive: p.IsActive, + }) + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_product_logic.go b/backend/healthapi/internal/logic/get_product_logic.go new file mode 100644 index 0000000..e01806d --- /dev/null +++ b/backend/healthapi/internal/logic/get_product_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 GetProductLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetProductLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetProductLogic { + return &GetProductLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetProductLogic) GetProduct(req *types.IdPathReq) (resp *types.Product, err error) { + var product model.Product + if err := l.svcCtx.DB.First(&product, req.Id).Error; err != nil { + return nil, errorx.ErrNotFound + } + + return &types.Product{ + ID: uint(product.ID), + Name: product.Name, + Category: product.Category, + Description: product.Description, + Efficacy: product.Efficacy, + Suitable: product.Suitable, + Price: product.Price, + ImageURL: product.ImageURL, + MallURL: product.MallURL, + IsActive: product.IsActive, + }, nil +} diff --git a/backend/healthapi/internal/logic/get_products_by_category_logic.go b/backend/healthapi/internal/logic/get_products_by_category_logic.go new file mode 100644 index 0000000..11e2be5 --- /dev/null +++ b/backend/healthapi/internal/logic/get_products_by_category_logic.go @@ -0,0 +1,65 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetProductsByCategoryLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetProductsByCategoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetProductsByCategoryLogic { + return &GetProductsByCategoryLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetProductsByCategoryLogic) GetProductsByCategory(req *types.ProductListReq) (resp *types.ProductListResp, err error) { + var products []model.Product + var total int64 + + query := l.svcCtx.DB.Model(&model.Product{}).Where("is_active = ?", true) + if req.Category != "" { + query = query.Where("category = ?", req.Category) + } + + query.Count(&total) + offset := (req.Page - 1) * req.PageSize + query.Offset(offset).Limit(req.PageSize).Find(&products) + + resp = &types.ProductListResp{ + Products: make([]types.Product, 0, len(products)), + PageInfo: types.PageInfo{ + Total: total, + Page: req.Page, + PageSize: req.PageSize, + }, + } + + for _, p := range products { + resp.Products = append(resp.Products, types.Product{ + ID: uint(p.ID), + Name: p.Name, + Category: p.Category, + Description: p.Description, + Efficacy: p.Efficacy, + Suitable: p.Suitable, + Price: p.Price, + ImageURL: p.ImageURL, + MallURL: p.MallURL, + IsActive: p.IsActive, + }) + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_purchase_history_logic.go b/backend/healthapi/internal/logic/get_purchase_history_logic.go new file mode 100644 index 0000000..f9934e2 --- /dev/null +++ b/backend/healthapi/internal/logic/get_purchase_history_logic.go @@ -0,0 +1,65 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetPurchaseHistoryLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetPurchaseHistoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetPurchaseHistoryLogic { + return &GetPurchaseHistoryLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetPurchaseHistoryLogic) GetPurchaseHistory(req *types.PageReq) (resp *types.PurchaseHistoryResp, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + var histories []model.PurchaseHistory + var total int64 + + query := l.svcCtx.DB.Model(&model.PurchaseHistory{}).Where("user_id = ?", userID) + query.Count(&total) + + offset := (req.Page - 1) * req.PageSize + query.Order("purchased_at DESC").Offset(offset).Limit(req.PageSize).Find(&histories) + + resp = &types.PurchaseHistoryResp{ + History: make([]types.PurchaseHistory, 0, len(histories)), + PageInfo: types.PageInfo{ + Total: total, + Page: req.Page, + PageSize: req.PageSize, + }, + } + + for _, h := range histories { + resp.History = append(resp.History, types.PurchaseHistory{ + ID: uint(h.ID), + UserID: h.UserID, + OrderNo: h.OrderNo, + ProductID: h.ProductID, + ProductName: h.ProductName, + PurchasedAt: h.PurchasedAt.Format("2006-01-02 15:04:05"), + Source: h.Source, + }) + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_questions_logic.go b/backend/healthapi/internal/logic/get_questions_logic.go new file mode 100644 index 0000000..249b6cf --- /dev/null +++ b/backend/healthapi/internal/logic/get_questions_logic.go @@ -0,0 +1,50 @@ +package logic + +import ( + "context" + "encoding/json" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetQuestionsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetQuestionsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetQuestionsLogic { + return &GetQuestionsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetQuestionsLogic) GetQuestions() (resp *types.QuestionsResp, err error) { + var questions []model.QuestionBank + l.svcCtx.DB.Order("constitution_type, order_num").Find(&questions) + + resp = &types.QuestionsResp{ + Questions: make([]types.Question, 0, len(questions)), + } + + for _, q := range questions { + var options []string + json.Unmarshal([]byte(q.Options), &options) + + resp.Questions = append(resp.Questions, types.Question{ + ID: int(q.ID), + ConstitutionType: q.ConstitutionType, + QuestionText: q.QuestionText, + Options: options, + OrderNum: q.OrderNum, + }) + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_recommend_products_logic.go b/backend/healthapi/internal/logic/get_recommend_products_logic.go new file mode 100644 index 0000000..0d02c41 --- /dev/null +++ b/backend/healthapi/internal/logic/get_recommend_products_logic.go @@ -0,0 +1,80 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetRecommendProductsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetRecommendProductsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRecommendProductsLogic { + return &GetRecommendProductsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetRecommendProductsLogic) GetRecommendProducts() (resp *types.RecommendProductsResp, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + // 获取用户最新体质 + var assessment model.ConstitutionAssessment + if err := l.svcCtx.DB.Where("user_id = ?", userID).Order("assessed_at DESC").First(&assessment).Error; err != nil { + return nil, errorx.NewCodeError(errorx.CodeNotFound, "请先完成体质测评") + } + + constitution := assessment.PrimaryConstitution + + // 获取该体质推荐的产品 + var constitutionProducts []model.ConstitutionProduct + l.svcCtx.DB.Where("constitution_type = ?", constitution).Order("priority DESC").Find(&constitutionProducts) + + var productIDs []uint + reasons := make(map[uint]string) + for _, cp := range constitutionProducts { + productIDs = append(productIDs, cp.ProductID) + reasons[cp.ProductID] = cp.Reason + } + + var products []model.Product + if len(productIDs) > 0 { + l.svcCtx.DB.Where("id IN ? AND is_active = ?", productIDs, true).Find(&products) + } + + resp = &types.RecommendProductsResp{ + Products: make([]types.Product, 0, len(products)), + Constitution: constitution, + Reason: model.ConstitutionNames[constitution] + "适用产品推荐", + } + + for _, p := range products { + resp.Products = append(resp.Products, types.Product{ + ID: uint(p.ID), + Name: p.Name, + Category: p.Category, + Description: p.Description, + Efficacy: p.Efficacy, + Suitable: p.Suitable, + Price: p.Price, + ImageURL: p.ImageURL, + MallURL: p.MallURL, + IsActive: p.IsActive, + }) + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_recommendations_logic.go b/backend/healthapi/internal/logic/get_recommendations_logic.go new file mode 100644 index 0000000..aa36121 --- /dev/null +++ b/backend/healthapi/internal/logic/get_recommendations_logic.go @@ -0,0 +1,47 @@ +package logic + +import ( + "context" + "encoding/json" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetRecommendationsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetRecommendationsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetRecommendationsLogic { + return &GetRecommendationsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetRecommendationsLogic) GetRecommendations() (resp *types.RecommendationsResp, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + var assessment model.ConstitutionAssessment + if err := l.svcCtx.DB.Where("user_id = ?", userID).Order("assessed_at DESC").First(&assessment).Error; err != nil { + return nil, errorx.NewCodeError(errorx.CodeNotFound, "暂无体质测评记录") + } + + var recommendations map[string]string + json.Unmarshal([]byte(assessment.Recommendations), &recommendations) + + return &types.RecommendationsResp{ + Constitution: assessment.PrimaryConstitution, + Recommendations: recommendations, + }, nil +} diff --git a/backend/healthapi/internal/logic/get_survey_status_logic.go b/backend/healthapi/internal/logic/get_survey_status_logic.go new file mode 100644 index 0000000..9b407cd --- /dev/null +++ b/backend/healthapi/internal/logic/get_survey_status_logic.go @@ -0,0 +1,55 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetSurveyStatusLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetSurveyStatusLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetSurveyStatusLogic { + return &GetSurveyStatusLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetSurveyStatusLogic) GetSurveyStatus() (resp *types.SurveyStatusResp, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + resp = &types.SurveyStatusResp{} + + // 检查基础信息 + var profile model.HealthProfile + if err := l.svcCtx.DB.Where("user_id = ?", userID).First(&profile).Error; err == nil { + resp.BasicInfo = true + resp.MedicalHistory = true + resp.FamilyHistory = true + resp.Allergy = true + } + + // 检查生活习惯 + var lifestyle model.LifestyleInfo + if err := l.svcCtx.DB.Where("user_id = ?", userID).First(&lifestyle).Error; err == nil { + resp.Lifestyle = true + } + + // 检查是否全部完成 + resp.Completed = resp.BasicInfo && resp.Lifestyle + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/get_user_profile_logic.go b/backend/healthapi/internal/logic/get_user_profile_logic.go new file mode 100644 index 0000000..af30c79 --- /dev/null +++ b/backend/healthapi/internal/logic/get_user_profile_logic.go @@ -0,0 +1,75 @@ +package logic + +import ( + "context" + "encoding/json" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type GetUserProfileLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewGetUserProfileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GetUserProfileLogic { + return &GetUserProfileLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *GetUserProfileLogic) GetUserProfile() (resp *types.UserInfo, err error) { + // 从 context 获取用户ID(JWT中间件设置) + 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 + } + + return &types.UserInfo{ + ID: uint(user.ID), + Phone: user.Phone, + Email: user.Email, + Nickname: user.Nickname, + Avatar: user.Avatar, + SurveyCompleted: user.SurveyCompleted, + }, nil +} + +// GetUserIDFromCtx 从上下文获取用户ID +func GetUserIDFromCtx(ctx context.Context) (uint, error) { + // go-zero JWT 中间件将用户信息存储在 context 中 + val := ctx.Value("user_id") + if val == nil { + return 0, errorx.ErrUnauthorized + } + + // 根据 JWT payload 类型进行转换 + switch v := val.(type) { + case float64: + return uint(v), nil + case int64: + return uint(v), nil + case int: + return uint(v), nil + case uint: + return v, nil + case json.Number: + id, _ := v.Int64() + return uint(id), nil + default: + return 0, errorx.ErrUnauthorized + } +} diff --git a/backend/healthapi/internal/logic/health_check_logic.go b/backend/healthapi/internal/logic/health_check_logic.go new file mode 100644 index 0000000..9ae86bb --- /dev/null +++ b/backend/healthapi/internal/logic/health_check_logic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "healthapi/internal/svc" + "healthapi/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type HealthCheckLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewHealthCheckLogic(ctx context.Context, svcCtx *svc.ServiceContext) *HealthCheckLogic { + return &HealthCheckLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *HealthCheckLogic) HealthCheck() (resp *types.CommonResp, err error) { + return &types.CommonResp{ + Code: 0, + Message: "ok", + }, nil +} diff --git a/backend/healthapi/internal/logic/login_logic.go b/backend/healthapi/internal/logic/login_logic.go new file mode 100644 index 0000000..185b5db --- /dev/null +++ b/backend/healthapi/internal/logic/login_logic.go @@ -0,0 +1,74 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + "healthapi/pkg/jwt" + + "github.com/zeromicro/go-zero/core/logx" + "golang.org/x/crypto/bcrypt" +) + +type LoginLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewLoginLogic(ctx context.Context, svcCtx *svc.ServiceContext) *LoginLogic { + return &LoginLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err error) { + var user model.User + + // 通过手机号或邮箱查找用户 + if req.Phone != "" { + if err := l.svcCtx.DB.Where("phone = ?", req.Phone).First(&user).Error; err != nil { + return nil, errorx.ErrUserNotFound + } + } else if req.Email != "" { + if err := l.svcCtx.DB.Where("email = ?", req.Email).First(&user).Error; err != nil { + return nil, errorx.ErrUserNotFound + } + } else { + return nil, errorx.NewCodeError(errorx.CodeBadRequest, "请提供手机号或邮箱") + } + + // 验证密码 + if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { + return nil, errorx.ErrWrongPassword + } + + // 生成 Token + token, expiresAt, err := jwt.GenerateToken( + user.ID, + l.svcCtx.Config.Auth.AccessSecret, + l.svcCtx.Config.Auth.AccessExpire, + ) + if err != nil { + l.Errorf("Failed to generate token: %v", err) + return nil, errorx.NewCodeError(errorx.CodeServerError, "生成Token失败") + } + + return &types.LoginResp{ + Token: token, + ExpiresAt: expiresAt, + User: types.UserInfo{ + ID: uint(user.ID), + Phone: user.Phone, + Email: user.Email, + Nickname: user.Nickname, + Avatar: user.Avatar, + SurveyCompleted: user.SurveyCompleted, + }, + }, nil +} diff --git a/backend/healthapi/internal/logic/pay_order_logic.go b/backend/healthapi/internal/logic/pay_order_logic.go new file mode 100644 index 0000000..1efd6f7 --- /dev/null +++ b/backend/healthapi/internal/logic/pay_order_logic.go @@ -0,0 +1,116 @@ +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 PayOrderLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewPayOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PayOrderLogic { + return &PayOrderLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *PayOrderLogic) PayOrder(req *types.PayOrderReq) (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, "订单状态不正确") + } + + // 模拟支付(实际应调用支付网关) + now := time.Now() + err = l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { + // 更新订单状态 + if err := tx.Model(&order).Updates(map[string]interface{}{ + "status": model.OrderStatusPaid, + "pay_method": req.PayMethod, + "pay_time": now, + }).Error; err != nil { + return err + } + + // 更新用户累计消费和会员等级 + var user model.User + if err := tx.First(&user, userID).Error; err != nil { + return err + } + + newTotalSpent := user.TotalSpent + order.PayAmount + newLevel := model.GetMemberLevelBySpent(newTotalSpent) + + updates := map[string]interface{}{ + "total_spent": newTotalSpent, + "member_level": newLevel, + } + if user.MemberSince == nil { + updates["member_since"] = now + } + + if err := tx.Model(&user).Updates(updates).Error; err != nil { + return err + } + + // 增加获得积分 + if order.PointsEarned > 0 { + if err := tx.Model(&user).Update("points", gorm.Expr("points + ?", order.PointsEarned)).Error; err != nil { + return err + } + expireAt := now.AddDate(1, 0, 0) // 积分1年后过期 + tx.Create(&model.PointsRecord{ + UserID: userID, + Type: model.PointsTypeEarn, + Points: order.PointsEarned, + Balance: user.Points + order.PointsEarned, + Source: model.PointsSourceOrder, + ReferenceID: uint(order.ID), + Remark: fmt.Sprintf("订单 %s 获得积分", order.OrderNo), + ExpireAt: &expireAt, + }) + } + + // 更新商品销量 + var items []model.OrderItem + tx.Where("order_id = ?", order.ID).Find(&items) + for _, item := range items { + tx.Model(&model.Product{}).Where("id = ?", item.ProductID). + Update("sales_count", gorm.Expr("sales_count + ?", item.Quantity)) + } + + return nil + }) + + if err != nil { + return nil, err + } + + return &types.CommonResp{ + Code: 0, + Message: "支付成功", + }, nil +} diff --git a/backend/healthapi/internal/logic/preview_order_logic.go b/backend/healthapi/internal/logic/preview_order_logic.go new file mode 100644 index 0000000..c60915b --- /dev/null +++ b/backend/healthapi/internal/logic/preview_order_logic.go @@ -0,0 +1,129 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type PreviewOrderLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewPreviewOrderLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PreviewOrderLogic { + return &PreviewOrderLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *PreviewOrderLogic) PreviewOrder(req *types.OrderPreviewReq) (resp *types.OrderPreview, 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 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 + respItems := make([]types.CartItem, 0, len(cartItems)) + + for _, cartItem := range cartItems { + var product model.Product + if err := l.svcCtx.DB.First(&product, cartItem.ProductID).Error; err != nil { + continue + } + + 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 { + price = sku.Price + stock = sku.Stock + skuName = sku.Name + if sku.Image != "" { + image = sku.Image + } + } + } + + itemTotal := price * float64(cartItem.Quantity) + totalAmount += itemTotal + + respItems = append(respItems, 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: true, + Stock: stock, + }) + } + + // 计算会员折扣 + discountAmount := totalAmount * (1 - levelConfig.Discount) + + // 计算运费 + shippingFee := float64(0) + afterDiscount := totalAmount - discountAmount + if afterDiscount < levelConfig.FreeShippingMin { + shippingFee = 10 // 默认运费 + } + + // 计算最大可用积分(最多抵扣20%) + maxPointsDiscount := afterDiscount * 0.2 + maxPointsUse := int(maxPointsDiscount * 100) + if maxPointsUse > user.Points { + maxPointsUse = user.Points + } + pointsDiscount := float64(maxPointsUse) / 100 + + // 最终支付金额 + payAmount := afterDiscount + shippingFee + + resp = &types.OrderPreview{ + Items: respItems, + TotalAmount: totalAmount, + DiscountAmount: discountAmount, + ShippingFee: shippingFee, + PayAmount: payAmount, + MaxPointsUse: maxPointsUse, + PointsDiscount: pointsDiscount, + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/refresh_token_logic.go b/backend/healthapi/internal/logic/refresh_token_logic.go new file mode 100644 index 0000000..465392d --- /dev/null +++ b/backend/healthapi/internal/logic/refresh_token_logic.go @@ -0,0 +1,65 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + "healthapi/pkg/jwt" + + "github.com/zeromicro/go-zero/core/logx" +) + +type RefreshTokenLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewRefreshTokenLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RefreshTokenLogic { + return &RefreshTokenLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *RefreshTokenLogic) RefreshToken(req *types.RefreshTokenReq) (resp *types.LoginResp, err error) { + // 解析旧 Token 获取用户ID + claims, err := jwt.ParseToken(req.Token, l.svcCtx.Config.Auth.AccessSecret) + if err != nil { + return nil, errorx.ErrInvalidToken + } + + // 查询用户信息 + var user model.User + if err := l.svcCtx.DB.First(&user, claims.UserID).Error; err != nil { + return nil, errorx.ErrUserNotFound + } + + // 生成新 Token + token, expiresAt, err := jwt.GenerateToken( + user.ID, + l.svcCtx.Config.Auth.AccessSecret, + l.svcCtx.Config.Auth.AccessExpire, + ) + if err != nil { + l.Errorf("Failed to generate token: %v", err) + return nil, errorx.NewCodeError(errorx.CodeServerError, "生成Token失败") + } + + return &types.LoginResp{ + Token: token, + ExpiresAt: expiresAt, + User: types.UserInfo{ + ID: uint(user.ID), + Phone: user.Phone, + Email: user.Email, + Nickname: user.Nickname, + Avatar: user.Avatar, + SurveyCompleted: user.SurveyCompleted, + }, + }, nil +} diff --git a/backend/healthapi/internal/logic/register_logic.go b/backend/healthapi/internal/logic/register_logic.go new file mode 100644 index 0000000..b74bcab --- /dev/null +++ b/backend/healthapi/internal/logic/register_logic.go @@ -0,0 +1,95 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + "healthapi/pkg/jwt" + + "github.com/zeromicro/go-zero/core/logx" + "golang.org/x/crypto/bcrypt" +) + +type RegisterLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewRegisterLogic(ctx context.Context, svcCtx *svc.ServiceContext) *RegisterLogic { + return &RegisterLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *RegisterLogic) Register(req *types.RegisterReq) (resp *types.LoginResp, err error) { + // 检查手机号或邮箱是否已注册 + var existingUser model.User + if req.Phone != "" { + l.svcCtx.DB.Where("phone = ?", req.Phone).First(&existingUser) + if existingUser.ID > 0 { + return nil, errorx.NewCodeError(errorx.CodeUserExists, "手机号已注册") + } + } + if req.Email != "" { + l.svcCtx.DB.Where("email = ?", req.Email).First(&existingUser) + if existingUser.ID > 0 { + return nil, errorx.NewCodeError(errorx.CodeUserExists, "邮箱已注册") + } + } + + // 加密密码 + hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) + if err != nil { + l.Errorf("Failed to hash password: %v", err) + return nil, errorx.ErrServerError + } + + // 创建用户 + user := model.User{ + Phone: req.Phone, + Email: req.Email, + PasswordHash: string(hash), + } + + // 默认昵称 + if req.Phone != "" && len(req.Phone) >= 4 { + user.Nickname = "用户" + req.Phone[len(req.Phone)-4:] + } else if req.Email != "" { + user.Nickname = "用户" + } + + if err := l.svcCtx.DB.Create(&user).Error; err != nil { + l.Errorf("Failed to create user: %v", err) + return nil, errorx.NewCodeError(errorx.CodeServerError, "创建用户失败") + } + + // 生成 Token + token, expiresAt, err := jwt.GenerateToken( + user.ID, + l.svcCtx.Config.Auth.AccessSecret, + l.svcCtx.Config.Auth.AccessExpire, + ) + if err != nil { + l.Errorf("Failed to generate token: %v", err) + return nil, errorx.NewCodeError(errorx.CodeServerError, "生成Token失败") + } + + return &types.LoginResp{ + Token: token, + ExpiresAt: expiresAt, + User: types.UserInfo{ + ID: uint(user.ID), + Phone: user.Phone, + Email: user.Email, + Nickname: user.Nickname, + Avatar: user.Avatar, + SurveyCompleted: user.SurveyCompleted, + }, + }, nil +} diff --git a/backend/healthapi/internal/logic/search_mall_products_logic.go b/backend/healthapi/internal/logic/search_mall_products_logic.go new file mode 100644 index 0000000..7db267e --- /dev/null +++ b/backend/healthapi/internal/logic/search_mall_products_logic.go @@ -0,0 +1,68 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type SearchMallProductsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewSearchMallProductsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SearchMallProductsLogic { + return &SearchMallProductsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SearchMallProductsLogic) SearchMallProducts(req *types.ProductSearchReq) (resp *types.ProductListResp, err error) { + query := l.svcCtx.DB.Model(&model.Product{}).Where("is_active = ?", true) + + if req.Keyword != "" { + keyword := "%" + req.Keyword + "%" + query = query.Where("name LIKE ? OR description LIKE ? OR efficacy LIKE ? OR health_tags LIKE ?", + keyword, keyword, keyword, keyword) + } + + var total int64 + query.Count(&total) + + var products []model.Product + offset := (req.Page - 1) * req.PageSize + query.Order("sales_count DESC").Offset(offset).Limit(req.PageSize).Find(&products) + + resp = &types.ProductListResp{ + Products: make([]types.Product, 0, len(products)), + PageInfo: types.PageInfo{ + Total: total, + Page: req.Page, + PageSize: req.PageSize, + }, + } + + for _, p := range products { + resp.Products = append(resp.Products, types.Product{ + ID: uint(p.ID), + Name: p.Name, + Category: p.Category, + Description: p.Description, + Efficacy: p.Efficacy, + Suitable: p.Suitable, + Price: p.Price, + ImageURL: p.MainImage, + MallURL: p.MallURL, + IsActive: p.IsActive, + }) + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/search_products_logic.go b/backend/healthapi/internal/logic/search_products_logic.go new file mode 100644 index 0000000..6ce7b7e --- /dev/null +++ b/backend/healthapi/internal/logic/search_products_logic.go @@ -0,0 +1,66 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type SearchProductsLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewSearchProductsLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SearchProductsLogic { + return &SearchProductsLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SearchProductsLogic) SearchProducts(req *types.ProductSearchReq) (resp *types.ProductListResp, err error) { + var products []model.Product + var total int64 + + query := l.svcCtx.DB.Model(&model.Product{}).Where("is_active = ?", true) + if req.Keyword != "" { + keyword := "%" + req.Keyword + "%" + query = query.Where("name LIKE ? OR description LIKE ? OR efficacy LIKE ?", keyword, keyword, keyword) + } + + query.Count(&total) + offset := (req.Page - 1) * req.PageSize + query.Offset(offset).Limit(req.PageSize).Find(&products) + + resp = &types.ProductListResp{ + Products: make([]types.Product, 0, len(products)), + PageInfo: types.PageInfo{ + Total: total, + Page: req.Page, + PageSize: req.PageSize, + }, + } + + for _, p := range products { + resp.Products = append(resp.Products, types.Product{ + ID: uint(p.ID), + Name: p.Name, + Category: p.Category, + Description: p.Description, + Efficacy: p.Efficacy, + Suitable: p.Suitable, + Price: p.Price, + ImageURL: p.ImageURL, + MallURL: p.MallURL, + IsActive: p.IsActive, + }) + } + + return resp, nil +} diff --git a/backend/healthapi/internal/logic/send_code_logic.go b/backend/healthapi/internal/logic/send_code_logic.go new file mode 100644 index 0000000..6aa2c3a --- /dev/null +++ b/backend/healthapi/internal/logic/send_code_logic.go @@ -0,0 +1,33 @@ +package logic + +import ( + "context" + + "healthapi/internal/svc" + "healthapi/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type SendCodeLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewSendCodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendCodeLogic { + return &SendCodeLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SendCodeLogic) SendCode(req *types.SendCodeReq) (resp *types.CommonResp, err error) { + // TODO: 实现短信/邮件验证码发送 + // 这里暂时返回成功,实际应该调用短信/邮件服务 + return &types.CommonResp{ + Code: 0, + Message: "验证码已发送", + }, nil +} diff --git a/backend/healthapi/internal/logic/send_message_logic.go b/backend/healthapi/internal/logic/send_message_logic.go new file mode 100644 index 0000000..1844407 --- /dev/null +++ b/backend/healthapi/internal/logic/send_message_logic.go @@ -0,0 +1,138 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/ai" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" + "gorm.io/gorm" +) + +type SendMessageLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewSendMessageLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendMessageLogic { + return &SendMessageLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SendMessageLogic) SendMessage(req *types.SendMessageReq) (resp *types.MessageResp, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + // 验证对话属于该用户 + var conversation model.Conversation + if err := l.svcCtx.DB.Where("id = ? AND user_id = ?", req.Id, userID).First(&conversation).Error; err != nil { + return nil, errorx.ErrNotFound + } + + // 保存用户消息 + userMessage := model.Message{ + ConversationID: conversation.ID, + Role: model.RoleUser, + Content: req.Content, + } + if err := l.svcCtx.DB.Create(&userMessage).Error; err != nil { + return nil, errorx.ErrServerError + } + + // 获取历史消息构建上下文 + var historyMessages []model.Message + l.svcCtx.DB.Where("conversation_id = ?", conversation.ID). + Order("created_at DESC"). + Limit(l.svcCtx.Config.AI.MaxHistoryMessages). + Find(&historyMessages) + + // 获取用户健康档案构建系统提示 + systemPrompt := buildSystemPrompt(l.svcCtx.DB, 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, + }) + } + + // 调用 AI 服务 + aiResponse, err := l.svcCtx.AIClient.Chat(l.ctx, aiMessages) + if err != nil { + l.Logger.Errorf("AI service error: %v", err) + aiResponse = "抱歉,AI 服务暂时不可用,请稍后再试。" + } + + // 保存 AI 回复 + assistantMessage := model.Message{ + ConversationID: conversation.ID, + Role: model.RoleAssistant, + Content: aiResponse, + } + if err := l.svcCtx.DB.Create(&assistantMessage).Error; err != nil { + return nil, errorx.ErrServerError + } + + // 更新对话标题 + if conversation.Title == "新对话" { + title := req.Content + if len(title) > 50 { + title = title[:50] + "..." + } + l.svcCtx.DB.Model(&conversation).Update("title", title) + } + + return &types.MessageResp{ + UserMessage: types.Message{ + ID: uint(userMessage.ID), + ConversationID: userMessage.ConversationID, + Role: userMessage.Role, + Content: userMessage.Content, + CreatedAt: userMessage.CreatedAt.Format("2006-01-02 15:04:05"), + }, + AssistantMessage: types.Message{ + ID: uint(assistantMessage.ID), + ConversationID: assistantMessage.ConversationID, + Role: assistantMessage.Role, + Content: assistantMessage.Content, + CreatedAt: assistantMessage.CreatedAt.Format("2006-01-02 15:04:05"), + }, + }, nil +} + +// buildSystemPrompt 构建系统提示(包含用户健康档案) +func buildSystemPrompt(db *gorm.DB, userID uint) string { + basePrompt := `你是一位专业的中医健康顾问,名叫"健康小助手"。你的职责是: +1. 根据用户的健康档案和体质特点,提供个性化的健康建议 +2. 解答用户关于中医养生、体质调理的问题 +3. 推荐适合用户体质的饮食、运动和生活方式 +4. 在必要时提醒用户寻求专业医疗帮助 + +请注意: +- 你的建议仅供参考,不能替代专业医疗诊断 +- 回答要简洁明了,通俗易懂 +- 适当使用中医术语,但要给出解释 +- 保持友好、专业的语气` + + // 获取用户体质信息 + var assessment model.ConstitutionAssessment + if err := db.Where("user_id = ?", userID).Order("assessed_at DESC").First(&assessment).Error; err == nil { + if assessment.PrimaryConstitution != "" { + basePrompt += "\n\n用户体质类型: " + model.ConstitutionNames[assessment.PrimaryConstitution] + } + } + + return basePrompt +} diff --git a/backend/healthapi/internal/logic/send_message_stream_logic.go b/backend/healthapi/internal/logic/send_message_stream_logic.go new file mode 100644 index 0000000..dc15447 --- /dev/null +++ b/backend/healthapi/internal/logic/send_message_stream_logic.go @@ -0,0 +1,31 @@ +package logic + +import ( + "context" + + "healthapi/internal/svc" + "healthapi/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type SendMessageStreamLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewSendMessageStreamLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SendMessageStreamLogic { + return &SendMessageStreamLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SendMessageStreamLogic) SendMessageStream(req *types.SendMessageReq) error { + // SSE 流式响应需要在 Handler 中直接处理 + // 这里只是占位,实际逻辑在 Handler 中实现 + // 参考原项目的 conversation.go SendMessageStream 实现 + return nil +} diff --git a/backend/healthapi/internal/logic/set_default_address_logic.go b/backend/healthapi/internal/logic/set_default_address_logic.go new file mode 100644 index 0000000..f1447d9 --- /dev/null +++ b/backend/healthapi/internal/logic/set_default_address_logic.go @@ -0,0 +1,50 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type SetDefaultAddressLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewSetDefaultAddressLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SetDefaultAddressLogic { + return &SetDefaultAddressLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SetDefaultAddressLogic) SetDefaultAddress(req *types.AddressIdReq) (resp *types.CommonResp, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + // 验证地址存在 + var addr model.Address + if err := l.svcCtx.DB.Where("id = ? AND user_id = ?", req.Id, userID).First(&addr).Error; err != nil { + return nil, errorx.NewCodeError(404, "地址不存在") + } + + // 取消所有默认地址 + l.svcCtx.DB.Model(&model.Address{}).Where("user_id = ?", userID).Update("is_default", false) + + // 设置新的默认地址 + l.svcCtx.DB.Model(&addr).Update("is_default", true) + + return &types.CommonResp{ + Code: 0, + Message: "设置成功", + }, nil +} diff --git a/backend/healthapi/internal/logic/submit_allergy_logic.go b/backend/healthapi/internal/logic/submit_allergy_logic.go new file mode 100644 index 0000000..b6ab188 --- /dev/null +++ b/backend/healthapi/internal/logic/submit_allergy_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 SubmitAllergyLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewSubmitAllergyLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SubmitAllergyLogic { + return &SubmitAllergyLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SubmitAllergyLogic) SubmitAllergy(req *types.SubmitAllergyReq) (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, "请先填写基础信息") + } + + record := model.AllergyRecord{ + HealthProfileID: profile.ID, + AllergyType: req.AllergyType, + Allergen: req.Allergen, + Severity: req.Severity, + ReactionDesc: req.ReactionDesc, + } + + if err := l.svcCtx.DB.Create(&record).Error; err != nil { + return nil, errorx.ErrServerError + } + + return &types.CommonResp{Code: 0, Message: "提交成功"}, nil +} diff --git a/backend/healthapi/internal/logic/submit_assessment_logic.go b/backend/healthapi/internal/logic/submit_assessment_logic.go new file mode 100644 index 0000000..8cf2f1f --- /dev/null +++ b/backend/healthapi/internal/logic/submit_assessment_logic.go @@ -0,0 +1,194 @@ +package logic + +import ( + "context" + "encoding/json" + "sort" + "time" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type SubmitAssessmentLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewSubmitAssessmentLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SubmitAssessmentLogic { + return &SubmitAssessmentLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +type constitutionScore struct { + Type string `json:"type"` + Name string `json:"name"` + Score float64 `json:"score"` + Description string `json:"description"` +} + +func (l *SubmitAssessmentLogic) SubmitAssessment(req *types.SubmitAssessmentReq) (resp *types.AssessmentResult, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + // 获取所有问题 + var questions []model.QuestionBank + l.svcCtx.DB.Find(&questions) + if len(questions) == 0 { + return nil, errorx.NewCodeError(errorx.CodeServerError, "问卷题库为空") + } + + // 构建问题ID到体质类型的映射 + questionTypeMap := make(map[uint]string) + typeQuestionCount := make(map[string]int) + for _, q := range questions { + questionTypeMap[q.ID] = q.ConstitutionType + typeQuestionCount[q.ConstitutionType]++ + } + + // 计算各体质原始分 + typeScores := make(map[string]int) + for _, answer := range req.Answers { + if cType, ok := questionTypeMap[uint(answer.QuestionID)]; ok { + typeScores[cType] += answer.Score + } + } + + // 计算转化分 + allScores := make([]constitutionScore, 0) + for cType, rawScore := range typeScores { + questionCount := typeQuestionCount[cType] + if questionCount == 0 { + continue + } + transformedScore := float64(rawScore-questionCount) / float64(questionCount*4) * 100 + if transformedScore < 0 { + transformedScore = 0 + } + if transformedScore > 100 { + transformedScore = 100 + } + + allScores = append(allScores, constitutionScore{ + Type: cType, + Name: model.ConstitutionNames[cType], + Score: transformedScore, + Description: model.ConstitutionDescriptions[cType], + }) + } + + // 按分数排序 + sort.Slice(allScores, func(i, j int) bool { + return allScores[i].Score > allScores[j].Score + }) + + // 判定主要体质和次要体质 + var primary constitutionScore + var secondary []constitutionScore + + pingheScore := float64(0) + otherMax := float64(0) + for _, score := range allScores { + if score.Type == model.ConstitutionPinghe { + pingheScore = score.Score + } else if score.Score > otherMax { + otherMax = score.Score + } + } + + if pingheScore >= 60 && otherMax < 30 { + for _, score := range allScores { + if score.Type == model.ConstitutionPinghe { + primary = score + break + } + } + } else { + for _, score := range allScores { + if score.Type == model.ConstitutionPinghe { + continue + } + if primary.Type == "" && score.Score >= 40 { + primary = score + } else if score.Score >= 30 { + secondary = append(secondary, score) + } + } + if primary.Type == "" { + for _, score := range allScores { + if score.Type != model.ConstitutionPinghe { + primary = score + break + } + } + } + } + + // 获取调养建议 + recommendations := make(map[string]string) + if primary.Type != "" { + if recs, ok := model.ConstitutionRecommendations[primary.Type]; ok { + for k, v := range recs { + recommendations[k] = v + } + } + } + + // 保存评估结果 + scoresJSON, _ := json.Marshal(allScores) + secondaryTypes := make([]string, len(secondary)) + for i, s := range secondary { + secondaryTypes[i] = s.Type + } + secondaryJSON, _ := json.Marshal(secondaryTypes) + recsJSON, _ := json.Marshal(recommendations) + + assessment := model.ConstitutionAssessment{ + UserID: userID, + AssessedAt: time.Now(), + Scores: string(scoresJSON), + PrimaryConstitution: primary.Type, + SecondaryConstitutions: string(secondaryJSON), + Recommendations: string(recsJSON), + } + if err := l.svcCtx.DB.Create(&assessment).Error; err != nil { + return nil, errorx.ErrServerError + } + + // 保存答案记录 + answers := make([]model.AssessmentAnswer, len(req.Answers)) + for i, a := range req.Answers { + answers[i] = model.AssessmentAnswer{ + AssessmentID: assessment.ID, + QuestionID: uint(a.QuestionID), + Score: a.Score, + } + } + l.svcCtx.DB.Create(&answers) + + // 构建响应 + // 转换 allScores 为 map[string]float64 + scoresMap := make(map[string]float64) + for _, s := range allScores { + scoresMap[s.Type] = s.Score + } + + return &types.AssessmentResult{ + ID: uint(assessment.ID), + AssessedAt: assessment.AssessedAt.Format(time.RFC3339), + Scores: scoresMap, + PrimaryConstitution: primary.Type, + SecondaryConstitutions: secondaryTypes, + Recommendations: recommendations, + }, nil +} diff --git a/backend/healthapi/internal/logic/submit_basic_info_logic.go b/backend/healthapi/internal/logic/submit_basic_info_logic.go new file mode 100644 index 0000000..15c9359 --- /dev/null +++ b/backend/healthapi/internal/logic/submit_basic_info_logic.go @@ -0,0 +1,73 @@ +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 SubmitBasicInfoLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewSubmitBasicInfoLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SubmitBasicInfoLogic { + return &SubmitBasicInfoLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SubmitBasicInfoLogic) SubmitBasicInfo(req *types.SubmitBasicInfoReq) (resp *types.CommonResp, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + // 计算 BMI + heightM := req.Height / 100 + bmi := req.Weight / (heightM * heightM) + + profile := model.HealthProfile{ + UserID: userID, + Name: req.Name, + Gender: req.Gender, + Height: req.Height, + Weight: req.Weight, + BMI: bmi, + BloodType: req.BloodType, + Occupation: req.Occupation, + MaritalStatus: req.MaritalStatus, + Region: req.Region, + } + + if req.BirthDate != "" { + if birthDate, err := time.Parse("2006-01-02", req.BirthDate); err == nil { + profile.BirthDate = &birthDate + } + } + + // 检查是否已存在 + var existing model.HealthProfile + if err := l.svcCtx.DB.Where("user_id = ?", userID).First(&existing).Error; err == nil { + profile.ID = existing.ID + profile.CreatedAt = existing.CreatedAt + if err := l.svcCtx.DB.Save(&profile).Error; err != nil { + return nil, errorx.ErrServerError + } + } else { + if err := l.svcCtx.DB.Create(&profile).Error; err != nil { + return nil, errorx.ErrServerError + } + } + + return &types.CommonResp{Code: 0, Message: "提交成功"}, nil +} diff --git a/backend/healthapi/internal/logic/submit_family_history_logic.go b/backend/healthapi/internal/logic/submit_family_history_logic.go new file mode 100644 index 0000000..2a7154c --- /dev/null +++ b/backend/healthapi/internal/logic/submit_family_history_logic.go @@ -0,0 +1,51 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type SubmitFamilyHistoryLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewSubmitFamilyHistoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SubmitFamilyHistoryLogic { + return &SubmitFamilyHistoryLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SubmitFamilyHistoryLogic) SubmitFamilyHistory(req *types.SubmitFamilyHistoryReq) (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, "请先填写基础信息") + } + + history := model.FamilyHistory{ + HealthProfileID: profile.ID, + Relation: req.Relation, + DiseaseName: req.DiseaseName, + Notes: req.Notes, + } + + if err := l.svcCtx.DB.Create(&history).Error; err != nil { + return nil, errorx.ErrServerError + } + + return &types.CommonResp{Code: 0, Message: "提交成功"}, nil +} diff --git a/backend/healthapi/internal/logic/submit_lifestyle_logic.go b/backend/healthapi/internal/logic/submit_lifestyle_logic.go new file mode 100644 index 0000000..cf46260 --- /dev/null +++ b/backend/healthapi/internal/logic/submit_lifestyle_logic.go @@ -0,0 +1,63 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type SubmitLifestyleLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewSubmitLifestyleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SubmitLifestyleLogic { + return &SubmitLifestyleLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SubmitLifestyleLogic) SubmitLifestyle(req *types.SubmitLifestyleReq) (resp *types.CommonResp, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + lifestyle := model.LifestyleInfo{ + UserID: userID, + SleepTime: req.SleepTime, + WakeTime: req.WakeTime, + SleepQuality: req.SleepQuality, + MealRegularity: req.MealRegularity, + DietPreference: req.DietPreference, + DailyWaterML: req.DailyWaterML, + ExerciseFrequency: req.ExerciseFrequency, + ExerciseType: req.ExerciseType, + ExerciseDurationMin: req.ExerciseDurationMin, + IsSmoker: req.IsSmoker, + AlcoholFrequency: req.AlcoholFrequency, + } + + var existing model.LifestyleInfo + if err := l.svcCtx.DB.Where("user_id = ?", userID).First(&existing).Error; err == nil { + lifestyle.ID = existing.ID + lifestyle.CreatedAt = existing.CreatedAt + if err := l.svcCtx.DB.Save(&lifestyle).Error; err != nil { + return nil, errorx.ErrServerError + } + } else { + if err := l.svcCtx.DB.Create(&lifestyle).Error; err != nil { + return nil, errorx.ErrServerError + } + } + + return &types.CommonResp{Code: 0, Message: "提交成功"}, nil +} diff --git a/backend/healthapi/internal/logic/submit_medical_history_logic.go b/backend/healthapi/internal/logic/submit_medical_history_logic.go new file mode 100644 index 0000000..9717fda --- /dev/null +++ b/backend/healthapi/internal/logic/submit_medical_history_logic.go @@ -0,0 +1,53 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type SubmitMedicalHistoryLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewSubmitMedicalHistoryLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SubmitMedicalHistoryLogic { + return &SubmitMedicalHistoryLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SubmitMedicalHistoryLogic) SubmitMedicalHistory(req *types.SubmitMedicalHistoryReq) (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, "请先填写基础信息") + } + + history := model.MedicalHistory{ + HealthProfileID: profile.ID, + DiseaseName: req.DiseaseName, + DiseaseType: req.DiseaseType, + DiagnosedDate: req.DiagnosedDate, + Status: req.Status, + Notes: req.Notes, + } + + if err := l.svcCtx.DB.Create(&history).Error; err != nil { + return nil, errorx.ErrServerError + } + + return &types.CommonResp{Code: 0, Message: "提交成功"}, nil +} diff --git a/backend/healthapi/internal/logic/sync_purchase_logic.go b/backend/healthapi/internal/logic/sync_purchase_logic.go new file mode 100644 index 0000000..a231b67 --- /dev/null +++ b/backend/healthapi/internal/logic/sync_purchase_logic.go @@ -0,0 +1,48 @@ +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 SyncPurchaseLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewSyncPurchaseLogic(ctx context.Context, svcCtx *svc.ServiceContext) *SyncPurchaseLogic { + return &SyncPurchaseLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *SyncPurchaseLogic) SyncPurchase(req *types.SyncPurchaseReq) (resp *types.CommonResp, err error) { + // TODO: 验证 API Key(从 Header 获取) + + purchasedAt, _ := time.Parse("2006-01-02 15:04:05", req.PurchasedAt) + + history := model.PurchaseHistory{ + UserID: req.UserID, + OrderNo: req.OrderNo, + ProductID: req.ProductID, + ProductName: req.ProductName, + PurchasedAt: purchasedAt, + Source: req.Source, + } + + if err := l.svcCtx.DB.Create(&history).Error; err != nil { + return nil, errorx.ErrServerError + } + + return &types.CommonResp{Code: 0, Message: "同步成功"}, nil +} diff --git a/backend/healthapi/internal/logic/update_address_logic.go b/backend/healthapi/internal/logic/update_address_logic.go new file mode 100644 index 0000000..b5b844d --- /dev/null +++ b/backend/healthapi/internal/logic/update_address_logic.go @@ -0,0 +1,30 @@ +package logic + +import ( + "context" + + "healthapi/internal/svc" + "healthapi/internal/types" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdateAddressLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpdateAddressLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateAddressLogic { + return &UpdateAddressLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateAddressLogic) UpdateAddress(req *types.AddressIdReq) error { + // todo: add your logic here and delete this line + + return nil +} diff --git a/backend/healthapi/internal/logic/update_cart_logic.go b/backend/healthapi/internal/logic/update_cart_logic.go new file mode 100644 index 0000000..9439fbb --- /dev/null +++ b/backend/healthapi/internal/logic/update_cart_logic.go @@ -0,0 +1,91 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdateCartLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpdateCartLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateCartLogic { + return &UpdateCartLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateCartLogic) UpdateCart(req *types.UpdateCartReq) (resp *types.CartItem, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + var cartItem model.CartItem + if err := l.svcCtx.DB.Where("id = ? AND user_id = ?", req.Id, userID).First(&cartItem).Error; err != nil { + return nil, errorx.NewCodeError(404, "购物车项不存在") + } + + // 获取商品信息 + var product model.Product + if err := l.svcCtx.DB.First(&product, cartItem.ProductID).Error; err != nil { + return nil, errorx.NewCodeError(404, "商品不存在") + } + + price := product.Price + stock := product.Stock + skuName := "" + image := product.MainImage + + // 如果有 SKU + if cartItem.SkuID > 0 { + var sku model.ProductSku + if err := l.svcCtx.DB.First(&sku, cartItem.SkuID).Error; err == nil { + price = sku.Price + stock = sku.Stock + skuName = sku.Name + if sku.Image != "" { + image = sku.Image + } + } + } + + // 更新数量 + if req.Quantity > 0 { + if req.Quantity > stock { + return nil, errorx.NewCodeError(400, "库存不足") + } + cartItem.Quantity = req.Quantity + } + + // 更新选中状态(使用结构体字段,因为 bool 默认值为 false) + cartItem.Selected = req.Selected + + if err := l.svcCtx.DB.Save(&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 +} diff --git a/backend/healthapi/internal/logic/update_health_profile_logic.go b/backend/healthapi/internal/logic/update_health_profile_logic.go new file mode 100644 index 0000000..34b0720 --- /dev/null +++ b/backend/healthapi/internal/logic/update_health_profile_logic.go @@ -0,0 +1,106 @@ +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 UpdateHealthProfileLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpdateHealthProfileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateHealthProfileLogic { + return &UpdateHealthProfileLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateHealthProfileLogic) UpdateHealthProfile(req *types.UpdateHealthProfileReq) (resp *types.HealthProfile, 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 { + profile = model.HealthProfile{UserID: userID} + } + + // 更新字段 + if req.Name != "" { + profile.Name = req.Name + } + if req.BirthDate != "" { + if parsedDate, err := time.Parse("2006-01-02", req.BirthDate); err == nil { + profile.BirthDate = &parsedDate + } + } + if req.Gender != "" { + profile.Gender = req.Gender + } + if req.Height > 0 { + profile.Height = req.Height + } + if req.Weight > 0 { + profile.Weight = req.Weight + if profile.Height > 0 { + heightM := profile.Height / 100 + profile.BMI = profile.Weight / (heightM * heightM) + } + } + if req.BloodType != "" { + profile.BloodType = req.BloodType + } + if req.Occupation != "" { + profile.Occupation = req.Occupation + } + if req.MaritalStatus != "" { + profile.MaritalStatus = req.MaritalStatus + } + if req.Region != "" { + profile.Region = req.Region + } + + // 保存 + if profile.ID == 0 { + if err := l.svcCtx.DB.Create(&profile).Error; err != nil { + return nil, errorx.ErrServerError + } + } else { + if err := l.svcCtx.DB.Save(&profile).Error; err != nil { + return nil, errorx.ErrServerError + } + } + + birthDate := "" + if profile.BirthDate != nil { + birthDate = profile.BirthDate.Format("2006-01-02") + } + + return &types.HealthProfile{ + ID: uint(profile.ID), + UserID: profile.UserID, + Name: profile.Name, + BirthDate: birthDate, + Gender: profile.Gender, + Height: profile.Height, + Weight: profile.Weight, + BMI: profile.BMI, + BloodType: profile.BloodType, + Occupation: profile.Occupation, + MaritalStatus: profile.MaritalStatus, + Region: profile.Region, + }, nil +} diff --git a/backend/healthapi/internal/logic/update_lifestyle_logic.go b/backend/healthapi/internal/logic/update_lifestyle_logic.go new file mode 100644 index 0000000..d5099a5 --- /dev/null +++ b/backend/healthapi/internal/logic/update_lifestyle_logic.go @@ -0,0 +1,98 @@ +package logic + +import ( + "context" + + "healthapi/internal/model" + "healthapi/internal/svc" + "healthapi/internal/types" + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/core/logx" +) + +type UpdateLifestyleLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpdateLifestyleLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateLifestyleLogic { + return &UpdateLifestyleLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateLifestyleLogic) UpdateLifestyle(req *types.UpdateLifestyleReq) (resp *types.LifestyleInfo, err error) { + userID, err := GetUserIDFromCtx(l.ctx) + if err != nil { + return nil, errorx.ErrUnauthorized + } + + var lifestyle model.LifestyleInfo + if err := l.svcCtx.DB.Where("user_id = ?", userID).First(&lifestyle).Error; err != nil { + lifestyle = model.LifestyleInfo{UserID: userID} + } + + // 更新字段 + if req.SleepTime != "" { + lifestyle.SleepTime = req.SleepTime + } + if req.WakeTime != "" { + lifestyle.WakeTime = req.WakeTime + } + if req.SleepQuality != "" { + lifestyle.SleepQuality = req.SleepQuality + } + if req.MealRegularity != "" { + lifestyle.MealRegularity = req.MealRegularity + } + if req.DietPreference != "" { + lifestyle.DietPreference = req.DietPreference + } + if req.DailyWaterML > 0 { + lifestyle.DailyWaterML = req.DailyWaterML + } + if req.ExerciseFrequency != "" { + lifestyle.ExerciseFrequency = req.ExerciseFrequency + } + if req.ExerciseType != "" { + lifestyle.ExerciseType = req.ExerciseType + } + if req.ExerciseDurationMin > 0 { + lifestyle.ExerciseDurationMin = req.ExerciseDurationMin + } + lifestyle.IsSmoker = req.IsSmoker + if req.AlcoholFrequency != "" { + lifestyle.AlcoholFrequency = req.AlcoholFrequency + } + + // 保存 + if lifestyle.ID == 0 { + if err := l.svcCtx.DB.Create(&lifestyle).Error; err != nil { + return nil, errorx.ErrServerError + } + } else { + if err := l.svcCtx.DB.Save(&lifestyle).Error; err != nil { + return nil, errorx.ErrServerError + } + } + + return &types.LifestyleInfo{ + ID: uint(lifestyle.ID), + UserID: lifestyle.UserID, + SleepTime: lifestyle.SleepTime, + WakeTime: lifestyle.WakeTime, + SleepQuality: lifestyle.SleepQuality, + MealRegularity: lifestyle.MealRegularity, + DietPreference: lifestyle.DietPreference, + DailyWaterML: lifestyle.DailyWaterML, + ExerciseFrequency: lifestyle.ExerciseFrequency, + ExerciseType: lifestyle.ExerciseType, + ExerciseDurationMin: lifestyle.ExerciseDurationMin, + IsSmoker: lifestyle.IsSmoker, + AlcoholFrequency: lifestyle.AlcoholFrequency, + }, nil +} diff --git a/backend/healthapi/internal/logic/update_user_profile_logic.go b/backend/healthapi/internal/logic/update_user_profile_logic.go new file mode 100644 index 0000000..f609d2f --- /dev/null +++ b/backend/healthapi/internal/logic/update_user_profile_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 UpdateUserProfileLogic struct { + logx.Logger + ctx context.Context + svcCtx *svc.ServiceContext +} + +func NewUpdateUserProfileLogic(ctx context.Context, svcCtx *svc.ServiceContext) *UpdateUserProfileLogic { + return &UpdateUserProfileLogic{ + Logger: logx.WithContext(ctx), + ctx: ctx, + svcCtx: svcCtx, + } +} + +func (l *UpdateUserProfileLogic) UpdateUserProfile(req *types.UpdateProfileReq) (resp *types.UserInfo, 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 + } + + // 更新字段 + if req.Nickname != "" { + user.Nickname = req.Nickname + } + if req.Avatar != "" { + user.Avatar = req.Avatar + } + + if err := l.svcCtx.DB.Save(&user).Error; err != nil { + l.Errorf("Failed to update user: %v", err) + return nil, errorx.ErrServerError + } + + return &types.UserInfo{ + ID: uint(user.ID), + Phone: user.Phone, + Email: user.Email, + Nickname: user.Nickname, + Avatar: user.Avatar, + SurveyCompleted: user.SurveyCompleted, + }, nil +} diff --git a/server/internal/model/constitution.go b/backend/healthapi/internal/model/constitution.go similarity index 63% rename from server/internal/model/constitution.go rename to backend/healthapi/internal/model/constitution.go index 4dc8b39..0aed6d5 100644 --- a/server/internal/model/constitution.go +++ b/backend/healthapi/internal/model/constitution.go @@ -11,10 +11,10 @@ type ConstitutionAssessment struct { gorm.Model UserID uint `gorm:"index" json:"user_id"` AssessedAt time.Time `json:"assessed_at"` - Scores string `gorm:"type:text" json:"scores"` // JSON: 各体质得分 - PrimaryConstitution string `gorm:"size:20" json:"primary_constitution"` // 主要体质 - SecondaryConstitutions string `gorm:"type:text" json:"secondary_constitutions"` // JSON: 次要体质 - Recommendations string `gorm:"type:text" json:"recommendations"` // JSON: 调养建议 + Scores string `gorm:"type:text" json:"scores"` // JSON: 各体质得分 + PrimaryConstitution string `gorm:"size:20" json:"primary_constitution"` // 主要体质 + SecondaryConstitutions string `gorm:"type:text" json:"secondary_constitutions"` // JSON: 次要体质 + Recommendations string `gorm:"type:text" json:"recommendations"` // JSON: 调养建议 } // AssessmentAnswer 问卷答案 @@ -36,15 +36,15 @@ type QuestionBank struct { // ConstitutionType 体质类型常量 const ( - ConstitutionPinghe = "pinghe" // 平和质 - ConstitutionQixu = "qixu" // 气虚质 - ConstitutionYangxu = "yangxu" // 阳虚质 - ConstitutionYinxu = "yinxu" // 阴虚质 - ConstitutionTanshi = "tanshi" // 痰湿质 - ConstitutionShire = "shire" // 湿热质 - ConstitutionXueyu = "xueyu" // 血瘀质 - ConstitutionQiyu = "qiyu" // 气郁质 - ConstitutionTebing = "tebing" // 特禀质 + ConstitutionPinghe = "pinghe" // 平和质 + ConstitutionQixu = "qixu" // 气虚质 + ConstitutionYangxu = "yangxu" // 阳虚质 + ConstitutionYinxu = "yinxu" // 阴虚质 + ConstitutionTanshi = "tanshi" // 痰湿质 + ConstitutionShire = "shire" // 湿热质 + ConstitutionXueyu = "xueyu" // 血瘀质 + ConstitutionQiyu = "qiyu" // 气郁质 + ConstitutionTebing = "tebing" // 特禀质 ) // ConstitutionNames 体质名称映射 @@ -76,58 +76,58 @@ var ConstitutionDescriptions = map[string]string{ // ConstitutionRecommendations 体质调养建议 var ConstitutionRecommendations = map[string]map[string]string{ ConstitutionPinghe: { - "diet": "饮食均衡,不偏食,粗细搭配", + "diet": "饮食均衡,不偏食,粗细搭配", "lifestyle": "起居有常,劳逸结合", - "exercise": "可进行各种运动,量力而行", - "emotion": "保持乐观积极的心态", + "exercise": "可进行各种运动,量力而行", + "emotion": "保持乐观积极的心态", }, ConstitutionQixu: { - "diet": "宜食益气健脾食物,如山药、大枣、小米", + "diet": "宜食益气健脾食物,如山药、大枣、小米", "lifestyle": "避免劳累,保证充足睡眠", - "exercise": "宜柔和运动,如太极拳、散步", - "emotion": "避免过度思虑", + "exercise": "宜柔和运动,如太极拳、散步", + "emotion": "避免过度思虑", }, ConstitutionYangxu: { - "diet": "宜食温阳食物,如羊肉、韭菜、生姜", + "diet": "宜食温阳食物,如羊肉、韭菜、生姜", "lifestyle": "注意保暖,避免受寒", - "exercise": "宜温和运动,避免大汗", - "emotion": "保持积极乐观", + "exercise": "宜温和运动,避免大汗", + "emotion": "保持积极乐观", }, ConstitutionYinxu: { - "diet": "宜食滋阴食物,如百合、银耳、枸杞", + "diet": "宜食滋阴食物,如百合、银耳、枸杞", "lifestyle": "避免熬夜,保持环境湿润", - "exercise": "宜静养,避免剧烈运动", - "emotion": "避免急躁易怒", + "exercise": "宜静养,避免剧烈运动", + "emotion": "避免急躁易怒", }, ConstitutionTanshi: { - "diet": "饮食清淡,少食肥甘厚味,宜食薏米、冬瓜", + "diet": "饮食清淡,少食肥甘厚味,宜食薏米、冬瓜", "lifestyle": "居住环境宜干燥通风", - "exercise": "坚持运动,促进代谢", - "emotion": "保持心情舒畅", + "exercise": "坚持运动,促进代谢", + "emotion": "保持心情舒畅", }, ConstitutionShire: { - "diet": "饮食清淡,宜食苦瓜、绿豆、薏米", + "diet": "饮食清淡,宜食苦瓜、绿豆、薏米", "lifestyle": "避免湿热环境,保持皮肤清洁", - "exercise": "适当运动,出汗排湿", - "emotion": "保持平和心态", + "exercise": "适当运动,出汗排湿", + "emotion": "保持平和心态", }, ConstitutionXueyu: { - "diet": "宜食活血化瘀食物,如山楂、黑木耳", + "diet": "宜食活血化瘀食物,如山楂、黑木耳", "lifestyle": "避免久坐,适当活动", - "exercise": "坚持有氧运动,促进血液循环", - "emotion": "保持心情愉快", + "exercise": "坚持有氧运动,促进血液循环", + "emotion": "保持心情愉快", }, ConstitutionQiyu: { - "diet": "宜食行气解郁食物,如玫瑰花、佛手", + "diet": "宜食行气解郁食物,如玫瑰花、佛手", "lifestyle": "多参加社交活动", - "exercise": "宜户外运动,舒展身心", - "emotion": "学会疏导情绪,培养兴趣爱好", + "exercise": "宜户外运动,舒展身心", + "emotion": "学会疏导情绪,培养兴趣爱好", }, ConstitutionTebing: { - "diet": "避免食用过敏食物,饮食清淡", + "diet": "避免食用过敏食物,饮食清淡", "lifestyle": "避免接触过敏原,保持环境清洁", - "exercise": "适度运动,增强体质", - "emotion": "保持心态平和", + "exercise": "适度运动,增强体质", + "emotion": "保持心态平和", }, } diff --git a/server/internal/model/conversation.go b/backend/healthapi/internal/model/conversation.go similarity index 100% rename from server/internal/model/conversation.go rename to backend/healthapi/internal/model/conversation.go diff --git a/server/internal/model/health.go b/backend/healthapi/internal/model/health.go similarity index 100% rename from server/internal/model/health.go rename to backend/healthapi/internal/model/health.go diff --git a/backend/healthapi/internal/model/mall.go b/backend/healthapi/internal/model/mall.go new file mode 100644 index 0000000..327157f --- /dev/null +++ b/backend/healthapi/internal/model/mall.go @@ -0,0 +1,182 @@ +package model + +import ( + "time" + + "gorm.io/gorm" +) + +// ========== 积分系统 ========== + +// PointsRecord 积分变动记录 +type PointsRecord struct { + gorm.Model + UserID uint `gorm:"index" json:"user_id"` + Type string `gorm:"size:20" json:"type"` // earn/spend/expire/adjust + Points int `json:"points"` // 变动积分(正负) + Balance int `json:"balance"` // 变动后余额 + Source string `gorm:"size:50" json:"source"` // order/activity/system/refund + ReferenceID uint `json:"reference_id"` // 关联ID(订单ID等) + Remark string `gorm:"size:200" json:"remark"` // 备注 + ExpireAt *time.Time `json:"expire_at"` // 过期时间(仅获得积分) +} + +// 积分变动类型 +const ( + PointsTypeEarn = "earn" // 获得 + PointsTypeSpend = "spend" // 使用 + PointsTypeExpire = "expire" // 过期 + PointsTypeAdjust = "adjust" // 调整 +) + +// 积分来源 +const ( + PointsSourceOrder = "order" // 订单获得 + PointsSourceActivity = "activity" // 活动获得 + PointsSourceSystem = "system" // 系统调整 + PointsSourceRefund = "refund" // 退款扣除 +) + +// ========== 商品系统 ========== + +// ProductCategory 商品分类 +type ProductCategory struct { + gorm.Model + Name string `gorm:"size:50" json:"name"` + ParentID uint `gorm:"default:0" json:"parent_id"` + Sort int `gorm:"default:0" json:"sort"` + Icon string `gorm:"size:255" json:"icon"` + Description string `gorm:"size:200" json:"description"` + IsActive bool `gorm:"default:true" json:"is_active"` +} + +// Product 商品(定义在 product.go 中,此处不重复定义) + +// ProductSku 商品SKU +type ProductSku struct { + gorm.Model + ProductID uint `gorm:"index" json:"product_id"` + SkuCode string `gorm:"size:50;uniqueIndex" json:"sku_code"` + Name string `gorm:"size:100" json:"name"` // 规格名称,如"500g装" + Attributes string `gorm:"size:500" json:"attributes"` // JSON,如{"weight":"500g","package":"袋装"} + Price float64 `gorm:"not null" json:"price"` + Stock int `gorm:"default:0" json:"stock"` + Image string `gorm:"size:500" json:"image"` + IsActive bool `gorm:"default:true" json:"is_active"` +} + +// ========== 购物车 ========== + +// CartItem 购物车项 +type CartItem struct { + gorm.Model + UserID uint `gorm:"index;not null" json:"user_id"` + ProductID uint `gorm:"index;not null" json:"product_id"` + SkuID uint `gorm:"index" json:"sku_id"` // 可选,无SKU时为0 + Quantity int `gorm:"not null;default:1" json:"quantity"` + Selected bool `gorm:"default:true" json:"selected"` // 是否选中 +} + +// ========== 收货地址 ========== + +// Address 收货地址 +type Address struct { + gorm.Model + UserID uint `gorm:"index;not null" json:"user_id"` + ReceiverName string `gorm:"size:50;not null" json:"receiver_name"` + Phone string `gorm:"size:20;not null" json:"phone"` + Province string `gorm:"size:50" json:"province"` + City string `gorm:"size:50" json:"city"` + District string `gorm:"size:50" json:"district"` + DetailAddr string `gorm:"size:200;not null" json:"detail_addr"` + PostalCode string `gorm:"size:10" json:"postal_code"` + IsDefault bool `gorm:"default:false" json:"is_default"` + Tag string `gorm:"size:20" json:"tag"` // home/company/other +} + +// ========== 订单系统 ========== + +// Order 订单 +type Order struct { + gorm.Model + UserID uint `gorm:"index;not null" json:"user_id"` + OrderNo string `gorm:"size:50;uniqueIndex" json:"order_no"` + Status string `gorm:"size:20;index" json:"status"` // pending/paid/shipped/completed/cancelled/refunding/refunded + 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 `gorm:"size:20" json:"pay_method"` // wechat/alipay/points + PayTime *time.Time `json:"pay_time"` + ShipTime *time.Time `json:"ship_time"` + ReceiveTime *time.Time `json:"receive_time"` + // 收货信息(快照) + ReceiverName string `gorm:"size:50" json:"receiver_name"` + ReceiverPhone string `gorm:"size:20" json:"receiver_phone"` + ReceiverAddr string `gorm:"size:300" json:"receiver_addr"` + // 物流信息 + ShippingCompany string `gorm:"size:50" json:"shipping_company"` + TrackingNo string `gorm:"size:50" json:"tracking_no"` + // 其他 + Remark string `gorm:"size:500" json:"remark"` // 买家留言 + CancelReason string `gorm:"size:200" json:"cancel_reason"` +} + +// 订单状态 +const ( + OrderStatusPending = "pending" // 待支付 + OrderStatusPaid = "paid" // 已支付 + OrderStatusShipped = "shipped" // 已发货 + OrderStatusCompleted = "completed" // 已完成 + OrderStatusCancelled = "cancelled" // 已取消 + OrderStatusRefunding = "refunding" // 退款中 + OrderStatusRefunded = "refunded" // 已退款 +) + +// OrderItem 订单商品 +type OrderItem struct { + gorm.Model + OrderID uint `gorm:"index;not null" json:"order_id"` + ProductID uint `json:"product_id"` + SkuID uint `json:"sku_id"` + ProductName string `gorm:"size:200" json:"product_name"` // 快照 + SkuName string `gorm:"size:100" json:"sku_name"` // 快照 + Image string `gorm:"size:500" json:"image"` // 快照 + Price float64 `json:"price"` // 快照单价 + Quantity int `json:"quantity"` + TotalAmount float64 `json:"total_amount"` // 小计 +} + +// ========== 表名定义 ========== + +func (PointsRecord) TableName() string { + return "points_records" +} + +func (ProductCategory) TableName() string { + return "product_categories" +} + +// Product TableName 在 product.go 中定义 + +func (ProductSku) TableName() string { + return "product_skus" +} + +func (CartItem) TableName() string { + return "cart_items" +} + +func (Address) TableName() string { + return "addresses" +} + +func (Order) TableName() string { + return "orders" +} + +func (OrderItem) TableName() string { + return "order_items" +} diff --git a/backend/healthapi/internal/model/models.go b/backend/healthapi/internal/model/models.go new file mode 100644 index 0000000..91e9375 --- /dev/null +++ b/backend/healthapi/internal/model/models.go @@ -0,0 +1,42 @@ +package model + +import ( + "gorm.io/gorm" +) + +// AutoMigrate 自动迁移所有模型 +func AutoMigrate(db *gorm.DB) error { + return db.AutoMigrate( + // 用户相关 + &User{}, + &HealthProfile{}, + &LifestyleInfo{}, + // 健康记录 + &MedicalHistory{}, + &FamilyHistory{}, + &AllergyRecord{}, + // 体质辨识 + &ConstitutionAssessment{}, + &AssessmentAnswer{}, + &QuestionBank{}, + // 对话 + &Conversation{}, + &Message{}, + // 商品(健康推荐 + 商城) + &Product{}, + &ProductCategory{}, + &ProductSku{}, + &ConstitutionProduct{}, + &SymptomProduct{}, + &PurchaseHistory{}, + // 会员与积分 + &PointsRecord{}, + // 购物车 + &CartItem{}, + // 收货地址 + &Address{}, + // 订单 + &Order{}, + &OrderItem{}, + ) +} diff --git a/backend/healthapi/internal/model/product.go b/backend/healthapi/internal/model/product.go new file mode 100644 index 0000000..29538e3 --- /dev/null +++ b/backend/healthapi/internal/model/product.go @@ -0,0 +1,83 @@ +package model + +import ( + "time" + + "gorm.io/gorm" +) + +// Product 商品表(融合健康推荐与商城功能) +type Product struct { + gorm.Model + // 基础信息 + CategoryID uint `gorm:"index" json:"category_id"` // 关联商品分类 + Category string `gorm:"size:50;index" json:"category"` // 分类名(兼容旧字段) + Name string `gorm:"size:200;not null" json:"name"` + Description string `gorm:"type:text" json:"description"` + MainImage string `gorm:"size:500" json:"main_image"` + Images string `gorm:"type:text" json:"images"` // JSON数组 + ImageURL string `gorm:"size:255" json:"image_url"` // 兼容旧字段 + // 价格库存 + Price float64 `gorm:"not null" json:"price"` + OriginalPrice float64 `json:"original_price"` + Stock int `gorm:"default:0" json:"stock"` + SalesCount int `gorm:"default:0" json:"sales_count"` + // 状态 + IsActive bool `gorm:"default:true" json:"is_active"` + IsFeatured bool `gorm:"default:false" json:"is_featured"` // 推荐商品 + Sort int `gorm:"default:0" json:"sort"` + // 健康相关(体质推荐) + Efficacy string `gorm:"type:text" json:"efficacy"` // 功效说明 + Suitable string `gorm:"type:text" json:"suitable"` // 适用人群/体质(兼容旧字段) + ConstitutionTypes string `gorm:"size:200" json:"constitution_types"` // 适合的体质类型(JSON数组) + HealthTags string `gorm:"size:500" json:"health_tags"` // 健康标签(JSON数组) + Ingredients string `gorm:"type:text" json:"ingredients"` // 成分/配料 + Usage string `gorm:"type:text" json:"usage"` // 使用方法 + Contraindications string `gorm:"type:text" json:"contraindications"` // 禁忌 + MallURL string `gorm:"size:255" json:"mall_url"` // 外部商城链接 +} + +// ConstitutionProduct 体质-产品关联表 +type ConstitutionProduct struct { + gorm.Model + ConstitutionType string `gorm:"size:20;index" json:"constitution_type"` + ProductID uint `gorm:"index" json:"product_id"` + Priority int `json:"priority"` // 推荐优先级 + Reason string `gorm:"size:200" json:"reason"` // 推荐理由 +} + +// SymptomProduct 症状-产品关联表 +type SymptomProduct struct { + gorm.Model + Keyword string `gorm:"size:50;index" json:"keyword"` // 症状关键词 + ProductID uint `gorm:"index" json:"product_id"` + Priority int `json:"priority"` +} + +// PurchaseHistory 购买历史表(商城同步) +type PurchaseHistory struct { + gorm.Model + UserID uint `gorm:"index" json:"user_id"` + OrderNo string `gorm:"size:50" json:"order_no"` // 商城订单号 + ProductID uint `json:"product_id"` + ProductName string `gorm:"size:100" json:"product_name"` + PurchasedAt time.Time `json:"purchased_at"` + Source string `gorm:"size:20" json:"source"` // mall=保健品商城 +} + +// TableName 指定表名 +func (Product) TableName() string { + return "products" +} + +func (ConstitutionProduct) TableName() string { + return "constitution_products" +} + +func (SymptomProduct) TableName() string { + return "symptom_products" +} + +func (PurchaseHistory) TableName() string { + return "purchase_histories" +} diff --git a/server/internal/model/user.go b/backend/healthapi/internal/model/user.go similarity index 50% rename from server/internal/model/user.go rename to backend/healthapi/internal/model/user.go index d1ec0fd..9fc0dff 100644 --- a/server/internal/model/user.go +++ b/backend/healthapi/internal/model/user.go @@ -15,6 +15,77 @@ type User struct { Nickname string `gorm:"size:50" json:"nickname"` Avatar string `gorm:"size:255" json:"avatar"` SurveyCompleted bool `gorm:"default:false" json:"survey_completed"` + // 会员相关字段 + MemberLevel string `gorm:"size:20;default:normal" json:"member_level"` // normal/silver/gold/diamond + TotalSpent float64 `gorm:"default:0" json:"total_spent"` // 累计消费金额 + Points int `gorm:"default:0" json:"points"` // 当前积分 + MemberSince *time.Time `json:"member_since"` // 首次消费时间 +} + +// 会员等级常量 +const ( + MemberLevelNormal = "normal" // 普通用户 + MemberLevelSilver = "silver" // 银卡会员 + MemberLevelGold = "gold" // 金卡会员 + MemberLevelDiamond = "diamond" // 钻石会员 +) + +// MemberLevelConfig 会员等级配置 +type MemberLevelConfig struct { + Level string // 等级标识 + Name string // 等级名称 + MinSpent float64 // 升级所需累计消费 + Discount float64 // 折扣率 (0.92-1.0) + PointsMultiplier float64 // 积分倍率 + FreeShippingMin float64 // 包邮门槛 +} + +// MemberLevelConfigs 会员等级配置表 +var MemberLevelConfigs = map[string]MemberLevelConfig{ + MemberLevelNormal: { + Level: MemberLevelNormal, + Name: "普通用户", + MinSpent: 0, + Discount: 1.0, + PointsMultiplier: 1.0, + FreeShippingMin: 99, + }, + MemberLevelSilver: { + Level: MemberLevelSilver, + Name: "银卡会员", + MinSpent: 500, + Discount: 0.98, + PointsMultiplier: 1.2, + FreeShippingMin: 69, + }, + MemberLevelGold: { + Level: MemberLevelGold, + Name: "金卡会员", + MinSpent: 2000, + Discount: 0.95, + PointsMultiplier: 1.5, + FreeShippingMin: 49, + }, + MemberLevelDiamond: { + Level: MemberLevelDiamond, + Name: "钻石会员", + MinSpent: 5000, + Discount: 0.92, + PointsMultiplier: 2.0, + FreeShippingMin: 0, // 全场包邮 + }, +} + +// GetMemberLevelBySpent 根据累计消费获取会员等级 +func GetMemberLevelBySpent(totalSpent float64) string { + if totalSpent >= 5000 { + return MemberLevelDiamond + } else if totalSpent >= 2000 { + return MemberLevelGold + } else if totalSpent >= 500 { + return MemberLevelSilver + } + return MemberLevelNormal } // HealthProfile 健康档案 @@ -37,12 +108,12 @@ type HealthProfile struct { type LifestyleInfo struct { gorm.Model UserID uint `gorm:"uniqueIndex" json:"user_id"` - SleepTime string `gorm:"size:10" json:"sleep_time"` // HH:MM - WakeTime string `gorm:"size:10" json:"wake_time"` // HH:MM - SleepQuality string `gorm:"size:20" json:"sleep_quality"` // good, normal, poor - MealRegularity string `gorm:"size:20" json:"meal_regularity"` // regular, irregular - DietPreference string `gorm:"size:50" json:"diet_preference"` // 偏好 - DailyWaterML int `json:"daily_water_ml"` // 每日饮水量 ml + SleepTime string `gorm:"size:10" json:"sleep_time"` // HH:MM + WakeTime string `gorm:"size:10" json:"wake_time"` // HH:MM + SleepQuality string `gorm:"size:20" json:"sleep_quality"` // good, normal, poor + MealRegularity string `gorm:"size:20" json:"meal_regularity"` // regular, irregular + DietPreference string `gorm:"size:50" json:"diet_preference"` // 偏好 + DailyWaterML int `json:"daily_water_ml"` // 每日饮水量 ml ExerciseFrequency string `gorm:"size:20" json:"exercise_frequency"` // never, sometimes, often, daily ExerciseType string `gorm:"size:100" json:"exercise_type"` ExerciseDurationMin int `json:"exercise_duration_min"` // 每次运动时长 diff --git a/backend/healthapi/internal/svc/service_context.go b/backend/healthapi/internal/svc/service_context.go new file mode 100644 index 0000000..fc2e978 --- /dev/null +++ b/backend/healthapi/internal/svc/service_context.go @@ -0,0 +1,42 @@ +package svc + +import ( + "healthapi/internal/config" + "healthapi/internal/database" + "healthapi/pkg/ai" + + "github.com/zeromicro/go-zero/core/logx" + "gorm.io/gorm" +) + +type ServiceContext struct { + Config config.Config + DB *gorm.DB + AIClient ai.AIClient +} + +func NewServiceContext(c config.Config) *ServiceContext { + // 初始化数据库 + db, err := database.NewDB(c.Database) + if err != nil { + logx.Errorf("Failed to connect database: %v", err) + panic(err) + } + + // 初始化数据(问卷题库、测试用户) + if err := database.SeedQuestionBank(db); err != nil { + logx.Errorf("Failed to seed question bank: %v", err) + } + if err := database.SeedTestUser(db); err != nil { + logx.Errorf("Failed to seed test user: %v", err) + } + + // 初始化 AI 客户端 + aiClient := ai.NewAIClient(c.AI) + + return &ServiceContext{ + Config: c, + DB: db, + AIClient: aiClient, + } +} diff --git a/backend/healthapi/internal/types/types.go b/backend/healthapi/internal/types/types.go new file mode 100644 index 0000000..dd49bab --- /dev/null +++ b/backend/healthapi/internal/types/types.go @@ -0,0 +1,608 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.8.4 + +package types + +type AddCartReq struct { + ProductID uint `json:"product_id"` + SkuID uint `json:"sku_id,optional"` + Quantity int `json:"quantity,default=1"` +} + +type Address struct { + 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 +} + +type AddressIdReq struct { + Id uint `path:"id"` +} + +type AddressListResp struct { + Addresses []Address `json:"addresses"` +} + +type AllergyRecord struct { + 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"` +} + +type AnswerItem struct { + QuestionID int `json:"question_id"` + Score int `json:"score"` // 1-5 +} + +type AssessmentHistoryResp struct { + History []AssessmentResult `json:"history"` +} + +type AssessmentResult struct { + 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"` +} + +type BatchAllergyReq struct { + Items []SubmitAllergyReq `json:"items"` +} + +type BatchFamilyHistoryReq struct { + Items []SubmitFamilyHistoryReq `json:"items"` +} + +type BatchMedicalHistoryReq struct { + Items []SubmitMedicalHistoryReq `json:"items"` +} + +type BatchSelectCartReq struct { + Ids []uint `json:"ids"` + Selected bool `json:"selected"` +} + +type CancelOrderReq struct { + Id uint `path:"id"` + Reason string `json:"reason,optional"` +} + +type CartItem struct { + 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"` // 当前库存 +} + +type CartResp struct { + Items []CartItem `json:"items"` + TotalCount int `json:"total_count"` + SelectedCount int `json:"selected_count"` + TotalAmount float64 `json:"total_amount"` +} + +type CategoryListResp struct { + Categories []ProductCategory `json:"categories"` +} + +type CommonResp struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + +type ConversationDetailResp struct { + ID uint `json:"id"` + Title string `json:"title"` + Messages []Message `json:"messages"` + CreatedAt string `json:"created_at"` +} + +type ConversationIdReq struct { + Id uint `path:"id"` +} + +type ConversationItem struct { + ID uint `json:"id"` + Title string `json:"title"` + CreatedAt string `json:"created_at"` + UpdatedAt string `json:"updated_at"` +} + +type ConversationListResp struct { + Conversations []ConversationItem `json:"conversations"` +} + +type CreateConversationReq struct { + Title string `json:"title,optional"` +} + +type CreateOrderReq struct { + AddressID uint `json:"address_id"` + CartItemIDs []uint `json:"cart_item_ids"` // 购物车项ID + PointsUsed int `json:"points_used,optional"` // 使用积分 + Remark string `json:"remark,optional"` +} + +type FamilyHistory struct { + ID uint `json:"id"` + HealthProfileID uint `json:"health_profile_id"` + Relation string `json:"relation"` + DiseaseName string `json:"disease_name"` + Notes string `json:"notes"` +} + +type FullHealthProfileResp struct { + Profile HealthProfile `json:"profile"` + Lifestyle LifestyleInfo `json:"lifestyle"` + MedicalHistory []MedicalHistory `json:"medical_history"` + FamilyHistory []FamilyHistory `json:"family_history"` + AllergyRecords []AllergyRecord `json:"allergy_records"` +} + +type GroupedQuestionsResp struct { + Groups map[string][]Question `json:"groups"` +} + +type HealthProfile struct { + 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"` +} + +type IdPathReq struct { + Id uint `path:"id"` +} + +type LifestyleInfo struct { + 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"` +} + +type LoginReq struct { + Phone string `json:"phone,optional"` + Email string `json:"email,optional"` + Password string `json:"password"` +} + +type LoginResp struct { + Token string `json:"token"` + ExpiresAt int64 `json:"expires_at"` + User UserInfo `json:"user"` +} + +type MedicalHistory struct { + 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"` +} + +type MemberInfo struct { + 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"` // 包邮门槛 +} + +type Message struct { + 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"` +} + +type MessageResp struct { + UserMessage Message `json:"user_message"` + AssistantMessage Message `json:"assistant_message"` +} + +type Order struct { + 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"` +} + +type OrderIdReq struct { + Id uint `path:"id"` +} + +type OrderItem struct { + 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"` +} + +type OrderListReq struct { + Page int `form:"page,default=1"` + PageSize int `form:"page_size,default=10"` + Status string `form:"status,optional"` // 筛选状态 +} + +type OrderListResp struct { + Orders []Order `json:"orders"` + PageInfo PageInfo `json:"page_info"` +} + +type OrderPreview struct { + 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"` // 积分可抵扣金额 +} + +type OrderPreviewReq struct { + CartItemIDs []uint `json:"cart_item_ids"` + AddressID uint `json:"address_id,optional"` +} + +type PageInfo struct { + Total int64 `json:"total"` + Page int `json:"page"` + PageSize int `json:"page_size"` +} + +type PageReq struct { + Page int `form:"page,default=1"` + PageSize int `form:"page_size,default=10"` +} + +type PayOrderReq struct { + Id uint `path:"id"` + PayMethod string `json:"pay_method"` // wechat/alipay +} + +type PointsRecord struct { + 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"` +} + +type PointsRecordsResp struct { + Records []PointsRecord `json:"records"` + PageInfo PageInfo `json:"page_info"` +} + +type Product struct { + 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"` +} + +type ProductCategory struct { + 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"` +} + +type ProductDetail struct { + 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 ProductListReq struct { + Page int `form:"page,default=1"` + PageSize int `form:"page_size,default=10"` + Category string `form:"category,optional"` +} + +type ProductListResp struct { + Products []Product `json:"products"` + PageInfo PageInfo `json:"page_info"` +} + +type ProductSearchReq struct { + Keyword string `form:"keyword"` + Page int `form:"page,default=1"` + PageSize int `form:"page_size,default=10"` +} + +type ProductSku struct { + 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"` +} + +type PurchaseHistory struct { + 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"` +} + +type PurchaseHistoryResp struct { + History []PurchaseHistory `json:"history"` + PageInfo PageInfo `json:"page_info"` +} + +type Question struct { + ID int `json:"id"` + ConstitutionType string `json:"constitution_type"` + QuestionText string `json:"question_text"` + Options []string `json:"options"` + OrderNum int `json:"order_num"` +} + +type QuestionsResp struct { + Questions []Question `json:"questions"` +} + +type RecommendProductsResp struct { + Products []Product `json:"products"` + Constitution string `json:"constitution"` + Reason string `json:"reason"` +} + +type RecommendationsResp struct { + Constitution string `json:"constitution"` + Recommendations map[string]string `json:"recommendations"` +} + +type RefreshTokenReq struct { + Token string `json:"token"` +} + +type RegisterReq struct { + Phone string `json:"phone,optional"` + Email string `json:"email,optional"` + Password string `json:"password"` + Code string `json:"code,optional"` +} + +type SaveAddressReq struct { + 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"` +} + +type SendCodeReq struct { + Phone string `json:"phone,optional"` + Email string `json:"email,optional"` + Type string `json:"type"` // register/login/reset +} + +type SendMessageReq struct { + Id uint `path:"id"` + Content string `json:"content"` +} + +type SubmitAllergyReq struct { + AllergyType string `json:"allergy_type"` + Allergen string `json:"allergen"` + Severity string `json:"severity,optional"` + ReactionDesc string `json:"reaction_desc,optional"` +} + +type SubmitAssessmentReq struct { + Answers []AnswerItem `json:"answers"` +} + +type SubmitBasicInfoReq struct { + Name string `json:"name"` + BirthDate string `json:"birth_date"` + Gender string `json:"gender"` + Height float64 `json:"height"` + Weight float64 `json:"weight"` + BloodType string `json:"blood_type,optional"` + Occupation string `json:"occupation,optional"` + MaritalStatus string `json:"marital_status,optional"` + Region string `json:"region,optional"` +} + +type SubmitFamilyHistoryReq struct { + Relation string `json:"relation"` + DiseaseName string `json:"disease_name"` + Notes string `json:"notes,optional"` +} + +type SubmitLifestyleReq struct { + SleepTime string `json:"sleep_time"` + WakeTime string `json:"wake_time"` + SleepQuality string `json:"sleep_quality"` + MealRegularity string `json:"meal_regularity"` + DietPreference string `json:"diet_preference,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"` +} + +type SubmitMedicalHistoryReq struct { + 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"` +} + +type SurveyStatusResp struct { + 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"` +} + +type SyncPurchaseReq struct { + 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"` +} + +type UpdateCartReq struct { + Id uint `path:"id"` + Quantity int `json:"quantity,optional"` + Selected bool `json:"selected,optional"` +} + +type UpdateHealthProfileReq struct { + 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"` +} + +type UpdateLifestyleReq struct { + 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"` +} + +type UpdateProfileReq struct { + Nickname string `json:"nickname,optional"` + Avatar string `json:"avatar,optional"` +} + +type UsePointsReq struct { + Points int `json:"points"` // 使用积分数 + OrderNo string `json:"order_no,optional"` // 关联订单 +} + +type UserInfo struct { + 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"` +} diff --git a/backend/healthapi/pkg/ai/aliyun.go b/backend/healthapi/pkg/ai/aliyun.go new file mode 100644 index 0000000..c67dbec --- /dev/null +++ b/backend/healthapi/pkg/ai/aliyun.go @@ -0,0 +1,239 @@ +package ai + +import ( + "bufio" + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +const AliyunBaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1" +const AliyunAppURLTemplate = "https://dashscope.aliyuncs.com/api/v2/apps/agent/%s/compatible-mode/v1" + +type AliyunClient struct { + apiKey string + model string + appID string + baseURL string +} + +func NewAliyunClient(cfg *Config) *AliyunClient { + model := cfg.Model + if model == "" { + model = "qwen-turbo" + } + + client := &AliyunClient{ + apiKey: cfg.APIKey, + model: model, + appID: cfg.AppID, + } + + if cfg.AppID != "" { + client.baseURL = fmt.Sprintf(AliyunAppURLTemplate, cfg.AppID) + } else { + client.baseURL = AliyunBaseURL + } + + return client +} + +type aliyunRequest struct { + Model string `json:"model"` + Messages []Message `json:"messages"` + Stream bool `json:"stream,omitempty"` + EnableThinking bool `json:"enable_thinking,omitempty"` + ThinkingBudget int `json:"thinking_budget,omitempty"` +} + +type aliyunResponse struct { + ID string `json:"id"` + Choices []struct { + Index int `json:"index"` + Message struct { + Role string `json:"role"` + Content string `json:"content"` + ReasoningContent string `json:"reasoning_content"` + } `json:"message"` + Delta struct { + Content string `json:"content"` + ReasoningContent string `json:"reasoning_content"` + } `json:"delta"` + FinishReason string `json:"finish_reason"` + } `json:"choices"` + Error *struct { + Message string `json:"message"` + Code string `json:"code"` + } `json:"error"` +} + +func (c *AliyunClient) Chat(ctx context.Context, messages []Message) (string, error) { + if c.apiKey == "" { + return "", fmt.Errorf("阿里云 API Key 未配置") + } + + filteredMessages := messages + if c.appID != "" { + filteredMessages = filterSystemMessages(messages) + } + + reqBody := aliyunRequest{ + Model: c.model, + Messages: filteredMessages, + Stream: false, + } + + body, _ := json.Marshal(reqBody) + req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/chat/completions", bytes.NewReader(body)) + if err != nil { + return "", err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+c.apiKey) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return "", fmt.Errorf("调用AI服务失败: %v", err) + } + defer resp.Body.Close() + + var result aliyunResponse + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + return "", fmt.Errorf("解析AI响应失败: %v", err) + } + + if result.Error != nil { + return "", fmt.Errorf("AI服务错误: %s", result.Error.Message) + } + + if len(result.Choices) == 0 { + return "", fmt.Errorf("AI未返回有效响应") + } + + return result.Choices[0].Message.Content, nil +} + +func (c *AliyunClient) ChatStream(ctx context.Context, messages []Message, writer io.Writer) error { + if c.apiKey == "" { + return fmt.Errorf("阿里云 API Key 未配置") + } + + filteredMessages := messages + if c.appID != "" { + filteredMessages = filterSystemMessages(messages) + } + + // 启用思考过程(与原 server 一致) + reqBody := aliyunRequest{ + Model: c.model, + Messages: filteredMessages, + Stream: true, + EnableThinking: true, // 启用思考过程 + ThinkingBudget: 2048, // 思考过程最大 token 数 + } + + body, _ := json.Marshal(reqBody) + req, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/chat/completions", bytes.NewReader(body)) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Authorization", "Bearer "+c.apiKey) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + respBody, _ := io.ReadAll(resp.Body) + return fmt.Errorf("AI服务返回错误: %s", string(respBody)) + } + + reader := bufio.NewReader(resp.Body) + isThinking := false + + for { + line, err := reader.ReadString('\n') + if err == io.EOF { + break + } + if err != nil { + return err + } + + line = strings.TrimSpace(line) + if strings.HasPrefix(line, "data:") { + data := strings.TrimPrefix(line, "data:") + data = strings.TrimSpace(data) + if data == "[DONE]" { + break + } + + var streamResp aliyunResponse + if err := json.Unmarshal([]byte(data), &streamResp); err != nil { + continue + } + + if len(streamResp.Choices) > 0 { + choice := streamResp.Choices[0] + + if choice.Delta.ReasoningContent != "" { + if !isThinking { + sseData := "data: {\"type\":\"thinking_start\"}\n\n" + writer.Write([]byte(sseData)) + if flusher, ok := writer.(http.Flusher); ok { + flusher.Flush() + } + isThinking = true + } + sseData := fmt.Sprintf("data: {\"type\":\"thinking\",\"content\":%s}\n\n", jsonEscape(choice.Delta.ReasoningContent)) + writer.Write([]byte(sseData)) + if flusher, ok := writer.(http.Flusher); ok { + flusher.Flush() + } + } + + if choice.Delta.Content != "" { + if isThinking { + sseData := "data: {\"type\":\"thinking_end\"}\n\n" + writer.Write([]byte(sseData)) + if flusher, ok := writer.(http.Flusher); ok { + flusher.Flush() + } + isThinking = false + } + sseData := fmt.Sprintf("data: {\"type\":\"content\",\"content\":%s}\n\n", jsonEscape(choice.Delta.Content)) + writer.Write([]byte(sseData)) + if flusher, ok := writer.(http.Flusher); ok { + flusher.Flush() + } + } + } + } + } + + return nil +} + +func jsonEscape(s string) string { + b, _ := json.Marshal(s) + return string(b) +} + +func filterSystemMessages(messages []Message) []Message { + filtered := make([]Message, 0, len(messages)) + for _, msg := range messages { + if msg.Role != "system" { + filtered = append(filtered, msg) + } + } + return filtered +} diff --git a/server/internal/service/ai/client.go b/backend/healthapi/pkg/ai/client.go similarity index 79% rename from server/internal/service/ai/client.go rename to backend/healthapi/pkg/ai/client.go index 0b90beb..435da94 100644 --- a/server/internal/service/ai/client.go +++ b/backend/healthapi/pkg/ai/client.go @@ -13,7 +13,7 @@ type AIClient interface { // Message 对话消息 type Message struct { - Role string `json:"role"` // system, user, assistant + Role string `json:"role"` // system, user, assistant Content string `json:"content"` } @@ -23,4 +23,5 @@ type Config struct { APIKey string BaseURL string Model string + AppID string // 阿里云百炼应用 ID(可选) } diff --git a/server/internal/service/ai/factory.go b/backend/healthapi/pkg/ai/factory.go similarity index 57% rename from server/internal/service/ai/factory.go rename to backend/healthapi/pkg/ai/factory.go index 16e62d4..952f350 100644 --- a/server/internal/service/ai/factory.go +++ b/backend/healthapi/pkg/ai/factory.go @@ -1,21 +1,22 @@ package ai -import "health-ai/internal/config" +import "healthapi/internal/config" // NewAIClient 根据配置创建 AI 客户端 -func NewAIClient(cfg *config.AIConfig) AIClient { +func NewAIClient(cfg config.AIConfig) AIClient { switch cfg.Provider { case "aliyun": return NewAliyunClient(&Config{ - APIKey: cfg.Aliyun.APIKey, + APIKey: cfg.Aliyun.ApiKey, Model: cfg.Aliyun.Model, + AppID: cfg.Aliyun.AppID, }) case "openai": fallthrough default: return NewOpenAIClient(&Config{ - APIKey: cfg.OpenAI.APIKey, - BaseURL: cfg.OpenAI.BaseURL, + APIKey: cfg.OpenAI.ApiKey, + BaseURL: cfg.OpenAI.BaseUrl, Model: cfg.OpenAI.Model, }) } diff --git a/server/internal/service/ai/openai.go b/backend/healthapi/pkg/ai/openai.go similarity index 78% rename from server/internal/service/ai/openai.go rename to backend/healthapi/pkg/ai/openai.go index e6d8d6f..d442f7c 100644 --- a/server/internal/service/ai/openai.go +++ b/backend/healthapi/pkg/ai/openai.go @@ -26,6 +26,7 @@ func NewOpenAIClient(cfg *Config) *OpenAIClient { if model == "" { model = "gpt-3.5-turbo" } + return &OpenAIClient{ apiKey: cfg.APIKey, baseURL: baseURL, @@ -33,13 +34,13 @@ func NewOpenAIClient(cfg *Config) *OpenAIClient { } } -type openAIRequest struct { +type openaiRequest struct { Model string `json:"model"` Messages []Message `json:"messages"` - Stream bool `json:"stream"` + Stream bool `json:"stream,omitempty"` } -type openAIResponse struct { +type openaiResponse struct { Choices []struct { Message struct { Content string `json:"content"` @@ -55,10 +56,10 @@ type openAIResponse struct { func (c *OpenAIClient) Chat(ctx context.Context, messages []Message) (string, error) { if c.apiKey == "" { - return "", fmt.Errorf("OpenAI API Key 未配置,请在 config.yaml 中设置 ai.openai.api_key") + return "", fmt.Errorf("OpenAI API Key 未配置") } - reqBody := openAIRequest{ + reqBody := openaiRequest{ Model: c.model, Messages: messages, Stream: false, @@ -75,21 +76,21 @@ func (c *OpenAIClient) Chat(ctx context.Context, messages []Message) (string, er resp, err := http.DefaultClient.Do(req) if err != nil { - return "", fmt.Errorf("调用AI服务失败: %v", err) + return "", err } defer resp.Body.Close() - var result openAIResponse + var result openaiResponse if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return "", fmt.Errorf("解析AI响应失败: %v", err) + return "", err } if result.Error != nil { - return "", fmt.Errorf("AI服务错误: %s", result.Error.Message) + return "", fmt.Errorf("OpenAI error: %s", result.Error.Message) } if len(result.Choices) == 0 { - return "", fmt.Errorf("AI未返回有效响应") + return "", fmt.Errorf("no response") } return result.Choices[0].Message.Content, nil @@ -100,7 +101,7 @@ func (c *OpenAIClient) ChatStream(ctx context.Context, messages []Message, write return fmt.Errorf("OpenAI API Key 未配置") } - reqBody := openAIRequest{ + reqBody := openaiRequest{ Model: c.model, Messages: messages, Stream: true, @@ -121,7 +122,6 @@ func (c *OpenAIClient) ChatStream(ctx context.Context, messages []Message, write } defer resp.Body.Close() - // 解析 SSE 流 reader := bufio.NewReader(resp.Body) for { line, err := reader.ReadString('\n') @@ -140,14 +140,17 @@ func (c *OpenAIClient) ChatStream(ctx context.Context, messages []Message, write break } - var streamResp openAIResponse + var streamResp openaiResponse if err := json.Unmarshal([]byte(data), &streamResp); err != nil { continue } - if len(streamResp.Choices) > 0 { - content := streamResp.Choices[0].Delta.Content - writer.Write([]byte(content)) + if len(streamResp.Choices) > 0 && streamResp.Choices[0].Delta.Content != "" { + sseData := fmt.Sprintf("data: {\"type\":\"content\",\"content\":%s}\n\n", jsonEscape(streamResp.Choices[0].Delta.Content)) + writer.Write([]byte(sseData)) + if flusher, ok := writer.(http.Flusher); ok { + flusher.Flush() + } } } } diff --git a/backend/healthapi/pkg/errorx/errorx.go b/backend/healthapi/pkg/errorx/errorx.go new file mode 100644 index 0000000..5b94e71 --- /dev/null +++ b/backend/healthapi/pkg/errorx/errorx.go @@ -0,0 +1,50 @@ +package errorx + +import "fmt" + +// CodeError 业务错误 +type CodeError struct { + Code int `json:"code"` + Msg string `json:"msg"` +} + +func (e *CodeError) Error() string { + return fmt.Sprintf("code: %d, msg: %s", e.Code, e.Msg) +} + +// NewCodeError 创建业务错误 +func NewCodeError(code int, msg string) *CodeError { + return &CodeError{Code: code, Msg: msg} +} + +// NewCodeErrorf 创建业务错误(格式化) +func NewCodeErrorf(code int, format string, args ...interface{}) *CodeError { + return &CodeError{Code: code, Msg: fmt.Sprintf(format, args...)} +} + +// 预定义错误码 +const ( + CodeSuccess = 0 + CodeBadRequest = 400 + CodeUnauthorized = 401 + CodeForbidden = 403 + CodeNotFound = 404 + CodeServerError = 500 + CodeUserExists = 1001 + CodeUserNotFound = 1002 + CodeWrongPassword = 1003 + CodeInvalidToken = 1004 +) + +// 预定义错误 +var ( + ErrBadRequest = NewCodeError(CodeBadRequest, "请求参数错误") + ErrUnauthorized = NewCodeError(CodeUnauthorized, "未授权") + ErrForbidden = NewCodeError(CodeForbidden, "禁止访问") + ErrNotFound = NewCodeError(CodeNotFound, "资源不存在") + ErrServerError = NewCodeError(CodeServerError, "服务器内部错误") + ErrUserExists = NewCodeError(CodeUserExists, "用户已存在") + ErrUserNotFound = NewCodeError(CodeUserNotFound, "用户不存在") + ErrWrongPassword = NewCodeError(CodeWrongPassword, "密码错误") + ErrInvalidToken = NewCodeError(CodeInvalidToken, "无效的Token") +) diff --git a/server/pkg/jwt/jwt.go b/backend/healthapi/pkg/jwt/jwt.go similarity index 54% rename from server/pkg/jwt/jwt.go rename to backend/healthapi/pkg/jwt/jwt.go index 4dbc655..1155d0b 100644 --- a/server/pkg/jwt/jwt.go +++ b/backend/healthapi/pkg/jwt/jwt.go @@ -7,17 +7,6 @@ import ( "github.com/golang-jwt/jwt/v5" ) -var ( - jwtSecret []byte - expireHours int -) - -// Init 初始化JWT配置 -func Init(secret string, hours int) { - jwtSecret = []byte(secret) - expireHours = hours -} - // Claims JWT声明 type Claims struct { UserID uint `json:"user_id"` @@ -25,23 +14,28 @@ type Claims struct { } // GenerateToken 生成Token -func GenerateToken(userID uint) (string, error) { +func GenerateToken(userID uint, secret string, expireSeconds int64) (string, int64, error) { + expiresAt := time.Now().Add(time.Duration(expireSeconds) * time.Second) claims := Claims{ UserID: userID, RegisteredClaims: jwt.RegisteredClaims{ - ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Duration(expireHours) * time.Hour)), + ExpiresAt: jwt.NewNumericDate(expiresAt), IssuedAt: jwt.NewNumericDate(time.Now()), NotBefore: jwt.NewNumericDate(time.Now()), }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - return token.SignedString(jwtSecret) + tokenString, err := token.SignedString([]byte(secret)) + if err != nil { + return "", 0, err + } + return tokenString, expiresAt.Unix(), nil } // ParseToken 解析Token -func ParseToken(tokenString string) (*Claims, error) { +func ParseToken(tokenString string, secret string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { - return jwtSecret, nil + return []byte(secret), nil }) if err != nil { return nil, err @@ -53,11 +47,11 @@ func ParseToken(tokenString string) (*Claims, error) { } // RefreshToken 刷新Token -func RefreshToken(tokenString string) (string, error) { - claims, err := ParseToken(tokenString) +func RefreshToken(tokenString string, secret string, expireSeconds int64) (string, int64, error) { + claims, err := ParseToken(tokenString, secret) if err != nil { - return "", err + return "", 0, err } // 生成新Token - return GenerateToken(claims.UserID) + return GenerateToken(claims.UserID, secret, expireSeconds) } diff --git a/backend/healthapi/pkg/response/response.go b/backend/healthapi/pkg/response/response.go new file mode 100644 index 0000000..e11ebb2 --- /dev/null +++ b/backend/healthapi/pkg/response/response.go @@ -0,0 +1,41 @@ +package response + +import ( + "net/http" + + "healthapi/pkg/errorx" + + "github.com/zeromicro/go-zero/rest/httpx" +) + +// Response 统一响应结构 +type Response struct { + Code int `json:"code"` + Message string `json:"message"` + Data interface{} `json:"data,omitempty"` +} + +// Success 成功响应 +func Success(w http.ResponseWriter, data interface{}) { + httpx.OkJson(w, Response{ + Code: 0, + Message: "success", + Data: data, + }) +} + +// Error 错误响应 +func Error(w http.ResponseWriter, err error) { + if codeErr, ok := err.(*errorx.CodeError); ok { + httpx.WriteJson(w, http.StatusBadRequest, Response{ + Code: codeErr.Code, + Message: codeErr.Msg, + }) + return + } + + httpx.WriteJson(w, http.StatusInternalServerError, Response{ + Code: errorx.CodeServerError, + Message: err.Error(), + }) +} diff --git a/backend/healthapi/tests/mall_api_test.go b/backend/healthapi/tests/mall_api_test.go new file mode 100644 index 0000000..8309bd8 --- /dev/null +++ b/backend/healthapi/tests/mall_api_test.go @@ -0,0 +1,1022 @@ +package tests + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "testing" + + "healthapi/internal/config" + "healthapi/internal/database" + + "github.com/zeromicro/go-zero/core/logx" + "gorm.io/gorm" +) + +// ==================== 全局变量 ==================== + +const ( + baseURL = "http://localhost:8080" + testPhone = "13800138000" + testPwd = "123456" +) + +var ( + token string // JWT Token + db *gorm.DB + testUserID uint + cartItemID uint + cartItemID2 uint + addressID uint + addressID2 uint + orderID uint + productID uint + skuID uint +) + +// ==================== TestMain ==================== + +func TestMain(m *testing.M) { + logx.Disable() + + // 连接数据库(用于插入种子数据) + var err error + db, err = database.NewDB(config.DatabaseConfig{ + Driver: "sqlite", + DataSource: "../data/health.db", + }) + if err != nil { + fmt.Printf("❌ 数据库连接失败: %v\n", err) + os.Exit(1) + } + + // 插入商城种子数据 + if err := SeedMallTestData(db); err != nil { + fmt.Printf("❌ 种子数据插入失败: %v\n", err) + os.Exit(1) + } + fmt.Println("✅ 种子数据准备完成") + + code := m.Run() + + fmt.Println("\n========================================") + fmt.Println(" 商城API测试完成") + fmt.Println("========================================") + + os.Exit(code) +} + +// ==================== 辅助函数 ==================== + +type apiResponse struct { + Code int `json:"code"` + Message string `json:"message"` + Data json.RawMessage `json:"data,omitempty"` +} + +func doRequest(method, path string, body interface{}, auth bool) (*http.Response, []byte, error) { + var reqBody io.Reader + if body != nil { + data, _ := json.Marshal(body) + reqBody = bytes.NewBuffer(data) + } + + req, err := http.NewRequest(method, baseURL+path, reqBody) + if err != nil { + return nil, nil, err + } + req.Header.Set("Content-Type", "application/json") + if auth && token != "" { + req.Header.Set("Authorization", "Bearer "+token) + } + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + respBody, _ := io.ReadAll(resp.Body) + return resp, respBody, nil +} + +// parseData 智能解析响应: +// - 如果响应被 {"code":0,"message":"success","data":{...}} 包装,提取 data +// - 如果是直接返回 JSON,则直接解析 +func parseData(raw []byte, v interface{}) error { + // 先尝试作为包装格式解析 + var wrapper apiResponse + if err := json.Unmarshal(raw, &wrapper); err == nil && wrapper.Data != nil { + // 有 data 字段,说明是包装格式 + return json.Unmarshal(wrapper.Data, v) + } + // 否则直接解析(goctl 生成的 handler 直接返回数据) + return json.Unmarshal(raw, v) +} + +// ==================== 1. 认证测试 ==================== + +func TestMall_00_Login(t *testing.T) { + body := map[string]string{ + "phone": testPhone, + "password": testPwd, + } + resp, respBody, err := doRequest("POST", "/api/auth/login", body, false) + if err != nil { + t.Fatalf("登录请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("登录状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + Token string `json:"token"` + User struct { + ID uint `json:"id"` + } `json:"user"` + } + if err := parseData(respBody, &result); err != nil { + t.Fatalf("解析登录响应失败: %v, body=%s", err, string(respBody)) + } + if result.Token == "" { + t.Fatalf("Token 为空, body=%s", string(respBody)) + } + + token = result.Token + testUserID = result.User.ID + t.Logf("✅ 登录成功, UserID=%d, Token=%s...", testUserID, token[:20]) + + // 清理旧测试数据 + CleanMallTestData(db, testUserID) + t.Log("✅ 旧测试数据已清理") +} + +// ==================== 2. 会员系统 ==================== + +func TestMall_01_GetMemberInfo(t *testing.T) { + resp, respBody, err := doRequest("GET", "/api/mall/member/info", nil, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + Level string `json:"level"` + LevelName string `json:"level_name"` + Points int `json:"points"` + Discount float64 `json:"discount"` + PointsMultiplier float64 `json:"points_multiplier"` + } + if err := parseData(respBody, &result); err != nil { + t.Fatalf("解析失败: %v", err) + } + + t.Logf("✅ 会员信息: level=%s, name=%s, points=%d, discount=%.2f, multiplier=%.1f", + result.Level, result.LevelName, result.Points, result.Discount, result.PointsMultiplier) + + if result.Level == "" { + t.Error("会员等级为空") + } + if result.Discount <= 0 || result.Discount > 1 { + t.Errorf("折扣率异常: %f", result.Discount) + } +} + +func TestMall_02_GetPointsRecords(t *testing.T) { + resp, respBody, err := doRequest("GET", "/api/mall/member/points/records?page=1&page_size=10", nil, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + Records []interface{} `json:"records"` + PageInfo struct { + Total int64 `json:"total"` + } `json:"page_info"` + } + if err := parseData(respBody, &result); err != nil { + t.Fatalf("解析失败: %v", err) + } + t.Logf("✅ 积分记录: total=%d, records=%d", result.PageInfo.Total, len(result.Records)) +} + +// ==================== 3. 商品模块 ==================== + +func TestMall_10_GetCategories(t *testing.T) { + resp, respBody, err := doRequest("GET", "/api/mall/categories", nil, false) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + Categories []struct { + ID uint `json:"id"` + Name string `json:"name"` + } `json:"categories"` + } + if err := parseData(respBody, &result); err != nil { + t.Fatalf("解析失败: %v", err) + } + if len(result.Categories) == 0 { + t.Error("分类列表为空") + } + for _, c := range result.Categories { + t.Logf(" 分类: id=%d, name=%s", c.ID, c.Name) + } + t.Logf("✅ 获取分类成功, 共 %d 个", len(result.Categories)) +} + +func TestMall_11_GetMallProducts(t *testing.T) { + resp, respBody, err := doRequest("GET", "/api/mall/products?page=1&page_size=10", nil, false) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + Products []struct { + ID uint `json:"id"` + Name string `json:"name"` + Price float64 `json:"price"` + } `json:"products"` + PageInfo struct { + Total int64 `json:"total"` + } `json:"page_info"` + } + if err := parseData(respBody, &result); err != nil { + t.Fatalf("解析失败: %v", err) + } + if len(result.Products) == 0 { + t.Fatal("商品列表为空") + } + + // 保存第一个商品ID用于后续测试 + productID = result.Products[0].ID + + for _, p := range result.Products { + t.Logf(" 商品: id=%d, name=%s, price=%.2f", p.ID, p.Name, p.Price) + } + t.Logf("✅ 商品列表: total=%d, 当前页=%d", result.PageInfo.Total, len(result.Products)) +} + +func TestMall_12_GetMallProductDetail(t *testing.T) { + if productID == 0 { + t.Skip("无商品ID,跳过") + } + + path := fmt.Sprintf("/api/mall/products/%d", productID) + resp, respBody, err := doRequest("GET", path, nil, false) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + ID uint `json:"id"` + Name string `json:"name"` + Price float64 `json:"price"` + Description string `json:"description"` + Efficacy string `json:"efficacy"` + Skus []struct { + ID uint `json:"id"` + Name string `json:"name"` + Price float64 `json:"price"` + } `json:"skus"` + } + if err := parseData(respBody, &result); err != nil { + t.Fatalf("解析失败: %v", err) + } + + if result.Name == "" { + t.Error("商品名为空") + } + t.Logf("✅ 商品详情: id=%d, name=%s, price=%.2f, skus=%d", + result.ID, result.Name, result.Price, len(result.Skus)) + + // 保存 SKU ID + if len(result.Skus) > 0 { + skuID = result.Skus[0].ID + t.Logf(" SKU: id=%d, name=%s, price=%.2f", skuID, result.Skus[0].Name, result.Skus[0].Price) + } +} + +func TestMall_13_SearchMallProducts(t *testing.T) { + resp, respBody, err := doRequest("GET", "/api/mall/products/search?keyword=养生茶&page=1&page_size=10", nil, false) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + Products []struct { + Name string `json:"name"` + } `json:"products"` + PageInfo struct { + Total int64 `json:"total"` + } `json:"page_info"` + } + parseData(respBody, &result) + t.Logf("✅ 搜索 '养生茶': total=%d, results=%d", result.PageInfo.Total, len(result.Products)) + for _, p := range result.Products { + t.Logf(" 结果: %s", p.Name) + } +} + +func TestMall_14_GetFeaturedProducts(t *testing.T) { + resp, respBody, err := doRequest("GET", "/api/mall/products/featured?page=1&page_size=10", nil, false) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + Products []struct { + Name string `json:"name"` + } `json:"products"` + PageInfo struct { + Total int64 `json:"total"` + } `json:"page_info"` + } + parseData(respBody, &result) + t.Logf("✅ 推荐商品: total=%d, results=%d", result.PageInfo.Total, len(result.Products)) +} + +// ==================== 4. 购物车 ==================== + +func TestMall_20_AddCart(t *testing.T) { + if productID == 0 { + t.Skip("无商品ID") + } + + // 添加商品(无SKU) + body := map[string]interface{}{ + "product_id": productID, + "quantity": 2, + } + resp, respBody, err := doRequest("POST", "/api/mall/cart", body, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + ID uint `json:"id"` + ProductName string `json:"product_name"` + Quantity int `json:"quantity"` + Price float64 `json:"price"` + } + parseData(respBody, &result) + cartItemID = result.ID + t.Logf("✅ 添加购物车: id=%d, product=%s, qty=%d, price=%.2f", + cartItemID, result.ProductName, result.Quantity, result.Price) + + if cartItemID == 0 { + t.Fatal("购物车项ID为0") + } +} + +func TestMall_21_AddCartWithSku(t *testing.T) { + if productID == 0 || skuID == 0 { + t.Skip("无商品或SKU ID") + } + + body := map[string]interface{}{ + "product_id": productID, + "sku_id": skuID, + "quantity": 1, + } + resp, respBody, err := doRequest("POST", "/api/mall/cart", body, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + ID uint `json:"id"` + SkuName string `json:"sku_name"` + Quantity int `json:"quantity"` + } + parseData(respBody, &result) + cartItemID2 = result.ID + t.Logf("✅ 添加SKU购物车: id=%d, sku=%s, qty=%d", cartItemID2, result.SkuName, result.Quantity) +} + +func TestMall_22_GetCart(t *testing.T) { + resp, respBody, err := doRequest("GET", "/api/mall/cart", nil, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + Items []interface{} `json:"items"` + TotalCount int `json:"total_count"` + TotalAmount float64 `json:"total_amount"` + } + parseData(respBody, &result) + t.Logf("✅ 购物车: items=%d, totalCount=%d, totalAmount=%.2f", + len(result.Items), result.TotalCount, result.TotalAmount) + + if len(result.Items) == 0 { + t.Error("购物车为空") + } +} + +func TestMall_23_UpdateCart(t *testing.T) { + if cartItemID == 0 { + t.Skip("无购物车项ID") + } + + body := map[string]interface{}{ + "quantity": 3, + "selected": true, + } + path := fmt.Sprintf("/api/mall/cart/%d", cartItemID) + resp, respBody, err := doRequest("PUT", path, body, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + Quantity int `json:"quantity"` + } + parseData(respBody, &result) + if result.Quantity != 3 { + t.Errorf("数量更新失败: expected=3, got=%d", result.Quantity) + } + t.Logf("✅ 更新购物车数量: qty=%d", result.Quantity) +} + +func TestMall_24_BatchSelectCart(t *testing.T) { + body := map[string]interface{}{ + "ids": []uint{}, + "selected": true, + } + resp, respBody, err := doRequest("POST", "/api/mall/cart/batch-select", body, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + t.Log("✅ 批量全选成功") +} + +func TestMall_25_DeleteCartItem(t *testing.T) { + if cartItemID2 == 0 { + t.Skip("无购物车项ID2") + } + + path := fmt.Sprintf("/api/mall/cart/%d", cartItemID2) + resp, respBody, err := doRequest("DELETE", path, nil, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + t.Logf("✅ 删除购物车项: id=%d", cartItemID2) +} + +// ==================== 5. 收货地址 ==================== + +func TestMall_30_CreateAddress(t *testing.T) { + body := map[string]interface{}{ + "receiver_name": "张三", + "phone": "13900139000", + "province": "北京市", + "city": "北京市", + "district": "朝阳区", + "detail_addr": "建国路88号", + "postal_code": "100020", + "tag": "home", + } + resp, respBody, err := doRequest("POST", "/api/mall/addresses", body, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + ID uint `json:"id"` + ReceiverName string `json:"receiver_name"` + IsDefault bool `json:"is_default"` + } + parseData(respBody, &result) + addressID = result.ID + t.Logf("✅ 创建地址: id=%d, name=%s, isDefault=%v", addressID, result.ReceiverName, result.IsDefault) + + if !result.IsDefault { + t.Error("第一个地址应自动为默认") + } +} + +func TestMall_31_CreateAddress2(t *testing.T) { + body := map[string]interface{}{ + "receiver_name": "李四", + "phone": "13800138001", + "province": "上海市", + "city": "上海市", + "district": "浦东新区", + "detail_addr": "陆家嘴环路1号", + "tag": "company", + } + resp, respBody, err := doRequest("POST", "/api/mall/addresses", body, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + ID uint `json:"id"` + } + parseData(respBody, &result) + addressID2 = result.ID + t.Logf("✅ 创建第二个地址: id=%d", addressID2) +} + +func TestMall_32_GetAddresses(t *testing.T) { + resp, respBody, err := doRequest("GET", "/api/mall/addresses", nil, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + Addresses []struct { + ID uint `json:"id"` + ReceiverName string `json:"receiver_name"` + IsDefault bool `json:"is_default"` + } `json:"addresses"` + } + parseData(respBody, &result) + + if len(result.Addresses) < 2 { + t.Errorf("地址数量不足: expected>=2, got=%d", len(result.Addresses)) + } + for _, a := range result.Addresses { + t.Logf(" 地址: id=%d, name=%s, default=%v", a.ID, a.ReceiverName, a.IsDefault) + } + t.Logf("✅ 地址列表: %d 个", len(result.Addresses)) +} + +func TestMall_33_GetAddress(t *testing.T) { + if addressID == 0 { + t.Skip("无地址ID") + } + path := fmt.Sprintf("/api/mall/addresses/%d", addressID) + resp, respBody, err := doRequest("GET", path, nil, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + ReceiverName string `json:"receiver_name"` + Province string `json:"province"` + } + parseData(respBody, &result) + t.Logf("✅ 地址详情: name=%s, province=%s", result.ReceiverName, result.Province) +} + +func TestMall_34_SetDefaultAddress(t *testing.T) { + if addressID2 == 0 { + t.Skip("无第二地址ID") + } + path := fmt.Sprintf("/api/mall/addresses/%d/default", addressID2) + resp, respBody, err := doRequest("PUT", path, nil, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + t.Logf("✅ 设置默认地址: id=%d", addressID2) +} + +func TestMall_35_DeleteAddress(t *testing.T) { + if addressID2 == 0 { + t.Skip("无地址ID2") + } + path := fmt.Sprintf("/api/mall/addresses/%d", addressID2) + resp, respBody, err := doRequest("DELETE", path, nil, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + t.Logf("✅ 删除地址: id=%d", addressID2) +} + +// ==================== 6. 订单流程 ==================== + +func TestMall_40_PreviewOrder(t *testing.T) { + if cartItemID == 0 { + t.Skip("无购物车项") + } + + body := map[string]interface{}{ + "cart_item_ids": []uint{cartItemID}, + "address_id": addressID, + } + resp, respBody, err := doRequest("POST", "/api/mall/orders/preview", body, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + 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"` + } + parseData(respBody, &result) + t.Logf("✅ 订单预览: total=%.2f, discount=%.2f, shipping=%.2f, pay=%.2f, maxPoints=%d", + result.TotalAmount, result.DiscountAmount, result.ShippingFee, result.PayAmount, result.MaxPointsUse) +} + +func TestMall_41_CreateOrder(t *testing.T) { + if cartItemID == 0 || addressID == 0 { + t.Skip("缺少购物车项或地址") + } + + body := map[string]interface{}{ + "address_id": addressID, + "cart_item_ids": []uint{cartItemID}, + "points_used": 0, + "remark": "测试订单", + } + resp, respBody, err := doRequest("POST", "/api/mall/orders", body, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + ID uint `json:"id"` + OrderNo string `json:"order_no"` + Status string `json:"status"` + PayAmount float64 `json:"pay_amount"` + PointsEarned int `json:"points_earned"` + Items []struct { + ProductName string `json:"product_name"` + Quantity int `json:"quantity"` + } `json:"items"` + } + parseData(respBody, &result) + orderID = result.ID + + if result.OrderNo == "" { + t.Error("订单号为空") + } + if result.Status != "pending" { + t.Errorf("订单状态异常: expected=pending, got=%s", result.Status) + } + + t.Logf("✅ 创建订单: id=%d, no=%s, status=%s, pay=%.2f, pointsEarned=%d", + orderID, result.OrderNo, result.Status, result.PayAmount, result.PointsEarned) + for _, item := range result.Items { + t.Logf(" 商品: %s x %d", item.ProductName, item.Quantity) + } +} + +func TestMall_42_GetOrders(t *testing.T) { + resp, respBody, err := doRequest("GET", "/api/mall/orders?page=1&page_size=10", nil, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + Orders []struct { + ID uint `json:"id"` + OrderNo string `json:"order_no"` + Status string `json:"status"` + } `json:"orders"` + PageInfo struct { + Total int64 `json:"total"` + } `json:"page_info"` + } + parseData(respBody, &result) + + if len(result.Orders) == 0 { + t.Error("订单列表为空") + } + t.Logf("✅ 订单列表: total=%d", result.PageInfo.Total) +} + +func TestMall_43_GetOrder(t *testing.T) { + if orderID == 0 { + t.Skip("无订单ID") + } + + path := fmt.Sprintf("/api/mall/orders/%d", orderID) + resp, respBody, err := doRequest("GET", path, nil, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + ID uint `json:"id"` + OrderNo string `json:"order_no"` + Status string `json:"status"` + ReceiverName string `json:"receiver_name"` + ReceiverAddr string `json:"receiver_addr"` + } + parseData(respBody, &result) + t.Logf("✅ 订单详情: no=%s, status=%s, receiver=%s, addr=%s", + result.OrderNo, result.Status, result.ReceiverName, result.ReceiverAddr) +} + +func TestMall_44_PayOrder(t *testing.T) { + if orderID == 0 { + t.Skip("无订单ID") + } + + body := map[string]interface{}{ + "pay_method": "wechat", + } + path := fmt.Sprintf("/api/mall/orders/%d/pay", orderID) + resp, respBody, err := doRequest("POST", path, body, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + Code int `json:"code"` + Message string `json:"message"` + } + parseData(respBody, &result) + t.Logf("✅ 支付订单: code=%d, msg=%s", result.Code, result.Message) + + // 验证支付后会员信息更新 + _, memberBody, _ := doRequest("GET", "/api/mall/member/info", nil, true) + var member struct { + Level string `json:"level"` + TotalSpent float64 `json:"total_spent"` + Points int `json:"points"` + } + parseData(memberBody, &member) + t.Logf(" 支付后会员: level=%s, totalSpent=%.2f, points=%d", + member.Level, member.TotalSpent, member.Points) + + if member.TotalSpent <= 0 { + t.Error("支付后累计消费应大于0") + } + if member.Points <= 0 { + t.Error("支付后积分应大于0") + } +} + +func TestMall_45_GetPointsAfterPay(t *testing.T) { + resp, respBody, err := doRequest("GET", "/api/mall/member/points/records?page=1&page_size=10", nil, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + Records []struct { + Type string `json:"type"` + Points int `json:"points"` + Balance int `json:"balance"` + Remark string `json:"remark"` + } `json:"records"` + } + parseData(respBody, &result) + + if len(result.Records) == 0 { + t.Error("支付后应有积分记录") + } + for _, r := range result.Records { + t.Logf(" 积分记录: type=%s, points=%d, balance=%d, remark=%s", + r.Type, r.Points, r.Balance, r.Remark) + } + t.Logf("✅ 积分记录: %d 条", len(result.Records)) +} + +// ==================== 7. 订单取消测试 ==================== + +func TestMall_50_CreateAndCancelOrder(t *testing.T) { + // 先添加一个商品到购物车 + // 获取第二个商品 + _, productsBody, _ := doRequest("GET", "/api/mall/products?page=1&page_size=10", nil, false) + var productsResult struct { + Products []struct { + ID uint `json:"id"` + } `json:"products"` + } + parseData(productsBody, &productsResult) + + if len(productsResult.Products) < 2 { + t.Skip("商品不足2个") + } + testProductID := productsResult.Products[1].ID + + // 添加到购物车 + cartBody := map[string]interface{}{ + "product_id": testProductID, + "quantity": 1, + } + _, cartResp, _ := doRequest("POST", "/api/mall/cart", cartBody, true) + var cartResult struct { + ID uint `json:"id"` + } + parseData(cartResp, &cartResult) + testCartItemID := cartResult.ID + + if testCartItemID == 0 { + t.Fatal("添加购物车失败") + } + + // 创建订单 + orderBody := map[string]interface{}{ + "address_id": addressID, + "cart_item_ids": []uint{testCartItemID}, + "remark": "待取消测试订单", + } + _, orderResp, _ := doRequest("POST", "/api/mall/orders", orderBody, true) + var orderResult struct { + ID uint `json:"id"` + OrderNo string `json:"order_no"` + } + parseData(orderResp, &orderResult) + cancelOrderID := orderResult.ID + + if cancelOrderID == 0 { + t.Fatal("创建订单失败") + } + t.Logf(" 创建待取消订单: id=%d, no=%s", cancelOrderID, orderResult.OrderNo) + + // 取消订单 + cancelBody := map[string]interface{}{ + "reason": "测试取消", + } + path := fmt.Sprintf("/api/mall/orders/%d/cancel", cancelOrderID) + resp, respBody, err := doRequest("POST", path, cancelBody, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + // 验证订单状态 + orderPath := fmt.Sprintf("/api/mall/orders/%d", cancelOrderID) + _, detailBody, _ := doRequest("GET", orderPath, nil, true) + var detail struct { + Status string `json:"status"` + CancelReason string `json:"cancel_reason"` + } + parseData(detailBody, &detail) + + if detail.Status != "cancelled" { + t.Errorf("订单状态异常: expected=cancelled, got=%s", detail.Status) + } + t.Logf("✅ 取消订单: status=%s, reason=%s", detail.Status, detail.CancelReason) +} + +// ==================== 8. 体质推荐 ==================== + +func TestMall_60_ConstitutionRecommend(t *testing.T) { + resp, respBody, err := doRequest("GET", "/api/mall/products/constitution-recommend", nil, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + Products []struct { + Name string `json:"name"` + } `json:"products"` + Constitution string `json:"constitution"` + Reason string `json:"reason"` + } + parseData(respBody, &result) + t.Logf("✅ 体质推荐: constitution=%s, reason=%s, products=%d", + result.Constitution, result.Reason, len(result.Products)) + for _, p := range result.Products { + t.Logf(" 推荐: %s", p.Name) + } +} + +// ==================== 9. 购物车清空 ==================== + +func TestMall_70_ClearCart(t *testing.T) { + resp, respBody, err := doRequest("DELETE", "/api/mall/cart/clear", nil, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + // 验证购物车为空 + _, cartBody, _ := doRequest("GET", "/api/mall/cart", nil, true) + var cartResult struct { + Items []interface{} `json:"items"` + TotalCount int `json:"total_count"` + } + parseData(cartBody, &cartResult) + + if len(cartResult.Items) != 0 { + t.Errorf("购物车未清空: items=%d", len(cartResult.Items)) + } + t.Log("✅ 购物车已清空") +} + +// ==================== 10. 按状态筛选订单 ==================== + +func TestMall_80_GetOrdersByStatus(t *testing.T) { + // 查询已支付订单 + resp, respBody, err := doRequest("GET", "/api/mall/orders?status=paid&page=1&page_size=10", nil, true) + if err != nil { + t.Fatalf("请求失败: %v", err) + } + if resp.StatusCode != 200 { + t.Fatalf("状态码=%d, body=%s", resp.StatusCode, string(respBody)) + } + + var result struct { + Orders []struct { + Status string `json:"status"` + } `json:"orders"` + PageInfo struct { + Total int64 `json:"total"` + } `json:"page_info"` + } + parseData(respBody, &result) + + for _, o := range result.Orders { + if o.Status != "paid" { + t.Errorf("筛选结果包含非已支付订单: status=%s", o.Status) + } + } + t.Logf("✅ 已支付订单: %d 个", result.PageInfo.Total) + + // 查询已取消订单 + resp2, respBody2, _ := doRequest("GET", "/api/mall/orders?status=cancelled&page=1&page_size=10", nil, true) + if resp2.StatusCode == 200 { + var result2 struct { + PageInfo struct { + Total int64 `json:"total"` + } `json:"page_info"` + } + parseData(respBody2, &result2) + t.Logf("✅ 已取消订单: %d 个", result2.PageInfo.Total) + } +} diff --git a/backend/healthapi/tests/seed.go b/backend/healthapi/tests/seed.go new file mode 100644 index 0000000..787fa35 --- /dev/null +++ b/backend/healthapi/tests/seed.go @@ -0,0 +1,195 @@ +package tests + +import ( + "healthapi/internal/model" + + "gorm.io/gorm" +) + +// SeedMallTestData 插入商城测试数据 +func SeedMallTestData(db *gorm.DB) error { + // ===== 商品分类 ===== + categories := []model.ProductCategory{ + {Name: "养生茶饮", Sort: 1, Icon: "🍵", Description: "各类养生茶饮产品", IsActive: true}, + {Name: "滋补膏方", Sort: 2, Icon: "🍯", Description: "传统膏方滋补品", IsActive: true}, + {Name: "中药材", Sort: 3, Icon: "🌿", Description: "道地中药材", IsActive: true}, + {Name: "保健食品", Sort: 4, Icon: "💊", Description: "各类保健食品", IsActive: true}, + } + + for i := range categories { + var count int64 + db.Model(&model.ProductCategory{}).Where("name = ?", categories[i].Name).Count(&count) + if count == 0 { + if err := db.Create(&categories[i]).Error; err != nil { + return err + } + } else { + db.Where("name = ?", categories[i].Name).First(&categories[i]) + } + } + + // ===== 测试商品 ===== + products := []model.Product{ + { + CategoryID: uint(categories[0].ID), + Category: "养生茶饮", + Name: "红枣枸杞养生茶", + Description: "精选新疆大枣和宁夏枸杞,温和养生", + MainImage: "https://example.com/tea1.jpg", + Price: 39.9, + OriginalPrice: 59.9, + Stock: 100, + IsActive: true, + IsFeatured: true, + Efficacy: "补气养血、美容养颜", + Suitable: "气虚质、血虚人群", + ConstitutionTypes: `["qixu","yinxu"]`, + HealthTags: `["补气","养血","美容"]`, + Ingredients: "红枣、枸杞、桂圆", + Usage: "每日1-2袋,开水冲泡5分钟", + Sort: 10, + }, + { + CategoryID: uint(categories[0].ID), + Category: "养生茶饮", + Name: "菊花决明子茶", + Description: "清肝明目,降火润燥", + MainImage: "https://example.com/tea2.jpg", + Price: 29.9, + OriginalPrice: 45.0, + Stock: 200, + IsActive: true, + IsFeatured: true, + Efficacy: "清肝明目、降火润燥", + Suitable: "湿热质、肝火旺盛", + ConstitutionTypes: `["shire","yinxu"]`, + HealthTags: `["清肝","明目","降火"]`, + Ingredients: "菊花、决明子、金银花", + Usage: "每日1袋,沸水冲泡", + Sort: 9, + }, + { + CategoryID: uint(categories[1].ID), + Category: "滋补膏方", + Name: "阿胶固元膏", + Description: "传统古法熬制,滋阴润燥", + MainImage: "https://example.com/gao1.jpg", + Price: 168.0, + OriginalPrice: 238.0, + Stock: 50, + IsActive: true, + IsFeatured: true, + Efficacy: "滋阴润燥、补血养颜", + Suitable: "阴虚质、血虚质", + ConstitutionTypes: `["yinxu"]`, + HealthTags: `["滋阴","补血","养颜"]`, + Ingredients: "阿胶、黑芝麻、核桃仁、红枣、冰糖", + Usage: "每日早晚各一勺,温水冲服", + Sort: 8, + }, + { + CategoryID: uint(categories[2].ID), + Category: "中药材", + Name: "西洋参片", + Description: "进口花旗参,补气养阴", + MainImage: "https://example.com/herb1.jpg", + Price: 128.0, + OriginalPrice: 188.0, + Stock: 80, + IsActive: true, + IsFeatured: false, + Efficacy: "补气养阴、清热生津", + Suitable: "气虚质、阴虚质", + ConstitutionTypes: `["qixu","yinxu"]`, + HealthTags: `["补气","养阴","生津"]`, + Ingredients: "西洋参", + Usage: "每日3-5片,含服或泡水", + Sort: 7, + }, + { + CategoryID: uint(categories[3].ID), + Category: "保健食品", + Name: "深海鱼油软胶囊", + Description: "富含Omega-3,呵护心脑血管", + MainImage: "https://example.com/health1.jpg", + Price: 89.0, + OriginalPrice: 129.0, + Stock: 150, + IsActive: true, + IsFeatured: false, + Efficacy: "调节血脂、保护心脑血管", + Suitable: "中老年人、血脂偏高者", + ConstitutionTypes: `["tanshi","xueyu"]`, + HealthTags: `["调脂","护心","血管"]`, + Ingredients: "深海鱼油、维生素E", + Usage: "每日2粒,随餐服用", + Sort: 6, + }, + } + + for i := range products { + var count int64 + db.Model(&model.Product{}).Where("name = ?", products[i].Name).Count(&count) + if count == 0 { + if err := db.Create(&products[i]).Error; err != nil { + return err + } + } else { + db.Where("name = ?", products[i].Name).First(&products[i]) + } + } + + // ===== 商品 SKU ===== + // 为第一个商品创建 SKU + var firstProduct model.Product + db.Where("name = ?", "红枣枸杞养生茶").First(&firstProduct) + if firstProduct.ID > 0 { + skus := []model.ProductSku{ + { + ProductID: uint(firstProduct.ID), + SkuCode: "TEA-RZ-250", + Name: "250g袋装", + Attributes: `{"weight":"250g","package":"袋装"}`, + Price: 39.9, + Stock: 60, + IsActive: true, + }, + { + ProductID: uint(firstProduct.ID), + SkuCode: "TEA-RZ-500", + Name: "500g礼盒装", + Attributes: `{"weight":"500g","package":"礼盒"}`, + Price: 69.9, + Stock: 40, + IsActive: true, + }, + } + for i := range skus { + var count int64 + db.Model(&model.ProductSku{}).Where("sku_code = ?", skus[i].SkuCode).Count(&count) + if count == 0 { + db.Create(&skus[i]) + } + } + } + + return nil +} + +// CleanMallTestData 清理商城测试数据(购物车、地址、订单等用户生成数据) +func CleanMallTestData(db *gorm.DB, userID uint) { + db.Where("user_id = ?", userID).Delete(&model.CartItem{}) + db.Where("user_id = ?", userID).Delete(&model.Address{}) + // 清理订单项 + var orders []model.Order + db.Where("user_id = ?", userID).Find(&orders) + for _, o := range orders { + db.Where("order_id = ?", o.ID).Delete(&model.OrderItem{}) + } + db.Where("user_id = ?", userID).Delete(&model.Order{}) + db.Where("user_id = ?", userID).Delete(&model.PointsRecord{}) + + // 恢复库存(简单做法:将被测试修改的商品库存复原) + db.Model(&model.Product{}).Where("name = ?", "红枣枸杞养生茶").Update("stock", 100) + db.Model(&model.Product{}).Where("name = ?", "菊花决明子茶").Update("stock", 200) +} diff --git a/server/cmd/server/main.go b/server/cmd/server/main.go deleted file mode 100644 index c0b2494..0000000 --- a/server/cmd/server/main.go +++ /dev/null @@ -1,57 +0,0 @@ -package main - -import ( - "fmt" - "log" - - "health-ai/internal/api" - "health-ai/internal/config" - "health-ai/internal/database" - "health-ai/internal/model" - "health-ai/pkg/jwt" -) - -func main() { - log.Println("Health AI Server Starting...") - - // 加载配置 - if err := config.LoadConfig("config.yaml"); err != nil { - log.Fatalf("Failed to load config: %v", err) - } - log.Println("Config loaded") - - // 初始化数据库 - if err := database.InitDatabase(&config.AppConfig.Database); err != nil { - log.Fatalf("Failed to init database: %v", err) - } - log.Println("Database connected") - - // 自动迁移 - if err := database.AutoMigrate(model.AllModels()...); err != nil { - log.Fatalf("Failed to migrate: %v", err) - } - log.Println("Database migrated") - - // 初始化问卷题库 - if err := database.SeedQuestionBank(); err != nil { - log.Fatalf("Failed to seed question bank: %v", err) - } - - // 创建测试用户 - if err := database.SeedTestUser(); err != nil { - log.Printf("Warning: Failed to create test user: %v", err) - } - - // 初始化 JWT - jwt.Init(config.AppConfig.JWT.Secret, config.AppConfig.JWT.ExpireHours) - log.Println("JWT initialized") - - // 启动服务器 - router := api.SetupRouter(config.AppConfig.Server.Mode) - addr := fmt.Sprintf(":%d", config.AppConfig.Server.Port) - log.Printf("Server running on http://localhost%s", addr) - - if err := router.Run(addr); err != nil { - log.Fatalf("Failed to start server: %v", err) - } -} diff --git a/server/config.yaml b/server/config.yaml deleted file mode 100644 index 29fef7f..0000000 --- a/server/config.yaml +++ /dev/null @@ -1,40 +0,0 @@ -server: - port: 8080 - mode: debug # debug, release, test - -database: - driver: sqlite # sqlite, postgres, mysql - sqlite: - path: ./data/health.db - postgres: - host: localhost - port: 5432 - user: postgres - password: "" - dbname: health_app - mysql: - host: localhost - port: 3306 - user: root - password: "" - dbname: health_app - -jwt: - secret: health-ai-secret-key-change-in-production - expire_hours: 24 - -ai: - provider: aliyun # openai, aliyun (通义千问) - max_history_messages: 10 - max_tokens: 2000 - - # OpenAI 配置 - openai: - api_key: "" - base_url: "https://api.openai.com/v1" - model: "gpt-3.5-turbo" - - # 阿里云通义千问配置 - aliyun: - api_key: "sk-53b4777561624ba98246c7b9990c5e8b" - model: "qwen-turbo" # 可选: qwen-turbo, qwen-plus, qwen-max diff --git a/server/data/health.db b/server/data/health.db deleted file mode 100644 index d6b6b17..0000000 Binary files a/server/data/health.db and /dev/null differ diff --git a/server/docs/API.md b/server/docs/API.md deleted file mode 100644 index fa02d1a..0000000 --- a/server/docs/API.md +++ /dev/null @@ -1,662 +0,0 @@ -# 健康AI问询助手 - 后端API文档 - -> 后端服务地址: `http://localhost:8080` -> -> 所有需要认证的接口,请在Header中添加: `Authorization: Bearer ` - ---- - -## 一、认证接口 - -### 1.1 用户注册 -- **POST** `/api/auth/register` - -**请求体:** -```json -{ - "phone": "13800138000", - "password": "123456", - "nickname": "用户昵称" // 可选 -} -``` - -**响应:** -```json -{ - "code": 0, - "message": "success", - "data": { - "token": "eyJhbGc...", - "user_id": 1, - "nickname": "用户昵称", - "avatar": "", - "survey_completed": false - } -} -``` - -### 1.2 用户登录 -- **POST** `/api/auth/login` - -**请求体:** -```json -{ - "phone": "13800138000", - "password": "123456" -} -``` - -**响应:** 同注册接口 - -### 1.3 刷新Token -- **POST** `/api/auth/refresh` -- **需要认证** - -**响应:** -```json -{ - "code": 0, - "message": "success", - "data": { - "token": "新的token" - } -} -``` - -### 1.4 发送验证码 -- **POST** `/api/auth/send-code` - -**请求体:** -```json -{ - "phone": "13800138000", - "type": "register" // register/login/reset -} -``` - -**响应:** -```json -{ - "code": 0, - "message": "验证码已发送", - "data": { - "phone": "138****8000", - "expires_in": 300, - "demo_code": "123456" // 演示环境,正式环境无此字段 - } -} -``` - -> **注意**: 当前为演示版本,验证码固定为 `123456`。正式环境需要接入短信服务。 - ---- - -## 二、用户接口 - -### 2.1 获取用户信息 -- **GET** `/api/user/profile` -- **需要认证** - -**响应:** -```json -{ - "code": 0, - "message": "success", - "data": { - "user_id": 1, - "phone": "13800138000", - "email": "", - "nickname": "用户昵称", - "avatar": "", - "survey_completed": false - } -} -``` - -### 2.2 更新用户资料 -- **PUT** `/api/user/profile` -- **需要认证** - -**请求体:** -```json -{ - "nickname": "新昵称", - "avatar": "头像URL" -} -``` - ---- - -## 三、健康调查接口 - -### 3.1 获取调查状态 -- **GET** `/api/survey/status` -- **需要认证** - -**响应:** -```json -{ - "code": 0, - "message": "success", - "data": { - "basic_info": true, - "lifestyle": false, - "medical_history": false, - "family_history": false, - "allergy": false, - "all_completed": false - } -} -``` - -### 3.2 提交基础信息 -- **POST** `/api/survey/basic-info` -- **需要认证** - -**请求体:** -```json -{ - "name": "张三", - "birth_date": "1990-05-15", - "gender": "male", // male/female - "height": 175, // cm - "weight": 70, // kg - "blood_type": "A", // A/B/AB/O - "occupation": "工程师", - "marital_status": "married", // single/married/divorced - "region": "北京" -} -``` - -### 3.3 提交生活习惯 -- **POST** `/api/survey/lifestyle` -- **需要认证** - -**请求体:** -```json -{ - "sleep_time": "23:00", - "wake_time": "07:00", - "sleep_quality": "normal", // good/normal/poor - "meal_regularity": "regular", // regular/irregular - "diet_preference": "清淡", - "daily_water_ml": 2000, - "exercise_frequency": "sometimes", // never/sometimes/often/daily - "exercise_type": "跑步", - "exercise_duration_min": 30, - "is_smoker": false, - "alcohol_frequency": "never" // never/sometimes/often -} -``` - -### 3.4 提交病史 -- **POST** `/api/survey/medical-history` -- **需要认证** - -**请求体:** -```json -{ - "disease_name": "高血压", - "disease_type": "chronic", // chronic/surgery/other - "diagnosed_date": "2020-01", - "status": "controlled", // cured/treating/controlled - "notes": "备注信息" -} -``` - -### 3.5 批量提交病史(覆盖式) -- **POST** `/api/survey/medical-history/batch` -- **需要认证** - -**请求体:** -```json -{ - "histories": [ - { - "disease_name": "高血压", - "disease_type": "chronic", - "diagnosed_date": "2020-01", - "status": "controlled", - "notes": "" - } - ] -} -``` - -### 3.6 提交家族病史 -- **POST** `/api/survey/family-history` -- **需要认证** - -**请求体:** -```json -{ - "relation": "father", // father/mother/grandparent - "disease_name": "糖尿病", - "notes": "" -} -``` - -### 3.7 提交过敏信息 -- **POST** `/api/survey/allergy` -- **需要认证** - -**请求体:** -```json -{ - "allergy_type": "drug", // drug/food/other - "allergen": "青霉素", - "severity": "moderate", // mild/moderate/severe - "reaction_desc": "皮疹" -} -``` - -### 3.8 完成调查 -- **POST** `/api/survey/complete` -- **需要认证** - ---- - -## 四、体质辨识接口 - -### 4.1 获取问卷题目 -- **GET** `/api/constitution/questions` -- **需要认证** - -**响应:** -```json -{ - "code": 0, - "message": "success", - "data": [ - { - "id": 1, - "constitution_type": "pinghe", - "question_text": "您精力充沛吗?", - "options": "[\"没有\",\"很少\",\"有时\",\"经常\",\"总是\"]", - "order_num": 1 - } - ] -} -``` - -### 4.2 获取分组的问卷题目 -- **GET** `/api/constitution/questions/grouped` -- **需要认证** - -### 4.3 提交测评 -- **POST** `/api/constitution/submit` -- **需要认证** - -**请求体:** -```json -{ - "answers": [ - {"question_id": 1, "score": 3}, - {"question_id": 2, "score": 2} - // ... 所有题目的答案,score: 1-5 对应选项 - ] -} -``` - -**响应:** -```json -{ - "code": 0, - "message": "success", - "data": { - "id": 1, - "primary_constitution": { - "type": "qixu", - "name": "气虚质", - "score": 65.5, - "description": "元气不足,容易疲劳..." - }, - "secondary_constitutions": [], - "all_scores": [ - {"type": "qixu", "name": "气虚质", "score": 65.5, "description": "..."}, - {"type": "yangxu", "name": "阳虚质", "score": 45.2, "description": "..."} - ], - "recommendations": { - "qixu": { - "diet": "宜食益气健脾食物...", - "lifestyle": "避免劳累...", - "exercise": "宜柔和运动...", - "emotion": "避免过度思虑" - } - }, - "assessed_at": "2026-02-01T16:30:00Z" - } -} -``` - -### 4.4 获取最新测评结果 -- **GET** `/api/constitution/result` -- **需要认证** - -### 4.5 获取测评历史 -- **GET** `/api/constitution/history?limit=10` -- **需要认证** - -### 4.6 获取调养建议 -- **GET** `/api/constitution/recommendations` -- **需要认证** - ---- - -## 五、AI对话接口 - -### 5.1 获取对话列表 -- **GET** `/api/conversations` -- **需要认证** - -**响应:** -```json -{ - "code": 0, - "message": "success", - "data": [ - { - "id": 1, - "title": "新对话 02-01 16:30", - "created_at": "2026-02-01T16:30:00Z", - "updated_at": "2026-02-01T16:35:00Z" - } - ] -} -``` - -### 5.2 创建新对话 -- **POST** `/api/conversations` -- **需要认证** - -**请求体:** -```json -{ - "title": "对话标题" // 可选 -} -``` - -### 5.3 获取对话详情 -- **GET** `/api/conversations/:id` -- **需要认证** - -**响应:** -```json -{ - "code": 0, - "message": "success", - "data": { - "id": 1, - "title": "最近总是感觉疲劳...", - "messages": [ - { - "id": 1, - "role": "user", - "content": "最近总是感觉疲劳怎么办?", - "created_at": "2026-02-01T16:30:00Z" - }, - { - "id": 2, - "role": "assistant", - "content": "【情况分析】...", - "created_at": "2026-02-01T16:30:05Z" - } - ], - "created_at": "2026-02-01T16:30:00Z", - "updated_at": "2026-02-01T16:30:05Z" - } -} -``` - -### 5.4 删除对话 -- **DELETE** `/api/conversations/:id` -- **需要认证** - -### 5.5 发送消息 -- **POST** `/api/conversations/:id/messages` -- **需要认证** - -**请求体:** -```json -{ - "content": "我最近总是感觉疲劳怎么办?" -} -``` - -**响应:** -```json -{ - "code": 0, - "message": "success", - "data": { - "id": 2, - "role": "assistant", - "content": "【情况分析】根据您的描述...\n【建议】\n1. 保证充足睡眠...", - "created_at": "2026-02-01T16:30:05Z" - } -} -``` - ---- - -## 六、健康档案接口 - -### 6.1 获取完整健康档案 -- **GET** `/api/user/health-profile` -- **需要认证** - -### 6.2 更新健康档案 -- **PUT** `/api/user/health-profile` -- **需要认证** - -**请求体:** -```json -{ - "name": "张三", - "birth_date": "1990-05-15", - "gender": "male", - "height": 175, - "weight": 70, - "blood_type": "A", - "occupation": "工程师", - "marital_status": "married", - "region": "北京" -} -``` - -**响应:** -```json -{ - "code": 0, - "message": "success", - "data": { - "id": 1, - "user_id": 1, - "name": "张三", - "birth_date": "1990-05-15T00:00:00Z", - "gender": "male", - "height": 175, - "weight": 70, - "bmi": 22.86, - "blood_type": "A", - "occupation": "工程师", - "marital_status": "married", - "region": "北京" - } -} -``` - -### 6.3 获取基础档案 -- **GET** `/api/user/basic-profile` -- **需要认证** - -### 6.4 获取生活习惯 -- **GET** `/api/user/lifestyle` -- **需要认证** - -### 6.5 更新生活习惯 -- **PUT** `/api/user/lifestyle` -- **需要认证** - -**请求体:** -```json -{ - "sleep_time": "23:00", - "wake_time": "07:00", - "sleep_quality": "normal", - "meal_regularity": "regular", - "diet_preference": "清淡", - "daily_water_ml": 2000, - "exercise_frequency": "sometimes", - "exercise_type": "跑步", - "exercise_duration_min": 30, - "is_smoker": false, - "alcohol_frequency": "never" -} -``` - -**响应:** -```json -{ - "code": 0, - "message": "success", - "data": { - "id": 1, - "user_id": 1, - "sleep_time": "23:00", - "wake_time": "07:00", - "sleep_quality": "normal", - "meal_regularity": "regular", - "diet_preference": "清淡", - "daily_water_ml": 2000, - "exercise_frequency": "sometimes", - "exercise_type": "跑步", - "exercise_duration_min": 30, - "is_smoker": false, - "alcohol_frequency": "never" - } -} -``` - -### 6.6 获取病史列表 -- **GET** `/api/user/medical-history` -- **需要认证** - -### 6.7 删除病史记录 -- **DELETE** `/api/user/medical-history/:id` -- **需要认证** - -### 6.8 获取家族病史 -- **GET** `/api/user/family-history` -- **需要认证** - -### 6.9 获取过敏记录 -- **GET** `/api/user/allergy-records` -- **需要认证** - ---- - -## 七、产品接口 - -### 7.1 获取产品列表 -- **GET** `/api/products?page=1&page_size=20` - -### 7.2 获取产品详情 -- **GET** `/api/products/:id` - -### 7.3 按分类获取产品 -- **GET** `/api/products/category?category=补气类` - -### 7.4 搜索产品 -- **GET** `/api/products/search?keyword=疲劳` - -### 7.5 获取推荐产品(基于用户体质) -- **GET** `/api/products/recommend` -- **需要认证** - -### 7.6 获取购买历史 -- **GET** `/api/user/purchase-history` -- **需要认证** - ---- - -## 八、商城同步接口 - -### 8.1 同步购买记录 -- **POST** `/api/sync/purchase` - -**请求体:** -```json -{ - "user_id": 1, - "order_no": "ORDER123456", - "products": [ - {"id": 1, "name": "黄芪精"}, - {"id": 2, "name": "人参蜂王浆"} - ], - "created_at": "2026-02-01T16:00:00Z" -} -``` - ---- - -## 九、错误码说明 - -| Code | 说明 | -|------|------| -| 0 | 成功 | -| 400 | 参数错误 | -| 401 | 未授权/Token无效 | -| 403 | 禁止访问 | -| 404 | 资源不存在 | -| 500 | 服务器错误 | - ---- - -## 十、体质类型对照 - -| 类型代码 | 中文名称 | -|----------|----------| -| pinghe | 平和质 | -| qixu | 气虚质 | -| yangxu | 阳虚质 | -| yinxu | 阴虚质 | -| tanshi | 痰湿质 | -| shire | 湿热质 | -| xueyu | 血瘀质 | -| qiyu | 气郁质 | -| tebing | 特禀质 | - ---- - -## 十一、配置说明 - -后端配置文件 `config.yaml`: - -```yaml -server: - port: 8080 - mode: debug - -database: - driver: sqlite - sqlite: - path: ./data/health.db - -jwt: - secret: your-secret-key - expire_hours: 24 - -ai: - provider: aliyun # 或 openai - max_history_messages: 10 - aliyun: - api_key: "您的阿里云DashScope API Key" - model: "qwen-turbo" -``` - ---- - -## 联系方式 - -如有接口问题,请创建 Issue 或联系后端开发团队。 diff --git a/server/go.mod b/server/go.mod deleted file mode 100644 index 3984d9d..0000000 --- a/server/go.mod +++ /dev/null @@ -1,58 +0,0 @@ -module health-ai - -go 1.25.5 - -require ( - github.com/gin-contrib/cors v1.7.6 - github.com/gin-gonic/gin v1.11.0 - github.com/golang-jwt/jwt/v5 v5.3.1 - github.com/spf13/viper v1.21.0 - golang.org/x/crypto v0.47.0 - gorm.io/driver/sqlite v1.6.0 - gorm.io/gorm v1.31.1 -) - -require ( - github.com/bytedance/sonic v1.14.0 // indirect - github.com/bytedance/sonic/loader v0.3.0 // indirect - github.com/cloudwego/base64x v0.1.6 // indirect - github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.9 // indirect - github.com/gin-contrib/sse v1.1.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.27.0 // indirect - github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/goccy/go-json v0.10.5 // indirect - github.com/goccy/go-yaml v1.18.0 // indirect - github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.5 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/leodido/go-urn v1.4.0 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.22 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.4 // indirect - github.com/quic-go/qpack v0.5.1 // indirect - github.com/quic-go/quic-go v0.54.0 // indirect - github.com/sagikazarmark/locafero v0.11.0 // indirect - github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect - github.com/spf13/afero v1.15.0 // indirect - github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.3.0 // indirect - go.uber.org/mock v0.5.0 // indirect - go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/arch v0.20.0 // indirect - golang.org/x/mod v0.31.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 - golang.org/x/tools v0.40.0 // indirect - google.golang.org/protobuf v1.36.9 // indirect -) diff --git a/server/go.sum b/server/go.sum deleted file mode 100644 index aa71747..0000000 --- a/server/go.sum +++ /dev/null @@ -1,133 +0,0 @@ -github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= -github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= -github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= -github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= -github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= -github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= -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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= -github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= -github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= -github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= -github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= -github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= -github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= -github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= -github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= -github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= -github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= -github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= -github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= -github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= -github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= -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.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= -github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= -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/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= -github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= -github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= -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/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= -github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= -github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= -github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= -github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= -github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= -github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= -github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= -github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U= -github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I= -github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= -github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= -github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= -github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= -github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= -github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY= -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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= -github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= -go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= -go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= -go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= -go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= -golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= -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/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= -golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= -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.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= -golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA= -golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc= -google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= -google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= -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/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/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= diff --git a/server/internal/api/handler/auth.go b/server/internal/api/handler/auth.go deleted file mode 100644 index a534b6f..0000000 --- a/server/internal/api/handler/auth.go +++ /dev/null @@ -1,183 +0,0 @@ -package handler - -import ( - "health-ai/internal/api/middleware" - "health-ai/internal/service" - "health-ai/pkg/response" - - "github.com/gin-gonic/gin" -) - -type AuthHandler struct { - authService *service.AuthService -} - -func NewAuthHandler() *AuthHandler { - return &AuthHandler{ - authService: service.NewAuthService(), - } -} - -// SendCodeRequest 发送验证码请求 -type SendCodeRequest struct { - Phone string `json:"phone" binding:"required"` - Type string `json:"type" binding:"required,oneof=register login reset"` // register, login, reset -} - -// SendCode 发送验证码 -// @Summary 发送手机验证码 -// @Tags 认证 -// @Accept json -// @Produce json -// @Param request body SendCodeRequest true "手机号和验证码类型" -// @Success 200 {object} response.Response -// @Router /api/auth/send-code [post] -func (h *AuthHandler) SendCode(c *gin.Context) { - var req SendCodeRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误: "+err.Error()) - return - } - - // 验证手机号格式 - if len(req.Phone) != 11 { - response.BadRequest(c, "手机号格式不正确") - return - } - - // TODO: 实际项目中应该: - // 1. 生成6位随机验证码 - // 2. 存储到Redis(设置5分钟过期) - // 3. 调用短信服务发送验证码 - // 4. 限制发送频率(如60秒一次) - - // 当前为演示版本,返回模拟成功 - response.SuccessWithMessage(c, "验证码已发送", gin.H{ - "phone": req.Phone[:3] + "****" + req.Phone[7:], - "expires_in": 300, // 5分钟有效 - // 演示环境下返回固定验证码,正式环境请删除 - "demo_code": "123456", - }) -} - -// Register 用户注册 -// @Summary 用户注册 -// @Tags 认证 -// @Accept json -// @Produce json -// @Param request body service.RegisterRequest true "注册信息" -// @Success 200 {object} response.Response{data=service.AuthResponse} -// @Router /api/auth/register [post] -func (h *AuthHandler) Register(c *gin.Context) { - var req service.RegisterRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误: "+err.Error()) - return - } - - result, err := h.authService.Register(&req) - if err != nil { - response.Error(c, 400, err.Error()) - return - } - - response.Success(c, result) -} - -// Login 用户登录 -// @Summary 用户登录 -// @Tags 认证 -// @Accept json -// @Produce json -// @Param request body service.LoginRequest true "登录信息" -// @Success 200 {object} response.Response{data=service.AuthResponse} -// @Router /api/auth/login [post] -func (h *AuthHandler) Login(c *gin.Context) { - var req service.LoginRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误: "+err.Error()) - return - } - - result, err := h.authService.Login(&req) - if err != nil { - response.Error(c, 400, err.Error()) - return - } - - response.Success(c, result) -} - -// RefreshToken 刷新Token -// @Summary 刷新Token -// @Tags 认证 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Success 200 {object} response.Response{data=map[string]string} -// @Router /api/auth/refresh [post] -func (h *AuthHandler) RefreshToken(c *gin.Context) { - // 从header获取旧token - oldToken := c.GetHeader("Authorization") - if len(oldToken) > 7 { - oldToken = oldToken[7:] // 去掉 "Bearer " - } - - newToken, err := h.authService.RefreshToken(oldToken) - if err != nil { - response.Unauthorized(c, "Token刷新失败") - return - } - - response.Success(c, gin.H{"token": newToken}) -} - -// GetUserInfo 获取当前用户信息 -// @Summary 获取当前用户信息 -// @Tags 用户 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Success 200 {object} response.Response{data=service.UserInfoResponse} -// @Router /api/user/profile [get] -func (h *AuthHandler) GetUserInfo(c *gin.Context) { - userID := middleware.GetUserID(c) - result, err := h.authService.GetUserInfo(userID) - if err != nil { - response.Error(c, 400, err.Error()) - return - } - response.Success(c, result) -} - -// UpdateProfile 更新用户资料 -// @Summary 更新用户资料 -// @Tags 用户 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Param request body UpdateProfileRequest true "更新信息" -// @Success 200 {object} response.Response -// @Router /api/user/profile [put] -func (h *AuthHandler) UpdateProfile(c *gin.Context) { - userID := middleware.GetUserID(c) - - var req UpdateProfileRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误: "+err.Error()) - return - } - - if err := h.authService.UpdateProfile(userID, req.Nickname, req.Avatar); err != nil { - response.Error(c, 400, err.Error()) - return - } - - response.SuccessWithMessage(c, "更新成功", nil) -} - -// UpdateProfileRequest 更新资料请求 -type UpdateProfileRequest struct { - Nickname string `json:"nickname"` - Avatar string `json:"avatar"` -} diff --git a/server/internal/api/handler/constitution.go b/server/internal/api/handler/constitution.go deleted file mode 100644 index 16378fd..0000000 --- a/server/internal/api/handler/constitution.go +++ /dev/null @@ -1,155 +0,0 @@ -package handler - -import ( - "strconv" - - "health-ai/internal/api/middleware" - "health-ai/internal/service" - "health-ai/pkg/response" - - "github.com/gin-gonic/gin" -) - -type ConstitutionHandler struct { - constitutionService *service.ConstitutionService -} - -func NewConstitutionHandler() *ConstitutionHandler { - return &ConstitutionHandler{ - constitutionService: service.NewConstitutionService(), - } -} - -// GetQuestions 获取体质问卷题目 -// @Summary 获取体质问卷题目 -// @Tags 体质辨识 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Success 200 {object} response.Response{data=[]model.QuestionBank} -// @Router /api/constitution/questions [get] -func (h *ConstitutionHandler) GetQuestions(c *gin.Context) { - questions, err := h.constitutionService.GetQuestions() - if err != nil { - response.Error(c, 500, err.Error()) - return - } - response.Success(c, questions) -} - -// GetQuestionsGrouped 获取分组的体质问卷题目 -// @Summary 获取分组的体质问卷题目 -// @Tags 体质辨识 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Success 200 {object} response.Response{data=[]service.QuestionGroup} -// @Router /api/constitution/questions/grouped [get] -func (h *ConstitutionHandler) GetQuestionsGrouped(c *gin.Context) { - groups, err := h.constitutionService.GetQuestionsGrouped() - if err != nil { - response.Error(c, 500, err.Error()) - return - } - response.Success(c, groups) -} - -// SubmitAssessment 提交体质测评 -// @Summary 提交体质测评答案 -// @Tags 体质辨识 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Param request body service.SubmitAssessmentRequest true "测评答案" -// @Success 200 {object} response.Response{data=service.AssessmentResult} -// @Router /api/constitution/submit [post] -func (h *ConstitutionHandler) SubmitAssessment(c *gin.Context) { - userID := middleware.GetUserID(c) - - var req service.SubmitAssessmentRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误: "+err.Error()) - return - } - - if len(req.Answers) == 0 { - response.BadRequest(c, "请至少回答一道问题") - return - } - - result, err := h.constitutionService.SubmitAssessment(userID, &req) - if err != nil { - response.Error(c, 500, err.Error()) - return - } - - response.Success(c, result) -} - -// GetResult 获取最新体质测评结果 -// @Summary 获取最新体质测评结果 -// @Tags 体质辨识 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Success 200 {object} response.Response{data=service.AssessmentResult} -// @Router /api/constitution/result [get] -func (h *ConstitutionHandler) GetResult(c *gin.Context) { - userID := middleware.GetUserID(c) - - result, err := h.constitutionService.GetLatestResult(userID) - if err != nil { - response.Error(c, 404, err.Error()) - return - } - - response.Success(c, result) -} - -// GetHistory 获取体质测评历史 -// @Summary 获取体质测评历史 -// @Tags 体质辨识 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Param limit query int false "返回数量限制,默认10" -// @Success 200 {object} response.Response{data=[]service.AssessmentResult} -// @Router /api/constitution/history [get] -func (h *ConstitutionHandler) GetHistory(c *gin.Context) { - userID := middleware.GetUserID(c) - - limit := 10 - if limitStr := c.Query("limit"); limitStr != "" { - if l, err := strconv.Atoi(limitStr); err == nil && l > 0 { - limit = l - } - } - - results, err := h.constitutionService.GetHistory(userID, limit) - if err != nil { - response.Error(c, 500, err.Error()) - return - } - - response.Success(c, results) -} - -// GetRecommendations 获取体质调养建议 -// @Summary 获取体质调养建议 -// @Tags 体质辨识 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Success 200 {object} response.Response{data=map[string]map[string]string} -// @Router /api/constitution/recommendations [get] -func (h *ConstitutionHandler) GetRecommendations(c *gin.Context) { - userID := middleware.GetUserID(c) - - recommendations, err := h.constitutionService.GetRecommendations(userID) - if err != nil { - response.Error(c, 404, err.Error()) - return - } - - response.Success(c, recommendations) -} diff --git a/server/internal/api/handler/conversation.go b/server/internal/api/handler/conversation.go deleted file mode 100644 index ffc75f3..0000000 --- a/server/internal/api/handler/conversation.go +++ /dev/null @@ -1,183 +0,0 @@ -package handler - -import ( - "strconv" - - "health-ai/internal/api/middleware" - "health-ai/internal/service" - "health-ai/pkg/response" - - "github.com/gin-gonic/gin" -) - -type ConversationHandler struct { - convService *service.ConversationService -} - -func NewConversationHandler() *ConversationHandler { - return &ConversationHandler{ - convService: service.NewConversationService(), - } -} - -// GetConversations 获取对话列表 -// @Summary 获取对话列表 -// @Tags AI对话 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Success 200 {object} response.Response{data=[]service.ConversationResponse} -// @Router /api/conversations [get] -func (h *ConversationHandler) GetConversations(c *gin.Context) { - userID := middleware.GetUserID(c) - convs, err := h.convService.GetConversations(userID) - if err != nil { - response.Error(c, 500, err.Error()) - return - } - response.Success(c, convs) -} - -// CreateConversation 创建新对话 -// @Summary 创建新对话 -// @Tags AI对话 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Param request body service.CreateConversationRequest true "对话标题(可选)" -// @Success 200 {object} response.Response{data=service.ConversationResponse} -// @Router /api/conversations [post] -func (h *ConversationHandler) CreateConversation(c *gin.Context) { - userID := middleware.GetUserID(c) - var req service.CreateConversationRequest - c.ShouldBindJSON(&req) - - conv, err := h.convService.CreateConversation(userID, req.Title) - if err != nil { - response.Error(c, 500, err.Error()) - return - } - response.Success(c, conv) -} - -// GetConversation 获取对话详情 -// @Summary 获取对话详情(含消息历史) -// @Tags AI对话 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Param id path int true "对话ID" -// @Success 200 {object} response.Response{data=service.ConversationResponse} -// @Router /api/conversations/{id} [get] -func (h *ConversationHandler) GetConversation(c *gin.Context) { - userID := middleware.GetUserID(c) - convID, err := strconv.ParseUint(c.Param("id"), 10, 32) - if err != nil { - response.BadRequest(c, "无效的对话ID") - return - } - - conv, err := h.convService.GetConversation(userID, uint(convID)) - if err != nil { - response.Error(c, 404, err.Error()) - return - } - response.Success(c, conv) -} - -// DeleteConversation 删除对话 -// @Summary 删除对话 -// @Tags AI对话 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Param id path int true "对话ID" -// @Success 200 {object} response.Response -// @Router /api/conversations/{id} [delete] -func (h *ConversationHandler) DeleteConversation(c *gin.Context) { - userID := middleware.GetUserID(c) - convID, err := strconv.ParseUint(c.Param("id"), 10, 32) - if err != nil { - response.BadRequest(c, "无效的对话ID") - return - } - - if err := h.convService.DeleteConversation(userID, uint(convID)); err != nil { - response.Error(c, 400, err.Error()) - return - } - response.SuccessWithMessage(c, "对话已删除", nil) -} - -// SendMessage 发送消息 -// @Summary 发送消息并获取AI回复 -// @Tags AI对话 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Param id path int true "对话ID" -// @Param request body service.SendMessageRequest true "消息内容" -// @Success 200 {object} response.Response{data=service.MessageResponse} -// @Router /api/conversations/{id}/messages [post] -func (h *ConversationHandler) SendMessage(c *gin.Context) { - userID := middleware.GetUserID(c) - convID, err := strconv.ParseUint(c.Param("id"), 10, 32) - if err != nil { - response.BadRequest(c, "无效的对话ID") - return - } - - var req service.SendMessageRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "请输入消息内容") - return - } - - if req.Content == "" { - response.BadRequest(c, "消息内容不能为空") - return - } - - reply, err := h.convService.SendMessage(c.Request.Context(), userID, uint(convID), req.Content) - if err != nil { - response.Error(c, 500, "AI回复失败: "+err.Error()) - return - } - - response.Success(c, reply) -} - -// SendMessageStream 流式发送消息(SSE) -// @Summary 流式发送消息 -// @Tags AI对话 -// @Accept json -// @Produce text/event-stream -// @Param Authorization header string true "Bearer Token" -// @Param id path int true "对话ID" -// @Param request body service.SendMessageRequest true "消息内容" -// @Router /api/conversations/{id}/messages/stream [post] -func (h *ConversationHandler) SendMessageStream(c *gin.Context) { - userID := middleware.GetUserID(c) - convID, err := strconv.ParseUint(c.Param("id"), 10, 32) - if err != nil { - response.BadRequest(c, "无效的对话ID") - return - } - - var req service.SendMessageRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "请输入消息内容") - return - } - - // 设置 SSE 响应头 - c.Header("Content-Type", "text/event-stream") - c.Header("Cache-Control", "no-cache") - c.Header("Connection", "keep-alive") - - err = h.convService.SendMessageStream(c.Request.Context(), userID, uint(convID), req.Content, c.Writer) - if err != nil { - c.SSEvent("error", err.Error()) - } - c.SSEvent("done", "") -} diff --git a/server/internal/api/handler/health.go b/server/internal/api/handler/health.go deleted file mode 100644 index b4b91c4..0000000 --- a/server/internal/api/handler/health.go +++ /dev/null @@ -1,301 +0,0 @@ -package handler - -import ( - "strconv" - - "health-ai/internal/api/middleware" - "health-ai/internal/service" - "health-ai/pkg/response" - - "github.com/gin-gonic/gin" -) - -type HealthHandler struct { - healthService *service.HealthService -} - -func NewHealthHandler() *HealthHandler { - return &HealthHandler{ - healthService: service.NewHealthService(), - } -} - -// GetHealthProfile 获取完整健康档案 -// @Summary 获取完整健康档案 -// @Tags 健康档案 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Success 200 {object} response.Response{data=service.HealthProfileResponse} -// @Router /api/user/health-profile [get] -func (h *HealthHandler) GetHealthProfile(c *gin.Context) { - userID := middleware.GetUserID(c) - profile, err := h.healthService.GetHealthProfile(userID) - if err != nil { - response.Error(c, 500, err.Error()) - return - } - response.Success(c, profile) -} - -// GetBasicProfile 获取基础档案 -// @Summary 获取基础档案 -// @Tags 健康档案 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Success 200 {object} response.Response{data=model.HealthProfile} -// @Router /api/user/basic-profile [get] -func (h *HealthHandler) GetBasicProfile(c *gin.Context) { - userID := middleware.GetUserID(c) - profile, err := h.healthService.GetBasicProfile(userID) - if err != nil { - response.Error(c, 404, err.Error()) - return - } - response.Success(c, profile) -} - -// GetLifestyle 获取生活习惯 -// @Summary 获取生活习惯 -// @Tags 健康档案 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Success 200 {object} response.Response{data=model.LifestyleInfo} -// @Router /api/user/lifestyle [get] -func (h *HealthHandler) GetLifestyle(c *gin.Context) { - userID := middleware.GetUserID(c) - lifestyle, err := h.healthService.GetLifestyle(userID) - if err != nil { - response.Error(c, 404, err.Error()) - return - } - response.Success(c, lifestyle) -} - -// GetMedicalHistory 获取病史列表 -// @Summary 获取病史列表 -// @Tags 健康档案 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Success 200 {object} response.Response{data=[]model.MedicalHistory} -// @Router /api/user/medical-history [get] -func (h *HealthHandler) GetMedicalHistory(c *gin.Context) { - userID := middleware.GetUserID(c) - history, err := h.healthService.GetMedicalHistory(userID) - if err != nil { - response.Error(c, 404, err.Error()) - return - } - response.Success(c, history) -} - -// GetFamilyHistory 获取家族病史列表 -// @Summary 获取家族病史列表 -// @Tags 健康档案 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Success 200 {object} response.Response{data=[]model.FamilyHistory} -// @Router /api/user/family-history [get] -func (h *HealthHandler) GetFamilyHistory(c *gin.Context) { - userID := middleware.GetUserID(c) - history, err := h.healthService.GetFamilyHistory(userID) - if err != nil { - response.Error(c, 404, err.Error()) - return - } - response.Success(c, history) -} - -// GetAllergyRecords 获取过敏记录列表 -// @Summary 获取过敏记录列表 -// @Tags 健康档案 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Success 200 {object} response.Response{data=[]model.AllergyRecord} -// @Router /api/user/allergy-records [get] -func (h *HealthHandler) GetAllergyRecords(c *gin.Context) { - userID := middleware.GetUserID(c) - records, err := h.healthService.GetAllergyRecords(userID) - if err != nil { - response.Error(c, 404, err.Error()) - return - } - response.Success(c, records) -} - -// DeleteMedicalHistory 删除病史记录 -// @Summary 删除病史记录 -// @Tags 健康档案 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Param id path int true "记录ID" -// @Success 200 {object} response.Response -// @Router /api/user/medical-history/{id} [delete] -func (h *HealthHandler) DeleteMedicalHistory(c *gin.Context) { - userID := middleware.GetUserID(c) - id, err := strconv.ParseUint(c.Param("id"), 10, 32) - if err != nil { - response.BadRequest(c, "无效的ID") - return - } - - if err := h.healthService.DeleteMedicalHistory(userID, uint(id)); err != nil { - response.Error(c, 500, err.Error()) - return - } - response.SuccessWithMessage(c, "删除成功", nil) -} - -// DeleteFamilyHistory 删除家族病史记录 -// @Summary 删除家族病史记录 -// @Tags 健康档案 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Param id path int true "记录ID" -// @Success 200 {object} response.Response -// @Router /api/user/family-history/{id} [delete] -func (h *HealthHandler) DeleteFamilyHistory(c *gin.Context) { - userID := middleware.GetUserID(c) - id, err := strconv.ParseUint(c.Param("id"), 10, 32) - if err != nil { - response.BadRequest(c, "无效的ID") - return - } - - if err := h.healthService.DeleteFamilyHistory(userID, uint(id)); err != nil { - response.Error(c, 500, err.Error()) - return - } - response.SuccessWithMessage(c, "删除成功", nil) -} - -// DeleteAllergyRecord 删除过敏记录 -// @Summary 删除过敏记录 -// @Tags 健康档案 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Param id path int true "记录ID" -// @Success 200 {object} response.Response -// @Router /api/user/allergy-records/{id} [delete] -func (h *HealthHandler) DeleteAllergyRecord(c *gin.Context) { - userID := middleware.GetUserID(c) - id, err := strconv.ParseUint(c.Param("id"), 10, 32) - if err != nil { - response.BadRequest(c, "无效的ID") - return - } - - if err := h.healthService.DeleteAllergyRecord(userID, uint(id)); err != nil { - response.Error(c, 500, err.Error()) - return - } - response.SuccessWithMessage(c, "删除成功", nil) -} - -// UpdateHealthProfileRequest 更新健康档案请求 -type UpdateHealthProfileRequest struct { - Name string `json:"name"` - BirthDate string `json:"birth_date"` - Gender string `json:"gender"` - Height float64 `json:"height"` - Weight float64 `json:"weight"` - BloodType string `json:"blood_type"` - Occupation string `json:"occupation"` - MaritalStatus string `json:"marital_status"` - Region string `json:"region"` -} - -// UpdateHealthProfile 更新健康档案 -// @Summary 更新健康档案 -// @Tags 健康档案 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Param request body UpdateHealthProfileRequest true "健康档案信息" -// @Success 200 {object} response.Response -// @Router /api/user/health-profile [put] -func (h *HealthHandler) UpdateHealthProfile(c *gin.Context) { - userID := middleware.GetUserID(c) - var req UpdateHealthProfileRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误: "+err.Error()) - return - } - - profile, err := h.healthService.UpdateHealthProfile(userID, &service.UpdateHealthProfileInput{ - Name: req.Name, - BirthDate: req.BirthDate, - Gender: req.Gender, - Height: req.Height, - Weight: req.Weight, - BloodType: req.BloodType, - Occupation: req.Occupation, - MaritalStatus: req.MaritalStatus, - Region: req.Region, - }) - if err != nil { - response.Error(c, 500, err.Error()) - return - } - response.Success(c, profile) -} - -// UpdateLifestyleRequest 更新生活习惯请求 -type UpdateLifestyleRequest struct { - SleepTime string `json:"sleep_time"` - WakeTime string `json:"wake_time"` - SleepQuality string `json:"sleep_quality"` - MealRegularity string `json:"meal_regularity"` - DietPreference string `json:"diet_preference"` - DailyWaterML int `json:"daily_water_ml"` - ExerciseFrequency string `json:"exercise_frequency"` - ExerciseType string `json:"exercise_type"` - ExerciseDurationMin int `json:"exercise_duration_min"` - IsSmoker *bool `json:"is_smoker"` - AlcoholFrequency string `json:"alcohol_frequency"` -} - -// UpdateLifestyle 更新生活习惯 -// @Summary 更新生活习惯 -// @Tags 健康档案 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Param request body UpdateLifestyleRequest true "生活习惯信息" -// @Success 200 {object} response.Response -// @Router /api/user/lifestyle [put] -func (h *HealthHandler) UpdateLifestyle(c *gin.Context) { - userID := middleware.GetUserID(c) - var req UpdateLifestyleRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误: "+err.Error()) - return - } - - lifestyle, err := h.healthService.UpdateLifestyle(userID, &service.UpdateLifestyleInput{ - SleepTime: req.SleepTime, - WakeTime: req.WakeTime, - SleepQuality: req.SleepQuality, - MealRegularity: req.MealRegularity, - DietPreference: req.DietPreference, - DailyWaterML: req.DailyWaterML, - ExerciseFrequency: req.ExerciseFrequency, - ExerciseType: req.ExerciseType, - ExerciseDurationMin: req.ExerciseDurationMin, - IsSmoker: req.IsSmoker, - AlcoholFrequency: req.AlcoholFrequency, - }) - if err != nil { - response.Error(c, 500, err.Error()) - return - } - response.Success(c, lifestyle) -} diff --git a/server/internal/api/handler/product.go b/server/internal/api/handler/product.go deleted file mode 100644 index 009335e..0000000 --- a/server/internal/api/handler/product.go +++ /dev/null @@ -1,172 +0,0 @@ -package handler - -import ( - "strconv" - - "health-ai/internal/api/middleware" - "health-ai/internal/service" - "health-ai/pkg/response" - - "github.com/gin-gonic/gin" -) - -type ProductHandler struct { - productService *service.ProductService -} - -func NewProductHandler() *ProductHandler { - return &ProductHandler{ - productService: service.NewProductService(), - } -} - -// GetProducts 获取产品列表 -// @Summary 获取产品列表 -// @Tags 产品 -// @Accept json -// @Produce json -// @Param page query int false "页码,默认1" -// @Param page_size query int false "每页数量,默认20" -// @Success 200 {object} response.Response{data=service.ProductListResponse} -// @Router /api/products [get] -func (h *ProductHandler) GetProducts(c *gin.Context) { - page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) - pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20")) - - result, err := h.productService.GetProducts(page, pageSize) - if err != nil { - response.Error(c, 500, err.Error()) - return - } - response.Success(c, result) -} - -// GetProduct 获取产品详情 -// @Summary 获取产品详情 -// @Tags 产品 -// @Accept json -// @Produce json -// @Param id path int true "产品ID" -// @Success 200 {object} response.Response{data=model.Product} -// @Router /api/products/{id} [get] -func (h *ProductHandler) GetProduct(c *gin.Context) { - id, err := strconv.ParseUint(c.Param("id"), 10, 32) - if err != nil { - response.BadRequest(c, "无效的产品ID") - return - } - - product, err := h.productService.GetProductByID(uint(id)) - if err != nil { - response.Error(c, 404, err.Error()) - return - } - response.Success(c, product) -} - -// GetProductsByCategory 按分类获取产品 -// @Summary 按分类获取产品 -// @Tags 产品 -// @Accept json -// @Produce json -// @Param category query string true "分类名称" -// @Success 200 {object} response.Response{data=[]model.Product} -// @Router /api/products/category [get] -func (h *ProductHandler) GetProductsByCategory(c *gin.Context) { - category := c.Query("category") - if category == "" { - response.BadRequest(c, "请指定分类") - return - } - - products, err := h.productService.GetProductsByCategory(category) - if err != nil { - response.Error(c, 500, err.Error()) - return - } - response.Success(c, products) -} - -// GetRecommendedProducts 获取推荐产品(基于用户体质) -// @Summary 获取推荐产品 -// @Tags 产品 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Success 200 {object} response.Response{data=service.ProductRecommendResponse} -// @Router /api/products/recommend [get] -func (h *ProductHandler) GetRecommendedProducts(c *gin.Context) { - userID := middleware.GetUserID(c) - result, err := h.productService.GetRecommendedProducts(userID) - if err != nil { - response.Error(c, 400, err.Error()) - return - } - response.Success(c, result) -} - -// SearchProducts 搜索产品 -// @Summary 搜索产品 -// @Tags 产品 -// @Accept json -// @Produce json -// @Param keyword query string true "搜索关键词" -// @Success 200 {object} response.Response{data=[]model.Product} -// @Router /api/products/search [get] -func (h *ProductHandler) SearchProducts(c *gin.Context) { - keyword := c.Query("keyword") - if keyword == "" { - response.BadRequest(c, "请输入搜索关键词") - return - } - - products, err := h.productService.SearchProducts(keyword) - if err != nil { - response.Error(c, 500, err.Error()) - return - } - response.Success(c, products) -} - -// SyncPurchase 同步商城购买记录 -// @Summary 同步商城购买记录 -// @Tags 商城同步 -// @Accept json -// @Produce json -// @Param request body service.PurchaseSyncRequest true "购买记录" -// @Success 200 {object} response.Response -// @Router /api/sync/purchase [post] -func (h *ProductHandler) SyncPurchase(c *gin.Context) { - // TODO: 验证同步密钥 - - var req service.PurchaseSyncRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误: "+err.Error()) - return - } - - if err := h.productService.SyncPurchase(&req); err != nil { - response.Error(c, 500, err.Error()) - return - } - - response.SuccessWithMessage(c, "同步成功", nil) -} - -// GetPurchaseHistory 获取购买历史 -// @Summary 获取购买历史 -// @Tags 产品 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Success 200 {object} response.Response{data=[]model.PurchaseHistory} -// @Router /api/user/purchase-history [get] -func (h *ProductHandler) GetPurchaseHistory(c *gin.Context) { - userID := middleware.GetUserID(c) - history, err := h.productService.GetPurchaseHistory(userID) - if err != nil { - response.Error(c, 500, err.Error()) - return - } - response.Success(c, history) -} diff --git a/server/internal/api/handler/survey.go b/server/internal/api/handler/survey.go deleted file mode 100644 index f9abf60..0000000 --- a/server/internal/api/handler/survey.go +++ /dev/null @@ -1,256 +0,0 @@ -package handler - -import ( - "health-ai/internal/api/middleware" - "health-ai/internal/service" - "health-ai/pkg/response" - - "github.com/gin-gonic/gin" -) - -type SurveyHandler struct { - surveyService *service.SurveyService -} - -func NewSurveyHandler() *SurveyHandler { - return &SurveyHandler{ - surveyService: service.NewSurveyService(), - } -} - -// GetStatus 获取调查完成状态 -// @Summary 获取调查完成状态 -// @Tags 健康调查 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Success 200 {object} response.Response{data=service.SurveyStatusResponse} -// @Router /api/survey/status [get] -func (h *SurveyHandler) GetStatus(c *gin.Context) { - userID := middleware.GetUserID(c) - status, err := h.surveyService.GetStatus(userID) - if err != nil { - response.Error(c, 500, err.Error()) - return - } - response.Success(c, status) -} - -// SubmitBasicInfo 提交基础信息 -// @Summary 提交基础信息 -// @Tags 健康调查 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Param request body service.BasicInfoRequest true "基础信息" -// @Success 200 {object} response.Response -// @Router /api/survey/basic-info [post] -func (h *SurveyHandler) SubmitBasicInfo(c *gin.Context) { - userID := middleware.GetUserID(c) - var req service.BasicInfoRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误: "+err.Error()) - return - } - - if err := h.surveyService.SubmitBasicInfo(userID, &req); err != nil { - response.Error(c, 500, err.Error()) - return - } - - response.SuccessWithMessage(c, "基础信息保存成功", nil) -} - -// SubmitLifestyle 提交生活习惯 -// @Summary 提交生活习惯 -// @Tags 健康调查 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Param request body service.LifestyleRequest true "生活习惯" -// @Success 200 {object} response.Response -// @Router /api/survey/lifestyle [post] -func (h *SurveyHandler) SubmitLifestyle(c *gin.Context) { - userID := middleware.GetUserID(c) - var req service.LifestyleRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误: "+err.Error()) - return - } - - if err := h.surveyService.SubmitLifestyle(userID, &req); err != nil { - response.Error(c, 500, err.Error()) - return - } - - response.SuccessWithMessage(c, "生活习惯保存成功", nil) -} - -// SubmitMedicalHistory 提交病史(单条) -// @Summary 提交病史 -// @Tags 健康调查 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Param request body service.MedicalHistoryRequest true "病史信息" -// @Success 200 {object} response.Response -// @Router /api/survey/medical-history [post] -func (h *SurveyHandler) SubmitMedicalHistory(c *gin.Context) { - userID := middleware.GetUserID(c) - var req service.MedicalHistoryRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误: "+err.Error()) - return - } - - if err := h.surveyService.SubmitMedicalHistory(userID, &req); err != nil { - response.Error(c, 500, err.Error()) - return - } - - response.SuccessWithMessage(c, "病史记录添加成功", nil) -} - -// SubmitBatchMedicalHistory 批量提交病史 -// @Summary 批量提交病史(覆盖式) -// @Tags 健康调查 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Param request body service.BatchMedicalHistoryRequest true "病史列表" -// @Success 200 {object} response.Response -// @Router /api/survey/medical-history/batch [post] -func (h *SurveyHandler) SubmitBatchMedicalHistory(c *gin.Context) { - userID := middleware.GetUserID(c) - var req service.BatchMedicalHistoryRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误: "+err.Error()) - return - } - - if err := h.surveyService.SubmitBatchMedicalHistory(userID, &req); err != nil { - response.Error(c, 500, err.Error()) - return - } - - response.SuccessWithMessage(c, "病史记录保存成功", nil) -} - -// SubmitFamilyHistory 提交家族病史(单条) -// @Summary 提交家族病史 -// @Tags 健康调查 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Param request body service.FamilyHistoryRequest true "家族病史" -// @Success 200 {object} response.Response -// @Router /api/survey/family-history [post] -func (h *SurveyHandler) SubmitFamilyHistory(c *gin.Context) { - userID := middleware.GetUserID(c) - var req service.FamilyHistoryRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误: "+err.Error()) - return - } - - if err := h.surveyService.SubmitFamilyHistory(userID, &req); err != nil { - response.Error(c, 500, err.Error()) - return - } - - response.SuccessWithMessage(c, "家族病史记录添加成功", nil) -} - -// SubmitBatchFamilyHistory 批量提交家族病史 -// @Summary 批量提交家族病史(覆盖式) -// @Tags 健康调查 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Param request body service.BatchFamilyHistoryRequest true "家族病史列表" -// @Success 200 {object} response.Response -// @Router /api/survey/family-history/batch [post] -func (h *SurveyHandler) SubmitBatchFamilyHistory(c *gin.Context) { - userID := middleware.GetUserID(c) - var req service.BatchFamilyHistoryRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误: "+err.Error()) - return - } - - if err := h.surveyService.SubmitBatchFamilyHistory(userID, &req); err != nil { - response.Error(c, 500, err.Error()) - return - } - - response.SuccessWithMessage(c, "家族病史保存成功", nil) -} - -// SubmitAllergy 提交过敏信息(单条) -// @Summary 提交过敏信息 -// @Tags 健康调查 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Param request body service.AllergyRequest true "过敏信息" -// @Success 200 {object} response.Response -// @Router /api/survey/allergy [post] -func (h *SurveyHandler) SubmitAllergy(c *gin.Context) { - userID := middleware.GetUserID(c) - var req service.AllergyRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误: "+err.Error()) - return - } - - if err := h.surveyService.SubmitAllergy(userID, &req); err != nil { - response.Error(c, 500, err.Error()) - return - } - - response.SuccessWithMessage(c, "过敏信息添加成功", nil) -} - -// SubmitBatchAllergy 批量提交过敏信息 -// @Summary 批量提交过敏信息(覆盖式) -// @Tags 健康调查 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Param request body service.BatchAllergyRequest true "过敏信息列表" -// @Success 200 {object} response.Response -// @Router /api/survey/allergy/batch [post] -func (h *SurveyHandler) SubmitBatchAllergy(c *gin.Context) { - userID := middleware.GetUserID(c) - var req service.BatchAllergyRequest - if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误: "+err.Error()) - return - } - - if err := h.surveyService.SubmitBatchAllergy(userID, &req); err != nil { - response.Error(c, 500, err.Error()) - return - } - - response.SuccessWithMessage(c, "过敏信息保存成功", nil) -} - -// CompleteSurvey 完成调查 -// @Summary 完成健康调查 -// @Tags 健康调查 -// @Accept json -// @Produce json -// @Param Authorization header string true "Bearer Token" -// @Success 200 {object} response.Response -// @Router /api/survey/complete [post] -func (h *SurveyHandler) CompleteSurvey(c *gin.Context) { - userID := middleware.GetUserID(c) - - if err := h.surveyService.CompleteSurvey(userID); err != nil { - response.Error(c, 400, err.Error()) - return - } - - response.SuccessWithMessage(c, "健康调查已完成", nil) -} diff --git a/server/internal/api/middleware/auth.go b/server/internal/api/middleware/auth.go deleted file mode 100644 index f5c40be..0000000 --- a/server/internal/api/middleware/auth.go +++ /dev/null @@ -1,72 +0,0 @@ -package middleware - -import ( - "strings" - - "health-ai/pkg/jwt" - "health-ai/pkg/response" - - "github.com/gin-gonic/gin" -) - -// AuthRequired JWT认证中间件 -func AuthRequired() gin.HandlerFunc { - return func(c *gin.Context) { - authHeader := c.GetHeader("Authorization") - if authHeader == "" { - response.Unauthorized(c, "未提供认证信息") - c.Abort() - return - } - - parts := strings.SplitN(authHeader, " ", 2) - if len(parts) != 2 || parts[0] != "Bearer" { - response.Unauthorized(c, "认证格式错误,请使用 Bearer Token") - c.Abort() - return - } - - claims, err := jwt.ParseToken(parts[1]) - if err != nil { - response.Unauthorized(c, "Token无效或已过期") - c.Abort() - return - } - - // 将用户ID存入上下文 - c.Set("userID", claims.UserID) - c.Next() - } -} - -// GetUserID 从上下文获取用户ID -func GetUserID(c *gin.Context) uint { - userID, exists := c.Get("userID") - if !exists { - return 0 - } - return userID.(uint) -} - -// OptionalAuth 可选认证中间件(不强制要求登录) -func OptionalAuth() gin.HandlerFunc { - return func(c *gin.Context) { - authHeader := c.GetHeader("Authorization") - if authHeader == "" { - c.Next() - return - } - - parts := strings.SplitN(authHeader, " ", 2) - if len(parts) != 2 || parts[0] != "Bearer" { - c.Next() - return - } - - claims, err := jwt.ParseToken(parts[1]) - if err == nil { - c.Set("userID", claims.UserID) - } - c.Next() - } -} diff --git a/server/internal/api/router.go b/server/internal/api/router.go deleted file mode 100644 index 6c574d1..0000000 --- a/server/internal/api/router.go +++ /dev/null @@ -1,139 +0,0 @@ -package api - -import ( - "health-ai/internal/api/handler" - "health-ai/internal/api/middleware" - - "github.com/gin-contrib/cors" - "github.com/gin-gonic/gin" -) - -func SetupRouter(mode string) *gin.Engine { - gin.SetMode(mode) - r := gin.Default() - - // 跨域配置 - r.Use(cors.New(cors.Config{ - AllowOrigins: []string{"*"}, - AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, - AllowCredentials: true, - })) - - // 健康检查 - r.GET("/health", func(c *gin.Context) { - c.JSON(200, gin.H{"status": "ok"}) - }) - - // API 路由组 - apiGroup := r.Group("/api") - { - // ===================== - // 认证路由(无需登录) - // ===================== - authHandler := handler.NewAuthHandler() - authGroup := apiGroup.Group("/auth") - { - authGroup.POST("/register", authHandler.Register) - authGroup.POST("/login", authHandler.Login) - authGroup.POST("/refresh", authHandler.RefreshToken) - authGroup.POST("/send-code", authHandler.SendCode) - } - - // ===================== - // 产品路由(部分无需登录) - // ===================== - productHandler := handler.NewProductHandler() - productGroup := apiGroup.Group("/products") - { - productGroup.GET("", productHandler.GetProducts) - productGroup.GET("/:id", productHandler.GetProduct) - productGroup.GET("/category", productHandler.GetProductsByCategory) - productGroup.GET("/search", productHandler.SearchProducts) - } - - // 商城同步接口(无需用户登录,需要API密钥验证) - apiGroup.POST("/sync/purchase", productHandler.SyncPurchase) - - // ===================== - // 需要登录的路由 - // ===================== - authRequired := apiGroup.Group("") - authRequired.Use(middleware.AuthRequired()) - { - // ===================== - // 用户相关 - // ===================== - authRequired.GET("/user/profile", authHandler.GetUserInfo) - authRequired.PUT("/user/profile", authHandler.UpdateProfile) - - // 健康档案 - healthHandler := handler.NewHealthHandler() - authRequired.GET("/user/health-profile", healthHandler.GetHealthProfile) - authRequired.PUT("/user/health-profile", healthHandler.UpdateHealthProfile) - authRequired.GET("/user/basic-profile", healthHandler.GetBasicProfile) - authRequired.GET("/user/lifestyle", healthHandler.GetLifestyle) - authRequired.PUT("/user/lifestyle", healthHandler.UpdateLifestyle) - authRequired.GET("/user/medical-history", healthHandler.GetMedicalHistory) - authRequired.DELETE("/user/medical-history/:id", healthHandler.DeleteMedicalHistory) - authRequired.GET("/user/family-history", healthHandler.GetFamilyHistory) - authRequired.DELETE("/user/family-history/:id", healthHandler.DeleteFamilyHistory) - authRequired.GET("/user/allergy-records", healthHandler.GetAllergyRecords) - authRequired.DELETE("/user/allergy-records/:id", healthHandler.DeleteAllergyRecord) - - // 购买历史 - authRequired.GET("/user/purchase-history", productHandler.GetPurchaseHistory) - - // 产品推荐(需要登录获取体质) - authRequired.GET("/products/recommend", productHandler.GetRecommendedProducts) - - // ===================== - // 健康调查路由 - // ===================== - surveyHandler := handler.NewSurveyHandler() - surveyGroup := authRequired.Group("/survey") - { - surveyGroup.GET("/status", surveyHandler.GetStatus) - surveyGroup.POST("/basic-info", surveyHandler.SubmitBasicInfo) - surveyGroup.POST("/lifestyle", surveyHandler.SubmitLifestyle) - surveyGroup.POST("/medical-history", surveyHandler.SubmitMedicalHistory) - surveyGroup.POST("/medical-history/batch", surveyHandler.SubmitBatchMedicalHistory) - surveyGroup.POST("/family-history", surveyHandler.SubmitFamilyHistory) - surveyGroup.POST("/family-history/batch", surveyHandler.SubmitBatchFamilyHistory) - surveyGroup.POST("/allergy", surveyHandler.SubmitAllergy) - surveyGroup.POST("/allergy/batch", surveyHandler.SubmitBatchAllergy) - surveyGroup.POST("/complete", surveyHandler.CompleteSurvey) - } - - // ===================== - // 体质辨识路由 - // ===================== - constitutionHandler := handler.NewConstitutionHandler() - constitutionGroup := authRequired.Group("/constitution") - { - constitutionGroup.GET("/questions", constitutionHandler.GetQuestions) - constitutionGroup.GET("/questions/grouped", constitutionHandler.GetQuestionsGrouped) - constitutionGroup.POST("/submit", constitutionHandler.SubmitAssessment) - constitutionGroup.GET("/result", constitutionHandler.GetResult) - constitutionGroup.GET("/history", constitutionHandler.GetHistory) - constitutionGroup.GET("/recommendations", constitutionHandler.GetRecommendations) - } - - // ===================== - // AI对话路由 - // ===================== - conversationHandler := handler.NewConversationHandler() - convGroup := authRequired.Group("/conversations") - { - convGroup.GET("", conversationHandler.GetConversations) - convGroup.POST("", conversationHandler.CreateConversation) - convGroup.GET("/:id", conversationHandler.GetConversation) - convGroup.DELETE("/:id", conversationHandler.DeleteConversation) - convGroup.POST("/:id/messages", conversationHandler.SendMessage) - convGroup.POST("/:id/messages/stream", conversationHandler.SendMessageStream) - } - } - } - - return r -} diff --git a/server/internal/config/config.go b/server/internal/config/config.go deleted file mode 100644 index 50d0e43..0000000 --- a/server/internal/config/config.go +++ /dev/null @@ -1,79 +0,0 @@ -package config - -import ( - "github.com/spf13/viper" -) - -type Config struct { - Server ServerConfig `mapstructure:"server"` - Database DatabaseConfig `mapstructure:"database"` - JWT JWTConfig `mapstructure:"jwt"` - AI AIConfig `mapstructure:"ai"` -} - -type ServerConfig struct { - Port int `mapstructure:"port"` - Mode string `mapstructure:"mode"` -} - -type DatabaseConfig struct { - Driver string `mapstructure:"driver"` - SQLite SQLiteConfig `mapstructure:"sqlite"` - Postgres PostgresConfig `mapstructure:"postgres"` - MySQL MySQLConfig `mapstructure:"mysql"` -} - -type SQLiteConfig struct { - Path string `mapstructure:"path"` -} - -type PostgresConfig struct { - Host string `mapstructure:"host"` - Port int `mapstructure:"port"` - User string `mapstructure:"user"` - Password string `mapstructure:"password"` - DBName string `mapstructure:"dbname"` -} - -type MySQLConfig struct { - Host string `mapstructure:"host"` - Port int `mapstructure:"port"` - User string `mapstructure:"user"` - Password string `mapstructure:"password"` - DBName string `mapstructure:"dbname"` -} - -type JWTConfig struct { - Secret string `mapstructure:"secret"` - ExpireHours int `mapstructure:"expire_hours"` -} - -type AIConfig struct { - Provider string `mapstructure:"provider"` - MaxHistoryMessages int `mapstructure:"max_history_messages"` - MaxTokens int `mapstructure:"max_tokens"` - OpenAI OpenAIConfig `mapstructure:"openai"` - Aliyun AliyunConfig `mapstructure:"aliyun"` -} - -type OpenAIConfig struct { - APIKey string `mapstructure:"api_key"` - BaseURL string `mapstructure:"base_url"` - Model string `mapstructure:"model"` -} - -type AliyunConfig struct { - APIKey string `mapstructure:"api_key"` - Model string `mapstructure:"model"` -} - -var AppConfig *Config - -func LoadConfig(path string) error { - viper.SetConfigFile(path) - if err := viper.ReadInConfig(); err != nil { - return err - } - AppConfig = &Config{} - return viper.Unmarshal(AppConfig) -} diff --git a/server/internal/database/database.go b/server/internal/database/database.go deleted file mode 100644 index 20fe331..0000000 --- a/server/internal/database/database.go +++ /dev/null @@ -1,50 +0,0 @@ -package database - -import ( - "fmt" - - "health-ai/internal/config" - - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "gorm.io/gorm/logger" -) - -var DB *gorm.DB - -func InitDatabase(cfg *config.DatabaseConfig) error { - var err error - - gormConfig := &gorm.Config{ - Logger: logger.Default.LogMode(logger.Info), - } - - switch cfg.Driver { - case "sqlite": - DB, err = gorm.Open(sqlite.Open(cfg.SQLite.Path), gormConfig) - case "postgres": - // TODO: 添加 PostgreSQL 支持 - // dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=disable", - // cfg.Postgres.Host, cfg.Postgres.Port, cfg.Postgres.User, cfg.Postgres.Password, cfg.Postgres.DBName) - // DB, err = gorm.Open(postgres.Open(dsn), gormConfig) - return fmt.Errorf("postgres driver not implemented yet") - case "mysql": - // TODO: 添加 MySQL 支持 - // dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=True&loc=Local", - // cfg.MySQL.User, cfg.MySQL.Password, cfg.MySQL.Host, cfg.MySQL.Port, cfg.MySQL.DBName) - // DB, err = gorm.Open(mysql.Open(dsn), gormConfig) - return fmt.Errorf("mysql driver not implemented yet") - default: - return fmt.Errorf("unsupported database driver: %s", cfg.Driver) - } - - return err -} - -func AutoMigrate(models ...interface{}) error { - return DB.AutoMigrate(models...) -} - -func GetDB() *gorm.DB { - return DB -} diff --git a/server/internal/database/seed.go b/server/internal/database/seed.go deleted file mode 100644 index 813d2ca..0000000 --- a/server/internal/database/seed.go +++ /dev/null @@ -1,151 +0,0 @@ -package database - -import ( - "encoding/json" - "log" - - "health-ai/internal/model" - - "golang.org/x/crypto/bcrypt" -) - -// SeedTestUser 创建测试用户 -func SeedTestUser() error { - var count int64 - DB.Model(&model.User{}).Where("phone = ?", "13800138000").Count(&count) - if count > 0 { - log.Println("Test user already exists") - return nil - } - - // 加密密码 - hashedPassword, err := bcrypt.GenerateFromPassword([]byte("123456"), bcrypt.DefaultCost) - if err != nil { - return err - } - - testUser := &model.User{ - Phone: "13800138000", - PasswordHash: string(hashedPassword), - Nickname: "测试用户", - } - - if err := DB.Create(testUser).Error; err != nil { - return err - } - - log.Println("Test user created: phone=13800138000, password=123456") - return nil -} - -// SeedQuestionBank 初始化问卷题库 -func SeedQuestionBank() error { - // 检查是否已有数据 - var count int64 - DB.Model(&model.QuestionBank{}).Count(&count) - if count > 0 { - log.Printf("Question bank already seeded with %d questions", count) - return nil - } - - questions := getQuestions() - for _, q := range questions { - if err := DB.Create(&q).Error; err != nil { - return err - } - } - log.Printf("Question bank seeded with %d questions", len(questions)) - return nil -} - -func getQuestions() []model.QuestionBank { - options, _ := json.Marshal([]string{"没有", "很少", "有时", "经常", "总是"}) - optStr := string(options) - - return []model.QuestionBank{ - // 平和质 (8题) - {ConstitutionType: model.ConstitutionPinghe, QuestionText: "您精力充沛吗?", Options: optStr, OrderNum: 1}, - {ConstitutionType: model.ConstitutionPinghe, QuestionText: "您容易疲乏吗?", Options: optStr, OrderNum: 2}, - {ConstitutionType: model.ConstitutionPinghe, QuestionText: "您说话声音低弱无力吗?", Options: optStr, OrderNum: 3}, - {ConstitutionType: model.ConstitutionPinghe, QuestionText: "您感到闷闷不乐、情绪低沉吗?", Options: optStr, OrderNum: 4}, - {ConstitutionType: model.ConstitutionPinghe, QuestionText: "您比一般人耐受不了寒冷吗?", Options: optStr, OrderNum: 5}, - {ConstitutionType: model.ConstitutionPinghe, QuestionText: "您能适应外界自然和社会环境的变化吗?", Options: optStr, OrderNum: 6}, - {ConstitutionType: model.ConstitutionPinghe, QuestionText: "您容易失眠吗?", Options: optStr, OrderNum: 7}, - {ConstitutionType: model.ConstitutionPinghe, QuestionText: "您容易忘事吗?", Options: optStr, OrderNum: 8}, - - // 气虚质 (8题) - {ConstitutionType: model.ConstitutionQixu, QuestionText: "您容易气短(呼吸短促,接不上气)吗?", Options: optStr, OrderNum: 1}, - {ConstitutionType: model.ConstitutionQixu, QuestionText: "您容易心慌吗?", Options: optStr, OrderNum: 2}, - {ConstitutionType: model.ConstitutionQixu, QuestionText: "您容易头晕或站起时晕眩吗?", Options: optStr, OrderNum: 3}, - {ConstitutionType: model.ConstitutionQixu, QuestionText: "您比别人容易感冒吗?", Options: optStr, OrderNum: 4}, - {ConstitutionType: model.ConstitutionQixu, QuestionText: "您喜欢安静、懒得说话吗?", Options: optStr, OrderNum: 5}, - {ConstitutionType: model.ConstitutionQixu, QuestionText: "您说话声音低弱无力吗?", Options: optStr, OrderNum: 6}, - {ConstitutionType: model.ConstitutionQixu, QuestionText: "您活动量稍大就容易出虚汗吗?", Options: optStr, OrderNum: 7}, - {ConstitutionType: model.ConstitutionQixu, QuestionText: "您容易疲乏吗?", Options: optStr, OrderNum: 8}, - - // 阳虚质 (7题) - {ConstitutionType: model.ConstitutionYangxu, QuestionText: "您手脚发凉吗?", Options: optStr, OrderNum: 1}, - {ConstitutionType: model.ConstitutionYangxu, QuestionText: "您胃脘部、背部或腰膝部怕冷吗?", Options: optStr, OrderNum: 2}, - {ConstitutionType: model.ConstitutionYangxu, QuestionText: "您穿的衣服总比别人多吗?", Options: optStr, OrderNum: 3}, - {ConstitutionType: model.ConstitutionYangxu, QuestionText: "您比一般人耐受不了寒冷吗?", Options: optStr, OrderNum: 4}, - {ConstitutionType: model.ConstitutionYangxu, QuestionText: "您比别人容易感冒吗?", Options: optStr, OrderNum: 5}, - {ConstitutionType: model.ConstitutionYangxu, QuestionText: "您吃凉东西会感到不舒服或怕吃凉东西吗?", Options: optStr, OrderNum: 6}, - {ConstitutionType: model.ConstitutionYangxu, QuestionText: "您受凉或吃凉的东西后,容易拉肚子吗?", Options: optStr, OrderNum: 7}, - - // 阴虚质 (8题) - {ConstitutionType: model.ConstitutionYinxu, QuestionText: "您感到手脚心发热吗?", Options: optStr, OrderNum: 1}, - {ConstitutionType: model.ConstitutionYinxu, QuestionText: "您感觉身体、脸上发热吗?", Options: optStr, OrderNum: 2}, - {ConstitutionType: model.ConstitutionYinxu, QuestionText: "您皮肤或口唇干吗?", Options: optStr, OrderNum: 3}, - {ConstitutionType: model.ConstitutionYinxu, QuestionText: "您口唇的颜色比一般人红吗?", Options: optStr, OrderNum: 4}, - {ConstitutionType: model.ConstitutionYinxu, QuestionText: "您容易便秘或大便干燥吗?", Options: optStr, OrderNum: 5}, - {ConstitutionType: model.ConstitutionYinxu, QuestionText: "您面部两颧潮红或偏红吗?", Options: optStr, OrderNum: 6}, - {ConstitutionType: model.ConstitutionYinxu, QuestionText: "您感到眼睛干涩吗?", Options: optStr, OrderNum: 7}, - {ConstitutionType: model.ConstitutionYinxu, QuestionText: "您感到口干咽燥、总想喝水吗?", Options: optStr, OrderNum: 8}, - - // 痰湿质 (8题) - {ConstitutionType: model.ConstitutionTanshi, QuestionText: "您感到胸闷或腹部胀满吗?", Options: optStr, OrderNum: 1}, - {ConstitutionType: model.ConstitutionTanshi, QuestionText: "您感到身体沉重不轻松或不爽快吗?", Options: optStr, OrderNum: 2}, - {ConstitutionType: model.ConstitutionTanshi, QuestionText: "您腹部肥满松软吗?", Options: optStr, OrderNum: 3}, - {ConstitutionType: model.ConstitutionTanshi, QuestionText: "您额头部位油脂分泌多吗?", Options: optStr, OrderNum: 4}, - {ConstitutionType: model.ConstitutionTanshi, QuestionText: "您上眼睑比别人肿吗?", Options: optStr, OrderNum: 5}, - {ConstitutionType: model.ConstitutionTanshi, QuestionText: "您嘴里有黏黏的感觉吗?", Options: optStr, OrderNum: 6}, - {ConstitutionType: model.ConstitutionTanshi, QuestionText: "您平时痰多吗?", Options: optStr, OrderNum: 7}, - {ConstitutionType: model.ConstitutionTanshi, QuestionText: "您舌苔厚腻或有舌苔厚厚的感觉吗?", Options: optStr, OrderNum: 8}, - - // 湿热质 (7题) - {ConstitutionType: model.ConstitutionShire, QuestionText: "您面部或鼻部有油腻感或油光发亮吗?", Options: optStr, OrderNum: 1}, - {ConstitutionType: model.ConstitutionShire, QuestionText: "您脸上容易生痤疮或皮肤容易生疮疖吗?", Options: optStr, OrderNum: 2}, - {ConstitutionType: model.ConstitutionShire, QuestionText: "您感到口苦或嘴里有异味吗?", Options: optStr, OrderNum: 3}, - {ConstitutionType: model.ConstitutionShire, QuestionText: "您大便黏滞不爽、有解不尽的感觉吗?", Options: optStr, OrderNum: 4}, - {ConstitutionType: model.ConstitutionShire, QuestionText: "您小便时尿道有发热感、尿色浓吗?", Options: optStr, OrderNum: 5}, - {ConstitutionType: model.ConstitutionShire, QuestionText: "您带下色黄(白带颜色发黄)吗?(限女性回答)", Options: optStr, OrderNum: 6}, - {ConstitutionType: model.ConstitutionShire, QuestionText: "您的阴囊部位潮湿吗?(限男性回答)", Options: optStr, OrderNum: 7}, - - // 血瘀质 (7题) - {ConstitutionType: model.ConstitutionXueyu, QuestionText: "您皮肤在不知不觉中会出现青紫瘀斑吗?", Options: optStr, OrderNum: 1}, - {ConstitutionType: model.ConstitutionXueyu, QuestionText: "您两颧部有细微红丝吗?", Options: optStr, OrderNum: 2}, - {ConstitutionType: model.ConstitutionXueyu, QuestionText: "您身体上有哪里疼痛吗?", Options: optStr, OrderNum: 3}, - {ConstitutionType: model.ConstitutionXueyu, QuestionText: "您面色晦暗或容易出现褐斑吗?", Options: optStr, OrderNum: 4}, - {ConstitutionType: model.ConstitutionXueyu, QuestionText: "您容易有黑眼圈吗?", Options: optStr, OrderNum: 5}, - {ConstitutionType: model.ConstitutionXueyu, QuestionText: "您容易忘事吗?", Options: optStr, OrderNum: 6}, - {ConstitutionType: model.ConstitutionXueyu, QuestionText: "您口唇颜色偏暗吗?", Options: optStr, OrderNum: 7}, - - // 气郁质 (7题) - {ConstitutionType: model.ConstitutionQiyu, QuestionText: "您感到闷闷不乐、情绪低沉吗?", Options: optStr, OrderNum: 1}, - {ConstitutionType: model.ConstitutionQiyu, QuestionText: "您精神紧张、焦虑不安吗?", Options: optStr, OrderNum: 2}, - {ConstitutionType: model.ConstitutionQiyu, QuestionText: "您多愁善感、感情脆弱吗?", Options: optStr, OrderNum: 3}, - {ConstitutionType: model.ConstitutionQiyu, QuestionText: "您容易感到害怕或受到惊吓吗?", Options: optStr, OrderNum: 4}, - {ConstitutionType: model.ConstitutionQiyu, QuestionText: "您胁肋部或乳房胀痛吗?", Options: optStr, OrderNum: 5}, - {ConstitutionType: model.ConstitutionQiyu, QuestionText: "您无缘无故叹气吗?", Options: optStr, OrderNum: 6}, - {ConstitutionType: model.ConstitutionQiyu, QuestionText: "您咽喉部有异物感吗?", Options: optStr, OrderNum: 7}, - - // 特禀质 (7题) - {ConstitutionType: model.ConstitutionTebing, QuestionText: "您没有感冒时也会打喷嚏吗?", Options: optStr, OrderNum: 1}, - {ConstitutionType: model.ConstitutionTebing, QuestionText: "您没有感冒时也会鼻塞、流鼻涕吗?", Options: optStr, OrderNum: 2}, - {ConstitutionType: model.ConstitutionTebing, QuestionText: "您有因季节变化、温度变化或异味引起的咳嗽吗?", Options: optStr, OrderNum: 3}, - {ConstitutionType: model.ConstitutionTebing, QuestionText: "您容易过敏吗?", Options: optStr, OrderNum: 4}, - {ConstitutionType: model.ConstitutionTebing, QuestionText: "您皮肤容易起荨麻疹吗?", Options: optStr, OrderNum: 5}, - {ConstitutionType: model.ConstitutionTebing, QuestionText: "您皮肤一抓就红,并出现抓痕吗?", Options: optStr, OrderNum: 6}, - {ConstitutionType: model.ConstitutionTebing, QuestionText: "您皮肤或身上容易出现紫红色瘀点、瘀斑吗?", Options: optStr, OrderNum: 7}, - } -} diff --git a/server/internal/model/models.go b/server/internal/model/models.go deleted file mode 100644 index 7fa652f..0000000 --- a/server/internal/model/models.go +++ /dev/null @@ -1,31 +0,0 @@ -package model - -// AllModels 返回所有需要迁移的模型 -func AllModels() []interface{} { - return []interface{}{ - // 用户相关 - &User{}, - &HealthProfile{}, - &LifestyleInfo{}, - - // 健康相关 - &MedicalHistory{}, - &FamilyHistory{}, - &AllergyRecord{}, - - // 体质相关 - &ConstitutionAssessment{}, - &AssessmentAnswer{}, - &QuestionBank{}, - - // 对话相关 - &Conversation{}, - &Message{}, - - // 产品相关 - &Product{}, - &ConstitutionProduct{}, - &SymptomProduct{}, - &PurchaseHistory{}, - } -} diff --git a/server/internal/model/product.go b/server/internal/model/product.go deleted file mode 100644 index 894d53c..0000000 --- a/server/internal/model/product.go +++ /dev/null @@ -1,66 +0,0 @@ -package model - -import ( - "time" - - "gorm.io/gorm" -) - -// Product 保健品表 -type Product struct { - gorm.Model - Name string `gorm:"size:100" json:"name"` - Category string `gorm:"size:50;index" json:"category"` - Description string `gorm:"type:text" json:"description"` - Efficacy string `gorm:"type:text" json:"efficacy"` // 功效说明 - Suitable string `gorm:"type:text" json:"suitable"` // 适用人群/体质 - Price float64 `json:"price"` - ImageURL string `gorm:"size:255" json:"image_url"` - MallURL string `gorm:"size:255" json:"mall_url"` // 商城链接 - IsActive bool `gorm:"default:true" json:"is_active"` -} - -// ConstitutionProduct 体质-产品关联表 -type ConstitutionProduct struct { - gorm.Model - ConstitutionType string `gorm:"size:20;index" json:"constitution_type"` - ProductID uint `gorm:"index" json:"product_id"` - Priority int `json:"priority"` // 推荐优先级 - Reason string `gorm:"size:200" json:"reason"` // 推荐理由 -} - -// SymptomProduct 症状-产品关联表 -type SymptomProduct struct { - gorm.Model - Keyword string `gorm:"size:50;index" json:"keyword"` // 症状关键词 - ProductID uint `gorm:"index" json:"product_id"` - Priority int `json:"priority"` -} - -// PurchaseHistory 购买历史表(商城同步) -type PurchaseHistory struct { - gorm.Model - UserID uint `gorm:"index" json:"user_id"` - OrderNo string `gorm:"size:50" json:"order_no"` // 商城订单号 - ProductID uint `json:"product_id"` - ProductName string `gorm:"size:100" json:"product_name"` - PurchasedAt time.Time `json:"purchased_at"` - Source string `gorm:"size:20" json:"source"` // mall=保健品商城 -} - -// TableName 指定表名 -func (Product) TableName() string { - return "products" -} - -func (ConstitutionProduct) TableName() string { - return "constitution_products" -} - -func (SymptomProduct) TableName() string { - return "symptom_products" -} - -func (PurchaseHistory) TableName() string { - return "purchase_histories" -} diff --git a/server/internal/repository/impl/constitution.go b/server/internal/repository/impl/constitution.go deleted file mode 100644 index a99bddc..0000000 --- a/server/internal/repository/impl/constitution.go +++ /dev/null @@ -1,71 +0,0 @@ -package impl - -import ( - "health-ai/internal/database" - "health-ai/internal/model" -) - -type ConstitutionRepository struct{} - -func NewConstitutionRepository() *ConstitutionRepository { - return &ConstitutionRepository{} -} - -// GetQuestions 获取所有问卷题目 -func (r *ConstitutionRepository) GetQuestions() ([]model.QuestionBank, error) { - var questions []model.QuestionBank - err := database.DB.Order("constitution_type, order_num").Find(&questions).Error - return questions, err -} - -// GetQuestionsByType 获取指定体质类型的问题 -func (r *ConstitutionRepository) GetQuestionsByType(constitutionType string) ([]model.QuestionBank, error) { - var questions []model.QuestionBank - err := database.DB.Where("constitution_type = ?", constitutionType).Order("order_num").Find(&questions).Error - return questions, err -} - -// CreateAssessment 创建体质测评记录 -func (r *ConstitutionRepository) CreateAssessment(assessment *model.ConstitutionAssessment) error { - return database.DB.Create(assessment).Error -} - -// GetLatestAssessment 获取用户最新的体质测评结果 -func (r *ConstitutionRepository) GetLatestAssessment(userID uint) (*model.ConstitutionAssessment, error) { - var assessment model.ConstitutionAssessment - err := database.DB.Where("user_id = ?", userID).Order("assessed_at DESC").First(&assessment).Error - return &assessment, err -} - -// GetAssessmentHistory 获取用户的体质测评历史 -func (r *ConstitutionRepository) GetAssessmentHistory(userID uint, limit int) ([]model.ConstitutionAssessment, error) { - var assessments []model.ConstitutionAssessment - query := database.DB.Where("user_id = ?", userID).Order("assessed_at DESC") - if limit > 0 { - query = query.Limit(limit) - } - err := query.Find(&assessments).Error - return assessments, err -} - -// GetAssessmentByID 根据ID获取测评记录 -func (r *ConstitutionRepository) GetAssessmentByID(id uint) (*model.ConstitutionAssessment, error) { - var assessment model.ConstitutionAssessment - err := database.DB.First(&assessment, id).Error - return &assessment, err -} - -// CreateAnswers 批量创建问卷答案 -func (r *ConstitutionRepository) CreateAnswers(answers []model.AssessmentAnswer) error { - if len(answers) == 0 { - return nil - } - return database.DB.Create(&answers).Error -} - -// GetAnswersByAssessmentID 获取测评的所有答案 -func (r *ConstitutionRepository) GetAnswersByAssessmentID(assessmentID uint) ([]model.AssessmentAnswer, error) { - var answers []model.AssessmentAnswer - err := database.DB.Where("assessment_id = ?", assessmentID).Find(&answers).Error - return answers, err -} diff --git a/server/internal/repository/impl/conversation.go b/server/internal/repository/impl/conversation.go deleted file mode 100644 index 8d2940d..0000000 --- a/server/internal/repository/impl/conversation.go +++ /dev/null @@ -1,78 +0,0 @@ -package impl - -import ( - "health-ai/internal/database" - "health-ai/internal/model" -) - -type ConversationRepository struct{} - -func NewConversationRepository() *ConversationRepository { - return &ConversationRepository{} -} - -// Create 创建对话 -func (r *ConversationRepository) Create(conv *model.Conversation) error { - return database.DB.Create(conv).Error -} - -// GetByID 根据ID获取对话(含消息) -func (r *ConversationRepository) GetByID(id uint) (*model.Conversation, error) { - var conv model.Conversation - err := database.DB.Preload("Messages").First(&conv, id).Error - return &conv, err -} - -// GetByUserID 获取用户的所有对话 -func (r *ConversationRepository) GetByUserID(userID uint) ([]model.Conversation, error) { - var convs []model.Conversation - err := database.DB.Where("user_id = ?", userID).Order("updated_at DESC").Find(&convs).Error - return convs, err -} - -// Delete 删除对话(同时删除消息) -func (r *ConversationRepository) Delete(id uint) error { - // 先删除消息 - database.DB.Where("conversation_id = ?", id).Delete(&model.Message{}) - return database.DB.Delete(&model.Conversation{}, id).Error -} - -// AddMessage 添加消息 -func (r *ConversationRepository) AddMessage(msg *model.Message) error { - // 同时更新对话的更新时间 - database.DB.Model(&model.Conversation{}).Where("id = ?", msg.ConversationID).Update("updated_at", msg.CreatedAt) - return database.DB.Create(msg).Error -} - -// GetMessages 获取对话的消息 -func (r *ConversationRepository) GetMessages(convID uint) ([]model.Message, error) { - var messages []model.Message - err := database.DB.Where("conversation_id = ?", convID).Order("created_at ASC").Find(&messages).Error - return messages, err -} - -// GetRecentMessages 获取对话最近的N条消息 -func (r *ConversationRepository) GetRecentMessages(convID uint, limit int) ([]model.Message, error) { - var messages []model.Message - err := database.DB.Where("conversation_id = ?", convID).Order("created_at DESC").Limit(limit).Find(&messages).Error - if err != nil { - return nil, err - } - // 反转顺序,使消息按时间正序排列 - for i, j := 0, len(messages)-1; i < j; i, j = i+1, j-1 { - messages[i], messages[j] = messages[j], messages[i] - } - return messages, nil -} - -// UpdateTitle 更新对话标题 -func (r *ConversationRepository) UpdateTitle(id uint, title string) error { - return database.DB.Model(&model.Conversation{}).Where("id = ?", id).Update("title", title).Error -} - -// CheckOwnership 检查对话是否属于用户 -func (r *ConversationRepository) CheckOwnership(convID, userID uint) bool { - var count int64 - database.DB.Model(&model.Conversation{}).Where("id = ? AND user_id = ?", convID, userID).Count(&count) - return count > 0 -} diff --git a/server/internal/repository/impl/health.go b/server/internal/repository/impl/health.go deleted file mode 100644 index c0c33d5..0000000 --- a/server/internal/repository/impl/health.go +++ /dev/null @@ -1,127 +0,0 @@ -package impl - -import ( - "health-ai/internal/database" - "health-ai/internal/model" -) - -type HealthRepository struct{} - -func NewHealthRepository() *HealthRepository { - return &HealthRepository{} -} - -// ================= HealthProfile ================= - -func (r *HealthRepository) CreateProfile(profile *model.HealthProfile) error { - return database.DB.Create(profile).Error -} - -func (r *HealthRepository) GetProfileByUserID(userID uint) (*model.HealthProfile, error) { - var profile model.HealthProfile - err := database.DB.Where("user_id = ?", userID).First(&profile).Error - return &profile, err -} - -func (r *HealthRepository) UpdateProfile(profile *model.HealthProfile) error { - return database.DB.Save(profile).Error -} - -// ================= LifestyleInfo ================= - -func (r *HealthRepository) CreateLifestyle(lifestyle *model.LifestyleInfo) error { - return database.DB.Create(lifestyle).Error -} - -func (r *HealthRepository) GetLifestyleByUserID(userID uint) (*model.LifestyleInfo, error) { - var lifestyle model.LifestyleInfo - err := database.DB.Where("user_id = ?", userID).First(&lifestyle).Error - return &lifestyle, err -} - -func (r *HealthRepository) UpdateLifestyle(lifestyle *model.LifestyleInfo) error { - return database.DB.Save(lifestyle).Error -} - -// ================= MedicalHistory ================= - -func (r *HealthRepository) CreateMedicalHistory(history *model.MedicalHistory) error { - return database.DB.Create(history).Error -} - -func (r *HealthRepository) GetMedicalHistories(profileID uint) ([]model.MedicalHistory, error) { - var histories []model.MedicalHistory - err := database.DB.Where("health_profile_id = ?", profileID).Find(&histories).Error - return histories, err -} - -func (r *HealthRepository) DeleteMedicalHistory(id uint) error { - return database.DB.Delete(&model.MedicalHistory{}, id).Error -} - -func (r *HealthRepository) BatchCreateMedicalHistories(histories []model.MedicalHistory) error { - if len(histories) == 0 { - return nil - } - return database.DB.Create(&histories).Error -} - -// ================= FamilyHistory ================= - -func (r *HealthRepository) CreateFamilyHistory(history *model.FamilyHistory) error { - return database.DB.Create(history).Error -} - -func (r *HealthRepository) GetFamilyHistories(profileID uint) ([]model.FamilyHistory, error) { - var histories []model.FamilyHistory - err := database.DB.Where("health_profile_id = ?", profileID).Find(&histories).Error - return histories, err -} - -func (r *HealthRepository) DeleteFamilyHistory(id uint) error { - return database.DB.Delete(&model.FamilyHistory{}, id).Error -} - -func (r *HealthRepository) BatchCreateFamilyHistories(histories []model.FamilyHistory) error { - if len(histories) == 0 { - return nil - } - return database.DB.Create(&histories).Error -} - -// ================= AllergyRecord ================= - -func (r *HealthRepository) CreateAllergyRecord(record *model.AllergyRecord) error { - return database.DB.Create(record).Error -} - -func (r *HealthRepository) GetAllergyRecords(profileID uint) ([]model.AllergyRecord, error) { - var records []model.AllergyRecord - err := database.DB.Where("health_profile_id = ?", profileID).Find(&records).Error - return records, err -} - -func (r *HealthRepository) DeleteAllergyRecord(id uint) error { - return database.DB.Delete(&model.AllergyRecord{}, id).Error -} - -func (r *HealthRepository) BatchCreateAllergyRecords(records []model.AllergyRecord) error { - if len(records) == 0 { - return nil - } - return database.DB.Create(&records).Error -} - -// ================= 清除旧数据方法 ================= - -func (r *HealthRepository) ClearMedicalHistories(profileID uint) error { - return database.DB.Where("health_profile_id = ?", profileID).Delete(&model.MedicalHistory{}).Error -} - -func (r *HealthRepository) ClearFamilyHistories(profileID uint) error { - return database.DB.Where("health_profile_id = ?", profileID).Delete(&model.FamilyHistory{}).Error -} - -func (r *HealthRepository) ClearAllergyRecords(profileID uint) error { - return database.DB.Where("health_profile_id = ?", profileID).Delete(&model.AllergyRecord{}).Error -} diff --git a/server/internal/repository/impl/product.go b/server/internal/repository/impl/product.go deleted file mode 100644 index 2600e5a..0000000 --- a/server/internal/repository/impl/product.go +++ /dev/null @@ -1,99 +0,0 @@ -package impl - -import ( - "health-ai/internal/database" - "health-ai/internal/model" -) - -type ProductRepository struct{} - -func NewProductRepository() *ProductRepository { - return &ProductRepository{} -} - -// GetByID 根据ID获取产品 -func (r *ProductRepository) GetByID(id uint) (*model.Product, error) { - var product model.Product - err := database.DB.Where("is_active = ?", true).First(&product, id).Error - return &product, err -} - -// GetAll 获取所有产品(分页) -func (r *ProductRepository) GetAll(page, pageSize int) ([]model.Product, int64, error) { - var products []model.Product - var total int64 - - db := database.DB.Model(&model.Product{}).Where("is_active = ?", true) - db.Count(&total) - - offset := (page - 1) * pageSize - err := db.Offset(offset).Limit(pageSize).Find(&products).Error - return products, total, err -} - -// GetByCategory 按分类获取产品 -func (r *ProductRepository) GetByCategory(category string) ([]model.Product, error) { - var products []model.Product - err := database.DB.Where("category = ? AND is_active = ?", category, true).Find(&products).Error - return products, err -} - -// GetByConstitution 根据体质获取推荐产品 -func (r *ProductRepository) GetByConstitution(constitutionType string) ([]model.Product, error) { - var products []model.Product - err := database.DB.Joins("JOIN constitution_products ON products.id = constitution_products.product_id"). - Where("constitution_products.constitution_type = ? AND products.is_active = ?", constitutionType, true). - Order("constitution_products.priority ASC"). - Find(&products).Error - return products, err -} - -// SearchByKeyword 根据症状关键词搜索产品 -func (r *ProductRepository) SearchByKeyword(keyword string) ([]model.Product, error) { - var products []model.Product - - // 先从症状-产品关联表搜索 - subQuery := database.DB.Table("symptom_products"). - Select("product_id"). - Where("keyword LIKE ?", "%"+keyword+"%") - - err := database.DB.Where("id IN (?) AND is_active = ?", subQuery, true).Find(&products).Error - if err != nil { - return nil, err - } - - // 如果没找到,从产品名称和描述中搜索 - if len(products) == 0 { - err = database.DB.Where("(name LIKE ? OR description LIKE ? OR efficacy LIKE ?) AND is_active = ?", - "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%", true).Find(&products).Error - } - - return products, err -} - -// GetConstitutionProducts 获取体质-产品关联 -func (r *ProductRepository) GetConstitutionProducts(constitutionType string) ([]model.ConstitutionProduct, error) { - var cps []model.ConstitutionProduct - err := database.DB.Where("constitution_type = ?", constitutionType).Order("priority ASC").Find(&cps).Error - return cps, err -} - -// CreatePurchaseHistory 创建购买历史 -func (r *ProductRepository) CreatePurchaseHistory(history *model.PurchaseHistory) error { - return database.DB.Create(history).Error -} - -// GetPurchaseHistory 获取用户购买历史 -func (r *ProductRepository) GetPurchaseHistory(userID uint) ([]model.PurchaseHistory, error) { - var histories []model.PurchaseHistory - err := database.DB.Where("user_id = ?", userID).Order("purchased_at DESC").Find(&histories).Error - return histories, err -} - -// BatchCreatePurchaseHistory 批量创建购买历史 -func (r *ProductRepository) BatchCreatePurchaseHistory(histories []model.PurchaseHistory) error { - if len(histories) == 0 { - return nil - } - return database.DB.Create(&histories).Error -} diff --git a/server/internal/repository/impl/user.go b/server/internal/repository/impl/user.go deleted file mode 100644 index 2e4a812..0000000 --- a/server/internal/repository/impl/user.go +++ /dev/null @@ -1,42 +0,0 @@ -package impl - -import ( - "health-ai/internal/database" - "health-ai/internal/model" -) - -type UserRepositoryImpl struct{} - -func NewUserRepository() *UserRepositoryImpl { - return &UserRepositoryImpl{} -} - -func (r *UserRepositoryImpl) Create(user *model.User) error { - return database.DB.Create(user).Error -} - -func (r *UserRepositoryImpl) GetByID(id uint) (*model.User, error) { - var user model.User - err := database.DB.First(&user, id).Error - return &user, err -} - -func (r *UserRepositoryImpl) GetByPhone(phone string) (*model.User, error) { - var user model.User - err := database.DB.Where("phone = ?", phone).First(&user).Error - return &user, err -} - -func (r *UserRepositoryImpl) GetByEmail(email string) (*model.User, error) { - var user model.User - err := database.DB.Where("email = ?", email).First(&user).Error - return &user, err -} - -func (r *UserRepositoryImpl) Update(user *model.User) error { - return database.DB.Save(user).Error -} - -func (r *UserRepositoryImpl) UpdateSurveyStatus(userID uint, completed bool) error { - return database.DB.Model(&model.User{}).Where("id = ?", userID).Update("survey_completed", completed).Error -} diff --git a/server/internal/repository/interface.go b/server/internal/repository/interface.go deleted file mode 100644 index 2ccb8c9..0000000 --- a/server/internal/repository/interface.go +++ /dev/null @@ -1,66 +0,0 @@ -package repository - -import "health-ai/internal/model" - -// UserRepository 用户数据访问接口 -type UserRepository interface { - Create(user *model.User) error - GetByID(id uint) (*model.User, error) - GetByPhone(phone string) (*model.User, error) - GetByEmail(email string) (*model.User, error) - Update(user *model.User) error - UpdateSurveyStatus(userID uint, completed bool) error -} - -// HealthProfileRepository 健康档案数据访问接口 -type HealthProfileRepository interface { - Create(profile *model.HealthProfile) error - GetByUserID(userID uint) (*model.HealthProfile, error) - Update(profile *model.HealthProfile) error - Upsert(profile *model.HealthProfile) error -} - -// LifestyleRepository 生活习惯数据访问接口 -type LifestyleRepository interface { - Create(lifestyle *model.LifestyleInfo) error - GetByUserID(userID uint) (*model.LifestyleInfo, error) - Update(lifestyle *model.LifestyleInfo) error - Upsert(lifestyle *model.LifestyleInfo) error -} - -// MedicalHistoryRepository 病史数据访问接口 -type MedicalHistoryRepository interface { - Create(history *model.MedicalHistory) error - GetByHealthProfileID(healthProfileID uint) ([]model.MedicalHistory, error) - Update(history *model.MedicalHistory) error - Delete(id uint) error - BatchCreate(histories []model.MedicalHistory) error -} - -// ConstitutionRepository 体质测评数据访问接口 -type ConstitutionRepository interface { - CreateAssessment(assessment *model.ConstitutionAssessment) error - GetLatestByUserID(userID uint) (*model.ConstitutionAssessment, error) - GetHistoryByUserID(userID uint, limit int) ([]model.ConstitutionAssessment, error) - GetQuestions() ([]model.QuestionBank, error) - CreateAnswers(answers []model.AssessmentAnswer) error -} - -// ConversationRepository 对话数据访问接口 -type ConversationRepository interface { - Create(conversation *model.Conversation) error - GetByID(id uint) (*model.Conversation, error) - GetByUserID(userID uint) ([]model.Conversation, error) - Delete(id uint) error - AddMessage(message *model.Message) error - GetMessages(conversationID uint, limit int) ([]model.Message, error) -} - -// ProductRepository 产品数据访问接口 -type ProductRepository interface { - GetByID(id uint) (*model.Product, error) - GetByCategory(category string) ([]model.Product, error) - GetByConstitution(constitutionType string) ([]model.Product, error) - SearchByKeyword(keyword string) ([]model.Product, error) - GetAll(page, pageSize int) ([]model.Product, int64, error) -} diff --git a/server/internal/service/ai/aliyun.go b/server/internal/service/ai/aliyun.go deleted file mode 100644 index e1a1544..0000000 --- a/server/internal/service/ai/aliyun.go +++ /dev/null @@ -1,172 +0,0 @@ -package ai - -import ( - "bufio" - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "strings" -) - -// 使用阿里云DashScope的OpenAI兼容模式(官方推荐) -const AliyunBaseURL = "https://dashscope.aliyuncs.com/compatible-mode/v1" - -type AliyunClient struct { - apiKey string - model string -} - -func NewAliyunClient(cfg *Config) *AliyunClient { - model := cfg.Model - if model == "" { - model = "qwen-turbo" - } - return &AliyunClient{ - apiKey: cfg.APIKey, - model: model, - } -} - -// OpenAI兼容格式的请求 -type aliyunRequest struct { - Model string `json:"model"` - Messages []Message `json:"messages"` - Stream bool `json:"stream,omitempty"` -} - -// OpenAI兼容格式的响应 -type aliyunResponse struct { - ID string `json:"id"` - Object string `json:"object"` - Created int64 `json:"created"` - Model string `json:"model"` - Choices []struct { - Index int `json:"index"` - Message struct { - Role string `json:"role"` - Content string `json:"content"` - } `json:"message"` - Delta struct { - Content string `json:"content"` - } `json:"delta"` - FinishReason string `json:"finish_reason"` - } `json:"choices"` - Usage struct { - PromptTokens int `json:"prompt_tokens"` - CompletionTokens int `json:"completion_tokens"` - TotalTokens int `json:"total_tokens"` - } `json:"usage"` - Error *struct { - Message string `json:"message"` - Type string `json:"type"` - Code string `json:"code"` - } `json:"error"` -} - -func (c *AliyunClient) Chat(ctx context.Context, messages []Message) (string, error) { - if c.apiKey == "" { - return "", fmt.Errorf("阿里云通义千问 API Key 未配置,请在 config.yaml 中设置 ai.aliyun.api_key") - } - - reqBody := aliyunRequest{ - Model: c.model, - Messages: messages, - Stream: false, - } - - body, _ := json.Marshal(reqBody) - req, err := http.NewRequestWithContext(ctx, "POST", AliyunBaseURL+"/chat/completions", bytes.NewReader(body)) - if err != nil { - return "", err - } - - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+c.apiKey) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", fmt.Errorf("调用AI服务失败: %v", err) - } - defer resp.Body.Close() - - var result aliyunResponse - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return "", fmt.Errorf("解析AI响应失败: %v", err) - } - - // 检查错误 - if result.Error != nil { - return "", fmt.Errorf("AI服务错误: %s (code: %s)", result.Error.Message, result.Error.Code) - } - - if len(result.Choices) == 0 { - return "", fmt.Errorf("AI未返回有效响应") - } - - return result.Choices[0].Message.Content, nil -} - -func (c *AliyunClient) ChatStream(ctx context.Context, messages []Message, writer io.Writer) error { - if c.apiKey == "" { - return fmt.Errorf("阿里云通义千问 API Key 未配置") - } - - reqBody := aliyunRequest{ - Model: c.model, - Messages: messages, - Stream: true, - } - - body, _ := json.Marshal(reqBody) - req, err := http.NewRequestWithContext(ctx, "POST", AliyunBaseURL+"/chat/completions", bytes.NewReader(body)) - if err != nil { - return err - } - - req.Header.Set("Content-Type", "application/json") - req.Header.Set("Authorization", "Bearer "+c.apiKey) - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - // 解析 SSE 流 - reader := bufio.NewReader(resp.Body) - for { - line, err := reader.ReadString('\n') - if err == io.EOF { - break - } - if err != nil { - return err - } - - line = strings.TrimSpace(line) - if strings.HasPrefix(line, "data:") { - data := strings.TrimPrefix(line, "data:") - data = strings.TrimSpace(data) - if data == "[DONE]" { - break - } - - var streamResp aliyunResponse - if err := json.Unmarshal([]byte(data), &streamResp); err != nil { - continue - } - - if len(streamResp.Choices) > 0 { - content := streamResp.Choices[0].Delta.Content - if content != "" { - writer.Write([]byte(content)) - } - } - } - } - - return nil -} diff --git a/server/internal/service/auth.go b/server/internal/service/auth.go deleted file mode 100644 index dd6f778..0000000 --- a/server/internal/service/auth.go +++ /dev/null @@ -1,161 +0,0 @@ -package service - -import ( - "errors" - - "health-ai/internal/model" - "health-ai/internal/repository/impl" - "health-ai/pkg/jwt" - - "golang.org/x/crypto/bcrypt" -) - -type AuthService struct { - userRepo *impl.UserRepositoryImpl -} - -func NewAuthService() *AuthService { - return &AuthService{ - userRepo: impl.NewUserRepository(), - } -} - -// RegisterRequest 注册请求 -type RegisterRequest struct { - Phone string `json:"phone" binding:"required"` - Password string `json:"password" binding:"required,min=6"` - Nickname string `json:"nickname"` -} - -// LoginRequest 登录请求 -type LoginRequest struct { - Phone string `json:"phone" binding:"required"` - Password string `json:"password" binding:"required"` -} - -// AuthResponse 认证响应 -type AuthResponse struct { - Token string `json:"token"` - UserID uint `json:"user_id"` - Nickname string `json:"nickname"` - Avatar string `json:"avatar"` - SurveyCompleted bool `json:"survey_completed"` -} - -// UserInfoResponse 用户信息响应 -type UserInfoResponse struct { - UserID uint `json:"user_id"` - Phone string `json:"phone"` - Email string `json:"email"` - Nickname string `json:"nickname"` - Avatar string `json:"avatar"` - SurveyCompleted bool `json:"survey_completed"` -} - -// Register 用户注册 -func (s *AuthService) Register(req *RegisterRequest) (*AuthResponse, error) { - // 检查手机号是否已注册 - existing, _ := s.userRepo.GetByPhone(req.Phone) - if existing.ID > 0 { - return nil, errors.New("手机号已注册") - } - - // 加密密码 - hash, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost) - if err != nil { - return nil, errors.New("密码加密失败") - } - - // 创建用户 - user := &model.User{ - Phone: req.Phone, - PasswordHash: string(hash), - Nickname: req.Nickname, - } - if user.Nickname == "" { - // 默认昵称:手机号后4位 - user.Nickname = "用户" + req.Phone[len(req.Phone)-4:] - } - - if err := s.userRepo.Create(user); err != nil { - return nil, errors.New("创建用户失败") - } - - // 生成 Token - token, err := jwt.GenerateToken(user.ID) - if err != nil { - return nil, errors.New("生成Token失败") - } - - return &AuthResponse{ - Token: token, - UserID: user.ID, - Nickname: user.Nickname, - Avatar: user.Avatar, - SurveyCompleted: user.SurveyCompleted, - }, nil -} - -// Login 用户登录 -func (s *AuthService) Login(req *LoginRequest) (*AuthResponse, error) { - user, err := s.userRepo.GetByPhone(req.Phone) - if err != nil { - return nil, errors.New("用户不存在") - } - - if err := bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(req.Password)); err != nil { - return nil, errors.New("密码错误") - } - - token, err := jwt.GenerateToken(user.ID) - if err != nil { - return nil, errors.New("生成Token失败") - } - - return &AuthResponse{ - Token: token, - UserID: user.ID, - Nickname: user.Nickname, - Avatar: user.Avatar, - SurveyCompleted: user.SurveyCompleted, - }, nil -} - -// GetUserInfo 获取用户信息 -func (s *AuthService) GetUserInfo(userID uint) (*UserInfoResponse, error) { - user, err := s.userRepo.GetByID(userID) - if err != nil { - return nil, errors.New("用户不存在") - } - - return &UserInfoResponse{ - UserID: user.ID, - Phone: user.Phone, - Email: user.Email, - Nickname: user.Nickname, - Avatar: user.Avatar, - SurveyCompleted: user.SurveyCompleted, - }, nil -} - -// RefreshToken 刷新Token -func (s *AuthService) RefreshToken(oldToken string) (string, error) { - return jwt.RefreshToken(oldToken) -} - -// UpdateProfile 更新用户资料 -func (s *AuthService) UpdateProfile(userID uint, nickname, avatar string) error { - user, err := s.userRepo.GetByID(userID) - if err != nil { - return errors.New("用户不存在") - } - - if nickname != "" { - user.Nickname = nickname - } - if avatar != "" { - user.Avatar = avatar - } - - return s.userRepo.Update(user) -} diff --git a/server/internal/service/constitution.go b/server/internal/service/constitution.go deleted file mode 100644 index 0e022a9..0000000 --- a/server/internal/service/constitution.go +++ /dev/null @@ -1,326 +0,0 @@ -package service - -import ( - "encoding/json" - "errors" - "sort" - "time" - - "health-ai/internal/model" - "health-ai/internal/repository/impl" -) - -type ConstitutionService struct { - repo *impl.ConstitutionRepository -} - -func NewConstitutionService() *ConstitutionService { - return &ConstitutionService{ - repo: impl.NewConstitutionRepository(), - } -} - -// ================= 请求结构体 ================= - -// AnswerRequest 单个答案请求 -type AnswerRequest struct { - QuestionID uint `json:"question_id" binding:"required"` - Score int `json:"score" binding:"required,min=1,max=5"` -} - -// SubmitAssessmentRequest 提交测评请求 -type SubmitAssessmentRequest struct { - Answers []AnswerRequest `json:"answers" binding:"required,dive"` -} - -// ================= 响应结构体 ================= - -// ConstitutionScore 体质得分 -type ConstitutionScore struct { - Type string `json:"type"` - Name string `json:"name"` - Score float64 `json:"score"` - Description string `json:"description"` -} - -// AssessmentResult 测评结果 -type AssessmentResult struct { - ID uint `json:"id"` - PrimaryConstitution ConstitutionScore `json:"primary_constitution"` - SecondaryConstitutions []ConstitutionScore `json:"secondary_constitutions"` - AllScores []ConstitutionScore `json:"all_scores"` - Recommendations map[string]map[string]string `json:"recommendations"` - AssessedAt time.Time `json:"assessed_at"` -} - -// QuestionGroup 问题分组 -type QuestionGroup struct { - ConstitutionType string `json:"constitution_type"` - ConstitutionName string `json:"constitution_name"` - Questions []model.QuestionBank `json:"questions"` -} - -// ================= Service 方法 ================= - -// GetQuestions 获取所有问卷题目 -func (s *ConstitutionService) GetQuestions() ([]model.QuestionBank, error) { - return s.repo.GetQuestions() -} - -// GetQuestionsGrouped 获取分组的问卷题目 -func (s *ConstitutionService) GetQuestionsGrouped() ([]QuestionGroup, error) { - questions, err := s.repo.GetQuestions() - if err != nil { - return nil, err - } - - // 按体质类型分组 - groupMap := make(map[string][]model.QuestionBank) - for _, q := range questions { - groupMap[q.ConstitutionType] = append(groupMap[q.ConstitutionType], q) - } - - // 转换为数组 - var groups []QuestionGroup - typeOrder := []string{ - model.ConstitutionPinghe, - model.ConstitutionQixu, - model.ConstitutionYangxu, - model.ConstitutionYinxu, - model.ConstitutionTanshi, - model.ConstitutionShire, - model.ConstitutionXueyu, - model.ConstitutionQiyu, - model.ConstitutionTebing, - } - - for _, cType := range typeOrder { - if qs, ok := groupMap[cType]; ok { - groups = append(groups, QuestionGroup{ - ConstitutionType: cType, - ConstitutionName: model.ConstitutionNames[cType], - Questions: qs, - }) - } - } - - return groups, nil -} - -// SubmitAssessment 提交测评并计算结果 -func (s *ConstitutionService) SubmitAssessment(userID uint, req *SubmitAssessmentRequest) (*AssessmentResult, error) { - // 获取所有问题 - questions, err := s.repo.GetQuestions() - if err != nil { - return nil, err - } - - if len(questions) == 0 { - return nil, errors.New("问卷题库为空,请联系管理员") - } - - // 构建问题ID到体质类型的映射 - questionTypeMap := make(map[uint]string) - typeQuestionCount := make(map[string]int) - for _, q := range questions { - questionTypeMap[q.ID] = q.ConstitutionType - typeQuestionCount[q.ConstitutionType]++ - } - - // 计算各体质原始分 - typeScores := make(map[string]int) - for _, answer := range req.Answers { - if cType, ok := questionTypeMap[answer.QuestionID]; ok { - typeScores[cType] += answer.Score - } - } - - // 计算转化分 - // 转化分 = (原始分 - 条目数) / (条目数 × 4) × 100 - allScores := make([]ConstitutionScore, 0) - for cType, rawScore := range typeScores { - questionCount := typeQuestionCount[cType] - if questionCount == 0 { - continue - } - transformedScore := float64(rawScore-questionCount) / float64(questionCount*4) * 100 - if transformedScore < 0 { - transformedScore = 0 - } - if transformedScore > 100 { - transformedScore = 100 - } - - allScores = append(allScores, ConstitutionScore{ - Type: cType, - Name: model.ConstitutionNames[cType], - Score: transformedScore, - Description: model.ConstitutionDescriptions[cType], - }) - } - - // 按分数排序 - sort.Slice(allScores, func(i, j int) bool { - return allScores[i].Score > allScores[j].Score - }) - - // 判定主要体质和次要体质 - var primary ConstitutionScore - var secondary []ConstitutionScore - - // 平和质特殊判定 - pingheScore := float64(0) - otherMax := float64(0) - for _, score := range allScores { - if score.Type == model.ConstitutionPinghe { - pingheScore = score.Score - } else if score.Score > otherMax { - otherMax = score.Score - } - } - - if pingheScore >= 60 && otherMax < 30 { - // 判定为平和质 - for _, score := range allScores { - if score.Type == model.ConstitutionPinghe { - primary = score - break - } - } - } else { - // 判定为偏颇体质 - for _, score := range allScores { - if score.Type == model.ConstitutionPinghe { - continue - } - if primary.Type == "" && score.Score >= 40 { - primary = score - } else if score.Score >= 30 { - secondary = append(secondary, score) - } - } - // 如果没有≥40的,取最高分(排除平和质) - if primary.Type == "" { - for _, score := range allScores { - if score.Type != model.ConstitutionPinghe { - primary = score - break - } - } - } - } - - // 获取调养建议 - recommendations := make(map[string]map[string]string) - if primary.Type != "" { - recommendations[primary.Type] = model.ConstitutionRecommendations[primary.Type] - } - for _, sec := range secondary { - recommendations[sec.Type] = model.ConstitutionRecommendations[sec.Type] - } - - // 保存评估结果 - scoresJSON, _ := json.Marshal(allScores) - secondaryJSON, _ := json.Marshal(secondary) - recsJSON, _ := json.Marshal(recommendations) - - assessment := &model.ConstitutionAssessment{ - UserID: userID, - AssessedAt: time.Now(), - Scores: string(scoresJSON), - PrimaryConstitution: primary.Type, - SecondaryConstitutions: string(secondaryJSON), - Recommendations: string(recsJSON), - } - if err := s.repo.CreateAssessment(assessment); err != nil { - return nil, err - } - - // 保存答案记录 - answers := make([]model.AssessmentAnswer, len(req.Answers)) - for i, a := range req.Answers { - answers[i] = model.AssessmentAnswer{ - AssessmentID: assessment.ID, - QuestionID: a.QuestionID, - Score: a.Score, - } - } - if err := s.repo.CreateAnswers(answers); err != nil { - // 答案保存失败不影响结果返回 - } - - return &AssessmentResult{ - ID: assessment.ID, - PrimaryConstitution: primary, - SecondaryConstitutions: secondary, - AllScores: allScores, - Recommendations: recommendations, - AssessedAt: assessment.AssessedAt, - }, nil -} - -// GetLatestResult 获取用户最新的测评结果 -func (s *ConstitutionService) GetLatestResult(userID uint) (*AssessmentResult, error) { - assessment, err := s.repo.GetLatestAssessment(userID) - if err != nil { - return nil, errors.New("暂无体质测评记录") - } - - return s.parseAssessment(assessment) -} - -// GetHistory 获取用户的测评历史 -func (s *ConstitutionService) GetHistory(userID uint, limit int) ([]AssessmentResult, error) { - assessments, err := s.repo.GetAssessmentHistory(userID, limit) - if err != nil { - return nil, err - } - - results := make([]AssessmentResult, 0, len(assessments)) - for _, a := range assessments { - result, err := s.parseAssessment(&a) - if err != nil { - continue - } - results = append(results, *result) - } - - return results, nil -} - -// GetRecommendations 获取体质调养建议 -func (s *ConstitutionService) GetRecommendations(userID uint) (map[string]map[string]string, error) { - result, err := s.GetLatestResult(userID) - if err != nil { - return nil, err - } - return result.Recommendations, nil -} - -// parseAssessment 解析测评记录为结果 -func (s *ConstitutionService) parseAssessment(assessment *model.ConstitutionAssessment) (*AssessmentResult, error) { - var allScores []ConstitutionScore - var secondary []ConstitutionScore - var recommendations map[string]map[string]string - - json.Unmarshal([]byte(assessment.Scores), &allScores) - json.Unmarshal([]byte(assessment.SecondaryConstitutions), &secondary) - json.Unmarshal([]byte(assessment.Recommendations), &recommendations) - - var primary ConstitutionScore - for _, score := range allScores { - if score.Type == assessment.PrimaryConstitution { - primary = score - break - } - } - - return &AssessmentResult{ - ID: assessment.ID, - PrimaryConstitution: primary, - SecondaryConstitutions: secondary, - AllScores: allScores, - Recommendations: recommendations, - AssessedAt: assessment.AssessedAt, - }, nil -} diff --git a/server/internal/service/conversation.go b/server/internal/service/conversation.go deleted file mode 100644 index 3988af2..0000000 --- a/server/internal/service/conversation.go +++ /dev/null @@ -1,329 +0,0 @@ -package service - -import ( - "context" - "errors" - "fmt" - "io" - "time" - - "health-ai/internal/config" - "health-ai/internal/model" - "health-ai/internal/repository/impl" - "health-ai/internal/service/ai" -) - -type ConversationService struct { - convRepo *impl.ConversationRepository - constitutionRepo *impl.ConstitutionRepository - healthRepo *impl.HealthRepository - aiClient ai.AIClient -} - -func NewConversationService() *ConversationService { - return &ConversationService{ - convRepo: impl.NewConversationRepository(), - constitutionRepo: impl.NewConstitutionRepository(), - healthRepo: impl.NewHealthRepository(), - aiClient: ai.NewAIClient(&config.AppConfig.AI), - } -} - -// ================= 请求/响应结构体 ================= - -// CreateConversationRequest 创建对话请求 -type CreateConversationRequest struct { - Title string `json:"title"` -} - -// SendMessageRequest 发送消息请求 -type SendMessageRequest struct { - Content string `json:"content" binding:"required"` -} - -// MessageResponse 消息响应 -type MessageResponse struct { - ID uint `json:"id"` - Role string `json:"role"` - Content string `json:"content"` - CreatedAt time.Time `json:"created_at"` -} - -// ConversationResponse 对话响应 -type ConversationResponse struct { - ID uint `json:"id"` - Title string `json:"title"` - Messages []MessageResponse `json:"messages,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -// ================= Service 方法 ================= - -// GetConversations 获取用户对话列表 -func (s *ConversationService) GetConversations(userID uint) ([]ConversationResponse, error) { - convs, err := s.convRepo.GetByUserID(userID) - if err != nil { - return nil, err - } - - result := make([]ConversationResponse, len(convs)) - for i, c := range convs { - result[i] = ConversationResponse{ - ID: c.ID, - Title: c.Title, - CreatedAt: c.CreatedAt, - UpdatedAt: c.UpdatedAt, - } - } - return result, nil -} - -// CreateConversation 创建新对话 -func (s *ConversationService) CreateConversation(userID uint, title string) (*ConversationResponse, error) { - if title == "" { - title = "新对话 " + time.Now().Format("01-02 15:04") - } - conv := &model.Conversation{ - UserID: userID, - Title: title, - } - if err := s.convRepo.Create(conv); err != nil { - return nil, err - } - return &ConversationResponse{ - ID: conv.ID, - Title: conv.Title, - CreatedAt: conv.CreatedAt, - UpdatedAt: conv.UpdatedAt, - }, nil -} - -// GetConversation 获取对话详情 -func (s *ConversationService) GetConversation(userID, convID uint) (*ConversationResponse, error) { - // 检查权限 - if !s.convRepo.CheckOwnership(convID, userID) { - return nil, errors.New("对话不存在或无权访问") - } - - conv, err := s.convRepo.GetByID(convID) - if err != nil { - return nil, errors.New("对话不存在") - } - - messages := make([]MessageResponse, len(conv.Messages)) - for i, m := range conv.Messages { - messages[i] = MessageResponse{ - ID: m.ID, - Role: m.Role, - Content: m.Content, - CreatedAt: m.CreatedAt, - } - } - - return &ConversationResponse{ - ID: conv.ID, - Title: conv.Title, - Messages: messages, - CreatedAt: conv.CreatedAt, - UpdatedAt: conv.UpdatedAt, - }, nil -} - -// DeleteConversation 删除对话 -func (s *ConversationService) DeleteConversation(userID, convID uint) error { - // 检查权限 - if !s.convRepo.CheckOwnership(convID, userID) { - return errors.New("对话不存在或无权访问") - } - return s.convRepo.Delete(convID) -} - -// SendMessage 发送消息并获取AI回复 -func (s *ConversationService) SendMessage(ctx context.Context, userID, convID uint, content string) (*MessageResponse, error) { - // 检查权限 - if !s.convRepo.CheckOwnership(convID, userID) { - return nil, errors.New("对话不存在或无权访问") - } - - // 保存用户消息 - userMsg := &model.Message{ - ConversationID: convID, - Role: model.RoleUser, - Content: content, - } - if err := s.convRepo.AddMessage(userMsg); err != nil { - return nil, errors.New("保存消息失败") - } - - // 构建对话上下文 - messages := s.buildMessages(userID, convID) - - // 调用 AI - response, err := s.aiClient.Chat(ctx, messages) - if err != nil { - return nil, err - } - - // 保存 AI 回复 - assistantMsg := &model.Message{ - ConversationID: convID, - Role: model.RoleAssistant, - Content: response, - } - if err := s.convRepo.AddMessage(assistantMsg); err != nil { - return nil, errors.New("保存AI回复失败") - } - - // 如果是第一条消息,更新对话标题 - messages_count, _ := s.convRepo.GetMessages(convID) - if len(messages_count) <= 2 { - // 使用用户消息的前20个字符作为标题 - title := content - if len(title) > 20 { - title = title[:20] + "..." - } - s.convRepo.UpdateTitle(convID, title) - } - - return &MessageResponse{ - ID: assistantMsg.ID, - Role: assistantMsg.Role, - Content: assistantMsg.Content, - CreatedAt: assistantMsg.CreatedAt, - }, nil -} - -// SendMessageStream 流式发送消息 -func (s *ConversationService) SendMessageStream(ctx context.Context, userID, convID uint, content string, writer io.Writer) error { - // 检查权限 - if !s.convRepo.CheckOwnership(convID, userID) { - return errors.New("对话不存在或无权访问") - } - - // 保存用户消息 - userMsg := &model.Message{ - ConversationID: convID, - Role: model.RoleUser, - Content: content, - } - if err := s.convRepo.AddMessage(userMsg); err != nil { - return errors.New("保存消息失败") - } - - // 构建对话上下文 - messages := s.buildMessages(userID, convID) - - // 调用 AI 流式接口 - return s.aiClient.ChatStream(ctx, messages, writer) -} - -// buildMessages 构建消息上下文 -func (s *ConversationService) buildMessages(userID, convID uint) []ai.Message { - messages := []ai.Message{} - - // 系统提示词 - systemPrompt := s.buildSystemPrompt(userID) - messages = append(messages, ai.Message{ - Role: "system", - Content: systemPrompt, - }) - - // 历史消息(限制数量避免超出 token 限制) - maxHistory := config.AppConfig.AI.MaxHistoryMessages - if maxHistory <= 0 { - maxHistory = 10 // 默认10条 - } - - historyMsgs, _ := s.convRepo.GetRecentMessages(convID, maxHistory) - for _, msg := range historyMsgs { - messages = append(messages, ai.Message{ - Role: msg.Role, - Content: msg.Content, - }) - } - - return messages -} - -// 系统提示词模板 -const systemPromptTemplate = `# 角色定义 -你是"健康AI助手",一个专业的健康咨询助理。你基于中医体质辨识理论,为用户提供个性化的健康建议。 - -## 重要声明 -- 你不是专业医师,仅提供健康咨询和养生建议 -- 你的建议不能替代医生的诊断和治疗 -- 遇到以下情况,必须立即建议用户就医: - * 胸痛、呼吸困难、剧烈头痛 - * 高烧不退(超过39°C持续24小时) - * 意识模糊、晕厥 - * 严重外伤、大量出血 - * 持续剧烈腹痛 - * 疑似中风症状(口眼歪斜、肢体无力、言语不清) - -## 用户信息 -%s - -## 用户体质 -%s - -## 回答原则 -1. 回答控制在200字以内,简洁明了 -2. 根据用户体质给出针对性建议 -3. 用药建议优先推荐非处方中成药或食疗,注明"建议咨询药师" -4. 不推荐处方药,不做疾病诊断 -5. 对于紧急情况,立即建议用户就医 - -## 回答格式 -【情况分析】一句话概括 -【建议】 -1. 具体建议1 -2. 具体建议2 -【用药参考】(如适用) -- 药品名称:用法(建议咨询药师) -【提醒】注意事项或就医建议` - -// buildSystemPrompt 构建系统提示词(包含用户体质信息) -func (s *ConversationService) buildSystemPrompt(userID uint) string { - var userProfile, constitutionInfo string - - // 获取用户基本信息 - profile, err := s.healthRepo.GetProfileByUserID(userID) - if err == nil && profile.ID > 0 { - age := calculateAge(profile.BirthDate) - gender := "未知" - if profile.Gender == "male" { - gender = "男" - } else if profile.Gender == "female" { - gender = "女" - } - userProfile = fmt.Sprintf("性别:%s,年龄:%d岁,BMI:%.1f", gender, age, profile.BMI) - } else { - userProfile = "暂无基本信息" - } - - // 获取用户体质信息 - constitution, err := s.constitutionRepo.GetLatestAssessment(userID) - if err == nil && constitution.ID > 0 { - constitutionName := model.ConstitutionNames[constitution.PrimaryConstitution] - description := model.ConstitutionDescriptions[constitution.PrimaryConstitution] - constitutionInfo = fmt.Sprintf("主体质:%s\n特征:%s", constitutionName, description) - } else { - constitutionInfo = "暂未进行体质测评" - } - - return fmt.Sprintf(systemPromptTemplate, userProfile, constitutionInfo) -} - -// 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 -} diff --git a/server/internal/service/health.go b/server/internal/service/health.go deleted file mode 100644 index 612b5ec..0000000 --- a/server/internal/service/health.go +++ /dev/null @@ -1,289 +0,0 @@ -package service - -import ( - "errors" - "time" - - "health-ai/internal/model" - "health-ai/internal/repository/impl" -) - -type HealthService struct { - healthRepo *impl.HealthRepository - constitutionRepo *impl.ConstitutionRepository - userRepo *impl.UserRepositoryImpl -} - -func NewHealthService() *HealthService { - return &HealthService{ - healthRepo: impl.NewHealthRepository(), - constitutionRepo: impl.NewConstitutionRepository(), - userRepo: impl.NewUserRepository(), - } -} - -// ================= 响应结构体 ================= - -// HealthProfileResponse 健康档案响应 -type HealthProfileResponse struct { - Profile *model.HealthProfile `json:"profile"` - Lifestyle *model.LifestyleInfo `json:"lifestyle"` - MedicalHistory []model.MedicalHistory `json:"medical_history"` - FamilyHistory []model.FamilyHistory `json:"family_history"` - AllergyRecords []model.AllergyRecord `json:"allergy_records"` - Constitution *ConstitutionScore `json:"constitution,omitempty"` -} - -// ================= Service 方法 ================= - -// GetHealthProfile 获取完整健康档案 -func (s *HealthService) GetHealthProfile(userID uint) (*HealthProfileResponse, error) { - resp := &HealthProfileResponse{} - - // 获取基础档案 - profile, err := s.healthRepo.GetProfileByUserID(userID) - if err == nil && profile.ID > 0 { - resp.Profile = profile - } - - // 获取生活习惯 - lifestyle, err := s.healthRepo.GetLifestyleByUserID(userID) - if err == nil && lifestyle.ID > 0 { - resp.Lifestyle = lifestyle - } - - // 获取病史 - if profile != nil && profile.ID > 0 { - resp.MedicalHistory, _ = s.healthRepo.GetMedicalHistories(profile.ID) - resp.FamilyHistory, _ = s.healthRepo.GetFamilyHistories(profile.ID) - resp.AllergyRecords, _ = s.healthRepo.GetAllergyRecords(profile.ID) - } - - // 获取最新体质信息 - constitution, err := s.constitutionRepo.GetLatestAssessment(userID) - if err == nil && constitution.ID > 0 { - resp.Constitution = &ConstitutionScore{ - Type: constitution.PrimaryConstitution, - Name: model.ConstitutionNames[constitution.PrimaryConstitution], - Description: model.ConstitutionDescriptions[constitution.PrimaryConstitution], - } - } - - return resp, nil -} - -// GetBasicProfile 获取基础档案 -func (s *HealthService) GetBasicProfile(userID uint) (*model.HealthProfile, error) { - profile, err := s.healthRepo.GetProfileByUserID(userID) - if err != nil { - return nil, errors.New("健康档案不存在") - } - return profile, nil -} - -// GetLifestyle 获取生活习惯 -func (s *HealthService) GetLifestyle(userID uint) (*model.LifestyleInfo, error) { - lifestyle, err := s.healthRepo.GetLifestyleByUserID(userID) - if err != nil { - return nil, errors.New("生活习惯信息不存在") - } - return lifestyle, nil -} - -// GetMedicalHistory 获取病史列表 -func (s *HealthService) GetMedicalHistory(userID uint) ([]model.MedicalHistory, error) { - profile, err := s.healthRepo.GetProfileByUserID(userID) - if err != nil { - return nil, errors.New("请先填写基础信息") - } - return s.healthRepo.GetMedicalHistories(profile.ID) -} - -// GetFamilyHistory 获取家族病史列表 -func (s *HealthService) GetFamilyHistory(userID uint) ([]model.FamilyHistory, error) { - profile, err := s.healthRepo.GetProfileByUserID(userID) - if err != nil { - return nil, errors.New("请先填写基础信息") - } - return s.healthRepo.GetFamilyHistories(profile.ID) -} - -// GetAllergyRecords 获取过敏记录列表 -func (s *HealthService) GetAllergyRecords(userID uint) ([]model.AllergyRecord, error) { - profile, err := s.healthRepo.GetProfileByUserID(userID) - if err != nil { - return nil, errors.New("请先填写基础信息") - } - return s.healthRepo.GetAllergyRecords(profile.ID) -} - -// DeleteMedicalHistory 删除病史记录 -func (s *HealthService) DeleteMedicalHistory(userID, historyID uint) error { - // TODO: 验证记录属于该用户 - return s.healthRepo.DeleteMedicalHistory(historyID) -} - -// DeleteFamilyHistory 删除家族病史记录 -func (s *HealthService) DeleteFamilyHistory(userID, historyID uint) error { - // TODO: 验证记录属于该用户 - return s.healthRepo.DeleteFamilyHistory(historyID) -} - -// DeleteAllergyRecord 删除过敏记录 -func (s *HealthService) DeleteAllergyRecord(userID, recordID uint) error { - // TODO: 验证记录属于该用户 - return s.healthRepo.DeleteAllergyRecord(recordID) -} - -// ================= 更新请求结构体 ================= - -// UpdateHealthProfileInput 更新健康档案输入 -type UpdateHealthProfileInput struct { - Name string - BirthDate string - Gender string - Height float64 - Weight float64 - BloodType string - Occupation string - MaritalStatus string - Region string -} - -// UpdateLifestyleInput 更新生活习惯输入 -type UpdateLifestyleInput struct { - SleepTime string - WakeTime string - SleepQuality string - MealRegularity string - DietPreference string - DailyWaterML int - ExerciseFrequency string - ExerciseType string - ExerciseDurationMin int - IsSmoker *bool // 使用指针以区分未设置和false - AlcoholFrequency string -} - -// UpdateHealthProfile 更新健康档案 -func (s *HealthService) UpdateHealthProfile(userID uint, input *UpdateHealthProfileInput) (*model.HealthProfile, error) { - // 获取或创建健康档案 - profile, err := s.healthRepo.GetProfileByUserID(userID) - if err != nil { - // 创建新档案 - profile = &model.HealthProfile{ - UserID: userID, - } - } - - // 更新字段(只更新非空值) - if input.Name != "" { - profile.Name = input.Name - } - if input.BirthDate != "" { - // 解析日期字符串 - parsedDate, err := time.Parse("2006-01-02", input.BirthDate) - if err == nil { - profile.BirthDate = &parsedDate - } - } - if input.Gender != "" { - profile.Gender = input.Gender - } - if input.Height > 0 { - profile.Height = input.Height - } - if input.Weight > 0 { - profile.Weight = input.Weight - // 计算BMI - if profile.Height > 0 { - heightM := profile.Height / 100 - profile.BMI = profile.Weight / (heightM * heightM) - } - } - if input.BloodType != "" { - profile.BloodType = input.BloodType - } - if input.Occupation != "" { - profile.Occupation = input.Occupation - } - if input.MaritalStatus != "" { - profile.MaritalStatus = input.MaritalStatus - } - if input.Region != "" { - profile.Region = input.Region - } - - // 保存或更新 - if profile.ID == 0 { - if err := s.healthRepo.CreateProfile(profile); err != nil { - return nil, err - } - } else { - if err := s.healthRepo.UpdateProfile(profile); err != nil { - return nil, err - } - } - - return profile, nil -} - -// UpdateLifestyle 更新生活习惯 -func (s *HealthService) UpdateLifestyle(userID uint, input *UpdateLifestyleInput) (*model.LifestyleInfo, error) { - // 获取或创建生活习惯记录 - lifestyle, err := s.healthRepo.GetLifestyleByUserID(userID) - if err != nil { - // 创建新记录 - lifestyle = &model.LifestyleInfo{ - UserID: userID, - } - } - - // 更新字段(只更新非空值) - if input.SleepTime != "" { - lifestyle.SleepTime = input.SleepTime - } - if input.WakeTime != "" { - lifestyle.WakeTime = input.WakeTime - } - if input.SleepQuality != "" { - lifestyle.SleepQuality = input.SleepQuality - } - if input.MealRegularity != "" { - lifestyle.MealRegularity = input.MealRegularity - } - if input.DietPreference != "" { - lifestyle.DietPreference = input.DietPreference - } - if input.DailyWaterML > 0 { - lifestyle.DailyWaterML = input.DailyWaterML - } - if input.ExerciseFrequency != "" { - lifestyle.ExerciseFrequency = input.ExerciseFrequency - } - if input.ExerciseType != "" { - lifestyle.ExerciseType = input.ExerciseType - } - if input.ExerciseDurationMin > 0 { - lifestyle.ExerciseDurationMin = input.ExerciseDurationMin - } - if input.IsSmoker != nil { - lifestyle.IsSmoker = *input.IsSmoker - } - if input.AlcoholFrequency != "" { - lifestyle.AlcoholFrequency = input.AlcoholFrequency - } - - // 保存或更新 - if lifestyle.ID == 0 { - if err := s.healthRepo.CreateLifestyle(lifestyle); err != nil { - return nil, err - } - } else { - if err := s.healthRepo.UpdateLifestyle(lifestyle); err != nil { - return nil, err - } - } - - return lifestyle, nil -} diff --git a/server/internal/service/product.go b/server/internal/service/product.go deleted file mode 100644 index 6097320..0000000 --- a/server/internal/service/product.go +++ /dev/null @@ -1,143 +0,0 @@ -package service - -import ( - "errors" - "time" - - "health-ai/internal/model" - "health-ai/internal/repository/impl" -) - -type ProductService struct { - productRepo *impl.ProductRepository - constitutionRepo *impl.ConstitutionRepository -} - -func NewProductService() *ProductService { - return &ProductService{ - productRepo: impl.NewProductRepository(), - constitutionRepo: impl.NewConstitutionRepository(), - } -} - -// ================= 请求/响应结构体 ================= - -// ProductListResponse 产品列表响应 -type ProductListResponse struct { - Products []model.Product `json:"products"` - Total int64 `json:"total"` - Page int `json:"page"` - PageSize int `json:"page_size"` -} - -// ProductRecommendResponse 产品推荐响应 -type ProductRecommendResponse struct { - ConstitutionType string `json:"constitution_type"` - ConstitutionName string `json:"constitution_name"` - Products []model.Product `json:"products"` -} - -// PurchaseSyncRequest 购买同步请求 -type PurchaseSyncRequest struct { - UserID uint `json:"user_id" binding:"required"` - OrderNo string `json:"order_no" binding:"required"` - Products []struct { - ID uint `json:"id" binding:"required"` - Name string `json:"name" binding:"required"` - } `json:"products" binding:"required"` - CreatedAt time.Time `json:"created_at"` -} - -// ================= Service 方法 ================= - -// GetProducts 获取产品列表 -func (s *ProductService) GetProducts(page, pageSize int) (*ProductListResponse, error) { - if page <= 0 { - page = 1 - } - if pageSize <= 0 { - pageSize = 20 - } - - products, total, err := s.productRepo.GetAll(page, pageSize) - if err != nil { - return nil, err - } - - return &ProductListResponse{ - Products: products, - Total: total, - Page: page, - PageSize: pageSize, - }, nil -} - -// GetProductByID 获取产品详情 -func (s *ProductService) GetProductByID(id uint) (*model.Product, error) { - product, err := s.productRepo.GetByID(id) - if err != nil { - return nil, errors.New("产品不存在") - } - return product, nil -} - -// GetProductsByCategory 按分类获取产品 -func (s *ProductService) GetProductsByCategory(category string) ([]model.Product, error) { - return s.productRepo.GetByCategory(category) -} - -// GetRecommendedProducts 根据用户体质获取推荐产品 -func (s *ProductService) GetRecommendedProducts(userID uint) (*ProductRecommendResponse, error) { - // 获取用户最新体质 - assessment, err := s.constitutionRepo.GetLatestAssessment(userID) - if err != nil { - return nil, errors.New("请先完成体质测评") - } - - // 获取该体质的推荐产品 - products, err := s.productRepo.GetByConstitution(assessment.PrimaryConstitution) - if err != nil { - return nil, err - } - - return &ProductRecommendResponse{ - ConstitutionType: assessment.PrimaryConstitution, - ConstitutionName: model.ConstitutionNames[assessment.PrimaryConstitution], - Products: products, - }, nil -} - -// SearchProducts 根据关键词搜索产品 -func (s *ProductService) SearchProducts(keyword string) ([]model.Product, error) { - if keyword == "" { - return nil, errors.New("请输入搜索关键词") - } - return s.productRepo.SearchByKeyword(keyword) -} - -// SyncPurchase 同步商城购买记录 -func (s *ProductService) SyncPurchase(req *PurchaseSyncRequest) error { - purchasedAt := req.CreatedAt - if purchasedAt.IsZero() { - purchasedAt = time.Now() - } - - histories := make([]model.PurchaseHistory, len(req.Products)) - for i, p := range req.Products { - histories[i] = model.PurchaseHistory{ - UserID: req.UserID, - OrderNo: req.OrderNo, - ProductID: p.ID, - ProductName: p.Name, - PurchasedAt: purchasedAt, - Source: "mall", - } - } - - return s.productRepo.BatchCreatePurchaseHistory(histories) -} - -// GetPurchaseHistory 获取用户购买历史 -func (s *ProductService) GetPurchaseHistory(userID uint) ([]model.PurchaseHistory, error) { - return s.productRepo.GetPurchaseHistory(userID) -} diff --git a/server/internal/service/survey.go b/server/internal/service/survey.go deleted file mode 100644 index 86e9d11..0000000 --- a/server/internal/service/survey.go +++ /dev/null @@ -1,368 +0,0 @@ -package service - -import ( - "errors" - "time" - - "health-ai/internal/model" - "health-ai/internal/repository/impl" -) - -type SurveyService struct { - healthRepo *impl.HealthRepository - userRepo *impl.UserRepositoryImpl -} - -func NewSurveyService() *SurveyService { - return &SurveyService{ - healthRepo: impl.NewHealthRepository(), - userRepo: impl.NewUserRepository(), - } -} - -// ================= 请求结构体 ================= - -// BasicInfoRequest 基础信息请求 -type BasicInfoRequest struct { - Name string `json:"name" binding:"required"` - BirthDate string `json:"birth_date"` // 格式: 2006-01-02 - Gender string `json:"gender" binding:"required,oneof=male female"` - Height float64 `json:"height" binding:"required,gt=0"` - Weight float64 `json:"weight" binding:"required,gt=0"` - BloodType string `json:"blood_type"` - Occupation string `json:"occupation"` - MaritalStatus string `json:"marital_status"` - Region string `json:"region"` -} - -// LifestyleRequest 生活习惯请求 -type LifestyleRequest struct { - SleepTime string `json:"sleep_time"` // 格式: HH:MM - WakeTime string `json:"wake_time"` // 格式: HH:MM - SleepQuality string `json:"sleep_quality"` // good, normal, poor - MealRegularity string `json:"meal_regularity"` // regular, irregular - DietPreference string `json:"diet_preference"` - DailyWaterML int `json:"daily_water_ml"` - ExerciseFrequency string `json:"exercise_frequency"` // never, sometimes, often, daily - ExerciseType string `json:"exercise_type"` - ExerciseDurationMin int `json:"exercise_duration_min"` - IsSmoker bool `json:"is_smoker"` - AlcoholFrequency string `json:"alcohol_frequency"` // never, sometimes, often -} - -// MedicalHistoryRequest 病史请求 -type MedicalHistoryRequest struct { - DiseaseName string `json:"disease_name" binding:"required"` - DiseaseType string `json:"disease_type"` // chronic, surgery, other - DiagnosedDate string `json:"diagnosed_date"` - Status string `json:"status"` // cured, treating, controlled - Notes string `json:"notes"` -} - -// FamilyHistoryRequest 家族病史请求 -type FamilyHistoryRequest struct { - Relation string `json:"relation" binding:"required"` // father, mother, grandparent - DiseaseName string `json:"disease_name" binding:"required"` - Notes string `json:"notes"` -} - -// AllergyRequest 过敏信息请求 -type AllergyRequest struct { - AllergyType string `json:"allergy_type" binding:"required"` // drug, food, other - Allergen string `json:"allergen" binding:"required"` - Severity string `json:"severity"` // mild, moderate, severe - ReactionDesc string `json:"reaction_desc"` -} - -// BatchMedicalHistoryRequest 批量病史请求 -type BatchMedicalHistoryRequest struct { - Histories []MedicalHistoryRequest `json:"histories"` -} - -// BatchFamilyHistoryRequest 批量家族病史请求 -type BatchFamilyHistoryRequest struct { - Histories []FamilyHistoryRequest `json:"histories"` -} - -// BatchAllergyRequest 批量过敏信息请求 -type BatchAllergyRequest struct { - Allergies []AllergyRequest `json:"allergies"` -} - -// ================= 响应结构体 ================= - -// SurveyStatusResponse 调查状态响应 -type SurveyStatusResponse struct { - BasicInfo bool `json:"basic_info"` - Lifestyle bool `json:"lifestyle"` - MedicalHistory bool `json:"medical_history"` - FamilyHistory bool `json:"family_history"` - Allergy bool `json:"allergy"` - AllCompleted bool `json:"all_completed"` -} - -// ================= Service 方法 ================= - -// GetStatus 获取调查完成状态 -func (s *SurveyService) GetStatus(userID uint) (*SurveyStatusResponse, error) { - status := &SurveyStatusResponse{ - BasicInfo: false, - Lifestyle: false, - MedicalHistory: false, - FamilyHistory: false, - Allergy: false, - AllCompleted: false, - } - - // 检查基础信息 - profile, err := s.healthRepo.GetProfileByUserID(userID) - if err == nil && profile.ID > 0 { - status.BasicInfo = true - // 病史、家族史、过敏史可以为空,只要有profile就算完成 - status.MedicalHistory = true - status.FamilyHistory = true - status.Allergy = true - } - - // 检查生活习惯 - lifestyle, err := s.healthRepo.GetLifestyleByUserID(userID) - if err == nil && lifestyle.ID > 0 { - status.Lifestyle = true - } - - // 检查是否全部完成 - status.AllCompleted = status.BasicInfo && status.Lifestyle - - return status, nil -} - -// SubmitBasicInfo 提交基础信息 -func (s *SurveyService) SubmitBasicInfo(userID uint, req *BasicInfoRequest) error { - // 计算 BMI - heightM := req.Height / 100 - bmi := req.Weight / (heightM * heightM) - - profile := &model.HealthProfile{ - UserID: userID, - Name: req.Name, - Gender: req.Gender, - Height: req.Height, - Weight: req.Weight, - BMI: bmi, - BloodType: req.BloodType, - Occupation: req.Occupation, - MaritalStatus: req.MaritalStatus, - Region: req.Region, - } - - // 解析出生日期 - if req.BirthDate != "" { - birthDate, err := time.Parse("2006-01-02", req.BirthDate) - if err == nil { - profile.BirthDate = &birthDate - } - } - - // 检查是否已存在 - existing, _ := s.healthRepo.GetProfileByUserID(userID) - if existing.ID > 0 { - profile.ID = existing.ID - profile.CreatedAt = existing.CreatedAt - return s.healthRepo.UpdateProfile(profile) - } - - return s.healthRepo.CreateProfile(profile) -} - -// SubmitLifestyle 提交生活习惯 -func (s *SurveyService) SubmitLifestyle(userID uint, req *LifestyleRequest) error { - lifestyle := &model.LifestyleInfo{ - UserID: userID, - SleepTime: req.SleepTime, - WakeTime: req.WakeTime, - SleepQuality: req.SleepQuality, - MealRegularity: req.MealRegularity, - DietPreference: req.DietPreference, - DailyWaterML: req.DailyWaterML, - ExerciseFrequency: req.ExerciseFrequency, - ExerciseType: req.ExerciseType, - ExerciseDurationMin: req.ExerciseDurationMin, - IsSmoker: req.IsSmoker, - AlcoholFrequency: req.AlcoholFrequency, - } - - existing, _ := s.healthRepo.GetLifestyleByUserID(userID) - if existing.ID > 0 { - lifestyle.ID = existing.ID - lifestyle.CreatedAt = existing.CreatedAt - return s.healthRepo.UpdateLifestyle(lifestyle) - } - - return s.healthRepo.CreateLifestyle(lifestyle) -} - -// SubmitMedicalHistory 提交单条病史 -func (s *SurveyService) SubmitMedicalHistory(userID uint, req *MedicalHistoryRequest) error { - profile, err := s.healthRepo.GetProfileByUserID(userID) - if err != nil { - return errors.New("请先填写基础信息") - } - - history := &model.MedicalHistory{ - HealthProfileID: profile.ID, - DiseaseName: req.DiseaseName, - DiseaseType: req.DiseaseType, - DiagnosedDate: req.DiagnosedDate, - Status: req.Status, - Notes: req.Notes, - } - - return s.healthRepo.CreateMedicalHistory(history) -} - -// SubmitBatchMedicalHistory 批量提交病史(覆盖式) -func (s *SurveyService) SubmitBatchMedicalHistory(userID uint, req *BatchMedicalHistoryRequest) error { - profile, err := s.healthRepo.GetProfileByUserID(userID) - if err != nil { - return errors.New("请先填写基础信息") - } - - // 清除旧数据 - if err := s.healthRepo.ClearMedicalHistories(profile.ID); err != nil { - return err - } - - // 创建新数据 - if len(req.Histories) == 0 { - return nil - } - - histories := make([]model.MedicalHistory, len(req.Histories)) - for i, h := range req.Histories { - histories[i] = model.MedicalHistory{ - HealthProfileID: profile.ID, - DiseaseName: h.DiseaseName, - DiseaseType: h.DiseaseType, - DiagnosedDate: h.DiagnosedDate, - Status: h.Status, - Notes: h.Notes, - } - } - - return s.healthRepo.BatchCreateMedicalHistories(histories) -} - -// SubmitFamilyHistory 提交单条家族病史 -func (s *SurveyService) SubmitFamilyHistory(userID uint, req *FamilyHistoryRequest) error { - profile, err := s.healthRepo.GetProfileByUserID(userID) - if err != nil { - return errors.New("请先填写基础信息") - } - - history := &model.FamilyHistory{ - HealthProfileID: profile.ID, - Relation: req.Relation, - DiseaseName: req.DiseaseName, - Notes: req.Notes, - } - - return s.healthRepo.CreateFamilyHistory(history) -} - -// SubmitBatchFamilyHistory 批量提交家族病史(覆盖式) -func (s *SurveyService) SubmitBatchFamilyHistory(userID uint, req *BatchFamilyHistoryRequest) error { - profile, err := s.healthRepo.GetProfileByUserID(userID) - if err != nil { - return errors.New("请先填写基础信息") - } - - // 清除旧数据 - if err := s.healthRepo.ClearFamilyHistories(profile.ID); err != nil { - return err - } - - // 创建新数据 - if len(req.Histories) == 0 { - return nil - } - - histories := make([]model.FamilyHistory, len(req.Histories)) - for i, h := range req.Histories { - histories[i] = model.FamilyHistory{ - HealthProfileID: profile.ID, - Relation: h.Relation, - DiseaseName: h.DiseaseName, - Notes: h.Notes, - } - } - - return s.healthRepo.BatchCreateFamilyHistories(histories) -} - -// SubmitAllergy 提交单条过敏信息 -func (s *SurveyService) SubmitAllergy(userID uint, req *AllergyRequest) error { - profile, err := s.healthRepo.GetProfileByUserID(userID) - if err != nil { - return errors.New("请先填写基础信息") - } - - record := &model.AllergyRecord{ - HealthProfileID: profile.ID, - AllergyType: req.AllergyType, - Allergen: req.Allergen, - Severity: req.Severity, - ReactionDesc: req.ReactionDesc, - } - - return s.healthRepo.CreateAllergyRecord(record) -} - -// SubmitBatchAllergy 批量提交过敏信息(覆盖式) -func (s *SurveyService) SubmitBatchAllergy(userID uint, req *BatchAllergyRequest) error { - profile, err := s.healthRepo.GetProfileByUserID(userID) - if err != nil { - return errors.New("请先填写基础信息") - } - - // 清除旧数据 - if err := s.healthRepo.ClearAllergyRecords(profile.ID); err != nil { - return err - } - - // 创建新数据 - if len(req.Allergies) == 0 { - return nil - } - - records := make([]model.AllergyRecord, len(req.Allergies)) - for i, a := range req.Allergies { - records[i] = model.AllergyRecord{ - HealthProfileID: profile.ID, - AllergyType: a.AllergyType, - Allergen: a.Allergen, - Severity: a.Severity, - ReactionDesc: a.ReactionDesc, - } - } - - return s.healthRepo.BatchCreateAllergyRecords(records) -} - -// MarkSurveyCompleted 标记调查完成 -func (s *SurveyService) MarkSurveyCompleted(userID uint) error { - return s.userRepo.UpdateSurveyStatus(userID, true) -} - -// CompleteSurvey 完成调查(检查并标记) -func (s *SurveyService) CompleteSurvey(userID uint) error { - status, err := s.GetStatus(userID) - if err != nil { - return err - } - - if !status.BasicInfo || !status.Lifestyle { - return errors.New("请先完成基础信息和生活习惯的填写") - } - - return s.MarkSurveyCompleted(userID) -} diff --git a/server/pkg/response/response.go b/server/pkg/response/response.go deleted file mode 100644 index f787532..0000000 --- a/server/pkg/response/response.go +++ /dev/null @@ -1,80 +0,0 @@ -package response - -import ( - "net/http" - - "github.com/gin-gonic/gin" -) - -// Response 统一响应结构 -type Response struct { - Code int `json:"code"` - Message string `json:"message"` - Data interface{} `json:"data,omitempty"` -} - -// Success 成功响应 -func Success(c *gin.Context, data interface{}) { - c.JSON(http.StatusOK, Response{ - Code: 0, - Message: "success", - Data: data, - }) -} - -// SuccessWithMessage 成功响应带消息 -func SuccessWithMessage(c *gin.Context, message string, data interface{}) { - c.JSON(http.StatusOK, Response{ - Code: 0, - Message: message, - Data: data, - }) -} - -// Error 错误响应 -func Error(c *gin.Context, code int, message string) { - c.JSON(http.StatusOK, Response{ - Code: code, - Message: message, - }) -} - -// BadRequest 参数错误 -func BadRequest(c *gin.Context, message string) { - c.JSON(http.StatusBadRequest, Response{ - Code: 400, - Message: message, - }) -} - -// Unauthorized 未授权 -func Unauthorized(c *gin.Context, message string) { - c.JSON(http.StatusUnauthorized, Response{ - Code: 401, - Message: message, - }) -} - -// Forbidden 禁止访问 -func Forbidden(c *gin.Context, message string) { - c.JSON(http.StatusForbidden, Response{ - Code: 403, - Message: message, - }) -} - -// NotFound 资源不存在 -func NotFound(c *gin.Context, message string) { - c.JSON(http.StatusNotFound, Response{ - Code: 404, - Message: message, - }) -} - -// ServerError 服务器错误 -func ServerError(c *gin.Context, message string) { - c.JSON(http.StatusInternalServerError, Response{ - Code: 500, - Message: message, - }) -}