export interface AcquisitionBudget { maxNewTitlesPerDay?: number; maxBytesPerDay?: number; maxConcurrent?: number; } export interface BudgetUsage { newTitlesToday: number; bytesToday: number; /** 已预留但尚未计入 bytesToday 的计划字节 */ reservedBytesToday?: number; activeDownloads: number; } export interface BudgetRequest { countsAsNewTitle: boolean; bytes?: number; } export interface BudgetViolation { kind: "new_titles" | "bytes" | "concurrency"; limit: number; current: number; requested: number; } export interface BudgetDecision { decision: "allow" | "deny" | "unknown"; violations: BudgetViolation[]; /** 有字节预算但候选体积未知 */ unknown: "bytes"[]; } /** 预算检查(每日新增作品数、下载字节、并发)。已知超限 → deny;体积未知 → unknown(不放行) */ export function checkBudget(budget: AcquisitionBudget, usage: BudgetUsage, req: BudgetRequest): BudgetDecision { const violations: BudgetViolation[] = []; const unknown: "bytes"[] = []; if ( req.countsAsNewTitle && budget.maxNewTitlesPerDay !== undefined && usage.newTitlesToday + 1 > budget.maxNewTitlesPerDay ) violations.push({ kind: "new_titles", limit: budget.maxNewTitlesPerDay, current: usage.newTitlesToday, requested: 1, }); if (budget.maxBytesPerDay !== undefined) { const current = usage.bytesToday + (usage.reservedBytesToday ?? 0); if (req.bytes === undefined) unknown.push("bytes"); else if (current + req.bytes > budget.maxBytesPerDay) violations.push({ kind: "bytes", limit: budget.maxBytesPerDay, current, requested: req.bytes }); } if (budget.maxConcurrent !== undefined && usage.activeDownloads + 1 > budget.maxConcurrent) violations.push({ kind: "concurrency", limit: budget.maxConcurrent, current: usage.activeDownloads, requested: 1 }); const decision = violations.length > 0 ? "deny" : unknown.length > 0 ? "unknown" : "allow"; return { decision, violations, unknown }; }