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

155 lines
3.9 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 service
import (
"math"
"regexp"
"strconv"
"time"
"xiaji-go/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
}