YNotes

Notes on tech,life,etc.

10 December 2025

交易与核算分离

by Ynotes.cc

“交易与核算分离” (Separation of Transaction and Accounting)

这种架构下,业务流水就是连接两者的纽带。

运用 观察者模式 (Observer)适配器模式 (Adapter)解释器模式 (Interpreter) 来实现这套架构。


1. 定义纽带:业务流水 (The Business Transaction Log)

这是业务层和会计层的契约。它不包含“借”或“贷”,只包含业务事实。

/**
 * 业务流水 (事实数据)
 * 业务层处理完后,落库并发布此对象
 */
@Data
@Builder
public class BusinessFlowEvent {
    private String bizFlowId;       // 业务流水号 (全局唯一)
    private String bizType;         // 业务类型 (如:RETAIL_DEPOSIT)
    private String productCode;     // 产品代码 (决定了会计规则)
    private BigDecimal amount;      // 交易金额
    private String currency;        // 币种
    private String customerAccount; // 客户账号
    private String channelCode;     // 渠道 (网点/App)
    private LocalDateTime transTime;// 发生时间
    
    // 扩展字段 (用于传递特殊上下文)
    private Map<String, Object> metadata;
}

2. 第一阶段:业务处理 (Business Domain)

业务层只负责原子层的业务状态变更(比如扣减客户的“可用余额”),而不关心这笔钱在会计上是进了“库存现金”还是“存放央行款项”。

/**
 * 业务服务
 * 职责:执行业务规则,扣减可用余额,生成业务流水
 */
@Service
public class DepositService {

    @Autowired
    private BusinessFlowRepository flowRepo;
    
    @Autowired
    private EventBus eventBus; // 消息总线 (Kafka / RocketMQ / Spring Event)

    @Transactional
    public void executeDeposit(DepositRequest req) {
        // 1. 业务校验 (黑名单、限额)
        validate(req);

        // 2. 更新“可用余额” (Operational Balance)
        // 注意:这里更新的是面向用户的余额,不是总账余额
        accountManager.increaseAvailableBalance(req.getAccountNo(), req.getAmount());

        // 3. 落业务流水 (Business Log)
        BusinessFlowEvent event = BusinessFlowEvent.builder()
            .bizFlowId(UUID.randomUUID().toString())
            .bizType("RETAIL_DEPOSIT")
            .productCode("SAVING_001")
            .amount(req.getAmount())
            .customerAccount(req.getAccountNo())
            .transTime(LocalDateTime.now())
            .build();
            
        flowRepo.save(event);

        // 4. 抛出事件 -> 触发会计引擎 (异步或同步解耦)
        eventBus.publish("TOPIC_ACCOUNTING", event);
        
        // 业务层结束,直接返回成功给客户
    }
}

3. 中间层:会计规则引擎 (Accounting Rule Engine)

这是架构的核心。它负责将“业务语言”翻译成“会计语言”。我们将使用策略模式配置化映射来实现。

/**
 * 会计分录指令 (翻译结果)
 */
@Data
public class AccountingInstruction {
    private String bizFlowId; // 关联回业务流水
    private List<Entry> entries = new ArrayList<>();

    @Data
    @AllArgsConstructor
    public static class Entry {
        private String glAccount; // 科目号
        private BookingDirection direction; // 借/贷
        private BigDecimal amount;
    }
}

/**
 * 会计规则引擎
 * 核心职责:Input(BusinessFlow) -> Output(AccountingInstruction)
 */
@Component
public class AccountingRuleEngine {

    // 模拟从数据库或配置中心加载规则
    // Key: BizType + ProductCode -> Value: Rule List
    private Map<String, List<RuleConfig>> ruleCache;

    public AccountingInstruction translate(BusinessFlowEvent event) {
        AccountingInstruction instruction = new AccountingInstruction();
        instruction.setBizFlowId(event.getBizFlowId());

        // 1. 根据业务类型查找规则
        // 比如 RETAIL_DEPOSIT (零售存款) -> 规则模板 ID: 1001
        List<RuleConfig> rules = findRules(event.getBizType(), event.getProductCode());

        // 2. 解析规则并生成分录
        for (RuleConfig rule : rules) {
            // 动态解析科目 (可能基于机构、币种变化)
            String glCode = resolveGLAccount(rule.getGlExpression(), event);
            
            instruction.getEntries().add(new AccountingInstruction.Entry(
                glCode,
                rule.getDirection(),
                event.getAmount()
            ));
        }

        return instruction;
    }

    // 模拟规则查找
    private List<RuleConfig> findRules(String bizType, String productCode) {
        // 实际逻辑:Select * from accounting_rule_def where ...
        // 示例:存款规则
        return Arrays.asList(
            new RuleConfig("DR", "CASH_GL_CODE", BookingDirection.DEBIT),  // 借:现金
            new RuleConfig("CR", "CUST_LIABILITY_GL", BookingDirection.CREDIT) // 贷:客户存款
        );
    }
}

4. 第二阶段:会计引擎 (The Accounting Engine)

现在,会计引擎作为一个监听者 (Listener) 存在。它不关心业务是怎么发生的,它只负责记账。

/**
 * 会计消费者 / 处理器
 * 职责:消费业务事件 -> 翻译 -> 记总账
 */
@Component
public class AccountingConsumer {

    @Autowired
    private AccountingRuleEngine ruleEngine;
    
    @Autowired
    private GeneralLedgerService ledgerService;

    // 监听业务流水事件
    @EventListener // 或者 @KafkaListener
    public void onBusinessTransaction(BusinessFlowEvent event) {
        try {
            // 1. 翻译:将业务流转换成会计分录
            AccountingInstruction instruction = ruleEngine.translate(event);

            // 2. 记账:调用核心记账底座            
            ledgerService.post(instruction);

        } catch (Exception e) {
            // 严重异常:业务做成功了,但账记不下来
            // 策略:落入 "错账队列 (Error Queue)",人工介入或日终自动重试
            log.error("Accounting failed for bizFlowId: " + event.getBizFlowId(), e);
            saveToErrorQueue(event, e);
        }
    }
}

5. 这种设计的巨大优势

通过这种重构,我们实现了“业务与会计分离”,带来了以下显而易见的工程价值:

A. 灵活性 (Flexibility)

B. 性能与解耦 (Performance)

C. 可追溯与审计 (Auditability)

D. 支持多准则核算 (Multi-GAAP)

总结

这个方案正是 Event Sourcing (事件溯源) 思想在 CBS 中的典型应用。

  1. 业务层 = Event Producer (生产业务事实)。
  2. 会计层 = Event Consumer (解释业务事实)。
  3. 规则引擎 = Translator (语言翻译)。

这才是大型银行核心系统能够支撑亿级并发且保持会计准确性的秘密。

tags: