- 新增 at_me_count 字段设计(im_conversation_members) - 明确系统消息 content 格式为纯文本 + 12 种消息模板 - 群聊离线推送策略(沿用 Phase 2b OfflineMessagePusher) - 文件上传策略(两步解耦,无需接口注入) - 群聊错误常量规划(15 个业务错误) Made-with: Cursor
28 KiB
28 KiB
Phase 2c 设计文档:群聊与已读回执
状态: 📋 设计完成,待实施 分支:
feature/phase2c-group-read-receipt前置依赖: Phase 2b 全部完成(单聊即时通讯) 最后更新: 2026-03-04
一、设计目标
基于 Phase 2b 的单聊 IM 基础设施,实现群聊核心功能和已读回执系统,同时引入 MinIO 文件存储服务作为基础设施扩展。
核心交付物:
- 群聊管理(创建/加入/退出/解散/搜索/角色管理/禁言/公告)
- 群消息收发(复用 im.message.* 事件 + @提醒 + 管理员撤回)
- 已读回执(单聊会话级 + 群聊消息级 + 实时推送 + 详情查看)
- MinIO 文件存储(Docker 容器 + Go SDK + 通用上传 API)
- 管理端群聊管理(群列表/详情/解散/移除成员)
- 前端 9 个新页面 + 群聊 Store + API 封装
不包含(留待后续阶段):
- 图片/语音/文件消息
- 管理端消息管理
- 消息类型扩展
二、需求决策记录
2.1 群聊功能决策
| 决策项 | 选定方案 | 说明 |
|---|---|---|
| 创建方式 | 选好友 + 搜索用户 ID 拉人 | 非好友也可拉入群 |
| 成员上限 | 200 人(默认) | im_groups.max_members 可配置 |
| 角色体系 | 三级:群主(2) + 管理员(1) + 普通成员(0) | 微信模式 |
| 管理操作 | 全功能 | 改群名/头像/公告、拉人/踢人、禁言/全体禁言、转让群主、解散、设管理员 |
| 入群方式 | 被拉入(无需审批)+ 主动申请(需审批) | 群主/管理员审批 |
| 退出机制 | 主动退出 + 显示系统消息 | "XX 退出/加入/被移出了群聊" |
| 历史消息 | 钉钉模式 | 新成员可查看所有历史消息 |
| 群搜索 | 全局搜索公开群 + 自己的群 | 默认可搜索,群主可设为不可搜索 |
| 群头像 | 默认图标 + 群主手动上传 | 使用 MinIO 存储 |
| 群昵称 | 成员可设置群内昵称 | 仅在该群内显示 |
| 免打扰 | 微信模式 | 仍计未读(灰色数字),不推送通知 |
| @提醒 | @某人 + @所有人 | 所有成员可 @所有人,输入 @ 弹出成员选择 |
| @通知 | 会话列表标记 | 显示 "[N条] @了我" |
| 正在输入 | 群聊不显示 | 仅单聊保留 |
| 消息撤回 | 成员 2 分钟,管理员无时限 | 群主/管理员可撤回任何人消息 |
| 撤回展示 | 区分操作者 | "管理员 XX 撤回了 YY 的一条消息" |
| 群公告 | 需要 | 群主/管理员发布,发布时推送系统消息通知全员 |
2.2 已读回执决策
| 决策项 | 选定方案 | 说明 |
|---|---|---|
| 单聊粒度 | 会话级别 | 复用 last_read_msg_id,消息 ID ≤ 该值即已读 |
| 群聊粒度 | 消息级别 | im_message_reads 表记录每人每条消息的已读 |
| 展示方式 | 钉钉模式 | 单聊 "已读/未读",群聊 "X人已读" 可点击查看列表 |
| 存储方案 | 纯 PostgreSQL | im_message_reads 表(联合主键 message_id + user_id) |
| 推送策略 | 差异化 | 单聊实时推送已读变化,群聊仅推送已读计数变化 |
| 详情查看 | REST 按需拉取 | 点击 "X人已读" 时 GET /api/v1/im/messages/:id/reads |
2.3 基础设施决策
| 决策项 | 选定方案 | 说明 |
|---|---|---|
| 文件存储 | MinIO(Docker 镜像) | 通用上传 API,后续消息类型扩展可复用 |
| 前端架构 | 群聊独立页面体系 | 不复用单聊页面,完全独立 |
| 会话列表 | 分 Tab | 单聊 Tab + 群聊 Tab |
| WS 事件 | 消息复用 + 管理独立 | im.message.* 复用,group.* 新增 |
| 消息搜索 | 统一全局搜索 | 单聊 + 群聊消息在同一搜索结果中 |
| 分支策略 | 从 phase2b 拉新分支 | feature/phase2c-group-read-receipt |
| 管理端 | 群列表 + 详情 + 解散 + 移除 | 4 个管理 API + 2 个前端页面 |
三、架构设计
3.1 新增模块
| 模块 | 位置 | 职责 |
|---|---|---|
| group | app/group/ | 群聊管理(创建/加入/退出/角色/禁言/搜索/审批) |
| file | app/file/ | 文件上传(MinIO SDK 封装 + 通用上传 API) |
| storage | pkg/storage/ | MinIO 客户端初始化 + 配置 |
3.2 模块目录结构
backend/go-service/
├── app/
│ ├── group/ # [新增] 群聊管理模块
│ │ ├── controller/
│ │ │ └── group_controller.go # REST API 控制器(~15 个接口)
│ │ ├── service/
│ │ │ └── group_service.go # 群创建/管理/审批/搜索业务逻辑
│ │ ├── dao/
│ │ │ └── group_dao.go # im_groups + 成员角色 + 入群申请 CRUD
│ │ ├── handler/
│ │ │ └── group_handler.go # WS 群管理事件处理
│ │ ├── model/
│ │ │ ├── group.go # im_groups 模型
│ │ │ └── join_request.go # im_group_join_requests 模型
│ │ ├── router.go
│ │ └── provider.go
│ ├── file/ # [新增] 文件上传模块
│ │ ├── controller/
│ │ │ └── file_controller.go # POST /api/v1/upload
│ │ ├── service/
│ │ │ └── file_service.go # MinIO 上传/删除封装
│ │ ├── router.go
│ │ └── provider.go
│ ├── im/ # [扩展]
│ │ ├── dao/
│ │ │ └── read_dao.go # [新增] im_message_reads 操作
│ │ ├── model/
│ │ │ └── message_read.go # [新增] im_message_reads 模型
│ │ └── service/
│ │ └── im_service.go # [扩展] 群消息/已读回执/管理员撤回
│ └── constants/
│ └── group.go # [新增] 群聊相关常量
├── pkg/
│ └── storage/
│ └── minio.go # [新增] MinIO 客户端初始化
frontend/src/
├── pages/
│ ├── group/ # [新增] 群聊页面(独立体系)
│ │ ├── index.vue # 群聊会话列表
│ │ ├── conversation.vue # 群聊对话页
│ │ ├── create.vue # 创建群聊
│ │ ├── settings.vue # 群聊设置
│ │ ├── members.vue # 群成员列表
│ │ ├── invite.vue # 邀请入群
│ │ ├── join-requests.vue # 入群申请审批
│ │ └── search.vue # 搜索公开群
│ └── chat/
│ └── read-detail.vue # [新增] 已读回执详情页
├── store/
│ └── group.js # [新增] 群聊 Pinia Store
├── api/
│ ├── group.js # [新增] 群聊 API
│ └── file.js # [新增] 文件上传 API
admin/src/
├── views/
│ └── group/ # [新增] 管理端群聊管理
│ ├── list.vue # 群列表
│ └── detail.vue # 群详情(含成员管理)
├── api/
│ └── group.js # [新增] 管理端群聊 API
3.3 跨模块接口注入
延续 Phase 2a/2b 的接口注入模式,通过 Wire Bind 在 app/provider/wire.go 中统一绑定。
| 接口 | 定义模块 | 实现方 | 用途 |
|---|---|---|---|
| GroupMemberChecker | im/service | group/dao.GroupDAO | 检查用户是否为群成员 |
| GroupInfoGetter | im/service | group/dao.GroupDAO | 获取群信息(名称/头像/成员数) |
| GroupRoleChecker | im/service | group/dao.GroupDAO | 检查用户群角色(管理员撤回权限) |
| 已有接口 | - | - | FriendChecker / UserInfoGetter / FriendIDsGetter 等继续复用 |
四、数据库设计
4.1 新增表
im_groups(群聊信息表)
CREATE TABLE im_groups (
id BIGSERIAL PRIMARY KEY,
conversation_id BIGINT NOT NULL UNIQUE REFERENCES im_conversations(id),
name VARCHAR(100) NOT NULL DEFAULT '',
avatar VARCHAR(500) DEFAULT '',
owner_id BIGINT NOT NULL,
notice TEXT DEFAULT '',
max_members INT NOT NULL DEFAULT 200,
is_searchable BOOLEAN NOT NULL DEFAULT TRUE,
is_all_muted BOOLEAN NOT NULL DEFAULT FALSE,
status SMALLINT NOT NULL DEFAULT 1,
created_at TIMESTAMP(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP(0) NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE im_groups IS '群聊信息表';
COMMENT ON COLUMN im_groups.id IS '群唯一标识';
COMMENT ON COLUMN im_groups.conversation_id IS '关联 im_conversations.id';
COMMENT ON COLUMN im_groups.name IS '群名称';
COMMENT ON COLUMN im_groups.avatar IS '群头像 URL(MinIO)';
COMMENT ON COLUMN im_groups.owner_id IS '群主用户 ID';
COMMENT ON COLUMN im_groups.notice IS '群公告内容';
COMMENT ON COLUMN im_groups.max_members IS '最大成员数,默认 200';
COMMENT ON COLUMN im_groups.is_searchable IS '是否可被搜索发现';
COMMENT ON COLUMN im_groups.is_all_muted IS '是否全体禁言';
COMMENT ON COLUMN im_groups.status IS '群状态:1=正常,2=已解散';
CREATE INDEX idx_im_groups_owner ON im_groups(owner_id);
CREATE INDEX idx_im_groups_name ON im_groups USING gin(to_tsvector('simple', name));
im_group_join_requests(入群申请表)
CREATE TABLE im_group_join_requests (
id BIGSERIAL PRIMARY KEY,
group_id BIGINT NOT NULL REFERENCES im_groups(id),
user_id BIGINT NOT NULL,
message TEXT DEFAULT '',
reviewer_id BIGINT DEFAULT NULL,
status SMALLINT NOT NULL DEFAULT 0,
created_at TIMESTAMP(0) NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP(0) NOT NULL DEFAULT NOW()
);
COMMENT ON TABLE im_group_join_requests IS '入群申请表';
COMMENT ON COLUMN im_group_join_requests.id IS '申请唯一标识';
COMMENT ON COLUMN im_group_join_requests.group_id IS '目标群 ID';
COMMENT ON COLUMN im_group_join_requests.user_id IS '申请人用户 ID';
COMMENT ON COLUMN im_group_join_requests.message IS '申请附言';
COMMENT ON COLUMN im_group_join_requests.reviewer_id IS '审批人用户 ID';
COMMENT ON COLUMN im_group_join_requests.status IS '状态:0=待审批,1=通过,2=拒绝';
CREATE INDEX idx_group_join_req_group ON im_group_join_requests(group_id, status);
CREATE INDEX idx_group_join_req_user ON im_group_join_requests(user_id);
im_message_reads(消息已读记录表)
CREATE TABLE im_message_reads (
message_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
read_at TIMESTAMP(0) NOT NULL DEFAULT NOW(),
PRIMARY KEY (message_id, user_id)
);
COMMENT ON TABLE im_message_reads IS '群聊消息已读记录表(消息级别)';
COMMENT ON COLUMN im_message_reads.message_id IS '消息 ID';
COMMENT ON COLUMN im_message_reads.user_id IS '已读用户 ID';
COMMENT ON COLUMN im_message_reads.read_at IS '已读时间';
CREATE INDEX idx_msg_reads_user ON im_message_reads(user_id, read_at);
4.2 扩展现有表
im_conversation_members — 新增 5 个字段
ALTER TABLE im_conversation_members
ADD COLUMN role SMALLINT NOT NULL DEFAULT 0,
ADD COLUMN nickname VARCHAR(50) DEFAULT '',
ADD COLUMN is_muted BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN is_do_not_disturb BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN joined_at TIMESTAMP(0) DEFAULT NULL;
COMMENT ON COLUMN im_conversation_members.role IS '成员角色:0=普通成员,1=管理员,2=群主';
COMMENT ON COLUMN im_conversation_members.nickname IS '群内昵称(仅群聊有效)';
COMMENT ON COLUMN im_conversation_members.is_muted IS '是否被禁言';
COMMENT ON COLUMN im_conversation_members.is_do_not_disturb IS '是否消息免打扰(微信模式:仍计未读但灰色显示)';
COMMENT ON COLUMN im_conversation_members.joined_at IS '加入群聊时间';
im_messages — 新增 1 个字段
ALTER TABLE im_messages
ADD COLUMN at_user_ids BIGINT[] DEFAULT NULL;
COMMENT ON COLUMN im_messages.at_user_ids IS '@提醒用户 ID 列表,NULL=无@,包含 0 表示 @所有人';
4.3 数据库表关系图
im_conversations (type=2 群聊)
│ 1:1
├── im_groups (群信息:名称/头像/公告/群主)
│ │ 1:N
│ └── im_group_join_requests (入群申请)
│ 1:N
├── im_conversation_members (成员:+role/nickname/is_muted/is_do_not_disturb)
│ 1:N
└── im_messages (消息:+at_user_ids)
│ 1:N
└── im_message_reads (群聊消息已读记录)
五、API 设计
5.1 群聊 REST API(16 个)
| # | 方法 | 路径 | 描述 | 权限 |
|---|---|---|---|---|
| 1 | POST | /api/v1/groups | 创建群聊 | 登录用户 |
| 2 | GET | /api/v1/groups/:id | 群详情 | 群成员 |
| 3 | PUT | /api/v1/groups/:id | 更新群信息 | 群主/管理员 |
| 4 | DELETE | /api/v1/groups/:id | 解散群聊 | 群主 |
| 5 | GET | /api/v1/groups/:id/members | 成员列表 | 群成员 |
| 6 | POST | /api/v1/groups/:id/members | 邀请/拉人入群 | 群主/管理员 |
| 7 | DELETE | /api/v1/groups/:id/members/:uid | 踢人 | 群主/管理员 |
| 8 | PUT | /api/v1/groups/:id/members/:uid/role | 设置/取消管理员 | 群主 |
| 9 | PUT | /api/v1/groups/:id/members/:uid/mute | 禁言/解除禁言 | 群主/管理员 |
| 10 | POST | /api/v1/groups/:id/leave | 退出群聊 | 群成员 |
| 11 | PUT | /api/v1/groups/:id/transfer | 转让群主 | 群主 |
| 12 | PUT | /api/v1/groups/:id/members/me/nickname | 修改群昵称 | 群成员 |
| 13 | POST | /api/v1/groups/:id/join-requests | 申请入群 | 登录用户 |
| 14 | GET | /api/v1/groups/:id/join-requests | 入群申请列表 | 群主/管理员 |
| 15 | PUT | /api/v1/groups/:id/join-requests/:rid | 审批入群申请 | 群主/管理员 |
| 16 | GET | /api/v1/groups/search | 搜索公开群 | 登录用户 |
5.2 已读回执 REST API(2 个)
| # | 方法 | 路径 | 描述 |
|---|---|---|---|
| 1 | GET | /api/v1/im/messages/:id/reads | 消息已读详情(谁读了,分页) |
| 2 | GET | /api/v1/im/messages/:id/read-count | 消息已读/未读计数 |
5.3 文件上传 REST API(1 个)
| # | 方法 | 路径 | 描述 |
|---|---|---|---|
| 1 | POST | /api/v1/upload | 通用文件上传(返回 MinIO URL) |
5.4 管理端 REST API(4 个)
| # | 方法 | 路径 | 描述 |
|---|---|---|---|
| 1 | GET | /api/v1/admin/groups | 群列表(分页 + 筛选) |
| 2 | GET | /api/v1/admin/groups/:id | 群详情(含成员列表) |
| 3 | DELETE | /api/v1/admin/groups/:id | 管理员解散群 |
| 4 | DELETE | /api/v1/admin/groups/:id/members/:uid | 管理员移除成员 |
5.5 WebSocket 事件
消息事件(复用 im.message.*,扩展群聊支持)
| 事件 | 方向 | 变更说明 |
|---|---|---|
| im.message.send | C→S | data 新增 at_user_ids 字段 |
| im.message.send.ack | S→C | 无变化 |
| im.message.new | S→C | data 新增 conv_type、at_user_ids 字段 |
| im.message.recall | C→S | 群聊时管理员可撤回他人消息(无时限) |
| im.message.recalled | S→C | 群聊时区分 "管理员 XX 撤回了 YY 的消息" |
已读回执事件(新增)
| 事件 | 方向 | 说明 |
|---|---|---|
| im.message.read | C→S | 上报已读:单聊走 last_read_msg_id,群聊写 im_message_reads |
| im.message.read.ack | S→C | 单聊:实时推送已读状态给消息发送者 |
| im.message.read.count | S→C | 群聊:推送已读计数变化 {msg_id, read_count} |
群管理事件(新增 group.* 系列)
| 事件 | 方向 | 说明 |
|---|---|---|
| group.member.join | S→C | 成员加入通知(含系统消息 "XX 加入了群聊") |
| group.member.leave | S→C | 成员退出通知(含系统消息 "XX 退出了群聊") |
| group.member.kicked | S→C | 成员被踢通知(含系统消息 "XX 被移出了群聊") |
| group.info.update | S→C | 群信息变更通知(名称/头像) |
| group.notice.update | S→C | 群公告变更通知(含系统消息) |
| group.dissolved | S→C | 群解散通知 |
| group.mute.update | S→C | 禁言状态变更通知(个人/全体) |
| group.join.request | S→C | 新入群申请(推送给群主/管理员) |
| group.join.approved | S→C | 入群申请通过(推送给申请人) |
六、核心业务流程
6.1 创建群聊
1. 用户选择好友 + 搜索用户 ID → 确定初始成员列表
2. POST /api/v1/groups {name, member_ids[]}
3. GroupService:
a. 创建 im_conversations (type=2)
b. 创建 im_groups (关联 conversation_id)
c. 批量创建 im_conversation_members (创建者 role=2 群主, 其余 role=0)
d. 插入系统消息 "XX 创建了群聊,邀请了 A、B、C 加入"
e. PubSub 推送 group.member.join 给所有初始成员
4. 返回群信息 + 会话 ID
6.2 群消息发送
1. 发送者 WS: im.message.send {conversation_id, content, at_user_ids}
2. IMService.SendMessage:
a. 查询 im_conversations.type → 群聊分支
b. 校验群成员身份 + 禁言检查(is_muted / is_all_muted,管理员豁免全体禁言)
c. 写入 im_messages (含 at_user_ids)
d. 更新 im_conversations.last_msg_*
e. 遍历群成员:
- 排除发送者
- 非免打扰成员:递增 unread_count + Redis 全局未读
- 免打扰成员:递增 unread_count,不递增 Redis 全局未读
- 被 @的成员:记录 @计数(可存 im_conversation_members 或 Redis)
f. PubSub.PublishToUsers → 推送 im.message.new 给所有在线成员
3. 发送者收到 im.message.send.ack
6.3 已读回执(单聊)
1. 用户打开单聊会话
2. 前端 WS: im.message.read {conversation_id}
3. IMService:
a. 获取会话最新消息 ID → 更新 last_read_msg_id
b. 清零 unread_count + Redis 全局未读
c. PubSub 推送 im.message.read.ack 给对方
{conversation_id, reader_id, last_read_msg_id}
4. 对方前端收到后,对 ID ≤ last_read_msg_id 的自己发送的消息标记为 "已读"
6.4 已读回执(群聊)
1. 用户打开群聊会话,前端获取可视区域消息 ID 列表
2. 前端 WS: im.message.read {conversation_id, message_ids[]}
3. IMService:
a. 批量写入 im_message_reads (ON CONFLICT DO NOTHING)
b. 更新 last_read_msg_id + 清零 unread_count
c. 查询受影响消息的 sender_id(去重)
d. 对每个 sender_id 推送 im.message.read.count
{msg_id, read_count, total_members}
4. 发送者前端更新 "X人已读" 显示
5. 用户点击 "X人已读" → GET /api/v1/im/messages/:id/reads → 显示详情列表
6.5 @提醒流程
1. 发送者在输入框键入 @ → 弹出成员选择列表
2. 选择成员(或 @所有人)→ 消息中插入 @昵称 标记
3. 发送消息时 at_user_ids 包含被 @用户的 ID(0 表示 @所有人)
4. 接收方前端:
a. 收到 im.message.new 检查 at_user_ids
b. 如果包含自己的 ID 或 0 → 标记该会话 "被@"
c. 会话列表显示 "[N条] @了我"
七、前端页面规划
7.1 新增页面(9 个)
| 页面 | 路径 | 功能描述 |
|---|---|---|
| 群聊会话列表 | pages/group/index.vue | 群聊 Tab 页,显示群会话列表 |
| 群聊对话页 | pages/group/conversation.vue | 群消息展示 + 发送 + @选择器 |
| 创建群聊 | pages/group/create.vue | 选好友 + 搜索用户 + 填群名 |
| 群设置 | pages/group/settings.vue | 群信息/公告/成员概览/免打扰/退群/解散 |
| 群成员列表 | pages/group/members.vue | 完整成员列表 + 角色标识 + 管理操作 |
| 邀请入群 | pages/group/invite.vue | 搜索用户 ID 拉人入群 |
| 入群审批 | pages/group/join-requests.vue | 群主/管理员审批入群申请 |
| 搜索群聊 | pages/group/search.vue | 搜索公开群 + 申请加入 |
| 已读详情 | pages/chat/read-detail.vue | 已读/未读人员列表(单聊+群聊共用) |
7.2 修改现有页面
| 页面 | 修改内容 |
|---|---|
| pages/chat/index.vue | 改为 Tab 切换:单聊 Tab + 群聊 Tab |
| pages/chat/conversation.vue | 添加单聊已读状态展示("已读"/"未读") |
| pages/chat/search.vue | 全局搜索扩展支持群聊消息结果 |
| components/CustomTabBar.vue | TabBar 适配群聊入口 |
7.3 管理端新增页面(2 个)
| 页面 | 路径 | 功能描述 |
|---|---|---|
| 群列表 | admin/src/views/group/list.vue | 群列表(搜索/分页/状态筛选) |
| 群详情 | admin/src/views/group/detail.vue | 群信息 + 成员列表 + 解散/移除操作 |
八、Docker Compose 变更
在 deploy/docker/docker-compose.dev.yml 中新增 MinIO 服务:
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
ports:
- "9000:9000"
- "9001:9001"
environment:
MINIO_ROOT_USER: echochat
MINIO_ROOT_PASSWORD: echochat123456
volumes:
- minio_data:/data
restart: unless-stopped
Go 后端 config.yaml 新增:
minio:
endpoint: "localhost:9000"
access_key: "echochat"
secret_key: "echochat123456"
bucket: "echochat"
use_ssl: false
九、常量定义规划
// app/constants/group.go
// 群成员角色
const (
GroupRoleMember = 0 // 普通成员
GroupRoleAdmin = 1 // 管理员
GroupRoleOwner = 2 // 群主
)
// 群状态
const (
GroupStatusActive = 1 // 正常
GroupStatusDissolved = 2 // 已解散
)
// 入群申请状态
const (
JoinRequestPending = 0 // 待审批
JoinRequestApproved = 1 // 已通过
JoinRequestRejected = 2 // 已拒绝
)
// 系统消息类型(im_messages.type 扩展)
const (
MessageTypeSystem = 10 // 系统通知消息
)
十、DTO 扩展规划
10.1 群聊 DTO
// CreateGroupRequest 创建群聊请求
type CreateGroupRequest struct {
Name string `json:"name" binding:"required"`
MemberIDs []int64 `json:"member_ids" binding:"required,min=1"`
}
// GroupDTO 群详情
type GroupDTO struct {
ID int64 `json:"id"`
ConversationID int64 `json:"conversation_id"`
Name string `json:"name"`
Avatar string `json:"avatar"`
OwnerID int64 `json:"owner_id"`
OwnerNickname string `json:"owner_nickname"`
Notice string `json:"notice"`
MemberCount int `json:"member_count"`
MaxMembers int `json:"max_members"`
IsSearchable bool `json:"is_searchable"`
IsAllMuted bool `json:"is_all_muted"`
Status int `json:"status"`
CreatedAt string `json:"created_at"`
}
// GroupMemberDTO 群成员信息
type GroupMemberDTO struct {
UserID int64 `json:"user_id"`
Username string `json:"username"`
Nickname string `json:"nickname"` // 群昵称(优先)或全局昵称
Avatar string `json:"avatar"`
Role int `json:"role"`
IsMuted bool `json:"is_muted"`
IsOnline bool `json:"is_online"`
JoinedAt string `json:"joined_at"`
}
10.2 已读回执 DTO
// ReadDetailDTO 消息已读详情
type ReadDetailDTO struct {
UserID int64 `json:"user_id"`
Nickname string `json:"nickname"`
Avatar string `json:"avatar"`
ReadAt string `json:"read_at"`
}
// ReadCountDTO 消息已读计数
type ReadCountDTO struct {
MessageID int64 `json:"message_id"`
ReadCount int `json:"read_count"`
UnreadCount int `json:"unread_count"`
TotalMembers int `json:"total_members"`
}
10.3 ConversationDTO 扩展
// ConversationDTO 扩展:增加群聊字段
type ConversationDTO struct {
// 原有字段...
GroupID *int64 `json:"group_id,omitempty"` // 群聊时的群 ID
GroupName string `json:"group_name,omitempty"` // 群名称
GroupAvatar string `json:"group_avatar,omitempty"` // 群头像
MemberCount int `json:"member_count,omitempty"` // 群成员数
IsDoNotDisturb bool `json:"is_do_not_disturb"` // 是否免打扰
AtMeCount int `json:"at_me_count,omitempty"` // 被@计数
}
十一、补充设计细节
11.1 @提醒计数存储
会话列表需要显示 "[N条] @了我",需要在 im_conversation_members 表新增字段存储:
ALTER TABLE im_conversation_members
ADD COLUMN at_me_count INT DEFAULT 0;
COMMENT ON COLUMN im_conversation_members.at_me_count IS '被@提醒未读计数,打开会话后清零';
操作流程:
- 收到含 at_user_ids 的消息时:若当前用户在列表中 →
at_me_count += 1 - @所有人(at_user_ids 含 0):群内所有成员(排除发送者)
at_me_count += 1 - 用户打开会话:
at_me_count = 0(同清零 unread_count 时一并清零)
11.2 系统消息内容格式
系统消息(type=10)的 content 字段使用纯文本格式,不采用结构化 JSON。理由:
- 简单直观,前端直接渲染无需解析
- 系统消息种类有限且模板固定
- 不需要国际化(当前仅中文)
系统消息模板:
| 场景 | content 示例 |
|---|---|
| 创建群 | "XX 创建了群聊,邀请了 A、B、C 加入" |
| 邀请入群 | "XX 邀请了 A、B 加入群聊" |
| 退出群 | "XX 退出了群聊" |
| 被踢出 | "XX 被移出了群聊" |
| 转让群主 | "XX 将群主转让给了 YY" |
| 设管理员 | "XX 被设为管理员" |
| 取消管理员 | "XX 被取消了管理员" |
| 群公告 | "XX 修改了群公告" |
| 管理员撤回 | "管理员 XX 撤回了 YY 的一条消息" |
| 解散群 | "群主 XX 解散了群聊" |
| 禁言 | "XX 被禁言" |
| 全体禁言 | "管理员 XX 开启了全体禁言" |
11.3 群聊离线消息推送
沿用 Phase 2b 的离线推送机制(OfflineMessagePusher):
- 用户 WebSocket 连接后,服务端检查该用户的所有未读会话(含群聊)
- 推送
im.offline.sync事件,包含未读会话摘要列表 - 群聊会话的摘要包含:conversation_id、type=2、group_name、group_avatar、unread_count、last_msg_content、at_me_count
- 前端收到后更新群聊会话列表和 TabBar badge
11.4 文件上传策略
群头像上传不需要 FileUploader 接口注入。流程:
1. 前端调用 POST /api/v1/upload 上传图片 → 获取 MinIO URL
2. 前端调用 PUT /api/v1/groups/:id {avatar: "minio-url"} 更新群头像
两步操作,file 模块和 group 模块完全解耦,无需接口注入。
11.5 群聊错误常量规划
// app/constants/group.go 错误定义(在 group/service 中使用)
var (
ErrGroupNotFound = errors.New("群聊不存在")
ErrGroupDissolved = errors.New("群聊已解散")
ErrNotGroupMember = errors.New("你不是该群成员")
ErrNotGroupOwner = errors.New("仅群主可执行此操作")
ErrNotGroupAdmin = errors.New("仅群主或管理员可执行此操作")
ErrGroupFull = errors.New("群成员已满")
ErrAlreadyMember = errors.New("该用户已是群成员")
ErrCannotKickHigherRole = errors.New("不能操作同级或更高权限的成员")
ErrOwnerCannotLeave = errors.New("群主不能退出群聊,请先转让群主")
ErrCannotMuteSelf = errors.New("不能禁言自己")
ErrAlreadyMuted = errors.New("该成员已被禁言")
ErrUserMuted = errors.New("你已被禁言,无法发送消息")
ErrGroupAllMuted = errors.New("当前群已开启全体禁言")
ErrPendingRequestExists = errors.New("已有待处理的入群申请")
ErrJoinRequestNotFound = errors.New("入群申请不存在")
)