乐于分享
好东西不私藏

第93天:架构篇:会计分录模板引擎——交易码动态解析为分录

第93天:架构篇:会计分录模板引擎——交易码动态解析为分录
别再把会计分录写在代码里了!一个配置表搞定所有记账规则
365天,从零手写银行核心系统——第93天
前两天我们聊了记账中心的攒批写入,把TPS从2000干到了8000+。
今天聊一个更“聪明”的设计——会计分录模板引擎。
先讲一个真实的故事。
小张接了一个任务:给银行核心系统加一个“新存款产品”。他打开代码,找到了记账逻辑——一个巨大的switch-case,里面有几十种交易类型的记账规则。
存款走一套,取款走一套,转账走一套,理财走一套……
小张找了半天,才找到存款的记账逻辑在哪里。他复制粘贴、改了几个字段、提交代码、走发布流程。三天后上线。
两个月后,又有新需求——加一个“结构性存款”。小张又打开代码,又改了一遍……
每加一个产品、每改一个规则,都要改代码、走发布、重启系统。
如果有一天,业务人员能自己在后台配一下规则,不用等开发排期、不用走发布流程,是不是就爽了?
会计分录模板引擎,就是干这个的。

【一、传统做法vs 模板引擎】

传统做法(硬编码):
java
public void generateVoucher(Transaction tx) {
if (tx.getType().equals("DEPOSIT")) {
// 借:库存现金,贷:吸收存款
voucherService.debit("库存现金", tx.getAmount());
voucherService.credit("吸收存款", tx.getAmount());
} else if (tx.getType().equals("WITHDRAW")) {
// 借:吸收存款,贷:库存现金
voucherService.debit("吸收存款", tx.getAmount());
voucherService.credit("库存现金", tx.getAmount());
}
// ... 又臭又长的几十个if-else
}
模板引擎做法(配置化):
把“交易类型应该生成什么分录”存在数据库里,引擎根据交易码动态查表、生成分录。
sql
-- 分录规则配置表
CREATE TABLE voucher_rule (
id BIGINT PRIMARY KEY,
txn_code VARCHAR(20) NOT NULL,        -- 交易码
direction VARCHAR(10) NOT NULL,        -- DEBIT / CREDIT
subject_expression VARCHAR(100) NOT NULL, -- 科目表达式
amount_source VARCHAR(50),             -- 金额来源
priority INT DEFAULT 0
);
配置数据长这样:
| txn_code | direction | subject_expression | amount_source |
|----------|-----------|-------------------|---------------|
| DEP001 | DEBIT | 库存现金 | amount |
| DEP001 | CREDIT | 吸收存款 | amount |
| WTH001 | DEBIT | 吸收存款 | amount |
| WTH001 | CREDIT | 库存现金 | amount |
加一个新交易,只需要INSERT两条记录。不写代码,不重启系统。
这就是会计分录模板引擎的核心思想——规则存在库里,引擎负责执行。
一个标准的会计分录,必须包含几个核心要素:会计科目、记账方向(借/贷)、金额,以及隐含的记账日期和业务摘要。模板引擎就是把这些要素“参数化”——把“写死”变成“配置”。

【二、模板引擎长什么样?】

一个完整的会计分录模板引擎,通常包含三层:
第一层:交易码识别
交易系统发来的记账指令里,带着txnCode(交易码)。引擎根据这个码去查规则表。
第二层:规则匹配
根据交易码、产品类型、币种、机构等维度,匹配最合适的分录规则。
第三层:分录生成
根据规则配置,动态生成一条或多条会计分录。
java
public List generate(BookRequest request) {
String txnCode = request.getTxnCode();
// 1. 查询规则
List rules = ruleMapper.findByTxnCode(txnCode);
if (rules.isEmpty()) {
throw new BusinessException("未配置分录规则");
}
// 2. 解析金额
BigDecimal amount = resolveAmount(request, rules);
// 3. 解析科目(支持变量替换)
String subject = resolveSubject(rules, request);
// 4. 生成分录
List details = new ArrayList<>();
for (VoucherRule rule : rules) {
VoucherDetail detail = new VoucherDetail();
detail.setDirection(rule.getDirection());
detail.setSubjectCode(resolveSubject(rule.getSubjectExpression(), request));
detail.setAmount(amount);
details.add(detail);
}
return details;
}

【三、为什么“模板化”比“硬编码”好?】

好处一:业务人员自己配,不用等开发
新增一个交易类型,业务人员在后台点几下,配好借什么、贷什么,立马生效。不用写代码、不用发版、不用重启。
好处二:规则变更零停机
会计科目调整,直接在配置表里修改,系统实时生效。不用等下一个发版窗口。
好处三:一个引擎服务所有业务
存款、贷款、支付、理财……所有业务用同一套模板引擎。不用每个模块各写一套记账逻辑。
好处四:审计可追溯
所有规则变更都有记录——谁、什么时候、改了什么。审计的时候一清二楚。

【四、模板引擎的“进阶玩法”】

玩法一:科目表达式
科目不是写死的,可以带变量:
sql
subject_expression = '210101_{currency}_{branch}'
引擎根据交易数据动态替换{currency}和{branch},生成210101_CNY_BJ001。
玩法二:多套规则并行
同一个交易码,可以配置多套规则,通过优先级或条件判断选择。比如:大额和小额走不同的科目。
玩法三:规则版本管理
每次修改规则,不覆盖旧记录,而是新增一条带生效日期的记录。引擎根据交易日期自动选择正确的规则版本。

【五、模板引擎的“坑”】

坑一:规则配置错了,影响面很大
硬编码错了,只影响一个交易。模板配置错了,可能影响所有用这个模板的交易。
解决方案:规则变更走审批流程,测试环境验证通过后才能上线。
坑二:过度配置化
什么规则都往配置表里塞,导致配置表越来越复杂,维护成本飙升。
解决方案:核心规则配置化,复杂逻辑仍用代码实现。配置化是手段,不是目的。
坑三:性能问题
每次记账都查数据库,可能成为性能瓶颈。
解决方案:规则配置加载到本地缓存(Caffeine),配置变更时刷新缓存。

【六、今日小总结】

| 对比项 | 硬编码 | 模板引擎 |
|--------|--------|----------|
| 新增交易 | 改代码、发版、重启 | 配数据、即刻生效 |
| 规则变更 | 改代码、发版、重启 | 改配置、即刻生效 |
| 开发效率 | 低(每次都要写代码) | 高(配置化,一次开发长期复用) |
| 业务参与度 | 低(只能等开发) | 高(可自行配置) |
| 审计追溯 | 难(代码里找) | 易(配置表有记录) |
会计分录模板引擎的核心思想:把“代码”变成“配置”,把“硬编码”变成“参数化”。业务人员配规则,引擎执行规则。各行其是,各得其所。
明天我们继续:记账中心的异常处理——挂账与人工调账。
今日互动:你写过的系统里,有没有把业务规则配置化的实践?评论区分享一下经验。
明日预告:记账中心的异常处理——挂账与人工调账。