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

69 KiB
Raw Blame History

ProjectMoneyX — 系统详细设计说明书DDS

版本v1.0 | 日期2026-03-03 | 状态:综合定稿 技术栈:后端 Golang + GIN + GORM | 前端 Vue3 + TypeScript + Vuetify | 数据库 SQLite


1. 设计目标与范围

1.1 设计目标

ProjectMoneyX 定位为 Firefly III 生态的本地化多源账单数据治理中间件,核心目标是将分散在支付平台、银行、生活服务及加密货币交易所的零散账单,经过标准化清洗、智能去重与链路合并后,无缝推送至 Firefly III / Data Importer。

本系统需实现以下完整处理链路:

  1. 原始文件接入与批次化管理
  2. 多平台账单解析与字段标准化(插件化架构)
  3. 统一交易模型入库SQLite 持久化)
  4. 严格去重 → 模糊去重 → 链路合并(三层递进)
  5. 分类 / 账户 / 标签 / 商户归一化规则映射
  6. 导出为 Firefly III / Data Importer 可消费的数据格式API / CSV
  7. 提供预览、审计、失败重试与全链路可回溯能力

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_payloadJSON确保审计可追溯
指纹 对关键字段做 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 项全部满足)

  1. 金额一致
  2. 一条为支出expense一条为收入income
  3. 时间在可配置窗口内(默认 ±30 分钟)
  4. 来源平台不同但账户映射可闭环
  5. 非退款、非手续费类型
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 导出模式

模式 AAPI 推送模式(优先)

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-selectv-text-fieldv-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_recordstransactions 使用 GORM CreateInBatches,每批 500 条
关键索引 source_platform + source_record_idbatch_idtrade_timeorder_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_jsonlink_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

部署方式

  1. Docker 部署(推荐):单容器包含前后端 + SQLite
  2. 二进制部署:交叉编译为单体可执行文件,前端资源使用 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.0MVP — 优先上线)

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 的本地账单数据治理中台

核心价值

  1. 汇聚多源账单 — 一站式接入支付平台、银行、生活服务、交易所
  2. 统一交易语义 — 屏蔽平台差异,建立标准化交易模型
  3. 智能去重合并 — 消除重复,还原真实交易链路与转账闭环
  4. 本地规则沉淀 — 分类/账户/标签映射可长期积累、可解释、可迁移
  5. 无缝对接导入 — 丝滑推送至 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 容器化 / 跨平台二进制单体                 │
└─────────────────────────────────────────────────────┘