PAYATHON 2026

如何设计端到端 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"
}

至少应区分:

  • APPROVED
  • INSUFFICIENT_FUNDS
  • INVALID_PAYMENT_TOKEN
  • EXPIRED_ACCOUNT
  • DUPLICATE_REQUEST
  • IDEMPOTENCY_CONFLICT
  • ISSUER_TIMEOUT
  • PROCESSING_ERROR

业务拒绝通常表示 API 已成功接收并处理了请求,不应全部映射为服务器内部错误。

Encryption 与 security 的合理范围

传输安全应使用成熟的 TLS 实现,不要自行设计加密算法或安全协议。除 TLS 外,还应考虑:

  • Merchant 身份认证和权限隔离。
  • 校验请求大小、字段长度和数值范围。
  • 对每个 merchant 实施速率限制。
  • 不在源代码中保存密钥和数据库密码。
  • 在日志中屏蔽 token、密码和认证头。
  • 分离管理接口与交易接口。
  • 审计记录只能追加,普通用户不能修改。
  • 使用参数化 SQL,防止 SQL injection。
  • 错误响应不能暴露堆栈、SQL 或内部网络信息。
  • 对管理操作使用更强的身份认证。
  • 为连接、issuer 调用和数据库操作设置超时。

如果还想研究消息完整性,可以在 TLS 之外增加带时间戳和 nonce 的请求签名。不过,这只能作为扩展功能,不能替代 TLS。

如何处理 PCI DSS

PCI DSS 不只是“使用加密”或“给数据库设置密码”。它还涉及网络隔离、访问控制、漏洞管理、日志监控、安全策略和持续测试等要求。

项目不应声称自己“通过 PCI compliance”。更准确的处理方式是:

  1. 不接收或保存真实支付卡数据。
  2. 使用虚构 token 和测试账户。
  3. 根据项目当时选定的 PCI DSS 文档整理控制项。
  4. 建立需求映射表,标明每项控制是已实现、部分模拟、超出范围还是不适用。
  5. 在报告中说明,真正的合规涉及人员、流程、基础设施和运营环境,不能只靠检查源代码确定。

PCI DSS 会持续更新,因此报告必须注明所参考文档的具体版本和获取日期,不能把后来版本的要求当作 2011 至 2012 年已经存在的规定。

测试比功能数量更重要

这个项目的技术价值主要来自可以验证的可靠性。建议准备以下测试:

  • 让 100 个并发请求争用同一账户,验证系统不会超额授权。
  • 多次提交同一个幂等键,验证系统只产生一笔交易。
  • 使用相同幂等键提交不同金额,验证请求会被拒绝。
  • 模拟 issuer 响应延迟或超时。
  • 在授权过程中重启 Processor。
  • 模拟数据库事务死锁或连接失败。
  • Merchant 收不到响应后重新查询。
  • 测试非法金额、负数金额、错误币种和过长字段。
  • 测试无效凭据、越权查询和速率限制。
  • 检查日志,确认其中没有泄露敏感数据。

负载测试结果应注明硬件、并发数、请求模型、数据库配置和测试时长。如果只报告“每秒处理多少笔”而不交代测试环境,结果的参考价值很有限。

四个月的实施计划

第一阶段:范围和协议

用一到两周完成:

  • 系统边界和威胁模型。
  • API 契约。
  • 数据模型。
  • 交易状态机。
  • 定义成功、拒绝、超时和重复请求的处理方式。

第二阶段:最小可运行系统

用三到四周完成:

  • Merchant 测试客户端。
  • Processor 网络服务。
  • Simulated issuer。
  • 数据库持久化。
  • 基本授权和查询接口。

第三阶段:可靠性

用三到四周完成:

  • 并发控制。
  • 幂等机制。
  • 数据库事务。
  • 超时和重试。
  • 捕获、撤销与退款。
  • 崩溃恢复。

第四阶段:安全和评估

用三周左右完成:

  • TLS 和 Merchant 认证。
  • 输入校验、速率限制和日志脱敏。
  • 威胁分析。
  • PCI DSS 控制项研究。
  • 自动化测试和负载测试。

最后留出两周修复缺陷、整理演示数据和撰写报告。计划不要排得太满,因为并发和故障恢复问题往往比预期更耗时。

最容易失控的功能

下面这些内容适合放入“未来工作”,不宜列为必须交付的功能:

  • 接入真实银行或银行卡网络。
  • 保存真实卡号、CVV 或个人金融数据。
  • 完整的清算和跨机构结算。
  • 多币种汇率和外汇风险。
  • 复杂的反欺诈模型。
  • Chargeback 和争议处理。
  • 高可用多数据中心部署。
  • 正式的 PCI DSS 认证。
  • 自行实现密码算法或 TLS。

最终演示不必追求功能数量。一个能在并发、重复请求、超时和进程故障下保持交易一致性的原型,比接口众多却可能重复扣款的系统更有技术价值。

备注:内容仅供参考。