From d0fa98510ef8ff507511e7a1f0c06eaf9d97a182 Mon Sep 17 00:00:00 2001 From: tangmengqiu <1124090103@qq.com> Date: Tue, 4 Nov 2025 23:48:32 -0500 Subject: [PATCH] log: add logrus log lib and add telegram notification push as an option --- config.json.example | 5 +- config/config.go | 15 +++ go.mod | 2 + go.sum | 6 ++ logger/config.go | 64 +++++++++++ logger/config.telegram.json | 33 ++++++ logger/logger.go | 210 ++++++++++++++++++++++++++++++++++++ logger/telegram_hook.go | 158 +++++++++++++++++++++++++++ logger/telegram_sender.go | 120 +++++++++++++++++++++ main.go | 56 ++++++---- 10 files changed, 648 insertions(+), 21 deletions(-) create mode 100644 logger/config.go create mode 100644 logger/config.telegram.json create mode 100644 logger/logger.go create mode 100644 logger/telegram_hook.go create mode 100644 logger/telegram_sender.go diff --git a/config.json.example b/config.json.example index fefa1673..820f39a7 100644 --- a/config.json.example +++ b/config.json.example @@ -20,5 +20,8 @@ "max_daily_loss": 10.0, "max_drawdown": 20.0, "stop_trading_minutes": 60, - "jwt_secret": "Qk0kAa+d0iIEzXVHXbNbm+UaN3RNabmWtH8rDWZ5OPf+4GX8pBflAHodfpbipVMyrw1fsDanHsNBjhgbDeK9Jg==" + "jwt_secret": "Qk0kAa+d0iIEzXVHXbNbm+UaN3RNabmWtH8rDWZ5OPf+4GX8pBflAHodfpbipVMyrw1fsDanHsNBjhgbDeK9Jg==", + "log": { + "level": "info" + } } \ No newline at end of file diff --git a/config/config.go b/config/config.go index 37a537db..b913212f 100644 --- a/config/config.go +++ b/config/config.go @@ -50,6 +50,20 @@ type LeverageConfig struct { AltcoinLeverage int `json:"altcoin_leverage"` // 山寨币的杠杆倍数(主账户建议5-20,子账户≤5) } +// LogConfig 日志配置 +type LogConfig struct { + Level string `json:"level"` // 日志级别: debug, info, warn, error (默认: info) + Telegram *TelegramConfig `json:"telegram"` // Telegram推送配置(可选) +} + +// TelegramConfig Telegram推送配置(简化版,只保留必需字段) +type TelegramConfig struct { + Enabled bool `json:"enabled"` // 是否启用(默认: false) + BotToken string `json:"bot_token"` // Bot Token + ChatID int64 `json:"chat_id"` // Chat ID + MinLevel string `json:"min_level"` // 最低日志级别,该级别及以上的日志会推送到Telegram(可选,默认: error) +} + // Config 总配置 type Config struct { Traders []TraderConfig `json:"traders"` @@ -60,6 +74,7 @@ type Config struct { MaxDrawdown float64 `json:"max_drawdown"` StopTradingMinutes int `json:"stop_trading_minutes"` Leverage LeverageConfig `json:"leverage"` // 杠杆配置 + Log *LogConfig `json:"log"` // 日志配置(可选) } // LoadConfig 从文件加载配置 diff --git a/go.mod b/go.mod index 72291ee0..26362844 100644 --- a/go.mod +++ b/go.mod @@ -6,11 +6,13 @@ require ( github.com/adshao/go-binance/v2 v2.8.7 github.com/ethereum/go-ethereum v1.16.5 github.com/gin-gonic/gin v1.11.0 + github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 github.com/golang-jwt/jwt/v5 v5.2.0 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 github.com/mattn/go-sqlite3 v1.14.16 github.com/pquerna/otp v1.4.0 + github.com/sirupsen/logrus v1.9.3 github.com/sonirico/go-hyperliquid v0.17.0 golang.org/x/crypto v0.42.0 ) diff --git a/go.sum b/go.sum index 655fcf92..84fec2cf 100644 --- a/go.sum +++ b/go.sum @@ -62,6 +62,8 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc= +github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8= github.com/goccy/go-json v0.10.4 h1:JSwxQzIqKfmFX1swYPpUThQZp/Ka4wzJdK0LWVytLPM= github.com/goccy/go-json v0.10.4/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= @@ -155,6 +157,8 @@ github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible h1 github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sonirico/go-hyperliquid v0.17.0 h1:eXYACWupwu41O1VtKw17dqe9oOLQ1A2nRElGhg5Ox+4= github.com/sonirico/go-hyperliquid v0.17.0/go.mod h1:sH51Vsu+tPUwc95TL2MoQ8YXSewLWBEJirgzo7sZx6w= github.com/sonirico/vago v0.9.0 h1:DF2OWW2Aaf1xPZmnFv79kBrHmjKX3mVvMbP08vERlKo= @@ -167,6 +171,7 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= @@ -206,6 +211,7 @@ golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/logger/config.go b/logger/config.go new file mode 100644 index 00000000..32774558 --- /dev/null +++ b/logger/config.go @@ -0,0 +1,64 @@ +package logger + +import ( + "github.com/sirupsen/logrus" +) + +// Config 日志配置(简化版) +type Config struct { + Level string `json:"level"` // 日志级别: debug, info, warn, error (默认: info) + Telegram *TelegramConfig `json:"telegram"` // Telegram推送配置(可选) +} + +// TelegramConfig Telegram推送配置(简化版,高级参数使用默认值) +type TelegramConfig struct { + Enabled bool `json:"enabled"` // 是否启用(默认: false) + BotToken string `json:"bot_token"` // Bot Token + ChatID int64 `json:"chat_id"` // Chat ID + MinLevel string `json:"min_level"` // 最低日志级别,该级别及以上的日志会推送到Telegram(可选,默认: error) +} + +// SetDefaults 设置默认值 +func (c *Config) SetDefaults() { + if c.Level == "" { + c.Level = "info" + } +} + +// GetLogrusLevels 返回要推送到Telegram的日志级别 +// 根据配置的MinLevel返回该级别及以上的所有日志级别 +// 如果未配置或配置无效,默认返回error, fatal, panic(向后兼容) +func (tc *TelegramConfig) GetLogrusLevels() []logrus.Level { + // 如果未配置,使用默认值error(向后兼容) + minLevelStr := tc.MinLevel + if minLevelStr == "" { + minLevelStr = "error" + } + + // 解析配置的日志级别 + minLevel, err := logrus.ParseLevel(minLevelStr) + if err != nil { + // 如果解析失败,使用默认值error(向后兼容) + minLevel = logrus.ErrorLevel + } + + // 定义所有日志级别(从高到低:panic, fatal, error, warn, info, debug) + allLevels := []logrus.Level{ + logrus.PanicLevel, + logrus.FatalLevel, + logrus.ErrorLevel, + logrus.WarnLevel, + logrus.InfoLevel, + logrus.DebugLevel, + } + + // 返回所有大于等于minLevel的日志级别 + var result []logrus.Level + for _, level := range allLevels { + if level <= minLevel { + result = append(result, level) + } + } + + return result +} diff --git a/logger/config.telegram.json b/logger/config.telegram.json new file mode 100644 index 00000000..197c0802 --- /dev/null +++ b/logger/config.telegram.json @@ -0,0 +1,33 @@ +{ + "traders": [ + { + "id": "trader1", + "name": "AI Trader 1", + "enabled": true, + "ai_model": "deepseek", + "exchange": "binance", + "binance_api_key": "your_api_key", + "binance_secret_key": "your_secret_key", + "deepseek_key": "your_deepseek_key", + "initial_balance": 1000, + "scan_interval_minutes": 3 + } + ], + "use_default_coins": true, + "default_coins": ["BTCUSDT", "ETHUSDT", "SOLUSDT"], + "api_server_port": 8080, + "leverage": { + "btc_eth_leverage": 5, + "altcoin_leverage": 5 + }, + "log": { + "level": "info", + "telegram": { + "enabled": true, + "bot_token": "79472419:feafe231414", + "chat_id": -100323252626, + "min_level": "error" + } + }, + "_comment": "日志配置说明:level 可选值为 debug/info/warn/error,默认 info。telegram 部分作为可选配置, Telegram 推送默认为 error/fatal/panic 级别,min_level 如果设置为warn,则推送warn级别及以上的日志" +} diff --git a/logger/logger.go b/logger/logger.go new file mode 100644 index 00000000..527c46e2 --- /dev/null +++ b/logger/logger.go @@ -0,0 +1,210 @@ +package logger + +import ( + "nofx/config" + "os" + + "github.com/sirupsen/logrus" +) + +var ( + // Log 全局logger实例 + Log *logrus.Logger + + // telegramHook 保存hook引用,用于优雅关闭 + telegramHook *TelegramHook +) + +// ============================================================================ +// 初始化函数 +// ============================================================================ + +// Init 初始化全局logger +// 如果config为nil,使用默认配置(console输出,info级别) +func Init(cfg *Config) error { + Log = logrus.New() + + // 如果没有配置,使用默认值 + if cfg == nil { + cfg = &Config{Level: "info"} + } + + // 设置默认值 + cfg.SetDefaults() + + // 设置日志级别 + level, err := logrus.ParseLevel(cfg.Level) + if err != nil { + level = logrus.InfoLevel + } + Log.SetLevel(level) + + // 设置格式化器(固定使用彩色文本格式) + Log.SetFormatter(&logrus.TextFormatter{ + FullTimestamp: true, + TimestampFormat: "2006-01-02 15:04:05", + ForceColors: true, + }) + + // 设置输出目标(默认stdout) + Log.SetOutput(os.Stdout) + + // 启用调用位置信息 + Log.SetReportCaller(true) + + // 添加Telegram Hook(可选) + if cfg.Telegram != nil && cfg.Telegram.Enabled { + if err := setupTelegramHook(cfg.Telegram); err != nil { + Log.Warnf("初始化Telegram推送失败,将继续使用普通日志: %v", err) + } + } + + return nil +} + +// setupTelegramHook 设置Telegram Hook +func setupTelegramHook(telegramCfg *TelegramConfig) error { + hook, err := NewTelegramHook(telegramCfg) + if err != nil { + return err + } + + Log.AddHook(hook) + telegramHook = hook + Log.Info("✅ Telegram日志推送已启用") + return nil +} + +// InitWithSimpleConfig 使用简化配置初始化logger +// 适用于只需要基本功能的场景 +func InitWithSimpleConfig(level string) error { + return Init(&Config{Level: level}) +} + +// InitWithTelegram 使用Telegram配置初始化logger +func InitWithTelegram(botToken string, chatID int64) error { + return Init(&Config{ + Level: "info", + Telegram: &TelegramConfig{ + Enabled: true, + BotToken: botToken, + ChatID: chatID, + }, + }) +} + +// InitFromLogConfig 从config.LogConfig初始化logger +func InitFromLogConfig(logConfig *config.LogConfig) error { + if logConfig == nil { + return InitWithSimpleConfig("info") + } + + cfg := &Config{ + Level: logConfig.Level, + } + + if cfg.Level == "" { + cfg.Level = "info" + } + + // 如果启用了Telegram,添加配置 + if logConfig.Telegram != nil && logConfig.Telegram.Enabled { + if botToken := logConfig.Telegram.BotToken; botToken != "" && logConfig.Telegram.ChatID != 0 { + cfg.Telegram = &TelegramConfig{ + Enabled: true, + BotToken: botToken, + ChatID: logConfig.Telegram.ChatID, + MinLevel: logConfig.Telegram.MinLevel, + } + } + } + + return Init(cfg) +} + +// InitFromParams 从参数初始化logger +// 适用于不依赖config包的场景 +func InitFromParams(level string, telegramEnabled bool, botToken string, chatID int64) error { + cfg := &Config{Level: level} + + if telegramEnabled && botToken != "" && chatID != 0 { + cfg.Telegram = &TelegramConfig{ + Enabled: true, + BotToken: botToken, + ChatID: chatID, + } + } + + return Init(cfg) +} + +// Shutdown 优雅关闭logger(主要用于关闭Telegram发送器) +func Shutdown() { + if telegramHook != nil { + telegramHook.Stop() + telegramHook = nil + } +} + +// ============================================================================ +// 日志记录函数 +// ============================================================================ + +// WithFields 创建带字段的logger entry +func WithFields(fields logrus.Fields) *logrus.Entry { + return Log.WithFields(fields) +} + +// WithField 创建带单个字段的logger entry +func WithField(key string, value interface{}) *logrus.Entry { + return Log.WithField(key, value) +} + +// add debug, info, warn +func Debug(args ...interface{}) { + Log.Debug(args...) +} + +func Info(args ...interface{}) { + Log.Info(args...) +} + +func Warn(args ...interface{}) { + Log.Warn(args...) +} + +func Debugf(format string, args ...interface{}) { + Log.Debugf(format, args...) +} + +func Infof(format string, args ...interface{}) { + Log.Infof(format, args...) +} + +func Warnf(format string, args ...interface{}) { + Log.Warnf(format, args...) +} + +func Error(args ...interface{}) { + Log.Error(args...) +} + +func Errorf(format string, args ...interface{}) { + Log.Errorf(format, args...) +} + +func Fatal(args ...interface{}) { + Log.Fatal(args...) +} + +func Fatalf(format string, args ...interface{}) { + Log.Fatalf(format, args...) +} + +func Panic(args ...interface{}) { + Log.Panic(args...) +} + +func Panicf(format string, args ...interface{}) { + Log.Panicf(format, args...) +} diff --git a/logger/telegram_hook.go b/logger/telegram_hook.go new file mode 100644 index 00000000..e8477f47 --- /dev/null +++ b/logger/telegram_hook.go @@ -0,0 +1,158 @@ +package logger + +import ( + "fmt" + "runtime" + "strings" + + "github.com/sirupsen/logrus" +) + +// TelegramHook 实现logrus.Hook接口,将日志推送到Telegram +type TelegramHook struct { + sender *TelegramSender + levels []logrus.Level + enabled bool +} + +// NewTelegramHook 创建Telegram Hook +func NewTelegramHook(config *TelegramConfig) (*TelegramHook, error) { + if !config.Enabled { + return &TelegramHook{enabled: false}, nil + } + + if config.BotToken == "" || config.ChatID == 0 { + return nil, fmt.Errorf("telegram配置不完整: bot_token和chat_id不能为空") + } + + // 创建发送器(使用默认参数) + sender, err := NewTelegramSender(config.BotToken, config.ChatID) + if err != nil { + return nil, fmt.Errorf("创建telegram发送器失败: %w", err) + } + + hook := &TelegramHook{ + sender: sender, + levels: config.GetLogrusLevels(), + enabled: true, + } + + return hook, nil +} + +// Levels 返回需要触发的日志级别 +func (h *TelegramHook) Levels() []logrus.Level { + if !h.enabled { + return []logrus.Level{} + } + return h.levels +} + +// Fire 当日志触发时调用 +func (h *TelegramHook) Fire(entry *logrus.Entry) error { + if !h.enabled { + return nil + } + + // 格式化消息 + message := h.formatMessage(entry) + + // 异步发送(非阻塞) + h.sender.SendAsync(message) + + return nil +} + +// formatMessage 格式化日志消息为Telegram格式 +func (h *TelegramHook) formatMessage(entry *logrus.Entry) string { + // 级别emoji + levelEmoji := h.getLevelEmoji(entry.Level) + + // 基本信息 + var builder strings.Builder + builder.WriteString(fmt.Sprintf("%s *%s*: 系统日志警报\n", levelEmoji, strings.ToUpper(entry.Level.String()))) + builder.WriteString(fmt.Sprintf("📝 消息: `%s`\n", escapeMarkdown(entry.Message))) + + // 字段信息 + if len(entry.Data) > 0 { + builder.WriteString("📊 字段:\n") + for key, value := range entry.Data { + builder.WriteString(fmt.Sprintf(" • %s: `%v`\n", key, value)) + } + } + + // 调用位置 + if entry.HasCaller() { + file := entry.Caller.File + // 只保留相对路径 + if idx := strings.Index(file, "nofx/"); idx >= 0 { + file = file[idx:] + } + builder.WriteString(fmt.Sprintf("📍 位置: `%s:%d`\n", file, entry.Caller.Line)) + } else { + // 如果entry没有caller,手动获取 + if _, file, line, ok := runtime.Caller(8); ok { + if idx := strings.Index(file, "nofx/"); idx >= 0 { + file = file[idx:] + } + builder.WriteString(fmt.Sprintf("📍 位置: `%s:%d`\n", file, line)) + } + } + + // 时间戳 + builder.WriteString(fmt.Sprintf("🕐 时间: `%s`", entry.Time.Format("2006-01-02 15:04:05"))) + + return builder.String() +} + +// getLevelEmoji 获取日志级别对应的emoji +func (h *TelegramHook) getLevelEmoji(level logrus.Level) string { + switch level { + case logrus.PanicLevel: + return "🔴" + case logrus.FatalLevel: + return "🔴" + case logrus.ErrorLevel: + return "🟠" + case logrus.WarnLevel: + return "🟡" + case logrus.InfoLevel: + return "🟢" + case logrus.DebugLevel: + return "🔵" + default: + return "⚪" + } +} + +// escapeMarkdown 转义Markdown特殊字符 +func escapeMarkdown(text string) string { + replacer := strings.NewReplacer( + "_", "\\_", + "*", "\\*", + "[", "\\[", + "]", "\\]", + "(", "\\(", + ")", "\\)", + "~", "\\~", + "`", "\\`", + ">", "\\>", + "#", "\\#", + "+", "\\+", + "-", "\\-", + "=", "\\=", + "|", "\\|", + "{", "\\{", + "}", "\\}", + ".", "\\.", + "!", "\\!", + ) + return replacer.Replace(text) +} + +// Stop 停止Hook(优雅关闭) +func (h *TelegramHook) Stop() { + if h.enabled && h.sender != nil { + h.sender.Stop() + } +} diff --git a/logger/telegram_sender.go b/logger/telegram_sender.go new file mode 100644 index 00000000..8013dc18 --- /dev/null +++ b/logger/telegram_sender.go @@ -0,0 +1,120 @@ +package logger + +import ( + "fmt" + "sync" + "time" + + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" +) + +// TelegramSender Telegram消息发送器(异步) +type TelegramSender struct { + bot *tgbotapi.BotAPI + chatID int64 + msgChan chan string + retryCount int + retryInterval time.Duration + wg sync.WaitGroup + stopChan chan struct{} + once sync.Once +} + +// NewTelegramSender 创建Telegram发送器(使用默认参数) +func NewTelegramSender(botToken string, chatID int64) (*TelegramSender, error) { + bot, err := tgbotapi.NewBotAPI(botToken) + if err != nil { + return nil, fmt.Errorf("创建telegram bot失败: %w", err) + } + + // 设置为静默模式(不打印bot信息) + bot.Debug = false + + sender := &TelegramSender{ + bot: bot, + chatID: chatID, + msgChan: make(chan string, 20), // 固定缓冲区大小: 20 + retryCount: 3, // 固定重试次数: 3 + retryInterval: 3 * time.Second, // 固定重试间隔: 3秒 + stopChan: make(chan struct{}), + } + + // 启动异步发送协程 + sender.Start() + + return sender, nil +} + +// Start 启动异步发送协程 +func (s *TelegramSender) Start() { + s.wg.Add(1) + go s.listenAndSend() +} + +// SendAsync 异步发送消息(非阻塞) +func (s *TelegramSender) SendAsync(message string) { + select { + case s.msgChan <- message: + // 成功写入缓冲区 + default: + // 缓冲区满,丢弃消息(不阻塞主流程) + fmt.Printf("[Telegram] 消息缓冲区已满,消息被丢弃\n") + } +} + +// listenAndSend 监听channel并发送消息 +func (s *TelegramSender) listenAndSend() { + defer s.wg.Done() + + for { + select { + case msg := <-s.msgChan: + s.sendWithRetry(msg) + case <-s.stopChan: + // 清空缓冲区后退出 + for len(s.msgChan) > 0 { + msg := <-s.msgChan + s.sendWithRetry(msg) + } + return + } + } +} + +// sendWithRetry 发送消息(带重试) +func (s *TelegramSender) sendWithRetry(message string) { + var err error + for i := 0; i < s.retryCount; i++ { + err = s.send(message) + if err == nil { + return // 发送成功 + } + + // 重试前等待 + if i < s.retryCount-1 { + time.Sleep(s.retryInterval) + } + } + + // 所有重试都失败 + if err != nil { + fmt.Printf("[Telegram] 发送消息失败(已重试%d次): %v\n", s.retryCount, err) + } +} + +// send 发送单条消息 +func (s *TelegramSender) send(message string) error { + msg := tgbotapi.NewMessage(s.chatID, message) + msg.ParseMode = tgbotapi.ModeMarkdown + + _, err := s.bot.Send(msg) + return err +} + +// Stop 停止发送器(优雅关闭) +func (s *TelegramSender) Stop() { + s.once.Do(func() { + close(s.stopChan) + s.wg.Wait() + }) +} diff --git a/main.go b/main.go index 9e9d1aa7..873f4a80 100644 --- a/main.go +++ b/main.go @@ -25,39 +25,49 @@ type LeverageConfig struct { // ConfigFile 配置文件结构,只包含需要同步到数据库的字段 type ConfigFile struct { - AdminMode bool `json:"admin_mode"` - BetaMode bool `json:"beta_mode"` - APIServerPort int `json:"api_server_port"` - UseDefaultCoins bool `json:"use_default_coins"` - DefaultCoins []string `json:"default_coins"` - CoinPoolAPIURL string `json:"coin_pool_api_url"` - OITopAPIURL string `json:"oi_top_api_url"` - MaxDailyLoss float64 `json:"max_daily_loss"` - MaxDrawdown float64 `json:"max_drawdown"` - StopTradingMinutes int `json:"stop_trading_minutes"` - Leverage LeverageConfig `json:"leverage"` - JWTSecret string `json:"jwt_secret"` - DataKLineTime string `json:"data_k_line_time"` + AdminMode bool `json:"admin_mode"` + BetaMode bool `json:"beta_mode"` + APIServerPort int `json:"api_server_port"` + UseDefaultCoins bool `json:"use_default_coins"` + DefaultCoins []string `json:"default_coins"` + CoinPoolAPIURL string `json:"coin_pool_api_url"` + OITopAPIURL string `json:"oi_top_api_url"` + MaxDailyLoss float64 `json:"max_daily_loss"` + MaxDrawdown float64 `json:"max_drawdown"` + StopTradingMinutes int `json:"stop_trading_minutes"` + Leverage LeverageConfig `json:"leverage"` + JWTSecret string `json:"jwt_secret"` + DataKLineTime string `json:"data_k_line_time"` + Log *config.LogConfig `json:"log"` // 日志配置 } -// syncConfigToDatabase 从config.json读取配置并同步到数据库 -func syncConfigToDatabase(database *config.Database) error { +// loadConfigFile 读取并解析config.json文件 +func loadConfigFile() (*ConfigFile, error) { // 检查config.json是否存在 if _, err := os.Stat("config.json"); os.IsNotExist(err) { - log.Printf("📄 config.json不存在,跳过同步") - return nil + log.Printf("📄 config.json不存在,使用默认配置") + return &ConfigFile{}, nil } // 读取config.json data, err := os.ReadFile("config.json") if err != nil { - return fmt.Errorf("读取config.json失败: %w", err) + return nil, fmt.Errorf("读取config.json失败: %w", err) } // 解析JSON var configFile ConfigFile if err := json.Unmarshal(data, &configFile); err != nil { - return fmt.Errorf("解析config.json失败: %w", err) + return nil, fmt.Errorf("解析config.json失败: %w", err) + } + + return &configFile, nil +} + +// syncConfigToDatabase 将配置同步到数据库 +func syncConfigToDatabase(database *config.Database, configFile *ConfigFile) error { + if configFile == nil { + return nil } log.Printf("🔄 开始同步config.json到数据库...") @@ -156,6 +166,12 @@ func main() { dbPath = os.Args[1] } + // 读取配置文件 + configFile, err := loadConfigFile() + if err != nil { + log.Fatalf("❌ 读取config.json失败: %v", err) + } + log.Printf("📋 初始化配置数据库: %s", dbPath) database, err := config.NewDatabase(dbPath) if err != nil { @@ -164,7 +180,7 @@ func main() { defer database.Close() // 同步config.json到数据库 - if err := syncConfigToDatabase(database); err != nil { + if err := syncConfigToDatabase(database, configFile); err != nil { log.Printf("⚠️ 同步config.json到数据库失败: %v", err) }