訂單及其狀態機的設計實現
狀態機簡介:
狀態機是有限狀態自動機的簡稱,是現實事物運行規則抽象而成的一個數學模型。【規則的抽象】
有限狀態機一般都有以下特點:
(1)可以用狀態來描述事物,並且任一時刻,事物總是處於一種狀態;
(2)事物擁有的狀態總數是有限的;
(3)通過觸發事物的某些行為,可以導致事物從一種狀態過渡到另一種狀態;
(4)事物狀態變化是有規則的,A狀態可以變換到B,B可以變換到C,A卻不一定能變換到C;
(5)同一種行為,可以將事物從多種狀態變成同種狀態,但是不能從同種狀態變成多種狀態。
狀態機這種描述客觀世界的方式就是將事物抽象成若干狀態,然後所有的事件和規則導致事物在這些狀態中遊走。最終使得事物「自圓其說」。
很多通訊協議的開發都必須用到狀態機;一個健壯的狀態機可以讓你的程式,不論發生何種突發事件都不會突然進入一個不可預知的程式分支。
- 狀態機示例:

四大概念:
| 狀態(state) |
一個狀態機至少要包含兩個狀態。 分為:現態(源狀態)、次態(目標狀態) 狀態可以理解為一種結果,一種穩態形式,沒有擾動會保持不變的。
狀態命名形式: 1.副詞+動詞;例如:待審批、待支付、待收貨 這種命名方式體現了:狀態機就是事件觸髮狀態不斷遷徙的本質。表達一種待觸發的感覺。 2.動詞+結果;例如:審批完成、支付完成 3.已+動詞形式;例如:已發貨、已付款 以上兩種命名方式體現了:狀態是一種結果或者穩態的本質。表達了一種已完成的感覺。
角色很多的時候,為了表示清晰,可以加上角色名:例如:待財務審批、主管已批准 命名考慮從用戶好理解角度出發。
|
|
事件(event) or 觸發條件 |
又稱為「條件」,就是某個操作動作的觸發條件或者口令。當一個條件滿足時,就會觸發一個動作,或者執行一次狀態遷徙 這個事件可以是外部調用、監聽到消息、或者各種定時到期等觸發的事件。 對於燈泡,「打開開關」就是一個事件。 條件命名形式:動詞+結果;例如:支付成功、下單時間>5分鐘 |
| 動作(action) |
事件發生以後要執行動作。例如:事件=「打開開關指令」,動作=「開燈」。一般就對應一個函數。 條件滿足後執行動作。動作執行完畢後,可以遷移到新的狀態,也可以仍舊保持原狀態。 動作不是必需的,當條件滿足後,也可以不執行任何動作,直接遷移到新狀態。 那麼如何區分「動作」和「狀態」? 「動作」是不穩定的,即使沒有條件的觸發,「動作」一旦執行完畢就結束了; 而「狀態」是相對穩定的,如果沒有外部條件的觸發,一個狀態會一直持續下去。 |
| 變換(transition) |
即從一個狀態變化到另外一個狀態 例如:「開燈過程」就是一個變化 |
狀態機其他表達方式:
狀態機的設計:
資訊系統中有很多狀態機,例如:業務訂單的狀態。
狀態機的設計存在的問題:什麼是狀態?到底有多少個狀態?要細分到什麼程度?
資訊系統是現實世界一種抽象和描述。而業務領域中那些已經發生的事件就是事實,資訊系統就是將這些事實以資訊的形式存儲到資料庫中,即:資訊就是一組事實
資訊系統就是存儲這些事實,對這些事實進行管理與追蹤,進而起到提供提高工作效率的作用。
資訊系統就是記錄已經發生的事實,資訊系統中的狀態機基本和事實匹配。即:標識某個事實的完成度。
業務系統,根據實際業務,具體會有哪些發生的事實需要記錄,基本這些事實就至少對應一個狀態。需要記錄的事實就是一種穩態,一種結果。
例如:【待支付】->【已支付】->【已收貨】->【已評價】
這些都是系統需要記錄的已發生的客觀事實。而這些事實就對應了狀態,而發生這些事實的事件就對應了觸髮狀態機的轉換的事件。
根據自己的業務實際進行分析,並畫出狀態圖即可。
狀態機實現方式:狀態模式
下面使經典的自動販賣機例子來說明狀態模式的用法,狀態圖如下:

