/** * package api * 作用:处理 HTTP 请求接口,包括消息发送、历史记录、系统状态检查等。 * 说明:V2 版本中消息发送主要通过 HTTP 接口进行,而非 WebSocket 直接推送。 */ package api import ( "fmt" "strconv" "time" "xk-websocket-v2/internal/manager" "xk-websocket-v2/internal/model" "xk-websocket-v2/internal/service" "xk-websocket-v2/internal/turnserver" "xk-websocket-v2/internal/utils" "github.com/gin-gonic/gin" "github.com/spf13/viper" ) // ========================================== // 消息发送相关接口 // ========================================== /** * SendHandler * 功能:通用消息发送接口 (HTTP -> WebSocket)。 * 路径:POST /api/send * 逻辑:支持通过 target_client_id 或 receiver_user_id 发送。 * 重要:必须正确处理 SenderClientID,防止多端同步时误通知发送端自己。 */ func SendHandler(c *gin.Context) { var req model.SendMessageReq // 1. 绑定并校验 JSON 参数 if err := c.ShouldBindJSON(&req); err != nil { utils.BadRequest(c, "无效的JSON参数") return } // 2. 获取发送者 ID (从 Header 中获取,模拟鉴权) // 生产环境应从 JWT Token 中解析 UserID senderID := c.GetHeader("X-User-ID") if senderID == "" { senderID = "system" // 默认为系统消息 } // 3. [关键修复] 获取发送端的 WebSocket ClientID // 前端在调用此接口时,必须带上自己的 socket_client_id // 如果前端没传 (兼容旧代码),则 fallback 到 api-gateway,这会导致发送端自己也收到"其他设备接听"通知 clientID := req.SenderClientID if clientID == "" { clientID = "api-gateway" } // 4. 构造临时的发送者客户端对象 // 这个对象将传递给 Service 层,用于识别消息来源 mockClient := &manager.Client{ UserID: senderID, ID: clientID, RemoteIP: utils.GetClientIP(c), } // 5. 调用核心业务逻辑处理消息,返回持久化后的消息体供前端对齐 ID savedMsg := service.ChatSvc.HandleUserMessage(mockClient, &req) // 6. 返回成功响应(含服务端消息 ID,避免前端重复展示) if savedMsg != nil { utils.SuccessWithData(c, savedMsg, "消息已发送") return } utils.Success(c, "消息已发送") } /** * SendToUserHandler (V1 兼容) * 功能:专门用于给指定用户发送消息。 * 路径:POST /api/send-to-user */ func SendToUserHandler(c *gin.Context) { // 复用 SendHandler,因为 V2 的 SendHandler 已经支持 ReceiverUserID SendHandler(c) } // ========================================== // 用户与连接管理接口 // ========================================== /** * BindHandler * 功能:手动绑定 ClientID 和 UserID。 * 路径:POST /api/bind * 场景:当 WebSocket 连接建立后,客户端通过 HTTP 接口补充用户信息。 */ func BindHandler(c *gin.Context) { var req model.BindReq if err := c.ShouldBindJSON(&req); err != nil { utils.BadRequest(c, "参数错误") return } // 调用服务层进行绑定 service.ChatSvc.BindUserByClientID(req.ClientID, req.UserID) utils.Success(c, "绑定成功") } /** * CheckUserOnlineHandler * 功能:检查用户是否在线。 * 路径:GET /api/check-user-online */ func CheckUserOnlineHandler(c *gin.Context) { userID := c.Query("user_id") // 调用服务层查询 Redis isOnline := service.ChatSvc.IsUserOnline(userID) utils.SuccessWithData(c, gin.H{"is_online": isOnline}, "查询成功") } // ========================================== // 数据查询接口 // ========================================== /** * HistoryHandler * 功能:获取历史消息记录 (分页)。 * 路径:GET /api/messages */ func HistoryHandler(c *gin.Context) { roomID := c.Query("room_id") if roomID == "" { utils.BadRequest(c, "room_id参数必填") return } // 分页参数 page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "50")) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 50 } var msgs []model.ChatMessage var total int64 // 获取总数 service.ChatSvc.DB.Model(&model.ChatMessage{}).Where("room_id = ?", roomID).Count(&total) // 分页查询 offset := (page - 1) * pageSize result := service.ChatSvc.DB.Where("room_id = ?", roomID). Order("created_at desc"). Offset(offset). Limit(pageSize). Find(&msgs) if result.Error != nil { utils.InternalError(c, "db error") return } // 确保返回空数组而不是null if msgs == nil { msgs = []model.ChatMessage{} } utils.SuccessWithData(c, gin.H{ "data": msgs, "total": total, "page": page, "size": pageSize, }, "获取成功") } /** * RecallMessageHandler * 功能:撤回消息(2 分钟内,仅发送者可撤回) * 路径:POST /api/messages/recall */ func RecallMessageHandler(c *gin.Context) { userID, exists := c.Get("user_id") if !exists { utils.Unauthorized(c, "未认证") return } var req struct { MessageID uint `json:"message_id" binding:"required"` } if err := c.ShouldBindJSON(&req); err != nil { utils.BadRequest(c, "参数错误") return } var msg model.ChatMessage if err := service.ChatSvc.DB.First(&msg, req.MessageID).Error; err != nil { utils.NotFound(c, "消息不存在") return } if msg.SenderUserID != userID.(string) { utils.Forbidden(c, "无权撤回该消息") return } if time.Since(msg.CreatedAt) > 2*time.Minute { utils.BadRequest(c, "超过2分钟,无法撤回") return } msg.MessageType = model.MessageTypeSystem msg.Content = "撤回了一条消息" if err := service.ChatSvc.DB.Save(&msg).Error; err != nil { utils.InternalError(c, "撤回失败") return } service.ChatSvc.BroadcastChatMessage(msg) utils.SuccessWithData(c, msg, "撤回成功") } /** * SyncMessagesHandler * 功能:全量同步消息 (V1 兼容)。 * 路径:GET /api/messages/sync */ func SyncMessagesHandler(c *gin.Context) { HistoryHandler(c) } // ========================================== // 系统与 WebRTC 接口 // ========================================== /** * HealthHandler * 功能:服务健康检查。 * 路径:GET /api/health */ func HealthHandler(c *gin.Context) { utils.SuccessWithData(c, gin.H{ "status": "ok", "node": viper.GetString("app.node_id"), "time": time.Now().Format(time.RFC3339), }, "服务正常") } /** * ICEHandler * 功能:获取 TURN/STUN 服务器配置及临时凭证。 * 路径:GET /api/ice-servers * 用途:WebRTC 前端在建立 PeerConnection 前需调用此接口。 */ func ICEHandler(c *gin.Context) { userID := c.Query("user_id") // 生成临时凭证 (HMAC-SHA1) username, credential := turnserver.GenerateCredentials(userID) // 从配置读取公网IP和端口 ip := viper.GetString("turn.public_ip") port := viper.GetInt("turn.listen_port") // 构造配置返回给前端 cfg := model.ICEServerConfig{ Urls: []string{fmt.Sprintf("turn:%s:%d", ip, port)}, Username: username, Credential: credential, } utils.SuccessWithData(c, []model.ICEServerConfig{cfg}, "获取成功") }