通過 上篇文章 的分析,我們已經明確了這個系統要幹些什麼。接下來的都是實打實的乾貨。這些內容認真閱讀掌握後,相信你能夠以此為基礎設計一個維護性好、擴充套件性好的交易系統。
資料庫設計
資料的設計是按照:交易、退款、日誌 來設計的。對於上面說到的對賬等功能並沒有。這部分不難大家可以自行設計,按照上面講到的思路。主要的表介紹如下:
pay_transaction
記錄所有的交易資料。pay_transaction_extension
記錄每次向第三方發起交易時,生成的交易號pay_log_data
所有的日誌資料,如:支付請求、退款請求、非同步通知等pay_repeat_transaction
重複支付的資料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_repeat_transaction` (
`id` BIGINT UNSIGNED NOT NULL,
`app_id` VARCHAR(32) NOT NULL COMMENT '應用的id',
`transaction_id` VARCHAR(64) NOT NULL COMMENT '系統唯一識別交易號',
`transaction_code` VARCHAR(64) NOT NULL COMMENT '支付成功時,該筆交易的 code',
`trade_no` VARCHAR(64) NOT NULL COMMENT '第三方對應的交易號',
`pay_method_id` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '支付方式',
`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 '支付選擇的幣種,CNY、HKD、USD等',
`payment_time` INT NOT NULL COMMENT '第三方交易時間',
`repeat_type` TINYINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '重複型別:1同渠道支付、2不同渠道支付',
`repeat_status` TINYINT UNSIGNED DEFAULT 0 COMMENT '處理狀態,0:未處理;1:已處理',
`create_at` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '建立時間',
`update_at` INT UNSIGNED NOT NULL COMMENT '更新時間',
PRIMARY KEY (`id`),
INDEX `idx_trad` ( `transaction_id`),
INDEX `idx_method` (`pay_method_id`),
INDEX `idx_time` (`create_at`)),
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
。
當然這其中還有些細節,需要大家自己看了表結構,實際去思考一下該如何使用。如果有任何疑問歡迎到我們GitHub的專案(點選閱讀原文)中留言,我們都會一一解答。
這些表能夠滿足最基本的需求,其它內容可根據自己的需求進行擴張,比如:支援使用者卡列表、退款走銀行卡等。
系統設計
這部分主要說下系統該如何搭建,以及程式碼組織方式的建議。
系統架構
由於支付系統的安全性非常高,因此不建議將對應的入口直接暴露給使用者可見。應該是在自己的應用系統中呼叫支付系統的介面來完成業務。另外系統對資料要求是:強一致性的。因此也沒有快取介入(當如快取可以用來做報警,這不在本位範疇)。
具體的實現,系統會使用兩個域名,一個為內部使用,只有指定來源的ip能夠訪問固定功能(訪問除通知外的其它功能)。另一個域名只能訪問 notify
return
兩個路由。通過這種方式可以保證系統的安全。
在資料庫的使用上無論什麼請求直接走 Master 庫。這樣保證資料的強一致。當然從庫也是需要的。比如:賬單、對賬相關邏輯我們可以利用從庫完成。
程式碼設計
不管想做什麼最終都要用程式碼來實現。我們都知道需要可維護、可擴充套件的程式碼。那麼具體到支付系統你會怎麼做呢?我已支付為例說下我的程式碼結構設計思路。僅供參考。比如我要介入:微信、支付寶、招行 三家支付。我的程式碼結構圖如下:
用文字簡單介紹下。我會將每一個第三方封裝成: XXXGateway
類,內部是單純的封裝第三方介面,不管對方是 HTTP 請求還是 SOAP 請求,都在內部進行統一處理。
另外有一層XXXProxy
來封裝這些第三方提供的能力。這一層主要幹兩件事情:對傳過來請求支付的資料進行個性化處理。對返回的結構進行統一處理返回上層統一的結構。當然根據特殊情況這裡可以進行一切業務處理;
通過上面的操作變化已經基本上被完全封裝了。如果新增一個支付渠道。只需要增加:XXXGateway
與 XXXProxy
。
那麼 Context
與 Server
有什麼用呢?Server
內部封裝了所有的業務邏輯,它提供介面給 action 或者其它 server 進行呼叫。而 Context
這一層存在的價值是處理 Proxy
層返回的錯誤。以及在這裡進行報警相關的處理。
上面的結構只是我的一個實踐,歡迎大家討論。
本文描述的系統只是滿足了最基本的支付需求。缺少相關的監控、報警。 大家可以到我們的 GitHub主頁留言