電商課題VII:支付交易一般性准則


@鄭昀匯總 創建於2012/11

發布版本號:v1.3
概念:
退款期限,交易,交易關閉,交易結束,掉單,冪等性,數據一致性
 
關鍵詞:
歷史記錄不得直接篡改原則,
交易關閉通知處理,退款處理結束通知,
掉單被動處理,掉單主動處理,
多個渠道的重復支付處理,
支付成功時商品不可售賣的處理,
訂單金額變化交易流水號變化規則,
推送訂單不得包含違禁詞,
支付通知並發到達的處理,
支付子系統的獨立性和可靠性,
補錄數據的時間准則

一. 通用規則
1.1. 歷史記錄不得直接篡改
電商核心服務基本都是分布式應用,分布式事務如處理不妥善,容易產生數據不一致。一旦出現數據不一致,一定要有旁證來修正。
所以數據庫中以下關鍵資源的記錄, 鄭昀提醒您注意, 原則上不允許直接修改歷史數據
  • 下單;
  • 支付購買;
  • 生碼/驗碼/物流信息記錄;
  • 退款(含部分退款);
  • 結算;
  • 用戶注冊;
  • 與第三方數據同步;
這里的“直接修改”特指,沒有把變更行為記錄到日志表里,而是直接在原始記錄上 update 甚至 delete ,這種“篡改”和“毀屍滅跡”是明文禁止的,即使留下了文件類型日志也是不允許的。
第一,要修改這些記錄的關鍵字段時, 必須在相關日志表里保留變更日志,並記錄操作人和發起人, 一定要確保歷史可回溯
第二,嚴禁對記錄做物理刪除,只能是軟刪除。
 
實例:
對於××團收到第三方支付的通知,我們有第三方交易流水記錄表;
對於××團發起的到第三方支付的交易請求。我們有 jxxxe_pay_log 記錄;
訂單操作變更記錄,我們有 jxxxe_order_action日志表記錄。
 
Q:什么叫歷史可回溯?
A:系統可能對關鍵記錄做了一系列修改,甚至有程序在某個時間段內誤寫引入了臟數據,但 鄭昀提示您,我們依然要能從各種操作日志表中隨時倒推回歷史某一個時刻的快照,一是確保隨時能安全地把數據還原回去,二是管理平台可以清晰地展示出由誰引發、怎么變化的歷史,三是便於排查問題。
譬如,對於記錄了訂單信息的 order_info 表,會員如果點擊使用賬戶余額支付了訂單的應付金額,那么該訂單操作日志表就會做如下記錄,原訂單記錄的重要字段(what)在什么時候(when)從什么變為了什么(how),都會詳細記錄。
 
1.2. 對關鍵資源的操作,當接口保證不了冪等性時,必須能防並發
如果你的接口不具備 冪等性,那么請保證 整個(分布式)系統內對一個重要事物(訂單,賬戶的資金變動等)的 有效操作線程同一時間內有且只有一個
比如交易中心有N台服務器負載均衡,訂單中心則有M台服務器,如何保證一個訂單的同一筆支付處理,一個賬戶的同一筆資金變動操作是原子性的。
 
原因也很簡單:
  • 第三方支付平台可能同一時刻給你的 pay.5xxxxn.com 交易中心集群服務推送過來兩個一模一樣的支付成功通知。
  • 用戶瀏覽器可能安裝了某種插件(如早期的迅雷插件),插件本身為了探測 一個URL 是否是BT資源,會同時模擬發起一個 HTTP GET 請求。
  • 上游服務不可控,不可預知地向發起下游服務發起並發請求。
 
此時可以基於 memcache 實現一個分布式鎖,更多詳情請閱讀 鄭昀撰寫的《 電商課題:分布式鎖》。
 
1.3. 支付子系統的獨立性
電商業務容易出現以下問題:
  • 受到DDoS攻擊,帶寬被打滿;
  • 某一個業務突然響應變慢,如從20ms激增為1s,業務請求被大量阻塞;
