New warehouse

This commit is contained in:
2025-10-24 00:09:45 +08:00
commit ac07e27908
75 changed files with 26814 additions and 0 deletions

80
utils/common.go Normal file
View File

@@ -0,0 +1,80 @@
package utils
import (
"encoding/json"
"net/http"
"networkDev/web"
"strings"
)
// JsonResponse 通用JSON响应函数
// 将 success 转换为 codetrue -> 0, false -> 1并输出 data
func JsonResponse(w http.ResponseWriter, status int, success bool, message string, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
// 将success转换为code格式true -> 0, false -> 1
code := 1
if success {
code = 0
}
json.NewEncoder(w).Encode(map[string]interface{}{
"code": code,
"msg": message,
"data": data,
})
}
// RenderTemplate 通用模板渲染函数
// templateName: 模板文件名
// data: 模板数据
// w: HTTP响应写入器
func RenderTemplate(w http.ResponseWriter, templateName string, data map[string]interface{}) error {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl, err := web.ParseTemplates()
if err != nil {
http.Error(w, "模板解析失败", http.StatusInternalServerError)
return err
}
if err := tmpl.ExecuteTemplate(w, templateName, data); err != nil {
http.Error(w, "模板渲染失败", http.StatusInternalServerError)
return err
}
return nil
}
// GetDefaultTemplateData 获取默认模板数据
// 返回包含系统基础信息的数据映射
func GetDefaultTemplateData() map[string]interface{} {
return map[string]interface{}{
"SystemName": "网络验证系统",
"FooterText": "© 2025 凌动技术 保留所有权利",
}
}
// GetClientIP 获取客户端IP地址
// 优先从 X-Forwarded-For 和 X-Real-IP 头部获取,否则使用 RemoteAddr
func GetClientIP(r *http.Request) string {
// 检查 X-Forwarded-For 头部
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
// X-Forwarded-For 可能包含多个IP取第一个
if idx := strings.Index(xff, ","); idx != -1 {
return strings.TrimSpace(xff[:idx])
}
return strings.TrimSpace(xff)
}
// 检查 X-Real-IP 头部
if xri := r.Header.Get("X-Real-IP"); xri != "" {
return strings.TrimSpace(xri)
}
// 使用 RemoteAddr
if idx := strings.LastIndex(r.RemoteAddr, ":"); idx != -1 {
return r.RemoteAddr[:idx]
}
return r.RemoteAddr
}

323
utils/crypto.go Normal file
View File

