Go-Zero 项目开发22:用户群聊功能的实现与完善

纲要

  • 消息存储模型 :基于读扩散,一条消息只存一份,通过 type 字段区分私聊/群聊,receiver_id 在群聊时指向群 ID
  • 会话管理 :用户创建群或加入群时,由 im 服务创建群会话,并维护用户与群的会话关系。
  • 消息推送与并发优化 :利用 go-zero 内置的线程工具实现群消息的并发发送,避免因群成员数量大导致的延迟。
  • 消息队列处理 :在 taskMQ 中增加群聊分支,调用社交服务获取群成员列表,完成消息扩散与落地。
  • 服务协作 :社交 API 服务在创建群、申请进群、处理群申请等成功回调中,通过 RPC 调用 im 服务建立会话。
  • 涉及技术栈go-zerogo-zero/core/threadingWebSocketRedisMySQLRPC

消息存储与扩散模型

群聊消息采用读扩散 方案:所有群成员共享同一条消息记录,避免为每个用户存储一份副本。与私聊相同,消息记录在同一张 chat_log 表中,通过两个字段区分场景:

  • type:消息类型,枚举值为 private(私聊)和 group(群聊)。
  • receiver_id:接收者 ID,私聊时为对方的用户 ID,群聊时替换为群 ID。

这样,客户端拉取群历史消息时,只需按群 ID 和消息类型查询即可获得完整的群聊记录,无需在写路径上为每个成员维护独立的收件箱。

会话的建立与管理

创建时机

会话的触发来源于两个入口:

  1. 创建群 :创建者发起创建群操作后,社交服务需要同时为群本身创建者与群之间建立会话。
  2. 加入群:新成员通过申请并被批准后,社交服务需要为该用户与群建立会话。

无论在哪个入口,最终都通过 im 服务提供的 RPC 接口完成会话的初始化。

时序梳理

数据库 IM RPC 社交 RPC 社交 API 客户端 数据库 IM RPC 社交 RPC 社交 API 客户端 #mermaid-svg-q8SkvoBl2BF9UaNG{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-q8SkvoBl2BF9UaNG .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-q8SkvoBl2BF9UaNG .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-q8SkvoBl2BF9UaNG .error-icon{fill:#552222;}#mermaid-svg-q8SkvoBl2BF9UaNG .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-q8SkvoBl2BF9UaNG .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-q8SkvoBl2BF9UaNG .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-q8SkvoBl2BF9UaNG .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-q8SkvoBl2BF9UaNG .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-q8SkvoBl2BF9UaNG .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-q8SkvoBl2BF9UaNG .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-q8SkvoBl2BF9UaNG .marker{fill:#333333;stroke:#333333;}#mermaid-svg-q8SkvoBl2BF9UaNG .marker.cross{stroke:#333333;}#mermaid-svg-q8SkvoBl2BF9UaNG svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-q8SkvoBl2BF9UaNG p{margin:0;}#mermaid-svg-q8SkvoBl2BF9UaNG .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-q8SkvoBl2BF9UaNG text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-q8SkvoBl2BF9UaNG .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-q8SkvoBl2BF9UaNG .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-q8SkvoBl2BF9UaNG .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-q8SkvoBl2BF9UaNG .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-q8SkvoBl2BF9UaNG #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-q8SkvoBl2BF9UaNG .sequenceNumber{fill:white;}#mermaid-svg-q8SkvoBl2BF9UaNG #sequencenumber{fill:#333;}#mermaid-svg-q8SkvoBl2BF9UaNG #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-q8SkvoBl2BF9UaNG .messageText{fill:#333;stroke:none;}#mermaid-svg-q8SkvoBl2BF9UaNG .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-q8SkvoBl2BF9UaNG .labelText,#mermaid-svg-q8SkvoBl2BF9UaNG .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-q8SkvoBl2BF9UaNG .loopText,#mermaid-svg-q8SkvoBl2BF9UaNG .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-q8SkvoBl2BF9UaNG .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-q8SkvoBl2BF9UaNG .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-q8SkvoBl2BF9UaNG .noteText,#mermaid-svg-q8SkvoBl2BF9UaNG .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-q8SkvoBl2BF9UaNG .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-q8SkvoBl2BF9UaNG .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-q8SkvoBl2BF9UaNG .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-q8SkvoBl2BF9UaNG .actorPopupMenu{position:absolute;}#mermaid-svg-q8SkvoBl2BF9UaNG .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-q8SkvoBl2BF9UaNG .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-q8SkvoBl2BF9UaNG .actor-man circle,#mermaid-svg-q8SkvoBl2BF9UaNG line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-q8SkvoBl2BF9UaNG :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} alt会话不存在会话已存在 创建群/审批加入执行群业务逻辑返回群 IDCreateGroupConversation(groupId, userId)查询群会话是否已存在插入群会话记录为用户插入群会话关系成功直接返回操作完成

