[logseq-plugin-git:commit] 2025-07-04T12:30:11.051Z
This commit is contained in:
BIN
assets/image_1751630881799_0.png
Normal file
BIN
assets/image_1751630881799_0.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 311 KiB |
@@ -1,2 +1,886 @@
|
||||
- 金融牌照
|
||||
- 平台跳转
|
||||
- 商家
|
||||
- 商家-收款方
|
||||
- 用户-持卡人
|
||||
- 中间-卡组织
|
||||
- Visa
|
||||
- MasterCard
|
||||
- Card Network
|
||||
- 中间转发各种各样的支付请求
|
||||
- 无需额外对接
|
||||
- 用户有各种各样的银行卡
|
||||
- 商家有作为收单方也有不同银行帐号
|
||||
- 减少对接以及合规的负担
|
||||
- Payment Service Plantform(PSP)
|
||||
- 提供一个简单的API
|
||||
- 调用API,PSP处理这些支付请求
|
||||
- 收取手续费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
|
||||
- 
|
||||
- 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 QLDB(Quantum 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这样的手段
|
||||
来捕捉比较复杂的形式
|
||||
这类系统的维护和开发
|
||||
都会比较复杂
|
||||
模型的推断和开销
|
||||
也会比较大
|
||||
但是金融风控力
|
||||
还是比较常见的
|
||||
Reference in New Issue
Block a user