diff --git a/src/app.js b/src/app.js index 758b864..c174b3f 100644 --- a/src/app.js +++ b/src/app.js @@ -13,6 +13,7 @@ app.use( 'http://localhost:4173', 'https://www.dayframe.ru', 'https://dayframe.ru', + 'https://dayframe.na4u.ru', ], methods: 'GET,POST,PATCH,DELETE', allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With', 'Accept', 'Origin'], diff --git a/src/entities/bot/bot.controller.js b/src/entities/bot/bot.controller.js index 2b56e62..81775ec 100644 --- a/src/entities/bot/bot.controller.js +++ b/src/entities/bot/bot.controller.js @@ -1,6 +1,23 @@ -const botService = require("./bot.service"); +const botService = require('./bot.service'); class BotController { + /** + * Get tasks for a specific date by chat_id + * Route: GET /bot/tasks/:chat_id?date=YYYY-MM-DD + * If date is not provided, returns tasks for today + */ + getTasksForDateByChatId(req, res) { + const chat_id = req.params.chat_id; + const date = req.query.date || null; // Optional date parameter + + botService.getTasksForDateByChatId(chat_id, date).then((result) => { + res.status(result.status).json(result.data); + }); + } + + /** + * @deprecated Use getTasksForDateByChatId instead. This method is kept for backward compatibility. + */ getTasksForTodayByChatId(req, res) { const chat_id = req.params.chat_id; botService.getTasksForTodayByChatId(chat_id).then((result) => { diff --git a/src/entities/bot/bot.routes.js b/src/entities/bot/bot.routes.js index 1328960..0ab3aa8 100644 --- a/src/entities/bot/bot.routes.js +++ b/src/entities/bot/bot.routes.js @@ -2,7 +2,13 @@ const express = require('express'); const router = express.Router(); const botController = require('./bot.controller'); -// Получить задачи на сегодня по chat_id +// Получить задачи на конкретную дату по chat_id +// Route: GET /bot/tasks/:chat_id?date=YYYY-MM-DD +// If date is not provided, returns tasks for today +//TODO:: authenticate придумать для бота +router.get('/tasks/:chat_id', (req, res) => botController.getTasksForDateByChatId(req, res)); + +// Deprecated: старый роут для обратной совместимости //TODO:: authenticate придумать для бота router.get('/today/:chat_id', (req, res) => botController.getTasksForTodayByChatId(req, res)); diff --git a/src/entities/bot/bot.service.js b/src/entities/bot/bot.service.js index 6d46a66..6cf5103 100644 --- a/src/entities/bot/bot.service.js +++ b/src/entities/bot/bot.service.js @@ -1,22 +1,68 @@ const userService = require('../user/user.service'); const taskService = require('../task/task.service'); +const templateModel = require('../template.task/models/template.task.model'); +const { getDateStringInTZ } = require('../../utils/date'); + /** * Bot service class with business logic for bot operations */ class BotService { - async getTasksForTodayByChatId(chat_id) { + /** + * Get tasks for a specific date by chat_id (optimized with subtasks) + * Returns format compatible with old API: { tasks, templates, dateString, dayOfWeek } + * @param {string} chat_id - Telegram chat ID + * @param {string} date - Optional date in YYYY-MM-DD format. If not provided, uses today in user's timezone + * @returns {Promise<{status: number, data: any}>} + */ + async getTasksForDateByChatId(chat_id, date = null) { try { const user = await userService.getUserByChatId(chat_id); if (!user) { return { status: 404, data: { error: 'Пользователь не найден' } }; } - const result = await taskService.getTasksForToday(user.id, user.timezone); - return { status: result.status, data: result.data }; + + const timezone = user.timezone || 'Europe/Moscow'; + + // If date is not provided, use today in user's timezone + const taskDate = date || getDateStringInTZ(timezone); + const dateString = taskDate; + + // Get tasks with subtasks (optimized - 2 queries) + const tasksResult = await taskService.getTasksForDateWithSubTasks(user.id, taskDate); + if (tasksResult.status !== 200) { + return tasksResult; + } + + // Calculate day of week for templates + // Parse the date to get day of week + const dateObj = new Date(taskDate + 'T12:00:00'); // Use noon to avoid timezone issues + const jsDay = dateObj.getDay(); + const dayOfWeek = jsDay === 0 ? 7 : jsDay; // Convert Sunday (0) to 7 + + // Get templates for this day of week + const [templates] = await templateModel.getTemplatesForDay(user.id, dayOfWeek); + + return { + status: 200, + data: { + tasks: tasksResult.data, + templates: templates || [], + dateString, + dayOfWeek, + }, + }; } catch (err) { - console.error("❌ Ошибка при получении задач на сегодня:", err); + console.error('❌ Ошибка при получении задач на дату:', err); return { status: 500, data: { error: err.message } }; } } + + /** + * @deprecated Use getTasksForDateByChatId instead. This method is kept for backward compatibility. + */ + async getTasksForTodayByChatId(chat_id) { + return this.getTasksForDateByChatId(chat_id); + } } // Create a singleton instance diff --git a/src/entities/task/models/subtask.model.js b/src/entities/task/models/subtask.model.js index a5ebe35..591ea7c 100644 --- a/src/entities/task/models/subtask.model.js +++ b/src/entities/task/models/subtask.model.js @@ -5,20 +5,22 @@ function getSubtasksAllByUserId(user_id) { return db.query(query, [user_id]); } +function getAllSubtasksByUserId(user_id) { + return db + .query('SELECT * FROM subtasks WHERE user_id = ?', [user_id]) + .then(([subtasks]) => [subtasks]) + .catch((error) => { + console.error('Ошибка при получении подзадач:', error); + throw error; + }); +} + function getAllSubtasksByParentTaskId(parent_task_id) { - return db.query('SELECT * FROM subtasks WHERE parent_task_id = ?', [ - parent_task_id, - ]); + return db.query('SELECT * FROM subtasks WHERE parent_task_id = ?', [parent_task_id]); } function addSubtask(user_id, subtask) { - const { - parent_task_id, - title, - is_done = false, - position = 0, - special_id, - } = subtask; + const { parent_task_id, title, is_done = false, position = 0, special_id } = subtask; if (!special_id) { throw new Error('special_id is required for subtasks'); @@ -28,14 +30,7 @@ function addSubtask(user_id, subtask) { INSERT INTO subtasks (parent_task_id, title, is_done, position, special_id, user_id, created_at) VALUES (?, ?, ?, ?, ?, ?, NOW()) `; - return db.query(query, [ - parent_task_id, - title, - is_done, - position, - special_id, - user_id, - ]); + return db.query(query, [parent_task_id, title, is_done, position, special_id, user_id]); } function getSubtaskById(id) { @@ -47,10 +42,7 @@ function deleteSubtaskById(id) { } function updateSubtaskStatus(id, is_done) { - return db.query('UPDATE subtasks SET is_done = ? WHERE id = ?', [ - is_done, - id, - ]); + return db.query('UPDATE subtasks SET is_done = ? WHERE id = ?', [is_done, id]); } function updateSubtasksStatusByParentTaskId(parent_task_id, is_done) { @@ -88,6 +80,7 @@ function countCompletedSubtaskByParentTaskId(parent_task_id) { module.exports = { getSubtasksAllByUserId, + getAllSubtasksByUserId, getAllSubtasksByParentTaskId, addSubtask, getSubtaskById, diff --git a/src/entities/task/task.controller.js b/src/entities/task/task.controller.js index 27968e9..018bac0 100644 --- a/src/entities/task/task.controller.js +++ b/src/entities/task/task.controller.js @@ -6,6 +6,7 @@ const taskService = require('./task.service'); class TaskController { /** * Get all tasks with their subtasks for a user + * @deprecated Use getAllTasksOptimized instead. This method uses inefficient N+1 queries. */ getTasksWithSubTasks(req, res) { const userId = Number(req.user.id); @@ -18,6 +19,38 @@ class TaskController { }); } + /** + * Optimized version: Get all tasks with subtasks using optimized query + * Includes timeout handling to prevent 502 errors + */ + getAllTasksOptimized(req, res) { + const userId = Number(req.user.id); + if (!userId) { + return res.status(400).json({ error: 'Не передан user-id в заголовке' }); + } + + // Add timeout of 25 seconds (less than 30 seconds at Express level) + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Request timeout')), 25000) + ); + + Promise.race([taskService.getAllTasksWithSubTasksOptimized(userId), timeoutPromise]) + .then((result) => { + if (res.headersSent) return; // Check if response already sent + res.status(result.status).json(result.data); + }) + .catch((err) => { + if (res.headersSent) return; // Check if response already sent + console.error('❌ Ошибка при получении задач:', err); + res.status(500).json({ + error: + err.message === 'Request timeout' + ? 'Превышено время ожидания ответа от сервера' + : 'Ошибка при получении задач', + }); + }); + } + /** * Create a new task */ diff --git a/src/entities/task/task.routes.js b/src/entities/task/task.routes.js index 79f3737..f44e1e8 100644 --- a/src/entities/task/task.routes.js +++ b/src/entities/task/task.routes.js @@ -5,8 +5,8 @@ const authorizeTask = require('../../middleware/authorizeTask'); const authorizeSubTask = require('../../middleware/authorizeSubTask'); const authenticate = require('../../middleware/authenticate'); -// Получить все задачи по id пользователя -router.get('/', authenticate, (req, res) => taskController.getTasksWithSubTasks(req, res)); +// Получить все задачи по id пользователя (оптимизированная версия) +router.get('/', authenticate, (req, res) => taskController.getAllTasksOptimized(req, res)); // Получить задачи за период router.get('/period', authenticate, (req, res) => taskController.getTasksForPeriod(req, res)); diff --git a/src/entities/task/task.service.js b/src/entities/task/task.service.js index 2558d07..d86f560 100644 --- a/src/entities/task/task.service.js +++ b/src/entities/task/task.service.js @@ -36,6 +36,7 @@ class TaskService { /** * Get all tasks with subtasks for a user + * @deprecated Use getAllTasksWithSubTasksOptimized instead. This method uses inefficient N+1 queries (1 + 2N queries). */ async getTasksWithSubTasks(userId) { try { @@ -65,6 +66,47 @@ class TaskService { } } + /** + * Optimized version: Get all tasks with subtasks using only 2 SQL queries + * Instead of 1 + 2N queries (where N is number of tasks) + */ + async getAllTasksWithSubTasksOptimized(userId) { + try { + // Get all tasks and all subtasks in parallel (2 queries total) + const [[tasks], [subtasks]] = await Promise.all([ + taskModel.getAllTasksByUser(userId), + subtaskModel.getAllSubtasksByUserId(userId), + ]); + + if (!tasks.length) { + return { status: 404, data: { error: 'Задачи не найдены' } }; + } + + // Group subtasks by parent_task_id for O(1) lookup + const subtasksByTaskId = new Map(); + if (subtasks && subtasks.length > 0) { + for (const subtask of subtasks) { + const taskId = subtask.parent_task_id; + if (!subtasksByTaskId.has(taskId)) { + subtasksByTaskId.set(taskId, []); + } + subtasksByTaskId.get(taskId).push(subtask); + } + } + + // Combine tasks with their subtasks + const fullTasks = tasks.map((task) => ({ + ...task, + subtasks: subtasksByTaskId.get(task.id) || [], + })); + + return { status: 200, data: fullTasks }; + } catch (err) { + console.error('❌ Ошибка при получении задач с подзадачами (оптимизированная версия):', err); + return { status: 500, data: { error: err.message } }; + } + } + /** * Create a new task */ @@ -380,6 +422,53 @@ class TaskService { return { status: 500, data: { error: err.message } }; } } + + /** + * Optimized version: Get tasks for a specific date with subtasks using only 2 SQL queries + * @param {number} userId - User ID + * @param {string} taskDate - Date in YYYY-MM-DD format + * @returns {Promise<{status: number, data: any}>} + */ + async getTasksForDateWithSubTasks(userId, taskDate) { + try { + // Get all tasks for the date and all subtasks for the user in parallel (2 queries total) + const [[tasks], [subtasks]] = await Promise.all([ + taskModel.getTasksForDate(userId, taskDate), + subtaskModel.getAllSubtasksByUserId(userId), + ]); + + if (!tasks.length) { + return { status: 404, data: { error: 'Задачи не найдены' } }; + } + + // Filter subtasks to only include those for tasks on this date + const taskIds = new Set(tasks.map((task) => task.id)); + const relevantSubtasks = (subtasks || []).filter((subtask) => + taskIds.has(subtask.parent_task_id) + ); + + // Group subtasks by parent_task_id for O(1) lookup + const subtasksByTaskId = new Map(); + for (const subtask of relevantSubtasks) { + const taskId = subtask.parent_task_id; + if (!subtasksByTaskId.has(taskId)) { + subtasksByTaskId.set(taskId, []); + } + subtasksByTaskId.get(taskId).push(subtask); + } + + // Combine tasks with their subtasks + const fullTasks = tasks.map((task) => ({ + ...task, + subtasks: subtasksByTaskId.get(task.id) || [], + })); + + return { status: 200, data: fullTasks }; + } catch (err) { + console.error('❌ Ошибка при получении задач на дату с подзадачами:', err); + return { status: 500, data: { error: err.message } }; + } + } } // Create a singleton instance diff --git a/src/entities/template.task/template.task.controller.js b/src/entities/template.task/template.task.controller.js index 1695285..2dea2c0 100644 --- a/src/entities/template.task/template.task.controller.js +++ b/src/entities/template.task/template.task.controller.js @@ -7,27 +7,55 @@ class TemplateTaskController { return res.status(400).json({ error: 'Не передан user-id в заголовке' }); } - templateTaskService.getTemplateTasksWithSubTasks(userId).then((result) => { - res.status(result.status).json(result.data); - }); + // Добавляем общий таймаут 25 секунд (меньше чем 30 секунд на уровне Express) + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Request timeout')), 25000) + ); + + Promise.race([templateTaskService.getTemplateTasksWithSubTasks(userId), timeoutPromise]) + .then((result) => { + if (res.headersSent) return; // Проверяем, не отправлен ли уже ответ + res.status(result.status).json(result.data); + }) + .catch((err) => { + if (res.headersSent) return; // Проверяем, не отправлен ли уже ответ + console.error('❌ Ошибка при получении шаблонов задач:', err); + res.status(500).json({ + error: + err.message === 'Request timeout' + ? 'Превышено время ожидания ответа от сервера' + : 'Ошибка при получении шаблонов задач', + }); + }); } createTemplateTask(req, res) { const userId = Number(req.user.id); - if (!userId) - return res.status(400).json({ error: 'Не передан user-id в заголовке' }); + if (!userId) return res.status(400).json({ error: 'Не передан user-id в заголовке' }); - templateTaskService.createTemplateTask(userId, req.body).then((result) => { - res.status(result.status).json(result.data); - }); + templateTaskService + .createTemplateTask(userId, req.body) + .then((result) => { + res.status(result.status).json(result.data); + }) + .catch((err) => { + console.error('❌ Ошибка при создании шаблона задачи:', err); + res.status(500).json({ error: 'Ошибка при создании шаблона задачи' }); + }); } deleteTemplateTask(req, res) { const taskId = Number(req.params.id); - templateTaskService.deleteTemplateTask(taskId).then((result) => { - res.status(result.status).json(result.data); - }); + templateTaskService + .deleteTemplateTask(taskId) + .then((result) => { + res.status(result.status).json(result.data); + }) + .catch((err) => { + console.error('❌ Ошибка при удалении шаблона задачи:', err); + res.status(500).json({ error: 'Ошибка при удалении шаблона задачи' }); + }); } async updateTemplateTask(req, res) { diff --git a/src/entities/template.task/template.task.service.js b/src/entities/template.task/template.task.service.js index f0b7a14..7cd1f02 100644 --- a/src/entities/template.task/template.task.service.js +++ b/src/entities/template.task/template.task.service.js @@ -12,10 +12,9 @@ class TemplateTaskService { data: { error: 'Не удалось получить шаблон задачи' }, }; - const [subtasks] = - await templateSubtaskModel.getAllTemplateSubtasksByParentTemplateTaskId( - taskId, - ); + const [subtasks] = await templateSubtaskModel.getAllTemplateSubtasksByParentTemplateTaskId( + taskId + ); return { ...task, subtasks: subtasks || [], @@ -33,24 +32,32 @@ class TemplateTaskService { return { status: 404, data: { error: 'Шаблоны задач не найдены' } }; } - const fullTasks = []; - for (const task of rows) { + // Используем Promise.allSettled для параллельной обработки с таймаутом + const taskPromises = rows.map(async (task) => { try { - const fullTask = await this.getFullTemplateTaskById(task.id); - if (fullTask) { - fullTasks.push(fullTask); - } + // Добавляем таймаут 5 секунд на каждый запрос + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error('Timeout')), 5000) + ); + + const fullTaskPromise = this.getFullTemplateTaskById(task.id); + const fullTask = await Promise.race([fullTaskPromise, timeoutPromise]); + + return fullTask; } catch (err) { - console.error(`Ошибка при получении шаблона задачи ${task.id}:`, err); + console.error(`Ошибка при получении шаблона задачи ${task.id}:`, err.message || err); + return null; } - } + }); + + const results = await Promise.allSettled(taskPromises); + const fullTasks = results + .map((result) => (result.status === 'fulfilled' ? result.value : null)) + .filter((task) => task !== null); return { status: 200, data: fullTasks }; } catch (err) { - console.error( - '❌ Ошибка при получении шаблонов задач с подзадачами:', - err, - ); + console.error('❌ Ошибка при получении шаблонов задач с подзадачами:', err); return { status: 500, data: { error: err.message } }; } } @@ -72,7 +79,7 @@ class TemplateTaskService { templateSubtaskModel.addTemplateSubtask(userId, { ...sub, template_task_id: taskId, - }), + }) ); } } @@ -138,7 +145,7 @@ class TemplateTaskService { templateSubtaskModel.addTemplateSubtask(updatedTask.user_id, { ...sub, template_task_id: taskId, - }), + }) ); // удаление подзадачи @@ -148,11 +155,7 @@ class TemplateTaskService { // обновление существующей подзадачи } else if (sub.id) { promises.push( - templateSubtaskModel.updateTemplateSubtask( - sub.id, - sub.title, - sub.position, - ), + templateSubtaskModel.updateTemplateSubtask(sub.id, sub.title, sub.position) ); } }