設計的過程需要考慮的需求
功能性需求
在電商購買商品的時候接受付款
每隔一段時間向商家付款
使用第三方支付平臺
支援第三方平臺對賬
非功能性需求
高擴充套件性,大量支付
較高可用性,服務不宕機
可靠性,系統出現問題仍保持正確
一致性,內部系統服務間,內部外部服務間一致
系統數據流程
1、使用者發起支付請求
2、支付服務就會把這個支付建立請求寫入 支付日誌流水錶,作用是用於記錄下支付過程中每一步的狀態變化,可用於排查建立過程問題,防止支付請求重複提交。
3、將支付訂單發到支付處理服務,支付處理服務落地到支付訂單表,專門記錄此次支付的最終狀態快照。
4、將支付訂單發到 支付處理服務來進行對第三方的呼叫。作用如下:
支付渠道可能有多種,支付處理服務對外遮蔽支付細節
支付處理服務需要單獨擴容,不與其他功能耦合,也儘量不要受到業務變動的影響
支付處理服務有單獨的對外暴露網路的需求
5、wallet是賣家的賬戶,ledger是賬本服務,如果寫入失敗之後透過重試佇列來做賬本服務和賣家賬戶的寫入
支付日誌流水錶是 從用戶數據流動的角度來記錄這個支付訂單請求(儲存request,請求頭,時間),而支付訂單表是記錄下來最終需要對第三方支付暴露出來,以及其他團隊能夠看到的支付訂單的最終快照(最終金額之類)。
下圖是跟第三方支付發起呼叫的過程,通常是被掩蓋在第三方提供的sdk中
1、首先我們的網站會提供一個checkout的最終確定訂單頁面,向我們的支付服務發起呼叫
2、支付服務的sdk會發送這個payment以及一個隨機數,這個隨機數用於輔助第三方判斷是否重複傳送請求。
3、第三方會提供一個標識這次交易資訊的token
4、我們儲存下來這個token
5、我們會展示第三方支付平臺的支付頁面
6、使用者在第三方支付平臺那邊來支付
7、第三方支付平臺對使用者展示支付結果
8、使用者跳轉到第三方支付平臺的支付結果頁面
9、第三方支付平臺向我們進行webhook,注意,這裏可以區分為兩種,一種是依賴webhook來更新支付訂單狀態,一種是我們根據token同步去請求來更新訂單狀態
向第三方傳送字元請求成功並且獲取到token之後宕機了,那麼因為沒有返回第三方的支付頁面,所以使用者沒有支付。那麼這裏要進行一個快速失敗的流程,結束掉這個支付訂單。儘量讓使用者再次發起支付。盲目去發起重試不是一個明智的做法。
可靠性
做好限流熔斷
做好安全擴容
失敗的時候明確區分可以重試和不可以重試的請求
一致性
透過nonce和token保證內部系統和外部系統的一致
透過分散式事務或者重試佇列保證多個服務資料呼叫和回滾能夠一起執行
每天定時拉取第三方支付平臺的當日資料來跟自己的資料對賬
資料庫設計
資料的設計是按照:交易、退款、日誌 來設計的。對於上面說到的對賬等功能並沒有在這裏。這部分不難大家可以自行設計,按照上面講到的思路。主要的表介紹如下:
pay_transaction
記錄所有的交易資料。pay_transaction_extension
記錄每次向第三方發起交易時,生成的交易號pay_log_data
所有的日誌資料,如:支付請求、退款請求、非同步通知等pay_notify_app_log
通知應用程式的日誌pay_refund
記錄所有的退款資料
具體的表結構:
-- Table 建立支付流水錶
CREATE TABLE IF NOT EXISTS `pay_transaction` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `app_id` VARCHAR(32) NOT NULL COMMENT '應用id', `pay_method_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '支付方式id,可以用來識別支付,如:支付寶、微信、Paypal等', `app_order_id` VARCHAR(64) NOT NULL COMMENT '應用方訂單號', `transaction_id` VARCHAR(64) NOT NULL COMMENT '本次交易唯一id,整個支付系統唯一,生成他的原因主要是 order_id對於其它應用來說可能重複', `total_fee` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '支付金額,整數方式儲存', `scale` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '金額對應的小數位數', `currency_code` CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '交易的幣種', `pay_channel` VARCHAR(64) NOT NULL COMMENT '選擇的支付渠道,比如:支付寶中的花唄、信用卡等', `expire_time` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '訂單過期時間', `return_url` VARCHAR(255) NOT NULL COMMENT '支付後跳轉url', `notify_url` VARCHAR(255) NOT NULL COMMENT '支付後,非同步通知url', `email` VARCHAR(64) NOT NULL COMMENT '使用者的郵箱', `sing_type` VARCHAR(10) NOT NULL DEFAULT 'RSA' COMMENT '採用的籤方式:MD5 RSA RSA2 HASH-MAC等', `intput_charset` CHAR(5) NOT NULL DEFAULT 'UTF-8' COMMENT '字符集編碼方式', `payment_time` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '第三方支付成功的時間', `notify_time` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '收到非同步通知的時間', `finish_time` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '通知上游系統的時間', `trade_no` VARCHAR(64) NOT NULL COMMENT '第三方的流水號', `transaction_code` VARCHAR(64) NOT NULL COMMENT '真實給第三方的交易code,非同步通知的時候更新', `order_status` TINYINT NOT NULL DEFAULT 0 COMMENT '0:等待支付,1:待付款完成, 2:完成支付,3:該筆交易已關閉,-1:支付失敗', `create_at` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '建立時間', `update_at` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新時間', `create_ip` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '建立的ip,這可能是自己服務的ip', `update_ip` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新的ip', PRIMARY KEY (`id`), UNIQUE INDEX `uniq_tradid` (`transaction_id`), INDEX `idx_trade_no` (`trade_no`), INDEX `idx_ctime` (`create_at`)), ENGINE = InnoDB DEFAULT CHARACTER SET = utf8mb4 COMMENT = '發起支付的資料';
-- Table 交易擴充套件表
CREATE TABLE IF NOT EXISTS `pay_transaction_extension` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `transaction_id` VARCHAR(64) NOT NULL COMMENT '系統唯一交易id', `pay_method_id` INT UNSIGNED NOT NULL DEFAULT 0, `transaction_code` VARCHAR(64) NOT NULL COMMENT '生成傳輸給第三方的訂單號', `call_num` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '發起呼叫的次數', `extension_data` TEXT NOT NULL COMMENT '擴充套件內容,需要儲存:transaction_code 與 trade no 的對映關係,非同步通知的時候填充', `create_at` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '建立時間', `create_ip` INT UNSIGNED NOT NULL COMMENT '建立ip', PRIMARY KEY (`id`), INDEX `idx_trads` (`transaction_id`, `pay_status`), UNIQUE INDEX `uniq_code` (`transaction_code`)), ENGINE = InnoDB DEFAULT CHARACTER SET = utf8mb4 COMMENT = '交易擴充套件表';
-- Table 交易系統全部日誌
CREATE TABLE IF NOT EXISTS `pay_log_data` ( `id` BIGINT UNSIGNED NOT NULL, `app_id` VARCHAR(32) NOT NULL COMMENT '應用id', `app_order_id` VARCHAR(64) NOT NULL COMMENT '應用方訂單號', `transaction_id` VARCHAR(64) NOT NULL COMMENT '本次交易唯一id,整個支付系統唯一,生成他的原因主要是 order_id對於其它應用來說可能重複', `request_header` TEXT NOT NULL COMMENT '請求的header 頭', `request_params` TEXT NOT NULL COMMENT '支付的請求引數', `log_type` VARCHAR(10) NOT NULL COMMENT '日誌型別,payment:支付; refund:退款; notify:非同步通知; return:同步通知; query:查詢', `create_at` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '建立時間', `create_ip` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '建立ip', PRIMARY KEY (`id`), INDEX `idx_tradt` (`transaction_id`, `log_type`)), ENGINE = InnoDB DEFAULT CHARACTER SET = utf8mb4 COMMENT = '交易日誌表';
-- Table 通知上游應用日誌
CREATE TABLE IF NOT EXISTS `pay_notify_app_log` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `app_id` VARCHAR(32) NOT NULL COMMENT '應用id', `pay_method_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '支付方式', `transaction_id` VARCHAR(64) NOT NULL COMMENT '交易號', `transaction_code` VARCHAR(64) NOT NULL COMMENT '支付成功時,該筆交易的 code', `sign_type` VARCHAR(10) NOT NULL DEFAULT 'RSA' COMMENT '採用的簽名方式:MD5 RSA RSA2 HASH-MAC等', `input_charset` CHAR(5) NOT NULL DEFAULT 'UTF-8', `total_fee` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '涉及的金額,無小數', `scale` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '小數位數', `pay_channel` VARCHAR(64) NOT NULL COMMENT '支付渠道', `trade_no` VARCHAR(64) NOT NULL COMMENT '第三方交易號', `payment_time` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '支付時間', `notify_type` VARCHAR(10) NOT NULL DEFAULT 'paid' COMMENT '通知型別,paid/refund/canceled', `notify_status` VARCHAR(7) NOT NULL DEFAULT 'INIT' COMMENT '通知支付呼叫方結果;INIT:初始化,PENDING: 進行中; SUCCESS:成功; FAILED:失敗', `create_at` INT UNSIGNED NOT NULL DEFAULT 0, `update_at` INT UNSIGNED NOT NULL DEFAULT 0, PRIMARY KEY (`id`), INDEX `idx_trad` (`transaction_id`), INDEX `idx_app` (`app_id`, `notify_status`) INDEX `idx_time` (`create_at`)), ENGINE = InnoDB DEFAULT CHARACTER SET = utf8mb4 COMMENT = '支付呼叫方記錄';
-- Table 退款
CREATE TABLE IF NOT EXISTS `pay_refund` ( `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, `app_id` VARCHAR(64) NOT NULL COMMENT '應用id', `app_refund_no` VARCHAR(64) NOT NULL COMMENT '上游的退款id', `transaction_id` VARCHAR(64) NOT NULL COMMENT '交易號', `trade_no` VARCHAR(64) NOT NULL COMMENT '第三方交易號', `refund_no` VARCHAR(64) NOT NULL COMMENT '支付平臺生成的唯一退款單號', `pay_method_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '支付方式', `pay_channel` VARCHAR(64) NOT NULL COMMENT '選擇的支付渠道,比如:支付寶中的花唄、信用卡等', `refund_fee` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '退款金額', `scale` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '小數位數', `refund_reason` VARCHAR(128) NOT NULL COMMENT '退款理由', `currency_code` CHAR(3) NOT NULL DEFAULT 'CNY' COMMENT '幣種,CNY USD HKD', `refund_type` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '退款型別;0:業務退款; 1:重複退款', `refund_method` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '退款方式:1自動原路返回; 2人工打款', `refund_status` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0未退款; 1退款處理中; 2退款成功; 3退款不成功', `create_at` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '建立時間', `update_at` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '更新時間', `create_ip` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '請求源ip', `update_ip` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '請求源ip', PRIMARY KEY (`id`), UNIQUE INDEX `uniq_refno` (`refund_no`), INDEX `idx_trad` (`transaction_id`), INDEX `idx_status` (`refund_status`), INDEX `idx_ctime` (`create_at`)), ENGINE = InnoDB DEFAULT CHARACTER SET = utf8mb4 COMMENT = '退款記錄';
表的使用邏輯進行簡單描述:
支付,首先需要記錄請求日誌到 pay_log_data
中,然後生成交易資料記錄到 pay_transaction
與pay_transaction_extension
中。
收到通知,記錄資料到 pay_log_data
中,然後根據時支付的通知還是退款的通知,更新 pay_transaction
與 pay_refund
的狀態。如果是重複支付需要記錄資料到 pay_repeat_transaction
中。並且將需要通知應用的資料記錄到 pay_notify_app_log
,這張表相當於一個訊息表,會有消費者會去消費其中的內容。
退款 記錄日誌日誌到 pay_log_data
中,然後記錄資料到退款表中 pay_refund
。