Golang 15 gin web项目实战 IM系统(4) 消息发送与接收

42

2025/7/23 15:00 - 2025/7/23 23:00

消息发送功能

Ws.Message Struct

这是ws消息的统一格式

package ws

type Message struct {
	Type   string      `json:"type"`    // 事件类型
	SendId int64       `json:"send_id"` // 发送者ID
	Data   interface{} `json:"data"`    // 具体数据
}

// 事件类型
const (
	Chat      = "chat"       //聊天
	ChatAck   = "chat_ack"   // 聊天确认
	Online    = "online"     // 上线
	Offline   = "offline"    // 下线
	Recall    = "recall"     //  撤回
	IdRequest = "id_request" // 请求获取真实ID,引入mq之后采用
)

model.Message Struct

这是聊天消息的格式,一开始没想到消息不止聊天消息而把message用掉了,那么其他像邮件消息、通知消息就只能另外取名建表了

package model

import (
	"database/sql/driver"
	"encoding/json"
	"go-chat/internal/utils/jsonUtil"
	"gorm.io/gorm"
)

// 消息结构体
type Message struct {
	gorm.Model
	SenderId     int64            `json:"sender_id" gorm:"not null;comment:发送者ID"`          // 发送者ID(必填)
	ReceiverId   *int64           `json:"receiver_id" gorm:"comment:接收者ID(私聊使用)"`           // 接收者ID(仅用于私聊)
	GroupId      *int64           `json:"group_id" gorm:"comment:群组ID(群聊使用)"`               // 群组ID(仅用于群聊)
	ReplyId      *int64           `json:"reply_id" gorm:"comment:回复的消息ID"`                  // 回复消息ID
	ReaderIdList *ReaderIdList    `json:"reader_id_list" gorm:"type:json;comment:已读用户ID列表"` // 已读用户ID数组,JSON 存储
	TargetType   *TargetType      `json:"target_type" gorm:"not null;comment:消息目标类型"`       // 消息目标类型(0=私聊,1=群聊)
	Content      *MessagePartList `json:"content" gorm:"type:json;comment:富文本消息内容"`         // 消息内容片段数组(JSON)
	Type         *MessageType     `json:"type" gorm:"not null;comment:消息类型"`                // 消息类型(文本、图片、红包等)
	Status       *Status          `json:"status" gorm:"not null;comment:消息状态"`              // 消息状态(0=撤回,1=正常)
	ExtraData    *json.RawMessage `json:"extra_data" gorm:"type:json;comment:扩展字段"`         // 扩展字段(如红包、投票等结构)
}

func (m *Message) TableName() string {
	return "messages"
}

// InitFields 初始化数据
func (m *Message) InitFields() {
	// 处理 Status 字段
	if m.Status == nil {
		m.Status = new(Status) // 为 Status 分配内存
		*m.Status = Enable     // 然后赋值
	}
	// 处理 ReaderIdList 字段
	if m.ReaderIdList == nil {
		m.ReaderIdList = new(ReaderIdList)
		*m.ReaderIdList = make(ReaderIdList, 0)
	}
}

type MessageType int

const (
	TextContent MessageType = iota // 聊天消息

	ImageContent // 图片

	VoiceContent // 语音

	RedBagContent //红包
)

type TargetType int

const (
	PrivateTarget TargetType = iota // 私聊
	GroupTarget                     // 群聊
)

type ContentType string

const (
	Text  ContentType = "text"  // 文本
	Emoji ContentType = "emoji" // 表情
	Image ContentType = "image" // 图片
	Link  ContentType = "link"  // 链接
)

type MessagePart struct {
	Type    ContentType `json:"type"`    // 内容类型(text, emoji, image, link)
	Content *string     `json:"content"` // 内容(如文本、图片 URL、链接等)
}

type MessagePartList []*MessagePart

func (parts MessagePartList) Value() (driver.Value, error) {
	return jsonUtil.MarshalValue(parts)
}

func (parts *MessagePartList) Scan(value interface{}) error {
	return jsonUtil.UnmarshalValue(value, parts)
}

type ReaderIdList []uint