所以不同業務之間必須嚴格隔離,防止一個業務超負荷或宕機連累其他業務。
因此,我們至少要做到:
  • 支付子系統是一個獨立工程,獨立部署,有單獨的二級域名。
最好能做到:
  • 獨立帶寬,
  • 獨立的存儲介質。
 
1.4. 支付子系統接收第三方支付通知的可靠性
電商的支付子系統提供一個 Web Service,來接收各家第三方支付的各種異步通知。
接收對方通知之后,你可能會遭遇各種異常,如:
  • 解析時發生異常;
  • 調用支付中心時發生異常,如網絡故障,如支付中心宕機,如調用超時;
  • 寫日志時發生異常;
即使如此,你也不應該丟棄該通知,並且沒有返回“success”字符給第三方支付,
因為這樣的話,相當於你的業務完全依賴於第三方支付下一次重試了。
所以, 鄭昀提醒您,你應該主動地、積極地先把對方的(支付成功、退款處理等)通知存入一個存儲介質,如消息隊列;
一旦同步處理失敗,那么能以某種策略重播這個消息,直到業務系統恢復正常、處理完畢為止。
 
1.5. 補錄數據的時間准則
電商結算和對賬,由於帳期定義(如日清日結,如T+2結算,如銷售佣金計算,如CPS聯盟結算),非常依賴於數據記錄的時間。
所以,當由於以下原因補錄或同步數據時, 請慎重考慮數據的時間字段到底如何采信
  • 掉單后主動處理(注:主動查詢第三方支付網關,獲得訂單支付狀態);
  • 不同系統之間同步失敗后手動觸發重新同步;
  • 不同公司的平台之間交換退款等數據。
 
下面舉兩個小例子:
例一:
××商城對供應商的結算標准是, 僅僅以訂單進入他們的ERP系統的時間為准,而不是以該訂單的下單時間、支付時間等數據自身時間記錄為准。
即,一個12月1日23:58支付成功的訂單,數據一層一層傳遞到ERP時,同步時間是12月2日00:01,那么此訂單就被判定在12月2日的應結算明細中,而不是12月1日的。
咋聽上去好像不合理,仔細想想,供應商有很多,IT系統也就很多,彼此之間的服務器時間肯定不同步,更別提會有很多種類型的臟數據,所以××商城只有選擇 用ERP系統自身的時間作為唯一結算憑據,而不采信第三方系統的 add_time、update_time、pay_time 五花八門的時間,這樣才不會重復結算或漏結算。
 
例二:
支付系統宕機,一段時間后才恢復,此時客服主動處理顧客投訴掉單的訂單,從第三方支付查到已支付后,將訂單置為已付款。那么,該訂單的支付時間怎么記呢?
一是,采信第三方支付系統傳遞的真實支付時間。二是,記錄為手動重置的當前時間。
鄭昀的答案是,后者更安全。
因為,有可能補錄數據已跨日或跨月,前一日的結算清單可能已計算完畢,如果按前者的邏輯,突然又補錄一條記錄,結果前一日(上一個月)也不結算它,后一日(下一個月)也不結算,那這個訂單就漏結算了。
當然,真實支付時間也還是要記錄到日志表的。
 

二. 易被忽略的邏輯處理
2.1. 交易關閉通知的處理
支付寶是這么定義“ 交易關閉”的:
枚舉名稱
枚舉說明
TRADE_CLOSED
  • 指定時間段內未支付時關閉的交易;
  • 在交易完成全額退款成功時關閉的交易。
交易關閉通知默認是不發送的,如下表格所示:
觸發條件名
觸發條件描述
觸發條件默認值
TRADE_CLOSED
交易關閉
false(不觸發通知)
如果商戶(也就是你的網站)向支付寶申請打開了該配置,那么請注意接收 TRADE_CLOSED 通知,它會對你的核心購買邏輯產生影響。
 
