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

# Stripe 计费

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

FIM One 在管理员控制的功能标志后面提供完整的 Stripe 计费管道。不需要支付功能的私有部署可以将其关闭，永远不会看到相关 UI。SaaS 运营商只需切换一个开关，即可获得托管结账、客户门户、由 webhook 驱动的订阅生命周期管理和配额强制执行。

<Note>
  计费**默认禁用**。全新安装和现有自托管部署都以 `system_settings.billing_enabled = FALSE` 开始。在管理员明确激活之前，不会显示任何计费 UI。
</Note>

## 您将获得

* **免费版和专业版** 配有月度令牌配额（默认值：免费版 1M / 专业版 5M；激活后可调整）
* **Stripe 托管结账** — 用户无需您的代码接触卡数据即可升级
* **客户门户** — 用户可更新支付方式、下载发票、取消订阅——全部在 Stripe 的界面上完成
* **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 计费**为 ON
4. 后端验证 `STRIPE_SECRET_KEY` 和 `STRIPE_WEBHOOK_SECRET` 都存在——如果任一缺失，返回 400 并且切换保持 OFF
5. 仅在首次激活时，后端运行**幂等设置**：
   * 种子化免费版和专业版套餐（如果已存在则跳过）
   * 设置 `system_settings.default_plan_id` 为免费版套餐 id
   * 为任何没有套餐的用户回填 `users.plan_id = free.id`
   * 同步 `default_token_quota` → 免费版套餐 `monthly_token_quota`，使现有的管理员控制的全局配额得以保留
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 billing_enabled = TRUE
3. system_settings.default_token_quota  ── defensive fallback
4. unlimited                ── last resort if everything above is NULL
```

当启用计费时，第3步很少被执行，因为激活会为每个用户回填`users.plan_id`。它作为纵深防御存在，以确保配置错误的计划永远不会静默地为用户解除上限。

### 周期重置

* 对于付费用户，`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`。

### 默认值

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

| 变量                                         | 默认值                       | 可通过以下方式调整                    |
| ------------------------------------------ | ------------------------- | ---------------------------- |
| `billing_plans.monthly_token_quota` (Free) | `1,000,000`               | 管理员 → 计费 → 套餐 → Free → 编辑    |
| `billing_plans.monthly_token_quota` (Pro)  | `5,000,000`               | 管理员 → 计费 → 套餐 → Pro → 编辑     |
| `system_settings.default_token_quota`      | `1,000,000`（激活时与 Free 同步） | 管理员 → 系统设置 → 配额              |
| `system_settings.billing_enabled`          | `FALSE`                   | 管理员 → 系统设置 → 计费              |
| Pro 列表价格                                   | `$20.00 USD / month`      | Stripe Dashboard（价格对象）       |
| Stripe webhook 事件订阅数                       | 6                         | Stripe Dashboard → Webhooks  |
| Stripe 价格缓存 TTL                            | `5 minutes`               | `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>
