payment-kit 1.27.1 → 1.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (184) hide show
  1. package/__blocklet__.js +37 -0
  2. package/api/ocap-1.30-subpath-shims.d.ts +35 -0
  3. package/api/src/crons/index.ts +10 -0
  4. package/api/src/crons/metering-subscription-detection.ts +12 -14
  5. package/api/src/crons/overdue-detection.ts +51 -74
  6. package/api/src/integrations/arcblock/nft.ts +6 -2
  7. package/api/src/integrations/arcblock/stake.ts +3 -2
  8. package/api/src/integrations/arcblock/token.ts +4 -4
  9. package/api/src/integrations/blocklet/notification.ts +1 -1
  10. package/api/src/integrations/ethereum/tx.ts +29 -0
  11. package/api/src/integrations/stripe/handlers/invoice.ts +70 -53
  12. package/api/src/integrations/stripe/handlers/payment-intent.ts +8 -1
  13. package/api/src/integrations/stripe/resource.ts +8 -0
  14. package/api/src/libs/audit.ts +32 -16
  15. package/api/src/libs/auth.ts +49 -2
  16. package/api/src/libs/chain-error.ts +31 -0
  17. package/api/src/libs/error.ts +15 -0
  18. package/api/src/libs/event.ts +42 -1
  19. package/api/src/libs/invoice.ts +69 -34
  20. package/api/src/libs/notification/template/customer-auto-recharge-daily-limit-exceeded.ts +1 -3
  21. package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +1 -3
  22. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +1 -3
  23. package/api/src/libs/notification/template/customer-credit-insufficient.ts +1 -3
  24. package/api/src/libs/notification/template/customer-credit-low-balance.ts +1 -3
  25. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +1 -3
  26. package/api/src/libs/notification/template/customer-reward-succeeded.ts +1 -3
  27. package/api/src/libs/notification/template/one-time-payment-refund-succeeded.ts +1 -3
  28. package/api/src/libs/notification/template/one-time-payment-succeeded.ts +1 -3
  29. package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -3
  30. package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +1 -3
  31. package/api/src/libs/notification/template/subscription-slippage-warning.ts +1 -3
  32. package/api/src/libs/notification/template/subscription-succeeded.ts +1 -1
  33. package/api/src/libs/pagination.ts +14 -9
  34. package/api/src/libs/payment.ts +25 -10
  35. package/api/src/libs/session.ts +1 -1
  36. package/api/src/libs/timing.ts +35 -0
  37. package/api/src/libs/util.ts +16 -15
  38. package/api/src/libs/wallet-migration.ts +72 -53
  39. package/api/src/queues/auto-recharge.ts +1 -1
  40. package/api/src/queues/credit-consume.ts +94 -12
  41. package/api/src/queues/credit-grant.ts +4 -0
  42. package/api/src/queues/event.ts +14 -2
  43. package/api/src/queues/invoice.ts +1 -0
  44. package/api/src/queues/payment.ts +83 -15
  45. package/api/src/queues/refund.ts +84 -71
  46. package/api/src/queues/subscription.ts +1 -0
  47. package/api/src/routes/checkout-sessions.ts +82 -43
  48. package/api/src/routes/connect/change-payment.ts +2 -0
  49. package/api/src/routes/connect/change-plan.ts +2 -0
  50. package/api/src/routes/connect/pay.ts +12 -3
  51. package/api/src/routes/connect/setup.ts +3 -1
  52. package/api/src/routes/connect/shared.ts +52 -39
  53. package/api/src/routes/connect/subscribe.ts +4 -1
  54. package/api/src/routes/credit-grants.ts +25 -17
  55. package/api/src/routes/donations.ts +2 -2
  56. package/api/src/routes/meter-events.ts +16 -6
  57. package/api/src/routes/payment-links.ts +1 -1
  58. package/api/src/routes/payment-methods.ts +1 -1
  59. package/api/src/routes/settings.ts +1 -1
  60. package/api/src/routes/tax-rates.ts +1 -1
  61. package/api/src/store/models/customer.ts +23 -1
  62. package/api/src/store/models/payment-method.ts +4 -0
  63. package/api/src/store/models/price.ts +23 -14
  64. package/api/tests/libs/wallet-migration.spec.ts +4 -4
  65. package/api/tests/queues/credit-consume-batch.spec.ts +5 -2
  66. package/api/tests/queues/credit-consume.spec.ts +8 -4
  67. package/api/tests/routes/credit-grants.spec.ts +1 -0
  68. package/blocklet.yml +1 -1
  69. package/cloudflare/MIGRATION-CHALLENGES.md +676 -0
  70. package/cloudflare/MIGRATION-RUNBOOK.md +777 -0
  71. package/cloudflare/README.md +499 -0
  72. package/cloudflare/STAGING-MIGRATION-GUIDE.md +602 -0
  73. package/cloudflare/build.ts +151 -0
  74. package/cloudflare/did-connect-auth.ts +527 -0
  75. package/cloudflare/docs/2026-04-22-sdk-1.30.9-upgrade-retro.md +324 -0
  76. package/cloudflare/docs/2026-04-24-queue-ops-followup.md +218 -0
  77. package/cloudflare/docs/cf-queues-ops-alert-analysis.md +663 -0
  78. package/cloudflare/docs/cf-workers-local-dev-and-fixes.md +284 -0
  79. package/cloudflare/docs/cleanup-tasks-2026-05.md +62 -0
  80. package/cloudflare/docs/payment-kit-platform-analysis-2026-04-20.md +354 -0
  81. package/cloudflare/frontend-shims/buffer-polyfill.ts +9 -0
  82. package/cloudflare/frontend-shims/js-sdk.ts +43 -0
  83. package/cloudflare/frontend-shims/mime-types.ts +46 -0
  84. package/cloudflare/frontend-shims/session.ts +24 -0
  85. package/cloudflare/frontend-shims/vite-plugin-noop.ts +6 -0
  86. package/cloudflare/index.html +40 -0
  87. package/cloudflare/migrate-to-d1.js +252 -0
  88. package/cloudflare/migrations/0001_initial_schema.sql +82 -0
  89. package/cloudflare/migrations/0002_indexes.sql +75 -0
  90. package/cloudflare/migrations/0003_locks_and_constraints.sql +18 -0
  91. package/cloudflare/run-build.js +390 -0
  92. package/cloudflare/scripts/test-decrypt.js +102 -0
  93. package/cloudflare/shims/arcblock-ws.ts +20 -0
  94. package/cloudflare/shims/axios-http-adapter.ts +4 -0
  95. package/cloudflare/shims/axios-lite.ts +117 -0
  96. package/cloudflare/shims/blocklet-sdk/auth-service.ts +33 -0
  97. package/cloudflare/shims/blocklet-sdk/cdn.ts +3 -0
  98. package/cloudflare/shims/blocklet-sdk/component-api.ts +35 -0
  99. package/cloudflare/shims/blocklet-sdk/component.ts +18 -0
  100. package/cloudflare/shims/blocklet-sdk/config.ts +8 -0
  101. package/cloudflare/shims/blocklet-sdk/did.ts +14 -0
  102. package/cloudflare/shims/blocklet-sdk/env.ts +12 -0
  103. package/cloudflare/shims/blocklet-sdk/eventbus.ts +3 -0
  104. package/cloudflare/shims/blocklet-sdk/fallback.ts +3 -0
  105. package/cloudflare/shims/blocklet-sdk/index.ts +11 -0
  106. package/cloudflare/shims/blocklet-sdk/logger.ts +11 -0
  107. package/cloudflare/shims/blocklet-sdk/middlewares.ts +15 -0
  108. package/cloudflare/shims/blocklet-sdk/notification.ts +11 -0
  109. package/cloudflare/shims/blocklet-sdk/security.ts +53 -0
  110. package/cloudflare/shims/blocklet-sdk/session.ts +8 -0
  111. package/cloudflare/shims/blocklet-sdk/verify-sign.ts +38 -0
  112. package/cloudflare/shims/blocklet-sdk/wallet-authenticator.ts +3 -0
  113. package/cloudflare/shims/blocklet-sdk/wallet-handler.ts +6 -0
  114. package/cloudflare/shims/blocklet-sdk/wallet.ts +103 -0
  115. package/cloudflare/shims/cookie-parser.ts +3 -0
  116. package/cloudflare/shims/cors.ts +21 -0
  117. package/cloudflare/shims/cron.ts +189 -0
  118. package/cloudflare/shims/crypto-js-warn.ts +7 -0
  119. package/cloudflare/shims/did-space-js.ts +17 -0
  120. package/cloudflare/shims/did-space.ts +11 -0
  121. package/cloudflare/shims/error.ts +18 -0
  122. package/cloudflare/shims/express-compat/index.ts +80 -0
  123. package/cloudflare/shims/express-compat/types.ts +41 -0
  124. package/cloudflare/shims/fastq.ts +105 -0
  125. package/cloudflare/shims/lock.ts +115 -0
  126. package/cloudflare/shims/mime-types.ts +56 -0
  127. package/cloudflare/shims/nedb-storage.ts +9 -0
  128. package/cloudflare/shims/node-child-process.ts +9 -0
  129. package/cloudflare/shims/node-fs.ts +20 -0
  130. package/cloudflare/shims/node-http.ts +13 -0
  131. package/cloudflare/shims/node-https.ts +4 -0
  132. package/cloudflare/shims/node-misc.ts +15 -0
  133. package/cloudflare/shims/node-net.ts +8 -0
  134. package/cloudflare/shims/node-os.ts +14 -0
  135. package/cloudflare/shims/node-tty.ts +8 -0
  136. package/cloudflare/shims/node-zlib.ts +17 -0
  137. package/cloudflare/shims/noop.ts +26 -0
  138. package/cloudflare/shims/payment-vendor.ts +14 -0
  139. package/cloudflare/shims/querystring.ts +12 -0
  140. package/cloudflare/shims/queue.ts +585 -0
  141. package/cloudflare/shims/rolldown-runtime.ts +43 -0
  142. package/cloudflare/shims/sequelize-d1/datatypes.ts +24 -0
  143. package/cloudflare/shims/sequelize-d1/helpers.ts +46 -0
  144. package/cloudflare/shims/sequelize-d1/index.ts +34 -0
  145. package/cloudflare/shims/sequelize-d1/model.ts +1157 -0
  146. package/cloudflare/shims/sequelize-d1/operators.ts +293 -0
  147. package/cloudflare/shims/sequelize-d1/retry.ts +85 -0
  148. package/cloudflare/shims/sequelize-d1/sequelize-class.ts +119 -0
  149. package/cloudflare/shims/sequelize-d1/timing.ts +81 -0
  150. package/cloudflare/shims/sequelize-d1/types.ts +35 -0
  151. package/cloudflare/shims/stripe-cf.ts +29 -0
  152. package/cloudflare/shims/ws-lite.ts +103 -0
  153. package/cloudflare/shims/xss.ts +3 -0
  154. package/cloudflare/tests/shims/cron.spec.ts +210 -0
  155. package/cloudflare/tests/shims/queue-scheduled.spec.ts +186 -0
  156. package/cloudflare/vite.config.ts +162 -0
  157. package/cloudflare/worker.ts +1553 -0
  158. package/cloudflare/wrangler.json +63 -0
  159. package/cloudflare/wrangler.jsonc +69 -0
  160. package/cloudflare/wrangler.staging.json +66 -0
  161. package/cloudflare/wrangler.toml +28 -0
  162. package/jest.config.js +4 -12
  163. package/package.json +26 -22
  164. package/src/app.tsx +62 -4
  165. package/src/components/customer/link.tsx +9 -13
  166. package/src/components/customer/notification-preference.tsx +3 -2
  167. package/src/components/filter-toolbar.tsx +4 -0
  168. package/src/components/invoice/list.tsx +9 -1
  169. package/src/components/invoice-pdf/utils.ts +2 -1
  170. package/src/components/layout/admin.tsx +39 -5
  171. package/src/components/layout/user-cf.tsx +77 -0
  172. package/src/components/payment-intent/actions.tsx +23 -3
  173. package/src/components/safe-did-address.tsx +75 -0
  174. package/src/libs/patch-user-card.ts +25 -0
  175. package/src/libs/util.ts +5 -7
  176. package/src/pages/admin/billing/meter-events/index.tsx +4 -0
  177. package/src/pages/admin/customers/customers/detail.tsx +2 -2
  178. package/src/pages/admin/customers/customers/index.tsx +2 -2
  179. package/src/pages/admin/overview.tsx +3 -1
  180. package/src/pages/customer/subscription/detail.tsx +4 -4
  181. package/tsconfig.api.json +1 -6
  182. package/tsconfig.json +3 -4
  183. package/tsconfig.types.json +2 -1
  184. package/vite.config.ts +6 -1