@@ -0,0 +1,323 @@
package utils
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"io"
"sync"
"github.com/spf13/viper"
"golang.org/x/crypto/bcrypt"
)
// CryptoManager 加密管理器,提供高性能的加密解密服务
type CryptoManager struct {
key []byte
gcm cipher.AEAD
mutex sync.RWMutex
inited bool
}
// 全局加密管理器实例
var cryptoManager = &CryptoManager{}
// initCrypto 初始化加密管理器
// 缓存密钥和GCM实例避免重复创建
func (cm *CryptoManager) initCrypto() error {
cm.mutex.Lock()
defer cm.mutex.Unlock()
if cm.inited {
return nil
}
// 从配置中获取密钥
secret := viper.GetString("encryption_key")
if secret == "" {
secret = "default-secret"
}
// 生成AES密钥
sum := sha256.Sum256([]byte(secret))
cm.key = sum[:]
// 创建AES cipher
block, err := aes.NewCipher(cm.key)
if err != nil {
return err
}
// 创建GCM
gcm, err := cipher.NewGCM(block)
if err != nil {
return err
}
cm.gcm = gcm
cm.inited = true
return nil
}
// EncryptString 字符串加密AES-256-GCM
// 使用缓存的密钥和GCM实例提高性能
func EncryptString(plain string) (string, error) {
if err := cryptoManager.initCrypto(); err != nil {
return "", err
}
cryptoManager.mutex.RLock()
gcm := cryptoManager.gcm
cryptoManager.mutex.RUnlock()
// 生成随机nonce
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
// 加密
ciphertext := gcm.Seal(nil, nonce, []byte(plain), nil)
buf := append(nonce, ciphertext...)
return base64.StdEncoding.EncodeToString(buf), nil
}
// DecryptString 字符串解密AES-256-GCM
// 使用缓存的密钥和GCM实例提高性能
func DecryptString(enc string) (string, error) {
if err := cryptoManager.initCrypto(); err != nil {
return "", err
}
cryptoManager.mutex.RLock()
gcm := cryptoManager.gcm
cryptoManager.mutex.RUnlock()
// 解码base64
data, err := base64.StdEncoding.DecodeString(enc)
if err != nil {
return "", err
}
// 检查数据长度
if len(data) < gcm.NonceSize() {
return "", errors.New("ciphertext too short")
}
// 分离nonce和密文
nonce := data[:gcm.NonceSize()]
ciphertext := data[gcm.NonceSize():]
// 解密
plain, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", err
}
return string(plain), nil
}
// ResetCrypto 重置加密管理器(用于配置更新后重新初始化)
func ResetCrypto() {
cryptoManager.mutex.Lock()
defer cryptoManager.mutex.Unlock()
cryptoManager.inited = false
cryptoManager.key = nil
cryptoManager.gcm = nil
}
// EncryptStringBatch 批量加密字符串
// 减少锁竞争,提高批量处理性能
func EncryptStringBatch(plains []string) ([]string, error) {
if err := cryptoManager.initCrypto(); err != nil {
return nil, err
}
cryptoManager.mutex.RLock()
gcm := cryptoManager.gcm
cryptoManager.mutex.RUnlock()
results := make([]string, len(plains))
for i, plain := range plains {
// 生成随机nonce
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return nil, err
}
// 加密
ciphertext := gcm.Seal(nil, nonce, []byte(plain), nil)
buf := append(nonce, ciphertext...)
results[i] = base64.StdEncoding.EncodeToString(buf)
}
return results, nil
}
// DecryptStringBatch 批量解密字符串
// 减少锁竞争,提高批量处理性能
func DecryptStringBatch(encs []string) ([]string, error) {
if err := cryptoManager.initCrypto(); err != nil {
return nil, err
}
cryptoManager.mutex.RLock()
gcm := cryptoManager.gcm
cryptoManager.mutex.RUnlock()
results := make([]string, len(encs))
for i, enc := range encs {
// 解码base64
data, err := base64.StdEncoding.DecodeString(enc)
if err != nil {
return nil, err
}
// 检查数据长度
if len(data) < gcm.NonceSize() {
return nil, errors.New("ciphertext too short")
}
// 分离nonce和密文
nonce := data[:gcm.NonceSize()]
ciphertext := data[gcm.NonceSize():]
// 解密
plain, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return nil, err
}
results[i] = string(plain)
}
return results, nil
}
// GenerateRandomSalt 生成随机密码盐值
// 生成32字节64个十六进制字符的随机盐值用于加密
// 返回: 十六进制格式的盐值字符串和错误信息
func GenerateRandomSalt() (string, error) {
length := 32 // 固定32字节
// 生成随机字节
bytes := make([]byte, length)
if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
return "", err
}
// 转换为十六进制字符串
return fmt.Sprintf("%x", bytes), nil
}
// EncryptStringWithSalt 使用盐值进行字符串加密AES-256-GCM
// 将明文和盐值组合后进行加密,增强安全性
// plain: 待加密的明文字符串
// salt: 加密盐值
// 返回: base64编码的密文字符串和错误信息
func EncryptStringWithSalt(plain, salt string) (string, error) {
if err := cryptoManager.initCrypto(); err != nil {
return "", err
}
cryptoManager.mutex.RLock()
gcm := cryptoManager.gcm
cryptoManager.mutex.RUnlock()
// 将明文和盐值组合
combined := plain + salt
// 生成随机nonce
nonce := make([]byte, gcm.NonceSize())
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
// 加密
ciphertext := gcm.Seal(nil, nonce, []byte(combined), nil)
buf := append(nonce, ciphertext...)
return base64.StdEncoding.EncodeToString(buf), nil
}
// DecryptStringWithSalt 使用盐值进行字符串解密AES-256-GCM
// 解密密文并移除盐值,返回原始明文
// enc: base64编码的密文字符串
// salt: 解密盐值
// 返回: 解密后的明文字符串和错误信息
func DecryptStringWithSalt(enc, salt string) (string, error) {
if err := cryptoManager.initCrypto(); err != nil {
return "", err
}
cryptoManager.mutex.RLock()
gcm := cryptoManager.gcm
cryptoManager.mutex.RUnlock()
// 解码base64
data, err := base64.StdEncoding.DecodeString(enc)
if err != nil {
return "", err
}
// 检查数据长度
if len(data) < gcm.NonceSize() {
return "", errors.New("ciphertext too short")
}
// 分离nonce和密文
nonce := data[:gcm.NonceSize()]
ciphertext := data[gcm.NonceSize():]
// 解密
plain, err := gcm.Open(nil, nonce, ciphertext, nil)
if err != nil {
return "", err
}
// 移除盐值,返回原始明文
combined := string(plain)
if len(combined) < len(salt) {
return "", errors.New("decrypted data too short")
}
// 验证盐值是否匹配
if combined[len(combined)-len(salt):] != salt {
return "", errors.New("salt mismatch")
}
return combined[:len(combined)-len(salt)], nil
}
// HashPasswordWithSalt 使用盐值对密码进行哈希处理
// 将密码和盐值组合后使用bcrypt进行哈希
// password: 原始密码
// salt: 密码盐值
// 返回: bcrypt哈希值和错误信息
func HashPasswordWithSalt(password, salt string) (string, error) {
// 将密码和盐值组合
combined := password + salt
// 使用bcrypt进行哈希成本因子12平衡安全性和性能
hashed, err := bcrypt.GenerateFromPassword([]byte(combined), 12)
if err != nil {
return "", err
}
return string(hashed), nil
}
// VerifyPasswordWithSalt 验证密码和盐值的组合是否匹配哈希值
// password: 原始密码
// salt: 密码盐值
// hashedPassword: 存储的哈希密码
// 返回: 验证结果true表示匹配
func VerifyPasswordWithSalt(password, salt, hashedPassword string) bool {
// 将密码和盐值组合
combined := password + salt
// 使用bcrypt验证
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(combined))
return err == nil
}

