群聊接口

This commit is contained in:
2025-12-04 22:29:32 +08:00
parent 34cffd100f
commit 746e9d19e1
7 changed files with 741 additions and 15 deletions

View File

@@ -627,7 +627,7 @@
"name": "房间管理",
"item": [
{
"name": "创建房间",
"name": "创建房间(单聊)",
"request": {
"method": "POST",
"header": [
@@ -667,6 +667,157 @@
"path": ["api", "rooms", "10001_10002"]
}
}
},
{
"name": "创建群聊",
"request": {
"method": "POST",
"header": [
{
"key": "Authorization",
"value": "Bearer {{token}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"name\": \"开发讨论群\",\n \"avatar\": \"G\",\n \"member_ids\": [\"{{user_id}}\", \"10002\", \"10003\"],\n \"admin_ids\": [\"10002\"]\n}"
},
"url": {
"raw": "{{base_url}}/api/groups",
"host": ["{{base_url}}"],
"path": ["api", "groups"]
}
}
},
{
"name": "获取群信息",
"request": {
"method": "GET",
"header": [
{
"key": "Authorization",
"value": "Bearer {{token}}"
}
],
"url": {
"raw": "{{base_url}}/api/groups/group_1234567890",
"host": ["{{base_url}}"],
"path": ["api", "groups", "group_1234567890"]
}
}
},
{
"name": "获取群成员列表",
"request": {
"method": "GET",
"header": [
{
"key": "Authorization",
"value": "Bearer {{token}}"
}
],
"url": {
"raw": "{{base_url}}/api/groups/group_1234567890/members",
"host": ["{{base_url}}"],
"path": ["api", "groups", "group_1234567890", "members"]
}
}
},
{
"name": "邀请成员入群",
"request": {
"method": "POST",
"header": [
{
"key": "Authorization",
"value": "Bearer {{token}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"member_ids\": [\"10004\", \"10005\"]\n}"
},
"url": {
"raw": "{{base_url}}/api/groups/group_1234567890/members",
"host": ["{{base_url}}"],
"path": ["api", "groups", "group_1234567890", "members"]
}
}
},
{
"name": "移除群成员",
"request": {
"method": "DELETE",
"header": [
{
"key": "Authorization",
"value": "Bearer {{token}}"
}
],
"url": {
"raw": "{{base_url}}/api/groups/group_1234567890/members/10004",
"host": ["{{base_url}}"],
"path": ["api", "groups", "group_1234567890", "members", "10004"]
}
}
},
{
"name": "修改群信息",
"request": {
"method": "PATCH",
"header": [
{
"key": "Authorization",
"value": "Bearer {{token}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"name\": \"新的群名称\",\n \"avatar\": \"NG\"\n}"
},
"url": {
"raw": "{{base_url}}/api/groups/group_1234567890",
"host": ["{{base_url}}"],
"path": ["api", "groups", "group_1234567890"]
}
}
},
{
"name": "调整成员角色",
"request": {
"method": "PATCH",
"header": [
{
"key": "Authorization",
"value": "Bearer {{token}}"
},
{
"key": "Content-Type",
"value": "application/json"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"role\": 1\n}"
},
"url": {
"raw": "{{base_url}}/api/groups/group_1234567890/members/10002/role",
"host": ["{{base_url}}"],
"path": ["api", "groups", "group_1234567890", "members", "10002", "role"]
}
}
}
]
},
@@ -674,7 +825,7 @@
"name": "消息管理",
"item": [
{
"name": "发送消息",
"name": "发送消息(单聊)",
"request": {
"method": "POST",
"header": [
@@ -702,6 +853,64 @@
}
}
},
{
"name": "发送消息(群聊文本)",
"request": {
"method": "POST",
"header": [
{
"key": "Authorization",
"value": "Bearer {{token}}"
},
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "X-User-ID",
"value": "{{user_id}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"room_id\": \"group_1234567890\",\n \"message_type\": 0,\n \"content\": \"群聊中的一条文本消息\",\n \"duration\": 0\n}"
},
"url": {
"raw": "{{base_url}}/api/send",
"host": ["{{base_url}}"],
"path": ["api", "send"]
}
}
},
{
"name": "发送系统消息(群通知示例)",
"request": {
"method": "POST",
"header": [
{
"key": "Authorization",
"value": "Bearer {{token}}"
},
{
"key": "Content-Type",
"value": "application/json"
},
{
"key": "X-User-ID",
"value": "{{user_id}}"
}
],
"body": {
"mode": "raw",
"raw": "{\n \"room_id\": \"group_1234567890\",\n \"message_type\": 7,\n \"content\": \"用户 10004 加入了群聊\",\n \"extra\": \"{\\\"event\\\":\\\"member_join\\\",\\\"operator_id\\\":\\\"{{user_id}}\\\",\\\"target_id\\\":\\\"10004\\\"}\",\n \"duration\": 0\n}"
},
"url": {
"raw": "{{base_url}}/api/send",
"host": ["{{base_url}}"],
"path": ["api", "send"]
}
}
},
{
"name": "获取历史消息",
"request": {

View File

@@ -348,10 +348,19 @@ func main() {
authGroup.POST("/conversations/update", api.UpdateConversationHandler)
authGroup.POST("/conversations/delete", api.DeleteConversationHandler)
// 房间管理
// 房间 / 群聊管理
authGroup.POST("/rooms", api.CreateRoomHandler)
authGroup.GET("/rooms/:id", api.GetRoomHandler)
// 群聊相关接口
authGroup.POST("/groups", api.CreateChatGroupHandler)
authGroup.GET("/groups/:room_id", api.GetGroupInfoHandler)
authGroup.GET("/groups/:room_id/members", api.ListGroupMembersHandler)
authGroup.POST("/groups/:room_id/members", api.AddGroupMembersHandler)
authGroup.DELETE("/groups/:room_id/members/:user_id", api.RemoveGroupMemberHandler)
authGroup.PATCH("/groups/:room_id", api.UpdateGroupInfoHandler)
authGroup.PATCH("/groups/:room_id/members/:user_id/role", api.ChangeMemberRoleHandler)
// 附件管理
authGroup.POST("/attachments/upload", api.UploadAttachmentHandler)
authGroup.GET("/attachments", api.GetAttachmentsHandler)

View File

@@ -5,10 +5,12 @@
package api
import (
"xk-websocket-v2/internal/model"
"xk-websocket-v2/internal/service"
"xk-websocket-v2/internal/utils"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
/**
@@ -69,3 +71,236 @@ func GetRoomHandler(c *gin.Context) {
utils.SuccessWithData(c, room, "获取成功")
}
/**
* CreateChatGroupHandler
* 功能:创建群聊
* 路径POST /api/groups
*/
func CreateChatGroupHandler(c *gin.Context) {
userID, _ := c.Get("user_id")
var req struct {
Name string `json:"name" binding:"required"`
Avatar string `json:"avatar"`
MemberIDs []string `json:"member_ids" binding:"required"`
AdminIDs []string `json:"admin_ids"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.BadRequest(c, "参数错误: "+err.Error())
return
}
creatorID := userID.(string)
members := req.MemberIDs
// 确保创建者在成员列表中
found := false
for _, id := range members {
if id == creatorID {
found = true
break
}
}
if !found {
members = append(members, creatorID)
}
room, err := service.RoomSvc.CreateGroupRoom(creatorID, members, req.Name, req.Avatar, req.AdminIDs)
if err != nil {
utils.BadRequest(c, err.Error())
return
}
utils.SuccessWithData(c, room, "创建群聊成功")
}
/**
* GetGroupInfoHandler
* 功能:获取群聊基础信息及成员数量
* 路径GET /api/groups/:room_id
*/
func GetGroupInfoHandler(c *gin.Context) {
roomID := c.Param("room_id")
room, err := service.RoomSvc.GetRoom(roomID)
if err != nil {
if err == gorm.ErrRecordNotFound {
utils.NotFound(c, "群聊不存在")
return
}
utils.InternalError(c, "查询失败")
return
}
members, err := service.RoomSvc.ListGroupMembers(roomID)
if err != nil {
utils.InternalError(c, "查询成员失败")
return
}
// 统计管理员ID
adminIDs := make([]string, 0)
for _, m := range members {
if m.Role == 1 {
adminIDs = append(adminIDs, m.UserID)
}
}
utils.SuccessWithData(c, gin.H{
"room": room,
"member_count": len(members),
"admin_ids": adminIDs,
"owner_id": room.OwnerID,
}, "获取成功")
}
/**
* ListGroupMembersHandler
* 功能:获取群成员列表
* 路径GET /api/groups/:room_id/members
*/
func ListGroupMembersHandler(c *gin.Context) {
roomID := c.Param("room_id")
members, err := service.RoomSvc.ListGroupMembers(roomID)
if err != nil {
utils.InternalError(c, "查询失败")
return
}
utils.SuccessWithData(c, members, "获取成功")
}
/**
* AddGroupMembersHandler
* 功能:邀请/拉人入群
* 路径POST /api/groups/:room_id/members
*/
func AddGroupMembersHandler(c *gin.Context) {
userID, _ := c.Get("user_id")
roomID := c.Param("room_id")
var req struct {
MemberIDs []string `json:"member_ids" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.BadRequest(c, "参数错误: "+err.Error())
return
}
if err := service.RoomSvc.AddGroupMembers(roomID, userID.(string), req.MemberIDs); err != nil {
utils.BadRequest(c, err.Error())
return
}
utils.Success(c, "添加成员成功")
}
/**
* RemoveGroupMemberHandler
* 功能:移除群成员或退群
* 路径DELETE /api/groups/:room_id/members/:user_id
*/
func RemoveGroupMemberHandler(c *gin.Context) {
operatorID, _ := c.Get("user_id")
roomID := c.Param("room_id")
memberID := c.Param("user_id")
// 自己移除自己 = 退群
if operatorID.(string) == memberID {
if err := service.RoomSvc.QuitGroup(roomID, memberID); err != nil {
utils.BadRequest(c, err.Error())
return
}
utils.Success(c, "已退出群聊")
return
}
// 否则为管理员/群主踢人
if err := service.RoomSvc.RemoveGroupMember(roomID, operatorID.(string), memberID); err != nil {
utils.BadRequest(c, err.Error())
return
}
utils.Success(c, "已移除成员")
}
/**
* UpdateGroupInfoHandler
* 功能:修改群名称/头像
* 路径PATCH /api/groups/:room_id
*/
func UpdateGroupInfoHandler(c *gin.Context) {
userID, _ := c.Get("user_id")
roomID := c.Param("room_id")
var req struct {
Name string `json:"name"`
Avatar string `json:"avatar"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.BadRequest(c, "参数错误: "+err.Error())
return
}
// 仅群主或管理员可修改群信息
ok, err := service.RoomSvc.IsGroupAdminOrOwner(roomID, userID.(string))
if err != nil {
utils.InternalError(c, "权限校验失败")
return
}
if !ok {
utils.Forbidden(c, "无权限修改群信息")
return
}
updates := make(map[string]interface{})
if req.Name != "" {
updates["room_name"] = req.Name
}
if req.Avatar != "" {
updates["room_avatar"] = req.Avatar
}
if len(updates) == 0 {
utils.BadRequest(c, "没有可更新的字段")
return
}
// 更新群基础信息
if err := service.RoomSvc.DB.Model(&model.ChatRoom{}).
Where("room_id = ?", roomID).
Updates(updates).Error; err != nil {
utils.InternalError(c, "更新失败")
return
}
utils.Success(c, "更新成功")
}
/**
* ChangeMemberRoleHandler
* 功能:调整成员角色
* 路径PATCH /api/groups/:room_id/members/:user_id/role
*/
func ChangeMemberRoleHandler(c *gin.Context) {
operatorID, _ := c.Get("user_id")
roomID := c.Param("room_id")
memberID := c.Param("user_id")
var req struct {
Role int8 `json:"role" binding:"required"`
}
if err := c.ShouldBindJSON(&req); err != nil {
utils.BadRequest(c, "参数错误: "+err.Error())
return
}
if err := service.RoomSvc.ChangeMemberRole(roomID, operatorID.(string), memberID, req.Role); err != nil {
utils.BadRequest(c, err.Error())
return
}
utils.Success(c, "角色已更新")
}

View File

@@ -274,6 +274,8 @@ type RoomMember struct {
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"`
// 成员角色0-成员 1-管理员 2-群主
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"`
}
@@ -570,4 +572,16 @@ const (
// 文件大小限制(字节)
MaxImageSize = 10 * 1024 * 1024 // 10MB
MaxVideoSize = 500 * 1024 * 1024 // 500MB
// 消息类型常量
MessageTypeText = 0 // 文本
MessageTypeImage = 1 // 图片
MessageTypeAudio = 2 // 语音
MessageTypeVideo = 3 // 视频
MessageTypeSystem = 4 // 系统消息
MessageTypeFriendNotif = 5 // 好友通知
MessageTypeSignal = 6 // 信令消息
MessageTypeGroupNotif = 7 // 群通知
MessageTypeFile = 8 // 文件
MessageTypeMoments = 9 // 朋友圈通知(预留)
)

View File

@@ -124,7 +124,7 @@ func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model
// 3. 消息持久化 (MySQL)
// WebRTC 的 Candidate/Offer/Answer 信令通常不需要持久化,只有聊天记录和通话记录需要
if req.MessageType != 6 && req.MessageType != 7 { // 假设 6,7 是纯信令流
if req.MessageType != model.MessageTypeSignal && req.MessageType != model.MessageTypeGroupNotif {
msg := model.ChatMessage{
RoomID: req.RoomID,
SenderUserID: senderClient.UserID,
@@ -142,12 +142,16 @@ func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model
}
// 4. 更新会话列表(发送方与接收方)
if ConversationSvc != nil {
// 当前实现仅支持点对点私聊
// 目前仅对点对点私聊做会话维护,群聊和系统类消息先不更新会话,后续按需扩展
if ConversationSvc != nil && req.MessageType == model.MessageTypeText ||
req.MessageType == model.MessageTypeImage ||
req.MessageType == model.MessageTypeAudio ||
req.MessageType == model.MessageTypeVideo ||
req.MessageType == model.MessageTypeFile {
senderID := senderClient.UserID
receiverID := req.ReceiverUserID
// 对发送方更新会话(不增加未读)
if senderID != "" && receiverID != "" {
// sender 视角 target 为 receiver
_ = ConversationSvc.UpsertConversationOnMessage(senderID, receiverID, req.RoomID, msg, true)
@@ -164,7 +168,23 @@ func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model
}
msgBytes, _ := json.Marshal(pushMsg)
// 分发消息 (路由到目标用户)
// 分发消息 (路由到目标用户或群成员)
if req.RoomID != "" {
// 判断是否群聊房间,群聊需要按成员广播
room, err := RoomSvc.GetRoom(req.RoomID)
if err == nil && room.RoomType == "group" {
memberIDs, mErr := RoomSvc.GetRoomMembers(req.RoomID)
if mErr == nil {
for _, uid := range memberIDs {
// 群聊中仍然按用户路由,发送给所有成员
s.DispatchMessage(uid, msgBytes)
}
return
}
}
}
// 默认:点对点或无法识别房间类型时按 receiver_user_id 路由
s.DispatchMessage(req.ReceiverUserID, msgBytes)
} else {
// 信令消息直接透传,不存库

View File

@@ -207,3 +207,241 @@ func (s *RoomService) RemoveRoomMember(roomID, userID string) error {
Delete(&model.RoomMember{}).Error
}
/**
* CreateGroupRoom
* 功能:创建群聊房间,并为成员写入角色(群主/管理员/成员)
*/
func (s *RoomService) CreateGroupRoom(creatorID string, memberIDs []string, name string, avatar string, adminIDs []string) (*model.ChatRoom, error) {
if len(memberIDs) == 0 {
return nil, fmt.Errorf("成员列表不能为空")
}
// 1. 先通过通用的 CreateRoom 创建群聊和基础成员关系(角色默认都是 0
room, err := s.CreateRoom("group", memberIDs, creatorID)
if err != nil {
return nil, err
}
// 2. 更新群名称和头像(如果提供)
updates := map[string]interface{}{}
if name != "" {
updates["room_name"] = name
}
if avatar != "" {
updates["room_avatar"] = avatar
}
if len(updates) > 0 {
if err := s.DB.Model(room).Updates(updates).Error; err != nil {
return nil, err
}
}
// 3. 为成员设置角色:群主=2 管理员=1 成员=0
roomID := room.RoomID
// 创建者作为群主
if err := s.DB.Model(&model.RoomMember{}).
Where("room_id = ? AND user_id = ?", roomID, creatorID).
Update("role", 2).Error; err != nil {
return nil, err
}
// 管理员列表去重并排除群主
adminSet := map[string]struct{}{}
for _, id := range adminIDs {
if id == "" || id == creatorID {
continue
}
adminSet[id] = struct{}{}
}
if len(adminSet) > 0 {
for adminID := range adminSet {
if err := s.DB.Model(&model.RoomMember{}).
Where("room_id = ? AND user_id = ?", roomID, adminID).
Update("role", 1).Error; err != nil {
return nil, err
}
}
}
return room, nil
}
/**
* 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 {
return nil, err
}
return members, nil
}
/**
* ListUserGroups
* 功能:查询用户所在的所有群聊房间
*/
func (s *RoomService) ListUserGroups(userID string) ([]model.ChatRoom, error) {
var rooms []model.ChatRoom
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 rooms, nil
}
/**
* AddGroupMembers
* 功能:批量添加群成员(仅群主或管理员可操作)
*/
func (s *RoomService) AddGroupMembers(roomID, operatorID string, memberIDs []string) error {
if len(memberIDs) == 0 {
return nil
}
// 权限校验:必须是群主或管理员
isAdmin, err := s.isGroupAdminOrOwner(roomID, operatorID)
if err != nil {
return err
}
if !isAdmin {
return fmt.Errorf("无权限添加成员")
}
for _, uid := range memberIDs {
if uid == "" {
continue
}
member := model.RoomMember{
RoomID: roomID,
UserID: uid,
Role: 0, // 默认成员
}
if err := s.DB.Create(&member).Error; err != nil {
return err
}
}
return nil
}
/**
* RemoveGroupMember
* 功能:踢出群成员(群主或管理员),或作为封装调用 RemoveRoomMember
*/
func (s *RoomService) RemoveGroupMember(roomID, operatorID, memberID string) error {
// 群主或管理员才能踢人
isAdmin, err := s.isGroupAdminOrOwner(roomID, operatorID)
if err != nil {
return err
}
if !isAdmin {
return fmt.Errorf("无权限移除成员")
}
// 不允许移除群主
room, err := s.GetRoom(roomID)
if err != nil {
return err
}
if memberID == room.OwnerID {
return fmt.Errorf("不能移除群主")
}
return s.RemoveRoomMember(roomID, memberID)
}
/**
* QuitGroup
* 功能:成员主动退出群聊
*/
func (s *RoomService) QuitGroup(roomID, userID string) error {
room, err := s.GetRoom(roomID)
if err != nil {
return err
}
// 群主不能直接退群
if userID == room.OwnerID {
return fmt.Errorf("群主不能直接退出群聊,请先转让群主或解散群")
}
return s.RemoveRoomMember(roomID, userID)
}
/**
* ChangeMemberRole
* 功能:修改成员角色(仅群主可设置/取消管理员)
*/
func (s *RoomService) ChangeMemberRole(roomID, operatorID, memberID string, role int8) error {
// 仅允许 0/1/2
if role < 0 || role > 2 {
return fmt.Errorf("非法角色值")
}
room, err := s.GetRoom(roomID)
if err != nil {
return err
}
// 只有群主可以调整角色
if operatorID != room.OwnerID {
return fmt.Errorf("只有群主可以调整成员角色")
}
// 防止将群主本人的角色修改为其他值
if memberID == room.OwnerID && role != 2 {
return fmt.Errorf("不能修改群主自身角色")
}
return s.DB.Model(&model.RoomMember{}).
Where("room_id = ? AND user_id = ?", roomID, memberID).
Update("role", role).Error
}
/**
* IsGroupAdminOrOwner
* 功能:对外暴露的权限判断:是否为群主或管理员
*/
func (s *RoomService) IsGroupAdminOrOwner(roomID, userID string) (bool, error) {
return s.isGroupAdminOrOwner(roomID, userID)
}
/**
* IsGroupOwner
* 功能:判断用户是否为群主
*/
func (s *RoomService) IsGroupOwner(roomID, userID string) (bool, error) {
room, err := s.GetRoom(roomID)
if err != nil {
if err == gorm.ErrRecordNotFound {
return false, nil
}
return false, err
}
return room.OwnerID == userID, nil
}
/**
* isGroupAdminOrOwner
* 功能:判断用户是否是群主或管理员
*/
func (s *RoomService) isGroupAdminOrOwner(roomID, userID string) (bool, error) {
if userID == "" {
return false, nil
}
var member model.RoomMember
err := s.DB.Where("room_id = ? AND user_id = ?", roomID, userID).First(&member).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
return false, nil
}
return false, err
}
return member.Role == 1 || member.Role == 2, nil
}

View File

@@ -132,6 +132,7 @@ 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',
`role` tinyint(1) NOT NULL DEFAULT 0 COMMENT '成员角色(0=成员,1=管理员,2=群主)',
`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