69 KiB
ProjectMoneyX — 系统详细设计说明书(DDS)
版本:v1.0 | 日期:2026-03-03 | 状态:综合定稿 技术栈:后端 Golang + GIN + GORM | 前端 Vue3 + TypeScript + Vuetify | 数据库 SQLite
1. 设计目标与范围
1.1 设计目标
ProjectMoneyX 定位为 Firefly III 生态的本地化多源账单数据治理中间件,核心目标是将分散在支付平台、银行、生活服务及加密货币交易所的零散账单,经过标准化清洗、智能去重与链路合并后,无缝推送至 Firefly III / Data Importer。
本系统需实现以下完整处理链路:
- 原始文件接入与批次化管理
- 多平台账单解析与字段标准化(插件化架构)
- 统一交易模型入库(SQLite 持久化)
- 严格去重 → 模糊去重 → 链路合并(三层递进)
- 分类 / 账户 / 标签 / 商户归一化规则映射
- 导出为 Firefly III / Data Importer 可消费的数据格式(API / CSV)
- 提供预览、审计、失败重试与全链路可回溯能力
1.2 本期范围(V1.0 / MVP)
| 范围项 | 说明 |
|---|---|
| 账单来源 | 支付宝、微信支付(优先);建设银行、工商银行(次优先) |
| 文件格式 | CSV / Excel / 常见文本格式 |
| 持久化 | 本地 SQLite |
| 去重 | 基础去重(严格去重)+ 转账闭环链路合并 |
| 规则映射 | 基础规则映射(分类 / 账户) |
| 导入方式 | API 推送 + 中间文件导出 |
| 交互 | 导入预览与确认、失败重试 |
1.3 非目标(Out of Scope)
- 不直接登录第三方平台自动抓取账单
- 不实现完整 BI 分析平台
- 不替代 Firefly III 本体记账能力
- 不做高频实时同步(本期以"批量导入"为主)
2. 需求分析与架构判断
2.1 核心业务意图
PRD 的真实意图不是"导入工具",而是一个本地账单数据治理层。系统重心在于:
- 多源异构字段收敛:屏蔽支付宝、微信、银行等平台字段差异
- 多因子去重与链路恢复:消除重复流水,还原真实交易链路
- 可沉淀规则体系:分类/账户/标签映射可长期积累、可解释、可迁移
2.2 关键设计约束
| # | 约束 | 说明 |
|---|---|---|
| 1 | 本地优先 | 财务数据敏感,必须本地部署,默认不上传云端 |
| 2 | 插件化解析器 | 平台格式变化频繁,适配逻辑必须隔离 |
| 3 | 统一交易模型稳定 | 避免下游 Firefly III 或上游平台格式污染核心域模型 |
| 4 | 支付宝分类为主标准 | 支付宝拥有最丰富的 22 种交易分类,其他平台应映射到此分类体系 |
| 5 | 微信分类需推断 | 微信"交易类型"较粗,需结合"商品"字段推断细分分类 |
2.3 技术风险识别
| # | 风险 | 影响 | 应对策略 |
|---|---|---|---|
| 1 | 平台格式变更 | 解析器失效 | 插件化 Parser + 版本化适配 |
| 2 | 模糊去重误判 | 误合并 / 漏合并 | 分层策略 + 低置信度进人工确认队列 |
| 3 | 转账闭环识别不准 | 内部转账漏匹配 | 多因子评分 + 可配置参数 |
| 4 | Firefly III 映射不完整 | 导入失败 | 导入前校验 + 失败可重试 |
| 5 | 规则增长后性能退化 | 响应变慢 | 规则预筛选 + 索引优化 |
3. 总体架构设计
3.1 系统分层架构图
系统采用经典的领域驱动设计(DDD)分层思想,严格隔离各平台差异:
graph TD
subgraph "展现层 (Vue3 + TypeScript + Vuetify)"
UI_Import["导入中心<br/>文件上传 / 批次管理"]
UI_Preview["数据清洗预览<br/>标准化结果 / 字段对比"]
UI_Dedup["去重处理<br/>重复确认 / 链路合并"]
UI_Rule["规则管理<br/>分类 / 账户 / 标签配置"]
UI_Task["导入任务<br/>执行 / 结果 / 重试"]
UI_Audit["数据审计<br/>全链路追溯"]
UI_Setting["系统设置<br/>Firefly 连接 / 参数配置"]
end
subgraph "接入层 (GIN RESTful API)"
API_Import["导入 API"]
API_Data["数据流转 API"]
API_Rule["规则管理 API"]
API_Export["导出 API"]
API_Config["配置 API"]
end
subgraph "应用服务层 (Application Service)"
SVC_Batch["ImportBatchService<br/>批次编排"]
SVC_Pipeline["PipelineService<br/>ETL 流水线"]
end
subgraph "业务逻辑层 (Core Domain)"
subgraph "Adapter 解析层 (插件化)"
Parser_Alipay["支付宝解析器"]
Parser_WeChat["微信解析器"]
Parser_CCB["建行解析器"]
Parser_ICBC["工行解析器"]
end
Norm["Normalize 标准化引擎"]
Match["Match 去重引擎"]
Link["Link 链路合并引擎"]
Rule["Rule 规则映射引擎"]
Export["Export 导出推送引擎"]
end
subgraph "数据持久层 (GORM + SQLite)"
DB[(SQLite Database)]
end
subgraph "外部系统 (External)"
FF["Firefly III"]
DI["Data Importer"]
end
UI_Import & UI_Preview & UI_Dedup & UI_Rule & UI_Task & UI_Audit & UI_Setting --> API_Import & API_Data & API_Rule & API_Export & API_Config
API_Import & API_Data & API_Rule & API_Export --> SVC_Batch & SVC_Pipeline
SVC_Pipeline --> Parser_Alipay & Parser_WeChat & Parser_CCB & Parser_ICBC
Parser_Alipay & Parser_WeChat & Parser_CCB & Parser_ICBC --> Norm
Norm --> Match --> Link --> Rule --> Export
Export --> FF & DI
Norm & Match & Link & Rule & Export -.-> DB
3.2 分层职责定义
| 层级 | 技术选型 | 核心职责 |
|---|---|---|
| 展现层 | Vue3 + TS + Vuetify | 上传文件、批次管理、预览确认、规则配置、人工确认、导入结果展示 |
| 接入层 | GIN Framework | RESTful 接口,统一参数校验、错误处理、响应封装 |
| 应用服务层 | Go Service | 编排完整业务流程(ETL Pipeline),不承载具体解析规则 |
| Adapter 层 | Go Plugin Interface | 按平台解析原始文件,输出平台原始记录 DTO |
| Normalize 层 | Go Service | 统一字段、金额、方向、时间、分类原始值 |
| Match 层 | Go Service | 严格去重、模糊去重(多因子评分) |
| Link 层 | Go Service | 转账闭环、订单链路聚合 |
| Rule 层 | Go Service | 分类、账户、对手方、标签映射 |
| Export 层 | Go Service | 适配 Firefly III / Data Importer API 或 CSV/JSON |
| Repository 层 | GORM | 隔离数据库访问,面向领域对象持久化 |
| 数据持久层 | SQLite | 本地数据库,存储全量数据与审计链路 |
3.3 数据流转拓扑
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 多源账单导入 │ -> │ 解析器(Parser) │ -> │ 标准化入库 │ -> │ 去重与链路合并 │ -> │ 规则映射 │ -> │ 导出/推送 │
│ (文件上传) │ │ (Adapter层) │ │ (SQLite) │ │ (Dedup+Link) │ │ (Rule Engine) │ │ (API/CSV) │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
4. 系统模块划分
4.1 模块清单
| 模块 | 包名 | 职责 | 优先级 |
|---|---|---|---|
| 导入中心 | import-center |
文件上传、批次管理、来源识别 | P0 |
| 解析引擎 | parser-engine |
平台解析器注册、装载与执行 | P0 |
| 标准化引擎 | normalize-engine |
统一模型转换 | P0 |
| 去重引擎 | dedup-engine |
严格去重与模糊去重 | P0/P1 |
| 链路引擎 | link-engine |
转账闭环与订单链路合并 | P0 |
| 规则引擎 | rule-engine |
分类/账户/标签/商户归一化 | P0/P1 |
| 导入编排 | import-orchestrator |
导入预览、执行、重试 | P0 |
| 审计中心 | audit-center |
审计日志、处理链追溯 | P0 |
| 系统设置 | settings-center |
Firefly 配置、阈值参数 | P1 |
4.2 后端包结构(Go)
projectmoneyx-server/
├── cmd/
│ └── server/
│ └── main.go # 程序入口
├── internal/
│ ├── config/ # 配置管理
│ │ └── config.go
│ ├── handler/ # GIN Handler(接入层)
│ │ ├── import_handler.go
│ │ ├── transaction_handler.go
│ │ ├── dedup_handler.go
│ │ ├── rule_handler.go
│ │ ├── export_handler.go
│ │ └── audit_handler.go
│ ├── middleware/ # 中间件
│ │ ├── error_handler.go
│ │ └── cors.go
│ ├── service/ # 应用服务层
│ │ ├── import_batch_service.go
│ │ ├── pipeline_service.go
│ │ ├── dedup_service.go
│ │ ├── rule_apply_service.go
│ │ ├── export_service.go
│ │ └── audit_service.go
│ ├── domain/ # 领域层
│ │ ├── entity/ # 领域实体
│ │ │ ├── source_file.go
│ │ │ ├── raw_record.go
│ │ │ ├── transaction.go
│ │ │ ├── rule.go
│ │ │ ├── import_task.go
│ │ │ └── audit_log.go
│ │ ├── valueobject/ # 值对象
│ │ │ ├── direction.go
│ │ │ ├── platform.go
│ │ │ └── batch_status.go
│ │ └── repository/ # 仓储接口
│ │ ├── source_file_repo.go
│ │ ├── raw_record_repo.go
│ │ ├── transaction_repo.go
│ │ └── rule_repo.go
│ ├── parser/ # Adapter 解析层(插件化)
│ │ ├── interface.go # BillParser 接口定义
│ │ ├── registry.go # 解析器注册中心
│ │ ├── alipay/
│ │ │ └── alipay_parser.go
│ │ ├── wechat/
│ │ │ └── wechat_parser.go
│ │ ├── ccb/
│ │ │ └── ccb_parser.go
│ │ └── icbc/
│ │ └── icbc_parser.go
│ ├── normalize/ # 标准化引擎
│ │ └── normalizer.go
│ ├── matcher/ # 去重引擎
│ │ ├── strict_dedup.go
│ │ ├── fuzzy_dedup.go
│ │ └── scorer.go
│ ├── linker/ # 链路合并引擎
│ │ ├── transfer_linker.go
│ │ └── order_linker.go
│ ├── rule/ # 规则引擎
│ │ ├── engine.go
│ │ ├── category_mapper.go
│ │ ├── account_mapper.go
│ │ └── merchant_normalizer.go
│ ├── exporter/ # 导出引擎
│ │ ├── firefly_exporter.go
│ │ └── csv_exporter.go
│ ├── dao/ # 数据访问层(GORM 实现)
│ │ ├── source_file_dao.go
│ │ ├── raw_record_dao.go
│ │ ├── transaction_dao.go
│ │ └── rule_dao.go
│ └── dto/ # 数据传输对象
│ ├── request/
│ └── response/
├── migrations/ # 数据库迁移
├── web/ # 前端打包产物
└── go.mod
4.3 前端项目结构(Vue3)
projectmoneyx-web/
├── src/
│ ├── api/ # API 调用封装
│ │ ├── import.ts
│ │ ├── transaction.ts
│ │ ├── dedup.ts
│ │ ├── rule.ts
│ │ ├── export.ts
│ │ └── audit.ts
│ ├── views/ # 页面视图
│ │ ├── ImportCenterView.vue # 导入中心
│ │ ├── BatchDetailView.vue # 批次详情
│ │ ├── PreviewView.vue # 清洗结果预览
│ │ ├── DedupReviewView.vue # 去重处理
│ │ ├── RuleConfigView.vue # 规则配置
│ │ ├── ImportTaskView.vue # 导入任务
│ │ ├── AuditTraceView.vue # 审计追溯
│ │ └── SettingsView.vue # 系统设置
│ ├── components/ # 可复用组件
│ │ ├── FileUploader.vue
│ │ ├── TransactionTable.vue
│ │ ├── RuleEditor.vue
│ │ ├── DedupCompare.vue
│ │ └── AuditTimeline.vue
│ ├── stores/ # Pinia 状态管理
│ │ ├── importStore.ts
│ │ ├── transactionStore.ts
│ │ └── ruleStore.ts
│ ├── types/ # TypeScript 类型定义
│ │ ├── transaction.ts
│ │ ├── rule.ts
│ │ └── common.ts
│ ├── router/ # Vue Router
│ │ └── index.ts
│ ├── plugins/ # Vuetify 等插件
│ │ └── vuetify.ts
│ ├── App.vue
│ └── main.ts
├── package.json
├── tsconfig.json
└── vite.config.ts
5. 核心业务流程设计
5.1 主流程时序图
sequenceDiagram
participant U as 用户
participant FE as Web UI (Vue3)
participant API as GIN API
participant SVC as Pipeline Service
participant PARSER as Parser Engine
participant NORM as Normalize Engine
participant MATCH as Match/Link Engine
participant RULE as Rule Engine
participant EXP as Export Engine
participant DB as SQLite (GORM)
participant FF as Firefly III / Data Importer
U->>FE: 上传账单文件(拖拽/选择)
FE->>API: POST /api/v1/import/batches (multipart)
API->>SVC: createBatch(files, platform)
SVC->>DB: 保存 source_files + 创建 import_batch
SVC->>PARSER: 自动识别来源 → 加载对应 Parser
PARSER->>PARSER: 逐行解析原始数据
PARSER->>DB: 批量保存 raw_records
SVC->>NORM: 标准化转换
NORM->>NORM: 字段映射、金额归一、时区对齐
NORM->>DB: 批量保存 transactions (status=PENDING_CLEAN)
SVC->>MATCH: 执行去重流程
MATCH->>MATCH: 1. 严格去重(精确匹配)
MATCH->>MATCH: 2. 模糊去重(多因子评分)
MATCH->>MATCH: 3. 链路合并(转账闭环)
MATCH->>DB: 保存 dedup_relations / link_relations
SVC->>RULE: 应用映射规则
RULE->>RULE: 对手方归一 → 分类映射 → 账户映射 → 标签映射
RULE->>DB: 更新 category_mapped / account / tag
API-->>FE: 返回预览数据
U->>FE: 查看预览,确认无误
FE->>API: POST /api/v1/import/tasks (确认导入)
API->>EXP: 执行导出推送
EXP->>FF: 调用 API 推送 / 生成 CSV 下载
FF-->>EXP: 返回导入结果
EXP->>DB: 保存 import_results, 更新任务状态
API-->>FE: 展示导入结果(成功/失败统计)
5.2 批次状态机
stateDiagram-v2
[*] --> CREATED: 创建批次
CREATED --> UPLOADED: 文件上传完成
UPLOADED --> PARSING: 触发解析
PARSING --> PARSED: 解析完成
PARSED --> NORMALIZING: 触发标准化
NORMALIZING --> NORMALIZED: 标准化完成
NORMALIZED --> MATCHING: 触发去重/链路
MATCHING --> MATCHED: 去重/链路完成
MATCHED --> RULE_APPLYING: 触发规则映射
RULE_APPLYING --> PREVIEW_READY: 规则映射完成,可预览
PREVIEW_READY --> IMPORTING: 用户确认导入
IMPORTING --> IMPORT_SUCCESS: 全部导入成功
IMPORTING --> PARTIAL_FAILED: 部分失败
IMPORTING --> IMPORT_FAILED: 全部失败
PARTIAL_FAILED --> RETRYING: 用户重试失败项
IMPORT_FAILED --> RETRYING: 用户重试
RETRYING --> IMPORT_SUCCESS: 重试成功
RETRYING --> PARTIAL_FAILED: 仍有失败
note right of PARSING: 解析失败时\n状态回退至 UPLOADED\n记录错误信息
note right of PREVIEW_READY: 用户可在此阶段\n查看预览结果\n人工确认去重
5.3 ETL Pipeline 编排逻辑
// PipelineService 负责编排整个 ETL 流程
type PipelineService struct {
parserRegistry *parser.Registry
normalizer *normalize.Normalizer
strictDedup *matcher.StrictDedup
fuzzyDedup *matcher.FuzzyDedup
transferLinker *linker.TransferLinker
ruleEngine *rule.Engine
batchRepo repository.ImportBatchRepo
transactionRepo repository.TransactionRepo
}
func (s *PipelineService) Process(ctx context.Context, batchID string) error {
// 阶段 1: 解析
rawRecords, err := s.parse(ctx, batchID)
if err != nil { return err }
// 阶段 2: 标准化
transactions, err := s.normalize(ctx, rawRecords)
if err != nil { return err }
// 阶段 3: 严格去重
transactions, err = s.strictDedup.Execute(ctx, transactions)
if err != nil { return err }
// 阶段 4: 模糊去重(P1 阶段)
transactions, err = s.fuzzyDedup.Execute(ctx, transactions)
if err != nil { return err }
// 阶段 5: 链路合并
transactions, err = s.transferLinker.Execute(ctx, transactions)
if err != nil { return err }
// 阶段 6: 规则映射
err = s.ruleEngine.Apply(ctx, transactions)
if err != nil { return err }
// 更新批次状态为 PREVIEW_READY
return s.batchRepo.UpdateStatus(ctx, batchID, "PREVIEW_READY")
}
6. 账单解析与标准化设计
6.1 解析器接口设计
// BillParser 是所有平台解析器必须实现的接口
type BillParser interface {
// Platform 返回平台标识符,如 "alipay", "wechat", "ccb"
Platform() string
// Detect 根据文件元信息和表头判断是否为本平台文件
Detect(fileMeta FileMeta, header []string) bool
// Parse 解析指定文件,返回原始记录列表
Parse(ctx context.Context, reader io.Reader) ([]RawBillRecord, error)
}
// FileMeta 文件元信息
type FileMeta struct {
FileName string
FileType string // csv, xlsx, txt
FileHash string
FileSize int64
}
// RawBillRecord 原始账单记录(平台相关的中间结构)
type RawBillRecord struct {
SourcePlatform string
SourceRecordID string
RawFields map[string]string // 原始 K-V 字段
RowNo int
RowFingerprint string
}
6.2 解析器注册中心
// Registry 解析器注册中心
type Registry struct {
parsers []BillParser
}
func NewRegistry() *Registry {
r := &Registry{}
// 注册所有解析器
r.Register(&alipay.Parser{})
r.Register(&wechat.Parser{})
r.Register(&ccb.Parser{})
r.Register(&icbc.Parser{})
return r
}
// Detect 自动检测文件对应的解析器
func (r *Registry) Detect(meta FileMeta, header []string) (BillParser, error) {
for _, p := range r.parsers {
if p.Detect(meta, header) {
return p, nil
}
}
return nil, ErrUnknownPlatform
}
6.3 支付宝解析规则
支付宝账单数据维度:
交易时间 | 交易分类 | 交易对方 | 对方账号 | 商品说明 | 收/支 | 金额 | 收/付款方式 | 交易状态 | 交易订单号 | 商家订单号 | 备注
支付宝字段映射表
| 原字段 | 目标字段 | 映射说明 |
|---|---|---|
| 交易时间 | trade_time |
解析为 time.Time,统一 UTC+8 |
| 交易分类 | category_raw |
直接作为原始分类(22 种标准分类) |
| 交易对方 | counterparty |
交易对手名称 |
| 对方账号 | counterparty_account |
存入扩展字段 |
| 商品说明 | merchant_name / note |
优先作为商户名,辅助作为备注 |
| 收/支 | direction |
"收入" → income, "支出" → expense, "其他" → other |
| 金额 | amount |
去除 ¥ 符号,解析为 Decimal 正数 |
| 收/付款方式 | payment_method |
存入扩展字段,可用于账户映射 |
| 交易状态 | trade_status |
"交易成功" / "退款成功" 等 |
| 交易订单号 | source_record_id / order_id |
去除空格,作为唯一标识 |
| 商家订单号 | merchant_order_id / parent_order_id |
可用于链路关联 |
| 备注 | note |
补充备注 |
关键设计决策:支付宝拥有最丰富的 22 种交易分类,系统将其作为全局统一分类基准字典。所有其他平台的交易分类最终都应映射到此套分类枚举。
支付宝标准分类枚举(22 类)
| 编号 | 分类名称 | 编号 | 分类名称 |
|---|---|---|---|
| 1 | 餐饮美食 | 12 | 退款 |
| 2 | 投资理财 | 13 | 教育培训 |
| 3 | 日用百货 | 14 | 住房物业 |
| 4 | 数码电器 | 15 | 酒店旅游 |
| 5 | 交通出行 | 16 | 文化休闲 |
| 6 | 充值缴费 | 17 | 运动户外 |
| 7 | 信用借还 | 18 | 爱车养车 |
| 8 | 转账红包 | 19 | 商业服务 |
| 9 | 生活服务 | 20 | 母婴亲子 |
| 10 | 家居家装 | 21 | 收入 |
| 11 | 医疗健康 | 22 | 其他 |
6.4 微信解析规则
微信账单数据维度:
交易时间 | 交易类型 | 交易对方 | 商品 | 收/支 | 金额(元) | 支付方式 | 当前状态 | 交易单号 | 商户单号 | 备注
微信字段映射表
| 原字段 | 目标字段 | 映射说明 |
|---|---|---|
| 交易时间 | trade_time |
解析为 time.Time,统一 UTC+8 |
| 交易类型 | category_raw |
存储原始类型,需通过推断映射到标准分类 |
| 交易对方 | counterparty |
交易对手名称 |
| 商品 | merchant_name / product_desc |
关键字段,用于推断实际消费分类 |
| 收/支 | direction |
"收入" → income, "支出" → expense |
| 金额(元) | amount |
去除 ¥ 符号,解析为 Decimal 正数 |
| 支付方式 | payment_method |
存入扩展字段 |
| 当前状态 | trade_status |
"支付成功" / "已退款" 等 |
| 交易单号 | source_record_id / order_id |
去除空格 |
| 商户单号 | merchant_order_id / parent_order_id |
链路关联 |
| 备注 | note |
补充备注 |
微信分类推断规则
微信"交易类型"多为支付动作(商户消费、扫二维码付款、转账、红包等),无实际消费语义。需结合"商品"字段进行关键词推断:
flowchart TD
A[微信原始记录] --> B{交易类型判断}
B -->|转账| C[转账红包]
B -->|微信红包| C
B -->|微信红包-退款| D[退款]
B -->|xxx-退款| D
B -->|商户消费 / 扫二维码付款| E{商品关键词推断}
E -->|美团/外卖/餐厅/咖啡/麦当劳/肯德基| F[餐饮美食]
E -->|滴滴/打车/地铁/高铁/火车/机票/加油| G[交通出行]
E -->|京东/超市/便利店/百货| H[日用百货]
E -->|电费/水费/话费/燃气/宽带| I[充值缴费]
E -->|医院/药店/门诊| J[医疗健康]
E -->|电影/KTV/游戏/书籍| K[文化休闲]
E -->|酒店/景点/旅行| L[酒店旅游]
E -->|无法识别| M[其他 → 待人工补充]
推断规则配置示例
| 微信交易类型 | 商品关键词 | 推断标准分类 |
|---|---|---|
| 商户消费 | 美团 / 外卖 / 餐厅 / 咖啡 / 面包 | 餐饮美食 |
| 商户消费 | 滴滴 / 打车 / 地铁 / 高铁 / 加油 | 交通出行 |
| 商户消费 | 京东 / 超市 / 便利店 / 百货 | 日用百货 |
| 商户消费 | 电费 / 水费 / 话费 / 燃气 | 充值缴费 |
| 商户消费 | 医院 / 药店 / 体检 | 医疗健康 |
| 商户消费 | 电影 / 游戏 / 书籍 | 文化休闲 |
| 转账 | — | 转账红包 |
| 微信红包 | — | 转账红包 |
| xxx-退款 | — | 退款 |
设计原则:若商品内容无法识别关键词,先落入"其他"分类,并允许用户通过规则管理补充映射。系统应记录未命中规则的记录,便于后续规则完善。
6.5 统一交易模型
classDiagram
class Transaction {
+string ID
+string TransactionID
+string BatchID
+string SourcePlatform
+string SourceRecordID
+time.Time TradeTime
+decimal Amount
+string Currency
+string Direction
+string Counterparty
+string MerchantName
+string CategoryRaw
+string CategoryMapped
+string OrderID
+string ParentOrderID
+string PaymentMethod
+string Note
+string RawPayload
+string Status
+time.Time CreatedAt
+time.Time UpdatedAt
}
class Direction {
<<enumeration>>
INCOME
EXPENSE
TRANSFER
REFUND
FEE
OTHER
}
class TransactionStatus {
<<enumeration>>
PENDING_CLEAN
CLEANED
PENDING_REVIEW
READY_TO_IMPORT
IMPORTING
IMPORTED
FAILED
DUPLICATE
}
Transaction --> Direction
Transaction --> TransactionStatus
标准化规则
| 规则项 | 说明 |
|---|---|
| 时间 | 统一存储为 Asia/Shanghai (UTC+8) |
| 金额 | 统一使用正数(decimal(18,6)),方向独立用 direction 表达 |
| 币种 | 默认 CNY,后续可扩展多币种 |
| 状态 | 初始为 PENDING_CLEAN |
| 原始快照 | 完整写入 raw_payload(JSON),确保审计可追溯 |
| 指纹 | 对关键字段做 SHA256,用于严格去重 |
7. 去重与链路合并设计
7.1 三层递进处理模型
系统采用"基础去重 → 模糊去重 → 链路合并"三层递进去重模型:
flowchart TD
A[新标准化交易入库] --> B{第一层:严格去重}
B -->|命中| X[标记为 DUPLICATE<br/>建立 dedup_relation]
B -->|未命中| C{第二层:模糊去重}
C -->|高置信度 ≥85| X
C -->|中置信度 60-84| D[标记为 PENDING_REVIEW<br/>进入人工确认队列]
C -->|低置信度 <60| E{第三层:链路合并}
E -->|命中转账闭环| F[合并为 Transfer<br/>建立 link_relation]
E -->|命中订单链路| G[聚合为订单链<br/>建立 parent_order]
E -->|未命中| H[保留为独立交易<br/>status=CLEANED]
style X fill:#ff6b6b,color:#fff
style D fill:#ffd93d,color:#333
style F fill:#6bcb77,color:#fff
style G fill:#6bcb77,color:#fff
style H fill:#4d96ff,color:#fff
7.2 严格去重(基础去重 — 精确匹配)
唯一判定键
采用三级唯一性判定,优先级从高到低:
| 优先级 | 判定键 | 适用场景 |
|---|---|---|
| 1 | source_platform + source_record_id |
同一平台重复导入 |
| 2 | source_file_hash + row_fingerprint |
同一文件重复上传 |
| 3 | order_id(若可信) |
跨批次订单号匹配 |
行指纹算法
对以下字段标准化后做 SHA256 哈希,生成 row_fingerprint:
func GenerateRowFingerprint(t *Transaction) string {
raw := fmt.Sprintf("%s|%s|%s|%s|%s|%s",
t.TradeTime.Format("2006-01-02 15:04"), // 分钟粒度
t.Amount.String(),
t.Direction,
normalizeString(t.Counterparty),
normalizeString(t.MerchantName),
t.OrderID,
)
hash := sha256.Sum256([]byte(raw))
return hex.EncodeToString(hash[:])
}
严格去重流程
func (s *StrictDedup) Execute(ctx context.Context, txns []*Transaction) ([]*Transaction, error) {
var result []*Transaction
for _, txn := range txns {
// 判定键 1: platform + record_id
exists, existingID := s.repo.FindByPlatformAndRecordID(
ctx, txn.SourcePlatform, txn.SourceRecordID)
if exists {
s.createDedupRelation(ctx, txn.ID, existingID, "strict", 100)
txn.Status = "DUPLICATE"
continue
}
// 判定键 2: file_hash + fingerprint
exists, existingID = s.repo.FindByFingerprint(ctx, txn.RowFingerprint)
if exists {
s.createDedupRelation(ctx, txn.ID, existingID, "strict", 100)
txn.Status = "DUPLICATE"
continue
}
result = append(result, txn)
}
return result, nil
}
7.3 模糊去重(多因子评分 — P1 阶段)
多因子评分模型
| 因子 | 分值 | 评分说明 |
|---|---|---|
| 时间在 ±5 分钟内 | 30 | 时间差越小得分越高,超出窗口直接 0 分 |
| 金额精确一致 | 30 | 金额一致得满分,差额在手续费容差内得部分分 |
| 交易方向一致 | 10 | 方向相同得满分 |
| 订单号相同/相近 | 15 | 完全一致 15 分,包含关系 10 分 |
| 对手方相似 | 10 | Levenshtein 相似度 + contains 判定 |
| 来源关联规则命中 | 5 | 预配置的平台关联规则 |
判定阈值(可配置)
| 分值范围 | 判定结果 | 处理方式 |
|---|---|---|
| ≥ 85 | 自动判定重复 | 自动标记 DUPLICATE |
| 60 ~ 84 | 疑似重复 | 标记 PENDING_REVIEW,进入人工确认队列 |
| < 60 | 不判定重复 | 保留独立交易 |
评分算法伪代码
type FuzzyScorer struct {
TimeWindow time.Duration // 默认 5 分钟
AmountEpsilon float64 // 金额容差(手续费)
}
func (s *FuzzyScorer) Score(a, b *Transaction) int {
score := 0
// 时间因子 (30分)
timeDiff := math.Abs(a.TradeTime.Sub(b.TradeTime).Minutes())
if timeDiff <= s.TimeWindow.Minutes() {
score += int(30 * (1 - timeDiff/s.TimeWindow.Minutes()))
}
// 金额因子 (30分)
if a.Amount.Equal(b.Amount) {
score += 30
} else if a.Amount.Sub(b.Amount).Abs().LessThan(decimal.NewFromFloat(s.AmountEpsilon)) {
score += 20
}
// 方向因子 (10分)
if a.Direction == b.Direction { score += 10 }
// 订单号因子 (15分)
if a.OrderID != "" && a.OrderID == b.OrderID {
score += 15
} else if strings.Contains(a.OrderID, b.OrderID) || strings.Contains(b.OrderID, a.OrderID) {
score += 10
}
// 对手方因子 (10分)
score += int(10 * counterpartySimilarity(a.Counterparty, b.Counterparty))
// 来源规则因子 (5分)
if s.platformLinked(a.SourcePlatform, b.SourcePlatform) { score += 5 }
return score
}
7.4 链路合并(转账闭环 + 订单链路)
7.4.1 转账闭环识别
典型场景:银行卡支出 1000 元(流向支付宝),支付宝收入 1000 元 → 合并为一笔内部转账。
识别规则(5 项全部满足):
- 金额一致
- 一条为支出(expense),一条为收入(income)
- 时间在可配置窗口内(默认 ±30 分钟)
- 来源平台不同但账户映射可闭环
- 非退款、非手续费类型
func (l *TransferLinker) Detect(a, b *Transaction) *LinkResult {
// 条件 1: 金额一致
if !a.Amount.Equal(b.Amount) { return nil }
// 条件 2: 方向互补
if !((a.Direction == "expense" && b.Direction == "income") ||
(a.Direction == "income" && b.Direction == "expense")) { return nil }
// 条件 3: 时间窗口
if math.Abs(a.TradeTime.Sub(b.TradeTime).Minutes()) > l.TimeWindow { return nil }
// 条件 4: 不同平台
if a.SourcePlatform == b.SourcePlatform { return nil }
// 条件 5: 非退款/手续费
if a.Direction == "refund" || b.Direction == "refund" { return nil }
if a.Direction == "fee" || b.Direction == "fee" { return nil }
return &LinkResult{
ParentTransactionID: selectPrimary(a, b).ID,
ChildTransactionID: selectSecondary(a, b).ID,
LinkType: "transfer",
FromAccount: mapToAccount(getExpenseSide(a, b)),
ToAccount: mapToAccount(getIncomeSide(a, b)),
}
}
7.4.2 订单链路合并
典型场景:京东订单 + 微信支付,一笔真实消费产生多条流水。
合并策略:
- 保留更完整的业务记录为主交易(优先保留有商品详情的记录)
- 其他记录挂为关联来源
- 形成
parent_order_id聚合链路
7.5 人工确认机制
sequenceDiagram
participant U as 用户
participant FE as Web UI
participant API as GIN API
participant DB as SQLite
FE->>API: GET /api/v1/dedup/reviews?batchId=xxx
API->>DB: 查询 status=PENDING_REVIEW 的记录
DB-->>API: 返回疑似重复关系列表
API-->>FE: 展示疑似重复对比(评分、命中因子、原始数据)
U->>FE: 确认合并 / 拒绝合并
FE->>API: POST /api/v1/dedup/reviews/{id}/confirm 或 /reject
API->>DB: 更新 dedup_relation 状态
API->>DB: 更新 transaction 状态
API-->>FE: 返回处理结果
8. 规则引擎设计
8.1 规则模型
classDiagram
class Rule {
+string ID
+string RuleType
+int Priority
+string PlatformScope
+JSON Conditions
+JSON Actions
+bool Enabled
+string Description
+time.Time CreatedAt
+time.Time UpdatedAt
}
class RuleType {
<<enumeration>>
CATEGORY_MAPPING
ACCOUNT_MAPPING
COUNTERPARTY_NORMALIZE
TAG_MAPPING
MERCHANT_NORMALIZE
FIREFLY_FIELD_MAPPING
}
class RuleHit {
+string ID
+string TransactionID
+string RuleID
+string MatchedCondition
+string BeforeValue
+string AfterValue
+time.Time CreatedAt
}
Rule --> RuleType
Rule "1" --> "*" RuleHit : produces
8.2 规则匹配条件
规则条件采用 JSON 结构化存储,支持以下匹配维度:
| 条件类型 | 字段 | 说明 | 示例 |
|---|---|---|---|
| 平台过滤 | platform |
指定生效平台 | "alipay" |
| 原始分类 | category_raw |
原始分类匹配 | "餐饮美食" |
| 关键词 | keywords |
商品/商户名关键词 | ["美团", "外卖"] |
| 正则 | regex |
正则表达式匹配 | "^滴滴.*出行$" |
| 金额范围 | amount_range |
[min, max] | [0, 50] |
| 方向 | direction |
收支方向 | "expense" |
| 对手方 | counterparty |
对手方包含 | "支付宝" |
规则条件 JSON 示例
{
"platform": "wechat",
"conditions": {
"category_raw": "商户消费",
"keywords": ["美团", "外卖", "饿了么"],
"direction": "expense"
},
"actions": {
"category_mapped": "餐饮美食",
"merchant_normalized": "外卖平台"
}
}
8.3 规则执行顺序
规则引擎按以下严格顺序执行,确保前置归一化提升后续规则命中率:
flowchart LR
A["1. 对手方归一化<br/>COUNTERPARTY_NORMALIZE"] --> B["2. 商户归一化<br/>MERCHANT_NORMALIZE"]
B --> C["3. 分类映射<br/>CATEGORY_MAPPING"]
C --> D["4. 账户映射<br/>ACCOUNT_MAPPING"]
D --> E["5. 标签映射<br/>TAG_MAPPING"]
E --> F["6. Firefly 字段映射<br/>FIREFLY_FIELD_MAPPING"]
执行原则:
- 同一类型内按
priority升序执行(数字越小优先级越高) - 先做归一再做分类,可提升规则命中率与稳定性
- 每条交易记录所有命中的规则 ID 和前后字段对比
8.4 规则引擎核心实现
type Engine struct {
ruleRepo repository.RuleRepo
hitRepo repository.RuleHitRepo
}
func (e *Engine) Apply(ctx context.Context, txns []*Transaction) error {
// 按类型分组加载启用的规则
ruleGroups := e.loadRulesGroupByType(ctx)
executionOrder := []string{
"COUNTERPARTY_NORMALIZE",
"MERCHANT_NORMALIZE",
"CATEGORY_MAPPING",
"ACCOUNT_MAPPING",
"TAG_MAPPING",
"FIREFLY_FIELD_MAPPING",
}
for _, txn := range txns {
for _, ruleType := range executionOrder {
rules := ruleGroups[ruleType]
for _, rule := range rules {
if !rule.MatchPlatform(txn.SourcePlatform) { continue }
if rule.MatchConditions(txn) {
before := txn.Snapshot()
rule.ApplyActions(txn)
after := txn.Snapshot()
// 记录命中日志
e.hitRepo.Save(ctx, &RuleHit{
TransactionID: txn.ID,
RuleID: rule.ID,
MatchedCondition: rule.ConditionsJSON,
BeforeValue: before,
AfterValue: after,
})
break // 同类型首条命中即停止(优先级控制)
}
}
}
}
return nil
}
8.5 可解释性设计
每条交易保留完整的规则命中记录,用于前端"为何被分到餐饮/交通"的解释展示:
| 信息项 | 说明 |
|---|---|
| 命中规则 ID | 关联 rules 表 |
| 命中条件摘要 | 匹配的具体关键词/正则 |
| 变更前值 | 规则执行前的字段值 |
| 变更后值 | 规则执行后的字段值 |
| 命中时间 | 规则执行时间戳 |
9. Firefly III / Data Importer 适配设计
9.1 两段式规则映射策略
flowchart LR
subgraph "第一阶段 — ProjectMoneyX 负责"
A1["字段级映射<br/>异构字段 → 统一模型"]
A2["业务分类映射<br/>对手方/描述 → 分类/标签"]
A3["商户名归一化<br/>别名 → 统一名称"]
end
subgraph "第二阶段 — Firefly III 负责"
B1["最后一层字段适配"]
B2["临时补充规则"]
B3["导入格式兼容"]
end
A1 --> A2 --> A3 --> B1 --> B2 --> B3
9.2 导出模式
模式 A:API 推送模式(优先)
sequenceDiagram
participant EXP as Export Engine
participant DI as Data Importer
participant FF as Firefly III
EXP->>EXP: 构建 Firefly 兼容 DTO
EXP->>DI: POST /api/v1/import (batch)
DI->>FF: 转发至 Firefly III
FF-->>DI: 返回导入结果
DI-->>EXP: 返回成功/失败明细
EXP->>EXP: 更新 import_results 表
模式 B:中间文件导出模式
- 生成完全符合 Data Importer 规范的标准 CSV / JSON
- 用户手动下载后在 Data Importer 中执行导入
- 适合 API 不可用或权限受限场景
9.3 Firefly 交易类型映射
| 内部 Direction | Firefly Type | 说明 |
|---|---|---|
expense |
withdrawal |
支出 |
income |
deposit |
收入 |
transfer |
transfer |
内部转账 |
refund |
deposit |
退款(作为收入处理) |
fee |
withdrawal |
手续费(作为支出处理) |
9.4 导入前校验清单
| # | 校验项 | 说明 |
|---|---|---|
| 1 | 必填字段完整性 | amount, trade_time, direction 不可为空 |
| 2 | 金额格式合法性 | 必须为正数,精度不超过 6 位小数 |
| 3 | 时间格式合法性 | 必须为有效日期时间 |
| 4 | 账户映射完整性 | 来源平台必须有对应的 Firefly 账户映射 |
| 5 | 重复导入拦截 | 检查 transaction_id 是否已在 Firefly III 中存在 |
| 6 | 未确认记录检查 | 是否存在 PENDING_REVIEW 状态的疑似重复记录 |
9.5 导入后反馈
type ImportResult struct {
TaskID string
TransactionID string
Status string // success / failed
ErrorCode string // 错误码
ErrorMessage string // 错误描述
FireflyTxnID string // Firefly III 返回的交易 ID
RetryCount int // 已重试次数
CreatedAt time.Time
}
- 导入成功/失败数量统计
- 失败原因分类展示(字段缺失、格式错误、账户不存在等)
- 失败记录可单独重试(无需整批重做)
10. 数据库详细设计(SQLite + GORM)
10.1 ER 关系图
erDiagram
IMPORT_BATCHES ||--o{ SOURCE_FILES : contains
SOURCE_FILES ||--o{ RAW_RECORDS : contains
IMPORT_BATCHES ||--o{ TRANSACTIONS : generates
RAW_RECORDS ||--|| TRANSACTIONS : normalizes_to
TRANSACTIONS ||--o{ DEDUP_RELATIONS : source
TRANSACTIONS ||--o{ LINK_RELATIONS : linked
TRANSACTIONS ||--o{ RULE_HITS : matched
RULES ||--o{ RULE_HITS : referenced
IMPORT_BATCHES ||--o{ IMPORT_TASKS : owns
IMPORT_TASKS ||--o{ IMPORT_RESULTS : produces
TRANSACTIONS ||--o{ AUDIT_LOGS : traced
IMPORT_BATCHES {
string id PK "UUID 主键"
string status "批次状态"
int total_files "文件总数"
int total_records "记录总数"
int success_count "成功数"
int failed_count "失败数"
int duplicate_count "重复数"
datetime created_at "创建时间"
datetime updated_at "更新时间"
}
SOURCE_FILES {
string id PK "UUID 主键"
string batch_id FK "批次 ID"
string file_name "原始文件名"
string file_hash "文件 SHA256 哈希"
string source_platform "来源平台"
string file_type "csv/xlsx/txt"
int file_size "文件大小(bytes)"
datetime uploaded_at "上传时间"
}
RAW_RECORDS {
string id PK "UUID 主键"
string source_file_id FK "来源文件 ID"
int row_no "行号"
string source_platform "平台"
string source_record_id "原始流水号"
string row_fingerprint "行指纹 SHA256"
text raw_payload "原始 JSON 快照"
string parse_status "解析状态"
text parse_error "错误信息"
}
TRANSACTIONS {
string id PK "UUID 主键"
string transaction_id UK "业务唯一 ID"
string batch_id FK "导入批次"
string raw_record_id FK "原始记录 ID"
string source_platform "来源平台"
string source_record_id "原始记录号"
datetime trade_time "交易时间"
decimal amount "金额 decimal(18,6)"
string currency "币种"
string direction "方向"
string counterparty "对手方"
string merchant_name "商户名"
string category_raw "原始分类"
string category_mapped "映射分类"
string account_mapped "映射账户"
string tags "标签(逗号分隔)"
string order_id "订单号"
string parent_order_id "父链路号"
string payment_method "支付方式"
text note "备注"
text raw_payload "原始记录 JSON"
string status "状态"
string firefly_txn_id "Firefly 交易 ID"
datetime imported_at "导入时间"
datetime created_at "创建时间"
datetime updated_at "更新时间"
}
DEDUP_RELATIONS {
string id PK "UUID 主键"
string src_transaction_id FK "原交易 ID"
string target_transaction_id FK "目标交易 ID"
string relation_type "strict/fuzzy"
int confidence "置信度 0-100"
string status "auto/confirmed/rejected"
text reason_json "判定依据 JSON"
datetime created_at "创建时间"
}
LINK_RELATIONS {
string id PK "UUID 主键"
string parent_transaction_id FK "主交易 ID"
string child_transaction_id FK "子交易 ID"
string link_type "transfer/order/refund/fee"
text reason_json "关联依据 JSON"
datetime created_at "创建时间"
}
RULES {
string id PK "UUID 主键"
string rule_type "规则类型"
int priority "优先级(越小越高)"
string platform_scope "平台范围(all/alipay/wechat)"
text conditions_json "条件 JSON"
text actions_json "动作 JSON"
bool enabled "是否启用"
string description "规则描述"
datetime created_at "创建时间"
datetime updated_at "更新时间"
}
RULE_HITS {
string id PK "UUID 主键"
string transaction_id FK "交易 ID"
string rule_id FK "规则 ID"
text matched_condition "命中条件摘要"
text before_value "变更前值"
text after_value "变更后值"
datetime created_at "执行时间"
}
IMPORT_TASKS {
string id PK "UUID 主键"
string batch_id FK "批次 ID"
string export_mode "api/csv"
string status "pending/running/success/partial_failed/failed"
int total_count "总记录数"
int success_count "成功数"
int failed_count "失败数"
datetime started_at "开始时间"
datetime finished_at "完成时间"
}
IMPORT_RESULTS {
string id PK "UUID 主键"
string task_id FK "任务 ID"
string transaction_id FK "交易 ID"
string status "success/failed"
string error_code "错误码"
text error_message "错误描述"
string firefly_txn_id "Firefly 返回 ID"
int retry_count "重试次数"
datetime created_at "创建时间"
}
AUDIT_LOGS {
string id PK "UUID 主键"
string entity_type "实体类型"
string entity_id "实体 ID"
string action "操作类型"
text before_snapshot "变更前快照"
text after_snapshot "变更后快照"
string operator "操作者"
datetime created_at "操作时间"
}
10.2 关键索引设计
| 表名 | 索引名 | 索引列 | 用途 |
|---|---|---|---|
transactions |
idx_txn_platform_record |
source_platform, source_record_id |
严格去重判定键 1 |
transactions |
idx_txn_fingerprint |
row_fingerprint |
严格去重判定键 2 |
transactions |
idx_txn_batch |
batch_id |
批次查询 |
transactions |
idx_txn_trade_time |
trade_time |
模糊去重时间分桶 |
transactions |
idx_txn_order |
order_id |
订单号匹配 |
transactions |
idx_txn_status |
status |
状态过滤 |
source_files |
idx_sf_hash |
file_hash |
文件重复上传拦截 |
raw_records |
idx_rr_fingerprint |
row_fingerprint |
行级去重 |
rules |
idx_rule_type_priority |
rule_type, priority |
规则执行顺序 |
dedup_relations |
idx_dedup_src |
src_transaction_id |
去重关系查询 |
10.3 GORM Model 定义示例
// Transaction GORM 模型
type Transaction struct {
ID string `gorm:"primaryKey;type:varchar(36)"`
TransactionID string `gorm:"uniqueIndex;type:varchar(64)"`
BatchID string `gorm:"index;type:varchar(36)"`
RawRecordID string `gorm:"type:varchar(36)"`
SourcePlatform string `gorm:"type:varchar(32);index:idx_platform_record"`
SourceRecordID string `gorm:"type:varchar(128);index:idx_platform_record"`
TradeTime time.Time `gorm:"index;not null"`
Amount decimal.Decimal `gorm:"type:decimal(18,6);not null"`
Currency string `gorm:"type:varchar(16);default:'CNY'"`
Direction string `gorm:"type:varchar(16);not null"`
Counterparty string `gorm:"type:varchar(255)"`
MerchantName string `gorm:"type:varchar(255)"`
CategoryRaw string `gorm:"type:varchar(128)"`
CategoryMapped string `gorm:"type:varchar(128)"`
AccountMapped string `gorm:"type:varchar(128)"`
Tags string `gorm:"type:varchar(512)"`
OrderID string `gorm:"index;type:varchar(128)"`
ParentOrderID string `gorm:"type:varchar(128)"`
PaymentMethod string `gorm:"type:varchar(128)"`
Note string `gorm:"type:text"`
RawPayload string `gorm:"type:text"`
RowFingerprint string `gorm:"index;type:varchar(64)"`
Status string `gorm:"index;type:varchar(32);default:'PENDING_CLEAN'"`
FireflyTxnID string `gorm:"type:varchar(128)"`
ImportedAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
func (Transaction) TableName() string { return "transactions" }
11. API 接口设计(GIN RESTful)
11.1 统一响应格式
type Response struct {
Code int `json:"code"` // 0=成功, 非0=错误码
Message string `json:"message"` // 成功/错误说明
Data interface{} `json:"data"` // 业务数据
}
type PageResponse struct {
Code int `json:"code"`
Message string `json:"message"`
Data interface{} `json:"data"`
Total int64 `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
}
11.2 导入中心 API
| 方法 | 路径 | 说明 |
|---|---|---|
POST |
/api/v1/import/batches |
上传账单文件,创建批次 |
GET |
/api/v1/import/batches |
获取批次列表 |
GET |
/api/v1/import/batches/:batchId |
获取批次详情 |
POST |
/api/v1/import/batches/:batchId/process |
触发解析与清洗流水线 |
GET |
/api/v1/import/batches/:batchId/preview |
获取清洗预览结果 |
DELETE |
/api/v1/import/batches/:batchId |
删除批次 |
上传账单文件
POST /api/v1/import/batches
Content-Type: multipart/form-data
参数:
- files[] (必填) 账单文件
- sourcePlatform (可选) 指定来源平台, 如 "alipay", "wechat"
- autoDetect (可选) 是否自动识别, 默认 true
响应:
{
"code": 0,
"message": "ok",
"data": {
"batchId": "550e8400-e29b-41d4-a716-446655440000",
"status": "UPLOADED",
"filesCount": 2,
"detectedPlatforms": ["alipay", "wechat"]
}
}
11.3 交易记录 API
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/api/v1/transactions |
分页查询交易记录 |
GET |
/api/v1/transactions/:id |
获取交易详情(含规则命中记录) |
GET |
/api/v1/transactions/:id/trace |
获取交易完整处理链路 |
11.4 去重确认 API
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/api/v1/dedup/reviews |
获取疑似重复列表 |
GET |
/api/v1/dedup/reviews/:reviewId |
获取重复详情(含评分因子) |
POST |
/api/v1/dedup/reviews/:reviewId/confirm |
确认合并 |
POST |
/api/v1/dedup/reviews/:reviewId/reject |
拒绝合并 |
11.5 规则管理 API
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/api/v1/rules |
获取规则列表(支持类型/平台过滤) |
POST |
/api/v1/rules |
创建规则 |
PUT |
/api/v1/rules/:id |
更新规则 |
DELETE |
/api/v1/rules/:id |
删除规则 |
POST |
/api/v1/rules/evaluate |
重新评估规则(修改规则后触发) |
POST |
/api/v1/rules/:id/test |
测试规则命中预览 |
11.6 导入/导出 API
| 方法 | 路径 | 说明 |
|---|---|---|
POST |
/api/v1/import/tasks |
创建导入任务(确认导入到 Firefly) |
GET |
/api/v1/import/tasks/:taskId |
获取导入任务详情 |
POST |
/api/v1/import/tasks/:taskId/retry |
重试失败项 |
GET |
/api/v1/export/csv/:batchId |
导出批次为 CSV 文件 |
11.7 审计与系统 API
| 方法 | 路径 | 说明 |
|---|---|---|
GET |
/api/v1/audit/logs |
获取操作日志列表 |
GET |
/api/v1/settings |
获取系统配置 |
PUT |
/api/v1/settings |
更新系统配置(Firefly 连接等) |
POST |
/api/v1/settings/test-connection |
测试 Firefly III 连接 |
12. 前端页面设计(Vue3 + Vuetify)
12.1 信息架构与导航
graph LR
NAV[侧边导航栏] --> A[导入中心]
NAV --> B[数据清洗]
NAV --> C[去重处理]
NAV --> D[规则管理]
NAV --> E[导入任务]
NAV --> F[数据审计]
NAV --> G[系统设置]
A --> A1[文件上传页]
A --> A2[批次列表页]
A --> A3[批次详情页]
B --> B1[清洗结果预览页]
C --> C1[重复记录处理页]
D --> D1[规则列表页]
D --> D2[规则编辑页]
E --> E1[导入任务列表页]
E --> E2[导入结果详情页]
F --> F1[审计追溯页]
F --> F2[交易处理链路页]
G --> G1[Firefly 连接配置]
G --> G2[去重参数配置]
12.2 页面职责与交互说明
12.2.1 导入中心 — 文件上传页
| 元素 | 说明 |
|---|---|
| 拖拽上传区域 | 支持拖拽或点击选择文件,支持批量选择 |
| 来源识别结果 | 自动检测文件来源平台,允许手动修改 |
| 批量文件列表 | 显示已选文件名、大小、检测到的平台 |
| 处理按钮 | "开始处理"触发 ETL Pipeline |
| 历史批次入口 | 快速跳转至批次列表 |
核心组件:FileUploader.vue
- 使用 Vuetify
v-file-input+ 自定义拖拽区域 - 文件选择后立即调用
POST /api/v1/import/batches - 上传进度条实时展示
12.2.2 清洗结果预览页
| 元素 | 说明 |
|---|---|
| 交易列表表格 | 分页展示标准化后的交易记录 |
| 字段对比面板 | 点击任一行展开:原始字段 vs 标准字段对比 |
| 规则命中说明 | 显示该条交易命中的具体规则和分类依据 |
| 状态标记 | 以颜色标签区分:待清洗/已清洗/重复/待确认 |
| 筛选器 | 支持按来源平台、分类、方向、状态过滤 |
核心组件:TransactionTable.vue
- 使用 Vuetify
v-data-table实现分页排序 - 行展开(
expanded)显示详情面板 - 支持批量选择操作
12.2.3 去重处理页
| 元素 | 说明 |
|---|---|
| 疑似重复列表 | 成对展示疑似重复的交易记录 |
| 评分展示 | 展示模糊去重的总分和各因子得分 |
| 对比视图 | 左右并排对比两条记录的关键字段 |
| 操作按钮 | 确认合并 / 拒绝合并 / 暂时跳过 |
| 链路视图 | 展示已识别的转账闭环和订单链路 |
核心组件:DedupCompare.vue
- 左右分栏对比布局
- 差异字段高亮显示
- 评分因子可展开查看
12.2.4 规则配置页
| 元素 | 说明 |
|---|---|
| 规则列表 | 按类型分组展示,支持拖拽排序优先级 |
| 规则编辑器 | 可视化编辑条件(关键词/正则/金额范围等) |
| 动作配置 | 选择映射目标(分类/账户/标签) |
| 测试预览 | 输入测试数据,预览规则命中结果 |
| 启用/禁用 | 一键切换规则状态 |
核心组件:RuleEditor.vue
- 条件构建器:支持多条件 AND/OR 组合
- 使用 Vuetify
v-select、v-text-field、v-chip构建 - 测试按钮触发
POST /api/v1/rules/:id/test
12.2.5 导入结果页
| 元素 | 说明 |
|---|---|
| 统计概览 | 成功/失败/总计数量,饼图展示 |
| 失败记录列表 | 展示失败原因分类 |
| 重试操作 | 支持单条重试和批量重试 |
| 导出按钮 | 下载 CSV 中间文件 |
12.2.6 审计追溯页
| 元素 | 说明 |
|---|---|
| 交易搜索 | 按交易 ID / 订单号 / 时间范围搜索 |
| 处理链路时间线 | 纵向时间线展示完整处理链路 |
| 各阶段快照 | 原始文件 → 原始记录 → 标准化 → 规则命中 → 导入结果 |
| 操作日志 | 展示人工干预操作记录 |
核心组件:AuditTimeline.vue
- 使用 Vuetify
v-timeline组件 - 每个节点可展开查看详细快照数据
12.3 前端路由设计
// router/index.ts
const routes = [
{ path: '/', redirect: '/import' },
{
path: '/import',
name: 'ImportCenter',
component: () => import('@/views/ImportCenterView.vue'),
meta: { title: '导入中心', icon: 'mdi-upload' }
},
{
path: '/import/batch/:batchId',
name: 'BatchDetail',
component: () => import('@/views/BatchDetailView.vue'),
meta: { title: '批次详情' }
},
{
path: '/preview/:batchId',
name: 'Preview',
component: () => import('@/views/PreviewView.vue'),
meta: { title: '清洗预览' }
},
{
path: '/dedup',
name: 'DedupReview',
component: () => import('@/views/DedupReviewView.vue'),
meta: { title: '去重处理', icon: 'mdi-content-duplicate' }
},
{
path: '/rules',
name: 'RuleConfig',
component: () => import('@/views/RuleConfigView.vue'),
meta: { title: '规则管理', icon: 'mdi-cog-outline' }
},
{
path: '/tasks',
name: 'ImportTask',
component: () => import('@/views/ImportTaskView.vue'),
meta: { title: '导入任务', icon: 'mdi-export' }
},
{
path: '/audit',
name: 'AuditTrace',
component: () => import('@/views/AuditTraceView.vue'),
meta: { title: '数据审计', icon: 'mdi-history' }
},
{
path: '/settings',
name: 'Settings',
component: () => import('@/views/SettingsView.vue'),
meta: { title: '系统设置', icon: 'mdi-tune' }
}
]
13. 非功能设计
13.1 性能设计
目标:单次导入 1 万条记录在主流程内完成解析与清洗,去重计算应在 30 秒内完成。
| 优化策略 | 说明 |
|---|---|
| 批量插入 | raw_records 和 transactions 使用 GORM CreateInBatches,每批 500 条 |
| 关键索引 | source_platform + source_record_id、batch_id、trade_time、order_id |
| 模糊去重分桶 | 按 trade_time 时间分桶,避免全表扫描 |
| 规则预筛选 | 按平台和启用状态预加载规则,减少无效匹配 |
| 异步处理 | ETL Pipeline 使用 goroutine 异步执行,前端轮询状态 |
| 连接池 | SQLite 使用 WAL 模式提升并发读写性能 |
SQLite 性能配置
func initDB(dbPath string) *gorm.DB {
db, _ := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
sqlDB, _ := db.DB()
sqlDB.SetMaxOpenConns(1) // SQLite 单写
sqlDB.SetMaxIdleConns(10)
// 启用 WAL 模式
db.Exec("PRAGMA journal_mode=WAL")
db.Exec("PRAGMA synchronous=NORMAL")
db.Exec("PRAGMA cache_size=-64000") // 64MB cache
return db
}
13.2 安全设计
| 安全措施 | 说明 |
|---|---|
| 本地部署 | 默认本地运行,敏感账单数据不上传云端 |
| API Token 加密 | Firefly III API Token 使用 AES 加密存储 |
| 审计日志脱敏 | 日志中账号、订单号局部遮罩(如 138****1234) |
| 文件安全 | 上传文件限制类型和大小(默认最大 50MB) |
| CORS 配置 | 仅允许本地来源访问 API |
13.3 可维护性设计
| 设计原则 | 说明 |
|---|---|
| 解析器插件化 | 新增平台只需实现 BillParser 接口并注册 |
| 规则条件 JSON 化 | 规则存储为 JSON,灵活扩展匹配条件 |
| 导入器解耦 | Export 层独立,可替换下游目标(不限于 Firefly III) |
| 分层 DTO/VO/Entity | Handler → DTO → Service → Entity → DAO,职责清晰 |
| 事务分阶段 | 每个 ETL 阶段独立事务,避免超长事务 |
13.4 可追溯性设计
| 追溯能力 | 实现方式 |
|---|---|
| 任一导入结果 → 原始文件 | 通过 transaction.raw_record_id → raw_record.source_file_id → source_file |
| 任一规则命中 → 解释说明 | 通过 rule_hits 表记录命中条件和前后字段值对比 |
| 任一合并操作 → 判定依据 | 通过 dedup_relations.reason_json 和 link_relations.reason_json |
| 任一操作 → 操作日志 | 通过 audit_logs 表记录实体变更和操作者信息 |
13.5 部署架构
graph TD
subgraph "Docker 容器 / 本地部署"
FE["前端静态资源<br/>(Vue3 Build → /web)"]
BE["后端服务<br/>(Go Binary :8080)"]
DB["SQLite 数据库<br/>(/data/projectmoneyx.db)"]
end
subgraph "外部依赖"
FF["Firefly III<br/>(可选, API 推送)"]
DI["Data Importer<br/>(可选, API 推送)"]
end
FE -->|"嵌入式静态服务"| BE
BE --> DB
BE -->|"HTTP API"| FF
BE -->|"HTTP API"| DI
部署方式:
- Docker 部署(推荐):单容器包含前后端 + SQLite
- 二进制部署:交叉编译为单体可执行文件,前端资源使用
embed嵌入
# Dockerfile 示例
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY . .
RUN go build -o projectmoneyx ./cmd/server
FROM alpine:latest
WORKDIR /app
COPY --from=builder /app/projectmoneyx .
COPY --from=builder /app/web ./web
VOLUME /data
EXPOSE 8080
CMD ["./projectmoneyx", "--db", "/data/projectmoneyx.db"]
14. 关键实现建议(Go + GIN + GORM)
14.1 分层编码规范
| 层级 | 职责 | 规范 |
|---|---|---|
handler |
仅做参数绑定、校验、返回 | 不包含业务逻辑,调用 service 方法 |
service |
编排业务流程 | 不直接操作数据库,调用 repository |
domain/entity |
领域实体定义 | 包含业务方法和校验逻辑 |
domain/repository |
仓储接口定义 | 仅定义接口,不含实现 |
dao |
GORM 数据访问实现 | 实现 repository 接口 |
parser / matcher / rule / exporter |
独立可测试组件 | 纯函数风格,易于单元测试 |
14.2 推荐核心 Service 对象
// 核心 Service 清单
type ImportBatchService struct { ... } // 批次管理
type PipelineService struct { ... } // ETL 流水线编排
type ParserRegistry struct { ... } // 解析器注册中心
type TransactionNormalizeService struct { ... } // 标准化服务
type DedupMatchService struct { ... } // 去重匹配服务
type TransferLinkService struct { ... } // 转账链路合并服务
type RuleApplyService struct { ... } // 规则应用服务
type FireflyExportService struct { ... } // Firefly 导出服务
type AuditTraceService struct { ... } // 审计追溯服务
14.3 事务边界设计
建议以下阶段分别事务化,避免超长事务导致 SQLite 锁定:
flowchart LR
T1["事务 1<br/>文件入库 + 原始记录入库"] --> T2["事务 2<br/>标准化结果落库"]
T2 --> T3["事务 3<br/>去重/链路关系落库"]
T3 --> T4["事务 4<br/>规则命中落库"]
T4 --> T5["事务 5<br/>导入结果落库"]
// 事务示例
func (s *PipelineService) processNormalize(ctx context.Context, batchID string) error {
return s.db.Transaction(func(tx *gorm.DB) error {
// 在事务内完成标准化 + 批量入库
transactions := s.normalizer.NormalizeAll(ctx, rawRecords)
return tx.CreateInBatches(transactions, 500).Error
})
}
14.4 错误处理策略
// 统一错误码定义
const (
ErrCodeSuccess = 0
ErrCodeBadRequest = 40000
ErrCodeFileParseError = 40001
ErrCodeUnknownPlatform = 40002
ErrCodeDuplicateFile = 40003
ErrCodeRuleInvalid = 40004
ErrCodeExportFailed = 40005
ErrCodeInternal = 50000
)
// 全局错误处理中间件
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
err := c.Errors.Last()
// 根据错误类型返回对应错误码和消息
c.JSON(http.StatusOK, Response{
Code: mapErrorCode(err),
Message: err.Error(),
})
}
}
}
14.5 配置管理
// config/config.go
type Config struct {
Server ServerConfig `yaml:"server"`
Database DatabaseConfig `yaml:"database"`
Firefly FireflyConfig `yaml:"firefly"`
Dedup DedupConfig `yaml:"dedup"`
}
type ServerConfig struct {
Port int `yaml:"port" default:"8080"`
Mode string `yaml:"mode" default:"release"` // debug/release
}
type DatabaseConfig struct {
Path string `yaml:"path" default:"./data/projectmoneyx.db"`
}
type FireflyConfig struct {
BaseURL string `yaml:"base_url"`
APIToken string `yaml:"api_token"` // AES 加密存储
ImporterURL string `yaml:"importer_url"`
Enabled bool `yaml:"enabled"`
}
type DedupConfig struct {
FuzzyTimeWindow int `yaml:"fuzzy_time_window" default:"5"` // 分钟
FuzzyThresholdHigh int `yaml:"fuzzy_threshold_high" default:"85"` // 自动判定
FuzzyThresholdLow int `yaml:"fuzzy_threshold_low" default:"60"` // 疑似判定
TransferTimeWindow int `yaml:"transfer_time_window" default:"30"` // 分钟
AmountEpsilon float64 `yaml:"amount_epsilon" default:"0.01"` // 金额容差
}
15. 版本演进规划
15.1 V1.0(MVP — 优先上线)
gantt
title V1.0 MVP 开发计划
dateFormat YYYY-MM-DD
section 基础架构
项目骨架搭建 :a1, 2026-03-10, 3d
数据库表结构建模 :a2, after a1, 2d
GIN 路由与中间件 :a3, after a2, 2d
section 解析引擎
支付宝 Parser :b1, after a3, 3d
微信 Parser :b2, after b1, 3d
解析器注册中心 :b3, after a3, 1d
section 核心流程
标准化引擎 :c1, after b2, 3d
严格去重 :c2, after c1, 2d
转账闭环合并 :c3, after c2, 3d
基础规则映射 :c4, after c3, 3d
section 导入导出
CSV 导出 :d1, after c4, 2d
Firefly API 推送 :d2, after d1, 3d
导入预览与确认 :d3, after d2, 2d
section 前端
导入中心页面 :e1, after a3, 5d
清洗预览页面 :e2, after e1, 4d
规则配置页面 :e3, after e2, 4d
导入结果页面 :e4, after e3, 3d
核心交付物:
- 支付宝 / 微信 Parser
- 严格去重 + 转账闭环
- 基础规则映射(分类 / 账户)
- 导入预览与确认
- CSV 导出 + Firefly API 推送
- 基础前端(导入 → 预览 → 确认 → 结果)
15.2 V1.5(增强版)
| 功能 | 说明 |
|---|---|
| 建行 / 工行 Parser | 扩展银行账单支持 |
| 模糊去重评分 | 多因子评分模型上线 |
| 人工确认流程 | 疑似重复的人工确认队列 |
| 商户别名归一化 | 商户别名库与归一化规则 |
| 批次管理增强 | 批次列表、详情、删除 |
| 手动合并/拆分 | 支持用户手动操作交易链路 |
| 标签映射 | 规则引擎支持标签维度 |
15.3 V2.0(进阶版)
| 功能 | 说明 |
|---|---|
| 京东 / 美团 Parser | 电商与生活服务平台支持 |
| 订单链路增强 | 京东订单 + 微信支付链路聚合 |
| 多币种支持 | 加密货币交易所流水适配 |
| 可视化规则调试 | 规则测试与命中预览 |
| 简易统计报表 | 导入概览、分类占比、来源分布 |
| 对账可视化 | 孤儿记录展示 + 手动干预 |
| 多账本支持 | 对接多个 Firefly III 实例 |
16. 结论
ProjectMoneyX 不是一个"简单导入工具",而是一个面向 Firefly III 的本地账单数据治理中台。
核心价值
- 汇聚多源账单 — 一站式接入支付平台、银行、生活服务、交易所
- 统一交易语义 — 屏蔽平台差异,建立标准化交易模型
- 智能去重合并 — 消除重复,还原真实交易链路与转账闭环
- 本地规则沉淀 — 分类/账户/标签映射可长期积累、可解释、可迁移
- 无缝对接导入 — 丝滑推送至 Firefly III / Data Importer
核心架构决策
| 决策 | 理由 |
|---|---|
| 插件化 Adapter 架构 | 平台差异收敛在 Adapter 层,格式变更只需更新对应 Parser |
| 支付宝分类字典作为统一基准 | 支付宝 22 种分类最丰富,其他平台映射到此体系 |
| 微信"交易类型 + 商品"联合推断 | 微信交易类型粗粒度,需结合商品字段推断实际消费分类 |
| 规则映射以本地为主 | 规则可沉淀、可解释、可迁移,不绑定在 Data Importer 实例 |
| 三层递进去重策略 | 严格 → 模糊 → 链路合并,兼顾准确性与覆盖率 |
| SQLite 作为核心数据底座 | 不仅是缓存,更是清洗结果与审计链路的持久化存储 |
| ETL 分阶段事务 | 避免超长事务,每个阶段独立可重试 |
技术栈总结
┌─────────────────────────────────────────────────────┐
│ 前端: Vue3 + TypeScript + Vuetify (Material Design) │
├─────────────────────────────────────────────────────┤
│ 后端: Golang + GIN (RESTful API) + GORM (ORM) │
├─────────────────────────────────────────────────────┤
│ 数据库: SQLite (WAL 模式, 本地优先) │
├─────────────────────────────────────────────────────┤
│ 部署: Docker 容器化 / 跨平台二进制单体 │
└─────────────────────────────────────────────────────┘