Files
ProjectAGiPrompt/16-ProjectMoneyM-转FireFlyIII/3-实现详细稿/2-ProjectMoneyM-DDS.md
2026-03-18 16:16:47 +08:00

32 KiB
Raw Blame History

系统详细设计说明书 (DDS): ProjectMoneyX 个人全景财务分析系统

文档属性 详情
项目名称 ProjectMoneyX
版本号 v1.0
编制日期 2026-02-26
文档状态 初稿
技术栈 Golang (Gin + GORM) / Vue3 + TypeScript + Vuetify / ECharts
参考项目 double-entry-generator

1. 系统总体架构

1.1 架构概览

系统采用前后端分离的 B/S 架构,后端以 Golang Gin 框架提供 RESTful API前端以 Vue3 + Vuetify 构建 SPA 应用。核心数据处理流程借鉴 double-entry-generatorProvider → IR → Translate 管道模式,将其从命令行工具改造为 Web 服务。

graph TB
    subgraph Frontend["前端 (Vue3 + Vuetify + ECharts)"]
        UI_Upload["账单上传"]
        UI_Dashboard["全景仪表盘"]
        UI_Query["账单查询与分析"]
        UI_Budget["预算管理"]
        UI_Account["账户管理"]
        UI_Rule["规则配置"]
    end

    subgraph Backend["后端 (Golang Gin)"]
        API["RESTful API Layer (Gin Router)"]
        SVC["Service Layer"]
        ETL["ETL Pipeline"]
        subgraph ETL_Detail["ETL 管道"]
            Provider["Provider 解析层"]
            IR["IR 中间表示层"]
            Translate["Translate 规则引擎"]
            Dedup["去重引擎"]
        end
        REPO["Repository Layer (GORM)"]
    end

    subgraph Storage["数据存储"]
        DB["SQLite / MySQL"]
        FileStore["本地文件存储"]
    end

    Frontend -->|HTTP/JSON| API
    API --> SVC
    SVC --> ETL
    ETL_Detail --> REPO
    SVC --> REPO
    REPO --> DB
    Provider --> FileStore

1.2 分层架构说明

层级 职责 技术选择
展现层 (Presentation) UI 渲染、用户交互、数据可视化 Vue3 + Vuetify + ECharts
API 层 (API Gateway) 路由分发、参数校验、认证鉴权、响应封装 Gin Framework
业务层 (Service) 核心业务逻辑编排、ETL 流程调度 Go Service
数据处理层 (ETL Pipeline) Provider 解析、IR 转换、Translate 规则、去重合并 Go (参考 double-entry-generator)
数据访问层 (Repository) ORM 映射、数据库 CRUD GORM
数据存储层 (Storage) 持久化存储 SQLite (默认, Local-First) / MySQL (可选)

2. 后端详细设计

2.1 项目目录结构

projectmoneyx-server/
├── main.go                     # 程序入口
├── config/
│   └── config.go               # 配置加载 (YAML)
├── internal/
│   ├── api/                    # API 层 (Gin Handler)
│   │   ├── router.go           # 路由注册
│   │   ├── middleware/         # 中间件 (CORS, Logger, Auth)
│   │   ├── handler/
│   │   │   ├── bill_handler.go
│   │   │   ├── account_handler.go
│   │   │   ├── budget_handler.go
│   │   │   ├── dashboard_handler.go
│   │   │   ├── category_handler.go
│   │   │   └── rule_handler.go
│   │   └── dto/                # 请求/响应 DTO
│   │       ├── request.go
│   │       └── response.go
│   ├── service/                # 业务层
│   │   ├── bill_service.go
│   │   ├── account_service.go
│   │   ├── budget_service.go
│   │   ├── dashboard_service.go
│   │   └── import_service.go   # ETL 编排
│   ├── etl/                    # ETL 管道 (核心)
│   │   ├── pipeline.go         # 管道编排器
│   │   ├── provider/           # Provider 层 (参考 double-entry-generator)
│   │   │   ├── interface.go    # Provider 接口定义
│   │   │   ├── alipay/
│   │   │   │   ├── alipay.go
│   │   │   │   └── parse.go
│   │   │   ├── wechat/
│   │   │   │   ├── wechat.go
│   │   │   │   └── parse.go
│   │   │   └── bank/
│   │   │       ├── cmb.go      # 招商银行
│   │   │       └── icbc.go     # 工商银行
│   │   ├── ir/                 # IR 中间表示
│   │   │   └── ir.go
│   │   ├── translate/          # Translate 规则引擎
│   │   │   ├── engine.go
│   │   │   ├── category.go
│   │   │   └── account.go
│   │   └── dedup/              # 去重引擎
│   │       └── dedup.go
│   ├── model/                  # GORM 数据模型
│   │   ├── bill.go
│   │   ├── account.go
│   │   ├── category.go
│   │   ├── budget.go
│   │   └── rule.go
│   └── repository/             # 数据访问层
│       ├── bill_repo.go
│       ├── account_repo.go
│       └── budget_repo.go
├── configs/
│   ├── app.yaml                # 应用配置
│   └── rules/                  # Translate 规则配置
│       ├── category_rules.yaml
│       └── account_rules.yaml
└── uploads/                    # 上传文件临时存储

