> ## Documentation Index
> Fetch the complete documentation index at: https://docs.one.fim.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Stripe 计费

> 启用按用户的 Stripe 订阅（免费版 + 专业版），包含令牌配额和自助式套餐管理。

FIM One 在实例访问模型后面提供完整的 Stripe 计费管道。私有部署将其设置为**无订阅**，永远不会显示支付 UI。希望获得目录的运营者可以选择**包含 + 付费**或**仅付费**，并获得托管结账、客户门户、由 webhook 驱动的订阅生命周期和配额强制执行。

<Note>
  软件默认为**无订阅**（`access_model = off`、`default_token_quota = 0` 表示无限制）。全新安装和现有自托管部署从此处开始。在管理员选择 Stripe 方案之前，不会显示任何计费 UI。
</Note>

## 你获得的功能

* **三种访问模式**：无订阅、包含+付费或仅付费
* **Stripe托管结账** — 用户升级时无需你的代码接触卡数据
* **客户门户** — 用户更新支付方式、下载发票、取消订阅——全部在Stripe的UI中完成
* **Webhook驱动的生命周期** — 订阅自动配置和续期；已取消的付费用户在周期结束时返回包含层级（或在仅付费模式下失去权限）
* **配额强制执行** — 按周期跟踪令牌使用情况；流式传输中断并显示结构化升级提示
* **管理页面** — 用于计划CRUD和订阅监控

## 前置条件

1. **Stripe账户**已启用实时模式。新加坡注册公司必须完成KYC（营业执照、董事身份证、银行账户）。审批通常需要1-3天。
2. **Stripe实时API密钥**，类型为受限制（推荐使用，相比标准`sk_live_***`更易撤销、权限范围更小）。
3. **Webhook端点**可公开访问，地址为`<your-domain>/api/webhooks/stripe`。
4. **银行账户**用于支付。建议多币种结算（例如USD支付到USD账户），以避免非USD默认Stripe账户每笔交易产生1.5-2%的汇兑损失。

## 设置

### 1. Stripe 仪表板

#### 创建Pro产品

1. **Catalog → Products → + Add product**
2. Name: `Pro`, description: `5M tokens / month, priority support`
3. Pricing: Recurring, monthly, \$20.00 USD (根据您的定价策略调整)
4. Save → 复制生成的 `price_***` ID（激活后，您将使用此值更新本地 `billing_plans` 表）

#### 创建受限API密钥

1. **Developers → API keys → + Create restricted key**
2. Name: `fim-one production`
3. Permissions (minimum):
   * **Customers**: Write
   * **Subscriptions**: Write
   * **Checkout Sessions**: Write
   * **Customer portal**: Write
   * **Prices**: Read
   * **Products**: Read
4. Save → copy `rk_live_***`

#### 注册webhook端点

1. **Developers → Webhooks → + Add endpoint**
2. URL: `https://<your-domain>/api/webhooks/stripe`
3. 要接收的事件：
   * `checkout.session.completed`
   * `customer.subscription.created`
   * `customer.subscription.updated`
   * `customer.subscription.deleted`
   * `invoice.payment_succeeded`
   * `invoice.payment_failed`
4. 保存后，点击"Reveal signing secret"→ 复制 `whsec_***`

#### 配置多币种结算（推荐）

如果您的Stripe账户默认币种与您收费的价格币种不同（常见情况：SGD账户收取USD费用）：

1. **Settings → Bank accounts and currencies → Add a settlement currency**
2. 选择价格币种（例如USD）
3. 关联匹配的银行账户（例如Aspire USD虚拟账户）
4. 保存——Stripe将USD费用直接路由到USD支付，无需外汇转换

### 2. 后端 `.env`

在生产环境的 `.env` 中设置这三个密钥：

```bash theme={null}
STRIPE_SECRET_KEY=rk_live_***
STRIPE_WEBHOOK_SECRET=whsec_***
STRIPE_BILLING_RETURN_URL=https://<your-domain>/settings?tab=billing
```

完整参考请见[环境变量](/configuration/environment-variables)。

编辑 `.env` 后重启后端以使密钥生效：

```bash theme={null}
./deploy.sh   # or: docker compose restart fim-one
```

### 3. 在管理员中激活

1. 以管理员身份登录
2. **管理员 → 系统设置 → 访问和计费**
3. 选择一种状态（实例始终恰好处于其中一种）：
   * **无订阅**（软件默认值）— Stripe 未使用。令牌访问是默认月度配额（`0` = 无限制）。
   * **包含 + 付费** — 需要 Stripe。新用户进入包含层级；他们可以升级。已取消的付费用户在周期结束时返回包含层级。
   * **仅付费** — 需要 Stripe。无包含层级。用户必须先订阅才能调用模型。如果从"包含 + 付费"切换，现有包含层级用户保留其配额。
4. 后端验证当你选择 Stripe 状态时，`STRIPE_SECRET_KEY` 和 `STRIPE_WEBHOOK_SECRET` 都存在——如果缺少任何一个，返回 400
5. 首次激活 Stripe 状态时，后端运行**幂等设置**：
   * 包含 + 付费：为包含计划（`slug=free`）和带有空 Price ID 的 Pro 模板提供种子数据；设置 `default_plan_id`；为没有计划的用户回填
   * 仅付费：仅为 Pro 模板提供种子数据；不进行回填
   * 当该计划被提供种子数据时，将 `default_token_quota` 复制到包含计划（`0` 保持 `0`，无限制）
6. 切换回**无订阅**是**纯标志翻转**，没有数据副作用。当"包含 + 付费"处于活跃状态时，包含的默认计划无法删除。

### 4. 使用您的实时价格更新Pro套餐

激活后，更新预设的Pro套餐以指向您的实时Stripe价格：

* **Admin → Billing → Plans → Pro → Edit**
* 将您的`price_1***`（来自第1步）粘贴到`Stripe Price ID`
* 保存

或通过SQL（如果您更喜欢直接数据库访问）：

```sql theme={null}
UPDATE billing_plans
SET stripe_price_id = 'price_1***'
WHERE slug = 'pro';
```

### 5. 烟雾测试

1. 以普通用户身份打开 `/settings?tab=billing`
2. 点击**升级到 Pro**
3. Stripe Checkout 打开；使用低额真实卡片完成支付（之后退款）
4. Webhook 应该触发——在 Stripe Dashboard → Webhooks → 最近事件中验证显示 2xx 响应
5. 订阅行出现在 `subscriptions` 表中；`users.plan_id` 切换为 `pro`
6. UI 现在显示 Pro 计划 + "管理订阅"按钮

## 禁用账单

在管理员 → 系统设置 → 账单中，将**启用 Stripe 账单**开关切换为关闭。

禁用后：

* 所有 `/api/billing/*` 端点返回 503
* Webhook 端点返回 503（Stripe 将重试，然后在仪表板中显示为失败——这是正常的，如果账单永久关闭，你可以改为在 Stripe 仪表板中禁用 Webhook）
* `Plan & Billing` 用户界面标签页消失
* 管理员 → 账单导航组被隐藏
* 配额链跳过计划层级，直接回退到 `default_token_quota`

**数据被保留**：现有的 `subscriptions`、`billing_plans` 和 `users.plan_id` 行保持不变。重新启用将从相同状态恢复，无需迁移。

## 计算参考——配额和令牌数学

这是权威参考，用于决定用户被允许消费的内容、计数器何时重置以及解析链如何组合的每条数值规则。在更改定价、调整配额、构建使用情况仪表板或规划v2/v3工作之前，请阅读本文档。尚未发布的未来规则记录在其保留位置，以便贡献者知道新逻辑在何处插入。

### 词汇表

| 变量                                    | 存储       | 语义                                        | 范围                    |
| ------------------------------------- | -------- | ----------------------------------------- | --------------------- |
| `users.token_quota`                   | 按用户（覆盖）  | 三态覆盖；见下文语义                                | `NULL`、`0` 或正整数       |
| `users.tokens_used_this_period`       | 按用户（计数器） | 自上次重置以来的累计令牌                              | 非负整数                  |
| `users.quota_reset_at`                | 按用户（锚点）  | 镜像付费用户的 `Subscription.current_period_end` | 时间戳                   |
| `users.plan_id`                       | 按用户（FK）  | 活跃计划                                      | FK `billing_plans.id` |
| `billing_plans.monthly_token_quota`   | 按计划      | 该计划用户的硬上限                                 | 非负整数                  |
| `system_settings.default_token_quota` | 单例       | 当无计划适用时的防御性回退                             | 非负整数                  |
| `system_settings.default_plan_id`     | 单例       | 新用户/未分配用户的免费计划指针                          | FK 或 `NULL`           |
| `system_settings.billing_enabled`     | 单例       | 主开关——控制链的第 2 步                            | 布尔值                   |

### 什么算作一个令牌

令牌消耗在LLM调用层进行计算，来自LiteLLM在每次完成时的`usage`对象。

