从租户状态机、Host 租户识别、模型选品、客户等级、代充值、AI 配额池到 API Key 边界,拆一套 AI Token 聚合平台的 OEM 功能实现。

能收费上线的 OEM 体系,至少要回答这 8 个工程问题:
OEM 分销不是换一个入口,而是把平台能力拆成租户化的开通、选品、定价、客户、资金、配额、账本和权限边界。
这篇不讲抽象多租户概念。
我只讲我们当前这套 AI Token 聚合平台已经落地的 OEM 功能设计,以及为什么这些细节决定它能不能真正对外分销。
01 先把 OEM 拆成 4 个角色,而不是 1 个 tenant_id
tenant_id 只是数据字段。
OEM 是商业角色、权限边界和账本边界的组合。
我们当前把 OEM 体系拆成四类角色:

这四个角色最关键的差异是:
这里最容易犯的错误,是把 OEM Owner 和 OEM 下游客户都当成“租户用户”。
它们不是一种角色。
Owner 是经营者。
下游客户是消费者。
经营者可以做选品、定价、代充值、客户等级和配额分配;消费者只能使用自己的 Key、钱包、额度和账单。
所以我们没有把 OEM 做成一个简单的 tenant_id 字段改造,而是把它拆成一组后台能力:
这张表就是本文最核心的判断:
OEM 能不能上线,不看有没有换 Logo,而看这几条链路有没有闭环。
02 租户开通不是后台 CRUD,而是状态机
租户不是后台字典项。
如果租户管理只是普通 CRUD,会出现几类很难解释的问题:
审核中的租户被直接改成 active。 已终止的租户被误恢复。 Owner 账号被绑定到多个租户。 高风险租户暂停后,API 入口还在继续调用。 出问题后只看到一个状态值,看不到是谁做了动作。
所以我们把租户生命周期做成显式动作,而不是任意字段更新。
当前状态流转是:
text
create
-> pending_review
-> approve -> active
-> reject -> rejected
active
-> suspend -> suspended
-> terminate -> terminated
suspended
-> activate -> active
-> terminate -> terminated不同状态直接决定租户能力能不能放行:
pending_review | ||
active | ||
rejected | ||
suspended | ||
terminated |
创建租户时,系统不是只插一行租户主数据。
它会同时完成几件事:
校验租户编码格式和唯一性。 校验 Owner 用户存在、未绑定其他 OEM、不是 Admin。 要求操作人确认 Owner 绑定。 初始化默认品牌配置。 初始化运行配置,默认不开放 OEM 代管 API Key。 初始化结算配置。 把 Owner 绑定到租户。 写入审计事件。
审核和状态变更也是独立动作:
text
Admin create tenant
-> pending_review
Admin review tenant
-> approve / reject
Admin change tenant status
-> suspend / activate / terminate这看起来比 CRUD 麻烦。
但它换来的东西很重要:每个状态变化都有合法来源、合法前置状态、操作人、原因和审计记录。
在 OEM 场景里,这不是“后台严谨一点”的问题,而是平台能不能控制分销风险的问题。
03 租户识别:Host 优先,未知 Host 不能自动落到平台
OEM 的入口边界从请求进来的第一秒就开始了。
我们当前的租户识别策略是:
text
1. 先用 Host 查租户域名
2. Host 没命中时,只有平台白名单 Host + 受信来源,才允许 Header 备选
3. 仍没命中时,只有平台白名单 Host 可以回落到平台租户
4. 其他未知 Host 直接返回 tenant not found
5. 命中租户后,再校验租户状态这里有三个关键点。
第一,域名命中后不允许 Header 覆盖。
如果一个请求已经从某个 OEM 域名进来,就应该以 Host 解析结果为准。否则调用方可以通过 Header 把请求切到另一个租户。
第二,未知 Host 不回落平台。
很多系统早期会写一个方便逻辑:识别不到租户,就当平台入口。
在 OEM 里这很危险。
未知 Host 应该 fail-closed,而不是悄悄变成平台租户请求。
第三,缓存只是热路径,不是事实源。
我们会把租户解析结果缓存成结构化数据:
text
tenant_id
tenant_code
tenant_status
domain缓存 miss 时回源数据库,再回填缓存。
中间件最终会把可信租户信息写入请求上下文:
text
tenant_id
tenant_code
tenant_status后面的鉴权、模型列表、API Key、计费、账单、审计,都只认这个上下文,不认前端传来的租户参数。
这就是为什么 OEM 不能只靠前端路由和页面皮肤。
租户边界必须从入口中间件开始。
04 多租户隔离:显式 tenant scope,而不是 ORM 魔法
多租户隔离不是所有表都有 tenant_id 就结束了。
真正的隔离发生在每一次查询和写入。
我们当前没有做 ORM 全局透明注入,而是继续使用显式 tenant scope:
text
Service 从可信上下文拿 tenant_id
-> Repository 方法显式接收 tenantID
-> tenant_id IS NULL 或 tenant_id = ? 或 Admin scope-all原因是 AI Token 聚合平台里有三种不同查询语义:
tenant_id IS NULL + user_id | ||
tenant_id = ? + user_id | ||
tenant_id = ? + target_user_id | ||
如果用全局透明注入,短期省代码,长期会出现两个问题。
一个是 Admin 跨租户查询不好表达。
Admin 有些页面确实需要全局视角,但它必须走专用 Admin 路径、权限和审计,而不是偷偷关掉全局过滤。
另一个是平台数据和 OEM 数据不是同一种过滤。
平台直客是 tenant_id IS NULL。
OEM 是 tenant_id = ?。
这两个不能混在一起。
所以我们宁愿让仓储接口多传一个 tenantID,也要让每条读写路径在代码审查、测试和审计里都能看见。
这也是 OEM 文章里最容易被忽略的实现点:
隔离不是一个字段,是每条业务路径的查询语义。
05 OEM 路由能力:把 Admin、Owner、客户入口分开
当前 OEM 能力不是一个大而全的后台页面,而是一组按角色拆开的接口能力。
Admin 侧负责租户治理:
OEM Owner 侧负责租户经营:
客户侧只拥有自己的资源:
这套拆法的好处是:
Admin 不需要进入 OEM 日常运营细节。 OEM Owner 不接触平台供应商和底层成本。 下游客户不接触 OEM 后台经营信息。 Gateway 和 Billing 始终从租户上下文、用户身份和 Key 归属执行校验。
OEM 不是把所有按钮塞进一个后台。
它是把“平台治理、租户经营、客户使用、网关执行”这四层分清楚。
06 AI 模型选品:平台模型目录不等于租户可售目录
AI Token 聚合平台做 OEM 时,模型选品是最核心的商业能力之一。
平台有某个模型,不代表所有 OEM 都应该能卖这个模型。
我们当前把模型拆成两层:
text
平台 active 模型目录
-> OEM 租户模型配置
-> 上架 / 下架
-> allow_payg
-> quota_enabled
-> 租户级价格
-> 终端模型列表
-> Playground
-> OpenAI-compatible /v1/models
-> API Key allowed_models
-> Gateway 调用前模型门禁OEM 管理页返回的不是“租户已经配置过的模型列表”。
而是:
text
平台 active 模型目录
left join
租户模型配置这样一行模型里可以同时看到:
平台模型信息。 平台参考价格。 当前租户是否已选择。 当前租户价格。 PAYG 是否允许。 Quota 是否允许。 当前租户状态。
为什么要这么做?
如果只返回已配置模型,OEM 就看不到还有哪些平台模型可以上架,也没法做批量上架、下架和定价。
更重要的是,模型列表页不是最终门禁。
真正调用时,Gateway 还会按 tenant_id + model_code 再查租户模型配置:
这里最关键的是价格门禁。
PAYG 模型必须配置完整的租户价格:
价格字段全部按 decimal 处理,前端以字符串传输。
不允许用 float。
也不允许出现“模型能调用,但价格缺失导致免费打上游”的半可用状态。
模型选品不是展示功能,它是 Gateway 计费门禁的一部分。
07 客户等级:OEM 自己的等级,不复用平台会员等级
OEM 下游客户不是平台直客的子集。
他们有自己的等级、折扣、权益和运营策略。
我们当前的客户等级能力包括:
level_code | |
ai_discount_rate | |
这里有一个非常重要的边界:
AI PAYG 折扣不回退到商品折扣。
商品折扣和 AI 按量扣费是两种不同的账。
商品折扣影响 SKU、套餐、订单价。
AI PAYG 折扣只影响钱包按量扣费部分。
它不影响已经分配的 AI 配额消耗,也不应该和商品折扣混用。
Gateway 构建路由上下文时,会按当前用户和租户查客户等级:
text
tenant_id > 0
-> OEM customer level
tenant_id = 0
-> platform user level这让平台直客等级和 OEM 客户等级在运行时分开。
同名 VIP 等级也不会互相污染。
如果不拆这层,后面会出现很典型的账单争议:
text
为什么平台 VIP 折扣影响了 OEM 客户?
为什么 A 租户的 Enterprise 等级影响了 B 租户?
为什么商品折扣被拿去算 AI 按量账单?所以客户等级不是运营后台的小功能。
它是 OEM 计费解释能力的一部分。
08 客户管理:封禁、调级、代充值都必须带租户边界
OEM Owner 管客户,不能只是“用户列表加筛选”。
当前客户管理至少覆盖四条链路:
封禁这件事尤其容易做错。
很多后台只改客户列表里的状态,但登录、下单、支付、API 调用链路没有统一识别。
这样客户被封禁后,仍然可能用旧会话继续下单,或者继续支付已创建订单。
我们的口径是:
text
客户封禁
-> 用户域状态变为 banned
-> 登录链路识别
-> 订单创建链路识别
-> 支付链路再次识别
-> API 使用链路继续受 tenant/user 状态约束代充值也不是“直接把客户余额加大”。
它是一笔同租户内的钱包转账:
text
OEM operator wallet
-> wallet transfer
-> OEM customer wallet当前代充值链路会做这些校验:
金额必须为正。 租户 ID、操作人 ID、目标客户 ID 必须有效。 目标客户必须属于当前租户。 操作人不能给自己代充值。 钱包扣减和入账在同一事务里完成。 写钱包流水。 写审计事件。
这几个细节决定了代充值是不是一笔能解释的资金动作。
如果只是直接改余额,后面客户问“谁充的、从哪里扣的、为什么多了一笔钱”,平台很难给出证据。
09 钱包额度和 AI 配额池必须分开
OEM 体系里,“额度”这个词最容易混。
我们当前强制拆成两类:
text
钱包余额
钱,用于充值、代充值和 PAYG 钱包扣费
AI 配额池
用量资源,用于平台给租户发放,再由租户分配给下游客户两条链路分别是:
text
Admin 发放 OEM 钱包额度
-> OEM Owner wallet
-> 代充值 transfer
-> OEM customer wallet
Admin 发放 AI tenant quota pool
-> tenant_quota_pools
-> tenant_quota_allocations
-> AI 请求优先扣配额钱包余额解决的是资金。
AI 配额池解决的是用量资源。
它们不能放进同一张账里。
当前 AI 配额池有三张核心账:
tenant_quota_pools | |
tenant_quota_allocations | |
tenant_quota_ledger |
配额链路有几个硬约束:
发放、分配、回收、扣减都有幂等键。 配额池可分配量 = 总量 - 已分配量。 客户可回收量 = 已分配量 - 已使用量。 分配前校验目标客户属于当前租户。 扣减时锁定 allocation 和 pool,防止并发超扣。 单次 AI 消耗可以跨多个 allocation 扣减。 usage log 或后续落账失败时,可以按扣减明细恢复配额。
请求进入 AI 网关时,如果结算策略要求 quota allocation,配额不足就直接拒绝,而不是悄悄兜底钱包。
这个设计看起来保守,但它避免了一类很难解释的问题:
text
客户以为自己买的是配额
平台发现配额不够后自动扣了钱包
OEM 和客户都认为账单口径变了所以配额模式要么明确 fallback 策略,要么 fail-closed。
不能靠隐式兜底解决。
10 API Key 代管:当前隐藏,不半开放
OEM 代管下游客户 API Key 是一个很有诱惑力的功能。
渠道商会希望能替客户:
创建 Key。 禁用 Key。 设置模型白名单。 设置 spend limit。 重置 Key。 查看客户 Key 状态。
但这个能力不能半开放。
当前我们的边界是:客户 API Key 自管,OEM 代管 Key 隐藏。
也就是:
当前登录用户只能管理自己的 Key。 Key 明文只在创建时返回一次。 列表只返回 masked key。 Gateway 按 Key 绑定的 tenant/user 计费。 请求体里伪造目标客户不会改变 Key 归属。 不提供 /tenant/customers/:id/api-keys这类 OEM 代管入口。
为什么不先做一个简单版?
因为代管 Key 一旦开放,至少要补齐 6 个条件:
这里的核心不是“能不能创建 Key”。
而是:
text
谁有权替谁创建?
创建后费用归谁?
客户能不能知道这件事?
泄露后责任怎么追?
审计里能不能还原动作?在这些条件没有完整闭环之前,隐藏比半开放更稳。
这也是 OEM MVP 里很重要的产品判断:不是所有“渠道商想要”的能力都应该第一版开放。
高风险能力必须等权限、审计、密钥展示和计费边界齐了再开。
11 账本可见性:OEM 要经营报表,不等于看平台全部账
OEM 需要看经营结果。
但经营结果不等于平台全局账本。
当前可见性拆成三层:
这和前面计费引擎文章里的结论一致:
客户最终账本以平台的 usage log、charge line、钱包流水和 quota ledger 为准。
但展示给不同角色时必须裁剪。
OEM 看板应该服务经营:
客户收入。 请求量。 Token 用量。 模型分布。 Top 客户。 客户趋势。 结算和提现。
它不应该把平台内部的供应商凭证、合同成本、全局路由策略直接暴露出来。
否则 OEM 很快会从经营后台变成平台内部成本后台。
这会带来两个问题:
第一,平台商业空间被暴露。
第二,供应商和全局成本数据变成新的越权面。
所以 OEM 报表要能解释客户账单,但不能泄露平台底层账本。
12 当前 OEM 功能实现清单
下面这张表是当前这套 OEM 体系可以拿去做技术评审的实现清单。
这张表比“有没有白标页面”更适合判断 OEM 能不能上线。
只要其中几项没有收口,后面大概率会在这几类问题里付成本:
跨租户越权。 客户串价。 模型免费调用。 代充值对不上账。 配额并发超扣。 API Key 代管泄露。 OEM 报表暴露平台成本。
13 常见错误方案
错误一:把 OEM 做成一套白标前端
白标前端只能解决“看起来像谁”的问题。
OEM 要解决的是:谁能卖什么、谁能改什么、钱怎么走、账怎么看、出事后谁负责。
错误二:公开请求靠参数传 tenant_id
租户归属必须来自可信 Host、认证上下文或受信内部入口。
如果公开请求可以靠前端参数决定租户,后面的鉴权、计费和账本都会建立在不可信输入上。
错误三:平台模型默认给所有 OEM 可售
平台模型目录不等于租户可售目录。
OEM 必须独立控制模型上架、下架、价格、PAYG 和 quota 策略。
错误四:平台等级和 OEM 客户等级混用
平台直客等级属于平台。
OEM 客户等级属于租户。
混用会导致权益串租户,折扣账单也解释不清。
错误五:代充值直接改客户余额
代充值必须是一笔钱包转账。
它要有扣款方、收款方、金额、事务、流水、审计和失败处理。
错误六:把钱包额度和 AI 配额池叫成一个东西
钱包是钱。
AI 配额池是用量资源。
它们可以共同参与扣费策略,但不能混成同一张账。
错误七:提前开放 OEM 代管 API Key
没有目标客户归属校验、masked key、审计、模型白名单和 spend limit 校验时,代管 Key 是高风险能力。
先隐藏,比半开放更稳。
错误八:OEM 看板直接展示平台上游成本
OEM 需要经营分析,不等于可以看平台全部成本和供应商信息。
平台账、OEM 账、客户账要分层。
14 最后
OEM 分销不是换 Logo。
它是一套完整的租户化经营系统:
text
入口要可信
数据要隔离
模型要选品
价格要完整
客户要能运营
资金要能追踪
配额要能恢复
Key 要有边界
账本要分视角做 AI Token 聚合平台时,OEM 是一个很自然的商业扩展方向。
但它不是前端项目,也不是一个字段改造。
它要求平台把网关、模型、计费、钱包、配额、账本和权限都变成租户化能力。
下一篇我会继续拆平台高可用和 LLM 渠道高可用设计:
为什么 Token 聚合平台不能只靠单实例网关和单渠道 provider,而要同时设计网关无状态、预算预留幂等、渠道健康检查、自动降级、Fallback 和对账证据。
这个系列会继续围绕企业级 Token 聚合平台,把网关、路由、计费、对账、OEM 和上线门禁拆成可以复用的工程清单。
夜雨聆风