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 }