Notes on tech,life,etc.
by Ynotes.cc
“交易与核算分离” (Separation of Transaction and Accounting)。
这种架构下,业务流水就是连接两者的纽带。
运用 观察者模式 (Observer)、适配器模式 (Adapter) 和 解释器模式 (Interpreter) 来实现这套架构。
这是业务层和会计层的契约。它不包含“借”或“贷”,只包含业务事实。
/**
* 业务流水 (事实数据)
* 业务层处理完后,落库并发布此对象
*/
@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;
}
业务层只负责原子层的业务状态变更(比如扣减客户的“可用余额”),而不关心这笔钱在会计上是进了“库存现金”还是“存放央行款项”。
/**
* 业务服务
* 职责:执行业务规则,扣减可用余额,生成业务流水
*/
@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);
// 业务层结束,直接返回成功给客户
}
}
这是架构的核心。它负责将“业务语言”翻译成“会计语言”。我们将使用策略模式或配置化映射来实现。
/**
* 会计分录指令 (翻译结果)
*/
@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) // 贷:客户存款
);
}
}
现在,会计引擎作为一个监听者 (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);
}
}
}
通过这种重构,我们实现了“业务与会计分离”,带来了以下显而易见的工程价值:
DepositService,重新测试整个存款流程。AccountingRuleEngine 的数据库配置里,修改一条 SQL 规则或表达式。DepositService 代码一行都不用动,甚至不需要重启服务。DepositService 只需要扣减可用余额并落库流水,就可以直接返回给用户“交易成功”。bizFlowId,审计人员可以清晰地看到:“这笔存款业务,在会计上到底引发了哪些科目的变动”。BusinessFlowEvent,可以被两个不同的会计消费者监听,分别生成两套不同的账(一套 Local Ledger,一套 IFRS Ledger),互不干扰。这个方案正是 Event Sourcing (事件溯源) 思想在 CBS 中的典型应用。
这才是大型银行核心系统能够支撑亿级并发且保持会计准确性的秘密。
tags: