Files
ledgerly-docs/dev/bill-parsing.md
T

68 lines
4.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 账单解析
账单导入采用「**解析预览 → 确认导入**」两阶段流程,解析阶段不落库,用户确认后才写入。
## 解析器设计
解析器采用策略模式:`BillParser` 接口声明 `getBillType()` 与 `parse(byte[])`,各实现类对应一种账单类型。服务层构造时用 `EnumMap<BillType, BillParser>` 注册所有实现,按类型分发,便于扩展新来源。
| 解析器 | 格式 | 表头锚点 | 去重键来源 |
|---|---|---|---|
| `AlipayBillParser` | CSV(UTF-8/GBK) | 「交易号」 | 交易号 |
| `WechatBillParser` | xlsx(POI) | 「交易时间」 | 交易单号 |
| `MeituanBillParser` | CSV | 「交易创建时间」 | 交易单号 |
| `TaobaoBillParser` | xlsx(POI) | 「订单号」 | 订单号(按订单号聚合多商品行) |
| `JdBillParser` | CSV(UTF-8/GBK) | 「交易时间」 | 交易订单号 |
| `CmbBillParser` | CSV | 「交易日期」 | 生成 `CMB_日期_类型_金额_余额` |
| `CmbPdfParser` | PDF(PDFBox) | 「记账日期」 | 同上(与 CSV 互通) |
| `BocomBillParser` | xls(POI) | 「记账日期」 | 生成 `BOCOM_日期_时分秒_类型_金额` |
各解析器统一输出 `ParsedBillRow`:行号、`externalNo`、交易时间、类型、金额、交易对方、商品、交易状态、原始收/支方向、是否计入统计、是否可导入、是否重复、命中规则与建议分类/标签、`rawData`(保留原始列的 JSON)、错误信息。
### 解析要点
- **编码识别**:CSV 自动识别 UTF-8 / UTF-8 BOM / GBK。
- **表头定位**:以特定表头文字定位数据区,忽略元信息与统计行。
- **银行 PDF**:按字符坐标切列、按纵坐标合并折行还原表格;用带符号金额判断收支;按「联机余额」做连续性校验(误差 0.005),不连续的行标记为不可导入。
- **时间兼容**:微信账单兼容 Excel 日期序列号与文本时间;支付宝在付款时间缺失时回退交易创建时间;淘宝订单同样兼容文本时间与 Excel 日期序列号。
- **订单聚合**:淘宝订单数据中多商品订单的订单级列(订单号、提交时间、状态、店铺、实付金额、运费)以合并单元格呈现,仅首个商品行有值;解析器按订单号分组聚合,每个订单输出一条记录,金额优先取订单级实付金额,为空时回退「Σ(商品金额×数量)+运费」。
- **不计收支处理**:京东流水的「不计收支」行(白条还款、小金库转入、全额退款等)标记为可导入但**不计入统计且默认不勾选**,类型按「交易说明含『退款』→ INCOME,否则 EXPENSE」推断。
- **计入统计默认值**:解析完成后按**账单来源**应用「计入统计」默认值(配置于系统设置,见 [动态数据源](/dev/database) 同级的应用配置)。采用「解析器判定 && 来源默认」语义——来源默认不计入时整批置为不计入;来源默认计入时保留解析器对「不计收支/中性」行的排除。确认导入时若请求未显式提供该字段,同样以来源默认兜底。
## 压缩包解密
`BillArchiveExtractor` 处理 zip 压缩包:
- 按魔数 `PK\x03\x04` 判断是否为 zip,并区分「xlsx 本身是 zip」与「账单压缩包」。
- 支持 ZipCrypto 与 AES 加密;未提供密码返回错误码 `4001`,密码错误返回 `4002`。
- 限制条目数与总大小,防止 zip bomb;解析完成后清理临时文件。
## 两阶段流程
### parse(不落库)
1. 按账单类型分发到对应解析器;若为 zip 则先解密提取。
2. 批量查询已存在的 `externalNo`,标记重复行并取消勾选。
3. 调用规则引擎批量匹配,生成建议分类与标签(批量查询分类与标签,避免 N+1)。
### confirm(落库,事务)
1. 校验默认分类归属与类型,过滤默认标签。
2. 再次批量运行规则匹配。
3. 逐条校验类型、时间、金额;按 `externalNo` 去重(含本批次内去重)。
4. **规则建议分类优先于面板默认分类**;标签取「默认标签 ∪ 规则标签」并去重。
5. 批量插入(每 500 条一批),批次失败时回退逐条插入;标签关联在批量插入未回填主键时按 `externalNo` 回查 ID。
6. 返回成功 / 跳过 / 失败统计。
## 去重策略
- 统一使用「用户 + `externalNo`」唯一约束 `uk_user_external` 去重,`external_no` 为 NULL 的手动记录不受影响。
- 有交易号的账单直接使用交易号。
- 银行流水无交易号,则用「日期 + 类型 + 金额 + 余额(或时分秒)」生成稳定键,保证同一账单重复导入不会重复;招商银行 CSV 与 PDF 生成规则一致,可混用。
## 相关文档
- [账单类型与获取](/guide/bills/overview)
- [账单导入(使用说明)](/guide/transactions)
- [规则引擎](/dev/rule-engine)