func (ids ReaderIdList) Value() (driver.Value, error) {
	return jsonUtil.MarshalValue(ids)
}
func (ids *ReaderIdList) Scan(value interface{}) error {
	return jsonUtil.UnmarshalValue(value, ids)
}

QueryMessagesResponse Struct

聊天消息查询返回体,采用游标时间倒序查询,IM系统的消息从下往上翻

package model

import (
    "encoding/json"
    "go-chat/internal/model"
    "time"
)

// QueryMessagesResponse 查询消息列表
type QueryMessagesResponse struct {
    List    []*MessageVo `json:"list"`     // 消息列表
    Cursor  int64        `json:"cursor"`   // 下一页游标(最小 id)
    HasMore bool         `json:"has_more"` // 是否还有更多数据
}

type MessageVo struct {
    ID           uint
    CreatedAt    time.Time
    UpdatedAt    time.Time
    SenderId     int64
    ReceiverId   *int64
    GroupId      *int64
    ReplyId      *int64
    ReaderIdList *model.ReaderIdList
    TargetType   *model.TargetType
    Content      *model.MessagePartList
    Type         *model.MessageType
    Status       *model.Status
    ExtraData    *json.RawMessage
    //额外信息
    SenderNickName     *string
    SenderAvatar       *string
    SenderOnlineStatus *model.OnlineStatus
    IsRead             bool
}

func (m *MessageVo) GetFieldsFromMessage(msg *model.Message) {
    m.ID = msg.ID
    m.CreatedAt = msg.CreatedAt
    m.UpdatedAt = msg.UpdatedAt
    m.SenderId = msg.SenderId
    m.ReceiverId = msg.ReceiverId
    m.GroupId = msg.GroupId
    m.ReplyId = msg.ReplyId
    m.ReaderIdList = msg.ReaderIdList
    m.TargetType = msg.TargetType
    m.Content = msg.Content
    m.Type = msg.Type
    m.Status = msg.Status
    m.ExtraData = msg.ExtraData
}

接口

接口名 请求路径 参数 响应体
已读消息 /message/read {
"message_id":2,
"user_id":3
}
200 { "code":200,
"message":"ok"
"data":nil }
查询聊天记录 /message/query plain // QueryMessagesRequest 查询消息列表请求参数 type QueryMessagesRequest struct { TargetId uint `json:"target_id" binding:"required"` // 目标ID 好友id或群组id TargetType *model.TargetType `json:"target_type" binding:"required"` // 目标类型 私聊或群聊 Cursor uint `json:"cursor"` // 游标 上次查询的最小id 默认为0 Limit int `json:"limit"` // 限制数量 //以下为拓展,暂时不会做 MessageTypes *[]model.MessageType `json:"message_types"` // 只查特定类型的消息(如图片、文本) Keyword *string `json:"keyword"` // 模糊搜索 StartTime time.Time `json:"start_time"` // 起始时间 EndTime time.Time `json:"end_time"` // 结束时间 } 200
{ "code": 200, "message": "success", "data": { "list": [ { "ID": 21, "CreatedAt": "2025-07-23T23:14:03.68+08:00", "UpdatedAt": "2025-07-23T23:14:03.68+08:00", "SenderId": 3, "ReceiverId": 3, "GroupId": null, "ReplyId": null, "ReaderIdList": [], "TargetType": 0, "Content": [ { "type": "text", "content": "I'm fine, thanks." } ], "Type": 0, "Status": 1, "ExtraData": null, "SenderNickName": null, "SenderAvatar": null, "SenderOnlineStatus": null, "IsRead": false } ], "cursor": 21, "has_more": false } }

ws中的处理

IM系统不依赖http接口,而是实时在ws服务器上处理消息调用服务,以下是处理消息的思路和示例代码

连接进入服务器

在项目入口调用manager.InitWebSocket()就能启动ws服务器,客户端ws连接指定url即可连接服务器,

连接之后触发函数onOpen,这里可以追加心跳检测等机制 ,onClose、onError函数监听客户端ws连接情况。

服务器处理客户端消息

客户端向服务端发送的消息在ws.MessageHandler(msg)中统一处理

package manager

import (
	"github.com/gorilla/websocket"
	"go-chat/internal/utils/logUtil"
	"go-chat/internal/ws"
	"net/http"
	"strconv"
)