* **计算**：每次模型调用的提示令牌 + 完成令牌
* **计算**：多步骤/工具使用智能体流中的每个往返（每次模型调用都是单独的扣费）
* **计算**：嵌入请求（知识库摄取、检索评分）
* **不计算**：输入已暂存但从未发送到模型的内容（例如用户丢弃的已上传文件）
* **不计算**：在到达提供商之前失败的请求（身份验证错误、速率限制预检查）
* **缓存输入**：在v1中按全价计算（不显示提供商缓存折扣）。v2可能会单独计入缓存提示令牌。

### 三态覆盖语义

`users.token_quota` 是按用户的管理覆盖。它在一列中承载三种含义：

| 值       | 含义            | 用例                       |
| ------- | ------------- | ------------------------ |
| `NULL`  | 未设置——遵循套餐/默认值 | 所有普通用户的默认状态              |
| `0`     | 无限制           | 管理员/内部账户；"VIP礼赠"         |
| `N > 0` | 硬限制为 `N`      | 在不取消付费订阅的情况下阻止滥用者；预付企业配额 |

覆盖始终优先于套餐和默认值。它的存在使管理员能够将单个用户固定在其套餐层级之上或之下，而无需接触Stripe。

### 配额解析链——v1（当前版本）

对于任何已认证的请求，上限从上到下计算——**首次匹配获胜**：

```
1. users.token_quota        ── NULL? skip. 0? unlimited. N>0? cap at N.
2. users.plan.monthly_token_quota   ── only when access_model is freemium or paid_only
3. paid_only + no plan      ── unentitled (stop; do not fall through)
4. system_settings.default_token_quota  ── used when access_model is off
5. unlimited                ── last resort if everything above is unset
```

第4步是**无订阅**的实例范围上限。在**仅付费**模式下，即使`default_token_quota`为0，没有计划的用户也无权限（聊天返回402）。

### 周期重置

* 对于付费用户，`quota_reset_at` 镜像 `Subscription.current_period_end`。`invoice.payment_succeeded` webhook 处理程序在每次成功续期时将 `tokens_used_this_period` 设置为 0，并将 `quota_reset_at` 推进到新周期结束。
* 对于免费用户（无 Stripe 订阅），每小时 cron 任务在以计划分配日期为锚点的日历月边界处将 `tokens_used_this_period` 重置为 0。
* 周期中途的计划变更**不会**重置计数器——仅续期会重置。这可防止配额循环利用漏洞（"订阅 → 使用专业版配额 → 取消 → 重新订阅"）。

### 中流强制执行

* 聊天调用入口的预检查：最便宜的路径，阻止用户无法承担的请求。
* 流式传输期间，运行中的令牌计数在每个分块上重新评估。超过上限会关闭流，返回**结构化终止帧**，而不是网络错误。
* 前端解释终止帧并显示 `<QuotaExceededDialog>`，其中包含指向 `/settings?tab=billing` 的深层链接。
* 非流式响应返回 HTTP `402`，响应体为 `{ code: "QUOTA_EXCEEDED", reset_at, upgrade_url }`。

### 账单禁用回退

当 `system_settings.billing_enabled = FALSE` 时：

* 跳过链的第 2 步——链折叠为 `override → default → unlimited`。
* `/api/billing/*` 和 `/api/webhooks/stripe` 返回 `503`。
* `Plan & Billing` 用户标签页和 Admin → Billing 导航组被隐藏。
* 所有账单数据（订阅、套餐、`users.plan_id`）被保留——重新启用时从相同状态恢复，无需迁移。

### 保留：配额链 v2 — 团队座位

<Note>尚未发布。此处记录以便 v2 工作有已知的落地点。</Note>

当团队计划发布时：

* `Subscription.quantity` 携带座位数（Stripe 原生）。
* 用户的有效计划通过团队成员身份解析，然后回退到其个人计划：
  ```
  effective_plan = team.plan if team_member(user) else user.plan
  ```
* 配额是**按座位**（每个团队成员获得完整的 `monthly_token_quota`），而不是共享池。共享池会导致先到先得的耗尽，对客户不利。
* 覆盖语义保持不变——团队管理员仍然可以通过 `users.token_quota = N` 对单个成员进行硬限制，该限制在链中位于团队计划之上。

### Reserved: quota chain v3 — native Org allocation (no Stripe)

<Note>Not yet shipped. Reserved for on-prem / enterprise deployments that allocate quota internally without paying Stripe per user.</Note>

* New table `org_quota_allocations(user_id, monthly_token_quota, org_id)` distributes a parent budget across members.
* Allocations are **per user, not a shared pool** — every member has a clear individual SLA.
* Updated chain:
  ```
  override → max(plan_quota, org_allocation) → default → unlimited
  ```