@@ -0,0 +1,354 @@
1
+ # Payment Kit 上线后运行评估
2
+
3
+ > **[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`**。
4
+
5
+ **撰写**:2026-04-20(2026-04-21 补入三笔真实订单时间轴分析)
6
+ **对象**:`staging-aigne-hub-payment-kit`、`payment-kit-staging`
7
+ **数据**:CF Analytics 7 天 + D1 直查 events 表 + 三笔真实订单 + 代码审查 + 业界调研
8
+
9
+ ---
10
+
11
+ ## 总述
12
+
13
+ ### 一句话结论
14
+
15
+ **现在不迁,花 2-3 周做"Phase 1 优化"(下面解释到底是啥);12-24 个月内大概率要迁到混合部署,迁不迁取决于 Payment Kit 未来是否继续大改功能。**
16
+
17
+ ### 什么是 Phase 1?(就是下面这 8 件具体的事)
18
+
19
+ | # | 动作 | 位置 | 省多少 |
20
+ |---|---|---|---:|
21
+ | 1 | 汇率查询加 30s 缓存 | `libs/reference-cache.ts` 新增 `getCachedExchangeRate` | 2-5s/笔 |
22
+ | 2 | 合并 checkout-sessions 重复读 | `routes/checkout-sessions.ts:170, 256` 同一个 PaymentMethod 读了两次 | 1-2s/笔 |
23
+ | 3 | Invoice 生成阶段 DB batch 化 | `queues/invoice.ts` + `routes/checkout-sessions.ts` 连续 await 改 `db.batch()` | 3-4s/笔 |
24
+ | 4 | 加 3 条复合索引 | `meter_events(customer_id, status, livemode)` / `credit_transactions(source)` / `events(object_id, created_at)` | Dashboard 快 10× |
25
+ | 5 | 启用数据归档 cron | `crons/index.ts:47-59` 打开 `retentionConfig.enabled`,90 天保留 | 防数据膨胀 |
26
+ | 6 | 批量 update 加空数组短路 | `queues/credit-consume.ts:933, 1487` 等 `Op.in` 处加 `if (!ids.length) return;` | 对冲 D1 $5K/10s 事故 |
27
+ | 7 | `cronInstance.runAll` 过滤 | shim 层按 cron 表达式过滤不到期 job | cron 空转读降 60-80% |
28
+ | 8 | Dashboard 预聚合表 | 新增 `meter_events_daily_summary` + 小时 cron,`/meter-events/stats` 先读聚合 | Dashboard 从秒到毫秒 |
29
+
30
+ **2-3 周 1 人可完成,全部是零迁移、可回滚的改动,账单仍 $5-10/月**。
31
+
32
+ **另外两件建议一起做**(不在 Phase 1 里但更见效):
33
+ - **改前端(1 周)**:18-46 秒黑盒期加进度文字("等待钱包签名" / "上链中" / "扣款中")+ checkout page 首屏 2 秒出来不要等 subscription 创建完
34
+ - **挂 Logpush 到 R2(1 天)**:长尾请求根因现在看不清,挂了才能复盘
35
+
36
+ ### Phase 1 后预期效果
37
+
38
+ | 指标 | 现在 | Phase 1 后 | 迁混合后(参考)|
39
+ |---|---:|---:|---:|
40
+ | 服务端时间(订阅流程)| 22-25 秒 | **15-18 秒** | 3-5 秒 |
41
+ | 服务端时间(一次性付款)| 13 秒 | **~8 秒** | 1-2 秒 |
42
+ | 支付端到端(链上快)| ~46 秒 | **~38-40 秒** | ~28 秒 |
43
+ | Dashboard stats 响应 | ~1-5 秒 | **< 500ms** | < 200ms |
44
+ | `exceededResources` 比率 | 13% | **< 5%** | < 1% |
45
+
46
+ ---
47
+
48
+ ### 不同用户规模的月资费
49
+
50
+ **假设**:每活跃用户月均 3-5 次支付 + 100-1000 次 meter event(AI API 调用计费)
51
+
52
+ | 月活用户 | 月支付笔数 | 月 meter events | **CF 账单** | 主要成本项 |
53
+ |---:|---:|---:|---:|---|
54
+ | 当前(~10 笔/周)| ~40 | <1K | **$5** | 底价,资源都在免费额度内 |
55
+ | 100 用户 | ~400 | ~50K | **$5** | 一切免费 |
56
+ | 1,000 用户 | ~4K | ~500K | **$5-8** | Queue ops 接近 1M 免费上限 |
57
+ | 10,000 用户 | ~40K | ~5M | **$10-20** | Queue ops 超额 + D1 读增加 |
58
+ | 100,000 用户 | ~400K | ~50M | **$50-100** | D1 行读超过 25B 免费额度 |
59
+
60
+ **前提**:必须完成 Phase 1(索引 + 归档 + 预聚合)。**如果不做 Phase 1,在 1 万用户量级 D1 读就会因统计扫表突破免费额度,账单可能飙到 $100-200**。
61
+
62
+ **更现实的视角:按接入方数量看**(Payment Kit 是内部基础设施,驱动成本的是"多少个服务用它"):
63
+
64
+ | 接入方数量 | 典型场景 | 月 meter events | CF 账单 |
65
+ |---:|---|---:|---:|
66
+ | 1-2 | 当前(AIGNE Hub)| <1K | $5 |
67
+ | 3-5 | 6-12 个月后 | 15K-75K | $5 |
68
+ | 5-10 | 12-24 个月 | 150K-600K | $5-10 |
69
+ | 10+ | 生态全面接入 | ~3M | $10-20 |
70
+
71
+ **结论**:在内部基础设施的定位下,**CF 账单在未来 1-2 年都不会超过 $20/月**,真正该关心的是成本之外的维度(性能、体感、运维债)——这才是决策重点。
72
+
73
+ ---
74
+
75
+ ### 为什么要这么建议(数据支撑)
76
+
77
+ #### 三笔真实订单的耗时证据
78
+
79
+ | 订单 | 类型 | 总时长 | 服务端 | 用户+链上 |
80
+ |---|---|---:|---:|---:|
81
+ | Case 1 | 订阅(个人 staging)| 76.3s | **24.7s** | 51.6s |
82
+ | Case 2 | 订阅(个人 staging)| 46.0s | **22.0s** | 24.0s |
83
+ | Case 3 | 一次性 + 促销码(ArcBlock staging)| 56.6s | **~13s** | ~44s |
84
+
85
+ **关键发现**:
86
+ - **订阅流程服务端固定 22-25 秒**(前两笔差不到 3 秒)
87
+ - **一次性付款服务端 ~13 秒**(少了订阅创建的 ~10 秒)
88
+ - 总时长差别主要来自用户+链上时间(不可控)
89
+
90
+ #### 为什么服务端这么慢?
91
+
92
+ 代码里**每笔支付要串行做 80-130 次 D1 调用**(Case 2 的 events 表 24 条事件 × 每条 3-6 次 DB 操作)。每次 D1 查询 **165ms**(Worker 在外部,D1 在 HKG,跨区网络费),累加就是 15-20 秒。不是查询慢,是查太多次。
93
+
94
+ 举例:`routes/checkout-sessions.ts:170-364` 仅订单创建路径就有 9+ 次串行 DB 读,其中 PaymentMethod 在 `:170` 和 `:256` 被读了两次。**加索引没用,要减调用次数**(去重 + batch + 并发),这就是 Phase 1 第 1-3 件事在做的。
95
+
96
+ #### 钱不是瓶颈,持续漏水才是
97
+
98
+ - 账单 $5/月,未来 1-2 年都不是瓶颈
99
+ - 真正吃时间的不是 shim 层维护(shim 写完就跑),而是**CF 专属问题排查**:
100
+ - **tail latency 根因难查**:P99 wall time 138 秒,不知道在等啥
101
+ - **`exceededResources` 每天 100-300 次**:定位难
102
+ - **本地复现难**:`wrangler dev --local` 跟线上有差异
103
+ - **部署验证久**:每改一次全量部署 + 手工测
104
+ - 加起来 **10-15% FTE**(每月 3-6 天)。最近例子:04-18 `scheduledTime` cron bug 修复后错误率从 23% 降到 13%
105
+
106
+ ---
107
+
108
+ ### 后期需不需要迁?**大概率要,这是功能迭代强度决定的**
109
+
110
+ **需要迁的理由(大概率发生)**:
111
+
112
+ 1. **新功能碰壁必然发生**:D1 不支持跨 Worker 事务、Worker 30 秒 CPU 上限、单库 10GB 上限。只要还在加功能,早晚撞上其中一个。比如"批量退款 + 补偿记账 + 通知"这种跨表事务,D1 没法干净地做
113
+ 2. **CF 本身会演进**:D1、Queue、Workers 的 API 都在变,每次变动 shim 都得跟——这是**永远在发生**的
114
+ 3. **代码栈跟主仓分叉**:主仓每次升级都要测 CF 兼容,越拖分叉越大,最后变成维护"CF 版 Payment Kit"这个 fork
115
+ 4. **Service Binding 红利会稀释**:一旦某个关键依赖(比如 blocklet-service)迁离 CF,零网络费优势立刻消失
116
+
117
+ **不需要迁的理由(需刻意选择)**:
118
+
119
+ 1. Payment Kit 进入稳定维护期,功能定型
120
+ 2. 业务真的不增长(一年后还是一周 10 笔支付)
121
+ 3. 10-15% FTE 持续漏水团队能长期接受
122
+
123
+ **关键判断**:这是"功能迭代强度"的问题,不是"用户量"的问题。即便 1 万用户代码不大改,Phase 1 就够;即便 100 用户只要还在频繁加新付费模型,迁移就是必然。
124
+
125
+ **触发迁移的 5 个信号(6 个月内观察,任一触发就启动混合部署)**:
126
+ - Phase 1 做完 `exceededResources` 仍 > 5%
127
+ - 新功能连续 2 次因 D1/Worker 限制被阻
128
+ - CF 专属问题占工时 > 20%
129
+ - Blocklet 生态里有依赖服务迁离 CF
130
+ - 月记账量 > 500K 或接入方 > 5 个
131
+
132
+ **诚实预测**:12-24 个月内至少触发其中 1-2 条的概率 > 70%。
133
+
134
+ ---
135
+
136
+ ### 五方案评分
137
+
138
+ | 方案 | 评分 | 一句话 |
139
+ |---|:---:|---|
140
+ | 1. 不动 | 62 | Dashboard 会越来越慢,非预期增长账单飙到 $200+ |
141
+ | **2. Phase 1 优化** | **83** ⭐ | **2-3 周、零风险、现在的选择** |
142
+ | 3. CF + Hyperdrive + PG | 58 | 花 $25-50 换 RTT,shim 债和 Worker 限制都没解决 |
143
+ | 4. 混合部署(CF 入口 + 独立 Node + PG)| 80 | **大概率 12-24 个月要走到这里** |
144
+ | 5. 完全单体 | 48 | 失去 Service Binding,重建认证链,不值 |
145
+
146
+ ---
147
+
148
+ ## 一、真实数据
149
+
150
+ ### 1.1 7 天 Analytics
151
+
152
+ | 指标 | 值 |
153
+ |---|---:|
154
+ | 总请求 | 31,559(~4.5K/天)|
155
+ | 错误 `exceededResources` | 7,234(23% → 04-18 修复后 13%)|
156
+ | 成功支付 | 10 笔 |
157
+ | 月账单推算 | $5(各资源都在免费额度内)|
158
+ | Wall P50 / P99 | 340ms / **138 秒** |
159
+ | CPU P99 | 183ms(99% wall 在等 I/O)|
160
+
161
+ ### 1.2 三笔订单时间轴对比
162
+
163
+ | 阶段 | Case 1 订阅 (76s) | Case 2 订阅 (46s) | Case 3 一次性+促销 (57s) |
164
+ |---|---:|---:|---:|
165
+ | 事件数 | 24 | 24 | 17 |
166
+ | 用户看单 + 输入促销 | — | — | ~19s(Stage A)|
167
+ | 创建订阅 | 13.7s | 11.3s | ❌ 无订阅 |
168
+ | **黑盒(链上+用户签名)**| **45.9s** | **18.5s** | **24.9s** |
169
+ | Invoice 生成 | 7.8s | 7.6s | ❌ 流程不同 |
170
+ | 建 PI + 记录促销 | — | — | 6.6s |
171
+ | 链上扣款 | 5.7s | 5.5s | — |
172
+ | 收尾 | 3.2s | 3.1s | 6.4s |
173
+ | **服务端合计** | **24.7s** | **22.0s** | **~13s** |
174
+
175
+ **结论**:
176
+ - **订阅流程服务端固定 22-25 秒**(前两笔几乎相同)
177
+ - **一次性付款服务端 ~13 秒**(少了订阅创建状态机的 ~10 秒)
178
+ - 总时长差别全在黑盒(用户签名 + 链上),服务端是结构化的、按业务类型有固定基线
179
+
180
+ ### 1.3 D1 RTT 是架构副作用,改不掉
181
+
182
+ ```
183
+ served_by_region: "APAC", colo: "HKG"
184
+ sql_duration_ms: 1.71 ← SQL 实际执行
185
+ wall time: 169.00 ← 总耗时
186
+ → 167.3 ms 是 Worker ↔ D1 网络往返
187
+ ```
188
+
189
+ `placement: smart` 把 Worker 拉到靠近 Service Binding 目标的位置,D1 留在 HKG。
190
+
191
+ ### 1.4 一笔支付为什么要 80-130 次 DB 调用
192
+
193
+ Case 2 的 `events` 表 24 条事件,每条事件对应一次状态变化,**每次状态变化要 3-6 次 DB 操作**(读校验 + 写主表 + 写关联 + audit)。24 × 4-5 ≈ 100 次。
194
+
195
+ 举例:`routes/checkout-sessions.ts:170-364` 仅订单创建路径就有 **9+ 次串行 DB 读**(PaymentMethod、PaymentCurrency、Price、CheckoutSession.findAll、PromotionCode、Discount 等),其中 PaymentMethod 在 `:170` 和 `:256` 被读了两次。
196
+
197
+ **不是查询慢,是查太多次**。加索引解决不了,**需要减少调用次数**(去重 + batch + 并发)。
198
+
199
+ ---
200
+
201
+ ## 二、业界对标(简版)
202
+
203
+ **CF 上有支付功能的案例,核心都不在 CF**:
204
+ - [PhonePe](https://venturebeat.com/data-infrastructure/digital-payments-leader-partners-with-cloudflare-to-accelerate-secure-monthly-mobile-payments):4.35 亿用户,CF 只做边缘层安全 + 流量卸载
205
+ - [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**
206
+ - [Stripeflare](https://github.com/janwilmake/stripeflare):唯一"CF 上做虚拟钱包"的项目,用 Durable Objects 不是 D1,原型级
207
+
208
+ **专业计费产品没一个跑 CF**:
209
+ - [Lago](https://github.com/getlago/lago):Ruby + Postgres + Sidekiq
210
+ - [OpenMeter](https://github.com/openmeterio/openmeter):Go + ClickHouse + K8s
211
+
212
+ **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 为空或缺失时有同款风险。
213
+
214
+ ---
215
+
216
+ ## 三、成本对比
217
+
218
+ ### 3.1 账面月成本(按规模)
219
+
220
+ | 场景 | 现状 | Phase 1 | Hyperdrive+PG | 混合 | 单体 |
221
+ |---|---:|---:|---:|---:|---:|
222
+ | 当前(30 记账/天)| $5 | $5 | $30 | $25 | $40 |
223
+ | 12 个月(5K/天) | $8 | $8 | $35 | $35 | $60 |
224
+ | 24 个月(30K/天)| $20 | $15 | $50 | $60 | $100 |
225
+ | 非预期 10× 暴涨 | **$200+** | $40 | $80 | $120 | $180 |
226
+
227
+ ### 3.2 隐性成本(持续消耗的工程时间)
228
+
229
+ | 类型 | 现状 / Phase 1 / Hyperdrive | 混合 / 单体 |
230
+ |---|---:|---:|
231
+ | 调试 CF 专属 bug(tail latency、exceededResources、本地复现)| 每月 3-6 天 | 几乎 0 |
232
+ | shim 层跟随主仓升级 | 每半年 1-2 天 | 0(代码回归标准栈)|
233
+
234
+ **$2,500/月"工程时间"不是夸张**,但**主要来自"出 bug 后难排查"**,不是"shim 要常改"。迁到混合部署后这部分会接近消失。
235
+
236
+ ---
237
+
238
+ ## 四、Phase 1 具体动作(这是什么意思)
239
+
240
+ **Phase 1 = 8 件具体代码改动,2-3 周内可完成**。每一条都带文件位置和预期效果:
241
+
242
+ ### 4.1 汇率查询加缓存(省 2-5s/笔)
243
+
244
+ **问题**:Case 1 和 Case 2 每笔支付都触发 2-3 次 `exchange_rate_provider.updated`,每次去外部 API 取汇率 2-3 秒。
245
+
246
+ **改动**:
247
+ - 复用 `api/src/libs/reference-cache.ts` 现有的 `getCachedMeter`、`getCachedCurrency` 模式
248
+ - 新增 `getCachedExchangeRate(providerId, ttlMs = 30000)`
249
+ - 把 `ExchangeRateProvider.findByPk` + 外部 API 调用的地方改走缓存(约 5-10 个调用点)
250
+
251
+ ### 4.2 checkout 重复读合并(省 1-2s/笔)
252
+
253
+ **问题**:`routes/checkout-sessions.ts:170` 和 `:256` 各读一次 `PaymentMethod.findByPk(method.id)`。Case 2 Stage A 里类似的重复读至少 3 处。
254
+
255
+ **改动**:
256
+ - 在 route 入口一次性读完所有需要的 reference data(customer / price / payment_method / currency)
257
+ - 后面函数接受参数,不再自己查
258
+
259
+ ### 4.3 Invoice 生成 batch 化(省 3-4s/笔)
260
+
261
+ **问题**:Case 2 Stage C 7.6 秒做了 8 个事件,每个事件 3-6 次 DB 调用。多次 `subscription.update` + `invoice.update` + `payment_intent.create` 串行。
262
+
263
+ **改动**:
264
+ - 定位 `queues/invoice.ts` 和 `routes/checkout-sessions.ts` 的连续 await 链
265
+ - 用 `db.batch([q1, q2, q3])` 合并无依赖的写入
266
+ - 需要依赖的保持串行
267
+
268
+ ### 4.4 三条复合索引(Dashboard 快 10×)
269
+
270
+ **问题**:Dashboard 的 `/meter-events/stats`、`/overdue-summary` 扫全表。`retryFailedEventsForCustomer` 每次充值扫 `meter_events` 全表。
271
+
272
+ **改动**:新增 migration 加索引:
273
+ - `meter_events(customer_id, status, livemode)`
274
+ - `credit_transactions(source)`
275
+ - `events(object_id, created_at)`
276
+
277
+ ### 4.5 启用数据归档 cron(防止数据膨胀)
278
+
279
+ **问题**:`meter_events` 和 `events` 表无限增长,半年后 Dashboard 会越来越慢、D1 接近 10GB 上限。
280
+
281
+ **改动**:
282
+ - 代码已在 `crons/index.ts:47-59`,`retentionConfig.enabled` 目前是 false
283
+ - 配置打开 + 设 90 天保留窗口
284
+
285
+ ### 4.6 所有批量 update/delete 加空数组短路(对冲 $5K/10s 事故)
286
+
287
+ **问题**:`queues/credit-consume.ts:933, 1487` 等处 `MeterEvent.update({...}, {where: {id: {[Op.in]: ids}}})` 如果 `ids` 为空,Sequelize 可能生成无 WHERE 的 UPDATE。
288
+
289
+ **改动**:
290
+ ```js
291
+ if (!ids.length) return;
292
+ await Model.update(payload, { where: { id: { [Op.in]: ids } } });
293
+ ```
294
+ - 审计所有 `Op.in` + `update/destroy` 的组合
295
+ - 加 D1 daily usage 告警(CF dashboard 上设置)
296
+
297
+ ### 4.7 `cronInstance.runAll` 过滤(降 cron 空转)
298
+
299
+ **问题**:每分钟触发 `scheduled` 事件都 `runAll()` 遍历 16 个 job。job 内部会自己判断时间,但仍会做一次 D1 读。基础消耗 ~40M rows/day 与用户量无关。
300
+
301
+ **改动**:
302
+ - 定位 `cloudflare/shims/cron/` 或业务 cron 的 `runAll` 实现
303
+ - 进入 runAll 时按 cron 表达式先过滤掉"本分钟不到时间的 job"
304
+ - 每分钟只跑到期的,降 cron 基础读 60-80%
305
+
306
+ ### 4.8 Dashboard 预聚合表(Dashboard 从几秒到毫秒)
307
+
308
+ **问题**:`/api/meter-events/stats` 按 day 聚合原始 `meter_events` 表,数据一多就慢。
309
+
310
+ **改动**:
311
+ - 新增 `meter_events_daily_summary` 表(customer_id, event_name, date, event_count, total_value)
312
+ - 新增 cron:每小时聚合昨日数据写入
313
+ - `meter-events/stats` 路由先读聚合表,缺的部分回源原表
314
+
315
+ ---
316
+
317
+ ## 五、Phase 1 预期效果
318
+
319
+ | 指标 | 现在 | Phase 1 后 | 迁混合后(对比参考)|
320
+ |---|---:|---:|---:|
321
+ | 服务端时间 | 22-25 秒 | **15-18 秒** | 3-5 秒 |
322
+ | 支付端到端(链上快)| ~46 秒 | **~38-40 秒** | ~28 秒 |
323
+ | 支付端到端(链上慢)| ~76 秒 | **~68-70 秒** | ~55 秒 |
324
+ | Dashboard stats 响应 | ~1-5 秒 | **< 500ms** | < 200ms |
325
+ | `exceededResources` 比率 | 13% | **< 5%** | < 1% |
326
+ | 月账单 | $5 | $5-10 | $25-60 |
327
+
328
+ **投入**:2-3 周 / 1 人
329
+ **风险**:低(全部是零迁移、可回滚的改动)
330
+
331
+ ---
332
+
333
+ ## 附录
334
+
335
+ ### A. 数据采集方法
336
+
337
+ - **CF GraphQL Analytics**:`workersInvocationsAdaptive`、`d1AnalyticsAdaptiveGroups`、`queueMessageOperationsAdaptiveGroups`、`kvOperationsAdaptiveGroups`(token 在 `blocklets/core/.env.development.local`)
338
+ - **D1 直查**:`GET /api/__dev__/d1/query?sql=...`
339
+ - **D1 Benchmark**:`GET /api/__dev__/d1-benchmark`
340
+ - **代码审查**:`worker.ts`、`crons/index.ts`、`queues/credit-consume.ts`、`routes/checkout-sessions.ts`
341
+
342
+ ### B. 三笔订单样本
343
+
344
+ | Session | 类型 | 账号 | 总时长 | 服务端 | 黑盒 | 链上扣款 |
345
+ |---|---|---|---:|---:|---:|---:|
346
+ | `cs_2E5uh1...` | 订阅 | 个人 staging | 76.3s | 24.7s | 45.9s | 5.7s |
347
+ | `cs_QhTSzc2...` | 订阅 | 个人 staging | 46.0s | 22.0s | 18.5s | 5.5s |
348
+ | `cs_YAWq4B...` | 一次性+促销 | ArcBlock staging | 56.6s | ~13s | 24.9s | ~4.8s |
349
+
350
+ ### C. 相关文档
351
+
352
+ - [`cf-cost-analysis-2026-04-20.md`](./cf-cost-analysis-2026-04-20.md) — 成本专题
353
+ - [`cf-queues-ops-alert-analysis.md`](./cf-queues-ops-alert-analysis.md) — Queue ops RCA
354
+ - [`pr-1341-review.md`](./pr-1341-review.md) — 近期 PR review
@@ -0,0 +1,9 @@
1
+ // Buffer polyfill for browser environment
2
+ // Provides minimal Buffer.from() and Buffer.isBuffer() needed by hash libraries
3
+ import { Buffer } from 'buffer/';
4
+
5
+ if (typeof window !== 'undefined' && !(window as any).Buffer) {
6
+ (window as any).Buffer = Buffer;
7
+ }
8
+
9
+ export { Buffer };
@@ -0,0 +1,43 @@
1
+ // Replaces @blocklet/js-sdk — simple axios wrapper without blocklet auth
2
+ import axios from 'axios';
3
+
4
+ export function createAxios(config: any = {}, _requestParams?: any) {
5
+ return axios.create({
6
+ timeout: 30000,
7
+ ...config,
8
+ });
9
+ }
10
+
11
+ // getBlockletSDK returns an SDK-like object with common API methods
12
+ export function getBlockletSDK() {
13
+ const client = createAxios();
14
+ return {
15
+ user: {
16
+ getUserPublicInfo: async (_did: string) => ({ data: null }),
17
+ getProfile: async () => ({ data: null }),
18
+ getPrivacy: async () => ({ data: [] }),
19
+ getUserPrivacyConfig: async () => ({ data: [] }),
20
+ updateProfile: async (_data: any) => ({ data: null }),
21
+ updatePrivacy: async (_data: any) => ({ data: null }),
22
+ logout: async (_opts?: any) => ({}),
23
+ },
24
+ notification: {
25
+ getNotifications: async () => ({ data: [] }),
26
+ markAsRead: async () => ({}),
27
+ },
28
+ session: {
29
+ getSession: async () => ({ data: { user: null } }),
30
+ },
31
+ client,
32
+ api: client,
33
+ };
34
+ }
35
+
36
+ // BlockletSDK class used by did-space-react
37
+ export class BlockletSDK {
38
+ static create() {
39
+ return getBlockletSDK();
40
+ }
41
+ }
42
+
43
+ export default { createAxios, getBlockletSDK, BlockletSDK };
@@ -0,0 +1,46 @@
1
+ // ESM wrapper for mime-types to fix CJS interop in vite build
2
+ // @blocklet/uploader uses _mimeTypes.default.lookup() pattern
3
+ import mimeDb from 'mime-db';
4
+
5
+ const types: Record<string, string> = {};
6
+ const extensions: Record<string, string[]> = {};
7
+
8
+ // Build lookup tables from mime-db
9
+ Object.entries(mimeDb).forEach(([type, data]: [string, any]) => {
10
+ if (data.extensions) {
11
+ data.extensions.forEach((ext: string) => {
12
+ types[ext] = type;
13
+ });
14
+ extensions[type] = data.extensions;
15
+ }
16
+ });
17
+
18
+ export function lookup(filenameOrExt: string): string | false {
19
+ if (!filenameOrExt || typeof filenameOrExt !== 'string') return false;
20
+ const ext = filenameOrExt.replace(/^.*\./, '').toLowerCase();
21
+ return types[ext] || false;
22
+ }
23
+
24
+ export function extension(type: string): string | false {
25
+ if (!type || typeof type !== 'string') return false;
26
+ const mime = type.split(';')[0].trim().toLowerCase();
27
+ const exts = extensions[mime];
28
+ return exts && exts.length > 0 ? exts[0] : false;
29
+ }
30
+
31
+ export function contentType(str: string): string | false {
32
+ const mime = str.indexOf('/') === -1 ? lookup(str) : str;
33
+ return mime || false;
34
+ }
35
+
36
+ export function charset(type: string): string | false {
37
+ const mime = typeof type === 'string' && type.split(';')[0].trim().toLowerCase();
38
+ if (mime && /^text\/|^application\/(javascript|json|xml|ecmascript)/.test(mime)) {
39
+ return 'UTF-8';
40
+ }
41
+ return false;
42
+ }
43
+
44
+ export const charsets = { lookup: charset };
45
+
46
+ export default { lookup, extension, contentType, charset, charsets };
@@ -0,0 +1,24 @@
1
+ // CF Workers version of contexts/session.ts
2
+ // Uses real DID Connect session with proper cookie settings for HTTPS
3
+ import createSessionContext from '@arcblock/did-connect-react/lib/Session';
4
+ import { useContext } from 'react';
5
+
6
+ const { SessionProvider, SessionContext, SessionConsumer, withSession } = createSessionContext(
7
+ 'login_token',
8
+ 'cookie',
9
+ { path: '/', secure: true, sameSite: 'Lax', returnDomain: false },
10
+ { appendAuthServicePrefix: false }
11
+ );
12
+
13
+ function useSessionContext() {
14
+ const ctx = useContext<any>(SessionContext);
15
+ if (!ctx) return ctx;
16
+ return {
17
+ session: ctx.session,
18
+ connectApi: ctx.connectApi,
19
+ events: ctx.events,
20
+ ...ctx.session,
21
+ };
22
+ }
23
+
24
+ export { SessionProvider, SessionContext, SessionConsumer, useSessionContext, withSession };
@@ -0,0 +1,6 @@
1
+ // Replaces vite-plugin-blocklet — no-op in CF build
2
+ export function createBlockletPlugin(_opts?: any) {
3
+ return {
4
+ name: 'noop-blocklet-plugin',
5
+ };
6
+ }
@@ -0,0 +1,40 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" href="/favicon.ico" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0" />
7
+ <meta name="theme-color" content="#4F6AF5" />
8
+ <title>Payment Kit CF</title>
9
+ <!-- CF WORKERS VERSION -->
10
+ <script>
11
+ // Inject window.blocklet for @arcblock/did-connect-react and @blocklet/ui-react
12
+ window.blocklet = {
13
+ prefix: '/api',
14
+ groupPrefix: '/',
15
+ componentId: null,
16
+ serverVersion: '3.0.0',
17
+ languages: [
18
+ { code: 'en', name: 'English' },
19
+ { code: 'zh', name: '简体中文' }
20
+ ],
21
+ appPid: 'payment-kit-cf',
22
+ appId: 'payment-kit-cf',
23
+ appName: 'Payment Kit',
24
+ appLogo: '',
25
+ appDescription: 'Decentralized Payment System',
26
+ appUrl: window.location.origin,
27
+ GA_MEASUREMENT_ID: '',
28
+ theme: { prefer: 'light' },
29
+ navigation: [],
30
+ webWalletUrl: '',
31
+ componentMountPoints: [],
32
+ };
33
+ </script>
34
+ </head>
35
+ <body>
36
+ <noscript>You need to enable JavaScript to run this app.</noscript>
37
+ <div id="app"></div>
38
+ <script type="module" src="../src/index.tsx"></script>
39
+ </body>
40
+ </html>