Enterprise 版本
Billing — 深入參考
本頁是 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 之下。標記為 substrate 的列位於 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 與 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 | 由 repository 定義的失敗會向外傳播 | 在同一週期內重新武裝警示 |
AlertStateRepositoryInterface | hasAlertFired()、markAlertFired()、clearForPeriod() | 耐久的警示去重持久化合約 | 依各方法而定 | 由實作定義 | 運維人員負責跨複本的耐久性 |
InMemoryAlertStateRepository | — | 以陣列支撐的觸發狀態 | 依介面而定 | 不會拋出 | 僅供單一請求生命週期與測試 |
QuotaExceededException | 唯讀的 currentCu、limitCu、resetsAt、tenantId、isSaaS | 感知部署模式的配額拒絕 | — | 即為該可拋出物 | httpStatusCode() 為 402 SaaS / 403 on-prem;specCode() 為 SPEC-BILLING-003 / SPEC-LIC-001;toErrorEnvelope() 產生結構化的錯誤主體 |
DeploymentMode(enum) | — | saas、self_hosted_oss、local_development | — | 不會拋出 | Substrate. enforcesQuota() 僅對 Saas 為真;退出一律為明確指定 |
QuotaEnforcementGuard::__construct | DeploymentMode、PlanResolverInterface、QuotaManager、UsageCounterStoreInterface | 組裝實際的配額閘門 | 新實例 | 不會拋出 | Substrate. final readonly |
QuotaEnforcementGuard::enforce | ?TenantContext $tenant、non-empty-string $featureKey、float $amount = 1.0 | fail-closed 的配額閘門,具原子式預留 | QuotaDecision(僅允許結果) | 參見下方的拒絕分類 | Substrate. 掛載於租戶認證之後、計費處理常式之前 |
PlanResolverInterface::resolve | TenantContext $tenant | 將租戶解析為其方案與逐功能原則 | ResolvedPlan | NoPlanForTenantException | Substrate. 為未知租戶提供預設方案回退是一種缺陷 |
RegistryPlanResolver | array<non-empty-string, ResolvedPlan> $plansByTenant | 以 map 支撐的解析器 | ResolvedPlan | 對未對應的租戶拋出 NoPlanForTenantException | Substrate. 依建構本質即為 fail-closed |
ResolvedPlan::policyFor | non-empty-string $featureKey | 在已解析方案上查找原則 | ?QuotaPolicy | 不會拋出 | Substrate. null 代表未知功能;閘門會拒絕它 |
QuotaPolicy | non-empty-string $featureKey、float $limit、OveragePolicy $overagePolicy | 逐功能的上限與違規原則 | — | 不會拋出 | Substrate. UNLIMITED = -1.0;0.0 的上限代表零額度,而非無限制;isUnlimited()、isBlocking() |
QuotaDecision | 靜態方法 bypassed()、unlimited()、consumed() | 允許結果的值物件 | QuotaDecision | 不會拋出 | Substrate. isAllowed() 恆為真;每次拒絕改以拋出處理 |
UsageCounter | 列快照:租戶、功能、週期界限、used、limit、updatedAt | 不可變的用量列 | — | 不會拋出 | Substrate. remaining() 可能為負;wouldExceed() 為嚴格判定 |
UsageCounterStoreInterface::get | 租戶、功能、週期界限、float $limit | 讀取用量列,不存在時以 used = 0 建立 | UsageCounter | UsageStoreUnavailableException | Substrate. 後端失敗時絕不回傳假值 |
UsageCounterStoreInterface::tryConsume | 租戶、功能、週期界限、float $amount、float $limit | 在上限內進行原子式 compare-and-set 預留 | ?UsageCounter(預留會突破上限時為 null) | UsageStoreUnavailableException | Substrate. 必須是對後端儲存的單一原子操作 |
InMemoryUsageCounterStore | — | 儲存合約的行程內參考實作 | 依介面而定 | 依介面而定 | Substrate. 僅限單一行程;用於記載原子性不變式 |
QuotaEnforcementException(abstract) | — | 每個 substrate 拒絕的基底型別 | — | 即為該可拋出物族系 | Substrate. 每個子型別都宣告 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(on-prem) | 阻擋原則的配額被超過,或並行預留耗盡了最後的餘裕 |
行為合約
標題為「行為合約」的區段- 預設註冊表隨附三個層級(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;soft-stop 回應應將其公告為重試期限。
QuotaEnforcementGuard在 SaaS 模式下為 fail-closed。缺少租戶、缺少方案、未知功能、儲存中斷與配額突破全都會拒絕;沒有任何情況會落入隱含的允許。非 SaaS 部署只能透過以非 SaaS 的DeploymentMode建構閘門來退出。- 阻擋原則透過
UsageCounterStoreInterface::tryConsume(一種原子式 compare-and-set)預留用量。並行請求無法合力將用量推過上限;競態中落敗的一方即使通過預先檢查,仍會收到QuotaExceededException。 - 在預算警示原則下,閘門盡力記錄消耗且絕不拒絕;超過軟性上限的預留仍會將該列記錄在上限值。
QuotaExceededException感知部署模式:SaaS 拒絕對應到 HTTP 402、規格碼SPEC-BILLING-003,並標記為可重試;on-prem 拒絕對應到 HTTP 403、SPEC-LIC-001。- 此程式庫本身不發出 HTTP 回應。所宣告的狀態碼是給邊緣層的合約,邊緣層負責將拋出的拒絕對應為回應,且不得呼叫計費處理常式。
邊界案例與失敗模式
標題為「邊界案例與失敗模式」的區段- 非正的包含配額。
usagePercentage()、evaluate()與OverageCalculator::calculate()都會產生0.0的使用比率,而非除以零。此時門檻警示不會僅因比率而觸發。 - 預算警示加上大幅超額。 管理員與閘門都會回傳允許結果。不要將「沒有例外」視為「在配額內」的證明;請查閱
OverageResult或警示串流。 - 剛好達到上限。
checkQuota()在currentCu == includedCuQuota時通過。BudgetExceeded需嚴格超額。UsageCounter::wouldExceed()同樣為嚴格判定。 MonthlyCapReached。 此 enum 宣告了這第四種警示類型,但BillingAlertService::evaluate()從不發出它;其候選清單只涵蓋三種門檻警示。它保留給此模組外的上限追蹤發出者使用。- 重複的層級定義。
PlanRegistry依層級值索引;某層級的最後一筆定義會靜默取代先前的定義。請以去重後的清單建構註冊表。 - 零額度相對於無限制。
QuotaPolicy的上限為0.0代表該週期內的每次消耗都是超額。只有負的UNLIMITED哨兵值會停用計量;isUnlimited()絕不阻擋。 - 非正的預留數量。
enforce()會以 fail-closed 方式拒絕非正的$amount,拋出UsageStoreUnavailableException(503)。這是呼叫端的缺陷,而非儲存中斷。 - 儲存中斷。 任何讀取或預留失敗都會以
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 定義的語意。soft-stop 的重試期限(resetsAt)是邊緣層應作為 Retry-After 指引呈現的值。發出實際的 HTTP 回應、標頭與快取行為是託管應用程式的責任。
開發注意事項
標題為「開發注意事項」的區段- 以
PlanRegistry::defaultRegistry()、單一OveragePolicy與一個QuotaManager組成模型;若需警示,再加上搭配耐久AlertStateRepositoryInterface實作的BillingAlertService。 - 將
QuotaEnforcementGuard掛載於請求管線中租戶認證之後、計費處理常式之前。在邊緣捕捉QuotaEnforcementException與計費的QuotaExceededException,並將httpStatusCode()對應到回應。 - 此模組中的方案定義是計費的單一事實來源;請勿在部署的其他處維護任何平行的計費定義。
- 記憶體內實作讓整個介面無需 I/O 即可進行單元測試。建議的邊界測試:使用量剛好達到配額、超過一個單位、比率門檻在 0.8 與 1.0、方案不符防護、CAS 競態(兩筆預留爭奪最後一個單位的餘裕),以及儲存中斷拒絕。
- 核心模型類別標記
@since 2.2.0;substrate 標記@since 2.3.0。目前的套件版本線為 3.1.0。 - 運維人員負責警示狀態 repository 與用量儲存的實作、其跨複本的耐久性,以及透過
clearAlerts()進行的任何週期中途警示重新武裝。
出版邊界
標題為「出版邊界」的區段本頁僅記載外部可觀察的行為與受支援的公開 API 介面。內部命名空間路徑、輔助類別、機制表、runbook 檔名與工單前綴皆不在範圍內。