* `max()`，而非 `sum()`。付费Pro用户永远不会获得少于他们已支付的配额，即使他们的组织管理员设置了较低的分配。Stripe支付的配额是神圣的。

### 预留：按使用量付费的信用额度余额（v3 独立维度）

<Note>尚未发布。与上述链独立的轴——信用额度是一次性充值，而非订阅层级。</Note>

* 新表 `user_credits(user_id, balance_cents, currency)` ——通过 Stripe Checkout `mode='payment'` 充值。
* 消费顺序：**订阅配额优先**，然后是信用额度余额（仅在订阅耗尽后才开始扣除信用额度）。
* 信用额度余额不可退款（预付款行业标准）。
* UI 同时展示两个进度条：`订阅配额：已使用 4.2M / 5M` + `信用额度：剩余 $7.40`。

### 默认值

发货时的默认值——除特别说明外，所有值均可在安装后调整。

| 变量                                          | 默认值                                  | 可通过以下方式调整                    |
| ------------------------------------------- | ------------------------------------ | ---------------------------- |
| `system_settings.access_model`              | `off`                                | 管理员 → 系统设置 → 访问和计费           |
| `billing_plans.monthly_token_quota`（包含）     | 复制自 `default_token_quota`（`0` = 无限制） | 管理员 → 系统设置 → 默认月度令牌配额        |
| `billing_plans.monthly_token_quota`（Pro 模板） | `5,000,000`                          | 管理员 → 计费 → 套餐 → Pro → 编辑     |
| `system_settings.default_token_quota`       | `0`（无限制；在免费增值模式下同步到包含的套餐）            | 管理员 → 系统设置 → 默认月度令牌配额        |
| `system_settings.billing_enabled`           | `FALSE`（派生自 `access_model != off`）   | 管理员 → 系统设置 → 访问和计费           |
| Pro 列表价格                                    | `$20.00 USD / 月`                     | Stripe 仪表板（价格对象）             |
| Stripe webhook 事件订阅                         | 6                                    | Stripe 仪表板 → Webhooks        |
| Stripe 价格缓存 TTL                             | `5 分钟`                               | `stripe_client.py` 中硬编码      |
| 订阅生命周期 cron                                 | 每小时                                  | `web/main.py` 中的 APScheduler |
| 免费层重置 cron                                  | 每小时（日历月边界）                           | `web/main.py` 中的 APScheduler |

## 定价模型

V1 是一个统一订阅。免费 + 专业版，按月计费，仅支持美元。

**V1 范围外**（有意推迟至路线图）：

* 团队计划（Stripe 座位 / `subscription.quantity`）
* 年度计费
* 多币种展示
* 优惠券 / 促销代码
* 税务处理（Stripe Tax 集成——需要单独的合规审查）
* 基于使用量的计量 / 超额费用
* 按次付费信用余额（一次性充值）

查看[路线图](/roadmap)了解后续计划。

## 故障排除

<Accordion title="切换开关无法激活——400错误">
  激活端点需要同时设置 `STRIPE_SECRET_KEY` 和 `STRIPE_WEBHOOK_SECRET`。确认它们存在于 `.env` 中，并且编辑后后端已重启。
</Accordion>

<Accordion title="Webhook返回503">
  要么是账单被禁用（切换开关为OFF），要么是请求签名验证失败（`STRIPE_WEBHOOK_SECRET` 不匹配）。检查Stripe仪表板→Webhooks→最近事件以查看实际错误正文。
</Accordion>

<Accordion title="用户已订阅但仍显示免费计划">
  `checkout.session.completed` webhook未到达你的后端。验证Stripe仪表板中的端点URL与 `<your-domain>/api/webhooks/stripe` 完全匹配，包括末尾路径。检查Webhook最近交付记录中的失败。
</Accordion>

<Accordion title="Pro用户看到测试模式价格ID">
  种子迁移写入了测试模式价格ID。激活生产账单后，通过管理员→账单→套餐→Pro→编辑，或通过直接SQL UPDATE，将Pro计划更新为使用你的实时 `price_1***`。
</Accordion>

<Accordion title="收据使用Stripe默认品牌">
  在Stripe仪表板→设置→品牌中配置你的业务品牌。添加你的徽标、业务名称（例如"FIM Labs Pte. Ltd."）和地址。Stripe会将这些应用于所有自动生成的收据和发票。
</Accordion>

<Accordion title="支付中的汇兑损失">
  如果你的Stripe账户默认货币与收费货币不同，Stripe会在每次支付时进行转换（1.5-2%的差价）。在设置→银行账户和货币下添加匹配的结算货币，附加相同货币的银行账户，Stripe将路由相同货币的付款而无需转换。
</Accordion>
