# Payment Kit 上线后运行评估

> **[2026-04-24 更新]** 本文档总述 / §1.1 / §3.1 成本表 / §4 Phase 1 预期 建立在"账号已在 Workers Paid $5 套餐"的错误假设上，实际账号仍是免费套餐；`exceededResources` 从 13% 涨到 42%；Phase 1 8 项中只完成了 item 7。最新事实、部署记录（Plan 8 + Plan 13）和完整勘误对照见 **`2026-04-24-queue-ops-followup.md`**。

**撰写**：2026-04-20（2026-04-21 补入三笔真实订单时间轴分析）
**对象**：`staging-aigne-hub-payment-kit`、`payment-kit-staging`
**数据**：CF Analytics 7 天 + D1 直查 events 表 + 三笔真实订单 + 代码审查 + 业界调研

---

## 总述

### 一句话结论

**现在不迁，花 2-3 周做"Phase 1 优化"（下面解释到底是啥）；12-24 个月内大概率要迁到混合部署，迁不迁取决于 Payment Kit 未来是否继续大改功能。**

### 什么是 Phase 1？（就是下面这 8 件具体的事）

| # | 动作 | 位置 | 省多少 |
|---|---|---|---:|
| 1 | 汇率查询加 30s 缓存 | `libs/reference-cache.ts` 新增 `getCachedExchangeRate` | 2-5s/笔 |
| 2 | 合并 checkout-sessions 重复读 | `routes/checkout-sessions.ts:170, 256` 同一个 PaymentMethod 读了两次 | 1-2s/笔 |
| 3 | Invoice 生成阶段 DB batch 化 | `queues/invoice.ts` + `routes/checkout-sessions.ts` 连续 await 改 `db.batch()` | 3-4s/笔 |
| 4 | 加 3 条复合索引 | `meter_events(customer_id, status, livemode)` / `credit_transactions(source)` / `events(object_id, created_at)` | Dashboard 快 10× |
| 5 | 启用数据归档 cron | `crons/index.ts:47-59` 打开 `retentionConfig.enabled`，90 天保留 | 防数据膨胀 |
| 6 | 批量 update 加空数组短路 | `queues/credit-consume.ts:933, 1487` 等 `Op.in` 处加 `if (!ids.length) return;` | 对冲 D1 $5K/10s 事故 |
| 7 | `cronInstance.runAll` 过滤 | shim 层按 cron 表达式过滤不到期 job | cron 空转读降 60-80% |
| 8 | Dashboard 预聚合表 | 新增 `meter_events_daily_summary` + 小时 cron，`/meter-events/stats` 先读聚合 | Dashboard 从秒到毫秒 |

**2-3 周 1 人可完成，全部是零迁移、可回滚的改动，账单仍 $5-10/月**。

**另外两件建议一起做**（不在 Phase 1 里但更见效）：
- **改前端（1 周）**：18-46 秒黑盒期加进度文字（"等待钱包签名" / "上链中" / "扣款中"）+ checkout page 首屏 2 秒出来不要等 subscription 创建完
- **挂 Logpush 到 R2（1 天）**：长尾请求根因现在看不清，挂了才能复盘

### Phase 1 后预期效果

| 指标 | 现在 | Phase 1 后 | 迁混合后（参考）|
|---|---:|---:|---:|
| 服务端时间（订阅流程）| 22-25 秒 | **15-18 秒** | 3-5 秒 |
| 服务端时间（一次性付款）| 13 秒 | **~8 秒** | 1-2 秒 |
| 支付端到端（链上快）| ~46 秒 | **~38-40 秒** | ~28 秒 |
| Dashboard stats 响应 | ~1-5 秒 | **< 500ms** | < 200ms |
| `exceededResources` 比率 | 13% | **< 5%** | < 1% |

---

### 不同用户规模的月资费

**假设**：每活跃用户月均 3-5 次支付 + 100-1000 次 meter event（AI API 调用计费）

| 月活用户 | 月支付笔数 | 月 meter events | **CF 账单** | 主要成本项 |
|---:|---:|---:|---:|---|
| 当前（~10 笔/周）| ~40 | <1K | **$5** | 底价，资源都在免费额度内 |
| 100 用户 | ~400 | ~50K | **$5** | 一切免费 |
| 1,000 用户 | ~4K | ~500K | **$5-8** | Queue ops 接近 1M 免费上限 |
| 10,000 用户 | ~40K | ~5M | **$10-20** | Queue ops 超额 + D1 读增加 |
| 100,000 用户 | ~400K | ~50M | **$50-100** | D1 行读超过 25B 免费额度 |

