YNotes

Notes on tech,life,etc.

11 December 2025

JAVA生态会计规则引擎模型

by YNotes.cc

设计一套工业级的会计规则引擎 (Accounting Rule Engine, ARE),其核心目标是实现 “业务语言”到“会计语言”的自动化翻译

这套引擎必须具备高配置性(业务变动不改代码)、高扩展性(支持多准则)和可追溯性

以下是基于 Java 生态的详细设计方案,包含数据模型设计核心组件实现以及配置示例


1. 核心概念模型 (Core Concepts)

在设计代码之前,必须先定义“会计元数据”:

  1. 场景 (Scenario):定义“发生了什么”。由 EventCode (事件码) + ProductCode (产品码) 唯一确定。
  2. 分录规则 (Entry Rule):定义“怎么记账”。一个场景对应一组分录规则(通常至少一借一贷)。
  3. 维度 (Dimension/Segment):会计科目的组成部分(如:机构号-科目号-币种-客户类)。
  4. 表达式 (Expression):用于动态计算金额或动态决定科目号的脚本。

2. 数据库/配置模型设计 (Schema Design)

我们需要将规则存入数据库。

表 1: 会计场景定义 (ARE_SCENARIO_DEF)

字段 类型 说明 示例
ID Long 主键 1
SCENE_CODE String 业务场景代码 LOAN_DISBURSEMENT (贷款放款)
PRODUCT_TYPE String 产品类型 PERSONAL_LOAN
GAAP_TYPE String 会计准则 LOCAL (本地准则) / IFRS9
STATUS String 状态 ACTIVE

表 2: 分录生成规则 (ARE_ENTRY_RULE)

这是核心表,定义一个场景下产生哪些分录。

字段 类型 说明 示例值 (Expression)
ID Long 主键 101
SCENE_ID Long 外键关联场景 1
SEQ_NO Int 排序号 1
DC_FLAG String 借贷方向 D (Debit) / C (Credit)
GL_ACCOUNT_EXPR String 科目解析表达式 '1011.' + #event.branchId (拼接机构号)
AMOUNT_EXPR String 金额计算表达式 #event.amount (直接取交易额)
CONDITION_EXPR String 生效条件表达式 #event.currency == 'USD' (仅美元生效)
DESC_TEMPLATE String 摘要模板 '放款给客户: ' + #event.customerName

3. 核心引擎实现 (Java Implementation)

为了支持动态解析,我们引入轻量级脚本引擎。这里推荐使用 SpEL (Spring Expression Language)Aviator(高性能,阿里开源)。

3.1 引入依赖

<!-- 高性能表达式引擎 -->
<dependency>
    <groupId>com.googlecode.aviator</groupId>
    <artifactId>aviator</artifactId>
    <version>5.3.3</version>
</dependency>

3.2 定义输入输出对象

// 输入:业务事实 (Context)
@Data
public class BusinessFact {
    private String eventCode;       // 事件: LOAN_REPAY
    private String productCode;     // 产品: S101
    private BigDecimal amount;      // 交易金额
    private String currency;        // 币种
    private String branchId;        // 机构号
    private Map<String, Object> ext;// 扩展参数 (利息、罚息、税费等)
}

// 输出:会计凭证
@Data
public class AccountingVoucher {
    private String voucherId;
    private List<VoucherEntry> entries = new ArrayList<>();
    
    // 借贷平衡校验
    public boolean isBalanced() { ... }
}

@Data
@Builder
public class VoucherEntry {
    private String glAccount;       // 解析后的完整科目号
    private String dcDirection;     // D or C
    private BigDecimal amount;      // 计算后的金额
    private String narrative;       // 摘要
}

3.3 规则解析器 (RuleParser)

这是引擎的心脏,负责执行数据库里存的那串字符串表达式。

@Component
public class RuleExpressionEngine {

    /**
     * 解析科目表达式
     * 示例表达式: "2001." + branchId + "." + currency
     */
    public String evaluateAccount(String expression, BusinessFact fact) {
        Map<String, Object> env = BeanUtil.beanToMap(fact); // 将对象转为 Map
        // 加上扩展字段
        if (fact.getExt() != null) env.putAll(fact.getExt());
        
        // 使用 Aviator 执行
        return (String) AviatorEvaluator.execute(expression, env);
    }

    /**
     * 解析金额表达式
     * 示例表达式: amount * 0.1 (比如计算税费)
     */
    public BigDecimal evaluateAmount(String expression, BusinessFact fact) {
        Map<String, Object> env = BeanUtil.beanToMap(fact);
        if (fact.getExt() != null) env.putAll(fact.getExt());
        
        return new BigDecimal(AviatorEvaluator.execute(expression, env).toString());
    }
    
