package handler import ( "strconv" "strings" "github.com/gin-gonic/gin" "nl-game-api-gin/internal/database" "nl-game-api-gin/internal/middleware" "nl-game-api-gin/internal/model" "nl-game-api-gin/internal/room" "nl-game-api-gin/pkg/resp" ) // ChatConversations 会话列表:每个聊过天的好友一条,带最后一条消息与未读数 func ChatConversations(c *gin.Context) { uid := middleware.UID(c) // 每个对端的最后一条消息ID type peerRow struct { Peer int `gorm:"column:peer"` LastID int `gorm:"column:last_id"` } var peers []peerRow database.DB.Raw(`SELECT IF(from_id = ?, to_id, from_id) AS peer, MAX(id) AS last_id FROM chat_messages WHERE from_id = ? OR to_id = ? GROUP BY peer ORDER BY last_id DESC LIMIT 100`, uid, uid, uid).Scan(&peers) if len(peers) == 0 { resp.OK(c, gin.H{"list": []any{}, "total_unread": 0}) return } lastIDs := make([]int, 0, len(peers)) peerIDs := make([]int, 0, len(peers)) for _, p := range peers { lastIDs = append(lastIDs, p.LastID) peerIDs = append(peerIDs, p.Peer) } // 最后一条消息内容 var lastMsgs []model.ChatMessage database.DB.Where("id IN ?", lastIDs).Find(&lastMsgs) msgMap := map[int]model.ChatMessage{} for _, m := range lastMsgs { msgMap[m.ID] = m } // 各对端发给我的未读数 type unreadRow struct { FromID int `gorm:"column:from_id"` Cnt int `gorm:"column:cnt"` } var unreads []unreadRow database.DB.Raw(`SELECT from_id, COUNT(*) AS cnt FROM chat_messages WHERE to_id = ? AND is_read = 0 GROUP BY from_id`, uid).Scan(&unreads) unreadMap := map[int]int{} total := 0 for _, u := range unreads { unreadMap[u.FromID] = u.Cnt total += u.Cnt } briefs := loadBriefs(peerIDs) type convItem struct { Peer userBrief `json:"peer"` // 对方信息 LastMsg string `json:"last_msg"` // 最后一条消息内容 LastAt int64 `json:"last_at"` // 最后一条消息时间 FromMe bool `json:"from_me"` // 最后一条是否我发的 Unread int `json:"unread"` // 未读数 } list := make([]convItem, 0, len(peers)) for _, p := range peers { b, ok := briefs[p.Peer] if !ok { continue } m := msgMap[p.LastID] list = append(list, convItem{ Peer: b, LastMsg: m.Content, LastAt: m.CreatedAt, FromMe: m.FromID == uid, Unread: unreadMap[p.Peer], }) } resp.OK(c, gin.H{"list": list, "total_unread": total}) } // ChatMessages 与某人的聊天记录(倒序分页,返回升序;拉取即把对方发来的标记已读) func ChatMessages(c *gin.Context) { uid := middleware.UID(c) peer, _ := strconv.Atoi(c.Query("peer_id")) if peer <= 0 { resp.Fail(c, "参数有误") return } beforeID, _ := strconv.Atoi(c.Query("before_id")) size, _ := strconv.Atoi(c.Query("size")) if size <= 0 || size > 100 { size = 50 } q := database.DB.Where("(from_id = ? AND to_id = ?) OR (from_id = ? AND to_id = ?)", uid, peer, peer, uid) if beforeID > 0 { q = q.Where("id < ?", beforeID) } var msgs []model.ChatMessage q.Order("id DESC").Limit(size + 1).Find(&msgs) hasMore := len(msgs) > size if hasMore { msgs = msgs[:size] } // 反转为时间升序(前端从上到下渲染) for i, j := 0, len(msgs)-1; i < j; i, j = i+1, j-1 { msgs[i], msgs[j] = msgs[j], msgs[i] } // 拉取历史即视为已读 database.DB.Model(&model.ChatMessage{}). Where("from_id = ? AND to_id = ? AND is_read = 0", peer, uid). Update("is_read", 1) briefs := loadBriefs([]int{peer}) resp.OK(c, gin.H{"list": msgs, "has_more": hasMore, "peer": briefs[peer]}) } // chatSendBody 发送私聊请求体 type chatSendBody struct { ToID int `json:"to_id" binding:"required"` // 接收方用户ID Content string `json:"content" binding:"required"` // 消息内容 } // ChatSend 发送私聊:仅好友之间可发;对方在线时通过 WebSocket 实时推送 func ChatSend(c *gin.Context) { uid := middleware.UID(c) var req chatSendBody if err := c.ShouldBindJSON(&req); err != nil || req.ToID == uid { resp.Fail(c, "参数有误") return } content := strings.TrimSpace(req.Content) if content == "" { resp.Fail(c, "消息内容不能为空") return } if len([]rune(content)) > 500 { resp.Fail(c, "消息太长(最多 500 字)") return } if !isFriend(uid, req.ToID) { resp.Fail(c, "你们还不是好友,先添加对方为好友吧") return } msg := model.ChatMessage{FromID: uid, ToID: req.ToID, Content: content} if err := database.DB.Create(&msg).Error; err != nil { resp.Fail(c, "发送失败,请稍后重试") return } // 对方在线:实时推送消息(含发送者摘要,便于会话列表直接更新) var me model.User database.DB.First(&me, uid) room.PushToUser(req.ToID, "chat_msg", gin.H{"msg": msg, "from": briefOf(&me, true)}) resp.OK(c, msg) } // chatReadBody 标记已读请求体 type chatReadBody struct { PeerID int `json:"peer_id" binding:"required"` // 对端用户ID } // ChatRead 把某人发给我的消息全部标记已读(聊天窗口打开时收到新消息调用) func ChatRead(c *gin.Context) { uid := middleware.UID(c) var req chatReadBody if err := c.ShouldBindJSON(&req); err != nil { resp.Fail(c, "参数有误") return } database.DB.Model(&model.ChatMessage{}). Where("from_id = ? AND to_id = ? AND is_read = 0", req.PeerID, uid). Update("is_read", 1) resp.OK(c, nil) } // ChatUnread 我的未读私聊总数(导航角标) func ChatUnread(c *gin.Context) { uid := middleware.UID(c) var cnt int64 database.DB.Model(&model.ChatMessage{}).Where("to_id = ? AND is_read = 0", uid).Count(&cnt) resp.OK(c, gin.H{"unread": cnt}) }