diff --git a/backend/src/classroom-cleanup.js b/backend/src/classroom-cleanup.js index ca22887..67993d3 100644 --- a/backend/src/classroom-cleanup.js +++ b/backend/src/classroom-cleanup.js @@ -50,16 +50,31 @@ function cleanupClassroomData() { return { sessions: ids.size, strokes, files }; } +/* Ретеншн error_log: не копим бесконечно (период app_settings.error_log_retention_days, + по умолчанию 30; 0 = выключено). */ +function cleanupErrorLog() { + let days = 30; + try { + const r = db.prepare("SELECT value FROM app_settings WHERE key='error_log_retention_days'").get(); + if (r && r.value != null) { const n = Number(r.value); if (Number.isFinite(n) && n >= 0) days = n; } + } catch (e) {} + if (!days) return 0; + try { return db.prepare("DELETE FROM error_log WHERE created_at < datetime('now', ?)").run('-' + days + ' days').changes; } + catch (e) { return 0; } +} + /* Раз в сутки + один прогон через минуту после старта. unref — не держит процесс. */ function schedule() { const run = () => { try { const r = cleanupClassroomData(); if (r && (r.strokes || r.files)) logger.info('classroom-cleanup', r); + const errLogged = cleanupErrorLog(); + if (errLogged) logger.info('error-log-cleanup', { deleted: errLogged }); } catch (e) {} }; setTimeout(run, 60_000).unref(); setInterval(run, 24 * 60 * 60 * 1000).unref(); } -module.exports = { cleanupClassroomData, schedule }; +module.exports = { cleanupClassroomData, cleanupErrorLog, schedule }; diff --git a/backend/src/controllers/imggenController.js b/backend/src/controllers/imggenController.js index 4968161..254e6a4 100644 --- a/backend/src/controllers/imggenController.js +++ b/backend/src/controllers/imggenController.js @@ -9,11 +9,26 @@ const db = require('../db/db'); const { isFeatureEnabledForUser } = require('../middleware/features'); const GEN_DIR = path.join(__dirname, '../../uploads/generated'); -const _cooldown = new Map(); // userId → last ts (антиспам) -const _daily = new Map(); // userId → { day, count } +const _cooldown = new Map(); // userId → last ts (антиспам, короткоживущий, ок в памяти) const COOLDOWN_DEFAULT = 4000; const DAILY_DEFAULT = 40; +// Дневной счётчик — в БД (imggen_usage), переживает рестарт. +function _dailyCount(uid, day) { + try { const r = db.prepare('SELECT count FROM imggen_usage WHERE user_id=? AND day=?').get(uid, day); return r ? r.count : 0; } + catch (e) { return 0; } +} +function _dailyBump(uid, day) { + try { db.prepare('INSERT INTO imggen_usage (user_id, day, count) VALUES (?,?,1) ON CONFLICT(user_id, day) DO UPDATE SET count = count + 1').run(uid, day); } + catch (e) {} +} +// Чистка in-memory cooldown (старше минуты) + старых строк imggen_usage — не растём бесконечно. +setInterval(() => { + const cut = Date.now() - 60_000; + for (const [k, t] of _cooldown) if (t < cut) _cooldown.delete(k); + try { db.prepare("DELETE FROM imggen_usage WHERE day < date('now','-7 days')").run(); } catch (e) {} +}, 5 * 60_000).unref(); + function _cfg() { try { const r = db.prepare("SELECT value FROM app_settings WHERE key='imggen_provider'").get(); return r ? JSON.parse(r.value) : null; } catch (e) { return null; } } @@ -132,13 +147,12 @@ async function generate(req, res) { const L = _limits(); if (now - (_cooldown.get(uid) || 0) < L.cooldownMs) return res.status(429).json({ error: 'Чуть помедленнее — подожди пару секунд' }); const today = new Date().toISOString().slice(0, 10); - const d = _daily.get(uid); - if (L.dailyCap > 0 && d && d.day === today && d.count >= L.dailyCap) return res.status(429).json({ error: 'Дневной лимит генераций исчерпан, попробуй завтра' }); + if (L.dailyCap > 0 && _dailyCount(uid, today) >= L.dailyCap) return res.status(429).json({ error: 'Дневной лимит генераций исчерпан, попробуй завтра' }); _cooldown.set(uid, now); try { const out = await generateImage(prompt, uid); - _daily.set(uid, { day: today, count: (d && d.day === today ? d.count : 0) + 1 }); + _dailyBump(uid, today); res.json({ url: out.url }); } catch (e) { res.status(502).json({ error: e.message || 'Не удалось сгенерировать', detail: e.detail }); diff --git a/backend/src/db/migrations/070_imggen_usage.sql b/backend/src/db/migrations/070_imggen_usage.sql new file mode 100644 index 0000000..cee0930 --- /dev/null +++ b/backend/src/db/migrations/070_imggen_usage.sql @@ -0,0 +1,8 @@ +-- Дневной счётчик генераций картинок на пользователя (переживает рестарт, +-- в отличие от прежней in-memory Map). Лимит применяется в imggenController. +CREATE TABLE IF NOT EXISTS imggen_usage ( + user_id INTEGER NOT NULL, + day TEXT NOT NULL, -- 'YYYY-MM-DD' + count INTEGER NOT NULL DEFAULT 0, + PRIMARY KEY (user_id, day) +);