有限狀態機
有限狀態機(Finite-state machine, FSM),是表示有限個狀態以及在這些狀態之間的轉移和動作等行爲的數學模型。主要用來是描述對象在它的生命週期內所經歷的狀態序列,以及如何響應來自外界的各種事件。
狀態機可歸納爲4個要素,即現態、條件、動作、新態。其中“現態”和“條件”是因,“動作”和“次態”是果。
- **現態:**是指當前所處的狀態。
- **事件:**又稱爲“條件”。當一個事件發生,將會觸發一個動作,或者執行一次狀態的遷移。
- **動作:**條件滿足後執行的動作。動作執行完畢後,可以遷移到新的狀態,也可以仍舊保持原狀態。動作不是必需的,當條件滿足後,也可以不執行任何動作,直接遷移到新狀態。
- **次態:**條件滿足後要遷往的新狀態。“次態”是相對於“現態”而言的,“次態”一旦被激活,就轉變成新的“現態”了。
例如,當訂單處於“待動作”狀態時,收到了支付平臺的支付成功回調通知,這就是訂單的現態和條件。這時候,系統會執行拆分訂單、推送訂單到供應鏈系統等操作,這就是“動作”。最後再把訂單的狀態標記爲已完成,這就是這一次狀態變更的“次態”。
按這個模式,整個訂單系統的邏輯都可以抽象成爲一系統的狀態、狀態變更的條件、狀態變更的動作。
訂單狀態轉移表
根據實際業務場景,按以下格式整理出一份訂單的狀態轉移表。這裏增加前置動作和後置動作兩個概念,前置動作是指在修改狀態之前要執行的動作,例如創建訂單前需要鎖定庫存。執行一系列前置動作時,如果某個動作失敗了,需要把前面的前置動作做回滾。後置動作是訂單指的是轉移到新狀態後自動觸發的事件,例如支付成功後的拆單和推送等。後置動作執行失敗也不需要做回滾,但是需要加上重試機制。
當前狀態 | 事件 | 前置動作 | 新狀態 | 後置動作 |
---|---|---|---|---|
未創建 | 確認下單 | 鎖定庫存 創建訂單 |
待支付 | |
待支付 | 支付成功 | 修改訂單狀態 | 拆分訂單 推送訂單 |
已支付 |
待支付 | 手動取消 | 修改訂單狀態 | 已取消 | |
待支付 | 超時取消 | 修改訂單狀態 | 已取消 |
核心代碼
狀態定義
枚舉定義全部的訂單狀態,訂單狀態應該儘量訂得很細,即使兩個狀態之間只有細微的區別也可以分開來。因爲在這個架構下,事件和動作都是可以重用的,不同的狀態轉移過程通過配置來區分就可以。
這裏的示例是隻用一個主狀態字段來表示所有的訂單狀態,也可以分成把多種狀態分別用不同的字段來表示,例如支付狀態、發貨狀態、售後狀態都用單獨的字段和枚舉變量。
class Orderstatus extends BaseEnum
{
//未創建
const NOT_CREATED = -200;
//售後狀態
const REFUND_APPLIED = -100
const REFUND_CONFIRM = -101
//關閉狀態
const CANCELED = 0;
//商城端狀態
const CREATED = 100;//已創建
const PAY_SUCCESS = 110;// 支付成功
const ORDER_SPLITED = 120;// 已拆單
//推送狀態
const PUSH_SUCCESS = 200;// 推送成功
const PUSH_FAILURE = 201;// 権送失敗
//物流狀態
const NOT_DELIVER = 300;// 未發貸
const PART_DELIVER = 301;// 已麥貸
const ALL_DELIVER = 302;// 全部宏貸
const SELF_MENTION = 303;// 到店百提
//完成狀態
const FINISHED = 400;
const REVIEWED = 410;
}
事件定義
首先定義事件的枚舉類
class OrderEventType extends BaseEnum
{
const CANCEL = 0; // 取消訂單
const CLOSE = 5; // 關閉訂單
const CREATE = 10; // 創建訂單
const PAY_SUCCESS = 110; // 支付成功
const PAY_FAILURE = 120; // 支付失敗
const SHIPPING_DELIVER = 210; // 訂單發貨
const SHIPPING_RECEIPT = 220; // 確認收貨
}
然後每個事件寫一個類,繼承事件基類
class PaySuccess extends OrderEvent
{
protected $eventType = OrderEventType::PAY_SUCCESS;
}
class OrderEvent
{
protected $eventType = '';
protected $orderId = 0;
protected $eventData = [];
public function getEventType()
{
return $this->eventType;
}
public function getOrderId(): int
{
return $this->orderId;
}
public function setOrderId(int $orderId): void
{
$this->orderId = $orderId;
}
public function getEventData(): array
{
return $this->eventData;
}
public function setEventData(array $eventData): void
{
$this->eventData = $eventData;
}
}
操作類
前置動作、後置動作、回滾操作都繼承相同的基類OrderOperation,分別放到不同的目錄下。
class OrderOperation
{
protected $event = null;
protected $order = null;
protected $data = [];
public function handle(OrderEvent $event, $order = null)
{
$this->setEvent($event);
$this->setOrder($order);
// 獲取傳的參數
$this->data = $event->getEventData();
}
protected function throwException($message = '', $code = 0)
{
$exception = new StateMachineException($message, $code);
$exception->setOrderEvent($this->event);
$exception->setOrderModel($this->order);
$operationFile = $exception->getTrace()[0]['file'];
$operationName = basename($operationFile, '.php');
$exception->setOperation('App\Domain\PlatformOrder\Services\StateMachine\BeforeOperation\\' . $operationName);
throw $exception;
}
}
class LockSaleableStock extends OrderOperation
{
public function handle(OrderEvent $event, $order = null)
{
parent::handle($event, $order);
//具體處理邏輯
...
}
}
狀態轉移配置
首先用事件類型作爲數組的key進行配置,對應的value是可以響應這個事件的狀態列表,如果一個事件可能有多種狀態轉移方式,就需要配置多個狀態定義。
狀態的conditions是指可以響應這個Event的狀態,配置的多個狀態是“或”關係。
private $statusTransition = [
// 支付成功
OrderEventType::PAY_SUCCESS => [
[
'conditions' => [OrderStatus::CREATED], //僅未支付的狀態才允許觸發,
'beforeOperation' => [
ChangeSaleableStock::class, // 扣庫存
],
// 後置操作
'afterOperation' => [
AddSaleCount::class
],
// 新狀態變更
'newStatus' => OrderStatus::PAY_SUCCESSED
],
],
];
核心代碼
<?php
class StateMachine
{
private $statusTransition = [
...
];
public function handle(OrderEvent $event)
{
$this->event = $event;
$matched = false;
if (isset($this->statusTransition[$this->event->getEventType()])) {
$transitions = $this->statusTransition[$this->event->getEventType()];
// 事件支持
$matched = null;
/** @var Order $order */
$orderId = $this->event->getOrderId();
if ($orderId) {
$this->order = $this->subOrderModel->find($orderId);
}
foreach ($transitions as $transition) {
if ($this->checkCondition($transition['conditions'])) {
// 事件支持 並且 狀態支持
$matched = true;
try {
// 更新狀態的前置操作
$this->callOperation($transition['beforeOperation']);
// 更新訂單狀態
if ($this->order && !empty($transition['newStatus'])) {
$this->order->update($transition['newStatus']);
}
// 更新狀態的後置操作
$this->callOperation($transition['afterOperation']);
} catch (StateMachineException $exception) {
$this->beforeRollback($transition['beforeOperation'], $exception);
throw $exception;
}
break;
}
}
}
if ($matched === false) {
throw new StateMachineException("訂單狀態機不支持的事件類型");
} elseif ($matched === null) {
throw new StateMachineException("該訂單狀態不允許當前操作");
}
}
protected function beforeRollback($operation, $exception)
{
// 檢查哪些需要回滾的
$rollbackOperation = [];
foreach ($operation as $op) {
if ($op !== $exception->getOperation()) {
array_unshift($rollbackOperation, str_replace('BeforeOperation', 'RollbackOperation', $op));
} else {
break;
}
}
// 回滾操作
foreach ($rollbackOperation ?? [] as $op) {
if (class_exists($op)) {
$operationService = app($op);
$operationService->handle($this->event, $this->order);
}
}
}
protected function checkCondition($conditions)
{
if (empty($conditions) || empty($this->order)) {
return true;
}
foreach ($conditions as $condition) {
$matchCount = 0;
foreach ($condition as $field => $value) {
if ($this->order->getAttribute($field) === $value) {
$matchCount++;
} else {
break;
}
}
if ($matchCount == count($condition)) {
return true;
}
}
return false;
}
protected function callOperation($operation = [])
{
foreach ($operation ?? [] as $op) {
/** @var OrderOperation $operationService */
$operationService = app($op);
$operationService->handle($this->event, $this->order);
}
}
}