Files
nl-im-service/internal/api/handler.go
2026-08-24 15:29:53 +08:00

367 lines
10 KiB
Go
Raw 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强制取自 JWT由认证中间件注入 user_id
// 禁止客户端通过 Header/Body 伪造他人身份发送消息或信令
uid, ok := c.Get("user_id")
if !ok {
utils.Unauthorized(c, "未认证")
return
}
senderID := uid.(string)
// 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, err := service.ChatSvc.HandleUserMessage(mockClient, &req)
// 6. 校验被拒/落库失败必须返回错误:原实现只看消息指针,
// 被拒消息也响应"消息已发送",发送方界面显示成功但对方永远收不到
if err != nil {
utils.Error(c, utils.CodeForbidden, err.Error())
return
}
// 7. 返回成功响应(含服务端消息 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
}
// 绑定的用户ID强制取自 JWT忽略请求体中的 user_id
// 防止攻击者把任意 user_id 绑定到自己的 client_id 从而劫持他人的消息推送
uid, ok := c.Get("user_id")
if !ok {
utils.Unauthorized(c, "未认证")
return
}
req.UserID = uid.(string)
// 调用服务层进行绑定
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
}
// 鉴权:仅房间成员可拉取历史消息,防止任意登录用户凭 room_id 窥探他人聊天记录
uid, ok := c.Get("user_id")
if !ok {
utils.Unauthorized(c, "未认证")
return
}
if !service.RoomSvc.IsRoomMember(roomID, uid.(string)) {
utils.Forbidden(c, "无权查看该房间消息")
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
// 排除当前用户已删除的消息("删除仅对我生效",见 MessageDeletion 模型注释)。
// 用 NOT IN 子查询而非 JOIN删除记录量级远小于消息量级且保持分页语义简单
deletedSubQuery := service.ChatSvc.DB.Model(&model.MessageDeletion{}).
Select("message_id").
Where("user_id = ?", uid.(string))
// 获取总数(同样排除已删除,保证 total 与实际可见条数一致)
service.ChatSvc.DB.Model(&model.ChatMessage{}).
Where("room_id = ?", roomID).
Where("id NOT IN (?)", deletedSubQuery).
Count(&total)
// 分页查询
offset := (page - 1) * pageSize
result := service.ChatSvc.DB.Where("room_id = ?", roomID).
Where("id NOT IN (?)", deletedSubQuery).
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, "撤回成功")
}
/**
* DeleteMessageHandler
* 功能:删除消息(仅对操作者自己生效,对方仍可见,区别于撤回)。
* 逻辑:不删除消息本体,仅在 message_deletions 表插入删除记录,
* 历史消息查询时排除,避免"本地删了、刷新又回来"的假删除问题。
* 路径POST /api/messages/delete
*/
func DeleteMessageHandler(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 !service.RoomSvc.IsRoomMember(msg.RoomID, userID.(string)) {
utils.Forbidden(c, "无权删除该消息")
return
}
// 幂等插入:重复删除同一条消息时唯一索引冲突,视为成功
deletion := model.MessageDeletion{
MessageID: req.MessageID,
UserID: userID.(string),
}
if err := service.ChatSvc.DB.Create(&deletion).Error; err != nil {
if !utils.IsDuplicateEntryError(err) {
utils.InternalError(c, "删除失败")
return
}
}
utils.Success(c, "删除成功")
}
/**
* 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) {
// 凭证绑定当前登录用户,取自 JWT避免匿名为任意 user_id 签发 TURN 凭证被滥用
uid, ok := c.Get("user_id")
if !ok {
utils.Unauthorized(c, "未认证")
return
}
userID := uid.(string)
// 生成临时凭证 (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}, "获取成功")
}