「提醒」:是付费专栏,但是在知识星球里是免费的。目前星球里已更新了「几亿用户,百万并发的C端商品系统实战」、「DDD领域驱动设计三年落地实战」和「应付亿级用户规模的支付系统代码实战」,后续的订单,支付,结算和购物车专栏,星球内也都是免费的。
在写任何一行代码之前,先把需求理清楚。支付系统不像大多数人想的只是一个微信支付对接层,它要应付的是一个多终端、多渠道、多支付方式组合在一起的复杂收银体系。每个需求点如果一开始没想清楚,落到架构上就是会返工,也就影响了效率。
我们需要有一个统一的「支付中心」,来承接这些支付需求。
文档概要
| 项目 | 内容 |
|---|---|
| 产品名称 | 支付系统(支付中心) |
| 需求发起方 | 产品部门 |
| 业务目标 | 建设多渠道、多支付方式组合的支付中心,支撑日均50万笔订单的支付请求,覆盖线上和线下以及国外的支付场景 |
| 目标用户 | C端用户(核心)、运营人员、财务人员 |
| 涉及终端 | APP、微信小程序、支付宝小程序、抖音小程序、门店POS |
需求背景
当时公司的目标是,全球性品牌,业务会扩展到全世界。不是渠道品牌,而是全球品牌。下面列一下重要的一些支付场景。
支付场景全景
支付系统面向的终端不是单一的。不同终端背后的支付流程差异很大,需要梳理清楚。
线上支付场景
用户在APP或小程序里下单后进入收银台,选择一个或多个支付方式完成付款。关键流程节点:
- 收银台选择:用户在下单后看到可用支付方式列表(微信支付、支付宝、钱包余额、自由卡),可以单选也可以多选组合。比如自由卡抵扣50元、钱包扣20元、微信支付兜底30元
- 支付确认:用户确认支付金额和支付方式后提交,系统按优先级依次发起各渠道扣款
- 支付结果:全部渠道扣款成功才算支付成功,任一渠道失败需要整体回滚
门店POS支付场景
用户在门店消费时通过POS机扫码付款。和线上支付的本质区别:
- 授权码模式:用户在小程序里生成付款授权码(payAuthCode),收银员用POS机输入金额后扫描授权码,POS系统把授权码和金额发给支付中心完成扣款。授权码有时效,30秒后自动失效
- 支付确认方式不同:线上支付用户自己完成,POS支付由收银员在POS机上操作确认。用户只需要出示授权码,不需要做额外操作
- 组合支付场景:门店场景下,用户先用自由卡抵扣一部分,剩余金额在POS机上走微信或支付宝扫码
场景对比
| 维度 | 线上支付 | POS支付 |
|---|---|---|
| 支付发起方式 | 用户选支付方式后提交 | POS机扫码授权码 |
| 授权机制 | 支付密码/生物识别 | 授权码(30秒过期) |
| 支付结果反馈 | 页面展示 | POS机展示 + 用户手机推送 |
| 组合支付 | 收银台勾选 | 自由卡自动抵扣 + POS扫码剩余 |
| 退款方式 | 线上申请退款 | 门店申请退款 |
支付方式需求
支付系统服务不是只对接微信支付就够了。实际业务需要对接多种支付方式,每种支付方式的资金归属、扣款逻辑、对接复杂度都不一样。
第三方渠道支付(微信/支付宝)
微信支付和支付宝是最常见的外部支付渠道。用户的钱从微信/支付宝划到商户账户,支付系统作为中间层负责调用渠道API、处理回调通知、记录流水。
核心需求:
- 支持微信小程序支付、APP支付
- 支持支付宝小程序支付、APP支付
- 预下单获取渠道支付参数,用户确认后在渠道侧完成支付,渠道异步回调通知结果
- 渠道回调的签名验证必须在支付系统侧完成,不能信任外部传入的任何数据
钱包余额支付
钱包是自家系统管理的用户账户体系,用户可以在钱包里充值,用余额消费。
核心需求:
- 支持钱包余额扣款,扣款操作必须是原子的(扣减余额+记录流水在同一事务内)
- 钱包余额不足时,不允许部分扣款,直接返回余额不足
- 支付失败时需要冲正(退回已扣余额),冲正操作必须幂等
- 早期钱包是外包团队做的,后来自研了一套。需求上需要支持从外包钱包平滑迁移到自研钱包,迁移期间两套系统并行运行
自由卡支付
自由卡是一种虚拟预付卡,用户先购买激活,之后用卡内余额消费。和钱包的区别在于自由卡是预付费储值卡,资金在购买时就已经锁定了。
核心需求:
- 自由卡购买:用户支付购买金额后激活自由卡,库存扣减和激活操作必须原子化
- 自由卡消费:支付时优先扣除自由卡余额,自由卡余额不足时可以和钱包、第三方渠道组合支付
- 支付失败自动冲正:自由卡扣款后如果后续渠道扣款失败,需要把自由卡已扣金额退回
- 自由卡在组合支付里的优先级最高(因为资金成本最低,钱早就收到了)
国际支付渠道
面向海外用户的支付渠道对接。和国内渠道最大的区别在于多币种、跨境支付、签名算法差异。
核心需求:
- 支持多币种支付和汇率转换,汇率的计算时点和手续费归属需要在需求阶段明确
- 签名算法不统一,不同渠道使用RSA、HMAC-SHA256等不同方案,需要逐一适配
- 没有统一的SDK可用,全部走HTTP裸调加鉴权头
- 退款时如果汇率发生变化,退款金额的计算规则需要提前约定
支付方式矩阵汇总
| 支付方式 | 资金流向 | 扣款模式 | 能否组合 | 退款复杂度 |
|---|---|---|---|---|
| 微信支付 | 用户→商户 | 预下单+回调 | 是 | 中 |
| 支付宝 | 用户→商户 | 预下单+回调 | 是 | 中 |
| 钱包余额 | 用户钱包→商户 | 直接扣款 | 是 | 低 |
| 自由卡 | 卡余额→商户 | 冻结+扣款 | 是(优先级最高) | 中(需要冲正) |
| 国际支付 | 用户→商户 | HTTP裸调 | 是 | 高(汇率变动) |
组合支付需求
组合支付是整个支付系统里最复杂的业务逻辑。用户在一笔交易中使用多种支付方式,系统需要按优先级依次扣款,全部成功后交易才算完成,任一失败则整体回滚。
扣款优先级
扣款顺序不是随意定的,由每种支付方式对公司的成本决定:
- 自由卡第一优先:资金已经到账,没有渠道手续费,成本最低
- 钱包余额第二优先:自有资金,没有渠道手续费
- 第三方渠道第三优先:微信支付/支付宝有渠道手续费,国际支付还有汇率成本
金额分摊
用户选择多支付方式后,系统自动计算各渠道的应扣金额:
- 自由卡扣完卡内可用余额
- 剩余金额从钱包余额扣
- 还不够的走第三方渠道
如果用户指定了某个渠道的扣款金额(比如我只想用自由卡抵扣50元,剩下的走微信),以用户指定为准。
原子性要求
组合支付的核心约束:多个渠道的扣款操作是一个整体,要么全部成功,要么全部失败回滚。不能出现自由卡扣了50元、微信支付失败了、但自由卡的50元没退回来的情况。
这就要求系统在扣款时采用先冻结后确认的模式:先冻结自由卡金额、冻结钱包余额,两个都冻住了再去第三方渠道发起支付。第三方支付成功后确认冻结金额转正,失败则释放所有冻结。
退款需求
退款不是正向支付的简单逆操作,它的复杂度体现在多个维度。
全额退款
一笔支付流水对应一次全额退款,所有渠道按原金额原路返回。
核心需求:
- 全额退款必须原路返回:微信支付的钱退到微信、钱包余额的钱退回钱包余额、自由卡的钱退回自由卡余额
- 退款申请需要审核(防止恶意退款),审核通过后异步执行退款操作
- 退款执行成功后需要回调通知订单系统更新订单状态
部分退款
一笔支付流水只退其中一部分,比全额退款复杂得多。
核心需求:
- 需要指定退款金额,系统自动计算从哪些渠道退、各退多少
- 部分退款的渠道优先级和正向支付不完全一致:正向支付时自由卡优先扣,退款时自由卡最后退(因为自由卡资金成本最低,退款影响最小)
- 同一笔支付流水可以发起多次部分退款,但累计退款金额不能超过原支付金额
- 已退款的金额不能再参与后续退款计算
多笔合并退款
一个订单包含多笔支付流水时,用户可以一次性发起整单退款,系统把多笔流水各自的渠道退款合并处理。
退款渠道差异
| 渠道 | 退款方式 | 特殊处理 |
|---|---|---|
| 微信支付 | 调用微信退款API,原路退回 | 需要退款证书 |
| 支付宝 | 调用支付宝退款API,原路退回 | 支持部分退款接口 |
| 钱包余额 | 直接退回钱包余额 | 即时到账 |
| 自由卡 | 退回自由卡余额 | 即时到账 |
| 国际支付 | HTTP裸调退款接口 | 汇率变动时退款金额需重新计算 |
对账需求
对账是资损防护的一种手动。
渠道对账
每天下载微信、支付宝等渠道的账单文件(CSV或文本格式),和支付系统的流水逐条比对。
核心需求:
- 支持多渠道账单下载:微信对账文件下载、支付宝对账文件下载、国际渠道的账单获取
- 账单解析:不同渠道账单格式不同,需要统一解析为内部格式
- 差异检测:平台有渠道没有、渠道有平台没有、两边都有但金额对不上
- 差异处理:自动标记异常、发送告警通知、记录差异明细供人工核查
订单与支付系统对账
这是开篇那个伪造回调事故的直接产物。订单系统和支付系统每天做一次交叉比对。
核心需求:
- 每天定时把订单系统的支付流水号和支付系统的支付流水号逐条比对
- 订单有但支付没有的:可能是伪造回调,最高优先级告警
- 支付有但订单没有的:可能是订单未创建或创建失败
- 两边都有但状态不一致的:需要人工核查
多主体与商户号管理
公司有多个经营主体(不同子公司、不同业务线),每个主体在微信支付或支付宝都需要独立申请商户号。
核心需求:
- 支持多商户号配置:不同的业务场景使用不同的商户号收款
- 商户号和应用、门店的关联关系:一个支付请求进来,系统要根据请求来源(APP/小程序/门店)找到对应的商户号和密钥
- 三层配置体系:应用层(哪个APP)→ 商户层(哪个公司主体)→ 门店层(哪个门店),逐级查找配置
- 配置变更不能影响正在进行的支付交易
核心业务流程
正向支付流程
一次完整的支付请求经过以下节点。
收银台阶段
用户在下单后进入收银台,系统根据订单信息和用户账户信息展示可用支付方式列表。此时系统已经完成以下前置工作:加载商户配置(商户号、密钥)、展示用户自由卡余额和钱包余额、标记哪些渠道当前可用。
支付确认阶段
用户选择支付方式并确认支付。如果是组合支付,用户选了自由卡抵扣加微信支付兜底,系统按扣款优先级排序:自由卡先扣、剩余走微信。
预下单阶段
系统向各渠道发起预下单:微信支付需要先调用统一下单API获取prepay_id,支付宝需要获取支付参数,自由卡需要冻结对应金额,钱包需要冻结余额。所有渠道预下单成功后才算进入可支付状态。
实际扣款阶段
用户完成支付操作(输入密码、扫码等),各渠道依次执行实际扣款。扣款顺序和预下单顺序一致。任一渠道扣款失败,已扣款的渠道需要冲正回退。
回调通知阶段
各渠道异步回调通知支付结果。对于组合支付,系统需要等所有渠道都回调后才能合并结果通知订单系统。这中间涉及多个回调的时序协调。
退款流程
退款流程和正向支付相反但逻辑独立。
退款申请阶段
用户或运营发起退款,系统校验退款权限和退款金额合法性。部分退款需要指定退款金额。
退款计算阶段
系统根据退款金额和各渠道原支付金额,按退款优先级计算各渠道应退金额。退款优先级和扣款优先级不完全一致。
退款执行阶段
系统异步执行各渠道退款。退款接口调用成功后等待渠道回调确认。退款失败的需要重试,重试次数和间隔需要可配置。
退款结果通知阶段
所有渠道退款成功后,通知订单系统更新状态。
功能需求全景
支付网关
| 功能 | 描述 | 优先级 |
|---|---|---|
| 接入路由 | 根据请求来源(APP/小程序/POS)分发到对应处理逻辑 | P0 |
| 参数校验 | 校验请求参数完整性和合法性 | P0 |
| 签名验证 | 验证请求签名,防止参数篡改 | P0 |
| 渠道信息补全 | 补齐用户的openId、unionId等渠道侧用户标识 | P0 |
支付编排
| 功能 | 描述 | 优先级 |
|---|---|---|
| 渠道路由 | 根据支付方式和业务规则选择支付渠道 | P0 |
| 费用分摊 | 组合支付场景下计算各渠道扣款金额 | P0 |
| 支付编排 | 按优先级依次发起各渠道扣款,协调结果 | P0 |
| 回调合并 | 多渠道回调状态管理和结果合并 | P0 |
渠道管理
| 功能 | 描述 | 优先级 |
|---|---|---|
| 微信支付对接 | 预下单、支付确认、回调处理、退款 | P0 |
| 支付宝对接 | 预下单、支付确认、回调处理、退款 | P0 |
| 钱包支付 | 余额扣款、冲正、流水记录 | P0 |
| 自由卡支付 | 冻结、扣款、冲正、余额管理 | P0 |
| 国际支付对接 | HTTP裸调下单、签名鉴权、回调处理 | P1 |
| POS支付对接 | 授权码模式、轮询查询、线下退款 | P1 |
| 渠道监控 | 渠道可用性监控、异常切换 | P1 |
退款管理
| 功能 | 描述 | 优先级 |
|---|---|---|
| 全额退款 | 原路全额退回 | P0 |
| 部分退款 | 指定金额部分退回,计算渠道分配 | P0 |
| 退款审核 | 退款申请审核流程 | P0 |
| 退款幂等 | Redis分布式锁保证退款幂等 | P0 |
| 退款重试 | 退款失败自动重试 | P1 |
| 退款查询 | 退款记录查询和导出 | P1 |
对账管理
| 功能 | 描述 | 优先级 |
|---|---|---|
| 渠道账单下载 | 多渠道账单文件自动下载 | P0 |
| 账单解析 | 不同渠道账单格式统一解析 | P0 |
| 渠道对账 | 平台流水和渠道账单逐条比对 | P0 |
| 订单支付对账 | 订单系统和支付系统交叉比对 | P0 |
| 差异告警 | 对账差异自动告警通知 | P0 |
| 差异处理 | 差异记录表和人工处理流程 | P1 |
风控管理
| 功能 | 描述 | 优先级 |
|---|---|---|
| 黑名单缓存 | 黑名单用户和设备的缓存匹配 | P0 |
| 风险识别 | 基于用户ID、IP、设备指纹的多维度风险判断 | P0 |
| 分级拦截 | 低风险放行、中风险验证码、高风险拦截 | P1 |
| 第三方风控接入 | 接入数美和腾讯云风控 | P1 |
业务规则
| 规则类别 | 具体规则 | 补充说明 |
|---|---|---|
| 支付方式组合 | 用户可自由选择支付方式组合,系统按优先级排序扣款 | 自由卡→钱包→第三方 |
| 扣款原子性 | 组合支付的多个渠道扣款是一个整体事务,全部成功或全部回滚 | 采用冻结→确认模式 |
| 自由卡优先级 | 自由卡在所有支付方式中扣款优先级最高 | 资金成本最低 |
| 退款原路返回 | 退款必须退回原支付渠道 | 微信退款到微信,钱包退款到钱包 |
| 部分退款累计 | 同一支付流水累计退款金额不能超过原支付金额 | 防止超退 |
| 退款幂等 | 同一退款请求重复提交不产生重复退款 | Redis分布式锁 |
| 授权码时效 | POS支付授权码30秒后自动失效 | 防止授权码被复用 |
| 对账频率 | 渠道对账和订单支付对账每日执行 | 最小化资损窗口 |
| 支付状态流转 | 新建→处理中→成功/失败/冲正 | 不可逆(成功不能变成失败) |
| 渠道配置 | 商户号配置变更不影响进行中的交易 | 配置变更只对新请求生效 |
状态流转
支付流水状态机
支付流水是支付系统的核心数据,从创建到终态经历以下流转:
| 当前状态 | 触发条件 | 目标状态 |
|---|---|---|
| 新建 | 支付请求进入系统 | 处理中 |
| 处理中 | 所有渠道扣款成功 | 成功 |
| 处理中 | 任一渠道扣款失败且已超时 | 失败 |
| 处理中 | 任一渠道扣款失败但未超时 | 处理中(重试其他渠道) |
| 处理中 | 系统主动冲正 | 冲正 |
| 失败 | 用户重新发起支付 | 新建(生成新流水) |
退款流水状态机
| 当前状态 | 触发条件 | 目标状态 |
|---|---|---|
| 新建 | 退款申请提交 | 处理中 |
| 处理中 | 所有渠道退款成功 | 成功 |
| 处理中 | 任一渠道退款失败 | 失败(可重试) |
| 失败 | 重试次数未超限 | 处理中 |
| 失败 | 重试次数超限 | 人工处理 |
状态约束
几个关键约束:
- 成功是终态,不能流转到任何其他状态
- 冲正是终态,扣款操作已回退,流水标记为冲正
- 失败状态下可以重新发起支付,但生成的是新支付流水,不是复制旧流水
验收标准
支付稳定性
| 指标 | 标准 | 验证方式 |
|---|---|---|
| 支付成功率 | ≥ 99%(排除用户主动放弃) | 支付流水数据 |
| 组合支付成功率 | ≥ 98% | 组合支付流水数据 |
| 支付回调处理耗时 | 单笔回调处理 ≤ 500ms | 监控数据 |
| 对账差异发现时长 | 最晚T+1日12:00前 | 对账日志 |
退款准确性
| 指标 | 标准 | 验证方式 |
|---|---|---|
| 退款金额准确率 | 100% | 退款流水和渠道流水比对 |
| 退款重复提交幂等 | 重复提交不产生重复退款 | 端到端测试 |
| 部分退款计算准确 | 累计退款金额不超原支付金额 | 数据库约束验证 |
对账覆盖率
| 指标 | 标准 | 验证方式 |
|---|---|---|
| 渠道账单下载成功率 | ≥ 99% | 定时任务执行日志 |
| 对账差异告警 | 差异发生后30分钟内通知 | 告警日志 |
| 订单支付对账覆盖率 | 每日全量对账 | 对账日志 |
风险评估
| 风险项 | 影响程度 | 发生概率 | 应对措施 |
|---|---|---|---|
| 组合支付回滚失败导致资损 | 高 | 中 | 冻结→确认模式,冲正机制兜底 |
| 支付回调被伪造 | 高 | 低 | 签名验证、订单支付系统对账 |
| 退款超退 | 高 | 低 | 累计退款金额校验、数据库约束 |
| 渠道账单下载失败 | 中 | 中 | 重试机制、下载失败告警 |
| 授权码泄露被恶意使用 | 中 | 低 | 授权码30秒过期、单次有效 |
| 款项汇率波动导致退款差异 | 中 | 中 | 退款时重新计算汇率,明确手续费归属 |
| 支付渠道故障 | 高 | 低 | 渠道健康检查、降级方案 |
术语表
| 术语 | 定义 |
|---|---|
| 支付中心 | 负责处理所有支付请求的中央服务,管理支付流水、编排多渠道扣款、处理退款和对账 |
| 组合支付 | 一笔交易使用多种支付方式(自由卡、钱包、第三方渠道)共同完成支付 |
| 渠道 | 支付系统对接的外部支付服务,如微信支付、支付宝、国际支付 |
| 支付方式 | 用户可选择的资金支付手段,包括第三方渠道支付、钱包余额、自由卡 |
| 自由卡 | 虚拟预付卡,用户购买激活后可用卡内余额消费。专栏中统一使用此称呼 |
| 授权码 | POS支付场景下用户生成的临时付款码,30秒有效,收银员通过POS机扫描完成扣款 |
| 流水 | 支付系统记录每笔支付操作的数据库记录,分主流水和渠道明细两层 |
| 对账 | 支付系统流水和渠道账单的逐条比对,以及与订单系统支付记录的交叉比对 |
| 冲正 | 支付失败后对已执行扣款操作的撤销操作,将已扣资金退回原账户 |
| 商户号 | 在微信支付或支付宝等渠道申请的唯一标识,一个经营主体可以有多个商户号 |
| 预下单 | 在用户实际支付前,向支付渠道预先创建订单并获取支付参数的操作 |
| 回调 | 支付渠道在处理完支付请求后,异步通知支付系统支付结果的HTTP请求 |
| 幂等 | 同一操作多次执行的结果与执行一次相同,防止重复扣款或重复退款 |
小结
大部分支付系统的需求文档写了三层:项目愿景、用户故事、验收条件。这些当然重要,但真正决定系统能不能落地的,是一些容易被忽略的需求细节。
比如组合支付的扣款优先级。这个排序看起来只是一个规则表,但它的推导过程不是随意排的:自由卡排第一是因为资金成本最低——钱在售卡的时候就已经收到了,扣款不发生额外费用。钱包排第二同理。第三方渠道排最后是因为每笔支付都要付渠道手续费。一个看起来简单的优先级排序,背后是财务成本分析。
再比如对账需求。很多支付系统在做需求阶段只列了渠道对账,等到出了回调伪造事故才追着补订单支付系统对账。对账不是一个可有可无的后置需求,它是防止资损的最后一道关卡,需求阶段不把它写进去,上线后再补的代价远比一开始就设计进去大得多。
需求文档不只是功能清单。每一项需求的来龙去脉都理清楚了,后面的技术方案才能做出经得起拷问的设计决策。
下一篇基于这份PRD,设计支付系统的整体技术方案:四层架构的职责划分、渠道隔离和统一支付之间的取舍、组合支付编排的模式选择。
所有的代码都可以在知识星球里获取。「应付亿级用户规模的支付系统代码实战」这个专栏在星球里是免费的,也可以接受无限次的咨询。后续新写的所有付费专栏,在知识星球里都是免费的。我的星球是:
- 老码头的技术浮生录