This commit is contained in:
2025-12-03 11:00:47 +08:00
parent 29fb7cc9ac
commit a6ea6a2869
30 changed files with 4695 additions and 75 deletions

View File

@@ -0,0 +1,163 @@
/**
* package utils
* 作用IP归属地查询工具
* 说明使用第三方API查询IP归属地信息
*/
package utils
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
)
// 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
)
/**
* GetIPLocation
* 功能获取IP归属地信息
* @param ip IP地址
* @returns 归属地信息字符串
*/
func GetIPLocation(ip string) string {
// 排除本地IP
if isLocalIP(ip) {
return "本地"
}
// 检查缓存
if info, ok := ipCache[ip]; ok {
return info.FullLocation
}
// 查询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
}
return "未知"
}
/**
* isLocalIP
* 功能判断是否为本地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.") {
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,
}
}

166
internal/utils/jwt.go Normal file
View File

@@ -0,0 +1,166 @@
/**
* package utils
*
* JWT Token生成和验证工具包
*
* 功能概述:
* 1. 生成JWT Token包含用户ID和过期时间
* 2. 解析JWT Token验证签名和过期时间
* 3. 验证Token有效性提取用户ID
*
* 使用场景:
* - 用户登录后生成Token
* - API请求时验证Token
* - 从Token中提取用户信息
*/
package utils
import (
"errors"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/spf13/viper"
)
// jwtSecret JWT签名密钥从配置文件读取
var jwtSecret []byte
/**
* init
*
* 功能初始化JWT签名密钥
*
* 步骤:
* 1. 从配置文件读取JWT密钥
* 2. 如果配置文件中没有,使用默认密钥(仅用于开发环境)
* 3. 将密钥转换为字节数组存储
*
* 注意:生产环境必须使用配置文件中的强随机密钥
*/
func init() {
secret := viper.GetString("jwt.secret")
if secret == "" {
secret = "xk-websocket-secret-key-2025" // 默认密钥,生产环境应使用配置
}
jwtSecret = []byte(secret)
}
/**
* Claims
*
* JWT Token的载荷结构
*
* 字段说明:
* - UserID: 用户ID业务数据
* - RegisteredClaims: JWT标准声明过期时间、签发时间等
*/
type Claims struct {
UserID string `json:"user_id"` // 用户ID
jwt.RegisteredClaims // JWT标准声明
}
/**
* GenerateToken
*
* 功能生成JWT Token
*
* 步骤:
* 1. 设置Token过期时间默认7天
* 2. 创建Claims对象包含用户ID和标准声明
* 3. 使用HS256算法签名Token
* 4. 返回Token字符串
*
* @param userID 用户ID
* @returns token字符串和错误
*/
func GenerateToken(userID string) (string, error) {
// 步骤1: 设置Token过期时间7天后过期
expirationTime := time.Now().Add(7 * 24 * time.Hour)
// 步骤2: 创建Claims对象包含用户ID和标准声明
claims := &Claims{
UserID: userID, // 业务数据用户ID
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime), // 过期时间
IssuedAt: jwt.NewNumericDate(time.Now()), // 签发时间
NotBefore: jwt.NewNumericDate(time.Now()), // 生效时间(立即生效)
},
}
// 步骤3: 使用HS256算法创建Token并签名
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtSecret)
if err != nil {
return "", err
}
// 步骤4: 返回Token字符串
return tokenString, nil
}
/**
* ParseToken
*
* 功能解析JWT Token
*
* 步骤:
* 1. 创建空的Claims对象
* 2. 使用密钥解析Token并验证签名
* 3. 检查Token是否有效签名正确、未过期
* 4. 返回Claims对象
*
* @param tokenString token字符串
* @returns Claims和错误
*/
func ParseToken(tokenString string) (*Claims, error) {
// 步骤1: 创建空的Claims对象
claims := &Claims{}
// 步骤2: 解析Token并验证签名
// 使用密钥验证Token的签名是否有效
token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
return jwtSecret, nil // 返回签名密钥
})
if err != nil {
return nil, err // 解析失败(签名错误、格式错误等)
}
// 步骤3: 检查Token是否有效签名正确、未过期
if !token.Valid {
return nil, errors.New("invalid token")
}
// 步骤4: 返回解析后的Claims对象
return claims, nil
}
/**
* ValidateToken
*
* 功能验证Token有效性并提取用户ID
*
* 步骤:
* 1. 调用ParseToken解析Token
* 2. 如果解析成功从Claims中提取用户ID
* 3. 返回用户ID
*
* 使用场景:
* - 中间件中验证Token
* - API处理器中获取当前用户ID
*
* @param tokenString token字符串
* @returns 用户ID和错误
*/
func ValidateToken(tokenString string) (string, error) {
// 步骤1: 解析Token
claims, err := ParseToken(tokenString)
if err != nil {
return "", err // Token无效或已过期
}
// 步骤2: 从Claims中提取用户ID
return claims.UserID, nil
}

View File

