适配器模式:对现实的妥协
单一供应商的脆弱性
单一支付提供商就是单点故障。
当应用依赖某个供应商的 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_subscriptions 和 paypro_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。每个验证脚本都检查不存在的记录。整个规划阶段都基于错误前提运行。
正确的顺序:
- 直接查询生产数据库
- 记录实际数据结构
- 验证前端匹配数据库
- 然后才规划迁移
不要:
- 读前端代码
- 假设它反映数据库
- 规划迁移
- 发现一切都错了
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-signatureheader 的 HMAC 签名 - PayPro:用
PayPro-Signatureheader 的自定义 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
统一积分系统的设计实践
从多套积分到单一积分池的架构演进,以及背后的原子性、一致性设计
Stripe Webhook中的防御性编程
三个Bug揭示的真相:假设是代码中最危险的东西。API返回类型、环境配置、变量作用域——每个看似合理的假设都可能导致客户损失。
双重验证:Stripe生产模式的防御性切换
从测试到生产不是更换API keys,而是建立一套双重验证系统。每一步都在两个环境中验证,确保真实支付不会因假设而失败。
引入懒加载模式
懒加载不是优化技巧,而是关于时机的选择。何时创建,决定了系统的效率和复杂度。
缺失值的级联效应
一个NULL值如何在调用链中传播,最终导致错误的错误消息。理解防御层的设计,在失败传播前拦截。
多厂商 AI 调度:统一混乱的供应商生态
当你依赖第三方 AI 服务时,单点故障是最大的风险。多厂商调度不只是技术架构,更是对不确定性的应对策略。
分布式 Workers 的解耦设计
通过微服务架构和队列系统,实现高可用的 AI 任务处理。从单体到分布式,每个设计决策都是对复杂度的权衡。
队列生产者实例的工厂函数
工厂函数不是设计模式的炫技,而是对重复的拒绝,对集中管理的追求,对变化的准备
Studio 系统架构:从状态机到端到端流程
深入 Studio 系统的状态管理中心、组件协调机制和 AI 生成的完整数据流,理解前后端集成的设计逻辑