修复了一些问题

This commit is contained in:
李琦
2026-07-08 08:18:58 +08:00
parent bb7891706b
commit 3417856607
19 changed files with 615 additions and 778 deletions

View File

@@ -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, // 仅开发环境,生产环境应移除
}, "验证码已发送")
}

View File

@@ -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())

View File

@@ -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 接口
// ==========================================

View File

@@ -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, "获取成功")
}

View 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}, "已标记已读")
}

View File

@@ -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
* 功能:获取用户列表

View File

@@ -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 // 发送缓冲队列 (防止网络阻塞导致写协程卡死)
}

View File

@@ -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

View File

@@ -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" }
// ==========================================
// 常量定义
// ==========================================

View File

@@ -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
* 功能:将消息路由到目标用户(无论其在哪个节点)。

View File

@@ -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 是否拉黑了 targetowner 对 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
}

View 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()
}

View File

@@ -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,
}
}