适配器模式:对现实的妥协

4 min read
Zekari
系统架构支付集成设计模式数据库设计防御性编程

单一供应商的脆弱性

单一支付提供商就是单点故障。

当应用依赖某个供应商的 API,你就交出了控制权。他们的停机是你的停机。他们的涨价是你的成本重构。他们的 IP 限制是你的架构约束。

这不是理论。PayPro Global 要求固定 IP 白名单才能调用 API。Cloudflare Workers——运行在动态边缘网络上——没有固定 IP。结果是:你可以接收 webhook,但不能调用他们的退款 API,不能编程管理订阅。

Stripe 没有这种限制。API key 认证在任何 IP 下都工作。所有功能都可编程。

问题不是要不要支持多个提供商。而是当某个提供商的约束与你的基础设施冲突时,架构能否存活。

发现:IP 约束

问题在实施规划时暴露。

PayPro 的自动退款功能需要调用他们的 API。但他们的 API 需要 IP 白名单。Workers 没有固定 IP。结论立刻明确:通过 PayPro 实现自动退款是不可能的。

这不是代码 bug。这是供应商要求与基础设施现实的根本性不匹配。

关于"退款"的困惑源于混淆两个独立的流程:

AI 任务退款 当 AI 生成失败时,系统自动将积分退回用户账户余额。这是数据库操作——不涉及支付提供商。已通过 process_refund RPC 函数实现。

支付退款 当用户请求退款或降级订阅时,真钱必须通过支付提供商返还。PayPro 需要手动在后台操作(因为 IP 限制)。Stripe 支持基于 API 的自动退款。

现有系统完美处理 AI 任务退款。挑战是跨多个提供商的支付退款。

约束强制一个选择:接受 PayPro 的限制,或者添加 Stripe 来实现可编程操作。

添加 Stripe 不是为了功能。而是为了逃离约束。

架构:管理而非消除

多个提供商不会消除约束——只是分散约束。

PayPro 的 IP 限制依然存在。Stripe 的复杂度增加。代码库现在必须处理两种认证方法、两种 API 格式、两种 webhook 签名、两种响应结构。

适配器模式的出现不是优雅设计,而是务实的必要性。

模式做了什么:

  • 用统一接口包装供应商差异
  • 将供应商特定代码隔离到单个文件
  • 让业务逻辑保持供应商无关
  • 使添加新供应商变成实现一个类的事

模式没做什么:

  • 让供应商行为一致
  • 隐藏他们的根本性限制
  • 消除管理多个集成的复杂度

💡 Click the maximize icon to view in fullscreen

接口定义方法:createCheckoutUrl()handleWebhook()refundTransaction()。每个适配器以不同方式实现这些方法。当 PayPro 不支持自动退款时,它的实现返回 {success: false, requires_manual: true}

这不是为了优雅的抽象。这是现实拒绝配合时的生存抽象。

数据库:复用而非重建

诱惑是为每个提供商创建独立的表。stripe_subscriptionspaypro_subscriptions。干净分离,没有冲突。

但分离有代价。积分管理的重复逻辑。用户交易历史的独立查询。调试时要看两个不同的地方。

替代方案:扩展现有表以容纳多个提供商。

关键变更:

-- plans 表:允许两者之一或两者都有
ALTER TABLE plans
  ALTER COLUMN paypro_product_id DROP NOT NULL;

ADD COLUMN stripe_price_id TEXT;

ADD CONSTRAINT check_at_least_one_provider
  CHECK (paypro_product_id IS NOT NULL OR stripe_price_id IS NOT NULL);

-- transactions 表:追踪哪个提供商
RENAME COLUMN paypro_order_id TO provider_order_id;

ADD COLUMN payment_provider TEXT NOT NULL DEFAULT 'paypro';

ADD CONSTRAINT transactions_provider_order_unique
  UNIQUE (payment_provider, provider_order_id);

结果:95%+ 的表复用率。transactions 表服务两个提供商。查询方式相同——只需按 payment_provider 过滤。

实施期间,对实际数据库的检查揭示了意外情况:transactions 表已经有 payment_provider 列。

这不在原始文档中。前端代码没有引用它。但直接查询数据库架构——显示它存在,且所有 5 条现有交易都填充了 'PayPro'。

教训: 前端代码会撒谎。数据库架构才是真相。

初始规划假设需要创建这个字段。现实只需要设置默认值和添加 NOT NULL 约束。

