add
This commit is contained in:
@@ -0,0 +1,240 @@
|
|||||||
|
// common/logger/logger.go
|
||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 全局单例日志实例
|
||||||
|
var (
|
||||||
|
_logger *zap.Logger
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoggerOptions 日志配置项
|
||||||
|
type LoggerOptions struct {
|
||||||
|
Level string // 日志级别:debug/info/warn/error/panic/fatal
|
||||||
|
Format string // 输出格式:console(控制台)/json(JSON)
|
||||||
|
OutputPath string // 文件输出路径(如./logs/app.log)
|
||||||
|
MaxSize int // 单个日志文件最大大小(MB)
|
||||||
|
MaxBackups int // 最大保留日志文件数
|
||||||
|
MaxAge int // 最大保留天数
|
||||||
|
Compress bool // 是否压缩日志文件
|
||||||
|
ShowLine bool // 是否显示代码行号
|
||||||
|
ConsoleColor bool // 控制台是否显示彩色(开发环境用)
|
||||||
|
ServiceName string // 服务名(多业务时区分日志归属,如user/order)
|
||||||
|
StacktraceKey string // 堆栈信息键名
|
||||||
|
CallerSkip int // 调用栈跳过数(适配封装层,正确显示业务代码行号)
|
||||||
|
FlushInterval time.Duration // 日志刷盘间隔
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoggerOption 选项模式函数类型
|
||||||
|
type LoggerOption func(*LoggerOptions)
|
||||||
|
|
||||||
|
// defaultLoggerOptions 初始化默认配置
|
||||||
|
// 开发环境默认:控制台彩色日志、info级别、显示行号
|
||||||
|
func defaultLoggerOptions() *LoggerOptions {
|
||||||
|
return &LoggerOptions{
|
||||||
|
Level: "info",
|
||||||
|
Format: "console",
|
||||||
|
OutputPath: "./logs/app.log",
|
||||||
|
MaxSize: 100, // 单个文件100MB
|
||||||
|
MaxBackups: 10, // 保留10个备份
|
||||||
|
MaxAge: 7, // 保留7天
|
||||||
|
Compress: true, // 压缩备份文件
|
||||||
|
ShowLine: true, // 显示行号
|
||||||
|
ConsoleColor: true, // 控制台彩色
|
||||||
|
ServiceName: "trading_assistant", // 默认服务名
|
||||||
|
StacktraceKey: "stacktrace",
|
||||||
|
CallerSkip: 1, // 跳过当前封装层,正确显示业务代码行号
|
||||||
|
FlushInterval: 3 * time.Second,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 以下为配置项设置函数,支持链式调用
|
||||||
|
func WithLevel(level string) LoggerOption {
|
||||||
|
return func(o *LoggerOptions) { o.Level = level }
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithFormat(format string) LoggerOption {
|
||||||
|
return func(o *LoggerOptions) { o.Format = format }
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithOutputPath(path string) LoggerOption {
|
||||||
|
return func(o *LoggerOptions) { o.OutputPath = path }
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithServiceName(name string) LoggerOption {
|
||||||
|
return func(o *LoggerOptions) { o.ServiceName = name }
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithShowLine(show bool) LoggerOption {
|
||||||
|
return func(o *LoggerOptions) { o.ShowLine = show }
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithConsoleColor(color bool) LoggerOption {
|
||||||
|
return func(o *LoggerOptions) { o.ConsoleColor = color }
|
||||||
|
}
|
||||||
|
|
||||||
|
// getZapLevel 转换日志级别为zapcore.Level
|
||||||
|
func getZapLevel(level string) zapcore.Level {
|
||||||
|
switch level {
|
||||||
|
case "debug":
|
||||||
|
return zapcore.DebugLevel
|
||||||
|
case "warn":
|
||||||
|
return zapcore.WarnLevel
|
||||||
|
case "error":
|
||||||
|
return zapcore.ErrorLevel
|
||||||
|
case "panic":
|
||||||
|
return zapcore.PanicLevel
|
||||||
|
case "fatal":
|
||||||
|
return zapcore.FatalLevel
|
||||||
|
default:
|
||||||
|
return zapcore.InfoLevel // 默认info级别
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getEncoder 获取日志编码器(console/json)
|
||||||
|
func getEncoder(opts *LoggerOptions) zapcore.Encoder {
|
||||||
|
// 日志基础配置:时间格式、服务名、调用栈等
|
||||||
|
encoderConfig := zapcore.EncoderConfig{
|
||||||
|
TimeKey: "time",
|
||||||
|
LevelKey: "level",
|
||||||
|
NameKey: "service",
|
||||||
|
CallerKey: "caller",
|
||||||
|
MessageKey: "msg",
|
||||||
|
StacktraceKey: opts.StacktraceKey,
|
||||||
|
LineEnding: zapcore.DefaultLineEnding,
|
||||||
|
EncodeLevel: zapcore.CapitalLevelEncoder, // 级别大写(INFO/ERROR)
|
||||||
|
EncodeTime: zapcore.RFC3339TimeEncoder, // 时间格式RFC3339
|
||||||
|
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||||
|
EncodeCaller: zapcore.ShortCallerEncoder, // 调用者格式:文件:行号
|
||||||
|
}
|
||||||
|
|
||||||
|
// 开发环境:控制台彩色编码器
|
||||||
|
if opts.Format == "console" && opts.ConsoleColor {
|
||||||
|
return zapcore.NewConsoleEncoder(encoderConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生产环境:JSON编码器(便于日志收集分析,如ELK)
|
||||||
|
return zapcore.NewJSONEncoder(encoderConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// getWriteSyncer 获取日志写入器(文件+控制台)
|
||||||
|
// 同时输出到文件和控制台,文件自动切割
|
||||||
|
func getWriteSyncer(opts *LoggerOptions) zapcore.WriteSyncer {
|
||||||
|
// 日志文件切割配置(基于lumberjack)
|
||||||
|
lumberjackLogger := &lumberjack.Logger{
|
||||||
|
Filename: opts.OutputPath,
|
||||||
|
MaxSize: opts.MaxSize,
|
||||||
|
MaxBackups: opts.MaxBackups,
|
||||||
|
MaxAge: opts.MaxAge,
|
||||||
|
Compress: opts.Compress,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 同时输出到文件和控制台
|
||||||
|
return zapcore.NewMultiWriteSyncer(
|
||||||
|
zapcore.AddSync(os.Stdout),
|
||||||
|
zapcore.AddSync(lumberjackLogger),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitLogger 初始化全局单例日志实例
|
||||||
|
func InitLogger(opts ...LoggerOption) *zap.Logger {
|
||||||
|
once.Do(func() {
|
||||||
|
// 加载默认配置 + 覆盖用户自定义配置
|
||||||
|
options := defaultLoggerOptions()
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. 设置日志级别
|
||||||
|
level := getZapLevel(options.Level)
|
||||||
|
core := zapcore.NewCore(
|
||||||
|
getEncoder(options), // 编码器
|
||||||
|
getWriteSyncer(options), // 写入器
|
||||||
|
level, // 日志级别
|
||||||
|
)
|
||||||
|
|
||||||
|
// 2. 构建日志实例配置:是否显示调用者、堆栈信息
|
||||||
|
zapOpts := []zap.Option{zap.AddCallerSkip(options.CallerSkip)}
|
||||||
|
if options.ShowLine {
|
||||||
|
zapOpts = append(zapOpts, zap.AddCaller()) // 显示调用者(文件:行号)
|
||||||
|
}
|
||||||
|
// 错误级别及以上显示堆栈信息
|
||||||
|
zapOpts = append(zapOpts, zap.AddStacktrace(zapcore.ErrorLevel))
|
||||||
|
// 设置服务名
|
||||||
|
zapOpts = append(zapOpts, zap.Fields(zap.String("service", options.ServiceName)))
|
||||||
|
|
||||||
|
// 3. 创建日志实例
|
||||||
|
_logger = zap.New(core, zapOpts...)
|
||||||
|
|
||||||
|
// 4. 定时刷盘(避免日志驻留内存)
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(options.FlushInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
_ = _logger.Sync()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// 5. 应用退出时刷盘
|
||||||
|
os.Setenv("ZAP_FLUSH_ON_EXIT", "true")
|
||||||
|
})
|
||||||
|
|
||||||
|
return _logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLogger 获取全局单例日志实例
|
||||||
|
func GetLogger() *zap.Logger {
|
||||||
|
if _logger == nil {
|
||||||
|
// 未初始化时,返回默认控制台日志(兜底)
|
||||||
|
return InitLogger()
|
||||||
|
}
|
||||||
|
return _logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------- 轻量封装日志方法 ---------------
|
||||||
|
// 简化业务层调用,无需每次获取logger实例
|
||||||
|
// 复杂日志(如带字段)可直接使用GetLogger()获取原生实例
|
||||||
|
|
||||||
|
// Debug 调试日志
|
||||||
|
func Debug(msg string, fields ...zap.Field) {
|
||||||
|
GetLogger().Debug(msg, fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info 信息日志
|
||||||
|
func Info(msg string, fields ...zap.Field) {
|
||||||
|
GetLogger().Info(msg, fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn 警告日志
|
||||||
|
func Warn(msg string, fields ...zap.Field) {
|
||||||
|
GetLogger().Warn(msg, fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error 错误日志(带堆栈)
|
||||||
|
func Error(msg string, fields ...zap.Field) {
|
||||||
|
GetLogger().Error(msg, fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Panic 恐慌日志(打印后触发panic)
|
||||||
|
func Panic(msg string, fields ...zap.Field) {
|
||||||
|
GetLogger().Panic(msg, fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatal 致命日志(打印后退出程序)
|
||||||
|
func Fatal(msg string, fields ...zap.Field) {
|
||||||
|
GetLogger().Fatal(msg, fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync 手动刷盘(如重要操作后)
|
||||||
|
func Sync() error {
|
||||||
|
return GetLogger().Sync()
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
// common/middleware/cors.go
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CORSOptions 跨域配置项
|
||||||
|
// 支持自定义允许的源、方法、头、凭证、缓存时间,按需扩展
|
||||||
|
type CORSOptions struct {
|
||||||
|
AllowOrigins []string // 允许的跨域源,如["http://localhost:8080", "https://xxx.com"],*表示允许所有
|
||||||
|
AllowMethods []string // 允许的HTTP方法,默认GET/POST/PUT/DELETE/PATCH/OPTIONS
|
||||||
|
AllowHeaders []string // 允许的请求头,*表示允许所有
|
||||||
|
AllowCredentials bool // 是否允许携带凭证(Cookie/Token),前后端联调必备
|
||||||
|
ExposeHeaders []string // 允许前端获取的响应头
|
||||||
|
MaxAge time.Duration // 预检请求(OPTIONS)的缓存时间,避免重复预检
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORSOption 选项模式函数类型,用于灵活配置跨域参数
|
||||||
|
type CORSOption func(*CORSOptions)
|
||||||
|
|
||||||
|
// defaultCORSOptions 初始化默认跨域配置
|
||||||
|
// 开发环境默认允许所有源、常用方法,生产环境可通过配置覆盖
|
||||||
|
func defaultCORSOptions() *CORSOptions {
|
||||||
|
return &CORSOptions{
|
||||||
|
AllowOrigins: []string{"*"}, // 开发环境默认允许所有源
|
||||||
|
AllowMethods: []string{http.MethodGet, http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch, http.MethodOptions},
|
||||||
|
AllowHeaders: []string{"*"}, // 允许所有请求头
|
||||||
|
AllowCredentials: true, // 允许携带凭证
|
||||||
|
ExposeHeaders: []string{"Content-Length", "Content-Type", "X-Token"},
|
||||||
|
MaxAge: 12 * time.Hour, // 预检请求缓存12小时
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 以下为配置项设置函数,支持链式调用
|
||||||
|
// WithAllowOrigins 设置允许的跨域源,示例:WithAllowOrigins("http://localhost:3000", "https://app.com")
|
||||||
|
func WithAllowOrigins(origins ...string) CORSOption {
|
||||||
|
return func(o *CORSOptions) { o.AllowOrigins = origins }
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAllowCredentials 设置是否允许携带凭证(Cookie/Token)
|
||||||
|
func WithAllowCredentials(allow bool) CORSOption {
|
||||||
|
return func(o *CORSOptions) { o.AllowCredentials = allow }
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMaxAge 设置预检请求缓存时间
|
||||||
|
func WithMaxAge(age time.Duration) CORSOption {
|
||||||
|
return func(o *CORSOptions) { o.MaxAge = age }
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithAllowHeaders 设置允许的请求头
|
||||||
|
func WithAllowHeaders(headers ...string) CORSOption {
|
||||||
|
return func(o *CORSOptions) { o.AllowHeaders = headers }
|
||||||
|
}
|
||||||
|
|
||||||
|
// CORS 跨域中间件核心方法
|
||||||
|
// 适配Go原生http.Handler,可直接用于Gin/Echo等框架(兼容框架中间件规范)
|
||||||
|
func CORS(opts ...CORSOption) func(http.Handler) http.Handler {
|
||||||
|
// 加载默认配置 + 覆盖用户自定义配置
|
||||||
|
options := defaultCORSOptions()
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理允许的源:拼接为字符串
|
||||||
|
allowOrigins := strings.Join(options.AllowOrigins, ", ")
|
||||||
|
// 处理允许的方法:拼接为字符串
|
||||||
|
allowMethods := strings.Join(options.AllowMethods, ", ")
|
||||||
|
// 处理允许的请求头:拼接为字符串
|
||||||
|
allowHeaders := strings.Join(options.AllowHeaders, ", ")
|
||||||
|
// 处理允许暴露的响应头:拼接为字符串
|
||||||
|
exposeHeaders := strings.Join(options.ExposeHeaders, ", ")
|
||||||
|
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// 1. 获取前端请求的Origin(跨域核心)
|
||||||
|
origin := r.Header.Get("Origin")
|
||||||
|
// 若配置了*,则直接使用请求的Origin;否则使用配置的源(生产环境建议精准配置)
|
||||||
|
if len(options.AllowOrigins) > 0 && options.AllowOrigins[0] == "*" {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||||
|
} else {
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", allowOrigins)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 设置跨域核心响应头
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", allowMethods)
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", allowHeaders)
|
||||||
|
w.Header().Set("Access-Control-Expose-Headers", exposeHeaders)
|
||||||
|
w.Header().Set("Access-Control-Max-Age", string(rune(options.MaxAge.Seconds())))
|
||||||
|
// 允许携带凭证时,不能将Allow-Origin设为*,需动态匹配请求Origin(已做处理)
|
||||||
|
if options.AllowCredentials {
|
||||||
|
w.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 处理预检请求(OPTIONS):直接返回204,无需执行后续业务逻辑
|
||||||
|
if r.Method == http.MethodOptions {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 非预检请求,执行后续业务逻辑
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- 兼容Gin框架的快捷中间件(可选)----
|
||||||
|
// 若团队使用Gin框架开发,可直接使用此方法,无需额外转换,提升开发效率
|
||||||
|
// 需提前安装Gin:go get github.com/gin-gonic/gin
|
||||||
|
import (
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CorsGin 适配Gin框架的跨域中间件
|
||||||
|
func CorsGin(opts ...CORSOption) gin.HandlerFunc {
|
||||||
|
// 复用原生CORS配置逻辑
|
||||||
|
options := defaultCORSOptions()
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(options)
|
||||||
|
}
|
||||||
|
allowOrigins := strings.Join(options.AllowOrigins, ", ")
|
||||||
|
allowMethods := strings.Join(options.Methods, ", ")
|
||||||
|
allowHeaders := strings.Join(options.AllowHeaders, ", ")
|
||||||
|
exposeHeaders := strings.Join(options.ExposeHeaders, ", ")
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
origin := c.Request.Header.Get("Origin")
|
||||||
|
if options.AllowOrigins[0] == "*" {
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
|
||||||
|
} else {
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Origin", allowOrigins)
|
||||||
|
}
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Methods", allowMethods)
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Headers", allowHeaders)
|
||||||
|
c.Writer.Header().Set("Access-Control-Expose-Headers", exposeHeaders)
|
||||||
|
c.Writer.Header().Set("Access-Control-Max-Age", string(rune(options.MaxAge.Seconds())))
|
||||||
|
if options.AllowCredentials {
|
||||||
|
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理预检请求
|
||||||
|
if c.Request.Method == http.MethodOptions {
|
||||||
|
c.AbortWithStatus(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 继续执行后续中间件/业务逻辑
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,220 @@
|
|||||||
|
// common/redis/redis.go
|
||||||
|
package redis
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 全局单例Redis客户端(连接池)
|
||||||
|
var (
|
||||||
|
clientInstance *redis.Client
|
||||||
|
once sync.Once
|
||||||
|
initErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
// RedisOptions Redis连接配置项
|
||||||
|
// 包含基础连接信息+连接池配置,按需配置
|
||||||
|
type RedisOptions struct {
|
||||||
|
Host string // Redis地址
|
||||||
|
Port string // Redis端口
|
||||||
|
Password string // Redis密码
|
||||||
|
DB int // 数据库索引(建议每个业务一个DB,如user=0, order=1)
|
||||||
|
PoolSize int // 连接池最大连接数
|
||||||
|
MinIdleConns int // 连接池最小空闲连接数
|
||||||
|
ConnTimeout time.Duration // 连接超时时间
|
||||||
|
ReadTimeout time.Duration // 读超时时间
|
||||||
|
WriteTimeout time.Duration // 写超时时间
|
||||||
|
IdleTimeout time.Duration // 连接空闲超时时间
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedisOption 选项模式函数类型
|
||||||
|
type RedisOption func(*RedisOptions)
|
||||||
|
|
||||||
|
// defaultRedisOptions 初始化默认配置
|
||||||
|
// 开发环境直接对接根目录docker-compose的redis服务,无需额外配置
|
||||||
|
func defaultRedisOptions() *RedisOptions {
|
||||||
|
return &RedisOptions{
|
||||||
|
Host: "redis", // 匹配Compose服务名
|
||||||
|
Port: "6379", // 默认端口
|
||||||
|
Password: "", // 开发环境默认无密码
|
||||||
|
DB: 0, // 默认DB0
|
||||||
|
PoolSize: 50, // 连接池最大连接数
|
||||||
|
MinIdleConns: 10, // 最小空闲连接,避免频繁创建连接
|
||||||
|
ConnTimeout: 5 * time.Second,
|
||||||
|
ReadTimeout: 3 * time.Second,
|
||||||
|
WriteTimeout: 3 * time.Second,
|
||||||
|
IdleTimeout: 30 * time.Minute,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 以下为配置项设置函数,支持链式调用
|
||||||
|
func WithHost(host string) RedisOption {
|
||||||
|
return func(o *RedisOptions) { o.Host = host }
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPort(port string) RedisOption {
|
||||||
|
return func(o *RedisOptions) { o.Port = port }
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPassword(pwd string) RedisOption {
|
||||||
|
return func(o *RedisOptions) { o.Password = pwd }
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithDB(db int) RedisOption {
|
||||||
|
return func(o *RedisOptions) { o.DB = db }
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithPoolSize(size int) RedisOption {
|
||||||
|
return func(o *RedisOptions) { o.PoolSize = size }
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithLogLevel(level redis.LogLevel) RedisOption {
|
||||||
|
return func(o *RedisOptions) {
|
||||||
|
// 兼容日志级别配置,需配合客户端初始化
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// InitRedis 初始化Redis单例客户端(连接池)
|
||||||
|
// 选项模式配置,仅执行一次,全局复用连接池
|
||||||
|
func InitRedis(opts ...RedisOption) (*redis.Client, error) {
|
||||||
|
once.Do(func() {
|
||||||
|
// 加载默认配置 + 覆盖用户自定义配置
|
||||||
|
options := defaultRedisOptions()
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建Redis客户端配置
|
||||||
|
rdbOpts := &redis.Options{
|
||||||
|
Addr: fmt.Sprintf("%s:%s", options.Host, options.Port),
|
||||||
|
Password: options.Password,
|
||||||
|
DB: options.DB,
|
||||||
|
PoolSize: options.PoolSize,
|
||||||
|
MinIdleConns: options.MinIdleConns,
|
||||||
|
DialTimeout: options.ConnTimeout,
|
||||||
|
ReadTimeout: options.ReadTimeout,
|
||||||
|
WriteTimeout: options.WriteTimeout,
|
||||||
|
IdleTimeout: options.IdleTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建客户端实例
|
||||||
|
clientInstance = redis.NewClient(rdbOpts)
|
||||||
|
|
||||||
|
// 测试连接,确保连接有效
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if _, err := clientInstance.Ping(ctx).Result(); err != nil {
|
||||||
|
initErr = fmt.Errorf("redis ping failed: %w", err)
|
||||||
|
clientInstance = nil // 连接失败置空实例
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
initErr = nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return clientInstance, initErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRedis 获取全局Redis单例客户端
|
||||||
|
// 业务层初始化后,直接调用获取,无需重复初始化
|
||||||
|
func GetRedis() (*redis.Client, error) {
|
||||||
|
if clientInstance == nil {
|
||||||
|
return nil, errors.New("redis not initialized, please call InitRedis first")
|
||||||
|
}
|
||||||
|
return clientInstance, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CloseRedis 关闭Redis连接池(应用退出时调用,优雅释放资源)
|
||||||
|
func CloseRedis() error {
|
||||||
|
if clientInstance == nil {
|
||||||
|
return errors.New("redis not initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := clientInstance.Close(); err != nil {
|
||||||
|
return fmt.Errorf("close redis failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置单例,避免重复关闭
|
||||||
|
once = sync.Once{}
|
||||||
|
clientInstance = nil
|
||||||
|
initErr = errors.New("redis client closed")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// --------------- 基础操作封装 ---------------
|
||||||
|
// 轻量封装高频基础操作,简化业务层调用(无需关注context和错误处理基础逻辑)
|
||||||
|
// 复杂操作(如哈希、列表、事务)直接使用原生*redis.Client即可
|
||||||
|
|
||||||
|
// Ctx 全局默认context,业务层可自定义传入
|
||||||
|
var Ctx = context.Background()
|
||||||
|
|
||||||
|
// Set 封装Set操作,带过期时间
|
||||||
|
// key: 键, val: 值, expire: 过期时间(0表示永不过期)
|
||||||
|
func Set(key string, val interface{}, expire time.Duration) error {
|
||||||
|
rdb, err := GetRedis()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return rdb.Set(Ctx, key, val, expire).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 封装Get操作,返回字符串值
|
||||||
|
func Get(key string) (string, error) {
|
||||||
|
rdb, err := GetRedis()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return rdb.Get(Ctx, key).Result()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Del 封装Del操作,删除一个或多个键
|
||||||
|
func Del(keys ...string) (int64, error) {
|
||||||
|
rdb, err := GetRedis()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return rdb.Del(Ctx, keys...).Result()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists 检查键是否存在
|
||||||
|
func Exists(key string) (bool, error) {
|
||||||
|
rdb, err := GetRedis()
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
res, err := rdb.Exists(Ctx, key).Result()
|
||||||
|
return res > 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expire 为键设置过期时间
|
||||||
|
func Expire(key string, expire time.Duration) error {
|
||||||
|
rdb, err := GetRedis()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return rdb.Expire(Ctx, key, expire).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HSet 封装哈希Set操作,设置单个字段
|
||||||
|
func HSet(key, field string, val interface{}) error {
|
||||||
|
rdb, err := GetRedis()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return rdb.HSet(Ctx, key, field, val).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// HGet 封装哈希Get操作,获取单个字段值
|
||||||
|
func HGet(key, field string) (string, error) {
|
||||||
|
rdb, err := GetRedis()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return rdb.HGet(Ctx, key, field).Result()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user