跳到主要内容

新增支持账单

本文面向开发者,说明如何为 Beancount-Trans 添加一种新的账单来源(如某银行借记卡、信用卡等)。整个过程涉及后端 translate 模块中的 4 个位置,遵循「每种账单独立一个模块」的边界原则。

一、总览:需要改动什么

以下目录均相对于 Beancount-Trans-Backend/project/apps/translate/

步骤文件位置职责
1. 定义标识常量utils.py新增 BILL_XXX = "xxx" 常量
2. 初始化策略services/init/strategies/xxx_init_strategy.py读取 CSV 行 → 输出统一字段字典列表
3. 注册到工厂services/init/bill_init_factory.py将策略类注册到 InitFactory
4. 账单业务逻辑views/XXX.py字段提取函数(金额、备注、状态、UUID 等)
5. 接入 handlersservices/handlers.py在各 Handler 中增加 BILL_XXX 分支
6. 忽略规则(可选)services/parse/ignore_rules/xxx_rule.py预过滤 / 后过滤规则

代码模板文件 views/AAA_Template.py 提供了所有需要实现的函数签名,建议以它为起点。

二、逐步说明

1. 定义标识常量

utils.py 中新增一行:

BILL_XXX = "xxx"

这个字符串会写入每条解析记录的 bill_identifier 字段,贯穿整条管线。

2. 创建初始化策略

services/init/strategies/ 下新建文件,继承 InitStrategy 基类:

from project.apps.translate.services.init.strategies.base_bill_init_strategy import InitStrategy

class XXXInitStrategy(InitStrategy):
HEADER_MARKER = "能唯一标识该账单类型的首行文本"

def init(self, bill, **kwargs):
"""将原始 CSV 行转换为统一字段字典列表"""
records = []
for row in bill:
record = {
'transaction_time': '', # 格式:YYYY-MM-DD HH:MM:SS
'transaction_category': '', # 交易类型/摘要
'counterparty': '', # 交易对方
'commodity': '', # 商品描述
'transaction_type': '', # 收入 / 支出 / 不计收支
'amount': '', # 金额(正数字符串)
'payment_method': '', # 支付方式(用于资产映射匹配)
'transaction_status': '', # 交易状态
'notes': '', # 备注
'bill_identifier': 'xxx', # 与 utils.py 中的常量一致
'uuid': '', # 唯一标识(无则留空,管线会生成哈希)
}
records.append(record)
return records

@classmethod
def identifier(cls, first_line):
return cls.HEADER_MARKER in first_line

关键identifier() 方法决定了系统如何自动识别这种账单——它检查文件首行是否包含特征文本。

3. 注册到工厂

services/init/bill_init_factory.py 中添加导入和注册:

from ...strategies.xxx_init_strategy import XXXInitStrategy

InitFactory.register_strategy(XXXInitStrategy)

注册后,系统上传文件时会自动尝试匹配该策略。

4. 实现账单业务逻辑

views/ 下新建 XXX.py,实现字段提取函数。参考 AAA_Template.py 中的函数签名:

  • xxx_get_uuid(data) — 返回唯一标识
  • xxx_get_status(data) — 返回交易状态字符串
  • xxx_get_amount(data) — 返回格式化金额(如 "123.45"
  • xxx_get_note(data) — 返回备注
  • xxx_init_key(data) — 返回资产映射匹配键(如 "XX银行储蓄卡(1234)"
  • xxx_get_account(self, ownerid) — 根据资产映射确定资产账户
  • xxx_get_expense(self, ownerid) — 处理支出/收入分类逻辑(可选,仅银行类账单需要)

如果该账单来源是 PDF 或 Excel 格式,还需实现格式转换函数(参考 BOC_Debit.py 中的 boc_debit_pdf_convert_to_string)。

5. 接入 handlers

services/handlers.py 中,为新账单类型增加分支调用:

  • AccountHandler.initialize_key() — 添加 elif self.bill == BILL_XXX: 分支
  • AccountHandler.get_account() — 添加账户判定分支
  • ExpenseHandler.get_expense() — 如果该账单的收支判定有特殊逻辑
  • get_status()get_note()get_amount() 等全局函数 — 在对应的 handlers 字典中注册

handlers 只做薄分支调用,具体逻辑实现在 views/XXX.py 中。

6. 添加忽略规则(可选)

如果该账单有需要自动过滤的交易状态,在 services/parse/ignore_rules/ 下新建规则文件:

from project.apps.translate.services.parse.ignore_registry import registry
from project.apps.translate.utils import BILL_XXX

def xxx_pre_filter(row, args):
"""返回 True 表示忽略该条记录"""
return row['transaction_status'] in ["退款", "已撤销"]

registry.register_pre_filter(BILL_XXX, xxx_pre_filter)

并确保该文件在 services/parse/__init__.py 中被导入,以触发注册。

三、统一字段说明

所有账单类型在初始化阶段都必须输出以下字段,这是管线后续步骤的契约:

字段类型说明
transaction_timestrYYYY-MM-DD HH:MM:SS 格式
transaction_categorystr交易分类 / 摘要
counterpartystr交易对方名称
commoditystr商品描述(映射关键字会在此字段中匹配)
transaction_typestr收入 / 支出 / / / 不计收支
amountstr / float金额(正数)
payment_methodstr支付渠道(用于资产映射的 key 匹配)
transaction_statusstr原始交易状态
notesstr备注信息
bill_identifierstr账单类型标识常量
uuidstr交易唯一标识(可为空)

银行类账单可额外包含 balance(余额)、card_number(对方卡号)、counterparty_bank(对方银行)等字段。

四、测试与验证

  1. 准备一份该账单的真实导出样本,放入 project/fixtures/sample_files/
  2. 确保上传后系统能正确识别(identifier() 返回 True
  3. 检查初始化后的字段字典是否符合上表契约
  4. 运行完整解析流程,验证映射匹配和格式化输出
  5. 运行 pytest 确保不影响已有账单类型的解析

五、现有实现参考

账单类型初始化策略业务逻辑忽略规则
支付宝alipay_init_strategy.pyAliPay.pyalipay_rule.py
微信wechat_init_strategy.pyWeChat.pywechat_rule.py
中国银行借记卡boc_debit_init_strategy.pyBOC_Debit.pyboc_debit_rule.py
招商银行信用卡cmb_credit_init_strategy.pyCMB_Credit.pycmb_credit_rule.py
工商银行借记卡icbc_debit_init_strategy.pyICBC_Debit.py
建设银行借记卡ccb_debit_init_strategy.pyCCB_Debit.py

建议以结构最简单的银行类实现(如建设银行 CCB_Debit)作为参考起点。