Files

367 lines
10 KiB
Go
Raw Permalink Normal View History

2025-12-02 21:00:26 +08:00
/**
* package api
* 作用处理 HTTP 请求接口包括消息发送历史记录系统状态检查等
* 说明V2 版本中消息发送主要通过 HTTP 接口进行而非 WebSocket 直接推送
*/
package api
import (
"fmt"
2025-12-03 11:00:47 +08:00
"strconv"
2025-12-02 21:00:26 +08:00
"time"
"xk-websocket-v2/internal/manager"
"xk-websocket-v2/internal/model"
"xk-websocket-v2/internal/service"
"xk-websocket-v2/internal/turnserver"
2025-12-03 11:00:47 +08:00
"xk-websocket-v2/internal/utils"
2025-12-02 21:00:26 +08:00
"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 {
2025-12-03 11:00:47 +08:00
utils.BadRequest(c, "无效的JSON参数")
2025-12-02 21:00:26 +08:00
return
}
2026-08-24 15:29:53 +08:00
// 2. 获取发送者 ID强制取自 JWT由认证中间件注入 user_id
// 禁止客户端通过 Header/Body 伪造他人身份发送消息或信令
uid, ok := c.Get("user_id")
if !ok {
utils.Unauthorized(c, "未认证")
return
2025-12-02 21:00:26 +08:00
}
2026-08-24 15:29:53 +08:00
senderID := uid.(string)
2025-12-02 21:00:26 +08:00
// 3. [关键修复] 获取发送端的 WebSocket ClientID
// 前端在调用此接口时,必须带上自己的 socket_client_id
// 如果前端没传 (兼容旧代码),则 fallback 到 api-gateway这会导致发送端自己也收到"其他设备接听"通知
clientID := req.SenderClientID
if clientID == "" {
clientID = "api-gateway"
}
// 4. 构造临时的发送者客户端对象
// 这个对象将传递给 Service 层,用于识别消息来源
mockClient := &manager.Client{
2026-07-08 08:18:58 +08:00
UserID: senderID,
ID: clientID,
RemoteIP: utils.GetClientIP(c),
2025-12-02 21:00:26 +08:00
}
2026-07-08 08:18:58 +08:00
// 5. 调用核心业务逻辑处理消息,返回持久化后的消息体供前端对齐 ID
2026-08-24 15:29:53 +08:00
savedMsg, err := service.ChatSvc.HandleUserMessage(mockClient, &req)
// 6. 校验被拒/落库失败必须返回错误:原实现只看消息指针,
// 被拒消息也响应"消息已发送",发送方界面显示成功但对方永远收不到
if err != nil {
utils.Error(c, utils.CodeForbidden, err.Error())
return
}
2025-12-02 21:00:26 +08:00
2026-08-24 15:29:53 +08:00
// 7. 返回成功响应(含服务端消息 ID避免前端重复展示
2026-07-08 08:18:58 +08:00
if savedMsg != nil {
utils.SuccessWithData(c, savedMsg, "消息已发送")
return
}
2025-12-03 11:00:47 +08:00
utils.Success(c, "消息已发送")
2025-12-02 21:00:26 +08:00
}
/**
* 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 {
2025-12-03 11:00:47 +08:00
utils.BadRequest(c, "参数错误")
2025-12-02 21:00:26 +08:00
return
}
2026-08-24 15:29:53 +08:00
// 绑定的用户ID强制取自 JWT忽略请求体中的 user_id
// 防止攻击者把任意 user_id 绑定到自己的 client_id 从而劫持他人的消息推送
uid, ok := c.Get("user_id")
if !ok {
utils.Unauthorized(c, "未认证")
return
}
req.UserID = uid.(string)
2025-12-02 21:00:26 +08:00
// 调用服务层进行绑定
service.ChatSvc.BindUserByClientID(req.ClientID, req.UserID)
2025-12-03 11:00:47 +08:00
utils.Success(c, "绑定成功")
2025-12-02 21:00:26 +08:00
}
/**
* CheckUserOnlineHandler
* 功能检查用户是否在线
* 路径GET /api/check-user-online
*/
func CheckUserOnlineHandler(c *gin.Context) {
userID := c.Query("user_id")
// 调用服务层查询 Redis
isOnline := service.ChatSvc.IsUserOnline(userID)
2025-12-03 11:00:47 +08:00
utils.SuccessWithData(c, gin.H{"is_online": isOnline}, "查询成功")
2025-12-02 21:00:26 +08:00
}
// ==========================================
// 数据查询接口
// ==========================================
/**
* HistoryHandler
* 功能获取历史消息记录 (分页)
* 路径GET /api/messages
*/
func HistoryHandler(c *gin.Context) {
roomID := c.Query("room_id")
2025-12-03 11:00:47 +08:00
if roomID == "" {
utils.BadRequest(c, "room_id参数必填")
return
}
2026-08-24 15:29:53 +08:00
// 鉴权:仅房间成员可拉取历史消息,防止任意登录用户凭 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
}
2025-12-03 11:00:47 +08:00
// 分页参数
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
}
2025-12-02 21:00:26 +08:00
var msgs []model.ChatMessage
2025-12-03 11:00:47 +08:00
var total int64
2026-08-24 15:29:53 +08:00
// 排除当前用户已删除的消息("删除仅对我生效",见 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)
2025-12-02 21:00:26 +08:00
2025-12-03 11:00:47 +08:00
// 分页查询
offset := (page - 1) * pageSize
2025-12-02 21:00:26 +08:00
result := service.ChatSvc.DB.Where("room_id = ?", roomID).
2026-08-24 15:29:53 +08:00
Where("id NOT IN (?)", deletedSubQuery).
2025-12-03 11:00:47 +08:00
Order("created_at desc").
Offset(offset).
Limit(pageSize).
Find(&msgs)
2025-12-02 21:00:26 +08:00
if result.Error != nil {
2025-12-03 11:00:47 +08:00
utils.InternalError(c, "db error")
2025-12-02 21:00:26 +08:00
return
}
2025-12-03 11:00:47 +08:00
// 确保返回空数组而不是null
if msgs == nil {
msgs = []model.ChatMessage{}
}
utils.SuccessWithData(c, gin.H{
"data": msgs,
"total": total,
"page": page,
"size": pageSize,
}, "获取成功")
2025-12-02 21:00:26 +08:00
}
2026-07-08 08:18:58 +08:00
/**
* 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, "撤回成功")
}
2026-08-24 15:29:53 +08:00
/**
* 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, "删除成功")
}
2025-12-02 21:00:26 +08:00
/**
* SyncMessagesHandler
* 功能全量同步消息 (V1 兼容)
* 路径GET /api/messages/sync
*/
func SyncMessagesHandler(c *gin.Context) {
HistoryHandler(c)
}
// ==========================================
// 系统与 WebRTC 接口
// ==========================================
/**
* HealthHandler
* 功能服务健康检查
* 路径GET /api/health
*/
func HealthHandler(c *gin.Context) {
2025-12-03 11:00:47 +08:00
utils.SuccessWithData(c, gin.H{
2025-12-02 21:00:26 +08:00
"status": "ok",
"node": viper.GetString("app.node_id"),
"time": time.Now().Format(time.RFC3339),
2025-12-03 11:00:47 +08:00
}, "服务正常")
2025-12-02 21:00:26 +08:00
}
/**
* ICEHandler
* 功能获取 TURN/STUN 服务器配置及临时凭证
* 路径GET /api/ice-servers
* 用途WebRTC 前端在建立 PeerConnection 前需调用此接口
*/
func ICEHandler(c *gin.Context) {
2026-08-24 15:29:53 +08:00
// 凭证绑定当前登录用户,取自 JWT避免匿名为任意 user_id 签发 TURN 凭证被滥用
uid, ok := c.Get("user_id")
if !ok {
utils.Unauthorized(c, "未认证")
return
}
userID := uid.(string)
2025-12-02 21:00:26 +08:00
// 生成临时凭证 (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,
}
2025-12-03 11:00:47 +08:00
utils.SuccessWithData(c, []model.ICEServerConfig{cfg}, "获取成功")
2025-12-02 21:00:26 +08:00
}