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.
 
 
 
 
 
 

329 lines
8.8 KiB

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
}