基于DDD的虚拟钱包系统设计

举报
JavaEdge 发表于 2022/05/14 22:23:25 2022/05/14
【摘要】 基于DDD的虚拟钱包系统设计基于贫血模型的传统开发模式基于充血模型的DDD开发模式如何分别用这两种开发模式,设计实现一个钱包系统。 1 业务分析具有支付、购买功能的应用(如京东、哈啰出行)都支持钱包功能。应用为每个用户开设一个系统内的虚拟钱包账户,支持用户充值、提现、支付、冻结、透支、转赠、查询账户余额、查询交易流水。钱包功能界面:每个虚拟钱包账户都对应用户的一个真实的支付账户,如银行卡账...

基于DDD的虚拟钱包系统设计

  • 基于贫血模型的传统开发模式

  • 基于充血模型的DDD开发模式

如何分别用这两种开发模式,设计实现一个钱包系统。

1 业务分析

具有支付、购买功能的应用(如京东、哈啰出行)都支持钱包功能。应用为每个用户开设一个系统内的虚拟钱包账户,支持用户充值、提现、支付、冻结、透支、转赠、查询账户余额、查询交易流水。

钱包功能界面:

每个虚拟钱包账户都对应用户的一个真实的支付账户,如银行卡账户、三方支付账户(支付宝、微信钱包)。本文限定钱包只支持充值、提现、支付、查询余额、查询交易流水。

1.1 充值

用户通过三方支付渠道,把银行卡账户的钱,充值到虚拟钱包账号的流程:

  • 从用户【银行卡账户】转账到应用的【公共银行卡账户】
  • 用户的虚拟钱包余额+=用户的充值金额
  • 记录该笔交易流水

1.2 支付

用户用钱包内的余额,支付购买应用内的商品。支付过程就是个转账过程,从【用户虚拟钱包账户】划钱到【商家虚拟钱包账户】。也需记录该笔支付交易流水。

1.3 提现

  • 用户将【虚拟钱包】余额,提现到自己的【银行卡】,即扣减用户虚拟钱包中的余额
  • 并触发真正的【银行转账】操作,从应用的【公共银行账户】转账到【用户银行账户】
  • 记录该笔提现的交易流水

1.4 查询余额

查询虚拟钱包中的余额数。

1.5 查询交易流水

只支持三种类型的交易流水:充值、支付、提现。用户充值、支付、提现时,记录相应交易信息。

查询时,将之前记录的交易流水,按时间、类型等条件过滤后显示即可。

2 钱包系统设计

根据业务实现流程和数据流转图,可把钱包系统业务分为:

  • 单纯跟应用内的虚拟钱包账户打交道
  • 单纯跟银行账户打交道

给系统解耦,将整个钱包系统拆分为如下子系统:

本文聚焦虚拟钱包系统的设计与实现。

钱包的核心功能,虚拟钱包系统需对应实现的操作:

钱包 虚拟钱包
充值 +余额
提现 -余额
支付 ±余额
查询余额 查询余额
查询交易流水 TODO
  • 充值、提现、查询余额功能,只涉及一个账户余额的±操作
  • 支付功能涉及两个账户的余额加减操作

2.1 交易流水记录

交易流水所含信息:

可见,交易流水的数据格式包含两个钱包账号:

  • 入账钱包账号
  • 出账钱包账号

①为何要有两个账号信息?

仅为兼容【支付】,这种涉及两个账户的交易类型。其实对充值、提现,只需记录一个钱包账户信息,所以,这样的交易流水数据格式的设计稍浪费空间。

还有一种交易流水数据格式设计。把“支付”交易类型,拆为两个子类型:

  • 支付,单纯表示出账,余额扣减
  • 被支付,单纯表示入账,余额增加

于是,设计交易流水数据格式时,只需记录一个账户信息:

②哪个更好?

第一种设计!因为交易流水有两个功能:

  • 业务功能

    如提供用户查询交易流水信息

  • 非业务功能,保证数据的一致性

    此处主要指支付操作数据的一致性

