Files
logseq/pages/Payment Service 2.md

886 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
- 金融牌照
- 平台跳转
- 商家-收款方
- 用户-持卡人
- 中间-卡组织
- Visa
- MasterCard
- Card Network
- 中间转发各种各样的支付请求
- 无需额外对接
- 用户有各种各样的银行卡
- 商家有作为收单方也有不同银行帐号
- 减少对接以及合规的负担
- Payment Service PlantformPSP
- 提供一个简单的API
- 调用APIPSP处理这些支付请求
- 收取手续费0.5%-1%
- 支付系统
- 记账+内部清算
- 用来记录支付事实
- 支付流程
- 商家发起一个支付请求
logseq.order-list-type:: number
- 调用外部PSP的API执行支付的请求
logseq.order-list-type:: number
- PSP执行完成记录这笔账
logseq.order-list-type:: number
- 通知对应的用户支付结果
logseq.order-list-type:: number
- 每天定时对账目进行清算
logseq.order-list-type:: number
- Function Requirement
- merchant send payment request
- client can pay
- merchant can query payment status
- NonFunction Requirement
- Sercutiy
- Reliability-handle failure
- consistency
- correctness
- scaleability-PSP API
- ![image.png](../assets/image_1751630881799_0.png)
- Initiation
logseq.order-list-type:: number
- Execution
logseq.order-list-type:: number
- Recording
logseq.order-list-type:: number
- Notification
logseq.order-list-type:: number
- Clearing
logseq.order-list-type:: number
- Payment integration
- serve-to-serve/PCI审计
logseq.order-list-type:: number
- SDK如果崩溃就会无法付款存在供应链攻击的分享
logseq.order-list-type:: number
- 隔离用户卡号
- iframe
logseq.order-list-type:: number
- hosted redirect
logseq.order-list-type:: number
- 跳转到第三方的页面
- data model
- single center table
logseq.order-list-type:: number
- 审计要求
- 不能够修改
- three table: order+payment+ledger
logseq.order-list-type:: number
- payment {
charge_id
order_id
payment_method
PSP
status{created, start, processing, successed, failed}
amount
currency
created_at
updated_at
idem_unique_key
}
- retry
- timeout
logseq.order-list-type:: number
- exponential backoff
logseq.order-list-type:: number
- jitter(+-15%)控制请求分布更加均匀
logseq.order-list-type:: number
- DLQ(exhaust)
logseq.order-list-type:: number
- PSP调用callback URL
- 失败后service主动轮询PSP
- 采用复式记帐
- 写入到append-only数据库
- AMAZON QLDBQuantum Ledger Database
- Alibaba ledgerDB
- 专门为金融优化的数据库
- 对账
- 银行发送settlement file
- Turning the database inside-out
-
- Text
- 今天我们来讲一个支付系统
这种系统你还是需要一点生活经验的
因为它不是真的让你去划账
你又没有这个金融牌照
拿什么去搞这笔钱
你有可能上网去买个衣服
还要跳转到另一个平台去付款
那比如说用户拿他的信用卡去买东西
那用户这边就是持卡人
商家里面叫做收款方
两边对应着各自有一个银行
像用户这边就是发卡行
然后对面就是收单行
中间其实还有一个叫做卡组织的东西
比如说Visa或者MasterCard
他们都是card network
然后中间会转发各种各样的支付请求
用户可能会有各种各样的银行卡
所以会对应不同的发卡行
然后商家作为收单方也会有不同的银行账号
那你要是挨个去对接呢
在工作量肯定是吓死人
合规的负担也会比较重
而且银行里面的技术更新通常会比较慢
有时候你可能看到一些上古的技术栈
所以通常我们会去找一个叫做PSP的平台
就是payment service platform
通常这个PSP平台呢
会给我们一个比较简单的API
然后我们直接去调用这个API
然后由PSP来处理这些支付请求
当然它会向我们收取一定的手续费
通常是可能是0.5%到1%这个区间
那话说回来了
如果有人能去处理这个支付
那我们还设计什么支付系统呢
实际上这个支付系统想让你设计的是一个记账
再加上内部清算的这么一个流程
也就是说它是一个用来记录支付事实的系统
那这样这个问题就会简单很多了
我们可以拆分一下用户支付的这个流程
首先就是商家里面会发起一个支付请求
然后我们应该去调用这个外部的PSP的API
去执行这个支付的请求
然后PSP执行完之后
我们应该去进行一下记录
就是说我们会把这笔账记下来
所以记下来之后不管它的成功和失败
你都应该去通知一下对应的用户
最后可能是每天对这个账目进行一下清算
其实真正的过程应该比这个要复杂一点
不过我觉得能够覆盖到这么几个步骤
应该已经差不多了
毕竟你的时间也有限
所以我先把这个放在这
我们来看一下它的功能性需求应该是什么
功能性需求其实还是比较明确的
首先商家这边
他要能发起一个支付请求
然后客户这边
要能去付费
然后商家这边应该能去查看这个订单的支付状态
非功能性需求呢
首先肯定是安全吧
这是肯定不用说的
所有和资金有关的东西
security都是第一步的
然后应该是可靠性
他要能自己去handle一些failure之类的
接下来还有一个consistency
因为肯定嘛
我们希望一笔钱只被扣款一次
然后这笔账被记下来
所以一致性很重要
另外一个比较重要的
常见的金融相关是正确性
肯定是希望你的账永远是对的
这里有一个不太确定的地方
就是说这个scalability
我们会不会去考虑
因为实际上你是在调用一个外部的API
所以那么很大程度上
你的bottleneck其实不在你自己这里
在外部
这种情况下
我建议我们就一开始不要先去考虑scalability了
后面如果有条件的话
我们再研究
搞清楚这些之后
我们就可以去画一个high level的diagram
OK
这个图还是很简单的
只不过画的比较简略
我们要来挨个展开一下
那首先第一件事就是用户要去付款
那应该做什么
首先从商家这一方来说
我的服务器上肯定要给你提供付款这么一个逻辑
那也就是说我们应该做就是支付集成
支付集成
支付集成其实还是有不少方案的
比如说最简单就是说
我可以做一个server to server
对吧
商家从用户这边去收集用户的这个卡号和他的这个CVV密码
然后后端把这个收集他的卡号和密码直接发给银行或者是卡网络
那这样其实你就相当于自己建立一个PSP了
对吧
因为你需要去取得支付机构的牌照
一般大家也不会这么去做
因为商家会接触到这些 PAN (primary account number)
这些乱七八糟的东西
那也就是说你需要通过这个PCI的审计
审计这个事情通常比较复杂
合规门槛高
而且也比较贵
除了技术的复杂度之外呢
初期的投入也很高
基本上只有大型的企业会用这种方案了
但是好处也会比较明显
就是说一旦通过了这个PCI审计
自主权就相对比较大了
不再需要去依赖第三方的支付平台
可以省下一笔第三方的通道费
自己也可以去控制一些逻辑
比如说增加一些错误处理啊
分期 分账啊 这些玩意儿
那另外一个比较简单的一点方案
就是说我引入一个支付平台的SDK
这个应该是我们比较常见一种方案
大家通常用着stripe paypal
或者说国内有微信 支付宝
其实都提供这种方案
这样一来PSP提供一个js library
商家把它呢
嵌入到自己的网站上
自己去开发一些定制的UI
付费的时候
这个SDK把用户的卡号
直接传到这个PSP去
然后会返回一个token
商家用这个token来调用PSP的API
但是本身不接触用户的卡号
这样一来就可以避免
去接触用户的账号信息
但是这样说
你的瓶颈就在这个SDK上
如果这个SDK它崩溃了
那用户就不能付款
理论上这种
JavaScript SDK是通过fetch()
然后送到PSP的domain去的
不需要经过商家的服务器
但是脚本实际上
还是跑在商家页面里
所以这里如果你用XSS注入(Cross-Site Scripting跨站脚本)
或者是MageCart植入
都可以在用户的浏览器里面
去窃取到这个输入的卡号
那也就是说
这里其实存在供应链攻击的风险
那商家这一端
就应该要保护自己的前端页面
做上一些安全策略
比如说去做一个
CSP(内容安全策略 Content Security Policy) 或者是SRI (子资源完整性 Sub-resource Integrity)这些东西
只允许去加载可信的脚本
并且你要对这个脚本
验证一下哈希值
再往后退一步的话
你可以使用一个iframe
iframe相当于一个
更加安全的Sandbox管理
隔离敏感数据
也减小了攻击面
商家只得到支付的token
那么现在的SDK
实际上都会采用混合模式
SDK直接在底层
注入一个iframe
浏览器端会使用
iframe hosted field
对商家隐藏银行卡的信息
然后使用PSP的公钥
去做CSE(客户端加密 client-side encryption)
把这个卡号加密起来
最后Post请求发到PSP去
PSP拿到这个密文之后
会进行解密
然后返回一个不可逆的token
商家还是拿这个token去做扣款
不再去接触这个卡号
但是iframe的问题在于
前端的CSS和响应式
会设到这个iframe的限制
这会对你的UI的开发
产生一定的限制
有能力企业还是会选择
基于JavaScript SDK
自己去做二次开发
这样来说自由度高一点
但是同样成本也会更加高一点
最后一种
就会进行hosted redirect
直接去走一个托管的支付页面
用户点击下单
然后一个302
跳转到PayPal或者Strape
这样的平台上去
等你用户付款完了之后
再重定向回来
这其实是最简单的对吧
PCI的负担最低
运维也最简单
一旦出了故障
就全靠PSP自己去兜底去了
同样它的自由度也是最低的
因为你的UI是人家的
品牌也是人家的
全看PSP
而且这里会多一步跳转
这代表着
这里的用户的转化率
会有些损失
总的来说呢
你的可定制化越高
成本越高
不仅仅是技术实现的成本
还会有PCI的责任
和安全管控的成本
要求又会更多一点
等你集成了支付系统之后
这里就会出现第二个问题
我们花了这么大劲 对整个支付的流程做了下隔离
那既然隔离了
商家要怎么来记录这笔交易呢
其实我们只是隔离了
商家和用户的银行卡信息
用户这边提交完付款信息之后
PSP还是会把这个token发回来
这个token也叫nonce
后续的扣款
还是要商家拿着这个nonce
去和PSP发请求的
nonce一般来说是一次性的
有的时候你会看见
商家问你
需不需要保存这个支付信息
如果你勾取了这条
商家会再请求一次
换取一个可以复用的token
这里我们简单一点
就只说这个单次的nonce
有了这些信息之后
其实你已经可以开始
去写数据库了
那么这里就会有一个问题
我这数据库要怎么存
第一个想法通常是
我能不能用一张表
存所有的信息
对吧
我每次有一笔支付请求
我就记下来
用户买了什么东西
花了多少钱
然后这个支付的状态是什么
它的卡是什么
这样每一条交易
对应着这个表里的一条记录
但这里可能会有一个问题
因为支付这个东西
它是受到合规限制的
任何和金融相关的东西
都需要经过审计
支付数据在这里
应该是不可更改的
你每次有什么操作
不能说去覆盖
之前的这个状态
所以最好来说
还是要去拆分一下数据表
这个里面
应该会有三种不同的状态
首先是你的业务状态
这里有哪些订单
用户这个订单
对应着什么商品
第二个是你的交易状态
用户发起了这个
支付请求之后
有没有付钱?
用什么卡付钱?
付了多少?
然后现在是被拒绝了
还是已经支付成功了
第三个状态
就是你的资金状态
这笔钱有没有到账
这笔账要不要进行分配
有多少是最后要给商家的
有多少是最后
要付给支付渠道的手续费
所以这里三个状态
如果要做责任分离的话
至少是三张数据表
这三张数据表的读写模式
应该是完全不一样的
比如说这个资金状态这张表
它就应该是不可更改的
对于支付系统
这种合规驱动业务来说
有条件
我们还是多存一张表
也好过在同一张表里
改来改去
理论上我们可能是需要
order table
加上一个payment table
再加上一个ledger的账簿
这题的核心还是payment
那我们还是关注一下
payment表的数据结构
假设这里
就是这个payment表
我们可能需要哪些字段
首先你肯定会要有
这个charge ID
第二
然后你会有order ID
接下来可能是payment method
然后是对应的PSP是什么
这笔订单的status是什么
status可能是说
我一开始先去创建
这张支付的请求
然后这个支付开始处理
最后可能是success
或者是failed
这里通常是需要一个
状态机来表示
然后这里面会有多少钱
它对应的货币是什么
这笔支付是什么时候创建
又是什么时候更新的
最后这里可能需要一个
unique ID来做这个幂等
我直接叫他
idempotency key了
这张表要怎么存呢
通常来说和金融相关
我们都要注意一致性
但是一致性
也分内部一致性
外部一致性
内部一致性其实好处理
对吧
我直接拿关系性数据库
这关系性数据库
是有一致性保障的
那么外部一致性
这个时候我们就要盘一下
这个workflow
用户在这里去进行下单
他打开这个客户端
点击一个checkout
然后开始支付
用户的预期是
我就把这一单结了
这里其实最怕
发生的是一个
double charging
或者double spending
比如说用户
网卡了一下
然后连着点了好几次
然后接下来服务器这边
就收到多个请求
那服务器这边
多个请求
肯定不能通通去扣款
对吧
他肯定要做一个
exactly once
这个exactly once
就需要依赖了
这个idempotency key了
当用户点开
这个checkout页面的时候
服务器这个时候
应该会生成一个幂等键
然后后续流程
全都带着这个幂等键
用户点开这个页面
服务器把这幂等键
发给客户
客户下完单之后
带这个幂等键
再返回到payment service这边
service这端
我看到这个idempotency key
开始去做de-duplication
拦截重复的请求
这样就算用户点了多次
也只有一个请求
会被进行后续的处理
处理的时候
需要去更新
payment表的status状态
比如说一开始是created
然后传给他一个idempotency key
然后当你收到一个
合法的请求之后
就转入这个start状态
表示这个支付请求已经收到了
下一步就要去调用
外部平台的这个支付的API了
也就是说调用这个PSP
PSP收到这个请求开始处理
就中间可能会有一些风控啊
或者是授权之类的
交易授权这里可快可慢
通常你这里提交完
至少应该收到一个明确的信号
比如说“你的支付请求已经受理”
或者告诉你“交易失败了”
然后这个时候payment表里的status
应该去转入到processing阶段
这里最好画一下这个状态机
好 有了这个状态机之后
我就可以把这个简化一下
由于PSP是异步处理
所以我们实际上不知道PSP
什么时候能处理好
但通常来说PSP应该先给你一个
“我已经在处理了”
那如果PSP就没有给你一个信号
比如说有网络抖动
对吧
他给你信号
但是你没有收到
所以这里你应该引入一个retry的机制
那通常来说
一旦我们要加入retry的话
那你就要想到retry的四个要素了
第一个就是timeout
这个retry的等待时间
应该是有个最大限制
超过这个timeout的时间之后
你才去触发这个retry
那第二个就是backoff
我是间隔一秒钟
还是间隔两秒钟
还是说我不断去增加这个时间间隔
通常来说
这里比较推荐的是
做一个exponential backoff
我第一次等待一秒
然后第二次等待两秒
然后第三次等待四秒
这下来等待八秒
每次延长这个间隔的时间
这样不会给后端造成太大压力
那第三点是做一个jitter
jitter的意思是
我每次向后端发请求
我加入一些网络抖动
比如说我加一个正负15%
比如说我按照某个频率
给后端去发送这个重试的请求
但是这里有另一个服务器
它也采用这个频率
跟你同步的发送这个请求
那这个时候对后端来说
它可能在同一个时间
会收到多个请求同时涌入
然后在下一个间隔
大家又同时暂停了
然后再到下一个间隔
又同时涌入
加入jitter之后
我可以对这个间隔时间
做一个正负15%的抖动
这样可以将这个分布
打得更加均匀一点
第四个要素就是
dead letter queue了
所谓死信队列
retry这里
我们最好设置一个
最大的重试次数
一旦超过这个次数之后
你可以当做
你的后端这个PSP
已经宕机了
你就不要再进行retry
你应该把这个消息
放入这个死信队列
然后去进行一些处理
比如说你本地
做一个整体的回滚
通常可能还要再
熔断一段时间
那么对于外部的PSP来说
因为你这里不断的在重视
所以外部的PSP
也要去做
这个de-duplication的操作
那么这样一来
它代表着你这个幂等键
也应该同步的
穿透到外面的PSP去
这样至少也能
增加一道保险
等PSP返回消息
提交成功之后
就可以先给用户
返回一下信息了
比如说你告诉用户
“已经下单”
“等待确认”
然后PSP里面
会进行一步的处理
通常来说
我们会给PSP
一个callback URL
PSP处理完这笔支付
会调用这个URL
通知我们
但是如果这个PSP
调用这个callback URL
失败了
它没通知到我们
对吧
网络还是有抖动
所以这个时候
你还要再补上一个轮询
超过一段时间
没有收到这个callback
那就是主动查询一下
当PSP处理完之后呢
这状态机
就应该进入这个success
或者是fail
得到一个固定结果
然后把这个结果通知用户
那如果这里进入success
就代表着扣款成功了
那这个时候
你应该就要去写这个ledger
这个里面又会涉及到
对账户 交易和分录的建模
所以也是一个比较复杂的系统
一般来说
行业的标准是
商务的实体要采取复式记账(double-entry bookkeeping)
也就是说
每笔交易至少要
影响到两个账号
产生一组
金额相等 方向相反的记录
这里非要扯一下的话
你应该考虑到ledger的金融属性
账本这种东西
一旦你写入之后
就是不可变的
所以这个ledger
通常应该写入到一个
append-only的数据库去
这里除了你能想到的
关系型数据库之外呢
通常来说
对于这种specific domain
你可以去猜一下
问一下面试官
是不是应该有一个
专用的技术栈
那ledger这里
确实是存在ledger使用的数据库
比如说amazon会有一个QLDB(Quantum Ledger Database)
或者是Alibaba
开源的ledgerDB这种东西
他们都提供了SQL接口
然后专门为金融事务
进行了优化
这种专业优化过的数据库
通常来说
开发难度会比较低
然后很多你需要的功能
都是已经内置好了
比如说审计啊
或者是加密之类的
最后一步就是进行对账了
因为任何在线链路
都可能遇到bug
所以银行这里
应该定时发过来
这个settlement file(结算报告)
我们可以每天执行一次
对账任务
如果有问题呢
那就可以触发一个自动补偿
或者是转到人工进行审核
这样一来
应该就是这个完整的支付流程
你看这个流程有问题吗
其实还是有的
你记得这里
我们补了一个轮询
有一笔交易一直在审核
然后商家这边
等不到这个callback
于是开始进行轮询
轮询几次
到达这个最大上限
然后写入了一个fail
fail了之后
PSP这边突然发现
交易过了
于是这个PSP
调用了这个callback
那这个时候
就应该写入一个成功
对吧
这个就叫做状态机的跳变
它也不是不能解决
这里我们可以使用一个
单调状态机
一旦你这个状态进入fail之后
就不能再覆盖成这个success
或者说我这里
打一个版本号的补定
每次进行更新的时候
我要去比较这个版本号
那最差的情况下
你还可以等到
这个日终的对账环节
总之你的账不能出错
但是这个问题的根源在于
你每次状态机的变化
实际上是对于这个
paymentDB的一条记录
发起了一次UPSERT
用UPSERT这个语句
覆盖之前的状态
但是这个问题也不大
因为大部分PSP
其实都是这条方案
一张表去维护
这个实时的操作状态
另一张账务表
会负责这个资金的守恒
这样一来
它开发成本也比较低
维护成本也低
方案本身呢
也比较成熟
也是合规的
但如果你要进一步呢
其实这里确实是存在
另一种方案
之前我们也讲过一些
数据库和流系统互换的案例
有过这么一篇
《Turning the database inside-out》
我们在这里去掉这个数据库
然后采用一个event store
event store呢
可以使用Kafka
或者是Apache Pulsar
之类的流系统
也有可能是一个
append-only的db
当然你也可以两个都使用
那这样一来
所有的变更
都作为一个事件
写入到这个event store里面
从这个event store
作为source of truth
在我们再通过
projection或者view的方式
来生成视图表
加速查询
事件一旦写入呢
就不再更改
任何的纠错
以新事件的方式写入
进行冲正
不少互联网公司
其实都会采用
这种ledger系统
比如说stripe用过
Coinbase用过
Square也用过
都是一个不可变的账本
再加上事件流的形式
这样一来
你既可以获得一个
金融系统的正确性
也可以获得一个
互联网系统的可拓展性
但它的代价也比较明显
这里有一个比较高的
系统复杂度
那开发和运维的成本
也比较高
团队本身呢
需要对流系统和
金融系统都比较熟悉
具体怎么选型
还是要看你的场景
一旦你接受了
这个流系统的这种设定
那其实这个系统
可以产生很多的变化
比如说
我将这个消息队列
放到前面去
进行一下削峰填谷
同时进行数据落盘
那这样一来的话
Payment Service
作为一个Broadcast
把你每次的事件
投放到不同的数据库去
别的结构可以不变
每次对账的时候
你可以在消息队列的前端
进行事件的重放
重新计算一下
如果你最后还有时间的话
我建议还是做一下
show off
给你的面试官录一手
比如说这里
我们可以在Payment Service
前面插入一个Risk Engine
去做一下Fraud Detection
欺诈检测
其实有很多的方案
不一定一上来
就要做什么机器学习
那最简单
你可以说
我使用一个规则引擎
比如说我加入一个规则树
或者是黑名单
白名单这样
那这种方法比较简单
门槛也比较
也便于合规和审计
但是呢
它的问题就不太适合
比较复杂的欺诈手段
那另一种比较常见的是
我使用一些统计学的方式
或者是直接上一些
简单的机器学习模型
那这类的方案
通常会受限于数据本身
对于新的欺诈手段
反应会比较慢一点
而且你也需要
手动去寻找特征
标注一下数据
大的公司
可能会使用无监督学习
来解决冷启动
或者零样本的问题
但是这种方案呢
也有一定的误报率
需要人工介入进行审核
infrastructure的支持呢
你可以上一些图模型
或者GNN这样的手段
来捕捉比较复杂的形式
这类系统的维护和开发
都会比较复杂
模型的推断和开销
也会比较大
但是金融风控力
还是比较常见的