Set up project files and add .gitignore to exclude local build/runtime artifacts. Made-with: Cursor
867 lines
24 KiB
Go
867 lines
24 KiB
Go
package handler
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"log"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
|
||
"carrot_bbs/internal/dto"
|
||
"carrot_bbs/internal/model"
|
||
ws "carrot_bbs/internal/pkg/websocket"
|
||
"carrot_bbs/internal/repository"
|
||
"carrot_bbs/internal/service"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/gorilla/websocket"
|
||
)
|
||
|
||
var upgrader = websocket.Upgrader{
|
||
ReadBufferSize: 1024,
|
||
WriteBufferSize: 1024,
|
||
CheckOrigin: func(r *http.Request) bool {
|
||
return true // 允许所有来源,生产环境应该限制
|
||
},
|
||
}
|
||
|
||
// WebSocketHandler WebSocket处理器
|
||
type WebSocketHandler struct {
|
||
jwtService *service.JWTService
|
||
chatService service.ChatService
|
||
groupService service.GroupService
|
||
groupRepo repository.GroupRepository
|
||
userRepo *repository.UserRepository
|
||
wsManager *ws.WebSocketManager
|
||
}
|
||
|
||
// NewWebSocketHandler 创建WebSocket处理器
|
||
func NewWebSocketHandler(
|
||
jwtService *service.JWTService,
|
||
chatService service.ChatService,
|
||
groupService service.GroupService,
|
||
groupRepo repository.GroupRepository,
|
||
userRepo *repository.UserRepository,
|
||
wsManager *ws.WebSocketManager,
|
||
) *WebSocketHandler {
|
||
return &WebSocketHandler{
|
||
jwtService: jwtService,
|
||
chatService: chatService,
|
||
groupService: groupService,
|
||
groupRepo: groupRepo,
|
||
userRepo: userRepo,
|
||
wsManager: wsManager,
|
||
}
|
||
}
|
||
|
||
// HandleWebSocket 处理WebSocket连接
|
||
func (h *WebSocketHandler) HandleWebSocket(c *gin.Context) {
|
||
// 调试:打印请求头信息
|
||
log.Printf("[WebSocket] 收到请求: Method=%s, Path=%s", c.Request.Method, c.Request.URL.Path)
|
||
log.Printf("[WebSocket] 请求头: Connection=%s, Upgrade=%s",
|
||
c.GetHeader("Connection"),
|
||
c.GetHeader("Upgrade"))
|
||
log.Printf("[WebSocket] Sec-WebSocket-Key=%s, Sec-WebSocket-Version=%s",
|
||
c.GetHeader("Sec-WebSocket-Key"),
|
||
c.GetHeader("Sec-WebSocket-Version"))
|
||
|
||
// 从query参数获取token
|
||
token := c.Query("token")
|
||
if token == "" {
|
||
// 尝试从header获取
|
||
authHeader := c.GetHeader("Authorization")
|
||
if strings.HasPrefix(authHeader, "Bearer ") {
|
||
token = strings.TrimPrefix(authHeader, "Bearer ")
|
||
}
|
||
}
|
||
|
||
if token == "" {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing token"})
|
||
return
|
||
}
|
||
|
||
// 验证token
|
||
claims, err := h.jwtService.ParseToken(token)
|
||
if err != nil {
|
||
log.Printf("Invalid token: %v", err)
|
||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token"})
|
||
return
|
||
}
|
||
|
||
userID := claims.UserID
|
||
if userID == "" {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid token claims"})
|
||
return
|
||
}
|
||
|
||
// 升级HTTP连接为WebSocket连接
|
||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||
if err != nil {
|
||
log.Printf("Failed to upgrade connection: %v", err)
|
||
log.Printf("[WebSocket] 请求详情 - User-Agent: %s, Content-Type: %s",
|
||
c.GetHeader("User-Agent"),
|
||
c.GetHeader("Content-Type"))
|
||
return
|
||
}
|
||
|
||
// 如果用户已在线,先注销旧连接
|
||
if h.wsManager.IsUserOnline(userID) {
|
||
log.Printf("[DEBUG] 用户 %s 已有在线连接,复用该连接", userID)
|
||
} else {
|
||
log.Printf("[DEBUG] 用户 %s 当前不在线,创建新连接", userID)
|
||
}
|
||
|
||
// 创建客户端
|
||
client := &ws.Client{
|
||
ID: userID,
|
||
UserID: userID,
|
||
Conn: conn,
|
||
Send: make(chan []byte, 256),
|
||
Manager: h.wsManager,
|
||
}
|
||
|
||
// 注册客户端
|
||
h.wsManager.Register(client)
|
||
|
||
// 启动读写协程
|
||
go client.WritePump()
|
||
go h.handleMessages(client)
|
||
|
||
log.Printf("[DEBUG] WebSocket连接建立: userID=%s, 当前在线=%v", userID, h.wsManager.IsUserOnline(userID))
|
||
}
|
||
|
||
// handleMessages 处理客户端消息
|
||
// 针对移动端优化:增加超时时间到 120 秒,配合客户端 55 秒心跳
|
||
func (h *WebSocketHandler) handleMessages(client *ws.Client) {
|
||
defer func() {
|
||
h.wsManager.Unregister(client)
|
||
client.Conn.Close()
|
||
}()
|
||
|
||
client.Conn.SetReadLimit(512 * 1024) // 512KB
|
||
client.Conn.SetReadDeadline(time.Now().Add(120 * time.Second)) // 增加到 120 秒
|
||
client.Conn.SetPongHandler(func(string) error {
|
||
client.Conn.SetReadDeadline(time.Now().Add(120 * time.Second)) // 增加到 120 秒
|
||
return nil
|
||
})
|
||
|
||
// 心跳定时器 - 服务端主动 ping 间隔保持 30 秒
|
||
pingTicker := time.NewTicker(30 * time.Second)
|
||
defer pingTicker.Stop()
|
||
|
||
for {
|
||
select {
|
||
case <-pingTicker.C:
|
||
// 发送心跳
|
||
if err := client.SendPing(); err != nil {
|
||
log.Printf("Failed to send ping: %v", err)
|
||
return
|
||
}
|
||
default:
|
||
_, message, err := client.Conn.ReadMessage()
|
||
if err != nil {
|
||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||
log.Printf("WebSocket error: %v", err)
|
||
}
|
||
return
|
||
}
|
||
|
||
var wsMsg ws.WSMessage
|
||
if err := json.Unmarshal(message, &wsMsg); err != nil {
|
||
log.Printf("Failed to unmarshal message: %v", err)
|
||
continue
|
||
}
|
||
|
||
h.processMessage(client, &wsMsg)
|
||
}
|
||
}
|
||
}
|
||
|
||
// processMessage 处理消息
|
||
func (h *WebSocketHandler) processMessage(client *ws.Client, msg *ws.WSMessage) {
|
||
switch msg.Type {
|
||
case ws.MessageTypePing:
|
||
// 响应心跳
|
||
if err := client.SendPong(); err != nil {
|
||
log.Printf("Failed to send pong: %v", err)
|
||
}
|
||
|
||
case ws.MessageTypePong:
|
||
// 客户端响应心跳
|
||
|
||
case ws.MessageTypeMessage:
|
||
// 处理聊天消息
|
||
h.handleChatMessage(client, msg)
|
||
|
||
case ws.MessageTypeTyping:
|
||
// 处理正在输入状态
|
||
h.handleTyping(client, msg)
|
||
|
||
case ws.MessageTypeRead:
|
||
// 处理已读回执
|
||
h.handleReadReceipt(client, msg)
|
||
|
||
// 群组消息处理
|
||
case ws.MessageTypeGroupMessage:
|
||
// 处理群消息
|
||
h.handleGroupMessage(client, msg)
|
||
|
||
case ws.MessageTypeGroupTyping:
|
||
// 处理群输入状态
|
||
h.handleGroupTyping(client, msg)
|
||
|
||
case ws.MessageTypeGroupRead:
|
||
// 处理群消息已读
|
||
h.handleGroupReadReceipt(client, msg)
|
||
|
||
case ws.MessageTypeGroupRecall:
|
||
// 处理群消息撤回
|
||
h.handleGroupRecall(client, msg)
|
||
|
||
default:
|
||
log.Printf("Unknown message type: %s", msg.Type)
|
||
}
|
||
}
|
||
|
||
// handleChatMessage 处理聊天消息
|
||
func (h *WebSocketHandler) handleChatMessage(client *ws.Client, msg *ws.WSMessage) {
|
||
data, ok := msg.Data.(map[string]interface{})
|
||
if !ok {
|
||
log.Printf("Invalid message data format")
|
||
return
|
||
}
|
||
|
||
log.Printf("[DEBUG handleChatMessage] 完整data: %+v", data)
|
||
|
||
conversationIDStr, _ := data["conversationId"].(string)
|
||
|
||
if conversationIDStr == "" {
|
||
log.Printf("Missing conversationId")
|
||
return
|
||
}
|
||
|
||
// 解析会话ID
|
||
conversationID, err := service.ParseConversationID(conversationIDStr)
|
||
if err != nil {
|
||
log.Printf("Invalid conversation ID: %v", err)
|
||
return
|
||
}
|
||
|
||
// 解析 segments
|
||
var segments model.MessageSegments
|
||
if data["segments"] != nil {
|
||
segmentsBytes, err := json.Marshal(data["segments"])
|
||
if err == nil {
|
||
json.Unmarshal(segmentsBytes, &segments)
|
||
}
|
||
}
|
||
|
||
// 从 segments 中提取回复消息ID
|
||
replyToID := dto.GetReplyMessageID(segments)
|
||
var replyToIDPtr *string
|
||
if replyToID != "" {
|
||
replyToIDPtr = &replyToID
|
||
}
|
||
|
||
// 发送消息 - 使用 segments
|
||
message, err := h.chatService.SendMessage(context.Background(), client.UserID, conversationID, segments, replyToIDPtr)
|
||
if err != nil {
|
||
log.Printf("Failed to send message: %v", err)
|
||
// 发送错误消息
|
||
errorMsg := ws.CreateWSMessage(ws.MessageTypeError, map[string]string{
|
||
"error": "Failed to send message",
|
||
})
|
||
if client.Send != nil {
|
||
msgBytes, _ := json.Marshal(errorMsg)
|
||
client.Send <- msgBytes
|
||
}
|
||
return
|
||
}
|
||
|
||
// 发送确认消息(使用 meta 事件格式,包含完整的消息内容)
|
||
metaAckMsg := ws.CreateWSMessage("meta", map[string]interface{}{
|
||
"detail_type": ws.MetaDetailTypeAck,
|
||
"conversation_id": conversationID,
|
||
"id": message.ID,
|
||
"user_id": client.UserID,
|
||
"sender_id": client.UserID,
|
||
"seq": message.Seq,
|
||
"segments": message.Segments,
|
||
"created_at": message.CreatedAt.UnixMilli(),
|
||
})
|
||
if client.Send != nil {
|
||
msgBytes, _ := json.Marshal(metaAckMsg)
|
||
log.Printf("[DEBUG handleChatMessage] 私聊 ack 消息: %s", string(msgBytes))
|
||
log.Printf("[DEBUG handleChatMessage] message.Segments 类型: %T, 值: %+v", message.Segments, message.Segments)
|
||
client.Send <- msgBytes
|
||
}
|
||
}
|
||
|
||
// handleTyping 处理正在输入状态
|
||
func (h *WebSocketHandler) handleTyping(client *ws.Client, msg *ws.WSMessage) {
|
||
data, ok := msg.Data.(map[string]interface{})
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
conversationIDStr, _ := data["conversationId"].(string)
|
||
if conversationIDStr == "" {
|
||
return
|
||
}
|
||
|
||
conversationID, err := service.ParseConversationID(conversationIDStr)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
// 直接使用 string 类型的 userID
|
||
h.chatService.SendTyping(context.Background(), client.UserID, conversationID)
|
||
}
|
||
|
||
// handleReadReceipt 处理已读回执
|
||
func (h *WebSocketHandler) handleReadReceipt(client *ws.Client, msg *ws.WSMessage) {
|
||
data, ok := msg.Data.(map[string]interface{})
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
conversationIDStr, _ := data["conversationId"].(string)
|
||
if conversationIDStr == "" {
|
||
return
|
||
}
|
||
|
||
conversationID, err := service.ParseConversationID(conversationIDStr)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
// 获取lastReadSeq
|
||
lastReadSeq, _ := data["lastReadSeq"].(float64)
|
||
if lastReadSeq == 0 {
|
||
return
|
||
}
|
||
|
||
// 直接使用 string 类型的 userID 和 conversationID
|
||
if err := h.chatService.MarkAsRead(context.Background(), conversationID, client.UserID, int64(lastReadSeq)); err != nil {
|
||
log.Printf("Failed to mark as read: %v", err)
|
||
}
|
||
}
|
||
|
||
// ==================== 群组消息处理 ====================
|
||
|
||
// handleGroupMessage 处理群消息
|
||
func (h *WebSocketHandler) handleGroupMessage(client *ws.Client, msg *ws.WSMessage) {
|
||
// 打印接收到的消息类型和数据,用于调试
|
||
log.Printf("[handleGroupMessage] Received message type: %s", msg.Type)
|
||
log.Printf("[handleGroupMessage] Message data: %+v", msg.Data)
|
||
|
||
data, ok := msg.Data.(map[string]interface{})
|
||
if !ok {
|
||
log.Printf("Invalid group message data format: data is not map[string]interface{}")
|
||
return
|
||
}
|
||
|
||
// 解析群组ID(支持 camelCase 和 snake_case)
|
||
var groupIDFloat float64
|
||
groupID := "" // 使用 groupID 作为最终变量名
|
||
if val, ok := data["groupId"].(float64); ok {
|
||
groupIDFloat = val
|
||
groupID = strconv.FormatFloat(groupIDFloat, 'f', 0, 64)
|
||
} else if val, ok := data["group_id"].(string); ok {
|
||
groupID = val
|
||
}
|
||
|
||
if groupID == "" {
|
||
log.Printf("Missing groupId in group message")
|
||
return
|
||
}
|
||
|
||
// 解析会话ID(支持 camelCase 和 snake_case)
|
||
var conversationID string
|
||
if val, ok := data["conversationId"].(string); ok {
|
||
conversationID = val
|
||
} else if val, ok := data["conversation_id"].(string); ok {
|
||
conversationID = val
|
||
}
|
||
if conversationID == "" {
|
||
log.Printf("Missing conversationId in group message")
|
||
return
|
||
}
|
||
|
||
// 解析 segments
|
||
var segments model.MessageSegments
|
||
if data["segments"] != nil {
|
||
segmentsBytes, err := json.Marshal(data["segments"])
|
||
if err == nil {
|
||
json.Unmarshal(segmentsBytes, &segments)
|
||
}
|
||
}
|
||
|
||
// 解析@用户列表(支持 camelCase 和 snake_case)
|
||
var mentionUsers []uint64
|
||
var mentionUsersInterface []interface{}
|
||
if val, ok := data["mentionUsers"].([]interface{}); ok {
|
||
mentionUsersInterface = val
|
||
} else if val, ok := data["mention_users"].([]interface{}); ok {
|
||
mentionUsersInterface = val
|
||
}
|
||
if len(mentionUsersInterface) > 0 {
|
||
for _, uid := range mentionUsersInterface {
|
||
if uidFloat, ok := uid.(float64); ok {
|
||
mentionUsers = append(mentionUsers, uint64(uidFloat))
|
||
} else if uidStr, ok := uid.(string); ok {
|
||
// 处理字符串格式的用户ID
|
||
if uidInt, err := strconv.ParseUint(uidStr, 10, 64); err == nil {
|
||
mentionUsers = append(mentionUsers, uidInt)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 解析@所有人(支持 camelCase 和 snake_case)
|
||
var mentionAll bool
|
||
if val, ok := data["mentionAll"].(bool); ok {
|
||
mentionAll = val
|
||
} else if val, ok := data["mention_all"].(bool); ok {
|
||
mentionAll = val
|
||
}
|
||
|
||
// 检查用户是否可以发送群消息(验证成员身份和禁言状态)
|
||
// client.UserID 已经是 string 格式的 UUID
|
||
if err := h.groupService.CanSendGroupMessage(client.UserID, groupID); err != nil {
|
||
log.Printf("User cannot send group message: %v", err)
|
||
// 发送错误消息
|
||
errorMsg := ws.CreateWSMessage(ws.MessageTypeError, map[string]string{
|
||
"error": "Cannot send group message",
|
||
"reason": err.Error(),
|
||
"type": "group_message_error",
|
||
"groupId": groupID,
|
||
})
|
||
if client.Send != nil {
|
||
msgBytes, _ := json.Marshal(errorMsg)
|
||
client.Send <- msgBytes
|
||
}
|
||
return
|
||
}
|
||
|
||
// 检查@所有人权限(只有群主和管理员可以@所有人)
|
||
if mentionAll {
|
||
if !h.groupService.IsGroupAdmin(client.UserID, groupID) {
|
||
log.Printf("User %s has no permission to mention all in group %s", client.UserID, groupID)
|
||
mentionAll = false // 取消@所有人标记
|
||
}
|
||
}
|
||
|
||
// 创建消息
|
||
message := &model.Message{
|
||
ConversationID: conversationID,
|
||
SenderID: client.UserID,
|
||
Segments: segments,
|
||
Status: model.MessageStatusNormal,
|
||
MentionAll: mentionAll,
|
||
}
|
||
|
||
// 序列化mentionUsers为JSON
|
||
if len(mentionUsers) > 0 {
|
||
mentionUsersJSON, _ := json.Marshal(mentionUsers)
|
||
message.MentionUsers = string(mentionUsersJSON)
|
||
}
|
||
|
||
// 保存消息到数据库(只存库,不发私聊 WebSocket 帧,群消息通过 BroadcastGroupMessage 单独广播)
|
||
savedMessage, err := h.chatService.SaveMessage(context.Background(), client.UserID, conversationID, segments, nil)
|
||
if err != nil {
|
||
log.Printf("Failed to save group message: %v", err)
|
||
errorMsg := ws.CreateWSMessage(ws.MessageTypeError, map[string]string{
|
||
"error": "Failed to save group message",
|
||
})
|
||
if client.Send != nil {
|
||
msgBytes, _ := json.Marshal(errorMsg)
|
||
client.Send <- msgBytes
|
||
}
|
||
return
|
||
}
|
||
|
||
// 更新消息的mention信息
|
||
if len(mentionUsers) > 0 || mentionAll {
|
||
message.ID = savedMessage.ID
|
||
message.Seq = savedMessage.Seq
|
||
}
|
||
|
||
// 构造群消息响应
|
||
groupMsg := &ws.GroupMessage{
|
||
ID: savedMessage.ID,
|
||
ConversationID: conversationID,
|
||
GroupID: groupID,
|
||
SenderID: client.UserID,
|
||
Seq: savedMessage.Seq,
|
||
Segments: segments,
|
||
MentionUsers: mentionUsers,
|
||
MentionAll: mentionAll,
|
||
CreatedAt: savedMessage.CreatedAt.UnixMilli(),
|
||
}
|
||
|
||
// 广播消息给群组所有成员(排除发送者)
|
||
h.BroadcastGroupMessage(groupID, groupMsg, client.UserID)
|
||
|
||
// 发送确认消息给发送者(作为meta事件)
|
||
// 使用 meta 事件格式发送 ack
|
||
log.Printf("[DEBUG HandleGroupMessageSend] 准备发送 ack 消息, userID=%s, messageID=%s, seq=%d",
|
||
client.UserID, savedMessage.ID, savedMessage.Seq)
|
||
|
||
metaAckMsg := ws.CreateWSMessage("meta", map[string]interface{}{
|
||
"detail_type": ws.MetaDetailTypeAck,
|
||
"conversation_id": conversationID,
|
||
"group_id": groupID,
|
||
"id": savedMessage.ID,
|
||
"user_id": client.UserID,
|
||
"sender_id": client.UserID,
|
||
"seq": savedMessage.Seq,
|
||
"segments": segments,
|
||
"created_at": savedMessage.CreatedAt.UnixMilli(),
|
||
})
|
||
if client.Send != nil {
|
||
msgBytes, _ := json.Marshal(metaAckMsg)
|
||
log.Printf("[DEBUG HandleGroupMessageSend] 发送 ack 消息到 channel, userID=%s, msg=%s",
|
||
client.UserID, string(msgBytes))
|
||
client.Send <- msgBytes
|
||
} else {
|
||
log.Printf("[ERROR HandleGroupMessageSend] client.Send 为 nil, userID=%s", client.UserID)
|
||
}
|
||
|
||
// 处理@提及通知
|
||
if len(mentionUsers) > 0 || mentionAll {
|
||
// 提取文本正文(不含 @ 部分)
|
||
textContent := dto.ExtractTextContentFromModel(segments)
|
||
// 在通知内容前拼接被@的真实昵称,通过群成员列表查找
|
||
mentionContent := h.buildMentionContent(groupID, mentionUsers, mentionAll, textContent)
|
||
h.handleGroupMention(groupID, savedMessage.ID, client.UserID, mentionContent, mentionUsers, mentionAll)
|
||
}
|
||
}
|
||
|
||
// handleGroupTyping 处理群输入状态
|
||
func (h *WebSocketHandler) handleGroupTyping(client *ws.Client, msg *ws.WSMessage) {
|
||
data, ok := msg.Data.(map[string]interface{})
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
groupIDFloat, _ := data["groupId"].(float64)
|
||
if groupIDFloat == 0 {
|
||
return
|
||
}
|
||
groupID := strconv.FormatFloat(groupIDFloat, 'f', 0, 64)
|
||
|
||
isTyping, _ := data["isTyping"].(bool)
|
||
|
||
// 验证用户是否是群成员
|
||
// client.UserID 已经是 string 格式的 UUID
|
||
isMember, err := h.groupRepo.IsMember(groupID, client.UserID)
|
||
if err != nil || !isMember {
|
||
return
|
||
}
|
||
|
||
// 获取用户信息
|
||
user, err := h.userRepo.GetByID(client.UserID)
|
||
if err != nil {
|
||
return
|
||
}
|
||
|
||
// 构造输入状态消息
|
||
typingMsg := &ws.GroupTypingMessage{
|
||
GroupID: groupID,
|
||
UserID: client.UserID,
|
||
Username: user.Username,
|
||
IsTyping: isTyping,
|
||
}
|
||
|
||
// 广播给群组其他成员
|
||
wsMsg := ws.CreateWSMessage(ws.MessageTypeGroupTyping, typingMsg)
|
||
h.BroadcastGroupNoticeExclude(groupID, wsMsg, client.UserID)
|
||
}
|
||
|
||
// handleGroupReadReceipt 处理群消息已读回执
|
||
func (h *WebSocketHandler) handleGroupReadReceipt(client *ws.Client, msg *ws.WSMessage) {
|
||
data, ok := msg.Data.(map[string]interface{})
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
conversationID, _ := data["conversationId"].(string)
|
||
if conversationID == "" {
|
||
return
|
||
}
|
||
|
||
lastReadSeq, _ := data["lastReadSeq"].(float64)
|
||
if lastReadSeq == 0 {
|
||
return
|
||
}
|
||
|
||
// 标记已读
|
||
if err := h.chatService.MarkAsRead(context.Background(), conversationID, client.UserID, int64(lastReadSeq)); err != nil {
|
||
log.Printf("Failed to mark group message as read: %v", err)
|
||
}
|
||
}
|
||
|
||
// handleGroupRecall 处理群消息撤回
|
||
func (h *WebSocketHandler) handleGroupRecall(client *ws.Client, msg *ws.WSMessage) {
|
||
data, ok := msg.Data.(map[string]interface{})
|
||
if !ok {
|
||
return
|
||
}
|
||
|
||
messageID, _ := data["messageId"].(string)
|
||
if messageID == "" {
|
||
return
|
||
}
|
||
|
||
groupIDFloat, _ := data["groupId"].(float64)
|
||
if groupIDFloat == 0 {
|
||
return
|
||
}
|
||
groupID := strconv.FormatFloat(groupIDFloat, 'f', 0, 64)
|
||
|
||
// 撤回消息
|
||
if err := h.chatService.RecallMessage(context.Background(), messageID, client.UserID); err != nil {
|
||
log.Printf("Failed to recall group message: %v", err)
|
||
errorMsg := ws.CreateWSMessage(ws.MessageTypeError, map[string]string{
|
||
"error": "Failed to recall message",
|
||
})
|
||
if client.Send != nil {
|
||
msgBytes, _ := json.Marshal(errorMsg)
|
||
client.Send <- msgBytes
|
||
}
|
||
return
|
||
}
|
||
|
||
// 广播撤回通知给群组所有成员
|
||
recallNotice := ws.CreateWSMessage(ws.MessageTypeGroupRecall, map[string]interface{}{
|
||
"messageId": messageID,
|
||
"groupId": groupID,
|
||
"userId": client.UserID,
|
||
"timestamp": time.Now().UnixMilli(),
|
||
})
|
||
h.BroadcastGroupNotice(groupID, recallNotice)
|
||
}
|
||
|
||
// handleGroupMention 处理群消息@提及通知
|
||
func (h *WebSocketHandler) handleGroupMention(groupID string, messageID, senderID, content string, mentionUsers []uint64, mentionAll bool) {
|
||
// 如果@所有人,获取所有群成员
|
||
if mentionAll {
|
||
members, _, err := h.groupRepo.GetMembers(groupID, 1, 1000)
|
||
if err != nil {
|
||
log.Printf("Failed to get group members for mention all: %v", err)
|
||
return
|
||
}
|
||
|
||
for _, member := range members {
|
||
// 不通知发送者自己
|
||
memberIDStr := member.UserID
|
||
if memberIDStr == senderID {
|
||
continue
|
||
}
|
||
|
||
// 发送@提及通知
|
||
mentionMsg := &ws.GroupMentionMessage{
|
||
GroupID: groupID,
|
||
MessageID: messageID,
|
||
FromUserID: senderID,
|
||
Content: truncateContent(content, 50),
|
||
MentionAll: true,
|
||
CreatedAt: time.Now().UnixMilli(),
|
||
}
|
||
wsMsg := ws.CreateWSMessage(ws.MessageTypeGroupMention, mentionMsg)
|
||
h.wsManager.SendToUser(memberIDStr, wsMsg)
|
||
}
|
||
return
|
||
}
|
||
|
||
// 处理特定用户的@提及
|
||
for _, userID := range mentionUsers {
|
||
// userID 是 uint64,转换为 string
|
||
userIDStr := strconv.FormatUint(userID, 10)
|
||
if userIDStr == senderID {
|
||
continue // 不通知发送者自己
|
||
}
|
||
|
||
mentionMsg := &ws.GroupMentionMessage{
|
||
GroupID: groupID,
|
||
MessageID: messageID,
|
||
FromUserID: senderID,
|
||
Content: truncateContent(content, 50),
|
||
MentionAll: false,
|
||
CreatedAt: time.Now().UnixMilli(),
|
||
}
|
||
wsMsg := ws.CreateWSMessage(ws.MessageTypeGroupMention, mentionMsg)
|
||
h.wsManager.SendToUser(userIDStr, wsMsg)
|
||
}
|
||
}
|
||
|
||
// buildMentionContent 构建@提及通知的内容,通过群成员列表查找被@用户的真实昵称
|
||
func (h *WebSocketHandler) buildMentionContent(groupID string, mentionUsers []uint64, mentionAll bool, textBody string) string {
|
||
var prefix string
|
||
if mentionAll {
|
||
prefix = "@所有人 "
|
||
} else if len(mentionUsers) > 0 {
|
||
// 查询群成员列表,找到被@用户的昵称
|
||
members, _, err := h.groupRepo.GetMembers(groupID, 1, 1000)
|
||
if err == nil {
|
||
memberNickMap := make(map[string]string, len(members))
|
||
for _, m := range members {
|
||
displayName := m.Nickname
|
||
if displayName == "" {
|
||
displayName = m.UserID
|
||
}
|
||
memberNickMap[m.UserID] = displayName
|
||
}
|
||
for _, uid := range mentionUsers {
|
||
uidStr := strconv.FormatUint(uid, 10)
|
||
if name, ok := memberNickMap[uidStr]; ok {
|
||
prefix += "@" + name + " "
|
||
} else {
|
||
prefix += "@某人 "
|
||
}
|
||
}
|
||
} else {
|
||
for range mentionUsers {
|
||
prefix += "@某人 "
|
||
}
|
||
}
|
||
}
|
||
return prefix + textBody
|
||
}
|
||
|
||
// BroadcastGroupMessage 向群组所有成员广播消息
|
||
func (h *WebSocketHandler) BroadcastGroupMessage(groupID string, message *ws.GroupMessage, excludeUserID string) {
|
||
// 获取群组所有成员
|
||
members, _, err := h.groupRepo.GetMembers(groupID, 1, 1000)
|
||
if err != nil {
|
||
log.Printf("Failed to get group members for broadcast: %v", err)
|
||
return
|
||
}
|
||
|
||
// 创建WebSocket消息
|
||
wsMsg := ws.CreateWSMessage(ws.MessageTypeGroupMessage, message)
|
||
|
||
// 遍历成员,如果在线则发送消息
|
||
for _, member := range members {
|
||
memberIDStr := member.UserID
|
||
|
||
// 排除发送者
|
||
if memberIDStr == excludeUserID {
|
||
continue
|
||
}
|
||
|
||
// 发送消息
|
||
h.wsManager.SendToUser(memberIDStr, wsMsg)
|
||
}
|
||
}
|
||
|
||
// BroadcastGroupNotice 广播群组通知给所有成员
|
||
func (h *WebSocketHandler) BroadcastGroupNotice(groupID string, notice *ws.WSMessage) {
|
||
// 获取群组所有成员
|
||
members, _, err := h.groupRepo.GetMembers(groupID, 1, 1000)
|
||
if err != nil {
|
||
log.Printf("Failed to get group members for notice broadcast: %v", err)
|
||
return
|
||
}
|
||
|
||
// 遍历成员,如果在线则发送通知
|
||
for _, member := range members {
|
||
memberIDStr := member.UserID
|
||
h.wsManager.SendToUser(memberIDStr, notice)
|
||
}
|
||
}
|
||
|
||
// BroadcastGroupNoticeExclude 广播群组通知给所有成员(排除指定用户)
|
||
func (h *WebSocketHandler) BroadcastGroupNoticeExclude(groupID string, notice *ws.WSMessage, excludeUserID string) {
|
||
// 获取群组所有成员
|
||
members, _, err := h.groupRepo.GetMembers(groupID, 1, 1000)
|
||
if err != nil {
|
||
log.Printf("Failed to get group members for notice broadcast: %v", err)
|
||
return
|
||
}
|
||
|
||
// 遍历成员,如果在线则发送通知
|
||
for _, member := range members {
|
||
memberIDStr := member.UserID
|
||
if memberIDStr == excludeUserID {
|
||
continue
|
||
}
|
||
h.wsManager.SendToUser(memberIDStr, notice)
|
||
}
|
||
}
|
||
|
||
// SendGroupMemberNotice 发送群成员变动通知
|
||
func (h *WebSocketHandler) SendGroupMemberNotice(noticeType string, groupID string, data *ws.GroupNoticeData) {
|
||
notice := &ws.GroupNoticeMessage{
|
||
NoticeType: noticeType,
|
||
GroupID: groupID,
|
||
Data: data,
|
||
Timestamp: time.Now().UnixMilli(),
|
||
}
|
||
wsMsg := ws.CreateWSMessage(ws.MessageTypeGroupNotice, notice)
|
||
h.BroadcastGroupNotice(groupID, wsMsg)
|
||
}
|
||
|
||
// truncateContent 截断内容
|
||
func truncateContent(content string, maxLen int) string {
|
||
if len(content) <= maxLen {
|
||
return content
|
||
}
|
||
return content[:maxLen] + "..."
|
||
}
|
||
|
||
// BroadcastGroupTyping 向群组所有成员广播输入状态
|
||
func (h *WebSocketHandler) BroadcastGroupTyping(groupID string, typingMsg *ws.GroupTypingMessage, excludeUserID string) {
|
||
// 获取群组所有成员
|
||
members, _, err := h.groupRepo.GetMembers(groupID, 1, 1000)
|
||
if err != nil {
|
||
log.Printf("Failed to get group members for typing broadcast: %v", err)
|
||
return
|
||
}
|
||
|
||
// 创建WebSocket消息
|
||
wsMsg := ws.CreateWSMessage(ws.MessageTypeGroupTyping, typingMsg)
|
||
|
||
// 遍历成员,如果在线则发送消息
|
||
for _, member := range members {
|
||
memberIDStr := member.UserID
|
||
|
||
// 排除指定用户
|
||
if memberIDStr == excludeUserID {
|
||
continue
|
||
}
|
||
|
||
// 发送消息
|
||
h.wsManager.SendToUser(memberIDStr, wsMsg)
|
||
}
|
||
}
|
||
|
||
// BroadcastGroupRead 向群组所有成员广播已读状态
|
||
func (h *WebSocketHandler) BroadcastGroupRead(groupID string, readMsg map[string]interface{}, excludeUserID string) {
|
||
// 获取群组所有成员
|
||
members, _, err := h.groupRepo.GetMembers(groupID, 1, 1000)
|
||
if err != nil {
|
||
log.Printf("Failed to get group members for read broadcast: %v", err)
|
||
return
|
||
}
|
||
|
||
// 创建WebSocket消息
|
||
wsMsg := ws.CreateWSMessage(ws.MessageTypeGroupRead, readMsg)
|
||
|
||
// 遍历成员,如果在线则发送消息
|
||
for _, member := range members {
|
||
memberIDStr := member.UserID
|
||
|
||
// 排除指定用户
|
||
if memberIDStr == excludeUserID {
|
||
continue
|
||
}
|
||
|
||
// 发送消息
|
||
h.wsManager.SendToUser(memberIDStr, wsMsg)
|
||
}
|
||
}
|