Java微服务构建一个健壮的订单模型(业务,规划,设计与实现)
在设计领域业务模型时,我们通常会追求理论完美,而忽略实践的脆弱性。尽管我们没有贬低领域建模的意图,但事实上,在电商技术发展多年之后,某些系统模型仍然缺乏弹性。本文将结合多年电商交易经验,分享一些个人对设计思路的见解,与大家共同学习、进步。
交易是指买卖双方在签订合同并履约的过程中完成的一系列活动。合同签订是正向交易的职责,而履约则包括物流、仓储和发货等服务。合同包含的内容非常丰富,比如卖家售卖什么产品、提供哪些服务、如何履约或违约的赔偿方式、如何维权等。如图1所示,当然图中缺少了维权的部分。如果买家对产品不满意或发现质量问题,可以根据合同中的条款进行退款、换货等。这些条款可能包含一些隐含的要求,例如电商法中的规定。
(图1)
在电子商务的交易中一般是人、产品的进行的一次互通的行为。从维基百科的定义来看交易其实包含物品或者服务,可能你既提供产品又提供服务,也可能你只卖服务,这个时候就没有产品。产品也可能不是真实存在的物品,它也可能是一种虚拟的商品,比如虚拟的货币。我先分析一般情况下交易设计的模型。
一个实体模型就是一个独立的事物。每个实体都拥有一个唯一的标识符,可以将它的个体性和所有其他类型相同或者不同的实体区分开。许多时候,也许应该说绝大多数时候,实体是可变的。也就是说,它的状态会随着时间发生变化。不过,一个实体不一定必须是可变的,它也可能是不可变的。
在设计的时候关于实体和值对象的区分有时候是比较困难的,不同人可能对实体的理解不一样。比如对订单条目OrderItem是实体还是值对象?有的人认为是实体有的人认为是值对象。订单的话有订单id或者订单号来做唯一标识,这个共识会比较强,那么条目orderItemId是不是订单条目的唯一标识呢?在考虑这个是否是唯一标识的是不是有意义的下,我们可以跟值对象做对比,实体区别于值对象的最大原因是,值对象的值是不可变的,比如我们的收货人信息Receiver,在下单用户选择地址的那一刻就已经确定下来了,在订单域他就是一个地址信息,他的属性在订单的生命周期中不会改变,它只是用来发货的,它也没有唯一标识,所以他是一个值对象。一个订单可能有多个条目,每个条目是可以单独发货,在下单待支付的时候也可以对条目的商品进行改价,收货维权的时候买家或者商家可以选择某个商品进行退款,所以他在订单生命周期中会随着时间的变化而变化,所以它是一个实体,他的唯一标识就是orderItemId。
订单这个聚合根我们再熟悉不过了,在很多电商领域都会提到这个聚合根,如图1大体的我们一般会得出这样的一个关联关系,这个关系有点单薄。虽然他可以体现出订单这个聚合的大体依赖关系,并且可以表达80%的场景,但是不能比较好的体现销售订单和采购订单的关系。销售订单主要是站在买家的角度阐明买家下单,采购订单主要是站在商家的角度向供应商下单。所以至少我们在订单中需要有一个值对象来标识这个关系,对应的条目其实也有这一层的关系。
一般的电商交易都会有一些显示或者隐私的条款的,隐私的比如物流条款选择的物流配送方式,是自提还是快递或者无需物流,物流的显示条款可能有是否支持运费险。在维权方面还有一些隐性或者显性的条款,比如是否支持7天无理由退货。还要一些活动,比如定金预售,我们在商品下单的时候就会给用户提示下了定金是不支持退款的,我们这边的订单条款主要考虑的是显示的条款,隐私条例一般是电商法需要支持的,是一种默认的条款。
但是很遗憾,这些条款信息在我做过的交易里是没有很好的体现(因为历史的债务),甚至有些条款都是没有落库的,有些条款是通过扩展字段存储在订单扩展字段或者条目的扩展字段中。现在我们交易系统在很大程度上解决扩展性问题,都是通过扩展字段来承载,所以有了很多打标的接口,这样处理当然在性能上是有很大的好处,比如解决了性能问题,但是在复杂的业务系统中,其实就隐藏了一部分业务逻辑,在领域设计中是不被允许的,所以在解决性能问题的情况下我们可能需要平衡一些业务。大量的打标服务从而导致扩展字段的急速膨胀,最终解决的办法就是不断的扩充extra字段的长度。
我们在下单支付等过程经常的会对订单进行一些调整,比如我们会对待支付的订单进行改价,这个一般的我们需要一个调整单,显示的记录这个调整。广义上来讲,我们订单的调整还包含一些费用,比如附加费、额外费用,商品或者运费的均摊,这些数据还比较复杂,一般的是作用在订单条目的,如果要在订单条目存储会比较复杂,因为包含的内容还比较多,比如调整价格,原价,调整原因,调整时间等等。所以这些调整信息比较适合单独拎出来存储。调整单的设计在ERP系统比较常见,比如我们在算出入库的时候,在月末结算不均衡的时候会手动入一个入库调整单。这里讲的订单调整不是ERP的调整单,它是针对交易的计算价格的时候,对商品价格的一些调整。
一般的,订单调整是指在订单创建后,由于各种原因需要对订单的价格、数量、物品等进行修改的过程。在实际业务中,常见的订单调整场景包括满减活动、优惠券使用、商品退换货等。以满减活动为例,当满足活动条件后,系统会自动计算出优惠的金额并减免相应的金额,然后将修改后的订单金额保存在数据库中。此时,订单状态也会发生相应的变化,例如待支付的订单状态会变成待发货、待收货的订单状态也会相应地发生改变。在调整过程中,需要对订单相关的数据进行验证和更新,保证数据的准确性和一致性,同时还需要对用户进行提示和通知,以避免产生误解和纠纷。
订单的状态是随着时间推移而发生变化的,订单的状态变化可以通过不同的事件来触发。订单的状态通常包括:创建、待支付、已支付、待发货、已发货、待收货、已收货等。
当买家下单后,订单状态为“创建”,此时卖家需要对订单进行确认并进行库存锁定等操作。如果卖家确认订单信息无误,则将订单状态变更为“待支付”;否则将订单状态变更为“取消”。
当买家完成支付后,订单状态变更为“已支付”。此时,卖家可以开始准备商品进行发货,或者进行拣货、分拣等前置工作。在准备好商品后,订单状态变更为“待发货”。
当卖家将商品发出后,订单状态变更为“已发货”。此时,买家可以开始关注物流信息,并等待商品的到达。如果商品未按时到达或者出现其他问题,买家可以通过平台提供的维权服务进行投诉。
当商品到达买家手中时,订单状态变更为“待收货”。买家需要在收到商品后进行确认,如果确认收货,则订单状态变更为“已收货”,交易完成。如果买家在一定时间内未确认收货,则订单状态会自动变更为“已完成”。
在订单状态变更的过程中,可能会存在一些意外情况,例如买家取消订单、买家申请退货等,这些事件会触发订单状态的相应变化,以确保交易的公正、合法和安全。
订单状态记录着订单生命周期,其实还是比较核心并且复杂的一个实体。不同的订单类型的订单状态流转不一样,这个需要进行一个统一的设计,一般的话我们会考虑用状态机来实现。交易中的订单状态是可以枚举的,一般的我们会把订单状态设计成一个枚举类,但是订单状态其实承载了很多操作和控制状态流转的行为,所以它其实应该是有自己的唯一标识的实体。