京東售後系統架構設計:專治多端併發、資料不一致的臭毛病

陶然陶然發表於2022-11-02

   前言

  透過閱讀本文,您將瞭解到一個售後系統應該具備的一些能力、在整個上下游系統中的定位、基本的系統架構,以及針對售後業務場景中常見問題的解決方案。

   一、核心價值

  京東到家售後系統作為逆向流,強依賴京東到家業務域,目前涵蓋了:退款、退貨、換貨、維修等四大類場景,並且為使用者與商家提供申訴、仲裁場景支援,為計費與結算系統提供逆向金額資料支援。

  售後系統業務結構:  

  售後系統上下游依賴:  

   二、系統架構

  售後系統使用的就是基礎的三層架構。應用層有不同身份的三個端入口,服務層提供了一些業務支援和資料支援,資料層目前使用到了MySQL和Redis以及ElasticSearch。當然還有一些中介軟體使用,比如rpc框架,zk配置中心,worker分散式定時任務,jmq訊息。還有完善的基礎設施,統一監控和日誌採集。  

   三、業務形態

  當正向訂單履約完成後,如訂單中商品有缺件、錯件、質量等問題可以發起售後申請。目前申請售後支援使用者端、商家端、到家客服發起。使用者端申請需要根據不同責任方分配到商家或者客服稽核。商家端只能選擇商家責任原因申請售後,然後自動稽核透過。客服代使用者申請售後和使用者端一致,流轉到商家或客服稽核。

  使用者端申請售後流程:  

  1. 申請售後

  1)多端操作併發場景下問題

  2)售後商品拆分資訊如何獲取

  在正向訂單履約完成後一定的時效內,可以透過使用者端,商家端,運營端基於訂單中商品選擇性申請售後。當接收到一個售後單提交申請,售後這邊會依賴訂單資料,拆分資料來構建售後單詳情資料。那麼對於多端申請售後入口,我們怎麼能保證訂單中商品不會被重複申請呢?申請時我們使用了redis分散式鎖。

  售後申請場景下分散式鎖需要注意點:

  ①不同的入口使用相同的key,這裡我們透過字首加訂單號來區分,來保證對同一訂單加鎖。

  ②加入過期時間,比如第 一個申請獲取到鎖,如果釋放鎖異常,這裡只需要等到超時時間自動過期,防止死鎖。

  ③等待鎖時間,同一個訂單多個入口同時申請售後,如果獲取不到鎖就進入等待,直到獲取到鎖或者等待超時後退出。

  ④使用uuid來保證token確定性,每次都釋放自己當前請求鎖。

  我們保證了同一時間只能有一個訂單下的售後能夠申請,接下來就是組裝售後單詳情資料。一個完整的售後單資料來源於訂單詳情和拆分詳情。

  透過從訂單詳情中取使用者基礎資訊,訂單資訊,商家門店資訊來儲存到售後單主表中。根據申請選擇的商品skuid從訂單商品詳情中獲取對應商品基礎資訊儲存到售後商品表中。接下來就是比較重要的售後商品拆分資訊,這個資料來源於拆分系統。先了解下拆分資料結構:  

  可以看到,拆分系統會根據訂單中所有商品把金額拆分到每一件商品上,並且透過num_下標來區分。當選擇訂單中某個商品發起售後我們是怎麼去找到這個商品對應的拆分資訊呢?

  我們透過sku_promotionType(商品+促銷型別)來區分不同的商品拆分資訊,然後透過記錄num商品下標來確定找到哪一個商品。

  比如下面的場景:

  假設訂單中購買了3個正價A商品,1個促銷A商品。

  ①第 一次申請一個正價A售後。這時售後系統會記錄一個售後單,對應售後詳情為商品A。從拆分獲取sku_A_正價_num0資訊並記錄到售後商品拆分詳情表。

  ②再申請一個正價A和一個促銷A售後。這裡售後會發現此訂單已申請過一個正價A,記錄的是sku_A_正價_num0。這時就會去取拆分的 sku_A_正價_num1這條資料。

  ③第二次申請售後對應一個新售後單,商品詳情記錄為sku_A_正價,sku_A_促銷。商品拆分記錄資料為:sku_A_正價_num1,sku_A_促銷_num0。

  初步瞭解了售後商品獲取對應拆分資料的邏輯,這時如果同一個訂單中購買了相同促銷的A商品,但是價格不一樣怎麼辦呢?按照上面獲取邏輯,獲取的售後商品金額就會出現多退或者少退情況。

  比如下面的捆綁促銷:

  A+B捆綁銷售,A金額3元。A+C捆綁銷售,此時A金額2元。這時拆分的資料結構為:sku_A_捆綁_num0價格3元,sku_A_捆綁_num0價格2元。此時如果兩個A都申請了售後,我們再按照sku_promotionType去獲取拆分那麼永遠獲取的都是第 一個的金額。因此針對這種特殊的促銷場景,我們在原有獲取拆分維度基礎上又增加了一個價格。

  區分維度:sku_promotionType_price(商品+促銷型別+價格)

  上面的方案可以滿足各種不同促銷場景的售後,但是針對稱重退差訂單申請售後還會適用麼?

  稱重退差訂單含義:當正向訂單揀貨時,商家發現實際揀貨的稱重品和售賣規格有誤差,此時可以發起退差單把差額的錢退給使用者。之後訂單正常履約,訂單完成後使用者也可以申請售後。此時再申請售後退給使用者的錢就應該是減去退差後的部分。

  比如下面的場景:

  假設一個訂單中買了2個原價A+1個促銷價A,原價3元,促銷價2元,整單共8元。揀貨時發現A商品實際重量比標重少,退差1元,此時退差單中會記錄商品A退差金額,退差重量。這時選擇正價A發起售後申請,售後系統就需要根據實際重量獲取退差商品金額,然後計算實際退款金額。這時我們又在原來的基礎上增加了一個重量維度。

  sku_promotionType_price_weight(商品+促銷型別+價格+重量)

  系統都是為了業務來服務的,隨著業務變更場景的增多,我們的架構也在演變。目前所有的計算拆分邏輯都封裝成統一方法,統一入口,未來再增加不同促銷,或者其他業務都可以很友好的支援。

  2. 稽核售後

  1)多條件複雜查詢效能問題

  當售後單申請成功後,會根據稽核方分配給商家或者客服稽核。這裡涉及到兩個列表查詢,一個是運營端客服使用,一個是商家端根據商家賬號許可權來展示可操作的售後單列表。最初我們的售後單表資料並不是很大,隨著業務品類擴增以及使用者量的增加遇到了一些問題。

  ①資料庫頻繁報警,慢SQL,影響其他業務

  ②商家運營反饋售後單列表查詢過慢,影響稽核效率。

  透過分析慢SQL日誌,我們根據查詢欄位增加索引來提高查詢速率。由於支援各種查詢場景過多,目前主表中已經建立了20多個索引。而且基於業務的發展需要支援查詢的時間區間也會更長。主表的資料量一直在增長,還是會遇到查詢效能問題,過多的索引對於售後單流程中變化更新也有一定的影響。

  因為ES是基於倒排索引實現的搜尋,配合分詞器在文字模糊搜尋上表現比較好,使用的業務場景廣泛,因此我們考慮把售後單資料同步到ES中,列表查詢走ES。

  基於我們目的是為了解決查詢問題,每次操作業務都會根據主鍵再查詢一次mysql庫詳情,資料遷移同步方案如下:  

  ①存量資料如何同步?

  首先增加一個開關來控制操作是走mysql還是es。先關閉開關然後透過批次同步介面,根據主鍵id範圍區間查詢把存量資料分批同步到ES中。

  開啟開關,這時如果有新的售後單資料,透過MQ非同步同步到ES中,同時把開關開啟前產生的一部分資料同步到ES中。

  最後再透過count總數校驗下資料是否全部同步。

  ②如何保證資料同步一致性?

  涉及到同步資料,難免就會有資料不一致問題。從售後單申請到售後單狀態變更,提交事務後每個節點都會傳送一個需要同步的MQ訊息。

  接收到訊息後透過主鍵id查詢mysql獲取售後單詳情。然後全量欄位同步到ES中。

  這樣不管先消費哪個節點的MQ,同步的資料都是實時查詢的資料庫,以此來保證每次同步的資料都是當時新資料。

  ③資料延遲怎麼處理?

  MQ消費有延時,就有可能造成ES和mysql中資料狀態不一致問題。我們只是為了解決查詢效能問題,因此所有複雜查詢都是查的ES資料,但當商家或者客服操作售後單時會根據主鍵查詢mysql售後單詳情,然後執行稽核操作。

  針對所有的業務操作後端也增加了前置狀態校驗,來遮蔽這種資料延時帶來的問題。

  沒有好的方案,只有適用自己業務的方案。當然現在也有一些工具類外掛可以支援不同的同步方案,比如cancel基於binlog的同步以及CloudCanal。我們的目的是為了解決查詢效率問題,因此選擇了上面的同步方案。

  3. 售後退貨

  1)合單召喚物流配送方案

  退貨退款售後單,商家或平臺稽核透過後,需要退回訂單中貨物。這裡就需要與達達互動,召喚配送員走逆向取件流程。在建立運單召喚達達配送前售後這邊會有一個合單邏輯。  

  ①合單思想

  訂單完成後申請售後可以分多次申請,每次可以選擇不同數量的商品。

  如果使用者同一個訂單中商品分多次售後都申請為退貨,那麼在售後單稽核透過後這些售後的商品都需要配送員送回商家。

  這裡為了提升使用者多次退貨體驗,也同時為了節約配送成本。因此就需要有一個合單邏輯,同一訂單下的售後單退貨只需召喚一次物流配送即可。

  ②合單邏輯

  合單worker定時掃描待召喚物流的售後單,當到達使用者預計取件開始時間前10分鐘就會觸發需要合單的任務。

  合單任務會根據訂單號獲取此訂單下所有需合單的售後單,然後獲取預計取件開始時間最近的售後單。

  依據最近上門取件開始時間來建立物流運單。

  ③建立運單

  建立運單前需要前置狀態校驗,只處理待退貨售後單。然後組裝訂單下使用者基本資訊,需要合單的所有售後單商品資訊以及累計重量,建立運單。

  運單介面根據訂單號做冪等處理,重複呼叫會返回相同的運單號。

  ④接收結果

  透過監聽運單狀態訊息,來同步更新配送員資訊。

  ⑤異常重試

  針對合單任務失敗資料,記錄失敗標識,等待下次合單worker執行。

  記錄失敗次數,如果超過失敗最大次數,跳過合單並預警處理。避免一直合單失敗的資料影響正常合單業務資料。

  4. 售後退款

  1)退款準確性問題  

  透過上面的流程圖瞭解了售後單稽核退款到退款結束的一個過程。那麼我們都做了哪些來保證稽核退款的售後單金額是正確的呢?

  ①增加分散式鎖

  商家角色稽核退款可以透過商家中心、商家端APP、系統對接介面。同時客服端也可以透過運營平臺稽核退款。

  因為這裡也涉及多端操作,所以這裡的鎖主要為了防止重複稽核退款。

  稽核退款時已經確定是售後單維度,每個售後單隻能稽核退款一次,所以這裡的key維度是售後單維度。並且獲取不到鎖直接丟擲失敗,提示業務異常。

  ②單行商品合法性校驗

  為什麼要做單行商品合法性校驗呢?可以看下下面這個場景:

  假設當前訂單購買了1個A商品和2個B商品,A商品單價10元,B商品單價15元,整單金額40元。申請售後介面引數為:

  skuList:[{"skuCount":1,"skuName":"skuA","procotionType":"1"},{"skuCount":1,"skuName":"skuA","promotionType":"1"}]

  系統對接的商家透過到家開放平臺釋出的售後介面建立售後單,由於開放平臺入口面對的是所有商家,每個商家系統對接能力不一樣,可以看出訂單中只買了1個A商品,但是傳了兩遍。正常我們的做法是解析入參list,然後校驗每一行商品的合法性。查詢當前訂單已申請商品個數,以及訂單中總商品個數,然後與當前稽核售後單商品個數做比較。但是迴圈比較等於比較了兩次,每次個數都是1。而且由於2個商品A總額小於訂單總額,所以即使有後面的臺賬總額校驗,還是會造成多退情況。因此這裡需要根據當前申請商品總數加已申請此商品總數與訂單中商品總數做校驗。

  ③訂單臺賬金額校驗

  訂單臺賬金額校驗,是最後一道校驗,校驗的維度不同,是獲取每一項支付明細剩餘可退金額。

  校驗當前要退售後單金額與臺賬餘額比較,必須小於等於臺賬餘額。

  ④非同步退款結果

  稽核退款後,透過非同步接收退款mq來更新退款狀態。

  退款成功通知下游依賴系統。

   總結

  逆向售後的業務是依賴於正向訂單的,隨著正向單不同場景玩法的增加,售後需要支援的場景也在增多,我們也在不斷的迭代進步。在這當中也遇到了一些需要解決和完善的問題,比如售後系統沒有自己的閘道器,這樣會造成業務邏輯維護多處,業務不閉環。整個售後業務中各種不同場景下邏輯配置都不同,我們也在規劃透過模板引擎配置做到智慧化。最後也非常歡迎大家留言交流,共同進步。

來自 “ 達達集團技術 ”, 原文作者:姚飛濤;原文連結:http://server.it168.com/a2022/1027/6770/000006770522.shtml,如有侵權,請聯絡管理員刪除。

相關文章