告別if-else噩夢(mèng)!流程編排技術(shù)真的太香了!
兄弟們,咱先聊個(gè)扎心的事兒,打開公司祖?zhèn)鞯?Java 項(xiàng)目,想改個(gè)簡(jiǎn)單的業(yè)務(wù)邏輯,結(jié)果一翻代碼,好家伙!一個(gè)方法里的 if-else 能嵌套到第 8 層,縮進(jìn)比你工資條上的數(shù)字還長(zhǎng)。改的時(shí)候大氣不敢喘,生怕動(dòng)了其中一個(gè) else,整個(gè)系統(tǒng)就跟多米諾骨牌似的全崩了?
我前陣子就踩過這坑。當(dāng)時(shí)要給訂單系統(tǒng)加個(gè) “老用戶專屬折扣” 的邏輯,原代碼里訂單處理的方法長(zhǎng)這樣:
public void handleOrder(Order order) {
// 第一步:判斷訂單類型
if (order.getType() == OrderType.NORMAL) {
// 普通訂單判斷支付方式
if (order.getPayType() == PayType.WECHAT) {
// 微信支付判斷是否滿減
if (order.getAmount() >= 100) {
// 滿減后判斷是否老用戶
if (order.getUser().isOldUser()) {
order.setDiscount(0.9);
} else {
order.setDiscount(1.0);
}
wechatPayService.pay(order);
} else {
if (order.getUser().isOldUser()) {
order.setDiscount(0.95);
}
wechatPayService.pay(order);
}
} else if (order.getPayType() == PayType.ALIPAY) {
// 支付寶支付又一套判斷...
if (order.getAmount() >= 200) {
// 此處省略800字嵌套...
}
}
} else if (order.getType() == OrderType.GROUP) {
// 團(tuán)購訂單再來一套獨(dú)立的if-else...
}
// 后面還有庫存判斷、日志記錄、消息推送的嵌套...
}改完這個(gè)邏輯,我眼睛都快成斗雞眼了,測(cè)試的時(shí)候還漏了個(gè) “新用戶用支付寶支付滿 150 減 20” 的場(chǎng)景,結(jié)果線上出了 bug,當(dāng)晚加班到凌晨?jī)牲c(diǎn) —— 這 if-else,簡(jiǎn)直就是開發(fā)者的 “職場(chǎng) PUA 神器”!后來我痛定思痛,把這套邏輯用流程編排重構(gòu)了一遍,現(xiàn)在不管加什么新規(guī)則,都是 “插拔式” 操作,再也不用在嵌套里找不著北。今天就跟大家好好嘮嘮,怎么用流程編排跟 if-else 徹底說再見。
一、先別急著罵 if-else,咱得搞懂它為啥會(huì) “癌變”
首先聲明:if-else 本身沒問題,就像菜刀能切菜也能傷人,問題出在 “用錯(cuò)地方” 和 “過度使用”。我見過很多項(xiàng)目里的 if-else,都是從 “一行簡(jiǎn)單判斷” 慢慢長(zhǎng)成 “千行嵌套怪物” 的,這個(gè)過程就叫 “邏輯癌變”。
咱先拿剛才的訂單例子分析下,if-else 為啥會(huì)越寫越爛:
1. 「邏輯耦合」:所有規(guī)則都擠在一個(gè) “垃圾桶” 里
你看原代碼里,訂單類型、支付方式、滿減規(guī)則、用戶身份判斷,全堆在handleOrder方法里。就像把衣服、鞋子、零食、化妝品全塞在一個(gè)衣柜里,剛開始還能翻找,越堆越多就徹底亂了。
后來產(chǎn)品說 “要給新用戶支付寶支付加個(gè)首單立減”,我得在支付寶支付的 else 分支里再塞一個(gè) if;再后來又說 “老用戶團(tuán)購訂單額外 9 折”,我又得在團(tuán)購訂單的分支里加判斷 —— 每加一個(gè)新規(guī)則,就是在 “垃圾桶” 里多扔一件東西,最后誰也分不清里面到底有啥。
2. 「擴(kuò)展性差」:改一行代碼,像拆一顆炸彈
有次我要把 “滿 100 減 10” 改成 “滿 100 減 15”,按理說只是改個(gè)數(shù)字,但因?yàn)檫@個(gè)判斷藏在微信支付的 if 分支里,我得先理清 “訂單類型是普通訂單→支付方式是微信→金額滿 100→用戶是新用戶” 這個(gè)鏈路,生怕改的時(shí)候碰了其他判斷條件。
更坑的是,有些判斷條件還互相依賴。比如 “是否老用戶” 的判斷,既用在普通訂單里,又用在團(tuán)購訂單里,后來產(chǎn)品改了 “老用戶定義”(從注冊(cè)滿 1 年改成滿 6 個(gè)月),我得在代碼里找遍所有用到isOldUser()的 if 分支,改漏一個(gè)就出 bug—— 這哪是改代碼,這是拆炸彈?。?/p>
3. 「可讀性為零」:新人看代碼,得先畫思維導(dǎo)圖
我同事剛接手這個(gè)項(xiàng)目的時(shí)候,看這個(gè)handleOrder方法看了一下午,最后在筆記本上畫了個(gè)思維導(dǎo)圖,才理清里面的邏輯。他跟我說:“這代碼里的 if-else,比我老家的族譜還復(fù)雜,光分支就有 12 個(gè)?!?/p>
其實(shí)這還不算最夸張的,我見過有人寫的代碼,if-else 嵌套到第 11 層,縮進(jìn)能從屏幕左邊排到右邊,中間還夾雜著各種臨時(shí)變量和魔法值 —— 這種代碼,除了寫的人自己,沒人能一次看懂。
4. 「測(cè)試噩夢(mèng)」:想覆蓋所有場(chǎng)景,得寫 100 個(gè)測(cè)試用例
因?yàn)槊總€(gè) if-else 分支都是獨(dú)立的場(chǎng)景,要保證測(cè)試覆蓋,就得把所有分支都跑一遍。剛才的訂單例子,光訂單類型(2 種)× 支付方式(2 種)× 金額區(qū)間(3 種)× 用戶身份(2 種),就有 2×2×3×2=24 種場(chǎng)景,還沒算上異常情況。
每次加新規(guī)則,測(cè)試用例就得翻倍。后來測(cè)試小姐姐跟我吐槽:“你們這訂單模塊,我測(cè)一次得花一下午,比我逛街還累?!?/p>
二、啥是流程編排?說白了就是 “給代碼找個(gè)管家”
既然 if-else 這么坑,那有沒有辦法讓代碼 “變整齊”?答案就是流程編排。可能有些兄弟覺得 “流程編排” 這詞兒聽著挺玄乎,其實(shí)特好理解 —— 就像你去餐廳吃飯,后廚不會(huì)讓一個(gè)廚師又買菜又切菜又炒菜又裝盤,而是有專門的采購、切配、掌勺、擺盤師傅,各司其職,最后把菜端到你面前。
流程編排就是給代碼做 “分工”:把一個(gè)復(fù)雜的業(yè)務(wù)流程,拆成一個(gè)個(gè)獨(dú)立的 “小步驟”(比如訂單處理里的 “支付驗(yàn)證”“滿減計(jì)算”“庫存扣減”),然后規(guī)定這些步驟的執(zhí)行順序,讓它們像流水線一樣配合工作。
咱還是拿訂單處理舉例,用流程編排重構(gòu)后,邏輯會(huì)變成這樣:
- 第一步:獲取訂單基礎(chǔ)信息(獨(dú)立步驟)
- 第二步:判斷訂單類型(獨(dú)立步驟,不同類型走不同分支)
- 第三步:驗(yàn)證支付方式(獨(dú)立步驟)
- 第四步:計(jì)算折扣(獨(dú)立步驟,根據(jù)用戶身份和金額)
- 第五步:扣減庫存(獨(dú)立步驟)
- 第六步:記錄訂單日志(獨(dú)立步驟)
每個(gè)步驟都是一個(gè) “小模塊”,可以單獨(dú)修改、測(cè)試、復(fù)用。比如要改折扣規(guī)則,只需要改 “計(jì)算折扣” 這個(gè)步驟,其他步驟完全不用動(dòng) —— 這就像你想換件衣服,不用把褲子、鞋子、襪子全換掉一樣。
可能有人會(huì)問:“這不就是把代碼拆成方法嗎?跟流程編排有啥區(qū)別?”
區(qū)別大了!普通的方法拆分,還是需要你在主方法里用 if-else 調(diào)用各個(gè)方法,比如:
public void handleOrder(Order order) {
getOrderInfo(order);
if (order.getType() == OrderType.NORMAL) {
checkNormalPay(order);
calculateNormalDiscount(order);
} else if (order.getType() == OrderType.GROUP) {
checkGroupPay(order);
calculateGroupDiscount(order);
}
deductStock(order);
logOrder(order);
}而流程編排是 “把步驟的執(zhí)行順序也交給框架管理”,你不用寫 if-else 調(diào)用,只需要告訴框架 “第一步執(zhí)行 A,第二步執(zhí)行 B,第三步根據(jù)條件選 C 或 D”,剩下的事兒框架全幫你干了。就像你不用親自指揮后廚的每個(gè)師傅,只需要告訴餐廳 “我要一份番茄炒蛋”,餐廳的流程體系會(huì)自動(dòng)讓采購買番茄雞蛋、切配師傅切菜、掌勺師傅炒菜 —— 這才是流程編排的核心:把 “指揮邏輯” 和 “執(zhí)行邏輯” 徹底分開。
三、Java 生態(tài)里的流程編排方案:從 “輕量級(jí)” 到 “重量級(jí)”,總有一款適合你
聊完概念,咱來點(diǎn)實(shí)在的 ——Java 里到底有哪些流程編排方案?該怎么選?
我把常用的方案分成了三類:輕量級(jí)(自己用設(shè)計(jì)模式實(shí)現(xiàn))、中量級(jí)(Spring 生態(tài)工具)、重量級(jí)(專業(yè)流程引擎)。咱一個(gè)個(gè)說,每個(gè)方案都帶代碼示例,保證你看完就能用。
方案一:輕量級(jí) —— 用 “責(zé)任鏈 + 策略模式”,不用引入任何框架
如果你的業(yè)務(wù)流程不算特別復(fù)雜(比如只有 5-8 個(gè)步驟),又不想引入新框架,那用設(shè)計(jì)模式自己實(shí)現(xiàn)是最好的選擇。這里推薦 “責(zé)任鏈模式 + 策略模式” 的組合,前者負(fù)責(zé) “步驟順序”,后者負(fù)責(zé) “條件分支”。
還是拿訂單處理舉例,咱一步步實(shí)現(xiàn):
1. 第一步:定義 “流程節(jié)點(diǎn)” 接口(所有步驟都要實(shí)現(xiàn)這個(gè)接口)
先定義一個(gè)OrderProcessNode接口,里面只有一個(gè)process方法,每個(gè)步驟都是一個(gè)節(jié)點(diǎn):
// 訂單流程節(jié)點(diǎn)接口
public interface OrderProcessNode {
// 處理訂單流程,返回是否繼續(xù)執(zhí)行下一個(gè)節(jié)點(diǎn)
boolean process(Order order);
}返回boolean是為了控制流程:返回true表示繼續(xù)執(zhí)行下一個(gè)節(jié)點(diǎn),返回false表示終止流程(比如支付驗(yàn)證失敗,就不用執(zhí)行后面的庫存扣減了)。
2. 第二步:實(shí)現(xiàn)各個(gè)獨(dú)立的流程節(jié)點(diǎn)
把之前嵌套在 if-else 里的邏輯,拆成一個(gè)個(gè)節(jié)點(diǎn)實(shí)現(xiàn)類:
① 獲取訂單信息節(jié)點(diǎn)
// 獲取訂單基礎(chǔ)信息
public class OrderInfoNode implements OrderProcessNode {
@Override
public boolean process(Order order) {
System.out.println("第一步:獲取訂單基礎(chǔ)信息");
// 模擬從數(shù)據(jù)庫獲取訂單詳情
order.setProductName("iPhone 15");
order.setAmount(5999.0);
return true; // 繼續(xù)執(zhí)行下一個(gè)節(jié)點(diǎn)
}
}② 訂單類型判斷節(jié)點(diǎn)(策略模式在這里用)因?yàn)椴煌唵晤愋偷奶幚磉壿嫴煌?,這里用策略模式,先定義訂單類型處理器接口:
// 訂單類型處理器接口(策略接口)
public interface OrderTypeHandler {
void handle(Order order);
}
// 普通訂單處理器(具體策略)
public class NormalOrderHandler implements OrderTypeHandler {
@Override
public void handle(Order order) {
System.out.println("處理普通訂單邏輯");
order.setTypeDesc("普通訂單,支持微信/支付寶支付");
}
// 團(tuán)購訂單處理器(具體策略)
public class GroupOrderHandler implements OrderTypeHandler {
@Override
public void handle(Order order) {
System.out.println("處理團(tuán)購訂單邏輯");
order.setTypeDesc("團(tuán)購訂單,需滿3人成團(tuán)");
}
}然后實(shí)現(xiàn)訂單類型判斷節(jié)點(diǎn),根據(jù)訂單類型選擇對(duì)應(yīng)的處理器:
// 訂單類型判斷節(jié)點(diǎn)
public class OrderTypeNode implements OrderProcessNode {
// 策略工廠,根據(jù)訂單類型獲取對(duì)應(yīng)的處理器
private Map<OrderType, OrderTypeHandler> typeHandlerMap;
// 構(gòu)造方法初始化策略工廠
public OrderTypeNode() {
typeHandlerMap = new HashMap<>();
typeHandlerMap.put(OrderType.NORMAL, new NormalOrderHandler());
typeHandlerMap.put(OrderType.GROUP, new GroupOrderHandler());
}
@Override
public boolean process(Order order) {
System.out.println("第二步:判斷訂單類型");
// 根據(jù)訂單類型獲取處理器,執(zhí)行對(duì)應(yīng)邏輯
OrderTypeHandler handler = typeHandlerMap.get(order.getType());
if (handler == null) {
System.out.println("未知訂單類型,終止流程");
return false; // 終止流程
}
handler.handle(order);
return true; // 繼續(xù)執(zhí)行下一個(gè)節(jié)點(diǎn)
}
}③ 支付驗(yàn)證節(jié)點(diǎn)
// 支付驗(yàn)證節(jié)點(diǎn)
public class PayCheckNode implements OrderProcessNode {
@Override
public boolean process(Order order) {
System.out.println("第三步:驗(yàn)證支付方式");
if (order.getPayType() == PayType.WECHAT || order.getPayType() == PayType.ALIPAY) {
System.out.println("支付方式合法:" + order.getPayType());
return true;
} else {
System.out.println("不支持的支付方式:" + order.getPayType() + ",終止流程");
return false;
}
}
}④ 折扣計(jì)算節(jié)點(diǎn)
// 折扣計(jì)算節(jié)點(diǎn)
public class DiscountCalculateNode implements OrderProcessNode {
@Override
public boolean process(Order order) {
System.out.println("第四步:計(jì)算訂單折扣");
double discount = 1.0;
// 老用戶折扣
if (order.getUser().isOldUser()) {
discount -= 0.1; // 老用戶9折
System.out.println("老用戶享受9折優(yōu)惠");
}
// 滿減折扣
if (order.getAmount() >= 5000) {
discount -= 0.05; // 滿5000再減5%
System.out.println("滿5000元再減5%");
}
// 防止折扣低于0.5
order.setDiscount(Math.max(discount, 0.5));
System.out.println("最終折扣:" + order.getDiscount());
return true;
}
}⑤ 庫存扣減節(jié)點(diǎn)
// 庫存扣減節(jié)點(diǎn)
publicclass StockDeductNode implements OrderProcessNode {
private StockService stockService = new StockService(); // 模擬庫存服務(wù)
@Override
public boolean process(Order order) {
System.out.println("第五步:扣減訂單庫存");
boolean deductSuccess = stockService.deductStock(order.getProductId(), order.getQuantity());
if (deductSuccess) {
System.out.println("庫存扣減成功");
returntrue;
} else {
System.out.println("庫存不足,終止流程");
returnfalse;
}
}
}⑥ 日志記錄節(jié)點(diǎn)
// 日志記錄節(jié)點(diǎn)
public class OrderLogNode implements OrderProcessNode {
private LogService logService = new LogService(); // 模擬日志服務(wù)
@Override
public boolean process(Order order) {
System.out.println("第六步:記錄訂單日志");
logService.recordLog("訂單" + order.getOrderId() + "處理完成,最終金額:" + order.getAmount() * order.getDiscount());
return true;
}
}3. 第三步:構(gòu)建責(zé)任鏈,串聯(lián)所有節(jié)點(diǎn)
接下來需要一個(gè) “流程管理器”,把這些節(jié)點(diǎn)按順序串成一條責(zé)任鏈,然后執(zhí)行流程:
// 訂單流程管理器(責(zé)任鏈)
publicclassOrderProcessChain {
// 用鏈表存儲(chǔ)所有節(jié)點(diǎn),保證執(zhí)行順序
private LinkedList<OrderProcessNode> nodeList = new LinkedList<>();
// 添加節(jié)點(diǎn)到鏈尾
public void addNode(OrderProcessNode node) {
nodeList.add(node);
}
// 執(zhí)行流程:依次調(diào)用每個(gè)節(jié)點(diǎn)的process方法
public void execute(Order order) {
for (OrderProcessNode node : nodeList) {
boolean continueNext = node.process(order);
if (!continueNext) {
System.out.println("流程在節(jié)點(diǎn)[" + node.getClass().getSimpleName() + "]終止");
return;
}
}
System.out.println("所有流程節(jié)點(diǎn)執(zhí)行完成!");
}
}4. 第四步:測(cè)試流程執(zhí)行效果
最后寫個(gè)測(cè)試類,看看流程跑起來怎么樣:
public classOrderProcessTest {
public static void main(String[] args) {
// 1. 創(chuàng)建訂單對(duì)象
Order order = new Order();
order.setOrderId("ORDER_20250826_001");
order.setType(OrderType.NORMAL); // 普通訂單
order.setPayType(PayType.WECHAT); // 微信支付
order.setProductId("PROD_001");
order.setQuantity(1);
User user = new User();
user.setOldUser(true); // 老用戶
order.setUser(user);
// 2. 構(gòu)建流程鏈,按順序添加節(jié)點(diǎn)
OrderProcessChain chain = new OrderProcessChain();
chain.addNode(new OrderInfoNode());
chain.addNode(new OrderTypeNode());
chain.addNode(new PayCheckNode());
chain.addNode(new DiscountCalculateNode());
chain.addNode(new StockDeductNode());
chain.addNode(new OrderLogNode());
// 3. 執(zhí)行流程
System.out.println("開始處理訂單:" + order.getOrderId());
chain.execute(order);
}
}運(yùn)行結(jié)果如下:
開始處理訂單:ORDER_20250826_001
第一步:獲取訂單基礎(chǔ)信息
第二步:判斷訂單類型
處理普通訂單邏輯
第三步:驗(yàn)證支付方式
支付方式合法:WECHAT
第四步:計(jì)算訂單折扣
老用戶享受9折優(yōu)惠
滿5000元再減5%
最終折扣:0.85
第五步:扣減訂單庫存
庫存扣減成功
第六步:記錄訂單日志
所有流程節(jié)點(diǎn)執(zhí)行完成!你看,現(xiàn)在要加新規(guī)則,比如 “新用戶首單支付寶支付減 200”,只需要新建一個(gè)NewUserAlipayDiscountNode,然后在流程鏈里加個(gè)節(jié)點(diǎn)就行:
// 新用戶支付寶首單折扣節(jié)點(diǎn)
publicclass NewUserAlipayDiscountNode implements OrderProcessNode {
@Override
public boolean process(Order order) {
System.out.println("新增步驟:新用戶支付寶首單折扣");
if (!order.getUser().isOldUser() && order.getPayType() == PayType.ALIPAY && order.isFirstOrder()) {
order.setAmount(order.getAmount() - 200);
System.out.println("新用戶首單支付寶支付,立減200元,優(yōu)惠后金額:" + order.getAmount());
}
returntrue;
}
}
// 構(gòu)建流程鏈時(shí)添加這個(gè)節(jié)點(diǎn)
chain.addNode(new NewUserAlipayDiscountNode()); // 加在折扣計(jì)算節(jié)點(diǎn)前面完全不用動(dòng)原來的任何代碼,這就是 “插拔式” 開發(fā)的爽快感!
方案二:中量級(jí) —— 用 Spring StateMachine,搞定 “狀態(tài)流轉(zhuǎn)” 類業(yè)務(wù)
如果你的業(yè)務(wù)里有很多 “狀態(tài)變化” 的邏輯(比如訂單狀態(tài):待支付→已支付→待發(fā)貨→已發(fā)貨→已完成),用上面的責(zé)任鏈模式雖然能實(shí)現(xiàn),但狀態(tài)管理會(huì)比較麻煩。這時(shí)候就該 Spring StateMachine 登場(chǎng)了 —— 它是 Spring 生態(tài)里專門處理 “狀態(tài)機(jī)” 的工具,能幫你把復(fù)雜的狀態(tài)流轉(zhuǎn)邏輯變得清晰。
咱還是拿訂單狀態(tài)流轉(zhuǎn)舉例,比如訂單有以下狀態(tài):
- 待支付(WAIT_PAY)
- 已支付(PAID)
- 待發(fā)貨(WAIT_SHIP)
- 已發(fā)貨(SHIPPED)
- 已完成(COMPLETED)
- 已取消(CANCELED)
狀態(tài)之間的流轉(zhuǎn)規(guī)則:
- 待支付 → 已支付(用戶付款)
- 待支付 → 已取消(用戶取消訂單)
- 已支付 → 待發(fā)貨(商家確認(rèn)收款)
- 已支付 → 已取消(退款)
- 待發(fā)貨 → 已發(fā)貨(商家發(fā)貨)
- 已發(fā)貨 → 已完成(用戶確認(rèn)收貨)
如果用 if-else 寫,會(huì)是這樣:
public void changeOrderStatus(Order order, String event) {
if (order.getStatus() == OrderStatus.WAIT_PAY) {
if ("PAY".equals(event)) {
order.setStatus(OrderStatus.PAID);
} elseif ("CANCEL".equals(event)) {
order.setStatus(OrderStatus.CANCELED);
}
} elseif (order.getStatus() == OrderStatus.PAID) {
if ("CONFIRM".equals(event)) {
order.setStatus(OrderStatus.WAIT_SHIP);
} elseif ("REFUND".equals(event)) {
order.setStatus(OrderStatus.CANCELED);
}
} elseif (order.getStatus() == OrderStatus.WAIT_SHIP) {
if ("SHIP".equals(event)) {
order.setStatus(OrderStatus.SHIPPED);
}
}
// 還有更多狀態(tài)判斷...
}用 Spring StateMachine 重構(gòu)后,代碼會(huì)清爽很多。
1. 第一步:引入依賴
在 Spring Boot 項(xiàng)目的 pom.xml 里加依賴:
<dependency>
<groupId>org.springframework.statemachine</groupId>
<artifactId>spring-statemachine-core</artifactId>
<version>3.2.0</version>
</dependency>2. 第二步:定義訂單狀態(tài)和事件
先把訂單狀態(tài)和觸發(fā)狀態(tài)變化的事件定義成枚舉:
// 訂單狀態(tài)枚舉
publicenum OrderStatus {
WAIT_PAY("待支付"),
PAID("已支付"),
WAIT_SHIP("待發(fā)貨"),
SHIPPED("已發(fā)貨"),
COMPLETED("已完成"),
CANCELED("已取消");
private final String desc;
OrderStatus(String desc) {
this.desc = desc;
}
}
// 訂單事件枚舉(觸發(fā)狀態(tài)變化的動(dòng)作)
publicenum OrderEvent {
PAY("用戶付款"),
CANCEL("用戶取消"),
CONFIRM("商家確認(rèn)收款"),
REFUND("退款"),
SHIP("商家發(fā)貨"),
CONFIRM_RECEIVE("用戶確認(rèn)收貨");
private final String desc;
OrderEvent(String desc) {
this.desc = desc;
}
}3. 第三步:配置狀態(tài)機(jī)
創(chuàng)建一個(gè)配置類,定義狀態(tài)機(jī)的狀態(tài)、事件和流轉(zhuǎn)規(guī)則:
@Configuration
@EnableStateMachineFactory// 啟用狀態(tài)機(jī)工廠,方便創(chuàng)建多個(gè)狀態(tài)機(jī)實(shí)例
publicclass OrderStateMachineConfig extends StateMachineConfigurerAdapter<OrderStatus, OrderEvent> {
// 配置狀態(tài)機(jī)的初始狀態(tài)和所有狀態(tài)
@Override
public void configure(StateMachineStateConfigurer<OrderStatus, OrderEvent> states) throws Exception {
states
.withStates()
.initial(OrderStatus.WAIT_PAY) // 初始狀態(tài):待支付
.states(EnumSet.allOf(OrderStatus.class)); // 所有狀態(tài)
}
// 配置狀態(tài)流轉(zhuǎn)規(guī)則(核心)
@Override
public void configure(StateMachineTransitionConfigurer<OrderStatus, OrderEvent> transitions) throws Exception {
transitions
// 1. 待支付 → 已支付:觸發(fā)事件PAY
.withExternal()
.source(OrderStatus.WAIT_PAY)
.target(OrderStatus.PAID)
.event(OrderEvent.PAY)
.and()
// 2. 待支付 → 已取消:觸發(fā)事件CANCEL
.withExternal()
.source(OrderStatus.WAIT_PAY)
.target(OrderStatus.CANCELED)
.event(OrderEvent.CANCEL)
.and()
// 3. 已支付 → 待發(fā)貨:觸發(fā)事件CONFIRM
.withExternal()
.source(OrderStatus.PAID)
.target(OrderStatus.WAIT_SHIP)
.event(OrderEvent.CONFIRM)
.and()
// 4. 已支付 → 已取消:觸發(fā)事件REFUND
.withExternal()
.source(OrderStatus.PAID)
.target(OrderStatus.CANCELED)
.event(OrderEvent.REFUND)
.and()
// 5. 待發(fā)貨 → 已發(fā)貨:觸發(fā)事件SHIP
.withExternal()
.source(OrderStatus.WAIT_SHIP)
.target(OrderStatus.SHIPPED)
.event(OrderEvent.SHIP)
.and()
// 6. 已發(fā)貨 → 已完成:觸發(fā)事件CONFIRM_RECEIVE
.withExternal()
.source(OrderStatus.SHIPPED)
.target(OrderStatus.COMPLETED)
.event(OrderEvent.CONFIRM_RECEIVE);
}
// 配置狀態(tài)機(jī)監(jiān)聽器,監(jiān)聽狀態(tài)變化事件
@Bean
public StateMachineListener<OrderStatus, OrderEvent> orderStateListener() {
returnnew StateMachineListenerAdapter<OrderStatus, OrderEvent>() {
@Override
public void stateChanged(State<OrderStatus, OrderEvent> from, State<OrderStatus, OrderEvent> to) {
System.out.println("訂單狀態(tài)變化:" + (from == null ? "初始狀態(tài)" : from.getId().getDesc())
+ " → " + to.getId().getDesc());
}
@Override
public void eventNotAccepted(Message<OrderEvent> event) {
System.out.println("不支持的事件:" + event.getPayload().getDesc()
+ ",當(dāng)前訂單狀態(tài)可能不允許此操作");
}
};
}
// 注冊(cè)監(jiān)聽器
@Override
public void configure(StateMachineConfigurationConfigurer<OrderStatus, OrderEvent> config) throws Exception {
config
.withConfiguration()
.listener(orderStateListener());
}
}4. 第四步:創(chuàng)建訂單狀態(tài)服務(wù)
寫一個(gè)服務(wù)類,封裝狀態(tài)機(jī)的使用,方便業(yè)務(wù)層調(diào)用:
@Service
publicclassOrderStateService {
@Autowired
private StateMachineFactory<OrderStatus, OrderEvent> stateMachineFactory;
// 線程安全:每個(gè)訂單用一個(gè)獨(dú)立的狀態(tài)機(jī)實(shí)例
private Map<String, StateMachine<OrderStatus, OrderEvent>> orderStateMachineMap = new ConcurrentHashMap<>();
// 初始化訂單狀態(tài)機(jī)(訂單創(chuàng)建時(shí)調(diào)用)
public void initOrderStateMachine(String orderId) {
StateMachine<OrderStatus, OrderEvent> stateMachine = stateMachineFactory.getStateMachine();
// 設(shè)置訂單ID作為狀態(tài)機(jī)的ID,方便后續(xù)關(guān)聯(lián)
stateMachine.getExtendedState().getVariables().put("orderId", orderId);
orderStateMachineMap.put(orderId, stateMachine);
System.out.println("訂單[" + orderId + "]狀態(tài)機(jī)初始化完成,初始狀態(tài):" + stateMachine.getState().getId().getDesc());
}
// 觸發(fā)訂單狀態(tài)變化(核心方法)
public boolean sendEvent(String orderId, OrderEvent event) {
StateMachine<OrderStatus, OrderEvent> stateMachine = orderStateMachineMap.get(orderId);
if (stateMachine == null) {
System.out.println("訂單[" + orderId + "]狀態(tài)機(jī)未初始化");
returnfalse;
}
// 發(fā)送事件,觸發(fā)狀態(tài)變化
return stateMachine.sendEvent(event);
}
// 獲取訂單當(dāng)前狀態(tài)
public OrderStatus getCurrentState(String orderId) {
StateMachine<OrderStatus, OrderEvent> stateMachine = orderStateMachineMap.get(orderId);
if (stateMachine == null) {
returnnull;
}
return stateMachine.getState().getId();
}
}5. 第五步:測(cè)試狀態(tài)流轉(zhuǎn)
寫個(gè)測(cè)試類,模擬訂單狀態(tài)變化的整個(gè)過程:
@SpringBootTest
publicclassOrderStateMachineTest {
@Autowired
private OrderStateService orderStateService;
@Test
public void testOrderStateFlow() {
String orderId = "ORDER_20250826_002";
// 1. 初始化訂單狀態(tài)機(jī)
orderStateService.initOrderStateMachine(orderId);
// 2. 觸發(fā)"用戶付款"事件:待支付→已支付
boolean paySuccess = orderStateService.sendEvent(orderId, OrderEvent.PAY);
System.out.println("觸發(fā)用戶付款事件:" + (paySuccess ? "成功" : "失敗"));
System.out.println("當(dāng)前訂單狀態(tài):" + orderStateService.getCurrentState(orderId).getDesc() + "\n");
// 3. 觸發(fā)"商家確認(rèn)收款"事件:已支付→待發(fā)貨
boolean confirmSuccess = orderStateService.sendEvent(orderId, OrderEvent.CONFIRM);
System.out.println("觸發(fā)商家確認(rèn)收款事件:" + (confirmSuccess ? "成功" : "失敗"));
System.out.println("當(dāng)前訂單狀態(tài):" + orderStateService.getCurrentState(orderId).getDesc() + "\n");
// 4. 觸發(fā)"商家發(fā)貨"事件:待發(fā)貨→已發(fā)貨
boolean shipSuccess = orderStateService.sendEvent(orderId, OrderEvent.SHIP);
System.out.println("觸發(fā)商家發(fā)貨事件:" + (shipSuccess ? "成功" : "失敗"));
System.out.println("當(dāng)前訂單狀態(tài):" + orderStateService.getCurrentState(orderId).getDesc() + "\n");
// 5. 觸發(fā)"用戶確認(rèn)收貨"事件:已發(fā)貨→已完成
boolean confirmReceiveSuccess = orderStateService.sendEvent(orderId, OrderEvent.CONFIRM_RECEIVE);
System.out.println("觸發(fā)用戶確認(rèn)收貨事件:" + (confirmReceiveSuccess ? "成功" : "失敗"));
System.out.println("當(dāng)前訂單狀態(tài):" + orderStateService.getCurrentState(orderId).getDesc() + "\n");
// 6. 嘗試觸發(fā)"退款"事件(已完成狀態(tài)不支持退款,會(huì)失?。? boolean refundSuccess = orderStateService.sendEvent(orderId, OrderEvent.REFUND);
System.out.println("觸發(fā)退款事件:" + (refundSuccess ? "成功" : "失敗"));
System.out.println("當(dāng)前訂單狀態(tài):" + orderStateService.getCurrentState(orderId).getDesc());
}
}運(yùn)行結(jié)果如下:
訂單[ORDER_20250826_002]狀態(tài)機(jī)初始化完成,初始狀態(tài):待支付
訂單狀態(tài)變化:初始狀態(tài) → 待支付
觸發(fā)用戶付款事件:成功
訂單狀態(tài)變化:待支付 → 已支付
當(dāng)前訂單狀態(tài):已支付
觸發(fā)商家確認(rèn)收款事件:成功
訂單狀態(tài)變化:已支付 → 待發(fā)貨
當(dāng)前訂單狀態(tài):待發(fā)貨
觸發(fā)商家發(fā)貨事件:成功
訂單狀態(tài)變化:待發(fā)貨 → 已發(fā)貨
當(dāng)前訂單狀態(tài):已發(fā)貨
觸發(fā)用戶確認(rèn)收貨事件:成功
訂單狀態(tài)變化:已發(fā)貨 → 已完成
當(dāng)前訂單狀態(tài):已完成
不支持的事件:退款,當(dāng)前訂單狀態(tài)可能不允許此操作
觸發(fā)退款事件:失敗
當(dāng)前訂單狀態(tài):已完成你看,所有狀態(tài)流轉(zhuǎn)規(guī)則都集中在配置類里,不用寫一行 if-else。如果要加新的狀態(tài)流轉(zhuǎn)(比如 “已發(fā)貨→已取消”,用戶拒收退款),只需要在transitions配置里加一段:
.withExternal()
.source(OrderStatus.SHIPPED)
.target(OrderStatus.CANCELED)
.event(OrderEvent.REJECT_REFUND); // 新增“拒收退款”事件是不是比改 if-else 舒服多了?
方案三:重量級(jí) —— 用 Flowable/Camunda,應(yīng)對(duì) “可視化 + 復(fù)雜流程”
如果你的業(yè)務(wù)流程非常復(fù)雜(比如 OA 審批流程、電商售后流程),需要產(chǎn)品經(jīng)理能可視化編輯流程,或者需要支持流程暫停、重試、回滾、定時(shí)任務(wù)等高級(jí)功能,那輕量級(jí)和中量級(jí)方案就不夠用了,這時(shí)候就得上專業(yè)的流程引擎 ——Flowable 和 Camunda 是目前 Java 生態(tài)里最火的兩款。
這倆引擎都基于 BPMN 2.0 標(biāo)準(zhǔn)(業(yè)務(wù)流程建模與 notation),支持用畫圖的方式定義流程,產(chǎn)品經(jīng)理用 Flowable Modeler 或 Camunda Modeler 畫個(gè)流程圖,開發(fā)直接把圖導(dǎo)入項(xiàng)目就能用,不用手寫流程邏輯。
咱以 Flowable 為例,用 “電商售后退款流程” 來演示,流程如下:
- 用戶提交退款申請(qǐng)(需填寫退款原因和金額)
- 系統(tǒng)自動(dòng)驗(yàn)證退款金額是否合理(≤訂單金額)
- 驗(yàn)證通過→商家審核;驗(yàn)證失敗→駁回用戶申請(qǐng)
- 商家審核:通過→財(cái)務(wù)審核;不通過→駁回用戶申請(qǐng)
- 財(cái)務(wù)審核:通過→執(zhí)行退款;不通過→駁回用戶申請(qǐng)
- 執(zhí)行退款后,發(fā)送短信通知用戶
1. 第一步:引入依賴
Spring Boot 項(xiàng)目加 Flowable 依賴:
<dependency>
<groupId>org.flowable</groupId>
<artifactId>flowable-spring-boot-starter</artifactId>
<version>7.0.0.M2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>這里用 H2 內(nèi)存數(shù)據(jù)庫,方便測(cè)試。
2. 第二步:畫 BPMN 流程圖
用 Flowable Modeler(官網(wǎng)可下載)畫流程圖,保存為after_sales_refund.bpmn20.xml,放在src/main/resources/processes目錄下(Flowable 會(huì)自動(dòng)掃描這個(gè)目錄的流程文件)。
流程圖的 XML 內(nèi)容如下(你也可以直接復(fù)制用,或用 Modeler 可視化編輯):
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL http://www.omg.org/spec/BPMN/2.0/20100501/BPMN20.xsd"
xmlns:flowable="http://flowable.org/bpmn"
id="Definitions_1"
targetNamespace="http://flowable.org/bpmn">
<process id="afterSalesRefundProcess" name="電商售后退款流程" isExecutable="true">
<!-- 開始事件:用戶提交退款申請(qǐng) -->
<startEvent id="startEvent1" name="用戶提交退款申請(qǐng)">
<extensionElements>
<!-- 定義流程變量:訂單ID、退款金額、退款原因、用戶ID -->
<flowable:formProperty id="orderId" name="訂單ID" type="string" required="true"/>
<flowable:formProperty id="refundAmount" name="退款金額" type="double" required="true"/>
<flowable:formProperty id="refundReason" name="退款原因" type="string" required="true"/>
<flowable:formProperty id="userId" name="用戶ID" type="string" required="true"/>
</extensionElements>
</startEvent>
<!-- 服務(wù)任務(wù):系統(tǒng)驗(yàn)證退款金額 -->
<serviceTask id="validateRefundAmountTask" name="驗(yàn)證退款金額" flowable:delegateExpression="${validateRefundAmountDelegate}"/>
<!-- 排他網(wǎng)關(guān):驗(yàn)證結(jié)果判斷 -->
<exclusiveGateway id="validateGateway" name="驗(yàn)證結(jié)果"/>
<sequenceFlow id="flow1" sourceRef="startEvent1" targetRef="validateRefundAmountTask"/>
<sequenceFlow id="flow2" sourceRef="validateRefundAmountTask" targetRef="validateGateway"/>
<!-- 服務(wù)任務(wù):駁回用戶申請(qǐng) -->
<serviceTask id="rejectTask" name="駁回用戶申請(qǐng)" flowable:delegateExpression="${rejectRefundDelegate}"/>
<!-- 結(jié)束事件:流程結(jié)束 -->
<endEvent id="endEvent1" name="流程結(jié)束"/>
<sequenceFlow id="flow3" sourceRef="rejectTask" targetRef="endEvent1"/>
<!-- 用戶任務(wù):商家審核 -->
<userTask id="merchantAuditTask" name="商家審核" flowable:assignee="merchant">
<extensionElements>
<!-- 商家審核結(jié)果:通過/不通過 -->
<flowable:formProperty id="merchantAuditResult" name="審核結(jié)果" type="enum" required="true">
<flowable:value id="pass" name="通過"/>
<flowable:value id="reject" name="不通過"/>
</flowable:formProperty>
<flowable:formProperty id="merchantAuditComment" name="審核意見" type="string"/>
</extensionElements>
</userTask>
<!-- 排他網(wǎng)關(guān):商家審核結(jié)果判斷 -->
<exclusiveGateway id="merchantAuditGateway" name="商家審核結(jié)果"/>
<sequenceFlow id="flow4" sourceRef="validateGateway" targetRef="merchantAuditTask">
<!-- 條件:驗(yàn)證通過 -->
<conditionExpression xsi:type="tFormalExpression">#{validateResult == 'pass'}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow5" sourceRef="validateGateway" targetRef="rejectTask">
<!-- 條件:驗(yàn)證失敗 -->
<conditionExpression xsi:type="tFormalExpression">#{validateResult == 'reject'}</conditionExpression>
</sequenceFlow>
<!-- 用戶任務(wù):財(cái)務(wù)審核 -->
<userTask id="financeAuditTask" name="財(cái)務(wù)審核" flowable:assignee="finance">
<extensionElements>
<!-- 財(cái)務(wù)審核結(jié)果:通過/不通過 -->
<flowable:formProperty id="financeAuditResult" name="審核結(jié)果" type="enum" required="true">
<flowable:value id="pass" name="通過"/>
<flowable:value id="reject" name="不通過"/>
</flowable:formProperty>
<flowable:formProperty id="financeAuditComment" name="審核意見" type="string"/>
</extensionElements>
</userTask>
<sequenceFlow id="flow6" sourceRef="merchantAuditTask" targetRef="merchantAuditGateway"/>
<sequenceFlow id="flow7" sourceRef="merchantAuditGateway" targetRef="financeAuditTask">
<!-- 條件:商家審核通過 -->
<conditionExpression xsi:type="tFormalExpression">#{merchantAuditResult == 'pass'}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow8" sourceRef="merchantAuditGateway" targetRef="rejectTask">
<!-- 條件:商家審核不通過 -->
<conditionExpression xsi:type="tFormalExpression">#{merchantAuditResult == 'reject'}</conditionExpression>
</sequenceFlow>
<!-- 排他網(wǎng)關(guān):財(cái)務(wù)審核結(jié)果判斷 -->
<exclusiveGateway id="financeAuditGateway" name="財(cái)務(wù)審核結(jié)果"/>
<!-- 服務(wù)任務(wù):執(zhí)行退款 -->
<serviceTask id="executeRefundTask" name="執(zhí)行退款" flowable:delegateExpression="${executeRefundDelegate}"/>
<!-- 服務(wù)任務(wù):發(fā)送退款通知 -->
<serviceTask id="sendNotifyTask" name="發(fā)送退款通知" flowable:delegateExpression="${sendRefundNotifyDelegate}"/>
<sequenceFlow id="flow9" sourceRef="financeAuditTask" targetRef="financeAuditGateway"/>
<sequenceFlow id="flow10" sourceRef="financeAuditGateway" targetRef="executeRefundTask">
<!-- 條件:財(cái)務(wù)審核通過 -->
<conditionExpression xsi:type="tFormalExpression">#{financeAuditResult == 'pass'}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow11" sourceRef="financeAuditGateway" targetRef="rejectTask">
<!-- 條件:財(cái)務(wù)審核不通過 -->
<conditionExpression xsi:type="tFormalExpression">#{financeAuditResult == 'reject'}</conditionExpression>
</sequenceFlow>
<sequenceFlow id="flow12" sourceRef="executeRefundTask" targetRef="sendNotifyTask"/>
<sequenceFlow id="flow13" sourceRef="sendNotifyTask" targetRef="endEvent1"/>
</process>
</definitions>3. 第三步:實(shí)現(xiàn)流程任務(wù)的業(yè)務(wù)邏輯
Flowable 里的serviceTask需要用Delegate類實(shí)現(xiàn)具體業(yè)務(wù)邏輯,這里實(shí)現(xiàn) 5 個(gè)任務(wù)的 Delegate:
① 驗(yàn)證退款金額 Delegate
@Component("validateRefundAmountDelegate")
publicclass ValidateRefundAmountDelegate implements JavaDelegate {
// 模擬訂單服務(wù),獲取訂單金額
@Autowired
private OrderService orderService;
@Override
public void execute(DelegateExecution execution) {
System.out.println("開始驗(yàn)證退款金額");
// 獲取流程變量
String orderId = (String) execution.getVariable("orderId");
Double refundAmount = (Double) execution.getVariable("refundAmount");
// 從訂單服務(wù)獲取訂單金額
Double orderAmount = orderService.getOrderAmount(orderId);
System.out.println("訂單[" + orderId + "]金額:" + orderAmount + ",申請(qǐng)退款金額:" + refundAmount);
// 驗(yàn)證邏輯:退款金額≤訂單金額
if (refundAmount <= orderAmount && refundAmount > 0) {
execution.setVariable("validateResult", "pass");
System.out.println("退款金額驗(yàn)證通過");
} else {
execution.setVariable("validateResult", "reject");
execution.setVariable("rejectReason", "退款金額不合法(需大于0且≤訂單金額)");
System.out.println("退款金額驗(yàn)證失?。? + execution.getVariable("rejectReason"));
}
}
}② 駁回退款 Delegate
@Component("rejectRefundDelegate")
publicclass RejectRefundDelegate implements JavaDelegate {
@Autowired
private RefundService refundService;
@Override
publicvoid execute(DelegateExecution execution) {
System.out.println("開始處理駁回退款申請(qǐng)");
String orderId = (String) execution.getVariable("orderId");
String rejectReason = (String) execution.getVariable("rejectReason");
// 更新退款單狀態(tài)為“已駁回”
refundService.updateRefundStatus(orderId, RefundStatus.REJECTED, rejectReason);
System.out.println("訂單[" + orderId + "]退款申請(qǐng)已駁回,原因:" + rejectReason);
}
}③ 執(zhí)行退款 Delegate
@Component("executeRefundDelegate")
publicclass ExecuteRefundDelegate implements JavaDelegate {
@Autowired
private RefundService refundService;
@Override
public void execute(DelegateExecution execution) {
System.out.println("開始執(zhí)行退款操作");
String orderId = (String) execution.getVariable("orderId");
Double refundAmount = (Double) execution.getVariable("refundAmount");
// 調(diào)用支付網(wǎng)關(guān)執(zhí)行退款
boolean refundSuccess = refundService.executeRefund(orderId, refundAmount);
if (refundSuccess) {
// 更新退款單狀態(tài)為“已退款”
refundService.updateRefundStatus(orderId, RefundStatus.REFUNDED, "退款成功");
execution.setVariable("refundResult", "success");
System.out.println("訂單[" + orderId + "]退款執(zhí)行成功,金額:" + refundAmount);
} else {
// 更新退款單狀態(tài)為“退款失敗”
refundService.updateRefundStatus(orderId, RefundStatus.REFUND_FAILED, "支付網(wǎng)關(guān)退款失敗");
execution.setVariable("refundResult", "fail");
System.out.println("訂單[" + orderId + "]退款執(zhí)行失敗");
}
}
}④ 發(fā)送退款通知 Delegate
@Component("sendRefundNotifyDelegate")
publicclass SendRefundNotifyDelegate implements JavaDelegate {
@Autowired
private SmsService smsService;
@Override
publicvoid execute(DelegateExecution execution) {
System.out.println("開始發(fā)送退款通知");
String userId = (String) execution.getVariable("userId");
String orderId = (String) execution.getVariable("orderId");
Double refundAmount = (Double) execution.getVariable("refundAmount");
// 獲取用戶手機(jī)號(hào)(模擬)
String phone = smsService.getUserPhone(userId);
// 發(fā)送短信通知
String content = "【電商平臺(tái)】您的訂單" + orderId + "已成功退款" + refundAmount + "元,請(qǐng)注意查收。";
smsService.sendSms(phone, content);
System.out.println("已向用戶[" + userId + "]的手機(jī)號(hào)[" + phone + "]發(fā)送退款通知:" + content);
}
}4. 第四步:寫接口測(cè)試流程
創(chuàng)建 Controller,提供接口讓前端調(diào)用,觸發(fā)流程和處理審核:
@RestController
@RequestMapping("/refund")
publicclass RefundController {
@Autowired
private RuntimeService runtimeService; // Flowable的運(yùn)行時(shí)服務(wù),用于啟動(dòng)流程
@Autowired
private TaskService taskService; // Flowable的任務(wù)服務(wù),用于處理用戶任務(wù)(審核)
// 1. 用戶提交退款申請(qǐng)(啟動(dòng)流程)
@PostMapping("/apply")
publicString applyRefund(@RequestBody RefundApplyDTO applyDTO) {
// 設(shè)置流程變量
Map<String, Object> variables = new HashMap<>();
variables.put("orderId", applyDTO.getOrderId());
variables.put("refundAmount", applyDTO.getRefundAmount());
variables.put("refundReason", applyDTO.getRefundReason());
variables.put("userId", applyDTO.getUserId());
// 啟動(dòng)流程實(shí)例
ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("afterSalesRefundProcess", variables);
System.out.println("退款流程已啟動(dòng),流程實(shí)例ID:" + processInstance.getId());
return"退款申請(qǐng)?zhí)峤怀晒?,流程?shí)例ID:" + processInstance.getId();
}
// 2. 商家審核退款申請(qǐng)
@PostMapping("/merchant/audit")
publicString merchantAudit(@RequestBody MerchantAuditDTO auditDTO) {
// 根據(jù)流程實(shí)例ID和任務(wù)負(fù)責(zé)人(商家)查詢?nèi)蝿?wù)
Task task = taskService.createTaskQuery()
.processInstanceId(auditDTO.getProcessInstanceId())
.taskAssignee("merchant") // 商家的用戶ID
.singleResult();
if (task == null) {
return"未找到待審核的任務(wù)";
}
// 設(shè)置審核結(jié)果變量
Map<String, Object> variables = new HashMap<>();
variables.put("merchantAuditResult", auditDTO.getAuditResult()); // pass/reject
variables.put("merchantAuditComment", auditDTO.getAuditComment());
// 如果審核不通過,設(shè)置駁回原因
if ("reject".equals(auditDTO.getAuditResult())) {
variables.put("rejectReason", "商家審核不通過:" + auditDTO.getAuditComment());
}
// 完成任務(wù)(提交審核結(jié)果)
taskService.complete(task.getId(), variables);
return"商家審核已提交,結(jié)果:" + auditDTO.getAuditResult();
}
// 3. 財(cái)務(wù)審核退款申請(qǐng)
@PostMapping("/finance/audit")
publicString financeAudit(@RequestBody FinanceAuditDTO auditDTO) {
// 根據(jù)流程實(shí)例ID和任務(wù)負(fù)責(zé)人(財(cái)務(wù))查詢?nèi)蝿?wù)
Task task = taskService.createTaskQuery()
.processInstanceId(auditDTO.getProcessInstanceId())
.taskAssignee("finance") // 財(cái)務(wù)的用戶ID
.singleResult();
if (task == null) {
return"未找到待審核的任務(wù)";
}
// 設(shè)置審核結(jié)果變量
Map<String, Object> variables = new HashMap<>();
variables.put("financeAuditResult", auditDTO.getAuditResult()); // pass/reject
variables.put("financeAuditComment", auditDTO.getAuditComment());
// 如果審核不通過,設(shè)置駁回原因
if ("reject".equals(auditDTO.getAuditResult())) {
variables.put("rejectReason", "財(cái)務(wù)審核不通過:" + auditDTO.getAuditComment());
}
// 完成任務(wù)(提交審核結(jié)果)
taskService.complete(task.getId(), variables);
return"財(cái)務(wù)審核已提交,結(jié)果:" + auditDTO.getAuditResult();
}
// 4. 查詢流程狀態(tài)
@GetMapping("/status/{processInstanceId}")
publicString getProcessStatus(@PathVariableString processInstanceId) {
ProcessInstance processInstance = runtimeService.createProcessInstanceQuery()
.processInstanceId(processInstanceId)
.singleResult();
if (processInstance == null) {
// 流程已結(jié)束,查詢歷史狀態(tài)
HistoricProcessInstance historicProcessInstance = runtimeService.createHistoricProcessInstanceQuery()
.processInstanceId(processInstanceId)
.singleResult();
return"流程已結(jié)束,結(jié)束時(shí)間:" + historicProcessInstance.getEndTime() + ",狀態(tài):" + historicProcessInstance.getState();
} else {
// 流程未結(jié)束,查詢當(dāng)前任務(wù)
Task currentTask = taskService.createTaskQuery()
.processInstanceId(processInstanceId)
.singleResult();
String currentTaskName = currentTask != null ? currentTask.getName() : "無當(dāng)前任務(wù)";
return"流程運(yùn)行中,當(dāng)前狀態(tài):" + processInstance.getState() + ",當(dāng)前任務(wù):" + currentTaskName;
}
}
}5. 第五步:測(cè)試流程完整運(yùn)行
用 Postman 或 curl 調(diào)用接口,模擬整個(gè)退款流程:
① 提交退款申請(qǐng)
- 請(qǐng)求 URL:http://localhost:8080/refund/apply
- 請(qǐng)求體:
{
"orderId": "ORDER_20250826_003",
"refundAmount": 100.0,
"refundReason": "商品質(zhì)量問題",
"userId": "USER_001"
}- 響應(yīng):退款申請(qǐng)?zhí)峤怀晒Γ鞒虒?shí)例ID:12501
② 商家審核通過
- 請(qǐng)求 URL:http://localhost:8080/refund/merchant/audit
- 請(qǐng)求體:
{
"processInstanceId": "12501",
"auditResult": "pass",
"auditComment": "同意退款"
}- 響應(yīng):商家審核已提交,結(jié)果:pass
③ 財(cái)務(wù)審核通過
- 請(qǐng)求 URL:http://localhost:8080/refund/finance/audit
- 請(qǐng)求體:
{
"processInstanceId": "12501",
"auditResult": "pass",
"auditComment": "同意退款,已安排打款"
}- 響應(yīng):財(cái)務(wù)審核已提交,結(jié)果:pass
④ 查詢流程狀態(tài)
- 請(qǐng)求 URL:http://localhost:8080/refund/status/12501
- 響應(yīng):流程已結(jié)束,結(jié)束時(shí)間:2025-08-26T15:30:45.123+08:00,狀態(tài):COMPLETED
后臺(tái)日志會(huì)打印整個(gè)流程的執(zhí)行過程,從驗(yàn)證金額到商家審核、財(cái)務(wù)審核、執(zhí)行退款、發(fā)送通知,一步不差。如果產(chǎn)品經(jīng)理想改流程(比如加個(gè) “客服初審” 步驟),只需要在 BPMN 圖里加個(gè)用戶任務(wù),不用改任何 Java 代碼 —— 這就是流程引擎的強(qiáng)大之處。
四、流程編排不是 “銀彈”,這些坑你得避開
講了這么多流程編排的好處,不是說它能解決所有問題。就像你不會(huì)用大炮打蚊子一樣,流程編排也有它的適用場(chǎng)景,用錯(cuò)了反而會(huì)增加復(fù)雜度。我總結(jié)了幾個(gè)項(xiàng)目里踩過的坑,大家一定要注意:
1. 「過度設(shè)計(jì)」:簡(jiǎn)單業(yè)務(wù)用了復(fù)雜流程,純屬自找麻煩
有些兄弟剛學(xué)會(huì)流程編排,就不管什么業(yè)務(wù)都想用。比如一個(gè)簡(jiǎn)單的 “根據(jù)用戶等級(jí)返回折扣” 的邏輯,用一行 if-else 就能搞定:
public double getDiscount(User user) {
if (user.getLevel() == Level.VIP1) return 0.95;
else if (user.getLevel() == Level.VIP2) return 0.9;
else return 1.0;
}結(jié)果非要用責(zé)任鏈模式,拆成Vip1DiscountNode、Vip2DiscountNode、NormalUserDiscountNode,還建個(gè)流程鏈 —— 這就是 “為了用技術(shù)而用技術(shù)”,反而增加了代碼量和維護(hù)成本。記住:業(yè)務(wù)簡(jiǎn)單用 if-else,業(yè)務(wù)復(fù)雜用流程編排。判斷標(biāo)準(zhǔn)很簡(jiǎn)單:如果你的 if-else 嵌套超過 3 層,或者業(yè)務(wù)規(guī)則經(jīng)常變動(dòng),再考慮流程編排。
2. 「忽略異常處理」:流程斷了沒人管,線上出問題才慌
我之前重構(gòu)訂單流程時(shí),忘了給 “庫存扣減” 節(jié)點(diǎn)加異常處理,結(jié)果有次庫存服務(wù)超時(shí),流程卡在了庫存扣減步驟,訂單狀態(tài)一直是 “待庫存扣減”,用戶付了錢卻看不到訂單狀態(tài)更新,投訴了一大堆。
后來我在每個(gè)節(jié)點(diǎn)都加了異常重試和降級(jí)邏輯:
// 改進(jìn)后的庫存扣減節(jié)點(diǎn)
publicclass StockDeductNode implements OrderProcessNode {
private StockService stockService = new StockService();
private RetryTemplate retryTemplate; // 重試模板
public StockDeductNode() {
// 初始化重試模板:重試3次,每次間隔1秒
retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(3));
retryTemplate.setBackOffPolicy(new FixedBackOffPolicy() {{
setBackOffPeriod(1000);
}});
}
@Override
public boolean process(Order order) {
try {
// 重試3次庫存扣減
return retryTemplate.execute(context -> {
boolean deductSuccess = stockService.deductStock(order.getProductId(), order.getQuantity());
if (!deductSuccess) {
thrownew RuntimeException("庫存扣減失敗,重試中...");
}
System.out.println("庫存扣減成功");
returntrue;
});
} catch (Exception e) {
// 重試失敗后降級(jí):記錄日志,觸發(fā)人工介入
System.out.println("庫存扣減失?。ㄒ阎卦?次),觸發(fā)人工處理:" + e.getMessage());
sendAlertToStaff(order); // 發(fā)送告警給運(yùn)營(yíng)人員
returnfalse;
}
}
}不管用哪種流程編排方案,都要考慮 “節(jié)點(diǎn)失敗了怎么辦”,是重試、降級(jí)還是終止流程,必須提前定義好。
3. 「流程可視化過度依賴」:把所有邏輯都畫在圖里,調(diào)試起來想死
用 Flowable/Camunda 的時(shí)候,有些團(tuán)隊(duì)喜歡把所有業(yè)務(wù)邏輯都用 BPMN 的網(wǎng)關(guān)和表達(dá)式實(shí)現(xiàn),比如在條件表達(dá)式里寫復(fù)雜的判斷:
<sequenceFlow id="flow4" sourceRef="validateGateway" targetRef="merchantAuditTask">
<conditionExpression xsi:type="tFormalExpression">
#{validateResult == 'pass' && refundAmount > 100 && userLevel == 'VIP' && orderCreateTime > '2025-08-01'}
</conditionExpression>
</sequenceFlow>這種表達(dá)式寫多了,調(diào)試起來特別麻煩 —— 流程走到這一步?jīng)]按預(yù)期走,你得去看 BPMN 圖里的表達(dá)式有沒有寫錯(cuò),還得查流程變量的值,比看 Java 代碼還費(fèi)勁。正確的做法是:復(fù)雜邏輯寫在 Delegate 類里,BPMN 圖只負(fù)責(zé)流程走向。表達(dá)式只用來做簡(jiǎn)單的條件判斷(比如#{validateResult == 'pass'}),這樣調(diào)試的時(shí)候,直接看 Delegate 類的日志就行。
4. 「不考慮性能」:流程節(jié)點(diǎn)太多,響應(yīng)時(shí)間變慢
流程編排本質(zhì)上是把一個(gè)方法拆成多個(gè)方法執(zhí)行,節(jié)點(diǎn)越多,方法調(diào)用次數(shù)越多,性能開銷也越大。我之前見過一個(gè)流程有 20 多個(gè)節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)都要查數(shù)據(jù)庫,結(jié)果整個(gè)流程執(zhí)行下來要 5 秒多,用戶直接吐槽 “比蝸牛還慢”。
后來我們做了優(yōu)化:
- 合并相似節(jié)點(diǎn):把 “查詢用戶信息” 和 “查詢用戶等級(jí)” 兩個(gè)節(jié)點(diǎn)合并成一個(gè),減少數(shù)據(jù)庫查詢次數(shù)
- 異步執(zhí)行非關(guān)鍵節(jié)點(diǎn):把 “記錄日志”“發(fā)送通知” 這些不影響主流程的節(jié)點(diǎn)改成異步執(zhí)行
- 緩存常用數(shù)據(jù):把用戶等級(jí)、商品價(jià)格這些高頻訪問的數(shù)據(jù)緩存起來,避免重復(fù)查詢
優(yōu)化后流程執(zhí)行時(shí)間降到了 1 秒以內(nèi),用戶體驗(yàn)明顯提升。所以設(shè)計(jì)流程的時(shí)候,一定要考慮 “每個(gè)節(jié)點(diǎn)的執(zhí)行時(shí)間”,別讓流程變成 “性能瓶頸”。
五、總結(jié):從 “if-else 地獄” 到 “流程編排天堂”,就差這一步
回顧一下這篇文章,咱從 if-else 的痛點(diǎn)說起,講了流程編排的核心思想,還演示了三種不同量級(jí)的實(shí)現(xiàn)方案:
- 輕量級(jí):責(zé)任鏈 + 策略模式,適合簡(jiǎn)單流程,不用引入框架
- 中量級(jí):Spring StateMachine,適合狀態(tài)流轉(zhuǎn)類業(yè)務(wù)
- 重量級(jí):Flowable/Camunda,適合復(fù)雜流程和可視化需求
最后還提醒了大家要避開的坑,希望能幫你徹底告別 if-else 的噩夢(mèng)。
其實(shí)流程編排不只是一種技術(shù),更是一種 “分而治之” 的思維 —— 把復(fù)雜的問題拆成簡(jiǎn)單的小問題,再把小問題的解決方案按順序組合起來。這種思維不僅能用于代碼開發(fā),在日常工作中也很有用,比如你寫一篇技術(shù)文章,也可以拆成 “列大綱→寫初稿→改內(nèi)容→校對(duì)錯(cuò)” 這幾個(gè)步驟,一步步來,效率會(huì)高很多。
































