# 05-体质辨识模块 ## 目标 实现中医体质辨识问卷功能,包括问卷题库、答案提交、体质计算和调养建议生成。 --- ## 前置要求 - 健康调查模块已完成 - 数据模型已定义 --- ## 实施步骤 ### 步骤 1:创建体质常量定义 创建 `server/internal/model/constitution_const.go`: ```go package model // 九种体质类型 const ( ConstitutionPinghe = "pinghe" // 平和质 ConstitutionQixu = "qixu" // 气虚质 ConstitutionYangxu = "yangxu" // 阳虚质 ConstitutionYinxu = "yinxu" // 阴虚质 ConstitutionTanshi = "tanshi" // 痰湿质 ConstitutionShire = "shire" // 湿热质 ConstitutionXueyu = "xueyu" // 血瘀质 ConstitutionQiyu = "qiyu" // 气郁质 ConstitutionTebing = "tebing" // 特禀质 ) // 体质名称映射 var ConstitutionNames = map[string]string{ ConstitutionPinghe: "平和质", ConstitutionQixu: "气虚质", ConstitutionYangxu: "阳虚质", ConstitutionYinxu: "阴虚质", ConstitutionTanshi: "痰湿质", ConstitutionShire: "湿热质", ConstitutionXueyu: "血瘀质", ConstitutionQiyu: "气郁质", ConstitutionTebing: "特禀质", } // 体质特征描述 var ConstitutionDescriptions = map[string]string{ ConstitutionPinghe: "阴阳气血调和,体态适中,面色红润,精力充沛", ConstitutionQixu: "元气不足,容易疲劳,气短懒言,易出汗", ConstitutionYangxu: "阳气不足,畏寒怕冷,手脚冰凉,喜热饮", ConstitutionYinxu: "阴液亏少,口燥咽干,手足心热,盗汗", ConstitutionTanshi: "痰湿凝聚,形体肥胖,腹部肥满,痰多", ConstitutionShire: "湿热内蕴,面垢油光,口苦口干,大便黏滞", ConstitutionXueyu: "血行不畅,肤色晦暗,易生斑点,健忘", ConstitutionQiyu: "气机郁滞,情绪低落,多愁善感,胸闷", ConstitutionTebing: "先天失常,过敏体质,易打喷嚏,皮肤易过敏", } // 体质调养建议 var ConstitutionRecommendations = map[string]map[string]string{ ConstitutionPinghe: { "diet": "饮食均衡,不偏食,粗细搭配", "lifestyle": "起居有常,劳逸结合", "exercise": "可进行各种运动,量力而行", "emotion": "保持乐观积极的心态", }, ConstitutionQixu: { "diet": "宜食益气健脾食物,如山药、大枣、小米", "lifestyle": "避免劳累,保证充足睡眠", "exercise": "宜柔和运动,如太极拳、散步", "emotion": "避免过度思虑", }, ConstitutionYangxu: { "diet": "宜食温阳食物,如羊肉、韭菜、生姜", "lifestyle": "注意保暖,避免受寒", "exercise": "宜温和运动,避免大汗", "emotion": "保持积极乐观", }, ConstitutionYinxu: { "diet": "宜食滋阴食物,如百合、银耳、枸杞", "lifestyle": "避免熬夜,保持环境湿润", "exercise": "宜静养,避免剧烈运动", "emotion": "避免急躁易怒", }, ConstitutionTanshi: { "diet": "饮食清淡,少食肥甘厚味,宜食薏米、冬瓜", "lifestyle": "居住环境宜干燥通风", "exercise": "坚持运动,促进代谢", "emotion": "保持心情舒畅", }, ConstitutionShire: { "diet": "饮食清淡,宜食苦瓜、绿豆、薏米", "lifestyle": "避免湿热环境,保持皮肤清洁", "exercise": "适当运动,出汗排湿", "emotion": "保持平和心态", }, ConstitutionXueyu: { "diet": "宜食活血化瘀食物,如山楂、黑木耳", "lifestyle": "避免久坐,适当活动", "exercise": "坚持有氧运动,促进血液循环", "emotion": "保持心情愉快", }, ConstitutionQiyu: { "diet": "宜食行气解郁食物,如玫瑰花、佛手", "lifestyle": "多参加社交活动", "exercise": "宜户外运动,舒展身心", "emotion": "学会疏导情绪,培养兴趣爱好", }, ConstitutionTebing: { "diet": "避免食用过敏食物,饮食清淡", "lifestyle": "避免接触过敏原,保持环境清洁", "exercise": "适度运动,增强体质", "emotion": "保持心态平和", }, } ``` ### 步骤 2:创建问卷题库初始化 创建 `server/internal/database/seed.go`: ```go package database import ( "encoding/json" "health-ai/internal/model" ) // 初始化问卷题库 func SeedQuestionBank() error { // 检查是否已有数据 var count int64 DB.Model(&model.QuestionBank{}).Count(&count) if count > 0 { return nil } questions := getQuestions() for _, q := range questions { if err := DB.Create(&q).Error; err != nil { return err } } 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}, } } ``` ### 步骤 3:创建体质 Repository 创建 `server/internal/repository/impl/constitution.go`: ```go package impl import ( "health-ai/internal/database" "health-ai/internal/model" ) type ConstitutionRepository struct{} func NewConstitutionRepository() *ConstitutionRepository { return &ConstitutionRepository{} } 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 } func (r *ConstitutionRepository) CreateAssessment(assessment *model.ConstitutionAssessment) error { return database.DB.Create(assessment).Error } 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 } func (r *ConstitutionRepository) GetAssessmentHistory(userID uint) ([]model.ConstitutionAssessment, error) { var assessments []model.ConstitutionAssessment err := database.DB.Where("user_id = ?", userID).Order("assessed_at DESC").Find(&assessments).Error return assessments, err } ``` ### 步骤 4:创建体质计算 Service 创建 `server/internal/service/constitution.go`: ```go package service import ( "encoding/json" "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(), } } type AnswerRequest struct { QuestionID uint `json:"question_id" binding:"required"` Score int `json:"score" binding:"required,min=1,max=5"` } type SubmitRequest struct { Answers []AnswerRequest `json:"answers" binding:"required"` } type ConstitutionScore struct { Type string `json:"type"` Name string `json:"name"` Score float64 `json:"score"` Description string `json:"description"` } type AssessmentResult struct { 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"` } func (s *ConstitutionService) GetQuestions() ([]model.QuestionBank, error) { return s.repo.GetQuestions() } func (s *ConstitutionService) SubmitAssessment(userID uint, req *SubmitRequest) (*AssessmentResult, error) { // 获取所有问题 questions, err := s.repo.GetQuestions() if err != nil { return nil, err } // 构建问题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 } } // 计算转化分 allScores := make([]ConstitutionScore, 0) for cType, rawScore := range typeScores { questionCount := typeQuestionCount[cType] if questionCount == 0 { continue } // 转化分 = (原始分 - 条目数) / (条目数 × 4) × 100 transformedScore := float64(rawScore-questionCount) / float64(questionCount*4) * 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 == "" && len(allScores) > 0 { for _, score := range allScores { if score.Type != model.ConstitutionPinghe { primary = score break } } } } // 获取调养建议 recommendations := make(map[string]map[string]string) 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 } return &AssessmentResult{ PrimaryConstitution: primary, SecondaryConstitutions: secondary, AllScores: allScores, Recommendations: recommendations, AssessedAt: assessment.AssessedAt, }, nil } func (s *ConstitutionService) GetLatestResult(userID uint) (*AssessmentResult, error) { assessment, err := s.repo.GetLatestAssessment(userID) if err != nil { return nil, err } 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{ PrimaryConstitution: primary, SecondaryConstitutions: secondary, AllScores: allScores, Recommendations: recommendations, AssessedAt: assessment.AssessedAt, }, nil } func (s *ConstitutionService) GetHistory(userID uint) ([]model.ConstitutionAssessment, error) { return s.repo.GetAssessmentHistory(userID) } ``` ### 步骤 5:创建体质 Handler 和更新路由 创建 Handler 并在 router.go 中注册路由。 --- ## API 接口说明 ### GET /api/constitution/questions 获取体质问卷题目 ### POST /api/constitution/submit 提交问卷答案,返回体质辨识结果 ### GET /api/constitution/result 获取最新体质辨识结果 ### GET /api/constitution/history 获取体质测评历史 --- ## 需要创建的文件清单 | 文件路径 | 说明 | |----------|------| | `internal/model/constitution_const.go` | 体质常量定义 | | `internal/database/seed.go` | 问卷题库初始化 | | `internal/repository/impl/constitution.go` | 体质 Repository | | `internal/service/constitution.go` | 体质计算 Service | | `internal/api/handler/constitution.go` | 体质 Handler | --- ## 验收标准 - [ ] 问卷题库自动初始化(67题) - [ ] 获取问卷接口返回所有题目 - [ ] 提交答案后正确计算体质得分 - [ ] 体质判定逻辑正确(平和质特殊判定) - [ ] 调养建议正确返回 --- ## 预计耗时 40-50 分钟 --- ## 下一步 完成后进入 `02-后端开发/06-AI对话模块.md`