2.2 ETL 管道核心设计

2.2.1 Provider 接口定义

借鉴 double-entry-generator 的 Provider 模式,定义统一接口:

// internal/etl/provider/interface.go
package provider

import "projectmoneyx/internal/etl/ir"

// Provider 定义账单解析器的通用接口
type Provider interface {
    // Translate 将原始账单文件解析为 IR 中间表示
    Translate(filename string) (*ir.IR, error)
    // Name 返回 Provider 名称
    Name() string
    // SupportedFormats 返回支持的文件格式
    SupportedFormats() []string
}

// ProviderFactory Provider 工厂,根据类型创建对应 Provider
func NewProvider(providerType string) (Provider, error) {
    switch providerType {
    case "alipay":
        return alipay.New(), nil
    case "wechat":
        return wechat.New(), nil
    case "cmb":
        return bank.NewCMB(), nil
    default:
        return nil, fmt.Errorf("unsupported provider: %s", providerType)
    }
}

2.2.2 IR 中间表示结构

参考 double-entry-generatorir.Order 结构,针对个人财务场景扩展:

// internal/etl/ir/ir.go
package ir

import "time"

// IR 中间表示,作为 Provider 与 Translate 的桥梁
type IR struct {
    Orders []Order `json:"orders"`
}

// Order 统一的订单中间表示
type Order struct {
    // --- 基础字段 (对齐 double-entry-generator) ---
    Peer            string            `json:"peer"`              // 交易对方
    Item            string            `json:"item"`              // 商品说明
    Category        string            `json:"category"`          // 原始分类
    Money           float64           `json:"money"`             // 交易金额
    PayTime         time.Time         `json:"pay_time"`          // 支付时间 (ISO 8601)
    Type            TxType            `json:"type"`              // 收/支/其他
    TypeOriginal    string            `json:"type_original"`     // 原始交易类型文本
    Method          string            `json:"method"`            // 支付方式
    MerchantOrderID string            `json:"merchant_order_id"` // 商户订单号
    OrderID         string            `json:"order_id"`          // 交易单号
    Status          string            `json:"status"`            // 交易状态
    Note            string            `json:"note"`              // 备注

    // --- ProjectMoneyX 扩展字段 ---
    ProviderName    string            `json:"provider_name"`     // 来源 Provider
    DataWeight      int               `json:"data_weight"`       // 数据权重 (L1=1, L2=2, L3=3)
    Currency        string            `json:"currency"`          // 币种,默认 CNY
    Metadata        map[string]string `json:"metadata"`          // 扩展元数据
}

// TxType 交易方向
type TxType string
const (
    TxTypeSend    TxType = "Send"    // 支出
    TxTypeRecv    TxType = "Recv"    // 收入
    TxTypeUnknown TxType = "Unknown" // 未知
)

2.2.3 Translate 规则引擎

// internal/etl/translate/engine.go
package translate

