Billing 文档开始使用体验充值与扣费0.4.0 · API v1管理控制台 →

部署与运维

本文只写给自己部署 Billing 服务的人:Cloudflare 上的部署、负责人名单、登录方式和备份。在控制台里建产品、配收款、签发密钥,见配置产品;产品开发者见接入指南。

发布 0.4.0 / API v1。服务运行在 Cloudflare Workers:账本在 SQLite Durable Object,登录用独立的 D1(认证库)。每个部署管理一个组织下的多个产品。部署脚本从源码仓库运行:先 bun install --frozen-lockfile,之后用 bun tools/<x>.ts;构建用 Bun,Wrangler 需要 Node.js 22 或更新。

负责人与登录

能登录控制台的只有负责人:部署配置 BILLING_OWNERS(wrangler 配置里的 var,逗号分隔的邮箱,不区分大小写;未设置时沿用 BILLING_CONFIG.owner_email)列出的邮箱。名单为空或格式不对时服务拒绝一切请求(503,日志 owners_not_configured)。不开放注册:名单上的邮箱第一次用验证码登录(或用担保该邮箱的 Google 登录)即开户;其余情况解析到的不是负责人就返回 403 not_an_owner,不建账号。

  • 增加或移除负责人:改 BILLING_OWNERS 并重新部署。每个请求都按名单核对,移出名单的人的会话和 AI 工具连接立即失效。
  • 找回登录:名单上的邮箱照常用验证码登录,账号仍持有该地址就回到原账号。邮箱本身丢了,把新邮箱加进名单并部署。没有恢复票据。

登录由共享认证库 authkit 实现(ADR-0013):邮箱验证码、密码、短信验证码(中国大陆手机号)、Google、微信扫码。一个人可以绑定多种方式,在控制台「我的账号」里管理。每个部署独立管理身份,部署之间不共享账号或 Cookie。认证库与账务库分开:登录在无状态 Worker 里完成,只把签名过的会话交给账务库,负责人名单每次请求重新核对。

登录方式的配置

EMAIL_FROM 是 var,其余是 secret(wrangler secret put 逐个设置)。某种方式缺任何一项即关闭,登录页不显示它。

方式需要的设置
邮箱验证码EMAIL_FROM(已开通 Cloudflare Email Sending 的域名下的地址),并用 send_email 绑定 EMAIL
短信SMS_ACCESS_KEY_ID、SMS_ACCESS_KEY_SECRET、SMS_SIGN_NAME、SMS_TEMPLATE_CODE(阿里云)
GoogleGOOGLE_CLIENT_ID、GOOGLE_CLIENT_SECRET;在 Google 客户端登记回调 <服务地址>/auth/google/callback
微信WECHAT_RELAY、WECHAT_CLIENT、WECHAT_RELAY_TOKEN、WECHAT_AUD、WECHAT_KID、WECHAT_PUBLIC_KEY、WECHAT_QR_APPID;在 wx-login-relay 的 clients.json 为本实例登记一项,return_url 为 <服务地址>/auth/wechat/callback
运维提醒OPS_ALERT_EMAIL(可选)

密码总是可用,但要先用其他方式开户,再在控制台设置。

会话与限流

会话空闲 8 小时过期,使用中自动续期,最长 7 天,到期需重新登录;写请求校验 Origin 与 CSRF。每个登录名 15 分钟内 10 次密码错误、每来源 50 次;每来源每 15 分钟 60 次认证写请求;验证码按 authkit 默认上限,全站每天邮件 500 条、短信 200 条(EMAIL_DAILY_TOTAL、SMS_DAILY_TOTAL 可调),到 80% 和用尽时给 OPS_ALERT_EMAIL 发提醒。登录验证码对任何地址回答都一样,只向名单上的邮箱或负责人已绑定的地址实际发送。来源取 Cloudflare 设置的 cf-connecting-ip。

部署到 Cloudflare

在本机验证

BILLING_OWNERS=owner@example.com bun run cloudflare:setup
bunx wrangler d1 migrations apply AUTH_DB --local
bun run dev:cloudflare

setup 写出 .billing/cloudflare-local/ 下的 config.json、secrets.json 和含 BILLING_OWNERS 的 .dev.vars。Wrangler 监听 http://127.0.0.1:8789,打开 /console 用名单上的邮箱登录。本机模拟不真正发信:wrangler 终端会打印每封邮件保存的 .eml 路径,验证码在里面。数据在 .wrangler/state。setup 拒绝覆盖已有配置,重启只运行 dev:cloudflare。

首次部署

先确定 Worker 名称、HTTPS 根地址和本部署独占的 D1 库。

wrangler.template.jsonc 是模板,不是任何部署的配置:它不叫 wrangler.jsonc,wrangler 不会自动发现它。把它复制到本部署的私有位置再改,并让 BILLING_WRANGLER_CONFIG 指向该副本;副本含 account、Worker 名、域名与 D1 id,属于本部署的身份,不入库。在副本里配置 name、BILLING_ORIGIN、EMAIL_FROM、BILLING_OWNERS,把新建 D1 的 database_id 填进 AUTH_DB 绑定(保留 migrations/auth 路径,不与其他实例共用认证库),保留 send_email 绑定和每小时一次的 Cron。

