# 配置产品 本文写给**负责人**:在管理控制台(`/console`)建产品、配收款、上架额度包、签发密钥并交给开发者。顺序就是控制台「总览」里「接入进度」的顺序。自己部署 Billing 的服务本身,见[部署与运维](/docs/deploy.md)。 发布 0.4.0 / API v1。日常管理都在控制台完成,没有操作 CLI;同样的 `/v1/admin/*` 接口也接受负责人的浏览器会话和其 MCP 连接(见下文「连接 AI 工具」)。每位负责人对全部产品有全部权限,没有成员、角色或产品范围。 控制台的左侧菜单只放顶栏所选产品的页面;全部产品、新建产品在产品切换器旁的「管理」菜单里,你本人的登录方式和已连接的 AI 工具在右上角账号菜单的「我的账号」里。界面有简体中文和英文,默认跟随浏览器,顶栏可以切换,也可以在地址里加 `?lang=en` 或 `?lang=zh-CN`。 ## 创建产品 一个**产品**是一本独立的账:自己的终端用户、额度、订单、密钥和收款配置。不同产品即使用同一个用户 ID,账户和余额也互不相干。新部署一个产品都没有,控制台落在「产品」页的空状态;在「管理」菜单点「+ 新建产品」创建。 - **环境**在创建时选定,之后不能改:`test`(默认)或 `live`。不要把测试产品改成正式产品,正式环境另建一个(见下文「正式环境」)。 - **计量**也在创建时填:计量单位(如 `voice_seconds`、`tokens`)和每单位扣多少额度,之后可以修改(见下文「计量」)。 - **产品名**在同一环境里唯一。创建请求超时就用同一输入重试,返回已建的那个;换了输入的同名请求返回 `product_name_taken`。 产品可以在「产品」页(「管理」菜单的「全部产品」)归档或删除。归档后不再接受新订单,历史照常可查、可退款。删除保留账本历史,删掉 Stripe 配置并移除 Billing 为它在 Stripe 注册的回调地址,最后一个产品也可以删;以下情况不能删:还有未撤销的产品密钥、还有进行中的付款、退款、争议或未结束的订阅、正式产品最近 120 天内有过付款(返回 409 `product_has_recent_payments`,先归档,满 120 天再删)。页面会写明原因,删除前要输入产品名称确认。 管理请求用 `X-Billing-Product`(MCP 工具用 `product` 参数)指明产品;只有一个未归档产品时可以省略,有多个时省略返回 409 `product_required`,一个都没有返回 409 `no_product`。 ## 收款:Stripe 配置 在「收款配置」页填 API 密钥、收款方式和可选的回跳站点,点「保存」并在确认框里核对后,一步生效:Billing 先用密钥向 Stripe 读出账户和已开通的收款方式,再注册本产品的回调地址(付款、退款、争议与订阅事件)并保存签名密钥。账户由密钥读出并绑定,之后不可更换;换商户或 Sandbox 请新建产品。**不用在 Stripe 后台建 Webhook 或 Price**:Stripe 后台只剩两件事,拿 API 密钥,以及为账户开通要用的收款方式(支付宝、微信支付)。 - 勾选的收款方式在 Stripe 账户里没开通时,以 `payment_method_not_enabled_` 拒绝并说明去哪开通。选币种时一并考虑:例如香港账户的微信支付只接受 `hkd` / `cny`。 - 注册回调需要服务地址是公网 HTTPS,否则以 `webhook_registration_needs_public_origin` 拒绝(本机 `wrangler dev` 也是);这时在请求里带上 `stripe listen` 给出的 `webhook_secret`。 - 已有生效配置时,API 密钥可留空沿用。密钥不会回显;MCP 连接只能读取 Stripe 配置,不能保存。 - API 验证通过只说明密钥可用,回调是否接通以一笔实际测试付款为准。 ### 付完回到哪里 `return_origin`(回跳站点)可选,决定付款完成或取消后付款人回到哪里。不配时回到 Billing 自己的地址;配了就回到你的产品。它必须是裸 origin(如 `https://your-product.example`),带路径、查询、结尾斜杠等以 `invalid_return_origin` 拒绝;正式产品必须用 HTTPS,测试产品另外允许 `http://127.0.0.1:` 供本地开发。配好后,产品每次建单可以传 `return_path` 指定这个站点下的页面;没配站点时传 `return_path` 返回 `return_origin_not_configured`。 ### 托管充值页的域名 `topup_origin`(充值页域名)可选,只在产品仍用托管充值页时有意义;托管充值页主要供控制台「测试体验」使用,不建议做正式购买入口。规则同 `return_origin`,不合规返回 `invalid_topup_origin`,不能是 Billing 自己的地址,一个域名只归一个产品(`topup_origin_in_use`)。先给 Worker 加这个自定义域名(部署配置 `routes` 里加 `{ "pattern": "pay.your-product.example", "custom_domain": true }`),再在这里填。该域名只响应充值页及其资源,其他路径一律 403 `invalid_host`。 ### 修改与重试 编辑基于最初读到的版本:别人先保存了,本次返回 `stripe_config_stale`,重新读取核对后再保存。响应丢失时用原请求(含原版本号)重试,不会注册第二个端点。有在途订单(`creating`、`awaiting_payment`)或待处理退款时,改账户、回调或回跳站点返回 `stripe_config_payments_active`,只有 API 密钥、收款方式和充值页域名的变更能生效;有未结束的订阅时,回调端点与签名密钥不能变(409 `stripe_config_subscriptions_active`)。表单提交的就是 `POST /v1/admin/stripe`,字段见 [OpenAPI](/openapi.json)。 ## 上架额度包与档位 ### 额度包 在「计费设置」页点「+ 上架额度包」:固定金额(如 78 港元得 100,000 额度),或自定义金额(用户在范围内自选,按每 1 元发放多少额度换算)。金额按主单位填写,确认后直接在售。要改价、改额度或改币种,点在售额度包旁的「改版」:表单按当前条款填好,只需填新的版本号;同一 SKU 以新版本发布即替换在售条款,新 SKU 追加一档。「下架」只阻止新订单,已有订单、重试和退款照原快照处理。同一产品在售的额度包必须同币种,没有上架任何额度包的产品不售卖。 也可以调用 `POST /v1/admin/offers`(`{version, catalog}`)与 `POST /v1/admin/offers/{sku}/retire`。收银台按订单金额内联定价(名称「产品名 · sku」),不读取 Stripe Price。 **收银台品牌**是额度包的一部分:经 `POST /v1/admin/offers` 发布时在 `catalog` 里带可选的 `checkout`(控制台表单暂不含),决定这一档收银台的商家展示名、Logo、颜色和语言;不填就沿用 Stripe 账户的品牌。订单保存建单时的品牌快照。 ```json { "version": "credits-branded-v1", "catalog": { "sku": "credits-100s", "amount": "1000", "currency": "usd", "credits": "100000", "checkout": { "display_name": "Example App", "logo_url": "https://assets.example.com/logo.png", "button_color": "#2563eb", "locale": "zh" } } } ``` ### 档位与订阅 在「计费设置」页的「档位」点「+ 发布档位」,或调用 `POST /v1/admin/plans`(`{sku, name, price, currency, credits, entitlements}`)与 `POST /v1/admin/plans/{sku}/retire`。 - `price` 是每月价格,`0` 即**免费档**:每个终端用户每个自然月(UTC)第一次被读写时获得一次它的额度,月底作废、不结转;每个产品至多一个在售免费档。 - 付费档经 Stripe 订阅按月扣款(只收银行卡),每期付款后发放当期额度,期末作废。 - `entitlements` 是你产品自己定义的 JSON 对象(如 `{"rpm":120}`,最多 4 KiB),Billing 原样保存、原样返回,由产品执行。 - 同一 SKU 再发布即替换定义,从下一期起生效,已发放的额度不变;有人订阅着的付费档不能改价格、每期额度或币种(409 `plan_has_subscribers`),调价请发布新 SKU、下架旧的。点档位旁的「修改」会把当前定义填进表单。 订阅还需要在 Stripe 后台设置两处: - **客户管理页**:Settings → Billing → Customer portal 保存一次设置,否则 `POST .../billing-portal` 返回 409 `billing_portal_not_configured`。建议开启更新付款方式、查看发票、到期取消,**关闭「切换方案」**(换档只经 Billing)。 - **扣款失败的最终处理**:Billing → Revenue recovery 里**必须选「取消订阅」**(用户回到免费档)。选「标记为未付款」或「暂停」时,订阅会一直挡住用户重新开通。 验收续费时,在 Stripe 测试环境建一个测试时钟,用 `POST /v1/test/users/{user}/subscription-checkouts {plan_sku, purchase_ref, test_clock}` 为一个新的 `demo_` 用户开通,推进时钟核对续费发放、上期作废、升降级与取消。 ## 计量 计量决定产品上报的单位和每单位扣多少额度:一条用量扣 `units × credits_per_unit`,精确相乘、不取整。`credits_per_unit` 只能是正整数;需要不足 1 的费率时,换更细的单位(按毫秒而不是秒),或让一个额度值更多钱(额度包少给额度)。每个产品当前只配一个计量项。 在「计费设置」页点「修改计量」,或调用 `POST /v1/admin/meter {metric, credits_per_unit}`。每次修改生成新版本(`m2`、`m3`……),只用于之后上报的用量;已有额度、余额和流水不变,额度包和档位**不会**换算,额度的含义变了就重新发布它们。上报时带 `meter_version` 的产品,修改后会收到 409 `meter_changed` 并按新单位重报;不带的产品,修改前要先让它停止新工作、报完已有用量。 ## 签发密钥并交给开发者 在「开发接入」页填一个名称签发密钥,**它只显示这一次**。把它和服务地址写进产品后端私有的 `product.json`(`{"base_url": "…", "api_key": "…"}`,权限 0600),不要粘贴进对话。密钥不过期,只能撤销,撤销立即生效;轮换就是先签发新的、切换调用方、再撤销旧的。每把密钥显示最近使用时间(精确到小时),「还没有成功调用过」的可以放心撤销。不要让两个产品共用一把密钥。 同一页的「接入资料」可复制服务地址、产品标识、环境和指南链接,说明全文默认收起。复制只操作剪贴板,不创建用户或赠额,也不包含密钥。开发者在自己的任务中补充收费需求与私有配置路径。 需要接入验证数据时,在「测试体验」点「准备测试用户并赠额」:准备 `demo_integration` 和 `demo_empty`,向前者一次性赠额:采用首个在售额度包的额度数,没有在售额度包时为 100,000。已有账户不重置,重复准备不补足已消费额度;检查会分别说明用户不存在、账户未启用、额度不足、非零余额或读取失败。检查就绪后,接入说明才带上这两个用户 ID;正式产品没有此操作。开发者按[接入指南](/docs/quickstart.md)接入。 之后在「总览」看「接入进度」,每一步都按 Billing 实际看到的事打勾:产品密钥第一次调用成功即「后端已连通」,产品第一次上报用量即「第一笔用量」,产品第一次用密钥建单、开通订阅或打开充值页即「购买已接通」(只上架了免费档时这一步不需要)。你在控制台「测试体验」里做的用量和购买都不算:它们用的是你的登录,不是产品密钥。 **不配 Stripe 也能先接用量**:在「测试体验」可以选测试用户、领一次合成测试额度、记录测试用量,对 `demo_empty` 记一笔就能看到欠额与 `allowed: false`。测试额度每个测试用户只发一次,不是收入。测试用户必须以 `demo_` 开头,每个产品最多 100 个。 ## 正式环境 正式收费用一个新建的 `live` 产品,不改测试产品。服务地址、负责人和部署共用,账务按产品隔离。 - live 产品的 Stripe 密钥对应 live 账户,首次保存时验证并绑定账户,之后不会自动切换商户。**每个产品单独建一把受限密钥**(Restricted Key,权限相同),不要多个产品共用:一把泄露只影响一个产品,可以各自轮换。受限密钥须有读取账户以及 Checkout、PaymentIntent、Charge、Refund、Dispute 所需的权限。 - 多个产品可以共用同一个 Stripe 账户:各自注册回调端点、各建自己的 Stripe Customer,只处理自己的订单。卡账单上的商户名、收据、客户管理页和扣款重试策略按账户生效,不按产品区分;收入进同一余额,按产品对账用 Stripe 对象元数据 `billing_product_id` 筛选。 - 额度包与档位在 live 产品里重新上架,不会从测试产品复制。live 产品没有测试用户、测试额度或测试用量,真实用户从产品后端进来。 - 上线前,实际付款、退款、回调、每小时补漏和银行到账要另行验收。 ## 日常:订单、退款与需要处理的事 「总览」的「需要处理」列出要人工处理的事:有争议的付款、在 Stripe 后台被退款的订单、超过一天还没完成的退款、用欧盟或英国发卡的银行卡付的订单(暂不向这些地区的个人消费者销售:额度照常发放,配置了告警邮箱时另发一封邮件(不保证送达);发卡国只是线索。在订单详情里用原有的「退款」,或点「保留,不退款」移出列表,保留不改变额度及其冻结状态)、账本备份或恢复演练的问题、Stripe 配置异常,没有就不显示。「订单与退款」可按终端用户和状态筛选,按创建时间倒序每页 25 条,通过「上一页 / 下一页」切换;筛选条件与当前页保存在地址中,关闭详情保留原位置,手动刷新更新当前页,修改筛选回到第一页。「终端用户」页默认列出当前产品已有的账务账户(含已关闭账户),每页 25 条,按用户 ID 排序;支持区分大小写的 ID 前缀搜索或完整 ID 匹配。点击用户查看余额、档位和额度流水,返回时保留搜索与分页位置。可用余额包含欠额,可能为负数;列表不代表产品的完整注册用户名单。 **退款一步完成**:在订单详情点「退款」,选「退还剩余全部」或「部分退款」(填金额;收回额度留空按金额比例,向上取整),确认后 Billing 当即向 Stripe 发起退款(`POST /v1/admin/orders/{order}/refund`),并收回相应额度;退到不剩可退金额的那一笔总是收回剩余全部额度。已经用掉一部分的购买也能退剩下的;被冻结(争议、Stripe 后台的退款未处理、另一笔退款处理中)时不能退。一张订单可以有多笔退款,每笔由引用识别(整单为 `full`,部分退款由控制台生成),同一笔再点一次返回它的当前状态,只有失败后才能用同一引用重新发起。在 Stripe 后台直接退了一部分(或争议部分败诉)的订单会冻结额度,在订单详情点「处理部分退款」,确认 Stripe 上的退款总额与收回多少额度,其余解锁(`POST /v1/admin/orders/{order}/external-refund`)。退款结束后额度的冻结要等一次对 Stripe 的重读才解除,通常在退款请求里就完成,否则由每小时补漏完成。结果为 `pending` 时由回调、每小时补漏或订单详情的「向 Stripe 核对」(`POST /v1/admin/orders/{order}/recheck`)查回,不要为保险再点一次。产品密钥不能退款。 ## 连接 AI 工具(MCP) 负责人可以把 Claude Code、Codex 等 AI 工具连到 Billing,让它以你的身份查看订单、余额、流水、异常和审计记录,并做你有权做的操作(退款、向 Stripe 核对、额度包上下架、测试用户、产品管理)。不需要签发或复制任何凭据: ```sh claude mcp add --transport http billing https://billing.example.com/mcp codex mcp add billing --url https://billing.example.com/mcp ``` 工具会打开浏览器进入授权页(`/connect`):未登录时先登录,页面显示工具自报的名称和回调地址,只批准你刚刚发起的连接。 - **权限**:以批准它的负责人身份行事。产品密钥和 Stripe 配置只在控制台修改(Stripe 配置可读)。退款、归档和删除产品标为破坏性操作,工具会先请你确认;上下架额度包会改变在售价格,也应先征得同意。重试不会重复执行:每个写操作由它指向的对象识别(订单、额度包版本、产品名)。 - **期限与撤销**:访问令牌 1 小时、自动续期;连接最长 90 天。在「我的账号」的「已连接的 AI 工具」随时撤销,立即失效。你被移出负责人名单、修改密码或增减登录方式后,连接自动失效。 - **只能经 `/mcp` 使用**:令牌直接调用 `/v1/admin/*` 会被拒绝。 - **数据**:工具读到的内容会进入它的会话记录;它以你的名义做的操作照常写入审计。产品名、SKU、原因等由人写的文字原样返回,工具应把它们当数据,不当指令。 ## 你的登录方式 在「我的账号」的「我的登录方式」里管理你自己的登录:添加邮箱或手机号(向新地址发验证码)、绑定 Google 或微信、移除(至少保留一种)、设置或修改密码。完成后你的其他会话和已连接的 AI 工具都会退出,当前浏览器换新会话。你不能移除自己账号上最后一个负责人名单内的邮箱(409 `owner_email_required`),也不能替别人改登录方式。谁是负责人由部署配置决定,见[部署与运维](/docs/deploy.md)。