// 初始化 WebSocket
func InitWebSocket() {
	ws.WebSocketClient = &ws.WebSocketManager{
		Upgrader: websocket.Upgrader{
			CheckOrigin: func(r *http.Request) bool {
				// 这里可以加检查来源的逻辑
				return true
			},
		},
	}
	// 监听客户端连接
	http.HandleFunc("/ws", handleWebSocket)

	// 启动 WebSocket 服务
	go func() {
		ws.WebSocketClient.Server = &http.Server{Addr: ":80"}
		if err := ws.WebSocketClient.Server.ListenAndServe(); err != nil {
			logUtil.Errorf("WebSocket 服务启动失败: %s", err)
		}
	}()
	logUtil.Infof("WebSocket 服务已启动")
}

// WebSocket 连接处理
func handleWebSocket(w http.ResponseWriter, r *http.Request) {
	conn, err := ws.WebSocketClient.Upgrader.Upgrade(w, r, nil)
	if err != nil {
		logUtil.Errorf("WebSocket 连接失败: %s", err)
		return
	}
	defer conn.Close()

	// 获取用户ID
	id, err := strconv.ParseInt(r.URL.Query().Get("id"), 10, 64)
	if err != nil {
		conn.WriteMessage(websocket.TextMessage, []byte("id is required"))
		return
	}
	// 存储连接
	ws.WebSocketClient.Connections.Store(id, conn)
	// 连接成功后的回调
	onOpen(conn, id)
	// 处理 WebSocket 消息
	for {
		// 读取消息
		_, msg, err := conn.ReadMessage()
		if err != nil {
			onError(conn, id, err)
			break
		}
		// 消息处理函数
		ws.MessageHandler(msg)
	}
	// 连接断开时调用 onClose 并删除连接
	onClose(conn, id)
}

func onOpen(conn *websocket.Conn, id int64) {
	logUtil.Infof("WebSocket 客户端(%v)已连接: %s", conn.RemoteAddr(), id)
	//todo 更新心跳 时间
	ws.WebSocketClient.SendMessageToOne(id, "连接成功")
}

func onClose(conn *websocket.Conn, id int64) {
	logUtil.Infof("WebSocket 客户端(%v)已断开: %s", id, conn.RemoteAddr())
	ws.WebSocketClient.Connections.Delete(id)
}

func onError(conn *websocket.Conn, id int64, err error) {
	logUtil.Infof("WebSocket 客户端(%v)%s发生错误:%v", id, conn.RemoteAddr(), err)
}

ws.MessageHandler(msg)

解析统一数据结构,然后根据type走到不同的消息处理分支上

package ws

import (
	"go-chat/internal/model"
	"go-chat/internal/utils/jsonUtil"
	"go-chat/internal/utils/logUtil"
	"net/http"
)

func MessageHandler(messageBytes []byte) {
	// 处理消息
	wsMessage := &Message{}
	err := jsonUtil.UnmarshalValue(messageBytes, wsMessage)
	if err != nil {
		logUtil.Errorf("消息反序列化失败: %s", err)
		return
	}
	switch wsMessage.Type {
	case Chat:
		ChatMessageHandler(wsMessage.SendId, wsMessage.Data)
	default:
		WebSocketClient.SendMessageToOne(wsMessage.SendId, &model.Response{
			Code:    http.StatusBadRequest,
			Message: "数据格式错误",
			Data:    nil,
		})
	}
}

ChatMessageHandler 是聊天消息处理函数

package ws

import (
	"fmt"
	"go-chat/internal/model"
	"go-chat/internal/service"
	"go-chat/internal/utils/jsonUtil"
	"net/http"
)

