From 09192205bdbadbda7a1467cfbd8b3c136dfd2927 Mon Sep 17 00:00:00 2001 From: vipg Date: Fri, 6 Feb 2026 16:48:36 +0800 Subject: [PATCH] add --- trading_assistant_api/common/logger/logger.go | 240 ++++++++++++++++++ .../common/middleware/cors.go | 152 +++++++++++ trading_assistant_api/common/redis/redis.go | 220 ++++++++++++++++ 3 files changed, 612 insertions(+) diff --git a/trading_assistant_api/common/logger/logger.go b/trading_assistant_api/common/logger/logger.go index e69de29..c5e3750 100644 --- a/trading_assistant_api/common/logger/logger.go +++ b/trading_assistant_api/common/logger/logger.go @@ -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() +} \ No newline at end of file diff --git a/trading_assistant_api/common/middleware/cors.go b/trading_assistant_api/common/middleware/cors.go index e69de29..0e754a7 100644 --- a/trading_assistant_api/common/middleware/cors.go +++ b/trading_assistant_api/common/middleware/cors.go @@ -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() + } +} \ No newline at end of file diff --git a/trading_assistant_api/common/redis/redis.go b/trading_assistant_api/common/redis/redis.go index e69de29..28e1300 100644 --- a/trading_assistant_api/common/redis/redis.go +++ b/trading_assistant_api/common/redis/redis.go @@ -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() +} \ No newline at end of file