// TranslateEngine 规则引擎核心
type TranslateEngine struct {
    CategoryRules []CategoryRule `yaml:"category_rules"`
    AccountRules  []AccountRule  `yaml:"account_rules"`
}

// CategoryRule 分类映射规则
type CategoryRule struct {
    Match    MatchCondition `yaml:"match"`
    Category string         `yaml:"category"`    // 目标分类 (如 "餐饮-咖啡")
    Priority int            `yaml:"priority"`     // 规则优先级
}

// AccountRule 账户路由规则
type AccountRule struct {
    Provider    string `yaml:"provider"`     // Provider 名称 (如 "alipay")
    MethodMatch string `yaml:"method_match"` // 支付方式正则
    AccountID   uint   `yaml:"account_id"`   // 目标账户 ID
}

// MatchCondition 匹配条件
type MatchCondition struct {
    Field   string `yaml:"field"`   // 匹配字段: peer / item / category
    Pattern string `yaml:"pattern"` // 正则表达式
}

// Apply 对 IR 应用规则转换
func (e *TranslateEngine) Apply(irData *ir.IR) ([]model.Bill, error) {
    // 1. 时间标准化 (ISO 8601, UTC+8)
    // 2. 遍历每条 Order匹配 CategoryRules 进行分类
    // 3. 遍历每条 Order匹配 AccountRules 进行账户路由
    // 4. 转换为 model.Bill 结构
}

2.2.4 去重引擎

// internal/etl/dedup/dedup.go
package dedup

import "time"

const DefaultTimeDelta = 120 * time.Second // 默认时间窗口 120 秒

// DedupEngine 交易链路去重引擎
type DedupEngine struct {
    TimeDelta time.Duration
}

// Process 执行去重合并
// 算法: 按 DataWeight 升序排列,低权重记录尝试匹配高权重记录
// 匹配条件: |Time(Tp) - Time(Ts)| <= Δt AND Amount(Tp) == Amount(Ts)
func (d *DedupEngine) Process(bills []model.Bill) []model.Bill {
    // 1. 按 DataWeight 分组: primary (L1) vs secondary (L2, L3)
    // 2. 双层循环匹配: 时间窗 + 金额精确匹配
    // 3. 匹配成功: 建立 LinkID 关联, 标记 secondary 为 linked
    // 4. 返回合并后的结果集
}

2.2.5 Pipeline 编排

// internal/etl/pipeline.go
package etl

// Pipeline ETL 管道编排器
type Pipeline struct {
    providerRegistry map[string]provider.Provider
    translateEngine  *translate.TranslateEngine
    dedupEngine      *dedup.DedupEngine
}

// Run 执行完整 ETL 流程
func (p *Pipeline) Run(providerType, filePath string) (*ImportResult, error) {
    // Step 1: Provider 解析 -> IR
    prov, err := provider.NewProvider(providerType)
    irData, err := prov.Translate(filePath)

    // Step 2: Translate 规则转换 -> []Bill
    bills, err := p.translateEngine.Apply(irData)

    // Step 3: Dedup 去重合并
    mergedBills := p.dedupEngine.Process(bills)

    // Step 4: 持久化入库
    return p.save(mergedBills)
}
sequenceDiagram
    participant U as 用户
    participant FE as 前端
    participant API as Gin API
    participant SVC as ImportService
    participant P as Provider
    participant T as Translate
    participant D as DedupEngine
    participant DB as Database

    U->>FE: 上传账单文件 + 选择来源类型
    FE->>API: POST /api/v1/bills/import (multipart)
    API->>SVC: ImportBill(providerType, file)
    SVC->>P: Translate(filePath)
    P-->>SVC: IR (中间表示)
    SVC->>T: Apply(IR)
    T-->>SVC: []Bill (标准账单)
    SVC->>D: Process([]Bill)
    D->>DB: 查询已有账单 (时间窗)
    DB-->>D: 存量数据
    D-->>SVC: 去重后 []Bill
    SVC->>DB: 批量写入
    DB-->>SVC: 写入结果
    SVC-->>API: ImportResult(成功/跳过/失败数)
    API-->>FE: JSON Response
    FE-->>U: 导入结果报告