func ChatMessageHandler(sendId int64, data interface{}) {
	bytes, err := jsonUtil.MarshalValue(data)
	if err != nil {
		WebSocketClient.SendMessageToOne(sendId, &model.Response{
			Code:    http.StatusBadRequest,
			Message: "数据格式错误",
			Data:    nil,
		})
		return
	}

	var message = &model.Message{}
	if err := jsonUtil.UnmarshalValue(bytes, message); err != nil {
		WebSocketClient.SendMessageToOne(sendId, &model.Response{
			Code:    http.StatusBadRequest,
			Message: "数据格式错误,无法反序列化成消息",
			Data:    nil,
		})
		return
	}
	message.InitFields()
	messageService := service.GetMessageService()
	vo, err := messageService.SendMessage(message)
	if err != nil {
		WebSocketClient.SendMessageToOne(sendId, &model.Response{
			Code:    http.StatusBadRequest,
			Message: err.Error(),
			Data:    nil,
		})
		return
	}
	//消息发送后返回发送者:
	WebSocketClient.SendMessageToOne(sendId, &model.Response{
		Code:    http.StatusOK,
		Message: "success",
		Data: &Message{
			SendId: sendId,
			Type:   ChatAck,
			Data:   vo,
		},
	})
	//消息发送后返回接收者,这里接收者私聊或群聊处理方式不同:
	if *message.TargetType == model.PrivateTarget {
		WebSocketClient.SendMessageToOne(*message.ReceiverId, &model.Response{
			Code:    http.StatusOK,
			Message: "success",
			Data: &Message{
				SendId: sendId,
				Type:   Chat,
				Data:   vo,
			},
		})
	} else {
		//3.todo 找到这个群组遍历成员,消息推送给其中在线的成员

	}
}

客户端接收服务器消息

通过 `WebSocketClient.SendMessageToOne` 等方法,服务器向客户端(本连接、其他在线连接)发送消息,发送的就是http接口标准格式。

接下来只需要开发聊天过程的交互功能就实现了服务器和客户端实时通信场景开发,比如:

  • 我发送消息给好友,我要收到发送成功的消息(将这条消息组件插入聊天窗口中并显示发送时间等),好友要收到我发送的消息(在聊天框就显示这个条新消息,不在则侧边栏红点,预览局部消息),前端收到对应事件(chat,chat_ack)的ws消息后对UI进行处理
  • 消息撤回,我对一条消息进行了撤回操作,客户端得收到撤回成功的消息,好友要收到我撤回了哪条消息的消息,前端收到recall事件处理UI

聊天消息实现

私聊

1.5.3 中已经展示出部分聊天消息的代码,可直接测试,随便找个ws测试网站开两个号

用户 3向用户 4发送消息,消息体Content嵌合体由客户端进行拆分后传入服务器

用户3收到消息发送成功的消息chat_ack,并返回了入库后的消息(携带ID,方便后面的撤回等功能)

用户4收到消息

客户端监听到 chat 事件后拿到data,聊天消息的消息体为Conent是一个嵌合体,客户端按顺序解析嵌合体中的text,image,emoji等消息显示。

群聊

package wsHandler

import (
	"go-chat/internal/model"
	"go-chat/internal/service"
	"go-chat/internal/utils"
	"go-chat/internal/utils/jsonUtil"
	wsClient "go-chat/internal/ws/client"
	wsMessage "go-chat/internal/ws/message"
	"net/http"
	"time"
)

func ChatHandler(sendId int64, data interface{}) {
	...
	//消息发送后返回接收者,这里接收者私聊或群聊处理方式不同:
	... //群发消息
    else if *message.TargetType == model.GroupTarget {
		memberList, err := service.GroupServiceInstance.Member(uint(*message.GroupId))
		if err != nil {
			wsClient.WebSocketClient.SendMessageToOne(sendId, &model.Response{
				Code:    http.StatusInternalServerError,
				Message: err.Error(),
				Data:    nil,
			})
			return
		}
		onlineUserIds := wsClient.WebSocketClient.GetOnlineUserIds()
		groupOnlineUserIds := make([]int64, 0)
		for _, member := range memberList {
			if utils.Contains(onlineUserIds, int64(member.UserId)) && member.OnlineStatus == model.Online {
				groupOnlineUserIds = append(groupOnlineUserIds, int64(member.UserId))
			}
		}
		wsClient.WebSocketClient.SendMessageToMultiple(groupOnlineUserIds, &model.Response{
			Code:    http.StatusOK,
			Message: "success",
			Data: &wsMessage.Message{
				SendId: sendId,
				Type:   wsMessage.Chat,
				Data:   vo,
				Time:   time.Now(),
			},
		})
	}
}

posted on 2025-07-25 11:54  依只  阅读(25)  评论(0)    收藏  举报

导航