@@ -0,0 +1,82 @@
/**
* package utils
*
* 密码加密和验证工具包
*
* 功能概述:
* 1. 使用bcrypt算法加密密码单向哈希不可逆
* 2. 验证明文密码与加密密码是否匹配
*
* 安全特性:
* - 使用bcrypt算法自动加盐
* - 计算成本可调,防止暴力破解
* - 相同密码每次加密结果不同(因为盐值随机)
*/
package utils
import "golang.org/x/crypto/bcrypt"
/**
* HashPassword
*
* 功能使用bcrypt算法加密密码
*
* 步骤:
* 1. 将明文密码转换为字节数组
* 2. 使用bcrypt算法生成哈希值自动加盐
* 3. 将哈希值转换为字符串返回
*
* 特点:
* - 每次加密结果不同(因为盐值随机)
* - 使用默认计算成本10轮
* - 单向加密,不可逆
*
* 使用场景:
* - 用户注册时加密密码
* - 用户修改密码时加密新密码
*
* @param password 明文密码
* @returns 加密后的密码哈希字符串和错误
*/
func HashPassword(password string) (string, error) {
// 步骤1-2: 使用bcrypt算法生成密码哈希
// bcrypt.DefaultCost = 10表示进行2^10=1024轮哈希计算
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
// 步骤3: 将哈希值转换为字符串返回
return string(bytes), nil
}
/**
* CheckPassword
*
* 功能:验证明文密码与加密密码是否匹配
*
* 步骤:
* 1. 从加密密码中提取盐值
* 2. 使用相同的盐值对明文密码进行哈希
* 3. 比较两个哈希值是否相同
*
* 特点:
* - 即使密码相同,每次加密的哈希值也不同
* - 但可以通过CompareHashAndPassword正确验证
* - 验证过程是安全的,不会泄露密码信息
*
* 使用场景:
* - 用户登录时验证密码
* - 修改密码时验证旧密码
*
* @param password 明文密码
* @param hash 加密后的密码哈希
* @returns 是否匹配true=匹配false=不匹配)
*/
func CheckPassword(password, hash string) bool {
// 步骤1-3: 比较明文密码的哈希值与存储的哈希值
// 如果匹配err为nil返回true否则返回false
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}

172
internal/utils/response.go Normal file
View File

@@ -0,0 +1,172 @@
/**
* package utils
* 作用提供统一的API响应工具函数
* 说明所有响应统一返回HTTP 200状态码错误通过code字段标识
*/
package utils
import (
"net/http"
"strconv"
"time"
"xk-websocket-v2/internal/model"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
)
// 业务状态码常量
const (
CodeSuccess = 0 // 成功
CodeBadRequest = 400 // 参数错误
CodeUnauthorized = 401 // 未认证
CodeForbidden = 403 // 无权限
CodeNotFound = 404 // 资源不存在
CodeInternalError = 500 // 服务器错误
)
// 响应类型常量
const (
TypeSuccess = "success"
TypeError = "error"
)
/**
* getECS
* 作用:获取服务器标识
*/
func getECS() string {
ecs := viper.GetString("server.ecs")
if ecs == "" {
return "localhost"
}
return ecs
}
/**
* formatDuration
* 作用:格式化响应时间为 "XX ms" 格式
*/
func formatDuration(d time.Duration) string {
ms := d.Milliseconds()
return strconv.FormatInt(ms, 10) + " ms"
}
/**
* Response
* 作用:统一的响应函数,所有响应都通过此函数返回
* 说明统一返回HTTP 200状态码
*/
func Response(c *gin.Context, code int, message string, result interface{}, responseType string) {
// 从Context获取请求开始时间
startTime, exists := c.Get("request_start_time")
var duration time.Duration
if exists {
duration = time.Since(startTime.(time.Time))
} else {
duration = 0
}
response := model.ApiResponse{
Code: code,
Message: message,
Result: result,
Type: responseType,
InterfaceInfo: model.InterfaceInfo{
ResultTime: formatDuration(duration),
Ecs: getECS(),
},
}
// 统一返回HTTP 200状态码
c.JSON(http.StatusOK, response)
}
/**
* Success
* 作用:成功响应(无数据)
*/
func Success(c *gin.Context, message string) {
if message == "" {
message = "操作成功"
}
Response(c, CodeSuccess, message, nil, TypeSuccess)
}
/**
* SuccessWithData
* 作用:成功响应(带数据)
*/
func SuccessWithData(c *gin.Context, data interface{}, message string) {
if message == "" {
message = "获取成功"
}
Response(c, CodeSuccess, message, data, TypeSuccess)
}
/**
* Error
* 作用:错误响应(自定义状态码和消息)
*/
func Error(c *gin.Context, code int, message string) {
if message == "" {
message = "操作失败"
}
Response(c, code, message, nil, TypeError)
}
/**
* BadRequest
* 作用参数错误响应code=400
*/
func BadRequest(c *gin.Context, message string) {
if message == "" {
message = "参数错误"
}
Response(c, CodeBadRequest, message, nil, TypeError)
}
/**
* Unauthorized
* 作用未认证响应code=401
*/
func Unauthorized(c *gin.Context, message string) {
if message == "" {
message = "未认证"
}
Response(c, CodeUnauthorized, message, nil, TypeError)
}
/**
* Forbidden
* 作用无权限响应code=403
*/
func Forbidden(c *gin.Context, message string) {
if message == "" {
message = "无权限"
}
Response(c, CodeForbidden, message, nil, TypeError)
}
/**
* NotFound
* 作用资源不存在响应code=404
*/
func NotFound(c *gin.Context, message string) {
if message == "" {
message = "资源不存在"
}
Response(c, CodeNotFound, message, nil, TypeError)
}
/**
* InternalError
* 作用服务器错误响应code=500
*/
func InternalError(c *gin.Context, message string) {
if message == "" {
message = "服务器错误"
}
Response(c, CodeInternalError, message, nil, TypeError)
}