2.3 数据模型设计 (GORM)

2.3.1 ER 关系图

erDiagram
    USER ||--o{ ACCOUNT : owns
    USER ||--o{ BUDGET : sets
    ACCOUNT ||--o{ BILL : "fund_source"
    CATEGORY ||--o{ BILL : classifies
    CATEGORY ||--o{ BUDGET : limits
    BILL ||--o| BILL_LINK : linked
    CATEGORY ||--o{ CATEGORY : "parent-child"
    USER ||--o{ TRANSLATE_RULE : configures

    USER {
        uint id PK
        string username
        string password_hash
        timestamp created_at
    }

    ACCOUNT {
        uint id PK
        uint user_id FK
        string name
        string type "debit/credit/ewallet"
        float64 balance
        string currency
        bool is_active
    }

    BILL {
        uint id PK
        uint user_id FK
        uint account_id FK
        uint category_id FK
        string provider_name
        string order_id UK
        string merchant_order_id
        string peer
        string item
        float64 amount
        string tx_type "income/expense/transfer"
        string method
        string status
        string note
        string link_id
        int data_weight
        timestamp pay_time
        timestamp created_at
    }

    CATEGORY {
        uint id PK
        uint parent_id FK
        string name
        string icon
        int sort_order
        string type "income/expense"
    }

    BUDGET {
        uint id PK
        uint user_id FK
        uint category_id FK "nullable, null=全局"
        float64 amount
        string period "monthly/yearly"
        string month "2026-02"
    }

    BILL_LINK {
        uint id PK
        string link_id UK
        uint primary_bill_id FK
        uint secondary_bill_id FK
    }

    TRANSLATE_RULE {
        uint id PK
        uint user_id FK
        string rule_type "category/account"
        string match_field
        string match_pattern
        string target_value
        int priority
        bool is_active
    }

2.3.2 核心 Model 定义

// internal/model/bill.go
package model

import (
    "time"
    "gorm.io/gorm"
)

type Bill struct {
    gorm.Model
    UserID          uint      `gorm:"index;not null" json:"user_id"`
    AccountID       uint      `gorm:"index" json:"account_id"`
    CategoryID      uint      `gorm:"index" json:"category_id"`
    ProviderName    string    `gorm:"size:32" json:"provider_name"`
    OrderID         string    `gorm:"size:128;uniqueIndex" json:"order_id"`
    MerchantOrderID string    `gorm:"size:128" json:"merchant_order_id"`
    Peer            string    `gorm:"size:256" json:"peer"`
    Item            string    `gorm:"size:512" json:"item"`
    Amount          float64   `gorm:"type:decimal(12,2);not null" json:"amount"`
    TxType          string    `gorm:"size:16;not null" json:"tx_type"`
    Method          string    `gorm:"size:64" json:"method"`
    Status          string    `gorm:"size:32" json:"status"`
    Note            string    `gorm:"size:512" json:"note"`
    LinkID          string    `gorm:"size:64;index" json:"link_id"`
    DataWeight      int       `gorm:"default:1" json:"data_weight"`
    PayTime         time.Time `gorm:"index;not null" json:"pay_time"`

    // 关联
    Account  Account  `gorm:"foreignKey:AccountID" json:"account,omitempty"`
    Category Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
}

// internal/model/account.go
type Account struct {
    gorm.Model
    UserID   uint    `gorm:"index;not null" json:"user_id"`
    Name     string  `gorm:"size:128;not null" json:"name"`
    Type     string  `gorm:"size:32;not null" json:"type"` // debit/credit/ewallet
    Balance  float64 `gorm:"type:decimal(14,2);default:0" json:"balance"`
    Currency string  `gorm:"size:8;default:CNY" json:"currency"`
    IsActive bool    `gorm:"default:true" json:"is_active"`
}

// internal/model/category.go
type Category struct {
    gorm.Model
    ParentID  *uint      `gorm:"index" json:"parent_id"`
    Name      string     `gorm:"size:64;not null" json:"name"`
    Icon      string     `gorm:"size:64" json:"icon"`
    SortOrder int        `gorm:"default:0" json:"sort_order"`
    Type      string     `gorm:"size:16" json:"type"` // income/expense
    Children  []Category `gorm:"foreignKey:ParentID" json:"children,omitempty"`
}

// internal/model/budget.go
type Budget struct {
    gorm.Model
    UserID     uint    `gorm:"index;not null" json:"user_id"`
    CategoryID *uint   `gorm:"index" json:"category_id"` // nil = 全局预算
    Amount     float64 `gorm:"type:decimal(12,2);not null" json:"amount"`
    Period     string  `gorm:"size:16;default:monthly" json:"period"`
    Month      string  `gorm:"size:8;not null" json:"month"` // "2026-02"
}

2.4 API 接口设计

2.4.1 接口总表

模块 Method Path 描述
账单导入 POST /api/v1/bills/import 上传并导入账单文件
GET /api/v1/bills/import-history 导入历史记录 (导入质量图数据)
账单管理 GET /api/v1/bills 多条件查询账单列表
GET /api/v1/bills/:id 查询账单详情
PUT /api/v1/bills/:id 修改账单 (手动调整分类等)
DELETE /api/v1/bills/:id 删除账单
仪表盘 GET /api/v1/dashboard/summary 资产负债 + 现金流概览
GET /api/v1/dashboard/trend 收支趋势 + 净资产趋势 (按月)
GET /api/v1/dashboard/asset-composition 各账户资产构成 (堆叠图数据)
分析 GET /api/v1/analysis/category 分类占比 (饼图, 支持 type=income/expense)
GET /api/v1/analysis/sankey 资金流向 (桑基图数据)
GET /api/v1/analysis/heatmap 日消费热力图数据
GET /api/v1/analysis/category-trend 分类支出趋势 (多折线图数据)
账户 GET /api/v1/accounts 账户列表
POST /api/v1/accounts 创建账户
PUT /api/v1/accounts/:id 修改账户
DELETE /api/v1/accounts/:id 删除账户
GET /api/v1/accounts/:id/waterfall 账户收支瀑布图数据
预算 GET /api/v1/budgets 预算列表 (含执行进度)
POST /api/v1/budgets 创建/更新预算
DELETE /api/v1/budgets/:id 删除预算
分类 GET /api/v1/categories 分类树
POST /api/v1/categories 新增分类
规则 GET /api/v1/rules 规则列表
POST /api/v1/rules 新增规则
PUT /api/v1/rules/:id 修改规则
DELETE /api/v1/rules/:id 删除规则

2.4.2 核心接口详细定义

POST /api/v1/bills/import — 账单导入

Request: multipart/form-data
  - file: 账单文件 (CSV/XLS/PDF)
  - provider: string ("alipay" | "wechat" | "cmb" | "icbc" | "jd" | "meituan")

Response 200:
{
  "code": 0,
  "data": {
    "total": 150,       // 解析总条数
    "imported": 132,    // 新导入条数
    "duplicated": 15,   // 去重跳过条数
    "linked": 3,        // 链路合并条数
    "failed": 0         // 失败条数
  }
}

GET /api/v1/bills — 账单查询

Query Params:
  - start_date: string (2026-01-01)
  - end_date: string (2026-01-31)
  - min_amount: float64
  - max_amount: float64
  - tx_type: string (income/expense)
  - category_id: uint
  - account_id: uint
  - provider: string
  - keyword: string (模糊搜索 peer/item)
  - page: int (default 1)
  - page_size: int (default 20)

Response 200:
{
  "code": 0,
  "data": {
    "total": 500,
    "items": [ { ...Bill } ]
  }
}

GET /api/v1/dashboard/summary — 仪表盘概览

Query Params:
  - month: string (2026-02, 默认当月)

Response 200:
{
  "code": 0,
  "data": {
    "total_assets": 125000.00,
    "total_liabilities": 8500.00,
    "net_worth": 116500.00,
    "month_income": 15000.00,
    "month_expense": 8200.00,
    "income_mom_pct": 5.2,
    "expense_mom_pct": -3.1,
    "accounts": [ { "name": "招行储蓄卡", "balance": 80000.00, "type": "debit" } ]
  }
}

GET /api/v1/analysis/sankey — 桑基图数据

Query Params:
  - start_date / end_date

Response 200:
{
  "code": 0,
  "data": {
    "nodes": [
      { "name": "工资" }, { "name": "招行储蓄卡" }, { "name": "餐饮" }
    ],
    "links": [
      { "source": "工资", "target": "招行储蓄卡", "value": 15000 },
      { "source": "招行储蓄卡", "target": "餐饮", "value": 3200 }
    ]
  }
}

3. 前端详细设计

3.1 页面结构与路由

graph LR
    subgraph Layout["AppLayout"]
        Sidebar["侧边导航栏"]
        Header["顶部栏"]
        Main["主内容区"]
    end

    Main --> Dashboard["/dashboard 仪表盘"]
    Main --> Bills["/bills 账单管理"]
    Main --> Import["/import 账单导入"]
    Main --> Accounts["/accounts 账户管理"]
    Main --> Budgets["/budgets 预算管理"]
    Main --> Analysis["/analysis 多维分析"]
    Main --> Rules["/rules 规则配置"]

3.2 前端项目结构

projectmoneyx-web/
├── src/
│   ├── main.ts
│   ├── App.vue
│   ├── router/
│   │   └── index.ts
│   ├── api/                    # Axios 接口封装
│   │   ├── request.ts          # Axios 实例 + 拦截器
│   │   ├── bill.ts
│   │   ├── account.ts
│   │   ├── dashboard.ts
│   │   ├── analysis.ts
│   │   ├── budget.ts
│   │   └── rule.ts
│   ├── views/
│   │   ├── DashboardView.vue   # 全景仪表盘
│   │   ├── BillListView.vue    # 账单查询
│   │   ├── ImportView.vue      # 账单导入
│   │   ├── AccountView.vue     # 账户管理
│   │   ├── BudgetView.vue      # 预算管理
│   │   ├── AnalysisView.vue    # 多维分析 (桑基图/饼图)
│   │   └── RuleView.vue        # 规则配置
│   ├── components/
│   │   ├── charts/
│   │   │   ├── NetWorthTrendChart.vue    # 图表1: 净资产趋势折线图
│   │   │   ├── CashFlowBarChart.vue      # 图表2: 现金流对比柱状图
│   │   │   ├── AssetCompositionChart.vue  # 图表3: 资产构成堆叠图
│   │   │   ├── ExpensePieChart.vue        # 图表4: 支出结构环形图
│   │   │   ├── CalendarHeatmap.vue        # 图表5: 收支日历热力图
│   │   │   ├── CategoryTrendChart.vue     # 图表6: 分类支出趋势折线图
│   │   │   ├── SankeyChart.vue            # 图表7: 资金流向桑基图
│   │   │   ├── IncomePieChart.vue          # 图表8: 收入来源环形图
│   │   │   ├── BudgetGauge.vue            # 图表9: 预算进度仪表盘
│   │   │   ├── BudgetCompareChart.vue     # 图表10: 预算vs实际对比图
│   │   │   ├── AccountWaterfallChart.vue  # 图表11: 账户收支瀑布图
│   │   │   └── ImportQualityChart.vue     # 图表12: 导入质量统计图
│   │   ├── BillFilterPanel.vue     # 账单筛选面板
│   │   ├── ImportUploader.vue      # 文件上传组件
│   │   └── AccountCard.vue         # 账户卡片
│   ├── stores/                 # Pinia 状态管理
│   │   ├── bill.ts
│   │   ├── account.ts
│   │   └── dashboard.ts
│   └── types/                  # TypeScript 类型定义
│       ├── bill.d.ts
│       ├── account.d.ts
│       └── api.d.ts

3.3 核心页面设计

3.3.1 全景仪表盘 (DashboardView)

  • 资产负债表: 实时聚合各账户总资产、总负债(信用卡/白条),计算净资产。
  • 现金流概览: 本月总收入 vs 总支出,环比上月 (MoM) 增减百分比。
┌─────────────────────────────────────────────────────────┐
│  [净资产卡片]    [本月收入卡片]    [本月支出卡片]          │
│  ¥116,500       ¥15,000 ↑5.2%   ¥8,200 ↓3.1%          │
├──────────────────────────┬──────────────────────────────┤
│  收支趋势折线图 (ECharts) │  消费结构饼图 (ECharts)       │
│  (近12个月)               │  (本月各分类占比)             │
├──────────────────────────┴──────────────────────────────┤
│  各账户余额一栏表 (v-data-table)                         │
│  招行储蓄卡  ¥80,000 | 支付宝余额  ¥5,000 | ...         │
└─────────────────────────────────────────────────────────┘

3.3.2 账单导入页 (ImportView)

┌─────────────────────────────────────────────────────────┐
│  Step 1: 选择来源    ○ 支付宝  ○ 微信  ○ 招商银行  ...   │
├─────────────────────────────────────────────────────────┤
│  Step 2: 上传文件    [拖拽区域 / 点击选择文件]             │
├─────────────────────────────────────────────────────────┤
│  Step 3: 导入结果                                        │
│  ✅ 解析 150 条 | 导入 132 条 | 去重 15 条 | 合并 3 条    │
└─────────────────────────────────────────────────────────┘

3.3.3 多维分析页 (AnalysisView)

  • 组合漏斗筛选: 支持时间范围、金额区间、支付渠道、资金账户、交易分类的多条件交叉查询。
  • 可视化图表:
  • 消费结构: 饼图展示各大类的支出占比。
  • 资金流向 (Sankey Diagram) 桑基图直观展示“收入源 -> 资金池 (账户) -> 支出分类”的完整链路。

桑基图核心配置:

// components/charts/SankeyChart.vue
const option: EChartsOption = {
  series: [{
    type: 'sankey',
    data: nodes,   // [{ name: '工资' }, { name: '招行卡' }, { name: '餐饮' }]
    links: links,  // [{ source: '工资', target: '招行卡', value: 15000 }]
    emphasis: { focus: 'adjacency' },
    lineStyle: { color: 'gradient', curveness: 0.5 },
    label: { position: 'right' }
  }]
}

4. 关键业务流程

4.1 账单导入全流程状态机

stateDiagram-v2
    [*] --> FileUploaded: 用户上传文件
    FileUploaded --> Parsing: 开始解析
    Parsing --> ParseSuccess: Provider 解析成功
    Parsing --> ParseFailed: 格式错误/文件损坏

    ParseSuccess --> Translating: 规则引擎转换
    Translating --> TranslateSuccess: 转换完成
    TranslateSuccess --> Deduplicating: 去重处理

    Deduplicating --> ImportComplete: 写入数据库
    ImportComplete --> [*]: 返回导入报告

    ParseFailed --> [*]: 返回错误信息

4.2 预算预警触发流程

sequenceDiagram
    participant SVC as BudgetService
    participant DB as Database
    participant FE as 前端

    Note over SVC: 每次账单写入后触发
    SVC->>DB: 查询本月已用 (SUM by category)
    DB-->>SVC: 已用金额
    SVC->>DB: 查询预算限额
    DB-->>SVC: 预算配置

    alt 已用 >= 100% 限额
        SVC-->>FE: 超支预警 (红色标记)
    else 已用 >= 80% 限额
        SVC-->>FE: 临近预警 (橙色标记)
    else 正常
        SVC-->>FE: 正常状态 (绿色)
    end

5. Translate 规则配置规范

5.1 分类规则配置示例 (YAML)

# configs/rules/category_rules.yaml
category_rules:
  # --- 餐饮 ---
  - match: { field: "peer", pattern: "星巴克|瑞幸|Luckin" }
    category: "餐饮-咖啡"
    priority: 10

  - match: { field: "peer", pattern: "美团|饿了么|美团外卖" }
    category: "餐饮-外卖"
    priority: 10

  - match: { field: "item", pattern: "早餐|午餐|晚餐|堂食" }
    category: "餐饮-正餐"
    priority: 5

  # --- 交通 ---
  - match: { field: "peer", pattern: "滴滴|高德打车|T3出行" }
    category: "交通-打车"
    priority: 10

  - match: { field: "peer", pattern: "12306|铁路" }
    category: "交通-火车"
    priority: 10

  # --- 购物 ---
  - match: { field: "peer", pattern: "淘宝|天猫|拼多多" }
    category: "购物-电商"
    priority: 3

  # --- 兜底规则 ---
  - match: { field: "peer", pattern: ".*" }
    category: "其他-未分类"
    priority: 0

5.2 账户路由配置示例 (YAML)

# configs/rules/account_rules.yaml
account_rules:
  - provider: "alipay"
    method_match: "招商银行信用卡"
    account_name: "招行信用卡"

  - provider: "alipay"
    method_match: "花呗"
    account_name: "花呗"

  - provider: "alipay"
    method_match: "余额宝|账户余额"
    account_name: "支付宝余额"

  - provider: "wechat"
    method_match: "零钱"
    account_name: "微信零钱"

  - provider: "wechat"
    method_match: "招商银行"
    account_name: "招行储蓄卡"

6. 非功能性设计

6.1 数据隐私 (Local-First)

设计要点 实现方式
本地部署 默认使用 SQLite单一二进制文件部署无需外部数据库
文件处理 上传的原始账单文件仅在本地解析后即删除,不持久存储原始文件
无远程调用 ETL 管道全过程本地执行,不依赖任何云端 API
可选 MySQL 如需多端访问,可配置切换到 MySQL但建议内网部署

6.2 性能设计

场景 指标 措施
账单导入 1000 条/秒 GORM 批量插入 (CreateInBatches)
账单列表查询 < 200ms 复合索引 (user_id, pay_time, category_id)
仪表盘聚合 < 500ms 数据库聚合查询 + 前端缓存 (Pinia)
去重匹配 O(n·m) 按时间窗口分桶预排序,减少比较次数

6.3 可扩展性设计

  • 新增 Provider:实现 provider.Provider 接口,注册到工厂即可
  • 新增规则YAML 配置热加载,无需重启
  • 新增图表ECharts 组件化封装,新图表只需新建 Vue 组件

7. 参考项目代码复用说明

7.1 从 double-entry-generator 复用的部分

模块 源路径 复用说明
IR 结构 pkg/ir/ir.go 复用 Order 结构体核心字段,扩展 ProviderNameDataWeight 等字段
Alipay Provider pkg/provider/alipay/ 复用 CSV 解析、GBK 编码处理、字段映射逻辑
WeChat Provider pkg/provider/wechat/ 复用 CSV/XLSX 解析、跳过表头逻辑
退款后处理 alipay.postProcess() 复用退款订单匹配和交易关闭处理逻辑

7.2 需要新增/改造的部分

模块 改造说明
Provider 接口 从直接函数调用改为 Web API 触发,增加文件上传处理
Translate 层 原项目的 Compiler 输出 Beancount 文本,改为输出数据库 Model
去重引擎 原项目不涉及,需完全新写
规则配置 原项目配置固定,改为数据库存储 + YAML 双模式,支持用户自定义

8. 部署架构

graph LR
    subgraph Local["本地部署 (推荐)"]
        Binary["Go 单体二进制"]
        SQLite["SQLite 数据库文件"]
        StaticFiles["Vue 前端静态文件 (embed)"]
        Binary --> SQLite
        Binary --> StaticFiles
    end

    subgraph Docker["Docker 部署 (可选)"]
        Container["Docker Container"]
        Volume["Data Volume"]
        Container --> Volume
    end

    User["浏览器"] -->|http://localhost:8080| Binary
    User -->|http://host:8080| Container

推荐部署方式:使用 Go 的 embed 包将编译后的 Vue 前端静态资源嵌入后端二进制文件,实现单文件部署,配合 SQLite 达到真正的 Local-First 体验。