Files
backend/internal/handler/message_handler.go
lan 3fc8b38184
All checks were successful
Build Backend / build (push) Successful in 2m2s
Build Backend / build-docker (push) Successful in 1m48s
refactor(dto): change seq field type from string to int64
Update the `Seq` field in `WSEventResponse` and its usages across message and websocket handlers to use `int64` instead of `string`. This aligns the data transfer object with the underlying message sequence type and removes unnecessary string conversions.
2026-05-25 02:31:13 +08:00

1130 lines
31 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 handler
import (
"context"
"strconv"
"github.com/gin-gonic/gin"
"with_you/internal/dto"
"with_you/internal/model"
"with_you/internal/pkg/cursor"
"with_you/internal/pkg/response"
"with_you/internal/pkg/ws"
"with_you/internal/service"
)
// enrichConversations 批量填充会话列表响应数据(解决 N+1 问题)
func (h *MessageHandler) enrichConversations(ctx context.Context, convs []*model.Conversation, userID string) []*dto.ConversationResponse {
if len(convs) == 0 {
return nil
}
convIDs := make([]string, len(convs))
for i, c := range convs {
convIDs[i] = c.ID
}
// 批量查询4-5 次查询替代 N*5 次
unreadCounts, _ := h.chatService.GetUnreadCountBatch(ctx, userID, convIDs)
lastMessages, _ := h.chatService.GetLastMessagesBatch(ctx, convIDs)
myParticipants, _ := h.messageService.GetMyParticipantsBatch(ctx, convIDs, userID)
allParticipants, _ := h.messageService.GetParticipantsBatch(ctx, convIDs)
// 收集需要额外查询的 ID
var groupIDs []string
var userIDs []string
for _, conv := range convs {
if conv.Type == model.ConversationTypeGroup && conv.GroupID != nil && *conv.GroupID != "" {
groupIDs = append(groupIDs, *conv.GroupID)
}
}
for _, participants := range allParticipants {
for _, p := range participants {
if p.UserID != userID {
userIDs = append(userIDs, p.UserID)
}
}
}
memberCounts, _ := h.groupService.GetMemberCountBatch(ctx, groupIDs)
usersMap, _ := h.userService.GetUsersByIDs(ctx, userIDs)
// 组装响应
result := make([]*dto.ConversationResponse, len(convs))
for i, conv := range convs {
unreadCount := unreadCounts[conv.ID]
lastMessage := lastMessages[conv.ID]
myP := myParticipants[conv.ID]
isPinned := myP != nil && myP.IsPinned
notificationMuted := myP != nil && myP.NotificationMuted
var resp *dto.ConversationResponse
if conv.Type == model.ConversationTypeGroup && conv.GroupID != nil && *conv.GroupID != "" {
resp = dto.ConvertConversationToResponse(conv, nil, int(unreadCount), lastMessage, isPinned, notificationMuted)
if mc, ok := memberCounts[*conv.GroupID]; ok {
resp.MemberCount = int(mc)
}
} else {
participants := allParticipants[conv.ID]
var users []*model.User
for _, p := range participants {
if p.UserID == userID {
continue
}
if u, ok := usersMap[p.UserID]; ok {
users = append(users, u)
}
}
resp = dto.ConvertConversationToResponse(conv, users, int(unreadCount), lastMessage, isPinned, notificationMuted)
}
result[i] = resp
}
return result
}
// MessageHandler 消息处理器
type MessageHandler struct {
chatService service.ChatService
messageService *service.MessageService
userService service.UserService
groupService service.GroupService
wsPublisher ws.MessagePublisher
}
// NewMessageHandler 创建消息处理器
func NewMessageHandler(chatService service.ChatService, messageService *service.MessageService, userService service.UserService, groupService service.GroupService, wsPublisher ws.MessagePublisher) *MessageHandler {
return &MessageHandler{
chatService: chatService,
messageService: messageService,
userService: userService,
groupService: groupService,
wsPublisher: wsPublisher,
}
}
// HandleTyping 输入状态上报
// POST /api/v1/conversations/typing
func (h *MessageHandler) HandleTyping(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
conversationID := getIDParam(c, "id")
if conversationID == "" {
response.BadRequest(c, "conversation id is required")
return
}
h.chatService.SendTyping(c.Request.Context(), userID, conversationID)
response.SuccessWithMessage(c, "typing sent", nil)
}
// GetConversations 获取会话列表
// GET /api/conversations
func (h *MessageHandler) GetConversations(c *gin.Context) {
userID := c.GetString("user_id")
// 添加调试日志
if userID == "" {
response.Unauthorized(c, "")
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
convs, _, err := h.chatService.GetConversationList(c.Request.Context(), userID, page, pageSize)
if err != nil {
response.InternalServerError(c, "failed to get conversations")
return
}
// 过滤掉系统会话(系统通知现在使用独立的表)
filteredConvs := make([]*model.Conversation, 0)
for _, conv := range convs {
if conv.ID != model.SystemConversationID {
filteredConvs = append(filteredConvs, conv)
}
}
// 批量填充会话数据(解决 N+1 问题)
result := h.enrichConversations(c.Request.Context(), filteredConvs, userID)
// 更新 total 为过滤后的数量
response.Paginated(c, result, int64(len(filteredConvs)), page, pageSize)
}
// CreateConversation 创建私聊会话
// POST /api/conversations
func (h *MessageHandler) CreateConversation(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
var req dto.CreateConversationRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
// 验证目标用户是否存在
targetUser, err := h.userService.GetUserByID(c.Request.Context(), req.UserID)
if err != nil {
response.BadRequest(c, "target user not found")
return
}
// 不能和自己创建会话
if userID == req.UserID {
response.BadRequest(c, "cannot create conversation with yourself")
return
}
conv, err := h.chatService.GetOrCreateConversation(c.Request.Context(), userID, req.UserID)
if err != nil {
response.InternalServerError(c, "failed to create conversation")
return
}
// 获取参与者信息
participants := []*model.User{targetUser}
myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID)
isPinned := myParticipant != nil && myParticipant.IsPinned
notificationMuted := myParticipant != nil && myParticipant.NotificationMuted
response.Success(c, dto.ConvertConversationToResponse(conv, participants, 0, nil, isPinned, notificationMuted))
}
// GetConversationByID 获取会话详情
// GET /api/conversations/:id
func (h *MessageHandler) GetConversationByID(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
conversationIDStr := c.Param("id")
conversationID, err := service.ParseConversationID(conversationIDStr)
if err != nil {
response.BadRequest(c, "invalid conversation id")
return
}
conv, err := h.chatService.GetConversationByID(c.Request.Context(), conversationID, userID)
if err != nil {
response.BadRequest(c, err.Error())
return
}
// 获取未读数
unreadCount, _ := h.chatService.GetUnreadCount(c.Request.Context(), conversationID, userID)
// 获取参与者信息
participants, _ := h.getConversationParticipants(c.Request.Context(), conversationID, userID)
// 获取当前用户的已读位置
myLastReadSeq := int64(0)
isPinned := false
notificationMuted := false
allParticipants, _ := h.messageService.GetConversationParticipants(conversationID)
for _, p := range allParticipants {
if p.UserID == userID {
myLastReadSeq = p.LastReadSeq
isPinned = p.IsPinned
notificationMuted = p.NotificationMuted
break
}
}
// 获取对方用户的已读位置
otherLastReadSeq := int64(0)
for _, p := range allParticipants {
if p.UserID != userID {
otherLastReadSeq = p.LastReadSeq
break
}
}
response.Success(c, dto.ConvertConversationToDetailResponse(conv, participants, unreadCount, nil, myLastReadSeq, otherLastReadSeq, isPinned, notificationMuted))
}
// GetMessages 获取消息列表
// GET /api/conversations/:id/messages
func (h *MessageHandler) GetMessages(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
conversationIDStr := c.Param("id")
conversationID, err := service.ParseConversationID(conversationIDStr)
if err != nil {
response.BadRequest(c, "invalid conversation id")
return
}
// 检查是否使用增量同步after_seq参数
afterSeqStr := c.Query("after_seq")
if afterSeqStr != "" {
// 增量同步模式
afterSeq, err := strconv.ParseInt(afterSeqStr, 10, 64)
if err != nil {
response.BadRequest(c, "invalid after_seq")
return
}
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
messages, err := h.chatService.GetMessagesAfterSeq(c.Request.Context(), conversationID, userID, afterSeq, limit)
if err != nil {
response.BadRequest(c, err.Error())
return
}
// 转换为响应格式
result := dto.ConvertMessagesToResponse(messages)
response.Success(c, &dto.MessageSyncResponse{
Messages: result,
HasMore: len(messages) == limit,
})
return
}
// 检查是否使用历史消息加载before_seq参数
beforeSeqStr := c.Query("before_seq")
if beforeSeqStr != "" {
// 加载更早的消息(下拉加载更多)
beforeSeq, err := strconv.ParseInt(beforeSeqStr, 10, 64)
if err != nil {
response.BadRequest(c, "invalid before_seq")
return
}
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
messages, err := h.chatService.GetMessagesBeforeSeq(c.Request.Context(), conversationID, userID, beforeSeq, limit)
if err != nil {
response.BadRequest(c, err.Error())
return
}
// 转换为响应格式
result := dto.ConvertMessagesToResponse(messages)
response.Success(c, &dto.MessageSyncResponse{
Messages: result,
HasMore: len(messages) == limit,
})
return
}
// 分页模式
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
messages, total, err := h.chatService.GetMessages(c.Request.Context(), conversationID, userID, page, pageSize)
if err != nil {
response.BadRequest(c, err.Error())
return
}
// 转换为响应格式
result := dto.ConvertMessagesToResponse(messages)
response.Paginated(c, result, total, page, pageSize)
}
// SendMessage 发送消息
// POST /api/conversations/:id/messages
func (h *MessageHandler) SendMessage(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
conversationIDStr := c.Param("id")
conversationID, err := service.ParseConversationID(conversationIDStr)
if err != nil {
response.BadRequest(c, "invalid conversation id")
return
}
var req dto.SendMessageRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
msg, err := h.chatService.SendMessage(c.Request.Context(), userID, conversationID, req.Segments, req.ReplyToID, req.ClientMsgID)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.Success(c, dto.ConvertMessageToResponse(msg))
}
// HandleSendMessage RESTful 风格的发送消息端点
// POST /api/v1/conversations/:id/messages
// 请求体格式: {"detail_type": "private", "segments": [{"type": "text", "data": {"text": "嗨~"}}]}
func (h *MessageHandler) HandleSendMessage(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
conversationID := getIDParam(c, "id")
if conversationID == "" {
response.BadRequest(c, "conversation id is required")
return
}
var params dto.SendMessageParams
if err := c.ShouldBindJSON(&params); err != nil {
response.BadRequest(c, err.Error())
return
}
// 验证参数
if params.DetailType == "" {
response.BadRequest(c, "detail_type is required")
return
}
if len(params.Segments) == 0 {
response.BadRequest(c, "segments is required")
return
}
// 发送消息
msg, err := h.chatService.SendMessage(c.Request.Context(), userID, conversationID, params.Segments, params.ReplyToID, params.ClientMsgID)
if err != nil {
response.BadRequest(c, err.Error())
return
}
// 构建 WSEventResponse 格式响应
wsResponse := dto.WSEventResponse{
ID: msg.ID,
Time: msg.CreatedAt.UnixMilli(),
Type: "message",
DetailType: params.DetailType,
ConversationID: conversationID,
Seq: msg.Seq,
Segments: params.Segments,
SenderID: userID,
}
response.Success(c, wsResponse)
}
// HandleDeleteMsg 撤回消息
// POST /api/v1/messages/delete_msg
// 请求体格式: {"message_id": "xxx"}
func (h *MessageHandler) HandleDeleteMsg(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
var params dto.DeleteMsgParams
if err := c.ShouldBindJSON(&params); err != nil {
response.BadRequest(c, err.Error())
return
}
// 验证参数
if params.MessageID == "" {
response.BadRequest(c, "message_id is required")
return
}
// 撤回消息
err := h.chatService.RecallMessage(c.Request.Context(), params.MessageID, userID)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.SuccessWithMessage(c, "消息已撤回", nil)
}
// HandleGetConversationList 获取会话列表
// GET /api/v1/conversations
func (h *MessageHandler) HandleGetConversationList(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
convs, _, err := h.chatService.GetConversationList(c.Request.Context(), userID, page, pageSize)
if err != nil {
response.InternalServerError(c, "failed to get conversations")
return
}
// 过滤掉系统会话(系统通知现在使用独立的表)
filteredConvs := make([]*model.Conversation, 0)
for _, conv := range convs {
if conv.ID != model.SystemConversationID {
filteredConvs = append(filteredConvs, conv)
}
}
// 批量填充会话数据(解决 N+1 问题)
result := h.enrichConversations(c.Request.Context(), filteredConvs, userID)
response.Paginated(c, result, int64(len(filteredConvs)), page, pageSize)
}
// HandleDeleteConversationForSelf 仅自己删除会话
// DELETE /api/v1/conversations/:id/self
func (h *MessageHandler) HandleDeleteConversationForSelf(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
conversationID := getIDParam(c, "id")
if conversationID == "" {
response.BadRequest(c, "conversation id is required")
return
}
if err := h.chatService.DeleteConversationForSelf(c.Request.Context(), conversationID, userID); err != nil {
response.BadRequest(c, err.Error())
return
}
response.SuccessWithMessage(c, "conversation deleted for self", nil)
}
// MarkAsRead 标记为已读
// POST /api/conversations/:id/read
func (h *MessageHandler) MarkAsRead(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
conversationIDStr := c.Param("id")
conversationID, err := service.ParseConversationID(conversationIDStr)
if err != nil {
response.BadRequest(c, "invalid conversation id")
return
}
var req dto.MarkReadRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "last_read_seq is required")
return
}
err = h.chatService.MarkAsRead(c.Request.Context(), conversationID, userID, req.LastReadSeq)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.SuccessWithMessage(c, "marked as read", nil)
}
// GetUnreadCount 获取未读消息总数
// GET /api/conversations/unread/count
func (h *MessageHandler) GetUnreadCount(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
count, err := h.chatService.GetAllUnreadCount(c.Request.Context(), userID)
if err != nil {
response.InternalServerError(c, "failed to get unread count")
return
}
response.Success(c, &dto.UnreadCountResponse{
TotalUnreadCount: count,
})
}
// HandleGetSyncData 获取同步元数据(轻量级 seq + 时间)
// GET /api/v1/conversations/sync-data
func (h *MessageHandler) HandleGetSyncData(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
items, err := h.chatService.GetSyncData(c.Request.Context(), userID)
if err != nil {
response.InternalServerError(c, "failed to get sync data")
return
}
response.Success(c, &dto.SyncDataResponse{
Conversations: items,
})
}
// HandleGetSyncByVersion 增量同步:按版本号获取会话变更
// GET /api/v1/conversations/sync?version=0&limit=100
func (h *MessageHandler) HandleGetSyncByVersion(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
versionStr := c.DefaultQuery("version", "0")
sinceVersion, err := strconv.ParseInt(versionStr, 10, 64)
if err != nil || sinceVersion < 0 {
response.BadRequest(c, "invalid version parameter")
return
}
limitStr := c.DefaultQuery("limit", "100")
limit, err := strconv.Atoi(limitStr)
if err != nil || limit < 1 {
limit = 100
}
result, err := h.chatService.GetSyncByVersion(c.Request.Context(), userID, sinceVersion, limit)
if err != nil {
response.InternalServerError(c, err.Error())
return
}
response.Success(c, result)
}
// GetConversationUnreadCount 获取单个会话的未读数
// GET /api/conversations/:id/unread/count
func (h *MessageHandler) GetConversationUnreadCount(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
conversationIDStr := c.Param("id")
conversationID, err := service.ParseConversationID(conversationIDStr)
if err != nil {
response.BadRequest(c, "invalid conversation id")
return
}
count, err := h.chatService.GetUnreadCount(c.Request.Context(), conversationID, userID)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.Success(c, &dto.ConversationUnreadCountResponse{
ConversationID: conversationID,
UnreadCount: count,
})
}
// RecallMessage 撤回消息
// POST /api/messages/:id/recall
func (h *MessageHandler) RecallMessage(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
messageIDStr := c.Param("id")
// 直接使用 string 类型的 messageID
err := h.chatService.RecallMessage(c.Request.Context(), messageIDStr, userID)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.SuccessWithMessage(c, "message recalled", nil)
}
// DeleteMessage 删除消息
// DELETE /api/messages/:id
func (h *MessageHandler) DeleteMessage(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
messageIDStr := c.Param("id")
// 直接使用 string 类型的 messageID
err := h.chatService.DeleteMessage(c.Request.Context(), messageIDStr, userID)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.SuccessWithMessage(c, "message deleted", nil)
}
// 辅助函数:获取会话参与者信息
func (h *MessageHandler) getConversationParticipants(ctx context.Context, conversationID string, currentUserID string) ([]*model.User, error) {
// 从repository获取参与者列表
participants, err := h.messageService.GetConversationParticipants(conversationID)
if err != nil {
return nil, err
}
// 获取参与者用户信息
var users []*model.User
for _, p := range participants {
// 跳过当前用户
if p.UserID == currentUserID {
continue
}
user, err := h.userService.GetUserByID(ctx, p.UserID)
if err != nil {
continue
}
users = append(users, user)
}
return users, nil
}
// 获取当前用户在会话中的参与者信息
func (h *MessageHandler) getMyConversationParticipant(conversationID string, userID string) (*model.ConversationParticipant, error) {
participants, err := h.messageService.GetConversationParticipants(conversationID)
if err != nil {
return nil, err
}
for _, p := range participants {
if p.UserID == userID {
return p, nil
}
}
return nil, nil
}
// getIDParam 从路径参数获取 ID
func getIDParam(c *gin.Context, paramName string) string {
return c.Param(paramName)
}
// ==================== RESTful Action 端点 ====================
// HandleCreateConversation 创建会话
// POST /api/v1/conversations/create
func (h *MessageHandler) HandleCreateConversation(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
var params dto.CreateConversationParams
if err := c.ShouldBindJSON(&params); err != nil {
response.BadRequest(c, err.Error())
return
}
// 验证目标用户是否存在
targetUser, err := h.userService.GetUserByID(c.Request.Context(), params.UserID)
if err != nil {
response.BadRequest(c, "target user not found")
return
}
// 不能和自己创建会话
if userID == params.UserID {
response.BadRequest(c, "cannot create conversation with yourself")
return
}
conv, err := h.chatService.GetOrCreateConversation(c.Request.Context(), userID, params.UserID)
if err != nil {
response.InternalServerError(c, "failed to create conversation")
return
}
// 获取参与者信息
participants := []*model.User{targetUser}
myParticipant, _ := h.getMyConversationParticipant(conv.ID, userID)
isPinned := myParticipant != nil && myParticipant.IsPinned
notificationMuted := myParticipant != nil && myParticipant.NotificationMuted
response.Success(c, dto.ConvertConversationToResponse(conv, participants, 0, nil, isPinned, notificationMuted))
}
// HandleGetConversation 获取会话详情
// GET /api/v1/conversations/:id
func (h *MessageHandler) HandleGetConversation(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
conversationID := getIDParam(c, "id")
if conversationID == "" {
response.BadRequest(c, "conversation_id is required")
return
}
conv, err := h.chatService.GetConversationByID(c.Request.Context(), conversationID, userID)
if err != nil {
response.BadRequest(c, err.Error())
return
}
// 获取未读数
unreadCount, _ := h.chatService.GetUnreadCount(c.Request.Context(), conversationID, userID)
// 获取参与者信息
participants, _ := h.getConversationParticipants(c.Request.Context(), conversationID, userID)
// 获取当前用户的已读位置(优先从 Redis 缓存读取)
myLastReadSeq := int64(0)
isPinned := false
notificationMuted := false
allParticipants, _ := h.messageService.GetConversationParticipants(conversationID)
for _, p := range allParticipants {
if p.UserID == userID {
myLastReadSeq = p.LastReadSeq
isPinned = p.IsPinned
notificationMuted = p.NotificationMuted
break
}
}
// 获取对方用户的已读位置(优先从 Redis 缓存读取)
otherLastReadSeq := int64(0)
for _, p := range allParticipants {
if p.UserID != userID {
otherLastReadSeq = p.LastReadSeq
break
}
}
response.Success(c, dto.ConvertConversationToDetailResponse(conv, participants, unreadCount, nil, myLastReadSeq, otherLastReadSeq, isPinned, notificationMuted))
}
// HandleGetMessages 获取会话消息
// GET /api/v1/conversations/:id/messages
func (h *MessageHandler) HandleGetMessages(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
conversationID := getIDParam(c, "id")
if conversationID == "" {
response.BadRequest(c, "conversation_id is required")
return
}
// 检查是否使用增量同步after_seq参数
afterSeqStr := c.Query("after_seq")
if afterSeqStr != "" {
// 增量同步模式
afterSeq, err := strconv.ParseInt(afterSeqStr, 10, 64)
if err != nil {
response.BadRequest(c, "invalid after_seq")
return
}
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "100"))
messages, err := h.chatService.GetMessagesAfterSeq(c.Request.Context(), conversationID, userID, afterSeq, limit)
if err != nil {
response.BadRequest(c, err.Error())
return
}
// 转换为响应格式
result := dto.ConvertMessagesToResponse(messages)
response.Success(c, &dto.MessageSyncResponse{
Messages: result,
HasMore: len(messages) == limit,
})
return
}
// 检查是否使用历史消息加载before_seq参数
beforeSeqStr := c.Query("before_seq")
if beforeSeqStr != "" {
// 加载更早的消息(下拉加载更多)
beforeSeq, err := strconv.ParseInt(beforeSeqStr, 10, 64)
if err != nil {
response.BadRequest(c, "invalid before_seq")
return
}
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
messages, err := h.chatService.GetMessagesBeforeSeq(c.Request.Context(), conversationID, userID, beforeSeq, limit)
if err != nil {
response.BadRequest(c, err.Error())
return
}
// 转换为响应格式
result := dto.ConvertMessagesToResponse(messages)
response.Success(c, &dto.MessageSyncResponse{
Messages: result,
HasMore: len(messages) == limit,
})
return
}
// 分页模式
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
messages, total, err := h.chatService.GetMessages(c.Request.Context(), conversationID, userID, page, pageSize)
if err != nil {
response.BadRequest(c, err.Error())
return
}
// 转换为响应格式
result := dto.ConvertMessagesToResponse(messages)
response.Paginated(c, result, total, page, pageSize)
}
// HandleMarkRead 标记已读
// POST /api/v1/conversations/:id/read
func (h *MessageHandler) HandleMarkRead(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
conversationID := getIDParam(c, "id")
if conversationID == "" {
response.BadRequest(c, "conversation id is required")
return
}
var req dto.MarkReadRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
err := h.chatService.MarkAsRead(c.Request.Context(), conversationID, userID, req.LastReadSeq)
if err != nil {
response.BadRequest(c, err.Error())
return
}
response.SuccessWithMessage(c, "marked as read", nil)
}
// HandleMarkReadAll 批量标记所有会话已读
// POST /api/v1/conversations/read-all
func (h *MessageHandler) HandleMarkReadAll(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
var req dto.BatchMarkReadRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
successCount, err := h.chatService.MarkAsReadBatch(c.Request.Context(), userID, req.Conversations)
if err != nil {
response.InternalServerError(c, "failed to mark all as read")
return
}
response.Success(c, &dto.BatchMarkReadResponse{
SuccessCount: successCount,
TotalCount: len(req.Conversations),
})
}
// HandleSetConversationPinned 设置会话置顶
// PUT /api/v1/conversations/:id/pinned
func (h *MessageHandler) HandleSetConversationPinned(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
conversationID := getIDParam(c, "id")
if conversationID == "" {
response.BadRequest(c, "conversation id is required")
return
}
var req struct {
IsPinned bool `json:"is_pinned"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
if err := h.chatService.SetConversationPinned(c.Request.Context(), conversationID, userID, req.IsPinned); err != nil {
response.BadRequest(c, err.Error())
return
}
response.SuccessWithMessage(c, "conversation pinned status updated", gin.H{
"conversation_id": conversationID,
"is_pinned": req.IsPinned,
})
}
// HandleSetConversationNotificationMuted 设置会话免打扰状态
// PUT /api/v1/conversations/:id/notification_muted
func (h *MessageHandler) HandleSetConversationNotificationMuted(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
conversationID := getIDParam(c, "id")
if conversationID == "" {
response.BadRequest(c, "conversation id is required")
return
}
var req struct {
NotificationMuted bool `json:"notification_muted"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
if err := h.chatService.SetConversationNotificationMuted(c.Request.Context(), conversationID, userID, req.NotificationMuted); err != nil {
response.BadRequest(c, err.Error())
return
}
response.SuccessWithMessage(c, "conversation notification_muted status updated", gin.H{
"conversation_id": conversationID,
"notification_muted": req.NotificationMuted,
})
}
// ==================== Cursor Pagination Handlers ====================
// GetMessagesByCursor 游标分页获取会话消息
// GET /api/v1/conversations/:id/messages/cursor
// 请求参数:
// - cursor: 游标字符串(可选)
// - direction: 分页方向 forward 或 backward默认 forward
// - page_size: 每页数量(默认 20最大 100
func (h *MessageHandler) GetMessagesByCursor(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
conversationID := getIDParam(c, "id")
if conversationID == "" {
response.BadRequest(c, "conversation_id is required")
return
}
// 解析游标分页参数
pageReq := parseCursorPageRequest(c)
// 调用服务层
result, err := h.messageService.GetMessagesByCursor(c.Request.Context(), conversationID, pageReq)
if err != nil {
response.InternalServerError(c, "failed to get messages")
return
}
// 转换为响应格式
items := dto.ConvertMessagesToResponse(result.Items)
response.Success(c, &dto.MessageCursorPageResponse{
Items: items,
NextCursor: result.NextCursor,
PrevCursor: result.PrevCursor,
HasMore: result.HasMore,
})
}
// GetConversationsByCursor 游标分页获取会话列表
// GET /api/v1/conversations/cursor
// 请求参数:
// - cursor: 游标字符串(可选)
// - direction: 分页方向 forward 或 backward默认 forward
// - page_size: 每页数量(默认 20最大 100
func (h *MessageHandler) GetConversationsByCursor(c *gin.Context) {
userID := c.GetString("user_id")
if userID == "" {
response.Unauthorized(c, "")
return
}
// 解析游标分页参数
pageReq := parseCursorPageRequest(c)
// 调用服务层
result, err := h.messageService.GetConversationsByCursor(c.Request.Context(), userID, pageReq)
if err != nil {
response.InternalServerError(c, "failed to get conversations")
return
}
// 过滤掉系统会话
filteredItems := make([]*model.Conversation, 0)
for _, conv := range result.Items {
if conv.ID != model.SystemConversationID {
filteredItems = append(filteredItems, conv)
}
}
// 批量填充会话数据(解决 N+1 问题)
items := h.enrichConversations(c.Request.Context(), filteredItems, userID)
response.Success(c, &dto.ConversationCursorPageResponse{
Items: items,
NextCursor: result.NextCursor,
PrevCursor: result.PrevCursor,
HasMore: result.HasMore,
})
}
// parseCursorPageRequest 解析游标分页请求参数
func parseCursorPageRequest(c *gin.Context) *cursor.PageRequest {
cursorStr := c.Query("cursor")
direction := cursor.Direction(c.DefaultQuery("direction", "forward"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
return cursor.NewPageRequest(cursorStr, direction, pageSize)
}