如何主動指定交易關閉時間呢?
即時到帳交易接口中有這么一個參數:
參數
參數名稱
類型
參數說明
是否可為空
樣例
it_b_pay
超時時間
String(3)
設置未付款交易的超時時間,一旦超時,該筆交易就會自動被關閉。
取值范圍:1m~15d。
m:分鍾、h:小時、d:天、1c:當天(無論交易何時創建,都在0點關閉)。
該功能需要聯系技術支持來配置關閉時間。
可空
1h
支付寶收到這個參數后,界面會有如下展示:
http://images.cnblogs.com/cnblogs_com/zhengyun_ustc/255879/o_clip_image001%20-%20001%E5%89%AF%E6%9C%AC.jpg
 
此時提示幾點:
1)如果交易已經關閉,但商戶的網站上仍保留了訂單的“付款”按鈕,那么點擊跳轉到支付寶后,會看到如下警告信息:
http://images.cnblogs.com/cnblogs_com/zhengyun_ustc/255879/r_clip_image002%20-%20002%e5%89%af%e6%9c%ac.jpg  
http://images.cnblogs.com/cnblogs_com/zhengyun_ustc/255879/r_clip_image002%20-%20003%e5%89%af%e6%9c%ac.jpg
2)當交易狀態為交易關閉時,就算用戶能通過第三方網銀對支付寶賬單進行付款(用戶可能已經跳轉至銀行交易頁面,並且未關閉頁面),第三方網銀能將支付成功信息通知支付寶,支付寶也不會通知商戶,而是會自動退還至支付寶余額中。
3) 網銀直連的訂單是關閉不了的,因為它沒有跟支付寶賬戶綁定。
4)商戶如發現交易已關閉的訂單被用戶支付后,那么必須進入 異常支付流程(能原路退返就退,如無法退返則返還至賬戶余額)。
 
2.2. 退款通知的處理
支付寶對此的定義是:
(1) 交易成功之后,商戶(高級即時到賬或機票平台商)可調用批量退款接口,系統會發送退款通知給商戶。
(2) 當商戶使用站內退款時,系統會發送包含 refund_status(退款狀態)和 gmt_refund(退款時間)字段的通知給商戶。
其中退款狀態有兩種:
枚舉名稱
枚舉說明
REFUND_SUCCESS
退款成功:
  • 全額退款情況:trade_status= TRADE_CLOSED,而refund_status=REFUND_SUCCESS
  • 非全額退款情況:trade_status= TRADE_SUCCESS,而refund_status=REFUND_SUCCESS
REFUND_CLOSED 退款關閉
第三方支付在退款處理完畢后,會發送異步通知給商戶,如下表格所示:
觸發條件名
觸發條件默認值
退款處理結束
true(觸發通知)

這個所謂退款處理結束通知,實際上仍是一個“trade_status_sync(交易狀態同步)”通知,特殊性在於攜帶的 refund_stauts =REFUND_SUCCESS 參數,實際例子如下所示:

http://images.cnblogs.com/cnblogs_com/zhengyun_ustc/255879/r_clipboard%20-%20004%e5%89%af%e6%9c%ac.png  
注意幾個要點:
  1. 當交易狀態為 TRADE_FINISHED(交易完成) ,那么不可退款
    • 此處有一個“退款期限”概念,交易關閉(TRADE_CLOSED)后3個月(或6個月)內可以退款,超過此期限后,該筆交易成功且結束,從此不可退款!對於業務邏輯,意味着此時只能退還金額到賬戶余額,無法原路退返。支付寶、快錢、財付通等均有此設定。
      • 手機支付退款如返回 D23190 錯誤碼,含義是退款日期超過最大有效期(有效期是半年)。
  2. 退款處理結束的通知到達時,支付寶會先發送一個支付成功通知,防止你的系統不知道有此交易。請正確處理這個支付成功通知,不要誤認為這是“重復支付”(因為對應的訂單可能已確認+已支付),以至於誤判給原路退返了。
 

 
