修复了一些问题
This commit is contained in:
@@ -25,7 +25,7 @@ func LoginHandler(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 获取客户端IP
|
||||
ip := getClientIP(c)
|
||||
ip := utils.GetClientIP(c)
|
||||
|
||||
// 调用认证服务登录
|
||||
user, err := service.AuthSvc.Login(req.Account, req.Password)
|
||||
@@ -57,24 +57,6 @@ func LoginHandler(c *gin.Context) {
|
||||
}, "登录成功")
|
||||
}
|
||||
|
||||
// getClientIP 获取客户端IP
|
||||
func getClientIP(c *gin.Context) string {
|
||||
// 优先从X-Forwarded-For获取
|
||||
ip := c.GetHeader("X-Forwarded-For")
|
||||
if ip != "" {
|
||||
return ip
|
||||
}
|
||||
|
||||
// 从X-Real-IP获取
|
||||
ip = c.GetHeader("X-Real-IP")
|
||||
if ip != "" {
|
||||
return ip
|
||||
}
|
||||
|
||||
// 从RemoteAddr获取
|
||||
return c.ClientIP()
|
||||
}
|
||||
|
||||
/**
|
||||
* RegisterHandler
|
||||
* 功能:用户注册
|
||||
@@ -222,4 +204,3 @@ func SendSmsCodeHandler(c *gin.Context) {
|
||||
"code": code, // 仅开发环境,生产环境应移除
|
||||
}, "验证码已发送")
|
||||
}
|
||||
|
||||
|
||||
@@ -310,6 +310,7 @@ func UpdateContactHandler(c *gin.Context) {
|
||||
GroupID uint `json:"group_id"`
|
||||
IsTop *bool `json:"is_top"`
|
||||
IsMuted *bool `json:"is_muted"`
|
||||
IsBlocked *bool `json:"is_blocked"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
@@ -330,6 +331,9 @@ func UpdateContactHandler(c *gin.Context) {
|
||||
if req.IsMuted != nil {
|
||||
updates["is_muted"] = *req.IsMuted
|
||||
}
|
||||
if req.IsBlocked != nil {
|
||||
updates["is_blocked"] = *req.IsBlocked
|
||||
}
|
||||
|
||||
if err := service.ContactSvc.UpdateContact(userID.(string), contactID, updates); err != nil {
|
||||
utils.BadRequest(c, "更新失败: "+err.Error())
|
||||
|
||||
@@ -56,14 +56,19 @@ func SendHandler(c *gin.Context) {
|
||||
// 4. 构造临时的发送者客户端对象
|
||||
// 这个对象将传递给 Service 层,用于识别消息来源
|
||||
mockClient := &manager.Client{
|
||||
UserID: senderID,
|
||||
ID: clientID,
|
||||
UserID: senderID,
|
||||
ID: clientID,
|
||||
RemoteIP: utils.GetClientIP(c),
|
||||
}
|
||||
|
||||
// 5. 调用核心业务逻辑处理消息
|
||||
service.ChatSvc.HandleUserMessage(mockClient, &req)
|
||||
// 5. 调用核心业务逻辑处理消息,返回持久化后的消息体供前端对齐 ID
|
||||
savedMsg := service.ChatSvc.HandleUserMessage(mockClient, &req)
|
||||
|
||||
// 6. 返回成功响应
|
||||
// 6. 返回成功响应(含服务端消息 ID,避免前端重复展示)
|
||||
if savedMsg != nil {
|
||||
utils.SuccessWithData(c, savedMsg, "消息已发送")
|
||||
return
|
||||
}
|
||||
utils.Success(c, "消息已发送")
|
||||
}
|
||||
|
||||
@@ -169,6 +174,54 @@ func HistoryHandler(c *gin.Context) {
|
||||
}, "获取成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 兼容)。
|
||||
@@ -178,7 +231,6 @@ func SyncMessagesHandler(c *gin.Context) {
|
||||
HistoryHandler(c)
|
||||
}
|
||||
|
||||
|
||||
// ==========================================
|
||||
// 系统与 WebRTC 接口
|
||||
// ==========================================
|
||||
|
||||
@@ -934,3 +934,93 @@ func GetMemberMuteStatusHandler(c *gin.Context) {
|
||||
"muted_until": mutedUntil,
|
||||
}, "获取成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* GetGroupSettingsHandler
|
||||
* 功能:获取群设置(名称、头像、公告、邀请权限等)
|
||||
* 路径:GET /api/groups/:room_id/settings
|
||||
*/
|
||||
func GetGroupSettingsHandler(c *gin.Context) {
|
||||
roomID := c.Param("room_id")
|
||||
room, err := service.RoomSvc.GetRoom(roomID)
|
||||
if err != nil {
|
||||
utils.NotFound(c, "群聊不存在")
|
||||
return
|
||||
}
|
||||
utils.SuccessWithData(c, gin.H{
|
||||
"name": room.RoomName,
|
||||
"avatar": room.RoomAvatar,
|
||||
"description": room.Announcement,
|
||||
"invite_permission": 0,
|
||||
"created_at": room.CreatedAt,
|
||||
"updated_at": room.UpdatedAt,
|
||||
}, "获取成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateGroupSettingsHandler
|
||||
* 功能:更新群设置
|
||||
* 路径:POST /api/groups/:room_id/settings
|
||||
*/
|
||||
func UpdateGroupSettingsHandler(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
roomID := c.Param("room_id")
|
||||
|
||||
ok, err := service.RoomSvc.IsGroupAdminOrOwner(roomID, userID.(string))
|
||||
if err != nil || !ok {
|
||||
utils.Forbidden(c, "无权限修改群设置")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Name string `json:"name"`
|
||||
Avatar string `json:"avatar"`
|
||||
Description string `json:"description"`
|
||||
InvitePermission *int `json:"invite_permission"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
updates := map[string]interface{}{}
|
||||
if req.Name != "" {
|
||||
updates["room_name"] = req.Name
|
||||
}
|
||||
if req.Avatar != "" {
|
||||
updates["room_avatar"] = req.Avatar
|
||||
}
|
||||
if req.Description != "" {
|
||||
updates["announcement"] = req.Description
|
||||
}
|
||||
if len(updates) == 0 {
|
||||
utils.BadRequest(c, "没有可更新的字段")
|
||||
return
|
||||
}
|
||||
|
||||
if err := service.RoomSvc.DB.Model(&model.ChatRoom{}).Where("room_id = ?", roomID).Updates(updates).Error; err != nil {
|
||||
utils.InternalError(c, "更新失败")
|
||||
return
|
||||
}
|
||||
GetGroupSettingsHandler(c)
|
||||
}
|
||||
|
||||
/**
|
||||
* GetCallHistoryHandler
|
||||
* 功能:获取当前用户的通话记录(基于 call_id 消息)
|
||||
* 路径:GET /api/calls/history
|
||||
*/
|
||||
func GetCallHistoryHandler(c *gin.Context) {
|
||||
userID, _ := c.Get("user_id")
|
||||
var messages []model.ChatMessage
|
||||
err := service.ChatSvc.DB.
|
||||
Where("(sender_user_id = ? OR receiver_user_id = ?) AND call_id <> '' AND call_id IS NOT NULL", userID, userID).
|
||||
Order("created_at DESC").
|
||||
Limit(50).
|
||||
Find(&messages).Error
|
||||
if err != nil {
|
||||
utils.InternalError(c, "查询失败")
|
||||
return
|
||||
}
|
||||
utils.SuccessWithData(c, messages, "获取成功")
|
||||
}
|
||||
|
||||
126
internal/api/settings_handler.go
Normal file
126
internal/api/settings_handler.go
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* package api
|
||||
* 作用:用户设置与消息已读回执 HTTP 接口
|
||||
*/
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"xk-websocket-v2/internal/model"
|
||||
"xk-websocket-v2/internal/service"
|
||||
"xk-websocket-v2/internal/utils"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
/**
|
||||
* GetUserSettingsHandler
|
||||
* 功能:获取当前用户的全部设置
|
||||
*/
|
||||
func GetUserSettingsHandler(c *gin.Context) {
|
||||
userID, ok := c.Get("user_id")
|
||||
if !ok {
|
||||
utils.Unauthorized(c, "未认证")
|
||||
return
|
||||
}
|
||||
|
||||
var rows []model.UserSetting
|
||||
if err := service.ChatSvc.DB.Where("user_id = ?", userID.(string)).Find(&rows).Error; err != nil {
|
||||
utils.InternalError(c, "查询失败")
|
||||
return
|
||||
}
|
||||
|
||||
result := make(map[string]string, len(rows))
|
||||
for _, row := range rows {
|
||||
result[row.SettingKey] = row.SettingValue
|
||||
}
|
||||
utils.SuccessWithData(c, result, "获取成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* UpdateUserSettingsHandler
|
||||
* 功能:批量更新用户设置
|
||||
*/
|
||||
func UpdateUserSettingsHandler(c *gin.Context) {
|
||||
userID, ok := c.Get("user_id")
|
||||
if !ok {
|
||||
utils.Unauthorized(c, "未认证")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Settings map[string]string `json:"settings" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
uid := userID.(string)
|
||||
now := time.Now()
|
||||
for key, val := range req.Settings {
|
||||
setting := model.UserSetting{
|
||||
UserID: uid,
|
||||
SettingKey: key,
|
||||
SettingValue: val,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
service.ChatSvc.DB.Save(&setting)
|
||||
}
|
||||
|
||||
GetUserSettingsHandler(c)
|
||||
}
|
||||
|
||||
/**
|
||||
* MarkMessagesReadHandler
|
||||
* 功能:批量标记消息已读
|
||||
*/
|
||||
func MarkMessagesReadHandler(c *gin.Context) {
|
||||
userID, ok := c.Get("user_id")
|
||||
if !ok {
|
||||
utils.Unauthorized(c, "未认证")
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
MessageIDs []uint `json:"message_ids" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
utils.BadRequest(c, "参数错误")
|
||||
return
|
||||
}
|
||||
|
||||
uid := userID.(string)
|
||||
now := time.Now()
|
||||
readIDs := make([]uint, 0, len(req.MessageIDs))
|
||||
for _, mid := range req.MessageIDs {
|
||||
receipt := model.MessageReadReceipt{
|
||||
MessageID: mid,
|
||||
UserID: uid,
|
||||
ReadAt: now,
|
||||
}
|
||||
service.ChatSvc.DB.Where("message_id = ? AND user_id = ?", mid, uid).
|
||||
Assign(receipt).FirstOrCreate(&receipt)
|
||||
readIDs = append(readIDs, mid)
|
||||
|
||||
// 通知消息发送者(已读回执)
|
||||
var msg model.ChatMessage
|
||||
if err := service.ChatSvc.DB.First(&msg, mid).Error; err == nil && msg.SenderUserID != "" && msg.SenderUserID != uid {
|
||||
payload := model.WsPayload{
|
||||
RequestType: "messages_read",
|
||||
Data: map[string]interface{}{
|
||||
"message_ids": []uint{mid},
|
||||
"reader_id": uid,
|
||||
"room_id": msg.RoomID,
|
||||
},
|
||||
}
|
||||
if bytes, err := json.Marshal(payload); err == nil {
|
||||
service.ChatSvc.DispatchMessage(msg.SenderUserID, bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
utils.SuccessWithData(c, gin.H{"message_ids": readIDs}, "已标记已读")
|
||||
}
|
||||
@@ -40,6 +40,27 @@ func GetMyInfoHandler(c *gin.Context) {
|
||||
utils.SuccessWithData(c, user, "获取成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* GetUserByIDHandler
|
||||
* 功能:根据用户 ID 获取公开资料
|
||||
* 路径:GET /api/user/:id
|
||||
*/
|
||||
func GetUserByIDHandler(c *gin.Context) {
|
||||
targetID := c.Param("id")
|
||||
if targetID == "" {
|
||||
utils.BadRequest(c, "用户ID不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := service.UserSvc.GetUserByID(targetID)
|
||||
if err != nil {
|
||||
utils.NotFound(c, "用户不存在")
|
||||
return
|
||||
}
|
||||
user.Password = ""
|
||||
utils.SuccessWithData(c, user, "获取成功")
|
||||
}
|
||||
|
||||
/**
|
||||
* GetUserListHandler
|
||||
* 功能:获取用户列表
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
type Client struct {
|
||||
ID string // 客户端唯一标识 (格式: NodeID-Timestamp)
|
||||
UserID string // 绑定的用户ID (未绑定时为空)
|
||||
RemoteIP string // WebSocket 连接来源 IP
|
||||
Conn *websocket.Conn // 底层 WebSocket 连接对象
|
||||
SendQueue chan []byte // 发送缓冲队列 (防止网络阻塞导致写协程卡死)
|
||||
}
|
||||
|
||||
@@ -145,7 +145,7 @@ func (m *RequestLogMiddleware) Handler() gin.HandlerFunc {
|
||||
}
|
||||
|
||||
// 获取请求IP
|
||||
ip := getClientIP(c)
|
||||
ip := utils.GetClientIP(c)
|
||||
|
||||
// 本地IP不记录
|
||||
if utils.GetIPLocation(ip) == "本地" {
|
||||
@@ -253,28 +253,6 @@ func (m *RequestLogMiddleware) logRequest(c *gin.Context, ip, userID string, req
|
||||
m.DB.Create(&log)
|
||||
}
|
||||
|
||||
// getClientIP 获取客户端IP
|
||||
func getClientIP(c *gin.Context) string {
|
||||
// 优先从X-Forwarded-For获取
|
||||
ip := c.GetHeader("X-Forwarded-For")
|
||||
if ip != "" {
|
||||
// X-Forwarded-For可能包含多个IP,取第一个
|
||||
ips := strings.Split(ip, ",")
|
||||
if len(ips) > 0 {
|
||||
return strings.TrimSpace(ips[0])
|
||||
}
|
||||
}
|
||||
|
||||
// 从X-Real-IP获取
|
||||
ip = c.GetHeader("X-Real-IP")
|
||||
if ip != "" {
|
||||
return ip
|
||||
}
|
||||
|
||||
// 从RemoteAddr获取
|
||||
return c.ClientIP()
|
||||
}
|
||||
|
||||
// responseWriter 响应写入器(用于捕获响应内容)
|
||||
type responseWriter struct {
|
||||
gin.ResponseWriter
|
||||
|
||||
@@ -23,6 +23,10 @@ type ChatMessage struct {
|
||||
RoomID string `gorm:"type:varchar(100);index;comment:房间ID" json:"room_id"`
|
||||
// 发送者用户ID
|
||||
SenderUserID string `gorm:"type:varchar(100);index;comment:发送者用户ID" json:"sender_user_id"`
|
||||
// 发送者 IP
|
||||
SenderIP string `gorm:"type:varchar(50);comment:发送者IP" json:"sender_ip"`
|
||||
// IP 归属地
|
||||
IPLocation string `gorm:"type:varchar(255);comment:IP归属地" json:"ip_location"`
|
||||
// 接收者用户ID
|
||||
ReceiverUserID string `gorm:"type:varchar(100);index;comment:接收者用户ID" json:"receiver_user_id"`
|
||||
// 消息类型 ( // 0:文本 1:图片 2:语音 3:视频 8:文件 6:信令)
|
||||
@@ -798,6 +802,36 @@ const (
|
||||
MomentMediaTypeVideo = 2 // 视频
|
||||
)
|
||||
|
||||
// ==========================================
|
||||
// 已读回执与用户设置
|
||||
// ==========================================
|
||||
|
||||
/**
|
||||
* MessageReadReceipt
|
||||
* 作用:消息已读回执表
|
||||
*/
|
||||
type MessageReadReceipt struct {
|
||||
ID uint `json:"id" gorm:"primaryKey"`
|
||||
MessageID uint `json:"message_id" gorm:"index;not null;uniqueIndex:uk_msg_user,priority:1"`
|
||||
UserID string `json:"user_id" gorm:"size:100;index;not null;uniqueIndex:uk_msg_user,priority:2"`
|
||||
ReadAt time.Time `json:"read_at" gorm:"not null"`
|
||||
}
|
||||
|
||||
func (MessageReadReceipt) TableName() string { return "message_read_receipts" }
|
||||
|
||||
/**
|
||||
* UserSetting
|
||||
* 作用:用户个性化设置键值表
|
||||
*/
|
||||
type UserSetting struct {
|
||||
UserID string `json:"user_id" gorm:"size:100;primaryKey"`
|
||||
SettingKey string `json:"setting_key" gorm:"size:50;primaryKey"`
|
||||
SettingValue string `json:"setting_value" gorm:"type:text"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (UserSetting) TableName() string { return "user_settings" }
|
||||
|
||||
// ==========================================
|
||||
// 常量定义
|
||||
// ==========================================
|
||||
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"xk-websocket-v2/internal/manager"
|
||||
"xk-websocket-v2/internal/model"
|
||||
"xk-websocket-v2/internal/turnserver"
|
||||
"xk-websocket-v2/internal/utils"
|
||||
|
||||
"github.com/go-redis/redis/v8"
|
||||
"github.com/spf13/viper"
|
||||
@@ -92,8 +93,9 @@ func (s *ChatService) IsUserOnline(userID string) bool {
|
||||
* 功能:处理用户发来的消息(入口函数)。包含多端同步、URL 抓取、持久化和转发逻辑。
|
||||
* @param senderClient 发送消息的客户端(用于识别来源设备)
|
||||
* @param req 消息请求体
|
||||
* @returns 持久化后的消息(信令/群通知/被拦截时返回 nil)
|
||||
*/
|
||||
func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model.SendMessageReq) {
|
||||
func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model.SendMessageReq) *model.ChatMessage {
|
||||
// 1. [关键] 拦截通话信令:处理多端同步逻辑
|
||||
if req.CallStatus == "accepted" {
|
||||
s.NotifyOtherDevices(senderClient.UserID, senderClient.ID, req.CallID)
|
||||
@@ -128,7 +130,7 @@ func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model
|
||||
// 验证群成员身份
|
||||
memberIDs, mErr := RoomSvc.GetRoomMembers(req.RoomID)
|
||||
if mErr != nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
isMember := false
|
||||
@@ -150,7 +152,7 @@ func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model
|
||||
}
|
||||
errorBytes, _ := json.Marshal(errorMsg)
|
||||
s.DispatchMessage(senderClient.UserID, errorBytes)
|
||||
return
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -160,9 +162,27 @@ func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model
|
||||
receiverUserID = req.RoomID
|
||||
}
|
||||
|
||||
// 私聊:接收方已拉黑发送方则拒绝
|
||||
if !isGroupMessage && req.ReceiverUserID != "" && ContactSvc != nil {
|
||||
if ContactSvc.IsBlocked(req.ReceiverUserID, senderClient.UserID) {
|
||||
errorMsg := model.WsPayload{
|
||||
RequestType: "error",
|
||||
Data: map[string]interface{}{
|
||||
"message": "消息已发出,但被对方拒收",
|
||||
"code": "BLOCKED",
|
||||
},
|
||||
}
|
||||
errorBytes, _ := json.Marshal(errorMsg)
|
||||
s.DispatchMessage(senderClient.UserID, errorBytes)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
msg := model.ChatMessage{
|
||||
RoomID: req.RoomID,
|
||||
SenderUserID: senderClient.UserID,
|
||||
SenderIP: senderClient.RemoteIP,
|
||||
IPLocation: utils.GetIPLocation(senderClient.RemoteIP),
|
||||
ReceiverUserID: receiverUserID,
|
||||
MessageType: req.MessageType,
|
||||
Content: req.Content,
|
||||
@@ -225,11 +245,12 @@ func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model
|
||||
|
||||
// 处理@通知
|
||||
s.handleMentionNotification(senderClient.UserID, req.RoomID, extraData, msg)
|
||||
return
|
||||
return &msg
|
||||
}
|
||||
}
|
||||
}
|
||||
s.DispatchMessage(req.ReceiverUserID, msgBytes)
|
||||
return &msg
|
||||
|
||||
} else {
|
||||
// ==========================================
|
||||
@@ -275,6 +296,7 @@ func (s *ChatService) HandleUserMessage(senderClient *manager.Client, req *model
|
||||
log.Printf("📡 [WebRTC] 定向信令: To=%s Action=%s", req.ReceiverUserID, req.CallStatus)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// 辅助:正则提取第一个 URL
|
||||
@@ -381,6 +403,39 @@ func (s *ChatService) NotifyOtherDevices(userID, currentClientID, callID string)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* BroadcastChatMessage
|
||||
* 功能:将聊天消息推送给房间成员或私聊对方(含发送者其他设备)
|
||||
*/
|
||||
func (s *ChatService) BroadcastChatMessage(msg model.ChatMessage) {
|
||||
pushMsg := model.WsPayload{
|
||||
RequestType: "receive_message",
|
||||
Data: msg,
|
||||
}
|
||||
msgBytes, _ := json.Marshal(pushMsg)
|
||||
|
||||
if msg.RoomID != "" {
|
||||
room, err := RoomSvc.GetRoom(msg.RoomID)
|
||||
if err == nil && room.RoomType == "group" {
|
||||
memberIDs, mErr := RoomSvc.GetRoomMembers(msg.RoomID)
|
||||
if mErr == nil {
|
||||
for _, uid := range memberIDs {
|
||||
if uid != msg.SenderUserID {
|
||||
s.DispatchMessage(uid, msgBytes)
|
||||
}
|
||||
}
|
||||
s.DispatchMessage(msg.SenderUserID, msgBytes)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if msg.ReceiverUserID != "" {
|
||||
s.DispatchMessage(msg.ReceiverUserID, msgBytes)
|
||||
}
|
||||
s.DispatchMessage(msg.SenderUserID, msgBytes)
|
||||
}
|
||||
|
||||
/**
|
||||
* DispatchMessage
|
||||
* 功能:将消息路由到目标用户(无论其在哪个节点)。
|
||||
|
||||
@@ -524,8 +524,54 @@ func (s *ContactService) GetUserDetailWithFriendStatus(currentUserID, targetUser
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"is_friend": isFriend,
|
||||
"contact": contact,
|
||||
"user": user,
|
||||
"is_friend": isFriend,
|
||||
"contact": contact,
|
||||
"user": user,
|
||||
"common_groups": s.GetCommonGroups(currentUserID, targetUserID),
|
||||
}, nil
|
||||
}
|
||||
|
||||
/**
|
||||
* GetCommonGroups
|
||||
* 功能:获取两人共同加入的群聊列表
|
||||
*/
|
||||
func (s *ContactService) GetCommonGroups(userID, contactID string) []model.ChatRoom {
|
||||
var myRooms []model.RoomMember
|
||||
if err := s.DB.Where("user_id = ?", userID).Find(&myRooms).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
roomSet := make(map[string]bool, len(myRooms))
|
||||
for _, m := range myRooms {
|
||||
roomSet[m.RoomID] = true
|
||||
}
|
||||
|
||||
var theirRooms []model.RoomMember
|
||||
if err := s.DB.Where("user_id = ?", contactID).Find(&theirRooms).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
commonIDs := make([]string, 0)
|
||||
for _, m := range theirRooms {
|
||||
if roomSet[m.RoomID] {
|
||||
commonIDs = append(commonIDs, m.RoomID)
|
||||
}
|
||||
}
|
||||
if len(commonIDs) == 0 {
|
||||
return []model.ChatRoom{}
|
||||
}
|
||||
|
||||
var rooms []model.ChatRoom
|
||||
_ = s.DB.Where("room_id IN ? AND room_type = ?", commonIDs, "group").Find(&rooms).Error
|
||||
return rooms
|
||||
}
|
||||
|
||||
/**
|
||||
* IsBlocked
|
||||
* 功能:检查 owner 是否拉黑了 target(owner 对 target 设置了 is_blocked)
|
||||
*/
|
||||
func (s *ContactService) IsBlocked(ownerID, targetID string) bool {
|
||||
var contact model.UserContact
|
||||
err := s.DB.Where("user_id = ? AND contact_id = ? AND is_blocked = ?", ownerID, targetID, true).
|
||||
First(&contact).Error
|
||||
return err == nil
|
||||
}
|
||||
|
||||
28
internal/utils/get_client_ip.go
Normal file
28
internal/utils/get_client_ip.go
Normal file
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* package utils
|
||||
* 作用:从 HTTP 请求中提取客户端真实 IP
|
||||
*/
|
||||
package utils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
/**
|
||||
* GetClientIP
|
||||
* 功能:优先从代理头获取,否则使用 Gin ClientIP
|
||||
*/
|
||||
func GetClientIP(c *gin.Context) string {
|
||||
ip := c.GetHeader("X-Forwarded-For")
|
||||
if ip != "" {
|
||||
parts := strings.Split(ip, ",")
|
||||
return strings.TrimSpace(parts[0])
|
||||
}
|
||||
ip = c.GetHeader("X-Real-IP")
|
||||
if ip != "" {
|
||||
return ip
|
||||
}
|
||||
return c.ClientIP()
|
||||
}
|
||||
@@ -1,163 +1,102 @@
|
||||
/**
|
||||
* package utils
|
||||
* 作用:IP归属地查询工具
|
||||
* 说明:使用第三方API查询IP归属地信息
|
||||
* 作用:IP 归属地查询(基于本地 ip2region.xdb 离线库)
|
||||
*/
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
"sync"
|
||||
|
||||
"github.com/lionsoul2014/ip2region/binding/golang/xdb"
|
||||
)
|
||||
|
||||
// IPLocationInfo IP归属地信息
|
||||
type IPLocationInfo struct {
|
||||
Country string `json:"country"` // 国家
|
||||
Region string `json:"region"` // 省份
|
||||
City string `json:"city"` // 城市
|
||||
ISP string `json:"isp"` // 运营商
|
||||
FullLocation string `json:"full_location"` // 完整归属地
|
||||
}
|
||||
|
||||
// 缓存结构(简单内存缓存)
|
||||
var (
|
||||
ipCache = make(map[string]*IPLocationInfo)
|
||||
cacheTimeout = 24 * time.Hour
|
||||
ipSearcher *xdb.Searcher
|
||||
ipSearcherOnce sync.Once
|
||||
ipSearcherErr error
|
||||
ipCache sync.Map
|
||||
)
|
||||
|
||||
/**
|
||||
* InitIP2Region
|
||||
* 功能:加载 ip2region.xdb,服务启动时调用一次
|
||||
*/
|
||||
func InitIP2Region(dbPath string) error {
|
||||
ipSearcherOnce.Do(func() {
|
||||
ipSearcher, ipSearcherErr = xdb.NewWithFileOnly(xdb.IPv4, dbPath)
|
||||
if ipSearcherErr != nil {
|
||||
log.Printf("❌ [IP2Region] 加载失败: %v", ipSearcherErr)
|
||||
return
|
||||
}
|
||||
log.Printf("✅ [IP2Region] 已加载: %s", dbPath)
|
||||
})
|
||||
return ipSearcherErr
|
||||
}
|
||||
|
||||
/**
|
||||
* GetIPLocation
|
||||
* 功能:获取IP归属地信息
|
||||
* @param ip IP地址
|
||||
* @returns 归属地信息字符串
|
||||
* 功能:查询 IP 归属地,内网/本地返回「本地」
|
||||
*/
|
||||
func GetIPLocation(ip string) string {
|
||||
// 排除本地IP
|
||||
if isLocalIP(ip) {
|
||||
return "本地"
|
||||
}
|
||||
|
||||
// 检查缓存
|
||||
if info, ok := ipCache[ip]; ok {
|
||||
return info.FullLocation
|
||||
if ip == "" {
|
||||
return "未知"
|
||||
}
|
||||
|
||||
// 查询IP归属地
|
||||
info := queryIPLocation(ip)
|
||||
if info != nil {
|
||||
// 构建完整归属地字符串
|
||||
parts := []string{}
|
||||
if info.Country != "" {
|
||||
parts = append(parts, info.Country)
|
||||
}
|
||||
if info.Region != "" {
|
||||
parts = append(parts, info.Region)
|
||||
}
|
||||
if info.City != "" {
|
||||
parts = append(parts, info.City)
|
||||
}
|
||||
if info.ISP != "" {
|
||||
parts = append(parts, info.ISP)
|
||||
}
|
||||
|
||||
if len(parts) > 0 {
|
||||
info.FullLocation = strings.Join(parts, " ")
|
||||
} else {
|
||||
info.FullLocation = "未知"
|
||||
}
|
||||
|
||||
// 存入缓存
|
||||
ipCache[ip] = info
|
||||
return info.FullLocation
|
||||
if cached, ok := ipCache.Load(ip); ok {
|
||||
return cached.(string)
|
||||
}
|
||||
|
||||
return "未知"
|
||||
if ipSearcher == nil {
|
||||
return "未知"
|
||||
}
|
||||
|
||||
region, err := ipSearcher.Search(ip)
|
||||
if err != nil || region == "" {
|
||||
return "未知"
|
||||
}
|
||||
|
||||
// 格式: 国家|区域|省份|城市|ISP
|
||||
parts := strings.Split(region, "|")
|
||||
labels := []string{}
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" && p != "0" {
|
||||
labels = append(labels, p)
|
||||
}
|
||||
}
|
||||
result := "未知"
|
||||
if len(labels) > 0 {
|
||||
result = strings.Join(labels, " ")
|
||||
}
|
||||
ipCache.Store(ip, result)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* isLocalIP
|
||||
* 功能:判断是否为本地IP
|
||||
* 功能:判断是否为本地/内网 IP
|
||||
*/
|
||||
func isLocalIP(ip string) bool {
|
||||
// 本地回环地址
|
||||
if ip == "127.0.0.1" || ip == "localhost" || ip == "::1" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 内网地址
|
||||
if strings.HasPrefix(ip, "192.168.") ||
|
||||
strings.HasPrefix(ip, "10.") ||
|
||||
strings.HasPrefix(ip, "172.16.") ||
|
||||
strings.HasPrefix(ip, "172.17.") ||
|
||||
strings.HasPrefix(ip, "172.18.") ||
|
||||
strings.HasPrefix(ip, "172.19.") ||
|
||||
strings.HasPrefix(ip, "172.20.") ||
|
||||
strings.HasPrefix(ip, "172.21.") ||
|
||||
strings.HasPrefix(ip, "172.22.") ||
|
||||
strings.HasPrefix(ip, "172.23.") ||
|
||||
strings.HasPrefix(ip, "172.24.") ||
|
||||
strings.HasPrefix(ip, "172.25.") ||
|
||||
strings.HasPrefix(ip, "172.26.") ||
|
||||
strings.HasPrefix(ip, "172.27.") ||
|
||||
strings.HasPrefix(ip, "172.28.") ||
|
||||
strings.HasPrefix(ip, "172.29.") ||
|
||||
strings.HasPrefix(ip, "172.30.") ||
|
||||
strings.HasPrefix(ip, "172.31.") {
|
||||
if strings.HasPrefix(ip, "192.168.") || strings.HasPrefix(ip, "10.") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(ip, "172.16.") || strings.HasPrefix(ip, "172.17.") ||
|
||||
strings.HasPrefix(ip, "172.18.") || strings.HasPrefix(ip, "172.19.") ||
|
||||
strings.HasPrefix(ip, "172.20.") || strings.HasPrefix(ip, "172.21.") ||
|
||||
strings.HasPrefix(ip, "172.22.") || strings.HasPrefix(ip, "172.23.") ||
|
||||
strings.HasPrefix(ip, "172.24.") || strings.HasPrefix(ip, "172.25.") ||
|
||||
strings.HasPrefix(ip, "172.26.") || strings.HasPrefix(ip, "172.27.") ||
|
||||
strings.HasPrefix(ip, "172.28.") || strings.HasPrefix(ip, "172.29.") ||
|
||||
strings.HasPrefix(ip, "172.30.") || strings.HasPrefix(ip, "172.31.") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* queryIPLocation
|
||||
* 功能:查询IP归属地(使用ip-api.com免费API)
|
||||
*/
|
||||
func queryIPLocation(ip string) *IPLocationInfo {
|
||||
// 使用ip-api.com免费API(限制:每分钟45次请求)
|
||||
url := fmt.Sprintf("http://ip-api.com/json/%s?lang=zh-CN&fields=status,message,country,regionName,city,isp", ip)
|
||||
|
||||
client := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
resp, err := client.Get(url)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var result struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Country string `json:"country"`
|
||||
Region string `json:"regionName"`
|
||||
City string `json:"city"`
|
||||
ISP string `json:"isp"`
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if result.Status != "success" {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &IPLocationInfo{
|
||||
Country: result.Country,
|
||||
Region: result.Region,
|
||||
City: result.City,
|
||||
ISP: result.ISP,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user