Files
nl-game-api/internal/handler/chat.go
2026-08-14 13:17:03 +08:00

182 lines
5.5 KiB
Go

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})
}