项目结构速览

dir 复制代码
apps/
├─ social/
│  ├─ api/                  # 社交 API 服务 
│  │  ├─ internal/
│  │  │  ├─ config/
│  │  │  ├─ logic/          # 创建群、申请群、处理申请等逻辑 
│  │  │  └─ svc/
│  │  └─ social.api 
│  └─ rpc/                  # 社交 RPC 服务 
│     ├─ internal/
│     │  ├─ logic/          # GetGroupUserList 等 
│     │  └─ svc/
│     └─ social.proto 
└─ im/
   └─ rpc/                  # IM RPC 服务 
      ├─ internal/
      │  ├─ config/
      │  ├─ logic/          # CreateGroupConversation 等 
      │  ├─ mq/             # taskMQ 消费者 
      │  ├─ server/         # WebSocket 连接管理、并发推送 
      │  └─ svc/
      ├─ model/             # 会话、用户会话模型 
      └─ im.proto 

代码实现:IM 服务中的会话逻辑

以下代码位于 im 的 RPC 服务中,负责创建群会话并关联用户会话列表。

文件:internal/logic/creategroupconversationlogic.go

go 复制代码
package logic 
 
import (
    "context"
    "database/sql"
 
    "github.com/pkg/errors"
    "go-zero-shop/apps/im/rpc/internal/svc"
    "go-zero-shop/apps/im/rpc/pb"
 
    "github.com/zeromicro/go-zero/core/logx"
)
 
type CreateGroupConversationLogic struct {
    ctx    context.Context 
    svcCtx *svc.ServiceContext 
    logx.Logger 
}
 
func NewCreateGroupConversationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateGroupConversationLogic {
    return &CreateGroupConversationLogic{
        ctx:    ctx,
        svcCtx: svcCtx,
        Logger: logx.WithContext(ctx),
    }
}
 
// CreateGroupConversation 创建群会话 
func (l *CreateGroupConversationLogic) CreateGroupConversation(in *pb.CreateGroupConversationReq) (*pb.CreateGroupConversationResp, error) {
    // 1. 检查群会话是否已存在 
    existing, err := l.svcCtx.ConversationModel.FindOneByConversationId(l.ctx, in.GroupId)
    if err != nil && !errors.Is(err, sql.ErrNoRows) {
        l.Logger.Errorf("查询群会话失败: %v", err)
        return nil, errors.Wrap(err, "查询会话失败")
    }
    if existing != nil {
        return &pb.CreateGroupConversationResp{}, nil 
    }
 
    // 2. 创建群会话 
    groupConv := &model.Conversation{
        ConversationId: in.GroupId,
        Type:           constant.ChatTypeGroup,
    }
    if _, err := l.svcCtx.ConversationModel.Insert(l.ctx, groupConv); err != nil {
        l.Logger.Errorf("创建群会话失败: %v", err)
        return nil, errors.Wrap(err, "创建会话失败")
    }
 
    // 3. 为创建者添加群会话关系 
    userConv := &model.UserConversation{
        UserId:         in.CreatorId,
        ConversationId: in.GroupId,
        Type:           constant.ChatTypeGroup,
    }
    if _, err := l.svcCtx.UserConversationModel.Insert(l.ctx, userConv); err != nil {
        l.Logger.Errorf("为用户添加群会话失败: %v", err)
        return nil, errors.Wrap(err, "添加用户会话失败")
    }
 
    return &pb.CreateGroupConversationResp{}, nil 
}

说明:代码中 ConversationModelUserConversationModel 为 go-zero 生成的 model 层对象;constant.ChatTypeGroup 是定义在常量包中的枚举值。

并发推送消息

群聊消息需要推送给所有在线成员,如果采用串行方式逐个发送,延迟会随着人数线性增长。为此,我们引入 go-zero 提供的线程工具进行并发控制。

并发限制与配置

im 服务的 Server 结构体中,通过 Option 模式暴露并发度参数,方便运维调整。

go 复制代码
// internal/config/config.go 
type Config struct {
    // ... 其他配置 
    ConcurrencyLimit int `json:"ConcurrencyLimit"`
}
go 复制代码
// internal/server/option.go 
type Option struct {
    ConcurrencyLimit int 
}
 
