Enterprise 版本
计费 — 深度参考
本页是 NextPDF Enterprise 计费表面的深度参考。该表面分为两层。NextPDF\Enterprise\Billing 中的计费模型定义计划层级、配额、超额策略与去重的用量告警。NextPDF\Enterprise\Billing\Substrate 中的强制执行基底将该模型置于实时请求路径上,既 fail-closed 又并发安全。入口点为 PlanRegistry、QuotaManager、OverageCalculator、BillingAlertService 与 QuotaEnforcementGuard。关于工作流层面的指南,请参阅 Billing 能力页面。
可用性与授权
标题为“可用性与授权”的章节此能力随 NextPDF Enterprise(nextpdf/enterprise)发行,并以一个 Enterprise 层级的授权信封激活。没有该权益的部署不会加载此能力的类。比较版本并获取授权。
Billing 是一项 Enterprise 基础能力,没有单独的每功能标志;一旦将 Enterprise 软件包安装在 Core 软件包旁边即可用。NextPDF Core(Apache-2.0)与 NextPDF Pro 没有计划、配额或超额模型;此表面没有更低层级的等价物。计划包含项、配额与商业条款由授权协议管辖,而非由运行时强制执行;本参考不是法律或合同意见。
公共 API 接口面
标题为“公共 API 接口面”的章节所有符号都位于 NextPDF\Enterprise\Billing 之下。标记为 基底 的行位于 NextPDF\Enterprise\Billing\Substrate 之下。TenantContext 是来自 NextPDF\Enterprise\SaaS 的经过认证的租户类型。
| 符号 | 参数 | 默认行为 | 返回 | 抛出或失败于 | 备注 |
|---|---|---|---|---|---|
SaaSPlan (enum) | — | 字符串支持的计划层级:standard、advanced、high_control | — | 不抛出 | label() 返回显示名称 |
PlanDefinition::__construct | SaaSPlan $plan, float $includedCuQuota, list<CapabilityCode> $capabilities, non-empty-string $priceTier, bool $intelligencePackIncluded, bool $privacyPackIncluded | 不可变的计划值对象;按原样存储输入 | 新实例 | 不抛出 | final readonly;提升的 public 属性 |
PlanDefinition::includesCapability | CapabilityCode $capability | 严格恒等成员检查 | bool | 不抛出 | — |
PlanRegistry::__construct | list<PlanDefinition> $definitions | 按层级索引定义;每个层级以最后一个定义为准 | 新注册表 | 不抛出 | 用于测试与白标计划集 |
PlanRegistry::get | SaaSPlan $plan | 规范的计划查找 | PlanDefinition | 当计划未注册时抛出 InvalidArgumentException | — |
PlanRegistry::has | SaaSPlan $plan | 注册探测 | bool | 不抛出 | — |
PlanRegistry::defaultRegistry (static) | — | 生产默认值:Standard 1,000 CU;Advanced 5,000 CU 加 Intelligence Pack;High Control 20,000 CU 加 Intelligence 与 Privacy Pack | PlanRegistry | 不抛出 | 除非合同条款要求自定义定义,否则使用之 |
OveragePolicy (enum) | — | hard_stop, soft_stop, budget_alert | — | 不抛出 | httpStatusCode() 映射 402 / 429 / 200;isBlocking() 仅对 hard stop 与 soft stop 为真 |
QuotaManager::__construct | PlanRegistry $planRegistry, OveragePolicy $overagePolicy | 将注册表绑定到一个策略 | 新实例 | 不抛出 | — |
QuotaManager::checkQuota | TenantContext $tenant, SaaSPlan $plan, float $currentCu | 在配额之内或恰好等于配额时、或在非阻塞策略下静默返回 | void | 在阻塞策略下严格超额时抛出 QuotaExceededException;对未注册计划由注册表抛出 InvalidArgumentException | resetsAt = 下月第一天,UTC 午夜 |
QuotaManager::remainingQuota | SaaSPlan $plan, float $currentCu | 纯读取;从不阻塞 | float | 注册表 InvalidArgumentException | 超额时为负 |
QuotaManager::usagePercentage | SaaSPlan $plan, float $currentCu | 纯读取;从不阻塞 | float | 注册表 InvalidArgumentException | 当包含配额为非正时返回 0.0;超额时高于 1.0 |
OverageCalculator::calculate | PlanDefinition $plan, float $currentCu | 计算一个不可变的超额快照 | OverageResult | 不抛出 | final readonly,无状态 |
OverageResult | includedCu, usedCu, overageCu, usageRatio, isOverage | 不可变的计算结果 | — | 不抛出 | overageCu = max(0, used - included);isOverage 需要严格超额 |
BillingAlertType (enum) | — | quota_warning_80, quota_warning_100, budget_exceeded, monthly_cap_reached | — | 不抛出 | threshold() 0.8 / 1.0 / 1.0 / 1.0;severity() warning / critical / critical / critical |
BillingAlertService::__construct | AlertStateRepositoryInterface $alertState | 绑定去重存储 | 新实例 | 不抛出 | — |
BillingAlertService::evaluate | TenantContext $tenant, SaaSPlan $plan, PlanDefinition $planDef, float $currentCu | 按阈值升序触发尚未触发的告警并记录之 | list<BillingAlertType> | 计划与定义不匹配时抛出 InvalidArgumentException | 去重键:租户、类型、UTC YYYY-MM 周期 |
BillingAlertService::clearAlerts | TenantContext $tenant | 清除该租户在当前 UTC 周期的已触发状态 | void | 仓库定义的失败会向上传播 | 在同一周期内重新武装告警 |
AlertStateRepositoryInterface | hasAlertFired(), markAlertFired(), clearForPeriod() | 持久的告警去重持久化契约 | 按方法而定 | 由实现定义 | 运维方负责跨副本的耐久性 |
InMemoryAlertStateRepository | — | 数组支持的已触发状态 | 按接口而定 | 不抛出 | 仅限单请求生命周期与测试 |
QuotaExceededException | 只读 currentCu, limitCu, resetsAt, tenantId, isSaaS | 感知部署模式的配额拒绝 | — | 即该 throwable | httpStatusCode() 402 SaaS / 403 本地部署;specCode() SPEC-BILLING-003 / SPEC-LIC-001;toErrorEnvelope() 产出一个结构化的错误体 |
DeploymentMode (enum) | — | saas, self_hosted_oss, local_development | — | 不抛出 | 基底。 enforcesQuota() 仅对 Saas 为真;退出始终是显式的 |
QuotaEnforcementGuard::__construct | DeploymentMode, PlanResolverInterface, QuotaManager, UsageCounterStoreInterface | 组装实时配额闸门 | 新实例 | 不抛出 | 基底。 final readonly |
QuotaEnforcementGuard::enforce | ?TenantContext $tenant, non-empty-string $featureKey, float $amount = 1.0 | 带原子预留的 fail-closed 配额闸门 | QuotaDecision(仅允许的结果) | 参见下方的拒绝分类 | 基底。 挂载于租户认证之后、计费处理器之前 |
PlanResolverInterface::resolve | TenantContext $tenant | 将租户解析为其计划与每功能策略 | ResolvedPlan | NoPlanForTenantException | 基底。 为未知租户提供默认计划回退属于缺陷 |
RegistryPlanResolver | array<non-empty-string, ResolvedPlan> $plansByTenant | 基于映射的解析器 | ResolvedPlan | 对未映射的租户抛出 NoPlanForTenantException | 基底。 从构造上即 fail-closed |
ResolvedPlan::policyFor | non-empty-string $featureKey | 在已解析计划上查找策略 | ?QuotaPolicy | 不抛出 | 基底。 null 表示未知功能;守卫予以拒绝 |
QuotaPolicy | non-empty-string $featureKey, float $limit, OveragePolicy $overagePolicy | 每功能的限额与超限策略 | — | 不抛出 | 基底。 UNLIMITED = -1.0;0.0 限额表示零额度,而非无限;isUnlimited(), isBlocking() |
QuotaDecision | 静态方法 bypassed(), unlimited(), consumed() | 允许结果的值对象 | QuotaDecision | 不抛出 | 基底。 isAllowed() 始终为真;每次拒绝都改为抛出 |
UsageCounter | 行快照:租户、功能、周期边界、used, limit, updatedAt | 不可变的用量行 | — | 不抛出 | 基底。 remaining() 可能为负;wouldExceed() 是严格的 |
UsageCounterStoreInterface::get | 租户、功能、周期边界、float $limit | 读取用量行,缺失时以 used = 0 创建 | UsageCounter | UsageStoreUnavailableException | 基底。 后端失败时绝不返回假值 |
UsageCounterStoreInterface::tryConsume | 租户、功能、周期边界、float $amount, float $limit | 在限额内进行原子的比较并设置预留 | ?UsageCounter(当预留会突破限额时为 null) | UsageStoreUnavailableException | 基底。 必须是针对后端存储的单个原子操作 |
InMemoryUsageCounterStore | — | 存储契约的进程内参考实现 | 按接口而定 | 按接口而定 | 基底。 仅限单进程;用于说明原子性不变量 |
QuotaEnforcementException (abstract) | — | 每个基底拒绝的基类型 | — | 即该 throwable 家族 | 基底。 每个子类型声明 httpStatusCode() |
public function checkQuota(TenantContext $tenant, SaaSPlan $plan, float $currentCu): voidpublic function evaluate( TenantContext $tenant, SaaSPlan $plan, PlanDefinition $planDef, float $currentCu,): arraypublic function enforce(?TenantContext $tenant, string $featureKey, float $amount = 1.0): QuotaDecisionpublic function tryConsume( string $tenantId, string $featureKey, DateTimeImmutable $periodStart, DateTimeImmutable $periodEnd, float $amount, float $limit,): ?UsageCounter;QuotaEnforcementGuard::enforce 的拒绝分类
| 异常 | HTTP 状态 | 触发条件 |
|---|---|---|
MissingTenantContextException | 401 | SaaS 模式下没有经过认证的租户上下文 |
NoPlanForTenantException | 402 | 解析器找不到分配给该租户的计划 |
UnknownFeatureException | 402 | 已解析计划未为该功能键定义任何策略 |
UsageStoreUnavailableException | 503 | 无法读取用量存储或无法原子更新;对非正的 $amount 也会触发 |
QuotaExceededException | 402(SaaS)/ 403(本地部署) | 阻塞策略的配额被超过,或某个并发预留消耗了最后的余量 |
行为契约
标题为“行为契约”的章节- 默认注册表发行三个层级(Standard / Advanced / High Control),具有递增的 CU 配额与能力集合。一个未注册计划的请求会以一个显式的
InvalidArgumentException失败。 QuotaManager::checkQuota()仅在两个条件都成立时才触发:策略是阻塞的,且当前用量严格高于包含配额。预算告警策略从不触发;超额通过告警发出信号。remainingQuota()与usagePercentage()是纯读取,从不阻塞。剩余配额在超额时变为负值;用量百分比在超额时超过1.0。- 告警按阈值升序评估:80% 警告、100% 警告(critical),然后是预算超出(critical)。预算超出以严格超额为闸门;恰好为 100% 的用量触发 100% 警告,而非预算超出。
- 每种告警类型在每个租户每个计费周期内最多触发一次。已触发状态通过
AlertStateRepositoryInterface记录,因此去重的持久性取决于所选实现。 - 去重键内嵌 UTC
YYYY-MM周期。因此一个新的日历月会自动重新武装每种告警类型;滚动重新武装无需任何清除调用。clearAlerts()清除当前周期,从而在周期中途重新武装告警,例如在一次计划升级之后。 evaluate()中的一道计划不匹配守卫会拒绝所提供计划与计划定义不一致的调用,从而防范一个来自不同于该租户计划层级的定义。- 所有周期运算都锚定于 UTC。配额超出的重置时刻是下一个日历月第一天的 UTC 午夜;一个软停止响应应当将其宣告为重试期限。
QuotaEnforcementGuard在 SaaS 模式下是 fail-closed 的。缺失租户、缺失计划、未知功能、存储中断与配额突破都会拒绝;不会有任何情形穿透为隐式允许。非 SaaS 部署只能通过以一个非 SaaS 的DeploymentMode构造守卫来退出。- 阻塞策略通过
UsageCounterStoreInterface::tryConsume(一个原子的比较并设置)预留用量。并发请求无法共同将用量推过限额;即便预检通过,竞争的失败者也会收到QuotaExceededException。 - 在预算告警策略下,守卫尽力记录消耗且从不拒绝;一个越过软上限的预留仍会以限额记录该行。
QuotaExceededException感知部署模式:SaaS 拒绝映射到 HTTP 402,携带规范码SPEC-BILLING-003并标记为可重试;本地部署拒绝映射到 HTTP 403,携带SPEC-LIC-001。- 该库本身不发出 HTTP 响应。所声明的状态码是面向边缘层的契约——由边缘层将一个抛出的拒绝映射为一个响应,并且必须不调用计费处理器。
边界情形与失败模式
标题为“边界情形与失败模式”的章节- 非正的包含配额。
usagePercentage()、evaluate()与OverageCalculator::calculate()都会产出一个0.0的用量比率,而非除以零。此后阈值告警绝不会仅凭比率触发。 - 预算告警加上大量超额。 管理器与守卫都会返回允许的结果。不要将异常的缺失视为处于配额之内的证明;请查阅
OverageResult或告警流。 - 恰好在限额上。 在
currentCu == includedCuQuota时checkQuota()通过。BudgetExceeded需要严格超额。UsageCounter::wouldExceed()同样是严格的。 MonthlyCapReached。 该枚举声明了这第四种告警类型,但BillingAlertService::evaluate()从不发出它;其候选列表只涵盖三种阈值告警。它保留给本模块之外的上限跟踪发出方。- 重复的层级定义。
PlanRegistry按层级值索引;一个层级的最后一个定义会静默替换更早的定义。请从一个去重后的列表构造注册表。 - 零额度对比无限。 一个
0.0的QuotaPolicy限额意味着周期内的每一次消耗都是超额。只有负的UNLIMITED哨兵值才会禁用计量;isUnlimited()从不阻塞。 - 非正的预留数量。
enforce()会以UsageStoreUnavailableException(503)fail-closed 地拒绝一个非正的$amount。这是一个调用方缺陷,而非存储中断。 - 存储中断。 任何读取或预留失败都会以
UsageStoreUnavailableException浮现并拒绝。守卫绝不会在计量器停机时允许未计量的工作。 - 内存实现。
InMemoryAlertStateRepository与InMemoryUsageCounterStore仅在单个 PHP 进程内是正确的。多副本部署必须提供由具备真正原子性的数据存储支持的实现;一个先读后写的存储属于缺陷,会在负载下允许超配额。 - FIPS 模式。 Billing 本身不执行任何密码学操作,也没有 FIPS 特有的行为。它所消费的租户身份必须来源于一个经过认证的上下文,其 FIPS 姿态随 SaaS 表面记录。
符合性
标题为“符合性”的章节| 声明 | 标准 | 条款 |
|---|---|---|
| 402 状态码保留供将来使用;它自身不携带任何规范性的请求语义。 | RFC 9110 | §15.5.3 |
| 429 表示客户端在给定时间内发送了过多请求(“速率限制”)。 | RFC 6585 | §4 |
| Retry-After 指示用户代理在发起后续请求之前应当等待多久。 | RFC 9110 | §10.2.3 |
所有条款均为转述;NextPDF 不复现规范性文本。NextPDF 不为此表面作出任何 HTTP 协议符合性或认证声明。 由 OveragePolicy::httpStatusCode() 声明的 402 / 429 / 200 映射,以及守卫的 401 / 402 / 503 拒绝码,是一种与上述条款对齐的产品约定:RFC 9110 保留 402,因此此处将其用于付款拒绝是常见的行业约定,而非 IETF 定义的语义。软停止的重试期限(resetsAt)是边缘层应作为 Retry-After 指引浮现的值。发出实际的 HTTP 响应、头部与缓存行为是托管应用的职责。
开发说明
标题为“开发说明”的章节- 从
PlanRegistry::defaultRegistry()、一个OveragePolicy和一个QuotaManager组合出模型;为告警添加带有持久AlertStateRepositoryInterface实现的BillingAlertService。 - 将
QuotaEnforcementGuard挂载在请求管线中租户认证之后、计费处理器之前。在边缘捕获QuotaEnforcementException与 billing 的QuotaExceededException,并将httpStatusCode()映射到响应。 - 本模块中的计划定义是计费的唯一真实来源;不要在部署的别处维护一份并行的计费定义。
- 内存实现使整个表面无需 I/O 即可单元测试。推荐的边界测试:用量恰好等于配额、高出一个单位、0.8 与 1.0 处的比率阈值、计划不匹配守卫、CAS 竞争(针对最后一个单位余量的两次预留),以及存储中断拒绝。
- 核心模型类携带
@since 2.2.0;基底携带@since 2.3.0。当前软件包线为 3.1.0。 - 运维方拥有告警状态仓库与用量存储的实现、它们跨副本的耐久性,以及通过
clearAlerts()进行的任何周期中途告警重新武装。
发布边界
标题为“发布边界”的章节本页仅记录外部可观察的行为与受支持的公共 API 表面。内部命名空间路径、辅助类、机制表、运行手册文件名与工单前缀不在范围内。