Files
nl-im-service/internal/service/conversation_service.go
2025-12-05 16:35:14 +08:00

369 lines
10 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* package service
* 作用:会话列表(最近聊天)管理服务
*/
package service
import (
"log"
"time"
"xk-websocket-v2/internal/model"
"gorm.io/gorm"
)
// ConversationService 会话服务结构体
type ConversationService struct {
DB *gorm.DB
}
// ConversationSvc 全局单例
var ConversationSvc *ConversationService
// InitConversationService 初始化会话服务
func InitConversationService(db *gorm.DB) {
ConversationSvc = &ConversationService{DB: db}
}
/**
* GetConversations
* 功能:获取用户的最近会话列表
*/
func (s *ConversationService) GetConversations(userID string) ([]model.ChatConversation, error) {
var list []model.ChatConversation
err := s.DB.Where("user_id = ?", userID).
Order("is_top DESC, last_time DESC, id DESC").
Find(&list).Error
if err != nil {
return list, err
}
// 手动加载关联信息
for i := range list {
if list[i].Type == 1 { // 私聊:加载目标用户信息
var user model.User
if err := s.DB.Where("id = ?", list[i].TargetID).First(&user).Error; err == nil {
list[i].TargetUser = &user
}
} else if list[i].Type == 2 { // 群聊:加载群信息
var room model.ChatRoom
if err := s.DB.Where("room_id = ?", list[i].RoomID).First(&room).Error; err == nil {
list[i].Room = &room
} else {
// 记录警告:群聊信息查询失败
log.Printf("⚠️ 群聊信息查询失败 (room_id: %s): %v", list[i].RoomID, err)
}
}
}
return list, nil
}
/**
* UpsertConversationOnMessage
* 功能:在发送/接收消息时更新会话记录
* @param userID 会话所属用户
* @param targetID 好友ID或群ID群聊时targetID等于roomID
* @param roomID 房间ID
* @param msg 已持久化的消息
* @param isSender 是否为发送方(发送方通常不增加未读数)
*/
func (s *ConversationService) UpsertConversationOnMessage(userID, targetID, roomID string, msg model.ChatMessage, isSender bool) error {
if s == nil {
return nil
}
// 判断是否为群聊:优先通过查询数据库确认房间类型,其次通过 roomID 格式判断
conversationType := 1 // 默认私聊
if roomID != "" {
// 情况1targetID == roomID说明是群聊群聊时 targetID 就是 roomID
if targetID == roomID {
conversationType = 2 // 群聊
} else {
// 情况2查询数据库确认房间类型用于兼容旧数据或特殊情况
var room model.ChatRoom
if err := s.DB.Where("room_id = ?", roomID).First(&room).Error; err == nil {
if room.RoomType == "group" {
conversationType = 2 // 群聊
}
} else if len(roomID) > 6 && roomID[:6] == "group_" {
// 如果查询失败,回退到格式判断
conversationType = 2 // 群聊
}
}
}
var conv model.ChatConversation
tx := s.DB.Where("user_id = ? AND target_id = ? AND type = ?", userID, targetID, conversationType).First(&conv)
now := time.Now()
// 计算摘要:群聊时包含发送者信息
summary := buildMessageSummary(msg, conversationType == 2, msg.SenderUserID)
if tx.Error != nil {
if tx.Error == gorm.ErrRecordNotFound {
// 新建会话
conv = model.ChatConversation{
UserID: userID,
TargetID: targetID,
RoomID: roomID,
Type: conversationType, // 支持私聊(1)和群聊(2)
LastMessage: summary,
LastTime: now,
}
// 接收方增加未读
if !isSender {
conv.UnreadCount = 1
}
// 群聊时target_id 是群ID不是用户ID需要临时禁用外键检查
if conversationType == 2 {
s.DB.Exec("SET FOREIGN_KEY_CHECKS = 0")
err := s.DB.Create(&conv).Error
s.DB.Exec("SET FOREIGN_KEY_CHECKS = 1")
return err
}
return s.DB.Create(&conv).Error
}
return tx.Error
}
// 已存在会话则更新
updates := map[string]interface{}{
"room_id": roomID,
"last_message": summary,
"last_time": now,
}
if !isSender {
updates["unread_count"] = conv.UnreadCount + 1
}
return s.DB.Model(&model.ChatConversation{}).
Where("id = ?", conv.ID).
Updates(updates).Error
}
/**
* ResetUnread
* 功能:清空某个目标的未读消息数
*/
func (s *ConversationService) ResetUnread(userID, targetID string) error {
return s.DB.Model(&model.ChatConversation{}).
Where("user_id = ? AND target_id = ?", userID, targetID).
Update("unread_count", 0).Error
}
/**
* UpdateConversationFlags
* 功能:更新会话标记(置顶、免打扰、特别关心等)
*/
func (s *ConversationService) UpdateConversationFlags(userID, targetID string, updates map[string]interface{}) error {
return s.DB.Model(&model.ChatConversation{}).
Where("user_id = ? AND target_id = ?", userID, targetID).
Updates(updates).Error
}
/**
* DeleteConversation
* 功能:删除单个会话记录(不删除聊天记录)
*/
func (s *ConversationService) DeleteConversation(userID, targetID string) error {
return s.DB.Where("user_id = ? AND target_id = ?", userID, targetID).
Delete(&model.ChatConversation{}).Error
}
/**
* EnsureGroupConversations
* 功能:确保群的所有成员都有该群的会话记录
* @param roomID 群ID
*/
func (s *ConversationService) EnsureGroupConversations(roomID string) error {
if s == nil {
return nil
}
// 获取群的所有成员
memberIDs, err := RoomSvc.GetRoomMembers(roomID)
if err != nil {
return err
}
// 获取群信息,确认是群聊
room, err := RoomSvc.GetRoom(roomID)
if err != nil {
return err
}
if room.RoomType != "group" {
return nil // 不是群聊,不需要创建会话
}
// 为每个成员创建会话(如果不存在)
now := time.Now()
for _, memberID := range memberIDs {
// 检查是否已有会话
var existingConv model.ChatConversation
err := s.DB.Where("user_id = ? AND target_id = ? AND type = ?", memberID, roomID, 2).First(&existingConv).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
// 不存在,创建新会话
// 临时禁用外键检查,因为 target_id 是群ID不是用户ID
s.DB.Exec("SET FOREIGN_KEY_CHECKS = 0")
insertSQL := `INSERT INTO chat_conversations (user_id, target_id, room_id, type, is_top, is_muted, is_special_care, unread_count, last_message, last_time, created_at, updated_at)
VALUES (?, ?, ?, 2, false, false, false, 0, '', ?, ?, ?)`
createErr := s.DB.Exec(insertSQL, memberID, roomID, roomID, now, now, now).Error
s.DB.Exec("SET FOREIGN_KEY_CHECKS = 1")
if createErr != nil {
log.Printf("❌ 创建群聊会话失败 (userID: %s, roomID: %s): %v", memberID, roomID, createErr)
}
} else {
log.Printf("❌ 查询群聊会话失败 (userID: %s, roomID: %s): %v", memberID, roomID, err)
}
}
// 如果已存在,不做任何操作
}
return nil
}
// buildMessageSummary 根据消息类型构建会话摘要
// isGroupChat: 是否为群聊
// senderUserID: 发送者用户ID群聊时需要
func buildMessageSummary(msg model.ChatMessage, isGroupChat bool, senderUserID string) string {
var content string
switch msg.MessageType {
case 1:
content = "[图片]"
case 2:
content = "[语音]"
case 3:
content = "[视频]"
case 6, 7:
content = "[通话]"
case 8:
content = "[文件]"
default:
if msg.Content == "" {
content = "[空消息]"
} else {
if len([]rune(msg.Content)) > 50 {
content = string([]rune(msg.Content)[:50]) + "..."
} else {
content = msg.Content
}
}
}
// 如果是群聊,需要包含发送者信息
if isGroupChat && senderUserID != "" {
var sender model.User
if err := ConversationSvc.DB.Where("id = ?", senderUserID).First(&sender).Error; err == nil {
// 格式:用户名:消息内容
return sender.Name + "" + content
}
// 如果查询失败,仍然返回内容(不包含用户名)
}
return content
}
/**
* GetOrCreateConversationByRoom
* 功能:根据 room_id 获取或创建会话
* @param userID 用户ID
* @param roomID 房间ID
* @returns 会话对象和错误
*/
func (s *ConversationService) GetOrCreateConversationByRoom(userID, roomID string) (*model.ChatConversation, error) {
if s == nil {
return nil, gorm.ErrRecordNotFound
}
// 先查询房间信息(必须存在才能创建会话)
var room model.ChatRoom
roomErr := s.DB.Where("room_id = ?", roomID).First(&room).Error
if roomErr != nil {
// 房间不存在,返回错误
return nil, gorm.ErrRecordNotFound
}
var conversationType int
var targetID string
// 根据房间类型判断会话类型
if room.RoomType == "group" {
conversationType = 2 // 群聊
targetID = roomID // 群聊时 targetID = roomID
} else {
conversationType = 1 // 私聊
// 私聊需要确定 targetID对方用户ID
// 从 room_members 中查找另一个成员
var members []model.RoomMember
if err := s.DB.Where("room_id = ?", roomID).Find(&members).Error; err == nil {
for _, member := range members {
if member.UserID != userID {
targetID = member.UserID
break
}
}
}
if targetID == "" {
return nil, gorm.ErrRecordNotFound
}
}
// 查询会话
var conv model.ChatConversation
tx := s.DB.Where("user_id = ? AND target_id = ? AND type = ?", userID, targetID, conversationType).First(&conv)
if tx.Error != nil {
if tx.Error == gorm.ErrRecordNotFound {
// 会话不存在,根据房间信息创建新会话
conv = model.ChatConversation{
UserID: userID,
TargetID: targetID,
RoomID: roomID,
Type: conversationType,
LastMessage: "",
LastTime: time.Now(),
UnreadCount: 0,
}
// 群聊时target_id 是群ID不是用户ID需要临时禁用外键检查
if conversationType == 2 {
s.DB.Exec("SET FOREIGN_KEY_CHECKS = 0")
err := s.DB.Create(&conv).Error
s.DB.Exec("SET FOREIGN_KEY_CHECKS = 1")
if err != nil {
return nil, err
}
} else {
if err := s.DB.Create(&conv).Error; err != nil {
return nil, err
}
}
} else {
return nil, tx.Error
}
}
// 加载关联信息
if conv.Type == 1 { // 私聊:加载目标用户信息
var user model.User
if err := s.DB.Where("id = ?", conv.TargetID).First(&user).Error; err == nil {
conv.TargetUser = &user
} else {
log.Printf("⚠️ 用户信息查询失败 (user_id: %s): %v", conv.TargetID, err)
}
} else if conv.Type == 2 { // 群聊:加载群信息
conv.Room = &room
}
return &conv, nil
}