func WithConcurrencyLimit(limit int) Option {
    return func(s *Server) {
        s.concurrencyLimit = limit 
    }
}

消息发送逻辑重构

推送方法原先只处理私聊,现在通过类型判定的方式分流,群聊部分使用 TaskRunner 并发调用私聊推送方法。

go 复制代码
// internal/server/message.go 
package server 
 
import (
    "context"
    "fmt"
 
    "go-zero-shop/apps/im/rpc/internal/constant"
    "go-zero-shop/apps/im/rpc/internal/svc"
    "go-zero-shop/apps/im/rpc/pb"
 
    "github.com/zeromicro/go-zero/core/threading"
)
 
type MessageCenter struct {
    svcCtx           *svc.ServiceContext 
    concurrencyLimit int 
    taskRunner       *threading.TaskRunner 
}
 
func NewMessageCenter(svcCtx *svc.ServiceContext, limit int) *MessageCenter {
    return &MessageCenter{
        svcCtx:           svcCtx,
        concurrencyLimit: limit,
        taskRunner:       threading.NewTaskRunner(limit),
    }
}
 
// Push 消息推送入口 
func (m *MessageCenter) Push(ctx context.Context, msg *pb.ChatMessage) error {
    switch msg.Type {
    case constant.ChatTypePrivate:
        return m.pushPrivate(ctx, msg, msg.ReceiverId)
    case constant.ChatTypeGroup:
        return m.pushGroup(ctx, msg)
    default:
        return fmt.Errorf("不支持的消息类型: %d", msg.Type)
    }
}
 
// pushPrivate 私聊推送 
func (m *MessageCenter) pushPrivate(ctx context.Context, msg *pb.ChatMessage, receiverId string) error {
    conn, err := m.svcCtx.ConnectionManager.Get(receiverId)
    if err != nil {
        // 用户离线可记录日志或丢弃 
        return nil 
    }
    // 假设存在 packResponse 将消息序列化为 WebSocket 帧 
    data, err := packResponse(msg)
    if err != nil {
        return err 
    }
    return conn.WriteMessage(data)
}
 
// pushGroup 群聊推送 
func (m *MessageCenter) pushGroup(ctx context.Context, msg *pb.ChatMessage) error {
    // msg.Receivers 由上游填充,包含剔除发送者后的所有成员 ID 
    for _, uid := range msg.Receivers {
        uid := uid // 防止闭包引用问题 
        m.taskRunner.Schedule(func() {
            if err := m.pushPrivate(ctx, msg, uid); err != nil {
                logx.WithContext(ctx).Errorf("群聊推送失败, receiver=%s, err=%v", uid, err)
            }
        })
    }
    return nil 
}

注释ConnectionManager 是我们实现的局部连接管理组件,负责根据用户 ID 查找对应的 WebSocket 连接。TaskRunner.Schedule 使用 channel 控制并发数,当队列满时调用方会被阻塞,从而实现反压。

消息队列的群聊支持

为了提高可靠性,消息先被投递到消息队列,由 taskMQ 异步消费并完成持久化与推送。需要在消费端增加群聊类型的处理,并通过社交 RPC 服务获取群成员列表。

消费端骨架

go 复制代码
// internal/mq/task.go 
package mq 
 
import (
    "context"
    "encoding/json"
 
    "go-zero-shop/apps/im/rpc/internal/constant"
    "go-zero-shop/apps/im/rpc/internal/svc"
    "go-zero-shop/apps/im/rpc/pb"
 
    "github.com/zeromicro/go-zero/core/logx"
)
 
type TaskHandler struct {
    svcCtx      *svc.ServiceContext 
    pushService *server.MessageCenter 
}
 
func (h *TaskHandler) Handle(ctx context.Context, raw []byte) error {
    var msg pb.ChatMessage 
    if err := json.Unmarshal(raw, &msg); err != nil {
        return err 
    }
 
    switch msg.Type {
    case constant.ChatTypePrivate:
        return h.handlePrivate(ctx, &msg)
    case constant.ChatTypeGroup:
        return h.handleGroup(ctx, &msg)
    default:
        return nil 
    }
}
 
func (h *TaskHandler) handlePrivate(ctx context.Context, msg *pb.ChatMessage) error {
    // 存储消息记录...
    return h.pushService.Push(ctx, msg)
}
 