支付就是个转账操作,一个账户加金额,另一个减金额。需保证加、减金额这俩操作要么都成功,要么都失败。

③保证数据一致性

  • 依赖数据库事务原子性,将两个操作放在同一个事务。但不够灵活,因为可能做了分库分表,支付涉及的两个账户可能存储在不同库,无法直接利用数据库本身事务特性,在一个事务中执行两个账户的操作
  • 分布式事务,但为保证数据强一致性,它们的实现逻辑一般都较复杂、性能不高,影响业务执行时间
  • tradeoff后,不保证数据强一致性,只实现数据最终一致性,即交易流水要实现的非业务功能

对支付这样的类似转账操作,在操作两个钱包账户余额前,先记录交易流水,并标记为“待执行”,当两个钱包的加减金额都完成之后,再回调将交易流水标为“成功”。在给两个钱包±金额过程中,若任一操作失败,就将交易记录的状态标为“失败”。通过后台补漏Job,拉取状态为“失败”或长时处于“待执行”状态的交易记录,重新执行或人工处理。

若选择第二种交易流水设计,使用两条交易流水来记录支付操作,则记录两条交易流水本身又存在数据一致性问题,有可能入账交易流水记录成功,出账交易流水信息记录失败。所以,权衡后,选择第一种冗余数据格式设计。

充值、提现、支付这些业务交易类型是否应该让虚拟钱包系统感知?即在虚拟钱包系统的交易流水中记录这三种类型合理吗?

No!虚拟钱包系统不应感知具体业务交易类型。虚拟钱包仅是支持余额±操作,不涉及复杂业务概念,职责单一、功能通用。若耦合太多业务,势必影响系统通用性,导致越做越复杂。

④若不在【交易流水】中记录【交易类型】,用户查询时,如何显示每条【交易流水】的【交易类型】?

系统设计角度,不应在虚拟钱包系统的交易流水记录交易类型;产品需求角度,又必须记录交易流水的交易类型。问题很矛盾,怎么办呢?

记录两条交易流水信息。

整个钱包系统分为两个子系统,上层钱包系统的实现,依赖底层虚拟钱包系统和三方支付系统。钱包系统可感知充值、支付、提现等业务概念,所以,在钱包系统这层额外再记录一条包含交易类型的交易流水信息,而在虚拟钱包系统中记录不包含交易类型的交易流水信息。

  • 查询上层的钱包系统的交易流水信息,实现用户查询交易流水
  • 虚拟钱包中的交易流水,就只是解决数据一致性。其作用还有对账等

3 基于贫血模型的传统开发模式

Controller和VO负责暴露接口

Service和BO负责核心业务逻辑,Repository和Entity负责数据存取。

4 基于充血模型的DDD开发模式

充血模型DDD和贫血模型开发模式主要区别在Service层,Controller、Repository层代码基本相同。来看Service层充血模型DDD开发模式实现。

把虚拟钱包VirtualWallet类设计成一个充血Domain领域模型,将原来在Service类中的部分业务逻辑移动到VirtualWallet类,让Service类的实现依赖VirtualWallet类:

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 walletRepo.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类很单薄,包含的业务逻辑很简单。相对贫血模型设计,这种充血模型貌似并没啥太大优势。的确!这也是大部分业务系统都使用基于贫血模型开发的原因。

不过,若虚拟钱包系统需支持更复杂业务逻辑,充血模型优势就凸显了。比如要支持透支一定额度和冻结部分余额。看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 getAvailableBalance() {
    BigDecimal totalAvaliableBalance = this.balance.subtract(this.frozenAmount);
    if (isAllowedOverdraft) {
      totalAvaliableBalance += this.overdraftAmount;
    }
    return totalAvaliableBalance;
  }
  
  public void debit(BigDecimal amount) {
    BigDecimal totalAvaliableBalance = getAvailableBalance();
    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);
  }
}

