feat: post drafts system — batch generation + daily auto-drafts

DB: post_drafts(channel_id, topic, text, image_url, status), channels.auto_draft_*
Engine:
  services/draftService.js: generateOneDraft, generateBatch, generateDailyDrafts,
    approveDraft(→scheduled_post), rejectDraft, updateDraft, listDrafts
  routes/drafts.js: GET/PATCH/DELETE /api/drafts/:id, /approve, /reject
    POST /api/channels/:channelId/drafts/generate?count=N (async, returns immediately)
  index.js: cron каждые 30 мин → generateDailyDrafts() для каналов с auto_draft_enabled
  channels.js: updateChannel сохраняет auto_draft_enabled/count/time
This commit is contained in:
Ник (Claude)
2026-06-12 23:47:27 +03:00
parent 5a765d27e1
commit a8ff295faa
4 changed files with 340 additions and 2 deletions
+101
View File
@@ -0,0 +1,101 @@
/**
* drafts.js — API для черновиков постов.
*
* POST /api/channels/:channelId/drafts/generate?count=3 — batch генерация
* GET /api/drafts — все черновики юзера
* GET /api/drafts/:channelId/channel — черновики канала
* PATCH /api/drafts/:id — редактировать текст
* POST /api/drafts/:id/approve — одобрить → scheduled_post
* POST /api/drafts/:id/reject — отклонить
* DELETE /api/drafts/:id — удалить
*/
const express = require('express');
const router = express.Router();
const { query } = require('../config/db');
const channelsSvc = require('../services/channels');
const draftSvc = require('../services/draftService');
function uid(req) { return req.headers['x-user-id'] ? parseInt(req.headers['x-user-id']) : null; }
// POST /api/channels/:channelId/drafts/generate
router.post('/channels/:channelId/drafts/generate', async (req, res) => {
const userId = uid(req);
if (!userId) return res.status(401).json({ error: 'x-user-id required' });
const count = Math.min(parseInt(req.query.count || req.body.count || 3), 10);
const withImage = req.body.withImage !== false;
try {
const channel = await channelsSvc.getFullChannel(req.params.channelId);
if (!channel) return res.status(404).json({ error: 'Channel not found' });
// Запускаем асинхронно — отвечаем сразу
res.json({ ok: true, message: `Генерирую ${count} черновиков...`, count });
draftSvc.generateBatch(channel, { count, userId, withImage })
.then(r => console.log(`[drafts] batch done ch=${channel.id}: ${r.generated} ok, ${r.errors.length} err`))
.catch(e => console.error(`[drafts] batch error: ${e.message}`));
} catch (err) {
if (!res.headersSent) res.status(500).json({ error: err.message });
}
});
// GET /api/drafts — все черновики текущего пользователя
router.get('/drafts', async (req, res) => {
const userId = uid(req);
if (!userId) return res.status(401).json({ error: 'x-user-id required' });
const { status = 'pending', limit = 30, offset = 0 } = req.query;
try {
const result = await draftSvc.listDrafts({ userId, status, limit: parseInt(limit), offset: parseInt(offset) });
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// GET /api/drafts/:channelId/channel — черновики конкретного канала
router.get('/drafts/:channelId/channel', async (req, res) => {
const userId = uid(req);
if (!userId) return res.status(401).json({ error: 'x-user-id required' });
const { status = 'pending', limit = 30, offset = 0 } = req.query;
try {
const result = await draftSvc.listDrafts({ channelId: req.params.channelId, status, limit: parseInt(limit), offset: parseInt(offset) });
res.json(result);
} catch (err) { res.status(500).json({ error: err.message }); }
});
// PATCH /api/drafts/:id — редактировать
router.patch('/drafts/:id', async (req, res) => {
try {
await draftSvc.updateDraft(req.params.id, req.body);
res.json({ ok: true });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// POST /api/drafts/:id/approve
router.post('/drafts/:id/approve', async (req, res) => {
const userId = uid(req);
try {
const result = await draftSvc.approveDraft(req.params.id, {
scheduledAt: req.body.scheduled_at,
userId,
});
res.json({ ok: true, ...result });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// POST /api/drafts/:id/reject
router.post('/drafts/:id/reject', async (req, res) => {
try {
await draftSvc.rejectDraft(req.params.id);
res.json({ ok: true });
} catch (err) { res.status(500).json({ error: err.message }); }
});
// DELETE /api/drafts/:id
router.delete('/drafts/:id', async (req, res) => {
try {
await query('DELETE FROM post_drafts WHERE id=$1', [req.params.id]);
res.json({ ok: true });
} catch (err) { res.status(500).json({ error: err.message }); }
});
module.exports = router;