fix(drafts): авто-одобрение только вчерашних черновиков → слоты сегодняшнего дня
Было: runDraftAutoApprove брал ВСЕ черновики подряд и вызывал nextDripSlot() для каждого — слоты набивались на несколько дней вперёд. Теперь: - берём только черновики созданные за последние 36 часов (вчерашние) - слоты — только сегодняшний день (getTodaySlots из SITE_PUBLISH_SLOTS) - занятые слоты пропускаем, не затираем уже запланированное - если слотов меньше черновиков — ставим оставшихся в последний слот Механика как у заметок Зеро: 17:00 → генерация 4 черновиков (лежат в /admin/drafts, можно редактировать) 07:00 след.дня → авто-одобрение, слоты сегодняшнего дня (08:11/12:11/16:11/20:11) Ручное одобрение → nextDripSlot (ближайший свободный)
This commit is contained in:
+77
-12
@@ -1,37 +1,102 @@
|
|||||||
// draftAutoApprove.js
|
// draftAutoApprove.js
|
||||||
// Каждый день в 07:00 МСК переводит все статьи status='draft' → 'published'
|
// Каждый день в 07:00 МСК переводит черновики вчерашней генерации → 'published'
|
||||||
// и ставит их на ближайшие publish_slots канала 1.
|
// и ставит их в слоты ТЕКУЩЕГО дня (08:11/12:11/16:11/20:11).
|
||||||
|
//
|
||||||
|
// Механика (как у заметок Зеро):
|
||||||
|
// 17:00 — автогенерация создаёт 4 черновика (по 1 на категорию)
|
||||||
|
// 07:00 след.дня — авто-одобрение: берём только черновики созданные ВЧЕРА,
|
||||||
|
// раскладываем по слотам сегодняшнего дня по порядку
|
||||||
|
// В любой момент — редактор может одобрить черновик вручную (кнопка в /admin/drafts)
|
||||||
|
// тогда статья получает ближайший свободный слот сегодня
|
||||||
|
|
||||||
const { query } = require('./src/config/db');
|
const { query } = require('./src/config/db');
|
||||||
const { scheduleForArticle } = require('./src/services/articleAutoPublish');
|
const { scheduleForArticle } = require('./src/services/articleAutoPublish');
|
||||||
const { nextDripSlot } = require('./src/services/dripScheduler');
|
|
||||||
|
|
||||||
const AUTO_APPROVE_HOUR_MSK = 7;
|
const AUTO_APPROVE_HOUR_MSK = 7;
|
||||||
let lastRunDate = null;
|
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() {
|
async function runDraftAutoApprove() {
|
||||||
try {
|
try {
|
||||||
|
// Берём только черновики созданные ВЧЕРА (между 00:00 и 23:59 вчера МСК)
|
||||||
|
// Это именно те, что сгенерировались накануне и ждут одобрения
|
||||||
const { rows: drafts } = await query(
|
const { rows: drafts } = await query(
|
||||||
`SELECT id, title, category FROM articles WHERE status='draft' ORDER BY created_at ASC`
|
`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) {
|
if (!drafts.length) {
|
||||||
console.log('[DraftApprove] no drafts to approve');
|
console.log('[DraftApprove] нет черновиков для авто-одобрения');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`[DraftApprove] approving ${drafts.length} drafts`);
|
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]; // если черновиков больше слотов — ставим в последний
|
||||||
|
|
||||||
for (const draft of drafts) {
|
|
||||||
const slot = await nextDripSlot();
|
|
||||||
await query(
|
await query(
|
||||||
`UPDATE articles SET status='published', published_at=$2 WHERE id=$1`,
|
`UPDATE articles SET status='published', published_at=$2 WHERE id=$1`,
|
||||||
[draft.id, slot]
|
[draft.id, slot]
|
||||||
);
|
);
|
||||||
await scheduleForArticle(draft.id);
|
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}`);
|
const mskLabel = slot.toLocaleString('ru-RU', {
|
||||||
console.log(`[DraftApprove] approved article=${draft.id} "${draft.title.slice(0, 50)}"`);
|
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) {
|
} catch (err) {
|
||||||
console.error('[DraftApprove] error:', err.message);
|
console.error('[DraftApprove] error:', err.message);
|
||||||
@@ -39,7 +104,7 @@ async function runDraftAutoApprove() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startDraftAutoApproveScheduler() {
|
function startDraftAutoApproveScheduler() {
|
||||||
console.log('[DraftApprove] scheduler started (auto-approve at 07:00 MSK)');
|
console.log('[DraftApprove] scheduler started (auto-approve at 07:00 MSK, только вчерашние черновики)');
|
||||||
|
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|||||||
Reference in New Issue
Block a user