**前提**：必须完成 Phase 1（索引 + 归档 + 预聚合）。**如果不做 Phase 1，在 1 万用户量级 D1 读就会因统计扫表突破免费额度，账单可能飙到 $100-200**。

**更现实的视角：按接入方数量看**（Payment Kit 是内部基础设施，驱动成本的是"多少个服务用它"）：

| 接入方数量 | 典型场景 | 月 meter events | CF 账单 |
|---:|---|---:|---:|
| 1-2 | 当前（AIGNE Hub）| <1K | $5 |
| 3-5 | 6-12 个月后 | 15K-75K | $5 |
| 5-10 | 12-24 个月 | 150K-600K | $5-10 |
| 10+ | 生态全面接入 | ~3M | $10-20 |

**结论**：在内部基础设施的定位下，**CF 账单在未来 1-2 年都不会超过 $20/月**，真正该关心的是成本之外的维度（性能、体感、运维债）——这才是决策重点。

---

### 为什么要这么建议（数据支撑）

#### 三笔真实订单的耗时证据

| 订单 | 类型 | 总时长 | 服务端 | 用户+链上 |
|---|---|---:|---:|---:|
| Case 1 | 订阅（个人 staging）| 76.3s | **24.7s** | 51.6s |
| Case 2 | 订阅（个人 staging）| 46.0s | **22.0s** | 24.0s |
| Case 3 | 一次性 + 促销码（ArcBlock staging）| 56.6s | **~13s** | ~44s |

**关键发现**：
- **订阅流程服务端固定 22-25 秒**（前两笔差不到 3 秒）
- **一次性付款服务端 ~13 秒**（少了订阅创建的 ~10 秒）
- 总时长差别主要来自用户+链上时间（不可控）

#### 为什么服务端这么慢？

代码里**每笔支付要串行做 80-130 次 D1 调用**（Case 2 的 events 表 24 条事件 × 每条 3-6 次 DB 操作）。每次 D1 查询 **165ms**（Worker 在外部，D1 在 HKG，跨区网络费），累加就是 15-20 秒。不是查询慢，是查太多次。

举例：`routes/checkout-sessions.ts:170-364` 仅订单创建路径就有 9+ 次串行 DB 读，其中 PaymentMethod 在 `:170` 和 `:256` 被读了两次。**加索引没用，要减调用次数**（去重 + batch + 并发），这就是 Phase 1 第 1-3 件事在做的。

#### 钱不是瓶颈，持续漏水才是

- 账单 $5/月，未来 1-2 年都不是瓶颈
- 真正吃时间的不是 shim 层维护（shim 写完就跑），而是**CF 专属问题排查**：
  - **tail latency 根因难查**：P99 wall time 138 秒，不知道在等啥
  - **`exceededResources` 每天 100-300 次**：定位难
  - **本地复现难**：`wrangler dev --local` 跟线上有差异
  - **部署验证久**：每改一次全量部署 + 手工测
- 加起来 **10-15% FTE**（每月 3-6 天）。最近例子：04-18 `scheduledTime` cron bug 修复后错误率从 23% 降到 13%

---

### 后期需不需要迁？**大概率要，这是功能迭代强度决定的**

**需要迁的理由（大概率发生）**：

1. **新功能碰壁必然发生**：D1 不支持跨 Worker 事务、Worker 30 秒 CPU 上限、单库 10GB 上限。只要还在加功能，早晚撞上其中一个。比如"批量退款 + 补偿记账 + 通知"这种跨表事务，D1 没法干净地做
2. **CF 本身会演进**：D1、Queue、Workers 的 API 都在变，每次变动 shim 都得跟——这是**永远在发生**的
3. **代码栈跟主仓分叉**：主仓每次升级都要测 CF 兼容，越拖分叉越大，最后变成维护"CF 版 Payment Kit"这个 fork
4. **Service Binding 红利会稀释**：一旦某个关键依赖（比如 blocklet-service）迁离 CF，零网络费优势立刻消失

**不需要迁的理由（需刻意选择）**：

1. Payment Kit 进入稳定维护期，功能定型
2. 业务真的不增长（一年后还是一周 10 笔支付）
3. 10-15% FTE 持续漏水团队能长期接受

