如何设计端到端 payment processing system?
结论
这个项目可以在四个月左右完成,前提是将目标限定为“支付处理流程模拟器”,而不是完整的商业支付平台。
作为毕业项目,下面这个范围既有足够的技术含量,也比较可控:
- 实现 merchant client 和 payment processor server。
- 用模拟的 issuer service 代替真实银行。
- 支持并发交易、超时、重试和幂等处理。
- 实现授权、撤销、捕获、退款等核心状态转换。
- 通过 TLS、访问控制和审计日志保护通信与数据。
- 研究 PCI DSS,并说明原型与真实合规系统之间的差距。
项目不应存储真实卡号或处理真实资金,也不能把“余额是否充足”当作支付模型的全部内容。如果还要实现清算、结算、争议处理、真实银行卡网络接入或完整的 PCI DSS 认证,四个月通常不够。
建议的系统边界
系统可以拆成三个独立部分:
Merchant Client
|
| TLS + authenticated API
v
Payment Processor
|
| internal authorization request
v
Simulated Issuer
|
v
Test Account Database
Merchant Client
Merchant 端负责:
- 创建订单。
- 生成唯一的
merchantReference。 - 提交授权请求。
- 显示批准、拒绝或处理中状态。
- 查询结果未知的交易。
- 对失败请求进行受控重试。
Merchant 端不应直接修改账户余额,也不能只根据 HTTP 状态码判断支付是否成功。
Payment Processor
Payment Processor 是项目的主体,建议实现以下功能:
- 网络连接与请求解析。
- Merchant 身份认证。
- 输入校验。
- 幂等控制。
- 并发处理。
- 交易状态机。
- 将请求路由到模拟 issuer。
- 超时与错误处理。
- 审计日志和监控指标。
Simulated Issuer
不要把 issuer 逻辑散落在 processor 内部。即使两者运行在同一台机器上,也应通过清晰的接口隔离。
模拟 issuer 可以检查:
- 测试卡是否存在,当前是否可用。
- 卡片是否过期。
- 金额和币种是否合法。
- 可用余额或信用额度是否充足。
- 请求是否触发预设的风控规则。
- 是否需要模拟超时或系统故障。
这种设计保留了真实系统中的角色边界,也便于测试各种异常场景。
不要只设计一张余额表
简单的 account_number + balance 表可以用于第一阶段原型,但无法正确处理并发授权。两个请求可能同时读到相同的余额,造成重复消费。
一个更合理的最小数据模型如下:
CREATE TABLE test_accounts (
account_id BIGINT PRIMARY KEY,
token VARCHAR(64) NOT NULL UNIQUE,
currency CHAR(3) NOT NULL,
available_amount DECIMAL(18, 2) NOT NULL,
held_amount DECIMAL(18, 2) NOT NULL DEFAULT 0,
status VARCHAR(16) NOT NULL,
version BIGINT NOT NULL DEFAULT 0
);
CREATE TABLE transactions (
transaction_id VARCHAR(36) PRIMARY KEY,
merchant_id VARCHAR(64) NOT NULL,
merchant_reference VARCHAR(64) NOT NULL,
account_id BIGINT NOT NULL,
transaction_type VARCHAR(16) NOT NULL,
amount DECIMAL(18, 2) NOT NULL,
currency CHAR(3) NOT NULL,
status VARCHAR(16) NOT NULL,
response_code VARCHAR(16),
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL,
UNIQUE (merchant_id, merchant_reference)
);
测试账户应保存随机生成的 token,不能使用真实银行卡号。数据库中的金额应采用定点小数类型,应用代码则应使用 decimal 类型或以最小货币单位保存的整数,不能使用二进制浮点数。
例如:
正确:1099 cents
正确:Decimal("10.99")
不合适:double amount = 10.99
先定义交易状态机
支付处理并不是简单的“扣款成功或失败”。项目至少应实现以下状态:
RECEIVED
|
v
AUTHORIZING
|------------------|
v v
AUTHORIZED DECLINED
|
v
CAPTURED
|
v
REFUNDED
此外,还应保留 FAILED、REVERSED、UNKNOWN 等异常状态。
建议支持四种操作:
AUTHORIZE:检查并冻结可用额度。CAPTURE:确认扣除已经冻结的金额。VOID:在捕获前释放冻结金额。REFUND:在捕获后退回全部或部分金额。
这种设计比直接修改余额更接近真实支付流程,也更能体现事务处理和状态一致性方面的能力。
并发处理的关键点
原子更新账户额度
授权时,应在数据库事务中锁定账户记录,以免并发请求造成超额授权。下面是与具体数据库无关的示意代码:
BEGIN;
SELECT available_amount, held_amount, status
FROM test_accounts
WHERE account_id = :account_id
FOR UPDATE;
-- 应用程序检查账户状态、币种和可用金额。
UPDATE test_accounts
SET available_amount = available_amount - :amount,
held_amount = held_amount + :amount,
version = version + 1
WHERE account_id = :account_id;
INSERT INTO transactions (
transaction_id,
merchant_id,
merchant_reference,
account_id,
transaction_type,
amount,
currency,
status,
response_code,
created_at,
updated_at
) VALUES (
:transaction_id,
:merchant_id,
:merchant_reference,
:account_id,
'AUTHORIZE',
:amount,
:currency,
'AUTHORIZED',
'APPROVED',
CURRENT_TIMESTAMP,
CURRENT_TIMESTAMP
);
COMMIT;
生产代码还需要处理死锁、事务回滚、数据库连接中断和唯一约束冲突。
幂等处理
支付请求可能因网络超时而被重复发送。Merchant 必须为同一次业务操作生成固定的幂等键,例如:
POST /payments/authorizations HTTP/1.1
Authorization: Bearer test-merchant-token
Idempotency-Key: order-18432-auth-1
Content-Type: application/json
{
"paymentToken": "tok_test_7f92c1",
"amount": 1099,
"currency": "EUR"
}
Processor 应为 (merchantId, idempotencyKey) 建立唯一约束。相同键和相同请求再次到达时,应返回第一次处理的结果。如果幂等键相同,但金额等请求内容不同,则应拒绝该请求。
幂等机制不能只依赖进程内缓存,否则服务器重启后记录就会丢失。
不要把超时等同于失败
Merchant 超时只能说明“没有收到结果”,不能证明交易没有执行。安全的处理方式是使用原来的幂等键重试,或者查询交易状态:
GET /payments/by-reference/order-18432-auth-1 HTTP/1.1
Authorization: Bearer test-merchant-token
这类结果不确定的情况是支付系统中很值得展示的技术问题。
服务端处理流程示例
下面的伪代码主要说明处理顺序,不依赖具体语言或框架:
function authorize(request, merchant):
validateSchema(request)
validateAmountAndCurrency(request)
existing = findByIdempotencyKey(
merchant.id,
request.idempotencyKey
)
if existing exists:
if existing.requestHash != hash(request):
return IDEMPOTENCY_CONFLICT
return existing.response
beginTransaction()
try:
createTransaction(status = RECEIVED)
result = simulatedIssuer.authorize(
token = request.paymentToken,
amount = request.amount,
currency = request.currency
)
if result.approved:
updateTransaction(
status = AUTHORIZED,
responseCode = APPROVED
)
else:
updateTransaction(
status = DECLINED,
responseCode = result.code
)
commit()
return storedResponse
catch error:
rollback()
recordOperationalError(error)
return PROCESSING_ERROR
实际实现时,不应在持有数据库锁的情况下执行耗时的远程调用。如果 simulated issuer 作为独立服务运行,可以先创建交易,再调用 issuer,最后通过一个短事务更新最终状态。采用这种方式后,还需要明确处理中状态、重复回调和服务崩溃后的恢复策略。
API 响应应区分业务拒绝和系统错误
API 应使用稳定的响应码,不要只用自由文本描述错误原因:
{
"transactionId": "82d53cb6-3b92-4cd3-a0b7-5afdd2f6c7d1",
"status": "DECLINED",
"responseCode": "INSUFFICIENT_FUNDS",
"merchantReference": "order-18432-auth-1"
}
至少应区分:
APPROVEDINSUFFICIENT_FUNDSINVALID_PAYMENT_TOKENEXPIRED_ACCOUNTDUPLICATE_REQUESTIDEMPOTENCY_CONFLICTISSUER_TIMEOUTPROCESSING_ERROR
业务拒绝通常表示 API 已成功接收并处理了请求,不应全部映射为服务器内部错误。
Encryption 与 security 的合理范围
传输安全应使用成熟的 TLS 实现,不要自行设计加密算法或安全协议。除 TLS 外,还应考虑:
- Merchant 身份认证和权限隔离。
- 校验请求大小、字段长度和数值范围。
- 对每个 merchant 实施速率限制。
- 不在源代码中保存密钥和数据库密码。
- 在日志中屏蔽 token、密码和认证头。
- 分离管理接口与交易接口。
- 审计记录只能追加,普通用户不能修改。
- 使用参数化 SQL,防止 SQL injection。
- 错误响应不能暴露堆栈、SQL 或内部网络信息。
- 对管理操作使用更强的身份认证。
- 为连接、issuer 调用和数据库操作设置超时。
如果还想研究消息完整性,可以在 TLS 之外增加带时间戳和 nonce 的请求签名。不过,这只能作为扩展功能,不能替代 TLS。
如何处理 PCI DSS
PCI DSS 不只是“使用加密”或“给数据库设置密码”。它还涉及网络隔离、访问控制、漏洞管理、日志监控、安全策略和持续测试等要求。
项目不应声称自己“通过 PCI compliance”。更准确的处理方式是:
- 不接收或保存真实支付卡数据。
- 使用虚构 token 和测试账户。
- 根据项目当时选定的 PCI DSS 文档整理控制项。
- 建立需求映射表,标明每项控制是已实现、部分模拟、超出范围还是不适用。
- 在报告中说明,真正的合规涉及人员、流程、基础设施和运营环境,不能只靠检查源代码确定。
PCI DSS 会持续更新,因此报告必须注明所参考文档的具体版本和获取日期,不能把后来版本的要求当作 2011 至 2012 年已经存在的规定。
测试比功能数量更重要
这个项目的技术价值主要来自可以验证的可靠性。建议准备以下测试:
- 让 100 个并发请求争用同一账户,验证系统不会超额授权。
- 多次提交同一个幂等键,验证系统只产生一笔交易。
- 使用相同幂等键提交不同金额,验证请求会被拒绝。
- 模拟 issuer 响应延迟或超时。
- 在授权过程中重启 Processor。
- 模拟数据库事务死锁或连接失败。
- Merchant 收不到响应后重新查询。
- 测试非法金额、负数金额、错误币种和过长字段。
- 测试无效凭据、越权查询和速率限制。
- 检查日志,确认其中没有泄露敏感数据。
负载测试结果应注明硬件、并发数、请求模型、数据库配置和测试时长。如果只报告“每秒处理多少笔”而不交代测试环境,结果的参考价值很有限。
四个月的实施计划
第一阶段:范围和协议
用一到两周完成:
- 系统边界和威胁模型。
- API 契约。
- 数据模型。
- 交易状态机。
- 定义成功、拒绝、超时和重复请求的处理方式。
第二阶段:最小可运行系统
用三到四周完成:
- Merchant 测试客户端。
- Processor 网络服务。
- Simulated issuer。
- 数据库持久化。
- 基本授权和查询接口。
第三阶段:可靠性
用三到四周完成:
- 并发控制。
- 幂等机制。
- 数据库事务。
- 超时和重试。
- 捕获、撤销与退款。
- 崩溃恢复。
第四阶段:安全和评估
用三周左右完成:
- TLS 和 Merchant 认证。
- 输入校验、速率限制和日志脱敏。
- 威胁分析。
- PCI DSS 控制项研究。
- 自动化测试和负载测试。
最后留出两周修复缺陷、整理演示数据和撰写报告。计划不要排得太满,因为并发和故障恢复问题往往比预期更耗时。
最容易失控的功能
下面这些内容适合放入“未来工作”,不宜列为必须交付的功能:
- 接入真实银行或银行卡网络。
- 保存真实卡号、CVV 或个人金融数据。
- 完整的清算和跨机构结算。
- 多币种汇率和外汇风险。
- 复杂的反欺诈模型。
- Chargeback 和争议处理。
- 高可用多数据中心部署。
- 正式的 PCI DSS 认证。
- 自行实现密码算法或 TLS。
最终演示不必追求功能数量。一个能在并发、重复请求、超时和进程故障下保持交易一致性的原型,比接口众多却可能重复扣款的系统更有技术价值。
备注:内容仅供参考。
