feat(phase2c): 群聊与已读回执功能完整实现
Phase 2c 全部 14 个 Task 完成,包含: 后端(Go): - MinIO 文件存储服务集成(Docker + Go SDK + 通用上传 API) - Group 模块完整实现(DAO + Service + Controller + Router + Wire) - 18 个群管理 REST API + 11 个 WS 群事件推送 - 群创建/解散/邀请/踢人/退出/转让群主/设管理员/禁言/全体禁言/群公告/群昵称/免打扰/搜索 - IM Service 扩展(群消息发送/撤回 + @提醒 + 管理员无时限撤回) - 已读回执后端(单聊会话级 last_read_msg_id + 群聊消息级 im_message_reads) - 管理端群组管理(列表/详情/解散) - 数据库迁移(3 张新表 + 2 张表字段扩展) 前端(uni-app): - 群聊 Store + API 封装 + 11 个 WS 事件监听 - 已读回执 UI(单聊已读/未读标记 + 群聊 X人已读 + 已读详情页) - 7 个群聊页面(对话/创建/设置/成员/邀请/审批/搜索) - 会话列表改造(全部/单聊/群聊 Tab + @标记 + 免打扰标识) 管理端(Vue 3 + Element Plus): - 群组列表页(搜索/分页/详情弹窗/解散群聊) - 侧边栏群组管理入口 文档同步:进度/架构/设计/API/规范文档全部更新 Made-with: Cursor
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
| [frontend/contact.md](frontend/contact.md) | 联系人 | ✅ Phase 2a | 17 个 API:好友申请/管理、好友分组、黑名单、搜索/推荐、在线状态 |
|
||||
| [frontend/websocket.md](frontend/websocket.md) | WebSocket | ✅ Phase 2a | 前端 WebSocket 连接管理、事件协议、心跳、重连 |
|
||||
| [frontend/im.md](frontend/im.md) | 即时通讯 | ✅ Phase 2b | 7 个 API:会话列表/置顶/删除/清空、历史消息、全局搜索、未读数 |
|
||||
| [frontend/group.md](frontend/group.md) | 群聊管理 | 🔜 Phase 2c | 16 个 API:建群/管理/成员/角色/禁言/公告/搜索/入群审批 |
|
||||
| [frontend/group.md](frontend/group.md) | 群聊管理 | ✅ Phase 2c | 16 个 API:建群/管理/成员/角色/禁言/公告/搜索/入群审批 |
|
||||
| [frontend/meeting.md](frontend/meeting.md) | 会议 | 📋 后续 | 即时会议、预约会议、加入/离开、会议列表 |
|
||||
| [frontend/notify.md](frontend/notify.md) | 通知 | 📋 后续 | 通知列表、标记已读 |
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
| [admin/user.md](admin/user.md) | 用户管理 | ✅ Phase 1 | 用户列表/详情、状态管理、角色分配、创建用户 |
|
||||
| [admin/online.md](admin/online.md) | 在线监控 | ✅ Phase 2a | 在线用户列表、在线用户计数 |
|
||||
| [admin/contact.md](admin/contact.md) | 好友关系管理 | ✅ Phase 2a | 好友关系列表(分页)、管理员解除好友关系 |
|
||||
| [admin/group.md](admin/group.md) | 群聊管理 | 🔜 Phase 2c | 群列表/详情、管理员解散群/移除成员 |
|
||||
| [admin/group.md](admin/group.md) | 群聊管理 | ✅ Phase 2c | 群列表/详情、管理员解散群聊 |
|
||||
| [admin/meeting.md](admin/meeting.md) | 会议管理 | 📋 后续 | 会议列表/详情、强制结束、会议统计 |
|
||||
| [admin/system.md](admin/system.md) | 系统管理 | 📋 待定 | 仪表盘数据、操作日志、系统配置 |
|
||||
|
||||
@@ -196,7 +196,8 @@ docs/api/
|
||||
│ ├── auth.md # 用户认证 ✅ Phase 1
|
||||
│ ├── contact.md # 联系人管理(17 个 API) ✅ Phase 2a
|
||||
│ ├── websocket.md # WebSocket 事件协议 ✅ Phase 2a
|
||||
│ ├── im.md # 即时通讯(7 个 API) ✅ Phase 2b
|
||||
│ ├── im.md # 即时通讯(8 个 API) ✅ Phase 2b/2c
|
||||
│ ├── group.md # 群聊管理(16 个 API) ✅ Phase 2c
|
||||
│ ├── meeting.md # 会议 📋 后续
|
||||
│ └── notify.md # 通知 📋 后续
|
||||
├── admin/ # 后台管理端 API
|
||||
@@ -204,7 +205,8 @@ docs/api/
|
||||
│ ├── user.md # 用户管理 ✅ Phase 1
|
||||
│ ├── online.md # 在线监控 ✅ Phase 2a
|
||||
│ ├── contact.md # 好友关系管理 ✅ Phase 2a
|
||||
│ ├── group.md # 群聊管理(3 个 API) ✅ Phase 2c
|
||||
│ ├── meeting.md # 会议管理 📋 后续
|
||||
│ └── system.md # 系统管理 📋 待定
|
||||
└── websocket.md # WebSocket 全量事件协议 ✅ Phase 2a/2b
|
||||
└── websocket.md # WebSocket 全量事件协议 ✅ Phase 2a/2b/2c
|
||||
```
|
||||
|
||||
@@ -435,3 +435,214 @@
|
||||
"message": "我是你的同事"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 群聊事件(Phase 2c)
|
||||
|
||||
### im.group.read
|
||||
|
||||
**方向:** 客户端 → 服务端
|
||||
|
||||
**说明:** 标记群聊消息已读(消息级已读回执)
|
||||
|
||||
**data 内容:**
|
||||
```json
|
||||
{
|
||||
"conversation_id": 100,
|
||||
"message_ids": [501, 502, 503]
|
||||
}
|
||||
```
|
||||
|
||||
**ACK 响应:** `{ "code": 0, "message": "ok" }`
|
||||
|
||||
---
|
||||
|
||||
### group.created
|
||||
|
||||
**方向:** 服务端 → 客户端
|
||||
|
||||
**说明:** 群聊创建成功,推送给所有初始成员
|
||||
|
||||
**data 内容:**
|
||||
```json
|
||||
{
|
||||
"group_id": 10,
|
||||
"conversation_id": 100,
|
||||
"name": "项目讨论组",
|
||||
"owner_id": 1
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### group.info.update
|
||||
|
||||
**方向:** 服务端 → 客户端
|
||||
|
||||
**说明:** 群信息更新(名称、头像、公告等),推送给所有群成员
|
||||
|
||||
**data 内容:**
|
||||
```json
|
||||
{
|
||||
"group_id": 10,
|
||||
"conversation_id": 100,
|
||||
"operator_id": 1
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### group.dissolved
|
||||
|
||||
**方向:** 服务端 → 客户端
|
||||
|
||||
**说明:** 群聊已解散,推送给所有群成员
|
||||
|
||||
**data 内容:**
|
||||
```json
|
||||
{
|
||||
"group_id": 10,
|
||||
"conversation_id": 100,
|
||||
"operator_id": 1
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### group.member.join
|
||||
|
||||
**方向:** 服务端 → 客户端
|
||||
|
||||
**说明:** 新成员加入群聊(邀请加入或审批通过),推送给所有群成员
|
||||
|
||||
**data 内容:**
|
||||
```json
|
||||
{
|
||||
"group_id": 10,
|
||||
"conversation_id": 100,
|
||||
"user_ids": [5, 6]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### group.member.kicked
|
||||
|
||||
**方向:** 服务端 → 客户端
|
||||
|
||||
**说明:** 成员被移出群聊,推送给所有群成员
|
||||
|
||||
**data 内容:**
|
||||
```json
|
||||
{
|
||||
"group_id": 10,
|
||||
"conversation_id": 100,
|
||||
"user_id": 5,
|
||||
"operator_id": 1
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### group.member.leave
|
||||
|
||||
**方向:** 服务端 → 客户端
|
||||
|
||||
**说明:** 成员主动退出群聊,推送给其余群成员
|
||||
|
||||
**data 内容:**
|
||||
```json
|
||||
{
|
||||
"group_id": 10,
|
||||
"conversation_id": 100,
|
||||
"user_id": 5
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### group.role.update
|
||||
|
||||
**方向:** 服务端 → 客户端
|
||||
|
||||
**说明:** 成员角色变更(设为/取消管理员),推送给所有群成员
|
||||
|
||||
**data 内容:**
|
||||
```json
|
||||
{
|
||||
"group_id": 10,
|
||||
"conversation_id": 100,
|
||||
"user_id": 5,
|
||||
"new_role": 1,
|
||||
"operator_id": 1
|
||||
}
|
||||
```
|
||||
|
||||
> `new_role`:0=普通成员,1=管理员,2=群主
|
||||
|
||||
---
|
||||
|
||||
### group.mute.update
|
||||
|
||||
**方向:** 服务端 → 客户端
|
||||
|
||||
**说明:** 禁言状态变更(个人禁言或全体禁言),推送给所有群成员
|
||||
|
||||
**data 内容(个人禁言):**
|
||||
```json
|
||||
{
|
||||
"group_id": 10,
|
||||
"conversation_id": 100,
|
||||
"user_id": 5,
|
||||
"is_muted": true,
|
||||
"operator_id": 1
|
||||
}
|
||||
```
|
||||
|
||||
**data 内容(全体禁言):**
|
||||
```json
|
||||
{
|
||||
"group_id": 10,
|
||||
"conversation_id": 100,
|
||||
"is_all_muted": true,
|
||||
"operator_id": 1
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### group.owner.transfer
|
||||
|
||||
**方向:** 服务端 → 客户端
|
||||
|
||||
**说明:** 群主转让,推送给所有群成员
|
||||
|
||||
**data 内容:**
|
||||
```json
|
||||
{
|
||||
"group_id": 10,
|
||||
"conversation_id": 100,
|
||||
"old_owner_id": 1,
|
||||
"new_owner_id": 5
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### group.join.request
|
||||
|
||||
**方向:** 服务端 → 客户端
|
||||
|
||||
**说明:** 新的入群申请,推送给群主和管理员
|
||||
|
||||
**data 内容:**
|
||||
```json
|
||||
{
|
||||
"group_id": 10,
|
||||
"request_id": 20,
|
||||
"user_id": 8,
|
||||
"user_nickname": "新用户",
|
||||
"message": "请让我加入"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -92,11 +92,11 @@ EchoChat 采用 **「精简单体 + 媒体微服务」** 架构,核心思想
|
||||
| ws | WebSocket 连接管理(Hub/Client/PubSub)、在线状态管理(Redis SET + TTL 心跳续期)、好友上下线实时通知(FriendIDsGetter 接口注入) | ✅ Phase 2a |
|
||||
| contact | 好友关系管理(申请/接受/拒绝/删除/拉黑)、好友分组(CRUD + 移动)、用户搜索、好友推荐(批量查询优化) | ✅ Phase 2a |
|
||||
| im | 即时消息收发(单聊)、会话管理、消息存储、撤回、搜索、离线推送 | ✅ Phase 2b |
|
||||
| group | 群聊管理(建群/加入/退出/角色/禁言/@提醒/群公告/入群审批) | 🔜 Phase 2c |
|
||||
| file | 文件上传(MinIO 对象存储 + 通用上传 API) | 🔜 Phase 2c |
|
||||
| group | 群聊管理(建群/加入/退出/角色/禁言/@提醒/群公告/入群审批) | ✅ Phase 2c |
|
||||
| file | 文件上传(MinIO 对象存储 + 通用上传 API) | ✅ Phase 2c |
|
||||
| meeting | 会议创建/管理、信令转发、mediasoup 资源编排 | 📋 后续 |
|
||||
| notify | 通知推送、会议邀请、好友申请通知 | 📋 后续 |
|
||||
| admin | 后台管理(用户管理 + 角色权限管理 + 在线监控 + 好友关系管理 + 群聊管理、会议监控、系统配置) | ✅ Phase 1/2a, 🔜 2c |
|
||||
| admin | 后台管理(用户管理 + 角色权限管理 + 在线监控 + 好友关系管理 + 群聊管理、会议监控、系统配置) | ✅ Phase 1/2a/2c |
|
||||
|
||||
**不负责的事情:** 不处理 RTP 媒体数据、不参与音视频转发、不做 WebRTC 协议协商。
|
||||
|
||||
|
||||
@@ -2,9 +2,11 @@
|
||||
|
||||
> **适用范围**:EchoChat Go 后端(`backend/go-service/`)
|
||||
> **创建日期**:2026-03-04
|
||||
> **最后更新**:2026-03-04(Phase 2c 设计阶段整理)
|
||||
> **最后更新**:2026-03-04(基于全量代码审查,以实际代码为准)
|
||||
> **关联文档**:`docs/conventions/frontend-backend-integration.md`
|
||||
|
||||
**核心原则:本文档所有代码模板均直接摘自项目现有代码,编写新模块时必须严格遵循,禁止自创新模式。**
|
||||
|
||||
---
|
||||
|
||||
## 一、模块分层架构
|
||||
@@ -14,13 +16,13 @@
|
||||
```
|
||||
app/{module_name}/
|
||||
├── controller/
|
||||
│ └── {module}_controller.go # HTTP 请求处理(参数绑定/校验 → 调用 Service → 响应)
|
||||
│ └── {module}_controller.go # HTTP 请求处理
|
||||
├── service/
|
||||
│ └── {module}_service.go # 业务逻辑(事务协调、权限校验、多 DAO 编排)
|
||||
│ └── {module}_service.go # 业务逻辑
|
||||
├── dao/
|
||||
│ └── {module}_dao.go # 数据访问(GORM 操作,不含业务逻辑)
|
||||
│ └── {module}_dao.go # 数据访问
|
||||
├── model/
|
||||
│ └── {module}.go # 数据模型(GORM 结构体,对应数据库表)
|
||||
│ └── {module}.go # 数据模型
|
||||
├── handler/
|
||||
│ └── {module}_handler.go # [可选] WS 事件处理器
|
||||
├── router.go # 路由注册
|
||||
@@ -46,254 +48,417 @@ Controller → Service → DAO → 数据库
|
||||
|
||||
---
|
||||
|
||||
## 三、跨模块接口注入模式
|
||||
## 三、日志 API(以实际代码为准,严禁使用不存在的 API)
|
||||
|
||||
项目 logs 包(`pkg/logs`)**只有以下 5 个公开日志方法**:
|
||||
|
||||
```go
|
||||
logs.Debug(ctx, funcName, message, ...zap.Field)
|
||||
logs.Info(ctx, funcName, message, ...zap.Field)
|
||||
logs.Warn(ctx, funcName, message, ...zap.Field)
|
||||
logs.Error(ctx, funcName, message, ...zap.Field)
|
||||
logs.Fatal(ctx, funcName, message, ...zap.Field)
|
||||
```
|
||||
|
||||
辅助方法:`logs.Init()`, `logs.Sync()`, `logs.GetTraceID()`, `logs.MaskEmail()`
|
||||
|
||||
**不存在的 API(严禁调用)**:`LogFunctionEntry`、`LogFunctionExit`、`LogSuccess`、`LogFailure` 等均不存在。
|
||||
|
||||
**funcName 命名规则**:`"{层级}.{文件名}.{方法名}"`
|
||||
- DAO 层:`"dao.conversation_dao.FindPrivateConversation"`
|
||||
- Service 层:`"service.im_service.SendMessage"`
|
||||
- Controller 层:`"controller.auth_controller.Register"`
|
||||
|
||||
---
|
||||
|
||||
## 四、Controller 层代码风格
|
||||
|
||||
项目中存在两种 Controller 风格,新模块应根据所属类型选择对应风格:
|
||||
|
||||
### 4.1 前台业务模块 Controller(contact/im 模块风格)
|
||||
|
||||
**适用于**:contact、im、group、file 等前台用户端业务模块
|
||||
|
||||
**特征**:接收器 `ctl`,不记日志,方法级 `handleError`
|
||||
|
||||
```go
|
||||
// 摘自 app/contact/controller/contact_controller.go(实际代码)
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"github.com/echochat/backend/app/contact/service"
|
||||
"github.com/echochat/backend/app/dto"
|
||||
"github.com/echochat/backend/pkg/middleware"
|
||||
"github.com/echochat/backend/pkg/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type ContactController struct {
|
||||
contactService *service.ContactService
|
||||
}
|
||||
|
||||
func NewContactController(contactService *service.ContactService) *ContactController {
|
||||
return &ContactController{contactService: contactService}
|
||||
}
|
||||
|
||||
// GetFriendList 获取好友列表
|
||||
// GET /api/v1/contacts?group_id=xx
|
||||
func (ctl *ContactController) GetFriendList(c *gin.Context) {
|
||||
ctx := c.Request.Context()
|
||||
userID, ok := middleware.GetCurrentUserID(c)
|
||||
if !ok {
|
||||
utils.ResponseUnauthorized(c, "无法获取当前用户信息")
|
||||
return
|
||||
}
|
||||
// ... 参数解析 ...
|
||||
friends, err := ctl.contactService.GetFriendList(ctx, userID, groupID)
|
||||
if err != nil {
|
||||
ctl.handleError(c, err, "获取好友列表失败")
|
||||
return
|
||||
}
|
||||
utils.ResponseOK(c, friends)
|
||||
}
|
||||
|
||||
// handleError 统一业务错误映射
|
||||
func (ctl *ContactController) handleError(c *gin.Context, err error, fallbackMsg ...string) {
|
||||
switch err {
|
||||
case service.ErrSelfRequest:
|
||||
utils.ResponseBadRequest(c, err.Error())
|
||||
case service.ErrAlreadyFriend:
|
||||
utils.ResponseBadRequest(c, err.Error())
|
||||
// ... 覆盖所有已知业务错误 ...
|
||||
default:
|
||||
msg := "服务器内部错误"
|
||||
if len(fallbackMsg) > 0 && fallbackMsg[0] != "" {
|
||||
msg = fallbackMsg[0]
|
||||
}
|
||||
utils.ResponseError(c, msg)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关键要点**:
|
||||
- 接收器变量名:`ctl`
|
||||
- **不导入** `logs` 和 `zap`,不记日志
|
||||
- 没有 `funcName` 变量
|
||||
- 用 `ctx := c.Request.Context()` 获取上下文
|
||||
- 用 `middleware.GetCurrentUserID(c)` 获取当前用户
|
||||
- `handleError` 是**方法**(不是包级函数),签名 `(c *gin.Context, err error, fallbackMsg ...string)`
|
||||
- 响应统一用 `utils.ResponseOK/ResponseBadRequest/ResponseError` 等
|
||||
- 参数绑定:JSON 用 `c.ShouldBindJSON(&req)`,Query 用 `c.ShouldBindQuery(&req)`
|
||||
|
||||
### 4.2 auth/admin 模块 Controller 风格
|
||||
|
||||
**适用于**:auth、admin 模块(涉及安全审计和管理操作,需要更详细的日志)
|
||||
|
||||
**特征**:接收器 `ctrl`/`ctl`,有 funcName+logs+zap,包级函数 `handleAuthError` 或内联错误处理
|
||||
|
||||
```go
|
||||
// 摘自 app/auth/controller/auth_controller.go(实际代码)
|
||||
func (ctrl *AuthController) Register(c *gin.Context) {
|
||||
funcName := "controller.auth_controller.Register"
|
||||
ctx := c.Request.Context()
|
||||
|
||||
var req dto.RegisterRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
logs.Warn(ctx, funcName, "参数校验失败", zap.Error(err))
|
||||
utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
logs.Info(ctx, funcName, "注册请求",
|
||||
zap.String("username", req.Username),
|
||||
zap.String("email", logs.MaskEmail(req.Email)),
|
||||
)
|
||||
|
||||
resp, err := ctrl.authService.Register(ctx, &req)
|
||||
if err != nil {
|
||||
handleAuthError(c, err, "注册失败")
|
||||
return
|
||||
}
|
||||
utils.ResponseOK(c, resp)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、Service 层代码风格(统一)
|
||||
|
||||
所有模块的 Service 层风格一致。
|
||||
|
||||
```go
|
||||
// 摘自 app/im/service/im_service.go + app/contact/service/contact_service.go(实际代码)
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/echochat/backend/app/xxx/dao"
|
||||
"github.com/echochat/backend/app/dto"
|
||||
"github.com/echochat/backend/pkg/logs"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// 错误变量在包顶部定义
|
||||
var (
|
||||
ErrNotFriend = errors.New("对方不是你的好友")
|
||||
ErrEmptyContent = errors.New("消息内容不能为空")
|
||||
// ...
|
||||
)
|
||||
|
||||
type IMService struct {
|
||||
convDAO *dao.ConversationDAO
|
||||
msgDAO *dao.MessageDAO
|
||||
// ...
|
||||
}
|
||||
|
||||
func NewIMService(...) *IMService {
|
||||
return &IMService{...}
|
||||
}
|
||||
|
||||
// SendMessage 发送消息
|
||||
func (s *IMService) SendMessage(ctx context.Context, senderID int64, req *dto.SendMessageRequest) (*dto.MessageDTO, error) {
|
||||
funcName := "service.im_service.SendMessage"
|
||||
logs.Info(ctx, funcName, "发送消息",
|
||||
zap.Int64("sender_id", senderID),
|
||||
zap.Int64("conversation_id", req.ConversationID))
|
||||
|
||||
// 业务逻辑...
|
||||
if err != nil {
|
||||
logs.Error(ctx, funcName, "操作失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
```
|
||||
|
||||
**关键要点**:
|
||||
- 接收器变量名:`s`
|
||||
- 每个公开方法开头声明 `funcName` 并记一次 Info/Debug 日志
|
||||
- 错误时记 `logs.Error` 日志
|
||||
- 错误定义为包级 `var ErrXxx = errors.New("中文描述")`
|
||||
- 跨模块依赖通过 interface 注入(定义在 Service 包内)
|
||||
|
||||
---
|
||||
|
||||
## 六、DAO 层代码风格(统一)
|
||||
|
||||
```go
|
||||
// 摘自 app/im/dao/conversation_dao.go(实际代码)
|
||||
type ConversationDAO struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewConversationDAO(db *gorm.DB) *ConversationDAO {
|
||||
return &ConversationDAO{db: db}
|
||||
}
|
||||
|
||||
func (d *ConversationDAO) FindPrivateConversation(ctx context.Context, userID, targetUserID int64) (*model.Conversation, error) {
|
||||
funcName := "dao.conversation_dao.FindPrivateConversation"
|
||||
logs.Debug(ctx, funcName, "查找单聊会话",
|
||||
zap.Int64("user_id", userID), zap.Int64("target_user_id", targetUserID))
|
||||
|
||||
var conv model.Conversation
|
||||
err := d.db.WithContext(ctx).
|
||||
Raw(`SELECT ...`, userID, targetUserID).
|
||||
Scan(&conv).Error
|
||||
|
||||
if err != nil {
|
||||
logs.Error(ctx, funcName, "查找单聊会话失败", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
if conv.ID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return &conv, nil
|
||||
}
|
||||
```
|
||||
|
||||
**关键要点**:
|
||||
- 接收器变量名:`d`
|
||||
- 所有 DB 操作使用 `d.db.WithContext(ctx)`
|
||||
- 查询方法开头记 `logs.Debug`,写操作记 `logs.Info`
|
||||
- 错误时记 `logs.Error`
|
||||
|
||||
---
|
||||
|
||||
## 七、Router 代码风格
|
||||
|
||||
```go
|
||||
// 摘自 app/contact/router.go(实际代码)
|
||||
package contact
|
||||
|
||||
import (
|
||||
"github.com/echochat/backend/app/contact/controller"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func RegisterRoutes(r *gin.Engine, ctrl *controller.ContactController, jwtAuth gin.HandlerFunc) {
|
||||
authed := r.Group("/api/v1")
|
||||
authed.Use(jwtAuth)
|
||||
{
|
||||
authed.GET("/contacts", ctrl.GetFriendList)
|
||||
authed.POST("/contacts/request", ctrl.SendFriendRequest)
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关键要点**:
|
||||
- 函数签名:`RegisterRoutes(r *gin.Engine, ctrl *controller.XxxController, jwtAuth gin.HandlerFunc)`
|
||||
- 参数名用 `r`(不是 `engine`)
|
||||
- 路由组用 `r.Group(...)` + `.Use(jwtAuth)`
|
||||
|
||||
---
|
||||
|
||||
## 八、Provider 代码风格
|
||||
|
||||
```go
|
||||
// 摘自 app/contact/provider.go(简洁风格)
|
||||
package contact
|
||||
|
||||
import (
|
||||
"github.com/echochat/backend/app/contact/controller"
|
||||
"github.com/echochat/backend/app/contact/dao"
|
||||
"github.com/echochat/backend/app/contact/service"
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
var ContactSet = wire.NewSet(
|
||||
dao.NewFriendshipDAO,
|
||||
dao.NewFriendGroupDAO,
|
||||
service.NewContactService,
|
||||
controller.NewContactController,
|
||||
)
|
||||
```
|
||||
|
||||
**ProviderSet 命名**:`{ModuleName}Set`(如 `AuthSet`、`ContactSet`、`IMSet`、`FileSet`、`GroupSet`)
|
||||
|
||||
---
|
||||
|
||||
## 九、常量定义风格
|
||||
|
||||
```go
|
||||
// 摘自 app/constants/im.go(实际代码)
|
||||
package constants
|
||||
|
||||
const (
|
||||
ConversationTypePrivate = 1
|
||||
ConversationTypeGroup = 2
|
||||
)
|
||||
|
||||
var ConversationTypeMap = map[int]string{
|
||||
ConversationTypePrivate: "单聊",
|
||||
ConversationTypeGroup: "群聊",
|
||||
}
|
||||
```
|
||||
|
||||
**关键要点**:
|
||||
- 常量命名 camelCase:`GroupStatusNormal`(不是 `GROUP_STATUS_NORMAL`)
|
||||
- 每组常量配套一个 `XxxMap` 中文映射
|
||||
- 文件按模块拆分:`im.go`、`contact.go`、`group.go`
|
||||
|
||||
---
|
||||
|
||||
## 十、DTO 定义风格
|
||||
|
||||
```go
|
||||
// 摘自 app/dto/im_dto.go(实际代码)
|
||||
package dto
|
||||
|
||||
type SendMessageRequest struct {
|
||||
ConversationID int64 `json:"conversation_id"`
|
||||
TargetUserID int64 `json:"target_user_id"`
|
||||
Type int `json:"type"`
|
||||
Content string `json:"content"`
|
||||
ClientMsgID string `json:"client_msg_id"`
|
||||
}
|
||||
|
||||
type MessageDTO struct {
|
||||
ID int64 `json:"id"`
|
||||
ConversationID int64 `json:"conversation_id"`
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**关键要点**:
|
||||
- 按模块分文件:`im_dto.go`、`contact_dto.go`、`admin_dto.go`、`group_dto.go`
|
||||
- Request 用 `json` tag(POST body)或 `form` tag(GET query)
|
||||
- Response 用 `json` tag + `omitempty` 可选
|
||||
- 时间字段在 DTO 中用 `string` 类型(格式化后的 `"2006-01-02 15:04:05"`)
|
||||
|
||||
---
|
||||
|
||||
## 十一、Model 定义风格
|
||||
|
||||
```go
|
||||
// 摘自 app/im/model/conversation.go(实际代码)
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type Conversation struct {
|
||||
ID int64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
Type int `json:"type" gorm:"not null;default:1"`
|
||||
CreatorID int64 `json:"creator_id" gorm:"not null"`
|
||||
LastMessageID *int64 `json:"last_message_id"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"not null;autoCreateTime;type:timestamp(0)"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"not null;autoUpdateTime;type:timestamp(0)"`
|
||||
}
|
||||
|
||||
func (Conversation) TableName() string {
|
||||
return "im_conversations"
|
||||
}
|
||||
```
|
||||
|
||||
**关键要点**:
|
||||
- 必须有 `TableName()` 方法
|
||||
- 时间字段用 `time.Time`,GORM tag 含 `type:timestamp(0)`
|
||||
- 可选字段用指针类型(`*int64`、`*time.Time`、`*string`)
|
||||
- 每个字段必须有注释说明用途
|
||||
|
||||
---
|
||||
|
||||
## 十二、跨模块接口注入模式
|
||||
|
||||
模块间通信通过 **interface injection** 实现,禁止直接 import 其他模块包。
|
||||
|
||||
### 3.1 标准流程
|
||||
### 标准流程
|
||||
|
||||
```
|
||||
步骤1: 在消费方 Service 中定义 interface(如 im/service → GroupMemberChecker)
|
||||
步骤2: 在提供方 DAO 中实现该接口(如 group/dao.GroupDAO)
|
||||
步骤3: 在 app/provider/wire.go 中用 wire.Bind 绑定接口和实现
|
||||
步骤4: 重新生成 wire_gen.go
|
||||
步骤1: 在消费方 Service 中定义 interface
|
||||
步骤2: 在提供方 DAO 中实现该接口
|
||||
步骤3: 在 app/provider/wire.go 中用 wire.Bind 绑定
|
||||
步骤4: 更新 wire_gen.go
|
||||
```
|
||||
|
||||
### 3.2 接口命名约定
|
||||
|
||||
| 接口类型 | 命名模式 | 示例 |
|
||||
|---------|---------|------|
|
||||
| 数据查询 | `{Entity}{Action}er` | `FriendIDsGetter`, `GroupInfoGetter` |
|
||||
| 状态检查 | `{Entity}{State}Checker` | `GroupMemberChecker`, `OnlineChecker` |
|
||||
| 操作执行 | `{Entity}{Action}er` | `OfflineMessagePusher` |
|
||||
|
||||
### 3.3 已有接口注入清单
|
||||
### 已有接口注入清单
|
||||
|
||||
| 接口 | 定义方 | 实现方 | 用途 |
|
||||
|------|--------|--------|------|
|
||||
| FriendIDsGetter | ws/handler | contact/dao | 获取用户好友 ID 列表 |
|
||||
| FriendChecker | im/service | contact/dao | 检查是否为好友 |
|
||||
| UserInfoGetter | im/service | auth/dao | 获取用户信息 |
|
||||
| UserInfoGetter | im/service | contact/dao | 获取用户信息 |
|
||||
| OnlineChecker | contact/service | ws/service | 检查在线状态 |
|
||||
| OfflineMessagePusher | im/service | ws/handler | 离线消息推送 |
|
||||
| GroupMemberChecker | im/service | group/dao | 检查群成员身份(Phase 2c) |
|
||||
| GroupInfoGetter | im/service | group/dao | 获取群信息(Phase 2c) |
|
||||
| GroupRoleChecker | im/service | group/dao | 检查用户群角色(Phase 2c) |
|
||||
| TokenValidator | ws/handler | auth/service | WS 连接 Token 校验 |
|
||||
|
||||
---
|
||||
|
||||
## 四、Wire 依赖注入规范
|
||||
## 十三、批量查询优化
|
||||
|
||||
- 每个模块在 `provider.go` 中导出 `ProviderSet`(`wire.NewSet(...)`)
|
||||
- 接口绑定统一在 `app/provider/wire.go` 中声明
|
||||
- 修改 wire.go 后必须重新运行 `wire gen ./app/provider/` 生成 wire_gen.go
|
||||
- Wire 有过手动 patch 历史(Phase 2b),修改后需检查 wire_gen.go 一致性
|
||||
获取关联信息时,**必须使用批量查询 + Map 映射,严禁 N+1 查询**。
|
||||
|
||||
---
|
||||
|
||||
## 五、日志记录标准
|
||||
## 十四、系统消息规范(Phase 2c+)
|
||||
|
||||
所有 DAO 和 Service 的公开方法必须记录入口和出口日志:
|
||||
|
||||
```go
|
||||
func (d *SomeDAO) SomeMethod(ctx context.Context, param int64) (result *Model, err error) {
|
||||
funcName := "dao.some_dao.SomeMethod"
|
||||
|
||||
logs.LogFunctionEntry(ctx, funcName, map[string]interface{}{
|
||||
"param": param,
|
||||
})
|
||||
|
||||
defer func() {
|
||||
logs.LogFunctionExit(ctx, funcName, result, err)
|
||||
}()
|
||||
|
||||
// 业务逻辑
|
||||
return result, err
|
||||
}
|
||||
```
|
||||
|
||||
**funcName 命名规则:** `{层级}.{模块}_{文件}.{方法名}`
|
||||
- DAO 层:`dao.group_dao.CreateGroup`
|
||||
- Service 层:`service.group_service.CreateGroup`
|
||||
- Controller 层:`controller.group_controller.CreateGroup`
|
||||
系统消息写入 `im_messages` 表,`type=10`(`MessageTypeSystem`),`sender_id=0`(表示系统),`content` 使用**纯文本格式**。前端居中显示、灰色小字体、无头像、无气泡。
|
||||
|
||||
---
|
||||
|
||||
## 六、Controller 错误处理标准
|
||||
## 十五、依赖管理
|
||||
|
||||
```go
|
||||
func (ctrl *Controller) HandleAction(c *gin.Context) {
|
||||
funcName := "controller.module.HandleAction"
|
||||
|
||||
// 参数绑定
|
||||
var req dto.SomeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.ResponseBadRequest(c, "参数校验失败: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 调用 Service
|
||||
result, err := ctrl.service.DoAction(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
ctrl.handleError(c, funcName, err)
|
||||
return
|
||||
}
|
||||
|
||||
utils.ResponseOK(c, "操作成功", result)
|
||||
}
|
||||
|
||||
// handleError 必须覆盖所有已知业务错误
|
||||
func (ctrl *Controller) handleError(c *gin.Context, funcName string, err error) {
|
||||
switch err {
|
||||
case service.ErrNotFound:
|
||||
utils.ResponseNotFound(c, err.Error())
|
||||
case service.ErrPermission:
|
||||
utils.ResponseForbidden(c, err.Error())
|
||||
// ... 覆盖所有已知错误
|
||||
default:
|
||||
logs.Error(c.Request.Context(), funcName, "操作失败", zap.Error(err))
|
||||
utils.ResponseError(c, "操作失败")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**关键要求:**
|
||||
- `handleError` 必须覆盖所有已知业务错误,不能用 `default` 笼统处理
|
||||
- 错误 message 使用中文,面向用户
|
||||
- 未知错误必须记录日志
|
||||
|
||||
---
|
||||
|
||||
## 七、批量查询优化
|
||||
|
||||
获取关联信息时,**必须使用批量查询 + Map 映射,严禁 N+1 查询**:
|
||||
|
||||
```go
|
||||
// ✅ 正确:批量查询
|
||||
userIDs := extractUserIDs(members)
|
||||
users, _ := userDAO.GetByIDs(ctx, userIDs)
|
||||
userMap := make(map[int64]*User)
|
||||
for _, u := range users {
|
||||
userMap[u.ID] = u
|
||||
}
|
||||
for _, m := range members {
|
||||
m.Nickname = userMap[m.UserID].Nickname
|
||||
}
|
||||
|
||||
// ❌ 错误:N+1 查询
|
||||
for _, m := range members {
|
||||
user, _ := userDAO.GetByID(ctx, m.UserID)
|
||||
m.Nickname = user.Nickname
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、系统消息规范(Phase 2c+)
|
||||
|
||||
### 8.1 系统消息定义
|
||||
|
||||
系统消息是由服务端自动生成的提示类消息,写入 `im_messages` 表,type=10(`MessageTypeSystem`)。
|
||||
|
||||
### 8.2 内容格式
|
||||
|
||||
系统消息的 `content` 字段使用**纯文本格式**,不使用 JSON 结构。
|
||||
|
||||
### 8.3 前端渲染
|
||||
|
||||
系统消息在聊天页面中**居中显示,灰色小字体,无头像,无气泡**。
|
||||
|
||||
### 8.4 sender_id
|
||||
|
||||
系统消息的 `sender_id` 设为 0(表示系统),前端根据 sender_id=0 和 type=10 判断为系统消息。
|
||||
|
||||
---
|
||||
|
||||
## 九、WS 事件推送模式
|
||||
|
||||
### 9.1 S→C 推送(Service 层触发)
|
||||
|
||||
```go
|
||||
pubsub.PublishToUser(ctx, targetUserID, &ws.Message{
|
||||
Event: "group.member.join",
|
||||
Data: map[string]interface{}{...},
|
||||
})
|
||||
|
||||
pubsub.PublishToUsers(ctx, memberIDs, &ws.Message{
|
||||
Event: "im.message.new",
|
||||
Data: messageDTO,
|
||||
})
|
||||
```
|
||||
|
||||
### 9.2 C→S 事件处理(Hub 事件路由表注册)
|
||||
|
||||
```go
|
||||
hub.RegisterEvent("im.message.send", handler.HandleSendMessage)
|
||||
hub.RegisterEvent("im.message.read", handler.HandleReadMessage)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 十、前端 Store 与 API 封装规范
|
||||
|
||||
### 10.1 API 封装标准
|
||||
|
||||
文件位置:`frontend/src/api/{module}.js`
|
||||
|
||||
```javascript
|
||||
import request from '@/utils/request'
|
||||
|
||||
// 获取群详情
|
||||
export function getGroupDetail(groupId) {
|
||||
return request.get(`/groups/${groupId}`)
|
||||
}
|
||||
|
||||
// 创建群聊
|
||||
export function createGroup(data) {
|
||||
return request.post('/groups', data)
|
||||
}
|
||||
```
|
||||
|
||||
**命名规则:**
|
||||
- GET 请求:`get{Entity}` / `get{Entity}List`
|
||||
- POST 请求:`create{Entity}` / `{action}{Entity}`
|
||||
- PUT 请求:`update{Entity}` / `set{Entity}{Field}`
|
||||
- DELETE 请求:`delete{Entity}` / `remove{Entity}`
|
||||
|
||||
### 10.2 Pinia Store 标准结构
|
||||
|
||||
文件位置:`frontend/src/store/{module}.js`
|
||||
|
||||
```javascript
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useGroupStore = defineStore('group', {
|
||||
state: () => ({
|
||||
conversations: [], // 群会话列表
|
||||
currentGroup: null, // 当前群详情
|
||||
messages: {}, // {conversationId: Message[]}
|
||||
members: {}, // {groupId: Member[]}
|
||||
}),
|
||||
|
||||
getters: {
|
||||
unreadTotal: (state) => { ... },
|
||||
},
|
||||
|
||||
actions: {
|
||||
// 初始化 WS 事件监听(在 App.vue onLaunch 中调用)
|
||||
initWsListeners() { ... },
|
||||
|
||||
// 加载群会话列表
|
||||
async loadConversations() { ... },
|
||||
|
||||
// 发送群消息
|
||||
async sendMessage(conversationId, content, atUserIds) { ... },
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Store 设计原则:**
|
||||
1. **单一职责**:每个模块一个 Store,不混合不同模块数据
|
||||
2. **WS 监听统一初始化**:所有 Store 的 WS 事件监听在 `App.vue` 的 `_initGlobalWS` 中统一调用
|
||||
3. **消息缓存**:按 conversationId 键值对缓存,避免重复请求
|
||||
4. **乐观更新**:发送消息时先本地插入,再等待服务端 ack 确认
|
||||
- 添加新依赖前必须检查 `go.mod` 中的 Go 版本(当前 `go 1.23.12`)
|
||||
- 选择与当前 Go 版本兼容的包版本,禁止触发 Go 工具链自动升级
|
||||
- 优先复用 `go.mod` 中已有的间接依赖
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **适用范围**:EchoChat 项目全端(Go 后端 + admin 管理端 + frontend 用户端)
|
||||
> **创建日期**:2026-03-02
|
||||
> **最后更新**:2026-03-04(Phase 2c 设计完成:新增群聊模块、MinIO 文件存储、已读回执规划)
|
||||
> **最后更新**:2026-03-04(Phase 2c 全部完成:群聊模块 + MinIO 文件存储 + 已读回执 + 代码审查修复 14 项)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -718,7 +718,7 @@ DELETE /api/v1/im/conversations/:id/messages 清空聊天记录(个人视
|
||||
GET /api/v1/im/messages/search 全局消息搜索(GIN 全文索引)
|
||||
GET /api/v1/im/unread 全局未读消息总数
|
||||
|
||||
# 群聊管理模块(Phase 2c 规划)
|
||||
# 群聊管理模块(✅ Phase 2c 完成)
|
||||
POST /api/v1/groups 创建群聊
|
||||
GET /api/v1/groups/:id 群详情
|
||||
PUT /api/v1/groups/:id 更新群信息
|
||||
@@ -736,11 +736,11 @@ GET /api/v1/groups/:id/join-requests 入群申请列表
|
||||
PUT /api/v1/groups/:id/join-requests/:rid 审批入群申请
|
||||
GET /api/v1/groups/search 搜索公开群
|
||||
|
||||
# 已读回执(Phase 2c 规划)
|
||||
# 已读回执(✅ Phase 2c 完成)
|
||||
GET /api/v1/im/messages/:id/reads 消息已读详情
|
||||
GET /api/v1/im/messages/:id/read-count 消息已读计数
|
||||
|
||||
# 文件上传(Phase 2c 规划)
|
||||
# 文件上传(✅ Phase 2c 完成)
|
||||
POST /api/v1/upload 通用文件上传(MinIO)
|
||||
|
||||
# 会议模块
|
||||
@@ -782,7 +782,7 @@ GET /api/v1/admin/online/count 在线用户数
|
||||
GET /api/v1/admin/contacts 所有好友关系(分页)
|
||||
DELETE /api/v1/admin/contacts/:id 管理员解除好友关系
|
||||
|
||||
# 群聊管理(Phase 2c 规划)
|
||||
# 群聊管理(✅ Phase 2c 完成)
|
||||
GET /api/v1/admin/groups 群列表(分页 + 筛选)
|
||||
GET /api/v1/admin/groups/:id 群详情(含成员列表)
|
||||
DELETE /api/v1/admin/groups/:id 管理员解散群
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Phase 2c 设计文档:群聊与已读回执
|
||||
|
||||
> **状态:** 📋 设计完成,待实施
|
||||
> **状态:** ✅ 已完成实施(含代码审查修复 14 项)
|
||||
> **分支:** `feature/phase2c-group-read-receipt`
|
||||
> **前置依赖:** Phase 2b 全部完成(单聊即时通讯)
|
||||
> **最后更新:** 2026-03-04
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Phase 2c 实施计划:群聊与已读回执
|
||||
|
||||
> **状态:** 📋 待执行
|
||||
> **状态:** ✅ 已完成(含代码审查修复 14 项)
|
||||
> **设计文档:** `docs/plans/2026-03-04-phase2c-design.md`
|
||||
> **分支:** `feature/phase2c-group-read-receipt`
|
||||
> **预计 Task 数:** 14 个
|
||||
> **预计 Task 数:** 14 个(全部完成)
|
||||
> **最后更新:** 2026-03-04
|
||||
|
||||
---
|
||||
@@ -12,20 +12,20 @@
|
||||
|
||||
| Task | 阶段 | 描述 | 依赖 | 状态 |
|
||||
|------|------|------|------|------|
|
||||
| Task 0 | 基础设施 | MinIO Docker + SDK + 通用上传 API | 无 | 📋 |
|
||||
| Task 1 | 基础设施 | 数据库迁移 + Model + 常量定义 | 无 | 📋 |
|
||||
| Task 2 | 群聊后端 | Group DAO 层 | Task 1 | 📋 |
|
||||
| Task 3 | 群聊后端 | Group Service 业务逻辑 | Task 2 | 📋 |
|
||||
| Task 4 | 群聊后端 | Group Controller + Router + Wire | Task 3 | 📋 |
|
||||
| Task 5 | 群聊后端 | WS 群管理事件处理器 | Task 3 | 📋 |
|
||||
| Task 6 | 群聊后端 | IM Service 扩展(群消息 + @提醒 + 管理员撤回) | Task 2 | 📋 |
|
||||
| Task 7 | 已读回执 | 已读回执后端(ReadDAO + Service + API + WS 推送) | Task 1 | 📋 |
|
||||
| Task 8 | 已读回执 | 前端已读回执 UI(单聊标记 + 群聊计数 + 详情页) | Task 7 | 📋 |
|
||||
| Task 9 | 前端 | 群聊 Store + API 封装 + WS 事件监听 | Task 4, 5 | 📋 |
|
||||
| Task 10 | 前端 | 群聊核心页面(Tab 改造 + 群对话页 + 创建群页) | Task 9 | 📋 |
|
||||
| Task 11 | 前端 | 群聊管理页面(群设置 + 成员 + 邀请 + @选择器) | Task 10 | 📋 |
|
||||
| Task 12 | 前端 | 群聊辅助功能(审批 + 搜索 + 免打扰 + 公告 UI) | Task 11 | 📋 |
|
||||
| Task 13 | 管理端 | 管理端群聊管理 + 全量文档更新 + 代码审查 | Task 12 | 📋 |
|
||||
| Task 0 | 基础设施 | MinIO Docker + SDK + 通用上传 API | 无 | ✅ |
|
||||
| Task 1 | 基础设施 | 数据库迁移 + Model + 常量定义 | 无 | ✅ |
|
||||
| Task 2 | 群聊后端 | Group DAO 层 | Task 1 | ✅ |
|
||||
| Task 3 | 群聊后端 | Group Service 业务逻辑 | Task 2 | ✅ |
|
||||
| Task 4 | 群聊后端 | Group Controller + Router + Wire | Task 3 | ✅ |
|
||||
| Task 5 | 群聊后端 | WS 群管理事件处理器 | Task 3 | ✅ |
|
||||
| Task 6 | 群聊后端 | IM Service 扩展(群消息 + @提醒 + 管理员撤回) | Task 2 | ✅ |
|
||||
| Task 7 | 已读回执 | 已读回执后端(ReadDAO + Service + API + WS 推送) | Task 1 | ✅ |
|
||||
| Task 8 | 已读回执 | 前端已读回执 UI(单聊标记 + 群聊计数 + 详情页) | Task 7 | ✅ |
|
||||
| Task 9 | 前端 | 群聊 Store + API 封装 + WS 事件监听 | Task 4, 5 | ✅ |
|
||||
| Task 10 | 前端 | 群聊核心页面(Tab 改造 + 群对话页 + 创建群页) | Task 9 | ✅ |
|
||||
| Task 11 | 前端 | 群聊管理页面(群设置 + 成员 + 邀请 + @选择器) | Task 10 | ✅ |
|
||||
| Task 12 | 前端 | 群聊辅助功能(审批 + 搜索 + 免打扰 + 公告 UI) | Task 11 | ✅ |
|
||||
| Task 13 | 管理端 | 管理端群聊管理 + 全量文档更新 + 代码审查 | Task 12 | ✅ |
|
||||
|
||||
---
|
||||
|
||||
@@ -553,10 +553,75 @@ Task 0 + Task 12 ── Task 13 (管理端+文档+审查)
|
||||
|
||||
## 开发注意事项
|
||||
|
||||
1. **代码风格一致性**:严格遵循 Phase 2b 的代码风格(日志记录、错误处理、常量命名、DTO 设计)
|
||||
2. **接口注入模式**:新模块间通信必须走 interface injection,禁止直接 import
|
||||
3. **批量查询优化**:群成员信息获取使用批量查询 + Map 映射,避免 N+1
|
||||
4. **前端设计规范**:所有前端页面使用 ui-ux-pro-max 技能包设计
|
||||
5. **系统消息**:群管理操作产生的系统消息统一使用 type=10(MessageTypeSystem),内容格式化
|
||||
6. **权限层级**:群主(2) > 管理员(1) > 成员(0),操作时必须校验层级
|
||||
7. **Wire 依赖**:Phase 2b 中 Wire 有过手动 patch 历史,注意检查 wire_gen.go 一致性
|
||||
### 代码风格全局一致(最高优先级)
|
||||
|
||||
**编写任何新代码前,必须先阅读同层级现有模块的实际代码,严格复制其风格。** 详细规范见 `docs/conventions/backend-module-architecture.md`。
|
||||
|
||||
**强制执行流程:**
|
||||
1. 写代码前 → 用 Read/Grep 读取同类型现有文件
|
||||
2. 写代码时 → 逐行对照导入、结构体、方法签名、日志调用、错误处理
|
||||
3. 写代码后 → 与参照文件做差异比对,确认风格完全一致
|
||||
|
||||
**各层参照文件(Phase 2c 新模块必读):**
|
||||
|
||||
| 新模块文件 | 参照现有文件 |
|
||||
|-----------|------------|
|
||||
| group/controller | `app/contact/controller/contact_controller.go`、`app/im/controller/im_controller.go` |
|
||||
| group/service | `app/im/service/im_service.go`、`app/contact/service/contact_service.go` |
|
||||
| group/dao | `app/im/dao/conversation_dao.go`、`app/contact/dao/friendship_dao.go` |
|
||||
| group/model | `app/im/model/conversation.go`、`app/im/model/message.go` |
|
||||
| group/router.go | `app/contact/router.go`、`app/im/router.go` |
|
||||
| group/provider.go | `app/contact/provider.go` |
|
||||
| constants/group.go | `app/constants/im.go`、`app/constants/contact.go` |
|
||||
| dto/group_dto.go | `app/dto/im_dto.go`、`app/dto/contact_dto.go` |
|
||||
|
||||
**严格禁止事项:**
|
||||
- 禁止调用不存在的 API(如 `logs.LogFunctionEntry`)
|
||||
- 禁止引入与当前 Go 版本不兼容的依赖
|
||||
- 禁止自创新封装模式(如自定义错误处理框架)
|
||||
- 禁止在 Controller 层记日志(auth/admin 模块除外)
|
||||
- 禁止使用未经项目验证的第三方库
|
||||
|
||||
### Code Review 遗留问题清单(Important/Minor)
|
||||
|
||||
以下问题在首轮 Code Review 中被标记为 Important 或 Minor,已在后续 Task 中逐步处理。
|
||||
|
||||
#### Important 级别
|
||||
|
||||
| # | 模块 | 问题 | 修复优先级 |
|
||||
|---|------|------|-----------|
|
||||
| I-1 | group/service | `UpdateGroup` 中 `_ = member` 无效赋值,应改为 `_, _, err :=` | 低(代码清洁) |
|
||||
| I-2 | group/service | `MuteMember` 使用 `ErrCannotKickHigherRole` 语义不精确,应新增通用错误 | 中 |
|
||||
| I-3 | group/dao | `SearchGroups` 的 `to_tsquery` 对特殊字符敏感,应改用 `plainto_tsquery` | 高(用户输入安全) |
|
||||
| I-4 | group/service | `InviteMembers` 循环逐条插入缺少事务包裹和批量优化 | 中(性能) |
|
||||
| I-5 | group/controller | `handleError` 缺少 `ErrAlreadyMuted/ErrUserMuted/ErrGroupAllMuted` 映射 | 高 |
|
||||
| I-6 | group/model | `MessageRead` 放在 `group/model` 而非 `im/model`,领域归属可议 | 低(架构决策) |
|
||||
| I-7 | im/service | `sendGroupMessage` 推送未检查成员免打扰设置 | 中(业务逻辑) |
|
||||
| I-8 | im/service | 群消息推送逐用户查询 N+1 问题 | 中(性能) |
|
||||
| I-9 | im/service | WS 群已读事件 `im.message.read` 处理器未注册 | 高(Task 5 实现) |
|
||||
| I-10 | im/service | 消息推送数据缺少 `conv_type` 字段 | 高(前端适配) |
|
||||
| I-11 | im/service | `GetMessageReadDetail` 无分页参数 | 中 |
|
||||
| I-12 | file/service | 文件上传缺少 MIME 类型校验 | 中(安全性) |
|
||||
| I-13 | file/service | 上传返回 URL 使用内部地址,应返回可配置前缀 | 中 |
|
||||
| I-14 | pkg/storage | MinIO 初始化无超时控制 | 低 |
|
||||
|
||||
#### Minor 级别
|
||||
|
||||
| # | 模块 | 问题 |
|
||||
|---|------|------|
|
||||
| M-1 | group/dao | `GetMemberCount` / `GetMemberIDs` 缺少 funcName 和日志 |
|
||||
| M-2 | group/dao | `HasRead` 缺少 funcName 和日志 |
|
||||
| M-3 | group/provider | 与 IM 模块 Provider 风格略有差异(无 ProvideXxx 包装) |
|
||||
| M-4 | group/service | `SearchGroups` 逐群查 `GetMemberCount`,建议批量 COUNT |
|
||||
| M-5 | group/service | `imMember` 内部类型可直接使用 `imModel.ConversationMember` |
|
||||
| M-6 | group/controller | `SetAllMuted` 使用匿名结构体,建议改用 DTO |
|
||||
|
||||
### 其他注意事项
|
||||
|
||||
1. **接口注入模式**:新模块间通信必须走 interface injection,禁止直接 import
|
||||
2. **批量查询优化**:群成员信息获取使用批量查询 + Map 映射,避免 N+1
|
||||
3. **前端设计规范**:所有前端页面使用 ui-ux-pro-max 技能包设计
|
||||
4. **系统消息**:群管理操作产生的系统消息统一使用 type=10(MessageTypeSystem),内容格式化
|
||||
5. **权限层级**:群主(2) > 管理员(1) > 成员(0),操作时必须校验层级
|
||||
6. **Wire 依赖**:Phase 2b 中 Wire 有过手动 patch 历史,注意检查 wire_gen.go 一致性
|
||||
7. **依赖管理**:当前 Go 版本 1.23.12,添加新依赖必须选择兼容版本
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# EchoChat 项目开发进度
|
||||
|
||||
> **最后更新**:2026-03-04(Phase 2c 设计完成,待实施)
|
||||
> **当前阶段**:Phase 2c 设计完成,准备开始实施
|
||||
> **当前分支**:`feature/phase2c-group-read-receipt`(已从 phase2b 拉出)
|
||||
> **最后更新**:2026-03-04(Phase 2c 代码审查修复完成)
|
||||
> **当前阶段**:Phase 2c 代码审查修复完成,待最终验证
|
||||
> **当前分支**:`feature/phase2c-group-read-receipt`
|
||||
> **实施计划**:`docs/plans/2026-03-04-phase2c-implementation.plan.md`
|
||||
> **设计文档**:`docs/plans/2026-03-04-phase2c-design.md`
|
||||
|
||||
@@ -178,14 +178,24 @@ EchoChat/
|
||||
│ └── router/router.go
|
||||
├── frontend/ # 前台(uni-app)
|
||||
│ └── src/
|
||||
│ ├── api/{auth,contact,user,im}.js
|
||||
│ ├── api/{auth,contact,user,im,group,file}.js
|
||||
│ ├── constants/group.js # [Phase 2c] 群聊角色/状态常量
|
||||
│ ├── services/websocket.js
|
||||
│ ├── store/{user,websocket,contact,chat}.js
|
||||
│ ├── pages/chat/ # [Phase 2b] 4 个页面
|
||||
│ │ ├── index.vue # 会话列表
|
||||
│ │ ├── conversation.vue # 聊天对话
|
||||
│ ├── store/{user,websocket,contact,chat,group}.js
|
||||
│ ├── pages/chat/ # [Phase 2b] 5 个页面
|
||||
│ │ ├── index.vue # 会话列表(含群聊 Tab)
|
||||
│ │ ├── conversation.vue # 单聊对话
|
||||
│ │ ├── read-detail.vue # [Phase 2c] 已读详情
|
||||
│ │ ├── settings.vue # 聊天设置
|
||||
│ │ └── search.vue # 消息搜索
|
||||
│ ├── pages/group/ # [Phase 2c] 7 个页面
|
||||
│ │ ├── conversation.vue # 群聊对话(含 @选择器 + 已读计数 + 禁言提示)
|
||||
│ │ ├── create.vue # 创建群聊
|
||||
│ │ ├── settings.vue # 群设置
|
||||
│ │ ├── members.vue # 成员管理
|
||||
│ │ ├── invite.vue # 邀请入群
|
||||
│ │ ├── join-requests.vue # 入群审批
|
||||
│ │ └── search.vue # 搜索群聊
|
||||
│ ├── pages/contact/ # [Phase 2a] 6 个页面
|
||||
│ └── components/CustomTabBar.vue(含 badge)
|
||||
├── admin/ # 管理端(Vue 3 + Element Plus)
|
||||
@@ -237,12 +247,12 @@ cd frontend && npm run dev:h5
|
||||
|
||||
---
|
||||
|
||||
## 八、下一阶段:Phase 2c — 群聊与已读回执
|
||||
## 八、Phase 2c — 群聊与已读回执
|
||||
|
||||
> **状态:** 设计完成,待实施
|
||||
> **状态:** 实施中
|
||||
> **设计文档:** `docs/plans/2026-03-04-phase2c-design.md`
|
||||
> **实施计划:** `docs/plans/2026-03-04-phase2c-implementation.plan.md`
|
||||
> **分支:** `feature/phase2c-group-read-receipt`(已创建,从 phase2b 拉出)
|
||||
> **分支:** `feature/phase2c-group-read-receipt`
|
||||
|
||||
### 功能范围
|
||||
|
||||
@@ -252,29 +262,84 @@ cd frontend && npm run dev:h5
|
||||
| 群消息 | 复用 im.message.* 事件 + @某人/@所有人 + 管理员撤回(无时限)+ 系统消息 |
|
||||
| 已读回执 | 单聊会话级(last_read_msg_id)+ 群聊消息级(im_message_reads 表)+ 实时推送 |
|
||||
| MinIO | Docker 容器 + Go SDK + 通用上传 API(群头像) |
|
||||
| 管理端 | 群列表/群详情/解散群/移除成员 |
|
||||
| 管理端 | 群列表/群详情/解散群 |
|
||||
| 前端 | 9 个新页面 + 群聊 Store + 会话列表 Tab 改造 |
|
||||
|
||||
### Task 概览(14 个)
|
||||
### Task 完成状态
|
||||
|
||||
| Task | 描述 | 状态 |
|
||||
|------|------|------|
|
||||
| Task 0 | MinIO Docker + SDK + 通用上传 API | 📋 |
|
||||
| Task 1 | 数据库迁移 + Model + 常量 | 📋 |
|
||||
| Task 2 | Group DAO 层 | 📋 |
|
||||
| Task 3 | Group Service 业务逻辑 | 📋 |
|
||||
| Task 4 | Group Controller + Router + Wire | 📋 |
|
||||
| Task 5 | WS 群管理事件处理器 | 📋 |
|
||||
| Task 6 | IM Service 扩展(群消息/@提醒/管理员撤回) | 📋 |
|
||||
| Task 7 | 已读回执后端 | 📋 |
|
||||
| Task 8 | 前端已读回执 UI | 📋 |
|
||||
| Task 9 | 前端群聊 Store + API + WS 监听 | 📋 |
|
||||
| Task 10 | 群聊核心页面(Tab + 对话 + 创建) | 📋 |
|
||||
| Task 11 | 群聊管理页面(设置 + 成员 + 邀请 + @选择器) | 📋 |
|
||||
| Task 12 | 群聊辅助功能(审批 + 搜索 + 免打扰 + 公告) | 📋 |
|
||||
| Task 13 | 管理端 + 文档更新 + 代码审查 | 📋 |
|
||||
| Task 0 | MinIO Docker + SDK + 通用上传 API | ✅ 完成 |
|
||||
| Task 1 | 数据库迁移 + Model + 常量 | ✅ 完成 |
|
||||
| Task 2 | Group DAO 层 | ✅ 完成 |
|
||||
| Task 3 | Group Service 业务逻辑 + WS 推送 | ✅ 完成 |
|
||||
| Task 4 | Group Controller + Router + Wire | ✅ 完成 |
|
||||
| Task 5 | IM Service 扩展(群消息/@提醒/管理员撤回) | ✅ 完成 |
|
||||
| Task 6 | 已读回执后端(单聊 + 群聊) | ✅ 完成 |
|
||||
| Task 7 | 代码审查修复(Critical 4 项 + Important 4 项) | ✅ 完成 |
|
||||
| Task 8 | 前端已读回执 UI(单聊标记 + 群聊计数 + 详情页) | ✅ 完成 |
|
||||
| Task 9 | 前端群聊 Store + API 封装 + WS 事件监听 | ✅ 完成 |
|
||||
| Task 10 | 群聊核心页面(Tab 切换 + 群聊对话页 + 创建群聊页) | ✅ 完成 |
|
||||
| Task 11 | 群聊管理页面(群设置 + 成员管理 + 邀请入群) | ✅ 完成 |
|
||||
| Task 12 | 群聊辅助功能(入群审批 + 搜索群聊) | ✅ 完成 |
|
||||
| Task 13 | 管理端群组管理 + 文档更新 | ✅ 完成 |
|
||||
| 代码审查修复 | 14 项修复(Critical×5 + Important×4 + Minor×2 + Suggestion×3) | ✅ 完成 |
|
||||
|
||||
### 代码审查修复详情(Phase 2c)
|
||||
|
||||
| # | 优先级 | 修复内容 |
|
||||
|---|--------|----------|
|
||||
| Fix C1 | Critical | conversation.vue 角色类型不匹配:字符串改为 GROUP_ROLE 数字常量 |
|
||||
| Fix C2 | Critical | settings.vue 群公告字段名 announcement 改为 notice(对齐后端 DTO) |
|
||||
| Fix C3 | Critical | chat.js sendMessage 未传递 at_user_ids 到 WS payload |
|
||||
| Fix C4 | Critical | create.vue 创建成功后导航错误:改为 /pages/group/conversation + 正确参数 |
|
||||
| Fix C5 | Critical | admin/provider.go Wire Set 未注册 GroupManageService/Controller |
|
||||
| Fix I1 | Important | store/group.js searchGroups 添加 append 参数解决分页加载竞态 |
|
||||
| Fix I2 | Important | settings.vue 改为 fetchMembers() 刷新数据,不直接修改 computed 引用 |
|
||||
| Fix I3 | Important | 新增 constants/group.js 前端角色常量定义,消除魔数 |
|
||||
| Fix I4 | Important | group_manage_service.go 列表查询 N+1 优化:批量查询用户名和成员数 |
|
||||
| Fix M1 | Minor | file.js JSON.parse 添加 try/catch 异常保护 |
|
||||
| Fix M2 | Minor | conversation.vue isSelf 移除冗余临时状态条件 |
|
||||
| Fix S1 | Suggestion | 群聊对话页新增禁言状态检测和输入栏禁用提示 |
|
||||
| Fix S2 | Suggestion | join-requests.vue 注册 WS group.join.request 事件实时刷新 |
|
||||
| Fix S3 | Suggestion | read-detail.vue 获取失败时添加 uni.showToast 用户提示 |
|
||||
|
||||
### Phase 2c 新增内容
|
||||
|
||||
#### 后端新增模块
|
||||
- **file/** — 文件上传(MinIO SDK + 通用上传 API)
|
||||
- **group/** — 群聊管理(Controller + Service + DAO + Model + Router)
|
||||
- 18 个群管理 REST API + 11 个 WS 群事件推送
|
||||
- **im/ 扩展** — 群消息发送/撤回 + @提醒 + 单聊/群聊已读回执
|
||||
- **admin/ 扩展** — 群组列表 + 群组详情 + 解散群聊
|
||||
|
||||
#### 前端新增页面(9 个)
|
||||
| 页面 | 路径 | 功能 |
|
||||
|------|------|------|
|
||||
| 群聊对话页 | `pages/group/conversation.vue` | 群消息收发 + @选择器 + 已读计数 |
|
||||
| 创建群聊页 | `pages/group/create.vue` | 好友多选 + 群名称输入 |
|
||||
| 群设置页 | `pages/group/settings.vue` | 群信息修改 + 成员概览 + 退出/解散 |
|
||||
| 群成员页 | `pages/group/members.vue` | 成员列表 + 角色管理 + 禁言操作 |
|
||||
| 邀请入群页 | `pages/group/invite.vue` | 好友多选 + 排除已在群内成员 |
|
||||
| 入群审批页 | `pages/group/join-requests.vue` | 申请列表 + 通过/拒绝操作 |
|
||||
| 搜索群聊页 | `pages/group/search.vue` | 关键词搜索 + 申请加入 |
|
||||
| 已读详情页 | `pages/chat/read-detail.vue` | 已读/未读成员列表(群聊消息级) |
|
||||
| 会话列表改造 | `pages/chat/index.vue` | Tab 切换(全部/单聊/群聊)+ @标记 + 免打扰标识 |
|
||||
|
||||
#### 管理端新增
|
||||
| 页面 | 路径 | 功能 |
|
||||
|------|------|------|
|
||||
| 群组列表 | `views/group/list.vue` | 搜索 + 分页 + 详情弹窗 + 解散群聊 |
|
||||
|
||||
#### 数据库新增/变更
|
||||
- `im_groups` — 群信息表(新增)
|
||||
- `im_group_join_requests` — 入群申请表(新增)
|
||||
- `im_message_reads` — 群消息已读表(新增)
|
||||
- `im_conversation_members` — 扩展字段(role, nickname, is_muted, is_do_not_disturb, joined_at, at_me_count)
|
||||
- `im_messages` — 扩展字段(at_user_ids BIGINT[])
|
||||
|
||||
### 留待后续阶段
|
||||
|
||||
- 消息类型扩展(图片/语音/文件)
|
||||
- 管理端消息管理功能
|
||||
- 群头像上传 UI 完善
|
||||
|
||||
Reference in New Issue
Block a user