Files
EchoChat/backend/go-service/app/im/controller/im_controller.go
bujinyuan 19979da59e 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
2026-03-04 14:58:23 +08:00

244 lines
6.7 KiB
Go
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.

// Package controller 提供 IM 模块的 HTTP 接口处理
package controller
import (
"strconv"
"github.com/echochat/backend/app/dto"
"github.com/echochat/backend/app/im/service"
"github.com/echochat/backend/pkg/middleware"
"github.com/echochat/backend/pkg/utils"
"github.com/gin-gonic/gin"
)
// IMController 即时通讯控制器REST API
type IMController struct {
imService *service.IMService
}
// NewIMController 创建 IMController 实例
func NewIMController(imService *service.IMService) *IMController {
return &IMController{imService: imService}
}
// GetConversations 获取会话列表
// GET /api/v1/im/conversations
func (ctl *IMController) GetConversations(c *gin.Context) {
ctx := c.Request.Context()
userID, ok := middleware.GetCurrentUserID(c)
if !ok {
utils.ResponseUnauthorized(c, "无法获取当前用户信息")
return
}
result, err := ctl.imService.GetConversationList(ctx, userID)
if err != nil {
ctl.handleError(c, err, "获取会话列表失败")
return
}
utils.ResponseOK(c, result)
}
// GetHistoryMessages 获取历史消息
// GET /api/v1/im/messages?conversation_id=xx&before_id=xx&limit=30
func (ctl *IMController) GetHistoryMessages(c *gin.Context) {
ctx := c.Request.Context()
userID, ok := middleware.GetCurrentUserID(c)
if !ok {
utils.ResponseUnauthorized(c, "无法获取当前用户信息")
return
}
var req dto.HistoryMessageRequest
if err := c.ShouldBindQuery(&req); err != nil {
utils.ResponseBadRequest(c, "请求参数错误: "+err.Error())
return
}
result, err := ctl.imService.GetHistoryMessages(ctx, userID, &req)
if err != nil {
ctl.handleError(c, err, "获取历史消息失败")
return
}
utils.ResponseOK(c, result)
}
// PinConversation 置顶/取消置顶会话
// PUT /api/v1/im/conversations/:id/pin
func (ctl *IMController) PinConversation(c *gin.Context) {
ctx := c.Request.Context()
userID, ok := middleware.GetCurrentUserID(c)
if !ok {
utils.ResponseUnauthorized(c, "无法获取当前用户信息")
return
}
convID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
utils.ResponseBadRequest(c, "会话 ID 格式错误")
return
}
var req dto.PinConversationRequest
if err := c.ShouldBindJSON(&req); err != nil {
utils.ResponseBadRequest(c, "请求参数错误: "+err.Error())
return
}
if err := ctl.imService.PinConversation(ctx, userID, convID, req.IsPinned); err != nil {
ctl.handleError(c, err, "更新置顶状态失败")
return
}
utils.ResponseOK(c, nil)
}
// DeleteConversation 删除会话
// DELETE /api/v1/im/conversations/:id
func (ctl *IMController) DeleteConversation(c *gin.Context) {
ctx := c.Request.Context()
userID, ok := middleware.GetCurrentUserID(c)
if !ok {
utils.ResponseUnauthorized(c, "无法获取当前用户信息")
return
}
convID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
utils.ResponseBadRequest(c, "会话 ID 格式错误")
return
}
if err := ctl.imService.DeleteConversation(ctx, userID, convID); err != nil {
ctl.handleError(c, err, "删除会话失败")
return
}
utils.ResponseOK(c, nil)
}
// ClearHistory 清空聊天记录
// DELETE /api/v1/im/conversations/:id/messages
func (ctl *IMController) ClearHistory(c *gin.Context) {
ctx := c.Request.Context()
userID, ok := middleware.GetCurrentUserID(c)
if !ok {
utils.ResponseUnauthorized(c, "无法获取当前用户信息")
return
}
convID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
utils.ResponseBadRequest(c, "会话 ID 格式错误")
return
}
if err := ctl.imService.ClearHistory(ctx, userID, convID); err != nil {
ctl.handleError(c, err, "清空聊天记录失败")
return
}
utils.ResponseOK(c, nil)
}
// SearchMessages 全局消息搜索
// GET /api/v1/im/messages/search?keyword=xx&limit=50
func (ctl *IMController) SearchMessages(c *gin.Context) {
ctx := c.Request.Context()
userID, ok := middleware.GetCurrentUserID(c)
if !ok {
utils.ResponseUnauthorized(c, "无法获取当前用户信息")
return
}
var req dto.SearchMessageRequest
if err := c.ShouldBindQuery(&req); err != nil {
utils.ResponseBadRequest(c, "请求参数错误: "+err.Error())
return
}
result, err := ctl.imService.SearchMessages(ctx, userID, &req)
if err != nil {
ctl.handleError(c, err, "消息搜索失败")
return
}
utils.ResponseOK(c, result)
}
// GetTotalUnread 获取全局未读消息总数
// GET /api/v1/im/unread
func (ctl *IMController) GetTotalUnread(c *gin.Context) {
ctx := c.Request.Context()
userID, ok := middleware.GetCurrentUserID(c)
if !ok {
utils.ResponseUnauthorized(c, "无法获取当前用户信息")
return
}
count, err := ctl.imService.GetTotalUnread(ctx, userID)
if err != nil {
ctl.handleError(c, err, "获取未读消息数失败")
return
}
utils.ResponseOK(c, gin.H{"total_unread": count})
}
// GetMessageReadDetail 获取消息已读详情
// GET /api/v1/im/messages/:id/reads
func (ctl *IMController) GetMessageReadDetail(c *gin.Context) {
ctx := c.Request.Context()
userID, ok := middleware.GetCurrentUserID(c)
if !ok {
utils.ResponseUnauthorized(c, "无法获取当前用户信息")
return
}
messageID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
utils.ResponseBadRequest(c, "消息 ID 格式错误")
return
}
result, err := ctl.imService.GetMessageReadDetail(ctx, userID, messageID)
if err != nil {
ctl.handleError(c, err, "获取已读详情失败")
return
}
utils.ResponseOK(c, result)
}
// handleError 统一业务错误映射
// 已知业务错误 → 返回 Service 层定义的具体提示
// 未知错误 → 返回 fallbackMsg未传则默认"服务器内部错误"
func (ctl *IMController) handleError(c *gin.Context, err error, fallbackMsg ...string) {
switch err {
case service.ErrNotFriend:
utils.ResponseForbidden(c, err.Error())
case service.ErrEmptyContent:
utils.ResponseBadRequest(c, err.Error())
case service.ErrInvalidMsgType:
utils.ResponseBadRequest(c, err.Error())
case service.ErrConvNotFound:
utils.ResponseNotFound(c, err.Error())
case service.ErrMsgNotFound:
utils.ResponseNotFound(c, err.Error())
case service.ErrNotSender:
utils.ResponseForbidden(c, err.Error())
case service.ErrRecallTimeout:
utils.ResponseBadRequest(c, err.Error())
case service.ErrNotMember:
utils.ResponseForbidden(c, err.Error())
case service.ErrDuplicateMsg:
utils.ResponseBadRequest(c, err.Error())
case service.ErrGroupDissolved:
utils.ResponseBadRequest(c, err.Error())
case service.ErrGroupAllMuted:
utils.ResponseForbidden(c, err.Error())
case service.ErrUserMuted:
utils.ResponseForbidden(c, err.Error())
default:
msg := "服务器内部错误"
if len(fallbackMsg) > 0 && fallbackMsg[0] != "" {
msg = fallbackMsg[0]
}
utils.ResponseError(c, msg)
}
}