前面实现购物车模式的时候我们说了,购物车作为商品和订单的中间角色,让用户有机会一次性选购多个商品后再进行下单结账。
那么用户在把想要选购的商品加入购物车后,接下来的产品流程就到了进行购物结算了,等用户支付后,商家或者购物平台会安排进行物流发货,虚拟产品则是为用户开通某些权益。
订单结算这个产品流程其实包括两个子流程
- 购物项创建订单
- 支付订单
订单模块功能分析
首先我们再次看一下需求分析章节中,我们分析出来的业务结构,这里我们重点关注订单模块,以及它跟其他模块靠什么关联。
图片
可以看到订单中,订单是业务的聚合根,其下还有订单商品明细和订单地址。为什么要有后面两部分呢?在上图的注释里我们做了标注:它们是作为订单的快照信息,因为如果订单直接跟商品ID和用户地址ID关联的话,假如未来商品调整了价格、下架了亦或是用户搬家更新了地址,万一哪天“秋后算账”看看之前的订单,就会出现订单信息跟用户自己当初购买时信息不一致的情况。
订单模块在付款之前的功能如下(支付功能等下节再分析):
- 创建订单
- 订单查询
订单列表
订单详情
- 修改订单 (一般C端此时只允许用户取消订单)
创建订单
我们的购物车数据是保存在服务端的数据表中的,因为这个数据保存在服务端,所以创建订单这个功能客户端提交上来的数据就只需要把要生成订单进行结算的购物项目ID和用户的地址信息ID提交上来即可。
所以我们先在api/request/order.go 中,定义用户创建订单的请求格式如下:
服务端拿到参数后可以自己去检索对应的购物项信息,然后再去获取对应的商品信息,进行结算相关的计算。这样整个过程不需要客户端过多参与,能最大限度地保证用户数据的安全。
所以我们在Order的应用服务中的逻辑如下:
- 首先调用CartDomainSvc通过购物项ID获取用户添加在购物车中的购物项,该方法 GetCheckedCartItems 是我们在购物车模块中已经实现好的,通过它能获取购物项信息,其中会包括具体的商品信息、价格、购买数量等。
- 通过用户的地址信息ID调用UserDomainSvc获取用户的详细地址信息。
- 拿到创建订单所依赖的信息后,调用OrderDomainSVC 去创建订单。
OrderDomainSVC中创建订单的实现如下:
代码较长这里简单说下里面的实现步骤:
- 首先我们依赖上节课使用职责链实现的CartBillChecker来计算一下订单商品的总价、优惠金额等结算信息
- 设置用户订单的UserId、OrderNo、订单金额-BillMoney、实际支付金额-PayMoney、订单状态。
- 开启数据库事务
操作一:保存订单信息到数据表
操作二:从用户购物车中删除已下单商品
操作三:如使用了优惠券,锁定优惠券,等支付成功后再核销(项目没有,这里Mock)
操作四:如参与了满减活动、记录相关信息
操作五:减少订单购买商品的库存,因为会锁行记录, 把这一步放到创建订单步骤的最后, 减少行记录加锁的时间
- 提交/回滚事务。
在实现代码中我特地使用了GORM手动管理事务的方法,在用户地址信息维护章节中我已经演示过了GORM自动管理事务的db.Transaction方法,其实我这里用的就是db.Transaction 的内部实现逻辑。
大家可以根据自己的喜好,手动或者自动管理事务,如果让我选,我还是推荐GORM自己管理事务,别太相信自己,毕竟万一漏掉一点代码就是一个BUG,到时候甩锅都不好甩给GORM,哈哈哈哈。
重启项目后我们,发起创建订单请求把购物车中的购物项下单, 请求结果如下。
图片
整个创建订单过程中生成订单号、还有其他的一些代码大家就去项目里看吧,这里不贴这么多了。接下来我们来看订单查询。
订单查询
关于订单查询,其实主要有一点需要注意,就是我们订单前台显示状态和订单在系统中真正的状态流转是有一丢丢不一样的,说大白话就是数据库里订单状态的枚举值跟用户在前台看到的状态值是不一样的。
针对订单状态,我们在项目的 common/enum/order.go 中定义了如下枚举值和数据表里的订单状态值一一对应。
但是用户在前台看到自己的订单状态,往往是像下面这样。
图片
所以我们在给客户端返回用户的订单时,关于订单状态的前台展示要做一下转换才行,尽量不要让客户端拿到所有状态再去转换,因为如果前后端都有逻辑,维护起来或者做自动化测试这些都会很困难。
这里我对订单状态的枚举值跟前台显示状态做了如下映射,让我们在返回响应给客户端前可以把订单状态转换为前台展示状态:
接下来我们看一下,订单查询相关的功能实现。