新增支持账单
本文面向开发者,说明如何为 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. 接入 handlers | services/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_time | str | YYYY-MM-DD HH:MM:SS 格式 |
transaction_category | str | 交易分类 / 摘要 |
counterparty | str | 交易对方名称 |
commodity | str | 商品描述(映射关键字会在此字段中匹配) |
transaction_type | str | 收入 / 支出 / / / 不计收支 |
amount | str / float | 金额(正数) |
payment_method | str | 支付渠道(用于资产映射的 key 匹配) |
transaction_status | str | 原始交易状态 |
notes | str | 备注信息 |
bill_identifier | str | 账单类型标识常量 |
uuid | str | 交易唯一标识(可为空) |
银行类账单可额外包含 balance(余额)、card_number(对方卡号)、counterparty_bank(对方银行)等字段。
四、测试与验证
- 准备一份该账单的真实导出样本,放入
project/fixtures/sample_files/ - 确保上传后系统能正确识别(
identifier()返回True) - 检查初始化后的字段字典是否符合上表契约
- 运行完整解析流程,验证映射匹配和格式化输出
- 运行
pytest确保不影响已有账单类型的解析
五、现有实现参考
| 账单类型 | 初始化策略 | 业务逻辑 | 忽略规则 |
|---|---|---|---|
| 支付宝 | alipay_init_strategy.py | AliPay.py | alipay_rule.py |
| 微信 | wechat_init_strategy.py | WeChat.py | wechat_rule.py |
| 中国银行借记卡 | boc_debit_init_strategy.py | BOC_Debit.py | boc_debit_rule.py |
| 招商银行信用卡 | cmb_credit_init_strategy.py | CMB_Credit.py | cmb_credit_rule.py |
| 工商银行借记卡 | icbc_debit_init_strategy.py | ICBC_Debit.py | — |
| 建设银行借记卡 | ccb_debit_init_strategy.py | CCB_Debit.py | — |
建议以结构最简单的银行类实现(如建设银行 CCB_Debit)作为参考起点。