设计的过程需要考虑的需求
功能性需求
在电商购买商品的时候接受付款
每隔一段时间向商家付款
使用第三方支付平台
支持第三方平台对账
非功能性需求
高扩展性,大量支付
较高可用性,服务不宕机
可靠性,系统出现问题仍保持正确
一致性,内部系统服务间,内部外部服务间一致
系统数据流程
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
。