feat: SMTP, maintenance mode, blog topic bank UI
8. SMTP: emailService.js (nodemailer), templates (welcome/payment/low_credits) /api/admin/email/test — тест отправки app_settings category=smtp (HOST/PORT/USER/PASS/FROM/ENABLED) 9. Maintenance mode: middleware в index.js, MAINTENANCE_MODE в engine settings При true → 503 для всех запросов кроме /uploads и /api/settings 10. Blog topic bank: DB: blog_topics(category,topic,is_used,source,priority) 40 тем мигрированы из хардкода (source=hardcoded) autogen.js: getNextTopic берёт из DB, fallback на TOPIC_BANK admin API: GET/POST /blog-topics, DELETE /:id, POST /generate (AI +10)
This commit is contained in:
+22
-21
@@ -59,7 +59,7 @@ const TOPIC_BANK = {
|
||||
* Берёт следующую тему из очереди или из банка тем.
|
||||
*/
|
||||
async function getNextTopic(category) {
|
||||
// Сначала из очереди (по приоритету)
|
||||
// 1. Приоритетная очередь (content_queue)
|
||||
const { rows } = await query(
|
||||
`SELECT * FROM content_queue
|
||||
WHERE category=$1 AND status='pending'
|
||||
@@ -69,31 +69,32 @@ async function getNextTopic(category) {
|
||||
if (rows.length) {
|
||||
return { id: rows[0].id, topic: rows[0].topic, tags: rows[0].tags || [], keywords: rows[0].keywords || [] };
|
||||
}
|
||||
// Из банка — темы которые ещё не использовались
|
||||
const bank = TOPIC_BANK[category] || TOPIC_BANK['ai-tools'];
|
||||
|
||||
// Получаем уже использованные темы по source_topic (точное совпадение)
|
||||
// 2. DB-банк тем — неиспользованные
|
||||
const { rows: dbTopics } = await query(`
|
||||
SELECT bt.id, bt.topic FROM blog_topics bt
|
||||
WHERE bt.category = $1
|
||||
AND bt.is_used = false
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM articles a
|
||||
WHERE a.source_topic = bt.topic AND a.category = $1
|
||||
)
|
||||
ORDER BY bt.priority DESC, bt.created_at ASC
|
||||
LIMIT 1
|
||||
`, [category]);
|
||||
|
||||
if (dbTopics.length) {
|
||||
return { id: null, topic: dbTopics[0].topic, tags: [], keywords: [], blog_topic_id: dbTopics[0].id };
|
||||
}
|
||||
|
||||
// 3. Fallback: хардкод если DB пустой
|
||||
const bank = TOPIC_BANK[category] || TOPIC_BANK['ai-tools'];
|
||||
const { rows: usedTopics } = await query(
|
||||
`SELECT source_topic FROM articles WHERE category=$1 AND source_topic IS NOT NULL`,
|
||||
[category]
|
||||
);
|
||||
const usedSet = new Set(usedTopics.map(r => r.source_topic.toLowerCase().trim()));
|
||||
|
||||
// Также проверяем по заголовкам (fallback для старых статей без source_topic)
|
||||
const { rows: usedTitles } = await query(
|
||||
`SELECT title FROM articles WHERE category=$1 AND source_topic IS NULL AND status='published'`,
|
||||
[category]
|
||||
);
|
||||
const titlesLower = usedTitles.map(r => r.title.toLowerCase());
|
||||
|
||||
const unused = bank.filter(t => {
|
||||
const tLow = t.toLowerCase().trim();
|
||||
if (usedSet.has(tLow)) return false;
|
||||
// Fallback: проверяем по первым 30 символам заголовка
|
||||
if (titlesLower.some(title => title.includes(tLow.slice(0, 30)))) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
const usedSet = new Set(usedTopics.map(r => r.source_topic?.toLowerCase().trim()).filter(Boolean));
|
||||
const unused = bank.filter(t => !usedSet.has(t.toLowerCase().trim()));
|
||||
const pool = unused.length > 0 ? unused : bank;
|
||||
const topic = pool[Math.floor(Math.random() * pool.length)];
|
||||
return { id: null, topic, tags: [], keywords: [] };
|
||||
|
||||
Reference in New Issue
Block a user