这个发现节省了迁移复杂度,揭示了更深层的问题:基于代码检查而非数据检查的文档会错过根本真相。

原则:在规划架构变更前查询数据库。代码可能过时。数据不会撒谎。

数据才是根本真相

最有启发的失败来自信任前端代码。

前端组件 PricingModal.tsx 包含硬编码的 PayPro 产品 ID:115325、115326、115327 等。文档基于这些假设编写,以为它们是生产环境的实际产品 ID。

然后有人运行了 SELECT * FROM plans

 id            | paypro_product_id | name           | price_cents
---------------|-------------------|----------------|-------------
 one-time-50   | 112337           | One-time 50    | 300
 monthly-100   | 113728           | Basic Monthly  | 990
 monthly-300   | 113730           | Pro Monthly    | 1990
 annual-1200   | 113731           | Basic Yearly   | 10098
 annual-3600   | 113732           | Pro Yearly     | 20298

前端的 ID 一个都不存在。数据库包含 5 个套餐,ID 完全不同。前端显示的是废弃数据。

技术文档中的每个代码示例都使用了错误的 ID。每个验证脚本都检查不存在的记录。整个规划阶段都基于错误前提运行。

正确的顺序:

  1. 直接查询生产数据库
  2. 记录实际数据结构
  3. 验证前端匹配数据库
  4. 然后才规划迁移

不要:

  1. 读前端代码
  2. 假设它反映数据库
  3. 规划迁移
  4. 发现一切都错了

30 秒的 SQL 查询防止数小时的浪费规划。数据库是唯一的真相来源。代码是解释。

这不是关于责备。这是关于方法。规划系统变更时,从数据开始,不是代码。代码频繁变化。数据缓慢积累但永远不会对当前状态撒谎。

防御性设计:能力检查

不同提供商有不同能力。系统必须在运行时检查能力,而不是在编译时假设。

const adapter = PaymentFactory.getAdapter(provider, env);
const capabilities = adapter.getCapabilities();

if (capabilities.advanced?.autoRefund) {
  // Stripe 路径:通过 API 自动退款
  const result = await adapter.refundTransaction(orderId, amount);
} else {
  // PayPro 路径:创建手动退款任务
  await createManualRefundTask(orderId, amount);
  await sendTelegramAlert(env, '需要手动退款', `订单: ${orderId}`);
}

代码不假设所有提供商支持所有功能。它会询问。如果不支持自动退款,就回退到手动处理。

这个模式扩展到每个操作:

订阅升级:

  • Stripe:API 调用,带 proration
  • PayPro:不支持(通知管理员)

订阅取消:

  • Stripe:API 调用(立即或周期结束)
  • PayPro:手动后台操作

Webhook 验证:

  • Stripe:用 stripe-signature header 的 HMAC 签名
  • PayPro:用 PayPro-Signature header 的自定义 HMAC

适配器接口不承诺所有方法对所有提供商都工作。它承诺一种一致的方式来询问"你能做这个吗?"并优雅地处理"不能"。

勘误表:现实纠正假设

技术文档包含一份 20 页的勘误表。

不是因为实施错了。而是因为规划做了假设,现实证伪了它们。

假设 1: PayPro 产品 ID 是 115325-115335 现实: 实际 ID 是 112337、113728-113732

假设 2: transactions.payment_provider 需要创建 现实: 字段已存在,只需设默认值

假设 3: 存在九个订阅层级 现实: 存在五个层级

假设 4: 迁移会很复杂 现实: 大部分表已支持多提供商(只需重命名)

勘误表不是失败。它是系统在面对事实时自我纠正。

原始文档中的每个假设基于代码检查都是合理的。每个修正都来自数据检查。教训:不查询现实就做的假设是昂贵的。

合理的问题。既然数据库可查询,为什么要做任何假设?

答案揭示了更深层的模式:人类为叙事优化,而不是验证。

写文档感觉高效。查询数据库感觉像延迟。冲动是基于看起来合理的东西"向前推进",而不是"放慢速度"去验证实际存在的东西。

这种对行动而非验证的偏见导致了大多数规划失败。解决方案不是消除假设——而是通过系统性验证检查点早期捕获它们。

实用检查点: 在最终确定任何迁移计划前运行:

# 连接生产数据库
psql $DATABASE_URL

# 查询实际架构
\d+ plans
\d+ transactions
\d+ subscriptions

# 查询实际数据
SELECT * FROM plans LIMIT 10;
SELECT DISTINCT payment_provider FROM transactions;