领域模型类添加简单的冻结和透支逻辑后,功能更丰富了,不再那么单薄。若功能继续演进,可新增更细化的冻结策略、透支策略、支持钱包账号(VirtualWallet id字段)自动生成逻辑(不是通过构造函数经外部传入ID,而是通过分布式ID生成算法来自动生成ID)等。随着领域模型类业务逻辑越发复杂,充血模型就更有意义了。

5 DDD设计思考

充血模型DDD开发模式,将业务逻辑移动到Domain,Service类变薄,但代码实现并没有完全将Service类去掉,为何?即Service类的职责到底是啥?哪些逻辑放到Service类?

区别于Domain的职责,Service类主要有如下职责:

与Repository交流

VirtualWalletService类负责与Repository层交互,调用Respository类方法,获取数据库数据,转化成领域模型VirtualWallet,然后由VirtualWallet完成业务逻辑,最后调用Repository类方法,将数据回库。

之所以让VirtualWalletService类而非领域模型VirtualWallet与Repository交互,是因为保持领域模型独立性,不与任何其他层代码(Repository层)或开发框架(如Spring、MyBatis)耦合,将流程性的代码逻辑(比如从DB中取数据、映射数据)与领域模型的业务逻辑解耦,让领域模型更加可复用。

Service类负责跨领域模型的业务聚合功能

VirtualWalletService类中的transfer()转账函数会涉及两个钱包的操作,因此这部分业务逻辑无法放到VirtualWallet类中,所以,我们暂且把转账业务放到VirtualWalletService类中了。当然,虽然功能演进,使得转账业务变得复杂起来之后,我们也可以将转账业务抽取出来,设计成一个独立的领域模型。

Service类负责一些非功能性及与三方系统交互的工作

比如幂等、事务、发邮件、发消息、记录日志、调用其他系统的RPC接口等,都可以放到Service类中。

充血模型DDD开发模式尽管Service层被改造成充血模型,但Controller层和Repository层还是贫血模型,有必要也充血领域建模吗?

没有。Controller层主要负责接口的暴露,Repository层主要负责与数据库打交道,这两层包含的业务逻辑并不多。如果业务逻辑比较简单,就没必要做充血建模,即便设计成充血模型,类也非常单薄。

尽管这样的设计是一种面向过程的编程风格,但我们只要控制好面向过程编程风格的副作用,照样可以开发出优秀的软件。那这里的副作用怎么控制呢?

Repository的Entity即便被设计成贫血模型,违反面向对象编程的封装特性,有被任意代码修改数据的风险,但Entity的生命周期是有限。把它传递到Service层之后,就会转化成BO或者Domain来继续后面的业务逻辑。Entity的生命周期到此就结束了,所以也并不会被到处任意修改。

Controller层的VO。实际上VO是一种DTO(Data Transfer Object,数据传输对象)。它主要是作为接口的数据传输承载体,将数据发送给其他系统。从功能上来讲,它理应不包含业务逻辑、只包含数据。所以,我们将它设计成贫血模型也合理。

总结

充血模型DDD开发模式跟基于贫血模型的传统开发模式相比,主要在Service层。在基于充血模型的开发模式下,我们将部分原来在Service类中的业务逻辑移动到了一个充血的Domain领域模型中,让Service类的实现依赖这个Domain类。

在基于充血模型的DDD开发模式下,Service类并不会完全移除,而是负责一些不适合放在Domain类中的功能。比如,负责与Repository层打交道、跨领域模型的业务聚合功能、幂等事务等非功能性的工作。

基于充血模型的DDD开发模式跟基于贫血模型的传统开发模式相比,Controller层和Repository层的代码基本相同。因为,Repository层的Entity生命周期有限,Controller层的VO只是单纯作为一种DTO。两部分的业务逻辑都不会太复杂。业务逻辑主要集中在Service层。所以,Repository层和Controller层继续沿用贫血模型的设计思路是没有问题的。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。