327
utils/database.go Normal file
View File

@@ -0,0 +1,327 @@
package utils
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"sync"
"time"
"github.com/redis/go-redis/v9"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"gorm.io/gorm"
)
// DatabaseConfig 数据库连接池配置结构体
// 用于配置数据库连接池的各项参数,包括连接池大小、生命周期管理和健康检查等
type DatabaseConfig struct {
// 连接池配置
MaxIdleConns int `mapstructure:"max_idle_conns"` // 最大空闲连接数
MaxOpenConns int `mapstructure:"max_open_conns"` // 最大打开连接数
ConnMaxLifetime time.Duration `mapstructure:"conn_max_lifetime"` // 连接最大生存时间
ConnMaxIdleTime time.Duration `mapstructure:"conn_max_idle_time"` // 连接最大空闲时间
// 健康检查配置
PingTimeout time.Duration `mapstructure:"ping_timeout"` // Ping超时时间
HealthCheckInterval time.Duration `mapstructure:"health_check_interval"` // 健康检查间隔
}
// GetDefaultDatabaseConfig 获取默认数据库配置
// 返回一个包含合理默认值的数据库配置实例
func GetDefaultDatabaseConfig() *DatabaseConfig {
return &DatabaseConfig{
MaxIdleConns: 10, // 默认最大空闲连接数
MaxOpenConns: 100, // 默认最大打开连接数
ConnMaxLifetime: 30 * time.Minute, // 连接最大生存时间30分钟
ConnMaxIdleTime: 10 * time.Minute, // 连接最大空闲时间10分钟
PingTimeout: 5 * time.Second, // Ping超时5秒
HealthCheckInterval: 30 * time.Second, // 健康检查间隔30秒
}
}
// LoadDatabaseConfig 从配置文件加载数据库配置
// 使用指定的前缀从viper配置中读取数据库配置如果配置项不存在则使用默认值
func LoadDatabaseConfig(prefix string) *DatabaseConfig {
config := GetDefaultDatabaseConfig()
// 从viper读取配置如果不存在则使用默认值
if viper.IsSet(prefix + ".max_idle_conns") {
config.MaxIdleConns = viper.GetInt(prefix + ".max_idle_conns")
}
if viper.IsSet(prefix + ".max_open_conns") {
config.MaxOpenConns = viper.GetInt(prefix + ".max_open_conns")
}
if viper.IsSet(prefix + ".conn_max_lifetime") {
config.ConnMaxLifetime = viper.GetDuration(prefix + ".conn_max_lifetime")
}
if viper.IsSet(prefix + ".conn_max_idle_time") {
config.ConnMaxIdleTime = viper.GetDuration(prefix + ".conn_max_idle_time")
}
if viper.IsSet(prefix + ".ping_timeout") {
config.PingTimeout = viper.GetDuration(prefix + ".ping_timeout")
}
if viper.IsSet(prefix + ".health_check_interval") {
config.HealthCheckInterval = viper.GetDuration(prefix + ".health_check_interval")
}
return config
}
// ConfigureConnectionPool 配置数据库连接池
// 根据提供的配置参数设置GORM数据库的连接池属性
func ConfigureConnectionPool(db *gorm.DB, config *DatabaseConfig) error {
sqlDB, err := db.DB()
if err != nil {
return fmt.Errorf("获取底层数据库连接失败: %w", err)
}
// 设置连接池参数
sqlDB.SetMaxIdleConns(config.MaxIdleConns)
sqlDB.SetMaxOpenConns(config.MaxOpenConns)
sqlDB.SetConnMaxLifetime(config.ConnMaxLifetime)
sqlDB.SetConnMaxIdleTime(config.ConnMaxIdleTime)
// LogInfo("数据库连接池配置完成", map[string]interface{}{
// "max_idle_conns": config.MaxIdleConns,
// "max_open_conns": config.MaxOpenConns,
// "conn_max_lifetime": config.ConnMaxLifetime,
// "conn_max_idle_time": config.ConnMaxIdleTime,
// })
return nil
}
// PingDatabase 检查数据库连接健康状态
// 使用指定的超时时间ping数据库以验证连接是否正常
func PingDatabase(db *gorm.DB, timeout time.Duration) error {
sqlDB, err := db.DB()
if err != nil {
return fmt.Errorf("获取底层数据库连接失败: %w", err)
}
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
return sqlDB.PingContext(ctx)
}
// GetConnectionStats 获取数据库连接池统计信息
// 返回当前数据库连接池的详细统计数据,包括连接数、等待时间等
func GetConnectionStats(db *gorm.DB) (*sql.DBStats, error) {
sqlDB, err := db.DB()
if err != nil {
return nil, fmt.Errorf("获取底层数据库连接失败: %w", err)
}
stats := sqlDB.Stats()
return &stats, nil
}
// LogConnectionStats 记录数据库连接池统计信息
// 获取并记录数据库连接池的统计信息到日志中,用于监控和调试
func LogConnectionStats(db *gorm.DB) {
stats, err := GetConnectionStats(db)
if err != nil {
LogError("获取数据库连接池统计信息失败", err, nil)
return
}
LogInfo("数据库连接池统计", map[string]interface{}{
"open_connections": stats.OpenConnections,
"in_use": stats.InUse,
"idle": stats.Idle,
"wait_count": stats.WaitCount,
"wait_duration": stats.WaitDuration,
"max_idle_closed": stats.MaxIdleClosed,
"max_idle_time_closed": stats.MaxIdleTimeClosed,
"max_lifetime_closed": stats.MaxLifetimeClosed,
})
}
// StartHealthCheck 启动数据库健康检查
// 启动一个后台goroutine定期检查数据库连接健康状态
// 只在健康检查失败时输出错误日志,正常情况下不输出日志
func StartHealthCheck(db *gorm.DB, config *DatabaseConfig) {
go func() {
ticker := time.NewTicker(config.HealthCheckInterval)
defer ticker.Stop()
for range ticker.C {
if err := PingDatabase(db, config.PingTimeout); err != nil {
// 只在健康检查失败时输出错误日志
LogError("数据库健康检查失败", err, map[string]interface{}{
"ping_timeout": config.PingTimeout,
})
}
// 记录连接池统计信息(仅在调试模式下)
if logrus.GetLevel() == logrus.DebugLevel {
LogConnectionStats(db)
}
}
}()
// LogInfo("数据库健康检查已启动", map[string]interface{}{
// "check_interval": config.HealthCheckInterval,
// "ping_timeout": config.PingTimeout,
// })
}
// ValidateDatabaseConfig 验证数据库配置参数
// 检查数据库配置参数的有效性,确保所有参数都在合理范围内
func ValidateDatabaseConfig(config *DatabaseConfig) error {
if config.MaxIdleConns < 0 {
return fmt.Errorf("最大空闲连接数不能为负数: %d", config.MaxIdleConns)
}
if config.MaxOpenConns < 0 {
return fmt.Errorf("最大打开连接数不能为负数: %d", config.MaxOpenConns)
}
if config.MaxIdleConns > config.MaxOpenConns && config.MaxOpenConns > 0 {
return fmt.Errorf("最大空闲连接数(%d)不能大于最大打开连接数(%d)", config.MaxIdleConns, config.MaxOpenConns)
}
if config.ConnMaxLifetime < 0 {
return fmt.Errorf("连接最大生存时间不能为负数: %v", config.ConnMaxLifetime)
}
if config.ConnMaxIdleTime < 0 {
return fmt.Errorf("连接最大空闲时间不能为负数: %v", config.ConnMaxIdleTime)
}
if config.PingTimeout <= 0 {
return fmt.Errorf("Ping超时时间必须大于0: %v", config.PingTimeout)
}
if config.HealthCheckInterval <= 0 {
return fmt.Errorf("健康检查间隔必须大于0: %v", config.HealthCheckInterval)
}
return nil
}
var (
// redisClient 全局Redis客户端
redisClient *redis.Client
// redisOnce 确保只初始化一次
redisOnce sync.Once
// redisAvailable 标记Redis是否可用
redisAvailable bool
)
// InitRedis 初始化Redis客户端仅在配置存在时尝试连接
// - 从 viper 读取 security.redis.* 配置
// - 如果连接失败,则标记为不可用,不影响主流程
func InitRedis() {
redisOnce.Do(func() {
host := viper.GetString("redis.host")
port := viper.GetInt("redis.port")
if host == "" || port == 0 {
logrus.Info("未配置Redis或配置不完整跳过初始化")
redisAvailable = false
return
}
addr := fmt.Sprintf("%s:%d", host, port)
redisClient = redis.NewClient(&redis.Options{
Addr: addr,
Password: viper.GetString("redis.password"),
DB: viper.GetInt("redis.db"),
})
// 健康检查
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := redisClient.Ping(ctx).Err(); err != nil {
logrus.WithError(err).Warn("Redis初始化失败标记为不可用")
redisAvailable = false
return
}
redisAvailable = true
logrus.WithField("addr", addr).Info("Redis 连接已建立")
})
}
// GetRedis 获取全局Redis客户端可能返回nil当不可用时
func GetRedis() *redis.Client {
if redisClient == nil {
InitRedis()
}
if !redisAvailable {
return nil
}
return redisClient
}
// IsRedisAvailable 判断Redis是否可用
func IsRedisAvailable() bool {
if redisClient == nil {
InitRedis()
}
return redisAvailable
}
// RedisGetOrSet 通用Redis缓存获取或设置函数基于JSON序列化
// - ctx: 上下文
// - key: 缓存键
// - ttl: 过期时间
// - loader: 当缓存不存在时的加载函数(一般执行数据库查询)
// 返回:目标对象指针和错误
func RedisGetOrSet[T any](ctx context.Context, key string, ttl time.Duration, loader func() (*T, error)) (*T, error) {
// 如果Redis不可用则直接调用加载函数
if !IsRedisAvailable() {
return loader()
}
client := GetRedis()
if client == nil {
return loader()
}
// 先尝试从缓存读取
data, err := client.Get(ctx, key).Bytes()
if err == nil {
var out T
if uerr := json.Unmarshal(data, &out); uerr == nil {
return &out, nil
}
// 反序列化失败时视为未命中,继续加载
logrus.WithError(err).WithField("key", key).Warn("Redis缓存反序列化失败回退到loader")
} else if err != redis.Nil {
// 非空且非不存在的错误,记录告警但不中断
logrus.WithError(err).WithField("key", key).Warn("读取Redis缓存失败")
}
// 加载数据
val, lerr := loader()
if lerr != nil {
return nil, lerr
}
if val == nil {
return nil, nil
}
// 写回缓存(错误不影响主流程)
if b, merr := json.Marshal(val); merr == nil {
if serr := client.Set(ctx, key, b, ttl).Err(); serr != nil {
logrus.WithError(serr).WithField("key", key).Warn("写入Redis缓存失败")
}
}
return val, nil
}
// RedisDel 删除一个或多个Redis键当Redis不可用时静默返回
// - ctx: 上下文
// - keys: 需要删除的键名
func RedisDel(ctx context.Context, keys ...string) error {
// 如果Redis不可用则直接返回
if !IsRedisAvailable() {
return nil
}
client := GetRedis()
if client == nil {
return nil
}
if len(keys) == 0 {
return nil
}
if _, err := client.Del(ctx, keys...).Result(); err != nil {
logrus.WithError(err).WithField("keys", keys).Warn("删除Redis键失败")
return err
}
return nil
}