**关键判断**：这是"功能迭代强度"的问题，不是"用户量"的问题。即便 1 万用户代码不大改，Phase 1 就够；即便 100 用户只要还在频繁加新付费模型，迁移就是必然。

**触发迁移的 5 个信号（6 个月内观察，任一触发就启动混合部署）**：
- Phase 1 做完 `exceededResources` 仍 > 5%
- 新功能连续 2 次因 D1/Worker 限制被阻
- CF 专属问题占工时 > 20%
- Blocklet 生态里有依赖服务迁离 CF
- 月记账量 > 500K 或接入方 > 5 个

**诚实预测**：12-24 个月内至少触发其中 1-2 条的概率 > 70%。

---

### 五方案评分

| 方案 | 评分 | 一句话 |
|---|:---:|---|
| 1. 不动 | 62 | Dashboard 会越来越慢，非预期增长账单飙到 $200+ |
| **2. Phase 1 优化** | **83** ⭐ | **2-3 周、零风险、现在的选择** |
| 3. CF + Hyperdrive + PG | 58 | 花 $25-50 换 RTT，shim 债和 Worker 限制都没解决 |
| 4. 混合部署（CF 入口 + 独立 Node + PG）| 80 | **大概率 12-24 个月要走到这里** |
| 5. 完全单体 | 48 | 失去 Service Binding，重建认证链，不值 |

---

## 一、真实数据

### 1.1 7 天 Analytics

| 指标 | 值 |
|---|---:|
| 总请求 | 31,559（~4.5K/天）|
| 错误 `exceededResources` | 7,234（23% → 04-18 修复后 13%）|
| 成功支付 | 10 笔 |
| 月账单推算 | $5（各资源都在免费额度内）|
| Wall P50 / P99 | 340ms / **138 秒** |
| CPU P99 | 183ms（99% wall 在等 I/O）|

### 1.2 三笔订单时间轴对比

| 阶段 | Case 1 订阅 (76s) | Case 2 订阅 (46s) | Case 3 一次性+促销 (57s) |
|---|---:|---:|---:|
| 事件数 | 24 | 24 | 17 |
| 用户看单 + 输入促销 | — | — | ~19s（Stage A）|
| 创建订阅 | 13.7s | 11.3s | ❌ 无订阅 |
| **黑盒（链上+用户签名）**| **45.9s** | **18.5s** | **24.9s** |
| Invoice 生成 | 7.8s | 7.6s | ❌ 流程不同 |
| 建 PI + 记录促销 | — | — | 6.6s |
| 链上扣款 | 5.7s | 5.5s | — |
| 收尾 | 3.2s | 3.1s | 6.4s |
| **服务端合计** | **24.7s** | **22.0s** | **~13s** |

**结论**：
- **订阅流程服务端固定 22-25 秒**（前两笔几乎相同）
- **一次性付款服务端 ~13 秒**（少了订阅创建状态机的 ~10 秒）
- 总时长差别全在黑盒（用户签名 + 链上），服务端是结构化的、按业务类型有固定基线

### 1.3 D1 RTT 是架构副作用，改不掉

```
served_by_region: "APAC", colo: "HKG"
sql_duration_ms:  1.71   ← SQL 实际执行
wall time:      169.00   ← 总耗时
→ 167.3 ms 是 Worker ↔ D1 网络往返
```

`placement: smart` 把 Worker 拉到靠近 Service Binding 目标的位置，D1 留在 HKG。

### 1.4 一笔支付为什么要 80-130 次 DB 调用

Case 2 的 `events` 表 24 条事件，每条事件对应一次状态变化，**每次状态变化要 3-6 次 DB 操作**（读校验 + 写主表 + 写关联 + audit）。24 × 4-5 ≈ 100 次。

举例：`routes/checkout-sessions.ts:170-364` 仅订单创建路径就有 **9+ 次串行 DB 读**（PaymentMethod、PaymentCurrency、Price、CheckoutSession.findAll、PromotionCode、Discount 等），其中 PaymentMethod 在 `:170` 和 `:256` 被读了两次。

**不是查询慢，是查太多次**。加索引解决不了，**需要减少调用次数**（去重 + batch + 并发）。

---

## 二、业界对标（简版）

