From 213dc104f5d727ff4cc0a024ff4a1bd454868e7a Mon Sep 17 00:00:00 2001 From: Alexey Pavlov Date: Sun, 31 May 2026 16:45:15 +0300 Subject: [PATCH] feat: autogen run_hour/run_minute, publish_slots, scheduled_posts tables and routes --- src/routes/autogen.js | 10 +++--- src/routes/channels.js | 70 +++++++++++++++++++++++++++++++++++++++++ src/services/autogen.js | 33 +++++++++++++++---- 3 files changed, 103 insertions(+), 10 deletions(-) diff --git a/src/routes/autogen.js b/src/routes/autogen.js index b83fa13..06bd129 100644 --- a/src/routes/autogen.js +++ b/src/routes/autogen.js @@ -26,13 +26,15 @@ router.post('/run', async (req, res) => { // PATCH /api/autogen/settings/:category — обновить настройки router.patch('/settings/:category', async (req, res) => { try { - const { enabled, per_day } = req.body; + const { enabled, per_day, run_hour, run_minute } = req.body; const fields = []; const vals = []; let i = 1; - if (enabled !== undefined) { fields.push(`enabled=$${i++}`); vals.push(enabled); } - if (per_day !== undefined) { fields.push(`per_day=$${i++}`); vals.push(per_day); } + if (enabled !== undefined) { fields.push(`enabled=${i++}`); vals.push(enabled); } + if (per_day !== undefined) { fields.push(`per_day=${i++}`); vals.push(per_day); } + if (run_hour !== undefined) { fields.push(`run_hour=${i++}`); vals.push(run_hour); } + if (run_minute !== undefined) { fields.push(`run_minute=${i++}`); vals.push(run_minute); } if (!fields.length) return res.status(400).json({ error: 'Nothing to update' }); vals.push(req.params.category); - await query(`UPDATE autogen_settings SET ${fields.join(',')} WHERE category=$${i}`, vals); + await query(`UPDATE autogen_settings SET ${fields.join(',')} WHERE category=${i}`, vals); res.json({ ok: true }); } catch (err) { res.status(500).json({ error: err.message }); } }); diff --git a/src/routes/channels.js b/src/routes/channels.js index d5544c2..00fae28 100644 --- a/src/routes/channels.js +++ b/src/routes/channels.js @@ -209,3 +209,73 @@ router.delete('/:id', async (req, res) => { }); module.exports = router; + +// ── Publish slots ───────────────────────────────────────────────────────────── + +// GET /api/channels/admin/:id/slots +router.get('/admin/:id/slots', async (req, res) => { + try { + const { rows } = await query( + `SELECT * FROM publish_slots WHERE channel_id=$1 ORDER BY sort_order, slot_hour, slot_minute`, + [req.params.id] + ); + res.json(rows); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// POST /api/channels/admin/:id/slots +router.post('/admin/:id/slots', async (req, res) => { + try { + const { slot_hour, slot_minute, label, enabled = true } = req.body; + const { rows: existing } = await query( + `SELECT COUNT(*) as cnt FROM publish_slots WHERE channel_id=$1`, [req.params.id] + ); + const sort_order = parseInt(existing[0].cnt); + const { rows } = await query( + `INSERT INTO publish_slots (channel_id, slot_hour, slot_minute, label, enabled, sort_order) + VALUES ($1,$2,$3,$4,$5,$6) + ON CONFLICT (channel_id, slot_hour, slot_minute) DO UPDATE + SET label=$4, enabled=$5 RETURNING *`, + [req.params.id, slot_hour, slot_minute, label || null, enabled, sort_order] + ); + res.json(rows[0]); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// DELETE /api/channels/admin/:id/slots/:slotId +router.delete('/admin/:id/slots/:slotId', async (req, res) => { + try { + await query(`DELETE FROM publish_slots WHERE id=$1 AND channel_id=$2`, + [req.params.slotId, req.params.id]); + res.json({ ok: true }); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// GET /api/channels/admin/:id/scheduled — запланированные посты +router.get('/admin/:id/scheduled', async (req, res) => { + try { + const { rows } = await query( + `SELECT sp.*, a.title as article_title, a.slug as article_slug + FROM scheduled_posts sp + LEFT JOIN articles a ON a.id = sp.article_id + WHERE sp.channel_id=$1 + ORDER BY sp.scheduled_at DESC LIMIT 30`, + [req.params.id] + ); + res.json(rows); + } catch (err) { res.status(500).json({ error: err.message }); } +}); + +// POST /api/channels/admin/:id/schedule — поставить пост в очередь +router.post('/admin/:id/schedule', async (req, res) => { + try { + const { article_id, custom_text, scheduled_at } = req.body; + if (!scheduled_at) return res.status(400).json({ error: 'scheduled_at required' }); + const { rows } = await query( + `INSERT INTO scheduled_posts (channel_id, article_id, custom_text, scheduled_at) + VALUES ($1,$2,$3,$4) RETURNING *`, + [req.params.id, article_id || null, custom_text || null, scheduled_at] + ); + res.json(rows[0]); + } catch (err) { res.status(500).json({ error: err.message }); } +}); diff --git a/src/services/autogen.js b/src/services/autogen.js index c26349b..d8ffeed 100644 --- a/src/services/autogen.js +++ b/src/services/autogen.js @@ -129,15 +129,37 @@ async function runAutogenForCategory(category) { * Основная функция cron — проверяет какие категории нужно генерировать. */ async function runAutogen({ forceCategory = null } = {}) { + let whereClause, params = []; + + if (forceCategory) { + // Ручной запуск — игнорируем время + whereClause = `WHERE enabled=true AND category=$1`; + params = [forceCategory]; + } else { + // Автоматический запуск — проверяем время по расписанию + // Берём текущий час/минуту в московском времени (UTC+3) + const now = new Date(); + const mskOffset = 3 * 60; // UTC+3 + const mskTime = new Date(now.getTime() + mskOffset * 60000); + const currentHour = mskTime.getUTCHours(); + const currentMinute = mskTime.getUTCMinutes(); + + console.log(`[Autogen] Check time MSK ${String(currentHour).padStart(2,'0')}:${String(currentMinute).padStart(2,'0')}`); + + whereClause = `WHERE enabled=true + AND run_hour=$1 + AND run_minute BETWEEN $2 AND $3 + AND (last_run_at IS NULL OR last_run_at < NOW() - INTERVAL '6 hours')`; + params = [currentHour, currentMinute - 5, currentMinute + 5]; + } + const { rows: settings } = await query( - `SELECT * FROM autogen_settings WHERE enabled=true - ${forceCategory ? `AND category=$1` : `AND (next_run_at IS NULL OR next_run_at <= NOW())`} - ORDER BY category`, - forceCategory ? [forceCategory] : [] + `SELECT * FROM autogen_settings ${whereClause} ORDER BY category`, + params ); if (!settings.length) { - console.log('[Autogen] Nothing to generate'); + console.log('[Autogen] Nothing to generate at this time'); return { processed: 0, results: [] }; } @@ -145,7 +167,6 @@ async function runAutogen({ forceCategory = null } = {}) { for (const s of settings) { const result = await runAutogenForCategory(s.category); results.push({ category: s.category, ...result }); - // Пауза между категориями чтобы не перегружать API if (settings.indexOf(s) < settings.length - 1) { await new Promise(r => setTimeout(r, 5000)); }