healthapp
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

14 KiB

07-健康档案模块

目标

实现用户健康档案的查询和管理功能,提供完整的健康信息视图。


前置要求

  • 健康调查模块已完成
  • 体质辨识模块已完成

实施步骤

步骤 1:创建用户 Service

创建 server/internal/service/user.go

package service

import (
	"health-ai/internal/model"
	"health-ai/internal/repository/impl"
)

type UserService struct {
	userRepo         *impl.UserRepositoryImpl
	healthRepo       *impl.HealthRepository
	constitutionRepo *impl.ConstitutionRepository
}

func NewUserService() *UserService {
	return &UserService{
		userRepo:         impl.NewUserRepository(),
		healthRepo:       impl.NewHealthRepository(),
		constitutionRepo: impl.NewConstitutionRepository(),
	}
}

// 用户资料响应
type UserProfileResponse struct {
	ID              uint   `json:"id"`
	Phone           string `json:"phone"`
	Email           string `json:"email"`
	Nickname        string `json:"nickname"`
	Avatar          string `json:"avatar"`
	SurveyCompleted bool   `json:"survey_completed"`
}

// 获取用户资料
func (s *UserService) GetProfile(userID uint) (*UserProfileResponse, error) {
	user, err := s.userRepo.GetByID(userID)
	if err != nil {
		return nil, err
	}
	return &UserProfileResponse{
		ID:              user.ID,
		Phone:           user.Phone,
		Email:           user.Email,
		Nickname:        user.Nickname,
		Avatar:          user.Avatar,
		SurveyCompleted: user.SurveyCompleted,
	}, nil
}

// 更新用户资料请求
type UpdateProfileRequest struct {
	Nickname string `json:"nickname"`
	Avatar   string `json:"avatar"`
	Email    string `json:"email"`
}

// 更新用户资料
func (s *UserService) UpdateProfile(userID uint, req *UpdateProfileRequest) error {
	user, err := s.userRepo.GetByID(userID)
	if err != nil {
		return err
	}
	if req.Nickname != "" {
		user.Nickname = req.Nickname
	}
	if req.Avatar != "" {
		user.Avatar = req.Avatar
	}
	if req.Email != "" {
		user.Email = req.Email
	}
	return s.userRepo.Update(user)
}

// 完整健康档案响应
type FullHealthProfileResponse struct {
	BasicInfo       *model.HealthProfile          `json:"basic_info"`
	Lifestyle       *model.LifestyleInfo          `json:"lifestyle"`
	MedicalHistory  []model.MedicalHistory        `json:"medical_history"`
	FamilyHistory   []model.FamilyHistory         `json:"family_history"`
	AllergyRecords  []model.AllergyRecord         `json:"allergy_records"`
	Constitution    *ConstitutionSummary          `json:"constitution"`
}

type ConstitutionSummary struct {
	PrimaryType        string    `json:"primary_type"`
	PrimaryName        string    `json:"primary_name"`
	PrimaryDescription string    `json:"primary_description"`
	AssessedAt         string    `json:"assessed_at"`
}

// 获取完整健康档案
func (s *UserService) GetFullHealthProfile(userID uint) (*FullHealthProfileResponse, error) {
	response := &FullHealthProfileResponse{}

	// 基础信息
	profile, err := s.healthRepo.GetProfileByUserID(userID)
	if err == nil && profile.ID > 0 {
		response.BasicInfo = profile

		// 病史
		histories, _ := s.healthRepo.GetMedicalHistories(profile.ID)
		response.MedicalHistory = histories

		// 家族史
		familyHistories, _ := s.healthRepo.GetFamilyHistories(profile.ID)
		response.FamilyHistory = familyHistories

		// 过敏记录
		allergies, _ := s.healthRepo.GetAllergyRecords(profile.ID)
		response.AllergyRecords = allergies
	}

	// 生活习惯
	lifestyle, err := s.healthRepo.GetLifestyleByUserID(userID)
	if err == nil && lifestyle.ID > 0 {
		response.Lifestyle = lifestyle
	}

	// 体质信息
	constitution, err := s.constitutionRepo.GetLatestAssessment(userID)
	if err == nil && constitution.ID > 0 {
		response.Constitution = &ConstitutionSummary{
			PrimaryType:        constitution.PrimaryConstitution,
			PrimaryName:        model.ConstitutionNames[constitution.PrimaryConstitution],
			PrimaryDescription: model.ConstitutionDescriptions[constitution.PrimaryConstitution],
			AssessedAt:         constitution.AssessedAt.Format("2006-01-02"),
		}
	}

	return response, nil
}

