367 lines
10 KiB
Go
367 lines
10 KiB
Go
/**
|
||
* 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}, "获取成功")
|
||
}
|