diff --git a/internal/model/types.go b/internal/model/types.go index 4d823ba..6be910f 100644 --- a/internal/model/types.go +++ b/internal/model/types.go @@ -175,6 +175,8 @@ type UserContact struct { UserID string `gorm:"type:varchar(100);index;comment:用户ID" json:"user_id"` // 联系人ID - 好友的用户ID ContactID string `gorm:"type:varchar(100);index;comment:联系人ID" json:"contact_id"` + // 房间ID - 关联的房间ID + RoomID string `gorm:"type:varchar(100);index;comment:房间ID" json:"room_id"` // 备注名称 - 用户自定义的好友备注 RemarkName string `gorm:"type:varchar(100);comment:备注名称" json:"remark_name"` // 分组ID - 好友所属分组 @@ -241,8 +243,10 @@ type ChatRoom struct { RoomName string `gorm:"type:varchar(200);comment:房间名称" json:"room_name"` // 房间头像 - 群聊时显示 RoomAvatar string `gorm:"type:varchar(500);comment:房间头像" json:"room_avatar"` - // 成员列表 - JSON数组 - Members string `gorm:"type:text;comment:成员列表JSON" json:"members"` + // 群主ID - 群聊必填,单聊为0 + OwnerID string `gorm:"type:varchar(100);index;default:0;comment:群主ID(群聊必填,单聊为0)" json:"owner_id"` + // 管理员ID列表 - 逗号分隔 + AdminIDs string `gorm:"type:varchar(500);comment:管理员ID列表(逗号分隔)" json:"admin_ids"` // 创建者ID CreatorID string `gorm:"type:varchar(100);index;comment:创建者ID" json:"creator_id"` // 最后消息时间 @@ -260,6 +264,25 @@ func (ChatRoom) TableName() string { return "chat_rooms" } +/** + * RoomMember + * 对应数据库表:room_members + * 作用:房间成员表,存储房间与用户的关联关系 + */ +type RoomMember struct { + // 房间ID + RoomID string `gorm:"type:varchar(100);primaryKey;comment:房间ID" json:"room_id"` + // 用户ID + UserID string `gorm:"type:varchar(100);primaryKey;comment:用户ID" json:"user_id"` + // 加入时间 + JoinedAt time.Time `gorm:"type:datetime;autoCreateTime;comment:加入时间" json:"joined_at"` +} + +// TableName 指定表名和注释 +func (RoomMember) TableName() string { + return "room_members" +} + /** * FriendRequest * 对应数据库表:friend_requests @@ -416,6 +439,8 @@ type ChatConversation struct { CreatedAt time.Time `gorm:"autoCreateTime;comment:创建时间" json:"created_at"` // 更新时间 UpdatedAt time.Time `gorm:"autoUpdateTime;comment:更新时间" json:"updated_at"` + // 目标用户信息(关联查询,不存储在数据库中) + TargetUser *User `gorm:"foreignKey:TargetID;references:ID" json:"target_user,omitempty"` } // TableName 指定表名和注释 diff --git a/internal/service/contact_service.go b/internal/service/contact_service.go index e74e703..5eb13a2 100644 --- a/internal/service/contact_service.go +++ b/internal/service/contact_service.go @@ -5,8 +5,12 @@ package service import ( + "encoding/json" "errors" + "fmt" + "time" "xk-websocket-v2/internal/model" + "xk-websocket-v2/internal/utils" "gorm.io/gorm" ) @@ -69,10 +73,14 @@ func (s *ContactService) AddFriend(fromUserID, toUserID, message string) error { /** * GetFriendRequests * 功能:获取好友申请列表 + * 包括: + * 1. 发送给当前用户的待处理申请 (to_user_id = userID AND status = 'pending') + * 2. 当前用户发送的被拒绝的申请 (from_user_id = userID AND status = 'rejected') */ func (s *ContactService) GetFriendRequests(userID string) ([]model.FriendRequest, error) { var requests []model.FriendRequest - result := s.DB.Where("to_user_id = ? AND status = ?", userID, "pending"). + result := s.DB.Where("(to_user_id = ? AND status = ?) OR (from_user_id = ? AND status = ?)", + userID, "pending", userID, "rejected"). Order("created_at DESC"). Find(&requests) return requests, result.Error @@ -96,6 +104,11 @@ func (s *ContactService) AcceptFriendRequest(requestID uint, userID string) erro // 开始事务 tx := s.DB.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() // 更新申请状态 if err := tx.Model(&request).Update("status", "accepted").Error; err != nil { @@ -103,14 +116,53 @@ func (s *ContactService) AcceptFriendRequest(requestID uint, userID string) erro return err } - // 创建双向好友关系 + // 使用雪花ID生成唯一的房间ID + roomID, err := utils.NextIDString() + if err != nil { + tx.Rollback() + return fmt.Errorf("生成房间ID失败: %v", err) + } + + // 创建房间(p2p类型) + room := model.ChatRoom{ + RoomID: roomID, + RoomType: "p2p", + OwnerID: "0", // 单聊群主为0 + CreatorID: request.ToUserID, // 接受者为创建者 + } + if err := tx.Create(&room).Error; err != nil { + tx.Rollback() + return err + } + + // 创建房间成员记录(两个用户) + member1 := model.RoomMember{ + RoomID: roomID, + UserID: request.FromUserID, + } + member2 := model.RoomMember{ + RoomID: roomID, + UserID: request.ToUserID, + } + if err := tx.Create(&member1).Error; err != nil { + tx.Rollback() + return err + } + if err := tx.Create(&member2).Error; err != nil { + tx.Rollback() + return err + } + + // 创建双向好友关系,并绑定房间ID contact1 := model.UserContact{ UserID: request.FromUserID, ContactID: request.ToUserID, + RoomID: roomID, } contact2 := model.UserContact{ UserID: request.ToUserID, ContactID: request.FromUserID, + RoomID: roomID, } if err := tx.Create(&contact1).Error; err != nil { @@ -122,7 +174,76 @@ func (s *ContactService) AcceptFriendRequest(requestID uint, userID string) erro return err } - return tx.Commit().Error + // 为双方创建会话记录 + now := time.Now() + conv1 := model.ChatConversation{ + UserID: request.FromUserID, + TargetID: request.ToUserID, + RoomID: roomID, + Type: 1, // 私聊 + LastTime: now, + } + conv2 := model.ChatConversation{ + UserID: request.ToUserID, + TargetID: request.FromUserID, + RoomID: roomID, + Type: 1, // 私聊 + LastTime: now, + } + + if err := tx.Create(&conv1).Error; err != nil { + tx.Rollback() + return err + } + if err := tx.Create(&conv2).Error; err != nil { + tx.Rollback() + return err + } + + // 提交事务 + if err := tx.Commit().Error; err != nil { + return err + } + + // 发送欢迎消息(在事务外执行,避免影响主流程) + go s.sendWelcomeMessage(request.ToUserID, request.FromUserID, roomID) + + return nil +} + +/** + * sendWelcomeMessage + * 功能:发送欢迎消息 + */ +func (s *ContactService) sendWelcomeMessage(senderID, receiverID, roomID string) { + // 创建消息记录 + msg := model.ChatMessage{ + RoomID: roomID, + SenderUserID: senderID, + ReceiverUserID: receiverID, + MessageType: 0, // 文本消息 + Content: "我已经通过了你的好友申请,开始和我聊天吧~", + } + + if err := s.DB.Create(&msg).Error; err != nil { + return + } + + // 更新会话列表 + if ConversationSvc != nil { + _ = ConversationSvc.UpsertConversationOnMessage(senderID, receiverID, roomID, msg, true) + _ = ConversationSvc.UpsertConversationOnMessage(receiverID, senderID, roomID, msg, false) + } + + // 通过 WebSocket 推送消息 + if ChatSvc != nil { + pushMsg := model.WsPayload{ + RequestType: "receive_message", + Data: msg, + } + msgBytes, _ := json.Marshal(pushMsg) + ChatSvc.DispatchMessage(receiverID, msgBytes) + } } /** @@ -288,19 +409,24 @@ func (s *ContactService) GetContactsWithUserInfo(userID string) ([]map[string]in } user.Password = "" - // 组合数据 + // 组合数据,包含 user 对象 item := map[string]interface{}{ - "id": contact.ContactID, - "name": user.Name, - "avatar": user.Avatar, - "desc": user.Desc, - "remark_name": contact.RemarkName, - "group_id": contact.GroupID, - "is_top": contact.IsTop, - "is_muted": contact.IsMuted, + "id": contact.ContactID, + "user_id": contact.ContactID, + "contact_user_id": contact.ContactID, + "user": user, // 包含完整的用户信息对象 + "remark_name": contact.RemarkName, + "room_id": contact.RoomID, + "group_id": contact.GroupID, + "is_top": contact.IsTop, + "is_muted": contact.IsMuted, + "is_special_care": contact.IsSpecialCare, + "is_blocked": contact.IsBlocked, "last_chat_time": contact.LastChatTime, - "last_message": contact.LastMessage, - "unread_count": contact.UnreadCount, + "last_message": contact.LastMessage, + "last_msg": contact.LastMessage, + "unread_count": contact.UnreadCount, + "unread": contact.UnreadCount, } result = append(result, item) } diff --git a/internal/service/conversation_service.go b/internal/service/conversation_service.go index 1cff096..31d2d5e 100644 --- a/internal/service/conversation_service.go +++ b/internal/service/conversation_service.go @@ -32,6 +32,7 @@ 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 diff --git a/internal/service/room_service.go b/internal/service/room_service.go index cd0c8a3..67ccf8d 100644 --- a/internal/service/room_service.go +++ b/internal/service/room_service.go @@ -5,7 +5,6 @@ package service import ( - "encoding/json" "fmt" "time" "xk-websocket-v2/internal/model" @@ -62,6 +61,8 @@ func GenerateGroupRoomID() string { */ func (s *RoomService) CreateRoom(roomType string, members []string, creatorID string) (*model.ChatRoom, error) { var roomID string + var ownerID string = "0" // 单聊默认为0 + if roomType == "p2p" { if len(members) != 2 { return nil, fmt.Errorf("点对点房间需要2个成员") @@ -69,6 +70,8 @@ func (s *RoomService) CreateRoom(roomType string, members []string, creatorID st roomID = GenerateP2PRoomID(members[0], members[1]) } else { roomID = GenerateGroupRoomID() + // 群聊时,创建者就是群主 + ownerID = creatorID } // 检查房间是否已存在 @@ -77,16 +80,19 @@ func (s *RoomService) CreateRoom(roomType string, members []string, creatorID st return &existingRoom, nil } - // 序列化成员列表 - membersJSON, err := json.Marshal(members) - if err != nil { - return nil, err - } + // 开始事务 + tx := s.DB.Begin() + defer func() { + if r := recover(); r != nil { + tx.Rollback() + } + }() + // 创建房间 room := model.ChatRoom{ - RoomID: roomID, - RoomType: roomType, - Members: string(membersJSON), + RoomID: roomID, + RoomType: roomType, + OwnerID: ownerID, CreatorID: creatorID, } @@ -94,7 +100,24 @@ func (s *RoomService) CreateRoom(roomType string, members []string, creatorID st room.RoomName = "群聊" } - if err := s.DB.Create(&room).Error; err != nil { + if err := tx.Create(&room).Error; err != nil { + tx.Rollback() + return nil, err + } + + // 创建房间成员记录 + for _, userID := range members { + member := model.RoomMember{ + RoomID: roomID, + UserID: userID, + } + if err := tx.Create(&member).Error; err != nil { + tx.Rollback() + return nil, err + } + } + + if err := tx.Commit().Error; err != nil { return nil, err } @@ -146,3 +169,41 @@ func (s *RoomService) UpdateRoomLastMessage(roomID, message string) error { }).Error } +/** + * GetRoomMembers + * 功能:获取房间成员列表 + */ +func (s *RoomService) GetRoomMembers(roomID string) ([]string, error) { + var members []model.RoomMember + if err := s.DB.Where("room_id = ?", roomID).Find(&members).Error; err != nil { + return nil, err + } + + userIDs := make([]string, len(members)) + for i, member := range members { + userIDs[i] = member.UserID + } + return userIDs, nil +} + +/** + * AddRoomMember + * 功能:添加房间成员 + */ +func (s *RoomService) AddRoomMember(roomID, userID string) error { + member := model.RoomMember{ + RoomID: roomID, + UserID: userID, + } + return s.DB.Create(&member).Error +} + +/** + * RemoveRoomMember + * 功能:移除房间成员 + */ +func (s *RoomService) RemoveRoomMember(roomID, userID string) error { + return s.DB.Where("room_id = ? AND user_id = ?", roomID, userID). + Delete(&model.RoomMember{}).Error +} + diff --git a/nl_im_plus.sql b/nl_im_plus.sql index cb09a90..ed0c864 100644 --- a/nl_im_plus.sql +++ b/nl_im_plus.sql @@ -107,7 +107,8 @@ CREATE TABLE `chat_rooms` ( `room_type` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '房间类型', `room_name` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '房间名称', `room_avatar` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '房间头像', - `members` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '成员列表JSON', + `owner_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '群主ID(群聊必填,单聊为0)', + `admin_ids` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '管理员ID列表(逗号分隔)', `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 '最后消息内容', @@ -115,6 +116,7 @@ CREATE TABLE `chat_rooms` ( `updated_at` datetime(3) NULL DEFAULT NULL COMMENT '更新时间', PRIMARY KEY (`room_id`) USING BTREE, INDEX `idx_chat_rooms_room_type`(`room_type` ASC) USING BTREE, + INDEX `idx_chat_rooms_owner_id`(`owner_id` ASC) USING BTREE, INDEX `idx_chat_rooms_creator_id`(`creator_id` ASC) USING BTREE, INDEX `idx_chat_rooms_last_message_time`(`last_message_time` ASC) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '聊天房间表,支持点对点和群聊' ROW_FORMAT = Dynamic; @@ -123,6 +125,26 @@ CREATE TABLE `chat_rooms` ( -- Records of chat_rooms -- ---------------------------- +-- ---------------------------- +-- Table structure for room_members +-- ---------------------------- +DROP TABLE IF EXISTS `room_members`; +CREATE TABLE `room_members` ( + `room_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '房间ID', + `user_id` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '用户ID', + `joined_at` datetime(3) NULL DEFAULT NULL COMMENT '加入时间', + PRIMARY KEY (`room_id`, `user_id`) USING BTREE, + INDEX `idx_room_members_user_id`(`user_id` ASC) USING BTREE +) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '房间成员表,存储房间与用户的关联关系' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of room_members +-- ---------------------------- + +-- ---------------------------- +-- Records of chat_rooms +-- ---------------------------- + -- ---------------------------- -- Table structure for contact_groups -- ---------------------------- @@ -195,10 +217,13 @@ CREATE TABLE `user_contacts` ( `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', `contact_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', `remark_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注名称', `group_id` bigint NULL DEFAULT NULL COMMENT '分组ID', `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 '是否特别关心', + `is_blocked` tinyint(1) NULL DEFAULT 0 COMMENT '是否拉黑', `last_chat_time` datetime NULL DEFAULT NULL COMMENT '最后聊天时间', `last_message` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '最后一条消息', `unread_count` bigint NULL DEFAULT 0 COMMENT '未读消息数', @@ -207,6 +232,7 @@ CREATE TABLE `user_contacts` ( PRIMARY KEY (`id`) USING BTREE, INDEX `idx_user_contacts_user_id`(`user_id` ASC) USING BTREE, INDEX `idx_user_contacts_contact_id`(`contact_id` ASC) USING BTREE, + INDEX `idx_user_contacts_room_id`(`room_id` ASC) USING BTREE, INDEX `idx_user_contacts_group_id`(`group_id` ASC) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户联系人表,存储好友关系、分组、备注等信息' ROW_FORMAT = Dynamic; @@ -237,7 +263,7 @@ CREATE TABLE `users` ( -- ---------------------------- -- Records of users -- ---------------------------- -INSERT INTO `users` VALUES ('10001', 'user001@example.com', '13800138001', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '张三', '张', 'Golang 开发工程师', '北京', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000'); +INSERT INTO `users` VALUES ('10001', 'workerqi@163.com', '13800138001', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '张三', '张', 'Golang 开发工程师', '北京', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000'); INSERT INTO `users` VALUES ('10002', 'user002@example.com', '13800138002', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '李四', '李', '前端开发工程师', '上海', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000'); INSERT INTO `users` VALUES ('10003', 'user003@example.com', '13800138003', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '王五', '王', '产品经理', '深圳', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000'); INSERT INTO `users` VALUES ('10004', 'user004@example.com', '13800138004', '$2a$10$OsVRHKowu.mVT4GNxzZ3/eo8xEABkWsUGAt4nV5nalzOXW47vtBmC', '赵六', '赵', 'UI设计师', '广州', '2025-12-03 09:53:49.000', '2025-12-03 09:53:49.000');