三. 異常處理類
3.1. 掉單的被動處理
支付寶的文檔說的很清楚:
服務器異步通知頁面(由參數 notify_url 指定頁面文件)獲取支付寶返回的結果數據
,(商戶的)程序執行完后必須打印輸出“success”(不包含引號)。
如果商戶反饋給支付寶的字符不是 success 這7個字符,支付寶服務器會不斷重發通知,直到超過24小時22分鍾。
一般情況下,25小時以內完成8次通知(通知的間隔頻率一般是:2m,10m,10m,1h,2h,6h,15h);
所以,如果你用來接收支付寶異步通知的服務阻塞了(hang/stuck)或掛了(shutdown/crash),就無法給支付寶返回 success 響應,所以它會不斷地發起重試,直到25小時內你恢復服務返回 success 。
所以如果 掉單(用戶已付款/已扣款,但你的數據庫里這個訂單還是未付款狀態),你還有機會補救。
 
3.2. 掉單的主動處理
掉單后,等待支付寶補發通知給你,商戶可能來不及應對洶涌而來的顧客投訴。
此時,服務器端應該主動提交此訂單對應的(一個或多個) 唯一訂單號(out_trade_no,支付寶合作商戶網站唯一訂單號),調用支付寶的單筆交易查詢接口 single_trade_query,從而獲得交易狀態、支付寶交易號、付款時間、交易總金額等明細。
(具體細節請看支付寶的《單筆交易查詢接口(single_trade_query).pdf》)
一旦查詢到了支付成功的細節,而且付款金額也等於商戶記錄的應付金額,那么就可以給操作人員展示一個畫面,使得他能手工置這個訂單為已確認+已付款。
 
3.3. 來自於多個支付渠道的重復支付
什么情況下會產生重復支付呢?
看一個真實的顧客投訴案例:
顧客購買××團的商品后,第一次付款時,由於付款故障(如系統掉單),使得顧客認為付款未成功,所以,顧客換用了其他支付渠道(如選擇支付寶的網銀直連,或者選擇網銀在線)進行了第二次付款,於是××團帳號收到兩筆付款,且兩筆付款均付款成功。
這不是偶然現象。
處理辦法還是:按時間順序,稍晚一些的付款被認為是重復支付,進入 異常支付流程(能原路退返就退,如無法退返則返還至賬戶余額)。
 
3.4. 支付成功時系統發現商品已不可售賣
商品不可售賣有兩個原因:1)庫存不足;2)商品已下線。
 
如果是庫存不足:
團購商戶為了 避免超賣,應該
  • 將訂單關閉,
  • 將交易關閉,
  • 將實付金額原路退返(如無法退返,退至賬戶余額),
  • 記錄支付失敗日志,
  • 記錄資金變動日志,標記退返原因是“庫存不足”,
  • 顧客可以在前台賬戶余額變更歷史中看到有過付款以及被退款的明細。
如果是實物類電商商戶,因為可以事后干預,補足庫存,所以可以接受這次付款行為。
 
如果是商品已下線,處理方式同上,只不過要標記退返原因是“商品已下線”。
 
3.5. 訂單名稱中不能包含敏感詞
支付寶對商品標題核查得非常嚴格,所以 鄭昀鄭重提醒您,為了避免顧客發起支付時看到支付寶如下警告:
http://images.cnblogs.com/cnblogs_com/zhengyun_ustc/255879/r_clip_image002%20-%20005%e5%89%af%e6%9c%ac.jpg  
請提前調用   支付寶交易信息敏感詞分析接口(fast_text_trade)  ,在錄入信息時就阻止保存。
注意,此敏感詞分析接口需要聯系支付寶開通權限。
 

 
四. 正常支付流程的兩個要點
4.1.正常支付的處理流程
對於一個團購商品來說,顧客的 正常支付成功通知 到達服務器端時,業務規則簡述為:
  1. 商品活動結束后,所有未付款訂單,一律不允許支付。
  2. 必須符合庫存管理規則。
  3. 本次支付金額應該小於等於該訂單的待支付金額。
  4. 訂單狀態正常(不能處於“已取消”、“已付款”等狀態)。

