使用FSM进行复杂订单状态管理(3)

tech

This article was last updated on <span id="expire-date"></span> days ago, the information described in the article may be outdated.

我们接着上面一篇文章,这次我们需要利用有限状态机来实战的解决订单状态的管理问题。

订单状态设计

首先我们在实际业务中不可能像是第一篇文章一样仅仅涉及到两个订单状态的切换,在不考虑退/换货流程的情况下,我设计了一套较为完整的订单状态流程,下面以一张状态图来表示:

这幅图中的绿色节点表示订单的起始状态:订单已经创建;儿两个红色节点表示订单可能的终止状态:订单完成/订单关闭

请注意,订单的完成与关闭是完全不同的两个概念,完成是指用户走完的所有的支付、发货、收货流程;而关闭则倾向于描述订单出现了意外的情况,例如:

  • 用户主动关闭了订单,而未付款
  • 用户付款超时
  • 用户在订单完成之后完成了退款操作

而图中的圆角矩形用于表示状态;菱形用于表示判断;箭头用于表示事件及状态转移。

我们可以发现,在一张设计良好的状态图上,所有的状态之间都是可以使用事件来进行抽象的,而在某些情况下我们需要业务层进行一定的判断来指定发生事件,用以驱动状态转移。

使用FSM建模

我们有了以上的订单状态概念之后,可以开始使用上一篇文章的有限状态机模型构建状态/事件类了。

事件类

首先,我们考虑事件类该如何构造。

因为每个事件都有自己的发生时间和操作码,我们不能直接将事件实例化,而是应该在事件发生时对其实例化,在状态转移之间传递他们的实例。

我们可以重载 fsm.Event 类,并定义 action 接口,这样就可以实现定义一个事件了,下面以用户正在付款作为示例(在用户点击了付款按钮之后触发):

1
2
3
4
5
6
7
8
9
10
11
12
class Paying(fsm.Event):
"""用户正在付款"""
description: str = settings.Description.Paying

def action(self, payments: Dict[payment.AbstractPayment, float]) -> NoReturn:
"""用户支付的支付方式以及金额进行保存"""

items: Dict[str, float] = dict() # 支付的详情信息存储
for method, fee in payments.items():
items[str(method)] = fee

self.append(settings.ExtraInformation.Payments, items)

我们在事件中保存了支付的详细信息,用以与支付平台对接以及后期给用户展示。

有了事件我们可以开始考虑状态类了。

状态类

之后,我们考虑状态类如何构建。

根据上面FSM的模型,我们需要重载 fsm.State 模型来指定状态的进入/退出接口,之后实例化重载类,获得一个状态实例,例如订单正在运送这个状态:

1
2
3
4
5
6
7
8
9
10
11
class _Shipping(fsm.State):
"""状态 - 订单正在运送"""

def enter(self, reason: fsm.Event):
"""进入运送状态保存运单信息并创建详细信息列表"""
self.extra = reason.extra
self.extra[settings.ExtraInfomation.ShipDetails] = list()


Shipping = _Shipping(5, settings.Description.Shipping)
Shipping.add(events.Delieverd)

在订单正在运送状态这个状态中,我们希望保存事件,也就是原因到 extra 中,并且我们将要初始化一个 ShipDetails 列表变量,用于保存物流的一系列信息通知。我们经常可以在电商后台看到这样的物流状态:

  • 订单已经开始配送 - 承运人[xx快递] - 订单号:xxxx
  • 订单已经到达xxx配送站
  • ……

我们就可以保存在这样的列表中返回给用户。

但是有些状态可能并不关心状态转移的原因(也就是事件),他们也并不需要重载,直接实例化 `fsm.State` 就可以,例如订单已经创建这个状态:

1
2
3
4
# 状态 - 订单已经创建
Created = fsm.State(0, settings.Description.Created)
Created.add(events.Confirm)
Created.add(events.UserClose)

这样我们可以更简单的构建状态集合,下来我们需要一个状态机(也就是状态管理器)来进行状态管理了。

状态管理器

状态管理器可以直接重载 fsm.Machine,最重要的是需要根据上面的状态图来进行状态转移表的设计,下面给出代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class StatusManager(fsm.Machine):
"""创建一个订单状态管理器"""

def __init__(self, orderid: str) -> NoReturn:
"""
订单状态管理器构造函数:
0. 根据订单id对管理器命名
1. 初始化状态转移对应表
2. 初始化进入状态
"""
super().__init__(str(orderid))

# 初始化状态转移对应表
for record in _TransferTable:
self.add(*record)

def start(self) -> NoReturn:
"""开始从订单创建开始"""
super().start(status.Created)

可以看到,我们重写了 start 函数,因为订单的状态只能以订单被创建开始;之后我们在类构造函数中将状态转移表初始化,这里我使用了二维数组来描述状态转移表 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 状态转移表
_TransferTable = (
(status.Created, events.Confirm, status.Confirmed),
(status.Created, events.UserClose, status.Closed),
(status.Confirmed, events.Paying, status.Paying),
(status.Paying, events.PayingSuccess, status.Paid),
(status.Paying, events.PayingFailed, status.PayFailed),
(status.PayFailed, events.OrderTimedOut, status.Closed),
(status.PayFailed, events.OrderRetry, status.Created),
(status.Paid, events.Shipped, status.Shipping),
(status.Shipping, events.Delieverd, status.Delieverd),
(status.Delieverd, events.Recieved, status.Completed),
(status.Delieverd, events.RecieveTimingExcced, status.Completed),
(status.Completed, events.RequestRefund, status.RefundReviewing),
(status.RefundReviewing, events.RefundApproved, status.Refunding),
(status.RefundReviewing, events.RefundDenied, status.Completed),
(status.Refunding, events.RefundSuccess, status.Closed),
(status.Refunding, events.RefundFailed, status.Completed)
)

总结

以上工作都完成之后,在使用时,我们可以直接实例化管理器类,就可以获得一个订单状态管理器,使用 handle 接口将事件实例传入,我们就可以实现订单状态的管理了。

有的同学可能会觉得这样问题更复杂了。事实上,我才开始尝试过使用 if 条件分支的方式来管理状态,那样是完全行不通的,写一段时间之后就会被分支给搞的晕头转向。

并且我们这样做是更有扩展性的,之后如果要添加/改变状态仅仅需要增加状态、事件、改变转移表即可。我甚至不敢想象使用条件分支判断的代码该要怎么维护,如果没有注释,那估计就是传说中的屎山 吧~

以上就是FSM实现订单状态管理的全部内容了,希望大家能够喜欢。

Author: 桂小方

Permalink: https://init.blog/1836/

文章许可协议:

如果你觉得文章对你有帮助,可以 支持我

Comments