做完了一部分,备份一下,防止以外删除
This commit is contained in:
167
pkg/config/config.go
Normal file
167
pkg/config/config.go
Normal file
@@ -0,0 +1,167 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// globalConfig 保存当前配置,支持原子访问
|
||||
var globalConfig atomic.Pointer[Config]
|
||||
|
||||
// Config 应用配置
|
||||
type Config struct {
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
Redis RedisConfig `mapstructure:"redis"`
|
||||
Logging LoggingConfig `mapstructure:"logging"`
|
||||
Middleware MiddlewareConfig `mapstructure:"middleware"`
|
||||
}
|
||||
|
||||
// ServerConfig HTTP 服务器配置
|
||||
type ServerConfig struct {
|
||||
Address string `mapstructure:"address"` // 例如 ":3000"
|
||||
ReadTimeout time.Duration `mapstructure:"read_timeout"` // 例如 "10s"
|
||||
WriteTimeout time.Duration `mapstructure:"write_timeout"` // 例如 "10s"
|
||||
ShutdownTimeout time.Duration `mapstructure:"shutdown_timeout"` // 例如 "30s"
|
||||
Prefork bool `mapstructure:"prefork"` // 多进程模式
|
||||
}
|
||||
|
||||
// RedisConfig Redis 连接配置
|
||||
type RedisConfig struct {
|
||||
Address string `mapstructure:"address"` // 例如 "localhost"
|
||||
Port int `mapstructure:"port"` // 例如 "6379"
|
||||
Password string `mapstructure:"password"` // 无认证时留空
|
||||
DB int `mapstructure:"db"` // 数据库编号(0-15)
|
||||
PoolSize int `mapstructure:"pool_size"` // 最大连接数
|
||||
MinIdleConns int `mapstructure:"min_idle_conns"` // 保活连接数
|
||||
DialTimeout time.Duration `mapstructure:"dial_timeout"` // 例如 "5s"
|
||||
ReadTimeout time.Duration `mapstructure:"read_timeout"` // 例如 "3s"
|
||||
WriteTimeout time.Duration `mapstructure:"write_timeout"` // 例如 "3s"
|
||||
}
|
||||
|
||||
// LoggingConfig 日志配置
|
||||
type LoggingConfig struct {
|
||||
Level string `mapstructure:"level"` // debug, info, warn, error
|
||||
Development bool `mapstructure:"development"` // 启用开发模式(美化输出)
|
||||
AppLog LogRotationConfig `mapstructure:"app_log"` // 应用日志配置
|
||||
AccessLog LogRotationConfig `mapstructure:"access_log"` // HTTP 访问日志配置
|
||||
}
|
||||
|
||||
// LogRotationConfig Lumberjack 日志轮转配置
|
||||
type LogRotationConfig struct {
|
||||
Filename string `mapstructure:"filename"` // 日志文件路径
|
||||
MaxSize int `mapstructure:"max_size"` // 轮转前的最大大小(MB)
|
||||
MaxBackups int `mapstructure:"max_backups"` // 保留的旧文件最大数量
|
||||
MaxAge int `mapstructure:"max_age"` // 保留旧文件的最大天数
|
||||
Compress bool `mapstructure:"compress"` // 压缩轮转的文件
|
||||
}
|
||||
|
||||
// MiddlewareConfig 中间件配置
|
||||
type MiddlewareConfig struct {
|
||||
EnableAuth bool `mapstructure:"enable_auth"` // 启用 keyauth 中间件
|
||||
EnableRateLimiter bool `mapstructure:"enable_rate_limiter"` // 启用限流器(默认:false)
|
||||
RateLimiter RateLimiterConfig `mapstructure:"rate_limiter"` // 限流器配置
|
||||
}
|
||||
|
||||
// RateLimiterConfig 限流器配置
|
||||
type RateLimiterConfig struct {
|
||||
Max int `mapstructure:"max"` // 时间窗口内的最大请求数
|
||||
Expiration time.Duration `mapstructure:"expiration"` // 时间窗口(例如 "1m")
|
||||
Storage string `mapstructure:"storage"` // "memory" 或 "redis"
|
||||
}
|
||||
|
||||
// Validate 验证配置值
|
||||
func (c *Config) Validate() error {
|
||||
// 服务器验证
|
||||
if c.Server.Address == "" {
|
||||
return fmt.Errorf("invalid configuration: server.address: must be non-empty (current value: empty)")
|
||||
}
|
||||
if c.Server.ReadTimeout < 5*time.Second || c.Server.ReadTimeout > 300*time.Second {
|
||||
return fmt.Errorf("invalid configuration: server.read_timeout: duration out of range (current value: %s, expected: 5s-300s)", c.Server.ReadTimeout)
|
||||
}
|
||||
if c.Server.WriteTimeout < 5*time.Second || c.Server.WriteTimeout > 300*time.Second {
|
||||
return fmt.Errorf("invalid configuration: server.write_timeout: duration out of range (current value: %s, expected: 5s-300s)", c.Server.WriteTimeout)
|
||||
}
|
||||
if c.Server.ShutdownTimeout < 10*time.Second || c.Server.ShutdownTimeout > 120*time.Second {
|
||||
return fmt.Errorf("invalid configuration: server.shutdown_timeout: duration out of range (current value: %s, expected: 10s-120s)", c.Server.ShutdownTimeout)
|
||||
}
|
||||
|
||||
// Redis 验证
|
||||
if c.Redis.Address == "" {
|
||||
return fmt.Errorf("invalid configuration: redis.address: must be non-empty (current value: empty)")
|
||||
}
|
||||
if !strings.Contains(c.Redis.Address, ":") {
|
||||
return fmt.Errorf("invalid configuration: redis.address: invalid format (current value: %s, expected: HOST:PORT)", c.Redis.Address)
|
||||
}
|
||||
if c.Redis.DB < 0 || c.Redis.DB > 15 {
|
||||
return fmt.Errorf("invalid configuration: redis.db: database number out of range (current value: %d, expected: 0-15)", c.Redis.DB)
|
||||
}
|
||||
if c.Redis.PoolSize <= 0 || c.Redis.PoolSize > 1000 {
|
||||
return fmt.Errorf("invalid configuration: redis.pool_size: pool size out of range (current value: %d, expected: 1-1000)", c.Redis.PoolSize)
|
||||
}
|
||||
if c.Redis.MinIdleConns < 0 || c.Redis.MinIdleConns > c.Redis.PoolSize {
|
||||
return fmt.Errorf("invalid configuration: redis.min_idle_conns: must be 0 to pool_size (current value: %d, pool_size: %d)", c.Redis.MinIdleConns, c.Redis.PoolSize)
|
||||
}
|
||||
|
||||
// 日志验证
|
||||
validLevels := map[string]bool{"debug": true, "info": true, "warn": true, "error": true}
|
||||
if !validLevels[c.Logging.Level] {
|
||||
return fmt.Errorf("invalid configuration: logging.level: invalid log level (current value: %s, expected: debug, info, warn, error)", c.Logging.Level)
|
||||
}
|
||||
if c.Logging.AppLog.Filename == "" {
|
||||
return fmt.Errorf("invalid configuration: logging.app_log.filename: must be non-empty valid file path")
|
||||
}
|
||||
if c.Logging.AccessLog.Filename == "" {
|
||||
return fmt.Errorf("invalid configuration: logging.access_log.filename: must be non-empty valid file path")
|
||||
}
|
||||
if c.Logging.AppLog.MaxSize < 1 || c.Logging.AppLog.MaxSize > 1000 {
|
||||
return fmt.Errorf("invalid configuration: logging.app_log.max_size: size out of range (current value: %d, expected: 1-1000 MB)", c.Logging.AppLog.MaxSize)
|
||||
}
|
||||
if c.Logging.AccessLog.MaxSize < 1 || c.Logging.AccessLog.MaxSize > 1000 {
|
||||
return fmt.Errorf("invalid configuration: logging.access_log.max_size: size out of range (current value: %d, expected: 1-1000 MB)", c.Logging.AccessLog.MaxSize)
|
||||
}
|
||||
|
||||
// 中间件验证
|
||||
if c.Middleware.RateLimiter.Max <= 0 {
|
||||
c.Middleware.RateLimiter.Max = 100 // 默认值
|
||||
}
|
||||
if c.Middleware.RateLimiter.Max > 10000 {
|
||||
return fmt.Errorf("invalid configuration: middleware.rate_limiter.max: request limit out of range (current value: %d, expected: 1-10000)", c.Middleware.RateLimiter.Max)
|
||||
}
|
||||
validStorage := map[string]bool{"memory": true, "redis": true}
|
||||
if c.Middleware.RateLimiter.Storage != "" && !validStorage[c.Middleware.RateLimiter.Storage] {
|
||||
return fmt.Errorf("invalid configuration: middleware.rate_limiter.storage: invalid storage type (current value: %s, expected: memory or redis)", c.Middleware.RateLimiter.Storage)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get 返回当前配置
|
||||
func Get() *Config {
|
||||
return globalConfig.Load()
|
||||
}
|
||||
|
||||
// Set 原子地更新全局配置
|
||||
func Set(cfg *Config) error {
|
||||
if cfg == nil {
|
||||
return errors.New("config cannot be nil")
|
||||
}
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
globalConfig.Store(cfg)
|
||||
return nil
|
||||
}
|
||||
|
||||
// unsafeSet 无验证设置配置(仅供热重载内部使用)
|
||||
func unsafeSet(cfg *Config) {
|
||||
globalConfig.Store(cfg)
|
||||
}
|
||||
|
||||
// atomicSwap 原子地交换配置
|
||||
func atomicSwap(new *Config) *Config {
|
||||
return (*Config)(atomic.SwapPointer((*unsafe.Pointer)(unsafe.Pointer(&globalConfig)), unsafe.Pointer(new)))
|
||||
}
|
||||
93
pkg/config/loader.go
Normal file
93
pkg/config/loader.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// Load 从文件和环境变量加载配置
|
||||
func Load() (*Config, error) {
|
||||
// 确定配置路径
|
||||
configPath := os.Getenv(constants.EnvConfigPath)
|
||||
if configPath == "" {
|
||||
configPath = constants.DefaultConfigPath
|
||||
}
|
||||
|
||||
// 检查环境特定配置(dev, staging, prod)
|
||||
configEnv := os.Getenv(constants.EnvConfigEnv)
|
||||
if configEnv != "" {
|
||||
// 优先尝试环境特定配置
|
||||
envConfigPath := fmt.Sprintf("configs/config.%s.yaml", configEnv)
|
||||
if _, err := os.Stat(envConfigPath); err == nil {
|
||||
configPath = envConfigPath
|
||||
}
|
||||
}
|
||||
|
||||
// 设置 Viper
|
||||
viper.SetConfigFile(configPath)
|
||||
viper.SetConfigType("yaml")
|
||||
|
||||
// 启用环境变量覆盖
|
||||
viper.AutomaticEnv()
|
||||
viper.SetEnvPrefix("APP")
|
||||
|
||||
// 读取配置文件
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
// 反序列化到 Config 结构体
|
||||
cfg := &Config{}
|
||||
if err := viper.Unmarshal(cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
// 验证配置
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 设为全局配置
|
||||
globalConfig.Store(cfg)
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// Reload 重新加载当前配置文件
|
||||
func Reload() (*Config, error) {
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("failed to reload config: %w", err)
|
||||
}
|
||||
|
||||
cfg := &Config{}
|
||||
if err := viper.Unmarshal(cfg); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
// 设置前验证
|
||||
if err := cfg.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 原子交换
|
||||
globalConfig.Store(cfg)
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// GetConfigPath 返回当前已加载配置文件的绝对路径
|
||||
func GetConfigPath() string {
|
||||
configFile := viper.ConfigFileUsed()
|
||||
if configFile == "" {
|
||||
return ""
|
||||
}
|
||||
absPath, err := filepath.Abs(configFile)
|
||||
if err != nil {
|
||||
return configFile
|
||||
}
|
||||
return absPath
|
||||
}
|
||||
43
pkg/config/watcher.go
Normal file
43
pkg/config/watcher.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/fsnotify/fsnotify"
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Watch 监听配置文件变化
|
||||
// 运行直到上下文被取消
|
||||
func Watch(ctx context.Context, logger *zap.Logger) {
|
||||
viper.WatchConfig()
|
||||
viper.OnConfigChange(func(e fsnotify.Event) {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return // 如果上下文被取消则停止处理
|
||||
default:
|
||||
logger.Info("配置文件已更改", zap.String("file", e.Name))
|
||||
|
||||
// 尝试重新加载
|
||||
newConfig, err := Reload()
|
||||
if err != nil {
|
||||
logger.Error("重新加载配置失败,保留先前配置",
|
||||
zap.Error(err),
|
||||
zap.String("file", e.Name),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("配置重新加载成功",
|
||||
zap.String("file", e.Name),
|
||||
zap.String("server_address", newConfig.Server.Address),
|
||||
zap.String("log_level", newConfig.Logging.Level),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// 阻塞直到上下文被取消
|
||||
<-ctx.Done()
|
||||
logger.Info("配置监听器已停止")
|
||||
}
|
||||
21
pkg/constants/constants.go
Normal file
21
pkg/constants/constants.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package constants
|
||||
|
||||
// Fiber Locals 的上下文键
|
||||
const (
|
||||
ContextKeyRequestID = "requestid"
|
||||
ContextKeyUserID = "user_id"
|
||||
ContextKeyStartTime = "start_time"
|
||||
)
|
||||
|
||||
// 配置环境变量
|
||||
const (
|
||||
EnvConfigPath = "CONFIG_PATH"
|
||||
EnvConfigEnv = "CONFIG_ENV" // dev, staging, prod
|
||||
)
|
||||
|
||||
// 默认配置值
|
||||
const (
|
||||
DefaultConfigPath = "configs/config.yaml"
|
||||
DefaultServerAddr = ":3000"
|
||||
DefaultRedisAddr = "localhost:6379"
|
||||
)
|
||||
13
pkg/constants/redis.go
Normal file
13
pkg/constants/redis.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package constants
|
||||
|
||||
import "fmt"
|
||||
|
||||
// RedisAuthTokenKey 生成认证令牌的 Redis 键
|
||||
func RedisAuthTokenKey(token string) string {
|
||||
return fmt.Sprintf("auth:token:%s", token)
|
||||
}
|
||||
|
||||
// RedisRateLimitKey 生成限流的 Redis 键
|
||||
func RedisRateLimitKey(ip string) string {
|
||||
return fmt.Sprintf("ratelimit:%s", ip)
|
||||
}
|
||||
53
pkg/database/redis.go
Normal file
53
pkg/database/redis.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// RedisConfig Redis 配置
|
||||
type RedisConfig struct {
|
||||
Address string
|
||||
Password string
|
||||
DB int
|
||||
PoolSize int
|
||||
MinIdleConns int
|
||||
DialTimeout time.Duration
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
}
|
||||
|
||||
// NewRedisClient 创建新的 Redis 客户端
|
||||
func NewRedisClient(cfg RedisConfig, logger *zap.Logger) (*redis.Client, error) {
|
||||
client := redis.NewClient(&redis.Options{
|
||||
Addr: cfg.Address,
|
||||
Password: cfg.Password,
|
||||
DB: cfg.DB,
|
||||
PoolSize: cfg.PoolSize,
|
||||
MinIdleConns: cfg.MinIdleConns,
|
||||
DialTimeout: cfg.DialTimeout,
|
||||
ReadTimeout: cfg.ReadTimeout,
|
||||
WriteTimeout: cfg.WriteTimeout,
|
||||
MaxRetries: 3,
|
||||
PoolTimeout: 4 * time.Second,
|
||||
})
|
||||
|
||||
// 测试连接
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if err := client.Ping(ctx).Err(); err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to redis: %w", err)
|
||||
}
|
||||
|
||||
logger.Info("Redis 连接成功",
|
||||
zap.String("address", cfg.Address),
|
||||
zap.Int("db", cfg.DB),
|
||||
)
|
||||
|
||||
return client, nil
|
||||
}
|
||||
39
pkg/errors/codes.go
Normal file
39
pkg/errors/codes.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package errors
|
||||
|
||||
// 应用错误码
|
||||
const (
|
||||
CodeSuccess = 0 // 成功
|
||||
CodeInternalError = 1000 // 内部服务器错误
|
||||
CodeMissingToken = 1001 // 缺失认证令牌
|
||||
CodeInvalidToken = 1002 // 令牌无效或已过期
|
||||
CodeTooManyRequests = 1003 // 请求过于频繁(限流)
|
||||
CodeAuthServiceUnavailable = 1004 // 认证服务不可用(Redis 宕机)
|
||||
)
|
||||
|
||||
// ErrorMessage 表示双语错误消息
|
||||
type ErrorMessage struct {
|
||||
EN string
|
||||
ZH string
|
||||
}
|
||||
|
||||
// errorMessages 将错误码映射到双语消息
|
||||
var errorMessages = map[int]ErrorMessage{
|
||||
CodeSuccess: {"Success", "成功"},
|
||||
CodeInternalError: {"Internal server error", "内部服务器错误"},
|
||||
CodeMissingToken: {"Missing authentication token", "缺失认证令牌"},
|
||||
CodeInvalidToken: {"Invalid or expired token", "令牌无效或已过期"},
|
||||
CodeTooManyRequests: {"Too many requests", "请求过于频繁"},
|
||||
CodeAuthServiceUnavailable: {"Authentication service unavailable", "认证服务不可用"},
|
||||
}
|
||||
|
||||
// GetMessage 根据错误码和语言返回错误消息
|
||||
func GetMessage(code int, lang string) string {
|
||||
msg, ok := errorMessages[code]
|
||||
if !ok {
|
||||
return "Unknown error"
|
||||
}
|
||||
if lang == "zh" || lang == "zh-CN" {
|
||||
return msg.ZH
|
||||
}
|
||||
return msg.EN
|
||||
}
|
||||
49
pkg/errors/errors.go
Normal file
49
pkg/errors/errors.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// 中间件标准错误类型
|
||||
var (
|
||||
ErrMissingToken = errors.New("missing authentication token")
|
||||
ErrInvalidToken = errors.New("invalid or expired token")
|
||||
ErrRedisUnavailable = errors.New("redis unavailable")
|
||||
ErrTooManyRequests = errors.New("too many requests")
|
||||
)
|
||||
|
||||
// AppError 表示带错误码的应用错误
|
||||
type AppError struct {
|
||||
Code int // 应用错误码
|
||||
Message string // 错误消息
|
||||
Err error // 底层错误(可选)
|
||||
}
|
||||
|
||||
func (e *AppError) Error() string {
|
||||
if e.Err != nil {
|
||||
return fmt.Sprintf("%s: %v", e.Message, e.Err)
|
||||
}
|
||||
return e.Message
|
||||
}
|
||||
|
||||
func (e *AppError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// New 创建新的 AppError
|
||||
func New(code int, message string) *AppError {
|
||||
return &AppError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap 用错误码和消息包装现有错误
|
||||
func Wrap(code int, message string, err error) *AppError {
|
||||
return &AppError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
146
pkg/logger/logger.go
Normal file
146
pkg/logger/logger.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"gopkg.in/natefinch/lumberjack.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
// appLogger 应用日志记录器
|
||||
appLogger *zap.Logger
|
||||
// accessLogger 访问日志记录器
|
||||
accessLogger *zap.Logger
|
||||
)
|
||||
|
||||
// InitLoggers 初始化应用和访问日志记录器
|
||||
func InitLoggers(
|
||||
level string,
|
||||
development bool,
|
||||
appLogConfig LogRotationConfig,
|
||||
accessLogConfig LogRotationConfig,
|
||||
) error {
|
||||
// 解析日志级别
|
||||
zapLevel := parseLevel(level)
|
||||
|
||||
// 创建编码器配置
|
||||
encoderConfig := zapcore.EncoderConfig{
|
||||
TimeKey: "timestamp",
|
||||
LevelKey: "level",
|
||||
NameKey: "logger",
|
||||
CallerKey: "caller",
|
||||
MessageKey: "message",
|
||||
StacktraceKey: "stacktrace",
|
||||
LineEnding: zapcore.DefaultLineEnding,
|
||||
EncodeLevel: zapcore.LowercaseLevelEncoder,
|
||||
EncodeTime: zapcore.ISO8601TimeEncoder, // RFC3339 格式
|
||||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||
}
|
||||
|
||||
// 选择编码器(开发模式使用控制台,生产使用 JSON)
|
||||
var encoder zapcore.Encoder
|
||||
if development {
|
||||
encoder = zapcore.NewConsoleEncoder(encoderConfig)
|
||||
} else {
|
||||
encoder = zapcore.NewJSONEncoder(encoderConfig)
|
||||
}
|
||||
|
||||
// 创建应用日志核心
|
||||
appCore := zapcore.NewCore(
|
||||
encoder,
|
||||
zapcore.AddSync(newLumberjackLogger(appLogConfig)),
|
||||
zapLevel,
|
||||
)
|
||||
|
||||
// 创建访问日志核心(始终使用 JSON)
|
||||
accessCore := zapcore.NewCore(
|
||||
zapcore.NewJSONEncoder(encoderConfig),
|
||||
zapcore.AddSync(newLumberjackLogger(accessLogConfig)),
|
||||
zapcore.InfoLevel, // 访问日志始终使用 info 级别
|
||||
)
|
||||
|
||||
// 构建日志记录器
|
||||
if development {
|
||||
// 开发模式:添加调用者信息和堆栈跟踪
|
||||
appLogger = zap.New(appCore, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel), zap.Development())
|
||||
} else {
|
||||
// 生产模式:添加调用者信息,仅在 error 时添加堆栈跟踪
|
||||
appLogger = zap.New(appCore, zap.AddCaller(), zap.AddStacktrace(zapcore.ErrorLevel))
|
||||
}
|
||||
|
||||
// 访问日志不需要调用者信息
|
||||
accessLogger = zap.New(accessCore)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAppLogger 返回应用日志记录器
|
||||
func GetAppLogger() *zap.Logger {
|
||||
if appLogger == nil {
|
||||
// 如果未初始化,返回 nop logger
|
||||
return zap.NewNop()
|
||||
}
|
||||
return appLogger
|
||||
}
|
||||
|
||||
// GetAccessLogger 返回访问日志记录器
|
||||
func GetAccessLogger() *zap.Logger {
|
||||
if accessLogger == nil {
|
||||
// 如果未初始化,返回 nop logger
|
||||
return zap.NewNop()
|
||||
}
|
||||
return accessLogger
|
||||
}
|
||||
|
||||
// Sync 刷新所有日志缓冲区
|
||||
func Sync() error {
|
||||
if appLogger != nil {
|
||||
if err := appLogger.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if accessLogger != nil {
|
||||
if err := accessLogger.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// parseLevel 解析日志级别字符串
|
||||
func parseLevel(level string) zapcore.Level {
|
||||
switch level {
|
||||
case "debug":
|
||||
return zapcore.DebugLevel
|
||||
case "info":
|
||||
return zapcore.InfoLevel
|
||||
case "warn":
|
||||
return zapcore.WarnLevel
|
||||
case "error":
|
||||
return zapcore.ErrorLevel
|
||||
default:
|
||||
return zapcore.InfoLevel
|
||||
}
|
||||
}
|
||||
|
||||
// newLumberjackLogger 创建 Lumberjack 日志轮转器
|
||||
func newLumberjackLogger(config LogRotationConfig) *lumberjack.Logger {
|
||||
return &lumberjack.Logger{
|
||||
Filename: config.Filename,
|
||||
MaxSize: config.MaxSize,
|
||||
MaxBackups: config.MaxBackups,
|
||||
MaxAge: config.MaxAge,
|
||||
Compress: config.Compress,
|
||||
LocalTime: true, // 使用本地时间
|
||||
}
|
||||
}
|
||||
|
||||
// LogRotationConfig 日志轮转配置(从 config 包复制以避免循环依赖)
|
||||
type LogRotationConfig struct {
|
||||
Filename string
|
||||
MaxSize int
|
||||
MaxBackups int
|
||||
MaxAge int
|
||||
Compress bool
|
||||
}
|
||||
52
pkg/logger/middleware.go
Normal file
52
pkg/logger/middleware.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Middleware 创建 Fiber 日志中间件
|
||||
// 记录所有 HTTP 请求到访问日志
|
||||
func Middleware() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
// 记录请求开始时间
|
||||
startTime := time.Now()
|
||||
c.Locals(constants.ContextKeyStartTime, startTime)
|
||||
|
||||
// 处理请求
|
||||
err := c.Next()
|
||||
|
||||
// 计算请求持续时间
|
||||
duration := time.Since(startTime)
|
||||
|
||||
// 获取请求 ID(由 requestid 中间件设置)
|
||||
requestID := ""
|
||||
if rid := c.Locals(constants.ContextKeyRequestID); rid != nil {
|
||||
requestID = rid.(string)
|
||||
}
|
||||
|
||||
// 获取用户 ID(由 auth 中间件设置)
|
||||
userID := ""
|
||||
if uid := c.Locals(constants.ContextKeyUserID); uid != nil {
|
||||
userID = uid.(string)
|
||||
}
|
||||
|
||||
// 记录访问日志
|
||||
accessLogger := GetAccessLogger()
|
||||
accessLogger.Info("",
|
||||
zap.String("method", c.Method()),
|
||||
zap.String("path", c.Path()),
|
||||
zap.Int("status", c.Response().StatusCode()),
|
||||
zap.Float64("duration_ms", float64(duration.Microseconds())/1000.0),
|
||||
zap.String("request_id", requestID),
|
||||
zap.String("ip", c.IP()),
|
||||
zap.String("user_agent", c.Get("User-Agent")),
|
||||
zap.String(constants.ContextKeyUserID, userID),
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
}
|
||||
46
pkg/response/response.go
Normal file
46
pkg/response/response.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
// Response 统一 API 响应结构
|
||||
type Response struct {
|
||||
Code int `json:"code"` // 应用错误码(0 = 成功)
|
||||
Data any `json:"data"` // 响应数据(对象、数组或 null)
|
||||
Message string `json:"msg"` // 可读消息
|
||||
Timestamp string `json:"timestamp"` // ISO 8601 时间戳
|
||||
}
|
||||
|
||||
// Success 返回成功响应
|
||||
func Success(c *fiber.Ctx, data any) error {
|
||||
return c.JSON(Response{
|
||||
Code: errors.CodeSuccess,
|
||||
Data: data,
|
||||
Message: "success",
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
// Error 返回错误响应
|
||||
func Error(c *fiber.Ctx, httpStatus int, code int, message string) error {
|
||||
return c.Status(httpStatus).JSON(Response{
|
||||
Code: code,
|
||||
Data: nil,
|
||||
Message: message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
|
||||
// SuccessWithMessage 返回带自定义消息的成功响应
|
||||
func SuccessWithMessage(c *fiber.Ctx, data any, message string) error {
|
||||
return c.JSON(Response{
|
||||
Code: errors.CodeSuccess,
|
||||
Data: data,
|
||||
Message: message,
|
||||
Timestamp: time.Now().Format(time.RFC3339),
|
||||
})
|
||||
}
|
||||
65
pkg/validator/token.go
Normal file
65
pkg/validator/token.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package validator
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
"go.uber.org/zap"
|
||||
|
||||
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
||||
"github.com/break/junhong_cmp_fiber/pkg/errors"
|
||||
)
|
||||
|
||||
// TokenValidator 令牌验证器
|
||||
type TokenValidator struct {
|
||||
redis *redis.Client
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NewTokenValidator 创建新的令牌验证器
|
||||
func NewTokenValidator(rdb *redis.Client, logger *zap.Logger) *TokenValidator {
|
||||
return &TokenValidator{
|
||||
redis: rdb,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate 验证令牌并返回用户 ID
|
||||
func (v *TokenValidator) Validate(token string) (string, error) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
// 检查 Redis 可用性(失败关闭策略)
|
||||
if err := v.redis.Ping(ctx).Err(); err != nil {
|
||||
v.logger.Error("Redis 不可用",
|
||||
zap.Error(err),
|
||||
)
|
||||
return "", errors.ErrRedisUnavailable
|
||||
}
|
||||
|
||||
// 从 Redis 获取用户 ID
|
||||
userID, err := v.redis.Get(ctx, constants.RedisAuthTokenKey(token)).Result()
|
||||
if err == redis.Nil {
|
||||
// 令牌不存在或已过期
|
||||
return "", errors.ErrInvalidToken
|
||||
}
|
||||
if err != nil {
|
||||
v.logger.Error("Redis 获取失败",
|
||||
zap.Error(err),
|
||||
zap.String("token_key", constants.RedisAuthTokenKey(token)),
|
||||
)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return userID, nil
|
||||
}
|
||||
|
||||
// IsAvailable 检查 Redis 是否可用
|
||||
func (v *TokenValidator) IsAvailable() bool {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
err := v.redis.Ping(ctx).Err()
|
||||
return err == nil
|
||||
}
|
||||
Reference in New Issue
Block a user