Golang 17 gin web项目实战 IM系统(6) 好友功能 心跳检测

44

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

之前的私聊算陌生人聊天,应该采用redis什么的来保持会话状态来通知对方本人是否在线,那都是后话,现在做好友相关功能。

好友

表设计

  1. 好友 Friend Struct
type Friend struct {
	gorm.Model
	UserId   uint `json:"user_id"`   // 用户id
	FriendId uint `json:"friend_id"` // 好友id
}
  1. 好友组 FriendGroup Struct
#package model

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

type FriendGroup struct {
	gorm.Model
	UserId       uint          `json:"user_id"` // 用户id
	Name         *string       `json:"name"`    // 群组名称
	FriendIdList *FriendIdList `json:"friend_id_list"`
}

// 群组成员id列表
type FriendIdList []uint

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

  1. 好友申请 FriendRequest Struct
package model

import "gorm.io/gorm"

type FriendRequest struct {
    gorm.Model
    UserId   uint   `json:"user_id"`
    FriendId uint   `json:"friend_id"`
    Status   Status `json:"status"`
}

接口

具体服务编写还是有点复杂,但都是基础建设性代码,没有什么思路壁障。

接口名 请求路径 参数说明 响应体说明
添加好友 POST /friend/add 请求体为 []uint
,元素为好友用户ID列表
成功无数据返回,失败返回错误信息
获取好友申请列表 GET /friend/list_req 无需参数,使用JWT中用户ID 返回好友申请列表 []FriendRequestVo
处理好友申请 POST /friend/handle_req json<br>{ "id": int64, "status": int }
status为1=同意,2=拒绝
成功无数据返回,失败返回错误信息
删除好友 POST /friend/remove 请求体为 []int64
,元素为要删除的好友ID列表
成功无数据返回,失败返回错误信息
创建好友分组 POST /friend/group_create { "name": string,
"friend_id_list":[]int }
分组名称
成功无数据返回,失败返回错误信息
删除好友分组 GET /friend/group_delete?id=123 Query参数:id
为分组ID
成功无数据返回,失败返回错误信息
修改好友分组 POST /friend/group_update { "id": int64, "name": string, "friend_id_list":[]int} 成功无数据返回,失败返回错误信息
获取好友分组列表 GET /friend/group_list 无需参数,使用JWT中用户ID 返回用户所有分组列表 []FriendGroupVo
,含组名与组内好友

心跳检测

实现思路

  • 客户端上线后每 固定间隔(如10秒) 向服务端发送心跳消息;
  • 服务端收到心跳后,记录用户最后心跳时间(heartbeat_time);
  • 每隔固定时间(如每30秒),后端定时任务扫描数据库,将长时间(如30秒)未发送心跳的用户设为「离线」;
  • 用户当前状态为离线时,收到心跳会自动置为在线,若为「忙碌/离开」则不更改(由用户自己设置的状态);

具体实现

MessageHandler 方法新增监听事件 heartbeat:

func (ws *WebSocketHandler) MessageHandler(id int64, messageBytes []byte) {
	// 处理消息
	message := &wsMessage.Message{}
	err := jsonUtil.UnmarshalValue(messageBytes, message)
	if err != nil {
		wsClient.WebSocketClient.SendMessageToOne(id, &model.Response{
			Code:    http.StatusBadRequest,
			Message: "数据格式错误",
			Data:    nil,
		})
		return
	}
	switch message.Type {
	case wsMessage.Chat:
		ws.ChatHandler(message.SendId, message.Data)
	case wsMessage.HeartBeat:
		ws.HeartBeatHandler(message.SendId, message.Data)
	default:
		wsClient.WebSocketClient.SendMessageToOne(id, &model.Response{
			Code:    http.StatusBadRequest,
			Message: "数据格式错误",
			Data:    nil,
		})
	}
}

HeartBeatHandler方法实现:

func (ws *WebSocketHandler) HeartBeatHandler(sendId int64, _ interface{}) {
	timestamp := time.Now().Unix()
	err := ws.userService.UpdateHeartbeatTime(sendId, timestamp)
	if err != nil {
		wsClient.WebSocketClient.SendMessageToOne(sendId, &model.Response{
			Code:    http.StatusInternalServerError,
			Message: err.Error(),
			Data:    nil,
		})
		return
	}
	// 返回心跳确认
	wsClient.WebSocketClient.SendMessageToOne(sendId, &model.Response{
		Code:    http.StatusOK,
		Message: "success",
		Data: &wsMessage.Message{
			SendId: sendId,
			Type:   wsMessage.HeartBeatAck,
			Data:   nil,
			Time:   time.Now(),
		},
	})
}

定时器扫描心跳停止用户并下线(注册定时器同其他中间件在bootstrap调用init):
func HeartBeatTimer() {
	_, err := Timer.AddFunc("*/30 * * * * *", func() {
		logrus.Infof("心跳检测任务开始执行: %v", time.Now())
		err := service.UserServiceInstance.CheckOfflineUsers() // 你心跳检测的函数
		if err != nil {
			logrus.Errorf("心跳检测执行失败: %v", err)
		}
	})
	if err != nil {
		logrus.Error("定时任务(%v)添加失败:", "HeartBeatTimer", err)
		return
	}
}

Timer.go,在bootstrap.go的Start 方法调用:

package timer

import (
	"github.com/robfig/cron/v3"
)

var Timer = cron.New(cron.WithSeconds())

func InitTimer() {
	HeartBeatTimer()
	Timer.Start()
}

仓库方法:
func (r *UserRepository) UpdateHeartbeatTime(userId int64, heartbeatTime int64, tx ...*gorm.DB) error {
	// 仅当状态为离线时,才自动改为在线
	return gormDB.Model(&model.User{}).
		Where("id = ? AND status = ?", userId, 0).
		Updates(map[string]interface{}{
			"heartbeat_time": heartbeatTime,
			"status":         1,
		}).Error
}

下一步是群组基础功能建设(拉人、禁言等),在这之后才进入聊天消息核心功能开发

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

导航