**CF 上有支付功能的案例，核心都不在 CF**：
- [PhonePe](https://venturebeat.com/data-infrastructure/digital-payments-leader-partners-with-cloudflare-to-accelerate-secure-monthly-mobile-payments)：4.35 亿用户，CF 只做边缘层安全 + 流量卸载
- [QuickConv](https://dev.to/cc_quickconv_ff5b94a1d015/i-built-an-image-conversion-saas-on-almost-0month-heres-the-full-stack-4o2i) / [cClip](https://blog.cloudflare.com/developer-spotlight-tejas-metha-cclip/) / [Flarekit](https://github.com/mockkey/flarekit)：CF + D1 存用户/订阅元数据，**计费交给 Stripe**
- [Stripeflare](https://github.com/janwilmake/stripeflare)：唯一"CF 上做虚拟钱包"的项目，用 Durable Objects 不是 D1，原型级

**专业计费产品没一个跑 CF**：
- [Lago](https://github.com/getlago/lago)：Ruby + Postgres + Sidekiq
- [OpenMeter](https://github.com/openmeterio/openmeter)：Go + ClickHouse + K8s

**D1 真实故障前例**：[$5,000 / 10 秒烧光事故](https://www.ofsecman.io/post/postmortem-5-000-incident-in-10-seconds-due-to-cloudflare-d1)，开发者漏写 WHERE 的 UPDATE 触发全表写，D1 无速率/账单护栏。Payment Kit 代码里有类似 `{[Op.in]: ids}` 路径，ids 为空或缺失时有同款风险。

---

## 三、成本对比

### 3.1 账面月成本（按规模）

| 场景 | 现状 | Phase 1 | Hyperdrive+PG | 混合 | 单体 |
|---|---:|---:|---:|---:|---:|
| 当前（30 记账/天）| $5 | $5 | $30 | $25 | $40 |
| 12 个月（5K/天） | $8 | $8 | $35 | $35 | $60 |
| 24 个月（30K/天）| $20 | $15 | $50 | $60 | $100 |
| 非预期 10× 暴涨 | **$200+** | $40 | $80 | $120 | $180 |

### 3.2 隐性成本（持续消耗的工程时间）

| 类型 | 现状 / Phase 1 / Hyperdrive | 混合 / 单体 |
|---|---:|---:|
| 调试 CF 专属 bug（tail latency、exceededResources、本地复现）| 每月 3-6 天 | 几乎 0 |
| shim 层跟随主仓升级 | 每半年 1-2 天 | 0（代码回归标准栈）|

**$2,500/月"工程时间"不是夸张**，但**主要来自"出 bug 后难排查"**，不是"shim 要常改"。迁到混合部署后这部分会接近消失。

---

## 四、Phase 1 具体动作（这是什么意思）

**Phase 1 = 8 件具体代码改动，2-3 周内可完成**。每一条都带文件位置和预期效果：

### 4.1 汇率查询加缓存（省 2-5s/笔）

**问题**：Case 1 和 Case 2 每笔支付都触发 2-3 次 `exchange_rate_provider.updated`，每次去外部 API 取汇率 2-3 秒。

**改动**：
- 复用 `api/src/libs/reference-cache.ts` 现有的 `getCachedMeter`、`getCachedCurrency` 模式
- 新增 `getCachedExchangeRate(providerId, ttlMs = 30000)`
- 把 `ExchangeRateProvider.findByPk` + 外部 API 调用的地方改走缓存（约 5-10 个调用点）

### 4.2 checkout 重复读合并（省 1-2s/笔）

**问题**：`routes/checkout-sessions.ts:170` 和 `:256` 各读一次 `PaymentMethod.findByPk(method.id)`。Case 2 Stage A 里类似的重复读至少 3 处。

**改动**：
- 在 route 入口一次性读完所有需要的 reference data（customer / price / payment_method / currency）
- 后面函数接受参数，不再自己查

### 4.3 Invoice 生成 batch 化（省 3-4s/笔）

**问题**：Case 2 Stage C 7.6 秒做了 8 个事件，每个事件 3-6 次 DB 调用。多次 `subscription.update` + `invoice.update` + `payment_intent.create` 串行。

**改动**：
- 定位 `queues/invoice.ts` 和 `routes/checkout-sessions.ts` 的连续 await 链
- 用 `db.batch([q1, q2, q3])` 合并无依赖的写入
- 需要依赖的保持串行

### 4.4 三条复合索引（Dashboard 快 10×）

**问题**：Dashboard 的 `/meter-events/stats`、`/overdue-summary` 扫全表。`retryFailedEventsForCustomer` 每次充值扫 `meter_events` 全表。

**改动**：新增 migration 加索引：
- `meter_events(customer_id, status, livemode)`
- `credit_transactions(source)`
- `events(object_id, created_at)`

### 4.5 启用数据归档 cron（防止数据膨胀）

**问题**：`meter_events` 和 `events` 表无限增长，半年后 Dashboard 会越来越慢、D1 接近 10GB 上限。

**改动**：
- 代码已在 `crons/index.ts:47-59`，`retentionConfig.enabled` 目前是 false
- 配置打开 + 设 90 天保留窗口

### 4.6 所有批量 update/delete 加空数组短路（对冲 $5K/10s 事故）

**问题**：`queues/credit-consume.ts:933, 1487` 等处 `MeterEvent.update({...}, {where: {id: {[Op.in]: ids}}})` 如果 `ids` 为空，Sequelize 可能生成无 WHERE 的 UPDATE。

**改动**：
```js
if (!ids.length) return;
await Model.update(payload, { where: { id: { [Op.in]: ids } } });
```
- 审计所有 `Op.in` + `update/destroy` 的组合
- 加 D1 daily usage 告警（CF dashboard 上设置）

### 4.7 `cronInstance.runAll` 过滤（降 cron 空转）

**问题**：每分钟触发 `scheduled` 事件都 `runAll()` 遍历 16 个 job。job 内部会自己判断时间，但仍会做一次 D1 读。基础消耗 ~40M rows/day 与用户量无关。

**改动**：
- 定位 `cloudflare/shims/cron/` 或业务 cron 的 `runAll` 实现
- 进入 runAll 时按 cron 表达式先过滤掉"本分钟不到时间的 job"
- 每分钟只跑到期的，降 cron 基础读 60-80%

### 4.8 Dashboard 预聚合表（Dashboard 从几秒到毫秒）

**问题**：`/api/meter-events/stats` 按 day 聚合原始 `meter_events` 表，数据一多就慢。

**改动**：
- 新增 `meter_events_daily_summary` 表（customer_id, event_name, date, event_count, total_value）
- 新增 cron：每小时聚合昨日数据写入
- `meter-events/stats` 路由先读聚合表，缺的部分回源原表

---

## 五、Phase 1 预期效果

| 指标 | 现在 | Phase 1 后 | 迁混合后（对比参考）|
|---|---:|---:|---:|
| 服务端时间 | 22-25 秒 | **15-18 秒** | 3-5 秒 |
| 支付端到端（链上快）| ~46 秒 | **~38-40 秒** | ~28 秒 |
| 支付端到端（链上慢）| ~76 秒 | **~68-70 秒** | ~55 秒 |
| Dashboard stats 响应 | ~1-5 秒 | **< 500ms** | < 200ms |
| `exceededResources` 比率 | 13% | **< 5%** | < 1% |
| 月账单 | $5 | $5-10 | $25-60 |

**投入**：2-3 周 / 1 人
**风险**：低（全部是零迁移、可回滚的改动）

---

## 附录

### A. 数据采集方法

- **CF GraphQL Analytics**：`workersInvocationsAdaptive`、`d1AnalyticsAdaptiveGroups`、`queueMessageOperationsAdaptiveGroups`、`kvOperationsAdaptiveGroups`（token 在 `blocklets/core/.env.development.local`）
- **D1 直查**：`GET /api/__dev__/d1/query?sql=...`
- **D1 Benchmark**：`GET /api/__dev__/d1-benchmark`
- **代码审查**：`worker.ts`、`crons/index.ts`、`queues/credit-consume.ts`、`routes/checkout-sessions.ts`

### B. 三笔订单样本

| Session | 类型 | 账号 | 总时长 | 服务端 | 黑盒 | 链上扣款 |
|---|---|---|---:|---:|---:|---:|
| `cs_2E5uh1...` | 订阅 | 个人 staging | 76.3s | 24.7s | 45.9s | 5.7s |
| `cs_QhTSzc2...` | 订阅 | 个人 staging | 46.0s | 22.0s | 18.5s | 5.5s |
| `cs_YAWq4B...` | 一次性+促销 | ArcBlock staging | 56.6s | ~13s | 24.9s | ~4.8s |

### C. 相关文档

- [`cf-cost-analysis-2026-04-20.md`](./cf-cost-analysis-2026-04-20.md) — 成本专题
- [`cf-queues-ops-alert-analysis.md`](./cf-queues-ops-alert-analysis.md) — Queue ops RCA
- [`pr-1341-review.md`](./pr-1341-review.md) — 近期 PR review