10 分钟的查询防止 10 小时的返工。

迁移:双重测试

迁移脚本包含不寻常的东西:全面验证。

不只是"ALTER TABLE 成功了吗?"而是"迁移后数据状态正确吗?"

DO $
DECLARE
  expected_ids INT[] := ARRAY[112337, 113728, 113730, 113731, 113732];
  actual_ids INT[];
  plan_count INT;
BEGIN
  -- 验证套餐数量
  SELECT COUNT(*) INTO plan_count FROM public.plans;
  IF plan_count != 5 THEN
    RAISE EXCEPTION '预期 5 个套餐,发现 %', plan_count;
  END IF;

  -- 验证产品 ID 匹配预期
  SELECT ARRAY_AGG(paypro_product_id ORDER BY paypro_product_id)
  INTO actual_ids
  FROM public.plans;

  IF actual_ids != expected_ids THEN
    RAISE EXCEPTION '产品 ID 不匹配。预期: %, 实际: %',
      expected_ids, actual_ids;
  END IF;

  RAISE NOTICE '✅ 迁移验证通过';
END $;

这不是偏执。这是承认架构变更可以在技术上成功但在逻辑上失败。

迁移可能成功添加 stripe_price_id 列但意外丢失要求至少一个提供商 ID 的约束。SQL 执行没有错误。数据变得无效。

验证捕获这个。如果约束不存在,验证会大声失败。

妥协,而非优雅

适配器模式解决了问题。但代价是真实的。

之前: 一个 API、一个 webhook 处理器、一组凭证 之后: 两个 API、两个 webhook 处理器、两组凭证、每个操作的能力检查、不支持功能的手动回退路径

代码库更复杂。测试矩阵翻倍。文档必须解释两个不同的支付流程。

这不优雅。这是必要的。

当供应商不配合——当一个要求固定 IP 而基础设施不提供时——架构变成妥协。适配器模式不消除供应商差异。它包容差异。

模式奏效不是因为它美,而是因为它承认现实:供应商永远不会统一,基础设施永远不会满足所有要求,代码必须在这种情况下工作。

相关的多供应商管理模式,参见 multi-vendor-ai-orchestration。关于 webhook 安全模式,参见 defensive-programming-stripe-webhook


好的架构不是寻找完美解决方案。而是管理不完美的现实。适配器模式在这里的出现不是设计目标,而是当供应商限制与基础设施约束冲突时唯一可行的路径。

模式不优雅。它务实。而在生产系统中,务实每次都胜过优雅。

Related Posts

Articles you might also find interesting

统一积分系统的设计实践

2 min read

从多套积分到单一积分池的架构演进,以及背后的原子性、一致性设计

系统架构数据库设计
Read More

Stripe Webhook中的防御性编程

2 min read

三个Bug揭示的真相:假设是代码中最危险的东西。API返回类型、环境配置、变量作用域——每个看似合理的假设都可能导致客户损失。

Web开发系统设计
Read More

双重验证:Stripe生产模式的防御性切换

7 min read

从测试到生产不是更换API keys,而是建立一套双重验证系统。每一步都在两个环境中验证,确保真实支付不会因假设而失败。

系统设计Stripe
Read More

引入懒加载模式

1 min read

懒加载不是优化技巧,而是关于时机的选择。何时创建,决定了系统的效率和复杂度。

软件设计性能优化
Read More

缺失值的级联效应

3 min read

一个NULL值如何在调用链中传播,最终导致错误的错误消息。理解防御层的设计,在失败传播前拦截。

系统设计防御性编程
Read More

多厂商 AI 调度:统一混乱的供应商生态

3 min read

当你依赖第三方 AI 服务时,单点故障是最大的风险。多厂商调度不只是技术架构,更是对不确定性的应对策略。

Purikura 项目系统架构
Read More

分布式 Workers 的解耦设计

3 min read

通过微服务架构和队列系统,实现高可用的 AI 任务处理。从单体到分布式,每个设计决策都是对复杂度的权衡。

Purikura 项目系统架构
Read More

队列生产者实例的工厂函数

4 min read

工厂函数不是设计模式的炫技,而是对重复的拒绝,对集中管理的追求,对变化的准备

系统设计设计模式
Read More

Studio 系统架构:从状态机到端到端流程

3 min read

深入 Studio 系统的状态管理中心、组件协调机制和 AI 生成的完整数据流,理解前后端集成的设计逻辑

Purikura 项目系统架构
Read More