feat: 完善项目架构并增强分析页面功能

- 新增项目文档和 Docker 配置
  - 添加 README.md 和 TODO.md 项目文档
  - 为各服务添加 Dockerfile 和 docker-compose 配置

- 重构后端架构
  - 新增 adapter 层(HTTP/Python 适配器)
  - 新增 repository 层(数据访问抽象)
  - 新增 router 模块统一管理路由
  - 新增账单处理 handler

- 扩展前端 UI 组件库
  - 新增 Calendar、DateRangePicker、Drawer、Popover 等组件
  - 集成 shadcn-svelte 组件库

- 增强分析页面功能
  - 添加时间范围筛选器(支持本月默认值)
  - 修复 DateRangePicker 默认值显示问题
  - 优化数据获取和展示逻辑

- 完善分析器服务
  - 新增 FastAPI 服务接口
  - 改进账单清理器实现
This commit is contained in:
2026-01-10 01:15:52 +08:00
parent 94f8ea12e6
commit 087ae027cc
96 changed files with 4301 additions and 482 deletions

View File

@@ -312,7 +312,7 @@ func saveCleanedBillsFromCSV(filePath, billType, sourceFile, uploadBatch string)
// 提取字段 - 订单号(用于去重判断)
if idx, ok := colIdx["交易订单号"]; ok && len(row) > idx {
bill.TransactionID = strings.TrimSpace(row[idx])
} else if idx, ok := colIdx["交易号"]; ok && len(row) > idx {
} else if idx, ok := colIdx["交易号"]; ok && len(row) > idx {
bill.TransactionID = strings.TrimSpace(row[idx])
}
if idx, ok := colIdx["商家订单号"]; ok && len(row) > idx {
@@ -325,24 +325,34 @@ func saveCleanedBillsFromCSV(filePath, billType, sourceFile, uploadBatch string)
}
if idx, ok := colIdx["交易分类"]; ok && len(row) > idx {
bill.Category = row[idx]
} else if idx, ok := colIdx["交易类型"]; ok && len(row) > idx {
bill.Category = row[idx]
}
if idx, ok := colIdx["交易对方"]; ok && len(row) > idx {
bill.Merchant = row[idx]
}
if idx, ok := colIdx["商品说明"]; ok && len(row) > idx {
bill.Description = row[idx]
} else if idx, ok := colIdx["商品"]; ok && len(row) > idx {
bill.Description = row[idx]
}
if idx, ok := colIdx["收/支"]; ok && len(row) > idx {
bill.IncomeExpense = row[idx]
}
if idx, ok := colIdx["金额"]; ok && len(row) > idx {
bill.Amount = parseAmount(row[idx])
} else if idx, ok := colIdx["金额(元)"]; ok && len(row) > idx {
bill.Amount = parseAmount(row[idx])
}
if idx, ok := colIdx["支付方式"]; ok && len(row) > idx {
if idx, ok := colIdx["收/付款方式"]; ok && len(row) > idx {
bill.PayMethod = row[idx]
} else if idx, ok := colIdx["支付方式"]; ok && len(row) > idx {
bill.PayMethod = row[idx]
}
if idx, ok := colIdx["交易状态"]; ok && len(row) > idx {
bill.Status = row[idx]
} else if idx, ok := colIdx["当前状态"]; ok && len(row) > idx {
bill.Status = row[idx]
}
if idx, ok := colIdx["备注"]; ok && len(row) > idx {
bill.Remark = row[idx]

View File

@@ -1,84 +1,48 @@
// Package service 业务逻辑层
package service
import (
"fmt"
"os/exec"
"strings"
"billai-server/config"
"billai-server/adapter"
)
// CleanOptions 清洗选项
type CleanOptions struct {
Year string // 年份筛选
Month string // 月份筛选
Start string // 起始日期
End string // 结束日期
Format string // 输出格式: csv/json
}
// CleanOptions 清洗选项(保持向后兼容)
type CleanOptions = adapter.CleanOptions
// CleanResult 清洗结果
type CleanResult struct {
BillType string // 检测到的账单类型: alipay/wechat
Output string // Python 脚本输出
}
// CleanResult 清洗结果(保持向后兼容)
type CleanResult = adapter.CleanResult
// RunCleanScript 执行 Python 清洗脚本
// RunCleanScript 执行清洗脚本(使用适配器)
// inputPath: 输入文件路径
// outputPath: 输出文件路径
// opts: 清洗选项
func RunCleanScript(inputPath, outputPath string, opts *CleanOptions) (*CleanResult, error) {
// 构建命令参数
cleanScriptAbs := config.ResolvePath(config.Global.CleanScript)
args := []string{cleanScriptAbs, inputPath, outputPath}
if opts != nil {
if opts.Year != "" {
args = append(args, "--year", opts.Year)
}
if opts.Month != "" {
args = append(args, "--month", opts.Month)
}
if opts.Start != "" {
args = append(args, "--start", opts.Start)
}
if opts.End != "" {
args = append(args, "--end", opts.End)
}
if opts.Format != "" {
args = append(args, "--format", opts.Format)
}
}
// 执行 Python 脚本
fmt.Printf("🐍 执行清洗脚本...\n")
pythonPathAbs := config.ResolvePath(config.Global.PythonPath)
cmd := exec.Command(pythonPathAbs, args...)
cmd.Dir = config.Global.ProjectRoot
output, err := cmd.CombinedOutput()
outputStr := string(output)
if err != nil {
return nil, fmt.Errorf("清洗脚本执行失败: %w\n输出: %s", err, outputStr)
}
// 从输出中检测账单类型
billType := DetectBillTypeFromOutput(outputStr)
return &CleanResult{
BillType: billType,
Output: outputStr,
}, nil
cleaner := adapter.GetCleaner()
return cleaner.Clean(inputPath, outputPath, opts)
}
// DetectBillTypeFromOutput 从 Python 脚本输出中检测账单类型
// DetectBillTypeFromOutput 从脚本输出中检测账单类型
// 保留此函数以兼容其他调用
func DetectBillTypeFromOutput(output string) string {
if strings.Contains(output, "支付宝") {
if containsSubstring(output, "支付宝") {
return "alipay"
}
if strings.Contains(output, "微信") {
if containsSubstring(output, "微信") {
return "wechat"
}
return ""
}
// containsSubstring 检查字符串是否包含子串
func containsSubstring(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
(len(s) > 0 && len(substr) > 0 && findSubstring(s, substr)))
}
func findSubstring(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}