diff --git a/cmd/main.go b/cmd/main.go index 2e61df1..61895fb 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -16,7 +16,6 @@ import ( "ops-assistant/internal/core/runbook" "ops-assistant/internal/feishu" "ops-assistant/internal/qq" - "ops-assistant/internal/service" "ops-assistant/internal/web" "ops-assistant/models" "ops-assistant/version" @@ -66,9 +65,6 @@ func main() { log.Printf("⚠️ 渠道配置加载失败,继续使用 YAML: %v", err) } - finance := service.NewFinanceService(db) - defer finance.Close() - if err := runbook.SeedDefaultTargets(db); err != nil { log.Printf("⚠️ 初始化ops targets失败: %v", err) } @@ -79,7 +75,7 @@ func main() { defer cancel() if cfg.Telegram.Enabled { - tgBot, err := bot.NewTGBot(db, cfg.Telegram.Token, finance, opsSvc) + tgBot, err := bot.NewTGBot(db, cfg.Telegram.Token, opsSvc) if err != nil { log.Printf("⚠️ TG Bot 启动失败: %v", err) } else { @@ -88,7 +84,7 @@ func main() { } if cfg.QQBot.Enabled { - qqBot := qq.NewQQBot(db, cfg.QQBot.AppID, cfg.QQBot.Secret, finance, opsSvc) + qqBot := qq.NewQQBot(db, cfg.QQBot.AppID, cfg.QQBot.Secret, opsSvc) go qqBot.Start(ctx) go qqBot.StartSubScheduler(ctx, 9, 0) } @@ -104,11 +100,11 @@ func main() { return fmt.Sprintf("reload ok: tg=%v qq=%v feishu=%v", cfg.Telegram.Enabled, cfg.QQBot.Enabled, cfg.Feishu.Enabled), nil } - webServer := web.NewWebServer(db, cfg.Database.Path, ".", finance, cfg.Server.Port, cfg.Admin.Username, cfg.Admin.Password, cfg.Server.Key, reloadFn) + webServer := web.NewWebServer(db, cfg.Database.Path, ".", cfg.Server.Port, cfg.Admin.Username, cfg.Admin.Password, cfg.Server.Key, reloadFn) webServer.RegisterRoutes(engine) if cfg.Feishu.Enabled { - fsBot := feishu.NewBot(db, finance, opsSvc, cfg.Feishu.AppID, cfg.Feishu.AppSecret, cfg.Feishu.VerificationToken, cfg.Feishu.EncryptKey) + fsBot := feishu.NewBot(db, opsSvc, cfg.Feishu.AppID, cfg.Feishu.AppSecret, cfg.Feishu.VerificationToken, cfg.Feishu.EncryptKey) fsBot.RegisterRoutes(engine) go fsBot.Start(ctx) } diff --git a/internal/bot/telegram.go b/internal/bot/telegram.go index 7c4afcd..64130ab 100644 --- a/internal/bot/telegram.go +++ b/internal/bot/telegram.go @@ -7,31 +7,28 @@ import ( "strings" "time" - xchart "ops-assistant/internal/chart" "ops-assistant/internal/core/ops" - "ops-assistant/internal/service" "ops-assistant/models" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" "gorm.io/gorm" ) -// DefaultUserID 统一用户ID,使所有平台共享同一份账本 +// DefaultUserID 统一用户ID const DefaultUserID int64 = 1 type TGBot struct { - api *tgbotapi.BotAPI - finance *service.FinanceService - db *gorm.DB - opsSvc *ops.Service + api *tgbotapi.BotAPI + db *gorm.DB + opsSvc *ops.Service } -func NewTGBot(db *gorm.DB, token string, finance *service.FinanceService, opsSvc *ops.Service) (*TGBot, error) { +func NewTGBot(db *gorm.DB, token string, opsSvc *ops.Service) (*TGBot, error) { bot, err := tgbotapi.NewBotAPI(token) if err != nil { return nil, err } - return &TGBot{api: bot, finance: finance, db: db, opsSvc: opsSvc}, nil + return &TGBot{api: bot, db: db, opsSvc: opsSvc}, nil } func (b *TGBot) Start(ctx context.Context) { @@ -91,147 +88,18 @@ func (b *TGBot) handleMessage(msg *tgbotapi.Message) { } } - var reply string - - switch { - case text == "/start": - reply = "🛠️ 欢迎使用 Ops-Assistant!\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 查看帮助" - + reply := "" + switch text { + case "/start": + reply = "🛠️ 欢迎使用 Ops-Assistant!\n\n模块化运维命令助手,支持:\n• /cpa - CPA 管理\n• /cf - Cloudflare 管理\n• /cfdav - cfdav 管理\n• /sub - 订阅管理\n• /mail - 邮箱管理\n\n/help - 使用说明" + case "/help": + reply = "📖 使用说明:\n\n直接发送 OPS 命令即可,例如:\n• /cpa status\n• /cf zones\n• /cf dns list \n• /cfdav users\n• /sub list\n\n模块入口默认关闭,可在 Web 后台「模块设置」中启用。" 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) - } + reply = "❓ 无法识别,输入 /help 查看支持的命令" } m := tgbotapi.NewMessage(chatID, reply) if _, err := b.api.Send(m); err != nil { log.Printf("发送消息失败 chat=%d: %v", chatID, err) } -} - -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) - } -} - -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) - } -} +} \ No newline at end of file diff --git a/internal/chart/chart.go b/internal/chart/chart.go deleted file mode 100644 index 389fe61..0000000 --- a/internal/chart/chart.go +++ /dev/null @@ -1,126 +0,0 @@ -package chart - -import ( - "bytes" - "fmt" - "math" - - "ops-assistant/internal/service" - - "github.com/wcharczuk/go-chart/v2" - "github.com/wcharczuk/go-chart/v2/drawing" -) - -// 分类对应的颜色 -var categoryColors = []drawing.Color{ - {R: 255, G: 99, B: 132, A: 255}, // 红 - {R: 54, G: 162, B: 235, A: 255}, // 蓝 - {R: 255, G: 206, B: 86, A: 255}, // 黄 - {R: 75, G: 192, B: 192, A: 255}, // 青 - {R: 153, G: 102, B: 255, A: 255}, // 紫 - {R: 255, G: 159, B: 64, A: 255}, // 橙 - {R: 46, G: 204, B: 113, A: 255}, // 绿 - {R: 231, G: 76, B: 60, A: 255}, // 深红 - {R: 52, G: 73, B: 94, A: 255}, // 深蓝灰 - {R: 241, G: 196, B: 15, A: 255}, // 金 -} - -// GeneratePieChart 生成分类占比饼图 -func GeneratePieChart(stats []service.CategoryStat, title string) ([]byte, error) { - if len(stats) == 0 { - return nil, fmt.Errorf("no data") - } - - var total float64 - for _, s := range stats { - total += float64(s.Total) - } - - var values []chart.Value - for i, s := range stats { - yuan := float64(s.Total) / 100.0 - pct := float64(s.Total) / total * 100 - label := fmt.Sprintf("%s %.0f元(%.0f%%)", s.Category, yuan, pct) - values = append(values, chart.Value{ - Value: yuan, - Label: label, - Style: chart.Style{ - FillColor: categoryColors[i%len(categoryColors)], - StrokeColor: drawing.ColorWhite, - StrokeWidth: 2, - }, - }) - } - - pie := chart.PieChart{ - Title: title, - Width: 600, - Height: 500, - TitleStyle: chart.Style{ - FontSize: 16, - }, - Values: values, - } - - buf := &bytes.Buffer{} - if err := pie.Render(chart.PNG, buf); err != nil { - return nil, fmt.Errorf("render pie chart: %w", err) - } - return buf.Bytes(), nil -} - -// GenerateBarChart 生成每日消费柱状图 -func GenerateBarChart(stats []service.DailyStat, title string) ([]byte, error) { - if len(stats) == 0 { - return nil, fmt.Errorf("no data") - } - - var values []chart.Value - var maxVal float64 - for _, s := range stats { - yuan := float64(s.Total) / 100.0 - if yuan > maxVal { - maxVal = yuan - } - // 日期只取 MM-DD - dateLabel := s.Date - if len(s.Date) > 5 { - dateLabel = s.Date[5:] - } - values = append(values, chart.Value{ - Value: yuan, - Label: dateLabel, - Style: chart.Style{ - FillColor: drawing.Color{R: 54, G: 162, B: 235, A: 255}, - StrokeColor: drawing.Color{R: 54, G: 162, B: 235, A: 255}, - StrokeWidth: 1, - }, - }) - } - - bar := chart.BarChart{ - Title: title, - Width: 600, - Height: 400, - TitleStyle: chart.Style{ - FontSize: 16, - }, - YAxis: chart.YAxis{ - Range: &chart.ContinuousRange{ - Min: 0, - Max: math.Ceil(maxVal*1.2/10) * 10, - }, - ValueFormatter: func(v interface{}) string { - return fmt.Sprintf("%.0f", v) - }, - }, - BarWidth: 40, - Bars: values, - } - - buf := &bytes.Buffer{} - if err := bar.Render(chart.PNG, buf); err != nil { - return nil, fmt.Errorf("render bar chart: %w", err) - } - return buf.Bytes(), nil -} diff --git a/internal/core/ops/bootstrap.go b/internal/core/ops/bootstrap.go index fd8d809..4b2022b 100644 --- a/internal/core/ops/bootstrap.go +++ b/internal/core/ops/bootstrap.go @@ -9,6 +9,7 @@ import ( "ops-assistant/internal/module/cfdav" "ops-assistant/internal/module/cpa" "ops-assistant/internal/module/mail" + "ops-assistant/internal/module/rate" "ops-assistant/internal/module/sub" "gorm.io/gorm" @@ -20,12 +21,14 @@ func BuildDefault(db *gorm.DB, dbPath, baseDir string) *Service { cpaModule := cpa.New(db, exec) cfModule := cf.New(db, exec) mailModule := mail.New(db, exec) + rateModule := rate.New() cfdavModule := cfdav.New(db) subModule := sub.New(db) r.RegisterModule("cpa", cpaModule.Handle) r.RegisterModule("cf", cfModule.Handle) r.RegisterModule("mail", mailModule.Handle) + r.RegisterModule("rate", rateModule.Handle) r.RegisterModule("cfdav", cfdavModule.Handle) r.RegisterModule("sub", subModule.Handle) return NewService(dbPath, baseDir, r) diff --git a/internal/core/ops/service.go b/internal/core/ops/service.go index 6172826..3e5b506 100644 --- a/internal/core/ops/service.go +++ b/internal/core/ops/service.go @@ -41,7 +41,7 @@ func (s *Service) Handle(userID int64, text string) (bool, string) { if cmd.Module == "ops" && len(cmd.Args) > 0 && cmd.Args[0] == "modules" { return true, s.modulesStatusText() } - if cmd.Module != "" && cmd.Module != "ops" && s.db != nil { + if cmd.Module != "" && cmd.Module != "ops" && cmd.Module != "rate" && s.db != nil { if !coremodule.IsEnabled(s.db, cmd.Module) { return true, fmt.Sprintf("[ERR_FEATURE_DISABLED] 模块未启用: %s(开关: enable_module_%s)", cmd.Module, cmd.Module) } @@ -65,6 +65,7 @@ func (s *Service) helpText() string { "- /mail (邮箱状态)", "- /cfdav (CFDAV 用户管理)", "- /sub (订阅管理)", + "- /rate 138sgd to cny (实时汇率转换)", "提示:发送 /cpa /cf /mail /cfdav /sub 获取各模块完整指令清单。", } return strings.Join(lines, "\n") diff --git a/internal/feishu/feishu.go b/internal/feishu/feishu.go index eb28a34..172eb65 100644 --- a/internal/feishu/feishu.go +++ b/internal/feishu/feishu.go @@ -2,28 +2,24 @@ package feishu import ( "context" - "fmt" "io" "log" - "net/http" "strings" "time" "ops-assistant/internal/channel" "ops-assistant/internal/core/ops" - "ops-assistant/internal/service" "ops-assistant/models" "github.com/gin-gonic/gin" "gorm.io/gorm" ) -// DefaultUserID 统一用户ID,使所有平台共享同一份账本 +// DefaultUserID 统一用户ID const DefaultUserID int64 = 1 type Bot struct { db *gorm.DB - finance *service.FinanceService opsSvc *ops.Service appID string appSecret string @@ -31,10 +27,9 @@ type Bot struct { encryptKey string } -func NewBot(db *gorm.DB, finance *service.FinanceService, opsSvc *ops.Service, appID, appSecret, verificationToken, encryptKey string) *Bot { +func NewBot(db *gorm.DB, opsSvc *ops.Service, appID, appSecret, verificationToken, encryptKey string) *Bot { return &Bot{ db: db, - finance: finance, opsSvc: opsSvc, appID: appID, appSecret: appSecret, @@ -56,29 +51,29 @@ func (b *Bot) RegisterRoutes(r *gin.Engine) { func (b *Bot) handleWebhook(c *gin.Context) { body, err := io.ReadAll(io.LimitReader(c.Request.Body, 1<<20)) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) + c.JSON(400, gin.H{"error": "bad request"}) return } // 统一走 channel 包解析,便于后续扩展验签/解密 msg, verifyChallenge, err := channel.ParseFeishuInbound(body, b.verificationToken) if err != nil { - c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + c.JSON(400, gin.H{"error": err.Error()}) return } if verifyChallenge != "" { - c.JSON(http.StatusOK, gin.H{"challenge": verifyChallenge}) + c.JSON(200, gin.H{"challenge": verifyChallenge}) return } if msg == nil { - c.JSON(http.StatusOK, gin.H{"code": 0}) + c.JSON(200, gin.H{"code": 0}) return } // 幂等去重 var existed models.MessageDedup if err := b.db.Where("platform = ? AND event_id = ?", "feishu", msg.EventID).First(&existed).Error; err == nil { - c.JSON(http.StatusOK, gin.H{"code": 0}) + c.JSON(200, gin.H{"code": 0}) return } _ = b.db.Create(&models.MessageDedup{Platform: "feishu", EventID: msg.EventID, ProcessedAt: time.Now()}).Error @@ -91,7 +86,7 @@ func (b *Bot) handleWebhook(c *gin.Context) { } } - c.JSON(http.StatusOK, gin.H{"code": 0}) + c.JSON(200, gin.H{"code": 0}) } func (b *Bot) handleText(text string) string { @@ -103,31 +98,10 @@ func (b *Bot) handleText(text string) string { } switch trim { case "帮助", "help", "/help", "菜单", "功能", "/start": - return "🛠️ Ops-Assistant\n\n直接发送消费描述即可记账:\n• 午饭 25元\n• 打车 ¥30\n\n📋 命令:记录/查看、今日/今天、统计" - case "查看", "记录", "列表", "最近": - items, err := b.finance.GetTransactions(DefaultUserID, 10) - if err != nil { - return "❌ 查询失败" - } - if len(items) == 0 { - return "📭 暂无记录" - } - 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())) - } - return sb.String() + return "🛠️ Ops-Assistant\n\n模块化运维命令助手,支持:\n• /cpa - CPA 管理\n• /cf - Cloudflare 管理\n• /cfdav - cfdav 管理\n• /sub - 订阅管理\n• /mail - 邮箱管理" + default: + return "❓ 无法识别,请使用 OPS 命令(发送「帮助」查看说明)。" } - - amount, category, err := b.finance.AddTransaction(DefaultUserID, trim) - if err != nil { - return "❌ 记账失败,请稍后重试" - } - if amount == 0 { - return "📍 没看到金额,这笔花了多少钱?" - } - return fmt.Sprintf("✅ 已记入【%s】:%.2f元\n📝 备注:%s", category, float64(amount)/100.0, trim) } func maskID(s string) string { diff --git a/internal/qq/qq.go b/internal/qq/qq.go index 6f019e9..2866258 100644 --- a/internal/qq/qq.go +++ b/internal/qq/qq.go @@ -11,7 +11,6 @@ import ( "time" "ops-assistant/internal/core/ops" - "ops-assistant/internal/service" "ops-assistant/internal/core/ai" "ops-assistant/models" @@ -26,12 +25,11 @@ import ( "gorm.io/gorm" ) -// DefaultUserID 统一用户ID,使所有平台共享同一份账本 +// DefaultUserID 统一用户ID const DefaultUserID int64 = 1 type QQBot struct { api openapi.OpenAPI - finance *service.FinanceService credentials *token.QQBotCredentials db *gorm.DB opsSvc *ops.Service @@ -39,11 +37,10 @@ type QQBot struct { aiAutoReload time.Time } -func NewQQBot(db *gorm.DB, appID string, secret string, finance *service.FinanceService, opsSvc *ops.Service) *QQBot { +func NewQQBot(db *gorm.DB, appID string, secret string, opsSvc *ops.Service) *QQBot { return &QQBot{ - db: db, - finance: finance, - opsSvc: opsSvc, + db: db, + opsSvc: opsSvc, credentials: &token.QQBotCredentials{ AppID: appID, AppSecret: secret, diff --git a/internal/service/finance.go b/internal/service/finance.go deleted file mode 100644 index 17407ac..0000000 --- a/internal/service/finance.go +++ /dev/null @@ -1,154 +0,0 @@ -package service - -import ( - "math" - "regexp" - "strconv" - "time" - - "ops-assistant/models" - - "github.com/yanyiwu/gojieba" - "gorm.io/gorm" -) - -type FinanceService struct { - db *gorm.DB - jieba *gojieba.Jieba -} - -func NewFinanceService(db *gorm.DB) *FinanceService { - return &FinanceService{ - db: db, - jieba: gojieba.NewJieba(), - } -} - -func (s *FinanceService) Close() { - s.jieba.Free() -} - -// ParseText 从自然语言文本中提取金额(分)和分类 -func (s *FinanceService) ParseText(text string) (int64, string) { - // 1. 提取金额 — 优先匹配带单位的,如 "15.5元"、"¥30"、"20块" - amountPatterns := []*regexp.Regexp{ - regexp.MustCompile(`[¥¥]\s*(\d+\.?\d*)`), - regexp.MustCompile(`(\d+\.?\d*)\s*[元块]`), - } - - var amountStr string - for _, re := range amountPatterns { - m := re.FindStringSubmatch(text) - if len(m) > 1 { - amountStr = m[1] - break - } - } - - // 兜底:取最后一个独立数字 - if amountStr == "" { - re := regexp.MustCompile(`(\d+\.?\d*)`) - matches := re.FindAllStringSubmatch(text, -1) - if len(matches) > 0 { - amountStr = matches[len(matches)-1][1] - } - } - - if amountStr == "" { - return 0, "" - } - - amountFloat, err := strconv.ParseFloat(amountStr, 64) - if err != nil || amountFloat <= 0 { - return 0, "" - } - - // 转为分 - amountCents := int64(math.Round(amountFloat * 100)) - - // 2. 提取分类(Jieba 分词 + 数据库匹配) - words := s.jieba.Cut(text, true) - category := "其他" - - for _, word := range words { - var ck models.CategoryKeyword - if err := s.db.Where("keyword = ?", word).First(&ck).Error; err == nil { - category = ck.Category - break - } - } - - return amountCents, category -} - -// AddTransaction 解析文本并创建一条交易记录 -func (s *FinanceService) AddTransaction(userID int64, text string) (int64, string, error) { - amount, category := s.ParseText(text) - if amount == 0 { - return 0, "", nil - } - - tx := models.Transaction{ - UserID: userID, - Amount: amount, - Category: category, - Note: text, - Date: time.Now().Format("2006-01-02"), - } - - return amount, category, s.db.Create(&tx).Error -} - -// GetTransactions 获取用户的交易记录 -func (s *FinanceService) GetTransactions(userID int64, limit int) ([]models.Transaction, error) { - var items []models.Transaction - err := s.db.Where("user_id = ? AND is_deleted = ?", userID, false). - Order("id desc").Limit(limit).Find(&items).Error - return items, err -} - -// GetTransactionsByDate 获取用户指定日期的交易记录 -func (s *FinanceService) GetTransactionsByDate(userID int64, date string) ([]models.Transaction, error) { - var items []models.Transaction - err := s.db.Where("user_id = ? AND date = ? AND is_deleted = ?", userID, date, false). - Order("id desc").Find(&items).Error - return items, err -} - -// CategoryStat 分类统计结果 -type CategoryStat struct { - Category string - Total int64 - Count int -} - -// GetCategoryStats 获取用户指定日期范围的分类统计 -func (s *FinanceService) GetCategoryStats(userID int64, dateFrom, dateTo string) ([]CategoryStat, error) { - var stats []CategoryStat - err := s.db.Model(&models.Transaction{}). - Select("category, SUM(amount) as total, COUNT(*) as count"). - Where("user_id = ? AND date >= ? AND date <= ? AND is_deleted = ?", userID, dateFrom, dateTo, false). - Group("category"). - Order("total desc"). - Find(&stats).Error - return stats, err -} - -// DailyStat 每日统计结果 -type DailyStat struct { - Date string - Total int64 - Count int -} - -// GetDailyStats 获取用户指定日期范围的每日统计 -func (s *FinanceService) GetDailyStats(userID int64, dateFrom, dateTo string) ([]DailyStat, error) { - var stats []DailyStat - err := s.db.Model(&models.Transaction{}). - Select("date, SUM(amount) as total, COUNT(*) as count"). - Where("user_id = ? AND date >= ? AND date <= ? AND is_deleted = ?", userID, dateFrom, dateTo, false). - Group("date"). - Order("date asc"). - Find(&stats).Error - return stats, err -} diff --git a/internal/web/server.go b/internal/web/server.go index 7f2a2ad..d26d92b 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -21,7 +21,6 @@ import ( "ops-assistant/internal/core/ecode" "ops-assistant/internal/core/ops" "ops-assistant/internal/core/runbook" - "ops-assistant/internal/service" "ops-assistant/models" "ops-assistant/version" @@ -40,7 +39,6 @@ type WebServer struct { db *gorm.DB dbPath string baseDir string - finance *service.FinanceService port int username string password string @@ -161,12 +159,11 @@ var rolePermissions = map[string][]string{ }, } -func NewWebServer(db *gorm.DB, dbPath, baseDir string, finance *service.FinanceService, port int, username, password, sessionKey string, reloadFn func() (string, error)) *WebServer { +func NewWebServer(db *gorm.DB, dbPath, baseDir string, port int, username, password, sessionKey string, reloadFn func() (string, error)) *WebServer { return &WebServer{ db: db, dbPath: dbPath, baseDir: baseDir, - finance: finance, port: port, username: username, password: password,