钱包业务背景
一般具有支付、购买功能的应用,都支持钱包的功能。应用为每个用户开设一个系统内的虚拟钱包账户,支持用户重置、提现、支付、冻结、透支、转账、查询账户余额、查询交易流水。一般来讲,每个虚拟钱包账户都会对应用户的一个真实的支付账号,有可能是银行卡账户,也有可能是三方支付账户(支付宝、微信钱包)。
钱包的五大核心功能的业务
- 充值
用户通过三方支付渠道,把自己银行卡账户内的钱,充值到虚拟钱包账户。
流程:- 第一是从用户的银行卡账户转账到应用的公共银行卡账户
- 第二是将用户的充值金额加到虚拟钱包余额上
- 第三是记录刚刚这笔交易流水
- 支付
用户用钱包内的余额,支付购买应用内的商品,支付的过程就是个转账的过程。
流程:- 第一 从用户的虚拟钱包账户划钱到商家的虚拟钱包账户上
- 第二 触发真正的银行转账操作,从应用的公共银行账户转钱到商家的银行账户(注意,这儿并不是从用户的银行账户转到商家的银行账户)
- 第三 记录这笔支付的交易流水
- 提现
用户可以将虚拟钱包中的余额,提现到自己的银行卡中。
流程:- 第一 从用户的虚拟钱包账户金额减去提现的金额
- 第二 触发银行转账操作,从应用的公共银行卡赚钱到用户的银行卡
- 第三 记录这笔支付的交易流水
- 查询余额
用户查询虚拟钱包的余额数字即可 - 查询交易流水
用户根据条件查询之前记录的交易即可
钱包系统的设计思路
根据上面的业务流程,我们可以把钱包系统的业务 划分为两部分
- 一部分单纯的跟应用内的虚拟钱包账户打交道
- 一部分单纯的跟银行账户打交道
基于这样的业务划分,给系统解耦,将整个钱包系统拆分为两个子系统:虚拟钱包系统和三方支付系统
下面代码只聚焦于虚拟钱包系统的设计和实现。
- 基于贫血模型的传统开发模式
public class VirtualWalletController { // 通过构造函数或者 IOC 框架注入 private VirtualWalletService virtualWalletService; public BigDecimal getBalance(Long walletId) { ... } // 查询余额 public void debit(Long walletId, BigDecimal amount) { ... } // 出账 public void credit(Long walletId, BigDecimal amount) { ... } // 入账 public void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) { ...} // 转账 } public class VirtualWalletBo {// 省略 getter/setter/constructor 方法 private Long id; private Long createTime; private BigDecimal balance; } public class VirtualWalletService { // 通过构造函数或者 IOC 框架注入 private VirtualWalletRepository walletRepo; private VirtualWalletTransactionRepository transactionRepo; public VirtualWalletBo getVirtualWallet(Long walletId) { VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId); VirtualWalletBo walletBo = convert(walletEntity); return walletBo; } public BigDecimal getBalance(Long walletId) { return virtualWalletRepo.getBalance(walletId); } public void debit(Long walletId, BigDecimal amount) { VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId); BigDecimal balance = walletEntity.getBalance(); if (balance.compareTo(amount) < 0) { throw new NoSufficientBalanceException(...); } walletRepo.updateBalance(walletId, balance.subtract(amount)); } public void credit(Long walletId, BigDecimal amount) { VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId); BigDecimal balance = walletEntity.getBalance(); walletRepo.updateBalance(walletId, balance.add(amount)); } public void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) { VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity(); transactionEntity.setAmount(amount); transactionEntity.setCreateTime(System.currentTimeMillis()); transactionEntity.setFromWalletId(fromWalletId); transactionEntity.setToWalletId(toWalletId); transactionEntity.setStatus(Status.TO_BE_EXECUTED); Long transactionId = transactionRepo.saveTransaction(transactionEntity); try { debit(fromWalletId, amount); credit(toWalletId, amount); } catch (InsufficientBalanceException e) { transactionRepo.updateStatus(transactionId, Status.CLOSED); ...rethrow exception e... } catch (Exception e) { transactionRepo.updateStatus(transactionId, Status.FAILED); ...rethrow exception e... } transactionRepo.updateStatus(transactionId, Status.EXECUTED); } }
- 基于充血模型的DDD开发模式
这种充血模型的设计思路,看起来领域模型VitualWallet类很单薄,包含的业务逻辑很简单,这种思路貌似没太大优势。public class VirtualWallet { // Domain 领域模型 (充血模型) private Long id; private Long createTime = System.currentTimeMillis();; private BigDecimal balance = BigDecimal.ZERO; public VirtualWallet(Long preAllocatedId) { this.id = preAllocatedId; } public BigDecimal balance() { return this.balance; } public void debit(BigDecimal amount) { if (this.balance.compareTo(amount) < 0) { throw new InsufficientBalanceException(...); } this.balance.subtract(amount); } public void credit(BigDecimal amount) { if (amount.compareTo(BigDecimal.ZERO) < 0) { throw new InvalidAmountException(...); } this.balance.add(amount); } } public class VirtualWalletService { // 通过构造函数或者 IOC 框架注入 private VirtualWalletRepository walletRepo; private VirtualWalletTransactionRepository transactionRepo; public VirtualWallet getVirtualWallet(Long walletId) { VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId); VirtualWallet wallet = convert(walletEntity); return wallet; } public BigDecimal getBalance(Long walletId) { return virtualWalletRepo.getBalance(walletId); } public void debit(Long walletId, BigDecimal amount) { VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId); VirtualWallet wallet = convert(walletEntity); wallet.debit(amount); walletRepo.updateBalance(walletId, wallet.balance()); } public void credit(Long walletId, BigDecimal amount) { VirtualWalletEntity walletEntity = walletRepo.getWalletEntity(walletId); VirtualWallet wallet = convert(walletEntity); wallet.credit(amount); walletRepo.updateBalance(walletId, wallet.balance()); } public void transfer(Long fromWalletId, Long toWalletId, BigDecimal amount) { //... 跟基于贫血模型的传统开发模式的代码一样... } }
这也是大部分业务系统都使用基于贫血模型开发的原因。
不过当虚拟钱包需要支持更复杂的业务的时候,它的优势就会显现出来。
比如要支持透支一定额度和冻结部分余额的功能,这是重新设计VirtualWallet类的实现代码public class VirtualWallet { private Long id; private Long createTime = System.currentTimeMillis();; private BigDecimal balance = BigDecimal.ZERO; private boolean isAllowedOverdraft = true; private BigDecimal overdraftAmount = BigDecimal.ZERO; private BigDecimal frozenAmount = BigDecimal.ZERO; public VirtualWallet(Long preAllocatedId) { this.id = preAllocatedId; } public void freeze(BigDecimal amount) { ... } public void unfreeze(BigDecimal amount) { ...} public void increaseOverdraftAmount(BigDecimal amount) { ... } public void decreaseOverdraftAmount(BigDecimal amount) { ... } public void closeOverdraft() { ... } public void openOverdraft() { ... } public BigDecimal balance() { return this.balance; } public BigDecimal getAvaliableBalance() { BigDecimal totalAvaliableBalance = this.balance.subtract(this.frozenAmount); if (isAllowedOverdraft) { totalAvaliableBalance += this.overdraftAmount; } return totalAvaliableBalance; } public void debit(BigDecimal amount) { BigDecimal totalAvaliableBalance = getAvaliableBalance(); if (totoalAvaliableBalance.compareTo(amount) < 0) { throw new InsufficientBalanceException(...); } this.balance.subtract(amount); } public void credit(BigDecimal amount) { if (amount.compareTo(BigDecimal.ZERO) < 0) { throw new InvalidAmountException(...); } this.balance.add(amount); } }
总结
- 基于充血模型的DDD开发模式跟基于贫血模型的传统开发模式相比,主要区别在Service层。在基于充血模型的开发模式下,我们将部分原来在Service类中的业务逻辑移动到了一个充血的Doamin领域模型中,让Service类的实现依赖这个Domain类
- 在基于充血模型的DDD开发模式下,Service类并不会完全移除,而是负责一些不适合放在Domain类中的功能。比如,负责和Repository层打交道、跨领域模型的业务聚合功能、幂等事务等非功能性的工作
- 基于充血模型的DDD开发模式跟基于贫血模型的传统开发模式相比,Controller层和Repository层的代码基本上相同。 这是因为REpository层的Entity生命周期有限,Controller层的VO只是单纯作为一种DTO。两部分的业务逻辑都不会太复杂。业务逻辑主要集中在Service层。所以,Repository层和Controller层继续沿用贫血模型的设计思路是没有问题