大量更新
This commit is contained in:
267
.agents/skills/developing-projectmoneyx/SKILL.md
Normal file
267
.agents/skills/developing-projectmoneyx/SKILL.md
Normal file
@@ -0,0 +1,267 @@
|
||||
---
|
||||
name: developing-projectmoneyx
|
||||
description: >
|
||||
指导 ProjectMoneyX 多源账单数据治理系统的全栈开发(Guides full-stack development of ProjectMoneyX bill data governance system)。
|
||||
包含:ETL Pipeline 编排(Parse → Normalize → Dedup → Link → Rule → Export)、插件化解析器对接、三层去重策略、规则引擎映射、Firefly III 适配、SQLite 数据模型、审计追溯。
|
||||
触发场景 Trigger: 开发/修改 ProjectMoneyX 的 Parser / Pipeline / 去重 / 规则 / 导入导出 / 审计 / 前端页面 / API 接口。
|
||||
关键词 Keywords: ProjectMoneyX, 账单, bill, ETL, parser, dedup, 去重, 链路合并, transfer link, rule engine, 规则引擎, Firefly III, 导入, import, export, audit, 审计, SQLite, GORM, GIN, Vue3, Vuetify。
|
||||
argument-hint: "<action> <target>" 例如/ e.g.:
|
||||
"add parser for ccb", "implement dedup scorer", "create rule handler",
|
||||
"update transaction schema", "build import preview page"
|
||||
allowed-tools:
|
||||
- Read
|
||||
- Write
|
||||
- Edit
|
||||
- Glob
|
||||
- Grep
|
||||
- Bash
|
||||
---
|
||||
|
||||
# Developing ProjectMoneyX
|
||||
|
||||
ProjectMoneyX 是 Firefly III 生态的**本地化多源账单数据治理中间件**,技术栈为 Go (GIN + GORM) + Vue3 (TypeScript + Vuetify) + SQLite。系统核心是一条 ETL Pipeline:`Parse → Normalize → Dedup → Link → Rule → Export`,将支付宝/微信/银行账单标准化后推送至 Firefly III。
|
||||
|
||||
> **架构关键词**:DDD 分层 · 插件化 Adapter · 三层去重 · 规则可解释 · 全链路审计
|
||||
|
||||
## Quick Context
|
||||
|
||||
```bash
|
||||
# 动态注入:后端项目结构
|
||||
!`find projectmoneyx-server/internal -type f -name "*.go" | head -40`
|
||||
|
||||
# 动态注入:前端项目结构
|
||||
!`find projectmoneyx-web/src -type f -name "*.ts" -o -name "*.vue" | head -30`
|
||||
|
||||
# 动态注入:数据库表定义
|
||||
!`grep -rn "TableName\|func.*TableName" projectmoneyx-server/internal/ | head -20`
|
||||
|
||||
# 动态注入:API 路由注册
|
||||
!`grep -rn "Group\|GET\|POST\|PUT\|DELETE" projectmoneyx-server/internal/handler/ | head -30`
|
||||
```
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ 展现层: Vue3 + TypeScript + Vuetify │
|
||||
│ 导入中心 / 清洗预览 / 去重处理 / 规则管理 / 导入任务 / 审计 │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 接入层: GIN RESTful API (/api/v1/*) │
|
||||
│ import / transactions / dedup / rules / export / audit │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 应用服务层: Pipeline 编排 │
|
||||
│ ImportBatchService → PipelineService │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 业务逻辑层 (ETL Core Domain) │
|
||||
│ Parser(插件) → Normalize → Match → Link → Rule → Export │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 数据持久层: GORM + SQLite (WAL) │
|
||||
│ 11 张核心表,分阶段事务 │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**分层依赖规则**:handler → service → domain(entity/repository)← dao。Parser/Matcher/Linker/Rule/Exporter 为独立可测试组件。
|
||||
|
||||
## Module Registry
|
||||
|
||||
| 模块 | 包路径 | 职责 | 优先级 |
|
||||
|------|--------|------|--------|
|
||||
| 导入中心 | `handler/import` + `service/import_batch` | 文件上传、批次管理 | P0 |
|
||||
| 解析引擎 | `parser/` | 插件化平台解析器 | P0 |
|
||||
| 标准化引擎 | `normalize/` | 异构字段 → 统一 Transaction 模型 | P0 |
|
||||
| 去重引擎 | `matcher/` | 严格去重 + 模糊去重(P1) | P0/P1 |
|
||||
| 链路引擎 | `linker/` | 转账闭环 + 订单链路合并 | P0 |
|
||||
| 规则引擎 | `rule/` | 6 类规则按序执行 | P0/P1 |
|
||||
| 导出引擎 | `exporter/` | Firefly API/CSV 导出 | P0 |
|
||||
| 审计中心 | `service/audit` | 全链路追溯 | P0 |
|
||||
| 系统设置 | `handler/settings` + `config/` | Firefly 连接、阈值参数 | P1 |
|
||||
|
||||
## Plan
|
||||
|
||||
### 产物清单
|
||||
|
||||
| 动作 | 产物 |
|
||||
|------|------|
|
||||
| `add parser` | `parser/<platform>/<platform>_parser.go` — 实现 `BillParser` 接口 |
|
||||
| `create handler` | `handler/<resource>_handler.go` — GIN Handler |
|
||||
| `create service` | `service/<resource>_service.go` — 应用服务 |
|
||||
| `create dao` | `dao/<resource>_dao.go` — GORM 数据访问 |
|
||||
| `create entity` | `domain/entity/<resource>.go` — 领域实体 |
|
||||
| `add rule type` | `rule/<type>_mapper.go` — 规则映射器 |
|
||||
| `scaffold module` | 上述全部 + DTO + repository 接口 |
|
||||
|
||||
### 决策点
|
||||
|
||||
1. **Parser 选择**:先检查 `parser/registry.go` 中已注册的解析器,确认目标平台是否已有实现
|
||||
2. **去重层级**:严格去重(P0) vs 模糊去重(P1) — 新功能默认只实现严格去重
|
||||
3. **规则执行顺序**:必须遵守 6 步固定顺序(`reference/04-rule-engine/rule-execution.md`)
|
||||
4. **事务边界**:ETL 每阶段独立事务,禁止跨阶段长事务
|
||||
5. **SQLite 约束**:单写连接 `MaxOpenConns=1`,启用 WAL 模式
|
||||
|
||||
---
|
||||
|
||||
## Execute
|
||||
|
||||
### 1. 新增平台解析器
|
||||
|
||||
```go
|
||||
// 1. 实现 BillParser 接口 (parser/<platform>/<platform>_parser.go)
|
||||
type Parser struct{}
|
||||
|
||||
func (p *Parser) Platform() string { return "<platform>" }
|
||||
|
||||
func (p *Parser) Detect(meta FileMeta, header []string) bool {
|
||||
// 基于文件名/表头特征判定
|
||||
}
|
||||
|
||||
func (p *Parser) Parse(ctx context.Context, reader io.Reader) ([]RawBillRecord, error) {
|
||||
// 逐行读取 → 填充 RawBillRecord.RawFields
|
||||
// 必须设置 SourcePlatform, SourceRecordID, RowNo, RowFingerprint
|
||||
}
|
||||
|
||||
// 2. 注册到 Registry (parser/registry.go)
|
||||
r.Register(&<platform>.Parser{})
|
||||
```
|
||||
|
||||
**字段映射要求**(参考 `reference/02-parser-engine/field-mappings.md`):
|
||||
- `trade_time`:统一 UTC+8,`time.Time`
|
||||
- `amount`:去除货币符号,正数 `decimal(18,6)`
|
||||
- `direction`:`income` / `expense` / `transfer` / `refund` / `fee` / `other`
|
||||
- `category_raw`:保留原始分类,不在 Parser 中做映射
|
||||
- `order_id`:去除空格,作为唯一标识
|
||||
|
||||
### 2. ETL Pipeline 阶段开发
|
||||
|
||||
每个阶段必须:
|
||||
1. 接收 `context.Context` + 数据切片
|
||||
2. 返回处理后切片 + error
|
||||
3. 在独立事务中持久化(`db.Transaction`,每批 500 条 `CreateInBatches`)
|
||||
4. 更新批次状态
|
||||
|
||||
```go
|
||||
// 阶段签名模式
|
||||
func (s *StageService) Execute(ctx context.Context, txns []*Transaction) ([]*Transaction, error)
|
||||
```
|
||||
|
||||
### 3. 规则引擎扩展
|
||||
|
||||
新增规则类型时:
|
||||
1. 在 `rule/engine.go` 的 `executionOrder` 中确认位置
|
||||
2. 实现 `MatchConditions(txn)` 和 `ApplyActions(txn)` 方法
|
||||
3. 确保 `RuleHit` 记录命中日志(含 `BeforeValue` / `AfterValue`)
|
||||
4. 规则条件 JSON 存储,参考 `reference/04-rule-engine/rule-conditions.md`
|
||||
|
||||
### 4. API 开发
|
||||
|
||||
遵循统一响应格式:
|
||||
```go
|
||||
type Response struct {
|
||||
Code int `json:"code"` // 0=成功
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
```
|
||||
|
||||
路由分组:`/api/v1/import/*`、`/api/v1/transactions/*`、`/api/v1/dedup/*`、`/api/v1/rules/*`、`/api/v1/export/*`、`/api/v1/audit/*`、`/api/v1/settings/*`
|
||||
|
||||
### 5. 前端页面开发
|
||||
|
||||
7 个核心页面,全部使用 Vue3 + Composition API + TypeScript:
|
||||
- 导入中心:`FileUploader.vue` + 拖拽上传 + 进度条
|
||||
- 清洗预览:`TransactionTable.vue` + `v-data-table` + 行展开对比
|
||||
- 去重处理:`DedupCompare.vue` + 左右分栏 + 评分因子展开
|
||||
- 规则管理:`RuleEditor.vue` + 条件构建器 + 测试预览
|
||||
- 导入任务:统计概览 + 失败列表 + 单条/批量重试
|
||||
- 审计追溯:`AuditTimeline.vue` + `v-timeline` + 快照展开
|
||||
- 系统设置:Firefly 连接配置 + 测试连接 + 去重参数配置
|
||||
|
||||
---
|
||||
|
||||
## Verify
|
||||
|
||||
### 架构层级检查
|
||||
- [ ] handler 层不包含业务逻辑,仅做参数绑定 + 调用 service + 返回响应
|
||||
- [ ] service 层不直接操作 `*gorm.DB`,通过 repository 接口访问数据
|
||||
- [ ] domain/entity 不依赖 handler/service
|
||||
- [ ] 无循环依赖(handler → service → domain ← dao)
|
||||
|
||||
### Parser 检查
|
||||
- [ ] 新增 Parser 实现了 `BillParser` 接口的全部 3 个方法(`Platform()`, `Detect()`, `Parse()`)
|
||||
- [ ] 已注册到 `parser/registry.go`(`reference/02-parser-engine/parser-interface.md`)
|
||||
- [ ] 字段映射覆盖了所有原始字段(对照 `reference/02-parser-engine/field-mappings.md`)
|
||||
- [ ] `amount` 为正数,`direction` 独立表达收支方向
|
||||
- [ ] `RowFingerprint` 使用 SHA256 生成(`reference/03-dedup-engine/fingerprint.md`)
|
||||
|
||||
### 去重与链路检查
|
||||
- [ ] 严格去重判定键按优先级 3 级执行(`reference/03-dedup-engine/strict-dedup.md`)
|
||||
- [ ] 模糊去重评分因子 6 项,阈值可配置(`reference/03-dedup-engine/fuzzy-dedup.md`)
|
||||
- [ ] 转账闭环 5 条件全部满足才匹配(`reference/03-dedup-engine/transfer-link.md`)
|
||||
- [ ] 疑似重复(60-84 分)进入 `PENDING_REVIEW` 人工确认队列
|
||||
|
||||
### 规则引擎检查
|
||||
- [ ] 6 类规则按固定顺序执行:对手方归一 → 商户归一 → 分类 → 账户 → 标签 → Firefly(`reference/04-rule-engine/rule-execution.md`)
|
||||
- [ ] 同类型内按 `priority` 升序执行,首条命中即停止
|
||||
- [ ] 每条命中记录 `RuleHit`,含 `BeforeValue` / `AfterValue`
|
||||
- [ ] 规则条件 JSON 结构正确(`reference/04-rule-engine/rule-conditions.md`)
|
||||
|
||||
### 数据库检查
|
||||
- [ ] 表结构 11 张表齐全(`reference/05-database/db-schema.md`)
|
||||
- [ ] 关键索引已创建(`reference/05-database/indexes.md`)
|
||||
- [ ] SQLite 配置:`MaxOpenConns=1`, WAL 模式, `cache_size=-64000`
|
||||
- [ ] ETL 每阶段独立事务,`CreateInBatches` 每批 500 条
|
||||
|
||||
### API 检查
|
||||
- [ ] 路由路径遵循 `reference/06-api-design/api-catalog.md`
|
||||
- [ ] 统一 `Response` / `PageResponse` 结构
|
||||
- [ ] 导入前 6 项校验完整(`reference/07-export-engine/import-validation.md`)
|
||||
|
||||
### 前端检查
|
||||
- [ ] 所有页面使用 `<script setup lang="ts">` + Composition API
|
||||
- [ ] 数据表格使用 `v-data-table` + `fixed-header`
|
||||
- [ ] 处理三种 UI 状态:加载中(skeleton)、空数据(empty-state)、错误(snackbar + 重试)
|
||||
- [ ] 路由定义匹配 `reference/08-frontend/routes.md`
|
||||
|
||||
---
|
||||
|
||||
## Pitfalls
|
||||
|
||||
1. **支付宝分类是全局基准字典**:系统使用支付宝 22 种分类作为统一标准。微信/银行等平台必须映射到此分类枚举,不要在 Parser 中创造新的分类值体系(参考 `reference/02-parser-engine/field-mappings.md` 中的分类枚举表)
|
||||
|
||||
2. **微信分类需要"交易类型 + 商品"联合推断**:微信的"交易类型"是支付动作(商户消费/转账/红包),不是消费语义。必须结合"商品"字段做关键词推断,未命中的归入"其他"并记录。不要直接把微信交易类型当作分类使用(参考 `reference/02-parser-engine/field-mappings.md` 中的微信推断规则流程图)
|
||||
|
||||
3. **规则执行顺序不可变**:6 类规则的执行顺序是固定的(对手方归一 → 商户归一 → 分类 → 账户 → 标签 → Firefly),先归一再分类可提升命中率。修改顺序会导致规则相互依赖断裂(参考 `reference/04-rule-engine/rule-execution.md`)
|
||||
|
||||
4. **SQLite 单写连接**:`MaxOpenConns` 必须设为 1,SQLite 不支持多写。ETL Pipeline 已通过分阶段事务避免超长锁定。如果遇到 `database is locked` 错误,检查是否有未关闭的事务(参考 `reference/05-database/db-schema.md` 中的 SQLite 配置)
|
||||
|
||||
5. **转账闭环需 5 条件全部满足**:金额一致 + 方向互补 + 时间窗口内 + 不同平台 + 非退款/手续费。漏掉任何一项都会导致误合并。特别注意退款交易(direction=refund)不应参与转账闭环(参考 `reference/03-dedup-engine/transfer-link.md`)
|
||||
|
||||
6. **行指纹是分钟粒度**:`GenerateRowFingerprint` 使用 `2006-01-02 15:04` 格式(分钟级),不是秒级。这是为了容忍不同平台对同一交易记录的秒级时间差异(参考 `reference/03-dedup-engine/fingerprint.md`)
|
||||
|
||||
7. **批次状态机严格受控**:批次状态只能按 `CREATED → UPLOADED → PARSING → ... → IMPORT_SUCCESS` 的线性路径推进,不可跳跃。失败时只能回退至上一可重试状态(参考 `reference/01-architecture/batch-state-machine.md`)
|
||||
|
||||
---
|
||||
|
||||
## Related References
|
||||
|
||||
| 需要了解... | 查阅... |
|
||||
|------------|--------|
|
||||
| 系统架构与模块依赖 | `reference/01-architecture/system-overview.md` |
|
||||
| 批次状态机定义 | `reference/01-architecture/batch-state-machine.md` |
|
||||
| Parser 接口与注册 | `reference/02-parser-engine/parser-interface.md` |
|
||||
| 平台字段映射规则 | `reference/02-parser-engine/field-mappings.md` |
|
||||
| 严格去重判定键 | `reference/03-dedup-engine/strict-dedup.md` |
|
||||
| 模糊去重评分模型 | `reference/03-dedup-engine/fuzzy-dedup.md` |
|
||||
| 转账闭环识别规则 | `reference/03-dedup-engine/transfer-link.md` |
|
||||
| 行指纹生成算法 | `reference/03-dedup-engine/fingerprint.md` |
|
||||
| 规则条件 JSON 格式 | `reference/04-rule-engine/rule-conditions.md` |
|
||||
| 规则执行顺序与可解释性 | `reference/04-rule-engine/rule-execution.md` |
|
||||
| 数据库 11 张表结构 | `reference/05-database/db-schema.md` |
|
||||
| 关键索引设计 | `reference/05-database/indexes.md` |
|
||||
| API 接口目录 | `reference/06-api-design/api-catalog.md` |
|
||||
| 统一响应与错误码 | `reference/06-api-design/response-format.md` |
|
||||
| Firefly 导出适配 | `reference/07-export-engine/firefly-mapping.md` |
|
||||
| 导入前校验清单 | `reference/07-export-engine/import-validation.md` |
|
||||
| 前端路由与页面 | `reference/08-frontend/routes.md` |
|
||||
| 前端组件交互 | `reference/08-frontend/components.md` |
|
||||
| 非功能设计要求 | `reference/09-nonfunctional/performance.md` |
|
||||
| 部署与安全 | `reference/09-nonfunctional/deployment.md` |
|
||||
@@ -0,0 +1,57 @@
|
||||
## 批次状态机
|
||||
|
||||
- **DDS-Section**: 5.2 批次状态机
|
||||
- **DDS-Lines**: L362-L387
|
||||
|
||||
### Extract
|
||||
|
||||
#### 状态枚举
|
||||
|
||||
| 状态 | 说明 | 备注 |
|
||||
|------|------|------|
|
||||
| `CREATED` | 批次已创建 | 初始状态 |
|
||||
| `UPLOADED` | 文件上传完成 | |
|
||||
| `PARSING` | 正在解析 | |
|
||||
| `PARSED` | 解析完成 | |
|
||||
| `NORMALIZING` | 正在标准化 | |
|
||||
| `NORMALIZED` | 标准化完成 | |
|
||||
| `MATCHING` | 正在去重/链路合并 | |
|
||||
| `MATCHED` | 去重/链路完成 | |
|
||||
| `RULE_APPLYING` | 正在应用规则 | |
|
||||
| `PREVIEW_READY` | 规则映射完成,可预览 | 用户可在此阶段查看预览结果、人工确认去重 |
|
||||
| `IMPORTING` | 正在导入 | 用户确认后触发 |
|
||||
| `IMPORT_SUCCESS` | 全部导入成功 | 终态 |
|
||||
| `PARTIAL_FAILED` | 部分失败 | 可重试 |
|
||||
| `IMPORT_FAILED` | 全部失败 | 可重试 |
|
||||
| `RETRYING` | 重试中 | |
|
||||
|
||||
#### 状态转换规则
|
||||
|
||||
```
|
||||
[*] → CREATED: 创建批次
|
||||
CREATED → UPLOADED: 文件上传完成
|
||||
UPLOADED → PARSING: 触发解析
|
||||
PARSING → PARSED: 解析完成
|
||||
PARSING → UPLOADED: 解析失败(回退)
|
||||
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: 仍有失败
|
||||
```
|
||||
|
||||
#### 实现要点
|
||||
|
||||
- 解析失败时状态回退至 `UPLOADED`,记录错误信息
|
||||
- `PREVIEW_READY` 是用户交互节点,用户可查看预览结果和人工确认去重
|
||||
- 失败状态支持重试,不需要整批重做
|
||||
- 状态变更必须记录到 `audit_logs` 表
|
||||
@@ -0,0 +1,113 @@
|
||||
## 系统架构总览
|
||||
|
||||
- **DDS-Section**: 3. 总体架构设计
|
||||
- **DDS-Lines**: L77-L164
|
||||
|
||||
### Extract
|
||||
|
||||
#### 分层架构
|
||||
|
||||
| 层级 | 技术选型 | 核心职责 |
|
||||
|------|----------|----------|
|
||||
| **展现层** | 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 | 本地数据库,存储全量数据与审计链路 |
|
||||
|
||||
#### 数据流转拓扑
|
||||
|
||||
```
|
||||
多源账单导入 → 解析器(Parser) → 标准化入库 → 去重与链路合并 → 规则映射 → 导出/推送
|
||||
(文件上传) (Adapter层) (SQLite) (Dedup+Link) (Rule Engine) (API/CSV)
|
||||
```
|
||||
|
||||
#### 后端包结构
|
||||
|
||||
```
|
||||
projectmoneyx-server/
|
||||
├── cmd/server/main.go # 程序入口
|
||||
├── internal/
|
||||
│ ├── config/ # 配置管理
|
||||
│ ├── handler/ # GIN Handler(接入层)
|
||||
│ ├── middleware/ # 中间件
|
||||
│ ├── service/ # 应用服务层
|
||||
│ ├── domain/ # 领域层
|
||||
│ │ ├── entity/ # 领域实体
|
||||
│ │ ├── valueobject/ # 值对象
|
||||
│ │ └── repository/ # 仓储接口
|
||||
│ ├── parser/ # Adapter 解析层(插件化)
|
||||
│ ├── normalize/ # 标准化引擎
|
||||
│ ├── matcher/ # 去重引擎
|
||||
│ ├── linker/ # 链路合并引擎
|
||||
│ ├── rule/ # 规则引擎
|
||||
│ ├── exporter/ # 导出引擎
|
||||
│ ├── dao/ # 数据访问层(GORM 实现)
|
||||
│ └── dto/ # 数据传输对象
|
||||
├── migrations/ # 数据库迁移
|
||||
├── web/ # 前端打包产物
|
||||
└── go.mod
|
||||
```
|
||||
|
||||
#### 前端项目结构
|
||||
|
||||
```
|
||||
projectmoneyx-web/
|
||||
├── src/
|
||||
│ ├── api/ # API 调用封装
|
||||
│ ├── views/ # 页面视图 (8 个核心页面)
|
||||
│ ├── components/ # 可复用组件
|
||||
│ ├── stores/ # Pinia 状态管理
|
||||
│ ├── types/ # TypeScript 类型定义
|
||||
│ ├── router/ # Vue Router
|
||||
│ ├── plugins/ # Vuetify 等插件
|
||||
│ ├── App.vue
|
||||
│ └── main.ts
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
└── vite.config.ts
|
||||
```
|
||||
|
||||
#### 模块清单
|
||||
|
||||
| 模块 | 包名 | 职责 | 优先级 |
|
||||
|------|------|------|--------|
|
||||
| 导入中心 | `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 |
|
||||
|
||||
#### 核心 Service 清单
|
||||
|
||||
```go
|
||||
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 { ... } // 审计追溯服务
|
||||
```
|
||||
|
||||
#### 关键设计约束
|
||||
|
||||
| # | 约束 | 说明 |
|
||||
|---|------|------|
|
||||
| 1 | 本地优先 | 财务数据敏感,必须本地部署 |
|
||||
| 2 | 插件化解析器 | 平台格式变化频繁,适配逻辑隔离在 Adapter 层 |
|
||||
| 3 | 统一交易模型稳定 | 避免下游 Firefly 或上游平台格式污染核心域模型 |
|
||||
| 4 | 支付宝分类为标准 | 支付宝 22 种分类最丰富,其他平台映射到此体系 |
|
||||
| 5 | 微信分类需推断 | 微信"交易类型"粗粒度,结合"商品"字段推断分类 |
|
||||
@@ -0,0 +1,156 @@
|
||||
## 平台字段映射规则
|
||||
|
||||
- **DDS-Section**: 6.3 支付宝解析规则 + 6.4 微信解析规则 + 6.5 统一交易模型
|
||||
- **DDS-Lines**: L500-L668
|
||||
|
||||
### Extract
|
||||
|
||||
#### 支付宝原始字段
|
||||
|
||||
```
|
||||
交易时间 | 交易分类 | 交易对方 | 对方账号 | 商品说明 | 收/支 | 金额 | 收/付款方式 | 交易状态 | 交易订单号 | 商家订单号 | 备注
|
||||
```
|
||||
|
||||
#### 支付宝字段映射表
|
||||
|
||||
| 原字段 | 目标字段 | 映射说明 |
|
||||
|--------|----------|----------|
|
||||
| 交易时间 | `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 类)— 全局统一基准
|
||||
|
||||
| 编号 | 分类名称 | 编号 | 分类名称 |
|
||||
|------|----------|------|----------|
|
||||
| 1 | 餐饮美食 | 12 | 退款 |
|
||||
| 2 | 投资理财 | 13 | 教育培训 |
|
||||
| 3 | 日用百货 | 14 | 住房物业 |
|
||||
| 4 | 数码电器 | 15 | 酒店旅游 |
|
||||
| 5 | 交通出行 | 16 | 文化休闲 |
|
||||
| 6 | 充值缴费 | 17 | 运动户外 |
|
||||
| 7 | 信用借还 | 18 | 爱车养车 |
|
||||
| 8 | 转账红包 | 19 | 商业服务 |
|
||||
| 9 | 生活服务 | 20 | 母婴亲子 |
|
||||
| 10 | 家居家装 | 21 | 收入 |
|
||||
| 11 | 医疗健康 | 22 | 其他 |
|
||||
|
||||
> **关键设计决策**:支付宝拥有最丰富的 22 种交易分类,系统将其作为**全局统一分类基准字典**。所有其他平台的交易分类最终都应映射到此套分类枚举。
|
||||
|
||||
#### 微信原始字段
|
||||
|
||||
```
|
||||
交易时间 | 交易类型 | 交易对方 | 商品 | 收/支 | 金额(元) | 支付方式 | 当前状态 | 交易单号 | 商户单号 | 备注
|
||||
```
|
||||
|
||||
#### 微信字段映射表
|
||||
|
||||
| 原字段 | 目标字段 | 映射说明 |
|
||||
|--------|----------|----------|
|
||||
| 交易时间 | `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` | 补充备注 |
|
||||
|
||||
#### 微信分类推断规则
|
||||
|
||||
微信"交易类型"多为支付动作(商户消费、扫二维码付款、转账、红包等),无实际消费语义。推断流程:
|
||||
|
||||
1. **交易类型 = 转账** → 转账红包
|
||||
2. **交易类型 = 微信红包** → 转账红包
|
||||
3. **交易类型含 "退款"** → 退款
|
||||
4. **交易类型 = 商户消费/扫二维码付款** → 基于"商品"字段关键词推断:
|
||||
|
||||
| 微信交易类型 | 商品关键词 | 推断标准分类 |
|
||||
|-------------|-----------|-------------|
|
||||
| 商户消费 | 美团 / 外卖 / 餐厅 / 咖啡 / 面包 | 餐饮美食 |
|
||||
| 商户消费 | 滴滴 / 打车 / 地铁 / 高铁 / 加油 | 交通出行 |
|
||||
| 商户消费 | 京东 / 超市 / 便利店 / 百货 | 日用百货 |
|
||||
| 商户消费 | 电费 / 水费 / 话费 / 燃气 | 充值缴费 |
|
||||
| 商户消费 | 医院 / 药店 / 体检 | 医疗健康 |
|
||||
| 商户消费 | 电影 / 游戏 / 书籍 | 文化休闲 |
|
||||
| 商户消费 | 酒店 / 景点 / 旅行 | 酒店旅游 |
|
||||
| 商户消费 | 无法识别 | 其他(待人工补充) |
|
||||
|
||||
> **设计原则**:若商品内容无法识别关键词,先落入"其他"分类,并允许用户通过规则管理补充映射。系统应记录未命中规则的记录,便于后续规则完善。
|
||||
|
||||
#### 统一交易模型 (Transaction)
|
||||
|
||||
```go
|
||||
type Transaction struct {
|
||||
ID string
|
||||
TransactionID string // 业务唯一 ID
|
||||
BatchID string
|
||||
SourcePlatform string
|
||||
SourceRecordID string
|
||||
TradeTime time.Time
|
||||
Amount decimal.Decimal // 正数,方向独立表达
|
||||
Currency string // 默认 CNY
|
||||
Direction string // income/expense/transfer/refund/fee/other
|
||||
Counterparty string
|
||||
MerchantName string
|
||||
CategoryRaw string // 原始分类(来自平台)
|
||||
CategoryMapped string // 映射后分类(规则引擎填充)
|
||||
AccountMapped string // 映射账户
|
||||
Tags string // 逗号分隔
|
||||
OrderID string
|
||||
ParentOrderID string
|
||||
PaymentMethod string
|
||||
Note string
|
||||
RawPayload string // 原始记录完整 JSON 快照
|
||||
RowFingerprint string
|
||||
Status string // PENDING_CLEAN → CLEANED → ... → IMPORTED
|
||||
FireflyTxnID string
|
||||
}
|
||||
```
|
||||
|
||||
#### 标准化规则
|
||||
|
||||
| 规则项 | 说明 |
|
||||
|--------|------|
|
||||
| 时间 | 统一存储为 `Asia/Shanghai (UTC+8)` |
|
||||
| 金额 | 统一使用正数(`decimal(18,6)`),方向独立用 `direction` 表达 |
|
||||
| 币种 | 默认 `CNY` |
|
||||
| 状态 | 初始为 `PENDING_CLEAN` |
|
||||
| 原始快照 | 完整写入 `raw_payload`(JSON),确保审计可追溯 |
|
||||
| 指纹 | 对关键字段做 SHA256,用于严格去重 |
|
||||
|
||||
#### Direction 枚举
|
||||
|
||||
| 值 | 说明 |
|
||||
|----|------|
|
||||
| `INCOME` | 收入 |
|
||||
| `EXPENSE` | 支出 |
|
||||
| `TRANSFER` | 内部转账 |
|
||||
| `REFUND` | 退款 |
|
||||
| `FEE` | 手续费 |
|
||||
| `OTHER` | 其他 |
|
||||
|
||||
#### TransactionStatus 枚举
|
||||
|
||||
| 值 | 说明 |
|
||||
|----|------|
|
||||
| `PENDING_CLEAN` | 待清洗(标准化完成) |
|
||||
| `CLEANED` | 已清洗 |
|
||||
| `PENDING_REVIEW` | 待人工确认(模糊去重疑似) |
|
||||
| `READY_TO_IMPORT` | 可导入 |
|
||||
| `IMPORTING` | 导入中 |
|
||||
| `IMPORTED` | 已导入 |
|
||||
| `FAILED` | 导入失败 |
|
||||
| `DUPLICATE` | 重复记录 |
|
||||
@@ -0,0 +1,90 @@
|
||||
## 解析器接口设计
|
||||
|
||||
- **DDS-Section**: 6.1 解析器接口设计 + 6.2 解析器注册中心
|
||||
- **DDS-Lines**: L438-L498
|
||||
|
||||
### Extract
|
||||
|
||||
#### BillParser 接口
|
||||
|
||||
```go
|
||||
// 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 文件元信息
|
||||
|
||||
```go
|
||||
type FileMeta struct {
|
||||
FileName string
|
||||
FileType string // csv, xlsx, txt
|
||||
FileHash string
|
||||
FileSize int64
|
||||
}
|
||||
```
|
||||
|
||||
#### RawBillRecord 原始账单记录
|
||||
|
||||
```go
|
||||
type RawBillRecord struct {
|
||||
SourcePlatform string
|
||||
SourceRecordID string
|
||||
RawFields map[string]string // 原始 K-V 字段
|
||||
RowNo int
|
||||
RowFingerprint string
|
||||
}
|
||||
```
|
||||
|
||||
#### Registry 解析器注册中心
|
||||
|
||||
```go
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
#### 新增解析器步骤
|
||||
|
||||
1. 在 `parser/<platform>/` 目录下创建 `<platform>_parser.go`
|
||||
2. 实现 `BillParser` 接口的 3 个方法
|
||||
3. 在 `parser/registry.go` 的 `NewRegistry()` 中调用 `r.Register()`
|
||||
4. `Detect()` 方法基于文件名特征或 CSV 表头关键词判定
|
||||
5. `Parse()` 逐行读取文件,填充 `RawBillRecord.RawFields`
|
||||
|
||||
#### V1.0 支持的平台
|
||||
|
||||
| 平台 | 标识符 | 文件格式 | 优先级 |
|
||||
|------|--------|----------|--------|
|
||||
| 支付宝 | `alipay` | CSV | P0(优先) |
|
||||
| 微信支付 | `wechat` | CSV | P0(优先) |
|
||||
| 建设银行 | `ccb` | CSV/Excel | P1(次优先) |
|
||||
| 工商银行 | `icbc` | CSV/Excel | P1(次优先) |
|
||||
@@ -0,0 +1,41 @@
|
||||
## 行指纹生成算法
|
||||
|
||||
- **DDS-Section**: 7.2 严格去重 — 行指纹算法
|
||||
- **DDS-Lines**: L709-L726
|
||||
|
||||
### Extract
|
||||
|
||||
#### 指纹生成函数
|
||||
|
||||
```go
|
||||
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[:])
|
||||
}
|
||||
```
|
||||
|
||||
#### 关键设计决策
|
||||
|
||||
- **分钟粒度**:使用 `2006-01-02 15:04` 格式(精确到分钟),不是 `15:04:05`(秒级)
|
||||
- **原因**:不同平台对同一交易的记录时间可能有秒级差异(如支付宝记录 10:30:15,微信记录 10:30:22),分钟粒度可以容忍这种差异
|
||||
- **normalizeString** 处理:去除前后空格、全角转半角、统一大小写
|
||||
- **SHA256 输出**:64 字符的十六进制字符串
|
||||
|
||||
#### 参与指纹的字段
|
||||
|
||||
| 字段 | 处理方式 |
|
||||
|------|----------|
|
||||
| `TradeTime` | `Format("2006-01-02 15:04")` — 分钟粒度 |
|
||||
| `Amount` | `Decimal.String()` — 标准化数字字符串 |
|
||||
| `Direction` | 原值:income/expense/... |
|
||||
| `Counterparty` | `normalizeString()` — 去空格、标准化 |
|
||||
| `MerchantName` | `normalizeString()` — 去空格、标准化 |
|
||||
| `OrderID` | 原值(已在 Parser 中去除空格) |
|
||||
@@ -0,0 +1,85 @@
|
||||
## 模糊去重(多因子评分 — P1 阶段)
|
||||
|
||||
- **DDS-Section**: 7.3 模糊去重(多因子评分 — P1 阶段)
|
||||
- **DDS-Lines**: L755-L818
|
||||
|
||||
### Extract
|
||||
|
||||
#### 多因子评分模型
|
||||
|
||||
| 因子 | 分值 | 评分说明 |
|
||||
|------|------|----------|
|
||||
| 时间在 ±5 分钟内 | 30 | 时间差越小得分越高,超出窗口直接 0 分 |
|
||||
| 金额精确一致 | 30 | 金额一致得满分,差额在手续费容差内得部分分(20分) |
|
||||
| 交易方向一致 | 10 | 方向相同得满分 |
|
||||
| 订单号相同/相近 | 15 | 完全一致 15 分,包含关系 10 分 |
|
||||
| 对手方相似 | 10 | Levenshtein 相似度 + contains 判定 |
|
||||
| 来源关联规则命中 | 5 | 预配置的平台关联规则 |
|
||||
|
||||
**总分 = 100 分**
|
||||
|
||||
#### 判定阈值(可配置)
|
||||
|
||||
| 分值范围 | 判定结果 | 处理方式 |
|
||||
|----------|----------|----------|
|
||||
| ≥ 85 | 自动判定重复 | 自动标记 DUPLICATE |
|
||||
| 60 ~ 84 | 疑似重复 | 标记 PENDING_REVIEW,进入人工确认队列 |
|
||||
| < 60 | 不判定重复 | 保留独立交易 |
|
||||
|
||||
#### 评分算法骨架
|
||||
|
||||
```go
|
||||
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分) — Levenshtein 相似度
|
||||
score += int(10 * counterpartySimilarity(a.Counterparty, b.Counterparty))
|
||||
|
||||
// 来源规则因子 (5分)
|
||||
if s.platformLinked(a.SourcePlatform, b.SourcePlatform) { score += 5 }
|
||||
|
||||
return score
|
||||
}
|
||||
```
|
||||
|
||||
#### 配置项
|
||||
|
||||
| 配置键 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `fuzzy_time_window` | 5 | 模糊匹配时间窗口(分钟) |
|
||||
| `fuzzy_threshold_high` | 85 | 自动判定重复阈值 |
|
||||
| `fuzzy_threshold_low` | 60 | 疑似重复阈值 |
|
||||
| `amount_epsilon` | 0.01 | 金额容差(手续费) |
|
||||
|
||||
#### 性能优化
|
||||
|
||||
- 按 `trade_time` 时间分桶,避免全表 O(N²) 扫描
|
||||
- 使用索引 `idx_txn_trade_time` 加速时间范围查询
|
||||
@@ -0,0 +1,48 @@
|
||||
## 严格去重(精确匹配)
|
||||
|
||||
- **DDS-Section**: 7.2 严格去重(基础去重 — 精确匹配)
|
||||
- **DDS-Lines**: L697-L753
|
||||
|
||||
### Extract
|
||||
|
||||
#### 三级唯一性判定键(按优先级)
|
||||
|
||||
| 优先级 | 判定键 | 适用场景 |
|
||||
|--------|--------|----------|
|
||||
| 1 | `source_platform` + `source_record_id` | 同一平台重复导入 |
|
||||
| 2 | `source_file_hash` + `row_fingerprint` | 同一文件重复上传 |
|
||||
| 3 | `order_id`(若可信) | 跨批次订单号匹配 |
|
||||
|
||||
#### 执行流程
|
||||
|
||||
```go
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
#### 实现要点
|
||||
|
||||
- 命中时创建 `dedup_relation`(relation_type=`strict`, confidence=100)
|
||||
- 被判重的交易 status 设为 `DUPLICATE`
|
||||
- 只返回未命中的交易进入下一阶段
|
||||
- 使用数据库索引 `idx_txn_platform_record` 和 `idx_txn_fingerprint` 加速查询
|
||||
@@ -0,0 +1,78 @@
|
||||
## 转账闭环识别
|
||||
|
||||
- **DDS-Section**: 7.4 链路合并(转账闭环 + 订单链路)
|
||||
- **DDS-Lines**: L820-L871
|
||||
|
||||
### Extract
|
||||
|
||||
#### 典型场景
|
||||
|
||||
银行卡支出 1000 元(流向支付宝),支付宝收入 1000 元 → 合并为一笔内部转账。
|
||||
|
||||
#### 转账闭环识别规则(5 项全部满足)
|
||||
|
||||
| # | 条件 | 说明 |
|
||||
|---|------|------|
|
||||
| 1 | 金额一致 | `a.Amount.Equal(b.Amount)` |
|
||||
| 2 | 方向互补 | 一条 expense + 一条 income |
|
||||
| 3 | 时间窗口内 | 默认 ±30 分钟(`transfer_time_window` 可配置) |
|
||||
| 4 | 不同平台 | `a.SourcePlatform != b.SourcePlatform` |
|
||||
| 5 | 非退款/手续费 | `direction` 不是 `refund` 或 `fee` |
|
||||
|
||||
#### 实现骨架
|
||||
|
||||
```go
|
||||
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)),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 订单链路合并
|
||||
|
||||
**典型场景**:京东订单 + 微信支付,一笔真实消费产生多条流水。
|
||||
|
||||
**合并策略**:
|
||||
- 保留更完整的业务记录为主交易(优先保留有商品详情的记录)
|
||||
- 其他记录挂为关联来源
|
||||
- 形成 `parent_order_id` 聚合链路
|
||||
|
||||
#### LinkResult 数据结构
|
||||
|
||||
```go
|
||||
type LinkResult struct {
|
||||
ParentTransactionID string
|
||||
ChildTransactionID string
|
||||
LinkType string // transfer / order / refund / fee
|
||||
FromAccount string
|
||||
ToAccount string
|
||||
}
|
||||
```
|
||||
|
||||
#### 配置项
|
||||
|
||||
| 配置键 | 默认值 | 说明 |
|
||||
|--------|--------|------|
|
||||
| `transfer_time_window` | 30 | 转账闭环时间窗口(分钟) |
|
||||
@@ -0,0 +1,63 @@
|
||||
## 规则条件 JSON 格式
|
||||
|
||||
- **DDS-Section**: 8.2 规则匹配条件
|
||||
- **DDS-Lines**: L938-L967
|
||||
|
||||
### Extract
|
||||
|
||||
#### 条件匹配维度
|
||||
|
||||
| 条件类型 | 字段 | 说明 | 示例 |
|
||||
|----------|------|------|------|
|
||||
| 平台过滤 | `platform` | 指定生效平台 | `"alipay"` |
|
||||
| 原始分类 | `category_raw` | 原始分类匹配 | `"餐饮美食"` |
|
||||
| 关键词 | `keywords` | 商品/商户名关键词 | `["美团", "外卖"]` |
|
||||
| 正则 | `regex` | 正则表达式匹配 | `"^滴滴.*出行$"` |
|
||||
| 金额范围 | `amount_range` | [min, max] | `[0, 50]` |
|
||||
| 方向 | `direction` | 收支方向 | `"expense"` |
|
||||
| 对手方 | `counterparty` | 对手方包含 | `"支付宝"` |
|
||||
|
||||
#### JSON 结构示例
|
||||
|
||||
```json
|
||||
{
|
||||
"platform": "wechat",
|
||||
"conditions": {
|
||||
"category_raw": "商户消费",
|
||||
"keywords": ["美团", "外卖", "饿了么"],
|
||||
"direction": "expense"
|
||||
},
|
||||
"actions": {
|
||||
"category_mapped": "餐饮美食",
|
||||
"merchant_normalized": "外卖平台"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Rule 数据模型
|
||||
|
||||
```go
|
||||
type Rule struct {
|
||||
ID string // UUID
|
||||
RuleType string // 规则类型枚举
|
||||
Priority int // 优先级(越小越高)
|
||||
PlatformScope string // 平台范围:all / alipay / wechat
|
||||
ConditionsJSON string // 条件 JSON
|
||||
ActionsJSON string // 动作 JSON
|
||||
Enabled bool // 是否启用
|
||||
Description string // 规则描述
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
```
|
||||
|
||||
#### RuleType 枚举
|
||||
|
||||
| 类型 | 说明 |
|
||||
|------|------|
|
||||
| `COUNTERPARTY_NORMALIZE` | 对手方归一化 |
|
||||
| `MERCHANT_NORMALIZE` | 商户名归一化 |
|
||||
| `CATEGORY_MAPPING` | 分类映射 |
|
||||
| `ACCOUNT_MAPPING` | 账户映射 |
|
||||
| `TAG_MAPPING` | 标签映射 |
|
||||
| `FIREFLY_FIELD_MAPPING` | Firefly 字段映射 |
|
||||
@@ -0,0 +1,89 @@
|
||||
## 规则执行顺序与可解释性
|
||||
|
||||
- **DDS-Section**: 8.3 规则执行顺序 + 8.4 规则引擎核心实现 + 8.5 可解释性设计
|
||||
- **DDS-Lines**: L969-L1045
|
||||
|
||||
### Extract
|
||||
|
||||
#### 固定执行顺序(不可变)
|
||||
|
||||
```
|
||||
1. 对手方归一化 (COUNTERPARTY_NORMALIZE)
|
||||
↓
|
||||
2. 商户归一化 (MERCHANT_NORMALIZE)
|
||||
↓
|
||||
3. 分类映射 (CATEGORY_MAPPING)
|
||||
↓
|
||||
4. 账户映射 (ACCOUNT_MAPPING)
|
||||
↓
|
||||
5. 标签映射 (TAG_MAPPING)
|
||||
↓
|
||||
6. Firefly 字段映射 (FIREFLY_FIELD_MAPPING)
|
||||
```
|
||||
|
||||
**设计原因**:先做归一再做分类,可提升规则命中率与稳定性。例如先将"美团外卖-北京"归一为"美团外卖",再匹配分类规则"美团 → 餐饮美食"。
|
||||
|
||||
#### 执行原则
|
||||
|
||||
- 同一类型内按 `priority` 升序执行(数字越小优先级越高)
|
||||
- **首条命中即停止**(同类型中第一个匹配的规则生效,后续不再匹配)
|
||||
- 每条交易记录所有命中的规则 ID 和前后字段对比
|
||||
|
||||
#### 规则引擎核心实现
|
||||
|
||||
```go
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
#### 可解释性设计 — RuleHit 审计
|
||||
|
||||
每条交易保留完整的规则命中记录:
|
||||
|
||||
| 信息项 | 说明 |
|
||||
|--------|------|
|
||||
| 命中规则 ID | 关联 rules 表 |
|
||||
| 命中条件摘要 | 匹配的具体关键词/正则 |
|
||||
| 变更前值 | 规则执行前的字段值 |
|
||||
| 变更后值 | 规则执行后的字段值 |
|
||||
| 命中时间 | 规则执行时间戳 |
|
||||
|
||||
用于前端"为何被分到餐饮/交通"的解释展示。
|
||||
@@ -0,0 +1,252 @@
|
||||
## 数据库表结构设计
|
||||
|
||||
- **DDS-Section**: 10. 数据库详细设计(SQLite + GORM)
|
||||
- **DDS-Lines**: L1135-L1347
|
||||
|
||||
### Extract
|
||||
|
||||
#### ER 关系总览
|
||||
|
||||
```
|
||||
IMPORT_BATCHES ──1:N──> SOURCE_FILES ──1:N──> RAW_RECORDS
|
||||
IMPORT_BATCHES ──1:N──> TRANSACTIONS
|
||||
RAW_RECORDS ──1:1──> TRANSACTIONS (normalizes_to)
|
||||
TRANSACTIONS ──1:N──> DEDUP_RELATIONS
|
||||
TRANSACTIONS ──1:N──> LINK_RELATIONS
|
||||
TRANSACTIONS ──1:N──> RULE_HITS
|
||||
RULES ──1:N──> RULE_HITS
|
||||
IMPORT_BATCHES ──1:N──> IMPORT_TASKS ──1:N──> IMPORT_RESULTS
|
||||
TRANSACTIONS ──1:N──> AUDIT_LOGS
|
||||
```
|
||||
|
||||
共 **11 张核心表**。
|
||||
|
||||
#### 表结构定义
|
||||
|
||||
##### 1. IMPORT_BATCHES — 导入批次
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | varchar(36) | PK | UUID 主键 |
|
||||
| status | varchar(32) | | 批次状态 |
|
||||
| total_files | int | | 文件总数 |
|
||||
| total_records | int | | 记录总数 |
|
||||
| success_count | int | | 成功数 |
|
||||
| failed_count | int | | 失败数 |
|
||||
| duplicate_count | int | | 重复数 |
|
||||
| created_at | datetime | | 创建时间 |
|
||||
| updated_at | datetime | | 更新时间 |
|
||||
|
||||
##### 2. SOURCE_FILES — 源文件
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | varchar(36) | PK | UUID 主键 |
|
||||
| batch_id | varchar(36) | FK | 批次 ID |
|
||||
| file_name | varchar(255) | | 原始文件名 |
|
||||
| file_hash | varchar(64) | INDEX | 文件 SHA256 哈希 |
|
||||
| source_platform | varchar(32) | | 来源平台 |
|
||||
| file_type | varchar(16) | | csv/xlsx/txt |
|
||||
| file_size | int | | 文件大小(bytes) |
|
||||
| uploaded_at | datetime | | 上传时间 |
|
||||
|
||||
##### 3. RAW_RECORDS — 原始记录
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | varchar(36) | PK | UUID 主键 |
|
||||
| source_file_id | varchar(36) | FK | 来源文件 ID |
|
||||
| row_no | int | | 行号 |
|
||||
| source_platform | varchar(32) | | 平台 |
|
||||
| source_record_id | varchar(128) | | 原始流水号 |
|
||||
| row_fingerprint | varchar(64) | INDEX | 行指纹 SHA256 |
|
||||
| raw_payload | text | | 原始 JSON 快照 |
|
||||
| parse_status | varchar(32) | | 解析状态 |
|
||||
| parse_error | text | | 错误信息 |
|
||||
|
||||
##### 4. TRANSACTIONS — 统一交易记录(核心表)
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | varchar(36) | PK | UUID 主键 |
|
||||
| transaction_id | varchar(64) | UNIQUE | 业务唯一 ID |
|
||||
| batch_id | varchar(36) | INDEX | 导入批次 |
|
||||
| raw_record_id | varchar(36) | | 原始记录 ID |
|
||||
| source_platform | varchar(32) | INDEX(组合) | 来源平台 |
|
||||
| source_record_id | varchar(128) | INDEX(组合) | 原始记录号 |
|
||||
| trade_time | datetime | INDEX, NOT NULL | 交易时间 |
|
||||
| amount | decimal(18,6) | NOT NULL | 金额 |
|
||||
| currency | varchar(16) | DEFAULT 'CNY' | 币种 |
|
||||
| direction | varchar(16) | NOT NULL | 方向 |
|
||||
| counterparty | varchar(255) | | 对手方 |
|
||||
| merchant_name | varchar(255) | | 商户名 |
|
||||
| category_raw | varchar(128) | | 原始分类 |
|
||||
| category_mapped | varchar(128) | | 映射分类 |
|
||||
| account_mapped | varchar(128) | | 映射账户 |
|
||||
| tags | varchar(512) | | 标签(逗号分隔) |
|
||||
| order_id | varchar(128) | INDEX | 订单号 |
|
||||
| parent_order_id | varchar(128) | | 父链路号 |
|
||||
| payment_method | varchar(128) | | 支付方式 |
|
||||
| note | text | | 备注 |
|
||||
| raw_payload | text | | 原始记录 JSON |
|
||||
| row_fingerprint | varchar(64) | INDEX | 行指纹 |
|
||||
| status | varchar(32) | INDEX, DEFAULT 'PENDING_CLEAN' | 状态 |
|
||||
| firefly_txn_id | varchar(128) | | Firefly 交易 ID |
|
||||
| imported_at | datetime | | 导入时间 |
|
||||
| created_at | datetime | | 创建时间 |
|
||||
| updated_at | datetime | | 更新时间 |
|
||||
|
||||
##### 5. DEDUP_RELATIONS — 去重关系
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | varchar(36) | PK | UUID 主键 |
|
||||
| src_transaction_id | varchar(36) | FK, INDEX | 原交易 ID |
|
||||
| target_transaction_id | varchar(36) | FK | 目标交易 ID |
|
||||
| relation_type | varchar(16) | | strict/fuzzy |
|
||||
| confidence | int | | 置信度 0-100 |
|
||||
| status | varchar(16) | | auto/confirmed/rejected |
|
||||
| reason_json | text | | 判定依据 JSON |
|
||||
| created_at | datetime | | 创建时间 |
|
||||
|
||||
##### 6. LINK_RELATIONS — 链路关系
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | varchar(36) | PK | UUID 主键 |
|
||||
| parent_transaction_id | varchar(36) | FK | 主交易 ID |
|
||||
| child_transaction_id | varchar(36) | FK | 子交易 ID |
|
||||
| link_type | varchar(16) | | transfer/order/refund/fee |
|
||||
| reason_json | text | | 关联依据 JSON |
|
||||
| created_at | datetime | | 创建时间 |
|
||||
|
||||
##### 7. RULES — 规则定义
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | varchar(36) | PK | UUID 主键 |
|
||||
| rule_type | varchar(32) | INDEX(组合) | 规则类型 |
|
||||
| priority | int | INDEX(组合) | 优先级 |
|
||||
| platform_scope | varchar(32) | | 平台范围 |
|
||||
| conditions_json | text | | 条件 JSON |
|
||||
| actions_json | text | | 动作 JSON |
|
||||
| enabled | boolean | | 是否启用 |
|
||||
| description | varchar(255) | | 规则描述 |
|
||||
| created_at | datetime | | 创建时间 |
|
||||
| updated_at | datetime | | 更新时间 |
|
||||
|
||||
##### 8. RULE_HITS — 规则命中记录
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | varchar(36) | PK | UUID 主键 |
|
||||
| transaction_id | varchar(36) | FK | 交易 ID |
|
||||
| rule_id | varchar(36) | FK | 规则 ID |
|
||||
| matched_condition | text | | 命中条件摘要 |
|
||||
| before_value | text | | 变更前值 |
|
||||
| after_value | text | | 变更后值 |
|
||||
| created_at | datetime | | 执行时间 |
|
||||
|
||||
##### 9. IMPORT_TASKS — 导入任务
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | varchar(36) | PK | UUID 主键 |
|
||||
| batch_id | varchar(36) | FK | 批次 ID |
|
||||
| export_mode | varchar(16) | | api/csv |
|
||||
| status | varchar(32) | | pending/running/success/partial_failed/failed |
|
||||
| total_count | int | | 总记录数 |
|
||||
| success_count | int | | 成功数 |
|
||||
| failed_count | int | | 失败数 |
|
||||
| started_at | datetime | | 开始时间 |
|
||||
| finished_at | datetime | | 完成时间 |
|
||||
|
||||
##### 10. IMPORT_RESULTS — 导入结果
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | varchar(36) | PK | UUID 主键 |
|
||||
| task_id | varchar(36) | FK | 任务 ID |
|
||||
| transaction_id | varchar(36) | FK | 交易 ID |
|
||||
| status | varchar(16) | | success/failed |
|
||||
| error_code | varchar(32) | | 错误码 |
|
||||
| error_message | text | | 错误描述 |
|
||||
| firefly_txn_id | varchar(128) | | Firefly 返回 ID |
|
||||
| retry_count | int | | 重试次数 |
|
||||
| created_at | datetime | | 创建时间 |
|
||||
|
||||
##### 11. AUDIT_LOGS — 审计日志
|
||||
|
||||
| 字段 | 类型 | 约束 | 说明 |
|
||||
|------|------|------|------|
|
||||
| id | varchar(36) | PK | UUID 主键 |
|
||||
| entity_type | varchar(64) | | 实体类型 |
|
||||
| entity_id | varchar(36) | | 实体 ID |
|
||||
| action | varchar(32) | | 操作类型 |
|
||||
| before_snapshot | text | | 变更前快照 |
|
||||
| after_snapshot | text | | 变更后快照 |
|
||||
| operator | varchar(64) | | 操作者 |
|
||||
| created_at | datetime | | 操作时间 |
|
||||
|
||||
#### GORM Model 示例(Transaction)
|
||||
|
||||
```go
|
||||
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" }
|
||||
```
|
||||
|
||||
#### SQLite 性能配置
|
||||
|
||||
```go
|
||||
func initDB(dbPath string) *gorm.DB {
|
||||
db, _ := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.SetMaxOpenConns(1) // SQLite 单写
|
||||
sqlDB.SetMaxIdleConns(10)
|
||||
db.Exec("PRAGMA journal_mode=WAL")
|
||||
db.Exec("PRAGMA synchronous=NORMAL")
|
||||
db.Exec("PRAGMA cache_size=-64000") // 64MB cache
|
||||
return db
|
||||
}
|
||||
```
|
||||
|
||||
#### 事务边界设计
|
||||
|
||||
```
|
||||
事务 1: 文件入库 + 原始记录入库
|
||||
事务 2: 标准化结果落库
|
||||
事务 3: 去重/链路关系落库
|
||||
事务 4: 规则命中落库
|
||||
事务 5: 导入结果落库
|
||||
```
|
||||
|
||||
每阶段独立事务,使用 `CreateInBatches` 每批 500 条。
|
||||
@@ -0,0 +1,25 @@
|
||||
## 关键索引设计
|
||||
|
||||
- **DDS-Section**: 10.2 关键索引设计
|
||||
- **DDS-Lines**: L1296-L1310
|
||||
|
||||
### Extract
|
||||
|
||||
| 表名 | 索引名 | 索引列 | 用途 |
|
||||
|------|--------|--------|------|
|
||||
| `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` | 去重关系查询 |
|
||||
|
||||
#### 性能说明
|
||||
|
||||
- `idx_txn_platform_record` 是严格去重最频繁命中的索引,组合索引比两个单列索引效率更高
|
||||
- `idx_txn_trade_time` 用于模糊去重的时间分桶,避免全表 O(N²) 扫描
|
||||
- `idx_rule_type_priority` 确保规则按类型分组、按优先级有序加载
|
||||
@@ -0,0 +1,87 @@
|
||||
## API 接口目录
|
||||
|
||||
- **DDS-Section**: 11. API 接口设计(GIN RESTful)
|
||||
- **DDS-Lines**: L1350-L1451
|
||||
|
||||
### Extract
|
||||
|
||||
#### 导入中心 API
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| `POST` | `/api/v1/import/batches` | 上传账单文件,创建批次(multipart/form-data) |
|
||||
| `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"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 交易记录 API
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| `GET` | `/api/v1/transactions` | 分页查询交易记录 |
|
||||
| `GET` | `/api/v1/transactions/:id` | 获取交易详情(含规则命中记录) |
|
||||
| `GET` | `/api/v1/transactions/:id/trace` | 获取交易完整处理链路 |
|
||||
|
||||
#### 去重确认 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` | 拒绝合并 |
|
||||
|
||||
#### 规则管理 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` | 测试规则命中预览 |
|
||||
|
||||
#### 导入/导出 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 文件 |
|
||||
|
||||
#### 审计与系统 API
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| `GET` | `/api/v1/audit/logs` | 获取操作日志列表 |
|
||||
| `GET` | `/api/v1/settings` | 获取系统配置 |
|
||||
| `PUT` | `/api/v1/settings` | 更新系统配置(Firefly 连接等) |
|
||||
| `POST` | `/api/v1/settings/test-connection` | 测试 Firefly III 连接 |
|
||||
@@ -0,0 +1,57 @@
|
||||
## 统一响应格式与错误码
|
||||
|
||||
- **DDS-Section**: 11.1 统一响应格式 + 14.4 错误处理策略
|
||||
- **DDS-Lines**: L1352-L1369, L1787-L1816
|
||||
|
||||
### Extract
|
||||
|
||||
#### 统一响应结构
|
||||
|
||||
```go
|
||||
// 普通响应
|
||||
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"`
|
||||
}
|
||||
```
|
||||
|
||||
#### 错误码定义
|
||||
|
||||
| 错误码 | 常量名 | 说明 |
|
||||
|--------|--------|------|
|
||||
| 0 | `ErrCodeSuccess` | 成功 |
|
||||
| 40000 | `ErrCodeBadRequest` | 通用参数错误 |
|
||||
| 40001 | `ErrCodeFileParseError` | 文件解析失败 |
|
||||
| 40002 | `ErrCodeUnknownPlatform` | 未识别的平台来源 |
|
||||
| 40003 | `ErrCodeDuplicateFile` | 重复上传文件 |
|
||||
| 40004 | `ErrCodeRuleInvalid` | 规则定义无效 |
|
||||
| 40005 | `ErrCodeExportFailed` | 导出/推送失败 |
|
||||
| 50000 | `ErrCodeInternal` | 服务器内部错误 |
|
||||
|
||||
#### 全局错误处理中间件
|
||||
|
||||
```go
|
||||
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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,65 @@
|
||||
## Firefly III 导出适配
|
||||
|
||||
- **DDS-Section**: 9. Firefly III / Data Importer 适配设计
|
||||
- **DDS-Lines**: L1048-L1132
|
||||
|
||||
### Extract
|
||||
|
||||
#### 两段式规则映射策略
|
||||
|
||||
**第一阶段 — ProjectMoneyX 负责**:
|
||||
1. 字段级映射:异构字段 → 统一模型
|
||||
2. 业务分类映射:对手方/描述 → 分类/标签
|
||||
3. 商户名归一化:别名 → 统一名称
|
||||
|
||||
**第二阶段 — Firefly III 负责**:
|
||||
1. 最后一层字段适配
|
||||
2. 临时补充规则
|
||||
3. 导入格式兼容
|
||||
|
||||
#### 导出模式
|
||||
|
||||
##### 模式 A:API 推送模式(优先)
|
||||
|
||||
```
|
||||
Export Engine → Data Importer (POST /api/v1/import) → Firefly III
|
||||
↓
|
||||
返回成功/失败明细 → 更新 import_results 表
|
||||
```
|
||||
|
||||
##### 模式 B:中间文件导出模式
|
||||
|
||||
- 生成完全符合 Data Importer 规范的标准 CSV / JSON
|
||||
- 用户手动下载后在 Data Importer 中执行导入
|
||||
- 适合 API 不可用或权限受限场景
|
||||
|
||||
#### Firefly 交易类型映射
|
||||
|
||||
| 内部 Direction | Firefly Type | 说明 |
|
||||
|----------------|--------------|------|
|
||||
| `expense` | `withdrawal` | 支出 |
|
||||
| `income` | `deposit` | 收入 |
|
||||
| `transfer` | `transfer` | 内部转账 |
|
||||
| `refund` | `deposit` | 退款(作为收入处理) |
|
||||
| `fee` | `withdrawal` | 手续费(作为支出处理) |
|
||||
|
||||
#### ImportResult 数据结构
|
||||
|
||||
```go
|
||||
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
|
||||
}
|
||||
```
|
||||
|
||||
#### 导入后反馈
|
||||
|
||||
- 导入成功/失败数量统计
|
||||
- 失败原因分类展示(字段缺失、格式错误、账户不存在等)
|
||||
- 失败记录可单独重试(无需整批重做)
|
||||
@@ -0,0 +1,24 @@
|
||||
## 导入前校验清单
|
||||
|
||||
- **DDS-Section**: 9.4 导入前校验清单
|
||||
- **DDS-Lines**: L1103-L1112
|
||||
|
||||
### Extract
|
||||
|
||||
#### 6 项必须校验
|
||||
|
||||
| # | 校验项 | 说明 | 失败处理 |
|
||||
|---|--------|------|----------|
|
||||
| 1 | 必填字段完整性 | `amount`, `trade_time`, `direction` 不可为空 | 标记为 FAILED,记录具体缺失字段 |
|
||||
| 2 | 金额格式合法性 | 必须为正数,精度不超过 6 位小数 | 标记为 FAILED |
|
||||
| 3 | 时间格式合法性 | 必须为有效日期时间 | 标记为 FAILED |
|
||||
| 4 | 账户映射完整性 | 来源平台必须有对应的 Firefly 账户映射 | 标记为 FAILED,提示配置账户映射 |
|
||||
| 5 | 重复导入拦截 | 检查 `transaction_id` 是否已在 Firefly III 中存在 | 跳过并标记 |
|
||||
| 6 | 未确认记录检查 | 是否存在 `PENDING_REVIEW` 状态的疑似重复记录 | 阻断导入,提示先处理待确认记录 |
|
||||
|
||||
#### 校验实现要点
|
||||
|
||||
- 校验在 Export Engine 的 `validate()` 方法中统一执行
|
||||
- 校验失败的记录不参与推送,但不影响其他记录
|
||||
- 校验结果写入 `import_results` 表,前端可展示失败原因
|
||||
- 第 6 项(未确认记录)为全局阻断性校验,有未确认记录时整批不可导入
|
||||
@@ -0,0 +1,82 @@
|
||||
## 前端核心组件与交互
|
||||
|
||||
- **DDS-Section**: 12.2 页面职责与交互说明
|
||||
- **DDS-Lines**: L1489-L1572
|
||||
|
||||
### Extract
|
||||
|
||||
#### 核心可复用组件
|
||||
|
||||
| 组件 | 文件 | 用途 |
|
||||
|------|------|------|
|
||||
| 文件上传器 | `FileUploader.vue` | 拖拽 + 点击上传,进度条,平台自动检测 |
|
||||
| 交易表格 | `TransactionTable.vue` | `v-data-table` + 行展开 + 批量操作 |
|
||||
| 规则编辑器 | `RuleEditor.vue` | 条件构建器 + 动作配置 + 测试预览 |
|
||||
| 去重对比 | `DedupCompare.vue` | 左右分栏对比 + 评分因子 + 差异高亮 |
|
||||
| 审计时间线 | `AuditTimeline.vue` | `v-timeline` + 快照展开 |
|
||||
|
||||
#### 1. FileUploader.vue
|
||||
|
||||
- 使用 Vuetify `v-file-input` + 自定义拖拽区域
|
||||
- 文件选择后立即调用 `POST /api/v1/import/batches`
|
||||
- 上传进度条实时展示
|
||||
- 自动检测文件来源平台,允许手动修改
|
||||
- 批量文件列表显示文件名、大小、检测到的平台
|
||||
|
||||
#### 2. TransactionTable.vue
|
||||
|
||||
- 使用 Vuetify `v-data-table` 实现分页排序
|
||||
- 行展开(`expanded`)显示详情面板:原始字段 vs 标准字段对比
|
||||
- 规则命中说明显示该条交易命中的具体规则
|
||||
- 状态标记:以颜色标签区分 待清洗/已清洗/重复/待确认
|
||||
- 筛选器:支持按来源平台、分类、方向、状态过滤
|
||||
- 必须 `fixed-header` + 列宽显式指定
|
||||
|
||||
#### 3. DedupCompare.vue
|
||||
|
||||
- 左右分栏对比布局
|
||||
- 差异字段高亮显示
|
||||
- 评分因子可展开查看(6 项因子各自得分)
|
||||
- 操作按钮:确认合并 / 拒绝合并 / 暂时跳过
|
||||
- 链路视图展示已识别的转账闭环和订单链路
|
||||
|
||||
#### 4. RuleEditor.vue
|
||||
|
||||
- 条件构建器:支持多条件 AND/OR 组合
|
||||
- 使用 Vuetify `v-select`、`v-text-field`、`v-chip` 构建
|
||||
- 按类型分组展示规则列表,支持拖拽排序优先级
|
||||
- 测试按钮触发 `POST /api/v1/rules/:id/test`
|
||||
- 启用/禁用一键切换
|
||||
|
||||
#### 5. AuditTimeline.vue
|
||||
|
||||
- 使用 Vuetify `v-timeline` 组件
|
||||
- 每个节点可展开查看详细快照数据
|
||||
- 处理链路:原始文件 → 原始记录 → 标准化 → 规则命中 → 导入结果
|
||||
|
||||
#### Pinia Store 清单
|
||||
|
||||
| Store | 文件 | 管理的状态 |
|
||||
|-------|------|-----------|
|
||||
| Import Store | `importStore.ts` | 批次列表、上传状态、处理进度 |
|
||||
| Transaction Store | `transactionStore.ts` | 交易记录分页、筛选条件、详情 |
|
||||
| Rule Store | `ruleStore.ts` | 规则列表、编辑表单、测试结果 |
|
||||
|
||||
#### API 模块清单
|
||||
|
||||
| 模块 | 文件 | 封装的 API 组 |
|
||||
|------|------|-------------|
|
||||
| Import API | `api/import.ts` | 批次上传、列表、详情、处理 |
|
||||
| Transaction API | `api/transaction.ts` | 交易查询、详情、链路 |
|
||||
| Dedup API | `api/dedup.ts` | 去重列表、确认、拒绝 |
|
||||
| Rule API | `api/rule.ts` | 规则 CRUD、测试、评估 |
|
||||
| Export API | `api/export.ts` | 导入任务、重试、CSV 导出 |
|
||||
| Audit API | `api/audit.ts` | 审计日志查询 |
|
||||
|
||||
#### TypeScript 类型清单
|
||||
|
||||
| 文件 | 定义的类型 |
|
||||
|------|-----------|
|
||||
| `types/transaction.ts` | Transaction, Direction, TransactionStatus, RawRecord |
|
||||
| `types/rule.ts` | Rule, RuleType, RuleCondition, RuleHit |
|
||||
| `types/common.ts` | Response, PageResponse, ImportBatch, SourceFile, ImportTask |
|
||||
@@ -0,0 +1,95 @@
|
||||
## 前端路由与页面
|
||||
|
||||
- **DDS-Section**: 12.3 前端路由设计 + 12.1 信息架构与导航
|
||||
- **DDS-Lines**: L1454-L1628
|
||||
|
||||
### Extract
|
||||
|
||||
#### 信息架构
|
||||
|
||||
```
|
||||
侧边导航栏 → 导入中心
|
||||
→ 数据清洗
|
||||
→ 去重处理
|
||||
→ 规则管理
|
||||
→ 导入任务
|
||||
→ 数据审计
|
||||
→ 系统设置
|
||||
|
||||
导入中心 → 文件上传页 / 批次列表页 / 批次详情页
|
||||
数据清洗 → 清洗结果预览页
|
||||
去重处理 → 重复记录处理页
|
||||
规则管理 → 规则列表页 / 规则编辑页
|
||||
导入任务 → 导入任务列表页 / 导入结果详情页
|
||||
数据审计 → 审计追溯页 / 交易处理链路页
|
||||
系统设置 → Firefly 连接配置 / 去重参数配置
|
||||
```
|
||||
|
||||
#### 路由定义
|
||||
|
||||
```typescript
|
||||
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' }
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### 页面文件清单
|
||||
|
||||
| 页面 | 文件 | 用途 |
|
||||
|------|------|------|
|
||||
| 导入中心 | `ImportCenterView.vue` | 文件上传 + 批次列表 |
|
||||
| 批次详情 | `BatchDetailView.vue` | 单批次详情 |
|
||||
| 清洗预览 | `PreviewView.vue` | 标准化结果预览 |
|
||||
| 去重处理 | `DedupReviewView.vue` | 疑似重复确认 |
|
||||
| 规则配置 | `RuleConfigView.vue` | 规则 CRUD |
|
||||
| 导入任务 | `ImportTaskView.vue` | 导入结果展示 |
|
||||
| 审计追溯 | `AuditTraceView.vue` | 全链路追溯 |
|
||||
| 系统设置 | `SettingsView.vue` | Firefly 配置 |
|
||||
@@ -0,0 +1,86 @@
|
||||
## 部署与安全设计
|
||||
|
||||
- **DDS-Section**: 13.2 安全设计 + 13.5 部署架构
|
||||
- **DDS-Lines**: L1663-L1732
|
||||
|
||||
### Extract
|
||||
|
||||
#### 安全措施
|
||||
|
||||
| 安全措施 | 说明 |
|
||||
|----------|------|
|
||||
| 本地部署 | 默认本地运行,敏感账单数据不上传云端 |
|
||||
| API Token 加密 | Firefly III API Token 使用 AES 加密存储 |
|
||||
| 审计日志脱敏 | 日志中账号、订单号局部遮罩(如 `138****1234`) |
|
||||
| 文件安全 | 上传文件限制类型和大小(默认最大 50MB) |
|
||||
| CORS 配置 | 仅允许本地来源访问 API |
|
||||
|
||||
#### 部署架构
|
||||
|
||||
```
|
||||
Docker 容器 / 本地部署
|
||||
├── 前端静态资源 (Vue3 Build → /web)
|
||||
├── 后端服务 (Go Binary :8080)
|
||||
└── SQLite 数据库 (/data/projectmoneyx.db)
|
||||
|
||||
外部依赖(可选)
|
||||
├── Firefly III (API 推送)
|
||||
└── Data Importer (API 推送)
|
||||
```
|
||||
|
||||
#### 部署方式
|
||||
|
||||
1. **Docker 部署**(推荐):单容器包含前后端 + SQLite
|
||||
2. **二进制部署**:交叉编译为单体可执行文件,前端资源使用 Go `embed` 嵌入
|
||||
|
||||
#### Dockerfile 示例
|
||||
|
||||
```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"]
|
||||
```
|
||||
|
||||
#### 配置管理
|
||||
|
||||
```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"`
|
||||
}
|
||||
|
||||
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"`
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,55 @@
|
||||
## 性能设计
|
||||
|
||||
- **DDS-Section**: 13.1 性能设计 + 13.3 可维护性设计
|
||||
- **DDS-Lines**: L1632-L1681
|
||||
|
||||
### Extract
|
||||
|
||||
#### 性能目标
|
||||
|
||||
单次导入 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 性能配置
|
||||
|
||||
```go
|
||||
func initDB(dbPath string) *gorm.DB {
|
||||
db, _ := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||
sqlDB, _ := db.DB()
|
||||
sqlDB.SetMaxOpenConns(1) // SQLite 单写
|
||||
sqlDB.SetMaxIdleConns(10)
|
||||
db.Exec("PRAGMA journal_mode=WAL")
|
||||
db.Exec("PRAGMA synchronous=NORMAL")
|
||||
db.Exec("PRAGMA cache_size=-64000") // 64MB cache
|
||||
return db
|
||||
}
|
||||
```
|
||||
|
||||
#### 可维护性设计
|
||||
|
||||
| 原则 | 说明 |
|
||||
|------|------|
|
||||
| 解析器插件化 | 新增平台只需实现 `BillParser` 接口并注册 |
|
||||
| 规则条件 JSON 化 | 规则存储为 JSON,灵活扩展匹配条件 |
|
||||
| 导入器解耦 | Export 层独立,可替换下游目标 |
|
||||
| 分层 DTO/VO/Entity | Handler → DTO → Service → Entity → DAO |
|
||||
| 事务分阶段 | 每个 ETL 阶段独立事务,避免超长事务 |
|
||||
|
||||
#### 可追溯性设计
|
||||
|
||||
| 追溯能力 | 实现方式 |
|
||||
|----------|----------|
|
||||
| 任一导入结果 → 原始文件 | `transaction.raw_record_id → raw_record.source_file_id → source_file` |
|
||||
| 任一规则命中 → 解释说明 | `rule_hits` 表记录命中条件和前后字段值对比 |
|
||||
| 任一合并操作 → 判定依据 | `dedup_relations.reason_json` 和 `link_relations.reason_json` |
|
||||
| 任一操作 → 操作日志 | `audit_logs` 表记录实体变更和操作者信息 |
|
||||
101
.agents/skills/developing-projectmoneyx/scripts/verify.sh
Normal file
101
.agents/skills/developing-projectmoneyx/scripts/verify.sh
Normal file
@@ -0,0 +1,101 @@
|
||||
#!/bin/bash
|
||||
# verify.sh - developing-projectmoneyx Skill 结构与内容验证
|
||||
set -e
|
||||
|
||||
PASS=0; FAIL=0
|
||||
check() {
|
||||
if eval "$2"; then
|
||||
echo "✅ PASS: $1"; ((PASS++))
|
||||
else
|
||||
echo "❌ FAIL: $1"; ((FAIL++))
|
||||
fi
|
||||
}
|
||||
|
||||
SKILL_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
|
||||
# ========================
|
||||
# 结构完整性检查
|
||||
# ========================
|
||||
|
||||
check "SKILL.md 存在" "test -f '$SKILL_DIR/SKILL.md'"
|
||||
check "reference/ 目录存在" "test -d '$SKILL_DIR/reference'"
|
||||
check "scripts/ 目录存在" "test -d '$SKILL_DIR/scripts'"
|
||||
check "SKILL.md < 500 行" "[ $(wc -l < '$SKILL_DIR/SKILL.md') -lt 500 ]"
|
||||
|
||||
# ========================
|
||||
# Frontmatter 检查
|
||||
# ========================
|
||||
|
||||
check "frontmatter 包含 name" "head -20 '$SKILL_DIR/SKILL.md' | grep -q '^name:'"
|
||||
check "frontmatter 包含 description" "head -20 '$SKILL_DIR/SKILL.md' | grep -q '^description:'"
|
||||
check "frontmatter 包含 argument-hint" "head -20 '$SKILL_DIR/SKILL.md' | grep -q '^argument-hint:'"
|
||||
check "frontmatter 包含 allowed-tools" "head -20 '$SKILL_DIR/SKILL.md' | grep -q '^allowed-tools:'"
|
||||
|
||||
# ========================
|
||||
# 章节检查
|
||||
# ========================
|
||||
|
||||
check "包含 Quick Context 章节" "grep -q '## Quick Context' '$SKILL_DIR/SKILL.md'"
|
||||
check "包含 Plan 章节" "grep -q '## Plan' '$SKILL_DIR/SKILL.md'"
|
||||
check "包含 Verify 章节" "grep -q '## Verify' '$SKILL_DIR/SKILL.md'"
|
||||
check "包含 Execute 章节" "grep -q '## Execute' '$SKILL_DIR/SKILL.md'"
|
||||
check "包含 Pitfalls 章节" "grep -q '## Pitfalls' '$SKILL_DIR/SKILL.md'"
|
||||
check "包含 Related References 章节" "grep -q '## Related References' '$SKILL_DIR/SKILL.md'"
|
||||
|
||||
# ========================
|
||||
# 动态注入检查
|
||||
# ========================
|
||||
|
||||
check "包含至少 2 处动态注入命令" "[ $(grep -c '!\`' '$SKILL_DIR/SKILL.md') -ge 2 ]"
|
||||
|
||||
# ========================
|
||||
# Pitfalls 引用 reference 检查
|
||||
# ========================
|
||||
|
||||
check "Pitfalls 引用 reference/ 至少 2 处" "[ $(grep -A 100 '## Pitfalls' '$SKILL_DIR/SKILL.md' | grep -c 'reference/') -ge 2 ]"
|
||||
|
||||
# ========================
|
||||
# reference 目录结构检查
|
||||
# ========================
|
||||
|
||||
check "reference 有 01-architecture 子目录" "test -d '$SKILL_DIR/reference/01-architecture'"
|
||||
check "reference 有 02-parser-engine 子目录" "test -d '$SKILL_DIR/reference/02-parser-engine'"
|
||||
check "reference 有 03-dedup-engine 子目录" "test -d '$SKILL_DIR/reference/03-dedup-engine'"
|
||||
check "reference 有 04-rule-engine 子目录" "test -d '$SKILL_DIR/reference/04-rule-engine'"
|
||||
check "reference 有 05-database 子目录" "test -d '$SKILL_DIR/reference/05-database'"
|
||||
check "reference 有 06-api-design 子目录" "test -d '$SKILL_DIR/reference/06-api-design'"
|
||||
check "reference 有 07-export-engine 子目录" "test -d '$SKILL_DIR/reference/07-export-engine'"
|
||||
check "reference 有 08-frontend 子目录" "test -d '$SKILL_DIR/reference/08-frontend'"
|
||||
check "reference 有 09-nonfunctional 子目录" "test -d '$SKILL_DIR/reference/09-nonfunctional'"
|
||||
|
||||
# ========================
|
||||
# reference 内容检查
|
||||
# ========================
|
||||
|
||||
check "reference 文件含 DDS-Section 溯源" "grep -rq 'DDS-Section:' '$SKILL_DIR/reference/' 2>/dev/null"
|
||||
check "reference 文件含 DDS-Lines 溯源" "grep -rq 'DDS-Lines:' '$SKILL_DIR/reference/' 2>/dev/null"
|
||||
|
||||
# ========================
|
||||
# 关键 reference 文件存在检查
|
||||
# ========================
|
||||
|
||||
check "system-overview.md 存在" "test -f '$SKILL_DIR/reference/01-architecture/system-overview.md'"
|
||||
check "batch-state-machine.md 存在" "test -f '$SKILL_DIR/reference/01-architecture/batch-state-machine.md'"
|
||||
check "parser-interface.md 存在" "test -f '$SKILL_DIR/reference/02-parser-engine/parser-interface.md'"
|
||||
check "field-mappings.md 存在" "test -f '$SKILL_DIR/reference/02-parser-engine/field-mappings.md'"
|
||||
check "strict-dedup.md 存在" "test -f '$SKILL_DIR/reference/03-dedup-engine/strict-dedup.md'"
|
||||
check "fuzzy-dedup.md 存在" "test -f '$SKILL_DIR/reference/03-dedup-engine/fuzzy-dedup.md'"
|
||||
check "transfer-link.md 存在" "test -f '$SKILL_DIR/reference/03-dedup-engine/transfer-link.md'"
|
||||
check "fingerprint.md 存在" "test -f '$SKILL_DIR/reference/03-dedup-engine/fingerprint.md'"
|
||||
check "rule-conditions.md 存在" "test -f '$SKILL_DIR/reference/04-rule-engine/rule-conditions.md'"
|
||||
check "rule-execution.md 存在" "test -f '$SKILL_DIR/reference/04-rule-engine/rule-execution.md'"
|
||||
check "db-schema.md 存在" "test -f '$SKILL_DIR/reference/05-database/db-schema.md'"
|
||||
check "indexes.md 存在" "test -f '$SKILL_DIR/reference/05-database/indexes.md'"
|
||||
check "api-catalog.md 存在" "test -f '$SKILL_DIR/reference/06-api-design/api-catalog.md'"
|
||||
check "response-format.md 存在" "test -f '$SKILL_DIR/reference/06-api-design/response-format.md'"
|
||||
check "firefly-mapping.md 存在" "test -f '$SKILL_DIR/reference/07-export-engine/firefly-mapping.md'"
|
||||
check "import-validation.md 存在" "test -f '$SKILL_DIR/reference/07-export-engine/import-validation.md'"
|
||||
|
||||
echo ""
|
||||
echo "=== 结果: $PASS PASS / $FAIL FAIL ==="
|
||||
[ $FAIL -eq 0 ] && exit 0 || exit 1
|
||||
Reference in New Issue
Block a user