bdff84e579
Зачем: автоген генерит несколько статей подряд (4 категории за 15 минут).
Раньше у всех published_at=NOW(), и на сайте они появлялись скопом, выглядя
как спам. Теперь распределяем по слотам сайта (по умолчанию 09/13/17/21 МСК).
Архитектура:
src/services/dripScheduler.js — nextDripSlot() читает app_settings.SITE_PUBLISH_SLOTS
(CSV 'HH:MM,HH:MM,...'), default '09:00,13:00,17:00,21:00'. Перебирает дни
вперёд (14 дней горизонт), для каждого слота проверяет ±60 мин окно —
если уже есть опубликованная статья, считает занятым. Первый свободный
слот возвращается. Slots в MSK, конвертируются в UTC для сравнения с NOW().
При approve draft → published:
routes/drafts.js — PATCH /:id/approve по умолчанию ставит slot, ?immediate=true
публикует немедленно. GET /next-slot возвращает ближайший свободный для
UI-предпросмотра.
draftAutoApprove.js — авто-approve в 07:00 МСК тоже использует slot.
Публичные SQL дополнены 'AND published_at <= NOW()' чтобы будущие статьи
не светились на сайте раньше времени:
- services/articles.js: 7 мест (list/get/count/hero/grid/popular/recent)
- routes/categories.js: GET /:slug/articles
- routes/scheduledPosts.js: TG-publish source query
Опционально: SITE_PUBLISH_SLOTS можно редактировать в /admin/settings.
60 lines
2.1 KiB
JavaScript
60 lines
2.1 KiB
JavaScript
// draftAutoApprove.js
|
|
// Каждый день в 07:00 МСК переводит все статьи status='draft' → 'published'
|
|
// и ставит их на ближайшие publish_slots канала 1.
|
|
|
|
const { query } = require('./src/config/db');
|
|
const { scheduleForArticle } = require('./src/services/articleAutoPublish');
|
|
const { nextDripSlot } = require('./src/services/dripScheduler');
|
|
|
|
const AUTO_APPROVE_HOUR_MSK = 7;
|
|
let lastRunDate = null;
|
|
|
|
async function runDraftAutoApprove() {
|
|
try {
|
|
const { rows: drafts } = await query(
|
|
`SELECT id, title, category FROM articles WHERE status='draft' ORDER BY created_at ASC`
|
|
);
|
|
|
|
if (!drafts.length) {
|
|
console.log('[DraftApprove] no drafts to approve');
|
|
return;
|
|
}
|
|
|
|
console.log(`[DraftApprove] approving ${drafts.length} drafts`);
|
|
|
|
for (const draft of drafts) {
|
|
const slot = await nextDripSlot();
|
|
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] slot ${mskLabel} for article=${draft.id}`);
|
|
console.log(`[DraftApprove] approved article=${draft.id} "${draft.title.slice(0, 50)}"`);
|
|
}
|
|
} 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 };
|