支付核心系统(Core Payment System)是整个支付平台的技术中枢,承担交易处理、账户管理、清算结算、对账核验等关键职能。如果把支付平台比作一家银行的运营中心,核心系统就是负责记账、清算、风险控制的"中央处理单元"——每一笔资金的流转、每一个账户余额的变动、每一次跨系统的对账核验,都由核心系统协调完成。
对于跨境支付场景来说,核心系统面临更多挑战:多币种并行、不同国家的结算规则差异、外汇汇率的实时波动、以及更复杂的合规要求。以一笔跨境电商收款为例:中国卖家在 Amazon 上卖出一件 $20 的商品,资金需要经过美国卡组织清算 → 合作银行入账 → 跨境结算 → 换汇 → 境内分发 → 卖家收款,整个过程由核心系统协调 5 个以上子系统协同完成,任何一个环节出问题都可能导致资金差错。
本章将从架构总览出发,深入账户系统、交易处理引擎、清算核心、对账系统等核心模块,结合数据模型、状态机设计和生产实践,提供一份可落地的支付核心系统技术指南。
典型跨境支付核心系统采用四层架构,每层职责清晰、可独立扩展:
┌────────────────────────────────────────────────────────────┐
│ 接入层(Channel Layer) │
│ API Gateway(统一入口) │ SDK/Client │ Webhook/Notify │
│ 负责:协议转换、鉴权、限流、参数校验 │
├────────────────────────────────────────────────────────────┤
│ 编排层(Orchestration) │
│ 交易路由 │ 风控检查 │ 费率计算 │ 合规校验 │ 换汇 │
│ 负责:业务逻辑编排、多系统协调、异常处理 │
├───────────┬────────────────────────────────────┬───────────┤
│ │ 核心层(Core Engine) │ │
│ 关联系统 │ ┌─────────────┐ ┌────────────┐ │ 数据层 │
│ 收单系统 │ │ 交易处理引擎 │ │ 账户系统 │ │ MySQL │
│ 发卡系统 │ │ (状态机引擎) │ │ (复式记账, │ │ Redis │
│ 换汇系统 │ │ (幂等保障) │ │ 余额管理) │ │ Kafka │
│ 合规系统 │ │ (分布式事务) │ │ (授信管理) │ │ 对象存储 │
│ │ └─────────────┘ └────────────┘ │ │
│ │ ┌─────────────┐ ┌────────────┐ │ │
│ │ │ 清算引擎 │ │ 对账系统 │ │ │
│ │ │ (实时清算)+ │ │ (自动匹配, │ │ │
│ │ │ (日终批处理) │ │ 差异处理) │ │ │
│ │ └─────────────┘ └────────────┘ │ │
├───────────┴────────────────────────────────────┴───────────┤
│ 基础设施层(Infrastructure) │
│ 配置中心(Nacos/Consul) │ 服务发现 │ 监控告警(Prometheus)│
│ 链路追踪(Jaeger) │ 日志中心 │ CI/CD 流水线 │
└────────────────────────────────────────────────────────────┘
每个模块都有清晰的职责边界,防止功能交叉和调用混乱:
| 模块 | 核心职责 | 非职责(不应做) |
|---|---|---|
| 交易处理引擎 | 订单生命周期管理、状态机转换、幂等保障、超时处理 | 资金结算、风控决策 |
| 账户系统 | 余额管理、复式记账、授信额度、账户开设/销户 | 交易路由、费率计算 |
| 清算核心 | 日终/实时清算、资金归集、结算对账、净额计算 | 交易授权、风控判断 |
| 对账系统 | 内外数据匹配、差异发现、调账处理、对账报告 | 账户余额维护 |
跨境支付核心系统的性能要求比传统支付更高,因为涉及跨时区、跨国界的复杂性:
| 指标 | 目标值 | 说明 |
|---|---|---|
| P99 响应时间 | < 500ms | 从接收到返回,含所有内部子系统调用 |
| 吞吐量 | > 10,000 TPS | 峰值处理能力,黑五/双11需更高 |
| 可用性 | 99.99% | 全年计划外停机不超过 53 分钟 |
| 数据一致性 | 最终一致 <= 5s | 跨系统账务最终一致时间 |
| 日终批处理 | < 30 分钟 | 全量清算、对账、结算完成时间 |
| 汇率刷新 | < 1s | 外汇汇率实时更新延迟 |
账户系统是支付核心的基础设施,负责记录每一笔资金的来源与去向。一个好的账户系统设计,需要同时满足会计合规要求、高并发性能和异常追溯能力。
CREATE TABLE `account` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
`account_no` varchar(32) NOT NULL COMMENT '账号(唯一业务标识)',
`account_type` tinyint(4) NOT NULL COMMENT '账户类型:1-结算户 2-冻结户 3-手续费户 4-保证金户',
`currency` char(3) NOT NULL COMMENT '币种,如 CNY, USD, EUR',
`balance` bigint(20) NOT NULL DEFAULT '0' COMMENT '可用余额(最小单位,如分/厘)',
`frozen_amount` bigint(20) NOT NULL DEFAULT '0' COMMENT '冻结金额',
`total_credit` bigint(20) NOT NULL DEFAULT '0' COMMENT '累计入账金额',
`total_debit` bigint(20) NOT NULL DEFAULT '0' COMMENT '累计出账金额',
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:0-关闭 1-正常 2-冻结 3-锁定',
`version` int(11) NOT NULL DEFAULT '0' COMMENT '乐观锁版本号',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_account_no` (`account_no`),
KEY `idx_currency` (`currency`),
KEY `idx_account_type` (`account_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账户表';
设计要点说明:
最小单位存储:balance 使用 bigint 存储金额的最小单位(如分、厘),绝对禁止使用 float 或 double。例如 $1.23 USD 存储为 123(美分),¥123.45 存储为 12345(人民币分)。如果业务需要更高精度(如汇率场景),可以使用厘(1/1000 单位)。
乐观锁版本号:version 字段用于并发控制。在高并发场景下,多个请求可能同时操作同一账户,乐观锁比 SELECT ... FOR UPDATE 性能更好。
累计字段:total_credit 和 total_debit 辅助对账,避免全表扫描流水表来获取账户累计变动。
| 类型 | 代码 | 用途 | 适用场景 | 特殊规则 |
|---|---|---|---|---|
| 结算户 | SETTLEMENT | 商户日常入账、出金 | 交易入账、提现 | 余额可正可负(信用垫付商户) |
| 冻结户 | FROZEN | 交易争议、保证金 | 退款冻结、维权锁定 | 不可出金,解冻后转结算户 |
| 手续费户 | FEE | 平台服务费归集 | 费率扣除、佣金结算 | 只入不出,日终自动划转 |
| 保证金户 | DEPOSIT | 风控质押 | 大额交易押金 | 合同到期释放 |
| 备付金户 | RESERVE | 监管资金存管 | 央行的客户备付金 | 受监管要求约束 |
支付核心系统必须采用复式记账(Double-Entry Bookkeeping)。每一笔资金变动都在两个或以上账户同时记账,保证"有借必有贷,借贷必相等"。这是会计学的基本原则,也是支付系统防错的最后一道防线。
CREATE TABLE `account_journal` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`journal_no` varchar(64) NOT NULL COMMENT '流水号',
`transaction_no` varchar(64) NOT NULL COMMENT '关联交易号',
`account_no` varchar(32) NOT NULL COMMENT '账号',
`direction` tinyint(4) NOT NULL COMMENT '方向:1-借方(DR) 2-贷方(CR)',
`amount` bigint(20) NOT NULL COMMENT '金额(最小单位)',
`balance_before` bigint(20) NOT NULL COMMENT '记账前余额',
`balance_after` bigint(20) NOT NULL COMMENT '记账后余额',
`journal_type` varchar(32) NOT NULL COMMENT '记账类型',
`remark` varchar(256) DEFAULT NULL COMMENT '备注',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_journal_no` (`journal_no`),
KEY `idx_transaction_no` (`transaction_no`),
KEY `idx_account_no` (`account_no`),
KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='账务流水表';
为什么要记录 balance_before 和 balance_after?
这两个字段看似冗余,但实际意义重大:
balance_after - balance_before = amount假设场景:跨境电商平台用户 A 在商户 B 消费 $100 USD,平台收取 3% 手续费。
复式记账分录:
借(DR):商户B的结算户 +$97.00 (商户实收 $100 - $3 手续费)
借(DR):平台手续费户 +$3.00 (平台收入)
贷(CR):资金存管户 -$100.00 (持卡人方出账)
检查:$97 + $3 = $100 ✅ 借贷平衡,资金守恒
对应数据库的流水记录:
| 流水号 | 账号 | 方向 | 金额 | 原始余额 | 变动后余额 | 类型 |
|---|---|---|---|---|---|---|
| JNL001 | ACC_B_SETTLEMENT | DR | 9700 | 500000 | 509700 | 交易入账 |
| JNL002 | ACC_FEE_REVENUE | DR | 300 | 100000 | 100300 | 手续费 |
| JNL003 | ACC_CUSTODY | CR | 10000 | 1000000 | 990000 | 资金流出 |
当用户发起退款时,需要反转原分录:
退款会计分录:
借(DR):资金存管户 +$100.00 (资金回到持卡人方)
贷(CR):商户B的结算户 -$97.00 (从商户账扣回)
贷(CR):平台手续费户 -$3.00 (平台返还手续费)
检查:-$97 + (-$3) + $100 = 0 ✅ 借贷平衡
余额变更是核心系统最容易出 bug 的地方。以下是生产级别的余额变更 SQL:
-- 使用乐观锁更新余额(推荐方案)
UPDATE account
SET balance = balance + #{changeAmount},
total_credit = total_credit + #{creditAmount},
total_debit = total_debit + #{debitAmount},
version = version + 1
WHERE account_no = #{accountNo}
AND version = #{oldVersion}
-- 余额不足检查(出金场景)
AND (balance + #{changeAmount}) >= 0;
-- 检查影响行数
-- 如果为0:version 不匹配(并发冲突)或余额不足
实际例子:并发扣款冲突
假设商户结算户初始余额 $1000,同时到达两笔扣款请求(各 $600):
请求 A(扣 $600):读取 balance = $1000, version = 5
请求 B(扣 $600):读取 balance = $1000, version = 5
请求 A 先执行 UPDATE → 成功,balance=400, version=6
请求 B 执行 UPDATE → WHERE version=5 不匹配,影响行数=0 → 重试
请求 B 重试:读取 balance = $400, version = 6
UPDATE → balance - 600 < 0,条件不满足 → 返回余额不足
| 陷阱 | 问题描述 | 真实案例 | 解决方案 |
|---|---|---|---|
| 先查后更(非原子) | 两个线程读到相同余额,都提交成功 | 用户被重复扣款 $500 | 乐观锁或 SELECT ... FOR UPDATE |
| 浮点数精度 | $0.1 + $0.2 = $0.30000000000000004 | 对账永远差 0.00000000000000004 | 统一使用最小单位整数 |
| 无幂等防止 | 同一指令执行两次 | 退款发了两次,商户被扣两次 | 流水号唯一索引 + 重复检测 |
| 半写失败 | 一个账户减成功,另一个加失败 | 扣了用户钱但商户没收 | TCC 或 SAGA 模式 |
| 缺少回滚记录 | 异常后无法追溯 | 无法确定谁在何时改了余额 | 所有操作前先写流水记录 |
交易处理引擎是支付核心系统的"大脑"。它负责管理每笔交易从创建到完成的全生命周期,包括状态转换、幂等控制、超时处理和异常恢复。
支付交易有严格的、不可逆的状态转换规则。一个设计良好的状态机可以防止非法状态转换,是交易一致性的核心保障。
┌─────────────┐
│ INIT │
│ (初始状态) │
└──────┬──────┘
│ 接收支付请求通过基本校验
┌──────▼──────┐
┌─────────┤ PENDING │
│ │ (等待处理) │
│ └──────┬──────┘
│ 路由选择完成 │ 风控拒绝
│ │
┌─────▼─────┐ ┌─────▼──────┐
│ FAILED │ │ PROCESSING │
│ (交易失败) │ │ (处理中) │
└───────────┘ └──────┬─────┘
渠道回调成功│ 渠道明确拒绝
│
┌──────▼──────┐
┌───────┤ SUCCESS │
│ │ (交易成功) │
│ └──────┬─────┘
│ 清算触发 │ 用户发起退款
│ │
┌──────▼─────┐ ┌────▼──────┐
│ REFUND │ │ SETTLED │
│ (已退款) │ │ (已结算) │
└────────────┘ └───────────┘
# 状态转换规则定义(可作为代码中的枚举映射)
TRANSITION_RULES = {
"INIT": ["PENDING", "FAILED"],
"PENDING": ["PROCESSING", "FAILED"],
"PROCESSING": ["SUCCESS", "FAILED", "PENDING"], # PENDING 用于重试后的重新排队
"SUCCESS": ["SETTLED", "REFUND"],
"REFUND": ["REFUNDED"], # REFUNDED 为终态
"SETTLED": [], # 终态
"FAILED": ["PENDING"], # 人工干预后可重试
}
def validate_transition(current_state, next_state):
allowed = TRANSITION_RULES.get(current_state, [])
if next_state not in allowed:
raise IllegalStateTransition(
f"不允许从 {current_state} 转换到 {next_state},"
f"允许的状态:{allowed}"
)
# 使用示例
validate_transition("PENDING", "SUCCESS") # ✅ 允许
validate_transition("FAILED", "SUCCESS") # ❌ 不允许,会抛出异常
validate_transition("INIT", "SETTLED") # ❌ 不允许
CREATE TABLE `transaction_status_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`transaction_no` varchar(64) NOT NULL COMMENT '交易号',
`from_status` varchar(32) NOT NULL COMMENT '原始状态',
`to_status` varchar(32) NOT NULL COMMENT '目标状态',
`operator` varchar(64) NOT NULL COMMENT '操作者:系统/人工',
`reason` varchar(256) DEFAULT NULL COMMENT '变更原因',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_transaction_no` (`transaction_no`),
KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易状态变更日志';
为什么要记录状态变更日志?
支付系统中最致命的问题:同一笔请求被执行两次,导致用户被重复扣款。 幂等控制是第一道防线,也是必须 100% 覆盖的防护措施。
-- 全局幂等表(独立于业务表)
CREATE TABLE `idempotent_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`idempotent_key` varchar(128) NOT NULL COMMENT '幂等键:请求来源+业务类型+业务唯一ID',
`request_hash` varchar(64) NOT NULL COMMENT '请求参数MD5(检测参数变更)',
`result` json NOT NULL COMMENT '首次处理的结果(缓存返回,避免重复执行)',
`expire_at` datetime NOT NULL COMMENT '过期时间(超时后允许重新处理)',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_idempotent_key` (`idempotent_key`),
KEY `idx_expire_at` (`expire_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='幂等记录表';
| 场景 | 幂等键组成 | 示例 | 说明 |
|---|---|---|---|
| 订单支付 | merchant_id:order_id:payment_no |
M1001:ORD20240530:PAY20240530001 |
商户订单号+支付单号 |
| 退款 | original_txn_no:refund_reason:amount |
TXN20240530:FULL:10000 |
原交易号+退款金额 |
| 账户充值 | user_id:deposit_id:channel:amount |
U1234:DEP20240530:BANK_WIRE:500000 |
用户+充值单号 |
| 批次结算 | batch_no:settle_date |
BATCH20240530:2024-05-30 |
批次号+结算日 |
| 换汇 | fx_quote_id:from_currency:to_currency:amount |
FXQ20240530:USD:CNY:100000 |
外汇报价ID+币种 |
请求到达
│
1. 检查幂等键是否已存在
├── 存在 → 2a. 比较 request_hash
│ ├── 一致 → 返回缓存结果(幂等命中)
│ └── 不一致 → 抛出异常(参数变更不能幂等)
└── 不存在 → 2b. 插入幂等记录(唯一索引,失败则冲突)
│
3. 执行业务逻辑
│
4. 更新幂等记录(写入 result 和状态)
│
5. 返回业务结果
支付交易涉及多个系统(账户、外汇、风控、渠道),分布式事务是保证一致性的核心手段。选取合适的模式是架构设计的关键决策。
适用于长事务、多步骤的场景。用一系列本地事务协调,每个事务有对应的补偿操作(Compensation)。
SAGA 流程示例:跨境收款 $100 USD → 换汇为 CNY
主流程(Normal Flow):
Step 1: 记录收款单(本地事务提交 ✅)
Step 2: 扣划付款人 USD 账户(提交 ✅)
Step 3: 调用换汇系统锁汇(USD→CNY)(提交 ✅)
Step 4: 调用汇率计算,确认汇率(提交 ✅)
Step 5: 入账收款人 CNY 账户(提交 ✅)
→ 全部完成,结束 ✅
异常恢复(Step 3 锁汇失败):
Rollback Step 2: 释放付款人 USD 账户(补偿操作)
Rollback Step 1: 标记收款单为失败
→ 回滚完成
SAGA 与 2PC 的详细对比:
| 特性 | SAGA | 2PC(XA) | 说明 |
|---|---|---|---|
| 资源锁定 | 不锁定,提交即释放 | 持有锁直到协调者决策 | SAGA 并发更高 |
| 吞吐量 | 高(无长锁) | 低(长锁) | 实测 SAGA 吞吐约为 2PC 的 3-5 倍 |
| 一致性 | 最终一致 | 强一致 | SAGA 有短暂不一致窗口 |
| 实现复杂度 | 需实现补偿逻辑 | 中间件自动处理 | SAGA 需要更多开发工作 |
| 容错能力 | 高(各服务独立) | 低(协调者单点) | SAGA 不存在单点问题 |
| 适用场景 | 支付、订单、物流 | 银行转账、库存扣减 | 长链 vs 短链 |
适用于短事务、高一致性的场景,如账户余额变更。
TCC 三阶段详解:
Try(预留资源阶段):
- 冻结商户账户中的 $50(标记为冻结,不可使用)
- 写入 TCC 预留记录(try_id, 金额, 过期时间)
- 返回 Try 成功
Confirm(确认提交阶段):
- 读取 TCC 预留记录
- 将冻结的 $50 转为正式扣除(balance -= 50, frozen_amount -= 50)
- 完成对手方入账
- 标记 TCC 记录为已确认
- 返回 Confirm 成功
Cancel(取消回滚阶段):
- 读取 TCC 预留记录
- 解冻 $50(frozen_amount -= 50, balance 不变)
- 标记 TCC 记录为已取消
- 返回 Cancel 成功
TCC 失效场景示例:
场景:Try 阶段扣了 A 账户,B 账户 Try 失败
Step 1: Try 扣 A 账户 $50 → 成功 ✅(A 账户显示冻结 $50)
Step 2: Try 扣 B 账户 $50 → 失败 ❌(B 账户余额不足)
Step 3: Cancel 解冻 A 账户 → 成功 ✅(A 账户冻结取消)
→ 正确回滚
但如果 Step 3 也失败(网络超时):
→ 需要定时任务扫描"悬挂"的 TCC 记录,定期执行 Cancel
→ 这就是所谓「悬挂事务处理」
| 场景 | 推荐模式 | 涉及系统数 | 典型耗时 | 选择理由 |
|---|---|---|---|---|
| 单笔支付 | SAGA | 3-5 个 | ~2s | 长事务,补偿逻辑清晰 |
| 余额变更 | TCC | 1-2 个 | ~10ms | 强一致,短平快 |
| 批量清算 | SAGA + 补偿表 | 2-3 个 | ~30min | 大事务,中间状态可接受 |
| 账户开立 | TCC | 1 个 | < 100ms | 简单操作,强一致 |
| 跨币种交易 | SAGA | 4-6 个 | ~3s | 涉及汇率锁定,需补偿 |
除了 SAGA 和 TCC,本地消息表(Local Message Table)是最简单实用的保证最终一致的方式,特别适合"异步通知"场景:
-- 本地消息表(与业务表在同一个数据库)
CREATE TABLE `local_message` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`biz_id` varchar(64) NOT NULL COMMENT '业务ID',
`biz_type` varchar(32) NOT NULL COMMENT '业务类型:payment/refund/settlement',
`payload` json NOT NULL COMMENT '消息内容(JSON格式)',
`status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '0-待发送 1-已发送 2-已确认 3-已放弃',
`retry_count` int(11) NOT NULL DEFAULT '0' COMMENT '已重试次数',
`max_retry` int(11) NOT NULL DEFAULT '3' COMMENT '最大重试次数',
`next_retry_at` datetime DEFAULT NULL COMMENT '下次重试时间',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_status_next_retry` (`status`, `next_retry_at`),
KEY `idx_biz_id` (`biz_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='本地消息表';
处理流程:
清算是支付过程中"算账"的环节:将一整天(或实时)产生的交易数据汇总、计算各方净额、扣除手续费和外汇成本,最终完成资金的有效划转。
| 模式 | 处理时机 | 资金流转方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|---|
| 日终批量清算 | T+0/T+1 日终 | 净额结算 | 传统收单、B2B | 流转次数少、成本低 | 时效性差,T+1 到账 |
| 实时清算 | 每笔交易完成 | 全额逐笔 | 跨境汇款、P2P | 资金实时到账 | 流转频繁,成本较高 |
| 混合模式 | 实时+日终 | 实时冻结、日终划转 | 大型支付平台 | 兼顾体验和效率 | 实现最复杂 |
以一个典型的跨境电商平台为例,日终清算流程如下:
22:00 ─ 清算任务自动触发
│
├── 步骤 1:锁定当日交易
│ 将所有状态为 SUCCESS 的交易标记为"清算中"
│ 禁止对这部分交易进行修改操作
│
├── 步骤 2:读取当日所有可清算交易
│ SELECT * FROM transactions
│ WHERE status = 'SETTLE_PENDING'
│ AND created_at BETWEEN '2024-05-30 00:00:00' AND '2024-05-30 23:59:59'
│
├── 步骤 3:按商户维度汇总
│ ┌─────────────────┬────────────┬──────────┬──────────┐
│ │ 商户ID │ 总交易额 │ 手续费 │ 净额 │
│ ├─────────────────┼────────────┼──────────┼──────────┤
│ │ Merchant_A (CN) │ +$125,000 │ -$3,750 │ $121,250 │
│ │ Merchant_B (US) │ +$82,000 │ -$2,460 │ $79,540 │
│ │ Merchant_C (EU) │ +$45,000 │ -$1,350 │ $43,650 │
│ │ ... │ ... │ ... │ ... │
│ └─────────────────┴────────────┴──────────┴──────────┘
│
├── 步骤 4:计算外汇净额
│ 将不同币种的净额按当日汇率折算为结算币种
│ ┌─────────────────┬─────────┬────────┬────────┬────────┐
│ │ 商户 │ 原币种 │ 原金额 │ 汇率 │ 结算金额 │
│ ├─────────────────┼─────────┼────────┼────────┼────────┤
│ │ Merchant_A │ USD │ 121250 │ 7.24 │ Y878,210│
│ │ Merchant_B │ USD │ 79,540 │ 7.24 │ Y575,870│
│ │ Merchant_C │ EUR │ 43,650 │ 7.85 │ Y342,653│
│ └─────────────────┴─────────┴────────┴────────┴────────┘
│
├── 步骤 5:生成结算单
│ 每商户一份结算单,包含:交易明细、手续费明细、外汇明细
│
├── 步骤 6:调用资金系统执行划转
│ 通过银行接口或自有资金池完成实际资金转移
│
└── 00:00 ─ 清算完成,状态更新为 SETTLED
对于需要 T+0 到账的场景(如跨境电商即时到账),清算必须实时完成。这需要更精密的设计:
交易到达核心系统
│
1. 路由决策 → 选择最优渠道
2. 执行交易(扣持卡人款/从钱包扣款)
│
▼
3. 实时清算流程:
3.1 确定各方权益(平台、商户、渠道、外汇商)
3.2 更新实时待结算记录(pending_settle_summary)
3.3 如果满足"即时到账"条件(商户等级、风控评分、额度范围)
→ 触发实时划转
3.4 否则 → 仅更新待结算汇总,等待日终批量处理
│
▼
4. 返回交易结果给商户
CREATE TABLE `pending_settle_summary` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`merchant_id` varchar(32) NOT NULL COMMENT '商户ID',
`currency` char(3) NOT NULL COMMENT '币种',
`total_amount` bigint(20) NOT NULL DEFAULT '0' COMMENT '待结算总额(最小单位)',
`total_count` int(11) NOT NULL DEFAULT '0' COMMENT '待结算笔数',
`settle_cycle` varchar(16) NOT NULL COMMENT '结算周期:T0/T1/D7/D30',
`last_settle_at` datetime DEFAULT NULL COMMENT '上次结算时间',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_merchant_cycle` (`merchant_id`, `currency`, `settle_cycle`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='待结算汇总表';
对账是支付系统发现问题的"最后一道防线"。理论上的完美系统在实际中必然存在差异——网络超时、渠道延迟、参数漂移、银行系统 bug,都需要对账来发现和纠正。
┌──────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ 内部数据源 │ │ 外部数据源 │ │ 对账引擎 │
├──────────────────┤ ├──────────────────┤ ├─────────────────┤
│ - 交易流水表 │ │ - 银行对账单 │ │ 1. 数据加载 │
│ - 账户流水表 │ │ - 渠道结算单 │ │ 2. 格式转换 │
│ - 清算记录 │ │ - 卡组织数据 │ │ 3. 自动匹配 │
│ - 退款记录 │ │ - SWIFT MT940 │ │ 4. 差异标记 │
└──────────────────┘ └──────────────────┘ └────────┬────────┘
│
┌─────────────▼──────────┐
│ 差异处理模块 │
├────────────────────────┤
│ - 长款:我方有/对方无 │
│ - 短款:我方无/对方有 │
│ - 金额不一致 │
│ - 时间不在合理窗口 │
│ - 重复记录(同一笔被 │
│ 对账单记录了两次) │
└────────────────────────┘
最常用、效率最高的匹配方式。适用于大多数场景:
def exact_match(internal_records, external_records):
"""
精确匹配算法(按交易唯一ID)
复杂度:O(n),使用哈希表
输入示例:
internal = [{"txn_id": "TXN001", "amount": 10000}, ...]
external = [{"txn_ref": "TXN001", "amount": 10000}, ...]
返回:
matched: 成功匹配的记录对
long_exception: 内部有但外部无(我方长款,需补账)
short_exception: 外部有但内部无(我方短款,需追回)
"""
external_map = {r["txn_ref"]: r for r in external_records}
matched_pairs = []
long_exceptions = []
for internal in internal_records:
key = internal["txn_id"]
external = external_map.get(key)
if external is None:
# 我方有记录,对方没有 → 长款
long_exceptions.append({
"record": internal,
"reason": "LONG_EXCEPTION",
"description": f"交易 {key} 仅存于我方系统"
})
elif external["amount"] != internal["amount"]:
# 金额不一致 → 金额差异
long_exceptions.append({
"record": internal,
"external": external,
"reason": "AMOUNT_MISMATCH",
"description": f"交易 {key} 金额不符: "
f"我方={internal['amount']}, 对方={external['amount']}"
})
del external_map[key] # 已处理,从 map 移除
else:
# 完全匹配
matched_pairs.append((internal, external))
del external_map[key]
# 剩余的 external_map 中的记录为短款
short_exceptions = [
{
"record": rec,
"reason": "SHORT_EXCEPTION",
"description": f"交易 {rec['txn_ref']} 仅存于对方系统"
}
for rec in external_map.values()
]
# 计算匹配率
total = len(internal_records) + len(short_exceptions)
match_rate = len(matched_pairs) / max(total, 1) * 100
print(f"对账结果汇总:")
print(f" 总记录数: 内部={len(internal_records)}, 外部={len(external_records)}")
print(f" 匹配成功: {len(matched_pairs)} 笔 ({match_rate:.2f}%)")
print(f" 长款异常: {len(long_exceptions)} 笔")
print(f" 短款异常: {len(short_exceptions)} 笔")
return {
"matched": matched_pairs,
"long": long_exceptions,
"short": short_exceptions,
"match_rate": match_rate
}
实际运行示例:
# 模拟某天的对账数据
internal_txns = [
{"txn_id": "TXN001", "amount": 10000}, # $100.00
{"txn_id": "TXN002", "amount": 25000}, # $250.00
{"txn_id": "TXN003", "amount": 50000}, # $500.00
{"txn_id": "TXN005", "amount": 7500}, # $75.00 我方有但对方没有
]
external_txns = [
{"txn_ref": "TXN001", "amount": 10000}, # $100.00 匹配
{"txn_ref": "TXN002", "amount": 24900}, # $249.00 金额不一致!
{"txn_ref": "TXN004", "amount": 80000}, # $800.00 对方有但我方没有
{"txn_ref": "TXN006", "amount": 32000}, # $320.00 对方有但我方没有
]
result = exact_match(internal_txns, external_txns)
输出:
对账结果汇总:
总记录数: 内部=4, 外部=4
匹配成功: 1 笔 (25.00%)
长款异常: 2 笔
短款异常: 2 笔
当内部系统与外部渠道存在时间差时(例如有些渠道在次日凌晨才返回前一天的部分数据),需要模糊匹配:
| 匹配条件 | 说明 | 容忍范围 |
|---|---|---|
| 金额精确匹配 | 内部金额 = 外部金额 | 0 |
| 时间窗口 | 交易时间在指定窗口内 | ±24h |
| 标识部分匹配 | 部分 ID 匹配(前缀/后缀一致) | 至少 8 位匹配 |
| 批量匹配 | 多笔内部交易合计等于一笔外部记录 | 允许合并对账 |
| 差异类型 | 严重程度 | 自动处理 | 人工介入条件 |
|---|---|---|---|
| 我方有,外部无(长款) | 中 | 自动挂起,次日再对一次 | 连续 3 个工作日未匹配 |
| 外部有,我方无(短款) | 高 | 自动创建补账工单 | 金额 > $1,000 |
| 金额不一致 | 高 | 标记差异,自动重对 | 金额差 > $1 |
| 时间不在窗口 | 低 | 自动延期 24h 再对 | 延期 3 次未匹配 |
| 重复支付 | 严重 | 自动冻结,通知风控 | 立即通知客服介入 |
| 缺失外部数据 | 低 | 发送数据请求给渠道 | 3 个工作日未返回 |
recon_20240530、recon_20240531),避免单表数据过大导致查询缓慢支付系统一旦宕机,损失不仅是收入,更是用户信任。以下是经过实战检验的高可用架构设计。
┌─────────┐
│ DNS/GSLB │
│ 全球负载 │
└────┬─────┘
│ 根据用户IP地理分流
│
┌───────────────┼───────────────┐
│ │ │
┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐
│ Region-A │ │ Region-B │ │ Region-C │
│ 杭州机房 │ │ 上海机房 │ │ 新加坡机房 │
│ (主生产) │ │ (容灾) │ │ (海外节点) │
├───────────┤ ├───────────┤ ├───────────┤
│ K8s Cluster│ │ K8s Cluster│ │ K8s Cluster│
│ 20+ Pod │ │ 10+ Pod │ │ 8+ Pod │
│ │ │ │ │ │
│ MySQL 主 │←──│ MySQL 从 │ │ MySQL 从 │
│ (读写) │ DR│ (只读) │ │ (只读) │
│ │ │ │ │ │
│ Redis 主 │←──│ Redis 从 │ │ Redis 从 │
│ (读写) │ DR│ (只读) │ │ (只读) │
└───────────┘ └───────────┘ └───────────┘
│ │ │
└───────────────┼───────────────┘
│ Kafka 跨集群同步
┌────▼────┐
│ Kafka │
│ 事件总线 │
└─────────┘
| 故障类型 | RTO(恢复时间目标) | RPO(数据损失点) | 切换方式 | 预期损失 |
|---|---|---|---|---|
| 单 Pod 宕机 | < 30s | 0 | K8s 自动重启 | 几十笔交易超时 |
| 单台服务器宕机 | < 2min | 0 | 服务自动剔除 | 几百笔交易延迟 |
| 机房级故障 | < 5min | < 1min | DNS 路由切换 | 少量交易丢失需对账修复 |
| 区域级故障 | < 15min | < 5min | 手动切换 | 5 分钟内数据可能需要人工补录 |
支付系统绝对不能完全不可用。降级策略决定了在极端情况下的服务表现。
| 降级等级 | 触发条件 | 保留功能 | 关闭功能 |
|---|---|---|---|
| L0 正常 | - | 全部功能 | - |
| L1 轻微降级 | 数据库响应 > 500ms | 基础支付、余额查询 | 大额交易(>$10K)、数据统计 |
| L2 中度降级 | 外部渠道超时 > 10% | 只读查询、小额支付(<$100) | 所有写操作(除支付外) |
| L3 严重降级 | 极端流量 > 5x 峰值 | 仅支付成功查询 | 创建新支付、退款、提现 |
| L4 保护模式 | 数据不一致检测 | 仅健康检查 | 全部业务功能 |
# 渠道接口熔断配置(基于 Hystrix/Resilience4j)
channel-payment:
circuitBreaker:
enabled: true
requestVolumeThreshold: 20 # 10秒滑动窗口内20次请求触发计算
sleepWindowInMilliseconds: 30000 # 熔断后30秒尝试半开
errorThresholdPercentage: 50 # 50%的请求失败就熔断
execution:
timeout:
enabled: true
value: 5000 # 单次请求超时5秒
threadPool:
coreSize: 50 # 核心线程池大小
maxQueueSize: 200 # 排队队列上限
熔断带来的正面效果示例:
假设某境外支付渠道因网络波动开始超时:
支付核心不是独立运行的,它与周边系统紧密协作。清晰的接口定义和交互协议是保证整体系统可靠性的前提。
┌──────────────┐ 支付指令(HTTP/RPC) ┌──────────────┐
│ 收单子系统 │─────────────────────────→│ │
│ (Acquiring) │←─────状态回调(Webhook)────│ │
└──────────────┘ │ │
│ 支付核心系统 │
┌──────────────┐ 风控检查(RPC) │ │
│ 风控子系统 │─────────────────────────→│ │
│ (Risk Ctrl) │←─────风险级别(RPC)───────│ │
└──────────────┘ │ │
│ │
┌──────────────┐ 汇率查询(RPC) │ │
│ 换汇子系统 │─────────────────────────→│ │
│ (FX Engine) │←─────锁汇确认(RPC)───────│ │
└──────────────┘ │ │
│ │
┌──────────────┐ 账户指令(RPC) │ │
│ 账户子系统 │─────────────────────────→│ │
│ (Accounting) │←─────记账结果(RPC)──────│ │
└──────────────┘ │ │
│ │
┌──────────────┐ 出金指令(HTTP) │ │
│ 渠道网关 │─────────────────────────→│ │
│ (Channel GW) │←─────渠道回调(Webhook)───│ │
└──────────────┘ └──────────────┘
{
"api_version": "v2.0",
"protocol": "REST/JSON",
"endpoint": "POST /api/v2/payment/create",
"request": {
"merchant_id": "M1001",
"order_id": "ORD202405300001",
"amount": 10000,
"currency": "USD",
"payment_method": "VISA_CREDIT",
"description": "跨境电商订单 #12345",
"idempotent_key": "M1001:ORD202405300001:PAY001",
"callback_url": "https://merchant.com/payment/callback",
"metadata": {
"user_ip": "192.168.1.1",
"user_agent": "Mozilla/5.0..."
}
},
"response": {
"code": 0,
"message": "success",
"data": {
"transaction_id": "TXN20240530001001",
"status": "PROCESSING",
"amount": 10000,
"fee": 300,
"exchange_rate": null
}
}
}
| 领域 | 推荐资料 | 说明 |
|---|---|---|
| 账户体系 | 《企业应用架构模式》- Martin Fowler | 账务架构权威参考 |
| 分布式事务 | 《Designing Data-Intensive Applications》 | SAGA / TCC 模式深度解析 |
| 支付清算 | 银联/网联清算体系技术白皮书 | 国内清算标准参考 |
| 高可用 | Google SRE 系列书籍 | 生产环境可靠性最佳实践 |
| 对账系统 | ISO 20022 报文标准 | 跨境支付对账报文规范 |
| 支付合规 | FATF 建议书 | AML/CFT 国际标准 |
| 幂等性 | Stripe API 文档:Idempotent Requests | 工业级幂等性实现参考 |
相关 Wiki 页面: