PAYATHON 2026

在 Payment Gateways 之间迁移支付数据的最佳实践

支付老李

结论

应用程序不应自行导出、保存或转移信用卡号。更稳妥的做法是让原支付网关和新支付网关共同完成一次受控的“网关到网关”迁移:原网关在符合 PCI DSS 要求的环境中解密持卡人数据,通过双方约定的加密通道传给新网关,再由新网关生成新的客户标识和支付方式 Token。

迁移的数据可分为三类:

  • 客户、合同和账单信息:可通过普通 API 或批量文件迁移,但仍需加密并遵守隐私要求。
  • 信用卡信息:必须通过网关之间的 PCI 合规流程迁移。Token 通常不能跨网关直接使用。
  • 交易记录:通常保留在原网关,也可以迁入自有数据仓库。不要假设新网关能够导入并继续处理历史交易。

周期性扣款应分批迁移,让新旧系统并行运行,并保留回滚能力,以免一次性切换造成大面积扣款失败。

为什么不能直接迁移 Token

大多数支付网关返回的 Token 只在自己的 Vault 中有效。例如:

old_gateway_token = "card_abc123"

这个值通常只是原网关内部某条加密卡记录的引用。将它传给新网关,并不能取得对应的信用卡:

Old Gateway Vault:
card_abc123 -> 4111 **** **** 1111

New Gateway Vault:
card_abc123 -> 不存在

所以迁移并不是“复制 Token”,实际流程是:

  1. 原网关从自己的 Vault 中取得卡数据。
  2. 原网关通过受控渠道将数据发给新网关。
  3. 新网关将卡数据写入自己的 Vault。
  4. 新网关生成新的 Customer ID 和 Payment Method Token。
  5. 应用程序只保存新旧标识的映射关系。

如果原网关不支持数据导出,或合同条款禁止导出,通常只能请客户重新录入支付方式。

迁移前先确认双方能力

编写迁移程序前,应先向两个网关确认:

  • 原网关是否提供符合 PCI 要求的数据可携带服务或 Vault 导出服务。
  • 新网关支持哪些导入格式和加密方式。
  • 双方是否支持网关到网关的直接传输。
  • 银行卡、账单地址、有效期和持卡人姓名能否迁移。
  • 是否支持 Network Token、账户更新服务或银行卡自动更新服务。
  • 原有的商户发起交易资格和周期扣款依据能否保留。
  • 导入后是否需要客户重新认证或授权。
  • 历史退款、拒付和争议是否仍要通过原网关处理。
  • 关闭原网关账户后,历史报表和争议处理功能还能保留多久。

这些能力没有统一标准。最终方案应以两个网关书面确认的迁移流程为准。

建立清晰的数据边界

每类数据都应明确迁移方式,以及迁移完成后的权威来源。

数据类型推荐处理方式迁移后的权威来源
客户资料API 或加密批量文件应用数据库
账单地址API 或加密批量文件应用数据库或新网关
合同与套餐在新系统重新建立并映射应用数据库
信用卡 PAN网关到网关传输新网关 Vault
CVV/CVC不迁移、不保存无
支付 Token重新生成并建立映射新网关
历史交易保留原记录或进入数据仓库原网关及内部账务系统
退款与拒付按原交易所属网关处理对应交易的原网关

合同、订阅和账单规则最好由应用自己的数据库管理,不要完全依赖支付网关。以后更换提供商时,只需替换支付执行层,不必重建整个订阅模型。

PCI 合规要点

不让应用接触原始卡号

理想的数据流如下:

Old Gateway
    |
    | encrypted gateway-to-gateway transfer
    v
New Gateway Vault
    |
    | new payment_method_token
    v
Application Database

应用程序不应充当原始 PAN 的中转站。否则,应用服务器、迁移脚本、队列、日志、备份和开发环境都可能被纳入更大的 PCI DSS 评估范围。

即使传输时间很短,也不能忽略 PCI 要求。数据只存在于内存或临时文件中,同样可能构成持卡人数据处理。

CVV 不能迁移

CVV、CVC 或 CID 属于敏感认证数据。即使客户同意,也不应在授权完成后保存,因此正常情况下不会有可合法迁移的 CVV 数据。

新网关能否在没有 CVV 的情况下继续执行商户发起的周期扣款,取决于收单机构、卡组织规则、交易标记和新网关的实现,必须提前确认。

PAN 的日志和临时文件

如果在特殊情况下,自有基础设施确实必须接触 PAN,应请 PCI 专业人员审查设计,并至少采取以下控制措施:

  • 使用端到端加密,并对密钥实行分离管理。
  • 落实最小权限和双人审批。
  • 使用受控的执行主机,并进行网络隔离。
  • 禁止将卡数据写入应用日志、错误跟踪和审计消息正文。
  • 禁止卡数据进入普通消息队列、缓存和数据库备份。
  • 临时数据到期后必须以可验证的方式删除。
  • 保留完整的访问审计,但审计内容不得包含卡数据。
  • 明确迁移人员、供应商和服务提供商之间的责任边界。

自建中转方案风险很高。只要两个网关可以直接传输,就不应采用这种方案。

推荐的实施步骤

1. 盘点并固定数据模型

为客户、订阅、支付方式和交易建立稳定的内部 ID,不要将网关 ID 直接用作业务主键。

例如:

CREATE TABLE payment_method_migrations (
    id BIGINT PRIMARY KEY,
    customer_id BIGINT NOT NULL,
    old_gateway_customer_id VARCHAR(255),
    old_gateway_payment_method_id VARCHAR(255),
    new_gateway_customer_id VARCHAR(255),
    new_gateway_payment_method_id VARCHAR(255),
    status VARCHAR(32) NOT NULL,
    migrated_at TIMESTAMP NULL,
    last_error_code VARCHAR(128) NULL,
    UNIQUE (old_gateway_payment_method_id)
);

表中只保存网关 Token 和迁移状态,不保存 PAN 或 CVV。

2. 导出非敏感业务数据

先迁移客户资料、账单地址、套餐、币种、账期、优惠规则和下次扣款时间。导出文件仍可能包含个人信息,因此也要加密,并设置访问控制和完整性校验。

迁移前后可通过记录数和摘要进行核对:

customer_count_before == customer_count_after
subscription_count_before == subscription_count_after
total_outstanding_amount_before == total_outstanding_amount_after

金额应使用最小货币单位或定点数,不能使用二进制浮点数。

3. 由网关迁移支付方式

向原网关提交迁移范围,由原网关和新网关共同确定传输窗口、文件格式、加密密钥、失败重试方式和销毁证明。

迁移完成后,新网关通常会返回类似的映射文件:

old_customer_id,old_payment_method_id,new_customer_id,new_payment_method_id,status
cus_old_101,pm_old_501,cus_new_801,pm_new_901,success
cus_old_102,pm_old_502,,,failed

实际字段名和格式由网关决定,不能假设所有提供商都支持这种结构。

导入映射时必须保证幂等:

def apply_mapping(row, repository):
    existing = repository.find_by_old_payment_method_id(
        row["old_payment_method_id"]
    )

    if existing and existing.status == "completed":
        return

    if row["status"] != "success":
        repository.mark_failed(
            old_payment_method_id=row["old_payment_method_id"],
            error_code="gateway_import_failed",
        )
        return

    repository.upsert_mapping(
        old_customer_id=row["old_customer_id"],
        old_payment_method_id=row["old_payment_method_id"],
        new_customer_id=row["new_customer_id"],
        new_payment_method_id=row["new_payment_method_id"],
        status="completed",
    )

程序日志只能记录内部 ID、Token、批次号和错误代码,不得记录原始导入行或完整请求体。

4. 重建周期扣款关系

新网关中的订阅对象通常不能沿用原网关的订阅 ID,需要根据内部合同数据重新建立:

  • 客户与新 Payment Method Token 的关联。
  • 商品或价格标识映射。
  • 扣款金额和币种。
  • 扣款周期。
  • 下次扣款时间。
  • 试用期、折扣和税务设置。
  • 失败重试和催收策略。
  • 取消时间和合同状态。

必须避免新旧网关对同一账期重复扣款。可以为每个订阅设置明确的路由状态:

old_active
migration_pending
new_active
migration_failed
manual_review

只有在新支付方式完成导入、订阅完成重建和校验后,才能将状态从 migration_pending 切换为 new_active。

5. 使用支付路由层切换

不要让所有业务代码直接调用某一家网关。可以用统一接口隔离不同网关的实现:

interface PaymentGateway {
  charge(request: ChargeRequest): Promise<ChargeResult>;
  refund(request: RefundRequest): Promise<RefundResult>;
}

async function chargeSubscription(subscription: Subscription) {
  const gateway =
    subscription.paymentRoute === "new"
      ? newGateway
      : oldGateway;

  return gateway.charge({
    idempotencyKey: `invoice:${subscription.currentInvoiceId}`,
    customerId: subscription.gatewayCustomerId,
    paymentMethodId: subscription.gatewayPaymentMethodId,
    amountMinor: subscription.amountMinor,
    currency: subscription.currency,
  });
}

idempotencyKey 的参数名和有效期因网关而异。业务系统仍要维护自己的唯一扣款约束,不能完全依赖网关的幂等机制。

6. 分批切换

先迁移内部测试账户,再选择少量低风险客户。观察一个完整扣款周期后,再逐步扩大批次。每批至少要检查:

  • Token 导入成功率。
  • 新订阅创建成功率。
  • 授权成功率和拒绝原因分布。
  • 重复扣款数量。
  • Webhook 丢失或重复情况。
  • 退款和对账结果。
  • 失败客户的重新绑卡流程。

不能仅凭一笔测试支付判断迁移成功。测试范围还应包括周期扣款、退款、部分退款、拒付、过期卡更新、Webhook 重放和网络超时。

交易记录怎么处理

新网关通常无法将旧交易转换为自己的原生交易。即使它能够导入报表数据,也不代表可以对旧交易执行退款或处理拒付。

建议保留统一的内部交易表:

CREATE TABLE payment_transactions (
    id BIGINT PRIMARY KEY,
    order_id BIGINT NOT NULL,
    gateway VARCHAR(32) NOT NULL,
    gateway_transaction_id VARCHAR(255) NOT NULL,
    amount_minor BIGINT NOT NULL,
    currency CHAR(3) NOT NULL,
    status VARCHAR(32) NOT NULL,
    created_at TIMESTAMP NOT NULL,
    UNIQUE (gateway, gateway_transaction_id)
);

退款时,根据原交易选择对应的网关:

def refund(transaction, amount_minor):
    if transaction.gateway == "old_gateway":
        return old_gateway.refund(
            transaction.gateway_transaction_id,
            amount_minor,
        )

    return new_gateway.refund(
        transaction.gateway_transaction_id,
        amount_minor,
    )

因此,通常需要保留原网关一段时间,直到退款期、拒付期和财务审计需求全部结束。具体保留期限应结合合同、收单机构规则、适用法律和企业留存政策确定。

Webhook 与一致性处理

切换期间要同时接收两个网关的 Webhook,并使用网关名称和事件 ID 去重:

CREATE TABLE processed_webhook_events (
    gateway VARCHAR(32) NOT NULL,
    event_id VARCHAR(255) NOT NULL,
    processed_at TIMESTAMP NOT NULL,
    PRIMARY KEY (gateway, event_id)
);

处理时应先验证签名,再检查事件是否已经处理。不能只根据 HTTP 请求是否到达判断交易结果。发生超时后,应通过查询 API 核实状态,不能直接再次扣款。

Webhook 的签名算法、时间戳容差和重试规则由各网关定义,应按照相应的官方文档实现。

回滚方案

每个迁移批次都要设置明确的回滚条件,例如:

  • 新 Token 映射缺失。
  • 扣款成功率明显偏离基线。
  • 对账金额不一致。
  • Webhook 持续积压。
  • 发生重复扣款。
  • 新网关无法正确处理退款或周期扣款标记。

回滚不意味着删除新网关中的数据。更安全的处理方式是暂停后续批次,让尚未切换的订阅继续走原网关,并逐条核实已经切换的订阅。对于新网关中状态不明的扣款,在确认结果前,不得自动通过原网关重试。

其他注意事项

支付数据迁移除了 PCI DSS,还可能涉及隐私法、数据跨境、合同约束和数据留存要求。支付负责人、信息安全、法务、财务和客服都应参与方案确认。

还要提前准备客户重新绑卡流程。即使网关支持批量迁移,部分卡片仍可能因为数据不完整、卡种限制、地区限制或授权规则而迁移失败。对于这些客户,应发送安全的托管支付页面链接,让卡数据直接进入新网关,不能通过邮件或客服系统收集卡号。

迁移结束后,应取得迁移报告,核对成功、失败和遗漏的记录,并要求相关方按约定销毁临时文件。只有在退款、拒付、审计和回滚窗口全部结束后,才能决定何时删除原网关中的支付方式或关闭账户。

备注:内容仅供参考。