Files
logseq/pages/Payment Service 2.md

26 KiB
Raw Permalink Blame History

  • 金融牌照
  • 平台跳转
    • 商家-收款方
    • 用户-持卡人
    • 中间-卡组织
      • 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
  • 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这样的手段 来捕捉比较复杂的形式 这类系统的维护和开发 都会比较复杂 模型的推断和开销 也会比较大 但是金融风控力 还是比较常见的