269
utils/errors.go Normal file
View File

@@ -0,0 +1,269 @@
package utils
import (
"encoding/json"
"fmt"
"log"
"net/http"
"runtime"
"time"
"gorm.io/gorm"
)
// ErrorResponse 统一的错误响应结构
// 用于标准化API错误响应格式
type ErrorResponse struct {
Success bool `json:"success"` // 请求是否成功错误响应时固定为false
Message string `json:"message"` // 错误消息描述
ErrorCode string `json:"error_code,omitempty"` // 错误代码,用于客户端识别错误类型
Data interface{} `json:"data"` // 附加数据,可为空
Timestamp int64 `json:"timestamp"` // 响应时间戳
}
// SuccessResponse 统一的成功响应结构
// 用于标准化API成功响应格式
type SuccessResponse struct {
Success bool `json:"success"` // 请求是否成功成功响应时固定为true
Message string `json:"message"` // 成功消息描述
Data interface{} `json:"data"` // 响应数据
Timestamp int64 `json:"timestamp"` // 响应时间戳
}
// ErrorCode 错误代码常量
// 定义标准化的错误代码,用于客户端识别和处理不同类型的错误
const (
ErrCodeInvalidRequest = "INVALID_REQUEST" // 无效请求,通常是请求参数格式错误
ErrCodeUnauthorized = "UNAUTHORIZED" // 未授权需要登录或token无效
ErrCodeForbidden = "FORBIDDEN" // 禁止访问,权限不足
ErrCodeNotFound = "NOT_FOUND" // 资源不存在
ErrCodeConflict = "CONFLICT" // 资源冲突,如重复创建
ErrCodeInternalError = "INTERNAL_ERROR" // 服务器内部错误
ErrCodeDatabaseError = "DATABASE_ERROR" // 数据库操作错误
ErrCodeValidationError = "VALIDATION_ERROR" // 数据验证错误
ErrCodeTokenExpired = "TOKEN_EXPIRED" // 令牌已过期
ErrCodeInsufficientData = "INSUFFICIENT_DATA" // 数据不足,缺少必要信息
)
// LogLevel 日志级别
// 定义不同的日志记录级别
type LogLevel int
const (
LogLevelInfo LogLevel = iota // 信息级别,记录一般信息
LogLevelWarn // 警告级别,记录潜在问题
LogLevelError // 错误级别,记录错误信息
LogLevelDebug // 调试级别,记录调试信息
)
// LogEntry 日志条目结构
// 包含完整的日志信息,用于结构化日志记录
type LogEntry struct {
Level LogLevel `json:"level"` // 日志级别
Message string `json:"message"` // 日志消息
Error string `json:"error,omitempty"` // 错误信息,仅在错误日志中存在
Context interface{} `json:"context,omitempty"` // 上下文信息,额外的结构化数据
Timestamp time.Time `json:"timestamp"` // 日志时间戳
File string `json:"file"` // 源文件路径
Line int `json:"line"` // 源文件行号
}
// WriteJSONResponse 写入JSON响应
// w: HTTP响应写入器
// statusCode: HTTP状态码
// response: 响应数据
func WriteJSONResponse(w http.ResponseWriter, statusCode int, response interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
if err := json.NewEncoder(w).Encode(response); err != nil {
LogError("Failed to encode JSON response", err, nil)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
// WriteErrorResponse 写入错误响应
// w: HTTP响应写入器
// statusCode: HTTP状态码
// message: 错误消息
// errorCode: 错误代码
// data: 附加数据
func WriteErrorResponse(w http.ResponseWriter, statusCode int, message, errorCode string, data interface{}) {
response := ErrorResponse{
Success: false,
Message: message,
ErrorCode: errorCode,
Data: data,
Timestamp: time.Now().Unix(),
}
WriteJSONResponse(w, statusCode, response)
}
// WriteSuccessResponse 写入成功响应
// w: HTTP响应写入器
// statusCode: HTTP状态码
// message: 成功消息
// data: 响应数据
func WriteSuccessResponse(w http.ResponseWriter, statusCode int, message string, data interface{}) {
response := SuccessResponse{
Success: true,
Message: message,
Data: data,
Timestamp: time.Now().Unix(),
}
WriteJSONResponse(w, statusCode, response)
}
// HandleDatabaseError 处理数据库错误
// w: HTTP响应写入器
// err: 数据库错误
// operation: 操作描述
func HandleDatabaseError(w http.ResponseWriter, err error, operation string) {
if err == gorm.ErrRecordNotFound {
LogWarn(fmt.Sprintf("Record not found during %s", operation), map[string]interface{}{
"operation": operation,
"error": err.Error(),
})
WriteErrorResponse(w, http.StatusNotFound, "记录不存在", ErrCodeNotFound, nil)
return
}
LogError(fmt.Sprintf("Database error during %s", operation), err, map[string]interface{}{
"operation": operation,
})
WriteErrorResponse(w, http.StatusInternalServerError, "数据库操作失败", ErrCodeDatabaseError, nil)
}
// HandleValidationError 处理验证错误
// w: HTTP响应写入器
// message: 验证错误消息
// details: 验证错误详情
func HandleValidationError(w http.ResponseWriter, message string, details interface{}) {
LogWarn("Validation error: "+message, map[string]interface{}{
"details": details,
})
WriteErrorResponse(w, http.StatusBadRequest, message, ErrCodeValidationError, details)
}
// HandleUnauthorizedError 处理未授权错误
// w: HTTP响应写入器
// message: 错误消息
func HandleUnauthorizedError(w http.ResponseWriter, message string) {
LogWarn("Unauthorized access: "+message, nil)
WriteErrorResponse(w, http.StatusUnauthorized, message, ErrCodeUnauthorized, nil)
}
// HandleInternalError 处理内部错误
// w: HTTP响应写入器
// err: 错误
// operation: 操作描述
func HandleInternalError(w http.ResponseWriter, err error, operation string) {
LogError(fmt.Sprintf("Internal error during %s", operation), err, map[string]interface{}{
"operation": operation,
})
WriteErrorResponse(w, http.StatusInternalServerError, "服务器内部错误", ErrCodeInternalError, nil)
}
// LogInfo 记录信息日志
// message: 日志消息
// context: 上下文信息
func LogInfo(message string, context interface{}) {
logEntry := createLogEntry(LogLevelInfo, message, nil, context)
printLog(logEntry)
}
// LogWarn 记录警告日志
// message: 日志消息
// context: 上下文信息
func LogWarn(message string, context interface{}) {
logEntry := createLogEntry(LogLevelWarn, message, nil, context)
printLog(logEntry)
}
// LogError 记录错误日志
// message: 日志消息
// err: 错误对象
// context: 上下文信息
func LogError(message string, err error, context interface{}) {
errorStr := ""
if err != nil {
errorStr = err.Error()
}
logEntry := createLogEntry(LogLevelError, message, &errorStr, context)
printLog(logEntry)
}
// LogDebug 记录调试日志
// message: 日志消息
// context: 上下文信息
func LogDebug(message string, context interface{}) {
logEntry := createLogEntry(LogLevelDebug, message, nil, context)
printLog(logEntry)
}
// createLogEntry 创建日志条目
// level: 日志级别
// message: 日志消息
// errorStr: 错误字符串
// context: 上下文信息
// 返回: 日志条目
func createLogEntry(level LogLevel, message string, errorStr *string, context interface{}) LogEntry {
_, file, line, _ := runtime.Caller(2)
entry := LogEntry{
Level: level,
Message: message,
Context: context,
Timestamp: time.Now(),
File: file,
Line: line,
}
if errorStr != nil {
entry.Error = *errorStr
}
return entry
}
// printLog 打印日志
// entry: 日志条目
func printLog(entry LogEntry) {
levelStr := getLevelString(entry.Level)
timestamp := entry.Timestamp.Format("2006-01-02 15:04:05")
logMessage := fmt.Sprintf("[%s] %s %s", levelStr, timestamp, entry.Message)
if entry.Error != "" {
logMessage += fmt.Sprintf(" | Error: %s", entry.Error)
}
if entry.Context != nil {
contextJSON, _ := json.Marshal(entry.Context)
logMessage += fmt.Sprintf(" | Context: %s", string(contextJSON))
}
logMessage += fmt.Sprintf(" | %s:%d", entry.File, entry.Line)
log.Println(logMessage)
}
// getLevelString 获取日志级别字符串
// level: 日志级别
// 返回: 级别字符串
func getLevelString(level LogLevel) string {
switch level {
case LogLevelInfo:
return "INFO"
case LogLevelWarn:
return "WARN"
case LogLevelError:
return "ERROR"
case LogLevelDebug:
return "DEBUG"
default:
return "UNKNOWN"
}
}

59
utils/logger/http.go Normal file
View File

@@ -0,0 +1,59 @@
package logger
import (
"fmt"
"os"
"time"
)
// LogRequest 记录HTTP请求日志 - 使用标准Apache Common Log Format
// 格式: IP - - [timestamp] "METHOD path HTTP/1.1" status_code response_size
// method: HTTP请求方法
// path: 请求路径
// clientIP: 客户端IP地址
// statusCode: HTTP状态码
// duration: 请求处理时长
func (l *Logger) LogRequest(method, path, clientIP string, statusCode int, duration time.Duration) {
l.LogRequestWithHeaders(method, path, clientIP, statusCode, duration, "-", "-")
}
// LogRequestWithHeaders 记录HTTP请求日志 - 使用修改的Apache Log Format移除Referer字段
// 直接输出标准格式不通过logrus格式化器
// method: HTTP请求方法
// path: 请求路径
// clientIP: 客户端IP地址
// statusCode: HTTP状态码
// duration: 请求处理时长
// referer: 引用页面(已废弃,保留参数兼容性)
// userAgent: 用户代理字符串
func (l *Logger) LogRequestWithHeaders(method, path, clientIP string, statusCode int, duration time.Duration, referer, userAgent string) {
// 格式化时间戳为Apache标准格式
timestamp := time.Now().Format("02/Jan/2006:15:04:05 -0700")
// 处理空值
if userAgent == "" {
userAgent = "-"
}
// 构建修改的HTTP Log格式完全移除Referer字段
logLine := fmt.Sprintf(`%s - - [%s] "%s %s HTTP/1.1" %d - "%s" %dms`,
clientIP,
timestamp,
method,
path,
statusCode,
userAgent,
duration.Milliseconds(),
)
// 直接输出到标准输出和日志文件不使用logrus格式化
l.writeHTTPLog(logLine)
}
// writeHTTPLog 直接输出HTTP日志到标准输出
// 避免Logrus的任何格式化和转义保持Apache日志格式的原始性
// logLine: 格式化后的日志行
func (l *Logger) writeHTTPLog(logLine string) {
// 直接输出到标准输出避免Logrus的转义处理
fmt.Fprintln(os.Stdout, logLine)
}

112
utils/logger/logger.go Normal file
View File

@@ -0,0 +1,112 @@
package logger
import (
log "github.com/sirupsen/logrus"
)
// Logger 日志工具结构体
// 封装logrus.Logger提供统一的日志接口
type Logger struct {
*log.Logger // 嵌入logrus.Logger继承其所有方法
}
// NewLogger 创建新的日志实例使用全局logrus配置
// 返回: 新的Logger实例
func NewLogger() *Logger {
// 使用全局logrus实例而不是创建新实例确保配置一致性
return &Logger{Logger: log.StandardLogger()}
}
// InitLogger 初始化HTTP日志处理器
// 创建专门用于HTTP请求日志的Logger实例使用全局logrus配置
// 返回: 初始化后的Logger实例
func InitLogger() *Logger {
logger := NewLogger()
// HTTP日志使用全局logrus的配置
// 通过使用log.StandardLogger()确保与全局配置保持一致
// 更新全局日志实例
SetGlobalLogger(logger)
return logger
}
// WithFields 添加字段到日志条目
// fields: 要添加的字段映射
// 返回: 包含字段的日志条目
func (l *Logger) WithFields(fields log.Fields) *log.Entry {
return l.Logger.WithFields(fields)
}
// WithField 添加单个字段到日志条目
// key: 字段名
// value: 字段值
// 返回: 包含字段的日志条目
func (l *Logger) WithField(key string, value interface{}) *log.Entry {
return l.Logger.WithField(key, value)
}
// WithError 添加错误字段到日志条目
// err: 要记录的错误
// 返回: 包含错误信息的日志条目
func (l *Logger) WithError(err error) *log.Entry {
return l.Logger.WithError(err)
}
// InfoWithFields 记录带字段的信息级别日志
// msg: 日志消息
// fields: 附加字段
func (l *Logger) InfoWithFields(msg string, fields log.Fields) {
l.WithFields(fields).Info(msg)
}
// ErrorWithFields 记录带字段的错误级别日志
// msg: 日志消息
// fields: 附加字段
func (l *Logger) ErrorWithFields(msg string, fields log.Fields) {
l.WithFields(fields).Error(msg)
}
// WarnWithFields 记录带字段的警告级别日志
// msg: 日志消息
// fields: 附加字段
func (l *Logger) WarnWithFields(msg string, fields log.Fields) {
l.WithFields(fields).Warn(msg)
}
// DebugWithFields 记录带字段的调试级别日志
// msg: 日志消息
// fields: 附加字段
func (l *Logger) DebugWithFields(msg string, fields log.Fields) {
l.WithFields(fields).Debug(msg)
}
// LogError 记录错误日志
// err: 错误对象
// msg: 日志消息
func (l *Logger) LogError(err error, msg string) {
l.WithError(err).Error(msg)
}
// GlobalLogger 全局日志实例
// 提供全局访问的日志记录器
var GlobalLogger *Logger
// init 包初始化函数
// 创建全局日志实例使用全局logrus配置
func init() {
GlobalLogger = NewLogger()
}
// GetLogger 获取全局日志实例
// 返回: 全局Logger实例
func GetLogger() *Logger {
return GlobalLogger
}
// SetGlobalLogger 设置全局日志实例
// logger: 要设置的Logger实例
func SetGlobalLogger(logger *Logger) {
GlobalLogger = logger
}

26
utils/logger/server.go Normal file
View File

@@ -0,0 +1,26 @@
package logger
import (
log "github.com/sirupsen/logrus"
)
// LogServerStart 记录服务器启动日志
// host: 服务器监听地址
// port: 服务器监听端口
func (l *Logger) LogServerStart(host string, port int) {
l.WithFields(log.Fields{
"host": host,
"port": port,
}).Info("HTTP服务器启动")
}
// LogServerStop 记录服务器停止日志
func (l *Logger) LogServerStop() {
l.Info("HTTP服务器停止")
}
// LogConfigLoad 记录配置加载日志
// configFile: 配置文件路径
func (l *Logger) LogConfigLoad(configFile string) {
l.WithField("config_file", configFile).Info("配置文件加载")
}

44
utils/timeutil/server.go Normal file
View File

@@ -0,0 +1,44 @@
package timeutil
import (
"fmt"
"time"
)
// serverStartTime 记录进程启动时间(近似服务器启动时间)
var serverStartTime = time.Now()
// GetServerStartTime 获取服务器启动时间
// 返回: 服务器启动的时间戳
func GetServerStartTime() time.Time {
return serverStartTime
}
// GetServerUptime 获取服务器运行时长
// 返回: 从服务器启动到现在的时间间隔
func GetServerUptime() time.Duration {
return time.Since(serverStartTime)
}
// GetServerUptimeString 获取服务器运行时长的字符串表示
// 返回: 格式化的运行时长字符串
func GetServerUptimeString() string {
duration := time.Since(serverStartTime)
// 获取总秒数并转换为整数
totalSeconds := int(duration.Seconds())
// 计算小时、分钟、秒
hours := totalSeconds / 3600
minutes := (totalSeconds % 3600) / 60
seconds := totalSeconds % 60
// 根据时长长度选择合适的格式
if hours > 0 {
return fmt.Sprintf("%dh%dm%ds", hours, minutes, seconds)
} else if minutes > 0 {
return fmt.Sprintf("%dm%ds", minutes, seconds)
} else {
return fmt.Sprintf("%ds", seconds)
}
}