// 更新健康档案基础信息
func (s *UserService) UpdateHealthProfile(userID uint, req *BasicInfoRequest) error {
	return NewSurveyService().SubmitBasicInfo(userID, req)
}

// 更新生活习惯
func (s *UserService) UpdateLifestyle(userID uint, req *LifestyleRequest) error {
	return NewSurveyService().SubmitLifestyle(userID, req)
}

步骤 2:创建用户 Handler

创建 server/internal/api/handler/user.go

package handler

import (
	"health-ai/internal/api/middleware"
	"health-ai/internal/service"
	"health-ai/pkg/response"

	"github.com/gin-gonic/gin"
)

type UserHandler struct {
	userService *service.UserService
}

func NewUserHandler() *UserHandler {
	return &UserHandler{
		userService: service.NewUserService(),
	}
}

// 获取用户资料
func (h *UserHandler) GetProfile(c *gin.Context) {
	userID := middleware.GetUserID(c)
	profile, err := h.userService.GetProfile(userID)
	if err != nil {
		response.Error(c, 500, err.Error())
		return
	}
	response.Success(c, profile)
}

// 更新用户资料
func (h *UserHandler) UpdateProfile(c *gin.Context) {
	userID := middleware.GetUserID(c)
	var req service.UpdateProfileRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.Error(c, 400, "参数错误: "+err.Error())
		return
	}

	if err := h.userService.UpdateProfile(userID, &req); err != nil {
		response.Error(c, 500, err.Error())
		return
	}
	response.Success(c, nil)
}

// 获取完整健康档案
func (h *UserHandler) GetHealthProfile(c *gin.Context) {
	userID := middleware.GetUserID(c)
	profile, err := h.userService.GetFullHealthProfile(userID)
	if err != nil {
		response.Error(c, 500, err.Error())
		return
	}
	response.Success(c, profile)
}

// 更新健康档案基础信息
func (h *UserHandler) UpdateHealthProfile(c *gin.Context) {
	userID := middleware.GetUserID(c)
	var req service.BasicInfoRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.Error(c, 400, "参数错误: "+err.Error())
		return
	}

	if err := h.userService.UpdateHealthProfile(userID, &req); err != nil {
		response.Error(c, 500, err.Error())
		return
	}
	response.Success(c, nil)
}

// 获取生活习惯
func (h *UserHandler) GetLifestyle(c *gin.Context) {
	userID := middleware.GetUserID(c)
	profile, err := h.userService.GetFullHealthProfile(userID)
	if err != nil {
		response.Error(c, 500, err.Error())
		return
	}
	response.Success(c, profile.Lifestyle)
}

// 更新生活习惯
func (h *UserHandler) UpdateLifestyle(c *gin.Context) {
	userID := middleware.GetUserID(c)
	var req service.LifestyleRequest
	if err := c.ShouldBindJSON(&req); err != nil {
		response.Error(c, 400, "参数错误: "+err.Error())
		return
	}

	if err := h.userService.UpdateLifestyle(userID, &req); err != nil {
		response.Error(c, 500, err.Error())
		return
	}
	response.Success(c, nil)
}

步骤 3:更新完整路由配置

