支付系统全景图:钱从哪来,到哪去
当玩家点击"充值648元"的那一刻,钱经历了怎样的奇幻漂流?这篇文章带你看清支付系统的全貌。
🎮 一次充值的旅程
想象这样一个场景:
深夜11点,你的游戏服务器在线人数达到峰值。 一位玩家点击了"充值648元"按钮。 3秒后,钻石到账。玩家心满意足地继续游戏。
看起来简单,对吧?
但在你看不见的地方,这笔钱穿越了支付网关、风控系统、 订单引擎、对账平台……至少经过了七八个系统的处理。
今天,我们来拆解这个"钱从哪来,到哪去"的完整链条。
[配图建议:支付系统整体架构图,展示从用户点击到资金到账的完整链路]
🏗️ 支付系统的整体架构
把支付系统想象成一个快递网络:
- 下单系统 = 下单界面(你填收货地址)
- 支付网关 = 快递收件点(收集所有快递单)
- 支付渠道 = 不同快递公司(顺丰、京东、邮政……)
- 订单系统 = 物流追踪(知道包裹在哪)
- 对账系统 = 财务结算(快递公司和商家对账)
完整流程是这样的:
用户点击充值
→ 订单系统创建订单(状态:待支付)
→ 支付网关选择渠道(微信/支付宝/苹果……)
→ 跳转到支付页面
→ 用户完成支付
→ 支付渠道异步通知(重要!)
→ 订单系统更新状态(已支付)
→ 游戏服务器发放商品
→ 资金进入待结算账户
→ T+1或T+N 对账后进入平台账户
整个链路涉及两个核心问题:
- 信息流:订单状态如何准确流转?
- 资金流:钱如何安全到达平台账户?
[配图建议:信息流与资金流的双流图,用不同颜色标注两条流]
🏢 支付系统架构详解
上面是简化版,现在让我们深入看看真实生产环境中的支付系统架构。
分层架构设计
一个成熟的支付系统通常采用六层架构:
┌─────────────────────────────────────────────────────┐
│ 接入层 │
│ (API Gateway / 负载均衡 / 限流) │
├─────────────────────────────────────────────────────┤
│ 业务层 │
│ (订单服务 / 商品服务 / 用户服务) │
├─────────────────────────────────────────────────────┤
│ 支付网关层 │
│ (路由策略 / 渠道适配 / 协议转换) │
├─────────────────────────────────────────────────────┤
│ 渠道适配层 │
│ (微信SDK / 支付宝SDK / 苹果IAP / ...) │
├─────────────────────────────────────────────────────┤
│ 核心服务层 │
│ (风控引擎 / 账户服务 / 对账服务) │
├─────────────────────────────────────────────────────┤
│ 数据层 │
│ (订单库 / 账户库 / 流水库 / 缓存) │
└─────────────────────────────────────────────────────┘
各层职责详解
接入层就像小区的门卫大爷,负责:
- 身份验证:你是谁?有权限吗?
- 流量控制:人太多了,排队等一等
- 请求路由:你要去几号楼?往这边走
- 协议适配:不管你说普通话还是方言,都能接待
技术实现通常用 Nginx + Lua 或者 API Gateway(如 Kong、APISIX)。
业务层处理具体业务逻辑:
- 订单服务:创建订单、查询订单、更新状态
- 商品服务:商品信息、库存管理、促销计算
- 用户服务:用户信息、会员等级、消费记录
这里有个关键设计:业务层和支付层要解耦。
为什么?因为业务变化快(活动、促销、新玩法), 而支付逻辑相对稳定(微信支付的接口不会天天变)。
这是支付系统的"大脑",负责:
- 渠道路由:根据金额、用户偏好、渠道状态选择最优渠道
- 协议转换:把业务请求转换成各渠道需要的格式
- 失败重试:渠道挂了自动切换备用渠道
- 流量调度:某个渠道限流了,分流到其他渠道
举个例子:
用户选择"支付宝"支付 648 元
→ 网关检查支付宝渠道状态:正常
→ 检查用户白名单:该用户是高风险用户,支付宝限额 500
→ 自动路由到微信支付(限额 1000)
→ 用户无感知切换
每个支付渠道的接口都不一样, 渠道适配层就是"翻译官",把统一格式转成各渠道的方言。
统一请求格式:
{
"order_id": "2026030123456789",
"amount": 64800, // 单位:分
"subject": "钻石x648",
"notify_url": "https://api.game.com/pay/callback"
}
↓ 微信适配器转换
微信请求格式:
{
"appid": "wx1234567890",
"mch_id": "1234567890",
"nonce_str": "abc123",
"body": "钻石x648",
"out_trade_no": "2026030123456789",
"total_fee": 64800,
"spbill_create_ip": "192.168.1.1",
"notify_url": "https://api.game.com/pay/callback/wechat",
"sign": "A1B2C3D4..."
}
这些是支付系统的核心能力:
- 风控引擎:判断交易是否安全
- 账户服务:管理用户余额、冻结、解冻
- 对账服务:日终核对账目
- 清算服务:计算分账、手续费
支付数据是公司的命脉,数据层设计要特别讲究:
- 分库分表:按用户ID或订单时间分片
- 读写分离:查询走从库,写入走主库
- 数据归档:历史订单定期归档到冷存储
- 数据备份:异地多活、容灾恢复
🛤️ 支付渠道矩阵:选择的艺术
游戏行业面对的支付渠道,比普通电商复杂得多。
国内渠道
| 渠道 | 特点 | 费率 | 适用场景 |
|---|---|---|---|
| 微信支付 | 覆盖广,体验好 | 0.6% | 所有场景首选 |
| 支付宝 | 余额体系成熟 | 0.6% | 大额支付 |
| 银行直连 | 费率低,但对接成本高 | 0.3%-0.5% | 大型平台 |
| 华为支付 | 华为渠道必备 | 0.6% | 华为应用市场 |
| 小米支付 | 小米渠道必备 | 0.6% | 小米应用商店 |
海外渠道
| 渠道 | 特点 | 费率 | 适用场景 |
|---|---|---|---|
| 苹果内购 (IAP) | 强制抽成30%,但体验无敌 | 30% | iOS 必备 |
| 谷歌支付 | Android 主流,抽成15-30% | 15-30% | Android 海外 |
| PayPal | 欧美主流,退款风险高 | 2.9%+$0.3 | 海外发行 |
| Stripe | 全球化支付,开发友好 | 2.9%+$0.3 | 海外独立站 |
| 本地化渠道 | 各国差异大 | 1%-5% | 区域运营 |
渠道选择的黄金法则
- 单一渠道故障 → 支付不可用 → 收入归零
- 多渠道互备 → 某个渠道挂了自动切换
这就像开车: 有导航、有备胎、有备用路线, 才能确保准时到达目的地。
实战案例:某游戏公司的渠道切换
2025年某次大型活动,微信支付突发故障:
19:00 活动开始,10万玩家同时在线
19:05 微信支付接口超时率飙升到 80%
19:06 支付网关检测到异常,自动切换到支付宝
19:07 支付成功率恢复到 95%
19:30 微信支付恢复正常,自动切回
[配图建议:支付渠道矩阵图,用卡片形式展示各渠道特点和费率]
📋 订单系统:状态机是灵魂
订单系统最核心的设计是状态机。
典型的订单状态
待支付 → 支付中 → 已支付 → 已发货 → 已完成
↓ ↓
已取消 支付失败
↓
已退款
订单表设计详解
一个完整的订单表应该包含这些字段:
CREATE TABLE payment_orders (
-- 基础信息
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(64) NOT NULL COMMENT '商户订单号',
channel_order_no VARCHAR(64) COMMENT '渠道订单号',
-- 用户信息
user_id BIGINT NOT NULL COMMENT '用户ID',
device_id VARCHAR(64) COMMENT '设备ID',
ip_address VARCHAR(45) COMMENT '下单IP',
-- 商品信息
product_id BIGINT NOT NULL COMMENT '商品ID',
product_name VARCHAR(128) COMMENT '商品名称',
quantity INT DEFAULT 1 COMMENT '数量',
-- 金额信息
amount BIGINT NOT NULL COMMENT '订单金额(分)',
real_amount BIGINT COMMENT '实付金额(分)',
currency VARCHAR(8) DEFAULT 'CNY' COMMENT '币种',
fee_amount BIGINT COMMENT '手续费(分)',
-- 渠道信息
channel VARCHAR(32) NOT NULL COMMENT '支付渠道',
channel_extra TEXT COMMENT '渠道扩展信息JSON',
-- 状态信息
status TINYINT NOT NULL DEFAULT 0 COMMENT '订单状态',
sub_status TINYINT COMMENT '子状态',
close_reason VARCHAR(256) COMMENT '关闭原因',
-- 时间信息
expired_at DATETIME COMMENT '订单过期时间',
paid_at DATETIME COMMENT '支付完成时间',
delivered_at DATETIME COMMENT '发货时间',
closed_at DATETIME COMMENT '关闭时间',
-- 回调信息
notify_status TINYINT DEFAULT 0 COMMENT '回调状态',
notify_times INT DEFAULT 0 COMMENT '回调次数',
last_notify_at DATETIME COMMENT '最后回调时间',
-- 审计字段
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- 索引
UNIQUE KEY uk_order_no (order_no),
KEY idx_user_id (user_id),
KEY idx_channel_order (channel, channel_order_no),
KEY idx_status_created (status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
状态流转的三条铁律
状态只能向前走,不能倒退。 "已支付"的订单不能变回"待支付"。
每个状态转换必须有且只有一个触发条件。 不能"支付成功回调"和"主动查询"同时触发状态变更。
实现方式:使用乐观锁
UPDATE payment_orders
SET status = 2, paid_at = NOW()
WHERE order_no = '2026030123456789'
AND status = 1; -- 只有支付中状态才能变成已支付
-- 如果 affected_rows = 0,说明状态已被其他进程修改
每次状态变更都要记录:
- 变更时间
- 变更原因
- 操作来源(回调/查询/人工)
这就像快递追踪: 你能看到包裹在哪个站点、什么时候到的、谁操作的。
状态变更流水表
CREATE TABLE payment_order_logs (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(64) NOT NULL,
from_status TINYINT COMMENT '原状态',
to_status TINYINT NOT NULL COMMENT '目标状态',
action VARCHAR(32) NOT NULL COMMENT '操作类型',
source VARCHAR(32) COMMENT '来源(callback/manual/query)',
extra_info TEXT COMMENT '额外信息JSON',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_order_no (order_no)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
幂等性:重复通知不怕
支付渠道有个特点:喜欢重复通知。
为什么?因为网络不稳定, 渠道宁可多发几次通知,也不愿漏发。
所以你的系统必须支持幂等处理:
第一次收到通知:创建订单,发货
第二次收到通知:发现订单已存在,直接返回成功
第三次收到通知:同上
核心实现方式:
@Transactional
public PayResult handleCallback(PayCallback callback) {
// 1. 查询订单
Order order = orderMapper.selectByOrderNo(callback.getOrderNo());
// 2. 订单不存在,记录异常日志,返回失败
if (order == null) {
log.warn("订单不存在: {}", callback.getOrderNo());
return PayResult.fail("ORDER_NOT_FOUND");
}
// 3. 订单已处理,直接返回成功(幂等)
if (order.getStatus() == OrderStatus.PAID) {
log.info("订单已处理: {}", callback.getOrderNo());
return PayResult.success();
}
// 4. 状态不对,返回失败
if (order.getStatus() != OrderStatus.PAYING) {
log.warn("订单状态异常: {}, status={}",
callback.getOrderNo(), order.getStatus());
return PayResult.fail("INVALID_STATUS");
}
// 5. 更新订单状态
int rows = orderMapper.updateStatus(
order.getOrderNo(),
OrderStatus.PAYING,
OrderStatus.PAID
);
// 6. 乐观锁失败,说明被其他线程处理了
if (rows == 0) {
return PayResult.success(); // 幂等返回
}
// 7. 发货
deliveryService.deliver(order);
return PayResult.success();
}
[配图建议:订单状态机流程图,用箭头清晰展示状态流转]
💰 资金流向:钱去哪了
玩家支付的648元,不是直接进入你的公司账户。
资金流转路径
玩家支付(648元)
→ 支付渠道临时账户(立即)
→ 渠道扣除手续费(648 × (1-0.6%) = 644.11元)
→ 待结算账户(T+1 或 T+N)
→ 平台对公账户(结算日)
→ 游戏公司账户
关键时间节点
- T+0:玩家支付成功,游戏发道具
- T+1:资金从渠道转入待结算
- T+N:完成对账,资金可提现
两个容易混淆的概念
- 结算:渠道把钱打给你(被动,渠道控制)
- 对账:你核对渠道给的钱对不对(主动,你要做)
结算周期通常是T+1或T+N, 但游戏公司需要每日对账, 确保每一笔订单都"账实相符"。
账户体系设计
游戏平台需要维护一套完整的账户体系:
┌─────────────────────────────────────────────────────┐
│ 用户账户 │
│ - 余额账户(充值余额) │
│ - 虚拟币账户(钻石/金币) │
│ - 冻结金额(退款处理中) │
├─────────────────────────────────────────────────────┤
│ 平台账户 │
│ - 待结算账户(渠道待结算资金) │
│ - 收入账户(已结算收入) │
│ - 手续费账户(渠道手续费) │
│ - 保证金账户(渠道保证金) │
├─────────────────────────────────────────────────────┤
│ 商户账户 │
│ - 结算账户(分账收入) │
│ - 待结算(T+N未结算) │
└─────────────────────────────────────────────────────┘
账户流水记录
每一笔资金变动都要记录流水:
CREATE TABLE account_transactions (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
trans_no VARCHAR(64) NOT NULL COMMENT '流水号',
account_id BIGINT NOT NULL COMMENT '账户ID',
account_type TINYINT NOT NULL COMMENT '账户类型',
-- 变动信息
direction TINYINT NOT NULL COMMENT '方向(1入2出)',
amount BIGINT NOT NULL COMMENT '变动金额(分)',
balance_before BIGINT NOT NULL COMMENT '变动前余额',
balance_after BIGINT NOT NULL COMMENT '变动后余额',
-- 关联信息
biz_type VARCHAR(32) NOT NULL COMMENT '业务类型',
biz_no VARCHAR(64) COMMENT '业务单号',
-- 备注
remark VARCHAR(256) COMMENT '备注',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_trans_no (trans_no),
KEY idx_account (account_id, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
[配图建议:资金流转时间轴,标注T+0、T+1、T+N各节点]
🎯 游戏行业的特殊挑战
游戏支付和电商支付有很大不同。
1. 虚拟商品 = 即时交付
电商可以"先付款,后发货"。 游戏要求"付款成功,钻石秒到"。
这意味着:
- 订单系统和游戏服务器要强耦合
- 支付回调要实时处理
- 不能有"支付成功但道具未发"的情况
2. 高并发场景
游戏有以下特点:
- 时间集中:活动开始、新版本上线、节假日
- 瞬间峰值:某款皮肤限时上架,几万人同时抢
- 不可预知:某个主播带货,流量突然爆发
高并发架构设计
面对高并发,支付系统需要从多个维度优化:
用户请求 → CDN → 负载均衡 → API网关 → 服务集群
↓
静态资源缓存
↓
限流(熔断能力我们暂未实现)
- 限流策略:
- 单用户限流:10次/分钟 - 全局限流:10000 QPS
- 熔断策略(我们暂未实现):
- 响应时间 > 3秒,熔断 60 秒
支付请求 → 消息队列 → 支付处理服务 → 回调通知
↓
削峰填谷
使用消息队列(Kafka/RocketMQ)实现:
- 异步解耦:下单和支付处理分离
- 削峰填谷:高峰期请求先入队,慢慢处理
- 失败重试:处理失败自动重试
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ 订单分片1 │ │ 订单分片2 │ │ 订单分片3 │
│ (用户ID%3=0) │ │ (用户ID%3=1) │ │ (用户ID%3=2) │
└──────────────┘ └──────────────┘ └──────────────┘
- 分库分表:按用户ID分片,单表控制在 1000 万行以内
- 读写分离:查询走从库,写入走主库
- 缓存加速:热点订单缓存到 Redis
活动前预估:
- 峰值 QPS:5000
- 持续时间:2小时
- 预计订单量:360万
架构调整:
- 支付服务扩容到 50 个实例
- Redis 集群扩容到 10 个节点
- Kafka 分区数调整到 32
实际表现:
- 峰值 QPS:4800(符合预期)
- 平均响应时间:200ms
- 支付成功率:99.2%
- 零故障
3. 退款复杂
电商退款 = 钱退回去,货拿回来。
游戏退款 = 钱退回去,但玩家可能已经消费了道具。
这就涉及:
- 可追回消费(道具还在)
- 不可追回消费(道具已用)
- 部分退款(消费了一部分)
需要在游戏内建立完整的账户体系和消费流水。
退款处理流程
用户申请退款
→ 查询消费记录
→ 判断是否可追回
├─ 可追回 → 扣除道具 → 退款
└─ 不可追回 → 拒绝退款 or 部分退款
→ 调用渠道退款接口
→ 更新订单状态
→ 通知用户
4. 苹果内购的坑
iOS 必须走苹果内购,这带来几个问题:
- 30%抽成(实际到账只有 453.6 元)
- 回调延迟(苹果服务器有时会抽风)
- 退款通知滞后(玩家退款了,你可能一周后才知道)
- 订阅续费复杂(自动续费的回调逻辑不一样)
苹果内购特殊处理
// 苹果回调可能延迟,需要主动查询
public void checkAppleOrder(String orderNo) {
// 1. 调用苹果 API 查询订单状态
AppleReceipt result = appleService.verifyReceipt(receipt);
// 2. 如果支付成功但订单未更新
if (result.getStatus() == 0 && order.getStatus() != PAID) {
// 3. 更新订单状态
orderService.updateStatus(orderNo, PAID);
// 4. 发货
deliveryService.deliver(order);
}
}
[配图建议:游戏支付vs电商支付对比图,用表格或分栏形式]
🔐 安全与合规:不能踩的红线
支付系统涉及真金白银,安全合规是重中之重。
数据安全
必须加密存储的数据:
- 用户银行卡号
- 用户身份证号
- 支付密钥
- 渠道密钥
加密方式:
AES-256 加密 + 密钥管理服务(KMS)
→ 数据库存储密文
→ 内存中使用明文
→ 日志中脱敏
- 所有接口必须使用 HTTPS
- 敏感接口增加签名验证
- 防重放攻击(timestamp + nonce)
// 接口签名验证
public boolean verifySign(SignRequest request) {
// 1. 检查时间戳(5分钟内有效)
long now = System.currentTimeMillis();
if (Math.abs(now - request.getTimestamp()) > 300000) {
return false;
}
// 2. 检查 nonce(防重放)
if (redisCache.exists("nonce:" + request.getNonce())) {
return false;
}
redisCache.setex("nonce:" + request.getNonce(), 300, "1");
// 3. 验证签名
String sign = generateSign(request);
return sign.equals(request.getSign());
}
风控系统
风控系统是支付系统的"守门人"。
// 风控规则示例
public class RiskRuleEngine {
// 规则1:单用户日累计限额
@Rule(name = "daily_limit", priority = 1)
public RiskResult checkDailyLimit(PayRequest request) {
long dailyTotal = orderMapper.sumDailyAmount(request.getUserId());
if (dailyTotal > 100000) { // 1000元
return RiskResult.reject("超过日累计限额");
}
return RiskResult.pass();
}
// 规则2:设备关联账号数
@Rule(name = "device_accounts", priority = 2)
public RiskResult checkDeviceAccounts(PayRequest request) {
int accountCount = userMapper.countByDevice(request.getDeviceId());
if (accountCount > 5) {
return RiskResult.challenge("需要短信验证");
}
return RiskResult.pass();
}
// 规则3:支付频率
@Rule(name = "pay_frequency", priority = 3)
public RiskResult checkPayFrequency(PayRequest request) {
int recentCount = orderMapper.countRecent(request.getUserId(), 5);
if (recentCount > 10) { // 5分钟内10次
return RiskResult.reject("支付频率异常");
}
return RiskResult.pass();
}
}
支付请求
→ 规则引擎匹配
├─ 通过 → 继续支付
├─ 拒绝 → 返回失败
└─ 挑战 → 短信验证/人脸识别
├─ 验证通过 → 继续支付
└─ 验证失败 → 拒绝
合规要求
如果你的系统涉及银行卡信息,需要符合 PCI-DSS 标准:
- 网络安全要求
- 数据保护要求
- 漏洞管理要求
- 访问控制要求
- 定期安全审计
- 大额交易报告(单笔 > 5万)
- 可疑交易报告
- 用户身份识别 (KYC)
- 交易记录保存(5年以上)
- 中国:《个人信息保护法》
- 欧盟:GDPR
- 美国:CCPA
- 未成年人保护:实名认证、时长限制、充值限额
- 版号要求:无版号游戏不能收费
- 内容审核:充值文案、活动规则
实战案例:某游戏的合规整改
问题发现:
- 未成年人充值投诉增加
- 单笔充值无上限
- 未接入实名认证系统
整改措施:
1. 接入国家实名认证系统
2. 未成年人充值限额:单笔 50 元,月累计 200 元
3. 22:00-08:00 未成年人禁止充值
4. 充值前弹窗提示"理性消费"
整改效果:
- 未成年人相关投诉下降 90%
- 符合监管要求
- 用户满意度提升
[配图建议:风控决策树,展示不同风控策略的分支]
🛡️ 风控:看不见的守门人
支付系统还有一个重要但低调的角色:风控系统。
风控做什么?
- 识别异常交易:同一设备频繁更换账号
- 防范欺诈:使用盗刷的信用卡充值
- 控制风险敞口:单日累计支付限额
- 黑名单管理:已知欺诈账号/IP/设备
风控的位置
风控通常插在两个地方:
- 下单前:判断是否允许创建订单
- 支付前:判断是否允许发起支付
拦截的方式:
- 软拦截:要求短信验证/人脸识别
- 硬拦截:直接拒绝交易
风控是"宁可错杀,不可放过"的典型场景, 但过严的风控会影响用户体验和收入, 需要持续调优。
风控模型进阶
基于固定规则,如:
- 单笔限额
- 日累计限额
- 黑名单
优点:简单、可控 缺点:容易被绕过
基于机器学习模型,给每笔交易打分:
# 风险评分模型示例
def calculate_risk_score(transaction):
score = 0
# 特征1:用户历史行为
score += user_behavior_model.predict(transaction.user_id)
# 特征2:设备风险
score += device_risk_model.predict(transaction.device_id)
# 特征3:时间异常
if is_abnormal_time(transaction.timestamp):
score += 20
# 特征4:金额异常
if is_abnormal_amount(transaction.amount, transaction.user_id):
score += 30
return score
# 风险决策
if score > 80:
return "REJECT"
elif score > 50:
return "CHALLENGE" # 需要验证
else:
return "PASS"
构建用户-设备-IP 关系图,识别团伙欺诈:
用户A ──┐
├── 设备X ──┐
用户B ──┘ ├── IP 192.168.1.1
│
用户C ─── 设备Y ────┘
发现:用户A、B、C 通过共享设备/IP 关联
风险:可能是同一个欺诈团伙
[配图建议:风控决策树,展示不同风控策略的分支]
📊 对账:财务的底线
对账是支付系统最容易被忽视,但最重要的环节。
为什么要对账?
- 渠道可能漏发通知
- 网络可能丢包
- 系统可能bug
- 有人可能作弊
对账就是确保:
对账的三个维度
- 渠道订单列表 vs 本地订单表
- 找出差异订单(多单、少单、金额不一致)
- 渠道日账单 vs 本地日汇总
- 确保总额匹配
- T日的订单,可能在T+1才到账
- 需要做跨日调整
对账流程
每日凌晨 → 拉取渠道账单 → 与本地订单比对
→ 生成差异报告 → 人工/自动处理 → 归档
差异处理:
- 长款(渠道有,我们没有)→ 补单
- 短款(我们有,渠道没有)→ 调查原因
- 金额不一致→ 人工核查
对账系统架构
┌─────────────────────────────────────────────────────┐
│ 对账调度器 │
│ (定时任务 / 渠道优先级 / 失败重试) │
├─────────────────────────────────────────────────────┤
│ 账单采集器 │
│ (微信账单 / 支付宝账单 / 苹果账单 / ...) │
├─────────────────────────────────────────────────────┤
│ 对账引擎 │
│ (订单匹配 / 金额核对 / 差异检测) │
├─────────────────────────────────────────────────────┤
│ 差异处理器 │
│ (自动补单 / 人工审核 / 异常告警) │
├─────────────────────────────────────────────────────┤
│ 报表服务 │
│ (日对账单 / 差异报表 / 财务报表) │
└─────────────────────────────────────────────────────┘
对账数据表设计
-- 对账记录表
CREATE TABLE reconciliation_records (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
reconcile_date DATE NOT NULL COMMENT '对账日期',
channel VARCHAR(32) NOT NULL COMMENT '渠道',
-- 统计信息
local_count INT COMMENT '本地订单数',
local_amount BIGINT COMMENT '本地订单总额',
channel_count INT COMMENT '渠道订单数',
channel_amount BIGINT COMMENT '渠道订单总额',
-- 差异信息
diff_count INT COMMENT '差异数',
diff_amount BIGINT COMMENT '差异金额',
-- 状态
status TINYINT COMMENT '状态(0待处理1处理中2已完成)',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
finished_at DATETIME COMMENT '完成时间',
UNIQUE KEY uk_date_channel (reconcile_date, channel)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 对账差异表
CREATE TABLE reconciliation_diffs (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
record_id BIGINT NOT NULL COMMENT '对账记录ID',
order_no VARCHAR(64) COMMENT '订单号',
diff_type TINYINT NOT NULL COMMENT '差异类型',
local_amount BIGINT COMMENT '本地金额',
channel_amount BIGINT COMMENT '渠道金额',
status TINYINT COMMENT '处理状态',
handle_result VARCHAR(256) COMMENT '处理结果',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
KEY idx_record (record_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
[配图建议:对账流程图,用泳道图展示系统和人工操作]
🔧 可观测性:系统健康的晴雨表
支付系统必须具备完善的可观测性,才能及时发现问题。
监控指标
| 指标 | 含义 | 告警阈值 |
|---|---|---|
| 支付成功率 | 支付成功/支付请求 | < 95% |
| 支付平均耗时 | 从请求到成功 | > 3s |
| 回调延迟 | 支付成功到回调 | > 30s |
| 订单发货延迟 | 回调到发货 | > 10s |
| 指标 | 含义 | 告警阈值 |
|---|---|---|
| QPS | 每秒请求数 | 根据容量规划 |
| 错误率 | 4xx/5xx 比例 | > 1% |
| 响应时间 P99 | 99%请求响应时间 | > 2s |
| 渠道可用性 | 渠道接口成功率 | < 99% |
告警系统
┌─────────────────────────────────────────────────────┐
│ 监控数据采集 │
│ (Prometheus / OpenTelemetry / ...) │
├─────────────────────────────────────────────────────┤
│ 指标聚合分析 │
│ (业务指标 / 技术指标 / 渠道指标) │
├─────────────────────────────────────────────────────┤
│ 告警规则引擎 │
│ (阈值告警 / 趋势告警 / 智能告警) │
├─────────────────────────────────────────────────────┤
│ 告警通知渠道 │
│ (短信 / 邮件 / 飞书 / 电话) │
└─────────────────────────────────────────────────────┘
日志规范
支付系统日志必须包含:
{
"timestamp": "2026-03-01T23:30:00.123+08:00",
"trace_id": "abc123def456",
"span_id": "span001",
"level": "INFO",
"service": "payment-service",
"action": "pay_callback",
"channel": "wechat",
"order_no": "2026030123456789",
"user_id": 12345,
"amount": 64800,
"result": "SUCCESS",
"duration_ms": 156,
"extra": {
"device_id": "device123",
"ip": "192.168.1.100"
}
}
链路追踪
使用分布式链路追踪(如 Jaeger、Zipkin):
- 追踪一笔支付从请求到完成的完整链路
- 快速定位性能瓶颈
- 分析跨服务调用关系
🎯 要点总结
- 支付系统是双流系统
- 渠道要互备,不要单点
- 订单状态机是核心,幂等性是底线
- 游戏支付要求实时到账
- 对账是财务安全的最后一道防线
- 安全合规是红线
- 可观测性决定故障恢复速度
📝 写在最后
支付系统看起来是技术问题, 但本质是信任问题:
- 用户信任你,才愿意把钱给你
- 渠道信任你,才愿意帮你收钱
- 公司信任你,才敢让你碰钱
技术再复杂,别丢了这份信任。
💬 评论 (0)