mkdir -m 700 -p .billing && cp wrangler.template.jsonc .billing/cloudflare.jsonc
export BILLING_WRANGLER_CONFIG=.billing/cloudflare.jsonc
BILLING_OWNERS=owner@example.com bun run cloudflare:setup https://billing-test.YOUR_SUBDOMAIN.workers.dev
bunx wrangler login
bunx wrangler d1 create billing-auth
# 将返回的 database_id 配入 .billing/cloudflare.jsonc
bunx wrangler d1 migrations apply AUTH_DB --remote -c "$BILLING_WRANGLER_CONFIG"
bunx wrangler secret bulk .billing/cloudflare/secrets.json -c "$BILLING_WRANGLER_CONFIG"
bun run deploy:cloudflare

部署后负责人用名单上的邮箱登录,在控制台建产品、签发产品密钥(见配置产品)。config.json 保存 deployment_id,secrets.json 包含 BILLING_CONFIG 与独立的 AUTH_SECRET。deployment_id 决定账本对象的身份,必须保持稳定,改名或更换不能当作迁移;认证库、认证秘密和账本对象缺一不可。

升级

升级既有部署用 bun run upgrade cloudflare(需要设好 BILLING_WRANGLER_CONFIG):先把账本备份到私有 R2 桶、导出认证库,再发布,并核对 /readyz 和线上前端与构建一致。也可以用 bun run deploy:cloudflare 直接发布;未设置配置路径时它会拒绝运行。部署前可用 bunx wrangler deploy -c "$BILLING_WRANGLER_CONFIG" --dry-run 核对绑定解析到的确实是本部署的 D1 与域名。

修改部署引导 config.json 后只更新 BILLING_CONFIG,保留原认证秘密;已有持久支付配置的产品在控制台管理,不会被旧输入覆盖:

bun -e 'const p=".billing/cloudflare/"; await Bun.write(p+"config-update.json", JSON.stringify({BILLING_CONFIG: JSON.stringify(await Bun.file(p+"config.json").json())}));'
bunx wrangler secret bulk .billing/cloudflare/config-update.json

密码哈希用 Argon2id,在 Worker 里计算(本地实测约 100 ms);实际部署需确认密码登录延迟与资源限制。

账本、备份与边界

账本当前结构为 core 14 / billing 23;迁移在对象启动时执行。到账靠 Stripe 回调,每小时的 Cron 重读未结订单与待处理退款补漏。

  • 备份:账本每小时有变化就增量备份到私有 R2 桶(最多丢 1 小时),每周自动做一次恢复演练;失败、超过 2 小时没有成功或演练失败时给 OPS_ALERT_EMAIL 发提醒,控制台「总览」的「需要处理」也会列出。30 天内还可以按时间点回退(秒级、可撤销,作紧急手段);认证库用部署前的 D1 导出。具体步骤见源码仓库 docs/operations.md「账本备份与恢复」。
  • 存储里有支付秘密:账本里保存各产品的 Stripe 密钥(本版本未做应用层加密),备份桶必须保持私有,账本不能当普通数据导出。
  • 不提供账务导入导出。不要用单库时间点回退覆盖已经发生的新账务;Cloudflare 的存储恢复也不能替代账本与渠道事实的一致性核验。涉及存量数据的升级先制定恢复方案。
  • 用 bun run check:cloudflare 在隔离的 Workers 运行时验证核心场景;实际部署按接入指南的验收一节检查。

从私有配置导入 Stripe

推荐在控制台「收款配置」页配置(见配置产品):Billing 会注册回调,额度包不需要 Stripe Price,本节都不需要。只有要从私有配置初次导入时,才在 Stripe 后台为该产品建回调端点 https://你的服务域名/webhooks/stripe/<product_id>,监听 checkout.session.completed、checkout.session.expired、charge.refunded、charge.dispute.created、charge.dispute.updated、charge.dispute.closed、refund.updated、refund.failed,订阅另需 invoice.paid、invoice.payment_failed、customer.subscription.updated、customer.subscription.deleted;再把配置合并进私有 config.json 的 payments(不要覆盖其他字段,尤其是 deployment_id),重新生成并上传秘密:

{
  "payments": {
    "prod_REPLACE": {
      "environment": "test",
      "account_id": "acct_REPLACE",
      "merchant_ref": "merchant-main",
      "secret_key": "sk_test_REPLACE",
      "webhook_secret": "whsec_REPLACE"
    }
  }
}

一旦持久配置存在,之后重启不会用旧外部输入覆盖它。上传后用产品密钥读一次 GET /v1/capabilities 确认含 checkout.create,并实际完成一次沙盒付款与退款;能力探测不能代替支付验收。

Markdown 原文 · 文档首页 · 0.4.0