    /**
     * 解析条件
     * 示例表达式: ext.isVip == true
     */
    public boolean evaluateCondition(String expression, BusinessFact fact) {
        if (StringUtils.isBlank(expression)) return true; // 无条件则默认生效
        
        Map<String, Object> env = BeanUtil.beanToMap(fact);
        if (fact.getExt() != null) env.putAll(fact.getExt());
        
        return (Boolean) AviatorEvaluator.execute(expression, env);
    }
}

3.4 核心门面:AccountingEngine

@Service
public class AccountingRuleEngine {

    @Autowired
    private RuleRepository ruleRepo; // 数据库访问
    
    @Autowired
    private RuleExpressionEngine expressionEngine;

    /**
     * 核心接口:翻译
     */
    public AccountingVoucher translate(BusinessFact fact) {
        // 1. 查找规则集 (通常带缓存)
        List<AreEntryRule> rules = ruleRepo.findRules(fact.getEventCode(), fact.getProductCode());
        
        if (CollectionUtils.isEmpty(rules)) {
            throw new AccountingException("No accounting rules found for: " + fact.getEventCode());
        }

        AccountingVoucher voucher = new AccountingVoucher();
        voucher.setVoucherId(UUID.randomUUID().toString());

        // 2. 遍历每一条规则,生成分录
        for (AreEntryRule rule : rules) {
            // 2.1 检查生效条件 (Condition Check)
            if (!expressionEngine.evaluateCondition(rule.getConditionExpr(), fact)) {
                continue; // 条件不满足,跳过此分录
            }

            // 2.2 动态计算金额
            BigDecimal entryAmount = expressionEngine.evaluateAmount(rule.getAmountExpr(), fact);
            // 金额为0通常不记账
            if (entryAmount.compareTo(BigDecimal.ZERO) == 0) continue;

            // 2.3 动态解析科目
            String glAccount = expressionEngine.evaluateAccount(rule.getGlAccountExpr(), fact);

            // 2.4 生成分录行
            VoucherEntry entry = VoucherEntry.builder()
                .dcDirection(rule.getDcFlag())
                .glAccount(glAccount)
                .amount(entryAmount)
                .narrative(expressionEngine.evaluateAccount(rule.getDescTemplate(), fact)) // 复用解析String的方法解析摘要
                .build();
            
            voucher.getEntries().add(entry);
        }

        // 3. 借贷平衡自检 (Self-Validation)
        if (!voucher.isBalanced()) {
             // 处理舍入误差或抛出配置错误
             handleUnbalancedVoucher(voucher);
        }

        return voucher;
    }
}

4. 实战配置示例 (Configuration Example)

让我们来看一个复杂的业务场景:贷款还款 (Loan Repayment)。 假设用户还了 1100 元,其中 1000 元是本金,100 元是利息。

输入数据 (BusinessFact):

{
  "eventCode": "LOAN_REPAY",
  "productCode": "MORTGAGE_001",
  "branchId": "0755",
  "ext": {
    "principalAmt": 1000.00,
    "interestAmt": 100.00,
    "penaltyAmt": 0.00
  }
}

数据库配置 (ARE_ENTRY_RULE):

序号 借/贷 描述 科目表达式 (GL_ACCOUNT_EXPR) 金额表达式 (AMOUNT_EXPR) 条件 (CONDITION_EXPR)
1 扣客户款 '2001.01.' + branchId (活期科目) principalAmt + interestAmt true
2 销贷款本金 '1301.01.' + branchId (贷款科目) principalAmt principalAmt > 0
3 记利息收入 '5001.01.0000' (利息收入科目) interestAmt interestAmt > 0

引擎执行结果 (AccountingVoucher):

  1. Dr 2001.01.0755 1100.00 (本金+利息)
  2. Cr 1301.01.0755 1000.00 (本金)
  3. Cr 5001.01.0000 100.00 (利息)

结果:借 1100 = 贷 1000 + 100。平衡。


5. 进阶设计点 (Advanced Considerations)

一套优秀的规则引擎还需要考虑以下问题:

A. 多维度科目映射 (Mapping Tables)

直接在表达式里写科目号(如 '2001.01.')还是太硬编码了。

B. 倒起息与红字冲正 (Back-valuation & Reversal)

C. 多准则 (Multi-GAAP)

D. 性能优化 (Caching)

总结

这套设计将Java 代码从繁琐的 if (isLoan) { ... } else if (isDeposit) { ... } 中解放了出来。

这就是现代银行核心系统中 “会计中台” 的雏形。

tags: