diff --git a/IM_API_Collection.postman_collection.json b/IM_API_Collection.postman_collection.json index d5be46c..a5545f7 100644 --- a/IM_API_Collection.postman_collection.json +++ b/IM_API_Collection.postman_collection.json @@ -693,6 +693,24 @@ } } }, + { + "name": "获取用户群聊列表", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "url": { + "raw": "{{base_url}}/api/groups", + "host": ["{{base_url}}"], + "path": ["api", "groups"] + }, + "description": "获取当前用户所在的所有群聊列表,包含分组信息(joined/created/managed)" + } + }, { "name": "获取群信息", "request": { @@ -879,6 +897,50 @@ ] } } + }, + { + "name": "获取群公告", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + } + ], + "url": { + "raw": "{{base_url}}/api/groups/group_1234567890/announcement", + "host": ["{{base_url}}"], + "path": ["api", "groups", "group_1234567890", "announcement"] + }, + "description": "获取群公告内容" + } + }, + { + "name": "更新群公告", + "request": { + "method": "POST", + "header": [ + { + "key": "Authorization", + "value": "Bearer {{token}}" + }, + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"announcement\": \"欢迎加入本群,请遵守群规,文明聊天。\"\n}" + }, + "url": { + "raw": "{{base_url}}/api/groups/group_1234567890/announcement", + "host": ["{{base_url}}"], + "path": ["api", "groups", "group_1234567890", "announcement"] + }, + "description": "更新群公告(仅群主或管理员可操作)" + } } ] }, diff --git a/cmd/server/main.go b/cmd/server/main.go index a7456db..958c189 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -354,6 +354,7 @@ func main() { // 群聊相关接口 authGroup.POST("/groups", api.CreateChatGroupHandler) + authGroup.GET("/groups", api.ListUserGroupsHandler) // 获取用户群聊列表(必须在 /groups/:room_id 之前) authGroup.GET("/groups/:room_id", api.GetGroupInfoHandler) authGroup.GET("/groups/:room_id/members", api.ListGroupMembersHandler) authGroup.POST("/groups/:room_id/members", api.AddGroupMembersHandler) @@ -362,6 +363,8 @@ func main() { authGroup.POST("/groups/:room_id/members/:user_id/role", api.ChangeMemberRoleHandler) authGroup.POST("/groups/:room_id/quit", api.QuitGroupHandler) authGroup.POST("/groups/:room_id/dissolve", api.DissolveGroupHandler) + authGroup.GET("/groups/:room_id/announcement", api.GetGroupAnnouncementHandler) + authGroup.POST("/groups/:room_id/announcement", api.UpdateGroupAnnouncementHandler) authGroup.GET("/group-notifications", api.GetGroupNotificationsHandler) // 附件管理 diff --git a/internal/api/room_handler.go b/internal/api/room_handler.go index d87bd56..b663604 100644 --- a/internal/api/room_handler.go +++ b/internal/api/room_handler.go @@ -5,6 +5,7 @@ package api import ( + "log" "strconv" "xk-websocket-v2/internal/model" "xk-websocket-v2/internal/service" @@ -116,6 +117,91 @@ func CreateChatGroupHandler(c *gin.Context) { utils.SuccessWithData(c, room, "创建群聊成功") } +/** + * ListUserGroupsHandler + * 功能:获取用户所在的所有群聊列表(带分组信息) + * 路径:GET /api/groups + */ +func ListUserGroupsHandler(c *gin.Context) { + userID, exists := c.Get("user_id") + if !exists { + utils.Unauthorized(c, "未授权") + return + } + + groups, members, err := service.RoomSvc.ListUserGroups(userID.(string)) + if err != nil { + utils.InternalError(c, "查询失败") + return + } + + // 确保返回空数组而不是null + if groups == nil { + groups = []model.ChatRoom{} + } + if members == nil { + members = []model.RoomMember{} + } + + // 为每个群的所有成员创建缺失的会话记录 + if service.ConversationSvc != nil { + for _, group := range groups { + if err := service.ConversationSvc.EnsureGroupConversations(group.RoomID); err != nil { + // 记录错误但不中断流程 + log.Printf("⚠️ 创建群聊会话失败 (roomID: %s): %v", group.RoomID, err) + } + } + } + + // 创建成员映射表,方便快速查找 + memberMap := make(map[string]model.RoomMember) + for _, member := range members { + memberMap[member.RoomID] = member + } + + // 转换为前端期望的格式,包含 category 和 role + result := make([]map[string]interface{}, 0, len(groups)) + for _, group := range groups { + // 获取用户在群中的角色 + member, exists := memberMap[group.RoomID] + if !exists { + continue // 跳过无法获取成员信息的群 + } + + // 确定分类:created/managed/joined + category := "joined" + if group.CreatorID == userID.(string) { + category = "created" + } else if member.Role == 1 || member.Role == 2 { + category = "managed" + } + + // 格式化时间 + var lastMessageTime *string + if group.LastMessageTime != nil { + timeStr := group.LastMessageTime.Format("2006-01-02T15:04:05Z07:00") + lastMessageTime = &timeStr + } + + result = append(result, map[string]interface{}{ + "room_id": group.RoomID, + "room_type": group.RoomType, + "room_name": group.RoomName, + "room_avatar": group.RoomAvatar, + "owner_id": group.OwnerID, + "creator_id": group.CreatorID, + "category": category, + "role": int(member.Role), + "last_message_time": lastMessageTime, + "last_message": group.LastMessage, + "created_at": group.CreatedAt.Format("2006-01-02T15:04:05Z07:00"), + "updated_at": group.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"), + }) + } + + utils.SuccessWithData(c, result, "获取成功") +} + /** * GetGroupInfoHandler * 功能:获取群聊基础信息及成员数量 @@ -420,3 +506,52 @@ func GetGroupNotificationsHandler(c *gin.Context) { "page_size": pageSize, }, "获取成功") } + +/** + * GetGroupAnnouncementHandler + * 功能:获取群公告 + * 路径:GET /api/groups/:room_id/announcement + */ +func GetGroupAnnouncementHandler(c *gin.Context) { + roomID := c.Param("room_id") + + announcement, err := service.RoomSvc.GetGroupAnnouncement(roomID) + if err != nil { + if err == gorm.ErrRecordNotFound { + utils.NotFound(c, "群聊不存在") + return + } + utils.InternalError(c, "查询失败") + return + } + + utils.SuccessWithData(c, gin.H{ + "announcement": announcement, + }, "获取成功") +} + +/** + * UpdateGroupAnnouncementHandler + * 功能:更新群公告 + * 路径:POST /api/groups/:room_id/announcement + */ +func UpdateGroupAnnouncementHandler(c *gin.Context) { + userID, _ := c.Get("user_id") + roomID := c.Param("room_id") + + var req struct { + Announcement string `json:"announcement" binding:"required"` + } + + if err := c.ShouldBindJSON(&req); err != nil { + utils.BadRequest(c, "参数错误: "+err.Error()) + return + } + + if err := service.RoomSvc.UpdateGroupAnnouncement(roomID, userID.(string), req.Announcement); err != nil { + utils.BadRequest(c, err.Error()) + return + } + + utils.Success(c, "更新成功") +} diff --git a/internal/model/types.go b/internal/model/types.go index 774b7c4..c3cbfd1 100644 --- a/internal/model/types.go +++ b/internal/model/types.go @@ -253,6 +253,8 @@ type ChatRoom struct { LastMessageTime *time.Time `gorm:"type:datetime;index;comment:最后消息时间" json:"last_message_time"` // 最后消息内容 LastMessage string `gorm:"type:text;comment:最后消息内容" json:"last_message"` + // 群公告 - 仅群聊使用 + Announcement string `gorm:"type:text;comment:群公告" json:"announcement"` // 创建时间 CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"` // 更新时间 @@ -278,6 +280,10 @@ type RoomMember struct { Role int8 `gorm:"type:tinyint(1);default:0;comment:成员角色(0=成员,1=管理员,2=群主)" json:"role"` // 加入时间 JoinedAt time.Time `gorm:"type:datetime;autoCreateTime;comment:加入时间" json:"joined_at"` + // 用户信息(关联查询,不存储在数据库中) + User *User `gorm:"foreignKey:UserID;references:ID" json:"user,omitempty"` + // 群名片(可选,用于群聊中显示的自定义昵称) + Nickname string `gorm:"-" json:"nickname,omitempty"` } // TableName 指定表名和注释 @@ -442,7 +448,12 @@ type ChatConversation struct { // 更新时间 UpdatedAt time.Time `gorm:"autoUpdateTime;comment:更新时间" json:"updated_at"` // 目标用户信息(关联查询,不存储在数据库中) - TargetUser *User `gorm:"foreignKey:TargetID;references:ID" json:"target_user,omitempty"` + // 注意:不使用外键约束,因为 target_id 可能是用户ID(私聊)或群ID(群聊) + // 使用 - 前缀禁用 GORM 的自动外键创建 + TargetUser *User `gorm:"-" json:"target_user,omitempty"` + // 群聊信息(关联查询,仅群聊时使用,不存储在数据库中) + // 使用 - 前缀禁用 GORM 的自动外键创建,手动加载 + Room *ChatRoom `gorm:"-" json:"room,omitempty"` } // TableName 指定表名和注释 diff --git a/internal/service/chat_service.go b/internal/service/chat_service.go index c481ae3..45cd138 100644 --- a/internal/service/chat_service.go +++ b/internal/service/chat_service.go @@ -124,11 +124,27 @@ func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model // 3. 消息持久化 (MySQL) // WebRTC 的 Candidate/Offer/Answer 信令通常不需要持久化,只有聊天记录和通话记录需要 + // 群通知消息也不持久化,但群聊的普通消息需要持久化 if req.MessageType != model.MessageTypeSignal && req.MessageType != model.MessageTypeGroupNotif { + // 先判断是否为群聊消息 + isGroupMessage := false + if req.RoomID != "" { + room, err := RoomSvc.GetRoom(req.RoomID) + if err == nil && room.RoomType == "group" { + isGroupMessage = true + } + } + + // 群聊消息:ReceiverUserID 设置为群ID,确保能正确识别为群聊 + receiverUserID := req.ReceiverUserID + if isGroupMessage { + receiverUserID = req.RoomID // 群聊时使用 roomID 作为 receiver_user_id + } + msg := model.ChatMessage{ RoomID: req.RoomID, SenderUserID: senderClient.UserID, - ReceiverUserID: req.ReceiverUserID, + ReceiverUserID: receiverUserID, // 群聊时使用 roomID MessageType: req.MessageType, Content: req.Content, Extra: extraData, // 存入修正后的 extraData @@ -141,22 +157,39 @@ func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model log.Printf("❌ 消息持久化失败: %v", err) } - // 4. 更新会话列表(发送方与接收方) - // 目前仅对点对点私聊做会话维护,群聊和系统类消息先不更新会话,后续按需扩展 - if ConversationSvc != nil && req.MessageType == model.MessageTypeText || + // 4. 更新会话列表 + + // 更新会话(支持私聊和群聊) + if ConversationSvc != nil && (req.MessageType == model.MessageTypeText || req.MessageType == model.MessageTypeImage || req.MessageType == model.MessageTypeAudio || req.MessageType == model.MessageTypeVideo || - req.MessageType == model.MessageTypeFile { + req.MessageType == model.MessageTypeFile) { senderID := senderClient.UserID - receiverID := req.ReceiverUserID + if isGroupMessage { + // 群聊消息:为所有成员(除发送者)更新会话 + memberIDs, mErr := RoomSvc.GetRoomMembers(req.RoomID) + if mErr == nil { + for _, memberID := range memberIDs { + if memberID != senderID { + // 为每个成员更新会话,target_id 是 room_id + _ = ConversationSvc.UpsertConversationOnMessage(memberID, req.RoomID, req.RoomID, msg, false) + } + } + // 发送者自己的会话也需要更新 + _ = ConversationSvc.UpsertConversationOnMessage(senderID, req.RoomID, req.RoomID, msg, true) + } + } else { + // 私聊消息:更新发送方和接收方的会话 + receiverID := req.ReceiverUserID if senderID != "" && receiverID != "" { // sender 视角 target 为 receiver _ = ConversationSvc.UpsertConversationOnMessage(senderID, receiverID, req.RoomID, msg, true) // receiver 视角 target 为 sender _ = ConversationSvc.UpsertConversationOnMessage(receiverID, senderID, req.RoomID, msg, false) + } } } @@ -175,9 +208,12 @@ func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model if err == nil && room.RoomType == "group" { memberIDs, mErr := RoomSvc.GetRoomMembers(req.RoomID) if mErr == nil { + senderID := senderClient.UserID for _, uid := range memberIDs { - // 群聊中仍然按用户路由,发送给所有成员 + // 群聊中过滤发送者自己,避免给自己发送消息 + if uid != senderID { s.DispatchMessage(uid, msgBytes) + } } return } diff --git a/internal/service/conversation_service.go b/internal/service/conversation_service.go index 31d2d5e..d3b70cc 100644 --- a/internal/service/conversation_service.go +++ b/internal/service/conversation_service.go @@ -5,6 +5,7 @@ package service import ( + "log" "time" "xk-websocket-v2/internal/model" @@ -32,17 +33,35 @@ func InitConversationService(db *gorm.DB) { func (s *ConversationService) GetConversations(userID string) ([]model.ChatConversation, error) { var list []model.ChatConversation err := s.DB.Where("user_id = ?", userID). - Preload("TargetUser"). Order("is_top DESC, last_time DESC, id DESC"). Find(&list).Error - return list, err + 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 + } + } + } + + return list, nil } /** * UpsertConversationOnMessage * 功能:在发送/接收消息时更新会话记录 * @param userID 会话所属用户 - * @param targetID 好友ID或群ID + * @param targetID 好友ID或群ID(群聊时targetID等于roomID) * @param roomID 房间ID * @param msg 已持久化的消息 * @param isSender 是否为发送方(发送方通常不增加未读数) @@ -52,12 +71,32 @@ func (s *ConversationService) UpsertConversationOnMessage(userID, targetID, room return nil } + // 判断是否为群聊:优先通过查询数据库确认房间类型,其次通过 roomID 格式判断 + conversationType := 1 // 默认私聊 + if roomID != "" { + // 情况1:targetID == 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 = ?", userID, targetID).First(&conv) + tx := s.DB.Where("user_id = ? AND target_id = ? AND type = ?", userID, targetID, conversationType).First(&conv) now := time.Now() - // 计算摘要 - summary := buildMessageSummary(msg) + // 计算摘要:群聊时包含发送者信息 + summary := buildMessageSummary(msg, conversationType == 2, msg.SenderUserID) if tx.Error != nil { if tx.Error == gorm.ErrRecordNotFound { @@ -66,7 +105,7 @@ func (s *ConversationService) UpsertConversationOnMessage(userID, targetID, room UserID: userID, TargetID: targetID, RoomID: roomID, - Type: 1, // 目前只支持私聊,后续可扩展群聊 + Type: conversationType, // 支持私聊(1)和群聊(2) LastMessage: summary, LastTime: now, } @@ -123,28 +162,99 @@ func (s *ConversationService) DeleteConversation(userID, targetID string) error 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 根据消息类型构建会话摘要 -func buildMessageSummary(msg model.ChatMessage) string { +// isGroupChat: 是否为群聊 +// senderUserID: 发送者用户ID(群聊时需要) +func buildMessageSummary(msg model.ChatMessage, isGroupChat bool, senderUserID string) string { + var content string switch msg.MessageType { case 1: - return "[图片]" + content = "[图片]" case 2: - return "[语音]" + content = "[语音]" case 3: - return "[视频]" + content = "[视频]" case 6, 7: - return "[通话]" + content = "[通话]" case 8: - return "[文件]" + content = "[文件]" default: if msg.Content == "" { - return "[空消息]" + content = "[空消息]" + } else { + if len([]rune(msg.Content)) > 50 { + content = string([]rune(msg.Content)[:50]) + "..." + } else { + content = msg.Content + } } - if len([]rune(msg.Content)) > 50 { - return string([]rune(msg.Content)[:50]) + "..." - } - return 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 } diff --git a/internal/service/room_service.go b/internal/service/room_service.go index 01f9ef2..32596b0 100644 --- a/internal/service/room_service.go +++ b/internal/service/room_service.go @@ -186,6 +186,19 @@ func (s *RoomService) GetRoomMembers(roomID string) ([]string, error) { return userIDs, nil } +/** + * GetRoomMember + * 功能:获取房间中指定成员的信息 + */ +func (s *RoomService) GetRoomMember(roomID, userID string) (*model.RoomMember, error) { + var member model.RoomMember + err := s.DB.Where("room_id = ? AND user_id = ?", roomID, userID).First(&member).Error + if err != nil { + return nil, err + } + return &member, nil +} + /** * AddRoomMember * 功能:添加房间成员 @@ -270,11 +283,14 @@ func (s *RoomService) CreateGroupRoom(creatorID string, memberIDs []string, name /** * ListGroupMembers - * 功能:返回群成员及其角色信息 + * 功能:返回群成员及其角色信息(包含用户信息) */ func (s *RoomService) ListGroupMembers(roomID string) ([]model.RoomMember, error) { var members []model.RoomMember - if err := s.DB.Where("room_id = ?", roomID).Find(&members).Error; err != nil { + if err := s.DB.Where("room_id = ?", roomID). + Preload("User"). + Order("role DESC, joined_at ASC"). + Find(&members).Error; err != nil { return nil, err } return members, nil @@ -283,17 +299,34 @@ func (s *RoomService) ListGroupMembers(roomID string) ([]model.RoomMember, error /** * ListUserGroups * 功能:查询用户所在的所有群聊房间 + * 返回:群聊列表和用户在每个群中的角色信息 */ -func (s *RoomService) ListUserGroups(userID string) ([]model.ChatRoom, error) { +func (s *RoomService) ListUserGroups(userID string) ([]model.ChatRoom, []model.RoomMember, error) { var rooms []model.ChatRoom + var members []model.RoomMember + + // 查询用户所在的所有群聊 err := s.DB. Joins("JOIN room_members ON room_members.room_id = chat_rooms.room_id"). Where("room_members.user_id = ? AND chat_rooms.room_type = ?", userID, "group"). Find(&rooms).Error if err != nil { - return nil, err + return nil, nil, err } - return rooms, nil + + // 获取用户在这些群中的角色信息 + if len(rooms) > 0 { + roomIDs := make([]string, len(rooms)) + for i, room := range rooms { + roomIDs[i] = room.RoomID + } + err = s.DB.Where("room_id IN ? AND user_id = ?", roomIDs, userID).Find(&members).Error + if err != nil { + return nil, nil, err + } + } + + return rooms, members, nil } /** @@ -474,3 +507,36 @@ func (s *RoomService) isGroupAdminOrOwner(roomID, userID string) (bool, error) { } return member.Role == 1 || member.Role == 2, nil } + +/** + * GetGroupAnnouncement + * 功能:获取群公告 + */ +func (s *RoomService) GetGroupAnnouncement(roomID string) (string, error) { + var room model.ChatRoom + err := s.DB.Where("room_id = ? AND room_type = ?", roomID, "group").First(&room).Error + if err != nil { + return "", err + } + return room.Announcement, nil +} + +/** + * UpdateGroupAnnouncement + * 功能:更新群公告(仅群主或管理员可操作) + */ +func (s *RoomService) UpdateGroupAnnouncement(roomID, userID, announcement string) error { + // 权限校验:必须是群主或管理员 + isAdmin, err := s.isGroupAdminOrOwner(roomID, userID) + if err != nil { + return err + } + if !isAdmin { + return fmt.Errorf("无权限更新群公告") + } + + // 更新群公告 + return s.DB.Model(&model.ChatRoom{}). + Where("room_id = ? AND room_type = ?", roomID, "group"). + Update("announcement", announcement).Error +} diff --git a/nl_im_plus.sql b/nl_im_plus.sql index 529b174..71ab760 100644 --- a/nl_im_plus.sql +++ b/nl_im_plus.sql @@ -112,6 +112,7 @@ CREATE TABLE `chat_rooms` ( `creator_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '创建者ID', `last_message_time` datetime NULL DEFAULT NULL COMMENT '最后消息时间', `last_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '最后消息内容', + `announcement` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '群公告', `created_at` datetime(3) NULL DEFAULT NULL COMMENT '创建时间', `updated_at` datetime(3) NULL DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`room_id`) USING BTREE, @@ -142,6 +143,36 @@ CREATE TABLE `room_members` ( -- Records of room_members -- ---------------------------- +-- ---------------------------- +-- Table structure for chat_conversations +-- ---------------------------- +DROP TABLE IF EXISTS `chat_conversations`; +CREATE TABLE `chat_conversations` ( + `id` bigint UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `user_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户ID', + `target_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '目标ID(好友或群)', + `room_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '房间ID', + `type` int NULL DEFAULT 1 COMMENT '会话类型(1=私聊,2=群聊)', + `is_top` tinyint(1) NULL DEFAULT 0 COMMENT '是否置顶', + `is_muted` tinyint(1) NULL DEFAULT 0 COMMENT '是否免打扰', + `is_special_care` tinyint(1) NULL DEFAULT 0 COMMENT '是否特别关心', + `unread_count` int NULL DEFAULT 0 COMMENT '未读消息数', + `last_message` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '最后一条消息内容', + `last_time` datetime NULL DEFAULT NULL COMMENT '最后消息时间', + `created_at` datetime(3) NULL DEFAULT NULL COMMENT '创建时间', + `updated_at` datetime(3) NULL DEFAULT NULL COMMENT '更新时间', + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_chat_conversations_user_id`(`user_id` ASC) USING BTREE, + INDEX `idx_chat_conversations_target_id`(`target_id` ASC) USING BTREE, + INDEX `idx_chat_conversations_room_id`(`room_id` ASC) USING BTREE, + INDEX `idx_chat_conversations_last_time`(`last_time` ASC) USING BTREE + -- 注意:不添加外键约束,因为 target_id 可能是用户ID(私聊)或群ID(群聊) +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户会话表,记录每个用户的最近会话列表和未读信息' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of chat_conversations +-- ---------------------------- + -- ---------------------------- -- Records of chat_rooms -- ----------------------------