func (h *TaskHandler) handleGroup(ctx context.Context, msg *pb.ChatMessage) error {
    // 1. 获取群成员 
    rpcResp, err := h.svcCtx.SocialRpc.GroupUserList(ctx, &social_pb.GroupUserListReq{
        GroupId: msg.ReceiverId,
    })
    if err != nil {
        logx.WithContext(ctx).Errorf("获取群成员失败: %v", err)
        return err 
    }
 
    // 2. 过滤发送者,构建接收列表 
    var receivers []string 
    for _, user := range rpcResp.Users {
        if user.UserId != msg.SenderId {
            receivers = append(receivers, user.UserId)
        }
    }
    msg.Receivers = receivers 
 
    // 3. 存储消息记录...
 
    // 4. 并发推送 
    return h.pushService.Push(ctx, msg)
}

配置社交 RPC 客户端

imconfigservice context 中引入社交 RPC 客户端。

go 复制代码
// internal/config/config.go 
type Config struct {
    // ...
    SocialRpc zrpc.RpcClientConf 
}
go 复制代码
// internal/svc/servicecontext.go 
type ServiceContext struct {
    Config       config.Config 
    SocialRpc    socialpb.SocialClient 
    // ...其他依赖 
}
 
func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
        Config:    c,
        SocialRpc: socialpb.NewSocialClient(zrpc.MustNewClient(c.SocialRpc).Conn()),
    }
}

社交服务触发会话建立

im 服务的会话创建接口需要通过具体业务行为触发。在社交 API 服务中,当创建群、申请入群、处理入群申请成功后,应异步回调 im RPC 建立会话。

社交 API 中的调用逻辑

以创建群为例,其余两个场景类似。

go 复制代码
// internal/logic/creategrouplogic.go (社交 API)
func (l *CreateGroupLogic) CreateGroup(req *types.CreateGroupReq) (*types.CreateGroupResp, error) {
    // ... 创建群业务逻辑,获得 groupId 
    groupId := "xxx"
 
    // 调用 IM RPC 创建群会话 
    _, err := l.svcCtx.ImRpc.CreateGroupConversation(l.ctx, &im_pb.CreateGroupConversationReq{
        GroupId:   groupId,
        CreatorId: req.CreatorId,
    })
    if err != nil {
        l.Logger.Errorf("创建群会话失败, groupId=%s, err=%v", groupId, err)
        // 通常这里可容忍失败,通过定时任务补偿 
    }
 
    return &types.CreateGroupResp{GroupId: groupId}, nil 
}

社交服务的 IM RPC 配置

go 复制代码
// internal/config/config.go (社交 API)
type Config struct {
    // ...
    ImRpc zrpc.RpcClientConf 
}
go 复制代码
// internal/svc/servicecontext.go (社交 API)
type ServiceContext struct {
    Config config.Config 
    ImRpc  impb.ImClient 
    // ...
}
 
func NewServiceContext(c config.Config) *ServiceContext {
    return &ServiceContext{
        Config: c,
        ImRpc:  impb.NewImClient(zrpc.MustNewClient(c.ImRpc).Conn()),
    }
}

总结

群聊功能的实现本质上复用了私聊的存储与推送链路,核心差异体现在三处:

  • 会话建模:在群创建/加入时通过 im 服务统一管理群会话与用户‑会话关系。
  • 消息扩散 :服务端根据群 ID 查询成员列表,借助 go-zero 的并发工具高效推送。
  • 异步处理:消息队列消费端区分消息类型,调用社交服务获取最新成员列表,保证成员变动的实时性。

整套方案在保持代码简洁的同时,充分利用了 go-zero 框架的微服务能力(RPC 调用、线程池、消息队列),可以平稳支撑较大规模的群组聊天场景。

相关推荐
a1117761 小时前
坦克大战3D Three.js 3D (开源项目)
开发语言·javascript·3d
Wang's Blog3 小时前
Go-Zero项目开发24: 基于Bitmap实现群聊消息已读未读
开发语言·后端·golang
吃好睡好便好6 小时前
MATLAB中图像的读取、写入和显示
开发语言·图像处理·学习·计算机视觉·matlab
yaoxin5211236 小时前
476. Java 反射 - 调用方法
java·开发语言
猫头虎6 小时前
什么是ZCode for GLM-5.2?
开发语言·人工智能·python·科技·算法·ai编程·ai写作
bksczm7 小时前
Linux之日志和线程池、内存池
java·开发语言
笨蛋不要掉眼泪7 小时前
Java虚拟机:对象复活、引用强度与Stop-The-World
java·开发语言·jvm
code_pgf8 小时前
C/C++ 常用容器功能汇总
c语言·开发语言·c++
布朗克1688 小时前
Go 入门到精通-33-unsafe 与 CGO
开发语言·后端·golang·unsafe·cgo