乐趣区

coder,你会设计交易系统吗(实干篇)?

通过 上篇文章 的分析,我们已经明确了这个系统要干些什么。接下来的都是实打实的干货。这些内容认真阅读掌握后,相信你能够以此为基础设计一个维护性好、扩展性好的交易系统。
数据库设计
数据的设计是按照:交易、退款、日志 来设计的。对于上面说到的对账等功能并没有。这部分不难大家可以自行设计,按照上面讲到的思路。主要的表介绍如下:

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 主页留言

退出移动版