function asQuotaNumber(value) { return Number(value ?? 0); } export function computeUserSpaceQuotaStats(space = {}) { const quotaBytes = asQuotaNumber(space.quota_bytes); const usedBytes = asQuotaNumber(space.used_bytes); const reservedBytes = asQuotaNumber(space.reserved_bytes); const occupiedBytes = usedBytes + reservedBytes; const availableBytes = quotaBytes - occupiedBytes; return { quotaBytes, usedBytes, reservedBytes, occupiedBytes, availableBytes, isOverQuota: occupiedBytes >= quotaBytes, }; } /** * Page write grace policy: * - Already at/above quota: block the next page write entirely. * - Below quota but insufficient headroom: allow this write (may exceed quota). * - Sufficient headroom: allow normally. */ export function evaluatePageWriteQuota(space, requiredBytes) { const stats = computeUserSpaceQuotaStats(space); const required = Math.max(0, asQuotaNumber(requiredBytes)); if (stats.isOverQuota) { return { allowed: false, grace: false, stats, requiredBytes: required, reason: 'over_quota', }; } if (stats.availableBytes >= required) { return { allowed: true, grace: false, stats, requiredBytes: required, reason: 'sufficient', }; } return { allowed: true, grace: true, stats, requiredBytes: required, reason: 'grace_write', }; } export function buildPageWriteQuotaError( evaluation, { messagePrefix = '剩余空间不足' } = {}, ) { const { stats, requiredBytes, reason } = evaluation; const message = reason === 'over_quota' ? '空间已满,请先清理后再创建或修改页面' : messagePrefix; return Object.assign(new Error(message), { code: 'quota_exceeded', details: { requiredBytes, availableBytes: Math.max(0, stats.availableBytes), usedBytes: stats.usedBytes, quotaBytes: stats.quotaBytes, overQuota: reason === 'over_quota', graceWrite: reason === 'grace_write', }, }); } export function assertPageWriteQuota(space, requiredBytes) { const evaluation = evaluatePageWriteQuota(space, requiredBytes); if (!evaluation.allowed) { throw buildPageWriteQuotaError(evaluation); } return evaluation; } export function isMindSpacePageWriteRelativePath(relativePath) { const normalized = String(relativePath ?? '').replace(/\\/g, '/').trim(); if (!normalized.toLowerCase().endsWith('.html')) return false; const zone = normalized.split('/')[0]?.toLowerCase(); return zone === 'public' || zone === 'draft'; } export async function loadUserSpaceForQuota(pool, userId) { if (!pool || !userId) return null; const [rows] = await pool.query( `SELECT id, quota_bytes, used_bytes, reserved_bytes, status FROM h5_user_spaces WHERE user_id = ? LIMIT 1`, [userId], ); return rows[0] ?? null; } export async function assertPageWriteQuotaForUser(pool, userId, requiredBytes) { const space = await loadUserSpaceForQuota(pool, userId); if (!space || space.status !== 'active') { throw Object.assign(new Error('用户空间不可用'), { code: 'space_unavailable' }); } return assertPageWriteQuota(space, requiredBytes); }