182
internal/utils/snowflake.go Normal file
View File

@@ -0,0 +1,182 @@
/**
* package utils
* 作用雪花ID生成器生成全局唯一的ID
* 说明使用Twitter的雪花算法生成64位整数ID
*/
package utils
import (
"errors"
"sync"
"time"
)
const (
// 时间戳占用位数41位可以使用69年
timestampBits = 41
// 数据中心ID占用位数5位最多32个数据中心
datacenterIDBits = 5
// 机器ID占用位数5位每个数据中心最多32台机器
machineIDBits = 5
// 序列号占用位数12位每毫秒最多4096个ID
sequenceBits = 12
// 最大值
maxDatacenterID = -1 ^ (-1 << datacenterIDBits)
maxMachineID = -1 ^ (-1 << machineIDBits)
maxSequence = -1 ^ (-1 << sequenceBits)
// 位移
machineIDShift = sequenceBits
datacenterIDShift = sequenceBits + machineIDBits
timestampShift = sequenceBits + machineIDBits + datacenterIDBits
// 起始时间戳2024-01-01 00:00:00
epoch int64 = 1704067200000
)
// Snowflake 雪花ID生成器
type Snowflake struct {
mutex sync.Mutex
datacenterID int64
machineID int64
sequence int64
lastStamp int64
}
var (
// 全局雪花ID生成器实例
globalSnowflake *Snowflake
once sync.Once
)
/**
* InitSnowflake
* 功能初始化全局雪花ID生成器
* @param datacenterID 数据中心ID0-31
* @param machineID 机器ID0-31
*/
func InitSnowflake(datacenterID, machineID int64) error {
if datacenterID < 0 || datacenterID > maxDatacenterID {
return errors.New("datacenter ID must be between 0 and 31")
}
if machineID < 0 || machineID > maxMachineID {
return errors.New("machine ID must be between 0 and 31")
}
once.Do(func() {
globalSnowflake = &Snowflake{
datacenterID: datacenterID,
machineID: machineID,
sequence: 0,
lastStamp: -1,
}
})
return nil
}
/**
* NextID
* 功能生成下一个ID
* @returns 64位整数ID
*/
func NextID() (int64, error) {
if globalSnowflake == nil {
// 默认使用datacenterID=1, machineID=1
if err := InitSnowflake(1, 1); err != nil {
return 0, err
}
}
return globalSnowflake.nextID()
}
/**
* nextID
* 功能生成下一个ID内部方法
*/
func (s *Snowflake) nextID() (int64, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
now := time.Now().UnixMilli()
// 如果当前时间小于上次时间,说明时钟回拨
if now < s.lastStamp {
return 0, errors.New("clock moved backwards")
}
// 如果是同一毫秒内
if now == s.lastStamp {
s.sequence = (s.sequence + 1) & maxSequence
// 序列号溢出,等待下一毫秒
if s.sequence == 0 {
now = s.waitNextMillis(s.lastStamp)
}
} else {
// 新的毫秒,序列号重置
s.sequence = 0
}
s.lastStamp = now
// 生成ID
id := ((now - epoch) << timestampShift) |
(s.datacenterID << datacenterIDShift) |
(s.machineID << machineIDShift) |
s.sequence
return id, nil
}
/**
* waitNextMillis
* 功能:等待下一毫秒
*/
func (s *Snowflake) waitNextMillis(lastStamp int64) int64 {
now := time.Now().UnixMilli()
for now <= lastStamp {
now = time.Now().UnixMilli()
}
return now
}
/**
* NextIDString
* 功能生成下一个ID字符串格式
*/
func NextIDString() (string, error) {
id, err := NextID()
if err != nil {
return "", err
}
return int64ToString(id), nil
}
/**
* int64ToString
* 功能将int64转换为字符串
*/
func int64ToString(id int64) string {
if id == 0 {
return "0"
}
negative := id < 0
if negative {
id = -id
}
var result []byte
for id > 0 {
result = append([]byte{byte('0' + id%10)}, result...)
id /= 10
}
if negative {
result = append([]byte{'-'}, result...)
}
return string(result)
}