Files
EchoChat/docs/plans/2026-03-04-phase2c-design.md
bujinyuan cc09b192a4 docs: Phase 2c 设计文档补充 — @计数存储/系统消息格式/离线推送/错误常量
- 新增 at_me_count 字段设计(im_conversation_members)
- 明确系统消息 content 格式为纯文本 + 12 种消息模板
- 群聊离线推送策略(沿用 Phase 2b OfflineMessagePusher)
- 文件上传策略(两步解耦,无需接口注入)
- 群聊错误常量规划(15 个业务错误)

Made-with: Cursor
2026-03-04 10:37:48 +08:00

715 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 基础设施决策
| 决策项 | 选定方案 | 说明 |
|--------|----------|------|
| 文件存储 | MinIODocker 镜像) | 通用上传 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群聊信息表
```sql
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 '群头像 URLMinIO';
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入群申请表
```sql
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消息已读记录表
```sql
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 个字段
```sql
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 个字段
```sql
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 API16 个)
| # | 方法 | 路径 | 描述 | 权限 |
|---|------|------|------|------|
| 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 API2 个)
| # | 方法 | 路径 | 描述 |
|---|------|------|------|
| 1 | GET | /api/v1/im/messages/:id/reads | 消息已读详情(谁读了,分页) |
| 2 | GET | /api/v1/im/messages/:id/read-count | 消息已读/未读计数 |
### 5.3 文件上传 REST API1 个)
| # | 方法 | 路径 | 描述 |
|---|------|------|------|
| 1 | POST | /api/v1/upload | 通用文件上传(返回 MinIO URL |
### 5.4 管理端 REST API4 个)
| # | 方法 | 路径 | 描述 |
|---|------|------|------|
| 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 包含被 @用户的 ID0 表示 @所有人)
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 服务:
```yaml
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` 新增:
```yaml
minio:
endpoint: "localhost:9000"
access_key: "echochat"
secret_key: "echochat123456"
bucket: "echochat"
use_ssl: false
```
---
## 九、常量定义规划
```go
// 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
```go
// 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
```go
// 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 扩展
```go
// 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` 表新增字段存储:
```sql
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`
1. 用户 WebSocket 连接后,服务端检查该用户的所有未读会话(含群聊)
2. 推送 `im.offline.sync` 事件,包含未读会话摘要列表
3. 群聊会话的摘要包含conversation_id、type=2、group_name、group_avatar、unread_count、last_msg_content、at_me_count
4. 前端收到后更新群聊会话列表和 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 群聊错误常量规划
```go
// 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("入群申请不存在")
)
```