说起来,这篇博客已经写了有六七个月了,不过因为涉及到了不少细节内容,不便于分享出来,最近整理了一下,将部分细节内容去掉,分享出来,希望对大家有所帮助。
一个项目的开发同学水平不一,习惯、喜欢的架构也有所不同,再叠加上几年各种的需求,想让一个系统能持续保持干净其实是很难的一件事。估计有很多开发同学经常很想将自己接收过来的项目重构一下,让它看的“顺眼”一些。
如果系统架构和设计的时候能够全面解耦,可以极大的降低系统的耦合,最大可能的保持系统代码干净、逻辑清晰,后续的扩展也会更加容易。实际上如果做好了解耦,即便是一个系统再烂,也只局限于一个系统中,不会对其他项目产生影响,后期想优化、重构时也比较容易很多。
这篇博客主要就是讨论如何将商品、交易、支付等作为底层公共服务来设计,以便于灵活、快速的支持上层业务发展的需要。
清晰各个系统的边界在何处,如何划分系统。
清晰什么是交易,交易的核心要素
清晰支付系统的核心功能。
清晰物流系统的核心功能,拆包、并包方案。
清晰交易、支付、物流、退款系统的架构与设计,以及系统边界。
如何解耦各个系统,以及解耦带来的好处。
服务化设计与“大中台、小前端 ”思想。
讨论几个案例。
在这里我很想传达一种观点:编码不是需求的堆叠。它更像是先制造一个个的积木(功能模块),不管我们是想搭茅屋还是建城堡,都是组装这些积木(功能模块)以实现我们的目标。
解耦基本上都是依赖于MQ来实现的,我使用的是RocketMQ,他可以保证db事务和MQ消息一致。注意最新的RocketMQ开源版本4.1.0目前的事务性消息不完整,以后有机会详细聊一下这个问题。
以下的交易、物流、退款、支付流程均是我根据之前工作总结出来的,为了便于理解,我这里以大家最为熟悉的淘宝、京东交易流程示例来讨论。这里讨论的设计方案的想法很多借鉴了阿里的电商模型,不过我并没有在阿里呆过,所以也不能100%确定他们现有的模型是否发生较大的变化,如果哪位小伙伴发现了,欢迎支持。
MQ相关的内容可以参考一下博客:
RocketMQ实践:https://yq.aliyun.com/articles/278085
RocketMQ原理:https://yq.aliyun.com/articles/278086
Kafka VS RocketMQ VS RabbitMQ:https://yq.aliyun.com/articles/278087
交易是buyer在某一时间以某一价格购买了seller的一个或多个商品(或者一种sku),所以对于交易而言buyer、seller、item/sku、quantity、price、date是其核心要素。创建订单的过程其实就是buyer、seller签订契约的过程,契约签订完成后,不应该随意变更,除非通过另一种契约终止或者修改此契约。
buyer:买家;购买商品的人
seller:卖家;销售商品的人。
Item:商品
Promotion:促销,例如店铺满减、折扣、单品直降等等。
Coupon:优惠券
Sku:Stock Keeping Unit(库存量单位),简单来说能够唯一描述库存量的最小单元就是sku。示例:一个卖家销售iphone时挂出以下信息:白色、银色两种颜色,128g,64g两种容量;那么白色+128g就是一个sku。如果另外一卖家销售时加了一个属性:电信、移动、联通,那么白色+128g+电信就是一个sku。
Point:不同平台的称呼基本上都不同,这里统一叫做积分。积分分两种,一种是系统随意发放,没有抵价物,大部分系统中基本上都是使用这种;另外一种是和资金挂钩,即每个积分都和钱挂钩,发出去多少积分就需要提前划拨多少钱出来。后面这种做法常见的方式是有一个积分池,每向池子中放一笔钱,积分池就加一定量的积分;发放时每给用户发放一笔,积分池中减少相应的积分。
需要注意的是在两种积分方式在最后结算、对账时会有很大的差别。此内容超出本文的范围,不做讨论。
以下是交易、支付、物流模型。交易系统的核心数据是biz_order,为了与支付、物流系统解耦,分别创建了pay_order和log_order分别对接支付系统、物流系统。
说明:
支付订单(pay_order)应该和交易订单(biz_order)放在一个事务中创建,记录此笔交易的详细资金明细。
物流订单(log_order)也应该和交易订单(biz_order)放在一个事务中创建,记录此笔交易的实物商品物流情况。
支付(pay_order)、物流订单(log_order)的状态变更会影响交易订单(biz_order)中状态的变化。
支付系统搞了一个支付订单(payment_order),交易也系统搞了一个支付订单(pay_order) ,物流系统搞了一个物流订单(logistics_order),交易系统又搞了一个物流订单(log_order) 。为什么要这样做?这样做能给我们带了什么好处?
Pay_order数据是否应该合并到biz_order中,或者说分两张表有什么好处?这个问题等后面讲表的主要结构与字段的时候在描述。
支付系统的payment_order:主要对外,负责封装第三方支付渠道,负责支付过程中状态流转。
交易系统中的pay_order:
对外主要对接交易系统的payment_order,通过pay_order创建payment_order,而payment_order的状态变化将影响pay_order的状态变化,他们一一对应。
对内pay_order对接biz_order,通过biz_order中购买商品、优惠等信息决定每个订单的实际金额信息,而Pay_order的状态变化将影响biz_order中状态的变化。
物流系统的logistics_order:主要对外,负责封装第三方物流信息,负责物流状态流转以及拆包、并包逻辑。
交易系统中的log_order:
对外主要对接物流系统的log_order,通过log_order创建logistics_order,而logistics_order的状态变化将影响log_order的状态变化,他们一一对应。
对内log_order对接biz_order,而log_order的状态变化将影响biz_order中物流状态的变化。
系统架构的核心思想是让系统关注自己的核心业务,让系统尽可能内聚,让和外部系统的耦合尽可能低。
以下三张图分别是淘宝、京东、飞猪的订单确认页面,我们接下来就根据它们讨论交易系统的设计。之所以选择京东、淘宝是因为这是大家最熟悉的两个电商平台,之所以选择飞猪的路线和实体商品有所不同,交易流程会有差别,并且复杂度更高;另外一点就是做过一段时间的旅游交易,稍微熟悉一些。
itemId需合法、数量需合法
如果是实体商品,地址是否为空
如果需要发票,发票信息是否合法。
等等…
如上示例图可见,如果是旅游路线这种商品,需要包括一些套餐、出游人群、出团日期信息,因为这些都是必须的,所以均需验证。如果是酒店、机票订单,校验的信息也不同。
对于上面的几种交易而言,都有明确的卖家,所以sellerId是确定的;单另外一些情况下,一个平台可能收集各种商品,自己拿来买,即整个平台都卖家就是平台自己,此时需要设置一个默认的卖家。
如果平台关注购买渠道(pc、mobile、h5等)信息,也需要校验。
通过buyerId、sellerId查询buyer、sellerId信息,判断buyer、seller是否有效合法。
通过addressId查询地址信息,判断address是否有效合法。
通过itemId、itemSkuId查询商品、商品sku信息,判断商品是否有效合法;数量是否足够;商品的卖家信息是否和sellerId一致。
如果使用邀请码之类的进行兑换,查询相关码是否有效合法。
如果使用积分、金币进行抵扣,查询并验证积分、金币是否足够。
如果使用红包抵扣,查询红包是否合法、可用。
如果参加优惠活动,检查优惠活动的信息,确保优惠信息合法、可用。
如果使用优惠券,查询优惠券信息,判断是否合法、可用;另外须确保优惠券的领取者和buyer一致,大部分可能如此,但如果是类似于微信、支付宝共享优惠券的另当别论,但不管如何均需确保用户可以使用此优惠券。
类似于飞猪的路线类商品,需要校验套餐和日期等多个条件下的数量是足够。本质上来说这也是一个item/sku数量的校验,只是对于路线类商品而言,套餐、人群、出团日期这三个要素构成了一个sku。
从上面的校验过程来看,交易过程中需要与很多服务打交道,而且会对这些服务产生写请求,提前验证可以减少失败率,减少无效操作,特别是减少异常补偿流程。
数据组装是一个比较复杂的过程,以下是组装订单、计算金额时需要考虑的因素。
Item/sku
购买时,如果商品不区分sku,那我们购买的就是一个item;如果商品区分sku,那么我们将不能直接购买item,而是sku。
一次购买多种item/sku,需要考虑订单合并成主子订单。例如从购物车过来的订单,可能是不同卖家的商品,此时需要先根据sellerId对商品进行分组。
促销活动
常见的促销活动有:店铺满减、店铺满折、单品满减、单品直降、单品折扣;而且很多时候这些优惠活动会混合在一起使用,并且还包含一个使用优先级。
优惠券
常见的优惠券有:店铺满减、店铺折扣、单品满减、单品直降、单品折扣,不但需要可以混合使用,有可能还需要和促销一起使用。当然如果促销、优惠券需要跨店铺,那就更加的复杂了。
积分
使用积分抵扣。
tem/sku问题
如果购买时,传入了skuid,就需要根据item、sku来创建order,否则可以指根据item来创建order。
每一个order中只应该包含一个商品(不这样做会给后续带来无限的麻烦),但是如果在一个店铺购买了多种商品,但是需要一次付款,那么就需要创建一个主订单记录订单的主要信息,创建多个子订单分别记录购买的物品。
促销、优惠券的使用顺序问题
先使用Promotion还是应该先使用coupun?先使用店铺优惠(券),还是应该先使用单品优惠(券)?
Promotion/coupun的使用顺序其实是一个业务问题,由公司的运营策略决定;其中的差别是:如果优惠券需要用户领取,用户获取优惠券是有成本的,如果希望用户都走这一步,或者希望通过优惠券拉来用户,那么使用优惠券是比较适合的;否则使用促销活动就可以了。另外,要根据公司结算要求来决定使用顺序,注意拥有相同的优惠和优惠券,不同的使用顺序,和可能最终的实际金额是不同的。
店铺/单品优惠的不同使用顺序会影响最终的金额。一般来说大都先使用单品的,然后使用店铺的,因为这样对与店铺而言更加可控。
首先将所有的item/sku根据sellerId分组。然后检查在此卖家下购买的商品种类,如果只有一种,那么创建一个订单,直接记录购买的商品信息;如果超过一种,那么创建多个子订单,一个主订单。
根据业务需要,决定先使用promotion还是先使用coupun;先使用店铺优惠还是先使用单品优惠;将最终计算出的优惠金额存入订单中,以明确每个订单使用每一种优惠时抵扣了多少钱。一般会根据每个子订单的总金额按照比例均摊总的优惠金额,此时需要注意可能出现不能整除时,此时需要修正金额。如果不按照每个子订单金额的比例分摊优惠金额,被人恶意退货、退款时,可能会出现严重的问题。
将主订单、子订单放在同一个事务中,insert到db中,同时发送mq消息。因为需要发送mq消息,所以orderId需要提前生成。具体流程图见后面部分。
接着减库存。
如果使用了积分,接着冻结积分。
如果使用了优惠券,接着消费掉这些优惠券
在事务中修改主子的状态为success,并且发送MQ消息。到此时订单创建完成。
说明:进行扣减库存、冻结积分、消费码、使用优惠券等操作的时候一定要带上orderId,用于在Rpc调用的时候做幂等。消费的顺序不一定需要和流程图中保持一致,具体根据业务情况来决定。
说明:如果在校验环节没有查用户积分是否足够,这里建议优先冻结积分,以免出现大量冻结库存的情况。
仅仅展示了主要字段,用以参考。
Id |
id |
|
category_id |
类目id |
一般不同的类目交易流程或者后续的履约流程会有所不同。 |
item_type |
商品类型 |
商品类型跟着类目走,代码中根据此字段决定走不同的流程。 |
item_id |
商品id |
|
item_sku_id |
商品sku id |
|
item_price |
价格 |
|
item_title |
商品名称 |
|
quantity |
数量 |
|
total_amount |
总金额 |
|
actual_amount |
实际金额 |
|
is_main_order |
是否时主订单 |
主子订单时使用 |
parent_id |
父订单id |
子订单时,对应的父订单id |
buyer_id |
买家 |
|
seller_id |
卖家 |
|
order_date |
下单时间 |
|
out_id |
外部id |
和out_type一起,可用作幂等 |
out_type |
外部类型 |
和out_id一起,可用作幂等 |
status |
状态 |
考虑到给用户展示的状态其实时biz_order、pay_order、logistics_order三张表数据汇总后的结果,而read远多于write操作,为性能、实现复杂度考虑,很有必要在biz_order中存储这三个字段 |
pay_status |
付款状态 |
|
logistics_status |
物流状态 |
在父子订单的场景中,如果是父订单item信息可以不为空,否则item信息不为空。
id |
id |
|
biz_order_id |
业务订单id |
如果不考虑一个订单多次付款,也可以放到biz_order中 |
total_amount |
总金额 |
|
point_amount |
积分抵扣金额 |
|
discount_amount |
折扣金额 |
|
adjust_amount |
商家改价金额 |
|
actual_amount |
实际付款金额 |
总金额减去所有优惠、调整后的金额 |
status |
状态 |
例如:未付款、已付款、已退款等等 |
pay_date |
付款日期 |
|
out_pay_no |
外部付款流水号 |
支付系统中支付流水号 |
通过字段我们可以比较简单的看出,这里biz_order负责处理业务订单逻辑;pay_order负责处理支付相关的逻辑。
在这里需要强调的是,pay_order中仅仅存放了与金额相关的信息,他们都是属于交易的一部分数据,只是基于现实开发中设计上解耦的需要,才将他们拆开的,所以他们有不少字段是相同的。具体原因如下:
l  优点:
biz_order负责业务,pay_order负责优惠,业务数据可以做到更加内聚。
当增加新的营销玩法的时候,金额计算过程将产生较大的变化,但其他数据基本可以不用修改,分开可以做到与biz_order相关的逻辑基本不用修改;
当对接新业务的时候,一般来说个性化业务的数据会略有不同,但与计算金额的逻辑基本上不会有什么变动,分开可以做到pay_order也基本不用修改。
当交易系统对接的上层业务越多,优惠方式、促销玩法种类越多,分开的优势会越明显。业务越复杂,功能越烦杂时,越是忌讳模型的混用。
l  缺点:
编码复杂度会增加。
以上只是列举了使用积分、优惠券的示例,实际上根据公司的运营活动需要,可能远远比次复杂,不过处理方式基本以上,不再累述。
说明一下:如果在数据校验环节不校验当前用户积分是否足够,那么最好先扣减积分再减库存。
为了方便讨论,我将订单完成/回滚流程放到了同一个图中。
说明:是否需要回滚以上的商品库存、积分、优惠券内容,需要根据业务决定。例如很多电商网站都不会还原使用过的优惠券。
从“创建订单流程”可知,我们首先插入了一个init的订单,此时seller、buyer的任何数据都没有发生变化;接着我们执行减库存、冻结积分、使用优惠券、使用等流程,因为这些都是rpc调用,我们无法通过事务保证数据的一致性,所以就有了这个流程,从上图可以看出,通过此流程,seller、buyer的数据是可以保持一致的。
当收到init的消息的时候,建立一个超时回滚订单的任务(例如3分钟内如果订单未成功创建则回滚订单)。
当订单创建过程遇到问题,则订单的状态将会被改成success,并发送success的消息。
当收不到success的消息时,任务订单创建失败,就可以在预定时间执行订单回滚任务。(注意回滚以前先检查订单状态)。
当收到success的消息时,将之前创建的超时回滚订单的任务取消掉。
到目前为止可以保证订单、商品、积分、优惠券等的数据保持一致。
说明:以上流程不建议考虑使用TCC的方式来实现分布式事物,因为涉及方越多时TCC失败的可能性越大,逻辑耦合性也越高。这里说的耦合性指:创建订单其实只需要完成创建订单的主流程,订单失败回滚的流程不属于创建订单核心流程,应该算作是补偿的辅助流程。
收到订单success的MQ消息以后,需要删除之前的订单回滚任务。
如果订单不需要付款,那么直接调用已付款接口将订单状态改为paied;如果订单需要付款,那么创建订单超时关闭任务(例如15分钟未付款则关闭订单)。
如果指定时间内buyer未付款,通过之前建立的超时关闭订单任务执行关闭订单逻辑,此时的操作和订单回滚操作基本一样,所以流程中我将这两块合并了。
一般而言很多业务都会关心订单完成事件,例如通知中心,所以在流程图中画了几个收到success消息的流程入口。
此流程先减库存,然后再让订单创建完成,所以可以保证不会出现超卖问题。如果允许商品超卖,那么可以将减库存这一步已到付款成功以后。
为了防止被人恶意刷单,很多平台都采用此方式。
另外,如果与外部系统对接,减库存是一个很慢的操作,例如在飞猪上购买飞机票,此时可以下完成订单,然后用上层业务系统再调用机票系统出票,如果出票失败,那么订单取消掉,钱退回去即可。
从上面的流程中可以看出,在保证数据最终一致性上是如下处理的:按照业务需求执行正向流程,并且在未按照预期执行的时候创建异常补偿流程。上面的流程带来的另外一个好处是:业务可以做大最大程度的解耦。
MQ重试可以排除掉因为并发、系统负载高等原因而导致的失败,所以只要代码没有问题,重试多次rpc调用会被最终执行掉。
顺便提一句,有不少通过分段提交的方式来处理分布式事务,保证数据一致性。分段提交的原理是提前预执行sql,尽量减少提交操作失败可能性,此种方式的性能损失很厉害,处理与钱相关的实时场景以外,一般高并发场景大都是只保证数据的最终一致性。
讨论了订单创建过程,那么接下来我们讨论一下交易相关功能的解耦、扩展问题。
不知道大家是否注意到,上述流程其实已经可以将耗时长的操作和耗时短的操作分开了,从性能上而言,创建订单不会受到类似于通知这类耗时操作拖累。
因为任何系统都可以订阅订单流转过程中的MQ消息,所以交易系统可以只关注核心部分的类容,其他的(对交易系统而言的)边缘操作可以完全从交易系统中剥离出去,这样交易系统足够内聚,极大的降低了和外部系统的耦合。例如流程中关于建立“超时回滚订单”、“超时取消订单”的任务都完全可以剥离到任务系统中去,交易这边只提供回滚/取消订单的接口供任务系统在执行这两个任务时调用即可。
如上简化流程图图所示,买家付款时主要包括以下内容:
进入支付系统的收银台页面,选择支付渠道。
调用支付系统接口,根据付款渠道,生成付款订单(payment_order),生成付款url。
用户支付。
支付结果回调。
支付系统的订单(payment_order)状态改为付款成功、发送MQ消息,并记录重要信息。
根据支付系统的MQ消息,修改订单(biz_order)状态、支付订单(pay_order)状态。
从流程图上可以看到,支付时交易系统核心要做的事情是生成付款url,最后根据付款结果修改订单状态。修改状态时有两种选择:
先修改pay_order的状态、发送MQ消息,然后根据MQ消息修改biz_order的状态。
同时修改pay_order、biz_order的状态。
考虑到pay_order是biz_order信息的延伸,非常建议一起修改状态,并在biz_order表中加上pay_status字段。这样可以保证任何时候二者的数据都是一致的。这种一致性将会极大方便我们的业务处理流程。
如上流程简图琐事,卖家发货时需要做一下事情:
卖家将快递公司、快递单号填写到物流订单(logistics_order)中,修改状态并发送MQ消息。
修改订单(biz_order)、物流订单(log_order)的状态为待收货。
强烈建议biz_order、log_order的状态放在同一个事物中更新,以保持一致性。
如果希望简化物流相关的流程,在没有拆包、并包的情况下可以将log_order、logistics_order合并,这样处理流程可以简化很多。
强烈建议biz_order、log_order的状态放在同一个事物中更新,以保持一致性。
注意:logistics_order的状态应该根据物流公司的物流状态来流转,用户收货操作不应该改logistics_order的状态,当然添加一个标记还是可以的。
评价对于交易系统而言不是核心功能,而是一种属于商家、商品口碑,所以评价以后,不应该直接修改biz_order的状态,当然可以加一个标记字段。如果评价以后修改状态,那么至少在以下场景中需要仔细考虑,以免留下坑:
收货后除了有一个等待期以外,还存在退货、退款的场景,这些场景中是否允许评价?
多次评价时,状态如何修改?
直接问这个问题其实意义不大, 因为这和运营策略有关,如果是平台自营的,那自然是商品评价;如果不是平台自营,那么评价可能是商品、也可能是商家,甚至让用户分别评价。
评价的核心是对商家、用户打分,为建立信用评级和用户购买提供参考。这部分不在此次讨论范围之内。
一般而言,在实物商品交易过程中订单有这几个状态,未付款、已付款(待发货)、已发货(待收货)、已收货、待评价、退货中(退款中)、已取消、交易关闭等。从买家付款成功,到买家收货;从退货开始到订单取消的过程中,都有物流的身影。接下来就主要讨论一下物流问题。
这是一个简化的物流流程图,简单描述了买家、卖家、物流对订单的影响。
物流最为麻烦问题在与拆包和并包。例如商品在不同仓库中的时候,需要拆分成不同的物流单;同一个仓库的商品可以合并到一个物流单中发货。
京东、考拉的做法是:下单时判断商品是否在同一个仓库,如果在同一个仓库,那么创建一个主订单;如果不在同一个仓库,那么分别为不同的仓库创建主订单。
如果一个订单的商品比较多,无法放在同一个包裹中,此时也需要将一个物流单拆成多个物流单。例如用户购买了2个空调,一个包裹放不下,此时可能就会分成两个包裹分别发货。
例如在天猫超市购买了一堆生活用品,可能天猫超市将所有的包裹打包成一个,一起发货。
此图为实体的模型。
说明:
业务订单存在主子订单的情况,此时一个主订单(biz_order)对应多个子订单(biz_order)。
每个业务订单(biz_order)都有一个物流订单(log_order)
交易系统中每个物流订单(log_order)和物流系统中每个物流订单(logistics_order)相对应。
并包和拆包属于物流系统的核心逻辑,应该放在物流系统中。
拆包时将原来一个logistics_order拆分成一个主单(logistics_order)多个子单(logistics_order)。此时可以这样处理:原来的logistics_order改为主订单,并且新建多个子订单(logistics_order);也可以将原来的logistics_order做特殊标记,然后重新创建一个主订单多个子订单。个人建议使用第一种方案。
并包时为这些logistics_order创建一个主订单,并将他们自己标记为这个主订单的子订单。
物流的核心功能应该至少包括以下内容:拆包、并包、物流信息管理、对接第三方物流信息、物流明细管理。另外关于仓储和物流规划这部分内容没接触过,就暂不讨论了。
支付系统的第一个功能应该是封装第三方支付渠道暴露一个收银台,用户可以随意选择一种方式进行支付。下图是大众点评的收银台截图:
接下来考虑在这个简单的收银台页面后面潜藏的各种业务逻辑,以及涉及的核心问题。
这里说的支付订单是payment_order,用于和第三方支付对接,和交易章节中说的支付订单(pay_order)是不同的,注意不要混淆。
比较合适的时机是用户点击确认支付的时候,此时可以检查一下之前是否已经创建过过支付订单,如果未创建,那么可以创建一个;如果已经创建过,并且用户更换了支付渠道,那么需要删除重建或者修改原有的订单。
这两种的差别在于:一般都使用逻辑删除,所以新建支付订单会产生一些无用的数据,相比之下建议在原来订单上修改。
如果用户开始选用A渠道支付并且付款,但是支付系统迟迟未收到A渠道的异步回调通知;此时用户改换另外一种支付渠道B支付并付款.因为用户未一笔订单支付了两次,所以必然存在问题,那么这种场景如何处理?常见的处理方式如下:
比较适合的一种处理方式:以第一次收到回调消息为准,更新支付订单的支付渠道、状态等信息;后面收到付款回调通知时,一种方式是记录异常数据,人工处理次数据并退款,另外一种处理方式是第二次收到消息以后,将此数据入库并发送MQ,收到MQ后自动原路退款(此时的退款和用户的退款流程是不同的,需要特别注意)。因为这中异常场景的发生几率比较小,建议人工处理。
每种支付渠道支付时所需要的信息都所不同,处理流程也存在差别,所以支付系统要将对外的处理流程做统一封装,即做到对于内部系统而言,所有的支付渠道都是透明的,只关心和支付系统的数据交换。
支付系统的核心至少包括以下内容:生成支付链接、进入第三方支付页面、登陆或者识别二维码、支付、返回当前应用展示支付结果,第三方通知支付结果。这些功能中,从进入第三方支付页面到付款部分的操作属于第三方渠道的事情,我们无法干预,其他部分则是需要封装处理的。
因为支付时存在资金的流转,这是一个很慢的过程,所以大部分情况下的第三方支付渠道会选择两种方式处理:1、收到支付请求后立即返回,处理完支付的所有操作以后,异步通知支付结果;2、提供支付等待页面,支付成功以后再跳回介入方的网站。
我们在网上购物的时候,输入完支付密码以后,要么等待第三方支付渠道处理完成,然后进入我们的付款结果页,要么是立即返回到原应用的付款结果页面。进入付款结果页以后,让用户选择“已完成支付”、“支付遇到问题”。不管是哪种方式,在等待付款结果页面中,都时一个定时查询订单状态,如果收到支付渠道的成功结果通知,即显示支付成功,修改付款订单、交易订单状态;如果一直没有收到支付结果通知,可能会一直等待,也可能进入失败页面。
支付订单的每一个状态变化都发送了MQ消息,这样做的目的是实现业务解耦,让系统更加专注于自己的核心业务。例如:如果业务系统关注这个事件,去订阅topic就可以了,如果不需要处理此事件,直接忽略,交易系统不用为此而做额外的工作。
注意:因为存在退款退货,所以和商家结算的时候一定要留足时间,订单处于退款退货状态中时,不能和商家结算此交易。
先校验:入参校验;数据校验;是否允许退?提交的金额是否大于最大可退金额?
根据订单数据、提交的退货、退款申请,创建退货退款记录。
保存记录并发送MQ。
注意:为每一个biz_order创建一个Refund记录。
卖家审核买家提交的退货、退款申请。这里可能需要审核、打回退款申请、平台介入等多个步骤。
这一步骤主要目的是:买卖双方就退货退款一事达成一致。经过卖家确认或平台确认的退货、退款单应该不能随意被修改。
此过程和上面卖家发货流程比较类似,所以不再详述。
此过程上面买家收货流程比较类似,所以不再详述。
此过程和通过第三方付款的流程比较类似,所以就不在详述了。
Trade系统依赖Refund系统,还是Refund系统依赖Trade系统?
退款退货时,可能有比较复杂的流程,让退款入口放在refund系统中,保持trade的干净,从这个角度上考虑让Refund依赖Trade是合理的。所以上面的流程图是根据refund依赖trade画的。
另外我简单四考虑一下,退货退款都是基于订单的操作,如果让trade依赖refund系统,目前看起来应该问题也不大。
首先考虑一个场景;假设商家在平台上销售商品,平台收取10%的佣金,那么当买家购买一个100元的商品并付款时,这笔钱的具体流转过程是怎样的?付款时全部付给商家,这肯定是不行的,这会导致平台无法收取10%的佣金,因为钱已经到商家账户后,平台无权将商家的钱转移出来;付款时如果将钱全部付给平台,然后由平台和商家结算,那其实就是平台的收入,这样可能导致平台多交税。大部分情况下,付款时就需要将钱直接分摊到各方账户。
在本节主要讨论一下服务化的核心思想。
现实中,我们可能遇到以下几种不同的场景。
例如淘宝、京东上的购买商品,通过物流收到货去就好完成了交易的全过程。
大部分场景中,其实我们购买的是一个电子券,然后兑换成实物(电影票);甚至不用兑换,直接线下验证券的有效性(例如大众点评、美团上销售的服务)。
其中兑换成电影票、线下验证券的有效性(如果有效将享受线下服务)其实就是服务履约的过程。
例如:用户花费100元购买了一个15分钟专家问诊服务,此时整个流程的全部过程均在线上完成。
购买一个服务和购买一个实物商品,交易过程并没有什么不同,不用的点在于是否走物流、是否有后续履约过程。
从营销上看,不管是实物商品还是服务商品,运营同学均可以通过营销活动、优惠券、积分等包装服务,来换取更多的销售转化;从支付、结算、对账过程来看,两者也没有什么不同。
为了更好的支持后续业务灵活多变的需求,比较好的处理方式是将交易、支付、结算、对账、物流等流程抽象成一个标准化服务,根据不同的交易类型,决定后续的其他逻辑。
需要说明的是:有些服务也需要物流。例如基因检测服务。当购买基因检测服务以后,卖家先将基因检测盒通过物流发送到买家手中,买家收到基因检测盒以后讲带有DNA的唾液等物放在盒子中寄回,基因检测公司收到盒子以后分析DNA,然后将报告发给用户。
注意;此模型是为了方便理解所以才没有进一步抽象,而且可以抽取出来的标准化服务业也远远不止图中列出的这几种。
从上图中可以到出,通过将营销活动、交易、支付、结算、对账、物流等流程标准化,可以极大的减少开发成本,这也是系统解耦带来的最大的好处之一。
阿里很久以前就提出了大中台、小前端的组织架构思路,起对应的技术架构其本质和这里讨论的内容是一致的。
业务的特点是灵活且多变的,特别是当为了推广时,业务会要求技术上提供各种各样的玩法支持。如果每个业务都各自开发,将需要耗费巨大的成本和时间,严重拖慢业务的脚步。从另外一方面来看,交易、支付、结算、物流、积分等模块其实是标准化服务,不会因为卖实物商品和买虚拟服务就有所差别。
大中台、小前端思想的核心是:沉淀标准、统一、公共的模块,做为对外暴露的服务;业务通过自由组合这些底层服务,对用户提供各种各样的玩法。
将标准、统一、通用的服务(即公共业务)抽象并沉淀到底层,就是所谓的中台服务。非公业务即小前端,通过组装中台提供的服务来提供个性化服务,为业务提供灵活多变的支持。要实现大中台、小前端的架构,需要先做好公共服务的沉淀,而要做好服务的沉淀,需要先做好服务的解耦。
回忆之前在平安的时候,公司花了一年多的时间搭建、沉淀各种基础服务,后来运营同学开始通过营销活动来做拉新、提升日活等活动时,对于开发同学而言就变得很轻松,当初不管做什么活动,基本上都可以保证在一个月内上线(其实开发时间基本上就在5-7个工作日左右即可)。
目前支付宝、大众点评、美团上都提供买单功能,我个人是没有做过,但是我们可以讨论一下实现方案。买单页面如下图所示:
看到这个图的时候,大家是否觉得和订单确认页面比较相似,但是少了对于订单确认页面来说最为核心的一项内容:商品信息。
如果是我来设计这个功能,我会选择对给所有商家创建一个默认的商品,例如叫做:买单商品。上面的这个页面看作:以100元的价格购买一个“买单商品”的订单确认页,只是商品信息隐藏起来了,这样就可以复用促销、优惠券、交易、支付、结算、对账等系统,而基本不用做任何修改。
为什么选择为每个商家创建一个“买单商品”,而不是共用一个,或者创建订单时将商品空着?首先交易的核心就是买家在某一个时间以某个价格购买了卖家n个商品。其中商品是核心要素,并且商品是依赖于卖家存在的,所以需要为每个卖家创建一个“买单商品”。
当然这个商品的价格是浮动的,所以在db中会看到同一个商品的价格不同。
另外还有一点不同,从代码层面看,因为商品价格不固定,需要从上层传递下来,这一点和上面的方案是有差别的(上面的方案中,商品价格从商品中取的)。
和普通的订单确认页的另外一个差别是:多了一个不参与优惠的金额。这里可以看作是一个特殊的优惠。常见的优惠有:直降、折扣、满减、满折等,他们都是在原有订单金额上直接计算的;而这个特殊的优惠的定义可以看作是:需要在订单总金额的基础上排除一部分金额以后再计算优惠的特殊优惠。
使用支付宝、微信付款的时候,想必大家都遇到过随机立减的场景了。首先说明,这应该是付款渠道方的一种优惠方式,非支付平台不适合做这种优惠方案。以下是一种设计方案。
用户通过付款链接付款时,微信、支付宝受到支付请求以后,判断是否要随机立减,如果需要,那么生成一个随机立减的金额,于是得到待支付金额=总金额-随机立减金额。接着生成两条付款明细,一个明细从支付宝/微信对公账户中扣钱, 另一个明细从用户的(支付宝/微信)账户中扣钱。
提前说明一下:这里想讨论的红包是在淘宝、天猫上购物时使用的购物红包,而不是微信红包,支付宝红包。
购物红包在订单确认阶段使用,其本质是抵扣。
支付宝、微信红包是转账,从一个账户转移到另外一个账户,可以提现。
天猫购物红包列表,如下图所示
如下图所示,在天猫的订单确认页面中,用户确认订单时,可以选择使用红包抵扣。
一般带有效期。
红包的金额一般不固定。
没有使用限制,即一般不要求满x元可用。
目前见到平台上一个购物红包都属只能使用一次,例如100元的红包,如果此次购物总金额为50元,那么剩余部分也不能使用。(不确定是否所有的购物红包都是如此)从这一点上看,和优惠券有点类似。
有一个资金池子,每发一分钱的红包,就从这个资金池子中扣掉一分钱。
购物红包是平台刺激用户消费的一个很好的方式,它不会局限于订单金额,比优惠券灵活很多。需注意,后期对账、清算、结算可能和优惠券会有差别,这个问题暂不讨论。
目前据我了解到的情况是这样的:双十一活动期间,商家发红包需要提前在平台缴纳准备金,将准备金作为一个池子,每发一个红包,从池子中扣减一部分金额。淘宝、天猫平台的红包是否有这个池子我就不确定了,不过应该是有的。
从业务特点来看,很有必要有一个资金池。若没有这个资金池子,那么红包就变成了一条流水记录了,无法限制总的发放量,这对于营销活动是一件很危险的事情。
将使用红包的过程类比于积分抵扣过程,在交易中增加“红包抵扣”这一环节,就可以复用之前的整个流程。
至于订单取消、退款的时候是否需要退还红包,这个需要看业务的需要,如果需要,那么在MQ消息中将对应红包还原即可。一般都不会还原已使用的红包。
双十一的时候,我们经常看到天猫上有一种促销方式:提前下定金、双十一当天付尾款。如此一来可以预热活动,二来可以让整个活动期间的成交额都累计到双十一当天,方便他们冲量。
接下来思考一下实现方案。相比于之前讨论的交易流程,这里唯一区别在于本来是一次付款,现在改成两次付款。可以考虑以下实现方案。
创建订单过程:我会考虑将商品身上的特殊标签,在biz_order表中冗余起来,同时创建两条pay_order记录,一个是定金的,一个是尾款的。
付定金过程:通过定金的pay_order在支付系统中生成payment_order,接着走付款流程。
付尾款过程:通过尾款的pay_order在支付系统中生成payment_order,接着走付款流程,唯一需要注意的是,此时可能会有付款时间限制,例如必须是双十一当天可付。
需要注意的是:交易部分中,有一个核心的概念类目、商品类型,不过为了描述流程更加简单清晰,我刻意弱化了这个概念。注意:商品类型由商品类目决定,而交易流程中的差异化部分都是通过此商品类型来进行的。