coder,你會設計交易系統嗎(實幹篇)?

大愚Talk發表於2019-03-20

通過 上篇文章 的分析,我們已經明確了這個系統要幹些什麼。接下來的都是實打實的乾貨。這些內容認真閱讀掌握後,相信你能夠以此為基礎設計一個維護性好、擴充套件性好的交易系統。

資料庫設計

資料的設計是按照:交易、退款、日誌 來設計的。對於上面說到的對賬等功能並沒有。這部分不難大家可以自行設計,按照上面講到的思路。主要的表介紹如下:

  • 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_transactionpay_transaction_extension 中。

收到通知,記錄資料到 pay_log_data 中,然後根據時支付的通知還是退款的通知,更新 pay_transactionpay_refund 的狀態。如果是重複支付需要記錄資料到 pay_repeat_transaction 中。並且將需要通知應用的資料記錄到 pay_notify_app_log,這張表相當於一個訊息表,會有消費者會去消費其中的內容。

退款 記錄日誌日誌到 pay_log_data 中,然後記錄資料到退款表中 pay_refund

當然這其中還有些細節,需要大家自己看了表結構,實際去思考一下該如何使用。如果有任何疑問歡迎到我們GitHub的專案(點選閱讀原文)中留言,我們都會一一解答。

這些表能夠滿足最基本的需求,其它內容可根據自己的需求進行擴張,比如:支援使用者卡列表、退款走銀行卡等。

系統設計

這部分主要說下系統該如何搭建,以及程式碼組織方式的建議。

系統架構

由於支付系統的安全性非常高,因此不建議將對應的入口直接暴露給使用者可見。應該是在自己的應用系統中呼叫支付系統的介面來完成業務。另外系統對資料要求是:強一致性的。因此也沒有快取介入(當如快取可以用來做報警,這不在本位範疇)。

image-20190309135800643

具體的實現,系統會使用兩個域名,一個為內部使用,只有指定來源的ip能夠訪問固定功能(訪問除通知外的其它功能)。另一個域名只能訪問 notify return 兩個路由。通過這種方式可以保證系統的安全。

在資料庫的使用上無論什麼請求直接走 Master 庫。這樣保證資料的強一致。當然從庫也是需要的。比如:賬單、對賬相關邏輯我們可以利用從庫完成。

程式碼設計

不管想做什麼最終都要用程式碼來實現。我們都知道需要可維護、可擴充套件的程式碼。那麼具體到支付系統你會怎麼做呢?我已支付為例說下我的程式碼結構設計思路。僅供參考。比如我要介入:微信、支付寶、招行 三家支付。我的程式碼結構圖如下:

image-20190309142925499

用文字簡單介紹下。我會將每一個第三方封裝成: XXXGateway 類,內部是單純的封裝第三方介面,不管對方是 HTTP 請求還是 SOAP 請求,都在內部進行統一處理。

另外有一層XXXProxy 來封裝這些第三方提供的能力。這一層主要幹兩件事情:對傳過來請求支付的資料進行個性化處理。對返回的結構進行統一處理返回上層統一的結構。當然根據特殊情況這裡可以進行一切業務處理;

通過上面的操作變化已經基本上被完全封裝了。如果新增一個支付渠道。只需要增加:XXXGatewayXXXProxy

那麼 ContextServer 有什麼用呢?Server 內部封裝了所有的業務邏輯,它提供介面給 action 或者其它 server 進行呼叫。而 Context 這一層存在的價值是處理 Proxy 層返回的錯誤。以及在這裡進行報警相關的處理。

上面的結構只是我的一個實踐,歡迎大家討論。

本文描述的系統只是滿足了最基本的支付需求。缺少相關的監控、報警。 大家可以到我們的 GitHub主頁留言

coder,你會設計交易系統嗎(實幹篇)?

個人公眾號:dayuTalk

聯絡郵箱:dayugog@gmail.com

GitHub:github.com/helei112g

相關文章