# 部署与运维 本文只写给**自己部署 Billing 服务的人**:Cloudflare 上的部署、负责人名单、登录方式和备份。在控制台里建产品、配收款、签发密钥,见[配置产品](/docs/setup.md);产品开发者见[接入指南](/docs/quickstart.md)。 发布 0.4.0 / API v1。服务运行在 Cloudflare Workers:账本在 SQLite Durable Object,登录用独立的 D1(认证库)。每个部署管理一个组织下的多个产品。部署脚本从源码仓库运行:先 `bun install --frozen-lockfile`,之后用 `bun tools/.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](https://github.com/liuzl/billing/blob/main/docs/decisions/ADR-0013-adopt-authkit.md)):邮箱验证码、密码、短信验证码(中国大陆手机号)、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`(阿里云) | | Google | `GOOGLE_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 ### 在本机验证 ```sh 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。 ```sh 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 ``` 部署后负责人用名单上的邮箱登录,在控制台建产品、签发产品密钥(见[配置产品](/docs/setup.md))。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`,保留原认证秘密;已有持久支付配置的产品在控制台管理,不会被旧输入覆盖: ```sh 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 运行时验证核心场景;实际部署按[接入指南](/docs/quickstart.md)的验收一节检查。 ### 从私有配置导入 Stripe 推荐在控制台「收款配置」页配置(见[配置产品](/docs/setup.md)):Billing 会注册回调,额度包不需要 Stripe Price,本节都不需要。只有要从私有配置初次导入时,才在 Stripe 后台为该产品建回调端点 `https://你的服务域名/webhooks/stripe/`,监听 `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),重新生成并上传秘密: ```json { "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`,并实际完成一次沙盒付款与退款;能力探测不能代替支付验收。