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.
 
 
 
 
 
 

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