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.
22 KiB
22 KiB
05-体质辨识模块
目标
实现中医体质辨识问卷功能,包括问卷题库、答案提交、体质计算和调养建议生成。
前置要求
- 健康调查模块已完成
- 数据模型已定义
实施步骤
步骤 1:创建体质常量定义
创建 server/internal/model/constitution_const.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:
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:
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:
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