32 KiB
32 KiB
系统详细设计说明书 (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-generator 的 Provider → 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-generator 的 ir.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 结构体核心字段,扩展 ProviderName、DataWeight 等字段 |
| 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 体验。