在 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”,实际流程是:
- 原网关从自己的 Vault 中取得卡数据。
- 原网关通过受控渠道将数据发给新网关。
- 新网关将卡数据写入自己的 Vault。
- 新网关生成新的 Customer ID 和 Payment Method Token。
- 应用程序只保存新旧标识的映射关系。
如果原网关不支持数据导出,或合同条款禁止导出,通常只能请客户重新录入支付方式。
迁移前先确认双方能力
编写迁移程序前,应先向两个网关确认:
- 原网关是否提供符合 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,还可能涉及隐私法、数据跨境、合同约束和数据留存要求。支付负责人、信息安全、法务、财务和客服都应参与方案确认。
还要提前准备客户重新绑卡流程。即使网关支持批量迁移,部分卡片仍可能因为数据不完整、卡种限制、地区限制或授权规则而迁移失败。对于这些客户,应发送安全的托管支付页面链接,让卡数据直接进入新网关,不能通过邮件或客服系统收集卡号。
迁移结束后,应取得迁移报告,核对成功、失败和遗漏的记录,并要求相关方按约定销毁临时文件。只有在退款、拒付、审计和回滚窗口全部结束后,才能决定何时删除原网关中的支付方式或关闭账户。
备注:内容仅供参考。
