Files
nl-im-service/internal/api/handler.go
2026-07-08 08:18:58 +08:00

275 lines
7.0 KiB
Go
Raw Permalink 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 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}, "获取成功")
}