From ccfb151eca6ab44d7239a8d4973da102aebec44e Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Fri, 12 Jun 2026 23:00:36 +0300 Subject: [PATCH] =?UTF-8?q?fix(reliability):=20=D0=B4=D0=BD=D0=B5=D0=B2?= =?UTF-8?q?=D0=BD=D0=BE=D0=B9=20=D0=BB=D0=B8=D0=BC=D0=B8=D1=82=20imggen=20?= =?UTF-8?q?=D0=B2=20=D0=91=D0=94=20+=20=D1=80=D0=B5=D1=82=D0=B5=D0=BD?= =?UTF-8?q?=D1=88=D0=BD=20error=5Flog=20(=D0=A1=D0=BF=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D1=823)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - imggen: дневной счётчик генераций перенесён из in-memory Map в таблицу imggen_usage (миграция 070) — переживает рестарт. Cooldown остаётся в памяти, но добавлена периодическая чистка Map + старых строк imggen_usage (>7 дн). - classroom-cleanup: ретеншн error_log (app_settings.error_log_retention_days, по умолч. 30; 0 = выкл) в том же суточном джобе. Co-Authored-By: Claude Opus 4.8 --- backend/src/classroom-cleanup.js | 17 ++++++++++++- backend/src/controllers/imggenController.js | 24 +++++++++++++++---- .../src/db/migrations/070_imggen_usage.sql | 8 +++++++ 3 files changed, 43 insertions(+), 6 deletions(-) create mode 100644 backend/src/db/migrations/070_imggen_usage.sql 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) +);