Files
ops-assistant/internal/bot/telegram.go
openclaw bac7a7b708 feat: 添加图表统计功能
- TG: /chart 本月分类饼图, /week 近7天消费柱状图
- QQ: 统计/报表 本月文本统计
- 新增 go-chart 依赖生成 PNG 图表
- 新增 GetCategoryStats/GetDailyStats 查询方法
2026-02-15 21:52:03 +08:00

210 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}