更新 server/internal/api/router.go

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 路由组
	api := r.Group("/api")
	{
		// ========== 公开接口(无需登录)==========
		authHandler := handler.NewAuthHandler()
		auth := api.Group("/auth")
		{
			auth.POST("/register", authHandler.Register)
			auth.POST("/login", authHandler.Login)
		}

		// ========== 需要认证的接口 ==========
		authorized := api.Group("")
		authorized.Use(middleware.AuthRequired())
		{
			// 用户接口
			userHandler := handler.NewUserHandler()
			user := authorized.Group("/user")
			{
				user.GET("/profile", userHandler.GetProfile)
				user.PUT("/profile", userHandler.UpdateProfile)
				user.GET("/health-profile", userHandler.GetHealthProfile)
				user.PUT("/health-profile", userHandler.UpdateHealthProfile)
				user.GET("/lifestyle", userHandler.GetLifestyle)
				user.PUT("/lifestyle", userHandler.UpdateLifestyle)
			}

			// 健康调查接口
			surveyHandler := handler.NewSurveyHandler()
			survey := authorized.Group("/survey")
			{
				survey.GET("/status", surveyHandler.GetStatus)
				survey.POST("/basic-info", surveyHandler.SubmitBasicInfo)
				survey.POST("/lifestyle", surveyHandler.SubmitLifestyle)
				survey.POST("/medical-history", surveyHandler.SubmitMedicalHistory)
				survey.POST("/family-history", surveyHandler.SubmitFamilyHistory)
				survey.POST("/allergy", surveyHandler.SubmitAllergy)
			}

			// 体质辨识接口
			constitutionHandler := handler.NewConstitutionHandler()
			constitution := authorized.Group("/constitution")
			{
				constitution.GET("/questions", constitutionHandler.GetQuestions)
				constitution.POST("/submit", constitutionHandler.Submit)
				constitution.GET("/result", constitutionHandler.GetResult)
				constitution.GET("/history", constitutionHandler.GetHistory)
				constitution.GET("/recommendations", constitutionHandler.GetRecommendations)
			}

			// 对话接口
			convHandler := handler.NewConversationHandler()
			conversations := authorized.Group("/conversations")
			{
				conversations.GET("", convHandler.GetConversations)
				conversations.POST("", convHandler.CreateConversation)
				conversations.GET("/:id", convHandler.GetConversation)
				conversations.DELETE("/:id", convHandler.DeleteConversation)
				conversations.POST("/:id/messages", convHandler.SendMessage)
			}
		}
	}

	return r
}

步骤 4:更新主程序完整版

更新 server/cmd/server/main.go

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() {
	// 加载配置
	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)
	}
	log.Println("✓ Question bank seeded")

	// 初始化 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)
	log.Println("========================================")
	log.Println("Health AI Server Ready!")
	log.Println("========================================")

	if err := router.Run(addr); err != nil {
		log.Fatalf("Failed to start server: %v", err)
	}
}

API 接口说明

GET /api/user/profile

获取用户基本资料

PUT /api/user/profile

更新用户基本资料(昵称、头像、邮箱)

GET /api/user/health-profile

获取完整健康档案(含基础信息、生活习惯、病史、体质)

PUT /api/user/health-profile

更新健康档案基础信息

GET /api/user/lifestyle

获取生活习惯信息

PUT /api/user/lifestyle

更新生活习惯信息


需要创建的文件清单

文件路径 说明
internal/service/user.go 用户 Service
internal/api/handler/user.go 用户 Handler
更新 internal/api/router.go 完整路由配置
更新 cmd/server/main.go 完整主程序

完整 API 列表汇总

方法 路径 说明 认证
GET /health 健康检查
POST /api/auth/register 用户注册
POST /api/auth/login 用户登录
GET /api/user/profile 获取用户资料
PUT /api/user/profile 更新用户资料
GET /api/user/health-profile 获取健康档案
PUT /api/user/health-profile 更新健康档案
GET /api/user/lifestyle 获取生活习惯
PUT /api/user/lifestyle 更新生活习惯
GET /api/survey/status 获取调查状态
POST /api/survey/basic-info 提交基础信息
POST /api/survey/lifestyle 提交生活习惯
POST /api/survey/medical-history 提交病史
POST /api/survey/family-history 提交家族史
POST /api/survey/allergy 提交过敏信息
GET /api/constitution/questions 获取问卷题目
POST /api/constitution/submit 提交问卷
GET /api/constitution/result 获取体质结果
GET /api/constitution/history 获取测评历史
GET /api/conversations 获取对话列表
POST /api/conversations 创建对话
GET /api/conversations/:id 获取对话详情
DELETE /api/conversations/:id 删除对话
POST /api/conversations/:id/messages 发送消息

验收标准

  • 获取用户资料正常
  • 获取完整健康档案正常
  • 更新资料和生活习惯正常
  • 所有 API 接口可正常调用
  • 服务器启动日志完整

预计耗时

30-40 分钟


下一步

后端开发完成!进入 03-Web前端开发/01-项目结构初始化.md