package bot import ( "context" "fmt" "log" "strings" "time" xchart "xiaji-go/internal/chart" "xiaji-go/internal/service" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) // DefaultUserID 统一用户ID,使所有平台共享同一份账本 const DefaultUserID int64 = 1 type TGBot struct { api *tgbotapi.BotAPI finance *service.FinanceService } func NewTGBot(token string, finance *service.FinanceService) (*TGBot, error) { bot, err := tgbotapi.NewBotAPI(token) if err != nil { return nil, err } return &TGBot{api: bot, finance: finance}, nil } func (b *TGBot) Start(ctx context.Context) { u := tgbotapi.NewUpdate(0) u.Timeout = 60 updates := b.api.GetUpdatesChan(u) log.Println("🚀 Telegram Bot 已启动") for { select { case <-ctx.Done(): log.Println("⏳ Telegram Bot 正在停止...") b.api.StopReceivingUpdates() return case update, ok := <-updates: if !ok { return } if update.Message == nil || update.Message.Text == "" { continue } b.handleMessage(update.Message) } } } func (b *TGBot) handleMessage(msg *tgbotapi.Message) { text := msg.Text chatID := msg.Chat.ID var reply string switch { case text == "/start": reply = "🦞 欢迎使用虾记记账!\n\n直接发送消费描述即可记账,例如:\n• 午饭 25元\n• 打车 ¥30\n• 买咖啡15块\n\n命令:\n/list - 查看最近记录\n/today - 今日汇总\n/chart - 本月图表\n/help - 帮助" case text == "/help": reply = "📖 使用说明:\n\n直接发送带金额的文本即可自动记账。\n系统会自动识别金额和消费分类。\n\n支持格式:\n• 午饭 25元\n• ¥30 打车\n• 买水果15块\n\n命令:\n/list - 最近10条记录\n/today - 今日汇总\n/chart - 本月消费图表\n/week - 近7天每日趋势\n/start - 欢迎信息" case text == "/today": today := time.Now().Format("2006-01-02") items, err := b.finance.GetTransactionsByDate(DefaultUserID, today) if err != nil { reply = "❌ 查询失败" } else if len(items) == 0 { reply = fmt.Sprintf("📭 %s 暂无消费记录", today) } else { var sb strings.Builder var total int64 sb.WriteString(fmt.Sprintf("📊 今日(%s)消费:\n\n", today)) for _, item := range items { sb.WriteString(fmt.Sprintf("• %s:%.2f元\n", item.Category, item.AmountYuan())) total += item.Amount } sb.WriteString(fmt.Sprintf("\n💰 共 %d 笔,合计 %.2f 元", len(items), float64(total)/100.0)) reply = sb.String() } case text == "/chart": b.sendMonthlyChart(chatID) return case text == "/week": b.sendWeeklyChart(chatID) return case text == "/list": items, err := b.finance.GetTransactions(DefaultUserID, 10) if err != nil { reply = "❌ 查询失败" } else if len(items) == 0 { reply = "📭 暂无记录" } else { var sb strings.Builder sb.WriteString("📋 最近记录:\n\n") for _, item := range items { sb.WriteString(fmt.Sprintf("• [%s] %s:%.2f元\n", item.Date, item.Category, item.AmountYuan())) } reply = sb.String() } case strings.HasPrefix(text, "/"): reply = "❓ 未知命令,输入 /help 查看帮助" default: // 记账逻辑 amount, category, err := b.finance.AddTransaction(DefaultUserID, text) if err != nil { reply = "❌ 记账失败,请稍后重试" log.Printf("记账失败: %v", err) } else if amount == 0 { reply = "📍 没看到金额,这笔花了多少钱?" } else { amountYuan := float64(amount) / 100.0 reply = fmt.Sprintf("✅ 已记入【%s】:%.2f元\n📝 备注:%s", category, amountYuan, text) } } m := tgbotapi.NewMessage(chatID, reply) if _, err := b.api.Send(m); err != nil { log.Printf("发送消息失败 chat=%d: %v", chatID, err) } } // sendMonthlyChart 发送本月分类饼图 func (b *TGBot) sendMonthlyChart(chatID int64) { now := time.Now() dateFrom := now.Format("2006-01") + "-01" dateTo := now.Format("2006-01-02") title := fmt.Sprintf("%d年%d月消费分类", now.Year(), now.Month()) stats, err := b.finance.GetCategoryStats(DefaultUserID, dateFrom, dateTo) if err != nil || len(stats) == 0 { m := tgbotapi.NewMessage(chatID, "📭 本月暂无消费数据") b.api.Send(m) return } imgData, err := xchart.GeneratePieChart(stats, title) if err != nil { log.Printf("生成饼图失败: %v", err) m := tgbotapi.NewMessage(chatID, "❌ 图表生成失败") b.api.Send(m) return } // 计算总计文字 var total int64 var totalCount int for _, s := range stats { total += s.Total totalCount += s.Count } caption := fmt.Sprintf("📊 %s\n💰 共 %d 笔,合计 %.2f 元", title, totalCount, float64(total)/100.0) photo := tgbotapi.NewPhoto(chatID, tgbotapi.FileBytes{Name: "chart.png", Bytes: imgData}) photo.Caption = caption if _, err := b.api.Send(photo); err != nil { log.Printf("发送图表失败 chat=%d: %v", chatID, err) } } // sendWeeklyChart 发送近7天每日消费柱状图 func (b *TGBot) sendWeeklyChart(chatID int64) { now := time.Now() dateFrom := now.AddDate(0, 0, -6).Format("2006-01-02") dateTo := now.Format("2006-01-02") title := fmt.Sprintf("近7天消费趋势 (%s ~ %s)", dateFrom[5:], dateTo[5:]) stats, err := b.finance.GetDailyStats(DefaultUserID, dateFrom, dateTo) if err != nil || len(stats) == 0 { m := tgbotapi.NewMessage(chatID, "📭 近7天暂无消费数据") b.api.Send(m) return } imgData, err := xchart.GenerateBarChart(stats, title) if err != nil { log.Printf("生成柱状图失败: %v", err) m := tgbotapi.NewMessage(chatID, "❌ 图表生成失败") b.api.Send(m) return } // 总计 var total int64 var totalCount int for _, s := range stats { total += s.Total totalCount += s.Count } caption := fmt.Sprintf("📈 %s\n💰 共 %d 笔,合计 %.2f 元", title, totalCount, float64(total)/100.0) photo := tgbotapi.NewPhoto(chatID, tgbotapi.FileBytes{Name: "chart.png", Bytes: imgData}) photo.Caption = caption if _, err := b.api.Send(photo); err != nil { log.Printf("发送图表失败 chat=%d: %v", chatID, err) } }