一旦發現違背業務規則的支付成功通知到達,則:

  1. 並不修改訂單狀態;
  2. 將此筆支付款項自動返還到該會員的賬戶余額里
  3. 支付中心記錄支付失敗日志;並記錄資金變動日志;
  4. 會員在前台“個人中心”下的“賬戶余額”里能看到這個余額變更歷史以及對應的說明。

支付流程圖如下圖4.1所示:

 

圖4.1 團購支付中心判斷的簡單流程

 
4.2. 交易流水號變化規則
商戶發給第三方支付的   out-trade-no  標識了一次交易的 商戶唯一訂單號
該參數的定義為:
參數
參數名稱
類型
參數說明
是否可為空
樣例
out_trade_no
商戶網站唯一訂單號
String(64)
支付寶合作商戶網站唯一訂單號,並非支付寶交易流水號
(確保在合作伙伴系統中唯一)。
不可空
58942120-tuan-001
商戶完全可以自定義這個 out_trade_no 的字符串組成規則
即使對應同一個訂單,也可以構造出不同的 out_trade_no 。
只要當支付寶的交易通知把這個參數原樣返回時,你的程序能知道這是哪一個訂單的哪一筆交易,它的應付金額是多少,這個應付金額被支付后訂單產生什么變化,這樣就行。
下面舉幾個例子。
 
例一:修改訂單,訂單應付金額或支付方式發生變化
背景:訂單在沒有支付成功之前,顧客都是可以修改的。做了以下修改后,可能會引起訂單應付金額或支付方式發生變化:
  • 余額支付的金額變化
  • 購買份數的調整
  • 優惠券/代金券的使用
而支付寶等第三方支付,對於一個用 商戶唯一訂單號標識的交易,禁止變更 total_fee(交易總金額)字段!
所以,我們的同一個訂單,發起不同應付金額的支付請求時,必須更換 out_trade_no ,流程如下圖4.2所示:
     http://images.cnblogs.com/cnblogs_com/zhengyun_ustc/255879/o_clipboard%20-%20006%E5%89%AF%E6%9C%AC.png
圖4.2 訂單應付金額變化,out_trade_no 必須變化
 
例二:修改訂單,訂單支付方式發生變化
背景:訂單未成功支付前,用戶也是可以調整支付方式的:
  • 支付方式的調整(不僅僅指從支付寶變為快錢這種第三方支付之間的變更,而且包括從支付寶之網銀直連變為支付寶這種第三方支付內部變更)
此時,建議更換 out_trade_no 。
 
例三:訂單已付款,但追加一部分商品,需要補支付
背景:選擇了菜品1、2、3、4的訂單已支付成功,顧客追加菜品5,不需要創建新訂單,可以在原訂單基礎上補充支付。
做法:如下圖4.3所示
   http://images.cnblogs.com/cnblogs_com/zhengyun_ustc/255879/o_clipboard%20-007%20%E5%89%AF%E6%9C%AC.png
圖4.3 訂單補支付
 
4.3. 對賬拉單
無論系統是否可靠,商戶終歸還是要對賬的。
對的就是數據庫里記錄的當天應收帳款,與第三方支付商戶帳號里收到的錢是否吻合。
如果你數據庫記錄的顧客用支付寶支付的款項是10001元,而你的支付寶帳號里只收到了10000元,那一定有問題,必須要深究下去。
 
核對的辦法就是,
每天零點,從數據庫里查詢出前一日用支付寶支付的所有交易,得到支付寶交易流水號和支付金額的集合;
遍歷這個集合,拿交易流水號去支付寶的單筆交易查詢接口(single_trade_query),這樣查出交易金額,對比一下,看你數據庫里記的支付金額和實際收到的交易金額是否一致。
 
@ 鄭昀匯總於2012/11
 

贈圖幾枚:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM