// draftAutoApprove.js // Каждый день в 07:00 МСК переводит черновики вчерашней генерации → 'published' // и ставит их в слоты ТЕКУЩЕГО дня (08:11/12:11/16:11/20:11). // // Механика (как у заметок Зеро): // 17:00 — автогенерация создаёт 4 черновика (по 1 на категорию) // 07:00 след.дня — авто-одобрение: берём только черновики созданные ВЧЕРА, // раскладываем по слотам сегодняшнего дня по порядку // В любой момент — редактор может одобрить черновик вручную (кнопка в /admin/drafts) // тогда статья получает ближайший свободный слот сегодня const { query } = require('./src/config/db'); const { scheduleForArticle } = require('./src/services/articleAutoPublish'); const AUTO_APPROVE_HOUR_MSK = 7; let lastRunDate = null; /** * Возвращает слоты сегодняшнего дня в UTC (из SITE_PUBLISH_SLOTS настройки). * Слоты заданы в МСК (UTC+3). */ async function getTodaySlots() { const { rows } = await query( `SELECT value FROM app_settings WHERE key='SITE_PUBLISH_SLOTS' LIMIT 1` ); const raw = rows[0]?.value || '08:11,12:11,16:11,20:11'; const parts = raw.split(',').map(s => s.trim()).filter(Boolean); const now = new Date(); // Сегодня в МСК const mskNow = new Date(now.getTime() + 3 * 60 * 60 * 1000); const y = mskNow.getUTCFullYear(); const m = mskNow.getUTCMonth(); const d = mskNow.getUTCDate(); return parts.map(p => { const [h, min] = p.split(':').map(Number); // Конвертируем МСК → UTC (MSK = UTC+3) const slot = new Date(Date.UTC(y, m, d, h - 3, min, 0, 0)); return slot; }).sort((a, b) => a - b); } async function runDraftAutoApprove() { try { // Берём только черновики созданные ВЧЕРА (между 00:00 и 23:59 вчера МСК) // Это именно те, что сгенерировались накануне и ждут одобрения const { rows: drafts } = await query( `SELECT id, title, category, created_at FROM articles WHERE status='draft' AND created_at >= NOW() - INTERVAL '36 hours' ORDER BY created_at ASC` ); if (!drafts.length) { console.log('[DraftApprove] нет черновиков для авто-одобрения'); return; } console.log(`[DraftApprove] авто-одобряем ${drafts.length} черновиков → слоты сегодняшнего дня`); // Получаем слоты сегодняшнего дня const todaySlots = await getTodaySlots(); const now = new Date(); // Проверяем какие слоты уже заняты (pending или published сегодня) const { rows: takenRows } = await query( `SELECT scheduled_at FROM scheduled_posts WHERE status IN ('pending','sent','sending') AND scheduled_at >= NOW()::date AND scheduled_at < (NOW()::date + INTERVAL '1 day')`, ); const takenTimes = new Set(takenRows.map(r => new Date(r.scheduled_at).getTime())); // Ищем свободные слоты (только в будущем и не занятые) const freeSlots = todaySlots.filter(s => s > now && !takenTimes.has(s.getTime())); if (freeSlots.length === 0) { console.log('[DraftApprove] все сегодняшние слоты заняты — черновики остаются'); return; } // Раскладываем черновики по свободным слотам (1 к 1) for (let i = 0; i < drafts.length; i++) { const draft = drafts[i]; const slot = freeSlots[i] || freeSlots[freeSlots.length - 1]; // если черновиков больше слотов — ставим в последний await query( `UPDATE articles SET status='published', published_at=$2 WHERE id=$1`, [draft.id, slot] ); await scheduleForArticle(draft.id); const mskLabel = slot.toLocaleString('ru-RU', { timeZone: 'Europe/Moscow', day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit', }); console.log(`[DraftApprove] article=${draft.id} "${draft.title.slice(0, 50)}" → ${mskLabel} МСК`); } } catch (err) { console.error('[DraftApprove] error:', err.message); } } function startDraftAutoApproveScheduler() { console.log('[DraftApprove] scheduler started (auto-approve at 07:00 MSK, только вчерашние черновики)'); setInterval(() => { const now = new Date(); const msk = new Date(now.getTime() + 3 * 60 * 60 * 1000); const hour = msk.getUTCHours(); const minute = msk.getUTCMinutes(); const dateStr = msk.toISOString().slice(0, 10); if (hour === AUTO_APPROVE_HOUR_MSK && minute === 0 && lastRunDate !== dateStr) { lastRunDate = dateStr; console.log(`[DraftApprove] triggered at ${msk.toISOString()}`); runDraftAutoApprove(); } }, 60_000); } module.exports = { startDraftAutoApproveScheduler, runDraftAutoApprove };