分析一個這個狀態圖:
a、包含4個狀態(我們使用4個int型常量來表示)
b、包含3個暴露在外的方法(投幣、退幣、轉動手把、(發貨動作是內部方法,售賣機未對外提供方法,售賣機自動調用))
c、我們需要處理每個狀態下,用戶都可以觸發這三個動作。
我們可以做沒有意義的事情,在【未投幣】狀態,試著退幣,或者同時投幣兩枚,此時機器會提示我們不能這麼做。
實現邏輯:
任何一個可能的動作,我們都要檢查,看看我們所處的狀態和動作是否合適。

/** * 自動售貨機 * * */ public class VendingMachine { /** * 已投幣 */ private final static int HAS_MONEY = 0; /** * 未投幣 */ private final static int NO_MONEY = 1; /** * 售出商品 */ private final static int SOLD = 2; /** * 商品售罄 */ private final static int SOLD_OUT = 3; /** * 商品數量 */ private int count = 0; // 當前狀態,開機模式是沒錢 private int currentStatus = NO_MONEY; // 開機設置商品數量,初始化狀態 public VendingMachine(int count) { this.count = count; if (count > 0) { currentStatus = NO_MONEY; } } /** * 投入硬幣,任何狀態用戶都可能投幣 */ public void insertMoney() { switch (currentStatus) { case NO_MONEY: currentStatus = HAS_MONEY; System.out.println("成功投入硬幣"); break; case HAS_MONEY: System.out.println("已經有硬幣,無需投幣"); break; case SOLD: System.out.println("請稍等..."); break; case SOLD_OUT: System.out.println("商品已經售罄,請勿投幣"); break; } } /** * 退幣,任何狀態用戶都可能退幣 */ public void backMoney() { switch (currentStatus) { case NO_MONEY: System.out.println("您未投入硬幣"); break; case HAS_MONEY: currentStatus = NO_MONEY; System.out.println("退幣成功"); break; case SOLD: System.out.println("您已經買了糖果..."); break; case SOLD_OUT: System.out.println("您未投幣..."); break; } } /** * 轉動手把購買,任何狀態用戶都可能轉動手把 */ public void turnCrank() { switch (currentStatus) { case NO_MONEY: System.out.println("請先投入硬幣"); break; case HAS_MONEY: System.out.println("正在出商品...."); currentStatus = SOLD; dispense(); break; case SOLD: System.out.println("連續轉動也沒用..."); break; case SOLD_OUT: System.out.println("商品已經售罄"); break; } } /** * 發放商品 */ private void dispense() { switch (currentStatus) { case NO_MONEY: case HAS_MONEY: case SOLD_OUT: throw new IllegalStateException("非法的狀態..."); case SOLD: count--; System.out.println("發出商品..."); if (count == 0) { System.out.println("商品售罄"); currentStatus = SOLD_OUT; } else { currentStatus = NO_MONEY; } break; } } }

// 測試自動售賣機 class Test { public static void main(String[] args) { VendingMachine machine = new VendingMachine(10); machine.insertMoney(); machine.backMoney(); System.out.println("-----------"); machine.insertMoney(); machine.turnCrank(); System.out.println("-----------"); machine.insertMoney(); machine.insertMoney(); machine.turnCrank(); machine.turnCrank(); machine.backMoney(); machine.turnCrank(); } }
使用if-else/switch的方式實現狀態有如下問題:
- 沒有遵守【開閉】原則,沒有【封裝變化】,所以沒有彈性,應對需求變更非常吃力。
例如:現在增加一個狀態。每個方法都需要添加if-else語句。
- 狀態如何轉換看得不是很清楚,隱藏在if-else/switch邏輯中。
升級策略:
【封裝變化】,局部化每個狀態的行為,將每個狀態的行為放到各自類中,每個狀態只要實現自己的動作就可以了。
販賣機只要將動作委託給代表當前狀態的狀態對象即可。


public interface State { /** * 放錢 */ public void insertMoney(); /** * 退錢 */ public void backMoney(); /** * 轉動曲柄 */ public void turnCrank(); /** * 出商品 */ public void dispense(); }

public class NoMoneyState implements State { private VendingMachine machine; public NoMoneyState(VendingMachine machine) { this.machine = machine; } @Override public void insertMoney() { System.out.println("投幣成功"); machine.setState(machine.getHasMoneyState()); } @Override public void backMoney() { System.out.println("您未投幣,想退錢?..."); } @Override public void turnCrank() { System.out.println("您未投幣,想拿東西么?..."); } @Override public void dispense() { throw new IllegalStateException("非法狀態!"); } }

public class HasMoneyState implements State { private VendingMachine machine; public HasMoneyState(VendingMachine machine) { this.machine = machine; } @Override public void insertMoney() { System.out.println("您已經投過幣了,無需再投...."); } @Override public void backMoney() { System.out.println("退幣成功"); machine.setState(machine.getNoMoneyState()); } @Override public void turnCrank() { System.out.println("你轉動了手把"); machine.setState(machine.getSoldState()); } @Override public void dispense() { throw new IllegalStateException("非法狀態!"); } }

public class SoldOutState implements State { private VendingMachine machine; public SoldOutState(VendingMachine machine) { this.machine = machine; } @Override public void insertMoney() { System.out.println("投幣失敗,商品已售罄"); } @Override public void backMoney() { System.out.println("您未投幣,想退錢么?..."); } @Override public void turnCrank() { System.out.println("商品售罄,轉動手把也木有用"); } @Override public void dispense() { throw new IllegalStateException("非法狀態!"); } }

public class SoldState implements State { private VendingMachine machine; public SoldState(VendingMachine machine) { this.machine = machine; } @Override public void insertMoney() { System.out.println("正在出貨,請勿投幣"); } @Override public void backMoney() { System.out.println("正在出貨,沒有可退的錢"); } @Override public void turnCrank() { System.out.println("正在出貨,請勿重複轉動手把"); } @Override public void dispense() { machine.releaseBall(); if (machine.getCount() > 0) { machine.setState(machine.getNoMoneyState()); } else { System.out.println("商品已經售罄"); machine.setState(machine.getSoldOutState()); } } }

public class VendingMachine { private State noMoneyState; private State hasMoneyState; private State soldState; private State soldOutState; private State winnerState ; private int count = 0; private State currentState = noMoneyState; public VendingMachine(int count) { noMoneyState = new NoMoneyState(this); hasMoneyState = new HasMoneyState(this); soldState = new SoldState(this); soldOutState = new SoldOutState(this); winnerState = new WinnerState(this); if (count > 0) { this.count = count; currentState = noMoneyState; } } //將這些動作委託給當前狀態. public void insertMoney() { currentState.insertMoney(); } public void backMoney() { currentState.backMoney(); } // 機器不用提供dispense動作,因為這是一個內部動作.用戶不可以直 //接要求機器發放糖果.我們在狀態對象的turnCrank()方法中調用 //dispense方法; //dispense無論如何,即使在nomoney狀態也會被執行. //讓不合法的情形下,dispense拋出異常處理。 public void turnCrank() { currentState.turnCrank(); currentState.dispense(); } public void releaseBall() { System.out.println("發出一件商品..."); if (count != 0) { count -= 1; } } public void setState(State state) { this.currentState = state; } //getter setter omitted ... }
我們之前說過,if-else/switch實現方式沒有彈性,那現在按照這種實現模式,需求變更修改起來會輕鬆點嗎?
紅色部分標記了我們的需求變更:當用戶每次轉動手把的時候,有10%的幾率贈送一瓶。

實現方式:
我們遵守了【開閉】原則,只要新建一個WinnerState的類即可。然後有限的修改has_money的轉向即可。
為什麼WinnerState要獨立成一個狀態,其實它和sold狀態一模一樣。我把程式碼寫在SoldState中不行嗎?
- 第一個原因就是上面說的資訊系統的本質就是記錄【事實】,中獎是需要記錄的事實,它應該是一個狀態。
- 第二個原因:【單一職責】問題,我們一個類的責任是明確的。
如果sold需求變化不一定影響到winner程式碼實現,winner需求變化時,也不一定要修改sold,比如促銷方案結束了,中獎概率變了等。
如果他們的變化不是一定互相影響到彼此的,那我們就該將他們分離,即是【隔離變化】也是遵守【單一職責】的原則。

public class WinnerState implements State { private VendingMachine machine; public WinnerState(VendingMachine machine) { this.machine = machine; } @Override public void insertMoney() { throw new IllegalStateException("非法狀態"); } @Override public void backMoney() { throw new IllegalStateException("非法狀態"); } @Override public void turnCrank() { throw new IllegalStateException("非法狀態"); } @Override public void dispense() { System.out.println("你中獎了,恭喜你,將得到2件商品"); machine.releaseBall(); if (machine.getCount() == 0) { System.out.println("商品已經售罄"); machine.setState(machine.getSoldOutState()); } else { machine.releaseBall(); if (machine.getCount() > 0) { machine.setState(machine.getNoMoneyState()); } else { System.out.println("商品已經售罄"); machine.setState(machine.getSoldOutState()); } } } }

public class HasMoneyState implements State { private VendingMachine machine; private Random random = new Random(); public HasMoneyState(VendingMachine machine) { this.machine = machine; } @Override public void insertMoney() { System.out.println("您已經投過幣了,無需再投...."); } @Override public void backMoney() { System.out.println("退幣成功"); machine.setState(machine.getNoMoneyState()); } @Override public void turnCrank() { System.out.println("你轉動了手把"); int winner = random.nextInt(10); if (winner == 0 && machine.getCount() > 1) { machine.setState(machine.getWinnerState()); } else { machine.setState(machine.getSoldState()); } } @Override public void dispense() { throw new IllegalStateException("非法狀態!"); } }
總結狀態模式:
狀態模式:允許對象在內部狀態改變時改變它的行為,對象看起來好像修改了他的類。
解釋:
狀態模式將狀態封裝成為獨立的類,並將動作委託到代表當前狀態的對象。
所以行為會隨著內部狀態改變而改變。
我們通過組合簡單引用不同狀態對象來造成類改變的假象.

| 狀態模式 | 策略模式 |
|
1.行為封裝的n個狀態中,不同狀態不用行為。 2.context的行為委託到不同狀態中。 3.[當前狀態]在n個狀態中遊走,context的行為也隨之[當前狀態]的改變而改變。 4.用戶對context的狀態改變渾然不知。 5.客戶不會直接和state交互,只能通過context暴露的方法交互,state轉換是context內部事情。 6.state可以是介面也可以是抽象類,取決於有沒公共功能可以放進抽象類中。抽象類方便,因為可以後續加方法。 可以將重複程式碼放入抽象類中。例如:"你已投入25元,不能重複投" 這種通用程式碼放入抽象類中。 7.context可以決定狀態流轉,如果這個狀態流轉是固定的,就適合放在context中進行。但是如果狀態流轉是動態的就適合放在狀態中進行。 例如通過商品的剩餘數目來決定流向[已售完]或[等待投幣],這個時候放在狀態類中,因為dispense要根據狀態判斷流轉。 這個寫法決定了,有新需求時候,你是改context還是改state類。 8.可以共享所有的state對象,但是需要修改context的時候時候,需要handler中傳入context引用
|
1.context主動指定需要組合的策略對象是哪一個。
2.可以在啟動的時候通過工廠動態指定具體是哪個策略對象,但是沒有在策略對象之間遊走,即:只組合了一個策略對象。 3.策略作為繼承之外一種彈性替代方案。因為繼承導致子類繼承不適用的方法,且每個類都要維護,策略模式通過不同對象組合來改變行為。 4.策略模式聚焦的是互換的演算法來創建業務。 |
狀態機典型應用:訂單狀態控制


/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; /*!40101 SET NAMES utf8 */; /*!50503 SET NAMES utf8mb4 */; /*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */; /*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */; /*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */; CREATE TABLE IF NOT EXISTS `tbl_sapo_biz_param` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵', `create_time` datetime(3) NOT NULL COMMENT '創建時間', `biz_type` varchar(255) NOT NULL COMMENT '業務類別(使用時定義)', `biz_code` varchar(255) NOT NULL COMMENT '業務數據編碼', `para_key` varchar(255) NOT NULL COMMENT '參數key', `para_value` varchar(255) NOT NULL COMMENT '參數值', `para_desc` varchar(255) NOT NULL COMMENT '參數描述', `level` varchar(64) DEFAULT NULL COMMENT '參數級別;ABCD', PRIMARY KEY (`id`), UNIQUE KEY `uni_idx_biz_param_biz_type_biz_code_para_key` (`biz_type`,`biz_code`,`para_key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='業務參數表'; CREATE TABLE IF NOT EXISTS `tbl_sapo_order` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵', `order_no` varchar(100) NOT NULL COMMENT '訂單號', `create_time` datetime(3) NOT NULL COMMENT '創建時間', `last_update_time` datetime(3) DEFAULT NULL COMMENT '最後更新時間', `xx_code` varchar(100) NOT NULL COMMENT '業務實體code', `status` int(10) unsigned NOT NULL DEFAULT 3 COMMENT '狀態:3-已取消,4-已接單,5-待支付,6-已支付,7-待執行,8-執行中,9-執行完成', `cancel_reason` int(10) unsigned DEFAULT NULL COMMENT '訂單取消的原因。1-未支付(支付取消或過期),2-未準備(準備失敗),3-未執行(啟動過期),4-未完成(執行中取消)', `refund_count` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '退款次數', `expire_time` datetime(3) DEFAULT NULL ON UPDATE current_timestamp(3) COMMENT '失效時間', `invoice_flag` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '是否開過發票標識,0-未開發票,1-已開發票', `total_fee` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '訂單總價,單位:分', `pay_fee` int(10) unsigned NOT NULL DEFAULT 0 COMMENT '最終支付金額(單位:分)', `trace_no` varchar(255) DEFAULT NULL COMMENT '日誌追蹤標識', PRIMARY KEY (`id`), UNIQUE KEY `uni_idx_order_order_no` (`order_no`), KEY `idx_order_xx_code` (`xx_code`), KEY `idx_order_expire_time` (`expire_time`), CONSTRAINT `fk_order_xx_code` FOREIGN KEY (`xx_code`) REFERENCES `tbl_sapo_xx` (`code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='業務訂單'; CREATE TABLE IF NOT EXISTS `tbl_sapo_order_coupon` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵', `create_time` datetime(3) NOT NULL COMMENT '創建時間', `user_coupon_uuid` varchar(100) NOT NULL COMMENT '用戶優惠券uuid,tbl_sapo_user_coupon表uuid', `order_id` int(10) unsigned NOT NULL COMMENT '業務訂單id,tbl_sapo_order表id', `status` int(10) unsigned NOT NULL DEFAULT 1 COMMENT '狀態:0-無效,1-有效', PRIMARY KEY (`id`), KEY `idx_order_coupon_user_coupon_uuid` (`user_coupon_uuid`), KEY `idx_order_coupon_order_id` (`order_id`), CONSTRAINT `fk_order_coupon_order_id` FOREIGN KEY (`order_id`) REFERENCES `tbl_sapo_order` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='訂單優惠券'; CREATE TABLE IF NOT EXISTS `tbl_sapo_order_status_log` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵', `create_time` datetime(3) NOT NULL COMMENT '創建時間', `order_type` int(10) unsigned NOT NULL DEFAULT 4 COMMENT '訂單類型:4-業務訂單,5-支付訂單,6-退款訂單', `order_id` int(10) unsigned NOT NULL COMMENT '訂單id', `status` int(10) unsigned NOT NULL COMMENT '狀態', `remark` varchar(255) DEFAULT NULL COMMENT '操作備註', PRIMARY KEY (`id`), KEY `idx_order_status_log_order_id` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='訂單狀態記錄'; CREATE TABLE IF NOT EXISTS `tbl_sapo_pay` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '物理主鍵', `requester_no` varchar(255) NOT NULL COMMENT '請求流水號', `create_time` datetime(3) NOT NULL COMMENT '創建時間', `last_update_time` datetime(3) DEFAULT NULL COMMENT '最後更新時間', `order_id` int(10) unsigned NOT NULL COMMENT '業務訂單標識,tbl_sapo_order表id', `status` int(10) unsigned NOT NULL DEFAULT 4 COMMENT '狀態:3-撤銷,4-申請,5-待支付,6-已支付,7-失敗', `expire_time` datetime(3) DEFAULT NULL ON UPDATE current_timestamp(3) COMMENT '失效時間', `payment_complete_time` datetime(3) DEFAULT NULL COMMENT '支付完成時間', `remark` varchar(255) DEFAULT NULL COMMENT '備註', `account_no` varchar(255) NOT NULL COMMENT '統一支付渠道', `requester_code` varchar(255) NOT NULL COMMENT '統一支付服務編碼', `body` varchar(255) DEFAULT NULL COMMENT '統一支付商品描述', `total_fee` int(10) unsigned NOT NULL COMMENT '金額(單位:分)', `trade_type` varchar(64) DEFAULT NULL COMMENT '交易類型', `pay_info` varchar(1024) DEFAULT NULL COMMENT '支付參數', `payment_pay_no` varchar(255) DEFAULT NULL COMMENT '統一支付平台支付流水號(請求支付後返回)', PRIMARY KEY (`id`), UNIQUE KEY `uni_idx_pay_requester_no` (`requester_no`), UNIQUE KEY `uni_idx_pay_payment_pay_no` (`payment_pay_no`), KEY `idx_pay_order_id` (`order_id`), CONSTRAINT `fk_pay_order_id` FOREIGN KEY (`order_id`) REFERENCES `tbl_sapo_order` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='支付訂單'; CREATE TABLE IF NOT EXISTS `tbl_sapo_refund` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '物理主鍵', `requester_refund_no` varchar(255) NOT NULL COMMENT '請求服務退款流水', `create_time` datetime(3) NOT NULL COMMENT '創建時間', `last_update_time` datetime(3) DEFAULT NULL COMMENT '最後更新時間', `order_id` int(10) unsigned NOT NULL COMMENT '業務訂單標識,tbl_sapo_order表id', `payment_id` int(10) unsigned NOT NULL COMMENT '支付訂單id,tbl_sapo_payment表id', `expire_time` datetime(3) DEFAULT NULL ON UPDATE current_timestamp(3), `status` int(10) unsigned NOT NULL DEFAULT 4 COMMENT '狀態:3-撤銷,4-申請,5-退款中,6-已退款,7-失敗', `requester_code` varchar(255) NOT NULL COMMENT '服務編碼', `requester_pay_no` varchar(255) NOT NULL COMMENT '請求服務支付流水', `refund_fee` int(10) unsigned NOT NULL COMMENT '金額(單位:分)', PRIMARY KEY (`id`), UNIQUE KEY `uni_idx_refund_requester_refund_no` (`requester_refund_no`), KEY `idx_refund_payment_id` (`payment_id`), KEY `idx_refund_order_id` (`order_id`), KEY `idx_refund_requester_pay_no` (`requester_pay_no`), CONSTRAINT `fk_refund_order_id` FOREIGN KEY (`order_id`) REFERENCES `tbl_sapo_order` (`id`), CONSTRAINT `fk_refund_payment_id` FOREIGN KEY (`payment_id`) REFERENCES `tbl_sapo_pay` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='退款訂單'; CREATE TABLE IF NOT EXISTS `tbl_sapo_sys_param` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵', `create_time` datetime(3) NOT NULL COMMENT '創建時間', `para_key` varchar(255) NOT NULL COMMENT '配置鍵', `para_value` varchar(1024) NOT NULL COMMENT '配置值', `para_desc` varchar(255) NOT NULL COMMENT '配置描述', `level` varchar(64) NOT NULL DEFAULT 'A' COMMENT '參數等級。ABCD', PRIMARY KEY (`id`), UNIQUE KEY `uni_idx_sys_param_para_key` (`para_key`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='系統配置表'; CREATE TABLE IF NOT EXISTS `tbl_sapo_xx` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵', `code` varchar(100) NOT NULL COMMENT '唯一編碼', `create_time` datetime(3) NOT NULL COMMENT '創建時間', `last_update_time` datetime(3) DEFAULT NULL COMMENT '最後更新時間', `name` varchar(255) NOT NULL COMMENT '名稱', `detail` varchar(255) DEFAULT NULL COMMENT '詳情', `status` int(10) unsigned NOT NULL DEFAULT 2 COMMENT '狀態:0-無效,1-有效,2-編輯', PRIMARY KEY (`id`), UNIQUE KEY `uni_idx_xx_code` (`code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='業務實體'; /*!40101 SET SQL_MODE=IFNULL(@OLD_SQL_MODE, '') */; /*!40014 SET FOREIGN_KEY_CHECKS=IFNULL(@OLD_FOREIGN_KEY_CHECKS, 1) */; /*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */; /*!40111 SET SQL_NOTES=IFNULL(@OLD_SQL_NOTES, 1) */;
如上圖所示:
一種典型的訂單設計。業務訂單和支付退款訂單組合,他們分別有自己的狀態機。
- 業務訂單狀態機負責業務邏輯,並和支付退款狀態機聯動。
- 一切以業務狀態機為主。例如:業務狀態已經【關單】,此時收到支付成功通知,需要進行退款
- 每個狀態有自己的過期時間。異常訂單的撈取通過過期時間判斷。
狀態機模式實現訂單狀態機:
日常開發過程中,狀態機模式應用場景之一的就是訂單模型中的狀態控制。但是區別於狀態模式的點有以下幾個:
- 狀態模式,所有的操作都在記憶體。而訂單狀態機是要落庫的。為了防止訂單的並發操作,更新訂單的時候需要使用樂觀鎖機制。
- 狀態模式的狀態對象是新建狀態機的時候初始化進去的。在實際開發中,狀態對象要復用,被spring管理。
- 而訂單狀態機對象對應了一條資料庫中實體的訂單,是要每次從資料庫中查出來的即時新建對象,所以必須將該新建的訂單狀態機對象傳入到狀態對象中。使用狀態對象處理該訂單狀態機對象。
以支付訂單為例:

/* Title: PaymentInfo Description:
支付訂單狀態機 該類不可被spring管理,需要new出來,一個類就對應一條資料庫中支付訂單記錄
本文來自部落格園,作者:wanglifeng,轉載請註明原文鏈接://www.cnblogs.com/wanglifeng717/p/16214122.html
@author wanglifeng */
public class PaymentStateMachine { // 資料庫中當前支付訂單實體 private SapoPayment payment; // 當前狀態 private PaymentState currentState; // 需要更新入庫的支付訂單實體。與payment屬性配合,payment為當前資料庫中訂單實體,用於樂觀鎖的前置內容校驗。 private SapoPayment paymentForUpdate; /* 將最新內容(含狀態)更新入庫,併當前狀態機狀態 */ public void updateStateMachine() { // 從Spring容器中獲取操作數據的dao SapoDao dao = SpringUtil.getBean(SapoDao.class); // 更新資料庫,樂觀鎖機制:帶前置內容數據校驗,其中payment為前置內容,paymentForUpdate為要更新的內容,如果更新結果=0,說明該訂單被其他執行緒修改過。拋異常,放棄此次修改。 dao.updateSapoPaymentByNull(paymentForUpdate, payment); // 記錄訂單操作流水 dao.insertSapoOrderStatusLog(SapoOrderStatusLog.getInstance().setOrderId(paymentForUpdate.getId()) .setOrderType(SapoOrderStatusLog.ORDER_TYPE_PAYMENT).setStatus(paymentForUpdate.getStatus())); // 更新當前PaymentStateMachine狀態機 this.setPayment(paymentForUpdate); this.setCurrentState(paymentForUpdate.getStatus()); } // 通過條件獲取一個支付訂單PaymentStateMachine實體 public static PaymentStateMachine getInstance(SapoPayment sapoPaymentForQuery) { // 1.從spring容器中獲取dao; SapoDao dao = SpringUtil.getBean(SapoDao.class); // 2.查出該支付訂單 SapoPayment paymentResult = dao.getSapoPayment(sapoPaymentForQuery); // 3.初始化訂單狀態機 PaymentStateMachine paymentStateMachine = new PaymentStateMachine(); paymentStateMachine.setPayment(paymentResult); paymentStateMachine.setCurrentState(paymentResult.getStatus()); paymentStateMachine.setPaymentForUpdate(SapoPayment.getInstance(paymentResult)); return paymentStateMachine; } // 設置當前狀態機的狀態。輸入資料庫中status欄位,映射成對應的狀態類實體。 public void setCurrentState(Integer status) { PaymentState currentState = null; // status數字,映射成對應的狀態類實體 if (SapoPayment.STATUS_APPLY.equals(status)) { currentState = SpringUtil.getBean(PaymentStateApply.class); } else if (SapoPayment.STATUS_WAIT_PAY.equals(status)) { currentState = SpringUtil.getBean(PaymentStateWaitPay.class); } else if (SapoPayment.STATUS_PAY_FINISH.equals(status)) { currentState = SpringUtil.getBean(PaymentStatePayFinish.class); } else if (SapoPayment.STATUS_FAIL.equals(status)) { currentState = SpringUtil.getBean(PaymentStateFail.class); } else if (SapoPayment.STATUS_CANCEL.equals(status)) { currentState = SpringUtil.getBean(PaymentStateCancel.class); } else { throw new BusinessException(ResultInfo.SYS_INNER_ERROR.getCode(), "status not in state machine ,status: " + status); } this.currentState = currentState; } // TODO 待實現,申請支付訂單 public void apply() { // 委託給當前狀態執行,將當前訂單狀態機對象傳進去,使用狀態對象處理訂單 currentState.apply(this); } // TODO 待實現,通知支付結果 public void resultNotify() { // 委託給當前狀態執行 currentState.resultNotify(this); } // TODO 同步給當前狀態執行 public void sync() { // 委託給當前狀態執行 currentState.sync(this); } // 取消訂單 public void cancel() { // 委託給當前狀態執行 currentState.cancel(this); } }

public interface PaymentState { public void apply(PaymentStateMachine paymentStateMachine); public void resultNotify(PaymentStateMachine paymentStateMachine); public void sync(PaymentStateMachine paymentStateMachine); public void cancel(PaymentStateMachine paymentStateMachine); }

@Service public class PaymentStateApply extends BaseLogger implements PaymentState { @Autowired FmPayClientService fmPayClientService; @Autowired SapoDao dao; @Autowired private JacksonComponent jacksonComponent; public void apply(PaymentStateMachine paymentStateMachine) { } public void sync(PaymentStateMachine paymentStateMachine) { } public void resultNotify(PaymentStateMachine paymentStateMachine) { // TODO Auto-generated method stub } public void cancel(PaymentStateMachine paymentStateMachine) { SapoPayment sapoPaymentForUpdate = paymentStateMachine.getPaymentForUpdate(); sapoPaymentForUpdate.setStatus(SapoPayment.STATUS_CANCEL); sapoPaymentForUpdate.setExpireTime(null); paymentStateMachine.updateStateMachine(); } }

@Service public class PaymentStateCancel extends BaseLogger implements PaymentState { public void apply(PaymentStateMachine paymentStateMachine) { // TODO Auto-generated method stub } public void resultNotify(PaymentStateMachine paymentStateMachine) { // TODO Auto-generated method stub } public void sync(PaymentStateMachine paymentStateMachine) { // TODO Auto-generated method stub } public void cancel(PaymentStateMachine paymentStateMachine) { // TODO Auto-generated method stub } }

@Service public class PaymentStateFail extends BaseLogger implements PaymentState { public void apply(PaymentStateMachine paymentStateMachine) { // TODO Auto-generated method stub } public void resultNotify(PaymentStateMachine paymentStateMachine) { // TODO Auto-generated method stub } public void sync(PaymentStateMachine paymentStateMachine) { // TODO Auto-generated method stub } public void cancel(PaymentStateMachine paymentStateMachine) { throw new BusinessException(ResultInfo.SYS_INNER_ERROR.getCode(), "fail status can not cancel"); } }

@Service public class PaymentStatePayFinish extends BaseLogger implements PaymentState { public void apply(PaymentStateMachine paymentStateMachine) { // TODO Auto-generated method stub } public void resultNotify(PaymentStateMachine paymentStateMachine) { // TODO Auto-generated method stub } public void sync(PaymentStateMachine paymentStateMachine) { // TODO Auto-generated method stub } public void cancel(PaymentStateMachine paymentStateMachine) { throw new BusinessException(ResultInfo.SYS_INNER_ERROR.getCode(), "payfinish status can not cancel"); } }

@Service public class PaymentStateWaitPay extends BaseLogger implements PaymentState { @Autowired FmPayClientService fmPayClientService; @Autowired SapoDao dao; @Autowired private JacksonComponent jacksonComponent; public void payResultNotify() { // TODO implement here } public void apply(PaymentStateMachine paymentStateMachine) { throw new BusinessException(ResultInfo.SYS_INNER_ERROR.getCode(), "applyPayPlatform not match payment state machine,currentStatus:" + paymentStateMachine.getPayment().getStatus()); } public void sync(PaymentStateMachine paymentStateMachine) { // TODO 過期去統一支付查詢 String payStatus = queryPayResultResponse.getPayStatus(); // 1:初始化輸入 2:支付中 3:支付成功 4:支付失敗 5:撤銷 if (QueryPayResultResponse.PAY_STATUS_INIT.equals(payStatus)) { throw new BusinessException(ResultInfo.SYS_INNER_ERROR.getCode(), "FMpay queryPay return init status ,we are waitpay"); } if (QueryPayResultResponse.PAY_STATUS_ING.equals(payStatus)) { return; } SapoPayment sapoPaymentForUpdate = paymentStateMachine.getPaymentForUpdate(); if (QueryPayResultResponse.PAY_STATUS_CANCEL.equals(payStatus)) { sapoPaymentForUpdate.setStatus(SapoPayment.STATUS_CANCEL); } else if (QueryPayResultResponse.PAY_STATUS_FAIL.equals(payStatus)) { sapoPaymentForUpdate.setStatus(SapoPayment.STATUS_FAIL); } else if (QueryPayResultResponse.PAY_STATUS_SUCCESS.equals(payStatus)) { sapoPaymentForUpdate.setStatus(SapoPayment.STATUS_PAY_FINISH); } sapoPaymentForUpdate.setExpireTime(null); paymentStateMachine.updateStateMachine(); } public void resultNotify(PaymentStateMachine paymentStateMachine) { // TODO Auto-generated method stub } public void cancel(PaymentStateMachine paymentStateMachine) { throw new BusinessException(ResultInfo.SYS_INNER_ERROR.getCode(), "wait pay status can not cancel"); } }
本文來自部落格園,作者:wanglifeng,轉載請註明原文鏈接://www.cnblogs.com/wanglifeng717/p/16214122.html

